mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Read undeclared literal keys as undefined and keep filled-in JavaScript vars dynamic
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
/* Element reads and writes (`a[i]`, `a[i] = v`), compound element updates, and keyed record reads. */
|
||||
import { OBJECT_PROTOTYPE_MEMBERS } from "../../shape-inference/domain.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
lowerOptionalIndex,
|
||||
@@ -1319,6 +1320,21 @@ export function lowerRecordKeyRead(
|
||||
};
|
||||
return lowerer.maybeNarrow(get, expr);
|
||||
}
|
||||
// A literal key the shape does not declare, on a shape without an index
|
||||
// signature, names no own property of any of its values: the read
|
||||
// answers undefined, as Node's does. The checker's key check cannot vouch
|
||||
// for it here, since the receiver can be typed from a package's
|
||||
// JavaScript while the checker read its declarations. Object.prototype
|
||||
// members keep the keyed read below.
|
||||
if (shape.indexValue === undefined && !OBJECT_PROTOTYPE_MEMBERS.has(litKey)) {
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: obj, loc }],
|
||||
result: dynUndefinedExpr(loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
let key =
|
||||
litKey !== null
|
||||
@@ -1489,7 +1505,7 @@ function lowerRecordPropertyKey(lowerer: Lowerer, key: IrExpr, node: ts.Expressi
|
||||
* exactly the key JS derives; negative/computed keys stay runtime
|
||||
* expressions and canonicalize through ensureString instead). Null for
|
||||
* everything else. */
|
||||
function recordKeyLiteralText(node: ts.Expression): string | null {
|
||||
export function recordKeyLiteralText(node: ts.Expression): string | null {
|
||||
if (ts.isStringLiteral(node)) return node.text;
|
||||
if (ts.isNumericLiteral(node)) return String(Number(node.text));
|
||||
return null;
|
||||
|
||||
@@ -96,7 +96,8 @@ import {
|
||||
lowerUnionKeyedRead,
|
||||
} from "./expressions/property-access.js";
|
||||
import { lowerNullishPair } from "./expressions/binary.js";
|
||||
import { lowerElementCompoundParts } from "./expressions/element-access.js";
|
||||
import { lowerElementCompoundParts, recordKeyLiteralText } from "./expressions/element-access.js";
|
||||
import { OBJECT_PROTOTYPE_MEMBERS } from "../shape-inference/domain.js";
|
||||
export {
|
||||
DEFERRED_READ_MESSAGE,
|
||||
abstractPropertyDeclOf,
|
||||
@@ -1964,6 +1965,20 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr
|
||||
if (shape && !shape.tuple && shape.indexValue && shape.indexValue.kind !== "dyn") {
|
||||
return lowerer.lowerRecordKeyRead(expr, receiverT.shapeId, shape, true);
|
||||
}
|
||||
// A literal key the shape does not declare is absent from every value
|
||||
// (lowerRecordKeyRead answers undefined), whatever type the checker gave
|
||||
// the read.
|
||||
const literal = recordKeyLiteralText(expr.argumentExpression);
|
||||
if (
|
||||
shape &&
|
||||
!shape.tuple &&
|
||||
!shape.indexValue &&
|
||||
literal !== null &&
|
||||
!OBJECT_PROTOTYPE_MEMBERS.has(literal) &&
|
||||
!shape.fields.some((field) => field.name === literal)
|
||||
) {
|
||||
return lowerer.lowerRecordKeyRead(expr, receiverT.shapeId, shape, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// `m[2]` on a JavaScript `RegExpExecArray | null` that a previous
|
||||
|
||||
@@ -1318,6 +1318,7 @@ function jsDeclarationExpandsObject(
|
||||
inferred !== null &&
|
||||
jsOpenObjectType(decl, inferred, lowerer.shapes, lowerer.unions).kind === "dyn") ||
|
||||
arms.some((arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm)) ||
|
||||
arms.some((arm) => arm.kind === "record" && jsObjectLiteralFilledLater(lowerer, decl, arm)) ||
|
||||
(isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
||||
!decl.type &&
|
||||
!hasJsTypeAnnotation(decl) &&
|
||||
@@ -6553,6 +6554,41 @@ function lowerVariableInitializer(
|
||||
}
|
||||
}
|
||||
|
||||
/** A JS object literal that leaves out a required field of the binding's
|
||||
* inferred record: tsc types `var p = {}; p.id = id;` by the members later
|
||||
* assignments add, so the object only gains them after its initializer, and
|
||||
* until then it is not a value of that record. Keep it a checked-dynamic
|
||||
* object, as an object that gains keys outside the record is. */
|
||||
function jsObjectLiteralFilledLater(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
type: IrType & { kind: "record" },
|
||||
): boolean {
|
||||
if (decl.type || !decl.initializer || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return false;
|
||||
let initializer = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(initializer)) initializer = initializer.expression;
|
||||
if (!ts.isObjectLiteralExpression(initializer) || hasJsTypeAnnotation(decl)) return false;
|
||||
const shape = lowerer.shapes.get(type.shapeId);
|
||||
if (!shape || shape.tuple) return false;
|
||||
const given = new Set<string>();
|
||||
for (const property of initializer.properties.nodes) {
|
||||
if (ts.isSpreadAssignment(property)) return false;
|
||||
const name = property.name;
|
||||
if (name === undefined) return false;
|
||||
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
|
||||
given.add(name.text);
|
||||
else return false;
|
||||
}
|
||||
// A field whose type holds undefined reads as it would before the write.
|
||||
const holdsUndefined = (t: IrType): boolean =>
|
||||
t.kind === "dyn" ||
|
||||
t.kind === "undefinedT" ||
|
||||
(t.kind === "union" && lowerer.armTag(t.unionId, UNDEFINED_T) >= 0);
|
||||
return shape.fields.some(
|
||||
(field) => !given.has(field.name) && !field.name.startsWith("%") && !holdsUndefined(field.type),
|
||||
);
|
||||
}
|
||||
|
||||
/** A JS object literal's inferred fields do not close its later storage.
|
||||
* Keep the original native object when writes add keys or delete fields;
|
||||
* converting it to a fixed record would discard that behavior. */
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/var-object-filled-later.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/types-keyed-construction/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/types-undeclared-quoted-key/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// A function-scoped `var` initialized with an object literal that later
|
||||
// assignments fill in: tsc types the binding by the members those
|
||||
// assignments add, but the object only gains them after its initializer.
|
||||
"use strict";
|
||||
function load(id) {
|
||||
var p = {};
|
||||
console.log(p.count, "count" in p, Object.keys(p).length);
|
||||
p.id = id;
|
||||
p.count = id * 2;
|
||||
p.label = "item-" + id;
|
||||
return p;
|
||||
}
|
||||
const p = load(7);
|
||||
console.log(p.id + p.count, p.label.toUpperCase(), JSON.stringify(p), Object.keys(p).join(","));
|
||||
|
||||
function partial(name) {
|
||||
var entry = { name: name };
|
||||
entry.size = name.length;
|
||||
entry.tags = [name, name + "!"];
|
||||
return entry;
|
||||
}
|
||||
const e = partial("abc");
|
||||
console.log(e.size * 10, e.tags.join("|"), JSON.stringify(e));
|
||||
|
||||
function nested(depth) {
|
||||
var node = {};
|
||||
node.depth = depth;
|
||||
node.child = depth > 0 ? nested(depth - 1) : null;
|
||||
return node;
|
||||
}
|
||||
console.log(JSON.stringify(nested(2)));
|
||||
|
||||
function rows(n) {
|
||||
var out = [];
|
||||
for (var i = 0; i < n; i++) {
|
||||
var row = {};
|
||||
row.index = i;
|
||||
row.square = i * i;
|
||||
out.push(row);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
console.log(rows(4).map(function (r) { return r.index + ":" + r.square; }).join(" "));
|
||||
|
||||
var top = {};
|
||||
top.ready = true;
|
||||
top.total = rows(3).reduce(function (s, r) { return s + r.square; }, 0);
|
||||
console.log(top.ready, top.total, JSON.stringify(top));
|
||||
@@ -0,0 +1,12 @@
|
||||
// A plain-JavaScript package typed by an @types package builds its result
|
||||
// key by key (`var p = {}; p.id = id; ...`): the object crosses the
|
||||
// declaration boundary with every field, including the declared fields
|
||||
// whose keys are not identifiers.
|
||||
import { loadProfile, type Profile } from "profilekit";
|
||||
|
||||
const p: Profile = loadProfile(7);
|
||||
console.log(p["a;b"].toUpperCase(), p["a;b"].length);
|
||||
console.log(p["my-key"], p["my-key"].length, p["x y"].length);
|
||||
console.log(p.id, p.count, JSON.stringify(p));
|
||||
const keys: string[] = Object.keys(p);
|
||||
console.log(keys.join("|"));
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export interface Profile {
|
||||
id: number;
|
||||
"a;b": string;
|
||||
"my-key": string;
|
||||
"x y": string;
|
||||
count: number;
|
||||
}
|
||||
export declare function loadProfile(id: number): Profile;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "@types/profilekit", "version": "1.0.0", "types": "index.d.ts" }
|
||||
Generated
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
// Plain JavaScript: the profile is built key by key.
|
||||
function loadProfile(id) {
|
||||
var p = {};
|
||||
p.id = id;
|
||||
p["a;b"] = "semi" + id;
|
||||
p["my-key"] = "dash";
|
||||
p["x y"] = "space";
|
||||
p.count = id * 2;
|
||||
return p;
|
||||
}
|
||||
exports.loadProfile = loadProfile;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "profilekit", "version": "1.0.0", "main": "index.js" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "types-keyed-construction-case", "private": true, "type": "module" }
|
||||
@@ -0,0 +1,20 @@
|
||||
// A package typed from its JavaScript returns profiles without a key its
|
||||
// declarations promise ("a;b"). A literal-key read of that key answers
|
||||
// undefined, as Node's does; it never reads another field.
|
||||
import { loadProfiles } from "hostilekit";
|
||||
|
||||
const profiles = loadProfiles();
|
||||
for (const p of profiles) {
|
||||
console.log(p.id, p["a;b"] === undefined, typeof p["a;b"], "a;b" in p);
|
||||
try {
|
||||
console.log(p["a;b"].toUpperCase());
|
||||
} catch (e) {
|
||||
console.log("upper threw", e instanceof TypeError);
|
||||
}
|
||||
try {
|
||||
console.log(p["a;b"].length);
|
||||
} catch (e) {
|
||||
console.log("length threw", e instanceof TypeError);
|
||||
}
|
||||
console.log(p.id.toFixed(1), JSON.stringify(p));
|
||||
}
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export type Profile = { id: number; "a;b": string };
|
||||
export declare function loadProfiles(): Profile[];
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
function loadProfiles() {
|
||||
return [{ id: 1 }];
|
||||
}
|
||||
exports.loadProfiles = loadProfiles;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "hostilekit", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "types-undeclared-quoted-key-case", "private": true, "type": "module" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3400 }
|
||||
Reference in New Issue
Block a user