Skip to content

Commit

Permalink
investigate and fix #86 (#87)
Browse files Browse the repository at this point in the history
* minimal repro of #86

* handle top-level statements
(also: make the test pass on netfx)
  • Loading branch information
mgravell authored Nov 20, 2023
1 parent 5d539db commit 07a183e
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/Dapper.AOT.Analyzers/Internal/Inspection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.CSharp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
8 changes: 8 additions & 0 deletions src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
65 changes: 65 additions & 0 deletions test/Dapper.AOT.Test/Interceptors/TopLevelStatements.input.cs
Original file line number Diff line number Diff line change
@@ -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<SomeThing>("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<IResult> Root(HttpContext context, SqlConnectionFactory sqlConnectionFactory, Stack stack)
{
#if !NETFRAMEWORK
await
#endif
using var connection = sqlConnectionFactory();
var item = await connection.QuerySingleAsync<SomeThing>("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
114 changes: 114 additions & 0 deletions test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.cs
Original file line number Diff line number Diff line change
@@ -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<global::SomeThing> 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<T> : global::Dapper.CommandFactory<T>
{
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<object?> DefaultCommandFactory = new();

private sealed class RowFactory0 : global::Dapper.RowFactory<global::SomeThing>
{
internal static readonly RowFactory0 Instance = new();
private RowFactory0() {}
public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span<int> 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<int> 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<int>(reader, columnOffset);
break;
case 1:
value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset);
break;
case 3:
value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue<string>(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;
}
}
}
114 changes: 114 additions & 0 deletions test/Dapper.AOT.Test/Interceptors/TopLevelStatements.output.netfx.cs
Original file line number Diff line number Diff line change
@@ -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<global::SomeThing> 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<T> : global::Dapper.CommandFactory<T>
{
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<object?> DefaultCommandFactory = new();

private sealed class RowFactory0 : global::Dapper.RowFactory<global::SomeThing>
{
internal static readonly RowFactory0 Instance = new();
private RowFactory0() {}
public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span<int> 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<int> 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<int>(reader, columnOffset);
break;
case 1:
value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset);
break;
case 3:
value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue<string>(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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 07a183e

Please sign in to comment.