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 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>
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 29, 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 29, 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 29, 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 29, 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 29, 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 29, 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 29, 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 and others added 10 commits October 30, 2026 00:54
Ctrl+. on a tuple expression, pattern or annotated tuple type converts it
to the other kind and follows the value through the solution: annotations
of values, parameters, record fields and function results it flows
through, tuple patterns taking it apart, and the arguments and values
flowing into it. Uses that cannot be followed (fst, snd, generic
collections) are left for the compiler to report.

CreateWithCodeAndDependency now tells FCS about both files, so the second
file can be type-checked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adding the second document to a single-file solution left Find All
References unable to see either file, so a chain through a function's
call sites stopped at the first file. The synthetic project gives both
files to the checker the way AddReturnTypeTests and FindReferencesTests
set up theirs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The synthetic project's checker reads the other file from disk, so
checking the refactored document saw the old definition; a struct tuple
pattern happens to accept a reference tuple, which hid that.

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

The only argument of a member or constructor is its parameter list, not
a tuple, so it is no longer offered. A tuple that is a whole curried
argument of a function or member now converts the matching argument at
every call, and `struct` no longer runs into a name the parenthesis
follows (`f(a, b)`).

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>
…y and match aliases

A parenthesized tuple applied to a method, a constructor or a union case of
several fields is an argument list however the call is spaced. Results flow
through try/finally and try/with, a match clause that names the value passes
it on, and removing `struct` from a type keeps a comment before its
parenthesis. `struct` is kept apart from a name ending in any identifier
character the lexer accepts, combining marks included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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)`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delete the comments the code says by itself and name what they described, share the edit code of tuple expressions and patterns, share the use scan of the propagation engine and keep every function at three nested levels or fewer.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Move the test helpers into RefactorTestFramework, let them add the module M wrapper and run the two-file refactoring with its re-check as a new project, and assert both directions of a conversion in one helper.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replaying the branch onto main left some files short of the branch's own content; this restores it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feature/tuple-struct-refactoring branch from e32822e to c1cc431 Compare October 30, 2026 01:02

@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 c1cc431; LGTM

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