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:
Max Hsu
2026-09-17 10:11:10 +08:00
parent 4f2b40080d
commit bbdef20542
5 changed files with 117 additions and 9 deletions
+11
View File
@@ -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" {
+17 -4
View File
@@ -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;
}