From 23d8a8798d4c9b6fff690223c664fdf7e91a87ed Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Wed, 16 Sep 2026 15:05:16 +0530 Subject: [PATCH] Add an end to end regression test for the reporter's exact repro A file that only imports a package subpath must resolve just like one that imports the bare package, using the exact package.json plus two file shape from the issue, instead of coming out fully disconnected from the framework it depends on. Co-Authored-By: Claude Sonnet 5 (cherry picked from commit 5fb7fc34c1e6f5ab4c97682e7a2d733db6dd09fe) --- tests/test_phantom_external_import.py | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/test_phantom_external_import.py b/tests/test_phantom_external_import.py index f250423a3..f55145f71 100644 --- a/tests/test_phantom_external_import.py +++ b/tests/test_phantom_external_import.py @@ -136,3 +136,54 @@ def test_multiple_tsx_files_do_not_all_alias_onto_one_python_file(tmp_path: Path if d.get("relation") == "imports_from" and ({u, v} & py_ids) ] assert not phantom, f"phantom edges onto colors.py: {phantom}" + + +def test_subpath_import_does_not_dangle_like_a_bare_import(tmp_path: Path): + """The reporter's exact repro for #3595: a package subpath import + ("next/image") produced no edge at all, while a bare import of the same + package ("next") resolved fine, so a file that only ever imports + subpaths came out fully disconnected from the framework it depends on.""" + _write( + tmp_path / "package.json", + '{"name": "mre", "dependencies": {"next": "16.2.7"}}\n', + ) + bare = _write( + tmp_path / "bare.ts", + 'import type { Metadata } from "next";\n' + "export const meta: Metadata = {};\n", + ) + subpath = _write( + tmp_path / "subpath.ts", + 'import Image from "next/image";\n' + "export const i = Image;\n", + ) + + result = extract( + [tmp_path / "package.json", bare, subpath], cache_root=tmp_path / "graphify-out" + ) + G = build_from_json(result, root=str(tmp_path)) + + def _imports_from(nid): + # The built graph may be undirected, which loses (u, v) edge-iteration + # order -- direction is preserved separately as _src/_tgt (build.py). + return { + d.get("_tgt") for u, v, d in G.edges(data=True) + if d.get("relation") == "imports_from" and d.get("_src") == nid + } + + bare_id = next( + n for n, d in G.nodes(data=True) + if str(d.get("source_file", "")).endswith("bare.ts") + ) + subpath_id = next( + n for n, d in G.nodes(data=True) + if str(d.get("source_file", "")).endswith("subpath.ts") + ) + bare_targets = _imports_from(bare_id) + subpath_targets = _imports_from(subpath_id) + assert bare_targets, "the bare import must resolve" + assert subpath_targets, "the subpath import must resolve, not dangle" + assert subpath_targets == bare_targets, ( + "a subpath import of a package must land on the same node as a bare " + "import of that package" + )