Skip to content

Fix false-positive DAP214 for {=XXX} literal-replacement syntax - #191

Merged
mgravell merged 3 commits into
DapperLib:mainfrom
HarnageaGabriel:fix/dap214-literal-tokens
Aug 20, 2026
Merged

Fix false-positive DAP214 for {=XXX} literal-replacement syntax#191
mgravell merged 3 commits into
DapperLib:mainfrom
HarnageaGabriel:fix/dap214-literal-tokens

Conversation

@HarnageaGabriel

Copy link
Copy Markdown
Contributor

Summary

Fixes a false-positive DAP214 (Variable @X is not declared and no corresponding parameter exists) when SQL uses the {=XXX} literal-replacement syntax, e.g.:

var result = await db.QueryAsync(
    "select Id, Name from Users where UserTypeId = {=Admin}",
    new { Admin = 123 });

Related issue: DapperLib/Dapper#2181

Root cause

Diagnosed by @andreasblueher in a comment on the issue:

the root of this issue is that GetParameters() only matches @param/:param/?param/$param via ParameterRegex — it does not recognize {=Admin} literal tokens.

SqlTools.GetParameters(string?) only scanned SQL with ParameterRegex (the @/:/?/$ prefixed forms), ignoring the already-existing LiteralTokens regex that matches {=XXX}. That method feeds SqlTools.GetUniqueParameters, which DapperAnalyzer.SharedGetParametersToInclude uses to build the filter of which anonymous-type/parameter-object members get included as known SQL parameters (ParameterMode.Filter).

Because Admin was invisible to that filter, it never made it into the ImmutableArray<SqlParameter> passed to TSqlProcessor.Execute. Separately, TSqlProcessor does rewrite {=Admin} to @Admin before AST parsing (via the same LiteralTokens regex) — so the parser sees a reference to @Admin, finds it isn't in the known-parameters set, and reports DAP214, even though the caller supplied it.

Fix

SqlTools.GetParameters now merges matches from both ParameterRegex and the existing LiteralTokens regex, so members referenced only via {=XXX} are correctly recognized as used/known parameters. No regex patterns were changed — only how their results are combined.

Test plan

  • dotnet build — analyzer project builds clean
  • Added [InlineData("select Id from Users where UserTypeId = {=Admin}", "Admin")] to SqlTests.DetectParameters
  • Added DAP214.NoFalsePositive_Issue2181 regression test: verifies {=Admin} with a matching parameter member produces zero diagnostics
  • dotnet test on test/Dapper.AOT.Test (net10.0): all 158 Verifiers diagnostic tests pass, including all other DAP21x cases (no regressions)
  • Full test run across net48/net8.0/net10.0: only pre-existing, unrelated failure (DateOnlyTimeOnlyPostgreSqlTests.ReadDateOnly, a DateOnlyIConvertible cast issue in Postgres integration tests, unrelated to this change)

SqlTools.GetParameters() only matched @/:/?/$ prefixed parameters via
ParameterRegex, ignoring the {=XXX} literal-replacement syntax handled
by the existing LiteralTokens regex. That meant a member referenced
only via {=XXX} was filtered out of the known-parameters set built in
DapperAnalyzer.SharedGetParametersToInclude, so when TSqlProcessor
later rewrote {=XXX} to @xxx for AST parsing, the variable appeared
undeclared, triggering a false DAP214.

Fix merges ParameterRegex and LiteralTokens matches in GetParameters().

Root cause diagnosed by @andreasblueher in DapperLib/Dapper#2181.
@mgravell

Copy link
Copy Markdown
Member

Thanks for this — the root-cause analysis is spot on (and thanks @andreasblueher for the original diagnosis): TSqlProcessor rewrites {=Admin} to @Admin before parsing, but GetParameters never told the known-parameters set about it, so DAP214 fires on SQL that vanilla Dapper handles fine. Definitely want this fixed.

One catch, though: GetParameters feeds two consumers, and they need different answers. The analyzer's known-parameters set should absolutely include literal tokens (your fix). But the same result also drives the generator's include-filter — so with this change, a member referenced only via {=x} now gets emitted as a bound DbParameter, which is wrong: a literal token must be injected into the SQL text, never parameter-bound (and since Dapper.AOT doesn't implement literal injection yet, those call-sites shouldn't gain parameters at all). You can see this concretely by running the InterceptorTests golden suite on current main: the TsqlTips fixture picks up a // parameter map: a it shouldn't have. (Heads-up on that suite: it auto-rewrites the .output.* files and then asserts, so run it twice and review the git diff — a change there is a generated-output change.)

So the shape I'd suggest: keep the literal names out of the path the generator consumes, and include them only where the analyzer builds the set handed to TSqlProcessor — e.g. a GetParameters(sql, includeLiteralTokens: true) overload or a separate method, so the parameter-vs-literal distinction survives. That distinction becomes load-bearing later: literal injection is on the roadmap, and when it lands, {=x} members need to be recognized and routed differently from @x ones. A small extra test with both forms in one SQL (where A = @a and B = {=b}) would pin the split nicely.

Happy to help get this over the line whichever way suits you — if you'd like, I can push the adjustment onto your branch (if "allow edits by maintainers" is enabled) and you can review, or leave it with you with the notes above. Either way this is a good catch and I'd like to land it.

@mgravell

Copy link
Copy Markdown
Member

Side note: the feature parity (the missing literal injection) is what I'm working on right now, so... timely.

… generator

SharedGetParametersToInclude fed two consumers that need different
answers: the analyzer's DAP214 validation (which must know about
{=XXX} literal tokens to avoid the false positive) and the
generator's BuildParameterMap (which must NOT treat literal-only
members as bound parameters, since literal injection isn't
implemented yet and {=x} values need to be injected into the SQL
text rather than bound as a DbParameter).

Threads an includeLiteralTokens flag through GetParameters,
GetUniqueParameters and SharedGetParametersToInclude, defaulting to
false (pre-fix behavior). The analyzer call site opts in explicitly;
the generator keeps the default.

Adds a regression case mixing a bound parameter and a literal token
in the same query, plus an InterceptorTests golden fixture confirming
the generated parameter map only binds the real parameter.

Per review from @mgravell on DapperLib#191.
@HarnageaGabriel

Copy link
Copy Markdown
Contributor Author

Good catch, thanks for the detailed breakdown. Pushed a fix along exactly the lines you suggested: threaded an includeLiteralTokens flag through GetParameters/GetUniqueParameters/SharedGetParametersToInclude, defaulting to false. The analyzer's validation path (CommonParse, feeds DAP214) opts in with true; the generator's BuildParameterMap keeps the default false so literal-only members no longer leak into the bound-parameter list.

Added a regression case mixing @a and {=Admin} in the same query, plus an InterceptorTests golden fixture (LiteralTokens) confirming the generated parameter map only binds a. Ran the affected suite twice per the golden-file convention and diffed — clean, only a shows up now.

No need to push to the branch yourself, but appreciate the offer. Let me know if the shape looks right or if you'd rather see it split differently.

Two resolutions:

- DapperAnalyzer.cs: main appended SuppliesSqlParameters at the end of the
  class, where this branch had only added the missing trailing newline.
  Kept both; the two changes do not interact (includeLiteralTokens is
  consumed earlier in SharedGetParametersToInclude, SuppliesSqlParameters
  gates the no-parameters-detected report in a branch this work does not
  touch).

- LiteralTokens.output.txt and LiteralTokens.output.netfx.txt: refreshed
  for the reworded DAP000 scorecard on main. These snapshots are new on
  this branch, so they merged cleanly while carrying the old wording.
@mgravell

Copy link
Copy Markdown
Member

merging with thanks

@mgravell
mgravell merged commit ac9e4fa into DapperLib:main Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants