From a3d675ef4d7c0d014ee928e598bcacb8a632470d Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Wed, 30 Sep 2026 08:22:09 +0000 Subject: [PATCH] fix(vue): Options API methods, computed, watchers and hooks are symbols (#2140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A ` +` + ); + const cg = await CodeGraph.init(root, { index: true }); + try { + const nodes = cg.getNodesInFile('src/views/login/index.vue'); + const method = (name: string) => nodes.find((n) => n.kind === 'method' && n.name === name)!; + expect(nodes.filter((n) => n.kind === 'method').map((n) => n.name).sort()).toEqual(['data', 'handleLogin', 'validate']); + expect(method('handleLogin').qualifiedName).toBe('index::handleLogin'); + // The call inside handleLogin is handleLogin's, not the file's. + const calls = cg.getOutgoingEdges(method('handleLogin').id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.name); + expect(calls).toContain('validate'); + // `@click="handleLogin"` in the template runs the method. + const component = nodes.find((n) => n.kind === 'component')!; + const handlers = cg.getOutgoingEdges(component.id).filter((e) => e.target === method('handleLogin').id); + expect(handlers.length).toBeGreaterThan(0); + } finally { + cg.close(); + } + }); +}); diff --git a/src/extraction/vue-extractor.ts b/src/extraction/vue-extractor.ts index 862f17c79..5429a90de 100644 --- a/src/extraction/vue-extractor.ts +++ b/src/extraction/vue-extractor.ts @@ -2,6 +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 { vueOptionsMembers } from './vue-options-api'; /** * Vue built-in components — skipped so a `` / `` in the @@ -114,6 +115,67 @@ export class VueExtractor { return node; } + /** + * Method nodes for an Options API component's members (see + * ./vue-options-api), and the references and edges written inside each — + * which the TS extractor attributed to the file — re-attributed to it. + * Lines are block-relative here; the caller offsets them with the rest. + */ + private addOptionsMembers( + block: { content: string; startLine: number }, + result: ExtractionResult, + componentNodeId: string + ): void { + const members = vueOptionsMembers(block.content); + if (members.length === 0) return; + const component = this.nodes.find((n) => n.id === componentNodeId); + const owner = component?.name ?? 'component'; + const lineAt = (offset: number) => block.content.slice(0, offset).split('\n').length; + const colAt = (offset: number) => offset - block.content.lastIndexOf('\n', offset - 1) - 1; + const now = Date.now(); + const created: Node[] = []; + for (const m of members) { + const startLine = lineAt(m.start); + const endLine = lineAt(m.end); + created.push({ + id: generateNodeId(this.filePath, 'method', `${owner}.${m.name}`, startLine + block.startLine), + kind: 'method', + name: m.name, + qualifiedName: `${owner}::${m.name}`, + filePath: this.filePath, + language: 'vue', + startLine, + endLine, + startColumn: colAt(m.start), + endColumn: colAt(m.end), + updatedAt: now, + }); + } + // Innermost member for a line: `computed: { x: { get() {…} } }` is one member. + const memberAt = (line: number): Node | undefined => { + let best: Node | undefined; + for (const n of created) { + if (n.startLine <= line && n.endLine >= line && (!best || n.startLine >= best.startLine)) best = n; + } + return best; + }; + // What the TS extractor attributed to the file (or to nothing narrower). + const fileNode = result.nodes.find((n) => n.kind === 'file'); + const narrower = new Set(result.nodes.filter((n) => n.kind !== 'file').map((n) => n.id)); + const isFileLevel = (id: string) => (fileNode ? id === fileNode.id : !narrower.has(id)); + for (const ref of result.unresolvedReferences) { + if (!isFileLevel(ref.fromNodeId)) continue; + const member = memberAt(ref.line); + if (member) ref.fromNodeId = member.id; + } + for (const edge of result.edges) { + if (edge.kind === 'contains' || !edge.line || !isFileLevel(edge.source)) continue; + const member = memberAt(edge.line); + if (member) edge.source = member.id; + } + result.nodes.push(...created); + } + /** * Extract