Skip to content

Add a refactoring between reference and struct tuples - #20548

Open
xperiandri wants to merge 10 commits into
dotnet:mainfrom
xperiandri:feature/tuple-struct-refactoring
Open

xperiandri wants to merge 10 commits into
dotnet:mainfrom
xperiandri:feature/tuple-struct-refactoring

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Offers Ctrl+. on a tuple expression, pattern or annotated tuple type to switch it between a reference and a struct tuple. The value is followed through annotations, patterns, record fields, function results and call arguments across the solution, so the result still type-checks. What can't be followed (fst, snd, generic collections) is left for the compiler to report.

Not offered for method argument lists, the only argument of a member (that is its parameter list), inside quotations, or when the file has a paired .fsi.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ Release notes checked


✅ 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/tuple-struct-refactoring branch from fb6c03d to 512e8cb Compare September 14, 2026 14:36
@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

This comment has been minimized.

@xperiandri xperiandri changed the title Refactoring between reference and struct tuples Add a refactoring between reference and struct tuples Sep 15, 2026
@xperiandri

Copy link
Copy Markdown
Contributor Author

Covered cases

Converts

  • A tuple used nowhere else converts on its own, and back: printfn "%A" (1, 2) → printfn "%A" struct (1, 2); a type argument list<int * int> → list<struct (int * int)>
  • A value converts together with the tuple patterns that take it apart and the annotations it flows into, both ways: let pair = (1, 2) + let (a, b) = pair + let copy: int * int = pair → let pair = struct (1, 2) + let struct (a, b) = pair + let copy: struct (int * int) = pair
  • A parameter annotation converts the patterns on the parameter and every argument passed to it, both literals and values, both ways: let sum (p: int * int) + let (a, b) = p + let pair = (3, 4) + sum pair + sum (1, 2) → let sum (p: struct (int * int)) + let struct (a, b) = p + let pair = struct (3, 4) + sum pair + sum struct (1, 2)
  • A parameter that is not first converts the matching argument of each call, both ways: curried let scale (factor: int) (p: int * int) + scale 2 (1, 2) → scale 2 struct (1, 2); a tuple inside a tupled parameter list let area (p: int * int, factor: int) + area ((2, 3), 4) → area (struct (2, 3), 4)
  • The tuple argument of a curried function or member converts with its calls, both ways: let add (a, b) c + add (1, 2) 3 → let add struct (a, b) c + add struct (1, 2) 3; member _.Add (a: int, b: int) (c: int) + Calc().Add (1, 2) 3 → member _.Add struct (a: int, b: int) (c: int) + Calc().Add struct (1, 2) 3
  • A space is added between struct and a name the parenthesis directly follows: let add(a, b) c → let add struct (a, b) c
  • A return type converts the result and the patterns that take it apart, both ways: let origin () : int * int = (0, 0) + let (x, y) = origin () → let origin () : struct (int * int) = struct (0, 0) + let struct (x, y) = origin ()
  • A record field converts only its own values and the patterns on it, both ways: { Start: int * int; Finish: int * int } + { Start = (0, 0); Finish = (1, 1) } + let (sx, sy) = line.Start → only Start becomes struct (int * int), struct (0, 0) and let struct (sx, sy); Finish is not changed
  • A value declared in a second file converts in that file: the caret on let (a, b) = A.pair in module B also rewrites let pair = (1, 2) in A to struct (1, 2); the result is checked as a new project and has no errors
  • A use the refactoring cannot follow is left for the compiler to report: let pair = (1, 2) + let first = fst pair → let pair = struct (1, 2), fst pair is unchanged and gives exactly one error on its line
  • The action titles name the target kind: Convert to struct tuple and Convert to reference tuple
  • Every result is re-type-checked and asserted to have no errors

Not offered

  • The caret is not on a tuple: let x = 1
  • The argument list of a .NET method call: System.Math.Max(1, 2)
  • A tuple inside a quotation: <@ (1, 2) @>
  • A member's parameter list, here with an optional parameter: member _.Greet(name: string, ?greeting: string)
  • A struct tuple that is a member's only parameter: member _.Add struct (a: int, b: int) = a + b
  • A constructor's parameter list: new(x: int, y: int, z: int) = Point(x + z, y)

@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.

🤖 🕵️ AI review — verify independently.

/// Whether the tuple is the argument list of a method, constructor or union case call rather than a tuple value.
let isArgumentList (tuple: SynExpr) (path: SyntaxVisitorPath) =
match path with
| SyntaxNode.SynExpr(SynExpr.Paren(expr = inner) as paren) :: SyntaxNode.SynExpr(SynExpr.App(flag = ExprAtomicFlag.Atomic; argExpr = arg) | SynExpr.New(

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.

🤖 🕵️ [P2] Converting 1, 2 passes one struct-tuple argument to Math.Max — valid method calls with whitespace before ( stop compiling.

module M
let biggest = System.Math.Max (1, 2)
// Convert "1, 2" to a struct tuple.
// Result: System.Math.Max struct (1, 2)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4cfee1f: a parenthesized tuple is an argument list when the applied name resolves to a method or constructor with one argument list (or a union case of several fields), whatever the spacing. Test: No action with System.Math.Max (1, 2) and Calc().Add (1, 2).

this.Retarget source result (SyntaxNode.SynMatchClause clause :: childPath)
| SynExpr.Sequential(expr2 = last) -> this.Retarget source last childPath
| SynExpr.LetOrUse letOrUse -> this.Retarget source letOrUse.Body childPath
| _ -> ()

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.

🤖 🕵️ [P2] Converting (x, y) leaves make returning a reference tuple through try/finally — the converted pattern fails to typecheck.

module M
let make () =
    try (1, 2)
    finally ()
let (x, y) = make ()
// Convert "x, y" to a struct tuple.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4cfee1f: results now flow into and out of the try body of try/finally, and the body and handlers of try/with. Test: Result flowing out of try converts every branch of it.


if isWholeAnnotation || isGenericArgument sourceText keyword.Start span.End then
[
TextChange(TextSpan(keyword.Start, keyword.Length + 1), "")

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.

🤖 🕵️ [P2] Converting this type to a reference tuple deletes the comment's opening ( — the result is invalid syntax.

module M
type R = { Pair: struct (* note *) (int * int) }
// Convert "int * int" to a reference tuple.
// Result: type R = { Pair: * note *) (int * int }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4cfee1f: only struct and the blanks after it are removed, and the tuple's ( is found past any comments. Test: Comment between struct and its parenthesis is kept.

/// `struct ` to insert in front of the `(` at the position, after a space when it would run into a name: `f(a, b)`.
let private structAt (sourceText: SourceText) (position: int) =
match if position > 0 then sourceText[position - 1] else ' ' with
| c when Char.IsLetterOrDigit c || c = '_' || c = '\'' || c = '`' || c = ')' || c = ']' -> " struct "

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.

🤖 🕵️ [P2] Converting (a,b) appends struct to the function identifier ending in U+0301 — its callers fail with an undefined-name error.

module M
let f́(a,b) = a+b
let x = f́ (1,2)
// Convert "a,b" to a struct tuple.
// Result: let f́struct (a,b) = a+b

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4cfee1f: the check for a preceding name now uses the lexer's identifier character categories (letters, Nl, Nd, Pc, Mn, Mc, Cf, '). Test: Struct keyword is separated from a name ending in a combining mark.

@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Sep 18, 2026
@T-Gro
T-Gro self-requested a review September 18, 2026 15:03
@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.

@xperiandri

Copy link
Copy Markdown
Contributor Author

Compaction is pushed as 2a032ce (sources) and e32822e (tests). The new comment lines in the diff go from 74 to 5 (6558 to 597 chars): three license headers, one note on a parser quirk and one on the synthetic-project checker; the rest is named instead, for example tryExprChanges and tryPatChanges now share tupleChanges, callsMethod reuses tryCallee, and the three scans over the uses of a symbol are one ForEachUse.

ProcessParameter is now two levels deep (7 by my count of every match/if/for/lambda level, 6 by yours), and no function in TupleConversion.fs, TuplePropagation.fs or ConvertTuple.fs nests deeper than three. The cross-file fixture, its re-check as a new project and the module M wrapper are helpers in RefactorTestFramework.fs (refactoredAcrossFiles, inModuleM); ConvertTupleTests.fs shrinks from 807 to 599 lines with the same 20 test methods and 52 cases, and the added lines of the PR go from 2336 to 2089.

Behaviour is unchanged: ConvertTupleTests and AddReturnTypeTests pass 74 of 74 before and after, and FSharp.Editor also builds with -p:Optimize=true without warnings. I left isIdentifierCharBefore as it is, because PrettyNaming.IsIdentifierPartCharacter knows neither the lexer's format characters nor surrogate pairs.

@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.

🤖🕵️ AI review — verify independently.

| SynPat.Named(ident = SynIdent(ident, _))
| SynPat.LongIdent(longDotId = SynLongIdent(id = [ ident ]); argPats = SynArgPats.Pats []) -> this.EnqueueValue source ident
| SynPat.Tuple _ when not (isParameterList pat path) -> this.AddOrFail source (tryPatChanges source.Text toStruct pat path)
| _ -> ()

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.

🤖🕵️ Conversion through an as pattern updates the value but not both patterns; FS0001 follows.

let pair = (1, 2)
let ((a, b) as alias) = pair

xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
Ctrl+. on a tuple expression, pattern or annotated tuple type converts it
to the other kind and follows the value through the solution. From
dotnet#20548 (release note left out: 18.vNext.md has local edits).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
From dotnet#20548: Find All References did not see the second
document added to a single-file test solution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
From dotnet#20548.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
…ents with their calls

From dotnet#20548 and dotnet#20549.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
…et#20549) up to their pull request heads

Their calls to SymbolHelpers.getSymbolUses ask for RelatedSymbolUseKind.AllInCode, as the XML doc rename work on this branch requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
…annotated patterns

The conversion now also follows a tuple through
- a function in parentheses, `(sum) (1, 2)`;
- a name bound to a lambda, its parameters and its result;
- a function alias and a partial application, `let alias = make`;
- a named argument, `M(p = pair)`;
- an annotated tuple pattern, `((a, b): int * int)`, from the pattern, the
  annotation or the value;
- a value whose binding head is in parentheses, `let (pair: int * int) = (1, 2)`.

It is no longer offered where the result cannot compile or nothing would change:
- on the annotation of a member's only argument, which is its parameter list;
- on the argument of a union case whose only field is declared a tuple;
- on the argument of a method in parentheses, `(c.Add) (1, 2)`;
- on a tuple over several lines without parentheses, which cannot change in place.

`struct` is kept apart from type arguments the parenthesis follows: `f<int> struct (1, 2)`.

This is b7bc303 of dotnet#20548 as dotnet#20549 merged it (3abaa70) into the engine
both kinds share: `StructConversion.fs` and `StructPropagation.fs` take what
went into `TupleConversion.fs` and `TuplePropagation.fs` there. A member's
parameter list is told by the kind, so the annotation of a member's only
argument stays convertible for an anonymous record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
Port of the dotnet#20549 compaction (feature/anonymous-record-struct-refactoring
4c7066a..5be006c) to all-fixes. The range includes the pull request's
merge of the compacted tuple refactoring (dotnet#20548), so this one commit covers
both pull requests: all-fixes has the generalized engine of dotnet#20549
(StructPropagation.fs, TupleConversion.fs as a kind of that engine) and no
TuplePropagation.fs, and the merge kept that layout.

- dotnet#20548: Compact the tuple refactoring sources
- dotnet#20548: Share the cross-file refactoring test setup
- dotnet#20549: Compact the shared struct conversion sources
- dotnet#20549: Share the cross-file test setup of the conversion tests

StructPropagation.fs reads the related uses once now (ForEachUse) and keeps the
RelatedSymbolUseKind.AllInCode argument that all-fixes needs there.

RefactorTestFramework.fs merges the pull request's helpers with those of the
Extract suites that the earlier commits of this series put there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
Ctrl+. on a tuple expression, pattern or annotated tuple type converts it
to the other kind and follows the value through the solution. From
dotnet#20548 (release note left out: 18.vNext.md has local edits).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
From dotnet#20548: Find All References did not see the second
document added to a single-file test solution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
From dotnet#20548.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
…ents with their calls

From dotnet#20548 and dotnet#20549.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
…et#20549) up to their pull request heads

Their calls to SymbolHelpers.getSymbolUses ask for RelatedSymbolUseKind.AllInCode, as the XML doc rename work on this branch requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
…annotated patterns

The conversion now also follows a tuple through
- a function in parentheses, `(sum) (1, 2)`;
- a name bound to a lambda, its parameters and its result;
- a function alias and a partial application, `let alias = make`;
- a named argument, `M(p = pair)`;
- an annotated tuple pattern, `((a, b): int * int)`, from the pattern, the
  annotation or the value;
- a value whose binding head is in parentheses, `let (pair: int * int) = (1, 2)`.

It is no longer offered where the result cannot compile or nothing would change:
- on the annotation of a member's only argument, which is its parameter list;
- on the argument of a union case whose only field is declared a tuple;
- on the argument of a method in parentheses, `(c.Add) (1, 2)`;
- on a tuple over several lines without parentheses, which cannot change in place.

`struct` is kept apart from type arguments the parenthesis follows: `f<int> struct (1, 2)`.

This is b7bc303 of dotnet#20548 as dotnet#20549 merged it (3abaa70) into the engine
both kinds share: `StructConversion.fs` and `StructPropagation.fs` take what
went into `TupleConversion.fs` and `TuplePropagation.fs` there. A member's
parameter list is told by the kind, so the annotation of a member's only
argument stays convertible for an anonymous record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 19, 2026
Port of the dotnet#20549 compaction (feature/anonymous-record-struct-refactoring
4c7066a..5be006c) to all-fixes. The range includes the pull request's
merge of the compacted tuple refactoring (dotnet#20548), so this one commit covers
both pull requests: all-fixes has the generalized engine of dotnet#20549
(StructPropagation.fs, TupleConversion.fs as a kind of that engine) and no
TuplePropagation.fs, and the merge kept that layout.

- dotnet#20548: Compact the tuple refactoring sources
- dotnet#20548: Share the cross-file refactoring test setup
- dotnet#20549: Compact the shared struct conversion sources
- dotnet#20549: Share the cross-file test setup of the conversion tests

StructPropagation.fs reads the related uses once now (ForEachUse) and keeps the
RelatedSymbolUseKind.AllInCode argument that all-fixes needs there.

RefactorTestFramework.fs merges the pull request's helpers with those of the
Extract suites that the earlier commits of this series put there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
Ctrl+. on a tuple expression, pattern or annotated tuple type converts it
to the other kind and follows the value through the solution. From
dotnet#20548 (release note left out: 18.vNext.md has local edits).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
From dotnet#20548: Find All References did not see the second
document added to a single-file test solution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
From dotnet#20548.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
…ents with their calls

From dotnet#20548 and dotnet#20549.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
…et#20549) up to their pull request heads

Their calls to SymbolHelpers.getSymbolUses ask for RelatedSymbolUseKind.AllInCode, as the XML doc rename work on this branch requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
…annotated patterns

The conversion now also follows a tuple through
- a function in parentheses, `(sum) (1, 2)`;
- a name bound to a lambda, its parameters and its result;
- a function alias and a partial application, `let alias = make`;
- a named argument, `M(p = pair)`;
- an annotated tuple pattern, `((a, b): int * int)`, from the pattern, the
  annotation or the value;
- a value whose binding head is in parentheses, `let (pair: int * int) = (1, 2)`.

It is no longer offered where the result cannot compile or nothing would change:
- on the annotation of a member's only argument, which is its parameter list;
- on the argument of a union case whose only field is declared a tuple;
- on the argument of a method in parentheses, `(c.Add) (1, 2)`;
- on a tuple over several lines without parentheses, which cannot change in place.

`struct` is kept apart from type arguments the parenthesis follows: `f<int> struct (1, 2)`.

This is b7bc303 of dotnet#20548 as dotnet#20549 merged it (3abaa70) into the engine
both kinds share: `StructConversion.fs` and `StructPropagation.fs` take what
went into `TupleConversion.fs` and `TuplePropagation.fs` there. A member's
parameter list is told by the kind, so the annotation of a member's only
argument stays convertible for an anonymous record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
Port of the dotnet#20549 compaction (feature/anonymous-record-struct-refactoring
4c7066a..5be006c) to all-fixes. The range includes the pull request's
merge of the compacted tuple refactoring (dotnet#20548), so this one commit covers
both pull requests: all-fixes has the generalized engine of dotnet#20549
(StructPropagation.fs, TupleConversion.fs as a kind of that engine) and no
TuplePropagation.fs, and the merge kept that layout.

- dotnet#20548: Compact the tuple refactoring sources
- dotnet#20548: Share the cross-file refactoring test setup
- dotnet#20549: Compact the shared struct conversion sources
- dotnet#20549: Share the cross-file test setup of the conversion tests

StructPropagation.fs reads the related uses once now (ForEachUse) and keeps the
RelatedSymbolUseKind.AllInCode argument that all-fixes needs there.

RefactorTestFramework.fs merges the pull request's helpers with those of the
Extract suites that the earlier commits of this series put there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
Ctrl+. on a tuple expression, pattern or annotated tuple type converts it
to the other kind and follows the value through the solution. From
dotnet#20548 (release note left out: 18.vNext.md has local edits).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
From dotnet#20548: Find All References did not see the second
document added to a single-file test solution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
From dotnet#20548.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
…ents with their calls

From dotnet#20548 and dotnet#20549.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
…et#20549) up to their pull request heads

Their calls to SymbolHelpers.getSymbolUses ask for RelatedSymbolUseKind.AllInCode, as the XML doc rename work on this branch requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
…annotated patterns

The conversion now also follows a tuple through
- a function in parentheses, `(sum) (1, 2)`;
- a name bound to a lambda, its parameters and its result;
- a function alias and a partial application, `let alias = make`;
- a named argument, `M(p = pair)`;
- an annotated tuple pattern, `((a, b): int * int)`, from the pattern, the
  annotation or the value;
- a value whose binding head is in parentheses, `let (pair: int * int) = (1, 2)`.

It is no longer offered where the result cannot compile or nothing would change:
- on the annotation of a member's only argument, which is its parameter list;
- on the argument of a union case whose only field is declared a tuple;
- on the argument of a method in parentheses, `(c.Add) (1, 2)`;
- on a tuple over several lines without parentheses, which cannot change in place.

`struct` is kept apart from type arguments the parenthesis follows: `f<int> struct (1, 2)`.

This is b7bc303 of dotnet#20548 as dotnet#20549 merged it (3abaa70) into the engine
both kinds share: `StructConversion.fs` and `StructPropagation.fs` take what
went into `TupleConversion.fs` and `TuplePropagation.fs` there. A member's
parameter list is told by the kind, so the annotation of a member's only
argument stays convertible for an anonymous record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 23, 2026
Port of the dotnet#20549 compaction (feature/anonymous-record-struct-refactoring
4c7066a..5be006c) to all-fixes. The range includes the pull request's
merge of the compacted tuple refactoring (dotnet#20548), so this one commit covers
both pull requests: all-fixes has the generalized engine of dotnet#20549
(StructPropagation.fs, TupleConversion.fs as a kind of that engine) and no
TuplePropagation.fs, and the merge kept that layout.

- dotnet#20548: Compact the tuple refactoring sources
- dotnet#20548: Share the cross-file refactoring test setup
- dotnet#20549: Compact the shared struct conversion sources
- dotnet#20549: Share the cross-file test setup of the conversion tests

StructPropagation.fs reads the related uses once now (ForEachUse) and keeps the
RelatedSymbolUseKind.AllInCode argument that all-fixes needs there.

RefactorTestFramework.fs merges the pull request's helpers with those of the
Extract suites that the earlier commits of this series put there.

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

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 AI-reviewed PR reviewed by AI review council 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.

4 participants