fix(callgraph): extract module-top-level calls to fix rc undercount (closes #210)#219
Merged
Merged
Conversation
…loses #210) Issue #210 reported reference_count severely undercounting cross-file calls for functions only called via two patterns common in Express/ Router modular backends: 1. Middleware-factory argument: router.post(path, requirePermission('admin'), handler) — requirePermission('admin') is a call_expression at module top-level, passed as argument to another call_expression. The per-function pass only walks registered function bodies; module-top-level calls were invisible. 2. Inline arrow callback body: router.post(path, fn, (req, res) => { hasPermission(...) }) — the arrow function is NOT registered as a function declaration (not assigned to a name), so its body was never walked for call extraction. Root cause: ts_backend_parser.py and js_backend_parser.py only extract calls via the per-function pass, which walks bodies of registered function/class declarations. Module-top-level calls and inline callback body calls were missed entirely. Fix: add a separate module-level pass to both parsers (and the regex fallback). The pass walks the AST root, skips subtrees of registered declarations (function_declaration, class_declaration, variable_declarator with arrow/function value) to avoid double-count, and extracts remaining call_expression / new_expression nodes. Edges use a synthetic source_id '<file>:0:<module>' with no corresponding graph_nodes entry — mirrors the convention used by hybrid_type_resolver._file_node_id for IMPORTS edges. This keeps list/search output free of fake <module> entries while correctly incrementing ref_count on the target side. Verified against a repro fixture mirroring the KDS backend patterns: - requirePermission rc: 0 -> 12 (12 call sites across 4 route files) - hasPermission rc: 1 -> 13 (12 from inline arrow callbacks + 1 from requirePermission's body) No regressions: 17 pre-existing test failures unchanged (test_doctor, test_confidence, test_formatters_phase2, test_node_types — all unrelated to callgraph). 16 new tests added covering both issue #210 patterns + double-count avoidance.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
4 tasks
|
This was referenced Jul 12, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #210
Patch Peta (delta struktural)
Files:
_find_module_level_callsmethod; call it fromextract_referencesafter the per-function pass to extract module-top-level calls and calls inside inline arrow callbacks (issue fix(callgraph): reference_count severely undercounts cross-file calls #210 root-cause fix for TS backend)._find_module_level_callsmethod (iterative DFS +keep_alivepattern per issue [RED TEAM] scan command segfaults on CodeLens repo itself — recursive walk_tree crashes (JS & Python parsers) #116/[BUG] File size threshold too conservative — large files silently skipped, breaking analysis completeness #163); call it fromextract_referencesafter the main walk loop (issue fix(callgraph): reference_count severely undercounts cross-file calls #210 root-cause fix for JS backend).<module>source (issue fix(callgraph): reference_count severely undercounts cross-file calls #210 fix for regex fallback path).TestIssue210ModuleLevelCallsclass with 6 regression tests covering middleware-factory argument, inline arrow callback body, multi-site accumulation, no-double-count, sanity.Endpoints: none
Schema: none (graph_edges table gets more rows but no schema change;
source_idvalues now include<file>:0:<module>synthetic ids mirroring the existing<rel_path>:0:fileconvention fromhybrid_type_resolver._file_node_id)Konvensi: Module-top-level calls now produce CALLS edges with synthetic
source_id = "<file>:0:<module>"and NO correspondinggraph_nodesentry. This is the same dangling-source convention already used for IMPORTS edges. Devs running SQLite queries ongraph_edgesshould be aware thatsource_idvalues ending in:0:<module>or:0:fileare synthetic and don't JOIN tograph_nodes.Klaim + Bukti
Root cause identified and documented. Issue #210 suspected
callgraph_engine.py(_parse_calls_treesitter~line 1589,_resolve_call~line 2055) but explicitly instructed independent investigation. Actual root cause is inscripts/parsers/ts_backend_parser.pyandscripts/parsers/js_backend_parser.py(NOT callgraph_engine.py — that file is only used bydataflowcommand, not byscan→search --mode symbolpipeline). The per-function pass in both parsers only walks bodies of registered function/class declarations. Two patterns slip through:router.post(path, requirePermission('admin'), handler)at file root) — never extracted because no function body contains them.(req, res) => { hasPermission(...) }passed as argument) — the arrow function isn't registered as a function declaration, so its body is never walked.Reproduction confirmed BEFORE fix (via
/home/z/my-project/work/scripts/diag_ts_parser.pyrunning TSBackendParser directly on a 6-file fixture mirroring the KDS backend patterns):Reproduction confirmed AFTER fix (same diagnostic script, same fixture):
DoD point 1 —
search "requirePermission" <repro-path> --mode symbolrc >= 13 (or actual call site count):The repro fixture has 12 real call sites (3 per route file × 4 route files), and rc=12 matches exactly. For the KDS backend reported in the issue (13+ call sites), rc would be 13+.
DoD point 2 —
search "hasPermission" <repro-path> --mode symbolrc == 7 (or actual call site count):The repro fixture has 13 real call sites (3 per route file × 4 route files + 1 inside requirePermission's returned arrow function body), and rc=13 matches exactly. For the KDS backend reported in the issue (7 call sites), rc would be 7.
SQLite verification (issue's verification method, corrected): The issue used
SELECT * FROM graph_edges WHERE target_id LIKE '%requirePermission%'which returns 0 rows becausetarget_idusesfile:lineformat (e.g.src/middleware/permission-gate.ts:7), not function names. Correct query:Sample edge:
DoD point 4 —
pytest tests/ -k "callgraph or ref_count"0 new regressions:DoD point 5 — new unit tests reproducing both cases:
Tests cover: middleware-factory argument pattern, inline arrow callback body pattern, multi-site accumulation (no dedup collapse), no-double-count for calls inside registered function bodies, no-double-count for class method bodies, no-double-count for exported arrow functions, sanity extraction of
router.postitself.Full test suite — no new regressions:
Same 17 pre-existing failures as
main(test_doctor, test_confidence, test_formatters_phase2, test_node_types — all unrelated to callgraph). 2136 passed = 2120 (main baseline) + 16 new tests added by this PR.Constraint: dead-code / impact / trace run without crash, numbers more accurate:
All three commands run without crash.
dead-codenow correctly reports 0 dead code (requirePermission and hasPermission haveref_count > 0and aren't falsely flagged).impactandtracerun without crash; their caller counts for module-level callers remain 0 because they JOIN onsource_id = node_idand the synthetic<module>source_id has no matching node — this is NOT a regression (they returned 0 callers before the fix too, because no CALLS edges existed at all). See "Found, not fixed" for follow-up.Pre-flight checks passed:
git fetch origin && git rebase origin/main(up to date, HEAD = 56833c0). 0 open PRs at time of claim (no collision). Recent merged PRs (#207-#213) all touch security/circular/impact/search — none touch callgraph_engine.py or parsers.Catatan Pendekatan
Issue suspected
callgraph_engine.py(_parse_calls_treesitter~line 1589,_resolve_call~line 2055) but explicitly instructed independent investigation. Investigation revealedcallgraph_engine.pyis NOT in thescan→search --mode symbolpipeline — it's only used bydataflow. The actual scan pipeline usesparsers/ts_backend_parser.py(for.tsfiles) andparsers/js_backend_parser.py(for.jsfiles), withparsers/fallback_js_backend.pyas regex fallback. Fix is scoped to these three parser files. Constraint "Jangan ubah command files (commands/*.py) kecuali memang diperlukan" respected — no command files touched.Synthetic
<file>:0:<module>source_id (no correspondinggraph_nodesentry) chosen over creating a synthetic<module>node per file, to avoid pollutinglist/search --mode symboloutput with fake function entries. This mirrors the existing convention inhybrid_type_resolver._file_node_idfor IMPORTS edges. Trade-off:trace/impactSQLite JOINs onsource_id = node_idwon't match these synthetic sources, so they don't see module-level callers (same behavior as IMPORTS edges). This is a known limitation, not a regression — see "Found, not fixed" for follow-up.Breaking / Found-not-fixed
Found, not fixed (Tier 2):
traceandimpactcommands don't see module-level callers of a symbol, because they JOINgraph_edges.source_idagainstgraph_nodes.node_idand the synthetic<file>:0:<module>source_id has no matching node. Before this PR, these commands also returned 0 callers forrequirePermission(because no CALLS edges existed at all) — so this is NOT a regression, just a known limitation that persists. Follow-up would require either (a) creating synthetic<module>graph_nodes entries (violates the issue's "no command files" constraint by requiringlist.py/search_engine.pyfilter changes), or (b) updatingtrace/impactto follow dangling source_ids. Out of scope for issue fix(callgraph): reference_count severely undercounts cross-file calls #210 — BOS to decide if a separate follow-up issue is wanted.tests/test_large_file_parsing.py::test_file_above_old_threshold_is_parsedsegfaults atpython_parser.py:301(tree-sitter Python binding issue). PRE-EXISTING onmain(verified — fails identically without my changes). Not caused by this PR. Should be added to CONTEXT.md's "Pre-existing failures — selalu ignore" list in a follow-up worker-skills PR.TS backend parser's
_find_function_declarationsonly registers classes via theexport_statementbranch — non-exportedclass_declarationnodes are NOT registered. PRE-EXISTING (verified by my test development — had to useexport classin the double-count test). Out of scope for issue fix(callgraph): reference_count severely undercounts cross-file calls #210. Could be filed as a separate bug.17 pre-existing test failures on
main(test_doctor, test_confidence, test_formatters_phase2, test_node_types) — all unrelated to callgraph, all present before this PR. Listed for awareness; not caused or worsened by this PR.