Skip to content

[typeless extension receivers] Tests for method-group receivers#83399

Draft
CyrusNajmabadi wants to merge 100 commits intodotnet:features/extension-members-on-typeless-receiversfrom
CyrusNajmabadi:extension-members-on-typeless-receivers/MethodGroup/Tests
Draft

[typeless extension receivers] Tests for method-group receivers#83399
CyrusNajmabadi wants to merge 100 commits intodotnet:features/extension-members-on-typeless-receiversfrom
CyrusNajmabadi:extension-members-on-typeless-receivers/MethodGroup/Tests

Conversation

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Apr 25, 2026

Championed issue: TBD
Speclet: TBD
Test plan: TBD

Summary

  • Four test files covering the four shape combinations on a method-group receiver.
  • Headline: Memoize-on-method-group; also static / instance method groups, generic extension inference, overloaded method group with no natural type.

Stacked:

  1. [typeless extension receivers] Initial binder support #83348
  2. [typeless extension receivers] Modern (C#14) extension support #83352
  3. [typeless extension receivers] Tests for collection-expression receivers #83397
  4. [typeless extension receivers] Allow lambda receivers #83355
  5. [typeless extension receivers] Tests for lambda receivers #83398
  6. [typeless extension receivers] Allow method-group receivers #83360
  7. [typeless extension receivers] Tests for method-group receivers #83399 (this PR)
  8. [typeless extension receivers] Allow target-typed new() receivers #83365
  9. [typeless extension receivers] Tests for new() / new(args) receivers #83400
  10. [typeless extension receivers] Allow null-literal receivers #83370
  11. [typeless extension receivers] Tests for null-literal receivers #83401
  12. [typeless extension receivers] Allow default-literal receivers #83375
  13. [typeless extension receivers] Tests for default-literal receivers #83402
  14. [typeless extension receivers] Allow conditional-expression receivers #83380
  15. [typeless extension receivers] Tests for conditional-expression receivers #83403
  16. [typeless extension receivers] Allow switch-expression receivers #83385
  17. [typeless extension receivers] Tests for switch-expression receivers #83404
  18. [typeless extension receivers] Allow tuple-literal receivers #83390
  19. [typeless extension receivers] Tests for tuple-literal receivers #83405
  20. [typeless extension receivers] Pin tests excluding throw expressions #83406
  21. [typeless extension receivers] Fix Debug.Assert in dynamic-arg extension diagnostic #83414
  22. [typeless extension receivers] Allow extension indexers #83415
  23. [typeless extension receivers] Tests for extension indexers #83416

Cyrus Najmabadi added 22 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
Consolidates the four shape PRs for the CollectionExpression area into a
single Tests PR. The area covers extension members invoked on a typeless
collection-expression receiver. All four shape combinations:

  - ClassicExtensionMethod: 18 tests (target type variety, edge cases,
    generic inference, overload resolution, chained, argument forwarding).
  - ModernExtensionMethod: 2 execution tests.
  - ModernExtensionProperty: 8 tests (instance properties on various
    target types, chained access, spread elements).
  - ModernExtensionIndexer: 2 pin tests (instance indexers in extension(T)
    blocks are not allowed; element access via [] on a typeless receiver
    is out of scope per the proposal).

Note: Classic and ModernMethod test files originate earlier in this PR's
stack lineage. The Property and Indexer files are added in this commit.
The cumulative diff vs main shows all four area test files.

Co-authored-by: Isaac
Consolidates four shape PRs (formerly dotnet#83356, dotnet#83357, dotnet#83358, dotnet#83359) into
a single tests PR for the Lambda area. Covers extension members invoked on
a lambda receiver:

  - ClassicExtensionMethod: 9 tests including the Memoize headline.
  - ModernExtensionMethod: 4 tests on Func<int,int> / Action / Func<T,T>.
  - ModernExtensionProperty: 3 tests.
  - ModernExtensionIndexer: 2 pin tests.

Co-authored-by: Isaac
Consolidates four shape PRs (formerly dotnet#83361, dotnet#83362, dotnet#83363, dotnet#83364)
into a single tests PR for the MethodGroup area. Covers extension members
invoked on a method-group receiver - the proposal's Memoize headline:

  - ClassicExtensionMethod: 6 tests (static / instance / generic /
    Memoize / overloaded-with-no-natural-type / negative).
  - ModernExtensionMethod: 3 tests.
  - ModernExtensionProperty: 2 tests.
  - ModernExtensionIndexer: 2 pin tests.

Co-authored-by: Isaac
Cyrus Najmabadi added 23 commits April 26, 2026 12:09
…behavior

When TryBindMemberAccessOnTypelessReceiver started handling MethodGroup
receivers, two pre-existing tests with applicable extension methods in
scope (using System.Linq's Select, and a custom Action-applicable
extension) now route through the new feature path:

- QueryTests.MethodGroupInFromClause: `Main.Select(...)` now reports
  ERR_CantInferMethTypeArgs (the typeless method group binds against
  the Linq.Queryable extension) instead of the pre-feature
  ERR_BadSKunknown.
- Symbols.ExtensionMethodTests.Delegates: `S.E.G();` (a static method
  group as receiver of extension `G(this Action<object>)`) now binds
  successfully because E converts to Action<object>; the third
  ERR_BadSKunknown drops out.

Co-authored-by: Isaac
…port' into extension-members-on-typeless-receivers/MethodGroup/Tests
- Collapse the typeArgumentsSyntax / typeArgumentsWithAnnotations conditional
  expressions onto single lines using property-pattern matching.
- Move the type-args computation below HasExtensionMemberCandidateInScope so
  we don't pay BindTypeArguments when speculation returns null.

Co-authored-by: Isaac
The original comment claimed the helper used "the same gate" as
GetMethodGroupDelegateType, but the natural-type code inlines the
ResultKind == Viable check as a guard around its instance-methods loop
(in GetUniqueSignatureFromMethodGroup) and adds a signature-uniqueness
check on top. They share the conceptual viability gate, not the full
logic. Be precise about which method contains the corresponding inlined
check.

Co-authored-by: Isaac
…xtension-members-on-typeless-receivers/ModernExtensionsBinderSupport
…sBinderSupport' into extension-members-on-typeless-receivers/CollectionExpression/Tests
…ssion/Tests' into extension-members-on-typeless-receivers/Lambda/Support
…nto extension-members-on-typeless-receivers/MethodGroup/Support
… into extension-members-on-typeless-receivers/Lambda/Tests
…port' into extension-members-on-typeless-receivers/MethodGroup/Tests
The helper was a single boolean expression; inlining it as a property
pattern (`when boundLeft is BoundMethodGroup { ResultKind: Viable,
Methods.Length: > 0 }`) keeps the switch self-contained and removes
indirection. The reasoning that previously lived in the helper's doc
comment moves to a comment above the case.

Co-authored-by: Isaac
…port' into extension-members-on-typeless-receivers/MethodGroup/Tests
For the new code in TryBindMemberAccessOnTypelessReceiver and
HasExtensionMemberCandidateInScope:

- Single-line condition + single-line body if-statements no longer wrap
  the body in braces.
- Block-like statements are followed by a blank line unless they are the
  last statement before a closing brace.

Co-authored-by: Isaac
…xtension-members-on-typeless-receivers/ModernExtensionsBinderSupport
…sBinderSupport' into extension-members-on-typeless-receivers/CollectionExpression/Tests
…ssion/Tests' into extension-members-on-typeless-receivers/Lambda/Support
… into extension-members-on-typeless-receivers/Lambda/Tests
…nto extension-members-on-typeless-receivers/MethodGroup/Support
…port' into extension-members-on-typeless-receivers/MethodGroup/Tests
When the speculative-binding redesign (in /product) made the typeless-
receiver helper only engage when an extension candidate is in scope,
these tests' "no extension found" assertions started reporting the
legacy ERR_BadUnaryOp from the UnboundLambda rejection in
BindMemberAccessWithBoundLeft instead of the new ERR_NoSuchMember.

Updated three tests across Classic / ModernMethod / ModernProperty.

Co-authored-by: Isaac
…nto extension-members-on-typeless-receivers/MethodGroup/Support
…port' into extension-members-on-typeless-receivers/MethodGroup/Tests
…llback

Three method-group "no extension found" tests now expect the legacy
ERR_BadSKunknown ('method is not valid in the given context') instead of
ERR_NoSuchMember, since the speculative-binding redesign falls back to
the legacy path when no extension candidate is in scope.

Co-authored-by: Isaac
@CyrusNajmabadi CyrusNajmabadi changed the title Add tests for extension members on typeless method-group receivers [typeless extension receivers] Tests for method-group receivers Apr 26, 2026
@CyrusNajmabadi CyrusNajmabadi changed the base branch from main to features/extension-members-on-typeless-receivers April 27, 2026 16:42
@jcouv
Copy link
Copy Markdown
Member

jcouv commented Apr 27, 2026

Test plan: #83428
(created by new-compiler-feature skill)

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. Feature - Extension members on typeless receivers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants