Fix get-or-create records, local-class type queries, builtin typeof and CommonJS top-level this (#1303)

This commit is contained in:
Chris Tate
2026-10-18 13:44:35 -05:00
committed by GitHub
31 changed files with 786 additions and 37 deletions
@@ -6415,7 +6415,7 @@ export function collectInheritedJsFieldWrites(
* hierarchy identifies the original declarations through aliases as well. */
export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.SourceFile[]): void {
const families: [ts.MethodDeclaration, ts.MethodDeclaration][] = [];
collectClassMethodMutations(lowerer, files);
lowerer.speculativeTypeQueries(() => collectClassMethodMutations(lowerer, files));
const visit = (node: ts.Node): void => {
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
const accessors = node.members.filter(
@@ -203,6 +203,7 @@ import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import {
fenceNodeModuleMutation,
isNodeModuleValue,
lowerCjsTopLevelThis,
lowerNodeModuleIdentifier,
lowerNodeModuleProperty,
lowerRequireCacheElement,
@@ -690,6 +691,30 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (!sym) {
return { kind: "strLit", value: "undefined", type: STRING, loc };
}
// `const { inspect } = require("util")` / `import { inspect } from
// "node:util"`: a binding of a builtin module's exported function,
// class or object. JavaScript sources represent such a value without
// its own lowering as an identity token (a string), so the operand's
// representation cannot answer. Node's answer follows the export's
// kind, and the binding read has no effects.
const imported = isJsSourceFile(tested.getSourceFile())
? lowerer.builtinImportOf(tested)
: null;
if (
imported &&
builtinModuleConstOf(lowerer, imported.module, imported.member) === undefined
) {
const t = lowerer.typeOf(tested);
if (
lowerer.checker.getCallSignatures(t).length > 0 ||
lowerer.checker.getConstructSignatures(t).length > 0
) {
return { kind: "strLit", value: "function", type: STRING, loc };
}
if ((t.flags & ts.TypeFlags.Object) !== 0) {
return { kind: "strLit", value: "object", type: STRING, loc };
}
}
if (lowerer.isStdlibSymbol(sym)) {
const t = lowerer.typeOf(expr.expression);
if (
@@ -1661,6 +1686,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// constant. TypeScript keeps the fence (noImplicitThis makes it a
// compile-time story there).
if (isJsSourceFile(expr.getSourceFile())) {
// Module scope of a CommonJS file: `this` is module.exports.
const exportsThis = lowerCjsTopLevelThis(lowerer, expr);
if (exportsThis) return exportsThis;
return { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc };
}
lowerer.unsupported("SC1080", expr);
@@ -2664,6 +2664,17 @@ export function lowerFileInit(
loc: loc0,
});
}
// Top-level `this` of a CommonJS module: its object exists before the
// module's first statement (lowerCjsTopLevelThis).
const exportsThis = lowerer.cjsExportsThis.get(sf);
if (exportsThis) {
header.push({
kind: "assign",
localId: exportsThis.id,
value: { kind: "dynObjLit", fields: [], type: DYN, loc: loc0 },
loc: loc0,
});
}
for (const { stmt, dep } of orderedImportsOf(lowerer.program, sf)) {
const npm = lowerer.npmInitActions.get(sf)?.get(stmt);
if (npm !== undefined && npm.length > 0) {
@@ -8,10 +8,11 @@ import * as win32 from "node:path/win32";
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import type { FileParts } from "./lower-modules.js";
import { isNodeEsmFile, locOf } from "../program.js";
import { isCjsJsFile, isNodeEsmFile, locOf } from "../program.js";
import { runtimePathForTarget } from "../runtime-resolve.js";
import {
BOOL,
DYN,
F64,
type IrExpr,
type IrStmt,
@@ -224,6 +225,135 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean {
return rendered === "ScriptcModule" || rendered === "NodeModule" || rendered === "NodeJS.Module";
}
/** `module.exports` used only for its truthiness (the UMD test `typeof
* module !== "undefined" && module.exports`): the exports object starts as
* a fresh object, so it stays truthy unless the module replaces it with a
* value that can be falsy. True when every `module.exports = value` in the
* file assigns an object or function. */
function moduleExportsAlwaysTruthy(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
if (!truthinessOnly(expr)) return false;
const sf = expr.getSourceFile();
if (isNodeEsmFile(sf, lowerer.program)) return false;
let truthy = true;
ts.walkPreorder(sf, (node) => {
if (
!ts.isBinaryExpression(node) ||
node.operatorToken.kind < ts.SyntaxKind.FirstAssignment ||
node.operatorToken.kind > ts.SyntaxKind.LastAssignment
)
return undefined;
let target = node.left;
while (ts.isParenthesizedExpression(target)) target = target.expression;
if (
!ts.isPropertyAccessExpression(target) ||
target.name.text !== "exports" ||
!isCjsModuleGlobal(lowerer, target.expression)
)
return undefined;
const type = lowerer.typeOf(node.right);
const objectLike = (t: ts.Type): boolean =>
t.isUnionType()
? ts.constituentTypes(t).every(objectLike)
: (t.flags & ts.TypeFlags.Object) !== 0 || (t.flags & ts.TypeFlags.NonPrimitive) !== 0;
if (node.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !objectLike(type)) {
truthy = false;
return "stop";
}
return undefined;
});
return truthy;
}
/** Whether only the truthiness of `node` can be observed: a condition, a
* `!` operand, or a `&&`/`||` operand of an expression that is itself used
* only for its truthiness. */
function truthinessOnly(node: ts.Expression): boolean {
let child: ts.Node = node;
for (let parent = node.parent; ; child = parent, parent = parent.parent) {
if (ts.isParenthesizedExpression(parent)) continue;
if (ts.isPrefixUnaryExpression(parent))
return parent.operator === ts.SyntaxKind.ExclamationToken;
if (
ts.isBinaryExpression(parent) &&
(parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
parent.operatorToken.kind === ts.SyntaxKind.BarBarToken)
)
continue;
if (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent))
return parent.expression === child;
if (ts.isForStatement(parent)) return parent.condition === child;
if (ts.isConditionalExpression(parent)) return parent.condition === child;
return false;
}
}
/** `this` at the top level of a CommonJS module (directly, or inside
* arrow functions there): Node calls the module wrapper with `this` bound
* to the module's original exports object. */
export function isCjsTopLevelThis(lowerer: Lowerer, node: ts.Node): boolean {
if (node.kind !== ts.SyntaxKind.ThisKeyword) return false;
for (let p: ts.Node | undefined = node.parent; p && !ts.isSourceFile(p); p = p.parent) {
if (ts.isArrowFunction(p)) continue;
if (
ts.isFunctionLike(p) ||
ts.isClassDeclaration(p) ||
ts.isClassExpression(p) ||
ts.isClassStaticBlockDeclaration(p)
)
return false;
}
return isCjsJsFile(node.getSourceFile(), lowerer.program);
}
export function usesCjsTopLevelThis(lowerer: Lowerer, sf: ts.SourceFile): boolean {
if (!isCjsJsFile(sf, lowerer.program)) return false;
let found = false;
ts.walkPreorder(sf, (node) => {
if (
node !== sf &&
!ts.isArrowFunction(node) &&
(ts.isFunctionLike(node) ||
ts.isClassDeclaration(node) ||
ts.isClassExpression(node) ||
ts.isClassStaticBlockDeclaration(node))
)
return "skip";
if (node.kind === ts.SyntaxKind.ThisKeyword) {
found = true;
return "stop";
}
return undefined;
});
return found;
}
/** The value of top-level `this` in a CommonJS module: the module's own
* object standing for its original exports, created when the module starts
* (lowerFileInit). Exports themselves are the static export table, so a
* property access through `this` would read or write an object no requirer
* sees; it keeps a named refusal instead. Typeof, truthiness, identity and
* passing the value along (the UMD wrapper `(function (root) { ... })(this)`)
* behave as in Node. */
export function lowerCjsTopLevelThis(lowerer: Lowerer, expr: ts.Expression): IrExpr | null {
if (!isCjsTopLevelThis(lowerer, expr)) return null;
const global = lowerer.cjsExportsThis.get(expr.getSourceFile());
if (!global) return null;
let receiver: ts.Node = expr;
while (ts.isParenthesizedExpression(receiver.parent)) receiver = receiver.parent;
const access = receiver.parent;
if (
(ts.isPropertyAccessExpression(access) || ts.isElementAccessExpression(access)) &&
access.expression === receiver
) {
lowerer.unsupported(
"SC1090",
access,
"property access through top-level 'this' in a CommonJS module (it is module.exports; spell exports.<name>)",
);
}
return varRef(global.id, DYN, locOf(expr));
}
function optionalModule(
lowerer: Lowerer,
node: ts.Expression,
@@ -309,6 +439,8 @@ export function lowerNodeModuleProperty(
if (!direct && !nodeModuleType(lowerer, expr.expression)) return null;
if (expr.questionDotToken) return null;
const member = expr.name.text;
if (member === "exports" && direct && moduleExportsAlwaysTruthy(lowerer, expr))
return boolLit(true, locOf(expr));
if (member === "exports" || member === "require") return null;
let moduleRef = direct
? cjsModuleRef(lowerer, expr.expression)
@@ -4512,16 +4512,21 @@ export function lowerVarDecl(
if (wrapped) return { kind: "assign", localId: g.id, value: wrapped, loc: locOf(decl) };
return null;
}
const raw = lowerVariableInitializer(
lowerer,
decl.initializer,
g.type.kind === "dyn" &&
(isNativeBuiltinValueInitializer(lowerer, decl.initializer) ||
ts.isArrayLiteralExpression(decl.initializer) ||
ts.isObjectLiteralExpression(decl.initializer))
? DYN
: undefined,
);
const guardedRead = isGuardedRecordRead(lowerer, decl)
? lowerAbsenceProbe(lowerer, decl.initializer)
: null;
const raw =
guardedRead ??
lowerVariableInitializer(
lowerer,
decl.initializer,
g.type.kind === "dyn" &&
(isNativeBuiltinValueInitializer(lowerer, decl.initializer) ||
ts.isArrayLiteralExpression(decl.initializer) ||
ts.isObjectLiteralExpression(decl.initializer))
? DYN
: undefined,
);
if (
isJsSourceFile(decl.getSourceFile()) &&
!decl.type &&
@@ -5317,29 +5322,111 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar
}
function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
if (decl.type || !ts.isIdentifier(decl.name) || !decl.initializer) return false;
const guard = immediateAbsenceGuard(lowerer, decl);
// `if (!x)`: the falsy test sees a missing value.
if (guard === "falsy") return true;
// `if (x === undefined)` and its spellings compare the binding with the
// missing value itself, so the read must be able to produce it. Only
// index-signature record reads take this form: array reads already
// carry their undefined arm, and their ordinary lowering keeps its
// bounds proofs.
return guard === "compare" && isIndexSignatureRecordRead(lowerer, decl);
}
/** A file-scope or block-scope `let e = map[k]` over an index-signature
* record whose next statement guards `e` for absence (`if (e ===
* undefined)`, `if (!e)`, ...): the get-or-create idiom. Its read keeps the
* missing key as undefined even without noUncheckedIndexedAccess. */
export function isGuardedRecordRead(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
return immediateAbsenceGuard(lowerer, decl) !== null && isIndexSignatureRecordRead(lowerer, decl);
}
function isIndexSignatureRecordRead(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
if (!decl.initializer) return false;
let init = decl.initializer;
while (ts.isParenthesizedExpression(init)) init = init.expression;
if (!ts.isElementAccessExpression(init)) return false;
// Under noUncheckedIndexedAccess the checker already spells the missing
// arm, and the ordinary read carries it.
const read = lowerer.typeOf(init);
if (
(read.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 ||
(read.isUnionType() &&
ts.constituentTypes(read).some((t) => (t.flags & ts.TypeFlags.Undefined) !== 0))
)
return false;
const receiver = lowerer.mapTypeOf(lowerer.typeOf(init.expression));
const shape = receiver?.kind === "record" ? lowerer.shapes.get(receiver.shapeId) : undefined;
return !!shape && !shape.tuple && !!shape.indexValue && shape.indexValue.kind !== "dyn";
}
/** How the statement right after an unannotated declaration tests the
* declared binding for absence: `if (!x)` ("falsy"), or a comparison with
* the missing value — `x === undefined`, `x == null`, `x !== void 0`,
* `typeof x === "undefined"` and their mirrored and negated spellings
* ("compare"). Null for anything else. */
function immediateAbsenceGuard(
lowerer: Lowerer,
decl: ts.VariableDeclaration,
): "falsy" | "compare" | null {
if (decl.type || !ts.isIdentifier(decl.name) || !decl.initializer) return null;
const statement = decl.parent?.parent;
if (!statement || !ts.isVariableStatement(statement)) return false;
if (!statement || !ts.isVariableStatement(statement)) return null;
const container = statement.parent;
const statements =
ts.isBlock(container) || ts.isSourceFile(container) ? container.statements : null;
if (!statements) return false;
if (!statements) return null;
const index = statements.indexOf(statement);
const next = index >= 0 ? statements[index + 1] : undefined;
if (!next || !ts.isIfStatement(next)) return false;
let condition = next.expression;
while (ts.isParenthesizedExpression(condition)) condition = condition.expression;
if (
!ts.isPrefixUnaryExpression(condition) ||
condition.operator !== ts.SyntaxKind.ExclamationToken
)
return false;
let operand = condition.operand;
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
return (
ts.isIdentifier(operand) &&
lowerer.resolveValueSymbol(operand) === lowerer.resolveValueSymbol(decl.name)
);
if (!next || !ts.isIfStatement(next)) return null;
const binding = lowerer.resolveValueSymbol(decl.name);
const peel = (node: ts.Expression): ts.Expression => {
while (ts.isParenthesizedExpression(node)) node = node.expression;
return node;
};
const isBinding = (node: ts.Expression): boolean => {
const operand = peel(node);
return ts.isIdentifier(operand) && lowerer.resolveValueSymbol(operand) === binding;
};
const condition = peel(next.expression);
if (ts.isPrefixUnaryExpression(condition))
return condition.operator === ts.SyntaxKind.ExclamationToken && isBinding(condition.operand)
? "falsy"
: null;
if (!ts.isBinaryExpression(condition)) return null;
const op = condition.operatorToken.kind;
const strict =
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
const loose =
op === ts.SyntaxKind.EqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsToken;
if (!strict && !loose) return null;
const isUndefined = (node: ts.Expression): boolean => {
const operand = peel(node);
if (ts.isVoidExpression(operand)) return true;
return (
ts.isIdentifier(operand) &&
operand.text === "undefined" &&
(lowerer.typeOf(operand).flags & ts.TypeFlags.Undefined) !== 0
);
};
const isNull = (node: ts.Expression): boolean => peel(node).kind === ts.SyntaxKind.NullKeyword;
const missing = (node: ts.Expression): boolean => isUndefined(node) || (loose && isNull(node));
const isTypeofBinding = (node: ts.Expression): boolean => {
const operand = peel(node);
return ts.isTypeOfExpression(operand) && isBinding(operand.expression);
};
const isUndefinedTag = (node: ts.Expression): boolean => {
const operand = peel(node);
return ts.isStringLiteralLike(operand) && operand.text === "undefined";
};
const { left, right } = condition;
return (isBinding(left) && missing(right)) ||
(missing(left) && isBinding(right)) ||
(isTypeofBinding(left) && isUndefinedTag(right)) ||
(isUndefinedTag(left) && isTypeofBinding(right))
? "compare"
: null;
}
function falsyExitRuntimeOptionalLocals(lowerer: Lowerer, stmt: ts.IfStatement): IrLocal[] {
@@ -271,7 +271,7 @@ import {
appendForkModules,
appendWorkerModules,
} from "./lower-modules.js";
import { prepareCjsModuleGraph } from "./lower-node-module.js";
import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js";
import { registerBuiltinEventTargetClasses } from "./lower-event-target.js";
import {
type ClassInfo,
@@ -533,6 +533,7 @@ import {
lowerForOf,
lowerForStatement,
matchGroupsPatternSource,
isGuardedRecordRead,
} from "./lower-stmts.js";
import {
type FieldTarget,
@@ -1859,6 +1860,9 @@ export class Lowerer {
* carry undefined at runtime. Identifier reads keep the union tag so
* typeof and later boundaries observe the real value. */
readonly runtimeOptionalGlobals = new Set<IrGlobal>();
/** Per CommonJS file using top-level `this`: the global holding the
* object that stands for its original exports (lowerCjsTopLevelThis). */
readonly cjsExportsThis = new Map<ts.SourceFile, IrGlobal>();
/** Bindings whose unchecked string arithmetic can produce either a number
* (NaN for an absent read) or a string. Preserve that result union at bare
* reads instead of applying the checker-bare string narrowing adapter. */
@@ -2868,7 +2872,8 @@ export class Lowerer {
return null;
}
const className = this.classNamer(decl);
if (!this.collectingExprClasses.has(decl)) this.lowerClassExpressionInfo(decl);
if (!this.collectingExprClasses.has(decl) && !this.collectLocalClassForType(decl))
return null;
return { kind: "object", className };
},
mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened),
@@ -3701,6 +3706,16 @@ export class Lowerer {
const id = `%g.${tag}%loaded`;
this.moduleGuardOf.set(fp.sf, id);
this.globalsList.push({ id, name: "%loaded", type: BOOL, mutable: true });
if (usesCjsTopLevelThis(this, fp.sf)) {
const exportsThis: IrGlobal = {
id: `%g.${tag}%exportsThis`,
name: "%exportsThis",
type: DYN,
mutable: true,
};
this.globalsList.push(exportsThis);
this.cjsExportsThis.set(fp.sf, exportsThis);
}
}
// A module is intrinsically async when an await/for-await occurs
@@ -3955,6 +3970,33 @@ export class Lowerer {
}
}
/** A file-scope get-or-create read (`let e = map[k]; if (e ===
* undefined) ...` over an index-signature record) keeps a missing key as
* undefined (isGuardedRecordRead). Its pre-registered global takes the
* undefined arm before any body reads it; the declaration lowers its
* initializer as the matching absence probe. */
analyzeGuardedRecordReadGlobals(parts: FileParts[]): void {
for (const { sf } of parts) {
for (const statement of sf.statements) {
if (!ts.isVariableStatement(statement)) continue;
for (const decl of statement.declarationList.declarations) {
if (!ts.isIdentifier(decl.name) || !isGuardedRecordRead(this, decl)) continue;
const global = this.globalOf(decl.name);
const symbol = this.resolveValueSymbol(decl.name);
if (!global || !symbol || global.tdz || this.runtimeOptionalBindingTypes.has(symbol))
continue;
const widened =
global.type.kind === "union"
? this.withUndefinedArmOf(global.type)
: this.withUndefinedArm(global.type);
if (!widened || typeEquals(widened, global.type)) continue;
global.type = widened;
this.runtimeOptionalBindingTypes.set(symbol, widened);
}
}
}
}
analyzeRuntimeOptionalArrayReads(parts: FileParts[]): void {
const provenance = RuntimeOptionalProvenance.fromEnvironment();
// The site whose value the current promotion follows (debug only).
@@ -6197,8 +6239,11 @@ export class Lowerer {
for (const fp of parts) this.checker.prefetchSourceFile(fp.sf);
}
this.collectProgram(parts);
this.analyzeRuntimeOptionalArrayReads(parts);
this.analyzeDateJsonResults(parts);
this.speculativeTypeQueries(() => {
this.analyzeRuntimeOptionalArrayReads(parts);
this.analyzeDateJsonResults(parts);
this.analyzeGuardedRecordReadGlobals(parts);
});
// Decorated classes analyze AFTER the whole collection pass: a
// decorator's return type may name a subclass declared below the
// class, and reference lowering needs each class's rebindability
@@ -6583,8 +6628,11 @@ export class Lowerer {
collectionEdges.add(name);
};
this.collectProgram(parts);
this.analyzeRuntimeOptionalArrayReads(parts);
this.analyzeDateJsonResults(parts);
this.speculativeTypeQueries(() => {
this.analyzeRuntimeOptionalArrayReads(parts);
this.analyzeDateJsonResults(parts);
this.analyzeGuardedRecordReadGlobals(parts);
});
// Decorated classes analyze post-collection here too: the %init seeds
// lower the decoration calls, whose edges (decorator bodies, construct
// thunks) reachable emit must see.
@@ -9736,6 +9784,49 @@ export class Lowerer {
return lowerClassExpressionInfo(this, expr);
}
/** Depth of the whole-program analyses that query types across every
* file before reachability is known (speculativeTypeQueries). */
private speculativeTypeQueryDepth = 0;
/** Runs a whole-program analysis whose type queries are speculative: they
* visit every file, reached or not, and no statement or declaration owns
* a refusal they trigger (collectLocalClassForType). */
speculativeTypeQueries(analysis: () => void): void {
this.speculativeTypeQueryDepth++;
try {
analysis();
} finally {
this.speculativeTypeQueryDepth--;
}
}
/** Collects a function-local class because a type query named its
* instance type. Collection and body lowering own a refusal of that
* class: the poison reaches the declaration or statement that asked, as
* before. A speculative whole-program analysis owns none, so there the
* class is unmappable for the query and its diagnostics are dropped;
* lowering the class's own declaration collects it again and reports
* them if the program reaches it. */
collectLocalClassForType(decl: ts.ClassLikeDeclaration): boolean {
if (this.speculativeTypeQueryDepth === 0 || this.fnStack.length > 0) {
this.lowerClassExpressionInfo(decl);
return true;
}
const sink: ScrDiagnostic[] = [];
const previousSink = this.diagSink;
this.diagSink = sink;
try {
this.lowerClassExpressionInfo(decl);
} catch (e) {
if (e instanceof PoisonError) return false;
throw e;
} finally {
this.diagSink = previousSink;
}
for (const d of sink) this.pushDiag(d);
return true;
}
lowerClassExpression(expr: ts.ClassLikeDeclaration): IrExpr {
return lowerClassExpression(this, expr);
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6100-record-get-or-create.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6101-record-get-or-create-unchecked/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6102-local-class-type-query.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6103-typeof-builtin-bindings.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6104-typeof-builtin-bindings.mjs"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6105-cjs-top-level-this/lib.js",
"<repo>/tests/corpus/6105-cjs-top-level-this/main.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6106-esm-top-level-this.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/local-class-type-query-brand.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/local-class-type-query.ts"
],
"diags": []
}
+119
View File
@@ -0,0 +1,119 @@
// Get-or-create over nested index-signature records without
// noUncheckedIndexedAccess: a missing key reads as undefined when the next
// statement tests the binding for absence, in every spelling of that test,
// at module scope and inside functions. A write with a computed key through
// a binding that may hold a missing element throws Node's TypeError after
// the key and the value evaluated.
type ExportMap = Record<string, Record<string, string>>;
interface Target {
subpath: string;
condition: string;
target: string;
}
function build(targets: Target[]): { exports: ExportMap; unmapped: string[] } {
const exports: ExportMap = {};
const unmapped: string[] = [];
for (const t of targets) {
if (t.target === "") {
unmapped.push(t.subpath);
continue;
}
let entry = exports[t.subpath];
if (entry === undefined) {
entry = {};
exports[t.subpath] = entry;
}
entry[t.condition] = `./${t.target}`;
entry["types"] = `./${t.target}`;
}
return { exports: exports, unmapped: unmapped };
}
const result = build([
{ subpath: ".", condition: "import", target: "src/index.ts" },
{ subpath: ".", condition: "require", target: "src/index.ts" },
{ subpath: "./x", condition: "default", target: "" },
{ subpath: "./y", condition: "node", target: "src/y.ts" },
]);
console.log(JSON.stringify(result.exports), result.unmapped.join(","));
function group(words: string[]): Record<string, string[]> {
const byInitial: Record<string, string[]> = {};
for (const w of words) {
const k = w.slice(0, 1);
let list = byInitial[k];
if (list == null) {
list = [];
byInitial[k] = list;
}
list.push(w);
}
return byInitial;
}
console.log(JSON.stringify(group(["apple", "avocado", "banana", "blueberry", "cherry"])));
function count(words: string[]): Record<string, number> {
const counts: Record<string, number> = {};
for (const w of words) {
const n = counts[w];
if (typeof n === "undefined") counts[w] = 1;
else counts[w] = n + 1;
}
return counts;
}
console.log(JSON.stringify(count(["a", "b", "a", "c", "a"])));
function lookup(table: Record<string, string>, key: string): string {
const hit = table[key];
if (hit !== undefined) return hit;
return `<${key} missing>`;
}
console.log(lookup({ a: "A" }, "a"), lookup({ a: "A" }, "b"));
function rows(table: Record<string, Record<string, number>>, key: string, col: string): number {
const row = table[key];
if (row != undefined) return row[col] ?? -1;
return -2;
}
console.log(rows({ r: { c: 3 } }, "r", "c"), rows({ r: { c: 3 } }, "r", "d"), rows({}, "x", "c"));
// Module scope.
const nested: Record<string, Record<string, number>> = {};
const key = process.argv.length > 99 ? "unused" : "k";
let slot = nested[key];
if (slot === undefined) {
slot = {};
nested[key] = slot;
}
slot[key] = 1;
slot["fixed"] = 2;
console.log(JSON.stringify(nested));
const names: Record<string, string> = { present: "yes" };
const missing = names[key];
if (!missing) console.log("falsy guard:", missing);
const present = names["present"];
if (present === void 0) console.log("unreachable");
else console.log("present:", present);
// A write with a computed key through a binding that may hold a missing
// array element throws Node's TypeError naming the key, after evaluating
// the key and the value.
function probe(tables: Record<string, string>[]): void {
const firstTable = tables[0];
const lastTable = tables[5];
const pick = (k: string): string => {
console.log("key", k);
return k;
};
firstTable[pick("b")] = pick("second");
console.log(JSON.stringify(firstTable));
try {
lastTable[pick("c")] = pick("value");
} catch (e) {
console.log((e as Error).name, (e as Error).message);
}
}
probe([{ a: "first" }]);
@@ -0,0 +1,70 @@
// Get-or-create over nested index-signature records under
// noUncheckedIndexedAccess: after the presence guard the binding is still
// stored with its undefined arm, and reads, writes and absence probes with
// computed keys go through it (an internal compiler error before).
type ExportMap = Record<string, Record<string, string>>;
function build(rows: { k: string; c: string; v: string }[]): ExportMap {
const map: ExportMap = {};
for (const r of rows) {
let entry = map[r.k];
if (entry === undefined) {
entry = {};
map[r.k] = entry;
}
entry[r.c] = r.v;
}
return map;
}
console.log(
JSON.stringify(
build([
{ k: "a", c: "x", v: "1" },
{ k: "a", c: "y", v: "2" },
{ k: "b", c: "x", v: "3" },
]),
),
);
function touch(map: ExportMap, k: string): string {
let e = map[k];
if (e === undefined) {
e = { [k]: "init" };
map[k] = e;
}
const before = e[k] ?? "none";
e[k] = e[k] + "!";
return before + (k in e ? "+" : "-");
}
const m: ExportMap = {};
console.log(touch(m, "a"), touch(m, "a"), touch(m, "b"), JSON.stringify(m));
function tally(counts: Record<string, Record<string, number>>, group: string, item: string): void {
let row = counts[group];
if (!row) {
row = {};
counts[group] = row;
}
row[item] = (row[item] ?? 0) + 1;
}
const counts: Record<string, Record<string, number>> = {};
for (const [g, i] of [
["fruit", "apple"],
["fruit", "pear"],
["veg", "kale"],
["fruit", "apple"],
]) {
tally(counts, g!, i!);
}
console.log(JSON.stringify(counts));
// Module scope.
const top: ExportMap = {};
const key = process.argv.length > 99 ? "unused" : "k";
let slot = top[key];
if (slot === undefined) {
slot = {};
top[key] = slot;
}
slot[key] = "v";
console.log(JSON.stringify(top), slot[key], slot["other"] ?? "missing");
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
@@ -0,0 +1,42 @@
// A whole-program analysis that queries types before reachability is known
// (the computed-key write census here) can name a function-local class whose
// collection is refused. That query has no owner for the refusal: the class
// stays unmapped for it, and the refusal belongs to the class's own
// declaration, which this program never executes (it used to crash the
// compiler with an uncaught PoisonError).
function main(run) {
if (!run) return "skipped";
const Project = class {
constructor() {
this.name = "p";
}
close() {
if (this.missingFilesMap) {
this.missingFilesMap.clear();
this.missingFilesMap = void 0;
}
}
add(k) {
(this.missingFilesMap || (this.missingFilesMap = new Map())).set(k, 1);
}
};
const holder = { p: new Project() };
const key = String(run);
holder.p[key] = "q";
return holder.p.name;
}
class Counter {
constructor() {
this.counts = {};
}
bump(key) {
this.counts[key] = (this.counts[key] || 0) + 1;
return this.counts[key];
}
}
const counter = new Counter();
const word = process.argv.length > 99 ? "unused" : "word";
counter.bump(word);
console.log(main(0), counter.bump(word));
@@ -0,0 +1,15 @@
// typeof on bindings destructured from Node builtin modules: Node answers by
// the export's kind ("function" for functions and classes, "object" for
// namespace objects), never by how the binding is represented.
const { inspect, format, promisify, deprecate, isDeepStrictEqual } = require("util");
const { join, resolve } = require("path");
const { readFileSync, promises } = require("fs");
const { EventEmitter } = require("events");
const { createHash } = require("crypto");
const util = require("node:util");
const { inspect: aliased } = util;
console.log(typeof inspect, typeof format, typeof promisify, typeof deprecate, typeof isDeepStrictEqual);
console.log(typeof join, typeof resolve, typeof readFileSync, typeof promises);
console.log(typeof EventEmitter, typeof createHash, typeof aliased, typeof util.inspect);
console.log(typeof inspect === "function" ? "callable" : "not callable");
console.log(join("a", "b"), format("%s=%d", "x", 1), inspect({ a: [1, 2] }));
@@ -0,0 +1,11 @@
// typeof on named, default-destructured and namespace-destructured builtin
// bindings in an ES module: Node answers by the export's kind.
import util, { inspect as namedInspect, format } from "node:util";
import * as path from "node:path";
import { createHash } from "node:crypto";
import { promises } from "node:fs";
const { inspect, promisify } = util;
const { join } = path;
console.log(typeof inspect, typeof promisify, typeof namedInspect, typeof format);
console.log(typeof join, typeof createHash, typeof util.inspect, typeof promises);
console.log(namedInspect("s"), format("%d", 2), join("x", "y"));
@@ -0,0 +1,11 @@
// A required CommonJS module sees its own exports object as `this`, and the
// UMD test for a CommonJS host takes the module.exports branch.
const kind = typeof this;
let host = "global";
if (typeof module !== "undefined" && module.exports) host = "commonjs";
(function (root) {
console.log("lib wrapper root:", typeof root, root ? "truthy" : "falsy");
})(this);
exports.describe = function () {
return kind + " " + host;
};
@@ -0,0 +1,12 @@
// Top-level `this` in a CommonJS module is module.exports (an object), also
// inside arrow functions at module scope.
const lib = require("./lib.js");
const self = this;
const arrow = () => this;
console.log(typeof this, self === this, arrow() === this, this ? "truthy" : "falsy");
console.log(lib.describe());
(function () {
console.log("call(this):", typeof this);
}).call(this);
if ((typeof module !== "undefined" && module !== null) && module.exports) console.log("commonjs host");
else console.log("browser host");
@@ -0,0 +1 @@
{ "type": "commonjs" }
+4
View File
@@ -0,0 +1,4 @@
// Top-level `this` in an ES module stays undefined, also inside arrow
// functions at module scope.
const arrow = () => this;
console.log(typeof this, this === undefined, typeof arrow());
@@ -0,0 +1,14 @@
// The same speculative element-type query, reaching a function-local class
// with a private brand through the function's returned array.
function make() {
class Box {
#v = 1;
read(): number {
return this.#v;
}
}
return [new Box()];
}
const xs = make();
const x = xs[0];
console.log(x === undefined ? -1 : x.read());
@@ -0,0 +1,14 @@
// A whole-program analysis queries the element type of `items` before any
// body lowers. The function-local class it names is refused: the refusal is
// reported once, at the class (it used to crash the compiler with an uncaught
// PoisonError).
function make() {
class Box {
static count = 0;
v = 1;
}
return new Box();
}
const items = [make()];
const first = items[0];
console.log(first ? first.v : 0);
@@ -0,0 +1,6 @@
local-class-type-query-brand.ts:4:3 - error SC1090: function-local classes with private brands are not supported yet
3 | function make() {
4 | class Box {
| ^~~~~~~~~~~
5 | #v = 1;
@@ -0,0 +1,6 @@
local-class-type-query.ts:6:3 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet
5 | function make() {
6 | class Box {
| ^~~~~~~~~~~
7 | static count = 0;
+14
View File
@@ -2200,6 +2200,13 @@
"6042-regex-match-object-destructuring.ts": 3393,
"6043-string-index-narrowed-union.ts": 2983,
"605-closures-forward-capture-tdz.ts": 3277,
"6100-record-get-or-create.ts": 3771,
"6101-record-get-or-create-unchecked/main.ts": 2107,
"6102-local-class-type-query.js": 2600,
"6103-typeof-builtin-bindings.cjs": 2202,
"6104-typeof-builtin-bindings.mjs": 2031,
"6105-cjs-top-level-this/main.js": 1371,
"6106-esm-top-level-this.mjs": 1915,
"6140-cjs-require-tdz-throws/main.js": 2731,
"6141-cjs-require-tdz-checked/main.js": 2856,
"6142-cjs-require-published-early/main.js": 3400,
@@ -5222,6 +5229,13 @@
"6042-regex-match-object-destructuring.ts": 3091,
"6043-string-index-narrowed-union.ts": 3627,
"605-closures-forward-capture-tdz.ts": 4638,
"6100-record-get-or-create.ts": 3173,
"6101-record-get-or-create-unchecked/main.ts": 3228,
"6102-local-class-type-query.js": 1967,
"6103-typeof-builtin-bindings.cjs": 1796,
"6104-typeof-builtin-bindings.mjs": 1715,
"6105-cjs-top-level-this/main.js": 3213,
"6106-esm-top-level-this.mjs": 1672,
"6140-cjs-require-tdz-throws/main.js": 2989,
"6141-cjs-require-tdz-checked/main.js": 4062,
"6142-cjs-require-published-early/main.js": 5156,
+2 -2
View File
@@ -73,7 +73,7 @@ const KNOWN: Record<string, Known> = {
"node24-env": { run: "SC1090" },
"node24-esm-metadata": { run: "SC1090" },
"node24-event-emitter": { run: "SC2020" },
"node24-file-url-conversion": { build: "SC1090" },
"node24-file-url-conversion": { run: "SC2020" },
"node24-fs-descriptors": { run: "SC1090" },
"node24-mime": { run: "SC2020" },
"node24-mime-errors": { run: "SC1090" },
@@ -104,7 +104,7 @@ const KNOWN: Record<string, Known> = {
"package-imports": { build: "SC2013" },
"promise-bridge": { build: "SC1090" },
"self-name": { build: "SC2013" },
"stream-shims": { build: "SC1090" },
"stream-shims": { run: "SC1090" },
"util-shims": { run: "SC2020" },
"web-streams": { build: "SC2013" },
"workspace-copied": { build: "SC2013" },