mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Infer untyped JavaScript collection types once, without re-entering
The inference reads types while it runs: resolving a candidate's
constructor through a destructured require (const { TextDecoder } = util)
asks for the namespace's type, which asked for the inferred collection
types again and restarted the inference until the stack overflowed. The
lowerer now stores an empty table before inferring, so reads during the
inference see no inferred types, and stores the result after.
This commit is contained in:
@@ -75,9 +75,15 @@ export function initializedCollectionType(
|
||||
|
||||
/** The inferred collection type of an identifier that names a proven
|
||||
* binding, or null. Computed for every JavaScript file of the program on
|
||||
* first use. */
|
||||
* first use. The inference itself reads types (a candidate's constructor
|
||||
* through a destructured require, for example); while it runs, those reads
|
||||
* see no inferred types rather than starting it again. */
|
||||
export function inferredCollectionType(lowerer: Lowerer, node: ts.Identifier): ts.Type | null {
|
||||
const types = (lowerer.inferredCollectionTypes ??= inferProgram(lowerer));
|
||||
if (lowerer.inferredCollectionTypes === undefined) {
|
||||
lowerer.inferredCollectionTypes = new Map();
|
||||
lowerer.inferredCollectionTypes = inferProgram(lowerer);
|
||||
}
|
||||
const types = lowerer.inferredCollectionTypes;
|
||||
return types.size === 0 ? null : (types.get(node) ?? null);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
@@ -62,3 +62,30 @@ test("untyped JavaScript collections take the types their uses prove, and only t
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Resolving a candidate's constructor through a destructured require reads
|
||||
// types while the inference runs; those reads must not start it again.
|
||||
test("the inference runs once when a candidate's values come from a destructured require", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-js-collection-reentry-"));
|
||||
try {
|
||||
const entry = join(dir, "main.cjs");
|
||||
writeFileSync(
|
||||
entry,
|
||||
[
|
||||
'const util = require("node:util");',
|
||||
"const { TextDecoder } = util;",
|
||||
"const decoder = new TextDecoder();",
|
||||
'const decoders = new Map([["utf-8", decoder]]);',
|
||||
"const seen = new Set([decoder]);",
|
||||
"console.log(decoders.size, seen.size);",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
const output = join(dir, "main.ir.json");
|
||||
const lowered = await compile(entry, { outputKind: "ir", outDir: dir, outPath: output });
|
||||
if (!lowered.ok) throw new Error(JSON.stringify(lowered.diagnostics));
|
||||
expect(validateModule(deserializeModule(readFileSync(output, "utf8")))).toEqual([]);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user