Skip to content

fix(callgraph): collect module-scope calls + object-literal arrow fns (closes #210)#221

Closed
Wolfvin wants to merge 1 commit into
mainfrom
fix/issue-210-callgraph-refcount-undercount
Closed

fix(callgraph): collect module-scope calls + object-literal arrow fns (closes #210)#221
Wolfvin wants to merge 1 commit into
mainfrom
fix/issue-210-callgraph-refcount-undercount

Conversation

@Wolfvin

@Wolfvin Wolfvin commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Closes #210

Patch Peta (delta struktural)

Files:

  • scripts/parsers/ts_backend_parser.py — tambah _find_module_scope_calls (collect calls di luar function body, attribute ke synthetic node) + _find_object_literal_methods + _parse_object_literal_pair (register arrow functions di object-literal pairs sebagai . nodes).
  • scripts/parsers/js_backend_parser.py — pasangan simetris: _find_module_scope_calls + _collect_object_literal_pairs + _parse_and_collect_object_pair.
  • tests/test_issue210_refcount_undercount.py — 9 regression tests (TS + JS, kedua pattern, + regression check untuk regular function calls).

Endpoints: none
Schema: node baru: synthetic node (id=<file>:0, fn=<module>, line=0, exported=True, node_type=module) hanya dibuat saat file punya module-scope calls. node baru: object_method nodes (fn=<varName>.<key>, node_type=object_method) untuk arrow functions di object-literal pairs.
Konvensi: rc/ref_count sekarang reliable untuk dua pattern yang sebelumnya silent-drop. Worker lain yang baca rc tidak perlu ubah — angka berubah jadi lebih akurat, bukan struktur field yang berubah.

Klaim + Bukti

Root cause (terverifikasi via reproduksi sebelum fix)

Issue body menduga bug di callgraph_engine.py (~line 1589/2055). Setelah investigasi: callgraph_engine.py TIDAK dipakai untuk rc field di search --mode symbol — itu hanya dipakai oleh dataflow command. Path rc yang benar:

scan command -> parsers/*.extract_references() -> edge_resolver.resolve_edges() -> registry (.codelens/backend_registry.json)
search --mode symbol -> search_symbols() -> load registry -> return ref_count as rc

Bukti callgraph_engine.py bukan path untuk rc:

$ grep -rn 'callgraph_engine\|CallGraphBuilder' scripts/commands/ scripts/search_engine.py scripts/registry.py
scripts/commands/dataflow.py:51:            from callgraph_engine import is_available
scripts/commands/dataflow.py:69:        from callgraph_engine import analyze_with_callgraph

Hanya dataflow.py yang pakai callgraph_engine.

Dua root cause di parsers

Root cause #1 — module-scope calls silent-dropped.

Parser hanya collect calls dari decl["body_node"] (function body). Calls di top-level file scope (di luar function apapun) tidak punya enclosing decl, jadi tidak pernah di-collect.

Reproduksi sebelum fix (TS parser):

src/routes/foo.ts:
  import { Router } from 'express';
  import { requirePermission } from '../middleware/permission-gate';
  const router = Router();
  router.post('/foo', requirePermission('admin'), (req, res) => { res.json({ ok: true }); });
  export default router;

Output parser: nodes=0, edges=0

Hasil: requirePermission dapat rc=0 meski dipanggil di file ini.

Root cause #2 — arrow functions di object literals invisible.

_parse_variable_declarator hanya handle value berupa arrow_function/function_expression/defineStore call. Saat value adalah object node (object literal), return None — arrow functions di dalam pair-nya tidak terdaftar sebagai node, sehingga calls di body-nya hilang.

Reproduksi sebelum fix (TS parser):

src/services/assignments.ts:
  export const assignmentService = {
    list: (ctx) => { if (!hasPermission(ctx.user, 'read')) return null; return []; },
    create: (ctx) => { if (!hasPermission(ctx.user, 'write')) throw new Error('no'); return {}; },
    remove: (ctx) => { if (!hasPermission(ctx.user, 'delete')) throw new Error('no'); return true; },
  };

Output parser: nodes=0, edges=0  (3 hasPermission calls hilang)

Kombinasi root cause #2 + 1 site di function biasa → hasPermission dapat rc=1 (hanya yang di function biasa tercatat), padahal ground truth 7.

Bukti fix (reproduksi sesudah fix)

Case 1 — requirePermission (3 route files, module-scope):

$ python /home/z/my-project/scripts/investigate_issue210.py
requirePermission: ref_count=3  (sebelumnya 0)
  <- from src/routes/foo.ts:0  (fn='<module>')
  <- from src/routes/bar.ts:0  (fn='<module>')
  <- from src/routes/baz.ts:0  (fn='<module>')

Case 2 — hasPermission (object-literal arrow fns + 1 regular fn = 7 sites):

$ python /home/z/my-project/scripts/investigate_issue210_v2.py
hasPermission: ref_count=7  (sebelumnya 1)
  <- from src/services/assignments.ts:4   (fn='assignmentService.list')
  <- from src/services/assignments.ts:8   (fn='assignmentService.create')
  <- from src/services/assignments.ts:12  (fn='assignmentService.remove')
  <- from src/services/task-templates.ts:4   (fn='taskTemplateService.list')
  <- from src/services/task-templates.ts:8   (fn='taskTemplateService.create')
  <- from src/services/task-templates.ts:12  (fn='taskTemplateService.remove')
  <- from src/middleware/permission-gate-use.ts:3  (fn='checkGate')

Sesuai Definition of Done #1 dan #2.

Test suite — 0 regresi baru

Filter yang relevan dengan issue (callgraph, ref_count, parser, edge_resolver, search, dead_code, impact):

$ python -m pytest tests/ --ignore=tests/test_integration.py --ignore=tests/test_lsp_server.py --ignore=tests/test_large_file_parsing.py -k 'callgraph or ref_count or parser or edge_resolver or search or dead_code or impact' -p no:cacheprovider
264 passed, 8 skipped, 1 failed (pre-existing: test_confidence schema_version)

Full suite (exclude pre-existing broken test files):

$ python -m pytest tests/ --ignore=tests/test_integration.py --ignore=tests/test_lsp_server.py --ignore=tests/test_large_file_parsing.py -p no:cacheprovider
2253 passed, 102 skipped, 17 failed (semua pre-existing: doctor/formatters/codelensignore/node_types — env-specific, bukan dari fix ini)

Baseline (sebelum fix, branch main): 17 failed, 2253 passed — jumlah sama, tidak ada failure baru. Pre-existing failures di-ignore per CONTEXT.md (test_integration.py workspace path issue, test_large_file_parsing.py tree-sitter Python binding segfault, test_lsp_server.py missing lsprotocol module).

Regression test baru — 9 tests, semua pass

$ python -m pytest tests/test_issue210_refcount_undercount.py -v
test_module_scope_middleware_factory_call_is_collected (TS) PASSED
test_object_literal_arrow_function_calls_are_collected (TS) PASSED
test_object_literal_arrow_functions_registered_as_named_nodes (TS) PASSED
test_module_node_only_created_when_module_scope_calls_exist (TS) PASSED
test_module_scope_middleware_factory_call_is_collected (JS) PASSED
test_object_literal_arrow_function_calls_are_collected (JS) PASSED
test_object_literal_arrow_functions_registered_as_named_nodes (JS) PASSED
test_ts_regular_function_calls_still_collected PASSED
test_js_regular_function_calls_still_collected PASSED
9 passed

Sesuai Definition of Done #4 dan #5.

Catatan Pendekatan

Issue body mengatakan: "Baca dulu _parse_calls_treesitter (baris ~1589) dan _resolve_call (baris ~2055) di callgraph_engine.py sebagai starting point — TAPI investigasi mandiri, jangan asumsikan lokasi bug ada di situ tanpa verifikasi." Investigasi menemukan bahwa callgraph_engine.py BUKAN path untuk rc field — path yang benar adalah parsers/*.pyedge_resolver.py. Fix diterapkan di tempat yang benar, bukan di lokasi yang di-suggest oleh issue body.

Fix sengaja diterapkan ke kedua parser (TS + JS) karena keduanya punya arsitektur paralel dan kemungkinan besar menderita bug yang sama. Test reproduksi dibuat untuk keduanya.

Breaking / Found-not-fixed

  • callgraph_engine.py (CallGraphBuilder._parse_calls_treesitter) juga punya bug serupa — calls di module scope tidak di-collect. Tapi karena callgraph_engine.py hanya dipakai oleh dataflow command (bukan rc/search/dead-code/impact/trace), fix di sana di luar scope issue fix(callgraph): reference_count severely undercounts cross-file calls #210. Worker lain bisa buat issue terpisah kalau menemukan dataflow under-report.
  • Anonymous arrow functions di module scope (e.g. (req, res) => { hasPermission(...); } sebagai argumen router.post(...)) — calls di dalamnya sekarang ter-attribute ke <module> (karena arrow_function ada di FN_BODY_TYPES ancestor check), tapi arrow function-nya sendiri tidak terdaftar sebagai node bernama. Ini acceptable: count-nya benar, hanya granularity caller yang coarse. Kalau perlu finer attribution, issue terpisah.
  • node_type field baru (module, object_method) ditambahkan ke node dict. Downstream commands yang filter berdasarkan node_type mungkin perlu update kalau tidak expect value baru ini. Cek grep: node_type sudah dipakai di parser lain (e.g. pinia_store, class), jadi field-nya sudah dikenal — value baru saja yang ditambah.

…closes #210)

reference_count (rc) was severely undercounting cross-file calls due to
two patterns silently dropped by the JS/TS backend parsers:

1. Module-scope calls — router.post(path, requirePermission('admin'), h)
   at the top level of a route file. The requirePermission('admin')
   call_expression was not inside any function declaration, so no edge
   was created. This caused middleware-factory functions to get rc=0
   even when used in dozens of route files.

2. Arrow functions in object literals — const svc = { list: (ctx) => { ... } }.
   The variable_declarator value is an 'object' node (not arrow_function),
   so _parse_variable_declarator returned None and the arrow body calls
   were lost. This caused service-map modules to be invisible to the
   call graph.

Root cause was in the PARSERS (js_backend_parser.py, ts_backend_parser.py),
NOT in callgraph_engine.py as the issue body suspected. callgraph_engine.py
is only used by the dataflow command; the rc field in search --mode symbol
comes from parser -> edge_resolver.resolve_edges() -> registry.

Fix:
- Add _find_module_scope_calls pass that walks the AST and collects
  call_expression / new_expression nodes not inside any function body.
  Attributed to a synthetic <module> node (exported=True, node_type=module)
  so dead-code does not flag it.
- Add _parse_object_literal_pair / _collect_object_literal_pairs that
  register arrow functions in object-literal pairs as <varName>.<key>
  nodes, so calls inside their bodies are properly attributed.
- For TS parser: separate _find_object_literal_methods pass (the main
  walk skips export_statement children to avoid double-counting).
- For JS parser: separate _collect_object_literal_pairs pass (same reason).

Tests: tests/test_issue210_refcount_undercount.py — 9 tests covering
both patterns for both TS and JS parsers, plus regression checks that
regular function-declaration call attribution is preserved.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
4.6% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@Wolfvin

Wolfvin commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

PR ditutup — konflik dengan PR #219 yang sudah merge duluan (issue #210 sudah closed).

Terima kasih untuk investigasi mendalamnya — 2 root cause yang ditemukan (module-scope calls + object-literal arrow functions) valid, tapi #219 sudah masuk lebih dulu dan cover root cause pertama (module-scope calls, inline arrow callback di argument) dengan approach synthetic source_id.

Root cause KEDUA yang ditemukan (arrow function sebagai object-literal value, contoh const service = { list: (ctx) => {...} }) BELUM ter-cover oleh #219 — ini genuinely gap terpisah dan valuable. Sudah dipecah jadi issue baru: #222.

Kalau mau lanjut kerjakan gap ini:

  1. Rebase ke main terbaru (sudah include fix(callgraph): extract module-top-level calls to fix rc undercount (closes #210) #219)
  2. Verifikasi ulang bahwa gap object-literal masih ada di state main sekarang (jangan asumsikan kode lama masih relevan)
  3. Buat PR baru HANYA untuk bagian object-literal — jangan include ulang module-scope-calls yang sudah di-handle fix(callgraph): extract module-top-level calls to fix rc undercount (closes #210) #219
  4. Reference issue fix(callgraph): arrow functions as object-literal values not tracked, undercounts ref_count #222, bukan fix(callgraph): reference_count severely undercounts cross-file calls #210 (sudah closed)

@Wolfvin Wolfvin closed this Jul 12, 2026
@Wolfvin Wolfvin deleted the fix/issue-210-callgraph-refcount-undercount branch July 12, 2026 03:02
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.

fix(callgraph): reference_count severely undercounts cross-file calls

1 participant