Skip to content

fix(extraction): name a function bound through a curried wrapper (#1747) - #1814

Open
maxmilian wants to merge 1 commit into
colbymchenry:mainfrom
maxmilian:fix/1747-wrapper-bound-fn
Open

maxmilian wants to merge 1 commit into
colbymchenry:mainfrom
maxmilian:fix/1747-wrapper-bound-fn

Conversation

@maxmilian

Copy link
Copy Markdown
Contributor

Fixes #1747.

reactHookBoundName already names an anonymous function after the variable_declarator that binds it. As @Dshuishui points out, the mechanism reads as a general one — it is bounded to three React hooks only by the callee test. This adds the same shape with a different bound, on both the TypeScript and the Rust kernel paths.

The bound

Not "any callee". Widening that far would give const total = useMemo(() => 1 + 1, []) a function node, and __tests__/react-hook-handlers.test.ts already asserts expect(names).not.toContain('total') — correctly, since a memo callback is a computation rather than a body worth a node.

The three shapes in the report have something narrower in common: the callee is itself a call, i.e. a factory that returns the wrapper.

shape callee admitted
Effect.fn("Session.run")(function* () {…}) Effect.fn("Session.run") — a call
connect(mapState)(function () {…}) connect(mapState) — a call
wrap("name")(() => {…}) wrap("name") — a call
useMemo(() => 1 + 1, []) useMemo — an identifier
[1,2].map(() => …) [1,2].map — a member expression

So the new path is curriedWrapperBoundName / curried_wrapper_bound_name, kept beside the React one rather than replacing it: the hook allowlist keeps its own meaning and its own tests, and this reads as one additional, independently bounded rule.

Generators are the second half of the fix

This is worth calling out because it is not visible in the issue's diagnosis. generator_function is in functionTypes (languages/typescript.ts:42, javascript.ts:6, mod.rs:85), but the guards in reactHookBoundName, react_hook_bound_name and declaratorBoundFunction all accept only arrow_function | function_expression.

The report's first repro is Effect.fn("Session.run")(function* () {…}), and function* is the common form in that ecosystem — broadening the callee test alone would not have fixed it. Generators are admitted in the new method and deliberately not added to reactHookBoundName: a React handler is never a generator, so nothing is gained there and the existing bound stays exactly as it was.

Tests

__tests__/curried-wrapper-handlers.test.ts, three of them:

  1. the fix — all four callee shapes (Effect.fn with a generator and with an arrow, connect, wrap) produce a function node named by the declarator.
  2. the guard that makes the bound meaningfuluseMemo and arr.map still produce none. If these ever start producing function nodes, the callee test has been widened past what this change claims.
  3. the other half of the report — the half a node count alone does not cover: refsFrom(viaEffectGen) contains helper, and refsFrom(file) does not. That is the "a file appeared to call what the function called" symptom.

Red control — reverting only src/extraction/tree-sitter.ts to main and re-running:

× names the wrapped function after its declarator, for every callee shape
    AssertionError: expected [ 'helper' ] to include 'viaEffectGen'
× attributes the body calls to the function, not to the file
    AssertionError: no function node for viaEffectGen: expected undefined to be defined
  Tests  2 failed | 1 passed

Test 2 passes in both directions, which is what a vacuity guard should do.

Verification

  • npx tsc --noEmit: clean.
  • cargo build --release for the kernel: clean; npm run build:kernel stages the binding.
  • __tests__/curried-wrapper-handlers.test.ts + __tests__/react-hook-handlers.test.ts: 8 passed — the React path is unchanged.
  • __tests__/kernel-tsjs-parity.test.ts: 21 passed.
  • node scripts/kernel-parity.mjs on a file holding all six shapes above: 1/1 files byte-parity, 0 diffs, 0 deferred-to-wasm. Since the TypeScript side is known to produce the four function nodes, byte-parity is what establishes the Rust side does too.
  • Full suite after npm run build:ui: 4570 passed, 18 failed, 11 skipped.

On those 18 — I checked rather than assuming they were pre-existing. I ran the same nine files on a clean upstream/main worktree: 14 of the 18 fail there identically. The remaining 4 are all kernel-dart-parity (torture.dart / TortureCtors.dart, parity and CRLF), which that baseline run skipped because the kernel was not built in it. Building the kernel there and re-running gives the same 4 failures on unmodified main. So: 18 = 14 + 4, all pre-existing, none of them TypeScript/JavaScript extraction, and this change introduces no regression.

Notes

  • CHANGELOG entry added under ## [Unreleased]### Fixes#### Symbols, tests and the viewer, in user-facing language, with the re-index note.
  • No version bump, no tag, no publish.
  • I have not indexed sst/opencode to confirm the 1,008-function figure from the report; the mechanism is verified here, that scale claim is @Dshuishui's measurement rather than mine.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WgtWMywGTyu6twnk1Bu9nP

…bymchenry#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
@maxmilian
maxmilian marked this pull request as ready for review September 9, 2026 02:24
Dshuishui added a commit to fmagent-project/codegraph that referenced this pull request Sep 14, 2026
…colbymchenry#1814

The previous commit built this patch on upstream PR colbymchenry#1814 (issue colbymchenry#1747),
which names such a function after the declarator that binds it. Measured on
`sst/opencode`, that reaches 825 of 1,056 string-named `Effect.fn*` wrappers
where the fork's own version reaches 1,055.

The gap is structural, not a missing case. colbymchenry#1814 requires the wrapper's
result to land in a binding it recognises — a `variable_declarator`, and with
follow-up work a `pair`. But an Effect service is usually an object
*returned from inside a function*:

    function make() {
      return {
        getMode: Effect.fn("ACP.Session.getMode")(function* (id) { … }),
      }
    }

Nothing binds that object at a point the walk treats as a declarator, so the
members are never reached. Widening colbymchenry#1814 far enough to cover it would mean
rewriting it into what the fork already had, so this restores the fork's
version: it asks, wherever an anonymous function is walked, whether a
string-named wrapper encloses it. That question has no structural
precondition, which is why it reaches the shape above.

colbymchenry#1814 is also unlikely to land — an outside contribution, unreviewed for
five days — so building on it bought nothing in return.

Both extraction paths carry it; the native kernel is the one that runs by
default. `fm-agent-wrapper-named-functions.test.ts` pins the behaviour,
including the returned-service shape that distinguishes the two approaches,
and the resolution test is renamed to sit beside it under the same prefix.
Dshuishui added a commit to fmagent-project/codegraph that referenced this pull request Sep 14, 2026
…y#1814

Records why: colbymchenry#1814 binds through a declarator, so it does not reach a service
object returned from inside a function -- 825 of 1,056 string-named wrappers on
sst/opencode, against 1,055 here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TypeScript/JavaScript: a function bound through a wrapper call gets no node unless the wrapper is a React hook

1 participant