diff --git a/tests/test_js_destructured_export.py b/tests/test_js_destructured_export.py new file mode 100644 index 000000000..8dc75e9f5 --- /dev/null +++ b/tests/test_js_destructured_export.py @@ -0,0 +1,41 @@ +"""#2604 Class 2: an exported object-destructure assignment must emit one node +per exported name, not one combined node for the whole pattern. + +`export const { auth, handlers } = NextAuth(config)` is NextAuth v5's own +documented boilerplate (next-intl's `createNavigation()` is the same shape). +Before this fix it collapsed to a single node whose id joined every +destructured name (`stem_auth_handlers`) and whose label was the literal +pattern syntax (`"{ auth, handlers }"`). A single-name import elsewhere +(`import { auth } from './auth'`) looks for a node named `auth` alone, which +never existed, so the import edge -- and any call resolved through it -- +dangled. +""" +from __future__ import annotations + +from graphify.extract import extract + +_NEXTAUTH_HELPER = "function NextAuth(config) { return {}; }\n" + + +def _extract(tmp_path, files: dict[str, str]): + for name, body in files.items(): + p = tmp_path / name + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(body) + r = extract([tmp_path / n for n in files], + cache_root=tmp_path / "graphify-out", parallel=False) + lbl = {n["id"]: n["label"] for n in r["nodes"]} + return r, lbl + + +def test_exported_destructure_emits_one_node_per_name(tmp_path): + r, lbl = _extract(tmp_path, { + "auth.ts": _NEXTAUTH_HELPER + + "export const { auth, handlers, signIn, signOut } = NextAuth({});\n", + }) + labels = set(lbl.values()) + for name in ("auth", "handlers", "signIn", "signOut"): + assert name in labels, f"no node for exported name {name!r}; got {sorted(labels)}" + assert not any(label.startswith("{") for label in labels), ( + f"a combined-pattern label survived: {sorted(labels)}" + )