Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace Meziantou.Analyzer.Internals;

internal sealed class CultureSensitiveFormattingContext(Compilation compilation)
{
private readonly HashSet<ISymbol> _excludedMethods = CreateExcludedMethods(compilation);

public INamedTypeSymbol? CultureInsensitiveTypeAttributeSymbol { get; } = compilation.GetBestTypeByMetadataName("Meziantou.Analyzer.Annotations.CultureInsensitiveTypeAttribute");
public INamedTypeSymbol? FormatProviderSymbol { get; } = compilation.GetBestTypeByMetadataName("System.IFormatProvider");
public INamedTypeSymbol? CultureInfoSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Globalization.CultureInfo");
Expand All @@ -24,6 +26,26 @@ internal sealed class CultureSensitiveFormattingContext(Compilation compilation)
public INamedTypeSymbol? SystemIFormattableSymbol { get; } = compilation.GetBestTypeByMetadataName("System.IFormattable");
public INamedTypeSymbol? SystemWindowsFontStretchSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Windows.FontStretch");
public INamedTypeSymbol? SystemWindowsMediaBrushSymbol { get; } = compilation.GetBestTypeByMetadataName("System.Windows.Media.Brush");
public INamedTypeSymbol? NuGetVersioningSemanticVersionSymbol { get; } = compilation.GetBestTypeByMetadataName("NuGet.Versioning.SemanticVersion");

private static HashSet<ISymbol> CreateExcludedMethods(Compilation compilation)
{
var result = new HashSet<ISymbol>(SymbolEqualityComparer.Default);
AddDocumentationId(result, compilation, "M:System.Convert.ToChar(System.String)");
AddDocumentationId(result, compilation, "M:System.Convert.ToChar(System.Object)");
AddDocumentationId(result, compilation, "M:System.Convert.ToBoolean(System.String)");
AddDocumentationId(result, compilation, "M:System.Convert.ToBoolean(System.Object)");
return result;

static void AddDocumentationId(HashSet<ISymbol> result, Compilation compilation, string id)
{
foreach (var item in DocumentationCommentId.GetSymbolsForDeclarationId(id, compilation))
{
result.Add(item);
}
}
}


private static bool MustUnwrapNullableOfT(CultureSensitiveOptions options)
{
Expand All @@ -40,6 +62,9 @@ public bool IsCultureSensitiveOperation(IOperation operation, CultureSensitiveOp

if (operation is IInvocationOperation invocation)
{
if (_excludedMethods.Contains(invocation.TargetMethod))
return false;

var methodName = invocation.TargetMethod.Name;
if (methodName is "ToString")
{
Expand All @@ -49,7 +74,7 @@ public bool IsCultureSensitiveOperation(IOperation operation, CultureSensitiveOp
{
foreach (var arg in invocation.Arguments)
{
if (arg.Value is { ConstantValue: { HasValue: true, Value: string } })
if (arg.Value is { ConstantValue: { HasValue: true, Value: string } } or IConversionOperation { Type.SpecialType: SpecialType.System_String, ConstantValue: { HasValue: true, Value: null } })
{
if (format is not null)
{
Expand Down Expand Up @@ -252,25 +277,31 @@ private bool IsCultureSensitiveType(ITypeSymbol? typeSymbol, CultureSensitiveOpt
if (typeSymbol.IsOrInheritFrom(SystemWindowsMediaBrushSymbol))
return false;

if (typeSymbol.IsOrInheritFrom(NuGetVersioningSemanticVersionSymbol))
return false;

if (!typeSymbol.Implements(SystemIFormattableSymbol))
return false;

if (!IsCultureSensitiveTypeUsingAttribute(typeSymbol, format: null))
if (!IsCultureSensitiveTypeUsingAttribute(typeSymbol, hasFormat: false, format: null))
return false;

return true;
}

private bool IsCultureSensitiveTypeUsingAttribute(ITypeSymbol typeSymbol, string? format)
private bool IsCultureSensitiveTypeUsingAttribute(ITypeSymbol typeSymbol, bool hasFormat, string? format)
{
var attributes = typeSymbol.GetAttributes(CultureInsensitiveTypeAttributeSymbol);
foreach (var attr in attributes)
{
if (attr.ConstructorArguments.IsEmpty)
return false; // no format is set, so the type is culture insensitive

var attrFormat = attr.ConstructorArguments[0].Value;
if (attrFormat is null || (attrFormat is string attrFormatValue && (string.IsNullOrEmpty(attrFormatValue) || attrFormatValue == format)))
if (!hasFormat)
continue;

var attrFormat = attr.ConstructorArguments[0].Value as string;
if (attrFormat == format)
return false; // no format is set, so the type is culture insensitive
}

Expand All @@ -284,8 +315,11 @@ private bool IsCultureSensitiveTypeUsingAttribute(ITypeSymbol typeSymbol, string
if (attribute.ConstructorArguments.Length == 1)
return false;

var attrFormat = attribute.ConstructorArguments[1].Value;
if (attrFormat is null || (attrFormat is string attrFormatValue && (string.IsNullOrEmpty(attrFormatValue) || attrFormatValue == format)))
if (!hasFormat)
continue;

var attrFormat = attribute.ConstructorArguments[1].Value as string;
if (attrFormat == format)
return false; // no format is set, so the type is culture insensitive
}
}
Expand All @@ -298,6 +332,7 @@ private bool IsCultureSensitiveType(ITypeSymbol? symbol, IOperation? format, IOp
if (!IsCultureSensitiveType(symbol, options))
return false;

var hasFormatString = format is { ConstantValue.HasValue: true };
var formatString = format?.ConstantValue.Value as string;

if (instance is not null)
Expand All @@ -320,7 +355,7 @@ private bool IsCultureSensitiveType(ITypeSymbol? symbol, IOperation? format, IOp
return false;
}

if (symbol is not null && !IsCultureSensitiveTypeUsingAttribute(symbol, formatString))
if (symbol is not null && !IsCultureSensitiveTypeUsingAttribute(symbol, hasFormatString, formatString))
return false;

return true;
Expand Down
38 changes: 12 additions & 26 deletions src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,6 @@ private sealed class AnalyzerContext(Compilation compilation)
{
private readonly CultureSensitiveFormattingContext _cultureSensitiveContext = new(compilation);
private readonly OverloadFinder _overloadFinder = new(compilation);
private readonly HashSet<ISymbol> _excludedMethods = CreateExcludedMethods(compilation);

private static HashSet<ISymbol> CreateExcludedMethods(Compilation compilation)
{
var result = new HashSet<ISymbol>(SymbolEqualityComparer.Default);
AddDocumentationId(result, compilation, "M:System.Convert.ToChar(System.String)");
AddDocumentationId(result, compilation, "M:System.Convert.ToChar(System.Object)");
AddDocumentationId(result, compilation, "M:System.Convert.ToBoolean(System.String)");
AddDocumentationId(result, compilation, "M:System.Convert.ToBoolean(System.Object)");
return result;

static void AddDocumentationId(HashSet<ISymbol> result, Compilation compilation, string id)
{
foreach (var item in DocumentationCommentId.GetSymbolsForDeclarationId(id, compilation))
{
result.Add(item);
}
}
}

public void AnalyzeInvocation(OperationAnalysisContext context)
{
Expand All @@ -65,7 +46,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context)

if (IsExcludedMethod(context, operation))
return;

var options = MustUnwrapNullableTypes(context, operation) ? CultureSensitiveOptions.UnwrapNullableOfT : CultureSensitiveOptions.None;
if (!_cultureSensitiveContext.IsCultureSensitiveOperation(operation, options))
return;
Expand All @@ -81,7 +62,11 @@ public void AnalyzeInvocation(OperationAnalysisContext context)
var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, operation, includeObsoleteMethods: false, allowOptionalParameters: false, _cultureSensitiveContext.FormatProviderSymbol);
if (overload is not null)
{
context.ReportDiagnostic(Rule, operation, operation.TargetMethod.Name, _cultureSensitiveContext.FormatProviderSymbol.ToDisplayString());
if (_cultureSensitiveContext.IsCultureSensitiveOperation(operation, CultureSensitiveOptions.None))
{
context.ReportDiagnostic(Rule, operation, operation.TargetMethod.Name, _cultureSensitiveContext.FormatProviderSymbol.ToDisplayString());
}

return;
}

Expand All @@ -108,17 +93,18 @@ public void AnalyzeInvocation(OperationAnalysisContext context)
var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, includeObsoleteMethods: false, allowOptionalParameters: false, _cultureSensitiveContext.CultureInfoSymbol);
if (overload is not null)
{
context.ReportDiagnostic(Rule, operation, operation.TargetMethod.Name, _cultureSensitiveContext.CultureInfoSymbol.ToDisplayString());
if (_cultureSensitiveContext.IsCultureSensitiveOperation(operation, CultureSensitiveOptions.None))
{
context.ReportDiagnostic(Rule, operation, operation.TargetMethod.Name, _cultureSensitiveContext.CultureInfoSymbol.ToDisplayString());
}

return;
}
}
}

private bool IsExcludedMethod(OperationAnalysisContext context, IInvocationOperation operation)
private static bool IsExcludedMethod(OperationAnalysisContext context, IInvocationOperation operation)
{
if (_excludedMethods.Contains(operation.TargetMethod))
return true;

// ToString show culture-sensitive data by default
if (operation?.GetContainingMethod(context.CancellationToken)?.Name == "ToString")
{
Expand Down
Loading