diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 8f47e2e0dbe..bdb38e8ab95 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -4,6 +4,7 @@ * Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880)) * Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188)) * Added a **Tools > Options > F# Tools > Compiler** option, **"Use the .NET SDK F# compiler for builds"**, on by default. Visual Studio uses the .NET SDK F# compiler when the project supplies SDK paths, matching `dotnet build`. Otherwise, it uses the bundled .NET Framework compiler. Turn the option off to use the bundled compiler for all projects. Changes are saved on Apply or OK; Cancel discards unsaved changes. Command-line builds are unaffected. ([Issue #20484](https://github.com/dotnet/fsharp/issues/20484), [PR #20485](https://github.com/dotnet/fsharp/pull/20485)) +* Add a refactoring that converts between a `fun x -> x.Prop` lambda and the F# 8 `_.Prop` shorthand. ([Issue #16234](https://github.com/dotnet/fsharp/issues/16234), [PR #20535](https://github.com/dotnet/fsharp/pull/20535)) ### Fixed diff --git a/src/Compiler/SyntaxTree/SyntaxTreeOps.fs b/src/Compiler/SyntaxTree/SyntaxTreeOps.fs index 5f157cca8f4..8742049612f 100644 --- a/src/Compiler/SyntaxTree/SyntaxTreeOps.fs +++ b/src/Compiler/SyntaxTree/SyntaxTreeOps.fs @@ -67,51 +67,46 @@ let mkSynSimplePatVar isOpt id = let mkSynCompGenSimplePatVar id = SynSimplePat.Id(id, None, true, false, false, id.idRange) -let rec pushUnaryArg expr arg = +let rec private splitDotLambdaChain expr : struct (SynExpr * (SynExpr -> SynExpr)) = + let inside inner wrap = + let struct (head, rebuild) = splitDotLambdaChain inner + struct (head, rebuild >> wrap) + match expr with - | SynExpr.App(ExprAtomicFlag.Atomic, infix, SynExpr.Ident ident, x1, m1) -> - SynExpr.App( - ExprAtomicFlag.Atomic, - infix, - SynExpr.LongIdent(false, SynLongIdent(arg :: [ ident ], [ ident.idRange ], [ None ]), None, ident.idRange), - x1, - m1 - ) - | SynExpr.App(ExprAtomicFlag.Atomic, - infix, - SynExpr.LongIdent(isOptional, SynLongIdent(id, dotRanges, trivia), altNameRefCell, range), - x1, - m1) -> - SynExpr.App( - ExprAtomicFlag.Atomic, - infix, - SynExpr.LongIdent(isOptional, SynLongIdent(arg :: id, dotRanges, trivia), altNameRefCell, range), - x1, - m1 - ) - | SynExpr.App(ExprAtomicFlag.Atomic, infix, (SynExpr.App _ as innerApp), x1, m1) -> - SynExpr.App(ExprAtomicFlag.Atomic, infix, (pushUnaryArg innerApp arg), x1, m1) - | SynExpr.App(ExprAtomicFlag.Atomic, infix, SynExpr.DotGet(synExpr, rangeOfDot, synLongIdent, range), x1, m1) -> - SynExpr.App(ExprAtomicFlag.Atomic, infix, SynExpr.DotGet((pushUnaryArg synExpr arg), rangeOfDot, synLongIdent, range), x1, m1) - | SynExpr.App(ExprAtomicFlag.Atomic, infix, innerExpr, x1, m1) -> - SynExpr.App(ExprAtomicFlag.Atomic, infix, pushUnaryArg innerExpr arg, x1, m1) - | SynExpr.Ident ident -> SynExpr.LongIdent(false, SynLongIdent(arg :: [ ident ], [ ident.idRange ], [ None ]), None, ident.idRange) - | SynExpr.LongIdent(isOptional, SynLongIdent(id, dotRanges, trivia), altNameRefCell, range) -> - SynExpr.LongIdent(isOptional, SynLongIdent(arg :: id, dotRanges, trivia), altNameRefCell, range) - | SynExpr.DotGet(synExpr, rangeOfDot, synLongIdent, range) -> SynExpr.DotGet(pushUnaryArg synExpr arg, rangeOfDot, synLongIdent, range) + | SynExpr.App(ExprAtomicFlag.Atomic, infix, funcExpr, argExpr, m) -> + inside funcExpr (fun funcExpr -> SynExpr.App(ExprAtomicFlag.Atomic, infix, funcExpr, argExpr, m)) + | SynExpr.DotGet(synExpr, rangeOfDot, synLongIdent, range) -> + inside synExpr (fun synExpr -> SynExpr.DotGet(synExpr, rangeOfDot, synLongIdent, range)) | SynExpr.DotIndexedGet(objectExpr, indexArgs, dotRange, range) -> - SynExpr.DotIndexedGet(pushUnaryArg objectExpr arg, indexArgs, dotRange, range) + inside objectExpr (fun objectExpr -> SynExpr.DotIndexedGet(objectExpr, indexArgs, dotRange, range)) | SynExpr.TypeApp(innerExpr, mLess, tyargs, mCommas, mGreater, mTypars, m) -> - let innerExpr = pushUnaryArg innerExpr arg - SynExpr.TypeApp(innerExpr, mLess, tyargs, mCommas, mGreater, mTypars, m) + inside innerExpr (fun innerExpr -> SynExpr.TypeApp(innerExpr, mLess, tyargs, mCommas, mGreater, mTypars, m)) + | _ -> struct (expr, id) + +let rec pushUnaryArg expr arg = + let struct (head, rebuild) = splitDotLambdaChain expr + + match head with + | SynExpr.Ident ident -> + rebuild (SynExpr.LongIdent(false, SynLongIdent(arg :: [ ident ], [ ident.idRange ], [ None ]), None, ident.idRange)) + | SynExpr.LongIdent(isOptional, SynLongIdent(id, dotRanges, trivia), altNameRefCell, range) -> + rebuild (SynExpr.LongIdent(isOptional, SynLongIdent(arg :: id, dotRanges, trivia), altNameRefCell, range)) | SynExpr.ArbitraryAfterError(_, m) when m.Start = m.End -> - SynExpr.DiscardAfterMissingQualificationAfterDot(SynExpr.Ident arg, m.StartRange, unionRanges arg.idRange m) + rebuild (SynExpr.DiscardAfterMissingQualificationAfterDot(SynExpr.Ident arg, m.StartRange, unionRanges arg.idRange m)) | SynExpr.DiscardAfterMissingQualificationAfterDot(synExpr, dotRange, m) -> - SynExpr.DiscardAfterMissingQualificationAfterDot(pushUnaryArg synExpr arg, dotRange, unionRanges arg.idRange m) + rebuild (SynExpr.DiscardAfterMissingQualificationAfterDot(pushUnaryArg synExpr arg, dotRange, unionRanges arg.idRange m)) | _ -> - errorR (Error(FSComp.SR.tcDotLambdaAtNotSupportedExpression (), expr.Range)) + errorR (Error(FSComp.SR.tcDotLambdaAtNotSupportedExpression (), head.Range)) expr +let tryPopUnaryArg body = + let struct (head, rebuild) = splitDotLambdaChain body + + match head with + | SynExpr.LongIdent(isOptional, SynLongIdent(root :: (_ :: _ as rest), dotRanges, trivia), altNameRefCell, range) -> + ValueSome(struct (root, rebuild (SynExpr.LongIdent(isOptional, SynLongIdent(rest, dotRanges, trivia), altNameRefCell, range)))) + | _ -> ValueNone + /// CAUTION: This function operates over the untyped tree, so should be used only when absolutely necessary. It doesn't verify assembly origine nor does it respect type aliases. /// Also, keep in mind that it will only check last part of the assembly (with or without the `Attribute` suffix). let inline findSynAttribute (attrName: string) (synAttrs: SynAttributes) = diff --git a/src/Compiler/SyntaxTree/SyntaxTreeOps.fsi b/src/Compiler/SyntaxTree/SyntaxTreeOps.fsi index d65abed05d6..8fd72eeb6de 100644 --- a/src/Compiler/SyntaxTree/SyntaxTreeOps.fsi +++ b/src/Compiler/SyntaxTree/SyntaxTreeOps.fsi @@ -45,6 +45,8 @@ val mkSynCompGenSimplePatVar: id: Ident -> SynSimplePat val pushUnaryArg: expr: SynExpr -> arg: Ident -> SynExpr +val tryPopUnaryArg: body: SynExpr -> struct (Ident * SynExpr) voption + val inline findSynAttribute: attrName: string -> synAttrs: SynAttributes -> bool [] diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj index c8bcf19a133..db4d3178bcc 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -91,6 +91,7 @@ + diff --git a/tests/FSharp.Compiler.Service.Tests/SyntaxTreeOpsTests.fs b/tests/FSharp.Compiler.Service.Tests/SyntaxTreeOpsTests.fs new file mode 100644 index 00000000000..5b9a00ce182 --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/SyntaxTreeOpsTests.fs @@ -0,0 +1,39 @@ +module FSharp.Compiler.Service.Tests.SyntaxTreeOpsTests + +open FSharp.Compiler.Service.Tests.Common +open FSharp.Compiler.Syntax +open FSharp.Compiler.SyntaxTreeOps +open Xunit + +let private exprOf source = + match getParseResults source with + | ParsedInput.ImplFile(ParsedImplFileInput(contents = [ SynModuleOrNamespace(decls = [ SynModuleDecl.Expr(expr = expr) ]) ])) -> expr + | tree -> failwith $"Expected a single expression, got %A{tree}" + +[] +[] +[] +[] +[] +[] +[()")>] +[] +let ``tryPopUnaryArg splits the root off a chain that pushUnaryArg restores`` (source: string) = + let body = exprOf source + + match tryPopUnaryArg body with + | ValueSome(struct (root, popped)) -> + Assert.Equal("x", root.idText) + Assert.Equal($"%A{body}", $"%A{pushUnaryArg popped root}") + | ValueNone -> Assert.Fail $"Expected {source} to have a root" + +[] +[] +[] +[] +[] +[] +[] +[] +let ``tryPopUnaryArg rejects what _. cannot express`` (source: string) = + Assert.True((tryPopUnaryArg (exprOf source)).IsNone) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 319bdd5a264..e2df0ed12e3 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -105,6 +105,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 1f1f632d770..cb93aa0cc7d 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -368,4 +368,10 @@ Use live (unsaved) buffers for analysis Returns: + + Convert to '_.' shorthand lambda + + + Convert to 'fun' lambda + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs b/vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs new file mode 100644 index 00000000000..9afb438900d --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs @@ -0,0 +1,207 @@ +// 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.CodeActions +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.Text +open Microsoft.VisualStudio.FSharp.Editor.Telemetry + +open FSharp.Compiler.Syntax +open FSharp.Compiler.SyntaxTreeOps +open FSharp.Compiler.SyntaxTrivia +open FSharp.Compiler.Text + +open CancellableTasks + +[] +module private DotLambdaConversion = + + [] + type Conversion = + { + Title: string + Direction: string + Change: TextChange + } + + let hasName (name: string) (ident: Ident) = + String.Equals(ident.idText, name, StringComparison.Ordinal) + + let identifiersIn (body: SynExpr) = + ([], [ SyntaxNode.SynExpr body ]) + ||> SyntaxNodes.fold (fun identifiers _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Ident ident) + | SyntaxNode.SynExpr(SynExpr.LongIdent(longDotId = SynLongIdent(id = ident :: _))) + | SyntaxNode.SynExpr(SynExpr.LongIdentSet(longDotId = SynLongIdent(id = ident :: _))) + | SyntaxNode.SynExpr(SynExpr.NamedIndexedPropertySet(longDotId = SynLongIdent(id = ident :: _))) + | SyntaxNode.SynPat(SynPat.Named(ident = SynIdent(ident, _))) -> ident :: identifiers + | _ -> identifiers) + + let parameterNameFor (body: SynExpr) = + let used = identifiersIn body + + Seq.initInfinite (fun i -> if i = 0 then "x" else $"x{i}") + |> Seq.find (fun name -> not (List.exists (hasName name) used)) + + let canBeCapturedByGeneratedArgument (ident: Ident) = + let prefix = "_arg" + let name = ident.idText + + let rec isDigitsFrom i = + i = name.Length || (Char.IsDigit name[i] && isDigitsFrom (i + 1)) + + name.Length > prefix.Length + && name.StartsWith(prefix, StringComparison.Ordinal) + && isDigitsFrom prefix.Length + + let hasCommentIn (comments: CommentTrivia list) (m: range) = + comments + |> List.exists (fun (CommentTrivia.LineComment comment | CommentTrivia.BlockComment comment) -> Range.rangeContainsRange m comment) + + let rec lastOffsideSensitiveLine (expr: SynExpr) = + match expr with + | SynExpr.App(flag = ExprAtomicFlag.Atomic; funcExpr = func) -> lastOffsideSensitiveLine func + | SynExpr.DotGet(longDotId = longDotId) -> longDotId.Range.EndLine + | SynExpr.DotIndexedGet(dotRange = dot) -> dot.EndLine + | _ -> expr.Range.EndLine + + let isInQuotation (path: SyntaxVisitorPath) = + path + |> List.exists (function + | SyntaxNode.SynExpr(SynExpr.Quote _) -> true + | _ -> false) + + let rec isAppliedDirectly (expr: SynExpr) (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynExpr(SynExpr.Paren _ as paren) :: outer -> isAppliedDirectly paren outer + | SyntaxNode.SynExpr(SynExpr.App(funcExpr = func)) :: _ -> obj.ReferenceEquals(func, expr) + | _ -> false + + let isBlankBetween (sourceText: SourceText) start finish = + start <= finish + && String.IsNullOrWhiteSpace(sourceText.ToString(TextSpan.FromBounds(start, finish))) + + [] + let (|SoleParenthesizedLambdaArgument|_|) (sourceText: SourceText) (lambda: SynExpr) (lambdaSpan: TextSpan) (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynExpr(SynExpr.Paren(expr = inner; leftParenRange = leftParen; rightParenRange = Some rightParen; range = parenRange) as paren) :: SyntaxNode.SynExpr(SynExpr.App( + flag = ExprAtomicFlag.NonAtomic; isInfix = false; argExpr = arg)) :: _ when + obj.ReferenceEquals(inner, lambda) + && obj.ReferenceEquals(arg, paren) + && isBlankBetween sourceText (RoslynHelpers.FSharpRangeToTextSpan(sourceText, leftParen)).End lambdaSpan.Start + && isBlankBetween sourceText lambdaSpan.End (RoslynHelpers.FSharpRangeToTextSpan(sourceText, rightParen)).Start + -> + ValueSome(RoslynHelpers.FSharpRangeToTextSpan(sourceText, parenRange)) + | _ -> ValueNone + + let toShorthand (sourceText: SourceText) (path: SyntaxVisitorPath) (lambda: SynExpr) (root: Ident) = + let lambdaSpan = RoslynHelpers.FSharpRangeToTextSpan(sourceText, lambda.Range) + let rootEnd = RoslynHelpers.FSharpRangeToTextSpan(sourceText, root.idRange).End + + match path with + | SoleParenthesizedLambdaArgument sourceText lambda lambdaSpan parenSpan -> + let chain = sourceText.ToString(TextSpan.FromBounds(rootEnd, lambdaSpan.End)) + TextChange(parenSpan, $"_{chain}") + | _ -> TextChange(TextSpan.FromBounds(lambdaSpan.Start, rootEnd), "_") + + let toLambda (sourceText: SourceText) (path: SyntaxVisitorPath) (body: SynExpr) (range: range) (trivia: SynExprDotLambdaTrivia) = + let name = parameterNameFor body + let lambda = $"fun {name} -> {name}" + + let underscore = + RoslynHelpers.FSharpRangeToTextSpan(sourceText, trivia.UnderscoreRange) + + match path with + | SyntaxNode.SynExpr(SynExpr.Paren _) :: _ + | SyntaxNode.SynBinding _ :: _ -> TextChange(underscore, lambda) + | _ -> + let span = RoslynHelpers.FSharpRangeToTextSpan(sourceText, range) + let chain = sourceText.ToString(TextSpan.FromBounds(underscore.End, span.End)) + TextChange(span, $"({lambda}{chain})") + + let tryFind (sourceText: SourceText) (position: pos) (parseTree: ParsedInput) = + let comments = + match parseTree with + | ParsedInput.ImplFile file -> file.Trivia.CodeComments + | ParsedInput.SigFile file -> file.Trivia.CodeComments + + (position, parseTree) + ||> ParsedInput.tryPickLast (fun path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Lambda( + fromMethod = false + parsedData = Some([ SynPat.Named(ident = SynIdent(parameter, _); isThisVal = false; accessibility = None) ], body)) as lambda) when + not (isInQuotation path) && not (isAppliedDirectly lambda path) + -> + let identifiers = identifiersIn body + + match tryPopUnaryArg body with + | ValueSome(struct (root, _)) when + hasName parameter.idText root + && (identifiers |> List.filter (hasName parameter.idText) |> List.length) = 1 + && not (List.exists canBeCapturedByGeneratedArgument identifiers) + && not (hasCommentIn comments (Range.withEnd root.idRange.End lambda.Range)) + -> + Some + { + Title = SR.ConvertToShorthandLambda() + Direction = "shorthand" + Change = toShorthand sourceText path lambda root + } + | _ -> None + + | SyntaxNode.SynExpr(SynExpr.DotLambda(expr = body; range = range; trivia = trivia)) when + not body.IsArbExprAndThusAlreadyReportedError + && not (isInQuotation path) + && lastOffsideSensitiveLine body = range.StartLine + -> + Some + { + Title = SR.ConvertToFunLambda() + Direction = "lambda" + Change = toLambda sourceText path body range trivia + } + + | _ -> None) + +[] +type internal FSharpConvertDotLambdaRefactoring [] () = + inherit CodeRefactoringProvider() + + override _.ComputeRefactoringsAsync context = + cancellableTask { + let document = context.Document + + if not document.IsFSharpSignatureFile then + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync cancellationToken + + let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpConvertDotLambdaRefactoring) + + let caret = sourceText.Lines.GetLinePosition context.Span.Start + let position = Position.fromZ caret.Line caret.Character + + match DotLambdaConversion.tryFind sourceText position parseResults.ParseTree with + | Some conversion -> + let changedDocument = + cancellableTask { + TelemetryReporter.ReportSingleEvent( + TelemetryEvents.RefactoringActivated, + [| + "name", box (nameof FSharpConvertDotLambdaRefactoring) + "direction", box conversion.Direction + |] + ) + + return document.WithText(sourceText.WithChanges conversion.Change) + } + + context.RegisterRefactoring(CodeAction.Create(conversion.Title, changedDocument, conversion.Title)) + | None -> () + } + |> CancellableTask.startAsTask context.CancellationToken diff --git a/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs b/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs index 230f4988438..a5bf5e6b4ef 100644 --- a/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs +++ b/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs @@ -18,6 +18,9 @@ module TelemetryEvents = [] let CodefixActivated = "codefixactivated" + [] + let RefactoringActivated = "refactoringactivated" + [] let Hints = "hints" diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf index cd8c46bf705..3ef136d2536 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -90,11 +90,21 @@ Navrhnout názvy pro nerozpoznané identifikátory; Převést na anonymní záznam + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check Pro kontrolu nerovnosti použijte <>. + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check Pro kontrolu rovnosti použijte =. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index bce1941f0b1..ca93d64614c 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -90,11 +90,21 @@ Namen für nicht aufgelöste Bezeichner vorschlagen; In anonymen Datensatz konvertieren + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check "<>" für die Überprüfung auf Ungleichheit verwenden + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check "=" für Gleichheitsüberprüfung verwenden diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index fa8cb62c422..4fc14b5665e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -90,11 +90,21 @@ Sugerir nombres para identificadores sin resolver; Convertir en registro anónimo + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check Usar "<>" para la comprobación de desigualdad + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check Usar "=" para la comprobación de igualdad diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index e7ec71e839e..a4fd8ad0d5b 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -90,11 +90,21 @@ Suggérer des noms pour les identificateurs non résolus ; Convertir en enregistrement anonyme + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check Utiliser '<>' pour vérifier l'inégalité + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check Utiliser '=' pour vérifier l'égalité diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 327a7ca362f..5bd08ff3e58 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -90,11 +90,21 @@ Suggerisci i nomi per gli identificatori non risolti; Converti in record anonimo + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check Usare '<>' per il controllo di disuguaglianza + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check Usare '=' per il controllo di uguaglianza diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index d45234c011a..dc75026bfd3 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -90,11 +90,21 @@ Suggest names for unresolved identifiers; 匿名レコードに変換 + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check 非等値のチェックには '<>' を使用します + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check 等値性のチェックには '=' を使用します diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index 3248e0641ea..833d2c81ab5 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -90,11 +90,21 @@ Suggest names for unresolved identifiers; 익명 레코드로 변환 + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check 같지 않음 검사에 '<>' 사용 + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check 같음 검사에 '=' 사용 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index abc39f15da5..ff8463459ae 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -90,11 +90,21 @@ Sugeruj nazwy dla nierozpoznanych identyfikatorów; Konwertuj na rekord anonimowy + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check Użyj operatora „<>” do sprawdzenia nierówności + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check Użyj znaku „=” w celu sprawdzenia równości 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 dfde43120f5..9e2d2e92599 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -90,11 +90,21 @@ Sugerir nomes para identificadores não resolvidos; Converter em Registro Anônimo + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check Usar '<>' para a verificação de desigualdade + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check Usar '=' para verificação de igualdade diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 47cda215312..66b7e0c9c65 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -90,11 +90,21 @@ Suggest names for unresolved identifiers; Преобразовать в анонимную запись + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check Используйте "<>" для проверки на неравенство + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check Используйте "=" для проверки равенства diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 58aa5d54c43..d5071371a7a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -90,11 +90,21 @@ Kullanılmayan değerleri analiz et ve bunlara düzeltmeler öner; Anonim Kayda Dönüştür + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check Eşitsizlik denetimi için '<>' kullanın + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check Eşitlik denetimi için '=' kullan 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 4fa703776fb..ddaa7832d82 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -90,11 +90,21 @@ Suggest names for unresolved identifiers; 转换为匿名记录 + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check 使用 "<>" 进行不相等检查 + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check 使用 "=" 进行同等性检查 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 fd46ef9919a..fa5ebeecedf 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -90,11 +90,21 @@ Suggest names for unresolved identifiers; 轉換為匿名記錄 + + Convert to 'fun' lambda + Convert to 'fun' lambda + + Use '<>' for inequality check 使用 '<>' 進行不等式檢查 + + Convert to '_.' shorthand lambda + Convert to '_.' shorthand lambda + + Use '=' for equality check 使用 '=' 檢查是否相等 diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index a62b25af1cf..7164d6de08e 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -75,6 +75,7 @@ + diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs new file mode 100644 index 00000000000..a6dacb16c3f --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs @@ -0,0 +1,125 @@ +module FSharp.Editor.Tests.Refactors.ConvertDotLambdaTests + +open System + +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks + +open Xunit + +open FSharp.Editor.Tests.Refactors.RefactorTestFramework + +let private inModule (binding: string) = + let definition = + if binding.StartsWith("let ", StringComparison.Ordinal) then + binding + else + $"let r = {binding}" + + $""" +module M + +{definition} +""" + +let private caretAt (code: string) (marker: string) = + code.IndexOf(marker, StringComparison.Ordinal) + +let private refactored (code: string) (marker: string) = + use context = TestContext.CreateWithCode code + + let document = + tryRefactor code (caretAt code marker) context (new FSharpConvertDotLambdaRefactoring()) + + let parseResults = + document.GetFSharpParseResultsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + Assert.False(parseResults.ParseHadErrors, $"%A{parseResults.Diagnostics}") + + (document.GetTextAsync() |> GetTaskResult).ToString() + +[] +[ List.map (fun x -> x.Prop)", "xs |> List.map _.Prop")>] +[ x.A.B) xs", "List.map _.A.B xs")>] +[ x.M(y))", "Seq.filter _.M(y)")>] +[ x.Xs[0]) arr", "Array.map _.Xs[0] arr")>] +[ x.M()) xs", "List.map _.M() xs")>] +[ x.Xs.[0]) xs", "List.map _.Xs.[0] xs")>] +[ x.M().P) xs", "List.map _.M().P xs")>] +[ x.P ) xs", "List.map _.P xs")>] +[ x.P) xs", "List.map _.P xs")>] +[ ``x``.P) xs", "List.map _.P xs")>] +[ y.P)", "x.M(_.P)")>] +[ x.P", "_.P")>] +[ x.P)", "(_.P)")>] +[ x .P", "_ .P")>] +[ x (* why *) .P", "_ (* why *) .P")>] +[ x.M(_argument)) xs", "List.map _.M(_argument) xs")>] +[\n x.Prop) xs", "List.map _.Prop xs")>] +[\n x.P", + "let r =\n _.P", + TestDisplayName = "Lambda spanning multiple lines still converts to shorthand")>] +let ``Lambda reading a member of its parameter converts to shorthand`` (before: string, after: string) = + Assert.Equal(inModule after, refactored (inModule before) "fun") + +[] +[ x.P")>] +[ x.A.B")>] +[ x.Xs[0]")>] +[ x.P) xs")>] +[ x.P)")>] +[ List.map _.M(x)", "xs |> List.map (fun x1 -> x1.M(x))")>] +[ x.M(_.P)")>] +[ x.Replace(\"a\", \"\"\"x\ny\"\"\")")>] +[ x .P")>] +[ x (* why *) .P")>] +[ x.P)", TestDisplayName = "Nested shorthand converts on its own")>] +[ string =\n _.PadLeft(\n 3\n )", + "let f : string -> string =\n (fun x -> x.PadLeft(\n 3\n ))", + TestDisplayName = "Shorthand whose arguments span multiple lines converts to lambda")>] +let ``Shorthand converts to lambda`` (marker: string, before: string, after: string) = + Assert.Equal(inModule after, refactored (inModule before) marker) + +[] +[ x.P")>] +[ x.P")>] +[ x.P")>] +[ (x.P)")>] +[ f x.P")>] +[ x.M y")>] +[ x.P + 1")>] +[ x.M(x)")>] +[ x.M(fun x -> x)")>] +[ x.M(x <- 1)")>] +[ x.M(x.P <- 1)")>] +[ x.M(x.Item(0) <- 1)")>] +[ x.M(x.M().P <- 1)")>] +[ x.M(x.Xs[0] <- 1)")>] +[ x.")>] +[ y.P")>] +[ x")>] +[ x[0]")>] +[ 1")>] +[ x.P) y")>] +[ x.P)) y")>] +[ (* why *) x.P")>] +[ x.P")>] +[ // why\n x.P")>] +[ (* why *) x.P) xs")>] +[ x.CompareTo(_arg1)")>] +[ x.P @>")>] +[")>] +[] +[] +[] +let ``No action`` (binding: string) = + let code = inModule binding + use context = TestContext.CreateWithCode code + + let marker = + [ "fun"; "_."; "+" ] + |> List.find (fun marker -> binding.IndexOf(marker, StringComparison.Ordinal) >= 0) + + Assert.Empty(tryGetRefactoringActions code (caretAt code marker) context (new FSharpConvertDotLambdaRefactoring()))