Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.VisualStudio/18.vNext.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
### Added

* Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880))
* Refactoring to convert a tuple between a reference tuple and a struct tuple, following its value through the solution: annotations of values, parameters, record fields and results it flows through, tuple patterns that take it apart, and the arguments and values that flow into it. What cannot be followed (`fst`, `snd`, generic collections) is left for the compiler to report. ([PR #20548](https://github.com/dotnet/fsharp/pull/20548))
* Expand `<inheritdoc/>` 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))

### Fixed
Expand Down
3 changes: 3 additions & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@
<Compile Include="Refactor\ChangeTypeofWithNameToNameofExpression.fs" />
<Compile Include="Refactor\AddExplicitTypeToParameter.fs" />
<Compile Include="Refactor\ChangeDerefToValueRefactoring.fs" />
<Compile Include="Refactor\TupleConversion.fs" />
<Compile Include="Refactor\TuplePropagation.fs" />
<Compile Include="Refactor\ConvertTuple.fs" />
<Compile Include="CodeFixes\IFSharpCodeFix.fs" />
<Compile Include="CodeFixes\CodeFixHelpers.fs" />
<Compile Include="CodeFixes\ChangeEqualsInFieldTypeToColon.fs" />
Expand Down
6 changes: 6 additions & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.resx
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,10 @@ Use live (unsaved) buffers for analysis</value>
<data name="ReturnsHeader" xml:space="preserve">
<value>Returns:</value>
</data>
<data name="ConvertToStructTuple" xml:space="preserve">
<value>Convert to struct tuple</value>
</data>
<data name="ConvertToReferenceTuple" xml:space="preserve">
<value>Convert to reference tuple</value>
</data>
</root>
86 changes: 86 additions & 0 deletions vsintegration/src/FSharp.Editor/Refactor/ConvertTuple.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace Microsoft.VisualStudio.FSharp.Editor

open System
open System.Composition
open System.Threading
open System.Threading.Tasks

open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.CodeActions
open Microsoft.CodeAnalysis.CodeRefactorings

open FSharp.Compiler.Syntax
open FSharp.Compiler.Text

open CancellableTasks
open TupleConversion

[<ExportCodeRefactoringProvider(FSharpConstants.FSharpLanguageName, Name = "ConvertTuple"); Shared>]
type internal FSharpConvertTupleRefactoring [<ImportingConstructor>] () =
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 isInQuotation (caretNode: CaretNode) =
let path =
match caretNode with
| CaretNode.Expr(path = path)
| CaretNode.Pat(path = path)
| CaretNode.Type(annotated = Annotated.Pattern(path = path))
| CaretNode.Type(annotated = Annotated.Expression(path = path)) -> path
| CaretNode.Type _ -> []

path
|> List.exists (function
| SyntaxNode.SynExpr(SynExpr.Quote _) -> true
| _ -> 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 FSharpConvertTupleRefactoring)

let caret =
let linePosition = sourceText.Lines.GetLinePosition context.Span.Start
Position.mkPos (Line.fromZ linePosition.Line) linePosition.Character

match tryCaretNode caret parseResults.ParseTree with
| ValueSome caretNode when not (isInQuotation caretNode) ->
let title =
if isStructNode caretNode then
SR.ConvertToReferenceTuple()
else
SR.ConvertToStructTuple()

let changedSolution =
cancellableTask {
let! converted = TuplePropagation.tryConvert document caretNode (nameof FSharpConvertTupleRefactoring)

return
match converted with
| ValueSome solution -> solution
| ValueNone -> document.Project.Solution
}

let action =
CodeAction.Create(
title,
Func<CancellationToken, Task<Solution>>(fun cancellationToken ->
CancellableTask.start cancellationToken changedSolution),
title
)

context.RegisterRefactoring action
| _ -> ()
}
|> CancellableTask.startAsTask context.CancellationToken
246 changes: 246 additions & 0 deletions vsintegration/src/FSharp.Editor/Refactor/TupleConversion.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module internal Microsoft.VisualStudio.FSharp.Editor.TupleConversion

open System

open Microsoft.CodeAnalysis.Text

open FSharp.Compiler.Syntax
open FSharp.Compiler.Text

let spanOf (sourceText: SourceText) (m: range) =
RoslynHelpers.FSharpRangeToTextSpan(sourceText, m)

let isSame (node: 'T) (other: 'T) = obj.ReferenceEquals(node, other)

let containsPos (m: range) (position: pos) =
Position.posGeq position m.Start && Position.posGeq m.End position

let rec stripParenTypes (ty: SynType) =
match ty with
| SynType.Paren(innerType = inner) -> stripParenTypes inner
| _ -> ty

/// `struct` and the blanks after it, at the start of a struct tuple's range.
let private structKeyword (sourceText: SourceText) (m: range) =
let start = (spanOf sourceText m).Start
let mutable finish = start + "struct".Length

while finish < sourceText.Length && Char.IsWhiteSpace sourceText[finish] do
finish <- finish + 1

TextSpan.FromBounds(start, finish)

/// The position of the `(` that, with its `)`, encloses only the span and blanks.
let private tryEnclosingParen (sourceText: SourceText) (span: TextSpan) =
let mutable before = span.Start - 1

while before >= 0 && Char.IsWhiteSpace sourceText[before] do
before <- before - 1

let mutable after = span.End

while after < sourceText.Length && Char.IsWhiteSpace sourceText[after] do
after <- after + 1

if
before >= 0
&& after < sourceText.Length
&& sourceText[before] = '('
&& sourceText[after] = ')'
then
ValueSome before
else
ValueNone

/// `struct ` to insert in front of the `(` at the position, after a space when it would run into a name: `f(a, b)`.
let private structAt (sourceText: SourceText) (position: int) =
match if position > 0 then sourceText[position - 1] else ' ' with
| c when Char.IsLetterOrDigit c || c = '_' || c = '\'' || c = '`' || c = ')' || c = ']' -> " struct "
| _ -> "struct "

/// Whether the text between start and finish is a whole generic argument: `<` or `,` before it, `>` or `,` after.
let private isGenericArgument (sourceText: SourceText) (start: int) (finish: int) =
let mutable before = start - 1

while before >= 0 && Char.IsWhiteSpace sourceText[before] do
before <- before - 1

let mutable after = finish

while after < sourceText.Length && Char.IsWhiteSpace sourceText[after] do
after <- after + 1

before >= 0
&& after < sourceText.Length
&& (sourceText[before] = '<' || sourceText[before] = ',')
&& (sourceText[after] = '>' || sourceText[after] = ',')

/// Changes giving a tuple type the target kind; a whole annotation also loses the parentheses `struct` needed.
let typeChanges (sourceText: SourceText) (toStruct: bool) (isWholeAnnotation: bool) (tupleType: SynType) =
match tupleType with
| SynType.Tuple(isStruct = isStruct; range = m) when isStruct <> toStruct ->
let span = spanOf sourceText m

if toStruct then
match tryEnclosingParen sourceText span with
| ValueSome openParen -> [ TextChange(TextSpan(openParen, 0), "struct ") ]
| ValueNone ->
[
TextChange(TextSpan(span.Start, 0), "struct (")
TextChange(TextSpan(span.End, 0), ")")
]
else
let keyword = structKeyword sourceText m

if isWholeAnnotation || isGenericArgument sourceText keyword.Start span.End then
[
TextChange(TextSpan(keyword.Start, keyword.Length + 1), "")
TextChange(TextSpan(span.End - 1, 1), "")
]
else
[ TextChange(keyword, "") ]
| _ -> []

/// Whether the tuple is the argument list of a method, constructor or union case call rather than a tuple value.
let isArgumentList (tuple: SynExpr) (path: SyntaxVisitorPath) =
match path with
| SyntaxNode.SynExpr(SynExpr.Paren(expr = inner) as paren) :: SyntaxNode.SynExpr(SynExpr.App(flag = ExprAtomicFlag.Atomic; argExpr = arg) | SynExpr.New(
expr = arg)) :: _ -> isSame inner tuple && isSame arg paren
| _ -> false

/// Whether the tuple pattern is the parameter list of a member or constructor: its only argument, parenthesized or
/// (a struct tuple) not.
let isParameterList (tuple: SynPat) (path: SyntaxVisitorPath) =
let struct (argument, headPath) =
match path with
| SyntaxNode.SynPat(SynPat.Paren(pat = inner) as paren) :: rest when isSame inner tuple -> struct (paren, rest)
| _ -> struct (tuple, path)

match headPath with
| SyntaxNode.SynPat(SynPat.LongIdent(argPats = SynArgPats.Pats [ only ])) :: SyntaxNode.SynBinding(SynBinding(
valData = SynValData(memberFlags = Some _))) :: _ -> isSame only argument
| _ -> false

/// Changes giving a tuple expression the target kind; ValueNone when it is not a tuple or cannot change in place.
let tryExprChanges (sourceText: SourceText) (toStruct: bool) (tuple: SynExpr) (path: SyntaxVisitorPath) =
match tuple with
| SynExpr.Tuple(isStruct = isStruct) when isStruct = toStruct -> ValueSome []
| SynExpr.Tuple(range = m) when not toStruct -> ValueSome [ TextChange(structKeyword sourceText m, "") ]
| SynExpr.Tuple(range = m) ->
match path with
| SyntaxNode.SynExpr(SynExpr.Paren(expr = inner; range = parenRange)) :: _ when isSame inner tuple ->
let start = (spanOf sourceText parenRange).Start
ValueSome [ TextChange(TextSpan(start, 0), structAt sourceText start) ]
| _ when m.StartLine = m.EndLine ->
let span = spanOf sourceText m

ValueSome
[
TextChange(TextSpan(span.Start, 0), "struct (")
TextChange(TextSpan(span.End, 0), ")")
]
| _ -> ValueNone
| _ -> ValueNone

/// Changes giving a tuple pattern the target kind; ValueNone when it is not a tuple or cannot change in place.
let tryPatChanges (sourceText: SourceText) (toStruct: bool) (tuple: SynPat) (path: SyntaxVisitorPath) =
match tuple with
| SynPat.Tuple(isStruct = isStruct) when isStruct = toStruct -> ValueSome []
| SynPat.Tuple(range = m) when not toStruct -> ValueSome [ TextChange(structKeyword sourceText m, "") ]
| SynPat.Tuple(range = m) ->
match path with
| SyntaxNode.SynPat(SynPat.Paren(pat = inner; range = parenRange)) :: _ when isSame inner tuple ->
let start = (spanOf sourceText parenRange).Start
ValueSome [ TextChange(TextSpan(start, 0), structAt sourceText start) ]
| _ when m.StartLine = m.EndLine ->
let span = spanOf sourceText m

ValueSome
[
TextChange(TextSpan(span.Start, 0), "struct (")
TextChange(TextSpan(span.End, 0), ")")
]
| _ -> ValueNone
| _ -> ValueNone

/// The innermost tuple type within the type that contains the position.
let rec tryTupleTypeAt (position: pos) (ty: SynType) =
if not (containsPos ty.Range position) then
ValueNone
else
let inner =
match ty with
| SynType.Paren(innerType = inner)
| SynType.Array(elementType = inner)
| SynType.WithGlobalConstraints(typeName = inner) -> tryTupleTypeAt position inner
| SynType.App(typeName = typeName; typeArgs = typeArgs)
| SynType.LongIdentApp(typeName = typeName; typeArgs = typeArgs) ->
typeName :: typeArgs |> Seq.tryPickV (tryTupleTypeAt position)
| SynType.Fun(argType = argType; returnType = returnType) -> [ argType; returnType ] |> Seq.tryPickV (tryTupleTypeAt position)
| SynType.Tuple(path = segments) ->
segments
|> Seq.tryPickV (function
| SynTupleTypeSegment.Type element -> tryTupleTypeAt position element
| _ -> ValueNone)
| _ -> ValueNone

match inner, ty with
| ValueSome _, _ -> inner
| ValueNone, SynType.Tuple _ -> ValueSome ty
| ValueNone, _ -> ValueNone

/// What a tuple type under the caret annotates.
[<RequireQualifiedAccess; NoComparison; NoEquality>]
type Annotated =
| Pattern of pat: SynPat * path: SyntaxVisitorPath
| Expression of expr: SynExpr * path: SyntaxVisitorPath
| Return of binding: SynBinding
| Field of field: SynField

[<RequireQualifiedAccess; NoComparison; NoEquality>]
type CaretNode =
| Expr of tuple: SynExpr * path: SyntaxVisitorPath
| Pat of tuple: SynPat * path: SyntaxVisitorPath
| Type of tuple: SynType * annotated: Annotated * isWholeAnnotation: bool

let isStructNode (node: CaretNode) =
match node with
| CaretNode.Expr(tuple = SynExpr.Tuple(isStruct = isStruct))
| CaretNode.Pat(tuple = SynPat.Tuple(isStruct = isStruct))
| CaretNode.Type(tuple = SynType.Tuple(isStruct = isStruct)) -> isStruct
| _ -> false

/// The innermost tuple expression, pattern or annotated tuple type under the caret.
let tryCaretNode (caret: pos) (parseTree: ParsedInput) =
let annotationAt (annotation: SynType) (annotated: Annotated) =
tryTupleTypeAt caret annotation
|> ValueOption.map (fun tuple -> CaretNode.Type(tuple, annotated, isSame (stripParenTypes annotation) tuple))

(ValueNone, parseTree)
||> ParsedInput.fold (fun found path node ->
match node with
| SyntaxNode.SynExpr(SynExpr.Tuple(range = m) as tuple) when containsPos m caret && not (isArgumentList tuple path) ->
ValueSome(CaretNode.Expr(tuple, path))
| SyntaxNode.SynPat(SynPat.Tuple(range = m) as tuple) when containsPos m caret && not (isParameterList tuple path) ->
ValueSome(CaretNode.Pat(tuple, path))
| SyntaxNode.SynPat(SynPat.Typed(targetType = annotation) as pat) when containsPos annotation.Range caret ->
annotationAt annotation (Annotated.Pattern(pat, path))
|> ValueOption.orElse found
| SyntaxNode.SynExpr(SynExpr.Typed(targetType = annotation) as expr) when containsPos annotation.Range caret ->
annotationAt annotation (Annotated.Expression(expr, path))
|> ValueOption.orElse found
| SyntaxNode.SynBinding(SynBinding(returnInfo = Some(SynBindingReturnInfo(typeName = annotation))) as binding) when
containsPos annotation.Range caret
->
annotationAt annotation (Annotated.Return binding) |> ValueOption.orElse found
| SyntaxNode.SynTypeDefn(SynTypeDefn(
typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Record(recordFieldsAndSpreads = fields), _))) ->
fields
|> Seq.tryPickV (function
| SynFieldOrSpread.Field(SynField(fieldType = annotation) as field) when containsPos annotation.Range caret ->
annotationAt annotation (Annotated.Field field)
| _ -> ValueNone)
|> ValueOption.orElse found
| _ -> found)
Loading
Loading