(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield)
| 2097 | const THUNK_FANOUT_CAP = 24; |
| 2098 | |
| 2099 | async function reduxThunkEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 2100 | let scanned255 = 0; |
| 2101 | const edges: Edge[] = []; |
| 2102 | const seen = new Set<string>(); |
| 2103 | for (const node of queries.iterateNodesByKind('constant')) { |
| 2104 | if ((++scanned255 & 63) === 0) await onYield(); |
| 2105 | // Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call — |
| 2106 | // avoids reading every constant's body on a large repo. |
| 2107 | if (!node.signature || !THUNK_DECL_RE.test(node.signature)) continue; |
| 2108 | const content = ctx.readFile(node.filePath); |
| 2109 | const src = content && sliceLines(content, node.startLine, node.endLine); |
| 2110 | if (!src) continue; |
| 2111 | // Thunks are TS/JS-family (same // and /* */ comment syntax); map to a CommentLang. |
| 2112 | const safe = stripCommentsForRegex(src, node.language === 'javascript' || node.language === 'jsx' ? 'javascript' : 'typescript'); |
| 2113 | THUNK_DISPATCH_RE.lastIndex = 0; |
| 2114 | let m: RegExpExecArray | null; |
| 2115 | let added = 0; |
| 2116 | while ((m = THUNK_DISPATCH_RE.exec(safe)) && added < THUNK_FANOUT_CAP) { |
| 2117 | const name = m[1]!; |
| 2118 | if (name === node.name) continue; // self-dispatch (recursive thunk) — skip |
| 2119 | // Resolve the dispatched name, PREFERRING the thunk/action-creator over a same-named |
| 2120 | // service function. `dispatch(X(...))` dispatches a thunk or an action-creator (both |
| 2121 | // `constant`s) — never an unrelated helper that merely shares the name. On octo-call, |
| 2122 | // `leaveCall` is BOTH a `createAsyncThunk` const AND a service function, and the bare |
| 2123 | // `.find()` picked the function (wrong). Order: thunk const > other const > same-file |
| 2124 | // callable > first match. A single candidate (no collision) is unaffected. |
| 2125 | const cands = ctx |
| 2126 | .getNodesByName(name) |
| 2127 | .filter((n) => n.kind === 'constant' || n.kind === 'function' || n.kind === 'method'); |
| 2128 | const target = |
| 2129 | cands.find((n) => !!n.signature && THUNK_DECL_RE.test(n.signature)) ?? |
| 2130 | cands.find((n) => n.kind === 'constant') ?? |
| 2131 | cands.find((n) => n.filePath === node.filePath) ?? |
| 2132 | cands[0]; |
| 2133 | if (!target || target.id === node.id) continue; |
| 2134 | const key = `${node.id}>${target.id}`; |
| 2135 | if (seen.has(key)) continue; |
| 2136 | seen.add(key); |
| 2137 | const line = node.startLine + safe.slice(0, m.index).split('\n').length - 1; |
| 2138 | edges.push({ |
| 2139 | source: node.id, |
| 2140 | target: target.id, |
| 2141 | kind: 'calls', |
| 2142 | line, |
| 2143 | provenance: 'heuristic', |
| 2144 | metadata: { synthesizedBy: 'redux-thunk', via: name, registeredAt: `${node.filePath}:${line}` }, |
| 2145 | }); |
| 2146 | added++; |
| 2147 | } |
| 2148 | } |
| 2149 | return edges; |
| 2150 | } |
| 2151 | |
| 2152 | // ── Object-literal registry dispatch ───────────────────────────────────────── |
| 2153 | // A command/handler registry maps string keys → handler class/function symbols in an |
no test coverage detected