From 617769279de0247a04d3a5917ce0d99c6a5cd801 Mon Sep 17 00:00:00 2001 From: Max Hsu Date: Fri, 30 Oct 2026 05:16:12 -0500 Subject: [PATCH] fix(astro,svelte): a self-closing `. 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 ` +`; + const result = extractFromSource('Styled.astro', code); + + const call = result.unresolvedReferences.find( + (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'formatDate' && ref.line === 2 + ); + expect(call).toBeDefined(); + }); + + it("still reads a call in a self-closing +`; + const result = extractFromSource('Schema.astro', code); + + expect(result.errors).toEqual([]); + expect(result.nodes.find((n) => n.name === 'ready')?.startLine).toBe(7); + const call = result.unresolvedReferences.find( + (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'serializeJsonLd' && ref.line === 5 + ); + expect(call).toBeDefined(); + }); }); describe('Instantiates + Decorates edge extraction', () => { diff --git a/__tests__/svelte-self-closing-script.test.ts b/__tests__/svelte-self-closing-script.test.ts new file mode 100644 index 000000000..e5c26a652 --- /dev/null +++ b/__tests__/svelte-self-closing-script.test.ts @@ -0,0 +1,60 @@ +/** + * A self-closing ``: 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 +`; + 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 +`; + 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); + }); +}); diff --git a/__tests__/ui-highlight.test.ts b/__tests__/ui-highlight.test.ts index df37a1a2c..abf4f31a5 100644 --- a/__tests__/ui-highlight.test.ts +++ b/__tests__/ui-highlight.test.ts @@ -95,6 +95,13 @@ describe('which languages classify', () => { expect(regions?.[0]?.language).toBe('typescript'); }); + it('skips a self-closing \n'; + const regions = syntaxRegionsFor(source, 'astro'); + expect(regions).toHaveLength(1); + expect(source.slice(regions![0]!.start, regions![0]!.end)).toBe('\nconst ready = true;\n'); + }); + it('has no grammar for the formats that only have file-level extraction', () => { for (const language of ['yaml', 'xml', 'properties', 'twig', 'unknown']) { expect(grammarFor(language)).toBeNull(); diff --git a/src/extraction/astro-extractor.ts b/src/extraction/astro-extractor.ts index efdc33f79..8b75796ad 100644 --- a/src/extraction/astro-extractor.ts +++ b/src/extraction/astro-extractor.ts @@ -159,11 +159,14 @@ export class AstroExtractor { private extractScriptBlocks(): Array<{ content: string; startLine: number }> { const blocks: Array<{ content: string; startLine: number }> = []; - const scriptRegex = /]*)?>(?[\s\S]*?)<\/script>/g; + // A self-closing ``. + const scriptRegex = /]*?)?(?:\/>|>(?[\s\S]*?)<\/script>)/g; let match; while ((match = scriptRegex.exec(this.source)) !== null) { - const content = match.groups?.content || match[2] || ''; + if (match.groups?.content === undefined) continue; + const content = match.groups.content; // Calculate the 0-indexed line where the content begins. The content // starts right after the opening tag's `>` — its leading `\n` is part @@ -225,9 +228,12 @@ export class AstroExtractor { coveredRanges.push([frontmatter.startLine - 1, frontmatter.endLine]); } - const tagRegex = /<(script|style)(\s[^>]*)?>[\s\S]*?<\/\1>/g; + const tagRegex = /<(script|style)(\s[^>]*?)?(?:\/>|>[\s\S]*?<\/\1>)/g; let tagMatch; while ((tagMatch = tagRegex.exec(this.source)) !== null) { + // A self-closing tag has no body to skip, and its attributes + // (`set:html={serializeJsonLd(schema)}`) are template expressions. + if (tagMatch[0].endsWith('/>')) continue; const startLine = (this.source.substring(0, tagMatch.index).match(/\n/g) || []).length; const endLine = startLine + (tagMatch[0].match(/\n/g) || []).length; coveredRanges.push([startLine, endLine]); diff --git a/src/extraction/svelte-extractor.ts b/src/extraction/svelte-extractor.ts index 5b954e6ce..9dd6c5921 100644 --- a/src/extraction/svelte-extractor.ts +++ b/src/extraction/svelte-extractor.ts @@ -125,12 +125,16 @@ export class SvelteExtractor { isTypeScript: boolean; }> = []; - const scriptRegex = /]*)?>(?[\s\S]*?)<\/script>/g; + // A self-closing ``. + const scriptRegex = /]*?)?(?:\/>|>(?[\s\S]*?)<\/script>)/g; let match; while ((match = scriptRegex.exec(this.source)) !== null) { + if (match.groups?.content === undefined) continue; const attrs = match[1] || ''; - const content = match.groups?.content || match[2] || ''; + const content = match.groups.content; // Detect TypeScript from lang attribute const isTypeScript = sfcScriptLanguage(attrs, 'svelte') === 'typescript'; @@ -204,9 +208,12 @@ export class SvelteExtractor { const coveredRanges: Array<[number, number]> = []; // Find all and ranges - const tagRegex = /<(script|style)(\s[^>]*)?>[\s\S]*?<\/\1>/g; + const tagRegex = /<(script|style)(\s[^>]*?)?(?:\/>|>[\s\S]*?<\/\1>)/g; let tagMatch; while ((tagMatch = tagRegex.exec(this.source)) !== null) { + // A self-closing tag has no body to skip, and its attributes + // (`src={assetUrl('/analytics.js')}`) are template expressions. + if (tagMatch[0].endsWith('/>')) continue; const startLine = (this.source.substring(0, tagMatch.index).match(/\n/g) || []).length; const endLine = startLine + (tagMatch[0].match(/\n/g) || []).length; coveredRanges.push([startLine, endLine]); @@ -260,9 +267,11 @@ export class SvelteExtractor { private extractTemplateComponents(componentNodeId: string): void { // Build ranges covered by `. +const SCRIPT_BLOCK = /]*?)?(?:\/>|>([\s\S]*?)<\/script>)/gi; /** Astro's frontmatter: a `---` fence at the very top of the file. */ const ASTRO_FRONTMATTER = /^(---\r?\n)([\s\S]*?)\r?\n---/;