diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts new file mode 100644 index 0000000000..d72792ae7c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -0,0 +1,430 @@ +/* A CommonJS module's own exports object, seen from inside the module. + * + * The compiled module system has no reflective exports object: each export + * is a module global (collectGlobals) or alias plumbing to a declaration. + * Inside the module that is enough for + * + * - member reads and member calls (`exports.f`, `exports.f(x)`, + * `module.exports.f(x)`): they are the member's export global, the + * storage every `exports.f =` statement assigns; + * - identity comparisons of the object itself (`root === module.exports`, + * `exports === module.exports`): while the module never replaces + * `module.exports` and never rebinds `exports`, both name the module's + * original exports object, which top-level `this` already represents + * (lowerCjsTopLevelThis). `typeof` and truthiness keep their existing + * lowerings. + * + * Every other use of the object as a value (passing it to a function, + * storing it, reading a member through an alias) would need a real object + * whose properties are the exports; those uses keep a named refusal. The + * dynamic CommonJS module object is the two-tier design's S9 stage. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + cjsExportAssignmentOf, + cjsExportTargetLiteral, + isCjsJsFile, + isModuleExportsAccess, + isRequireStatement, + locOf, +} from "../program.js"; +import { isCjsModuleGlobal } from "./lower-node-module.js"; +import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js"; + +/** True for the CommonJS wrapper's `exports` binding (not shadowed by any + * source declaration) in a CommonJS JS file. */ +export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): boolean { + if (!ts.isIdentifier(node) || node.text !== "exports") return false; + if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false; + const symbol = lowerer.checker.getSymbolAtLocation(node); + const decls = symbol ? lowerer.checker.declarationsOf(symbol) : []; + return !decls.some( + (d) => + !d.getSourceFile().isDeclarationFile && + (ts.isVariableDeclaration(d) || + ts.isParameter(d) || + ts.isBindingElement(d) || + ts.isFunctionDeclaration(d) || + ts.isClassDeclaration(d) || + ts.isImportSpecifier(d) || + ts.isNamespaceImport(d) || + ts.isImportEqualsDeclaration(d)), + ); +} + +/** `module.exports` over the CommonJS `module` global in a CommonJS file. */ +export function isCjsModuleExports(lowerer: Lowerer, node: ts.Expression): boolean { + return ( + isModuleExportsAccess(node) && + isCjsModuleGlobal(lowerer, node.expression) && + isCjsJsFile(node.getSourceFile(), lowerer.program) + ); +} + +/** The export global behind a same-module member access `exports.` / + * `module.exports.`, or null (identifier-valued members are alias + * plumbing with no storage, and other receivers are not the exports + * object). */ +export function cjsInFileExportGlobalOf( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrGlobal | null { + if (access.questionDotToken || !ts.isIdentifier(access.name)) return null; + const sf = access.getSourceFile(); + const recv = + isModuleExportsAccess(access.expression) || + (ts.isIdentifier(access.expression) && + access.expression.text === "exports" && + !lowerer.resolveLocal(access.expression) && + !lowerer.globalOf(access.expression)); + if (!recv || !isCjsJsFile(sf, lowerer.program)) return null; + const sym = + lowerer.checker.getSymbolAtLocation(access.name) ?? + lowerer.cjsModuleExportSymbol(sf, access.name.text); + const resolved = + sym !== undefined && sym.flags & ts.SymbolFlags.Alias + ? lowerer.checker.getAliasedSymbol(sym) + : sym; + return ( + (sym ? lowerer.globalsBySymbol.get(sym) : undefined) ?? + (resolved ? lowerer.globalsBySymbol.get(resolved) : undefined) ?? + null + ); +} + +/** Whether `node` is an operand of an equality operator: only its identity + * is observed. */ +function identityUse(node: ts.Expression): boolean { + let child: ts.Node = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined || !ts.isBinaryExpression(parent)) return false; + const op = parent.operatorToken.kind; + return ( + (op === ts.SyntaxKind.EqualsEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsEqualsToken || + op === ts.SyntaxKind.EqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsToken) && + (parent.left === child || parent.right === child) + ); +} + +/** Uses whose answer does not need the object: `typeof`, `void`, and + * truthiness tests (their existing lowerings answer them). */ +function answeredWithoutObject(node: ts.Expression): boolean { + let child: ts.Node = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isTypeOfExpression(parent) || ts.isVoidExpression(parent)) return true; + return truthinessOnly(child as ts.Expression); +} + +function truthinessOnly(node: ts.Expression): boolean { + let child: ts.Node = node; + for (let parent = node.parent; parent !== undefined; 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; + } + return false; +} + +/** Which of the module's exports bindings the file ever assigns (any + * nesting, compound operators included), cached per file on the lowerer + * (Lowerer.cjsExportsAssignments). */ +function exportsAssignments( + lowerer: Lowerer, + sf: ts.SourceFile, +): { exports: boolean; moduleExports: boolean } { + const cached = lowerer.cjsExportsAssignments.get(sf); + if (cached !== undefined) return cached; + const out = { exports: false, moduleExports: false }; + lowerer.cjsExportsAssignments.set(sf, out); + ts.walkPreorder(sf, (node) => { + // A fresh `exports = module.exports = {}` reset leaves both bindings + // naming one (new, empty) exports object for the rest of the module. + if (ts.isExpressionStatement(node) && isFreshExportsReset(lowerer, node)) return "skip"; + 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 (isCjsModuleExports(lowerer, target)) out.moduleExports = true; + if (isCjsExportsIdentifier(lowerer, target)) out.exports = true; + return undefined; + }); + return out; +} + +/** Whether `node` still names the module's ORIGINAL exports object wherever + * it runs: `exports` unless the file rebinds it; `module.exports` unless the + * file rebinds `exports` or replaces `module.exports`. */ +function namesOriginalExports(lowerer: Lowerer, node: ts.Expression): boolean { + const assigned = exportsAssignments(lowerer, node.getSourceFile()); + if (assigned.exports) return false; + return ts.isIdentifier(node) || !assigned.moduleExports; +} + +/** The uses this module answers with the original exports object: identity + * comparisons and `typeof` of either spelling, and truthiness or `void` of + * `exports` (`module.exports` truthiness keeps its existing fold). */ +function objectAnsweredUse(node: ts.Expression): boolean { + if (identityUse(node)) return true; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) parent = parent.parent; + if (parent !== undefined && ts.isTypeOfExpression(parent)) return true; + return ts.isIdentifier(node) && answeredWithoutObject(node); +} + +/** Whether `node` (`exports` or `module.exports`) is a use of the module's + * exports object as a VALUE (not a member access receiver, not an + * assignment target). */ +export function isCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): boolean { + if (!isCjsExportsIdentifier(lowerer, node) && !isCjsModuleExports(lowerer, node)) return false; + let outer: ts.Node = node; + while (outer.parent !== undefined && ts.isParenthesizedExpression(outer.parent)) + outer = outer.parent; + const parent = outer.parent; + if (parent === undefined) return false; + if ( + (ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) && + parent.expression === outer + ) + return false; + if ( + ts.isBinaryExpression(parent) && + parent.left === outer && + parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + return false; + return true; +} + +/** Files whose exports object is observed by identity somewhere: their top- + * level `this` object must exist (lowerFileInit creates it). */ +export function usesCjsExportsIdentity(lowerer: Lowerer, sf: ts.SourceFile): boolean { + if (!isCjsJsFile(sf, lowerer.program)) return false; + let found = false; + ts.walkPreorder(sf, (node) => { + if ( + (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) && + isCjsExportsObjectValue(lowerer, node) && + objectAnsweredUse(node) && + namesOriginalExports(lowerer, node) + ) { + found = true; + return "stop"; + } + return undefined; + }); + return found; +} + +/** `exports` / `module.exports` used as a value inside its own module: the + * original exports object for identity, typeof and truthiness, or a named + * refusal. Null when `node` is not such a use. */ +export function lowerCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): IrExpr | null { + if (!isCjsExportsObjectValue(lowerer, node)) return null; + const sf = node.getSourceFile(); + // A module that replaces module.exports answers `module.exports` reads + // through the replacement's own machinery (the class-expression whole + // export, the replaced-exports reads). + if (!ts.isIdentifier(node) && exportsAssignments(lowerer, sf).moduleExports) return null; + const spelled = ts.isIdentifier(node) ? "exports" : "module.exports"; + const original = lowerer.cjsExportsThis.get(sf); + if (objectAnsweredUse(node) && original !== undefined && namesOriginalExports(lowerer, node)) { + return { kind: "varRef", localId: original.id, type: DYN, loc: locOf(node) }; + } + // `module.exports` truthiness keeps its existing fold (and anything the + // object is not needed for keeps whatever answered it before). + if (answeredWithoutObject(node)) return null; + lowerer.unsupported( + "SC1090", + node, + identityUse(node) + ? `comparing '${spelled}' in a module that replaces module.exports or rebinds exports (the compiled exports are a static table, not an object with an identity)` + : `'${spelled}' as a value other than a member access, comparison, typeof or truthiness test (the compiled exports are a static table, not an object: read ${spelled}.)`, + ); +} + +/** `exports = module.exports = {}` (either order) as a top-level statement + * that runs before anything could observe or extend the original exports + * object: only directives, require statements and function declarations + * precede it, and the module assigns neither binding anywhere else. Node + * swaps the original empty exports object for another empty one that both + * bindings name; the compiled module's static exports start out exactly + * the same, so the statement is a no-op (semver's internal/re.js). */ +export function isFreshExportsReset(lowerer: Lowerer, stmt: ts.ExpressionStatement): boolean { + const sf = stmt.parent; + if (!ts.isSourceFile(sf) || !isCjsJsFile(sf, lowerer.program)) return false; + let e: ts.Expression = stmt.expression; + const targets: ts.Expression[] = []; + for (;;) { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break; + targets.push(e.left); + e = e.right; + } + if (!ts.isObjectLiteralExpression(e) || e.properties.length !== 0) return false; + if (targets.length !== 2) return false; + const hasExports = targets.some((t) => isCjsExportsIdentifier(lowerer, t)); + const hasModuleExports = targets.some((t) => isCjsModuleExports(lowerer, t)); + if (!hasExports || !hasModuleExports) return false; + for (const prior of sf.statements) { + if (prior === stmt) break; + if (ts.isFunctionDeclaration(prior) || isRequireStatement(prior)) continue; + if (ts.isExpressionStatement(prior) && ts.isStringLiteral(prior.expression)) continue; + return false; + } + let others = 0; + 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 (isCjsModuleExports(lowerer, target) || isCjsExportsIdentifier(lowerer, target)) others++; + return undefined; + }); + return others === 2; +} + +/** The export global a WRITE `exports. = v` / `module.exports. = + * v` updates when it is not a top-level export statement (inside a + * function, or in value position): only while the module keeps its + * original exports object, which is then the object every requirer reads + * the member from. */ +export function cjsExportMemberWriteGlobalOf( + lowerer: Lowerer, + target: ts.PropertyAccessExpression, +): IrGlobal | null { + const sf = target.getSourceFile(); + if (!isCjsJsFile(sf, lowerer.program)) return null; + const assigned = exportsAssignments(lowerer, sf); + if (assigned.exports || assigned.moduleExports) return null; + return cjsInFileExportGlobalOf(lowerer, target); +} + +/** Whether a function body reads `this` (arrow functions inherit it; other + * nested functions bind their own). */ +function functionUsesThis(fn: ts.FunctionLikeDeclaration): boolean { + if (fn.body === undefined) return false; + let found = false; + ts.walkPreorder(fn.body, (node) => { + if (node !== fn.body && ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return "skip"; + if (node.kind === ts.SyntaxKind.ThisKeyword) { + found = true; + return "stop"; + } + return undefined; + }); + return found; +} + +/** A function declaration, or a variable initialized with a function or + * arrow expression, that never reads `this`. */ +function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): boolean { + const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const fn = decls.find(ts.isFunctionDeclaration); + if (fn !== undefined) return !functionUsesThis(fn); + const v = decls.find(ts.isVariableDeclaration); + let init = v?.initializer; + while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression; + return ( + init !== undefined && + (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) && + !functionUsesThis(init) + ); +} + +/** True when every value the module assigns to its export `name` is a + * function that never reads `this`. A call through the exports object + * (`exports.f(x)`) binds `this` to that object, which the static export + * table cannot supply, so only these calls lower; the others keep a fence. */ +export function cjsExportCallIgnoresThis( + lowerer: Lowerer, + sf: ts.SourceFile, + name: string, + depth = 0, +): boolean { + if (depth > 16) return false; + const values: ts.Expression[] = []; + ts.walkPreorder(sf, (node) => { + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(node.left) && + node.left.name.text === name && + (isCjsExportsIdentifier(lowerer, node.left.expression) || + isCjsModuleExports(lowerer, node.left.expression)) + ) { + values.push(node.right); + } + return undefined; + }); + // Entries of a `module.exports = { ... }` table (direct, or a const + // literal the statement names). + let methodEntry: ts.MethodDeclaration | null = null; + let shorthandOnly = false; + for (const stmt of sf.statements) { + const cjs = cjsExportAssignmentOf(stmt); + if (cjs?.kind !== "table") continue; + const table = cjs.obj ?? cjsExportTargetLiteral(cjs.expr.right, sf); + for (const prop of table?.properties ?? []) { + if (prop.name === undefined || !ts.isIdentifier(prop.name) || prop.name.text !== name) + continue; + if (ts.isPropertyAssignment(prop)) values.push(prop.initializer); + else if (ts.isShorthandPropertyAssignment(prop)) { + const value = lowerer.checker.getShorthandAssignmentValueSymbol(prop); + if (!symbolIsThisFreeFunction(lowerer, value)) return false; + shorthandOnly = true; + } else if (ts.isMethodDeclaration(prop)) methodEntry = prop; + } + } + if (methodEntry !== null) return values.length === 0 && !functionUsesThis(methodEntry); + if (values.length === 0) return shorthandOnly; + return values.every((v) => { + let e = v; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return !functionUsesThis(e); + if (ts.isIdentifier(e)) { + return symbolIsThisFreeFunction(lowerer, lowerer.checker.getSymbolAtLocation(e)); + } + // Another module's member (`exports.f = require("./lib").f`, or through + // a module binding): that module's own export values decide. + if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.name)) { + const dep = ts.isIdentifier(e.expression) + ? lowerer.cjsLocalModuleBindingOf(e.expression) + ? lowerer.cjsModuleBindingDepOf(e.expression) + : null + : lowerer.cjsModuleRefDepOf(e.expression); + return dep !== null && cjsExportCallIgnoresThis(lowerer, dep, e.name.text, depth + 1); + } + return false; + }); +} diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts new file mode 100644 index 0000000000..7f31337f5b --- /dev/null +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -0,0 +1,370 @@ +/* Inline CommonJS `require("./x")` of a program module in expression + * position. A top-level `const m = require("./x")` binding is alias + * plumbing: member reads resolve to the exporter's declarations, and the + * statement lowers to the dependency's run-once %init call. An inline + * require is the same thing without the binding: the %init call runs at + * exactly the call's position (Node evaluates the module there, or + * answers from its cache), and the value is then read statically: + * + * - `require("./x").f` and `require("./x").f(...)` over a module whose + * exports are a table (member exports or `module.exports = {...}`) + * read the member like a binding would; + * - `require("./x")` as a value over a module whose whole export is one + * value (`module.exports = fn` / `= class {}` / `= make()` / `= 42` / + * `= require("./y")`) answers that value. + * + * A table module's exports object as a value has no static + * representation; it keeps a named refusal (a first-class CommonJS module + * object is the two-tier design's S9 stage). */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + canonicalBuiltinModule, + cjsExportAssignmentOf, + cjsExportDiscardReason, + cjsExportTargetLiteral, + isCjsJsFile, + isJsSourceFile, + isModuleExportsAccess, + locOf, + npmStaticDepSf7, + requireSpecOf, + resolveImport, +} from "../program.js"; +import { createRequireCalleeFileOf } from "./builtins/module-bindings.js"; +import { isCjsRequireGlobal } from "./lower-node-module.js"; +import { DYN, type IrExpr } from "../../ir/ir.js"; +import { nodeThrowExpr } from "../../ir/build.js"; +import { probeNodeRequireRefusal } from "../npm.js"; + +export interface InlineRequire { + call: ts.CallExpression; + spec: string; + dep: ts.SourceFile; +} + +/** A `require("")` call of the CommonJS global in a CommonJS JS + * file whose target is a compiled program module (relative, aliased, or an + * opted-in --npm-static package). Builtins, JSON documents and + * createRequire bindings answer null: their own lowerings own them. */ +export function inlineProgramRequireOf( + lowerer: Lowerer, + expr: ts.Expression, +): InlineRequire | null { + let e = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression)) return null; + const spec = requireSpecOf(e); + if (spec === null || canonicalBuiltinModule(spec) !== null) return null; + const sf = e.getSourceFile(); + if (!isCjsJsFile(sf, lowerer.program)) return null; + if (!isCjsRequireGlobal(lowerer, e.expression)) return null; + if (createRequireCalleeFileOf(lowerer, e.expression) !== null) return null; + const dep = + resolveImport(lowerer.program, sf, spec, "require") ?? + npmStaticDepSf7(lowerer.program, sf, spec, "require"); + if (!dep || dep.isDeclarationFile || dep.fileName.endsWith(".json")) return null; + return { call: e, spec, dep }; +} + +/** The kept (last, not discarded) top-level `module.exports = ` of a + * CommonJS module whose whole export is a single value, or null when its + * exports are a table: member exports only, or a literal / const-literal / + * Proxy-over-literal table. */ +export function cjsWholeExportValueOf( + sf: ts.SourceFile, +): { assign: ts.BinaryExpression; rhs: ts.Expression } | null { + let kept: ts.BinaryExpression | null = null; + for (const stmt of sf.statements) { + const cjs = cjsExportAssignmentOf(stmt); + if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) kept = cjs.expr; + } + if (kept === null) return null; + let rhs: ts.Expression = kept.right; + while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + if (ts.isObjectLiteralExpression(rhs) || cjsExportTargetLiteral(rhs, sf) !== null) return null; + return { assign: kept, rhs }; +} + +/** True when `dep`'s exports, through `module.exports = require("./y")` + * re-export chains, are a table that member reads resolve against. */ +export function cjsNamespaceModule(lowerer: Lowerer, dep: ts.SourceFile, depth = 0): boolean { + const whole = cjsWholeExportValueOf(dep); + if (whole === null) return true; + const inner = inlineProgramRequireOf(lowerer, whole.rhs); + return inner !== null && depth < 32 && cjsNamespaceModule(lowerer, inner.dep, depth + 1); +} + +/** An inline program-module require over a table module (member reads and + * member calls resolve through it), or null. */ +export function inlineNamespaceRequireOf( + lowerer: Lowerer, + expr: ts.Expression, +): InlineRequire | null { + const req = inlineProgramRequireOf(lowerer, expr); + return req !== null && cjsNamespaceModule(lowerer, req.dep) ? req : null; +} + +/** `value` evaluated after the required module's run-once %init (and its + * module-registry link) at the require's own position. */ +export function withRequireInit(lowerer: Lowerer, req: InlineRequire, value: IrExpr): IrExpr { + if (lowerer.inlineRequireCycleCalls.has(req.call)) { + lowerer.unsupported( + "SC1090", + req.call, + `require() of '${req.spec}' while it is still evaluating (a require cycle: Node answers its partial exports, which the static export table does not model)`, + ); + } + // Every inline program require is a preflight module edge, so its module + // has an initializer; never read its exports without evaluating it. + if (!lowerer.initNameOf.has(req.dep)) { + lowerer.unsupported( + "SC1090", + req.call, + `require() of '${req.spec}' here (the module is not part of the compiled module order)`, + ); + } + const init = lowerer.requireInitStmt(req.spec, req.call); + if (init === null) return value; + return { kind: "seqExpr", stmts: [init], result: value, type: value.type, loc: value.loc }; +} + +/** The whole-export value of `dep` read from the requirer, following + * `module.exports = require("./y")` re-exports (the inner module already + * evaluated inside dep's own %init). Null for table modules and for + * whole-export shapes with no represented value. */ +export function wholeExportValue( + lowerer: Lowerer, + dep: ts.SourceFile, + blame: ts.Node, + depth = 0, +): IrExpr | null { + const whole = cjsWholeExportValueOf(dep); + if (whole === null || depth > 32) return null; + const inner = inlineProgramRequireOf(lowerer, whole.rhs); + if (inner !== null) return wholeExportValue(lowerer, inner.dep, blame, depth + 1); + // An identifier names a declaration of the exporting module (or a + // builtin global): reading it IS the export, exactly what requirer + // bindings alias to. + if (ts.isIdentifier(whole.rhs)) return lowerer.lowerExpr(whole.rhs); + if (ts.isClassExpression(whole.rhs)) { + return lowerer.classValueRef(lowerer.lowerClassExpressionInfo(whole.rhs), blame); + } + const g = lowerer.globalsByDeclNode.get(whole.assign); + if (g === undefined) return null; + return { kind: "varRef", localId: g.id, type: g.type, loc: locOf(blame) }; +} + +/** `require("./x")` as a value (`require("debug")("ns")`, `new + * (require("./C"))()`, `{ verify: require("./verify") }`): the module's + * %init at this position, then its whole-export value. Null when `call` + * is not an inline program-module require. */ +export function lowerInlineRequireValue(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const req = inlineProgramRequireOf(lowerer, call); + if (req === null) return null; + const value = wholeExportValue(lowerer, req.dep, call, 0); + if (value === null) { + lowerer.unsupported( + "SC1090", + call, + cjsNamespaceModule(lowerer, req.dep) + ? `the exports object of '${req.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(req.spec)}).member directly, or bind const m = require(${JSON.stringify(req.spec)}) at the module's top level)` + : `the whole export of '${req.spec}' as a value (its 'module.exports =' value has no static representation)`, + ); + } + return withRequireInit(lowerer, req, value); +} + +/** Lowers `inner` (a member read or member call whose receiver is the + * inline require) with that require marked as handled, so the receiver + * resolves as a module binding, then sequences the module's %init first. */ +export function lowerWithInlineRequireInit( + lowerer: Lowerer, + req: InlineRequire, + inner: () => IrExpr, +): IrExpr { + lowerer.inlineRequireInitDone.add(req.call); + try { + return withRequireInit(lowerer, req, inner()); + } finally { + lowerer.inlineRequireInitDone.delete(req.call); + } +} + +/** A `require(...)` call of the CommonJS global in a CommonJS file that no + * other lowering claimed (not a builtin, JSON document, program module or + * createRequire binding): a package nothing installed resolves throws + * Node's catchable MODULE_NOT_FOUND at the call, and every other form is + * refused by name instead of as an unlowerable `require` value. */ +export function lowerUncompiledRequireCall( + lowerer: Lowerer, + call: ts.CallExpression, +): IrExpr | null { + if (!ts.isIdentifier(call.expression) || call.expression.text !== "require") return null; + const sf = call.getSourceFile(); + if (!isCjsJsFile(sf, lowerer.program) || !isCjsRequireGlobal(lowerer, call.expression)) + return null; + if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null; + const spec = requireSpecOf(call); + if (spec === null) { + lowerer.unsupported( + "SC1090", + call, + 'require() with a computed specifier (the compiled module graph is fixed at build time: require("") lowers)', + ); + } + if (canonicalBuiltinModule(spec) !== null) return null; + if (!spec.startsWith(".") && !spec.startsWith("/") && !spec.startsWith("#")) { + const refusal = probeNodeRequireRefusal(sf.fileName, spec); + if (refusal !== null) { + return nodeThrowExpr(0, "MODULE_NOT_FOUND", refusal.message, DYN, locOf(call)); + } + lowerer.unsupported( + "SC1090", + call, + `require() of the package '${spec}' in an expression (the package is not compiled into this program)`, + ); + } + lowerer.unsupported( + "SC1090", + call, + `require() of '${spec}' in an expression (it does not resolve to a compiled program module)`, + ); +} + +/** The elements of a top-level `const { key: { ... } } = require("./x")` + * declaration over a program module whose nested pattern destructures the + * named export's value (preflight admits exactly these). Plain elements + * stay alias plumbing. */ +export function requireNestedPatternsOf(decl: ts.VariableDeclaration): ts.BindingElement[] { + if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return []; + const spec = requireSpecOf(decl.initializer); + if (spec === null || canonicalBuiltinModule(spec) !== null) return []; + if (!isJsSourceFile(decl.getSourceFile())) return []; + return decl.name.elements.filter( + (e) => + e.name !== undefined && + ts.isObjectBindingPattern(e.name) && + e.propertyName !== undefined && + ts.isIdentifier(e.propertyName) && + e.initializer === undefined && + e.dotDotDotToken === undefined, + ); +} + +/** A plain method entry of a `module.exports = { ... }` table: identifier + * name, a body, not a generator. Its value is the method's function. */ +export function cjsTableMethodEntry( + prop: ts.ObjectLiteralElementLike, +): (ts.MethodDeclaration & { name: ts.Identifier; body: ts.Block }) | null { + if (!ts.isMethodDeclaration(prop) || !ts.isIdentifier(prop.name) || prop.body === undefined) + return null; + if (prop.asteriskToken !== undefined) return null; + return prop as ts.MethodDeclaration & { name: ts.Identifier; body: ts.Block }; +} + +/** `exports. = value` / `module.exports. = value` assignments in + * VALUE position inside a top-level statement (`const src = exports.src = + * []`), outside function bodies and outside the statement's own export + * chain (`statementExport`, which the statement-level machinery owns). */ +export function nestedCjsExportAssignmentsOf( + stmt: ts.Statement, + statementExport: ts.BinaryExpression | undefined, +): { name: ts.Identifier; value: ts.Expression }[] { + if (!ts.isVariableStatement(stmt) && !ts.isExpressionStatement(stmt)) return []; + const owned = new Set(); + for (let e: ts.Expression | undefined = statementExport; e !== undefined;) { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break; + owned.add(e); + e = e.right; + } + const out: { name: ts.Identifier; value: ts.Expression }[] = []; + ts.walkPreorder(stmt, (node) => { + if ( + node !== stmt && + (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node)) + ) + return "skip"; + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + !owned.has(node) && + ts.isPropertyAccessExpression(node.left) && + node.left.questionDotToken === undefined && + ts.isIdentifier(node.left.name) && + ((ts.isIdentifier(node.left.expression) && node.left.expression.text === "exports") || + isModuleExportsAccess(node.left.expression)) + ) { + out.push({ name: node.left.name, value: node.right }); + } + return undefined; + }); + return out; +} + +/** `const { A, B: b } = ns` at a module's top level, where `ns` is a + * CommonJS module binding over a table module (picomatch's `const { + * MAX_LENGTH, ... } = constants`): each name takes a snapshot of the named + * export's value at this statement, exactly like destructuring the exports + * object would. Null for every other declaration; the plain elements and + * their key names otherwise. */ +export function cjsNamespaceDestructureOf( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): { dep: ts.SourceFile; elements: { name: ts.Identifier; key: string }[] } | null { + if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return null; + const stmt = decl.parent?.parent; + if (stmt === undefined || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) + return null; + if (!isJsSourceFile(stmt.parent)) return null; + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (!ts.isIdentifier(init) || !lowerer.cjsLocalModuleBindingOf(init)) return null; + const dep = lowerer.cjsModuleBindingDepOf(init); + if (dep === null) return null; + const elements: { name: ts.Identifier; key: string }[] = []; + for (const e of decl.name.elements) { + if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return null; + if (e.name === undefined || !ts.isIdentifier(e.name)) return null; + const key = + e.propertyName === undefined + ? e.name.text + : ts.isIdentifier(e.propertyName) || ts.isStringLiteral(e.propertyName) + ? e.propertyName.text + : null; + if (key === null) return null; + elements.push({ name: e.name, key }); + } + return { dep, elements }; +} + +/** The value of `dep`'s export `key` read from another module: its export + * global, or the function/class declaration it aliases. Null when it has no + * static value. */ +export function cjsExportValueByName( + lowerer: Lowerer, + dep: ts.SourceFile, + key: string, + blame: ts.Node, +): IrExpr | null { + let sym = lowerer.cjsModuleExportSymbol(dep, key); + if (sym === undefined) return null; + if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); + const direct = lowerer.globalsBySymbol.get(sym); + if (direct) return { kind: "varRef", localId: direct.id, type: direct.type, loc: locOf(blame) }; + for (const d of lowerer.checker.declarationsOf(sym)) { + const byDecl = lowerer.globalsByDeclNode.get(d); + if (byDecl) return { kind: "varRef", localId: byDecl.id, type: byDecl.type, loc: locOf(blame) }; + // Alias plumbing: a shorthand/identifier table entry or a member + // export naming a declaration. Reading that identifier IS the value. + if (ts.isShorthandPropertyAssignment(d)) return lowerer.lowerShorthandValue(d); + if (ts.isPropertyAssignment(d) && ts.isIdentifier(d.initializer)) { + return lowerer.lowerExpr(d.initializer); + } + if ((ts.isFunctionDeclaration(d) || ts.isClassDeclaration(d)) && d.name !== undefined) { + return lowerer.lowerExpr(d.name); + } + } + return null; +} diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts new file mode 100644 index 0000000000..f7955fc239 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts @@ -0,0 +1,223 @@ +/* tsc's CommonJS import interop: `const x_1 = __importDefault(require("./m"))` + * and `const x_1 = __importStar(require("./m"))` at a module's top level, + * with the helpers tsc emits at the top of the file: + * + * var __importDefault = (this && this.__importDefault) || function (mod) { + * return (mod && mod.__esModule) ? mod : { "default": mod }; + * }; + * + * For a CommonJS dependency the helper's answer is fixed by the + * dependency's source: stamped with `__esModule` (tsc output), the binding + * IS the module's exports; otherwise `x_1.default` is the module's exports + * (and __importStar also copies every export onto x_1). Both are the + * static alias model a plain `const x_1 = require("./m")` already has, so + * the binding lowers as a require binding: the statement is the module's + * %init, `x_1.f` / `x_1.default.f` read the exports statically, and + * `x_1.default` over a single-value module is that value. The helper + * declarations themselves are dead once every call is such a binding (the + * self-probe `this.__importDefault` reads the pristine exports object, + * which never has the name), so they lower to nothing. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + isCjsJsFile, + isEsModuleStamp, + npmStaticDepSf7, + requireSpecOf, + resolveImport, +} from "../program.js"; +import { cjsNamespaceModule } from "./cjs-inline-require.js"; + +const HELPERS = new Set([ + "__importDefault", + "__importStar", + "__createBinding", + "__setModuleDefault", +]); + +export interface TscInteropBinding { + helper: "__importDefault" | "__importStar"; + spec: string; + dep: ts.SourceFile; + /** The dependency stamps `__esModule`: the binding IS its exports. */ + stamped: boolean; +} + +/** `var = (this && this.) || ` at a module's top + * level: the tsc helper declaration shape. */ +function tscHelperDeclName(stmt: ts.Statement): string | null { + if (!ts.isVariableStatement(stmt)) return null; + const decls = stmt.declarationList.declarations; + if (decls.length !== 1) return null; + const d = decls[0]!; + if (!ts.isIdentifier(d.name) || !HELPERS.has(d.name.text) || d.initializer === undefined) + return null; + let init: ts.Expression = d.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (!ts.isBinaryExpression(init) || init.operatorToken.kind !== ts.SyntaxKind.BarBarToken) + return null; + let probe: ts.Expression = init.left; + while (ts.isParenthesizedExpression(probe)) probe = probe.expression; + if ( + !ts.isBinaryExpression(probe) || + probe.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken || + probe.left.kind !== ts.SyntaxKind.ThisKeyword || + !ts.isPropertyAccessExpression(probe.right) || + probe.right.expression.kind !== ts.SyntaxKind.ThisKeyword || + probe.right.name.text !== d.name.text + ) + return null; + // The fallback is the helper body: a function, or (newer tsc output) a + // conditional or IIFE producing one. It never runs when the helper is dead. + return d.name.text; +} + +/** True when the dependency stamps itself as an ES module translation. */ +function stampsEsModule(dep: ts.SourceFile): boolean { + return dep.statements.some((s) => { + if (!ts.isExpressionStatement(s)) return false; + if (isEsModuleStamp(s.expression)) return true; + const e = s.expression; + return ( + ts.isBinaryExpression(e) && + e.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(e.left) && + ts.isIdentifier(e.left.expression) && + e.left.expression.text === "exports" && + e.left.name.text === "__esModule" && + e.right.kind === ts.SyntaxKind.TrueKeyword + ); + }); +} + +/** The interop binding a top-level `const x = __importDefault(require(s))` / + * `__importStar(require(s))` declaration makes, or null. The helper must be + * the module's own tsc helper declaration and the dependency a compiled + * CommonJS program module. */ +export function tscInteropBindingOfDecl( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): TscInteropBinding | null { + if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null; + const stmt = decl.parent?.parent; + if (stmt === undefined || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) + return null; + const sf = stmt.parent; + if (!isCjsJsFile(sf, lowerer.program)) return null; + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (!ts.isCallExpression(init) || init.arguments.length !== 1) return null; + const callee = init.expression; + if (!ts.isIdentifier(callee)) return null; + if (callee.text !== "__importDefault" && callee.text !== "__importStar") return null; + if (!sf.statements.some((s) => tscHelperDeclName(s) === callee.text)) return null; + const spec = requireSpecOf(init.arguments[0]!); + if (spec === null) return null; + const dep = + resolveImport(lowerer.program, sf, spec, "require") ?? + npmStaticDepSf7(lowerer.program, sf, spec, "require"); + if (!dep || dep.isDeclarationFile || !isCjsJsFile(dep, lowerer.program)) return null; + return { + helper: callee.text as TscInteropBinding["helper"], + spec, + dep, + stamped: stampsEsModule(dep), + }; +} + +/** The interop binding an identifier reads, or null. */ +export function tscInteropBindingOf( + lowerer: Lowerer, + expr: ts.Expression, +): TscInteropBinding | null { + if (!ts.isIdentifier(expr)) return null; + const sym = lowerer.checker.getSymbolAtLocation(expr); + const decl = sym ? lowerer.checker.declarationsOf(sym).find(ts.isVariableDeclaration) : undefined; + return decl ? tscInteropBindingOfDecl(lowerer, decl) : null; +} + +/** The module whose exports `expr` denotes through an interop binding: + * the binding itself when the dependency is stamped (or for __importStar, + * whose copy carries every export), and `binding.default` when it is not. + * Null for anything else. */ +export function tscInteropModuleOf( + lowerer: Lowerer, + expr: ts.Expression, +): TscInteropBinding | null { + let e = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + const direct = tscInteropBindingOf(lowerer, e); + if (direct !== null) { + return direct.stamped || direct.helper === "__importStar" ? direct : null; + } + if (ts.isPropertyAccessExpression(e) && !e.questionDotToken && e.name.text === "default") { + const via = tscInteropBindingOf(lowerer, e.expression); + if (via !== null && !via.stamped) return via; + } + return null; +} + +/** `binding.default` over an unstamped single-value dependency: that value + * (the caller lowers the dependency's whole export). */ +export function tscInteropDefaultValueOf( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): TscInteropBinding | null { + if (expr.questionDotToken || expr.name.text !== "default") return null; + const via = tscInteropBindingOf(lowerer, expr.expression); + if (via === null || via.stamped) return null; + return cjsNamespaceModule(lowerer, via.dep) ? null : via; +} + +/** True for a tsc helper declaration every reference to which is a + * recognized interop binding's callee, its own self-probe, or a reference + * from another such dead helper: the statement lowers to nothing. */ +export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boolean { + const name = tscHelperDeclName(stmt); + if (name === null) return false; + const sf = stmt.getSourceFile(); + const helperSpans = new Map(); + for (const s of sf.statements) { + const n = tscHelperDeclName(s); + if (n !== null) helperSpans.set(n, s); + } + const dead = new Set(); + for (let changed = true; changed;) { + changed = false; + for (const [helper, decl] of helperSpans) { + if (dead.has(helper)) continue; + let live = false; + ts.walkPreorder(sf, (node) => { + if (!ts.isIdentifier(node) || node.text !== helper) return undefined; + const parent = node.parent; + if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node) + return undefined; + const at = node.getStart(sf); + if (at >= decl.getStart(sf) && at < decl.getEnd()) return undefined; + for (const [other, otherDecl] of helperSpans) { + if (other !== helper && dead.has(other)) { + if (at >= otherDecl.getStart(sf) && at < otherDecl.getEnd()) return undefined; + } + } + const call = parent; + const binding = call?.parent; + if ( + call !== undefined && + ts.isCallExpression(call) && + call.expression === node && + binding !== undefined && + ts.isVariableDeclaration(binding) && + tscInteropBindingOfDecl(lowerer, binding) !== null + ) + return undefined; + live = true; + return "stop"; + }); + if (!live) { + dead.add(helper); + changed = true; + } + } + } + return dead.has(name); +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2037ba17c1..213e605bcb 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -24,6 +24,13 @@ import { lowerZlibStreamMethodCall, } from "./builtins/compression.js"; import type { Lowerer } from "./lowerer.js"; +import { + inlineNamespaceRequireOf, + lowerInlineRequireValue, + lowerUncompiledRequireCall, + lowerWithInlineRequireInit, +} from "./cjs-inline-require.js"; +import { cjsExportCallIgnoresThis, cjsInFileExportGlobalOf } from "./cjs-exports-object.js"; import { lowerGenMethodCall } from "./lower-generators.js"; import { lowerClassSymbolMethodCall } from "./symbol-methods.js"; import { errorToStringCall } from "./error-methods.js"; @@ -234,6 +241,32 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const json = lowerJsonRequireCall(lowerer, expr); if (json) return json; + // Inline require() of a program module (cjs-inline-require.ts): a + // member call over its export table resolves like a binding's member + // call after the module's %init at this position; the require itself + // as a value answers the module's whole-export value. + if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) { + const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression); + if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) { + // Node calls the member with `this` bound to the module's exports. + if ( + ts.isIdentifier(expr.expression.name) && + !cjsExportCallIgnoresThis(lowerer, req.dep, expr.expression.name.text) + ) { + lowerer.unsupported( + "SC1090", + expr, + `calling '${expr.expression.name.text}' through require(${JSON.stringify(req.spec)}) when the function may read 'this' (Node binds it to the module's exports object, which compiles to a static table — bind the module and call a local function instead)`, + ); + } + return lowerWithInlineRequireInit(lowerer, req, () => lowerCall(lowerer, expr)); + } + } + { + const inlineValue = lowerInlineRequireValue(lowerer, expr); + if (inlineValue) return inlineValue; + } + // A call whose chain ROOTS at an ambient-undefined name (`declare // const value: Y | undefined; value?.foo("a")`, `declare function // chain...; chain(o).mapValues(f).value()`, a trap binding's read): @@ -310,6 +343,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerImportMetaResolveCall(lowerer, expr) ?? lowerRequireResolveCall(lowerer, expr); if (resolved) return resolved; } + // Any other require() call of the CommonJS global: MODULE_NOT_FOUND for + // packages nothing installed resolves, a named refusal otherwise. + { + const uncompiled = lowerUncompiledRequireCall(lowerer, expr); + if (uncompiled) return uncompiled; + } // `process.getuid?.()` — intercepted BEFORE the optional-chain // machinery (the member always exists on a POSIX target, so the @@ -1274,6 +1313,32 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const args = completeFuncValueArgs(lowerer, expr, callee.type, loc); return { kind: "callValue", callee, args, type: callee.type.ret, loc }; } + // `exports.f(x)` / `module.exports.f(x)` inside the module: a call of + // the member's export global (cjs-exports-object.ts), the value every + // `exports.f =` statement assigns. + if (ts.isPropertyAccessExpression(expr.expression) && !expr.questionDotToken) { + const g = cjsInFileExportGlobalOf(lowerer, expr.expression); + if ( + g && + (g.type.kind === "func" || g.type.kind === "dyn") && + !cjsExportCallIgnoresThis(lowerer, expr.getSourceFile(), expr.expression.name.text) + ) { + lowerer.unsupported( + "SC1090", + expr, + `calling '${expr.expression.getText()}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`, + ); + } + if (g && (g.type.kind === "func" || g.type.kind === "dyn")) { + const callee: IrExpr = { kind: "varRef", localId: g.id, type: g.type, loc }; + const checked = lowerCheckedCallableValue(lowerer, expr, callee, loc); + if (checked) return checked; + if (callee.type.kind === "func") { + const args = completeFuncValueArgs(lowerer, expr, callee.type, loc); + return { kind: "callValue", callee, args, type: callee.type.ret, loc }; + } + } + } if (ts.isPropertyAccessExpression(expr.expression)) { const intrinsic = fenceNodeModuleMutationCall(lowerer, expr, expr.expression) ?? diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index f3a4830c36..13101b1d1a 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -18,6 +18,7 @@ import { builtinClassRefOf, } from "./builtins/class-values.js"; import type { FnCtx, Lowerer } from "./lowerer.js"; +import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js"; import { BOOL, DATE_T, @@ -8468,6 +8469,14 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean { export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); + // `new (require("./x").C)()`: the module's %init at the require's + // position, then construction through the member as for a binding. + if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) { + const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression); + if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) { + return lowerWithInlineRequireInit(lowerer, req, () => lowerNew(lowerer, expr)); + } + } const worker = lowerWorkerNew(lowerer, expr); if (worker) return worker; const readlineUi = lowerReadlineUiNew(lowerer, expr); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 149c252483..26cc821de1 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -45,6 +45,18 @@ import { constituentTypes } from "../ts7/checker.js"; import { dirname } from "node:path"; import * as posix from "node:path/posix"; import type { Lowerer } from "./lowerer.js"; +import { + inlineNamespaceRequireOf, + lowerWithInlineRequireInit, + wholeExportValue, +} from "./cjs-inline-require.js"; +import { tscInteropDefaultValueOf } from "./cjs-tsc-interop.js"; +import { + cjsExportMemberWriteGlobalOf, + cjsInFileExportGlobalOf, + isCjsExportsIdentifier, + lowerCjsExportsObjectValue, +} from "./cjs-exports-object.js"; import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js"; import { checkedArmExtraction } from "./coercions/unions.js"; import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js"; @@ -207,6 +219,7 @@ import { isNodeModuleValue, lowerCjsTopLevelThis, lowerNodeModuleIdentifier, + isCjsModuleGlobal, lowerNodeModuleProperty, lowerRequireCacheElement, lowerRequireCacheHas, @@ -862,6 +875,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const moduleValue = lowerNodeModuleIdentifier(lowerer, expr); if (moduleValue) return moduleValue; + if (expr.text === "exports") { + const exportsValue = lowerCjsExportsObjectValue(lowerer, expr); + if (exportsValue) return exportsValue; + } if (lowerer.isSelfReference(expr)) { return { kind: "selfRef", type: lowerer.ctx.selfType!, loc }; } @@ -1853,6 +1870,28 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); if (ts.isPropertyAccessExpression(expr)) { + // `require("./x").member` over a program module's export table: the + // module's %init at this position, then the member read exactly like + // a `const m = require("./x")` binding's (cjs-inline-require.ts). + if (!expr.questionDotToken) { + const req = inlineNamespaceRequireOf(lowerer, expr.expression); + if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) { + return lowerWithInlineRequireInit(lowerer, req, () => lowerExpr(lowerer, expr)); + } + // `module.exports` compared by identity inside its own module, or + // refused by name as any other value (cjs-exports-object.ts). + if (expr.name.text === "exports") { + const exportsValue = lowerCjsExportsObjectValue(lowerer, expr); + if (exportsValue) return exportsValue; + } + // tsc's `x_1.default` over a dependency whose whole export is one + // value: that value (cjs-tsc-interop.ts). + const interop = tscInteropDefaultValueOf(lowerer, expr); + if (interop !== null) { + const value = wholeExportValue(lowerer, interop.dep, expr); + if (value !== null) return value; + } + } { const member = lifetimeRegionMemberOf(lowerer.program, expr, lowerer.experimentalFiles); if (member !== null) @@ -2199,35 +2238,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // global (pure alias plumbing) and fall through unchanged; write // positions belong to the export-assignment machinery. { - const sf = expr.getSourceFile(); - const cjsMemberRecv = - isModuleExportsAccess(expr.expression) || - (ts.isIdentifier(expr.expression) && - expr.expression.text === "exports" && - !lowerer.resolveLocal(expr.expression) && - !lowerer.globalOf(expr.expression)); const writePos = ts.isBinaryExpression(expr.parent) && expr.parent.left === expr && expr.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken; - if ( - !expr.questionDotToken && - cjsMemberRecv && - !writePos && - isCjsJsFile(sf, lowerer.program) - ) { - const sym = - lowerer.checker.getSymbolAtLocation(expr.name) ?? - lowerer.cjsModuleExportSymbol(sf, expr.name.text); - const resolved = - sym !== undefined && sym.flags & ts.SymbolFlags.Alias - ? lowerer.checker.getAliasedSymbol(sym) - : sym; - const g = - (sym ? lowerer.globalsBySymbol.get(sym) : undefined) ?? - (resolved ? lowerer.globalsBySymbol.get(resolved) : undefined); - if (g) return { kind: "varRef", localId: g.id, type: g.type, loc }; - } + const g = cjsInFileExportGlobalOf(lowerer, expr); + if (g && !writePos) return { kind: "varRef", localId: g.id, type: g.type, loc }; } // Namespace-qualified reads (`N.x`, `A.B.C.f`, import= alias // chains): the member is a compile-time-known declaration of a @@ -2823,6 +2839,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { ); } } + // A member a JavaScript module attached to one of its functions + // (`status.message = codes; module.exports = status`), read from + // TypeScript: JavaScript stores such members on the function's own + // property table (`fn.member = v` writes through the function's + // checked-dynamic face), so the read takes the same path and enters + // the static code at the type inference gave the member. + if (recvLowered.type.kind === "func" && jsFunctionExpandoMember(lowerer, expr)) { + const read: IrExpr = { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(recvLowered, DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) }, + type: DYN, + loc, + }; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); + return mapped && mapped.kind !== "void" ? lowerer.coerceInto(expr, read, mapped) : read; + } lowerer.unsupported( "SC1090", expr, @@ -10169,6 +10202,35 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr // RHS value is the expression's value — its OWN type, so the // consumer sees exactly what the checker typed. if (ts.isPropertyAccessExpression(expr.left) && !expr.left.questionDotToken) { + // `const src = exports.src = []`: an export member assigned in + // value position writes its export global and yields the value. + { + let inFunction = false; + for (let p: ts.Node | undefined = expr.parent; p !== undefined; p = p.parent) { + if (ts.isFunctionLike(p) || ts.isClassDeclaration(p) || ts.isClassExpression(p)) { + inFunction = true; + break; + } + } + const g = inFunction + ? cjsExportMemberWriteGlobalOf(lowerer, expr.left) + : cjsInFileExportGlobalOf(lowerer, expr.left); + if (g) { + const value = lowerer.lowerExprExpecting(expr.right, g.type); + const tmp = lowerer.declareHiddenLocal("%exportValue", value.type); + const result = varRef(tmp.id, value.type, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: tmp.id, init: value, loc }, + { kind: "assign", localId: g.id, value: result, loc }, + ], + result, + type: value.type, + loc, + }; + } + } const recv = tryLowerExpression(lowerer, expr.left.expression); if (recv && recv.type.kind === "dyn") return lowerDynMemberAssignment(lowerer, expr, recv); const unionWrite = lowerUnionFieldWrite(lowerer, expr.left, expr.right); @@ -14996,6 +15058,9 @@ export function fieldTarget( access: ts.PropertyAccessExpression, ): FieldTarget | null { if (lowerer.cjsLocalModuleBindingOf(access.expression)) return null; + // `module.exports` / `module.id`: CommonJS module surface, never a field. + if (isCjsModuleGlobal(lowerer, access.expression)) return null; + if (isCjsExportsIdentifier(lowerer, access.expression)) return null; if (lowerer.chainBlocked(access)) return null; if ( access.name.text === "stackTraceLimit" && @@ -16282,3 +16347,28 @@ function isBuiltinModuleValueDecl(decl: ts.Node): boolean { ts.isVariableDeclaration(decl)) ); } + +/** True when `access` reads a member that JavaScript sources attach to a + * function by assignment (`fn.member = value`): the receiver's type is + * callable and every declaration of the member is such an assignment in a + * JS file. */ +function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { + if (access.questionDotToken || !ts.isIdentifier(access.name)) return false; + const recvType = lowerer.typeOf(access.expression); + if (lowerer.checker.getCallSignatures(recvType).length === 0) return false; + const member = lowerer.checker.getPropertyOfType(recvType, access.name.text); + const decls = member ? lowerer.checker.declarationsOf(member) : []; + return ( + decls.length > 0 && + decls.every((d) => { + if (!isJsSourceFile(d.getSourceFile())) return false; + const assign = ts.isBinaryExpression(d) ? d : d.parent; + return ( + assign !== undefined && + ts.isBinaryExpression(assign) && + assign.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(assign.left) + ); + }) + ); +} diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 3c035cd455..b375668bcc 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -18,7 +18,9 @@ import { isRelativeSpecifier } from "../workspace-registry.js"; import { canonicalBuiltinModule, cjsExportAssignmentOf, + cjsExportChainOf, cjsExportDiscardReason, + cjsExportTargetLiteral, entryPackageFilePredicate, isCjsJsFile, isJsSourceFile, @@ -26,6 +28,7 @@ import { locOf, makeCycleAdmission, orderedImportsOf, + requireSpecOf, resolveImport, resolveNpmImport, } from "../program.js"; @@ -36,6 +39,13 @@ import { withCycleInitFlags, } from "./cycle-initialization.js"; import { markRequireEarlyBindings } from "./require-initialization.js"; +import { + cjsTableMethodEntry, + inlineProgramRequireOf, + nestedCjsExportAssignmentsOf, + requireNestedPatternsOf, +} from "./cjs-inline-require.js"; +import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js"; import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js"; import { invalidJsonModuleDiag, @@ -1391,6 +1401,11 @@ export function collectGlobals( deferJsCollectionDiags(lowerer, sf, diagsBefore); } }; + // Export members assigned in VALUE position at the module's top + // level (`const src = exports.src = []`) are exports too. + for (const nested of nestedCjsExportAssignmentsOf(stmt, cjs?.expr)) { + registerExport(nested.name, nested.name.text, nested.value); + } if (cjs?.kind === "member" && ts.isIdentifier(cjs.name)) { // `exports.C = C` naming a CLASS declaration: alias plumbing — // no storage (a class VALUE global would fence for @@ -1398,6 +1413,13 @@ export function collectGlobals( // symbol (cjsMemberExportClassSymbol) and the class registry // applies unchanged. if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) continue; + // A chain `exports.a = exports.b = value`: every member is an + // export holding the chain's one value. + const chain = cjsExportChainOf(cjs.expr); + if (chain !== null) { + for (const nameNode of chain.names) registerExport(nameNode, nameNode.text, chain.value); + continue; + } registerExport(cjs.name, cjs.name.text, cjs.value); continue; } @@ -1429,7 +1451,22 @@ export function collectGlobals( // statement lowering evaluates the call exactly once at its // source position and assigns that slot. Unmappable calls retain // the existing statement-level fence. - const factoryValued = ts.isCallExpression(rhs); + // Any other computed value (`os.homedir || function () {…}`, + // `cond ? a : b`, `new Registry()`, `ns.member`) is the same single + // snapshot: Node stores whatever the expression produced. A + // require() of a program module is not: the exporter re-exports + // that module's exports, which requirers resolve through the + // checker's alias (the statement lowers to the dep's %init). + const factoryValued = + !ts.isIdentifier(rhs) && + !ts.isClassExpression(rhs) && + !ts.isObjectLiteralExpression(rhs) && + !( + requireSpecOf(rhs) !== null && + inlineProgramRequireOf(lowerer, rhs) !== null && + !sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "member") + ) && + cjsExportTargetLiteral(rhs, sf) === null; if (cjsScalarLiteral(rhs) || fnValued || factoryValued) { const diagsBefore = lowerer.diags.length; try { @@ -1464,6 +1501,10 @@ export function collectGlobals( if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { registerExport(prop.name, prop.name.text, prop.initializer); } + // A METHOD entry (`module.exports = { f(x) { … } }`) is an + // expression-valued export whose value is the method's function. + const method = cjsTableMethodEntry(prop); + if (method !== null) registerExport(method.name, method.name.text, method.name); // other property forms fence in the statement lowering } continue; @@ -1527,6 +1568,16 @@ export function collectGlobals( collectNestedVarGlobals(lowerer, sf, stmt, tag); continue; } + // tsc's interop bindings and its then-dead helper declarations own no + // storage either (cjs-tsc-interop.ts). + if ( + isJsSourceFile(sf) && + (isDeadTscHelperDecl(lowerer, stmt) || + (stmt.declarationList.declarations.length === 1 && + tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations[0]!) !== null)) + ) { + continue; + } // CommonJS require declarations are alias plumbing (preflight owns // the module edges; resolveValueSymbol routes reads through to the // exporter's declarations): no storage, and the statement lowers to @@ -1549,6 +1600,38 @@ export function collectGlobals( stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer); } } + // `const { codes: { ERR_X } } = require("./errors")`: the outer + // keys stay alias plumbing, but a nested pattern destructures the + // export's value at the require, so its bindings own storage like + // any destructured declaration (lowerVarDeclarator assigns them). + if (ts.isVariableStatement(stmt)) { + const isLetList = + (stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 || + (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0; + for (const decl of stmt.declarationList.declarations) { + for (const element of requireNestedPatternsOf(decl)) { + for (const nameNode of boundIdentifiersOf(element.name!)) { + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode)); + const type = mapped && mapped.kind !== "void" ? mapped : DYN; + const g: IrGlobal = { + id: `%g.${tag}${nameNode.text}`, + name: nameNode.text, + type, + mutable: isLetList, + source: bindingSource(nameNode), + ...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl) + ? { tdz: true as const } + : {}), + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsList.push(g); + if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); + } + } + } + } continue; } // `declare const __VERSION__: string` — AMBIENT: no storage exists diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index c157496017..292b38e87b 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -8,6 +8,7 @@ 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 { lowerCjsExportsObjectValue } from "./cjs-exports-object.js"; import { isCjsJsFile, isNodeEsmFile, locOf } from "../program.js"; import { runtimePathForTarget } from "../runtime-resolve.js"; import { @@ -177,6 +178,12 @@ export function cjsModuleRef(lowerer: Lowerer, node: ts.Node): IrExpr | null { return id === undefined ? null : numLit(id, locOf(node)); } +/** True when `node` is the CommonJS `require` global (not shadowed by a + * source binding, and not in an ES module). */ +export function isCjsRequireGlobal(lowerer: Lowerer, node: ts.Expression): boolean { + return cjsGlobal(lowerer, node, "require"); +} + export function isCjsModuleGlobal(lowerer: Lowerer, node: ts.Expression): boolean { return cjsGlobal(lowerer, node, "module"); } @@ -428,6 +435,13 @@ export function lowerNodeModuleIdentifier(lowerer: Lowerer, expr: ts.Identifier) if (!isCjsModuleGlobal(lowerer, expr)) return null; const ref = cjsModuleRef(lowerer, expr); if (ref) return ref; + if (isCjsJsFile(expr.getSourceFile(), lowerer.program)) { + lowerer.unsupported( + "SC1090", + expr, + "the CommonJS 'module' object in this position (module.exports members, the whole module.exports in comparisons, and the module metadata fields lower)", + ); + } lowerer.unsupported("SC1090", expr, "the CommonJS 'module' object in an ES module"); } @@ -441,6 +455,10 @@ export function lowerNodeModuleProperty( const member = expr.name.text; if (member === "exports" && direct && moduleExportsAlwaysTruthy(lowerer, expr)) return boolLit(true, locOf(expr)); + if (member === "exports" && direct) { + const value = lowerCjsExportsObjectValue(lowerer, expr); + if (value) return value; + } if (member === "exports" || member === "require") return null; let moduleRef = direct ? cjsModuleRef(lowerer, expr.expression) diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 57e6c23118..e11d0cd03f 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -15,6 +15,19 @@ import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; import { bindingSource } from "../binding-source.js"; import type { Lowerer } from "./lowerer.js"; +import { + cjsExportValueByName, + cjsNamespaceDestructureOf, + cjsTableMethodEntry, + inlineProgramRequireOf, + requireNestedPatternsOf, +} from "./cjs-inline-require.js"; +import { + cjsExportMemberWriteGlobalOf, + isCjsExportsIdentifier, + isFreshExportsReset, +} from "./cjs-exports-object.js"; +import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js"; import { arrayValueRead, arrayValueStore, @@ -60,6 +73,7 @@ import { import { enforceLibBoundary, lowerSurplusCalls } from "./lib-boundary.js"; import { cjsExportAssignmentOf, + cjsExportChainOf, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, @@ -1556,6 +1570,16 @@ function lowerVarDeclarator( const init = lowerer.requireInitStmt(target.spec, decl); return init ? [init] : []; } + // tsc's interop bindings (`const x_1 = __importDefault(require("./m"))`) + // are require bindings (cjs-tsc-interop.ts): the module's %init here. + if (ts.isSourceFile(stmt.parent)) { + const interop = tscInteropBindingOfDecl(lowerer, decl); + if (interop !== null) { + const init = lowerer.requireInitStmt(interop.spec, decl); + return init ? [init] : []; + } + if (isDeadTscHelperDecl(lowerer, stmt)) return []; + } // CommonJS require declarations (JS files): at the module's top // level the BINDINGS are alias plumbing (no storage; // resolveValueSymbol routes the reads), but the require itself is @@ -1576,7 +1600,21 @@ function lowerVarDeclarator( if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false); if (ts.isSourceFile(stmt.parent)) { const init = lowerer.requireInitStmt(spec, decl); - return init ? [init] : []; + const out: IrStmt[] = init ? [init] : []; + // Nested patterns destructure the named export's value right after + // the module evaluated (`const { codes: { ERR_X } } = require(...)`). + for (const element of requireNestedPatternsOf(decl)) { + const key = element.propertyName as ts.Identifier; + const value = lowerer.lowerExpr(key); + lowerer.lowerBindingPattern( + element.name as ts.ObjectBindingPattern, + () => lowerer.lowerExpr(key), + value.type, + isLet, + out, + ); + } + return out; } // A nested BUILTIN require (`get inFreeBSDJail() { const { execSync } // = require('child_process'); ... }` — test/common's lazy-getter @@ -1605,6 +1643,35 @@ function lowerVarDeclarator( // `const { createSign } = crypto` over a builtin NAMESPACE binding: // alias plumbing (builtinImportOf routes the reads) — no statement. if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return []; + // `const { A, B } = ns` over a CommonJS module binding: each name + // snapshots the named export's value here (cjs-inline-require.ts). + { + const nsDestructure = cjsNamespaceDestructureOf(lowerer, decl); + if (nsDestructure !== null) { + const out: IrStmt[] = []; + for (const element of nsDestructure.elements) { + const target = lowerer.globalOf(element.name); + const value = cjsExportValueByName(lowerer, nsDestructure.dep, element.key, element.name); + if (target === null || value === null) { + lowerer.unsupported( + "SC1090", + element.name, + `destructuring the export '${element.key}' of a CommonJS module binding (it has no static value)`, + ); + } + out.push({ + kind: "assign", + localId: target.id, + value: typeEquals(value.type, target.type) + ? value + : lowerer.coerceInto(element.name, value, target.type), + loc: locOf(element.name), + ...("tdz" in target && target.tdz ? { initializes: true as const } : {}), + }); + } + return out; + } + } return lowerer.lowerDestructuringDecl(decl, isLet); } // STORED protocol values with statically closed representations: @@ -6054,6 +6121,49 @@ function lowerCjsExportStatement( if (whole?.kind !== "table" || cjsExportDiscardReason(stmt) !== null) continue; root = lowerer.globalsByDeclNode.get(whole.expr); } + // A CHAIN `exports.a = exports.b = value`: Node evaluates the value + // once, assigns the innermost member first, and every member holds + // the same value. Each member is its own export global + // (collectGlobals registers the inner ones too). + { + const chain = cjsExportChainOf(cjs.expr); + if (chain !== null && root?.type.kind !== "dyn") { + const globalOfName = (nameNode: ts.Identifier): IrGlobal => { + const symbol = + lowerer.checker.getSymbolAtLocation(nameNode) ?? + lowerer.cjsModuleExportSymbol(sf, nameNode.text); + const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; + if (!g) { + lowerer.lowerExpr(chain.value); + lowerer.badType(chain.value, lowerer.typeOf(chain.value)); + } + return g; + }; + const inner = chain.names[chain.names.length - 1]!; + const innerG = globalOfName(inner); + const body: IrStmt[] = [ + { + kind: "assign", + localId: innerG.id, + value: lowerer.lowerExprExpecting(chain.value, innerG.type), + loc: locOf(chain.value), + }, + ]; + for (let i = chain.names.length - 2; i >= 0; i--) { + const g = globalOfName(chain.names[i]!); + const read: IrExpr = { kind: "varRef", localId: innerG.id, type: innerG.type, loc }; + body.push({ + kind: "assign", + localId: g.id, + value: typeEquals(g.type, innerG.type) + ? read + : lowerer.coerceInto(chain.value, read, g.type), + loc, + }); + } + return { kind: "block", body, loc }; + } + } return assignExport(cjs.name, cjs.value, root); } let tableObj = cjs.obj; @@ -6119,6 +6229,30 @@ function lowerCjsExportStatement( body.push(assignExport(prop.name, prop.initializer)); continue; } + // METHOD entries: the export global holds the method's function, + // created at this statement like a function-expression entry. Node + // calls it with `this` bound to the table, which the static export + // table cannot represent, so a body that uses `this` keeps a fence. + { + const method = cjsTableMethodEntry(prop); + if (method !== null && !resolvedTarget) { + lowerer.rejectThisInObjectMethod(method.body); + const symbol = + lowerer.checker.getSymbolAtLocation(method.name) ?? + lowerer.cjsModuleExportSymbol(cjs.expr.getSourceFile(), method.name.text); + const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; + if (g) { + const fn = lowerer.lowerLambda(method); + body.push({ + kind: "assign", + localId: g.id, + value: typeEquals(fn.type, g.type) ? fn : lowerer.coerceInto(method, fn, g.type), + loc: locOf(method), + }); + continue; + } + } + } // ACCESSOR entries: no storage and no code here — reads lift the // getter as a module-level function and call it per read // (cjsExportAccessorRead); writes through the setter keep their @@ -6134,7 +6268,15 @@ function lowerCjsExportStatement( if (ts.isSpreadAssignment(prop)) { let inner: ts.Expression = prop.expression; while (ts.isParenthesizedExpression(inner)) inner = inner.expression; - if (requireSpecOf(inner) !== null) continue; + if (requireSpecOf(inner) !== null) { + // The spread still EVALUATES the module, at its place among the + // table's entries (a program module joins the graph through this + // inline require; cjs-inline-require.ts). + const req = inlineProgramRequireOf(lowerer, inner); + const init = req !== null ? lowerer.requireInitStmt(req.spec, req.call) : null; + if (init !== null && !resolvedTarget) body.push(init); + continue; + } } lowerer.unsupported( "SC1090", @@ -6167,6 +6309,16 @@ function lowerCjsSingleValueExport( ): IrStmt | null { let rhs: ts.Expression = expr.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + // `module.exports = require("./x")`: the module re-exports x's exports. + // Requirers resolve through the checker's alias to x's own export + // machinery; this statement is x's evaluation at this position. + { + const req = inlineProgramRequireOf(lowerer, rhs); + // With later member exports (`module.exports.extra = …`) the module + // extends that object, so it keeps the factory root registration. + if (req !== null && !lowerer.globalsByDeclNode.has(expr)) + return lowerer.requireInitStmt(req.spec, req.call) ?? { kind: "block", body: [], loc }; + } if (ts.isIdentifier(rhs)) { const sym = lowerer.resolveValueSymbol(rhs); if (!sym) return null; @@ -6177,6 +6329,17 @@ function lowerCjsSingleValueExport( ) { return { kind: "block", body: [], loc }; } + // A require binding (`const x = require("./x"); module.exports = x`) + // re-exports that module exactly like the inline form (the binding's + // own statement already evaluated it), and a builtin global + // (`module.exports = TypeError`) is the global itself: requirer + // bindings alias straight through to it, so nothing is stored. + if ( + lowerer.checker.declarationsOf(sym).some((d) => ts.isSourceFile(d)) || + lowerer.isStdlibSymbol(sym) + ) { + return { kind: "block", body: [], loc }; + } const g = lowerer.globalsBySymbol.get(sym); if (g) { if (g.mutable) { @@ -6398,6 +6561,21 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ); } if (topLevelJs && ts.isExpressionStatement(stmtNode)) { + if (isFreshExportsReset(lowerer, stmtNode)) + return { kind: "block", body: [], loc: locOf(expr) }; + // Any other `exports = …` rebinds the wrapper's exports binding: the + // members later attached through it belong to the new value. + if ( + ts.isBinaryExpression(expr) && + expr.operatorToken.kind === ts.SyntaxKind.EqualsToken && + isCjsExportsIdentifier(lowerer, expr.left) + ) { + lowerer.unsupported( + "SC1090", + expr.left, + "rebinding 'exports' (exports = module.exports = value): members attached through 'exports' afterwards are properties of that value, which the static export table does not model — write module.exports. = … and module.exports = value", + ); + } const cjs = cjsExportAssignmentOf(stmtNode); if (cjs) return lowerCjsExportStatement(lowerer, cjs); // The tsc CJS interop stamp — `Object.defineProperty(exports, @@ -6939,6 +7117,19 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm const parts = lowerDestructuringAssignParts(lowerer, expr.left, expr.right, locOf(expr)); return { kind: "block", body: parts.stmts, loc: locOf(expr) }; } + // `exports.x = v` inside a function updates the member's export + // global (cjs-exports-object.ts). + if (ts.isPropertyAccessExpression(expr.left)) { + const g = cjsExportMemberWriteGlobalOf(lowerer, expr.left); + if (g) { + return { + kind: "assign", + localId: g.id, + value: lowerer.lowerExprExpecting(expr.right, g.type), + loc: locOf(expr), + }; + } + } if (!ts.isIdentifier(expr.left)) { lowerer.unsupported("SC1090", expr.left, "assignment to non-variables"); } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a1ded6aeec..55447e1e08 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -281,6 +281,13 @@ import { appendWorkerModules, } from "./lower-modules.js"; import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js"; +import { + cjsNamespaceModule, + inlineNamespaceRequireOf, + inlineProgramRequireOf, +} from "./cjs-inline-require.js"; +import { tscInteropModuleOf } from "./cjs-tsc-interop.js"; +import { usesCjsExportsIdentity } from "./cjs-exports-object.js"; import { registerBuiltinEventTargetClasses } from "./lower-event-target.js"; import { type ClassInfo, @@ -746,6 +753,8 @@ export interface LowerOptions { * resolution walk and named-import link checks): the program * compiles to that startup crash. */ startupCrash?: StartupCrash | null; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; /** LIBRARY mode's reachability roots: the profile-mapped exports of the * entry module. Executable builds root at the entry's top level alone * (an unreferenced export dead-strips); a library's exports are called from @@ -838,6 +847,8 @@ export interface LowererMode { * module init — Node refuses the whole graph before anything evaluates, * so nothing runs. */ startupCrash?: StartupCrash | null; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; /** The build's outbound native FFI declarations. */ ffiImports?: readonly IrFfiImport[]; /** LowerOptions.libraryCallbacks (see there). */ @@ -951,6 +962,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -974,6 +986,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1004,6 +1017,7 @@ export function lowerToIr( reachable, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1062,6 +1076,7 @@ export function lowerToIr( reachable, remainder: true, alreadyFlushed: resultLowerer.flushedSymbols, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, targetPlatform, ffiImports, libraryCallbacks, @@ -1621,6 +1636,15 @@ export class Lowerer { readonly checker: ts.TypeChecker; readonly diags: ScrDiagnostic[] = []; readonly fnSigsBySymbol = new Map(); + /** Per file: whether it assigns `exports` and `module.exports` + * (cjs-exports-object.ts). */ + readonly cjsExportsAssignments = new Map< + ts.SourceFile, + { exports: boolean; moduleExports: boolean } + >(); + /** Inline require() calls whose module edge closes a require cycle + * (LoadResult.inlineRequireCycleCalls). */ + readonly inlineRequireCycleCalls: ReadonlySet; /** Immutable callable aliases and compiler-generated adapters. The map * is symbol-keyed, so shadowing and cross-module imports keep the same * exact binding discipline as globals/functions. */ @@ -1917,6 +1941,9 @@ export class Lowerer { /** Per CommonJS file using top-level `this`: the global holding the * object that stands for its original exports (lowerCjsTopLevelThis). */ readonly cjsExportsThis = new Map(); + /** Inline program-module require calls whose %init the enclosing member + * read or member call is already sequencing (cjs-inline-require.ts). */ + readonly inlineRequireInitDone = new Set(); /** 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. */ @@ -2849,6 +2876,7 @@ export class Lowerer { this.alreadyFlushed = mode.alreadyFlushed ?? new Set(); this.targetPlatform = mode.targetPlatform ?? process.platform; this.startupCrash = mode.startupCrash ?? null; + this.inlineRequireCycleCalls = mode.inlineRequireCycleCalls ?? new Set(); this.projectCheckJs = mode.projectCheckJs ?? false; this.ffiImports = mode.ffiImports ?? []; this.libraryCallbacks = mode.libraryCallbacks ?? false; @@ -3160,8 +3188,10 @@ export class Lowerer { ident.parent.name === ident ) { const recv = ident.parent.expression; - if (ts.isIdentifier(recv) && this.cjsLocalModuleBindingOf(recv)) { - const dep = this.localModuleBindingDepOf(recv); + if (this.cjsLocalModuleBindingOf(recv)) { + const dep = ts.isIdentifier(recv) + ? this.localModuleBindingDepOf(recv) + : this.cjsModuleRefDepOf(recv); const exported = dep ? this.cjsModuleExportSymbol(dep, ident.text) : undefined; // A createRequire call is typed as `any`, so tsgo can manufacture a // transient property symbol with no relation to the target module. @@ -3553,6 +3583,21 @@ export class Lowerer { return dep; } + /** The table module a non-identifier module reference names: an inline + * `require("./x")` (cjs-inline-require.ts) or tsc's unstamped + * `x_1.default` interop read (cjs-tsc-interop.ts). */ + cjsModuleRefDepOf(expr: ts.Expression): ts.SourceFile | null { + const inline = inlineNamespaceRequireOf(this, expr); + if (inline !== null) return inline.dep; + const interop = tscInteropModuleOf(this, expr); + return interop !== null && cjsNamespaceModule(this, interop.dep) ? interop.dep : null; + } + + /** The table module a CommonJS module binding identifier names. */ + cjsModuleBindingDepOf(expr: ts.Identifier): ts.SourceFile | null { + return this.localModuleBindingDepOf(expr); + } + /** The compiled module behind a require-like namespace binding: an * ordinary CommonJS require declaration, a canonical createRequire * declaration, or an ESM default import of CommonJS. One resolver keeps @@ -3565,6 +3610,10 @@ export class Lowerer { if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl); if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || !decl.initializer) return null; + { + const interop = tscInteropModuleOf(this, expr); + if (interop !== null) return cjsNamespaceModule(this, interop.dep) ? interop.dep : null; + } const directSpec = requireSpecOf(decl.initializer); if (directSpec !== null) { if (isJsonRequireDecl(this, decl)) return null; @@ -3611,6 +3660,10 @@ export class Lowerer { if (wholeAssignment === undefined) return null; let rhs: ts.Expression = wholeAssignment.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + // A pure re-export (`module.exports = require("./core")`) is the inner + // module's export table, read like any namespace binding. + if (inlineProgramRequireOf(this, rhs) !== null && !this.globalsByDeclNode.has(wholeAssignment)) + return null; if (ts.isIdentifier(rhs)) { const value = this.checker.getSymbolAtLocation(rhs); if (value) { @@ -3663,7 +3716,9 @@ export class Lowerer { * symbols → resolveValueSymbol); the bare value keeps the * namespace-object fence, like ESM namespace imports of builtins. */ cjsLocalModuleBindingOf(expr: ts.Expression): boolean { - if (!ts.isIdentifier(expr)) return false; + // An inline `require("./x")` over a table module reads its members like + // a binding (cjs-inline-require.ts sequences the module's %init). + if (!ts.isIdentifier(expr)) return this.cjsModuleRefDepOf(expr) !== null; const sym = this.checker.getSymbolAtLocation(expr); if (this.localModuleBindingDepOf(expr) === null) return false; if (this.createRequireWholeExportSymbolOf(expr) !== null) return false; @@ -3676,6 +3731,8 @@ export class Lowerer { // the object literal) keep the namespace reading — member accesses // resolve through the table's property symbols. if (sym && sym.flags & ts.SymbolFlags.Alias) { + // `module.exports = TypeError`: the binding IS the builtin global. + if (this.isStdlibSymbol(this.checker.getAliasedSymbol(sym))) return false; const d = this.checker.declarationsOf(this.checker.getAliasedSymbol(sym))[0]; if (d && (ts.isClassDeclaration(d) || ts.isFunctionDeclaration(d))) return false; if (d && ts.isVariableDeclaration(d)) { @@ -3773,7 +3830,7 @@ 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)) { + if (usesCjsTopLevelThis(this, fp.sf) || usesCjsExportsIdentity(this, fp.sf)) { const exportsThis: IrGlobal = { id: `%g.${tag}%exportsThis`, name: "%exportsThis", diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index c769ed99e7..f7ab33429e 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -39,6 +39,21 @@ import { THREADS_MODULE } from "./threads-import.js"; import { erasedTypeOnlyImport } from "./type-only-imports.js"; import { EXPERIMENTAL_MODULE } from "./experimental-import.js"; +/** Where a package-anchored preflight diagnostic sits, as + * `:` — the SC2013 refusal that drops the package + * reports only at the consumer's import, so its reason carries the inner + * location. */ +function packageDiagWhere(load: LoadResult, loc: SrcLoc): string { + const file = loc.file.split("\\").join("/"); + const at = file.lastIndexOf("/node_modules/"); + const shown = at >= 0 ? file.slice(at + "/node_modules/".length) : file; + const text = load.program.getSourceFile(loc.file)?.text; + if (text === undefined) return shown; + let line = 1; + for (let i = 0; i < loc.start && i < text.length; i++) if (text.charCodeAt(i) === 10) line++; + return `${shown}:${line}`; +} + export interface ProgramLoadOptions { npmStatic?: readonly string[]; externalTypes?: Readonly> | undefined; @@ -445,7 +460,8 @@ function loadFrontend( const reasons = new Map(npmStaticOffenders()); for (const d of preflight) { const pkg = npmStaticPackageOfPath(d.loc.file); - if (pkg !== null && !reasons.has(pkg)) reasons.set(pkg, `${d.code}: ${d.message}`); + if (pkg !== null && !reasons.has(pkg)) + reasons.set(pkg, `${d.code} at ${packageDiagWhere(load, d.loc)}: ${d.message}`); } if (![...reasons.keys()].some((p) => effective.has(p))) { const named = new Map(); @@ -577,6 +593,7 @@ function loadFrontend( ...opts, frontendServices: finalLoad.services, startupCrash: finalLoad.startupCrash ?? null, + inlineRequireCycleCalls: finalLoad.inlineRequireCycleCalls, projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false, externalTypes: finalLoad.externalTypes, externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index dcdaf9040b..8433338d03 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -338,6 +338,12 @@ export interface LoadResult { * last. Empty until checkPreflight runs (and empty when preflight fails * structurally). */ moduleOrder: ts.SourceFile[]; + /** Inline require() calls whose edge closes a require cycle. Node answers + * them with the still-evaluating module's partial exports, which the + * static export globals do not model; their lowering refuses them by name + * at run time instead of refusing the whole program. Filled by + * checkPreflight. */ + inlineRequireCycleCalls?: ReadonlySet; /** Node's startup refusal, when the graph carries an import edge Node * rejects before ANY module evaluates — a RESOLUTION refusal (a bare * specifier nothing installed resolves, an exports/imports map that @@ -975,9 +981,10 @@ export function loadProgram( * preflight diagnostics. Preserve the caller's complete load shape when * mutating it, including its owned services and disposal callback. */ export function checkPreflight(load: T): ScrDiagnostic[] { - const { diags, moduleOrder, startupCrash } = preflight7(load); + const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls } = preflight7(load); load.moduleOrder = moduleOrder; load.startupCrash = startupCrash; + load.inlineRequireCycleCalls = inlineRequireCycleCalls; return diags; } @@ -1842,6 +1849,36 @@ function nestedBareRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] { return out; } +/** INLINE `require("...")` calls in expression position anywhere in a file + * (`require("./x").f`, `require("debug")("ns")`, `module.exports = + * require("./y")`, `{ v: require("./v") }`): each evaluates its module at + * that position, exactly like a bare require statement. Top-level require + * statements and bare require statements are collected by their own + * scans; a require that initializes a nested declaration keeps its + * lowering fence and is not an edge. */ +function inlineRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] { + const out: ts.CallExpression[] = []; + ts.walkPreorder(sf, (n) => { + if (!ts.isCallExpression(n) || requireSpecOf7(n) === null) return; + let outer: ts.Node = n; + while ( + outer.parent !== undefined && + (ts.isParenthesizedExpression(outer.parent) || + ts.isAsExpression(outer.parent) || + ts.isTypeAssertion(outer.parent) || + ts.isNonNullExpression(outer.parent)) + ) { + outer = outer.parent; + } + const parent = outer.parent; + if (parent !== undefined && ts.isExpressionStatement(parent)) return; + if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.initializer === outer) + return; + out.push(n); + }); + return out; +} + type RequireCallBindingKind7 = "ambient" | "createRequire" | "source"; function stripRequireCasts7(expr: ts.Expression): ts.Expression { @@ -2408,6 +2445,13 @@ function nsBindingUsesAreBareStatements7( export interface CycleEdge { dep: ts.SourceFile; stmt?: ts.ImportDeclaration | ts.ExportDeclaration; + /** An inline require() in a deferred position (a function body): Node + * evaluates the dependency when that code runs, after this module's own + * top level, so the edge orders the dependency after the module and + * never closes a cycle through the module's initialization. */ + lazy?: true; + /** The inline require() call behind the edge (inline edges only). */ + call?: ts.CallExpression; } /** True when the reference at `node` can only evaluate AFTER the module @@ -4085,6 +4129,7 @@ function preflight7(load: LoadResult): { diags: ScrDiagnostic[]; moduleOrder: ts.SourceFile[]; startupCrash: StartupCrash | null; + inlineRequireCycleCalls: ReadonlySet; } { const { program, entry } = load; const diags: ScrDiagnostic[] = [...load.configDiags]; @@ -4885,6 +4930,21 @@ function preflight7(load: LoadResult): { pattern.elements.every((e) => { if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return false; if (e.name !== undefined && ts.isIdentifier(e.name)) return true; + // A nested pattern under a named export of a program module + // destructures that export's VALUE at the require + // (`const { codes: { ERR_X } } = require("./errors")`): the + // inner bindings are ordinary destructured storage. + if ( + module === null && + builtin === null && + bindingKind !== "createRequire" && + pattern === req.decl!.name && + e.name !== undefined && + ts.isObjectBindingPattern(e.name) && + e.propertyName !== undefined && + ts.isIdentifier(e.propertyName) + ) + return true; if (module === null || e.name === undefined || !ts.isObjectBindingPattern(e.name)) return false; const key = e.propertyName; @@ -4998,6 +5058,32 @@ function preflight7(load: LoadResult): { continue; } } + // Inline requires of program modules are module edges like the bare + // statements above (cjs-inline-require.ts lowers each to the module's + // %init at its position). Only the CommonJS global in a CommonJS file + // counts, and targets outside the compiled graph add nothing here: + // their use sites keep their existing lowering fences. + if (jsRequireFile && !isNodeEsmFile7(sf, program)) { + for (const call of inlineRequiresOf7(sf)) { + if (requireCallBindingKind7(program, call) !== "ambient") continue; + const spec = requireSpecOf7(call)!; + if (load.externalTypes.has(spec) || canonicalBuiltinModule(spec) !== null) continue; + const lazy = inDeferredPosition7(call); + const edgeTo = (dep: ts.SourceFile): CycleEdge => + lazy ? { dep, lazy: true, call } : { dep, call }; + const projectDep = resolveImport7(program, sf, spec, "require"); + if (projectDep !== null) { + if (!projectDep.fileName.endsWith(".json")) deps.push(edgeTo(projectDep)); + continue; + } + if (isRelativeSpecifier(spec) || spec.startsWith("#")) continue; + const npmReq = resolveNpmImport7(sf.fileName, spec, "require"); + if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) { + const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile); + if (nDep !== null) deps.push(edgeTo(nDep)); + } + } + } } } @@ -5024,8 +5110,12 @@ function preflight7(load: LoadResult): { // reads are all let/const bindings with module storage — the // lowering checks each one (cycleEarlyBindings) and throws Node's // ReferenceError on a read before the declaration ran. - const cycleAdmissionReason = makeCycleAdmission(program, (sf) => edges.get(sf) ?? []); + const cycleAdmissionReason = makeCycleAdmission(program, (sf) => { + const all = edges.get(sf) ?? []; + return all.some((e) => e.call !== undefined) ? all.filter((e) => e.call === undefined) : all; + }); const order: ts.SourceFile[] = []; + const inlineRequireCycleCalls = new Set(); const state = new Map(); const stack: string[] = []; const visit = (sf: ts.SourceFile): void => { @@ -5033,9 +5123,14 @@ function preflight7(load: LoadResult): { stack.push(sf.fileName); for (const e of edges.get(sf) ?? []) { if (e.dep === sf) continue; // self-import: a Node cache hit, not a cycle + if (e.lazy === true) continue; // visited after this module completes const s = state.get(e.dep); if (s === "done") continue; if (s === "visiting") { + if (e.call !== undefined) { + inlineRequireCycleCalls.add(e.call); + continue; + } // A back-edge: Node answers it from the cache (the module is // already evaluating) — admitted by the cheap per-edge rule, the // declaration-only init window rule, or the exact initialization @@ -5059,6 +5154,11 @@ function preflight7(load: LoadResult): { stack.pop(); state.set(sf, "done"); order.push(sf); + // Lazy inline requires run after this module's top level: their + // modules join the order afterwards (cache hits when already loaded). + for (const e of edges.get(sf) ?? []) { + if (e.lazy === true && state.get(e.dep) === undefined) visit(e.dep); + } }; visit(entry); @@ -5112,7 +5212,12 @@ function preflight7(load: LoadResult): { esmNamedImportLinkCheck(program, entry, order, diags), ); - return { diags, moduleOrder: order, startupCrash: resolveCrash ?? linkCrash }; + return { + diags, + moduleOrder: order, + startupCrash: resolveCrash ?? linkCrash, + inlineRequireCycleCalls, + }; } /* ── the CJS named-import link check ───────────────────────────────────── @@ -5815,6 +5920,37 @@ export function cjsExportAssignmentOf( return null; } +/** The members and shared value of a CHAINED member export + * `exports.a = exports.b = module.exports.c = value` (outermost name + * first), or null when the assignment is not a chain of at least two + * identifier-named export members or its value is undefined. */ +export function cjsExportChainOf( + e: ts.BinaryExpression, +): { names: ts.Identifier[]; value: ts.Expression } | null { + const names: ts.Identifier[] = []; + let cur: ts.Expression = e; + for (;;) { + while (ts.isParenthesizedExpression(cur)) cur = cur.expression; + if (!ts.isBinaryExpression(cur) || cur.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break; + const left = cur.left; + if ( + !ts.isPropertyAccessExpression(left) || + left.questionDotToken || + !ts.isIdentifier(left.name) + ) + break; + const recv = left.expression; + if (!((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv))) break; + names.push(left.name); + cur = cur.right; + } + // tsc's `exports.a = exports.b = void 0;` preamble is not a value chain + // (the export-statement lowering owns it). + const isVoid = + cur.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(cur) && cur.text === "undefined"); + return names.length >= 2 && !isVoid ? { names, value: cur } : null; +} + /** Why Node would DISCARD this CommonJS export statement, or null when it * survives (Node's export-object identity rules — checker-free, pure * AST): `module.exports = {...}` REPLACES the export object, so members diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json new file mode 100644 index 0000000000..5f1e322f73 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json @@ -0,0 +1,20 @@ +{ + "order": [ + "/tests/corpus/6150-cjs-inline-require/lib.js", + "/tests/corpus/6150-cjs-inline-require/inner.js", + "/tests/corpus/6150-cjs-inline-require/reexport-inline.js", + "/tests/corpus/6150-cjs-inline-require/reexport-binding.js", + "/tests/corpus/6150-cjs-inline-require/type-error.js", + "/tests/corpus/6150-cjs-inline-require/pick.js", + "/tests/corpus/6150-cjs-inline-require/registry.js", + "/tests/corpus/6150-cjs-inline-require/make-logger.js", + "/tests/corpus/6150-cjs-inline-require/point.js", + "/tests/corpus/6150-cjs-inline-require/shapes.js", + "/tests/corpus/6150-cjs-inline-require/table.js", + "/tests/corpus/6150-cjs-inline-require/verify.js", + "/tests/corpus/6150-cjs-inline-require/fallback.js", + "/tests/corpus/6150-cjs-inline-require/main.js", + "/tests/corpus/6150-cjs-inline-require/late.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json new file mode 100644 index 0000000000..883aa872a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json @@ -0,0 +1,10 @@ +{ + "order": [ + "/tests/corpus/6151-cjs-export-chains/chain.js", + "/tests/corpus/6151-cjs-export-chains/constants.js", + "/tests/corpus/6151-cjs-export-chains/re.js", + "/tests/corpus/6151-cjs-export-chains/sem.js", + "/tests/corpus/6151-cjs-export-chains/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json new file mode 100644 index 0000000000..19a1b20dec --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/6152-cjs-tsc-interop/plain.js", + "/tests/corpus/6152-cjs-tsc-interop/stamped.js", + "/tests/corpus/6152-cjs-tsc-interop/fn.js", + "/tests/corpus/6152-cjs-tsc-interop/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json new file mode 100644 index 0000000000..070b131e37 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6153-cjs-exports-object/lib.js", + "/tests/corpus/6153-cjs-exports-object/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json new file mode 100644 index 0000000000..ee9f71d229 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6154-cjs-require-nested-destructure/errors.js", + "/tests/corpus/6154-cjs-require-nested-destructure/constants.js", + "/tests/corpus/6154-cjs-require-nested-destructure/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json new file mode 100644 index 0000000000..9ac5f0e41e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6155-cjs-export-table-methods/more.js", + "/tests/corpus/6155-cjs-export-table-methods/lib.js", + "/tests/corpus/6155-cjs-export-table-methods/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json new file mode 100644 index 0000000000..e73e603e38 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6156-cjs-function-members-from-ts/status.cjs", + "/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs", + "/tests/corpus/6156-cjs-function-members-from-ts/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs index 1b35465188..fbb1a65fcc 100644 --- a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs +++ b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs @@ -1,12 +1,12 @@ -// @deferred-fences: 9 +// @deferred-fences: 1 // Function expressions lowered speculatively — the `.call(this)` and // `.apply` receivers of CommonJS wrappers, and a computed class base — keep -// every statement of their bodies. A statement with no static lowering -// (here `module.exports` read as a value) compiles to its runtime fence -// instead of vanishing; these never run, so the binary matches Node. -// Coverage counts the three fenced statements nine times: each lowering of -// a wrapper body counts its statements, and the `.call` and `.apply` -// wrapper bodies are each lowered four times. +// every statement of their bodies. The wrappers' `root === module.exports` +// and `typeof module.exports` compile; a statement with no static lowering +// (here `module.exports` passed as a value in the class base) compiles to +// its runtime fence instead of vanishing. It never runs, so the binary +// matches Node. Coverage counts that one fenced statement once: the class +// base's body is lowered once. const verbose = process.argv.length > 99; (function () { var root = this; diff --git a/tests/corpus/6150-cjs-inline-require/fallback.js b/tests/corpus/6150-cjs-inline-require/fallback.js new file mode 100644 index 0000000000..d89da1a008 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/fallback.js @@ -0,0 +1,6 @@ +const preferred = undefined; +module.exports = + preferred || + function fallback() { + return "fallback"; + }; diff --git a/tests/corpus/6150-cjs-inline-require/inner.js b/tests/corpus/6150-cjs-inline-require/inner.js new file mode 100644 index 0000000000..7aa065c1e9 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/inner.js @@ -0,0 +1,4 @@ +console.log("inner: evaluated"); +exports.inner = function () { + return "inner"; +}; diff --git a/tests/corpus/6150-cjs-inline-require/late.js b/tests/corpus/6150-cjs-inline-require/late.js new file mode 100644 index 0000000000..a28525b26e --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/late.js @@ -0,0 +1,2 @@ +console.log("late: evaluated"); +exports.value = "late value"; diff --git a/tests/corpus/6150-cjs-inline-require/lib.js b/tests/corpus/6150-cjs-inline-require/lib.js new file mode 100644 index 0000000000..94a5872a43 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/lib.js @@ -0,0 +1,6 @@ +console.log("lib: evaluated"); +exports.f = function (x) { + return x * 10; +}; +exports.n = 7; +exports.counter = { hits: 0 }; diff --git a/tests/corpus/6150-cjs-inline-require/main.js b/tests/corpus/6150-cjs-inline-require/main.js new file mode 100644 index 0000000000..0ebc20d364 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/main.js @@ -0,0 +1,55 @@ +// Inline require() of program modules: each call evaluates its module at +// exactly that position (or answers from the cache), and the value is the +// module's exports, read statically. +console.log("main: start"); +const f = require("./lib.js").f; +console.log("main: first inline require done", f(1)); +console.log("main: cached", require("./lib.js").n, require("./lib.js").f(2)); +exports.viaInline = require("./lib.js").f; +console.log("main: exported member", exports.viaInline(3), exports.viaInline === f); + +// Mutation through an inline read is visible to every requirer. +require("./lib.js").counter.hits++; +const lib = require("./lib.js"); +console.log("main: shared counter", lib.counter.hits, require("./lib.js").counter === lib.counter); + +// A whole-export function called directly off the require. +const debug = require("./make-logger.js")("app"); +console.log(debug("ready")); + +// Construction through a whole-export class and through a member class. +const p = new (require("./point.js"))(1, 2); +console.log("point", p.x + p.y); +const q = new (require("./shapes.js").Square)(3); +console.log("square", q.area()); + +// Lazy inline require: the module loads at the first call, once. +function late() { + return require("./late.js").value; +} +console.log("main: before late"); +console.log("late", late(), late()); + +// Member calls off an inline require of a table module. +console.log("table member", require("./table.js").twice(4), require("./table.js").label); + +// Table entries holding inline requires. +const table = { verify: require("./verify.js"), sign: require("./lib.js").f }; +console.log("table", table.verify("tok"), table.sign(4)); + +// Re-exports: module.exports = require(...) and through a binding. +const a = require("./reexport-inline.js"); +const b = require("./reexport-binding.js"); +console.log("reexports", a.inner(), b.inner(), a.inner === b.inner); + +// Single computed values as the whole export. +const T = require("./type-error.js"); +try { + throw new T("bad input"); +} catch (e) { + console.log(e instanceof TypeError, String(e)); +} +const pick = require("./pick.js"); +console.log("pick", pick(5), typeof require("./fallback.js"), require("./fallback.js")()); +const reg = require("./registry.js"); +console.log("registry", reg.size(), reg.add("x"), reg.size(), require("./registry.js") === reg); diff --git a/tests/corpus/6150-cjs-inline-require/make-logger.js b/tests/corpus/6150-cjs-inline-require/make-logger.js new file mode 100644 index 0000000000..f698b54a72 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/make-logger.js @@ -0,0 +1,6 @@ +console.log("make-logger: evaluated"); +module.exports = function (ns) { + return function (msg) { + return ns + ": " + msg; + }; +}; diff --git a/tests/corpus/6150-cjs-inline-require/package.json b/tests/corpus/6150-cjs-inline-require/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6150-cjs-inline-require/pick.js b/tests/corpus/6150-cjs-inline-require/pick.js new file mode 100644 index 0000000000..aadc1eb8e3 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/pick.js @@ -0,0 +1,7 @@ +function double(x) { + return x * 2; +} +function triple(x) { + return x * 3; +} +module.exports = process.env.SCRIPTC_CORPUS_NEVER_SET ? triple : double; diff --git a/tests/corpus/6150-cjs-inline-require/point.js b/tests/corpus/6150-cjs-inline-require/point.js new file mode 100644 index 0000000000..0f002800c6 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/point.js @@ -0,0 +1,6 @@ +module.exports = class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } +}; diff --git a/tests/corpus/6150-cjs-inline-require/reexport-binding.js b/tests/corpus/6150-cjs-inline-require/reexport-binding.js new file mode 100644 index 0000000000..e8307cc659 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/reexport-binding.js @@ -0,0 +1,3 @@ +const inner = require("./inner.js"); +console.log("reexport-binding: evaluated"); +module.exports = inner; diff --git a/tests/corpus/6150-cjs-inline-require/reexport-inline.js b/tests/corpus/6150-cjs-inline-require/reexport-inline.js new file mode 100644 index 0000000000..26b6d44ea6 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/reexport-inline.js @@ -0,0 +1,2 @@ +console.log("reexport-inline: evaluated"); +module.exports = require("./inner.js"); diff --git a/tests/corpus/6150-cjs-inline-require/registry.js b/tests/corpus/6150-cjs-inline-require/registry.js new file mode 100644 index 0000000000..e34b1015fe --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/registry.js @@ -0,0 +1,13 @@ +class Registry { + constructor() { + this.items = []; + } + add(item) { + this.items.push(item); + return this.items.length; + } + size() { + return this.items.length; + } +} +module.exports = new Registry(); diff --git a/tests/corpus/6150-cjs-inline-require/shapes.js b/tests/corpus/6150-cjs-inline-require/shapes.js new file mode 100644 index 0000000000..4dd4103840 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/shapes.js @@ -0,0 +1,9 @@ +class Square { + constructor(side) { + this.side = side; + } + area() { + return this.side * this.side; + } +} +exports.Square = Square; diff --git a/tests/corpus/6150-cjs-inline-require/table.js b/tests/corpus/6150-cjs-inline-require/table.js new file mode 100644 index 0000000000..ce8f1e364b --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/table.js @@ -0,0 +1,4 @@ +function twice(x) { + return x * 2; +} +module.exports = { twice, label: "table" }; diff --git a/tests/corpus/6150-cjs-inline-require/type-error.js b/tests/corpus/6150-cjs-inline-require/type-error.js new file mode 100644 index 0000000000..483613b44c --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/type-error.js @@ -0,0 +1 @@ +module.exports = TypeError; diff --git a/tests/corpus/6150-cjs-inline-require/verify.js b/tests/corpus/6150-cjs-inline-require/verify.js new file mode 100644 index 0000000000..7adb1f64a6 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/verify.js @@ -0,0 +1,4 @@ +console.log("verify: evaluated"); +module.exports = function verify(token) { + return "verified " + token; +}; diff --git a/tests/corpus/6151-cjs-export-chains/chain.js b/tests/corpus/6151-cjs-export-chains/chain.js new file mode 100644 index 0000000000..533e35ba87 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/chain.js @@ -0,0 +1,10 @@ +let evaluations = 0; +function make() { + evaluations++; + return function () { + return "shared"; + }; +} +exports.a = exports.b = make(); +module.exports.c = exports.d = module.exports.e = 5; +exports.evaluations = evaluations; diff --git a/tests/corpus/6151-cjs-export-chains/constants.js b/tests/corpus/6151-cjs-export-chains/constants.js new file mode 100644 index 0000000000..7a67f9f213 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/constants.js @@ -0,0 +1 @@ +exports.VERSION = "1.2.3"; diff --git a/tests/corpus/6151-cjs-export-chains/main.js b/tests/corpus/6151-cjs-export-chains/main.js new file mode 100644 index 0000000000..394c5e3d2e --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/main.js @@ -0,0 +1,9 @@ +// Chained member exports share one value, evaluated once; a fresh +// `exports = module.exports = {}` at the top of a module is a no-op reset. +const chain = require("./chain.js"); +console.log(chain.a(), chain.b(), chain.a === chain.b, chain.evaluations); +console.log(chain.c, chain.d, chain.e); +const re = require("./re.js"); +console.log(re.src.length, re.t.FULL, re.tokens.join(",")); +const sem = require("./sem.js"); +console.log(sem.src.join(" "), sem.re.join(","), sem.count()); diff --git a/tests/corpus/6151-cjs-export-chains/package.json b/tests/corpus/6151-cjs-export-chains/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6151-cjs-export-chains/re.js b/tests/corpus/6151-cjs-export-chains/re.js new file mode 100644 index 0000000000..8839b5cb42 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/re.js @@ -0,0 +1,15 @@ +'use strict' +const constants = require("./constants.js"); +exports = module.exports = {} +const src = []; +const t = {}; +exports.src = src; +exports.t = t; +exports.tokens = []; +function createToken(name, value) { + t[name] = src.length; + src.push(value); + exports.tokens.push(name); +} +createToken("FULL", "^" + constants.VERSION + "$"); +createToken("LOOSE", "v?" + constants.VERSION); diff --git a/tests/corpus/6151-cjs-export-chains/sem.js b/tests/corpus/6151-cjs-export-chains/sem.js new file mode 100644 index 0000000000..f12b5b25fe --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/sem.js @@ -0,0 +1,16 @@ +'use strict' +// semver's internal/re.js shape: export members assigned in value position. +exports = module.exports = {} +const re = exports.re = [] +const src = exports.src = [] +const t = exports.t = {} +let R = 0 +const createToken = (name, value) => { + const index = R++ + t[name] = index + src[index] = value + re[index] = value.length +} +exports.count = () => R +createToken('MAIN', 'x.y.z') +createToken('LOOSE', 'v?x') diff --git a/tests/corpus/6152-cjs-tsc-interop/fn.js b/tests/corpus/6152-cjs-tsc-interop/fn.js new file mode 100644 index 0000000000..09b227f5b8 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/fn.js @@ -0,0 +1,3 @@ +module.exports = function square(x) { + return x * x; +}; diff --git a/tests/corpus/6152-cjs-tsc-interop/main.js b/tests/corpus/6152-cjs-tsc-interop/main.js new file mode 100644 index 0000000000..0924dce356 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/main.js @@ -0,0 +1,17 @@ +"use strict"; +// tsc's CommonJS output: interop helpers over require(), the void 0 +// preamble, and the (0, x.default)() call spelling. +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.run = void 0; +console.log("main: start"); +const plain_1 = __importDefault(require("./plain.js")); +const stamped_1 = __importDefault(require("./stamped.js")); +const fn_1 = __importDefault(require("./fn.js")); +function run() { + return [plain_1.default.greet("a"), (0, stamped_1.default)("b"), stamped_1.helper(), (0, fn_1.default)(2)].join(" "); +} +exports.run = run; +console.log(run(), plain_1.default.count, typeof fn_1.default); diff --git a/tests/corpus/6152-cjs-tsc-interop/package.json b/tests/corpus/6152-cjs-tsc-interop/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6152-cjs-tsc-interop/plain.js b/tests/corpus/6152-cjs-tsc-interop/plain.js new file mode 100644 index 0000000000..60e6732ab2 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/plain.js @@ -0,0 +1,5 @@ +console.log("plain: evaluated"); +exports.greet = function (who) { + return "hello " + who; +}; +exports.count = 3; diff --git a/tests/corpus/6152-cjs-tsc-interop/stamped.js b/tests/corpus/6152-cjs-tsc-interop/stamped.js new file mode 100644 index 0000000000..9e844af055 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/stamped.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.helper = exports.default = void 0; +console.log("stamped: evaluated"); +function greet(who) { + return "stamped " + who; +} +exports.default = greet; +function helper() { + return "helper"; +} +exports.helper = helper; diff --git a/tests/corpus/6153-cjs-exports-object/lib.js b/tests/corpus/6153-cjs-exports-object/lib.js new file mode 100644 index 0000000000..4f69bfca2b --- /dev/null +++ b/tests/corpus/6153-cjs-exports-object/lib.js @@ -0,0 +1,13 @@ +exports.lookup = function (ext) { + return "application/" + ext; +}; +exports.contentType = function (ext) { + return exports.lookup(ext) + "; charset=utf-8"; +}; +exports.describe = function () { + return module.exports.lookup("xml"); +}; +// A later reassignment is what member calls see from then on. +exports.lookup = function (ext) { + return "text/" + ext; +}; diff --git a/tests/corpus/6153-cjs-exports-object/main.js b/tests/corpus/6153-cjs-exports-object/main.js new file mode 100644 index 0000000000..39cf61c6ca --- /dev/null +++ b/tests/corpus/6153-cjs-exports-object/main.js @@ -0,0 +1,19 @@ +// A module's own exports object, used inside the module: identity +// comparisons (the UMD self-test), typeof and truthiness, and member calls. +const lib = require("./lib.js"); +(function () { + var root = this; + console.log("wrapper", root === module.exports, root === exports, typeof root); +}).call(this); +function check() { + return [exports === module.exports, typeof exports, typeof module.exports, !exports].join(" "); +} +console.log(check(), module.exports ? "truthy" : "falsy"); +console.log(lib.contentType("json"), lib.describe()); +exports.answer = function () { + return 42; +}; +function ask() { + return module.exports.answer() + exports.answer(); +} +console.log(exports.answer(), ask()); diff --git a/tests/corpus/6153-cjs-exports-object/package.json b/tests/corpus/6153-cjs-exports-object/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6153-cjs-exports-object/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6154-cjs-require-nested-destructure/constants.js b/tests/corpus/6154-cjs-require-nested-destructure/constants.js new file mode 100644 index 0000000000..6a53c5d81d --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/constants.js @@ -0,0 +1,8 @@ +exports.MAX_LENGTH = 1024; +exports.SEP = "/"; +exports.helper = function (x) { + return x * 3; +}; +exports.bump = function () { + exports.MAX_LENGTH = 2048; +}; diff --git a/tests/corpus/6154-cjs-require-nested-destructure/errors.js b/tests/corpus/6154-cjs-require-nested-destructure/errors.js new file mode 100644 index 0000000000..bdddfec3fa --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/errors.js @@ -0,0 +1,12 @@ +console.log("errors: evaluated"); +const codes = { + ERR_ONE: { code: "ERR_ONE" }, + ERR_TWO: { code: "ERR_TWO" }, +}; +class AbortError extends Error { + constructor() { + super("aborted"); + this.name = "AbortError"; + } +} +module.exports = { codes, AbortError, version: "4.0" }; diff --git a/tests/corpus/6154-cjs-require-nested-destructure/main.js b/tests/corpus/6154-cjs-require-nested-destructure/main.js new file mode 100644 index 0000000000..d928d5a27b --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/main.js @@ -0,0 +1,14 @@ +// Nested patterns under a named export of a program module destructure +// that export's value at the require (a snapshot), while plain keys stay +// live bindings to the export. +console.log("main: start"); +const { codes: { ERR_ONE, ERR_TWO }, AbortError, version } = require("./errors.js"); +console.log(ERR_ONE.code, ERR_TWO.code, new AbortError().name, version); +const errors = require("./errors.js"); +errors.codes.ERR_ONE = { code: "replaced" }; +console.log(ERR_ONE.code, errors.codes.ERR_ONE.code); +// Destructuring a module BINDING snapshots its exports the same way. +const constants = require("./constants.js"); +const { MAX_LENGTH, SEP: sep, helper } = constants; +constants.bump(); +console.log(MAX_LENGTH, constants.MAX_LENGTH, sep, helper(2)); diff --git a/tests/corpus/6154-cjs-require-nested-destructure/package.json b/tests/corpus/6154-cjs-require-nested-destructure/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6155-cjs-export-table-methods/lib.js b/tests/corpus/6155-cjs-export-table-methods/lib.js new file mode 100644 index 0000000000..d8aa56495f --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/lib.js @@ -0,0 +1,20 @@ +console.log("lib: start"); +const WIN = { SEP: "\\" }; +const POSIX = { SEP: "/" }; +function describe() { + return "table"; +} +module.exports = { + scale(x) { + return x * 10; + }, + chars(win32) { + return win32 === true ? WIN.SEP : POSIX.SEP; + }, + get label() { + return describe(); + }, + ...require("./more.js"), + k: (console.log("lib: k entry"), 1), +}; +console.log("lib: end"); diff --git a/tests/corpus/6155-cjs-export-table-methods/main.js b/tests/corpus/6155-cjs-export-table-methods/main.js new file mode 100644 index 0000000000..b2ce150d0c --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/main.js @@ -0,0 +1,5 @@ +// Export tables with method entries, accessors and spreads of required +// modules: the spread evaluates its module at its place in the table. +console.log("main: start"); +const m = require("./lib.js"); +console.log(m.scale(2), m.chars(true), m.chars(false), m.label, m.extra(), m.k); diff --git a/tests/corpus/6155-cjs-export-table-methods/more.js b/tests/corpus/6155-cjs-export-table-methods/more.js new file mode 100644 index 0000000000..4051ce3119 --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/more.js @@ -0,0 +1,4 @@ +console.log("more: evaluated"); +exports.extra = function () { + return "more"; +}; diff --git a/tests/corpus/6155-cjs-export-table-methods/package.json b/tests/corpus/6155-cjs-export-table-methods/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs b/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs new file mode 100644 index 0000000000..12299e189b --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs @@ -0,0 +1,4 @@ +const status = require("./status.cjs"); +exports.describe = function (code) { + return code + " " + status.message[code]; +}; diff --git a/tests/corpus/6156-cjs-function-members-from-ts/main.ts b/tests/corpus/6156-cjs-function-members-from-ts/main.ts new file mode 100644 index 0000000000..070d576ffa --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/main.ts @@ -0,0 +1,9 @@ +// A CommonJS module exporting a function that carries its own members +// (statuses' `status.message = codes; module.exports = status`), read from +// TypeScript through the default import: calls, member reads, member calls. +import status from "./status.cjs"; +import { describe } from "./consumer.cjs"; + +console.log(status(404), status.message[200], status.codes.length, status.isOk(200)); +console.log(status.redirect[301] === true, status.redirect[302]); +console.log(describe(500)); diff --git a/tests/corpus/6156-cjs-function-members-from-ts/package.json b/tests/corpus/6156-cjs-function-members-from-ts/package.json new file mode 100644 index 0000000000..5ffd9800b9 --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6156-cjs-function-members-from-ts/status.cjs b/tests/corpus/6156-cjs-function-members-from-ts/status.cjs new file mode 100644 index 0000000000..7a06d9ab1b --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/status.cjs @@ -0,0 +1,10 @@ +function status(code) { + return code + 1; +} +status.message = { 200: "OK", 404: "Not Found", 500: "Internal Server Error" }; +status.codes = [200, 404, 500]; +status.redirect = { 301: true, 302: true }; +status.isOk = function (code) { + return code === 200; +}; +module.exports = status; diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 18147374d3..5b25b9df65 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2224,6 +2224,13 @@ "6143-cjs-require-json/main.js": 3245, "6144-cjs-require-builtin-nested/main.js": 2181, "6145-cjs-require-cycle/main.js": 3507, + "6150-cjs-inline-require/main.js": 4200, + "6151-cjs-export-chains/main.js": 3000, + "6152-cjs-tsc-interop/main.js": 2600, + "6153-cjs-exports-object/main.js": 2400, + "6154-cjs-require-nested-destructure/main.js": 2800, + "6155-cjs-export-table-methods/main.js": 2600, + "6156-cjs-function-members-from-ts/main.ts": 3000, "700-classes-basic.ts": 2530, "701-classes-composition.ts": 2357, "702-classes-this-capture.ts": 2140, @@ -5264,6 +5271,13 @@ "6143-cjs-require-json/main.js": 3050, "6144-cjs-require-builtin-nested/main.js": 3329, "6145-cjs-require-cycle/main.js": 4761, + "6150-cjs-inline-require/main.js": 9600, + "6151-cjs-export-chains/main.js": 7000, + "6152-cjs-tsc-interop/main.js": 6000, + "6153-cjs-exports-object/main.js": 5600, + "6154-cjs-require-nested-destructure/main.js": 6400, + "6155-cjs-export-table-methods/main.js": 6000, + "6156-cjs-function-members-from-ts/main.ts": 7000, "700-classes-basic.ts": 2674, "701-classes-composition.ts": 2606, "702-classes-this-capture.ts": 2403, diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 51f04d3108..564d53bc33 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -972,7 +972,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () `(function () { var root = this; console.log("before"); - console.log(root === module.exports); + console.log(root === eval("this")); console.log("after"); }).call(this); `, @@ -983,7 +983,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () "speculative-apply", `(function () { console.log("before"); - console.log(typeof module.exports); + console.log(typeof eval("1")); }).apply(null, []); `, "before\n", @@ -994,7 +994,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () `class Base {} class Derived extends (function () { console.log("before"); - console.log(typeof module.exports); + console.log(typeof eval("1")); return Base; })() {} console.log(typeof Derived); @@ -1008,8 +1008,88 @@ console.log(typeof Derived); expect(r.stdout).toBe(stdout); expect(r.stderr).toMatch( new RegExp( - `^Uncaught Error: 'module\\.exports' is part of the standard library types but has no scriptc lowering yet \\[SC2020 at .*${name}\\.cjs:${line}\\]\\n$`, + `^Uncaught Error: values of type '\\(x: string\\) => any' cannot be compiled: .* \\[SC2009 at .*${name}\\.cjs:${line}\\]\\n$`, ), ); }); }); + +// CommonJS forms around the module's exports that keep a refusal name the +// construct itself (they used to report an unlowerable `require` value, a +// "binding form with no lowering", or the ES-module `module` message). +describe("CommonJS export-object and inline require refusals", () => { + test.each([ + [ + "exports-as-argument", + `exports.x = 1; +function keys(o) { + return Object.keys(o); +} +console.log("before"); +console.log(keys(exports)); +`, + {}, + "before\n", + 6, + "'exports' as a value other than a member access, comparison, typeof or truthiness test \\(the compiled exports are a static table, not an object: read exports\\.\\)", + ], + [ + "exports-rebinding", + `console.log("before"); +exports = module.exports = function hash(x) { + return x; +}; +exports.extra = 1; +`, + {}, + "before\n", + 2, + "rebinding 'exports' \\(exports = module\\.exports = value\\)", + ], + [ + "require-computed", + `const name = "./lib.cjs"; +console.log("before"); +console.log(require(name).x); +`, + { "lib.cjs": "exports.x = 1;\n" }, + "before\n", + 3, + "require\\(\\) with a computed specifier", + ], + [ + "exports-call-reads-this", + `exports.timeouts = function (n) { + return this.scale * n; +}; +exports.scale = 2; +console.log("before"); +console.log(exports.timeouts(3)); +`, + {}, + "before\n", + 6, + "calling 'exports\\.timeouts' through the module's exports object when the function may read 'this'", + ], + [ + "require-table-as-value", + `function count(o) { + return Object.keys(o).length; +} +console.log("before"); +console.log(count(require("./lib.cjs"))); +`, + { "lib.cjs": "exports.x = 1;\n" }, + "before\n", + 5, + "the exports object of '\\./lib\\.cjs' as a value", + ], + ] as const)("%s", async (name, source, extra, stdout, line, message) => { + const r = await compileAndRun(name, source, "cjs", extra); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe(stdout); + expect(r.stderr).toMatch( + new RegExp(`^Uncaught Error: ${message}.*\\[SC1090 at .*${name}\\.cjs:${line}\\]\\n$`), + ); + }); +}); diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index a6cdee2b5b..136a58fcde 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -43,15 +43,15 @@ const KNOWN: Record = { "cjs-lexer-invisible": { build: "SC2013" }, "commander-calc": { run: "SC1090", argvs: [["--help"]] }, "commander-calc-async": { run: "SC1090", argvs: [["--help"]] }, - "create-require": { run: "SC2009" }, - "dotfolder": { run: "SC2009" }, + "create-require": { run: "SC1090" }, + "dotfolder": { run: "SC1090" }, "dual-package-kinds": { build: "SC2013" }, "dynamic-import": { build: "SC2012" }, "dynamic-import-local": { build: "SC2012" }, "esbuild-require": { build: "SC2013" }, "esm-missing-default": { build: "SC1090" }, "esm-missing-export": { build: "SC1090" }, - "fetch-destructure": { build: "SC1090" }, + "fetch-destructure": { run: "SC1031" }, "fs-shims": { run: "SC2020" }, "hoisted-workspace": { build: "SC2013" }, "import-refusal": { build: "SC2020" }, @@ -65,7 +65,7 @@ const KNOWN: Record = { "misc-shims": { build: "SC2013" }, "namespace": { build: "SC1013" }, "node24-async-events": { run: "SC2020" }, - "node24-commonjs-metadata": { run: "SC1090" }, + "node24-commonjs-metadata": { run: "SC2020" }, "node24-crypto-kdf": { run: "SC2020" }, "node24-deep-equality": { run: "SC2020" }, "node24-diagnostics-async": { run: "SC1090" },