mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
* fix(javascript): emit no promise for a literal non-callable executor
Node's `Promise` constructor calls the executor synchronously and throws a
`TypeError` when it is not callable, before any promise object exists. The
analysis reported a `constructor` promise operation with a hardcoded
`sourceResolution: "complete"`, which the graph projects as
`creates-promise` `resolved`.
Measured on Node 24.22.1:
new Promise() -> THREW TypeError
new Promise(1) / (-1) -> THREW TypeError
new Promise("work") / (`work`) -> THREW TypeError
new Promise(true) / (!0) -> THREW TypeError
new Promise(null) / (void 1) -> THREW TypeError
new Promise(1n) / (/re/) -> THREW TypeError
new Promise([]) / ({}) -> THREW TypeError
--- accepted ---
new Promise(() => {}) -> OK Promise
new Promise(async () => {}) -> OK Promise
new Promise(function () {}) -> OK Promise
new Promise(class {}) -> OK Promise
A class passes Node's own `typeof` check, so it keeps its operation. An
identifier, a call result or a spread may still supply a function and also keeps
its operation.
The filter gates the candidate push rather than `promiseCandidateDetails`, which
also decides whether an `await` becomes an `awaited-expression`: gating there
made `await new Promise(1)` emit a spurious task fact, whereas gating at the push
site makes it emit nothing, which is the honest answer.
* chore: retrigger title and readiness checks after the title was corrected
* fix(javascript): reject a non-finite executor, not only a literal
greptile caught that `new Promise(1e309)` still emitted a promise. Babel stores
that literal as `Infinity`, and `readExactJavaScriptLiteral` refuses to price a
non-finite number, so the guard missed it.
Measured on Node 24.22.1 — every one throws before a promise exists:
new Promise(1e309) -> THREW TypeError
new Promise(1e400) -> THREW TypeError
new Promise(Infinity) -> THREW TypeError
new Promise(-Infinity) -> THREW TypeError
new Promise(NaN) -> THREW TypeError
A numeric literal is now read directly rather than through the exact-value
reader. `Infinity` and `NaN` are identifiers rather than literals, so they are
decided through `isUnmodifiedGlobalReference`; a shadowed binding keeps its
promise, which is the same scoping rule the timer fix uses.
91 lines
3.4 KiB
TypeScript
91 lines
3.4 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
|
|
|
/**
|
|
* Node's `Promise` constructor calls the executor synchronously and throws a
|
|
* `TypeError` when it is not callable — before any promise object exists. A
|
|
* provable literal that cannot be a function, or a missing argument, is
|
|
* therefore not a promise construction.
|
|
*
|
|
* Measured on Node 24.22.1:
|
|
*
|
|
* new Promise() / (1) / ("work") / (true) / (null) / (void 0) -> THREW TypeError
|
|
* new Promise(1n) / (/re/) / ([]) / ({}) -> THREW TypeError
|
|
* new Promise(() => {}) / (function () {}) / (class {}) -> OK Promise
|
|
*/
|
|
const promisesFor = (source: string) =>
|
|
analyzeJavaScriptSemantics(source).promiseOperations.map(
|
|
({ kind, method, sourceResolution }) => ({
|
|
kind,
|
|
method,
|
|
sourceResolution,
|
|
}),
|
|
);
|
|
|
|
it.each([
|
|
["no argument", `new Promise()`],
|
|
["a numeric executor", `new Promise(1)`],
|
|
["a negative numeric executor", `new Promise(-1)`],
|
|
["a string executor", `new Promise("work")`],
|
|
["a template-literal executor", "new Promise(`work`)"],
|
|
["a boolean executor", `new Promise(true)`],
|
|
["a negated boolean executor", `new Promise(!0)`],
|
|
["a null executor", `new Promise(null)`],
|
|
["a void executor", `new Promise(void 1)`],
|
|
["a BigInt executor", `new Promise(1n)`],
|
|
["a RegExp executor", `new Promise(/re/)`],
|
|
// Babel stores an overflowing numeric literal as `Infinity`, which is not
|
|
// callable either.
|
|
["an overflowing numeric executor", `new Promise(1e309)`],
|
|
["an Infinity executor", `new Promise(Infinity)`],
|
|
["a NaN executor", `new Promise(NaN)`],
|
|
["an array executor", `new Promise([])`],
|
|
["an object executor", `new Promise({})`],
|
|
] as const)("emits no promise for %s", (_name, source) => {
|
|
expect(promisesFor(source + ";")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
["an arrow executor", `new Promise(() => {})`],
|
|
["an async arrow executor", `new Promise(async () => {})`],
|
|
["a function expression executor", `new Promise(function () {})`],
|
|
["a class expression executor", `new Promise(class {})`],
|
|
] as const)("keeps the promise for %s", (_name, source) => {
|
|
expect(promisesFor(source + ";")).toHaveLength(1);
|
|
});
|
|
|
|
it("keeps a promise whose executor is an identifier", () => {
|
|
expect(promisesFor(`const p = new Promise(run);`)).toEqual([
|
|
{ kind: "constructor", method: "new", sourceResolution: "complete" },
|
|
]);
|
|
});
|
|
|
|
it("keeps a promise whose executor is a call result", () => {
|
|
expect(promisesFor(`const p = new Promise(makeExecutor());`)).toHaveLength(1);
|
|
});
|
|
|
|
it("keeps a promise whose executor is a spread", () => {
|
|
expect(promisesFor(`const p = new Promise(...list);`)).toHaveLength(1);
|
|
});
|
|
|
|
/**
|
|
* Filtering happens at the candidate-push site rather than inside
|
|
* `promiseCandidateDetails`, which also decides whether `await` becomes an
|
|
* `awaited-expression`. A rejected executor must produce neither fact.
|
|
*/
|
|
it("emits no awaited-expression task for an awaited rejected executor", () => {
|
|
expect(promisesFor(`async function f() { await new Promise(1); }`)).toEqual(
|
|
[],
|
|
);
|
|
});
|
|
|
|
it("keeps the awaited ownership for a callable executor under await", () => {
|
|
const operations = analyzeJavaScriptSemantics(
|
|
`async function f() { await new Promise(() => {}); }`,
|
|
).promiseOperations;
|
|
expect(operations).toHaveLength(1);
|
|
expect(operations[0]?.kind).toBe("constructor");
|
|
expect(operations[0]?.ownership).toBe("awaited");
|
|
});
|