Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support DbCommand command-text via constructor string sql query parse #135

Merged
merged 4 commits into from
Feb 7, 2025
Merged
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
35 changes: 27 additions & 8 deletions src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -35,8 +35,11 @@ private void OnCompilationStart(CompilationStartAnalysisContext context)
// per-run state (in particular, so we can have "first time only" diagnostics)
var state = new AnalyzerState(context);

// respond to method usages
context.RegisterOperationAction(state.OnOperation, OperationKind.Invocation, OperationKind.SimpleAssignment);
context.RegisterOperationAction(state.OnOperation,
OperationKind.Invocation, // for Dapper method invocations
OperationKind.SimpleAssignment, // for assignments of query
OperationKind.ObjectCreation // for instantiating Command objects
);

// final actions
context.RegisterCompilationEndAction(state.OnCompilationEndAction);
@@ -111,8 +114,9 @@ public void OnOperation(OperationAnalysisContext ctx)
try
{
// we'll look for:
// method calls with a parameter called "sql" or marked [Sql]
// property assignments to "CommandText"
// - method calls with a parameter called "sql" or marked [Sql]
// - property assignments to "CommandText"
// - allocation of SqlCommand (i.e. `new SqlCommand(queryString, ...)` )
switch (ctx.Operation.Kind)
{
case OperationKind.Invocation when ctx.Operation is IInvocationOperation invoke:
@@ -155,6 +159,21 @@ public void OnOperation(OperationAnalysisContext ctx)
ValidatePropertyUsage(ctx, assignment.Value, false);
}
}
break;
case OperationKind.ObjectCreation when ctx.Operation is IObjectCreationOperation objectCreationOperation:

var ctor = objectCreationOperation.Constructor;
var receiverType = ctor?.ReceiverType;

if (ctor is not null && IsSqlClient(receiverType))
{
var sqlParam = ctor.Parameters.FirstOrDefault();
if (sqlParam is not null && sqlParam.Type.SpecialType == SpecialType.System_String && sqlParam.Name == "cmdText")
{
ValidateParameterUsage(ctx, objectCreationOperation.Arguments.First(), sqlUsage: objectCreationOperation);
}
}

break;
}
}
@@ -327,11 +346,11 @@ private void ValidateSurroundingLinqUsage(in OperationAnalysisContext ctx, Opera
}
}

private void ValidateParameterUsage(in OperationAnalysisContext ctx, IOperation sqlSource)
private void ValidateParameterUsage(in OperationAnalysisContext ctx, IOperation sqlSource, IOperation? sqlUsage = null)
{
// TODO: check other parameters for special markers like command type?
var flags = SqlParseInputFlags.None;
ValidateSql(ctx, sqlSource, flags, SqlParameters.None);
ValidateSql(ctx, sqlSource, flags, SqlParameters.None, sqlUsageOperation: sqlUsage);
}

private void ValidatePropertyUsage(in OperationAnalysisContext ctx, IOperation sqlSource, bool isCommand)
@@ -372,12 +391,12 @@ public AnalyzerState(CompilationStartAnalysisContext context)
}

private void ValidateSql(in OperationAnalysisContext ctx, IOperation sqlSource, SqlParseInputFlags flags,
ImmutableArray<SqlParameter> parameters, Location? location = null)
ImmutableArray<SqlParameter> parameters, Location? location = null, IOperation? sqlUsageOperation = null)
{
var parseState = new ParseState(ctx);

// should we consider this as a syntax we can handle?
var syntax = IdentifySqlSyntax(parseState, ctx.Operation, out var caseSensitive) ?? DefaultSqlSyntax ?? SqlSyntax.General;
var syntax = IdentifySqlSyntax(parseState, sqlUsageOperation ?? ctx.Operation, out var caseSensitive) ?? DefaultSqlSyntax ?? SqlSyntax.General;
switch (syntax)
{
case SqlSyntax.SqlServer:
38 changes: 38 additions & 0 deletions src/Dapper.AOT.Analyzers/Internal/Inspection.cs
Original file line number Diff line number Diff line change
@@ -146,6 +146,24 @@ public static bool IsEnabled(in ParseState ctx, IOperation op, string attributeN
return false;
}

public static bool IsSqlClient(ITypeSymbol? typeSymbol) => typeSymbol is
{
Name: "SqlCommand",
ContainingNamespace:
{
Name: "SqlClient",
ContainingNamespace:
{
Name: "Data",
ContainingNamespace:
{
Name: "Microsoft" or "System", // either Microsoft.Data.SqlClient or System.Data.SqlClient
ContainingNamespace.IsGlobalNamespace: true
}
}
}
};

public static bool IsDapperAttribute(AttributeData attrib)
=> attrib.AttributeClass is
{
@@ -1432,6 +1450,26 @@ internal static bool IsCommand(INamedTypeSymbol type)
}
}
}
else if (op is IObjectCreationOperation objectCreationOp)
{
var ctorTypeNamespace = objectCreationOp.Type?.ContainingNamespace;
var ctorTypeName = objectCreationOp.Type?.Name;

if (ctorTypeNamespace is not null && ctorTypeName is not null)
{
foreach (var candidate in KnownConnectionTypes)
{
var current = ctorTypeNamespace;
if (ctorTypeName == candidate.Command
&& AssertAndAscend(ref current, candidate.Namespace0)
&& AssertAndAscend(ref current, candidate.Namespace1)
&& AssertAndAscend(ref current, candidate.Namespace2))
{
return candidate.Syntax;
}
}
}
}

return null;

2 changes: 1 addition & 1 deletion src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs
Original file line number Diff line number Diff line change
@@ -110,7 +110,7 @@ internal virtual bool IsGlobalStatement(SyntaxNode syntax, out SyntaxNode? entry

internal virtual StringSyntaxKind? TryDetectOperationStringSyntaxKind(IOperation operation)
{
if (operation is null) return null;
if (operation is null || operation is ILiteralOperation) return null;
if (operation is IBinaryOperation)
{
return StringSyntaxKind.ConcatenatedString;
96 changes: 69 additions & 27 deletions test/Dapper.AOT.Test/Verifiers/SqlDetection.cs
Original file line number Diff line number Diff line change
@@ -184,41 +184,82 @@ static class Program
{
static void Main()
{
using var conn = new SqlConnection("my connection string here");
string name = "abc";
conn.{|#0:Execute|}("""
select Id, Name, Age
from Users
where Id = @id
and Name = @name
and Age = @age
and Something = {|#1:null|}
""", new
{
name,
id = 42,
age = 24,
});
using var conn = new SqlConnection("my connection string here");
string name = "abc";
conn.{|#0:Execute|}("""
select Id, Name, Age
from Users
where Id = @id
and Name = @name
and Age = @age
and Something = {|#1:null|}
""", new
{
name,
id = 42,
age = 24,
});

using var cmd = new SqlCommand("should ' verify this too", conn);
cmd.CommandText = """
select Id, Name, Age
from Users
where Id = @id
and Name = @name
and Age = @age
and Something = {|#2:null|}
""";
cmd.ExecuteNonQuery();
using var cmd = new SqlCommand("should {|#3:|}' verify this too", conn);
cmd.CommandText = """
select Id, Name, Age
from Users
where Id = @id
and Name = @name
and Age = @age
and Something = {|#2:null|}
""";
cmd.ExecuteNonQuery();
}
}
"""", [], [
// (not enabled) Diagnostic(DapperAnalyzer.Diagnostics.DapperAotNotEnabled).WithLocation(0).WithArguments(1),
Diagnostic(DapperAnalyzer.Diagnostics.ExecuteCommandWithQuery).WithLocation(0),
Diagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison).WithLocation(1),
Diagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison).WithLocation(2),
Diagnostic(DapperAnalyzer.Diagnostics.ParseError).WithLocation(3).WithArguments(46030, "Expected but did not find a closing quotation mark after the character string ' verify this too.")
], SqlSyntax.General, refDapperAot: false);

[Theory]
[InlineData("Microsoft.Data.SqlClient")]
[InlineData("System.Data.SqlClient")]
public Task SqlClientCommandReportsParseError(string @namespace) => CSVerifyAsync($$""""
using {{@namespace}};
using Dapper;

static class Program
{
static void Main()
{
using var conn = new {{@namespace}}.SqlConnection("my connection string here");
using var cmd = new {{@namespace}}.SqlCommand("should {|#0:|}' verify this too", conn);
cmd.ExecuteNonQuery();
}
}
"""", [], [ Diagnostic(DapperAnalyzer.Diagnostics.ParseError).WithLocation(0).WithArguments(46030, "Expected but did not find a closing quotation mark after the character string ' verify this too.") ], SqlSyntax.General, refDapperAot: false);

[Theory]
[InlineData("Microsoft.Data.SqlClient")]
[InlineData("System.Data.SqlClient")]
public Task SqlClientCommandInlineCreationReportsParseError(string @namespace) => CSVerifyAsync($$""""
using {{@namespace}};
using Dapper;

static class Program
{
static void Main()
{
using var conn = new {{@namespace}}.SqlConnection("my connection string here");
RunCommand(new {{@namespace}}.SqlCommand("should {|#0:|}' verify this too", conn));
}

static void RunCommand({{@namespace}}.SqlCommand cmd)
{
cmd.ExecuteNonQuery();
}
}
"""", [], [Diagnostic(DapperAnalyzer.Diagnostics.ParseError).WithLocation(0).WithArguments(46030, "Expected but did not find a closing quotation mark after the character string ' verify this too.")], SqlSyntax.General, refDapperAot: false);

[Fact]
public Task VBSmokeTestVanilla() => VBVerifyAsync("""
Imports Dapper
@@ -235,7 +276,7 @@ from Users
and Age = @age
and Something = {|#1:null|}", New With {name, .id = 42, .age = 24 })

Using cmd As New SqlCommand("should ' verify this too", conn)
Using cmd As New SqlCommand("should {|#3:|}' verify this too", conn)
cmd.CommandText = "
select Id, Name, Age
from Users
@@ -251,6 +292,7 @@ End Module
""", [], [
Diagnostic(DapperAnalyzer.Diagnostics.ExecuteCommandWithQuery).WithLocation(0),
Diagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison).WithLocation(1),
Diagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison).WithLocation(2)
Diagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison).WithLocation(2),
Diagnostic(DapperAnalyzer.Diagnostics.ParseError).WithLocation(3).WithArguments(46030, "Expected but did not find a closing quotation mark after the character string ' verify this too.")
], SqlSyntax.General, refDapperAot: false);
}