Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nullness :: Render C# code analysis attributes in tooltips #17485

Merged
merged 5 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/9.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 45 additions & 4 deletions src/Compiler/Checking/NicePrint.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 '[<attribs>]' above another block
and layoutAttribs denv startOpt isLiteral kind attrs restL =

Expand Down Expand Up @@ -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 =
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Facilities/TextLayoutRender.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Facilities/TextLayoutRender.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Compiler/Service/ServiceDeclarationLists.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/TypedTree/TypedTreeOps.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3145,6 +3145,7 @@ type DisplayEnv =
shortConstraints: bool
useColonForReturnType: bool
showAttributes: bool
showCsharpCodeAnalysisAttributes: bool
showOverrides: bool
showStaticallyResolvedTyparAnnotations: bool
showNullnessAnnotations: bool option
Expand Down Expand Up @@ -3180,6 +3181,7 @@ type DisplayEnv =
suppressMutableKeyword = false
showMemberContainers = false
showAttributes = false
showCsharpCodeAnalysisAttributes = false
showOverrides = true
showStaticallyResolvedTyparAnnotations = true
showNullnessAnnotations = None
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/TypedTree/TypedTreeOps.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,7 @@ type DisplayEnv =
shortConstraints: bool
useColonForReturnType: bool
showAttributes: bool
showCsharpCodeAnalysisAttributes: bool
showOverrides: bool
showStaticallyResolvedTyparAnnotations: bool
showNullnessAnnotations: bool option
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Utilities/sformat.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Utilities/sformat.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
84 changes: 69 additions & 15 deletions tests/FSharp.Compiler.Service.Tests/TooltipTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -390,29 +390,83 @@ c.Abc

testToolTipSquashing source 7 5 "c.Abc" [ "c"; "Abc" ] FSharpTokenTag.Identifier

[<Fact>]
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")

[<Fact>]
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"

[<FactForNETCOREAPP>]
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([<NotNullWhenAttribute (true)>] path: string | null) : bool"


[<FactForNETCOREAPP>]
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 ("""[<return:NotNullIfNotNullAttribute ("path")>]
System.IO.Path.GetFileName(path: string | null) : string | null""" |> normalize)

[<FactForNETCOREAPP>]
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([<NotNullWhenAttribute (true)>] input: string | null, [<NotNullWhenAttribute (true)>] result: byref<System.Version | null>) : bool""")

[<FactForNETCOREAPP>]
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([<NotNullWhenAttribute (true)>] input: string | null,
[<NotNullWhenAttribute (true)>] result: byref<System.Version | null>) : bool""" |> normalize)
Loading