CA2028: Avoid redundant Regex.IsMatch before Regex.Match#54071
CA2028: Avoid redundant Regex.IsMatch before Regex.Match#54071danmoseley wants to merge 18 commits intodotnet:mainfrom
Conversation
Implements analyzer CA2028 that detects the pattern where Regex.IsMatch()
is used as a condition followed by Regex.Match() with the same arguments
in the if body, causing the regex engine to execute twice.
The C#-specific fixer transforms the pattern to use property pattern
matching: if (Regex.Match(...) is { Success: true } m) { ... }
Key features:
- IOperation-based analyzer (works for C# and VB)
- Semantic argument equivalence (locals, parameters, constants, readonly fields)
- Intervening write detection (bails if tracked symbols are modified)
- Instance method receiver verification
- Fixer gates on C# >= 8.0 (property patterns), first-statement guard,
and name collision detection for else branch
- 46 comprehensive tests including real-world patterns from GitHub
Addresses dotnet/runtime#111239. Successor to abandoned PR dotnet#51214.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ConstantPatternString_Flags, InstanceWithStartAtParameter_Flags, and MultipleMatchCallsInBody_FlagsFirst all have fixable patterns (Match is first statement, local declaration) but were only testing the analyzer. Convert them to use VerifyCodeFixCSharp9Async with proper fixedSource. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add 3 real-world-inspired tests: else-if chain, loop, const field - Change all 'diagnostic but no fix' C# tests to verify fixer produces no code changes (source -> source) - All 49 tests pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Analyzer: - Detect ref/out argument mutations of tracked symbols Fixer: - Reject fix when declared type is not var or exact Match type - Broaden name-collision check to patterns, foreach, catch, out-var - Check subsequent sibling statements for name conflicts Tests: - 8 new tests for ref/out, name conflicts, type declarations - All 57 tests passing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…source attributions - Fix timeout overload test to use local variable (method calls aren't stable operands) - Remove span overload test (framework lacks ReadOnlySpan<char> Regex APIs) - Fix AddMutableSymbol to recurse into IFieldReferenceOperation.Instance (Opus B1) - Add readonly-field receiver/argument reassignment tests - Remove source attributions from test comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ReadOnlySpan<char> IsMatch has no corresponding Match overload, so no diagnostic should fire. Uses ReferenceAssemblies.Net.Net70 to make the span APIs available in the test framework. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds the CA2028 analyzer and C# code fix to detect and fix redundant Regex.IsMatch(...) guards immediately followed by Regex.Match(...) with equivalent operands, reducing duplicated regex evaluation work across C# and VB (analyzer) with a C#-only fixer.
Changes:
- Introduces CA2028 analyzer (
IOperation-based) to detect redundantIsMatch→Matchpatterns with operand equivalence + intervening-write checks. - Adds a C# code fix to rewrite the pattern into
Regex.Match(...) is { Success: true } mwhen safe. - Adds extensive unit tests plus rule metadata/resource updates (resx/xlf), documentation, and SARIF entries.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Runtime/AvoidRedundantRegexIsMatchBeforeMatchTests.cs | Adds CA2028 analyzer/fixer coverage across many positive/negative/edge scenarios (incl. VB analyzer smoke tests). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/AvoidRedundantRegexIsMatchBeforeMatch.cs | New CA2028 analyzer implementation for redundant Regex.IsMatch before Regex.Match. |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/CSharpAvoidRedundantRegexIsMatchBeforeMatch.Fixer.cs | New C# code fix provider for CA2028 using property-pattern matching. |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx | Adds CA2028 title/message/description/fix strings. |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hant.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.zh-Hans.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.tr.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ru.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pt-BR.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.pl.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ko.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.ja.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf | Adds new CA2028 localized resource entries (state=new). |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md | Registers CA2028 in the unshipped analyzer release list. |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif | Adds CA2028 rule metadata for SARIF. |
| src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md | Documents CA2028 in the analyzer rules list. |
Copilot's findings
- Files reviewed: 20/20 changed files
- Comments generated: 4
… params, fix diagnostic ID range - Remove unused root variable and matchCallNode parameter in ApplyFixAsync - Add WalkDownParentheses() before WalkDownConversion() in GetUnwrappedInvocation - Fix trailing trivia on parenthesized conditions causing extra whitespace - Rewrite HasConflictingNameInSubsequentSiblings to walk up else-if chains - Add ForEachVariableStatementSyntax handling for deconstruction foreach - Update DiagnosticCategoryAndIdRanges.txt to include CA2028 - Add 3 new tests: parenthesized condition, deconstruction foreach, non-block parent Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (4)
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/CSharpAvoidRedundantRegexIsMatchBeforeMatch.Fixer.cs:18
using Microsoft.CodeAnalysis.Operations;is not used in this file. Please remove it to avoid unused using warnings.
using Microsoft.CodeAnalysis.Operations;
using Microsoft.NetCore.Analyzers;
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/CSharpAvoidRedundantRegexIsMatchBeforeMatch.Fixer.cs:199
- The fix currently strips trailing trivia from the original
ifcondition (WithTrailingTrivia(TriviaList())). This can drop end-of-condition comments/formatting. Preserve the original condition trivia (or useWithTriviaFrom) so the code fix doesn't delete user comments.
var newCondition = SyntaxFactory.IsPatternExpression(
matchCallExpression.WithoutTrivia(),
successPattern)
.WithLeadingTrivia(ifStatement.Condition.GetLeadingTrivia())
.WithTrailingTrivia(SyntaxFactory.TriviaList());
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/CSharpAvoidRedundantRegexIsMatchBeforeMatch.Fixer.cs:196
matchCallExpression.WithoutTrivia()will drop any leading/trailing trivia (including comments) attached to theRegex.Match(...)initializer when it’s moved into theifcondition. Please preserve trivia from the original initializer where possible.
var newCondition = SyntaxFactory.IsPatternExpression(
matchCallExpression.WithoutTrivia(),
successPattern)
src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/CSharpAvoidRedundantRegexIsMatchBeforeMatch.Fixer.cs:205
editor.RemoveNode(matchDeclarationStatement)uses the default remove options, which can drop leading/trailing trivia (e.g., comments) attached to the declaration statement. Consider using remove options that preserve trivia so the fixer doesn’t delete comments inside theifbody.
// Remove the Match declaration statement from the if body
editor.RemoveNode(matchDeclarationStatement);
- Files reviewed: 21/21 changed files
- Comments generated: 1
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…gion rename - Fixer: verify initializer expression matches the Match invocation span before offering code fix (unwrapping parens/casts) - Analyzer: handle ICoalesceAssignmentOperation (??=) in GetWrittenSymbol - Analyzer: handle IDeconstructionAssignmentOperation in ContainsWriteToSymbols with recursive ContainsTrackedSymbolReference helper - Tests: add InterveningCoalesceAssignment and InterveningDeconstructionAssignment - Tests: rename model-specific region to content-based name Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…based type check, preserve leading trivia Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The pre-declaration assignment fix path would produce uncompilable code
if the variable was referenced in the else branch, since the pattern
variable introduced by 'is { Success: true } m' is not definitely
assigned in the else branch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r model, remove null check - Replace ImmutableArray<ISymbol> matchMembers with INamedTypeSymbol regexType for simpler containing type comparison instead of GetMembers+Contains - Rename GetUnwrappedInvocation to UnwrapInvocationFromCondition, take non-nullable IOperation since Condition is never null - Shadow outer compilation context variable name with context - Defer SemanticModel fetch past early returns in fixer - Use context.Diagnostics[0] instead of FirstOrDefault() in fixer - Remove unused System.Linq import from analyzer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ifyCodeFixAsync
- Replace {|CA2028:...|} with [|...|] since analyzer has single rule
- Replace VerifyAnalyzerAsync(source) with VerifyCodeFixAsync(source, source)
for stronger assertion that no code fix is registered
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…NotNull in conditional access - GetWrittenSymbol and ContainsTrackedSymbolReference now call WalkDownParentheses() so parenthesized assignment targets like (input) = "other" are recognized as writes. - FindMatchInExpression now recurses into both Operation and WhenNotNull of IConditionalAccessOperation, so patterns like obj?.Process(Regex.Match(...)) are detected. - Added tests for both cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@Youssef1313 good to merge? |
|
Best to get a review from @dotnet/dotnet-analyzers |
|
@dotnet/dotnet-analyzers can you please review? |
Implements CA2028: Avoid redundant Regex.IsMatch before Regex.Match.
Closes dotnet/runtime#111239
Supersedes #51214 (abandoned — Copilot was not converging at the time)
Pattern detected
What's included
IConditionalOperation, validates operand equivalence (restricted to stable sources: locals, parameters, constants, readonly/const fields), tracks intervening writes including ref/out argument mutations, and reports diagnostic with additional location on the Match call.Match m = Regex.Match(...)inside the if body — replaces withispattern.Match m = null; if (...) { m = Regex.Match(...); ... }— removes the declaration and assignment, replaces withispattern. Includes safety checks: variable must not be referenced after the if statement, initializer must be absent/null/default.Addresses all feedback from #51214
All 15 review comments from @stephentoub on the abandoned PR are addressed:
!IsMatchguard) intentionally not supported per reviewer guidance — adds too much complexityEmptyCodeFixProvider)context.Diagnostics[0]not inlined because it's used 4xDesign decisions
DoNotUseNonCancelableTaskDelayWithWhenAnyAddMutableSymbolrecurses intoIFieldReferenceOperation.Instanceto handleobj.ReadonlyFieldreceiver patternsReadOnlySpan) intentionally not flagged —IsMatchreturns bool but there's no correspondingMatchoverload returningMatchParameter.Ordinal(not array index) for correctnessWalkDownParentheses()applied consistently beforeWalkDownConversion()in all operand/symbol tracking paths