feat(parser): capture JS/TS member-assigned function expressions#672
feat(parser): capture JS/TS member-assigned function expressions#672avzamelek wants to merge 3 commits into
Conversation
Prototype- and module-augmentation assignments — `app.handle = function () {}`,
`Router.prototype.dispatch = () => {}`, `exports.render = function () {}` — are
how Express, Koa and many older JS/TS modules define their entire public API,
but they produced no Function node. Only `const x = fn` (variable_declarator)
and class fields were captured, so these members were invisible to callers_of,
impact, dead-code and search.
Add `_extract_js_member_functions`, mirroring `_extract_js_var_functions`: an
`assignment_expression` whose LHS is a `member_expression` and whose RHS is an
arrow/function value becomes a Function node qualified by its full member path
(`file::app.handle`), with a CONTAINS edge and body-call recursion. Computed
access (`obj[key] = fn`) is skipped.
On express (expressjs/express) this adds 89 Function nodes (1771 -> 1860),
surfacing the prototype-augmented public API (`app.use`, `app.handle`,
`app.route`, `req.accepts*`, `res.*`). This improves structural coverage; it
does not by itself move flow-completeness recall, which is gated on JS
member-call resolution.
Tests: TestJsMemberAssignedFunctions in tests/test_parser.py (6 cases; fail
before, pass after).
|
Follow-up added: static same-file JS/TS member calls now resolve to member-assigned function definitions. |
avzamelek
left a comment
There was a problem hiding this comment.
Follow-up added for optional chaining.
app?.handle() is now normalized to the same static member path as app.handle(), so when app.handle = fn exists in the same file, the CALLS edge resolves to file::app.handle.
Computed optional access such as app?.key remains intentionally unresolved.
Added regression coverage; tests/test_parser.py passes (113 passed).
Problem
Prototype- and module-augmentation assignments —
app.handle = function () {},Router.prototype.dispatch = () => {},exports.render = function () {}— are howExpress, Koa and many older JS/TS modules define their entire public API. The parser
only captured
const x = fn(variable_declarator) and class fields, so these membersproduced no Function node at all — invisible to
callers_of, impact, dead-code andsearch. This is part of the README's "JavaScript … flow detection needs work", and
matches the note in
eval/configs/express.yaml("JS modules use prototypes +module.exports heavily, so most methods are not represented").
Change
Add
_extract_js_member_functions, mirroring the existing_extract_js_var_functions:an
assignment_expressionwhose LHS is amember_expressionand whose RHS is anarrow/function value becomes a Function node qualified by its full member path
(
file::app.handle, the same shape asfile::Class.method), with aCONTAINSedge andbody-call recursion. Computed access (
obj[key] = fn) is skipped.Impact
On
expressjs/expressthis adds 89 Function nodes (1771 → 1860), surfacing theprototype-augmented public API (
app.use,app.handle,app.route,app.param,req.accepts*,res.*). Scope is structural coverage only; it does not by itselfmove
flow_completenessrecall, which is gated on JS member-call resolution.app.get/app.postremain uncaptured because express defines them via a computedmethods.forEach(m => app[m] = …)loop, which this handler intentionally skips.Tests
TestJsMemberAssignedFunctionsintests/test_parser.py(6 cases; fail before, passafter). Full suite green (
1965 passed);ruff check code_review_graph/clean.