Skip to content

Type-based APIs become an explicit non-goal, and say so (DAP056) - #214

Open
mgravell wants to merge 2 commits into
mainfrom
type-apis-nongoal
Open

Type-based APIs become an explicit non-goal, and say so (DAP056)#214
mgravell wants to merge 2 commits into
mainfrom
type-apis-nongoal

Conversation

@mgravell

Copy link
Copy Markdown
Member

Includes #213 (parity notes), since it edits the same rows.

Dapper's Type-argument overloads pick the row type at execution time, which is the one thing compile-time generation cannot follow. Supporting them means a build-time registry of candidate types plus runtime dispatch keyed on Type — a lookup on the hot path, an open world for the trimmer, and dispatch ILC cannot resolve unless every announced type is rooted. That is the shape #206 was closed over; rebuilding it for a lower-value feature would be incoherent.

It costs no behaviour. These already generated nothing — a five-call probe handled 0 of 5. What changes is what we say: DAP056 now fires at the call-site and names the generic overload, replacing either silence or a confusing DAP009 about an unexpected type parameter.

Detection is call-site sensitive, deliberately

GetRowParser<T>(concreteType: null) — the default, and the common case — is perfectly supportable; only a call that actually passes a Type defers the decision. So the check looks at the arguments, not the symbol. GetRowParser<T>() stays supported, and the fixture pins both halves:

handled 2 of 7 enabled call-sites (0 unsupported API, 5 refused with diagnostics, 0 skipped silently)

GetRowParser(concreteType) goes too

It is the one row where "use the generic form" is not available advice, because the choice is data-dependent — so docs/rules/DAP056.md gives the pattern that is actually correct under AOT: a switch over GetRowParser<T>() per candidate type. More code, and strictly better, because it roots exactly the types you use where a registry roots everything registered.

Reported from the generator, not the analyzer

Most of these overloads carry no sql string, so the analyzer never sees them (the reason 7 of them were in the silent bucket). SkippedSourceState gains a reason enum — plain data, so the cached model stays equatable and ModelShapeTests stays happy — and Generate emits from it.

Knock-on effects

  • the surface report grows a non-goal: Type-based bucket for overloads whose Type parameter is required, so silent drops 27 → 22 and undiagnosed-unsupported 13 → 12; the mute total goes 40 → 34;
  • GetRowParser's existing fixture goes from 1 silent skip to 0;
  • the optional-Type overload stays in its previous bucket, because a symbol cannot say what a call will pass — the report's stated bound;
  • parity.md marks the affected rows 🚫 with the decision recorded, and its "announced types" item shrinks to cover only untyped parameters: it is no longer a design that unlocks several rows.

Suites green on net8.0 (369) and net48 (362); solution builds clean.

#212 made "what does Dapper.AOT do with this overload?" a derived fact, so this
stops the table answering it from memory. ApiSurface.expected.txt is named as
the source of truth, with the three things it deliberately does not cover -
behaviour, per-call-site refusals, and non-extension statics like Format and
LookupDbType, which are called rather than intercepted.

Three ❓ rows are settled by the report and now cite it: Parse (all three
overloads sit outside the generator's name filter), Format/ReplaceLiterals
(same, and Format is not even an extension method), and CommandDefinition -
which was the worst of the stale rows. It read as an unverified question about
CommandFlags.Pipelined; it is in fact 27 overloads, every one skipped in
silence, which is high impact rather than the "med" it carried. Split into the
overload row and a separate CommandFlags row, since the second is moot until the
first is fixed.

The delta summary now leads with both measurements rather than one, because they
answer different questions: 40 of 110 overloads tell the consumer nothing, and
677 of 793 corpus tests pass. Adds a row 0 - saying *something* at those 40 - as
the cheapest item on the list: it supports no new API, and turns a silent
runtime AOT failure into a build warning.

Remaining ❓ now means "needs a corpus run" unless a row says otherwise.
Decided 2026-08-26. Dapper's Type-argument overloads pick the row type at
execution time, which is the one thing compile-time generation cannot follow.
Supporting them means a build-time registry of candidate types plus runtime
dispatch keyed on Type - a lookup on the hot path, an open world for the
trimmer, and dispatch ILC cannot resolve unless every announced type is rooted.
That is the shape #206 was closed over; rebuilding it for a lower-value feature
would be incoherent.

Costs no behaviour: these already generated nothing (a five-call probe handled
0 of 5). What changes is what we say. DAP056 fires at the call-site and names
the generic overload, replacing either silence or a confusing DAP009 about an
unexpected 'type' parameter.

Detection is call-site sensitive, not symbol-level, because
GetRowParser<T>(concreteType: null) - the default, and the common case - is
perfectly supportable. Only a call that actually passes a Type defers the
decision. GetRowParser<T>() stays supported and the fixture pins both halves.

GetRowParser(concreteType) goes too, on Marc's call. It is the one row where
"use the generic form" is not available advice - the choice is data-dependent -
so the rule doc gives the pattern that is actually correct under AOT: a switch
over GetRowParser<T>() per candidate, which roots exactly the types used rather
than everything registered.

Reported from the generator rather than the analyzer: most of these overloads
carry no `sql` string, so the analyzer never sees them. SkippedSourceState
gains a reason enum (plain data, so the cached model stays equatable) and
Generate emits from it.

Knock-on: the surface report grows a 'non-goal: Type-based' bucket for overloads
whose Type parameter is required, so the silent count drops 27 -> 22 and
undiagnosed-unsupported 13 -> 12. GetRowParser's fixture goes from 1 silent skip
to 0. The optional-Type overload stays where it was, since the symbol cannot
say what a call will pass.
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.

1 participant