diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonAnalyzer.cs index e395d65..97846fa 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonAnalyzer.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonAnalyzer.cs @@ -118,6 +118,18 @@ static bool HasStringComparisonParameter(IMethodSymbol? m, INamedTypeSymbol scTy var (hasStringComparisonArg, isValidValue, invalidArgLocation, comparisonValueName) = CheckStringComparisonArgument(invocation, semantic, stringComparisonType); + // The rule only applies to overloads whose value parameter is a string — + // overloads like IndexOf(char) / StartsWith(char) have no StringComparison sibling. + // For non-reduced extension method calls (e.g. StringBuilderExtensions.IndexOf(sb, "x")), + // Parameters[0] is the receiver, so skip past it. + static bool ValueParameterIsString(IMethodSymbol? m) + { + if (m == null || m.Parameters.Length == 0) return false; + int valueIndex = m.IsExtensionMethod && m.ReducedFrom == null ? 1 : 0; + return m.Parameters.Length > valueIndex + && m.Parameters[valueIndex].Type.SpecialType == SpecialType.System_String; + } + // If resolved symbol available if (methodSymbol != null) { @@ -125,6 +137,9 @@ static bool HasStringComparisonParameter(IMethodSymbol? m, INamedTypeSymbol scTy if (!ContainingTypeIsStringOrJ2N(methodSymbol.ContainingType)) return; + if (!ValueParameterIsString(methodSymbol)) + return; + // If the method has StringComparison parameter in signature bool methodHasComparisonParam = HasStringComparisonParameter(methodSymbol, stringComparisonType); @@ -160,9 +175,9 @@ static bool HasStringComparisonParameter(IMethodSymbol? m, INamedTypeSymbol scTy // Handle ambiguous candidates if (candidateSymbols.Length > 0) { - // Check if any candidate is from String or J2N types + // Check if any candidate is from String or J2N types and takes a string value parameter var relevantCandidates = candidateSymbols - .Where(c => ContainingTypeIsStringOrJ2N(c.ContainingType)) + .Where(c => ContainingTypeIsStringOrJ2N(c.ContainingType) && ValueParameterIsString(c)) .ToImmutableArray(); if (relevantCandidates.Length == 0) diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_6002_StringComparisonAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_6002_StringComparisonAnalyzer.cs index fcc4b1d..97a5d01 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_6002_StringComparisonAnalyzer.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_6002_StringComparisonAnalyzer.cs @@ -78,6 +78,81 @@ public void M() await test.RunAsync(); } + [Test] + public async Task Skips_IndexOf_CharLiteral() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int index = text.IndexOf('H'); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } // IndexOf(char) has no StringComparison overload; no diagnostic + }; + + await test.RunAsync(); + } + + [Test] + public async Task Skips_IndexOf_CharVariable() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + char c = 'H'; + int index = text.IndexOf(c); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } // IndexOf(char) has no StringComparison overload; no diagnostic + }; + + await test.RunAsync(); + } + + [Test] + public async Task Skips_StartsWith_CharVariable() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + char c = 'H'; + bool starts = text.StartsWith(c); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) + { + TestCode = testCode, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, // string.StartsWith(char) requires .NET Core 2.0+ + ExpectedDiagnostics = { } // StartsWith(char) has no StringComparison overload; no diagnostic + }; + + await test.RunAsync(); + } + [Test] public async Task Detects_IndexOf_MissingStringComparison() { @@ -507,5 +582,82 @@ public void M() await test.RunAsync(); } + + [Test] + public async Task Detects_J2NStringBuilderExtensions_ReducedForm_MissingStringComparison() + { + var testCode = @" +using System.Text; +using J2N.Text; + +namespace J2N.Text +{ + public static class StringBuilderExtensions + { + public static int IndexOf(this StringBuilder sb, string value) => 0; + } +} + +public class Sample +{ + public void M() + { + var sb = new StringBuilder(""hello""); + int index = sb.IndexOf(""he""); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison.Id, DiagnosticSeverity.Error) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 18, column: 24); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_J2NStringBuilderExtensions_StaticForm_MissingStringComparison() + { + var testCode = @" +using System.Text; + +namespace J2N.Text +{ + public static class StringBuilderExtensions + { + public static int IndexOf(this StringBuilder sb, string value) => 0; + } +} + +public class Sample +{ + public void M() + { + var sb = new StringBuilder(""hello""); + int index = J2N.Text.StringBuilderExtensions.IndexOf(sb, ""he""); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison.Id, DiagnosticSeverity.Error) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 17, column: 54); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } } }