blob: e395d6586058fc39f3d0f8ac2cb444a1751cd1b1 [file]
/*
* 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_6002_StringComparisonAnalyzer : DiagnosticAnalyzer
{
private static readonly ImmutableHashSet<string> TargetMethodNames =
ImmutableHashSet.Create("StartsWith", "EndsWith", "IndexOf", "LastIndexOf");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(
Descriptors.LuceneDev6001_MissingStringComparison,
Descriptors.LuceneDev6002_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;
// 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 (LuceneDev6005 handles it); skip 6001/6002 here.
return;
}
// Has StringComparison -> do not skip; 6001/6002 validation continues.
}
}
// Get symbol info
var symbolInfo = semantic.GetSymbolInfo(memberAccess);
var methodSymbol = symbolInfo.Symbol as IMethodSymbol;
var candidateSymbols = symbolInfo.CandidateSymbols.OfType<IMethodSymbol>().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, comparisonValueName) =
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.LuceneDev6002_InvalidStringComparison,
invalidArgLocation ?? memberAccess.Name.GetLocation(),
methodName,
comparisonValueName ?? "non-ordinal comparison");
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.LuceneDev6002_InvalidStringComparison,
invalidArgLocation ?? memberAccess.Name.GetLocation(),
methodName,
comparisonValueName ?? "non-ordinal comparison");
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, string? valueName) CheckStringComparisonArgument(
InvocationExpressionSyntax invocation,
SemanticModel semantic,
INamedTypeSymbol stringComparisonType)
{
foreach (var arg in invocation.ArgumentList.Arguments)
{
var argType = semantic.GetTypeInfo(arg.Expression).Type;
bool typeMatches = argType != null && SymbolEqualityComparer.Default.Equals(argType, stringComparisonType);
var argSymbol = semantic.GetSymbolInfo(arg.Expression).Symbol as IFieldSymbol;
bool symbolMatches = argSymbol != null && SymbolEqualityComparer.Default.Equals(argSymbol.ContainingType, stringComparisonType);
if (typeMatches || symbolMatches)
{
bool isValid = IsValidStringComparisonValue(semantic, arg.Expression, stringComparisonType);
string? name = argSymbol?.Name ?? GetStringComparisonNameFromConstant(semantic, arg.Expression);
return (true, isValid, arg.Expression.GetLocation(), name);
}
}
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(
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;
}
}
}