From efe156aa9b03989a819baf968148f8b50eab1c2c Mon Sep 17 00:00:00 2001 From: Alexander Efremov Date: Thu, 1 Oct 2026 14:18:30 +0700 Subject: [PATCH 1/2] Cache LookupSymbols results in OverloadFinder Analyzers that look for a better overload (MA0002, MA0032, MA0040, TimeProvider rules) call LookupSymbols for every invocation. The same method on the same type is looked up again and again, and each lookup scans all imported namespaces for extension methods. Cache the result per file, enclosing type, container type and method name. Imports and member accessibility can only differ between these scopes, so calls in the same scope get the same answer. --- .../Internals/OverloadFinder.cs | 22 ++- ...adThatHasCancellationTokenAnalyzerTests.cs | 184 ++++++++++++++++++ 2 files changed, 204 insertions(+), 2 deletions(-) diff --git a/src/Meziantou.Analyzer/Internals/OverloadFinder.cs b/src/Meziantou.Analyzer/Internals/OverloadFinder.cs index 6eba982fa..fc85d76c7 100644 --- a/src/Meziantou.Analyzer/Internals/OverloadFinder.cs +++ b/src/Meziantou.Analyzer/Internals/OverloadFinder.cs @@ -1,3 +1,6 @@ +using System.Collections.Concurrent; +using Microsoft.CodeAnalysis.CSharp.Syntax; + namespace Meziantou.Analyzer.Internals; internal sealed class OverloadFinder(Compilation compilation) @@ -13,6 +16,7 @@ internal sealed class OverloadFinder(Compilation compilation) private readonly INamedTypeSymbol? _ienumerableOfTSymbol = compilation.GetTypeByMetadataName("System.Collections.Generic.IEnumerable`1"); private readonly INamedTypeSymbol? _halfSymbol = compilation.GetTypeByMetadataName("System.Half"); private readonly Lazy>> _extensionMethodsByName = new(() => CreateExtensionMethodsByName(compilation)); + private readonly ConcurrentDictionary<(SyntaxTree Tree, int ScopeStart, ITypeSymbol Container, string Name, bool IncludeReducedExtensionMethods), ImmutableArray> _lookupCache = new(); private static ReadOnlySpan Wrap(ReadOnlySpan types) { @@ -671,10 +675,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.SyntaxNode, 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.SyntaxNode, position, reducedReceiverType, methodName, includeReducedExtensionMethods: false), results, knownSymbols); AddSymbols(reducedReceiverType.GetMembers(methodName), results, knownSymbols); } @@ -695,6 +699,20 @@ static void AddSymbols(IEnumerable symbols, List results, Hash return results; } + // A member lookup only depends on the enclosing type declaration (accessibility, imported namespaces), the container and the name + private ImmutableArray LookupSymbols(SemanticModel semanticModel, SyntaxNode node, int position, ITypeSymbol container, string name, bool includeReducedExtensionMethods) + { + var scopeStart = node.FirstAncestorOrSelf()?.SpanStart ?? -1; + var key = (node.SyntaxTree, scopeStart, container, name, includeReducedExtensionMethods); + if (!_lookupCache.TryGetValue(key, out var symbols)) + { + symbols = semanticModel.LookupSymbols(position, container, name, includeReducedExtensionMethods); + _lookupCache.TryAdd(key, symbols); + } + + return symbols; + } + /// /// 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/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs index 46759698d..e2235e460 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs @@ -1953,4 +1953,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 OverloadFinder + 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 OverloadFinder + 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 OverloadFinder + 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 OverloadFinder + 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(); + } } From 8bbc3197ababd8246a92a2505aee5e8777bdfef7 Mon Sep 17 00:00:00 2001 From: Alexander Efremov Date: Fri, 2 Oct 2026 21:54:22 +0700 Subject: [PATCH 2/2] Scope the OverloadFinder lookup cache to the analyzed type The lookup cache is now an OverloadLookupCache created per named type by RegisterSymbolStartAction and passed through OverloadOptions, so it is freed with the type instead of living for the whole compilation. The key uses SymbolEqualityComparer for the container symbol. The tree and enclosing declaration stay in the key because of partial and nested types. --- .../Directory.Build.props | 1 + .../Internals/OverloadFinder.cs | 22 ++----- .../Internals/OverloadLookupCache.cs | 57 +++++++++++++++++++ .../Internals/OverloadOptions.cs | 3 +- ...otUseBlockingCallInAsyncContextAnalyzer.cs | 21 ++++--- ...verloadThatHasCancellationTokenAnalyzer.cs | 20 ++++--- ...seAnOverloadThatHasTimeProviderAnalyzer.cs | 14 +++-- .../Rules/UseIFormatProviderAnalyzer.cs | 14 +++-- .../UseInKeywordForInParameterAnalyzer.cs | 13 +++-- .../Rules/UseStringComparerAnalyzer.cs | 14 +++-- .../Rules/UseStringComparisonAnalyzer.cs | 10 +++- ...adThatHasCancellationTokenAnalyzerTests.cs | 29 ++++++++-- 12 files changed, 159 insertions(+), 59 deletions(-) create mode 100644 src/Meziantou.Analyzer/Internals/OverloadLookupCache.cs 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 fc85d76c7..7986bf5f7 100644 --- a/src/Meziantou.Analyzer/Internals/OverloadFinder.cs +++ b/src/Meziantou.Analyzer/Internals/OverloadFinder.cs @@ -1,6 +1,3 @@ -using System.Collections.Concurrent; -using Microsoft.CodeAnalysis.CSharp.Syntax; - namespace Meziantou.Analyzer.Internals; internal sealed class OverloadFinder(Compilation compilation) @@ -16,7 +13,6 @@ internal sealed class OverloadFinder(Compilation compilation) private readonly INamedTypeSymbol? _ienumerableOfTSymbol = compilation.GetTypeByMetadataName("System.Collections.Generic.IEnumerable`1"); private readonly INamedTypeSymbol? _halfSymbol = compilation.GetTypeByMetadataName("System.Half"); private readonly Lazy>> _extensionMethodsByName = new(() => CreateExtensionMethodsByName(compilation)); - private readonly ConcurrentDictionary<(SyntaxTree Tree, int ScopeStart, ITypeSymbol Container, string Name, bool IncludeReducedExtensionMethods), ImmutableArray> _lookupCache = new(); private static ReadOnlySpan Wrap(ReadOnlySpan types) { @@ -675,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(LookupSymbols(semanticModel, options.SyntaxNode, 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(LookupSymbols(semanticModel, options.SyntaxNode, position, reducedReceiverType, methodName, includeReducedExtensionMethods: false), results, knownSymbols); + AddSymbols(LookupSymbols(semanticModel, options, position, reducedReceiverType, methodName, includeReducedExtensionMethods: false), results, knownSymbols); AddSymbols(reducedReceiverType.GetMembers(methodName), results, knownSymbols); } @@ -699,18 +695,12 @@ static void AddSymbols(IEnumerable symbols, List results, Hash return results; } - // A member lookup only depends on the enclosing type declaration (accessibility, imported namespaces), the container and the name - private ImmutableArray LookupSymbols(SemanticModel semanticModel, SyntaxNode node, int position, ITypeSymbol container, string name, bool includeReducedExtensionMethods) + private static ImmutableArray LookupSymbols(SemanticModel semanticModel, OverloadOptions options, int position, ITypeSymbol container, string name, bool includeReducedExtensionMethods) { - var scopeStart = node.FirstAncestorOrSelf()?.SpanStart ?? -1; - var key = (node.SyntaxTree, scopeStart, container, name, includeReducedExtensionMethods); - if (!_lookupCache.TryGetValue(key, out var symbols)) - { - symbols = semanticModel.LookupSymbols(position, container, name, includeReducedExtensionMethods); - _lookupCache.TryAdd(key, symbols); - } + if (options.LookupCache is null) + return semanticModel.LookupSymbols(position, container, name, includeReducedExtensionMethods); - return symbols; + return options.LookupCache.LookupSymbols(semanticModel, options.SyntaxNode!, position, container, name, includeReducedExtensionMethods); } /// 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 e2235e460..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() { @@ -1957,7 +1978,7 @@ public class Sample [Fact] public Task ExtensionMethod_ImportedInOneNamespaceBlockOfTheFile() { - // Each call is made twice so that the second one is answered from the lookup cache of OverloadFinder + // 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 @@ -2006,7 +2027,7 @@ public static class SampleExtensions [Fact] public Task ExtensionMethod_ImportedInOneFileOfTheCompilation() { - // Each call is made twice so that the second one is answered from the lookup cache of OverloadFinder + // 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; @@ -2059,7 +2080,7 @@ public static class Unrelated [Fact] public Task PrivateOverload_OnlyReportedWhereAccessible() { - // Each call is made twice so that the second one is answered from the lookup cache of OverloadFinder + // 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 @@ -2099,7 +2120,7 @@ public void M(Sample sample, System.Threading.CancellationToken cancellationToke [Fact] public Task ExtensionMethod_ImportedInNamespaceBlockAfterTopLevelStatements() { - // Each call is made twice so that the second one is answered from the lookup cache of OverloadFinder + // 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 = """