mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
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:
@@ -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 {
|
||||
|
||||
+6
@@ -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"));
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user