diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index ea5f5f1b..d6503643 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -111,6 +111,10 @@ public static bool IsNestedSqlMapperType(ITypeSymbol? type, string name, TypeKin { return syntax; } + if (syntax.IsGlobalStatement(out var entryPoint)) + { + return entryPoint; + } syntax = syntax.Parent; } return null; diff --git a/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.CSharp.cs b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.CSharp.cs index c055d10f..8b3a2217 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.CSharp.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.CSharp.cs @@ -33,6 +33,16 @@ internal override bool IsMemberAccess(SyntaxNode syntax) internal override bool IsMethodDeclaration(SyntaxNode syntax) => syntax.IsKind(SyntaxKind.MethodDeclaration); + internal override bool IsGlobalStatement(SyntaxNode syntax, out SyntaxNode? entryPoint) + { + if (syntax.IsKind(SyntaxKind.GlobalStatement)) + { + // compilation-unit relating to the top-level statement entry point + entryPoint = syntax.Parent; + return true; + } + return base.IsGlobalStatement(syntax, out entryPoint); + } internal override bool IsName(SyntaxNode syntax) => syntax is SimpleNameSyntax; // NameSyntax? diff --git a/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs index cf131453..4fc44163 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs @@ -26,6 +26,9 @@ public static bool IsMemberAccess(this SyntaxNode syntax) public static bool IsMethodDeclaration(this SyntaxNode syntax) => GetHelper(syntax.Language).IsMethodDeclaration(syntax); + public static bool IsGlobalStatement(this SyntaxNode syntax, out SyntaxNode? entryPoint) + => GetHelper(syntax.Language).IsGlobalStatement(syntax, out entryPoint); + public static StringSyntaxKind? TryDetectOperationStringSyntaxKind(this IOperation operation) => GetHelper(operation.Syntax?.Language).TryDetectOperationStringSyntaxKind(operation); @@ -95,6 +98,11 @@ internal abstract partial class LanguageHelper { internal abstract bool IsMemberAccess(SyntaxNode syntax); internal abstract bool IsMethodDeclaration(SyntaxNode syntax); + internal virtual bool IsGlobalStatement(SyntaxNode syntax, out SyntaxNode? entryPoint) + { + entryPoint = null; + return false; + } internal abstract bool TryGetLiteralToken(SyntaxNode syntax, out SyntaxToken token); internal abstract bool TryGetStringSpan(SyntaxToken token, string text, scoped in TSqlProcessor.Location location, out int skip, out int take); internal abstract bool IsName(SyntaxNode syntax); diff --git a/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.input.cs b/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.input.cs new file mode 100644 index 00000000..c6fc8d4f --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.input.cs @@ -0,0 +1,65 @@ +using Dapper; +using System; +using System.Data.Common; +using System.Threading.Tasks; + + +[module: DapperAot] + +#if !NETFRAMEWORK +Console.WriteLine(System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported ? "Running with JIT" : "Running with AOT"); +#endif + +// Dapper.AOT, at least at the moment (it was released like 4 days ago), doesn't seem to be able to handle things if we don't put it in a class +app.MapGet("/", async (HttpContext context, SqlConnectionFactory sqlConnectionFactory, Stack stack) => +{ +#if !NETFRAMEWORK + await +#endif + using var connection = sqlConnectionFactory(); + var item = await connection.QuerySingleAsync("SELECT SomeId, SomeText FROM SomeThing LIMIT 1"); + //context.Response.Headers["stack"] = stack.Name; + return Results.Ok(item); +}); + +public static class Handlers +{ + public static async Task Root(HttpContext context, SqlConnectionFactory sqlConnectionFactory, Stack stack) + { +#if !NETFRAMEWORK + await +#endif + using var connection = sqlConnectionFactory(); + var item = await connection.QuerySingleAsync("SELECT SomeId, SomeText FROM SomeThing LIMIT 1"); + // context.Response.Headers["stack"] = stack.Name; + return Results.Ok(item); + } +} + +public record SomeThing(int SomeId, string SomeText); + +public record Stack(string Name); + + +// spoof enough fake http code to build +public static class app +{ + public static void MapGet(string path, Delegate handler) => throw new NotImplementedException(); +} + +public delegate DbConnection SqlConnectionFactory(); +public interface IResult { } +public static class Results +{ + public static IResult Ok(object whatever) => throw new NotImplementedException(); +} +public class HttpContext {} + +#if NETFRAMEWORK +namespace System.Runtime.CompilerServices +{ + file static class IsExternalInit + { + } +} +#endif \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.cs b/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.cs new file mode 100644 index 00000000..aee9995e --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.cs @@ -0,0 +1,114 @@ +#nullable enable +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\TopLevelStatements.input.cs", 20, 33)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\TopLevelStatements.input.cs", 33, 37)] + internal static global::System.Threading.Tasks.Task QuerySingleAsync0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, Async, TypedResult, SingleRow, Text, AtLeastOne, AtMostOne, BindResultsByName + // returns data: global::SomeThing + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(param is null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QuerySingleAsync(param, RowFactory0.Instance); + + } + + private class CommonCommandFactory : global::Dapper.CommandFactory + { + public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args) + { + var cmd = base.GetCommand(connection, sql, commandType, args); + // apply special per-provider command initialization logic for OracleCommand + if (cmd is global::Oracle.ManagedDataAccess.Client.OracleCommand cmd0) + { + cmd0.BindByName = true; + cmd0.InitialLONGFetchSize = -1; + + } + return cmd; + } + + } + + private static readonly CommonCommandFactory DefaultCommandFactory = new(); + + private sealed class RowFactory0 : global::Dapper.RowFactory + { + internal static readonly RowFactory0 Instance = new(); + private RowFactory0() {} + public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) + { + for (int i = 0; i < tokens.Length; i++) + { + int token = -1; + var name = reader.GetName(columnOffset); + var type = reader.GetFieldType(columnOffset); + switch (NormalizedHash(name)) + { + case 3328690628U when NormalizedEquals(name, "someid"): + token = type == typeof(int) ? 0 : 2; // two tokens for right-typed and type-flexible + break; + case 1017211458U when NormalizedEquals(name, "sometext"): + token = type == typeof(string) ? 1 : 3; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::SomeThing Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + int value0 = default; + string? value1 = default; + foreach (var token in tokens) + { + switch (token) + { + case 0: + value0 = reader.GetInt32(columnOffset); + break; + case 2: + value0 = GetValue(reader, columnOffset); + break; + case 1: + value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 3: + value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return new global::SomeThing(value0, value1); + } + } + + + } +} +namespace System.Runtime.CompilerServices +{ + // this type is needed by the compiler to implement interceptors - it doesn't need to + // come from the runtime itself, though + + [global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber) + { + _ = path; + _ = lineNumber; + _ = columnNumber; + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.netfx.cs new file mode 100644 index 00000000..aee9995e --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.netfx.cs @@ -0,0 +1,114 @@ +#nullable enable +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\TopLevelStatements.input.cs", 20, 33)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\TopLevelStatements.input.cs", 33, 37)] + internal static global::System.Threading.Tasks.Task QuerySingleAsync0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, Async, TypedResult, SingleRow, Text, AtLeastOne, AtMostOne, BindResultsByName + // returns data: global::SomeThing + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(param is null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QuerySingleAsync(param, RowFactory0.Instance); + + } + + private class CommonCommandFactory : global::Dapper.CommandFactory + { + public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args) + { + var cmd = base.GetCommand(connection, sql, commandType, args); + // apply special per-provider command initialization logic for OracleCommand + if (cmd is global::Oracle.ManagedDataAccess.Client.OracleCommand cmd0) + { + cmd0.BindByName = true; + cmd0.InitialLONGFetchSize = -1; + + } + return cmd; + } + + } + + private static readonly CommonCommandFactory DefaultCommandFactory = new(); + + private sealed class RowFactory0 : global::Dapper.RowFactory + { + internal static readonly RowFactory0 Instance = new(); + private RowFactory0() {} + public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) + { + for (int i = 0; i < tokens.Length; i++) + { + int token = -1; + var name = reader.GetName(columnOffset); + var type = reader.GetFieldType(columnOffset); + switch (NormalizedHash(name)) + { + case 3328690628U when NormalizedEquals(name, "someid"): + token = type == typeof(int) ? 0 : 2; // two tokens for right-typed and type-flexible + break; + case 1017211458U when NormalizedEquals(name, "sometext"): + token = type == typeof(string) ? 1 : 3; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::SomeThing Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + int value0 = default; + string? value1 = default; + foreach (var token in tokens) + { + switch (token) + { + case 0: + value0 = reader.GetInt32(columnOffset); + break; + case 2: + value0 = GetValue(reader, columnOffset); + break; + case 1: + value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 3: + value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return new global::SomeThing(value0, value1); + } + } + + + } +} +namespace System.Runtime.CompilerServices +{ + // this type is needed by the compiler to implement interceptors - it doesn't need to + // come from the runtime itself, though + + [global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber) + { + _ = path; + _ = lineNumber; + _ = columnNumber; + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.netfx.txt new file mode 100644 index 00000000..5d21549b --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.netfx.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 2 of 2 possible call-sites using 1 interceptors, 0 commands and 1 readers diff --git a/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.txt b/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.txt new file mode 100644 index 00000000..5d21549b --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 2 of 2 possible call-sites using 1 interceptors, 0 commands and 1 readers