[mixed object and collection initializers] Pass 3a/3 of IDE0017+IDE0028 unification: merged walk#83761
Draft
CyrusNajmabadi wants to merge 119 commits into
Conversation
added 30 commits
April 22, 2026 11:26
…nitializers Accept any compound assignment operator (and `??=`) after the target of a named member initializer (`Prop += 1`) or a dictionary member initializer (`[key] += value`), producing the matching `AssignmentExpressionSyntax` kind instead of unconditionally `SimpleAssignmentExpression`. The object vs collection initializer classifier now recognizes any assignment whose left is an `IdentifierName` or `ImplicitElementAccess`, so a brace list containing only compound members still classifies as an object initializer. This makes the parser well-formed for the compound-assignment-in- initializer feature (dotnet/csharplang#9896). Binding-time legality checks (feature availability, target kind, duplicate rules, event-only `+=`/`-=`, nested `{ ... }` rejection) are unchanged and still apply on top of the new trees.
The lexer splits `>>=` and `>>>=` into multiple `>` / `>=` tokens so that
nested generic argument lists stay parseable. The expression parser
reconstructs the single token on demand (`EatExpressionOperatorToken`).
The member initializer operator path I added in the previous commit
missed that detail, so `new Foo { [0] >>= 1 }` and `new Foo { [0] >>>= 1 }`
emitted a spurious `'=' expected` diagnostic.
Factor the adjacency check into `IsSplitRightShiftAssignmentAt`, use it
in both `IsNamedMemberInitializer` (to trigger the named-member path for
`Prop >>= 1`) and `EatMemberInitializerOperatorToken` (to reconstruct
the merged token), and delegate to the expression parser's existing
`EatExpressionOperatorToken` for the actual consumption.
`CompoundAssignmentInitializerParsingTests` covers the parser changes for dotnet/csharplang#9896: * All 11 compound operators (and `??=`, which the parser accepts as a permissive shape for the binder to reject) on named and indexer member initializers in object initializer and `with` expression contexts. * Parses cleanly at every language version from C# 1 through Preview with no parse diagnostics. * Object-vs-collection classifier: compound members with an identifier or implicit element access target classify as object initializer; compound members with other shapes (e.g. `a.b += 1`) do not. * Missing RHS, trailing comma, generic RHS expression, `ref` on RHS, nested `{ ... }` on RHS (permissive parse). * Colon recovery for the simple-assignment form remains intact. Link `ParsingTests.cs` and `SyntaxExtensions.cs` from the Syntax test project into the CSharp15 test project so the usual `UsingTree`, `UsingExpression`, `N`, `M`, and `EOF` helpers are available here. Update the baseline in `RefFieldParsingTests.ObjectInitializer_CompoundAssignment` to match the new classification (the brace list now classifies as `ObjectInitializerExpression` because any compound assignment with an identifier target counts as object-initializer evidence) and to drop the parser-time `ref`-on-RHS diagnostic, which is now the binder's responsibility.
The assignment kind is derivable from the token's kind via SyntaxFacts.GetAssignmentExpression, including the recovery paths where the returned token is an EqualsToken. Simplify the helper to return a single SyntaxToken and have each caller compute the kind at the SyntaxFactory.AssignmentExpression call site.
Drops the hand-maintained AllLanguageVersions TheoryData in favor of [CombinatorialData] on the LanguageVersion parameter, which iterates every enum value (including the Default/Latest/LatestMajor aliases). Also fixes a stray cref in the EatMemberInitializerOperatorToken doc comment.
Converts every single-op test in CompoundAssignmentInitializerParsingTests to a [Theory] over the existing CompoundOperators member data, so each shape now gets exercised with all 11 compound operators plus ??=. MissingRightHandSide computes the diagnostic column from the operator length so <<= / >>= / >>>= / ??= trees all line up. Test count grows from 94 to 226; runtime is still negligible since these are pure parser-tree shape checks.
…ression
Implements the binding side of the compound-assignment-in-initializer-and-with
feature (csharplang#9896), building on the parser support already landed on
this branch.
* New MessageID.IDS_FeatureCompoundAssignmentInInitializer at LanguageVersion
.Preview, checked on the operator token in BindInitializerMemberAssignment.
* BindInitializerMemberAssignment grows an additional arm that covers all 11
compound_assignment_operator kinds. ??= is deliberately not included; the
parser accepted it for resilience and the existing default fallthrough
reports CS0747.
* BindObjectInitializerMember/BindObjectInitializerMemberCommon gain an
overload that takes an explicit BindValueKind (so the compound path can
request BindValueKind.CompoundAssignment) and exposes the unwrapped member
access through an `out BoundExpression rawAccess` parameter.
* BindCompoundAssignment is split: a new internal BindCompoundAssignmentCore
runs the operator-resolution body against pre-bound left/right. The
ordinary expression wrapper still binds them up front.
* Event targets with `+=`/`-=` are detected via `rawAccess is BoundEventAccess`
and dispatched directly to BindEventAssignment (which produces a
BoundEventAssignmentOperator). Event targets with any other compound op
fall through to BindCompoundAssignmentCore, which reports CS0019.
* ReportDuplicateObjectMemberInitializers is rewritten as a per-name
None/SeenEquals/SeenCompound state machine to enforce the spec rule "at
most one `=`, `=` must come before any compound, compound is unrestricted"
for field/property targets. Event and indexer targets stay unrestricted.
No lowering changes yet; this commit makes `new T { Prop += 1 }` and
`r with { Value -= 1 }` bind to the expected BoundCompoundAssignmentOperator /
BoundEventAssignmentOperator shapes. Phase 3 will teach the object-initializer
lowerer to emit IL for the new bound-tree shapes.
…itializers Pass the raw member access (BoundPropertyAccess / BoundFieldAccess / BoundEventAccess / BoundIndexerAccess / BoundDynamicObjectInitializerMember / ...) to BindCompoundAssignmentCore instead of the BoundObjectInitializerMember wrapper. CheckValueKind has no case for BoundObjectInitializerMember, so shouldTryUserDefinedInstanceOperator was incorrectly returning false and skipping user-defined `operator +=` resolution for any type that relied on the in-place form. The wrapper exists to drive simple-assignment lowering; for compound we rely on the outer BoundObjectInitializerExpression.Placeholder being registered for substitution at lowering time, which lets the raw access (rooted at the placeholder) lower normally. Also removes the explicit event dispatch in the compound branch; it now falls out of BindCompoundAssignmentCore's existing `left.Kind == EventAccess` check, which produces the same BoundEventAssignmentOperator.
…ject brace RHS
Three fixes to the compound-assignment-in-initializer binder path surfaced by
an adversarial review of the Phase 2a diff:
* Set WasPropertyBackingFieldAccessChecked on BoundPropertyAccess after the
value-kind check in BindObjectInitializerMemberCommon so the post-bind
walker in MethodCompiler does not assert on a compound-initializer's
raw-access left (which is no longer wrapped in BoundObjectInitializerMember).
* When BindObjectInitializerMemberCommon reports a value-kind error (CS0200 on
get-only, CS0154 on set-only, CS0191 on readonly, CS8331 on ref-readonly,
and similar) the hasErrors flag is set on the wrapper but does not propagate
to the raw access. Wrap the raw access in ToBadExpression in that case so
downstream flow analysis / operator resolution sees a known-bad left.
* Reject InitializerExpressionSyntax as the RHS of a compound member
initializer (`Prop += { 1, 2 }`) with ERR_InvalidInitializerElementInitializer
before falling through; the previous break was routing the whole expression
through BindValue -> BindCompoundAssignment -> BindValue on a brace-list
kind that BindExpressionInternal does not handle, tripping a debug trace
listener.
CompoundAssignmentInitializerBindingTests covers:
* All 11 compound operators on property / field / indexer / with-record.
* Target kinds: writable field, readonly field, get-only / set-only /
init-only / ref-returning / ref-readonly properties, indexers (including
dictionary-style multi-key), field-like and custom events, static
members, dynamic-typed member.
* Event target restricted to += / -=; other compound ops produce CS0019.
* Event targets in a `with` expression on records.
* Language version gating: Preview compiles; C# 14 (where user-defined
`operator +=` is available but our feature is not) produces a clean
feature-unavailable diagnostic on each of 11 compound operators.
* `??=` in a member initializer is rejected.
* Duplicate-rule permutations (= =, = +=, += =, += +=, = += =, record with).
* Indexer and event targets are unrestricted for duplicates.
* Compound RHS: ref rejected, nested brace-list rejected.
* Containers: struct, record class, record struct, anonymous type.
* User-defined operators: legacy `operator +` resolves on properties,
direct `operator +=` resolves on fields (and wins over legacy when both
are defined), in-place-only on properties fails because properties are
not variables.
* `required` members: compound alone does not satisfy the obligation;
`= 0, += 1` does (matches the "No" recommendation in the LDM question
added to dotnet/csharplang#10132).
Update four existing UnsignedRightShiftTests baselines. The classifier
change from Phase 1 makes `new List<int> { x >>= 1 }` classify as an
object initializer (`x` as a named member) rather than a collection
initializer, so the diagnostic shifts from CS0747 to CS0117 (no member
named `x`). The new behavior is consistent with the feature.
…t path The non-compound path wraps BoundPropertyAccess inside BoundObjectInitializerMember, and the walker in MethodCompiler never inspects the wrapped access, so the debug flag is only needed when the raw access is handed out unwrapped to BindCompoundAssignmentCore. Gate on the valueKind the compound branch already passes in (CompoundAssignment) rather than setting the flag unconditionally.
- Add enum target coverage: flag-enum bitwise (|=/&=/^=), plain-enum += with an int literal, enum+enum rejection, *= rejection, flag-enum in `with`, and the mixed `= then |= then &=` rule on an enum property. - Drop explicit `parseOptions: TestOptions.RegularPreview` from happy-path tests (preview is the default) and remove the now-redundant LangVersion_Preview_Compiles; LangVersion gating stays on the CS13/CS14 tests. - Drop `targetFramework: TargetFramework.NetCoreApp`; bundle a single `Polyfills` source (IsExternalInit, CompilerFeatureRequiredAttribute, Required/SetsRequiredMembersAttribute from CSharpTestBase) into every compilation so individual tests don't need to pick a framework.
`BindObjectInitializerMemberAccess` returns the raw bound member access;
`WrapAsObjectInitializerMember` wraps it in `BoundObjectInitializerMember` for
callers that need the simple-assignment lowering shape. Removes the dual-return
(wrapped + `out rawAccess`) contortion on `BindObjectInitializerMember`, and
makes the compound-assignment path's need for the raw access an explicit
structural choice at the call site rather than a side channel.
Simple-assignment and missing-assignment callers wrap; the compound-assignment
branch uses the raw access directly (unchanged behavior).
Also drops a redundant CS1918 that the binder produced on `new C { P += { 1, 2 } }`
as a side effect of treating the compound RHS as a nested initializer for the
property-nested-initializer check. `isRhsNestedInitializer` is now unconditionally
false on the compound path (compound initializer members cannot take a nested
initializer; that's CS0747 before anything else runs), so only CS0747 fires.
`LocalRewriter.AddObjectInitializers` hard-cast every initializer to
`BoundAssignmentOperator`, so `new C { P += v }`, `with { P -= v }`, or
`new C { E += h }` would crash lowering with `InvalidCastException`. The binder
tests passed only because `VerifyDiagnostics()` stops before lowering.
Changes:
- Binder: after `BindCompoundAssignmentCore`, swap the raw access in
`BoundCompoundAssignmentOperator.Left` for a `BoundObjectInitializerMember`
wrapper via `.Update(...)`. `LeftConversion` references `LeftPlaceholder` (not
`Left`), so the swap is safe. Events stay as `BoundEventAssignmentOperator`
with a placeholder `ReceiverOpt` (no `Left` to wrap). Drops the debug-only
`WasPropertyBackingFieldAccessChecked` workaround — no longer needed once the
wrapper hides `BoundPropertyAccess` on the compound path too.
- Lowering: replace the hard-cast in `AddObjectInitializers` with a `Kind`
switch. `BoundAssignmentOperator` takes the existing path; new
`AddCompoundObjectInitializer` reuses `VisitObjectInitializerMember` +
`MakeObjectInitializerMemberAccess` to substitute the placeholder receiver,
rebuilds the compound op with the real access as `Left`, and hands it to the
general compound-assignment lowering to emit the read-op-write sequence;
`AddEventObjectInitializer` substitutes the placeholder in `ReceiverOpt` and
hands to `VisitEventAssignmentOperator` to emit the add_/remove_ call.
- `NullableWalker.VisitObjectInitializerMember` now performs a reasonable visit
instead of throwing `Unreachable`. The simple-assignment path still dispatches
inline via `VisitObjectElementInitializer`; the compound path reaches us via
`VisitCompoundAssignmentOperator.Visit(Left)` and needs a real result.
- `GetLValueAnnotations` delegates to the wrapped member symbol for
`BoundObjectInitializerMember` (property and field cases).
Minor behavior change: for a binding-error case like `new C { E *= h }` where
`E` is an event, the event's associated-field read is no longer tracked through
the wrapper, so `WRN_UnreferencedEvent` fires alongside the primary
`ERR_BadBinaryOps`. Test baseline updated to expect both.
New `CompoundAssignmentInitializerEmitTests`:
- For every one of the 11 compound operators, on property / field / indexer
targets, assert that `new C { P op= rhs }` produces the same final state as
`var c = new C(); c.P op= rhs;` via a CompileAndVerify smoke test.
- `with` expression on a record property.
- Event `+=` / `-=` in an object initializer, running and verifying handler
invocation.
- User-defined in-place `operator +=` on a struct field target.
- Flag enum `|=` (starting from a non-zero default) and mixed `= ... , &= ~X`.
- IL verification for three representative cases: property `+=`, indexer `|=`,
and event `+=`.
New binder test `ExpressionTree_CompoundMemberInitializer_Fails` pins the
`ERR_ExpressionTreeContainsAssignment` error path, confirming the diagnostics
pass catches compound member initializers before `ExpressionLambdaRewriter`'s
hard-cast could fire.
Rework BindInitializerMemberAssignment so the compound branch mirrors the
simple-assignment branch: bind the left via BindObjectInitializerMember, bind
the right, dispatch to BindCompoundAssignmentCore (just like the normal
`a.b += c` path hands left and right to BindCompoundAssignmentCore). Drop the
BindObjectInitializerMemberAccess + WrapAsObjectInitializerMember split and the
post-bind Update swap.
BindObjectInitializerMember pre-validates the target with
BindValueKind.CompoundAssignment when the member initializer is compound, so
set-only properties and readonly fields error at bind-member time (just like
they do for `a.b += c`). BindCompoundAssignmentCore sees the wrapped
BoundObjectInitializerMember and needs two small accommodations:
- CheckValueKind grows a BoundObjectInitializerMember case. For non-RefersToLocation
queries the pre-validation result carries (wrapper never errors here), and
for RefersToLocation queries (the gate in shouldTryUserDefinedInstanceOperator
that decides whether user-defined in-place `operator +=` is applicable) it
dispatches on the wrapped MemberSymbol: non-readonly fields and ref-returning
properties are ref-assignable locations, regular properties, indexers, and
events are not. This matches the normal `a.b += c` outcome.
- The event `+=` / `-=` dispatch in BindCompoundAssignmentCore also matches
`BoundObjectInitializerMember { MemberSymbol: EventSymbol }`. It synthesizes
a BoundEventAccess with a placeholder receiver (lowering substitutes it in
AddEventObjectInitializer) and hands to BindEventAssignment as usual, so
`new C { E += h }` and `c with { E += h }` lower to add_E accessor calls
rather than bypassing the accessor with a ldfld/Combine/stfld sequence.
Also hoist the nested-initializer rejection above RHS binding in the compound
branch so `new C { P += { 1, 2 } }` reports CS0747 alone rather than also
emitting CS1918 / CS1922 for the brace-list RHS. Updates the RefOnRhs_Rejected
baseline from CS1073 (parser-level "unexpected 'ref'") to CS8373
(ERR_RefLocalOrParamExpected) — the new diagnostic is more specific and
mirrors what `c.P = ref x` produces outside an initializer.
80k+ existing tests across Semantic / Symbol / Emit / Emit3 / IOperation
remain green.
…d RHS Move the `compound + nested-initializer RHS` rejection from above the left / right bind to below it. Binding both sides first lets the user see each individual diagnostic (CS1918 on the nested-initializer-into-value-type target, CS1922 on the brace-list-into-non-IEnumerable RHS), and the compound-specific CS0747 is added as a summary. Returns a `BoundBadExpression` whose children are the bound left and right so semantic-model / IDE consumers still see what each side resolved to.
`ReportDuplicateObjectMemberInitializers` was using a three-value enum but only ever cared about "have we seen any initializer for this member?". Collapse to `PooledHashSet<string>` — a second `=` (or a `=` following a compound) is the only error, detected by `Add` returning false. Expand event-target coverage: - `Event_NonPlusOrMinusCompound_Fails` is now a theory over all 9 non-+/- compound operators (`*=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `>>>=`); each produces CS0019 via fall-through to binary-operator overload resolution on the delegate type. - Add `Event_SimpleAssignment_FromInsideContainingType_Succeeds`: Roslyn's long-standing field-like-event spec violation (treating `E = h` as write to the backing field when accessed from inside the declaring type) still works in initializer context. - Add `Event_SimpleAssignment_FromOutsideContainingType_Fails`: from outside the declaring type, `E = h` errors with CS0070 just like `c.E = h` would — the object-initializer context doesn't relax the check. - Add `Event_SimpleAssignment_CustomEvent_Fails`: a custom event (explicit add/remove accessors) has no backing field, so `E = h` fails with CS0079 even from inside the declaring type.
…elpers The wrapper's previous simplified value-kind logic duplicated bits of CheckFieldValueKind / CheckPropertyValueKind and got details wrong: it accepted `ref readonly` properties as ref-assignable (they aren't — CS8331 should fire), and it ignored ref-readonly fields, fixed-size-buffer fields, and value-type receiver modifiability rules. Reconstruct a raw BoundFieldAccess / BoundPropertyAccess / BoundIndexerAccess / BoundEventAccess from the wrapper's MemberSymbol + arguments + a placeholder receiver, and hand it to CheckValueKind. The existing per-kind helpers then apply the full check for free. Tests added: - `UserDefined_InPlaceOnly_OnRefReturningProperty_Succeeds`: ref-returning property is a location; in-place `operator +=` applies. (Was previously un-covered.) - `UserDefined_InPlaceOnly_OnRefReadonlyProperty_Fails`: `ref readonly` property is a location but not ref-assignable; compound initializer emits CS8331. (Would previously have incorrectly let the in-place operator run.)
…entAssignment BindEventAssignment only reads three things from its BoundEventAccess argument: EventSymbol, ReceiverOpt, and the delegate Type. Refactor it to take those directly, so the wrapper-event case in BindCompoundAssignmentCore doesn't need to allocate a BoundEventAccess just to have it unpacked immediately. Tuple- switch at the call site extracts the three pieces from either a raw BoundEventAccess or a BoundObjectInitializerMember whose MemberSymbol is an EventSymbol.
…tAssignment The tuple-switch that unpacks either a BoundEventAccess or a BoundObjectInitializerMember-wrapped event and dispatches to BindEventAssignment was inline. Lift it to a named helper so the outer local function stays flat and the event-path invariants live in one place.
…rage - AddCompoundObjectInitializer now dispatches over every Left shape BindObjectInitializerMemberCommon can produce (BoundObjectInitializerMember, BoundImplicitIndexerAccess, BoundArrayAccess, BoundPointerElementAccess, BoundDynamicObjectInitializerMember) rather than hard-casting to the wrapper, closing two reachable crashes (Index/Range-indexer, dynamic nested initializer). - NullableWalker.VisitObjectCreationInitializer's inner switch now routes BoundKind.CompoundAssignmentOperator (and BoundKind.NullCoalescingAssignment- Operator) through a new slot-tracking helper that mirrors VisitObjectElement- Initializer. Without this, the container's per-member nullable state was stale after a compound member initializer. - BindInitializerMemberAssignment's switch now admits CoalesceAssignmentExpression alongside the eleven regular compound operators. Binder_Operators extracts BindNullCoalescingAssignmentOperatorCore (parallel to BindCompoundAssignment- Core) so the initializer path can supply a pre-bound Left validated with BindValueKind.CompoundAssignment. Lowering shares a RewriteInitializerMember- LeftOperand dispatcher across compound and ??= paths; NullableWalker shares UpdateInitializerMemberSlot. - The spec diff targeted ECMA-334 v7, whose assignment_operator production pre-dates ??=. The C# 8 null-coalescing-assignment proposal defines ??= to follow compound-assignment semantic rules, so we group it with compound_assignment_operator in the initializer admission here; the separate csharplang PR updates the proposal text. - 22 new tests: crash regressions for the three bugs above, plus coverage the original adversarial audit flagged as missing (semantic model / IOperation, indexer args evaluated exactly once, checked / unchecked propagation, string compound, nullable flow through the LHS, primary-constructor positional property in with), plus a full Coalesce_* region pinning ??= behavior across nullable value types, reference types, with expressions, duplicate rules, required members, the language-version gate, and the event corner cases.
On a freshly constructed C, a field-like event is guaranteed null. `??= h` reduces to an unconditional write, which on a null event field is functionally equivalent to `+= h` (subscribe via add_E) — legal from outside C per the event spec. So this test pinning clean compilation documents correct behavior, not a hole in binding.
When a constructor carries [SetsRequiredMembers], required-member enforcement is lifted for that new-expression, so the initializer is free to read-modify- write the pre-initialized value. Counterpart to Required_CompoundAlone_DoesNotSatisfy.
Exercises the interaction of record struct copy semantics (`with` clones) + ref-returning property ([UnscopedRef] into the clone's field) + user-defined in-place `operator +=` on a struct (mutates via the ref). Pins that the clone sees the bump while the original stays untouched.
added 18 commits
April 28, 2026 12:30
… carve-out Audit follow-up against the now-merged spec (csharplang#10151 + dotnet#10152): * `Binder_Expressions.ReportDuplicateObjectMemberInitializers` — SPEC comment block updated to match the merged paragraph: "any number of member initializers using a compound assignment operator are permitted for the same target. If present, an `=` member initializer shall appear in lexical order before any other member initializer for that target. No such restriction applies to indexer targets." Inner comment next to the duplicate-fires guard restated to the same vocabulary. * `Binder_Expressions.BindObjectInitializerExpression` — companion comment on the `memberNameMap` allocation updated to the same phrasing. * `Duplicate_Indexer_Unrestricted` test comment — drop the stale "event or indexer" wording (spec only carves out indexer targets). * `Duplicate_Indexer_FirstForm_SameKeyUnrestricted` (new test) — pin the spec's deliberate exclusion of indexer targets from the first-form exclusivity rule. Two `[k] = { … }` initializers for the same indexer key are permitted; both invoke the indexer getter and configure the returned instance.
…lizers The "mixed object and collection initializers" feature (dotnet/csharplang#10185) relaxes binding so that a single `{ ... }` initializer body may contain both member-shaped initializer elements (`Name = value`, `Name op= value`, `[args] = value`) and bare-expression element initializers (`Add` targets). At the parser level no change is needed: `ParseObjectOrCollectionInitializer` already accepts mixed-shape lists, and its classifier flips the wrapper to `ObjectInitializerExpression` whenever at least one element is an `AssignmentExpressionSyntax` whose `Left.Kind` is `IdentifierName` or `ImplicitElementAccess`, otherwise to `CollectionInitializerExpression`. The binder continues to reject mixed lists at every language version until the binding PR flips the feature gate. This PR adds `MixedInitializerParsingTests.cs` to pin that classification behavior so subsequent PRs cannot silently regress it. Coverage includes: * Sanity invariants (empty / pure members / pure elements). * Minimum-shape "flips" (one qualifying element is enough, leading or trailing). * Mixed orderings (members-first, elements-first, interleaved). * Simple and compound member shapes (`Name =`, `Name op=`, `[args] =`, `[args] op=`) theorized over every compound assignment operator. * Brace-list element initializer, nested object-creation, nested `target = { ... }` first-form initializer alongside top-level elements. * Classifier boundary cases pinning that qualifier-style assignments (`a.b op= …`) do not contribute to the wrapper flip in a mixed list. * `new T(args) { ... }` and target-typed `new() { ... }` forms. * Trailing comma and `;` separator. * Colon recovery (`X: 1` → `SimpleAssignmentExpression`) still qualifies. * `with { ... }` parser-permissive shape (out of feature scope; pinned to catch incidental regressions). * Parses cleanly at every language version (binder gates the feature later).
…and ET rejection Feature-gated end-to-end implementation of dotnet/csharplang#10185. Under LanguageVersion.Preview, an `ObjectInitializerExpression` may now contain both member-shape initializer elements (`Name = value`, `Name op= value`, `[args] = value`) and bare-expression element initializers (`Add` targets). Each child binds, lowers, flows, and emits per its own kind's existing rules; the binder dispatches per-element in `BindObjectInitializerExpression`, the lowering switch in `LocalRewriter.AddObjectInitializers` gains `CollectionElementInitializer` and `DynamicCollectionElementInitializer` arms, and `NullableWalker.VisitObjectCreationInitializer`'s object branch delegates collection-element-shape children to `VisitCollectionElementInitializer`. The `IEnumerable` + `Add`-resolution requirement is computed lazily, so pure-member object initializers are unaffected. `with` expressions stay out of feature scope: the dispatch is gated on `isObjectInit`, so `WithInitializerExpression` continues to reject any non-member child via the existing `BindInitializerMemberAssignment` path. `required` member discharge is unchanged in semantics: the existing `CheckRequiredMembersInObjectInitializer` already skips non-`BoundAssignmentOperator` entries, so `element_initializer`s correctly do not satisfy `required`. The mixed shape is explicitly rejected inside `Expression<>` lambdas via a new `ERR_ExpressionTreeContainsMixedObjectAndCollectionInitializer` reported from `DiagnosticsPass_ExpressionTrees.VisitObjectInitializerExpression` — the `System.Linq.Expressions.MemberInit` shape has no representation for "Add this value alongside the member assignments". Pre-feature behavior is preserved at sub-preview language versions: the new dispatch is gated on `Compilation.IsFeatureEnabled`, so the existing `ERR_InvalidInitializerElementInitializer` / recovery path continues to fire unchanged at C# 14 and below. Eight existing tests across `ObjectAndCollectionInitializerTests`, `IOperationTests_IObjectCreationExpression`, `SemanticErrorTests`, and `RefFieldTests` are pinned at `TestOptions.Regular14` since they assert the pre-feature diagnostic shape. Adds two new CSharp15 test files: * `MixedInitializerBindingTests.cs` — 24 tests covering members + elements in all orderings, simple/compound/indexer/event/null-coalescing members, multi-arg `{ a, b }` Add, extension `Add`, target-typed `new()`, ctor-args form, `init` members, dynamic `Add` arguments, dictionary indexer, generic `Add` overload, type-without-`IEnumerable` / type-without-`Add` rejection, `required` member behavior (pre-PR behavior preserved), `with` regression pin, expression-tree rejection, duplicate-member rule preservation, and language-version gating. * `MixedInitializerEmitTests.cs` — 7 runtime/IL tests pinning lexical ordering across `Add` calls and member writes (the headline observable behavior), compound/indexer/event/brace-list interactions, and a canonical IL shape.
Updates the shared workspace/IDE-foundation helpers that pattern-match on
`SyntaxKind.ObjectInitializerExpression` / `CollectionInitializerExpression`
so the mixed `{ ... }` shape (`ObjectInitializerExpression` with element-shape
children, dotnet/csharplang#10185) is treated correctly by downstream IDE
features without per-feature changes:
* `FormattingHelpers.IsInitializerForObjectOrAnonymousObjectCreationExpression`
now keys off the parser-assigned wrapper kind instead of a "first element
is `AssignmentExpressionSyntax`" heuristic, which mis-classified mixed
initializers whose first element happens to be a bare expression
(e.g. `new C { 1, X = 2 }`).
* `SpeculationAnalyzer` gains an `ObjectInitializerExpression` arm that
routes element-shape children through the same
`ReplacementBreaksCollectionInitializerAddMethod` guard the pure
collection initializer arm has, so speculative replacement of an
element inside a mixed wrapper can't silently change `Add` resolution.
* `UseUtf8StringLiteralDiagnosticAnalyzer` widened its parent-kind pattern
to also recognize `ObjectInitializerExpression` parents, so a
`params byte[]`-Add invocation inside a mixed wrapper still surfaces the
UTF-8 literal suggestion.
Adds two regression tests pinning the new behavior — one in
`FormattingTests.ObjectInitializer_MixedObjectAndCollection_ElementFirst` and
one in `UseUtf8StringLiteralTests.TestMixedObjectAndCollectionInitializer`.
Deliberately deferred:
* `CSharpSemanticModel.GetCollectionInitializerSymbolInfo` widening to
accept element-shape children whose parent is `ObjectInitializerExpression`
— naturally lands with the SemanticModel/IOperation PR.
* `AbstractReferenceFinder.FindReferencesInCollectionInitializer` +
`SyntaxTreeIndex_Create.containsCollectionInitializer` widenings — depend
on the above compiler API change.
* `UseInitializerHelpers.GetNewObjectCreation` first-element-only wrapper-kind
selection — lands with the IDE0017/IDE0028 unification PR.
* `CSharpAddImportFeatureService` collection-only gates — lands with the IDE
polish PR.
… IOperation tests Widens `CSharpSemanticModel.GetCollectionInitializerSymbolInfo` (and the matching `MemberSemanticModel` worker) to accept element-shape children whose parent is the mixed `ObjectInitializerExpression` wrapper (dotnet/csharplang#10185), so the public API returns the `Add` symbol info for those children the same way it does for pure-collection wrappers. The downstream `AbstractReferenceFinder.FindReferencesInCollectionInitializer` + `SyntaxTreeIndex_Create.containsCollectionInitializer` consumers are also widened to walk mixed wrappers; `CSharpTypeInferenceService.TypeInferrer`'s element-type inference arm is widened to also fire on non-assignment-shape children of `ObjectInitializerExpression`. Adds eight tests in `MixedInitializerSemanticModelTests.cs` covering: `GetCollectionInitializerSymbolInfo` on element-shape children of mixed wrappers, member-shape children (returns `SymbolInfo.None`), pure-collection parity, nested-mixed (`Prop = { mixed }`), `ComplexElementInitializer` / multi-arg `Add` in a mixed wrapper, and the IOperation tree shape (unified `IObjectOrCollectionInitializerOperation` with mixed `IAssignmentOperation` + `IInvocationOperation` / `IDynamicInvocationOperation` children, in lexical order). Adds one parallel `TypeInferrerTests` pin (EditorFeatures, Windows-only at runtime). Deferred (per PR plan): * `UseInitializerHelpers.GetNewObjectCreation` first-element-only wrapper-kind selection — lands with the IDE0017/IDE0028 unification PR. * `CSharpAddImportFeatureService` collection-only gates — lands with the IDE polish PR. * `SpeculationAnalyzer` + `TypeInferrer` position-only-cursor case — EditorFeatures-only tests; will pin in a follow-up that doesn't depend on the Windows test surface.
Extends the UseObjectInitializer (IDE0017) analyzer so subsequent
`instance.Add(value)` expression-statements fold into an existing object
initializer as bare-element initializers alongside member-shape children,
producing `new C { X = 1, 10, 20 }` for the mixed object/collection
initializer feature. Gated on `SupportsMixedObjectAndCollectionInitializers`
(C# Preview+, VB always false) and on the target type implementing
`IEnumerable` (mirrors IDE0028's precondition). Also fixes
`UseInitializerHelpers.GetNewObjectCreation` to pick the wrapper kind by
scanning all expressions rather than only the first, so the generated
wrapper matches the parser's classification for mixed lists.
Stacked:
* #83XXX mixed-init-syntax-facts (parser tests)
* #83XXX mixed-init-bind (binding + lowering + flow + emit + ET)
* #83XXX mixed-init-ide-foundation (formatter + speculation + UseUtf8)
* #83XXX mixed-init-semmodel (SemanticModel + find-refs + IOperation)
* >>> this PR <<<
…e carve-outs Two remaining workspace/feature call sites still treated `CollectionInitializerExpression` as the only valid wrapper for collection-element shapes; widens both to recognize element- shape children of `ObjectInitializerExpression` (the mixed wrapper kind chosen by the parser when any child is assignment-shape): * `CSharpAddImportFeatureService` — `IsAddMethodContext` and the CS7036/CS0308/CS0428/CS1061 error-recovery arm route through a new `IsCollectionElementInitializerContext` helper that accepts either `CollectionInitializerExpression` parents or non-assignment children of `ObjectInitializerExpression`. * `ExpressionSyntaxExtensions.CanReplaceWithLValue` — adds a guarded `ObjectInitializerExpression` case (excluding `AssignmentExpressionSyntax` children, which must remain non-replaceable because they bind as member initializers). Adds tests covering both widenings, including a negative IntroduceVariable test that pins the member-name LHS of a mixed init as non-replaceable, and parses the C# SpeculationAnalyzer test base under `LanguageVersion.Preview` so the mixed-init speculation case binds. Stacked: * dotnet#83750 mixed-init-syntax-facts (parser tests) * dotnet#83751 mixed-init-bind (binding + lowering + flow + emit + ET) * dotnet#83752 mixed-init-ide-foundation (formatter + speculation + UseUtf8) * dotnet#83753 mixed-init-semmodel (SemanticModel + find-refs + IOperation) * dotnet#83754 mixed-init-ide0017-extension (IDE0017 Add-fold extension) * >>> this PR <<<
…ia new IDE0400
Introduces IDE0400 (UseMixedObjectAndCollectionInitializer) and refactors the
UseObjectInitializer analyzer to classify-and-route based on the shape of the
synthesized initializer rather than always reporting IDE0017:
* Pure member-shape synthesis (existing behavior) -> IDE0017.
* Pure Add-shape synthesis with no existing member init -> no report; IDE0028
is the canonical owner of that shape and continues to fire unchanged.
* Mixed synthesis (member + Add across existing initializer + new matches) ->
IDE0400 (new). Notification severity = the lesser of PreferObjectInitializer
and PreferCollectionInitializer; mixed is suppressed entirely when either
preference is disabled.
This eliminates the duplicate IDE0017+IDE0028 squiggle introduced in PR 5 on
Preview pure-Add sequences (`var c = new C(); c.Add(1); c.Add(2);`), and gives
mixed sequences a distinct diagnostic ID for severity and suppression. IDE0028
analyzer is untouched. Pre-Preview behavior is preserved unchanged.
Adds:
* IDE0400 to IDEDiagnosticIds, EnforceOnBuildValues, analyzer resources, code
cleanup and configure-severity tables, and the rules-missing-documentation
index.
* `MixedTest` test wrapper that maps `[|...|]` markup defaults to IDE0400 for
tests whose synthesized initializer is mixed; the 5 mixed-init tests from
PR 5 now go through it.
* New IDE0028 test pinning that pure-Add at LanguageVersion.Preview still
reports IDE0028 (and only IDE0028) after the unification.
* UseObjectInitializer regression tests: IDE0017 still fires on pure-member
under Preview, IDE0400 is suppressed when PreferCollectionInitializer is
false, pure-Add yields silently.
* Differentiated code-fix title: lightbulb for IDE0400 reads "Object and
collection initialization can be merged" rather than the legacy IDE0017
string.
Stacked:
* dotnet#83750 mixed-init-syntax-facts (parser tests)
* dotnet#83751 mixed-init-bind (binding + lowering + flow + emit + ET)
* dotnet#83752 mixed-init-ide-foundation (formatter + speculation + UseUtf8)
* dotnet#83753 mixed-init-semmodel (SemanticModel + find-refs + IOperation)
* dotnet#83754 mixed-init-ide0017-extension (IDE0017 Add-fold extension)
* dotnet#83755 mixed-init-ide-polish (AddImport + CanReplaceWithLValue carve-outs)
* >>> this PR <<<
…cation (shared match shape)
First of three planned passes that flatten the use-object-initializer (IDE0017)
and use-collection-initializer (IDE0028) analyzer families down to a single
walk + single fixer + single diagnostic analyzer. This pass changes only the
data shape that flows through both pipelines; behavior is identical on every
input. Passes 2 and 3 (merge fixers, merge analyzers) build on this.
Introduces `InitializerMatch<TNode>` (new) as the union of fields both walks'
match types carried before:
* `Node` (was `Statement` / `CollectionMatch.Node`) — the fold-candidate
syntactic node.
* `Kind` (new) — `InitializerMatchKind` discriminator covering
MemberInitializer, AddInvocation, IndexAssignment, ForEach, and the
collection-expression-only ConstructorArgument shape.
* `UseSpread` / `UseCast` / `UseKeyValue` — inherited from `CollectionMatch`,
used by collection-expression synthesis only; member-init matches leave
them at their defaults.
Both walks and both per-language fixers are migrated to consume this shape:
* IDE0017's `AbstractUseNamedMemberInitializerAnalyzer` and the C#/VB
`UseObjectInitializerCodeFixProvider` recover rich member-access /
initializer data from the statement at fix time (no longer carried in the
match), guarded by a `Kind`-driven dispatch with `ExceptionUtilities.UnexpectedValue`
for unknown kinds.
* IDE0028's `AbstractUseCollectionInitializerAnalyzer` and the C#/VB
`UseCollectionInitializerCodeFixProvider` produce `InitializerMatch`
instead of `CollectionMatch`; the collection-expression rewriter (shared
with the IDE0300+ family that's NOT part of this unification) keeps its
`CollectionMatch` signature and the IDE0028 fixer translates at the
boundary.
* `UseCollectionInitializerHelpers.GetLocationsToFade` gains an
`InitializerMatch` overload sharing a node-based core with the existing
`CollectionMatch` overload — the IDE0300+ analyzers use the legacy form
and are unaffected.
Tests: 942 tests pass across IDE0017, IDE0028, and the IDE0300-IDE0306
collection-expression family with zero behavior changes; 20620-test full
Features suite also green.
Stacked:
* dotnet#83750 mixed-init-syntax-facts (parser tests)
* dotnet#83751 mixed-init-bind (binding + lowering + flow + emit + ET)
* dotnet#83752 mixed-init-ide-foundation (formatter + speculation + UseUtf8)
* dotnet#83753 mixed-init-semmodel (SemanticModel + find-refs + IOperation)
* dotnet#83754 mixed-init-ide0017-extension (IDE0017 Add-fold extension)
* dotnet#83755 mixed-init-ide-polish (AddImport + CanReplaceWithLValue carve-outs)
* dotnet#83757 mixed-init-unify-analyzers (IDE0017+IDE0028 routing + IDE0400)
* >>> this PR <<<
…cation (merged fix providers) Second of three planned passes that flatten the use-object-initializer (IDE0017) and use-collection-initializer (IDE0028) analyzer families. Pass 1 (dotnet#83758) unified the match data shape; this pass collapses the two per-language fix provider classes into one. The two walks remain separate at this layer — Pass 3 collapses them into a single walk. Introduces `AbstractUseInitializerCodeFixProvider<…>` (12 type parameters, covering the union of both legacy abstract bases). One per-language concrete each: `CSharpUseInitializerCodeFixProvider` and `VisualBasicUseInitializerCodeFixProvider`. Each registers for IDE0017, IDE0028, AND IDE0400 in a single `FixableDiagnosticIds` list, and dispatches inside `FixAsync` based on a property-bag tag set by the analyzer: * `UseMemberInitializerName` (new) → member-init synthesis (IDE0017/IDE0400). * `UseCollectionExpressionName` (existing) → collection-expression synthesis. * Neither → collection-init synthesis. The property-bag tag is needed because `ForkingSyntaxEditorBasedCodeFixProvider.FixAsync` only receives the diagnostic's properties (not the diagnostic itself), and under the mixed object/collection initializer feature both walks can return non-empty matches for the same object creation — probing alone would mis- route IDE0028 fix requests on Preview. The use-object-initializer analyzer now attaches the `UseMemberInitializerName` property when reporting any of its three IDs; fade-out diagnostics receive the same property so fix-all across both primary and faded diagnostics dispatches consistently. Deletes: * `AbstractUseObjectInitializerCodeFixProvider` and `AbstractUseCollectionInitializerCodeFixProvider` (replaced by the new unified abstract base). * `CSharpUseObjectInitializerCodeFixProvider`, `CSharpUseCollectionInitializerCodeFixProvider`, `VisualBasicUseObjectInitializerCodeFixProvider`, `VisualBasicUseCollectionInitializerCodeFixProvider` (replaced by the unified per-language concretes). The existing C# partials (`_CollectionInitializer.cs` and `_CollectionExpression.cs`) are re-namespaced and re-declared as partials of the new class; their static helpers carry over verbatim. Per-language MEF registration uses `PredefinedCodeFixProviderNames.UseCollectionInitializer` for the unified provider — the previous `UseObjectInitializer` constant is unused but kept (no public API removal). Tests, telemetry table, and projitems updated. Tests: 942 init-suite tests + 20620-test full Features suite both green; zero behavior changes. Stacked: * dotnet#83750 mixed-init-syntax-facts (parser tests) * dotnet#83751 mixed-init-bind (binding + lowering + flow + emit + ET) * dotnet#83752 mixed-init-ide-foundation (formatter + speculation + UseUtf8) * dotnet#83753 mixed-init-semmodel (SemanticModel + find-refs + IOperation) * dotnet#83754 mixed-init-ide0017-extension (IDE0017 Add-fold extension) * dotnet#83755 mixed-init-ide-polish (AddImport + CanReplaceWithLValue carve-outs) * dotnet#83757 mixed-init-unify-analyzers (IDE0017+IDE0028 routing + IDE0400) * dotnet#83758 mixed-init-unify-pass1-match-shape (Pass 1: shared match shape) * >>> this PR — Pass 2/3 of full unification <<< Pass 3 (merge walks + delete duplicate diagnostic analyzers) follows as a separate stacked PR.
…ication (merged walk) Third pass of the IDE0017+IDE0028 unification — Pass 1 (dotnet#83758) unified the match data shape, Pass 2 (dotnet#83760) collapsed the two fix providers into one, and this pass extends the use-collection-initializer walk (`AbstractUseCollectionInitializerAnalyzer`) to also produce `InitializerMatchKind.MemberInitializer` matches when the language admits member-init folds. The walk's per-statement try-order becomes member-init → Add/AddRange → index, with a shape-lock so pre-mixed-init languages don't interleave member and collection-element kinds and a duplicate-target rule (`seenNames`) preserved from the legacy `AbstractUseNamedMemberInitializer` walk. All the member-init helpers (`IsExplicitlyImplemented`, `ImplicitMemberAccessWouldBeAffected`, the duplicate-target / shape-lock / compound-assignment-statement gates) move into the unified walk. Two abstract hooks are added so the per-language concretes opt into the member-init behavior: `SupportsCompoundAssignmentInInitializer` (already mirrored on the now-secondary `AbstractUseNamedMemberInitializerAnalyzer`) and `SupportsMixedObjectAndCollectionInitializers`. C# returns the language-version-gated answer; VB returns false for both. The setup loop now also pre-populates `seenIndexAssignment` from element-access children of an existing `ObjectInitializerExpression` (`{ [k] = v }`) so subsequent `c.Add(…)` statements don't falsely match through the unified walk on iteration-2 fix verification — pre-Pass-3 the legacy `HasExistingInvalidInitializerForCollection` precondition short- circuited those shapes via `ShouldAnalyze`, but the unified walk has to preserve the invariant explicitly. Per-statement Add detection is gated on `GetAddMethods().Any()` for the same reason — explicit-interface-implemented `Add` methods bind via `GetSymbolInfo` but aren't found by `LookupSymbols`, and without the gate they would falsely match. `AbstractUseCollectionInitializerDiagnosticAnalyzer.AnalyzeNode` bails out when the merged match list contains any `MemberInitializer` entry; the use-object-initializer diagnostic analyzer remains the canonical reporter for member-init-bearing match lists (it already owns the IDE0017 / IDE0400 routing from PR 7). This prevents the two analyzers from double-reporting on mixed scenarios under the mixed object/collection initializer feature (csharplang#10185). Per-language concretes (C# + VB) gain a new `TAssignmentStatementSyntax` generic slot threaded through the walk → diagnostic analyzer → fix provider chain so the unified member-init detection can use the language's assignment-statement-shaped type. The pre-existing `AbstractUseNamedMemberInitializerAnalyzer` walk and `AbstractUseObjectInitializerDiagnosticAnalyzer` diagnostic analyzer are intentionally left in place by this pass — they continue to handle pure- member-init scenarios on types without an accessible `Add` (where IDE0028's strict `ShouldAnalyze` rules out the walk entirely). A follow-on pass can delete those once IDE0028's `ShouldAnalyze` is loosened in lock-step with tightening the collection-expression synthesis path (`CanUseCollectionExpression`) to skip problematic existing initializers. 942 init-suite tests + 20620-test full Features suite both green; zero behavior changes user-visibly. The cross-analyzer double-report bug that Pass 3 was specifically aimed at preventing (the walk now produces member matches that IDE0028 would have reported on without the new cede) is closed by the member-init bail-out in `AnalyzeNode`. Stacked: * dotnet#83750 mixed-init-syntax-facts (parser tests) * dotnet#83751 mixed-init-bind (binding + lowering + flow + emit + ET) * dotnet#83752 mixed-init-ide-foundation (formatter + speculation + UseUtf8) * dotnet#83753 mixed-init-semmodel (SemanticModel + find-refs + IOperation) * dotnet#83754 mixed-init-ide0017-extension (IDE0017 Add-fold extension) * dotnet#83755 mixed-init-ide-polish (AddImport + CanReplaceWithLValue carve-outs) * dotnet#83757 mixed-init-unify-analyzers (IDE0017+IDE0028 routing + IDE0400) * dotnet#83758 mixed-init-unify-pass1-match-shape (Pass 1: shared match shape) * dotnet#83760 mixed-init-unify-pass2-fix-providers (Pass 2: merged fix providers) * >>> this PR — Pass 3a: merged walk (member-init produced by unified walk) <<<
This was referenced May 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third pass of the IDE0017+IDE0028 unification — Pass 1 (#83758) unified the match data shape, Pass 2 (#83760) collapsed the two fix providers into one, and this pass extends the use-collection-initializer walk to also produce `InitializerMatchKind.MemberInitializer` matches when the language admits member-init folds.
The walk's per-statement try-order becomes member-init → Add/AddRange → index, with a shape-lock so pre-mixed-init languages don't interleave member and collection-element kinds and a duplicate-target rule (`seenNames`) preserved from the legacy member-init walk. All the member-init helpers (`IsExplicitlyImplemented`, `ImplicitMemberAccessWouldBeAffected`, the duplicate-target / shape-lock / compound-assignment-statement gates) move into the unified walk.
Two abstract hooks are added so the per-language concretes opt into the member-init behavior (`SupportsCompoundAssignmentInInitializer`, `SupportsMixedObjectAndCollectionInitializers`). C# returns the language-version-gated answer; VB returns false for both. Per-language concretes gain a new `TAssignmentStatementSyntax` generic slot threaded through the walk → diagnostic analyzer → fix provider chain.
`AbstractUseCollectionInitializerDiagnosticAnalyzer.AnalyzeNode` bails out when the merged match list contains any `MemberInitializer` entry; the use-object-initializer diagnostic analyzer remains the canonical reporter for those (it owns the IDE0017 / IDE0400 routing from PR 7). This prevents the two analyzers from double-reporting on mixed scenarios under the mixed object/collection initializer feature.
The pre-existing `AbstractUseNamedMemberInitializerAnalyzer` walk and `AbstractUseObjectInitializerDiagnosticAnalyzer` are intentionally left in place by this pass — they continue to handle pure-member-init scenarios on types without an accessible `Add` (where IDE0028's strict `ShouldAnalyze` rules out the walk entirely). A follow-on pass can delete those once IDE0028's `ShouldAnalyze` is loosened in lock-step with tightening the collection-expression synthesis path to skip problematic existing initializers.
Two correctness fixes the unified walk needs that pre-Pass-3 `ShouldAnalyze` short-circuiting handled implicitly:
942 init-suite tests + 20620-test full Features suite both green; zero behavior changes user-visibly.
Stacked:
Microsoft Reviewers: Open in CodeFlow