mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-15 08:29:26 +00:00
fix(extract): parse TS 5 export type star re-exports (#3942)
Landed from PR #4303 (squashed, authorship preserved).
This commit is contained in:
1 parent
fb6e7c7888
commit
9dafae2acc
2 files changed
+109
-1
No files matched your search
+32
-1
@@ -2019,6 +2019,33 @@ def _ts_mask_candidate_is_malformed(
|
||||
return False
|
||||
|
||||
|
||||
_TS_EXPORT_TYPE_STAR_RE = re.compile(rb"\bexport\s+(type)\s*\*")
|
||||
|
||||
|
||||
def _normalize_ts_export_type_star(source: bytes) -> bytes | None:
|
||||
"""Rewrite ``export type * from "..."`` to a plain ``export * from "..."``.
|
||||
|
||||
TS 5.0's type-only star re-export has no equivalent in tree-sitter-typescript
|
||||
0.23 (the latest release as of #3942): the grammar raises an ERROR on the
|
||||
``type`` keyword in this position and the whole file is dropped, losing
|
||||
every symbol, not just the one statement. The construct is erased at
|
||||
compile time, the same as ``import type`` / ``export type { X }``, so the
|
||||
graph only needs the re-export edge, not the type-only distinction —
|
||||
blank out ``type`` (keeping every other byte and offset identical) so the
|
||||
grammar parses the ordinary, already-supported ``export * from "..."``
|
||||
form instead. Unlike ``import(...)``, this prefix is never ambiguous with
|
||||
a runtime expression, so no AST-based disambiguation pass is needed here.
|
||||
"""
|
||||
matches = list(_TS_EXPORT_TYPE_STAR_RE.finditer(source))
|
||||
if not matches:
|
||||
return None
|
||||
masked = bytearray(source)
|
||||
for match in matches:
|
||||
start, end = match.span(1)
|
||||
masked[start:end] = b" " * (end - start)
|
||||
return bytes(masked)
|
||||
|
||||
|
||||
def _normalize_ts_import_types(source: bytes, *, tsx: bool = False) -> bytes | None:
|
||||
"""Rewrite only syntactic TypeScript ``import(...)`` type arguments.
|
||||
|
||||
@@ -2132,7 +2159,11 @@ def extract_js(path: Path) -> dict:
|
||||
if is_ts:
|
||||
try:
|
||||
source = _read_source_bytes(path)
|
||||
source_override = _normalize_ts_import_types(source, tsx=suffix == ".tsx")
|
||||
working = _normalize_ts_export_type_star(source) or source
|
||||
normalized = _normalize_ts_import_types(working, tsx=suffix == ".tsx")
|
||||
source_override = normalized if normalized is not None else (
|
||||
working if working is not source else None
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
result = _extract_generic(path, config, source_override=source_override)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""#3942: TypeScript 5.0's `export type * from "..."` has no equivalent in
|
||||
tree-sitter-typescript 0.23 (the latest release) — the grammar raises an
|
||||
ERROR on the `type` keyword in this position, and the whole file is dropped,
|
||||
losing every symbol, not just the one statement.
|
||||
|
||||
The construct is erased at compile time, the same as `import type` /
|
||||
`export type { X }`, so the graph only needs the re-export edge. Blanking
|
||||
out `type` (same byte length, same offsets) before parsing lets the grammar
|
||||
read the ordinary, already-supported `export * from "..."` form instead.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import _normalize_ts_export_type_star, extract
|
||||
|
||||
|
||||
def _extract(tmp_path: Path, files: dict[str, str]):
|
||||
for name, body in files.items():
|
||||
p = tmp_path / name
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(body, encoding="utf-8")
|
||||
old = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmp_path)
|
||||
r = extract([Path(n) for n in files],
|
||||
cache_root=tmp_path / ".cache", parallel=False)
|
||||
finally:
|
||||
os.chdir(old)
|
||||
return r
|
||||
|
||||
|
||||
def test_export_type_star_no_longer_a_syntax_error(tmp_path, capsys):
|
||||
r = _extract(tmp_path, {
|
||||
"index.ts": 'export type * from "./types.js";\n',
|
||||
"types.ts": "export type FetchFn = typeof globalThis.fetch;\n",
|
||||
})
|
||||
err = capsys.readouterr().err
|
||||
assert "syntax error" not in err, err
|
||||
|
||||
|
||||
def test_export_type_star_still_produces_the_re_export_edge(tmp_path):
|
||||
r = _extract(tmp_path, {
|
||||
"index.ts": 'export type * from "./types.js";\n',
|
||||
"types.ts": "export type FetchFn = typeof globalThis.fetch;\n",
|
||||
})
|
||||
rels = {e["relation"] for e in r["edges"]
|
||||
if e["source"].endswith("index_ts") or e["source"] == "index"}
|
||||
assert "imports_from" in rels or "re_exports" in rels, r["edges"]
|
||||
# types.ts's own symbol must survive too — the whole point is that
|
||||
# neither file is dropped.
|
||||
assert "FetchFn" in {n["label"] for n in r["nodes"]}
|
||||
|
||||
|
||||
def test_export_type_named_form_is_unaffected(tmp_path):
|
||||
"""Control: the already-working named type re-export must not be touched
|
||||
by a regex that is specifically anchored on the `*` star form."""
|
||||
r = _extract(tmp_path, {
|
||||
"named.ts": 'export type { FetchFn } from "./types.js";\n',
|
||||
"types.ts": "export type FetchFn = typeof globalThis.fetch;\n",
|
||||
})
|
||||
type_only_edges = [e for e in r["edges"] if e.get("type_only")]
|
||||
assert type_only_edges, "the named form must keep its type_only stamp (#3123)"
|
||||
|
||||
|
||||
def test_normalize_function_only_blanks_the_type_keyword():
|
||||
source = b'export type * from "./types.js";\n'
|
||||
normalized = _normalize_ts_export_type_star(source)
|
||||
assert normalized is not None
|
||||
assert len(normalized) == len(source)
|
||||
assert normalized == b'export * from "./types.js";\n'
|
||||
|
||||
|
||||
def test_normalize_function_returns_none_when_no_match():
|
||||
assert _normalize_ts_export_type_star(b'export * from "./x";\n') is None
|
||||
assert _normalize_ts_export_type_star(b'export type { X } from "./x";\n') is None
|
||||
Reference in new issue
Block a user