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
7 changes: 7 additions & 0 deletions docs/Rules/MA0192.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ This rule reports patterns such as:
- `(value & MyEnum.Flag1) is 0`
- `(value & MyEnum.Flag1) is not 0`

The compared flag doesn't have to be a constant. `(value & flags) == flags` is reported when both operands reference the same value, such as a parameter, a local variable, or a field:

- `(value & flags) == flags`
- `(value & flags) != flags`

Expressions that may return a different value on each evaluation, such as properties, method calls, or `volatile` fields, are not reported.

For comparisons against `0`, the enum member used in the bitwise `&` must be a single-bit value (for example `1`, `2`, `4`, `8`, ...). Combined values are ignored.

Zero-valued enum members are not reported by this rule. They are covered by [MA0201](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0201.md).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ rightOperand.Syntax is ExpressionSyntax enumValueExpression2 &&
return null;
}

private static bool TryGetEnumFlagReference(IOperation potentialFlag, IOperation comparedOperand, [NotNullWhen(true)] out IFieldReferenceOperation? flagOperation, out bool comparedWithZero)
private static bool TryGetEnumFlagReference(IOperation potentialFlag, IOperation comparedOperand, [NotNullWhen(true)] out IOperation? flagOperation, out bool comparedWithZero)
{
potentialFlag = potentialFlag.UnwrapImplicitConversions();
comparedOperand = comparedOperand.UnwrapImplicitConversions();
Expand All @@ -219,6 +219,13 @@ private static bool TryGetEnumFlagReference(IOperation potentialFlag, IOperation
}
}

if (!potentialFlag.IsConstantZero() && UseHasFlagMethodCommon.AreEquivalentOperands(potentialFlag, comparedOperand))
{
flagOperation = comparedOperand;
comparedWithZero = false;
return true;
}

flagOperation = null;
comparedWithZero = false;
return false;
Expand Down
8 changes: 7 additions & 1 deletion src/Meziantou.Analyzer/Rules/UseHasFlagMethodAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ private static bool TryGetHasFlagPattern(IOperation operation, [NotNullWhen(true
return null;
}

private static bool TryGetEnumFlagReference(IOperation potentialFlag, IOperation comparedOperand, [NotNullWhen(true)] out IFieldReferenceOperation? flagOperation)
private static bool TryGetEnumFlagReference(IOperation potentialFlag, IOperation comparedOperand, [NotNullWhen(true)] out IOperation? flagOperation)
{
potentialFlag = potentialFlag.UnwrapImplicitConversions();
comparedOperand = comparedOperand.UnwrapImplicitConversions();
Expand All @@ -329,6 +329,12 @@ private static bool TryGetEnumFlagReference(IOperation potentialFlag, IOperation
}
}

if (!potentialFlag.IsConstantZero() && UseHasFlagMethodCommon.AreEquivalentOperands(potentialFlag, comparedOperand))
{
flagOperation = comparedOperand;
return true;
}

flagOperation = null;
return false;
}
Expand Down
26 changes: 26 additions & 0 deletions src/Meziantou.Analyzer/Rules/UseHasFlagMethodCommon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Meziantou.Analyzer.Rules;

internal static class UseHasFlagMethodCommon
{
/// <summary>
/// Determines if both operations reference the same value, so that evaluating them twice is equivalent to evaluating them once.
/// Only side-effect free references are supported (parameters, locals, fields), so that <c>(value &amp; flag) == flag</c> can safely be replaced by <c>value.HasFlag(flag)</c>.
/// </summary>
public static bool AreEquivalentOperands(IOperation? left, IOperation? right)
{
if (left is null || right is null)
return false;

left = left.UnwrapImplicitConversions();
right = right.UnwrapImplicitConversions();

return (left, right) switch
{
(IParameterReferenceOperation a, IParameterReferenceOperation b) => a.Parameter.IsEqualTo(b.Parameter),
(ILocalReferenceOperation a, ILocalReferenceOperation b) => a.Local.IsEqualTo(b.Local),
(IFieldReferenceOperation a, IFieldReferenceOperation b) => a.Field.IsEqualTo(b.Field) && !a.Field.IsVolatile && (a.Field.IsStatic || AreEquivalentOperands(a.Instance, b.Instance)),
(IInstanceReferenceOperation a, IInstanceReferenceOperation b) => a.ReferenceKind == b.ReferenceKind && a.Type.IsEqualTo(b.Type),
_ => false,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ enum MyEnum

static class MyEnumExtensions
{
public static bool HasFlags(this MyEnum value, MyEnum flags) => (value & flags) == flags;
public static bool HasFlags(this MyEnum value, MyEnum flags) => {|MA0192:(value & flags) == flags|};
}

class Sample
Expand Down Expand Up @@ -604,4 +604,290 @@ class Sample
.ValidateAsync();
}

[Fact]
public async Task ParameterFlag_ReportDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
Flag2 = 2,
}

class Sample
{
bool M(MyEnum value, MyEnum comparand) => {|MA0192:(value & comparand) == comparand|};
}
""")
.ShouldFixCodeWith("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
Flag2 = 2,
}

class Sample
{
bool M(MyEnum value, MyEnum comparand) => value.HasFlag(comparand);
}
""")
.ValidateAsync();
}

[Fact]
public async Task ParameterFlag_ReversedAndOperands_ReportDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
bool M(MyEnum value, MyEnum comparand) => {|MA0192:(comparand & value) == comparand|};
}
""")
.ShouldFixCodeWith("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
bool M(MyEnum value, MyEnum comparand) => value.HasFlag(comparand);
}
""")
.ValidateAsync();
}

[Fact]
public async Task ParameterFlag_NotEquals_ReportDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
bool M(MyEnum value, MyEnum comparand) => {|MA0192:(value & comparand) != comparand|};
}
""")
.ShouldFixCodeWith("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
bool M(MyEnum value, MyEnum comparand) => !value.HasFlag(comparand);
}
""")
.ValidateAsync();
}

[Fact]
public async Task LocalFlag_ReportDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
bool M(MyEnum value)
{
var comparand = MyEnum.Flag1;
return {|MA0192:(value & comparand) == comparand|};
}
}
""")
.ShouldFixCodeWith("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
bool M(MyEnum value)
{
var comparand = MyEnum.Flag1;
return value.HasFlag(comparand);
}
}
""")
.ValidateAsync();
}

[Fact]
public async Task FieldFlag_ReportDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
private MyEnum _comparand;

bool M(MyEnum value) => {|MA0192:(value & _comparand) == this._comparand|};
}
""")
.ShouldFixCodeWith("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
private MyEnum _comparand;

bool M(MyEnum value) => value.HasFlag(this._comparand);
}
""")
.ValidateAsync();
}

[Fact]
public async Task DifferentParameters_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
bool M(MyEnum value, MyEnum comparand, MyEnum other) => (value & comparand) == other;
}
""")
.ValidateAsync();
}

[Fact]
public async Task DifferentInstanceFields_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
private MyEnum _comparand;

bool M(MyEnum value, Sample other) => (value & _comparand) == other._comparand;
}
""")
.ValidateAsync();
}

[Fact]
public async Task VolatileFieldFlag_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
private volatile MyEnum _comparand;

bool M(MyEnum value) => (value & _comparand) == _comparand;
}
""")
.ValidateAsync();
}

[Fact]
public async Task PropertyFlag_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
private MyEnum Comparand => MyEnum.Flag1;

bool M(MyEnum value) => (value & Comparand) == Comparand;
}
""")
.ValidateAsync();
}

[Fact]
public async Task MethodCallFlag_NoDiagnostic()
{
await CreateProjectBuilder()
.WithSourceCode("""
[System.Flags]
enum MyEnum
{
None = 0,
Flag1 = 1,
}

class Sample
{
private MyEnum GetComparand() => MyEnum.Flag1;

bool M(MyEnum value) => (value & GetComparand()) == GetComparand();
}
""")
.ValidateAsync();
}
}
Loading