diff --git a/src/Meziantou.Analyzer.CodeFixers/Directory.Build.props b/src/Meziantou.Analyzer.CodeFixers/Directory.Build.props
index c7ab8d9f1..b5b6c71ac 100644
--- a/src/Meziantou.Analyzer.CodeFixers/Directory.Build.props
+++ b/src/Meziantou.Analyzer.CodeFixers/Directory.Build.props
@@ -38,6 +38,7 @@
+
diff --git a/src/Meziantou.Analyzer/Internals/OverloadFinder.cs b/src/Meziantou.Analyzer/Internals/OverloadFinder.cs
index 6eba982fa..7986bf5f7 100644
--- a/src/Meziantou.Analyzer/Internals/OverloadFinder.cs
+++ b/src/Meziantou.Analyzer/Internals/OverloadFinder.cs
@@ -671,10 +671,10 @@ static void AddSymbols(IEnumerable symbols, List results, Hash
var semanticModel = compilation.GetSemanticModel(options.SyntaxNode.SyntaxTree);
var position = options.SyntaxNode.GetLocation().SourceSpan.End;
- AddSymbols(semanticModel.LookupSymbols(position, methodSymbol.ContainingType, methodName, includeReducedExtensionMethods: true), results, knownSymbols);
+ AddSymbols(LookupSymbols(semanticModel, options, position, methodSymbol.ContainingType, methodName, includeReducedExtensionMethods: true), results, knownSymbols);
if (reducedReceiverType is not null)
{
- AddSymbols(semanticModel.LookupSymbols(position, reducedReceiverType, methodName, includeReducedExtensionMethods: false), results, knownSymbols);
+ AddSymbols(LookupSymbols(semanticModel, options, position, reducedReceiverType, methodName, includeReducedExtensionMethods: false), results, knownSymbols);
AddSymbols(reducedReceiverType.GetMembers(methodName), results, knownSymbols);
}
@@ -695,6 +695,14 @@ static void AddSymbols(IEnumerable symbols, List results, Hash
return results;
}
+ private static ImmutableArray LookupSymbols(SemanticModel semanticModel, OverloadOptions options, int position, ITypeSymbol container, string name, bool includeReducedExtensionMethods)
+ {
+ if (options.LookupCache is null)
+ return semanticModel.LookupSymbols(position, container, name, includeReducedExtensionMethods);
+
+ return options.LookupCache.LookupSymbols(semanticModel, options.SyntaxNode!, position, container, name, includeReducedExtensionMethods);
+ }
+
///
/// Adds the extension methods that apply to the receiver of , are accessible at
/// , and are not in scope because their namespace is not imported. They are added after the
diff --git a/src/Meziantou.Analyzer/Internals/OverloadLookupCache.cs b/src/Meziantou.Analyzer/Internals/OverloadLookupCache.cs
new file mode 100644
index 000000000..2ad777efc
--- /dev/null
+++ b/src/Meziantou.Analyzer/Internals/OverloadLookupCache.cs
@@ -0,0 +1,57 @@
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Meziantou.Analyzer.Internals;
+
+///
+/// Caches the member lookups done by . A cache is meant to be created when the analysis of a named
+/// type starts (RegisterSymbolStartAction) and shared by the actions of that type, so it is released with the type.
+/// It is thread-safe because the members of a type can be analyzed concurrently.
+///
+internal sealed class OverloadLookupCache
+{
+ private readonly ConcurrentDictionary> _cache = new(LookupKeyComparer.Instance);
+
+ // A member lookup only depends on the file and enclosing type declaration (accessibility, imported namespaces), the container and the name
+ public ImmutableArray LookupSymbols(SemanticModel semanticModel, SyntaxNode node, int position, ITypeSymbol container, string name, bool includeReducedExtensionMethods)
+ {
+ var scopeStart = node.FirstAncestorOrSelf()?.SpanStart ?? -1;
+ var key = new LookupKey(node.SyntaxTree, scopeStart, container, name, includeReducedExtensionMethods);
+ if (!_cache.TryGetValue(key, out var symbols))
+ {
+ symbols = semanticModel.LookupSymbols(position, container, name, includeReducedExtensionMethods);
+ _ = _cache.TryAdd(key, symbols);
+ }
+
+ return symbols;
+ }
+
+ private readonly record struct LookupKey(SyntaxTree Tree, int ScopeStart, ITypeSymbol Container, string Name, bool IncludeReducedExtensionMethods);
+
+ private sealed class LookupKeyComparer : IEqualityComparer
+ {
+ public static LookupKeyComparer Instance { get; } = new();
+
+ public bool Equals(LookupKey x, LookupKey y)
+ {
+ return ReferenceEquals(x.Tree, y.Tree)
+ && x.ScopeStart == y.ScopeStart
+ && SymbolEqualityComparer.Default.Equals(x.Container, y.Container)
+ && string.Equals(x.Name, y.Name, StringComparison.Ordinal)
+ && x.IncludeReducedExtensionMethods == y.IncludeReducedExtensionMethods;
+ }
+
+ public int GetHashCode(LookupKey obj)
+ {
+ unchecked
+ {
+ var hashCode = RuntimeHelpers.GetHashCode(obj.Tree);
+ hashCode = (hashCode * 397) ^ obj.ScopeStart;
+ hashCode = (hashCode * 397) ^ SymbolEqualityComparer.Default.GetHashCode(obj.Container);
+ hashCode = (hashCode * 397) ^ StringComparer.Ordinal.GetHashCode(obj.Name);
+ return (hashCode * 397) ^ (obj.IncludeReducedExtensionMethods ? 1 : 0);
+ }
+ }
+ }
+}
diff --git a/src/Meziantou.Analyzer/Internals/OverloadOptions.cs b/src/Meziantou.Analyzer/Internals/OverloadOptions.cs
index fdf4e76a6..965b41ed2 100644
--- a/src/Meziantou.Analyzer/Internals/OverloadOptions.cs
+++ b/src/Meziantou.Analyzer/Internals/OverloadOptions.cs
@@ -12,4 +12,5 @@ internal record struct OverloadOptions(
bool AllowInterfaceConversions = true,
bool AllowBaseTypeConversions = true,
Func? ShouldCheckMethod = null,
- bool IncludeExtensionMethodsFromNotImportedNamespaces = false);
+ bool IncludeExtensionMethodsFromNotImportedNamespaces = false,
+ OverloadLookupCache? LookupCache = null);
diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs
index 0742ddc2b..0b651d05a 100755
--- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs
+++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs
@@ -50,7 +50,11 @@ public override void Initialize(AnalysisContext context)
var analyzerContext = new Context(ctx.Compilation);
if (analyzerContext.IsValid)
{
- ctx.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
+ ctx.RegisterSymbolStartAction(symbolContext =>
+ {
+ var lookupCache = new OverloadLookupCache();
+ symbolContext.RegisterOperationAction(operationContext => analyzerContext.AnalyzeInvocation(operationContext, lookupCache), OperationKind.Invocation);
+ }, SymbolKind.NamedType);
ctx.RegisterOperationAction(analyzerContext.AnalyzePropertyReference, OperationKind.PropertyReference);
ctx.RegisterOperationAction(analyzerContext.AnalyzeUsing, OperationKind.Using);
ctx.RegisterOperationAction(analyzerContext.AnalyzeUsingDeclaration, OperationKind.UsingDeclaration);
@@ -205,7 +209,7 @@ public Context(Compilation compilation)
public bool IsValid => TaskSymbol is not null && TaskOfTSymbol is not null && TaskAwaiterSymbol is not null;
- internal void AnalyzeInvocation(OperationAnalysisContext context)
+ internal void AnalyzeInvocation(OperationAnalysisContext context, OverloadLookupCache lookupCache)
{
var operation = (IInvocationOperation)context.Operation;
@@ -240,7 +244,7 @@ internal void AnalyzeInvocation(OperationAnalysisContext context)
var sqliteSpecialCasesEnabled = IsSqliteSpecialCasesEnabled(context, operation);
var includeExtensionMethodsFromNotImportedNamespaces = context.Options.GetConfigurationValue(operation, IncludeExtensionMethodsFromNotImportedNamespacesInAsyncContextConfiguration)
|| context.Options.GetConfigurationValue(operation, IncludeExtensionMethodsFromNotImportedNamespacesConfiguration);
- var result = FindAsyncEquivalent(operation, sqliteSpecialCasesEnabled, includeExtensionMethodsFromNotImportedNamespaces, context.CancellationToken, out var diagnosticMessage);
+ var result = FindAsyncEquivalent(operation, sqliteSpecialCasesEnabled, includeExtensionMethodsFromNotImportedNamespaces, lookupCache, context.CancellationToken, out var diagnosticMessage);
if (diagnosticMessage is not null)
{
ReportDiagnosticIfNeeded(context, diagnosticMessage.CreateProperties(), operation, diagnosticMessage.DiagnosticMessage, asyncContextKind, requiresNamespaceImport: diagnosticMessage.NamespaceToImport is not null);
@@ -256,7 +260,7 @@ internal void AnalyzeInvocation(OperationAnalysisContext context)
/// Searches for an async equivalent of the called method, visible from the call site.
/// is set when the result is .
///
- private AsyncEquivalentSearchResult FindAsyncEquivalent(IInvocationOperation operation, bool sqliteSpecialCasesEnabled, bool includeExtensionMethodsFromNotImportedNamespaces, CancellationToken cancellationToken, out DiagnosticData? data)
+ private AsyncEquivalentSearchResult FindAsyncEquivalent(IInvocationOperation operation, bool sqliteSpecialCasesEnabled, bool includeExtensionMethodsFromNotImportedNamespaces, OverloadLookupCache lookupCache, CancellationToken cancellationToken, out DiagnosticData? data)
{
data = null;
var targetMethod = operation.TargetMethod;
@@ -366,10 +370,10 @@ private AsyncEquivalentSearchResult FindAsyncEquivalent(IInvocationOperation ope
// as the code fix must add a using directive to call them
IMethodSymbol? notImportedAsyncEquivalentMethod = null;
string? namespaceToImport = null;
- var asyncEquivalentMethod = FindPotentialAsyncEquivalent(operation, targetMethod, targetMethod.Name, includeExtensionMethodsFromNotImportedNamespaces, ref notImportedAsyncEquivalentMethod, ref namespaceToImport);
+ var asyncEquivalentMethod = FindPotentialAsyncEquivalent(operation, targetMethod, targetMethod.Name, includeExtensionMethodsFromNotImportedNamespaces, lookupCache, ref notImportedAsyncEquivalentMethod, ref namespaceToImport);
if (asyncEquivalentMethod is null && !targetMethod.Name.EndsWith("Async", StringComparison.Ordinal))
{
- asyncEquivalentMethod = FindPotentialAsyncEquivalent(operation, targetMethod, targetMethod.Name + "Async", includeExtensionMethodsFromNotImportedNamespaces, ref notImportedAsyncEquivalentMethod, ref namespaceToImport);
+ asyncEquivalentMethod = FindPotentialAsyncEquivalent(operation, targetMethod, targetMethod.Name + "Async", includeExtensionMethodsFromNotImportedNamespaces, lookupCache, ref notImportedAsyncEquivalentMethod, ref namespaceToImport);
}
if (asyncEquivalentMethod is null)
@@ -519,13 +523,14 @@ private bool IsSqliteSpecialCaseMethod(IInvocationOperation operation, Cancellat
/// is set, the first async equivalent that requires to import its namespace is set to , and its
/// namespace to , if they are not already set.
///
- private IMethodSymbol? FindPotentialAsyncEquivalent(IInvocationOperation operation, IMethodSymbol targetMethod, string methodName, bool includeExtensionMethodsFromNotImportedNamespaces, ref IMethodSymbol? notImportedMethod, ref string? namespaceToImport)
+ private IMethodSymbol? FindPotentialAsyncEquivalent(IInvocationOperation operation, IMethodSymbol targetMethod, string methodName, bool includeExtensionMethodsFromNotImportedNamespaces, OverloadLookupCache lookupCache, ref IMethodSymbol? notImportedMethod, ref string? namespaceToImport)
{
var options = new OverloadOptions(
AllowOptionalParameters: false,
IncludeExtensionsMethods: true,
SyntaxNode: operation.Syntax,
- IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces);
+ IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces,
+ LookupCache: lookupCache);
// When the method name is the same as the original method, and the original is non-generic
// while a candidate is generic, the compiler will always prefer the non-generic original
diff --git a/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasCancellationTokenAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasCancellationTokenAnalyzer.cs
index 089bfd58e..d5b832035 100644
--- a/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasCancellationTokenAnalyzer.cs
+++ b/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasCancellationTokenAnalyzer.cs
@@ -63,8 +63,12 @@ public override void Initialize(AnalysisContext context)
if (analyzerContext.CancellationTokenSymbol is null)
return;
- ctx.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
- ctx.RegisterOperationAction(analyzerContext.AnalyzeLoop, OperationKind.Loop);
+ ctx.RegisterSymbolStartAction(symbolContext =>
+ {
+ var lookupCache = new OverloadLookupCache();
+ symbolContext.RegisterOperationAction(context => analyzerContext.AnalyzeInvocation(context, lookupCache), OperationKind.Invocation);
+ symbolContext.RegisterOperationAction(context => analyzerContext.AnalyzeLoop(context, lookupCache), OperationKind.Loop);
+ }, SymbolKind.NamedType);
});
}
@@ -132,7 +136,7 @@ private bool HasExplicitCancellationTokenArgument(IInvocationOperation operation
/// The namespace to import to call the overload, when it is an extension method declared in a namespace that is not imported.
private sealed record AdditionalParameterInfo(int ParameterIndex, string? Name, bool HasEnumeratorCancellationAttribute, string? NamespaceToImport = null);
- private bool HasAnOverloadWithCancellationToken(OperationAnalysisContext context, IInvocationOperation operation, [NotNullWhen(true)] out AdditionalParameterInfo? parameterInfo)
+ private bool HasAnOverloadWithCancellationToken(OperationAnalysisContext context, IInvocationOperation operation, OverloadLookupCache lookupCache, [NotNullWhen(true)] out AdditionalParameterInfo? parameterInfo)
{
parameterInfo = default;
var method = operation.TargetMethod;
@@ -145,7 +149,7 @@ private bool HasAnOverloadWithCancellationToken(OperationAnalysisContext context
var allowOptionalParameters = context.Options.GetConfigurationValue(operation, AllowOverloadsWithOptionalParametersConfiguration);
var includeExtensionMethodsFromNotImportedNamespaces = context.Options.GetConfigurationValue(operation, IncludeExtensionMethodsFromNotImportedNamespacesConfiguration)
|| context.Options.GetConfigurationValue(operation, IncludeExtensionMethodsFromNotImportedNamespacesWhenACancellationTokenIsAvailableConfiguration);
- var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions(AllowOptionalParameters: allowOptionalParameters, IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces), [CancellationTokenSymbol]);
+ var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions(AllowOptionalParameters: allowOptionalParameters, IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces, LookupCache: lookupCache), [CancellationTokenSymbol]);
if (overload is not null)
{
var namespaceToImport = includeExtensionMethodsFromNotImportedNamespaces ? _overloadFinder.GetNamespaceToImport(overload, operation.Syntax) : null;
@@ -188,13 +192,13 @@ private bool HasEnumerableCancellationAttribute(IParameterSymbol? parameterSymbo
return parameterSymbol.HasAttribute(EnumeratorCancellationAttributeSymbol, inherits: false);
}
- public void AnalyzeInvocation(OperationAnalysisContext context)
+ public void AnalyzeInvocation(OperationAnalysisContext context, OverloadLookupCache lookupCache)
{
var operation = (IInvocationOperation)context.Operation;
if (HasExplicitCancellationTokenArgument(operation))
return;
- if (!HasAnOverloadWithCancellationToken(context, operation, out var parameterInfo))
+ if (!HasAnOverloadWithCancellationToken(context, operation, lookupCache, out var parameterInfo))
return;
if (IsExcluded(operation.TargetMethod))
@@ -231,7 +235,7 @@ private static bool IsOverloadIncluded(OperationAnalysisContext context, IOperat
return context.Options.GetConfigurationValue(operation, configuration);
}
- public void AnalyzeLoop(OperationAnalysisContext context)
+ public void AnalyzeLoop(OperationAnalysisContext context, OverloadLookupCache lookupCache)
{
if (context.Operation is not IForEachLoopOperation op)
return;
@@ -261,7 +265,7 @@ public void AnalyzeLoop(OperationAnalysisContext context)
return;
// Already handled by AnalyzeInvocation
- if (HasAnOverloadWithCancellationToken(context, invocation, out var invocationParameterInfo) &&
+ if (HasAnOverloadWithCancellationToken(context, invocation, lookupCache, out var invocationParameterInfo) &&
(invocationParameterInfo.NamespaceToImport is null || IsOverloadIncluded(context, invocation, invocationParameterInfo, hasAvailableCancellationTokens: _cancellationTokenFinder.FindPaths(invocation, context.CancellationToken).Length > 0)))
{
return;
diff --git a/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasTimeProviderAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasTimeProviderAnalyzer.cs
index 9ca37a10c..67b1e197f 100644
--- a/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasTimeProviderAnalyzer.cs
+++ b/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasTimeProviderAnalyzer.cs
@@ -41,7 +41,11 @@ public override void Initialize(AnalysisContext context)
if (analyzerContext.TimeProviderSymbol is null)
return;
- ctx.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
+ ctx.RegisterSymbolStartAction(symbolContext =>
+ {
+ var lookupCache = new OverloadLookupCache();
+ symbolContext.RegisterOperationAction(context => analyzerContext.AnalyzeInvocation(context, lookupCache), OperationKind.Invocation);
+ }, SymbolKind.NamedType);
});
}
@@ -74,14 +78,14 @@ private bool HasExplicitTimeProviderArgument(IInvocationOperation operation)
/// The namespace to import to call the overload, when it is an extension method declared in a namespace that is not imported.
private sealed record AdditionalParameterInfo(int ParameterIndex, string? Name, string? NamespaceToImport = null);
- private bool HasAnOverloadWithTimeProvider(OperationAnalysisContext context, IInvocationOperation operation, [NotNullWhen(true)] out AdditionalParameterInfo? parameterInfo)
+ private bool HasAnOverloadWithTimeProvider(OperationAnalysisContext context, IInvocationOperation operation, OverloadLookupCache lookupCache, [NotNullWhen(true)] out AdditionalParameterInfo? parameterInfo)
{
if (IsArgumentImplicitlyDeclared(operation, TimeProviderSymbol, out parameterInfo))
return true;
var includeExtensionMethodsFromNotImportedNamespaces = context.Options.GetConfigurationValue(operation, IncludeExtensionMethodsFromNotImportedNamespacesConfiguration)
|| context.Options.GetConfigurationValue(operation, IncludeExtensionMethodsFromNotImportedNamespacesWhenAvailableConfiguration);
- var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions(IncludeObsoleteMembers: false, AllowOptionalParameters: true, IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces), [TimeProviderSymbol]);
+ var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions(IncludeObsoleteMembers: false, AllowOptionalParameters: true, IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces, LookupCache: lookupCache), [TimeProviderSymbol]);
if (overload is not null)
{
var namespaceToImport = includeExtensionMethodsFromNotImportedNamespaces ? _overloadFinder.GetNamespaceToImport(overload, operation.Syntax) : null;
@@ -116,13 +120,13 @@ static bool IsArgumentImplicitlyDeclared(IInvocationOperation invocationOperatio
}
}
- public void AnalyzeInvocation(OperationAnalysisContext context)
+ public void AnalyzeInvocation(OperationAnalysisContext context, OverloadLookupCache lookupCache)
{
var operation = (IInvocationOperation)context.Operation;
if (HasExplicitTimeProviderArgument(operation))
return;
- if (!HasAnOverloadWithTimeProvider(context, operation, out var parameterInfo))
+ if (!HasAnOverloadWithTimeProvider(context, operation, lookupCache, out var parameterInfo))
return;
var availableTimeProviders = _timeProviderFinder.FindPaths(operation, context.CancellationToken);
diff --git a/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs
index 1e6380dce..f7b4446fc 100644
--- a/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs
+++ b/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs
@@ -30,7 +30,11 @@ public override void Initialize(AnalysisContext context)
context.RegisterCompilationStartAction(context =>
{
var analyzerContext = new AnalyzerContext(context.Compilation);
- context.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
+ context.RegisterSymbolStartAction(symbolContext =>
+ {
+ var lookupCache = new OverloadLookupCache();
+ symbolContext.RegisterOperationAction(operationContext => analyzerContext.AnalyzeInvocation(operationContext, lookupCache), OperationKind.Invocation);
+ }, SymbolKind.NamedType);
});
}
@@ -39,7 +43,7 @@ private sealed class AnalyzerContext(Compilation compilation)
private readonly CultureSensitiveFormattingContext _cultureSensitiveContext = new(compilation);
private readonly OverloadFinder _overloadFinder = new(compilation);
- public void AnalyzeInvocation(OperationAnalysisContext context)
+ public void AnalyzeInvocation(OperationAnalysisContext context, OverloadLookupCache lookupCache)
{
var operation = (IInvocationOperation)context.Operation;
if (operation is null)
@@ -55,7 +59,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context)
var includeExtensionMethodsFromNotImportedNamespaces = context.Options.GetConfigurationValue(operation, IncludeExtensionMethodsFromNotImportedNamespacesConfiguration);
// The overloads with an additional styles or format parameter are searched with the default options of OverloadFinder
- var stylesOverloadOptions = new OverloadOptions { IncludeExtensionMethodsFromNotImportedNamespaces = includeExtensionMethodsFromNotImportedNamespaces };
+ var stylesOverloadOptions = new OverloadOptions { IncludeExtensionMethodsFromNotImportedNamespaces = includeExtensionMethodsFromNotImportedNamespaces, LookupCache = lookupCache };
if (_cultureSensitiveContext.FormatProviderSymbol is not null && !operation.HasArgumentOfType(_cultureSensitiveContext.FormatProviderSymbol))
{
if (operation.TargetMethod.Name == "ToString" && operation.Arguments.Length == 0 && operation.TargetMethod.ContainingType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T)
@@ -64,7 +68,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context)
return;
}
- var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions(IncludeObsoleteMembers: false, AllowOptionalParameters: true, IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces), [_cultureSensitiveContext.FormatProviderSymbol]);
+ var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions(IncludeObsoleteMembers: false, AllowOptionalParameters: true, IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces, LookupCache: lookupCache), [_cultureSensitiveContext.FormatProviderSymbol]);
if (overload is not null)
{
if (CultureSensitiveFormattingContext.IsCultureSensitive(_cultureSensitiveContext.GetCultureSensitivity(operation, GetOptions(context, operation, unwrapNullableTypes: false)), options))
@@ -101,7 +105,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context)
if (_cultureSensitiveContext.CultureInfoSymbol is not null && !operation.HasArgumentOfType(_cultureSensitiveContext.CultureInfoSymbol))
{
- var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions(IncludeObsoleteMembers: false, AllowOptionalParameters: false, IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces), [_cultureSensitiveContext.CultureInfoSymbol]);
+ var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions(IncludeObsoleteMembers: false, AllowOptionalParameters: false, IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces, LookupCache: lookupCache), [_cultureSensitiveContext.CultureInfoSymbol]);
if (overload is not null)
{
if (CultureSensitiveFormattingContext.IsCultureSensitive(_cultureSensitiveContext.GetCultureSensitivity(operation, GetOptions(context, operation, unwrapNullableTypes: false)), options))
diff --git a/src/Meziantou.Analyzer/Rules/UseInKeywordForInParameterAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseInKeywordForInParameterAnalyzer.cs
index cb408afca..43c26af7a 100644
--- a/src/Meziantou.Analyzer/Rules/UseInKeywordForInParameterAnalyzer.cs
+++ b/src/Meziantou.Analyzer/Rules/UseInKeywordForInParameterAnalyzer.cs
@@ -39,11 +39,15 @@ public override void Initialize(AnalysisContext context)
context.RegisterCompilationStartAction(context =>
{
var overloadFinder = new OverloadFinder(context.Compilation);
- context.RegisterOperationAction(context => AnalyzeArgument(context, overloadFinder), OperationKind.Argument);
+ context.RegisterSymbolStartAction(symbolContext =>
+ {
+ var lookupCache = new OverloadLookupCache();
+ symbolContext.RegisterOperationAction(operationContext => AnalyzeArgument(operationContext, overloadFinder, lookupCache), OperationKind.Argument);
+ }, SymbolKind.NamedType);
});
}
- private static void AnalyzeArgument(OperationAnalysisContext context, OverloadFinder overloadFinder)
+ private static void AnalyzeArgument(OperationAnalysisContext context, OverloadFinder overloadFinder, OverloadLookupCache lookupCache)
{
var operation = (IArgumentOperation)context.Operation;
if (operation.Parameter is null)
@@ -72,7 +76,7 @@ private static void AnalyzeArgument(OperationAnalysisContext context, OverloadFi
// The arguments are in source order, which differs from the parameter order when named arguments are reordered
var includeExtensionMethodsFromNotImportedNamespaces = context.Options.GetConfigurationValue(operation, IncludeExtensionMethodsFromNotImportedNamespacesConfiguration);
- if (FindInOverloadWithEquivalentParameters(invocationOperation, operation.Parameter.Ordinal, overloadFinder, includeExtensionMethodsFromNotImportedNamespaces) is { } overload)
+ if (FindInOverloadWithEquivalentParameters(invocationOperation, operation.Parameter.Ordinal, overloadFinder, lookupCache, includeExtensionMethodsFromNotImportedNamespaces) is { } overload)
{
var properties = ImmutableDictionary.Empty;
if (includeExtensionMethodsFromNotImportedNamespaces && overloadFinder.GetNamespaceToImport(overload, invocationOperation.Syntax) is { } namespaceToImport)
@@ -92,7 +96,7 @@ private static bool CanUseInAtCallSite(IArgumentOperation operation)
return UseInKeywordForInParameterCommon.CanBePassedByReference(operation.Value);
}
- private static IMethodSymbol? FindInOverloadWithEquivalentParameters(IInvocationOperation invocationOperation, int parameterIndex, OverloadFinder overloadFinder, bool includeExtensionMethodsFromNotImportedNamespaces)
+ private static IMethodSymbol? FindInOverloadWithEquivalentParameters(IInvocationOperation invocationOperation, int parameterIndex, OverloadFinder overloadFinder, OverloadLookupCache lookupCache, bool includeExtensionMethodsFromNotImportedNamespaces)
{
var targetMethod = invocationOperation.TargetMethod;
if (targetMethod.ContainingType is null)
@@ -118,6 +122,7 @@ private static bool CanUseInAtCallSite(IArgumentOperation operation)
AllowInModifierCompatibility: true,
AllowInterfaceConversions: false,
IncludeExtensionMethodsFromNotImportedNamespaces: includeExtensionMethodsFromNotImportedNamespaces,
+ LookupCache: lookupCache,
ShouldCheckMethod: method =>
{
if (method.Parameters.Length != targetMethod.Parameters.Length)
diff --git a/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs
index eac165b21..ab8e24a09 100644
--- a/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs
+++ b/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs
@@ -107,7 +107,11 @@ public override void Initialize(AnalysisContext context)
{
var analyzerContext = new AnalyzerContext(ctx.Compilation);
ctx.RegisterOperationAction(analyzerContext.AnalyzeConstructor, OperationKind.ObjectCreation);
- ctx.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
+ ctx.RegisterSymbolStartAction(symbolContext =>
+ {
+ var lookupCache = new OverloadLookupCache();
+ symbolContext.RegisterOperationAction(operationContext => analyzerContext.AnalyzeInvocation(operationContext, lookupCache), OperationKind.Invocation);
+ }, SymbolKind.NamedType);
#if ROSLYN_4_14_OR_GREATER
ctx.RegisterOperationAction(analyzerContext.AnalyzeCollectionExpression, OperationKind.CollectionExpression);
#endif
@@ -163,7 +167,7 @@ public void AnalyzeConstructor(OperationAnalysisContext ctx)
}
}
- public void AnalyzeInvocation(OperationAnalysisContext ctx)
+ public void AnalyzeInvocation(OperationAnalysisContext ctx, OverloadLookupCache lookupCache)
{
var operation = (IInvocationOperation)ctx.Operation;
if (HasEqualityComparerArgument(operation.Arguments))
@@ -197,7 +201,7 @@ public void AnalyzeInvocation(OperationAnalysisContext ctx)
if (QueryableType is not null && method.ContainingType.IsEqualTo(QueryableType))
return;
- if (HasOverloadWithComparer(ctx, operation, out var namespaceToImport))
+ if (HasOverloadWithComparer(ctx, operation, lookupCache, out var namespaceToImport))
{
if (IsInvocationReportSuppressedByOrdinalOption(ctx, operation, method))
return;
@@ -253,11 +257,11 @@ public void AnalyzeInvocation(OperationAnalysisContext ctx)
/// parameter. An overload that does not require a new using directive is preferred. is set when the
/// overload is an extension method declared in a namespace that is not imported.
///
- private bool HasOverloadWithComparer(OperationAnalysisContext context, IInvocationOperation operation, out string? namespaceToImport)
+ private bool HasOverloadWithComparer(OperationAnalysisContext context, IInvocationOperation operation, OverloadLookupCache lookupCache, out string? namespaceToImport)
{
namespaceToImport = null;
var includeExtensionMethodsFromNotImportedNamespaces = context.Options.GetConfigurationValue(operation, IncludeExtensionMethodsFromNotImportedNamespacesConfiguration);
- var options = new OverloadOptions { IncludeExtensionMethodsFromNotImportedNamespaces = includeExtensionMethodsFromNotImportedNamespaces };
+ var options = new OverloadOptions { IncludeExtensionMethodsFromNotImportedNamespaces = includeExtensionMethodsFromNotImportedNamespaces, LookupCache = lookupCache };
var found = false;
foreach (var comparerType in (ReadOnlySpan)[EqualityComparerStringType, ComparerStringType])
diff --git a/src/Meziantou.Analyzer/Rules/UseStringComparisonAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseStringComparisonAnalyzer.cs
index 685418b46..59c190bd9 100644
--- a/src/Meziantou.Analyzer/Rules/UseStringComparisonAnalyzer.cs
+++ b/src/Meziantou.Analyzer/Rules/UseStringComparisonAnalyzer.cs
@@ -41,7 +41,11 @@ public override void Initialize(AnalysisContext context)
if (!analyzerContext.IsValid)
return;
- context.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
+ context.RegisterSymbolStartAction(symbolContext =>
+ {
+ var lookupCache = new OverloadLookupCache();
+ symbolContext.RegisterOperationAction(operationContext => analyzerContext.AnalyzeInvocation(operationContext, lookupCache), OperationKind.Invocation);
+ }, SymbolKind.NamedType);
});
}
@@ -104,7 +108,7 @@ void Add(string documentationId)
}
}
- public void AnalyzeInvocation(OperationAnalysisContext context)
+ public void AnalyzeInvocation(OperationAnalysisContext context, OverloadLookupCache lookupCache)
{
var operation = (IInvocationOperation)context.Operation;
if (!operation.HasArgumentOfType(_stringComparisonSymbol))
@@ -117,7 +121,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context)
// Check if there is an overload with a StringComparison
var includeExtensionMethodsFromNotImportedNamespaces = context.Options.GetConfigurationValue(operation, UseStringComparisonIncludeExtensionMethodsFromNotImportedNamespacesConfiguration)
|| context.Options.GetConfigurationValue(operation, AvoidCultureSensitiveMethodIncludeExtensionMethodsFromNotImportedNamespacesConfiguration);
- var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions { IncludeExtensionMethodsFromNotImportedNamespaces = includeExtensionMethodsFromNotImportedNamespaces }, [_stringComparisonSymbol]);
+ var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation, new OverloadOptions { IncludeExtensionMethodsFromNotImportedNamespaces = includeExtensionMethodsFromNotImportedNamespaces, LookupCache = lookupCache }, [_stringComparisonSymbol]);
if (overload is not null)
{
var (rule, configuration) = IsNonCultureSensitiveMethod(operation)
diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs
index 46759698d..44bfb13ac 100644
--- a/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs
+++ b/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs
@@ -1476,6 +1476,27 @@ void Foo()
return test.RunAsync();
}
+ [Fact]
+ public Task TopLevelStatements_InvocationIsReported()
+ {
+ var test = CreateTest();
+ test.TestState.OutputKind = OutputKind.ConsoleApplication;
+ test.TestCode = """
+ using System.Threading;
+
+ var cancellationToken = CancellationToken.None;
+ {|MA0032:Sample.Repro()|};
+
+ class Sample
+ {
+ public static void Repro() => throw null;
+ public static void Repro(CancellationToken cancellationToken) => throw null;
+ }
+ """;
+
+ return test.RunAsync();
+ }
+
[Fact]
public Task ExcludedMethod_Attribute_ShouldNotReportDiagnostic()
{
@@ -1953,4 +1974,188 @@ public class Sample
return test.RunAsync();
}
+
+ [Fact]
+ public Task ExtensionMethod_ImportedInOneNamespaceBlockOfTheFile()
+ {
+ // Each call is made twice so that the second one is answered from the lookup cache of the analyzed type
+ var test = CreateTest();
+ test.TestCode = """
+ namespace A
+ {
+ using Ext;
+
+ class TestA
+ {
+ public void M(Sample sample, System.Threading.CancellationToken cancellationToken)
+ {
+ {|MA0040:sample.Run()|};
+ {|MA0040:sample.Run()|};
+ }
+ }
+ }
+
+ namespace B
+ {
+ class TestB
+ {
+ public void M(Sample sample, System.Threading.CancellationToken cancellationToken)
+ {
+ sample.Run();
+ sample.Run();
+ }
+ }
+ }
+
+ public class Sample
+ {
+ public void Run() => throw null;
+ }
+
+ namespace Ext
+ {
+ public static class SampleExtensions
+ {
+ public static void Run(this Sample sample, System.Threading.CancellationToken cancellationToken) => throw null;
+ }
+ }
+ """;
+
+ return test.RunAsync();
+ }
+
+ [Fact]
+ public Task ExtensionMethod_ImportedInOneFileOfTheCompilation()
+ {
+ // Each call is made twice so that the second one is answered from the lookup cache of the analyzed type
+ var test = CreateTest();
+ test.TestState.Sources.Add(("File1.cs", """
+ using Ext;
+
+ class TestA
+ {
+ public void M(Sample sample, System.Threading.CancellationToken cancellationToken)
+ {
+ {|MA0040:sample.Run()|};
+ {|MA0040:sample.Run()|};
+ }
+ }
+ """));
+ test.TestState.Sources.Add(("File2.cs", """
+ using Bar;
+
+ class TestB
+ {
+ public void M(Sample sample, System.Threading.CancellationToken cancellationToken)
+ {
+ sample.Run();
+ sample.Run();
+ }
+ }
+
+ public class Sample
+ {
+ public void Run() => throw null;
+ }
+
+ namespace Ext
+ {
+ public static class SampleExtensions
+ {
+ public static void Run(this Sample sample, System.Threading.CancellationToken cancellationToken) => throw null;
+ }
+ }
+
+ namespace Bar
+ {
+ public static class Unrelated
+ {
+ }
+ }
+ """));
+
+ return test.RunAsync();
+ }
+
+ [Fact]
+ public Task PrivateOverload_OnlyReportedWhereAccessible()
+ {
+ // Each call is made twice so that the second one is answered from the lookup cache of the analyzed type
+ var test = CreateTest();
+ test.TestCode = """
+ public class Sample
+ {
+ public void Run() => throw null;
+ private void Run(System.Threading.CancellationToken cancellationToken) => throw null;
+
+ public void M(Sample other, System.Threading.CancellationToken cancellationToken)
+ {
+ {|MA0040:other.Run()|};
+ {|MA0040:other.Run()|};
+ }
+
+ private sealed class Nested
+ {
+ public void M(Sample sample, System.Threading.CancellationToken cancellationToken)
+ {
+ {|MA0040:sample.Run()|};
+ {|MA0040:sample.Run()|};
+ }
+ }
+ }
+
+ class Test
+ {
+ public void M(Sample sample, System.Threading.CancellationToken cancellationToken)
+ {
+ sample.Run();
+ sample.Run();
+ }
+ }
+ """;
+
+ return test.RunAsync();
+ }
+
+ [Fact]
+ public Task ExtensionMethod_ImportedInNamespaceBlockAfterTopLevelStatements()
+ {
+ // Each call is made twice so that the second one is answered from the lookup cache of the analyzed type
+ var test = CreateTest();
+ test.TestState.OutputKind = OutputKind.ConsoleApplication;
+ test.TestCode = """
+ var cancellationToken = System.Threading.CancellationToken.None;
+ new Sample().Run();
+ new Sample().Run();
+
+ namespace A
+ {
+ using Ext;
+
+ class TestA
+ {
+ public void M(Sample sample, System.Threading.CancellationToken cancellationToken)
+ {
+ {|MA0040:sample.Run()|};
+ {|MA0040:sample.Run()|};
+ }
+ }
+ }
+
+ public class Sample
+ {
+ public void Run() => throw null;
+ }
+
+ namespace Ext
+ {
+ public static class SampleExtensions
+ {
+ public static void Run(this Sample sample, System.Threading.CancellationToken cancellationToken) => throw null;
+ }
+ }
+ """;
+
+ return test.RunAsync();
+ }
}