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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
35 changes: 27 additions & 8 deletions src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
38 changes: 38 additions & 0 deletions src/Dapper.AOT.Analyzers/Internal/Inspection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
96 changes: 69 additions & 27 deletions test/Dapper.AOT.Test/Verifiers/SqlDetection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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);
}