mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
9fb2c25bd5
* fix(extraction): name a function bound through a curried wrapper (#1747) reactHookBoundName already names an anonymous function after the variable_declarator that binds it. The shape is general, but the method is bounded to three React hooks, so `const run = Effect.fn("Session.run")(function* () {…})` produced no function node at all -- and its body's calls were attributed to the enclosing container, so a file gained an outgoing edge belonging to a function and the callee's caller list named the file instead. Bound the new path by a different, equally decidable test: the callee is itself a call, i.e. a factory returning the wrapper. That admits Effect.fn(...)(fn), connect(mapState)(fn) and a project's own wrap("n")(fn), and excludes the single-call forms whose argument is a computation -- useMemo(() => …, []), arr.map(…) -- which stay anonymous exactly as before. Widening to "any callee" would have given useMemo a function node and reddened react-hook-handlers.test.ts. Generators are admitted here and not in reactHookBoundName: a React handler is never a generator, while function* is the common form in the ecosystem this shape comes from, and it is what the report opens with -- broadening the callee test alone would not have fixed that repro. Mirrored in the Rust kernel; kernel-parity.mjs reports byte-parity on the new shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgtWMywGTyu6twnk1Bu9nP (cherry picked from commit4f2b40080d) * fix(extraction): name a curried-wrapper function that is an object member (#1747) The curried-wrapper bound stopped at a variable_declarator parent, so `return { getMode: Effect.fn("ACP.Session.getMode")(function* () {…}) }` still produced no function node. An Effect service is usually an object a factory returns, and on sst/opencode (df23b7f) these member positions were the bulk of what the branch missed. The member already has a name in the source: its property key. This repo names `key: () => {}` pairs by that key (extractObjectLiteralFunctions / objectKeyName), so a `pair` parent is now named the same way. The callee-is-a-call rule is unchanged, so `key: useMemo(() => …, [])` and `key: arr.map(…)` stay anonymous. extractObjectLiteralFunctions also reaches such a member, because an exported object with an inline function member is extracted member by member and its initializer is never walked. A wrapper passed as an argument to another call has no binding at all and is left as it was. Mirrored in the Rust kernel; kernel-parity.mjs reports byte-parity on the new shapes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dfb3aazPY1ccmp5vam4k3L (cherry picked from commitbbdef20542) * chore: drop CHANGELOG entry (collected separately) --------- Co-authored-by: Max Hsu <maxmilian@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
152 lines
6.7 KiB
TypeScript
152 lines
6.7 KiB
TypeScript
/**
|
|
* A function bound through a curried wrapper is named by its declarator (#1747).
|
|
*
|
|
* `react_hook_bound_name` already names an anonymous function after the
|
|
* `variable_declarator` that binds it, but only when the callee is one of three
|
|
* React hooks. The same shape with any other wrapper produced no function node
|
|
* at all — and the loss is not only a missing node: the body's calls attribute
|
|
* to the enclosing container instead, so a file or an outer function picks up an
|
|
* outgoing edge that belongs to the function, and the callee's caller list names
|
|
* the wrong thing.
|
|
*
|
|
* The bound used here is that the callee is itself a call — a factory that
|
|
* returns the wrapper. That admits `Effect.fn("x")(fn)`, `connect(m)(fn)` and a
|
|
* project's own `wrap("n")(fn)`, and leaves the single-call forms whose argument
|
|
* is a computation (`useMemo`, `arr.map`) anonymous exactly as before.
|
|
*/
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import { extractFromSource } from '../src/extraction';
|
|
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
|
|
|
|
beforeAll(async () => {
|
|
await initGrammars();
|
|
await loadAllGrammars();
|
|
});
|
|
|
|
const refsFrom = (result: ReturnType<typeof extractFromSource>, id: string) =>
|
|
result.unresolvedReferences.filter((r) => r.fromNodeId === id).map((r) => r.referenceName);
|
|
|
|
const CODE = `
|
|
declare const Effect: { fn: (n: string) => (b: unknown) => unknown };
|
|
declare function connect(m: unknown): (c: unknown) => unknown;
|
|
declare function wrap(n: string): (c: unknown) => unknown;
|
|
declare function useMemo<T>(f: () => T, d: unknown[]): T;
|
|
|
|
function helper() { return 1; }
|
|
|
|
const viaEffectGen = Effect.fn("Session.run")(function* () { return helper(); });
|
|
const viaEffectArrow = Effect.fn("Session.go")(() => { return helper(); });
|
|
const viaConnect = connect({})(function () { return helper(); });
|
|
const viaWrapArrow = wrap("n")(() => { return helper(); });
|
|
|
|
const total = useMemo(() => 1 + 1, []);
|
|
const mapped = [1, 2].map(() => helper());
|
|
`;
|
|
|
|
describe('curried wrapper handlers', () => {
|
|
it('names the wrapped function after its declarator, for every callee shape', () => {
|
|
const result = extractFromSource('src/b.ts', CODE);
|
|
const names = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
|
|
|
|
// A generator is the common form in the ecosystem this comes from, and it
|
|
// is the shape the report opens with — `function*` is not accepted by
|
|
// reactHookBoundName, so broadening the callee test alone would not fix it.
|
|
expect(names).toContain('viaEffectGen');
|
|
expect(names).toContain('viaEffectArrow');
|
|
expect(names).toContain('viaConnect');
|
|
expect(names).toContain('viaWrapArrow');
|
|
});
|
|
|
|
it('leaves single-call arguments anonymous, so the graph does not gain nodes for computations', () => {
|
|
const result = extractFromSource('src/b.ts', CODE);
|
|
const names = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
|
|
|
|
// The guard that makes the bound meaningful. `useMemo(() => …, [])` and
|
|
// `arr.map(() => …)` are single calls whose argument is a computation; if
|
|
// these ever start producing function nodes, the callee test has been
|
|
// widened past what this change claims.
|
|
expect(names).not.toContain('total');
|
|
expect(names).not.toContain('mapped');
|
|
});
|
|
|
|
it('attributes the body calls to the function, not to the file', () => {
|
|
const result = extractFromSource('src/b.ts', CODE);
|
|
const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'viaEffectGen');
|
|
expect(fn, 'no function node for viaEffectGen').toBeDefined();
|
|
|
|
// The half of the report that a node count alone does not cover: the file
|
|
// must not be the one calling helper.
|
|
expect(refsFrom(result, fn!.id)).toContain('helper');
|
|
const file = result.nodes.find((n) => n.kind === 'file');
|
|
expect(file, 'no file node').toBeDefined();
|
|
expect(refsFrom(result, file!.id)).not.toContain('helper');
|
|
});
|
|
});
|
|
|
|
// Object members (#1747 follow-up). An Effect service is usually an object a
|
|
// factory returns, so the wrapper's result lands in a `pair`, not a declarator.
|
|
// The property key names it — the same key extractObjectLiteralFunctions
|
|
// already uses for `key: () => {}`.
|
|
const MEMBER_CODE = `
|
|
declare const Effect: { fn: (n: string) => (b: unknown) => unknown };
|
|
declare function wrap(n: string): (c: unknown) => unknown;
|
|
declare function useMemo<T>(f: () => T, d: unknown[]): T;
|
|
|
|
function helper() { return 1; }
|
|
|
|
function make() {
|
|
return {
|
|
getMode: Effect.fn("ACP.Session.getMode")(function* (id: string) { return helper(); }),
|
|
'quotedKey': wrap("n")(() => { return helper(); }),
|
|
computed: useMemo(() => 1 + 1, []),
|
|
mapped: [1, 2].map((x) => x + 1),
|
|
};
|
|
}
|
|
|
|
const service = {
|
|
run: Effect.fn("Service.run")(function* () { return helper(); }),
|
|
plain: () => helper(),
|
|
};
|
|
|
|
// Exported with an inline function member: extractObjectLiteralFunctions mints
|
|
// the members one by one and the initializer is not walked, so the curried
|
|
// member has to be reached from there too.
|
|
export const mixed = {
|
|
direct: () => helper(),
|
|
wrapped: Effect.fn("Mixed.wrapped")(function* () { return helper(); }),
|
|
};
|
|
`;
|
|
|
|
describe('curried wrapper handlers as object members', () => {
|
|
const fnNodes = (result: ReturnType<typeof extractFromSource>) =>
|
|
result.nodes.filter((n) => n.kind === 'function');
|
|
|
|
it('names the wrapped function after its property key', () => {
|
|
const names = fnNodes(extractFromSource('src/m.ts', MEMBER_CODE)).map((n) => n.name);
|
|
expect(names).toContain('getMode');
|
|
expect(names).toContain('quotedKey');
|
|
expect(names).toContain('run');
|
|
expect(names).toContain('wrapped');
|
|
});
|
|
|
|
it('leaves single-call member values anonymous, and does not duplicate plain members', () => {
|
|
const names = fnNodes(extractFromSource('src/m.ts', MEMBER_CODE)).map((n) => n.name);
|
|
expect(names).not.toContain('computed');
|
|
expect(names).not.toContain('mapped');
|
|
// Neither path may mint a member twice.
|
|
expect(names.filter((n) => n === 'run')).toHaveLength(1);
|
|
expect(names.filter((n) => n === 'wrapped')).toHaveLength(1);
|
|
expect(names.filter((n) => n === 'plain').length).toBeLessThanOrEqual(1);
|
|
});
|
|
|
|
it('attributes the member body calls to the member, not to the factory', () => {
|
|
const result = extractFromSource('src/m.ts', MEMBER_CODE);
|
|
const getMode = fnNodes(result).find((n) => n.name === 'getMode');
|
|
expect(getMode, 'no function node for getMode').toBeDefined();
|
|
expect(refsFrom(result, getMode!.id)).toContain('helper');
|
|
const make = fnNodes(result).find((n) => n.name === 'make');
|
|
expect(make, 'no function node for make').toBeDefined();
|
|
expect(refsFrom(result, make!.id)).not.toContain('helper');
|
|
});
|
|
});
|