Skip to content

fix(js-analyzer): repair JS/TS analyzer inventory, call-graph, schema and functionId bugs#70

Closed
gadievron wants to merge 1 commit into
masterfrom
fix/js-analyzer-repair-js-ts-analyzer-inventory-call
Closed

fix(js-analyzer): repair JS/TS analyzer inventory, call-graph, schema and functionId bugs#70
gadievron wants to merge 1 commit into
masterfrom
fix/js-analyzer-repair-js-ts-analyzer-inventory-call

Conversation

@gadievron

Copy link
Copy Markdown
Collaborator

Fixes a cluster of defects in the JavaScript/TypeScript analysis pipeline,
grouped into five areas plus a local functionId parse fix. All root causes are in
the JS parser files; the c/ruby call_graph_builder.py siblings are correct parity
references and are not edited.

J1 call-edge extraction:

  • extractCallsFromFunction passed funcNode.getKind() to getDescendantsOfKind
    instead of ts.SyntaxKind.CallExpression, so every out-edge list was empty.

J2 AST inventory gaps (extractFunctionsFromFile now visits these shapes):

  • module.exports = class {} (ClassExpression)
  • anon export default function, Foo.prototype.bar=fn, this.method=fn,
    Object.assign(proto, ...)
  • this.x=function(){} assigned in a constructor body
  • class get/set accessors were dropped because ts-morph getMethods() excludes
    GetAccessor/SetAccessor. Now also iterate getGetAccessors()+getSetAccessors() at
    all three class-member sites (inventory builder, callGraph builder, and the
    single-member lookup), so get x()/set x(v) emit as Class.x functions WITH
    callGraph companions.
  • Object.defineProperty(X.prototype,"n",{value:fn})
  • bare top-level CallExpression modules
  • HOC const-initializers (memo/forwardRef/styled)
  • barrel re-export object-literal property values

J3 Pattern-A companion:

  • buildCallGraphForFile now visits the same 5 emit paths as
    extractFunctionsFromFile so callGraph keeps pace with functions.

J4 call-edge content:

  • walk getArguments() to record callback identifiers
  • normalize callee name to an identifier (JS-native)
  • bucket unresolved/dynamic calls into indirect_calls

J5 schema and framework classification:

  • emit snake_case reverse_call_graph, repository, and per-function parameters
  • route-handler coverage now spans Koa/Fastify on BOTH axes -- the param-shape
    classifier (_hasRouteHandlerSignature) and the route-REGISTRATION walker.
    Added the route verb to EXPRESS_VERBS and the fastify/koa receiver stems
    to EXPRESS_RECEIVER_STEMS / _isPlausibleExpressReceiver (additive; existing
    Express detection unchanged) so fastify.get('/x', handler) and koa.get(...)
    synthesise route_handler entry-point units. Also fixed the React
    (request,response) false-positive.

functionId parse:

  • unit_generator.js and test_pipeline.py split the ":"
    id on the last colon, mangling multi-colon Express ids. Switched to first-colon
    split to match the dependency_resolver contract. The id format is unchanged
    (backward-compatible). The dependency_resolver.js half is deferred.

Tests: RED-first tests under tests/parsers/javascript/ drive the Node analyzer /
UnitGenerator on inline fixtures, including class get/set accessors and
Fastify/Koa route registration. Suite: 245 passed, 22 skipped, 0 failed. ruff
clean; node --check clean.

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

… and functionId bugs

Fixes a cluster of defects in the JavaScript/TypeScript analysis pipeline,
grouped into five areas plus a local functionId parse fix. All root causes are in
the JS parser files; the c/ruby call_graph_builder.py siblings are correct parity
references and are not edited.

J1 call-edge extraction:
- extractCallsFromFunction passed funcNode.getKind() to getDescendantsOfKind
  instead of ts.SyntaxKind.CallExpression, so every out-edge list was empty.

J2 AST inventory gaps (extractFunctionsFromFile now visits these shapes):
- module.exports = class {} (ClassExpression)
- anon export default function, Foo.prototype.bar=fn, this.method=fn,
  Object.assign(proto, ...)
- this.x=function(){} assigned in a constructor body
- class get/set accessors were dropped because ts-morph getMethods() excludes
  GetAccessor/SetAccessor. Now also iterate getGetAccessors()+getSetAccessors() at
  all three class-member sites (inventory builder, callGraph builder, and the
  single-member lookup), so `get x()`/`set x(v)` emit as Class.x functions WITH
  callGraph companions.
- Object.defineProperty(X.prototype,"n",{value:fn})
- bare top-level CallExpression modules
- HOC const-initializers (memo/forwardRef/styled)
- barrel re-export object-literal property values

J3 Pattern-A companion:
- buildCallGraphForFile now visits the same 5 emit paths as
  extractFunctionsFromFile so callGraph keeps pace with functions.

J4 call-edge content:
- walk getArguments() to record callback identifiers
- normalize callee name to an identifier (JS-native)
- bucket unresolved/dynamic calls into indirect_calls

J5 schema and framework classification:
- emit snake_case reverse_call_graph, repository, and per-function parameters
- route-handler coverage now spans Koa/Fastify on BOTH axes -- the param-shape
  classifier (_hasRouteHandlerSignature) and the route-REGISTRATION walker.
  Added the `route` verb to EXPRESS_VERBS and the `fastify`/`koa` receiver stems
  to EXPRESS_RECEIVER_STEMS / _isPlausibleExpressReceiver (additive; existing
  Express detection unchanged) so `fastify.get('/x', handler)` and `koa.get(...)`
  synthesise route_handler entry-point units. Also fixed the React
  (request,response) false-positive.

functionId parse:
- unit_generator.js and test_pipeline.py split the "<filePath>:<functionName>"
  id on the last colon, mangling multi-colon Express ids. Switched to first-colon
  split to match the dependency_resolver contract. The id format is unchanged
  (backward-compatible). The dependency_resolver.js half is deferred.

Tests: RED-first tests under tests/parsers/javascript/ drive the Node analyzer /
UnitGenerator on inline fixtures, including class get/set accessors and
Fastify/Koa route registration. Suite: 245 passed, 22 skipped, 0 failed. ruff
clean; node --check clean.

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

Copy link
Copy Markdown
Contributor

Thanks for this — meaty, well-targeted, and the test coverage is genuinely strong: one fixture per AST shape, all gated on Node/npm availability so CI stays green cross-platform.

I verified the three contract claims against the consumers on master, and they all hold:

  • unit_type (snake_case) is what entry_point_detector.py keys off — the camelCase-only output really did mean no JS route was ever treated as an entry point.
  • funcId.split(':')[0] is the dependency_resolver.js contract, so the first-colon switch is the correct fix.
  • call_graph / reverse_call_graph / repository are what the C/Python/Ruby/Zig/Go unit_generator.py siblings consume — JS was the odd one out.

The headline catch — getDescendantsOfKind(funcNode.getKind()) returning an empty call graph for every function — is a good one.

No blockers; I'd take this as-is. Two non-blocking precision/recall trade-offs for your call — flagged because reachability gates the paid LLM stages, so graph noise translates to cost (traced through test_pipeline.pyReachabilityAnalyzer.get_all_reachable()).


🟡 Medium — precision → cost · extractCallsFromFunction (callback-argument loop)

Every bare Identifier argument is recorded as a call edge. That catches addEventListener('click', handler) / setTimeout(cb) as intended, but it can't tell a callback from data — logger.info(config), render(props), dispatch(action) all emit an edge to config / props / action. Combined with the unique-name-across-repo resolution in _buildResolvedGraphs, a coincidental name match becomes a resolved edge, so that callee's whole subtree is pulled into reachable_units and sent to the paid Stage-1/2 LLM.

Safe direction (over-approximation only adds noise/$, never drops a real vuln), so non-blocking — but it does tax the cost-reduction goal. Worth considering: only record an identifier arg as an edge when it resolves to a known function id (the byName / byFile indexes already exist in _buildResolvedGraphs).

🟡 Medium — entry-point recall · _hasRouteHandlerSignature (dropped (request, response) pattern)

Dropping the bare (request, response) signature is the right call for the React false-positive, but it's a recall trade in the unsafe direction: a plain-JS Express handler written function h(request, response){…} that isn't registered via app.get(...) and has no TS type annotations will no longer classify as a route_handler — and since an undetected entry point takes its whole subtree out of reachability, it can silently drop from analysis.

Blast radius is small in practice (the route-registration walker plus the Python EntryPointDetector input patterns like req.body / request.GET catch most cases), so non-blocking — just flagging the direction so it's a conscious trade. The in-code rationale comment is helpful.


Low (no action needed):

  • Unique-name cross-repo resolution in _buildResolvedGraphs can manufacture an edge when a call to an external/library function coincidentally matches a single same-named repo function. Conservative (only resolves when exactly one candidate), so low rate — same over-approximation bucket as Medium Configure Renovate #1.
  • getDescendantsOfKind(CallExpression) attributes calls from nested/inner functions to the outer function. Standard call-graph imprecision; broadens reachability slightly.
  • HOC wrappers (memo(() => {…})) get parameters: []. Harmless unless a future check reads params for HOC components.

Nice work overall — the contract-citing comments throughout made this fast to verify.

@gadievron

Copy link
Copy Markdown
Collaborator Author

Closing as already resolved on current master. This fix's behavior shipped via the parser-fix-stack release (#133/#134); confirmed by independent reproduction — the original bug (reconstructed from its bug-pipeline investigation) no longer manifests on master, not merely by line-presence. Part of a full validated sweep of the open-PR corpus.

@gadievron gadievron closed this Jul 10, 2026
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