Skip to content

Commit

Permalink
Add ConvertToAnonymousRecord quick fixeroony (#10493)
Browse files Browse the repository at this point in the history
  • Loading branch information
cartermp committed Nov 19, 2020
1 parent 5d486d4 commit abd9316
Show file tree
Hide file tree
Showing 20 changed files with 181 additions and 13 deletions.
33 changes: 21 additions & 12 deletions src/fsharp/service/ServiceUntypedParse.fs
Expand Up @@ -100,21 +100,30 @@ type FSharpParseFileResults(errors: FSharpErrorInfo[], input: ParsedInput option

member scope.ParseTree = input

member scope.TryRangeOfRecordExpressionContainingPos pos =
match input with
| Some input ->
AstTraversal.Traverse(pos, input, { new AstTraversal.AstVisitorBase<_>() with
member _.VisitExpr(_, _, defaultTraverse, expr) =
match expr with
| SynExpr.Record(_, _, _, range) when rangeContainsPos range pos ->
Some range
| _ -> defaultTraverse expr })
| None ->
None

member scope.TryRangeOfRefCellDereferenceContainingPos expressionPos =
match input with
| Some input ->
let res =
AstTraversal.Traverse(expressionPos, input, { new AstTraversal.AstVisitorBase<_>() with
member _.VisitExpr(_, _, defaultTraverse, expr) =
match expr with
| SynExpr.App(_, false, SynExpr.Ident funcIdent, expr, _) ->
if funcIdent.idText = "op_Dereference" && rangeContainsPos expr.Range expressionPos then
Some funcIdent.idRange
else
None
| _ -> defaultTraverse expr
})
res
AstTraversal.Traverse(expressionPos, input, { new AstTraversal.AstVisitorBase<_>() with
member _.VisitExpr(_, _, defaultTraverse, expr) =
match expr with
| SynExpr.App(_, false, SynExpr.Ident funcIdent, expr, _) ->
if funcIdent.idText = "op_Dereference" && rangeContainsPos expr.Range expressionPos then
Some funcIdent.idRange
else
None
| _ -> defaultTraverse expr })
| None ->
None

Expand Down
3 changes: 3 additions & 0 deletions src/fsharp/service/ServiceUntypedParse.fsi
Expand Up @@ -19,6 +19,9 @@ type public FSharpParseFileResults =
/// The syntax tree resulting from the parse
member ParseTree : ParsedInput option

/// Attempts to find the range of a record expression containing the given position.
member TryRangeOfRecordExpressionContainingPos: pos: pos -> Option<range>

/// <summary>
/// Given the position of an expression, attempts to find the range of the
/// '!' in a derefence operation of that expression, like:
Expand Down
Expand Up @@ -22752,6 +22752,7 @@ FSharp.Compiler.SourceCodeServices.FSharpParseFileResults: FSharp.Compiler.Sourc
FSharp.Compiler.SourceCodeServices.FSharpParseFileResults: FSharp.Compiler.SourceCodeServices.FSharpErrorInfo[] get_Errors()
FSharp.Compiler.SourceCodeServices.FSharpParseFileResults: FSharp.Compiler.SourceCodeServices.FSharpNavigationItems GetNavigationItems()
FSharp.Compiler.SourceCodeServices.FSharpParseFileResults: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Range+range] TryRangeOfRefCellDereferenceContainingPos(pos)
FSharp.Compiler.SourceCodeServices.FSharpParseFileResults: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Range+range] TryRangeOfRecordExpressionContainingPos(pos)
FSharp.Compiler.SourceCodeServices.FSharpParseFileResults: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Range+range] ValidateBreakpointLocation(pos)
FSharp.Compiler.SourceCodeServices.FSharpParseFileResults: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.SourceCodeServices.FSharpNoteworthyParamInfoLocations] FindNoteworthyParamInfoLocations(pos)
FSharp.Compiler.SourceCodeServices.FSharpParseFileResults: Boolean IsPositionContainedInACurriedParameter(pos)
Expand Down
26 changes: 25 additions & 1 deletion tests/service/ServiceUntypedParseTests.fs
Expand Up @@ -357,4 +357,28 @@ let y = !(x = false)
|> fst
|> shouldEqual (3, 8)
| None ->
Assert.Fail("No deref operator found in source.")
Assert.Fail("No deref operator found in source.")

[<Test>]
let ``TryRangeOfRecordExpressionContainingPos - contained``() =
let source = """
let x = { Name = "Hello" }
"""
let parseFileResults, _ = getParseAndCheckResults source
let res = parseFileResults.TryRangeOfRecordExpressionContainingPos (mkPos 2 10)
match res with
| Some res ->
res
|> tups
|> shouldEqual ((2, 8), (2, 26))
| None ->
Assert.Fail("No range of record found in source.")

[<Test>]
let ``TryRangeOfRecordExpressionContainingPos - not contained``() =
let source = """
let x = { Name = "Hello" }
"""
let parseFileResults, _ = getParseAndCheckResults source
let res = parseFileResults.TryRangeOfRecordExpressionContainingPos (mkPos 2 7)
Assert.True(res.IsNone, "Expected not to find a range.")
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace Microsoft.VisualStudio.FSharp.Editor

open System.Composition
open System.Threading
open System.Threading.Tasks

open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.CodeFixes
open Microsoft.CodeAnalysis.CodeActions

[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = "ConvertToAnonymousRecord"); Shared>]
type internal FSharpConvertToAnonymousRecordCodeFixProvider
[<ImportingConstructor>]
(
checkerProvider: FSharpCheckerProvider,
projectInfoManager: FSharpProjectOptionsManager
) =
inherit CodeFixProvider()

static let userOpName = "ConvertToAnonymousRecord"

let fixableDiagnosticIds = set ["FS0039"]

override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds

override _.RegisterCodeFixesAsync context : Task =
asyncMaybe {
let document = context.Document
let! parsingOptions, _ = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document, context.CancellationToken, userOpName)
let! sourceText = context.Document.GetTextAsync(context.CancellationToken)
let! parseResults = checkerProvider.Checker.ParseFile(document.FilePath, sourceText.ToFSharpSourceText(), parsingOptions, userOpName) |> liftAsync

let errorRange = RoslynHelpers.TextSpanToFSharpRange(document.FilePath, context.Span, sourceText)
let! recordRange = parseResults.TryRangeOfRecordExpressionContainingPos errorRange.Start
let! recordSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, recordRange)

let getChangedText () =
sourceText.WithChanges(TextChange(TextSpan(recordSpan.Start + 1, 0), "|"))
.WithChanges(TextChange(TextSpan(recordSpan.End, 0), "|"))

let diagnostics =
context.Diagnostics
|> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id)
|> Seq.toImmutableArray

let title = SR.ConvertToAnonymousRecord()

let codeFix =
CodeAction.Create(
title,
(fun (cancellationToken: CancellationToken) ->
async {
return context.Document.WithText(getChangedText())
} |> RoslynHelpers.StartAsyncAsTask(cancellationToken)),
title)

context.RegisterCodeFix(codeFix, diagnostics)
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
1 change: 1 addition & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Expand Up @@ -89,6 +89,7 @@
<Compile Include="Commands\FsiCommandService.fs" />
<Compile Include="Commands\XmlDocCommandService.fs" />
<Compile Include="CodeFix\CodeFixHelpers.fs" />
<Compile Include="CodeFix\ConvertToAnonymousRecord.fs" />
<Compile Include="CodeFix\UseMutationWhenValueIsMutable.fs" />
<Compile Include="CodeFix\MakeDeclarationMutable.fs" />
<Compile Include="CodeFix\ChangeToUpcast.fs" />
Expand Down
3 changes: 3 additions & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.resx
Expand Up @@ -243,6 +243,9 @@
<data name="ChangePrefixNegationToInfixSubtraction" xml:space="preserve">
<value>Use subtraction instead of negation</value>
</data>
<data name="ConvertToAnonymousRecord" xml:space="preserve">
<value>Convert to Anonymous Record</value>
</data>
<data name="UseMutationWhenValueIsMutable" xml:space="preserve">
<value>Use '&lt;-' to mutate value</value>
</data>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Přidejte klíčové slovo new.</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Schlüsselwort "new" hinzufügen</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Agregar "nueva" palabra clave</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Ajouter le mot clé 'new'</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Aggiungi la parola chiave 'new'</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">'new' キーワードを追加する</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">'new' 키워드 추가</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Dodaj słowo kluczowe „new”</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Adicionar a palavra-chave 'new'</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">Добавить ключевое слово "new"</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">'new' anahtar sözcüğünü ekleme</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">添加“新”关键字</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf
Expand Up @@ -12,6 +12,11 @@
<target state="translated">新增 'new' 關鍵字</target>
<note />
</trans-unit>
<trans-unit id="ConvertToAnonymousRecord">
<source>Convert to Anonymous Record</source>
<target state="new">Convert to Anonymous Record</target>
<note />
</trans-unit>
<trans-unit id="ConvertToSingleEqualsEqualityExpression">
<source>Use '=' for equality check</source>
<target state="new">Use '=' for equality check</target>
Expand Down

0 comments on commit abd9316

Please sign in to comment.