Skip to content

fix(cli): generate type-only doc-test imports under verbatimModuleSyntax#33508

Merged
bartlomieju merged 3 commits into
denoland:mainfrom
fibibot:fix/doc-test-type-only-imports
Jun 8, 2026
Merged

fix(cli): generate type-only doc-test imports under verbatimModuleSyntax#33508
bartlomieju merged 3 commits into
denoland:mainfrom
fibibot:fix/doc-test-type-only-imports

Conversation

@fibibot

@fibibot fibibot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Summary

`deno test --doc` extracts each fenced ```ts` block and injects an `import { ... } from ""` statement that pulls in every named export, so examples can reference module-level bindings without spelling out the import. That synthesized import was always a value import, even for pure-TypeScript exports (`type` aliases, `interface`s, `export type { ... }` re-exports). Under `verbatimModuleSyntax: true` TypeScript flagged each one:

```
TS1484 [ERROR]: 'Bar' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
import { Bar } from "file:///.../main.ts";
~~~
```

Track type-only-ness alongside the named exports in `ExportCollector` and emit the per-specifier `type` modifier for those symbols, so the generated import becomes e.g. `import { type Bar, foo } from "..."`. Mixed exports stay in a single import statement.

Fixes #31385.

Test plan

  • Manually re-ran the issue's repro under canary; `deno test --doc main.ts` now passes type-checking and runs the doc test.
  • Updated the existing `test_extract_doc_tests` snapshot for `Args` (a `type` alias) and added a fresh case covering type alias + interface + class in the same file (verifies the per-specifier modifier and the value/type ordering in the generated import).
  • `cargo fmt -- --check`.

`deno test --doc` injects an `import { ... }` statement into each
extracted snippet pulling in *every* named export of the base file, so
that examples can reference them without spelling out the import. The
generated import was unconditionally a value import, even for symbols
that are pure TypeScript constructs (`type` aliases, `interface`s, or
`export type { ... }` re-exports). Under `verbatimModuleSyntax: true`
that crashed type-checking with `TS1484: 'X' is a type and must be
imported using a type-only import`.

Track type-only-ness alongside `named_exports` and emit the per-specifier
`type` modifier (`import { type Foo, bar } from "..."`) for those
symbols. Mixed exports stay in a single import statement.

Fixes denoland#31385.

@lunadogbot lunadogbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The collector now tracks named_type_only_exports as a strict subset of named_exports (every insertion site adds to the value set first or simultaneously), and to_import_specifiers flips is_type_only from a hard-coded false to a per-specifier lookup. Walked the cases:

  • export type Args = ... -> Decl::TsTypeAlias -> both sets. ✓
  • export interface Bar {} -> Decl::TsInterface -> both sets. ✓
  • export class Quux {} -> Decl::Class -> only value set. ✓
  • export type { Foo } -> named_export.type_only=true -> both sets. ✓
  • export { type Foo } -> named.is_type_only=true -> both sets. ✓
  • enum Foo -> only value set (correct -- enum has both name spaces, value-import is a strict superset).
  • BTreeSet ordering produces the alphabetical { type Bar, type Foo, Quux, useFoo } shown in the new snapshot. ✓
  • The old visit_export_named_specifier override is replaced by direct iteration of named_export.specifiers; nothing else in the file calls into the specifier visitor, so no double-counting.

One coordination note (not blocking): #33511 also rewrites visit_named_export, in incompatible ways -- this PR keeps the src.is_some() early-return; #33511 changes that branch to collect the re-export specifier names. Whichever lands second will conflict. If you want them stacked, the merge resolution is straightforward: keep the type-only tracking from this PR and apply the re-export-specifier loop from #33511 inside the src.is_some() branch (and remember to honour is_type_only there too).

…y-imports

# Conflicts:
#	cli/util/extract.rs
A name can be both a value export and a type export through declaration
merging (e.g. `export const Foo` + `export interface Foo`). The previous
logic marked such a name type-only, so the injected doc-test import became
`import { type Foo }`, dropping the value binding and breaking snippets
that use it under `verbatimModuleSyntax: true`.

Route every export insertion through helpers that enforce a value export
always winning over a type-only one, regardless of declaration order, and
add a regression test.
@bartlomieju
bartlomieju enabled auto-merge (squash) June 8, 2026 11:32
@bartlomieju
bartlomieju merged commit 0ce171f into denoland:main Jun 8, 2026
136 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.

Imports auto-inserted by Deno for doc test don't respect verbatimModuleSyntax

3 participants