From a7f8c3fda1d222fe7f5ef4193c1233052cf5f45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 5 May 2026 20:46:57 -0400 Subject: [PATCH] Expand SQLite special-case exclusions Treat all method calls on SqliteConnection, SqliteCommand, and SqliteDataReader as SQLite special cases for MA0042/MA0045 by default. Update tests and rule docs to reflect the broader type-based behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/Rules/MA0042.md | 14 ++-- docs/Rules/MA0045.md | 8 +-- ...otUseBlockingCallInAsyncContextAnalyzer.cs | 47 +++++--------- ...nAsyncContextAnalyzer_AsyncContextTests.cs | 65 +++++++++++++++---- ...yncContextAnalyzer_NonAsyncContextTests.cs | 63 ++++++++++++++---- 5 files changed, 131 insertions(+), 66 deletions(-) diff --git a/docs/Rules/MA0042.md b/docs/Rules/MA0042.md index 0f0b81954..dbb7b91d4 100644 --- a/docs/Rules/MA0042.md +++ b/docs/Rules/MA0042.md @@ -55,14 +55,12 @@ public sealed class Sample The rule does not report a diagnostic for `IDbContextFactory.CreateDbContext()`. The `CreateDbContextAsync()` overload was introduced only for specific edge-case scenarios where the factory itself must perform asynchronous initialization, and is not intended as a general-purpose replacement. See [dotnet/efcore#26630](https://github.com/dotnet/efcore/issues/26630) for more details. -The rule does not report a diagnostic for the following SQLite APIs by default: -- `SqliteConnection.Open()` -- `SqliteConnection.CreateCommand()` -- `SqliteCommand.ExecuteNonQuery()` -- `SqliteCommand.ExecuteScalar()` -- `SqliteCommand.ExecuteReader()` - -`SqliteCommand` does not override `DisposeAsync`, and SQLite async APIs have documented limitations. See [Async limitations](https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/async) for details. +The rule does not report a diagnostic for method invocations on the following SQLite types by default: +- `SqliteConnection` +- `SqliteCommand` +- `SqliteDataReader` + +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`. diff --git a/docs/Rules/MA0045.md b/docs/Rules/MA0045.md index 71772277d..c6c3e0005 100644 --- a/docs/Rules/MA0045.md +++ b/docs/Rules/MA0045.md @@ -31,11 +31,9 @@ public async Task Sample() ```` This rule shares the same SQLite special-cases as [MA0042](MA0042.md) by default: -- `SqliteConnection.Open()` -- `SqliteConnection.CreateCommand()` -- `SqliteCommand.ExecuteNonQuery()` -- `SqliteCommand.ExecuteScalar()` -- `SqliteCommand.ExecuteReader()` +- `SqliteConnection` method invocations +- `SqliteCommand` method invocations +- `SqliteDataReader` method invocations ## Configuration diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index 677d0a1a5..c849b397b 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -81,6 +81,7 @@ public Context(Compilation compilation) DbCommandSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbCommand"); SqliteConnectionSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Data.Sqlite.SqliteConnection"); SqliteCommandSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Data.Sqlite.SqliteCommand"); + SqliteDataReaderSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Data.Sqlite.SqliteDataReader"); CancellationTokenSymbol = compilation.GetBestTypeByMetadataName("System.Threading.CancellationToken"); ObsoleteAttributeSymbol = compilation.GetBestTypeByMetadataName("System.ObsoleteAttribute"); @@ -133,6 +134,7 @@ public Context(Compilation compilation) private INamedTypeSymbol? DbCommandSymbol { get; } private INamedTypeSymbol? SqliteConnectionSymbol { get; } private INamedTypeSymbol? SqliteCommandSymbol { get; } + private INamedTypeSymbol? SqliteDataReaderSymbol { get; } private ISymbol[] ConsoleErrorAndOutSymbols { get; } private INamedTypeSymbol? CancellationTokenSymbol { get; } private INamedTypeSymbol? ObsoleteAttributeSymbol { get; } @@ -170,7 +172,7 @@ internal void AnalyzeInvocation(OperationAnalysisContext context) var operation = (IInvocationOperation)context.Operation; var targetMethod = operation.TargetMethod; var sqliteSpecialCasesEnabled = IsSqliteSpecialCasesEnabled(context, operation); - var isSqliteSpecialCaseMethod = IsSqliteSpecialCaseMethod(targetMethod); + var isSqliteSpecialCaseMethod = IsSqliteSpecialCaseMethod(operation); // The cache only contains methods with no async equivalent methods. // This optimizes the best-case scenario where code is correctly written according to this analyzer. @@ -270,11 +272,10 @@ private bool HasAsyncEquivalent(IInvocationOperation operation, bool sqliteSpeci return false; } - // SqliteConnection.Open() and CreateCommand() are synchronous by design in Microsoft.Data.Sqlite. - // SqliteConnection.CreateCommand() always returns SqliteCommand and SqliteCommand does not override - // DisposeAsync, so there is no async alternative to require. + // Async APIs in Microsoft.Data.Sqlite have documented limitations. + // Ignore any invocation on SqliteConnection, SqliteCommand, or SqliteDataReader by default. // https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/async - else if (sqliteSpecialCasesEnabled && IsSqliteSpecialCaseMethod(targetMethod)) + else if (sqliteSpecialCasesEnabled && IsSqliteSpecialCaseMethod(operation)) { return false; } @@ -320,38 +321,26 @@ private static bool IsSqliteSpecialCasesEnabled(OperationAnalysisContext context return context.Options.GetConfigurationValue(operation, RuleIdentifiers.DoNotUseBlockingCall + ".enable_sqlite_special_cases", defaultValue); } - private bool IsSqliteConnectionCreateCommand(IMethodSymbol targetMethod) + private bool IsSqliteSpecialCaseType(INamedTypeSymbol type) { - if (SqliteConnectionSymbol is null || SqliteCommandSymbol is null) - return false; - - return targetMethod.Name is "CreateCommand" && - targetMethod.ContainingType.IsEqualTo(SqliteConnectionSymbol) && - targetMethod.ReturnType.IsEqualTo(SqliteCommandSymbol); + return type.IsEqualToAny(SqliteConnectionSymbol, SqliteCommandSymbol, SqliteDataReaderSymbol); } - private bool IsSqliteConnectionOpen(IMethodSymbol targetMethod) + private bool IsSqliteSpecialCaseMethod(IInvocationOperation operation) { - if (SqliteConnectionSymbol is null) - return false; + if (IsSqliteSpecialCaseType(operation.TargetMethod.ContainingType)) + return true; - return targetMethod.Name is "Open" && - targetMethod.ContainingType.IsEqualTo(SqliteConnectionSymbol) && - targetMethod.Parameters.Length == 0; - } + if (operation.TargetMethod.IsExtensionMethod) + return false; - private bool IsSqliteCommandMethod(IMethodSymbol targetMethod) - { - if (SqliteCommandSymbol is null) + if (operation.TargetMethod.IsStatic) return false; - return targetMethod.ContainingType.IsEqualTo(SqliteCommandSymbol) && - targetMethod.Name is "ExecuteNonQuery" or "ExecuteScalar" or "ExecuteReader"; - } + if (operation.Instance?.GetActualType() is not INamedTypeSymbol type) + return false; - private bool IsSqliteSpecialCaseMethod(IMethodSymbol targetMethod) - { - return IsSqliteConnectionOpen(targetMethod) || IsSqliteConnectionCreateCommand(targetMethod) || IsSqliteCommandMethod(targetMethod); + return IsSqliteSpecialCaseType(type); } private IMethodSymbol? FindPotentialAsyncEquivalent(IInvocationOperation operation, IMethodSymbol targetMethod, string methodName) @@ -553,7 +542,7 @@ private bool CanBeAwaitUsing(IOperation operation, bool sqliteSpecialCasesEnable var unwrappedOperation = operation.UnwrapImplicitConversionOperations(); if (sqliteSpecialCasesEnabled && unwrappedOperation is IInvocationOperation invocationOperation && - IsSqliteConnectionCreateCommand(invocationOperation.TargetMethod)) + IsSqliteSpecialCaseMethod(invocationOperation)) { return false; } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index 4ef395954..6d20ec451 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -2026,7 +2026,7 @@ public async Task A(SqliteConnection connection) [Fact] [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] - public async Task SqliteConnection_Open_NoDiagnostic() + public async Task SqliteConnection_Close_NoDiagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) @@ -2039,7 +2039,7 @@ class Test { public async Task A(SqliteConnection connection) { - connection.Open(); + connection.Close(); } } """) @@ -2048,7 +2048,7 @@ public async Task A(SqliteConnection connection) [Fact] [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] - public async Task SqliteCommand_ExecuteMethods_NoDiagnostic() + public async Task SqliteCommand_Prepare_NoDiagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) @@ -2061,9 +2061,7 @@ class Test { public async Task A(SqliteCommand command) { - command.ExecuteNonQuery(); - command.ExecuteScalar(); - command.ExecuteReader(); + command.Prepare(); } } """) @@ -2095,7 +2093,7 @@ public async Task A(SqliteConnection connection) [Fact] [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] - public async Task SqliteConnection_Open_OptionDisabled_Diagnostic() + public async Task SqliteConnection_Close_OptionDisabled_Diagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) @@ -2109,7 +2107,7 @@ class Test { public async Task A(SqliteConnection connection) { - [|connection.Open()|]; + [|connection.Close()|]; } } """) @@ -2118,7 +2116,7 @@ public async Task A(SqliteConnection connection) [Fact] [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] - public async Task SqliteCommand_ExecuteMethods_OptionDisabled_Diagnostic() + public async Task SqliteCommand_Prepare_OptionDisabled_Diagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) @@ -2132,9 +2130,52 @@ class Test { public async Task A(SqliteCommand command) { - [|command.ExecuteNonQuery()|]; - [|command.ExecuteScalar()|]; - [|command.ExecuteReader()|]; + [|command.Prepare()|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task SqliteDataReader_Read_NoDiagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .AddNuGetReference("Microsoft.Data.Sqlite.Core", "8.0.0", "lib/net8.0/") + .WithSourceCode(""" + using System.Threading.Tasks; + using Microsoft.Data.Sqlite; + + class Test + { + public async Task A(SqliteDataReader reader) + { + reader.Read(); + } + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task SqliteDataReader_Read_OptionDisabled_Diagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .AddNuGetReference("Microsoft.Data.Sqlite.Core", "8.0.0", "lib/net8.0/") + .AddAnalyzerConfiguration("MA0042.enable_sqlite_special_cases", "false") + .WithSourceCode(""" + using System.Threading.Tasks; + using Microsoft.Data.Sqlite; + + class Test + { + public async Task A(SqliteDataReader reader) + { + [|reader.Read()|]; } } """) diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs index fe66628b0..15be0e731 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs @@ -140,7 +140,7 @@ private void A(SqliteConnection connection) [Fact] [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] - public async Task PrivateNonAsync_SqliteCommand_ExecuteMethods_NoDiagnostic() + public async Task PrivateNonAsync_SqliteCommand_Prepare_NoDiagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) @@ -152,9 +152,7 @@ class Test { private void A(SqliteCommand command) { - command.ExecuteNonQuery(); - command.ExecuteScalar(); - command.ExecuteReader(); + command.Prepare(); } } """) @@ -163,7 +161,7 @@ private void A(SqliteCommand command) [Fact] [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] - public async Task PrivateNonAsync_SqliteConnection_Open_NoDiagnostic() + public async Task PrivateNonAsync_SqliteConnection_Close_NoDiagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) @@ -175,7 +173,7 @@ class Test { private void A(SqliteConnection connection) { - connection.Open(); + connection.Close(); } } """) @@ -184,7 +182,7 @@ private void A(SqliteConnection connection) [Fact] [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] - public async Task PrivateNonAsync_SqliteConnection_Open_OptionDisabled_Diagnostic() + public async Task PrivateNonAsync_SqliteConnection_Close_OptionDisabled_Diagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) @@ -197,7 +195,7 @@ class Test { private void A(SqliteConnection connection) { - [|connection.Open()|]; + [|connection.Close()|]; } } """) @@ -206,7 +204,7 @@ private void A(SqliteConnection connection) [Fact] [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] - public async Task PrivateNonAsync_SqliteCommand_ExecuteMethods_OptionDisabled_Diagnostic() + public async Task PrivateNonAsync_SqliteCommand_Prepare_OptionDisabled_Diagnostic() { await CreateProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) @@ -219,9 +217,50 @@ class Test { private void A(SqliteCommand command) { - [|command.ExecuteNonQuery()|]; - [|command.ExecuteScalar()|]; - [|command.ExecuteReader()|]; + [|command.Prepare()|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task PrivateNonAsync_SqliteDataReader_Read_NoDiagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .AddNuGetReference("Microsoft.Data.Sqlite.Core", "8.0.0", "lib/net8.0/") + .WithSourceCode(""" + using Microsoft.Data.Sqlite; + + class Test + { + private void A(SqliteDataReader reader) + { + reader.Read(); + } + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task PrivateNonAsync_SqliteDataReader_Read_OptionDisabled_Diagnostic() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .AddNuGetReference("Microsoft.Data.Sqlite.Core", "8.0.0", "lib/net8.0/") + .AddAnalyzerConfiguration("MA0042.enable_sqlite_special_cases", "false") + .WithSourceCode(""" + using Microsoft.Data.Sqlite; + + class Test + { + private void A(SqliteDataReader reader) + { + [|reader.Read()|]; } } """)