Skip to content

fix(ts): CALLS edge collector attribution for HOF/callback patterns (#1166)#1179

Closed
abhigyanpatwari wants to merge 1 commit into
mainfrom
fix/issue-1166-calls-edges
Closed

fix(ts): CALLS edge collector attribution for HOF/callback patterns (#1166)#1179
abhigyanpatwari wants to merge 1 commit into
mainfrom
fix/issue-1166-calls-edges

Conversation

@abhigyanpatwari

@abhigyanpatwari abhigyanpatwari commented Apr 29, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #1166. Two roots in the call-attribution code path (findEnclosingFunctionId in parse-worker.ts + the parallel findEnclosingFunction in call-processor.ts) caused ~75% of CALLS edges to disappear in HOF / callback patterns:

  • Bug A — phantom Function IDs. genericFuncName scanned arrow_function / function_expression children for the first identifier and returned it. For unparenthesized arrows like file => processFile(file) the first identifier is the parameter file, so calls inside got attributed to a synthetic Function file and emitted dangling CALLS edges. The user's (:Function)-[:CALLS]->() queries never saw them.
  • Bug B — anonymous object-property arrows. tsExtractFunctionName only named arrows whose parent was variable_declarator. addItem: (item) => set(...) (Zustand actions, TanStack queryFn, React Context providers, config objects) lives under a pair — treated as anonymous. With no named ancestor up to the file, calls inside fell back to the File and were invisible to context() / impact().

Fix:

  • genericFuncName returns null for anonymous JS/TS function-likes — the language hook is authoritative.
  • tsExtractFunctionName resolves names from pair parents (property_identifier / string keys; computed keys stay anonymous).
  • Mirror the new shape in TYPESCRIPT_QUERIES / JAVASCRIPT_QUERIES / the scope-resolution query so pair-with-arrow becomes a @definition.function node — call sourceIds resolve to a real graph node.

Why this fixes the user's data

For the issue's file-upload.ts repro:

  • processFile → fileToDataUrl (was missed via await fileToDataUrl(file) inside an outer arrow) → now captured.
  • processSelectedFiles → processFile inside Promise.all(files.map(file => processFile(file))) → was attributed to phantom Function file; now correctly attributed to processSelectedFiles.

For Zustand-style stores:

  • addItem, fetchData, etc. become Function nodes via the new pair capture pattern.
  • Calls inside their bodies (set, doSomething, api.fetch, …) attribute to the action name instead of the file.

The reproducer in the issue (Zustand useStore = create<State>()(devtools(persist((set, get) => ({ addItem: ..., fetchData: ... }))))) now resolves all expected edges; calls in the top-level expression (create/devtools/persist) correctly stay file-attributed because they live in the value of useStore, not inside a function body.

Test plan

  • Added 18 unit tests in test/unit/call-attribution-issue-1166.test.ts covering Bug A (genericFuncName for anonymous arrows, .map(x => fn(x)), parenthesized callbacks), Bug B (object-property arrows, function expressions, string keys, computed-key anonymity, Zustand and TanStack patterns), and regression guards (plain helpers, Promise constructor callbacks, top-level module-init expressions).
  • Definition-phase tests confirm pair-with-arrow now produces @definition.function captures so call sourceIds resolve to real Function nodes.
  • Full unit suite: 4795 passed, 2 skipped (was 4777 + 2 skipped — delta matches the 18 new tests).
  • Full integration suite: 2670/2670 passed (TS-focused subset re-run: 436 + 154 passed).
  • tsc --noEmit clean.

Files

  • src/core/ingestion/utils/ast-helpers.tsgenericFuncName early-return for arrow_function / function_expression.
  • src/core/ingestion/languages/typescript.tstsExtractFunctionName handles pair parents.
  • src/core/ingestion/tree-sitter-queries.ts — pair-with-arrow patterns in TYPESCRIPT_QUERIES and JAVASCRIPT_QUERIES.
  • src/core/ingestion/languages/typescript/query.ts — same patterns mirrored in the scope-resolution query.
  • test/unit/call-attribution-issue-1166.test.ts — new test file.

🤖 Generated with Claude Code


Note

Medium Risk
Updates TypeScript/JavaScript function-name inference and tree-sitter capture patterns, which can change how call graphs and symbol IDs are generated across many files; mistakes would mainly impact analysis accuracy rather than runtime security.

Overview
Fixes TS/JS call attribution in higher-order-function and callback-heavy code by stopping genericFuncName from inventing names for arrow_function/function_expression and instead relying on declarative context.

Extends TypeScript’s extractFunctionName to name arrow/functions assigned to object properties (e.g. { addItem: (...) => ... }, including string keys) while leaving computed keys anonymous, and mirrors this in both TYPESCRIPT_QUERIES/JAVASCRIPT_QUERIES and the TypeScript scope query so these property functions are emitted as real @definition.function nodes.

Adds a focused regression test suite for issue #1166 covering the previously-missed patterns (Promise/map callbacks, Zustand/TanStack-style objects) and guarding against phantom parameter-named functions.

Reviewed by Cursor Bugbot for commit 93a0be2. Bugbot is set up for automated code reviews on this repo. Configure here.

…nction

Two roots in `findEnclosingFunctionId` (parse-worker) and the parallel
`findEnclosingFunction` (call-processor):

A. `genericFuncName` scanned `arrow_function` / `function_expression`
   children for the first identifier and returned it. For unparenthesized
   arrows like `file => processFile(file)` the first identifier is the
   parameter `file`, so calls inside got attributed to a phantom
   `Function file` ID and emitted dangling CALLS edges that never showed
   up in `(:Function)-[:CALLS]->()` queries.

B. `tsExtractFunctionName` only named arrows whose parent was
   `variable_declarator`. Object-property arrows like
   `addItem: (item) => set(...)` (Zustand stores, TanStack queryFn,
   React Context providers, config objects) live under a `pair`, so they
   were treated as anonymous. With no named ancestor up to the file,
   every call inside fell back to the File and became invisible to
   `context()` / `impact()`.

Fix:
- `genericFuncName` returns null for anonymous JS/TS function-likes —
  the language hook is authoritative.
- `tsExtractFunctionName` resolves names from `pair` parents
  (property_identifier / string keys; computed keys stay anonymous).
- Mirror the new shape in `TYPESCRIPT_QUERIES` / `JAVASCRIPT_QUERIES` /
  the scope-resolution query so pair-with-arrow becomes a Function
  declaration node — call sourceIds resolve to a real graph node.

Adds 18 unit tests pinning attribution and definition behaviour for
plain helpers, `arr.map(x => fn(x))`, Promise constructor callbacks,
Zustand-style nested HOFs, TanStack query factories, string-keyed
pairs, and computed-key anonymity.

Fixes #1166

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gitnexus Ready Ready Preview, Comment Apr 29, 2026 0:04am

Request Review

@ReidenXerx

ReidenXerx commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

@abhigyanpatwari hi man appreciate your job! Could you also check my pr for this problem? #1175 i think our combined approach would be best

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 93a0be2. Configure here.

// Object property pair: `{ addItem: (item) => ... }`.
// tree-sitter-typescript uses `pair`; tree-sitter-javascript also exposes
// `pair`. (Older grammars used `property_assignment`; we accept both.)
if (parent.type === 'pair' || parent.type === 'property_assignment') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Queries missing property_assignment patterns cause dangling edges

Low Severity

tsExtractFunctionName accepts property_assignment parents (in addition to pair) and will return a function name for arrows under them, but the tree-sitter query patterns in TYPESCRIPT_QUERIES, JAVASCRIPT_QUERIES, and the scope-resolution query only add pair-based @definition.function / @declaration.function patterns — no property_assignment equivalents. If a grammar produces property_assignment nodes, findEnclosingFunctionId would construct a Function ID that has no corresponding definition-phase node, producing exactly the dangling CALLS edges this PR aims to eliminate.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 93a0be2. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

CI Report

Some checks failed

Pipeline Status

Stage Status Details
❌ Typecheck failure tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
7687 7686 0 1 323s

✅ All 7686 tests passed

1 test(s) skipped — expand for details
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 76.94% 22696/29496 76.93% 📈 +0.0 🟢 ███████████████░░░░░
Branches 65.95% 14640/22196 65.91% 📈 +0.0 🟢 █████████████░░░░░░░
Functions 81.91% 2210/2698 81.9% 📈 +0.0 🟢 ████████████████░░░░
Lines 79.79% 20479/25663 79.78% 📈 +0.0 🟢 ███████████████░░░░░

📋 View full run · Generated by CI

@abhigyanpatwari

Copy link
Copy Markdown
Owner Author

Hey @ReidenXerx , thanks for flagging your PR, agreed, your scope-resolution fixes (pass2AttachDeclarations anchor placement, JSX-as-CALLS, the Variable exclusion in isCallerAnchorLabel, and the findExportByName callable preference) are the right architectural treatment for the registry-primary path. Mine is narrower.

Would you mind pulling this branch (fix/issue-1166-calls-edges) into #1175 so yours becomes the umbrella PR? I'll
close #1179 once it's in. Also, please remember to add Fixes #1166 to the PR description Happy to leave any follow-up
notes on #1175 directly.

@ReidenXerx

Copy link
Copy Markdown
Contributor

Hey @ReidenXerx , thanks for flagging your PR, agreed, your scope-resolution fixes (pass2AttachDeclarations anchor placement, JSX-as-CALLS, the Variable exclusion in isCallerAnchorLabel, and the findExportByName callable preference) are the right architectural treatment for the registry-primary path. Mine is narrower.

Would you mind pulling this branch (fix/issue-1166-calls-edges) into #1175 so yours becomes the umbrella PR? I'll close #1179 once it's in. Also, please remember to add Fixes #1166 to the PR description Happy to leave any follow-up notes on #1175 directly.

ofc my man i will do

@ReidenXerx

Copy link
Copy Markdown
Contributor

@abhigyanpatwari merged your fix/issue-1166-calls-edges branch into #1175 as commit bb6aa0c8 — clean auto-merge, your 93a0be23 is preserved with authorship intact. Updated #1175's body to the umbrella scope with Fixes #1166, Closes #1179, and full credit. All your 18 unit tests + my 15 integration tests + the existing 236 TS tests pass on the merged branch.

Feel free to close this PR whenever — #1175 supersedes it. Thanks for the parallel work and the clear factoring of Bug A / Bug B; made the consolidation trivial.

@magyargergo

Copy link
Copy Markdown
Collaborator

Let's close this PR @abhigyanpatwari as @ReidenXerx merged your changes and we can proceed with his PR.

ReidenXerx added a commit to ReidenXerx/GitNexus that referenced this pull request Apr 30, 2026
Single line-length fix in `gitnexus/src/core/ingestion/languages/typescript.ts`
flagged by `quality / format` CI on commit ef96603. The unformatted block came
from the merge of upstream PR abhigyanpatwari#1179 (`fix/issue-1166-calls-edges`) where the
`pair`-with-arrow / `pair`-with-string-key handling was added; prettier wanted
the `.find` callback inlined onto a single line.

No behavior change. Pre-commit hook would have caught this locally if the
husky postinstall step had been able to write `.git/config` on this dev machine.

Made-with: Cursor
ReidenXerx added a commit to ReidenXerx/GitNexus that referenced this pull request Apr 30, 2026
…r arrow

Addresses the medium-severity finding in @abhigyanpatwari's review of abhigyanpatwari#1175:
the four `pair`-with-arrow patterns in `query.ts` anchored
`@declaration.function` on the outer `pair` node instead of the inner
`arrow_function` / `function_expression`. For multi-action object literals
like Zustand's

    persist((set) => ({
      addItem:    (item) => doA(item),
      removeItem: (item) => doB(item),
      fetchData:  ()     => doC(),
    }))

`pass2AttachDeclarations.atPosition(pair.startLine, pair.startCol)`
resolved to the *parent* `(set) => ({...})` callback's scope (because the
pair node starts at the property-key token, before the inner arrow's
`@scope.function` range). All three pair-function defs landed in the
same parent's `ownedDefs`, and `resolveCallerGraphId.ownedDefs.find(...)`
returned the FIRST one — `addItem` — for every walk-up. Calls inside
`removeItem` and `fetchData` mis-attributed to `addItem`; those two
functions had zero outgoing CALLS edges in the registry-primary path.

Single-pair fixtures (`bump` in `store.ts`, `queryFn` in `query-hook.ts`)
masked the defect because there is no ambiguity when only one
Function-like def lives in the parent's `ownedDefs` — `find()` is
deterministic over a single-element set.

Fix: move the `@declaration.function` anchor from the outer `pair` to
the inner `arrow_function` / `function_expression`, mirroring the
`lexical_declaration` patterns above (`const fn = () => {}`). The def
then lands in the arrow's own scope's `ownedDefs`, the
`rangesEqual(anchor.range, innermost.range)` auto-hoist promotes the
binding to the parent scope (so importers + lookups still find the name
in the surrounding scope), and each pair-arrow becomes an independent
caller anchor in the walk.

Tests:

  * Updated `useFeature → fetchData` expectation to `queryFn → fetchData`
    in `typescript-hof-callbacks.test.ts`. The new attribution is
    structurally correct: `fetchData()` is called from inside the named
    pair-arrow `queryFn: () => fetchData()`. The pre-fix expectation
    only worked because the pair-pattern bug rerouted the walk past the
    syntactic owner.

  * Added `multi-action-store.ts` fixture with three pair-arrows
    (`addItem` / `removeItem` / `fetchData`) plus three top-level call
    targets (`doA` / `doB` / `doC`). Four new tests pin per-action
    attribution: positive (each action calls its own target), negative
    (no sibling leakage), exact-set (the full pair set is what we
    expect), and the regression fingerprint (`addItem → doB` MUST be
    empty).

Validation:

  * `REGISTRY_PRIMARY_TYPESCRIPT=1 vitest run` on
    typescript-hof-callbacks (12 tests, +4 new), typescript-jsx-as-call
    (7), typescript (236), typescript-finalize, typescript-cross-file-imports,
    call-attribution-issue-1166 (18), all scope-resolution unit suites:
    886/886 pass on registry-primary AND legacy DAG paths.
  * Legacy DAG attribution was already correct via @abhigyanpatwari's
    `tsExtractFunctionName` pair-parent handling (abhigyanpatwari#1179, merged into this
    PR earlier); this fix brings the registry-primary path to the same
    behavior, restoring parity for multi-action objects.
  * `npx prettier --check .`, `tsc --noEmit`, and `eslint` clean on the
    three modified/added files.

Made-with: Cursor
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.

CALLS edge collector misses ~75% of functions; concentrated in HOF/callback patterns

3 participants