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: 13 additions & 1 deletion docs/Rules/MA0042.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ 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`, `DbConnection`, or `DbCommand` subclass that is directly instantiated with `new` when the concrete type does not override `DisposeAsync`. `Stream.DisposeAsync`, `DbConnection.DisposeAsync`, and `DbCommand.DisposeAsync` merely 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`, `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`.

````csharp
public async Task Sample()
Expand All @@ -82,6 +82,18 @@ public async Task Sample()

// Diagnostic: obtained from a factory, runtime type may override DisposeAsync.
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.
using var reader2 = CreateReader();

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

// Diagnostic: obtained from a factory, runtime type may override DisposeAsync.
using var writer2 = CreateWriter();
}
````

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,12 @@ public Context(Compilation compilation)

ProcessSymbol = compilation.GetBestTypeByMetadataName("System.Diagnostics.Process");
StreamSymbol = compilation.GetBestTypeByMetadataName("System.IO.Stream");
TextWriterSymbol = compilation.GetBestTypeByMetadataName("System.IO.TextWriter");
DbConnectionSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbConnection");
DbCommandSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbCommand");
DbDataReaderSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbDataReader");
DbTransactionSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbTransaction");
DbBatchSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbBatch");
SqliteConnectionSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Data.Sqlite.SqliteConnection");
SqliteCommandSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Data.Sqlite.SqliteCommand");
SqliteDataReaderSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Data.Sqlite.SqliteDataReader");
Expand Down Expand Up @@ -129,9 +133,13 @@ public Context(Compilation compilation)
}

private ISymbol? StreamSymbol { get; }
private INamedTypeSymbol? TextWriterSymbol { get; }
private ISymbol? ProcessSymbol { get; }
private INamedTypeSymbol? DbConnectionSymbol { get; }
private INamedTypeSymbol? DbCommandSymbol { get; }
private INamedTypeSymbol? DbDataReaderSymbol { get; }
private INamedTypeSymbol? DbTransactionSymbol { get; }
private INamedTypeSymbol? DbBatchSymbol { get; }
private INamedTypeSymbol? SqliteConnectionSymbol { get; }
private INamedTypeSymbol? SqliteCommandSymbol { get; }
private INamedTypeSymbol? SqliteDataReaderSymbol { get; }
Expand Down Expand Up @@ -580,6 +588,46 @@ unwrappedOperation is IInvocationOperation invocationOperation &&
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.
if (DbDataReaderSymbol is not null && type.InheritsFrom(DbDataReaderSymbol))
{
if (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.
if (DbTransactionSymbol is not null && type.InheritsFrom(DbTransactionSymbol))
{
if (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.
if (DbBatchSymbol is not null && type.InheritsFrom(DbBatchSymbol))
{
if (unwrappedOperation is IObjectCreationOperation)
return HasDisposeAsyncMethodDeclaredInSubclass(type, DbBatchSymbol);
}

// For TextWriter subclasses created directly (new T()), only report if the exact
// type being instantiated (or an intermediate subclass up to but not including
// TextWriter) actually overrides DisposeAsync. TextWriter.DisposeAsync just calls
// Dispose() synchronously by default, so it is not a meaningful async override.
if (TextWriterSymbol is not null && type.InheritsFrom(TextWriterSymbol))
{
if (unwrappedOperation is IObjectCreationOperation)
return HasDisposeAsyncMethodDeclaredInSubclass(type, TextWriterSymbol);
}

return HasDisposeAsyncMethod(type);
}

Expand Down
Loading
Loading