Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0820968
fix: Parse Svelte script blocks with the SFC masker
wn-mitch Aug 28, 2026
42acff9
fix: Node destructured bindings, not the pattern text
wn-mitch Aug 28, 2026
c4d6b3b
fix: Resolve Rust use declarations through the AST
wn-mitch Aug 28, 2026
690f0ef
fix: Rescue Svelte static imports when the script fails to parse
Sep 1, 2026
aab82ec
fix: Document that a require initializer may be a member access
Sep 1, 2026
a412fbf
fix: Correct three Rust use-declaration edge cases
Sep 1, 2026
cbad248
fix: Bound Rust use-path resolution to the crate
Sep 2, 2026
5aa5c49
fix: Dedupe rescued Svelte imports on every path
Sep 2, 2026
2b5df71
fix: Resolve Rust use paths anchored on their own module
Sep 2, 2026
f702e17
fix: Anchor crate:: at the crate root that owns the file
Sep 2, 2026
739d54d
fix: Anchor auto-target crate modules and record use aliases
Sep 2, 2026
8ef89c6
fix: Attribute deep self:: paths and mod.rs module aliases
Sep 2, 2026
973a5b8
fix: Close the remaining use-path resolution gaps
Sep 2, 2026
050c4b5
fix: Make the SFC mask byte-preserving
Sep 2, 2026
ddccf89
fix: Never label a node with destructuring pattern source
Sep 2, 2026
e566e6f
fix: Resolve items of a file-backed parent module
Sep 2, 2026
47a6cbb
fix: Pick the grammar that parses every SFC script block
Sep 2, 2026
671ab19
fix: Resolve keyword-only Rust use paths
Sep 2, 2026
0af6a24
fix: Use the TSX grammar for TSX single-file components
Sep 2, 2026
ea44203
fix: Only treat a literal require specifier as a CJS import
Sep 2, 2026
3d90c10
fix: Treat a bare Rust use path as a crate from edition 2018
Sep 2, 2026
84e7943
refactor: Dedupe rescued edges via build.dedupe_edges
Sep 2, 2026
c083c1d
fix: Rescue Svelte imports on a hard extractor failure too
Sep 2, 2026
0f34d29
fix: Report unreadable SFC components instead of returning empty
Sep 3, 2026
4a1d901
fix: Read a workspace member's inherited Rust edition
Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 82 additions & 27 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
_ts_collect_type_refs,
_ts_heritage_clause_entries,
_ts_walk_class_members,
_sfc_mask_non_script,
_vue_mask_non_script,
_walk_js_tree,
_walk_python_tree,
Expand Down Expand Up @@ -1599,56 +1600,107 @@ def _emit_rescued_import(


def extract_svelte(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 6 callees (efferent coupling); 13 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 17 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 17 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 18 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 18 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 18 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 18 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 18 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 19 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 20 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 20 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 20 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 20 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_svelte()

fans out to 7 callees (efferent coupling); 21 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

"""Extract imports from .svelte files: script-block via JS AST + template regex fallback.
"""Extract imports, symbols, and type refs from a ``.svelte`` component.

Tree-sitter only sees the <script> block. Svelte template syntax like
{#await import('./X.svelte')} lives in the markup layer and is invisible
to the JS parser, so a regex pass covers those dynamic imports.
Masks the non-``<script>`` regions and parses the script with the grammar
its ``lang`` implies (``tsx``->TSX, ``js``/``jsx``->JS, ``ts`` or unset->TS;
TS is a superset of JS so it is a safe default), mirroring
:func:`extract_vue`. Feeding the whole component to the JS grammar makes the
markup a top-level ERROR node, so ``import_statement`` and declaration nodes
are never reached and everything but a stray symbol is dropped (#713).

Both script blocks of a Svelte 5 component survive the mask, so a
``<script module>`` block is parsed alongside the instance script.

A regex pass then recovers ``import('...')`` dynamic imports, which the AST
pass does not edge and which legally live in markup-layer template syntax
such as ``{#await import('./X.svelte')}`` — outside every script block, and
so blanked out of the masked source the AST sees.

Static imports are edged by the AST pass, so the old regex rescue for them
would double-emit and is used only when the script fails to parse, where
the AST pass reaches no ``import_statement`` at all.
"""
result = _extract_generic(path, _JS_CONFIG)
try:
import re as _re
src = path.read_text(encoding="utf-8", errors="replace")
except OSError as e:
# Report the failure rather than returning an empty result: an
# unreadable component is indistinguishable from an empty one
# otherwise, and extract() warns on `error` (#2551).
return {"nodes": [], "edges": [], "error": f"cannot read {path}: {e}"}

masked, lang = _sfc_mask_non_script(src)
if lang == "tsx":
config = _TSX_CONFIG
elif lang in ("js", "jsx"):
config = _JS_CONFIG
else: # "ts" or unspecified — default to the TS grammar (superset of JS)
config = _TS_CONFIG

result = _extract_generic(path, config, source_override=masked.encode("utf-8"))

try:
import re as _re
existing_ids = {n["id"] for n in result.get("nodes", [])}
# Source file node ID must match the one _extract_generic creates:
# _make_id(str(path)) - single arg, no stem prefix. Otherwise the source
# endpoint is a phantom node and build_from_json drops the edge (#701).
file_node_id = _make_id(str(path))
if result.get("error") and not result.get("nodes"):
# A hard failure (grammar missing, unreadable source) returns no
# nodes at all, so a rescued edge would have a dangling source and
# be dropped at build time. Mint the file node _extract_generic
# would have, in the same shape, so the rescue is worth running.
# `error` stays on the result for extract()'s own reporting.
result.setdefault("nodes", []).append({
"id": file_node_id, "label": path.name,
"file_type": "code", "source_file": str(path),
"source_location": "L1",
})
existing_ids.add(file_node_id)
aliases = _load_tsconfig_aliases(path.parent)
base_url = _load_tsconfig_base_url(path.parent)
# Scanned over the raw source, not the masked one, so template-layer
# dynamic imports are seen. Resolution is shared with the static pass:
# relative paths and tsconfig aliases probe real on-disk extensions
# (#716, #701), and a target that IS a real file emits an edge stamped
# with target_file instead of an absolute-id ghost stub (#2195).
for m in _re.finditer(r"""import\(\s*['"]([^'"]+)['"]\s*\)""", src):
raw = m.group(1)
if not raw:
continue
# Resolution + emit shared with the static pass below: relative
# paths and tsconfig aliases probe real on-disk extensions (#716,
# #701), and a target that IS a real file emits an edge stamped
# with target_file instead of an absolute-id ghost stub (#2195).
_emit_rescued_import(
result, existing_ids, file_node_id, path, raw,
"dynamic_import", aliases, base_url,
)
# Static imports inside <script> blocks. The JS tree-sitter parser fed
# the full .svelte file produces a top-level ERROR node (HTML markup
# is not valid JS), so import_statement nodes are never reached and
# static imports are silently dropped (#713). Regex over each script
# body recovers them.
script_re = _re.compile(
r"<script\b[^>]*>([\s\S]*?)</script\s*>", _re.IGNORECASE
)
static_import_re = _re.compile(
r"""import\s+(?:[^'"`;]+?\s+from\s+)?['"]([^'"]+)['"]"""
)
for script_match in script_re.finditer(src):
script_body = script_match.group(1)
for m in static_import_re.finditer(script_body):
if result.get("parse_errors") or result.get("error"):
# The AST pass produced no usable tree — the masked script parsed
# WITH errors (`parse_errors`, so `import_statement` nodes may
# never have been reached), or it failed outright (`error`: the
# grammar is missing, the source unreadable). Both leave imports
# unedged, so fall back to the regex rescue the pre-mask extractor
# ran unconditionally.
# Gated on the failure so a clean parse does not double-emit: the
# AST already edges those specifiers. Scanned over the MASKED
# source, whose only surviving text is the script regions.
static_import_re = _re.compile(
r"""import\s+(?:[^'"`;]+?\s+from\s+)?['"]([^'"]+)['"]"""
)
for m in static_import_re.finditer(masked):
raw = m.group(1)
if not raw:
continue
_emit_rescued_import(
result, existing_ids, file_node_id, path, raw,
"imports_from", aliases, base_url,
)
# Both rescues can repeat an edge: a recovered parse edges some
# imports before the error node, and a specifier imported twice in one
# file (`import('./X')` in two markup branches) is matched twice.
# Imported inside the function, as cli.py and watch.py do, to keep
# extract.py free of a module-level dependency on build.py.
from graphify.build import dedupe_edges
result["edges"] = dedupe_edges(result.get("edges", []))
except Exception:
pass
return result
Expand Down Expand Up @@ -1732,10 +1784,13 @@ def extract_vue(path: Path) -> dict:
"""
try:
src = path.read_text(encoding="utf-8", errors="replace")
except OSError:
return {"nodes": [], "edges": []}
except OSError as e:
# Report the failure rather than returning an empty result: an
# unreadable component is indistinguishable from an empty one
# otherwise, and extract() warns on `error` (#2551).
return {"nodes": [], "edges": [], "error": f"cannot read {path}: {e}"}

masked, lang = _vue_mask_non_script(src)
masked, lang = _sfc_mask_non_script(src)
if lang == "tsx":
config = _TSX_CONFIG
elif lang in ("js", "jsx"):
Expand Down
134 changes: 129 additions & 5 deletions graphify/extractors/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1912,6 +1912,36 @@ def _find_require_call(value_node):
return _find_require_call(obj)
return None

def _is_require_initializer(value_node, source: bytes) -> bool:
"""True when a declarator's initializer is a ``require(...)`` call.

The call may be reached through member access — ``require('./m').sub`` is
still a CJS import, and :func:`_require_imports_js` edges that form, so
both sides must agree on what counts or a destructured binding would be
both imported and shadowed by a local stub.

A STRING-LITERAL specifier is required, because that is what
:func:`_require_imports_js` needs to emit an edge at all. A computed
specifier (``require(name)``, ``require('./' + x)``) names no module it
can resolve, so suppressing the local binding for one would delete the
name from the graph outright rather than repoint it.

``_find_require_call`` matches the call *shape* only — any
``identifier(...)`` — and leaves the callee-name check to its callers, so
it must not be used alone to recognise a CJS import.
"""
call = _find_require_call(value_node)
if call is None:
return False
fn = call.child_by_field_name("function")
if fn is None or _read_text(fn, source) != "require":
return False
args = call.child_by_field_name("arguments")
if args is None:
return False
return any(arg.type == "string" for arg in args.children)


def _require_imports_js(node, source: bytes, importer_nid: str, stem: str, edges: list, str_path: str) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_require_imports_js()

high coupling complexity (Ca·Ce = 15).

Grounded coupling-delta finding (deterministic), not an LLM guess.

"""Detect CommonJS require imports inside lexical_declaration / variable_declaration.

Expand Down Expand Up @@ -2001,6 +2031,11 @@ def _require_imports_js(node, source: bytes, importer_nid: str, stem: str, edges

_JS_FUNCTION_VALUE_TYPES = frozenset({"arrow_function", "function_expression", "function", "generator_function"})

# Declarator `name` node types that are a destructuring PATTERN rather than a
# single bound identifier. Their `name` field is the pattern source, never a
# symbol, so it must not be used as a node label.
_JS_DESTRUCTURING_PATTERNS = frozenset({"object_pattern", "array_pattern"})


def _scan_js_nested_function_declarations(
container_node, parent_nid: str, *, source: bytes, config,
Expand Down Expand Up @@ -2130,6 +2165,61 @@ def _js_member_assignment_target(left, source: bytes):
return ("prototype", inner_obj_name, member_name)
return None

_JS_PATTERN_TYPES = frozenset({"object_pattern", "array_pattern"})


def _js_pattern_bound_names(name_node, source: bytes) -> list[str]:
"""Return the identifiers a destructuring declarator actually binds.

``const { a, b: renamed, c = 1, ...rest } = x`` binds ``a``, ``renamed``,
``c`` and ``rest`` — not the text of the pattern. Reading the declarator's
``name`` field verbatim instead mints one node labelled with the whole
pattern source (``{ a, b: renamed, c = 1, ...rest }``), which names no
symbol and can never be the target of a reference. Svelte 5 makes the shape
universal — every component destructures ``$props()`` — but the same
declarator shape is ordinary JS/TS.

Walks only the binding side: a ``pair_pattern``'s value (``b: renamed``
binds ``renamed``, not the property key ``b``) and an assignment pattern's
left operand (``c = $bindable()`` binds ``c``, not ``$bindable``). Nested
patterns recurse, so ``{ deep: { inner } }`` binds ``inner``. Returns an
empty list for a non-pattern node.
"""
if name_node is None or name_node.type not in _JS_PATTERN_TYPES:
return []
names: list[str] = []

def visit(node) -> None:
t = node.type
if t in ("identifier", "shorthand_property_identifier_pattern"):
text = _read_text(node, source)
if text and text not in names:
names.append(text)
return
if t == "pair_pattern":
# `key: target` — only the value side is bound.
value = node.child_by_field_name("value")
if value is not None:
visit(value)
return
if t in ("object_assignment_pattern", "assignment_pattern"):
# `target = default` — the default is an expression, not a binding.
left = node.child_by_field_name("left")
if left is None:
left = node.named_children[0] if node.named_children else None
if left is not None:
visit(left)
return
if t in ("rest_pattern", "object_pattern", "array_pattern"):
for child in node.named_children:
visit(child)
return
# Anything else (type annotations, holes in `[a, , c]`) binds nothing.

visit(name_node)
return names


def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_js_extra_walk()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

nodes: list, edges: list, seen_ids: set, function_bodies: list,
parent_class_nid: str | None, add_node_fn, add_edge_fn,
Expand Down Expand Up @@ -2303,13 +2393,47 @@ def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
):
# Simple exported identifiers are part of the module API
# regardless of initializer shape. Keep other scalar noise suppressed.
const_nid = None
if name_node:
const_name = _read_text(name_node, source)
line = child.start_point[0] + 1
const_nid = _make_id(stem, const_name)
add_node_fn(const_nid, const_name, line)
add_edge_fn(file_nid, const_nid, "contains", line)
const_found = True
# A destructuring declarator binds several names and
# its `name` field is the pattern source, not a
# symbol — node each identifier it actually binds.
const_names = _js_pattern_bound_names(name_node, source)
if const_names and _is_require_initializer(value, source):
# `const { doWork } = require('./lib')` binds an
# IMPORT, not a local definition. `_require_imports_js`
# already edges those names at the file level; noding
# them here would shadow the real cross-file target,
# so a call to `doWork()` would resolve to this file's
# stub instead of the callee's definition.
const_names = []
elif not const_names:
if name_node.type in _JS_DESTRUCTURING_PATTERNS:
# A pattern that binds nothing (`const {} =`,
# `const { a: {} } =`) has no name to node.
# Falling back to the pattern SOURCE is the
# bug `_js_pattern_bound_names` exists to
# fix — it mints `{ a: {} }` as a symbol.
const_names = []
else:
const_names = [_read_text(name_node, source)]
for const_name in const_names:
# A name that normalizes to nothing would collapse
# the id to the absolute file-stem and leak the
# scan path (#1899); skip it, as the arrow branch does.
if not const_name or not normalize_id(const_name):
continue
nid = _make_id(stem, const_name)
add_node_fn(nid, const_name, line)
add_edge_fn(file_nid, nid, "contains", line)
const_found = True
if const_nid is None:
# Closures in the initializer are attributed to
# the first binding; a destructured initializer
# has no single owning symbol.
const_nid = nid
if const_nid is not None:
# #2552: `const handler = wrapper(async (req) => …)`
# created the const node above but, unlike the arrow
# branch, never tracked the callback's body — so
Expand Down
Loading