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 @@ -63,12 +63,12 @@ public bool IsCultureSensitiveOperation(IOperation operation, CultureSensitiveOp
if (_excludedMethods.Contains(invocation.TargetMethod))
return false;

if (invocation.HasArgumentOfType(FormatProviderSymbol, inherits: true))
return false;

var methodName = invocation.TargetMethod.Name;
if (methodName is "ToString")
{
if (invocation.HasArgumentOfType(FormatProviderSymbol, inherits: true))
return false;

// Try get the format. Most of ToString have only 1 string parameter to define the format
IOperation? format = null;
if (invocation.Arguments.Length > 0)
Expand Down Expand Up @@ -281,13 +281,25 @@ private bool IsCultureSensitiveType(ITypeSymbol? typeSymbol, CultureSensitiveOpt
if (typeSymbol.IsOrInheritFrom(NuGetVersioningSemanticVersionSymbol))
return false;

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

if (!IsCultureSensitiveTypeUsingAttribute(typeSymbol))
return false;

return true;

bool IsFormattableType(ITypeSymbol type)
{
if (type.Implements(SystemIFormattableSymbol))
return true;

// May have ToString(IFormatProvider) even if IFormattable is not implemented directly
if (type.GetAllMembers().OfType<IMethodSymbol>().Any(m => m is { Name: "ToString", IsStatic: false, ReturnType: { SpecialType: SpecialType.System_String }, Parameters: [var param1] } && param1.Type.IsOrInheritFrom(FormatProviderSymbol) && m.DeclaredAccessibility is Accessibility.Public))
return true;

return false;
}
}

private bool IsCultureSensitiveTypeUsingAttribute(ITypeSymbol typeSymbol)
Expand Down
146 changes: 98 additions & 48 deletions src/Meziantou.Analyzer/Internals/OverloadFinder.cs
Original file line number Diff line number Diff line change
@@ -1,77 +1,115 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Internals;

internal sealed class OverloadFinder(Compilation compilation)
{
private readonly ITypeSymbol? _obsoleteSymbol = compilation.GetBestTypeByMetadataName("System.ObsoleteAttribute");

public bool HasOverloadWithAdditionalParameterOfType(
IMethodSymbol methodSymbol,
params ITypeSymbol[] additionalParameterTypes)
private static ReadOnlySpan<OverloadParameterType> Wrap(ReadOnlySpan<ITypeSymbol?> types)
{
return FindOverloadWithAdditionalParameterOfType(methodSymbol, additionalParameterTypes) is not null;
var result = new OverloadParameterType[types.Length];
for (var i = 0; i < types.Length; i++)
{
result[i] = new OverloadParameterType(types[i]);
}

return result;
}

public bool HasOverloadWithAdditionalParameterOfType(
IMethodSymbol methodSymbol,
IOperation currentOperation,
params ITypeSymbol[] additionalParameterTypes)
private static ReadOnlySpan<OverloadParameterType> RemoveNulls(ReadOnlySpan<OverloadParameterType> types)
{
if (currentOperation.SemanticModel is null)
return false;
foreach (var type in types)
{
if (type.Symbol is not null)
continue;

var list = new List<OverloadParameterType>(types.Length - 1); // We know there is at least one null item
foreach (var t in types)
{
if (t.Symbol is not null)
{
list.Add(t);
}
}

return FindOverloadWithAdditionalParameterOfType(methodSymbol, syntaxNode: currentOperation.Syntax, includeObsoleteMethods: false, allowOptionalParameters: false, additionalParameterTypes) is not null;
return list.ToArray();
}

return types;
}

private IMethodSymbol? FindOverloadWithAdditionalParameterOfType(
IMethodSymbol methodSymbol,
params ITypeSymbol[] additionalParameterTypes)
public bool HasOverloadWithAdditionalParameterOfType(IObjectCreationOperation operation, OverloadOptions options, ReadOnlySpan<ITypeSymbol?> additionalParameterTypes)
{
return FindOverloadWithAdditionalParameterOfType(methodSymbol, includeObsoleteMethods: false, allowOptionalParameters: false, additionalParameterTypes);
return FindOverloadWithAdditionalParameterOfType(operation, options, additionalParameterTypes) is not null;
}

public IMethodSymbol? FindOverloadWithAdditionalParameterOfType(
IMethodSymbol methodSymbol,
bool includeObsoleteMethods,
bool allowOptionalParameters,
params ITypeSymbol[] additionalParameterTypes)
public bool HasOverloadWithAdditionalParameterOfType(IInvocationOperation operation, OverloadOptions options, ReadOnlySpan<ITypeSymbol?> additionalParameterTypes)
{
return FindOverloadWithAdditionalParameterOfType(methodSymbol, syntaxNode: null, includeObsoleteMethods, allowOptionalParameters, additionalParameterTypes);
return FindOverloadWithAdditionalParameterOfType(operation, options, additionalParameterTypes) is not null;
}

public IMethodSymbol? FindOverloadWithAdditionalParameterOfType(
IMethodSymbol methodSymbol,
IOperation operation,
bool includeObsoleteMethods,
bool allowOptionalParameters,
params ITypeSymbol[] additionalParameterTypes)
public bool HasOverloadWithAdditionalParameterOfType(IInvocationOperation operation, OverloadOptions options, ReadOnlySpan<OverloadParameterType> additionalParameterTypes)
{
if (operation.SemanticModel is null)
return null;
return FindOverloadWithAdditionalParameterOfType(operation, options, additionalParameterTypes) is not null;
}

return FindOverloadWithAdditionalParameterOfType(methodSymbol, operation.Syntax, includeObsoleteMethods, allowOptionalParameters, additionalParameterTypes);
public bool HasOverloadWithAdditionalParameterOfType(IMethodSymbol methodSymbol, OverloadOptions options, ReadOnlySpan<ITypeSymbol?> additionalParameterTypes)
{
return FindOverloadWithAdditionalParameterOfType(methodSymbol, options, additionalParameterTypes) is not null;
}

public IMethodSymbol? FindOverloadWithAdditionalParameterOfType(
IMethodSymbol methodSymbol,
SyntaxNode? syntaxNode,
bool includeObsoleteMethods,
bool allowOptionalParameters,
params ITypeSymbol[] additionalParameterTypes)
public bool HasOverloadWithAdditionalParameterOfType(IMethodSymbol methodSymbol, OverloadOptions options, ReadOnlySpan<OverloadParameterType> additionalParameterTypes)
{
if (additionalParameterTypes is null)
return FindOverloadWithAdditionalParameterOfType(methodSymbol, options, additionalParameterTypes) is not null;
}

public IMethodSymbol? FindOverloadWithAdditionalParameterOfType(IInvocationOperation operation, OverloadOptions options, ReadOnlySpan<ITypeSymbol?> additionalParameterTypes)
{
if (options.SyntaxNode is null)
{
options = options with { SyntaxNode = operation.Syntax };
}

return FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, options, Wrap(additionalParameterTypes));
}

public IMethodSymbol? FindOverloadWithAdditionalParameterOfType(IInvocationOperation operation, OverloadOptions options, ReadOnlySpan<OverloadParameterType> additionalParameterTypes)
{
if (options.SyntaxNode is null)
{
options = options with { SyntaxNode = operation.Syntax };
}

return FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, options, additionalParameterTypes);
}

public IMethodSymbol? FindOverloadWithAdditionalParameterOfType(IObjectCreationOperation operation, OverloadOptions options, ReadOnlySpan<ITypeSymbol?> additionalParameterTypes)
{
if (operation.Constructor is null)
return null;

additionalParameterTypes = [.. additionalParameterTypes.Where(type => type is not null)];
if (additionalParameterTypes.Length == 0)
return FindOverloadWithAdditionalParameterOfType(operation.Constructor, options, Wrap(additionalParameterTypes));
}

public IMethodSymbol? FindOverloadWithAdditionalParameterOfType(IMethodSymbol methodSymbol, OverloadOptions options, ReadOnlySpan<ITypeSymbol?> additionalParameterTypes)
{
return FindOverloadWithAdditionalParameterOfType(methodSymbol, options, Wrap(additionalParameterTypes));
}

public IMethodSymbol? FindOverloadWithAdditionalParameterOfType(IMethodSymbol methodSymbol, OverloadOptions options, ReadOnlySpan<OverloadParameterType> additionalParameterTypes)
{
additionalParameterTypes = RemoveNulls(additionalParameterTypes);
if (additionalParameterTypes.IsEmpty)
return null;

ImmutableArray<ISymbol> members;
if (syntaxNode is not null)
if (options.SyntaxNode is not null)
{
var semanticModel = compilation.GetSemanticModel(syntaxNode.SyntaxTree);
members = semanticModel.LookupSymbols(syntaxNode.GetLocation().SourceSpan.End, methodSymbol.ContainingType, methodSymbol.Name, includeReducedExtensionMethods: true);
var semanticModel = compilation.GetSemanticModel(options.SyntaxNode.SyntaxTree);
members = semanticModel.LookupSymbols(options.SyntaxNode.GetLocation().SourceSpan.End, methodSymbol.ContainingType, methodSymbol.Name, includeReducedExtensionMethods: true);
}
else
{
Expand All @@ -82,17 +120,22 @@ public bool HasOverloadWithAdditionalParameterOfType(
{
if (member is IMethodSymbol method)
{
if (!includeObsoleteMethods && IsObsolete(method))
if (!options.IncludeObsoleteMembers && IsObsolete(method))
continue;

if (HasSimilarParameters(methodSymbol, method, allowOptionalParameters, additionalParameterTypes))
if (HasSimilarParameters(methodSymbol, method, options.AllowOptionalParameters, additionalParameterTypes))
return method;
}
}

return null;
}

public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, bool allowOptionalParameters, params ReadOnlySpan<ITypeSymbol?> additionalParameterTypes)
{
return HasSimilarParameters(method, otherMethod, allowOptionalParameters, Wrap(additionalParameterTypes));
}

/// <summary>
/// Methods are similar if:
/// <list type="bullet">
Expand All @@ -102,7 +145,7 @@ public bool HasOverloadWithAdditionalParameterOfType(
/// <item>If <paramref name="allowOptionalParameters"/>, <paramref name="otherMethod"/> can have more parameters if they are optional</item>
/// </list>
/// </summary>
public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, bool allowOptionalParameters, params ITypeSymbol[] additionalParameterTypes)
public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, bool allowOptionalParameters, params ReadOnlySpan<OverloadParameterType> additionalParameterTypes)
{
if (method.IsEqualTo(otherMethod))
return false;
Expand Down Expand Up @@ -135,13 +178,13 @@ public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol othe
break;

var additionalParameter = additionalParameterTypes[additionalParameterIndex];
if (methodParameter.Type.IsEqualTo(additionalParameter))
if (IsEqualTo(methodParameter.Type, additionalParameter))
{
i++;
continue;
}

if (otherMethodParameter.Type.IsEqualTo(additionalParameter))
if (IsEqualTo(otherMethodParameter.Type, additionalParameter))
{
j++;
continue;
Expand Down Expand Up @@ -181,7 +224,7 @@ public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol othe
var found = false;
for (var i = 0; i < otherMethodParameters.Length; i++)
{
if (otherMethodParameters[i].Type.IsEqualTo(paramType))
if (IsEqualTo(otherMethodParameters[i].Type, paramType))
{
otherMethodParameters = otherMethodParameters.RemoveAt(i);
found = true;
Expand All @@ -204,6 +247,13 @@ public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol othe

return false;
}

static bool IsEqualTo(ITypeSymbol left, OverloadParameterType right)
{
return right.AllowInherits
? left.IsOrInheritFrom(right.Symbol)
: left.IsEqualTo(right.Symbol);
}
}

private bool IsObsolete(IMethodSymbol methodSymbol)
Expand Down
5 changes: 5 additions & 0 deletions src/Meziantou.Analyzer/Internals/OverloadOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Microsoft.CodeAnalysis;

namespace Meziantou.Analyzer.Internals;

internal record struct OverloadOptions(bool IncludeObsoleteMembers = false, bool AllowOptionalParameters = false, bool IncludeExtensionsMethods = false, SyntaxNode? SyntaxNode = null);
5 changes: 5 additions & 0 deletions src/Meziantou.Analyzer/Internals/OverloadParameterType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Microsoft.CodeAnalysis;

namespace Meziantou.Analyzer.Internals;

internal record struct OverloadParameterType(ITypeSymbol? Symbol, bool AllowInherits = false);
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,10 @@ private bool IsPotentialMember(IInvocationOperation operation, IMethodSymbol met
if (methodSymbol.HasAttribute(ObsoleteAttributeSymbol))
return false;

if (OverloadFinder.HasSimilarParameters(method, methodSymbol, allowOptionalParameters: false))
if (OverloadFinder.HasSimilarParameters(method, methodSymbol, allowOptionalParameters: false, default(ReadOnlySpan<OverloadParameterType>)))
return true;

if (CancellationTokenSymbol is not null && OverloadFinder.HasSimilarParameters(method, methodSymbol, allowOptionalParameters: false, CancellationTokenSymbol))
if (CancellationTokenSymbol is not null && OverloadFinder.HasSimilarParameters(method, methodSymbol, allowOptionalParameters: false, [CancellationTokenSymbol]))
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ private static void AnalyzeThrow(OperationAnalysisContext context, OverloadFinde
var argument = objectCreationOperation.Arguments.FirstOrDefault(arg => IsPotentialParameter(arg?.Parameter, exceptionSymbol));
if (argument is null)
{
if (overloadFinder.HasOverloadWithAdditionalParameterOfType(objectCreationOperation.Constructor, exceptionSymbol))
if (overloadFinder.HasOverloadWithAdditionalParameterOfType(objectCreationOperation, options: default, [exceptionSymbol]))
{
context.ReportDiagnostic(Rule, objectCreationOperation);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ private bool HasAnOverloadWithCancellationToken(OperationAnalysisContext context
return true;

var allowOptionalParameters = context.Options.GetConfigurationValue(operation, "MA0032.allowOverloadsWithOptionalParameters", defaultValue: false);
var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, operation, includeObsoleteMethods: false, allowOptionalParameters, CancellationTokenSymbol);
var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions(AllowOptionalParameters: allowOptionalParameters), [CancellationTokenSymbol]);
if (overload is not null)
{
for (var i = 0; i < overload.Parameters.Length; i++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private bool HasAnOverloadWithTimeProvider(IInvocationOperation operation, [NotN
if (IsArgumentImplicitlyDeclared(operation, TimeProviderSymbol, out parameterInfo))
return true;

var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, operation, includeObsoleteMethods: false, allowOptionalParameters: true, TimeProviderSymbol);
var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions(IncludeObsoleteMembers: false, AllowOptionalParameters: true), [TimeProviderSymbol]);
if (overload is not null)
{
for (var i = 0; i < overload.Parameters.Length; i++)
Expand Down
Loading