fix(svelte): parse <script> blocks so static imports produce edges (#713)#714
Closed
jippi wants to merge 2 commits into
Closed
fix(svelte): parse <script> blocks so static imports produce edges (#713)#714jippi wants to merge 2 commits into
jippi wants to merge 2 commits into
Conversation
…raphify-Labs#713) extract_svelte() handed the entire raw .svelte file (template + style + script) to tree-sitter's JavaScript grammar via _extract_generic. The grammar choked on the surrounding markup before reaching script-block imports, so static `import X from './foo.svelte'` produced zero edges. Across a real 1,873-file SvelteKit codebase, only 4 of 1,348 `imports` edges targeted .svelte files — making queries like "where is this component used?" or dead-code detection unanswerable. Three additional gaps with the original approach: - <script lang="ts"> was parsed with _JS_CONFIG, silently degrading on TS-specific syntax (type imports, generics, `as const`). - <script context="module"> (Svelte 4) and <script module> (Svelte 5) were never parsed. - The Graphify-Labs#701 regex pass could only find dynamic imports, not static. Approach -------- Slice <script> blocks out and parse each with the right grammar (JS or TS, picked from the lang= attribute), then run the existing Graphify-Labs#701 regex pass over the FULL .svelte source so markup-level dynamic imports keep working unchanged. Implementation uses a whitespace-mask trick rather than a string-slice + line-offset refactor: non-script content is replaced with spaces while preserving newlines, so tree-sitter parses only the script body but node.start_point[0] line numbers remain aligned with the original .svelte file. _extract_generic gains an optional source_bytes= kwarg; all other extractors are untouched. Coverage matrix --------------- <script> → JS grammar <script lang="ts"> → TS grammar <script context="module"> → JS grammar (Svelte 4) <script context="module" lang="ts"> → TS grammar <script module> → JS grammar (Svelte 5) <script module lang="ts"> → TS grammar Instance + module blocks together → both parsed independently Mixed langs (instance JS + module TS) → each block uses its own config .svelte.ts (Svelte 5 rune-only file) → unchanged; dispatched to extract_js + _TS_CONFIG via path.suffix == ".ts" already Graphify-Labs#701 regression guards ---------------------- - {#await import('./X.svelte')} markup imports still extracted - await import('./X.svelte') in <script> body still extracted - source_location stays accurate (function on line 7 reports L7) 19 new tests in tests/test_svelte.py; full suite still passes (7 pre-existing failures on v7 unrelated to this change: sql parser, incremental manifest, macOS case-insensitive .F90/.f90 collision).
Adds 6 tests targeting the load-bearing invariant of the whitespace-mask
strategy: kept <script> content must occupy the same byte positions
(and therefore the same line numbers) in the masked source as in the
original .svelte file.
- test_line_numbers_for_imports_stay_aligned: import edges'
source_location reports the .svelte line, not script-relative
- test_line_numbers_with_deeply_offset_script: 30-line markup prefix,
function on L32 reports L32
- test_line_numbers_across_two_script_blocks: module-block fn and
instance-block fn each report their own .svelte line
- test_byte_offset_alignment_via_tree_sitter: end-to-end check via
extract_svelte output
- test_mask_byte_count_equals_original: mask is character-substitution
only — never inserts or deletes bytes; newline byte positions match
- test_mask_preserves_byte_offsets_of_kept_content: kept script body
bytes appear at identical offsets in both original and masked source
These guard against any future masking change that would silently shift
content (e.g. switching from per-character mask to a tag-stripping
approach) and break source_location accuracy across the board.
This was referenced May 4, 2026
Contributor
Author
|
@safishamsi bump on this PR since it implements the bug reported including tests - I'm still not clear if you want PRs or prefer fixing thing on your own based on the changes here |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #713. Builds on #700 + #701; aware of #712 (separate PR forthcoming).
Code written by Claude Code, but reviewed by me (I'm not super familiar with Python, but SWE with +20 years experience)
Problem
extract_svelte()handed the entire raw.sveltefile (template + style + script) to tree-sitter's JavaScript grammar via_extract_generic. The grammar choked on the surrounding markup before reaching script-block imports, so staticimport X from './foo.svelte'produced zero edges.On a real 1,873-file SvelteKit codebase: only 4 of 1,348
importsedges targeted.sveltefiles. Queries like "where is this component used?" or dead-component detection were unanswerable from the graph.Three additional gaps with the original approach:
<script lang=\"ts\">was parsed with_JS_CONFIG, silently degrading on TS-specific syntax (type imports, generics,as const).<script context=\"module\">(Svelte 4) and<script module>(Svelte 5) were never parsed.Approach
Slice
<script>blocks out and parse each with the right grammar (JS or TS, picked from thelang=attribute), then run the existing #701 regex pass over the full.sveltesource so markup-level dynamic imports keep working unchanged.Implementation uses a whitespace-mask trick rather than a string-slice + line-offset refactor: non-script content is replaced with spaces while preserving newlines, so tree-sitter parses only the script body but
node.start_point[0]line numbers remain aligned with the original.sveltefile._extract_genericgains an optionalsource_bytes=kwarg; all other extractors are untouched.Coverage matrix
<script><script lang=\"ts\"><script context=\"module\">(Svelte 4)<script context=\"module\" lang=\"ts\"><script module>(Svelte 5)<script module lang=\"ts\">.svelte.ts(Svelte 5 rune-only file)extract_js+_TS_CONFIG)#701 regression guards
The regex pass for
{#await import('./X.svelte')}runs unchanged over the full source. Verified by:test_markup_dynamic_import_still_extractedtest_script_block_dynamic_import_still_extractedTests (25 new, all passing)
tests/test_svelte.pycovers every cell in the coverage matrix plus the load-bearing invariants:import X from './foo.svelte'— script-block content not sliced before tree-sitter JS #713 reproducer: 9.svelteimports all produce edgesimport type, generics (<T,>) don't block subsequent imports{#await import(...)}and script-bodyawait import(...)_make_id(str(path))(extract_svelte() regex fallback: alias paths skipped + synthetic IDs don't match real file nodes, so 100% of generated edges are dropped by build #701 invariant).svelteline; newline byte positions match exactly between original and masked sourceValidation on real codebase
Re-ran
graphify update .on a 1,873-file SvelteKit app:importsedges →.svelteimports_fromedges →.sveltedynamic_importedges (#701 check)Spot-checked all 6 remaining candidates: 5 are genuinely dead components, 1 is a barrel-reexport limitation (
export { default as X } from './x.svelte'inindex.ts) that's orthogonal to this fix and worth a follow-up issue.Test plan
pytest tests/test_svelte.py— 25/25 passpytest tests/— 568 pass, 7 pre-existing failures unrelated to this change (SQL parser, incremental manifest, macOS case-insensitive.F90/.f90collision).svelte → .svelteimport edges now appear; previously-broken "where is X used?" queries return correct results