From 0059effe31c4045795d1042994ec84790e443567 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:26:06 -0700 Subject: [PATCH 01/29] release 0.1.8: every package and plugin manifest at 0.1.8 (the parser keeps its own version) Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- gemini-extension.json | 2 +- package.json | 12 ++++++------ plugins/axiomcode/.claude-plugin/plugin.json | 2 +- plugins/axiomcode/.codex-plugin/plugin.json | 2 +- plugins/axiomcode/.cursor-plugin/plugin.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gemini-extension.json b/gemini-extension.json index 6327e15c3..9b59561d6 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.1.7", + "version": "0.1.8", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", "contextFileName": "plugins/axiomcode/AGENTS.md", "mcpServers": { diff --git a/package.json b/package.json index 449d86de7..0b64aeea1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@axiomcode/code-graph", - "version": "0.1.7", + "version": "0.1.8", "description": "AxiomCode Graph: a resolved call graph of your codebase grounded in formal methods, so you and your coding agents can see who calls what, what a change breaks, and which tests it reaches.", "repository": { "type": "git", @@ -30,11 +30,11 @@ ], "license": "FSL-1.1-Apache-2.0", "optionalDependencies": { - "@axiomcode/engine-darwin-arm64": "0.1.7", - "@axiomcode/engine-darwin-x64": "0.1.7", - "@axiomcode/engine-linux-x64": "0.1.7", - "@axiomcode/engine-linux-arm64": "0.1.7", - "@axiomcode/engine-win32-x64": "0.1.7" + "@axiomcode/engine-darwin-arm64": "0.1.8", + "@axiomcode/engine-darwin-x64": "0.1.8", + "@axiomcode/engine-linux-x64": "0.1.8", + "@axiomcode/engine-linux-arm64": "0.1.8", + "@axiomcode/engine-win32-x64": "0.1.8" }, "bin": { "axiomcode": "bin/axiomcode.js" diff --git a/plugins/axiomcode/.claude-plugin/plugin.json b/plugins/axiomcode/.claude-plugin/plugin.json index 02e47b328..cd9da1b86 100644 --- a/plugins/axiomcode/.claude-plugin/plugin.json +++ b/plugins/axiomcode/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "axiomcode", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", - "version": "0.1.7", + "version": "0.1.8", "author": { "name": "AxiomCode" } diff --git a/plugins/axiomcode/.codex-plugin/plugin.json b/plugins/axiomcode/.codex-plugin/plugin.json index c17386769..27eb62efc 100644 --- a/plugins/axiomcode/.codex-plugin/plugin.json +++ b/plugins/axiomcode/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.1.7", + "version": "0.1.8", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", "author": { "name": "AxiomCode", "url": "https://github.com/AxiomCodeAI/axiomcodegraph" }, "homepage": "https://github.com/AxiomCodeAI/axiomcodegraph", diff --git a/plugins/axiomcode/.cursor-plugin/plugin.json b/plugins/axiomcode/.cursor-plugin/plugin.json index db0fce09f..60d16349b 100644 --- a/plugins/axiomcode/.cursor-plugin/plugin.json +++ b/plugins/axiomcode/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.1.7", + "version": "0.1.8", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed. Java, TypeScript, Python, JavaScript.", "author": { "name": "AxiomCode" }, "homepage": "https://github.com/AxiomCodeAI/axiomcodegraph", From ff8a324985d83180e7b423d0b9355545a8e451a2 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:24:33 -0700 Subject: [PATCH 02/29] typescript, javascript: index Vue single-file components A .vue file was never read, so a helper called only from a template had no caller and a component rendered only by a template had no use. Each .vue component is now read as a virtual script that keeps every line: its inline + + diff --git a/graph/test/javascript/cases/69-vue-sfc/src/TemplateOnly.vue b/graph/test/javascript/cases/69-vue-sfc/src/TemplateOnly.vue new file mode 100644 index 000000000..25d1e389b --- /dev/null +++ b/graph/test/javascript/cases/69-vue-sfc/src/TemplateOnly.vue @@ -0,0 +1,3 @@ + diff --git a/graph/test/javascript/cases/69-vue-sfc/src/Typed.vue b/graph/test/javascript/cases/69-vue-sfc/src/Typed.vue new file mode 100644 index 000000000..ad39656cd --- /dev/null +++ b/graph/test/javascript/cases/69-vue-sfc/src/Typed.vue @@ -0,0 +1,4 @@ + + diff --git a/graph/test/javascript/cases/69-vue-sfc/src/child.js b/graph/test/javascript/cases/69-vue-sfc/src/child.js new file mode 100644 index 000000000..83c1bf8e6 --- /dev/null +++ b/graph/test/javascript/cases/69-vue-sfc/src/child.js @@ -0,0 +1,7 @@ +export function Child(props) { + return props.msg; +} + +export function MyBadge() { + return 'badge'; +} diff --git a/graph/test/javascript/cases/69-vue-sfc/src/main.js b/graph/test/javascript/cases/69-vue-sfc/src/main.js new file mode 100644 index 000000000..6d2103e88 --- /dev/null +++ b/graph/test/javascript/cases/69-vue-sfc/src/main.js @@ -0,0 +1,5 @@ +import Comp from './Comp.vue'; + +export function mount() { + return Comp; +} diff --git a/graph/test/javascript/expected/69-vue-sfc.diag b/graph/test/javascript/expected/69-vue-sfc.diag new file mode 100644 index 000000000..3ef2afad0 --- /dev/null +++ b/graph/test/javascript/expected/69-vue-sfc.diag @@ -0,0 +1 @@ +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - diff --git a/graph/test/javascript/expected/69-vue-sfc.edges b/graph/test/javascript/expected/69-vue-sfc.edges new file mode 100644 index 000000000..d7efd225a --- /dev/null +++ b/graph/test/javascript/expected/69-vue-sfc.edges @@ -0,0 +1,5 @@ +Comp.vue:10:34 FUNCTION_CALL sfcHelper -> known_edge Comp.vue:3:1 sfcHelper +Comp.vue:10:6 FUNCTION_CALL Child -> known_edge child.js:1:1 Child +Comp.vue:11:6 FUNCTION_CALL MyBadge -> known_edge child.js:5:1 MyBadge +Comp.vue:12:21 FUNCTION_CALL onSave -> known_edge Comp.vue:4:1 onSave +Comp.vue:12:40 FUNCTION_CALL sfcHelper -> known_edge Comp.vue:3:1 sfcHelper diff --git a/graph/test/javascript/expected/69-vue-sfc.oracle b/graph/test/javascript/expected/69-vue-sfc.oracle new file mode 100644 index 000000000..8675b52d3 --- /dev/null +++ b/graph/test/javascript/expected/69-vue-sfc.oracle @@ -0,0 +1 @@ +# defects: 0 diff --git a/graph/test/typescript/cases/80-vue-sfc/src/Comp.vue b/graph/test/typescript/cases/80-vue-sfc/src/Comp.vue new file mode 100644 index 000000000..b4f89c5b6 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/Comp.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/graph/test/typescript/cases/80-vue-sfc/src/Plain.vue b/graph/test/typescript/cases/80-vue-sfc/src/Plain.vue new file mode 100644 index 000000000..a8b318cce --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/Plain.vue @@ -0,0 +1,4 @@ + + diff --git a/graph/test/typescript/cases/80-vue-sfc/src/TemplateFirst.vue b/graph/test/typescript/cases/80-vue-sfc/src/TemplateFirst.vue new file mode 100644 index 000000000..d4ab960c2 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/TemplateFirst.vue @@ -0,0 +1,14 @@ + + + diff --git a/graph/test/typescript/cases/80-vue-sfc/src/TemplateOnly.vue b/graph/test/typescript/cases/80-vue-sfc/src/TemplateOnly.vue new file mode 100644 index 000000000..25d1e389b --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/TemplateOnly.vue @@ -0,0 +1,3 @@ + diff --git a/graph/test/typescript/cases/80-vue-sfc/src/child.ts b/graph/test/typescript/cases/80-vue-sfc/src/child.ts new file mode 100644 index 000000000..157bb63e7 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/child.ts @@ -0,0 +1,11 @@ +export function Child(props: { msg: string }): string { + return props.msg; +} + +export function MyBadge(): string { + return 'badge'; +} + +export function format(n: number): string { + return n.toFixed(2); +} diff --git a/graph/test/typescript/cases/80-vue-sfc/src/main.ts b/graph/test/typescript/cases/80-vue-sfc/src/main.ts new file mode 100644 index 000000000..c804793d3 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/main.ts @@ -0,0 +1,5 @@ +import Comp from './Comp.vue'; + +export function mount(): unknown { + return Comp; +} diff --git a/graph/test/typescript/cases/80-vue-sfc/src/shims-vue.d.ts b/graph/test/typescript/cases/80-vue-sfc/src/shims-vue.d.ts new file mode 100644 index 000000000..821cca508 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/shims-vue.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + const component: unknown; + export default component; +} diff --git a/graph/test/typescript/cases/80-vue-sfc/src/tsconfig.json b/graph/test/typescript/cases/80-vue-sfc/src/tsconfig.json new file mode 100644 index 000000000..98ff2e003 --- /dev/null +++ b/graph/test/typescript/cases/80-vue-sfc/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true + }, + "include": ["**/*.ts", "**/*.vue"] +} diff --git a/graph/test/typescript/expected/80-vue-sfc.edges b/graph/test/typescript/expected/80-vue-sfc.edges new file mode 100644 index 000000000..716281dd0 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.edges @@ -0,0 +1,8 @@ +ambiguous_unknown METHOD_CALL child#format(number) @L10 -> - +known_edge FUNCTION_CALL Comp.vue#() @L10 -> Comp.vue#sfcHelper() +known_edge FUNCTION_CALL Comp.vue#() @L10 -> child#Child({ msg: string }) +known_edge FUNCTION_CALL Comp.vue#() @L11 -> Comp.vue#onSave(number) +known_edge FUNCTION_CALL Comp.vue#() @L11 -> child#format(number) +known_edge FUNCTION_CALL TemplateFirst.vue#() @L3 -> TemplateFirst.vue#shown() +known_edge FUNCTION_CALL TemplateFirst.vue#() @L3 -> child#MyBadge() +known_edge FUNCTION_CALL TemplateFirst.vue#() @L5 -> TemplateFirst.vue#onInput() diff --git a/graph/test/typescript/expected/80-vue-sfc.entries b/graph/test/typescript/expected/80-vue-sfc.entries new file mode 100644 index 000000000..3d43a8448 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.entries @@ -0,0 +1,5 @@ +── entry_point (4) ── + exported_from_entry_module main#mount main.ts:3 + unimported_module TemplateFirst.vue# TemplateFirst.vue:1 + unimported_module main# main.ts:1 + unimported_module shims-vue# shims-vue.d.ts:1 diff --git a/graph/test/typescript/expected/80-vue-sfc.fields b/graph/test/typescript/expected/80-vue-sfc.fields new file mode 100644 index 000000000..2c586263a --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.fields @@ -0,0 +1 @@ +known_edge read child#Child({ msg: string }) -> { msg: string }#msg diff --git a/graph/test/typescript/expected/80-vue-sfc.fields-oracle b/graph/test/typescript/expected/80-vue-sfc.fields-oracle new file mode 100644 index 000000000..706b5d611 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.fields-oracle @@ -0,0 +1,9 @@ +80-vue-sfc [fields] + precision 0.0000 (0 correct, 1 wrong) + recall 0.0000 (0 of 1 the compiler resolved) + sites 1 resolved 1 (100.0%) + tiers known_edge=1 + access read=1 + not scored: 0 rows whose target is not a client declaration + WRONG child#Child({ msg: string }) READ { msg: string }#msg + MISSING child#Child({ msg: string }) READ child#msg diff --git a/graph/test/typescript/expected/80-vue-sfc.oracle b/graph/test/typescript/expected/80-vue-sfc.oracle new file mode 100644 index 000000000..8f740ea84 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.oracle @@ -0,0 +1,8 @@ +oracle=0 engine=7 agree=0 missing=0 (known 0, NEW 0) extra=7 + extra Comp.vue#() -> Comp.vue#onSave(number) + extra Comp.vue#() -> Comp.vue#sfcHelper() + extra Comp.vue#() -> child#Child({ msg: string }) + extra Comp.vue#() -> child#format(number) + extra TemplateFirst.vue#() -> TemplateFirst.vue#onInput() + extra TemplateFirst.vue#() -> TemplateFirst.vue#shown() + extra TemplateFirst.vue#() -> child#MyBadge() diff --git a/graph/test/typescript/expected/80-vue-sfc.type-use b/graph/test/typescript/expected/80-vue-sfc.type-use new file mode 100644 index 000000000..e69de29bb diff --git a/graph/test/typescript/expected/80-vue-sfc.types-oracle b/graph/test/typescript/expected/80-vue-sfc.types-oracle new file mode 100644 index 000000000..01d62a428 --- /dev/null +++ b/graph/test/typescript/expected/80-vue-sfc.types-oracle @@ -0,0 +1,7 @@ +80-vue-sfc [types] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 0 resolved 0 + tiers + contexts + not scored: 0 rows whose target is not a client declaration diff --git a/parser/src/parsers/javascript/extractors/js-fact-extractor.ts b/parser/src/parsers/javascript/extractors/js-fact-extractor.ts index cea8168dc..639334369 100644 --- a/parser/src/parsers/javascript/extractors/js-fact-extractor.ts +++ b/parser/src/parsers/javascript/extractors/js-fact-extractor.ts @@ -111,6 +111,11 @@ export interface JsFileExtractionOptions { readonly projectModuleHashes: ReadonlyMap; /** Absolute path -> project-relative path, extension stripped. */ readonly toProjectRelative: (absolutePath: string) => string; + /** + * How `sourceText` parses, when the file's extension cannot say: a `.vue` + * component's virtual script is JS or JSX by its `lang`, not by its name. + */ + readonly scriptKind?: ts.ScriptKind; } export interface JsFileFacts { @@ -160,7 +165,7 @@ export function extractJavaScriptFile(options: JsFileExtractionOptions): JsFileF options.sourceText, ts.ScriptTarget.Latest, true, - scriptKindFor(options.absoluteFilePath) + options.scriptKind ?? scriptKindFor(options.absoluteFilePath) ); const moduleResult = extractModule({ diff --git a/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts b/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts index d6e2e89eb..2766c34b6 100644 --- a/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts +++ b/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts @@ -28,6 +28,7 @@ import { isRequireCall, pointOf, } from '@/utils/javascript'; +import { resolveVueSpecifier } from '@/utils/vue-sfc'; /** * `js_import` and `js_export`, minted in a **second pass from expression rows**. @@ -1199,7 +1200,10 @@ class JsModuleEdgeExtractor { undefined, m ).resolvedModule?.resolvedFileName; - const resolved = resolveIn(mode) ?? (mode === ts.ModuleKind.ESNext ? resolveIn(ts.ModuleKind.CommonJS) : undefined); + // tsc resolves no `.vue` import itself (its extensions are fixed), so a + // component in this program is looked up by path. + const resolved = resolveIn(mode) ?? (mode === ts.ModuleKind.ESNext ? resolveIn(ts.ModuleKind.CommonJS) : undefined) + ?? resolveVueSpecifier(specifier, this.options.absoluteFilePath); if (resolved === undefined) { return { filePath: '', outcome: JsImportResolutionOutcome.UNRESOLVED_MISSING }; } diff --git a/parser/src/parsers/typescript/extractors/ts-fact-extractor.ts b/parser/src/parsers/typescript/extractors/ts-fact-extractor.ts index 6c07f5461..da67a6b4b 100644 --- a/parser/src/parsers/typescript/extractors/ts-fact-extractor.ts +++ b/parser/src/parsers/typescript/extractors/ts-fact-extractor.ts @@ -43,6 +43,7 @@ import { ResolutionStats, TsLocalResolver, } from '@/parsers/typescript/extractors/ts-resolution-linker'; +import { resolveVueSpecifier } from '@/utils/vue-sfc'; /** * Extracts the whole fact spine for ONE TypeScript file. @@ -91,6 +92,11 @@ export interface TsFileExtractionOptions { readonly projectModuleHashes: ReadonlyMap; /** Absolute path -> project-relative path, extension stripped. */ readonly toProjectRelative: (absolutePath: string) => string; + /** + * How `sourceText` parses, when the file's extension cannot say: a `.vue` + * component's virtual script is TS or TSX by its `lang`, not by its name. + */ + readonly scriptKind?: ts.ScriptKind; } export interface TsFileFacts { @@ -142,7 +148,7 @@ export function extractTypeScriptFile(options: TsFileExtractionOptions): TsFileF options.sourceText, ts.ScriptTarget.Latest, true, - scriptKindFor(options.absoluteFilePath) + options.scriptKind ?? scriptKindFor(options.absoluteFilePath) ); const modules = extractModules({ @@ -173,7 +179,8 @@ export function extractTypeScriptFile(options: TsFileExtractionOptions): TsFileF options.compilerOptions, ts.sys ); - const fileName = resolved.resolvedModule?.resolvedFileName; + const fileName = resolved.resolvedModule?.resolvedFileName + ?? resolveVueSpecifier(specifier, options.absoluteFilePath); if (!fileName) { return undefined; } diff --git a/parser/src/parsers/typescript/extractors/ts-import-extractor.ts b/parser/src/parsers/typescript/extractors/ts-import-extractor.ts index 27c96967b..8c7e89525 100644 --- a/parser/src/parsers/typescript/extractors/ts-import-extractor.ts +++ b/parser/src/parsers/typescript/extractors/ts-import-extractor.ts @@ -5,6 +5,7 @@ import * as ts from 'typescript'; import { TsImportRegistry } from '@/analysis-types/typescript/TsImportRegistry'; import { TsImportKind, TsImportResolutionKind } from '@/enums/typescript/imports'; import { nodeId } from '@/parsers/typescript/extractors/ts-binder'; +import { resolveVueSpecifier, VUE_EXTENSION } from '@/utils/vue-sfc'; /** * Emits `ts_import` rows — schema §4.12. @@ -442,6 +443,18 @@ export class TsImportExtractor { ts.sys ); const module = resolved.resolvedModule; + const vueFile = module ? undefined : resolveVueSpecifier(specifier, this.options.absoluteFilePath); + if (vueFile !== undefined && this.options.projectModuleHashes.has(vueFile)) { + // A component in this program; tsc resolves no `.vue` import itself. + return { + absolutePath: vueFile, + relativePath: this.options.toProjectRelative(vueFile), + moduleHash: this.options.projectModuleHashes.get(vueFile)!, + kind: TsImportResolutionKind.RELATIVE_FILE, + extension: VUE_EXTENSION, + packageName: '', + }; + } if (!module) { // `undefined` is an honest answer, not a failure to try. It is also the // right answer for a wildcard ambient specifier like `"*.svg"`, which diff --git a/parser/src/parsers/typescript/tsconfig-resolver.ts b/parser/src/parsers/typescript/tsconfig-resolver.ts index 0363f3c8f..f36937469 100644 --- a/parser/src/parsers/typescript/tsconfig-resolver.ts +++ b/parser/src/parsers/typescript/tsconfig-resolver.ts @@ -243,7 +243,11 @@ export class TsConfigResolver { ts.sys, path.dirname(configPath), undefined, - configPath + configPath, + undefined, + // `.vue` components are the program's files too, as vue-tsc reads it: + // without this `include: ["src/**/*.vue"]` claims nothing. + [{ extension: '.vue', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }] ); result = { configPath, diff --git a/parser/src/utils/vue-sfc.ts b/parser/src/utils/vue-sfc.ts new file mode 100644 index 000000000..151e09f35 --- /dev/null +++ b/parser/src/utils/vue-sfc.ts @@ -0,0 +1,454 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import * as ts from 'typescript'; + +/** + * A Vue single-file component read as the script it compiles to. + * + * ## Why a virtual script, and why it keeps every line + * + * A `.vue` file is three languages in one file: ` + diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/List.svelte b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/List.svelte new file mode 100644 index 000000000..7d6739141 --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/List.svelte @@ -0,0 +1,8 @@ + +{#each items as p}
  • {show(p)} {onlyInMarkup(p)}
  • {/each} diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Price.vue b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Price.vue new file mode 100644 index 000000000..28d5800cb --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Price.vue @@ -0,0 +1,9 @@ + + + diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Typed.vue b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Typed.vue new file mode 100644 index 000000000..e695a8ce2 --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/components/Typed.vue @@ -0,0 +1,5 @@ + + diff --git a/graph/test/javascript/cases/70-single-file-component-scripts/src/lib/format.js b/graph/test/javascript/cases/70-single-file-component-scripts/src/lib/format.js new file mode 100644 index 000000000..46a90a21d --- /dev/null +++ b/graph/test/javascript/cases/70-single-file-component-scripts/src/lib/format.js @@ -0,0 +1,8 @@ +// Helpers called only from single-file components: every call to them lives in a +// diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.diag b/graph/test/javascript/expected/70-single-file-component-scripts.diag new file mode 100644 index 000000000..f5093dfa5 --- /dev/null +++ b/graph/test/javascript/expected/70-single-file-component-scripts.diag @@ -0,0 +1,13 @@ +import_cause components/List.svelte:2:12 svelte not_staged +import_cause components/Price.vue:4:10 vue not_staged +import_cause main.js:2:10 vue not_staged +package_entry single-file-component-scripts . [] DEFAULT_INDEX index.js MISSING_FILE -> - +unresolved components/Legacy.vue:7:26 METHOD_CALL $emit member_absent +unresolved components/List.svelte:5:3 FUNCTION_CALL onMount callee_untyped +unresolved components/Price.vue:6:15 FUNCTION_CALL defineProps callee_untyped +unresolved components/Price.vue:7:15 FUNCTION_CALL computed callee_untyped +unresolved components/Price.vue:8:22 METHOD_CALL log no_target +unresolved lib/format.js:3:47 METHOD_CALL toFixed no_target +unresolved lib/format.js:4:37 METHOD_CALL toLowerCase no_target +unresolved main.js:5:1 FUNCTION_CALL createApp callee_untyped +unresolved main.js:5:1 METHOD_CALL mount receiver_untyped diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.edges b/graph/test/javascript/expected/70-single-file-component-scripts.edges new file mode 100644 index 000000000..10564fe80 --- /dev/null +++ b/graph/test/javascript/expected/70-single-file-component-scripts.edges @@ -0,0 +1,20 @@ +components/Legacy.vue:6:31 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +components/Legacy.vue:7:26 METHOD_CALL this.$emit -> ambiguous_unknown - +components/Legacy.vue:7:48 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +components/List.svelte:5:3 FUNCTION_CALL onMount -> ambiguous_unknown - +components/List.svelte:5:3 FUNCTION_CALL onMount -> callback_registered components/List.svelte:5:11 +components/List.svelte:5:39 FUNCTION_CALL fetchProducts -> known_edge lib/format.js:5:1 fetchProducts +components/List.svelte:6:29 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice +components/Price.vue:6:15 FUNCTION_CALL defineProps -> ambiguous_unknown - +components/Price.vue:7:15 FUNCTION_CALL computed -> ambiguous_unknown - +components/Price.vue:7:15 FUNCTION_CALL computed -> callback_registered components/Price.vue:7:24 +components/Price.vue:7:30 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice +components/Price.vue:8:22 METHOD_CALL console.log -> ambient_terminal - +lib/format.js:3:47 METHOD_CALL n.toFixed -> ambient_terminal - +lib/format.js:4:37 METHOD_CALL s.toLowerCase -> ambient_terminal - +main.js:5:1 FUNCTION_CALL createApp -> ambiguous_unknown - +main.js:5:1 METHOD_CALL createApp(Price).mount -> ambiguous_unknown - +main.js:5:30 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +pages/index.astro:3:18 FUNCTION_CALL fetchProducts -> known_edge lib/format.js:5:1 fetchProducts +pages/index.astro:4:14 FUNCTION_CALL slugify -> known_edge lib/format.js:4:1 slugify +pages/index.astro:9:20 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.oracle b/graph/test/javascript/expected/70-single-file-component-scripts.oracle new file mode 100644 index 000000000..72b4acd06 --- /dev/null +++ b/graph/test/javascript/expected/70-single-file-component-scripts.oracle @@ -0,0 +1,2 @@ +main.js:5:30 FUNCTION_CALL slugify EXACT lib/format.js:4:1 +# defects: 0 diff --git a/parser/src/constants/javascript-constants.ts b/parser/src/constants/javascript-constants.ts index 74c41034a..0463c83e9 100644 --- a/parser/src/constants/javascript-constants.ts +++ b/parser/src/constants/javascript-constants.ts @@ -98,6 +98,19 @@ export const JS_DEFAULT_EXPORT_NAME = 'default'; */ export const JS_SOURCE_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs'] as const; +/** + * Single-file components: markup files whose JavaScript lives in `\n'], + ['a.vue', '\n\n'], + ['a.svelte', '{g()}'], + ['a.astro', '---\n', 'h()\n', '---\n

    {h()}

    \n'], + ]; + for (const [name, ...parts] of COMPONENTS) { + const text = parts.join(''); + const want = parts.map((part, i) => (i % 2 === 1 ? part : part.replace(/[^\n]/g, ' '))).join(''); + const got = sfcScriptText(name, text).text; + if (got !== want) { + failures += fail(`sfcScriptText(${name}, ${JSON.stringify(text)}) is ${JSON.stringify(got)}, ` + + `expected ${JSON.stringify(want)}`); + } + } + // ---- 2. source-level: nobody rolls their own. const KNOWN = ['.js', '.jsx', '.mjs', '.cjs', '.flow']; const files: string[] = []; diff --git a/parser/src/utils/javascript/javascript-module-utils.ts b/parser/src/utils/javascript/javascript-module-utils.ts index f301664b2..fad52cfd2 100644 --- a/parser/src/utils/javascript/javascript-module-utils.ts +++ b/parser/src/utils/javascript/javascript-module-utils.ts @@ -1,4 +1,4 @@ -import { JS_SOURCE_EXTENSIONS } from '@/constants/javascript-constants'; +import { JS_COMPONENT_EXTENSIONS, JS_SOURCE_EXTENSIONS } from '@/constants/javascript-constants'; /** * What counts as a platform module, in one place. @@ -129,7 +129,96 @@ export function jsExtensionOf(fileName: string): string { // table, which is the argument for having written one: the function was in use // at eight call sites and every one of them happened to pass a `.js` family // name, so nothing else would ever have shown it. - return (JS_SOURCE_EXTENSIONS as readonly string[]).includes(extension) ? extension : ''; + if ((JS_SOURCE_EXTENSIONS as readonly string[]).includes(extension)) { + return extension; + } + // A component has no `.flow` spelling: `a.vue.flow` is not a declaration file. + return base === name && (JS_COMPONENT_EXTENSIONS as readonly string[]).includes(extension) + ? extension + : ''; +} + +/** Is this a single-file component (`.vue`, `.svelte`, `.astro`), by name? */ +export function isJsComponentFile(fileName: string): boolean { + return (JS_COMPONENT_EXTENSIONS as readonly string[]).includes(jsExtensionOf(fileName)); +} + +/** One `