@@ -61,34 +61,23 @@ public override void Initialize(AnalysisContext context)
6161 return ;
6262 }
6363
64- // Look for Regex.Match calls in the appropriate branch
65- // For normal IsMatch, look in when-true; for negated (!IsMatch), look after the conditional
66- IOperation branchToSearch = isNegated ? conditional . WhenTrue : conditional . WhenTrue ;
67- var matchCalls = GetRegexMatchCalls ( branchToSearch , regexMatchSymbols ) ;
68-
69- // Check if any Match call has the same arguments as the IsMatch call
70- foreach ( var matchCall in matchCalls )
64+ // For normal IsMatch, look in when-true branch for corresponding Match call
65+ if ( ! isNegated )
7166 {
72- if ( AreInvocationArgumentsEqual ( isMatchCall , matchCall , context . Operation ) &&
73- AreInvocationsOnSameInstance ( isMatchCall , matchCall ) )
67+ if ( FindMatchCallInBranch ( conditional . WhenTrue , regexMatchSymbols , isMatchCall , context . Operation , out var matchCall ) )
7468 {
7569 context . ReportDiagnostic ( isMatchCall . CreateDiagnostic ( Rule ) ) ;
7670 return ;
7771 }
7872 }
79-
8073 // For negated IsMatch with early return pattern, check subsequent operations
81- if ( isNegated && IsEarlyReturnPattern ( conditional ) )
74+ else if ( IsEarlyReturnPattern ( conditional ) )
8275 {
8376 // Look for Match calls after the conditional in the parent block
84- if ( FindMatchCallAfterConditional ( conditional , regexMatchSymbols , out var subsequentMatchCall ) )
77+ if ( FindMatchCallAfterConditional ( conditional , regexMatchSymbols , isMatchCall , context . Operation , out var subsequentMatchCall ) )
8578 {
86- if ( AreInvocationArgumentsEqual ( isMatchCall , subsequentMatchCall , context . Operation ) &&
87- AreInvocationsOnSameInstance ( isMatchCall , subsequentMatchCall ) )
88- {
89- context . ReportDiagnostic ( isMatchCall . CreateDiagnostic ( Rule ) ) ;
90- return ;
91- }
79+ context . ReportDiagnostic ( isMatchCall . CreateDiagnostic ( Rule ) ) ;
80+ return ;
9281 }
9382 }
9483 } , OperationKind . Conditional ) ;
@@ -119,28 +108,59 @@ private static bool IsRegexIsMatchCall(IOperation condition, ImmutableArray<IMet
119108 return false ;
120109 }
121110
111+ private static bool FindMatchCallInBranch ( IOperation branch , ImmutableArray < IMethodSymbol > regexMatchSymbols , IInvocationOperation isMatchCall , IOperation conditionalOperation , out IInvocationOperation matchCall )
112+ {
113+ // Search for the first Match call that matches criteria
114+ return FindMatchCallRecursive ( branch , regexMatchSymbols , isMatchCall , conditionalOperation , out matchCall ) ;
115+ }
116+
117+ private static bool FindMatchCallRecursive ( IOperation operation , ImmutableArray < IMethodSymbol > regexMatchSymbols , IInvocationOperation isMatchCall , IOperation conditionalOperation , out IInvocationOperation matchCall )
118+ {
119+ if ( operation is IInvocationOperation invocation &&
120+ regexMatchSymbols . Contains ( invocation . TargetMethod , SymbolEqualityComparer . Default ) )
121+ {
122+ if ( AreInvocationArgumentsEqual ( isMatchCall , invocation , conditionalOperation ) &&
123+ AreInvocationsOnSameInstance ( isMatchCall , invocation , conditionalOperation ) )
124+ {
125+ matchCall = invocation ;
126+ return true ;
127+ }
128+ }
129+
130+ foreach ( var child in operation . Children )
131+ {
132+ if ( FindMatchCallRecursive ( child , regexMatchSymbols , isMatchCall , conditionalOperation , out matchCall ) )
133+ {
134+ return true ;
135+ }
136+ }
137+
138+ matchCall = null ! ;
139+ return false ;
140+ }
141+
122142 private static bool IsEarlyReturnPattern ( IConditionalOperation conditional )
123143 {
124- // Check if the when-true branch is an early return/ throw/continue/break
144+ // Check if the when-true branch is an early return or throw (not break /continue/goto)
125145 if ( conditional . WhenTrue is IBlockOperation block )
126146 {
127147 foreach ( var statement in block . Operations )
128148 {
129- if ( statement is IReturnOperation or IThrowOperation or IBranchOperation )
149+ if ( statement is IReturnOperation or IThrowOperation )
130150 {
131151 return true ;
132152 }
133153 }
134154 }
135- else if ( conditional . WhenTrue is IReturnOperation or IThrowOperation or IBranchOperation )
155+ else if ( conditional . WhenTrue is IReturnOperation or IThrowOperation )
136156 {
137157 return true ;
138158 }
139159
140160 return false ;
141161 }
142162
143- private static bool FindMatchCallAfterConditional ( IConditionalOperation conditional , ImmutableArray < IMethodSymbol > regexMatchSymbols , out IInvocationOperation matchCall )
163+ private static bool FindMatchCallAfterConditional ( IConditionalOperation conditional , ImmutableArray < IMethodSymbol > regexMatchSymbols , IInvocationOperation isMatchCall , IOperation conditionalOperation , out IInvocationOperation matchCall )
144164 {
145165 // Navigate to the parent block to find subsequent operations
146166 var parent = conditional . Parent ;
@@ -162,11 +182,9 @@ private static bool FindMatchCallAfterConditional(IConditionalOperation conditio
162182
163183 if ( foundConditional )
164184 {
165- // Look for Match calls in subsequent operations
166- var matchCalls = GetRegexMatchCalls ( operation , regexMatchSymbols ) ;
167- if ( matchCalls . Length > 0 )
185+ // Look for the first Match call that matches criteria
186+ if ( FindMatchCallRecursive ( operation , regexMatchSymbols , isMatchCall , conditionalOperation , out matchCall ) )
168187 {
169- matchCall = matchCalls [ 0 ] ;
170188 return true ;
171189 }
172190 }
@@ -177,28 +195,7 @@ private static bool FindMatchCallAfterConditional(IConditionalOperation conditio
177195 return false ;
178196 }
179197
180- private static ImmutableArray < IInvocationOperation > GetRegexMatchCalls ( IOperation operation , ImmutableArray < IMethodSymbol > regexMatchSymbols )
181- {
182- var builder = ImmutableArray . CreateBuilder < IInvocationOperation > ( ) ;
183- GetRegexMatchCallsRecursive ( operation , regexMatchSymbols , builder ) ;
184- return builder . ToImmutable ( ) ;
185- }
186-
187- private static void GetRegexMatchCallsRecursive ( IOperation operation , ImmutableArray < IMethodSymbol > regexMatchSymbols , ImmutableArray < IInvocationOperation > . Builder builder )
188- {
189- if ( operation is IInvocationOperation invocation &&
190- regexMatchSymbols . Contains ( invocation . TargetMethod , SymbolEqualityComparer . Default ) )
191- {
192- builder . Add ( invocation ) ;
193- }
194-
195- foreach ( var child in operation . Children )
196- {
197- GetRegexMatchCallsRecursive ( child , regexMatchSymbols , builder ) ;
198- }
199- }
200-
201- private static bool AreInvocationsOnSameInstance ( IInvocationOperation invocation1 , IInvocationOperation invocation2 )
198+ private static bool AreInvocationsOnSameInstance ( IInvocationOperation invocation1 , IInvocationOperation invocation2 , IOperation conditionalOperation )
202199 {
203200 var instance1 = invocation1 . Instance ? . WalkDownConversion ( ) ;
204201 var instance2 = invocation2 . Instance ? . WalkDownConversion ( ) ;
@@ -215,14 +212,39 @@ private static bool AreInvocationsOnSameInstance(IInvocationOperation invocation
215212 return false ;
216213 }
217214
218- return ( instance1 , instance2 ) switch
215+ // Check if instances are the same
216+ bool sameInstance = ( instance1 , instance2 ) switch
219217 {
220- ( IFieldReferenceOperation fieldRef1 , IFieldReferenceOperation fieldRef2 ) => SymbolEqualityComparer . Default . Equals ( fieldRef1 . Member , fieldRef2 . Member ) ,
221- ( IPropertyReferenceOperation propRef1 , IPropertyReferenceOperation propRef2 ) => SymbolEqualityComparer . Default . Equals ( propRef1 . Member , propRef2 . Member ) ,
222- ( IParameterReferenceOperation paramRef1 , IParameterReferenceOperation paramRef2 ) => SymbolEqualityComparer . Default . Equals ( paramRef1 . Parameter , paramRef2 . Parameter ) ,
223218 ( ILocalReferenceOperation localRef1 , ILocalReferenceOperation localRef2 ) => SymbolEqualityComparer . Default . Equals ( localRef1 . Local , localRef2 . Local ) ,
219+ ( IParameterReferenceOperation paramRef1 , IParameterReferenceOperation paramRef2 ) => SymbolEqualityComparer . Default . Equals ( paramRef1 . Parameter , paramRef2 . Parameter ) ,
220+ ( IFieldReferenceOperation fieldRef1 , IFieldReferenceOperation fieldRef2 ) when
221+ fieldRef1 . Member is IFieldSymbol field1 && fieldRef2 . Member is IFieldSymbol field2 && field1 . IsReadOnly && field2 . IsReadOnly =>
222+ SymbolEqualityComparer . Default . Equals ( fieldRef1 . Member , fieldRef2 . Member ) ,
224223 _ => false ,
225224 } ;
225+
226+ if ( ! sameInstance )
227+ {
228+ return false ;
229+ }
230+
231+ // For locals and parameters, check if they're modified between calls
232+ if ( instance1 is ILocalReferenceOperation localRef )
233+ {
234+ if ( conditionalOperation is IConditionalOperation conditional )
235+ {
236+ return ! HasAssignmentToSymbol ( localRef . Local , conditional . WhenTrue ) ;
237+ }
238+ }
239+ else if ( instance1 is IParameterReferenceOperation paramRef )
240+ {
241+ if ( conditionalOperation is IConditionalOperation conditional )
242+ {
243+ return ! HasAssignmentToSymbol ( paramRef . Parameter , conditional . WhenTrue ) ;
244+ }
245+ }
246+
247+ return true ;
226248 }
227249
228250 private static bool AreInvocationArgumentsEqual ( IInvocationOperation invocation1 , IInvocationOperation invocation2 , IOperation conditionalOperation )
0 commit comments