fix(extract): skip dynamic template literals in import() args
import(`./handlers/${name}`) previously produced a garbage edge to a
path containing the unresolved ${name} expression. Now detects
template_substitution child nodes and breaks without emitting an edge.
Static template literals (no interpolation) still resolve correctly.
Adds 2 new tests: one asserting dynamic templates produce no edge,
one asserting static templates resolve like plain strings.
This commit is contained in:
+45
-38
@@ -255,46 +255,53 @@ def _dynamic_import_js(node, source: bytes, caller_nid: str, str_path: str, edge
|
||||
if args is None:
|
||||
return True # It's an import() but no args — skip
|
||||
for arg in args.children:
|
||||
if arg.type in ("string", "template_string"):
|
||||
raw = _read_text(arg, source).strip("'\"` ")
|
||||
if not raw:
|
||||
if arg.type == "template_string":
|
||||
# Skip dynamic template literals — path can't be statically resolved
|
||||
if any(c.type == "template_substitution" for c in arg.children):
|
||||
break
|
||||
# Resolve path using the same logic as static imports
|
||||
if raw.startswith("."):
|
||||
resolved = Path(os.path.normpath(Path(str_path).parent / raw))
|
||||
if resolved.suffix == ".js":
|
||||
resolved = resolved.with_suffix(".ts")
|
||||
elif resolved.suffix == ".jsx":
|
||||
resolved = resolved.with_suffix(".tsx")
|
||||
tgt_nid = _make_id(str(resolved))
|
||||
else:
|
||||
aliases = _load_tsconfig_aliases(Path(str_path).parent)
|
||||
resolved_alias = None
|
||||
for alias_prefix, alias_base in aliases.items():
|
||||
if raw == alias_prefix or raw.startswith(alias_prefix + "/"):
|
||||
rest = raw[len(alias_prefix):].lstrip("/")
|
||||
resolved_alias = Path(os.path.normpath(Path(alias_base) / rest))
|
||||
break
|
||||
if resolved_alias is not None:
|
||||
tgt_nid = _make_id(str(resolved_alias))
|
||||
else:
|
||||
module_name = raw.split("/")[-1]
|
||||
if not module_name:
|
||||
break
|
||||
tgt_nid = _make_id(module_name)
|
||||
pair = (caller_nid, tgt_nid)
|
||||
if pair not in seen_dyn_pairs:
|
||||
seen_dyn_pairs.add(pair)
|
||||
edges.append({
|
||||
"source": caller_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
"weight": 1.0,
|
||||
})
|
||||
raw = _read_text(arg, source).strip("`")
|
||||
elif arg.type == "string":
|
||||
raw = _read_text(arg, source).strip("'\" ")
|
||||
else:
|
||||
continue
|
||||
if not raw:
|
||||
break
|
||||
# Resolve path using the same logic as static imports
|
||||
if raw.startswith("."):
|
||||
resolved = Path(os.path.normpath(Path(str_path).parent / raw))
|
||||
if resolved.suffix == ".js":
|
||||
resolved = resolved.with_suffix(".ts")
|
||||
elif resolved.suffix == ".jsx":
|
||||
resolved = resolved.with_suffix(".tsx")
|
||||
tgt_nid = _make_id(str(resolved))
|
||||
else:
|
||||
aliases = _load_tsconfig_aliases(Path(str_path).parent)
|
||||
resolved_alias = None
|
||||
for alias_prefix, alias_base in aliases.items():
|
||||
if raw == alias_prefix or raw.startswith(alias_prefix + "/"):
|
||||
rest = raw[len(alias_prefix):].lstrip("/")
|
||||
resolved_alias = Path(os.path.normpath(Path(alias_base) / rest))
|
||||
break
|
||||
if resolved_alias is not None:
|
||||
tgt_nid = _make_id(str(resolved_alias))
|
||||
else:
|
||||
module_name = raw.split("/")[-1]
|
||||
if not module_name:
|
||||
break
|
||||
tgt_nid = _make_id(module_name)
|
||||
pair = (caller_nid, tgt_nid)
|
||||
if pair not in seen_dyn_pairs:
|
||||
seen_dyn_pairs.add(pair)
|
||||
edges.append({
|
||||
"source": caller_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports_from",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
"weight": 1.0,
|
||||
})
|
||||
break
|
||||
return True
|
||||
|
||||
|
||||
|
||||
Vendored
+13
-1
@@ -13,8 +13,20 @@ async function pollMessages(orgId: string) {
|
||||
await commsQueue.add('check-inbound', { orgId });
|
||||
}
|
||||
|
||||
async function loadHandler(handlerName: string) {
|
||||
// dynamic template literal — path not statically resolvable, should produce no edge
|
||||
const mod = await import(`./handlers/${handlerName}`);
|
||||
return mod.default;
|
||||
}
|
||||
|
||||
async function loadStatic() {
|
||||
// static template literal (no interpolation) — should resolve like a plain string
|
||||
const { helper } = await import(`./staticHelper`);
|
||||
return helper;
|
||||
}
|
||||
|
||||
function syncOnly() {
|
||||
logger.info('no dynamic imports here');
|
||||
}
|
||||
|
||||
export { processInbound, pollMessages, syncOnly };
|
||||
export { processInbound, pollMessages, loadHandler, loadStatic, syncOnly };
|
||||
|
||||
@@ -607,3 +607,21 @@ def test_ts_no_dynamic_import_in_sync_fn():
|
||||
sync_imports = [e for e in r["edges"]
|
||||
if e["source"] == sync_nid and e["relation"] == "imports_from"]
|
||||
assert len(sync_imports) == 0
|
||||
|
||||
def test_ts_dynamic_template_literal_skipped():
|
||||
"""Dynamic template literals (with ${}) must not produce an imports_from edge."""
|
||||
r = extract_js(FIXTURES / "dynamic_import.ts")
|
||||
targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"}
|
||||
# loadHandler uses `./handlers/${handlerName}` — no static path, must be absent
|
||||
assert not any("handler" in t.lower() and "$" in t for t in targets), \
|
||||
f"Garbage edge from dynamic template literal found: {targets}"
|
||||
# More robust: no target should contain a brace character
|
||||
assert not any("{" in t or "}" in t for t in targets), \
|
||||
f"Target contains unresolved template expression: {targets}"
|
||||
|
||||
def test_ts_static_template_literal_resolved():
|
||||
"""Static template literals (no ${}) should resolve the same as a plain string."""
|
||||
r = extract_js(FIXTURES / "dynamic_import.ts")
|
||||
targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"}
|
||||
assert any("statichelper" in t.lower() for t in targets), \
|
||||
f"Static template literal import not resolved: {targets}"
|
||||
|
||||
Reference in New Issue
Block a user