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
14 changes: 10 additions & 4 deletions docs/Rules/MA0042.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ The rule does not report a diagnostic for method invocations on the following SQ

SQLite async APIs have documented limitations. See [Async limitations](https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/async) for details.

The rule will not report a diagnostic for a `using` statement on a `Stream`, `TextWriter`, `DbConnection`, `DbCommand`, `DbDataReader`, `DbTransaction`, or `DbBatch` subclass that is directly instantiated with `new` when the concrete type does not override `DisposeAsync`. The base implementations of `DisposeAsync` for these types call `Dispose()` synchronously by default, so switching to `await using` brings no benefit for such types. When the instance is obtained from a factory method rather than a direct `new` expression, the rule still reports a diagnostic because the runtime type may be a deeper subclass that does override `DisposeAsync`.
The rule will not report a diagnostic for a `using` statement on a `Stream` or `TextWriter` subclass that is directly instantiated with `new` when the concrete type does not override `DisposeAsync`.

For `DbConnection`, `DbCommand`, `DbDataReader`, `DbTransaction`, and `DbBatch`, the default behavior is less strict: the analyzer treats the compile-time Db\* hierarchy as effectively sealed and does not report a diagnostic unless the known type hierarchy already overrides `DisposeAsync`. This avoids common false positives (for example `SqlDataReader`) where `DisposeAsync` falls back to synchronous disposal in base implementations. You can disable this behavior if your code relies on runtime-polymorphic Db\* implementations.

````csharp
public async Task Sample()
Expand All @@ -74,19 +76,19 @@ public async Task Sample()
// No diagnostic: SqlConnection does not override DisposeAsync.
using var connection1 = new SqlConnection(connectionString);

// Diagnostic: obtained from a factory, runtime type may override DisposeAsync.
// No diagnostic by default: Db* types are treated as effectively sealed.
using var connection2 = CreateConnection();

// No diagnostic: SqlCommand does not override DisposeAsync.
using var command1 = new SqlCommand();

// Diagnostic: obtained from a factory, runtime type may override DisposeAsync.
// No diagnostic by default: Db* types are treated as effectively sealed.
using var command2 = CreateCommand();

// No diagnostic: DataTableReader does not override DisposeAsync.
using var reader1 = new DataTableReader(new DataTable());

// Diagnostic: obtained from a factory, runtime type may override DisposeAsync.
// No diagnostic by default: Db* types are treated as effectively sealed.
using var reader2 = CreateReader();

// No diagnostic: StringWriter does not override DisposeAsync.
Expand All @@ -103,6 +105,10 @@ public async Task Sample()
# .editorconfig file
# Enable SQLite special-cases for MA0042/MA0045. default: true
MA0042.enable_sqlite_special_cases = true

# Assume DbConnection/DbCommand/DbDataReader/DbTransaction/DbBatch hierarchies are sealed
# for await-using analysis. default: true
MA0042.enable_db_special_cases = true
````

## Additional resources
Expand Down
11 changes: 11 additions & 0 deletions docs/Rules/MA0045.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,23 @@ This rule shares the same SQLite special-cases as [MA0042](MA0042.md) by default
- `SqliteCommand` method invocations
- `SqliteDataReader` method invocations

It also shares the same Db\* await-using special-cases as MA0042 by default for:
- `DbConnection`
- `DbCommand`
- `DbDataReader`
- `DbTransaction`
- `DbBatch`

## Configuration

````
# .editorconfig file
# Enable SQLite special-cases for MA0042/MA0045. default: true
MA0042.enable_sqlite_special_cases = true

# Assume DbConnection/DbCommand/DbDataReader/DbTransaction/DbBatch hierarchies are sealed
# for await-using analysis. default: true
MA0042.enable_db_special_cases = true
````

## Additional resources
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,12 @@ private static bool IsSqliteSpecialCasesEnabled(OperationAnalysisContext context
return context.Options.GetConfigurationValue(operation, RuleIdentifiers.DoNotUseBlockingCall + ".enable_sqlite_special_cases", defaultValue);
}

private static bool IsDbSpecialCasesEnabled(OperationAnalysisContext context, IOperation operation)
{
var defaultValue = context.Options.GetConfigurationValue(operation, RuleIdentifiers.DoNotUseBlockingCallInAsyncContext + ".enable_db_special_cases", defaultValue: true);
return context.Options.GetConfigurationValue(operation, RuleIdentifiers.DoNotUseBlockingCall + ".enable_db_special_cases", defaultValue);
}

private bool IsSqliteSpecialCaseType(INamedTypeSymbol type)
{
return type.IsEqualToAny(SqliteConnectionSymbol, SqliteCommandSymbol, SqliteDataReaderSymbol);
Expand Down Expand Up @@ -545,7 +551,7 @@ private bool HasDisposeAsyncMethodDeclaredInSubclass(INamedTypeSymbol symbol, IN
return false;
}

private bool CanBeAwaitUsing(IOperation operation, bool sqliteSpecialCasesEnabled)
private bool CanBeAwaitUsing(IOperation operation, bool sqliteSpecialCasesEnabled, bool dbSpecialCasesEnabled)
{
var unwrappedOperation = operation.UnwrapImplicitConversionOperations();
if (sqliteSpecialCasesEnabled &&
Expand All @@ -558,6 +564,8 @@ unwrappedOperation is IInvocationOperation invocationOperation &&
if (operation.GetActualType() is not INamedTypeSymbol type)
return false;

var isSqliteSpecialCaseType = IsSqliteSpecialCaseType(type);

// For Stream subclasses (including MemoryStream) created directly (new T()), only report
// if the concrete type being instantiated (or an intermediate subclass up to but not
// including Stream) actually overrides DisposeAsync. Stream.DisposeAsync merely calls
Expand All @@ -568,53 +576,53 @@ unwrappedOperation is IInvocationOperation invocationOperation &&
return HasDisposeAsyncMethodDeclaredInSubclass(type, streamSymbol);
}

// For DbConnection subclasses created directly (new T()), only report if the exact
// type being instantiated (or an intermediate subclass up to but not including
// DbConnection) actually overrides DisposeAsync. DbConnection.DisposeAsync just calls
// Dispose() synchronously, so it is not a meaningful async override.
// DbConnection DisposeAsync has a synchronous fallback in the base implementation.
// When db special-cases are enabled, treat the compile-time DbConnection type
// hierarchy as final and only report when there is a DisposeAsync override below
// DbConnection, even for factory-returned instances.
if (DbConnectionSymbol is not null && type.InheritsFrom(DbConnectionSymbol))
{
if (unwrappedOperation is IObjectCreationOperation)
if ((dbSpecialCasesEnabled && !isSqliteSpecialCaseType) || unwrappedOperation is IObjectCreationOperation)
return HasDisposeAsyncMethodDeclaredInSubclass(type, DbConnectionSymbol);
}

// For DbCommand subclasses created directly (new T()), only report if the exact
// type being instantiated (or an intermediate subclass up to but not including
// DbCommand) actually overrides DisposeAsync. DbCommand.DisposeAsync just calls
// Dispose() synchronously, so it is not a meaningful async override.
// DbCommand DisposeAsync has a synchronous fallback in the base implementation.
// When db special-cases are enabled, treat the compile-time DbCommand type
// hierarchy as final and only report when there is a DisposeAsync override below
// DbCommand, even for factory-returned instances.
if (DbCommandSymbol is not null && type.InheritsFrom(DbCommandSymbol))
{
if (unwrappedOperation is IObjectCreationOperation)
if ((dbSpecialCasesEnabled && !isSqliteSpecialCaseType) || unwrappedOperation is IObjectCreationOperation)
return HasDisposeAsyncMethodDeclaredInSubclass(type, DbCommandSymbol);
}

// For DbDataReader subclasses created directly (new T()), only report if the exact
// type being instantiated (or an intermediate subclass up to but not including
// DbDataReader) actually overrides DisposeAsync. DbDataReader.DisposeAsync just calls
// Dispose() synchronously, so it is not a meaningful async override.
// DbDataReader DisposeAsync has a synchronous fallback in the base implementation.
// When db special-cases are enabled, treat the compile-time DbDataReader type
// hierarchy as final and only report when there is a DisposeAsync override below
// DbDataReader, even for factory-returned instances.
if (DbDataReaderSymbol is not null && type.InheritsFrom(DbDataReaderSymbol))
{
if (unwrappedOperation is IObjectCreationOperation)
if ((dbSpecialCasesEnabled && !isSqliteSpecialCaseType) || unwrappedOperation is IObjectCreationOperation)
return HasDisposeAsyncMethodDeclaredInSubclass(type, DbDataReaderSymbol);
}

// For DbTransaction subclasses created directly (new T()), only report if the exact
// type being instantiated (or an intermediate subclass up to but not including
// DbTransaction) actually overrides DisposeAsync. DbTransaction.DisposeAsync just calls
// Dispose() synchronously, so it is not a meaningful async override.
// DbTransaction DisposeAsync has a synchronous fallback in the base implementation.
// When db special-cases are enabled, treat the compile-time DbTransaction type
// hierarchy as final and only report when there is a DisposeAsync override below
// DbTransaction, even for factory-returned instances.
if (DbTransactionSymbol is not null && type.InheritsFrom(DbTransactionSymbol))
{
if (unwrappedOperation is IObjectCreationOperation)
if (dbSpecialCasesEnabled || unwrappedOperation is IObjectCreationOperation)
return HasDisposeAsyncMethodDeclaredInSubclass(type, DbTransactionSymbol);
}

// For DbBatch subclasses created directly (new T()), only report if the exact
// type being instantiated (or an intermediate subclass up to but not including
// DbBatch) actually overrides DisposeAsync. DbBatch.DisposeAsync just calls
// Dispose() synchronously, so it is not a meaningful async override.
// DbBatch DisposeAsync has a synchronous fallback in the base implementation.
// When db special-cases are enabled, treat the compile-time DbBatch type
// hierarchy as final and only report when there is a DisposeAsync override below
// DbBatch, even for factory-returned instances.
if (DbBatchSymbol is not null && type.InheritsFrom(DbBatchSymbol))
{
if (unwrappedOperation is IObjectCreationOperation)
if (dbSpecialCasesEnabled || unwrappedOperation is IObjectCreationOperation)
return HasDisposeAsyncMethodDeclaredInSubclass(type, DbBatchSymbol);
}

Expand All @@ -631,13 +639,13 @@ unwrappedOperation is IInvocationOperation invocationOperation &&
return HasDisposeAsyncMethod(type);
}

private bool ReportIfCanBeAwaitUsing(OperationAnalysisContext context, IOperation usingOperation, IVariableDeclarationGroupOperation operation, bool sqliteSpecialCasesEnabled)
private bool ReportIfCanBeAwaitUsing(OperationAnalysisContext context, IOperation usingOperation, IVariableDeclarationGroupOperation operation, bool sqliteSpecialCasesEnabled, bool dbSpecialCasesEnabled)
{
foreach (var declaration in operation.Declarations)
{
if ((declaration.Initializer?.Value) is not null)
{
if (CanBeAwaitUsing(declaration.Initializer.Value, sqliteSpecialCasesEnabled))
if (CanBeAwaitUsing(declaration.Initializer.Value, sqliteSpecialCasesEnabled, dbSpecialCasesEnabled))
{
var data = new DiagnosticData("Prefer using 'await using'", DoNotUseBlockingCallInAsyncContextData.Using);
ReportDiagnosticIfNeeded(context, data.CreateProperties(), usingOperation, data.DiagnosticMessage);
Expand All @@ -647,7 +655,7 @@ private bool ReportIfCanBeAwaitUsing(OperationAnalysisContext context, IOperatio

foreach (var declarator in declaration.Declarators)
{
if (declarator.Initializer is not null && CanBeAwaitUsing(declarator.Initializer.Value, sqliteSpecialCasesEnabled))
if (declarator.Initializer is not null && CanBeAwaitUsing(declarator.Initializer.Value, sqliteSpecialCasesEnabled, dbSpecialCasesEnabled))
{
var data = new DiagnosticData("Prefer using 'await using'", DoNotUseBlockingCallInAsyncContextData.UsingDeclarator);
ReportDiagnosticIfNeeded(context, data.CreateProperties(), usingOperation, data.DiagnosticMessage);
Expand All @@ -666,13 +674,14 @@ internal void AnalyzeUsing(OperationAnalysisContext context)
return;

var sqliteSpecialCasesEnabled = IsSqliteSpecialCasesEnabled(context, operation);
var dbSpecialCasesEnabled = IsDbSpecialCasesEnabled(context, operation);
if (operation.Resources is IVariableDeclarationGroupOperation variableDeclarationGroupOperation)
{
if (ReportIfCanBeAwaitUsing(context, operation, variableDeclarationGroupOperation, sqliteSpecialCasesEnabled))
if (ReportIfCanBeAwaitUsing(context, operation, variableDeclarationGroupOperation, sqliteSpecialCasesEnabled, dbSpecialCasesEnabled))
return;
}

if (CanBeAwaitUsing(operation.Resources, sqliteSpecialCasesEnabled))
if (CanBeAwaitUsing(operation.Resources, sqliteSpecialCasesEnabled, dbSpecialCasesEnabled))
{
var data = new DiagnosticData("Prefer using 'await using'", DoNotUseBlockingCallInAsyncContextData.Using);
ReportDiagnosticIfNeeded(context, data.CreateProperties(), operation, data.DiagnosticMessage);
Expand All @@ -686,7 +695,8 @@ internal void AnalyzeUsingDeclaration(OperationAnalysisContext context)
return;

var sqliteSpecialCasesEnabled = IsSqliteSpecialCasesEnabled(context, operation);
ReportIfCanBeAwaitUsing(context, operation, operation.DeclarationGroup, sqliteSpecialCasesEnabled);
var dbSpecialCasesEnabled = IsDbSpecialCasesEnabled(context, operation);
ReportIfCanBeAwaitUsing(context, operation, operation.DeclarationGroup, sqliteSpecialCasesEnabled, dbSpecialCasesEnabled);
}
}

Expand Down
Loading
Loading