Skip to content

feat(symbol-table): materialize this.x = fn and object-literal methods as callables #85

Description

@rahlk

Plan

codellm-devkit/.githubdocs/design/roadmap.md — candidate 10 of the 2026-08-05 planning pass.

Problem

Two common ways of declaring a method are never materialized as callables, so they are absent from the symbol table and every call to them is unresolvable — edges are gated to allSignatures (src/semantic_analysis/callGraph.ts:50-55), which is built from the symbol table.

Minimal repro, one fixture per idiom, analyzed at -a 1:

src/klass.js       class Dao   → method getById, save, constructor    ✓ captured
src/klass_ts.ts    class Dao2  → method getById, save, constructor    ✓ captured
src/ctorfn.js      fn Dao      → this.getById / this.save             ✗ missing
src/ctorfn_ts.ts   fn Dao      → this.getById / this.save             ✗ missing
src/objlit.js      module_var api                                     ✗ missing
src/objlit_ts.ts   module_var api2                                    ✗ missing
  1. this.<name> = fn inside a constructor function — lands in the enclosing callable's local_variables, never in inner_callables. Confirmed on app/data/user-dao.js: the module reports 1 function (UserDAO) and 2 inner callables (comparePassword, validateUserDoc, both const x = () => {}), while the six this.getUserById = …-style methods sit in local_variables.
  2. Object-literal methods — the object is emitted as a module variable; getById(){} and save: function(){} inside it are not callables.

This is not a JavaScript problem. Both idioms are missed identically in TypeScript. ES6 classes are captured correctly in .js.

Scope boundary

Fixes symbol-table materialization for the two idioms above. This issue does not:

  • change any schema shape — these become ordinary inner_callables entries in the existing v1 model;
  • touch discovery (done in feat(discovery): analyze .js/.jsx/.mjs/.cjs sources #84), CommonJS require/module.exports module-level modeling, or HTML;
  • attempt general escape analysis or aliasing — only the syntactic forms this.<name> = <function|arrow> within a function body, and method/function-valued properties of an object literal;
  • change the call-graph providers. Edges follow automatically once the targets exist in allSignatures.

Goals

  • this.<name> = function(){} / this.<name> = () => {} inside a function body become inner_callables of that function
  • Object-literal method shorthand { foo(){} } and function-valued properties { foo: function(){} } become callables homed on the owning variable
  • Signatures follow the existing scheme and stay stable (<module>.<owner>.<name>)
  • Fixtures for both idioms, in both .js and .ts, since the gap is language-neutral
  • Existing fixture output unchanged (no signature churn for code that already worked)

Caveats and known risks

  • Recall impact is large, so the diff is not cosmetic. On NodeGoat only 11% of call sites resolve, against 81% for idiomatic TypeScript (sample-app). Expect the emitted call graph to grow substantially once these targets exist, which will change edge counts in any test asserting totals.
  • Signature collisions. Two constructor functions in one module can each declare this.save; the owner segment must disambiguate. Existing signatures must not change — only new ones added.
  • this is not always the enclosing constructor. In an arrow function this is lexical; in a plain function it is dynamic. Restricting to the syntactic form inside a function body is deliberately an over-approximation, and will occasionally home a method on a function that is never used as a constructor. That is preferable to missing it, but should be stated in the code rather than discovered later.
  • Interaction with jelly. Jelly already synthesizes anonymous callables for some of these (88 on NodeGoat). Once tsc materializes them too, the union merge must not double-count — the (source, target) key should coalesce them, but this needs an explicit assertion.
  • Scope is the v1 (0.x) line, where the supported releases are. Forward-porting to main is a separate PR.

Definition of done

  • The six-case probe above yields callables for all six rows, not two.
  • On app/data/user-dao.js specifically, the seven this.* methods appear as callables with stable signatures, and getUserById — which has 6 unresolved call sites today — resolves.
  • Call-site resolution on NodeGoat rises from its current 30/262; the new figure is recorded in the PR rather than asserted as a threshold.
  • Fixtures exist for both idioms in .js and .ts, each failing on the parent commit.
  • sample-app output is byte-identical, proving no signature churn for code that already worked.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or capability

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions