@@ -131,15 +131,20 @@ private static void AnalyzeAnonymousFunction(OperationAnalysisContext context)
131131 }
132132
133133 // For lambdas/anonymous functions we do NOT enforce "must have CT" (PT0002),
134- // because the delegate signature is usually dictated by an external API
135- // (Hangfire, ASP.NET, minimal APIs binder, etc.).
134+ // because the delegate signature is usually dictated by an external API.
136135 // We only normalize name and position if a CT parameter already exists.
137136 var ctInfo = GetCancellationTokenInfo ( symbol ) ;
138137 if ( ! ctInfo . HasCt )
139138 {
140139 return ;
141140 }
142141
142+ // If the lambda is inside a test method, skip CT name/position completely.
143+ if ( IsInsideTestMethod ( anon ) )
144+ {
145+ return ;
146+ }
147+
143148 const string displayName = "anonymous function" ;
144149 var location = anon . Syntax . GetLocation ( ) ;
145150
@@ -173,6 +178,10 @@ private static void AnalyzeAsyncMember(IMethodSymbol method,
173178 Action < Diagnostic > report )
174179 {
175180 var isContract = method . IsContractImplementation ( ) ;
181+ var isTest = IsTestMethod ( method ) ;
182+
183+ // For CT missing / position we skip both contracts and tests.
184+ var skipCtMissingAndPosition = isContract || isTest ;
176185
177186 // PT0001 – name must end with Async (for named methods),
178187 // but we DO NOT enforce it on contract implementations (MediatR Handle, overrides, etc.).
@@ -186,10 +195,10 @@ private static void AnalyzeAsyncMember(IMethodSymbol method,
186195 var ctInfo = GetCancellationTokenInfo ( method ) ;
187196
188197 // PT0002 – missing CancellationToken
189- // Only enforced on non-contract methods (e.g. interface itself, normal class methods) .
198+ // Only enforced on non-contract, non-test methods.
190199 if ( ! ctInfo . HasCt )
191200 {
192- if ( ! isContract )
201+ if ( ! skipCtMissingAndPosition )
193202 {
194203 report ( Diagnostic . Create ( CancellationTokenMissingRule , location , displayName ) ) ;
195204 }
@@ -198,17 +207,15 @@ private static void AnalyzeAsyncMember(IMethodSymbol method,
198207 }
199208
200209 // PT0003 – name must be ct
201- // Enforced only on non-contract methods (interfaces / non-contract class methods).
202- // Implementations of contracts (overrides / interface impls) are skipped so we don't
203- // fight the “implementation params must match interface” rule or external contracts.
210+ // Enforced only on non-contract methods (including tests, per "maybe maximum naming").
204211 if ( ! ctInfo . IsNamedCt && ! isContract )
205212 {
206213 report ( Diagnostic . Create ( CancellationTokenNameRule , location , displayName , ctInfo . Name ) ) ;
207214 }
208215
209216 // PT0004 – CT must be last
210- // Only enforced on non-contract methods (interface / own class methods) .
211- if ( ! ctInfo . IsLast && ! isContract )
217+ // Only enforced on non-contract, non-test methods.
218+ if ( ! ctInfo . IsLast && ! skipCtMissingAndPosition )
212219 {
213220 report ( Diagnostic . Create ( CancellationTokenPositionRule , location , displayName , ctInfo . Name ) ) ;
214221 }
@@ -224,6 +231,49 @@ private static bool IsTaskLike(INamedTypeSymbol type)
224231 return type . Name is "Task" or "ValueTask" ;
225232 }
226233
234+ private static bool IsTestMethod ( IMethodSymbol method )
235+ {
236+ foreach ( var attr in method . GetAttributes ( ) )
237+ {
238+ var attrClass = attr . AttributeClass ;
239+ if ( attrClass is null )
240+ {
241+ continue ;
242+ }
243+
244+ var name = attrClass . Name ;
245+
246+ if ( name . EndsWith ( "Attribute" , StringComparison . Ordinal ) )
247+ {
248+ name = name . Substring ( 0 , name . Length - "Attribute" . Length ) ;
249+ }
250+
251+ if ( name is "Fact"
252+ or "Theory"
253+ or "Test"
254+ or "TestCase"
255+ or "TestMethod"
256+ or "DataTestMethod" )
257+ {
258+ return true ;
259+ }
260+ }
261+
262+ return false ;
263+ }
264+
265+
266+ private static bool IsInsideTestMethod ( IAnonymousFunctionOperation anon )
267+ {
268+ if ( anon . Symbol . ContainingSymbol is IMethodSymbol method )
269+ {
270+ return IsTestMethod ( method ) ;
271+ }
272+
273+ return false ;
274+ }
275+
276+
227277 private static CtInfo GetCancellationTokenInfo ( IMethodSymbol method )
228278 {
229279 IParameterSymbol ? ctParam = null ;
0 commit comments