diff --git a/docs/Rules/MA0128.md b/docs/Rules/MA0128.md index 62cca9185..525d44b4a 100644 --- a/docs/Rules/MA0128.md +++ b/docs/Rules/MA0128.md @@ -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(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. diff --git a/src/Meziantou.Analyzer/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzer.cs index 8edd15545..097148802 100644 --- a/src/Meziantou.Analyzer/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzer.cs @@ -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) diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzerTests.cs index 0965ef592..60fea428f 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzerTests.cs @@ -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(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(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 value = default; + _ = {|MA0128:value.SequenceEqual("")|}; + """; + test.FixedCode = """ + using System; + ReadOnlySpan value = default; + _ = value is ""; + """; + + return test.RunAsync(); + } }