feat(extraction): index Vuex/Pinia store actions, mutations, and getters
A Vue store's callable surface — Vuex `actions`/`mutations`/`getters` and Pinia
store actions — lived only as object-literal properties, so the symbols an agent
looks for (`login`, `getSessionList`, `getAuthMenuList`) were never nodes:
`codegraph search`/`codegraph_node` returned "not found" and the agent had to
read the store by hand. This extracts them as function nodes (with their real
bodies + callees), the foundation under any later dispatch-bridge synthesis.
A corpus probe (vue-element-admin, vue2-elm, Geeker-Admin, MallChatWeb) showed
Vue store dispatch is NOT one clean string-keyed shape but ~5; extraction here
covers the three dominant definition forms:
- Vuex MODULE: non-exported `const actions/mutations = {…}` collections
(gated by a ≥2-signal looksLikeVueStoreFile + the object-of-functions shape,
so a Redux file's stray `const actions` is a 0-node no-op).
- Pinia OPTIONS: `defineStore({ actions: {…}, getters: {…} })` — methods of
the actions/mutations/getters properties of a store-factory config.
- Pinia SETUP: `defineStore('id', () => { const foo = …; return {…} })` — the
body-local function consts (findPiniaSetupFn + extractPiniaSetupBody; the
generic body walk doesn't reach nested function scopes). Distinguished from
an inline action map via objectHasInlineFunctions so zustand/SvelteKit
extraction is unchanged.
Validated findable on element-admin (50 fns), Geeker (21), MallChat (68);
0-node no-op on a non-Vue control (uwave-web, unchanged at 4496 nodes). Deferred
(documented in the backlog): vue2-elm's `export default {…}` split-file +
computed-key `commit(CONST)` form (n=1), and the dispatch BRIDGE synthesis
(Vuex string-key + Pinia useStore().action()). Suite green (1610); new
__tests__/vue-store-extraction.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### New Features
|
||||
|
||||
- Vue store actions, mutations, and getters are now indexed as symbols you can find and read. Whether your store is **Vuex** (`mutations` / `actions` objects in a module) or **Pinia** — both the options form (`defineStore({ actions: { … } })`) and the setup form (`defineStore('id', () => { … })`, where actions are local functions) — each action, mutation, and getter is now a real node. So `codegraph search` finds `login` or `getSessionList`, and `codegraph_explore` / `codegraph_node` show its body and what it calls, instead of "not found" because the function only existed as an object-literal property. (Connecting a component's `dispatch('user/login')` / `store.action()` call through to the action is a separate follow-up.)
|
||||
- `codegraph_explore` now connects React data-fetching flows built on **RTK Query** (Redux Toolkit's `createApi`). An endpoint defined inside `createApi({ endpoints })` and the `useGetXQuery` / `useUpdateYMutation` hook it generates were both invisible to analysis — so "what does this component fetch?" or "where does `useGetThingQuery` get its data?" dead-ended, because the hook, the endpoint, and the component had nothing linking them. CodeGraph now indexes each endpoint and each generated hook as real symbols and wires the path `component → useGetXQuery → getX → queryFn`, so the flow resolves in one explore call instead of reading the API slice by hand. Both the arrow (`endpoints: build => ({ … })`) and method (`endpoints(builder) { return { … } }`) styles are recognized, along with the `useLazyGetXQuery` variant; hand-written hooks of a similar name are left untouched.
|
||||
|
||||
- `codegraph_explore` now surfaces the right code in large multi-layer projects. When you ask a backend-flow question in a repo that pairs an API server with a big frontend that mirrors the same domain words — say an `app/` admin UI sitting over an `api/` server — the server-side file that genuinely matches several of your query's terms is no longer pushed out of the results by the larger, more interconnected frontend layer. A file corroborated by two or more distinct query terms is now kept in the answer even when a denser unrelated layer would otherwise crowd it out, so "how does X read items / handle the request" returns the service or handler that does the work instead of a wall of frontend views. Single-layer projects are unaffected; set `CODEGRAPH_RANK_NO_MULTITERM=1` to revert to the previous ranking.
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Vue store action/mutation/getter extraction (the foundation for finding and
|
||||
* reading store logic — `codegraph_node login` / `getSessionList`).
|
||||
*
|
||||
* Vuex/Pinia define a store's callable surface as object-literal members nested
|
||||
* under `actions`/`mutations`/`getters`, or as body-local consts in a Pinia setup
|
||||
* store — none of which were extracted, so the symbols an agent looks for didn't
|
||||
* exist as nodes. This covers the three dominant forms:
|
||||
* - Vuex module: non-exported `const actions = {…}` / `const mutations = {…}`.
|
||||
* - Pinia options: `defineStore({ actions: {…}, getters: {…} })`.
|
||||
* - Pinia setup: `defineStore('id', () => { const foo = …; return { foo } })`.
|
||||
* And the precision gate: a non-exported `const actions = {…}` in a file that
|
||||
* isn't a Vue store contributes nothing.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
describe('vue store extraction', () => {
|
||||
let dir: string;
|
||||
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vue-store-')); });
|
||||
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
it('extracts Vuex module + Pinia options + Pinia setup store members as function nodes', async () => {
|
||||
// Vuex MODULE form: non-exported `const mutations`/`const actions` collections,
|
||||
// wired via a default export (element-admin style). Method shorthand + arrow pairs.
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'userModule.js'),
|
||||
`import { persistToken } from './auth-utils';
|
||||
const state = { token: '' };
|
||||
const mutations = {
|
||||
SET_TOKEN: (state, token) => { state.token = token; },
|
||||
};
|
||||
const actions = {
|
||||
login({ commit }, info) {
|
||||
persistToken(info.token);
|
||||
},
|
||||
async logout({ commit }) {
|
||||
commit('SET_TOKEN', '');
|
||||
},
|
||||
};
|
||||
export default { namespaced: true, state, mutations, actions };
|
||||
`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'auth-utils.js'),
|
||||
`export function persistToken(token) { return token; }
|
||||
`
|
||||
);
|
||||
// Pinia OPTIONS form: actions + getters as object properties of a defineStore config.
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'authStore.ts'),
|
||||
`import { defineStore } from 'pinia';
|
||||
export const useAuthStore = defineStore({
|
||||
id: 'auth',
|
||||
state: () => ({ name: '' }),
|
||||
getters: {
|
||||
upperName: state => state.name.toUpperCase(),
|
||||
},
|
||||
actions: {
|
||||
async fetchMenu() { return loadMenu(); },
|
||||
setName(n: string) { this.name = n; },
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
// Pinia SETUP form: actions are body-local consts exposed via the return block.
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'chatStore.ts'),
|
||||
`import { defineStore } from 'pinia';
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const list = reactive([]);
|
||||
const getList = async () => { return fetchList(); };
|
||||
function pushItem(x) { list.push(x); }
|
||||
return { list, getList, pushItem };
|
||||
});
|
||||
`
|
||||
);
|
||||
|
||||
const cg = await CodeGraph.init(dir, { silent: true });
|
||||
await cg.indexAll();
|
||||
const db = (cg as any).db.db;
|
||||
const fn = (name: string) =>
|
||||
db.prepare(`SELECT count(*) c FROM nodes WHERE name = ? AND kind = 'function'`).get(name).c;
|
||||
|
||||
// Vuex module: actions + mutations extracted.
|
||||
expect(fn('login')).toBeGreaterThan(0);
|
||||
expect(fn('logout')).toBeGreaterThan(0);
|
||||
expect(fn('SET_TOKEN')).toBeGreaterThan(0);
|
||||
// Pinia options: actions + getter extracted.
|
||||
expect(fn('fetchMenu')).toBeGreaterThan(0);
|
||||
expect(fn('setName')).toBeGreaterThan(0);
|
||||
expect(fn('upperName')).toBeGreaterThan(0);
|
||||
// Pinia setup: body-local actions extracted (and reachable via their bodies).
|
||||
expect(fn('getList')).toBeGreaterThan(0);
|
||||
expect(fn('pushItem')).toBeGreaterThan(0);
|
||||
|
||||
// The extracted action spans its real body — `login`'s `persistToken(...)`
|
||||
// call attributes to it (extraction, not the deferred dispatch synthesis).
|
||||
const loginCalls = db
|
||||
.prepare(
|
||||
`SELECT t.name FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
|
||||
WHERE s.name = 'login' AND e.kind = 'calls'`
|
||||
)
|
||||
.all()
|
||||
.map((r: any) => r.name);
|
||||
expect(loginCalls).toContain('persistToken');
|
||||
|
||||
cg.close?.();
|
||||
});
|
||||
|
||||
it('does not extract a non-exported `const actions = {…}` outside a Vue store file', async () => {
|
||||
// A plain module that happens to hold a non-exported `const actions` object of
|
||||
// functions, but lacks any second Vue-store signal — the gate must not fire.
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'commands.js'),
|
||||
`const actions = {
|
||||
doThing() { return 1; },
|
||||
doOther() { return 2; },
|
||||
};
|
||||
export function run(key) { return actions[key](); }
|
||||
`
|
||||
);
|
||||
|
||||
const cg = await CodeGraph.init(dir, { silent: true });
|
||||
await cg.indexAll();
|
||||
const db = (cg as any).db.db;
|
||||
|
||||
expect(db.prepare(`SELECT count(*) c FROM nodes WHERE name = 'doThing'`).get().c).toBe(0);
|
||||
expect(db.prepare(`SELECT count(*) c FROM nodes WHERE name = 'doOther'`).get().c).toBe(0);
|
||||
// The real exported function is still extracted normally.
|
||||
expect(db.prepare(`SELECT count(*) c FROM nodes WHERE name = 'run' AND kind='function'`).get().c).toBeGreaterThan(0);
|
||||
|
||||
cg.close?.();
|
||||
});
|
||||
});
|
||||
@@ -56,7 +56,7 @@ Status legend (matches the playbook): ✅ done+validated · 🟡 shipped but und
|
||||
|---|---|---|---|---|
|
||||
| **Name→class registry / command bus** | any (TS/JS first) | object-literal registry `{key: Handler}` + computed-key dispatch `(new) reg[var](…)` | S (fan-out, `object-registry`) | ✅ **SHIPPED v1 (2026-06-20)** — `objectRegistryEdges`. Links each dispatcher fn → each registered handler's callable entry (a class's `execute`/run/handle method — preferring the method chained at the dispatch — or the function value). Precise on **xrengine** (CommandManager, 64 edges, class registry → `.execute`), **Prebid.js** (7: builder/consent/message dispatch, fn registry), **warp-drive** (1). **0 false positives** after: minified-file skip (avg line >200), **depth-aware** entry parse (top-level `key: Ident` only — method-shorthand/nested-object bodies don't leak), callable-only targets (no data `constant`), dynamic-dispatch gate. Handles constructor + field-initializer (`this.` normalized) forms. **Deferred (recall, documented):** assign-then-call (`const h=reg[k]; h()` — warp-drive's main `COMMANDS`), augmentation (`reg[k]=H` — Prebid single-entry), method-shorthand entry recall, and the **cross-file barrel-namespace** variant (trezor `getMethod`: `import * as M; M[method]→new` + computed dynamic import + camel↔Pascal — the hard tier, still 🔬). |
|
||||
| **RTK Query** | TS / Redux Toolkit | `createApi({ endpoints: b => ({ getX: b.query(...) }) })` → generated `useGetXQuery` hook → component; endpoint name ↔ hook name (`getX`↔`useGetXQuery`) is convention | X (extract endpoints) + S (endpoint→hook) | ✅ **SHIPPED (2026-06-20)** — `synthesizedBy:'rtk-query'`. **X:** extraction mints a function node per endpoint (named by its key, spanning the `queryFn`/`query` handler so its calls attribute; both `endpoints: b => ({…})` arrow and `endpoints(b){ return {…} }` method forms; a factory-handler endpoint `queryFn: makeFn(url)` falls back to a bare node spanning the builder call) **and** per generated-hook binding from `export const {…} = api` (carrying the sentinel signature `= RTK Query generated hook`). **S:** `rtkQueryEdges` bridges hook→same-file endpoint by the naming convention (strip `use` + optional `Lazy` + `Query`/`Mutation`, lc head). Component→hook is normal import/call resolution; hook→endpoint surfaces in explore as `dynamic: rtk query`. Validated **100% precision** (hooks == synth edges, **0 cross-file**) on **basetool** (small, 54 edges, both forms + factory fallback), **minusx-metabase** (small, 11), **shapeshift** (large, 13); **0** on the uwave-web control (no `createApi` → a complete no-op, 0 nodes/edges added). Sentinel gate correctly ignores hand-written look-alikes (shapeshift's `useFoxyQuery` is a real custom hook, never bridged). **Deferred:** cross-module `injectEndpoints` where the hook destructuring's RHS isn't the same bare api const (synth requires same-file endpoint). |
|
||||
| **Vuex / Pinia** | Vue | `store.dispatch('ns/action')` / `commit('mutation')` → action/mutation by string key (namespaced) | S (string-keyed, like `event-emitter`) | ⬜ |
|
||||
| **Vuex / Pinia** | Vue | `store.dispatch('ns/action')` / `commit('mutation')` → action/mutation by string key (namespaced); Pinia `useStore().action()` instance call | **X (extract collections) ✅ + S (dispatch bridge) ⬜** | 🟡 **EXTRACTION FOUNDATION SHIPPED (2026-06-20)** — store actions/mutations/getters are now nodes (`codegraph_node login`/`getSessionList` works). Corpus probe found this is **NOT one clean string-keyed shape** — it's ~5: **(1)** Vuex MODULE non-exported `const actions/mutations = {…}` (element-admin), **(2)** Vuex split-file `export default {…}` + computed-key `commit(CONST)` + `mapActions` (vue2-elm), **(3)** Pinia OPTIONS `defineStore({actions:{…}})` (Geeker), **(4)** Pinia SETUP `defineStore('id',()=>{const f=…;return{f}})` body-locals (MallChat), **(5)** Pinia `useStore().action()` instance dispatch. Extraction covers **1, 3, 4** (`extractObjectLiteralFunctions` on `actions`/`mutations`/`getters` collections + a `findPiniaSetupFn`/`extractPiniaSetupBody` for setup locals; `looksLikeVueStoreFile` ≥2-signal gate + the shape gate make it a **0-node no-op on a Redux control** despite the word "actions"). Validated findable on element-admin (50 fns), Geeker (21), MallChat (68); vue2-elm form-2 + computed-key **deferred** (n=1, needs export-default dispatch + const-string resolution). **Next — the dispatch BRIDGE synth (⬜), 2 separate members:** **(a)** Vuex string-key `dispatch('ns/action')`/`commit('M')` → action/mutation node (an `event-emitter`-style string-key clone, + namespace `module/`) — **n=1 in hand (element-admin)**, needs a 2nd string-literal Vuex repo; **(b)** Pinia `useStore().action()` → action (per-file `const s=useXStore()` var-binding → store-file method) — **n=2 in hand (Geeker+MallChat)**. Corpus: `/tmp/cg-vuex-eval/{vue-element-admin,vue2-elm,Geeker-Admin,MallChatWeb}`. |
|
||||
| **NgRx effects** | Angular | `createEffect(() => actions.pipe(ofType(LoginAction), …))` → effect handler; `Store.dispatch(new LoginAction())` → effect by action type/class | S (type/class-keyed) | ⬜ |
|
||||
|
||||
### Tier B — backend command/event/message buses (each needs its own canonical flow + ≥2 repos)
|
||||
|
||||
@@ -44,6 +44,16 @@ export { generateNodeId } from './tree-sitter-helpers';
|
||||
*/
|
||||
const RTK_HOOK_NAME_RE = /^use[A-Z][A-Za-z0-9]*(?:Query|Mutation)$/;
|
||||
|
||||
/** Vue store collections whose object-literal members are the symbols an agent
|
||||
* looks for. Extracted as function nodes so `actions`/`mutations`/`getters` are
|
||||
* findable + readable (the foundation under any later dispatch-bridge synth). */
|
||||
const VUE_STORE_COLLECTION_NAMES = new Set(['actions', 'mutations', 'getters']);
|
||||
/** Store-definition callees whose config object carries those collections. */
|
||||
const VUE_STORE_FACTORY_CALLEES = new Set(['defineStore', 'createStore']);
|
||||
/** Distinct signals that a file is a Vuex/Pinia store (≥2 ⇒ treat a bare
|
||||
* `const actions = {…}` as a store collection — see looksLikeVueStoreFile). */
|
||||
const VUE_STORE_FILE_SIGNAL = /\bdefineStore\b|\bcreateStore\b|\bVuex\b|\bmutations\b|\bactions\b|\bgetters\b|\bnamespaced\b/g;
|
||||
|
||||
/**
|
||||
* Extract the name from a node based on language
|
||||
*/
|
||||
@@ -325,6 +335,8 @@ export class TreeSitterExtractor {
|
||||
// (see flushFnRefCandidates).
|
||||
private fnRefSpec: FnRefSpec | undefined;
|
||||
private fnRefCandidates: Array<FnRefCandidate & { fromNodeId: string }> = [];
|
||||
// Memoized "is this a Vue store file" verdict (per-extractor = per-file).
|
||||
private vueStoreFile: boolean | null = null;
|
||||
|
||||
constructor(filePath: string, source: string, language?: Language) {
|
||||
this.filePath = filePath;
|
||||
@@ -2103,6 +2115,118 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
/** Cheap per-file heuristic: the file carries ≥2 distinct Vue-store signals
|
||||
* (defineStore/createStore/Vuex, or the actions/mutations/getters/namespaced
|
||||
* vocabulary). Gates the non-exported `const actions = {…}` Vuex-module form so
|
||||
* a stray `const actions` in unrelated code is never mistaken for a store. */
|
||||
private looksLikeVueStoreFile(): boolean {
|
||||
if (this.vueStoreFile !== null) return this.vueStoreFile;
|
||||
const seen = new Set<string>();
|
||||
VUE_STORE_FILE_SIGNAL.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = VUE_STORE_FILE_SIGNAL.exec(this.source))) {
|
||||
seen.add(m[0]);
|
||||
if (seen.size >= 2) break;
|
||||
}
|
||||
this.vueStoreFile = seen.size >= 2;
|
||||
return this.vueStoreFile;
|
||||
}
|
||||
|
||||
/** True if an object literal has ≥1 inline function member (`key: () => …` /
|
||||
* `method(){}`) — distinguishes an inline action map (zustand/SvelteKit form
|
||||
* actions) from a Pinia SETUP store's all-shorthand `return { foo, bar }`
|
||||
* (whose functions are body-local consts, walked normally instead). */
|
||||
private objectHasInlineFunctions(obj: SyntaxNode): boolean {
|
||||
for (let i = 0; i < obj.namedChildCount; i++) {
|
||||
const member = obj.namedChild(i);
|
||||
if (member?.type === 'method_definition') return true;
|
||||
if (member?.type === 'pair') {
|
||||
const v = getChildByField(member, 'value');
|
||||
if (v?.type === 'arrow_function' || v?.type === 'function_expression') return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Vue store action/mutation/getter collections defined INLINE in a store call:
|
||||
* `defineStore({ actions: {…}, getters: {…} })` (Pinia options form),
|
||||
* `defineStore('id', { actions: {…} })`, `createStore({ mutations: {…} })`,
|
||||
* `new Vuex.Store({ actions: {…} })`. Returns the object literals under those
|
||||
* keys so their methods become nodes. Gated on the store-factory callee. */
|
||||
private findVueStoreCollectionObjects(callNode: SyntaxNode): SyntaxNode[] {
|
||||
const callee = getChildByField(callNode, 'function') ?? getChildByField(callNode, 'constructor');
|
||||
if (!callee) return [];
|
||||
const calleeName =
|
||||
callee.type === 'identifier'
|
||||
? getNodeText(callee, this.source)
|
||||
: callee.type === 'member_expression'
|
||||
? getNodeText(getChildByField(callee, 'property') ?? callee, this.source)
|
||||
: '';
|
||||
if (!VUE_STORE_FACTORY_CALLEES.has(calleeName) && calleeName !== 'Store') return [];
|
||||
const args = getChildByField(callNode, 'arguments');
|
||||
if (!args) return [];
|
||||
const objects: SyntaxNode[] = [];
|
||||
for (let i = 0; i < args.namedChildCount; i++) {
|
||||
const arg = args.namedChild(i);
|
||||
if (arg?.type !== 'object' && arg?.type !== 'object_expression') continue;
|
||||
for (let j = 0; j < arg.namedChildCount; j++) {
|
||||
const member = arg.namedChild(j);
|
||||
if (member?.type !== 'pair') continue;
|
||||
const key = getChildByField(member, 'key');
|
||||
if (!key || !VUE_STORE_COLLECTION_NAMES.has(getNodeText(key, this.source))) continue;
|
||||
const value = getChildByField(member, 'value');
|
||||
if (value && (value.type === 'object' || value.type === 'object_expression')) {
|
||||
objects.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
/** The SETUP function of a Pinia setup store (`defineStore('id', () => {…})`)
|
||||
* — an arrow/function arg with a block body. Returns null for the options form
|
||||
* (`defineStore({…})`) and for any non-defineStore call. The setup body's local
|
||||
* function consts are the store's actions; the generic body walk doesn't reach
|
||||
* them (nested functions are separate scopes), so they're extracted explicitly. */
|
||||
private findPiniaSetupFn(callNode: SyntaxNode): SyntaxNode | null {
|
||||
const callee = getChildByField(callNode, 'function');
|
||||
if (!callee || callee.type !== 'identifier' || getNodeText(callee, this.source) !== 'defineStore') return null;
|
||||
const args = getChildByField(callNode, 'arguments');
|
||||
if (!args) return null;
|
||||
for (let i = 0; i < args.namedChildCount; i++) {
|
||||
const arg = args.namedChild(i);
|
||||
if (arg?.type !== 'arrow_function' && arg?.type !== 'function_expression') continue;
|
||||
const body = getChildByField(arg, 'body');
|
||||
if (body?.type === 'statement_block') return arg; // block body ⇒ setup form
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Extract a Pinia setup store's actions: the body-local `const foo = () => …`
|
||||
* / `function foo(){}` declarations, named by the binding. (State refs and other
|
||||
* consts are left to the normal value-extraction; only the functions matter as
|
||||
* the store's callable surface.) */
|
||||
private extractPiniaSetupBody(setupFn: SyntaxNode): void {
|
||||
const body = getChildByField(setupFn, 'body');
|
||||
if (!body || body.type !== 'statement_block') return;
|
||||
for (let i = 0; i < body.namedChildCount; i++) {
|
||||
const stmt = body.namedChild(i);
|
||||
if (!stmt) continue;
|
||||
if (stmt.type === 'function_declaration') {
|
||||
this.extractFunction(stmt);
|
||||
} else if (this.extractor!.variableTypes.includes(stmt.type)) {
|
||||
for (let j = 0; j < stmt.namedChildCount; j++) {
|
||||
const decl = stmt.namedChild(j);
|
||||
if (decl?.type !== 'variable_declarator') continue;
|
||||
const v = getChildByField(decl, 'value');
|
||||
if (v?.type === 'arrow_function' || v?.type === 'function_expression') {
|
||||
this.extractFunction(v); // name resolved from the parent declarator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a variable declaration (const, let, var, etc.)
|
||||
*
|
||||
@@ -2190,7 +2314,13 @@ export class TreeSitterExtractor {
|
||||
: valueNode?.type === 'call_expression'
|
||||
? this.findInitializerReturnedObject(valueNode)
|
||||
: null;
|
||||
const extractObjectMethods = isExported && !!objectOfFns;
|
||||
// Only treat as an inline object-of-functions when the object actually
|
||||
// HAS inline functions. A Pinia SETUP store `defineStore('id', () => {
|
||||
// const foo = …; return { foo } })` returns an ALL-SHORTHAND object
|
||||
// whose functions are body-local consts — it must fall through to a
|
||||
// normal body walk (extracting those consts), not be skipped here.
|
||||
const hasInlineFns = !!objectOfFns && this.objectHasInlineFunctions(objectOfFns);
|
||||
const extractObjectMethods = isExported && !!objectOfFns && hasInlineFns;
|
||||
|
||||
// RTK Query: `createApi`/`injectEndpoints` define endpoints as
|
||||
// object-literal properties whose values are `build.query/mutation(...)`
|
||||
@@ -2202,16 +2332,39 @@ export class TreeSitterExtractor {
|
||||
const rtkEndpoints =
|
||||
valueNode?.type === 'call_expression' ? this.findRtkEndpointsObject(valueNode) : null;
|
||||
|
||||
// Pinia SETUP store: `defineStore('id', () => { const foo = …; return {…} })`.
|
||||
// Its actions are body-local consts the generic walk can't reach.
|
||||
const piniaSetup =
|
||||
valueNode?.type === 'call_expression' ? this.findPiniaSetupFn(valueNode) : null;
|
||||
|
||||
// Vue store collections — make `actions`/`mutations`/`getters` findable
|
||||
// function nodes (the foundation under any later dispatch-bridge synth).
|
||||
// Two positions: INLINE in a store call (`defineStore({ actions: {…} })`
|
||||
// / `createStore` / `new Vuex.Store`), and the non-exported Vuex-MODULE
|
||||
// form (`const actions = {…}` at a store file's top level, wired via a
|
||||
// `export default { actions }`). The Pinia SETUP form is handled by the
|
||||
// body walk above (its actions are local consts).
|
||||
const storeCollections: SyntaxNode[] = [];
|
||||
if (valueNode?.type === 'call_expression' || valueNode?.type === 'new_expression') {
|
||||
storeCollections.push(...this.findVueStoreCollectionObjects(valueNode));
|
||||
}
|
||||
if (objectOfFns && !extractObjectMethods &&
|
||||
VUE_STORE_COLLECTION_NAMES.has(name) && this.looksLikeVueStoreFile()) {
|
||||
storeCollections.push(objectOfFns);
|
||||
}
|
||||
|
||||
// Visit the initializer body for calls — EXCEPT object literals (their
|
||||
// function-valued properties are extracted below) and the store-factory
|
||||
// / createApi call whose returned object we extract method-by-method
|
||||
// below (walking the whole call would re-visit those method arrows and
|
||||
// mis-attribute their inner calls to the file/module scope).
|
||||
// / createApi / store-collection call whose nested objects we extract
|
||||
// method-by-method below (walking the whole call would re-visit those
|
||||
// method arrows and mis-attribute their inner calls to the file scope).
|
||||
if (valueNode &&
|
||||
valueNode.type !== 'object' &&
|
||||
valueNode.type !== 'object_expression' &&
|
||||
!(extractObjectMethods && valueNode.type === 'call_expression') &&
|
||||
!rtkEndpoints) {
|
||||
!rtkEndpoints &&
|
||||
!piniaSetup &&
|
||||
storeCollections.length === 0) {
|
||||
this.visitFunctionBody(valueNode, '');
|
||||
}
|
||||
|
||||
@@ -2221,6 +2374,12 @@ export class TreeSitterExtractor {
|
||||
if (rtkEndpoints) {
|
||||
this.extractRtkEndpoints(rtkEndpoints);
|
||||
}
|
||||
if (piniaSetup) {
|
||||
this.extractPiniaSetupBody(piniaSetup);
|
||||
}
|
||||
for (const coll of storeCollections) {
|
||||
this.extractObjectLiteralFunctions(coll);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user