From 63b2a85cd3e8dfbaaa6ff65849296c0410c0b1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 22 Sep 2026 21:12:04 -0400 Subject: [PATCH 1/2] Fix MA0128 behavior change when the span is a string --- docs/Rules/MA0128.md | 2 ++ .../UseIsPatternInsteadOfSequenceEqualAnalyzer.cs | 4 ++++ ...IsPatternInsteadOfSequenceEqualAnalyzerTests.cs | 14 ++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/docs/Rules/MA0128.md b/docs/Rules/MA0128.md index 62cca9185..d49ae52f2 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 the span is a `string` converted to a span. A `null` string is converted to an empty span, so `MemoryExtensions.SequenceEqual(value, "")` is `true` when `value` is `null`, whereas `value is ""` is `false`. diff --git a/src/Meziantou.Analyzer/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzer.cs index 8edd15545..10d8fae2c 100644 --- a/src/Meziantou.Analyzer/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzer.cs @@ -45,6 +45,10 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy if (!method.ContainingType.IsEqualTo(memoryExtensionsSymbol)) return; + // A null string is converted to an empty span, so it is equal to an empty string, whereas the 'is' pattern is false for null + if (operation.Arguments.Length > 0 && operation.Arguments[0].Value.UnwrapImplicitConversions().Type is { SpecialType: SpecialType.System_String }) + return; + if (method.Name is "SequenceEqual" && method.Parameters.Length == 2 && method.Parameters[0].Type.IsEqualToAny(readOnlySpanCharSymbol, spanCharSymbol)) { if (IsConstantValue(operation.Arguments[1].Value)) diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzerTests.cs index 0965ef592..ed3c9a28d 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzerTests.cs @@ -223,4 +223,18 @@ public Task SpanChar_SequenceEqual() return test.RunAsync(); } + + [Fact] + public Task StringConvertedToSpan() + { + var test = CreateTest(); + test.TestCode = """ + using System; + string value = null; + _ = MemoryExtensions.SequenceEqual(value, ""); + _ = MemoryExtensions.Equals(value, "", StringComparison.Ordinal); + """; + + return test.RunAsync(); + } } From f88711fb95ca0883b0057a50ae7667701fb8600f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 22 Sep 2026 21:14:23 -0400 Subject: [PATCH 2/2] Only skip MA0128 when a string span is compared to an empty string --- docs/Rules/MA0128.md | 2 +- ...IsPatternInsteadOfSequenceEqualAnalyzer.cs | 17 ++++---- ...ternInsteadOfSequenceEqualAnalyzerTests.cs | 40 ++++++++++++++++++- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/docs/Rules/MA0128.md b/docs/Rules/MA0128.md index d49ae52f2..525d44b4a 100644 --- a/docs/Rules/MA0128.md +++ b/docs/Rules/MA0128.md @@ -15,4 +15,4 @@ _ = span.SequenceEqual("bar"); // report diagnostic _ = span is "bar"; ```` -The rule is not reported when the span is a `string` converted to a span. 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 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 10d8fae2c..097148802 100644 --- a/src/Meziantou.Analyzer/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzer.cs @@ -45,29 +45,28 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy if (!method.ContainingType.IsEqualTo(memoryExtensionsSymbol)) return; - // A null string is converted to an empty span, so it is equal to an empty string, whereas the 'is' pattern is false for null - if (operation.Arguments.Length > 0 && operation.Arguments[0].Value.UnwrapImplicitConversions().Type is { SpecialType: SpecialType.System_String }) - return; - 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 ed3c9a28d..60fea428f 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseIsPatternInsteadOfSequenceEqualAnalyzerTests.cs @@ -225,7 +225,7 @@ public Task SpanChar_SequenceEqual() } [Fact] - public Task StringConvertedToSpan() + public Task StringConvertedToSpan_EmptyConstant() { var test = CreateTest(); test.TestCode = """ @@ -237,4 +237,42 @@ public Task StringConvertedToSpan() 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(); + } }