Skip to content

Derive the gap table: classify Dapper's whole public surface, and pin it - #212

Merged
mgravell merged 1 commit into
honest-skip-bucketsfrom
api-surface-coverage
Aug 26, 2026
Merged

mgravell merged 1 commit into
honest-skip-bucketsfrom
api-surface-coverage

Conversation

@mgravell

Copy link
Copy Markdown
Member

Stacked on #211 (which it needs for IsVisibleToAnalyzer).

parity.md's statuses were remembered rather than checked — 20 rows carried ❓, and both of the ones I probed by hand this week turned out understated. This makes the mechanically-answerable half of the gap table derived: what does Dapper.AOT do with each public overload? parity.md then keeps only what a human has to supply — impact, complexity, and decisions like the runtime-registration non-goal.

How

The dispatch decision depends only on the method symbol, never on the call-site, so IsDapperMethod gains a symbol overload and the test classifies all 110 public SqlMapper extension methods without synthesising a single call. No database, no harness, about a second.

The report is checked in as ApiSurface.expected.txt and compared on every run: an overload added upstream fails the test instead of passing unnoticed, and a row moving into "skipped silently" shows up as a diff. On mismatch the test writes the new content next to the test assembly and says where, so accepting a change is a copy.

What it found

disposition count
candidate 40 generation is attempted
unsupported API (diagnosed) 16 refused, DAP001 names it
unsupported API (undiagnosed) 13 refused, nothing says so
skipped silently 27 dropped mute — vanilla Dapper under JIT, runtime failure under native AOT, no build-time signal
not inspected 9 outside the generator's name filter: AsList, Parse, GetTypeName, ReplaceLiterals, AsTableValuedParameter. Correct — they need no interception, and it is worth being able to see that nothing real is in this bucket

40 of 110 overloads give a consumer nothing to act on, almost all of them CommandDefinition-shaped. That is the finding; fixing it is separate work, and the report is what will show it fixed.

Bounds, stated in the file itself

  • It reports what happens to an overload, not to every call of one — a supported overload can still be refused at a particular call-site (unconstructable result type, generic-by-containment, …). Those refusals are diagnosed and covered by the interceptor fixtures.
  • It says nothing about behaviour. "Generated" is not "matches Dapper" — that is what round 5 found when 84 tests compiled clean and failed at runtime. Only the Dapper suite answers that, and parity.md should keep saying so.

Suites green on net8.0 (368) and net48 (361).

The parity table's statuses were remembered rather than checked, and the two I
probed by hand this week were both understated. This replaces the guessing for
the question that can be answered mechanically - "what does Dapper.AOT do with
this overload?" - and leaves parity.md the judgement it is actually good for:
impact, complexity, and decisions like the runtime-registration non-goal.

The dispatch decision turns out to depend only on the method symbol, never on
the call-site, so IsDapperMethod gains a symbol overload and the test classifies
all 110 public SqlMapper extension methods without synthesising a single call.
No database, no harness, runs in a second.

The five dispositions, and what they found:

  candidate                      40  generation is attempted
  unsupported API (diagnosed)    16  refused, DAP001 names it
  unsupported API (undiagnosed)  13  refused, nothing says so
  skipped silently               27  dropped mute - vanilla under JIT, runtime
                                     failure under AOT, no build-time signal
  not inspected                   9  outside the name filter: AsList, Parse,
                                     GetTypeName and friends, which is correct -
                                     they need no interception

So 40 of 110 overloads give a consumer nothing to act on, almost all of them
CommandDefinition-shaped. That is the finding; fixing it is separate work.

The report is checked in and compared on every run, so an overload added
upstream fails the test rather than passing unnoticed, and a row moving into
'skipped silently' shows up in a diff.

Bounded deliberately: this says what happens to an *overload*, not to every call
of one - a supported overload can still be refused at a call-site for reasons of
its own. And it says nothing about behaviour; only the Dapper suite does that.
@mgravell
mgravell merged commit 8294c09 into honest-skip-buckets Aug 26, 2026
mgravell added a commit that referenced this pull request Aug 26, 2026
* DAP000: separate refused-with-diagnostics from skipped-silently

The scorecard had two buckets, "unsupported API" and "skipped due to
diagnostics", and the second was not true of everything in it. A call-site can
be dropped with nothing said at all, and those were being counted as though a
diagnostic had explained them.

Measured on the Dapper suite, the old line read "82 unsupported API, 110
skipped due to diagnostics". It now reads "82 unsupported API, 75 refused with
diagnostics, 35 skipped silently" - so a third of the skips had no explanation
attached, and the instrument we have been steering by said otherwise.

Three sources, all now visible rather than inferred:

- CommandDefinition overloads. The analyzer only inspects call-sites with a
  string `sql` parameter (or one marked [Sql]); these carry the SQL inside the
  struct, so it never validates them and never reports. The generator sees them
  - it filters by method name - so they are counted, just not explained.
  IsVisibleToAnalyzer mirrors that entry condition so the generator can tell
  which of its skips anything will have reported.
- the self-binding guards from #197 and #198 (multi-exec over an expandable
  member; expandable alongside an output parameter). Documented in the fixture
  comments, invisible to consumers.
- GetRowParser's concreteType overload.

Adds CommandDefinitionOverloads as a pinned fixture, so the count moves if that
changes. Every golden .txt shifts with the message, and DAP004 asserts DAP000's
arguments explicitly, so it gains the new one.

Not fixed here: driving the silent count to zero. That needs a diagnostic at
each of those sites, which is a separate change - this one makes the number
visible so it can be driven down and kept there.

* Derive the gap table: classify Dapper's whole public surface, and pin it (#212)

The parity table's statuses were remembered rather than checked, and the two I
probed by hand this week were both understated. This replaces the guessing for
the question that can be answered mechanically - "what does Dapper.AOT do with
this overload?" - and leaves parity.md the judgement it is actually good for:
impact, complexity, and decisions like the runtime-registration non-goal.

The dispatch decision turns out to depend only on the method symbol, never on
the call-site, so IsDapperMethod gains a symbol overload and the test classifies
all 110 public SqlMapper extension methods without synthesising a single call.
No database, no harness, runs in a second.

The five dispositions, and what they found:

  candidate                      40  generation is attempted
  unsupported API (diagnosed)    16  refused, DAP001 names it
  unsupported API (undiagnosed)  13  refused, nothing says so
  skipped silently               27  dropped mute - vanilla under JIT, runtime
                                     failure under AOT, no build-time signal
  not inspected                   9  outside the name filter: AsList, Parse,
                                     GetTypeName and friends, which is correct -
                                     they need no interception

So 40 of 110 overloads give a consumer nothing to act on, almost all of them
CommandDefinition-shaped. That is the finding; fixing it is separate work.

The report is checked in and compared on every run, so an overload added
upstream fails the test rather than passing unnoticed, and a row moving into
'skipped silently' shows up in a diff.

Bounded deliberately: this says what happens to an *overload*, not to every call
of one - a supported overload can still be refused at a call-site for reasons of
its own. And it says nothing about behaviour; only the Dapper suite does that.
mgravell added a commit that referenced this pull request Aug 26, 2026
…213)

#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.
@mgravell
mgravell deleted the api-surface-coverage branch September 11, 2026 13:34
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