diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 2357fbcdff4..aab3bf38155 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -2,6 +2,7 @@ * Suggest active-pattern cases from unopened modules and offer valid Add Open and qualification edits. ([PR #20719](https://github.com/dotnet/fsharp/pull/20719)) * Add maintainer-requested F# Apex Visual Studio integration tests on DartLab-1ES CloudTest, using PR pipeline definitions pinned to an explicitly requested commit SHA. ([PR #20625](https://github.com/dotnet/fsharp/pull/20625)) +* 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)) * Added a **Tools > Options > F# Tools > Compiler** option, **"Use the .NET SDK F# compiler for builds"**, on by default. Visual Studio uses the .NET SDK F# compiler when the project supplies SDK paths, matching `dotnet build`. Otherwise, it uses the bundled .NET Framework compiler. Turn the option off to use the bundled compiler for all projects. Changes are saved on Apply or OK; Cancel discards unsaved changes. Command-line builds are unaffected. ([Issue #20484](https://github.com/dotnet/fsharp/issues/20484), [PR #20485](https://github.com/dotnet/fsharp/pull/20485)) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index f5d6a8e0b04..e0a96d15e46 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -109,6 +109,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index 8d1a5854269..37a92777b5c 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -373,4 +373,13 @@ Use live (unsaved) buffers for analysis {2} Tooltip of an F# declaration in the Copilot Chat "#" picker, as Copilot shows a C# one. {0} is the kind of declaration (Class, Method, Field...), {1} the file name, {2} the declaration's name. + + 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..3504a465042 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Refactor/ConvertOptionalParameterDefaultValue.fs @@ -0,0 +1,844 @@ +// 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 = + | FSharpOptional of optionalValRange: range + | DotNetOptional 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 tryGetSymbolUse (checkResults: FSharpCheckFileResults) (sourceText: SourceText) (longIdent: Ident list) = + longIdent + |> List.tryLast + |> Option.bind (fun ident -> + let line = sourceText.Lines[Line.toZ ident.idRange.EndLine].ToString() + + checkResults.GetSymbolUseAtLocation(ident.idRange.EndLine, ident.idRange.EndColumn, line, longIdent |> List.map _.idText)) + + let private isInFSharpCore (entityFullName: string) (symbol: FSharpSymbol) = + let declaringEntityName = + match symbol with + | :? FSharpUnionCase as case -> case.DeclaringEntity.TryFullName + | :? FSharpMemberOrFunctionOrValue as value -> value.DeclaringEntity |> Option.bind _.TryFullName + | _ -> None + + match declaringEntityName with + | Some name -> + String.Equals(name, entityFullName, StringComparison.Ordinal) + && String.Equals(symbol.Assembly.SimpleName, "FSharp.Core", StringComparison.Ordinal) + | None -> false + + let private isFSharpCoreSymbol + (checkResults: FSharpCheckFileResults) + (sourceText: SourceText) + (entityFullName: string) + (longIdent: Ident list) + = + tryGetSymbolUse checkResults sourceText longIdent + |> Option.exists (fun symbolUse -> isInFSharpCore entityFullName symbolUse.Symbol) + + let private operators = "Microsoft.FSharp.Core.Operators" + + let private fsharpCoreNameAt + (checkResults: FSharpCheckFileResults) + (compiledContainer: string) + (container: string) + (name: string) + (position: pos) + = + let isNamed (symbolName: string) = + String.Equals(symbolName, name, StringComparison.Ordinal) + + let isInScope (symbol: FSharpSymbol) = + checkResults.IsRelativeNameResolvableFromSymbol(position, [], symbol) + + let entity = + checkResults.ProjectContext.GetReferencedAssemblies() + |> List.tryPick (fun assembly -> + if String.Equals(assembly.SimpleName, "FSharp.Core", StringComparison.Ordinal) then + assembly.Contents.FindEntityByPath [ "Microsoft"; "FSharp"; "Core"; compiledContainer ] + else + None) + + let resolves = + match entity with + | Some entity when entity.IsFSharpUnion -> + entity.UnionCases + |> Seq.exists (fun case -> isNamed case.Name && isInScope case) + | Some entity -> + entity.MembersFunctionsAndValues + |> Seq.exists (fun value -> isNamed value.DisplayName && isInScope value) + | None -> false + + if resolves then + name + else + $"Microsoft.FSharp.Core.{container}.{name}" + + [] + let private (|SingleIdent|_|) (expr: SynExpr) = + match expr with + | SynExpr.Ident ident + | SynExpr.LongIdent(longDotId = SynLongIdent(id = [ ident ])) -> ValueSome ident + | _ -> ValueNone + + [] + let private (|IdentsAndLast|_|) (expr: SynExpr) = + match expr with + | SynExpr.Ident ident -> ValueSome(struct ([ ident ], ident)) + | SynExpr.LongIdent(longDotId = SynLongIdent(id = ids)) -> + match List.tryLast ids with + | Some last -> ValueSome(struct (ids, last)) + | None -> ValueNone + | _ -> 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" ] + + let rec private tryAnnotatedParameter (attributes: SynAttributes) (pat: SynPat) = + match pat with + | SynPat.Paren(pat = inner) -> tryAnnotatedParameter attributes inner + | SynPat.Attrib(pat = inner; attributes = more) -> tryAnnotatedParameter [ 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 tryDotNetOptional (attributes: SynAttributes) = + let listed = attributes |> List.collect _.Attributes + + let hasOnlyOptionalAttributes = + List.exists isOptional listed + && listed + |> List.forall (fun attribute -> isOptional attribute || isDefaultParameterValue attribute) + + match List.tryFind isDefaultParameterValue listed with + | _ when not hasOnlyOptionalAttributes -> ValueNone + | None -> ValueSome(Form.DotNetOptional(attributes, ValueNone)) + | Some attribute -> + match attribute.ArgExpr with + | SynExpr.Paren(expr = SynExpr.Const _ as constant) -> ValueSome(Form.DotNetOptional(attributes, ValueSome constant)) + | _ -> 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.FSharpOptional m) + | ValueNone -> tryDotNetOptional attributes + + let rec private tryEnclosingMember (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 -> tryEnclosingMember rest + | _ -> ValueNone + + let private searchResultAt (path: SyntaxVisitorPath) (pat: SynPat) = + match tryAnnotatedParameter [] pat, tryEnclosingMember 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 + + 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 Range.rangeContainsPos pat.Range caret -> searchResultAt path pat + | _ -> search) + + match search with + | Search.Found parameter -> parameter + | Search.Searching -> ValueNone + + let isConstantOfType (typeFullName: string option) (constant: SynConst) = + match typeFullName, constant with + | Some "System.Int32", SynConst.Int32 _ + | Some "System.Int64", SynConst.Int64 _ + | Some "System.Int16", SynConst.Int16 _ + | Some "System.SByte", SynConst.SByte _ + | Some "System.Byte", SynConst.Byte _ + | Some "System.UInt16", SynConst.UInt16 _ + | Some "System.UInt32", SynConst.UInt32 _ + | Some "System.UInt64", SynConst.UInt64 _ + | Some "System.Double", SynConst.Double _ + | Some "System.Single", SynConst.Single _ + | Some "System.Boolean", SynConst.Bool _ + | Some "System.Char", SynConst.Char _ + | Some "System.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) + + let private tryDefaultArgUse + (checkResults: FSharpCheckFileResults) + (sourceText: SourceText) + (parseTree: ParsedInput) + (useRange: range) + = + match tryUseNode parseTree useRange with + | Some(node, + SyntaxNode.SynExpr(SynExpr.App(isInfix = false; funcExpr = IdentsAndLast(func, name); 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" name + && isSame funcExpr inner + && isFSharpCoreSymbol checkResults sourceText operators func + -> + ValueSome(struct (application, defaultValue, rest)) + | _ -> ValueNone + + let private tryShadowingLineSpan (sourceText: SourceText) (name: string) (application: SynExpr) (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynBinding(SynBinding( + isMutable = false; 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 + + let rec private hasOptionalValBefore (position: pos) (pat: SynPat) = + match pat with + | SynPat.OptionalVal(range = m) -> Position.posLt m.Start position + | SynPat.Paren(pat = inner) + | SynPat.Typed(pat = inner) + | SynPat.Attrib(pat = inner) -> hasOptionalValBefore position inner + | SynPat.Tuple(elementPats = pats) + | SynPat.LongIdent(argPats = SynArgPats.Pats pats) -> pats |> List.exists (hasOptionalValBefore position) + | _ -> false + + let private interopServices = [ "System"; "Runtime"; "InteropServices" ] + + let rec private tryContainerRange (path: SyntaxVisitorPath) = + match path with + | SyntaxNode.SynModuleOrNamespace moduleOrNamespace :: _ -> ValueSome moduleOrNamespace.Range + | SyntaxNode.SynModule(SynModuleDecl.NestedModule(range = m)) :: _ -> ValueSome m + | _ :: rest -> tryContainerRange rest + | [] -> ValueNone + + let private hasInteropServicesOpenAt (position: pos) (parseTree: ParsedInput) = + (false, parseTree) + ||> ParsedInput.fold (fun found path node -> + found + || match node with + | SyntaxNode.SynModule(SynModuleDecl.Open( + target = SynOpenDeclTarget.ModuleOrNamespace(longId = SynLongIdent(id = ids)); range = m)) when + Position.posLt m.End position + && ids.Length = interopServices.Length + && List.forall2 hasText interopServices ids + -> + match tryContainerRange path with + | ValueSome container -> Range.rangeContainsPos container position + | ValueNone -> false + | _ -> false) + + let tryToDotNetChanges + (checkResults: FSharpCheckFileResults) + (sourceText: SourceText) + (parseTree: ParsedInput) + (parameter: Parameter) + (parameterTypeName: string option) + (optionalValRange: range) + (uses: range list) + = + let (SynBinding(headPat = headPat)) = parameter.MemberBinding + + let found = + (ValueSome [], uses) + ||> List.fold (fun found useRange -> + match found, tryDefaultArgUse checkResults sourceText parseTree useRange with + | ValueSome found, ValueSome defaultArgUse -> ValueSome(defaultArgUse :: found) + | _ -> ValueNone) + + let textOf (expr: SynExpr) = + sourceText.ToString(spanOf sourceText expr.Range) + + let attributes = + match found with + | ValueSome [] -> ValueSome [ "Optional" ] + | ValueSome(struct (_, (SynExpr.Const(constant, _) as defaultValue), _) :: others) when + isConstantOfType parameterTypeName constant + && others + |> List.forall (fun struct (_, other, _) -> String.Equals(textOf other, textOf defaultValue, StringComparison.Ordinal)) + -> + ValueSome [ "Optional"; $"DefaultParameterValue({textOf defaultValue})" ] + | _ -> ValueNone + + match found, attributes with + | ValueSome found, ValueSome attributes when not (hasOptionalValBefore optionalValRange.Start headPat) -> + let qualifier = + if hasInteropServicesOpenAt parameter.MemberName.idRange.Start parseTree then + "" + else + String.Join(".", interopServices) + "." + + let attributeText = + attributes + |> Seq.map (fun attribute -> qualifier + attribute) + |> String.concat "; " + + let name = sourceText.ToString(spanOf sourceText parameter.Ident.idRange) + + [ + TextChange(TextSpan((spanOf sourceText optionalValRange).Start, 1), $"[<{attributeText}>] ") + + for struct (application, _, path) in found do + match tryShadowingLineSpan sourceText parameter.Ident.idText application path with + | ValueSome line -> TextChange(line, "") + | ValueNone -> TextChange(spanOf sourceText application.Range, name) + ] + |> List.sortBy _.Span.Start + |> ValueSome + | _ -> ValueNone + + 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) + + let rec private startOfWhiteSpaceBefore (sourceText: SourceText) (position: int) = + if position > 0 && Char.IsWhiteSpace sourceText[position - 1] then + startOfWhiteSpaceBefore sourceText (position - 1) + else + position + + let rec private isOptionalVal (pat: SynPat) = + match pat with + | SynPat.OptionalVal _ -> true + | SynPat.Paren(pat = inner) + | SynPat.Typed(pat = inner) + | SynPat.Attrib(pat = inner) -> isOptionalVal inner + | _ -> false + + let rec private hasRequiredAfter (position: pos) (pat: SynPat) = + match pat with + | SynPat.Paren(pat = inner) -> hasRequiredAfter position inner + | SynPat.Tuple(elementPats = pats) -> + pats + |> List.exists (fun pat -> Position.posGt pat.Range.Start position && not (isOptionalVal pat)) + | SynPat.LongIdent(argPats = SynArgPats.Pats pats) -> pats |> List.exists (hasRequiredAfter position) + | _ -> false + + let tryToFSharpChanges + (checkResults: FSharpCheckFileResults) + (sourceText: SourceText) + (indentSize: int) + (parameter: Parameter) + (lists: SynAttributeList list) + (defaultValue: SynExpr voption) + (asStruct: bool) + = + let name = sourceText.ToString(spanOf sourceText parameter.Ident.idRange) + + 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 (SynBinding(headPat = headPat; expr = body; returnInfo = returnInfo; trivia = trivia)) = + parameter.MemberBinding + + let body = + match returnInfo, body with + | Some _, SynExpr.Typed(expr = inner) -> inner + | _ -> body + + let defaultFunction = + fsharpCoreNameAt checkResults "Operators" "Operators" defaultFunction body.Range.Start + + let declaration = $"let {name} = {defaultFunction} {name} {defaultText}" + 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 _ when body.Range.StartLine = body.Range.EndLine -> + let indent = String(' ', leadingSpaces memberLine + indentSize) + let bodyStart = (spanOf sourceText body.Range).Start + + let whiteSpaceBeforeBody = + TextSpan.FromBounds(startOfWhiteSpaceBefore sourceText bodyStart, bodyStart) + + ValueSome(TextChange(whiteSpaceBeforeBody, $"{lineBreak}{indent}{declaration}{lineBreak}{indent}")) + | _ -> ValueNone + + match bodyChange with + | ValueSome bodyChange when not (hasRequiredAfter parameter.Ident.idRange.End headPat) -> + [ + for list in lists do + TextChange(listRemoval sourceText list, "") + + TextChange(TextSpan((spanOf sourceText parameter.Ident.idRange).Start, 0), prefix) + bodyChange + ] + |> List.sortBy _.Span.Start + |> ValueSome + | _ -> ValueNone + + let rec private functionOf (expr: SynExpr) = + match expr with + | SynExpr.App(isInfix = false; funcExpr = funcExpr) -> functionOf funcExpr + | _ -> expr + + let private isAtomic (expr: SynExpr) = + match expr with + | SynExpr.Ident _ + | SynExpr.LongIdent _ + | SynExpr.Paren _ + | SynExpr.Const _ -> true + | _ -> false + + 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 = SingleIdent equals + argExpr = SynExpr.LongIdent(isOptional = true; longDotId = SynLongIdent(id = [ argumentName ]))) + argExpr = value) when hasText "op_Equality" equals && hasText name argumentName -> Some value + | _ -> None) + + let private valueOptionChanges (checkResults: FSharpCheckFileResults) (sourceText: SourceText) (value: SynExpr) = + let valueSpan = spanOf sourceText value.Range + + match functionOf value with + | SingleIdent head when + (hasText "Some" head || hasText "None" head) + && isFSharpCoreSymbol checkResults sourceText "Microsoft.FSharp.Core.FSharpOption`1" [ head ] + -> + let case = if hasText "Some" head then "ValueSome" else "ValueNone" + + [ + TextChange( + spanOf sourceText head.idRange, + fsharpCoreNameAt checkResults "FSharpValueOption`1" "ValueOption" case head.idRange.Start + ) + ] + | _ when isAtomic value -> [ TextChange(TextSpan(valueSpan.Start, 0), "ValueOption.ofOption ") ] + | _ -> + [ + TextChange(TextSpan(valueSpan.Start, 0), "ValueOption.ofOption (") + TextChange(TextSpan(valueSpan.End, 0), ")") + ] + + let callSiteChanges + (checkResults: FSharpCheckFileResults) + (sourceText: SourceText) + (parseTree: ParsedInput) + (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 (valueOptionChanges checkResults sourceText) + | None -> [] + +[] +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 isConvertibleMember (symbol: FSharpSymbol) = + match symbol with + | :? FSharpMemberOrFunctionOrValue as mfv -> + not ( + mfv.IsOverrideOrExplicitInterfaceImplementation + || mfv.IsDispatchSlot + || mfv.IsConstructor + || mfv.IsExtensionMember + ) + | _ -> false + + static let hasFullName (fullName: string) (entity: FSharpEntity) = + match entity.TryFullName with + | Some name -> String.Equals(name, fullName, StringComparison.Ordinal) + | None -> false + + static let isAttribute (fullName: string) (attribute: FSharpAttribute) = + hasFullName fullName attribute.AttributeType + + static let isInteropOptional = + isAttribute "System.Runtime.InteropServices.OptionalAttribute" + + static let isInteropDefaultParameterValue = + isAttribute "System.Runtime.InteropServices.DefaultParameterValueAttribute" + + static let isByRefLike (definition: FSharpEntity) = + definition.IsByRef + || definition.Attributes + |> Seq.exists (isAttribute "System.Runtime.CompilerServices.IsByRefLikeAttribute") + + static let tryTypeDefinition (fsharpType: FSharpType) = + let stripped = fsharpType.StripAbbreviations() + + if stripped.HasTypeDefinition then + ValueSome stripped.TypeDefinition + else + ValueNone + + static let tryOptionalValueTypeName (symbol: FSharpSymbol) = + match symbol with + | :? FSharpMemberOrFunctionOrValue as parameter -> + let arguments = parameter.FullType.StripAbbreviations().GenericArguments + + match arguments |> Seq.tryHeadV |> ValueOption.bind tryTypeDefinition with + | ValueSome definition -> definition.TryFullName + | ValueNone -> None + | _ -> None + + static let tryParameterSymbol (memberSymbol: FSharpSymbol) (name: string) = + match memberSymbol with + | :? FSharpMemberOrFunctionOrValue as mfv -> + mfv.CurriedParameterGroups + |> Seq.concat + |> Seq.tryFindV (fun candidate -> + match candidate.Name with + | Some candidateName -> String.Equals(candidateName, name, StringComparison.Ordinal) + | None -> false) + | _ -> ValueNone + + static let canBeFSharpOptional (defaultValue: SynExpr voption) (parameter: FSharpParameter) = + let attributes = parameter.Attributes + + let passesMissingValueWhenOmitted (definition: FSharpEntity) = hasFullName "System.Object" definition + + attributes |> Seq.exists isInteropOptional + && attributes + |> Seq.forall (fun attribute -> isInteropOptional attribute || isInteropDefaultParameterValue attribute) + && match tryTypeDefinition parameter.Type, defaultValue with + | ValueSome definition, _ when isByRefLike definition -> false + | ValueSome definition, ValueNone -> not (passesMissingValueWhenOmitted definition) + | ValueSome definition, ValueSome(SynExpr.Const(constant, _)) -> + OptionalParameterDefaultValueConversion.isConstantOfType definition.TryFullName constant + | ValueNone, ValueNone -> true + | _ -> false + + static let isConvertibleDotNetOptional + (memberSymbol: FSharpSymbol) + (parameter: OptionalParameterDefaultValueConversion.Parameter) + (defaultValue: SynExpr voption) + = + tryParameterSymbol memberSymbol parameter.Ident.idText + |> ValueOption.exists (canBeFSharpOptional defaultValue) + + static let projectAndDependentsSupportStructOptionals (project: Project) = + cancellableTask { + let solution = project.Solution + + let projects = + [ + project + + for id in solution.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject project.Id do + let dependent = solution.GetProject id + + if dependent.IsFSharp then + dependent + ] + + let mutable isSupported = true + + for candidate in projects do + if isSupported then + let! _, _, parsingOptions, _ = candidate.GetFSharpCompilationOptionsAsync() + + isSupported <- + LanguageVersion(parsingOptions.LangVersionText).SupportsFeature + LanguageFeature.SupportValueOptionsAsOptionalParameters + + return isSupported + } + + static let documentAction (document: Document) (sourceText: SourceText) (title: string) (changes: TextChange list) = + CodeAction.Create(title, cancellableTask { return document.WithText(sourceText.WithChanges changes) }, title) + + static let changedSolution + (document: Document) + (sourceText: SourceText) + (checkResults: FSharpCheckFileResults) + (memberUse: FSharpSymbolUse) + (parameter: OptionalParameterDefaultValueConversion.Parameter) + (definitionChanges: TextChange list) + = + 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, useCheckResults = + useDocument.GetFSharpParseAndCheckResultsAsync(nameof FSharpConvertOptionalParameterDefaultValueRefactoring) + + let changes = + [ + if documentId = document.Id then + yield! definitionChanges + + for _, useRange in documentUses do + yield! + OptionalParameterDefaultValueConversion.callSiteChanges + useCheckResults + text + useParseResults.ParseTree + 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 + } + + static let actionsFor + (document: Document) + (sourceText: SourceText) + (parseTree: ParsedInput) + (checkResults: FSharpCheckFileResults) + (memberUse: FSharpSymbolUse) + (parameter: OptionalParameterDefaultValueConversion.Parameter) + = + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken () + + match parameter.Form with + | OptionalParameterDefaultValueConversion.Form.FSharpOptional optionalValRange -> + match OptionalParameterDefaultValueConversion.tryGetSymbolUse checkResults sourceText [ parameter.Ident ] with + | Some parameterUse -> + let memberRange = parameter.MemberBinding.RangeOfBindingWithRhs + + let isUseInMemberBody (symbolUse: FSharpSymbolUse) = + not symbolUse.IsFromDefinition + && Position.posGeq symbolUse.Range.Start memberRange.Start + && Position.posGeq memberRange.End symbolUse.Range.End + + let usesInBody = + checkResults.GetUsesOfSymbolInFile(parameterUse.Symbol, cancellationToken = cancellationToken) + |> Seq.filter isUseInMemberBody + |> Seq.map _.Range + |> List.ofSeq + + return + OptionalParameterDefaultValueConversion.tryToDotNetChanges + checkResults + sourceText + parseTree + parameter + (tryOptionalValueTypeName parameterUse.Symbol) + optionalValRange + usesInBody + |> ValueOption.map (documentAction document sourceText (SR.UseDotNetOptionalParameter())) + |> ValueOption.toList + | None -> return [] + | OptionalParameterDefaultValueConversion.Form.DotNetOptional(lists, defaultValue) when + isConvertibleDotNetOptional memberUse.Symbol parameter defaultValue + -> + let! options = document.GetOptionsAsync cancellationToken + let! isStructSupported = projectAndDependentsSupportStructOptionals document.Project + + let indentSize = + options.GetOption(FormattingOptions.IndentationSize, FSharpConstants.FSharpLanguageName) + + let tryChanges (asStruct: bool) = + OptionalParameterDefaultValueConversion.tryToFSharpChanges + checkResults + sourceText + indentSize + parameter + lists + defaultValue + asStruct + + return + [ + match tryChanges false with + | ValueSome changes -> yield documentAction document sourceText (SR.UseFSharpOptionalParameter()) changes + | ValueNone -> () + + match tryChanges true with + | ValueSome definitionChanges when isStructSupported -> + let title = SR.UseFSharpStructOptionalParameter() + + yield + CodeAction.Create( + title, + changedSolution document sourceText checkResults memberUse parameter definitionChanges, + title + ) + | _ -> () + ] + | OptionalParameterDefaultValueConversion.Form.DotNetOptional _ -> return [] + } + + 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 OptionalParameterDefaultValueConversion.tryGetSymbolUse checkResults sourceText [ parameter.MemberName ] with + | Some memberUse when isConvertibleMember memberUse.Symbol -> + let! actions = actionsFor document sourceText parseResults.ParseTree checkResults memberUse parameter + List.iter context.RegisterRefactoring actions + | _ -> () + } + |> 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 5af8dfaab66..9634f409ee1 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf @@ -395,11 +395,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 e7cf1e34711..bc07a9ef789 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf @@ -395,11 +395,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 a763e64aca8..453057a78ab 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf @@ -395,11 +395,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 6fd805187e5..8aae6fd49b1 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf @@ -395,11 +395,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 766483153f0..2f598b6f7fe 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf @@ -395,11 +395,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 30834f89e84..ae6f7e188d7 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf @@ -395,11 +395,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 1ef22e3047d..55ce5cd5a48 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf @@ -395,11 +395,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 1091913daa2..d37776029ff 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf @@ -395,11 +395,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 28e5ff703d2..41ecf3ae180 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf @@ -395,11 +395,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 5787d9818f3..c9dc8382b3c 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf @@ -395,11 +395,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 933e42779d4..66a407e60c9 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf @@ -395,11 +395,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 69381cc377d..49fc20347c5 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf @@ -395,11 +395,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 4bdafbf0121..63d37f746f6 100644 --- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf +++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf @@ -395,11 +395,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 d5d1a09c79a..c2ad3f8d305 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -79,6 +79,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..6866844d3c8 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertOptionalParameterDefaultValueTests.fs @@ -0,0 +1,658 @@ +module FSharp.Editor.Tests.Refactors.ConvertOptionalParameterDefaultValueTests + +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 +open FSharp.Test.ProjectGeneration + +let private newLineOf (code: string) = + if code.IndexOf('\r') >= 0 then "\r\n" else "\n" + +let private moduleM (code: string) = + let newLine = newLineOf code + $"{newLine}module M{newLine}{newLine}{code}{newLine}" + +let private moduleMWithInterop (code: string) = + let newLine = newLineOf code + moduleM $"open System.Runtime.InteropServices{newLine}{newLine}{code}" + +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 hasTitle (title: string) (action: CodeAction) = + String.Equals(action.Title, title, StringComparison.Ordinal) + +let private textOf (document: Document) = + (document.GetTextAsync() |> GetTaskResult).ToString() + +let private assertNoErrors (document: Document) = + let _, checkResults = + document.GetFSharpParseAndCheckResultsAsync "test" + |> CancellableTask.runSynchronouslyWithoutCancellation + + Assert.Empty( + checkResults.Diagnostics + |> Array.filter (fun diagnostic -> diagnostic.Severity = FSharpDiagnosticSeverity.Error) + ) + +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 + assertNoErrors document + textOf document + +let private refactored (code: string) (marker: string) = refactoredBy Seq.head code marker + +let private refactoredWith (title: string) (code: string) (marker: string) = + refactoredBy (Seq.find (hasTitle title)) code marker + +let private assertRefactored (wrap: string -> string) (marker: string) (before: string) (after: string) = + Assert.Equal(wrap after, refactored (wrap before) marker) + +let private assertRefactoredWith (title: string) (wrap: string -> string) (marker: string) (before: string) (after: string) = + Assert.Equal(wrap after, refactoredWith title (wrap before) marker) + +let private actionsAt (code: string) (marker: string) = + use context = TestContext.CreateWithCode code + tryGetRefactoringActions code (caretAt code marker) context (new FSharpConvertOptionalParameterDefaultValueRefactoring()) + +let private actionsIn (document: Document) (marker: string) = + let actions = ResizeArray() + + let context = + CodeRefactoringContext( + document, + TextSpan(caretAt (textOf document) marker, 1), + (fun action -> actions.Add action), + CancellationToken.None + ) + + (new FSharpConvertOptionalParameterDefaultValueRefactoring()).ComputeRefactoringsAsync(context).GetAwaiter().GetResult() + + actions + +let private titlesOf (actions: CodeAction seq) = + actions |> Seq.map _.Title |> List.ofSeq + +let private documentOf (solution: Solution) (project: SyntheticProject) (fileId: string) = + solution.GetDocumentIdsWithFilePath(project.GetFilePath fileId) + |> Seq.exactlyOne + |> solution.GetDocument + +let private fileOf (fileId: string) (source: string) = + { sourceFile fileId [] with + Source = source + } + +let private fsharpForm = + moduleM + """type C() = + static member M(?x: int) = defaultArg x 0""" + +let private dotNetForm = + moduleMWithInterop + """type C() = + static member M([] x: int) = x""" + +[] +[] greeting: string) = + $"{greeting}, {name}" + +let a = Greeter().Greet("Ada") +let b = Greeter().Greet("Ada", greeting = "Hi")""")>] +[] greeting: string) = greeting + +let a = Greeter().Greet()""")>] +let ``Attributes are fully qualified where System.Runtime.InteropServices is not open`` (_scenario: string, before: string, after: string) = + assertRefactored moduleM "?greeting" before after + +[] +[] step: int) = + step + 1 + +let n = Counter.Next()""")>] +[] step: float) = + float value + step""")>] +[] step: Step) = + step + 1""")>] +[] step: int) = + step + 1""")>] +let ``Shadowing default converts both ways`` (_scenario: string, fsharpForm: string, dotNetForm: string) = + assertRefactored moduleMWithInterop "?step" fsharpForm dotNetForm + assertRefactored moduleMWithInterop "step:" dotNetForm fsharpForm + +[] +[] step: int) = value + step""", + "?step")>] +[] step: int) = value + step""", + """type Counter() = + static member Next(value: int, ?step: int) = + let step = defaultArg step 1 + value + step""", + "step:")>] +[] flag: bool) = 1""", + "?flag")>] +[] step: int) = + step + 1""", + """type C() = + static member M(?step: int) = + let step = defaultArg step Unchecked.defaultof<_> + step + 1""", + "step:")>] +[] step: int) = (* keep this *) step + 1""", + """type C() = + static member M(?step: int) = (* keep this *) + let step = defaultArg step Unchecked.defaultof<_> + step + 1""", + "step:")>] +[] x: int) = + let mutable x = x + x <- x + 1 + x""", + "?x")>] +let ``Optional parameter converts between the F# and the .NET form`` (_scenario: string, before: string, after: string, marker: string) = + assertRefactored moduleMWithInterop marker before after + +[] +[] x: int) = x""")>] +[] x: int) = x""")>] +let ``Attributes are qualified unless an enclosing module opens System.Runtime.InteropServices`` + (_scenario: string, before: string, after: string) + = + assertRefactored moduleM "?x" before after + +[] +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""", + """type Counter() = + static member Next(value: int, [] ?step: int) = + let step = defaultValueArg step 1 + value + step""")>] +[] step: int) = + step + 1""", + """type C() = + static member M([] ?step: int) = + let step = defaultValueArg step Unchecked.defaultof<_> + step + 1""")>] +[] step: int) = + step + 1""", + """let defaultValueArg (_: int voption) (fallback: int) = fallback + 1 + +type C() = + static member M([] ?step: int) = + let step = Microsoft.FSharp.Core.Operators.defaultValueArg step 1 + step + 1""")>] +[] step: int) = + step + 1 + +let a = Counter.Next() +let b = Counter.Next(?step = Some 2) +let c = Counter.Next(?step = None) +let d (s: int option) = Counter.Next(?step = s) +let e (s: int list) = Counter.Next(?step = List.tryHead s)""", + """type Counter() = + static member Next([] ?step: int) = + let step = defaultValueArg step 1 + step + 1 + +let a = Counter.Next() +let b = Counter.Next(?step = ValueSome 2) +let c = Counter.Next(?step = ValueNone) +let d (s: int option) = Counter.Next(?step = ValueOption.ofOption s) +let e (s: int list) = Counter.Next(?step = ValueOption.ofOption (List.tryHead s))""")>] +[([] step: int) = + step + sizeof<'T> + +let a = Counter.Next(?step = Some 2)""", + """type Counter() = + static member Next<'T>([] ?step: int) = + let step = defaultValueArg step 1 + step + sizeof<'T> + +let a = Counter.Next(?step = ValueSome 2)""")>] +[] step: int) = + step + 1 + +let a = Counter.Next(?step = ``Some`` 2) +let b = Counter.Next(?step = ``None``)""", + """type Counter() = + static member Next([] ?step: int) = + let step = defaultValueArg step 1 + step + 1 + +let a = Counter.Next(?step = ValueSome 2) +let b = Counter.Next(?step = ValueNone)""")>] +[] step: int) = + step + 1 + +type Shadow = + | ValueSome of int + | ValueNone + +let a = Counter.Next(?step = Some 2) +let b = Counter.Next(?step = None)""", + """type Counter() = + static member Next([] ?step: int) = + let step = defaultValueArg step 1 + step + 1 + +type Shadow = + | ValueSome of int + | ValueNone + +let a = Counter.Next(?step = Microsoft.FSharp.Core.ValueOption.ValueSome 2) +let b = Counter.Next(?step = Microsoft.FSharp.Core.ValueOption.ValueNone)""")>] +let ``Converting to a struct optional parameter`` (_scenario: string, before: string, after: string) = + assertRefactoredWith structTitle moduleMWithInterop "step:" before after + +[] +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)")>] +[] +[] +[] x: obj) = x""", + "x:")>] +[] +[] x: int) = x""", + "x:")>] +[] x: int) = x""", + "x:")>] +[] x: string) = x""", + "x:")>] +[] x: int, y: int) = x + y""", + "x:")>] +[] x: outref) = x <- true""", + "x:")>] +[] +type S = + val Value: int + +type C() = + static member M([] x: S) = x.Value""", + "x:")>] +[] [] x: int[]) = x.Length""", + "x:")>] +let ``No action`` (_scenario: string, code: string, marker: string) = + Assert.Empty(actionsAt (moduleM code) marker) + +[] +let ``Escaped parameter name keeps its backticks`` () = + let fsharpForm = + """type C() = + static member M(?``type``: int) = defaultArg ``type`` 7""" + + let dotNetForm = + """type C() = + static member M([] ``type``: int) = ``type``""" + + let shadowingForm = + """type C() = + static member M(?``type``: int) = + let ``type`` = defaultArg ``type`` 7 + ``type``""" + + assertRefactored moduleMWithInterop "?``type``" fsharpForm dotNetForm + assertRefactored moduleMWithInterop "``type``:" dotNetForm shadowingForm + +[] +let ``No action when the file has a signature`` () = + let signature = + moduleM + """type C = + new: unit -> C + static member M: ?x: int -> int""" + + let document = + RoslynTestHelpers.GetFsiAndFsDocuments signature fsharpForm |> Seq.last + + Assert.False(document.IsFSharpSignatureFile) + Assert.Empty(actionsIn document "?x") + +let private counterDefinition = + """ +module Definition + +open System.Runtime.InteropServices + +type Counter() = + static member Next([] step: int) = + step + 1 +""" + +let private counterCallers = + """ +module Callers + +let a = Definition.Counter.Next() +let b = Definition.Counter.Next(?step = Some 2) +""" + +let private inOneProject (definition: string) (callers: string) = + { SyntheticProject.Create(fileOf "Definition" definition, fileOf "Callers" callers) with + AutoAddModules = false + } + +let private assertCompilesAsProject (definition: string) (callers: string) = + let solution, _ = RoslynTestHelpers.CreateSolution(inOneProject definition callers) + use _ = new TestContext(solution) + + for document in (Seq.exactlyOne solution.Projects).Documents do + assertNoErrors document + +[] +let ``Converting to a struct optional parameter converts optional arguments in another file`` () = + let project = inOneProject counterDefinition counterCallers + let solution, _ = RoslynTestHelpers.CreateSolution project + use _ = new TestContext(solution) + + let action = + actionsIn (documentOf solution project "Definition") "step:" + |> Seq.find (hasTitle structTitle) + + let operation = + action.GetOperationsAsync CancellationToken.None + |> GetTaskResult + |> Seq.exactlyOne + + let changed = (operation :?> ApplyChangesOperation).ChangedSolution + + let definition = textOf (documentOf changed project "Definition") + let callers = textOf (documentOf changed project "Callers") + + Assert.Equal( + """ +module Definition + +open System.Runtime.InteropServices + +type Counter() = + static member Next([] ?step: int) = + let step = defaultValueArg step 1 + step + 1 +""", + definition + ) + + Assert.Equal( + """ +module Callers + +let a = Definition.Counter.Next() +let b = Definition.Counter.Next(?step = ValueSome 2) +""", + callers + ) + + assertCompilesAsProject definition callers + +[] +[] +[] +let ``Struct optional parameter is offered only when every dependent F# project supports it`` + (dependentLangVersion: string, isOffered: bool) + = + let library = + { SyntheticProject.Create("Library", fileOf "Definition" counterDefinition) with + AutoAddModules = false + } + + let app = + { SyntheticProject.Create("App", fileOf "Callers" counterCallers) with + AutoAddModules = false + DependsOn = [ library ] + OtherOptions = [ dependentLangVersion ] + } + + let struct (solution, _) = RoslynTestHelpers.CreateMultiProjectSolution app + use _ = new TestContext(solution) + + let titles = + actionsIn (documentOf solution library "Definition") "step:" |> titlesOf + + Assert.Equal( + [ + fsharpTitle + if isOffered then + structTitle + ], + titles + )