From f48cdcb5dd24a583e4bf59ce0eee8f6d6d96b566 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 14:27:47 +0200 Subject: [PATCH 1/3] Add a refactoring between option-returning and struct partial active patterns A single-case partial active pattern whose results are all Some/None (or all ValueSome/ValueNone) switches between the two: the result cases and the return type annotation are renamed, and [] is added or removed. Match sites need no change. Local active patterns cannot carry attributes, so there the value option return alone makes them struct, which needs F# 9. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + .../src/FSharp.Editor/FSharp.Editor.resx | 6 + .../Refactor/ConvertActivePatternReturn.fs | 322 ++++++++++++++++++ .../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 + .../ConvertActivePatternReturnTests.fs | 135 ++++++++ 19 files changed, 596 insertions(+) create mode 100644 vsintegration/src/FSharp.Editor/Refactor/ConvertActivePatternReturn.fs create mode 100644 vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertActivePatternReturnTests.fs diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index ba03f663967..4ccba0659d5 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 switch a single-case partial active pattern between returning `option` and returning a struct `voption` (`[]`), rewriting the `Some`/`None` results and the return type annotation; match sites stay unchanged. ### Fixed diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 319bdd5a264..8eb7599d787 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..6212de651e2 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: + + Use struct return for active pattern + + + Use option return for active pattern + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Refactor/ConvertActivePatternReturn.fs b/vsintegration/src/FSharp.Editor/Refactor/ConvertActivePatternReturn.fs new file mode 100644 index 00000000000..e22d0094174 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ConvertActivePatternReturn.fs @@ -0,0 +1,322 @@ +// 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 +open Microsoft.CodeAnalysis.CodeActions +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.Text + +open FSharp.Compiler.Features +open FSharp.Compiler.Syntax +open FSharp.Compiler.SyntaxTrivia +open FSharp.Compiler.Text + +open CancellableTasks + +[] +module private ActivePatternReturnConversion = + + [] + type Annotation = + | Absent + | Head of Ident + | Unsupported + + [] + type Target = + { + IsStruct: bool + Cases: Ident list + Annotation: Annotation + StructAttribute: struct (SynAttributeList * SynAttribute) voption + Keyword: SynLeadingKeyword + IsModuleLevel: bool + } + + let private spanOf (sourceText: SourceText) (m: range) = + RoslynHelpers.FSharpRangeToTextSpan(sourceText, m) + + /// Whether the name is a case or type of a value option (true) or of an option (false). + let private structnessOf (name: string) = + match name with + | "Some" + | "None" + | "option" + | "Option" -> ValueSome false + | "ValueSome" + | "ValueNone" + | "voption" + | "ValueOption" -> ValueSome true + | _ -> ValueNone + + let private raisingFunctions = + set + [ + "failwith" + "failwithf" + "invalidArg" + "invalidOp" + "nullArg" + "raise" + "reraise" + ] + + let private isSingleCasePartialActivePattern (name: Ident) = + match name.idText.Split([| '|' |], StringSplitOptions.RemoveEmptyEntries) with + | [| _; "_" |] -> name.idText.StartsWith("|", StringComparison.Ordinal) + | _ -> false + + let rec private resultLeaves (expr: SynExpr) = + [ + match expr with + | SynExpr.Paren(expr = inner) -> yield! resultLeaves inner + | SynExpr.IfThenElse(thenExpr = thenExpr; elseExpr = Some elseExpr) -> + yield! resultLeaves thenExpr + yield! resultLeaves elseExpr + | SynExpr.Match(clauses = clauses) + | SynExpr.MatchLambda(matchClauses = clauses) -> + for SynMatchClause(resultExpr = result) in clauses do + yield! resultLeaves result + | SynExpr.TryWith(tryExpr = tryExpr; withCases = clauses) -> + yield! resultLeaves tryExpr + + for SynMatchClause(resultExpr = result) in clauses do + yield! resultLeaves result + | SynExpr.TryFinally(tryExpr = body) + | SynExpr.Sequential(expr2 = body) + | SynExpr.Lambda(parsedData = Some(_, body)) -> yield! resultLeaves body + | SynExpr.LetOrUse letOrUse when not letOrUse.IsBang -> yield! resultLeaves letOrUse.Body + | _ -> expr + ] + + let rec private applicationHead (expr: SynExpr) = + match expr with + | SynExpr.Ident ident -> ValueSome ident + | SynExpr.App(isInfix = false; funcExpr = funcExpr) -> applicationHead funcExpr + | _ -> ValueNone + + /// The option cases the results are built with, when every result is a case of one option kind or raises. + let private tryCases (body: SynExpr) = + let heads = resultLeaves body |> List.map applicationHead + + let isRecognized head = + match head with + | ValueSome(head: Ident) -> (structnessOf head.idText).IsSome || raisingFunctions.Contains head.idText + | ValueNone -> false + + let cases = + heads + |> List.choose (function + | ValueSome head when (structnessOf head.idText).IsSome -> Some head + | _ -> None) + + match cases with + | first :: _ when List.forall isRecognized heads -> + let structness = structnessOf first.idText + + if cases |> List.forall (fun case -> structnessOf case.idText = structness) then + structness |> ValueOption.map (fun isStruct -> struct (isStruct, cases)) + else + ValueNone + | _ -> ValueNone + + let private annotationOf (isStruct: bool) (returnInfo: SynBindingReturnInfo option) = + match returnInfo with + | None -> Annotation.Absent + | Some(SynBindingReturnInfo(typeName = SynType.App(typeName = SynType.LongIdent(SynLongIdent(id = [ head ]))))) when + structnessOf head.idText = ValueSome isStruct + -> + Annotation.Head head + | Some _ -> Annotation.Unsupported + + let private isReturnStruct (attribute: SynAttribute) = + match attribute.Target, List.tryLast attribute.TypeName.LongIdent with + | Some target, Some name -> + String.Equals(target.idText, "return", StringComparison.Ordinal) + && (String.Equals(name.idText, "Struct", StringComparison.Ordinal) + || String.Equals(name.idText, "StructAttribute", StringComparison.Ordinal)) + | _ -> false + + let private tryReturnStructAttribute (attributes: SynAttributes) = + attributes + |> Seq.tryPickV (fun list -> + list.Attributes + |> Seq.tryFindV isReturnStruct + |> ValueOption.map (fun attribute -> struct (list, attribute))) + + /// Whether the caret is on the attributes, the keyword, the name or the parameters of the binding. + let private isOnHeader (caret: pos) (attributes: SynAttributes) (keyword: SynLeadingKeyword) (headPat: SynPat) = + let start = + (keyword.Range.Start, attributes) + ||> List.fold (fun start list -> + if Position.posGeq start list.Range.Start then + list.Range.Start + else + start) + + Position.posGeq caret start && Position.posGeq headPat.Range.End caret + + let tryTarget (caret: pos) (parseTree: ParsedInput) = + (ValueNone, parseTree) + ||> ParsedInput.fold (fun found path node -> + match found, node with + | ValueNone, + SyntaxNode.SynBinding(SynBinding( + attributes = attributes + headPat = SynPat.LongIdent(longDotId = SynLongIdent(id = [ name ])) as headPat + returnInfo = returnInfo + expr = body + trivia = trivia)) when + isSingleCasePartialActivePattern name + && isOnHeader caret attributes trivia.LeadingKeyword headPat + -> + let body = + match returnInfo, body with + | Some _, SynExpr.Typed(expr = inner) -> inner + | _ -> body + + match trivia.LeadingKeyword, tryCases body with + | (SynLeadingKeyword.Let _ | SynLeadingKeyword.LetRec _ | SynLeadingKeyword.And _), ValueSome(struct (isStruct, cases)) -> + let structAttribute = tryReturnStructAttribute attributes + + match annotationOf isStruct returnInfo with + | Annotation.Unsupported -> ValueNone + | _ when not isStruct && structAttribute.IsSome -> ValueNone + | annotation -> + ValueSome + { + IsStruct = isStruct + Cases = cases + Annotation = annotation + StructAttribute = structAttribute + Keyword = trivia.LeadingKeyword + IsModuleLevel = + match path with + | SyntaxNode.SynModule(SynModuleDecl.Let _) :: _ -> true + | _ -> false + } + | _ -> ValueNone + | _ -> found) + + let private lineBreakOf (sourceText: SourceText) (line: TextLine) = + match line.EndIncludingLineBreak - line.End with + | 0 -> Environment.NewLine + | length -> sourceText.ToString(TextSpan(line.End, length)) + + /// Adds or removes the `Value`/`v` prefix: `Some` ⟷ `ValueSome`, `option` ⟷ `voption`. + let private renamed (sourceText: SourceText) (isStruct: bool) (ident: Ident) = + let start = (spanOf sourceText ident.idRange).Start + let prefix = if Char.IsLower ident.idText[0] then "v" else "Value" + + if isStruct then + TextChange(TextSpan(start, prefix.Length), "") + else + TextChange(TextSpan(start, 0), prefix) + + let private structAttributeInsertion (sourceText: SourceText) (target: Target) = + let keyword = target.Keyword.Range + let line = sourceText.Lines[Line.toZ keyword.StartLine] + let indent = sourceText.ToString(TextSpan(line.Start, keyword.StartColumn)) + + match target.Keyword with + | SynLeadingKeyword.Let _ + | SynLeadingKeyword.LetRec _ when String.IsNullOrWhiteSpace indent -> + TextChange(TextSpan(line.Start, 0), $"{indent}[]{lineBreakOf sourceText line}") + | _ -> TextChange(TextSpan((spanOf sourceText keyword).End, 0), " []") + + let private attributeRemoval (sourceText: SourceText) (list: SynAttributeList) (attribute: SynAttribute) = + match list.Attributes with + | [ _ ] -> + let listSpan = spanOf sourceText list.Range + let line = sourceText.Lines.GetLineFromPosition listSpan.Start + let before = sourceText.ToString(TextSpan.FromBounds(line.Start, listSpan.Start)) + let after = sourceText.ToString(TextSpan.FromBounds(listSpan.End, line.End)) + + if String.IsNullOrWhiteSpace before && String.IsNullOrWhiteSpace after then + TextSpan.FromBounds(line.Start, line.EndIncludingLineBreak) + else + TextSpan(listSpan.Start, listSpan.Length + after.Length - after.TrimStart().Length) + | attributes -> + let index = + attributes + |> List.findIndex (fun other -> obj.ReferenceEquals(other, attribute)) + + let attributeSpan = spanOf sourceText attribute.Range + + if index + 1 < attributes.Length then + TextSpan.FromBounds(attributeSpan.Start, (spanOf sourceText (List.item (index + 1) attributes).Range).Start) + else + TextSpan.FromBounds((spanOf sourceText (List.item (index - 1) attributes).Range).End, attributeSpan.End) + + let changes (sourceText: SourceText) (target: Target) = + [ + for case in target.Cases do + renamed sourceText target.IsStruct case + + match target.Annotation with + | Annotation.Head head -> renamed sourceText target.IsStruct head + | Annotation.Absent + | Annotation.Unsupported -> () + + // Attributes are not permitted on local bindings: there the value option return type alone makes it struct. + match target.IsStruct, target.StructAttribute with + | false, _ when target.IsModuleLevel -> structAttributeInsertion sourceText target + | true, ValueSome(struct (list, attribute)) -> TextChange(attributeRemoval sourceText list attribute, "") + | _ -> () + ] + |> List.sortBy _.Span.Start + +[] +type internal FSharpConvertActivePatternReturnRefactoring [] () = + inherit CodeRefactoringProvider() + + static let hasSignatureFile (document: Document) = + let signaturePath = document.FilePath + "i" + + document.Project.Documents + |> Seq.exists (fun d -> String.Equals(d.FilePath, signaturePath, StringComparison.OrdinalIgnoreCase)) + + 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 FSharpConvertActivePatternReturnRefactoring) + + let caret = + let linePosition = sourceText.Lines.GetLinePosition context.Span.Start + Position.mkPos (Line.fromZ linePosition.Line) linePosition.Character + + match ActivePatternReturnConversion.tryTarget caret parseResults.ParseTree with + | ValueSome target when not (hasSignatureFile document) -> + let! _, langVersion = document.GetFsharpParsingOptionsAsync(nameof FSharpConvertActivePatternReturnRefactoring) + + let isSupported = + target.IsStruct + || target.IsModuleLevel + || LanguageVersion(langVersion).SupportsFeature + LanguageFeature.BooleanReturningAndReturnTypeDirectedPartialActivePattern + + if isSupported then + let title = + if target.IsStruct then + SR.UseOptionActivePatternReturn() + else + SR.UseStructActivePatternReturn() + + let changedDocument = + cancellableTask { + let changes = ActivePatternReturnConversion.changes sourceText target + return document.WithText(sourceText.WithChanges changes) + } + + context.RegisterRefactoring(CodeAction.Create(title, changedDocument, title)) + | _ -> () + } + |> CancellableTask.startAsTask context.CancellationToken diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf index cd8c46bf705..15df8c78883 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -403,6 +403,16 @@ Zobrazit poznámky v Rychlých informacích Použít nameof + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. Použijte interpolaci řetězce v trojitých uvozovkách. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index bce1941f0b1..98d437f45b1 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -403,6 +403,16 @@ Hinweise in QuickInfo anzeigen "nameof" verwenden + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. Verwenden Sie die Interpolation von dreifachen Zeichenfolgen in Anführungszeichen. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index fa8cb62c422..328ddd29293 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -403,6 +403,16 @@ Mostrar comentarios en Información rápida Usar 'nameof' + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. Use la interpolación de cadenas entre comillas triples. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index e7ec71e839e..744aee883fd 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -403,6 +403,16 @@ Afficher les notes dans Info express Utiliser « nameof » + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. Utilisez l’interpolation de chaîne entre guillemets triples. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 327a7ca362f..2dab438a02e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -403,6 +403,16 @@ Mostra i commenti in Informazioni rapide Usa 'nameof' + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. Usare l'interpolazione di stringhe con virgolette triple. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index d45234c011a..bc4a63828b8 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -403,6 +403,16 @@ F# 構文規則に準拠した改行を追加して、署名を指定された 'nameof' を使用する + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. 三重引用符で囲まれた文字列補間を使用します。 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index 3248e0641ea..1d514725b87 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -403,6 +403,16 @@ F# 구문 규칙에 맞는 줄 바꿈을 추가하여 지정된 너비에 시그 'nameof' 사용 + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. 삼중 따옴표로 묶인 분자열 보간을 사용합니다. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index abc39f15da5..76afb43f459 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -403,6 +403,16 @@ Pokaż uwagi w szybkich informacjach Użyj wyrażenia "nameof" + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. Użyj interpolacji ciągu z potrójnym cudzysłowem. 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..6891281b65d 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -403,6 +403,16 @@ Mostrar os comentários nas Informações Rápidas Usar 'nameof' + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. Usar interpolação de cadeia de caracteres entre aspas triplas. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 47cda215312..47b6fed717e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -403,6 +403,16 @@ Show remarks in Quick Info Использовать "nameof" + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. Использовать интерполяции строк в тройных кавычках. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 58aa5d54c43..e2fa042d0b0 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -403,6 +403,16 @@ Açıklamaları Hızlı Bilgide göster “Nameof” kullanın + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. Üçlü tırnak içine alınmış dize ilişkilendirmesini kullanın. 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..9e50a0bc718 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -403,6 +403,16 @@ Show remarks in Quick Info 使用 "nameof" + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. 使用三引号字符串内插。 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..38573a37608 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -403,6 +403,16 @@ Show remarks in Quick Info 使用 'nameof' + + Use option return for active pattern + Use option return for active pattern + + + + Use struct return for active pattern + Use struct return for active pattern + + Use triple quoted string interpolation. 使用三引號字串插補。 diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index ecce1205b8c..574ad9b0074 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/ConvertActivePatternReturnTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertActivePatternReturnTests.fs new file mode 100644 index 00000000000..e0d0939a0f0 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertActivePatternReturnTests.fs @@ -0,0 +1,135 @@ +module FSharp.Editor.Tests.Refactors.ConvertActivePatternReturnTests + +open System +open System.Threading + +open Microsoft.CodeAnalysis.CodeActions +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.Text + +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks + +open Xunit + +open FSharp.Compiler.Diagnostics + +open FSharp.Editor.Tests.Helpers +open FSharp.Editor.Tests.Refactors.RefactorTestFramework + +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 FSharpConvertActivePatternReturnRefactoring()) + + let _, checkResults = + document.GetFSharpParseAndCheckResultsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + Assert.Empty( + checkResults.Diagnostics + |> Array.filter (fun diagnostic -> diagnostic.Severity = FSharpDiagnosticSeverity.Error) + ) + + (document.GetTextAsync() |> GetTaskResult).ToString() + +let private actionsAt (code: string) (marker: string) = + use context = TestContext.CreateWithCode code + tryGetRefactoringActions code (caretAt code marker) context (new FSharpConvertActivePatternReturnRefactoring()) + +[] +[]\nlet (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n")>] +[ 0 -> Some v\n | _ -> None\n", + "(|Positive", + "module M\n\n[]\nlet (|Positive|_|) x =\n match x with\n | v when v > 0 -> ValueSome v\n | _ -> ValueNone\n")>] +[ None\n", + "(|Int", + "module M\n\n[]\nlet (|Int|_|) (s: string) =\n if s.Length = 0 then failwith \"empty\"\n else\n try ValueSome(int s) with _ -> ValueNone\n")>] +[]\nlet (|Even|_|) (x: int) : unit voption = if x % 2 = 0 then ValueSome () else ValueNone\n")>] +[ 1\n | _ -> 0\n", + "(|Even", + "module M\n\nlet f v =\n let (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n match v with\n | Even -> 1\n | _ -> 0\n")>] +[]\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n", + "(|Even", + "module M\n\n[]\n[]\nlet (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n")>] +[ 0 then Some () else None\nand (|B|_|) x = if x < 0 then Some () else None\n", + "(|B", + "module M\n\nlet rec (|A|_|) x = if x > 0 then Some () else None\nand [] (|B|_|) x = if x < 0 then ValueSome () else ValueNone\n")>] +let ``Option-returning active pattern converts to a struct one`` (before: string, marker: string, after: string) = + Assert.Equal(after, refactored before marker) + +[] +[] +[] let (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n", + "module M\n\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n")>] +[]\nlet (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n", + "module M\n\n[]\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n")>] +let ``Struct active pattern converts to an option-returning one`` (before: string, after: string) = + Assert.Equal(after, refactored before "(|Even") + +[] +[] +[ 0 -> Some v\n | _ -> None\n", "(|Positive")>] +[ 1\n | _ -> 0\n", + "(|Even")>] +[]\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n", "(|Even")>] +[ 0 then Some () else None\nand (|B|_|) x = if x < 0 then Some () else None\n", "(|B")>] +let ``Converting to struct and back restores the active pattern`` (original: string, marker: string) = + Assert.Equal(original, refactored (refactored original marker) marker) + +[] +let ``Title names the target return kind`` () = + let optionPattern = + "module M\n\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n" + + let structPattern = + "module M\n\n[]\nlet (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n" + + Assert.Equal("Use struct return for active pattern", (actionsAt optionPattern "(|Even" |> Seq.exactlyOne).Title) + Assert.Equal("Use option return for active pattern", (actionsAt structPattern "(|Even" |> Seq.exactlyOne).Title) + +[] +[] +[ 0 then Some () else None\n", "(|A")>] +[ Option.filter (fun v -> v > 0)\n", "(|Positive")>] +[] +[] +[] +[] +[] +[] +let ``No action`` (code: string, marker: string) = Assert.Empty(actionsAt code marker) + +[] +let ``Local active pattern is not converted to struct before F# 9`` () = + let code = + "module M\n\nlet f v =\n let (|Even|_|) x = if x % 2 = 0 then Some () else None\n match v with\n | Even -> 1\n | _ -> 0\n" + + use context = + new TestContext(RoslynTestHelpers.CreateSolution(code, extraFSharpProjectOtherOptions = [| "--langversion:8.0" |])) + + Assert.Empty(tryGetRefactoringActions code (caretAt code "(|Even") context (new FSharpConvertActivePatternReturnRefactoring())) + +[] +let ``No action when the file has a signature`` () = + let code = "module M\n\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n" + let signature = "module M\n\nval (|Even|_|): int -> unit option\n" + let document = RoslynTestHelpers.GetFsiAndFsDocuments signature code |> Seq.last + let actions = ResizeArray() + + let context = + CodeRefactoringContext(document, TextSpan(caretAt code "(|Even", 1), (fun action -> actions.Add action), CancellationToken.None) + + (new FSharpConvertActivePatternReturnRefactoring()).ComputeRefactoringsAsync(context).GetAwaiter().GetResult() + + Assert.False(document.IsFSharpSignatureFile) + Assert.Empty(actions) From f706111ea1f4acfa163389bae3087c70b79a6a01 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 15:36:50 +0200 Subject: [PATCH 2/3] Write the active pattern test code as multi-line strings Co-Authored-By: Claude Opus 5 (1M context) --- .../ConvertActivePatternReturnTests.fs | 303 +++++++++++++++--- 1 file changed, 252 insertions(+), 51 deletions(-) diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertActivePatternReturnTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertActivePatternReturnTests.fs index e0d0939a0f0..b8a5d39aeb1 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertActivePatternReturnTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertActivePatternReturnTests.fs @@ -41,93 +41,294 @@ let private actionsAt (code: string) (marker: string) = use context = TestContext.CreateWithCode code tryGetRefactoringActions code (caretAt code marker) context (new FSharpConvertActivePatternReturnRefactoring()) +let private evenOption = + """ +module M + +let (|Even|_|) x = if x % 2 = 0 then Some () else None +""" + +let private evenStruct = + """ +module M + +[] +let (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone +""" + +let private localEven = + """ +module M + +let f v = + let (|Even|_|) x = if x % 2 = 0 then Some () else None + match v with + | Even -> 1 + | _ -> 0 +""" + [] -[]\nlet (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n")>] -[ 0 -> Some v\n | _ -> None\n", +[ 0 -> Some v + | _ -> None +""", "(|Positive", - "module M\n\n[]\nlet (|Positive|_|) x =\n match x with\n | v when v > 0 -> ValueSome v\n | _ -> ValueNone\n")>] -[ None\n", + """ +module M + +[] +let (|Positive|_|) x = + match x with + | v when v > 0 -> ValueSome v + | _ -> ValueNone +""")>] +[ None +""", "(|Int", - "module M\n\n[]\nlet (|Int|_|) (s: string) =\n if s.Length = 0 then failwith \"empty\"\n else\n try ValueSome(int s) with _ -> ValueNone\n")>] -[] +let (|Int|_|) (s: string) = + if s.Length = 0 then failwith "empty" + else + try ValueSome(int s) with _ -> ValueNone +""")>] +[]\nlet (|Even|_|) (x: int) : unit voption = if x % 2 = 0 then ValueSome () else ValueNone\n")>] -[ 1\n | _ -> 0\n", + """ +module M + +[] +let (|Even|_|) (x: int) : unit voption = if x % 2 = 0 then ValueSome () else ValueNone +""")>] +[ 1 + | _ -> 0 +""", "(|Even", - "module M\n\nlet f v =\n let (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n match v with\n | Even -> 1\n | _ -> 0\n")>] -[]\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n", + """ +module M + +let f v = + let (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone + match v with + | Even -> 1 + | _ -> 0 +""")>] +[] +let (|Even|_|) x = if x % 2 = 0 then Some () else None +""", "(|Even", - "module M\n\n[]\n[]\nlet (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n")>] -[ 0 then Some () else None\nand (|B|_|) x = if x < 0 then Some () else None\n", + """ +module M + +[] +[] +let (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone +""")>] +[ 0 then Some () else None +and (|B|_|) x = if x < 0 then Some () else None +""", "(|B", - "module M\n\nlet rec (|A|_|) x = if x > 0 then Some () else None\nand [] (|B|_|) x = if x < 0 then ValueSome () else ValueNone\n")>] + """ +module M + +let rec (|A|_|) x = if x > 0 then Some () else None +and [] (|B|_|) x = if x < 0 then ValueSome () else ValueNone +""")>] let ``Option-returning active pattern converts to a struct one`` (before: string, marker: string, after: string) = Assert.Equal(after, refactored before marker) +[] +let ``Option-returning active pattern gets the attribute on its own line`` () = + Assert.Equal(evenStruct, refactored evenOption "(|Even") + [] -[] -[] let (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n", - "module M\n\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n")>] -[]\nlet (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n", - "module M\n\n[]\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n")>] +[] +[] let (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone +""", + """ +module M + +let (|Even|_|) x = if x % 2 = 0 then Some () else None +""")>] +[] +let (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone +""", + """ +module M + +[] +let (|Even|_|) x = if x % 2 = 0 then Some () else None +""")>] let ``Struct active pattern converts to an option-returning one`` (before: string, after: string) = Assert.Equal(after, refactored before "(|Even") [] -[] -[ 0 -> Some v\n | _ -> None\n", "(|Positive")>] -[ 1\n | _ -> 0\n", +[ 0 -> Some v + | _ -> None +""", + "(|Positive")>] +[] +let (|Even|_|) x = if x % 2 = 0 then Some () else None +""", "(|Even")>] -[]\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n", "(|Even")>] -[ 0 then Some () else None\nand (|B|_|) x = if x < 0 then Some () else None\n", "(|B")>] +[ 0 then Some () else None +and (|B|_|) x = if x < 0 then Some () else None +""", + "(|B")>] let ``Converting to struct and back restores the active pattern`` (original: string, marker: string) = Assert.Equal(original, refactored (refactored original marker) marker) +[] +let ``Converting to struct and back restores module-level and local active patterns`` () = + Assert.Equal(evenOption, refactored (refactored evenOption "(|Even") "(|Even") + Assert.Equal(localEven, refactored (refactored localEven "(|Even") "(|Even") + [] let ``Title names the target return kind`` () = - let optionPattern = - "module M\n\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n" + Assert.Equal("Use struct return for active pattern", (actionsAt evenOption "(|Even" |> Seq.exactlyOne).Title) + Assert.Equal("Use option return for active pattern", (actionsAt evenStruct "(|Even" |> Seq.exactlyOne).Title) - let structPattern = - "module M\n\n[]\nlet (|Even|_|) x = if x % 2 = 0 then ValueSome () else ValueNone\n" +[] +[ Seq.exactlyOne).Title) - Assert.Equal("Use option return for active pattern", (actionsAt structPattern "(|Even" |> Seq.exactlyOne).Title) +let (|Even|Odd|) x = if x % 2 = 0 then Even else Odd +""", + "(|Even")>] +[] -[] -[ 0 then Some () else None\n", "(|A")>] -[ Option.filter (fun v -> v > 0)\n", "(|Positive")>] -[] -[] -[] -[] -[] -[] +let (|A|B|_|) x = if x > 0 then Some () else None +""", + "(|A")>] +[ Option.filter (fun v -> v > 0) +""", + "(|Positive")>] +[] +[] +[] +[] +[] +[] let ``No action`` (code: string, marker: string) = Assert.Empty(actionsAt code marker) [] let ``Local active pattern is not converted to struct before F# 9`` () = - let code = - "module M\n\nlet f v =\n let (|Even|_|) x = if x % 2 = 0 then Some () else None\n match v with\n | Even -> 1\n | _ -> 0\n" - use context = - new TestContext(RoslynTestHelpers.CreateSolution(code, extraFSharpProjectOtherOptions = [| "--langversion:8.0" |])) + new TestContext(RoslynTestHelpers.CreateSolution(localEven, extraFSharpProjectOtherOptions = [| "--langversion:8.0" |])) - Assert.Empty(tryGetRefactoringActions code (caretAt code "(|Even") context (new FSharpConvertActivePatternReturnRefactoring())) + Assert.Empty( + tryGetRefactoringActions localEven (caretAt localEven "(|Even") context (new FSharpConvertActivePatternReturnRefactoring()) + ) [] let ``No action when the file has a signature`` () = - let code = "module M\n\nlet (|Even|_|) x = if x % 2 = 0 then Some () else None\n" - let signature = "module M\n\nval (|Even|_|): int -> unit option\n" - let document = RoslynTestHelpers.GetFsiAndFsDocuments signature code |> Seq.last + let signature = + """ +module M + +val (|Even|_|): int -> unit option +""" + + let document = + RoslynTestHelpers.GetFsiAndFsDocuments signature evenOption |> Seq.last + let actions = ResizeArray() let context = - CodeRefactoringContext(document, TextSpan(caretAt code "(|Even", 1), (fun action -> actions.Add action), CancellationToken.None) + CodeRefactoringContext( + document, + TextSpan(caretAt evenOption "(|Even", 1), + (fun action -> actions.Add action), + CancellationToken.None + ) (new FSharpConvertActivePatternReturnRefactoring()).ComputeRefactoringsAsync(context).GetAwaiter().GetResult() From 0a0b284d87a6dc5a705cc35fb09edbcfe3f2f8e2 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 14 Sep 2026 21:36:15 +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 4ccba0659d5..ce21031b0f8 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -1,8 +1,8 @@ ### Added * 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)) +* Refactoring to switch a single-case partial active pattern between returning `option` and returning a struct `voption` (`[]`), rewriting the `Some`/`None` results and the return type annotation; match sites stay unchanged. ([PR #20545](https://github.com/dotnet/fsharp/pull/20545)) * 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 switch a single-case partial active pattern between returning `option` and returning a struct `voption` (`[]`), rewriting the `Some`/`None` results and the return type annotation; match sites stay unchanged. ### Fixed