From f5a6c1a6079abe5d07346d7952e5542f419f7f18 Mon Sep 17 00:00:00 2001 From: Nehan Pathan Date: Sun, 19 Oct 2025 20:33:43 +0530 Subject: [PATCH 1/4] Add analyzers, codefixes, sample and test files for 6001, 6002, 6003 --- Directory.Packages.props | 2 - ...Dev6001_StringComparisonCodeFixProvider.cs | 124 +++++ ...neDev6002_SpanComparisonCodeFixProvider.cs | 237 +++++++++ ...Dev6003_SingleCharStringCodeFixProvider.cs | 127 +++++ .../Lucene.Net.CodeAnalysis.Dev.Sample.csproj | 20 +- .../LuceneDev6001_StringComparisonSample.cs | 53 ++ .../LuceneDev6002_SpanComparisonSample.cs | 108 ++++ .../LuceneDev6003_SingleCharStringSample.cs | 58 ++ .../AnalyzerReleases.Unshipped.md | 6 +- .../LuceneDev6001_StringComparisonAnalyzer.cs | 228 ++++++++ .../LuceneDev6002_SpanComparisonAnalyzer.cs | 214 ++++++++ .../LuceneDev6003_SingleCharStringAnalyzer.cs | 229 ++++++++ .../Utility/Descriptors.LuceneDev6xxx.cs | 70 +++ .../Utility/Descriptors.cs | 4 +- ...Dev6001_StringComparisonCodeFixProvider.cs | 498 ++++++++++++++++++ ...neDev6002_SpanComparisonCodeFixProvider.cs | 191 +++++++ ...Dev6003_SingleCharStringCodeFixProvider.cs | 177 +++++++ ...tLuceneDev6001_StringComparisonAnalyzer.cs | 461 ++++++++++++++++ ...estLuceneDev6002_SpanComparisonAnalyzer.cs | 339 ++++++++++++ ...tLuceneDev6003_SingleCharStringAnalyzer.cs | 111 ++++ 20 files changed, 3236 insertions(+), 21 deletions(-) create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs create mode 100644 src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs create mode 100644 tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs create mode 100644 tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs create mode 100644 tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs create mode 100644 tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs create mode 100644 tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonAnalyzer.cs create mode 100644 tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringAnalyzer.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index b4b8e75..8796964 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,11 +23,9 @@ true true - 5.3.0 - diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs new file mode 100644 index 0000000..c2e274b --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6001_StringComparisonCodeFixProvider)), Shared] + public sealed class LuceneDev6001_StringComparisonCodeFixProvider : CodeFixProvider + { + private const string TitleOrdinal = "Use StringComparison.Ordinal"; + private const string TitleOrdinalIgnoreCase = "Use StringComparison.OrdinalIgnoreCase"; + + public override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create( + Descriptors.LuceneDev6001_MissingStringComparison.Id, + Descriptors.LuceneDev6001_InvalidStringComparison.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root == null) return; + + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var invocation = root.FindToken(diagnosticSpan.Start) + .Parent? + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (invocation == null) return; + + // Offer both Ordinal and OrdinalIgnoreCase fixes + context.RegisterCodeFix(CodeAction.Create( + title: TitleOrdinal, + createChangedDocument: c => FixInvocationAsync(context.Document, invocation, "Ordinal", c), + equivalenceKey: TitleOrdinal), + diagnostic); + + context.RegisterCodeFix(CodeAction.Create( + title: TitleOrdinalIgnoreCase, + createChangedDocument: c => FixInvocationAsync(context.Document, invocation, "OrdinalIgnoreCase", c), + equivalenceKey: TitleOrdinalIgnoreCase), + diagnostic); + } + + private static async Task FixInvocationAsync(Document document, InvocationExpressionSyntax invocation, string comparisonMember, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root == null) return document; + + // Create the StringComparison expression + var stringComparisonExpr = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("StringComparison"), + SyntaxFactory.IdentifierName(comparisonMember)); + + var newArg = SyntaxFactory.Argument(stringComparisonExpr); + + // Check if a StringComparison argument already exists + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var stringComparisonType = semanticModel?.Compilation.GetTypeByMetadataName("System.StringComparison"); + var existingArg = invocation.ArgumentList.Arguments.FirstOrDefault(arg => + semanticModel != null && + (SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arg.Expression).Type, stringComparisonType) || + (semanticModel.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && SymbolEqualityComparer.Default.Equals(f.ContainingType, stringComparisonType)))); + + // Replace existing argument or add new one + var newInvocation = existingArg != null + ? invocation.ReplaceNode(existingArg, newArg) + : invocation.WithArgumentList(invocation.ArgumentList.AddArguments(newArg)); + + // Combine adding 'using System;' and replacing invocation in a single root + var newRoot = EnsureSystemUsing(root).ReplaceNode(invocation, newInvocation); + + return document.WithSyntaxRoot(newRoot); + } + + private static SyntaxNode EnsureSystemUsing(SyntaxNode root) + { + if (root is CompilationUnitSyntax compilationUnit) + { + var hasSystemUsing = compilationUnit.Usings.Any(u => + u.Name is IdentifierNameSyntax id && id.Identifier.ValueText == "System"); + + if (!hasSystemUsing) + { + var systemUsing = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("System")) + .WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); + return compilationUnit.AddUsings(systemUsing); + } + } + + return root; + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs new file mode 100644 index 0000000..5e66bc4 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6002_SpanComparisonCodeFixProvider)), Shared] + public sealed class LuceneDev6002_SpanComparisonCodeFixProvider : CodeFixProvider + { + private const string TitleRemoveOrdinal = "Remove redundant StringComparison.Ordinal"; + private const string TitleReplaceWithOrdinal = "Replace with StringComparison.Ordinal"; + private const string TitleReplaceWithOrdinalIgnoreCase = "Replace with StringComparison.OrdinalIgnoreCase"; + + public override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create( + Descriptors.LuceneDev6002_RedundantOrdinal.Id, + Descriptors.LuceneDev6002_InvalidComparison.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root == null) + return; + + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + var invocation = root.FindNode(diagnosticSpan).FirstAncestorOrSelf(); + if (invocation == null) + return; + + switch (diagnostic.Id) + { + case var id when id == Descriptors.LuceneDev6002_RedundantOrdinal.Id: + context.RegisterCodeFix( + CodeAction.Create( + title: "Remove redundant StringComparison.Ordinal", + createChangedDocument: c => RemoveStringComparisonArgumentAsync(context.Document, invocation, c), + equivalenceKey: "RemoveRedundantOrdinal"), + diagnostic); + break; + + case var id when id == Descriptors.LuceneDev6002_InvalidComparison.Id: + context.RegisterCodeFix( + CodeAction.Create( + title: "Use StringComparison.Ordinal", + createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "Ordinal", c), + equivalenceKey: "ReplaceWithOrdinal"), + diagnostic); + + context.RegisterCodeFix( + CodeAction.Create( + title: "Use StringComparison.OrdinalIgnoreCase", + createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "OrdinalIgnoreCase", c), + equivalenceKey: "ReplaceWithOrdinalIgnoreCase"), + diagnostic); + break; + } + } + + private static async Task RemoveStringComparisonArgumentAsync( + Document document, + InvocationExpressionSyntax invocation, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root == null) + return document; + + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (semanticModel == null) + return document; + + var compilation = semanticModel.Compilation; + var stringComparisonType = compilation.GetTypeByMetadataName("System.StringComparison"); + if (stringComparisonType == null) + return document; + + // Find the StringComparison argument + ArgumentSyntax? argumentToRemove = null; + foreach (var arg in invocation.ArgumentList.Arguments) + { + var argType = semanticModel.GetTypeInfo(arg.Expression, cancellationToken).Type; + if (argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType)) + { + argumentToRemove = arg; + break; + } + + // fallback: check if it's a member access of StringComparison.* + if (argumentToRemove == null && arg.Expression is MemberAccessExpressionSyntax member && + member.Expression is IdentifierNameSyntax idName && + idName.Identifier.ValueText == "StringComparison") + { + argumentToRemove = arg; + break; + } + + } + + if (argumentToRemove == null) + return document; + + // Remove the argument and normalize formatting + var newArguments = invocation.ArgumentList.Arguments.Remove(argumentToRemove); + var newArgumentList = invocation.ArgumentList.WithArguments(newArguments); + var newInvocation = invocation.WithArgumentList(newArgumentList) + .WithTriviaFrom(invocation) // preserve trivia + .NormalizeWhitespace(); // clean formatting + + var newRoot = root.ReplaceNode(invocation, newInvocation); + return document.WithSyntaxRoot(newRoot); + } + + private static async Task ReplaceWithStringComparisonAsync( + Document document, + InvocationExpressionSyntax invocation, + string comparisonMember, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root == null) + return document; + + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (semanticModel == null) + return document; + + var compilation = semanticModel.Compilation; + var stringComparisonType = compilation.GetTypeByMetadataName("System.StringComparison"); + if (stringComparisonType == null) + return document; + + // Find the StringComparison argument + ArgumentSyntax? argumentToReplace = null; + foreach (var arg in invocation.ArgumentList.Arguments) + { + var argType = semanticModel.GetTypeInfo(arg.Expression, cancellationToken).Type; + if (argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType)) + { + argumentToReplace = arg; + break; + } + + // fallback: check if it's a member access of StringComparison.* + if (argumentToReplace == null && arg.Expression is MemberAccessExpressionSyntax member && + member.Expression is IdentifierNameSyntax idName && + idName.Identifier.ValueText == "StringComparison") + { + argumentToReplace = arg; + break; + } + + } + + if (argumentToReplace == null) + return document; + + // Check if argument already uses System.StringComparison + bool isFullyQualified = argumentToReplace.Expression.ToString().StartsWith("System.StringComparison"); + + // Create new StringComparison expression + var baseExpression = isFullyQualified + ? (ExpressionSyntax)SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("System"), + SyntaxFactory.IdentifierName("StringComparison")) + : SyntaxFactory.IdentifierName("StringComparison"); + + var newExpression = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + baseExpression, + SyntaxFactory.IdentifierName(comparisonMember)); + + + var newArgument = argumentToReplace.WithExpression(newExpression); + var newInvocation = invocation.ReplaceNode(argumentToReplace, newArgument) + .WithTriviaFrom(invocation) + .NormalizeWhitespace(); + + var newRoot = root; + if (!isFullyQualified) + { + newRoot = EnsureSystemUsing(newRoot); + } + newRoot = newRoot.ReplaceNode(invocation, newInvocation); + return document.WithSyntaxRoot(newRoot); + } + + private static SyntaxNode EnsureSystemUsing(SyntaxNode root) + { + if (root is CompilationUnitSyntax compilationUnit) + { + var hasSystemUsing = compilationUnit.Usings.Any(u => + u.Name is IdentifierNameSyntax id && id.Identifier.ValueText == "System"); + + // only add if missing + if (!hasSystemUsing) + { + var systemUsing = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("System")) + .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed); + + return compilationUnit.AddUsings(systemUsing); + } + } + + return root; + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs new file mode 100644 index 0000000..b605425 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6003_SingleCharStringCodeFixProvider))] + [Shared] + public sealed class LuceneDev6003_SingleCharStringCodeFixProvider : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds + => ImmutableArray.Create(Descriptors.LuceneDev6003_SingleCharStringAnalyzer.Id); + + public override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics[0]; + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root == null) + return; + + var diagnosticSpan = diagnostic.Location.SourceSpan; + var node = root.FindNode(diagnosticSpan); + + if (node is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression)) + { + context.RegisterCodeFix( + CodeAction.Create( + "Use char literal", + c => ReplaceWithCharLiteralAsync(context.Document, literal, c), + nameof(LuceneDev6003_SingleCharStringCodeFixProvider)), + diagnostic); + } + } + + private static async Task ReplaceWithCharLiteralAsync( + Document document, + LiteralExpressionSyntax stringLiteral, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root == null) + return document; + + // Get the original escaped token text (e.g., "\"", "\n", "H") + var token = stringLiteral.Token; + + // Get unescaped value + var valueText = token.ValueText; + if (string.IsNullOrEmpty(valueText) || valueText.Length != 1) + return document; + + char ch = valueText[0]; + + // Escape it properly as a char literal + string escapedCharText = EscapeCharLiteral(ch); + var charLiteral = SyntaxFactory.LiteralExpression( + SyntaxKind.CharacterLiteralExpression, + SyntaxFactory.Literal(escapedCharText, ch)); + + var newRoot = root.ReplaceNode(stringLiteral, charLiteral); + return document.WithSyntaxRoot(newRoot); + } + + private static string EscapeCharLiteral(char ch) + { + switch (ch) + { + case '\'': + return @"'\''"; // escape single quote + case '\\': + return @"'\\'"; // escape backslash + case '\n': + return @"'\n'"; + case '\r': + return @"'\r'"; + case '\t': + return @"'\t'"; + case '\0': + return @"'\0'"; + case '\b': + return @"'\b'"; + case '\f': + return @"'\f'"; + case '\v': + return @"'\v'"; + default: + // Printable character or Unicode escape + if (char.IsControl(ch) || char.IsSurrogate(ch)) + { + // Unicode escape sequence + return $"'\\u{((int)ch).ToString("X4", CultureInfo.InvariantCulture)}'"; + } + return $"'{ch}'"; + } + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj b/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj index baa0e34..d29b0fc 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/Lucene.Net.CodeAnalysis.Dev.Sample.csproj @@ -1,4 +1,4 @@ - + - + - + - + diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs new file mode 100644 index 0000000..638909a --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +namespace Lucene.Net.CodeAnalysis.Dev.Sample; + +public class LuceneDev6001_StringComparisonSample +{ + // public void BadExample_MissingStringComparison() + // { + // string text = "Hello World"; + + // //Missing StringComparison parameter + // int index = text.IndexOf("Hello"); + // bool starts = text.StartsWith("Hello"); + // bool ends = text.EndsWith("World"); + // } + + public void GoodExample_Ordinal() + { + string text = "Hello World"; + + //Correct usage with StringComparison.Ordinal + int index = text.IndexOf("Hello", System.StringComparison.Ordinal); + bool starts = text.StartsWith("Hello", System.StringComparison.Ordinal); + bool ends = text.EndsWith("World", System.StringComparison.Ordinal); + } + + public void GoodExample_OrdinalIgnoreCase() + { + string text = "Hello World"; + + // Correct usage with StringComparison.OrdinalIgnoreCase + int index = text.IndexOf("hello", System.StringComparison.OrdinalIgnoreCase); + bool starts = text.StartsWith("HELLO", System.StringComparison.OrdinalIgnoreCase); + bool ends = text.EndsWith("world", System.StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs new file mode 100644 index 0000000..5f16f62 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using System; + +namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev6xxx +{ + /// + /// Sample code demonstrating LuceneDev6002 analyzer rules for Span types. + /// Rule: Span types should not use StringComparison.Ordinal (redundant) + /// and must only use Ordinal or OrdinalIgnoreCase. + /// + public class LuceneDev6002_SpanComparisonSample + { + // public void BadExamples_RedundantOrdinal() + // { + // ReadOnlySpan span = "Hello World".AsSpan(); + + // // Redundant StringComparison.Ordinal + // int index1 = span.IndexOf("Hello".AsSpan(), StringComparison.Ordinal); + // int index2 = span.LastIndexOf("World".AsSpan(), StringComparison.Ordinal); + // bool starts = span.StartsWith("Hello".AsSpan(), StringComparison.Ordinal); + // bool ends = span.EndsWith("World".AsSpan(), StringComparison.Ordinal); + // } + + // public void BadExamples_InvalidComparison() + // { + // ReadOnlySpan span = "Hello World".AsSpan(); + + // // Culture-sensitive comparisons are not allowed on Span types + // int index1 = span.IndexOf("Hello", StringComparison.CurrentCulture); + // int index2 = span.LastIndexOf("World", StringComparison.CurrentCultureIgnoreCase); + // bool starts = span.StartsWith("Hello", StringComparison.InvariantCulture); + // bool ends = span.EndsWith("World", StringComparison.InvariantCultureIgnoreCase); + // } + + public void GoodExamples_NoStringComparison() + { + ReadOnlySpan span = "Hello World".AsSpan(); + + // Correct: defaults to Ordinal + int index1 = span.IndexOf("Hello".AsSpan()); + int index2 = span.LastIndexOf("World".AsSpan()); + bool starts = span.StartsWith("Hello".AsSpan()); + bool ends = span.EndsWith("World".AsSpan()); + + // Single char operations + int charIndex = span.IndexOf('H'); + bool startsWithChar = span[0] == 'H'; + } + + public void GoodExamples_WithOrdinalIgnoreCase() + { + ReadOnlySpan span = "Hello World".AsSpan(); + + // Correct: case-insensitive search + int index = span.IndexOf("hello", StringComparison.OrdinalIgnoreCase); + int lastIndex = span.LastIndexOf("WORLD", StringComparison.OrdinalIgnoreCase); + bool starts = span.StartsWith("HELLO", StringComparison.OrdinalIgnoreCase); + bool ends = span.EndsWith("world", StringComparison.OrdinalIgnoreCase); + } + + public void RealWorldExamples() + { + string path = @"C:\Users\Documents\file.txt"; + ReadOnlySpan pathSpan = path.AsSpan(); + + // Correct: OrdinalIgnoreCase allowed + bool isTxtFile = pathSpan.EndsWith(".txt", StringComparison.OrdinalIgnoreCase); + + // Correct: No StringComparison needed + ReadOnlySpan url = "https://example.com".AsSpan(); + bool isHttps = url.StartsWith("https://"); + + ReadOnlySpan token = "Bearer:abc123".AsSpan(); + int separatorIndex = token.IndexOf(':'); + } + + public void StringTypeComparison() + { + // Analyzer applies only to Span types + string text = "Hello World"; + + // String types require StringComparison + int index = text.IndexOf("Hello", StringComparison.Ordinal); + + // Span types should not specify Ordinal + ReadOnlySpan span = text.AsSpan(); + int index2 = span.IndexOf("Hello"); + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs new file mode 100644 index 0000000..460737a --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using System; + +namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev6xxx +{ + /// + /// Sample code for LuceneDev6003: Suggest using char overloads instead of single-character string literals. + /// + public class LuceneDev6003_SingleCharStringSample + { + public void Example() + { + string input = "Hello"; + + // BAD: Using string.Equals with single-character string literal + // if (string.Equals(input[0].ToString(), "H")) + // { + // Console.WriteLine("Starts with H"); + // } + + // BAD: Using Equals instance method + // if (input[0].ToString().Equals("H")) + // { + // Console.WriteLine("Starts with H"); + // } + + // GOOD: Using char comparison instead of string + if (input[0] == 'H') + { + Console.WriteLine("Starts with H"); + } + + //GOOD: Using Char.Equals + if (char.Equals(input[0], 'H')) + { + Console.WriteLine("Starts with H"); + } + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md index 30950e2..af77060 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md @@ -5,6 +5,8 @@ Rule ID | Category | Severity | Notes LuceneDev1007 | Design | Warning | Generic Dictionary indexer should not be used to retrieve values because it may throw KeyNotFoundException (value type value) LuceneDev1008 | Design | Warning | Generic Dictionary indexer should not be used to retrieve values because it may throw KeyNotFoundException (reference type value) LuceneDev6000 | Usage | Info | IDictionary indexer may be used to retrieve values, but must be checked for null before using the value -LuceneDev6001 | Usage | Error | String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf must be called with StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase -LuceneDev6002 | Usage | Warning | Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf should not pass non-Ordinal StringComparison +LuceneDev6001_1 | Usage | Error | Missing StringComparison argument in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; must use Ordinal/OrdinalIgnoreCase +LuceneDev6001_2 | Usage | Warning | Invalid StringComparison value in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal/OrdinalIgnoreCase allowed +LuceneDev6002_1 | Usage | Warning | Redundant StringComparison.Ordinal argument in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; should be removed +LuceneDev6002_2 | Usage | Error | Invalid StringComparison value in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal or OrdinalIgnoreCase allowed LuceneDev6003 | Usage | Info | Single-character string arguments should use the char overload of StartsWith/EndsWith/IndexOf/LastIndexOf instead of a string diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs new file mode 100644 index 0000000..263bc8f --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Linq; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class LuceneDev6001_StringComparisonAnalyzer : DiagnosticAnalyzer + { + private static readonly ImmutableHashSet TargetMethodNames = + ImmutableHashSet.Create("StartsWith", "EndsWith", "IndexOf", "LastIndexOf"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create( + Descriptors.LuceneDev6001_MissingStringComparison, + Descriptors.LuceneDev6001_InvalidStringComparison); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) + { + var invocation = (InvocationExpressionSyntax)ctx.Node; + + if (!(invocation.Expression is MemberAccessExpressionSyntax memberAccess)) + return; + + var methodName = memberAccess.Name.Identifier.ValueText; + if (!TargetMethodNames.Contains(methodName)) + return; + + var semantic = ctx.SemanticModel; + var compilation = semantic.Compilation; + var stringComparisonType = compilation.GetTypeByMetadataName("System.StringComparison"); + + if (stringComparisonType == null) + return; + + // Get symbol info + var symbolInfo = semantic.GetSymbolInfo(memberAccess); + var methodSymbol = symbolInfo.Symbol as IMethodSymbol; + var candidateSymbols = symbolInfo.CandidateSymbols.OfType().ToImmutableArray(); + + // Determine if containing type qualifies: System.String or J2N.StringBuilderExtensions variants + static bool ContainingTypeIsStringOrJ2N(INamedTypeSymbol? containingType) + { + if (containingType == null) return false; + if (containingType.SpecialType == SpecialType.System_String) + return true; + + // Accept both "J2N.Text.StringBuilderExtensions" and "J2N.StringBuilderExtensions" + var fullname = containingType.ToDisplayString(); + return fullname == "J2N.Text.StringBuilderExtensions" || fullname == "J2N.StringBuilderExtensions"; + } + + // Check if method has StringComparison parameter + static bool HasStringComparisonParameter(IMethodSymbol? m, INamedTypeSymbol scType) + { + if (m == null) return false; + return m.Parameters.Any(p => SymbolEqualityComparer.Default.Equals(p.Type, scType)); + } + + // Check if invocation has StringComparison argument and validate it + var (hasStringComparisonArg, isValidValue, invalidArgLocation) = + CheckStringComparisonArgument(invocation, semantic, stringComparisonType); + + // If resolved symbol available + if (methodSymbol != null) + { + // Only apply rule to System.String or J2N.StringBuilderExtensions containing type + if (!ContainingTypeIsStringOrJ2N(methodSymbol.ContainingType)) + return; + + // If the method has StringComparison parameter in signature + bool methodHasComparisonParam = HasStringComparisonParameter(methodSymbol, stringComparisonType); + + if (hasStringComparisonArg) + { + // Argument is present - check if it's valid + if (!isValidValue) + { + var diag = Diagnostic.Create( + Descriptors.LuceneDev6001_InvalidStringComparison, + invalidArgLocation ?? memberAccess.Name.GetLocation(), + methodName); + ctx.ReportDiagnostic(diag); + } + return; + } + + // No StringComparison argument provided + if (!methodHasComparisonParam) + { + // Method doesn't have StringComparison parameter - report error + var diag = Diagnostic.Create( + Descriptors.LuceneDev6001_MissingStringComparison, + memberAccess.Name.GetLocation(), + methodName); + ctx.ReportDiagnostic(diag); + } + + return; + } + + // Handle ambiguous candidates + if (candidateSymbols.Length > 0) + { + // Check if any candidate is from String or J2N types + var relevantCandidates = candidateSymbols + .Where(c => ContainingTypeIsStringOrJ2N(c.ContainingType)) + .ToImmutableArray(); + + if (relevantCandidates.Length == 0) + return; + + // If StringComparison argument is provided + if (hasStringComparisonArg) + { + if (!isValidValue) + { + var diag = Diagnostic.Create( + Descriptors.LuceneDev6001_InvalidStringComparison, + invalidArgLocation ?? memberAccess.Name.GetLocation(), + methodName); + ctx.ReportDiagnostic(diag); + } + return; + } + + // No StringComparison argument - check if any candidate has it + bool anyCandidateHasComparison = relevantCandidates + .Any(c => HasStringComparisonParameter(c, stringComparisonType)); + + if (!anyCandidateHasComparison) + { + // None of the candidates have StringComparison parameter + var diag = Diagnostic.Create( + Descriptors.LuceneDev6001_MissingStringComparison, + memberAccess.Name.GetLocation(), + methodName); + ctx.ReportDiagnostic(diag); + } + } + } + + private static (bool hasArgument, bool isValid, Location? location) CheckStringComparisonArgument( + InvocationExpressionSyntax invocation, + SemanticModel semantic, + INamedTypeSymbol stringComparisonType) + { + foreach (var arg in invocation.ArgumentList.Arguments) + { + var argType = semantic.GetTypeInfo(arg.Expression).Type; + + // Check if argument type is StringComparison + if (argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType)) + { + bool isValid = IsValidStringComparisonValue(semantic, arg.Expression, stringComparisonType); + return (true, isValid, arg.Expression.GetLocation()); + } + + // Also check for enum member access (e.g., StringComparison.Ordinal) + var argSymbol = semantic.GetSymbolInfo(arg.Expression).Symbol as IFieldSymbol; + if (argSymbol != null && SymbolEqualityComparer.Default.Equals(argSymbol.ContainingType, stringComparisonType)) + { + bool isValid = IsValidStringComparisonValue(semantic, arg.Expression, stringComparisonType); + return (true, isValid, arg.Expression.GetLocation()); + } + } + + return (false, true, null); + } + + private static bool IsValidStringComparisonValue( + SemanticModel semantic, + ExpressionSyntax expression, + INamedTypeSymbol stringComparisonType) + { + // Get the constant value if available + var constantValue = semantic.GetConstantValue(expression); + if (constantValue.HasValue && constantValue.Value is int intValue) + { + // StringComparison.Ordinal = 4, OrdinalIgnoreCase = 5 + return intValue == 4 || intValue == 5; + } + + // Try to get field symbol + var symbolInfo = semantic.GetSymbolInfo(expression); + var fieldSymbol = symbolInfo.Symbol as IFieldSymbol; + + if (fieldSymbol != null && SymbolEqualityComparer.Default.Equals(fieldSymbol.ContainingType, stringComparisonType)) + { + var memberName = fieldSymbol.Name; + return memberName == "Ordinal" || memberName == "OrdinalIgnoreCase"; + } + + // If we can't determine, be conservative and allow it + return true; + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs new file mode 100644 index 0000000..363858e --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Linq; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class LuceneDev6002_SpanComparisonAnalyzer : DiagnosticAnalyzer + { + private static readonly ImmutableHashSet TargetMethodNames = + ImmutableHashSet.Create("StartsWith", "EndsWith", "IndexOf", "LastIndexOf"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create( + Descriptors.LuceneDev6002_RedundantOrdinal, + Descriptors.LuceneDev6002_InvalidComparison); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) + { + var invocation = (InvocationExpressionSyntax)ctx.Node; + + if (!(invocation.Expression is MemberAccessExpressionSyntax memberAccess)) + return; + + var methodName = memberAccess.Name.Identifier.ValueText; + if (!TargetMethodNames.Contains(methodName)) + return; + + var semantic = ctx.SemanticModel; + var compilation = semantic.Compilation; + var stringComparisonType = compilation.GetTypeByMetadataName("System.StringComparison"); + + if (stringComparisonType == null) + return; + + // Get symbol info + var symbolInfo = semantic.GetSymbolInfo(memberAccess); + var methodSymbol = symbolInfo.Symbol as IMethodSymbol; + var candidateSymbols = symbolInfo.CandidateSymbols.OfType().ToImmutableArray(); + + // Determine if this is a span-like type + var receiverType = semantic.GetTypeInfo(memberAccess.Expression).Type; + + // Check if calling on System.String - if so, skip (handled by LuceneDev6001) + if (receiverType != null && receiverType.SpecialType == SpecialType.System_String) + return; + + // Check if receiver is span-like + bool isSpanLike = IsSpanLikeReceiver(receiverType); + + // If not span-like based on receiver, check method symbol + if (!isSpanLike && methodSymbol != null) + { + isSpanLike = IsSpanLikeReceiver(methodSymbol.ContainingType); + } + + // Check candidates if still not determined + if (!isSpanLike && candidateSymbols.Length > 0) + { + isSpanLike = candidateSymbols.Any(c => IsSpanLikeReceiver(c.ContainingType)); + } + + if (!isSpanLike) + return; + + // Check if this is a char overload - ignore those + if (methodSymbol != null && IsCharOverload(methodSymbol)) + return; + + if (candidateSymbols.Length > 0 && candidateSymbols.All(c => IsCharOverload(c))) + return; + + // Check for StringComparison argument + var (hasComparison, comparisonValue, argLocation) = + CheckStringComparisonArgument(invocation, semantic, stringComparisonType); + + if (!hasComparison) + { + // No StringComparison argument - this is OK for span types (default is Ordinal) + return; + } + + // Has StringComparison argument - validate it + if (comparisonValue == "Ordinal") + { + // Redundant - suggest removal (Warning) + var diag = Diagnostic.Create( + Descriptors.LuceneDev6002_RedundantOrdinal, + argLocation ?? memberAccess.Name.GetLocation(), + methodName); + ctx.ReportDiagnostic(diag); + } + else if (comparisonValue == "OrdinalIgnoreCase") + { + // Valid - no warning + return; + } + else + { + // Invalid comparison (CurrentCulture, InvariantCulture, etc.) - Error + var diag = Diagnostic.Create( + Descriptors.LuceneDev6002_InvalidComparison, + argLocation ?? memberAccess.Name.GetLocation(), + methodName, + comparisonValue ?? "non-ordinal comparison"); + ctx.ReportDiagnostic(diag); + } + } + + private static bool IsSpanLikeReceiver(ITypeSymbol? type) + { + if (type == null) return false; + + // Check for Span or ReadOnlySpan + if (type is INamedTypeSymbol namedType && namedType.IsGenericType) + { + var constructedFrom = namedType.ConstructedFrom.ToDisplayString(); + if (constructedFrom == "System.Span" || constructedFrom == "System.ReadOnlySpan") + { + // Verify it's char + var typeArg = namedType.TypeArguments.FirstOrDefault(); + if (typeArg != null && typeArg.SpecialType == SpecialType.System_Char) + return true; + } + } + + // Check for custom span-like types + var fullname = type.ToDisplayString(); + return fullname == "J2N.Text.OpenStringBuilder" || + fullname == "Lucene.Net.Text.ValueStringBuilder"; + } + + private static bool IsCharOverload(IMethodSymbol? method) + { + if (method == null) return false; + // Check if the first parameter (value parameter) is char + return method.Parameters.Length > 0 && + method.Parameters[0].Type.SpecialType == SpecialType.System_Char; + } + + private static (bool hasArgument, string? value, Location? location) CheckStringComparisonArgument( + InvocationExpressionSyntax invocation, + SemanticModel semantic, + INamedTypeSymbol stringComparisonType) + { + foreach (var arg in invocation.ArgumentList.Arguments) + { + var argType = semantic.GetTypeInfo(arg.Expression).Type; + + if (argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType)) + { + // Try to get the enum member name + var symbol = semantic.GetSymbolInfo(arg.Expression).Symbol as IFieldSymbol; + if (symbol != null && SymbolEqualityComparer.Default.Equals(symbol.ContainingType, stringComparisonType)) + { + return (true, symbol.Name, arg.Expression.GetLocation()); + } + + // Check constant value + var constantValue = semantic.GetConstantValue(arg.Expression); + if (constantValue.HasValue && constantValue.Value is int intValue) + { + string? name = intValue switch + { + 4 => "Ordinal", + 5 => "OrdinalIgnoreCase", + 0 => "CurrentCulture", + 1 => "CurrentCultureIgnoreCase", + 2 => "InvariantCulture", + 3 => "InvariantCultureIgnoreCase", + _ => null + }; + return (true, name, arg.Expression.GetLocation()); + } + + // Has StringComparison but can't determine value + return (true, null, arg.Expression.GetLocation()); + } + } + + return (false, null, null); + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs new file mode 100644 index 0000000..bd0d42e --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Linq; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx +{ + /// + /// Analyzer to detect single-character string literals (including escaped characters) + /// that should use char overload instead for better performance. + /// Applies to String, Span, and custom span-like types. + /// + /// Examples of violations: + /// - text.IndexOf("H") -> should use text.IndexOf('H') + /// - text.IndexOf("\n") -> should use text.IndexOf('\n') // Escaped newline + /// - text.IndexOf("\"") -> should use text.IndexOf('\"') // Escaped quote + /// - span.StartsWith("a") -> should use span.StartsWith('a') + /// + /// Severity: Info (suggestion only, not enforced) + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class LuceneDev6003_SingleCharStringAnalyzer : DiagnosticAnalyzer + { + private static readonly ImmutableHashSet TargetMethodNames = + ImmutableHashSet.Create("StartsWith", "EndsWith", "IndexOf", "LastIndexOf"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptors.LuceneDev6003_SingleCharStringAnalyzer); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) + { + var invocation = (InvocationExpressionSyntax)ctx.Node; + + if (!(invocation.Expression is MemberAccessExpressionSyntax memberAccess)) + return; + + var methodName = memberAccess.Name.Identifier.ValueText; + if (!TargetMethodNames.Contains(methodName)) + return; + + var semantic = ctx.SemanticModel; + + // Check if invocation has arguments + if (invocation.ArgumentList.Arguments.Count == 0) + return; + + // Get the first argument (the value to search for) + var firstArg = invocation.ArgumentList.Arguments[0]; + + // Must be a string literal + if (!(firstArg.Expression is LiteralExpressionSyntax literal)) + return; + + if (!literal.IsKind(SyntaxKind.StringLiteralExpression)) + return; + + // Get the actual character value (handles escape sequences automatically) + // token.ValueText gives us the UNESCAPED string value + // For example: "\"" -> ValueText = '"' (length 1) + // "\n" -> ValueText = '\n' (length 1) + // "\x0020" -> ValueText = ' ' (length 1) + var token = literal.Token; + var valueText = token.ValueText; // This is the unescaped string value + + // Check if it's exactly one character after unescaping + if (valueText.Length != 1) + return; + + // Get the method symbol to verify it's called on a valid type + var symbolInfo = semantic.GetSymbolInfo(memberAccess); + var methodSymbol = symbolInfo.Symbol as IMethodSymbol; + var candidateSymbols = symbolInfo.CandidateSymbols.OfType().ToImmutableArray(); + + // Determine the receiver type + var receiverType = semantic.GetTypeInfo(memberAccess.Expression).Type; + + // Check if this is a valid target type (String, Span, or custom span-like) + bool isValidType = IsValidTargetType(receiverType); + + if (!isValidType && methodSymbol != null) + { + isValidType = IsValidTargetType(methodSymbol.ContainingType); + } + + if (!isValidType && candidateSymbols.Length > 0) + { + isValidType = candidateSymbols.Any(c => IsValidTargetType(c.ContainingType)); + } + + if (!isValidType) + return; + + // Check if a char overload exists + bool hasCharOverload = HasCharOverload(methodSymbol, candidateSymbols, receiverType, methodName); + + if (!hasCharOverload) + return; + + // Report diagnostic with Info severity + // token.Text shows the ORIGINAL text as written in code (with escaping) + // For example: "\"" shows as "\"" + // "\n" shows as "\n" + var diag = Diagnostic.Create( + Descriptors.LuceneDev6003_SingleCharStringAnalyzer, + literal.GetLocation(), + methodName, + literal.Token.Text); // Show the original escaped text in the message + + ctx.ReportDiagnostic(diag); + } + + /// + /// Determines if the given type is a valid target for this analyzer. + /// Valid types: System.String, Span<char>, ReadOnlySpan<char>, + /// J2N.Text.OpenStringBuilder, Lucene.Net.Text.ValueStringBuilder + /// + private static bool IsValidTargetType(ITypeSymbol? type) + { + if (type == null) return false; + + // System.String + if (type.SpecialType == SpecialType.System_String) + return true; + + // Span or ReadOnlySpan + if (type is INamedTypeSymbol namedType && namedType.IsGenericType) + { + var constructedFrom = namedType.ConstructedFrom.ToDisplayString(); + if (constructedFrom == "System.Span" || constructedFrom == "System.ReadOnlySpan") + { + // Verify it's specifically Span or ReadOnlySpan + var typeArg = namedType.TypeArguments.FirstOrDefault(); + if (typeArg != null && typeArg.SpecialType == SpecialType.System_Char) + return true; + } + } + + // Custom span-like types from Lucene.NET and J2N + var fullname = type.ToDisplayString(); + return fullname == "J2N.Text.OpenStringBuilder" || + fullname == "Lucene.Net.Text.ValueStringBuilder"; + } + + /// + /// Checks if a char overload exists for the given method. + /// A char overload is a method with the same name where the first parameter is System.Char. + /// + private static bool HasCharOverload( + IMethodSymbol? methodSymbol, + ImmutableArray candidateSymbols, + ITypeSymbol? receiverType, + string methodName) + { + ImmutableArray methodsToCheck = ImmutableArray.Empty; + + // Strategy 1: Get all methods with the same name from the resolved method's containing type + if (methodSymbol != null && methodSymbol.ContainingType != null) + { + methodsToCheck = methodSymbol.ContainingType + .GetMembers(methodName) + .OfType() + .ToImmutableArray(); + } + // Strategy 2: Use candidate symbols if method couldn't be resolved + else if (candidateSymbols.Length > 0) + { + methodsToCheck = candidateSymbols; + + // Also try to get more methods from the first candidate's containing type + var containingType = candidateSymbols.FirstOrDefault()?.ContainingType; + if (containingType != null) + { + var additionalMethods = containingType.GetMembers(methodName).OfType(); + methodsToCheck = methodsToCheck.Concat(additionalMethods).ToImmutableArray(); + } + } + // Strategy 3: Use receiver type if nothing else worked + else if (receiverType != null) + { + methodsToCheck = receiverType + .GetMembers(methodName) + .OfType() + .ToImmutableArray(); + } + + // Look for a char overload + // The char overload should have System.Char as the first parameter (the value parameter) + foreach (var method in methodsToCheck) + { + if (method.Parameters.Length > 0 && + method.Parameters[0].Type.SpecialType == SpecialType.System_Char) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs new file mode 100644 index 0000000..044d59b --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + * OF ANY KIND, either express or implied. See the License for + * the specific language governing permissions and limitations + * under the License. + */ + +using Microsoft.CodeAnalysis; +using static Microsoft.CodeAnalysis.DiagnosticSeverity; +using static Lucene.Net.CodeAnalysis.Dev.Utility.Category; + +namespace Lucene.Net.CodeAnalysis.Dev.Utility +{ + public static partial class Descriptors + { + // IMPORTANT: Do not make these into properties! + // The AnalyzerReleases release management analyzers do not recognize them + // and will report RS2002 warnings if it cannot read the DiagnosticDescriptor + // instance through a field. + + // 6001: Missing StringComparison argument + public static readonly DiagnosticDescriptor LuceneDev6001_MissingStringComparison = + Diagnostic( + "LuceneDev6001_1", + Usage, + Error + ); + + // 6001: Invalid StringComparison value (not Ordinal or OrdinalIgnoreCase) + public static readonly DiagnosticDescriptor LuceneDev6001_InvalidStringComparison = + Diagnostic( + "LuceneDev6001_2", + Usage, + Warning + ); + + // 6002: Redundant Ordinal (StringComparison.Ordinal on span-like) + public static readonly DiagnosticDescriptor LuceneDev6002_RedundantOrdinal = + Diagnostic( + "LuceneDev6002_1", + Usage, + Warning + ); + + // 6002: Invalid comparison on span (e.g., CurrentCulture, InvariantCulture) + public static readonly DiagnosticDescriptor LuceneDev6002_InvalidComparison = + Diagnostic( + "LuceneDev6002_2", + Usage, + Error + ); + public static readonly DiagnosticDescriptor LuceneDev6003_SingleCharStringAnalyzer = + Diagnostic( + "LuceneDev6003", + Usage, + Info + ); + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.cs index 629c56c..0e5e62e 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.cs @@ -6,9 +6,9 @@ * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs new file mode 100644 index 0000000..c7e1e4e --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs @@ -0,0 +1,498 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx; +using Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.LuceneDev6xxx +{ + [TestFixture] + public class TestLuceneDev6001_StringComparisonCodeFixProvider + { + [Test] + public async Task TestFix_IndexOf_MissingStringComparison() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + int index = text.IndexOf(""Hello""); + } +}"; + + var fixedCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + int index = text.IndexOf(""Hello"", StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 26); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_StartsWith_MissingStringComparison() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + bool starts = text.StartsWith(""Hello""); + } +}"; + + var fixedCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + bool starts = text.StartsWith(""Hello"", StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("StartsWith") + .WithLocation("/0/Test0.cs", line: 9, column: 28); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_EndsWith_MissingStringComparison() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + bool ends = text.EndsWith(""World""); + } +}"; + + var fixedCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + bool ends = text.EndsWith(""World"", StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("EndsWith") + .WithLocation("/0/Test0.cs", line: 9, column: 26); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_LastIndexOf_MissingStringComparison() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World Hello""; + int index = text.LastIndexOf(""Hello""); + } +}"; + + var fixedCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World Hello""; + int index = text.LastIndexOf(""Hello"", StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("LastIndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 26); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_IndexOf_WithStartIndex_MissingStringComparison() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + int index = text.IndexOf(""World"", 5); + } +}"; + + var fixedCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + int index = text.IndexOf(""World"", 5, StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 26); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_IndexOf_WithStartIndexAndCount_MissingStringComparison() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + int index = text.IndexOf(""World"", 0, 11); + } +}"; + + var fixedCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + int index = text.IndexOf(""World"", 0, 11, StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 26); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_IndexOf_InvalidStringComparison_CurrentCulture() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + int index = text.IndexOf(""Hello"", StringComparison.CurrentCulture); + } +}"; + + var fixedCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + int index = text.IndexOf(""Hello"", StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 43); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_StartsWith_InvalidStringComparison_InvariantCulture() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + bool starts = text.StartsWith(""Hello"", StringComparison.InvariantCulture); + } +}"; + + var fixedCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + bool starts = text.StartsWith(""Hello"", StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithArguments("StartsWith") + .WithLocation("/0/Test0.cs", line: 9, column: 48); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_EndsWith_InvalidStringComparison_CurrentCultureIgnoreCase() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + bool ends = text.EndsWith(""WORLD"", StringComparison.CurrentCultureIgnoreCase); + } +}"; + + var fixedCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + bool ends = text.EndsWith(""WORLD"", StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithArguments("EndsWith") + .WithLocation("/0/Test0.cs", line: 9, column: 44); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestNoWarning_WithOrdinal() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + int index = text.IndexOf(""Hello"", StringComparison.Ordinal); + bool starts = text.StartsWith(""Hello"", StringComparison.Ordinal); + bool ends = text.EndsWith(""World"", StringComparison.Ordinal); + int lastIndex = text.LastIndexOf(""World"", StringComparison.Ordinal); + } +}"; + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = testCode, + ExpectedDiagnostics = { } // No diagnostics expected + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestNoWarning_WithOrdinalIgnoreCase() + { + var testCode = @" +using System; + +public class MyClass +{ + public void MyMethod() + { + string text = ""Hello World""; + int index = text.IndexOf(""hello"", StringComparison.OrdinalIgnoreCase); + bool starts = text.StartsWith(""HELLO"", StringComparison.OrdinalIgnoreCase); + bool ends = text.EndsWith(""WORLD"", StringComparison.OrdinalIgnoreCase); + int lastIndex = text.LastIndexOf(""world"", StringComparison.OrdinalIgnoreCase); + } +}"; + + var test = new InjectableCodeFixTest( + () => new LuceneDev6001_StringComparisonAnalyzer(), + () => new LuceneDev6001_StringComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = testCode, + ExpectedDiagnostics = { } // No diagnostics expected + }; + + await test.RunAsync(); + } + + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs new file mode 100644 index 0000000..055d06f --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx; +using Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.LuceneDev6xxx +{ + [TestFixture] + public class TestLuceneDev6002_SpanComparisonCodeFixProvider + { + [Test] + public async Task TestFix_RemoveRedundantOrdinal() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""test"", StringComparison.Ordinal); + } +}"; + + var fixedCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""test""); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_RedundantOrdinal) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(9, 42, 9, 66) + .WithArguments("IndexOf"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6002_SpanComparisonAnalyzer(), + () => new LuceneDev6002_SpanComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + +// [Test] +// public async Task TestFix_ReplaceInvalidWithOrdinal() +// { +// var testCode = @" +// using System; + +// public class Sample +// { +// public void M() +// { +// ReadOnlySpan span = ""Hello"".AsSpan(); +// int index = span.IndexOf(""test"", StringComparison.CurrentCulture); +// } +// }"; + +// var fixedCode = @" +// using System; + +// public class Sample +// { +// public void M() +// { +// ReadOnlySpan span = ""Hello"".AsSpan(); +// int index = span.IndexOf(""test"", StringComparison.Ordinal); +// } +// }"; + +// var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) +// .WithSeverity(DiagnosticSeverity.Error) +// .WithSpan(9, 42, 9, 73) +// .WithArguments("IndexOf", "CurrentCulture"); + +// var test = new InjectableCodeFixTest( +// () => new LuceneDev6002_SpanComparisonAnalyzer(), +// () => new LuceneDev6002_SpanComparisonCodeFixProvider()) +// { +// TestCode = testCode, +// FixedCode = fixedCode, +// ExpectedDiagnostics = { expected }, +// CodeActionIndex = 0 +// }; + +// await test.RunAsync(); +// } + [Test] + public async Task TestFix_RemoveRedundantOrdinal_Simple() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hi World"".AsSpan(); + int index = span.IndexOf(""x"", StringComparison.Ordinal); + } +}"; + + var fixedCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hi World"".AsSpan(); + int index = span.IndexOf(""x""); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_RedundantOrdinal) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(9, 39, 9, 63) + .WithArguments("IndexOf"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6002_SpanComparisonAnalyzer(), + () => new LuceneDev6002_SpanComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task NoDiagnostic_For_CharOverload() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + Span span = stackalloc char[5]; + int index = span.IndexOf('t'); + } +}"; + + var test = new InjectableCodeFixTest( + () => new LuceneDev6002_SpanComparisonAnalyzer(), + () => new LuceneDev6002_SpanComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = testCode, + ExpectedDiagnostics = { } // no diagnostics expected + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs new file mode 100644 index 0000000..aad31a9 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev6xxx +{ + [TestFixture] + public class TestLuceneDev6003_SingleCharStringCodeFixProvider + { + [Test] + public async Task Fix_SingleCharacter_StringLiteral() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int index = text.IndexOf(""H""); + } +}"; + + var fixedCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int index = text.IndexOf('H'); + } +}"; + + // "H" starts at column 39 and ends at column 42 (3 chars wide) + var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + .WithSeverity(DiagnosticSeverity.Info) + .WithArguments("IndexOf", "\"H\"") + .WithSpan(10, 39, 10, 42); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6003_SingleCharStringAnalyzer(), + () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected }, + CodeActionIndex = 0 + }; + + await test.RunAsync(); + } + + [Test] +public async Task Fix_EscapedCharacter_StringLiteral() +{ + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int index = text.IndexOf(""\""""); + } +}"; + + var fixedCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int index = text.IndexOf('""'); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + .WithSeverity(DiagnosticSeverity.Info) + .WithArguments("IndexOf", "\"\\\"\"") + .WithSpan(10, 39, 10, 43); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6003_SingleCharStringAnalyzer(), + () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected }, + CodeActionIndex = 0 + }; + + await test.RunAsync(); +} + + [Test] + public async Task FixAll_SingleCharacterStringLiterals() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int i1 = text.IndexOf(""H""); + int i2 = text.IndexOf(""\n""); + } +}"; + + var fixedCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int i1 = text.IndexOf('H'); + int i2 = text.IndexOf('\n'); + } +}"; + + // First: "H" (line 10, columns 38–41 → 3 chars) + var expected1 = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + .WithSeverity(DiagnosticSeverity.Info) + .WithArguments("IndexOf", "\"H\"") + .WithSpan(10, 38, 10, 41); + + // Second: "\n" (line 11, columns 38–42 → 4 chars) + var expected2 = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + .WithSeverity(DiagnosticSeverity.Info) + .WithArguments("IndexOf", "\"\\n\"") + .WithSpan(11, 38, 11, 42); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6003_SingleCharStringAnalyzer(), + () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected1, expected2 }, + CodeActionIndex = 0 + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs new file mode 100644 index 0000000..720a307 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs @@ -0,0 +1,461 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev6xxx +{ + [TestFixture] + public class TestLuceneDev6001_StringComparisonAnalyzer + { + [Test] + public async Task Detects_IndexOf_MissingStringComparison() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + int index = text.IndexOf(""Hello""); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 26); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_StartsWith_MissingStringComparison() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + bool starts = text.StartsWith(""Hello""); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("StartsWith") + .WithLocation("/0/Test0.cs", line: 9, column: 28); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_EndsWith_MissingStringComparison() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + bool ends = text.EndsWith(""World""); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("EndsWith") + .WithLocation("/0/Test0.cs", line: 9, column: 26); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_LastIndexOf_MissingStringComparison() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World Hello""; + int index = text.LastIndexOf(""Hello""); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("LastIndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 26); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_IndexOf_WithStartIndex_MissingStringComparison() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + int index = text.IndexOf(""World"", 5); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 26); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_IndexOf_WithStartIndexAndCount_MissingStringComparison() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + int index = text.IndexOf(""World"", 0, 11); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 26); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_InvalidStringComparison_CurrentCulture() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + int index = text.IndexOf(""Hello"", StringComparison.CurrentCulture); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 43); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_InvalidStringComparison_CurrentCultureIgnoreCase() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + bool starts = text.StartsWith(""hello"", StringComparison.CurrentCultureIgnoreCase); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithArguments("StartsWith") + .WithLocation("/0/Test0.cs", line: 9, column: 48); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_InvalidStringComparison_InvariantCulture() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + bool ends = text.EndsWith(""World"", StringComparison.InvariantCulture); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithArguments("EndsWith") + .WithLocation("/0/Test0.cs", line: 9, column: 44); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_InvalidStringComparison_InvariantCultureIgnoreCase() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + int index = text.LastIndexOf(""World"", StringComparison.InvariantCultureIgnoreCase); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithArguments("LastIndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 47); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task NoWarning_WithOrdinal() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + int index = text.IndexOf(""Hello"", StringComparison.Ordinal); + bool starts = text.StartsWith(""Hello"", StringComparison.Ordinal); + bool ends = text.EndsWith(""World"", StringComparison.Ordinal); + int lastIndex = text.LastIndexOf(""World"", StringComparison.Ordinal); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } // No diagnostics expected + }; + + await test.RunAsync(); + } + + [Test] + public async Task NoWarning_WithOrdinalIgnoreCase() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + int index = text.IndexOf(""hello"", StringComparison.OrdinalIgnoreCase); + bool starts = text.StartsWith(""HELLO"", StringComparison.OrdinalIgnoreCase); + bool ends = text.EndsWith(""WORLD"", StringComparison.OrdinalIgnoreCase); + int lastIndex = text.LastIndexOf(""world"", StringComparison.OrdinalIgnoreCase); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } // No diagnostics expected + }; + + await test.RunAsync(); + } + + + [Test] + public async Task Detects_MultipleViolations_InSameMethod() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello World""; + int index1 = text.IndexOf(""Hello""); + int index2 = text.IndexOf(""World"", StringComparison.CurrentCulture); + bool starts = text.StartsWith(""Hello""); + } +}"; + + var expected1 = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 9, column: 27); + + var expected2 = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + .WithSeverity(DiagnosticSeverity.Warning) + .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithArguments("IndexOf") + .WithLocation("/0/Test0.cs", line: 10, column: 44); + + var expected3 = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithMessageFormat(Descriptors.LuceneDev6001_MissingStringComparison.MessageFormat) + .WithArguments("StartsWith") + .WithLocation("/0/Test0.cs", line: 11, column: 28); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected1, expected2, expected3 } + }; + + await test.RunAsync(); + } + + [Test] + public async Task NoWarning_OnNonStringTypes() + { + var testCode = @" +using System; + +public class CustomType +{ + public int IndexOf(string value) => 0; + public bool StartsWith(string value) => false; +} + +public class Sample +{ + public void M() + { + var custom = new CustomType(); + int index = custom.IndexOf(""test""); + bool starts = custom.StartsWith(""test""); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } // No diagnostics expected - not on System.String + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonAnalyzer.cs new file mode 100644 index 0000000..c851f94 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonAnalyzer.cs @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev6xxx +{ + [TestFixture] + public class TestLuceneDev6002_SpanComparisonAnalyzer + { + [Test] + public async Task Detects_RedundantOrdinal_OnReadOnlySpan_IndexOf() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""test"", StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_RedundantOrdinal) + .WithSeverity(DiagnosticSeverity.Warning) + .WithArguments("IndexOf") + .WithSpan(9, 42, 9, 66); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_RedundantOrdinal_OnSpan_StartsWith() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = stackalloc char[5]; + bool starts = span.StartsWith(""test"", StringComparison.Ordinal); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_RedundantOrdinal) + .WithSeverity(DiagnosticSeverity.Warning) + .WithArguments("StartsWith") + .WithSpan(9, 47, 9, 71); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + + [Test] + public async Task Detects_InvalidComparison_CurrentCulture() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""test"", StringComparison.CurrentCulture); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("IndexOf", "CurrentCulture") + .WithSpan(9, 42, 9, 73); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_InvalidComparison_CurrentCultureIgnoreCase() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.LastIndexOf(""test"", StringComparison.CurrentCultureIgnoreCase); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("LastIndexOf", "CurrentCultureIgnoreCase") + .WithSpan(9, 46, 9, 87); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_InvalidComparison_InvariantCulture() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + bool ends = span.EndsWith(""test"", StringComparison.InvariantCulture); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("EndsWith", "InvariantCulture") + .WithSpan(9, 43, 9, 76); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_InvalidComparison_InvariantCultureIgnoreCase() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + bool ends = span.EndsWith(""test"", StringComparison.InvariantCultureIgnoreCase); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("EndsWith", "InvariantCultureIgnoreCase") + .WithSpan(9, 43, 9, 86); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task NoWarning_WithoutStringComparison() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""test""); + bool starts = span.StartsWith(""Hello""); + bool ends = span.EndsWith(""lo""); + int lastIndex = span.LastIndexOf(""ll""); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + public async Task NoWarning_WithOrdinalIgnoreCase() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""TEST"", StringComparison.OrdinalIgnoreCase); + bool starts = span.StartsWith(""HELLO"", StringComparison.OrdinalIgnoreCase); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + public async Task NoWarning_OnStringType() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + // String types are handled by LuceneDev6001, not 6002 + int index = text.IndexOf(""test"", StringComparison.Ordinal); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + [Test] + public async Task NoWarning_CharOverloads() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""H""); + bool starts = span.StartsWith(""H""); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } // No diagnostics + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_MultipleViolations() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index1 = span.IndexOf(""test"", StringComparison.Ordinal); + int index2 = span.LastIndexOf(""test"", StringComparison.CurrentCulture); + } +}"; + + var expected1 = new DiagnosticResult(Descriptors.LuceneDev6002_RedundantOrdinal) + .WithSeverity(DiagnosticSeverity.Warning) + .WithArguments("IndexOf") + .WithSpan(9, 43, 9, 67); + + var expected2 = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("LastIndexOf", "CurrentCulture") + .WithSpan(10, 47, 10, 78); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected1, expected2 } + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringAnalyzer.cs new file mode 100644 index 0000000..4866e89 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringAnalyzer.cs @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev6xxx +{ + [TestFixture] + public class TestLuceneDev6003_SingleCharStringAnalyzer + { + [Test] + public async Task Detects_SingleCharacter_StringLiteral() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int index = text.IndexOf(""H""); + } +}"; + var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + .WithSeverity(DiagnosticSeverity.Info) + .WithSpan(9, 34, 9, 37) + .WithArguments("IndexOf", "\"H\""); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_SingleCharStringAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Detects_EscapedCharacter_StringLiteral() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int index = text.IndexOf(""\""""); // Added missing semicolon + } +}"; + var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + .WithSeverity(DiagnosticSeverity.Info) + .WithSpan(9, 34, 9, 38) + .WithArguments("IndexOf", "\"\\\"\""); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_SingleCharStringAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task NoDiagnostic_For_MultiCharacterString() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int index = text.IndexOf(""He""); + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_SingleCharStringAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + } +} From 03733bd19623224cb795f3a3b7aa423eb31b4468 Mon Sep 17 00:00:00 2001 From: Nehan Pathan Date: Tue, 21 Oct 2025 12:50:51 +0530 Subject: [PATCH 2/4] feat(analyzers): smart Span fixes, char optimizations,NRT updatesand add Documentation as per suggestion --- ...Dev6001_StringComparisonCodeFixProvider.cs | 200 ++++++++++++++---- ...neDev6002_SpanComparisonCodeFixProvider.cs | 169 ++++++++++++--- .../AnalyzerReleases.Unshipped.md | 2 +- .../LuceneDev6001_StringComparisonAnalyzer.cs | 27 +++ .../LuceneDev6002_SpanComparisonAnalyzer.cs | 26 +++ .../LuceneDev6003_SingleCharStringAnalyzer.cs | 58 +++-- .../Resources.resx | 56 +++++ .../Utility/Descriptors.LuceneDev6xxx.cs | 2 +- ...Dev6001_StringComparisonCodeFixProvider.cs | 12 +- ...neDev6002_SpanComparisonCodeFixProvider.cs | 142 +++++++++---- ...Dev6003_SingleCharStringCodeFixProvider.cs | 112 ++++++++-- ...tLuceneDev6001_StringComparisonAnalyzer.cs | 64 +++++- 12 files changed, 701 insertions(+), 169 deletions(-) diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs index c2e274b..b780d2b 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs @@ -1,20 +1,9 @@ /* * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * or more contributor license agreements. See the NOTICE file for additional information. + * The ASF licenses this file under the Apache License, Version 2.0. */ + using System.Collections.Immutable; using System.Composition; using System.Linq; @@ -24,14 +13,16 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp; namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx { [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6001_StringComparisonCodeFixProvider)), Shared] public sealed class LuceneDev6001_StringComparisonCodeFixProvider : CodeFixProvider { + private const string Ordinal = "Ordinal"; + private const string OrdinalIgnoreCase = "OrdinalIgnoreCase"; private const string TitleOrdinal = "Use StringComparison.Ordinal"; private const string TitleOrdinalIgnoreCase = "Use StringComparison.OrdinalIgnoreCase"; @@ -42,41 +33,143 @@ public sealed class LuceneDev6001_StringComparisonCodeFixProvider : CodeFixProvi public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + /// + /// Registers available code fixes for all diagnostics in the context. + /// public override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); if (root == null) return; - var diagnostic = context.Diagnostics.First(); - var diagnosticSpan = diagnostic.Location.SourceSpan; + // Iterate over ALL diagnostics in the context to ensure all issues are offered a fix. + foreach (var diagnostic in context.Diagnostics) + { + var invocation = root.FindToken(diagnostic.Location.SourceSpan.Start) + .Parent? + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (invocation == null) continue; + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel == null) continue; + + //Double check to Skip char literals and single-character string literals when safe --- + var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; + if (firstArgExpr is LiteralExpressionSyntax lit) + { + if (lit.IsKind(SyntaxKind.CharacterLiteralExpression)) + return; // already char overload; no diagnostic + + if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1) + { + // Check if a StringComparison argument is present + bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg => + semanticModel.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t && + t.ToDisplayString() == "System.StringComparison" + || (semanticModel.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && + f.ContainingType?.ToDisplayString() == "System.StringComparison")); + + if (!hasStringComparisonArgForLiteral) + { + // safe to convert to char (6003), so skip 6001 reporting + return; + } + // else: has StringComparison -> do not skip; let codefix handle it + } + } - var invocation = root.FindToken(diagnosticSpan.Start) - .Parent? - .AncestorsAndSelf() - .OfType() - .FirstOrDefault(); - if (invocation == null) return; + // --- Fix Registration Logic --- - // Offer both Ordinal and OrdinalIgnoreCase fixes - context.RegisterCodeFix(CodeAction.Create( - title: TitleOrdinal, - createChangedDocument: c => FixInvocationAsync(context.Document, invocation, "Ordinal", c), - equivalenceKey: TitleOrdinal), - diagnostic); + if (diagnostic.Id == Descriptors.LuceneDev6001_MissingStringComparison.Id) + { + // Case 1: Argument is missing. Only offer Ordinal as the safe, conservative default. + RegisterFix(context, invocation, Ordinal, TitleOrdinal, diagnostic); + } + else if (diagnostic.Id == Descriptors.LuceneDev6001_InvalidStringComparison.Id) + { + // Case 2: Invalid argument is present. Determine the best replacement. + if (TryDetermineReplacement(invocation, semanticModel, out string? targetComparison)) + { + var title = (targetComparison!) == Ordinal ? TitleOrdinal : TitleOrdinalIgnoreCase; + RegisterFix(context, invocation, targetComparison!, title, diagnostic); + } + // If TryDetermineReplacement returns false, the argument is an invalid non-constant + // expression (e.g., a variable). We skip the fix to avoid arbitrary changes. + } + } + } + private static void RegisterFix( + CodeFixContext context, + InvocationExpressionSyntax invocation, + string comparisonMember, + string title, + Diagnostic diagnostic) + { context.RegisterCodeFix(CodeAction.Create( - title: TitleOrdinalIgnoreCase, - createChangedDocument: c => FixInvocationAsync(context.Document, invocation, "OrdinalIgnoreCase", c), - equivalenceKey: TitleOrdinalIgnoreCase), + title: title, + createChangedDocument: c => FixInvocationAsync(context.Document, invocation, comparisonMember, c), + equivalenceKey: title), diagnostic); } + /// + /// Determines the appropriate ordinal replacement (Ordinal or OrdinalIgnoreCase) + /// for an existing culture-sensitive StringComparison argument. + /// Only operates on constant argument values. + /// + /// True if a valid replacement was determined, false otherwise (e.g., if argument is non-constant). + private static bool TryDetermineReplacement(InvocationExpressionSyntax invocation, SemanticModel semanticModel, out string? targetComparison) + { + targetComparison = null; + var stringComparisonType = semanticModel.Compilation.GetTypeByMetadataName("System.StringComparison"); + var existingArg = invocation.ArgumentList.Arguments.FirstOrDefault(arg => + SymbolEqualityComparer.Default.Equals( + semanticModel.GetTypeInfo(arg.Expression).Type, stringComparisonType)); + + if (existingArg != null) + { + var constVal = semanticModel.GetConstantValue(existingArg.Expression); + if (constVal.HasValue && constVal.Value is int intVal) + { + // Map original comparison to corresponding ordinal variant for constant values + switch ((System.StringComparison)intVal) + { + case System.StringComparison.CurrentCulture: + case System.StringComparison.InvariantCulture: + targetComparison = Ordinal; + return true; + case System.StringComparison.CurrentCultureIgnoreCase: + case System.StringComparison.InvariantCultureIgnoreCase: + targetComparison = OrdinalIgnoreCase; + return true; + case System.StringComparison.Ordinal: + case System.StringComparison.OrdinalIgnoreCase: + return false; // Already correct + } + } + // Argument exists, but is not a constant value (e.g., a variable). We skip the fix. + return false; + } + + // Should not be called for missing arguments by the caller. + return false; + } + + /// + /// Creates the new document by either replacing an existing StringComparison argument + /// or adding a new one, based on the fix action. + /// private static async Task FixInvocationAsync(Document document, InvocationExpressionSyntax invocation, string comparisonMember, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (root == null) return document; - // Create the StringComparison expression + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var stringComparisonType = semanticModel?.Compilation.GetTypeByMetadataName("System.StringComparison"); + + // 1. Create the new StringComparison argument expression var stringComparisonExpr = SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName("StringComparison"), @@ -84,25 +177,42 @@ private static async Task FixInvocationAsync(Document document, Invoca var newArg = SyntaxFactory.Argument(stringComparisonExpr); - // Check if a StringComparison argument already exists - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var stringComparisonType = semanticModel?.Compilation.GetTypeByMetadataName("System.StringComparison"); + // 2. Find existing argument for replacement/addition check var existingArg = invocation.ArgumentList.Arguments.FirstOrDefault(arg => semanticModel != null && - (SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arg.Expression).Type, stringComparisonType) || - (semanticModel.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && SymbolEqualityComparer.Default.Equals(f.ContainingType, stringComparisonType)))); + SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arg.Expression).Type, stringComparisonType)); - // Replace existing argument or add new one - var newInvocation = existingArg != null - ? invocation.ReplaceNode(existingArg, newArg) - : invocation.WithArgumentList(invocation.ArgumentList.AddArguments(newArg)); + // 3. Perform the syntax replacement/addition + InvocationExpressionSyntax newInvocation; + if (existingArg != null) + { + // Argument exists (Replacement case: InvalidComparison) + // Preserve leading/trailing trivia (spaces/comma) from the expression being replaced + var newExprWithTrivia = stringComparisonExpr + .WithLeadingTrivia(existingArg.Expression.GetLeadingTrivia()) + .WithTrailingTrivia(existingArg.Expression.GetTrailingTrivia()); - // Combine adding 'using System;' and replacing invocation in a single root - var newRoot = EnsureSystemUsing(root).ReplaceNode(invocation, newInvocation); + var newArgWithTrivia = existingArg.WithExpression(newExprWithTrivia); + newInvocation = invocation.ReplaceNode(existingArg, newArgWithTrivia); + } + else + { + // Argument is missing (Addition case: MissingComparison) + // Use AddArguments, relying on Roslyn to correctly handle comma/spacing trivia. + newInvocation = invocation.WithArgumentList( + invocation.ArgumentList.AddArguments(newArg) + ); + } + + // 4. Update the document root (Ensure using statement is present and replace invocation) + var newRoot = EnsureSystemUsing(root).ReplaceNode(invocation, newInvocation); return document.WithSyntaxRoot(newRoot); } + /// + /// Ensures a 'using System;' directive is present in the document. + /// private static SyntaxNode EnsureSystemUsing(SyntaxNode root) { if (root is CompilationUnitSyntax compilationUnit) @@ -113,7 +223,7 @@ private static SyntaxNode EnsureSystemUsing(SyntaxNode root) if (!hasSystemUsing) { var systemUsing = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("System")) - .WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); + .WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); return compilationUnit.AddUsings(systemUsing); } } diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs index 5e66bc4..81ab0da 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs @@ -7,7 +7,7 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -34,8 +34,16 @@ namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx public sealed class LuceneDev6002_SpanComparisonCodeFixProvider : CodeFixProvider { private const string TitleRemoveOrdinal = "Remove redundant StringComparison.Ordinal"; - private const string TitleReplaceWithOrdinal = "Replace with StringComparison.Ordinal"; - private const string TitleReplaceWithOrdinalIgnoreCase = "Replace with StringComparison.OrdinalIgnoreCase"; + private const string TitleOptimizeToDefaultOrdinal = "Optimize to default Ordinal comparison (remove argument)"; + private const string TitleReplaceWithOrdinalIgnoreCase = "Use StringComparison.OrdinalIgnoreCase"; + + // Integer values for StringComparison Enum members (used for semantic analysis) + private const int CurrentCulture = 0; + private const int CurrentCultureIgnoreCase = 1; + private const int InvariantCulture = 2; + private const int InvariantCultureIgnoreCase = 3; + private const int Ordinal = 4; + private const int OrdinalIgnoreCase = 5; public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( @@ -56,39 +64,136 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) if (invocation == null) return; + //Double check to Skip char literals and single-character string literals when safe --- + var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; + + if (firstArgExpr is LiteralExpressionSyntax lit) + + { + + if (lit.IsKind(SyntaxKind.CharacterLiteralExpression)) + + return; // already char overload; skip 6002 fix + + + + if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1) + + { + + // Check if a StringComparison argument is present + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + + if (semanticModel == null) + + return; + + + + bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg => + + semanticModel.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t && + + t.ToDisplayString() == "System.StringComparison" + + || (semanticModel.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && + + f.ContainingType?.ToDisplayString() == "System.StringComparison")); + + + + if (!hasStringComparisonArgForLiteral) + + { + + // safe to convert to char (6003), skip 6002 fix + + return; + + } + + // else: has StringComparison -> let the codefix continue + + } + + } switch (diagnostic.Id) { case var id when id == Descriptors.LuceneDev6002_RedundantOrdinal.Id: context.RegisterCodeFix( CodeAction.Create( - title: "Remove redundant StringComparison.Ordinal", + title: TitleRemoveOrdinal, createChangedDocument: c => RemoveStringComparisonArgumentAsync(context.Document, invocation, c), equivalenceKey: "RemoveRedundantOrdinal"), diagnostic); break; case var id when id == Descriptors.LuceneDev6002_InvalidComparison.Id: - context.RegisterCodeFix( - CodeAction.Create( - title: "Use StringComparison.Ordinal", - createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "Ordinal", c), - equivalenceKey: "ReplaceWithOrdinal"), - diagnostic); - - context.RegisterCodeFix( - CodeAction.Create( - title: "Use StringComparison.OrdinalIgnoreCase", - createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "OrdinalIgnoreCase", c), - equivalenceKey: "ReplaceWithOrdinalIgnoreCase"), - diagnostic); + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel == null) + return; + + var comparisonArg = invocation.ArgumentList.Arguments.FirstOrDefault(arg => + semanticModel.GetTypeInfo(arg.Expression).Type?.ToDisplayString() == "System.StringComparison"); + + if (comparisonArg == null) + return; + + var originalComparisonValue = semanticModel.GetConstantValue(comparisonArg.Expression); + + if (originalComparisonValue.HasValue && originalComparisonValue.Value is int intValue) + { + // Check if the original comparison was case-insensitive + bool wasCaseInsensitive = intValue == CurrentCultureIgnoreCase || + intValue == InvariantCultureIgnoreCase; + + if (wasCaseInsensitive) + { + // Fix 1: Case-Insensitive Invalid -> OrdinalIgnoreCase (Single, targeted fix) + context.RegisterCodeFix( + CodeAction.Create( + title: TitleReplaceWithOrdinalIgnoreCase, + createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "OrdinalIgnoreCase", c), + equivalenceKey: "ReplaceWithOrdinalIgnoreCase"), + diagnostic); + + // Optionally, still offer the case-sensitive fix for completeness + context.RegisterCodeFix( + CodeAction.Create( + title: "Use StringComparison.Ordinal", // Offer Ordinal as second choice + createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "Ordinal", c), + equivalenceKey: "ReplaceWithOrdinal"), + diagnostic); + } + else + { + // Fix 1: Case-Sensitive Invalid (CurrentCulture/InvariantCulture) -> Optimal Default (Remove argument) + // This skips the redundant intermediate step (Ordinal) + context.RegisterCodeFix( + CodeAction.Create( + title: TitleOptimizeToDefaultOrdinal, + createChangedDocument: c => RemoveStringComparisonArgumentAsync(context.Document, invocation, c), + equivalenceKey: "OptimizeToDefaultOrdinal"), + diagnostic); + + // Optionally, still offer the case-insensitive fix for completeness + context.RegisterCodeFix( + CodeAction.Create( + title: TitleReplaceWithOrdinalIgnoreCase, + createChangedDocument: c => ReplaceWithStringComparisonAsync(context.Document, invocation, "OrdinalIgnoreCase", c), + equivalenceKey: "ReplaceWithOrdinalIgnoreCase"), + diagnostic); + } + } break; } } private static async Task RemoveStringComparisonArgumentAsync( - Document document, - InvocationExpressionSyntax invocation, - CancellationToken cancellationToken) + Document document, + InvocationExpressionSyntax invocation, + CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (root == null) @@ -128,12 +233,13 @@ member.Expression is IdentifierNameSyntax idName && if (argumentToRemove == null) return document; - // Remove the argument and normalize formatting + // Remove the argument var newArguments = invocation.ArgumentList.Arguments.Remove(argumentToRemove); var newArgumentList = invocation.ArgumentList.WithArguments(newArguments); + + // CRITICAL FIX: Removed NormalizeWhitespace() which causes test instability var newInvocation = invocation.WithArgumentList(newArgumentList) - .WithTriviaFrom(invocation) // preserve trivia - .NormalizeWhitespace(); // clean formatting + .WithTriviaFrom(invocation); // Preserving trivia on the outer node is usually fine var newRoot = root.ReplaceNode(invocation, newInvocation); return document.WithSyntaxRoot(newRoot); @@ -188,11 +294,11 @@ member.Expression is IdentifierNameSyntax idName && // Create new StringComparison expression var baseExpression = isFullyQualified - ? (ExpressionSyntax)SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName("System"), - SyntaxFactory.IdentifierName("StringComparison")) - : SyntaxFactory.IdentifierName("StringComparison"); + ? (ExpressionSyntax)SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("System"), + SyntaxFactory.IdentifierName("StringComparison")) + : SyntaxFactory.IdentifierName("StringComparison"); var newExpression = SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, @@ -201,9 +307,9 @@ member.Expression is IdentifierNameSyntax idName && var newArgument = argumentToReplace.WithExpression(newExpression); - var newInvocation = invocation.ReplaceNode(argumentToReplace, newArgument) - .WithTriviaFrom(invocation) - .NormalizeWhitespace(); + + // CRITICAL FIX: Removed WithTriviaFrom(invocation) and NormalizeWhitespace() which cause test instability + var newInvocation = invocation.ReplaceNode(argumentToReplace, newArgument); var newRoot = root; if (!isFullyQualified) @@ -214,6 +320,7 @@ member.Expression is IdentifierNameSyntax idName && return document.WithSyntaxRoot(newRoot); } + // EnsureSystemUsing remains unchanged as it looks correct for adding a using directive private static SyntaxNode EnsureSystemUsing(SyntaxNode root) { if (root is CompilationUnitSyntax compilationUnit) diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md index af77060..538ae10 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md @@ -6,7 +6,7 @@ LuceneDev1007 | Design | Warning | Generic Dictionary indexer s LuceneDev1008 | Design | Warning | Generic Dictionary indexer should not be used to retrieve values because it may throw KeyNotFoundException (reference type value) LuceneDev6000 | Usage | Info | IDictionary indexer may be used to retrieve values, but must be checked for null before using the value LuceneDev6001_1 | Usage | Error | Missing StringComparison argument in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; must use Ordinal/OrdinalIgnoreCase -LuceneDev6001_2 | Usage | Warning | Invalid StringComparison value in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal/OrdinalIgnoreCase allowed +LuceneDev6001_2 | Usage | Error | Invalid StringComparison value in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal/OrdinalIgnoreCase allowed LuceneDev6002_1 | Usage | Warning | Redundant StringComparison.Ordinal argument in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; should be removed LuceneDev6002_2 | Usage | Error | Invalid StringComparison value in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal or OrdinalIgnoreCase allowed LuceneDev6003 | Usage | Info | Single-character string arguments should use the char overload of StartsWith/EndsWith/IndexOf/LastIndexOf instead of a string diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs index 263bc8f..59e65a8 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs @@ -63,6 +63,33 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) if (stringComparisonType == null) return; + // Skip char literals and single-character string literals when safe --- + // early in AnalyzeInvocation, after verifying target method & span/string scope + var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; + if (firstArgExpr is LiteralExpressionSyntax lit) + { + if (lit.IsKind(SyntaxKind.CharacterLiteralExpression)) + return; // already char overload; no diagnostic + + if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1) + { + // Check if a StringComparison argument is present + bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg => + semantic.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t && + t.ToDisplayString() == "System.StringComparison" + || (semantic.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && + f.ContainingType?.ToDisplayString() == "System.StringComparison")); + + if (!hasStringComparisonArgForLiteral ) + { + // safe to convert to char (6003), so skip 6001 reporting + return; + } + // else: has StringComparison -> do not skip; let 6001/6002 validate or codefix handle it + } + } + + // Get symbol info var symbolInfo = semantic.GetSymbolInfo(memberAccess); var methodSymbol = symbolInfo.Symbol as IMethodSymbol; diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs index 363858e..fc9d413 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs @@ -100,6 +100,32 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) if (candidateSymbols.Length > 0 && candidateSymbols.All(c => IsCharOverload(c))) return; + // Skip char literals and single-character string literals when safe --- + var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; + if (firstArgExpr is LiteralExpressionSyntax lit) + { + if (lit.IsKind(SyntaxKind.CharacterLiteralExpression)) + return; // already char overload; no diagnostic + + if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1) + { + // Check if a StringComparison argument is present + bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg => + semantic.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t && + t.ToDisplayString() == "System.StringComparison" + || (semantic.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && + f.ContainingType?.ToDisplayString() == "System.StringComparison")); + + if (!hasStringComparisonArgForLiteral ) + { + // safe to convert to char (6003), so skip 6001 reporting + return; + } + // else: has StringComparison -> do not skip; let 6001/6002 validate or codefix handle it + } + } + + // Check for StringComparison argument var (hasComparison, comparisonValue, argLocation) = CheckStringComparisonArgument(invocation, semantic, stringComparisonType); diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs index bd0d42e..33e0f28 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs @@ -84,10 +84,6 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) return; // Get the actual character value (handles escape sequences automatically) - // token.ValueText gives us the UNESCAPED string value - // For example: "\"" -> ValueText = '"' (length 1) - // "\n" -> ValueText = '\n' (length 1) - // "\x0020" -> ValueText = ' ' (length 1) var token = literal.Token; var valueText = token.ValueText; // This is the unescaped string value @@ -104,20 +100,41 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) var receiverType = semantic.GetTypeInfo(memberAccess.Expression).Type; // Check if this is a valid target type (String, Span, or custom span-like) - bool isValidType = IsValidTargetType(receiverType); + bool isSpanLike = IsSpanLikeReceiver(receiverType); + bool isValidTarget = IsValidTargetType(receiverType) + || (methodSymbol != null && IsValidTargetType(methodSymbol.ContainingType)) + || candidateSymbols.Any(c => IsValidTargetType(c.ContainingType)); - if (!isValidType && methodSymbol != null) + if (!isValidTarget) + return; + + // 🌟 CRITICAL FIX: Handle Span/ReadOnlySpan differences + // For Span and ReadOnlySpan: + // 1. StartsWith/EndsWith only take ReadOnlySpan, NOT a single char, so we must skip the diagnostic. + // 2. IndexOf/LastIndexOf only have single-argument overloads for the 'char' (or 'value span') overload. + if (isSpanLike) { - isValidType = IsValidTargetType(methodSymbol.ContainingType); - } + if (methodName == "StartsWith" || methodName == "EndsWith") + { + // Span/ReadOnlySpan do not have 'char' overloads for StartsWith/EndsWith. + // The string literal "a" is correctly resolved to the ReadOnlySpan overload. + return; + } - if (!isValidType && candidateSymbols.Length > 0) + // For IndexOf/LastIndexOf on spans, if the invocation has more than 1 argument, + // it's likely a custom extension method or an invalid call, and it won't resolve + // to the simple `IndexOf(char value)` or `IndexOf(ReadOnlySpan value)` methods. + // We only target the simplest case for replacement. + if (invocation.ArgumentList.Arguments.Count != 1) + return; + } + else { - isValidType = candidateSymbols.Any(c => IsValidTargetType(c.ContainingType)); + // For System.String and custom types, we allow multiple arguments (e.g., IndexOf("a", 5)) + // because the char overloads like IndexOf('a', 5) exist. + // We rely on the `HasCharOverload` check below to validate that the char overload exists. } - - if (!isValidType) - return; + // ----------------------------------------------------- // Check if a char overload exists bool hasCharOverload = HasCharOverload(methodSymbol, candidateSymbols, receiverType, methodName); @@ -170,6 +187,21 @@ private static bool IsValidTargetType(ITypeSymbol? type) fullname == "Lucene.Net.Text.ValueStringBuilder"; } + /// + /// Determines if the receiver type is Span<char> or ReadOnlySpan<char>. + /// + private static bool IsSpanLikeReceiver(ITypeSymbol? type) + { + if (type is INamedTypeSymbol namedType && namedType.IsGenericType) + { + var constructedFrom = namedType.ConstructedFrom.ToDisplayString(); + if ((constructedFrom == "System.Span" || constructedFrom == "System.ReadOnlySpan") && + namedType.TypeArguments.FirstOrDefault()?.SpecialType == SpecialType.System_Char) + return true; + } + return false; + } + /// /// Checks if a char overload exists for the given method. /// A char overload is a method with the same name where the first parameter is System.Char. diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx index 2786230..c50dba7 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx +++ b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx @@ -218,4 +218,60 @@ under the License. '{0}' may fail due to floating point precision issues on .NET Framework and .NET Core prior to version 3.0. Floating point values should be formatted with J2N.Numerics.Single.ToString() or J2N.Numerics.Double.ToString() before being embedded into strings. The format-able message the diagnostic displays. + + + + Missing StringComparison argument + + + Calls to string comparison methods like StartsWith, EndsWith, IndexOf, and LastIndexOf must explicitly specify a StringComparison to enforce culture-invariant and consistent behavior. + + + Call to '{0}' must specify a StringComparison argument. Use StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase. + + + + + Invalid StringComparison argument + + + Only StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase are allowed to ensure predictable and high-performance string comparisons. + + + Call to '{0}' uses invalid StringComparison value '{1}'. Only Ordinal or OrdinalIgnoreCase are allowed. + + + + + Redundant StringComparison.Ordinal argument + + + Span-based overloads already perform ordinal comparison by default. Removing redundant arguments simplifies the code and improves clarity. + + + Call to '{0}' on span overload already uses ordinal comparison. Remove the redundant StringComparison.Ordinal argument. + + + + + Invalid StringComparison argument for span overload + + + Span-based methods only support StringComparison.Ordinal and StringComparison.OrdinalIgnoreCase. Other values are not valid and should be removed or corrected. + + + Call to '{0}' uses invalid StringComparison value '{1}'. Span overloads only support Ordinal or OrdinalIgnoreCase. + + + + + Single-character string argument should use char overload + + + Using char overloads instead of single-character string literals avoids unnecessary string allocations and improves performance. + + + Call to '{0}' uses a single-character string literal. Use the char overload instead (e.g., 'x' instead of "x"). + + diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs index 044d59b..781a79a 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs @@ -42,7 +42,7 @@ public static partial class Descriptors Diagnostic( "LuceneDev6001_2", Usage, - Warning + Error ); // 6002: Redundant Ordinal (StringComparison.Ordinal on span-like) diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs index c7e1e4e..8c926ba 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs @@ -327,7 +327,7 @@ public void MyMethod() }"; var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) - .WithSeverity(DiagnosticSeverity.Warning) + .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) .WithArguments("IndexOf") .WithLocation("/0/Test0.cs", line: 9, column: 43); @@ -372,7 +372,7 @@ public void MyMethod() }"; var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) - .WithSeverity(DiagnosticSeverity.Warning) + .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) .WithArguments("StartsWith") .WithLocation("/0/Test0.cs", line: 9, column: 48); @@ -412,12 +412,12 @@ public class MyClass public void MyMethod() { string text = ""Hello World""; - bool ends = text.EndsWith(""WORLD"", StringComparison.Ordinal); + bool ends = text.EndsWith(""WORLD"", StringComparison.OrdinalIgnoreCase); } }"; var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) - .WithSeverity(DiagnosticSeverity.Warning) + .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) .WithArguments("EndsWith") .WithLocation("/0/Test0.cs", line: 9, column: 44); @@ -435,7 +435,7 @@ public void MyMethod() } [Test] - public async Task TestNoWarning_WithOrdinal() + public async Task TestNoError_WithOrdinal() { var testCode = @" using System; @@ -465,7 +465,7 @@ public void MyMethod() } [Test] - public async Task TestNoWarning_WithOrdinalIgnoreCase() + public async Task TestNoError_WithOrdinalIgnoreCase() { var testCode = @" using System; diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs index 055d06f..3729559 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs @@ -6,7 +6,7 @@ * (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -73,50 +73,102 @@ public void M() await test.RunAsync(); } -// [Test] -// public async Task TestFix_ReplaceInvalidWithOrdinal() -// { -// var testCode = @" -// using System; - -// public class Sample -// { -// public void M() -// { -// ReadOnlySpan span = ""Hello"".AsSpan(); -// int index = span.IndexOf(""test"", StringComparison.CurrentCulture); -// } -// }"; - -// var fixedCode = @" -// using System; - -// public class Sample -// { -// public void M() -// { -// ReadOnlySpan span = ""Hello"".AsSpan(); -// int index = span.IndexOf(""test"", StringComparison.Ordinal); -// } -// }"; - -// var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) -// .WithSeverity(DiagnosticSeverity.Error) -// .WithSpan(9, 42, 9, 73) -// .WithArguments("IndexOf", "CurrentCulture"); - -// var test = new InjectableCodeFixTest( -// () => new LuceneDev6002_SpanComparisonAnalyzer(), -// () => new LuceneDev6002_SpanComparisonCodeFixProvider()) -// { -// TestCode = testCode, -// FixedCode = fixedCode, -// ExpectedDiagnostics = { expected }, -// CodeActionIndex = 0 -// }; - -// await test.RunAsync(); -// } + [Test] + public async Task TestFix_InvalidToOptimalRemoval_CaseSensitive() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""test"", StringComparison.CurrentCulture); + } +}"; + + var fixedCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""test""); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithSpan(9, 42, 9, 73) + .WithArguments("IndexOf", "CurrentCulture"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6002_SpanComparisonAnalyzer(), + () => new LuceneDev6002_SpanComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected }, + // The new logic offers "Optimize to default Ordinal" as CodeActionIndex = 0 + CodeActionIndex = 0, + // CRITICAL FIX: The smarter fix takes only 1 iteration now. + NumberOfFixAllIterations = 1 + }; + + await test.RunAsync(); + } + + [Test] + public async Task TestFix_InvalidToOptimalOrdinalIgnoreCase_CaseInsensitive() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""test"", StringComparison.CurrentCultureIgnoreCase); + } +}"; + + var fixedCode = @" +using System; + +public class Sample +{ + public void M() + { + ReadOnlySpan span = ""Hello"".AsSpan(); + int index = span.IndexOf(""test"", StringComparison.OrdinalIgnoreCase); + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + .WithSeverity(DiagnosticSeverity.Error) + .WithSpan(9, 42, 9, 83) + .WithArguments("IndexOf", "CurrentCultureIgnoreCase"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6002_SpanComparisonAnalyzer(), + () => new LuceneDev6002_SpanComparisonCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected }, + // The new logic should offer "OrdinalIgnoreCase" as CodeActionIndex = 0 for case-insensitive inputs + CodeActionIndex = 0, + // The fixed code does not trigger RedundantOrdinal, so 1 iteration is sufficient. + NumberOfFixAllIterations = 1 + }; + + await test.RunAsync(); + } + [Test] public async Task TestFix_RemoveRedundantOrdinal_Simple() { diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs index aad31a9..1ecaffd 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs @@ -75,10 +75,10 @@ public void M() await test.RunAsync(); } - [Test] -public async Task Fix_EscapedCharacter_StringLiteral() -{ - var testCode = @" + [Test] + public async Task Fix_EscapedCharacter_StringLiteral() + { + var testCode = @" using System; public class Sample @@ -90,7 +90,7 @@ public void M() } }"; - var fixedCode = @" + var fixedCode = @" using System; public class Sample @@ -102,23 +102,23 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) - .WithSeverity(DiagnosticSeverity.Info) - .WithArguments("IndexOf", "\"\\\"\"") - .WithSpan(10, 39, 10, 43); + var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + .WithSeverity(DiagnosticSeverity.Info) + .WithArguments("IndexOf", "\"\\\"\"") + .WithSpan(10, 39, 10, 43); - var test = new InjectableCodeFixTest( - () => new LuceneDev6003_SingleCharStringAnalyzer(), - () => new LuceneDev6003_SingleCharStringCodeFixProvider()) - { - TestCode = testCode, - FixedCode = fixedCode, - ExpectedDiagnostics = { expected }, - CodeActionIndex = 0 - }; + var test = new InjectableCodeFixTest( + () => new LuceneDev6003_SingleCharStringAnalyzer(), + () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected }, + CodeActionIndex = 0 + }; - await test.RunAsync(); -} + await test.RunAsync(); + } [Test] public async Task FixAll_SingleCharacterStringLiterals() @@ -171,6 +171,78 @@ public void M() CodeActionIndex = 0 }; + await test.RunAsync(); + } + [Test] + public async Task Fix_Span_IndexOf_SingleCharacter() + { + var testCode = @" +using System; + +public class Sample +{ + public void M(ReadOnlySpan span) + { + int index = span.IndexOf(""X""); + } +}"; + + var fixedCode = @" +using System; + +public class Sample +{ + public void M(ReadOnlySpan span) + { + int index = span.IndexOf('X'); + } +}"; + + // "X" starts at column 30 and ends at column 33 (3 chars wide) + var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + .WithSeverity(DiagnosticSeverity.Info) + .WithArguments("IndexOf", "\"X\"") + .WithSpan(9, 30, 9, 33); + + var test = new InjectableCodeFixTest( + () => new LuceneDev6003_SingleCharStringAnalyzer(), + () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected }, + CodeActionIndex = 0 + }; + + await test.RunAsync(); + } + + [Test] + public async Task NoFix_Span_StartsWith_SingleCharacter() + { + var testCode = @" +using System; + +public class Sample +{ + public void M(ReadOnlySpan span) + { + bool starts = span.StartsWith(""X""); + } +}"; + + // This test expects NO diagnostic, ensuring the Analyzer correctly skips + // ReadOnlySpan.StartsWith/EndsWith calls when the argument is a single-character string literal. + var test = new InjectableCodeFixTest( + () => new LuceneDev6003_SingleCharStringAnalyzer(), + () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + { + TestCode = testCode, + FixedCode = testCode, // Fixed code is the same as test code + ExpectedDiagnostics = { }, + CodeActionIndex = 0 + }; + await test.RunAsync(); } } diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs index 720a307..8589191 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs @@ -28,6 +28,56 @@ namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev6xxx [TestFixture] public class TestLuceneDev6001_StringComparisonAnalyzer { + [Test] + public async Task Skips_SingleCharStringLiteral_Alone() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int index = text.IndexOf(""H""); // Single-character string + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + // Expect no diagnostics because 6001 should skip single-character string literal alone + ExpectedDiagnostics = { } // No diagnostics expected + }; + + await test.RunAsync(); + } + + [Test] + public async Task NoDiagnostic_For_SingleCharString_MissingComparison() + { + var testCode = @" +using System; + +public class Sample +{ + public void M() + { + string text = ""Hello""; + int index = text.IndexOf(""H"", 0, 5); // Single-character string with startIndex/count + } +}"; + + // Change the test to use InjectableAnalyzerTest (no CodeFix) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { } // Asserting NO diagnostics are expected + }; + + await test.RunAsync(); + } + [Test] public async Task Detects_IndexOf_MissingStringComparison() { @@ -224,7 +274,7 @@ public void M() }"; var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) - .WithSeverity(DiagnosticSeverity.Warning) + .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) .WithArguments("IndexOf") .WithLocation("/0/Test0.cs", line: 9, column: 43); @@ -254,7 +304,7 @@ public void M() }"; var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) - .WithSeverity(DiagnosticSeverity.Warning) + .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) .WithArguments("StartsWith") .WithLocation("/0/Test0.cs", line: 9, column: 48); @@ -284,7 +334,7 @@ public void M() }"; var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) - .WithSeverity(DiagnosticSeverity.Warning) + .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) .WithArguments("EndsWith") .WithLocation("/0/Test0.cs", line: 9, column: 44); @@ -314,7 +364,7 @@ public void M() }"; var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) - .WithSeverity(DiagnosticSeverity.Warning) + .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) .WithArguments("LastIndexOf") .WithLocation("/0/Test0.cs", line: 9, column: 47); @@ -329,7 +379,7 @@ public void M() } [Test] - public async Task NoWarning_WithOrdinal() + public async Task NoError_WithOrdinal() { var testCode = @" using System; @@ -356,7 +406,7 @@ public void M() } [Test] - public async Task NoWarning_WithOrdinalIgnoreCase() + public async Task NoError_WithOrdinalIgnoreCase() { var testCode = @" using System; @@ -407,7 +457,7 @@ public void M() .WithLocation("/0/Test0.cs", line: 9, column: 27); var expected2 = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) - .WithSeverity(DiagnosticSeverity.Warning) + .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) .WithArguments("IndexOf") .WithLocation("/0/Test0.cs", line: 10, column: 44); From 1c82f19dfa3c9d0d803137c1af8a4a32e27d197b Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Thu, 23 Apr 2026 20:56:23 -0600 Subject: [PATCH 3/4] Address LuceneDev600x review feedback and fix tests - LuceneDev6003 code fix: use getInnermostNodeForTie + descendant fallback so FindNode resolves to the LiteralExpressionSyntax. - LuceneDev6003 analyzer: detect char overload correctly for spans. ReadOnlySpan.IndexOf(T) / LastIndexOf(T) are MemoryExtensions extensions, so the previous search on the containing type missed them. StartsWith/EndsWith intentionally remain excluded (no char overload exists for spans). - LuceneDev6001 analyzer: pass the invalid StringComparison value name to Diagnostic.Create so the {1} placeholder in the message format is substituted (previously rendered as literal "{1}"). - LuceneDev6001 code fix: use continue instead of return inside the diagnostic loop so multiple diagnostics in one file all get fixes offered. - Sample project: uncomment violating examples so the diagnostics fire live in the IDE (matches the existing LuceneDevXxxxSample convention). Add .editorconfig to downgrade error-severity rules to warning in the sample project so it still compiles. - LuceneDev6003 sample: rewrite to use the analyzer's target methods (IndexOf/LastIndexOf/StartsWith/EndsWith) rather than string.Equals, including a ReadOnlySpan example. - Clean up excessive blank lines in 6002 code fix and a stray trailing space in a variable name. - Update 6001 tests to include the comparison-value argument. - Fix off-by-one spans in 6003 code fix tests and add a span test. --- ...Dev6001_StringComparisonCodeFixProvider.cs | 25 ++--- ...neDev6002_SpanComparisonCodeFixProvider.cs | 43 ++------ ...Dev6003_SingleCharStringCodeFixProvider.cs | 9 +- .../.editorconfig | 7 ++ .../LuceneDev6001_StringComparisonSample.cs | 39 +++---- .../LuceneDev6002_SpanComparisonSample.cs | 102 ++++-------------- .../LuceneDev6003_SingleCharStringSample.cs | 48 +++------ .../LuceneDev6001_StringComparisonAnalyzer.cs | 51 ++++++--- .../LuceneDev6002_SpanComparisonAnalyzer.cs | 4 +- .../LuceneDev6003_SingleCharStringAnalyzer.cs | 52 ++++----- ...Dev6001_StringComparisonCodeFixProvider.cs | 6 +- ...Dev6003_SingleCharStringCodeFixProvider.cs | 14 +-- ...tLuceneDev6001_StringComparisonAnalyzer.cs | 10 +- 13 files changed, 147 insertions(+), 263 deletions(-) create mode 100644 src/Lucene.Net.CodeAnalysis.Dev.Sample/.editorconfig diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs index b780d2b..4c5c6f0 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs @@ -54,28 +54,23 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); if (semanticModel == null) continue; - //Double check to Skip char literals and single-character string literals when safe --- + // Skip char literals and single-character string literals when safe (covered by 6003 instead). var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; if (firstArgExpr is LiteralExpressionSyntax lit) { if (lit.IsKind(SyntaxKind.CharacterLiteralExpression)) - return; // already char overload; no diagnostic + continue; if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1) { - // Check if a StringComparison argument is present - bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg => - semanticModel.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t && - t.ToDisplayString() == "System.StringComparison" - || (semanticModel.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && - f.ContainingType?.ToDisplayString() == "System.StringComparison")); - - if (!hasStringComparisonArgForLiteral) - { - // safe to convert to char (6003), so skip 6001 reporting - return; - } - // else: has StringComparison -> do not skip; let codefix handle it + bool hasStringComparisonArg = invocation.ArgumentList.Arguments.Any(arg => + (semanticModel.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t && + t.ToDisplayString() == "System.StringComparison") + || (semanticModel.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && + f.ContainingType?.ToDisplayString() == "System.StringComparison")); + + if (!hasStringComparisonArg) + continue; } } diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs index 81ab0da..92b45f2 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs @@ -64,59 +64,28 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) if (invocation == null) return; - //Double check to Skip char literals and single-character string literals when safe --- + // Skip char literals and single-character string literals when safe (covered by 6003 instead). var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; - if (firstArgExpr is LiteralExpressionSyntax lit) - { - if (lit.IsKind(SyntaxKind.CharacterLiteralExpression)) - - return; // already char overload; skip 6002 fix - - + return; if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1) - { - - // Check if a StringComparison argument is present - var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); - if (semanticModel == null) - return; - - - bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg => - - semanticModel.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t && - - t.ToDisplayString() == "System.StringComparison" - + bool hasStringComparisonArg = invocation.ArgumentList.Arguments.Any(arg => + (semanticModel.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t && + t.ToDisplayString() == "System.StringComparison") || (semanticModel.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && - f.ContainingType?.ToDisplayString() == "System.StringComparison")); - - - if (!hasStringComparisonArgForLiteral) - - { - - // safe to convert to char (6003), skip 6002 fix - + if (!hasStringComparisonArg) return; - - } - - // else: has StringComparison -> let the codefix continue - } - } switch (diagnostic.Id) { diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs index b605425..3a66887 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs @@ -16,10 +16,10 @@ * limitations under the License. */ -using System; using System.Collections.Immutable; using System.Composition; using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Lucene.Net.CodeAnalysis.Dev.Utility; @@ -49,9 +49,12 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) return; var diagnosticSpan = diagnostic.Location.SourceSpan; - var node = root.FindNode(diagnosticSpan); + var node = root.FindNode(diagnosticSpan, getInnermostNodeForTie: true); - if (node is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression)) + var literal = node as LiteralExpressionSyntax + ?? node.DescendantNodesAndSelf().OfType().FirstOrDefault(l => l.Span == diagnosticSpan); + + if (literal != null && literal.IsKind(SyntaxKind.StringLiteralExpression)) { context.RegisterCodeFix( CodeAction.Create( diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/.editorconfig b/src/Lucene.Net.CodeAnalysis.Dev.Sample/.editorconfig new file mode 100644 index 0000000..89c6179 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/.editorconfig @@ -0,0 +1,7 @@ +# The Sample project intentionally contains code that fires Lucene.NET analyzers +# so contributors can see the diagnostics live in their IDE. Downgrade Error-severity +# rules to Warning here so the project still compiles. +[*.cs] +dotnet_diagnostic.LuceneDev6001_1.severity = warning +dotnet_diagnostic.LuceneDev6001_2.severity = warning +dotnet_diagnostic.LuceneDev6002_2.severity = warning diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs index 638909a..f5949dd 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs @@ -1,4 +1,4 @@ -/* +/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -17,37 +17,26 @@ * under the License. */ +using System; + namespace Lucene.Net.CodeAnalysis.Dev.Sample; public class LuceneDev6001_StringComparisonSample { - // public void BadExample_MissingStringComparison() - // { - // string text = "Hello World"; - - // //Missing StringComparison parameter - // int index = text.IndexOf("Hello"); - // bool starts = text.StartsWith("Hello"); - // bool ends = text.EndsWith("World"); - // } - - public void GoodExample_Ordinal() + public void MyMethod() { string text = "Hello World"; - //Correct usage with StringComparison.Ordinal - int index = text.IndexOf("Hello", System.StringComparison.Ordinal); - bool starts = text.StartsWith("Hello", System.StringComparison.Ordinal); - bool ends = text.EndsWith("World", System.StringComparison.Ordinal); - } - - public void GoodExample_OrdinalIgnoreCase() - { - string text = "Hello World"; + // Missing StringComparison argument: triggers LuceneDev6001_1 (Error). + int index1 = text.IndexOf("Hello"); + bool starts1 = text.StartsWith("Hello"); + bool ends1 = text.EndsWith("World"); + int lastIndex1 = text.LastIndexOf("World"); - // Correct usage with StringComparison.OrdinalIgnoreCase - int index = text.IndexOf("hello", System.StringComparison.OrdinalIgnoreCase); - bool starts = text.StartsWith("HELLO", System.StringComparison.OrdinalIgnoreCase); - bool ends = text.EndsWith("world", System.StringComparison.OrdinalIgnoreCase); + // Invalid StringComparison value: triggers LuceneDev6001_2 (Error). + int index2 = text.IndexOf("Hello", StringComparison.CurrentCulture); + bool starts2 = text.StartsWith("hello", StringComparison.CurrentCultureIgnoreCase); + bool ends2 = text.EndsWith("World", StringComparison.InvariantCulture); + int lastIndex2 = text.LastIndexOf("world", StringComparison.InvariantCultureIgnoreCase); } } diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs index 5f16f62..61532c8 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs @@ -1,4 +1,4 @@ -/* +/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -19,90 +19,24 @@ using System; -namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev6xxx +namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev6xxx; + +public class LuceneDev6002_SpanComparisonSample { - /// - /// Sample code demonstrating LuceneDev6002 analyzer rules for Span types. - /// Rule: Span types should not use StringComparison.Ordinal (redundant) - /// and must only use Ordinal or OrdinalIgnoreCase. - /// - public class LuceneDev6002_SpanComparisonSample + public void MyMethod() { - // public void BadExamples_RedundantOrdinal() - // { - // ReadOnlySpan span = "Hello World".AsSpan(); - - // // Redundant StringComparison.Ordinal - // int index1 = span.IndexOf("Hello".AsSpan(), StringComparison.Ordinal); - // int index2 = span.LastIndexOf("World".AsSpan(), StringComparison.Ordinal); - // bool starts = span.StartsWith("Hello".AsSpan(), StringComparison.Ordinal); - // bool ends = span.EndsWith("World".AsSpan(), StringComparison.Ordinal); - // } - - // public void BadExamples_InvalidComparison() - // { - // ReadOnlySpan span = "Hello World".AsSpan(); - - // // Culture-sensitive comparisons are not allowed on Span types - // int index1 = span.IndexOf("Hello", StringComparison.CurrentCulture); - // int index2 = span.LastIndexOf("World", StringComparison.CurrentCultureIgnoreCase); - // bool starts = span.StartsWith("Hello", StringComparison.InvariantCulture); - // bool ends = span.EndsWith("World", StringComparison.InvariantCultureIgnoreCase); - // } - - public void GoodExamples_NoStringComparison() - { - ReadOnlySpan span = "Hello World".AsSpan(); - - // Correct: defaults to Ordinal - int index1 = span.IndexOf("Hello".AsSpan()); - int index2 = span.LastIndexOf("World".AsSpan()); - bool starts = span.StartsWith("Hello".AsSpan()); - bool ends = span.EndsWith("World".AsSpan()); - - // Single char operations - int charIndex = span.IndexOf('H'); - bool startsWithChar = span[0] == 'H'; - } - - public void GoodExamples_WithOrdinalIgnoreCase() - { - ReadOnlySpan span = "Hello World".AsSpan(); - - // Correct: case-insensitive search - int index = span.IndexOf("hello", StringComparison.OrdinalIgnoreCase); - int lastIndex = span.LastIndexOf("WORLD", StringComparison.OrdinalIgnoreCase); - bool starts = span.StartsWith("HELLO", StringComparison.OrdinalIgnoreCase); - bool ends = span.EndsWith("world", StringComparison.OrdinalIgnoreCase); - } - - public void RealWorldExamples() - { - string path = @"C:\Users\Documents\file.txt"; - ReadOnlySpan pathSpan = path.AsSpan(); - - // Correct: OrdinalIgnoreCase allowed - bool isTxtFile = pathSpan.EndsWith(".txt", StringComparison.OrdinalIgnoreCase); - - // Correct: No StringComparison needed - ReadOnlySpan url = "https://example.com".AsSpan(); - bool isHttps = url.StartsWith("https://"); - - ReadOnlySpan token = "Bearer:abc123".AsSpan(); - int separatorIndex = token.IndexOf(':'); - } - - public void StringTypeComparison() - { - // Analyzer applies only to Span types - string text = "Hello World"; - - // String types require StringComparison - int index = text.IndexOf("Hello", StringComparison.Ordinal); - - // Span types should not specify Ordinal - ReadOnlySpan span = text.AsSpan(); - int index2 = span.IndexOf("Hello"); - } + ReadOnlySpan span = "Hello World".AsSpan(); + + // Redundant StringComparison.Ordinal on span: triggers LuceneDev6002_1 (Warning). + int index1 = span.IndexOf("Hello".AsSpan(), StringComparison.Ordinal); + int lastIndex1 = span.LastIndexOf("World".AsSpan(), StringComparison.Ordinal); + bool starts1 = span.StartsWith("Hello".AsSpan(), StringComparison.Ordinal); + bool ends1 = span.EndsWith("World".AsSpan(), StringComparison.Ordinal); + + // Invalid comparison on span: triggers LuceneDev6002_2 (Error). + int index2 = span.IndexOf("Hello".AsSpan(), StringComparison.CurrentCulture); + int lastIndex2 = span.LastIndexOf("World".AsSpan(), StringComparison.CurrentCultureIgnoreCase); + bool starts2 = span.StartsWith("Hello".AsSpan(), StringComparison.InvariantCulture); + bool ends2 = span.EndsWith("World".AsSpan(), StringComparison.InvariantCultureIgnoreCase); } } diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs index 460737a..32f39c1 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs @@ -1,4 +1,4 @@ -/* +/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -19,40 +19,26 @@ using System; -namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev6xxx +namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev6xxx; + +public class LuceneDev6003_SingleCharStringSample { - /// - /// Sample code for LuceneDev6003: Suggest using char overloads instead of single-character string literals. - /// - public class LuceneDev6003_SingleCharStringSample + public void MyMethod() { - public void Example() - { - string input = "Hello"; - - // BAD: Using string.Equals with single-character string literal - // if (string.Equals(input[0].ToString(), "H")) - // { - // Console.WriteLine("Starts with H"); - // } + string text = "Hello World"; - // BAD: Using Equals instance method - // if (input[0].ToString().Equals("H")) - // { - // Console.WriteLine("Starts with H"); - // } + // Single-character string literal: triggers LuceneDev6003 (Info). + int index1 = text.IndexOf("H", StringComparison.Ordinal); + int lastIndex1 = text.LastIndexOf("d", StringComparison.Ordinal); + bool starts1 = text.StartsWith("H", StringComparison.Ordinal); + bool ends1 = text.EndsWith("d", StringComparison.Ordinal); - // GOOD: Using char comparison instead of string - if (input[0] == 'H') - { - Console.WriteLine("Starts with H"); - } + // Escaped single-character string literal: also triggers LuceneDev6003. + int newlineIndex = text.IndexOf("\n", StringComparison.Ordinal); - //GOOD: Using Char.Equals - if (char.Equals(input[0], 'H')) - { - Console.WriteLine("Starts with H"); - } - } + // IndexOf/LastIndexOf have a char overload on ReadOnlySpan: triggers LuceneDev6003. + ReadOnlySpan span = text.AsSpan(); + int index2 = span.IndexOf("H"); + int lastIndex2 = span.LastIndexOf("d"); } } diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs index 59e65a8..de995c4 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs @@ -74,13 +74,13 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1) { // Check if a StringComparison argument is present - bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg => + bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg => semantic.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t && t.ToDisplayString() == "System.StringComparison" || (semantic.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && f.ContainingType?.ToDisplayString() == "System.StringComparison")); - if (!hasStringComparisonArgForLiteral ) + if (!hasStringComparisonArgForLiteral) { // safe to convert to char (6003), so skip 6001 reporting return; @@ -115,7 +115,7 @@ static bool HasStringComparisonParameter(IMethodSymbol? m, INamedTypeSymbol scTy } // Check if invocation has StringComparison argument and validate it - var (hasStringComparisonArg, isValidValue, invalidArgLocation) = + var (hasStringComparisonArg, isValidValue, invalidArgLocation, comparisonValueName) = CheckStringComparisonArgument(invocation, semantic, stringComparisonType); // If resolved symbol available @@ -136,7 +136,8 @@ static bool HasStringComparisonParameter(IMethodSymbol? m, INamedTypeSymbol scTy var diag = Diagnostic.Create( Descriptors.LuceneDev6001_InvalidStringComparison, invalidArgLocation ?? memberAccess.Name.GetLocation(), - methodName); + methodName, + comparisonValueName ?? "non-ordinal comparison"); ctx.ReportDiagnostic(diag); } return; @@ -175,7 +176,8 @@ static bool HasStringComparisonParameter(IMethodSymbol? m, INamedTypeSymbol scTy var diag = Diagnostic.Create( Descriptors.LuceneDev6001_InvalidStringComparison, invalidArgLocation ?? memberAccess.Name.GetLocation(), - methodName); + methodName, + comparisonValueName ?? "non-ordinal comparison"); ctx.ReportDiagnostic(diag); } return; @@ -197,7 +199,7 @@ static bool HasStringComparisonParameter(IMethodSymbol? m, INamedTypeSymbol scTy } } - private static (bool hasArgument, bool isValid, Location? location) CheckStringComparisonArgument( + private static (bool hasArgument, bool isValid, Location? location, string? valueName) CheckStringComparisonArgument( InvocationExpressionSyntax invocation, SemanticModel semantic, INamedTypeSymbol stringComparisonType) @@ -206,23 +208,38 @@ private static (bool hasArgument, bool isValid, Location? location) CheckStringC { var argType = semantic.GetTypeInfo(arg.Expression).Type; - // Check if argument type is StringComparison - if (argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType)) - { - bool isValid = IsValidStringComparisonValue(semantic, arg.Expression, stringComparisonType); - return (true, isValid, arg.Expression.GetLocation()); - } - - // Also check for enum member access (e.g., StringComparison.Ordinal) + bool typeMatches = argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType); var argSymbol = semantic.GetSymbolInfo(arg.Expression).Symbol as IFieldSymbol; - if (argSymbol != null && SymbolEqualityComparer.Default.Equals(argSymbol.ContainingType, stringComparisonType)) + bool symbolMatches = argSymbol != null && SymbolEqualityComparer.Default.Equals(argSymbol.ContainingType, stringComparisonType); + + if (typeMatches || symbolMatches) { bool isValid = IsValidStringComparisonValue(semantic, arg.Expression, stringComparisonType); - return (true, isValid, arg.Expression.GetLocation()); + string? name = argSymbol?.Name ?? GetStringComparisonNameFromConstant(semantic, arg.Expression); + return (true, isValid, arg.Expression.GetLocation(), name); } } - return (false, true, null); + return (false, true, null, null); + } + + private static string? GetStringComparisonNameFromConstant(SemanticModel semantic, ExpressionSyntax expression) + { + var constantValue = semantic.GetConstantValue(expression); + if (constantValue.HasValue && constantValue.Value is int intValue) + { + return intValue switch + { + 0 => "CurrentCulture", + 1 => "CurrentCultureIgnoreCase", + 2 => "InvariantCulture", + 3 => "InvariantCultureIgnoreCase", + 4 => "Ordinal", + 5 => "OrdinalIgnoreCase", + _ => null + }; + } + return null; } private static bool IsValidStringComparisonValue( diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs index fc9d413..333d17d 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs @@ -110,13 +110,13 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) if (lit.IsKind(SyntaxKind.StringLiteralExpression) && lit.Token.ValueText.Length == 1) { // Check if a StringComparison argument is present - bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg => + bool hasStringComparisonArgForLiteral = invocation.ArgumentList.Arguments.Any(arg => semantic.GetTypeInfo(arg.Expression).Type is INamedTypeSymbol t && t.ToDisplayString() == "System.StringComparison" || (semantic.GetSymbolInfo(arg.Expression).Symbol is IFieldSymbol f && f.ContainingType?.ToDisplayString() == "System.StringComparison")); - if (!hasStringComparisonArgForLiteral ) + if (!hasStringComparisonArgForLiteral) { // safe to convert to char (6003), so skip 6001 reporting return; diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs index 33e0f28..ea345c2 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs @@ -212,44 +212,32 @@ private static bool HasCharOverload( ITypeSymbol? receiverType, string methodName) { - ImmutableArray methodsToCheck = ImmutableArray.Empty; - - // Strategy 1: Get all methods with the same name from the resolved method's containing type - if (methodSymbol != null && methodSymbol.ContainingType != null) - { - methodsToCheck = methodSymbol.ContainingType - .GetMembers(methodName) - .OfType() - .ToImmutableArray(); - } - // Strategy 2: Use candidate symbols if method couldn't be resolved - else if (candidateSymbols.Length > 0) + // Span/ReadOnlySpan: IndexOf(char) and LastIndexOf(char) exist as generic + // MemoryExtensions extension methods with signature IndexOf(this ReadOnlySpan, T). + // StartsWith/EndsWith have no char overload on spans. + if (IsSpanLikeReceiver(receiverType)) + return methodName == "IndexOf" || methodName == "LastIndexOf"; + + // For strings and other types: search the containing/receiver types for an overload + // whose first non-receiver parameter is System.Char. + var methodsToCheck = ImmutableArray.CreateBuilder(); + if (receiverType != null) + methodsToCheck.AddRange(receiverType.GetMembers(methodName).OfType()); + if (methodSymbol?.ContainingType != null) + methodsToCheck.AddRange(methodSymbol.ContainingType.GetMembers(methodName).OfType()); + if (candidateSymbols.Length > 0) { - methodsToCheck = candidateSymbols; - - // Also try to get more methods from the first candidate's containing type - var containingType = candidateSymbols.FirstOrDefault()?.ContainingType; + methodsToCheck.AddRange(candidateSymbols); + var containingType = candidateSymbols[0].ContainingType; if (containingType != null) - { - var additionalMethods = containingType.GetMembers(methodName).OfType(); - methodsToCheck = methodsToCheck.Concat(additionalMethods).ToImmutableArray(); - } - } - // Strategy 3: Use receiver type if nothing else worked - else if (receiverType != null) - { - methodsToCheck = receiverType - .GetMembers(methodName) - .OfType() - .ToImmutableArray(); + methodsToCheck.AddRange(containingType.GetMembers(methodName).OfType()); } - // Look for a char overload - // The char overload should have System.Char as the first parameter (the value parameter) foreach (var method in methodsToCheck) { - if (method.Parameters.Length > 0 && - method.Parameters[0].Type.SpecialType == SpecialType.System_Char) + var valueParamIndex = method.IsExtensionMethod && method.ReducedFrom == null ? 1 : 0; + if (method.Parameters.Length > valueParamIndex && + method.Parameters[valueParamIndex].Type.SpecialType == SpecialType.System_Char) { return true; } diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs index 8c926ba..7707685 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs @@ -329,7 +329,7 @@ public void MyMethod() var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) - .WithArguments("IndexOf") + .WithArguments("IndexOf", "CurrentCulture") .WithLocation("/0/Test0.cs", line: 9, column: 43); var test = new InjectableCodeFixTest( @@ -374,7 +374,7 @@ public void MyMethod() var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) - .WithArguments("StartsWith") + .WithArguments("StartsWith", "InvariantCulture") .WithLocation("/0/Test0.cs", line: 9, column: 48); var test = new InjectableCodeFixTest( @@ -419,7 +419,7 @@ public void MyMethod() var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) - .WithArguments("EndsWith") + .WithArguments("EndsWith", "CurrentCultureIgnoreCase") .WithLocation("/0/Test0.cs", line: 9, column: 44); var test = new InjectableCodeFixTest( diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs index 1ecaffd..adb1452 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs @@ -56,11 +56,10 @@ public void M() } }"; - // "H" starts at column 39 and ends at column 42 (3 chars wide) var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) .WithSeverity(DiagnosticSeverity.Info) .WithArguments("IndexOf", "\"H\"") - .WithSpan(10, 39, 10, 42); + .WithSpan(9, 34, 9, 37); var test = new InjectableCodeFixTest( () => new LuceneDev6003_SingleCharStringAnalyzer(), @@ -105,7 +104,7 @@ public void M() var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) .WithSeverity(DiagnosticSeverity.Info) .WithArguments("IndexOf", "\"\\\"\"") - .WithSpan(10, 39, 10, 43); + .WithSpan(9, 34, 9, 38); var test = new InjectableCodeFixTest( () => new LuceneDev6003_SingleCharStringAnalyzer(), @@ -149,17 +148,15 @@ public void M() } }"; - // First: "H" (line 10, columns 38–41 → 3 chars) var expected1 = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) .WithSeverity(DiagnosticSeverity.Info) .WithArguments("IndexOf", "\"H\"") - .WithSpan(10, 38, 10, 41); + .WithSpan(9, 31, 9, 34); - // Second: "\n" (line 11, columns 38–42 → 4 chars) var expected2 = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) .WithSeverity(DiagnosticSeverity.Info) .WithArguments("IndexOf", "\"\\n\"") - .WithSpan(11, 38, 11, 42); + .WithSpan(10, 31, 10, 35); var test = new InjectableCodeFixTest( () => new LuceneDev6003_SingleCharStringAnalyzer(), @@ -198,11 +195,10 @@ public void M(ReadOnlySpan span) } }"; - // "X" starts at column 30 and ends at column 33 (3 chars wide) var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) .WithSeverity(DiagnosticSeverity.Info) .WithArguments("IndexOf", "\"X\"") - .WithSpan(9, 30, 9, 33); + .WithSpan(8, 34, 8, 37); var test = new InjectableCodeFixTest( () => new LuceneDev6003_SingleCharStringAnalyzer(), diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs index 8589191..0a0d063 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs @@ -276,7 +276,7 @@ public void M() var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) - .WithArguments("IndexOf") + .WithArguments("IndexOf", "CurrentCulture") .WithLocation("/0/Test0.cs", line: 9, column: 43); var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) @@ -306,7 +306,7 @@ public void M() var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) - .WithArguments("StartsWith") + .WithArguments("StartsWith", "CurrentCultureIgnoreCase") .WithLocation("/0/Test0.cs", line: 9, column: 48); var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) @@ -336,7 +336,7 @@ public void M() var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) - .WithArguments("EndsWith") + .WithArguments("EndsWith", "InvariantCulture") .WithLocation("/0/Test0.cs", line: 9, column: 44); var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) @@ -366,7 +366,7 @@ public void M() var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) - .WithArguments("LastIndexOf") + .WithArguments("LastIndexOf", "InvariantCultureIgnoreCase") .WithLocation("/0/Test0.cs", line: 9, column: 47); var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) @@ -459,7 +459,7 @@ public void M() var expected2 = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) - .WithArguments("IndexOf") + .WithArguments("IndexOf", "CurrentCulture") .WithLocation("/0/Test0.cs", line: 10, column: 44); var expected3 = new DiagnosticResult(Descriptors.LuceneDev6001_MissingStringComparison) From fab92c34d4b4177cceb83dc1653bee9061cd016f Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Thu, 23 Apr 2026 21:10:42 -0600 Subject: [PATCH 4/4] Renumber LuceneDev6xxx rules to contiguous IDs Replace the underscore-suffixed IDs (6001_1, 6001_2, 6002_1, 6002_2) with contiguous numeric IDs matching the rest of the Lucene.NET analyzer taxonomy: 6001_1 -> 6001 Missing StringComparison on String overload 6001_2 -> 6002 Invalid StringComparison on String overload 6002_1 -> 6003 Redundant Ordinal on span overload 6002_2 -> 6004 Invalid StringComparison on span overload 6003 -> 6005 Single-character string literal Each analyzer class still bundles the two related diagnostics that share symbol resolution; files are named with the ID range (e.g., LuceneDev6001_6002_StringComparisonAnalyzer.cs) and the LuceneDev6005 analyzer stands alone. Update Descriptors, Resources.resx, AnalyzerReleases.Unshipped.md, DiagnosticCategoryAndIdRanges.txt, samples, editorconfig overrides, and tests to reference the new IDs. Tighten the 6005 summary doc to note that spans only have char overloads for IndexOf/LastIndexOf. --- DiagnosticCategoryAndIdRanges.txt | 2 +- ...1_6002_StringComparisonCodeFixProvider.cs} | 10 ++-- ...003_6004_SpanComparisonCodeFixProvider.cs} | 14 ++--- ...ev6005_SingleCharStringCodeFixProvider.cs} | 8 +-- .../.editorconfig | 6 +- ...eneDev6001_6002_StringComparisonSample.cs} | 6 +- ...uceneDev6003_6004_SpanComparisonSample.cs} | 6 +- ...> LuceneDev6005_SingleCharStringSample.cs} | 2 +- .../AnalyzerReleases.Unshipped.md | 10 ++-- ...eDev6001_6002_StringComparisonAnalyzer.cs} | 12 ++-- ...eneDev6003_6004_SpanComparisonAnalyzer.cs} | 14 ++--- ...LuceneDev6005_SingleCharStringAnalyzer.cs} | 24 ++++---- .../Resources.resx | 40 ++++++------- .../Utility/Descriptors.LuceneDev6xxx.cs | 30 +++++----- ...1_6002_StringComparisonCodeFixProvider.cs} | 58 +++++++++---------- ...003_6004_SpanComparisonCodeFixProvider.cs} | 30 +++++----- ...ev6005_SingleCharStringCodeFixProvider.cs} | 32 +++++----- ...eDev6001_6002_StringComparisonAnalyzer.cs} | 54 ++++++++--------- ...eneDev6003_6004_SpanComparisonAnalyzer.cs} | 40 ++++++------- ...LuceneDev6005_SingleCharStringAnalyzer.cs} | 12 ++-- 20 files changed, 204 insertions(+), 206 deletions(-) rename src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/{LuceneDev6001_StringComparisonCodeFixProvider.cs => LuceneDev6001_6002_StringComparisonCodeFixProvider.cs} (97%) rename src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/{LuceneDev6002_SpanComparisonCodeFixProvider.cs => LuceneDev6003_6004_SpanComparisonCodeFixProvider.cs} (96%) rename src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/{LuceneDev6003_SingleCharStringCodeFixProvider.cs => LuceneDev6005_SingleCharStringCodeFixProvider.cs} (94%) rename src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/{LuceneDev6001_StringComparisonSample.cs => LuceneDev6001_6002_StringComparisonSample.cs} (95%) rename src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/{LuceneDev6002_SpanComparisonSample.cs => LuceneDev6003_6004_SpanComparisonSample.cs} (92%) rename src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/{LuceneDev6003_SingleCharStringSample.cs => LuceneDev6005_SingleCharStringSample.cs} (97%) rename src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/{LuceneDev6001_StringComparisonAnalyzer.cs => LuceneDev6001_6002_StringComparisonAnalyzer.cs} (96%) rename src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/{LuceneDev6002_SpanComparisonAnalyzer.cs => LuceneDev6003_6004_SpanComparisonAnalyzer.cs} (94%) rename src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/{LuceneDev6003_SingleCharStringAnalyzer.cs => LuceneDev6005_SingleCharStringAnalyzer.cs} (92%) rename tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/{TestLuceneDev6001_StringComparisonCodeFixProvider.cs => TestLuceneDev6001_6002_StringComparisonCodeFixProvider.cs} (86%) rename tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/{TestLuceneDev6002_SpanComparisonCodeFixProvider.cs => TestLuceneDev6003_6004_SpanComparisonCodeFixProvider.cs} (87%) rename tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/{TestLuceneDev6003_SingleCharStringCodeFixProvider.cs => TestLuceneDev6005_SingleCharStringCodeFixProvider.cs} (87%) rename tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/{TestLuceneDev6001_StringComparisonAnalyzer.cs => TestLuceneDev6001_6002_StringComparisonAnalyzer.cs} (91%) rename tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/{TestLuceneDev6002_SpanComparisonAnalyzer.cs => TestLuceneDev6003_6004_SpanComparisonAnalyzer.cs} (91%) rename tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/{TestLuceneDev6003_SingleCharStringAnalyzer.cs => TestLuceneDev6005_SingleCharStringAnalyzer.cs} (92%) diff --git a/DiagnosticCategoryAndIdRanges.txt b/DiagnosticCategoryAndIdRanges.txt index f8e5bce..e5c2083 100644 --- a/DiagnosticCategoryAndIdRanges.txt +++ b/DiagnosticCategoryAndIdRanges.txt @@ -18,7 +18,7 @@ Globalization: Mobility: Performance: Security: -Usage: LuceneDev6000-LuceneDev6003 +Usage: LuceneDev6000-LuceneDev6005 Naming: Interoperability: Maintainability: diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonCodeFixProvider.cs similarity index 97% rename from src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs rename to src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonCodeFixProvider.cs index 4c5c6f0..98a676d 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_StringComparisonCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonCodeFixProvider.cs @@ -18,8 +18,8 @@ namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6001_StringComparisonCodeFixProvider)), Shared] - public sealed class LuceneDev6001_StringComparisonCodeFixProvider : CodeFixProvider + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6001_6002_StringComparisonCodeFixProvider)), Shared] + public sealed class LuceneDev6001_6002_StringComparisonCodeFixProvider : CodeFixProvider { private const string Ordinal = "Ordinal"; private const string OrdinalIgnoreCase = "OrdinalIgnoreCase"; @@ -29,7 +29,7 @@ public sealed class LuceneDev6001_StringComparisonCodeFixProvider : CodeFixProvi public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( Descriptors.LuceneDev6001_MissingStringComparison.Id, - Descriptors.LuceneDev6001_InvalidStringComparison.Id); + Descriptors.LuceneDev6002_InvalidStringComparison.Id); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -54,7 +54,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); if (semanticModel == null) continue; - // Skip char literals and single-character string literals when safe (covered by 6003 instead). + // Skip char literals and single-character string literals when safe (LuceneDev6005 handles conversion). var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; if (firstArgExpr is LiteralExpressionSyntax lit) { @@ -81,7 +81,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) // Case 1: Argument is missing. Only offer Ordinal as the safe, conservative default. RegisterFix(context, invocation, Ordinal, TitleOrdinal, diagnostic); } - else if (diagnostic.Id == Descriptors.LuceneDev6001_InvalidStringComparison.Id) + else if (diagnostic.Id == Descriptors.LuceneDev6002_InvalidStringComparison.Id) { // Case 2: Invalid argument is present. Determine the best replacement. if (TryDetermineReplacement(invocation, semanticModel, out string? targetComparison)) diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_6004_SpanComparisonCodeFixProvider.cs similarity index 96% rename from src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs rename to src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_6004_SpanComparisonCodeFixProvider.cs index 92b45f2..2489c87 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6002_SpanComparisonCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_6004_SpanComparisonCodeFixProvider.cs @@ -30,8 +30,8 @@ namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev6xxx { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6002_SpanComparisonCodeFixProvider)), Shared] - public sealed class LuceneDev6002_SpanComparisonCodeFixProvider : CodeFixProvider + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6003_6004_SpanComparisonCodeFixProvider)), Shared] + public sealed class LuceneDev6003_6004_SpanComparisonCodeFixProvider : CodeFixProvider { private const string TitleRemoveOrdinal = "Remove redundant StringComparison.Ordinal"; private const string TitleOptimizeToDefaultOrdinal = "Optimize to default Ordinal comparison (remove argument)"; @@ -47,8 +47,8 @@ public sealed class LuceneDev6002_SpanComparisonCodeFixProvider : CodeFixProvide public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( - Descriptors.LuceneDev6002_RedundantOrdinal.Id, - Descriptors.LuceneDev6002_InvalidComparison.Id); + Descriptors.LuceneDev6003_RedundantOrdinal.Id, + Descriptors.LuceneDev6004_InvalidComparison.Id); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -64,7 +64,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) if (invocation == null) return; - // Skip char literals and single-character string literals when safe (covered by 6003 instead). + // Skip char literals and single-character string literals when safe (LuceneDev6005 handles conversion). var firstArgExpr = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; if (firstArgExpr is LiteralExpressionSyntax lit) { @@ -89,7 +89,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) } switch (diagnostic.Id) { - case var id when id == Descriptors.LuceneDev6002_RedundantOrdinal.Id: + case var id when id == Descriptors.LuceneDev6003_RedundantOrdinal.Id: context.RegisterCodeFix( CodeAction.Create( title: TitleRemoveOrdinal, @@ -98,7 +98,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) diagnostic); break; - case var id when id == Descriptors.LuceneDev6002_InvalidComparison.Id: + case var id when id == Descriptors.LuceneDev6004_InvalidComparison.Id: var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); if (semanticModel == null) return; diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6005_SingleCharStringCodeFixProvider.cs similarity index 94% rename from src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs rename to src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6005_SingleCharStringCodeFixProvider.cs index 3a66887..84312e8 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6003_SingleCharStringCodeFixProvider.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev6xxx/LuceneDev6005_SingleCharStringCodeFixProvider.cs @@ -31,12 +31,12 @@ namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx { - [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6003_SingleCharStringCodeFixProvider))] + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev6005_SingleCharStringCodeFixProvider))] [Shared] - public sealed class LuceneDev6003_SingleCharStringCodeFixProvider : CodeFixProvider + public sealed class LuceneDev6005_SingleCharStringCodeFixProvider : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds - => ImmutableArray.Create(Descriptors.LuceneDev6003_SingleCharStringAnalyzer.Id); + => ImmutableArray.Create(Descriptors.LuceneDev6005_SingleCharString.Id); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -60,7 +60,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) CodeAction.Create( "Use char literal", c => ReplaceWithCharLiteralAsync(context.Document, literal, c), - nameof(LuceneDev6003_SingleCharStringCodeFixProvider)), + nameof(LuceneDev6005_SingleCharStringCodeFixProvider)), diagnostic); } } diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/.editorconfig b/src/Lucene.Net.CodeAnalysis.Dev.Sample/.editorconfig index 89c6179..5d24a55 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/.editorconfig +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/.editorconfig @@ -2,6 +2,6 @@ # so contributors can see the diagnostics live in their IDE. Downgrade Error-severity # rules to Warning here so the project still compiles. [*.cs] -dotnet_diagnostic.LuceneDev6001_1.severity = warning -dotnet_diagnostic.LuceneDev6001_2.severity = warning -dotnet_diagnostic.LuceneDev6002_2.severity = warning +dotnet_diagnostic.LuceneDev6001.severity = warning +dotnet_diagnostic.LuceneDev6002.severity = warning +dotnet_diagnostic.LuceneDev6004.severity = warning diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonSample.cs similarity index 95% rename from src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs rename to src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonSample.cs index f5949dd..8aa31b1 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_StringComparisonSample.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonSample.cs @@ -21,19 +21,19 @@ namespace Lucene.Net.CodeAnalysis.Dev.Sample; -public class LuceneDev6001_StringComparisonSample +public class LuceneDev6001_6002_StringComparisonSample { public void MyMethod() { string text = "Hello World"; - // Missing StringComparison argument: triggers LuceneDev6001_1 (Error). + // Missing StringComparison argument: triggers LuceneDev6001 (Error). int index1 = text.IndexOf("Hello"); bool starts1 = text.StartsWith("Hello"); bool ends1 = text.EndsWith("World"); int lastIndex1 = text.LastIndexOf("World"); - // Invalid StringComparison value: triggers LuceneDev6001_2 (Error). + // Invalid StringComparison value: triggers LuceneDev6002 (Error). int index2 = text.IndexOf("Hello", StringComparison.CurrentCulture); bool starts2 = text.StartsWith("hello", StringComparison.CurrentCultureIgnoreCase); bool ends2 = text.EndsWith("World", StringComparison.InvariantCulture); diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_6004_SpanComparisonSample.cs similarity index 92% rename from src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs rename to src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_6004_SpanComparisonSample.cs index 61532c8..2626a36 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6002_SpanComparisonSample.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_6004_SpanComparisonSample.cs @@ -21,19 +21,19 @@ namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev6xxx; -public class LuceneDev6002_SpanComparisonSample +public class LuceneDev6003_6004_SpanComparisonSample { public void MyMethod() { ReadOnlySpan span = "Hello World".AsSpan(); - // Redundant StringComparison.Ordinal on span: triggers LuceneDev6002_1 (Warning). + // Redundant StringComparison.Ordinal on span: triggers LuceneDev6003 (Warning). int index1 = span.IndexOf("Hello".AsSpan(), StringComparison.Ordinal); int lastIndex1 = span.LastIndexOf("World".AsSpan(), StringComparison.Ordinal); bool starts1 = span.StartsWith("Hello".AsSpan(), StringComparison.Ordinal); bool ends1 = span.EndsWith("World".AsSpan(), StringComparison.Ordinal); - // Invalid comparison on span: triggers LuceneDev6002_2 (Error). + // Invalid comparison on span: triggers LuceneDev6004 (Error). int index2 = span.IndexOf("Hello".AsSpan(), StringComparison.CurrentCulture); int lastIndex2 = span.LastIndexOf("World".AsSpan(), StringComparison.CurrentCultureIgnoreCase); bool starts2 = span.StartsWith("Hello".AsSpan(), StringComparison.InvariantCulture); diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6005_SingleCharStringSample.cs similarity index 97% rename from src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs rename to src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6005_SingleCharStringSample.cs index 32f39c1..8d6c421 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6003_SingleCharStringSample.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev6xxx/LuceneDev6005_SingleCharStringSample.cs @@ -21,7 +21,7 @@ namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev6xxx; -public class LuceneDev6003_SingleCharStringSample +public class LuceneDev6005_SingleCharStringSample { public void MyMethod() { diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md index 538ae10..53ac42b 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md @@ -5,8 +5,8 @@ Rule ID | Category | Severity | Notes LuceneDev1007 | Design | Warning | Generic Dictionary indexer should not be used to retrieve values because it may throw KeyNotFoundException (value type value) LuceneDev1008 | Design | Warning | Generic Dictionary indexer should not be used to retrieve values because it may throw KeyNotFoundException (reference type value) LuceneDev6000 | Usage | Info | IDictionary indexer may be used to retrieve values, but must be checked for null before using the value -LuceneDev6001_1 | Usage | Error | Missing StringComparison argument in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; must use Ordinal/OrdinalIgnoreCase -LuceneDev6001_2 | Usage | Error | Invalid StringComparison value in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal/OrdinalIgnoreCase allowed -LuceneDev6002_1 | Usage | Warning | Redundant StringComparison.Ordinal argument in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; should be removed -LuceneDev6002_2 | Usage | Error | Invalid StringComparison value in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal or OrdinalIgnoreCase allowed -LuceneDev6003 | Usage | Info | Single-character string arguments should use the char overload of StartsWith/EndsWith/IndexOf/LastIndexOf instead of a string +LuceneDev6001 | Usage | Error | Missing StringComparison argument in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; must use Ordinal/OrdinalIgnoreCase +LuceneDev6002 | Usage | Error | Invalid StringComparison value in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal/OrdinalIgnoreCase allowed +LuceneDev6003 | Usage | Warning | Redundant StringComparison.Ordinal argument in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; should be removed +LuceneDev6004 | Usage | Error | Invalid StringComparison value in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal or OrdinalIgnoreCase allowed +LuceneDev6005 | Usage | Info | Single-character string arguments should use the char overload of StartsWith/EndsWith/IndexOf/LastIndexOf instead of a string diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonAnalyzer.cs similarity index 96% rename from src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs rename to src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonAnalyzer.cs index de995c4..e395d65 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_StringComparisonAnalyzer.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6001_6002_StringComparisonAnalyzer.cs @@ -27,7 +27,7 @@ namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx { [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class LuceneDev6001_StringComparisonAnalyzer : DiagnosticAnalyzer + public sealed class LuceneDev6001_6002_StringComparisonAnalyzer : DiagnosticAnalyzer { private static readonly ImmutableHashSet TargetMethodNames = ImmutableHashSet.Create("StartsWith", "EndsWith", "IndexOf", "LastIndexOf"); @@ -35,7 +35,7 @@ public sealed class LuceneDev6001_StringComparisonAnalyzer : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( Descriptors.LuceneDev6001_MissingStringComparison, - Descriptors.LuceneDev6001_InvalidStringComparison); + Descriptors.LuceneDev6002_InvalidStringComparison); public override void Initialize(AnalysisContext context) { @@ -82,10 +82,10 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) if (!hasStringComparisonArgForLiteral) { - // safe to convert to char (6003), so skip 6001 reporting + // Safe to convert to char (LuceneDev6005 handles it); skip 6001/6002 here. return; } - // else: has StringComparison -> do not skip; let 6001/6002 validate or codefix handle it + // Has StringComparison -> do not skip; 6001/6002 validation continues. } } @@ -134,7 +134,7 @@ static bool HasStringComparisonParameter(IMethodSymbol? m, INamedTypeSymbol scTy if (!isValidValue) { var diag = Diagnostic.Create( - Descriptors.LuceneDev6001_InvalidStringComparison, + Descriptors.LuceneDev6002_InvalidStringComparison, invalidArgLocation ?? memberAccess.Name.GetLocation(), methodName, comparisonValueName ?? "non-ordinal comparison"); @@ -174,7 +174,7 @@ static bool HasStringComparisonParameter(IMethodSymbol? m, INamedTypeSymbol scTy if (!isValidValue) { var diag = Diagnostic.Create( - Descriptors.LuceneDev6001_InvalidStringComparison, + Descriptors.LuceneDev6002_InvalidStringComparison, invalidArgLocation ?? memberAccess.Name.GetLocation(), methodName, comparisonValueName ?? "non-ordinal comparison"); diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_6004_SpanComparisonAnalyzer.cs similarity index 94% rename from src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs rename to src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_6004_SpanComparisonAnalyzer.cs index 333d17d..791c730 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6002_SpanComparisonAnalyzer.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_6004_SpanComparisonAnalyzer.cs @@ -27,15 +27,15 @@ namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx { [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class LuceneDev6002_SpanComparisonAnalyzer : DiagnosticAnalyzer + public sealed class LuceneDev6003_6004_SpanComparisonAnalyzer : DiagnosticAnalyzer { private static readonly ImmutableHashSet TargetMethodNames = ImmutableHashSet.Create("StartsWith", "EndsWith", "IndexOf", "LastIndexOf"); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( - Descriptors.LuceneDev6002_RedundantOrdinal, - Descriptors.LuceneDev6002_InvalidComparison); + Descriptors.LuceneDev6003_RedundantOrdinal, + Descriptors.LuceneDev6004_InvalidComparison); public override void Initialize(AnalysisContext context) { @@ -118,10 +118,10 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) if (!hasStringComparisonArgForLiteral) { - // safe to convert to char (6003), so skip 6001 reporting + // Safe to convert to char (LuceneDev6005 handles it); skip 6003/6004 here. return; } - // else: has StringComparison -> do not skip; let 6001/6002 validate or codefix handle it + // Has StringComparison -> do not skip; 6003/6004 validation continues. } } @@ -141,7 +141,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) { // Redundant - suggest removal (Warning) var diag = Diagnostic.Create( - Descriptors.LuceneDev6002_RedundantOrdinal, + Descriptors.LuceneDev6003_RedundantOrdinal, argLocation ?? memberAccess.Name.GetLocation(), methodName); ctx.ReportDiagnostic(diag); @@ -155,7 +155,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) { // Invalid comparison (CurrentCulture, InvariantCulture, etc.) - Error var diag = Diagnostic.Create( - Descriptors.LuceneDev6002_InvalidComparison, + Descriptors.LuceneDev6004_InvalidComparison, argLocation ?? memberAccess.Name.GetLocation(), methodName, comparisonValue ?? "non-ordinal comparison"); diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6005_SingleCharStringAnalyzer.cs similarity index 92% rename from src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs rename to src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6005_SingleCharStringAnalyzer.cs index ea345c2..60290b4 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6003_SingleCharStringAnalyzer.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev6xxx/LuceneDev6005_SingleCharStringAnalyzer.cs @@ -27,26 +27,22 @@ namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev6xxx { /// - /// Analyzer to detect single-character string literals (including escaped characters) - /// that should use char overload instead for better performance. - /// Applies to String, Span, and custom span-like types. - /// - /// Examples of violations: - /// - text.IndexOf("H") -> should use text.IndexOf('H') - /// - text.IndexOf("\n") -> should use text.IndexOf('\n') // Escaped newline - /// - text.IndexOf("\"") -> should use text.IndexOf('\"') // Escaped quote - /// - span.StartsWith("a") -> should use span.StartsWith('a') - /// - /// Severity: Info (suggestion only, not enforced) + /// Detects single-character string literals (including escaped characters) passed to + /// StartsWith/EndsWith/IndexOf/LastIndexOf where a char overload is available and + /// would avoid the unnecessary string allocation. + /// Applies to System.String, Span<char>, ReadOnlySpan<char>, and custom span-like + /// types. Note that Span<char>/ReadOnlySpan<char> only have char overloads for + /// IndexOf/LastIndexOf, not StartsWith/EndsWith. + /// Severity: Info. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] - public sealed class LuceneDev6003_SingleCharStringAnalyzer : DiagnosticAnalyzer + public sealed class LuceneDev6005_SingleCharStringAnalyzer : DiagnosticAnalyzer { private static readonly ImmutableHashSet TargetMethodNames = ImmutableHashSet.Create("StartsWith", "EndsWith", "IndexOf", "LastIndexOf"); public override ImmutableArray SupportedDiagnostics - => ImmutableArray.Create(Descriptors.LuceneDev6003_SingleCharStringAnalyzer); + => ImmutableArray.Create(Descriptors.LuceneDev6005_SingleCharString); public override void Initialize(AnalysisContext context) { @@ -147,7 +143,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx) // For example: "\"" shows as "\"" // "\n" shows as "\n" var diag = Diagnostic.Create( - Descriptors.LuceneDev6003_SingleCharStringAnalyzer, + Descriptors.LuceneDev6005_SingleCharString, literal.GetLocation(), methodName, literal.Token.Text); // Show the original escaped text in the message diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx index c50dba7..f17eceb 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx +++ b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx @@ -219,58 +219,58 @@ under the License. The format-able message the diagnostic displays. - - + + Missing StringComparison argument - + Calls to string comparison methods like StartsWith, EndsWith, IndexOf, and LastIndexOf must explicitly specify a StringComparison to enforce culture-invariant and consistent behavior. - + Call to '{0}' must specify a StringComparison argument. Use StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase. - - + + Invalid StringComparison argument - + Only StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase are allowed to ensure predictable and high-performance string comparisons. - + Call to '{0}' uses invalid StringComparison value '{1}'. Only Ordinal or OrdinalIgnoreCase are allowed. - - + + Redundant StringComparison.Ordinal argument - + Span-based overloads already perform ordinal comparison by default. Removing redundant arguments simplifies the code and improves clarity. - + Call to '{0}' on span overload already uses ordinal comparison. Remove the redundant StringComparison.Ordinal argument. - - + + Invalid StringComparison argument for span overload - + Span-based methods only support StringComparison.Ordinal and StringComparison.OrdinalIgnoreCase. Other values are not valid and should be removed or corrected. - + Call to '{0}' uses invalid StringComparison value '{1}'. Span overloads only support Ordinal or OrdinalIgnoreCase. - - + + Single-character string argument should use char overload - + Using char overloads instead of single-character string literals avoids unnecessary string allocations and improves performance. - + Call to '{0}' uses a single-character string literal. Use the char overload instead (e.g., 'x' instead of "x"). diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs index 781a79a..a6734f3 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev6xxx.cs @@ -1,4 +1,4 @@ -/* +/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -29,40 +29,42 @@ public static partial class Descriptors // and will report RS2002 warnings if it cannot read the DiagnosticDescriptor // instance through a field. - // 6001: Missing StringComparison argument + // 6001: Missing StringComparison argument on String overload public static readonly DiagnosticDescriptor LuceneDev6001_MissingStringComparison = Diagnostic( - "LuceneDev6001_1", + "LuceneDev6001", Usage, Error ); - // 6001: Invalid StringComparison value (not Ordinal or OrdinalIgnoreCase) - public static readonly DiagnosticDescriptor LuceneDev6001_InvalidStringComparison = + // 6002: Invalid StringComparison value on String overload (not Ordinal or OrdinalIgnoreCase) + public static readonly DiagnosticDescriptor LuceneDev6002_InvalidStringComparison = Diagnostic( - "LuceneDev6001_2", + "LuceneDev6002", Usage, Error ); - // 6002: Redundant Ordinal (StringComparison.Ordinal on span-like) - public static readonly DiagnosticDescriptor LuceneDev6002_RedundantOrdinal = + // 6003: Redundant StringComparison.Ordinal on span-like overload + public static readonly DiagnosticDescriptor LuceneDev6003_RedundantOrdinal = Diagnostic( - "LuceneDev6002_1", + "LuceneDev6003", Usage, Warning ); - // 6002: Invalid comparison on span (e.g., CurrentCulture, InvariantCulture) - public static readonly DiagnosticDescriptor LuceneDev6002_InvalidComparison = + // 6004: Invalid StringComparison value on span-like overload + public static readonly DiagnosticDescriptor LuceneDev6004_InvalidComparison = Diagnostic( - "LuceneDev6002_2", + "LuceneDev6004", Usage, Error ); - public static readonly DiagnosticDescriptor LuceneDev6003_SingleCharStringAnalyzer = + + // 6005: Single-character string argument should use the char overload + public static readonly DiagnosticDescriptor LuceneDev6005_SingleCharString = Diagnostic( - "LuceneDev6003", + "LuceneDev6005", Usage, Info ); diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_6002_StringComparisonCodeFixProvider.cs similarity index 86% rename from tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs rename to tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_6002_StringComparisonCodeFixProvider.cs index 7707685..ebb8584 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6001_6002_StringComparisonCodeFixProvider.cs @@ -27,7 +27,7 @@ namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.LuceneDev6xxx { [TestFixture] - public class TestLuceneDev6001_StringComparisonCodeFixProvider + public class TestLuceneDev6001_6002_StringComparisonCodeFixProvider { [Test] public async Task TestFix_IndexOf_MissingStringComparison() @@ -63,8 +63,8 @@ public void MyMethod() .WithLocation("/0/Test0.cs", line: 9, column: 26); var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -108,8 +108,8 @@ public void MyMethod() .WithLocation("/0/Test0.cs", line: 9, column: 28); var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -153,8 +153,8 @@ public void MyMethod() .WithLocation("/0/Test0.cs", line: 9, column: 26); var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -198,8 +198,8 @@ public void MyMethod() .WithLocation("/0/Test0.cs", line: 9, column: 26); var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -243,8 +243,8 @@ public void MyMethod() .WithLocation("/0/Test0.cs", line: 9, column: 26); var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -288,8 +288,8 @@ public void MyMethod() .WithLocation("/0/Test0.cs", line: 9, column: 26); var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -326,15 +326,15 @@ public void MyMethod() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) - .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithMessageFormat(Descriptors.LuceneDev6002_InvalidStringComparison.MessageFormat) .WithArguments("IndexOf", "CurrentCulture") .WithLocation("/0/Test0.cs", line: 9, column: 43); var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -371,15 +371,15 @@ public void MyMethod() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) - .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithMessageFormat(Descriptors.LuceneDev6002_InvalidStringComparison.MessageFormat) .WithArguments("StartsWith", "InvariantCulture") .WithLocation("/0/Test0.cs", line: 9, column: 48); var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -416,15 +416,15 @@ public void MyMethod() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) - .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithMessageFormat(Descriptors.LuceneDev6002_InvalidStringComparison.MessageFormat) .WithArguments("EndsWith", "CurrentCultureIgnoreCase") .WithLocation("/0/Test0.cs", line: 9, column: 44); var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -453,8 +453,8 @@ public void MyMethod() }"; var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = testCode, @@ -483,8 +483,8 @@ public void MyMethod() }"; var test = new InjectableCodeFixTest( - () => new LuceneDev6001_StringComparisonAnalyzer(), - () => new LuceneDev6001_StringComparisonCodeFixProvider()) + () => new LuceneDev6001_6002_StringComparisonAnalyzer(), + () => new LuceneDev6001_6002_StringComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = testCode, diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_6004_SpanComparisonCodeFixProvider.cs similarity index 87% rename from tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs rename to tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_6004_SpanComparisonCodeFixProvider.cs index 3729559..2173850 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_6004_SpanComparisonCodeFixProvider.cs @@ -27,7 +27,7 @@ namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.LuceneDev6xxx { [TestFixture] - public class TestLuceneDev6002_SpanComparisonCodeFixProvider + public class TestLuceneDev6003_6004_SpanComparisonCodeFixProvider { [Test] public async Task TestFix_RemoveRedundantOrdinal() @@ -56,14 +56,14 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6002_RedundantOrdinal) + var expected = new DiagnosticResult(Descriptors.LuceneDev6003_RedundantOrdinal) .WithSeverity(DiagnosticSeverity.Warning) .WithSpan(9, 42, 9, 66) .WithArguments("IndexOf"); var test = new InjectableCodeFixTest( - () => new LuceneDev6002_SpanComparisonAnalyzer(), - () => new LuceneDev6002_SpanComparisonCodeFixProvider()) + () => new LuceneDev6003_6004_SpanComparisonAnalyzer(), + () => new LuceneDev6003_6004_SpanComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -100,14 +100,14 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6004_InvalidComparison) .WithSeverity(DiagnosticSeverity.Error) .WithSpan(9, 42, 9, 73) .WithArguments("IndexOf", "CurrentCulture"); var test = new InjectableCodeFixTest( - () => new LuceneDev6002_SpanComparisonAnalyzer(), - () => new LuceneDev6002_SpanComparisonCodeFixProvider()) + () => new LuceneDev6003_6004_SpanComparisonAnalyzer(), + () => new LuceneDev6003_6004_SpanComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -148,14 +148,14 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6004_InvalidComparison) .WithSeverity(DiagnosticSeverity.Error) .WithSpan(9, 42, 9, 83) .WithArguments("IndexOf", "CurrentCultureIgnoreCase"); var test = new InjectableCodeFixTest( - () => new LuceneDev6002_SpanComparisonAnalyzer(), - () => new LuceneDev6002_SpanComparisonCodeFixProvider()) + () => new LuceneDev6003_6004_SpanComparisonAnalyzer(), + () => new LuceneDev6003_6004_SpanComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -196,14 +196,14 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6002_RedundantOrdinal) + var expected = new DiagnosticResult(Descriptors.LuceneDev6003_RedundantOrdinal) .WithSeverity(DiagnosticSeverity.Warning) .WithSpan(9, 39, 9, 63) .WithArguments("IndexOf"); var test = new InjectableCodeFixTest( - () => new LuceneDev6002_SpanComparisonAnalyzer(), - () => new LuceneDev6002_SpanComparisonCodeFixProvider()) + () => new LuceneDev6003_6004_SpanComparisonAnalyzer(), + () => new LuceneDev6003_6004_SpanComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -229,8 +229,8 @@ public void M() }"; var test = new InjectableCodeFixTest( - () => new LuceneDev6002_SpanComparisonAnalyzer(), - () => new LuceneDev6002_SpanComparisonCodeFixProvider()) + () => new LuceneDev6003_6004_SpanComparisonAnalyzer(), + () => new LuceneDev6003_6004_SpanComparisonCodeFixProvider()) { TestCode = testCode, FixedCode = testCode, diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6005_SingleCharStringCodeFixProvider.cs similarity index 87% rename from tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs rename to tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6005_SingleCharStringCodeFixProvider.cs index adb1452..6683cb6 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringCodeFixProvider.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev6xxx/TestLuceneDev6005_SingleCharStringCodeFixProvider.cs @@ -27,7 +27,7 @@ namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev6xxx { [TestFixture] - public class TestLuceneDev6003_SingleCharStringCodeFixProvider + public class TestLuceneDev6005_SingleCharStringCodeFixProvider { [Test] public async Task Fix_SingleCharacter_StringLiteral() @@ -56,14 +56,14 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + var expected = new DiagnosticResult(Descriptors.LuceneDev6005_SingleCharString) .WithSeverity(DiagnosticSeverity.Info) .WithArguments("IndexOf", "\"H\"") .WithSpan(9, 34, 9, 37); var test = new InjectableCodeFixTest( - () => new LuceneDev6003_SingleCharStringAnalyzer(), - () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + () => new LuceneDev6005_SingleCharStringAnalyzer(), + () => new LuceneDev6005_SingleCharStringCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -101,14 +101,14 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + var expected = new DiagnosticResult(Descriptors.LuceneDev6005_SingleCharString) .WithSeverity(DiagnosticSeverity.Info) .WithArguments("IndexOf", "\"\\\"\"") .WithSpan(9, 34, 9, 38); var test = new InjectableCodeFixTest( - () => new LuceneDev6003_SingleCharStringAnalyzer(), - () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + () => new LuceneDev6005_SingleCharStringAnalyzer(), + () => new LuceneDev6005_SingleCharStringCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -148,19 +148,19 @@ public void M() } }"; - var expected1 = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + var expected1 = new DiagnosticResult(Descriptors.LuceneDev6005_SingleCharString) .WithSeverity(DiagnosticSeverity.Info) .WithArguments("IndexOf", "\"H\"") .WithSpan(9, 31, 9, 34); - var expected2 = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + var expected2 = new DiagnosticResult(Descriptors.LuceneDev6005_SingleCharString) .WithSeverity(DiagnosticSeverity.Info) .WithArguments("IndexOf", "\"\\n\"") .WithSpan(10, 31, 10, 35); var test = new InjectableCodeFixTest( - () => new LuceneDev6003_SingleCharStringAnalyzer(), - () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + () => new LuceneDev6005_SingleCharStringAnalyzer(), + () => new LuceneDev6005_SingleCharStringCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -195,14 +195,14 @@ public void M(ReadOnlySpan span) } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + var expected = new DiagnosticResult(Descriptors.LuceneDev6005_SingleCharString) .WithSeverity(DiagnosticSeverity.Info) .WithArguments("IndexOf", "\"X\"") .WithSpan(8, 34, 8, 37); var test = new InjectableCodeFixTest( - () => new LuceneDev6003_SingleCharStringAnalyzer(), - () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + () => new LuceneDev6005_SingleCharStringAnalyzer(), + () => new LuceneDev6005_SingleCharStringCodeFixProvider()) { TestCode = testCode, FixedCode = fixedCode, @@ -230,8 +230,8 @@ public void M(ReadOnlySpan span) // This test expects NO diagnostic, ensuring the Analyzer correctly skips // ReadOnlySpan.StartsWith/EndsWith calls when the argument is a single-character string literal. var test = new InjectableCodeFixTest( - () => new LuceneDev6003_SingleCharStringAnalyzer(), - () => new LuceneDev6003_SingleCharStringCodeFixProvider()) + () => new LuceneDev6005_SingleCharStringAnalyzer(), + () => new LuceneDev6005_SingleCharStringCodeFixProvider()) { TestCode = testCode, FixedCode = testCode, // Fixed code is the same as test code diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_6002_StringComparisonAnalyzer.cs similarity index 91% rename from tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs rename to tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_6002_StringComparisonAnalyzer.cs index 0a0d063..fcc4b1d 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_StringComparisonAnalyzer.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6001_6002_StringComparisonAnalyzer.cs @@ -26,7 +26,7 @@ namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev6xxx { [TestFixture] - public class TestLuceneDev6001_StringComparisonAnalyzer + public class TestLuceneDev6001_6002_StringComparisonAnalyzer { [Test] public async Task Skips_SingleCharStringLiteral_Alone() @@ -43,7 +43,7 @@ public void M() } }"; - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, // Expect no diagnostics because 6001 should skip single-character string literal alone @@ -69,7 +69,7 @@ public void M() }"; // Change the test to use InjectableAnalyzerTest (no CodeFix) - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { } // Asserting NO diagnostics are expected @@ -99,7 +99,7 @@ public void M() .WithArguments("IndexOf") .WithLocation("/0/Test0.cs", line: 9, column: 26); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -129,7 +129,7 @@ public void M() .WithArguments("StartsWith") .WithLocation("/0/Test0.cs", line: 9, column: 28); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -159,7 +159,7 @@ public void M() .WithArguments("EndsWith") .WithLocation("/0/Test0.cs", line: 9, column: 26); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -189,7 +189,7 @@ public void M() .WithArguments("LastIndexOf") .WithLocation("/0/Test0.cs", line: 9, column: 26); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -219,7 +219,7 @@ public void M() .WithArguments("IndexOf") .WithLocation("/0/Test0.cs", line: 9, column: 26); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -249,7 +249,7 @@ public void M() .WithArguments("IndexOf") .WithLocation("/0/Test0.cs", line: 9, column: 26); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -273,13 +273,13 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) - .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithMessageFormat(Descriptors.LuceneDev6002_InvalidStringComparison.MessageFormat) .WithArguments("IndexOf", "CurrentCulture") .WithLocation("/0/Test0.cs", line: 9, column: 43); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -303,13 +303,13 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) - .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithMessageFormat(Descriptors.LuceneDev6002_InvalidStringComparison.MessageFormat) .WithArguments("StartsWith", "CurrentCultureIgnoreCase") .WithLocation("/0/Test0.cs", line: 9, column: 48); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -333,13 +333,13 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) - .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithMessageFormat(Descriptors.LuceneDev6002_InvalidStringComparison.MessageFormat) .WithArguments("EndsWith", "InvariantCulture") .WithLocation("/0/Test0.cs", line: 9, column: 44); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -363,13 +363,13 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) - .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithMessageFormat(Descriptors.LuceneDev6002_InvalidStringComparison.MessageFormat) .WithArguments("LastIndexOf", "InvariantCultureIgnoreCase") .WithLocation("/0/Test0.cs", line: 9, column: 47); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -396,7 +396,7 @@ public void M() } }"; - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { } // No diagnostics expected @@ -423,7 +423,7 @@ public void M() } }"; - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { } // No diagnostics expected @@ -456,9 +456,9 @@ public void M() .WithArguments("IndexOf") .WithLocation("/0/Test0.cs", line: 9, column: 27); - var expected2 = new DiagnosticResult(Descriptors.LuceneDev6001_InvalidStringComparison) + var expected2 = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidStringComparison) .WithSeverity(DiagnosticSeverity.Error) - .WithMessageFormat(Descriptors.LuceneDev6001_InvalidStringComparison.MessageFormat) + .WithMessageFormat(Descriptors.LuceneDev6002_InvalidStringComparison.MessageFormat) .WithArguments("IndexOf", "CurrentCulture") .WithLocation("/0/Test0.cs", line: 10, column: 44); @@ -468,7 +468,7 @@ public void M() .WithArguments("StartsWith") .WithLocation("/0/Test0.cs", line: 11, column: 28); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected1, expected2, expected3 } @@ -499,7 +499,7 @@ public void M() } }"; - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_StringComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6001_6002_StringComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { } // No diagnostics expected - not on System.String diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6003_6004_SpanComparisonAnalyzer.cs similarity index 91% rename from tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonAnalyzer.cs rename to tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6003_6004_SpanComparisonAnalyzer.cs index c851f94..2198d40 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6002_SpanComparisonAnalyzer.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6003_6004_SpanComparisonAnalyzer.cs @@ -26,7 +26,7 @@ namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev6xxx { [TestFixture] - public class TestLuceneDev6002_SpanComparisonAnalyzer + public class TestLuceneDev6003_6004_SpanComparisonAnalyzer { [Test] public async Task Detects_RedundantOrdinal_OnReadOnlySpan_IndexOf() @@ -43,12 +43,12 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6002_RedundantOrdinal) + var expected = new DiagnosticResult(Descriptors.LuceneDev6003_RedundantOrdinal) .WithSeverity(DiagnosticSeverity.Warning) .WithArguments("IndexOf") .WithSpan(9, 42, 9, 66); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -72,12 +72,12 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6002_RedundantOrdinal) + var expected = new DiagnosticResult(Descriptors.LuceneDev6003_RedundantOrdinal) .WithSeverity(DiagnosticSeverity.Warning) .WithArguments("StartsWith") .WithSpan(9, 47, 9, 71); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -102,12 +102,12 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6004_InvalidComparison) .WithSeverity(DiagnosticSeverity.Error) .WithArguments("IndexOf", "CurrentCulture") .WithSpan(9, 42, 9, 73); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -131,12 +131,12 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6004_InvalidComparison) .WithSeverity(DiagnosticSeverity.Error) .WithArguments("LastIndexOf", "CurrentCultureIgnoreCase") .WithSpan(9, 46, 9, 87); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -160,12 +160,12 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6004_InvalidComparison) .WithSeverity(DiagnosticSeverity.Error) .WithArguments("EndsWith", "InvariantCulture") .WithSpan(9, 43, 9, 76); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -189,12 +189,12 @@ public void M() } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + var expected = new DiagnosticResult(Descriptors.LuceneDev6004_InvalidComparison) .WithSeverity(DiagnosticSeverity.Error) .WithArguments("EndsWith", "InvariantCultureIgnoreCase") .WithSpan(9, 43, 9, 86); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -221,7 +221,7 @@ public void M() } }"; - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode }; @@ -245,7 +245,7 @@ public void M() } }"; - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode }; @@ -269,7 +269,7 @@ public void M() } }"; - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode }; @@ -292,7 +292,7 @@ public void M() } }"; - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { } // No diagnostics @@ -317,17 +317,17 @@ public void M() } }"; - var expected1 = new DiagnosticResult(Descriptors.LuceneDev6002_RedundantOrdinal) + var expected1 = new DiagnosticResult(Descriptors.LuceneDev6003_RedundantOrdinal) .WithSeverity(DiagnosticSeverity.Warning) .WithArguments("IndexOf") .WithSpan(9, 43, 9, 67); - var expected2 = new DiagnosticResult(Descriptors.LuceneDev6002_InvalidComparison) + var expected2 = new DiagnosticResult(Descriptors.LuceneDev6004_InvalidComparison) .WithSeverity(DiagnosticSeverity.Error) .WithArguments("LastIndexOf", "CurrentCulture") .WithSpan(10, 47, 10, 78); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6002_SpanComparisonAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_6004_SpanComparisonAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected1, expected2 } diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6005_SingleCharStringAnalyzer.cs similarity index 92% rename from tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringAnalyzer.cs rename to tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6005_SingleCharStringAnalyzer.cs index 4866e89..42f23f2 100644 --- a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6003_SingleCharStringAnalyzer.cs +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev6xxx/TestLuceneDev6005_SingleCharStringAnalyzer.cs @@ -27,7 +27,7 @@ namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev6xxx { [TestFixture] - public class TestLuceneDev6003_SingleCharStringAnalyzer + public class TestLuceneDev6005_SingleCharStringAnalyzer { [Test] public async Task Detects_SingleCharacter_StringLiteral() @@ -43,12 +43,12 @@ public void M() int index = text.IndexOf(""H""); } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + var expected = new DiagnosticResult(Descriptors.LuceneDev6005_SingleCharString) .WithSeverity(DiagnosticSeverity.Info) .WithSpan(9, 34, 9, 37) .WithArguments("IndexOf", "\"H\""); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_SingleCharStringAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6005_SingleCharStringAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -71,12 +71,12 @@ public void M() int index = text.IndexOf(""\""""); // Added missing semicolon } }"; - var expected = new DiagnosticResult(Descriptors.LuceneDev6003_SingleCharStringAnalyzer) + var expected = new DiagnosticResult(Descriptors.LuceneDev6005_SingleCharString) .WithSeverity(DiagnosticSeverity.Info) .WithSpan(9, 34, 9, 38) .WithArguments("IndexOf", "\"\\\"\""); - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_SingleCharStringAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6005_SingleCharStringAnalyzer()) { TestCode = testCode, ExpectedDiagnostics = { expected } @@ -100,7 +100,7 @@ public void M() } }"; - var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6003_SingleCharStringAnalyzer()) + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev6005_SingleCharStringAnalyzer()) { TestCode = testCode };