diff --git a/docs/release-notes/.FSharp.Compiler.Service/9.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/9.0.100.md index 0d750d049e5..e2562e5a4c0 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/9.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/9.0.100.md @@ -15,6 +15,7 @@ * Parser: recover on missing union case fields (PR [#17452](https://github.com/dotnet/fsharp/pull/17452)) * Parser: recover on missing union case field types (PR [#17455](https://github.com/dotnet/fsharp/pull/17455)) * Sink: report function domain type ([PR #17470](https://github.com/dotnet/fsharp/pull/17470)) +* Render C# nullable-analysis attributes in tooltips ([PR #17485](https://github.com/dotnet/fsharp/pull/17485)) ### Changed diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index 265f0570d7e..046f119ad08 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -38,6 +38,8 @@ module internal PrintUtilities = let squareAngleL x = LeftL.leftBracketAngle ^^ x ^^ RightL.rightBracketAngle + let squareAngleReturn x = LeftL.leftBracketAngle ^^ WordL.keywordReturn ^^ SepL.colon ^^ x ^^ RightL.rightBracketAngle + let angleL x = SepL.leftAngle ^^ x ^^ RightL.rightAngle let braceL x = wordL leftBrace ^^ x ^^ wordL rightBrace @@ -638,6 +640,23 @@ module PrintTypes = let argsL = bracketL (sepListL RightL.comma (List.map (layoutILAttribElement denv) args)) PrintIL.layoutILType denv [] ty ++ argsL + /// Layout nullness attributes for C# flow-analysis + /// F# does not process them, this way we can at least show them. + and layoutCsharpCodeAnalysisIlAttributes denv (attrs:ILAttributes) (layoutCombinator: Layout -> Layout -> Layout) restL = + let denvShortNames() = { denv with shortTypeNames = true } + let attrsL = + [ for a in attrs.AsArray() do + let name = a.Method.DeclaringType.BasicQualifiedName + if name.StartsWith("System.Diagnostics.CodeAnalysis") then + let parms, _args = decodeILAttribData a + layoutILAttrib (denvShortNames()) (a.Method.DeclaringType, parms) + ] + match attrsL with + | [] -> restL + | _ -> + let separated = sepListL RightL.semicolon attrsL + layoutCombinator separated restL + /// Layout '[]' above another block and layoutAttribs denv startOpt isLiteral kind attrs restL = @@ -1638,11 +1657,33 @@ module InfoMemberPrinting = let idL = ConvertValLogicalNameToDisplayLayout false (tagMethod >> tagNavArbValRef minfo.ArbitraryValRef >> wordL) minfo.LogicalName SepL.dot ^^ PrintTypes.layoutTyparDecls denv idL true minfo.FormalMethodTypars ^^ - SepL.leftParen + SepL.leftParen + + let layout,paramLayouts = + match denv.showCsharpCodeAnalysisAttributes, minfo with + | true, ILMeth(_g,mi,_e) -> + let methodLayout = + // Render Method attributes and [return:..] attributes on separate lines above (@@) the method definition + PrintTypes.layoutCsharpCodeAnalysisIlAttributes denv (minfo.GetCustomAttrs()) (squareAngleL >> (@@)) layout + |> PrintTypes.layoutCsharpCodeAnalysisIlAttributes denv (mi.RawMetadata.Return.CustomAttrs) (squareAngleReturn >> (@@)) + let paramLayouts = + minfo.GetParamDatas (amap, m, minst) + |> List.head + |> List.zip (mi.ParamMetadata) + |> List.map(fun (ilParams,paramData) -> + layoutParamData denv paramData + // Render parameter attributes next to (^^) the parameter definition + |> PrintTypes.layoutCsharpCodeAnalysisIlAttributes denv (ilParams.CustomAttrs) (squareAngleL >> (^^)) ) + methodLayout,paramLayouts + | _ -> + layout, + minfo.GetParamDatas (amap, m, minst) + |> List.concat + |> List.map (layoutParamData denv) + - let paramDatas = minfo.GetParamDatas (amap, m, minst) - let layout = layout ^^ sepListL RightL.comma ((List.concat >> List.map (layoutParamData denv)) paramDatas) - layout ^^ RightL.rightParen ^^ WordL.colon ^^ PrintTypes.layoutType denv retTy + let layout = layout ^^ sepListL RightL.comma paramLayouts + layout ^^ RightL.rightParen ^^ WordL.colon ^^ PrintTypes.layoutType denv retTy // Todo enrich return type // Prettify an ILMethInfo let prettifyILMethInfo (amap: Import.ImportMap) m (minfo: MethInfo) typarInst ilMethInfo = diff --git a/src/Compiler/Facilities/TextLayoutRender.fs b/src/Compiler/Facilities/TextLayoutRender.fs index 12595b69211..735d44b82ad 100644 --- a/src/Compiler/Facilities/TextLayoutRender.fs +++ b/src/Compiler/Facilities/TextLayoutRender.fs @@ -63,6 +63,7 @@ module WordL = let leftAngle = wordL TaggedText.leftAngle let keywordModule = wordL TaggedText.keywordModule let keywordNamespace = wordL TaggedText.keywordNamespace + let keywordReturn = wordL TaggedText.keywordReturn module LeftL = let leftParen = leftL TaggedText.leftParen diff --git a/src/Compiler/Facilities/TextLayoutRender.fsi b/src/Compiler/Facilities/TextLayoutRender.fsi index 0e52b9998f2..96d4b13a184 100644 --- a/src/Compiler/Facilities/TextLayoutRender.fsi +++ b/src/Compiler/Facilities/TextLayoutRender.fsi @@ -102,6 +102,7 @@ module internal WordL = val leftAngle: Layout val keywordModule: Layout val keywordNamespace: Layout + val keywordReturn: Layout module internal LeftL = val leftParen: Layout diff --git a/src/Compiler/Service/ServiceDeclarationLists.fs b/src/Compiler/Service/ServiceDeclarationLists.fs index 92729d789f8..39dcb183028 100644 --- a/src/Compiler/Service/ServiceDeclarationLists.fs +++ b/src/Compiler/Service/ServiceDeclarationLists.fs @@ -157,7 +157,7 @@ module DeclarationListHelpers = let rec FormatItemDescriptionToToolTipElement displayFullName (infoReader: InfoReader) ad m denv (item: ItemWithInst) symbol (width: int option) = let g = infoReader.g let amap = infoReader.amap - let denv = SimplerDisplayEnv denv + let denv = {SimplerDisplayEnv denv with showCsharpCodeAnalysisAttributes = true } let xml = GetXmlCommentForItem infoReader m item.Item match item.Item with diff --git a/src/Compiler/TypedTree/TypedTreeOps.fs b/src/Compiler/TypedTree/TypedTreeOps.fs index 685f9a7377c..be78209034e 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.fs @@ -3145,6 +3145,7 @@ type DisplayEnv = shortConstraints: bool useColonForReturnType: bool showAttributes: bool + showCsharpCodeAnalysisAttributes: bool showOverrides: bool showStaticallyResolvedTyparAnnotations: bool showNullnessAnnotations: bool option @@ -3180,6 +3181,7 @@ type DisplayEnv = suppressMutableKeyword = false showMemberContainers = false showAttributes = false + showCsharpCodeAnalysisAttributes = false showOverrides = true showStaticallyResolvedTyparAnnotations = true showNullnessAnnotations = None diff --git a/src/Compiler/TypedTree/TypedTreeOps.fsi b/src/Compiler/TypedTree/TypedTreeOps.fsi index 2dda4ad3f82..3a44513aa0f 100755 --- a/src/Compiler/TypedTree/TypedTreeOps.fsi +++ b/src/Compiler/TypedTree/TypedTreeOps.fsi @@ -1079,6 +1079,7 @@ type DisplayEnv = shortConstraints: bool useColonForReturnType: bool showAttributes: bool + showCsharpCodeAnalysisAttributes: bool showOverrides: bool showStaticallyResolvedTyparAnnotations: bool showNullnessAnnotations: bool option diff --git a/src/Compiler/Utilities/sformat.fs b/src/Compiler/Utilities/sformat.fs index 611c31235d9..55963dbb034 100644 --- a/src/Compiler/Utilities/sformat.fs +++ b/src/Compiler/Utilities/sformat.fs @@ -262,6 +262,7 @@ module TaggedText = let keywordInline = tagKeyword "inline" let keywordModule = tagKeyword "module" let keywordNamespace = tagKeyword "namespace" + let keywordReturn = tagKeyword "return" let punctuationUnit = tagPunctuation "()" #endif diff --git a/src/Compiler/Utilities/sformat.fsi b/src/Compiler/Utilities/sformat.fsi index 1c740a38dce..f2ca573d9c4 100644 --- a/src/Compiler/Utilities/sformat.fsi +++ b/src/Compiler/Utilities/sformat.fsi @@ -213,6 +213,7 @@ module internal TaggedText = val internal keywordInline: TaggedText val internal keywordModule: TaggedText val internal keywordNamespace: TaggedText + val internal keywordReturn: TaggedText val internal punctuationUnit: TaggedText type internal IEnvironment = diff --git a/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs b/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs index 62ff75d34df..1edac6d4f6e 100644 --- a/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs +++ b/tests/FSharp.Compiler.Service.Tests/TooltipTests.fs @@ -390,29 +390,83 @@ c.Abc testToolTipSquashing source 7 5 "c.Abc" [ "c"; "Abc" ] FSharpTokenTag.Identifier -[] -let ``Auto property should display a single tool tip`` () = - let source = """ -namespace Foo - -/// Some comment on class -type Bar() = - /// Some comment on class member - member val Foo = "bla" with get, set -""" +let getCheckResults source options = let fileName, options = mkTestFileAndOptions source - Array.empty + options let _, checkResults = parseAndCheckFile fileName source options - let (ToolTipText(items)) = checkResults.GetToolTip(7, 18, " member val Foo = \"bla\" with get, set", [ "Foo" ], FSharpTokenTag.Identifier) - Assert.True (items.Length = 1) + checkResults + +let assertAndGetSingleToolTipText (ToolTipText(items)) = + Assert.Equal(1,items.Length) match items.[0] with | ToolTipElement.Group [ { MainDescription = description } ] -> let toolTipText = description |> Array.map (fun taggedText -> taggedText.Text) |> String.concat "" - - Assert.Equal("property Bar.Foo: string with get, set", toolTipText) + toolTipText | _ -> failwith $"Expected group, got {items.[0]}" + +let normalize (s:string) = s.Replace("\r\n", "\n").Replace("\n\n", "\n") + +[] +let ``Auto property should display a single tool tip`` () = + let source = """ +namespace Foo + +/// Some comment on class +type Bar() = + /// Some comment on class member + member val Foo = "bla" with get, set +""" + let checkResults = getCheckResults source Array.empty + checkResults.GetToolTip(7, 18, " member val Foo = \"bla\" with get, set", [ "Foo" ], FSharpTokenTag.Identifier) + |> assertAndGetSingleToolTipText + |> Assert.shouldBeEquivalentTo "property Bar.Foo: string with get, set" + +[] +let ``Should display nullable Csharp code analysis annotations on method argument`` () = + + let source = """module Foo +let exists() = System.IO.Path.Exists(null:string) +""" + let checkResults = getCheckResults source [|"--checknulls+";"--langversion:preview"|] + checkResults.GetToolTip(2, 36, "let exists() = System.IO.Path.Exists(null:string)", [ "Exists" ], FSharpTokenTag.Identifier) + |> assertAndGetSingleToolTipText + |> Assert.shouldBeEquivalentTo "System.IO.Path.Exists([] path: string | null) : bool" + + +[] +let ``Should display nullable Csharp code analysis annotations on method return type`` () = + + let source = """module Foo +let getPath() = System.IO.Path.GetFileName(null:string) +""" + let checkResults = getCheckResults source [|"--checknulls+";"--langversion:preview"|] + checkResults.GetToolTip(2, 42, "let getPath() = System.IO.Path.GetFileName(null:string)", [ "GetFileName" ], FSharpTokenTag.Identifier) + |> assertAndGetSingleToolTipText + |> Assert.shouldBeEquivalentTo ("""[] +System.IO.Path.GetFileName(path: string | null) : string | null""" |> normalize) + +[] +let ``Should display nullable Csharp code analysis annotations on TryParse pattern`` () = + let source = """module Foo +let success,version = System.Version.TryParse(null) +""" + let checkResults = getCheckResults source [|"--checknulls+";"--langversion:preview"|] + checkResults.GetToolTip(2, 45, "let success,version = System.Version.TryParse(null)", [ "TryParse" ], FSharpTokenTag.Identifier) + |> assertAndGetSingleToolTipText + |> Assert.shouldBeEquivalentTo ("""System.Version.TryParse([] input: string | null, [] result: byref) : bool""") + +[] +let ``Display with nullable annotations can be squashed`` () = + let source = """module Foo +let success,version = System.Version.TryParse(null) +""" + let checkResults = getCheckResults source [|"--checknulls+";"--langversion:preview"|] + checkResults.GetToolTip(2, 45, "let success,version = System.Version.TryParse(null)", [ "TryParse" ], FSharpTokenTag.Identifier,width=100) + |> assertAndGetSingleToolTipText + |> Assert.shouldBeEquivalentTo ("""System.Version.TryParse([] input: string | null, + [] result: byref) : bool""" |> normalize) \ No newline at end of file