From 426b1a82b4425a59cf3b2b61adfdf98fade42df8 Mon Sep 17 00:00:00 2001 From: Alexander Efremov Date: Sat, 3 Oct 2026 15:10:12 +0700 Subject: [PATCH] Check for a set instance only before an MA0002 report MA0002 ran a data flow analysis of the instance of every invocation, up to three times, to skip calls on sets. It now runs it once, only for a call it would report. --- .../Rules/UseStringComparerAnalyzer.cs | 36 +++++++-- .../Rules/UseStringComparerAnalyzerTests.cs | 79 +++++++++++++++++++ 2 files changed, 107 insertions(+), 8 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs index ab8e24a09..f49986436 100644 --- a/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs @@ -181,16 +181,10 @@ public void AnalyzeInvocation(OperationAnalysisContext ctx, OverloadLookupCache // Most ISet implementation already configured the IEqualityComparer in this constructor, // so it should be ok to skip method calls on those types. // A concrete use-case is HashSet.Contains which has an extension method IEnumerable.Contains(value, comparer) + // The instance of the call is checked by IsCalledOnSetInstance, only before a report. foreach (var type in (ReadOnlySpan)[ISetType, IReadOnlySetType, IImmutableSetType]) { - - if (type is null) - continue; - - if (method.ContainingType.IsOrImplements(type)) - return; - - if (operation.Instance is not null && operation.Instance.GetActualType(ctx.CancellationToken)?.IsOrImplements(type) is true) + if (type is not null && method.ContainingType.IsOrImplements(type)) return; } @@ -203,6 +197,9 @@ public void AnalyzeInvocation(OperationAnalysisContext ctx, OverloadLookupCache if (HasOverloadWithComparer(ctx, operation, lookupCache, out var namespaceToImport)) { + if (IsCalledOnSetInstance(operation, ctx.CancellationToken)) + return; + if (IsInvocationReportSuppressedByOrdinalOption(ctx, operation, method)) return; @@ -252,6 +249,29 @@ public void AnalyzeInvocation(OperationAnalysisContext ctx, OverloadLookupCache } } + /// + /// Indicates whether the instance of the call is a set of strings, whatever the declared type of the expression. + /// Finding the actual type runs a data flow analysis of the local the instance comes from, so it is done once, and + /// only for a call that would be reported. + /// + private bool IsCalledOnSetInstance(IInvocationOperation operation, CancellationToken cancellationToken) + { + if (operation.Instance is null) + return false; + + var instanceType = operation.Instance.GetActualType(cancellationToken); + if (instanceType is null) + return false; + + foreach (var type in (ReadOnlySpan)[ISetType, IReadOnlySetType, IImmutableSetType]) + { + if (type is not null && instanceType.IsOrImplements(type)) + return true; + } + + return false; + } + /// /// Indicates whether the invoked method has an overload with an IEqualityComparer<string> or an IComparer<string> /// parameter. An overload that does not require a new using directive is preferred. is set when the diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseStringComparerAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseStringComparerAnalyzerTests.cs index 4f57b1ebf..0d4d324a4 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseStringComparerAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseStringComparerAnalyzerTests.cs @@ -2024,4 +2024,83 @@ public static class SampleExtensions return test.RunAsync(); } + + [Fact] + public Task MethodOnLocalHoldingSetDeclaredAsInterface_ShouldNotReportDiagnostic() + { + var test = CreateTest(); + test.TestCode = """ + class TypeName + { + public void Test() + { + IContainer values = new CustomSet(); + _ = values.Contains("a"); + } + } + + interface IContainer + { + bool Contains(string value); + bool Contains(string value, System.Collections.Generic.IEqualityComparer comparer); + } + + sealed class CustomSet : System.Collections.Generic.HashSet, IContainer + { + public bool Contains(string value, System.Collections.Generic.IEqualityComparer comparer) => throw null; + } + """; + + return test.RunAsync(); + } + + [Fact] + public Task MethodOnLocalHoldingListDeclaredAsInterface_ShouldReportDiagnostic() + { + var test = CreateTest(); + test.TestCode = """ + class TypeName + { + public void Test() + { + IContainer values = new CustomList(); + _ = values.{|MA0002:Contains("a")|}; + } + } + + interface IContainer + { + bool Contains(string value); + bool Contains(string value, System.Collections.Generic.IEqualityComparer comparer); + } + + sealed class CustomList : System.Collections.Generic.List, IContainer + { + public bool Contains(string value, System.Collections.Generic.IEqualityComparer comparer) => throw null; + } + """; + test.FixedCode = """ + class TypeName + { + public void Test() + { + IContainer values = new CustomList(); + _ = values.Contains("a", System.StringComparer.Ordinal); + } + } + + interface IContainer + { + bool Contains(string value); + bool Contains(string value, System.Collections.Generic.IEqualityComparer comparer); + } + + sealed class CustomList : System.Collections.Generic.List, IContainer + { + public bool Contains(string value, System.Collections.Generic.IEqualityComparer comparer) => throw null; + } + """; + + return test.RunAsync(); + } }