From 64a53c43044b0cf5655c425e9d5ca9bf2380b1ac Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 01:31:29 +0200 Subject: [PATCH 1/3] Add a refactoring between 'fun x -> x.Prop' and the '_.Prop' shorthand A lambda whose only use of its parameter is the head of a member, method, indexer or type-application chain converts to the F# 8 shorthand, and a shorthand converts back to a 'fun' lambda with a parameter name the body does not use. The accepted chains mirror SyntaxTreeOps.pushUnaryArg, so the shorthand produced always type checks like the lambda it replaces. A lambda spanning multiple lines still converts as long as its body is such a chain; the result collapses onto the line the '_.' replaces 'fun' on. Fixes #16234 Co-Authored-By: Claude Sonnet 5 --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + .../src/FSharp.Editor/FSharp.Editor.resx | 6 + .../Refactor/ConvertDotLambda.fs | 187 ++++++++++++++++++ .../Telemetry/TelemetryReporter.fs | 3 + .../FSharp.Editor/xlf/FSharp.Editor.cs.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.de.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.es.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.fr.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.it.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.ja.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.ko.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.pl.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.ru.xlf | 10 + .../FSharp.Editor/xlf/FSharp.Editor.tr.xlf | 10 + .../xlf/FSharp.Editor.zh-Hans.xlf | 10 + .../xlf/FSharp.Editor.zh-Hant.xlf | 10 + .../FSharp.Editor.Tests.fsproj | 1 + .../Refactors/ConvertDotLambdaTests.fs | 98 +++++++++ 20 files changed, 427 insertions(+) create mode 100644 vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs create mode 100644 vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index ba03f663967..bde0442f6eb 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -2,6 +2,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)) +* Refactoring to convert a lambda that only reads a member of its parameter (`fun x -> x.Prop`, `fun x -> x.M(y)`, `fun x -> x.Items[0]`) into the F# 8 shorthand `_.Prop`, and a `_.Prop` shorthand back into a `fun` lambda. ([Issue #16234](https://github.com/dotnet/fsharp/issues/16234)) ### Fixed 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..96bbff82e7d --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs @@ -0,0 +1,187 @@ +// 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.Collections.Generic +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.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) + + // The shapes SyntaxTreeOps.pushUnaryArg accepts under `_.`, with the parameter as the head of the chain. + let rec tryChainRoot expr = + match expr with + | SynExpr.LongIdent(longDotId = SynLongIdent(id = root :: _ :: _; dotRanges = dot :: _)) when + Position.posEq dot.Start root.idRange.End + -> + ValueSome root + | SynExpr.DotGet(expr = inner) + | SynExpr.DotIndexedGet(objectExpr = inner) + | SynExpr.TypeApp(expr = inner) + | SynExpr.App(flag = ExprAtomicFlag.Atomic; isInfix = false; funcExpr = inner) -> tryChainRoot inner + | _ -> ValueNone + + let occurrencesOf (name: string) (body: SynExpr) = + (0, [ SyntaxNode.SynExpr body ]) + ||> SyntaxNodes.fold (fun count _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Ident ident) + | SyntaxNode.SynExpr(SynExpr.LongIdent(longDotId = SynLongIdent(id = ident :: _))) + | SyntaxNode.SynPat(SynPat.Named(ident = SynIdent(ident, _))) when hasName name ident -> count + 1 + | _ -> count) + + let parameterNameFor (body: SynExpr) = + let used = + (HashSet(StringComparer.Ordinal), [ SyntaxNode.SynExpr body ]) + ||> SyntaxNodes.fold (fun names _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Ident ident) + | SyntaxNode.SynExpr(SynExpr.LongIdent(longDotId = SynLongIdent(id = ident :: _))) + | SyntaxNode.SynPat(SynPat.Named(ident = SynIdent(ident, _))) -> ignore (names.Add ident.idText) + | _ -> () + + names) + + Seq.initInfinite (fun i -> if i = 0 then "x" else $"x{i}") + |> Seq.find (used.Contains >> not) + + let isInQuotation (path: SyntaxVisitorPath) = + path + |> List.exists (function + | SyntaxNode.SynExpr(SynExpr.Quote _) -> true + | _ -> false) + + let isAppliedDirectly (lambda: SynExpr) (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynExpr(SynExpr.Paren(expr = inner) as paren) :: SyntaxNode.SynExpr(SynExpr.App(funcExpr = func)) :: _ -> + obj.ReferenceEquals(inner, lambda) && obj.ReferenceEquals(func, paren) + | SyntaxNode.SynExpr(SynExpr.App(funcExpr = func)) :: _ -> obj.ReferenceEquals(func, lambda) + | _ -> false + + let spanOf (sourceText: SourceText) (m: range) = + RoslynHelpers.FSharpRangeToTextSpan(sourceText, m) + + let isBlankBetween (sourceText: SourceText) start finish = + start <= finish + && String.IsNullOrWhiteSpace(sourceText.ToString(TextSpan.FromBounds(start, finish))) + + let toShorthand (sourceText: SourceText) (path: SyntaxVisitorPath) (lambda: SynExpr) (root: Ident) = + let lambdaSpan = spanOf sourceText lambda.Range + let rootEnd = (spanOf sourceText root.idRange).End + + 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 (spanOf sourceText leftParen).End lambdaSpan.Start + && isBlankBetween sourceText lambdaSpan.End (spanOf sourceText rightParen).Start + -> + let chain = sourceText.ToString(TextSpan.FromBounds(rootEnd, lambdaSpan.End)) + TextChange(spanOf sourceText parenRange, $"_{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 underscoreStart = (spanOf sourceText trivia.UnderscoreRange).Start + let dotStart = (spanOf sourceText trivia.DotRange).Start + + match path with + | SyntaxNode.SynExpr(SynExpr.Paren _) :: _ + | SyntaxNode.SynBinding _ :: _ -> TextChange(TextSpan.FromBounds(underscoreStart, dotStart), lambda) + | _ -> + let span = spanOf sourceText range + let chain = sourceText.ToString(TextSpan.FromBounds(dotStart, span.End)) + TextChange(span, $"({lambda}{chain})") + + let tryFind (sourceText: SourceText) (position: pos) (parseTree: ParsedInput) = + (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) + -> + match tryChainRoot body with + | ValueSome root when hasName parameter.idText root && occurrencesOf parameter.idText body = 1 -> + 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 + -> + 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.mkPos (Line.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 ecce1205b8c..4f5fc7ccc43 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -74,6 +74,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..7f58a1a3f64 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs @@ -0,0 +1,98 @@ +module FSharp.Editor.Tests.Refactors.ConvertDotLambdaTests + +open System + +open Microsoft.VisualStudio.FSharp.Editor + +open Xunit + +open FSharp.Editor.Tests.Refactors.RefactorTestFramework + +let private inModule (binding: string) = $"module M\n\nlet r = {binding}\n" + +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()) + + (document.GetTextAsync() |> GetTaskResult).ToString() + +let private actionsIn (context: TestContext) (code: string) (marker: string) = + tryGetRefactoringActions code (caretAt code marker) context (new FSharpConvertDotLambdaRefactoring()) + +[] +[ 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")>] +[ y.P)", "x.M(_.P)")>] +[ x.P", "_.P")>] +[ x.P)", "(_.P)")>] +[\n x.Prop) xs", "List.map _.Prop xs")>] +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)")>] +let ``Shorthand converts to lambda`` (before: string, after: string) = + Assert.Equal(inModule after, refactored (inModule before) "_.") + +[] +let ``Nested shorthand converts on its own`` () = + Assert.Equal(inModule "_.M(fun x -> x.P)", refactored (inModule "_.M(_.P)") "_.P") + +[] +[ 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)")>] +[ y.P")>] +[ x")>] +[ x[0]")>] +[ 1")>] +[ x.P) y")>] +[ x.P @>")>] +[] +let ``No action`` (binding: string) = + let code = inModule binding + use context = TestContext.CreateWithCode code + + let marker = + if binding.IndexOf("fun", StringComparison.Ordinal) >= 0 then + "fun" + else + "+" + + Assert.Empty(actionsIn context code marker) + +[] +let ``Lambda spanning multiple lines still converts to shorthand`` () = + let before = "module M\n\nlet r =\n fun x ->\n x.P\n" + let after = "module M\n\nlet r =\n _.P\n" + Assert.Equal(after, refactored before "fun") + +[] +let ``Converting to shorthand and back restores the lambda`` () = + let original = inModule "List.map (fun x -> x.P) xs" + let shorthand = refactored original "fun" + Assert.Equal(original, refactored shorthand "_.") From 47ba05e48e0acf14c216082fc09e02b852d472f1 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 15:22:54 +0200 Subject: [PATCH 2/3] Write the refactoring test code as multi-line strings Co-Authored-By: Claude Opus 5 (1M context) --- .../Refactors/ConvertDotLambdaTests.fs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs index 7f58a1a3f64..e09ee2f0af0 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs @@ -8,7 +8,12 @@ open Xunit open FSharp.Editor.Tests.Refactors.RefactorTestFramework -let private inModule (binding: string) = $"module M\n\nlet r = {binding}\n" +let private inModule (binding: string) = + $""" +module M + +let r = {binding} +""" let private caretAt (code: string) (marker: string) = code.IndexOf(marker, StringComparison.Ordinal) @@ -37,7 +42,9 @@ let private actionsIn (context: TestContext) (code: string) (marker: string) = [ y.P)", "x.M(_.P)")>] [ x.P", "_.P")>] [ x.P)", "(_.P)")>] -[\n x.Prop) xs", "List.map _.Prop xs")>] +[ + x.Prop) xs""", + "List.map _.Prop xs")>] let ``Lambda reading a member of its parameter converts to shorthand`` (before: string, after: string) = Assert.Equal(inModule after, refactored (inModule before) "fun") @@ -87,8 +94,23 @@ let ``No action`` (binding: string) = [] let ``Lambda spanning multiple lines still converts to shorthand`` () = - let before = "module M\n\nlet r =\n fun x ->\n x.P\n" - let after = "module M\n\nlet r =\n _.P\n" + let before = + """ +module M + +let r = + fun x -> + x.P +""" + + let after = + """ +module M + +let r = + _.P +""" + Assert.Equal(after, refactored before "fun") [] From 0bc4d7575664094f006abf65efe2ae85609975e0 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 21:35:58 +0200 Subject: [PATCH 3/3] Link the release note to the pull request and move it to a random line of its section Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.vNext.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index bde0442f6eb..11a864f16fb 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -2,7 +2,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)) -* Refactoring to convert a lambda that only reads a member of its parameter (`fun x -> x.Prop`, `fun x -> x.M(y)`, `fun x -> x.Items[0]`) into the F# 8 shorthand `_.Prop`, and a `_.Prop` shorthand back into a `fun` lambda. ([Issue #16234](https://github.com/dotnet/fsharp/issues/16234)) +* Refactoring to convert a lambda that only reads a member of its parameter (`fun x -> x.Prop`, `fun x -> x.M(y)`, `fun x -> x.Items[0]`) into the F# 8 shorthand `_.Prop`, and a `_.Prop` shorthand back into a `fun` lambda. ([Issue #16234](https://github.com/dotnet/fsharp/issues/16234), [PR #20535](https://github.com/dotnet/fsharp/pull/20535)) ### Fixed