Files
graphify/tests/test_ts_namespace.py
Paulo Pinto 869aaf7502 fix(extract): emit a node for TS namespace / module containers
`namespace Foo {}` parses as `internal_module` and `module Bar {}` (and
ambient `declare module "pkg" {}`) as a named `module` node. Neither kind
was in `class_types`/`function_types` nor handled by an extra-walk, so the
container produced no node — its members were still reached by the default
recurse, but the namespace/module itself was invisible to the graph and its
members lost their namespace context.

Add `_ts_extra_walk`, dispatched for TypeScript after `_js_extra_walk`,
mirroring `_csharp_extra_walk`: it emits a container node + a file→container
`contains` edge and recurses the body, leaving members file-contained as
before. `internal_module` exposes `name`/`body` fields; `module` exposes
none, so name (identifier / nested_identifier / quote-stripped string) and
body (`statement_block`) are found positionally. The `is_named` guard skips
the anonymous `module` keyword token, which shares the `module` type string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:38:29 +01:00

74 lines
2.7 KiB
Python

"""Regression tests: TypeScript namespace/module container nodes.
`namespace Foo {}` parses as `internal_module`, `module Bar {}` and ambient
`declare module "pkg" {}` as `module`. Neither was in `class_types`/`function_types`
nor handled by an extra-walk, so the container produced no node. Members were
still reached by the default recurse but the namespace itself was invisible.
"""
from pathlib import Path
from graphify.extract import _file_stem, _make_id, extract
def _write(path: Path, text: str) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
return path
def _node_label(result: dict, file: str, sym: str):
nid = _make_id(_file_stem(Path(file)), sym)
return next((n["label"] for n in result["nodes"] if n["id"] == nid), None)
def _has_node(result: dict, file: str, sym: str) -> bool:
return _node_label(result, file, sym) is not None
def test_namespace_is_node(tmp_path):
f = _write(tmp_path / "src" / "n.ts",
"export namespace Geometry { export const PI = 3.14; }\n")
r = extract([f], cache_root=tmp_path)
assert _has_node(r, "src/n.ts", "Geometry")
def test_module_keyword_is_node(tmp_path):
f = _write(tmp_path / "src" / "m.ts",
"module Legacy { export class Thing {} }\n")
r = extract([f], cache_root=tmp_path)
assert _has_node(r, "src/m.ts", "Legacy")
def test_nested_namespace_name(tmp_path):
f = _write(tmp_path / "src" / "nn.ts",
"namespace App.Core.Util { export const v = 1; }\n")
r = extract([f], cache_root=tmp_path)
assert _node_label(r, "src/nn.ts", "App.Core.Util") == "App.Core.Util"
def test_namespace_members_still_extracted(tmp_path):
"""The container node must not cost us the members the default recurse reached."""
f = _write(tmp_path / "src" / "n.ts",
"namespace Shapes {\n"
" export class Circle {}\n"
" export function area() { return 0; }\n"
"}\n")
r = extract([f], cache_root=tmp_path)
assert _has_node(r, "src/n.ts", "Shapes")
assert _has_node(r, "src/n.ts", "Circle")
assert _has_node(r, "src/n.ts", "area")
def test_ambient_string_module_quotes_stripped(tmp_path):
f = _write(tmp_path / "src" / "amb.ts",
'declare module "pkg-name" { export const z = 3; }\n')
r = extract([f], cache_root=tmp_path)
assert _node_label(r, "src/amb.ts", "pkg-name") == "pkg-name"
def test_namespace_node_not_emitted_in_js(tmp_path):
"""The handler is TS-only; plain JS has no namespace syntax to confuse it."""
f = _write(tmp_path / "src" / "p.js", "function ok() {}\n")
r = extract([f], cache_root=tmp_path)
assert _has_node(r, "src/p.js", "ok")