fix JS/TS phantom god-nodes from arrow-fn locals and drop markdown code-block orphan nodes (#1077)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Safi
2026-05-29 13:46:30 +01:00
co-authored by Claude Sonnet 4.6
parent 5642c1b533
commit 925eb81ec1
2 changed files with 66 additions and 38 deletions
+24 -32
View File
@@ -1696,10 +1696,24 @@ def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
# CJS require imports — emit edges, do not block other lexical_declaration handling
require_found = _require_imports_js(node, source, file_nid, stem, edges, str_path)
# Scope guard (#1077): only emit nodes for module-level declarations.
# Without this, `const x = ...` inside an arrow callback (e.g. inside
# `describe(() => { const set = new Set(...) })`) emits a bare-named
# node, and the same name collides across unrelated files producing
# phantom god-nodes. Bodies of arrow functions are walked separately
# via function_bodies, so we never need to emit nodes for locals here.
parent = node.parent
is_module_level = parent is not None and (
parent.type == "program"
or (parent.type == "export_statement"
and parent.parent is not None
and parent.parent.type == "program")
)
# Arrow function declarations and module-level const literals (lexical_declaration only)
arrow_found = False
const_found = False
if node.type == "lexical_declaration":
if node.type == "lexical_declaration" and is_module_level:
for child in node.children:
if child.type == "variable_declarator":
value = child.child_by_field_name("value")
@@ -7819,14 +7833,17 @@ def extract_markdown(path: Path) -> dict:
Produces nodes for:
- The file itself
- Each heading (# / ## / ### etc.)
- Each fenced code block (``` ... ```)
Produces edges for:
- file --contains--> heading
- parent heading --contains--> child heading (nesting by level)
- heading --contains--> code block
- heading --references--> other node (when backtick `Name` matches a known pattern)
Fenced code blocks (``` ... ```) are skipped during parsing so their
contents don't get treated as headings, but no node is emitted for
them they were always orphans (only a single contains edge to the
parent doc) and inflated the disconnected-component count (#1077).
No tree-sitter dependency pure line-by-line parsing.
"""
try:
@@ -7858,44 +7875,19 @@ def extract_markdown(path: Path) -> dict:
# Track heading stack for nesting: [(level, nid), ...]
heading_stack: list[tuple[int, str]] = []
in_code_block = False
code_block_lang: str | None = None
code_block_start: int = 0
code_block_lines: list[str] = []
code_block_count = 0
lines = source.splitlines()
for line_num_0, line_text in enumerate(lines):
line_num = line_num_0 + 1
# Toggle fenced code blocks
# Skip over fenced code blocks so their contents are not parsed as
# headings, but do not emit nodes/edges for them (#1077).
stripped = line_text.strip()
if stripped.startswith("```"):
if not in_code_block:
in_code_block = True
code_block_lang = stripped[3:].strip().split()[0] if len(stripped) > 3 else None
code_block_start = line_num
code_block_lines = []
continue
else:
# End of code block — create a node
in_code_block = False
code_block_count += 1
snippet = "\n".join(code_block_lines[:3]) # first 3 lines as preview
label = f"code:{code_block_lang}" if code_block_lang else f"code:block{code_block_count}"
if snippet:
# Use first meaningful line as label hint
first_line = code_block_lines[0].strip()[:60] if code_block_lines else ""
if first_line:
label = f"{label} ({first_line})"
cb_nid = _make_id(stem, f"codeblock_{code_block_count}")
add_node(cb_nid, label, code_block_start)
# Attach to nearest heading or file
parent = heading_stack[-1][1] if heading_stack else file_nid
add_edge(parent, cb_nid, "contains", code_block_start)
continue
in_code_block = not in_code_block
continue
if in_code_block:
code_block_lines.append(line_text)
continue
# Detect headings: # Heading, ## Heading, etc.
+42 -6
View File
@@ -1080,6 +1080,36 @@ def test_ts_static_template_literal_resolved():
f"Static template literal import not resolved: {targets}"
def test_js_local_const_does_not_emit_phantom_node(tmp_path):
"""Local const/let/var inside an arrow callback must NOT emit a node (#1077).
Previously `_js_extra_walk` recursed into arrow_function bodies and
emitted a node for every `const x = ...` inside e.g. `describe(() => {})`,
so bare names like `set`, `sorted` collided across unrelated test files.
"""
src = (
"describe('suite', () => {\n"
" const inner = new Set([1, 2, 3]);\n"
" let other = [1, 2];\n"
"});\n"
"\n"
"const moduleConst = new Set([4, 5]);\n"
"export const exportedConst = { a: 1 };\n"
)
f = tmp_path / "scope_guard.js"
f.write_text(src)
r = extract_js(f)
labels = _labels(r)
# Locals inside the arrow callback must not produce nodes.
assert "inner" not in labels, f"phantom node for arrow-body local 'inner': {labels}"
assert "other" not in labels, f"phantom node for arrow-body local 'other': {labels}"
# Module-level consts should still produce nodes.
assert "moduleConst" in labels, f"module-level const 'moduleConst' missing: {labels}"
assert "exportedConst" in labels, f"exported const 'exportedConst' missing: {labels}"
# ── Markdown ─────────────────────────────────────────────────────────────────
from graphify.extract import extract_markdown
@@ -1102,19 +1132,25 @@ def test_markdown_finds_nested_heading():
labels = _labels(r)
assert any("Database Migration" in l for l in labels)
def test_markdown_finds_code_blocks():
def test_markdown_skips_fenced_code_blocks():
"""Fenced code blocks should NOT emit nodes (#1077).
They were always orphans (single contains edge to parent doc) and
inflated the disconnected-component count. We still skip over their
*contents* when parsing so the inside of a fence is not misread as a
heading.
"""
r = extract_markdown(FIXTURES / "deploy_guide.md")
labels = _labels(r)
assert any("code:bash" in l for l in labels)
assert any("code:sql" in l for l in labels)
assert any("code:python" in l for l in labels)
assert not any(l.startswith("code:") for l in labels), \
f"Expected no code:* nodes after #1077 fix, got: {[l for l in labels if l.startswith('code:')]}"
def test_markdown_contains_edges():
"""Headings and code blocks should be connected via 'contains' edges."""
"""Headings should be connected via 'contains' edges (file->h, h->h)."""
r = extract_markdown(FIXTURES / "deploy_guide.md")
assert "contains" in _relations(r)
contains_edges = [e for e in r["edges"] if e["relation"] == "contains"]
assert len(contains_edges) >= 5 # file->h1, h1->h2s, h2->h3, h2->codeblocks
assert len(contains_edges) >= 4 # file->h1, h1->h2s, h2->h3
def test_markdown_no_dangling_edges():
r = extract_markdown(FIXTURES / "deploy_guide.md")