Skip to content

Extension members on typeless receivers - PR 23: NullLiteral x ClassicExtensionMethod tests#83371

Closed
CyrusNajmabadi wants to merge 30 commits intodotnet:mainfrom
CyrusNajmabadi:extension-members-on-typeless-receivers/NullLiteral/ClassicExtensionMethod
Closed

Extension members on typeless receivers - PR 23: NullLiteral x ClassicExtensionMethod tests#83371
CyrusNajmabadi wants to merge 30 commits intodotnet:mainfrom
CyrusNajmabadi:extension-members-on-typeless-receivers/NullLiteral/ClassicExtensionMethod

Conversation

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Apr 25, 2026

Stacked on #83370. Five tests covering null-literal receivers: null to reference type, null to nullable value type, overload resolution prefers more specific, ambiguity between unrelated reference types, no candidate in scope.

This pull request and its description were written by Isaac.

Microsoft Reviewers: Open in CodeFlow

Cyrus Najmabadi added 30 commits April 25, 2026 15:32
…onversionFromExpression for typeless source
…rted forms are not forced through BindToNaturalType
…llection expressions

The collection-expression smoke test was Skip'd because compiling
[1, 2, 3].CountIt() triggered AssertUnderlyingConversionsCheckedRecursive
in Binder_Conversions.cs. The receiver's BoundUnconvertedCollectionExpression
was being destructively converted by ReplaceTypeOrValueReceiver's default
branch (which calls BindToNaturalType -> BindCollectionExpressionForErrorRecovery)
before reaching coerceArgument, so the line-274 early-return that handles
collection-expression conversions correctly never fired.

Skip ReplaceTypeOrValueReceiver in BindInvocationExpressionContinued when the
receiver is typeless and the call is invokedAsExtensionMethod. Safe because
BindToNaturalType is a no-op for typed receivers (NeedsToBeConverted early
return) and ReplaceTypeOrValueReceiver's named purpose - replacing
TypeOrValueExpression or QueryClause - never applies here since both wrappers
always have a type.

Scope TryBindMemberAccessOnTypelessReceiver's inclusion list to just
UnconvertedCollectionExpression for Phase 1. The other receiver categories
(target-typed new(), conditional/switch with no common arm type, lambda
without natural type, method group, tuple literal with typeless element,
default literal, null literal) will be enabled in their respective Phase 2
area PRs together with their test coverage. Until then, those categories
keep producing their pre-feature diagnostics.

Un-skip CollectionExpression_ClassicExtensionMethod_Executes (now passes).
Skip NullLiteral and Lambda smoke tests with a TODO referencing the relevant
Phase 2 area.

Co-authored-by: Isaac
…eceiver feature

The extension-members-on-typeless-receivers feature changes the meaning of 8
existing tests in CollectionExpressionTests:

* TypeInference_07/08/NullableValueType_ExtensionMethod: [...]ExtensionMethod()
  now succeeds via type inference through the collection-expression conversion,
  instead of producing ERR_CollectionExpressionNoTargetType on the receiver.

* TypeInference_09: [4].AsCollection() now reports the same
  ERR_CantInferMethTypeArgs as the explicit-form call, instead of
  ERR_CollectionExpressionNoTargetType on the receiver.

* MemberAccess_01/02/03/04: [].GetHashCode() (direct '.' on a typeless
  collection-expression receiver) now enters extension-method search and
  reports ERR_NoSuchMember, instead of ERR_CollectionExpressionNoTargetType.
  The '?.' and '[]' forms are out of scope per the proposal and continue to
  produce ERR_CollectionExpressionNoTargetType.

A follow-up PR (Phase 2 CollectionExpression area) should add C#14 variants
of these tests so both pre-feature and post-feature behavior is documented.

Co-authored-by: Isaac
…receiver feature

Eighteen consolidated tests covering classic extension methods invoked on a
collection-expression receiver. Confidence-focused, not exhaustive. Categories:

  - Target type variety: array, List<T>, IEnumerable<T>, IReadOnlyList<T>.
  - Edge cases: empty collection (T cannot be inferred), spread, nested
    collection expressions, string element type.
  - Generic inference: receiver and additional arguments, struct constraint
    (satisfied and violated), two-parameter generic.
  - Negative: receiver-type-not-constructible, no-extension-in-scope.
  - Overload resolution: ambiguity between equally-applicable candidates,
    preference for the more specific candidate (T[] over IEnumerable<T>).
  - Chained: typeless-receiver call followed by typed-receiver call.
  - Argument forwarding: positional and named additional arguments.

Three tests use CompileAndVerify with expectedOutput to confirm the lowered
code executes correctly, not just that it binds.

Co-authored-by: Isaac
…l modern path is wired up)

Probed whether modern (C# 14) extension members declared inside an
`extension(T) { ... }` block bind on a typeless collection-expression
receiver. They do not: the classic path runs through BindInstanceMemberAccess
which TryBindMemberAccessOnTypelessReceiver routes to, but the modern path
runs through GetExtensionMemberAccess, which does not yet honor a typeless
receiver.

Two tests document the current ERR_CollectionExpressionNoTargetType behavior
so the file is green. Both have a TODO referencing the future flip to
asserting successful binding once the modern path is enabled.

Co-authored-by: Isaac
Phase 1 only relaxed the receiver-conversion path for classic extension
methods. C#14 modern extension members declared inside an extension(T) { ... }
block use two adjacent code paths that also called ReplaceTypeOrValueReceiver
on the receiver, which destructively converts typeless forms (BoundUnconverted
CollectionExpression, etc.) into error-recovery wrappers and reports
ERR_CollectionExpressionNoTargetType:

  1. BindInvocationExpressionContinued (modern method invocation): line 1242.
     Phase 1 gated the relaxation on invokedAsExtensionMethod, but the modern
     method path resets invokedAsExtensionMethod to false above, so the
     relaxation didn't apply.

  2. GetExtensionMemberAccess (modern property and indexer access): line 8265.
     Same shape, no analogous relaxation.

For both call sites: skip ReplaceTypeOrValueReceiver when the receiver has no
type. The downstream conversion is then applied by CheckAndConvertExtensionReceiver
against the extension's declared receiver parameter, just like for typed
receivers. ReplaceTypeOrValueReceiver's named purpose (replacing
TypeOrValueExpression or unwrapping QueryClause) does not apply since both
of those wrappers always have a type.

Flips the two placeholder tests in
CollectionExpression_ModernExtensionMethod_Tests.cs (added in the prior PR in
this stack) from documenting the limitation to asserting successful binding
(CompileAndVerify with expectedOutput "6" and "3").

All eight .NET Core CSharp test projects pass with zero failures.

Co-authored-by: Isaac
…-receiver feature

Eight tests covering C#14 modern extension properties accessed on a
collection-expression receiver. Confidence-focused, not exhaustive.

  - Basic property access on IEnumerable<T> with execution.
  - Generic extension property with execution (Count<T>).
  - Get-only property on IReadOnlyList<T>, on int[], on IEnumerable<string>.
  - Chained property access (typeless receiver -> typed result -> typed access).
  - Spread elements feeding the collection-expression receiver.
  - Negative: no candidate in scope reports ERR_NoSuchMember.

Six tests use CompileAndVerify with expectedOutput to confirm the lowered code
executes correctly, not just that it binds.

A ninth test for empty-collection + generic extension property is deferred:
it triggers a Debug.Fail in OverloadResolutionResult.TypeInferenceFailed
during property-resolution error reporting (non-serializable argument). That
diagnostic-reporting bug also affects typed receivers in the same scenario,
so it's tracked separately and out of scope for this PR.

Co-authored-by: Isaac
… pins)

Modern extension indexers and the typeless-receivers feature don't currently
meet on either side:

  - Instance indexers are not allowed inside extension(T) { ... } blocks
    (CS9282 ERR_ExtensionDisallowsMember). So no instance indexer can be
    reached through the typeless-receiver path.

  - Element access via expr[args] on a typeless receiver is explicitly out
    of scope per the proposal: dot-form only.

Two tests pin those facts so a future change to either makes the failure
surface and we can revisit.

Co-authored-by: Isaac
Adds BoundKind.UnboundLambda to TryBindMemberAccessOnTypelessReceiver's
inclusion list so that member access on a lambda whose natural type cannot
be inferred (per the C#10 / C#13 natural-type rules) routes through the
typeless-extension-receiver path. The headline scenario is the Memoize
example from the proposal:

  (x => x * 2).Apply(5)

where Apply is an extension method on the inferred delegate type.

Un-skips the corresponding smoke test (Lambda_ClassicExtensionMethod_Executes).
The eight .NET Core CSharp test projects pass with zero failures - existing
tests of "(arg => ...).ToString()"-style errors continue to pass because
lambdas whose member access is downstream of a target-typed assignment do
not enter this path as BoundUnboundLambda.

Co-authored-by: Isaac
Nine consolidated tests covering classic extension methods invoked on a
lambda (without natural type) receiver. Confidence-focused, not exhaustive.

  - Func<int,int>, Action, Func<int,int,int> targets with execution.
  - Lambda with explicit typed parameter still routes through the typeless
    path until the extension binds it to a delegate.
  - Generic extension Apply<T>(Func<T,T>) inferred from the lambda body.
  - Negative: no candidate in scope reports ERR_NoSuchMember on
    'lambda expression'.
  - Overload resolution: ambiguity, preference for the more specific
    candidate (Func<int,int> over Delegate).
  - Headline: Memoize over a lambda, returning a memoized Func<int,int>.

Six tests use CompileAndVerify with expectedOutput.

Co-authored-by: Isaac
Four consolidated tests covering C#14 modern extension methods (declared
inside extension(T) { ... } blocks) invoked on a lambda receiver.

  - extension(Func<int,int>) Apply with execution.
  - Generic extension<T>(Func<T,T>) Apply with execution.
  - extension(Action) RunIt with execution.
  - Negative: no candidate in scope reports ERR_NoSuchMember.

Three tests use CompileAndVerify with expectedOutput.

Co-authored-by: Isaac
Three tests covering modern extension properties accessed on a lambda
receiver: Func<int,int>, generic Func<T,T>, and a negative no-candidate
case. Two tests use CompileAndVerify with expectedOutput.

Co-authored-by: Isaac
Same pattern as CollectionExpression_ModernExtensionIndexer_Tests:

  - Instance indexers in an extension(T) { ... } block on Func<int,int>
    report CS9282 ERR_ExtensionDisallowsMember.
  - `(x => x)[0]` element access on a lambda is rejected with
    ERR_BadIndexLHS; the typeless-receivers proposal does not extend `[]`.

Closes the Lambda area (4 of 4 shape PRs).

Co-authored-by: Isaac
Adds BoundKind.MethodGroup (filtered to non-empty / non-errored method groups)
to TryBindMemberAccessOnTypelessReceiver's inclusion list. This enables the
Memoize-on-method-group example from the proposal:

  Square.RunIt(5)  // where RunIt is an extension on Func<int,int>

The filter (Methods.Length > 0 && LookupError is null) excludes empty /
errored method groups produced when an inaccessible nested-type lookup or a
similar failed lookup falls through to extension-method search. Without
this filter, scenarios such as `I1.T4.B` where T4 is an inaccessible enum
would produce ERR_FeatureInPreview instead of preserving ERR_BadAccess.

All eight .NET Core CSharp test projects pass with zero failures.

Co-authored-by: Isaac
…feature

Six consolidated tests covering classic extension methods invoked on a
method-group receiver, the proposal's Memoize headline scenario.

  - Static method group as receiver of an extension on Func<int,int>.
  - Instance method group as receiver.
  - Generic extension Apply<T>(Func<T,T>) inferred from the method group.
  - Memoize over a method group, returning a memoized Func<int,int>.
  - Overloaded method group (no natural type) target-typed against the
    extension's first parameter type.
  - Negative: no candidate in scope reports ERR_NoSuchMember on
    'method group'.

Five tests use CompileAndVerify with expectedOutput.

Co-authored-by: Isaac
…eature

Three tests covering modern (C#14) extension methods invoked on a method-group
receiver:

  - extension(Func<int,int>) RunIt with execution.
  - extension<T>(Func<T,T>) Apply with execution.
  - Negative: no candidate in scope reports ERR_NoSuchMember.

Two tests use CompileAndVerify with expectedOutput.

Co-authored-by: Isaac
… feature

Two tests covering modern extension properties accessed on a method-group
receiver: one positive (extension(Func<int,int>) Property with execution),
one negative (no candidate in scope). A generic-property test is deferred
with TODO referencing the same diagnostic-reporting Debug.Fail seen in
the earlier empty-collection + generic-property scenario.

Co-authored-by: Isaac
Two pin tests, same pattern as CollectionExpression / Lambda indexer PRs:
instance indexers in extension(T) blocks are not allowed (CS9282), and
element access via [] on a method-group receiver continues to report
ERR_BadIndexLHS.

Closes the MethodGroup area (4 of 4 shape PRs).

Co-authored-by: Isaac
Adds BoundKind.UnconvertedObjectCreationExpression to TryBindMemberAccessOnTypelessReceiver's
inclusion list so that member access on `new()` / `new(args)` routes through the typeless-
extension-receiver path. Headline scenario:

  new(arg).SomeExtension()

where SomeExtension is an extension on the inferred receiver type.

All eight .NET Core CSharp test projects pass with zero failures.

This is the support PR for the NewExpression area; the four shape PRs follow.

Co-authored-by: Isaac
…r feature

Four tests covering classic extension methods invoked on a target-typed
new() / new(args) receiver:

  - new() with parameterless ctor + extension on the type, with execution.
  - new(arg) with single-arg ctor, with execution.
  - new(a, b) with multi-arg ctor + ctor overload resolution, with execution.
  - Negative: no candidate in scope reports ERR_NoSuchMember on 'new()'.

Three tests use CompileAndVerify with expectedOutput.

Co-authored-by: Isaac
… feature

Three tests covering modern (C#14) extension methods invoked on a target-typed
new() / new(args) receiver. Two execute, one negative.

Co-authored-by: Isaac
…er feature

Three tests covering modern extension properties accessed on a target-typed
new() / new(args) receiver. Two execute, one negative.

Co-authored-by: Isaac
Two pin tests, same pattern as previous indexer PRs. Closes the NewExpression area.

Co-authored-by: Isaac
Adds BoundLiteral with constant-value null to TryBindMemberAccessOnTypelessReceiver's
inclusion list. Un-skips the corresponding smoke test (NullLiteral_ClassicExtensionMethod_Executes).

All eight .NET Core CSharp test projects pass with zero failures.

Co-authored-by: Isaac
…feature

Five tests covering classic extension methods invoked on a null-literal receiver:
null to reference type, null to nullable value type, overload resolution prefers
the more specific candidate, ambiguity between two unrelated reference types,
no candidate in scope.

Three tests use CompileAndVerify with expectedOutput.

Co-authored-by: Isaac
@dotnet-policy-service dotnet-policy-service Bot added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Apr 25, 2026
@CyrusNajmabadi
Copy link
Copy Markdown
Contributor Author

Consolidated.

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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant