|
@@ -2004,12 +2004,100 @@ function piniaStoreEdges(ctx: ResolutionContext): Edge[] {
|
|
|
return edges;
|
|
return edges;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ── Vuex string-keyed dispatch / commit bridge ───────────────────────────────
|
|
|
|
|
+// Vuex dispatches actions/mutations by a runtime STRING key: `dispatch('user/login')`
|
|
|
|
|
+// / `commit('SET_TOKEN')` / `this.$store.dispatch('app/toggleDevice')`. The action
|
|
|
|
|
+// & mutation definitions are object-literal methods in store module files (now
|
|
|
|
|
+// extracted as function nodes). Bridge the string key to its node: the LAST `/`
|
|
|
|
|
+// segment is the action/mutation name; the preceding segment is the namespace
|
|
|
|
|
+// (≈ the store module's file). Resolve the name to a function node IN A STORE FILE
|
|
|
|
|
+// (the store-file gate excludes a same-named `api/` helper — `getInfo`/`login`
|
|
|
|
|
+// commonly collide), disambiguated by the namespace appearing in the path (or, for
|
|
|
|
|
+// a root key, the same file — Vuex's local-module `commit('M')` inside an action).
|
|
|
|
|
+const VUEX_DISPATCH_RE = /\b(?:dispatch|commit)\s*\(\s*['"]([A-Za-z][\w/]*)['"]/g;
|
|
|
|
|
+const VUEX_STORE_SIGNAL = /\bdefineStore\b|\bcreateStore\b|\bVuex\b|\bmutations\b|\bactions\b|\bgetters\b|\bnamespaced\b/g;
|
|
|
|
|
+const VUEX_FANOUT_CAP = 120;
|
|
|
|
|
+
|
|
|
|
|
+/** A path segment (dir or filename stem) equals `seg` — `…/modules/user.js` has
|
|
|
|
|
+ * the segment `user` for namespace `user`. */
|
|
|
|
|
+function pathHasSegment(filePath: string, seg: string): boolean {
|
|
|
|
|
+ return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function vuexDispatchEdges(ctx: ResolutionContext): Edge[] {
|
|
|
|
|
+ const storeFileCache = new Map<string, boolean>();
|
|
|
|
|
+ const isStoreFile = (file: string): boolean => {
|
|
|
|
|
+ let v = storeFileCache.get(file);
|
|
|
|
|
+ if (v === undefined) {
|
|
|
|
|
+ const c = ctx.readFile(file);
|
|
|
|
|
+ const seen = new Set<string>();
|
|
|
|
|
+ if (c) {
|
|
|
|
|
+ VUEX_STORE_SIGNAL.lastIndex = 0;
|
|
|
|
|
+ let sm: RegExpExecArray | null;
|
|
|
|
|
+ while ((sm = VUEX_STORE_SIGNAL.exec(c))) { seen.add(sm[0]); if (seen.size >= 2) break; }
|
|
|
|
|
+ }
|
|
|
|
|
+ v = seen.size >= 2;
|
|
|
|
|
+ storeFileCache.set(file, v);
|
|
|
|
|
+ }
|
|
|
|
|
+ return v;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const resolve = (key: string, dispatchFile: string): Node | null => {
|
|
|
|
|
+ const segs = key.split('/');
|
|
|
|
|
+ const action = segs[segs.length - 1]!;
|
|
|
|
|
+ const cands = ctx.getNodesByName(action).filter((n) => n.kind === 'function' && isStoreFile(n.filePath));
|
|
|
|
|
+ if (!cands.length) return null;
|
|
|
|
|
+ if (segs.length > 1) {
|
|
|
|
|
+ const mod = segs[segs.length - 2]!; // immediate namespace ≈ the module file
|
|
|
|
|
+ return cands.find((c) => pathHasSegment(c.filePath, mod)) ?? (cands.length === 1 ? cands[0]! : null);
|
|
|
|
|
+ }
|
|
|
|
|
+ // Root key: a local `commit('M')` inside an action targets the same module file;
|
|
|
|
|
+ // otherwise accept only an unambiguous single store-wide match.
|
|
|
|
|
+ return cands.find((c) => c.filePath === dispatchFile) ?? (cands.length === 1 ? cands[0]! : null);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const edges: Edge[] = [];
|
|
|
|
|
+ const seen = new Set<string>();
|
|
|
|
|
+ for (const file of ctx.getAllFiles()) {
|
|
|
|
|
+ if (!PINIA_CONSUMER_EXT.test(file)) continue;
|
|
|
|
|
+ const content = ctx.readFile(file);
|
|
|
|
|
+ if (!content || (!content.includes('dispatch(') && !content.includes('commit('))) continue;
|
|
|
|
|
+ const safe = stripCommentsForRegex(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript');
|
|
|
|
|
+ const nodesInFile = ctx.getNodesInFile(file);
|
|
|
|
|
+ const fallback = nodesInFile.find((n) => n.kind === 'component'); // .vue top-level
|
|
|
|
|
+ VUEX_DISPATCH_RE.lastIndex = 0;
|
|
|
|
|
+ let m: RegExpExecArray | null;
|
|
|
|
|
+ let added = 0;
|
|
|
|
|
+ while ((m = VUEX_DISPATCH_RE.exec(safe)) && added < VUEX_FANOUT_CAP) {
|
|
|
|
|
+ const key = m[1]!;
|
|
|
|
|
+ const line = safe.slice(0, m.index).split('\n').length;
|
|
|
|
|
+ const disp = enclosingFn(nodesInFile, line) ?? fallback;
|
|
|
|
|
+ if (!disp) continue;
|
|
|
|
|
+ const target = resolve(key, file);
|
|
|
|
|
+ if (!target || target.id === disp.id) continue;
|
|
|
|
|
+ const edgeKey = `${disp.id}>${target.id}`;
|
|
|
|
|
+ if (seen.has(edgeKey)) continue;
|
|
|
|
|
+ seen.add(edgeKey);
|
|
|
|
|
+ edges.push({
|
|
|
|
|
+ source: disp.id,
|
|
|
|
|
+ target: target.id,
|
|
|
|
|
+ kind: 'calls',
|
|
|
|
|
+ line,
|
|
|
|
|
+ provenance: 'heuristic',
|
|
|
|
|
+ metadata: { synthesizedBy: 'vuex-dispatch', via: key, registeredAt: `${file}:${line}` },
|
|
|
|
|
+ });
|
|
|
|
|
+ added++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return edges;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Synthesize dispatcher→callback edges (field observers + EventEmitters +
|
|
* Synthesize dispatcher→callback edges (field observers + EventEmitters +
|
|
|
* React re-render + JSX children + Vue templates + SvelteKit load + RN event
|
|
* React re-render + JSX children + Vue templates + SvelteKit load + RN event
|
|
|
* channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain +
|
|
* channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain +
|
|
|
* Redux-thunk dispatch chain + object-literal registry dispatch + RTK Query
|
|
* Redux-thunk dispatch chain + object-literal registry dispatch + RTK Query
|
|
|
- * generated-hook → endpoint + Pinia useStore().action() dispatch).
|
|
|
|
|
|
|
+ * generated-hook → endpoint + Pinia useStore().action() + Vuex string dispatch).
|
|
|
* Returns the count added. Never throws into indexing — callers wrap in try/catch.
|
|
* Returns the count added. Never throws into indexing — callers wrap in try/catch.
|
|
|
*/
|
|
*/
|
|
|
export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): number {
|
|
export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): number {
|
|
@@ -2051,6 +2139,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
|
|
|
const registryEdges = objectRegistryEdges(ctx);
|
|
const registryEdges = objectRegistryEdges(ctx);
|
|
|
const rtkEdges = rtkQueryEdges(queries, ctx);
|
|
const rtkEdges = rtkQueryEdges(queries, ctx);
|
|
|
const piniaEdges = piniaStoreEdges(ctx);
|
|
const piniaEdges = piniaStoreEdges(ctx);
|
|
|
|
|
+ const vuexEdges = vuexDispatchEdges(ctx);
|
|
|
|
|
|
|
|
const merged: Edge[] = [];
|
|
const merged: Edge[] = [];
|
|
|
const seen = new Set<string>();
|
|
const seen = new Set<string>();
|
|
@@ -2078,6 +2167,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
|
|
|
...registryEdges,
|
|
...registryEdges,
|
|
|
...rtkEdges,
|
|
...rtkEdges,
|
|
|
...piniaEdges,
|
|
...piniaEdges,
|
|
|
|
|
+ ...vuexEdges,
|
|
|
]) {
|
|
]) {
|
|
|
const key = `${e.source}>${e.target}`;
|
|
const key = `${e.source}>${e.target}`;
|
|
|
if (seen.has(key)) continue;
|
|
if (seen.has(key)) continue;
|