mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Compile string-key destructuring of any values and cast-guarded Buffer aliases
Co-authored-by: patlux <4481570+patlux@users.noreply.github.com> Co-authored-by: jlucaso1 <55464917+jlucaso1@users.noreply.github.com>
This commit is contained in:
co-authored by
patlux
jlucaso1
parent
850710fbea
commit
e75174aeea
@@ -744,6 +744,17 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc };
|
||||
const nativeAlias = stdlibGlobalNameOf(lowerer, expr);
|
||||
if (nativeAlias === "Buffer" && expr.text !== "Buffer") {
|
||||
// An alias through a cast of globalThis carries the cast's static type,
|
||||
// which the identity token below does not have.
|
||||
const aliasSymbol = identifierReadSymbol(lowerer, expr);
|
||||
if (aliasSymbol && lowerer.castGlobalAliases.has(aliasSymbol)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"the Buffer constructor read through a cast of globalThis as a value",
|
||||
"test it in a condition, read its typeof, or call its members",
|
||||
);
|
||||
}
|
||||
return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc };
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr, "fetch")) {
|
||||
|
||||
@@ -88,6 +88,12 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
|
||||
if (builtinClassRefOf(lowerer, expr.expression) !== null) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
// The Buffer constructor and its aliases (`const B = globalThis.Buffer`,
|
||||
// `(globalThis as { Buffer?: B }).Buffer`): a value read of an alias is an
|
||||
// identity token, so its typeof is decided here.
|
||||
if (stdlibGlobalNameOf(lowerer, expr.expression) === "Buffer") {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr.expression)) {
|
||||
const member = expr.expression;
|
||||
const prototype = member.expression;
|
||||
|
||||
@@ -191,6 +191,10 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
|
||||
operatorUnsupported(lowerer, expr, "+", [raw.type]);
|
||||
}
|
||||
case ts.SyntaxKind.ExclamationToken: {
|
||||
// `!runtimeBuffer` over an alias of the Buffer global: Node always
|
||||
// defines Buffer (the condition twin is lowerCondition's).
|
||||
if (lowerer.isStdlibGlobal(expr.operand, "Buffer"))
|
||||
return { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
// `!x` is ToBoolean-then-negate: f64/string operands go through toBool.
|
||||
const operand = lowerer.ensureBool(
|
||||
deferredSlotOr(
|
||||
|
||||
@@ -1087,6 +1087,10 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
lowerer.isStdlibGlobal(e.expression, "Object")
|
||||
)
|
||||
return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) };
|
||||
// `if (runtimeBuffer)` over an alias of the Buffer global (a
|
||||
// capability probe): Node always defines Buffer.
|
||||
if (lowerer.isStdlibGlobal(e, "Buffer"))
|
||||
return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) };
|
||||
if (ts.isPropertyAccessExpression(e)) {
|
||||
const presence = lowerPromiseThenPresence(lowerer, e, {
|
||||
kind: "boolLit",
|
||||
|
||||
@@ -8749,13 +8749,20 @@ function destrSpellingOf(rhs: ts.Expression): string | null {
|
||||
return base === null ? null : `${base}.${e.name.text}`;
|
||||
}
|
||||
// `table[key]` and `list[0]`; V8 prints a numeric key in its canonical
|
||||
// number form (`o[1.50]` reads "o[1.5]").
|
||||
// number form (`o[1.50]` reads "o[1.5]"), and a string key as a property
|
||||
// name with its text decoded (`o["a.b"]` and `o[\`a.b\`]` read "o.a.b").
|
||||
// The message is a C string, so a key holding NUL keeps the refusal.
|
||||
if (ts.isElementAccessExpression(e) && !e.questionDotToken) {
|
||||
const base = destrSpellingOf(e.expression);
|
||||
const key = e.argumentExpression;
|
||||
if (base === null) return null;
|
||||
if (ts.isIdentifier(key)) return `${base}[${key.text}]`;
|
||||
if (ts.isNumericLiteral(key)) return `${base}[${String(Number(key.text))}]`;
|
||||
if (
|
||||
(ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) &&
|
||||
!key.text.includes("\0")
|
||||
)
|
||||
return `${base}.${key.text}`;
|
||||
return null;
|
||||
}
|
||||
if (ts.isCallExpression(e) && !e.questionDotToken) {
|
||||
|
||||
@@ -2937,6 +2937,10 @@ export class Lowerer {
|
||||
* checks resolve through this map (stdlibGlobalNameOf), declarations
|
||||
* emit nothing. */
|
||||
readonly stdlibGlobalAliases = new Map<ts.Symbol, string>();
|
||||
/** The stdlibGlobalAliases bound through a cast of the global object
|
||||
* (`(globalThis as { Buffer?: B }).Buffer`). Their static type is the
|
||||
* cast's, so their value reads are refused (identifiers.ts). */
|
||||
readonly castGlobalAliases = new Set<ts.Symbol>();
|
||||
/** CJS export-table ACCESSORS (`module.exports = { get path() {...} }`),
|
||||
* lifted lazily as module-level functions and interned per accessor
|
||||
* declaration: member reads call the getter (lower-exprs). */
|
||||
|
||||
@@ -6,6 +6,20 @@ function unparenthesized(e: ts.Expression): ts.Expression {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** `e` without parentheses and type-only wrappers (as, satisfies, type
|
||||
* assertions, non-null assertions), which change no runtime value. */
|
||||
function withoutTypeCasts(e: ts.Expression): ts.Expression {
|
||||
while (
|
||||
ts.isParenthesizedExpression(e) ||
|
||||
ts.isAsExpression(e) ||
|
||||
ts.isSatisfiesExpression(e) ||
|
||||
ts.isTypeAssertion(e) ||
|
||||
ts.isNonNullExpression(e)
|
||||
)
|
||||
e = e.expression;
|
||||
return e;
|
||||
}
|
||||
import { UNSUPPORTED } from "../../diagnostics/diagnostic.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -3440,6 +3454,17 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
|
||||
return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved);
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
|
||||
// A cast of the global object (`(globalThis as { Buffer?: B }).Buffer`,
|
||||
// `(globalThis as any).Buffer`) changes only its static type: the
|
||||
// member is the global object's own.
|
||||
const uncast = withoutTypeCasts(expr.expression);
|
||||
if (uncast !== unparenthesized(expr.expression)) {
|
||||
if (!ts.isIdentifier(uncast) || stdlibGlobalNameOf(lowerer, uncast) !== "globalThis")
|
||||
return null;
|
||||
const member = lowerer.checker.getPropertyOfType(lowerer.typeOf(uncast), expr.name.text);
|
||||
if (!member || !lowerer.isStdlibSymbol(member)) return null;
|
||||
return ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member);
|
||||
}
|
||||
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null;
|
||||
// Through a detection chain the checker types the receiver as the
|
||||
// union of every arm; the selected arm (the global object) owns the
|
||||
@@ -3572,6 +3597,13 @@ export function stdlibGlobalAliasDecl(
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
if (!symbol) return false;
|
||||
lowerer.stdlibGlobalAliases.set(symbol, name);
|
||||
const access = init !== undefined ? unparenthesized(init) : undefined;
|
||||
if (
|
||||
access !== undefined &&
|
||||
ts.isPropertyAccessExpression(access) &&
|
||||
withoutTypeCasts(access.expression) !== unparenthesized(access.expression)
|
||||
)
|
||||
lowerer.castGlobalAliases.add(symbol);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7309-globalthis-buffer-aliases.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7310-any-destructure-string-keys.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/any-destructure-unspelled-keys.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/globalthis-buffer-alias-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Aliases of the global Buffer constructor read through a cast of
|
||||
// globalThis, the capability-probe spelling of code that also runs where
|
||||
// Buffer is absent: typed and `as any` casts, `if`, ternary, typeof and `!`
|
||||
// guards, member calls through the alias, and typeof of the alias.
|
||||
type RuntimeBuffer = {
|
||||
byteLength(value: string, encoding: "utf8"): number;
|
||||
from(value: string, encoding: "utf8"): Uint8Array;
|
||||
isBuffer(value: unknown): boolean;
|
||||
};
|
||||
|
||||
const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer;
|
||||
const anyBuffer = (globalThis as any).Buffer;
|
||||
const text = "héllo wörld";
|
||||
|
||||
if (runtimeBuffer) {
|
||||
console.log("typed", runtimeBuffer.byteLength(text, "utf8"));
|
||||
} else {
|
||||
console.log("typed: no Buffer");
|
||||
}
|
||||
if (typeof anyBuffer === "function") {
|
||||
console.log("any", anyBuffer.byteLength(text, "utf8"));
|
||||
}
|
||||
const bytes = runtimeBuffer ? runtimeBuffer.from(text, "utf8") : undefined;
|
||||
console.log("from", bytes ? bytes.length : -1);
|
||||
console.log("isBuffer", runtimeBuffer ? runtimeBuffer.isBuffer(bytes) : false);
|
||||
console.log("typeof", typeof runtimeBuffer, typeof anyBuffer, !runtimeBuffer);
|
||||
|
||||
function utf8Length(value: string): number {
|
||||
return runtimeBuffer ? runtimeBuffer.byteLength(value, "utf8") : value.length;
|
||||
}
|
||||
console.log("function", utf8Length("✓✓"), utf8Length("abc"));
|
||||
@@ -0,0 +1,60 @@
|
||||
// Destructuring an `any` source read through a string-literal key
|
||||
// (`const { value } = events["a.b"]`): reads, renames, defaults, rest, a
|
||||
// nested pattern, the assignment form, a template-literal key, chained
|
||||
// string keys, and V8's TypeError for undefined and null sources, whose
|
||||
// message spells the key as a property name with its text decoded.
|
||||
const events: any = {
|
||||
"a.b": { value: 1, label: "first", extra: true, nested: { deep: "d" } },
|
||||
"x-y": { value: 2 },
|
||||
outer: { "in.ner": { value: 3 } },
|
||||
"nul": null,
|
||||
};
|
||||
|
||||
const { value } = events["a.b"];
|
||||
const { value: renamed, missing = "fallback", ...rest } = events["a.b"];
|
||||
const {
|
||||
nested: { deep },
|
||||
} = events["a.b"];
|
||||
console.log(value, renamed, missing, JSON.stringify(rest), deep);
|
||||
|
||||
let assigned: unknown;
|
||||
({ value: assigned } = events[`x-y`]);
|
||||
const { value: chained } = events["outer"]["in.ner"];
|
||||
console.log(assigned, chained);
|
||||
|
||||
function attempt(label: string, f: () => unknown): void {
|
||||
try {
|
||||
console.log(label, f());
|
||||
} catch (e) {
|
||||
console.log(label, e instanceof TypeError, (e as Error).message);
|
||||
}
|
||||
}
|
||||
attempt("missing", () => {
|
||||
const { value } = events["no.such"];
|
||||
return value;
|
||||
});
|
||||
attempt("null", () => {
|
||||
const { value } = events["nul"];
|
||||
return value;
|
||||
});
|
||||
attempt("quotes", () => {
|
||||
const { value } = events["it's \"quoted\""];
|
||||
return value;
|
||||
});
|
||||
attempt("escapes", () => {
|
||||
const { value } = events["tab\there\nnext"];
|
||||
return value;
|
||||
});
|
||||
attempt("accents", () => {
|
||||
const { value } = events["été ✓"];
|
||||
return value;
|
||||
});
|
||||
attempt("template", () => {
|
||||
const { value } = events[`plain`];
|
||||
return value;
|
||||
});
|
||||
attempt("assignment", () => {
|
||||
let target: unknown;
|
||||
({ value: target } = events["gone"]);
|
||||
return target;
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
// Destructuring an `any` source keeps SC1031 where the source has no
|
||||
// compiled spelling for V8's TypeError: a key computed by an expression, an
|
||||
// optional element read, a template key with a substitution, and a string key
|
||||
// holding NUL (the message is a C string). String-literal keys compile
|
||||
// (tests/corpus/7310-any-destructure-string-keys.ts).
|
||||
const events: any = { ab: { value: 1 } };
|
||||
const k = "a";
|
||||
const { value: computed } = events["a" + "b"];
|
||||
const { value: optional } = events?.["ab"];
|
||||
const { value: template } = events[`${k}b`];
|
||||
const { value: nul } = events["a\0b"];
|
||||
console.log(computed, optional, template, nul);
|
||||
@@ -0,0 +1,12 @@
|
||||
// An alias of the Buffer global read through a cast of globalThis compiles
|
||||
// in conditions, typeof and member calls
|
||||
// (tests/corpus/7309-globalthis-buffer-aliases.ts). Its other value uses
|
||||
// keep a named SC1090 refusal: the alias has the cast's static type.
|
||||
type RuntimeBuffer = { from(value: string, encoding: "utf8"): Uint8Array };
|
||||
const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer;
|
||||
function keep(value: unknown): unknown {
|
||||
return value;
|
||||
}
|
||||
const bytes = runtimeBuffer && runtimeBuffer.from("abc", "utf8");
|
||||
keep(runtimeBuffer);
|
||||
keep(bytes);
|
||||
@@ -0,0 +1,27 @@
|
||||
any-destructure-unspelled-keys.ts:8:29 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet
|
||||
|
||||
7 | const k = "a";
|
||||
8 | const { value: computed } = events["a" + "b"];
|
||||
| ^~~~~~~~~~~~~~~~~
|
||||
9 | const { value: optional } = events?.["ab"];
|
||||
|
||||
any-destructure-unspelled-keys.ts:9:29 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet
|
||||
|
||||
8 | const { value: computed } = events["a" + "b"];
|
||||
9 | const { value: optional } = events?.["ab"];
|
||||
| ^~~~~~~~~~~~~~
|
||||
10 | const { value: template } = events[`${k}b`];
|
||||
|
||||
any-destructure-unspelled-keys.ts:10:29 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet
|
||||
|
||||
9 | const { value: optional } = events?.["ab"];
|
||||
10 | const { value: template } = events[`${k}b`];
|
||||
| ^~~~~~~~~~~~~~~
|
||||
11 | const { value: nul } = events["a\0b"];
|
||||
|
||||
any-destructure-unspelled-keys.ts:11:24 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet
|
||||
|
||||
10 | const { value: template } = events[`${k}b`];
|
||||
11 | const { value: nul } = events["a\0b"];
|
||||
| ^~~~~~~~~~~~~~
|
||||
12 | console.log(computed, optional, template, nul);
|
||||
@@ -0,0 +1,17 @@
|
||||
globalthis-buffer-alias-values.ts:10:15 - error SC1090: the Buffer constructor read through a cast of globalThis as a value is not supported yet
|
||||
|
||||
9 | }
|
||||
10 | const bytes = runtimeBuffer && runtimeBuffer.from("abc", "utf8");
|
||||
| ^~~~~~~~~~~~~
|
||||
11 | keep(runtimeBuffer);
|
||||
|
||||
hint: test it in a condition, read its typeof, or call its members
|
||||
|
||||
globalthis-buffer-alias-values.ts:11:6 - error SC1090: the Buffer constructor read through a cast of globalThis as a value is not supported yet
|
||||
|
||||
10 | const bytes = runtimeBuffer && runtimeBuffer.from("abc", "utf8");
|
||||
11 | keep(runtimeBuffer);
|
||||
| ^~~~~~~~~~~~~
|
||||
12 | keep(bytes);
|
||||
|
||||
hint: test it in a condition, read its typeof, or call its members
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2600, "san": 3400 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1500, "san": 3900 }
|
||||
Reference in New Issue
Block a user