From 51ae2bca56b7645adee1508b4b4f031cf30ea20e Mon Sep 17 00:00:00 2001 From: Phillip Carter Date: Wed, 16 Dec 2020 15:32:02 -0800 Subject: [PATCH] Add MakeOuterBindingRecursive code fix (#10666) * Add MakeOuterBindingRecursive code fix * Allow other bindings to come before a nested should-be-recursive binding * more fixy * formatting and comment * Add to service layer * Area and add name to message --- src/fsharp/service/ServiceUntypedParse.fs | 53 +++++++++++++ src/fsharp/service/ServiceUntypedParse.fsi | 3 + .../SurfaceArea.netstandard.fs | 1 + tests/service/ServiceUntypedParseTests.fs | 74 +++++++++++++++++++ .../CodeFix/MakeOuterBindingRecursive.fs | 56 ++++++++++++++ .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + .../src/FSharp.Editor/FSharp.Editor.resx | 3 + .../FSharp.Editor/xlf/FSharp.Editor.cs.xlf | 5 ++ .../FSharp.Editor/xlf/FSharp.Editor.de.xlf | 5 ++ .../FSharp.Editor/xlf/FSharp.Editor.es.xlf | 5 ++ .../FSharp.Editor/xlf/FSharp.Editor.fr.xlf | 5 ++ .../FSharp.Editor/xlf/FSharp.Editor.it.xlf | 5 ++ .../FSharp.Editor/xlf/FSharp.Editor.ja.xlf | 5 ++ .../FSharp.Editor/xlf/FSharp.Editor.ko.xlf | 5 ++ .../FSharp.Editor/xlf/FSharp.Editor.pl.xlf | 5 ++ .../FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf | 5 ++ .../FSharp.Editor/xlf/FSharp.Editor.ru.xlf | 5 ++ .../FSharp.Editor/xlf/FSharp.Editor.tr.xlf | 5 ++ .../xlf/FSharp.Editor.zh-Hans.xlf | 5 ++ .../xlf/FSharp.Editor.zh-Hant.xlf | 5 ++ 20 files changed, 256 insertions(+) create mode 100644 vsintegration/src/FSharp.Editor/CodeFix/MakeOuterBindingRecursive.fs diff --git a/src/fsharp/service/ServiceUntypedParse.fs b/src/fsharp/service/ServiceUntypedParse.fs index ce39d97459b..a37166c9ce2 100755 --- a/src/fsharp/service/ServiceUntypedParse.fs +++ b/src/fsharp/service/ServiceUntypedParse.fs @@ -99,6 +99,59 @@ type FSharpParseFileResults(errors: FSharpErrorInfo[], input: ParsedInput option member scope.ParseHadErrors = parseHadErrors member scope.ParseTree = input + + member scope.TryRangeOfNameOfNearestOuterBindingContainingPos pos = + let tryGetIdentRangeFromBinding binding = + match binding with + | SynBinding.Binding (_, _, _, _, _, _, _, headPat, _, _, _, _) -> + match headPat with + | SynPat.LongIdent (longIdentWithDots, _, _, _, _, _) -> + Some longIdentWithDots.Range + | SynPat.Named(_, ident, false, _, _) -> + Some ident.idRange + | _ -> + None + + let rec walkBinding expr workingRange = + match expr with + + // This lets us dive into subexpressions that may contain the binding we're after + | SynExpr.Sequential (_, _, expr1, expr2, _) -> + if rangeContainsPos expr1.Range pos then + walkBinding expr1 workingRange + else + walkBinding expr2 workingRange + + + | SynExpr.LetOrUse(_, _, bindings, bodyExpr, _) -> + let potentialNestedRange = + bindings + |> List.tryFind (fun binding -> rangeContainsPos binding.RangeOfBindingAndRhs pos) + |> Option.bind tryGetIdentRangeFromBinding + match potentialNestedRange with + | Some range -> + walkBinding bodyExpr range + | None -> + walkBinding bodyExpr workingRange + + + | _ -> + Some workingRange + + match scope.ParseTree with + | Some input -> + AstTraversal.Traverse(pos, input, { new AstTraversal.AstVisitorBase<_>() with + member _.VisitExpr(_, _, defaultTraverse, expr) = + defaultTraverse expr + + override _.VisitBinding(defaultTraverse, binding) = + match binding with + | SynBinding.Binding (_, _, _, _, _, _, _, _, _, expr, _range, _) as b when rangeContainsPos b.RangeOfBindingAndRhs pos -> + match tryGetIdentRangeFromBinding b with + | Some range -> walkBinding expr range + | None -> None + | _ -> defaultTraverse binding }) + | None -> None member scope.TryIdentOfPipelineContainingPosAndNumArgsApplied pos = match scope.ParseTree with diff --git a/src/fsharp/service/ServiceUntypedParse.fsi b/src/fsharp/service/ServiceUntypedParse.fsi index 5d484b83f22..d3f6dc8cd68 100755 --- a/src/fsharp/service/ServiceUntypedParse.fsi +++ b/src/fsharp/service/ServiceUntypedParse.fsi @@ -19,6 +19,9 @@ type public FSharpParseFileResults = /// The syntax tree resulting from the parse member ParseTree : ParsedInput option + /// Attempts to find the range of the name of the nearest outer binding that contains a given position. + member TryRangeOfNameOfNearestOuterBindingContainingPos: pos: pos -> Option + /// Attempts to find the range of an attempted lambda expression or pattern, the argument range, and the expr range when writing a C#-style "lambda" (which is actually an operator application) member TryRangeOfParenEnclosingOpEqualsGreaterUsage: opGreaterEqualPos: pos -> Option diff --git a/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs b/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs index 3dbaac7ba2b..2ada79c0c21 100644 --- a/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs +++ b/tests/FSharp.Compiler.Service.Tests/SurfaceArea.netstandard.fs @@ -22729,6 +22729,7 @@ FSharp.Compiler.SourceCodeServices.FSharpParseFileResults: Boolean get_ParseHadE FSharp.Compiler.SourceCodeServices.FSharpParseFileResults: FSharp.Compiler.SourceCodeServices.FSharpErrorInfo[] Errors 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] TryRangeOfNameOfNearestOuterBindingContainingPos(pos) 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] TryRangeOfExprInYieldOrReturn(pos) diff --git a/tests/service/ServiceUntypedParseTests.fs b/tests/service/ServiceUntypedParseTests.fs index bd06ac65859..472fc7e2e7f 100644 --- a/tests/service/ServiceUntypedParseTests.fs +++ b/tests/service/ServiceUntypedParseTests.fs @@ -927,3 +927,77 @@ let ``TryRangeOfParenEnclosingOpEqualsGreaterUsage - parenthesized lambda``() = |> shouldEqual [((2, 21), (2, 31)); ((2, 21), (2, 22)); ((2, 26), (2, 31))] | None -> Assert.Fail("Expected to get a range back, but got none.") + +[] +let ``TryRangeOfNameOfNearestOuterBindingContainingPos - simple``() = + let source = """ +let x = nameof x +""" + let parseFileResults, _ = getParseAndCheckResults source + let res = parseFileResults.TryRangeOfNameOfNearestOuterBindingContainingPos (mkPos 2 15) + match res with + | Some range -> + range + |> tups + |> shouldEqual ((2, 4), (2, 5)) + | None -> + Assert.Fail("Expected to get a range back, but got none.") + +[] +let ``TryRangeOfNameOfNearestOuterBindingContainingPos - inside match``() = + let source = """ +let mySum xs acc = + match xs with + | [] -> acc + | _ :: tail -> + mySum tail (acc + 1) +""" + let parseFileResults, _ = getParseAndCheckResults source + let res = parseFileResults.TryRangeOfNameOfNearestOuterBindingContainingPos (mkPos 6 8) + match res with + | Some range -> + range + |> tups + |> shouldEqual ((2, 4), (2, 9)) + | None -> + Assert.Fail("Expected to get a range back, but got none.") + +[] +let ``TryRangeOfNameOfNearestOuterBindingContainingPos - nested binding``() = + let source = """ +let f x = + let z = 12 + let h x = + h x + g x +""" + let parseFileResults, _ = getParseAndCheckResults source + let res = parseFileResults.TryRangeOfNameOfNearestOuterBindingContainingPos (mkPos 5 8) + match res with + | Some range -> + range + |> tups + |> shouldEqual ((4, 8), (4, 9)) + | None -> + Assert.Fail("Expected to get a range back, but got none.") + +[] +let ``TryRangeOfNameOfNearestOuterBindingContainingPos - nested and after other statements``() = + let source = """ +let f x = + printfn "doot doot" + printfn "toot toot" + let z = 12 + let h x = + h x + g x +""" + let parseFileResults, _ = getParseAndCheckResults source + let res = parseFileResults.TryRangeOfNameOfNearestOuterBindingContainingPos (mkPos 7 8) + match res with + | Some range -> + range + |> tups + |> shouldEqual ((6, 8), (6, 9)) + | None -> + Assert.Fail("Expected to get a range back, but got none.") diff --git a/vsintegration/src/FSharp.Editor/CodeFix/MakeOuterBindingRecursive.fs b/vsintegration/src/FSharp.Editor/CodeFix/MakeOuterBindingRecursive.fs new file mode 100644 index 00000000000..581380d2c23 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/CodeFix/MakeOuterBindingRecursive.fs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Composition + +open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.CodeFixes + +[] +type internal FSharpMakeOuterBindingRecursiveCodeFixProvider + [] + ( + checkerProvider: FSharpCheckerProvider, + projectInfoManager: FSharpProjectOptionsManager + ) = + inherit CodeFixProvider() + + static let userOpName = "MakeOuterBindingRecursive" + let fixableDiagnosticIds = set ["FS0039"] + + override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds + + override _.RegisterCodeFixesAsync context = + asyncMaybe { + let! sourceText = context.Document.GetTextAsync(context.CancellationToken) + let! parsingOptions, _ = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(context.Document, context.CancellationToken, userOpName) + let! parseResults = checkerProvider.Checker.ParseFile(context.Document.FilePath, sourceText.ToFSharpSourceText(), parsingOptions, userOpName) |> liftAsync + + let diagnosticRange = RoslynHelpers.TextSpanToFSharpRange(context.Document.FilePath, context.Span, sourceText) + do! Option.guard (parseResults.IsPosContainedInApplication diagnosticRange.Start) + + let! outerBindingRange = parseResults.TryRangeOfNameOfNearestOuterBindingContainingPos diagnosticRange.Start + let! outerBindingNameSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, outerBindingRange) + + // One last check to verify the names are the same + do! Option.guard (sourceText.GetSubText(outerBindingNameSpan).ContentEquals(sourceText.GetSubText(context.Span))) + + let diagnostics = + context.Diagnostics + |> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id) + |> Seq.toImmutableArray + + let title = String.Format(SR.MakeOuterBindingRecursive(), sourceText.GetSubText(outerBindingNameSpan).ToString()) + + let codeFix = + CodeFixHelpers.createTextChangeCodeFix( + title, + context, + (fun () -> asyncMaybe.Return [| TextChange(TextSpan(outerBindingNameSpan.Start, 0), "rec ") |])) + + context.RegisterCodeFix(codeFix, diagnostics) + } + |> Async.Ignore + |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index ada59f78ce5..a8ca0445e7d 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -185,6 +185,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 596442035df..87076943ea0 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -264,4 +264,7 @@ Use F# lambda syntax + + Make '{0}' recursive + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf index 5ab8eca94be..5c069ffbd7e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -62,6 +62,11 @@ Nastavte deklaraci jako mutable. + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore Před {0} vložte podtržítko. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index d7248fcd49f..e538846342d 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -62,6 +62,11 @@ Deklaration "änderbar" machen + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore "{0}" einen Unterstrich voranstellen diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index 947b067884a..c0b21f5978a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -62,6 +62,11 @@ Convertir la declaración en "mutable" + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore Colocar un carácter de subrayado delante de "{0}" diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index 2a0e7f9307e..c3279ecdcc3 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -62,6 +62,11 @@ Rendre la déclaration 'mutable' + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore Faire précéder '{0}' d'un trait de soulignement diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index f0f9ca41537..4eb00f6e453 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -62,6 +62,11 @@ Impostare la dichiarazione come 'mutable' + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore Anteponi a '{0}' un carattere di sottolineatura diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index 4bc3db3ca72..997fdbb34c2 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -62,6 +62,11 @@ 'mutable' を宣言する + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore アンダースコアが含まれているプレフィックス '{0}' diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index af33c0404c2..296fdd04ce7 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -62,6 +62,11 @@ 선언을 '변경 가능'으로 지정 + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore 밑줄이 있는 '{0}' 접두사 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index 4046f1adabb..0422c21b5bc 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -62,6 +62,11 @@ Nadaj deklaracji właściwość „mutable” + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore Prefiks „{0}” ze znakiem podkreślenia diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf index 52f4aec741d..6085b32124d 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -62,6 +62,11 @@ Fazer com que a declaração seja 'mutable' + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore Prefixo '{0}' sem sublinhado diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 21b44abfe58..f6dd2c6f6ad 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -62,6 +62,11 @@ Сделайте объявление "изменяемым" + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore Добавить символ подчеркивания как префикс "{0}" diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index a16a7d29a7e..e78dffc69a7 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -62,6 +62,11 @@ Bildirimi 'mutable' yapın + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore '{0}' öğesinin önüne alt çizgi ekleme diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf index c6317c2d83d..ce0669b423a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -62,6 +62,11 @@ 将声明设为“可变” + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore 带下划线的前缀“{0}” diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf index 2956b6b65a5..063176df13e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -62,6 +62,11 @@ 將宣告設定為「可變動」 + + Make '{0}' recursive + Make '{0}' recursive + + Prefix '{0}' with underscore 有底線的前置詞 '{0}'