From 8757b20d05c61cf244b7bcea000b48a733f92a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 11 Sep 2026 16:24:26 -0400 Subject: [PATCH 1/2] Do not report MA0148/MA0149 when the constant does not convert to the operand type The equality operator may implicitly convert the operand (numeric promotion, nullable lifting, user-defined conversions), while the pattern is matched against the type of the operand itself. For instance, 'intValue == 1L' is valid but 'intValue is 1L' fails with CS0266. The analyzer and the code fixer (single and merged comparisons) now only rewrite the comparison when the constant is implicitly convertible to the type of the operand, without user-defined conversion. This replaces the previous check that only excluded implicit user-defined conversions. --- docs/Rules/MA0148.md | 10 +++ docs/Rules/MA0149.md | 10 +++ ...ternMatchingForEqualityComparisonsFixer.cs | 4 +- ...nMatchingForEqualityComparisonsAnalyzer.cs | 3 +- ...ernMatchingForEqualityComparisonsCommon.cs | 29 ++++-- ...hingForEqualityComparisonsAnalyzerTests.cs | 90 +++++++++++++++++++ 6 files changed, 134 insertions(+), 12 deletions(-) diff --git a/docs/Rules/MA0148.md b/docs/Rules/MA0148.md index 7270d7361..035766b95 100644 --- a/docs/Rules/MA0148.md +++ b/docs/Rules/MA0148.md @@ -24,3 +24,13 @@ class Sample Sample value = null; _ = value == 0; // ok ```` + +Cases where the constant cannot be implicitly converted to the type of the value are also ignored. The equality operator converts both operands to a common type, while the pattern is matched against the type of the value: + +````c# +int value = 0; +_ = value == 1L; // ok, 'value is 1L' does not compile + +byte b = 0; +_ = b == 1; // not compliant, 'b is 1' is valid +```` diff --git a/docs/Rules/MA0149.md b/docs/Rules/MA0149.md index 10cf5c915..f506b9876 100644 --- a/docs/Rules/MA0149.md +++ b/docs/Rules/MA0149.md @@ -24,3 +24,13 @@ class Sample Sample value = null; _ = value != 0; // ok ```` + +Cases where the constant cannot be implicitly converted to the type of the value are also ignored. The equality operator converts both operands to a common type, while the pattern is matched against the type of the value: + +````c# +int value = 0; +_ = value != 1L; // ok, 'value is not 1L' does not compile + +byte b = 0; +_ = b != 1; // not compliant, 'b is not 1' is valid +```` diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs index 45ae63a91..dd7d9f320 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs @@ -210,7 +210,7 @@ private static bool TryCreateDiscreteComparisonCandidate(ExpressionSyntax expres return false; var expressionOperation = leftIsConstant ? operation.RightOperand : operation.LeftOperand; - if (UsePatternMatchingForEqualityComparisonsCommon.HasImplicitUserDefinedConversion(expressionOperation)) + if (!UsePatternMatchingForEqualityComparisonsCommon.CanUseConstantPattern(expressionOperation, constantOperation, cancellationToken)) return false; if (expressionOperation.Syntax is not ExpressionSyntax valueExpression || constantOperation.Syntax is not ExpressionSyntax constantExpression) @@ -246,7 +246,7 @@ private static bool TryCreatePatternExpression(BinaryExpressionSyntax binaryExpr var constantOperation = leftIsConstant ? operation.LeftOperand : operation.RightOperand; var expressionOperation = leftIsConstant ? operation.RightOperand : operation.LeftOperand; - if (UsePatternMatchingForEqualityComparisonsCommon.HasImplicitUserDefinedConversion(expressionOperation)) + if (!UsePatternMatchingForEqualityComparisonsCommon.CanUseConstantPattern(expressionOperation, constantOperation, cancellationToken)) return false; if (constantOperation.Syntax is not ExpressionSyntax constantExpression || expressionOperation.Syntax is not ExpressionSyntax valueExpression) diff --git a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs index 54347fcba..a806a9eb6 100644 --- a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs @@ -88,8 +88,9 @@ public void AnalyzeBinary(OperationAnalysisContext context) var rightIsConstant = UsePatternMatchingForEqualityComparisonsCommon.IsConstantLiteral(operation.RightOperand); if (leftIsConstant ^ rightIsConstant) { + var constantOperation = leftIsConstant ? operation.LeftOperand : operation.RightOperand; var expressionOperation = leftIsConstant ? operation.RightOperand : operation.LeftOperand; - if (UsePatternMatchingForEqualityComparisonsCommon.HasImplicitUserDefinedConversion(expressionOperation)) + if (!UsePatternMatchingForEqualityComparisonsCommon.CanUseConstantPattern(expressionOperation, constantOperation, context.CancellationToken)) return; if (_operationUtilities.IsInExpressionContext(operation)) diff --git a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs index 3129664a9..8c3931469 100644 --- a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs +++ b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs @@ -1,4 +1,8 @@ -namespace Meziantou.Analyzer.Rules; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Meziantou.Analyzer.Rules; + internal static class UsePatternMatchingForEqualityComparisonsCommon { public static bool IsNull(IOperation operation) @@ -20,16 +24,23 @@ public static bool IsConstantLiteral(IOperation operation) return false; } - public static bool HasImplicitUserDefinedConversion(IOperation operation) + // The equality operator may implicitly convert the operand (numeric promotion, user-defined conversion, etc.), + // while the pattern is matched against the type of the operand itself. For instance, 'intValue == 1L' is valid + // but 'intValue is 1L' is not. The constant pattern is valid only if the constant implicitly converts to the operand type. + public static bool CanUseConstantPattern(IOperation expressionOperation, IOperation constantOperation, CancellationToken cancellationToken) { - while (operation is IConversionOperation { IsImplicit: true } conversionOperation) - { - if (conversionOperation.Conversion.IsUserDefined) - return true; + if (expressionOperation is not IConversionOperation { IsImplicit: true }) + return true; - operation = conversionOperation.Operand; - } + var semanticModel = expressionOperation.SemanticModel; + if (semanticModel is null || expressionOperation.Syntax is not ExpressionSyntax expression || constantOperation.Syntax is not ExpressionSyntax constantExpression) + return false; - return false; + var operandType = semanticModel.GetTypeInfo(expression, cancellationToken).Type; + if (operandType is null) + return false; + + var conversion = semanticModel.ClassifyConversion(constantExpression, operandType.GetUnderlyingNullableTypeOrSelf()); + return conversion is { IsImplicit: true, IsUserDefined: false }; } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UsePatternMatchingForEqualityComparisonsAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UsePatternMatchingForEqualityComparisonsAnalyzerTests.cs index 4f20cd2b7..082afca37 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/UsePatternMatchingForEqualityComparisonsAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UsePatternMatchingForEqualityComparisonsAnalyzerTests.cs @@ -369,4 +369,94 @@ class Sample return test.RunAsync(); } + + [Theory] + [InlineData("int", "1L")] + [InlineData("int", "1.0")] + [InlineData("int", "1m")] + [InlineData("int?", "1L")] + [InlineData("float", "0.1")] + [InlineData("byte", "300")] + [InlineData("char", "65")] + public async Task EqualityComparison_NumericPromotion_ConstantNotConvertibleToOperandType_NoDiagnostic(string type, string constant) + { + var test = CreateTest(); + test.TestCode = $$""" + {{type}} value = default; + _ = value == {{constant}}; + _ = {{constant}} != value; + """; + + await test.RunAsync(); + } + + [Theory] + [InlineData("byte", "1")] + [InlineData("short", "1")] + [InlineData("ushort", "1")] + [InlineData("byte?", "1")] + public async Task EqualityComparison_NumericPromotion_ConstantConvertibleToOperandType(string type, string constant) + { + var test = CreateTest(); + test.TestCode = $$""" + {{type}} value = default; + _ = {|MA0148:value == {{constant}}|}; + _ = {|MA0149:{{constant}} != value|}; + """; + test.FixedCode = $$""" + {{type}} value = default; + _ = value is {{constant}}; + _ = value is not {{constant}}; + """; + + await test.RunAsync(); + } + + [Fact] + public Task EqualityComparison_MixedWithNumericPromotion_OnlyFixValidExpression() + { + var test = CreateTest(); + test.TestCode = """ + int value = 0; + _ = {|MA0148:value == 1|} || value == 2L; + """; + test.FixedCode = """ + int value = 0; + _ = value is 1 || value == 2L; + """; + + return test.RunAsync(); + } + + [Fact] + public Task InequalityComparison_MixedWithNumericPromotion_OnlyFixValidExpression() + { + var test = CreateTest(); + test.TestCode = """ + int value = 0; + _ = value != 1L && {|MA0149:value != 2|}; + """; + test.FixedCode = """ + int value = 0; + _ = value != 1L && value is not 2; + """; + + return test.RunAsync(); + } + + [Fact] + public Task EqualityComparison_MergeWithNumericPromotion() + { + var test = CreateTest(); + test.TestCode = """ + byte value = 0; + _ = {|MA0148:value == 1|} || {|MA0148:value == 2|}; + """; + test.FixedCode = """ + byte value = 0; + _ = value is 1 or 2; + """; + + return test.RunAsync(); + } } From 15af262fbb22d0c954e90eb7cffdb71de368fc2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 11 Sep 2026 22:01:22 -0400 Subject: [PATCH 2/2] Use the type of the operand of the conversion instead of the semantic model The type of the operand can be read from the operation tree. The semantic model is still needed to classify the conversion of the constant, as it depends on its value ('byteValue == 1' is valid, 'byteValue == 300' is not). --- ...ePatternMatchingForEqualityComparisonsFixer.cs | 4 ++-- ...tternMatchingForEqualityComparisonsAnalyzer.cs | 2 +- ...PatternMatchingForEqualityComparisonsCommon.cs | 15 +++++++-------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs index dd7d9f320..66ce20888 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UsePatternMatchingForEqualityComparisonsFixer.cs @@ -210,7 +210,7 @@ private static bool TryCreateDiscreteComparisonCandidate(ExpressionSyntax expres return false; var expressionOperation = leftIsConstant ? operation.RightOperand : operation.LeftOperand; - if (!UsePatternMatchingForEqualityComparisonsCommon.CanUseConstantPattern(expressionOperation, constantOperation, cancellationToken)) + if (!UsePatternMatchingForEqualityComparisonsCommon.CanUseConstantPattern(expressionOperation, constantOperation)) return false; if (expressionOperation.Syntax is not ExpressionSyntax valueExpression || constantOperation.Syntax is not ExpressionSyntax constantExpression) @@ -246,7 +246,7 @@ private static bool TryCreatePatternExpression(BinaryExpressionSyntax binaryExpr var constantOperation = leftIsConstant ? operation.LeftOperand : operation.RightOperand; var expressionOperation = leftIsConstant ? operation.RightOperand : operation.LeftOperand; - if (!UsePatternMatchingForEqualityComparisonsCommon.CanUseConstantPattern(expressionOperation, constantOperation, cancellationToken)) + if (!UsePatternMatchingForEqualityComparisonsCommon.CanUseConstantPattern(expressionOperation, constantOperation)) return false; if (constantOperation.Syntax is not ExpressionSyntax constantExpression || expressionOperation.Syntax is not ExpressionSyntax valueExpression) diff --git a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs index a806a9eb6..ba3a6688e 100644 --- a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsAnalyzer.cs @@ -90,7 +90,7 @@ public void AnalyzeBinary(OperationAnalysisContext context) { var constantOperation = leftIsConstant ? operation.LeftOperand : operation.RightOperand; var expressionOperation = leftIsConstant ? operation.RightOperand : operation.LeftOperand; - if (!UsePatternMatchingForEqualityComparisonsCommon.CanUseConstantPattern(expressionOperation, constantOperation, context.CancellationToken)) + if (!UsePatternMatchingForEqualityComparisonsCommon.CanUseConstantPattern(expressionOperation, constantOperation)) return; if (_operationUtilities.IsInExpressionContext(operation)) diff --git a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs index 8c3931469..4b62424f6 100644 --- a/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs +++ b/src/Meziantou.Analyzer/Rules/UsePatternMatchingForEqualityComparisonsCommon.cs @@ -27,20 +27,19 @@ public static bool IsConstantLiteral(IOperation operation) // The equality operator may implicitly convert the operand (numeric promotion, user-defined conversion, etc.), // while the pattern is matched against the type of the operand itself. For instance, 'intValue == 1L' is valid // but 'intValue is 1L' is not. The constant pattern is valid only if the constant implicitly converts to the operand type. - public static bool CanUseConstantPattern(IOperation expressionOperation, IOperation constantOperation, CancellationToken cancellationToken) + public static bool CanUseConstantPattern(IOperation expressionOperation, IOperation constantOperation) { - if (expressionOperation is not IConversionOperation { IsImplicit: true }) + if (expressionOperation is not IConversionOperation { IsImplicit: true } conversionOperation) return true; var semanticModel = expressionOperation.SemanticModel; - if (semanticModel is null || expressionOperation.Syntax is not ExpressionSyntax expression || constantOperation.Syntax is not ExpressionSyntax constantExpression) + var operandType = conversionOperation.Operand.Type?.GetUnderlyingNullableTypeOrSelf(); + if (semanticModel is null || operandType is null || constantOperation.Syntax is not ExpressionSyntax constantExpression) return false; - var operandType = semanticModel.GetTypeInfo(expression, cancellationToken).Type; - if (operandType is null) - return false; - - var conversion = semanticModel.ClassifyConversion(constantExpression, operandType.GetUnderlyingNullableTypeOrSelf()); + // The conversion depends on the value of the constant ('byteValue == 1' is valid, whereas 'byteValue == 300' is not), + // so it must be classified from the expression instead of the type of the constant + var conversion = semanticModel.ClassifyConversion(constantExpression, operandType); return conversion is { IsImplicit: true, IsUserDefined: false }; } }