Skip to content

Commit ed4263f

Browse files
authored
Nullness :: Render C# code analysis attributes in tooltips (#17485)
1 parent a0f1e31 commit ed4263f

File tree

10 files changed

+123
-20
lines changed

10 files changed

+123
-20
lines changed

docs/release-notes/.FSharp.Compiler.Service/9.0.100.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* Parser: recover on missing union case fields (PR [#17452](https://github.com/dotnet/fsharp/pull/17452))
1616
* Parser: recover on missing union case field types (PR [#17455](https://github.com/dotnet/fsharp/pull/17455))
1717
* Sink: report function domain type ([PR #17470](https://github.com/dotnet/fsharp/pull/17470))
18+
* Render C# nullable-analysis attributes in tooltips ([PR #17485](https://github.com/dotnet/fsharp/pull/17485))
1819

1920
### Changed
2021

src/Compiler/Checking/NicePrint.fs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ module internal PrintUtilities =
3838

3939
let squareAngleL x = LeftL.leftBracketAngle ^^ x ^^ RightL.rightBracketAngle
4040

41+
let squareAngleReturn x = LeftL.leftBracketAngle ^^ WordL.keywordReturn ^^ SepL.colon ^^ x ^^ RightL.rightBracketAngle
42+
4143
let angleL x = SepL.leftAngle ^^ x ^^ RightL.rightAngle
4244

4345
let braceL x = wordL leftBrace ^^ x ^^ wordL rightBrace
@@ -638,6 +640,23 @@ module PrintTypes =
638640
let argsL = bracketL (sepListL RightL.comma (List.map (layoutILAttribElement denv) args))
639641
PrintIL.layoutILType denv [] ty ++ argsL
640642

643+
/// Layout nullness attributes for C# flow-analysis
644+
/// F# does not process them, this way we can at least show them.
645+
and layoutCsharpCodeAnalysisIlAttributes denv (attrs:ILAttributes) (layoutCombinator: Layout -> Layout -> Layout) restL =
646+
let denvShortNames() = { denv with shortTypeNames = true }
647+
let attrsL =
648+
[ for a in attrs.AsArray() do
649+
let name = a.Method.DeclaringType.BasicQualifiedName
650+
if name.StartsWith("System.Diagnostics.CodeAnalysis") then
651+
let parms, _args = decodeILAttribData a
652+
layoutILAttrib (denvShortNames()) (a.Method.DeclaringType, parms)
653+
]
654+
match attrsL with
655+
| [] -> restL
656+
| _ ->
657+
let separated = sepListL RightL.semicolon attrsL
658+
layoutCombinator separated restL
659+
641660
/// Layout '[<attribs>]' above another block
642661
and layoutAttribs denv startOpt isLiteral kind attrs restL =
643662

@@ -1638,11 +1657,33 @@ module InfoMemberPrinting =
16381657
let idL = ConvertValLogicalNameToDisplayLayout false (tagMethod >> tagNavArbValRef minfo.ArbitraryValRef >> wordL) minfo.LogicalName
16391658
SepL.dot ^^
16401659
PrintTypes.layoutTyparDecls denv idL true minfo.FormalMethodTypars ^^
1641-
SepL.leftParen
1660+
SepL.leftParen
1661+
1662+
let layout,paramLayouts =
1663+
match denv.showCsharpCodeAnalysisAttributes, minfo with
1664+
| true, ILMeth(_g,mi,_e) ->
1665+
let methodLayout =
1666+
// Render Method attributes and [return:..] attributes on separate lines above (@@) the method definition
1667+
PrintTypes.layoutCsharpCodeAnalysisIlAttributes denv (minfo.GetCustomAttrs()) (squareAngleL >> (@@)) layout
1668+
|> PrintTypes.layoutCsharpCodeAnalysisIlAttributes denv (mi.RawMetadata.Return.CustomAttrs) (squareAngleReturn >> (@@))
1669+
let paramLayouts =
1670+
minfo.GetParamDatas (amap, m, minst)
1671+
|> List.head
1672+
|> List.zip (mi.ParamMetadata)
1673+
|> List.map(fun (ilParams,paramData) ->
1674+
layoutParamData denv paramData
1675+
// Render parameter attributes next to (^^) the parameter definition
1676+
|> PrintTypes.layoutCsharpCodeAnalysisIlAttributes denv (ilParams.CustomAttrs) (squareAngleL >> (^^)) )
1677+
methodLayout,paramLayouts
1678+
| _ ->
1679+
layout,
1680+
minfo.GetParamDatas (amap, m, minst)
1681+
|> List.concat
1682+
|> List.map (layoutParamData denv)
1683+
16421684

1643-
let paramDatas = minfo.GetParamDatas (amap, m, minst)
1644-
let layout = layout ^^ sepListL RightL.comma ((List.concat >> List.map (layoutParamData denv)) paramDatas)
1645-
layout ^^ RightL.rightParen ^^ WordL.colon ^^ PrintTypes.layoutType denv retTy
1685+
let layout = layout ^^ sepListL RightL.comma paramLayouts
1686+
layout ^^ RightL.rightParen ^^ WordL.colon ^^ PrintTypes.layoutType denv retTy // Todo enrich return type
16461687

16471688
// Prettify an ILMethInfo
16481689
let prettifyILMethInfo (amap: Import.ImportMap) m (minfo: MethInfo) typarInst ilMethInfo =

src/Compiler/Facilities/TextLayoutRender.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ module WordL =
6363
let leftAngle = wordL TaggedText.leftAngle
6464
let keywordModule = wordL TaggedText.keywordModule
6565
let keywordNamespace = wordL TaggedText.keywordNamespace
66+
let keywordReturn = wordL TaggedText.keywordReturn
6667

6768
module LeftL =
6869
let leftParen = leftL TaggedText.leftParen

src/Compiler/Facilities/TextLayoutRender.fsi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ module internal WordL =
102102
val leftAngle: Layout
103103
val keywordModule: Layout
104104
val keywordNamespace: Layout
105+
val keywordReturn: Layout
105106

106107
module internal LeftL =
107108
val leftParen: Layout

src/Compiler/Service/ServiceDeclarationLists.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ module DeclarationListHelpers =
157157
let rec FormatItemDescriptionToToolTipElement displayFullName (infoReader: InfoReader) ad m denv (item: ItemWithInst) symbol (width: int option) =
158158
let g = infoReader.g
159159
let amap = infoReader.amap
160-
let denv = SimplerDisplayEnv denv
160+
let denv = {SimplerDisplayEnv denv with showCsharpCodeAnalysisAttributes = true }
161161
let xml = GetXmlCommentForItem infoReader m item.Item
162162

163163
match item.Item with

src/Compiler/TypedTree/TypedTreeOps.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3145,6 +3145,7 @@ type DisplayEnv =
31453145
shortConstraints: bool
31463146
useColonForReturnType: bool
31473147
showAttributes: bool
3148+
showCsharpCodeAnalysisAttributes: bool
31483149
showOverrides: bool
31493150
showStaticallyResolvedTyparAnnotations: bool
31503151
showNullnessAnnotations: bool option
@@ -3180,6 +3181,7 @@ type DisplayEnv =
31803181
suppressMutableKeyword = false
31813182
showMemberContainers = false
31823183
showAttributes = false
3184+
showCsharpCodeAnalysisAttributes = false
31833185
showOverrides = true
31843186
showStaticallyResolvedTyparAnnotations = true
31853187
showNullnessAnnotations = None

src/Compiler/TypedTree/TypedTreeOps.fsi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,7 @@ type DisplayEnv =
10791079
shortConstraints: bool
10801080
useColonForReturnType: bool
10811081
showAttributes: bool
1082+
showCsharpCodeAnalysisAttributes: bool
10821083
showOverrides: bool
10831084
showStaticallyResolvedTyparAnnotations: bool
10841085
showNullnessAnnotations: bool option

src/Compiler/Utilities/sformat.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ module TaggedText =
262262
let keywordInline = tagKeyword "inline"
263263
let keywordModule = tagKeyword "module"
264264
let keywordNamespace = tagKeyword "namespace"
265+
let keywordReturn = tagKeyword "return"
265266
let punctuationUnit = tagPunctuation "()"
266267
#endif
267268

src/Compiler/Utilities/sformat.fsi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ module internal TaggedText =
213213
val internal keywordInline: TaggedText
214214
val internal keywordModule: TaggedText
215215
val internal keywordNamespace: TaggedText
216+
val internal keywordReturn: TaggedText
216217
val internal punctuationUnit: TaggedText
217218

218219
type internal IEnvironment =

tests/FSharp.Compiler.Service.Tests/TooltipTests.fs

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -390,29 +390,83 @@ c.Abc
390390

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

393-
[<Fact>]
394-
let ``Auto property should display a single tool tip`` () =
395-
let source = """
396-
namespace Foo
397-
398-
/// Some comment on class
399-
type Bar() =
400-
/// Some comment on class member
401-
member val Foo = "bla" with get, set
402-
"""
393+
let getCheckResults source options =
403394
let fileName, options =
404395
mkTestFileAndOptions
405396
source
406-
Array.empty
397+
options
407398
let _, checkResults = parseAndCheckFile fileName source options
408-
let (ToolTipText(items)) = checkResults.GetToolTip(7, 18, " member val Foo = \"bla\" with get, set", [ "Foo" ], FSharpTokenTag.Identifier)
409-
Assert.True (items.Length = 1)
399+
checkResults
400+
401+
let assertAndGetSingleToolTipText (ToolTipText(items)) =
402+
Assert.Equal(1,items.Length)
410403
match items.[0] with
411404
| ToolTipElement.Group [ { MainDescription = description } ] ->
412405
let toolTipText =
413406
description
414407
|> Array.map (fun taggedText -> taggedText.Text)
415408
|> String.concat ""
416-
417-
Assert.Equal("property Bar.Foo: string with get, set", toolTipText)
409+
toolTipText
418410
| _ -> failwith $"Expected group, got {items.[0]}"
411+
412+
let normalize (s:string) = s.Replace("\r\n", "\n").Replace("\n\n", "\n")
413+
414+
[<Fact>]
415+
let ``Auto property should display a single tool tip`` () =
416+
let source = """
417+
namespace Foo
418+
419+
/// Some comment on class
420+
type Bar() =
421+
/// Some comment on class member
422+
member val Foo = "bla" with get, set
423+
"""
424+
let checkResults = getCheckResults source Array.empty
425+
checkResults.GetToolTip(7, 18, " member val Foo = \"bla\" with get, set", [ "Foo" ], FSharpTokenTag.Identifier)
426+
|> assertAndGetSingleToolTipText
427+
|> Assert.shouldBeEquivalentTo "property Bar.Foo: string with get, set"
428+
429+
[<FactForNETCOREAPP>]
430+
let ``Should display nullable Csharp code analysis annotations on method argument`` () =
431+
432+
let source = """module Foo
433+
let exists() = System.IO.Path.Exists(null:string)
434+
"""
435+
let checkResults = getCheckResults source [|"--checknulls+";"--langversion:preview"|]
436+
checkResults.GetToolTip(2, 36, "let exists() = System.IO.Path.Exists(null:string)", [ "Exists" ], FSharpTokenTag.Identifier)
437+
|> assertAndGetSingleToolTipText
438+
|> Assert.shouldBeEquivalentTo "System.IO.Path.Exists([<NotNullWhenAttribute (true)>] path: string | null) : bool"
439+
440+
441+
[<FactForNETCOREAPP>]
442+
let ``Should display nullable Csharp code analysis annotations on method return type`` () =
443+
444+
let source = """module Foo
445+
let getPath() = System.IO.Path.GetFileName(null:string)
446+
"""
447+
let checkResults = getCheckResults source [|"--checknulls+";"--langversion:preview"|]
448+
checkResults.GetToolTip(2, 42, "let getPath() = System.IO.Path.GetFileName(null:string)", [ "GetFileName" ], FSharpTokenTag.Identifier)
449+
|> assertAndGetSingleToolTipText
450+
|> Assert.shouldBeEquivalentTo ("""[<return:NotNullIfNotNullAttribute ("path")>]
451+
System.IO.Path.GetFileName(path: string | null) : string | null""" |> normalize)
452+
453+
[<FactForNETCOREAPP>]
454+
let ``Should display nullable Csharp code analysis annotations on TryParse pattern`` () =
455+
let source = """module Foo
456+
let success,version = System.Version.TryParse(null)
457+
"""
458+
let checkResults = getCheckResults source [|"--checknulls+";"--langversion:preview"|]
459+
checkResults.GetToolTip(2, 45, "let success,version = System.Version.TryParse(null)", [ "TryParse" ], FSharpTokenTag.Identifier)
460+
|> assertAndGetSingleToolTipText
461+
|> Assert.shouldBeEquivalentTo ("""System.Version.TryParse([<NotNullWhenAttribute (true)>] input: string | null, [<NotNullWhenAttribute (true)>] result: byref<System.Version | null>) : bool""")
462+
463+
[<FactForNETCOREAPP>]
464+
let ``Display with nullable annotations can be squashed`` () =
465+
let source = """module Foo
466+
let success,version = System.Version.TryParse(null)
467+
"""
468+
let checkResults = getCheckResults source [|"--checknulls+";"--langversion:preview"|]
469+
checkResults.GetToolTip(2, 45, "let success,version = System.Version.TryParse(null)", [ "TryParse" ], FSharpTokenTag.Identifier,width=100)
470+
|> assertAndGetSingleToolTipText
471+
|> Assert.shouldBeEquivalentTo ("""System.Version.TryParse([<NotNullWhenAttribute (true)>] input: string | null,
472+
[<NotNullWhenAttribute (true)>] result: byref<System.Version | null>) : bool""" |> normalize)

0 commit comments

Comments
 (0)