mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
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
This commit is contained in:
+1
-1
@@ -295,7 +295,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
- Python calls and file dependencies through `from package import module as alias` now appear in the graph, so renamed imports no longer hide live callers or imported modules. Thanks @JoeyNPP. (#1626)
|
||||
|
||||
- **A function bound through a wrapper call is now part of the graph.** `const run = Effect.fn("Session.run")(function* () {…})` — and the same shape with Redux's `connect(mapState)(…)` or a project's own `wrap("name")(…)` — produced no function at all: only React's `useCallback`, `useEffectEvent` and `useEvent` were recognized as naming the function they wrap. The calls inside those bodies were attributed to the file instead, so a file appeared to call what the function called, and asking who calls a helper named the file rather than the function that really calls it. One codebase carried over a thousand of these. Any wrapper that is itself the result of a call now names the function it wraps, generators included; a callback passed straight to a single call, such as `useMemo` or `map`, is a computation and stays anonymous as before. Re-index after upgrading. (#1747)
|
||||
- **A function bound through a wrapper call is now part of the graph.** `const run = Effect.fn("Session.run")(function* () {…})` — and the same shape with Redux's `connect(mapState)(…)` or a project's own `wrap("name")(…)` — produced no function at all: only React's `useCallback`, `useEffectEvent` and `useEvent` were recognized as naming the function they wrap. The calls inside those bodies were attributed to the file instead, so a file appeared to call what the function called, and asking who calls a helper named the file rather than the function that really calls it. One codebase carried over a thousand of these. Any wrapper that is itself the result of a call now names the function it wraps — after the variable it is assigned to, or after its property key when it is an object member such as `{ getMode: Effect.fn("…")(function* () {…}) }` — generators included; a callback passed straight to a single call, such as `useMemo` or `map`, is a computation and stays anonymous as before. Re-index after upgrading. (#1747)
|
||||
|
||||
## [1.6.0] - 2026-08-26
|
||||
|
||||
|
||||
@@ -82,3 +82,70 @@ describe('curried wrapper handlers', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -559,6 +559,17 @@ impl<'t> Walker<'t> {
|
||||
if matches!(v.kind(), "arrow_function" | "function_expression") {
|
||||
let name = util::object_key_name(self.text(k));
|
||||
self.extract_function(v, Some(name));
|
||||
} else if v.kind() == "call_expression" {
|
||||
// `key: Effect.fn("…")(function* () {…})` — see
|
||||
// curried_wrapper_bound_name.
|
||||
let fun = v
|
||||
.child_by_field_name("arguments")
|
||||
.and_then(|a| a.named_child(0));
|
||||
if let Some(f) = fun {
|
||||
if let Some(bound) = self.curried_wrapper_bound_name(f) {
|
||||
self.extract_function(f, Some(bound));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if member.kind() == "method_definition" {
|
||||
|
||||
@@ -850,7 +850,9 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
|
||||
/// The declarator name for an anonymous function passed to a CURRIED
|
||||
/// wrapper call — `const NAME = factory(...)(function () {…})` — or None.
|
||||
/// wrapper call — `const NAME = factory(...)(function () {…})` — or the
|
||||
/// property key when the call is an object member, `{ NAME: factory(...)(fn) }`;
|
||||
/// else None.
|
||||
///
|
||||
/// `react_hook_bound_name` above names a function through the declarator
|
||||
/// that binds it; the shape is general, but that method is bounded to the
|
||||
@@ -890,11 +892,22 @@ impl<'t> Walker<'t> {
|
||||
if callee.kind() != "call_expression" {
|
||||
return None;
|
||||
}
|
||||
let declarator = call.parent()?;
|
||||
if declarator.kind() != "variable_declarator" {
|
||||
let binder = call.parent()?;
|
||||
// `{ getMode: Effect.fn("…")(function* () {…}) }`: an object member is
|
||||
// named by its property key, as extract_object_literal_functions names
|
||||
// `key: () => {}`.
|
||||
if binder.kind() == "pair" {
|
||||
let key = binder.child_by_field_name("key")?;
|
||||
let value = binder.child_by_field_name("value")?;
|
||||
if value.start_byte() != call.start_byte() || value.end_byte() != call.end_byte() {
|
||||
return None;
|
||||
}
|
||||
return Some(util::object_key_name(self.text(key)));
|
||||
}
|
||||
if binder.kind() != "variable_declarator" {
|
||||
return None;
|
||||
}
|
||||
let name_node = declarator.child_by_field_name("name")?;
|
||||
let name_node = binder.child_by_field_name("name")?;
|
||||
if name_node.kind() != "identifier" {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -2305,6 +2305,11 @@ export class TreeSitterExtractor {
|
||||
const value = getChildByField(member, 'value');
|
||||
if (key && value && (value.type === 'arrow_function' || value.type === 'function_expression')) {
|
||||
this.extractFunction(value, this.objectKeyName(key));
|
||||
} else if (value?.type === 'call_expression') {
|
||||
// `key: Effect.fn("…")(function* () {…})` — see curriedWrapperBoundName.
|
||||
const fn = getChildByField(value, 'arguments')?.namedChild(0);
|
||||
const bound = fn ? this.curriedWrapperBoundName(fn) : null;
|
||||
if (fn && bound) this.extractFunction(fn, bound);
|
||||
}
|
||||
} else if (member.type === 'method_definition') {
|
||||
// Method shorthand: `{ fetchUser() {...} }`. extractMethod deliberately
|
||||
@@ -5593,7 +5598,8 @@ export class TreeSitterExtractor {
|
||||
|
||||
/**
|
||||
* The declarator name for an anonymous function passed to a CURRIED wrapper
|
||||
* call — `const NAME = factory(...)(function () {…})` — or null.
|
||||
* call — `const NAME = factory(...)(function () {…})` — or the property key
|
||||
* when the call is an object member, `{ NAME: factory(...)(fn) }`; else null.
|
||||
*
|
||||
* `reactHookBoundName` above already names a function through the declarator
|
||||
* that binds it; the shape is general, but that method is deliberately
|
||||
@@ -5638,9 +5644,20 @@ export class TreeSitterExtractor {
|
||||
// itself a call, so this is the second application of a curried wrapper.
|
||||
const callee = getChildByField(call, 'function');
|
||||
if (!callee || callee.type !== 'call_expression') return null;
|
||||
const declarator = call.parent;
|
||||
if (!declarator || declarator.type !== 'variable_declarator') return null;
|
||||
const nameNode = getChildByField(declarator, 'name');
|
||||
const binder = call.parent;
|
||||
if (!binder) return null;
|
||||
// `{ getMode: Effect.fn("…")(function* () {…}) }` — a service is often an
|
||||
// object a factory returns, so the wrapper's result lands in a `pair`. The
|
||||
// property key names it, as extractObjectLiteralFunctions names
|
||||
// `key: () => {}`.
|
||||
if (binder.type === 'pair') {
|
||||
const key = getChildByField(binder, 'key');
|
||||
const value = getChildByField(binder, 'value');
|
||||
if (!key || !value || value.startIndex !== call.startIndex || value.endIndex !== call.endIndex) return null;
|
||||
return this.objectKeyName(key);
|
||||
}
|
||||
if (binder.type !== 'variable_declarator') return null;
|
||||
const nameNode = getChildByField(binder, 'name');
|
||||
return nameNode?.type === 'identifier' ? getNodeText(nameNode, this.source) : null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user