From 1d163c1473787877046bc0c61fc105ef8890d737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 12 May 2026 13:33:19 -0400 Subject: [PATCH 01/10] Add NonAwaitableTypeAttribute support Add a dedicated annotations attribute to configure MA0042/MA0045 await exclusions by type, wire it into await and await-using analysis, and update tests and docs accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/Rules/MA0042.md | 14 +++ docs/Rules/MA0045.md | 14 +++ .../Meziantou.Analyzer.Annotations.csproj | 2 +- .../NonAwaitableTypeAttribute.cs | 12 ++ src/Meziantou.Analyzer.Annotations/README.md | 9 ++ .../Internals/AnnotationAttributes.cs | 22 ++++ ...otUseBlockingCallInAsyncContextAnalyzer.cs | 76 ++++++++++++- ...nAsyncContextAnalyzer_AsyncContextTests.cs | 107 ++++++++++++++++++ ...yncContextAnalyzer_NonAsyncContextTests.cs | 81 +++++++++++++ 9 files changed, 332 insertions(+), 5 deletions(-) create mode 100644 src/Meziantou.Analyzer.Annotations/NonAwaitableTypeAttribute.cs diff --git a/docs/Rules/MA0042.md b/docs/Rules/MA0042.md index 180c2c230..880035877 100644 --- a/docs/Rules/MA0042.md +++ b/docs/Rules/MA0042.md @@ -123,6 +123,20 @@ 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`: + +```csharp +[assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(System.Data.Common.DbCommand))] +``` + +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..f8cce8218 100644 --- a/docs/Rules/MA0045.md +++ b/docs/Rules/MA0045.md @@ -66,6 +66,20 @@ 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`: + +```csharp +[assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(System.Data.Common.DbCommand))] +``` + +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/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..b38c2d693 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) | | `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,11 @@ 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. + +```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/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index c115242c8..6a5c8e37a 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 _excludedAwaitTypes; 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); + _excludedAwaitTypes = CreateExcludedAwaitTypes(compilation); } private ISymbol? StreamSymbol { get; } @@ -419,6 +421,28 @@ static void AddExcludedSymbol(HashSet symbols, ISymbol symbol) } } + private static HashSet CreateExcludedAwaitTypes(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; + } + private bool IsExcludedDiagnosticSymbol(ISymbol symbol) { if (_excludedDiagnosticSymbols.Count == 0) @@ -431,6 +455,42 @@ private bool IsExcludedDiagnosticSymbol(ISymbol symbol) return !ReferenceEquals(symbol, originalDefinition) && _excludedDiagnosticSymbols.Contains(originalDefinition); } + private bool IsExcludedAwaitType(ITypeSymbol? type) + { + if (_excludedAwaitTypes.Count == 0 || type is not INamedTypeSymbol namedType) + return false; + + if (IsExcludedAwaitTypeCore(namedType)) + return true; + + if (namedType is { TypeArguments.Length: 1 } && + namedType.OriginalDefinition.IsEqualToAny(TaskOfTSymbol, ValueTaskOfTSymbol) && + namedType.TypeArguments[0] is INamedTypeSymbol awaitedType && + IsExcludedAwaitTypeCore(awaitedType)) + { + return true; + } + + return false; + } + + private bool IsExcludedAwaitTypeCore(INamedTypeSymbol type) + { + if (_excludedAwaitTypes.Contains(type)) + return true; + + if (!ReferenceEquals(type, type.OriginalDefinition) && _excludedAwaitTypes.Contains(type.OriginalDefinition)) + return true; + + foreach (var excludedType in _excludedAwaitTypes) + { + if (type.InheritsFrom(excludedType)) + return true; + } + + return false; + } + private bool IsSqliteSpecialCaseType(INamedTypeSymbol type) { return type.IsEqualToAny(SqliteConnectionSymbol, SqliteCommandSymbol, SqliteDataReaderSymbol); @@ -495,6 +555,9 @@ private bool IsPotentialAsyncEquivalent(IInvocationOperation operation, IMethodS if (!_awaitableTypes.IsAwaitable(methodSymbol.ReturnType, operation.SemanticModel!, operation.Syntax.SpanStart)) return false; + if (IsExcludedAwaitType(methodSymbol.ReturnType)) + return false; + // In test projects, exclude async methods from Meziantou.Framework.TemporaryDirectory if (IsTestProject && TemporaryDirectorySymbol is not null && methodSymbol.ContainingType.IsEqualTo(TemporaryDirectorySymbol)) return false; @@ -653,16 +716,21 @@ 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 (IsExcludedDiagnosticSymbol(invocationOperation.TargetMethod)) + return false; + + if (sqliteSpecialCasesEnabled && IsSqliteSpecialCaseMethod(invocationOperation)) + return false; } if (operation.GetActualType() is not INamedTypeSymbol type) return false; + if (IsExcludedAwaitType(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/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index c22f9a5aa..fb3167ce8 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -3586,4 +3586,111 @@ public void Dispose() { } """) .ValidateAsync(); } + + [Fact] + public async Task ExcludeFromBlockingCallAnalysisAttribute_MethodSignature_AffectsAwaitUsing() + { + 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_DoesAffectAwaitSuggestion() + { + 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(); + } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs index 048a33a33..3014ed461 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs @@ -388,4 +388,85 @@ private void A() """) .ValidateAsync(); } + + [Fact] + public async Task ExcludeFromBlockingCallAnalysisAttribute_MethodSignature_AffectsAwaitUsing() + { + 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_DoesAffectAwaitSuggestion() + { + 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(); + } } From 4e397df438c2abc8671890cf6bc138146f5ac49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 12 May 2026 13:43:54 -0400 Subject: [PATCH 02/10] Restrict NonAwaitableTypeAttribute to exact types Limit NonAwaitableTypeAttribute matching to the declared type instead of its hierarchy, add regression tests for derived await/await-using cases, and document exact-type semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/Rules/MA0042.md | 2 + docs/Rules/MA0045.md | 2 + src/Meziantou.Analyzer.Annotations/README.md | 1 + ...otUseBlockingCallInAsyncContextAnalyzer.cs | 6 -- ...nAsyncContextAnalyzer_AsyncContextTests.cs | 55 +++++++++++++++++++ ...yncContextAnalyzer_NonAsyncContextTests.cs | 29 ++++++++++ 6 files changed, 89 insertions(+), 6 deletions(-) diff --git a/docs/Rules/MA0042.md b/docs/Rules/MA0042.md index 880035877..ea2e211cc 100644 --- a/docs/Rules/MA0042.md +++ b/docs/Rules/MA0042.md @@ -129,6 +129,8 @@ You can also exclude `await` / `await using` recommendations for specific types [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 diff --git a/docs/Rules/MA0045.md b/docs/Rules/MA0045.md index f8cce8218..90d9ab7e7 100644 --- a/docs/Rules/MA0045.md +++ b/docs/Rules/MA0045.md @@ -72,6 +72,8 @@ You can also exclude `await` / `await using` recommendations for specific types [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 diff --git a/src/Meziantou.Analyzer.Annotations/README.md b/src/Meziantou.Analyzer.Annotations/README.md index b38c2d693..f5b13a459 100644 --- a/src/Meziantou.Analyzer.Annotations/README.md +++ b/src/Meziantou.Analyzer.Annotations/README.md @@ -36,6 +36,7 @@ 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. +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/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index 6a5c8e37a..6210b9a57 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -482,12 +482,6 @@ private bool IsExcludedAwaitTypeCore(INamedTypeSymbol type) if (!ReferenceEquals(type, type.OriginalDefinition) && _excludedAwaitTypes.Contains(type.OriginalDefinition)) return true; - foreach (var excludedType in _excludedAwaitTypes) - { - if (type.InheritsFrom(excludedType)) - return true; - } - return false; } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index fb3167ce8..bb8b2b9b9 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -3693,4 +3693,59 @@ 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 3014ed461..219612fb7 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs @@ -469,4 +469,33 @@ 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(); + } } From 2c5c0e87027ebfca8704426272407e61375d8b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 12 May 2026 13:56:20 -0400 Subject: [PATCH 03/10] Move non-awaitable exclusions to AwaitableTypes Centralize NonAwaitableTypeAttribute parsing/matching in AwaitableTypes so rules sharing awaitable detection can reuse it, and update MA0042/MA0045 to consume the shared API. Add regression coverage for MA0134 and MA0137 behavior with NonAwaitableTypeAttribute. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Meziantou.Analyzer.Annotations/README.md | 2 +- .../Internals/AwaitableTypes.cs | 67 ++++++++++++++++++- ...otUseBlockingCallInAsyncContextAnalyzer.cs | 59 +--------------- ...waitableMethodInSyncMethodAnalyzerTests.cs | 24 +++++++ ...TypeMustHaveTheAsyncSuffixAnalyzerTests.cs | 18 +++++ 5 files changed, 109 insertions(+), 61 deletions(-) diff --git a/src/Meziantou.Analyzer.Annotations/README.md b/src/Meziantou.Analyzer.Annotations/README.md index f5b13a459..281399853 100644 --- a/src/Meziantou.Analyzer.Annotations/README.md +++ b/src/Meziantou.Analyzer.Annotations/README.md @@ -19,7 +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) | +| `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) | diff --git a/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs b/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs index bea041024..cc829a359 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); @@ -17,14 +18,16 @@ public AwaitableTypes(Compilation compilation) IAsyncEnumeratorSymbol = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerator`1"); TaskSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task"); TaskOfTSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1"); + var valueTaskSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask"); + ValueTaskOfTSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); if (INotifyCompletionSymbol is not null) { var taskLikeSymbols = new List(4); taskLikeSymbols.AddIfNotNull(TaskSymbol); taskLikeSymbols.AddIfNotNull(TaskOfTSymbol); - taskLikeSymbols.AddIfNotNull(compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask")); - taskLikeSymbols.AddIfNotNull(compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask`1")); + taskLikeSymbols.AddIfNotNull(valueTaskSymbol); + taskLikeSymbols.AddIfNotNull(ValueTaskOfTSymbol); _taskOrValueTaskSymbols = [.. taskLikeSymbols]; } else @@ -32,16 +35,70 @@ public AwaitableTypes(Compilation compilation) _taskOrValueTaskSymbols = []; } + _nonAwaitableTypes = CreateNonAwaitableTypes(compilation); _compilation = compilation; } private INamedTypeSymbol? TaskSymbol { get; } private INamedTypeSymbol? TaskOfTSymbol { get; } + private INamedTypeSymbol? ValueTaskOfTSymbol { get; } private INamedTypeSymbol? INotifyCompletionSymbol { get; } private INamedTypeSymbol? AsyncMethodBuilderAttributeSymbol { get; } public INamedTypeSymbol? IAsyncEnumerableSymbol { get; } public INamedTypeSymbol? IAsyncEnumeratorSymbol { get; } + public bool IsNonAwaitableType(ITypeSymbol? symbol) + { + if (_nonAwaitableTypes.Count == 0 || symbol is not INamedTypeSymbol namedType) + return false; + + if (IsNonAwaitableTypeCore(namedType)) + return true; + + if (namedType is { TypeArguments.Length: 1 } && + namedType.OriginalDefinition.IsEqualToAny(TaskOfTSymbol, ValueTaskOfTSymbol) && + namedType.TypeArguments[0] is INamedTypeSymbol awaitedType && + IsNonAwaitableTypeCore(awaitedType)) + { + return true; + } + + return false; + } + + 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 +108,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 +150,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 6210b9a57..722a8e568 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -59,7 +59,6 @@ private sealed class Context private readonly INamedTypeSymbol[] _taskAwaiterLikeSymbols; private readonly HashSet _excludedDiagnosticSymbols; - private readonly HashSet _excludedAwaitTypes; private readonly ConcurrentHashSet _symbolsWithNoAsyncOverloads = new(SymbolEqualityComparer.Default); public Context(Compilation compilation) @@ -133,7 +132,6 @@ public Context(Compilation compilation) taskAwaiterLikeSymbols.AddIfNotNull(ValueTaskAwaiterOfTSymbol); _taskAwaiterLikeSymbols = [.. taskAwaiterLikeSymbols]; _excludedDiagnosticSymbols = CreateExcludedDiagnosticSymbols(compilation); - _excludedAwaitTypes = CreateExcludedAwaitTypes(compilation); } private ISymbol? StreamSymbol { get; } @@ -421,28 +419,6 @@ static void AddExcludedSymbol(HashSet symbols, ISymbol symbol) } } - private static HashSet CreateExcludedAwaitTypes(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; - } - private bool IsExcludedDiagnosticSymbol(ISymbol symbol) { if (_excludedDiagnosticSymbols.Count == 0) @@ -455,36 +431,6 @@ private bool IsExcludedDiagnosticSymbol(ISymbol symbol) return !ReferenceEquals(symbol, originalDefinition) && _excludedDiagnosticSymbols.Contains(originalDefinition); } - private bool IsExcludedAwaitType(ITypeSymbol? type) - { - if (_excludedAwaitTypes.Count == 0 || type is not INamedTypeSymbol namedType) - return false; - - if (IsExcludedAwaitTypeCore(namedType)) - return true; - - if (namedType is { TypeArguments.Length: 1 } && - namedType.OriginalDefinition.IsEqualToAny(TaskOfTSymbol, ValueTaskOfTSymbol) && - namedType.TypeArguments[0] is INamedTypeSymbol awaitedType && - IsExcludedAwaitTypeCore(awaitedType)) - { - return true; - } - - return false; - } - - private bool IsExcludedAwaitTypeCore(INamedTypeSymbol type) - { - if (_excludedAwaitTypes.Contains(type)) - return true; - - if (!ReferenceEquals(type, type.OriginalDefinition) && _excludedAwaitTypes.Contains(type.OriginalDefinition)) - return true; - - return false; - } - private bool IsSqliteSpecialCaseType(INamedTypeSymbol type) { return type.IsEqualToAny(SqliteConnectionSymbol, SqliteCommandSymbol, SqliteDataReaderSymbol); @@ -549,9 +495,6 @@ private bool IsPotentialAsyncEquivalent(IInvocationOperation operation, IMethodS if (!_awaitableTypes.IsAwaitable(methodSymbol.ReturnType, operation.SemanticModel!, operation.Syntax.SpanStart)) return false; - if (IsExcludedAwaitType(methodSymbol.ReturnType)) - return false; - // In test projects, exclude async methods from Meziantou.Framework.TemporaryDirectory if (IsTestProject && TemporaryDirectorySymbol is not null && methodSymbol.ContainingType.IsEqualTo(TemporaryDirectorySymbol)) return false; @@ -722,7 +665,7 @@ private bool CanBeAwaitUsing(IOperation operation, bool sqliteSpecialCasesEnable if (operation.GetActualType() is not INamedTypeSymbol type) return false; - if (IsExcludedAwaitType(type)) + if (_awaitableTypes.IsNonAwaitableType(type)) return false; var isSqliteSpecialCaseType = IsSqliteSpecialCaseType(type); diff --git a/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs index 0b280440b..9b865534b 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs @@ -156,6 +156,30 @@ Task A() .ValidateAsync(); } + [Fact] + public async Task NoReport_NonAwaitableTypeAttribute_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/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs index 78c75e0a0..80d71b5dd 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs @@ -33,6 +33,24 @@ class TypeName } """) .ValidateAsync(); + + [Fact] + public Task AsyncMethodWithoutSuffix_NonAwaitableTypeAttribute_NoDiagnostic() + => CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(Result))] + + class TypeName + { + Task Test() => throw null; + } + + class Result { } + """) + .ValidateAsync(); + [Fact] public Task VoidMethodWithSuffix() => CreateProjectBuilder() From 05565912a6fd64a0b29d054d3240877e24890ac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 12 May 2026 14:14:20 -0400 Subject: [PATCH 04/10] Document NonAwaitableType on affected rules Update MA0100, MA0134, MA0137, and MA0138 documentation to describe NonAwaitableTypeAttribute support and exact-type matching behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/Rules/MA0100.md | 10 ++++++++++ docs/Rules/MA0134.md | 10 ++++++++++ docs/Rules/MA0137.md | 8 ++++++++ docs/Rules/MA0138.md | 10 ++++++++++ 4 files changed, 38 insertions(+) diff --git a/docs/Rules/MA0100.md b/docs/Rules/MA0100.md index c788f7ef1..86682cc3e 100644 --- a/docs/Rules/MA0100.md +++ b/docs/Rules/MA0100.md @@ -25,6 +25,16 @@ class TestClass } ```` +## Configuration via annotations + +You can mark specific types as non-awaitable using `NonAwaitableTypeAttribute`. Returns of those types are ignored by MA0100. + +```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..ac3175e8b 100644 --- a/docs/Rules/MA0134.md +++ b/docs/Rules/MA0134.md @@ -16,3 +16,13 @@ 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. + +```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..3f2e45dd7 100644 --- a/docs/Rules/MA0137.md +++ b/docs/Rules/MA0137.md @@ -26,3 +26,11 @@ 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. + +```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..c5f08c6ae 100644 --- a/docs/Rules/MA0138.md +++ b/docs/Rules/MA0138.md @@ -12,3 +12,13 @@ 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. + +```csharp +[assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(MyTaskResult))] +``` + +The match is exact-type only. Derived types are not excluded unless explicitly listed. From b254b80adde6071b9d8ff0662787e1fe1ad1ce65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 12 May 2026 14:17:10 -0400 Subject: [PATCH 05/10] Add open-generic NonAwaitableType tests Add test coverage for NonAwaitableTypeAttribute(typeof(T<>)) so open generic declarations are validated across MA0042, MA0134, and MA0137 behaviors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...waitableMethodInSyncMethodAnalyzerTests.cs | 24 ++++++++++++++++++ ...nAsyncContextAnalyzer_AsyncContextTests.cs | 25 +++++++++++++++++++ ...TypeMustHaveTheAsyncSuffixAnalyzerTests.cs | 17 +++++++++++++ 3 files changed, 66 insertions(+) diff --git a/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs index 9b865534b..342e3aafc 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs @@ -180,6 +180,30 @@ class Result { } .ValidateAsync(); } + [Fact] + public async Task NoReport_NonAwaitableTypeAttribute_OpenGenericType_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 bb8b2b9b9..6e3257955 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -3668,6 +3668,31 @@ class AwaitResult { } .ValidateAsync(); } + [Fact] + public async Task NonAwaitableTypeAttribute_OpenGenericType_DoesAffectAwaitSuggestion() + { + 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() { diff --git a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs index 80d71b5dd..e87a38c37 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs @@ -51,6 +51,23 @@ class Result { } """) .ValidateAsync(); + [Fact] + public Task AsyncMethodWithoutSuffix_NonAwaitableTypeAttribute_OpenGenericType_NoDiagnostic() + => CreateProjectBuilder() + .AddMeziantouAttributes() + .WithSourceCode(""" + using System.Threading.Tasks; + [assembly: Meziantou.Analyzer.Annotations.NonAwaitableTypeAttribute(typeof(Result<>))] + + class TypeName + { + Task> Test() => throw null; + } + + class Result { } + """) + .ValidateAsync(); + [Fact] public Task VoidMethodWithSuffix() => CreateProjectBuilder() From 5c12609a6eab561df99bed2ec7b74bb76be7b20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 12 May 2026 14:31:33 -0400 Subject: [PATCH 06/10] Add annotations README links in rule docs Ensure each rule doc mentioning NonAwaitableTypeAttribute also references the Meziantou.Analyzer.Annotations README, following the MA0042 pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/Rules/MA0042.md | 1 + docs/Rules/MA0045.md | 1 + docs/Rules/MA0100.md | 1 + docs/Rules/MA0134.md | 1 + docs/Rules/MA0137.md | 1 + docs/Rules/MA0138.md | 1 + 6 files changed, 6 insertions(+) diff --git a/docs/Rules/MA0042.md b/docs/Rules/MA0042.md index ea2e211cc..410fa1a9b 100644 --- a/docs/Rules/MA0042.md +++ b/docs/Rules/MA0042.md @@ -124,6 +124,7 @@ The attribute supports: - `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))] diff --git a/docs/Rules/MA0045.md b/docs/Rules/MA0045.md index 90d9ab7e7..df8404427 100644 --- a/docs/Rules/MA0045.md +++ b/docs/Rules/MA0045.md @@ -67,6 +67,7 @@ The attribute supports: - `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))] diff --git a/docs/Rules/MA0100.md b/docs/Rules/MA0100.md index 86682cc3e..8dba811d4 100644 --- a/docs/Rules/MA0100.md +++ b/docs/Rules/MA0100.md @@ -28,6 +28,7 @@ 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))] diff --git a/docs/Rules/MA0134.md b/docs/Rules/MA0134.md index ac3175e8b..a044addd7 100644 --- a/docs/Rules/MA0134.md +++ b/docs/Rules/MA0134.md @@ -20,6 +20,7 @@ void Sample() ## 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))] diff --git a/docs/Rules/MA0137.md b/docs/Rules/MA0137.md index 3f2e45dd7..a970ddc8a 100644 --- a/docs/Rules/MA0137.md +++ b/docs/Rules/MA0137.md @@ -28,6 +28,7 @@ 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))] diff --git a/docs/Rules/MA0138.md b/docs/Rules/MA0138.md index c5f08c6ae..da6669c24 100644 --- a/docs/Rules/MA0138.md +++ b/docs/Rules/MA0138.md @@ -16,6 +16,7 @@ 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))] From eb7e27158f9ba99500a511471293ce486c15e14b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 12 May 2026 14:51:23 -0400 Subject: [PATCH 07/10] Do not unwrap Task in NonAwaitableType matching Adjust AwaitableTypes to evaluate only the provided type in IsNonAwaitableType, without unwrapping Task/ValueTask. Update rule tests to reflect task-wrapped types are no longer excluded by inner type annotations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Meziantou.Analyzer/Internals/AwaitableTypes.cs | 13 +------------ ...AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs | 8 ++++---- ...gCallInAsyncContextAnalyzer_AsyncContextTests.cs | 8 ++++---- ...llInAsyncContextAnalyzer_NonAsyncContextTests.cs | 4 ++-- ...itableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs | 8 ++++---- 5 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs b/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs index cc829a359..e90fdef5c 100644 --- a/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs +++ b/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs @@ -52,18 +52,7 @@ public bool IsNonAwaitableType(ITypeSymbol? symbol) if (_nonAwaitableTypes.Count == 0 || symbol is not INamedTypeSymbol namedType) return false; - if (IsNonAwaitableTypeCore(namedType)) - return true; - - if (namedType is { TypeArguments.Length: 1 } && - namedType.OriginalDefinition.IsEqualToAny(TaskOfTSymbol, ValueTaskOfTSymbol) && - namedType.TypeArguments[0] is INamedTypeSymbol awaitedType && - IsNonAwaitableTypeCore(awaitedType)) - { - return true; - } - - return false; + return IsNonAwaitableTypeCore(namedType); } private bool IsNonAwaitableTypeCore(INamedTypeSymbol type) diff --git a/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs index 342e3aafc..72882aa32 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/AwaitAwaitableMethodInSyncMethodAnalyzerTests.cs @@ -157,7 +157,7 @@ Task A() } [Fact] - public async Task NoReport_NonAwaitableTypeAttribute_InSyncMethod() + public async Task Report_NonAwaitableTypeAttribute_TaskWrappedType_InSyncMethod() { await CreateProjectBuilder() .AddMeziantouAttributes() @@ -169,7 +169,7 @@ class Test { void A() { - B(); + [|B()|]; } Task B() => throw null; @@ -181,7 +181,7 @@ class Result { } } [Fact] - public async Task NoReport_NonAwaitableTypeAttribute_OpenGenericType_InSyncMethod() + public async Task Report_NonAwaitableTypeAttribute_OpenGenericTaskWrappedType_InSyncMethod() { await CreateProjectBuilder() .AddMeziantouAttributes() @@ -193,7 +193,7 @@ class Test { void A() { - B(); + [|B()|]; } Task> B() => throw null; diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index 6e3257955..72836e2e9 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -3644,7 +3644,7 @@ public void Dispose() { } } [Fact] - public async Task NonAwaitableTypeAttribute_DoesAffectAwaitSuggestion() + public async Task NonAwaitableTypeAttribute_DoesNotAffectTaskWrappedAwaitSuggestion() { await CreateProjectBuilder() .AddMeziantouAttributes() @@ -3656,7 +3656,7 @@ class Test { public async Task A() { - Create(); + [|Create()|]; } private AwaitResult Create() => throw null; @@ -3669,7 +3669,7 @@ class AwaitResult { } } [Fact] - public async Task NonAwaitableTypeAttribute_OpenGenericType_DoesAffectAwaitSuggestion() + public async Task NonAwaitableTypeAttribute_OpenGenericType_DoesNotAffectTaskWrappedAwaitSuggestion() { await CreateProjectBuilder() .AddMeziantouAttributes() @@ -3681,7 +3681,7 @@ class Test { public async Task A() { - Create(); + [|Create()|]; } private AwaitResult Create() => throw null; diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs index 219612fb7..892fa6f8e 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs @@ -446,7 +446,7 @@ public void Dispose() { } } [Fact] - public async Task NonAwaitableTypeAttribute_DoesAffectAwaitSuggestion() + public async Task NonAwaitableTypeAttribute_DoesNotAffectTaskWrappedAwaitSuggestion() { await CreateProjectBuilder() .AddMeziantouAttributes() @@ -458,7 +458,7 @@ class Test { private void A() { - Create(); + [|Create()|]; } private AwaitResult Create() => throw null; diff --git a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs index e87a38c37..dbb8f20f8 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs @@ -35,7 +35,7 @@ class TypeName .ValidateAsync(); [Fact] - public Task AsyncMethodWithoutSuffix_NonAwaitableTypeAttribute_NoDiagnostic() + public Task AsyncMethodWithoutSuffix_NonAwaitableTypeAttribute_TaskWrappedType_Diagnostic() => CreateProjectBuilder() .AddMeziantouAttributes() .WithSourceCode(""" @@ -44,7 +44,7 @@ public Task AsyncMethodWithoutSuffix_NonAwaitableTypeAttribute_NoDiagnostic() class TypeName { - Task Test() => throw null; + Task {|MA0137:Test|}() => throw null; } class Result { } @@ -52,7 +52,7 @@ class Result { } .ValidateAsync(); [Fact] - public Task AsyncMethodWithoutSuffix_NonAwaitableTypeAttribute_OpenGenericType_NoDiagnostic() + public Task AsyncMethodWithoutSuffix_NonAwaitableTypeAttribute_OpenGenericTaskWrappedType_Diagnostic() => CreateProjectBuilder() .AddMeziantouAttributes() .WithSourceCode(""" @@ -61,7 +61,7 @@ public Task AsyncMethodWithoutSuffix_NonAwaitableTypeAttribute_OpenGenericType_N class TypeName { - Task> Test() => throw null; + Task> {|MA0137:Test|}() => throw null; } class Result { } From 24f6aefb5633f6c947921aa425f8f9659ffae632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 12 May 2026 15:05:00 -0400 Subject: [PATCH 08/10] Inline ValueTask symbol lookups Inline ValueTask and ValueTask generic symbol lookups directly in the constructor branch and remove temporary locals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Meziantou.Analyzer/Internals/AwaitableTypes.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs b/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs index e90fdef5c..57c63e5a2 100644 --- a/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs +++ b/src/Meziantou.Analyzer/Internals/AwaitableTypes.cs @@ -18,16 +18,14 @@ public AwaitableTypes(Compilation compilation) IAsyncEnumeratorSymbol = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IAsyncEnumerator`1"); TaskSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task"); TaskOfTSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task`1"); - var valueTaskSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask"); - ValueTaskOfTSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask`1"); if (INotifyCompletionSymbol is not null) { var taskLikeSymbols = new List(4); taskLikeSymbols.AddIfNotNull(TaskSymbol); taskLikeSymbols.AddIfNotNull(TaskOfTSymbol); - taskLikeSymbols.AddIfNotNull(valueTaskSymbol); - taskLikeSymbols.AddIfNotNull(ValueTaskOfTSymbol); + taskLikeSymbols.AddIfNotNull(compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask")); + taskLikeSymbols.AddIfNotNull(compilation.GetBestTypeByMetadataName("System.Threading.Tasks.ValueTask`1")); _taskOrValueTaskSymbols = [.. taskLikeSymbols]; } else @@ -41,7 +39,6 @@ public AwaitableTypes(Compilation compilation) private INamedTypeSymbol? TaskSymbol { get; } private INamedTypeSymbol? TaskOfTSymbol { get; } - private INamedTypeSymbol? ValueTaskOfTSymbol { get; } private INamedTypeSymbol? INotifyCompletionSymbol { get; } private INamedTypeSymbol? AsyncMethodBuilderAttributeSymbol { get; } public INamedTypeSymbol? IAsyncEnumerableSymbol { get; } From 99220b8a505480c365761b05e42fc9e40d71dd9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 12 May 2026 15:30:22 -0400 Subject: [PATCH 09/10] wip --- .../Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index 722a8e568..24b4b0fb4 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -655,9 +655,6 @@ private bool CanBeAwaitUsing(IOperation operation, bool sqliteSpecialCasesEnable var unwrappedOperation = operation.UnwrapImplicitConversionOperations(); if (unwrappedOperation is IInvocationOperation invocationOperation) { - if (IsExcludedDiagnosticSymbol(invocationOperation.TargetMethod)) - return false; - if (sqliteSpecialCasesEnabled && IsSqliteSpecialCaseMethod(invocationOperation)) return false; } From ec030f5f1fb9768c76185ddad71de7f557397469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 12 May 2026 15:45:30 -0400 Subject: [PATCH 10/10] Update await-using exclusion tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...UseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs | 4 ++-- ...BlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index 72836e2e9..f8ffd4f2c 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -3588,7 +3588,7 @@ public void Dispose() { } } [Fact] - public async Task ExcludeFromBlockingCallAnalysisAttribute_MethodSignature_AffectsAwaitUsing() + public async Task ExcludeFromBlockingCallAnalysisAttribute_MethodSignature_DoesNotAffectAwaitUsing() { await CreateProjectBuilder() .AddMeziantouAttributes() @@ -3601,7 +3601,7 @@ class Test { public async Task A() { - using var value = Create(); + [|using var value = Create();|] } private AsyncDisposable Create() => throw null; diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs index 892fa6f8e..e6d007f1f 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs @@ -390,7 +390,7 @@ private void A() } [Fact] - public async Task ExcludeFromBlockingCallAnalysisAttribute_MethodSignature_AffectsAwaitUsing() + public async Task ExcludeFromBlockingCallAnalysisAttribute_MethodSignature_DoesNotAffectAwaitUsing() { await CreateProjectBuilder() .AddMeziantouAttributes() @@ -403,7 +403,7 @@ class Test { private void A() { - using var value = Create(); + [|using var value = Create();|] } private AsyncDisposable Create() => throw null;