diff --git a/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs b/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs index e12affd5..4367fc39 100644 --- a/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs +++ b/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs @@ -7,6 +7,7 @@ namespace Meziantou.Analyzer.Internals; internal sealed class AwaitableTypes { private readonly INamedTypeSymbol[] _taskOrValueTaskSymbols; + private readonly Compilation _compilation; public AwaitableTypes(Compilation compilation) { @@ -30,6 +31,8 @@ public AwaitableTypes(Compilation compilation) { _taskOrValueTaskSymbols = []; } + + _compilation = compilation; } private INamedTypeSymbol? TaskSymbol { get; } @@ -74,7 +77,7 @@ public bool IsAwaitable(ITypeSymbol? symbol, SemanticModel semanticModel, int po return false; } - public bool IsAwaitable(ITypeSymbol? symbol, Compilation compilation) + public bool IsAwaitable(ITypeSymbol? symbol) { if (symbol is null) return false; @@ -95,7 +98,7 @@ public bool IsAwaitable(ITypeSymbol? symbol, Compilation compilation) if (potentialSymbol is not IMethodSymbol getAwaiterMethod) continue; - if (!compilation.IsSymbolAccessibleWithin(potentialSymbol, compilation.Assembly)) + if (!_compilation.IsSymbolAccessibleWithin(potentialSymbol, _compilation.Assembly)) continue; if (!getAwaiterMethod.Parameters.IsEmpty) diff --git a/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs b/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs index fec78c89..e574dcb1 100644 --- a/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs @@ -86,7 +86,7 @@ public void AnalyzeSymbol(SymbolAnalysisContext context) return; var hasAsyncSuffix = method.Name.EndsWith("Async", StringComparison.Ordinal); - if (_awaitableTypes.IsAwaitable(method.ReturnType, context.Compilation)) + if (_awaitableTypes.IsAwaitable(method.ReturnType)) { if (!hasAsyncSuffix) { @@ -119,7 +119,7 @@ public void AnalyzeLocalFunction(OperationAnalysisContext context) var method = operation.Symbol; var hasAsyncSuffix = method.Name.EndsWith("Async", StringComparison.Ordinal); - if (_awaitableTypes.IsAwaitable(method.ReturnType, context.Compilation)) + if (_awaitableTypes.IsAwaitable(method.ReturnType)) { if (!hasAsyncSuffix) { diff --git a/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs index a9e14483..127949af 100644 --- a/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseStringComparerAnalyzer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -77,6 +77,8 @@ private sealed class AnalyzerContext(Compilation compilation) public INamedTypeSymbol? ComparerStringType { get; } = GetIComparerString(compilation); public INamedTypeSymbol? EnumerableType { get; } = compilation.GetBestTypeByMetadataName("System.Linq.Enumerable"); public INamedTypeSymbol? ISetType { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Generic.ISet`1")?.Construct(compilation.GetSpecialType(SpecialType.System_String)); + public INamedTypeSymbol? IReadOnlySetType { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IReadOnlySet`1")?.Construct(compilation.GetSpecialType(SpecialType.System_String)); + public INamedTypeSymbol? IImmutableSetType { get; } = compilation.GetBestTypeByMetadataName("System.Collections.Immutable.IImmutableSet`1")?.Construct(compilation.GetSpecialType(SpecialType.System_String)); public void AnalyzeConstructor(OperationAnalysisContext ctx) { @@ -109,11 +111,18 @@ public void AnalyzeInvocation(OperationAnalysisContext ctx) // 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) - if (ISetType is not null && method.ContainingType.IsOrImplements(ISetType)) - return; + foreach (var type in (ReadOnlySpan)[ISetType, IReadOnlySetType, IImmutableSetType]) + { - if (operation.Instance is not null && operation.Instance.GetActualType()?.IsOrImplements(ISetType) == true) - return; + if (type is null) + continue; + + if (method.ContainingType.IsOrImplements(type)) + return; + + if (operation.Instance is not null && operation.Instance.GetActualType()?.IsOrImplements(type) is true) + return; + } if (operation.IsImplicit && IsQueryOperator(operation) && ctx.Options.GetConfigurationValue(operation, Rule.Id + ".exclude_query_operator_syntaxes", defaultValue: false)) return; diff --git a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.Validation.cs b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.Validation.cs index 08485a46..94c1cfcf 100755 --- a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.Validation.cs +++ b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.Validation.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -219,10 +219,10 @@ private Task CreateProject() break; } - AddNuGetReference("System.Collections.Immutable", "1.5.0", "lib/netstandard2.0/"); if (TargetFramework is not TargetFramework.Net7_0 and not TargetFramework.Net8_0 and not TargetFramework.Net9_0) { + AddNuGetReference("System.Collections.Immutable", "1.5.0", "lib/netstandard2.0/"); AddNuGetReference("System.Numerics.Vectors", "4.5.0", "ref/netstandard2.0/"); } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseStringComparerAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseStringComparerAnalyzerTests.cs index 14ca7c3b..049d0602 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/UseStringComparerAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseStringComparerAnalyzerTests.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Meziantou.Analyzer.Rules; using Meziantou.Analyzer.Test.Helpers; using TestHelper; @@ -458,6 +458,48 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task IReadOnlySet_Contain() + { + const string SourceCode = """ + using System.Linq; + class TypeName + { + public void Test() + { + System.Collections.Generic.IReadOnlySet obj = null; + obj.Contains(""); + } + } + """; + + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net6_0) + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task IImmutableSet_Contain() + { + const string SourceCode = """ + using System.Linq; + class TypeName + { + public void Test() + { + System.Collections.Immutable.IImmutableSet obj = null; + obj.Contains(""); + } + } + """; + + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + [Fact] public async Task StringArray_QuerySyntax_GroupBy_NoConfiguration() {