CA2028: Avoid redundant Regex.IsMatch before Regex.Match#54071
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>
krwq
left a comment
There was a problem hiding this comment.
Couple of comments about potential false positives from AI to verify
- Skip ternary `?:` (and VB `If(...)`) — only report on if-statements by bailing when conditional.Type is not null. - Extend FindMatchInExpression to recognize Match calls inside object/array creation, tuples, interpolated strings, coalesce, binary, unary, and await expressions. - Restrict IInstanceReferenceOperation equivalence to reference types, since `this` can be reassigned inside struct instance methods. - Add WalkDownParentheses() to ref/out arg unwrap in ContainsWriteToSymbols for consistency with other unwrap sites. - In the C# fixer, extract a ContainsIdentifierReference helper that excludes member-access right-hand sides, qualified-name suffixes, and named-argument labels, so unrelated `something.m = 1` or `Helper(m: 1)` after the if no longer suppress the fix. - Use Formatter.Annotation on the synthesized IsPatternExpression in BuildIsPatternCondition so the host's formatter handles spacing. - Add 13 regression tests covering each change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- FindMatchInExpression now walks IObjectOrCollectionInitializerOperation
(member assignments inside `new Foo { Bar = Regex.Match(...) }`) and
ISimpleAssignmentOperation (the member-assignment shape).
- ContainsWriteToSymbols now short-circuits at IAnonymousFunctionOperation
and ILocalFunctionOperation, so writes inside a local function or lambda
body are no longer treated as intervening writes when the function itself
is the statement being scanned.
- Add regression tests for both.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- C# fixer now accepts DefaultExpressionSyntax (e.g. `default(Match)`) as a removable initializer in addition to the `null` and `default` literal forms. - Remove the unreachable second ISimpleAssignmentOperation branch in FindMatchInExpression (the earlier branch handles every case). - Add a regression test for the default(T) initializer case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t scan
Pattern variables introduced by 'is { Success: true } m' scope to the entire
enclosing block. The fixer's HasConflictingName check now also rejects names
that are already bound by a subsequent LINQ query clause (from/let/join/
join-into/into) anywhere in the parent block, preventing the fixer from
introducing CS0136-style name collisions.
Added a regression test that confirms no fix is offered when the parent
block has a later 'from m in items' query and the fixer would otherwise
name its pattern variable 'm'.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
The failures are unrelated. |
|
|
||
| struct S | ||
| { | ||
| public Regex r; |
There was a problem hiding this comment.
Per AI:
The diagnostic doesn't fire — but not because of the new IInstanceReferenceOperation restriction. It doesn't fire because the existing readonly-field
equivalence check already rejects non-readonly fields. To actually exercise the new behavior, the field must be public readonly Regex r;. As written, this test would pass even if the new restriction were reverted.
krwq
left a comment
There was a problem hiding this comment.
LGTM but one test case is probably always passing and need extra readonly on the field. Note that this is only partially reviewed manually, most of the feedback is from AI.
|
/ba-g unrelated |
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