mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
* 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>
This commit is contained in:
@@ -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" {
|
||||
|
||||
@@ -752,6 +752,13 @@ impl<'t> Walker<'t> {
|
||||
self.extract_function(node, Some(bound));
|
||||
return;
|
||||
}
|
||||
// `const run = Effect.fn("Session.run")(function* () {…})` (#1747):
|
||||
// the same declarator binding through a curried wrapper. Mirrors
|
||||
// TreeSitterExtractor's curriedWrapperBoundName.
|
||||
if let Some(bound) = self.curried_wrapper_bound_name(node) {
|
||||
self.extract_function(node, Some(bound));
|
||||
return;
|
||||
}
|
||||
// `const handleClear = () => {…}` inside a body (#1669): named by
|
||||
// its declarator, like at module scope. Mirrors
|
||||
// TreeSitterExtractor's declaratorBoundFunction.
|
||||
@@ -842,6 +849,71 @@ impl<'t> Walker<'t> {
|
||||
Some(self.text(name_node).to_string())
|
||||
}
|
||||
|
||||
/// The declarator name for an anonymous function passed to a CURRIED
|
||||
/// 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
|
||||
/// three React handler hooks. This is the same shape with a different,
|
||||
/// equally decidable bound: the callee is itself a call, i.e. a factory
|
||||
/// that returns the wrapper (#1747). Requiring that keeps it narrow —
|
||||
/// `useMemo(|| …, [])` and `arr.map(…)` are single calls and stay
|
||||
/// anonymous, exactly as before.
|
||||
///
|
||||
/// Generators are admitted here and not in `react_hook_bound_name`: a
|
||||
/// React handler is never a generator, while `function*` is the common
|
||||
/// form in the ecosystem this shape comes from.
|
||||
///
|
||||
/// Mirrors TreeSitterExtractor's curriedWrapperBoundName.
|
||||
fn curried_wrapper_bound_name(&self, node: Node<'t>) -> Option<String> {
|
||||
if !matches!(
|
||||
node.kind(),
|
||||
"arrow_function" | "function_expression" | "generator_function"
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let args = node.parent()?;
|
||||
if args.kind() != "arguments" {
|
||||
return None;
|
||||
}
|
||||
let first = args.named_child(0)?;
|
||||
if first.start_byte() != node.start_byte() || first.end_byte() != node.end_byte() {
|
||||
return None;
|
||||
}
|
||||
let call = args.parent()?;
|
||||
if call.kind() != "call_expression" {
|
||||
return None;
|
||||
}
|
||||
// The bound that replaces the hook allowlist: the thing being called
|
||||
// is itself a call, so this is a curried wrapper's second application.
|
||||
let callee = call.child_by_field_name("function")?;
|
||||
if callee.kind() != "call_expression" {
|
||||
return None;
|
||||
}
|
||||
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 = binder.child_by_field_name("name")?;
|
||||
if name_node.kind() != "identifier" {
|
||||
return None;
|
||||
}
|
||||
Some(self.text(name_node).to_string())
|
||||
}
|
||||
|
||||
/// extractName / extractNameRaw for the TS/JS configs.
|
||||
fn extract_name(&self, node: Node) -> String {
|
||||
// javascriptExtractor.resolveName: field_definition names its key the
|
||||
|
||||
Reference in New Issue
Block a user