Skip to content

typescript: impact chain gaps from a mutation oracle (JSX hand-over, imported holders, platform receivers, list element types) - #1876

Merged
swapnilpaliwal-sd merged 4 commits into
apps/integration-0.1.9from
fix/ts-impact-loop-2
Oct 10, 2026
Merged

swapnilpaliwal-sd merged 4 commits into
apps/integration-0.1.9from
fix/ts-impact-loop-2

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

TypeScript impact / test selection: chain gaps found by a mutation oracle (a function made to throw on entry; truth = the test files that newly fail), measured on 10 tuning repositories, with 4 more held out and evaluated only at the end. Each fix is a pattern seen in at least two repositories, has a tests/cases/typescript case that fails on the base engine and passes here, and a control that must not change.

1. JSX hands its functions to the element; a class-component tag hands over render and its lifecycle (6453aa5)

  • Symptom: a handler written as onPointerMove={this.handleMove} or onClick={onSave} had no caller, and a class component tag reached only its constructor. So render, componentDidMount and the handlers, plus everything below them, were unreachable from every test that mounts the component.
  • Mechanism: the parser writes each JSX {…} as an expression root linked to no element, and intrinsic tags have no call site at all. The {…} is now the site that hands the function over (callback_registered), and a class-component tag hands over render and its lifecycle methods.
  • Shape population: function-valued JSX roots appear in 9 of 14 repositories; class components are mounted by tag in 4.
  • Effect: on the React application in the set, impact recall 0.49 → 0.81, precision 0.17 → 0.24, empty selections 4 → 2, path 32 → 38/42. The other 9 tuning repositories are byte-identical.

2. A call through an imported const runs the function it holds (b2e7cda)

  • Symptom: export const createDraft = engine.createDraft.bind(engine), and the same with no bind, was followed for a call in its own module. A call from any other module ran nothing, and that includes the package's whole test suite.
  • Mechanism: a call whose callee is an imported const, or a namespace member, now runs what the exporting module's variable holds. The import is followed to that module, never by name (control: the same exported name bound in another module).
  • Effect: recall 0.94 → 0.96, 0.42 → 0.47 and 0.91 → 0.93 on three repositories; path 404 → 410/491. Precision −0.01 to −0.03 on those three.

3. A call on the platform's own objects is no by-name caller (5c2130a)

  • Symptom: with no standard library staged, JSON.parse, Promise.resolve, Object.keys(…).forEach, Reflect.getMetadata, document.createElement and similar calls were untyped calls to impact. Any project method with the same name collected all of them as by-name callers. Across 13 of 14 repositories there are 1–1007 such sites per repository.
  • Mechanism: the engine traces the receiver to the platform's global object, as it already does for a package's value. The site seeds no closure and is listed apart.
  • Effect: by-name seeds on one repository 188 → 34, and its by-name test entries 1454 → 385. File-level selection is unchanged on the oracle, because other routes reach the same files.

4. A list's element is typed through an object-type alias and through a type variable's constraint (21c0f9f)

  • Symptom: plugins.map((p) => p.setup()) gave p nothing to dispatch on in two cases:
    • the element is an object type written as an alias (type Plugin = { setup(): … }). The no-lib callback binding only passed on a type, and an alias of an object type has none, only members. An annotated parameter of the same type did resolve.
    • the list is typed by <Ms extends [Module, ...Module[]]>, whose element had no type at all.
  • Mechanism: the callback parameter now also takes the element's shape. A type variable constrained to an array or tuple yields the constraint's element, for callbacks and for for…of, exactly as <T extends HasId> already does for a receiver.
  • Effect: newly resolved sites in 6 repositories (1–6 each). On the module system built this way, impact recall 0.47 → 0.96, precision 0.39 → 0.41, tests recall 0.42 → 0.96, path 48 → 59/62. The type-variable half resolves nothing on its own; it matters together with the alias half.

Totals, base → this branch

set impact recall (micro / macro) impact precision empty selections path reached
tuning (10 repos, 223 mutants) 0.74 → 0.84 / 0.776 → 0.865 0.21 → 0.22 16 → 14 398 → 421 / 491
held out (4 repos, 96 mutants) 0.74 → 0.74 / 0.894 → 0.894 0.27 → 0.27 1 → 1 163 → 163 / 183

The held-out repositories do not exercise these shapes on their truth chains, so they show neither a gain nor a regression. Files selected for mutants whose truth is empty rise 854 → 1010, all of it on the React application: mounting the root component now reaches its handlers. Engine solve time on the same IR is unchanged within noise (2.2 / 8.7 / 8.8 / 24 / 33 s, base and fix interleaved).

Open, not fixed (each seen in one repository only):

  • a package self-import that resolves to a dev-build stub in dist/;
  • (x as typeof ConstObject).method();
  • functions assigned onto a function object and called through an interface;
  • JS tests reaching TS source through require() namespaces.

Checks: tests/run.py TypeScript 296/296, JavaScript 307/307, Python 306/306, Java 333/333. TypeScript engine suite 101/101 (on the final engine); two goldens gain only the intended new edges, and the oracle files are unchanged.

swapnilpaliwal-sd and others added 4 commits October 9, 2026 08:35
…class component tag hands over render and its lifecycle

A function written inside a JSX `{…}` — `onPointerMove={this.handleMove}`, `onClick={onSave}`,
`renderItem={drawRow}` — was reached by nothing. The parser writes each `{…}` as an expression root
of its own, linked to no element (an intrinsic tag has no call site at all), so no hand-over rule saw
it, and a handler read off a field or named by reference was cut off from every test that renders the
component and fires the event. The `{…}` is now the site that hands the function over, from the
function it is written in (callback_registered, as for `xs.map(cb)`); what the value is read through is
followed exactly as for an argument. A call there hands over what it returns and stays out.

A tag that builds a class component resolved only to its constructor, so render and the lifecycle
methods the renderer calls (componentDidMount, componentWillUnmount, …) had no caller, and nor did
anything below them. The tag now hands them over too, for a class that has a render method.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
A package's public functions are often its default instance's methods exported as consts:
`export const createDraft = immer.createDraft.bind(immer)`, `export const plain = engine.plain`.
The holder rules already knew what such a const holds (a method read as a value, through `bind`),
and a call to it in the defining module ran that method. A call from any other module names the const
through an import, and only a local variable, a parameter or a field was followed — so every caller
outside the defining module, its whole test suite included, ran nothing.

A call whose callee is an imported const, or a namespace import's member (`api.applyPatches(…)`),
now runs what that module's own variable holds. The import is followed to its module, never matched
by name: the same exported name bound in another module runs that module's method.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
… of a project method

With no standard library staged, `JSON.parse(s)`, `Promise.resolve(v).then(f)`,
`Object.keys(o).forEach(g)`, `Reflect.getMetadata(k, t)`, `document.createElement('a')` and
`console.error(e)` resolve to nothing, so every one was an untyped `x.parse()` to impact: a project
method that happens to be named parse, resolve, all, keys, error or getMetadata got every such line in
the repository as a by-name caller, and every test around those lines entered its closure.

The engine now traces the receiver to the platform's global object (an AMBIENT_GLOBAL reference to
JSON, Promise, Object, Reflect, Array, document, console, …), and what calling it returns, the way it
already traces a package's value: the site stays unresolved, is listed apart as a call on a package's
or the platform's value, and seeds no closure. A program that declares one of those names itself binds
it as its own variable and is unaffected; window, globalThis and self are left out, since a program may
hang its own functions on them. An untyped receiver stays a by-name caller.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…raint and an object-type alias

`plugins.map((p) => p.setup(app))` left `p` with nothing to dispatch on in two common shapes:

- the element is an object type written as an alias (`type Plugin = { setup(): … }`): the no-lib
  callback binding gave the parameter the element's TYPE only, and an alias of an object type has no
  type declaration, only members — so every call on it was unresolved, while an annotated parameter of
  the same type resolved through its shape. The callback parameter now takes the element's shape too.
- the list is typed by a type variable constrained to an array or tuple
  (`<Ms extends [Module, ...Module[]]>(...modules: Ms)`): its element had no type at all. The
  constraint is what every element is at least, as it already is for a `<T extends HasId>` receiver;
  the element is now read through it, for callbacks and for `for…of`, by the exact link to the type
  parameter or by its name in scope.

A plugin/module system built this way — a builder that maps over its modules and calls `init` — was
reached by none of the tests that drive it.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
@swapnilpaliwal-sd swapnilpaliwal-sd changed the title typescript: impact chain gaps from a mutation oracle (JSX hand-over, imported holders, platform receivers) typescript: impact chain gaps from a mutation oracle (JSX hand-over, imported holders, platform receivers, list element types) Oct 9, 2026
@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit 1ec53ab into apps/integration-0.1.9 Oct 10, 2026
12 checks passed
@swapnilpaliwal-sd
swapnilpaliwal-sd deleted the fix/ts-impact-loop-2 branch October 10, 2026 03:48
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