From 5a8cc991f90b757dee6c9e1d342f0b1073b392e8 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 13 Sep 2026 20:06:15 +0200 Subject: [PATCH 1/4] Record the parameter and type parameter counts of a navigable item Navigate To orders matches that are otherwise equal by these counts for C# and VB. NavigableItem now carries them: every curried and tupled argument of the method a declaration compiles to, without the instance and without a solitary unit argument, and its explicitly declared type parameters. Co-Authored-By: Claude Opus 5 --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Service/ServiceNavigation.fs | 63 ++++++++++++++++--- src/Compiler/Service/ServiceNavigation.fsi | 20 ++++-- ...iler.Service.SurfaceArea.netstandard20.bsl | 6 +- .../FSharp.Compiler.Service.Tests.fsproj | 1 + .../NavigateToTests.fs | 63 +++++++++++++++++++ 6 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index f383908e195..a94b11cfd88 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -169,6 +169,7 @@ * Fix signature generation (`fsc --sig`, `GenerateSignature`, `GetValSignatureText`) dropping the parentheses around a destructured or pattern-annotated tuple parameter when a sibling argument in the same curried group is named, so `(int * float) * z: string` no longer prints as the flat 3-tuple `int * float * z: string`. ([Issue #20397](https://github.com/dotnet/fsharp/issues/20397), [PR #20589](https://github.com/dotnet/fsharp/pull/20589)) ### Added +* `NavigableItem` carries `ParameterCount` and `TypeParameterCount`, the counts C# and VB give Navigate To to order matches that are otherwise equal. * FCS: add FSharpCheckFileResults.FileSignature ([PR #20478](https://github.com/dotnet/fsharp/pull/20478)) * Added the `ReraiseInComputationExpressions` language feature (`--langversion:preview`): `reraise ()` in the `with` handler of a computation expression is compiled to a rethrow through `ExceptionDispatchInfo` instead of being rejected with FS0413. ([Suggestion #660](https://github.com/fsharp/fslang-suggestions/issues/660), [RFC FS-1347](https://github.com/fsharp/fslang-design/pull/843), [PR #20405](https://github.com/dotnet/fsharp/pull/20405)) diff --git a/src/Compiler/Service/ServiceNavigation.fs b/src/Compiler/Service/ServiceNavigation.fs index f42b335da53..c09ab0c188e 100755 --- a/src/Compiler/Service/ServiceNavigation.fs +++ b/src/Compiler/Service/ServiceNavigation.fs @@ -764,10 +764,37 @@ type NavigableItem = IsSignature: bool Kind: NavigableItemKind Container: NavigableContainer + ParameterCount: int + TypeParameterCount: int } [] module NavigateTo = + let private typeParameterCountOf (typars: SynTyparDecls option) = + match typars with + | Some typars -> typars.TyparDecls.Length + | None -> 0 + + let rec private isUnitType synType = + match synType with + | SynType.LongIdent(SynLongIdent([ id ], _, _)) -> id.idText = "unit" + | SynType.Paren(innerType, _) + | SynType.WithGlobalConstraints(innerType, _, _) -> isUnitType innerType + | _ -> false + + /// The parameters of the compiled method: the parser leaves a solitary unit argument of a signature in its arity, + /// where a binding has already dropped it. + let private parameterCountOfSignature (SynValInfo(curriedArgInfos, _)) (synType: SynType) = + match curriedArgInfos, synType with + | [ [ _ ] ], SynType.Fun(argType = argType) when isUnitType argType -> 0 + | [ [ _ ] ], SynType.WithGlobalConstraints(SynType.Fun(argType = argType), _, _) when isUnitType argType -> 0 + | _ -> List.sumBy List.length curriedArgInfos + + let private parameterCountOfBinding (SynValData(memberFlags = memberFlags; valInfo = SynValInfo(curriedArgInfos, _))) = + match memberFlags, curriedArgInfos with + | Some memberFlags, _self :: argInfos when memberFlags.IsInstance -> List.sumBy List.length argInfos + | _ -> List.sumBy List.length curriedArgInfos + let GetNavigableItems (parsedInput: ParsedInput) : NavigableItem[] = let convertToDisplayName name = @@ -778,7 +805,7 @@ module NavigateTo = let result = ResizeArray() - let addLongIdent kind (lid: LongIdent) (isSignature: bool) (container: NavigableContainer) = + let addLongIdent kind (lid: LongIdent) (isSignature: bool) (container: NavigableContainer) typeParameterCount = if not lid.IsEmpty then let name = textOfLid lid @@ -789,10 +816,12 @@ module NavigateTo = IsSignature = isSignature Kind = kind Container = container + ParameterCount = 0 + TypeParameterCount = typeParameterCount } |> result.Add - let addIdent kind (id: Ident) (isSignature: bool) (container: NavigableContainer) = + let addIdentWithArity kind (id: Ident) (isSignature: bool) (container: NavigableContainer) parameterCount typeParameterCount = if not (String.IsNullOrEmpty id.idText) then let name = convertToDisplayName id.idText @@ -803,11 +832,16 @@ module NavigateTo = IsSignature = isSignature Kind = kind Container = container + ParameterCount = parameterCount + TypeParameterCount = typeParameterCount } |> result.Add + let addIdent kind id isSignature container = + addIdentWithArity kind id isSignature container 0 0 + let addModule lid isSig container = - addLongIdent NavigableItemKind.Module lid isSig container + addLongIdent NavigableItemKind.Module lid isSig container 0 let addModuleAbbreviation (id: Ident) isSig container = addIdent NavigableItemKind.ModuleAbbreviation id isSig container @@ -818,14 +852,17 @@ module NavigateTo = NavigableContainer.Container(NavigableContainerType.Exception, [ id.idText ], container) let addComponentInfo containerType kind (info: SynComponentInfo) isSig container = + let (SynComponentInfo(typeParams = typeParams)) = info let lid = info.LongIdent - addLongIdent kind lid isSig container + addLongIdent kind lid isSig container (typeParameterCountOf typeParams) NavigableContainer.Container(containerType, pathOfLid lid, container) let addValSig kind synValSig isSig container = - let (SynValSig(ident = SynIdent(id, _))) = synValSig - addIdent kind id isSig container + let (SynValSig(ident = SynIdent(id, _); explicitTypeParams = SynValTyparDecls(typars, _); synType = synType; arity = arity)) = + synValSig + + addIdentWithArity kind id isSig container (parameterCountOfSignature arity synType) (typeParameterCountOf typars) let addField synField isSig container = let (SynField(idOpt = id)) = synField @@ -863,17 +900,25 @@ module NavigateTo = | Some mf -> mapMemberKind mf.MemberKind | _ -> NavigableItemKind.ModuleValue + let typeParameterCount = + match headPat with + | SynPat.LongIdent(typarDecls = Some(SynValTyparDecls(typars, _))) -> typeParameterCountOf typars + | _ -> 0 + + let addBindingIdent id = + addIdentWithArity kind id false container (parameterCountOfBinding valData) typeParameterCount + match headPat with | SynPat.LongIdent(longDotId = SynLongIdent([ _; id ], _, _)) -> // instance members - addIdent kind id false container + addBindingIdent id | SynPat.LongIdent(longDotId = SynLongIdent([ id ], _, _)) -> // functions - addIdent kind id false container + addBindingIdent id | SynPat.Named(SynIdent(id, _), _, _, _) | SynPat.As(_, SynPat.Named(SynIdent(id, _), _, _, _), _) -> // values - addIdent kind id false container + addBindingIdent id | _ -> () let addMember valSig (memberFlags: SynMemberFlags) isSig container = diff --git a/src/Compiler/Service/ServiceNavigation.fsi b/src/Compiler/Service/ServiceNavigation.fsi index cfccd6ef20c..49475c7f93b 100755 --- a/src/Compiler/Service/ServiceNavigation.fsi +++ b/src/Compiler/Service/ServiceNavigation.fsi @@ -112,12 +112,20 @@ type NavigableContainer = member Name: string type NavigableItem = - { Name: string - NeedsBackticks: bool - Range: range - IsSignature: bool - Kind: NavigableItemKind - Container: NavigableContainer } + { + Name: string + NeedsBackticks: bool + Range: range + IsSignature: bool + Kind: NavigableItemKind + Container: NavigableContainer + /// The number of parameters of the method the declaration compiles to, as C# and VB count them to order + /// equally good Navigate To matches: every curried and tupled argument, without the instance and without a + /// solitary unit argument. + ParameterCount: int + /// The number of explicitly declared type parameters. + TypeParameterCount: int + } [] module public NavigateTo = diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index bce0bc86e9b..357272884a2 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -3949,10 +3949,14 @@ FSharp.Compiler.EditorServices.NavigableItem: FSharp.Compiler.Text.Range Range FSharp.Compiler.EditorServices.NavigableItem: FSharp.Compiler.Text.Range get_Range() FSharp.Compiler.EditorServices.NavigableItem: Int32 GetHashCode() FSharp.Compiler.EditorServices.NavigableItem: Int32 GetHashCode(System.Collections.IEqualityComparer) +FSharp.Compiler.EditorServices.NavigableItem: Int32 ParameterCount +FSharp.Compiler.EditorServices.NavigableItem: Int32 TypeParameterCount +FSharp.Compiler.EditorServices.NavigableItem: Int32 get_ParameterCount() +FSharp.Compiler.EditorServices.NavigableItem: Int32 get_TypeParameterCount() FSharp.Compiler.EditorServices.NavigableItem: System.String Name FSharp.Compiler.EditorServices.NavigableItem: System.String ToString() FSharp.Compiler.EditorServices.NavigableItem: System.String get_Name() -FSharp.Compiler.EditorServices.NavigableItem: Void .ctor(System.String, Boolean, FSharp.Compiler.Text.Range, Boolean, FSharp.Compiler.EditorServices.NavigableItemKind, FSharp.Compiler.EditorServices.NavigableContainer) +FSharp.Compiler.EditorServices.NavigableItem: Void .ctor(System.String, Boolean, FSharp.Compiler.Text.Range, Boolean, FSharp.Compiler.EditorServices.NavigableItemKind, FSharp.Compiler.EditorServices.NavigableContainer, Int32, Int32) FSharp.Compiler.EditorServices.NavigableItemKind+Tags: Int32 Constructor FSharp.Compiler.EditorServices.NavigableItemKind+Tags: Int32 EnumCase FSharp.Compiler.EditorServices.NavigableItemKind+Tags: Int32 Exception diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj index 2ff9555dee6..77ccb5a9b0a 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -52,6 +52,7 @@ + diff --git a/tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs b/tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs new file mode 100644 index 00000000000..cf67e20c870 --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs @@ -0,0 +1,63 @@ +module FSharp.Compiler.Service.Tests.NavigateToTests + +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Service.Tests.Common +open FSharp.Compiler.Syntax +open Xunit + +let private implementation = + """ +let value = 1 +let curried a b = a + b +let tupledAndCurried (a, b) c = a + b + c +let takesUnit () = 1 +let generic<'T> (x: 'T) = x + +type C<'T, 'U>() = + member _.Method(a: int, b: int) = a + b + member _.Property = 1 + static member Static x y = x + y +""" + +let private signature = + """ +module M + +val curried: int -> int -> int +val takesUnit: unit -> int +val generic<'T> : 'T * 'T -> 'T + +type C<'T> = + member Method: a: int * b: int -> int + abstract Abstract: unit -> unit +""" + +let private arityOf (parseTree: ParsedInput) name = + let item = + NavigateTo.GetNavigableItems parseTree + |> Array.find (fun item -> item.Name = name) + + item.ParameterCount, item.TypeParameterCount + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``A declaration in an implementation file counts the parameters it compiles to`` (name: string, parameterCount: int, typeParameterCount: int) = + Assert.Equal((parameterCount, typeParameterCount), arityOf (getParseResults implementation) name) + +[] +[] +[] +[] +[] +[] +[] +let ``A declaration in a signature file counts the parameters it compiles to`` (name: string, parameterCount: int, typeParameterCount: int) = + Assert.Equal((parameterCount, typeParameterCount), arityOf (getParseResultsOfSignatureFile signature) name) From f1b560cce4229796b28ca8508143001fa550ecbe Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 13 Sep 2026 20:11:30 +0200 Subject: [PATCH 2/4] Link the release note to its PR Co-Authored-By: Claude Opus 5 --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index a94b11cfd88..61faf95c599 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -169,7 +169,7 @@ * Fix signature generation (`fsc --sig`, `GenerateSignature`, `GetValSignatureText`) dropping the parentheses around a destructured or pattern-annotated tuple parameter when a sibling argument in the same curried group is named, so `(int * float) * z: string` no longer prints as the flat 3-tuple `int * float * z: string`. ([Issue #20397](https://github.com/dotnet/fsharp/issues/20397), [PR #20589](https://github.com/dotnet/fsharp/pull/20589)) ### Added -* `NavigableItem` carries `ParameterCount` and `TypeParameterCount`, the counts C# and VB give Navigate To to order matches that are otherwise equal. +* `NavigableItem` carries `ParameterCount` and `TypeParameterCount`, the counts C# and VB give Navigate To to order matches that are otherwise equal. ([PR #20531](https://github.com/dotnet/fsharp/pull/20531)) * FCS: add FSharpCheckFileResults.FileSignature ([PR #20478](https://github.com/dotnet/fsharp/pull/20478)) * Added the `ReraiseInComputationExpressions` language feature (`--langversion:preview`): `reraise ()` in the `with` handler of a computation expression is compiled to a rethrow through `ExceptionDispatchInfo` instead of being rejected with FS0413. ([Suggestion #660](https://github.com/fsharp/fslang-suggestions/issues/660), [RFC FS-1347](https://github.com/fsharp/fslang-design/pull/843), [PR #20405](https://github.com/dotnet/fsharp/pull/20405)) From bfb24868a53d245939edda7cf2b5f167c6eb4d8b Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sat, 26 Sep 2026 02:36:15 +0200 Subject: [PATCH 3/4] Count a named unit argument of a signature as no argument A signature keeps the solitary unit argument the compiler drops, so the arity it parses to is read through the type: `val f: unit -> int` takes none. Naming the argument - `val f: u: unit -> int` - wraps the type in a SynType.SignatureParameter the check did not look through, so the same member counted one argument in the signature and none in the implementation, and Navigate To sorted the two declarations of it apart. Co-Authored-By: Claude Opus 5 (1M context) --- src/Compiler/Service/ServiceNavigation.fs | 4 +++- tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Service/ServiceNavigation.fs b/src/Compiler/Service/ServiceNavigation.fs index c09ab0c188e..b444bdccfb2 100755 --- a/src/Compiler/Service/ServiceNavigation.fs +++ b/src/Compiler/Service/ServiceNavigation.fs @@ -779,7 +779,9 @@ module NavigateTo = match synType with | SynType.LongIdent(SynLongIdent([ id ], _, _)) -> id.idText = "unit" | SynType.Paren(innerType, _) - | SynType.WithGlobalConstraints(innerType, _, _) -> isUnitType innerType + | SynType.WithGlobalConstraints(innerType, _, _) + // `val f: u: unit -> int` names the argument the compiler drops, so the name must not hide it. + | SynType.SignatureParameter(usedType = innerType) -> isUnitType innerType | _ -> false /// The parameters of the compiled method: the parser leaves a solitary unit argument of a signature in its arity, diff --git a/tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs b/tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs index cf67e20c870..a4f6e578bfc 100644 --- a/tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs @@ -25,6 +25,7 @@ module M val curried: int -> int -> int val takesUnit: unit -> int +val takesNamedUnit: u: unit -> int val generic<'T> : 'T * 'T -> 'T type C<'T> = @@ -55,6 +56,7 @@ let ``A declaration in an implementation file counts the parameters it compiles [] [] [] +[] [] [] [] From 86cfab817aa0750004b4223fb187674e2785c94e Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sat, 26 Sep 2026 13:41:37 +0200 Subject: [PATCH 4/4] Move the release note to the version in development `main` opened 11.0.200 for SDK 11.0.200; 11.0.100 has shipped. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 - docs/release-notes/.FSharp.Compiler.Service/11.0.200.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 61faf95c599..f383908e195 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -169,7 +169,6 @@ * Fix signature generation (`fsc --sig`, `GenerateSignature`, `GetValSignatureText`) dropping the parentheses around a destructured or pattern-annotated tuple parameter when a sibling argument in the same curried group is named, so `(int * float) * z: string` no longer prints as the flat 3-tuple `int * float * z: string`. ([Issue #20397](https://github.com/dotnet/fsharp/issues/20397), [PR #20589](https://github.com/dotnet/fsharp/pull/20589)) ### Added -* `NavigableItem` carries `ParameterCount` and `TypeParameterCount`, the counts C# and VB give Navigate To to order matches that are otherwise equal. ([PR #20531](https://github.com/dotnet/fsharp/pull/20531)) * FCS: add FSharpCheckFileResults.FileSignature ([PR #20478](https://github.com/dotnet/fsharp/pull/20478)) * Added the `ReraiseInComputationExpressions` language feature (`--langversion:preview`): `reraise ()` in the `with` handler of a computation expression is compiled to a rethrow through `ExceptionDispatchInfo` instead of being rejected with FS0413. ([Suggestion #660](https://github.com/fsharp/fslang-suggestions/issues/660), [RFC FS-1347](https://github.com/fsharp/fslang-design/pull/843), [PR #20405](https://github.com/dotnet/fsharp/pull/20405)) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 6212188b65f..8672f3b03ed 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -1,6 +1,7 @@ ### Added * F# Interactive gains a JSON-RPC server mode, `--fsi-server-jsonrpc:`, in which a host submits interactions over a named pipe and receives structured results — diagnostics with positions, escaping exceptions, the values each interaction bound, and the session's own process id — instead of recovering them by looking for a `SERVER-PROMPT>` marker in the output text. Program output continues to flow through the redirected console streams. The pipe admits only the user running the session; `--fsi-server-client-pid:` names the host process whose exit ends the session. `FsiEvaluationSession` exposes both options as `JsonRpcServerPipeName` and `JsonRpcClientProcessId`. The mode is part of the .NET fsi only. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) +* `NavigableItem` carries `ParameterCount` and `TypeParameterCount`, the counts C# and VB give Navigate To to order matches that are otherwise equal. ([PR #20531](https://github.com/dotnet/fsharp/pull/20531)) ### Fixed