diff --git a/paket.lock b/paket.lock index 2f44e8a69..9a0212790 100644 --- a/paket.lock +++ b/paket.lock @@ -55,8 +55,8 @@ NUGET FSharp.Core (9.0.100) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= net8.0)) (&& (== netstandard2.1) (>= net8.0)) McMaster.NETCore.Plugins (>= 1.4) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= net8.0)) (&& (== netstandard2.1) (>= net8.0)) Microsoft.Extensions.Logging.Abstractions (>= 6.0) - restriction: || (== net8.0) (== net9.0) (&& (== netstandard2.0) (>= net8.0)) (&& (== netstandard2.1) (>= net8.0)) - FSharp.Compiler.Service (43.9.100) - FSharp.Core (9.0.100) + FSharp.Compiler.Service (43.9.101) + FSharp.Core (9.0.101) System.Buffers (>= 4.5.1) System.Collections.Immutable (>= 8.0) System.Diagnostics.DiagnosticSource (>= 8.0) diff --git a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs index 9458913f5..0e304b1ca 100644 --- a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs +++ b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs @@ -54,7 +54,7 @@ let tryFindAbstractClassExprInBufferAtPos let allMembers = reprMembers @ members - let! inheritType, inheritMemberRange = // this must exist for abstract types + let! inheritType, inheritMemberRange = allMembers |> List.tryPick (function | SynMemberDefn.ImplicitInherit(inheritType, _, _, range) -> Some(inheritType, range) diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index b278ed907..1c7b56def 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -367,7 +367,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ) let allFlags = - Array.append [| "--targetprofile:netstandard" |] fsiAdditionalArguments + Array.append [| "--targetprofile:netstandard"; "--checknulls+"; "--define:NULLABLE" |] fsiAdditionalArguments let! (opts, errors) = checker.GetProjectOptionsFromScript( diff --git a/src/FsAutoComplete.Core/DocumentationFormatter.fs b/src/FsAutoComplete.Core/DocumentationFormatter.fs index c497c855d..bb22f75f4 100644 --- a/src/FsAutoComplete.Core/DocumentationFormatter.fs +++ b/src/FsAutoComplete.Core/DocumentationFormatter.fs @@ -309,7 +309,7 @@ module DocumentationFormatter = if String.IsNullOrWhiteSpace formattedParam then formattedParam else - "(requires " + formattedParam + " )" + "(requires " + formattedParam + ")" else "" @@ -379,7 +379,7 @@ module DocumentationFormatter = if String.IsNullOrWhiteSpace formattedParam then formattedParam else - "(requires " + formattedParam + " )" + "(requires " + formattedParam + ")" if paramConstraint = retTypeConstraint then paddedParam diff --git a/src/FsAutoComplete.Core/SignatureFormatter.fs b/src/FsAutoComplete.Core/SignatureFormatter.fs index c79e34433..d88b5300e 100644 --- a/src/FsAutoComplete.Core/SignatureFormatter.fs +++ b/src/FsAutoComplete.Core/SignatureFormatter.fs @@ -43,6 +43,8 @@ module SignatureFormatter = let rec formatFSharpType (context: FSharpDisplayContext) (typ: FSharpType) : string = let context = context.WithPrefixGenericParameters() + let nullabilityClause = if typ.HasNullAnnotation then " | null" else "" + try if typ.IsTupleType || typ.IsStructTupleType then let refTupleStr = @@ -59,7 +61,7 @@ module SignatureFormatter = else refTupleStr elif typ.IsGenericParameter then // no longer need to differentiate between SRTP and normal generic parameter types - "'" + typ.GenericParameter.Name + "'" + typ.GenericParameter.Name + nullabilityClause elif typ.HasTypeDefinition && typ.GenericArguments.Count > 0 then let typeDef = typ.TypeDefinition @@ -68,19 +70,20 @@ module SignatureFormatter = if entityIsArray typeDef then if typ.GenericArguments.Count = 1 && typ.GenericArguments.[0].IsTupleType then - sprintf "(%s) array" genericArgs + $"(%s{genericArgs}) array%s{nullabilityClause}" else - sprintf "%s array" genericArgs + $"%s{genericArgs} array%s{nullabilityClause}" elif isMeasureType typeDef then - typ.Format context + typ.Format context + nullabilityClause else - sprintf "%s<%s>" (FSharpKeywords.NormalizeIdentifierBackticks typeDef.DisplayName) genericArgs + $"%s{FSharpKeywords.NormalizeIdentifierBackticks typeDef.DisplayName}<%s{genericArgs}>%s{nullabilityClause}" else if typ.HasTypeDefinition then FSharpKeywords.NormalizeIdentifierBackticks typ.TypeDefinition.DisplayName + + nullabilityClause else - typ.Format context + typ.Format context + nullabilityClause with _ -> - typ.Format context + typ.Format context + nullabilityClause let formatGenericParameter includeMemberConstraintTypes displayContext (param: FSharpGenericParameter) = @@ -357,7 +360,7 @@ module SignatureFormatter = if String.IsNullOrWhiteSpace formattedParam then formattedParam else - "(requires " + formattedParam + " )" + "(requires " + formattedParam + ")" if paramConstraint = retTypeConstraint then paramFormat diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index c84bbbcb9..26526718e 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -141,10 +141,7 @@ let initTests createServer = }) let validateSymbolExists msgType symbolInfos predicate = - Expect.exists - symbolInfos - predicate - $"{msgType}s do not contain the expected symbol" + Expect.exists symbolInfos predicate $"{msgType}s do not contain the expected symbol" let allSymbolInfosExist (infos: SymbolInformation seq) predicates = predicates |> List.iter (validateSymbolExists (nameof SymbolInformation) infos) @@ -187,16 +184,12 @@ let documentSymbolTest state = | Ok(Some(U2.C1 symbolInformations)) -> Expect.equal symbolInformations.Length 15 "Document Symbol has all symbols" - allSymbolInfosExist - symbolInformations - [fun n -> n.Name = "MyDateTime" && n.Kind = SymbolKind.Class] + allSymbolInfosExist symbolInformations [ fun n -> n.Name = "MyDateTime" && n.Kind = SymbolKind.Class ] | Ok(Some(U2.C2 documentSymbols)) -> Expect.equal documentSymbols.Length 15 "Document Symbol has all symbols" - allDocumentSymbolsExist - documentSymbols - [fun n -> n.Name = "MyDateTime" && n.Kind = SymbolKind.Class] + allDocumentSymbolsExist documentSymbols [ fun n -> n.Name = "MyDateTime" && n.Kind = SymbolKind.Class ] } ] let workspaceSymbolTest state = @@ -213,8 +206,7 @@ let workspaceSymbolTest state = testList "Workspace Symbols Tests" - [ - testCaseAsync "Get Workspace Symbols Using Filename of Script File as Query" + [ testCaseAsync "Get Workspace Symbols Using Filename of Script File as Query" <| async { let! server, _path = server @@ -231,16 +223,12 @@ let workspaceSymbolTest state = | Ok(Some(U2.C1 symbolInfos)) -> Expect.equal symbolInfos.Length 1 "Workspace did not find all the expected symbols" - allSymbolInfosExist - symbolInfos - [fun n -> n.Name = "Script" && n.Kind = SymbolKind.Module] + allSymbolInfosExist symbolInfos [ fun n -> n.Name = "Script" && n.Kind = SymbolKind.Module ] | Ok(Some(U2.C2 workspaceSymbols)) -> Expect.equal workspaceSymbols.Length 1 "Workspace did not find all the expected symbols" - allWorkspaceSymbolsExist - workspaceSymbols - [fun n -> n.Name = "Script" && n.Kind = SymbolKind.Module] + allWorkspaceSymbolsExist workspaceSymbols [ fun n -> n.Name = "Script" && n.Kind = SymbolKind.Module ] } testCaseAsync "Get Workspace Symbols Using Query w/ Text" @@ -262,26 +250,22 @@ let workspaceSymbolTest state = allSymbolInfosExist symbolInfos - [ - fun n -> n.Name = "X" && n.Kind = SymbolKind.Class + [ fun n -> n.Name = "X" && n.Kind = SymbolKind.Class fun n -> n.Name = "X" && n.Kind = SymbolKind.Class fun n -> n.Name = "X.X" && n.Kind = SymbolKind.Module fun n -> n.Name = "X.Y" && n.Kind = SymbolKind.Module - fun n -> n.Name = "X.Z" && n.Kind = SymbolKind.Class - ] + fun n -> n.Name = "X.Z" && n.Kind = SymbolKind.Class ] | Ok(Some(U2.C2 workspaceSymbols)) -> Expect.equal workspaceSymbols.Length 5 "Workspace did not find all the expected symbols" allWorkspaceSymbolsExist workspaceSymbols - [ - fun n -> n.Name = "X" && n.Kind = SymbolKind.Class + [ fun n -> n.Name = "X" && n.Kind = SymbolKind.Class fun n -> n.Name = "X" && n.Kind = SymbolKind.Class fun n -> n.Name = "X.X" && n.Kind = SymbolKind.Module fun n -> n.Name = "X.Y" && n.Kind = SymbolKind.Module - fun n -> n.Name = "X.Z" && n.Kind = SymbolKind.Class - ] + fun n -> n.Name = "X.Z" && n.Kind = SymbolKind.Class ] } testCaseAsync "Get Workspace Symbols Using Query w/o Text" @@ -298,12 +282,9 @@ let workspaceSymbolTest state = match res with | Result.Error e -> failtestf "Request failed: %A" e | Ok None -> failtest "Request none" - | Ok(Some(U2.C1 res)) -> - Expect.equal res.Length 0 "Workspace found symbols when we didn't expect to find any" - | Ok(Some(U2.C2 res)) -> - Expect.equal res.Length 0 "Workspace found symbols when we didn't expect to find any" - } - ] + | Ok(Some(U2.C1 res)) -> Expect.equal res.Length 0 "Workspace found symbols when we didn't expect to find any" + | Ok(Some(U2.C2 res)) -> Expect.equal res.Length 0 "Workspace found symbols when we didn't expect to find any" + } ] let foldingTests state = @@ -392,9 +373,9 @@ let tooltipTests state = } |> Async.Cache - let verifySignature line character expectedSignature = + let verifySignature name line character expectedSignature = testCaseAsync - (sprintf "tooltip for line %d character %d should be '%s'" line character expectedSignature) + name (async { let! server, scriptPath = server @@ -411,11 +392,11 @@ let tooltipTests state = let concatLines = String.concat Environment.NewLine - let verifyDescriptionImpl testCaseAsync line character expectedDescription = + let verifyDescriptionImpl testCaseAsync name line character expectedDescription = let expectedDescription = concatLines expectedDescription testCaseAsync - (sprintf "description for line %d character %d" line character) + (sprintf name) (async { let! server, scriptPath = server @@ -430,8 +411,8 @@ let tooltipTests state = | Result.Error errors -> failtestf "Error while getting description: %A" errors }) - let verifyDescription line character expectedDescription = - verifyDescriptionImpl testCaseAsync line character expectedDescription + let verifyDescription name line character expectedDescription = + verifyDescriptionImpl testCaseAsync name line character expectedDescription testSequenced <| testList @@ -439,6 +420,7 @@ let tooltipTests state = [ testList "tests" [ verifyDescription + "language keywords" 0u 2u [ "**Description**" @@ -446,12 +428,17 @@ let tooltipTests state = "" "Used to associate, or bind, a name to a value or function." "" ] // `let` keyword - verifySignature 0u 4u "val arrayOfTuples: (int * int) array" // verify that even the first letter of the tooltip triggers correctly - verifySignature 0u 5u "val arrayOfTuples: (int * int) array" // inner positions trigger - verifySignature 1u 5u "val listOfTuples: list" // verify we default to prefix-generics style - verifySignature 2u 5u "val listOfStructTuples: list" // verify we render struct tuples in a round-tripabble format - verifySignature 3u 5u "val floatThatShouldHaveGenericReportedInTooltip: float" // verify we strip measure annotations + verifySignature "trigger.firstCharacter" 0u 4u "val arrayOfTuples: (int * int) array" // verify that even the first letter of the tooltip triggers correctly + verifySignature "trigger.innerCharacter" 0u 5u "val arrayOfTuples: (int * int) array" // inner positions trigger + verifySignature "uses prefix generics" 1u 5u "val listOfTuples: list" // verify we default to prefix-generics style + verifySignature "struct tuple handling" 2u 5u "val listOfStructTuples: list" // verify we render struct tuples in a round-tripabble format + verifySignature + "strip meaningless units of measure" + 3u + 5u + "val floatThatShouldHaveGenericReportedInTooltip: float" // verify we strip measure annotations verifyDescription + "tooltip formatting for external library functions" 4u 4u [ "**Description**" @@ -470,6 +457,7 @@ let tooltipTests state = "" "* `'T` is `System.String`" ] // verify fancy descriptions for external library functions and correct backticks for multiple segments verifyDescription + "multiple generic parameters are explained" 13u 11u [ "**Description**" @@ -487,47 +475,65 @@ let tooltipTests state = "**Returns**" "" "" ] - verifySignature 14u 5u "val nestedTuples: int * ((int * int) * int)" // verify that tuples render correctly (parens, etc) - verifySignature 15u 5u "val nestedStructTuples: int * struct (int * int)" // verify we can differentiate between struct and non-struct tuples - verifySignature 21u 9u "val speed: float" // verify we nicely-render measure annotations + verifySignature "nested tuples" 14u 5u "val nestedTuples: int * ((int * int) * int)" // verify that tuples render correctly (parens, etc) + verifySignature "mixed struct and reference tuples" 15u 5u "val nestedStructTuples: int * struct (int * int)" // verify we can differentiate between struct and non-struct tuples + verifySignature "units of measure are rendered nicely" 21u 9u "val speed: float" // verify we nicely-render measure annotations // verify formatting of function-parameters to values. NOTE: we want to wrap them in parens for user clarity eventually. verifySignature + "parameters that are functions are wrapped in parens for clarity" 26u 5u (concatLines [ "val funcWithFunParam:"; " f: (int -> unit) ->"; " i: int"; " -> unit" ]) // verify formatting of tuple args. NOTE: we want to wrap tuples in parens for user clarify eventually. verifySignature + "tuple args are split on each line" 30u 12u (concatLines [ "val funcWithTupleParam:"; " int *"; " int"; " -> int * int" ]) // verify formatting of struct tuple args in parameter tooltips. verifySignature + "struct tuple parameters are rendered correctly" 32u 12u (concatLines [ "val funcWithStructTupleParam:" " f: struct (int * int)" " -> struct (int * int)" ]) - verifySignature 36u 15u (concatLines [ "member Foo:"; " stuff: int * int * int"; " -> int" ]) verifySignature + "tuple member parameters are gathered on single line" + 36u + 15u + (concatLines [ "member Foo:"; " stuff: int * int * int"; " -> int" ]) + verifySignature + "multiple named member parameters are split on each line" 37u 15u (concatLines [ "member Bar:"; " a: int *"; " b: int *"; " c: int"; " -> int" ]) // verify formatting for multi-char operators - verifySignature 39u 7u (concatLines [ "val ( .>> ):"; " x: int ->"; " y: int"; " -> int" ]) + verifySignature + "multi-char operator rendering" + 39u + 7u + (concatLines [ "val ( .>> ):"; " x: int ->"; " y: int"; " -> int" ]) // verify formatting for single-char operators - verifySignature 41u 6u (concatLines [ "val ( ^ ):"; " x: int ->"; " y: int"; " -> int" ]) + verifySignature + "single-char operator rendering" + 41u + 6u + (concatLines [ "val ( ^ ):"; " x: int ->"; " y: int"; " -> int" ]) // verify rendering of generic constraints verifySignature + "generic constraints on parameters" 43u 13u (concatLines [ "val inline add:" - " x: 'a (requires static member ( + ) ) ->" - " y: 'b (requires static member ( + ) )" + " x: 'a (requires static member ( + )) ->" + " y: 'b (requires static member ( + ))" " -> 'c" ]) //verify rendering of solved generic constraints in tooltips for members where they are solved verifyDescription + "solved generic parameters are called out in tooltip" 45u 15u [ "**Generic Parameters**" @@ -536,6 +542,7 @@ let tooltipTests state = "* `'b` is `int`" "* `'c` is `int`" ] verifySignature + "optional member parameters are rendered with leading question mark" 48u 28u (concatLines @@ -543,8 +550,13 @@ let tooltipTests state = " body : (MailboxProcessor -> Async) *" " ?cancellationToken: System.Threading.CancellationToken" " -> MailboxProcessor" ]) - verifySignature 54u 9u "Case2 of string * newlineBefore: bool * newlineAfter: bool" verifySignature + "union case named parameters are rendered" + 54u + 9u + "Case2 of string * newlineBefore: bool * newlineAfter: bool" + verifySignature + "active pattern signatures with potentially-nullable results" 60u 7u (concatLines @@ -558,22 +570,23 @@ let tooltipTests state = " -> option" ]) #endif verifySignature + "generic constraint rendering for IWSAM" 77u 5u - (concatLines [ - "val testIWSAMTest:" + (concatLines + [ "val testIWSAMTest:" " unit" - " -> Result (requires :> IWSAMTest<'e>)" - ]) + " -> Result (requires :> IWSAMTest<'e>)" ]) verifySignature + "multiple generic constraints for IWASMs" 90u 25u - (concatLines [ - "static member GetAwaiter:" - " awaitable: 'Awaitable (requires member GetAwaiter )" - " -> Awaiter<^Awaiter,'TResult> (requires :> ICriticalNotifyCompletion and member IsCompleted and member GetResult)" - ]) + (concatLines + [ "static member GetAwaiter:" + " awaitable: 'Awaitable (requires member GetAwaiter)" + " -> Awaiter<^Awaiter,'TResult> (requires :> ICriticalNotifyCompletion and member IsCompleted and member GetResult)" ]) verifySignature + "basic active pattern" 65u 7u (concatLines @@ -581,6 +594,7 @@ let tooltipTests state = " input: Expr" " -> option" ]) verifySignature + "basic active pattern with nullability awareness" 70u 7u (concatLines @@ -594,12 +608,42 @@ let tooltipTests state = " -> option" ]) #endif verifySignature + "interface with members with and without parameter names" 96u 7u (concatLines [ "interface IWithAndWithoutParamNames" " abstract member WithParamNames: arg1: int * arg2: float -> string" - " abstract member WithoutParamNames: int * string -> int" ]) ] ] + " abstract member WithoutParamNames: int * string -> int" ]) +#if NET8_0_OR_GREATER + verifySignature + "function with unsolved nullable parameter" + 102u + 7u + (concatLines [ "val usesNullable:"; " x: 't | null"; " -> 't (requires reference)" ]) + verifySignature + "function with concrete nullable parameter" + 103u + 7u + (concatLines [ "val usesConcreteNullable:"; " x: string | null"; " -> String" ]) + verifySignature + "function with generic nullable return" + 104u + 7u + (concatLines [ "val makesNullable:"; " x: 'x"; " -> 'x | null (requires reference)" ]) + verifySignature + "function with concrete nullable return" + 105u + 7u + (concatLines [ "val makesConcreteNullable:"; " x: string"; " -> string | null" ]) + verifySignature + "function with nullable return from BCL call" + 106u + 7u + (concatLines [ "val usesBCLNullable:"; " key: string"; " -> string | null" ]) + verifySignature "simple value" 107u 7u ("val envKey: string | null") +#endif + ] ] let closeTests state = // Note: clear diagnostics also implies clear caches (-> remove file & project options from State). diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx b/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx index a36c69ed5..2a2a2b681 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx @@ -97,3 +97,12 @@ type Awaitable = type IWithAndWithoutParamNames = abstract member WithParamNames : arg1: int * arg2: float -> string abstract member WithoutParamNames : int * string -> int + +#nullable enable + +let usesNullable (x: 't | null) = nonNull x +let usesConcreteNullable (x: string | null) = nonNull x +let makesNullable (x: 'x): 'x | null = null +let makesConcreteNullable (x: string): string | null = null +let usesBCLNullable (key: string) = System.Environment.GetEnvironmentVariable(key) +let envKey = System.Environment.GetEnvironmentVariable("PATH")