diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index ba03f663967..660430f4793 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 F# `?x: T` and the .NET-compatible `[] x: T`, moving the default between `defaultArg x c` in the body and the attribute; F# call sites keep working unchanged. From F# 10 the .NET form also converts back to `[] ?x: T` with `defaultValueArg x c`. ([PR #20547](https://github.com/dotnet/fsharp/pull/20547)) * 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..48a1317eb29 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..26722fc9e22 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -368,4 +368,13 @@ Use live (unsaved) buffers for analysis Returns: + + Use [<Optional; DefaultParameterValue>] for optional parameter + + + Use F# '?' optional parameter + + + Use F# '[<Struct>] ?' optional parameter + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Refactor/ConvertOptionalParameterDefaultValue.fs b/vsintegration/src/FSharp.Editor/Refactor/ConvertOptionalParameterDefaultValue.fs new file mode 100644 index 00000000000..0b8d85cf6db --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ConvertOptionalParameterDefaultValue.fs @@ -0,0 +1,477 @@ +// 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.Formatting +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 OptionalParameterDefaultValueConversion = + + [] + type Form = + /// `?x: T` + | FSharp of optionalValRange: range + /// `[] x: T`, with the attribute lists holding only those attributes. + | DotNet of lists: SynAttributeList list * defaultValue: SynExpr voption + + [] + type Parameter = + { + Ident: Ident + TypeName: SynType + Form: Form + MemberName: Ident + MemberBinding: SynBinding + } + + [] + type private Search = + | Searching + | Found of Parameter voption + + 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 (|SingleIdent|_|) (expr: SynExpr) = + match expr with + | SynExpr.Ident ident + | SynExpr.LongIdent(longDotId = SynLongIdent(id = [ ident ])) -> ValueSome ident + | _ -> ValueNone + + let private isAttribute (names: string list) (attribute: SynAttribute) = + attribute.Target.IsNone + && match List.tryLast attribute.TypeName.LongIdent with + | Some name -> names |> List.exists (fun candidate -> hasText candidate name) + | None -> false + + let private isOptional = isAttribute [ "Optional"; "OptionalAttribute" ] + + let private isDefaultParameterValue = + isAttribute [ "DefaultParameterValue"; "DefaultParameterValueAttribute" ] + + let private isStruct = isAttribute [ "Struct"; "StructAttribute" ] + + /// The annotated parameter the pattern declares, with the attributes around it and the range of `?x`, if any. + let rec private tryShape (attributes: SynAttributes) (pat: SynPat) = + match pat with + | SynPat.Paren(pat = inner) -> tryShape attributes inner + | SynPat.Attrib(pat = inner; attributes = more) -> tryShape [ yield! attributes; yield! more ] inner + | SynPat.Typed(pat = SynPat.OptionalVal(ident, m); targetType = typeName) -> + ValueSome(struct (ident, typeName, attributes, ValueSome m)) + | SynPat.Typed(pat = SynPat.Named(ident = SynIdent(ident, _); isThisVal = false); targetType = typeName) -> + ValueSome(struct (ident, typeName, attributes, ValueNone)) + | _ -> ValueNone + + let private tryForm (attributes: SynAttributes) (optionalValRange: range voption) = + match optionalValRange with + | ValueSome _ when attributes |> List.exists (fun list -> List.exists isStruct list.Attributes) -> ValueNone + | ValueSome m -> ValueSome(Form.FSharp m) + | ValueNone -> + let isOptionalAttribute attribute = + isOptional attribute || isDefaultParameterValue attribute + + let lists = + attributes + |> List.filter (fun list -> List.exists isOptionalAttribute list.Attributes) + + let listed = lists |> List.collect _.Attributes + + if not (List.exists isOptional listed && List.forall isOptionalAttribute listed) then + ValueNone + else + match List.tryFind isDefaultParameterValue listed with + | None -> ValueSome(Form.DotNet(lists, ValueNone)) + | Some attribute -> + match attribute.ArgExpr with + | SynExpr.Paren(expr = SynExpr.Const _ as constant) -> ValueSome(Form.DotNet(lists, ValueSome constant)) + | _ -> ValueNone + + 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)) + | None -> ValueNone + | SyntaxNode.SynPat _ :: rest -> tryMember rest + | _ -> ValueNone + + let tryParameter (caret: pos) (parseTree: ParsedInput) = + let search = + (Search.Searching, parseTree) + ||> ParsedInput.fold (fun search path node -> + match search, node with + | Search.Searching, SyntaxNode.SynPat pat when Position.posGeq caret pat.Range.Start && Position.posGeq pat.Range.End caret -> + match tryShape [] pat, tryMember path with + | ValueSome(struct (ident, typeName, attributes, optionalValRange)), ValueSome(struct (memberName, binding)) -> + tryForm attributes optionalValRange + |> ValueOption.map (fun form -> + { + Ident = ident + TypeName = typeName + Form = form + MemberName = memberName + MemberBinding = binding + }) + |> Search.Found + | _ -> Search.Searching + | _ -> search) + + match search with + | Search.Found parameter -> parameter + | Search.Searching -> ValueNone + + /// Whether the constant can be the `DefaultParameterValue` of a parameter of the named type. + let private isConstantOfType (typeText: string) (constant: SynConst) = + match typeText, constant with + | ("int" | "int32"), SynConst.Int32 _ + | "int64", SynConst.Int64 _ + | "int16", SynConst.Int16 _ + | "sbyte", SynConst.SByte _ + | "byte", SynConst.Byte _ + | "uint16", SynConst.UInt16 _ + | "uint32", SynConst.UInt32 _ + | "uint64", SynConst.UInt64 _ + | ("float" | "double"), SynConst.Double _ + | ("float32" | "single"), SynConst.Single _ + | "bool", SynConst.Bool _ + | "char", SynConst.Char _ + | "string", SynConst.String _ -> true + | _ -> false + + 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) + + /// `defaultArg x c` around a use of the parameter, with its constant and what contains it. + let private tryDefaultArgUse (parseTree: ParsedInput) (useRange: range) = + match tryUseNode parseTree useRange with + | Some(node, + SyntaxNode.SynExpr(SynExpr.App(isInfix = false; funcExpr = SingleIdent func; argExpr = arg) as inner) :: SyntaxNode.SynExpr(SynExpr.App( + isInfix = false; funcExpr = funcExpr; argExpr = (SynExpr.Const _ as defaultValue)) as application) :: rest) when + isSame arg node && hasText "defaultArg" func && isSame funcExpr inner + -> + ValueSome(struct (application, defaultValue, rest)) + | _ -> ValueNone + + /// The whole line of `let x = defaultArg x c` when that line does nothing but rebind the parameter. + let private tryShadowingLine (sourceText: SourceText) (name: string) (application: SynExpr) (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynBinding(SynBinding(headPat = SynPat.Named(ident = SynIdent(ident, _)); expr = rhs; trivia = trivia)) :: SyntaxNode.SynExpr(SynExpr.LetOrUse letOrUse) :: _ when + isSame rhs application + && hasText name ident + && not letOrUse.IsRecursive + && not letOrUse.IsBang + && letOrUse.Bindings.Length = 1 + -> + let keyword = trivia.LeadingKeyword.Range + let line = sourceText.Lines[Line.toZ keyword.StartLine] + let lineText = line.ToString() + + if + application.Range.EndLine = keyword.StartLine + && String.IsNullOrWhiteSpace(lineText.Substring(0, keyword.StartColumn)) + && String.IsNullOrWhiteSpace(lineText.Substring application.Range.EndColumn) + then + ValueSome(TextSpan.FromBounds(line.Start, line.EndIncludingLineBreak)) + else + ValueNone + | _ -> ValueNone + + /// `?x: T` with every use being `defaultArg x c` becomes `[] x: T`. + let tryToDotNetChanges + (sourceText: SourceText) + (parseTree: ParsedInput) + (parameter: Parameter) + (optionalValRange: range) + (uses: range list) + = + let found = + (ValueSome [], uses) + ||> List.fold (fun found useRange -> + match found, tryDefaultArgUse parseTree useRange with + | ValueSome found, ValueSome defaultArgUse -> ValueSome(defaultArgUse :: found) + | _ -> ValueNone) + + let textOf (expr: SynExpr) = + sourceText.ToString(spanOf sourceText expr.Range) + + let typeText = + sourceText.ToString(spanOf sourceText parameter.TypeName.Range).Trim() + + let attributeText = + match found with + | ValueSome [] -> ValueSome "[] " + | ValueSome(struct (_, (SynExpr.Const(constant, _) as defaultValue), _) :: others) when + isConstantOfType typeText constant + && others + |> List.forall (fun struct (_, other, _) -> String.Equals(textOf other, textOf defaultValue, StringComparison.Ordinal)) + -> + ValueSome $"[] " + | _ -> ValueNone + + match found, attributeText with + | ValueSome found, ValueSome attributeText -> + [ + TextChange(TextSpan((spanOf sourceText optionalValRange).Start, 1), attributeText) + + for struct (application, _, path) in found do + match tryShadowingLine sourceText parameter.Ident.idText application path with + | ValueSome line -> TextChange(line, "") + | ValueNone -> TextChange(spanOf sourceText application.Range, parameter.Ident.idText) + ] + |> List.sortBy _.Span.Start + |> ValueSome + | _ -> ValueNone + + let private interopServices = [ "System"; "Runtime"; "InteropServices" ] + + let private hasInteropServicesOpen (parseTree: ParsedInput) = + (false, parseTree) + ||> ParsedInput.fold (fun found _ node -> + found + || match node with + | SyntaxNode.SynModule(SynModuleDecl.Open(target = SynOpenDeclTarget.ModuleOrNamespace(longId = SynLongIdent(id = ids)))) -> + (ids |> List.map _.idText) = interopServices + | _ -> false) + + let withInteropServicesOpen (parseTree: ParsedInput) (memberName: Ident) (text: SourceText) = + if hasInteropServicesOpen parseTree then + text + else + let insertionContext = + FSharp.Compiler.EditorServices.ParsedInput.FindNearestPointToInsertOpenDeclaration + memberName.idRange.StartLine + parseTree + (List.toArray interopServices) + FSharp.Compiler.EditorServices.OpenStatementInsertionPoint.TopLevel + + OpenDeclarationHelper.insertOpenDeclaration text insertionContext (String.Join(".", interopServices)) + |> fst + + let private leadingSpaces (line: TextLine) = + let text = line.ToString() + text.Length - text.TrimStart(' ').Length + + let private lineBreakOf (sourceText: SourceText) (line: TextLine) = + match line.EndIncludingLineBreak - line.End with + | 0 -> Environment.NewLine + | length -> sourceText.ToString(TextSpan(line.End, length)) + + let private listRemoval (sourceText: SourceText) (list: SynAttributeList) = + let listSpan = spanOf sourceText list.Range + let line = sourceText.Lines.GetLineFromPosition listSpan.End + let after = sourceText.ToString(TextSpan.FromBounds(listSpan.End, line.End)) + TextSpan(listSpan.Start, listSpan.Length + after.Length - after.TrimStart().Length) + + /// `[] x: T` becomes `?x: T` with `let x = defaultArg x c` starting the body, + /// or `[] ?x: T` with `let x = defaultValueArg x c`. + let tryToFSharpChanges + (sourceText: SourceText) + (indentSize: int) + (parameter: Parameter) + (lists: SynAttributeList list) + (defaultValue: SynExpr voption) + (asStruct: bool) + = + let name = parameter.Ident.idText + + let defaultText = + match defaultValue with + | ValueSome value -> sourceText.ToString(spanOf sourceText value.Range) + | ValueNone -> "Unchecked.defaultof<_>" + + let struct (prefix, defaultFunction) = + if asStruct then + struct ("[] ?", "defaultValueArg") + else + struct ("?", "defaultArg") + + let declaration = $"let {name} = {defaultFunction} {name} {defaultText}" + + let (SynBinding(expr = body; returnInfo = returnInfo; trivia = trivia)) = + parameter.MemberBinding + + let body = + match returnInfo, body with + | Some _, SynExpr.Typed(expr = inner) -> inner + | _ -> body + + let memberLine = sourceText.Lines[Line.toZ trivia.LeadingKeyword.Range.StartLine] + let lineBreak = lineBreakOf sourceText memberLine + + let bodyChange = + match trivia.EqualsRange with + | Some equals when body.Range.StartLine > equals.EndLine -> + let line = sourceText.Lines[Line.toZ body.Range.StartLine] + ValueSome(TextChange(TextSpan(line.Start, 0), $"{String(' ', body.Range.StartColumn)}{declaration}{lineBreak}")) + | Some equals when body.Range.StartLine = body.Range.EndLine -> + let indent = String(' ', leadingSpaces memberLine + indentSize) + + ValueSome( + TextChange( + TextSpan.FromBounds((spanOf sourceText equals).End, (spanOf sourceText body.Range).Start), + $"{lineBreak}{indent}{declaration}{lineBreak}{indent}" + ) + ) + | _ -> ValueNone + + bodyChange + |> ValueOption.map (fun bodyChange -> + [ + for list in lists do + TextChange(listRemoval sourceText list, "") + + TextChange(TextSpan((spanOf sourceText parameter.Ident.idRange).Start, 0), prefix) + bodyChange + ] + |> List.sortBy _.Span.Start) + +[] +type internal FSharpConvertOptionalParameterDefaultValueRefactoring [] () = + 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 FSharpConvertOptionalParameterDefaultValueRefactoring) + + let caret = + let linePosition = sourceText.Lines.GetLinePosition context.Span.Start + Position.mkPos (Line.fromZ linePosition.Line) linePosition.Character + + match OptionalParameterDefaultValueConversion.tryParameter caret parseResults.ParseTree with + | ValueNone -> () + | ValueSome parameter -> + let! _, checkResults = + document.GetFSharpParseAndCheckResultsAsync(nameof FSharpConvertOptionalParameterDefaultValueRefactoring) + + match tryGetSymbolUse checkResults sourceText parameter.MemberName with + | Some memberUse when isConvertibleMember memberUse.Symbol -> + match parameter.Form with + | OptionalParameterDefaultValueConversion.Form.FSharp optionalValRange -> + match tryGetSymbolUse checkResults sourceText parameter.Ident with + | Some parameterUse -> + let memberRange = parameter.MemberBinding.RangeOfBindingWithRhs + + let uses = + checkResults.GetUsesOfSymbolInFile(parameterUse.Symbol, cancellationToken = cancellationToken) + // Named arguments at call sites are uses of the parameter too; only the body matters here. + |> Seq.filter (fun symbolUse -> + not symbolUse.IsFromDefinition + && Position.posGeq symbolUse.Range.Start memberRange.Start + && Position.posGeq memberRange.End symbolUse.Range.End) + |> Seq.map _.Range + |> List.ofSeq + + match + OptionalParameterDefaultValueConversion.tryToDotNetChanges + sourceText + parseResults.ParseTree + parameter + optionalValRange + uses + with + | ValueSome changes -> + let title = SR.UseDotNetOptionalParameter() + + let changedDocument = + cancellableTask { + let changed = + sourceText.WithChanges changes + |> OptionalParameterDefaultValueConversion.withInteropServicesOpen + parseResults.ParseTree + parameter.MemberName + + return document.WithText changed + } + + context.RegisterRefactoring(CodeAction.Create(title, changedDocument, title)) + | ValueNone -> () + | None -> () + | OptionalParameterDefaultValueConversion.Form.DotNet(lists, defaultValue) -> + let! options = document.GetOptionsAsync cancellationToken + + let! _, langVersion = + document.GetFsharpParsingOptionsAsync(nameof FSharpConvertOptionalParameterDefaultValueRefactoring) + + let indentSize = + options.GetOption(FormattingOptions.IndentationSize, FSharpConstants.FSharpLanguageName) + + let register (asStruct: bool) (title: string) = + match + OptionalParameterDefaultValueConversion.tryToFSharpChanges + sourceText + indentSize + parameter + lists + defaultValue + asStruct + with + | ValueSome changes -> + let changedDocument = + cancellableTask { return document.WithText(sourceText.WithChanges changes) } + + context.RegisterRefactoring(CodeAction.Create(title, changedDocument, title)) + | ValueNone -> () + + register false (SR.UseFSharpOptionalParameter()) + + if LanguageVersion(langVersion).SupportsFeature LanguageFeature.SupportValueOptionsAsOptionalParameters then + register true (SR.UseFSharpStructOptionalParameter()) + | _ -> () + } + |> 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..9de62478b06 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -388,11 +388,26 @@ Zobrazit poznámky v Rychlých informacích Formátování + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax Použít syntaxi lambda jazyka F# + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value Pokud chcete změnit hodnotu, 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..9d21fbbb3ef 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -388,11 +388,26 @@ Hinweise in QuickInfo anzeigen Formatierung + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax F#-Lambdasyntax verwenden + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value "<-" zum Ändern des Werts 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..c12b16c7bbb 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -388,11 +388,26 @@ Mostrar comentarios en Información rápida Formato + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax Usar la sintaxis lambda de F# + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value Usar "<-" para mutar el valor diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf index e7ec71e839e..099e7f54fa8 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -388,11 +388,26 @@ Afficher les notes dans Info express Mise en forme + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax Utiliser la syntaxe lambda F# + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value Utiliser '<-' pour muter la valeur diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf index 327a7ca362f..9c24afb31e6 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -388,11 +388,26 @@ Mostra i commenti in Informazioni rapide Formattazione + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax Usa la sintassi lambda di F# + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value Usare '<-' per modificare il valore diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf index d45234c011a..099521eaee0 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -388,11 +388,26 @@ F# 構文規則に準拠した改行を追加して、署名を指定された 書式設定 + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax F# のラムダ構文を使用する + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value '<-' を使用して値を変換する diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf index 3248e0641ea..2d96216f379 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -388,11 +388,26 @@ F# 구문 규칙에 맞는 줄 바꿈을 추가하여 지정된 너비에 시그 서식 + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax F# 람다 구문 사용 + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value '<-'를 사용하여 값 변경 diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf index abc39f15da5..8177c4ed95f 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -388,11 +388,26 @@ Pokaż uwagi w szybkich informacjach Formatowanie + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax Użyj składni wyrażenia lambda języka F# + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value Użyj znaku „<-” w celu zmodyfikowania wartoś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..597540ff90d 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -388,11 +388,26 @@ Mostrar os comentários nas Informações Rápidas Formatação + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax Usar a sintaxe lambda F# + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value Usar '<-' para modificar o valor diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf index 47cda215312..5cba6e3fb1d 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -388,11 +388,26 @@ Show remarks in Quick Info Форматирование + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax Использовать синтаксис лямбда F# + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value Используйте "<-", чтобы изменить значение diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf index 58aa5d54c43..7e21aa1886a 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -388,11 +388,26 @@ Açıklamaları Hızlı Bilgide göster Biçimlendirme + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax F# lambda söz dizimini kullan + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value Değeri değiştirmek 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..8fb03ac17dd 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -388,11 +388,26 @@ Show remarks in Quick Info 正在格式化 + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax 使用 F# lambda 语法 + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value 使用 "<-" 来更改值 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..997ce973180 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -388,11 +388,26 @@ Show remarks in Quick Info 格式化 + + Use [<Optional; DefaultParameterValue>] for optional parameter + Use [<Optional; DefaultParameterValue>] for optional parameter + + Use F# lambda syntax 使用 F# lambda 語法 + + Use F# '?' optional parameter + Use F# '?' optional parameter + + + + Use F# '[<Struct>] ?' optional parameter + Use F# '[<Struct>] ?' optional parameter + + Use '<-' to mutate value 使用 '<-' 來變動值 diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index ecce1205b8c..24b23edc9f7 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/ConvertOptionalParameterDefaultValueTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertOptionalParameterDefaultValueTests.fs new file mode 100644 index 00000000000..9219dc541fe --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertOptionalParameterDefaultValueTests.fs @@ -0,0 +1,398 @@ +module FSharp.Editor.Tests.Refactors.ConvertOptionalParameterDefaultValueTests + +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 fsharpTitle = "Use F# '?' optional parameter" +let private structTitle = "Use F# '[] ?' optional parameter" + +let private refactoredBy (pick: CodeAction seq -> CodeAction) (code: string) (marker: string) = + use context = TestContext.CreateWithCode code + + let action = + tryGetRefactoringActions code (caretAt code marker) context (new FSharpConvertOptionalParameterDefaultValueRefactoring()) + |> pick + + for operation in action.GetOperationsAsync CancellationToken.None |> GetTaskResult do + let applyChanges = operation :?> ApplyChangesOperation + applyChanges.Apply(context.Solution.Workspace, CancellationToken.None) + context.Solution <- applyChanges.ChangedSolution + + let document = RoslynTestHelpers.GetLastDocument context.Solution + + 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 refactored (code: string) (marker: string) = refactoredBy Seq.head code marker + +let private refactoredWith (title: string) (code: string) (marker: string) = + refactoredBy (Seq.find (fun action -> String.Equals(action.Title, title, StringComparison.Ordinal))) code marker + +let private actionsAt (code: string) (marker: string) = + use context = TestContext.CreateWithCode code + tryGetRefactoringActions code (caretAt code marker) context (new FSharpConvertOptionalParameterDefaultValueRefactoring()) + +[] +let ``Optional parameter with a default value becomes a .NET optional parameter and the open is added`` () = + let before = + """ +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 after = + """ +module M + +open System.Runtime.InteropServices + +type Greeter() = + member _.Greet(name: string, [] greeting: string) = + $"{greeting}, {name}" + +let a = Greeter().Greet("Ada") +let b = Greeter().Greet("Ada", greeting = "Hi") +""" + + Assert.Equal(after, refactored before "?greeting") + +[] +[] step: int) = + step + 1 + +let n = Counter.Next() +""")>] +[] step: float) = + float value + step +""")>] +let ``Shadowing default converts both ways`` (fsharpForm: string, dotNetForm: string) = + Assert.Equal(dotNetForm, refactored fsharpForm "?step") + Assert.Equal(fsharpForm, refactored dotNetForm "step:") + +[] +let ``Inline defaults are replaced by the parameter`` () = + let before = + """ +module M + +open System.Runtime.InteropServices + +type Counter() = + static member Next(value: int, ?step: int) = value + defaultArg step 1 +""" + + let after = + """ +module M + +open System.Runtime.InteropServices + +type Counter() = + static member Next(value: int, [] step: int) = value + step +""" + + Assert.Equal(after, refactored before "?step") + +[] +let ``Body on the member line moves below it when converting back`` () = + let before = + """ +module M + +open System.Runtime.InteropServices + +type Counter() = + static member Next(value: int, [] step: int) = value + step +""" + + let after = + """ +module M + +open System.Runtime.InteropServices + +type Counter() = + static member Next(value: int, ?step: int) = + let step = defaultArg step 1 + value + step +""" + + Assert.Equal(after, refactored before "step:") + +[] +[] flag: bool) = 1 +""")>] +[] step: int) = + step + 1 +""", + "step:", + """ +module M + +open System.Runtime.InteropServices + +type C() = + static member M(?step: int) = + let step = defaultArg step Unchecked.defaultof<_> + step + 1 +""")>] +let ``Optional without a default value`` (before: string, marker: string, after: string) = + Assert.Equal(after, refactored before marker) + +let private fsharpForm = + """ +module M + +type C() = + static member M(?x: int) = defaultArg x 0 +""" + +let private dotNetForm = + """ +module M + +open System.Runtime.InteropServices + +type C() = + static member M([] x: int) = x +""" + +let private titlesOf (actions: CodeAction seq) = + actions |> Seq.map _.Title |> List.ofSeq + +[] +let ``Title names the target form`` () = + Assert.Equal("Use [] for optional parameter", (actionsAt fsharpForm "?x" |> Seq.exactlyOne).Title) + Assert.Equal([ fsharpTitle; structTitle ], titlesOf (actionsAt dotNetForm "x:")) + +[] +[] step: int) = + value + step +""", + """ +module M + +open System.Runtime.InteropServices + +type Counter() = + static member Next(value: int, [] ?step: int) = + let step = defaultValueArg step 1 + value + step +""")>] +[] step: int) = + step + 1 +""", + """ +module M + +open System.Runtime.InteropServices + +type C() = + static member M([] ?step: int) = + let step = defaultValueArg step Unchecked.defaultof<_> + step + 1 +""")>] +let ``Converting back to a struct optional parameter uses defaultValueArg`` (dotNetForm: string, structForm: string) = + Assert.Equal(structForm, refactoredWith structTitle dotNetForm "step:") + +[] +let ``Struct optional parameter is not offered before F# 10`` () = + use context = + new TestContext(RoslynTestHelpers.CreateSolution(dotNetForm, extraFSharpProjectOtherOptions = [| "--langversion:9.0" |])) + + let actions = + tryGetRefactoringActions dotNetForm (caretAt dotNetForm "x:") context (new FSharpConvertOptionalParameterDefaultValueRefactoring()) + + Assert.Equal([ fsharpTitle ], titlesOf actions) + +[] +[] +[] +[] +[] +[] +[] ?x: int) = defaultValueArg x 0 +""", + "?x")>] +[] x: int) = x +""", + "x:")>] +[ int + default _.M(?x: int) = defaultArg x 0 +""", + "?x: int)")>] +[] +let ``No action`` (code: string, marker: string) = Assert.Empty(actionsAt code marker) + +[] +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 fsharpForm |> Seq.last + + let actions = ResizeArray() + + let context = + CodeRefactoringContext(document, TextSpan(caretAt fsharpForm "?x", 1), (fun action -> actions.Add action), CancellationToken.None) + + (new FSharpConvertOptionalParameterDefaultValueRefactoring()).ComputeRefactoringsAsync(context).GetAwaiter().GetResult() + + Assert.False(document.IsFSharpSignatureFile) + Assert.Empty(actions)