Skip to content

Commit 3bd330a

Browse files
feat: Better resolution error hints (#2404)
1 parent 7be93f6 commit 3bd330a

12 files changed

Lines changed: 739 additions & 53 deletions
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import * as tinyest from 'tinyest';
2+
3+
const { NodeTypeCatalog: NODE } = tinyest;
4+
5+
export function stringifyNode(node: tinyest.AnyNode): string {
6+
if (isExpression(node)) {
7+
return stringifyExpression(node, '');
8+
}
9+
return stringifyStatement(node, '');
10+
}
11+
12+
function stringifyStatement(node: tinyest.Statement, ident: string): string {
13+
if (isExpression(node)) {
14+
return `${ident}${stringifyExpression(node, ident)};`;
15+
}
16+
17+
if (node[0] === NODE.block) {
18+
const statements = node[1].map((n) => stringifyStatement(n, ident + ' '));
19+
return `{\n${statements.join('\n')}\n${ident}}`;
20+
}
21+
22+
if (node[0] === NODE.return) {
23+
const expr = node[1] === undefined ? '' : ` ${stringifyExpression(node[1], '')}`;
24+
return `${ident}return${expr};`;
25+
}
26+
27+
if (node[0] === NODE.if) {
28+
const cond = stringifyExpression(node[1], ident);
29+
const then = stringifyStatement(node[2], ident);
30+
const base = `${ident}if (${cond}) ${then}`;
31+
if (node[3] !== undefined) {
32+
return `${base} else ${stringifyStatement(node[3], ident)}`;
33+
}
34+
return base;
35+
}
36+
37+
if (node[0] === NODE.let) {
38+
if (node[2] !== undefined) {
39+
return `${ident}let ${node[1]} = ${stringifyExpression(node[2], ident)};`;
40+
}
41+
return `${ident}let ${node[1]};`;
42+
}
43+
44+
if (node[0] === NODE.const) {
45+
if (node[2] !== undefined) {
46+
return `${ident}const ${node[1]} = ${stringifyExpression(node[2], ident)};`;
47+
}
48+
return `${ident}const ${node[1]};`;
49+
}
50+
51+
if (node[0] === NODE.for) {
52+
const init = node[1] ? stringifyStatement(node[1], '') : ';';
53+
const cond = node[2] ? stringifyExpression(node[2], ident) : '';
54+
const update = node[3] ? stringifyStatement(node[3], '') : '';
55+
const body = stringifyStatement(node[4], ident);
56+
return `${ident}for (${init} ${cond}; ${update.slice(0, -1) /* trim the ';' */}) ${body}`;
57+
}
58+
59+
if (node[0] === NODE.while) {
60+
const cond = stringifyExpression(node[1], ident);
61+
const body = stringifyStatement(node[2], ident);
62+
return `${ident}while (${cond}) ${body}`;
63+
}
64+
65+
if (node[0] === NODE.continue) {
66+
return `${ident}continue;`;
67+
}
68+
69+
if (node[0] === NODE.break) {
70+
return `${ident}break;`;
71+
}
72+
73+
if (node[0] === NODE.forOf) {
74+
const leftKind = node[1][0] === NODE.const ? 'const' : 'let';
75+
const leftName = node[1][1];
76+
const right = stringifyExpression(node[2], ident);
77+
const body = stringifyStatement(node[3], ident);
78+
return `${ident}for (${leftKind} ${leftName} of ${right}) ${body}`;
79+
}
80+
81+
assertExhaustive(node);
82+
}
83+
84+
function stringifyExpression(node: tinyest.Expression, ident: string): string {
85+
if (typeof node === 'string') {
86+
return node;
87+
}
88+
89+
if (typeof node === 'boolean') {
90+
return `${node}`;
91+
}
92+
93+
if (node[0] === NODE.numericLiteral) {
94+
return node[1];
95+
}
96+
97+
if (node[0] === NODE.stringLiteral) {
98+
return JSON.stringify(node[1]);
99+
}
100+
101+
if (node[0] === NODE.arrayExpr) {
102+
const elements = node[1].map((n) => stringifyExpression(n, ident));
103+
return `[${elements.join(', ')}]`;
104+
}
105+
106+
if (node[0] === NODE.binaryExpr) {
107+
return `${wrapIfComplex(node[1], ident)} ${node[2]} ${wrapIfComplex(node[3], ident)}`;
108+
}
109+
110+
if (node[0] === NODE.assignmentExpr) {
111+
return `${stringifyExpression(node[1], ident)} ${node[2]} ${stringifyExpression(node[3], ident)}`;
112+
}
113+
114+
if (node[0] === NODE.logicalExpr) {
115+
return `${wrapIfComplex(node[1], ident)} ${node[2]} ${wrapIfComplex(node[3], ident)}`;
116+
}
117+
118+
if (node[0] === NODE.unaryExpr) {
119+
// Unary word operators like void, instanceof and delete require a space
120+
const sep = node[1].length > 1 ? ' ' : '';
121+
return `${node[1]}${sep}${wrapIfComplex(node[2], ident)}`;
122+
}
123+
124+
if (node[0] === NODE.call) {
125+
const callee = wrapIfComplex(node[1], ident);
126+
const args = node[2].map((n) => stringifyExpression(n, ident)).join(', ');
127+
return `${callee}(${args})`;
128+
}
129+
130+
if (node[0] === NODE.memberAccess) {
131+
if (Array.isArray(node[1]) && node[1][0] === NODE.numericLiteral) {
132+
return `(${stringifyExpression(node[1], ident)}).${node[2]}`;
133+
}
134+
return `${wrapIfComplex(node[1], ident)}.${node[2]}`;
135+
}
136+
137+
if (node[0] === NODE.indexAccess) {
138+
return `${wrapIfComplex(node[1], ident)}[${stringifyExpression(node[2], ident)}]`;
139+
}
140+
141+
if (node[0] === NODE.preUpdate) {
142+
return `${node[1]}${wrapIfComplex(node[2], ident)}`;
143+
}
144+
145+
if (node[0] === NODE.postUpdate) {
146+
return `${wrapIfComplex(node[2], ident)}${node[1]}`;
147+
}
148+
149+
if (node[0] === NODE.objectExpr) {
150+
const entries = Object.entries(node[1]).map(
151+
([key, val]) => `${key}: ${stringifyExpression(val, ident)}`,
152+
);
153+
return `{ ${entries.join(', ')} }`;
154+
}
155+
156+
if (node[0] === NODE.conditionalExpr) {
157+
return `${wrapIfComplex(node[1], ident)} ? ${wrapIfComplex(node[2], ident)} : ${wrapIfComplex(node[3], ident)}`;
158+
}
159+
160+
assertExhaustive(node);
161+
}
162+
163+
function assertExhaustive(value: never): never {
164+
throw new Error(`'${JSON.stringify(value)}' was not handled by the stringify function.`);
165+
}
166+
167+
function isExpression(node: tinyest.AnyNode): node is tinyest.Expression {
168+
if (
169+
typeof node === 'string' ||
170+
typeof node === 'boolean' ||
171+
node[0] === NODE.numericLiteral ||
172+
node[0] === NODE.stringLiteral ||
173+
node[0] === NODE.arrayExpr ||
174+
node[0] === NODE.binaryExpr ||
175+
node[0] === NODE.assignmentExpr ||
176+
node[0] === NODE.logicalExpr ||
177+
node[0] === NODE.unaryExpr ||
178+
node[0] === NODE.call ||
179+
node[0] === NODE.memberAccess ||
180+
node[0] === NODE.indexAccess ||
181+
node[0] === NODE.preUpdate ||
182+
node[0] === NODE.postUpdate ||
183+
node[0] === NODE.objectExpr ||
184+
node[0] === NODE.conditionalExpr
185+
) {
186+
node satisfies tinyest.Expression;
187+
return true;
188+
}
189+
node satisfies Exclude<tinyest.AnyNode, tinyest.Expression>;
190+
return false;
191+
}
192+
193+
const SIMPLE_NODES: number[] = [
194+
NODE.memberAccess, // highest precedence
195+
NODE.indexAccess, // highest precedence
196+
NODE.call, // highest precedence
197+
NODE.arrayExpr, // [] make things not ambiguous
198+
NODE.stringLiteral,
199+
NODE.numericLiteral,
200+
];
201+
/**
202+
* Stringifies expression, and wraps it in parentheses if they cannot be trivially omitted
203+
*/
204+
function wrapIfComplex(node: tinyest.Expression, ident: string): string {
205+
const s = stringifyExpression(node, ident);
206+
if (typeof node === 'string' || typeof node === 'boolean') {
207+
return s;
208+
}
209+
if (SIMPLE_NODES.includes(node[0])) {
210+
return s;
211+
}
212+
return `(${s})`;
213+
}

0 commit comments

Comments
 (0)