Files
codegraph/__tests__/svelte-self-closing-script.test.ts
T
Max HsuandClaude Opus 5.5 617769279d fix(astro,svelte): a self-closing <script /> no longer swallows the next script
A self-closing `<script ... />`, such as Astro's
`<script is:inline set:html={code} />` or a third-party
`<script src="/analytics.js" />` in Svelte's `<svelte:head>`, matched the
extractors' script regex as an opening tag, so the match ran on to the next
`</script>`. The real script that followed was parsed with the markup in
between, failed with "parse produced no symbols", and contributed nothing.
The covered-range regexes had the same shape, so the template scans also
skipped the markup between the two tags, and a self-closing `<style />` hid
the markup after it.

The opening-tag pattern now matches a `/>` ending on its own as an empty
tag: in the Astro extractor's script blocks and covered ranges, the Svelte
extractor's script blocks and both of its covered-range scans, and the
viewer's highlight regions.

With a fix of ours: a self-closing tag has no body, so the covered-range
scans skip it, and a call in its attributes, like
`set:html={serializeJsonLd(schema)}`, is read as on main. Matched as a
range, such a tag hid those calls even where no script followed it.

Validation on main bc9291af (Windows 11):
- Red/green, 6 tests: 4 from the PRs, plus 2 of ours for an attribute call
  on a self-closing script in Astro and in Svelte. With main's
  astro-extractor.ts, svelte-extractor.ts and syntax-tokens.ts all 6 fail;
  with the PRs' code as written the 2 of ours fail; with this commit all 6
  pass.
- Svelte 5.56.10's own compiler reads the Svelte fixture as an empty head
  script plus an instance script, as the extractor now does.
- A/B against main on 7 Astro repos, with flask and express as controls.
  starlight, diploi.com and both controls are byte-identical, and
  devfest-website keeps every edge. Gains: the genkit docsite 2 import
  links, nodejsdesignpatterns.com 2 nodes and 2 import links, a portfolio
  25 component references, and wm-project-astro-components 3
  `serializeJsonLd` calls from JSON-LD scripts plus 1 call in a script main
  swallowed. That repo also drops 31 false calls, CSS like
  `border-radius: var(...)` that main read as calls after parsing a
  swallowed `<style>` block as TypeScript. No other edge changes.
- Index time unchanged: median of 3 interleaved runs on
  wm-project-astro-components, 4.67 s on main and 4.52 s with the fix.

Vue's extractor has the same pattern and the same bug; a follow-up shares
one pattern across the three extractors.

Original PRs #2995 and #2997 by @maxmilian.
Refs #2989

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:24:40 +00:00

61 lines
2.2 KiB
TypeScript

/**
* A self-closing `<script ... />` in a Svelte component, such as a
* third-party script in `<svelte:head>`, has no body. It used to be read as
* an opening tag running on to the next `</script>`: the component's real
* script was parsed with the markup in between, failed, and contributed no
* symbols, and the template scan skipped that markup. The same fix as #2989
* for Astro.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
beforeAll(async () => {
await initGrammars();
await loadAllGrammars();
});
describe('Svelte self-closing <script />', () => {
it('does not swallow the component script that follows it', () => {
const code = `<svelte:head>
<script src="/analytics.js" />
</svelte:head>
<p>{formatDate(date)}</p>
<Header />
<script>
const ready = true;
</script>
`;
const result = extractFromSource('src/routes/+page.svelte', code);
expect(result.errors).toEqual([]);
const ready = result.nodes.find((n) => n.name === 'ready');
expect(ready).toBeDefined();
expect(ready?.startLine).toBe(7);
// The markup between the two tags is still scanned.
const call = result.unresolvedReferences.find(
(ref) => ref.referenceKind === 'calls' && ref.referenceName === 'formatDate' && ref.line === 4
);
expect(call).toBeDefined();
expect(result.unresolvedReferences.some((ref) => ref.referenceName === 'Header' && ref.line === 5)).toBe(true);
});
it("still reads a call in a self-closing <script />'s attributes", () => {
// A self-closing tag has no body for the template scan to skip.
const code = `<svelte:head>
<script src={assetUrl('/analytics.js')} />
</svelte:head>
<script>
const ready = true;
</script>
`;
const result = extractFromSource('src/routes/+layout.svelte', code);
expect(result.errors).toEqual([]);
expect(result.nodes.find((n) => n.name === 'ready')?.startLine).toBe(5);
expect(result.unresolvedReferences.some(
(ref) => ref.referenceKind === 'calls' && ref.referenceName === 'assetUrl' && ref.line === 2
)).toBe(true);
});
});