[mixed object and collection initializers] SemanticModel + IOperation + find-refs#83753
Draft
CyrusNajmabadi wants to merge 113 commits into
Draft
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:20
…ests
Three tests were re-pinning shapes already covered by sibling tests in
the same file:
* `Classifier_AllCompoundMembersAreObjectInitializer` —
`new Goo { Prop op 1, Event op Handler }`. The classifier-evidence
shape (compound first element) is already pinned by
`ObjectInitializer_NamedMember_AllCompoundOperators`
(`new Goo { Prop op 1 }`); the second member is parser-irrelevant
to the classifier decision.
* `ObjectInitializer_IndexerMember_AllCompoundOperators` —
`new Goo { [0] op 1 }`. The two-indexer-entry variant
(`Classifier_IndexerCompoundMembersAreObjectInitializer`,
`new Goo { [0] op a, [1] op b }`) covers the same single-entry parse
tree plus comma-separation, so the single-entry test is a strict
subset.
* `Classifier_EmptyBracesIsObjectInitializer` — `new Goo { }`. Existing
pre-feature behavior (empty braces have always classified as
object-init); not specific to compound assignment in initializers.
Test count: 226 → 201 (12 + 12 + 1 = 25 fewer cases).
… 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.
This was referenced May 18, 2026
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.
Widens
GetCollectionInitializerSymbolInfoto recognize element-shape children of the mixedObjectInitializerExpressionwrapper (dotnet/csharplang#10185); plumbs the corresponding find-refs / index / type-inferrer consumers; adds SemanticModel + IOperation tests.Part of the mixed-object-and-collection-initializers language feature implementation.
Depends on #83752.
Stacked:
Microsoft Reviewers: Open in CodeFlow