Skip to content

Commit 18a11bb

Browse files
Copilotstephentoub
andcommitted
Address code review feedback: fix bugs and add comprehensive tests
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent a223c4b commit 18a11bb

File tree

3 files changed

+421
-150
lines changed

3 files changed

+421
-150
lines changed

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/AvoidRedundantRegexIsMatchBeforeMatch.cs

Lines changed: 74 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.VisualBasic.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/BasicAvoidRedundantRegexIsMatchBeforeMatch.Fixer.vb

Lines changed: 0 additions & 97 deletions
This file was deleted.

0 commit comments

Comments
 (0)