Skip to content

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

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

xperiandri wants to merge 3 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


✅ 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 and others added 3 commits September 14, 2026 21:46
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>
@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

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Design-Time
Affects-Design-Time: Modifies Visual Studio code executed while projects are open.

Generated by PR Tooling Safety Check · gpt56 1.8M ·

String.Equals(ident.idText, name, StringComparison.Ordinal)

// The shapes SyntaxTreeOps.pushUnaryArg accepts under `_.`, with the parameter as the head of the chain.
let rec tryChainRoot expr =

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.

Is there no way to expose this logic from F.C.Service as an API ? (and reuse/parametrize/abstract with existing logic there is inside)

This duplication has potential to drift if compiler's logic changes.

Comment on lines +96 to +101
| 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 (spanOf sourceText leftParen).End lambdaSpan.Start
&& isBlankBetween sourceText lambdaSpan.End (spanOf sourceText rightParen).Start

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.

Extract, e.g. to a well named AP where the AP will explain what this attempts to check.

@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) xsList.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: _.Pfun x -> x.P, _.A.Bfun x -> x.A.B, _.Xs[0]fun x -> x.Xs[0]
  • Shorthand as an argument gets parentheses: List.map _.P xsList.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

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

Labels

⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

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

2 participants