diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs index 5d9999c9..182f2ba2 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs @@ -432,6 +432,17 @@ StringSyntaxKind.ConcatenatedString or StringSyntaxKind.FormatString location ??= ctx.Operation.Syntax.GetLocation(); + // a constant declared in generated code would have its SQL diagnostics dropped by the driver; + // re-home them onto the call-site argument so they stay visible (inline/same-file keep the token) + // https://github.com/DapperLib/DapperAOT/issues/177 + var sqlLocation = location; + if (sqlSyntax is not null && sqlSyntax.SyntaxTree != sqlSource.Syntax.SyntaxTree + && IsGeneratedDocument(sqlSyntax.SyntaxTree, ctx.Compilation, ctx.CancellationToken)) + { + sqlLocation = sqlSource.Syntax.GetLocation(); + sqlSyntax = null; + } + if (DebugSqlFlags is not null) { var debugModeFlags = DebugSqlFlags.Value; @@ -459,7 +470,7 @@ StringSyntaxKind.ConcatenatedString or StringSyntaxKind.FormatString forgiveSyntaxErrors = true; // we're just taking a punt, honestly goto case SqlSyntax.SqlServer; case SqlSyntax.SqlServer: - var proc = new OperationAnalysisContextTSqlProcessor(ctx, null, flags, location, sqlSyntax); + var proc = new OperationAnalysisContextTSqlProcessor(ctx, null, flags, sqlLocation, sqlSyntax); proc.Execute(sql!, parameters); parseFlags = proc.Flags; // paramMembers); diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index a43c1d22..f962f415 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -1594,6 +1594,42 @@ internal static bool IsCommand(INamedTypeSymbol type) return false; } + public static bool IsGeneratedDocument(SyntaxTree tree, Compilation compilation, CancellationToken cancellationToken) + { + // explicit editorconfig `generated_code` + switch (compilation.Options.SyntaxTreeOptionsProvider?.IsGenerated(tree, cancellationToken)) + { + case GeneratedKind.MarkedGenerated: return true; + case GeneratedKind.NotGenerated: return false; + } + + // file-name convention + var name = System.IO.Path.GetFileNameWithoutExtension(tree.FilePath); + if (name.StartsWith("TemporaryGeneratedFile_", StringComparison.OrdinalIgnoreCase) + || name.EndsWith(".designer", StringComparison.OrdinalIgnoreCase) + || name.EndsWith(".generated", StringComparison.OrdinalIgnoreCase) + || name.EndsWith(".g", StringComparison.OrdinalIgnoreCase) + || name.EndsWith(".g.i", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // `` header (text scan; only comment trivia contains it, so language-agnostic) + foreach (var trivia in tree.GetRoot(cancellationToken).GetLeadingTrivia()) + { + if (trivia.Span.Length > 0) + { + var text = trivia.ToString(); + if (text.IndexOf("= 0 + || text.IndexOf("= 0) + { + return true; + } + } + } + return false; + } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0042:Deconstruct variable declaration", Justification = "Fine as is; let's not pay the unwrap cost")] public static SqlSyntax? IdentifySqlSyntax(in ParseState ctx, IOperation op, out bool caseSensitive) { diff --git a/test/Dapper.AOT.Test/Verifiers/DAP214.cs b/test/Dapper.AOT.Test/Verifiers/DAP214.cs index 5f8d3d01..62a08032 100644 --- a/test/Dapper.AOT.Test/Verifiers/DAP214.cs +++ b/test/Dapper.AOT.Test/Verifiers/DAP214.cs @@ -40,4 +40,66 @@ public void Foo(DbConnection connection) new { Admin = 1, a = 2 }); } """", DefaultConfig, []); + + private const string SqlClass = """ + static class Sql + { + public const string GetUsers = + "select Id from Users where LoginCount >= {|#2:@MinLogins|}"; + } + """; + + // same class behind an header, as a source generator would emit + private const string GeneratedSqlClass = "// \n" + SqlClass; + + [Fact] // const sql in a generated doc re-homes to the call-site, https://github.com/DapperLib/DapperAOT/issues/177 + public Task ConstSqlInGeneratedDocument() => CSVerifyAsync("""" + using Dapper; + using System.Data.Common; + + [DapperAot] + static class Q + { + public static void ViaConst(DbConnection c) => + c.Query({|#0:Sql.GetUsers|}, {|#1:new { Wrong = 1 }|}); + } + """", DefaultConfig, [ + Diagnostic(Diagnostics.VariableNotDeclared).WithLocation(0).WithArguments("@MinLogins"), + Diagnostic(Diagnostics.SqlParametersNotDetected).WithLocation(1), + ], additionalSources: [GeneratedSqlClass]); + + [Fact] // const sql in a visible sibling doc keeps its declaration token, https://github.com/DapperLib/DapperAOT/issues/177 + public Task ConstSqlInVisibleSiblingDocument() => CSVerifyAsync("""" + using Dapper; + using System.Data.Common; + + [DapperAot] + static class Q + { + public static void ViaConst(DbConnection c) => + c.Query(Sql.GetUsers, {|#1:new { Wrong = 1 }|}); + } + """", DefaultConfig, [ + Diagnostic(Diagnostics.VariableNotDeclared).WithLocation(2).WithArguments("@MinLogins"), + Diagnostic(Diagnostics.SqlParametersNotDetected).WithLocation(1), + ], additionalSources: [SqlClass]); + + [Fact] // const sql in the same doc keeps its declaration token, https://github.com/DapperLib/DapperAOT/issues/177 + public Task ConstSqlInSameDocument() => CSVerifyAsync("""" + using Dapper; + using System.Data.Common; + + [DapperAot] + static class Q + { + public const string GetUsers = + "select Id from Users where LoginCount >= {|#0:@MinLogins|}"; + + public static void ViaConst(DbConnection c) => + c.Query(GetUsers, {|#1:new { Wrong = 1 }|}); + } + """", DefaultConfig, [ + Diagnostic(Diagnostics.VariableNotDeclared).WithLocation(0).WithArguments("@MinLogins"), + Diagnostic(Diagnostics.SqlParametersNotDetected).WithLocation(1), + ]); } diff --git a/test/Dapper.AOT.Test/Verifiers/Verifier.cs b/test/Dapper.AOT.Test/Verifiers/Verifier.cs index abc53ebf..d349234a 100644 --- a/test/Dapper.AOT.Test/Verifiers/Verifier.cs +++ b/test/Dapper.AOT.Test/Verifiers/Verifier.cs @@ -34,11 +34,11 @@ protected static DiagnosticResult InterceptorsGenerated(int handled, int total, internal Task CSVerifyAsync(string source, Func[] transforms, DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None, bool refDapperAot = true, - string? pinDapperPackageVersion = null) + string? pinDapperPackageVersion = null, string[]? additionalSources = null) where TAnalyzer : DiagnosticAnalyzer, new() { var test = new CSharpAnalyzerTest(); - return ExecuteAsync(test, source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot, pinDapperPackageVersion); + return ExecuteAsync(test, source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot, pinDapperPackageVersion, additionalSources); } internal Task CSVerifyAsync(string source, Func[] transforms, @@ -71,9 +71,16 @@ internal Task VBVerifyAsync(string source, internal Task ExecuteAsync(AnalyzerTest test, string source, Func[] transforms, DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags, bool refDapperAot, - string? pinDapperPackageVersion = null) + string? pinDapperPackageVersion = null, string[]? additionalSources = null) { test.TestCode = source; + if (additionalSources is not null) + { + foreach (var additionalSource in additionalSources) + { + test.TestState.Sources.Add(additionalSource); + } + } // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (expected is not null) @@ -214,8 +221,8 @@ class SomeCode internal Task CSVerifyAsync(string source, Func[] transforms, DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None, bool refDapperAot = true, - string? pinDapperPackageVersion = null) - => base.CSVerifyAsync(source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot, pinDapperPackageVersion); + string? pinDapperPackageVersion = null, string[]? additionalSources = null) + => base.CSVerifyAsync(source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot, pinDapperPackageVersion, additionalSources); new internal Task CSVerifyAsync(string source, Func[] transforms,