Skip to content

Commit 25203a3

Browse files
authored
feat: support DbCommand command-text via constructor string sql query parse (#135)
* support SqlCommand ctors for Sql parsing * address PR comments * and prettify * adjust for VB validation
1 parent d162ec9 commit 25203a3

File tree

4 files changed

+135
-36
lines changed

4 files changed

+135
-36
lines changed

src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ private void OnCompilationStart(CompilationStartAnalysisContext context)
3535
// per-run state (in particular, so we can have "first time only" diagnostics)
3636
var state = new AnalyzerState(context);
3737

38-
// respond to method usages
39-
context.RegisterOperationAction(state.OnOperation, OperationKind.Invocation, OperationKind.SimpleAssignment);
38+
context.RegisterOperationAction(state.OnOperation,
39+
OperationKind.Invocation, // for Dapper method invocations
40+
OperationKind.SimpleAssignment, // for assignments of query
41+
OperationKind.ObjectCreation // for instantiating Command objects
42+
);
4043

4144
// final actions
4245
context.RegisterCompilationEndAction(state.OnCompilationEndAction);
@@ -111,8 +114,9 @@ public void OnOperation(OperationAnalysisContext ctx)
111114
try
112115
{
113116
// we'll look for:
114-
// method calls with a parameter called "sql" or marked [Sql]
115-
// property assignments to "CommandText"
117+
// - method calls with a parameter called "sql" or marked [Sql]
118+
// - property assignments to "CommandText"
119+
// - allocation of SqlCommand (i.e. `new SqlCommand(queryString, ...)` )
116120
switch (ctx.Operation.Kind)
117121
{
118122
case OperationKind.Invocation when ctx.Operation is IInvocationOperation invoke:
@@ -155,6 +159,21 @@ public void OnOperation(OperationAnalysisContext ctx)
155159
ValidatePropertyUsage(ctx, assignment.Value, false);
156160
}
157161
}
162+
break;
163+
case OperationKind.ObjectCreation when ctx.Operation is IObjectCreationOperation objectCreationOperation:
164+
165+
var ctor = objectCreationOperation.Constructor;
166+
var receiverType = ctor?.ReceiverType;
167+
168+
if (ctor is not null && IsSqlClient(receiverType))
169+
{
170+
var sqlParam = ctor.Parameters.FirstOrDefault();
171+
if (sqlParam is not null && sqlParam.Type.SpecialType == SpecialType.System_String && sqlParam.Name == "cmdText")
172+
{
173+
ValidateParameterUsage(ctx, objectCreationOperation.Arguments.First(), sqlUsage: objectCreationOperation);
174+
}
175+
}
176+
158177
break;
159178
}
160179
}
@@ -327,11 +346,11 @@ private void ValidateSurroundingLinqUsage(in OperationAnalysisContext ctx, Opera
327346
}
328347
}
329348

330-
private void ValidateParameterUsage(in OperationAnalysisContext ctx, IOperation sqlSource)
349+
private void ValidateParameterUsage(in OperationAnalysisContext ctx, IOperation sqlSource, IOperation? sqlUsage = null)
331350
{
332351
// TODO: check other parameters for special markers like command type?
333352
var flags = SqlParseInputFlags.None;
334-
ValidateSql(ctx, sqlSource, flags, SqlParameters.None);
353+
ValidateSql(ctx, sqlSource, flags, SqlParameters.None, sqlUsageOperation: sqlUsage);
335354
}
336355

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

374393
private void ValidateSql(in OperationAnalysisContext ctx, IOperation sqlSource, SqlParseInputFlags flags,
375-
ImmutableArray<SqlParameter> parameters, Location? location = null)
394+
ImmutableArray<SqlParameter> parameters, Location? location = null, IOperation? sqlUsageOperation = null)
376395
{
377396
var parseState = new ParseState(ctx);
378397

379398
// should we consider this as a syntax we can handle?
380-
var syntax = IdentifySqlSyntax(parseState, ctx.Operation, out var caseSensitive) ?? DefaultSqlSyntax ?? SqlSyntax.General;
399+
var syntax = IdentifySqlSyntax(parseState, sqlUsageOperation ?? ctx.Operation, out var caseSensitive) ?? DefaultSqlSyntax ?? SqlSyntax.General;
381400
switch (syntax)
382401
{
383402
case SqlSyntax.SqlServer:

src/Dapper.AOT.Analyzers/Internal/Inspection.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,24 @@ public static bool IsEnabled(in ParseState ctx, IOperation op, string attributeN
146146
return false;
147147
}
148148

149+
public static bool IsSqlClient(ITypeSymbol? typeSymbol) => typeSymbol is
150+
{
151+
Name: "SqlCommand",
152+
ContainingNamespace:
153+
{
154+
Name: "SqlClient",
155+
ContainingNamespace:
156+
{
157+
Name: "Data",
158+
ContainingNamespace:
159+
{
160+
Name: "Microsoft" or "System", // either Microsoft.Data.SqlClient or System.Data.SqlClient
161+
ContainingNamespace.IsGlobalNamespace: true
162+
}
163+
}
164+
}
165+
};
166+
149167
public static bool IsDapperAttribute(AttributeData attrib)
150168
=> attrib.AttributeClass is
151169
{
@@ -1432,6 +1450,26 @@ internal static bool IsCommand(INamedTypeSymbol type)
14321450
}
14331451
}
14341452
}
1453+
else if (op is IObjectCreationOperation objectCreationOp)
1454+
{
1455+
var ctorTypeNamespace = objectCreationOp.Type?.ContainingNamespace;
1456+
var ctorTypeName = objectCreationOp.Type?.Name;
1457+
1458+
if (ctorTypeNamespace is not null && ctorTypeName is not null)
1459+
{
1460+
foreach (var candidate in KnownConnectionTypes)
1461+
{
1462+
var current = ctorTypeNamespace;
1463+
if (ctorTypeName == candidate.Command
1464+
&& AssertAndAscend(ref current, candidate.Namespace0)
1465+
&& AssertAndAscend(ref current, candidate.Namespace1)
1466+
&& AssertAndAscend(ref current, candidate.Namespace2))
1467+
{
1468+
return candidate.Syntax;
1469+
}
1470+
}
1471+
}
1472+
}
14351473

14361474
return null;
14371475

src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ internal virtual bool IsGlobalStatement(SyntaxNode syntax, out SyntaxNode? entry
110110

111111
internal virtual StringSyntaxKind? TryDetectOperationStringSyntaxKind(IOperation operation)
112112
{
113-
if (operation is null) return null;
113+
if (operation is null || operation is ILiteralOperation) return null;
114114
if (operation is IBinaryOperation)
115115
{
116116
return StringSyntaxKind.ConcatenatedString;

test/Dapper.AOT.Test/Verifiers/SqlDetection.cs

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -184,41 +184,82 @@ static class Program
184184
{
185185
static void Main()
186186
{
187-
using var conn = new SqlConnection("my connection string here");
188-
string name = "abc";
189-
conn.{|#0:Execute|}("""
190-
select Id, Name, Age
191-
from Users
192-
where Id = @id
193-
and Name = @name
194-
and Age = @age
195-
and Something = {|#1:null|}
196-
""", new
197-
{
198-
name,
199-
id = 42,
200-
age = 24,
201-
});
187+
using var conn = new SqlConnection("my connection string here");
188+
string name = "abc";
189+
conn.{|#0:Execute|}("""
190+
select Id, Name, Age
191+
from Users
192+
where Id = @id
193+
and Name = @name
194+
and Age = @age
195+
and Something = {|#1:null|}
196+
""", new
197+
{
198+
name,
199+
id = 42,
200+
age = 24,
201+
});
202202
203-
using var cmd = new SqlCommand("should ' verify this too", conn);
204-
cmd.CommandText = """
205-
select Id, Name, Age
206-
from Users
207-
where Id = @id
208-
and Name = @name
209-
and Age = @age
210-
and Something = {|#2:null|}
211-
""";
212-
cmd.ExecuteNonQuery();
203+
using var cmd = new SqlCommand("should {|#3:|}' verify this too", conn);
204+
cmd.CommandText = """
205+
select Id, Name, Age
206+
from Users
207+
where Id = @id
208+
and Name = @name
209+
and Age = @age
210+
and Something = {|#2:null|}
211+
""";
212+
cmd.ExecuteNonQuery();
213213
}
214214
}
215215
"""", [], [
216216
// (not enabled) Diagnostic(DapperAnalyzer.Diagnostics.DapperAotNotEnabled).WithLocation(0).WithArguments(1),
217217
Diagnostic(DapperAnalyzer.Diagnostics.ExecuteCommandWithQuery).WithLocation(0),
218218
Diagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison).WithLocation(1),
219219
Diagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison).WithLocation(2),
220+
Diagnostic(DapperAnalyzer.Diagnostics.ParseError).WithLocation(3).WithArguments(46030, "Expected but did not find a closing quotation mark after the character string ' verify this too.")
220221
], SqlSyntax.General, refDapperAot: false);
221222

223+
[Theory]
224+
[InlineData("Microsoft.Data.SqlClient")]
225+
[InlineData("System.Data.SqlClient")]
226+
public Task SqlClientCommandReportsParseError(string @namespace) => CSVerifyAsync($$""""
227+
using {{@namespace}};
228+
using Dapper;
229+
230+
static class Program
231+
{
232+
static void Main()
233+
{
234+
using var conn = new {{@namespace}}.SqlConnection("my connection string here");
235+
using var cmd = new {{@namespace}}.SqlCommand("should {|#0:|}' verify this too", conn);
236+
cmd.ExecuteNonQuery();
237+
}
238+
}
239+
"""", [], [ 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);
240+
241+
[Theory]
242+
[InlineData("Microsoft.Data.SqlClient")]
243+
[InlineData("System.Data.SqlClient")]
244+
public Task SqlClientCommandInlineCreationReportsParseError(string @namespace) => CSVerifyAsync($$""""
245+
using {{@namespace}};
246+
using Dapper;
247+
248+
static class Program
249+
{
250+
static void Main()
251+
{
252+
using var conn = new {{@namespace}}.SqlConnection("my connection string here");
253+
RunCommand(new {{@namespace}}.SqlCommand("should {|#0:|}' verify this too", conn));
254+
}
255+
256+
static void RunCommand({{@namespace}}.SqlCommand cmd)
257+
{
258+
cmd.ExecuteNonQuery();
259+
}
260+
}
261+
"""", [], [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);
262+
222263
[Fact]
223264
public Task VBSmokeTestVanilla() => VBVerifyAsync("""
224265
Imports Dapper
@@ -235,7 +276,7 @@ from Users
235276
and Age = @age
236277
and Something = {|#1:null|}", New With {name, .id = 42, .age = 24 })
237278
238-
Using cmd As New SqlCommand("should ' verify this too", conn)
279+
Using cmd As New SqlCommand("should {|#3:|}' verify this too", conn)
239280
cmd.CommandText = "
240281
select Id, Name, Age
241282
from Users
@@ -251,6 +292,7 @@ End Module
251292
""", [], [
252293
Diagnostic(DapperAnalyzer.Diagnostics.ExecuteCommandWithQuery).WithLocation(0),
253294
Diagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison).WithLocation(1),
254-
Diagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison).WithLocation(2)
295+
Diagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison).WithLocation(2),
296+
Diagnostic(DapperAnalyzer.Diagnostics.ParseError).WithLocation(3).WithArguments(46030, "Expected but did not find a closing quotation mark after the character string ' verify this too.")
255297
], SqlSyntax.General, refDapperAot: false);
256298
}

0 commit comments

Comments
 (0)