Repository navigation
fix(extract): resolve relative JS/TS imports with a ?query or #fragment (#4860) - #4876
Jitendarmahara wants to merge 2 commits into
Conversation
Relative ES module specifiers that carry a cache-busting query string or fragment (`./foo.js?v=123`, `./foo.js#frag`) were resolved verbatim via `start_dir / raw`, so the target file never matched. graphify emitted a phantom `ref_..._v_123` node instead of an `imports_from` edge, and the dependent per-symbol `imports` and cross-file `calls` edges were lost too. Strip the `?query` / `#fragment` from the relative specifier in `_resolve_js_module_path` before resolving, mirroring the existing Markdown extractor normalization (markdown.py). Browsers and bundlers resolve the module as if the suffix were absent. Only the relative branch is affected; bare and alias specifiers are untouched. Adds regression tests that fail before the change: unit coverage for the resolver (query, fragment, both) and an end-to-end extract_js check that the real file node receives the edge with no phantom, plus a guard that external modules still resolve externally. Fixes Graphify-Labs#4860
|
Thanks for the pull request, @Jitendarmahara. A maintainer will review it soon. Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions. A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic. |
There was a problem hiding this comment.
Graphify reviewed this change.
No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.
Not checked: tests were not run; formal verification proved 0 of 1 changed function(s) (1 sampled, not proven).
Formal verification. PR-changed functions: 1/1 verified (0 proven, 1 may-equivalent, 0 distinguished) · 0 not verified.
Graphify review — findings
Fixes JS import resolution for relative specifiers carrying a ?query or #fragment suffix (e.g. ./lib/foo.js?v=134): _resolve_js_module_path now strips the suffix before resolving, so the import maps to the real file node instead of a phantom ref_..._v_134 target. The strip only runs in the relative-specifier branch, so tsconfig-alias imports with a suffix still take the old path, while bare external modules like lodash-es fall through to the external reference path as before. New tests cover query, fragment, and combined suffixes, plus the end-to-end imports_from and per-symbol imports edges for the reproducer.
No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3320 functions depend on the 255 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract_js()— 97 callers, 7 callees - worse:
_resolve_js_module_path()— 45 callers, 9 callees
Verification — 3320 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1397 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
34 of 469 test file(s) selected (7%) via static blast radius.
tests/test_astro_extraction.py— impacttests/test_build.py— impacttests/test_cjs_module_extension.py— impacttests/test_ejs_extractor.py— impacttests/test_ext_tagged_id_clash.py— impacttests/test_extract.py— impacttests/test_forwarding_review_findings.py— impacttests/test_import_extension_resolution.py— impact, changed-testtests/test_indirect_call_block_scoped_shadow.py— impacttests/test_js_exported_scalar_bindings.py— impacttests/test_js_member_collisions.py— impacttests/test_js_walk_order.py— impacttests/test_language_override_imports.py— impacttests/test_languages.py— impacttests/test_loose_sibling_import_resolution.py— impacttests/test_multilang.py— impacttests/test_package_json_subpath_imports.py— impacttests/test_pascal.py— impacttests/test_pascal_resolution.py— impacttests/test_phantom_external_import.py— impacttests/test_pr10_resolution_overrides.py— impacttests/test_python_import_resolution.py— impacttests/test_python_member_call_language_gate.py— impacttests/test_python_parse_memoization.py— impacttests/test_rationale.py— impacttests/test_resolve_memoization.py— impacttests/test_source_key_memoization.py— impacttests/test_src_layout_import_resolution.py— impacttests/test_svelte_extraction.py— impacttests/test_ts_new_expression_calls.py— impacttests/test_typescript_module_extensions.py— impacttests/test_unmapped_at_alias_resolution.py— impacttests/test_vue_extraction.py— impacttests/test_walk_python_tree_iterative.py— impact
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
No difference found (not proven): No behavior difference found in \_resolve\_js\_module\_path (not a proof).
The verifier ran both versions of \_resolve\_js\_module\_path on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
· 2 more finding(s) on lines outside this diff (see the check run).
|
Landed in v0.9.98, now on PyPI (pip install graphifyy==0.9.98). A relative JS/TS import with a ?query or #fragment suffix resolves to the real module instead of a phantom node. Thanks for the fix, @Jitendarmahara. |
|
Thanks @safishamsi, glad it landed I enjoyed tracing the resolver on this
one. I noticed the same suffix-stripping likely helps the ../styles.css?url
case in #2849 — happy to look at that next, or at any area where you'd like
contributor help. I'll keep picking up issues.
…On Sat, Oct 31, 2026 at 4:30 AM Safi ***@***.***> wrote:
*safishamsi* left a comment (Graphify-Labs/graphify#4876)
<#4876 (comment)>
Landed in v0.9.98, now on PyPI (pip install graphifyy==0.9.98). A relative
JS/TS import with a ?query or #fragment suffix resolves to the real module
instead of a phantom node. Thanks for the fix, @Jitendarmahara
<https://github.com/Jitendarmahara>.
—
Reply to this email directly, view it on GitHub
<#4876?email_source=notifications&email_token=ARZCVA2PSQ2JFOYIJ6XNTOD5WUMRLA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTMNJTGUZTAMJWGE42M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-6535301619>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ARZCVA5LCDHY6JRKZO6WJ535WUMRLAVCNFSNUABGKJSXA33TNF2G64TZHMYTEMBQGU4TOMRWGM5US43TOVSTWNRRGIYTGOBWGQ2TTILWAI>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Problem
Relative ES module imports that carry a cache-busting query string or fragment
are not resolved:
Instead of an
imports_fromedge tolib/foo.js, graphify creates a phantomnode (
ref_lib_foo_js_v_134) and the real file gets no inbound edge. Thedependent per-symbol
importsedges and cross-filecallsare lost too, sincethey depend on the module resolving first.
Root cause
_resolve_js_module_path(graphify/extractors/resolution.py) resolves arelative specifier with
start_dir / rawwithout stripping the?query/#fragmentpart, so the path never matches a real file. The Markdown extractoralready strips these (
graphify/extractors/markdown.py).Fix
Strip the
?query/#fragmentfrom the relative specifier before resolving,mirroring the Markdown extractor. Only the relative (
.-prefixed) branch istouched; bare and alias specifiers are unchanged.
Verification
Commands run locally (Python 3,
uv):uv run pytest tests/test_import_extension_resolution.py-> 45 passed(6 new regression tests; the 4 core ones fail without the fix, confirming the
phantom
ref_..._v_134node, and pass with it).uv run ruff check .-> clean.uv run pyright-> no new errors introduced by this change (project baselineunchanged).
uv run pytest tests-> 8653 passed. The 7 failures intest_ext_tagged_id_clash.pyare pre-existing onv8HEAD (identical withthis change reverted) and unrelated to this fix.
Scope / limitations
../styles.css?urlcase noted in JS/TS graph build resolves internal imports to missing nodes and overwrites parallel directed edges #2849.Fixes #4860