{{ formatPrice(item.price) }}
+ + + + +`, + 'components/Options.vue': ` +{{ total(items) }}
+ + + + +`, + }); + cg = await CodeGraph.init(root, { index: true }); + + const edgesFrom = (file: string) => { + const comp = cg!.getNodesInFile(file).find((n) => n.kind === 'component')!; + return cg!.getOutgoingEdges(comp.id) + .filter((e) => e.kind === 'calls') + .map((e) => { + const t = cg!.getNode(e.target)!; + return `${t.name}@${t.filePath}:${e.line}`; + }) + .sort(); + }; + // The import decides which `formatPrice`; each edge is on its template line. + expect(edgesFrom('components/Setup.vue')).toEqual([ + 'formatPrice@utils/format.ts:2', + 'save@components/Setup.vue:3', + ]); + // A template calls the component's own methods without `this.`. + expect(edgesFrom('components/Options.vue')).toEqual([ + 'submit@components/Options.vue:3', + 'total@components/Options.vue:2', + ]); + }); + + it("calls a component's own function even where Nuxt auto-imports one of that name", async () => { + write({ + 'package.json': JSON.stringify({ name: 'nuxt-app', dependencies: { nuxt: '^4.0.0' } }), + 'pages/lists.vue': ` + + + + +`, + }); + cg = await CodeGraph.init(root, { index: true }); + + const nodes = cg.getNodesInFile('pages/lists.vue'); + const comp = nodes.find((n) => n.kind === 'component')!; + const own = nodes.find((n) => n.kind === 'function' && n.name === 'clearError')!; + const calls = cg.getOutgoingEdges(comp.id).filter((e) => e.kind === 'calls'); + // Both calls are the local function's — not Nuxt's `clearError`, which + // the framework rule used to answer with an edge from the component to itself. + expect(calls.filter((e) => e.target === own.id).map((e) => e.line).sort()).toEqual([2, 7]); + expect(calls.filter((e) => e.target === comp.id)).toEqual([]); + }); + + it('keeps a bare handler linked, and binds a handler expression only through its own call', async () => { + write({ + 'lib/bus.ts': 'export class Bus {\n emit(event: string) { return event; }\n}\n', + 'components/Dialog.vue': ` + + + + + + +`, + }); + cg = await CodeGraph.init(root, { index: true }); + + const comp = cg.getNodesInFile('components/Dialog.vue').find((n) => n.kind === 'component')!; + const targets = cg.getOutgoingEdges(comp.id) + .filter((e) => e.kind === 'calls') + .map((e) => cg!.getNode(e.target)!) + .map((n) => `${n.kind}:${n.name}`); + // `@click="close"` is still the handler it names. + expect(targets).toContain('function:close'); + // `emit('cancel')` is the component's `defineEmits` binding, and `open = + // !open` calls nothing: neither reaches Bus.emit. + expect(targets).not.toContain('method:emit'); + }); +}); diff --git a/codegraph-kernel/src/tsjs/extractors.rs b/codegraph-kernel/src/tsjs/extractors.rs index 6426db315..d8d087fc5 100644 --- a/codegraph-kernel/src/tsjs/extractors.rs +++ b/codegraph-kernel/src/tsjs/extractors.rs @@ -387,7 +387,7 @@ impl<'t> Walker<'t> { let Some(name_node) = child.child_by_field_name("name") else { continue }; let value = child.child_by_field_name("value"); - // Destructured patterns are skipped — except RTK Query generated + // Destructured patterns mint no node — except RTK Query generated // hooks (`export const { useGetXQuery } = api`). if matches!(name_node.kind(), "object_pattern" | "array_pattern") { if name_node.kind() == "object_pattern" @@ -395,6 +395,11 @@ impl<'t> Walker<'t> { { self.extract_rtk_hook_bindings(name_node, is_exported); } + // The initializer (and any default in the pattern) still runs: + // walk the declarator as a function body walks it, with the + // enclosing scope on the stack (#2340). Mirrors + // TreeSitterExtractor.extractVariable. + self.visit_function_body(child); continue; } let name = self.text(name_node).to_string(); diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 17b0b02ca..36cd473b9 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -2989,6 +2989,14 @@ export class TreeSitterExtractor { if (nameNode.type === 'object_pattern' && valueNode?.type === 'identifier') { this.extractRtkHookBindings(nameNode, isExported); } + // A pattern declares no symbol of its own, but its initializer + // (and any default value in the pattern) still runs. Walk the + // declarator the way a function body walks it, with the + // enclosing scope on the stack: the file for module code, which + // a `