Skip to content

Commit 8be2f03

Browse files
authored
Fix the type annotation code fix (#15812)
1 parent 0dfd880 commit 8be2f03

File tree

3 files changed

+185
-85
lines changed

3 files changed

+185
-85
lines changed

vsintegration/src/FSharp.Editor/CodeFixes/AddTypeAnnotationToObjectOfIndeterminateType.fs

Lines changed: 94 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ namespace Microsoft.VisualStudio.FSharp.Editor
44

55
open System
66
open System.Composition
7-
open System.Threading.Tasks
8-
open System.Threading
97
open System.Collections.Immutable
108

119
open Microsoft.CodeAnalysis.Text
@@ -14,6 +12,7 @@ open Microsoft.CodeAnalysis.CodeFixes
1412
open FSharp.Compiler.EditorServices
1513
open FSharp.Compiler.Text
1614
open FSharp.Compiler.Symbols
15+
1716
open CancellableTasks
1817

1918
[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.AddTypeAnnotationToObjectOfIndeterminateType); Shared>]
@@ -24,88 +23,98 @@ type internal AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider [<Importin
2423

2524
override _.FixableDiagnosticIds = ImmutableArray.Create("FS0072", "FS3245")
2625

27-
override _.RegisterCodeFixesAsync context : Task =
28-
asyncMaybe {
29-
30-
let document = context.Document
31-
let position = context.Span.Start
32-
33-
let! sourceText = document.GetTextAsync(context.CancellationToken)
34-
let textLine = sourceText.Lines.GetLineFromPosition position
35-
let textLinePos = sourceText.Lines.GetLinePosition position
36-
let fcsTextLineNumber = Line.fromZ textLinePos.Line
37-
38-
let! lexerSymbol =
39-
document.TryFindFSharpLexerSymbolAsync(
40-
position,
41-
SymbolLookupKind.Greedy,
42-
false,
43-
false,
44-
nameof (AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider)
45-
)
46-
47-
let! _, checkFileResults =
48-
document.GetFSharpParseAndCheckResultsAsync(nameof (AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider))
49-
|> CancellableTask.start context.CancellationToken
50-
|> Async.AwaitTask
51-
|> liftAsync
52-
53-
let decl =
54-
checkFileResults.GetDeclarationLocation(
55-
fcsTextLineNumber,
56-
lexerSymbol.Ident.idRange.EndColumn,
57-
textLine.ToString(),
58-
lexerSymbol.FullIsland,
59-
false
60-
)
61-
62-
match decl with
63-
| FindDeclResult.DeclFound declRange when declRange.FileName = document.FilePath ->
64-
let! declSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, declRange)
65-
let declTextLine = sourceText.Lines.GetLineFromPosition declSpan.Start
66-
67-
let! symbolUse =
68-
checkFileResults.GetSymbolUseAtLocation(
69-
declRange.StartLine,
70-
declRange.EndColumn,
71-
declTextLine.ToString(),
72-
lexerSymbol.FullIsland
26+
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this
27+
28+
interface IFSharpCodeFixProvider with
29+
member _.GetCodeFixIfAppliesAsync context =
30+
cancellableTask {
31+
let document = context.Document
32+
let position = context.Span.Start
33+
34+
let! lexerSymbolOpt =
35+
document.TryFindFSharpLexerSymbolAsync(
36+
position,
37+
SymbolLookupKind.Greedy,
38+
false,
39+
false,
40+
nameof AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider
7341
)
7442

75-
match symbolUse.Symbol with
76-
| :? FSharpMemberOrFunctionOrValue as mfv ->
77-
let typeString = mfv.FullType.FormatWithConstraints symbolUse.DisplayContext
78-
79-
let alreadyWrappedInParens =
80-
let rec leftLoop ch pos =
81-
if not (Char.IsWhiteSpace(ch)) then
82-
ch = '('
83-
else
84-
leftLoop sourceText.[pos - 1] (pos - 1)
85-
86-
let rec rightLoop ch pos =
87-
if not (Char.IsWhiteSpace(ch)) then
88-
ch = ')'
89-
else
90-
rightLoop sourceText.[pos + 1] (pos + 1)
91-
92-
let hasLeftParen = leftLoop sourceText.[declSpan.Start - 1] (declSpan.Start - 1)
93-
let hasRightParen = rightLoop sourceText.[declSpan.End] declSpan.End
94-
hasLeftParen && hasRightParen
95-
96-
let changes =
97-
[
98-
if alreadyWrappedInParens then
99-
TextChange(TextSpan(declSpan.End, 0), ": " + typeString)
100-
else
101-
TextChange(TextSpan(declSpan.Start, 0), "(")
102-
TextChange(TextSpan(declSpan.End + 1, 0), ": " + typeString + ")")
103-
]
104-
105-
context.RegisterFsharpFix(CodeFix.AddTypeAnnotationToObjectOfIndeterminateType, title, changes)
106-
107-
| _ -> ()
108-
| _ -> ()
109-
}
110-
|> Async.Ignore
111-
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
43+
match lexerSymbolOpt with
44+
| None -> return ValueNone
45+
| Some lexerSymbol ->
46+
let! sourceText = context.GetSourceTextAsync()
47+
let textLine = sourceText.Lines.GetLineFromPosition position
48+
let textLinePos = sourceText.Lines.GetLinePosition position
49+
let fcsTextLineNumber = Line.fromZ textLinePos.Line
50+
51+
let! _, checkFileResults =
52+
document.GetFSharpParseAndCheckResultsAsync(nameof AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider)
53+
54+
let decl =
55+
checkFileResults.GetDeclarationLocation(
56+
fcsTextLineNumber,
57+
lexerSymbol.Ident.idRange.EndColumn,
58+
textLine.ToString(),
59+
lexerSymbol.FullIsland,
60+
false
61+
)
62+
63+
match decl with
64+
| FindDeclResult.DeclFound declRange when declRange.FileName = document.FilePath ->
65+
let declSpan = RoslynHelpers.FSharpRangeToTextSpan(sourceText, declRange)
66+
let declTextLine = sourceText.Lines.GetLineFromPosition declSpan.Start
67+
68+
let symbolUseOpt =
69+
checkFileResults.GetSymbolUseAtLocation(
70+
declRange.StartLine,
71+
declRange.EndColumn,
72+
declTextLine.ToString(),
73+
lexerSymbol.FullIsland
74+
)
75+
76+
match symbolUseOpt with
77+
| None -> return ValueNone
78+
| Some symbolUse ->
79+
match symbolUse.Symbol with
80+
| :? FSharpMemberOrFunctionOrValue as mfv when not mfv.FullType.IsGenericParameter ->
81+
let typeString = mfv.FullType.FormatWithConstraints symbolUse.DisplayContext
82+
83+
let alreadyWrappedInParens =
84+
let rec leftLoop ch pos =
85+
if not (Char.IsWhiteSpace(ch)) then
86+
ch = '('
87+
else
88+
leftLoop sourceText[pos - 1] (pos - 1)
89+
90+
let rec rightLoop ch pos =
91+
if not (Char.IsWhiteSpace(ch)) then
92+
ch = ')'
93+
else
94+
rightLoop sourceText[pos + 1] (pos + 1)
95+
96+
let hasLeftParen = leftLoop sourceText[declSpan.Start - 1] (declSpan.Start - 1)
97+
let hasRightParen = rightLoop sourceText[declSpan.End] declSpan.End
98+
hasLeftParen && hasRightParen
99+
100+
let changes =
101+
[
102+
if alreadyWrappedInParens then
103+
TextChange(TextSpan(declSpan.End, 0), ": " + typeString)
104+
else
105+
TextChange(TextSpan(declSpan.Start, 0), "(")
106+
TextChange(TextSpan(declSpan.End, 0), ": " + typeString + ")")
107+
]
108+
109+
return
110+
ValueSome
111+
{
112+
Name = CodeFix.AddTypeAnnotationToObjectOfIndeterminateType
113+
Message = title
114+
Changes = changes
115+
}
116+
117+
| _ -> return ValueNone
118+
119+
| _ -> return ValueNone
120+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
module FSharp.Editor.Tests.CodeFixes.AddTypeAnnotationToObjectOfIndeterminateTypeTests
4+
5+
open Microsoft.VisualStudio.FSharp.Editor
6+
open Xunit
7+
8+
open CodeFixTestFramework
9+
10+
let private codeFix = AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider()
11+
12+
[<Theory>]
13+
[<InlineData("", "")>]
14+
[<InlineData("(", ")")>]
15+
let ``Fixes FS0072`` leftParen rightParen =
16+
let code =
17+
$"""
18+
let db =
19+
[
20+
{{| Name = "Liam"; Id = 2 |}}
21+
{{| Name = "Noel"; Id = 3 |}}
22+
]
23+
24+
let f = List.filter (fun {leftParen}x{rightParen} -> x.Id = 7) db
25+
"""
26+
27+
let expected =
28+
Some
29+
{
30+
Message = "Add type annotation"
31+
FixedCode =
32+
"""
33+
let db =
34+
[
35+
{| Name = "Liam"; Id = 2 |}
36+
{| Name = "Noel"; Id = 3 |}
37+
]
38+
39+
let f = List.filter (fun (x: {| Id: int; Name: string |}) -> x.Id = 7) db
40+
"""
41+
}
42+
43+
let actual = codeFix |> tryFix code Auto
44+
45+
Assert.Equal(expected, actual)
46+
47+
[<Fact>]
48+
let ``Doesn't fix FS0072 for generic infered types`` () =
49+
let code =
50+
"""
51+
let f x =
52+
x.IsSpecial
53+
"""
54+
55+
let expected = None
56+
57+
let actual = codeFix |> tryFix code Auto
58+
59+
Assert.Equal(expected, actual)
60+
61+
[<Theory>]
62+
[<InlineData("", "")>]
63+
[<InlineData("(", ")")>]
64+
let ``Fixes FS3245`` leftParen rightParen =
65+
let code =
66+
$"""
67+
let count numbers =
68+
numbers
69+
|> List.fold
70+
(fun {leftParen}s{rightParen} _ -> {{| s with Count = s.Count + 1 |}})
71+
{{| Count = 0 |}}
72+
"""
73+
74+
let expected =
75+
Some
76+
{
77+
Message = "Add type annotation"
78+
FixedCode =
79+
"""
80+
let count numbers =
81+
numbers
82+
|> List.fold
83+
(fun (s: {| Count: int |}) _ -> {| s with Count = s.Count + 1 |})
84+
{| Count = 0 |}
85+
"""
86+
}
87+
88+
let actual = codeFix |> tryFix code Auto
89+
90+
Assert.Equal(expected, actual)

vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<Compile Include="CodeFixes\ConvertToNotEqualsEqualityExpressionTests.fs" />
4848
<Compile Include="CodeFixes\ConvertToSingleEqualsEqualityExpressionTests.fs" />
4949
<Compile Include="CodeFixes\AddMissingEqualsToTypeDefinitionTests.fs" />
50+
<Compile Include="CodeFixes\AddTypeAnnotationToObjectOfIndeterminateTypeTests.fs" />
5051
<Compile Include="CodeFixes\ChangeRefCellDerefToNotExpressionTests.fs" />
5152
<Compile Include="CodeFixes\ChangePrefixNegationToInfixSubtractionTests.fs" />
5253
<Compile Include="CodeFixes\AddNewKeywordToDisposableConstructorInvocationTests.fs" />

0 commit comments

Comments
 (0)