diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d2e784b..3ac94fef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In VB.NET, reading or setting a `Shared` field or property through its class or module name, like `AppSession.SessionId`, `AppSession.CurrentUser = "demo"` or `AppSession.Items(0)`, now counts as a use of that member and of the class, and so do reading an `Enum` value like `Mode.Fast` and calling a `Shared` function without parentheses. Before, only calls written with parentheses were linked, so `codegraph callers` on such a field, property or class came back empty and impact missed most of the code that depends on it. A local, parameter or field that only shares a class's name is not mistaken for the class. Thanks @serkanince for the report and @ChrisPrapas. (#2305) - In Vue and Nuxt components, calls written in the template (`{{ formatDate(post) }}`, `:to="getAccountRoute(account)"`, `@click="save(item)"`) and calls in a destructuring declaration at the top of ` +`; + const result = extractFromSource('Page.astro', code); + + expect(result.errors).toEqual([]); + const ready = result.nodes.find((n) => n.name === 'ready'); + expect(ready).toBeDefined(); + expect(ready?.startLine).toBe(8); + // The markup between the two scripts is still scanned. + const call = result.unresolvedReferences.find( + (ref) => ref.referenceKind === 'calls' && ref.referenceName === 'formatDate' && ref.line === 6 + ); + expect(call).toBeDefined(); + }); + + it('should not let a self-closing hide the markup after it (#2989)', () => { + const code = ` +
{formatDate(date)}
+ +`; + 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 's attributes (#2989)", () => { + // A self-closing tag has no body for the template scan to skip: + // `set:html={…}` runs when the page renders, like any attribute expression. + const code = `--- +import { serializeJsonLd } from '../lib/jsonld'; +const schema = {}; +--- + + +`; + 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 `` in a Svelte component, such as a + * third-party script in `{formatDate(date)}
+hi
\n\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 = /`. + const scriptRegex = /`. + const scriptRegex = / 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 = /