Skip to content

[mixed object and collection initializers] SemanticModel + IOperation + find-refs#83753

Draft
CyrusNajmabadi wants to merge 113 commits into
dotnet:features/compound-assignment-in-initializerfrom
CyrusNajmabadi:mixed-init-semmodel
Draft

[mixed object and collection initializers] SemanticModel + IOperation + find-refs#83753
CyrusNajmabadi wants to merge 113 commits into
dotnet:features/compound-assignment-in-initializerfrom
CyrusNajmabadi:mixed-init-semmodel

Conversation

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

@CyrusNajmabadi CyrusNajmabadi commented May 18, 2026

Widens GetCollectionInitializerSymbolInfo to recognize element-shape children of the mixed ObjectInitializerExpression wrapper (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

Cyrus Najmabadi 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.
Cyrus Najmabadi 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers Community The pull request was submitted by a contributor who is not a Microsoft employee. VSCode

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant