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
Expand Up @@ -4,6 +4,7 @@
* 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 `<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))
* 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))
* Add a refactoring that converts between a `fun x -> x.Prop` lambda and the F# 8 `_.Prop` shorthand. ([Issue #16234](https://github.com/dotnet/fsharp/issues/16234), [PR #20535](https://github.com/dotnet/fsharp/pull/20535))

### Fixed

Expand Down
69 changes: 32 additions & 37 deletions src/Compiler/SyntaxTree/SyntaxTreeOps.fs
Original file line number Diff line number Diff line change
Expand Up @@ -67,51 +67,46 @@ let mkSynSimplePatVar isOpt id =
let mkSynCompGenSimplePatVar id =
SynSimplePat.Id(id, None, true, false, false, id.idRange)

let rec pushUnaryArg expr arg =
let rec private splitDotLambdaChain expr : struct (SynExpr * (SynExpr -> SynExpr)) =
let inside inner wrap =
let struct (head, rebuild) = splitDotLambdaChain inner
struct (head, rebuild >> wrap)

match expr with
| SynExpr.App(ExprAtomicFlag.Atomic, infix, SynExpr.Ident ident, x1, m1) ->
SynExpr.App(
ExprAtomicFlag.Atomic,
infix,
SynExpr.LongIdent(false, SynLongIdent(arg :: [ ident ], [ ident.idRange ], [ None ]), None, ident.idRange),
x1,
m1
)
| SynExpr.App(ExprAtomicFlag.Atomic,
infix,
SynExpr.LongIdent(isOptional, SynLongIdent(id, dotRanges, trivia), altNameRefCell, range),
x1,
m1) ->
SynExpr.App(
ExprAtomicFlag.Atomic,
infix,
SynExpr.LongIdent(isOptional, SynLongIdent(arg :: id, dotRanges, trivia), altNameRefCell, range),
x1,
m1
)
| SynExpr.App(ExprAtomicFlag.Atomic, infix, (SynExpr.App _ as innerApp), x1, m1) ->
SynExpr.App(ExprAtomicFlag.Atomic, infix, (pushUnaryArg innerApp arg), x1, m1)
| SynExpr.App(ExprAtomicFlag.Atomic, infix, SynExpr.DotGet(synExpr, rangeOfDot, synLongIdent, range), x1, m1) ->
SynExpr.App(ExprAtomicFlag.Atomic, infix, SynExpr.DotGet((pushUnaryArg synExpr arg), rangeOfDot, synLongIdent, range), x1, m1)
| SynExpr.App(ExprAtomicFlag.Atomic, infix, innerExpr, x1, m1) ->
SynExpr.App(ExprAtomicFlag.Atomic, infix, pushUnaryArg innerExpr arg, x1, m1)
| SynExpr.Ident ident -> SynExpr.LongIdent(false, SynLongIdent(arg :: [ ident ], [ ident.idRange ], [ None ]), None, ident.idRange)
| SynExpr.LongIdent(isOptional, SynLongIdent(id, dotRanges, trivia), altNameRefCell, range) ->
SynExpr.LongIdent(isOptional, SynLongIdent(arg :: id, dotRanges, trivia), altNameRefCell, range)
| SynExpr.DotGet(synExpr, rangeOfDot, synLongIdent, range) -> SynExpr.DotGet(pushUnaryArg synExpr arg, rangeOfDot, synLongIdent, range)
| SynExpr.App(ExprAtomicFlag.Atomic, infix, funcExpr, argExpr, m) ->
inside funcExpr (fun funcExpr -> SynExpr.App(ExprAtomicFlag.Atomic, infix, funcExpr, argExpr, m))
| SynExpr.DotGet(synExpr, rangeOfDot, synLongIdent, range) ->
inside synExpr (fun synExpr -> SynExpr.DotGet(synExpr, rangeOfDot, synLongIdent, range))
| SynExpr.DotIndexedGet(objectExpr, indexArgs, dotRange, range) ->
SynExpr.DotIndexedGet(pushUnaryArg objectExpr arg, indexArgs, dotRange, range)
inside objectExpr (fun objectExpr -> SynExpr.DotIndexedGet(objectExpr, indexArgs, dotRange, range))
| SynExpr.TypeApp(innerExpr, mLess, tyargs, mCommas, mGreater, mTypars, m) ->
let innerExpr = pushUnaryArg innerExpr arg
SynExpr.TypeApp(innerExpr, mLess, tyargs, mCommas, mGreater, mTypars, m)
inside innerExpr (fun innerExpr -> SynExpr.TypeApp(innerExpr, mLess, tyargs, mCommas, mGreater, mTypars, m))
| _ -> struct (expr, id)

let rec pushUnaryArg expr arg =
let struct (head, rebuild) = splitDotLambdaChain expr

match head with
| SynExpr.Ident ident ->
rebuild (SynExpr.LongIdent(false, SynLongIdent(arg :: [ ident ], [ ident.idRange ], [ None ]), None, ident.idRange))
| SynExpr.LongIdent(isOptional, SynLongIdent(id, dotRanges, trivia), altNameRefCell, range) ->
rebuild (SynExpr.LongIdent(isOptional, SynLongIdent(arg :: id, dotRanges, trivia), altNameRefCell, range))
| SynExpr.ArbitraryAfterError(_, m) when m.Start = m.End ->
SynExpr.DiscardAfterMissingQualificationAfterDot(SynExpr.Ident arg, m.StartRange, unionRanges arg.idRange m)
rebuild (SynExpr.DiscardAfterMissingQualificationAfterDot(SynExpr.Ident arg, m.StartRange, unionRanges arg.idRange m))
| SynExpr.DiscardAfterMissingQualificationAfterDot(synExpr, dotRange, m) ->
SynExpr.DiscardAfterMissingQualificationAfterDot(pushUnaryArg synExpr arg, dotRange, unionRanges arg.idRange m)
rebuild (SynExpr.DiscardAfterMissingQualificationAfterDot(pushUnaryArg synExpr arg, dotRange, unionRanges arg.idRange m))
| _ ->
errorR (Error(FSComp.SR.tcDotLambdaAtNotSupportedExpression (), expr.Range))
errorR (Error(FSComp.SR.tcDotLambdaAtNotSupportedExpression (), head.Range))
expr

let tryPopUnaryArg body =
let struct (head, rebuild) = splitDotLambdaChain body

match head with
| SynExpr.LongIdent(isOptional, SynLongIdent(root :: (_ :: _ as rest), dotRanges, trivia), altNameRefCell, range) ->
ValueSome(struct (root, rebuild (SynExpr.LongIdent(isOptional, SynLongIdent(rest, dotRanges, trivia), altNameRefCell, range))))
| _ -> ValueNone

/// CAUTION: This function operates over the untyped tree, so should be used only when absolutely necessary. It doesn't verify assembly origine nor does it respect type aliases.
/// Also, keep in mind that it will only check last part of the assembly (with or without the `Attribute` suffix).
let inline findSynAttribute (attrName: string) (synAttrs: SynAttributes) =
Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/SyntaxTree/SyntaxTreeOps.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ val mkSynCompGenSimplePatVar: id: Ident -> SynSimplePat

val pushUnaryArg: expr: SynExpr -> arg: Ident -> SynExpr

val tryPopUnaryArg: body: SynExpr -> struct (Ident * SynExpr) voption

val inline findSynAttribute: attrName: string -> synAttrs: SynAttributes -> bool

[<return: Struct>]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
<Compile Include="SourceTextTests.fs" />
<Compile Include="SynExprTests.fs" />
<Compile Include="SynPatTests.fs" />
<Compile Include="SyntaxTreeOpsTests.fs" />
<Compile Include="CapturedTypes.fs" />
<Compile Include="WarnScopeTests.fs" />
<Compile Include="OverloadCacheTests.fs" />
Expand Down
39 changes: 39 additions & 0 deletions tests/FSharp.Compiler.Service.Tests/SyntaxTreeOpsTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module FSharp.Compiler.Service.Tests.SyntaxTreeOpsTests

open FSharp.Compiler.Service.Tests.Common
open FSharp.Compiler.Syntax
open FSharp.Compiler.SyntaxTreeOps
open Xunit

let private exprOf source =
match getParseResults source with
| ParsedInput.ImplFile(ParsedImplFileInput(contents = [ SynModuleOrNamespace(decls = [ SynModuleDecl.Expr(expr = expr) ]) ])) -> expr
| tree -> failwith $"Expected a single expression, got %A{tree}"

[<Theory>]
[<InlineData("x.P")>]
[<InlineData("x.A.B")>]
[<InlineData("x.M(y)")>]
[<InlineData("x.Xs[0]")>]
[<InlineData("x.Xs.[0]")>]
[<InlineData("x.M<int>()")>]
[<InlineData("x.M().P")>]
let ``tryPopUnaryArg splits the root off a chain that pushUnaryArg restores`` (source: string) =
let body = exprOf source

match tryPopUnaryArg body with
| ValueSome(struct (root, popped)) ->
Assert.Equal("x", root.idText)
Assert.Equal($"%A{body}", $"%A{pushUnaryArg popped root}")
| ValueNone -> Assert.Fail $"Expected {source} to have a root"

[<Theory>]
[<InlineData("f x.P")>]
[<InlineData("x.M y")>]
[<InlineData("x.P + 1")>]
[<InlineData("x")>]
[<InlineData("x[0]")>]
[<InlineData("(x.P)")>]
[<InlineData("x.")>]
let ``tryPopUnaryArg rejects what _. cannot express`` (source: string) =
Assert.True((tryPopUnaryArg (exprOf source)).IsNone)
1 change: 1 addition & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
<Compile Include="Refactor\ChangeTypeofWithNameToNameofExpression.fs" />
<Compile Include="Refactor\AddExplicitTypeToParameter.fs" />
<Compile Include="Refactor\ChangeDerefToValueRefactoring.fs" />
<Compile Include="Refactor\ConvertDotLambda.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="ConvertToShorthandLambda" xml:space="preserve">
<value>Convert to '_.' shorthand lambda</value>
</data>
<data name="ConvertToFunLambda" xml:space="preserve">
<value>Convert to 'fun' lambda</value>
</data>
</root>
207 changes: 207 additions & 0 deletions vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// 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.CodeActions
open Microsoft.CodeAnalysis.CodeRefactorings
open Microsoft.CodeAnalysis.Text
open Microsoft.VisualStudio.FSharp.Editor.Telemetry

open FSharp.Compiler.Syntax
open FSharp.Compiler.SyntaxTreeOps
open FSharp.Compiler.SyntaxTrivia
open FSharp.Compiler.Text

open CancellableTasks

[<RequireQualifiedAccess>]
module private DotLambdaConversion =

[<NoComparison; NoEquality>]
type Conversion =
{
Title: string
Direction: string
Change: TextChange
}

let hasName (name: string) (ident: Ident) =
String.Equals(ident.idText, name, StringComparison.Ordinal)

let identifiersIn (body: SynExpr) =
([], [ SyntaxNode.SynExpr body ])
||> SyntaxNodes.fold (fun identifiers _ node ->
match node with
| SyntaxNode.SynExpr(SynExpr.Ident ident)
| SyntaxNode.SynExpr(SynExpr.LongIdent(longDotId = SynLongIdent(id = ident :: _)))
| SyntaxNode.SynExpr(SynExpr.LongIdentSet(longDotId = SynLongIdent(id = ident :: _)))
| SyntaxNode.SynExpr(SynExpr.NamedIndexedPropertySet(longDotId = SynLongIdent(id = ident :: _)))
| SyntaxNode.SynPat(SynPat.Named(ident = SynIdent(ident, _))) -> ident :: identifiers
| _ -> identifiers)

let parameterNameFor (body: SynExpr) =
let used = identifiersIn body

Seq.initInfinite (fun i -> if i = 0 then "x" else $"x{i}")
|> Seq.find (fun name -> not (List.exists (hasName name) used))

let canBeCapturedByGeneratedArgument (ident: Ident) =
let prefix = "_arg"
let name = ident.idText

let rec isDigitsFrom i =
i = name.Length || (Char.IsDigit name[i] && isDigitsFrom (i + 1))

name.Length > prefix.Length
&& name.StartsWith(prefix, StringComparison.Ordinal)
&& isDigitsFrom prefix.Length

let hasCommentIn (comments: CommentTrivia list) (m: range) =
comments
|> List.exists (fun (CommentTrivia.LineComment comment | CommentTrivia.BlockComment comment) -> Range.rangeContainsRange m comment)

let rec lastOffsideSensitiveLine (expr: SynExpr) =
match expr with
| SynExpr.App(flag = ExprAtomicFlag.Atomic; funcExpr = func) -> lastOffsideSensitiveLine func
| SynExpr.DotGet(longDotId = longDotId) -> longDotId.Range.EndLine
| SynExpr.DotIndexedGet(dotRange = dot) -> dot.EndLine
| _ -> expr.Range.EndLine

let isInQuotation (path: SyntaxVisitorPath) =
path
|> List.exists (function
| SyntaxNode.SynExpr(SynExpr.Quote _) -> true
| _ -> false)

let rec isAppliedDirectly (expr: SynExpr) (path: SyntaxVisitorPath) =
match path with
| SyntaxNode.SynExpr(SynExpr.Paren _ as paren) :: outer -> isAppliedDirectly paren outer
| SyntaxNode.SynExpr(SynExpr.App(funcExpr = func)) :: _ -> obj.ReferenceEquals(func, expr)
| _ -> false

let isBlankBetween (sourceText: SourceText) start finish =
start <= finish
&& String.IsNullOrWhiteSpace(sourceText.ToString(TextSpan.FromBounds(start, finish)))

[<return: Struct>]
let (|SoleParenthesizedLambdaArgument|_|) (sourceText: SourceText) (lambda: SynExpr) (lambdaSpan: TextSpan) (path: SyntaxVisitorPath) =
match path with
| SyntaxNode.SynExpr(SynExpr.Paren(expr = inner; leftParenRange = leftParen; rightParenRange = Some rightParen; range = parenRange) as paren) :: SyntaxNode.SynExpr(SynExpr.App(
flag = ExprAtomicFlag.NonAtomic; isInfix = false; argExpr = arg)) :: _ when
obj.ReferenceEquals(inner, lambda)
&& obj.ReferenceEquals(arg, paren)
&& isBlankBetween sourceText (RoslynHelpers.FSharpRangeToTextSpan(sourceText, leftParen)).End lambdaSpan.Start
&& isBlankBetween sourceText lambdaSpan.End (RoslynHelpers.FSharpRangeToTextSpan(sourceText, rightParen)).Start
->
ValueSome(RoslynHelpers.FSharpRangeToTextSpan(sourceText, parenRange))
| _ -> ValueNone

let toShorthand (sourceText: SourceText) (path: SyntaxVisitorPath) (lambda: SynExpr) (root: Ident) =
let lambdaSpan = RoslynHelpers.FSharpRangeToTextSpan(sourceText, lambda.Range)
let rootEnd = RoslynHelpers.FSharpRangeToTextSpan(sourceText, root.idRange).End

match path with
| SoleParenthesizedLambdaArgument sourceText lambda lambdaSpan parenSpan ->
let chain = sourceText.ToString(TextSpan.FromBounds(rootEnd, lambdaSpan.End))
TextChange(parenSpan, $"_{chain}")
| _ -> TextChange(TextSpan.FromBounds(lambdaSpan.Start, rootEnd), "_")
Comment thread
abonie marked this conversation as resolved.

let toLambda (sourceText: SourceText) (path: SyntaxVisitorPath) (body: SynExpr) (range: range) (trivia: SynExprDotLambdaTrivia) =
let name = parameterNameFor body
let lambda = $"fun {name} -> {name}"

let underscore =
RoslynHelpers.FSharpRangeToTextSpan(sourceText, trivia.UnderscoreRange)

match path with
| SyntaxNode.SynExpr(SynExpr.Paren _) :: _
| SyntaxNode.SynBinding _ :: _ -> TextChange(underscore, lambda)
| _ ->
let span = RoslynHelpers.FSharpRangeToTextSpan(sourceText, range)
let chain = sourceText.ToString(TextSpan.FromBounds(underscore.End, span.End))
TextChange(span, $"({lambda}{chain})")

Comment thread
abonie marked this conversation as resolved.
let tryFind (sourceText: SourceText) (position: pos) (parseTree: ParsedInput) =
let comments =
match parseTree with
| ParsedInput.ImplFile file -> file.Trivia.CodeComments
| ParsedInput.SigFile file -> file.Trivia.CodeComments

(position, parseTree)
||> ParsedInput.tryPickLast (fun path node ->
match node with
| SyntaxNode.SynExpr(SynExpr.Lambda(
fromMethod = false
parsedData = Some([ SynPat.Named(ident = SynIdent(parameter, _); isThisVal = false; accessibility = None) ], body)) as lambda) when
not (isInQuotation path) && not (isAppliedDirectly lambda path)
->
let identifiers = identifiersIn body

match tryPopUnaryArg body with
| ValueSome(struct (root, _)) when
hasName parameter.idText root
&& (identifiers |> List.filter (hasName parameter.idText) |> List.length) = 1
&& not (List.exists canBeCapturedByGeneratedArgument identifiers)
&& not (hasCommentIn comments (Range.withEnd root.idRange.End lambda.Range))
->
Comment thread
abonie marked this conversation as resolved.
Some
{
Title = SR.ConvertToShorthandLambda()
Direction = "shorthand"
Change = toShorthand sourceText path lambda root
Comment thread
abonie marked this conversation as resolved.
}
| _ -> None

| SyntaxNode.SynExpr(SynExpr.DotLambda(expr = body; range = range; trivia = trivia)) when
not body.IsArbExprAndThusAlreadyReportedError
Comment thread
abonie marked this conversation as resolved.
&& not (isInQuotation path)
&& lastOffsideSensitiveLine body = range.StartLine
->
Some
{
Title = SR.ConvertToFunLambda()
Direction = "lambda"
Change = toLambda sourceText path body range trivia
}

| _ -> None)

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

override _.ComputeRefactoringsAsync context =
cancellableTask {
let document = context.Document

if not document.IsFSharpSignatureFile then
let! cancellationToken = CancellableTask.getCancellationToken ()
let! sourceText = document.GetTextAsync cancellationToken

let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpConvertDotLambdaRefactoring)

let caret = sourceText.Lines.GetLinePosition context.Span.Start
let position = Position.fromZ caret.Line caret.Character

match DotLambdaConversion.tryFind sourceText position parseResults.ParseTree with
| Some conversion ->
let changedDocument =
cancellableTask {
TelemetryReporter.ReportSingleEvent(
TelemetryEvents.RefactoringActivated,
[|
"name", box (nameof FSharpConvertDotLambdaRefactoring)
"direction", box conversion.Direction
|]
)

return document.WithText(sourceText.WithChanges conversion.Change)
}

context.RegisterRefactoring(CodeAction.Create(conversion.Title, changedDocument, conversion.Title))
| None -> ()
}
|> CancellableTask.startAsTask context.CancellationToken
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ module TelemetryEvents =
[<Literal>]
let CodefixActivated = "codefixactivated"

[<Literal>]
let RefactoringActivated = "refactoringactivated"

[<Literal>]
let Hints = "hints"

Expand Down
Loading
Loading