Skip to content

fix(resolution): model CommonJS export values, with a pre-model fallback for unmodelled files - #29

Draft
inth3shadows wants to merge 8 commits into
fix/commonjs-missing-memberfrom
fix/commonjs-eval-callee-context
Draft

inth3shadows wants to merge 8 commits into
fix/commonjs-missing-memberfrom
fix/commonjs-eval-callee-context

Conversation

@inth3shadows

@inth3shadows inth3shadows commented Nov 9, 2026 •

Copy link
Copy Markdown
Owner

Warning

DO NOT MERGE (2026-11-11). This tip (f41f795) adds wrong edges that baseline does not have: review rounds 3 and 4 reproduced high-severity cases on accepted shapes (R3-1..R3-4, R4-1..R4-4). The value model is parked while codegraph-qa compares it with option 3B (PR #28 plus removal-only lexical rules). Issues #30-#41 track its known gaps and stay open for that comparison.

Follow-up to #28, targeting its branch. Draft.

What it does

  1. 87576a8: passing eval to a wrapper (wrap(eval)("…")) is no longer mistaken for a grouped direct eval. That mistake let a missing CommonJS member resolve to an unrelated decoy method.
  2. 4064015: adds a binding-aware CommonJS export value model (commonjs-export-evidence.ts, commonjs-value-graph.ts). It answers which declaration an export, or a member of it, really is, and whether that value is callable or constructible. Exported data, absent members and non-callable values therefore stop resolving to same-named functions elsewhere (F14, F15, F16).
  3. 7a39a00: round-12 fallback, superseded by the next commit.
  4. 068c9e2 and f41f795: the model answers only for files it can model completely. Every other file gets commonJs: false and resolves through the pre-model lexical path, exactly as before this PR.
    • Rule 1: every use of module, exports, module.exports or an alias of them must be in a modelled position. Member reads may be anywhere; writes and bare uses only at top level; no delete, no top-level this or return.
    • Rule 2: any unknown reachable from the exports bails the file. The exception is the opaque escapes the model is ruled to block on.
    • Also bails: unparsed source, __esModule with exports.default, and a whole-export forward into an unmodelled file.
    • A member forwarded into an unmodelled file resolves in that file: through its ordinary export lookup, or else through its single top-level declaration of that name that it writes to exports.

Validation on f41f795

  • Full suite: 879 test files passed, 19 skipped; 11,028 tests passed, 2 expected failures, 448 skipped; exit 0.
  • Focused CommonJS set: 13 files, 482 passed, 2 skipped. Each review finding has a regression test that fails on the commit before its fix.
  • QA replay: BROKEN 32 / FIXED 36 / GUESSED 2, 70/70 canaries pass, unchanged.
  • Graph builds: Requests, Click, Mux and Ky identical to the pre-PR build. Express has 64 fewer call edges (wrong targets removed), identical to 4064015.
  • Metamorphic relations: no new differences.
  • 173 review repro cases (rounds 12-14): 99 identical to the pre-PR graph, 70 identical to 4064015's reviewed answers, 4 gains, 0 lost edges.

Known gaps (filed)

Lost edges in modelled files, mostly shared with 4064015: #30 control flow, #31 destructuring/for targets, #32 Rule 2 gaps, #33 Rule 1 gaps, #34 minified __esModule and read-only calls, #35 import reader and memo. Earlier open review items F22-F28 are unchanged. The indexing cost (Express about +38% CPU, from the per-file TS parse) is still open.

Files outside the model keep the pre-PR graph, including its wrong edges. That is the accepted cost of answering only where the model is complete.

No merge, deployment or upstream report is included.

…p patch

The full suite found PR29 dropped static members of exported class
expressions (colbymchenry#2499). The uncommitted fix is now covered by the source
hash manifest and the working-tree backup patch, which reconstructs all
30 pending paths from 87576a8. START_HERE notes that the harness
results predate the fix.
Every harness stage on a build of the fixed checkout matches the pre-fix
candidate run: replay, graph diffs, shadow, modes and metamorphic. The
full suite passes apart from two load-sensitive UI timing tests that
pass in isolation. The Express indexing regression remains open, and its
profiling attribution is noted.
Track binding-owned values and object properties for CommonJS exports.
Reference, callable and constructor evidence are kept separate, so exported
data never becomes a call target and assigned functions resolve to their
implementations. Import resolution carries that intent through named,
default, namespace and forwarded imports, and failed forwarded calls retry
after their sources change.

Class expressions are seeded like class declarations (static members,
prototype and the construction check). This keeps `module.exports = class
{ static f() {} }` resolvable (colbymchenry#2499).

On Express this removes 64 call edges: 52 had wrong targets and 12 pointed
at external-import variables without a proven callable body. The other four
QA corpora are unchanged. Indexing is slower on CommonJS-heavy projects
(Express about +38%) because of per-file TypeScript analysis. That cost is
documented in the handoff and still open.
…17, F18, F19a)

CommonJS export lookups now answer positive, blocked or unknown. Absent,
non-callable and conflicting answers stay blocking. An unknown answer from
code this analysis does not model (constructed or inherited instances,
Object.assign/defineProperty, object spread, unparsable source, closure
writes) may fall back to ordinary matching. The fallback is limited to
functions, methods or classes in the module's evidence files, so it never
lands on a decoy elsewhere or on a storage variable. An opaque call or
dynamic write on a known value still gives no edge. Object.freeze and the
other read-only Object calls no longer count as escapes, and spread
require() calls are recorded as sources.

A hashbang is blanked in place before wrapping. Legacy octal literals,
octal escapes and Flow annotations no longer make the whole file unknown.
IIFEs, UMD factories and callbacks that write module/exports now make the
export unknown instead of a false complete "absent".

The reviewer's F17, F18 and F19a cases recover their baseline edges. Replay,
metamorphic and the five QA corpus graphs are unchanged.
…s (round 13)

Replace the round-12 evidence-file fallback with a file-level bailout. A
file the value model cannot answer for completely gets commonJs:false and
resolves through the pre-model lexical path, exactly as before the model.

- Rule 1: every use of module, exports, module.exports or an alias must be
  in a modelled position (member reads anywhere; writes and bare uses at
  top level only; no delete; no top-level this or return).
- Rule 2: any unknown reachable from the exports, other than the opaque
  escapes the model blocks on, bails the file.
- Also bail on unparsed source, __esModule with exports.default, and a
  whole-export forward into an unmodelled file.
- Model: hashbang (BOM, CR), sloppy-mode diagnostics, Object.freeze-style
  reads, literal-key data defineProperty, hoisted function declarations,
  constructor-function prototypes.
- Restore the pre-model COMMONJS_DEFAULT_EXPORT regex and lexical member
  reader for unmodelled files.
… (round 14)

- R14-S1: the lexical reader used for unmodelled files is now verbatim the
  PR's first commit (87576a8), restoring the wrap(eval) prefix guard.
- R14-6: a member a modelled file forwards into a file outside the model
  resolves in that file: its ordinary export lookup, else its one
  top-level declaration of that name that it writes to exports.
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.

1 participant