mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
fix(js,ts): a factory's argument is not the module's default export
`export default createStore(reducer)` exports a store, but the wrapper
reading took `reducer` as the default, so `store.dispatch()` through the
default import linked the reducer. A wrapper call now hands on its
argument only when it is a component's name (`memo(Card)`,
`observer(function Settings…)`) or ends a curried chain
(`connect(mapState)(view)`, `traceFunction({…})(provision)`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -358,6 +358,38 @@ export function start() { return makeApp(); }
|
||||
expect(callees).toEqual(['createApp']);
|
||||
});
|
||||
|
||||
it('does not reach the argument of a factory a module exports by default', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'factory.ts'),
|
||||
`export function createStore(r: (s: unknown) => unknown) { return { dispatch(a: unknown) { return r(a); } }; }\n`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'store.ts'),
|
||||
`import { createStore } from './factory';
|
||||
function reducer(state: unknown) { return state; }
|
||||
export default createStore(reducer);
|
||||
`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'main.ts'),
|
||||
`import store from './store';
|
||||
export function start() { return store.dispatch({ type: 'go' }); }
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(dir, { silent: true });
|
||||
await cg.indexAll();
|
||||
const callees = (cg as any).db.db
|
||||
.prepare(
|
||||
`SELECT t.name AS n FROM edges e
|
||||
JOIN nodes s ON s.id = e.source
|
||||
JOIN nodes t ON t.id = e.target
|
||||
WHERE s.name = 'start' AND e.kind = 'calls'`
|
||||
)
|
||||
.all()
|
||||
.map((r: any) => r.n);
|
||||
expect(callees).not.toContain('reducer');
|
||||
});
|
||||
|
||||
it('reaches the function a wrapper call hands on, and the methods of an exported instance', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'tracing.ts'),
|
||||
|
||||
@@ -162,17 +162,19 @@ function skipTypeArguments(code: string, at: number): number {
|
||||
* function or class (`function Vans() {…}`, `class Baz …`), or what a wrapper
|
||||
* call wraps — the first argument of its last call, read the same way
|
||||
* (`memo(Foo)`, `connect(mapState)(Bar)`, `withRouter(connect(m)(Bar))`,
|
||||
* `observer(function Settings() {…})`, `styled(Button)\`…\``), or the class of an
|
||||
* instance (`new Storage()`, whose members are the class's). Null for one that
|
||||
* names nothing: an arrow function, an object literal, `Primitive.Root`, a
|
||||
* call on a literal (`requireNativeComponent('X')`). `code` is comment-free.
|
||||
* `observer(function Settings() {…})`, `styled(Button)\`…\``) when it is a
|
||||
* component's name or ends a curried chain (see {@link handsOn}) — or the
|
||||
* class of an instance (`new Storage()`, whose members are the class's). Null
|
||||
* for one that names nothing: an arrow function, an object literal,
|
||||
* `Primitive.Root`, a call on a literal (`requireNativeComponent('X')`), a
|
||||
* factory's argument (`createStore(reducer)`). `code` is comment-free.
|
||||
*/
|
||||
function namedBindingAt(code: string, at: number, wrapped = false, depth = 0): NamedBinding | null {
|
||||
function namedBindingAt(code: string, at: number, wrapped = false, depth = 0, curried = false): NamedBinding | null {
|
||||
const head = code.slice(at, at + 200);
|
||||
const declared = /^(?:async\s+)?function\b\s*\*?\s*([A-Za-z_$][\w$]*)?|^(?:abstract\s+)?class\b(?:\s+(?!extends\b|implements\b)([A-Za-z_$][\w$]*))?/.exec(head);
|
||||
if (declared) {
|
||||
const name = declared[1] ?? declared[2];
|
||||
return name ? { name, wrapped, nameAt: at + declared[0].length - name.length } : null;
|
||||
return name && handsOn(name, wrapped, curried) ? { name, wrapped, nameAt: at + declared[0].length - name.length } : null;
|
||||
}
|
||||
const constructed = /^new\s+([A-Za-z_$][\w$]*)\b(?!\s*\.)/.exec(head);
|
||||
if (constructed) return { name: constructed[1]!, wrapped: true };
|
||||
@@ -182,7 +184,7 @@ function namedBindingAt(code: string, at: number, wrapped = false, depth = 0): N
|
||||
while (code[i] === ' ' || code[i] === '\t') i++;
|
||||
// A name on its own, up to the end of its statement or argument.
|
||||
if (!callee.includes('.') && /^(?:[;,)\]}]|\r?\n|$|(?:as|satisfies)\b)/.test(code.slice(i, i + 12))) {
|
||||
return { name: callee, wrapped };
|
||||
return handsOn(callee, wrapped, curried) ? { name: callee, wrapped } : null;
|
||||
}
|
||||
i = skipSpace(code, i);
|
||||
if (code[i] === '<') {
|
||||
@@ -193,9 +195,11 @@ function namedBindingAt(code: string, at: number, wrapped = false, depth = 0): N
|
||||
// The call chain: `(…)(…)` and tagged templates; the last argument list is the one that wraps.
|
||||
// One whose end is out of reach (an apostrophe in JSX text reads as a quote) is the last one seen.
|
||||
let args = -1;
|
||||
let calls = 0;
|
||||
for (;;) {
|
||||
if (code[i] === '(') {
|
||||
args = i;
|
||||
calls++;
|
||||
const close = matchBracket(code, i);
|
||||
if (close < 0) break;
|
||||
i = skipSpace(code, close + 1);
|
||||
@@ -207,7 +211,18 @@ function namedBindingAt(code: string, at: number, wrapped = false, depth = 0): N
|
||||
}
|
||||
// `createSlice(…).reducer` hands on a member of what the call returns.
|
||||
if (args < 0 || code[i] === '.' || depth >= 4) return null;
|
||||
return namedBindingAt(code, skipSpace(code, args + 1), true, depth + 1);
|
||||
return namedBindingAt(code, skipSpace(code, args + 1), true, depth + 1, calls > 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a wrapper call hands on the name it is given: a component's name
|
||||
* (`memo(Card)`, `observer(function Settings() {…})`), or whatever ends a
|
||||
* curried chain (`connect(mapState)(view)`, `traceFunction({…})(provision)`).
|
||||
* `createStore(reducer)` hands on a store, not `reducer`. A name the
|
||||
* expression holds on its own is always handed on.
|
||||
*/
|
||||
function handsOn(name: string, wrapped: boolean, curried: boolean): boolean {
|
||||
return !wrapped || curried || /^[A-Z]/.test(name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user