Skip to content

Refactorings to extract a selected expression into a local function, a module function or a private member - #20538

Open
xperiandri wants to merge 9 commits into
dotnet:mainfrom
xperiandri:feature/extract-function-refactoring
Open

xperiandri wants to merge 9 commits into
dotnet:mainfrom
xperiandri:feature/extract-function-refactoring

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Description

Stacked on #20537: this branch contains its commits; the extract-function commits on top of them belong to this PR.

Adds three refactorings (Ctrl+. on a selection) that extract the selected expression into a local function, a module-level function, or a private member — building on the selection/placement logic from #20537. As with Extract to let binding, a caret alone inside the fun <params> -> header of a parenthesized lambda extracts the whole lambda.

Values the selection reads from its enclosing function, member or lambda (but doesn't itself define) become parameters, in the order they're first used; module-level values, other members and this/base stay where they are and aren't parameterized. A member with a named self identifier (member this.M, not member _.M) additionally gets "Extract to private member", called on that identifier; a selection using base offers only that variant (a module or local function can't reach base). Parameter types default to fully annotated (FullType.FormatWithConstraints), since the extracted binding is its own generalization point and would otherwise default surprising types (e.g. int from x * y before the call site is seen) or fail outright on a plain member access; a new Parameter types in Extract to function option under Tools ▸ Options ▸ F# ▸ Code Fixes lets that be relaxed to "only where inference needs it" or "never". The refactoring isn't offered at all when the selection assigns to a captured mutable (the new parameter would only get a copy) or reads a byref/byref-like value from outside the selection.

Covered cases

Offered

  • Values the selection uses from the enclosing declaration become parameters of a local function: printfn "%d" (w * h + 1)let extractedFunction (w: int) (h: int) = w * h + 1 + printfn "%d" (extractedFunction w h)
  • The module function is inserted before the declaration that uses it: let private extractedFunction (w: int) (h: int) = w * h + 1 above let area …
  • Selecting (w * h + 1) with its parentheses gives the same result, and the call stays parenthesized
  • A selection that uses no outside values becomes a function of unit: 1 + 2let extractedFunction () = 1 + 2 + extractedFunction ()
  • A selection that uses this becomes a private member after the enclosing member: l * this.Rate in List.sumBy (fun l -> l * this.Rate)this.ExtractedMethod(l) + member private this.ExtractedMethod(l: int) = l * this.Rate
  • The parameter-annotation option: Alwayslet extractedFunction (s: string) (n: int) =, WhenNeeded(s: string) n (only the receiver of s.ToUpper() is annotated), Nevers n
  • Which actions appear: a plain selection (w * h + 1) offers "Extract to local function" and "Extract to module function"; a selection using base (base.ToString() + "!") offers only "Extract to private member"
  • A caret in the header of a parenthesized lambda (x -> in (fun x -> x + n)) offers the same actions, with the same results, as selecting the whole (fun x -> x + n)

Not offered

  • With no selection, a caret in the lambda body: x + n in (fun x -> x + n)
  • The selection assigns to a captured mutable local: for i in 1 .. 3 do n <- n + i
  • The selection reads a byref parameter: x + 1 with x: byref<int>

Relates to #14449 (extract method).

Checklist

  • Test cases added
  • Performance benchmarks added in case of performance changes (not applicable)
  • Release notes entry updated

🤖 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 and others added 8 commits September 14, 2026 16:36
…g or a literal

The selected expression is bound to a new name in front of the statement that uses it: above a statement that starts its line, or, when the body of a binding, match clause, branch or lambda shares the line with its keyword, on new lines under that keyword. A constant inside a module-level declaration can instead become a [<Literal>] in front of that declaration. The shared selection and placement logic lives in Refactor/RefactoringHelpers.fs, and the refactoring test framework gains helpers that run a provider on a selected span.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion, a module function or a private member

The values the selection reads from the enclosing member or module-level declaration become parameters in the order they are first used; module values, members and 'this' stay where they are. A member that uses 'this' or 'base' can extract into a private member called on its self identifier. Parameter type annotations follow a new Code Fixes option: always, only for receivers of member and indexer accesses and operator operands, or never. A selection that assigns to a captured mutable local, or captures a byref-like value, is not offered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C# offers Introduce Constant without a selection. With a caret alone only the
literal is offered; a let binding still needs a selection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A caret between 'fun' and the end of '->' acts as a selection of the
parenthesized lambda; in the body a caret offers nothing new.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ader

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feature/extract-function-refactoring branch from 9689259 to dc118df Compare September 14, 2026 14:36
…e of its section

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@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
@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 ·

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.

1 participant