Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,28 @@ 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)
{
// Only apply rule to System.String or J2N.StringBuilderExtensions containing type
if (!ContainingTypeIsStringOrJ2N(methodSymbol.ContainingType))
return;

if (!ValueParameterIsString(methodSymbol))
return;

// If the method has StringComparison parameter in signature
bool methodHasComparisonParam = HasStringComparisonParameter(methodSymbol, stringComparisonType);

Expand Down Expand Up @@ -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 first parameter
var relevantCandidates = candidateSymbols
.Where(c => ContainingTypeIsStringOrJ2N(c.ContainingType))
.Where(c => ContainingTypeIsStringOrJ2N(c.ContainingType) && ValueParameterIsString(c))
.ToImmutableArray();

if (relevantCandidates.Length == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,80 @@ 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,
ExpectedDiagnostics = { } // StartsWith(char) has no StringComparison overload; no diagnostic
};

await test.RunAsync();
}

[Test]
public async Task Detects_IndexOf_MissingStringComparison()
{
Expand Down Expand Up @@ -507,5 +581,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();
}
}
}