diff --git a/docs/Rules/MA0042.md b/docs/Rules/MA0042.md index 4ffa3ec8f..8225c16f6 100644 --- a/docs/Rules/MA0042.md +++ b/docs/Rules/MA0042.md @@ -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() @@ -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. @@ -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 diff --git a/docs/Rules/MA0045.md b/docs/Rules/MA0045.md index c6c3e0005..5d5317e36 100644 --- a/docs/Rules/MA0045.md +++ b/docs/Rules/MA0045.md @@ -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 diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index e6a5933b9..bb8fce9ba 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -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); @@ -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 && @@ -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 @@ -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); } @@ -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); @@ -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); @@ -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); @@ -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); } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index 7d9b35f72..47772ab0b 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -2664,7 +2664,7 @@ class MySqlConnection : DbConnection } [Fact] - public async Task UsingFactoryMethod_DbConnectionSubclass_NoDisposeAsyncOverride_Diagnostic() + public async Task UsingFactoryMethod_DbConnectionSubclass_NoDisposeAsyncOverride_NoDiagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) @@ -2673,6 +2673,44 @@ await CreateProjectBuilder() using System.Data.Common; using System.Threading.Tasks; + class Test + { + public async Task A() + { + using var conn = CreateConnection(); + } + + private MySqlConnection CreateConnection() => throw null; + } + + class MySqlConnection : DbConnection + { + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw null; + protected override DbCommand CreateDbCommand() => throw null; + public override void ChangeDatabase(string databaseName) => throw null; + public override void Close() => throw null; + public override void Open() => throw null; + public override string ConnectionString { get => throw null; set => throw null; } + public override string Database => throw null; + public override string DataSource => throw null; + public override string ServerVersion => throw null; + public override ConnectionState State => throw null; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task UsingFactoryMethod_DbConnectionSubclass_NoDisposeAsyncOverride_OptionDisabled_Diagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .AddAnalyzerConfiguration("MA0042.enable_db_special_cases", "false") + .WithSourceCode(""" + using System.Data; + using System.Data.Common; + using System.Threading.Tasks; + class Test { public async Task A() @@ -2814,10 +2852,52 @@ class MyDbCommand : DbCommand } [Fact] - public async Task UsingFactoryMethod_DbCommandSubclass_NoDisposeAsyncOverride_Diagnostic() + public async Task UsingFactoryMethod_DbCommandSubclass_NoDisposeAsyncOverride_NoDiagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .WithSourceCode(""" + using System.Data; + using System.Data.Common; + using System.Threading.Tasks; + + class Test + { + public async Task A() + { + using var command = CreateCommand(); + } + + private MyDbCommand CreateCommand() => throw null; + } + + class MyDbCommand : DbCommand + { + public override string CommandText { get => throw null; set => throw null; } + public override int CommandTimeout { get => throw null; set => throw null; } + public override CommandType CommandType { get => throw null; set => throw null; } + public override bool DesignTimeVisible { get => throw null; set => throw null; } + public override UpdateRowSource UpdatedRowSource { get => throw null; set => throw null; } + protected override DbConnection DbConnection { get => throw null; set => throw null; } + protected override DbParameterCollection DbParameterCollection => throw null; + protected override DbTransaction DbTransaction { get => throw null; set => throw null; } + public override void Cancel() => throw null; + public override int ExecuteNonQuery() => throw null; + public override object ExecuteScalar() => throw null; + public override void Prepare() => throw null; + protected override DbParameter CreateDbParameter() => throw null; + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw null; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task UsingFactoryMethod_DbCommandSubclass_NoDisposeAsyncOverride_OptionDisabled_Diagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) + .AddAnalyzerConfiguration("MA0042.enable_db_special_cases", "false") .WithSourceCode(""" using System.Data; using System.Data.Common; @@ -2957,7 +3037,7 @@ public async Task A() } [Fact] - public async Task UsingFactoryMethod_DbDataReaderSubclass_NoDisposeAsyncOverride_Diagnostic() + public async Task UsingFactoryMethod_DbDataReaderSubclass_NoDisposeAsyncOverride_NoDiagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) @@ -2965,6 +3045,29 @@ await CreateProjectBuilder() using System.Data; using System.Threading.Tasks; + class Test + { + public async Task A() + { + using var reader = CreateReader(); + } + + private DataTableReader CreateReader() => new DataTableReader(new DataTable()); + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task UsingFactoryMethod_DbDataReaderSubclass_NoDisposeAsyncOverride_OptionDisabled_Diagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .AddAnalyzerConfiguration("MA0042.enable_db_special_cases", "false") + .WithSourceCode(""" + using System.Data; + using System.Threading.Tasks; + class Test { public async Task A() @@ -3069,10 +3172,42 @@ class MyDbTransaction : DbTransaction } [Fact] - public async Task UsingFactoryMethod_DbTransactionSubclass_NoDisposeAsyncOverride_Diagnostic() + public async Task UsingFactoryMethod_DbTransactionSubclass_NoDisposeAsyncOverride_NoDiagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .WithSourceCode(""" + using System.Data; + using System.Data.Common; + using System.Threading.Tasks; + + class Test + { + public async Task A() + { + using var transaction = CreateTransaction(); + } + + private MyDbTransaction CreateTransaction() => throw null; + } + + class MyDbTransaction : DbTransaction + { + protected override DbConnection DbConnection => throw null; + public override IsolationLevel IsolationLevel => throw null; + public override void Commit() => throw null; + public override void Rollback() => throw null; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task UsingFactoryMethod_DbTransactionSubclass_NoDisposeAsyncOverride_OptionDisabled_Diagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) + .AddAnalyzerConfiguration("MA0042.enable_db_special_cases", "false") .WithSourceCode(""" using System.Data; using System.Data.Common; @@ -3173,10 +3308,53 @@ class MyDbBatch : DbBatch } [Fact] - public async Task UsingFactoryMethod_DbBatchSubclass_NoDisposeAsyncOverride_Diagnostic() + public async Task UsingFactoryMethod_DbBatchSubclass_NoDisposeAsyncOverride_NoDiagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .WithSourceCode(""" + using System.Data; + using System.Data.Common; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public async Task A() + { + using var batch = CreateBatch(); + } + + private MyDbBatch CreateBatch() => throw null; + } + + class MyDbBatch : DbBatch + { + public override int Timeout { get => throw null; set => throw null; } + protected override DbBatchCommandCollection DbBatchCommands => throw null; + protected override DbConnection DbConnection { get => throw null; set => throw null; } + protected override DbTransaction DbTransaction { get => throw null; set => throw null; } + public override void Cancel() => throw null; + protected override DbBatchCommand CreateDbBatchCommand() => throw null; + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw null; + protected override Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) => throw null; + public override int ExecuteNonQuery() => throw null; + public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken = default) => throw null; + public override object ExecuteScalar() => throw null; + public override Task ExecuteScalarAsync(CancellationToken cancellationToken = default) => throw null; + public override void Prepare() => throw null; + public override Task PrepareAsync(CancellationToken cancellationToken = default) => throw null; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task UsingFactoryMethod_DbBatchSubclass_NoDisposeAsyncOverride_OptionDisabled_Diagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) + .AddAnalyzerConfiguration("MA0042.enable_db_special_cases", "false") .WithSourceCode(""" using System.Data; using System.Data.Common; diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs index 15be0e731..83aa3940a 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs @@ -266,4 +266,67 @@ private void A(SqliteDataReader reader) """) .ValidateAsync(); } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1134")] + public async Task PrivateNonAsync_UsingFactoryMethod_DbTransaction_NoDisposeAsyncOverride_NoDiagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .WithSourceCode(""" + using System.Data; + using System.Data.Common; + + class Test + { + private void A() + { + using var transaction = CreateTransaction(); + } + + private MyDbTransaction CreateTransaction() => throw null; + } + + class MyDbTransaction : DbTransaction + { + protected override DbConnection DbConnection => throw null; + public override IsolationLevel IsolationLevel => throw null; + public override void Commit() => throw null; + public override void Rollback() => throw null; + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1134")] + public async Task PrivateNonAsync_UsingFactoryMethod_DbTransaction_NoDisposeAsyncOverride_OptionDisabled_Diagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .AddAnalyzerConfiguration("MA0042.enable_db_special_cases", "false") + .WithSourceCode(""" + using System.Data; + using System.Data.Common; + + class Test + { + private void A() + { + [|using var transaction = CreateTransaction();|] + } + + private MyDbTransaction CreateTransaction() => throw null; + } + + class MyDbTransaction : DbTransaction + { + protected override DbConnection DbConnection => throw null; + public override IsolationLevel IsolationLevel => throw null; + public override void Commit() => throw null; + public override void Rollback() => throw null; + } + """) + .ValidateAsync(); + } }