Answer builtin globals read through a sloppy-mode function's this by key

A sloppy-mode function called with undefined or null as this reads the
global object. Element reads of that this, or of a variable it initializes,
now reach the program's lookup too: a literal key (`this["Map"]`) adds that
name, and a computed key (`this[name]`) installs the lookup for every
declared global, as `globalThis[name]` does. Before, a literal key read the
opaque identity token and a computed key threw SC2020.
This commit is contained in:
Chris Tate
2026-10-28 06:05:39 -05:00
parent efdd53912d
commit 0f47b9cbab
5 changed files with 83 additions and 8 deletions
@@ -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<string> {
const cached = lowerer.sloppyThisGlobalReads.get(sf);
if (cached !== undefined) return cached;
const reads = new Set<string>();
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<string>();
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 {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/sloppy-this-global-computed.cjs"
],
"diags": []
}
@@ -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"));
+7
View File
@@ -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;
}
@@ -0,0 +1 @@
{ "plain": 3300, "san": 4550 }