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
2 changes: 2 additions & 0 deletions docs/Rules/MA0128.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ _ = span is "bar";
_ = span.SequenceEqual("bar"); // report diagnostic
_ = span is "bar";
````

The rule is not reported when a `string` converted to a span is compared to an empty string. A `null` string is converted to an empty span, so `MemoryExtensions.SequenceEqual<char>(value, "")` is `true` when `value` is `null`, whereas `value is ""` is `false`. The comparisons to a non-empty string are still reported, as both forms are `false` for a `null` string.
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,26 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy

if (method.Name is "SequenceEqual" && method.Parameters.Length == 2 && method.Parameters[0].Type.IsEqualToAny(readOnlySpanCharSymbol, spanCharSymbol))
{
if (IsConstantValue(operation.Arguments[1].Value))
if (CanUseIsPattern(operation.Arguments[0].Value, operation.Arguments[1].Value))
{
context.ReportDiagnostic(Rule, operation, method.Name);
}
}
else if (method.Name is "Equals" && method.Parameters.Length == 3 && method.Parameters[0].Type.IsEqualTo(readOnlySpanCharSymbol))
{
if (IsConstantValue(operation.Arguments[1].Value) && IsStringComparisonOrdinal(operation.Arguments[2].Value, stringComparisonSymbol))
if (CanUseIsPattern(operation.Arguments[0].Value, operation.Arguments[1].Value) && IsStringComparisonOrdinal(operation.Arguments[2].Value, stringComparisonSymbol))
{
context.ReportDiagnostic(Rule, operation, method.Name);
}
}

static bool IsConstantValue(IOperation operation)
static bool CanUseIsPattern(IOperation span, IOperation value)
{
operation = operation.UnwrapImplicitConversions();
return operation is { ConstantValue: { HasValue: true, Value: string } };
if (value.UnwrapImplicitConversions() is not { ConstantValue: { HasValue: true, Value: string constantValue } })
return false;

// A null string is converted to an empty span, so it is equal to "", whereas the 'is ""' pattern is false for null
return constantValue.Length > 0 || span.UnwrapImplicitConversions().Type is not { SpecialType: SpecialType.System_String };
}

static bool IsStringComparisonOrdinal(IOperation operation, INamedTypeSymbol stringComparisonSymbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,56 @@ public Task SpanChar_SequenceEqual()

return test.RunAsync();
}

[Fact]
public Task StringConvertedToSpan_EmptyConstant()
{
var test = CreateTest();
test.TestCode = """
using System;
string value = null;
_ = MemoryExtensions.SequenceEqual<char>(value, "");
_ = MemoryExtensions.Equals(value, "", StringComparison.Ordinal);
""";

return test.RunAsync();
}

[Fact]
public Task StringConvertedToSpan_NonEmptyConstant()
{
var test = CreateTest();
test.TestCode = """
using System;
string value = null;
_ = {|MA0128:MemoryExtensions.SequenceEqual<char>(value, "abc")|};
_ = {|MA0128:MemoryExtensions.Equals(value, "abc", StringComparison.Ordinal)|};
""";
test.FixedCode = """
using System;
string value = null;
_ = value is "abc";
_ = value is "abc";
""";

return test.RunAsync();
}

[Fact]
public Task ReadOnlySpanChar_SequenceEqual_EmptyConstant()
{
var test = CreateTest();
test.TestCode = """
using System;
ReadOnlySpan<char> value = default;
_ = {|MA0128:value.SequenceEqual("")|};
""";
test.FixedCode = """
using System;
ReadOnlySpan<char> value = default;
_ = value is "";
""";

return test.RunAsync();
}
}
Loading