mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-17 12:57:18 +00:00
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>
This commit is contained in:
@@ -2587,6 +2587,56 @@ def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
|
||||
return False
|
||||
|
||||
|
||||
# ── TS extra walk for namespace / module declarations ─────────────────────────
|
||||
|
||||
def _ts_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
|
||||
nodes: list, edges: list, seen_ids: set, function_bodies: list,
|
||||
parent_class_nid: str | None, add_node_fn, add_edge_fn,
|
||||
walk_fn) -> bool:
|
||||
"""Emit a container node for a TS `namespace`/`module` declaration.
|
||||
|
||||
`namespace Foo {}` parses as `internal_module` (with `name`/`body` fields);
|
||||
`module Bar {}` and ambient `declare module "pkg" {}` parse as a named
|
||||
`module` node that exposes no fields, so its name and body are found
|
||||
positionally. Without this the container was never a node — its members were
|
||||
still reached by the default recurse but lost their namespace context. The
|
||||
members stay file-contained (parity with C#'s `_csharp_extra_walk`); the
|
||||
namespace becomes a sibling marker node so it is queryable. Returns True if
|
||||
handled.
|
||||
|
||||
The guard requires `is_named` because the anonymous `module` keyword token
|
||||
shares the `module` type string and would otherwise match here.
|
||||
"""
|
||||
if node.is_named and node.type in ("internal_module", "module"):
|
||||
name_node = node.child_by_field_name("name")
|
||||
if name_node is None:
|
||||
for child in node.children:
|
||||
if child.is_named and child.type in (
|
||||
"identifier", "nested_identifier", "string"):
|
||||
name_node = child
|
||||
break
|
||||
body = node.child_by_field_name("body")
|
||||
if body is None:
|
||||
for child in node.children:
|
||||
if child.type == "statement_block":
|
||||
body = child
|
||||
break
|
||||
if name_node is not None:
|
||||
ns_name = _read_text(name_node, source)
|
||||
if name_node.type == "string":
|
||||
ns_name = ns_name.strip("'\"`")
|
||||
if ns_name:
|
||||
ns_nid = _make_id(stem, ns_name)
|
||||
line = node.start_point[0] + 1
|
||||
add_node_fn(ns_nid, ns_name, line)
|
||||
add_edge_fn(file_nid, ns_nid, "contains", line)
|
||||
if body is not None:
|
||||
for child in body.children:
|
||||
walk_fn(child, parent_class_nid)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ── C# extra walk for namespace declarations ──────────────────────────────────
|
||||
|
||||
def _csharp_namespace_name(node, source: bytes) -> str:
|
||||
@@ -4441,6 +4491,13 @@ def _extract_generic(
|
||||
callable_def_nids, local_bound_names):
|
||||
return
|
||||
|
||||
# TS namespace / module containers (internal_module, module)
|
||||
if config.ts_module == "tree_sitter_typescript":
|
||||
if _ts_extra_walk(node, source, file_nid, stem, str_path,
|
||||
nodes, edges, seen_ids, function_bodies,
|
||||
parent_class_nid, add_node, add_edge, walk):
|
||||
return
|
||||
|
||||
if config.ts_module == "tree_sitter_c_sharp":
|
||||
if _csharp_extra_walk(node, source, file_nid, stem, str_path,
|
||||
nodes, edges, seen_ids, function_bodies,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user