@@ -67,64 +67,133 @@ x is IPropertySymbol xProperty &&
6767 } ) . ToList ( ) ;
6868
6969 // Expression-property candidates: a property returning Expression<TDelegate>.
70- // Supported in the generator only when the projectable member is a method.
71- // When the projectable member is a property, the runtime resolver handles it.
72- var exprPropertyCandidates = memberSymbol is IMethodSymbol
73- ? allCandidates . Where ( IsExpressionDelegateProperty ) . ToList ( )
74- : [ ] ;
75-
76- // Filter Expression<TDelegate> candidates whose Func generic-argument count is
77- // compatible with the projectable method's parameter list.
70+ // Supported in the generator for both projectable methods and projectable properties.
71+ var exprPropertyCandidates = allCandidates . Where ( IsExpressionDelegateProperty ) . ToList ( ) ;
72+
73+ // Filter Expression<TDelegate> candidates whose Func generic-argument count, return type,
74+ // and parameter types are all compatible with the projectable member's signature.
7875 List < ISymbol > compatibleExprPropertyCandidates = [ ] ;
79- if ( exprPropertyCandidates . Count > 0 && memberSymbol is IMethodSymbol exprCheckMethod )
76+ if ( exprPropertyCandidates . Count > 0 )
8077 {
81- var isExtensionBlock = memberSymbol . ContainingType is { IsExtension : true } ;
82- var hasImplicitThis = ! exprCheckMethod . IsStatic || isExtensionBlock ;
83- var expectedFuncArgCount = exprCheckMethod . Parameters . Length + ( hasImplicitThis ? 2 : 1 ) ;
84-
85- compatibleExprPropertyCandidates = exprPropertyCandidates . Where ( x =>
78+ if ( memberSymbol is IMethodSymbol exprCheckMethod )
8679 {
87- if ( x is not IPropertySymbol propSym )
88- {
89- return false ;
90- }
80+ var isExtensionBlock = memberSymbol . ContainingType is { IsExtension : true } ;
81+ var hasImplicitThis = ! exprCheckMethod . IsStatic || isExtensionBlock ;
82+ var expectedFuncArgCount = exprCheckMethod . Parameters . Length + ( hasImplicitThis ? 2 : 1 ) ;
9183
92- if ( propSym . Type is not INamedTypeSymbol exprType || exprType . TypeArguments . Length != 1 )
84+ // Determine the expected receiver type when hasImplicitThis is true.
85+ // For extension-block members the receiver is the extension parameter's type;
86+ // for ordinary instance methods it is the containing type.
87+ ITypeSymbol ? expectedReceiverType = null ;
88+ if ( hasImplicitThis )
9389 {
94- return false ;
90+ expectedReceiverType = isExtensionBlock
91+ ? exprCheckMethod . ContainingType . ExtensionParameter ? . Type
92+ : exprCheckMethod . ContainingType ;
9593 }
9694
97- if ( exprType . TypeArguments [ 0 ] is not INamedTypeSymbol delegateType )
95+ compatibleExprPropertyCandidates = exprPropertyCandidates . Where ( x =>
9896 {
99- return false ;
100- }
97+ if ( x is not IPropertySymbol propSym )
98+ {
99+ return false ;
100+ }
101101
102- return delegateType . TypeArguments . Length == expectedFuncArgCount ;
103- } ) . ToList ( ) ;
104- }
102+ if ( propSym . Type is not INamedTypeSymbol exprType || exprType . TypeArguments . Length != 1 )
103+ {
104+ return false ;
105+ }
105106
106- // Step 3: if no generator-handled candidates exist, diagnose or skip
107- if ( regularCompatible . Count == 0 && compatibleExprPropertyCandidates . Count == 0 )
108- {
109- // Expression properties were found but all have incompatible Func signatures.
110- if ( exprPropertyCandidates . Count > 0 )
111- {
112- context . ReportDiagnostic ( Diagnostic . Create (
113- Diagnostics . UseMemberBodyIncompatible ,
114- member . GetLocation ( ) ,
115- memberSymbol . Name ,
116- useMemberBody ) ) ;
117- return null ;
118- }
107+ if ( exprType . TypeArguments [ 0 ] is not INamedTypeSymbol delegateType )
108+ {
109+ return false ;
110+ }
111+
112+ if ( delegateType . TypeArguments . Length != expectedFuncArgCount )
113+ {
114+ return false ;
115+ }
116+
117+ // Receiver-type check: when hasImplicitThis is true, TypeArguments[0] must match
118+ // the implicit receiver — the containing type for instance methods, or the extension
119+ // receiver type for extension-block members.
120+ if ( hasImplicitThis && expectedReceiverType is not null )
121+ {
122+ if ( ! comparer . Equals ( delegateType . TypeArguments [ 0 ] , expectedReceiverType ) )
123+ {
124+ return false ;
125+ }
126+ }
127+
128+ // Return-type check: the last type argument of the delegate must match the method's return type.
129+ var delegateReturnType = delegateType . TypeArguments [ delegateType . TypeArguments . Length - 1 ] ;
130+ if ( ! comparer . Equals ( delegateReturnType , exprCheckMethod . ReturnType ) )
131+ {
132+ return false ;
133+ }
134+
135+ // Parameter-type checks: each explicit parameter type must match.
136+ // When hasImplicitThis is true, TypeArguments[0] is the implicit receiver — skip it.
137+ var paramOffset = hasImplicitThis ? 1 : 0 ;
138+ for ( var i = 0 ; i < exprCheckMethod . Parameters . Length ; i ++ )
139+ {
140+ if ( ! comparer . Equals ( delegateType . TypeArguments [ paramOffset + i ] , exprCheckMethod . Parameters [ i ] . Type ) )
141+ {
142+ return false ;
143+ }
144+ }
119145
120- // A projectable *property* backed by an Expression<TDelegate> property is
121- // handled at runtime by ProjectionExpressionResolver; skip silently so the
122- // runtime path can take over without a spurious error.
123- if ( memberSymbol is IPropertySymbol && allCandidates . Any ( IsExpressionDelegateProperty ) )
146+ return true ;
147+ } ) . ToList ( ) ;
148+ }
149+ else if ( memberSymbol is IPropertySymbol exprCheckProperty )
124150 {
125- return null ;
151+ // Instance property: Func<ContainingType, PropType> — 2 type arguments.
152+ // Static property: Func<PropType> — 1 type argument.
153+ var expectedFuncArgCount = exprCheckProperty . IsStatic ? 1 : 2 ;
154+
155+ compatibleExprPropertyCandidates = exprPropertyCandidates . Where ( x =>
156+ {
157+ if ( x is not IPropertySymbol propSym )
158+ {
159+ return false ;
160+ }
161+
162+ if ( propSym . Type is not INamedTypeSymbol exprType || exprType . TypeArguments . Length != 1 )
163+ {
164+ return false ;
165+ }
166+
167+ if ( exprType . TypeArguments [ 0 ] is not INamedTypeSymbol delegateType )
168+ {
169+ return false ;
170+ }
171+
172+ if ( delegateType . TypeArguments . Length != expectedFuncArgCount )
173+ {
174+ return false ;
175+ }
176+
177+ // For instance properties, the first delegate type argument must be the containing type
178+ // (the implicit receiver).
179+ if ( ! exprCheckProperty . IsStatic )
180+ {
181+ var receiverType = delegateType . TypeArguments [ 0 ] ;
182+ if ( ! comparer . Equals ( receiverType , exprCheckProperty . ContainingType ) )
183+ {
184+ return false ;
185+ }
186+ }
187+ // Return-type check: the last type argument of the delegate must match the property type.
188+ var delegateReturnType = delegateType . TypeArguments [ delegateType . TypeArguments . Length - 1 ] ;
189+ return comparer . Equals ( delegateReturnType , exprCheckProperty . Type ) ;
190+ } ) . ToList ( ) ;
126191 }
192+ }
127193
194+ // Step 3: if no generator-handled candidates exist, diagnose
195+ if ( regularCompatible . Count == 0 && compatibleExprPropertyCandidates . Count == 0 )
196+ {
128197 context . ReportDiagnostic ( Diagnostic . Create (
129198 Diagnostics . UseMemberBodyIncompatible ,
130199 member . GetLocation ( ) ,
@@ -165,6 +234,9 @@ x is IPropertySymbol xProperty &&
165234 // These don't need to share the member's static modifier because a
166235 // static Expression<Func<...>> property can legitimately back either
167236 // a static or an instance projectable method.
237+ // They are also allowed to live in a different file (e.g. a split partial class):
238+ // a direct lambda body can be extracted purely syntactically without a shared
239+ // SemanticModel; the runtime fallback handles any cases the generator can't inline.
168240 if ( resolvedBody is null && compatibleExprPropertyCandidates . Count > 0 )
169241 {
170242 resolvedBody = compatibleExprPropertyCandidates
@@ -173,7 +245,7 @@ x is IPropertySymbol xProperty &&
173245 . OfType < MemberDeclarationSyntax > ( )
174246 . FirstOrDefault ( x =>
175247 {
176- if ( x == null || x . SyntaxTree != member . SyntaxTree )
248+ if ( x == null )
177249 {
178250 return false ;
179251 }
@@ -198,25 +270,17 @@ x is IPropertySymbol xProperty &&
198270 }
199271
200272 /// <summary>Returns true when a <see cref="PropertyDeclarationSyntax"/> has a readable body.</summary>
201- private static bool HasReadablePropertyBody ( PropertyDeclarationSyntax xProp )
273+ private static bool HasReadablePropertyBody ( PropertyDeclarationSyntax prop )
202274 {
203- if ( xProp . ExpressionBody is not null )
275+ if ( prop . ExpressionBody is not null )
204276 {
205277 return true ;
206278 }
207279
208- if ( xProp . AccessorList is not null )
209- {
210- var getter = xProp . AccessorList . Accessors
211- . FirstOrDefault ( a => a . IsKind ( SyntaxKind . GetAccessorDeclaration ) ) ;
212-
213- if ( getter ? . ExpressionBody is not null || getter ? . Body is not null )
214- {
215- return true ;
216- }
217- }
218-
219- return false ;
280+ var getter = prop . AccessorList ? . Accessors
281+ . FirstOrDefault ( a => a . IsKind ( SyntaxKind . GetAccessorDeclaration ) ) ;
282+
283+ return getter ? . ExpressionBody is not null || getter ? . Body is not null ;
220284 }
221285
222286 /// <summary>Returns true when a symbol is a property returning <c>Expression<TDelegate></c>.</summary>
0 commit comments