fix(ts): a type-only re-export like export type * from './types' now parses (#2837)

TypeScript 5.0's type-only re-export of a whole module, `export type * from
'./x'` and `export type * as ns from './x'`, has no rule in the vendored
tree-sitter-typescript grammar (v0.23.2, the revision the kernel's crate
compiles) or in upstream master: `type` parses as an ERROR inside the export
statement. The graph was already right, since the import resolver reads the
statement from the file (PRs #1955 and #2526), but the error tree meant a
barrel of re-exports got the "parse produced no symbols" warning and a
parse_error row, `codegraph status` listed it among the files it could not
parse, and the native kernel deferred every file with such a line to wasm.

The TypeScript extractor's preParse (.ts and .tsx) now blanks that `type` to
four spaces where a statement starts (a line start, or after `;`, `{`, `}` or
a block comment) when `*` is followed by `from` or `as`, so the line reads as
the `export *` it is to the graph. Offsets are kept. The kernel route applies
the preParse before calling the kernel, so both engines parse the same bytes
and no Rust change is needed. A grammar patch would mean vendoring and
regenerating the grammar for both dialects, two wasm rebuilds and a kernel
off the crates.io crate, for a statement `export *` already models. A
literal pre-check skips other files: 16 ms over strapi's 4,659 TS files.

The only node change is an import node's stored statement text, which now
reads `export      * from './x'`. Import nodes are filtered from search,
explore and the context builder, and nothing in resolution reads it.

status-index-health.test.ts used `export type * from` as its unparseable
file (issue #2336); it now uses a file with merge-conflict markers.

Validation:
- New ts-export-type-star.test.ts, 15 tests: 13 fail with main's
  typescript.ts, 11 with the blank unwired, all pass with the fix (kernel
  and wasm, .ts and .tsx, LF and CRLF).
- A/B, kernel engine, main vs fix, dumps by natural keys: masto.js,
  kysely, TanStack/query, graffle and strapi keep every edge and ref
  (162,421 and 192,248 on strapi). The only changes are 883 import
  signatures, one per `export type *` statement, and 76 cleared warnings
  (masto.js 11, kysely 1, query 4, graffle 28, strapi 32). The Go and
  Python controls gin and requests are byte-identical.
- Kernel parity, typescript and tsx: 0 diffs on every sweep. Deferrals
  drop masto.js 11 to 0, graffle 203 to 13, strapi 105 to 69, query 32
  to 27, kysely 14 to 12; the rest are other grammar gaps.
- Index time, median of 3 interleaved: graffle 12.7 s vs 13.1 s, query
  11.2 s vs 11.4 s, within noise.

Fixes #2474

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-23 09:09:05 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 51c4124dad
commit cf3e6bc525
4 changed files with 277 additions and 5 deletions
+27 -1
View File
@@ -69,6 +69,32 @@ export function blankFlowSyntax(source: string, filePath?: string): string {
.replace(/\.\.\.(?=\s*[},])/g, ' ');
}
/** Whether the file writes `export type *` anywhere: most files don't, and skip the rest. */
const EXPORT_TYPE_STAR_HINT = /export\s+type\s*\*/;
/**
* `export type *` where a statement starts (a line's first word, or after `;`,
* `{`, `}` or the end of a block comment), followed by `from` or `as`.
*/
const EXPORT_TYPE_STAR = /(^|[;{}]|\*\/)([ \t]*export\s+)type(?=\s*\*\s*(?:from|as)\b)/gm;
/**
* TypeScript 5.0's type-only re-export of a whole module, `export type * from
* './types'` and `export type * as ns from './types'`, with its `type` blanked
* to spaces (offsets survive), so it reads as the `export *` it is to the
* graph. The grammar has no rule for it (nor does upstream's): `type` parsed
* as an error, so a barrel of re-exports was reported as producing no symbols
* and counted by `codegraph status` as a file it could not parse, and the
* kernel handed every such file to the wasm parser (#2474). Only a statement
* start is blanked, so a mention in a comment or a doc keeps its text. The
* import resolver reads the statement as written, `type` included, from the
* file itself (`extractReExports`). Applied before both engines parse (the
* kernel's preParse hoist), so they agree.
*/
export function blankExportTypeStar(source: string): string {
if (!EXPORT_TYPE_STAR_HINT.test(source)) return source;
return source.replace(EXPORT_TYPE_STAR, '$1$2 ');
}
/**
* Whether `node` is declared in a `declare module 'x' { … }` or `declare
* global { … }` body, which exports it without an `export` keyword.
@@ -147,7 +173,7 @@ export function isAmbientFunctionSignature(node: SyntaxNode, filePath: string):
}
export const typescriptExtractor: LanguageExtractor = {
preParse: blankFlowSyntax,
preParse: (source, filePath) => blankExportTypeStar(blankFlowSyntax(source, filePath)),
functionTypes: ['function_declaration', 'generator_function_declaration', 'arrow_function', 'function_expression', 'generator_function'],
isAmbientFunction: isAmbientFunctionSignature,
// `class` is a class expression, `register(class Widget extends Base {…})`: