diff --git a/docs/Rules/MA0042.md b/docs/Rules/MA0042.md index 410fa1a9b..49cc62196 100644 --- a/docs/Rules/MA0042.md +++ b/docs/Rules/MA0042.md @@ -123,7 +123,7 @@ The attribute supports: - a documentation ID (`M:...` for methods, `P:...` for properties) for exact matching - `Type` + member name (+ optional parameter types) for direct signature-based matching -You can also exclude `await` / `await using` recommendations for specific types using `NonAwaitableTypeAttribute`: +You can also exclude `await` recommendations for specific types using `NonAwaitableTypeAttribute`: To use `Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute`, add the `Meziantou.Analyzer.Annotations` NuGet package (or copy the attribute source into your project). See [Meziantou.Analyzer.Annotations README](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.Annotations/README.md). ```csharp @@ -132,6 +132,15 @@ To use `Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute`, add the `Mezi The match is exact-type only. Derived types are not excluded unless explicitly listed. +You can also exclude `await using` recommendations for specific types using `NonAsyncDisposableTypeAttribute`: +To use `Meziantou.Analyzer.Annotations.NonAsyncDisposableTypeAttribute`, add the `Meziantou.Analyzer.Annotations` NuGet package (or copy the attribute source into your project). See [Meziantou.Analyzer.Annotations README](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.Annotations/README.md). + +```csharp +[assembly: Meziantou.Analyzer.Annotations.NonAsyncDisposableTypeAttribute(typeof(System.Data.Common.DbCommand))] +``` + +The match is exact-type only. Derived types are not excluded unless explicitly listed. + This is useful for cases such as: ```csharp diff --git a/docs/Rules/MA0045.md b/docs/Rules/MA0045.md index df8404427..f541b530d 100644 --- a/docs/Rules/MA0045.md +++ b/docs/Rules/MA0045.md @@ -66,7 +66,7 @@ The attribute supports: - a documentation ID (`M:...` for methods, `P:...` for properties) for exact matching - `Type` + member name (+ optional parameter types) for direct signature-based matching -You can also exclude `await` / `await using` recommendations for specific types using `NonAwaitableTypeAttribute`: +You can also exclude `await` recommendations for specific types using `NonAwaitableTypeAttribute`: To use `Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute`, add the `Meziantou.Analyzer.Annotations` NuGet package (or copy the attribute source into your project). See [Meziantou.Analyzer.Annotations README](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.Annotations/README.md). ```csharp @@ -75,6 +75,15 @@ To use `Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute`, add the `Mezi The match is exact-type only. Derived types are not excluded unless explicitly listed. +You can also exclude `await using` recommendations for specific types using `NonAsyncDisposableTypeAttribute`: +To use `Meziantou.Analyzer.Annotations.NonAsyncDisposableTypeAttribute`, add the `Meziantou.Analyzer.Annotations` NuGet package (or copy the attribute source into your project). See [Meziantou.Analyzer.Annotations README](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.Annotations/README.md). + +```csharp +[assembly: Meziantou.Analyzer.Annotations.NonAsyncDisposableTypeAttribute(typeof(System.Data.Common.DbCommand))] +``` + +The match is exact-type only. Derived types are not excluded unless explicitly listed. + This is useful for cases such as: ```csharp diff --git a/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj b/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj index 53bd6e92f..5b3a0a455 100644 --- a/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj +++ b/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 1.4.0 + 1.5.0 Annotations to configure Meziantou.Analyzer Meziantou.Analyzer, analyzers True diff --git a/src/Meziantou.Analyzer.Annotations/NonAsyncDisposableTypeAttribute.cs b/src/Meziantou.Analyzer.Annotations/NonAsyncDisposableTypeAttribute.cs new file mode 100644 index 000000000..416e1e90c --- /dev/null +++ b/src/Meziantou.Analyzer.Annotations/NonAsyncDisposableTypeAttribute.cs @@ -0,0 +1,12 @@ +#pragma warning disable CS1591 +#pragma warning disable IDE0060 +#pragma warning disable CA1019 + +namespace Meziantou.Analyzer.Annotations; + +[System.Diagnostics.Conditional("MEZIANTOU_ANALYZER_ANNOTATIONS")] +[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] +public sealed class NonAsyncDisposableTypeAttribute : System.Attribute +{ + public NonAsyncDisposableTypeAttribute(System.Type type) { } +} diff --git a/src/Meziantou.Analyzer.Annotations/README.md b/src/Meziantou.Analyzer.Annotations/README.md index 281399853..e61d15754 100644 --- a/src/Meziantou.Analyzer.Annotations/README.md +++ b/src/Meziantou.Analyzer.Annotations/README.md @@ -19,7 +19,8 @@ If you want to keep these attributes in the metadata (for example, for reflectio | Attribute | Purpose | Related rules | | --- | --- | --- | | `CultureInsensitiveTypeAttribute` | Marks a type (or a specific format) as culture-insensitive. | [MA0011](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0011.md), [MA0075](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0075.md), [MA0076](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0076.md), [MA0185](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0185.md) | -| `NonAwaitableTypeAttribute` | Excludes await/await-using recommendations for specific types. | [MA0042](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0042.md), [MA0045](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0045.md), [MA0134](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0134.md), [MA0137](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0137.md), [MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md) | +| `NonAwaitableTypeAttribute` | Excludes await recommendations for specific types. | [MA0042](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0042.md), [MA0045](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0045.md), [MA0134](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0134.md), [MA0137](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0137.md), [MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md) | +| `NonAsyncDisposableTypeAttribute` | Excludes `await using` recommendations for specific types. | [MA0042](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0042.md), [MA0045](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0045.md) | | `ExcludeFromBlockingCallAnalysisAttribute` | Excludes specific methods/properties from blocking-call diagnostics. | [MA0042](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0042.md), [MA0045](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0045.md) | | `RequireNamedArgumentAttribute` | Requires named arguments for decorated parameters. | [MA0003](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0003.md) | | `StructuredLogFieldAttribute` | Declares allowed types for named log properties in an assembly. | [MA0124](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0124.md), [MA0139](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0139.md) | @@ -35,9 +36,18 @@ Use `ExcludeFromBlockingCallAnalysisAttribute` to exclude specific MA0042/MA0045 ## NonAwaitableTypeAttribute -Use `NonAwaitableTypeAttribute` to exclude MA0042/MA0045 `await`/`await using` recommendations for specific types at the assembly level. +Use `NonAwaitableTypeAttribute` to exclude MA0042/MA0045 `await` recommendations for specific types at the assembly level. The match is exact-type only. Derived types are not excluded unless explicitly listed. ```csharp [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(System.Data.Common.DbCommand))] ``` + +## NonAsyncDisposableTypeAttribute + +Use `NonAsyncDisposableTypeAttribute` to exclude MA0042/MA0045 `await using` recommendations for specific types at the assembly level. +The match is exact-type only. Derived types are not excluded unless explicitly listed. + +```csharp +[assembly: Meziantou.Analyzer.Annotations.NonAsyncDisposableTypeAttribute(typeof(System.Data.Common.DbCommand))] +``` diff --git a/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs b/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs index bdbd25061..ad30dc135 100644 --- a/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs +++ b/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs @@ -92,4 +92,26 @@ public static bool IsNonAwaitableTypeAttributeSymbol(ITypeSymbol? symbol) } }; } + + public static bool IsNonAsyncDisposableTypeAttributeSymbol(ITypeSymbol? symbol) + { + // Meziantou.Analyzer.Annotations.NonAsyncDisposableTypeAttribute + return symbol is INamedTypeSymbol + { + Name: "NonAsyncDisposableTypeAttribute", + ContainingSymbol: INamespaceSymbol + { + Name: "Annotations", + ContainingSymbol: INamespaceSymbol + { + Name: "Analyzer", + ContainingSymbol: INamespaceSymbol + { + Name: "Meziantou", + ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } + } + } + } + }; + } } diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index 24b4b0fb4..7cc0de523 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -59,6 +59,7 @@ private sealed class Context private readonly INamedTypeSymbol[] _taskAwaiterLikeSymbols; private readonly HashSet _excludedDiagnosticSymbols; + private readonly HashSet _nonAsyncDisposableTypes; private readonly ConcurrentHashSet _symbolsWithNoAsyncOverloads = new(SymbolEqualityComparer.Default); public Context(Compilation compilation) @@ -132,6 +133,7 @@ public Context(Compilation compilation) taskAwaiterLikeSymbols.AddIfNotNull(ValueTaskAwaiterOfTSymbol); _taskAwaiterLikeSymbols = [.. taskAwaiterLikeSymbols]; _excludedDiagnosticSymbols = CreateExcludedDiagnosticSymbols(compilation); + _nonAsyncDisposableTypes = CreateNonAsyncDisposableTypes(compilation); } private ISymbol? StreamSymbol { get; } @@ -419,6 +421,47 @@ static void AddExcludedSymbol(HashSet symbols, ISymbol symbol) } } + private bool IsNonAsyncDisposableType(ITypeSymbol? symbol) + { + if (_nonAsyncDisposableTypes.Count == 0 || symbol is not INamedTypeSymbol namedType) + return false; + + return IsConfiguredType(namedType, _nonAsyncDisposableTypes); + } + + private static HashSet CreateNonAsyncDisposableTypes(Compilation compilation) + { + var result = new HashSet(SymbolEqualityComparer.Default); + foreach (var attribute in compilation.Assembly.GetAttributes()) + { + if (!AnnotationAttributes.IsNonAsyncDisposableTypeAttributeSymbol(attribute.AttributeClass)) + continue; + + var constructorArguments = attribute.ConstructorArguments; + if (constructorArguments is [{ Value: INamedTypeSymbol type }]) + { + result.Add(type); + if (!ReferenceEquals(type, type.OriginalDefinition)) + { + result.Add(type.OriginalDefinition); + } + } + } + + return result; + } + + private static bool IsConfiguredType(INamedTypeSymbol type, HashSet configuredTypes) + { + if (configuredTypes.Contains(type)) + return true; + + if (!ReferenceEquals(type, type.OriginalDefinition) && configuredTypes.Contains(type.OriginalDefinition)) + return true; + + return false; + } + private bool IsExcludedDiagnosticSymbol(ISymbol symbol) { if (_excludedDiagnosticSymbols.Count == 0) @@ -662,7 +705,7 @@ private bool CanBeAwaitUsing(IOperation operation, bool sqliteSpecialCasesEnable if (operation.GetActualType() is not INamedTypeSymbol type) return false; - if (_awaitableTypes.IsNonAwaitableType(type)) + if (IsNonAsyncDisposableType(type)) return false; var isSqliteSpecialCaseType = IsSqliteSpecialCaseType(type); diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index f8ffd4f2c..478dfee41 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -3617,7 +3617,7 @@ public void Dispose() { } } [Fact] - public async Task NonAwaitableTypeAttribute_DoesAffectAwaitUsing() + public async Task NonAwaitableTypeAttribute_DoesNotAffectAwaitUsing() { await CreateProjectBuilder() .AddMeziantouAttributes() @@ -3626,6 +3626,33 @@ await CreateProjectBuilder() using System.Threading.Tasks; [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(AsyncDisposable))] + class Test + { + public async Task A() + { + [|using var value = new AsyncDisposable();|] + } + } + + class AsyncDisposable : IDisposable, IAsyncDisposable + { + public void Dispose() { } + public ValueTask DisposeAsync() => default; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NonAsyncDisposableTypeAttribute_DoesAffectAwaitUsing() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System; + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAsyncDisposableTypeAttribute(typeof(AsyncDisposable))] + class Test { public async Task A() diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs index e6d007f1f..805645f9a 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs @@ -419,7 +419,7 @@ public void Dispose() { } } [Fact] - public async Task NonAwaitableTypeAttribute_DoesAffectAwaitUsing() + public async Task NonAwaitableTypeAttribute_DoesNotAffectAwaitUsing() { await CreateProjectBuilder() .AddMeziantouAttributes() @@ -428,6 +428,33 @@ await CreateProjectBuilder() using System.Threading.Tasks; [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(AsyncDisposable))] + class Test + { + private void A() + { + [|using var value = new AsyncDisposable();|] + } + } + + class AsyncDisposable : IDisposable, IAsyncDisposable + { + public void Dispose() { } + public ValueTask DisposeAsync() => default; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NonAsyncDisposableTypeAttribute_DoesAffectAwaitUsing() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System; + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAsyncDisposableTypeAttribute(typeof(AsyncDisposable))] + class Test { private void A()