diff --git a/packages/compiler/src/frontend/lowering/global-values.ts b/packages/compiler/src/frontend/lowering/global-values.ts index 6fc3cb694..af52fe94e 100644 --- a/packages/compiler/src/frontend/lowering/global-values.ts +++ b/packages/compiler/src/frontend/lowering/global-values.ts @@ -168,16 +168,22 @@ const URI_FUNCTIONS = new Set([ ]); /** The builtin globals that sloppy-mode functions of the JavaScript source - * `sf` read through their `this` (`this.Array`) or through a variable it - * initializes (`var root = this; root.Array`). Called with undefined or null - * as `this`, such a function reads them on the global object, which answers - * them through the program's lookup. */ + * `sf` read through their `this` (`this.Array`, `this["Array"]`) or through + * a variable it initializes (`var root = this; root.Array`); every declared + * global when such a read takes a computed key (`this[name]`). Called with + * undefined or null as `this`, such a function reads them on the global + * object, which answers them through the program's lookup. */ export function sloppyThisGlobalReadsOf(lowerer: Lowerer, sf: ts.SourceFile): ReadonlySet { const cached = lowerer.sloppyThisGlobalReads.get(sf); if (cached !== undefined) return cached; const reads = new Set(); if (isJsSourceFile(sf)) { const declared = new Set(declaredGlobalNames(lowerer, sf)); + // Element reads of `this` or of a variable, and the names of the + // variables initialized with `this`: the checker resolves only the + // element reads of `this` and of those variables. + const elementReads: ts.ElementAccessExpression[] = []; + const thisAliases = new Set(); ts.walkPreorder(sf, (node) => { if ( ts.isPropertyAccessExpression(node) && @@ -186,8 +192,32 @@ export function sloppyThisGlobalReadsOf(lowerer: Lowerer, sf: ts.SourceFile): Re readsSloppyThis(lowerer, node.expression) ) reads.add(node.name.text); + else if (ts.isElementAccessExpression(node)) { + const target = withoutParentheses(node.expression); + if (target.kind === ts.SyntaxKind.ThisKeyword || ts.isIdentifier(target)) + elementReads.push(node); + } else if ( + ts.isVariableDeclaration(node) && + ts.isIdentifier(node.name) && + node.initializer !== undefined && + withoutParentheses(node.initializer).kind === ts.SyntaxKind.ThisKeyword + ) + thisAliases.add(node.name.text); return undefined; }); + for (const node of elementReads) { + const target = withoutParentheses(node.expression); + if (ts.isIdentifier(target) && !thisAliases.has(target.text)) continue; + if (!readsSloppyThis(lowerer, node.expression) || !readsBuiltinByName(lowerer, node)) + continue; + const key = withoutParentheses(node.argumentExpression); + if (ts.isStringLiteralLike(key)) { + if (declared.has(key.text)) reads.add(key.text); + } else if (!ts.isNumericLiteral(key)) { + for (const name of declared) reads.add(name); + break; + } + } } lowerer.sloppyThisGlobalReads.set(sf, reads); return reads; @@ -196,8 +226,7 @@ export function sloppyThisGlobalReadsOf(lowerer: Lowerer, sf: ts.SourceFile): Re /** Whether `expr` is the `this` of a sloppy-mode function, or a variable * whose only declaration initializes it with that `this`. */ function readsSloppyThis(lowerer: Lowerer, expr: ts.Expression): boolean { - let inner = expr; - while (ts.isParenthesizedExpression(inner)) inner = inner.expression; + const inner = withoutParentheses(expr); if (inner.kind === ts.SyntaxKind.ThisKeyword) return sloppyReceiverOf(inner); if (!ts.isIdentifier(inner)) return false; const declarations = lowerer.checker.getSymbolAtLocation(inner)?.declarations ?? []; @@ -208,11 +237,17 @@ function readsSloppyThis(lowerer: Lowerer, expr: ts.Expression): boolean { !ts.isVariableDeclaration(declaration) ) return false; - let init = declaration.initializer; - while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression; + const init = + declaration.initializer === undefined ? undefined : withoutParentheses(declaration.initializer); return init !== undefined && init.kind === ts.SyntaxKind.ThisKeyword && sloppyReceiverOf(init); } +function withoutParentheses(expr: ts.Expression): ts.Expression { + let inner = expr; + while (ts.isParenthesizedExpression(inner)) inner = inner.expression; + return inner; +} + /** Whether the `this` at `node` is a sloppy-mode function's receiver * (lower-functions.ts declareCallReceiver): arrows take the enclosing one. */ function sloppyReceiverOf(node: ts.Node): boolean { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/sloppy-this-global-computed.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/sloppy-this-global-computed.cjs.json new file mode 100644 index 000000000..ba45eb398 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/sloppy-this-global-computed.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/sloppy-this-global-computed.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/sloppy-this-global-computed.cjs b/tests/corpus/sloppy-this-global-computed.cjs new file mode 100644 index 000000000..ac526e086 --- /dev/null +++ b/tests/corpus/sloppy-this-global-computed.cjs @@ -0,0 +1,26 @@ +// A sloppy-mode function called with undefined or null as its receiver reads +// builtin globals on the global object by computed keys, directly and through +// a variable its `this` initializes. + +function readByKey(key) { + return this[key]; +} +function aliasedRead(key) { + var root = this; + return root[key]; +} + +console.log(readByKey("Array") === Array, readByKey("JSON") === JSON); +console.log(aliasedRead("Map") === Map, typeof aliasedRead("setTimeout")); +console.log(readByKey.call(null, "Promise") === Promise, readByKey.call({ Promise: 1 }, "Promise")); +console.log(readByKey("globalThis") === globalThis, readByKey("missingGlobal")); + +const names = ["Object", "Symbol", "Uint8Array"]; +const values = [Object, Symbol, Uint8Array]; +console.log(names.map((name, i) => readByKey(name) === values[i])); + +function strictRead(key) { + "use strict"; + return this === undefined ? "no receiver" : this[key]; +} +console.log(strictRead("Array"), strictRead.call({ Array: 2 }, "Array")); diff --git a/tests/corpus/sloppy-this-global.cjs b/tests/corpus/sloppy-this-global.cjs index dfca51b30..ca7f212db 100644 --- a/tests/corpus/sloppy-this-global.cjs +++ b/tests/corpus/sloppy-this-global.cjs @@ -19,6 +19,13 @@ function aliasedRoot() { } console.log(readBuiltins(), aliasedRoot()); +// Builtins read through the global `this` by literal keys. +function readByLiteral() { + var root = this; + return [this["Map"] === Map, root["Math"] === Math, typeof this[`Set`], this[0]]; +} +console.log(readByLiteral(), readByLiteral.call({ Map: 1 })[0]); + function receiver() { return this; } diff --git a/tests/harness/corpus-costs/sloppy-this-global-computed.cjs.json b/tests/harness/corpus-costs/sloppy-this-global-computed.cjs.json new file mode 100644 index 000000000..2249e0825 --- /dev/null +++ b/tests/harness/corpus-costs/sloppy-this-global-computed.cjs.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4550 }