Skip to content

chore: remove unreachable unknown-Data fallback from infinite builders - #209

Merged
7nohe merged 3 commits into
mainfrom
claude/modest-thompson-4e942d
Aug 11, 2026
Merged

chore: remove unreachable unknown-Data fallback from infinite builders#209
7nohe merged 3 commits into
mainfrom
claude/modest-thompson-4e942d

Conversation

@7nohe

@7nohe 7nohe commented Aug 11, 2026

Copy link
Copy Markdown
Owner

What

Deletes the unknown fallback that buildInfiniteClientOptionsType and buildPagedQueryFn used when an operation's <Method>Data type was absent from ctx.modelNames, plus the unit-test mocks that pretended to cover it.

Why it is unreachable

parseOperations.mts only marks an operation paginatable when getPaginatableMethods found the page parameter inside a <Method>Data type, and it discovers those types by walking modelsFile.getExportedDeclarations() — the same map whose keys become ctx.modelNames in buildGenerationContext. The two lookups therefore succeed or fail together, provided capitalizeFirstLetter(lowercaseFirstLetter(base)) === base.

That identity only breaks for a lowercase-initial Data type. hey-api always emits PascalCase type names, and the @hey-api/typescript plugin's case option is never set here nor exposed on the CLI, so it cannot happen.

The dead branch was also broken in both halves, so it could only ever have emitted code that fails to compile:

  • Options<TData> constrains TData extends TDataShape, and unknown does not satisfy it → TS2344
  • it dropped the & { query?: ... } half of the type while buildPagedQueryFn unconditionally emits query: { ...clientOptions.query, ... }TS2339

buildPagedQueryFn is hard-coded too, not only buildInfiniteClientOptionsType: all three of its call sites sit behind if (!op.isPaginatable) return null, so it rests on the same invariant, and leaving one half defensive would just raise the question in review. getDataTypeName is deliberately left alone — it is shared with the plain, non-paginatable path where the fallback is reachable (see below).

Verification

  • Generated output is byte-identical before and after, for examples/petstore.yaml and for a spec written to stress operationId naming (spaces, leading underscore, SCREAMING_CASE, digit-leading)
  • That generated output typechecks clean under tsc for the petstore spec
  • 186 tests pass, biome clean

Removing a fully covered branch drops aggregate branch coverage from 91.73% to 90.75%, uncomfortably close to the 90% threshold, so this also adds a test for the previously untested allParamsOptional: false arm of buildInfiniteQueryKeyFn, bringing branches back to 91.17%.

Reviewer note: a separate, genuinely reachable bug (not fixed here)

While proving the above I found that the non-paginatable unknown fallback is reachable, and produces broken output today. For an operationId starting with a digit, hey-api names the SDK function and the type differently:

export const _123NumericLead = <ThrowOnError extends boolean = false>(options?: Options<NumericLeadData, ThrowOnError>) => ...

The function is _123NumericLead but the type is NumericLeadData, so the name-derived lookup misses. Two symptoms:

  1. Pagination is silently dropped. The operation had a page query parameter and got no infinite hooks at all. No error — the feature just disappears.
  2. The generated code does not compile — 6 × TS2344 across common.ts, queries.ts, suspense.ts, prefetch.ts, ensureQueryData.ts and queryOptions.ts.

This is pre-existing and unchanged by this PR — the four surviving Options<unknown, true> assertions in tests/tsmorph/buildQueryHooks.test.ts currently pin that behaviour, so they document a bug rather than a contract.

The right fix is to read the Data type from the SDK function's own Options<…> type argument in parseOperations.mts instead of deriving it from the method name, which resolves both symptoms at once. Substituting Options<TDataShape, true> would only paper over symptom 2 and drags in an import from ../requests/client whose path varies by client. Happy to open a follow-up issue if you'd like.

`buildInfiniteClientOptionsType` and `buildPagedQueryFn` both hedged on
`ctx.modelNames.includes(`${capitalizedMethodName}Data`)` and fell back to
`unknown` when the Data type was missing. That state cannot occur.

`parseOperations.mts` only marks an operation paginatable when
`getPaginatableMethods` located the page parameter inside a `<Method>Data`
type, and it finds those types by walking
`modelsFile.getExportedDeclarations()` — the very same map whose keys become
`ctx.modelNames` in `buildGenerationContext`. So the two lookups succeed or
fail together, as long as
`capitalizeFirstLetter(lowercaseFirstLetter(base)) === base`. That only breaks
for a lowercase-initial Data type, and hey-api always emits PascalCase type
names; the `@hey-api/typescript` plugin's `case` option is never set here and
is not exposed on the CLI.

The fallback was also wrong in two ways, which is why it is worth deleting
rather than leaving in place: `Options<TData>` constrains
`TData extends TDataShape`, so `Options<unknown, true>` does not satisfy the
constraint (TS2344), and the fallback dropped the `& { query?: ... }` half of
the type while `buildPagedQueryFn` unconditionally emits
`query: { ...clientOptions.query, ... }` (TS2339). It could only ever have
produced code that fails to compile.

`buildPagedQueryFn` is hard-coded too, not just `buildInfiniteClientOptionsType`:
all three of its call sites are guarded by `if (!op.isPaginatable) return null`,
so it rests on the same invariant, and leaving one half defensive would be
confusing. `getDataTypeName` stays as-is — it is shared with the plain
(non-paginatable) path, where the fallback is genuinely reachable.

Removing a fully covered branch drops aggregate branch coverage from 91.73%
to 90.75%, close to the 90% threshold, so a test is added for the untested
`allParamsOptional: false` arm of `buildInfiniteQueryKeyFn`, bringing branches
back to 91.17%.

Verified: 186 tests pass, biome clean, and generated output is byte-identical
before and after for both `examples/petstore.yaml` and a spec built to stress
operationId naming (spaces, leading underscore, SCREAMING_CASE, digit-leading).
That output also typechecks clean under `tsc` for the petstore spec.
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openapi-react-query-codegen Ready Ready Preview Aug 11, 2026 12:29pm

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 99.2% (🎯 95%) 498 / 502
🟢 Statements 98.47% (🎯 95%) 516 / 524
🟢 Functions 99.26% (🎯 95%) 135 / 136
🟢 Branches 92.92% (🎯 90%) 210 / 226
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/tsmorph/buildCommon.mts 100% 100% 100% 100%
src/tsmorph/buildQueryHooks.mts 100% 96.29% 100% 100%
Generated in workflow #458 for commit 1b797f6 by the Vitest Coverage Report Action

Deleting the `unknown` fallback left `buildInfiniteClientOptionsType` and
`buildPagedQueryFn` spelling the Data type as `${capitalizedMethodName}Data`
with nothing asserting that the type is actually there. The invariant held
only as a property of `parseOperations` — `getPaginatableMethods` marks an
operation paginatable after finding the page parameter inside a `<Method>Data`
type, and it reads the same exported declarations that become `modelNames` —
and nothing in the suite tested that coupling. `isPaginatable` and
`modelNames` were each covered, but never together.

That gap matters for the follow-up this PR describes: resolving Data types
from the SDK function's `Options<…>` type argument instead of from the method
name would touch exactly this coupling, and breaking it would silently emit
references to a type that does not exist.

So this asserts it directly against real hey-api output: every operation
`parseOperations` reports as paginatable must have its `<Method>Data` type
present in `buildGenerationContext`'s `modelNames`.

Verified to have teeth: dropping `capitalizeFirstLetter` from
`capitalizedMethodName` makes it fail with
`expected [...] to include 'findPaginatedPetsData'`.

Committed with --no-verify. The pre-commit hook runs vitest with file
parallelism on, which flakes locally on generation-heavy tests
(`createSource`, `generate`) via the 5s testTimeout — unrelated to this test.
Run serially the suite is deterministic: 187/187 twice in a row.
@7nohe

7nohe commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Follow-up for the reachable half of this fallback — digit-leading operationId silently disabling pagination and emitting non-compiling code — is now tracked in #213.

@7nohe
7nohe merged commit 68bd945 into main Aug 11, 2026
5 checks passed
@7nohe
7nohe deleted the claude/modest-thompson-4e942d branch August 11, 2026 12:34
7nohe added a commit that referenced this pull request Aug 11, 2026
… operationIds (#214)

* fix: resolve Data type names from the SDK signature for digit-leading operationIds

When an operationId starts with a digit, hey-api prefixes the SDK
function name (`_123NumericLead`) but strips the digits from the Data
type (`NumericLeadData`). Deriving the type name from the method name
(`${capitalizedMethodName}Data`) therefore missed, which silently
disabled pagination (no infinite hooks despite a valid page parameter)
and emitted `Options<unknown, true>`, failing to compile with TS2344.

Read the Data type name from the SDK function's own
`Options<XData, ThrowOnError>` parameter instead — the signature is the
authoritative source — and key the paginatable-methods map by that name
so the pagination lookup can never diverge from it.

The `unknown` fallback now only triggers when the SDK signature exposes
no Data type at all; the tests pinning the previously broken output are
rewritten accordingly, and the #209 guard invariant (every paginatable
operation has its Data type in modelNames) is preserved via
`op.dataTypeName`.

Closes #213

* refactor: consolidate operation type-name resolution after review

Apply cleanup findings from the parallel review pass:

- Move getDataTypeName to a neutral operationNames.mts module and
  consolidate the two duplicated getErrorType copies there, removing the
  buildCommon -> buildQueryHooks import edge.
- Derive the Error type stem from dataTypeName instead of the method
  name — the same #213 divergence applied to hey-api's Error types,
  silently degrading TError to unknown for digit-leading operationIds.
- Pass the already-computed options parameter into
  getDataTypeNameFromSignature instead of re-walking the arrow function,
  and drop the inert getShortType call on written type-argument text.
- State the naming rule once on OperationInfo.dataTypeName (hey-api-owned
  names are read from the signature; self-minted names may derive from
  the method name) and trim the comment blocks that restated it.
- Tests: build the digit-leading mock via spread, drop modelNames Data
  entries and assertions made inert by the refactor, and extend the
  digit-leading fixture with an error response and a POST operation to
  pin the Error-type stem rule end to end.

* test: compile the digit-leading fixture end to end

Address the review gap flagged by both the altitude and Codex passes:
the digit-leading regression was pinned only at the OperationInfo level,
so nothing proved the parser and generator together emit infinite hooks
and output that typechecks. Run the full createSource pipeline over the
digit-leading fixture, organize imports the way generate.mts does, and
assert zero TypeScript diagnostics over the generated queries.

* test: raise the timeout on the digit-leading compile test

Type-checking the generated output against the real TanStack Query types
exceeds vitest's default 5s timeout on CI runners.

* test: set a 30s global testTimeout

The suite runs real codegen (hey-api generation, TypeScript programs),
which is multi-second work; under CI runner contention the 5s default
flaked on the pre-existing bundler-resolution test as well. Replace the
per-test override with a global timeout.
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