Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Simplify analyzer and fixer: remove inverted pattern support, apply c…
…ode review feedback

Co-authored-by: stephentoub <[email protected]>
  • Loading branch information
Copilot and stephentoub committed Oct 17, 2025
commit cdda6087c80e607ba0db4d195e446f8ccec2b70b
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,12 @@ public sealed class CSharpAvoidRedundantRegexIsMatchBeforeMatchFixer : CodeFixPr

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not { } root ||
await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is not { } semanticModel)
{
return;
}

var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (semanticModel is null)
{
return;
}

var diagnostic = context.Diagnostics[0];
var node = root.FindNode(context.Span, getInnermostNodeForTie: true);

if (node is not InvocationExpressionSyntax isMatchInvocation)
Expand All @@ -61,7 +54,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources.AvoidRedundantRegexIsMatchBeforeMatchFix,
ct => RemoveRedundantIsMatchAsync(context.Document, root, ifStatement, isMatchInvocation, semanticModel, ct),
equivalenceKey: NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources.AvoidRedundantRegexIsMatchBeforeMatchFix),
diagnostic);
context.Diagnostics[0]);
}

private static async Task<Document> RemoveRedundantIsMatchAsync(
Expand All @@ -74,77 +67,55 @@ private static async Task<Document> RemoveRedundantIsMatchAsync(
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

// Determine if this is a negated pattern (if (!IsMatch) { return; })
bool isNegated = ifStatement.Condition is PrefixUnaryExpressionSyntax { RawKind: (int)SyntaxKind.LogicalNotExpression };

if (isNegated && IsEarlyReturnPattern(ifStatement))
// Find the Match call in the body and use it to replace the condition
var matchCall = FindMatchCallInBlock(ifStatement.Statement, isMatchInvocation, semanticModel);
if (matchCall is not null)
{
// For inverted early return pattern: remove the entire if statement
editor.RemoveNode(ifStatement);
}
else
{
// For normal pattern: transform the if statement
// Find the Match call in the body and use it to replace the condition
var matchCall = FindMatchCallInBlock(ifStatement.Statement, isMatchInvocation, semanticModel);
// Create the new condition: Regex.Match(...) is { Success: true } variableName
var matchDeclaration = matchCall.Parent?.Parent as LocalDeclarationStatementSyntax;

if (matchCall is not null)
if (matchDeclaration is not null)
{
// Create the new condition: Regex.Match(...) is { Success: true } variableName
var matchDeclaration = matchCall.Parent?.Parent as LocalDeclarationStatementSyntax;

if (matchDeclaration is not null)
var variableDeclarator = matchDeclaration.Declaration.Variables.First();
var variableName = variableDeclarator.Identifier.Text;

// Create pattern: is { Success: true }
var successPattern = SyntaxFactory.RecursivePattern()
.WithPropertyPatternClause(
SyntaxFactory.PropertyPatternClause(
SyntaxFactory.SingletonSeparatedList<SubpatternSyntax>(
SyntaxFactory.Subpattern(
SyntaxFactory.NameColon("Success"),
SyntaxFactory.ConstantPattern(
SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression))))))
.WithDesignation(SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier(variableName)));

var newCondition = SyntaxFactory.IsPatternExpression(
matchCall,
successPattern);

// Remove the Match declaration from the body
var newBody = ifStatement.Statement;
if (ifStatement.Statement is BlockSyntax block)
{
var variableDeclarator = matchDeclaration.Declaration.Variables.First();
var variableName = variableDeclarator.Identifier.Text;

// Create pattern: is { Success: true }
var successPattern = SyntaxFactory.RecursivePattern()
.WithPropertyPatternClause(
SyntaxFactory.PropertyPatternClause(
SyntaxFactory.SingletonSeparatedList<SubpatternSyntax>(
SyntaxFactory.Subpattern(
SyntaxFactory.NameColon("Success"),
SyntaxFactory.ConstantPattern(
SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression))))))
.WithDesignation(SyntaxFactory.SingleVariableDesignation(SyntaxFactory.Identifier(variableName)));

var newCondition = SyntaxFactory.IsPatternExpression(
matchCall,
successPattern);

// Remove the Match declaration from the body
var newBody = ifStatement.Statement;
if (ifStatement.Statement is BlockSyntax block)
{
var statements = block.Statements.Where(s => s != matchDeclaration);
newBody = block.WithStatements(SyntaxFactory.List(statements));
}
var statements = block.Statements.Where(s => s != matchDeclaration);
newBody = block.WithStatements(SyntaxFactory.List(statements));
}

// Create new if statement
var newIfStatement = ifStatement
.WithCondition(newCondition.WithTriviaFrom(ifStatement.Condition))
.WithStatement(newBody)
.WithAdditionalAnnotations(Formatter.Annotation);
// Create new if statement
var newIfStatement = ifStatement
.WithCondition(newCondition.WithTriviaFrom(ifStatement.Condition))
.WithStatement(newBody)
.WithAdditionalAnnotations(Formatter.Annotation);

editor.ReplaceNode(ifStatement, newIfStatement);
}
editor.ReplaceNode(ifStatement, newIfStatement);
}
}

return editor.GetChangedDocument();
}

private static bool IsEarlyReturnPattern(IfStatementSyntax ifStatement)
{
if (ifStatement.Statement is BlockSyntax block)
{
return block.Statements.Any(s => s is ReturnStatementSyntax or ThrowStatementSyntax or BreakStatementSyntax or ContinueStatementSyntax);
}

return ifStatement.Statement is ReturnStatementSyntax or ThrowStatementSyntax or BreakStatementSyntax or ContinueStatementSyntax;
}

private static InvocationExpressionSyntax? FindMatchCallInBlock(StatementSyntax statement, InvocationExpressionSyntax isMatchInvocation, SemanticModel semanticModel)
{
if (statement is not BlockSyntax block)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,48 +55,26 @@ public override void Initialize(AnalysisContext context)
{
var conditional = (IConditionalOperation)context.Operation;

// Check if condition is Regex.IsMatch call (direct or negated)
if (!IsRegexIsMatchCall(conditional.Condition, regexIsMatchSymbols, out var isMatchCall, out bool isNegated))
// Check if condition is Regex.IsMatch call (not negated)
if (!IsRegexIsMatchCall(conditional.Condition, regexIsMatchSymbols, out var isMatchCall))
{
return;
}

// For normal IsMatch, look in when-true branch for corresponding Match call
if (!isNegated)
// Look in when-true branch for corresponding Match call
if (FindMatchCallInBranch(conditional.WhenTrue, regexMatchSymbols, isMatchCall, context.Operation, out var matchCall))
{
if (FindMatchCallInBranch(conditional.WhenTrue, regexMatchSymbols, isMatchCall, context.Operation, out var matchCall))
{
context.ReportDiagnostic(isMatchCall.CreateDiagnostic(Rule));
return;
}
}
// For negated IsMatch with early return pattern, check subsequent operations
else if (IsEarlyReturnPattern(conditional))
{
// Look for Match calls after the conditional in the parent block
if (FindMatchCallAfterConditional(conditional, regexMatchSymbols, isMatchCall, context.Operation, out var subsequentMatchCall))
{
context.ReportDiagnostic(isMatchCall.CreateDiagnostic(Rule));
return;
}
context.ReportDiagnostic(isMatchCall.CreateDiagnostic(Rule));
}
}, OperationKind.Conditional);
});
}

private static bool IsRegexIsMatchCall(IOperation condition, ImmutableArray<IMethodSymbol> regexIsMatchSymbols, out IInvocationOperation isMatchCall, out bool isNegated)
private static bool IsRegexIsMatchCall(IOperation condition, ImmutableArray<IMethodSymbol> regexIsMatchSymbols, out IInvocationOperation isMatchCall)
{
// Handle unwrapping of conversions and parenthesized expressions
var unwrapped = condition.WalkDownConversion();

// Check for negation
isNegated = false;
if (unwrapped is IUnaryOperation { OperatorKind: UnaryOperatorKind.Not } unaryOp)
{
isNegated = true;
unwrapped = unaryOp.Operand.WalkDownConversion();
}

if (unwrapped is IInvocationOperation invocation &&
regexIsMatchSymbols.Contains(invocation.TargetMethod, SymbolEqualityComparer.Default))
{
Expand Down Expand Up @@ -139,62 +117,6 @@ private static bool FindMatchCallRecursive(IOperation operation, ImmutableArray<
return false;
}

private static bool IsEarlyReturnPattern(IConditionalOperation conditional)
{
// Check if the when-true branch is an early return or throw (not break/continue/goto)
if (conditional.WhenTrue is IBlockOperation block)
{
foreach (var statement in block.Operations)
{
if (statement is IReturnOperation or IThrowOperation)
{
return true;
}
}
}
else if (conditional.WhenTrue is IReturnOperation or IThrowOperation)
{
return true;
}

return false;
}

private static bool FindMatchCallAfterConditional(IConditionalOperation conditional, ImmutableArray<IMethodSymbol> regexMatchSymbols, IInvocationOperation isMatchCall, IOperation conditionalOperation, out IInvocationOperation matchCall)
{
// Navigate to the parent block to find subsequent operations
var parent = conditional.Parent;
while (parent is not null && parent is not IBlockOperation)
{
parent = parent.Parent;
}

if (parent is IBlockOperation parentBlock)
{
bool foundConditional = false;
foreach (var operation in parentBlock.Operations)
{
if (operation == conditional)
{
foundConditional = true;
continue;
}

if (foundConditional)
{
// Look for the first Match call that matches criteria
if (FindMatchCallRecursive(operation, regexMatchSymbols, isMatchCall, conditionalOperation, out matchCall))
{
return true;
}
}
}
}

matchCall = null!;
return false;
}

private static bool AreInvocationsOnSameInstance(IInvocationOperation invocation1, IInvocationOperation invocation2, IOperation conditionalOperation)
{
var instance1 = invocation1.Instance?.WalkDownConversion();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,28 +234,6 @@ void M(string input, string pattern)
""");
}

[Fact]
public async Task RedundantIsMatchGuard_InvertedWithEarlyReturn_CSharp_ReportsDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync("""
using System.Text.RegularExpressions;

class C
{
void M(string input, string pattern)
{
if (!{|CA2027:Regex.IsMatch(input, pattern)|})
{
return;
}

Match m = Regex.Match(input, pattern);
// use m
}
}
""");
}

[Fact]
public async Task NoRedundantIsMatchGuard_VariableReassigned_CSharp_NoDiagnostic()
{
Expand Down Expand Up @@ -530,54 +508,6 @@ void M(string input, string pattern)
""");
}

[Fact]
public async Task RedundantIsMatchGuard_InvertedWithMultipleStatements_CSharp_ReportsDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync("""
using System.Text.RegularExpressions;

class C
{
void M(string input, string pattern)
{
if (!{|CA2027:Regex.IsMatch(input, pattern)|})
{
System.Console.WriteLine("No match");
return;
}

Match m = Regex.Match(input, pattern);
System.Console.WriteLine(m.Value);
}
}
""");
}

[Fact]
public async Task NoRedundantIsMatchGuard_InvertedWithContinue_CSharp_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync("""
using System.Text.RegularExpressions;

class C
{
void M(string[] inputs, string pattern)
{
foreach (var input in inputs)
{
if (!Regex.IsMatch(input, pattern))
{
continue;
}

Match m = Regex.Match(input, pattern);
System.Console.WriteLine(m.Value);
}
}
}
""");
}

[Fact]
public async Task RedundantIsMatchGuard_LocalRegexVariable_CSharp_ReportsDiagnostic()
{
Expand Down Expand Up @@ -622,27 +552,5 @@ void M(string input, string pattern)
""");
}

[Fact]
public async Task RedundantIsMatchGuard_WithThrow_CSharp_ReportsDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync("""
using System;
using System.Text.RegularExpressions;

class C
{
void M(string input, string pattern)
{
if (!{|CA2027:Regex.IsMatch(input, pattern)|})
{
throw new ArgumentException("No match");
}

Match m = Regex.Match(input, pattern);
System.Console.WriteLine(m.Value);
}
}
""");
}
}
}