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 (#8264)
* 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.
This commit is contained in:
1 parent
6aeaa04e54
commit
3640f01512
2 files changed
+136
-1
No files matched your search
@@ -0,0 +1,90 @@
|
||||
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");
|
||||
});
|
||||
Reference in new issue
Block a user