Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/Rules/MA0042.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
17 changes: 17 additions & 0 deletions docs/Rules/MA0045.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
11 changes: 11 additions & 0 deletions docs/Rules/MA0100.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions docs/Rules/MA0134.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
9 changes: 9 additions & 0 deletions docs/Rules/MA0137.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
11 changes: 11 additions & 0 deletions docs/Rules/MA0138.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Version>1.3.1</Version>
<Version>1.4.0</Version>
<Description>Annotations to configure Meziantou.Analyzer</Description>
<PackageTags>Meziantou.Analyzer, analyzers</PackageTags>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
Expand Down
12 changes: 12 additions & 0 deletions src/Meziantou.Analyzer.Annotations/NonAwaitableTypeAttribute.cs
Original file line number Diff line number Diff line change
@@ -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) { }
}
10 changes: 10 additions & 0 deletions src/Meziantou.Analyzer.Annotations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand All @@ -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))]
```
22 changes: 22 additions & 0 deletions src/Meziantou.Analyzer/Internals/AnnotationAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
}
}
};
}
}
49 changes: 49 additions & 0 deletions src/Meziantou.Analyzer/Internals/AwaitableTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Meziantou.Analyzer.Internals;
internal sealed class AwaitableTypes
{
private readonly INamedTypeSymbol[] _taskOrValueTaskSymbols;
private readonly HashSet<INamedTypeSymbol> _nonAwaitableTypes;
private readonly Compilation _compilation;
private readonly ConcurrentDictionary<ITypeSymbol, bool> _isAwaitableCache = new(SymbolEqualityComparer.Default);

Expand All @@ -32,6 +33,7 @@ public AwaitableTypes(Compilation compilation)
_taskOrValueTaskSymbols = [];
}

_nonAwaitableTypes = CreateNonAwaitableTypes(compilation);
_compilation = compilation;
}

Expand All @@ -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<INamedTypeSymbol> CreateNonAwaitableTypes(Compilation compilation)
{
var result = new HashSet<INamedTypeSymbol>(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)
{
Expand All @@ -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;

Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result> 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<Result<int>> B() => throw null;
}

class Result<T> { }
""")
.ValidateAsync();
}

[Fact]
public async Task Report_TaskInSyncVoidMethod()
{
Expand Down
Loading
Loading