Skip to content

fix(svelte): parse <script> blocks so static imports produce edges (#713)#714

Closed
jippi wants to merge 2 commits into
Graphify-Labs:v7from
jippi:fix/svelte-script-block-extraction
Closed

fix(svelte): parse <script> blocks so static imports produce edges (#713)#714
jippi wants to merge 2 commits into
Graphify-Labs:v7from
jippi:fix/svelte-script-block-extraction

Conversation

@jippi

@jippi jippi commented May 4, 2026

Copy link
Copy Markdown
Contributor

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 .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.

On a real 1,873-file SvelteKit codebase: only 4 of 1,348 imports edges targeted .svelte files. Queries like "where is this component used?" or dead-component detection were unanswerable from the graph.

Three additional gaps with the original approach:

Approach

Slice <script> blocks out and parse each with the right grammar (JS or TS, picked from the lang= attribute), then run the existing #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 block flavor Behavior before After
<script> partial / fails parsed with JS grammar
<script lang=\"ts\"> silently degrades parsed with TS grammar
<script context=\"module\"> (Svelte 4) not seen parsed with JS grammar
<script context=\"module\" lang=\"ts\"> not seen parsed with TS grammar
<script module> (Svelte 5) not seen parsed with JS grammar
<script module lang=\"ts\"> not seen parsed with TS grammar
Instance + module together (any langs) inst. degrades; module not seen both parsed, each with own config
.svelte.ts (Svelte 5 rune-only file) works (extract_js + _TS_CONFIG) unchanged — out of scope

#701 regression guards

The regex pass for {#await import('./X.svelte')} runs unchanged over the full source. Verified by:

  • test_markup_dynamic_import_still_extracted
  • test_script_block_dynamic_import_still_extracted

Tests (25 new, all passing)

tests/test_svelte.py covers every cell in the coverage matrix plus the load-bearing invariants:

Validation on real codebase

Re-ran graphify update . on a 1,873-file SvelteKit app:

Metric Before After Δ
Total edges 12,096 15,603 +29%
imports edges → .svelte 4 170 +42×
imports_from edges → .svelte 351 1,538 +4.4×
dynamic_import edges (#701 check) 97 97 unchanged
Unreferenced-component candidates 214 6 −97%

Spot-checked all 6 remaining candidates: 5 are genuinely dead components, 1 is a barrel-reexport limitation (export { default as X } from './x.svelte' in index.ts) that's orthogonal to this fix and worth a follow-up issue.

Test plan

  • pytest tests/test_svelte.py — 25/25 pass
  • pytest tests/ — 568 pass, 7 pre-existing failures unrelated to this change (SQL parser, incremental manifest, macOS case-insensitive .F90/.f90 collision)
  • Smoke test on a real 1,873-file SvelteKit project — .svelte → .svelte import edges now appear; previously-broken "where is X used?" queries return correct results

jippi added 2 commits May 4, 2026 21:47
…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.
@jippi

jippi commented May 7, 2026

Copy link
Copy Markdown
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

extract_svelte() misses static import X from './foo.svelte' — script-block content not sliced before tree-sitter JS

1 participant