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: 6 additions & 8 deletions docs/Rules/MA0042.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,12 @@ public sealed class Sample

The rule does not report a diagnostic for `IDbContextFactory<TContext>.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`.

Expand Down
8 changes: 3 additions & 5 deletions docs/Rules/MA0045.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -2039,7 +2039,7 @@ class Test
{
public async Task A(SqliteConnection connection)
{
connection.Open();
connection.Close();
}
}
""")
Expand All @@ -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)
Expand All @@ -2061,9 +2061,7 @@ class Test
{
public async Task A(SqliteCommand command)
{
command.ExecuteNonQuery();
command.ExecuteScalar();
command.ExecuteReader();
command.Prepare();
}
}
""")
Expand Down Expand Up @@ -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)
Expand All @@ -2109,7 +2107,7 @@ class Test
{
public async Task A(SqliteConnection connection)
{
[|connection.Open()|];
[|connection.Close()|];
}
}
""")
Expand All @@ -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)
Expand All @@ -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()|];
}
}
""")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -152,9 +152,7 @@ class Test
{
private void A(SqliteCommand command)
{
command.ExecuteNonQuery();
command.ExecuteScalar();
command.ExecuteReader();
command.Prepare();
}
}
""")
Expand All @@ -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)
Expand All @@ -175,7 +173,7 @@ class Test
{
private void A(SqliteConnection connection)
{
connection.Open();
connection.Close();
}
}
""")
Expand All @@ -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)
Expand All @@ -197,7 +195,7 @@ class Test
{
private void A(SqliteConnection connection)
{
[|connection.Open()|];
[|connection.Close()|];
}
}
""")
Expand All @@ -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)
Expand All @@ -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()|];
}
}
""")
Expand Down
Loading