mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Compile assignment and update targets wrapped in parentheses or non-null assertions (#1665)
This commit is contained in:
@@ -101,6 +101,8 @@ const cases: Record<string, string> = {
|
||||
'/** @typedef {Object} Options\n * @property {string} name\n * @property {number} count\n */\n/** @type {Options} */ const options = { name: "x", count: 2 };',
|
||||
"missing.ts": "const incomplete = ;\nfunction f( {\n",
|
||||
"empty.ts": "",
|
||||
"targets.ts":
|
||||
"let x: number | undefined = 1;\nconst a = [1];\n(x) = 2;\nx! = 3;\na[0]! += 1;\n(a[0])!++;\n--(x!);\nfor ((x) of a) {}\nconst y = (x!) + 1;\n",
|
||||
"lists.ts":
|
||||
"const trailing = [1, 2 /* last */, ];\nconst plain = [1, 2];\nf(1, 2,);\nexport abstract class Flags { protected static readonly value = 1; }\n",
|
||||
};
|
||||
@@ -167,6 +169,38 @@ function ids(
|
||||
return nodes?.map((n) => (n as OracleNode).index);
|
||||
}
|
||||
|
||||
test("the writeTargets option answers write targets without their wrappers", () => {
|
||||
const { bytes, file: plain } = decoded.get("targets.ts")!;
|
||||
const file = new AstFile(bytes, undefined, { writeTargets: true });
|
||||
const statements = (root: AstNode) =>
|
||||
(root.childList("statements") as AstNodeList).nodes.map((statement) =>
|
||||
statement.kind === SyntaxKind.ExpressionStatement ? statement.expression : statement,
|
||||
);
|
||||
const text = (node: AstNode | undefined) => node?.getText();
|
||||
const writes = statements(file.root);
|
||||
const raw = statements(plain.root);
|
||||
// Assignment left sides, update operands and a for-of head.
|
||||
const targets = [writes[2]!.left, writes[3]!.left, writes[4]!.left, writes[5]!.operand];
|
||||
expect(targets.map(text)).toEqual(["x", "x", "a[0]", "a[0]"]);
|
||||
expect(text(writes[6]!.operand)).toBe("x");
|
||||
expect(text(writes[7]!.initializer)).toBe("x");
|
||||
for (const [index, target] of targets.entries())
|
||||
expect(target!.parent, `target ${index}`).toBe(writes[index + 2]);
|
||||
expect(writes[7]!.initializer!.parent).toBe(writes[7]);
|
||||
// The reference keeps its own source span.
|
||||
expect(writes[4]!.left!.getStart()).toBe(raw[4]!.left!.getStart());
|
||||
expect(writes[4]!.left!.end).toBe(raw[4]!.left!.end - 1);
|
||||
// A wrapper outside a write target is kept.
|
||||
const init = (writes[8]!.childNode("declarationList")!.childList("declarations") as AstNodeList)
|
||||
.nodes[0]!.initializer!;
|
||||
expect(text(init.left)).toBe("(x!)");
|
||||
expect(init.left!.kind).toBe(SyntaxKind.ParenthesizedExpression);
|
||||
// Without the option, the file is TypeScript's exact tree.
|
||||
expect(raw[3]!.left!.kind).toBe(SyntaxKind.NonNullExpression);
|
||||
expect(raw[2]!.left!.kind).toBe(SyntaxKind.ParenthesizedExpression);
|
||||
expect(raw[5]!.operand!.kind).toBe(SyntaxKind.NonNullExpression);
|
||||
});
|
||||
|
||||
test("node lists report a trailing comma and modifier lists their flags", () => {
|
||||
const { file } = decoded.get("lists.ts")!;
|
||||
const lists: [SyntaxKind, boolean][] = [];
|
||||
|
||||
@@ -60,6 +60,18 @@ export class AstModifierList<T extends AstNode = AstNode> extends AstNodeList<T>
|
||||
* list.
|
||||
* Parent access is lazy, so resolving a checker handle near the bottom
|
||||
* of a deep tree never recursively constructs its ancestors. */
|
||||
export interface AstFileOptions {
|
||||
/** Answer the reference an assignment, a `++`/`--` update or a for-in/of
|
||||
* head writes without the parentheses and non-null assertions around it.
|
||||
* TypeScript accepts both around a target (`(x) = 1`, `a[i]! += 1`) and
|
||||
* both are erased at run time. With this option the writer's `left`,
|
||||
* `operand` or `initializer` is the reference itself and the reference's
|
||||
* `parent` is the writer, so every reader of a write target sees one
|
||||
* shape. Positions stay the source's: the reference keeps its own span.
|
||||
* Files read for other purposes keep TypeScript's exact tree. */
|
||||
writeTargets?: boolean;
|
||||
}
|
||||
|
||||
export class AstFile {
|
||||
readonly wire: AstWireFile;
|
||||
readonly root: AstNode;
|
||||
@@ -73,10 +85,16 @@ export class AstFile {
|
||||
private lines: number[] | undefined;
|
||||
private childIndices: Uint32Array | undefined;
|
||||
|
||||
/** Whether nodes answer write targets without their wrappers (see
|
||||
* AstFileOptions.writeTargets). */
|
||||
readonly writeTargets: boolean;
|
||||
|
||||
constructor(
|
||||
bytes: Uint8Array,
|
||||
private readonly materialized?: () => void,
|
||||
options: AstFileOptions = {},
|
||||
) {
|
||||
this.writeTargets = options.writeTargets === true;
|
||||
this.wire = new AstWireFile(bytes);
|
||||
this.nodes = new Array<AstNode>(this.wire.nodeCount);
|
||||
this.lists = new Array<AstNode[]>(this.wire.nodeCount);
|
||||
@@ -192,6 +210,73 @@ export class AstFile {
|
||||
/** Concrete native layout shared by every syntax kind. Kind-specific
|
||||
* properties are views over the wire; they never copy identity-bearing
|
||||
* nodes into structural records. */
|
||||
/* Write targets without their wrappers (AstFileOptions.writeTargets). */
|
||||
|
||||
function isTargetWrapper(node: AstNode): boolean {
|
||||
return node.kind === AstKind.ParenthesizedExpression || node.kind === AstKind.NonNullExpression;
|
||||
}
|
||||
|
||||
function unwrapTarget(node: AstNode): AstNode {
|
||||
let target = node;
|
||||
while (isTargetWrapper(target)) {
|
||||
const inner = target.childNode("expression");
|
||||
if (inner === undefined) break;
|
||||
target = inner;
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/** Whether `field` of `node` is a write target: an assignment's left side,
|
||||
* a `++`/`--` operand, or a for-in/of head that declares nothing. */
|
||||
function writesTarget(node: AstNode, field: "left" | "operand" | "initializer"): boolean {
|
||||
switch (node.kind) {
|
||||
case AstKind.BinaryExpression: {
|
||||
if (field !== "left") return false;
|
||||
const operator = node.childNode("operatorToken")?.kind;
|
||||
return (
|
||||
operator !== undefined &&
|
||||
operator >= AstKind.FirstAssignment &&
|
||||
operator <= AstKind.LastAssignment
|
||||
);
|
||||
}
|
||||
case AstKind.PrefixUnaryExpression:
|
||||
case AstKind.PostfixUnaryExpression:
|
||||
return (
|
||||
field === "operand" &&
|
||||
(node.operator === AstKind.PlusPlusToken || node.operator === AstKind.MinusMinusToken)
|
||||
);
|
||||
case AstKind.ForInStatement:
|
||||
case AstKind.ForOfStatement:
|
||||
return (
|
||||
field === "initializer" &&
|
||||
node.childNode("initializer")?.kind !== AstKind.VariableDeclarationList
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The parent of `node`, whose syntactic parent `wrapper` is a parenthesis
|
||||
* or a non-null assertion: the writer when the wrappers around `node` are
|
||||
* the whole target of an assignment, update or for-in/of head, and
|
||||
* `wrapper` otherwise. */
|
||||
function writerOf(node: AstNode, wrapper: AstNode): AstNode {
|
||||
let outer = wrapper;
|
||||
for (;;) {
|
||||
const up = outer.parent;
|
||||
if (up === undefined) return wrapper;
|
||||
if (isTargetWrapper(up)) {
|
||||
outer = up;
|
||||
continue;
|
||||
}
|
||||
for (const field of ["left", "operand", "initializer"] as const) {
|
||||
if (writesTarget(up, field) && up.childNode(field) === outer && unwrapTarget(outer) === node)
|
||||
return up;
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
export class AstNode {
|
||||
readonly kind: SyntaxKind;
|
||||
readonly pos: number;
|
||||
@@ -221,7 +306,11 @@ export class AstNode {
|
||||
get parent(): AstNode | undefined {
|
||||
if (!this.parentResolved) {
|
||||
const index = this.file.wire.semanticParent(this.index);
|
||||
this.parentCache = index === 0 || index === this.index ? undefined : this.file.node(index);
|
||||
const parent = index === 0 || index === this.index ? undefined : this.file.node(index);
|
||||
this.parentCache =
|
||||
parent !== undefined && this.file.writeTargets && isTargetWrapper(parent)
|
||||
? writerOf(this, parent)
|
||||
: parent;
|
||||
this.parentResolved = true;
|
||||
}
|
||||
return this.parentCache;
|
||||
@@ -612,7 +701,10 @@ export class AstNode {
|
||||
return this.childNode("indexType");
|
||||
}
|
||||
get initializer(): AstNode | undefined {
|
||||
return this.childNode("initializer");
|
||||
const initializer = this.childNode("initializer");
|
||||
return initializer !== undefined && this.file.writeTargets && writesTarget(this, "initializer")
|
||||
? unwrapTarget(initializer)
|
||||
: initializer;
|
||||
}
|
||||
get jsdocPropertyTags(): AstNode | undefined {
|
||||
return this.childNode("jsdocPropertyTags");
|
||||
@@ -621,7 +713,10 @@ export class AstNode {
|
||||
return this.childNode("label");
|
||||
}
|
||||
get left(): AstNode | undefined {
|
||||
return this.childNode("left");
|
||||
const left = this.childNode("left");
|
||||
return left !== undefined && this.file.writeTargets && writesTarget(this, "left")
|
||||
? unwrapTarget(left)
|
||||
: left;
|
||||
}
|
||||
get literal(): AstNode | undefined {
|
||||
return this.childNode("literal");
|
||||
@@ -663,7 +758,10 @@ export class AstNode {
|
||||
return this.childNode("openingFragment");
|
||||
}
|
||||
get operand(): AstNode | undefined {
|
||||
return this.childNode("operand");
|
||||
const operand = this.childNode("operand");
|
||||
return operand !== undefined && this.file.writeTargets && writesTarget(this, "operand")
|
||||
? unwrapTarget(operand)
|
||||
: operand;
|
||||
}
|
||||
get operatorToken(): AstNode | undefined {
|
||||
return this.childNode("operatorToken");
|
||||
|
||||
@@ -298,9 +298,13 @@ export class Ts7SessionProgram {
|
||||
if (source === undefined) {
|
||||
const bytes = this.session.binary("getSourceFile", JSON.stringify(this.query(file)));
|
||||
if (bytes.length !== 0) {
|
||||
const ast = new AstFile(bytes, () => {
|
||||
this.session.timing.materialized();
|
||||
});
|
||||
const ast = new AstFile(
|
||||
bytes,
|
||||
() => {
|
||||
this.session.timing.materialized();
|
||||
},
|
||||
{ writeTargets: true },
|
||||
);
|
||||
this.session.timing.fetched(Math.max(0, ast.wire.nodeCount - 2));
|
||||
source = this.session.cache.set(path, ast.sourceFile, this.snapshot.id, this.project.id);
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/wrapped-assignment-targets.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Assignment, update and for-of targets wrapped in parentheses or non-null
|
||||
// assertions write the reference inside them, as in JavaScript, and the
|
||||
// writes count for every analysis of the binding or member they write.
|
||||
const h = { sums: [1, 2, 3] };
|
||||
const k = 1;
|
||||
h.sums[k]! += 10;
|
||||
console.log(h.sums.join(","));
|
||||
let x = 1;
|
||||
(x) = 2;
|
||||
console.log(x);
|
||||
let y: number | undefined = 1;
|
||||
y! = 5;
|
||||
console.log(y);
|
||||
const a = [1, 2];
|
||||
(a[0]) += 1;
|
||||
a[1]! = 7;
|
||||
a[0]!++;
|
||||
++(a[1]!);
|
||||
console.log(a.join(","));
|
||||
const o: { n?: number } = { n: 1 };
|
||||
o.n! = 4;
|
||||
(o.n)! *= 2;
|
||||
console.log(o.n);
|
||||
let counter = 0;
|
||||
for ((counter) of [3, 4]) {}
|
||||
console.log(counter);
|
||||
const m = new Map<string, number[]>([["a", [1]]]);
|
||||
m.get("a")![0]! += 4;
|
||||
console.log(m.get("a")!.join(","));
|
||||
const r = (a[0]! += 2);
|
||||
console.log(r, a[0]);
|
||||
let limit = 3;
|
||||
function bump(): void {
|
||||
(limit) = 9;
|
||||
}
|
||||
bump();
|
||||
console.log(limit);
|
||||
class Counter {
|
||||
count: number | undefined = 0;
|
||||
readonly fixed = 1;
|
||||
inc(): void {
|
||||
this.count! += 1;
|
||||
(this.count)!++;
|
||||
}
|
||||
}
|
||||
const c = new Counter();
|
||||
c.inc();
|
||||
console.log(c.count, c.fixed);
|
||||
let flag = false;
|
||||
const set = (): void => {
|
||||
flag! = true;
|
||||
};
|
||||
set();
|
||||
console.log(flag);
|
||||
Reference in New Issue
Block a user