Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 10 additions & 0 deletions docs/Rules/MA0148.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
````
10 changes: 10 additions & 0 deletions docs/Rules/MA0149.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
````
Original file line number Diff line number Diff line change
Expand Up @@ -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))
return false;

if (expressionOperation.Syntax is not ExpressionSyntax valueExpression || constantOperation.Syntax is not ExpressionSyntax constantExpression)
Expand Down Expand Up @@ -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))
return false;

if (constantOperation.Syntax is not ExpressionSyntax constantExpression || expressionOperation.Syntax is not ExpressionSyntax valueExpression)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
return;

if (_operationUtilities.IsInExpressionContext(operation))
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -20,16 +24,22 @@ 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)
{
while (operation is IConversionOperation { IsImplicit: true } conversionOperation)
{
if (conversionOperation.Conversion.IsUserDefined)
return true;
if (expressionOperation is not IConversionOperation { IsImplicit: true } conversionOperation)
return true;

operation = conversionOperation.Operand;
}
var semanticModel = expressionOperation.SemanticModel;
var operandType = conversionOperation.Operand.Type?.GetUnderlyingNullableTypeOrSelf();
if (semanticModel is null || operandType is null || constantOperation.Syntax is not ExpressionSyntax constantExpression)
return false;

return false;
// 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 };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}