Skip to content

feat(parser): capture JS/TS member-assigned function expressions#672

Open
avzamelek wants to merge 3 commits into
tirth8205:mainfrom
avzamelek:feat/js-member-assigned-functions
Open

feat(parser): capture JS/TS member-assigned function expressions#672
avzamelek wants to merge 3 commits into
tirth8205:mainfrom
avzamelek:feat/js-member-assigned-functions

Conversation

@avzamelek

Copy link
Copy Markdown

Problem

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. The parser
only captured const x = fn (variable_declarator) and class fields, so these members
produced no Function node at all — invisible to callers_of, impact, dead-code and
search. 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_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, the same shape as file::Class.method), with a CONTAINS edge and
body-call recursion. Computed access (obj[key] = fn) is skipped.

Impact

On expressjs/express this adds 89 Function nodes (1771 → 1860), surfacing the
prototype-augmented public API (app.use, app.handle, app.route, app.param,
req.accepts*, res.*). Scope is structural coverage only; it does not by itself
move flow_completeness recall, which is gated on JS member-call resolution. app.get/
app.post remain uncaptured because express defines them via a computed
methods.forEach(m => app[m] = …) loop, which this handler intentionally skips.

Tests

TestJsMemberAssignedFunctions in tests/test_parser.py (6 cases; fail before, pass
after). Full suite green (1965 passed); ruff check code_review_graph/ clean.

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).
@avzamelek
avzamelek marked this pull request as draft July 19, 2026 19:02
@avzamelek
avzamelek marked this pull request as ready for review July 20, 2026 08:03
@avzamelek
avzamelek marked this pull request as draft July 20, 2026 08:13
@avzamelek

Copy link
Copy Markdown
Author

Follow-up added: static same-file JS/TS member calls now resolve to member-assigned function definitions.
For example, after app.handle = function () {}, a later app.handle() now creates a CALLS edge to file::app.handle instead of the bare handle target.
The resolver keeps the existing fallback for unresolved external calls such as thirdParty.handle().
Added regression coverage; tests/test_parser.py passes (112 passed).
Dynamic/computed assignments (app[m] = fn) and alias/destructuring resolution remain intentionally out of scope for this PR.

@avzamelek
avzamelek marked this pull request as ready for review July 20, 2026 08:25

@avzamelek avzamelek left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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).

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.

2 participants