From d822e78925b0422017d58c3e2441047051e57eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Sat, 23 May 2026 11:47:01 -0400 Subject: [PATCH] Fix MA0148/MA0149 implicit conversion false positives Skip constant-pattern diagnostics and fixes when ==/!= relies on implicit user-defined conversions, add regression tests, and document the behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/Rules/MA0148.md | 12 ++++ docs/Rules/MA0149.md | 12 ++++ ...ternMatchingForEqualityComparisonsFixer.cs | 5 ++ ...nMatchingForEqualityComparisonsAnalyzer.cs | 4 ++ ...ernMatchingForEqualityComparisonsCommon.cs | 13 ++++ ...hingForEqualityComparisonsAnalyzerTests.cs | 59 +++++++++++++++++++ 6 files changed, 105 insertions(+) diff --git a/docs/Rules/MA0148.md b/docs/Rules/MA0148.md index cc4bc0f0a..7270d7361 100644 --- a/docs/Rules/MA0148.md +++ b/docs/Rules/MA0148.md @@ -12,3 +12,15 @@ value == 0 || value == 1; // not compliant value is 0 or 1; // ok ```` + +Cases that rely on implicit user-defined conversions are ignored because replacing `==` with `is` would be invalid: + +````c# +class Sample +{ + public static implicit operator int(Sample value) => 0; +} + +Sample value = null; +_ = value == 0; // ok +```` diff --git a/docs/Rules/MA0149.md b/docs/Rules/MA0149.md index 9ca9f6adb..10cf5c915 100644 --- a/docs/Rules/MA0149.md +++ b/docs/Rules/MA0149.md @@ -12,3 +12,15 @@ value != 0 && value != 1; // not compliant value is not (0 or 1); // ok ```` + +Cases that rely on implicit user-defined conversions are ignored because replacing `!=` with `is not` would be invalid: + +````c# +class Sample +{ + public static implicit operator int(Sample value) => 0; +} + +Sample value = null; +_ = value != 0; // ok +```` diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs index 37e4a74a2..e99449dce 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs @@ -222,6 +222,9 @@ private static bool TryCreateDiscreteComparisonCandidate(ExpressionSyntax expres return false; var expressionOperation = leftIsConstant ? operation.RightOperand : operation.LeftOperand; + if (UsePatternMatchingForEqualityComparisonsCommon.HasImplicitUserDefinedConversion(expressionOperation)) + return false; + if (expressionOperation.Syntax is not ExpressionSyntax valueExpression || constantOperation.Syntax is not ExpressionSyntax constantExpression) return false; @@ -255,6 +258,8 @@ private static bool TryCreatePatternExpression(BinaryExpressionSyntax binaryExpr var constantOperation = leftIsConstant ? operation.LeftOperand : operation.RightOperand; var expressionOperation = leftIsConstant ? operation.RightOperand : operation.LeftOperand; + if (UsePatternMatchingForEqualityComparisonsCommon.HasImplicitUserDefinedConversion(expressionOperation)) + return false; if (constantOperation.Syntax is not ExpressionSyntax constantExpression || expressionOperation.Syntax is not ExpressionSyntax valueExpression) return false; diff --git a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs index 8733f4b94..b058367bc 100644 --- a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs @@ -94,6 +94,10 @@ public void AnalyzeBinary(OperationAnalysisContext context) var rightIsConstant = UsePatternMatchingForEqualityComparisonsCommon.IsConstantLiteral(operation.RightOperand); if (leftIsConstant ^ rightIsConstant) { + var expressionOperation = leftIsConstant ? operation.RightOperand : operation.LeftOperand; + if (UsePatternMatchingForEqualityComparisonsCommon.HasImplicitUserDefinedConversion(expressionOperation)) + return; + if (_operationUtilities.IsInExpressionContext(operation)) return; diff --git a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs index 45d8ccdc9..7be924514 100644 --- a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs +++ b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs @@ -23,4 +23,17 @@ public static bool IsConstantLiteral(IOperation operation) return false; } + + public static bool HasImplicitUserDefinedConversion(IOperation operation) + { + while (operation is IConversionOperation { IsImplicit: true } conversionOperation) + { + if (conversionOperation.Conversion.IsUserDefined) + return true; + + operation = conversionOperation.Operand; + } + + return false; + } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UsePatternMatchingForEqualityComparisonsAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UsePatternMatchingForEqualityComparisonsAnalyzerTests.cs index e7b53c087..f6e187935 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/UsePatternMatchingForEqualityComparisonsAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UsePatternMatchingForEqualityComparisonsAnalyzerTests.cs @@ -260,4 +260,63 @@ class Sample """) .ValidateAsync(); } + + [Fact] + public async Task EqualityComparison_ImplicitConversion_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + Sample value = null; + _ = value == 0; + + class Sample + { + public static implicit operator int(Sample value) => 0; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task InequalityComparison_ImplicitConversion_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + Sample value = null; + _ = value != 0; + + class Sample + { + public static implicit operator int(Sample value) => 0; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task EqualityComparison_MixedWithImplicitConversion_OnlyFixValidExpression() + { + await CreateProjectBuilder() + .WithSourceCode(""" + var number = 0; + Sample value = null; + _ = [|number == 0|] || value == 0; + + class Sample + { + public static implicit operator int(Sample value) => 0; + } + """) + .ShouldFixCodeWith(""" + var number = 0; + Sample value = null; + _ = number is 0 || value == 0; + + class Sample + { + public static implicit operator int(Sample value) => 0; + } + """) + .ValidateAsync(); + } }