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(); + } }