diff --git a/docs/Rules/MA0042.md b/docs/Rules/MA0042.md index 180c2c230..410fa1a9b 100644 --- a/docs/Rules/MA0042.md +++ b/docs/Rules/MA0042.md @@ -123,6 +123,23 @@ 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`: +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 +[assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(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 +using var command = connection.CreateCommand(); +``` + +where `CreateCommand()` returns an await-using-capable type but `await using` is intentionally not desired. + ## Additional resources - [Enforcing asynchronous code good practices using a Roslyn analyzer](https://www.meziantou.net/enforcing-asynchronous-code-good-practices-using-a-roslyn-analyzer.htm) diff --git a/docs/Rules/MA0045.md b/docs/Rules/MA0045.md index 599ab43e7..df8404427 100644 --- a/docs/Rules/MA0045.md +++ b/docs/Rules/MA0045.md @@ -66,6 +66,23 @@ 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`: +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 +[assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(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 +using var command = connection.CreateCommand(); +``` + +where `CreateCommand()` returns an await-using-capable type but `await using` is intentionally not desired. + ## Additional resources - [Enforcing asynchronous code good practices using a Roslyn analyzer](https://www.meziantou.net/enforcing-asynchronous-code-good-practices-using-a-roslyn-analyzer.htm) diff --git a/docs/Rules/MA0100.md b/docs/Rules/MA0100.md index c788f7ef1..8dba811d4 100644 --- a/docs/Rules/MA0100.md +++ b/docs/Rules/MA0100.md @@ -25,6 +25,17 @@ class TestClass } ```` +## Configuration via annotations + +You can mark specific types as non-awaitable using `NonAwaitableTypeAttribute`. Returns of those types are ignored by MA0100. +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 +[assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(MyTaskResult))] +``` + +The match is exact-type only. Derived types are not excluded unless explicitly listed. + ## Exception for ExecutionContext.SuppressFlow() The rule does not alert when the disposable resource is `System.Threading.AsyncFlowControl` (returned by `ExecutionContext.SuppressFlow()`). This is safe because the execution context is captured at the moment of task creation, so the task doesn't need to be awaited before the using block ends. diff --git a/docs/Rules/MA0134.md b/docs/Rules/MA0134.md index 74be52503..a044addd7 100644 --- a/docs/Rules/MA0134.md +++ b/docs/Rules/MA0134.md @@ -16,3 +16,14 @@ void Sample() _ = Task.Delay(1); // ok } ```` + +## Configuration via annotations + +You can mark specific types as non-awaitable using `NonAwaitableTypeAttribute`. Calls returning those types are ignored by MA0134. +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 +[assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(MyTaskResult))] +``` + +The match is exact-type only. Derived types are not excluded unless explicitly listed. diff --git a/docs/Rules/MA0137.md b/docs/Rules/MA0137.md index 2412c6acd..a970ddc8a 100644 --- a/docs/Rules/MA0137.md +++ b/docs/Rules/MA0137.md @@ -26,3 +26,12 @@ Set to `false` to also report diagnostics on test methods: ````editorconfig MA0137.exclude_test_methods = false ```` + +You can also mark specific types as non-awaitable using `NonAwaitableTypeAttribute`. Methods returning those types are ignored by MA0137. +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 +[assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(MyTaskResult))] +``` + +The match is exact-type only. Derived types are not excluded unless explicitly listed. diff --git a/docs/Rules/MA0138.md b/docs/Rules/MA0138.md index 97fc1ccaf..da6669c24 100644 --- a/docs/Rules/MA0138.md +++ b/docs/Rules/MA0138.md @@ -12,3 +12,14 @@ void Foo() { } // non-compliant void FooAsync() { } ```` + +## Configuration via annotations + +You can mark specific types as non-awaitable using `NonAwaitableTypeAttribute`. Methods returning those types are considered non-awaitable by MA0138. +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 +[assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(MyTaskResult))] +``` + +The match is exact-type only. Derived types are not excluded unless explicitly listed. diff --git a/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj b/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj index 4922212fe..53bd6e92f 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.3.1 + 1.4.0 Annotations to configure Meziantou.Analyzer Meziantou.Analyzer, analyzers True diff --git a/src/Meziantou.Analyzer.Annotations/NonAwaitableTypeAttribute.cs b/src/Meziantou.Analyzer.Annotations/NonAwaitableTypeAttribute.cs new file mode 100644 index 000000000..6eafb783e --- /dev/null +++ b/src/Meziantou.Analyzer.Annotations/NonAwaitableTypeAttribute.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 NonAwaitableTypeAttribute : System.Attribute +{ + public NonAwaitableTypeAttribute(System.Type type) { } +} diff --git a/src/Meziantou.Analyzer.Annotations/README.md b/src/Meziantou.Analyzer.Annotations/README.md index cfdc9ddff..281399853 100644 --- a/src/Meziantou.Analyzer.Annotations/README.md +++ b/src/Meziantou.Analyzer.Annotations/README.md @@ -19,6 +19,7 @@ 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) | | `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) | @@ -31,3 +32,12 @@ Use `ExcludeFromBlockingCallAnalysisAttribute` to exclude specific MA0042/MA0045 [assembly: Meziantou.Analyzer.Annotations.ExcludeFromBlockingCallAnalysisAttribute("M:System.Threading.Tasks.Task.Wait")] [assembly: Meziantou.Analyzer.Annotations.ExcludeFromBlockingCallAnalysisAttribute(typeof(System.Threading.Thread), "Sleep", typeof(int))] ``` + +## NonAwaitableTypeAttribute + +Use `NonAwaitableTypeAttribute` to exclude MA0042/MA0045 `await`/`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.NonAwaitableTypeAttribute(typeof(System.Data.Common.DbCommand))] +``` diff --git a/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs b/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs index 8440877ca..bdbd25061 100644 --- a/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs +++ b/src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs @@ -70,4 +70,26 @@ public static bool IsExcludeFromBlockingCallAnalysisAttributeSymbol(ITypeSymbol? } }; } + + public static bool IsNonAwaitableTypeAttributeSymbol(ITypeSymbol? symbol) + { + // Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute + return symbol is INamedTypeSymbol + { + Name: "NonAwaitableTypeAttribute", + ContainingSymbol: INamespaceSymbol + { + Name: "Annotations", + ContainingSymbol: INamespaceSymbol + { + Name: "Analyzer", + ContainingSymbol: INamespaceSymbol + { + Name: "Meziantou", + ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } + } + } + } + }; + } } diff --git a/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs b/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs index bea041024..57c63e5a2 100644 --- a/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs +++ b/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs @@ -6,6 +6,7 @@ namespace Meziantou.Analyzer.Internals; internal sealed class AwaitableTypes { private readonly INamedTypeSymbol[] _taskOrValueTaskSymbols; + private readonly HashSet _nonAwaitableTypes; private readonly Compilation _compilation; private readonly ConcurrentDictionary _isAwaitableCache = new(SymbolEqualityComparer.Default); @@ -32,6 +33,7 @@ public AwaitableTypes(Compilation compilation) _taskOrValueTaskSymbols = []; } + _nonAwaitableTypes = CreateNonAwaitableTypes(compilation); _compilation = compilation; } @@ -42,6 +44,47 @@ public AwaitableTypes(Compilation compilation) public INamedTypeSymbol? IAsyncEnumerableSymbol { get; } public INamedTypeSymbol? IAsyncEnumeratorSymbol { get; } + public bool IsNonAwaitableType(ITypeSymbol? symbol) + { + if (_nonAwaitableTypes.Count == 0 || symbol is not INamedTypeSymbol namedType) + return false; + + return IsNonAwaitableTypeCore(namedType); + } + + private bool IsNonAwaitableTypeCore(INamedTypeSymbol type) + { + if (_nonAwaitableTypes.Contains(type)) + return true; + + if (!ReferenceEquals(type, type.OriginalDefinition) && _nonAwaitableTypes.Contains(type.OriginalDefinition)) + return true; + + return false; + } + + private static HashSet CreateNonAwaitableTypes(Compilation compilation) + { + var result = new HashSet(SymbolEqualityComparer.Default); + foreach (var attribute in compilation.Assembly.GetAttributes()) + { + if (!AnnotationAttributes.IsNonAwaitableTypeAttributeSymbol(attribute.AttributeClass)) + continue; + + var constructorArguments = attribute.ConstructorArguments; + if (constructorArguments is [{ Value: INamedTypeSymbol type }]) + { + result.Add(type); + if (!ReferenceEquals(type.OriginalDefinition, type)) + { + result.Add(type.OriginalDefinition); + } + } + } + + return result; + } + // https://github.com/dotnet/roslyn/blob/248e85149427c534c4a156a436ecff69bab83b59/src/Compilers/CSharp/Portable/Binder/Binder_Await.cs#L347 public bool IsAwaitable(ITypeSymbol? symbol, SemanticModel semanticModel, int position) { @@ -51,6 +94,9 @@ public bool IsAwaitable(ITypeSymbol? symbol, SemanticModel semanticModel, int po if (INotifyCompletionSymbol is null) return false; + if (IsNonAwaitableType(symbol)) + return false; + if (symbol.SpecialType is SpecialType.System_Void || symbol.TypeKind is TypeKind.Dynamic) return false; @@ -90,6 +136,9 @@ private bool IsAwaitableCore(ITypeSymbol symbol) if (INotifyCompletionSymbol is null) return false; + if (IsNonAwaitableType(symbol)) + return false; + if (symbol.SpecialType is SpecialType.System_Void || symbol.TypeKind is TypeKind.Dynamic) return false; diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index c115242c8..24b4b0fb4 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -653,16 +653,18 @@ private bool HasDisposeAsyncMethodDeclaredInSubclass(INamedTypeSymbol symbol, IN private bool CanBeAwaitUsing(IOperation operation, bool sqliteSpecialCasesEnabled, bool dbSpecialCasesEnabled) { var unwrappedOperation = operation.UnwrapImplicitConversionOperations(); - if (sqliteSpecialCasesEnabled && - unwrappedOperation is IInvocationOperation invocationOperation && - IsSqliteSpecialCaseMethod(invocationOperation)) + if (unwrappedOperation is IInvocationOperation invocationOperation) { - return false; + if (sqliteSpecialCasesEnabled && IsSqliteSpecialCaseMethod(invocationOperation)) + return false; } if (operation.GetActualType() is not INamedTypeSymbol type) return false; + if (_awaitableTypes.IsNonAwaitableType(type)) + return false; + var isSqliteSpecialCaseType = IsSqliteSpecialCaseType(type); // For Stream subclasses (including MemoryStream) created directly (new T()), only report diff --git a/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs index 0b280440b..72882aa32 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs @@ -156,6 +156,54 @@ Task A() .ValidateAsync(); } + [Fact] + public async Task Report_NonAwaitableTypeAttribute_TaskWrappedType_InSyncMethod() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(Result))] + + class Test + { + void A() + { + [|B()|]; + } + + Task B() => throw null; + } + + class Result { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Report_NonAwaitableTypeAttribute_OpenGenericTaskWrappedType_InSyncMethod() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(Result<>))] + + class Test + { + void A() + { + [|B()|]; + } + + Task> B() => throw null; + } + + class Result { } + """) + .ValidateAsync(); + } + [Fact] public async Task Report_TaskInSyncVoidMethod() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index c22f9a5aa..f8ffd4f2c 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -3586,4 +3586,191 @@ public void Dispose() { } """) .ValidateAsync(); } + + [Fact] + public async Task ExcludeFromBlockingCallAnalysisAttribute_MethodSignature_DoesNotAffectAwaitUsing() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System; + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.ExcludeFromBlockingCallAnalysisAttribute(typeof(Test), "Create")] + + class Test + { + public async Task A() + { + [|using var value = Create();|] + } + + private AsyncDisposable Create() => throw null; + } + + class AsyncDisposable : IDisposable, IAsyncDisposable + { + public void Dispose() { } + public ValueTask DisposeAsync() => default; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NonAwaitableTypeAttribute_DoesAffectAwaitUsing() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System; + 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 NonAwaitableTypeAttribute_DoesNotAffectTaskWrappedAwaitSuggestion() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(AwaitResult))] + + class Test + { + public async Task A() + { + [|Create()|]; + } + + private AwaitResult Create() => throw null; + private Task CreateAsync() => throw null; + } + + class AwaitResult { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NonAwaitableTypeAttribute_OpenGenericType_DoesNotAffectTaskWrappedAwaitSuggestion() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(AwaitResult<>))] + + class Test + { + public async Task A() + { + [|Create()|]; + } + + private AwaitResult Create() => throw null; + private Task> CreateAsync() => throw null; + } + + class AwaitResult { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NonAwaitableTypeAttribute_DoesNotAffectOtherTypes() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(OtherResult))] + + class Test + { + public async Task A() + { + [|Create()|]; + } + + private AwaitResult Create() => throw null; + private Task CreateAsync() => throw null; + } + + class AwaitResult { } + class OtherResult { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NonAwaitableTypeAttribute_DoesNotAffectDerivedType_AwaitUsing() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System; + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(BaseAsyncDisposable))] + + class Test + { + public async Task A() + { + [|using var value = new DerivedAsyncDisposable();|] + } + } + + class BaseAsyncDisposable : IDisposable, IAsyncDisposable + { + public void Dispose() { } + public ValueTask DisposeAsync() => default; + } + + class DerivedAsyncDisposable : BaseAsyncDisposable { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NonAwaitableTypeAttribute_DoesNotAffectDerivedType_AwaitSuggestion() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(BaseResult))] + + class Test + { + public async Task A() + { + [|Create()|]; + } + + private BaseResult Create() => throw null; + private Task CreateAsync() => throw null; + } + + class BaseResult { } + class DerivedResult : BaseResult { } + """) + .ValidateAsync(); + } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs index 048a33a33..e6d007f1f 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs @@ -388,4 +388,114 @@ private void A() """) .ValidateAsync(); } + + [Fact] + public async Task ExcludeFromBlockingCallAnalysisAttribute_MethodSignature_DoesNotAffectAwaitUsing() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System; + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.ExcludeFromBlockingCallAnalysisAttribute(typeof(Test), "Create")] + + class Test + { + private void A() + { + [|using var value = Create();|] + } + + private AsyncDisposable Create() => throw null; + } + + class AsyncDisposable : IDisposable, IAsyncDisposable + { + public void Dispose() { } + public ValueTask DisposeAsync() => default; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NonAwaitableTypeAttribute_DoesAffectAwaitUsing() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System; + 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 NonAwaitableTypeAttribute_DoesNotAffectTaskWrappedAwaitSuggestion() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(AwaitResult))] + + class Test + { + private void A() + { + [|Create()|]; + } + + private AwaitResult Create() => throw null; + private Task CreateAsync() => throw null; + } + + class AwaitResult { } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NonAwaitableTypeAttribute_DoesNotAffectDerivedType_AwaitUsing() + { + await CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System; + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(BaseAsyncDisposable))] + + class Test + { + private void A() + { + [|using var value = new DerivedAsyncDisposable();|] + } + } + + class BaseAsyncDisposable : IDisposable, IAsyncDisposable + { + public void Dispose() { } + public ValueTask DisposeAsync() => default; + } + + class DerivedAsyncDisposable : BaseAsyncDisposable { } + """) + .ValidateAsync(); + } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs index 78c75e0a0..dbb8f20f8 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs @@ -33,6 +33,41 @@ class TypeName } """) .ValidateAsync(); + + [Fact] + public Task AsyncMethodWithoutSuffix_NonAwaitableTypeAttribute_TaskWrappedType_Diagnostic() + => CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(Result))] + + class TypeName + { + Task {|MA0137:Test|}() => throw null; + } + + class Result { } + """) + .ValidateAsync(); + + [Fact] + public Task AsyncMethodWithoutSuffix_NonAwaitableTypeAttribute_OpenGenericTaskWrappedType_Diagnostic() + => CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(Result<>))] + + class TypeName + { + Task> {|MA0137:Test|}() => throw null; + } + + class Result { } + """) + .ValidateAsync(); + [Fact] public Task VoidMethodWithSuffix() => CreateProjectBuilder()