diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef6b7e4c..06cc4570c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,7 +105,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 ``: the ` +`; + const result = extractFromSource('src/components/Page.vue', code); + + expect(result.errors).toEqual([]); + expect(result.nodes.find((n) => n.name === 'ready')?.startLine).toBe(7); + expect(result.nodes.find((n) => n.name === 'go')?.startLine).toBe(8); + expect(calls(result, 'helper', 8)).toBe(true); + expect(calls(result, 'formatDate', 2)).toBe(true); + expect(renders(result, 'Header', 3)).toBe(true); + }); + + it('keeps the template it comes before', () => { + const code = ` +`; + const result = extractFromSource('src/components/First.vue', code); + + expect(result.errors).toEqual([]); + expect(result.nodes.find((n) => n.name === 'ready')?.startLine).toBe(7); + expect(calls(result, 'formatDate', 3)).toBe(true); + expect(renders(result, 'Header', 4)).toBe(true); + }); + + it("still reads a call in a self-closing +`; + const result = extractFromSource('src/components/Post.vue', code); + + expect(result.errors).toEqual([]); + expect(result.nodes.find((n) => n.name === 'ready')?.startLine).toBe(9); + expect(calls(result, 'toJsonLd', 3)).toBe(true); + expect(calls(result, 'formatDate', 4)).toBe(true); + expect(renders(result, 'Header', 5)).toBe(true); + }); + + // The template-call scan always took `` as a closing tag; with one + // pattern for every site, the script reader does too. + it('ends a block at a closing tag written with a space, ``', () => { + const code = ` + +`; + const result = extractFromSource('src/components/Spaced.vue', code); + + expect(result.errors).toEqual([]); + expect(result.nodes.find((n) => n.name === 'ready')?.startLine).toBe(5); + expect(calls(result, 'formatDate', 2)).toBe(true); + }); +}); diff --git a/src/extraction/astro-extractor.ts b/src/extraction/astro-extractor.ts index 8b75796ad..0ecba779a 100644 --- a/src/extraction/astro-extractor.ts +++ b/src/extraction/astro-extractor.ts @@ -2,7 +2,7 @@ import { Node, Edge, ExtractionResult, ExtractionError, UnresolvedReference } fr import { generateNodeId } from './tree-sitter-helpers'; import { TreeSitterExtractor } from './tree-sitter'; import { isLanguageSupported } from './grammars'; -import { foldScriptResult, sfcFileNode } from './sfc-script'; +import { foldScriptResult, sfcFileNode, SFC_SCRIPT_ELEMENT, sfcBlockLineRanges } from './sfc-script'; /** * Astro built-in components — compiler-provided (``) or shipped by @@ -161,7 +161,7 @@ export class AstroExtractor { // A self-closing ``. - const scriptRegex = /]*?)?(?:\/>|>(?[\s\S]*?)<\/script>)/g; + const scriptRegex = new RegExp(SFC_SCRIPT_ELEMENT); let match; while ((match = scriptRegex.exec(this.source)) !== null) { @@ -228,16 +228,9 @@ export class AstroExtractor { coveredRanges.push([frontmatter.startLine - 1, frontmatter.endLine]); } - 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]); - } + // A self-closing tag covers nothing: its attributes + // (`set:html={serializeJsonLd(schema)}`) are template expressions. + coveredRanges.push(...sfcBlockLineRanges(this.source)); return coveredRanges; } diff --git a/src/extraction/sfc-script.ts b/src/extraction/sfc-script.ts index cc96473b1..e6de92549 100644 --- a/src/extraction/sfc-script.ts +++ b/src/extraction/sfc-script.ts @@ -46,6 +46,51 @@ export function sfcScriptLanguage(attrs: string, host: Language): Language { return 'javascript'; } +/** + * A ``, and that script was parsed with the markup in between and + * contributed nothing (#2989). + * + * Shared by the three extractors and the viewer's syntax regions. It carries + * the `g` flag, so each loop runs on its own copy (`new RegExp(...)`). + */ +export const SFC_SCRIPT_ELEMENT = /]*?)?(?:\/>|>(?[\s\S]*?)<\/script\s*>)/g; + +/** + * A ``. - const scriptRegex = /]*?)?(?:\/>|>(?[\s\S]*?)<\/script>)/g; + const scriptRegex = new RegExp(SFC_SCRIPT_ELEMENT); let match; while ((match = scriptRegex.exec(this.source)) !== null) { @@ -204,20 +204,10 @@ export class SvelteExtractor { componentNodeId: string, _scriptBlocks: Array<{ content: string; startLine: number }> ): void { - // Build a set of line ranges covered by and ranges - 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]); - } + // Line ranges covered by `. -const SCRIPT_BLOCK = /]*?)?(?:\/>|>([\s\S]*?)<\/script>)/gi; +// The extractors' own script element, read case-insensitively here. A +// self-closing ``. +const SCRIPT_BLOCK = new RegExp(SFC_SCRIPT_ELEMENT.source, 'gi'); /** Astro's frontmatter: a `---` fence at the very top of the file. */ const ASTRO_FRONTMATTER = /^(---\r?\n)([\s\S]*?)\r?\n---/; @@ -390,7 +391,8 @@ export function syntaxRegionsFor(source: string, language: Language): SyntaxRegi while ((match = SCRIPT_BLOCK.exec(source)) !== null) { const body = match[2] ?? ''; if (body.trim() === '') continue; - const start = match.index + match[0].length - body.length - ''.length; + // The body starts right after the opening tag, whose attributes hold no `>`. + const start = match.index + match[0].indexOf('>') + 1; regions.push({ start, end: start + body.length, diff --git a/src/extraction/vue-extractor.ts b/src/extraction/vue-extractor.ts index 126c103ad..d17e7d2e1 100644 --- a/src/extraction/vue-extractor.ts +++ b/src/extraction/vue-extractor.ts @@ -2,7 +2,7 @@ import { Node, Edge, ExtractionResult, ExtractionError, UnresolvedReference, Lan import { generateNodeId } from './tree-sitter-helpers'; import { TreeSitterExtractor } from './tree-sitter'; import { isLanguageSupported } from './grammars'; -import { foldScriptResult, sfcFileNode, sfcScriptLanguage } from './sfc-script'; +import { foldScriptResult, sfcFileNode, sfcScriptLanguage, SFC_SCRIPT_ELEMENT, sfcBlockLineRanges } from './sfc-script'; import { vueOptionsMembers } from './vue-options-api'; import { vueTemplateCalls } from './vue-template-calls'; @@ -201,12 +201,15 @@ export class VueExtractor { language: Language; }> = []; - const scriptRegex = /]*)?>(?[\s\S]*?)<\/script>/g; + // A self-closing ``. + const scriptRegex = new RegExp(SFC_SCRIPT_ELEMENT); 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; // TypeScript, TSX or JavaScript, from the lang attribute const language = sfcScriptLanguage(attrs, 'vue'); @@ -288,15 +291,9 @@ export class VueExtractor { // Ranges covered by