diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index ba03f663967..b4a81fd20fd 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -1,5 +1,6 @@ ### Added +* Refactoring to switch a member's optional parameter between `?x` (`option`) and `[] ?x` (`voption`), rewriting `defaultArg`, `Option` functions and `match` cases on it in the body and `?x = value` arguments at its call sites in the solution. ([PR #20546](https://github.com/dotnet/fsharp/pull/20546)) * 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)) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 319bdd5a264..9c42bb8bb73 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..8222df5af71 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 'voption' for optional parameter + + + Use 'option' for optional parameter + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Refactor/ConvertOptionalParameterStruct.fs b/vsintegration/src/FSharp.Editor/Refactor/ConvertOptionalParameterStruct.fs new file mode 100644 index 00000000000..0f6a5777446 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ConvertOptionalParameterStruct.fs @@ -0,0 +1,451 @@ +// 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 System.Threading +open System.Threading.Tasks + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CodeActions +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.Text + +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Features +open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text + +open CancellableTasks + +[] +module private OptionalParameterConversion = + + [] + type Parameter = + { + Ident: Ident + OptionalValRange: range + StructAttribute: struct (SynAttributeList * SynAttribute) voption + MemberName: Ident + MemberRange: range + } + + member this.IsStruct = this.StructAttribute.IsSome + + let private spanOf (sourceText: SourceText) (m: range) = + RoslynHelpers.FSharpRangeToTextSpan(sourceText, m) + + let private hasText (text: string) (ident: Ident) = + String.Equals(ident.idText, text, StringComparison.Ordinal) + + let private isSame (expr: SynExpr) (other: SynExpr) = obj.ReferenceEquals(expr, other) + + let private isOperator (name: string) (expr: SynExpr) = + match expr with + | SynExpr.LongIdent(longDotId = SynLongIdent(id = [ operator ])) -> hasText name operator + | _ -> false + + [] + let private (|SingleIdent|_|) (expr: SynExpr) = + match expr with + | SynExpr.Ident ident + | SynExpr.LongIdent(longDotId = SynLongIdent(id = [ ident ])) -> ValueSome ident + | _ -> ValueNone + + /// Functions of both option modules that take the option last and return the same type for either kind. + let private moduleFunctions = + set + [ + "contains" + "count" + "defaultValue" + "defaultWith" + "exists" + "forall" + "get" + "isNone" + "isSome" + "iter" + "toArray" + "toList" + "toNullable" + "toObj" + ] + + let private isStructAttribute (attribute: SynAttribute) = + attribute.Target.IsNone + && match List.tryLast attribute.TypeName.LongIdent with + | Some name -> hasText "Struct" name || hasText "StructAttribute" name + | None -> false + + let rec private tryOptionalVal (pat: SynPat) = + match pat with + | SynPat.OptionalVal(ident, m) -> ValueSome(struct (ident, m)) + | SynPat.Typed(pat = inner) + | SynPat.Attrib(pat = inner) + | SynPat.Paren(pat = inner) -> tryOptionalVal inner + | _ -> ValueNone + + let rec private attributesOf (pat: SynPat) = + [ + match pat with + | SynPat.Attrib(pat = inner; attributes = attributes) -> + yield! attributes + yield! attributesOf inner + | SynPat.Typed(pat = inner) + | SynPat.Paren(pat = inner) -> yield! attributesOf inner + | _ -> () + ] + + let private tryStructAttribute (attributes: SynAttributes) = + attributes + |> Seq.tryPickV (fun list -> + list.Attributes + |> Seq.tryFindV isStructAttribute + |> ValueOption.map (fun attribute -> struct (list, attribute))) + + /// The name and the whole range of the member whose parameters contain the pattern, when it is a member of a type. + let rec private tryMember (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynBinding(SynBinding(headPat = SynPat.LongIdent(longDotId = SynLongIdent(id = ids))) as binding) :: SyntaxNode.SynMemberDefn(SynMemberDefn.Member _) :: _ -> + match List.tryLast ids with + | Some name -> ValueSome(struct (name, binding.RangeOfBindingWithRhs)) + | None -> ValueNone + | SyntaxNode.SynPat _ :: rest -> tryMember rest + | _ -> ValueNone + + let tryParameter (caret: pos) (parseTree: ParsedInput) = + (ValueNone, parseTree) + ||> ParsedInput.fold (fun found path node -> + match found, node with + | ValueNone, SyntaxNode.SynPat pat when Position.posGeq caret pat.Range.Start && Position.posGeq pat.Range.End caret -> + match tryOptionalVal pat, tryMember path with + | ValueSome(struct (ident, m)), ValueSome(struct (memberName, memberRange)) -> + ValueSome + { + Ident = ident + OptionalValRange = m + StructAttribute = tryStructAttribute (attributesOf pat) + MemberName = memberName + MemberRange = memberRange + } + | _ -> ValueNone + | _ -> found) + + let rec private functionOf (expr: SynExpr) = + match expr with + | SynExpr.App(isInfix = false; funcExpr = funcExpr) -> functionOf funcExpr + | _ -> expr + + let private tryModuleQualifier (isStruct: bool) (func: SynExpr) = + match functionOf func with + | SynExpr.LongIdent(longDotId = SynLongIdent(id = [ qualifier; name ])) when + hasText (if isStruct then "ValueOption" else "Option") qualifier + && moduleFunctions.Contains name.idText + -> + ValueSome [ qualifier ] + | _ -> ValueNone + + let private isCase (isStruct: bool) (ident: Ident) = + if isStruct then + hasText "ValueSome" ident || hasText "ValueNone" ident + else + hasText "Some" ident || hasText "None" ident + + let private tryClauseHeads (isStruct: bool) (clauses: SynMatchClause list) = + (ValueSome [], clauses) + ||> List.fold (fun heads (SynMatchClause(pat = pat)) -> + match heads, pat with + | ValueSome heads, (SynPat.LongIdent(longDotId = SynLongIdent(id = [ head ])) | SynPat.Named(ident = SynIdent(head, _))) when + isCase isStruct head + -> + ValueSome(head :: heads) + | ValueSome heads, (SynPat.Wild _ | SynPat.Named _) -> ValueSome heads + | _ -> ValueNone) + + /// The identifiers to rename for one use of the parameter in the member body, when the use keeps its type. + let private tryUseRenames (isStruct: bool) (node: SynExpr) (path: SyntaxVisitorPath) = + match node, path with + | SynExpr.LongIdent(longDotId = SynLongIdent(id = _ :: property :: _)), _ when + hasText "IsSome" property + || hasText "IsNone" property + || hasText "Value" property + -> + ValueSome [] + | _, SyntaxNode.SynExpr(SynExpr.App(isInfix = false; funcExpr = SingleIdent func; argExpr = arg)) :: _ when + isSame arg node + && hasText (if isStruct then "defaultValueArg" else "defaultArg") func + -> + ValueSome [ func ] + | _, SyntaxNode.SynExpr(SynExpr.App(isInfix = false; funcExpr = func; argExpr = arg)) :: _ when isSame arg node -> + tryModuleQualifier isStruct func + | _, + SyntaxNode.SynExpr(SynExpr.App(isInfix = true; funcExpr = pipe; argExpr = arg)) :: SyntaxNode.SynExpr(SynExpr.App( + isInfix = false; argExpr = func)) :: _ when isSame arg node && isOperator "op_PipeRight" pipe -> + tryModuleQualifier isStruct func + | _, SyntaxNode.SynExpr(SynExpr.Match(expr = scrutinee; clauses = clauses)) :: _ when isSame scrutinee node -> + tryClauseHeads isStruct clauses + | _ -> ValueNone + + let private tryUseNode (parseTree: ParsedInput) (useRange: range) = + (useRange.Start, parseTree) + ||> ParsedInput.tryPickLast (fun path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Ident ident as expr) + | SyntaxNode.SynExpr(SynExpr.LongIdent(longDotId = SynLongIdent(id = ident :: _)) as expr) when + Position.posEq ident.idRange.Start useRange.Start + && Position.posEq ident.idRange.End useRange.End + -> + Some(expr, path) + | _ -> None) + + /// The identifiers to rename in the member body, when every use of the parameter keeps its type. + let tryBodyRenames (parseTree: ParsedInput) (isStruct: bool) (uses: range seq) = + (ValueSome [], uses) + ||> Seq.fold (fun renames useRange -> + match renames, tryUseNode parseTree useRange with + | ValueSome renames, Some(node, path) -> + tryUseRenames isStruct node path + |> ValueOption.map (fun more -> [ yield! more; yield! renames ]) + | _ -> ValueNone) + + /// `Some` ⟷ `ValueSome`, `Option` ⟷ `ValueOption`, `defaultArg` ⟷ `defaultValueArg`. + let private renamed (sourceText: SourceText) (isStruct: bool) (ident: Ident) = + let text = + match isStruct, ident.idText with + | false, "defaultArg" -> "defaultValueArg" + | true, "defaultValueArg" -> "defaultArg" + | false, name -> "Value" + name + | true, name -> name.Substring "Value".Length + + TextChange(spanOf sourceText ident.idRange, text) + + 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 after = sourceText.ToString(TextSpan.FromBounds(listSpan.End, line.End)) + 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 definitionChanges (sourceText: SourceText) (parameter: Parameter) (renames: Ident list) = + [ + match parameter.StructAttribute with + | ValueSome(struct (list, attribute)) -> TextChange(attributeRemoval sourceText list attribute, "") + | ValueNone -> TextChange(TextSpan((spanOf sourceText parameter.OptionalValRange).Start, 0), "[] ") + + for ident in renames do + renamed sourceText parameter.IsStruct ident + ] + + let private isAtomic (expr: SynExpr) = + match expr with + | SynExpr.Ident _ + | SynExpr.LongIdent _ + | SynExpr.Paren _ + | SynExpr.Const _ -> true + | _ -> false + + /// The values passed as `?name = value` in the arguments of a call. + let private optionalArgumentValues (name: string) (arguments: SynExpr) = + let arguments = + match arguments with + | SynExpr.Paren(expr = SynExpr.Tuple(exprs = exprs)) -> exprs + | SynExpr.Paren(expr = single) -> [ single ] + | _ -> [] + + arguments + |> List.choose (function + | SynExpr.App( + isInfix = false + funcExpr = SynExpr.App( + isInfix = true + funcExpr = equals + argExpr = SynExpr.LongIdent(isOptional = true; longDotId = SynLongIdent(id = [ argumentName ]))) + argExpr = value) when isOperator "op_Equality" equals && hasText name argumentName -> Some value + | _ -> None) + + let private valueChanges (sourceText: SourceText) (isStruct: bool) (value: SynExpr) = + let valueSpan = spanOf sourceText value.Range + let conversion = if isStruct then "toOption" else "ofOption" + let inverse = if isStruct then "ofOption" else "toOption" + + match functionOf value, value with + | SingleIdent head, _ when isCase isStruct head -> [ renamed sourceText isStruct head ] + | SynExpr.LongIdent(longDotId = SynLongIdent(id = [ qualifier; name ])), SynExpr.App(isInfix = false; argExpr = inner) when + hasText "ValueOption" qualifier && hasText inverse name + -> + [ TextChange(valueSpan, sourceText.ToString(spanOf sourceText inner.Range)) ] + | _ when isAtomic value -> [ TextChange(TextSpan(valueSpan.Start, 0), $"ValueOption.{conversion} ") ] + | _ -> + [ + TextChange(TextSpan(valueSpan.Start, 0), $"ValueOption.{conversion} (") + TextChange(TextSpan(valueSpan.End, 0), ")") + ] + + /// Changes to the `?name = value` arguments of the call whose function ends at the use of the member. + let callSiteChanges (sourceText: SourceText) (parseTree: ParsedInput) (isStruct: bool) (name: string) (useRange: range) = + let arguments = + (useRange.Start, parseTree) + ||> ParsedInput.tryPickLast (fun _ node -> + match node with + | SyntaxNode.SynExpr(SynExpr.App(isInfix = false; funcExpr = func; argExpr = arguments)) when + Position.posEq func.Range.End useRange.End + -> + Some arguments + | _ -> None) + + match arguments with + | Some arguments -> + optionalArgumentValues name arguments + |> List.collect (valueChanges sourceText isStruct) + | None -> [] + +[] +type internal FSharpConvertOptionalParameterStructRefactoring [] () = + 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)) + + static let tryGetSymbolUse (checkResults: FSharpCheckFileResults) (sourceText: SourceText) (ident: Ident) = + let line = sourceText.Lines[Line.toZ ident.idRange.EndLine].ToString() + checkResults.GetSymbolUseAtLocation(ident.idRange.EndLine, ident.idRange.EndColumn, line, [ ident.idText ]) + + static let isConvertibleMember (symbol: FSharpSymbol) = + match symbol with + | :? FSharpMemberOrFunctionOrValue as mfv -> + not ( + mfv.IsOverrideOrExplicitInterfaceImplementation + || mfv.IsDispatchSlot + || mfv.IsConstructor + || mfv.IsExtensionMember + ) + | _ -> false + + override _.ComputeRefactoringsAsync context = + cancellableTask { + let document = context.Document + + if not (document.IsFSharpSignatureFile || hasSignatureFile document) then + let! cancellationToken = CancellableTask.getCancellationToken () + let! sourceText = document.GetTextAsync cancellationToken + let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpConvertOptionalParameterStructRefactoring) + + let caret = + let linePosition = sourceText.Lines.GetLinePosition context.Span.Start + Position.mkPos (Line.fromZ linePosition.Line) linePosition.Character + + match OptionalParameterConversion.tryParameter caret parseResults.ParseTree with + | ValueNone -> () + | ValueSome parameter -> + let! _, langVersion = document.GetFsharpParsingOptionsAsync(nameof FSharpConvertOptionalParameterStructRefactoring) + + if + parameter.IsStruct + || LanguageVersion(langVersion).SupportsFeature LanguageFeature.SupportValueOptionsAsOptionalParameters + then + let! _, checkResults = + document.GetFSharpParseAndCheckResultsAsync(nameof FSharpConvertOptionalParameterStructRefactoring) + + match + tryGetSymbolUse checkResults sourceText parameter.MemberName, + tryGetSymbolUse checkResults sourceText parameter.Ident + with + | Some memberUse, Some parameterUse when isConvertibleMember memberUse.Symbol -> + let uses = + checkResults.GetUsesOfSymbolInFile(parameterUse.Symbol, cancellationToken = cancellationToken) + // Named arguments at call sites are uses of the parameter too; only the body is rewritten here. + |> Seq.filter (fun symbolUse -> + not symbolUse.IsFromDefinition + && Position.posGeq symbolUse.Range.Start parameter.MemberRange.Start + && Position.posGeq parameter.MemberRange.End symbolUse.Range.End) + |> Seq.map _.Range + + match OptionalParameterConversion.tryBodyRenames parseResults.ParseTree parameter.IsStruct uses with + | ValueSome renames -> + let title = + if parameter.IsStruct then + SR.UseOptionForOptionalParameter() + else + SR.UseValueOptionForOptionalParameter() + + let definitionChanges = + OptionalParameterConversion.definitionChanges sourceText parameter renames + + let changedSolution = + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken () + let! memberUses = SymbolHelpers.getSymbolUses memberUse document checkResults + + let usesByDocument = + memberUses + |> Seq.groupBy (fun (useDocument: Document, _) -> useDocument.Id) + |> Seq.toArray + + let mutable solution = document.Project.Solution + + for documentId, documentUses in usesByDocument do + let useDocument = solution.GetDocument documentId + let! text = useDocument.GetTextAsync cancellationToken + + let! useParseResults = + useDocument.GetFSharpParseResultsAsync( + nameof FSharpConvertOptionalParameterStructRefactoring + ) + + let changes = + [ + if documentId = document.Id then + yield! definitionChanges + + for _, useRange in documentUses do + yield! + OptionalParameterConversion.callSiteChanges + text + useParseResults.ParseTree + parameter.IsStruct + parameter.Ident.idText + useRange + ] + |> List.distinctBy _.Span + |> List.sortBy _.Span.Start + + solution <- solution.WithDocumentText(documentId, text.WithChanges changes) + + if not (usesByDocument |> Array.exists (fun (documentId, _) -> documentId = document.Id)) then + solution <- solution.WithDocumentText(document.Id, sourceText.WithChanges definitionChanges) + + return solution + } + + let action = + CodeAction.Create( + title, + Func>(fun cancellationToken -> + CancellableTask.start cancellationToken changedSolution), + title + ) + + context.RegisterRefactoring action + | ValueNone -> () + | _ -> () + } + |> 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..87260cb8df5 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -403,6 +403,11 @@ Zobrazit poznámky v Rychlých informacích Použít nameof + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. Použijte interpolaci řetězce v trojitých uvozovkách. @@ -428,6 +433,11 @@ Zobrazit poznámky v Rychlých informacích Pokud chcete k výrazu přistupovat přes ukazatel, použijte .Value. + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses Uzavřít výraz do závorek diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf index bce1941f0b1..ba5a472f1c0 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -403,6 +403,11 @@ Hinweise in QuickInfo anzeigen "nameof" verwenden + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. Verwenden Sie die Interpolation von dreifachen Zeichenfolgen in Anführungszeichen. @@ -428,6 +433,11 @@ Hinweise in QuickInfo anzeigen ".Value" zum Dereferenzieren eines Ausdrucks verwenden + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses Ausdruck in Klammern einschließen diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf index fa8cb62c422..6f4f75ae800 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -403,6 +403,11 @@ Mostrar comentarios en Información rápida Usar 'nameof' + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. Use la interpolación de cadenas entre comillas triples. @@ -428,6 +433,11 @@ Mostrar comentarios en Información rápida Usar ".Value" para desreferenciar la expresión + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses Encapsular la expresión entre paréntesis diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index e7ec71e839e..443a491d75e 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -403,6 +403,11 @@ Afficher les notes dans Info express Utiliser « nameof » + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. Utilisez l’interpolation de chaîne entre guillemets triples. @@ -428,6 +433,11 @@ Afficher les notes dans Info express Utilisez '.Value' pour déréférencer l'expression + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses Mettre l'expression entre parenthèses diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 327a7ca362f..f893a71be9f 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -403,6 +403,11 @@ Mostra i commenti in Informazioni rapide Usa 'nameof' + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. Usare l'interpolazione di stringhe con virgolette triple. @@ -428,6 +433,11 @@ Mostra i commenti in Informazioni rapide Usa '.Value' per dereferenziare l'espressione + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses Racchiudere l'espressione tra parentesi diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index d45234c011a..054c728f2f2 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -403,6 +403,11 @@ F# 構文規則に準拠した改行を追加して、署名を指定された 'nameof' を使用する + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. 三重引用符で囲まれた文字列補間を使用します。 @@ -428,6 +433,11 @@ F# 構文規則に準拠した改行を追加して、署名を指定された '.Value' を使用して式を逆参照する + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses 式をかっこで囲む diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index 3248e0641ea..1b3d4772c0f 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -403,6 +403,11 @@ F# 구문 규칙에 맞는 줄 바꿈을 추가하여 지정된 너비에 시그 'nameof' 사용 + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. 삼중 따옴표로 묶인 분자열 보간을 사용합니다. @@ -428,6 +433,11 @@ F# 구문 규칙에 맞는 줄 바꿈을 추가하여 지정된 너비에 시그 식을 역참조하려면 '.Value' 사용 + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses 식을 괄호로 래핑 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index abc39f15da5..2ef5c44b2e6 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -403,6 +403,11 @@ Pokaż uwagi w szybkich informacjach Użyj wyrażenia "nameof" + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. Użyj interpolacji ciągu z potrójnym cudzysłowem. @@ -428,6 +433,11 @@ Pokaż uwagi w szybkich informacjach Użyj elementu „.Value”, aby wyłuskać wyrażenie + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses Ujmij wyrażenie w nawiasy 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..cf0522cf37d 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,11 @@ Mostrar os comentários nas Informações Rápidas Usar 'nameof' + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. Usar interpolação de cadeia de caracteres entre aspas triplas. @@ -428,6 +433,11 @@ Mostrar os comentários nas Informações Rápidas Use '.Value' para desreferenciar a expressão + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses Coloque a expressão entre parênteses diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 47cda215312..8f239bc42c8 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -403,6 +403,11 @@ Show remarks in Quick Info Использовать "nameof" + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. Использовать интерполяции строк в тройных кавычках. @@ -428,6 +433,11 @@ Show remarks in Quick Info Использовать синтаксис ".значение" для разыменования выражения + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses Заключите выражение в круглые скобки. diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 58aa5d54c43..778335735f6 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -403,6 +403,11 @@ Açıklamaları Hızlı Bilgide göster “Nameof” kullanın + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. Üçlü tırnak içine alınmış dize ilişkilendirmesini kullanın. @@ -428,6 +433,11 @@ Açıklamaları Hızlı Bilgide göster İfadeye başvurmak için '.Value' kullanın + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses İfadeyi parantez içinde sarmalayı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..755993bcd3c 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,11 @@ Show remarks in Quick Info 使用 "nameof" + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. 使用三引号字符串内插。 @@ -428,6 +433,11 @@ Show remarks in Quick Info 对取消引用表达式使用 ".Value" + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses 将表达式用括号括起来 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..7ec6fe94b79 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,11 @@ Show remarks in Quick Info 使用 'nameof' + + Use 'option' for optional parameter + Use 'option' for optional parameter + + Use triple quoted string interpolation. 使用三引號字串插補。 @@ -428,6 +433,11 @@ Show remarks in Quick Info 使用 '.Value' 擷取運算式的值 + + Use 'voption' for optional parameter + Use 'voption' for optional parameter + + Wrap expression in parentheses 使用括弧包裝運算式 diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index ecce1205b8c..3ab9a058623 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/ConvertOptionalParameterStructTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertOptionalParameterStructTests.fs new file mode 100644 index 00000000000..f009148a511 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertOptionalParameterStructTests.fs @@ -0,0 +1,242 @@ +module FSharp.Editor.Tests.Refactors.ConvertOptionalParameterStructTests + +open System +open System.Threading + +open Microsoft.CodeAnalysis +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 FSharpConvertOptionalParameterStructRefactoring()) + + 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 actionsIn (context: TestContext) (code: string) (marker: string) = + tryGetRefactoringActions code (caretAt code marker) context (new FSharpConvertOptionalParameterStructRefactoring()) + +let private actionsAt (code: string) (marker: string) = + use context = TestContext.CreateWithCode code + actionsIn context code marker + +let private greeter = + """ +module M + +type Greeter() = + member _.Greet(name: string, ?greeting: string) = + let greeting = defaultArg greeting "Hello" + $"{greeting}, {name}" + +let a = Greeter().Greet("Ada") +let b = Greeter().Greet("Ada", greeting = "Hi") +let c = Greeter().Greet("Ada", ?greeting = Some "Hey") +let d (g: string option) = Greeter().Greet("Ada", ?greeting = g) +""" + +let private structGreeter = + """ +module M + +type Greeter() = + member _.Greet(name: string, [] ?greeting: string) = + let greeting = defaultValueArg greeting "Hello" + $"{greeting}, {name}" + +let a = Greeter().Greet("Ada") +let b = Greeter().Greet("Ada", greeting = "Hi") +let c = Greeter().Greet("Ada", ?greeting = ValueSome "Hey") +let d (g: string option) = Greeter().Greet("Ada", ?greeting = ValueOption.ofOption g) +""" + +[] +let ``Optional parameter and its optional arguments convert to value options`` () = + Assert.Equal(structGreeter, refactored greeter "?greeting") + +[] +let ``Struct optional parameter and its optional arguments convert back to options`` () = + Assert.Equal(greeter, refactored structGreeter "?greeting") + +[] +[ s + | None -> 1 +""", + """ +module M + +type Counter() = + member _.Next([] ?step: int) = + match step with + | ValueSome s -> s + | ValueNone -> 1 +""")>] +[ Option.defaultValue 0 else 1 +""", + """ +module M + +type Counter() = + static member Describe([] ?step: int) = + if ValueOption.isSome step && step.IsSome then step |> ValueOption.defaultValue 0 else 1 +""")>] +[] ?step: int) = defaultValueArg step 1 + +let next (step: int option) = Counter.Next(?step = ValueOption.ofOption (if true then step else None)) +""")>] +let ``Uses of the parameter in the member body follow the conversion`` (before: string, after: string) = + Assert.Equal(after, refactored before "?step") + Assert.Equal(before, refactored after "?step") + +[] +let ``Value option passed to a struct optional parameter is converted to an option`` () = + let before = + """ +module M + +type Counter() = + static member Next([] ?step: int) = defaultValueArg step 1 + +let next (step: int voption) = Counter.Next(?step = step) +""" + + let after = + """ +module M + +type Counter() = + static member Next(?step: int) = defaultArg step 1 + +let next (step: int voption) = Counter.Next(?step = ValueOption.toOption step) +""" + + Assert.Equal(after, refactored before "?step") + +[] +let ``Title names the target option kind`` () = + Assert.Equal("Use 'voption' for optional parameter", (actionsAt greeter "?greeting" |> Seq.exactlyOne).Title) + Assert.Equal("Use 'option' for optional parameter", (actionsAt structGreeter "?greeting" |> Seq.exactlyOne).Title) + +let private counter = + """ +module M + +type C() = + static member M(?x: int) = defaultArg x 0 +""" + +[] +[] +[] +[ int + default _.M(?x) = defaultArg x 0 +""", + "?x)")>] +[] +[] +let ``No action`` (code: string, marker: string) = Assert.Empty(actionsAt code marker) + +[] +let ``Value option is not offered before F# 10`` () = + use context = + new TestContext(RoslynTestHelpers.CreateSolution(counter, extraFSharpProjectOtherOptions = [| "--langversion:9.0" |])) + + Assert.Empty(actionsIn context counter "?x") + +[] +let ``No action when the file has a signature`` () = + let signature = + """ +module M + +type C = + new: unit -> C + static member M: ?x: int -> int +""" + + let document = RoslynTestHelpers.GetFsiAndFsDocuments signature counter |> Seq.last + let actions = ResizeArray() + + let context = + CodeRefactoringContext(document, TextSpan(caretAt counter "?x", 1), (fun action -> actions.Add action), CancellationToken.None) + + (new FSharpConvertOptionalParameterStructRefactoring()).ComputeRefactoringsAsync(context).GetAwaiter().GetResult() + + Assert.False(document.IsFSharpSignatureFile) + Assert.Empty(actions)