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
2 changes: 2 additions & 0 deletions docs/release-notes/.VisualStudio/18.vNext.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
### Added

* Refactoring to convert an anonymous record between `{| … |}` and `struct {| … |}`, following its value through the solution the same way. A copy-and-update `{| r with … |}` has its own form and converts independently of `r`. ([PR #20549](https://github.com/dotnet/fsharp/pull/20549))
* 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
6 changes: 6 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,12 @@
<Compile Include="Refactor\ChangeTypeofWithNameToNameofExpression.fs" />
<Compile Include="Refactor\AddExplicitTypeToParameter.fs" />
<Compile Include="Refactor\ChangeDerefToValueRefactoring.fs" />
<Compile Include="Refactor\StructConversion.fs" />
<Compile Include="Refactor\TupleConversion.fs" />
<Compile Include="Refactor\AnonymousRecordConversion.fs" />
<Compile Include="Refactor\StructPropagation.fs" />
<Compile Include="Refactor\ConvertTuple.fs" />
<Compile Include="Refactor\ConvertAnonymousRecord.fs" />
<Compile Include="CodeFixes\IFSharpCodeFix.fs" />
<Compile Include="CodeFixes\CodeFixHelpers.fs" />
<Compile Include="CodeFixes\ChangeEqualsInFieldTypeToColon.fs" />
Expand Down
12 changes: 12 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,16 @@ 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>
<data name="ConvertToStructAnonymousRecord" xml:space="preserve">
<value>Convert to struct anonymous record</value>
</data>
<data name="ConvertToReferenceAnonymousRecord" xml:space="preserve">
<value>Convert to reference anonymous record</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module internal Microsoft.VisualStudio.FSharp.Editor.AnonymousRecordConversion

open Microsoft.CodeAnalysis.Text

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

open StructConversion

/// Both forms differ only by `struct` in front of `{|`, which the node's range starts with.
let private keywordChanges (sourceText: SourceText) (toStruct: bool) (isStruct: bool) (m: range) =
match isStruct, toStruct with
| true, true
| false, false -> []
| false, true -> [ TextChange(TextSpan((spanOf sourceText m).Start, 0), "struct ") ]
| true, false -> [ TextChange(structKeyword sourceText m, "") ]

let kind: StructKind =
{
IsExpr =
fun expr _ ->
match expr with
| SynExpr.AnonRecd _ -> true
| _ -> false
IsPat = fun _ _ -> false
IsType =
function
| SynType.AnonRecd _ -> true
| _ -> false
IsStruct =
function
| CaretNode.Expr(node = SynExpr.AnonRecd(isStruct = isStruct))
| CaretNode.Type(node = SynType.AnonRecd(isStruct = isStruct)) -> isStruct
| _ -> false
ExprChanges =
fun sourceText toStruct expr _ ->
match expr with
| SynExpr.AnonRecd(isStruct = isStruct; range = m) -> ValueSome(keywordChanges sourceText toStruct isStruct m)
| _ -> ValueNone
PatChanges = fun _ _ _ _ -> ValueNone
TypeChanges =
fun sourceText toStruct _ ty ->
match ty with
| SynType.AnonRecd(isStruct = isStruct; range = m) -> keywordChanges sourceText toStruct isStruct m
| _ -> []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace Microsoft.VisualStudio.FSharp.Editor

open System.Composition

open Microsoft.CodeAnalysis.CodeRefactorings

open CancellableTasks

[<ExportCodeRefactoringProvider(FSharpConstants.FSharpLanguageName, Name = "ConvertAnonymousRecord"); Shared>]
type internal FSharpConvertAnonymousRecordRefactoring [<ImportingConstructor>] () =
inherit CodeRefactoringProvider()

override _.ComputeRefactoringsAsync context =
StructPropagation.registerConversion
context
AnonymousRecordConversion.kind
SR.ConvertToStructAnonymousRecord
SR.ConvertToReferenceAnonymousRecord
(nameof FSharpConvertAnonymousRecordRefactoring)
|> CancellableTask.startAsTask context.CancellationToken
22 changes: 22 additions & 0 deletions vsintegration/src/FSharp.Editor/Refactor/ConvertTuple.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace Microsoft.VisualStudio.FSharp.Editor

open System.Composition

open Microsoft.CodeAnalysis.CodeRefactorings

open CancellableTasks

[<ExportCodeRefactoringProvider(FSharpConstants.FSharpLanguageName, Name = "ConvertTuple"); Shared>]
type internal FSharpConvertTupleRefactoring [<ImportingConstructor>] () =
inherit CodeRefactoringProvider()

override _.ComputeRefactoringsAsync context =
StructPropagation.registerConversion
context
TupleConversion.kind
SR.ConvertToStructTuple
SR.ConvertToReferenceTuple
(nameof FSharpConvertTupleRefactoring)
|> CancellableTask.startAsTask context.CancellationToken
122 changes: 122 additions & 0 deletions vsintegration/src/FSharp.Editor/Refactor/StructConversion.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

module internal Microsoft.VisualStudio.FSharp.Editor.StructConversion

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 node's range.
let 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)

/// What a 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 node: SynExpr * path: SyntaxVisitorPath
| Pat of node: SynPat * path: SyntaxVisitorPath
| Type of node: SynType * annotated: Annotated * isWholeAnnotation: bool

/// A kind of node written in a reference and a struct form: how to recognize it and how to change its form.
[<NoComparison; NoEquality>]
type StructKind =
{
/// Whether the expression is a node of the kind, not only shaped like one (a method's argument list).
IsExpr: SynExpr -> SyntaxVisitorPath -> bool
/// Whether the pattern is a node of the kind, not only shaped like one (a method's parameter list).
IsPat: SynPat -> SyntaxVisitorPath -> bool
IsType: SynType -> bool
IsStruct: CaretNode -> bool
/// Changes giving a node of the kind the target form; ValueNone when it cannot change in place.
ExprChanges: SourceText -> bool -> SynExpr -> SyntaxVisitorPath -> TextChange list voption
PatChanges: SourceText -> bool -> SynPat -> SyntaxVisitorPath -> TextChange list voption
/// Changes giving a type of the kind the target form, knowing whether it is a whole annotation.
TypeChanges: SourceText -> bool -> bool -> SynType -> TextChange list
}

/// The innermost type of the kind within the type that contains the position.
let rec private tryTypeAt (kind: StructKind) (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) -> tryTypeAt kind position inner
| SynType.App(typeName = typeName; typeArgs = typeArgs)
| SynType.LongIdentApp(typeName = typeName; typeArgs = typeArgs) ->
typeName :: typeArgs |> Seq.tryPickV (tryTypeAt kind position)
| SynType.Fun(argType = argType; returnType = returnType) -> [ argType; returnType ] |> Seq.tryPickV (tryTypeAt kind position)
| SynType.Tuple(path = segments) ->
segments
|> Seq.tryPickV (function
| SynTupleTypeSegment.Type element -> tryTypeAt kind position element
| _ -> ValueNone)
| SynType.AnonRecd(fields = fields) -> fields |> Seq.tryPickV (fun (_, fieldType) -> tryTypeAt kind position fieldType)
| _ -> ValueNone

match inner with
| ValueSome _ -> inner
| ValueNone when kind.IsType ty -> ValueSome ty
| ValueNone -> ValueNone

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

(ValueNone, parseTree)
||> ParsedInput.fold (fun found path node ->
match node with
| SyntaxNode.SynExpr expr when containsPos expr.Range caret && kind.IsExpr expr path -> ValueSome(CaretNode.Expr(expr, path))
| SyntaxNode.SynPat pat when containsPos pat.Range caret && kind.IsPat pat path -> ValueSome(CaretNode.Pat(pat, 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