diff --git a/Directory.Build.props b/Directory.Build.props index db609fb0..22ab1eef 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,6 @@ False Build time (AOT) tools for Dapper $(MSBuildThisFileDirectory)Dapper.AOT.snk - 9 enable Dapper Apache-2.0 @@ -26,6 +25,7 @@ en-US false latest-Recommended + preview ($Features);strict diff --git a/Directory.Packages.props b/Directory.Packages.props index ffab96e0..ba644146 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,6 +13,7 @@ + @@ -30,5 +31,7 @@ + + \ No newline at end of file diff --git a/docs/rules/DAP001.md b/docs/rules/DAP001.md new file mode 100644 index 00000000..e54bfc53 --- /dev/null +++ b/docs/rules/DAP001.md @@ -0,0 +1,7 @@ +# DAP001 + +We're focusing on delivering the most common Dapper scenarios first; not everything currently has AOT support. + +The good news is: we'll just let regular vanilla Dapper handle the code, so it should still work fine. Just; without AOT. Sorry. + +If you think a particular API deserves attention: [*let us know*](https://github.com/DapperLib/DapperAOT/issues).. \ No newline at end of file diff --git a/docs/rules/DAP006.md b/docs/rules/DAP006.md new file mode 100644 index 00000000..4a5a5eeb --- /dev/null +++ b/docs/rules/DAP006.md @@ -0,0 +1,21 @@ +# DAP006 + +In C#, tuple-types like `(int id, string name)` are implemented by the compiler using trickery; that trickery is passed *outwards* to *consuming* +code, but that name information (`"id"` and `"name"`) is not available at runtime and the names are not passed *inwards* at runtime to library code. + +Because of this, **vanilla** (runtime-only) Dapper can't really use such values for parameters. Since this would involve "boxing" *anyway*, this isn't +really a problem (although Dapper.AOT *can* see the names at build-time, and can act correctly). + +Short version: don't use this syntax for parameters. Perhaps use anonymous types instead. + +Bad: + +``` csharp +conn.Execute("somesql", (id: 42, name: "abc")); +``` + +Good: + +``` csharp +conn.Execute("somesql", new { id = 42, name = "abc" }); +``` diff --git a/docs/rules/DAP007.md b/docs/rules/DAP007.md new file mode 100644 index 00000000..a55a2fcd --- /dev/null +++ b/docs/rules/DAP007.md @@ -0,0 +1,18 @@ +# DAP007 + +The [`CommandType`](https://learn.microsoft.com/dotnet/api/system.data.commandtype) in ADO.NET is quite important. If Dapper +can *see* your command-type at compile-time, but knows that it isn't something it understands, then it isn't sure what you +want to do. If you genuinely have a need to use an unknown command-type, maybe [log an issue](https://github.com/DapperLib/DapperAOT/issues), +explaining the context! + +Bad: + +``` csharp +conn.Execute("somesql", commandType: (CommandType)42); +``` + +Good: + +``` csharp +conn.Execute("somesql", commandType: CommandType.StoredProcedure); +``` diff --git a/docs/rules/DAP009.md b/docs/rules/DAP009.md new file mode 100644 index 00000000..e787d243 --- /dev/null +++ b/docs/rules/DAP009.md @@ -0,0 +1,7 @@ +# DAP009 + +Huh; I guess we missed an overload, and you found a parameter for Dapper that we aren't currently expecting. + +Sorry, mea cupla. + +Please [log an issue](https://github.com/DapperLib/DapperAOT/issues) with example code; we want to fix this! \ No newline at end of file diff --git a/docs/rules/DAP011.md b/docs/rules/DAP011.md new file mode 100644 index 00000000..3739ce8f --- /dev/null +++ b/docs/rules/DAP011.md @@ -0,0 +1,22 @@ +# DAP011 + +Historically, Dapper has bound tuples *positionally*, because it does not have access to name metadata. + +It looks like you have bind-by-name semantics enabled (`[BindTupleByName]`), and you're using named tuples - so you +probably expect this query to be bound ... by name; however, this query is going to be executed by vanilla non-AOT Dapper, +so: that won't happen. + +Suggestions: + +- remove `[BindTupleByName]` or use `[BindTupleByName(false)]` to explicitly acknowledge that you're using positional binding +- find out why Dapper.AOT can't help with this method, and fix that - maybe add `[DapperAot]`? +- use a regular non-tuple type - maybe a `struct record` +- remove the names from the tuple (because: they don't have any effect here) + +As an example of the `struct record` suggestion: + +``` csharp +var data = conn.Query("somesql"); +// ... +public readonly record struct MyData(string name, int id); +``` diff --git a/docs/rules/DAP012.md b/docs/rules/DAP012.md new file mode 100644 index 00000000..f3ec3bd8 --- /dev/null +++ b/docs/rules/DAP012.md @@ -0,0 +1,19 @@ +# DAP012 + +As explained in [DAP006](DAP006), Dapper.AOT can make use of the names in tuple syntax (for example `(id: 42, name: "abc")`). However, because this represents +a change from runtime-only Dapper, we want to be sure that this is what you *wanted*. + +To help clarify your intent, please add `[BindTupleByName]` or `[BindTupleByName(false)]` to enable/disable use of name data. Or alternatively, +just use an anonymous type instead of a tuple: + +Ambiguous (unless `BindTupleByNameAttribute` is specified): + +``` csharp +var data = conn.Query("somesql", (id: 42, name: "abc")); +``` + +Always clear: + +``` csharp +var data = conn.Query("somesql", new { id = 42, name = "abc" }); +``` diff --git a/docs/rules/DAP013.md b/docs/rules/DAP013.md new file mode 100644 index 00000000..3ce5bcdf --- /dev/null +++ b/docs/rules/DAP013.md @@ -0,0 +1,20 @@ +# DAP013 + +Dapper.AOT does not currently support tuple-type results. It absolutely *will*; I just haven't got there yet. + +This includes both positional and nomimal usage, i.e. `Query<(int, string)>` and `Query<(int id, string name)>`. + +Suggestions: + +- use a regular non-tuple type - maybe a `struct record` +- use vanilla Dapper by adding `[DapperAot(false)]` +- use a non-tuple type as the result, noting that this will be bound by position **only** (see [DAP011](DAP011)) + + +As an example of the `struct record` suggestion: + +``` csharp +var data = conn.Query("somesql"); +// ... +public readonly record struct MyData(string name, int id); +``` diff --git a/docs/rules/DAP014.md b/docs/rules/DAP014.md new file mode 100644 index 00000000..fe421d69 --- /dev/null +++ b/docs/rules/DAP014.md @@ -0,0 +1,18 @@ +# DAP014 + +As explained in [DAP006](DAP006), Dapper.AOT can make use of the names in tuple syntax (for example `(id: 42, name: "abc")`). That's the theory. In practice, +we haven't implemented that just yet, so ... it isn't going to work. Hey, we're human. + +Maybe just use an anonymous type instead? + +Not yet supported: + +``` csharp +var data = conn.Query("somesql", (id: 42, name: "abc")); +``` + +Good: + +``` csharp +var data = conn.Query("somesql", new { id = 42, name = "abc" }); +``` diff --git a/docs/rules/DAP015.md b/docs/rules/DAP015.md new file mode 100644 index 00000000..856e7dd6 --- /dev/null +++ b/docs/rules/DAP015.md @@ -0,0 +1,24 @@ +# DAP015 + +To handle parameters efficiently, Dapper.AOT first needs to understand what you're doing. If your parameter data is `object`, `dynamic` etc, +then we can't do that at compile-time, so: Dapper.AOT can't help. + +Either used typed parameters, or disable Dapper.AOT (`[DapperAot(false)]`). + +If you're using `DynamicParameters`, note that this is often *wildly* overused. If your usage looks like + +``` csharp +var args = new DynamicParameters(); +args.Add("id", 42); +args.Add("name", "Fred"); +conn.Execute("somesql", args); +``` + +then you can make this *much* more efficient (and Dapper.AOT-friendly) with simply: + +``` csharp +conn.Execute("somesql", new { id = 42, name = "Fred" }); +``` + +If you're using `DynamicParameters` for things like output-parameters, Dapper.AOT has new options for that; you can use `[DbValue(Direction = ...)]` on +properties of a custom type. \ No newline at end of file diff --git a/docs/rules/DAP016.md b/docs/rules/DAP016.md new file mode 100644 index 00000000..e701151b --- /dev/null +++ b/docs/rules/DAP016.md @@ -0,0 +1,7 @@ +# DAP016 + +As discussed in [DAP015](DAP015), Dapper.AOT wants to understand your data at compile-time, and it can't do that when generics are involved. + +Specifically, you can't use a `T args` (or `TSomething args`) as the parameters to a Dapper call. + +Either use non-generic arguments when calling Dapper, or disable Dapper.AOT with `[DapperAot(false)]`. \ No newline at end of file diff --git a/docs/rules/DAP017.md b/docs/rules/DAP017.md new file mode 100644 index 00000000..faaae17d --- /dev/null +++ b/docs/rules/DAP017.md @@ -0,0 +1,21 @@ +# DAP017 + +Dapper.AOT works by generating code to handle your types; to do that, those types need to be accessible to the generated code; that means they must have +at least `internal` accessibility. + +Since Dapper.AOT is adding code to your assembly, this problem is limited to nested types that are `private`, `protected`, `private protected`, or similar - +basically anywhere where *any other part of your code* couldn't mention the type by name. + +Suggestions: + +- make the type `internal`: + - `private` => `internal` + - `protected` => `protected internal` +- disable Dapper.AOT (`[DapperAot(false)]`) + + + +> Can Dapper.AOT not just generate the code inside the relevant types via `partial` types? + +No; generators are limited to placing their output in pre-defined namespaces, which precludes generating code directly inside your types. +We may be able to revisit this at a later date by having you add additional configuration to your project file, allowing Dapper.AOT to generate inside *your* namespaces. diff --git a/docs/rules/DAP025.md b/docs/rules/DAP025.md new file mode 100644 index 00000000..464d6cb9 --- /dev/null +++ b/docs/rules/DAP025.md @@ -0,0 +1,24 @@ +# DAP025 + +It looks like you're using an `Execute` operation (which doesn't expect rows) to +invoke something that *definitely has a query*. Those results will be +silently ignored, which probably isn't what you intended. Did you mean to use +a `Query` API? + +Bad: + +``` c# +conn.Execute(""" + select Id, Name + from SomeLookupTable + """); +``` + +Good: + +``` c# +var rows = conn.Query(""" + select Id, Name + from SomeLookupTable + """); +``` \ No newline at end of file diff --git a/docs/rules/DAP026.md b/docs/rules/DAP026.md new file mode 100644 index 00000000..d941c93e --- /dev/null +++ b/docs/rules/DAP026.md @@ -0,0 +1,33 @@ +# DAP026 + +It looks like you're using a `Query` operation (which expects rows) to +invoke something that *definitely does not have a query*. This won't work; did you +mean to use an `Execute` API, or fetch something? + +Bad: + +``` c# +var id = conn.QuerySingle(""" + insert SomeLookupTable (Name) + values ('North') + """); +``` + +Good: + +``` c# +var id = conn.QuerySingle(""" + insert SomeLookupTable (Name) + output inserted.Id + values ('North') + """); +``` + +Also fine: + +``` c# +conn.Execute(""" + insert SomeLookupTable (Name) + values ('North') + """); +``` \ No newline at end of file diff --git a/docs/rules/DAP027.md b/docs/rules/DAP027.md new file mode 100644 index 00000000..b13b3342 --- /dev/null +++ b/docs/rules/DAP027.md @@ -0,0 +1,21 @@ +# DAP027 + +The `Query` APIs allow multiple rows to be returned, which is *fine* - but if you only want a single row, there are +scenario-specific APIs that allow for significant additional optimizations. The +`QueryFirst`, `QueryFirstOrDefault`, `QuerySingle` and `QuerySingleOrDefault` work as you would expect, and compare directly +to the corresponding LINQ [`First`](https://learn.microsoft.com/dotnet/api/system.linq.enumerable.first), +[`FirstOrDefault`](https://learn.microsoft.com/dotnet/api/system.linq.enumerable.firstordefault), +[`Single`](https://learn.microsoft.com/dotnet/api/system.linq.enumerable.single) +and [`SingleOrDefault`](https://learn.microsoft.com/dotnet/api/system.linq.enumerable.singleordefault) methods. + +Bad: + +``` c# +var order = conn.Query(sql, args).First(); +``` + +Good: + +``` c# +var order = conn.QueryFirst(sql, args); +``` \ No newline at end of file diff --git a/docs/rules/DAP028.md b/docs/rules/DAP028.md new file mode 100644 index 00000000..95ae8cee --- /dev/null +++ b/docs/rules/DAP028.md @@ -0,0 +1,19 @@ +# DAP028 + +The `Query` APIs allow optional buffering, so it returns the value as `IEnumerable`. However, the default is "buffered", which means that +most of the time, the result *actually is* a `List`. If you call `.ToList()` on that, you create an **additional** `List` with the same +contents, which is unnecessary. + +To avoid this, Dapper provides an `AsList()` method, which gives you the *existing* `List` if it is one, otherwise it creates one. + +Bad: + +``` c# +var orders = conn.Query(sql, args).ToList(); +``` + +Good: + +``` c# +var orders = conn.Query(sql, args).AsList(); +``` \ No newline at end of file diff --git a/docs/rules/DAP035.md b/docs/rules/DAP035.md new file mode 100644 index 00000000..0999bad3 --- /dev/null +++ b/docs/rules/DAP035.md @@ -0,0 +1,29 @@ +# DAP035 + +It looks like you have multiple constructors on a type marked `[ExplicitConstructor]`; that's just confusing! +Pick one, and remove the attribute from the others. This should *probably* be the one with the most parameters. + +Bad: + +``` csharp +class MyType +{ + [ExplicitConstructor] + public MyType(int id, string name) { } + + [ExplicitConstructor] + public MyType(DateTime when, int id, string name) { } +} +``` + +Good: + +``` csharp +class MyType +{ + public MyType(int id, string name) { } + + [ExplicitConstructor] + public MyType(DateTime when, int id, string name) { } +} +``` \ No newline at end of file diff --git a/docs/rules/DAP036.md b/docs/rules/DAP036.md new file mode 100644 index 00000000..94cb46d9 --- /dev/null +++ b/docs/rules/DAP036.md @@ -0,0 +1,33 @@ +# DAP035 + +Your type has multiple constructors. Vanilla Dapper would be +happy to pick one based on the exact columns, but Dapper.AOT wants +a single constructor to use with all data. You might consider +marking your preferred constructor with `[ExplicitConstructor]`. +This should *probably* be the one with the most parameters. + +Bad: + +``` csharp +class MyType +{ + public MyType(int id, string name) { } + + public MyType(DateTime when, int id, string name) { } +} +``` + +Good: + +``` csharp +class MyType +{ + public MyType(int id, string name) { } + + [ExplicitConstructor] + public MyType(DateTime when, int id, string name) { } +} +``` + +Note that parameters not found in the result grid will be assigned the default value +for the parameter type (`null`, `0`, `false`, etc). \ No newline at end of file diff --git a/docs/rules/DAP038.md b/docs/rules/DAP038.md new file mode 100644 index 00000000..ba390689 --- /dev/null +++ b/docs/rules/DAP038.md @@ -0,0 +1,30 @@ +# DAP038 + +The `QueryFirstOrDefault(...)` and `QuerySingleOrDefault(...)` APIs return `default(T)` if zero rows are returned. + +For reference-type `T`, you can then use a `null` test to see whether a row came back, but a value-type `T` (that isn't `Nullable`, aka `T?`) +is *never* null, so it will be hard to test whether a row came back. + +This does not impact `QueryFirst(...)` or `QuerySingle(...)` because they *throw* if zero rows are returned - you do not need to test anything. + +As a fix, consider using `Nullable` for such scenarios: + +Bad: + +``` c# +var row = conn.QueryFirstOrDefault(sql); +if (row is not null) +{ + // ... +} +``` + +Good: + +``` c# +var row = conn.QueryFirstOrDefault(sql); +if (row is not null) +{ + // ... +} +``` \ No newline at end of file diff --git a/docs/rules/DAP200.md b/docs/rules/DAP200.md new file mode 100644 index 00000000..2d0d2ce0 --- /dev/null +++ b/docs/rules/DAP200.md @@ -0,0 +1,4 @@ +# DAP200 + +Something went very wrong when parsing your SQL. This is probably my fault, not yours. Even if your query was actually wrong, we should have +done better than this - I'd love to hear about it. You could [log an issue here](https://github.com/DapperLib/DapperAOT/issues). \ No newline at end of file diff --git a/docs/rules/DAP201.md b/docs/rules/DAP201.md new file mode 100644 index 00000000..5c41ca07 --- /dev/null +++ b/docs/rules/DAP201.md @@ -0,0 +1,32 @@ +# DAP201 + +The `GO` token commonly used in tools like SSMS isn't actually SQL. Rather, the tool (SSMS etc) +uses that token to split your query file into separate commands. + +`DbCommand` doesn't support that usage; it is meant to represent a *single* +operation.You must issue each part as a separate command. + +In the future, Dapper may be able to support that scenario via the newer +[`DbBatch`](https://learn.microsoft.com/dotnet/api/system.data.common.dbbatch) API, but few providers +support that at present. + +Bad: + +``` csharp +conn.Execute(""" + /* first things */ + GO + /* second things */ + """); +``` + +Good: + +``` csharp +conn.Execute(""" + /* first things */ + """); +conn.Execute(""" + /* second things */ + """); +``` diff --git a/docs/rules/DAP202.md b/docs/rules/DAP202.md new file mode 100644 index 00000000..556cbba9 --- /dev/null +++ b/docs/rules/DAP202.md @@ -0,0 +1,31 @@ +# DAP202 + +Like in most languages, SQL local variables must be uniquely named. If you're seeing this, it means you have something like: + +``` sql +declare @id int; +-- some code... +declare @id int; +``` + +Simply change the name of one of the variables, or remove the redundant copy. + +Note that the *scope* of SQL variables is "anywhere later in the code"; you cannot +re-declare a local in a branch. For example, this is not valid: + +``` sql +if -- some test +begin + declare @id int + -- more code +end +else +begin + declare @id int + -- more code +end +``` + +You *can*, however, assign to `@id` in the `else` branch *without* declaring it there, since +we are later in the code. Likewise, we can read `@id` below (and outside) the `if` scope. SQL has +weird scope/declaration behavior! \ No newline at end of file diff --git a/docs/rules/DAP203.md b/docs/rules/DAP203.md new file mode 100644 index 00000000..f7a16392 --- /dev/null +++ b/docs/rules/DAP203.md @@ -0,0 +1,28 @@ +# DAP203 + +`@@identity` is the [global identity function](https://learn.microsoft.com/sql/t-sql/functions/identity-transact-sql), and returns the last-inserted identity value; it is considered deprecated and actively harmful, as +it is very hard to predict what value it represents, especially when things like triggers are in play. It is also limited to a single value, where-as `insert` can be multi-row. + +You should use `SCOPE_IDENTITY()` instead of `@@identity`, or (preferably) use [the `OUTPUT` clause](https://learn.microsoft.com/sql/t-sql/queries/output-clause-transact-sql). + +Bad: + +``` sql +insert SomeTable (A) values (1) +select @@identity +``` + +Better: + +``` sql +insert SomeTable (A) values (1) +select SCOPE_IDENTITY() +``` + +Best: + +``` sql +insert SomeTable (A) +output inserted.Id +values (1) +``` diff --git a/docs/rules/DAP204.md b/docs/rules/DAP204.md new file mode 100644 index 00000000..92b2184c --- /dev/null +++ b/docs/rules/DAP204.md @@ -0,0 +1,26 @@ +# DAP204 + +`SCOPE_IDENTITY()` is the [scoped identity function](https://learn.microsoft.com/sql/t-sql/functions/scope-identity-transact-sql), and returns the last-inserted identity value; although not actively harmful like [@@identity](DAP203), +it still has limitations (in particular: single-row vs multi-row `insert`) and is indirect; it is worth considering use [the `OUTPUT` clause](https://learn.microsoft.com/sql/t-sql/queries/output-clause-transact-sql). + +Bad: + +``` sql +insert SomeTable (A) values (1) +select @@identity +``` + +Better: + +``` sql +insert SomeTable (A) values (1) +select SCOPE_IDENTITY() +``` + +Best: + +``` sql +insert SomeTable (A) +output inserted.Id +values (1) +``` diff --git a/docs/rules/DAP205.md b/docs/rules/DAP205.md new file mode 100644 index 00000000..b3284149 --- /dev/null +++ b/docs/rules/DAP205.md @@ -0,0 +1,24 @@ +# DAP205 + +Nothing compares to `null`; not even `null`. Nothing is ever *equal* to `null`, but also nothing is ever *not equal* to `null`; **all comparisons return false** (at least, unless you're running with exotic configuration options). + +To correctly compare to `null`, you should use [`is [not] null`](https://learn.microsoft.com/sql/t-sql/queries/is-null-transact-sql). + +Bad: + +``` sql +select A, B +from SomeTable +where X = null +and Y <> null +``` + +Good: + +``` sql +select A, B +from SomeTable +where X is null +and Y is not null +``` + diff --git a/docs/rules/DAP206.md b/docs/rules/DAP206.md new file mode 100644 index 00000000..6f732523 --- /dev/null +++ b/docs/rules/DAP206.md @@ -0,0 +1,6 @@ +# DAP206 + +General SQL parse error. The message will probably tell you what you're doing wrong (assuming that you're [using the right SQL variant](/sqlsyntax)), so: read that? + +If that fails, try executing your query (either simply by running your application, or in your DB-specific tool such as SQL Server Management Studio), +and see what happens. \ No newline at end of file diff --git a/docs/rules/DAP207.md b/docs/rules/DAP207.md new file mode 100644 index 00000000..8508e474 --- /dev/null +++ b/docs/rules/DAP207.md @@ -0,0 +1,18 @@ +# DAP207 + +There are two kinds of SQL variables; scalar variables that hold a single typed value, and table-variables that work like temporary-tables, but +with very specific scoping rules. You cannot treat a scalar variable like a table; you can't `insert` into, `update`, `delete` from or `select` from a scalar variable: + +Bad: + +``` sql +declare @id int = 0; +select * from @id; +``` + +Good: + +``` sql +declare @id int = 0; +select @id; +``` \ No newline at end of file diff --git a/docs/rules/DAP208.md b/docs/rules/DAP208.md new file mode 100644 index 00000000..11437a0e --- /dev/null +++ b/docs/rules/DAP208.md @@ -0,0 +1,21 @@ +# DAP208 + +There are two kinds of SQL variables; scalar variables that hold a single typed value, and table-variables that work like temporary-tables, but +with very specific scoping rules. You cannot treat a table-variable like a scalar; you can't use `set`/`select` assignment, `select` *the value* (rather than +columns `from` the value), etc. + +Bad: + +``` sql +declare @t table (Id int not null); +insert @t (Id) values (42); +select @t; +``` + +Good: + +``` sql +declare @t table (Id int not null); +insert @t (Id) values (42); +select Id from @t; +``` \ No newline at end of file diff --git a/docs/rules/DAP209.md b/docs/rules/DAP209.md new file mode 100644 index 00000000..e37d4aa1 --- /dev/null +++ b/docs/rules/DAP209.md @@ -0,0 +1,22 @@ +# DAP209 + +If you *definitely* haven't populated a table-variable, then: doing something with the values (that aren't there) is probably not what you intended. + +Populate the table! + +(the table-variable will implicitly be empty, but... you probably meant it to be something else) + +Bad: + +``` sql +declare @t table (Value int not null); +select Value from @t; +``` + +Good: + +``` sql +declare @t table (Value int not null); +insert @t (Value) values (1); +select Value from @t; +``` \ No newline at end of file diff --git a/docs/rules/DAP210.md b/docs/rules/DAP210.md new file mode 100644 index 00000000..a0fc0a00 --- /dev/null +++ b/docs/rules/DAP210.md @@ -0,0 +1,22 @@ +# DAP210 + +If you *definitely* haven't assigned a value to a scalar variable, then: doing something with the value (that isn't there) is probably not what you intended. + +Assign a value to the variable! + +(the value will implicitly be `null`, but... you probably meant it to be something else; if not: *just use the `null` literal*) + +Bad: + +``` sql +declare @i int; +select @i; +``` + +Good: + +``` sql +declare @i int; +set @i = 42; +select @i; +``` \ No newline at end of file diff --git a/docs/rules/DAP211.md b/docs/rules/DAP211.md new file mode 100644 index 00000000..8d798215 --- /dev/null +++ b/docs/rules/DAP211.md @@ -0,0 +1,17 @@ +# DAP211 + +Variables must be declared before being used; you probably moved some code around... + +Bad: + +``` sql +select @i; +declare @i int = 42; +``` + +Good: + +``` sql +declare @i int = 42; +select @i; +``` \ No newline at end of file diff --git a/docs/rules/DAP212.md b/docs/rules/DAP212.md new file mode 100644 index 00000000..ad03d01a --- /dev/null +++ b/docs/rules/DAP212.md @@ -0,0 +1,27 @@ +# DAP212 + +Executing dynamically generated SQL inside SQL can be a cause of all the same problems as unparameterized SQL, including SQL injection and performance (query plan cache misses). + +It looks like you're executing SQL that you've composed dynamically. This *could* be safe, but: you should ideally use [`sp_executesql`](https://learn.microsoft.com/sql/relational-databases/system-stored-procedures/sp-executesql-transact-sql), taking +care to correctly **fully** parameterize any inputs. + +This is a **complex topic**; please consult the SQL documentation carefully, as incorrect usage may present a security vulnerability. + +Bad: + +``` sql +exec ('select * from Customers where Name = ''' + @s + ''''); +``` + +Good: + +``` sql +exec sp_executesql + N'select * from Customers where Name = @name', -- parameterized SQL to execute + N'@name nvarchar(100)', -- parameter declarations (comma-delimited) + @s; -- arguments (comma-delimited) +``` + +Note that rather than *compose the value* of `@s` into SQL directly (with [predictable results](https://xkcd.com/327/)) , we're +leaving the query parameterized (`select * from Customers where Name = @name'`), passing the *value* `@s` as an argument. Note +also that the parameter names *inside* the executed query (`@name` in this case) do not need to match. diff --git a/docs/rules/DAP213.md b/docs/rules/DAP213.md new file mode 100644 index 00000000..b7a25f89 --- /dev/null +++ b/docs/rules/DAP213.md @@ -0,0 +1,19 @@ +# DAP213 + +You seem to have assigned a value to a scalar that is never used. This probably isn't intentional. + +Bad: + +``` sql +declare @i int = 42; -- never used +set @i = 15; +select @i; +``` + +Good: + +``` sql +declare @i int; +set @i = 15; +select @i; +``` \ No newline at end of file diff --git a/docs/rules/DAP214.md b/docs/rules/DAP214.md new file mode 100644 index 00000000..f3b99562 --- /dev/null +++ b/docs/rules/DAP214.md @@ -0,0 +1,19 @@ +# DAP214 + +A variable is not declared, and no corresponding parameter could be found. Did you mean to pass a parameter? Or declare the variable? + +Bad: + +``` sql +select @i; +``` + +Good: + +``` sql +declare @i int = 15; +select @i; +``` + +Note that this warning only happens if the tooling is *sure* it understands your parameter usage; otherwise it has to assume that the +unknown variable is in fact a parameter. \ No newline at end of file diff --git a/docs/rules/DAP215.md b/docs/rules/DAP215.md new file mode 100644 index 00000000..3695311e --- /dev/null +++ b/docs/rules/DAP215.md @@ -0,0 +1,3 @@ +# DAP215 + +As far as I know, table variables cannot be used as output parameters. They just can't. Sorry. \ No newline at end of file diff --git a/docs/rules/DAP216.md b/docs/rules/DAP216.md new file mode 100644 index 00000000..c27befb0 --- /dev/null +++ b/docs/rules/DAP216.md @@ -0,0 +1,21 @@ +# DAP216 + +It is good practice to explicitly specify columns when performing `insert` operations, as the column order isn't *necessarily* always +the same in all environments (for example, a cleanly created database vs a database that has had cumulative change scripts applied +can end up with slightly different column orders). + +Bad: + +``` sql +insert SomeTable +values (42) +``` + +Good: + +``` sql +insert SomeTable (SomeColumn) +values (42) +``` + +It also makes your intent clear to the reader, which is valuable by itself. \ No newline at end of file diff --git a/docs/rules/DAP217.md b/docs/rules/DAP217.md new file mode 100644 index 00000000..7f62717a --- /dev/null +++ b/docs/rules/DAP217.md @@ -0,0 +1,17 @@ +# DAP217 + +The number of columns *specified* in an `insert` must match the number of *values* specified. + +Bad: + +``` sql +insert SomeTable (Name, Value) +values ('abc', 42, 'def') +``` + +Good: + +``` sql +insert SomeTable (Name, Value, Description) +values ('abc', 42, 'def') +``` diff --git a/docs/rules/DAP218.md b/docs/rules/DAP218.md new file mode 100644 index 00000000..8b920979 --- /dev/null +++ b/docs/rules/DAP218.md @@ -0,0 +1,17 @@ +# DAP218 + +When performing a multi-row `insert` using `values`, the number of values in each row must match. + +Bad: + +``` sql +insert SomeTable (Name, Value) +values ('abc', 42), ('def', 63, 'ghi') +``` + +Good: + +``` sql +insert SomeTable (Name, Value, Description) +values ('abc', 42, ''), ('def', 63, 'ghi') +``` diff --git a/docs/rules/DAP219.md b/docs/rules/DAP219.md new file mode 100644 index 00000000..caa09c20 --- /dev/null +++ b/docs/rules/DAP219.md @@ -0,0 +1,17 @@ +# DAP219 + +Friends don't let friends `select *`. This is fine for inspecting tables in ad-hoc tools, but is bad practice in application code. +In addition to fetching unnecessary columns (which can impact performance, especially if the columns have large clob/blob data), +the *order* of columns can be unpredictable (see also [DAP216](DAP216)). + +Bad: + +``` sql +select * from SomeTable +``` + +Good: + +``` sql +select Id, Name, Description from SomeTable +``` diff --git a/docs/rules/DAP220.md b/docs/rules/DAP220.md new file mode 100644 index 00000000..7788aab7 --- /dev/null +++ b/docs/rules/DAP220.md @@ -0,0 +1,17 @@ +# DAP220 + +If Dapper is binding columns *by name*, then the columns must all *have names* - anonymous columns will never be bound, and are just wasting bandwidth. + +Bad: + +``` sql +select Id, Credit - Debit +from Accounts +``` + +Good: + +``` sql +select Id, Credit - Debit as [Balance] +from Accounts +``` diff --git a/docs/rules/DAP221.md b/docs/rules/DAP221.md new file mode 100644 index 00000000..a8ac9102 --- /dev/null +++ b/docs/rules/DAP221.md @@ -0,0 +1,19 @@ +# DAP221 + +If Dapper is binding columns *by name*, then the columns must all have *unique names* - duplicate columns be bound to the final value, with +any earlier values just wasting bandwidth. This *usually* happens as the result of `join` operations, in which case you should +probably alias one of the columns. + +Bad: + +``` sql +select a.Id, a.Name, b.Name +from -- not shown +``` + +Good: + +``` sql +select a.Id, a.Name, b.Name as [Category] +from -- not shown +``` diff --git a/docs/rules/DAP222.md b/docs/rules/DAP222.md new file mode 100644 index 00000000..635258f0 --- /dev/null +++ b/docs/rules/DAP222.md @@ -0,0 +1,12 @@ +# DAP222 + +The `select` construct can *either* assign a value *or* read values; it can't do both. Pick one. + +Example: + +``` sql +select @id = Id, Name +from ... +``` + +Decide whether we're trying to update the local variable, or read results. \ No newline at end of file diff --git a/docs/rules/DAP223.md b/docs/rules/DAP223.md new file mode 100644 index 00000000..6c48ee1f --- /dev/null +++ b/docs/rules/DAP223.md @@ -0,0 +1,18 @@ +# DAP223 + +You very rarely want to `delete` all the rows in a table (and when you do: you might consider `truncate`). Maybe be more specific? + +Bad: + +``` sql +delete +from Accounts +``` + +Good: + +``` sql +delete +from Accounts +where Id = @id +``` \ No newline at end of file diff --git a/docs/rules/DAP224.md b/docs/rules/DAP224.md new file mode 100644 index 00000000..d548c950 --- /dev/null +++ b/docs/rules/DAP224.md @@ -0,0 +1,18 @@ +# DAP224 + +You very rarely want to `delete` all the rows in a table. Maybe be more specific? + +Bad: + +``` sql +update Accounts +set Balance = 0 +``` + +Good: + +``` sql +update Accounts +set Balance = 0 +where Id = @id +``` \ No newline at end of file diff --git a/docs/rules/DAP225.md b/docs/rules/DAP225.md new file mode 100644 index 00000000..465b9f40 --- /dev/null +++ b/docs/rules/DAP225.md @@ -0,0 +1,24 @@ +# DAP225 + +When you have a complex operation involving multiple tables, it can be hard to track which table is which - and where each column is coming +from. Using table and column aliases makes this much clearer. + +Add aliases to all tables / expressions, and *use* those aliases for all column access. + +Bad: + +``` sql +select Id, Name, Address +from Users +inner join Addresses + on UserId = Id +``` + +Good: + +``` sql +select u.Id, u.Name, a.Address +from Users u +inner join Addresses a + on a.UserId = u.Id +``` \ No newline at end of file diff --git a/docs/rules/DAP226.md b/docs/rules/DAP226.md new file mode 100644 index 00000000..dbbcd1a1 --- /dev/null +++ b/docs/rules/DAP226.md @@ -0,0 +1,24 @@ +# DAP226 + +When you have a complex operation involving multiple tables, it can be hard to track which table is which - and where each column is coming +from. Using table and column aliases makes this much clearer. + +Add aliases to all tables / expressions, and *use* those aliases for all column access. + +Bad: + +``` sql +select Id, Name, Address +from Users +inner join Addresses + on UserId = Id +``` + +Good: + +``` sql +select u.Id, u.Name, a.Address +from Users u +inner join Addresses a + on a.UserId = u.Id +``` \ No newline at end of file diff --git a/docs/rules/DAP227.md b/docs/rules/DAP227.md new file mode 100644 index 00000000..72099821 --- /dev/null +++ b/docs/rules/DAP227.md @@ -0,0 +1,19 @@ +# DAP227 + +When using a literal `top` value (without the `percent` modifier), that value must be an integer. + +Bad: + +``` sql +select top 23.5 Id, Name, Address +from Users +``` + +Good: + +``` sql +select u.Id, u.Name, a.Address +from Users u +inner join Addresses a + on a.UserId = u.Id +``` \ No newline at end of file diff --git a/docs/rules/DAP228.md b/docs/rules/DAP228.md new file mode 100644 index 00000000..fa77fdc3 --- /dev/null +++ b/docs/rules/DAP228.md @@ -0,0 +1,17 @@ +# DAP228 + +When using a literal `top` value, that value should be positive. + +Bad: + +``` sql +select top 0 Id, Name +from Users +``` + +Good: + +``` sql +select top 10 Id, Name +from Users +``` \ No newline at end of file diff --git a/docs/rules/DAP229.md b/docs/rules/DAP229.md new file mode 100644 index 00000000..802ad7cc --- /dev/null +++ b/docs/rules/DAP229.md @@ -0,0 +1,18 @@ +# DAP229 + +When performing a `First`-style query and `top` is specified, you should use `top 1`; additional rows will be silently ignored, so they're +just wasting bandwidth. + +Bad: + +``` sql +select top 10 Id, Name +from Users +``` + +Good: + +``` sql +select top 1 Id, Name +from Users +``` \ No newline at end of file diff --git a/docs/rules/DAP230.md b/docs/rules/DAP230.md new file mode 100644 index 00000000..9d6d03b3 --- /dev/null +++ b/docs/rules/DAP230.md @@ -0,0 +1,23 @@ +# DAP230 + +When performing a `Single`-style query and `top` is specified, you should use `top 2`; additional rows *above* that will be silently ignored, so they're +just wasting bandwidth. If we only select `top 1`, then we'll never perform the "multiple row" validation that defines `Single`, so: we're actually +only performing a `First`. + +Change the value of `top`, or switch to `First` instead of `Single`. + +Bad: + +``` sql +select top 1 Id, Name +from Users +where UserName = @cn +``` + +Good: + +``` sql +select top 2 Id, Name +from Users +where UserName = @cn +``` \ No newline at end of file diff --git a/docs/rules/DAP231.md b/docs/rules/DAP231.md new file mode 100644 index 00000000..a64f2671 --- /dev/null +++ b/docs/rules/DAP231.md @@ -0,0 +1,26 @@ +# DAP231 + +When reading a single row, you should probably have a `where` clause. Alternatively, a `top` combined with `order by` would be acceptable. + +Bad: + +``` sql +select Id, Name +from Users +``` + +Good: + +``` sql +select Id, Name +from Users +where UserName = @cn +``` + +Also fine: + +``` sql +select top 1 Id, Name +from Users +order by CreationDate desc +``` \ No newline at end of file diff --git a/docs/rules/DAP232.md b/docs/rules/DAP232.md new file mode 100644 index 00000000..31c943e5 --- /dev/null +++ b/docs/rules/DAP232.md @@ -0,0 +1,23 @@ +# DAP232 + +When using a literal `fetch` value, that value should be positive. + +Bad: + +``` sql +select Id, Name +from Users +order by Name +offset 0 rows +fetch next 0 row only +``` + +Good: + +``` sql +select Id, Name +from Users +order by Name +offset 0 rows +fetch next 1 row only +``` diff --git a/docs/rules/DAP233.md b/docs/rules/DAP233.md new file mode 100644 index 00000000..af3ecb7e --- /dev/null +++ b/docs/rules/DAP233.md @@ -0,0 +1,23 @@ +# DAP233 + +When using a literal `offset` value, that value should be non-negative. + +Bad: + +``` sql +select Id, Name +from Users +order by Name +offset -1 rows +fetch next 1 row only +``` + +Good: + +``` sql +select Id, Name +from Users +order by Name +offset 0 rows +fetch next 1 row only +``` \ No newline at end of file diff --git a/docs/rules/DAP234.md b/docs/rules/DAP234.md new file mode 100644 index 00000000..fdc94353 --- /dev/null +++ b/docs/rules/DAP234.md @@ -0,0 +1,27 @@ +# DAP234 + +You seem to have a complex expression that be statically evaluated to a constant; maybe don't do that? + +Bad: + +``` sql +select Id, Number +from Accounts +where Balance = 23 + 5 +``` + +Good: + +``` sql +select Id, Number +from Accounts +where Balance = 28 +``` + +If you're trying to show where the number is coming from, maybe a comment instead? + +``` sql +select Id, Number +from Accounts +where Balance = 28 -- 23 + 5 +``` \ No newline at end of file diff --git a/docs/rules/DAP235.md b/docs/rules/DAP235.md new file mode 100644 index 00000000..8534b21e --- /dev/null +++ b/docs/rules/DAP235.md @@ -0,0 +1,22 @@ +# DAP235 + +In TSQL, `TOP` cannot be mixed with `OFFSET`; use a `FETCH` clause instead! + +Bad: + +``` sql +select top 10 Id, Name +from Users +order by Name +offset 0 rows +``` + +Good: + +``` sql +select Id, Name +from Users +order by Name +offset 0 rows +fetch next 10 rows only +``` diff --git a/docs/rules/DAP999.md b/docs/rules/DAP999.md new file mode 100644 index 00000000..4e62f164 --- /dev/null +++ b/docs/rules/DAP999.md @@ -0,0 +1,16 @@ +# DAP999 + +Well this is embarrassing; something completely unexpected happened. I don't know what, but if you +could [report it](https://github.com/DapperLib/DapperAOT/issues/), we can try to take a look. + +Please include any information you think might be relevant to ... whatever it says happened. + +Things that might matter: + +- what **exactly** did the message say? +- anything unusual in your code? +- what library versions of Dapper / Dapper.AOT / Dapper.Advisor are you using? +- what runtime (.NET 7, etc) are you using? +- what build SDK are you using? (`dotnet --version`) +- are you using an IDE (if so: which?) or the command-line? +- what operating system are you using? \ No newline at end of file diff --git a/docs/sqlsyntax.md b/docs/sqlsyntax.md index 6b698e8c..23e54975 100644 --- a/docs/sqlsyntax.md +++ b/docs/sqlsyntax.md @@ -4,9 +4,15 @@ There are many SQL syntax variants, all provider-specific. By default, Dapper st match which can lead to false-positives of some errors (for example, mistaking local variables for parameters). If the SQL variant is known, there are much more advanced tools available (at the time of writing, this is limited -to TSQL / SQL Server). There are multiple ways of letting the tools know more: +to TSQL / SQL Server). There are multiple ways of letting the tools know more (in order): 1. if the connection is statically recognized (`SqlConnection` etc rather than `DbConnection`), it will infer the SQL variant from that 2. if Dapper.AOT is installed and `[SqlSyntax(...)]` is in scope, it will use the variant specified -3. **not yet implemented** ~~if the `...` property is specified in the project file (in a ``), it will use - the variant specified; supported options: (not specified), `SqlServer`~~ \ No newline at end of file +3. if a `dapper.sqlsyntax = ...` entry is specified in an [analyzer configuration file](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files) (typically a [Global AnalyzerConfig](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#global-analyzerconfig)) + with a known values, it will be used +4. if a `...` property is specified in the project file (inside a ``) with a known value, it will be used +5. otherwise no SQL variant is applied + +For options 3 & 4, The "known values" are the names from the `SqlSyntax` enumeration, evaluated case-insensitively. + +At the current time, only the `SqlServer` option provides enhanced syntax analysis. \ No newline at end of file diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs new file mode 100644 index 00000000..ca10459b --- /dev/null +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs @@ -0,0 +1,74 @@ +using Microsoft.CodeAnalysis; + +namespace Dapper.CodeAnalysis; + +partial class DapperAnalyzer +{ + internal sealed class Diagnostics : DiagnosticsBase + { + public static readonly DiagnosticDescriptor + // general usage + UnknownError = LibraryWarning("DAP999", "Unknown analyzer error", "This isn't you; this is me; please log it! '{0}', '{1}'", true), + UnsupportedMethod = LibraryInfo("DAP001", "Unsupported method", "The Dapper method '{0}' is not currently supported by Dapper.AOT", true), + DapperAotNotEnabled = LibraryInfo("DAP005", "Dapper.AOT not enabled", "{0} candidate Dapper methods detected, but none have Dapper.AOT enabled", true), + DapperLegacyTupleParameter = LibraryWarning("DAP006", "Dapper tuple-type parameter", "Dapper (original) does not work well with tuple-type parameters as name information is inaccessible", true), + UnexpectedCommandType = LibraryInfo("DAP007", "Unexpected command type", "The command type specified is not understood", true), + UnexpectedArgument = LibraryInfo("DAP009", "Unexpected parameter", "The parameter '{0}' is not understood", true), + DapperLegacyBindNameTupleResults = LibraryWarning("DAP011", "Named-tuple results", "Dapper (original) does not support tuple results with bind-by-name semantics", true), + DapperAotAddBindTupleByName = LibraryWarning("DAP012", "Add BindTupleByName", "Because of differences in how Dapper and Dapper.AOT can process tuple-types, please add '[BindTupleByName({true|false})]' to clarify your intent", true), + DapperAotTupleResults = LibraryInfo("DAP013", "Tuple-type results", "Tuple-type results are not currently supported", true), + DapperAotTupleParameter = LibraryInfo("DAP014", "Tuple-type parameter", "Tuple-type parameters are not currently supported", true), + UntypedParameter = LibraryInfo("DAP015", "Untyped parameter", "The parameter type could not be resolved", true), + GenericTypeParameter = LibraryInfo("DAP016", "Generic type parameter", "Generic type parameters ({0}) are not currently supported", true), + NonPublicType = LibraryInfo("DAP017", "Non-accessible type", "Type '{0}' is not accessible; {1} types are not currently supported", true), + DuplicateParameter = LibraryWarning("DAP021", "Duplicate parameter", "Members '{0}' and '{1}' both have the database name '{2}'; '{0}' will be ignored"), + DuplicateReturn = LibraryWarning("DAP022", "Duplicate return parameter", "Members '{0}' and '{1}' are both designated as return values; '{0}' will be ignored"), + DuplicateRowCount = LibraryWarning("DAP023", "Duplicate row-count member", "Members '{0}' and '{1}' are both marked [RowCount]"), + RowCountDbValue = LibraryWarning("DAP024", "Member is both row-count and mapped value", "Member '{0}' is marked both [RowCount] and [DbValue]; [DbValue] will be ignored"), + ExecuteCommandWithQuery = SqlWarning("DAP025", "Execute command with query", "The command has a query that will be ignored", true), + QueryCommandMissingQuery = SqlError("DAP026", "Query/scalar command lacks query", "The command lacks a query", true), + UseSingleRowQuery = PerformanceWarning("DAP027", "Use single-row query", "Use {0}() instead of Query(...).{1}()", true), + UseQueryAsList = PerformanceWarning("DAP028", "Use AsList instead of ToList", "Use Query(...).AsList() instead of Query(...).ToList()", true), + ConstructorMultipleExplicit = LibraryError("DAP035", "Multiple explicit constructors", "Only one constructor should be marked [ExplicitConstructor] for type '{0}'", true), + ConstructorAmbiguous = LibraryError("DAP036", "Ambiguous constructors", "Type '{0}' has more than 1 constructor; mark one constructor with [ExplicitConstructor] or reduce constructors", true), + ValueTypeSingleFirstOrDefaultUsage = LibraryWarning("DAP038", "Value-type single row 'OrDefault' usage", "Type '{0}' is a value-type; it will not be trivial to identify missing rows from {1}", true), + + // SQL parse specific + GeneralSqlError = SqlWarning("DAP200", "SQL error", "SQL error: {0}"), + MultipleBatches = SqlError("DAP201", "Multiple batches", "Multiple batches are not permitted"), + DuplicateVariableDeclaration = SqlError("DAP202", "Duplicate variable declaration", "The variable {0} is declared multiple times"), + GlobalIdentity = SqlError("DAP203", "Do not use @@identity", "@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid"), + SelectScopeIdentity = SqlInfo("DAP204", "Prefer OUTPUT over SELECT", "Consider using OUTPUT INSERTED.yourid in the INSERT instead of SELECT SCOPE_IDENTITY()"), + NullLiteralComparison = SqlWarning("DAP205", "Null comparison", "Literal null used in comparison; 'is null' or 'is not null' should be preferred"), + ParseError = SqlError("DAP206", "SQL parse error", "Error {0}: {1}"), + ScalarVariableUsedAsTable = SqlError("DAP207", "Scalar used like table", "Scalar variable {0} is used like a table"), + TableVariableUsedAsScalar = SqlError("DAP208", "Table used like scalar", "Table-variable {0} is used like a scalar"), + TableVariableAccessedBeforePopulate = SqlError("DAP209", "Table used before populate", "Table-variable {0} is accessed before it populated"), + VariableAccessedBeforeAssignment = SqlError("DAP210", "Variable used before assigned", "Variable {0} is accessed before it is assigned a value"), + VariableAccessedBeforeDeclaration = SqlError("DAP211", "Variable used before declared", "Variable {0} is accessed before it is declared"), + ExecComposedSql = SqlWarning("DAP212", "EXEC with composed SQL", "EXEC with composed SQL may be susceptible to SQL injection; consider EXEC sp_executesql, taking care to fully parameterize the composed query"), + VariableValueNotConsumed = SqlWarning("DAP213", "Variable value not consumed", "Variable {0} has a value that is not consumed"), + VariableNotDeclared = SqlError("DAP214", "Variable not declared", "Variable {0} is not declared and no corresponding parameter exists"), + TableVariableOutputParameter = SqlWarning("DAP215", "Table variable used as output parameter", "Table variable {0} cannot be used as an output parameter"), + InsertColumnsNotSpecified = SqlWarning("DAP216", "INSERT without target columns", "INSERT should explicitly specify target columns"), + InsertColumnsMismatch = SqlError("DAP217", "INSERT with mismatched columns", "The INSERT values do not match the target columns"), + InsertColumnsUnbalanced = SqlError("DAP218", "INSERT with unbalanced rows", "The INSERT rows have different widths"), + SelectStar = SqlWarning("DAP219", "SELECT with wildcard columns", "SELECT columns should be specified explicitly"), + SelectEmptyColumnName = SqlWarning("DAP220", "SELECT with missing column name", "SELECT column name is missing: {0}"), + SelectDuplicateColumnName = SqlWarning("DAP221", "SELECT with duplicate column name", "SELECT column name is duplicated: '{0}'"), + SelectAssignAndRead = SqlWarning("DAP222", "SELECT with assignment and reads", "SELECT statement assigns variable and performs reads"), + DeleteWithoutWhere = SqlWarning("DAP223", "DELETE without WHERE", "DELETE statement lacks WHERE clause"), + UpdateWithoutWhere = SqlWarning("DAP224", "UPDATE without WHERE", "UPDATE statement lacks WHERE clause"), + FromMultiTableMissingAlias = SqlWarning("DAP225", "Multi-element FROM missing alias", "FROM expressions with multiple elements should use aliases"), + FromMultiTableUnqualifiedColumn = SqlWarning("DAP226", "Multi-element FROM with unqualified column", "FROM expressions with multiple elements should qualify all columns; it is unclear where '{0}' is located"), + NonIntegerTop = SqlError("DAP227", "Non-integer TOP", "TOP literals should be integers"), + NonPositiveTop = SqlError("DAP228", "Non-positive TOP", "TOP literals should be positive"), + SelectFirstTopError = SqlWarning("DAP229", "SELECT for First* with invalid TOP", "SELECT for First* should use TOP 1"), + SelectSingleTopError = SqlWarning("DAP230", "SELECT for Single* with invalid TOP", "SELECT for Single* should use TOP 2; if you do not need to test over-read, use First*"), + SelectSingleRowWithoutWhere = SqlWarning("DAP231", "SELECT for single row without WHERE", "SELECT for single row without WHERE or (TOP and ORDER BY)"), + NonPositiveFetch = SqlError("DAP232", "Non-positive FETCH", "FETCH literals should be positive"), + NegativeOffset = SqlError("DAP233", "Negative OFFSET", "OFFSET literals should be non-negative"), + SimplifyExpression = SqlInfo("DAP234", "Expression can be simplified", "Expression evaluates to a constant and can be replaced with '{0}'"), + TopWithOffset = SqlError("DAP235", "TOP with OFFSET clause", "TOP cannot be used in a query with OFFSET; use FETCH instead"); + } +} diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs new file mode 100644 index 00000000..78397593 --- /dev/null +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs @@ -0,0 +1,878 @@ +using Dapper.Internal; +using Dapper.Internal.Roslyn; +using Dapper.SqlAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Data; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using static Dapper.Internal.Inspection; + +namespace Dapper.CodeAnalysis; + +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public sealed partial class DapperAnalyzer : DiagnosticAnalyzer +{ + public sealed override ImmutableArray SupportedDiagnostics => DiagnosticsBase.All(); + + public override void Initialize(AnalysisContext context) + { +#if !DEBUG + context.EnableConcurrentExecution(); +#endif + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(OnCompilationStart); + } + + 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); + + // final actions + context.RegisterCompilationEndAction(state.OnCompilationEndAction); + } + + + + private sealed class AnalyzerState + { + private readonly List _missedOpportunities = new(); + private int _dapperHits; // this isn't a true count; it'll usually be 0 or 1, no matter the number of calls, because we stop tracking + private void OnDapperAotHit() + { + if (Thread.VolatileRead(ref _dapperHits) == 0 && IsDapperAotAvailable) // fast short-circuit if we know we're all good + { + lock (_missedOpportunities) + { + if (++_dapperHits == 1) // no point using Interlocked here; we need the lock anyway, to reset the list + { + // we only report about enabling Dapper.AOT if it isn't done anywhere + // (i.e. probably not known about), so: if they've *used* Dapper: + // we can stop tracking our failures + _missedOpportunities.Clear(); + } + } + } + } + private void OnDapperAotMiss(OperationAnalysisContext ctx, Location location) + { + if (Thread.VolatileRead(ref _dapperHits) == 0 // fast short-circuit if we know we're all good + && IsDapperAotAvailable) // don't warn if not available! + { + lock (_missedOpportunities) + { + if (_dapperHits == 0) + { + // see comment in OnDapperAotHit + _missedOpportunities.Add(location); + } + } + } + } + + internal void OnCompilationEndAction(CompilationAnalysisContext ctx) + { + lock (_missedOpportunities) + { + var count = _missedOpportunities.Count; + if (count != 0) + { + var location = _missedOpportunities[0]; + Location[]? additionalLocations = null; + if (count > 1) + { + additionalLocations = new Location[count - 1]; + _missedOpportunities.CopyTo(1, additionalLocations, 0, count - 1); + } + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.DapperAotNotEnabled, location, additionalLocations, count)); + } + } + } + + public void OnOperation(OperationAnalysisContext ctx) + { + try + { + // we'll look for: + // method calls with a parameter called "sql" or marked [Sql] + // property assignments to "CommandText" + switch (ctx.Operation.Kind) + { + case OperationKind.Invocation when ctx.Operation is IInvocationOperation invoke: + int index = 0; + foreach (var p in invoke.TargetMethod.Parameters) + { + if (p.Type.SpecialType == SpecialType.System_String && ( + p.Name == "sql" || Inspection.GetDapperAttribute(p, Types.SqlAttribute) is not null)) + { + if (Inspection.IsDapperMethod(invoke, out var dapperFlags)) + { + ValidateDapperMethod(ctx, invoke.Arguments[index], dapperFlags); + } + else + { + ValidateParameterUsage(ctx, invoke.Arguments[index]); + } + } + index++; + } + break; + case OperationKind.SimpleAssignment when ctx.Operation is ISimpleAssignmentOperation assignment + && assignment.Target is IPropertyReferenceOperation propRef: + if (propRef.Member is IPropertySymbol + { + Type.SpecialType: SpecialType.System_String, + IsStatic: false, + IsIndexer: false, + // check for DbConnection? + } prop) + { + if (prop.Name == "CommandText" && Inspection.IsCommand(prop.ContainingType)) + { + // write to CommandText + ValidatePropertyUsage(ctx, assignment.Value, true); + } + else if (Inspection.GetDapperAttribute(prop, Types.SqlAttribute) is not null) + { + // write SQL to a string property + ValidatePropertyUsage(ctx, assignment.Value, false); + } + } + break; + } + } + catch (Exception ex) + { + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnknownError, null, ex.Message, ex.StackTrace)); + } + } + + private void ValidateDapperMethod(in OperationAnalysisContext ctx, IOperation sqlSource, OperationFlags flags) + { + Action onDiagnostic = ctx.ReportDiagnostic; + if (ctx.Operation is not IInvocationOperation invoke) return; + + // check the args + var parseState = new ParseState(ctx); + bool aotEnabled = Inspection.IsEnabled(in parseState, invoke, Types.DapperAotAttribute, out var aotAttribExists); + if (!aotEnabled) flags |= OperationFlags.DoNotGenerate; + var location = SharedParseArgsAndFlags(parseState, invoke, ref flags, out var sql, out var paramType, onDiagnostic, out _); + + // report our AOT readiness + if (aotEnabled) + { + OnDapperAotHit(); // all good for AOT + if (flags.HasAny(OperationFlags.NotAotSupported)) + { + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnsupportedMethod, location, invoke.GetSignature())); + } + } + else if (!aotAttribExists && !flags.HasAny(OperationFlags.NotAotSupported)) + { + // we might have been able to do more, but Dapper.AOT wasn't enabled + OnDapperAotMiss(ctx, location); + } + + // check the types + var resultMap = MemberMap.Create(invoke.GetResultType(flags), false); + if (resultMap is not null) + { + // check for single-row value-type usage + if (flags.HasAny(OperationFlags.SingleRow) && !flags.HasAny(OperationFlags.AtLeastOne) + && resultMap.ElementType.IsValueType && !Inspection.CouldBeNullable(resultMap.ElementType)) + { + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.ValueTypeSingleFirstOrDefaultUsage, location, resultMap.ElementType.Name, invoke.TargetMethod.Name)); + } + + // check for constructors on the materialized type + DiagnosticDescriptor? ctorFault = Inspection.ChooseConstructor(resultMap.ElementType, out var ctor) switch + { + Inspection.ConstructorResult.FailMultipleExplicit => Diagnostics.ConstructorMultipleExplicit, + Inspection.ConstructorResult.FailMultipleImplicit when aotEnabled => Diagnostics.ConstructorAmbiguous, + _ => null, + }; + if (ctorFault is not null) + { + var loc = ctor?.Locations.FirstOrDefault() ?? resultMap.ElementType.Locations.FirstOrDefault(); + ctx.ReportDiagnostic(Diagnostic.Create(ctorFault, loc, resultMap.ElementType.GetDisplayString())); + } + } + + var map = MemberMap.Create(paramType, true); + var args = SharedGetParametersToInclude(map, flags, sql, onDiagnostic, out var parseFlags); + + + ValidateSql(ctx, sqlSource, GetModeFlags(flags), location); + + /* + * + + if (paramNames.IsEmpty && (parseFlags & SqlParseOutputFlags.Return) == 0) // return is a parameter, sort of + { + if (flags.HasAny(OperationFlags.HasParameters)) + { + Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.SqlParametersNotDetected, loc)); + } + return ""; + } + + // so, we definitely detect parameters (note: don't warn just for return) + if (!flags.HasAny(OperationFlags.HasParameters)) + { + if (!paramNames.IsEmpty) + { + Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.NoParametersSupplied, loc)); + } + return ""; + } + + // we can only consider this an error if we're confident in how well we parsed the input + // (unless we detected dynamic args, in which case: we don't know what we don't know) + if ((parseFlags & (SqlParseOutputFlags.Reliable | SqlParseOutputFlags.KnownParameters)) == (SqlParseOutputFlags.Reliable | SqlParseOutputFlags.KnownParameters)) + { + Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.SqlParameterNotBound, loc, sqlParamName, CodeWriter.GetTypeName(parameterType))); + } + */ + + ValidateSurroundingLinqUsage(ctx, flags); + + } + + static SqlParseInputFlags GetModeFlags(OperationFlags flags) + { + + var modeFlags = SqlParseInputFlags.None; + // if (caseSensitive) modeFlags |= ModeFlags.CaseSensitive; + if ((flags & OperationFlags.BindResultsByName) != 0) modeFlags |= SqlParseInputFlags.ValidateSelectNames; + if ((flags & OperationFlags.SingleRow) != 0) modeFlags |= SqlParseInputFlags.SingleRow; + if ((flags & OperationFlags.AtMostOne) != 0) modeFlags |= SqlParseInputFlags.AtMostOne; + if ((flags & OperationFlags.Scalar) != 0) + { + modeFlags |= SqlParseInputFlags.ExpectQuery | SqlParseInputFlags.SingleRow | SqlParseInputFlags.SingleQuery | SqlParseInputFlags.SingleColumn; + } + else if ((flags & OperationFlags.Query) != 0) + { + modeFlags |= SqlParseInputFlags.ExpectQuery; + if ((flags & OperationFlags.QueryMultiple) == 0) modeFlags |= SqlParseInputFlags.SingleQuery; + } + else if ((flags & (OperationFlags.Execute)) != 0) modeFlags |= SqlParseInputFlags.ExpectNoQuery; + + return modeFlags; + } + + private void ValidateSurroundingLinqUsage(in OperationAnalysisContext ctx, OperationFlags flags) + { + if (flags.HasAny(OperationFlags.Query) && !flags.HasAny(OperationFlags.SingleRow) + && ctx.Operation.Parent is IArgumentOperation arg + && arg.Parent is IInvocationOperation parent && parent.TargetMethod is + { + IsExtensionMethod: true, + Parameters.Length: 1, Arity: 1, ContainingType: + { + Name: nameof(Enumerable), + ContainingType: null, + ContainingNamespace: + { + Name: "Linq", + ContainingNamespace: + { + Name: "System", + ContainingNamespace.IsGlobalNamespace: true + } + } + } + } target) + { + bool useSingleRowQuery = parent.TargetMethod.Name switch + { + nameof(Enumerable.First) => true, + nameof(Enumerable.Single) => true, + nameof(Enumerable.FirstOrDefault) => true, + nameof(Enumerable.SingleOrDefault) => true, + _ => false, + }; + if (useSingleRowQuery) + { + var name = parent.TargetMethod.Name; + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.UseSingleRowQuery, parent.GetMemberLocation(), + "Query" + name, name)); + } + else if (parent.TargetMethod.Name == nameof(Enumerable.ToList)) + { + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.UseQueryAsList, parent.GetMemberLocation())); + } + } + } + + private void ValidateParameterUsage(in OperationAnalysisContext ctx, IOperation sqlSource) + { + // TODO: check other parameters for special markers like command type? + var flags = SqlParseInputFlags.None; + ValidateSql(ctx, sqlSource, flags); + } + + private void ValidatePropertyUsage(in OperationAnalysisContext ctx, IOperation sqlSource, bool isCommand) + { + var flags = SqlParseInputFlags.None; + if (isCommand) + { + // TODO: check other parameters for special markers like command type? + // check the method being invoked - scalar, reader, etc + } + ValidateSql(ctx, sqlSource, flags); + } + + private static readonly Regex HasWhitespace = new Regex(@"\s", RegexOptions.Compiled | RegexOptions.Multiline); + + + private readonly SqlSyntax? DefaultSqlSyntax; + private readonly SqlParseInputFlags? DebugSqlFlags; + private readonly Compilation _compilation; + private bool? _isDapperAotAvailable; + + internal bool IsDapperAotAvailable => _isDapperAotAvailable ?? LazyIsDapperAotAvailable(); + private bool LazyIsDapperAotAvailable() + { + _isDapperAotAvailable = _compilation.Language == LanguageNames.CSharp && _compilation.GetTypeByMetadataName("Dapper." + Types.DapperAotAttribute) is not null; + return _isDapperAotAvailable.Value; + } +#pragma warning disable RS1012 // Start action has no registered actions + public AnalyzerState(CompilationStartAnalysisContext context) +#pragma warning restore RS1012 // Start action has no registered actions + { + _compilation = context.Compilation; + if (context.Options.TryGetSqlSyntax(out var syntax)) + { + DefaultSqlSyntax = syntax; + } + if (context.Options.TryGetDebugModeFlags(out var flags)) + { + DebugSqlFlags = flags; + } + } + + private void ValidateSql(in OperationAnalysisContext ctx, IOperation sqlSource, SqlParseInputFlags flags, Location? location = null) + { + var parseState = new ParseState(ctx); + + // should we consider this as a syntax we can handle? + var syntax = Inspection.IdentifySqlSyntax(parseState, ctx.Operation, out var caseSensitive) ?? DefaultSqlSyntax ?? SqlSyntax.General; + switch (syntax) + { + case SqlSyntax.SqlServer: + // other known types here + break; + default: + return; + } + if (syntax != SqlSyntax.SqlServer) return; + + if (caseSensitive) flags |= SqlParseInputFlags.CaseSensitive; + + // can we get the SQL itself? + if (!Inspection.TryGetConstantValueWithSyntax(sqlSource, out string? sql, out var sqlSyntax)) return; + if (string.IsNullOrWhiteSpace(sql) || !HasWhitespace.IsMatch(sql)) return; // need non-trivial content to validate + + location ??= ctx.Operation.Syntax.GetLocation(); + + if (DebugSqlFlags is not null) + { + var debugModeFlags = DebugSqlFlags.Value; + if ((debugModeFlags & SqlParseInputFlags.DebugMode) != 0) + { + // debug mode flags **replace** all other flags + flags = debugModeFlags; + } + else + { + // otherwise we *extend* the flags + flags |= debugModeFlags; + } + } + + try + { + SqlParseOutputFlags parseFlags; + switch (syntax) + { + case SqlSyntax.SqlServer: + + var proc = new OperationAnalysisContextTSqlProcessor(ctx, null, flags, location, sqlSyntax); + proc.Execute(sql!, members: default); + parseFlags = proc.Flags; + // paramMembers); + //parseFlags = proc.Flags; + //paramNames = (from var in proc.Variables + // where var.IsParameter + // select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name + // ).ToImmutableHashSet(); + //diagnostics = proc.DiagnosticsObject; + break; + default: + parseFlags = SqlParseOutputFlags.None; + break; + } + + // if we're sure we understood the SQL (reliable and no syntax error), we can check our query expectations + // (note some other similar rules are enforced *inside* the parser - single-row, for example) + if ((parseFlags & (SqlParseOutputFlags.SyntaxError | SqlParseOutputFlags.Reliable)) == SqlParseOutputFlags.Reliable) + { + switch (flags & (SqlParseInputFlags.ExpectQuery | SqlParseInputFlags.ExpectNoQuery)) + { + case SqlParseInputFlags.ExpectQuery when ((parseFlags & (SqlParseOutputFlags.Query | SqlParseOutputFlags.MaybeQuery)) == 0): + // definitely do not have a query + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.QueryCommandMissingQuery, location)); + break; + case SqlParseInputFlags.ExpectNoQuery when ((parseFlags & SqlParseOutputFlags.Query) != 0): + // definitely have a query + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.ExecuteCommandWithQuery, location)); + break; + } + } + } + catch (Exception ex) + { + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.GeneralSqlError, location, ex.Message)); + } + } + } + + // we want a common understanding of the setup between the analyzer and generator + internal static Location SharedParseArgsAndFlags(in ParseState ctx, IInvocationOperation op, ref OperationFlags flags, out string? sql, out ITypeSymbol? paramType, + Action? reportDiagnostic, out ITypeSymbol? resultType) + { + var callLocation = op.GetMemberLocation(); + Location? argLocation = null; + sql = null; + paramType = null; + bool? buffered = null; + // check the args + foreach (var arg in op.Arguments) + { + + switch (arg.Parameter?.Name) + { + case "sql": + if (Inspection.TryGetConstantValueWithSyntax(arg, out string? s, out _)) + { + sql = s; + } + break; + case "buffered": + if (Inspection.TryGetConstantValue(arg, out bool b)) + { + buffered = b; + } + break; + case "param": + if (arg.Value is not IDefaultValueOperation) + { + var expr = arg.Value; + if (expr is IConversionOperation conv && expr.Type?.SpecialType == SpecialType.System_Object) + { + expr = conv.Operand; + } + if (expr.ConstantValue.HasValue && expr.ConstantValue.Value is null) + { + // another way of saying null, especially in VB + } + else + { + paramType = expr?.Type; + flags |= OperationFlags.HasParameters; + } + } + argLocation = arg.Syntax.GetLocation(); + break; + case "cnn": + case "commandTimeout": + case "transaction": + // nothing to do + break; + case "commandType": + if (Inspection.TryGetConstantValue(arg, out int? ct)) + { + switch (ct) + { + case null when !string.IsNullOrWhiteSpace(sql): + // if no spaces: interpret as stored proc, else: text + flags |= sql!.Trim().IndexOf(' ') < 0 ? OperationFlags.StoredProcedure : OperationFlags.Text; + break; + case null: + break; // flexible + case 1: + flags |= OperationFlags.Text; + break; + case 4: + flags |= OperationFlags.StoredProcedure; + break; + case 512: + flags |= OperationFlags.TableDirect; + break; + default: // treat as flexible + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.UnexpectedCommandType, arg.Syntax.GetLocation())); + break; + } + } + break; + default: + if (!flags.HasAny(OperationFlags.NotAotSupported | OperationFlags.DoNotGenerate)) + { + flags |= OperationFlags.DoNotGenerate; + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.UnexpectedArgument, arg.Syntax.GetLocation(), arg.Parameter?.Name)); + } + break; + } + } + // additional flags + if (Inspection.IsEnabled(ctx, op, Types.CacheCommandAttribute, out _)) + { + flags |= OperationFlags.CacheCommand; + } + + if (!string.IsNullOrWhiteSpace(sql) && flags.HasAny(OperationFlags.Text) + && Inspection.IsEnabled(ctx, op, Types.IncludeLocationAttribute, out _)) + { + flags |= OperationFlags.IncludeLocation; + } + + if (flags.HasAny(OperationFlags.Query) && buffered.HasValue) + { + flags |= buffered.GetValueOrDefault() ? OperationFlags.Buffered : OperationFlags.Unbuffered; + } + resultType = op.GetResultType(flags); + if (flags.HasAny(OperationFlags.Query) && Inspection.IdentifyDbType(resultType, out _) is null) + { + flags |= OperationFlags.BindResultsByName; + } + + if (flags.HasAny(OperationFlags.Query) || flags.HasAll(OperationFlags.Execute | OperationFlags.Scalar)) + { + bool resultTuple = Inspection.InvolvesTupleType(resultType, out var withNames), bindByNameDefined = false; + // tuples are positional by default + if (resultTuple && !Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out bindByNameDefined)) + { + flags &= ~OperationFlags.BindResultsByName; + } + + if (flags.HasAny(OperationFlags.DoNotGenerate)) + { + // extra checks specific to Dapper vanilla + if (resultTuple && withNames && Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out _)) + { // Dapper vanilla supports bind-by-position for tuples; warn if bind-by-name is enabled + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.DapperLegacyBindNameTupleResults, callLocation)); + } + } + else + { + // extra checks specific to DapperAOT + if (Inspection.InvolvesGenericTypeParameter(resultType)) + { + flags |= OperationFlags.DoNotGenerate; + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.GenericTypeParameter, callLocation, resultType!.ToDisplayString())); + } + else if (resultTuple) + { + if (!bindByNameDefined) + { + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.DapperAotAddBindTupleByName, callLocation)); + } + + // but not implemented currently! + flags |= OperationFlags.DoNotGenerate; + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.DapperAotTupleResults, callLocation)); + } + else if (!Inspection.IsPublicOrAssemblyLocal(resultType, ctx, out var failing)) + { + flags |= OperationFlags.DoNotGenerate; + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.NonPublicType, callLocation, failing!.ToDisplayString(), Inspection.NameAccessibility(failing))); + } + } + } + + // additional parameter checks + if (flags.HasAny(OperationFlags.HasParameters)) + { + argLocation ??= callLocation; + bool paramTuple = Inspection.InvolvesTupleType(paramType, out _); + if (flags.HasAny(OperationFlags.DoNotGenerate)) + { + // extra checks specific to Dapper vanilla + if (paramTuple) + { + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.DapperLegacyTupleParameter, argLocation)); + } + } + else + { + // extra checks specific to DapperAOT + if (paramTuple) + { + if (Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out var defined)) + { + flags |= OperationFlags.BindTupleParameterByName; + } + if (!defined) + { + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.DapperAotAddBindTupleByName, argLocation)); + } + + // but not implemented currently! + flags |= OperationFlags.DoNotGenerate; + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.DapperAotTupleParameter, argLocation)); + } + else if (Inspection.InvolvesGenericTypeParameter(paramType)) + { + flags |= OperationFlags.DoNotGenerate; + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.GenericTypeParameter, argLocation, paramType!.ToDisplayString())); + } + else if (Inspection.IsMissingOrObjectOrDynamic(paramType) || Inspection.IsDynamicParameters(paramType)) + { + flags |= OperationFlags.DoNotGenerate; + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.UntypedParameter, argLocation)); + } + else if (!Inspection.IsPublicOrAssemblyLocal(paramType, ctx, out var failing)) + { + flags |= OperationFlags.DoNotGenerate; + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.NonPublicType, argLocation, failing!.ToDisplayString(), Inspection.NameAccessibility(failing))); + } + } + } + return callLocation; + } + + enum ParameterMode + { + None, All, Defer, Filter + } + + internal static AdditionalCommandState? SharedGetAdditionalCommandState(ISymbol target, MemberMap? map, Action? reportDiagnostic) + { + var attribs = target.GetAttributes(); + if (attribs.IsDefaultOrEmpty) return null; + + int cmdPropsCount = 0; + int estimatedRowCount = 0; + string? estimatedRowCountMember = null; + if (map is not null) + { + foreach (var member in map.Members) + { + if (member.IsEstimatedRowCount) + { + if (estimatedRowCountMember is not null) + { + reportDiagnostic?.Invoke(Diagnostic.Create(DapperInterceptorGenerator.Diagnostics.MemberRowCountHintDuplicated, member.GetLocation())); + } + estimatedRowCountMember = member.Member.Name; + } + } + } + var location = target.Locations.FirstOrDefault(); + foreach (var attrib in attribs) + { + if (Inspection.IsDapperAttribute(attrib)) + { + switch (attrib.AttributeClass!.Name) + { + case Types.EstimatedRowCountAttribute: + if (attrib.ConstructorArguments.Length == 1 && attrib.ConstructorArguments[0].Value is int i + && i > 0) + { + if (estimatedRowCountMember is null) + { + estimatedRowCount = i; + } + else + { + reportDiagnostic?.Invoke(Diagnostic.Create(DapperInterceptorGenerator.Diagnostics.MethodRowCountHintRedundant, location, estimatedRowCountMember)); + } + } + else + { + reportDiagnostic?.Invoke(Diagnostic.Create(DapperInterceptorGenerator.Diagnostics.MethodRowCountHintInvalid, location)); + } + break; + case Types.CommandPropertyAttribute: + cmdPropsCount++; + break; + } + } + } + + ImmutableArray cmdProps; + if (cmdPropsCount != 0) + { + var builder = ImmutableArray.CreateBuilder(cmdPropsCount); + foreach (var attrib in attribs) + { + if (Inspection.IsDapperAttribute(attrib) && attrib.AttributeClass!.Name == Types.CommandPropertyAttribute + && attrib.AttributeClass.Arity == 1 + && attrib.AttributeClass.TypeArguments[0] is INamedTypeSymbol cmdType + && attrib.ConstructorArguments.Length == 2 + && attrib.ConstructorArguments[0].Value is string name + && attrib.ConstructorArguments[1].Value is object value) + { + builder.Add(new(cmdType, name, value, location)); + } + } + cmdProps = builder.ToImmutable(); + } + else + { + cmdProps = ImmutableArray.Empty; + } + + + return cmdProps.IsDefaultOrEmpty && estimatedRowCount <= 0 && estimatedRowCountMember is null + ? null : new(estimatedRowCount, estimatedRowCountMember, cmdProps); + } + + internal static ImmutableArray? SharedGetParametersToInclude(MemberMap? map, OperationFlags flags, string? sql, Action? reportDiagnostic, out SqlParseOutputFlags parseFlags) + { + SortedDictionary? byDbName = null; + var filter = ImmutableHashSet.Empty; + ParameterMode mode; + if (flags.HasAny(OperationFlags.StoredProcedure | OperationFlags.TableDirect)) + { + parseFlags = flags.HasAny(OperationFlags.StoredProcedure) ? SqlParseOutputFlags.MaybeQuery : SqlParseOutputFlags.Query; + mode = ParameterMode.All; + } + else + { + parseFlags = SqlParseOutputFlags.MaybeQuery; + + // if command-type or command is not known statically: defer decision + if (!flags.HasAny(OperationFlags.Text) || string.IsNullOrWhiteSpace(sql)) + { + mode = ParameterMode.Defer; + } + else + { + mode = ParameterMode.Filter; + filter = SqlTools.GetUniqueParameters(sql); + } + } + if (!flags.HasAny(OperationFlags.HasParameters)) + { + mode = ParameterMode.None; + } + + if (!(map is null || map.IsUnknownParameters)) + { + // check the shape of the parameter type + string? rowCountMember = null, returnCodeMember = null; + foreach (var member in map.Members) + { + if (member.IsRowCount) + { + if (rowCountMember is not null) + { + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.DuplicateRowCount, member.GetLocation(), member.CodeName, rowCountMember)); + } + else + { + rowCountMember = member.CodeName; + } + if (member.HasDbValueAttribute) + { + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.RowCountDbValue, member.GetLocation(), member.CodeName)); + } + } + if (member.Kind != ElementMemberKind.None) + { + continue; // not treated as parameters for naming etc purposes + } + + var dbName = member.DbName; + bool isRetVal = member.Direction == ParameterDirection.ReturnValue && flags.HasAny(OperationFlags.StoredProcedure); + switch (mode) + { + case ParameterMode.None: + case ParameterMode.Defer: + continue; // don't include anything + case ParameterMode.Filter: + if (isRetVal) + { // always include retval on sproc + break; + } + if (!filter.Contains(dbName)) + { + // exclude other things that we can't see + continue; + } + break; + case ParameterMode.All: + break; // g + } + + byDbName ??= new(StringComparer.InvariantCultureIgnoreCase); + if (byDbName.TryGetValue(dbName, out var existing)) + { + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.DuplicateParameter, member.GetLocation(), member, existing, dbName)); + } + else + { + byDbName.Add(dbName, member); + } + + if (isRetVal) + { + if (returnCodeMember is null) + { + returnCodeMember = member.CodeName; + } + else + { + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.DuplicateReturn, member.GetLocation(), member.CodeName, returnCodeMember)); + } + } + } + } + + if (mode == ParameterMode.Defer) + { + return null; + } + if (byDbName is null) + { + // nothing found + return ImmutableArray.Empty; + } + if (byDbName.Count == 0) + { + // so common that it is worth special-casing + return ImmutableArray.Empty.Add(byDbName.Single().Value); + } + var builder = ImmutableArray.CreateBuilder(byDbName.Count); + // add everything we found, keeping "result" at the end; note already sorted + foreach (var pair in byDbName) + { + var member = pair.Value; + if (member.Direction != ParameterDirection.ReturnValue) + { + builder.Add(member); + } + } + foreach (var pair in byDbName) + { + var member = pair.Value; + if (member.Direction == ParameterDirection.ReturnValue) + { + builder.Add(member); + } + } + return builder.ToImmutable(); + + } +} \ No newline at end of file diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperCodeFixes.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperCodeFixes.cs index f4e48f00..f8a0a31e 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperCodeFixes.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperCodeFixes.cs @@ -15,7 +15,7 @@ public sealed class DapperCodeFixes : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds => ImmutableArray.CreateRange(new[] { - DapperInterceptorGenerator.Diagnostics.DapperAotNotEnabled.Id, + DapperAnalyzer.Diagnostics.DapperAotNotEnabled.Id, }); public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -35,7 +35,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) switch (fieldName) { - case nameof(DapperInterceptorGenerator.Diagnostics.DapperAotNotEnabled): + case nameof(DapperAnalyzer.Diagnostics.DapperAotNotEnabled): Register("Enable AOT", GlobalEnableAOT, nameof(GlobalEnableAOT)); break; } diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Diagnostics.cs index ad4fc38c..49414e5b 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Diagnostics.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Diagnostics.cs @@ -9,39 +9,17 @@ internal sealed class Diagnostics : DiagnosticsBase internal static readonly DiagnosticDescriptor InterceptorsGenerated = LibraryHidden("DAP000", "Interceptors generated", "Dapper.AOT handled {0} of {1} enabled call-sites using {2} interceptors, {3} commands and {4} readers"), - UnsupportedMethod = LibraryInfo("DAP001", "Unsupported method", "The Dapper method '{0}' is not currently supported by Dapper.AOT"), //UntypedResults = new("DAP002", "Untyped result types", // "Dapper.AOT does not currently support untyped/dynamic results", Category.Library, DiagnosticSeverity.Info, true), InterceptorsNotEnabled = LibraryWarning("DAP003", "Interceptors not enabled", "Interceptors need to be enabled (see help-link)", true), LanguageVersionTooLow = LibraryWarning("DAP004", "Language version too low", "Interceptors require at least C# version 11", true), - DapperAotNotEnabled = LibraryInfo("DAP005", "Dapper.AOT not enabled", - "Candidate Dapper methods were detected, but none have Dapper.AOT enabled; [DapperAot] can be added at the method, type, module or assembly level (for example '[module:DapperAot]')", true), - DapperLegacyTupleParameter = LibraryWarning("DAP006", "Dapper tuple-type parameter", "Dapper (original) does not work well with tuple-type parameters as name information is inaccessible"), - UnexpectedCommandType = LibraryInfo("DAP007", "Unexpected command type", "The command type specified is not understood"), - // space - UnexpectedArgument = LibraryInfo("DAP009", "Unexpected parameter", "The parameter '{0}' is not understood"), + // space - DapperLegacyBindNameTupleResults = LibraryWarning("DAP011", "Named-tuple results", "Dapper (original) does not support tuple results with bind-by-name semantics"), - DapperAotAddBindTupleByName = LibraryWarning("DAP012", "Add BindTupleByName", "Because of differences in how Dapper and Dapper.AOT can process tuple-types, please add '[BindTupleByName({true|false})]' to clarify your intent"), - DapperAotTupleResults = LibraryInfo("DAP013", "Tuple-type results", "Tuple-type results are not currently supported"), - DapperAotTupleParameter = LibraryInfo("DAP014", "Tuple-type parameter", "Tuple-type parameters are not currently supported"), - UntypedParameter = LibraryInfo("DAP015", "Untyped parameter", "The parameter type could not be resolved"), - GenericTypeParameter = LibraryInfo("DAP016", "Generic type parameter", "Generic type parameters ({0}) are not currently supported"), - NonPublicType = LibraryInfo("DAP017", "Non-accessible type", "Type '{0}' is not accessible; {1} types are not currently supported"), SqlParametersNotDetected = SqlWarning("DAP018", "SQL parameters not detected", "Parameters are being supplied, but no parameters were detected in the command"), - NoParametersSupplied = SqlWarning("DAP019", "No parameters supplied", "SQL parameters were detected, but no parameters are being supplied", true), - SqlParameterNotBound = SqlWarning("DAP020", "SQL parameter not bound", "No member could be found for the SQL parameter '{0}' from type '{1}'"), - DuplicateParameter = LibraryWarning("DAP021", "Duplicate parameter", "Members '{0}' and '{1}' both have the database name '{2}'; '{0}' will be ignored"), - DuplicateReturn = LibraryWarning("DAP022", "Duplicate return parameter", "Members '{0}' and '{1}' are both designated as return values; '{0}' will be ignored"), - DuplicateRowCount = LibraryWarning("DAP023", "Duplicate row-count member", - "Members '{0}' and '{1}' are both marked [RowCount]"), - RowCountDbValue = LibraryWarning("DAP024", "Member is both row-count and mapped value", - "Member '{0}' is marked both [RowCount] and [DbValue]; [DbValue] will be ignored"), - ExecuteCommandWithQuery = SqlWarning("DAP025", "Execute command with query", "The command has a query that will be ignored"), - QueryCommandMissingQuery = SqlError("DAP026", "Query/scalar command lacks query", "The command lacks a query"), - UseSingleRowQuery = PerformanceWarning("DAP027", "Use single-row query", "Use {0}() instead of Query(...).{1}()"), - UseQueryAsList = PerformanceWarning("DAP028", "Use AsList instead of ToList", "Use Query(...).AsList() instead of Query(...).ToList()"), + NoParametersSupplied = SqlWarning("DAP019", "No parameters supplied", "SQL parameters were detected, but no parameters are being supplied", false), + SqlParameterNotBound = SqlWarning("DAP020", "SQL parameter not bound", "No member could be found for the SQL parameter '{0}' from type '{1}'", false), + MethodRowCountHintRedundant = LibraryInfo("DAP029", "Method-level row-count hint redundant", "The [EstimatedRowCount] will be ignored due to parameter member '{0}'"), MethodRowCountHintInvalid = LibraryError("DAP030", "Method-level row-count hint invalid", "The [EstimatedRowCount] parameters are invalid; a positive integer must be supplied"), @@ -53,47 +31,7 @@ internal static readonly DiagnosticDescriptor "Command property {0}.{1} was not found or was not valid; attribute will be ignored"), CommandPropertyReserved = LibraryWarning("DAP034", "Command property reserved", "Command property {1} is reserved for internal usage; attribute will be ignored"), - TooManyDapperAotEnabledConstructors = LibraryError("DAP035", "Too many Dapper.AOT enabled constructors", - "Only one constructor can be Dapper.AOT enabled per type '{0}'"), - TooManyStandardConstructors = LibraryError("DAP036", "Type has more than 1 constructor to choose for creating an instance", - "Type has more than 1 constructor, please, either mark one constructor with [DapperAot] or reduce amount of constructors"), UserTypeNoSettableMembersFound = LibraryError("DAP037", "No settable members exist for user type", - "Type '{0}' has no settable members (fields or properties)"), - ValueTypeSingleFirstOrDefaultUsage = LibraryWarning("DAP038", "Value-type single row 'OrDefault' usage", - "Type '{0}' is a value-type; it will not be trivial to identify missing rows from {1}"), - - // SQL parse specific - GeneralSqlError = SqlWarning("DAP200", "SQL error", "SQL error: {0}"), - MultipleBatches = SqlError("DAP201", "Multiple batches", "Multiple batches are not permitted (L{0} C{1})"), - DuplicateVariableDeclaration = SqlError("DAP202", "Duplicate variable declaration", "The variable {0} is declared multiple times (L{1} C{2})"), - GlobalIdentity = SqlError("DAP203", "Do not use @@identity", "@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L{0} C{1})"), - SelectScopeIdentity = SqlInfo("DAP204", "Prefer OUTPUT over SELECT", "Consider using OUTPUT INSERTED.yourid in the INSERT instead of SELECT SCOPE_IDENTITY() (L{0} C{1})"), - NullLiteralComparison = SqlWarning("DAP205", "Null comparison", "Literal null used in comparison; 'is null' or 'is not null' should be preferred (L{0} C{1})"), - ParseError = SqlError("DAP206", "SQL parse error", "{0} (#{1} L{2} C{3})"), - ScalarVariableUsedAsTable = SqlError("DAP207", "Scalar used like table", "Scalar variable {0} is used like a table (L{1} C{2})"), - TableVariableUsedAsScalar = SqlError("DAP208", "Table used like scalar", "Table-variable {0} is used like a scalar (L{1} C{2})"), - TableVariableAccessedBeforePopulate = SqlError("DAP209", "Table used before populate", "Table-variable {0} is accessed before it populated (L{1} C{2})"), - VariableAccessedBeforeAssignment = SqlError("DAP210", "Variable used before assigned", "Variable {0} is accessed before it is assigned a value (L{1} C{2})"), - VariableAccessedBeforeDeclaration = SqlError("DAP211", "Variable used before declared", "Variable {0} is accessed before it is declared (L{1} C{2})"), - ExecVariable = SqlWarning("DAP212", "EXEC with composed SQL", "EXEC with composed SQL may be susceptible to SQL injection; consider EXEC sp_executesql, taking care to fully parameterize the composed query (L{0} C{1})"), - VariableValueNotConsumed = SqlWarning("DAP213", "Variable used before declared", "Variable {0} has a value that is not consumed (L{1} C{2})"), - VariableNotDeclared = SqlError("DAP214", "Variable not declared", "Variable {0} is not declared and no corresponding parameter exists (L{1} C{2})"), - TableVariableOutputParameter = SqlWarning("DAP215", "Variable used before declared", "Table variable {0} cannot be used as an output parameter (L{1} C{2})"), - InsertColumnsNotSpecified = SqlWarning("DAP216", "INSERT without target columns", "INSERT should explicitly specify target columns (L{0} C{1})"), - InsertColumnsMismatch = SqlError("DAP217", "INSERT with mismatched columns", "The INSERT values do not match the target columns (L{0} C{1})"), - InsertColumnsUnbalanced = SqlError("DAP218", "INSERT with unbalanced rows", "The INSERT rows have different widths (L{0} C{1})"), - SelectStar = SqlWarning("DAP219", "SELECT with wildcard columns", "SELECT columns should be specified explicitly (L{0} C{1})"), - SelectEmptyColumnName = SqlWarning("DAP220", "SELECT with missing column name", "SELECT column name is missing: {0} (L{1} C{2})"), - SelectDuplicateColumnName = SqlWarning("DAP221", "SELECT with duplicate column name", "SELECT column name is duplicated: '{0}' (L{1} C{2})"), - SelectAssignAndRead = SqlWarning("DAP222", "SELECT with assignment and reads", "SELECT statement assigns variable and performs reads (L{0} C{1})"), - DeleteWithoutWhere = SqlWarning("DAP223", "DELETE without WHERE", "DELETE statement lacks WHERE clause (L{0} C{1})"), - UpdateWithoutWhere = SqlWarning("DAP224", "UPDATE without WHERE", "UPDATE statement lacks WHERE clause (L{0} C{1})"), - FromMultiTableMissingAlias = SqlWarning("DAP225", "Multi-element FROM missing alias", "FROM expressions with multiple elements should use aliases (L{0} C{1})"), - FromMultiTableUnqualifiedColumn = SqlWarning("DAP226", "Multi-element FROM with unqualified column", "FROM expressions with multiple elements should qualify all columns; it is unclear where '{0}' is located (L{1} C{2})"), - NonIntegerTop = SqlError("DAP227", "Non-integer TOP", "TOP literals should be integers (L{0} C{1})"), - NonPositiveTop = SqlError("DAP228", "Non-positive TOP", "TOP literals should be positive (L{0} C{1})"), - SelectFirstTopError = SqlWarning("DAP229", "SELECT for First* with invalid TOP", "SELECT for First* should use TOP 1 (L{0} C{1})"), - SelectSingleTopError = SqlWarning("DAP230", "SELECT for Single* with invalid TOP", "SELECT for Single* should use TOP 2; if you do not need to test over-read, use First* (L{0} C{1})"), - SelectSingleRowWithoutWhere = SqlWarning("DAP231", "SELECT for single row without WHERE", "SELECT for single row without WHERE or (TOP and ORDER BY) (L{0} C{1})"); + "Type '{0}' has no settable members (fields or properties)"); } } diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index 4f6b2f58..ff8dcd2a 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -2,12 +2,13 @@ using Dapper.CodeAnalysis.Writers; using Dapper.Internal; using Dapper.Internal.Roslyn; +using Dapper.SqlAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; -using Microsoft.CodeAnalysis.Text; +using Microsoft.SqlServer.TransactSql.ScriptDom; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -15,7 +16,6 @@ using System.Data.Common; using System.Globalization; using System.Linq; -using System.Net.Mime; using System.Text; using System.Threading; @@ -57,288 +57,24 @@ internal bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) internal SourceState? Parse(in ParseState ctx) { if (ctx.Node is not InvocationExpressionSyntax ie - || ctx.SemanticModel.GetOperation(ie) is not IInvocationOperation op) + || ctx.SemanticModel.GetOperation(ie) is not IInvocationOperation op + || !op.IsDapperMethod(out var flags) + || flags.HasAny(OperationFlags.NotAotSupported) + || !Inspection.IsEnabled(ctx, op, Types.DapperAotAttribute, out var aotAttribExists)) { return null; } - var methodKind = op.IsSupportedDapperMethod(out var flags); - switch (methodKind) - { - case DapperMethodKind.DapperUnsupported: - flags |= OperationFlags.DoNotGenerate; - break; - case DapperMethodKind.DapperSupported: - break; - default: // includes NotDapper - return null; - } - // everything requires location - Location? loc = null; - if (op.Syntax.ChildNodes().FirstOrDefault() is MemberAccessExpressionSyntax ma) - { - loc = ma.ChildNodes().Skip(1).FirstOrDefault()?.GetLocation(); - } - loc ??= op.Syntax.GetLocation(); - if (loc is null) - { - return null; - } - - // check whether we can use this method - object? diagnostics = null; - bool dapperEnabled = Inspection.IsEnabled(ctx, op, Types.DapperAotAttribute, out var aotAttribExists); - if (dapperEnabled) - { - if (methodKind == DapperMethodKind.DapperUnsupported) - { - flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UnsupportedMethod, loc, GetSignature(op.TargetMethod))); - } - } - else - { - // no diagnostic per-item; the generator will emit a single diagnostic if everything is disabled - flags |= OperationFlags.AotNotEnabled | OperationFlags.DoNotGenerate; - if (aotAttribExists) flags |= OperationFlags.AotDisabled; // active opt-out - // but we still process the other bits, as there might be relevant additional things - } - - if (Inspection.IsEnabled(ctx, op, Types.CacheCommandAttribute, out _)) - { - flags |= OperationFlags.CacheCommand; - } - - ITypeSymbol? resultType = null, paramType = null; - if (flags.HasAny(OperationFlags.TypedResult)) - { - var typeArgs = op.TargetMethod.TypeArguments; - if (typeArgs.Length == 1) - { - resultType = typeArgs[0]; - - // check for value-type single - if (flags.HasAny(OperationFlags.SingleRow) && !flags.HasAny(OperationFlags.AtLeastOne) - && resultType.IsValueType && !CouldBeNullable(resultType)) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.ValueTypeSingleFirstOrDefaultUsage, loc, resultType.Name, op.TargetMethod.Name)); - } - } - } - - string? sql = null; - SyntaxNode? sqlSyntax = null; - bool? buffered = null; - - // check the args - foreach (var arg in op.Arguments) - { - switch (arg.Parameter?.Name) - { - case "sql": - if (TryGetConstantValueWithSyntax(arg, out string? s, out sqlSyntax)) - { - sql = s; - } - break; - case "buffered": - if (TryGetConstantValue(arg, out bool b)) - { - buffered = b; - } - break; - case "param": - if (arg.Value is not IDefaultValueOperation) - { - var expr = arg.Value; - if (expr is IConversionOperation conv && expr.Type?.SpecialType == SpecialType.System_Object) - { - expr = conv.Operand; - } - paramType = expr?.Type; - flags |= OperationFlags.HasParameters; - } - break; - case "cnn": - case "commandTimeout": - case "transaction": - // nothing to do - break; - case "commandType": - if (TryGetConstantValue(arg, out int? ct)) - { - switch (ct) - { - case null when !string.IsNullOrWhiteSpace(sql): - // if no spaces: interpret as stored proc, else: text - flags |= sql!.Trim().IndexOf(' ') < 0 ? OperationFlags.StoredProcedure : OperationFlags.Text; - break; - case null: - break; // flexible - case 1: - flags |= OperationFlags.Text; - break; - case 4: - flags |= OperationFlags.StoredProcedure; - break; - case 512: - flags |= OperationFlags.TableDirect; - break; - default: - flags |= OperationFlags.DoNotGenerate; - if (dapperEnabled) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UnexpectedCommandType, arg.Syntax.GetLocation())); - } - break; - } - } - break; - default: - if (dapperEnabled && !flags.HasAny(OperationFlags.DoNotGenerate)) - { - flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UnexpectedArgument, arg.Syntax.GetLocation(), arg.Parameter?.Name)); - } - break; - } - } + var location = DapperAnalyzer.SharedParseArgsAndFlags(ctx, op, ref flags, out var sql, out var paramType, reportDiagnostic: null, out var resultType); - if (!string.IsNullOrWhiteSpace(sql) && flags.HasAny(OperationFlags.Text) - && Inspection.IsEnabled(ctx, op, Types.IncludeLocationAttribute, out _)) - { - flags |= OperationFlags.IncludeLocation; - } - if (flags.HasAny(OperationFlags.Query) && buffered.HasValue) - { - flags |= buffered.GetValueOrDefault() ? OperationFlags.Buffered : OperationFlags.Unbuffered; - } // additional result-type checks - if (flags.HasAny(OperationFlags.Query) && Inspection.IdentifyDbType(resultType, out _) is null) - { - flags |= OperationFlags.BindResultsByName; - } - - if (flags.HasAny(OperationFlags.Query) || flags.HasAll(OperationFlags.Execute | OperationFlags.Scalar)) - { - bool resultTuple = Inspection.InvolvesTupleType(resultType, out _), bindByNameDefined = false; - // tuples are positional by default - if (resultTuple && !Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out bindByNameDefined)) - { - flags &= ~OperationFlags.BindResultsByName; - } - - if (flags.HasAny(OperationFlags.DoNotGenerate)) - { - // extra checks specific to Dapper vanilla - if (resultTuple && Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out _)) - { // Dapper vanilla supports bind-by-position for tuples; warn if bind-by-name is enabled - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DapperLegacyBindNameTupleResults, loc)); - } - } - else - { - // extra checks specific to DapperAOT - if (Inspection.InvolvesGenericTypeParameter(resultType)) - { - flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.GenericTypeParameter, loc, resultType!.ToDisplayString())); - } - else if (resultTuple) - { - if (!bindByNameDefined) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DapperAotAddBindTupleByName, loc)); - } - - // but not implemented currently! - flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DapperAotTupleResults, loc)); - } - else if (!Inspection.IsPublicOrAssemblyLocal(resultType, ctx, out var failing)) - { - flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.NonPublicType, loc, failing!.ToDisplayString(), Inspection.NameAccessibility(failing))); - } - } - } - - // additional parameter checks - if (flags.HasAny(OperationFlags.HasParameters)) - { - bool paramTuple = Inspection.InvolvesTupleType(paramType, out _); - if (flags.HasAny(OperationFlags.DoNotGenerate)) - { - // extra checks specific to Dapper vanilla - if (paramTuple) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DapperLegacyTupleParameter, loc)); - } - } - else - { - // extra checks specific to DapperAOT - if (paramTuple) - { - if (Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out var defined)) - { - flags |= OperationFlags.BindTupleParameterByName; - } - if (!defined) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DapperAotAddBindTupleByName, loc)); - } - - // but not implemented currently! - flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DapperAotTupleParameter, loc)); - } - else if (Inspection.InvolvesGenericTypeParameter(paramType)) - { - flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.GenericTypeParameter, loc, paramType!.ToDisplayString())); - } - else if (Inspection.IsMissingOrObjectOrDynamic(paramType)) - { - flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UntypedParameter, loc)); - } - else if (!Inspection.IsPublicOrAssemblyLocal(paramType, ctx, out var failing)) - { - flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.NonPublicType, loc, failing!.ToDisplayString(), Inspection.NameAccessibility(failing))); - } - } - } // perform SQL inspection - var parameterMap = BuildParameterMap(ctx, op, sql, flags, paramType, loc, ref diagnostics, sqlSyntax, out var parseFlags); - - // if we have a good parser *and* the SQL isn't invalid: check for obvious query/exec mismatch - if ((parseFlags & (ParseFlags.Reliable | ParseFlags.SyntaxError)) == ParseFlags.Reliable) - { - switch (flags & (OperationFlags.Execute | OperationFlags.Query | OperationFlags.Scalar)) - { - case OperationFlags.Execute: - if ((parseFlags & ParseFlags.Query) != 0) - { - // definitely have a query - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.ExecuteCommandWithQuery, loc)); - } - break; - case OperationFlags.Query: - case OperationFlags.Execute | OperationFlags.Scalar: - if ((parseFlags & (ParseFlags.Query | ParseFlags.MaybeQuery)) == 0) - { - // definitely do not have a query - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.QueryCommandMissingQuery, loc)); - } - break; - } - } + var map = MemberMap.Create(paramType, true); + var parameterMap = BuildParameterMap(ctx, op, sql, flags, map, location, out var parseFlags); if (flags.HasAny(OperationFlags.CacheCommand)) { @@ -352,344 +88,36 @@ internal bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) if (!canBeCached) flags &= ~OperationFlags.CacheCommand; } - var estimatedRowCount = AdditionalCommandState.Parse(Inspection.GetSymbol(ctx, op), paramType, ref diagnostics); - CheckCallValidity(op, flags, ref diagnostics); - - return new SourceState(loc, op.TargetMethod, flags, sql, resultType, paramType, parameterMap, estimatedRowCount, diagnostics); - - //static bool HasDiagnostic(object? diagnostics, DiagnosticDescriptor diagnostic) - //{ - // if (diagnostic is null) throw new ArgumentNullException(nameof(diagnostic)); - // switch (diagnostics) - // { - // case null: return false; - // case Diagnostic single: return single.Descriptor == diagnostic; - // case IEnumerable list: - // foreach (var single in list) - // { - // if (single.Descriptor == diagnostic) return true; - // } - // return false; - // default: throw new ArgumentException(nameof(diagnostics)); - // } - //} - - static bool TryGetConstantValue(IArgumentOperation op, out T? value) - => TryGetConstantValueWithSyntax(op, out value, out _); - - static bool TryGetConstantValueWithSyntax(IArgumentOperation op, out T? value, out SyntaxNode? syntax) - { - try - { - if (op.ConstantValue.HasValue) - { - value = (T?)op.ConstantValue.Value; - syntax = op.Syntax; - return true; - } - var val = op.Value; - // work through any implicit/explicit conversion steps - while (val is IConversionOperation conv) - { - val = conv.Operand; - } - - // type-level constants - if (val is IFieldReferenceOperation field && field.Field.HasConstantValue) - { - value = (T?)field.Field.ConstantValue; - syntax = field.Field.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(); - return true; - } - - // local constants - if (val is ILocalReferenceOperation local && local.Local.HasConstantValue) - { - value = (T?)local.Local.ConstantValue; - syntax = local.Local.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(); - return true; - } + var estimatedRowCount = AdditionalCommandState.Parse(Inspection.GetSymbol(ctx, op), map); - // other non-trivial default constant values - if (val.ConstantValue.HasValue) - { - value = (T?)val.ConstantValue.Value; - syntax = val.Syntax; - return true; - } + return new SourceState(location, op.TargetMethod, flags, sql, resultType, paramType, parameterMap, estimatedRowCount); - // other trivial default constant values - if (val is ILiteralOperation or IDefaultValueOperation) - { - // we already ruled out explicit constant above, so: must be default - value = default; - syntax = val.Syntax; - return true; - } - } - catch { } - value = default!; - syntax = null; - return false; - } - static string BuildParameterMap(in ParseState ctx, IInvocationOperation op, string? sql, OperationFlags flags, ITypeSymbol? parameterType, Location loc, ref object? diagnostics, SyntaxNode? sqlSyntax, out ParseFlags parseFlags) + static string BuildParameterMap(in ParseState ctx, IInvocationOperation op, string? sql, OperationFlags flags, MemberMap? map, Location loc, out SqlParseOutputFlags parseFlags) { - // if command-type is known statically to be stored procedure etc: pass everything - if (flags.HasAny(OperationFlags.StoredProcedure | OperationFlags.TableDirect)) - { - parseFlags = flags.HasAny(OperationFlags.StoredProcedure) ? ParseFlags.MaybeQuery : ParseFlags.Query; - return flags.HasAny(OperationFlags.HasParameters) ? "*" : ""; - } - // if command-type or command is not known statically: defer decision - if (!flags.HasAny(OperationFlags.Text) || string.IsNullOrWhiteSpace(sql)) - { - parseFlags = ParseFlags.MaybeQuery; - return flags.HasAny(OperationFlags.HasParameters) ? "?" : ""; - } - // check the arg type - if (Inspection.IsCollectionType(parameterType, out var innerType)) - { - parameterType = innerType; - } + var args = DapperAnalyzer.SharedGetParametersToInclude(map, flags, sql, null, out parseFlags); + if (args is null) return "?"; // deferred + var arr = args.Value; - var paramMembers = Inspection.GetMembers(parameterType); + if (arr.IsDefaultOrEmpty) return ""; // nothing to add - // so: we know statically that we have known command-text - // first, try try to find any parameters - ImmutableHashSet paramNames; - switch (IdentifySqlSyntax(ctx, op, out bool caseSensitive)) - { - case SqlSyntax.SqlServer: - SqlAnalysis.TSqlProcessor.ModeFlags modeFlags = SqlAnalysis.TSqlProcessor.ModeFlags.None; - if (caseSensitive) modeFlags |= SqlAnalysis.TSqlProcessor.ModeFlags.CaseSensitive; - if ((flags & OperationFlags.BindResultsByName) != 0) modeFlags |= SqlAnalysis.TSqlProcessor.ModeFlags.ValidateSelectNames; - if ((flags & OperationFlags.SingleRow) != 0) modeFlags |= SqlAnalysis.TSqlProcessor.ModeFlags.SingleRow; - if ((flags & OperationFlags.AtMostOne) != 0) modeFlags |= SqlAnalysis.TSqlProcessor.ModeFlags.AtMostOne; - - var proc = new DiagnosticTSqlProcessor(parameterType, modeFlags, diagnostics, loc, sqlSyntax); - try - { - proc.Execute(sql!, paramMembers); - parseFlags = proc.Flags; - paramNames = (from var in proc.Variables - where var.IsParameter - select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name - ).ToImmutableHashSet(); - diagnostics = proc.DiagnosticsObject; - } - catch (Exception ex) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.GeneralSqlError, loc, ex.Message)); - goto default; // some internal failure - } - break; - default: - paramNames = SqlTools.GetUniqueParameters(sql, out parseFlags); - break; - } - - if (paramNames.IsEmpty && (parseFlags & ParseFlags.Return) == 0) // return is a parameter, sort of - { - if (flags.HasAny(OperationFlags.HasParameters)) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.SqlParametersNotDetected, loc)); - } - return ""; - } - - // so, we definitely detect parameters (note: don't warn just for return) - if (!flags.HasAny(OperationFlags.HasParameters)) - { - if (!paramNames.IsEmpty) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.NoParametersSupplied, loc)); - } - return ""; - } - if (flags.HasAny(OperationFlags.HasParameters) && Inspection.IsMissingOrObjectOrDynamic(parameterType)) + switch (arr.Length) { - // unknown parameter type; defer decision - return "?"; + case 0: return ""; + case 1: return arr[0].CodeName; + case 2: return arr[0].CodeName + " " + arr[1].CodeName; } - - // ok, so the SQL is using parameters; check what we have - var memberDbToCodeNames = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - string? returnCodeMember = null, rowCountMember = null; - foreach (var member in paramMembers) + var sb = new StringBuilder(); + foreach (var arg in arr) { - if (member.IsRowCount) - { - if (rowCountMember is null) - { - rowCountMember = member.CodeName; - } - else - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DuplicateRowCount, loc, member.CodeName, rowCountMember)); - } - if (member.HasDbValueAttribute) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.RowCountDbValue, loc, member.CodeName)); - } - } - if (member.Kind != Inspection.ElementMemberKind.None) - { - continue; // not treated as parameters for naming etc purposes - } - var dbName = member.DbName; - if (memberDbToCodeNames.TryGetValue(dbName, out var existing)) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DuplicateParameter, loc, member.CodeName, existing, dbName)); - } - else - { - memberDbToCodeNames.Add(dbName, member.CodeName); - } - if (member.Direction == ParameterDirection.ReturnValue) - { - if (returnCodeMember is null) - { - returnCodeMember = member.CodeName; - } - else - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DuplicateReturn, loc, member.CodeName, returnCodeMember)); - } - } + if (sb.Length != 0) sb.Append(' '); + sb.Append(arg.CodeName); } - - StringBuilder? sb = null; - foreach (var sqlParamName in paramNames.OrderBy(x => x, StringComparer.InvariantCultureIgnoreCase)) - { - if (memberDbToCodeNames.TryGetValue(sqlParamName, out var codeName)) - { - WithSpace(ref sb).Append(codeName); - } - else - { - // we can only consider this an error if we're confident in how well we parsed the input - // (unless we detected dynamic args, in which case: we don't know what we don't know) - if ((parseFlags & (ParseFlags.Reliable | ParseFlags.DynamicParameters)) == ParseFlags.Reliable) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.SqlParameterNotBound, loc, sqlParamName, CodeWriter.GetTypeName(parameterType))); - } - } - } - if ((parseFlags & ParseFlags.Return) != 0 && returnCodeMember is not null) - { - WithSpace(ref sb).Append(returnCodeMember); - } - return sb is null ? "" : sb.ToString(); - - static StringBuilder WithSpace(ref StringBuilder? sb) => sb is null ? (sb = new()) : (sb.Length == 0 ? sb : sb.Append(' ')); + return sb.ToString(); } } - private void CheckCallValidity(IInvocationOperation op, OperationFlags flags, ref object? diagnostics) - { - if (flags.HasAny(OperationFlags.Query) && !flags.HasAny(OperationFlags.SingleRow) - && op.Parent is IArgumentOperation arg - && arg.Parent is IInvocationOperation parent && parent.TargetMethod is - { - IsExtensionMethod: true, - Parameters.Length: 1, Arity: 1, ContainingType: - { - Name: nameof(Enumerable), - ContainingType: null, - ContainingNamespace: - { - Name: "Linq", - ContainingNamespace: - { - Name: "System", - ContainingNamespace.IsGlobalNamespace: true - } - } - } - } target) - { - string? preferred = parent.TargetMethod.Name switch - { - nameof(Enumerable.First) => "Query" + nameof(Enumerable.First), - nameof(Enumerable.Single) => "Query" + nameof(Enumerable.Single), - nameof(Enumerable.FirstOrDefault) => "Query" + nameof(Enumerable.FirstOrDefault), - nameof(Enumerable.SingleOrDefault) => "Query" + nameof(Enumerable.SingleOrDefault), - _ => null, - }; - if (preferred is not null) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UseSingleRowQuery, parent.Syntax.GetLocation(), preferred, parent.TargetMethod.Name)); - } - else if (parent.TargetMethod.Name == nameof(Enumerable.ToList)) - { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UseQueryAsList, parent.Syntax.GetLocation())); - } - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0042:Deconstruct variable declaration", Justification = "Fine as is; let's not pay the unwrap cost")] - private static SqlSyntax IdentifySqlSyntax(in ParseState ctx, IInvocationOperation op, out bool caseSensitive) - { - caseSensitive = false; - if (op.Arguments[0].Value is IConversionOperation conv && conv.Operand.Type is INamedTypeSymbol { Arity: 0, ContainingType: null } type) - { - var ns = type.ContainingNamespace; - foreach (var candidate in KnownConnectionTypes) - { - var current = ns; - if (type.Name == candidate.Connection - && AssertAndAscend(ref current, candidate.Namespace0) - && AssertAndAscend(ref current, candidate.Namespace1) - && AssertAndAscend(ref current, candidate.Namespace2)) - { - return candidate.Syntax; - } - } - } - - // get fom [SqlSyntax(...)] hint - var attrib = Inspection.GetClosestDapperAttribute(ctx, op, Types.SqlSyntaxAttribute); - if (attrib is not null && attrib.ConstructorArguments.Length == 1 && attrib.ConstructorArguments[0].Value is int i) - { - return (SqlSyntax)i; - } - - return SqlSyntax.General; - - static bool AssertAndAscend(ref INamespaceSymbol ns, string? expected) - { - if (expected is null) - { - return ns.IsGlobalNamespace; - } - else - { - if (ns.Name == expected) - { - ns = ns.ContainingNamespace; - return true; - } - return false; - } - } - } - - private static readonly ImmutableArray<(string? Namespace2, string? Namespace1, string Namespace0, string Connection, SqlSyntax Syntax)> KnownConnectionTypes = new[] - { - ("System", "Data", "SqlClient", "SqlConnection", SqlSyntax.SqlServer), - ("Microsoft", "Data", "SqlClient", "SqlConnection", SqlSyntax.SqlServer), - - (null, null, "Npgsql", "NpgsqlConnection", SqlSyntax.PostgreSql), - - ("MySql", "Data", "MySqlClient", "MySqlConnection", SqlSyntax.MySql), - - ("Oracle", "DataAccess", "Client", "OracleConnection", SqlSyntax.Oracle), - - ("Microsoft", "Data", "Sqlite", "SqliteConnection", SqlSyntax.SQLite), - }.ToImmutableArray(); private static string? GetCommandFactory(Compilation compilation, out bool canConstruct) { @@ -782,11 +210,6 @@ private bool CheckPrerequisites(in GenerateState ctx, out int enabledCount) } } } - if (passiveDisabledCount == ctx.Nodes.Length && IsDapperAotAvailable(ctx.Compilation)) - { - ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.DapperAotNotEnabled, null)); - errorCount++; - } if (doNotGenerateCount == ctx.Nodes.Length) { @@ -799,9 +222,6 @@ private bool CheckPrerequisites(in GenerateState ctx, out int enabledCount) return false; // nothing to validate - so: nothing to do, quick exit } - private bool IsDapperAotAvailable(Compilation compilation) - => compilation.GetTypeByMetadataName("Dapper." + Types.DapperAotAttribute) is not null; - private void Generate(SourceProductionContext ctx, (Compilation Compilation, ImmutableArray Nodes) state) => Generate(new(ctx, state)); @@ -1226,17 +646,17 @@ static bool IsReserved(string name) private static void WriteRowFactory(in GenerateState context, CodeWriter sb, ITypeSymbol type, int index) { - var hasExplicitConstructor = Inspection.TryGetSingleCompatibleDapperAotConstructor(type, out var constructor, out var errorDiagnostic); - if (!hasExplicitConstructor && errorDiagnostic is not null) + var ctorType = Inspection.ChooseConstructor(type, out var constructor); + if (ctorType is Inspection.ConstructorResult.FailMultipleImplicit or Inspection.ConstructorResult.FailMultipleExplicit) { - context.ReportDiagnostic(errorDiagnostic); - // error is emitted, but we still generate default RowFactory to not emit more errors for this type WriteRowFactoryHeader(); WriteRowFactoryFooter(); return; } + bool hasExplicitConstructor = ctorType == Inspection.ConstructorResult.SuccessSingleExplicit + && constructor is not null; var members = Inspection.GetMembers(type, dapperAotConstructor: constructor).ToImmutableArray(); var membersCount = members.Length; @@ -1313,7 +733,7 @@ void WriteReadMethod() var constructorArgumentsOrdered = new SortedList(); if (useDeferredConstruction) { - // dont create an instance now, but define the variables to create an instance later like + // don't create an instance now, but define the variables to create an instance later like // ``` // Type? member0 = default; // Type? member1 = default; @@ -1323,7 +743,7 @@ void WriteReadMethod() { var variableName = DeferredConstructionVariableName + token; - if (CouldBeNullable(member.CodeType)) sb.Append(CodeWriter.GetTypeName(member.CodeType.WithNullableAnnotation(NullableAnnotation.Annotated))); + if (Inspection.CouldBeNullable(member.CodeType)) sb.Append(CodeWriter.GetTypeName(member.CodeType.WithNullableAnnotation(NullableAnnotation.Annotated))); else sb.Append(CodeWriter.GetTypeName(member.CodeType)); sb.Append(' ').Append(variableName).Append(" = default;").NewLine(); @@ -1353,7 +773,7 @@ void WriteReadMethod() var memberType = member.CodeType; member.GetDbType(out var readerMethod); - var nullCheck = CouldBeNullable(memberType) ? $"reader.IsDBNull(columnOffset) ? ({CodeWriter.GetTypeName(memberType.WithNullableAnnotation(NullableAnnotation.Annotated))})null : " : ""; + var nullCheck = Inspection.CouldBeNullable(memberType) ? $"reader.IsDBNull(columnOffset) ? ({CodeWriter.GetTypeName(memberType.WithNullableAnnotation(NullableAnnotation.Annotated))})null : " : ""; sb.Append("case ").Append(token).Append(":").NewLine().Indent(false); // write `result.X = ` or `member0 = ` @@ -1438,10 +858,6 @@ void WriteReadMethod() } } - static bool CouldBeNullable(ITypeSymbol symbol) => symbol.IsValueType - ? symbol.NullableAnnotation == NullableAnnotation.Annotated - : symbol.NullableAnnotation != NullableAnnotation.NotAnnotated; - [Flags] enum WriteArgsFlags { diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs index eb245be8..b62b5ca0 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs @@ -11,7 +11,7 @@ internal abstract class DiagnosticsBase { protected const string DocsRoot = "https://aot.dapperlib.dev/", RulesRoot = DocsRoot + "rules/"; - private static DiagnosticDescriptor Create(string id, string title, string messageFormat, string? category, DiagnosticSeverity severity, bool docs) => + private static DiagnosticDescriptor Create(string id, string title, string messageFormat, string category, DiagnosticSeverity severity, bool docs) => new(id, title, messageFormat, category, severity, true, helpLinkUri: docs ? (RulesRoot + id) : null); @@ -23,11 +23,11 @@ private static DiagnosticDescriptor Create(string id, string title, string messa protected static DiagnosticDescriptor LibraryInfo(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Library, DiagnosticSeverity.Info, docs); - protected static DiagnosticDescriptor SqlWarning(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Sql, DiagnosticSeverity.Warning, docs); + protected static DiagnosticDescriptor SqlWarning(string id, string title, string messageFormat, bool docs = true) => Create(id, title, messageFormat, Category.Sql, DiagnosticSeverity.Warning, docs); - protected static DiagnosticDescriptor SqlError(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Sql, DiagnosticSeverity.Error, docs); + protected static DiagnosticDescriptor SqlError(string id, string title, string messageFormat, bool docs = true) => Create(id, title, messageFormat, Category.Sql, DiagnosticSeverity.Error, docs); - protected static DiagnosticDescriptor SqlInfo(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Sql, DiagnosticSeverity.Info, docs); + protected static DiagnosticDescriptor SqlInfo(string id, string title, string messageFormat, bool docs = true) => Create(id, title, messageFormat, Category.Sql, DiagnosticSeverity.Info, docs); protected static DiagnosticDescriptor PerformanceWarning(string id, string title, string messageFormat, bool docs = false) => Create(id, title, messageFormat, Category.Performance, DiagnosticSeverity.Warning, docs); @@ -41,7 +41,7 @@ public static bool TryGetFieldName(string id, out string field) return (_idsToFieldNames ??= Build()).TryGetValue(id, out field!); static ImmutableDictionary Build() => GetAllFor() - .Concat(GetAllFor()) + .Concat(GetAllFor()) .Concat(GetAllFor()) .ToImmutableDictionary(x => x.Value.Id, x => x.Key, StringComparer.Ordinal, StringComparer.Ordinal); } diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/GlobalOptions.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/GlobalOptions.cs new file mode 100644 index 00000000..f5200394 --- /dev/null +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/GlobalOptions.cs @@ -0,0 +1,49 @@ +using Dapper.SqlAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using System; + +namespace Dapper.CodeAnalysis; + +internal static class GlobalOptions +{ + public static class Keys + { + public const string GlobalOptions_DapperSqlSyntax = "dapper.sqlsyntax"; + public const string GlobalOptions_DapperDebugSqlParseInputFlags = "dapper.debug_mode_sql_parse_input_flags"; // this is for test purposes; if you find and use this: don't blame me! + public const string ProjectProperties_DapperSqlSyntax = "build_property.Dapper_SqlSyntax"; + + } + + public static bool TryGetSqlSyntax(this AnalyzerOptions? options, out SqlSyntax syntax) + { + if (options is not null) + { + if (options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(Keys.GlobalOptions_DapperSqlSyntax, out var value) + && Enum.TryParse(value, true, out syntax)) + { + return true; + } + if (options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(Keys.ProjectProperties_DapperSqlSyntax, out value) + && Enum.TryParse(value, true, out syntax)) + { + return true; + } + } + syntax = SqlSyntax.General; + return false; + } + + public static bool TryGetDebugModeFlags(this AnalyzerOptions? options, out SqlParseInputFlags flags) + { + if (options is not null) + { + if (options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(Keys.GlobalOptions_DapperDebugSqlParseInputFlags, out var value) + && Enum.TryParse(value, true, out flags)) + { + return true; + } + } + flags = SqlParseInputFlags.None; + return false; + } +} diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/OpinionatedAnalyzer.Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/OpinionatedAnalyzer.Diagnostics.cs deleted file mode 100644 index dab5978c..00000000 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/OpinionatedAnalyzer.Diagnostics.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Dapper.CodeAnalysis; - -partial class OpinionatedAnalyzer -{ - internal sealed class Diagnostics : DiagnosticsBase - { - - } -} diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/OpinionatedAnalyzer.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/OpinionatedAnalyzer.cs deleted file mode 100644 index c5cd7806..00000000 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/OpinionatedAnalyzer.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Dapper.Internal; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; -using System.Collections.Immutable; - -namespace Dapper.CodeAnalysis; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed partial class OpinionatedAnalyzer : DiagnosticAnalyzer -{ - public override ImmutableArray SupportedDiagnostics => DiagnosticsBase.All(); - - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - - // respond to method usages - context.RegisterOperationAction(OnOperation, OperationKind.Invocation); - } - - private void OnOperation(OperationAnalysisContext context) - { - if (context.Operation is not IInvocationOperation op) - { - return; // not our concern - } - var methodKind = op.IsSupportedDapperMethod(out var flags); - switch (methodKind) - { - case DapperMethodKind.DapperUnsupported: - flags |= OperationFlags.DoNotGenerate; - break; - case DapperMethodKind.DapperSupported: - break; - default: // includes NotDapper - return; - } - - } -} diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/ParseState.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/ParseState.cs index 7855e703..a6922bf4 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/ParseState.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/ParseState.cs @@ -1,7 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; -using System; using System.Collections.Immutable; using System.Text; using System.Threading; @@ -17,16 +16,26 @@ public ParseState(ParseContextProxy proxy) CancellationToken = proxy.CancellationToken; Node = proxy.Node; SemanticModel = proxy.SemanticModel; + Options = proxy.Options; } - public ParseState(GeneratorSyntaxContext context, CancellationToken cancellationToken) + public ParseState(in GeneratorSyntaxContext context, CancellationToken cancellationToken) { CancellationToken = cancellationToken; Node = context.Node; SemanticModel = context.SemanticModel; + Options = null; + } + public ParseState(in OperationAnalysisContext context) + { + CancellationToken = context.CancellationToken; + Node = context.Operation.Syntax; + SemanticModel = context.Operation.SemanticModel!; + Options = context.Options; } public readonly CancellationToken CancellationToken; public readonly SyntaxNode Node; public readonly SemanticModel SemanticModel; + public readonly AnalyzerOptions? Options; } internal abstract class ParseContextProxy @@ -34,19 +43,21 @@ internal abstract class ParseContextProxy public abstract SyntaxNode Node { get; } public abstract CancellationToken CancellationToken { get; } public abstract SemanticModel SemanticModel { get; } + public abstract AnalyzerOptions? Options { get; } - public static ParseContextProxy Create(in OperationAnalysisContext context) - => new OperationAnalysisContextProxy(in context); + //public static ParseContextProxy Create(in OperationAnalysisContext context) + // => new OperationAnalysisContextProxy(in context); - private sealed class OperationAnalysisContextProxy : ParseContextProxy - { - private readonly OperationAnalysisContext context; - public OperationAnalysisContextProxy(in OperationAnalysisContext context) - => this.context = context; - public override SyntaxNode Node => context.Operation.Syntax; - public override CancellationToken CancellationToken => context.CancellationToken; - public override SemanticModel SemanticModel => context.Compilation.GetSemanticModel(Node.SyntaxTree); - } + //private sealed class OperationAnalysisContextProxy : ParseContextProxy + //{ + // private readonly OperationAnalysisContext context; + // public OperationAnalysisContextProxy(in OperationAnalysisContext context) + // => this.context = context; + // public override SyntaxNode Node => context.Operation.Syntax; + // public override CancellationToken CancellationToken => context.CancellationToken; + // public override SemanticModel SemanticModel => context.Compilation.GetSemanticModel(Node.SyntaxTree); + // public override AnalyzerOptions? Options => context.Options; + //} } diff --git a/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj b/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj index 3341a58d..8be71cbf 100644 --- a/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj +++ b/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj @@ -23,20 +23,24 @@ DapperInterceptorGenerator.cs - - OpinionatedAnalyzer.cs + + DapperAnalyzer.cs + + + LanguageHelper.cs TypeAccessorInterceptorGenerator.cs - + + @@ -49,5 +53,8 @@ DapperInterceptorGenerator.cs + + DapperAnalyzer.cs + diff --git a/src/Dapper.AOT.Analyzers/Internal/AdditionalCommandState.cs b/src/Dapper.AOT.Analyzers/Internal/AdditionalCommandState.cs index 970ee211..b96673ee 100644 --- a/src/Dapper.AOT.Analyzers/Internal/AdditionalCommandState.cs +++ b/src/Dapper.AOT.Analyzers/Internal/AdditionalCommandState.cs @@ -42,99 +42,17 @@ internal sealed class AdditionalCommandState : IEquatable !CommandProperties.IsDefaultOrEmpty; - public static AdditionalCommandState? Parse(ISymbol? target, ITypeSymbol? parameterType, ref object? diagnostics) + public static AdditionalCommandState? Parse(ISymbol? target, MemberMap? map) { if (target is null) return null; - var inherited = target is IAssemblySymbol ? null : Parse(target.ContainingSymbol, parameterType, ref diagnostics); - var local = ReadFor(target, parameterType, ref diagnostics); + var inherited = target is IAssemblySymbol ? null : Parse(target.ContainingSymbol, map); + var local = DapperAnalyzer.SharedGetAdditionalCommandState(target, map, null); if (inherited is null) return local; if (local is null) return inherited; return Combine(inherited, local); } - private static AdditionalCommandState? ReadFor(ISymbol target, ITypeSymbol? parameterType, ref object? diagnostics) - { - var attribs = target.GetAttributes(); - if (attribs.IsDefaultOrEmpty) return null; - - int cmdPropsCount = 0; - int estimatedRowCount = 0; - string? estimatedRowCountMember = null; - if (parameterType is not null) - { - foreach (var member in Inspection.GetMembers(parameterType)) - { - if (member.IsEstimatedRowCount) - { - if (estimatedRowCountMember is not null) - { - DiagnosticsBase.Add(ref diagnostics, Diagnostic.Create(DapperInterceptorGenerator.Diagnostics.MemberRowCountHintDuplicated, member.GetLocation())); - } - estimatedRowCountMember = member.Member.Name; - } - } - } - var location = target.Locations.FirstOrDefault(); - foreach (var attrib in attribs) - { - if (Inspection.IsDapperAttribute(attrib)) - { - switch (attrib.AttributeClass!.Name) - { - case Types.EstimatedRowCountAttribute: - if (attrib.ConstructorArguments.Length == 1 && attrib.ConstructorArguments[0].Value is int i - && i > 0) - { - if (estimatedRowCountMember is null) - { - estimatedRowCount = i; - } - else - { - DiagnosticsBase.Add(ref diagnostics, Diagnostic.Create(DapperInterceptorGenerator.Diagnostics.MethodRowCountHintRedundant, location, estimatedRowCountMember)); - } - } - else - { - DiagnosticsBase.Add(ref diagnostics, Diagnostic.Create(DapperInterceptorGenerator.Diagnostics.MethodRowCountHintInvalid, location)); - } - break; - case Types.CommandPropertyAttribute: - cmdPropsCount++; - break; - } - } - } - - ImmutableArray cmdProps; - if (cmdPropsCount != 0) - { - var builder = ImmutableArray.CreateBuilder(cmdPropsCount); - foreach (var attrib in attribs) - { - if (Inspection.IsDapperAttribute(attrib) && attrib.AttributeClass!.Name == Types.CommandPropertyAttribute - && attrib.AttributeClass.Arity == 1 - && attrib.AttributeClass.TypeArguments[0] is INamedTypeSymbol cmdType - && attrib.ConstructorArguments.Length == 2 - && attrib.ConstructorArguments[0].Value is string name - && attrib.ConstructorArguments[1].Value is object value) - { - builder.Add(new(cmdType, name, value, location)); - } - } - cmdProps = builder.ToImmutable(); - } - else - { - cmdProps = ImmutableArray.Empty; - } - - - return cmdProps.IsDefaultOrEmpty && estimatedRowCount <= 0 && estimatedRowCountMember is null - ? null : new(estimatedRowCount, estimatedRowCountMember, cmdProps); - } - private static AdditionalCommandState Combine(AdditionalCommandState inherited, AdditionalCommandState overrides) { if (inherited is null) return overrides; @@ -167,7 +85,7 @@ static ImmutableArray Concat(ImmutableArray x, return builder.ToImmutable(); } - private AdditionalCommandState(int estimatedRowCount, string? estimatedRowCountMemberName, ImmutableArray commandProperties) + internal AdditionalCommandState(int estimatedRowCount, string? estimatedRowCountMemberName, ImmutableArray commandProperties) { EstimatedRowCount = estimatedRowCount; EstimatedRowCountMemberName = estimatedRowCountMemberName; diff --git a/src/Dapper.AOT.Analyzers/Internal/DiagnosticTSqlProcessor.cs b/src/Dapper.AOT.Analyzers/Internal/DiagnosticTSqlProcessor.cs index c63f8da7..764b5980 100644 --- a/src/Dapper.AOT.Analyzers/Internal/DiagnosticTSqlProcessor.cs +++ b/src/Dapper.AOT.Analyzers/Internal/DiagnosticTSqlProcessor.cs @@ -2,7 +2,7 @@ using Dapper.Internal.Roslyn; using Dapper.SqlAnalysis; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.SqlServer.TransactSql.ScriptDom; using System; using System.Data; @@ -10,153 +10,164 @@ namespace Dapper.Internal; -internal class DiagnosticTSqlProcessor : TSqlProcessor +internal sealed class OperationAnalysisContextTSqlProcessor : DiagnosticTSqlProcessor +{ + private readonly OperationAnalysisContext _ctx; + public OperationAnalysisContextTSqlProcessor(in OperationAnalysisContext ctx, ITypeSymbol? parameterType, SqlParseInputFlags flags, + Microsoft.CodeAnalysis.Location? location, SyntaxNode? sqlSyntax) + :base(parameterType, flags, location, sqlSyntax) + { + _ctx = ctx; + } + protected override void OnDiagnostic(Diagnostic diagnostic) => _ctx.ReportDiagnostic(diagnostic); +} +internal abstract class DiagnosticTSqlProcessor : TSqlProcessor { - private object? _diagnostics; private readonly Microsoft.CodeAnalysis.Location? _location; - private readonly LiteralExpressionSyntax? _literal; - public object? DiagnosticsObject => _diagnostics; + private readonly SyntaxToken? _sourceToken; + private readonly ITypeSymbol? _parameterType; - private readonly bool _assumeParameterExists; - public DiagnosticTSqlProcessor(ITypeSymbol? parameterType, ModeFlags flags, object? diagnostics, - Microsoft.CodeAnalysis.Location? location, SyntaxNode? sqlSyntax) : base(flags) + + private static SqlParseInputFlags CheckForKnownParameterType(SqlParseInputFlags flags, ITypeSymbol? parameterType) + { + if (parameterType is not null) + { + if (Inspection.IsMissingOrObjectOrDynamic(parameterType) || Inspection.IsDynamicParameters(parameterType)) + { } + else + { + // we think we understand what is going on, yay! + flags |= SqlParseInputFlags.KnownParameters; + } + } + return flags; + } + public DiagnosticTSqlProcessor(ITypeSymbol? parameterType, SqlParseInputFlags flags, + Microsoft.CodeAnalysis.Location? location, SyntaxNode? sqlSyntax) : base(CheckForKnownParameterType(flags, parameterType)) { - _diagnostics = diagnostics; _location = sqlSyntax?.GetLocation() ?? location; _parameterType = parameterType; - _assumeParameterExists = Inspection.IsMissingOrObjectOrDynamic(_parameterType) || IsDyanmicParameters(parameterType); - if (_assumeParameterExists) AddFlags(ParseFlags.DynamicParameters); - switch (sqlSyntax) + if (sqlSyntax.TryGetLiteralToken(out var token)) { - case LiteralExpressionSyntax direct: - _literal = direct; - break; - case VariableDeclaratorSyntax decl when decl.Initializer?.Value is LiteralExpressionSyntax indirect: - _literal = indirect; - break; - // other interesting possibilities include InterpolatedStringExpression, AddExpression + _sourceToken = token; } } + protected abstract void OnDiagnostic(Diagnostic diagnostic); + private void OnDiagnostic(DiagnosticDescriptor descriptor, Location location, params object[] args) + => OnDiagnostic(Diagnostic.Create(descriptor, GetLocation(location), args)); + public override void Reset() { base.Reset(); - if (_assumeParameterExists) AddFlags(ParseFlags.DynamicParameters); - } - - static bool IsDyanmicParameters(ITypeSymbol? type) - { - if (type is null || type.SpecialType != SpecialType.None) return false; - if (Inspection.IsBasicDapperType(type, Types.DynamicParameters) - || Inspection.IsNestedSqlMapperType(type, Types.IDynamicParameters, TypeKind.Interface)) return true; - foreach (var i in type.AllInterfaces) - { - if (Inspection.IsNestedSqlMapperType(i, Types.IDynamicParameters, TypeKind.Interface)) return true; - } - return false; } private Microsoft.CodeAnalysis.Location? GetLocation(in Location location) { - if (_literal is not null) + if (_sourceToken is not null) { - return LiteralLocationHelper.ComputeLocation(_literal.Token, in location); + return _sourceToken.GetValueOrDefault().ComputeLocation(in location); } // just point to the operation return _location; } - private void AddDiagnostic(DiagnosticDescriptor diagnostic, in Location location, params object?[]? args) - { - DiagnosticsBase.Add(ref _diagnostics, Diagnostic.Create(diagnostic, GetLocation(location), args)); - } protected override void OnError(string error, in Location location) { Debug.Fail("unhandled error: " + error); - AddDiagnostic(DapperInterceptorGenerator.Diagnostics.GeneralSqlError, location, error, location.Line, location.Column); + OnDiagnostic(DapperAnalyzer.Diagnostics.GeneralSqlError, location, error); } protected override void OnAdditionalBatch(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.MultipleBatches, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.MultipleBatches, location); protected override void OnDuplicateVariableDeclaration(Variable variable) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.DuplicateVariableDeclaration, variable.Location, variable.Name, variable.Location.Line, variable.Location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.DuplicateVariableDeclaration, variable.Location, variable.Name); - protected override void OnExecVariable(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.ExecVariable, location, location.Line, location.Column); + protected override void OnExecComposedSql(Location location) + => OnDiagnostic(DapperAnalyzer.Diagnostics.ExecComposedSql, location); protected override void OnSelectScopeIdentity(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.SelectScopeIdentity, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.SelectScopeIdentity, location); protected override void OnGlobalIdentity(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.GlobalIdentity, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.GlobalIdentity, location); protected override void OnNullLiteralComparison(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.NullLiteralComparison, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.NullLiteralComparison, location); protected override void OnParseError(ParseError error, Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.ParseError, location, error.Message, error.Number, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.ParseError, location, error.Number, error.Message); protected override void OnScalarVariableUsedAsTable(Variable variable) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.ScalarVariableUsedAsTable, variable.Location, variable.Name, variable.Location.Line, variable.Location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.ScalarVariableUsedAsTable, variable.Location, variable.Name); protected override void OnTableVariableUsedAsScalar(Variable variable) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.TableVariableUsedAsScalar, variable.Location, variable.Name, variable.Location.Line, variable.Location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.TableVariableUsedAsScalar, variable.Location, variable.Name); protected override void OnVariableAccessedBeforeAssignment(Variable variable) - => AddDiagnostic(variable.IsTable - ? DapperInterceptorGenerator.Diagnostics.TableVariableAccessedBeforePopulate - : DapperInterceptorGenerator.Diagnostics.VariableAccessedBeforeAssignment, variable.Location, variable.Name, variable.Location.Line, variable.Location.Column); + => OnDiagnostic(variable.IsTable + ? DapperAnalyzer.Diagnostics.TableVariableAccessedBeforePopulate + : DapperAnalyzer.Diagnostics.VariableAccessedBeforeAssignment, variable.Location, variable.Name); protected override void OnVariableNotDeclared(Variable variable) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.VariableNotDeclared, variable.Location, variable.Name, variable.Location.Line, variable.Location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.VariableNotDeclared, variable.Location, variable.Name); protected override void OnVariableAccessedBeforeDeclaration(Variable variable) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.VariableAccessedBeforeDeclaration, variable.Location, variable.Name, variable.Location.Line, variable.Location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.VariableAccessedBeforeDeclaration, variable.Location, variable.Name); protected override void OnVariableValueNotConsumed(Variable variable) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.VariableValueNotConsumed, variable.Location, variable.Name, variable.Location.Line, variable.Location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.VariableValueNotConsumed, variable.Location, variable.Name); protected override void OnTableVariableOutputParameter(Variable variable) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.TableVariableOutputParameter, variable.Location, variable.Name, variable.Location.Line, variable.Location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.TableVariableOutputParameter, variable.Location, variable.Name); protected override void OnInsertColumnMismatch(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.InsertColumnsMismatch, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.InsertColumnsMismatch, location); protected override void OnInsertColumnsNotSpecified(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.InsertColumnsNotSpecified, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.InsertColumnsNotSpecified, location); protected override void OnInsertColumnsUnbalanced(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.InsertColumnsUnbalanced, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.InsertColumnsUnbalanced, location); protected override void OnSelectStar(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.SelectStar, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.SelectStar, location); protected override void OnSelectEmptyColumnName(Location location, int column) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.SelectEmptyColumnName, location, column, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.SelectEmptyColumnName, location, column); protected override void OnSelectDuplicateColumnName(Location location, string name) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.SelectDuplicateColumnName, location, name, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.SelectDuplicateColumnName, location, name); protected override void OnSelectAssignAndRead(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.SelectAssignAndRead, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.SelectAssignAndRead, location); protected override void OnUpdateWithoutWhere(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.UpdateWithoutWhere, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.UpdateWithoutWhere, location); protected override void OnDeleteWithoutWhere(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.DeleteWithoutWhere, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.DeleteWithoutWhere, location); protected override void OnFromMultiTableMissingAlias(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.FromMultiTableMissingAlias, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.FromMultiTableMissingAlias, location); protected override void OnFromMultiTableUnqualifiedColumn(Location location, string name) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.FromMultiTableUnqualifiedColumn, location, name, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.FromMultiTableUnqualifiedColumn, location, name); protected override void OnNonIntegerTop(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.NonIntegerTop, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.NonIntegerTop, location); protected override void OnNonPositiveTop(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.NonPositiveTop, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.NonPositiveTop, location); + protected override void OnNonPositiveFetch(Location location) + => OnDiagnostic(DapperAnalyzer.Diagnostics.NonPositiveFetch, location); + protected override void OnNegativeOffset(Location location) + => OnDiagnostic(DapperAnalyzer.Diagnostics.NegativeOffset, location); protected override void OnSelectFirstTopError(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.SelectFirstTopError, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.SelectFirstTopError, location); protected override void OnSelectSingleRowWithoutWhere(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.SelectSingleRowWithoutWhere, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.SelectSingleRowWithoutWhere, location); protected override void OnSelectSingleTopError(Location location) - => AddDiagnostic(DapperInterceptorGenerator.Diagnostics.SelectSingleTopError, location, location.Line, location.Column); + => OnDiagnostic(DapperAnalyzer.Diagnostics.SelectSingleTopError, location); + + protected override void OnSimplifyExpression(Location location, string value) + => OnDiagnostic(DapperAnalyzer.Diagnostics.SimplifyExpression, location, value); + + protected override void OnTopWithOffset(Location location) + => OnDiagnostic(DapperAnalyzer.Diagnostics.TopWithOffset, location); protected override bool TryGetParameter(string name, out ParameterDirection direction) { // we have knowledge of the type system; use it - if (_parameterType is null) { // no parameter @@ -164,12 +175,7 @@ protected override bool TryGetParameter(string name, out ParameterDirection dire return false; } - if (_assumeParameterExists) // dynamic, etc - { - // dynamic - direction = ParameterDirection.Input; - return true; - } + if (!string.IsNullOrEmpty(name) && name[0] == '@') { diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index 26298387..8a293939 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -1,9 +1,8 @@ using Dapper.CodeAnalysis; using Dapper.Internal.Roslyn; +using Dapper.SqlAnalysis; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Operations; -using Microsoft.SqlServer.TransactSql.ScriptDom; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -11,7 +10,6 @@ using System.Data.Common; using System.Diagnostics; using System.Linq; -using System.Threading; namespace Dapper.Internal; @@ -27,16 +25,24 @@ public static bool InvolvesTupleType(this ITypeSymbol? type, out bool hasNames) hasNames = false; if (named is not null) { + int i = 1; foreach (var field in named.TupleElements) { - if (!string.IsNullOrWhiteSpace(field.Name)) + if (!IsDefaultFieldName(field.Name, i)) { hasNames = true; break; } + i++; } return true; } + + static bool IsDefaultFieldName(string name, int index) + { + if (string.IsNullOrWhiteSpace(name)) return true; + return name.StartsWith("Item") && name == $"Item{index}"; + } } if (type is IArrayTypeSymbol array) { @@ -101,7 +107,7 @@ public static bool IsNestedSqlMapperType(ITypeSymbol? type, string name, TypeKin var syntax = op.Syntax; while (syntax is not null) { - if (syntax.IsKind(SyntaxKind.MethodDeclaration)) + if (syntax.IsMethodDeclaration()) { return syntax; } @@ -153,6 +159,18 @@ public static bool IsDapperAttribute(AttributeData attrib) public static bool IsMissingOrObjectOrDynamic(ITypeSymbol? type) => type is null || type.SpecialType == SpecialType.System_Object || type.TypeKind == TypeKind.Dynamic; + internal static bool IsDynamicParameters(ITypeSymbol? type) + { + if (type is null || type.SpecialType != SpecialType.None) return false; + if (Inspection.IsBasicDapperType(type, Types.DynamicParameters) + || Inspection.IsNestedSqlMapperType(type, Types.IDynamicParameters, TypeKind.Interface)) return true; + foreach (var i in type.AllInterfaces) + { + if (Inspection.IsNestedSqlMapperType(i, Types.IDynamicParameters, TypeKind.Interface)) return true; + } + return false; + } + public static bool IsPublicOrAssemblyLocal(ISymbol? symbol, in ParseState ctx, out ISymbol? failingSymbol) => IsPublicOrAssemblyLocal(symbol, ctx.SemanticModel.Compilation.Assembly, out failingSymbol); @@ -430,55 +448,13 @@ public override bool Equals(object obj) => obj is ElementMember other public Location? GetLocation() => Member.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax()?.GetLocation(); } - /// - /// Chooses a single constructor of type which to use for type's instances creation. - /// - /// symbol for type to analyze - /// the method symbol for selected constructor - /// if constructor selection was invalid, contains a diagnostic with error to emit to generation context - /// - public static bool TryGetSingleCompatibleDapperAotConstructor( - ITypeSymbol? typeSymbol, - out IMethodSymbol? constructor, - out Diagnostic? errorDiagnostic) + public enum ConstructorResult { - var (standardCtors, dapperAotEnabledCtors) = ChooseDapperAotCompatibleConstructors(typeSymbol); - if (standardCtors.Count == 0 && dapperAotEnabledCtors.Count == 0) - { - errorDiagnostic = null; - constructor = null!; - return false; - } - - // if multiple constructors remain, and multiple are marked [DapperAot]/[DapperAot(true)], a generator error is emitted and no constructor is selected - if (dapperAotEnabledCtors.Count > 1) - { - // attaching diagnostic to first location of first ctor - var loc = dapperAotEnabledCtors.First().Locations.First(); - - errorDiagnostic = Diagnostic.Create(DapperInterceptorGenerator.Diagnostics.TooManyDapperAotEnabledConstructors, loc, typeSymbol!.ToDisplayString()); - constructor = null!; - return false; - } - - if (dapperAotEnabledCtors.Count == 1) - { - errorDiagnostic = null; - constructor = dapperAotEnabledCtors.First(); - return true; - } - - if (standardCtors.Count == 1) - { - errorDiagnostic = null; - constructor = standardCtors.First(); - return true; - } - - // we cant choose a constructor, so we simply dont choose any - errorDiagnostic = null; - constructor = null!; - return false; + NoneFound, + SuccessSingleExplicit, + SuccessSingleImplicit, + FailMultipleExplicit, + FailMultipleImplicit, } /// @@ -486,59 +462,97 @@ public static bool TryGetSingleCompatibleDapperAotConstructor( /// a) parameterless /// b) marked with [DapperAot(false)] /// - private static (IReadOnlyCollection standardConstructors, IReadOnlyCollection dapperAotEnabledConstructors) ChooseDapperAotCompatibleConstructors(ITypeSymbol? typeSymbol) + internal static ConstructorResult ChooseConstructor(ITypeSymbol? typeSymbol, out IMethodSymbol? constructor) { - if (!typeSymbol.TryGetConstructors(out var constructors)) + constructor = null; + if (typeSymbol is not INamedTypeSymbol named) { - return (standardConstructors: Array.Empty(), dapperAotEnabledConstructors: Array.Empty()); + return ConstructorResult.NoneFound; } - // special case - if (typeSymbol!.IsRecord && constructors?.Length == 2) + var ctors = named.Constructors; + if (ctors.IsDefaultOrEmpty) { - // in case of record syntax with primary constructor like: - // `public record MyRecord(int Id, string Name);` - // we need to pick the first constructor, which is the primary one. The second one would contain single parameter of type itself. - // So checking second constructor suits this rule and picking the first one. + return ConstructorResult.NoneFound; + } - if (constructors.Value[1].Parameters.Length == 1 && constructors.Value[1].Parameters.First().Type.ToDisplayString() == typeSymbol.ToDisplayString()) + // look for [ExplicitConstructor] + foreach (var ctor in ctors) + { + if (GetDapperAttribute(ctor, Types.ExplicitConstructorAttribute) is not null) { - return (standardConstructors: new[] { constructors.Value.First() }, dapperAotEnabledConstructors: Array.Empty()); + if (constructor is not null) + { + return ConstructorResult.FailMultipleExplicit; + } + constructor = ctor; } } - - var standardCtors = new List(); - var dapperAotEnabledCtors = new List(); - - foreach (var constructorMethodSymbol in constructors!) + if (constructor is not null) { - // not taking into an account parameterless constructors - if (constructorMethodSymbol.Parameters.Length == 0) continue; + // we found one [ExplicitConstructor] + return ConstructorResult.SuccessSingleExplicit; + } - var dapperAotAttribute = GetDapperAttribute(constructorMethodSymbol, Types.DapperAotAttribute); - if (dapperAotAttribute is null) + // look for remaining constructors + foreach (var ctor in ctors) + { + switch (ctor.DeclaredAccessibility) { - // picking constructor which is not marked with [DapperAot] attribute at all - standardCtors.Add(constructorMethodSymbol); - continue; + case Accessibility.Private: + case Accessibility.Protected: + case Accessibility.Friend: + case Accessibility.ProtectedAndInternal: + continue; // we can't use it, so... } + var args = ctor.Parameters; + if (args.Length == 0) continue; // default constructor - if (dapperAotAttribute.ConstructorArguments.Length == 0) + // exclude copy constructors (anything that takes the own type) and serialization constructors + bool exclude = false; + foreach (var arg in args) + { + if (SymbolEqualityComparer.Default.Equals(arg.Type, named)) + { + exclude = true; + break; + } + if (arg.Type is INamedTypeSymbol + { + Name: "SerializationInfo" or "StreamingContext", + ContainingType: null, + Arity: 0, + ContainingNamespace: + { + Name: "Serialization", + ContainingNamespace: + { + Name: "Runtime", + ContainingNamespace: { + Name: "System", + ContainingNamespace.IsGlobalNamespace: true + } + } + } + }) + { + exclude = true; + break; + } + } + if (exclude) { - // picking constructor which is marked with [DapperAot] attribute without arguments (its enabled by default) - dapperAotEnabledCtors.Add(constructorMethodSymbol); + // ruled out by signature continue; } - - var typedArg = dapperAotAttribute.ConstructorArguments.First(); - if (typedArg.Value is bool isAot && isAot) + + if (constructor is not null) { - // picking constructor which is marked with explicit [DapperAot(true)] - dapperAotEnabledCtors.Add(constructorMethodSymbol); + return ConstructorResult.FailMultipleImplicit; } + constructor = ctor; } - - return (standardCtors, dapperAotEnabledCtors); + return constructor is null ? ConstructorResult.NoneFound : ConstructorResult.SuccessSingleImplicit; } /// @@ -775,18 +789,18 @@ public static ITypeSymbol MakeNonNullable(ITypeSymbol type) return null; } - public static DapperMethodKind IsSupportedDapperMethod(this IInvocationOperation operation, out OperationFlags flags) + public static bool IsDapperMethod(this IInvocationOperation operation, out OperationFlags flags) { flags = OperationFlags.None; var method = operation?.TargetMethod; if (method is null || !method.IsExtensionMethod) { - return DapperMethodKind.NotDapper; + return false; } var type = method.ContainingType; if (type is not { Name: Types.SqlMapper, ContainingNamespace: { Name: "Dapper", ContainingNamespace.IsGlobalNamespace: true } }) { - return DapperMethodKind.NotDapper; + return false; } if (method.Name.EndsWith("Async")) { @@ -800,7 +814,8 @@ public static DapperMethodKind IsSupportedDapperMethod(this IInvocationOperation { case "Query": flags |= OperationFlags.Query; - return method.Arity <= 1 ? DapperMethodKind.DapperSupported : DapperMethodKind.DapperUnsupported; + if (method.Arity > 1) flags |= OperationFlags.NotAotSupported; + return true; case "QueryAsync": case "QueryUnbufferedAsync": flags |= method.Name.Contains("Unbuffered") ? OperationFlags.Unbuffered : OperationFlags.Buffered; @@ -821,21 +836,226 @@ public static DapperMethodKind IsSupportedDapperMethod(this IInvocationOperation case "QuerySingleOrDefaultAsync": flags |= OperationFlags.SingleRow | OperationFlags.AtMostOne; goto case "Query"; + case "QueryMultiple": + case "QueryMultipleAsync": + flags |= OperationFlags.Query | OperationFlags.QueryMultiple | OperationFlags.NotAotSupported; + return true; case "Execute": case "ExecuteAsync": flags |= OperationFlags.Execute; - return method.Arity == 0 ? DapperMethodKind.DapperSupported : DapperMethodKind.DapperUnsupported; + if (method.Arity != 0) flags |= OperationFlags.NotAotSupported; + return true; case "ExecuteScalar": case "ExecuteScalarAsync": flags |= OperationFlags.Execute | OperationFlags.Scalar; - return method.Arity <= 1 ? DapperMethodKind.DapperSupported : DapperMethodKind.DapperUnsupported; + if (method.Arity >= 2) flags |= OperationFlags.NotAotSupported; + return true; default: - return DapperMethodKind.DapperUnsupported; + flags = OperationFlags.NotAotSupported; + return true; } } public static bool HasAny(this OperationFlags value, OperationFlags testFor) => (value & testFor) != 0; public static bool HasAll(this OperationFlags value, OperationFlags testFor) => (value & testFor) == testFor; + public static bool TryGetConstantValue(IOperation op, out T? value) + => TryGetConstantValueWithSyntax(op, out value, out _); + + public static ITypeSymbol? GetResultType(this IInvocationOperation invocation, OperationFlags flags) + { + if (flags.HasAny(OperationFlags.TypedResult)) + { + var typeArgs = invocation.TargetMethod.TypeArguments; + if (typeArgs.Length == 1) + { + return typeArgs[0]; + } + } + return null; + } + + internal static bool CouldBeNullable(ITypeSymbol symbol) => symbol.IsValueType + ? symbol.NullableAnnotation == NullableAnnotation.Annotated + : symbol.NullableAnnotation != NullableAnnotation.NotAnnotated; + + public static bool TryGetConstantValueWithSyntax(IOperation val, out T? value, out SyntaxNode? syntax) + { + try + { + if (val.ConstantValue.HasValue) + { + value = (T?)val.ConstantValue.Value; + syntax = val.Syntax; + return true; + } + if (val is IArgumentOperation arg) + { + val = arg.Value; + } + // work through any implicit/explicit conversion steps + while (val is IConversionOperation conv) + { + val = conv.Operand; + } + + // type-level constants + if (val is IFieldReferenceOperation field && field.Field.HasConstantValue) + { + value = (T?)field.Field.ConstantValue; + syntax = field.Field.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(); + return true; + } + + // local constants + if (val is ILocalReferenceOperation local && local.Local.HasConstantValue) + { + value = (T?)local.Local.ConstantValue; + syntax = local.Local.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(); + return true; + } + + // other non-trivial default constant values + if (val.ConstantValue.HasValue) + { + value = (T?)val.ConstantValue.Value; + syntax = val.Syntax; + return true; + } + + // other trivial default constant values + if (val is ILiteralOperation or IDefaultValueOperation) + { + // we already ruled out explicit constant above, so: must be default + value = default; + syntax = val.Syntax; + return true; + } + } + catch { } + value = default!; + syntax = null; + return false; + } + + internal static bool IsCommand(INamedTypeSymbol type) + { + switch (type.TypeKind) + { + case TypeKind.Interface: + return type is + { + Name: nameof(IDbCommand), + ContainingType: null, + Arity: 0, + IsGenericType: false, + ContainingNamespace: + { + Name: "Data", + ContainingNamespace: + { + Name: "System", + ContainingNamespace.IsGlobalNamespace: true, + } + } + }; + case TypeKind.Class when !type.IsStatic: + while (type.SpecialType != SpecialType.System_Object) + { + if (type is + { + Name: nameof(DbCommand), + ContainingType: null, + Arity: 0, + IsGenericType: false, + ContainingNamespace: + { + Name: "Common", + ContainingNamespace: + { + Name: "Data", + ContainingNamespace: + { + Name: "System", + ContainingNamespace.IsGlobalNamespace: true, + } + } + } + }) + { + return true; + } + if (type.BaseType is null) break; + type = type.BaseType; + } + break; + } + 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) + { + caseSensitive = false; + // get from known connection-type + if (op is IInvocationOperation invoke) + { + if (!invoke.Arguments.IsDefaultOrEmpty && invoke.Arguments[0].Value is IConversionOperation conv && conv.Operand.Type is INamedTypeSymbol { Arity: 0, ContainingType: null } type) + { + var ns = type.ContainingNamespace; + foreach (var candidate in KnownConnectionTypes) + { + var current = ns; + if (type.Name == candidate.Connection + && AssertAndAscend(ref current, candidate.Namespace0) + && AssertAndAscend(ref current, candidate.Namespace1) + && AssertAndAscend(ref current, candidate.Namespace2)) + { + return candidate.Syntax; + } + } + } + } + + // get fom [SqlSyntax(...)] hint + var attrib = GetClosestDapperAttribute(ctx, op, Types.SqlSyntaxAttribute); + if (attrib is not null && attrib.ConstructorArguments.Length == 1 && attrib.ConstructorArguments[0].Value is int i) + { + return (SqlSyntax)i; + } + + return null; + + static bool AssertAndAscend(ref INamespaceSymbol ns, string? expected) + { + if (expected is null) + { + return ns.IsGlobalNamespace; + } + else + { + if (ns.Name == expected) + { + ns = ns.ContainingNamespace; + return true; + } + return false; + } + } + } + + private static readonly ImmutableArray<(string? Namespace2, string? Namespace1, string Namespace0, string Connection, SqlSyntax Syntax)> KnownConnectionTypes = new[] + { + ("System", "Data", "SqlClient", "SqlConnection", SqlSyntax.SqlServer), + ("Microsoft", "Data", "SqlClient", "SqlConnection", SqlSyntax.SqlServer), + + (null, null, "Npgsql", "NpgsqlConnection", SqlSyntax.PostgreSql), + + ("MySql", "Data", "MySqlClient", "MySqlConnection", SqlSyntax.MySql), + + ("Oracle", "DataAccess", "Client", "OracleConnection", SqlSyntax.Oracle), + + ("Microsoft", "Data", "Sqlite", "SqliteConnection", SqlSyntax.SQLite), + }.ToImmutableArray(); } enum DapperMethodKind @@ -870,4 +1090,7 @@ enum OperationFlags BindTupleParameterByName = 1 << 18, CacheCommand = 1 << 19, IncludeLocation = 1 << 20, // include -- SomeFile.cs#40 when possible + UnknownParameters = 1 << 21, + QueryMultiple = 1 << 22, + NotAotSupported = 1 << 23, } diff --git a/src/Dapper.AOT.Analyzers/Internal/MemberMap.cs b/src/Dapper.AOT.Analyzers/Internal/MemberMap.cs new file mode 100644 index 00000000..38a1b227 --- /dev/null +++ b/src/Dapper.AOT.Analyzers/Internal/MemberMap.cs @@ -0,0 +1,77 @@ +using Dapper.SqlAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.SqlServer.TransactSql.ScriptDom; +using System; +using System.Collections.Immutable; +using System.Linq; +using static Dapper.Internal.Inspection; + +namespace Dapper.Internal; + +internal sealed class MemberMap +{ + public ITypeSymbol DeclaredType { get; } + public ITypeSymbol ElementType { get; } + public string? CSharpCastType { get; } + + private readonly MapFlags _flags; + + public IMethodSymbol? Constructor { get; } + public bool IsUnknownParameters => (_flags & (MapFlags.IsObject | MapFlags.IsDapperDynamic)) != 0; + + public bool IsObject => (_flags & MapFlags.IsObject) != 0; + public bool IsDapperDynamic => (_flags & MapFlags.IsDapperDynamic) != 0; + public bool IsCollection => (_flags & MapFlags.IsCollection) != 0; + + public ImmutableArray Members { get; } + + enum MapFlags + { + None = 0, + IsObject = 1 << 0, + IsDapperDynamic = 1 << 1, + IsCollection = 1 << 2, + } + + public static MemberMap? Create(ITypeSymbol? type, bool checkCollection) + => type is null ? null : new(type, checkCollection); + public MemberMap(ITypeSymbol declaredType, bool checkCollection) + { + ElementType = DeclaredType = declaredType; + if (checkCollection && IsCollectionType(declaredType, out var elementType, out var castType) + && elementType is not null) + { + ElementType = elementType; + CSharpCastType = castType; + _flags |= MapFlags.IsCollection; + } + if (IsMissingOrObjectOrDynamic(ElementType)) + { + _flags |= MapFlags.IsObject; + } + else if (IsDynamicParameters(ElementType)) + { + _flags |= MapFlags.IsDapperDynamic; + } + switch (ChooseConstructor(ElementType, out var constructor)) + { + case ConstructorResult.SuccessSingleImplicit: + case ConstructorResult.SuccessSingleExplicit: + Constructor = constructor; + break; + } + Members = GetMembers(ElementType, constructor); + } + + static bool IsDynamicParameters(ITypeSymbol? type) + { + if (type is null || type.SpecialType != SpecialType.None) return false; + if (IsBasicDapperType(type, Types.DynamicParameters) + || IsNestedSqlMapperType(type, Types.IDynamicParameters, TypeKind.Interface)) return true; + foreach (var i in type.AllInterfaces) + { + if (IsNestedSqlMapperType(i, Types.IDynamicParameters, TypeKind.Interface)) return true; + } + return false; + } +} diff --git a/src/Dapper.AOT.Analyzers/Internal/Roslyn/LiteralLocationHelper.cs b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.CSharp.cs similarity index 76% rename from src/Dapper.AOT.Analyzers/Internal/Roslyn/LiteralLocationHelper.cs rename to src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.CSharp.cs index c1ec915d..770d55e5 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Roslyn/LiteralLocationHelper.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.CSharp.cs @@ -1,24 +1,46 @@ using Dapper.SqlAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.CSharp.Syntax; using System; namespace Dapper.Internal.Roslyn; - -internal static class LiteralLocationHelper +partial class LanguageHelper { - internal static Location ComputeLocation(SyntaxToken token, in TSqlProcessor.Location location) + public static readonly LanguageHelper CSharp = new CSharpLanguageHelper(); + private sealed class CSharpLanguageHelper : LanguageHelper { - var origin = token.GetLocation(); - if (origin.SourceTree is null) return origin; // we need a tree! - try + internal override bool TryGetLiteralToken(SyntaxNode syntax, out SyntaxToken token) { - var text = token.Text; - TextSpan originSpan = token.Span; - ReadOnlySpan s; - int skip, take; + switch (syntax) + { + case LiteralExpressionSyntax direct: + token = direct.Token; + return true; + case VariableDeclaratorSyntax decl when decl.Initializer?.Value is LiteralExpressionSyntax indirect: + token = indirect.Token; + return true; + default: + token = default; + return false; + } + } + + internal override bool IsMemberAccess(SyntaxNode syntax) + => syntax is MemberAccessExpressionSyntax; + + internal override bool IsMethodDeclaration(SyntaxNode syntax) + => syntax.IsKind(SyntaxKind.MethodDeclaration); + internal override bool IsName(SyntaxNode syntax) + => syntax is SimpleNameSyntax; // NameSyntax? + + internal override string GetDisplayString(ISymbol symbol) + => symbol.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); + + internal override bool TryGetStringSpan(SyntaxToken token, string text, scoped in TSqlProcessor.Location location, out int skip, out int take) + { + ReadOnlySpan s; switch (token.Kind()) { case SyntaxKind.StringLiteralToken when token.IsVerbatimStringLiteral(): @@ -26,7 +48,7 @@ internal static Location ComputeLocation(SyntaxToken token, in TSqlProcessor.Loc skip = 2 + SkipLines(ref s, location.Line - 1); skip += SkipVerbatimStringCharacters(ref s, location.Column - 1); take = SkipVerbatimStringCharacters(ref s, location.Length); - break; + return true; case SyntaxKind.StringLiteralToken: s = text.AsSpan().Slice(1); // take off the " skip = 1; @@ -34,35 +56,27 @@ internal static Location ComputeLocation(SyntaxToken token, in TSqlProcessor.Loc // a range of other things; use Offset skip += SkipEscapedStringCharacters(ref s, (location.Line == 1 ? location.Column : location.Offset) - 1); take = SkipEscapedStringCharacters(ref s, location.Length); - break; + return true; case SyntaxKind.SingleLineRawStringLiteralToken when location.Line == 1: // no escape sequences; just need to skip the preamble """ etc skip = CountQuotes(text) + location.Column - 1; take = location.Length; - break; + return true; case SyntaxKind.MultiLineRawStringLiteralToken: var quotes = CountQuotes(text); s = text.AsSpan(); var lastLineStart = s.LastIndexOfAny('\r', '\n'); - if (lastLineStart < 0) return origin; // ???? + if (lastLineStart < 0) break; // ???? var indent = s.Length - quotes - lastLineStart - 1; // last line defines the indent skip = SkipLines(ref s, location.Line) // note we're also skipping the first one here + indent + location.Column - 1; take = location.Length; - break; - default: - return origin; - } - var finalSpan = new TextSpan(originSpan.Start + skip, take); - if (originSpan.Contains(finalSpan)) // make sure we haven't messed up the math! - { - return Location.Create(origin.SourceTree, finalSpan); + return true; } + skip = take = 0; + return false; } - catch - { } // best efforts only - return origin; } static int CountQuotes(string text) diff --git a/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.Null.cs b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.Null.cs new file mode 100644 index 00000000..796b059e --- /dev/null +++ b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.Null.cs @@ -0,0 +1,30 @@ +using Dapper.SqlAnalysis; +using Microsoft.CodeAnalysis; + +namespace Dapper.Internal.Roslyn; +partial class LanguageHelper +{ + public static readonly LanguageHelper Null = new NullLanguageHelper(); + private sealed class NullLanguageHelper : LanguageHelper + { + internal override bool TryGetLiteralToken(SyntaxNode syntax, out SyntaxToken token) + { + token = default; + return false; + } + + internal override bool IsMemberAccess(SyntaxNode syntax) => false; + + internal override bool TryGetStringSpan(SyntaxToken syntax, string text, scoped in TSqlProcessor.Location location, out int skip, out int take) + { + skip = take = 0; + return false; + } + + internal override bool IsMethodDeclaration(SyntaxNode syntax) => false; + internal override bool IsName(SyntaxNode syntax) => false; + + internal override string GetDisplayString(ISymbol symbol) + => symbol?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)!; + } +} diff --git a/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.VisualBasic.cs b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.VisualBasic.cs new file mode 100644 index 00000000..f7301eb1 --- /dev/null +++ b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.VisualBasic.cs @@ -0,0 +1,57 @@ +using Dapper.SqlAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using System; + +namespace Dapper.Internal.Roslyn; + +partial class LanguageHelper +{ + public static readonly LanguageHelper VisualBasic = new VisualBasicLanguageHelper(); + private sealed class VisualBasicLanguageHelper : LanguageHelper + { + internal override bool TryGetLiteralToken(SyntaxNode syntax, out SyntaxToken token) + { + switch (syntax) + { + case LiteralExpressionSyntax direct: + token = direct.Token; + return true; + case VariableDeclaratorSyntax decl when decl.Initializer?.Value is LiteralExpressionSyntax indirect: + token = indirect.Token; + return true; + default: + token = default; + return false; + } + } + internal override bool IsMemberAccess(SyntaxNode syntax) + => syntax is MemberAccessExpressionSyntax; + + internal override bool IsMethodDeclaration(SyntaxNode syntax) + => syntax.IsKind(SyntaxKind.DeclareSubStatement) || syntax.IsKind(SyntaxKind.DeclareFunctionStatement); + internal override bool IsName(SyntaxNode syntax) + => syntax is SimpleNameSyntax; // NameSyntax? + + internal override string GetDisplayString(ISymbol symbol) + => symbol.ToDisplayString(SymbolDisplayFormat.VisualBasicShortErrorMessageFormat); + + internal override bool TryGetStringSpan(SyntaxToken token, string text, scoped in TSqlProcessor.Location location, out int skip, out int take) + { + ReadOnlySpan s; + switch (token.Kind()) + { + // VB strings are multi-line verbatim literals + case SyntaxKind.StringLiteralToken: + s = text.AsSpan().Slice(1); // take off the " + skip = 1 + SkipLines(ref s, location.Line - 1); + skip += SkipVerbatimStringCharacters(ref s, location.Column - 1); + take = SkipVerbatimStringCharacters(ref s, location.Length); + return true; + } + skip = take = 0; + return false; + } + } +} \ No newline at end of file diff --git a/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs new file mode 100644 index 00000000..923ae5d9 --- /dev/null +++ b/src/Dapper.AOT.Analyzers/Internal/Roslyn/LanguageHelper.cs @@ -0,0 +1,99 @@ +using Dapper.SqlAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Diagnostics; + +namespace Dapper.Internal.Roslyn; + +internal static class SyntaxExtensions +{ + private static LanguageHelper GetHelper(string? language) + => language switch + { + LanguageNames.CSharp => LanguageHelper.CSharp, + LanguageNames.VisualBasic => LanguageHelper.VisualBasic, + _ => LanguageHelper.Null, + }; + + public static bool TryGetLiteralToken(this SyntaxNode? syntax, out SyntaxToken token) + => GetHelper(syntax?.Language).TryGetLiteralToken(syntax!, out token); + + public static bool IsMemberAccess(this SyntaxNode syntax) + => GetHelper(syntax.Language).IsMemberAccess(syntax); + + public static bool IsMethodDeclaration(this SyntaxNode syntax) + => GetHelper(syntax.Language).IsMethodDeclaration(syntax); + + public static Location ComputeLocation(this SyntaxToken token, scoped in TSqlProcessor.Location location) + { + var origin = token.GetLocation(); + try + { + if (origin.SourceTree is not null) + { + var text = token.Text; + TextSpan originSpan = token.Span; + if (GetHelper(token.Language).TryGetStringSpan(token, text, location, out var skip, out var take)) + { + var finalSpan = new TextSpan(originSpan.Start + skip, take); + if (originSpan.Contains(finalSpan)) // make sure we haven't messed up the math! + { + return Location.Create(origin.SourceTree, finalSpan); + } + } + } + } + catch (Exception ex)// best efforts only + { + Debug.WriteLine(ex.Message); + } + return origin; + } + + public static Location GetMemberLocation(this IInvocationOperation call) + => GetMemberSyntax(call).GetLocation(); + + public static SyntaxNode GetMemberSyntax(this IInvocationOperation call) + { + var syntax = call?.Syntax; + if (syntax is null) return null!; // GIGO + + var helper = GetHelper(syntax.Language); + foreach (var outer in syntax.ChildNodesAndTokens()) + { + var outerNode = outer.AsNode(); + if (outerNode is not null && helper.IsMemberAccess(outerNode)) + { + // if there is an identifier, we want the **last** one - think Foo.Bar.Blap(...) + SyntaxNode? identifier = null; + foreach (var inner in outerNode.ChildNodesAndTokens()) + { + var innerNode = inner.AsNode(); + if (innerNode is not null && helper.IsName(innerNode)) + identifier = innerNode; + } + // we'd prefer an identifier, but we'll allow the entire member-access + return identifier ?? outerNode; + } + } + return syntax; + } + + public static string GetSignature(this IInvocationOperation call) + => GetHelper(call?.Language).GetDisplayString(call?.TargetMethod!); + + public static string GetDisplayString(this ISymbol symbol) + => GetHelper(symbol.Language).GetDisplayString(symbol); + +} +internal abstract partial class LanguageHelper +{ + internal abstract bool IsMemberAccess(SyntaxNode syntax); + internal abstract bool IsMethodDeclaration(SyntaxNode syntax); + 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); + internal abstract string GetDisplayString(ISymbol method); +} \ No newline at end of file diff --git a/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs b/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs index 0da0a82d..9620998e 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs @@ -6,18 +6,6 @@ namespace Dapper.Internal.Roslyn; internal static class TypeSymbolExtensions { - public static bool TryGetConstructors(this ITypeSymbol? typeSymbol, out ImmutableArray? constructors) - { - constructors = null; - if (typeSymbol is not INamedTypeSymbol namedTypeSymbol) - { - return false; - } - - constructors = namedTypeSymbol.Constructors; - return true; - } - public static string? GetTypeDisplayName(this ITypeSymbol? typeSymbol) { if (typeSymbol is null) return null; @@ -167,7 +155,7 @@ private static bool IsStandardCollection(ITypeSymbol? type, string name, string /// if found, an interface type symbol /// /// Most likely is one of the last defined interfaces in a chain of implementations - /// https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.itypesymbol.allinterfaces?view=roslyn-dotnet + /// https://learn.microsoft.com/dotnet/api/microsoft.codeanalysis.itypesymbol.allinterfaces?view=roslyn-dotnet /// public static bool ImplementsIEnumerable(this ITypeSymbol? typeSymbol, out ITypeSymbol? searchedTypeSymbol) => typeSymbol.ImplementsInterface(SpecialType.System_Collections_Generic_IEnumerable_T, out searchedTypeSymbol, searchFromStart: false); diff --git a/src/Dapper.AOT.Analyzers/Internal/Shims.cs b/src/Dapper.AOT.Analyzers/Internal/Shims.cs new file mode 100644 index 00000000..609dc45b --- /dev/null +++ b/src/Dapper.AOT.Analyzers/Internal/Shims.cs @@ -0,0 +1,6 @@ +namespace System.Diagnostics.CodeAnalysis +{ +internal sealed class NotNullAttribute : Attribute + { + } +} diff --git a/src/Dapper.AOT.Analyzers/Internal/SqlTools.cs b/src/Dapper.AOT.Analyzers/Internal/SqlTools.cs index 80224f70..6f9ea111 100644 --- a/src/Dapper.AOT.Analyzers/Internal/SqlTools.cs +++ b/src/Dapper.AOT.Analyzers/Internal/SqlTools.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Immutable; using System.Text.RegularExpressions; +using Dapper.SqlAnalysis; namespace Dapper.Internal; @@ -16,8 +17,8 @@ internal static class SqlTools internal static readonly Regex LiteralTokens = new(@"(? GetUniqueParameters(string? sql, out ParseFlags flags) - => ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, GetParameters(sql, out flags)); + public static ImmutableHashSet GetUniqueParameters(string? sql) + => ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, GetParameters(sql)); public static bool IncludeParameter(string map, string name, out bool test) { @@ -52,19 +53,13 @@ public static bool IncludeParameter(string map, string name, out bool test) } - public static string[] GetParameters(string? sql, out ParseFlags flags) + public static string[] GetParameters(string? sql) { - flags = ParseFlags.None; if (string.IsNullOrWhiteSpace(sql)) { return Array.Empty(); } - if (sql!.IndexOf("return", StringComparison.InvariantCultureIgnoreCase) >= 0) - { - flags |= ParseFlags.Return; - } - if (!ParameterRegex.IsMatch(sql)) { return Array.Empty(); diff --git a/src/Dapper.AOT.Analyzers/Internal/Types.cs b/src/Dapper.AOT.Analyzers/Internal/Types.cs index 81d82bdc..4d09dc8e 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Types.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Types.cs @@ -14,5 +14,7 @@ public const string CommandPropertyAttribute = nameof(CommandPropertyAttribute), DynamicParameters = nameof(DynamicParameters), IDynamicParameters = nameof(IDynamicParameters), - SqlMapper = nameof(SqlMapper); + SqlMapper = nameof(SqlMapper), + SqlAttribute = nameof(SqlAttribute), + ExplicitConstructorAttribute = nameof(ExplicitConstructorAttribute); } diff --git a/src/Dapper.AOT.Analyzers/ParseFlags.cs b/src/Dapper.AOT.Analyzers/ParseFlags.cs deleted file mode 100644 index 3e2401ae..00000000 --- a/src/Dapper.AOT.Analyzers/ParseFlags.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Dapper; - -[Flags] -internal enum ParseFlags -{ - None = 0, - Reliable = 1 << 0, - SyntaxError = 1 << 1, - Return = 1 << 2, - Query = 1 << 3, - Queries = 1 << 4, - MaybeQuery = 1 << 5, // think "exec": we don't know! - DynamicParameters = 1 << 6, -} diff --git a/src/Dapper.AOT.Analyzers/SqlAnalysis/SqlParseOutputFlags.cs b/src/Dapper.AOT.Analyzers/SqlAnalysis/SqlParseOutputFlags.cs new file mode 100644 index 00000000..645c241a --- /dev/null +++ b/src/Dapper.AOT.Analyzers/SqlAnalysis/SqlParseOutputFlags.cs @@ -0,0 +1,34 @@ +using System; + +namespace Dapper.SqlAnalysis; + +[Flags] +internal enum SqlParseOutputFlags +{ + None = 0, + Reliable = 1 << 0, + SyntaxError = 1 << 1, + Return = 1 << 2, + Query = 1 << 3, + Queries = 1 << 4, + MaybeQuery = 1 << 5, // think "exec": we don't know! + SqlAdjustedForDapperSyntax = 1 << 6, // for example '{=x}' => ' @x ' + KnownParameters = 1 << 7, +} + + +[Flags] +internal enum SqlParseInputFlags +{ + None = 0, + CaseSensitive = 1 << 0, + ValidateSelectNames = 1 << 1, + SingleRow = 1 << 2, + AtMostOne = 1 << 3, + ExpectQuery = 1 << 4, // active *DO* expect a query + ExpectNoQuery = 1 << 5, // actively do *NOT* expect a query + SingleQuery = 1 << 6, // actively expect *exactly* one query + SingleColumn = 1 << 7, // actively expect *exactly* one column in any query + KnownParameters = 1 << 8, // we understand the parameters + DebugMode = 1 << 9, +} diff --git a/src/Dapper.AOT.Analyzers/SqlAnalysis/SqlSyntax.cs b/src/Dapper.AOT.Analyzers/SqlAnalysis/SqlSyntax.cs new file mode 100644 index 00000000..a968b182 --- /dev/null +++ b/src/Dapper.AOT.Analyzers/SqlAnalysis/SqlSyntax.cs @@ -0,0 +1,53 @@ +namespace Dapper.SqlAnalysis +{ + // THIS FILE MUST BE KEPT IN SYNC BETWEEN THE LIB AND ANALYZERS + + /// + /// Indicates the family of SQL variant used + /// + public enum SqlSyntax + { + // RESERVED: 0 + + /// + /// General purpose SQL; handles most common variants, but with low fidelity; multiple parameter + /// conventions may work, including @value and :value + /// + General = 1, + + /// + /// General purpose SQL, using the @value parameter convention + /// + GeneralWithAtParameters = 2, + + /// + /// General purpose SQL, using the :value parameter convention + /// + GeneralWithColonParameters = 3, + + /// + /// SQL Server (Transact-SQL)using the @value parameter convention; has full syntax processing support + /// + SqlServer = 100, + + /// + /// PostgreSQL, using the :value parameter convention + /// + PostgreSql = 200, + + /// + /// MySQL and MariaDB, using the @value parameter convention + /// + MySql = 300, + + /// + /// Oracle, using the :value parameter convention + /// + Oracle = 400, + + /// + /// SQLite, using the @value parameter convention + /// + SQLite = 500, + } +} diff --git a/src/Dapper.AOT.Analyzers/SqlAnalysis/TSqlProcessor.cs b/src/Dapper.AOT.Analyzers/SqlAnalysis/TSqlProcessor.cs index 455ad447..fa234d80 100644 --- a/src/Dapper.AOT.Analyzers/SqlAnalysis/TSqlProcessor.cs +++ b/src/Dapper.AOT.Analyzers/SqlAnalysis/TSqlProcessor.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Data; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -15,15 +16,7 @@ namespace Dapper.SqlAnalysis; internal class TSqlProcessor { - [Flags] - internal enum ModeFlags - { - None = 0, - CaseSensitive = 1 << 0, - ValidateSelectNames = 1 << 1, - SingleRow = 1 << 2, - AtMostOne = 1 << 3, - } + [Flags] internal enum VariableFlags { @@ -38,12 +31,12 @@ internal enum VariableFlags protected bool CaseSensitive => _visitor.CaseSensitive; protected bool ValidateSelectNames => _visitor.ValidateSelectNames; - public TSqlProcessor(ModeFlags flags = ModeFlags.None, Action? log = null) + public TSqlProcessor(SqlParseInputFlags flags = SqlParseInputFlags.None, Action? log = null) { _visitor = log is null ? new VariableTrackingVisitor(flags, this) : new LoggingVariableTrackingVisitor(flags, this, log); } - static string HackAroundDapperSyntax(string sql, ImmutableArray members) + static string ReplaceDapperSyntaxWithValidSql(string sql, ImmutableArray members) { if (SqlTools.LiteralTokens.IsMatch(sql)) { @@ -86,24 +79,45 @@ public virtual bool Execute(string sql, ImmutableArray members = members = ImmutableArray.Empty; } Reset(); - sql = HackAroundDapperSyntax(sql, members); + var fixedSql = ReplaceDapperSyntaxWithValidSql(sql, members); + if (fixedSql != sql) + { + Flags |= SqlParseOutputFlags.SqlAdjustedForDapperSyntax; + } var parser = new TSql160Parser(true, SqlEngineType.All); TSqlFragment tree; - using (var reader = new StringReader(sql)) + using (var reader = new StringReader(fixedSql)) { tree = parser.Parse(reader, out var errors); if (errors is not null && errors.Count != 0) { - Flags |= ParseFlags.SyntaxError; + Flags |= SqlParseOutputFlags.SyntaxError; foreach (var error in errors) { OnParseError(error, new Location(error.Line, error.Column, error.Offset, 0)); } } + else + { + Flags |= SqlParseOutputFlags.Reliable; + } } + tree.Accept(_visitor); foreach (var variable in _visitor.Variables) { + if (_visitor.KnownParameters) + { + if (variable.IsParameter && TryGetParameter(variable.Name, out var direction) && direction != ParameterDirection.ReturnValue) + { + // fine, handled + } + else + { + // no such parameter? then: it wasn't a parameter, and wasn't declared + OnVariableNotDeclared(variable); + } + } if (variable.IsUnconsumed && !variable.IsTable && !variable.IsOutputParameter) { if (_visitor.AssignmentTracking) OnVariableValueNotConsumed(variable); @@ -114,11 +128,13 @@ public virtual bool Execute(string sql, ImmutableArray members = public IEnumerable Variables => _visitor.Variables; - public ParseFlags Flags { get; private set; } - protected void AddFlags(ParseFlags flags) => Flags |= flags; + public SqlParseOutputFlags Flags { get; private set; } public virtual void Reset() { - Flags = ParseFlags.Reliable; + var flags = SqlParseOutputFlags.None; + if (_visitor.KnownParameters) flags |= SqlParseOutputFlags.KnownParameters; + Flags = flags; + _visitor.Reset(); } @@ -151,6 +167,13 @@ protected virtual void OnTableVariableUsedAsScalar(Variable variable) protected virtual void OnNullLiteralComparison(Location location) => OnError($"Null literals should not be used in binary comparisons; prefer 'is null' and 'is not null'", location); + private void OnSimplifyExpression(Location location, int? value) + => OnSimplifyExpression(location, value is null ? "null" : value.Value.ToString(CultureInfo.InvariantCulture)); + private void OnSimplifyExpression(Location location, decimal? value) + => OnSimplifyExpression(location, value is null ? "null" : value.Value.ToString(CultureInfo.InvariantCulture)); + protected virtual void OnSimplifyExpression(Location location, string value) + => OnError($"Expression can be simplified to '{value}'", location); + protected virtual void OnAdditionalBatch(Location location) => OnError($"Multiple batches are not permitted", location); @@ -160,7 +183,7 @@ protected virtual void OnGlobalIdentity(Location location) protected virtual void OnSelectScopeIdentity(Location location) => OnError($"Consider OUTPUT INSERTED.yourid on the INSERT instead of SELECT SCOPE_IDENTITY()", location); - protected virtual void OnExecVariable(Location location) + protected virtual void OnExecComposedSql(Location location) => OnError($"EXEC with composed SQL may be susceptible to SQL injection; consider EXEC sp_executesql with parameters", location); protected virtual void OnTableVariableOutputParameter(Variable variable) @@ -196,17 +219,24 @@ protected virtual void OnSelectSingleRowWithoutWhere(Location location) => OnError($"SELECT for single row without WHERE or (TOP and ORDER BY)", location); protected virtual void OnNonPositiveTop(Location location) => OnError($"TOP literals should be positive", location); + protected virtual void OnNonPositiveFetch(Location location) + => OnError($"FETCH literals should be positive", location); + protected virtual void OnNegativeOffset(Location location) + => OnError($"OFFSET literals should be non-negative", location); protected virtual void OnNonIntegerTop(Location location) => OnError($"TOP literals should be integers", location); protected virtual void OnFromMultiTableMissingAlias(Location location) => OnError($"FROM expressions with multiple elements should use aliases", location); protected virtual void OnFromMultiTableUnqualifiedColumn(Location location, string name) => OnError($"FROM expressions with multiple elements should qualify all columns; it is unclear where '{name}' is located", location); + protected virtual void OnTopWithOffset(Location location) + => OnError($"TOP cannot be used when OFFSET is specified", location); internal readonly struct Location { - public Location(TSqlFragment source) : this() + public static implicit operator Location(TSqlFragment source) => new(source); + private Location(TSqlFragment source) : this() { Line = source.StartLine; Column = source.StartColumn; @@ -245,13 +275,13 @@ public Variable(Identifier identifier, VariableFlags flags) { Flags = flags; Name = identifier.Value; - Location = new Location(identifier); + Location = identifier; } public Variable(VariableReference reference, VariableFlags flags) { Flags = flags; Name = reference.Name; - Location = new Location(reference); + Location = reference; } private Variable(scoped in Variable source, VariableFlags flags) @@ -269,12 +299,12 @@ private Variable(scoped in Variable source, Location location) public Variable WithUnconsumedValue() => new(in this, (Flags & ~VariableFlags.NoValue) | VariableFlags.Unconsumed); public Variable WithFlags(VariableFlags flags) => new(in this, flags); - public Variable WithLocation(TSqlFragment node) => new(in this, new Location(node)); + public Variable WithLocation(TSqlFragment node) => new(in this, node); } class LoggingVariableTrackingVisitor : VariableTrackingVisitor { private readonly Action log; - public LoggingVariableTrackingVisitor(ModeFlags flags, TSqlProcessor parser, Action log) : base(flags, parser) + public LoggingVariableTrackingVisitor(SqlParseInputFlags flags, TSqlProcessor parser, Action log) : base(flags, parser) { this.log = log; } @@ -336,18 +366,19 @@ class VariableTrackingVisitor : TSqlFragmentVisitor // call base.ExplicitVisit; VariableTableReference is an example, omitting node.Variable?.Accept(this) // to avoid a problem; also, be sure to think "nulls", so: ?.Accept(this), if you're not sure - private readonly ModeFlags _flags; - public VariableTrackingVisitor(ModeFlags flags, TSqlProcessor parser) + private readonly SqlParseInputFlags _flags; + public VariableTrackingVisitor(SqlParseInputFlags flags, TSqlProcessor parser) { _flags = flags; variables = CaseSensitive ? new(StringComparer.Ordinal) : new(StringComparer.OrdinalIgnoreCase); this.parser = parser; } - public bool CaseSensitive => (_flags & ModeFlags.CaseSensitive) != 0; - public bool ValidateSelectNames => (_flags & ModeFlags.ValidateSelectNames) != 0; - public bool SingleRow => (_flags & ModeFlags.SingleRow) != 0; - public bool AtMostOne => (_flags & ModeFlags.AtMostOne) != 0; + public bool CaseSensitive => (_flags & SqlParseInputFlags.CaseSensitive) != 0; + public bool ValidateSelectNames => (_flags & SqlParseInputFlags.ValidateSelectNames) != 0; + public bool SingleRow => (_flags & SqlParseInputFlags.SingleRow) != 0; + public bool AtMostOne => (_flags & SqlParseInputFlags.AtMostOne) != 0; + public bool KnownParameters => (_flags & SqlParseInputFlags.KnownParameters) != 0; private readonly Dictionary variables; private int batchCount; @@ -367,7 +398,7 @@ public override void Visit(TSqlBatch node) { if (++batchCount >= 2) { - parser.OnAdditionalBatch(new Location(node)); + parser.OnAdditionalBatch(node); } base.Visit(node); } @@ -442,7 +473,7 @@ public override void ExplicitVisit(OutputIntoClause node) public override void Visit(ReturnStatement node) { - parser.Flags |= ParseFlags.Return; + parser.Flags |= SqlParseOutputFlags.Return; base.Visit(node); } @@ -467,18 +498,29 @@ public override void Visit(BooleanComparisonExpression node) { if (node.FirstExpression is NullLiteral) { - parser.OnNullLiteralComparison(new Location(node.FirstExpression)); + parser.OnNullLiteralComparison(node.FirstExpression); } if (node.SecondExpression is NullLiteral) { - parser.OnNullLiteralComparison(new Location(node.SecondExpression)); + parser.OnNullLiteralComparison(node.SecondExpression); } base.Visit(node); } public override void ExplicitVisit(QuerySpecification node) { - base.ExplicitVisit(node); + var oldDemandAlias = _demandAliases; + try + { + // set ambient state so we can complain more as we walk the nodes + _demandAliases = IsMultiTable(node.FromClause) + ? new(true, null) : default; + base.ExplicitVisit(node); + } + finally + { + _demandAliases = oldDemandAlias; + } foreach (var el in node.SelectElements) { if (el is SelectSetVariable setVar && setVar.Variable is not null) @@ -522,23 +564,33 @@ public override void Visit(DeclareTableVariableBody node) public override void Visit(ExecuteStatement node) { - parser.Flags |= ParseFlags.MaybeQuery; + parser.Flags |= SqlParseOutputFlags.MaybeQuery; base.Visit(node); } private bool AddQuery() // returns true if the first { - switch (parser.Flags & (ParseFlags.Query | ParseFlags.Queries)) + switch (parser.Flags & (SqlParseOutputFlags.Query | SqlParseOutputFlags.Queries)) { - case ParseFlags.None: - parser.Flags |= ParseFlags.Query; + case SqlParseOutputFlags.None: + parser.Flags |= SqlParseOutputFlags.Query; return true; - case ParseFlags.Query: - parser.Flags |= ParseFlags.Queries; + case SqlParseOutputFlags.Query: + parser.Flags |= SqlParseOutputFlags.Queries; break; } return false; } + + public override void Visit(QuerySpecification node) + { + if (node.TopRowFilter is not null && node.OffsetClause is not null) + { + parser.OnTopWithOffset(node.TopRowFilter); + } + base.Visit(node); + } + public override void Visit(SelectStatement node) { if (node.QueryExpression is QuerySpecification spec) @@ -548,7 +600,7 @@ public override void Visit(SelectStatement node) && spec.SelectElements[0] is SelectScalarExpression { Expression: FunctionCall func } && string.Equals(func.FunctionName?.Value, "SCOPE_IDENTITY", StringComparison.OrdinalIgnoreCase)) { - parser.OnSelectScopeIdentity(new Location(func)); + parser.OnSelectScopeIdentity(func); } int sets = 0, reads = 0; var checkNames = ValidateSelectNames; @@ -560,7 +612,7 @@ public override void Visit(SelectStatement node) switch (el) { case SelectStarExpression: - parser.OnSelectStar(new Location(el)); + parser.OnSelectStar(el); reads++; break; case SelectScalarExpression scalar: @@ -577,11 +629,11 @@ public override void Visit(SelectStatement node) } if (string.IsNullOrWhiteSpace(name)) { - parser.OnSelectEmptyColumnName(new(scalar), index); + parser.OnSelectEmptyColumnName(scalar, index); } else if (!names.Add(name!)) { - parser.OnSelectDuplicateColumnName(new(scalar), name!); + parser.OnSelectDuplicateColumnName(scalar, name!); } } reads++; @@ -596,70 +648,329 @@ public override void Visit(SelectStatement node) { if (sets != 0) { - parser.OnSelectAssignAndRead(new(spec)); + parser.OnSelectAssignAndRead(spec); } bool firstQuery = AddQuery(); - if (firstQuery && SingleRow) + if (firstQuery && SingleRow // optionally enforce single-row validation + && spec.FromClause is not null) // no "from" is things like 'select @id, @name' - always one row { - bool haveTop = false; - if (spec.TopRowFilter is { Percent: false, Expression: IntegerLiteral top } - && ParseInt32(top, out int i)) + bool haveTopOrFetch = false; + if (spec.TopRowFilter is { Percent: false, Expression: ScalarExpression top }) { - haveTop = true; - if (AtMostOne) // Single[OrDefault][Async] - { - if (i != 2) - { - parser.OnSelectSingleTopError(new(node)); - } - } - else // First[OrDefault][Async] - { - if (i != 1) - { - parser.OnSelectFirstTopError(new(node)); - } - } + haveTopOrFetch = EnforceTop(top); + } + else if (spec.OffsetClause is { FetchExpression: ScalarExpression fetch }) + { + haveTopOrFetch = EnforceTop(fetch); } // we want *either* a WHERE (which we will allow with/without a TOP), // or a TOP + ORDER BY if (!IsUnfiltered(spec.FromClause, spec.WhereClause)) { } // fine - else if (haveTop && spec.OrderByClause is not null) { } // fine + else if (haveTopOrFetch && spec.OrderByClause is not null) { } // fine else { - parser.OnSelectSingleRowWithoutWhere(new(node)); + parser.OnSelectSingleRowWithoutWhere(node); } } - } + } + base.Visit(node); + } + + private bool EnforceTop(ScalarExpression expr) + { + if (IsInt32(expr, out var i, complex: true) && i.HasValue) + { + if (AtMostOne) // Single[OrDefault][Async] + { + if (i.Value != 2) + { + parser.OnSelectSingleTopError(expr); + } + } + else // First[OrDefault][Async] + { + if (i.Value != 1) + { + parser.OnSelectFirstTopError(expr); + } + } + return true; } + return false; + } + public override void Visit(OffsetClause node) + { + if (IsInt32(node.FetchExpression, out var i, true) && i <= 0) + { + parser.OnNonPositiveFetch(node.FetchExpression); + } + if (IsInt32(node.OffsetExpression, out i, true) && i < 0) + { + parser.OnNegativeOffset(node.OffsetExpression); + } base.Visit(node); } - static bool ParseInt32(IntegerLiteral node, out int value) => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out value); - static bool ParseNumeric(NumericLiteral node, out decimal value) => decimal.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out value); + static bool IsInt32(ScalarExpression scalar, out int? value, bool complex) + { + try + { + checked + { + switch (scalar) + { + case NullLiteral: + value = null; + return true; + case IntegerLiteral integer when int.TryParse(integer.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i): + value = i; + return true; + case UnaryExpression unary when IsInt32(unary.Expression, out value, complex): + switch (unary.UnaryExpressionType) + { + case UnaryExpressionType.Negative: // note -ve is always allowed, even when not complex + value = -value; + return true; + case UnaryExpressionType.Positive when complex: + return true; + case UnaryExpressionType.BitwiseNot when complex: + value = ~value; + return true; + } + break; + case BinaryExpression binary when complex: + var haveFirst = IsInt32(binary.FirstExpression, out var first, true); + var haveSecond = IsInt32(binary.SecondExpression, out var second, true); + + // if either half is *known* to be null; we're good + if ((haveFirst && first is null) || (haveSecond && second is null)) + { + switch (binary.BinaryExpressionType) + { + case BinaryExpressionType.Add: + case BinaryExpressionType.Subtract: + case BinaryExpressionType.Divide: + case BinaryExpressionType.Multiply: + case BinaryExpressionType.Modulo: + case BinaryExpressionType.BitwiseXor: + case BinaryExpressionType.BitwiseOr: + case BinaryExpressionType.BitwiseAnd: + value = null; + return true; + } + break; + } + + // otherwise, need both + if (!(haveFirst && haveSecond)) + { + break; + } + + switch (binary.BinaryExpressionType) + { + case BinaryExpressionType.Add: + value = first + second; + return true; + case BinaryExpressionType.Subtract: + value = first - second; + return true; + case BinaryExpressionType.Divide: + value = first / second; // TSQL is integer division + return true; + case BinaryExpressionType.Multiply: + value = first * second; + return true; + case BinaryExpressionType.Modulo: + value = first % second; + return true; + case BinaryExpressionType.BitwiseXor: + value = first ^ second; + return true; + case BinaryExpressionType.BitwiseOr: + value = first | second; + return true; + case BinaryExpressionType.BitwiseAnd: + value = first & second; + return true; + case BinaryExpressionType.LeftShift: + if (first is null || second is null) break; // null not allowed here + if (second < 0) + { + second = -second; // TSQL: neg-shift allowed + goto case BinaryExpressionType.RightShift; + } + if (second >= 32) // c# shift masks "by" to 5 bits + { + value = 0; + return true; + } + value = first << second; + return true; + case BinaryExpressionType.RightShift: + if (first is null || second is null) break; // null not allowed here + if (second < 0) + { + second = -second; // TSQL: neg-shift allowed + goto case BinaryExpressionType.LeftShift; + } + if (second >= 32) // c# shift masks "by" to 5 bits + { + value = 0; + return true; + } + value = first >>> second; // TSQL right-shift is unsigned + return true; + } + break; + } + } + } + catch { } // overflow etc + value = default; + return false; + } + + static bool IsDecimal(ScalarExpression scalar, out decimal? value, bool complex = false) + { + try + { + checked + { + switch (scalar) + { + case NullLiteral: + value = null; + return true; + case IntegerLiteral integer when int.TryParse(integer.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i): + value = i; + return true; + case NumericLiteral number when decimal.TryParse(number.Value, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var d): + value = d; + return true; + case UnaryExpression unary when IsDecimal(unary.Expression, out value, complex): + switch (unary.UnaryExpressionType) + { + case UnaryExpressionType.Negative: // note -ve is always allowed, even when not complex + value = -value; + return true; + case UnaryExpressionType.Positive when complex: + return true; + } + break; + case BinaryExpression binary when complex: + var haveFirst = IsDecimal(binary.FirstExpression, out var first, true); + var haveSecond = IsDecimal(binary.SecondExpression, out var second, true); + + // if either half is *known* to be null; we're good + if ((haveFirst && first is null) || (haveSecond && second is null)) + { + switch (binary.BinaryExpressionType) + { + case BinaryExpressionType.Add: + case BinaryExpressionType.Subtract: + case BinaryExpressionType.Divide: + case BinaryExpressionType.Multiply: + case BinaryExpressionType.Modulo: + value = null; + return true; + } + break; + } + + // otherwise, need both + if (!(haveFirst && haveSecond)) + { + break; + } + + switch (binary.BinaryExpressionType) + { + case BinaryExpressionType.Add: + value = first + second; + return true; + case BinaryExpressionType.Subtract: + value = first - second; + return true; + case BinaryExpressionType.Divide: + value = first / second; // TSQL is integer division + return true; + case BinaryExpressionType.Multiply: + value = first * second; + return true; + case BinaryExpressionType.Modulo: + value = first % second; + return true; + } + break; + } + } + } + catch { } // overflow etc + value = default; + return false; + } + public override void Visit(TopRowFilter node) { - if (node.Expression is IntegerLiteral iLit && ParseInt32(iLit, out var i) && i <= 0) + if (node.Expression is ScalarExpression scalar) { - parser.OnNonPositiveTop(new(node)); + if (IsInt32(scalar, out var i, true)) + { + if (i <= 0) + { + parser.OnNonPositiveTop(scalar); + } + } + else if (IsDecimal(scalar, out var d, true)) + { + if (!node.Percent) + { + parser.OnNonIntegerTop(scalar); + } + if (d <= 0) + { // don't expect to see this; parser rejects them + parser.OnNonPositiveTop(scalar); + } + } } - else if (node.Expression is NumericLiteral nLit) + base.Visit(node); + } + + public override void Visit(UnaryExpression node) + { + if (node.UnaryExpressionType != UnaryExpressionType.Negative) // need to allow unary { - if (!node.Percent) + // if operand is simple, compute and report + if (IsInt32(node.Expression, out _, complex: false)) { - parser.OnNonIntegerTop(new(node)); + if (IsInt32(node, out var value, complex: true)) parser.OnSimplifyExpression(node, value); } - if (ParseNumeric(nLit, out var n) && n <= 0) - { // don't expect to see this; parser rejects them - parser.OnNonPositiveTop(new(node)); + else if (IsDecimal(node.Expression, out _, complex: false)) + { + if (IsDecimal(node, out var value, complex: true)) parser.OnSimplifyExpression(node, value); } } base.Visit(node); } + public override void Visit(BinaryExpression node) + { + // if operands are simple, compute and report + bool haveNull = node.FirstExpression is NullLiteral || node.SecondExpression is NullLiteral; + if (haveNull || (IsInt32(node.FirstExpression, out _, complex: false) && IsInt32(node.SecondExpression, out _, complex: false))) + { + if (IsInt32(node, out var value, complex: true)) parser.OnSimplifyExpression(node, value); + } + else if (haveNull || (IsDecimal(node.FirstExpression, out _, complex: false) && IsDecimal(node.SecondExpression, out _, complex: false))) + { + if (IsDecimal(node, out var value, complex: true)) parser.OnSimplifyExpression(node, value); + } + base.Visit(node); + } public override void Visit(OutputClause node) { @@ -680,76 +991,87 @@ private bool IsMultiTable(FromClause? from) return tables.Count > 1 || tables[0] is JoinTableReference; } - public override void ExplicitVisit(SelectStatement node) + public override void ExplicitVisit(UpdateSpecification node) { - bool oldDemandAlias = _demandAliases; + Debug.Assert(!_demandAliases.Active); + var oldDemandAlias = _demandAliases; try { // set ambient state so we can complain more as we walk the nodes - _demandAliases = IsMultiTable((node.QueryExpression as QuerySpecification)?.FromClause); + _demandAliases = IsMultiTable(node.FromClause) ? new(true, node.Target) : default; base.ExplicitVisit(node); + if (_demandAliases.Active && !_demandAliases.AmnestyNodeIsAlias) + parser.OnFromMultiTableMissingAlias(node.Target); } finally { _demandAliases = oldDemandAlias; } } - public override void ExplicitVisit(UpdateStatement node) + public override void ExplicitVisit(DeleteSpecification node) { - bool oldDemandAlias = _demandAliases; + Debug.Assert(!_demandAliases.Active); + var oldDemandAlias = _demandAliases; try { // set ambient state so we can complain more as we walk the nodes - _demandAliases = IsMultiTable(node.UpdateSpecification.FromClause); + _demandAliases = IsMultiTable(node.FromClause) ? new(true, node.Target) : default; base.ExplicitVisit(node); + if (_demandAliases.Active && !_demandAliases.AmnestyNodeIsAlias) + parser.OnFromMultiTableMissingAlias(node.Target); } finally { _demandAliases = oldDemandAlias; } } - public override void ExplicitVisit(DeleteStatement node) - { - bool oldDemandAlias = _demandAliases; - try - { - // set ambient state so we can complain more as we walk the nodes - _demandAliases = IsMultiTable(node.DeleteSpecification.FromClause); - base.ExplicitVisit(node); - } - finally - { - _demandAliases = oldDemandAlias; - } - } - private bool _demandAliases; - public override void ExplicitVisit(FromClause node) + + private struct DemandAliasesState { - bool oldDemandAlias = _demandAliases; - try - { - // set ambient state so we can complain more as we walk the nodes - _demandAliases = IsMultiTable(node); - base.ExplicitVisit(node); - } - finally + public DemandAliasesState(bool active, TableReference? amnesty) { - _demandAliases = oldDemandAlias; + Active = active; + Amnesty = amnesty; + AmnestyNodeIsAlias = false; + HuntingAlias = null; + if (amnesty is NamedTableReference { Alias: null } named && named.SchemaObject is { Count: 1 } schema) + { + HuntingAlias = schema[0].Value; + } } + public readonly string? HuntingAlias; + public readonly bool Active; + public readonly TableReference? Amnesty; // we can't validate the target until too late + public bool AmnestyNodeIsAlias; } + + private DemandAliasesState _demandAliases; + public override void Visit(TableReferenceWithAlias node) { - if (_demandAliases && string.IsNullOrWhiteSpace(node.Alias?.Value)) + if (_demandAliases.Active) { - parser.OnFromMultiTableMissingAlias(new(node)); + if (ReferenceEquals(node, _demandAliases.Amnesty)) + { + // ignore for now + } + else if (string.IsNullOrWhiteSpace(node.Alias?.Value)) + { + parser.OnFromMultiTableMissingAlias(node); + } + else if (node.Alias!.Value == _demandAliases.HuntingAlias) + { + // we've resolved the Target node as an alias + _demandAliases.AmnestyNodeIsAlias = true; + } } base.Visit(node); } public override void Visit(ColumnReferenceExpression node) { - if (_demandAliases && node.MultiPartIdentifier.Count == 1) + if (_demandAliases.Active && node.MultiPartIdentifier.Count == 1) { - parser.OnFromMultiTableUnqualifiedColumn(new(node), node.MultiPartIdentifier[0].Value); + parser.OnFromMultiTableUnqualifiedColumn(node, node.MultiPartIdentifier[0].Value); } base.Visit(node); } @@ -758,7 +1080,7 @@ public override void Visit(DeleteSpecification node) { if (IsUnfiltered(node.FromClause, node.WhereClause)) { - parser.OnDeleteWithoutWhere(new(node)); + parser.OnDeleteWithoutWhere(node); } base.Visit(node); } @@ -767,7 +1089,7 @@ public override void Visit(UpdateSpecification node) { if (IsUnfiltered(node.FromClause, node.WhereClause)) { - parser.OnUpdateWithoutWhere(new(node)); + parser.OnUpdateWithoutWhere(node); } base.Visit(node); } @@ -780,10 +1102,16 @@ public override void ExplicitVisit(ExecuteSpecification node) if (node.ExecutableEntity is not null) { node.ExecutableEntity.Accept(this); - if (node.ExecutableEntity is ExecutableStringList list && list.Strings.Count == 1 - && list.Strings[0] is VariableReference) + if (node.ExecutableEntity is ExecutableStringList list) { - parser.OnExecVariable(new Location(node)); + if (list.Strings.Count == 0) + { } // ?? + else if (list.Strings.Count == 1 && list.Strings[0] is StringLiteral) + { } // we'll let them off + else + { + parser.OnExecComposedSql(node); + } } } if (node.Variable is not null) @@ -845,7 +1173,7 @@ public override void Visit(GlobalVariableExpression node) { if (string.Equals(node.Name, "@@IDENTITY", StringComparison.OrdinalIgnoreCase)) { - parser.OnGlobalIdentity(new Location(node)); + parser.OnGlobalIdentity(node); } base.Visit(node); } @@ -855,15 +1183,15 @@ public override void Visit(InsertSpecification node) var knownInsertedCount = TryCount(node.InsertSource, out var count, out bool unbalanced); if (unbalanced) { - parser.OnInsertColumnsUnbalanced(new Location(node.InsertSource)); + parser.OnInsertColumnsUnbalanced(node.InsertSource); } if (node.Columns.Count == 0) { - parser.OnInsertColumnsNotSpecified(new Location(node)); + parser.OnInsertColumnsNotSpecified(node); } else if (knownInsertedCount && count != node.Columns.Count) { - parser.OnInsertColumnMismatch(new Location(node.InsertSource)); + parser.OnInsertColumnMismatch(node.InsertSource); } base.Visit(node); @@ -939,8 +1267,10 @@ private void EnsureOrMarkAssigned(VariableReference node, bool isTable, bool mar { var flags = VariableFlags.Parameter | (isTable ? VariableFlags.Table : VariableFlags.None); - if (parser.TryGetParameter(node.Name, out var direction) && direction != ParameterDirection.ReturnValue) + ParameterDirection direction; + if (KnownParameters && parser.TryGetParameter(node.Name, out direction) && direction != ParameterDirection.ReturnValue) { + // if it is known, we can infer the directionality and thus assignment state switch (direction) { case ParameterDirection.Output: @@ -948,31 +1278,32 @@ private void EnsureOrMarkAssigned(VariableReference node, bool isTable, bool mar flags |= VariableFlags.OutputParameter; break; } + } + else + { + direction = ParameterDirection.Input; + } - if (mark && !isTable) flags |= VariableFlags.Unconsumed; - var variable = new Variable(node, flags); - OnDeclare(variable); + if (mark && !isTable) flags |= VariableFlags.Unconsumed; - if (!mark && direction == ParameterDirection.Output) - { - // pure output param, and first time we're seeing it is a read: that's not right - parser.OnVariableAccessedBeforeAssignment(variable); - } - else if (mark && direction is ParameterDirection.Input or ParameterDirection.InputOutput) - { - // we haven't consumed the original value - but watch out for "if" etc - if (AssignmentTracking) parser.OnVariableValueNotConsumed(variable); - } + var variable = new Variable(node, flags); + OnDeclare(variable); - if (variable.IsTable && variable.IsOutputParameter) - { - parser.OnTableVariableOutputParameter(variable); - } + if (!mark && direction == ParameterDirection.Output) + { + // pure output param, and first time we're seeing it is a read: that's not right + parser.OnVariableAccessedBeforeAssignment(variable); } - else + else if (mark && direction is ParameterDirection.Input or ParameterDirection.InputOutput) + { + // we haven't consumed the original value - but watch out for "if" etc + if (AssignmentTracking) parser.OnVariableValueNotConsumed(variable); + } + + if (variable.IsTable && variable.IsOutputParameter) { - parser.OnVariableNotDeclared(new(node, flags)); + parser.OnTableVariableOutputParameter(variable); } } } diff --git a/src/Dapper.AOT/SqlAttribute.cs b/src/Dapper.AOT/SqlAttribute.cs new file mode 100644 index 00000000..03be0ece --- /dev/null +++ b/src/Dapper.AOT/SqlAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.ComponentModel; + +namespace Dapper; + +/// +/// Indicates that a value should be interpreted as SQL +/// +[ImmutableObject(true)] +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false)] +public sealed class SqlAttribute : Attribute { } diff --git a/src/Dapper.AOT/SqlSyntax.cs b/src/Dapper.AOT/SqlSyntax.cs index fcd3c343..3eae9363 100644 --- a/src/Dapper.AOT/SqlSyntax.cs +++ b/src/Dapper.AOT/SqlSyntax.cs @@ -1,5 +1,7 @@ namespace Dapper; +// THIS FILE MUST BE KEPT IN SYNC BETWEEN THE LIB AND ANALYZERS + /// /// Indicates the family of SQL variant used /// @@ -24,7 +26,7 @@ public enum SqlSyntax GeneralWithColonParameters = 3, /// - /// SQL Server (Transact-SQL)using the @value parameter convention; has full syntax processing support + /// SQL Server (Transact-SQL)using the @value parameter convention; has full syntax processing support /// SqlServer = 100, diff --git a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj index 10beaa55..edf11ced 100644 --- a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj +++ b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj @@ -38,12 +38,15 @@ all runtime; build; native; contentfiles; analyzers - + + + + diff --git a/test/Dapper.AOT.Test/Interceptors/BaseCommandFactory.output.cs b/test/Dapper.AOT.Test/Interceptors/BaseCommandFactory.output.cs index ddc30769..29cec504 100644 --- a/test/Dapper.AOT.Test/Interceptors/BaseCommandFactory.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/BaseCommandFactory.output.cs @@ -6,7 +6,7 @@ internal static int Execute0(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/BaseCommandFactory.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/BaseCommandFactory.output.netfx.cs index ddc30769..29cec504 100644 --- a/test/Dapper.AOT.Test/Interceptors/BaseCommandFactory.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/BaseCommandFactory.output.netfx.cs @@ -6,7 +6,7 @@ internal static int Execute0(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/Blame.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/Blame.output.netfx.txt index 1047cf8b..469f6ff0 100644 --- a/test/Dapper.AOT.Test/Interceptors/Blame.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/Blame.output.netfx.txt @@ -1,73 +1,4 @@ -Generator produced 24 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 23 of 23 enabled call-sites using 1 interceptors, 0 commands and 0 readers - -Error DAP203 Interceptors/Blame.input.cs L12 C48 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L13 C51 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L14 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L15 C50 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L16 C49 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L17 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L18 C51 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L20 C8 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L22 C50 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L25 C20 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L28 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L33 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C27) - -Error DAP203 Interceptors/Blame.input.cs L35 C53 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L37 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L39 C55 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L41 C54 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L43 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L45 C56 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L48 C8 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L51 C55 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L55 C20 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L59 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L65 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C27) diff --git a/test/Dapper.AOT.Test/Interceptors/Blame.output.txt b/test/Dapper.AOT.Test/Interceptors/Blame.output.txt index 1047cf8b..469f6ff0 100644 --- a/test/Dapper.AOT.Test/Interceptors/Blame.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/Blame.output.txt @@ -1,73 +1,4 @@ -Generator produced 24 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 23 of 23 enabled call-sites using 1 interceptors, 0 commands and 0 readers - -Error DAP203 Interceptors/Blame.input.cs L12 C48 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L13 C51 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L14 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L15 C50 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L16 C49 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L17 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L18 C51 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L20 C8 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L22 C50 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L25 C20 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L28 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L33 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C27) - -Error DAP203 Interceptors/Blame.input.cs L35 C53 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L37 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L39 C55 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L41 C54 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L43 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L45 C56 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L48 C8 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L51 C55 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C26) - -Error DAP203 Interceptors/Blame.input.cs L55 C20 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L59 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L2 C8) - -Error DAP203 Interceptors/Blame.input.cs L65 C22 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L1 C27) diff --git a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs index 8271511a..6ec1667e 100644 --- a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs @@ -6,7 +6,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName, CacheCommand // takes parameter: - // parameter map: (everything) + // parameter map: Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -22,7 +22,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName, CacheCommand // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -38,7 +38,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName, CacheCommand // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -54,7 +54,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -71,7 +71,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); diff --git a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs index 8271511a..6ec1667e 100644 --- a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs @@ -6,7 +6,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName, CacheCommand // takes parameter: - // parameter map: (everything) + // parameter map: Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -22,7 +22,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName, CacheCommand // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -38,7 +38,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName, CacheCommand // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -54,7 +54,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -71,7 +71,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); diff --git a/test/Dapper.AOT.Test/Interceptors/ComplexGenerics.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/ComplexGenerics.output.netfx.txt deleted file mode 100644 index 7d222f9d..00000000 --- a/test/Dapper.AOT.Test/Interceptors/ComplexGenerics.output.netfx.txt +++ /dev/null @@ -1,7 +0,0 @@ -Generator produced 2 diagnostics: - -Info DAP016 Interceptors/ComplexGenerics.input.cs L16 C36 -Generic type parameters (SomeLayer.Foo.Bar.Blap) are not currently supported - -Info DAP016 Interceptors/ComplexGenerics.input.cs L17 C36 -Generic type parameters (SomeLayer.Foo.Bar.Blap) are not currently supported diff --git a/test/Dapper.AOT.Test/Interceptors/ComplexGenerics.output.txt b/test/Dapper.AOT.Test/Interceptors/ComplexGenerics.output.txt deleted file mode 100644 index 7d222f9d..00000000 --- a/test/Dapper.AOT.Test/Interceptors/ComplexGenerics.output.txt +++ /dev/null @@ -1,7 +0,0 @@ -Generator produced 2 diagnostics: - -Info DAP016 Interceptors/ComplexGenerics.input.cs L16 C36 -Generic type parameters (SomeLayer.Foo.Bar.Blap) are not currently supported - -Info DAP016 Interceptors/ComplexGenerics.input.cs L17 C36 -Generic type parameters (SomeLayer.Foo.Bar.Blap) are not currently supported diff --git a/test/Dapper.AOT.Test/Interceptors/EnumerableExtensions.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/EnumerableExtensions.output.netfx.txt index 9566085e..33103c81 100644 --- a/test/Dapper.AOT.Test/Interceptors/EnumerableExtensions.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/EnumerableExtensions.output.netfx.txt @@ -1,19 +1,4 @@ -Generator produced 6 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 6 of 6 enabled call-sites using 1 interceptors, 0 commands and 0 readers - -Warning DAP028 Interceptors/EnumerableExtensions.input.cs L10 C13 -Use Query(...).AsList() instead of Query(...).ToList() - -Warning DAP027 Interceptors/EnumerableExtensions.input.cs L13 C13 -Use QueryFirst() instead of Query(...).First() - -Warning DAP027 Interceptors/EnumerableExtensions.input.cs L14 C13 -Use QuerySingle() instead of Query(...).Single() - -Warning DAP027 Interceptors/EnumerableExtensions.input.cs L15 C13 -Use QueryFirstOrDefault() instead of Query(...).FirstOrDefault() - -Warning DAP027 Interceptors/EnumerableExtensions.input.cs L16 C13 -Use QuerySingleOrDefault() instead of Query(...).SingleOrDefault() diff --git a/test/Dapper.AOT.Test/Interceptors/EnumerableExtensions.output.txt b/test/Dapper.AOT.Test/Interceptors/EnumerableExtensions.output.txt index 9566085e..33103c81 100644 --- a/test/Dapper.AOT.Test/Interceptors/EnumerableExtensions.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/EnumerableExtensions.output.txt @@ -1,19 +1,4 @@ -Generator produced 6 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 6 of 6 enabled call-sites using 1 interceptors, 0 commands and 0 readers - -Warning DAP028 Interceptors/EnumerableExtensions.input.cs L10 C13 -Use Query(...).AsList() instead of Query(...).ToList() - -Warning DAP027 Interceptors/EnumerableExtensions.input.cs L13 C13 -Use QueryFirst() instead of Query(...).First() - -Warning DAP027 Interceptors/EnumerableExtensions.input.cs L14 C13 -Use QuerySingle() instead of Query(...).Single() - -Warning DAP027 Interceptors/EnumerableExtensions.input.cs L15 C13 -Use QueryFirstOrDefault() instead of Query(...).FirstOrDefault() - -Warning DAP027 Interceptors/EnumerableExtensions.input.cs L16 C13 -Use QuerySingleOrDefault() instead of Query(...).SingleOrDefault() diff --git a/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.cs b/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.cs index 7e545b56..5ff989e7 100644 --- a/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.cs @@ -20,7 +20,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -36,7 +36,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: global::Foo.DynamicHint - // parameter map: (everything) + // parameter map: Bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -190,20 +190,12 @@ public override void AddParameters(global::System.Data.Common.DbCommand cmd, glo p.Value = AsValue(args.Bar); ps.Add(p); - p = cmd.CreateParameter(); - p.ParameterName = "Count"; - p.DbType = global::System.Data.DbType.Int32; - p.Direction = global::System.Data.ParameterDirection.Input; - p.Value = AsValue(args.Count); - ps.Add(p); - } public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, global::Foo.DynamicHint args) { var ps = cmd.Parameters; ps[0].Value = AsValue(args.Foo); ps[1].Value = AsValue(args.Bar); - ps[2].Value = AsValue(args.Count); } public override bool CanPrepare => true; diff --git a/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.netfx.cs index 7e545b56..5ff989e7 100644 --- a/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.netfx.cs @@ -20,7 +20,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -36,7 +36,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: global::Foo.DynamicHint - // parameter map: (everything) + // parameter map: Bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -190,20 +190,12 @@ public override void AddParameters(global::System.Data.Common.DbCommand cmd, glo p.Value = AsValue(args.Bar); ps.Add(p); - p = cmd.CreateParameter(); - p.ParameterName = "Count"; - p.DbType = global::System.Data.DbType.Int32; - p.Direction = global::System.Data.ParameterDirection.Input; - p.Value = AsValue(args.Count); - ps.Add(p); - } public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, global::Foo.DynamicHint args) { var ps = cmd.Parameters; ps[0].Value = AsValue(args.Foo); ps[1].Value = AsValue(args.Bar); - ps[2].Value = AsValue(args.Count); } public override bool CanPrepare => true; diff --git a/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.netfx.txt index 3ba9b43c..09351906 100644 --- a/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.netfx.txt @@ -1,7 +1,4 @@ -Generator produced 2 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 3 of 3 enabled call-sites using 3 interceptors, 2 commands and 1 readers - -Info DAP029 Interceptors/EstimatedRowCount.input.cs L9 C17 -The [EstimatedRowCount] will be ignored due to parameter member 'Count' diff --git a/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.txt b/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.txt index 3ba9b43c..09351906 100644 --- a/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/EstimatedRowCount.output.txt @@ -1,7 +1,4 @@ -Generator produced 2 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 3 of 3 enabled call-sites using 3 interceptors, 2 commands and 1 readers - -Info DAP029 Interceptors/EstimatedRowCount.input.cs L9 C17 -The [EstimatedRowCount] will be ignored due to parameter member 'Count' diff --git a/test/Dapper.AOT.Test/Interceptors/Execute.output.cs b/test/Dapper.AOT.Test/Interceptors/Execute.output.cs index 2ddb5cdf..2c944698 100644 --- a/test/Dapper.AOT.Test/Interceptors/Execute.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/Execute.output.cs @@ -6,7 +6,7 @@ internal static int Execute0(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -44,7 +44,7 @@ internal static int Execute2(this global::System.Data.IDbConnection cnn, string { // Execute, Async, HasParameters, StoredProcedure // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/Execute.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/Execute.output.netfx.cs index 2ddb5cdf..2c944698 100644 --- a/test/Dapper.AOT.Test/Interceptors/Execute.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/Execute.output.netfx.cs @@ -6,7 +6,7 @@ internal static int Execute0(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -44,7 +44,7 @@ internal static int Execute2(this global::System.Data.IDbConnection cnn, string { // Execute, Async, HasParameters, StoredProcedure // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.cs b/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.cs index f8da5792..a8bfc371 100644 --- a/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.cs @@ -6,7 +6,7 @@ internal static int Execute0(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::System.Collections.Generic.List - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -20,7 +20,7 @@ internal static int Execute1(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::System.Collections.Generic.List> - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -34,7 +34,7 @@ internal static int Execute2(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.Customer[] - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -62,7 +62,7 @@ internal static int Execute4(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.CustomerEnumerable - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -76,7 +76,7 @@ internal static int Execute5(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.CustomerICollection - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -90,7 +90,7 @@ internal static int Execute6(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.CustomerIList - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -104,7 +104,7 @@ internal static int Execute7(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.CustomerIReadOnlyCollection - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -118,7 +118,7 @@ internal static int Execute8(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.CustomerIReadOnlyList - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.netfx.cs index f8da5792..a8bfc371 100644 --- a/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.netfx.cs @@ -6,7 +6,7 @@ internal static int Execute0(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::System.Collections.Generic.List - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -20,7 +20,7 @@ internal static int Execute1(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::System.Collections.Generic.List> - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -34,7 +34,7 @@ internal static int Execute2(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.Customer[] - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -62,7 +62,7 @@ internal static int Execute4(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.CustomerEnumerable - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -76,7 +76,7 @@ internal static int Execute5(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.CustomerICollection - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -90,7 +90,7 @@ internal static int Execute6(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.CustomerIList - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -104,7 +104,7 @@ internal static int Execute7(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.CustomerIReadOnlyCollection - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -118,7 +118,7 @@ internal static int Execute8(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::Foo.CustomerIReadOnlyList - // parameter map: (everything) + // parameter map: X Y Z global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/ExecuteNotEnabled.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/ExecuteNotEnabled.output.netfx.txt deleted file mode 100644 index 47939bd7..00000000 --- a/test/Dapper.AOT.Test/Interceptors/ExecuteNotEnabled.output.netfx.txt +++ /dev/null @@ -1,4 +0,0 @@ -Generator produced 1 diagnostics: - -Info DAP005 L1 C1 -Candidate Dapper methods were detected, but none have Dapper.AOT enabled; [DapperAot] can be added at the method, type, module or assembly level (for example '[module:DapperAot]') diff --git a/test/Dapper.AOT.Test/Interceptors/ExecuteNotEnabled.output.txt b/test/Dapper.AOT.Test/Interceptors/ExecuteNotEnabled.output.txt deleted file mode 100644 index 47939bd7..00000000 --- a/test/Dapper.AOT.Test/Interceptors/ExecuteNotEnabled.output.txt +++ /dev/null @@ -1,4 +0,0 @@ -Generator produced 1 diagnostics: - -Info DAP005 L1 C1 -Candidate Dapper methods were detected, but none have Dapper.AOT enabled; [DapperAot] can be added at the method, type, module or assembly level (for example '[module:DapperAot]') diff --git a/test/Dapper.AOT.Test/Interceptors/ExecuteScalar.output.cs b/test/Dapper.AOT.Test/Interceptors/ExecuteScalar.output.cs index 2a609b93..67f3d057 100644 --- a/test/Dapper.AOT.Test/Interceptors/ExecuteScalar.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/ExecuteScalar.output.cs @@ -6,7 +6,7 @@ file static class DapperGeneratedInterceptors { // Execute, HasParameters, StoredProcedure, Scalar // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -44,7 +44,7 @@ internal static float ExecuteScalar3(this global::System.Data.IDbConnection cnn, { // Execute, TypedResult, HasParameters, StoredProcedure, Scalar // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: float global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -85,7 +85,7 @@ internal static float ExecuteScalar5(this global::System.Data.IDbConnection cnn, { // Execute, Async, HasParameters, StoredProcedure, Scalar // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -123,7 +123,7 @@ internal static float ExecuteScalar5(this global::System.Data.IDbConnection cnn, { // Execute, Async, TypedResult, HasParameters, StoredProcedure, Scalar // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: float global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); diff --git a/test/Dapper.AOT.Test/Interceptors/ExecuteScalar.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/ExecuteScalar.output.netfx.cs index 2a609b93..67f3d057 100644 --- a/test/Dapper.AOT.Test/Interceptors/ExecuteScalar.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/ExecuteScalar.output.netfx.cs @@ -6,7 +6,7 @@ file static class DapperGeneratedInterceptors { // Execute, HasParameters, StoredProcedure, Scalar // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -44,7 +44,7 @@ internal static float ExecuteScalar3(this global::System.Data.IDbConnection cnn, { // Execute, TypedResult, HasParameters, StoredProcedure, Scalar // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: float global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -85,7 +85,7 @@ internal static float ExecuteScalar5(this global::System.Data.IDbConnection cnn, { // Execute, Async, HasParameters, StoredProcedure, Scalar // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -123,7 +123,7 @@ internal static float ExecuteScalar5(this global::System.Data.IDbConnection cnn, { // Execute, Async, TypedResult, HasParameters, StoredProcedure, Scalar // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: float global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); diff --git a/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.cs b/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.cs index 81d67825..1935ec83 100644 --- a/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.cs @@ -6,7 +6,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::SomeApp.SomeQueryType global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); diff --git a/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.cs index 81d67825..1935ec83 100644 --- a/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.cs @@ -6,7 +6,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::SomeApp.SomeQueryType global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); diff --git a/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.cs b/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.cs index e55ad7c5..bad2b2bd 100644 --- a/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.cs @@ -17,6 +17,7 @@ internal static int Execute0(this global::System.Data.IDbConnection cnn, string [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\MappedSqlDetection.input.cs", 17, 20)] [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\MappedSqlDetection.input.cs", 23, 20)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\MappedSqlDetection.input.cs", 29, 20)] internal static int Execute1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) { // Execute, HasParameters, Text @@ -57,22 +58,8 @@ internal static int Execute3(this global::System.Data.IDbConnection cnn, string } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\MappedSqlDetection.input.cs", 29, 20)] - internal static int Execute4(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) - { - // Execute, HasParameters, Text - // takes parameter: global::Foo.SomeArg - // parameter map: E - 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 not null); - - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory4.Instance).Execute((global::Foo.SomeArg)param!); - - } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\MappedSqlDetection.input.cs", 32, 20)] - internal static int Execute5(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + internal static int Execute4(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) { // Execute, HasParameters, Text // takes parameter: global::Foo.SomeOtherArg @@ -81,7 +68,7 @@ internal static int Execute5(this global::System.Data.IDbConnection cnn, string global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); global::System.Diagnostics.Debug.Assert(param is not null); - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory5.Instance).Execute((global::Foo.SomeOtherArg)param!); + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory4.Instance).Execute((global::Foo.SomeOtherArg)param!); } @@ -204,46 +191,9 @@ public override void PostProcess(global::System.Data.Common.DbCommand cmd, globa } - private sealed class CommandFactory4 : CommonCommandFactory + private sealed class CommandFactory4 : CommonCommandFactory { internal static readonly CommandFactory4 Instance = new(); - public override void AddParameters(global::System.Data.Common.DbCommand cmd, global::Foo.SomeArg args) - { - var ps = cmd.Parameters; - global::System.Data.Common.DbParameter p; - p = cmd.CreateParameter(); - p.ParameterName = "E"; - p.DbType = global::System.Data.DbType.String; - p.Size = -1; - p.Direction = global::System.Data.ParameterDirection.ReturnValue; - p.Value = global::System.DBNull.Value; - ps.Add(p); - - } - public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, global::Foo.SomeArg args) - { - var ps = cmd.Parameters; - ps[0].Value = global::System.DBNull.Value; - - } - public override void PostProcess(global::System.Data.Common.DbCommand cmd, global::Foo.SomeArg args) - { - var ps = cmd.Parameters; - args.E = Parse(ps[0].Value); - - } - public override void PostProcess(global::System.Data.Common.DbCommand cmd, global::Foo.SomeArg args, int rowCount) - { - args.RowCount = rowCount; - PostProcess(cmd, args); - } - public override bool CanPrepare => true; - - } - - private sealed class CommandFactory5 : CommonCommandFactory - { - internal static readonly CommandFactory5 Instance = new(); public override void AddParameters(global::System.Data.Common.DbCommand cmd, global::Foo.SomeOtherArg args) { var ps = cmd.Parameters; diff --git a/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.netfx.cs index e55ad7c5..bad2b2bd 100644 --- a/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.netfx.cs @@ -17,6 +17,7 @@ internal static int Execute0(this global::System.Data.IDbConnection cnn, string [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\MappedSqlDetection.input.cs", 17, 20)] [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\MappedSqlDetection.input.cs", 23, 20)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\MappedSqlDetection.input.cs", 29, 20)] internal static int Execute1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) { // Execute, HasParameters, Text @@ -57,22 +58,8 @@ internal static int Execute3(this global::System.Data.IDbConnection cnn, string } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\MappedSqlDetection.input.cs", 29, 20)] - internal static int Execute4(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) - { - // Execute, HasParameters, Text - // takes parameter: global::Foo.SomeArg - // parameter map: E - 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 not null); - - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory4.Instance).Execute((global::Foo.SomeArg)param!); - - } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\MappedSqlDetection.input.cs", 32, 20)] - internal static int Execute5(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + internal static int Execute4(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) { // Execute, HasParameters, Text // takes parameter: global::Foo.SomeOtherArg @@ -81,7 +68,7 @@ internal static int Execute5(this global::System.Data.IDbConnection cnn, string global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); global::System.Diagnostics.Debug.Assert(param is not null); - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory5.Instance).Execute((global::Foo.SomeOtherArg)param!); + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory4.Instance).Execute((global::Foo.SomeOtherArg)param!); } @@ -204,46 +191,9 @@ public override void PostProcess(global::System.Data.Common.DbCommand cmd, globa } - private sealed class CommandFactory4 : CommonCommandFactory + private sealed class CommandFactory4 : CommonCommandFactory { internal static readonly CommandFactory4 Instance = new(); - public override void AddParameters(global::System.Data.Common.DbCommand cmd, global::Foo.SomeArg args) - { - var ps = cmd.Parameters; - global::System.Data.Common.DbParameter p; - p = cmd.CreateParameter(); - p.ParameterName = "E"; - p.DbType = global::System.Data.DbType.String; - p.Size = -1; - p.Direction = global::System.Data.ParameterDirection.ReturnValue; - p.Value = global::System.DBNull.Value; - ps.Add(p); - - } - public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, global::Foo.SomeArg args) - { - var ps = cmd.Parameters; - ps[0].Value = global::System.DBNull.Value; - - } - public override void PostProcess(global::System.Data.Common.DbCommand cmd, global::Foo.SomeArg args) - { - var ps = cmd.Parameters; - args.E = Parse(ps[0].Value); - - } - public override void PostProcess(global::System.Data.Common.DbCommand cmd, global::Foo.SomeArg args, int rowCount) - { - args.RowCount = rowCount; - PostProcess(cmd, args); - } - public override bool CanPrepare => true; - - } - - private sealed class CommandFactory5 : CommonCommandFactory - { - internal static readonly CommandFactory5 Instance = new(); public override void AddParameters(global::System.Data.Common.DbCommand cmd, global::Foo.SomeOtherArg args) { var ps = cmd.Parameters; diff --git a/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.netfx.txt index 03ac671b..26cdda61 100644 --- a/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.netfx.txt @@ -1,10 +1,4 @@ -Generator produced 3 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 -Dapper.AOT handled 7 of 7 enabled call-sites using 6 interceptors, 6 commands and 0 readers - -Warning DAP023 Interceptors/MappedSqlDetection.input.cs L32 C20 -Members 'DuplicateToCauseProblems' and 'RowCount' are both marked [RowCount] - -Warning DAP024 Interceptors/MappedSqlDetection.input.cs L32 C20 -Member 'DuplicateToCauseProblems' is marked both [RowCount] and [DbValue]; [DbValue] will be ignored +Dapper.AOT handled 7 of 7 enabled call-sites using 5 interceptors, 5 commands and 0 readers diff --git a/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.txt b/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.txt index 03ac671b..26cdda61 100644 --- a/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/MappedSqlDetection.output.txt @@ -1,10 +1,4 @@ -Generator produced 3 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 -Dapper.AOT handled 7 of 7 enabled call-sites using 6 interceptors, 6 commands and 0 readers - -Warning DAP023 Interceptors/MappedSqlDetection.input.cs L32 C20 -Members 'DuplicateToCauseProblems' and 'RowCount' are both marked [RowCount] - -Warning DAP024 Interceptors/MappedSqlDetection.input.cs L32 C20 -Member 'DuplicateToCauseProblems' is marked both [RowCount] and [DbValue]; [DbValue] will be ignored +Dapper.AOT handled 7 of 7 enabled call-sites using 5 interceptors, 5 commands and 0 readers diff --git a/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.cs b/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.cs index af5be3d9..fef50c4e 100644 --- a/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.cs @@ -43,7 +43,7 @@ internal static int Execute3(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::SomeCode.InternalNesting.SomePublicType - // parameter map: (everything) + // parameter map: Id global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -70,7 +70,7 @@ internal static int Execute5(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::SomeCode.InternalNesting.SomeInternalType - // parameter map: (everything) + // parameter map: Id global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -97,7 +97,7 @@ internal static int Execute7(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::SomeCode.InternalNesting.SomeProtectedInternalType - // parameter map: (everything) + // parameter map: Id global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.netfx.cs index af5be3d9..fef50c4e 100644 --- a/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.netfx.cs @@ -43,7 +43,7 @@ internal static int Execute3(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::SomeCode.InternalNesting.SomePublicType - // parameter map: (everything) + // parameter map: Id global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -70,7 +70,7 @@ internal static int Execute5(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::SomeCode.InternalNesting.SomeInternalType - // parameter map: (everything) + // parameter map: Id global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -97,7 +97,7 @@ internal static int Execute7(this global::System.Data.IDbConnection cnn, string { // Execute, HasParameters, StoredProcedure // takes parameter: global::SomeCode.InternalNesting.SomeProtectedInternalType - // parameter map: (everything) + // parameter map: Id global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.netfx.txt index 5fdb40cf..acb207e9 100644 --- a/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.netfx.txt @@ -1,55 +1,4 @@ -Generator produced 18 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 9 of 23 enabled call-sites using 8 interceptors, 3 commands and 3 readers - -Warning DAP012 Interceptors/MiscDiagnostics.input.cs L16 C20 -Because of differences in how Dapper and Dapper.AOT can process tuple-types, please add '[BindTupleByName({true|false})]' to clarify your intent - -Info DAP013 Interceptors/MiscDiagnostics.input.cs L16 C20 -Tuple-type results are not currently supported - -Warning DAP012 Interceptors/MiscDiagnostics.input.cs L20 C20 -Because of differences in how Dapper and Dapper.AOT can process tuple-types, please add '[BindTupleByName({true|false})]' to clarify your intent - -Info DAP014 Interceptors/MiscDiagnostics.input.cs L20 C20 -Tuple-type parameters are not currently supported - -Info DAP015 Interceptors/MiscDiagnostics.input.cs L23 C20 -The parameter type could not be resolved - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L43 C24 -Type 'SomeCode.InternalNesting.SomePrivateType' is not accessible; private types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L44 C24 -Type 'SomeCode.InternalNesting.SomePrivateType' is not accessible; private types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L47 C24 -Type 'SomeCode.InternalNesting.SomeProtectedType' is not accessible; protected types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L48 C24 -Type 'SomeCode.InternalNesting.SomeProtectedType' is not accessible; protected types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L51 C24 -Type 'SomeCode.InternalNesting.SomePrivateProtectedType' is not accessible; private protected types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L52 C24 -Type 'SomeCode.InternalNesting.SomePrivateProtectedType' is not accessible; private protected types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L91 C24 -Type 'SomeCode.PrivateNesting' is not accessible; private types are not currently supported - -Info DAP016 Interceptors/MiscDiagnostics.input.cs L105 C20 -Generic type parameters (T) are not currently supported - -Info DAP016 Interceptors/MiscDiagnostics.input.cs L108 C20 -Generic type parameters (T) are not currently supported - -Info DAP013 Interceptors/MiscDiagnostics.input.cs L115 C20 -Tuple-type results are not currently supported - -Info DAP014 Interceptors/MiscDiagnostics.input.cs L118 C20 -Tuple-type parameters are not currently supported - -Warning DAP011 Interceptors/MiscDiagnostics.input.cs L128 C20 -Dapper (original) does not support tuple results with bind-by-name semantics diff --git a/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.txt b/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.txt index 5fdb40cf..acb207e9 100644 --- a/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/MiscDiagnostics.output.txt @@ -1,55 +1,4 @@ -Generator produced 18 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 9 of 23 enabled call-sites using 8 interceptors, 3 commands and 3 readers - -Warning DAP012 Interceptors/MiscDiagnostics.input.cs L16 C20 -Because of differences in how Dapper and Dapper.AOT can process tuple-types, please add '[BindTupleByName({true|false})]' to clarify your intent - -Info DAP013 Interceptors/MiscDiagnostics.input.cs L16 C20 -Tuple-type results are not currently supported - -Warning DAP012 Interceptors/MiscDiagnostics.input.cs L20 C20 -Because of differences in how Dapper and Dapper.AOT can process tuple-types, please add '[BindTupleByName({true|false})]' to clarify your intent - -Info DAP014 Interceptors/MiscDiagnostics.input.cs L20 C20 -Tuple-type parameters are not currently supported - -Info DAP015 Interceptors/MiscDiagnostics.input.cs L23 C20 -The parameter type could not be resolved - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L43 C24 -Type 'SomeCode.InternalNesting.SomePrivateType' is not accessible; private types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L44 C24 -Type 'SomeCode.InternalNesting.SomePrivateType' is not accessible; private types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L47 C24 -Type 'SomeCode.InternalNesting.SomeProtectedType' is not accessible; protected types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L48 C24 -Type 'SomeCode.InternalNesting.SomeProtectedType' is not accessible; protected types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L51 C24 -Type 'SomeCode.InternalNesting.SomePrivateProtectedType' is not accessible; private protected types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L52 C24 -Type 'SomeCode.InternalNesting.SomePrivateProtectedType' is not accessible; private protected types are not currently supported - -Info DAP017 Interceptors/MiscDiagnostics.input.cs L91 C24 -Type 'SomeCode.PrivateNesting' is not accessible; private types are not currently supported - -Info DAP016 Interceptors/MiscDiagnostics.input.cs L105 C20 -Generic type parameters (T) are not currently supported - -Info DAP016 Interceptors/MiscDiagnostics.input.cs L108 C20 -Generic type parameters (T) are not currently supported - -Info DAP013 Interceptors/MiscDiagnostics.input.cs L115 C20 -Tuple-type results are not currently supported - -Info DAP014 Interceptors/MiscDiagnostics.input.cs L118 C20 -Tuple-type parameters are not currently supported - -Warning DAP011 Interceptors/MiscDiagnostics.input.cs L128 C20 -Dapper (original) does not support tuple results with bind-by-name semantics diff --git a/test/Dapper.AOT.Test/Interceptors/Query.output.cs b/test/Dapper.AOT.Test/Interceptors/Query.output.cs index 3f92ff03..db40d915 100644 --- a/test/Dapper.AOT.Test/Interceptors/Query.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/Query.output.cs @@ -20,7 +20,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -36,7 +36,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -95,7 +95,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); diff --git a/test/Dapper.AOT.Test/Interceptors/Query.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/Query.output.netfx.cs index 678ab5d7..341c6781 100644 --- a/test/Dapper.AOT.Test/Interceptors/Query.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/Query.output.netfx.cs @@ -20,7 +20,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -36,7 +36,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -95,7 +95,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.cs index c0f9bb85..48056c2f 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.cs @@ -316,7 +316,7 @@ private RowFactory1() {} columnOffset++; } - return new global::Foo.GetOnlyPropertiesViaConstructor(value0, value1, value2); + return new global::Foo.GetOnlyPropertiesViaConstructor; } } @@ -352,43 +352,38 @@ private RowFactory2() {} } public override global::Foo.RecordClass Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) { - int value0 = default; - string? value1 = default; - double? value2 = default; + global::Foo.RecordClass result = new(); foreach (var token in tokens) { switch (token) { case 0: - value0 = reader.GetInt32(columnOffset); + result.X = reader.GetInt32(columnOffset); break; case 3: - value0 = GetValue(reader, columnOffset); + result.X = GetValue(reader, columnOffset); break; case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); break; case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); break; case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); break; case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); break; } columnOffset++; } - return new global::Foo.RecordClass - { - X = value0, - Y = value1, - Z = value2, - }; + return result; + } + } private sealed class RowFactory3 : global::Dapper.RowFactory @@ -453,7 +448,7 @@ private RowFactory3() {} columnOffset++; } - return new global::Foo.RecordClassSimpleCtor(value0, value1, value2); + return new global::Foo.RecordClassSimpleCtor; } } @@ -555,38 +550,38 @@ private RowFactory5() {} } public override global::Foo.RecordStructSimpleCtor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) { - int value0 = default; - string? value1 = default; - double? value2 = default; + global::Foo.RecordStructSimpleCtor result = new(); foreach (var token in tokens) { switch (token) { case 0: - value0 = reader.GetInt32(columnOffset); + result.X = reader.GetInt32(columnOffset); break; case 3: - value0 = GetValue(reader, columnOffset); + result.X = GetValue(reader, columnOffset); break; case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); break; case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); break; case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); break; case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); break; } columnOffset++; } - return new global::Foo.RecordStructSimpleCtor(value0, value1, value2); + return result; + } + } private sealed class RowFactory6 : global::Dapper.RowFactory @@ -722,7 +717,7 @@ private RowFactory7() {} columnOffset++; } - return new global::Foo.InitPropsAndDapperAotCtor(value1) + return new global::Foo.InitPropsAndDapperAotCtor { X = value0, Z = value2, @@ -734,65 +729,6 @@ private sealed class RowFactory8 : global::Dapper.RowFactory 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 4245442695U when NormalizedEquals(name, "x"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 4228665076U when NormalizedEquals(name, "y"): - token = type == typeof(string) ? 1 : 4; - break; - case 4278997933U when NormalizedEquals(name, "z"): - token = type == typeof(double) ? 2 : 5; - break; - - } - tokens[i] = token; - columnOffset++; - - } - return null; - } - public override global::Foo.OnlyNonDapperAotCtor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - global::Foo.OnlyNonDapperAotCtor result = new(); - foreach (var token in tokens) - { - switch (token) - { - case 0: - result.X = reader.GetInt32(columnOffset); - break; - case 3: - result.X = GetValue(reader, columnOffset); - break; - case 1: - result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); - break; - case 5: - result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); - break; - - } - columnOffset++; - - } - return result; - - } } @@ -800,66 +736,7 @@ private sealed class RowFactory9 : global::Dapper.RowFactory 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 4245442695U when NormalizedEquals(name, "x"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 4228665076U when NormalizedEquals(name, "y"): - token = type == typeof(string) ? 1 : 4; - break; - case 4278997933U when NormalizedEquals(name, "z"): - token = type == typeof(double) ? 2 : 5; - break; - - } - tokens[i] = token; - columnOffset++; - } - return null; - } - public override global::Foo.SingleDefaultCtor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - int value0 = default; - string? value1 = default; - double? value2 = default; - foreach (var token in tokens) - { - switch (token) - { - case 0: - value0 = reader.GetInt32(columnOffset); - break; - case 3: - value0 = GetValue(reader, columnOffset); - break; - case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); - break; - case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); - break; - - } - columnOffset++; - - } - return new global::Foo.SingleDefaultCtor(value0, value1, value2); - } } private sealed class RowFactory10 : global::Dapper.RowFactory @@ -873,66 +750,7 @@ private sealed class RowFactory11 : global::Dapper.RowFactory 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 4245442695U when NormalizedEquals(name, "x"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 4228665076U when NormalizedEquals(name, "y"): - token = type == typeof(string) ? 1 : 4; - break; - case 4278997933U when NormalizedEquals(name, "z"): - token = type == typeof(double) ? 2 : 5; - break; - } - tokens[i] = token; - columnOffset++; - - } - return null; - } - public override global::Foo.SingleDapperAotCtor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - int value0 = default; - string? value1 = default; - double? value2 = default; - foreach (var token in tokens) - { - switch (token) - { - case 0: - value0 = reader.GetInt32(columnOffset); - break; - case 3: - value0 = GetValue(reader, columnOffset); - break; - case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); - break; - case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); - break; - - } - columnOffset++; - - } - return new global::Foo.SingleDapperAotCtor(value0, value1, value2); - } } diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.netfx.cs index c0f9bb85..48056c2f 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.netfx.cs @@ -316,7 +316,7 @@ private RowFactory1() {} columnOffset++; } - return new global::Foo.GetOnlyPropertiesViaConstructor(value0, value1, value2); + return new global::Foo.GetOnlyPropertiesViaConstructor; } } @@ -352,43 +352,38 @@ private RowFactory2() {} } public override global::Foo.RecordClass Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) { - int value0 = default; - string? value1 = default; - double? value2 = default; + global::Foo.RecordClass result = new(); foreach (var token in tokens) { switch (token) { case 0: - value0 = reader.GetInt32(columnOffset); + result.X = reader.GetInt32(columnOffset); break; case 3: - value0 = GetValue(reader, columnOffset); + result.X = GetValue(reader, columnOffset); break; case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); break; case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); break; case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); break; case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); break; } columnOffset++; } - return new global::Foo.RecordClass - { - X = value0, - Y = value1, - Z = value2, - }; + return result; + } + } private sealed class RowFactory3 : global::Dapper.RowFactory @@ -453,7 +448,7 @@ private RowFactory3() {} columnOffset++; } - return new global::Foo.RecordClassSimpleCtor(value0, value1, value2); + return new global::Foo.RecordClassSimpleCtor; } } @@ -555,38 +550,38 @@ private RowFactory5() {} } public override global::Foo.RecordStructSimpleCtor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) { - int value0 = default; - string? value1 = default; - double? value2 = default; + global::Foo.RecordStructSimpleCtor result = new(); foreach (var token in tokens) { switch (token) { case 0: - value0 = reader.GetInt32(columnOffset); + result.X = reader.GetInt32(columnOffset); break; case 3: - value0 = GetValue(reader, columnOffset); + result.X = GetValue(reader, columnOffset); break; case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); break; case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); break; case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); break; case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); break; } columnOffset++; } - return new global::Foo.RecordStructSimpleCtor(value0, value1, value2); + return result; + } + } private sealed class RowFactory6 : global::Dapper.RowFactory @@ -722,7 +717,7 @@ private RowFactory7() {} columnOffset++; } - return new global::Foo.InitPropsAndDapperAotCtor(value1) + return new global::Foo.InitPropsAndDapperAotCtor { X = value0, Z = value2, @@ -734,65 +729,6 @@ private sealed class RowFactory8 : global::Dapper.RowFactory 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 4245442695U when NormalizedEquals(name, "x"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 4228665076U when NormalizedEquals(name, "y"): - token = type == typeof(string) ? 1 : 4; - break; - case 4278997933U when NormalizedEquals(name, "z"): - token = type == typeof(double) ? 2 : 5; - break; - - } - tokens[i] = token; - columnOffset++; - - } - return null; - } - public override global::Foo.OnlyNonDapperAotCtor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - global::Foo.OnlyNonDapperAotCtor result = new(); - foreach (var token in tokens) - { - switch (token) - { - case 0: - result.X = reader.GetInt32(columnOffset); - break; - case 3: - result.X = GetValue(reader, columnOffset); - break; - case 1: - result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); - break; - case 5: - result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); - break; - - } - columnOffset++; - - } - return result; - - } } @@ -800,66 +736,7 @@ private sealed class RowFactory9 : global::Dapper.RowFactory 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 4245442695U when NormalizedEquals(name, "x"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 4228665076U when NormalizedEquals(name, "y"): - token = type == typeof(string) ? 1 : 4; - break; - case 4278997933U when NormalizedEquals(name, "z"): - token = type == typeof(double) ? 2 : 5; - break; - - } - tokens[i] = token; - columnOffset++; - } - return null; - } - public override global::Foo.SingleDefaultCtor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - int value0 = default; - string? value1 = default; - double? value2 = default; - foreach (var token in tokens) - { - switch (token) - { - case 0: - value0 = reader.GetInt32(columnOffset); - break; - case 3: - value0 = GetValue(reader, columnOffset); - break; - case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); - break; - case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); - break; - - } - columnOffset++; - - } - return new global::Foo.SingleDefaultCtor(value0, value1, value2); - } } private sealed class RowFactory10 : global::Dapper.RowFactory @@ -873,66 +750,7 @@ private sealed class RowFactory11 : global::Dapper.RowFactory 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 4245442695U when NormalizedEquals(name, "x"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 4228665076U when NormalizedEquals(name, "y"): - token = type == typeof(string) ? 1 : 4; - break; - case 4278997933U when NormalizedEquals(name, "z"): - token = type == typeof(double) ? 2 : 5; - break; - } - tokens[i] = token; - columnOffset++; - - } - return null; - } - public override global::Foo.SingleDapperAotCtor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - int value0 = default; - string? value1 = default; - double? value2 = default; - foreach (var token in tokens) - { - switch (token) - { - case 0: - value0 = reader.GetInt32(columnOffset); - break; - case 3: - value0 = GetValue(reader, columnOffset); - break; - case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); - break; - case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); - break; - - } - columnOffset++; - - } - return new global::Foo.SingleDapperAotCtor(value0, value1, value2); - } } diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.netfx.txt index b32c1530..6fefd7e6 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.netfx.txt @@ -1,7 +1,20 @@ -Generator produced 2 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 12 of 12 enabled call-sites using 12 interceptors, 0 commands and 12 readers +Output code has 5 diagnostics from 'Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs': -Error DAP035 Interceptors/QueryCustomConstruction.input.cs L132 C16 -Only one constructor can be Dapper.AOT enabled per type 'Foo.MultipleDapperAotCtors' +Error CS7036 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L319 C24 +There is no argument given that corresponds to the required parameter 'x' of 'Foo.GetOnlyPropertiesViaConstructor.GetOnlyPropertiesViaConstructor(int, string, double?)' + +Error CS1526 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L319 C67 +A new expression requires an argument list or (), [], or {} after type + +Error CS7036 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L451 C24 +There is no argument given that corresponds to the required parameter 'X' of 'Foo.RecordClassSimpleCtor.RecordClassSimpleCtor(int, string, double?)' + +Error CS1526 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L451 C57 +A new expression requires an argument list or (), [], or {} after type + +Error CS7036 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L720 C24 +There is no argument given that corresponds to the required parameter 'y' of 'Foo.InitPropsAndDapperAotCtor.InitPropsAndDapperAotCtor(string)' diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.txt index b32c1530..6fefd7e6 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.txt @@ -1,7 +1,20 @@ -Generator produced 2 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 12 of 12 enabled call-sites using 12 interceptors, 0 commands and 12 readers +Output code has 5 diagnostics from 'Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs': -Error DAP035 Interceptors/QueryCustomConstruction.input.cs L132 C16 -Only one constructor can be Dapper.AOT enabled per type 'Foo.MultipleDapperAotCtors' +Error CS7036 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L319 C24 +There is no argument given that corresponds to the required parameter 'x' of 'Foo.GetOnlyPropertiesViaConstructor.GetOnlyPropertiesViaConstructor(int, string, double?)' + +Error CS1526 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L319 C67 +A new expression requires an argument list or (), [], or {} after type + +Error CS7036 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L451 C24 +There is no argument given that corresponds to the required parameter 'X' of 'Foo.RecordClassSimpleCtor.RecordClassSimpleCtor(int, string, double?)' + +Error CS1526 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L451 C57 +A new expression requires an argument list or (), [], or {} after type + +Error CS7036 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L720 C24 +There is no argument given that corresponds to the required parameter 'y' of 'Foo.InitPropsAndDapperAotCtor.InitPropsAndDapperAotCtor(string)' diff --git a/test/Dapper.AOT.Test/Interceptors/QueryDetection.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/QueryDetection.output.netfx.txt index c32af25c..92fc6f31 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryDetection.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryDetection.output.netfx.txt @@ -1,19 +1,4 @@ -Generator produced 6 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 11 of 11 enabled call-sites using 7 interceptors, 1 commands and 1 readers - -Warning DAP025 Interceptors/QueryDetection.input.cs L12 C12 -The command has a query that will be ignored - -Warning DAP231 Interceptors/QueryDetection.input.cs L17 C35 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP219 Interceptors/QueryDetection.input.cs L17 C42 -SELECT columns should be specified explicitly (L1 C8) - -Error DAP026 Interceptors/QueryDetection.input.cs L21 C12 -The command lacks a query - -Error DAP026 Interceptors/QueryDetection.input.cs L22 C12 -The command lacks a query diff --git a/test/Dapper.AOT.Test/Interceptors/QueryDetection.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryDetection.output.txt index c32af25c..92fc6f31 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryDetection.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryDetection.output.txt @@ -1,19 +1,4 @@ -Generator produced 6 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 11 of 11 enabled call-sites using 7 interceptors, 1 commands and 1 readers - -Warning DAP025 Interceptors/QueryDetection.input.cs L12 C12 -The command has a query that will be ignored - -Warning DAP231 Interceptors/QueryDetection.input.cs L17 C35 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP219 Interceptors/QueryDetection.input.cs L17 C42 -SELECT columns should be specified explicitly (L1 C8) - -Error DAP026 Interceptors/QueryDetection.input.cs L21 C12 -The command lacks a query - -Error DAP026 Interceptors/QueryDetection.input.cs L22 C12 -The command lacks a query diff --git a/test/Dapper.AOT.Test/Interceptors/QueryMultiType.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/QueryMultiType.output.netfx.txt deleted file mode 100644 index b912cba0..00000000 --- a/test/Dapper.AOT.Test/Interceptors/QueryMultiType.output.netfx.txt +++ /dev/null @@ -1,37 +0,0 @@ -Generator produced 12 diagnostics: - -Info DAP001 Interceptors/QueryMultiType.input.cs L12 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L13 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L14 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L15 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L16 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L17 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L19 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L20 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L21 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L22 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L23 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L24 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT diff --git a/test/Dapper.AOT.Test/Interceptors/QueryMultiType.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryMultiType.output.txt deleted file mode 100644 index b912cba0..00000000 --- a/test/Dapper.AOT.Test/Interceptors/QueryMultiType.output.txt +++ /dev/null @@ -1,37 +0,0 @@ -Generator produced 12 diagnostics: - -Info DAP001 Interceptors/QueryMultiType.input.cs L12 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L13 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L14 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L15 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L16 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L17 C24 -The Dapper method 'SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L19 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L20 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L21 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L22 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L23 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiType.input.cs L24 C30 -The Dapper method 'SqlMapper.QueryAsync(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)' is not currently supported by Dapper.AOT diff --git a/test/Dapper.AOT.Test/Interceptors/QueryMultiple.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/QueryMultiple.output.netfx.txt deleted file mode 100644 index 36d0309e..00000000 --- a/test/Dapper.AOT.Test/Interceptors/QueryMultiple.output.netfx.txt +++ /dev/null @@ -1,7 +0,0 @@ -Generator produced 2 diagnostics: - -Info DAP001 Interceptors/QueryMultiple.input.cs L11 C39 -The Dapper method 'SqlMapper.QueryMultiple(IDbConnection, string, object?, IDbTransaction?, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiple.input.cs L18 C45 -The Dapper method 'SqlMapper.QueryMultipleAsync(IDbConnection, string, object?, IDbTransaction?, int?, CommandType?)' is not currently supported by Dapper.AOT diff --git a/test/Dapper.AOT.Test/Interceptors/QueryMultiple.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryMultiple.output.txt deleted file mode 100644 index 36d0309e..00000000 --- a/test/Dapper.AOT.Test/Interceptors/QueryMultiple.output.txt +++ /dev/null @@ -1,7 +0,0 @@ -Generator produced 2 diagnostics: - -Info DAP001 Interceptors/QueryMultiple.input.cs L11 C39 -The Dapper method 'SqlMapper.QueryMultiple(IDbConnection, string, object?, IDbTransaction?, int?, CommandType?)' is not currently supported by Dapper.AOT - -Info DAP001 Interceptors/QueryMultiple.input.cs L18 C45 -The Dapper method 'SqlMapper.QueryMultipleAsync(IDbConnection, string, object?, IDbTransaction?, int?, CommandType?)' is not currently supported by Dapper.AOT diff --git a/test/Dapper.AOT.Test/Interceptors/QueryNonGeneric.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryNonGeneric.output.cs index ea523c9f..7ece3044 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryNonGeneric.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryNonGeneric.output.cs @@ -19,7 +19,7 @@ file static class DapperGeneratedInterceptors { // Query, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(buffered is true); @@ -34,7 +34,7 @@ file static class DapperGeneratedInterceptors { // Query, HasParameters, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -89,7 +89,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -130,7 +130,7 @@ internal static dynamic QueryFirst9(this global::System.Data.IDbConnection cnn, { // Query, HasParameters, SingleRow, StoredProcedure, AtLeastOne, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -144,7 +144,7 @@ internal static dynamic QueryFirst9(this global::System.Data.IDbConnection cnn, { // Query, HasParameters, SingleRow, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -184,7 +184,7 @@ internal static dynamic QuerySingle11(this global::System.Data.IDbConnection cnn { // Query, Async, HasParameters, SingleRow, StoredProcedure, AtLeastOne, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -198,7 +198,7 @@ internal static dynamic QuerySingle11(this global::System.Data.IDbConnection cnn { // Query, Async, HasParameters, SingleRow, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/QueryNonGeneric.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/QueryNonGeneric.output.netfx.cs index 8887ea86..eae2631e 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryNonGeneric.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryNonGeneric.output.netfx.cs @@ -19,7 +19,7 @@ file static class DapperGeneratedInterceptors { // Query, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(buffered is true); @@ -34,7 +34,7 @@ file static class DapperGeneratedInterceptors { // Query, HasParameters, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -89,7 +89,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -104,7 +104,7 @@ internal static dynamic QueryFirst7(this global::System.Data.IDbConnection cnn, { // Query, HasParameters, SingleRow, StoredProcedure, AtLeastOne, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -118,7 +118,7 @@ internal static dynamic QueryFirst7(this global::System.Data.IDbConnection cnn, { // Query, HasParameters, SingleRow, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -158,7 +158,7 @@ internal static dynamic QuerySingle9(this global::System.Data.IDbConnection cnn, { // Query, Async, HasParameters, SingleRow, StoredProcedure, AtLeastOne, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -172,7 +172,7 @@ internal static dynamic QuerySingle9(this global::System.Data.IDbConnection cnn, { // Query, Async, HasParameters, SingleRow, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/QueryPrimitive.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/QueryPrimitive.output.netfx.txt index 57dda07c..4ec711d1 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryPrimitive.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryPrimitive.output.netfx.txt @@ -1,10 +1,4 @@ -Generator produced 3 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 18 of 18 enabled call-sites using 18 interceptors, 0 commands and 0 readers - -Warning DAP038 Interceptors/QueryPrimitive.input.cs L17 C24 -Type 'Int32' is a value-type; it will not be trivial to identify missing rows from QueryFirstOrDefault - -Warning DAP038 Interceptors/QueryPrimitive.input.cs L32 C30 -Type 'Int32' is a value-type; it will not be trivial to identify missing rows from QueryFirstOrDefaultAsync diff --git a/test/Dapper.AOT.Test/Interceptors/QueryPrimitive.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryPrimitive.output.txt index d7fcdaab..bc414dc6 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryPrimitive.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryPrimitive.output.txt @@ -1,10 +1,4 @@ -Generator produced 3 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 21 of 21 enabled call-sites using 21 interceptors, 0 commands and 0 readers - -Warning DAP038 Interceptors/QueryPrimitive.input.cs L17 C24 -Type 'Int32' is a value-type; it will not be trivial to identify missing rows from QueryFirstOrDefault - -Warning DAP038 Interceptors/QueryPrimitive.input.cs L32 C30 -Type 'Int32' is a value-type; it will not be trivial to identify missing rows from QueryFirstOrDefaultAsync diff --git a/test/Dapper.AOT.Test/Interceptors/QueryUntyped.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryUntyped.output.cs index 32ef39f2..633ec2e0 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryUntyped.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryUntyped.output.cs @@ -20,7 +20,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: object global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -36,7 +36,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: object global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -95,7 +95,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: dynamic global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -111,7 +111,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: dynamic global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -169,7 +169,7 @@ file static class DapperGeneratedInterceptors { // Query, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(buffered is true); @@ -184,7 +184,7 @@ file static class DapperGeneratedInterceptors { // Query, HasParameters, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -240,7 +240,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: object global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -298,7 +298,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: dynamic global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -355,7 +355,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/QueryUntyped.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/QueryUntyped.output.netfx.cs index 992b2286..17aabcdc 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryUntyped.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryUntyped.output.netfx.cs @@ -20,7 +20,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: object global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -36,7 +36,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: object global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -95,7 +95,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: dynamic global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -111,7 +111,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: dynamic global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -169,7 +169,7 @@ file static class DapperGeneratedInterceptors { // Query, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(buffered is true); @@ -184,7 +184,7 @@ file static class DapperGeneratedInterceptors { // Query, HasParameters, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); @@ -240,7 +240,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: object global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -270,7 +270,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, TypedResult, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: dynamic global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -299,7 +299,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, HasParameters, Buffered, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(param is not null); diff --git a/test/Dapper.AOT.Test/Interceptors/Single.output.cs b/test/Dapper.AOT.Test/Interceptors/Single.output.cs index ccd584b2..1d44d50a 100644 --- a/test/Dapper.AOT.Test/Interceptors/Single.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/Single.output.cs @@ -6,7 +6,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, SingleRow, StoredProcedure, AtLeastOne, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -21,7 +21,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, SingleRow, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -64,7 +64,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, TypedResult, HasParameters, SingleRow, StoredProcedure, AtLeastOne, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -79,7 +79,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, TypedResult, HasParameters, SingleRow, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); diff --git a/test/Dapper.AOT.Test/Interceptors/Single.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/Single.output.netfx.cs index ccd584b2..1d44d50a 100644 --- a/test/Dapper.AOT.Test/Interceptors/Single.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/Single.output.netfx.cs @@ -6,7 +6,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, SingleRow, StoredProcedure, AtLeastOne, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -21,7 +21,7 @@ file static class DapperGeneratedInterceptors { // Query, TypedResult, HasParameters, SingleRow, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -64,7 +64,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, TypedResult, HasParameters, SingleRow, StoredProcedure, AtLeastOne, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -79,7 +79,7 @@ file static class DapperGeneratedInterceptors { // Query, Async, TypedResult, HasParameters, SingleRow, StoredProcedure, BindResultsByName // takes parameter: - // parameter map: (everything) + // parameter map: bar Foo // returns data: global::Foo.Customer global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); diff --git a/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.netfx.txt index 5856fae6..a8d9748a 100644 --- a/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.netfx.txt @@ -1,16 +1,7 @@ -Generator produced 4 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 7 of 7 enabled call-sites using 5 interceptors, 2 commands and 0 readers - -Warning DAP019 Interceptors/SqlDetection.input.cs L20 C20 -SQL parameters were detected, but no parameters are being supplied - -Warning DAP018 Interceptors/SqlDetection.input.cs L23 C20 -Parameters are being supplied, but no parameters were detected in the command - -Warning DAP018 Interceptors/SqlDetection.input.cs L34 C20 -Parameters are being supplied, but no parameters were detected in the command Output code has 2 diagnostics from 'Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs': Warning CS8604 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L55 C190 diff --git a/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.txt b/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.txt index c2092697..fa9fcf87 100644 --- a/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.txt @@ -1,16 +1,7 @@ -Generator produced 4 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 7 of 7 enabled call-sites using 5 interceptors, 2 commands and 0 readers - -Warning DAP019 Interceptors/SqlDetection.input.cs L20 C20 -SQL parameters were detected, but no parameters are being supplied - -Warning DAP018 Interceptors/SqlDetection.input.cs L23 C20 -Parameters are being supplied, but no parameters were detected in the command - -Warning DAP018 Interceptors/SqlDetection.input.cs L34 C20 -Parameters are being supplied, but no parameters were detected in the command Output code has 1 diagnostics from 'Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs': Warning CS8620 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L68 C190 diff --git a/test/Dapper.AOT.Test/Interceptors/SqlParse.output.cs b/test/Dapper.AOT.Test/Interceptors/SqlParse.output.cs index ac852505..726d7bca 100644 --- a/test/Dapper.AOT.Test/Interceptors/SqlParse.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/SqlParse.output.cs @@ -33,21 +33,9 @@ internal static int Execute1(this global::System.Data.IDbConnection cnn, string } [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\SqlParse.input.cs", 56, 12)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\SqlParse.input.cs", 73, 12)] [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\SqlParse.input.cs", 84, 12)] internal static int Execute2(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) - { - // Execute, HasParameters, Text - // takes parameter: global::Foo.Customer - 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 not null); - - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).Execute((global::Foo.Customer)param!); - - } - - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\SqlParse.input.cs", 73, 12)] - internal static int Execute3(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) { // Execute, HasParameters, Text // takes parameter: global::Foo.Customer @@ -56,7 +44,7 @@ internal static int Execute3(this global::System.Data.IDbConnection cnn, string global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); global::System.Diagnostics.Debug.Assert(param is not null); - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory2.Instance).Execute((global::Foo.Customer)param!); + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).Execute((global::Foo.Customer)param!); } @@ -112,16 +100,9 @@ public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, } - private sealed class CommandFactory1 : CommonCommandFactory + private sealed class CommandFactory1 : CommonCommandFactory { internal static readonly CommandFactory1 Instance = new(); - public override bool CanPrepare => true; - - } - - private sealed class CommandFactory2 : CommonCommandFactory - { - internal static readonly CommandFactory2 Instance = new(); public override void AddParameters(global::System.Data.Common.DbCommand cmd, global::Foo.Customer args) { var ps = cmd.Parameters; diff --git a/test/Dapper.AOT.Test/Interceptors/SqlParse.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/SqlParse.output.netfx.cs index ac852505..726d7bca 100644 --- a/test/Dapper.AOT.Test/Interceptors/SqlParse.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/SqlParse.output.netfx.cs @@ -33,21 +33,9 @@ internal static int Execute1(this global::System.Data.IDbConnection cnn, string } [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\SqlParse.input.cs", 56, 12)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\SqlParse.input.cs", 73, 12)] [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\SqlParse.input.cs", 84, 12)] internal static int Execute2(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) - { - // Execute, HasParameters, Text - // takes parameter: global::Foo.Customer - 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 not null); - - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).Execute((global::Foo.Customer)param!); - - } - - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\SqlParse.input.cs", 73, 12)] - internal static int Execute3(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) { // Execute, HasParameters, Text // takes parameter: global::Foo.Customer @@ -56,7 +44,7 @@ internal static int Execute3(this global::System.Data.IDbConnection cnn, string global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); global::System.Diagnostics.Debug.Assert(param is not null); - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory2.Instance).Execute((global::Foo.Customer)param!); + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).Execute((global::Foo.Customer)param!); } @@ -112,16 +100,9 @@ public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, } - private sealed class CommandFactory1 : CommonCommandFactory + private sealed class CommandFactory1 : CommonCommandFactory { internal static readonly CommandFactory1 Instance = new(); - public override bool CanPrepare => true; - - } - - private sealed class CommandFactory2 : CommonCommandFactory - { - internal static readonly CommandFactory2 Instance = new(); public override void AddParameters(global::System.Data.Common.DbCommand cmd, global::Foo.Customer args) { var ps = cmd.Parameters; diff --git a/test/Dapper.AOT.Test/Interceptors/SqlParse.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/SqlParse.output.netfx.txt index fa1df375..12a606ac 100644 --- a/test/Dapper.AOT.Test/Interceptors/SqlParse.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/SqlParse.output.netfx.txt @@ -1,178 +1,4 @@ -Generator produced 59 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 -Dapper.AOT handled 10 of 12 enabled call-sites using 4 interceptors, 3 commands and 0 readers - -Error DAP206 Interceptors/SqlParse.input.cs L6 C18 -Incorrect syntax near as. (#46010 L1 C24) - -Error DAP206 Interceptors/SqlParse.input.cs L6 C18 -Incorrect syntax near as. (#46010 L1 C24) - -Error DAP206 Interceptors/SqlParse.input.cs L6 C18 -Incorrect syntax near as. (#46010 L1 C24) - -Error DAP214 Interceptors/SqlParse.input.cs L12 C35 -Variable @Missing is not declared and no corresponding parameter exists (L3 C27) - -Error DAP214 Interceptors/SqlParse.input.cs L12 C35 -Variable @Missing is not declared and no corresponding parameter exists (L3 C27) - -Error DAP211 Interceptors/SqlParse.input.cs L18 C5 -Variable @id is accessed before it is declared (L1 C5) - -Error DAP211 Interceptors/SqlParse.input.cs L18 C5 -Variable @id is accessed before it is declared (L1 C5) - -Error DAP211 Interceptors/SqlParse.input.cs L18 C5 -Variable @id is accessed before it is declared (L1 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L18 C5 -Variable @id has a value that is not consumed (L1 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L18 C5 -Variable @id has a value that is not consumed (L1 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L18 C5 -Variable @id has a value that is not consumed (L1 C5) - -Error DAP202 Interceptors/SqlParse.input.cs L21 C9 -The variable @id is declared multiple times (L4 C9) - -Error DAP202 Interceptors/SqlParse.input.cs L21 C9 -The variable @id is declared multiple times (L4 C9) - -Error DAP202 Interceptors/SqlParse.input.cs L21 C9 -The variable @id is declared multiple times (L4 C9) - -Error DAP201 Interceptors/SqlParse.input.cs L24 C1 -Multiple batches are not permitted (L7 C1) - -Error DAP201 Interceptors/SqlParse.input.cs L24 C1 -Multiple batches are not permitted (L7 C1) - -Error DAP201 Interceptors/SqlParse.input.cs L24 C1 -Multiple batches are not permitted (L7 C1) - -Error DAP203 Interceptors/SqlParse.input.cs L24 C26 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L7 C26) - -Error DAP203 Interceptors/SqlParse.input.cs L24 C26 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L7 C26) - -Error DAP203 Interceptors/SqlParse.input.cs L24 C26 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L7 C26) - -Warning DAP219 Interceptors/SqlParse.input.cs L25 C8 -SELECT columns should be specified explicitly (L8 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L25 C8 -SELECT columns should be specified explicitly (L8 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L25 C8 -SELECT columns should be specified explicitly (L8 C8) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C38 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C38) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C38 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C38) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C38 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C38) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C47 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C47) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C47 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C47) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C47 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C47) - -Warning DAP219 Interceptors/SqlParse.input.cs L29 C8 -SELECT columns should be specified explicitly (L12 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L29 C8 -SELECT columns should be specified explicitly (L12 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L29 C8 -SELECT columns should be specified explicitly (L12 C8) - -Error DAP209 Interceptors/SqlParse.input.cs L29 C15 -Table-variable @t is accessed before it populated (L12 C15) - -Error DAP209 Interceptors/SqlParse.input.cs L29 C15 -Table-variable @t is accessed before it populated (L12 C15) - -Error DAP209 Interceptors/SqlParse.input.cs L29 C15 -Table-variable @t is accessed before it populated (L12 C15) - -Warning DAP219 Interceptors/SqlParse.input.cs L31 C8 -SELECT columns should be specified explicitly (L14 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L31 C8 -SELECT columns should be specified explicitly (L14 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L31 C8 -SELECT columns should be specified explicitly (L14 C8) - -Error DAP210 Interceptors/SqlParse.input.cs L36 C8 -Variable @s is accessed before it is assigned a value (L19 C8) - -Error DAP210 Interceptors/SqlParse.input.cs L36 C8 -Variable @s is accessed before it is assigned a value (L19 C8) - -Error DAP210 Interceptors/SqlParse.input.cs L36 C8 -Variable @s is accessed before it is assigned a value (L19 C8) - -Warning DAP213 Interceptors/SqlParse.input.cs L40 C8 -Variable @id has a value that is not consumed (L23 C8) - -Warning DAP213 Interceptors/SqlParse.input.cs L40 C8 -Variable @id has a value that is not consumed (L23 C8) - -Warning DAP213 Interceptors/SqlParse.input.cs L40 C8 -Variable @id has a value that is not consumed (L23 C8) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Error DAP206 Interceptors/SqlParse.input.cs L47 C1 -Incorrect syntax near gibb. (#46010 L30 C1) - -Error DAP206 Interceptors/SqlParse.input.cs L47 C1 -Incorrect syntax near gibb. (#46010 L30 C1) - -Error DAP206 Interceptors/SqlParse.input.cs L47 C1 -Incorrect syntax near gibb. (#46010 L30 C1) - -Warning DAP018 Interceptors/SqlParse.input.cs L56 C12 -Parameters are being supplied, but no parameters were detected in the command - -Info DAP015 Interceptors/SqlParse.input.cs L62 C12 -The parameter type could not be resolved - -Info DAP015 Interceptors/SqlParse.input.cs L64 C12 -The parameter type could not be resolved - -Warning DAP018 Interceptors/SqlParse.input.cs L64 C12 -Parameters are being supplied, but no parameters were detected in the command - -Warning DAP018 Interceptors/SqlParse.input.cs L84 C12 -Parameters are being supplied, but no parameters were detected in the command +Dapper.AOT handled 10 of 12 enabled call-sites using 3 interceptors, 2 commands and 0 readers diff --git a/test/Dapper.AOT.Test/Interceptors/SqlParse.output.txt b/test/Dapper.AOT.Test/Interceptors/SqlParse.output.txt index fa1df375..12a606ac 100644 --- a/test/Dapper.AOT.Test/Interceptors/SqlParse.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/SqlParse.output.txt @@ -1,178 +1,4 @@ -Generator produced 59 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 -Dapper.AOT handled 10 of 12 enabled call-sites using 4 interceptors, 3 commands and 0 readers - -Error DAP206 Interceptors/SqlParse.input.cs L6 C18 -Incorrect syntax near as. (#46010 L1 C24) - -Error DAP206 Interceptors/SqlParse.input.cs L6 C18 -Incorrect syntax near as. (#46010 L1 C24) - -Error DAP206 Interceptors/SqlParse.input.cs L6 C18 -Incorrect syntax near as. (#46010 L1 C24) - -Error DAP214 Interceptors/SqlParse.input.cs L12 C35 -Variable @Missing is not declared and no corresponding parameter exists (L3 C27) - -Error DAP214 Interceptors/SqlParse.input.cs L12 C35 -Variable @Missing is not declared and no corresponding parameter exists (L3 C27) - -Error DAP211 Interceptors/SqlParse.input.cs L18 C5 -Variable @id is accessed before it is declared (L1 C5) - -Error DAP211 Interceptors/SqlParse.input.cs L18 C5 -Variable @id is accessed before it is declared (L1 C5) - -Error DAP211 Interceptors/SqlParse.input.cs L18 C5 -Variable @id is accessed before it is declared (L1 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L18 C5 -Variable @id has a value that is not consumed (L1 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L18 C5 -Variable @id has a value that is not consumed (L1 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L18 C5 -Variable @id has a value that is not consumed (L1 C5) - -Error DAP202 Interceptors/SqlParse.input.cs L21 C9 -The variable @id is declared multiple times (L4 C9) - -Error DAP202 Interceptors/SqlParse.input.cs L21 C9 -The variable @id is declared multiple times (L4 C9) - -Error DAP202 Interceptors/SqlParse.input.cs L21 C9 -The variable @id is declared multiple times (L4 C9) - -Error DAP201 Interceptors/SqlParse.input.cs L24 C1 -Multiple batches are not permitted (L7 C1) - -Error DAP201 Interceptors/SqlParse.input.cs L24 C1 -Multiple batches are not permitted (L7 C1) - -Error DAP201 Interceptors/SqlParse.input.cs L24 C1 -Multiple batches are not permitted (L7 C1) - -Error DAP203 Interceptors/SqlParse.input.cs L24 C26 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L7 C26) - -Error DAP203 Interceptors/SqlParse.input.cs L24 C26 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L7 C26) - -Error DAP203 Interceptors/SqlParse.input.cs L24 C26 -@@identity should not be used; prefer SCOPE_IDENTITY() or OUTPUT INSERTED.yourid (L7 C26) - -Warning DAP219 Interceptors/SqlParse.input.cs L25 C8 -SELECT columns should be specified explicitly (L8 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L25 C8 -SELECT columns should be specified explicitly (L8 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L25 C8 -SELECT columns should be specified explicitly (L8 C8) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C38 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C38) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C38 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C38) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C38 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C38) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C47 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C47) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C47 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C47) - -Warning DAP205 Interceptors/SqlParse.input.cs L25 C47 -Literal null used in comparison; 'is null' or 'is not null' should be preferred (L8 C47) - -Warning DAP219 Interceptors/SqlParse.input.cs L29 C8 -SELECT columns should be specified explicitly (L12 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L29 C8 -SELECT columns should be specified explicitly (L12 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L29 C8 -SELECT columns should be specified explicitly (L12 C8) - -Error DAP209 Interceptors/SqlParse.input.cs L29 C15 -Table-variable @t is accessed before it populated (L12 C15) - -Error DAP209 Interceptors/SqlParse.input.cs L29 C15 -Table-variable @t is accessed before it populated (L12 C15) - -Error DAP209 Interceptors/SqlParse.input.cs L29 C15 -Table-variable @t is accessed before it populated (L12 C15) - -Warning DAP219 Interceptors/SqlParse.input.cs L31 C8 -SELECT columns should be specified explicitly (L14 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L31 C8 -SELECT columns should be specified explicitly (L14 C8) - -Warning DAP219 Interceptors/SqlParse.input.cs L31 C8 -SELECT columns should be specified explicitly (L14 C8) - -Error DAP210 Interceptors/SqlParse.input.cs L36 C8 -Variable @s is accessed before it is assigned a value (L19 C8) - -Error DAP210 Interceptors/SqlParse.input.cs L36 C8 -Variable @s is accessed before it is assigned a value (L19 C8) - -Error DAP210 Interceptors/SqlParse.input.cs L36 C8 -Variable @s is accessed before it is assigned a value (L19 C8) - -Warning DAP213 Interceptors/SqlParse.input.cs L40 C8 -Variable @id has a value that is not consumed (L23 C8) - -Warning DAP213 Interceptors/SqlParse.input.cs L40 C8 -Variable @id has a value that is not consumed (L23 C8) - -Warning DAP213 Interceptors/SqlParse.input.cs L40 C8 -Variable @id has a value that is not consumed (L23 C8) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Warning DAP213 Interceptors/SqlParse.input.cs L45 C5 -Variable @id has a value that is not consumed (L28 C5) - -Error DAP206 Interceptors/SqlParse.input.cs L47 C1 -Incorrect syntax near gibb. (#46010 L30 C1) - -Error DAP206 Interceptors/SqlParse.input.cs L47 C1 -Incorrect syntax near gibb. (#46010 L30 C1) - -Error DAP206 Interceptors/SqlParse.input.cs L47 C1 -Incorrect syntax near gibb. (#46010 L30 C1) - -Warning DAP018 Interceptors/SqlParse.input.cs L56 C12 -Parameters are being supplied, but no parameters were detected in the command - -Info DAP015 Interceptors/SqlParse.input.cs L62 C12 -The parameter type could not be resolved - -Info DAP015 Interceptors/SqlParse.input.cs L64 C12 -The parameter type could not be resolved - -Warning DAP018 Interceptors/SqlParse.input.cs L64 C12 -Parameters are being supplied, but no parameters were detected in the command - -Warning DAP018 Interceptors/SqlParse.input.cs L84 C12 -Parameters are being supplied, but no parameters were detected in the command +Dapper.AOT handled 10 of 12 enabled call-sites using 3 interceptors, 2 commands and 0 readers diff --git a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.cs b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.cs index e6430ca9..8e15c5ef 100644 --- a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.cs @@ -102,7 +102,6 @@ internal static dynamic QuerySingle5(this global::System.Data.IDbConnection cnn, { // Query, HasParameters, SingleRow, Text, AtLeastOne, AtMostOne, BindResultsByName // takes parameter: - // parameter map: a 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 not null); @@ -303,29 +302,9 @@ public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, } - private sealed class CommandFactory2 : CommonCommandFactory // + private sealed class CommandFactory2 : CommonCommandFactory { internal static readonly CommandFactory2 Instance = new(); - public override void AddParameters(global::System.Data.Common.DbCommand cmd, object? args) - { - var typed = Cast(args, static () => new { a = default(int), b = default(string)! }); // expected shape - var ps = cmd.Parameters; - global::System.Data.Common.DbParameter p; - p = cmd.CreateParameter(); - p.ParameterName = "a"; - p.DbType = global::System.Data.DbType.Int32; - p.Direction = global::System.Data.ParameterDirection.Input; - p.Value = AsValue(typed.a); - ps.Add(p); - - } - public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, object? args) - { - var typed = Cast(args, static () => new { a = default(int), b = default(string)! }); // expected shape - var ps = cmd.Parameters; - ps[0].Value = AsValue(typed.a); - - } public override bool CanPrepare => true; } diff --git a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.cs index e6430ca9..8e15c5ef 100644 --- a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.cs @@ -102,7 +102,6 @@ internal static dynamic QuerySingle5(this global::System.Data.IDbConnection cnn, { // Query, HasParameters, SingleRow, Text, AtLeastOne, AtMostOne, BindResultsByName // takes parameter: - // parameter map: a 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 not null); @@ -303,29 +302,9 @@ public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, } - private sealed class CommandFactory2 : CommonCommandFactory // + private sealed class CommandFactory2 : CommonCommandFactory { internal static readonly CommandFactory2 Instance = new(); - public override void AddParameters(global::System.Data.Common.DbCommand cmd, object? args) - { - var typed = Cast(args, static () => new { a = default(int), b = default(string)! }); // expected shape - var ps = cmd.Parameters; - global::System.Data.Common.DbParameter p; - p = cmd.CreateParameter(); - p.ParameterName = "a"; - p.DbType = global::System.Data.DbType.Int32; - p.Direction = global::System.Data.ParameterDirection.Input; - p.Value = AsValue(typed.a); - ps.Add(p); - - } - public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, object? args) - { - var typed = Cast(args, static () => new { a = default(int), b = default(string)! }); // expected shape - var ps = cmd.Parameters; - ps[0].Value = AsValue(typed.a); - - } public override bool CanPrepare => true; } diff --git a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.txt index ffff23f7..747b399d 100644 --- a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.txt @@ -1,145 +1,8 @@ -Generator produced 48 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 54 of 56 enabled call-sites using 10 interceptors, 3 commands and 1 readers +Output code has 1 diagnostics from 'Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs': -Error DAP228 Interceptors/TsqlTips.input.cs L12 C34 -TOP literals should be positive (L1 C8) - -Error DAP227 Interceptors/TsqlTips.input.cs L14 C34 -TOP literals should be integers (L1 C8) - -Error DAP206 Interceptors/TsqlTips.input.cs L19 C38 -Incorrect syntax near -. (#46010 L1 C12) - -Warning DAP229 Interceptors/TsqlTips.input.cs L28 C32 -SELECT for First* should use TOP 1 (L1 C1) - -Warning DAP230 Interceptors/TsqlTips.input.cs L29 C33 -SELECT for Single* should use TOP 2; if you do not need to test over-read, use First* (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L31 C32 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L32 C33 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L34 C32 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L35 C33 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L36 C32 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L37 C33 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP226 Interceptors/TsqlTips.input.cs L40 C44 -FROM expressions with multiple elements should qualify all columns; it is unclear where 'B' is located (L1 C13) - -Warning DAP226 Interceptors/TsqlTips.input.cs L43 C39 -FROM expressions with multiple elements should qualify all columns; it is unclear where 'A' is located (L1 C8) - -Warning DAP226 Interceptors/TsqlTips.input.cs L43 C42 -FROM expressions with multiple elements should qualify all columns; it is unclear where 'B' is located (L1 C11) - -Warning DAP225 Interceptors/TsqlTips.input.cs L43 C72 -FROM expressions with multiple elements should use aliases (L1 C41) - -Warning DAP226 Interceptors/TsqlTips.input.cs L43 C86 -FROM expressions with multiple elements should qualify all columns; it is unclear where 'FromSomewhere' is located (L1 C55) - -Warning DAP216 Interceptors/TsqlTips.input.cs L53 C29 -INSERT should explicitly specify target columns (L1 C1) - -Error DAP217 Interceptors/TsqlTips.input.cs L57 C56 -The INSERT values do not match the target columns (L1 C28) - -Error DAP218 Interceptors/TsqlTips.input.cs L59 C53 -The INSERT rows have different widths (L1 C25) - -Warning DAP231 Interceptors/TsqlTips.input.cs L64 C37 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP219 Interceptors/TsqlTips.input.cs L64 C44 -SELECT columns should be specified explicitly (L1 C8) - -Warning DAP231 Interceptors/TsqlTips.input.cs L67 C37 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP219 Interceptors/TsqlTips.input.cs L67 C44 -SELECT columns should be specified explicitly (L1 C8) - -Warning DAP231 Interceptors/TsqlTips.input.cs L70 C37 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP220 Interceptors/TsqlTips.input.cs L79 C38 -SELECT column name is missing: 0 (L1 C8) - -Warning DAP221 Interceptors/TsqlTips.input.cs L86 C47 -SELECT column name is duplicated: 'Balance' (L1 C17) - -Warning DAP222 Interceptors/TsqlTips.input.cs L98 C13 -SELECT statement assigns variable and performs reads (L2 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L103 C46 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP220 Interceptors/TsqlTips.input.cs L103 C53 -SELECT column name is missing: 0 (L1 C8) - -Warning DAP231 Interceptors/TsqlTips.input.cs L105 C41 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Info DAP013 Interceptors/TsqlTips.input.cs L111 C24 -Tuple-type results are not currently supported - -Warning DAP231 Interceptors/TsqlTips.input.cs L111 C52 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP220 Interceptors/TsqlTips.input.cs L111 C59 -SELECT column name is missing: 0 (L1 C8) - -Info DAP013 Interceptors/TsqlTips.input.cs L117 C24 -Tuple-type results are not currently supported - -Warning DAP231 Interceptors/TsqlTips.input.cs L117 C48 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP025 Interceptors/TsqlTips.input.cs L123 C20 -The command has a query that will be ignored - -Warning DAP225 Interceptors/TsqlTips.input.cs L149 C36 -FROM expressions with multiple elements should use aliases (L1 C8) - -Warning DAP223 Interceptors/TsqlTips.input.cs L152 C29 -DELETE statement lacks WHERE clause (L1 C1) - -Warning DAP224 Interceptors/TsqlTips.input.cs L153 C29 -UPDATE statement lacks WHERE clause (L1 C1) - -Warning DAP225 Interceptors/TsqlTips.input.cs L155 C36 -FROM expressions with multiple elements should use aliases (L1 C8) - -Warning DAP230 Interceptors/TsqlTips.input.cs L158 C43 -SELECT for Single* should use TOP 2; if you do not need to test over-read, use First* (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L158 C43 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L159 C43 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP230 Interceptors/TsqlTips.input.cs L161 C43 -SELECT for Single* should use TOP 2; if you do not need to test over-read, use First* (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L161 C43 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L163 C43 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP019 Interceptors/TsqlTips.input.cs L173 C17 -SQL parameters were detected, but no parameters are being supplied +Warning CS8604 Dapper.AOT.Analyzers/Dapper.CodeAnalysis.DapperInterceptorGenerator/Test.generated.cs L109 C194 +Possible null reference argument for parameter 'args' in 'dynamic Command.QuerySingle(object args, RowFactory? rowFactory = null)'. diff --git a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.txt b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.txt index ffff23f7..dfbb2d2d 100644 --- a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.txt @@ -1,145 +1,4 @@ -Generator produced 48 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 54 of 56 enabled call-sites using 10 interceptors, 3 commands and 1 readers - -Error DAP228 Interceptors/TsqlTips.input.cs L12 C34 -TOP literals should be positive (L1 C8) - -Error DAP227 Interceptors/TsqlTips.input.cs L14 C34 -TOP literals should be integers (L1 C8) - -Error DAP206 Interceptors/TsqlTips.input.cs L19 C38 -Incorrect syntax near -. (#46010 L1 C12) - -Warning DAP229 Interceptors/TsqlTips.input.cs L28 C32 -SELECT for First* should use TOP 1 (L1 C1) - -Warning DAP230 Interceptors/TsqlTips.input.cs L29 C33 -SELECT for Single* should use TOP 2; if you do not need to test over-read, use First* (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L31 C32 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L32 C33 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L34 C32 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L35 C33 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L36 C32 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L37 C33 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP226 Interceptors/TsqlTips.input.cs L40 C44 -FROM expressions with multiple elements should qualify all columns; it is unclear where 'B' is located (L1 C13) - -Warning DAP226 Interceptors/TsqlTips.input.cs L43 C39 -FROM expressions with multiple elements should qualify all columns; it is unclear where 'A' is located (L1 C8) - -Warning DAP226 Interceptors/TsqlTips.input.cs L43 C42 -FROM expressions with multiple elements should qualify all columns; it is unclear where 'B' is located (L1 C11) - -Warning DAP225 Interceptors/TsqlTips.input.cs L43 C72 -FROM expressions with multiple elements should use aliases (L1 C41) - -Warning DAP226 Interceptors/TsqlTips.input.cs L43 C86 -FROM expressions with multiple elements should qualify all columns; it is unclear where 'FromSomewhere' is located (L1 C55) - -Warning DAP216 Interceptors/TsqlTips.input.cs L53 C29 -INSERT should explicitly specify target columns (L1 C1) - -Error DAP217 Interceptors/TsqlTips.input.cs L57 C56 -The INSERT values do not match the target columns (L1 C28) - -Error DAP218 Interceptors/TsqlTips.input.cs L59 C53 -The INSERT rows have different widths (L1 C25) - -Warning DAP231 Interceptors/TsqlTips.input.cs L64 C37 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP219 Interceptors/TsqlTips.input.cs L64 C44 -SELECT columns should be specified explicitly (L1 C8) - -Warning DAP231 Interceptors/TsqlTips.input.cs L67 C37 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP219 Interceptors/TsqlTips.input.cs L67 C44 -SELECT columns should be specified explicitly (L1 C8) - -Warning DAP231 Interceptors/TsqlTips.input.cs L70 C37 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP220 Interceptors/TsqlTips.input.cs L79 C38 -SELECT column name is missing: 0 (L1 C8) - -Warning DAP221 Interceptors/TsqlTips.input.cs L86 C47 -SELECT column name is duplicated: 'Balance' (L1 C17) - -Warning DAP222 Interceptors/TsqlTips.input.cs L98 C13 -SELECT statement assigns variable and performs reads (L2 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L103 C46 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP220 Interceptors/TsqlTips.input.cs L103 C53 -SELECT column name is missing: 0 (L1 C8) - -Warning DAP231 Interceptors/TsqlTips.input.cs L105 C41 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Info DAP013 Interceptors/TsqlTips.input.cs L111 C24 -Tuple-type results are not currently supported - -Warning DAP231 Interceptors/TsqlTips.input.cs L111 C52 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP220 Interceptors/TsqlTips.input.cs L111 C59 -SELECT column name is missing: 0 (L1 C8) - -Info DAP013 Interceptors/TsqlTips.input.cs L117 C24 -Tuple-type results are not currently supported - -Warning DAP231 Interceptors/TsqlTips.input.cs L117 C48 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP025 Interceptors/TsqlTips.input.cs L123 C20 -The command has a query that will be ignored - -Warning DAP225 Interceptors/TsqlTips.input.cs L149 C36 -FROM expressions with multiple elements should use aliases (L1 C8) - -Warning DAP223 Interceptors/TsqlTips.input.cs L152 C29 -DELETE statement lacks WHERE clause (L1 C1) - -Warning DAP224 Interceptors/TsqlTips.input.cs L153 C29 -UPDATE statement lacks WHERE clause (L1 C1) - -Warning DAP225 Interceptors/TsqlTips.input.cs L155 C36 -FROM expressions with multiple elements should use aliases (L1 C8) - -Warning DAP230 Interceptors/TsqlTips.input.cs L158 C43 -SELECT for Single* should use TOP 2; if you do not need to test over-read, use First* (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L158 C43 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L159 C43 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP230 Interceptors/TsqlTips.input.cs L161 C43 -SELECT for Single* should use TOP 2; if you do not need to test over-read, use First* (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L161 C43 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP231 Interceptors/TsqlTips.input.cs L163 C43 -SELECT for single row without WHERE or (TOP and ORDER BY) (L1 C1) - -Warning DAP019 Interceptors/TsqlTips.input.cs L173 C17 -SQL parameters were detected, but no parameters are being supplied diff --git a/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs b/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs index b58d6cce..af11cccc 100644 --- a/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs +++ b/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.CodeAnalysis.Operations; using Dapper.TestCommon; +using static Dapper.Internal.Inspection; namespace Dapper.Internal.Roslyn { @@ -20,9 +21,9 @@ public void CheckTypeUsage_WithCustomConstructor() var argumentOperation = GetInvocationArgumentOperation(text); var typeSymbol = GetConversionTypeSymbol(argumentOperation); - var result = typeSymbol.TryGetConstructors(out var constructors); - Assert.True(result); - Assert.Single(constructors!); + var result = ChooseConstructor(typeSymbol, out var ctor); + Assert.Equal(ConstructorResult.SuccessSingleImplicit, result); + Assert.NotNull(ctor); } [Fact] diff --git a/test/Dapper.AOT.Test/SqlTests.cs b/test/Dapper.AOT.Test/SqlTests.cs index 2423d3c6..f73b02a3 100644 --- a/test/Dapper.AOT.Test/SqlTests.cs +++ b/test/Dapper.AOT.Test/SqlTests.cs @@ -1,5 +1,4 @@ using Dapper.Internal; -using System; using Xunit; namespace Dapper.AOT.Test; @@ -20,14 +19,10 @@ from SomeTable """, "x y z")] [InlineData(""" do thing; - return 12; - """, "", "Return")] - public void DetectParameters(string sql, string expected, string flags = "") + """, "")] + public void DetectParameters(string sql, string expected) { - var expectedFlags = string.IsNullOrWhiteSpace(flags) ? ParseFlags.None - : (ParseFlags)Enum.Parse(typeof(ParseFlags), flags); - Assert.Equal(expected, string.Join(" ", SqlTools.GetParameters(sql, out var actualFlags))); - Assert.Equal(expectedFlags, actualFlags); + Assert.Equal(expected, string.Join(" ", SqlTools.GetParameters(sql))); } diff --git a/test/Dapper.AOT.Test/TSqlParserTests.cs b/test/Dapper.AOT.Test/TSqlParserTests.cs index 8a4e2eec..b746e810 100644 --- a/test/Dapper.AOT.Test/TSqlParserTests.cs +++ b/test/Dapper.AOT.Test/TSqlParserTests.cs @@ -16,7 +16,7 @@ public TSqlParserTests(ITestOutputHelper log) private readonly Action _log; - private TestTSqlProcessor GetProcessor() => new(log: _log); + private TestTSqlProcessor GetProcessor(SqlParseInputFlags flags = SqlParseInputFlags.None) => new(flags, log: _log); [Fact] public void DetectBadNullLiteralUsage() @@ -33,9 +33,8 @@ select @s var args = parser.GetParameters(out var errors); Assert.Empty(args); - Assert.Equal(2, errors.Length); - Assert.Equal("Null literals should not be used in binary comparisons; prefer 'is null' and 'is not null' L4 C12", errors[0]); - Assert.Equal("Null literals should not be used in binary comparisons; prefer 'is null' and 'is not null' L4 C29", errors[1]); + Assert.Equal(["Null literals should not be used in binary comparisons; prefer 'is null' and 'is not null' L4 C12", + "Null literals should not be used in binary comparisons; prefer 'is null' and 'is not null' L4 C29"], errors); } [Fact] @@ -322,9 +321,10 @@ declare @sql varchar(8000) = concat('select top 5 * from Sales.Store where Busin var args = parser.GetParameters(out var errors); Assert.Equal("@id", Assert.Single(args)); - Assert.Equal(2, errors.Length); - Assert.Equal("EXEC with composed SQL may be susceptible to SQL injection; consider EXEC sp_executesql with parameters L2 C1", errors[0]); - Assert.Equal("Multiple batches are not permitted L4 C1", errors[1]); + Assert.Equal([ + "EXEC with composed SQL may be susceptible to SQL injection; consider EXEC sp_executesql with parameters L2 C1", + "Multiple batches are not permitted L4 C1", + ], errors); } [Fact] @@ -359,7 +359,7 @@ goto label public void HandleParameterAssignment(ParameterDirection direction) { - var parser = GetProcessor(); + var parser = GetProcessor(SqlParseInputFlags.KnownParameters); parser.AddParameter("@a", direction); // consumed before assign parser.AddParameter("@b", direction); // assigned, consumed parser.AddParameter("@c", direction); // assigned, not consumed @@ -404,7 +404,7 @@ print @b class TestTSqlProcessor : TSqlProcessor { - public TestTSqlProcessor(ModeFlags flags = ModeFlags.None, Action? log = null) : base(flags, log) { } + public TestTSqlProcessor(SqlParseInputFlags flags = SqlParseInputFlags.None, Action? log = null) : base(flags, log) { } public string[] GetParameters(out string[] errors) { var parameters = (from p in this.Variables diff --git a/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs b/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs index a450405d..301cd6f5 100644 --- a/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs +++ b/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs @@ -44,7 +44,7 @@ public void OnCompilationEnd(CompilationAnalysisContext context) public void OnOperation(OperationAnalysisContext context) { if (!inner.PreFilter(context.Operation.Syntax, context.CancellationToken)) return; - var state = new ParseState(ParseContextProxy.Create(context)); + var state = new ParseState(context); var parsed = inner.Parse(state); if (parsed is not null) { diff --git a/test/Dapper.AOT.Test/Verifiers/DAP001.cs b/test/Dapper.AOT.Test/Verifiers/DAP001.cs new file mode 100644 index 00000000..fabc72fc --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP001.cs @@ -0,0 +1,27 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP001 : Verifier +{ + [Fact] + public Task UnsupportedMethod() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot(true)] + class SomeCode + { + public void Foo(DbConnection conn) + { + _ = conn.{|#0:Query|}("proc", null!); + _ = conn.Query("proc"); + } + } + """, DefaultConfig, + [Diagnostic(Diagnostics.UnsupportedMethod).WithLocation(0) + .WithArguments("SqlMapper.Query(IDbConnection, string, Func, object?, IDbTransaction?, bool, string, int?, CommandType?)")]); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP004.cs b/test/Dapper.AOT.Test/Verifiers/DAP004.cs index 2ea0046b..74f56ec2 100644 --- a/test/Dapper.AOT.Test/Verifiers/DAP004.cs +++ b/test/Dapper.AOT.Test/Verifiers/DAP004.cs @@ -9,34 +9,43 @@ namespace Dapper.AOT.Test.Verifiers; public class DAP004 : Verifier { [Fact] - public Task LanguageTooLow() => VerifyAsync(""" -using Dapper; -using System.Data.Common; + public Task LanguageTooLow() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; -[DapperAot(true)] -class SomeCode -{ - public void Foo(DbConnection conn) => conn.Execute("some sql"); -} -""", new[] - { - InterceptorsEnabled, WithLanguageVersion(LanguageVersion.CSharp10) - }, Diagnostic(DapperInterceptorGenerator.Diagnostics.LanguageVersionTooLow)); + [DapperAot(true)] + class SomeCode + { + public void Foo(DbConnection conn) => conn.Execute("some sql"); + } + """, + [InterceptorsEnabled, WithCSharpLanguageVersion(LanguageVersion.CSharp10)], + [Diagnostic(DapperInterceptorGenerator.Diagnostics.LanguageVersionTooLow)]); [Fact] - public Task FineIfInactive() => VerifyAsync(""" -using Dapper; -using System.Data.Common; + public Task CSFineIfInactive() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; -[DapperAot(false)] -class SomeCode -{ - public void Foo(DbConnection conn) => conn.Execute("some sql"); -} -""", new[] - { - InterceptorsEnabled, WithLanguageVersion(LanguageVersion.CSharp10) - }); + [DapperAot(false)] + class SomeCode + { + public void Foo(DbConnection conn) => conn.Execute("some sql"); + } + """, [InterceptorsEnabled, WithCSharpLanguageVersion(LanguageVersion.CSharp10)], []); + + [Fact] + public Task VBFineIfInactive() => VBVerifyAsync(""" + Imports Dapper + Imports System.Data.Common + + ' [DapperAot(false)] + Module SomeCode + Public Sub Foo(conn As DbConnection) + conn.Execute("some sql") + End Sub + End Module + """, [InterceptorsEnabled, WithCSharpLanguageVersion(LanguageVersion.CSharp10)], []); // we don't need to test higher-level language versions: *every other test does that!* } \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP005.cs b/test/Dapper.AOT.Test/Verifiers/DAP005.cs index e76d54ab..e395658e 100644 --- a/test/Dapper.AOT.Test/Verifiers/DAP005.cs +++ b/test/Dapper.AOT.Test/Verifiers/DAP005.cs @@ -1,55 +1,73 @@ -using Dapper.AOT.Test.TestCommon; -using Dapper.CodeAnalysis; +using Dapper.CodeAnalysis; using System.Threading.Tasks; using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; namespace Dapper.AOT.Test.Verifiers; -public class DAP005 : Verifier +public class DAP005 : Verifier { [Fact] - public Task ShouldFlagWhenUsedAndNotAttrib() => VerifyAsync(""" -using Dapper; -using System.Data.Common; + public Task ShouldFlagWhenUsedAndNotAttrib() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; -class SomeCode -{ - public void Foo(DbConnection conn) => conn.Execute("some sql"); -} -""", Diagnostic(DapperInterceptorGenerator.Diagnostics.DapperAotNotEnabled)); + class SomeCode + { + public void Foo(DbConnection conn) + { + conn.{|#0:Execute|}("somesql"); + conn.{|#1:Execute|}("somesql"); // to avoid clutter: only one diagnostic emitted (multiple locations) + } + } + """, DefaultConfig, [Diagnostic(Diagnostics.DapperAotNotEnabled).WithLocation(0).WithLocation(1).WithArguments(2)]); [Fact] - public Task ShouldNotFlagWhenNotUsedAndNoAttrib() => VerifyAsync(""" -using Dapper; -using System.Data.Common; + public Task ShouldNotFlagWhenNotUsedAndNoAttrib() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; -class SomeCode -{ - public void Foo(DbConnection conn) {} -} -"""); + class SomeCode + { + public void Foo(DbConnection conn) {} + } + """, DefaultConfig, []); [Fact] - public Task ShouldNotFlagWhenUsedAndOptedOut() => VerifyAsync(""" -using Dapper; -using System.Data.Common; + public Task ShouldNotFlagWhenUsedAnywhere() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; -[DapperAot(false)] -class SomeCode -{ - public void Foo(DbConnection conn) => conn.Execute("some sql"); -} -"""); + class SomeCode + { + [DapperAot] + public void Foo(DbConnection conn) => conn.Execute("somesql"); + + public void Bar(DbConnection conn) => conn.Execute("somesql"); + } + """, DefaultConfig, []); [Fact] - public Task ShouldNotFlagWhenUsedAndOptedIn() => VerifyAsync(""" -using Dapper; -using System.Data.Common; + public Task ShouldNotFlagWhenUsedAndOptedOut() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; -[DapperAot(true)] -class SomeCode -{ - public void Foo(DbConnection conn) => conn.Execute("some sql"); -} -""", InterceptorsGenerated(1, 1, 1, 0, 0)); + [DapperAot(false)] + class SomeCode + { + public void Foo(DbConnection conn) => conn.Execute("somesql"); + } + """, DefaultConfig, []); + + [Fact] + public Task ShouldNotFlagWhenUsedAndOptedIn() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot(true)] + class SomeCode + { + public void Foo(DbConnection conn) => conn.Execute("somesql"); + } + """, DefaultConfig, []); } \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP006.cs b/test/Dapper.AOT.Test/Verifiers/DAP006.cs new file mode 100644 index 00000000..fea5d5a4 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP006.cs @@ -0,0 +1,26 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP006 : Verifier +{ + [Fact] + public Task DapperLegacyTupleParameter() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + class SomeCode + { + [DapperAot(false)] + public void LegacyMode(DbConnection conn) + { + conn.Execute("somesql", {|#0:(id: 42, name: "abc")|}); + conn.Execute("somesql", new { id = 42, name = "abc" }); + } + } + """, DefaultConfig, [Diagnostic(Diagnostics.DapperLegacyTupleParameter).WithLocation(0)]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP007.cs b/test/Dapper.AOT.Test/Verifiers/DAP007.cs new file mode 100644 index 00000000..409630f9 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP007.cs @@ -0,0 +1,33 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP007 : Verifier +{ + [Fact] + public Task UnexpectedCommandType() => CSVerifyAsync(""" + using Dapper; + using System.Data; + using System.Data.Common; + + [DapperAot] + class SomeCode + { + public void SpecifiedButInvalid(DbConnection conn) + { + conn.Execute("somesql", {|#0:commandType: (CommandType)42|}); + } + public void ExplicitCorrect(DbConnection conn) + { + conn.Execute("somesql", commandType: CommandType.StoredProcedure); + } + public void NotSpecified(DbConnection conn) + { + conn.Execute("somesql"); + } + } + """, DefaultConfig, [Diagnostic(Diagnostics.UnexpectedCommandType).WithLocation(0)]); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP011.cs b/test/Dapper.AOT.Test/Verifiers/DAP011.cs new file mode 100644 index 00000000..38d8d6c7 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP011.cs @@ -0,0 +1,45 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP013 : Verifier +{ + [Fact] + public Task DapperAotTupleResults() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot(true)] + class SomeCode + { + [BindTupleByName(false)] + public void EnabledWithoutNames(DbConnection conn) + { + _ = conn.{|#0:Query<(string,int)>|}("somesql"); + } + [BindTupleByName(true)] + public void EnabledWithNames(DbConnection conn) + { + _ = conn.{|#1:Query<(string s,int a)>|}("somesql"); + } + [DapperAot(false)] + public void UseVanillaDapper(DbConnection conn) + { + _ = conn.Query<(string,int)>("somesql"); + } + public void UseRecordStruct(DbConnection conn) + { + _ = conn.Query("somesql"); + } + public readonly record struct MyData(string name, int id); + } + namespace System.Runtime.CompilerServices + { + static file class IsExternalInit {} + } + """, DefaultConfig, [Diagnostic(Diagnostics.DapperAotTupleResults).WithLocation(0), + Diagnostic(Diagnostics.DapperAotTupleResults).WithLocation(1)]); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP012.cs b/test/Dapper.AOT.Test/Verifiers/DAP012.cs new file mode 100644 index 00000000..0d7ab8f4 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP012.cs @@ -0,0 +1,37 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP012 : Verifier +{ + [Fact] + public Task DapperAotAddBindTupleByName() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + #pragma warning disable DAP014 // feature not supported yet + [DapperAot(true)] + class SomeCode + { + public void Unclear(DbConnection conn) + { + conn.Execute("somesql", {|#0:(id: 42, name: "abc")|}); + } + [BindTupleByName] + public void Explicit(DbConnection conn) + { + conn.Execute("somesql", (id: 42, name: "abc")); + } + [BindTupleByName(false)] + public void ExplicitOff(DbConnection conn) + { + conn.Execute("somesql", (id: 42, name: "abc")); + } + } + #pragma warning restore DAP014 + """, DefaultConfig, [Diagnostic(Diagnostics.DapperAotAddBindTupleByName).WithLocation(0)]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP013.cs b/test/Dapper.AOT.Test/Verifiers/DAP013.cs new file mode 100644 index 00000000..3dc63e09 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP013.cs @@ -0,0 +1,35 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP011 : Verifier +{ + [Fact] + public Task DapperLegacyTupleParameter() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot(false)] + class SomeCode + { + [BindTupleByName] + public void EnabledWithoutNames(DbConnection conn) + { + _ = conn.Query<(string,int)>("somesql"); + } + [BindTupleByName] + public void EnabledWithNames(DbConnection conn) + { + _ = conn.{|#0:Query<(string s,int a)>|}("somesql"); + } + public void NotEnabled(DbConnection conn) + { + _ = conn.Query<(string,int)>("somesql"); + } + } + """, DefaultConfig, [Diagnostic(Diagnostics.DapperLegacyBindNameTupleResults).WithLocation(0)]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP014.cs b/test/Dapper.AOT.Test/Verifiers/DAP014.cs new file mode 100644 index 00000000..0b5b1fb8 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP014.cs @@ -0,0 +1,29 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP014 : Verifier +{ + [Fact] + public Task DapperAotTupleParameter() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot, BindTupleByName] + class SomeCode + { + public void NotSupported(DbConnection conn) + { + conn.Execute("somesql", {|#0:(id: 42, name: "abc")|}); + } + public void Workaround(DbConnection conn) + { + conn.Execute("somesql", new { id = 42, name = "abc" }); + } + } + """, DefaultConfig, [Diagnostic(Diagnostics.DapperAotTupleParameter).WithLocation(0)]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP015.cs b/test/Dapper.AOT.Test/Verifiers/DAP015.cs new file mode 100644 index 00000000..1fd28f19 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP015.cs @@ -0,0 +1,33 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP015 : Verifier +{ + [Fact] + public Task UntypedParameter() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot] + class SomeCode + { + public void WithObject(DbConnection conn, object args) => conn.Execute("somesql", {|#0:args|}); + public void WithDynamicParameters(DbConnection conn, DynamicParameters args) => conn.Execute("somesql", {|#1:args|}); + public void WithCustomType(DbConnection conn, Customer args) => conn.Execute("somesql", args); + + [DapperAot(false)] + public void DisabledWithObject(DbConnection conn, object args) => conn.Execute("somesql", args); + [DapperAot(false)] + public void DisabledWithDynamicParameters(DbConnection conn, DynamicParameters args) => conn.Execute("somesql", args); + + public class Customer {} + } + """, DefaultConfig, [ + Diagnostic(Diagnostics.UntypedParameter).WithLocation(0), + Diagnostic(Diagnostics.UntypedParameter).WithLocation(1)]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP016.cs b/test/Dapper.AOT.Test/Verifiers/DAP016.cs new file mode 100644 index 00000000..72fa2cab --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP016.cs @@ -0,0 +1,34 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP016 : Verifier +{ + [Fact] + public Task UntypedParameter() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot] + class GenericType + { + void WithTypeArg(DbConnection conn, TX args) => conn.Execute("somesql", {|#0:args|}); + } + [DapperAot] + class NonGenericType + { + void WithMethodArg(DbConnection conn, TY args) => conn.Execute("somesql", {|#1:args|}); + class InnerGenercType + { + void WithInnerTypeArg(DbConnection conn, TZ args) => conn.Execute("somesql", {|#2:args|}); + } + } + """, DefaultConfig, [ + Diagnostic(Diagnostics.GenericTypeParameter).WithLocation(0).WithArguments("TX"), + Diagnostic(Diagnostics.GenericTypeParameter).WithLocation(1).WithArguments("TY"), + Diagnostic(Diagnostics.GenericTypeParameter).WithLocation(2).WithArguments("TZ")]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP017.cs b/test/Dapper.AOT.Test/Verifiers/DAP017.cs new file mode 100644 index 00000000..906a1450 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP017.cs @@ -0,0 +1,83 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP017 : Verifier +{ + [Fact] + public Task NonPublicType() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + internal class NestedInternal + { + public class InnerPublic {} + + [DapperAot] + class AotEnabled + { + void ArgsA(DbConnection conn, OuterPublic args) => conn.Execute("somesql", args); + void ArgsB(DbConnection conn, OuterInternal args) => conn.Execute("somesql", args); + void ArgsC(DbConnection conn, NestedInternal.InnerPublic args) => conn.Execute("somesql", args); + void ArgsD(DbConnection conn, NestedInternal.InnerPrivate args) => conn.Execute("somesql", {|#0:args|}); + void ArgsE(DbConnection conn, NestedInternal.InnerPrivate.InnerInnerPublic args) => conn.Execute("somesql", {|#1:args|}); + void ArgsF(DbConnection conn, NestedInternal.InnerProtected args) => conn.Execute("somesql", {|#2:args|}); + void ArgsG(DbConnection conn, NestedInternal.InnerProtectedInternal args) => conn.Execute("somesql", args); + void ArgsH(DbConnection conn, NestedInternal.InnerPrivateProtected args) => conn.Execute("somesql", {|#3:args|}); + + void QueryA(DbConnection conn) => _ = conn.Query("somesql"); + void QueryB(DbConnection conn) => _ = conn.Query("somesql"); + void QueryC(DbConnection conn) => _ = conn.Query("somesql"); + void QueryD(DbConnection conn) => _ = conn.{|#4:Query|}("somesql"); + void QueryE(DbConnection conn) => _ = conn.{|#5:Query|}("somesql"); + void QueryF(DbConnection conn) => _ = conn.{|#6:Query|}("somesql"); + void QueryG(DbConnection conn) => _ = conn.Query("somesql"); + void QueryH(DbConnection conn) => _ = conn.{|#7:Query|}("somesql"); + + } + [DapperAot(false)] + class AotDisabled + { + void ArgsA(DbConnection conn, OuterPublic args) => conn.Execute("somesql", args); + void ArgsB(DbConnection conn, OuterInternal args) => conn.Execute("somesql", args); + void ArgsC(DbConnection conn, NestedInternal.InnerPublic args) => conn.Execute("somesql", args); + void ArgsD(DbConnection conn, NestedInternal.InnerPrivate args) => conn.Execute("somesql", args); + void ArgsE(DbConnection conn, NestedInternal.InnerPrivate.InnerInnerPublic args) => conn.Execute("somesql", args); + void ArgsF(DbConnection conn, NestedInternal.InnerProtected args) => conn.Execute("somesql", args); + void ArgsG(DbConnection conn, NestedInternal.InnerProtectedInternal args) => conn.Execute("somesql", args); + void ArgsH(DbConnection conn, NestedInternal.InnerPrivateProtected args) => conn.Execute("somesql", args); + + void QueryA(DbConnection conn) => _ = conn.Query("somesql"); + void QueryB(DbConnection conn) => _ = conn.Query("somesql"); + void QueryC(DbConnection conn) => _ = conn.Query("somesql"); + void QueryD(DbConnection conn) => _ = conn.Query("somesql"); + void QueryE(DbConnection conn) => _ = conn.Query("somesql"); + void QueryF(DbConnection conn) => _ = conn.Query("somesql"); + void QueryG(DbConnection conn) => _ = conn.Query("somesql"); + void QueryH(DbConnection conn) => _ = conn.Query("somesql"); + } + protected class InnerProtected {} + protected internal class InnerProtectedInternal {} + private protected class InnerPrivateProtected {} + private class InnerPrivate + { + public class InnerInnerPublic {} + } + } + + public class OuterPublic {} + internal class OuterInternal {} + """, DefaultConfig, [ + Diagnostic(Diagnostics.NonPublicType).WithLocation(0).WithArguments("NestedInternal.InnerPrivate", "private"), + Diagnostic(Diagnostics.NonPublicType).WithLocation(1).WithArguments("NestedInternal.InnerPrivate", "private"), + Diagnostic(Diagnostics.NonPublicType).WithLocation(2).WithArguments("NestedInternal.InnerProtected", "protected"), + Diagnostic(Diagnostics.NonPublicType).WithLocation(3).WithArguments("NestedInternal.InnerPrivateProtected", "private protected"), + Diagnostic(Diagnostics.NonPublicType).WithLocation(4).WithArguments("NestedInternal.InnerPrivate", "private"), + Diagnostic(Diagnostics.NonPublicType).WithLocation(5).WithArguments("NestedInternal.InnerPrivate", "private"), + Diagnostic(Diagnostics.NonPublicType).WithLocation(6).WithArguments("NestedInternal.InnerProtected", "protected"), + Diagnostic(Diagnostics.NonPublicType).WithLocation(7).WithArguments("NestedInternal.InnerPrivateProtected", "private protected"), + ]); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP025.cs b/test/Dapper.AOT.Test/Verifiers/DAP025.cs new file mode 100644 index 00000000..51a5ac09 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP025.cs @@ -0,0 +1,49 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP025 : Verifier +{ + [Fact] + public Task QueryCommandMissingQuery() => CSVerifyAsync("""" + using Dapper; + using System.Data.Common; + using System.Linq; + + [DapperAot(true)] + class SomeCode + { + public void Foo(DbConnection conn) + { + // no query + var id = conn.{|#0:Execute|}(""" + select Id, Name + from SomeLookupTable + """); + + // fine if we don't have a query + id = conn.Execute(""" + insert SomeLookupTable (Name) + values ('North') + """); + + // fine if we use query + conn.Query(""" + select Id, Name + from SomeLookupTable + """); + + // fine if it looks like an SP call + conn.Execute(""" + exec SomeLookupFetch + """); + } + } + """", DefaultConfig, [ + Diagnostic(Diagnostics.ExecuteCommandWithQuery).WithLocation(0), + ]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP026.cs b/test/Dapper.AOT.Test/Verifiers/DAP026.cs new file mode 100644 index 00000000..5df09bf6 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP026.cs @@ -0,0 +1,50 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP026 : Verifier +{ + [Fact] + public Task QueryCommandMissingQuery() => CSVerifyAsync("""" + using Dapper; + using System.Data.Common; + using System.Linq; + + [DapperAot(true)] + class SomeCode + { + public void Foo(DbConnection conn) + { + // no query + var id = conn.{|#0:QuerySingle|}(""" + insert SomeLookupTable (Name) + values ('North') + """); + + // fine if we have a query + id = conn.QuerySingle(""" + insert SomeLookupTable (Name) + output inserted.Id + values ('North') + """); + + // fine if we use execute + conn.Execute(""" + insert SomeLookupTable (Name) + values ('North') + """); + + // fine if it looks like an SP call + conn.Execute(""" + exec SomeLookupInsert 'North' + """); + } + } + """", DefaultConfig, [ + Diagnostic(Diagnostics.QueryCommandMissingQuery).WithLocation(0), + ]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP027.cs b/test/Dapper.AOT.Test/Verifiers/DAP027.cs new file mode 100644 index 00000000..96f62852 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP027.cs @@ -0,0 +1,54 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP027 : Verifier +{ + [Fact] + public Task UseSingleRowQuery() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + using System.Linq; + + [DapperAot(true)] + class SomeCode + { + public void Foo(DbConnection conn) + { + // as extension methods + _ = conn.Query("storedproc").{|#0:First|}(); + _ = conn.Query("storedproc").{|#1:Single|}(); + _ = conn.Query("storedproc").{|#2:FirstOrDefault|}(); + _ = conn.Query("storedproc").{|#3:SingleOrDefault|}(); + + // fully generics + _ = conn.Query("storedproc").{|#4:First|}(); + + // explicit call (location test) + _ = Enumerable.{|#5:First|}(conn.Query("storedproc"));; + + // fully qualified explicit call (location test) + _ = global::System.Linq.Enumerable.{|#6:Single|}(conn.Query("storedproc")); + + // valid usage + _ = conn.QueryFirst("storedproc"); + _ = conn.QuerySingle("storedproc"); + _ = conn.QueryFirstOrDefault("storedproc"); + _ = conn.QuerySingleOrDefault("storedproc"); + } + } + class SomeType {} + """, DefaultConfig, [ + Diagnostic(Diagnostics.UseSingleRowQuery).WithLocation(0).WithArguments("QueryFirst", "First"), + Diagnostic(Diagnostics.UseSingleRowQuery).WithLocation(1).WithArguments("QuerySingle", "Single"), + Diagnostic(Diagnostics.UseSingleRowQuery).WithLocation(2).WithArguments("QueryFirstOrDefault", "FirstOrDefault"), + Diagnostic(Diagnostics.UseSingleRowQuery).WithLocation(3).WithArguments("QuerySingleOrDefault", "SingleOrDefault"), + Diagnostic(Diagnostics.UseSingleRowQuery).WithLocation(4).WithArguments("QueryFirst", "First"), + Diagnostic(Diagnostics.UseSingleRowQuery).WithLocation(5).WithArguments("QueryFirst", "First"), + Diagnostic(Diagnostics.UseSingleRowQuery).WithLocation(6).WithArguments("QuerySingle", "Single"), + ]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP028.cs b/test/Dapper.AOT.Test/Verifiers/DAP028.cs new file mode 100644 index 00000000..33b5f579 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP028.cs @@ -0,0 +1,45 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP028 : Verifier +{ + [Fact] + public Task UseQueryAsList() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + using System.Linq; + + [DapperAot(true)] + class SomeCode + { + public void Foo(DbConnection conn) + { + // as extension methods + _ = conn.Query("storedproc").{|#0:ToList|}(); + + // explicit call (location test) + _ = Enumerable.{|#1:ToList|}(conn.Query("storedproc"));; + + // full generics + _ = conn.Query("storedproc").{|#2:ToList|}(); + + // fully qualified explicit call (location test) + _ = global::System.Linq.Enumerable.{|#3:ToList|}(conn.Query("storedproc")); + + // valid usage + _ = conn.Query("storedproc").AsList(); + } + } + class SomeType {} + """, DefaultConfig, [ + Diagnostic(Diagnostics.UseQueryAsList).WithLocation(0), + Diagnostic(Diagnostics.UseQueryAsList).WithLocation(1), + Diagnostic(Diagnostics.UseQueryAsList).WithLocation(2), + Diagnostic(Diagnostics.UseQueryAsList).WithLocation(3), + ]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP035.cs b/test/Dapper.AOT.Test/Verifiers/DAP035.cs new file mode 100644 index 00000000..888fcc12 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP035.cs @@ -0,0 +1,44 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP035 : Verifier +{ + [Fact] + public Task ConstructorMultipleExplicit() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot] + class SomeCode + { + public void Foo(DbConnection conn) + { + _ = conn.Query("storedproc"); + _ = conn.Query("storedproc"); + _ = conn.Query("storedproc"); + } + } + class NoConstructors {} + class SingleExplicit + { + [ExplicitConstructor] public SingleExplicit(int a) {} + public SingleExplicit(string b) {} + public SingleExplicit(decimal c) {} + public SingleExplicit(SingleExplicit d) {} + } + class MultipleExplicit + { + [ExplicitConstructor] public {|#0:MultipleExplicit|}(int a) {} + [ExplicitConstructor] public MultipleExplicit(string b) {} + public MultipleExplicit(decimal c) {} + public MultipleExplicit(MultipleExplicit d) {} + } + """, DefaultConfig, [ + Diagnostic(Diagnostics.ConstructorMultipleExplicit).WithLocation(0).WithArguments("MultipleExplicit"), + ]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP036.cs b/test/Dapper.AOT.Test/Verifiers/DAP036.cs new file mode 100644 index 00000000..c8cab13c --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP036.cs @@ -0,0 +1,63 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP036 : Verifier +{ + [Fact] + public Task ConstructorAmbiguous() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + using System.Runtime.Serialization; + + [DapperAot] + class SomeCode + { + public void Foo(DbConnection conn) + { + _ = conn.Query("storedproc"); + _ = conn.Query("storedproc"); + _ = conn.Query("storedproc"); + + _ = conn.Query("storedproc"); + _ = conn.Query("storedproc"); + _ = conn.Query("storedproc"); + } + + [DapperAot(false)] + public void NoAotMode(DbConnection conn) + { + // not a problem in legacy mode + _ = conn.Query("storedproc"); + } + } + class NoConstructors {} + [System.Serializable] + class SingleImplicit + { + public SingleImplicit(string a) {} + public SingleImplicit(SingleImplicit b) {} + public SingleImplicit(SerializationInfo info, StreamingContext ctx) {} + } + class MultipleImplicit + { + public {|#0:MultipleImplicit|}(int a) {} + public MultipleImplicit(string b) {} + public MultipleImplicit(MultipleImplicit c) {} + } + record class RecordClass(int a); + record struct RecordStruct(int a); + readonly record struct ReadOnlyRecordStruct(int a); + + namespace System.Runtime.CompilerServices + { + static file class IsExternalInit {} + } + """, DefaultConfig, [ + Diagnostic(Diagnostics.ConstructorAmbiguous).WithLocation(0).WithArguments("MultipleImplicit"), + ]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP038.cs b/test/Dapper.AOT.Test/Verifiers/DAP038.cs new file mode 100644 index 00000000..2c0a811f --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP038.cs @@ -0,0 +1,49 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP038 : Verifier +{ + [Fact] + public Task ValueTypeSingleFirstOrDefaultUsage() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + using System.Threading.Tasks; + + [DapperAot(true)] + class SomeCode + { + public async Task Foo(DbConnection conn) + { + // issue only affects value-type and {First|Single}OrDefault + _ = conn.{|#0:QueryFirstOrDefault|}("storedproc"); + _ = conn.{|#1:QuerySingleOrDefault|}("storedproc"); + // check async + _ = await conn.{|#2:QueryFirstOrDefaultAsync|}("storedproc"); + + _ = conn.QueryFirstOrDefault("storedproc"); + _ = conn.QuerySingleOrDefault("storedproc"); + _ = await conn.QuerySingleOrDefaultAsync("storedproc"); + + _ = conn.QueryFirst("storedproc"); + _ = conn.QuerySingle("storedproc"); + _ = conn.QueryFirstOrDefault("storedproc"); + _ = conn.QuerySingleOrDefault("storedproc"); + _ = conn.QueryFirst("storedproc"); + _ = conn.QuerySingle("storedproc"); + + + } + } + class SomeClass {} + struct SomeStruct {} + """, DefaultConfig, [ + Diagnostic(Diagnostics.ValueTypeSingleFirstOrDefaultUsage).WithLocation(0).WithArguments("SomeStruct", "QueryFirstOrDefault"), + Diagnostic(Diagnostics.ValueTypeSingleFirstOrDefaultUsage).WithLocation(1).WithArguments("SomeStruct", "QuerySingleOrDefault"), + Diagnostic(Diagnostics.ValueTypeSingleFirstOrDefaultUsage).WithLocation(2).WithArguments("SomeStruct", "QueryFirstOrDefaultAsync"), + ]); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP201.cs b/test/Dapper.AOT.Test/Verifiers/DAP201.cs new file mode 100644 index 00000000..00aec00a --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP201.cs @@ -0,0 +1,26 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP201 : Verifier +{ + [Fact] + public Task MultipleBatches() => SqlVerifyAsync(""" + print 'abc' + go + {|#0:print 'def'|} + """, + Diagnostic(Diagnostics.MultipleBatches) + .WithLocation(0)); + + // note: a command of JUST "GO", or *ending* with "GO" is really hard to isolate; I'm not going to + // worry about it - at that point, it is just self-inflicted, and just worry about true multi-batch commands + + [Fact] + public Task SingleBatch() => SqlVerifyAsync(""" + print 'abc' + """); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP202.cs b/test/Dapper.AOT.Test/Verifiers/DAP202.cs new file mode 100644 index 00000000..56104f1e --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP202.cs @@ -0,0 +1,25 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP202 : Verifier +{ + + [Fact] + public Task DuplicateVariableDeclaration() => SqlVerifyAsync(""" + declare @a int = 1; + declare {|#0:@a|} int; + select @a; + """, + Diagnostic(Diagnostics.DuplicateVariableDeclaration) + .WithLocation(0).WithArguments("@a")); + + [Fact] + public Task SingleLocal() => SqlVerifyAsync(""" + declare @a int = 1; + select @a; + """); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP203.cs b/test/Dapper.AOT.Test/Verifiers/DAP203.cs new file mode 100644 index 00000000..b4abd90a --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP203.cs @@ -0,0 +1,17 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP203 : Verifier +{ + [Fact] + public Task GlobalIdentity() => SqlVerifyAsync(""" + insert SomeTable (A) values (1) + select {|#0:@@identity|} + """, + Diagnostic(Diagnostics.GlobalIdentity) + .WithLocation(0)); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP204.cs b/test/Dapper.AOT.Test/Verifiers/DAP204.cs new file mode 100644 index 00000000..adca1f8e --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP204.cs @@ -0,0 +1,25 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP204 : Verifier +{ + + [Fact] + public Task SelectScopeIdentity() => SqlVerifyAsync(""" + insert SomeTable (A) values (1) + select {|#0:scope_identity()|} + """, + Diagnostic(Diagnostics.SelectScopeIdentity) + .WithLocation(0)); + + [Fact] + public Task InsertWithOutput() => SqlVerifyAsync(""" + insert SomeTable (A) + output inserted.Id + values (1) + """); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP205.cs b/test/Dapper.AOT.Test/Verifiers/DAP205.cs new file mode 100644 index 00000000..98eddcfe --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP205.cs @@ -0,0 +1,26 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP205 : Verifier +{ + [Fact] + public Task NullLiteralComparison() => SqlVerifyAsync(""" + select A, B + from SomeTable + where X = {|#0:null|} and Y != {|#1:null|} and Z > {|#2:null|} + """, + Diagnostic(Diagnostics.NullLiteralComparison).WithLocation(0), + Diagnostic(Diagnostics.NullLiteralComparison).WithLocation(1), + Diagnostic(Diagnostics.NullLiteralComparison).WithLocation(2)); + + [Fact] + public Task CompareWithAnsiNulls() => SqlVerifyAsync(""" + select A, B + from SomeTable + where X is null and Y is not null + """); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP206.cs b/test/Dapper.AOT.Test/Verifiers/DAP206.cs new file mode 100644 index 00000000..b34dd7c6 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP206.cs @@ -0,0 +1,17 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP206 : Verifier +{ + [Fact] + public Task ValidSql() => SqlVerifyAsync("exec valid"); + + [Fact] + public Task ParseError() => SqlVerifyAsync("{|#0:|}111 invalid", + Diagnostic(Diagnostics.ParseError) + .WithLocation(0).WithArguments(46010, "Incorrect syntax near 111.")); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP207.cs b/test/Dapper.AOT.Test/Verifiers/DAP207.cs new file mode 100644 index 00000000..c4b78753 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP207.cs @@ -0,0 +1,18 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP207 : Verifier +{ + [Fact] + public Task ScalarVariableUsedAsTable() => SqlVerifyAsync(""" + declare @id int = 0; + declare @t table (Value int not null); + insert @t (Value) values (1); + select Value from {|#0:@id|}; + select Value from @t; + """, Diagnostic(Diagnostics.ScalarVariableUsedAsTable).WithLocation(0).WithArguments("@id")); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP208.cs b/test/Dapper.AOT.Test/Verifiers/DAP208.cs new file mode 100644 index 00000000..06a4bcdd --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP208.cs @@ -0,0 +1,18 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP208 : Verifier +{ + [Fact] + public Task TableVariableUsedAsScalar() => SqlVerifyAsync(""" + declare @id int = 0; + declare @t table (Value int not null); + insert @t (Value) values (1); + select @id; + select {|#0:@t|}; + """, Diagnostic(Diagnostics.TableVariableUsedAsScalar).WithLocation(0).WithArguments("@t")); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP209.cs b/test/Dapper.AOT.Test/Verifiers/DAP209.cs new file mode 100644 index 00000000..b64afb29 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP209.cs @@ -0,0 +1,17 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP209 : Verifier +{ + [Fact] + public Task TableVariableAccessedBeforePopulate() => SqlVerifyAsync(""" + declare @t table (Value int not null); + select Value from {|#0:@t|}; + insert @t (Value) values (1); + select Value from @t; + """, Diagnostic(Diagnostics.TableVariableAccessedBeforePopulate).WithLocation(0).WithArguments("@t")); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP210.cs b/test/Dapper.AOT.Test/Verifiers/DAP210.cs new file mode 100644 index 00000000..deb8e963 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP210.cs @@ -0,0 +1,17 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP210 : Verifier +{ + [Fact] + public Task TableVariableAccessedBeforePopulate() => SqlVerifyAsync(""" + declare @i int; + select {|#0:@i|}; + set @i = 42; + select @i; + """, Diagnostic(Diagnostics.VariableAccessedBeforeAssignment).WithLocation(0).WithArguments("@i")); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP211.cs b/test/Dapper.AOT.Test/Verifiers/DAP211.cs new file mode 100644 index 00000000..a89e6958 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP211.cs @@ -0,0 +1,15 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP211 : Verifier +{ + [Fact] + public Task VariableAccessedBeforeDeclaration() => SqlVerifyAsync(""" + select {|#0:@i|}; + declare @i int; + """, Diagnostic(Diagnostics.VariableAccessedBeforeDeclaration).WithLocation(0).WithArguments("@i")); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP212.cs b/test/Dapper.AOT.Test/Verifiers/DAP212.cs new file mode 100644 index 00000000..deda6c23 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP212.cs @@ -0,0 +1,23 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP212 : Verifier +{ + [Fact] + public Task ExecComposedSql() => SqlVerifyAsync(""" + declare @s nvarchar(100) = 'fred' -- assume this is a parameter + declare @sql nvarchar(2000) = 'select * from Customers where Name = ''' + @s + ''''; + {|#0:exec (@sql)|}; + + {|#1:exec ('select * from Customers where Name = ''' + @s + '''')|}; + + exec sp_executesql N'select * from Customers where Name = @name', N'@name nvarchar(100)', @s + + exec ('select * from Customers where Name = ''fred''') -- not composed + """, Diagnostic(Diagnostics.ExecComposedSql).WithLocation(0), + Diagnostic(Diagnostics.ExecComposedSql).WithLocation(1)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP213.cs b/test/Dapper.AOT.Test/Verifiers/DAP213.cs new file mode 100644 index 00000000..b9dc830e --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP213.cs @@ -0,0 +1,18 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP213 : Verifier +{ + [Fact] + public Task VariableValueNotConsumed() => SqlVerifyAsync(""" + declare @i int = 42; + set {|#0:@i|} = 1; + select @i; + set {|#1:@i|} = 43; + """, Diagnostic(Diagnostics.VariableValueNotConsumed).WithLocation(0).WithArguments("@i"), + Diagnostic(Diagnostics.VariableValueNotConsumed).WithLocation(1).WithArguments("@i")); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP214.cs b/test/Dapper.AOT.Test/Verifiers/DAP214.cs new file mode 100644 index 00000000..683b3a58 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP214.cs @@ -0,0 +1,14 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP214 : Verifier +{ + [Fact] + public Task VariableNotDeclared() => SqlVerifyAsync(""" + select {|#0:@i|}; + """, SqlAnalysis.SqlParseInputFlags.KnownParameters, Diagnostic(Diagnostics.VariableNotDeclared).WithLocation(0).WithArguments("@i")); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP216.cs b/test/Dapper.AOT.Test/Verifiers/DAP216.cs new file mode 100644 index 00000000..faf128db --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP216.cs @@ -0,0 +1,15 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP216 : Verifier +{ + [Fact] + public Task InsertColumnsNotSpecified() => SqlVerifyAsync(""" + {|#0:insert SomeTable values (42)|} + insert SomeTable (A) values (42) + """, Diagnostic(Diagnostics.InsertColumnsNotSpecified).WithLocation(0)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP217.cs b/test/Dapper.AOT.Test/Verifiers/DAP217.cs new file mode 100644 index 00000000..b162524b --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP217.cs @@ -0,0 +1,18 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP217 : Verifier +{ + [Fact] + public Task InsertColumnsMismatch() => SqlVerifyAsync(""" + insert SomeTable (A, B) + {|#0:values (1, 2, 3)|} + + insert SomeTable (A, B) + values (4,5) + """, Diagnostic(Diagnostics.InsertColumnsMismatch).WithLocation(0)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP218.cs b/test/Dapper.AOT.Test/Verifiers/DAP218.cs new file mode 100644 index 00000000..ab29306c --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP218.cs @@ -0,0 +1,18 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP218 : Verifier +{ + [Fact] + public Task InsertColumnsUnbalanced() => SqlVerifyAsync(""" + insert SomeTable (A, B) + {|#0:values (1, 2), (3, 4, 5)|} + + insert SomeTable (A, B) + values (6, 7), (8, 9) + """, Diagnostic(Diagnostics.InsertColumnsUnbalanced).WithLocation(0)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP219.cs b/test/Dapper.AOT.Test/Verifiers/DAP219.cs new file mode 100644 index 00000000..65e07b8b --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP219.cs @@ -0,0 +1,16 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP219 : Verifier +{ + [Fact] + public Task SelectStar() => SqlVerifyAsync(""" + select {|#0:*|} from Users + + select Id, Name from Users + """, Diagnostic(Diagnostics.SelectStar).WithLocation(0)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP220.cs b/test/Dapper.AOT.Test/Verifiers/DAP220.cs new file mode 100644 index 00000000..69ae98a4 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP220.cs @@ -0,0 +1,16 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP220 : Verifier +{ + [Fact] + public Task SelectEmptyColumnName() => SqlVerifyAsync(""" + select Id, {|#0:Credit - Debit|} from Accounts + + select Id, Credit - Debit as [Balance] from Accounts + """, SqlAnalysis.SqlParseInputFlags.ValidateSelectNames, Diagnostic(Diagnostics.SelectEmptyColumnName).WithLocation(0).WithArguments(1)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP221.cs b/test/Dapper.AOT.Test/Verifiers/DAP221.cs new file mode 100644 index 00000000..f52fcf20 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP221.cs @@ -0,0 +1,16 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP221 : Verifier +{ + [Fact] + public Task SelectDuplicateColumnName() => SqlVerifyAsync(""" + select Id, Name, {|#0:FirstName + ' ' + Surname as [Name]|} from Users + + select Id, Name, FirstName + ' ' + Surname as [FullName] from Users + """, SqlAnalysis.SqlParseInputFlags.ValidateSelectNames, Diagnostic(Diagnostics.SelectDuplicateColumnName).WithLocation(0).WithArguments("Name")); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP222.cs b/test/Dapper.AOT.Test/Verifiers/DAP222.cs new file mode 100644 index 00000000..74c6966c --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP222.cs @@ -0,0 +1,16 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP222 : Verifier +{ + [Fact] + public Task SelectAssignAndRead() => SqlVerifyAsync(""" + declare @id int; + {|#0:select @id = Id, Name from Users|}; + select @id; + """, Diagnostic(Diagnostics.SelectAssignAndRead).WithLocation(0).WithArguments("Name")); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP223.cs b/test/Dapper.AOT.Test/Verifiers/DAP223.cs new file mode 100644 index 00000000..1776c0b0 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP223.cs @@ -0,0 +1,20 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP223 : Verifier +{ + [Fact] + public Task DeleteWithoutWhere() => SqlVerifyAsync(""" + {|#0:delete from Users|} + + delete from Users where Region='North' + + delete u + from Users u + inner join Foo x on x.Id = u.Id + """, Diagnostic(Diagnostics.DeleteWithoutWhere).WithLocation(0)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP224.cs b/test/Dapper.AOT.Test/Verifiers/DAP224.cs new file mode 100644 index 00000000..b8766767 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP224.cs @@ -0,0 +1,21 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP224 : Verifier +{ + [Fact] + public Task UpdateWithoutWhere() => SqlVerifyAsync(""" + {|#0:update Users set Balance = 0|} + + update Users set Balance = 0 where Region='North' + + update u + set u.Balance = 0 + from Users u + inner join Foo x on x.Id = u.Id + """, Diagnostic(Diagnostics.UpdateWithoutWhere).WithLocation(0)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP225.cs b/test/Dapper.AOT.Test/Verifiers/DAP225.cs new file mode 100644 index 00000000..6f402abe --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP225.cs @@ -0,0 +1,55 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP225 : Verifier +{ + [Fact] + public Task FromMultiTableMissingAlias() => SqlVerifyAsync(""" + select a.Id + from A a + inner join {|#0:B|} on a.X = a.Id + + select a.Id + from A a + inner join B b on b.X = a.Id + + select Id + from Users + where Id in ( + select a.Id + from A a + inner join B b on b.X = a.Id) + + select Id + from Users + where Id in ( + select a.Id + from A a + inner join {|#1:B|} on a.X = a.Id) + + delete {|#2:Users|} + from Users u + inner join B b on b.X = u.Id + + delete u + from Users u + inner join B b on b.X = u.Id + + update {|#3:Users|} + set u.Id = 42 + from Users u + inner join B b on b.X = u.Id + + update u + set u.Id = 42 + from Users u + inner join B b on b.X = u.Id + """, Diagnostic(Diagnostics.FromMultiTableMissingAlias).WithLocation(0), + Diagnostic(Diagnostics.FromMultiTableMissingAlias).WithLocation(1), + Diagnostic(Diagnostics.FromMultiTableMissingAlias).WithLocation(2), + Diagnostic(Diagnostics.FromMultiTableMissingAlias).WithLocation(3)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP226.cs b/test/Dapper.AOT.Test/Verifiers/DAP226.cs new file mode 100644 index 00000000..0ef1cbfa --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP226.cs @@ -0,0 +1,20 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP226 : Verifier +{ + [Fact] + public Task FromMultiTableUnqualifiedColumn() => SqlVerifyAsync(""" + select a.Id, {|#0:Name|} + from A a + inner join B b on b.X = a.Id + + select a.Id, b.Name + from A a + inner join B b on b.X = a.Id + """, Diagnostic(Diagnostics.FromMultiTableUnqualifiedColumn).WithLocation(0).WithArguments("Name")); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP227.cs b/test/Dapper.AOT.Test/Verifiers/DAP227.cs new file mode 100644 index 00000000..f479e257 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP227.cs @@ -0,0 +1,18 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP227 : Verifier +{ + [Fact] + public Task NonIntegerTop() => SqlVerifyAsync(""" + select top {|#0:22.5|} Id, Name from Users + + select top 22.5 percent Id, Name from Users + + select top 22 Id, Name from Users + """, Diagnostic(Diagnostics.NonIntegerTop).WithLocation(0)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP228.cs b/test/Dapper.AOT.Test/Verifiers/DAP228.cs new file mode 100644 index 00000000..c1cd3654 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP228.cs @@ -0,0 +1,19 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP228 : Verifier +{ + [Fact] + public Task NonPositiveTop() => SqlVerifyAsync(""" + select top {|#0:0|} Id, Name from Users + + select top {|#1:0|} percent Id, Name from Users + + select top 1 Id, Name from Users + select top 1 percent Id, Name from Users + """, Diagnostic(Diagnostics.NonPositiveTop).WithLocation(0), Diagnostic(Diagnostics.NonPositiveTop).WithLocation(1)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP229.cs b/test/Dapper.AOT.Test/Verifiers/DAP229.cs new file mode 100644 index 00000000..5c3ab790 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP229.cs @@ -0,0 +1,19 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP229 : Verifier +{ + [Fact] + public Task SelectFirstTopError() => SqlVerifyAsync(""" + select top {|#0:2|} Id, Name from Users where Id=42 + """, SqlAnalysis.SqlParseInputFlags.SingleRow, Diagnostic(Diagnostics.SelectFirstTopError).WithLocation(0)); + + [Fact] + public Task SelectFirstTopError_Pass() => SqlVerifyAsync(""" + select top 1 Id, Name from Users where Id=42 + """, SqlAnalysis.SqlParseInputFlags.SingleRow); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP230.cs b/test/Dapper.AOT.Test/Verifiers/DAP230.cs new file mode 100644 index 00000000..34bbf450 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP230.cs @@ -0,0 +1,30 @@ +using Dapper.CodeAnalysis; +using Dapper.SqlAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP230 : Verifier +{ + [Fact] + public Task SelectSingleTopError() => SqlVerifyAsync(""" + select top {|#0:1|} Id, Name from Users where Id=42 + """, SqlParseInputFlags.SingleRow | SqlParseInputFlags.AtMostOne, Diagnostic(Diagnostics.SelectSingleTopError).WithLocation(0)); + + [Fact] + public Task SelectSingleTopError_Fetch() => SqlVerifyAsync(""" + select Id, Name from Users where Id=42 order by Name offset 0 rows fetch next {|#0:1|} row only + """, SqlParseInputFlags.SingleRow | SqlParseInputFlags.AtMostOne, Diagnostic(Diagnostics.SelectSingleTopError).WithLocation(0)); + + [Fact] + public Task SelectSingleTopError_TooHigh() => SqlVerifyAsync(""" + select top {|#0:3|} Id, Name from Users where Id=42 + """, SqlParseInputFlags.SingleRow | SqlParseInputFlags.AtMostOne, Diagnostic(Diagnostics.SelectSingleTopError).WithLocation(0)); + + [Fact] + public Task SelectSingleTopError_Pass() => SqlVerifyAsync(""" + select top 2 Id, Name from Users where Id=42 + """, SqlParseInputFlags.SingleRow | SqlParseInputFlags.AtMostOne); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP231.cs b/test/Dapper.AOT.Test/Verifiers/DAP231.cs new file mode 100644 index 00000000..fbaf4f28 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP231.cs @@ -0,0 +1,40 @@ +using Dapper.CodeAnalysis; +using Dapper.SqlAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP231 : Verifier +{ + [Fact] + public Task SelectSingleRowWithoutWhere() => SqlVerifyAsync(""" + {|#0:select Id, Name from Users|} + """, SqlParseInputFlags.SingleRow, Diagnostic(Diagnostics.SelectSingleRowWithoutWhere).WithLocation(0)); + + [Fact] + public Task SelectSingleRowWithoutWhere_WithWhere() => SqlVerifyAsync(""" + select Id, Name from Users where Id=42 + """, SqlParseInputFlags.SingleRow); + + [Fact] + public Task SelectSingleRowWithoutWhere_WithTopAndOrderBy() => SqlVerifyAsync(""" + select top 1 Id, Name from Users order by Name + """, SqlParseInputFlags.SingleRow); + + [Fact] + public Task SelectSingleRowWithoutWhere_WithTopNoOrderBy() => SqlVerifyAsync(""" + {|#0:select top 1 Id, Name from Users|} + """, SqlParseInputFlags.SingleRow, Diagnostic(Diagnostics.SelectSingleRowWithoutWhere).WithLocation(0)); + + [Fact] + public Task SelectSingleRowWithoutWhere_WithOrderByNoTop() => SqlVerifyAsync(""" + {|#0:select Id, Name from Users order by Name|} + """, SqlParseInputFlags.SingleRow, Diagnostic(Diagnostics.SelectSingleRowWithoutWhere).WithLocation(0)); + + [Fact] + public Task SelectSingleRowWithoutFrom() => SqlVerifyAsync(""" + select HasRecords = case when exists (select top 1 1 from MyTable) then 1 else 0 end + """, SqlParseInputFlags.SingleRow); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP232.cs b/test/Dapper.AOT.Test/Verifiers/DAP232.cs new file mode 100644 index 00000000..ba42d83a --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP232.cs @@ -0,0 +1,24 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP232 : Verifier +{ + [Fact] + public Task NonPositiveFetch() => SqlVerifyAsync(""" + select Id, Name + from Users + order by Name + offset 0 rows + fetch next {|#0:0|} row only + + select Id, Name + from Users + order by Name + offset 0 rows + fetch next 1 row only + """, Diagnostic(Diagnostics.NonPositiveFetch).WithLocation(0)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP233.cs b/test/Dapper.AOT.Test/Verifiers/DAP233.cs new file mode 100644 index 00000000..e31ae60d --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP233.cs @@ -0,0 +1,24 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP233 : Verifier +{ + [Fact] + public Task NegativeOffset() => SqlVerifyAsync(""" + select Id, Name + from Users + order by Name + offset {|#0:-1|} rows + fetch next 1 row only + + select Id, Name + from Users + order by Name + offset 0 rows + fetch next 1 row only + """, Diagnostic(Diagnostics.NegativeOffset).WithLocation(0)); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP234.cs b/test/Dapper.AOT.Test/Verifiers/DAP234.cs new file mode 100644 index 00000000..369e10da --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP234.cs @@ -0,0 +1,28 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP234 : Verifier +{ + [Fact] + public Task SimplifyExpression() => SqlVerifyAsync(""" + select 1 + ({|#0:2 * 5|}) as [Score], -1 as [Allowed], {|#1:+1|} as [NotAllowed] + + select {|#2:1 + 10|} as [Score] + + select 11 as [Score], (11) as [Meh] + + select {|#3:1 / null|} as [oops] + declare @i int = 42; + select {|#4:15 / @i + null|} + """, + Diagnostic(Diagnostics.SimplifyExpression).WithLocation(0).WithArguments("10"), + Diagnostic(Diagnostics.SimplifyExpression).WithLocation(1).WithArguments("1"), + Diagnostic(Diagnostics.SimplifyExpression).WithLocation(2).WithArguments("11"), + Diagnostic(Diagnostics.SimplifyExpression).WithLocation(3).WithArguments("null"), + Diagnostic(Diagnostics.SimplifyExpression).WithLocation(4).WithArguments("null") + ); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP235.cs b/test/Dapper.AOT.Test/Verifiers/DAP235.cs new file mode 100644 index 00000000..f71b14a0 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP235.cs @@ -0,0 +1,25 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics; +namespace Dapper.AOT.Test.Verifiers; + +public class DAP235 : Verifier +{ + [Fact] + public Task TopWithOffset() => SqlVerifyAsync(""" + select {|#0:top 10|} Id, Name + from Users + order by Name + offset 0 rows + + select Id, Name + from Users + order by Name + offset 0 rows + fetch next 10 rows only + """, + Diagnostic(Diagnostics.TopWithOffset).WithLocation(0) + ); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/SqlDetection.cs b/test/Dapper.AOT.Test/Verifiers/SqlDetection.cs new file mode 100644 index 00000000..f6ccacbf --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/SqlDetection.cs @@ -0,0 +1,145 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; + +namespace Dapper.AOT.Test.Verifiers; + +public class SqlDetection : Verifier +{ + [Fact] + public Task CSViaDapper() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot(true)] + class SomeCode + { + public void Foo(DbConnection conn) + { + conn.Execute("exec valid"); + conn.Execute("{|#0:|}111 invalid"); + } + } + """, DefaultConfig, [Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(0).WithArguments(46010, "Incorrect syntax near 111.")]); + + [Fact] + public Task CSViaProperty() => CSVerifyAsync(""" + using System.Data.Common; + using Dapper; + + class SomeCode + { + public void Foo(DbCommand cmd) + { + cmd.CommandText = "exec valid"; + cmd.CommandText = "{|#0:|}111 invalid"; + + CommandText = "222 invalid"; + Bar = "333 invalid"; + Blap = "{|#1:|}444 invalid"; + } + + public string CommandText {get;set;} + public string Bar {get;set;} + [Sql] + public string Blap {get;set;} + } + """, DefaultConfig, [Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(0).WithArguments(46010, "Incorrect syntax near 111."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(1).WithArguments(46010, "Incorrect syntax near 444.")]); + + [Fact] + public Task CSViaParam() => CSVerifyAsync(""" + using System.Data.Common; + using Dapper; + + class SomeCode + { + static void Foo(string sql) {} + static void Bar(string whatever) {} + static void Blap([Sql] string whatever) {} + public void Usage() + { + Foo("exec valid"); + Foo("{|#0:|}111 invalid"); + Bar("222 invalid"); + Blap("{|#1:|}333 invalid"); + } + } + """, DefaultConfig, + [Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(0) + .WithArguments(46010, "Incorrect syntax near 111."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(1) + .WithArguments(46010, "Incorrect syntax near 333.")]); + + [Fact] + public Task VBViaDapper() => VBVerifyAsync(""" + Imports Dapper + Imports System.Data.Common + + + Class SomeCode + Public Sub Foo(conn As DbConnection) + conn.Execute("exec valid") + conn.Execute("{|#0:|}111 invalid") + End Sub + End Class + """, DefaultConfig, [Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(0).WithArguments(46010, "Incorrect syntax near 111.")]); + + [Fact] + public Task VBViaProperty() => VBVerifyAsync(""" + Imports System.Data.Common + Imports Dapper + + Class SomeCode + Public Sub Foo(cmd as DbCommand) + cmd.CommandText = "exec valid" + cmd.CommandText = "{|#0:|}111 invalid" + + CommandText = "222 invalid" + Bar = "333 invalid" + Blap = "{|#1:|}444 invalid" + End Sub + + Public Property CommandText As String + Public Property Bar As String + + Public Property Blap As String + End Class + """, DefaultConfig, [Diagnostic(DapperAnalyzer.Diagnostics.ParseError) +.WithLocation(0).WithArguments(46010, "Incorrect syntax near 111."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) +.WithLocation(1).WithArguments(46010, "Incorrect syntax near 444.")]); + + [Fact] + public Task VBViaParam() => VBVerifyAsync(""" + Imports System.Data.Common + Imports Dapper + + Class SomeCode + Sub Foo(sql as String) + End Sub + Sub Bar(whatever as String) + End Sub + Sub Blap( whatever As String) + End Sub + Public Sub Usage() + Foo("exec valid") + Foo("{|#0:|}111 invalid") + Bar("222 invalid") + Blap("{|#1:|}333 invalid") + End Sub + End Class + """, DefaultConfig, + [Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(0) + .WithArguments(46010, "Incorrect syntax near 111."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(1) + .WithArguments(46010, "Incorrect syntax near 333.")]); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/SqlSyntaxDetection.cs b/test/Dapper.AOT.Test/Verifiers/SqlSyntaxDetection.cs new file mode 100644 index 00000000..5685c70c --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/SqlSyntaxDetection.cs @@ -0,0 +1,217 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; + +namespace Dapper.AOT.Test.Verifiers; + +public class SqlSyntaxDetection : Verifier +{ + [Fact] + public Task GlobalEnabled_NoAttributes() => CSVerifyAsync(""" + using Dapper; + + [DapperAot] + class SomeCode + { + public void Foo(System.Data.Common.DbConnection conn) + { + conn.Execute("{|#0:|}111 invalid"); + } + public void Foo(System.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#1:|}222 invalid"); + } + public void Foo(Microsoft.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#2:|}333 invalid"); + } + } + """, DefaultConfig, [ + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(0).WithArguments(46010, "Incorrect syntax near 111."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(1).WithArguments(46010, "Incorrect syntax near 222."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(2).WithArguments(46010, "Incorrect syntax near 333."), + ], SqlSyntax.SqlServer); + + [Fact] + public Task GlobalDisabled_NoAttributes() => CSVerifyAsync(""" + using Dapper; + + [DapperAot] + class SomeCode + { + public void Foo(System.Data.Common.DbConnection conn) + { + conn.Execute("{|#0:|}111 invalid"); + } + public void Foo(System.Data.SqlClient.SqlConnection conn) // still picked up by type + { + conn.Execute("{|#1:|}222 invalid"); + } + public void Foo(Microsoft.Data.SqlClient.SqlConnection conn) // still picked up by type + { + conn.Execute("{|#2:|}333 invalid"); + } + } + """, DefaultConfig, [ + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(1).WithArguments(46010, "Incorrect syntax near 222."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(2).WithArguments(46010, "Incorrect syntax near 333."), + ], SqlSyntax.General); + + [Fact] + public Task GlobalDisabled_MethodAttributes() => CSVerifyAsync(""" + using Dapper; + + [DapperAot] + class SomeCode + { + [SqlSyntax(SqlSyntax.SqlServer)] + public void Foo(System.Data.Common.DbConnection conn) + { + conn.Execute("{|#0:|}111 invalid"); + } + [SqlSyntax(SqlSyntax.SqlServer)] + public void Foo(System.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#1:|}222 invalid"); + } + [SqlSyntax(SqlSyntax.SqlServer)] + public void Foo(Microsoft.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#2:|}333 invalid"); + } + } + """, DefaultConfig, [ + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(0).WithArguments(46010, "Incorrect syntax near 111."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(1).WithArguments(46010, "Incorrect syntax near 222."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(2).WithArguments(46010, "Incorrect syntax near 333."), + ], SqlSyntax.General); + + [Fact] + public Task GlobalDisabled_TypeAttribute() => CSVerifyAsync(""" + using Dapper; + + [SqlSyntax(SqlSyntax.SqlServer), DapperAot] + class SomeCode + { + public void Foo(System.Data.Common.DbConnection conn) + { + conn.Execute("{|#0:|}111 invalid"); + } + public void Foo(System.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#1:|}222 invalid"); + } + public void Foo(Microsoft.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#2:|}333 invalid"); + } + } + """, DefaultConfig, [ + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(0).WithArguments(46010, "Incorrect syntax near 111."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(1).WithArguments(46010, "Incorrect syntax near 222."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(2).WithArguments(46010, "Incorrect syntax near 333."), + ], SqlSyntax.General); + + [Fact] + public Task GlobalDisabled_ContainingTypeAttribute() => CSVerifyAsync(""" + using Dapper; + + [SqlSyntax(SqlSyntax.SqlServer), DapperAot] + class SomeWrapper + { + class SomeCode + { + public void Foo(System.Data.Common.DbConnection conn) + { + conn.Execute("{|#0:|}111 invalid"); + } + public void Foo(System.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#1:|}222 invalid"); + } + public void Foo(Microsoft.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#2:|}333 invalid"); + } + } + } + """, DefaultConfig, [ + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(0).WithArguments(46010, "Incorrect syntax near 111."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(1).WithArguments(46010, "Incorrect syntax near 222."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(2).WithArguments(46010, "Incorrect syntax near 333."), + ], SqlSyntax.General); + + [Fact] + public Task GlobalDisabled_ModuleAttribute() => CSVerifyAsync(""" + using Dapper; + + [module:SqlSyntax(SqlSyntax.SqlServer), DapperAot] + class SomeCode + { + public void Foo(System.Data.Common.DbConnection conn) + { + conn.Execute("{|#0:|}111 invalid"); + } + public void Foo(System.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#1:|}222 invalid"); + } + public void Foo(Microsoft.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#2:|}333 invalid"); + } + } + """, DefaultConfig, [ + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(0).WithArguments(46010, "Incorrect syntax near 111."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(1).WithArguments(46010, "Incorrect syntax near 222."), + Diagnostic(DapperAnalyzer.Diagnostics.ParseError) + .WithLocation(2).WithArguments(46010, "Incorrect syntax near 333."), +], SqlSyntax.General); + + [Fact] + public Task GlobalDisabled_AssemblyAttribute() => CSVerifyAsync(""" + using Dapper; + + [assembly:SqlSyntax(SqlSyntax.SqlServer), DapperAot] + class SomeCode + { + public void Foo(System.Data.Common.DbConnection conn) + { + conn.Execute("{|#0:|}111 invalid"); + } + public void Foo(System.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#1:|}222 invalid"); + } + public void Foo(Microsoft.Data.SqlClient.SqlConnection conn) + { + conn.Execute("{|#2:|}333 invalid"); + } + } + """, DefaultConfig, [ +Diagnostic(DapperAnalyzer.Diagnostics.ParseError) +.WithLocation(0).WithArguments(46010, "Incorrect syntax near 111."), +Diagnostic(DapperAnalyzer.Diagnostics.ParseError) +.WithLocation(1).WithArguments(46010, "Incorrect syntax near 222."), +Diagnostic(DapperAnalyzer.Diagnostics.ParseError) +.WithLocation(2).WithArguments(46010, "Incorrect syntax near 333."), +], SqlSyntax.General); + + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/Verifier.cs b/test/Dapper.AOT.Test/Verifiers/Verifier.cs index ce9023b0..63661eb5 100644 --- a/test/Dapper.AOT.Test/Verifiers/Verifier.cs +++ b/test/Dapper.AOT.Test/Verifiers/Verifier.cs @@ -1,15 +1,22 @@ -using Microsoft.CodeAnalysis; +using Dapper.CodeAnalysis; +using Dapper.SqlAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Testing; using System; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; + namespace Dapper.AOT.Test.Verifiers; // inspiration: https://www.thinktecture.com/en/net/roslyn-source-generators-analyzers-code-fixes-testing/ public abstract class Verifier @@ -18,37 +25,59 @@ public abstract class Verifier protected static DiagnosticResult Diagnostic(DiagnosticDescriptor diagnostic) => new DiagnosticResult(diagnostic); - protected static DiagnosticResult InterceptorsNotEnabled = Diagnostic(CodeAnalysis.DapperInterceptorGenerator.Diagnostics.InterceptorsNotEnabled); + + protected static DiagnosticResult InterceptorsNotEnabled = Diagnostic(DapperInterceptorGenerator.Diagnostics.InterceptorsNotEnabled); protected static DiagnosticResult InterceptorsGenerated(int handled, int total, int interceptors, int commands, int readers) - => new DiagnosticResult(CodeAnalysis.DapperInterceptorGenerator.Diagnostics.InterceptorsGenerated) + => new DiagnosticResult(DapperInterceptorGenerator.Diagnostics.InterceptorsGenerated) .WithArguments(handled, total, interceptors, commands, readers); - public Task VerifyAsync(string source, + internal Task CSVerifyAsync(string source, Func[] transforms, - params DiagnosticResult[] expected) + DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None) where TAnalyzer : DiagnosticAnalyzer, new() { var test = new CSharpAnalyzerTest(); - return ExecuteAsync(test, source, transforms, expected); + return ExecuteAsync(test, source, transforms, expected, sqlSyntax, sqlParseInputFlags); } - public Task VerifyAsync(string source, + internal Task CSVerifyAsync(string source, Func[] transforms, - params DiagnosticResult[] expected) + DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None) where TAnalyzer : DiagnosticAnalyzer, new() - where TCodeFixProvider : CodeFixProvider, new() + where TCodeFix : CodeFixProvider, new() { - var test = new CSharpAnalyzerTest(); - return ExecuteAsync(test, source, transforms, expected); + var test = new CSharpCodeFixTest(); + return ExecuteAsync(test, source, transforms, expected, sqlSyntax, sqlParseInputFlags); } - protected Task ExecuteAsync(AnalyzerTest test, string source, + internal Task VBVerifyAsync(string source, + Func[] transforms, + DiagnosticResult[] expected, SqlSyntax sqlSyntax) + where TAnalyzer : DiagnosticAnalyzer, new() + { + var test = new VisualBasicAnalyzerTest(); + return ExecuteAsync(test, source, transforms, expected, sqlSyntax, SqlParseInputFlags.None); + } + internal Task VBVerifyAsync(string source, Func[] transforms, - DiagnosticResult[] expected) + DiagnosticResult[] expected, SqlSyntax sqlSyntax) + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() + { + var test = new VisualBasicCodeFixTest(); + return ExecuteAsync(test, source, transforms, expected, sqlSyntax, SqlParseInputFlags.None); + } + + internal Task ExecuteAsync(AnalyzerTest test, string source, + Func[] transforms, + DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags) { test.TestCode = source; - test.ExpectedDiagnostics.AddRange(expected); + if (expected is not null) + { + test.ExpectedDiagnostics.AddRange(expected); + } #if NETFRAMEWORK test.ReferenceAssemblies = ReferenceAssemblies.NetFramework.Net472.Default; #elif NET8_0_OR_GREATER @@ -62,24 +91,65 @@ protected Task ExecuteAsync(AnalyzerTest test, string source, #else test.ReferenceAssemblies = ReferenceAssemblies.Net.Net60; #endif + if (sqlSyntax == SqlSyntax.SqlServer && sqlParseInputFlags == SqlParseInputFlags.None) + { + test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", AssumeSqlServer)); + } + else if (sqlSyntax != SqlSyntax.General || sqlParseInputFlags != SqlParseInputFlags.None) + { + test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", CreateEditorConfig(sqlSyntax, sqlParseInputFlags))); + } test.TestState.AdditionalReferences.Add(typeof(SqlMapper).Assembly); test.TestState.AdditionalReferences.Add(typeof(DapperAotAttribute).Assembly); - if (transforms is not null) test.SolutionTransforms.AddRange(transforms); + test.TestState.AdditionalReferences.Add(typeof(System.Data.SqlClient.SqlConnection).Assembly); + test.TestState.AdditionalReferences.Add(typeof(Microsoft.Data.SqlClient.SqlConnection).Assembly); + if (transforms is not null) + { + test.SolutionTransforms.AddRange(transforms); + } return test.RunAsync(CancellationToken); } + + static SourceText CreateEditorConfig(SqlSyntax syntax, SqlParseInputFlags sqlParseInputFlags) + { + var sb = new StringBuilder().AppendLine("is_global = true"); + if (syntax != SqlSyntax.General) sb.Append(GlobalOptions.Keys.GlobalOptions_DapperSqlSyntax).Append(" = ").Append(syntax).AppendLine(); + if (sqlParseInputFlags != SqlParseInputFlags.None) sb.Append(GlobalOptions.Keys.GlobalOptions_DapperDebugSqlParseInputFlags).Append(" = ").Append(sqlParseInputFlags).AppendLine(); + return SourceText.From(sb.ToString(), Encoding.UTF8); + } + + private static readonly SourceText AssumeSqlServer = CreateEditorConfig(SqlSyntax.SqlServer, SqlParseInputFlags.None); + protected static Func InterceptorsEnabled = WithFeatures( new("InterceptorsPreview", "true"), // rc 1 new("InterceptorsPreviewNamespaces", "Dapper.AOT") // rc2 ? ); - protected static Func CSharpPreview = WithLanguageVersion(LanguageVersion.Preview); - protected static Func[] DefaultConfig = new[] { InterceptorsEnabled, CSharpPreview }; + protected static Func CSharpPreview = WithCSharpLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview); - protected static Func WithLanguageVersion(LanguageVersion version) - => WithParseOptions(options => options.WithLanguageVersion(version)); + protected static Func VisualBasic14 = WithVisualBasicLanguageVersion(Microsoft.CodeAnalysis.VisualBasic.LanguageVersion.VisualBasic14); + + protected static Func[] DefaultConfig = new[] { + InterceptorsEnabled, CSharpPreview, VisualBasic14 }; + + protected static Func WithCSharpLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion version) + => WithCSharpParseOptions(options => options.WithLanguageVersion(version)); + + protected static Func WithVisualBasicLanguageVersion(Microsoft.CodeAnalysis.VisualBasic.LanguageVersion version) + => WithVisualBasicParseOptions(options => options.WithLanguageVersion(version)); protected static Func WithFeatures(params KeyValuePair[] features) => WithParseOptions(options => options.WithFeatures(features)); - protected static Func WithParseOptions(Func func) + protected static Func WithParseOptions(Func func) + { + if (func is null) return static (solution, _) => solution; + return (solution, projectId) => + { + var options = solution.GetProject(projectId)?.ParseOptions; + if (options is null) return solution; + return solution.WithProjectParseOptions(projectId, func(options)); + }; + } + protected static Func WithCSharpParseOptions(Func func) { if (func is null) return static (solution, _) => solution; return (solution, projectId) => @@ -89,35 +159,77 @@ protected static Func WithParseOptions(Func WithVisualBasicParseOptions(Func func) + { + if (func is null) return static (solution, _) => solution; + return (solution, projectId) => + { + var options = solution.GetProject(projectId)?.ParseOptions as VisualBasicParseOptions; + if (options is null) return solution; + return solution.WithProjectParseOptions(projectId, func(options)); + }; + } } public class Verifier : Verifier where TAnalyzer : DiagnosticAnalyzer, new() { - protected Task VerifyAsync(string source, + internal Task SqlVerifyAsync(string sql, + params DiagnosticResult[] expected) => SqlVerifyAsync(sql, SqlParseInputFlags.None, expected); + + internal Task SqlVerifyAsync(string sql, SqlParseInputFlags sqlParseInputFlags, params DiagnosticResult[] expected) + { + var cs = $$""" + using Dapper; + using System.Data.Common; + + [DapperAot(false)] + class SomeCode + { + public void Foo(DbConnection conn) => conn.Execute(@"{{sql.Replace("\"", "\"\"")}}"); + } + """; + return CSVerifyAsync(cs, DefaultConfig, expected, SqlSyntax.SqlServer, sqlParseInputFlags | SqlParseInputFlags.DebugMode); + } + + internal Task CSVerifyAsync(string source, Func[] transforms, - params DiagnosticResult[] expected) - => base.VerifyAsync(source, transforms, expected); - protected Task VerifyAsync(string source, params DiagnosticResult[] expected) - => base.VerifyAsync(source, DefaultConfig, expected); + DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None) + => base.CSVerifyAsync(source, transforms, expected, sqlSyntax, sqlParseInputFlags); - new protected Task VerifyAsync(string source, + new internal Task CSVerifyAsync(string source, Func[] transforms, - params DiagnosticResult[] expected) - where TCodeFixProvider : CodeFixProvider, new() - => VerifyAsync(source, transforms, expected); + DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None) + where TCodeFix : CodeFixProvider, new() + => CSVerifyAsync(source, transforms, expected, sqlSyntax, sqlParseInputFlags); + + internal Task VBVerifyAsync(string source, + Func[] transforms, + DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer) + => base.VBVerifyAsync(source, transforms, expected, sqlSyntax); + + new internal Task VBVerifyAsync(string source, + Func[] transforms, + DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer) + where TCodeFix : CodeFixProvider, new() + => VBVerifyAsync(source, transforms, expected, sqlSyntax); - protected Task VerifyAsync(string source, params DiagnosticResult[] expected) - where TCodeFixProvider : CodeFixProvider, new() - => VerifyAsync(source, DefaultConfig, expected); -} -public class Verifier : Verifier - where TAnalyzer : DiagnosticAnalyzer, new() - where TCodeFixProvider : CodeFixProvider, new() -{ - protected Task VerifyAsync(string source, params DiagnosticResult[] expected) - => VerifyAsync(source, DefaultConfig, expected); - protected Task VerifyAsync(string source, Func[] transforms, - params DiagnosticResult[] expected) - => VerifyAsync(source, transforms, expected); } +//public class Verifier : Verifier +// where TAnalyzer : DiagnosticAnalyzer, new() +// where TCodeFix : CodeFixProvider, new() +//{ +// protected Task CSVerifyAsync(string source, Func[] transforms, +// DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer) +// => CSVerifyAsync(source, transforms, expected, sqlSyntax); + +// protected Task VBVerifyAsync(string source, Func[] transforms, +// DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer) +// => VBVerifyAsync(source, transforms, expected, sqlSyntax); + +// protected Task VerifyAsync(string source, +// Func[] transforms, +// params DiagnosticResult[] expected) +// => base.VerifyAsync(source, transforms, expected); +// protected Task VerifyAsync(string source, params DiagnosticResult[] expected) +// => base.VerifyAsync(source, DefaultConfig, expected); +//}