Skip to content

Add a refactoring between fun x -> x.Prop and _.Prop - #20535

Open
xperiandri wants to merge 11 commits into
dotnet:mainfrom
xperiandri:feature/dot-lambda-refactoring
Open

xperiandri wants to merge 11 commits into
dotnet:mainfrom
xperiandri:feature/dot-lambda-refactoring

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #16234

Offers Ctrl+. to turn a lambda that only reads a member chain of its parameter (fun x -> x.Name, fun x -> x.M(y), fun x -> x.Items[0]) into the _. shorthand, and a shorthand back into a fun lambda. It is a refactoring rather than a code fix because both forms are correct.

Not offered for lambdas with several or annotated parameters, bodies that do more than read the chain, lambdas applied directly, or inside quotations.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev

@xperiandri,

Caution

No release notes found for the changed paths (see table below).

Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format.

The following format is recommended for this repository:

`* . (PR #XXXXX)`

See examples in the files, listed in the table below or in th full documentation at https://fsharp.github.io/fsharp-compiler-docs/release-notes/About.html.

If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.

Change path Release notes path Description
`src/Compiler` docs/release-notes/.FSharp.Compiler.Service/11.0.200.md No release notes found or release notes format is not correct

✅ Found changes and release notes in following paths:

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md

@xperiandri
xperiandri force-pushed the feature/dot-lambda-refactoring branch from ee5d78c to 119ccbf Compare September 14, 2026 10:48
@xperiandri
xperiandri marked this pull request as ready for review September 14, 2026 19:40
@xperiandri
xperiandri requested a review from a team as a code owner September 14, 2026 19:40
@xperiandri xperiandri changed the title Refactoring to convert between 'fun x -> x.Prop' and the '_.Prop' shorthand Refactoring to convert between fun x -> x.Prop and the _.Prop shorthand Sep 14, 2026
@xperiandri
xperiandri force-pushed the feature/dot-lambda-refactoring branch from f1fdbc0 to 0bc4d75 Compare September 14, 2026 19:46
@github-actions github-actions Bot added the ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager label Sep 14, 2026
@github-actions

This comment has been minimized.

Comment thread vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs Outdated
Comment thread vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs Outdated

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in general, could you run e.g. 2-3 rounds of the "code compaction" skill from this repo which will check any shared patterns between added code and code which already exists in the compiler (like various Ops, helpers or other pieces of logic) - the skill covers some common patterns of logic reuse.

@xperiandri xperiandri changed the title Refactoring to convert between fun x -> x.Prop and the _.Prop shorthand Add a refactoring between fun x -> x.Prop and _.Prop Sep 15, 2026
@xperiandri

Copy link
Copy Markdown
Contributor Author

Covered cases

Converts

  • A lambda passed as an argument loses its parentheses: xs |> List.map (fun x -> x.Prop) → xs |> List.map _.Prop, List.map (fun x -> x.A.B) xs → List.map _.A.B xs
  • Member chains on the parameter: method call Seq.filter (fun x -> x.M(y)) → Seq.filter _.M(y), indexers x.Xs[0] and x.Xs.[0], generic method x.M<int>(), member of a call result x.M().P
  • Spaces inside the parentheses ( fun x -> x.P ) and a backticked parameter fun ``x`` -> ``x``.P both → _.P
  • A method argument keeps the call's parentheses: x.M(fun y -> y.P) → x.M(_.P); a bare fun x -> x.P → _.P; a lone (fun x -> x.P) → (_.P)
  • Multi-line lambdas: List.map (fun x -> with x.Prop) xs on the next line → List.map _.Prop xs; a binding body fun x -> with x.P on its own line → _.P
  • Shorthand to lambda: _.P → fun x -> x.P, _.A.B → fun x -> x.A.B, _.Xs[0] → fun x -> x.Xs[0]
  • Shorthand as an argument gets parentheses: List.map _.P xs → List.map (fun x -> x.P) xs; inside method parentheses they are not added: x.M(_.P) → x.M(fun x -> x.P)
  • The parameter name avoids names used in the body: xs |> List.map _.M(x) → xs |> List.map (fun x1 -> x1.M(x))
  • Nested shorthands convert one at a time: caret on the outer one of _.M(_.P) → fun x -> x.M(_.P), on the inner one → _.M(fun x -> x.P)
  • Round trip: List.map (fun x -> x.P) xs converted to shorthand and back gives the original text

Not offered

  • Several parameters or a non-plain parameter pattern: fun x y -> x.P, fun (x: T) -> x.P, fun (x) -> x.P
  • The body is more than a member chain: fun x -> (x.P), fun x -> f x.P, fun x -> x.M y, fun x -> x.P + 1
  • The parameter is used more than once: fun x -> x.M(x), fun x -> x.M(fun x -> x)
  • The chain doesn't start at the parameter, or there is no member access: fun x -> y.P, fun x -> x, fun x -> x[0], fun _ -> 1
  • A lambda applied directly: (fun x -> x.P) y
  • A lambda inside a quotation: <@ fun x -> x.P @>
  • Caret on neither a lambda nor a shorthand: 1 + 2

@T-Gro T-Gro added the vsintegration-only Changes only Visual Studio integration, plus optional docs, release notes, or tests label Sep 21, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Design-Time
Affects-Design-Time: Changes IDE integration executed during design-time.

Generated by PR Tooling Safety Check · gpt56 1.7M · ◷

@github-actions github-actions Bot added the ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure label Sep 28, 2026

@abonie abonie left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖🕵️ AI review — verify independently.

Comment thread vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs Outdated
Copilot AI balanced review requested due to automatic review settings September 29, 2026 17:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The refactoring can generate invalid SRTP code, discard comments, and appear in explicitly excluded contexts.

Review effort: Balanced
Findings: 2 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Adds bidirectional Visual Studio refactoring between explicit lambdas and F# _. shorthand, backed by shared compiler syntax-tree logic.

Changes:

  • Adds the refactoring, telemetry, resources, and localization.
  • Extracts reusable syntax-chain conversion operations.
  • Adds compiler-service and editor tests plus release notes.
File Description
vsintegration/​tests/​FSharp.Editor.Tests/​Refactors/​ConvertDotLambdaTests.fs Tests refactoring behavior.
vsintegration/​tests/​FSharp.Editor.Tests/​FSharp.Editor.Tests.fsproj Includes new editor tests.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.zh-Hant.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.zh-Hans.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.tr.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.ru.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.pt-BR.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.pl.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.ko.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.ja.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.it.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.fr.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.es.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.de.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.cs.xlf Adds localized resources.
vsintegration/​src/​FSharp.Editor/​Telemetry/​TelemetryReporter.fs Defines refactoring telemetry event.
vsintegration/​src/​FSharp.Editor/​Refactor/​ConvertDotLambda.fs Implements bidirectional conversion.
vsintegration/​src/​FSharp.Editor/​FSharp.Editor.resx Adds action titles.
vsintegration/​src/​FSharp.Editor/​FSharp.Editor.fsproj Includes the provider.
tests/​FSharp.Compiler.Service.Tests/​SyntaxTreeOpsTests.fs Tests syntax-chain operations.
tests/​FSharp.Compiler.Service.Tests/​FSharp.Compiler.Service.Tests.fsproj Includes syntax tests.
src/​Compiler/​SyntaxTree/​SyntaxTreeOps.fsi Declares chain conversion helper.
src/​Compiler/​SyntaxTree/​SyntaxTreeOps.fs Implements shared chain traversal.
docs/​release-notes/​.VisualStudio/​18.vNext.md Documents the refactoring.
docs/​release-notes/​.FSharp.Compiler.Service/​11.0.200.md Documents syntax-operation changes.

Comment thread vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs Outdated
Comment thread vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs
Comment thread vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs
@xperiandri
xperiandri force-pushed the feature/dot-lambda-refactoring branch from 3d2645a to 7d64d6e Compare October 2, 2026 16:03

@abonie abonie left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖🕵️ AI review — verify independently.

Comment thread vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs

@abonie abonie left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖🕵️ AI review — verify independently.

Comment thread vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs
@xperiandri
xperiandri force-pushed the feature/dot-lambda-refactoring branch from 7d64d6e to 7b928dc Compare October 8, 2026 01:11

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 🕵️ Re-reviewed all recent changes as of 7b928dc; 1 findings

Comment thread vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs
@xperiandri
xperiandri force-pushed the feature/dot-lambda-refactoring branch from 7b928dc to 92978a6 Compare October 10, 2026 22:28

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff is still way too big - apply the NoBloat instructions and CodeCompaction skill.

Especially the massive code comments would benefit not just with drastic reduction, but ideally with well named functions/arguments/bindings/types making the intention clear.

If a well named piece of code just tells what the comment says - delete the comment.

  • Delete the 9 non-license implementation comment lines. Rename the internal helpers so dot-lambda, generated-argument, offside-line, and sole-parenthesis intent is in code rather than XML prose.
  • Delete the FCS release note: the shared internal syntax walker and its inverse are not user-visible FCS behavior. Keep a compact Visual Studio note for the refactoring action.
  • Fold the standalone success Facts and multiline cases into the existing parameterized conversion tables.

@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Oct 14, 2026
xperiandri and others added 7 commits October 15, 2026 01:38
A lambda whose only use of its parameter is the head of a member, method, indexer or type-application chain converts to the F# 8 shorthand, and a shorthand converts back to a 'fun' lambda with a parameter name the body does not use. The accepted chains mirror SyntaxTreeOps.pushUnaryArg, so the shorthand produced always type checks like the lambda it replaces. A lambda spanning multiple lines still converts as long as its body is such a chain; the result collapses onto the line the '_.' replaces 'fun' on.

Fixes dotnet#16234

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e of its section

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pushUnaryArg now finds the head of the atomic chain (atomic applications,
member and index lookups, type applications) through a zipper that also
rebuilds the chain, and tryPopUnaryArg uses the same zipper to split the
root identifier off a lambda body. The editor refactoring between
`fun x -> x.P` and `_.P` can then accept exactly the bodies the compiler
accepts instead of keeping its own copy of the rule.

pushUnaryArg keeps its recovery branches and reports FS3584 on the same
node as before. Both functions stay internal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment targets as uses

The refactoring drops its own chain walker for SyntaxTreeOps.tryPopUnaryArg,
the inverse of what the compiler does to a `_.` body, so the two cannot drift.
The check that the dot follows the root without a space goes with it:
`x .P` is the same chain and `_ .P` is a valid shorthand.

`fun x -> x.M(x.P <- 1)` was offered as `_.M(x.P <- 1)`, where `x` then names
an outer value: an assignment to `x`, `x.P` or `x.Xs[0]` keeps its target as
a long identifier rather than an expression, so the use was not counted.

The condition for dropping the parentheses around the lambda argument is now
the ParenthesizedLambdaArgument active pattern, both folds over the body share
one identifier collector, and spans come from RoslynHelpers directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reak the layout

Lambda to shorthand is no longer offered when
- a comment stands in the `fun x -> x` text that `_` replaces;
- the body uses a name such as `_arg1`: the compiler names the parameter of
  `_.` that way, so the use would bind to it instead of the outer value;
- the lambda is applied through more than one pair of parentheses,
  `((fun x -> x.P)) y`, like the single pair that was already excluded.

Shorthand to lambda is no longer offered
- inside a quotation, where the name of the lambda variable is observable;
- when the chain itself continues on a later line: `_.Trim()` followed by
  `.Length` on the next line is valid after `_` but offside as the body of a
  lambda that starts further right. Arguments may still span lines.

It replaces only the `_` token, so what stands between `_` and the dot, a
space or a comment, stays.

The tests now parse every converted document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bullet was listed twice, once without the pull request link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feature/dot-lambda-refactoring branch from 4be0d7d to e18b128 Compare October 15, 2026 00:46
xperiandri and others added 3 commits October 15, 2026 03:09
Delete the nine implementation comment lines and let the helper names say
what the comments said:

- splitDotLambdaChain: the head of a dot-lambda body and the function that
  rebuilds the chain around another head (was atomicChainHead)
- canBeCapturedByGeneratedArgument: a name the generated parameter of _. would
  capture (was isGeneratedArgumentName)
- lastOffsideSensitiveLine: the last line of a chain outside its argument
  brackets (was lastLineOutsideArguments)
- SoleParenthesizedLambdaArgument: the parentheses that hold only the lambda
  (was ParenthesizedLambdaArgument)

SyntaxTreeOps is an internal module, so pushUnaryArg and tryPopUnaryArg lose
their doc comments as well.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The four success Facts and the multi-line cases become rows of the two
conversion tables; the rows that were Facts keep their names as display names.
The shorthand-to-lambda table takes the caret marker as its first argument,
which also absorbs the table that needed another marker, and a binding that
starts with "let " spells out its own definition.

Test methods go from 8 to 3 and the cases stay at 61. SyntaxTreeOpsTests
interpolates instead of calling sprintf.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…oring

The shared internal syntax walker and its inverse are not user-visible FCS
behavior, so the FCS note is deleted. The Visual Studio note names the
refactoring action and nothing else.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@xperiandri

Copy link
Copy Markdown
Contributor Author

Pushed e47c07c (names instead of the nine implementation comments), 12d14fd (the tests folded into the tables) and b5496e8 (release notes): the PR now adds one comment line, the license header, down from 10 lines and 1037 characters, and the added lines in total go from 604 to 548. The helpers carry the intent in their names (splitDotLambdaChain, canBeCapturedByGeneratedArgument, lastOffsideSensitiveLine, SoleParenthesizedLambdaArgument), the FCS release note is deleted, and the Visual Studio note is a single short line. The four success Facts and the multi-line cases are rows of the two conversion tables (the former Fact names are kept as display names), so ConvertDotLambdaTests goes from 8 test methods to 3 with the same 61 cases and from 171 to 125 lines; the round trip Fact is now the pair of rows List.map (fun x -> x.P) xs and List.map _.P xs. I left behaviour, the 31-row No action table and the generated xlf files alone; ConvertDotLambdaTests (61), SyntaxTreeOpsTests (14), Language.DotLambdaTests (39) and the AddReturnTypeTests control (22) pass before and after, and FSharp.Editor also builds with Optimize=true.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager vsintegration-only Changes only Visual Studio integration, plus optional docs, release notes, or tests

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

Refactoring for _.Property / _.MethodCall() / _.IndexerAccess[idx] shorthand for accessor functions

4 participants