[typeless extension receivers] Tests for method-group receivers#83399
Draft
CyrusNajmabadi wants to merge 100 commits intodotnet:features/extension-members-on-typeless-receiversfrom
Conversation
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
This was referenced Apr 25, 2026
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
Member
|
Test plan: #83428 |
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.
Championed issue: TBD
Speclet: TBD
Test plan: TBD
Summary
Stacked: