Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ namespace Microsoft.VisualStudio.FSharp.Editor

open System
open System.Composition
open System.Threading.Tasks
open System.Threading
open System.Collections.Immutable

open Microsoft.CodeAnalysis.Text
Expand All @@ -14,6 +12,7 @@ open Microsoft.CodeAnalysis.CodeFixes
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Text
open FSharp.Compiler.Symbols

open CancellableTasks

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

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

override _.RegisterCodeFixesAsync context : Task =
asyncMaybe {

let document = context.Document
let position = context.Span.Start

let! sourceText = document.GetTextAsync(context.CancellationToken)
let textLine = sourceText.Lines.GetLineFromPosition position
let textLinePos = sourceText.Lines.GetLinePosition position
let fcsTextLineNumber = Line.fromZ textLinePos.Line

let! lexerSymbol =
document.TryFindFSharpLexerSymbolAsync(
position,
SymbolLookupKind.Greedy,
false,
false,
nameof (AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider)
)

let! _, checkFileResults =
document.GetFSharpParseAndCheckResultsAsync(nameof (AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider))
|> CancellableTask.start context.CancellationToken
|> Async.AwaitTask
|> liftAsync

let decl =
checkFileResults.GetDeclarationLocation(
fcsTextLineNumber,
lexerSymbol.Ident.idRange.EndColumn,
textLine.ToString(),
lexerSymbol.FullIsland,
false
)

match decl with
| FindDeclResult.DeclFound declRange when declRange.FileName = document.FilePath ->
let! declSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, declRange)
let declTextLine = sourceText.Lines.GetLineFromPosition declSpan.Start

let! symbolUse =
checkFileResults.GetSymbolUseAtLocation(
declRange.StartLine,
declRange.EndColumn,
declTextLine.ToString(),
lexerSymbol.FullIsland
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this

interface IFSharpCodeFixProvider with
member _.GetCodeFixIfAppliesAsync context =
cancellableTask {
let document = context.Document
let position = context.Span.Start

let! lexerSymbolOpt =
document.TryFindFSharpLexerSymbolAsync(
position,
SymbolLookupKind.Greedy,
false,
false,
nameof AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider
)

match symbolUse.Symbol with
| :? FSharpMemberOrFunctionOrValue as mfv ->
let typeString = mfv.FullType.FormatWithConstraints symbolUse.DisplayContext

let alreadyWrappedInParens =
let rec leftLoop ch pos =
if not (Char.IsWhiteSpace(ch)) then
ch = '('
else
leftLoop sourceText.[pos - 1] (pos - 1)

let rec rightLoop ch pos =
if not (Char.IsWhiteSpace(ch)) then
ch = ')'
else
rightLoop sourceText.[pos + 1] (pos + 1)

let hasLeftParen = leftLoop sourceText.[declSpan.Start - 1] (declSpan.Start - 1)
let hasRightParen = rightLoop sourceText.[declSpan.End] declSpan.End
hasLeftParen && hasRightParen

let changes =
[
if alreadyWrappedInParens then
TextChange(TextSpan(declSpan.End, 0), ": " + typeString)
else
TextChange(TextSpan(declSpan.Start, 0), "(")
TextChange(TextSpan(declSpan.End + 1, 0), ": " + typeString + ")")
]

context.RegisterFsharpFix(CodeFix.AddTypeAnnotationToObjectOfIndeterminateType, title, changes)

| _ -> ()
| _ -> ()
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
match lexerSymbolOpt with
| None -> return ValueNone
| Some lexerSymbol ->
let! sourceText = context.GetSourceTextAsync()
let textLine = sourceText.Lines.GetLineFromPosition position
let textLinePos = sourceText.Lines.GetLinePosition position
let fcsTextLineNumber = Line.fromZ textLinePos.Line

let! _, checkFileResults =
document.GetFSharpParseAndCheckResultsAsync(nameof AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider)

let decl =
checkFileResults.GetDeclarationLocation(
fcsTextLineNumber,
lexerSymbol.Ident.idRange.EndColumn,
textLine.ToString(),
lexerSymbol.FullIsland,
false
)

match decl with
| FindDeclResult.DeclFound declRange when declRange.FileName = document.FilePath ->
let declSpan = RoslynHelpers.FSharpRangeToTextSpan(sourceText, declRange)
let declTextLine = sourceText.Lines.GetLineFromPosition declSpan.Start

let symbolUseOpt =
checkFileResults.GetSymbolUseAtLocation(
declRange.StartLine,
declRange.EndColumn,
declTextLine.ToString(),
lexerSymbol.FullIsland
)

match symbolUseOpt with
| None -> return ValueNone
| Some symbolUse ->
match symbolUse.Symbol with
| :? FSharpMemberOrFunctionOrValue as mfv when not mfv.FullType.IsGenericParameter ->
let typeString = mfv.FullType.FormatWithConstraints symbolUse.DisplayContext

let alreadyWrappedInParens =
let rec leftLoop ch pos =
if not (Char.IsWhiteSpace(ch)) then
ch = '('
else
leftLoop sourceText[pos - 1] (pos - 1)

let rec rightLoop ch pos =
if not (Char.IsWhiteSpace(ch)) then
ch = ')'
else
rightLoop sourceText[pos + 1] (pos + 1)

let hasLeftParen = leftLoop sourceText[declSpan.Start - 1] (declSpan.Start - 1)
let hasRightParen = rightLoop sourceText[declSpan.End] declSpan.End
hasLeftParen && hasRightParen

let changes =
[
if alreadyWrappedInParens then
TextChange(TextSpan(declSpan.End, 0), ": " + typeString)
else
TextChange(TextSpan(declSpan.Start, 0), "(")
TextChange(TextSpan(declSpan.End, 0), ": " + typeString + ")")
]

return
ValueSome
{
Name = CodeFix.AddTypeAnnotationToObjectOfIndeterminateType
Message = title
Changes = changes
}

| _ -> return ValueNone

| _ -> return ValueNone
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module FSharp.Editor.Tests.CodeFixes.AddTypeAnnotationToObjectOfIndeterminateTypeTests

open Microsoft.VisualStudio.FSharp.Editor
open Xunit

open CodeFixTestFramework

let private codeFix = AddTypeAnnotationToObjectOfIndeterminateTypeFixProvider()

[<Theory>]
[<InlineData("", "")>]
[<InlineData("(", ")")>]
let ``Fixes FS0072`` leftParen rightParen =
let code =
$"""
let db =
[
{{| Name = "Liam"; Id = 2 |}}
{{| Name = "Noel"; Id = 3 |}}
]

let f = List.filter (fun {leftParen}x{rightParen} -> x.Id = 7) db
"""

let expected =
Some
{
Message = "Add type annotation"
FixedCode =
"""
let db =
[
{| Name = "Liam"; Id = 2 |}
{| Name = "Noel"; Id = 3 |}
]

let f = List.filter (fun (x: {| Id: int; Name: string |}) -> x.Id = 7) db
"""
}

let actual = codeFix |> tryFix code Auto

Assert.Equal(expected, actual)

[<Fact>]
let ``Doesn't fix FS0072 for generic infered types`` () =
let code =
"""
let f x =
x.IsSpecial
"""

let expected = None

let actual = codeFix |> tryFix code Auto

Assert.Equal(expected, actual)

[<Theory>]
[<InlineData("", "")>]
[<InlineData("(", ")")>]
let ``Fixes FS3245`` leftParen rightParen =
let code =
$"""
let count numbers =
numbers
|> List.fold
(fun {leftParen}s{rightParen} _ -> {{| s with Count = s.Count + 1 |}})
{{| Count = 0 |}}
"""

let expected =
Some
{
Message = "Add type annotation"
FixedCode =
"""
let count numbers =
numbers
|> List.fold
(fun (s: {| Count: int |}) _ -> {| s with Count = s.Count + 1 |})
{| Count = 0 |}
"""
}

let actual = codeFix |> tryFix code Auto

Assert.Equal(expected, actual)
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
<Compile Include="CodeFixes\ConvertToNotEqualsEqualityExpressionTests.fs" />
<Compile Include="CodeFixes\ConvertToSingleEqualsEqualityExpressionTests.fs" />
<Compile Include="CodeFixes\AddMissingEqualsToTypeDefinitionTests.fs" />
<Compile Include="CodeFixes\AddTypeAnnotationToObjectOfIndeterminateTypeTests.fs" />
<Compile Include="CodeFixes\ChangeRefCellDerefToNotExpressionTests.fs" />
<Compile Include="CodeFixes\ChangePrefixNegationToInfixSubtractionTests.fs" />
<Compile Include="CodeFixes\AddNewKeywordToDisposableConstructorInvocationTests.fs" />
Expand Down