From 1a8446acf3b6d8556daa500f9eb97351942301ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 5 May 2026 13:59:11 -0400 Subject: [PATCH 1/2] Add configurable SQLite special cases for MA0042/MA0045 Add opt-out config (MA0042.enable_sqlite_special_cases), extend SQLite method coverage, update docs, and add regression tests for MA0042/MA0045 including option-disabled behavior and API contract validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/Rules/MA0042.md | 16 ++ docs/Rules/MA0045.md | 14 ++ ...otUseBlockingCallInAsyncContextAnalyzer.cs | 86 +++++++-- ...nAsyncContextAnalyzer_AsyncContextTests.cs | 167 ++++++++++++++++++ ...yncContextAnalyzer_NonAsyncContextTests.cs | 69 ++++++++ 5 files changed, 335 insertions(+), 17 deletions(-) diff --git a/docs/Rules/MA0042.md b/docs/Rules/MA0042.md index 4f4745683..7de133832 100644 --- a/docs/Rules/MA0042.md +++ b/docs/Rules/MA0042.md @@ -55,6 +55,14 @@ 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.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 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`. ````csharp @@ -78,6 +86,14 @@ public async Task Sample() } ```` +## Configuration + +```` +# .editorconfig file +# Enable SQLite special-cases for MA0042/MA0045. default: true +MA0042.enable_sqlite_special_cases = true +```` + ## Additional resources - [Enforcing asynchronous code good practices using a Roslyn analyzer](https://www.meziantou.net/enforcing-asynchronous-code-good-practices-using-a-roslyn-analyzer.htm) diff --git a/docs/Rules/MA0045.md b/docs/Rules/MA0045.md index 4f9617151..2366c8dab 100644 --- a/docs/Rules/MA0045.md +++ b/docs/Rules/MA0045.md @@ -30,6 +30,20 @@ public async Task Sample() } ```` +This rule shares the same SQLite special-cases as [MA0042](MA0042.md) by default: +- `SqliteConnection.CreateCommand()` +- `SqliteCommand.ExecuteNonQuery()` +- `SqliteCommand.ExecuteScalar()` +- `SqliteCommand.ExecuteReader()` + +## Configuration + +```` +# .editorconfig file +# Enable SQLite special-cases for MA0042/MA0045. default: true +MA0042.enable_sqlite_special_cases = true +```` + ## Additional resources - [Enforcing asynchronous code good practices using a Roslyn analyzer](https://www.meziantou.net/enforcing-asynchronous-code-good-practices-using-a-roslyn-analyzer.htm) diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index ee512c6c0..1b69203ad 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Runtime.CompilerServices; +using Meziantou.Analyzer.Configurations; using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -78,6 +79,8 @@ public Context(Compilation compilation) StreamSymbol = compilation.GetBestTypeByMetadataName("System.IO.Stream"); DbConnectionSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbConnection"); DbCommandSymbol = compilation.GetBestTypeByMetadataName("System.Data.Common.DbCommand"); + SqliteConnectionSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Data.Sqlite.SqliteConnection"); + SqliteCommandSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Data.Sqlite.SqliteCommand"); CancellationTokenSymbol = compilation.GetBestTypeByMetadataName("System.Threading.CancellationToken"); ObsoleteAttributeSymbol = compilation.GetBestTypeByMetadataName("System.ObsoleteAttribute"); @@ -128,6 +131,8 @@ public Context(Compilation compilation) private ISymbol? ProcessSymbol { get; } private INamedTypeSymbol? DbConnectionSymbol { get; } private INamedTypeSymbol? DbCommandSymbol { get; } + private INamedTypeSymbol? SqliteConnectionSymbol { get; } + private INamedTypeSymbol? SqliteCommandSymbol { get; } private ISymbol[] ConsoleErrorAndOutSymbols { get; } private INamedTypeSymbol? CancellationTokenSymbol { get; } private INamedTypeSymbol? ObsoleteAttributeSymbol { get; } @@ -164,23 +169,25 @@ internal void AnalyzeInvocation(OperationAnalysisContext context) { var operation = (IInvocationOperation)context.Operation; var targetMethod = operation.TargetMethod; + var sqliteSpecialCasesEnabled = IsSqliteSpecialCasesEnabled(context, operation); + var isSqliteSpecialCaseMethod = IsSqliteSpecialCaseMethod(targetMethod); // 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. - if (_symbolsWithNoAsyncOverloads.Contains(targetMethod)) + if (!isSqliteSpecialCaseMethod && _symbolsWithNoAsyncOverloads.Contains(targetMethod)) return; - if (HasAsyncEquivalent(operation, out var diagnosticMessage)) + if (HasAsyncEquivalent(operation, sqliteSpecialCasesEnabled, out var diagnosticMessage)) { ReportDiagnosticIfNeeded(context, diagnosticMessage.CreateProperties(), operation, diagnosticMessage.DiagnosticMessage); } - else + else if (!isSqliteSpecialCaseMethod) { _symbolsWithNoAsyncOverloads.Add(targetMethod); } } - private bool HasAsyncEquivalent(IInvocationOperation operation, [NotNullWhen(true)] out DiagnosticData? data) + private bool HasAsyncEquivalent(IInvocationOperation operation, bool sqliteSpecialCasesEnabled, [NotNullWhen(true)] out DiagnosticData? data) { data = null; var targetMethod = operation.TargetMethod; @@ -263,6 +270,14 @@ private bool HasAsyncEquivalent(IInvocationOperation operation, [NotNullWhen(tru return false; } + // SqliteConnection.CreateCommand() always returns SqliteCommand. + // SqliteCommand does not override DisposeAsync, so there is no async alternative to require. + // https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/async + else if (sqliteSpecialCasesEnabled && IsSqliteSpecialCaseMethod(targetMethod)) + { + return false; + } + else if (Moq_MockSymbol is not null && targetMethod.Name is "Raise" && targetMethod.ContainingType.OriginalDefinition.IsEqualTo(Moq_MockSymbol)) { return false; @@ -298,6 +313,36 @@ private bool HasAsyncEquivalent(IInvocationOperation operation, [NotNullWhen(tru return false; } + private static bool IsSqliteSpecialCasesEnabled(OperationAnalysisContext context, IOperation operation) + { + var defaultValue = context.Options.GetConfigurationValue(operation, RuleIdentifiers.DoNotUseBlockingCallInAsyncContext + ".enable_sqlite_special_cases", defaultValue: true); + return context.Options.GetConfigurationValue(operation, RuleIdentifiers.DoNotUseBlockingCall + ".enable_sqlite_special_cases", defaultValue); + } + + private bool IsSqliteConnectionCreateCommand(IMethodSymbol targetMethod) + { + if (SqliteConnectionSymbol is null || SqliteCommandSymbol is null) + return false; + + return targetMethod.Name is "CreateCommand" && + targetMethod.ContainingType.IsEqualTo(SqliteConnectionSymbol) && + targetMethod.ReturnType.IsEqualTo(SqliteCommandSymbol); + } + + private bool IsSqliteCommandMethod(IMethodSymbol targetMethod) + { + if (SqliteCommandSymbol is null) + return false; + + return targetMethod.ContainingType.IsEqualTo(SqliteCommandSymbol) && + targetMethod.Name is "ExecuteNonQuery" or "ExecuteScalar" or "ExecuteReader"; + } + + private bool IsSqliteSpecialCaseMethod(IMethodSymbol targetMethod) + { + return IsSqliteConnectionCreateCommand(targetMethod) || IsSqliteCommandMethod(targetMethod); + } + private IMethodSymbol? FindPotentialAsyncEquivalent(IInvocationOperation operation, IMethodSymbol targetMethod, string methodName) { var options = new OverloadOptions( @@ -492,8 +537,16 @@ private bool HasDisposeAsyncMethodDeclaredInSubclass(INamedTypeSymbol symbol, IN return false; } - private bool CanBeAwaitUsing(IOperation operation) + private bool CanBeAwaitUsing(IOperation operation, bool sqliteSpecialCasesEnabled) { + var unwrappedOperation = operation.UnwrapImplicitConversionOperations(); + if (sqliteSpecialCasesEnabled && + unwrappedOperation is IInvocationOperation invocationOperation && + IsSqliteConnectionCreateCommand(invocationOperation.TargetMethod)) + { + return false; + } + if (operation.GetActualType() is not INamedTypeSymbol type) return false; @@ -503,8 +556,7 @@ private bool CanBeAwaitUsing(IOperation operation) // Dispose() synchronously by default, so it is not a meaningful async override. if (StreamSymbol is INamedTypeSymbol streamSymbol && type.InheritsFrom(streamSymbol)) { - var unwrapped = operation.UnwrapImplicitConversionOperations(); - if (unwrapped is IObjectCreationOperation) + if (unwrappedOperation is IObjectCreationOperation) return HasDisposeAsyncMethodDeclaredInSubclass(type, streamSymbol); } @@ -514,8 +566,7 @@ private bool CanBeAwaitUsing(IOperation operation) // Dispose() synchronously, so it is not a meaningful async override. if (DbConnectionSymbol is not null && type.InheritsFrom(DbConnectionSymbol)) { - var unwrapped = operation.UnwrapImplicitConversionOperations(); - if (unwrapped is IObjectCreationOperation) + if (unwrappedOperation is IObjectCreationOperation) return HasDisposeAsyncMethodDeclaredInSubclass(type, DbConnectionSymbol); } @@ -525,21 +576,20 @@ private bool CanBeAwaitUsing(IOperation operation) // Dispose() synchronously, so it is not a meaningful async override. if (DbCommandSymbol is not null && type.InheritsFrom(DbCommandSymbol)) { - var unwrapped = operation.UnwrapImplicitConversionOperations(); - if (unwrapped is IObjectCreationOperation) + if (unwrappedOperation is IObjectCreationOperation) return HasDisposeAsyncMethodDeclaredInSubclass(type, DbCommandSymbol); } return HasDisposeAsyncMethod(type); } - private bool ReportIfCanBeAwaitUsing(OperationAnalysisContext context, IOperation usingOperation, IVariableDeclarationGroupOperation operation) + private bool ReportIfCanBeAwaitUsing(OperationAnalysisContext context, IOperation usingOperation, IVariableDeclarationGroupOperation operation, bool sqliteSpecialCasesEnabled) { foreach (var declaration in operation.Declarations) { if ((declaration.Initializer?.Value) is not null) { - if (CanBeAwaitUsing(declaration.Initializer.Value)) + if (CanBeAwaitUsing(declaration.Initializer.Value, sqliteSpecialCasesEnabled)) { var data = new DiagnosticData("Prefer using 'await using'", DoNotUseBlockingCallInAsyncContextData.Using); ReportDiagnosticIfNeeded(context, data.CreateProperties(), usingOperation, data.DiagnosticMessage); @@ -549,7 +599,7 @@ private bool ReportIfCanBeAwaitUsing(OperationAnalysisContext context, IOperatio foreach (var declarator in declaration.Declarators) { - if (declarator.Initializer is not null && CanBeAwaitUsing(declarator.Initializer.Value)) + if (declarator.Initializer is not null && CanBeAwaitUsing(declarator.Initializer.Value, sqliteSpecialCasesEnabled)) { var data = new DiagnosticData("Prefer using 'await using'", DoNotUseBlockingCallInAsyncContextData.UsingDeclarator); ReportDiagnosticIfNeeded(context, data.CreateProperties(), usingOperation, data.DiagnosticMessage); @@ -567,13 +617,14 @@ internal void AnalyzeUsing(OperationAnalysisContext context) if (operation.IsAsynchronous) return; + var sqliteSpecialCasesEnabled = IsSqliteSpecialCasesEnabled(context, operation); if (operation.Resources is IVariableDeclarationGroupOperation variableDeclarationGroupOperation) { - if (ReportIfCanBeAwaitUsing(context, operation, variableDeclarationGroupOperation)) + if (ReportIfCanBeAwaitUsing(context, operation, variableDeclarationGroupOperation, sqliteSpecialCasesEnabled)) return; } - if (CanBeAwaitUsing(operation.Resources)) + if (CanBeAwaitUsing(operation.Resources, sqliteSpecialCasesEnabled)) { var data = new DiagnosticData("Prefer using 'await using'", DoNotUseBlockingCallInAsyncContextData.Using); ReportDiagnosticIfNeeded(context, data.CreateProperties(), operation, data.DiagnosticMessage); @@ -586,7 +637,8 @@ internal void AnalyzeUsingDeclaration(OperationAnalysisContext context) if (operation.IsAsynchronous) return; - ReportIfCanBeAwaitUsing(context, operation, operation.DeclarationGroup); + var sqliteSpecialCasesEnabled = IsSqliteSpecialCasesEnabled(context, operation); + ReportIfCanBeAwaitUsing(context, operation, operation.DeclarationGroup, sqliteSpecialCasesEnabled); } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index d54b3d432..b5c6a2227 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -1,3 +1,7 @@ +using System.Collections.Immutable; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using Meziantou.Analyzer.Rules; using Meziantou.Analyzer.Test.Helpers; using TestHelper; @@ -1998,6 +2002,132 @@ async Task A() .ValidateAsync(); } + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task SqliteConnection_CreateCommand_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(SqliteConnection connection) + { + using var command = connection.CreateCommand(); + } + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task SqliteCommand_ExecuteMethods_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(SqliteCommand command) + { + command.ExecuteNonQuery(); + command.ExecuteScalar(); + command.ExecuteReader(); + } + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task SqliteConnection_CreateCommand_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(SqliteConnection connection) + { + [|using var command = connection.CreateCommand();|] + } + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task SqliteCommand_ExecuteMethods_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(SqliteCommand command) + { + [|command.ExecuteNonQuery()|]; + [|command.ExecuteScalar()|]; + [|command.ExecuteReader()|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public void SqliteConnection_CreateCommand_ReturnsSqliteCommand() + { + var builder = new ProjectBuilder() + .WithTargetFramework(TargetFramework.Net8_0) + .AddNuGetReference("Microsoft.Data.Sqlite.Core", "8.0.0", "lib/net8.0/"); + + var sqliteReference = Assert.Single(builder.References, static reference => + string.Equals(Path.GetFileName(reference.Display), "Microsoft.Data.Sqlite.dll", StringComparison.OrdinalIgnoreCase)); + + using var stream = File.OpenRead(sqliteReference.Display!); + using var peReader = new PEReader(stream); + var metadataReader = peReader.GetMetadataReader(); + + var sqliteConnectionHandle = metadataReader.TypeDefinitions.Single(handle => + { + var type = metadataReader.GetTypeDefinition(handle); + return string.Equals(metadataReader.GetString(type.Namespace), "Microsoft.Data.Sqlite", StringComparison.Ordinal) && + string.Equals(metadataReader.GetString(type.Name), "SqliteConnection", StringComparison.Ordinal); + }); + var sqliteConnectionType = metadataReader.GetTypeDefinition(sqliteConnectionHandle); + + var typeNameProvider = new MetadataTypeNameProvider(); + var createCommand = sqliteConnectionType.GetMethods() + .Select(metadataReader.GetMethodDefinition) + .Select(method => (Method: method, Signature: method.DecodeSignature(typeNameProvider, genericContext: default(object)))) + .Single(item => string.Equals(metadataReader.GetString(item.Method.Name), "CreateCommand", StringComparison.Ordinal) && item.Signature.ParameterTypes.Length == 0); + + Assert.Equal("Microsoft.Data.Sqlite.SqliteCommand", createCommand.Signature.ReturnType); + } + [Fact] public async Task IAsyncEnumerable() { @@ -2751,4 +2881,41 @@ class DerivedDbCommand : BaseDbCommand { } """) .ValidateAsync(); } + + private sealed class MetadataTypeNameProvider : ISignatureTypeProvider + { + public string GetArrayType(string elementType, ArrayShape shape) => elementType + "[]"; + public string GetByReferenceType(string elementType) => "ref " + elementType; + public string GetFunctionPointerType(MethodSignature signature) => "methodptr"; + public string GetGenericInstantiation(string genericType, ImmutableArray typeArguments) => genericType + "<" + string.Join(", ", typeArguments) + ">"; + public string GetGenericMethodParameter(object? genericContext, int index) => "!!" + index; + public string GetGenericTypeParameter(object? genericContext, int index) => "!" + index; + public string GetModifiedType(string modifierType, string unmodifiedType, bool isRequired) => unmodifiedType; + public string GetPinnedType(string elementType) => elementType; + public string GetPointerType(string elementType) => elementType + "*"; + public string GetPrimitiveType(PrimitiveTypeCode typeCode) => typeCode.ToString(); + public string GetSZArrayType(string elementType) => elementType + "[]"; + + public string GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + var type = reader.GetTypeDefinition(handle); + var @namespace = reader.GetString(type.Namespace); + var name = reader.GetString(type.Name); + return string.IsNullOrEmpty(@namespace) ? name : @namespace + "." + name; + } + + public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + var type = reader.GetTypeReference(handle); + var @namespace = reader.GetString(type.Namespace); + var name = reader.GetString(type.Name); + return string.IsNullOrEmpty(@namespace) ? name : @namespace + "." + name; + } + + public string GetTypeFromSpecification(MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) => + reader.GetTypeSpecification(handle).DecodeSignature(this, genericContext); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")] + public string GetUnsupportedSignatureTypeKind(SignatureTypeKind rawTypeKind) => rawTypeKind.ToString(); + } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs index 514d33bc8..cb0a30f34 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_NonAsyncContextTests.cs @@ -1,4 +1,5 @@ using Meziantou.Analyzer.Rules; +using Meziantou.Analyzer.Test.Helpers; using TestHelper; namespace Meziantou.Analyzer.Test.Rules; @@ -115,4 +116,72 @@ private void A() }") .ValidateAsync(); } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task PrivateNonAsync_SqliteConnection_CreateCommand_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(SqliteConnection connection) + { + using var command = connection.CreateCommand(); + } + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task PrivateNonAsync_SqliteCommand_ExecuteMethods_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(SqliteCommand command) + { + command.ExecuteNonQuery(); + command.ExecuteScalar(); + command.ExecuteReader(); + } + } + """) + .ValidateAsync(); + } + + [Fact] + [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] + public async Task PrivateNonAsync_SqliteCommand_ExecuteMethods_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(SqliteCommand command) + { + [|command.ExecuteNonQuery()|]; + [|command.ExecuteScalar()|]; + [|command.ExecuteReader()|]; + } + } + """) + .ValidateAsync(); + } } From 602610593a0b350775ef3e55989b7b23843a3cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 5 May 2026 14:46:13 -0400 Subject: [PATCH 2/2] remove test --- ...nAsyncContextAnalyzer_AsyncContextTests.cs | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index b5c6a2227..a955c0488 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -2096,38 +2096,6 @@ public async Task A(SqliteCommand command) .ValidateAsync(); } - [Fact] - [Trait("Issue", "https://github.com/meziantou/Meziantou.Analyzer/issues/1121")] - public void SqliteConnection_CreateCommand_ReturnsSqliteCommand() - { - var builder = new ProjectBuilder() - .WithTargetFramework(TargetFramework.Net8_0) - .AddNuGetReference("Microsoft.Data.Sqlite.Core", "8.0.0", "lib/net8.0/"); - - var sqliteReference = Assert.Single(builder.References, static reference => - string.Equals(Path.GetFileName(reference.Display), "Microsoft.Data.Sqlite.dll", StringComparison.OrdinalIgnoreCase)); - - using var stream = File.OpenRead(sqliteReference.Display!); - using var peReader = new PEReader(stream); - var metadataReader = peReader.GetMetadataReader(); - - var sqliteConnectionHandle = metadataReader.TypeDefinitions.Single(handle => - { - var type = metadataReader.GetTypeDefinition(handle); - return string.Equals(metadataReader.GetString(type.Namespace), "Microsoft.Data.Sqlite", StringComparison.Ordinal) && - string.Equals(metadataReader.GetString(type.Name), "SqliteConnection", StringComparison.Ordinal); - }); - var sqliteConnectionType = metadataReader.GetTypeDefinition(sqliteConnectionHandle); - - var typeNameProvider = new MetadataTypeNameProvider(); - var createCommand = sqliteConnectionType.GetMethods() - .Select(metadataReader.GetMethodDefinition) - .Select(method => (Method: method, Signature: method.DecodeSignature(typeNameProvider, genericContext: default(object)))) - .Single(item => string.Equals(metadataReader.GetString(item.Method.Name), "CreateCommand", StringComparison.Ordinal) && item.Signature.ParameterTypes.Length == 0); - - Assert.Equal("Microsoft.Data.Sqlite.SqliteCommand", createCommand.Signature.ReturnType); - } - [Fact] public async Task IAsyncEnumerable() { @@ -2881,41 +2849,4 @@ class DerivedDbCommand : BaseDbCommand { } """) .ValidateAsync(); } - - private sealed class MetadataTypeNameProvider : ISignatureTypeProvider - { - public string GetArrayType(string elementType, ArrayShape shape) => elementType + "[]"; - public string GetByReferenceType(string elementType) => "ref " + elementType; - public string GetFunctionPointerType(MethodSignature signature) => "methodptr"; - public string GetGenericInstantiation(string genericType, ImmutableArray typeArguments) => genericType + "<" + string.Join(", ", typeArguments) + ">"; - public string GetGenericMethodParameter(object? genericContext, int index) => "!!" + index; - public string GetGenericTypeParameter(object? genericContext, int index) => "!" + index; - public string GetModifiedType(string modifierType, string unmodifiedType, bool isRequired) => unmodifiedType; - public string GetPinnedType(string elementType) => elementType; - public string GetPointerType(string elementType) => elementType + "*"; - public string GetPrimitiveType(PrimitiveTypeCode typeCode) => typeCode.ToString(); - public string GetSZArrayType(string elementType) => elementType + "[]"; - - public string GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) - { - var type = reader.GetTypeDefinition(handle); - var @namespace = reader.GetString(type.Namespace); - var name = reader.GetString(type.Name); - return string.IsNullOrEmpty(@namespace) ? name : @namespace + "." + name; - } - - public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) - { - var type = reader.GetTypeReference(handle); - var @namespace = reader.GetString(type.Namespace); - var name = reader.GetString(type.Name); - return string.IsNullOrEmpty(@namespace) ? name : @namespace + "." + name; - } - - public string GetTypeFromSpecification(MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) => - reader.GetTypeSpecification(handle).DecodeSignature(this, genericContext); - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")] - public string GetUnsupportedSignatureTypeKind(SignatureTypeKind rawTypeKind) => rawTypeKind.ToString(); - } }