jslike 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/wang-validate.js +4 -5
- package/src/index.js +2 -197
- package/src/interpreter/interpreter.js +356 -3
- package/src/validator/index.js +2 -5
package/package.json
CHANGED
package/src/cli/wang-validate.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { readFile } from 'fs/promises';
|
|
4
4
|
import { stdin } from 'process';
|
|
5
5
|
import { parse } from 'acorn';
|
|
6
|
-
import {
|
|
6
|
+
import { isTopLevelAwait } from '../index.js';
|
|
7
7
|
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
|
|
@@ -59,12 +59,11 @@ async function validate() {
|
|
|
59
59
|
process.exit(1);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
const hasAwait = isTopLevelAwait(processedCode);
|
|
62
|
+
// Check for top-level await
|
|
63
|
+
const hasAwait = isTopLevelAwait(code);
|
|
65
64
|
|
|
66
65
|
// Parse the code to validate syntax
|
|
67
|
-
const ast = parse(
|
|
66
|
+
const ast = parse(code, {
|
|
68
67
|
ecmaVersion: 'latest',
|
|
69
68
|
sourceType: hasAwait ? 'module' : 'script',
|
|
70
69
|
locations: true
|
package/src/index.js
CHANGED
|
@@ -23,212 +23,18 @@ function containsModuleSyntax(code) {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// Pre-process code to fix common patterns that fail in strict mode
|
|
27
|
-
function preprocessCode(code) {
|
|
28
|
-
// Pattern: object literal at statement level with commas
|
|
29
|
-
// Example: let arr = [1,2,3]\n{ first: first(arr), last: last(arr) }
|
|
30
|
-
// This fails to parse because labeled statements can't have commas
|
|
31
|
-
// We need to wrap it in parentheses to make it an expression: ({ ... })
|
|
32
|
-
|
|
33
|
-
const lines = code.split('\n');
|
|
34
|
-
const result = [];
|
|
35
|
-
|
|
36
|
-
for (let i = 0; i < lines.length; i++) {
|
|
37
|
-
let line = lines[i];
|
|
38
|
-
const trimmed = line.trim();
|
|
39
|
-
|
|
40
|
-
// Handle array literals and parenthesized expressions at statement level
|
|
41
|
-
// Add semicolon to previous line to prevent ASI issues
|
|
42
|
-
// Example: let x = 10\n[a, b] parses as: let x = 10[a, b] without semicolon
|
|
43
|
-
// Example: let x = 2\n({ y: 3 }) parses as: let x = 2({ y: 3 }) without semicolon
|
|
44
|
-
// But don't add if previous line ends with an operator (continuing expression)
|
|
45
|
-
// Note: We DON'T handle standalone `{` here because it's too ambiguous (could be a block)
|
|
46
|
-
if ((trimmed.startsWith('[') || trimmed.startsWith('(')) && result.length > 0) {
|
|
47
|
-
// Skip back over empty lines and comments to find the last real statement
|
|
48
|
-
let targetIdx = result.length - 1;
|
|
49
|
-
while (targetIdx >= 0) {
|
|
50
|
-
const checkLine = result[targetIdx].trim();
|
|
51
|
-
if (checkLine && !checkLine.startsWith('//') && !checkLine.startsWith('/*')) {
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
targetIdx--;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (targetIdx >= 0) {
|
|
58
|
-
const prevLine = result[targetIdx];
|
|
59
|
-
const prevTrimmed = prevLine.trim();
|
|
60
|
-
// Don't add semicolon if previous line ends with an operator, block opener, or already has semicolon
|
|
61
|
-
// But / at the end might be a regex literal, not division operator
|
|
62
|
-
const endsWithOperator = /[+\-*%&|^<>=!?:]$/.test(prevTrimmed);
|
|
63
|
-
// Check for regex literal at end: /pattern/flags
|
|
64
|
-
const endsWithRegex = /\/[gims]*$/.test(prevTrimmed);
|
|
65
|
-
// Don't add if previous line looks like a control statement (if, while, etc.)
|
|
66
|
-
const isControlStatement = /^\s*(if|else|for|while|do|try|catch|finally|switch|function|class)\b/.test(prevTrimmed);
|
|
67
|
-
if (prevTrimmed && !prevTrimmed.endsWith(';') && !prevTrimmed.endsWith('{') &&
|
|
68
|
-
!prevTrimmed.endsWith('(') && !prevTrimmed.endsWith(',') && !endsWithOperator && !isControlStatement) {
|
|
69
|
-
result[targetIdx] = prevLine + ';';
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Handle inline object literals at statement level: { key: value, key2: value2 }
|
|
75
|
-
// Pattern: line has {...} with comma and/or colon inside (object-like)
|
|
76
|
-
// This needs to be wrapped in parens to avoid being parsed as a block
|
|
77
|
-
const hasInlineBlock = trimmed.match(/^(.*?)\s*\{([^{}]+)\}\s*$/);
|
|
78
|
-
if (hasInlineBlock && !trimmed.match(/^\s*(if|else|for|while|do|try|catch|finally|switch|function|class)\b/)) {
|
|
79
|
-
const before = hasInlineBlock[1].trim();
|
|
80
|
-
const inside = hasInlineBlock[2];
|
|
81
|
-
|
|
82
|
-
// Check if inside looks like object literal (has comma and/or colon)
|
|
83
|
-
const hasComma = inside.includes(',');
|
|
84
|
-
const hasColon = inside.includes(':');
|
|
85
|
-
|
|
86
|
-
// If nothing before or just await/return, and inside has object-like syntax, wrap it
|
|
87
|
-
if ((!before || before.match(/^(await|return)$/)) && (hasComma || hasColon)) {
|
|
88
|
-
// Check if previous line indicates we're inside an array/object (don't wrap in that case)
|
|
89
|
-
if (result.length > 0) {
|
|
90
|
-
const prevLine = result[result.length - 1];
|
|
91
|
-
const prevTrimmed = prevLine.trim();
|
|
92
|
-
// Skip wrapping if inside array or object literal
|
|
93
|
-
if (prevTrimmed.endsWith(',') || prevTrimmed.endsWith('[') || prevTrimmed.endsWith('{')) {
|
|
94
|
-
result.push(line);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Add semicolon to previous line to avoid ASI issues
|
|
100
|
-
// Skip back over empty lines and comments
|
|
101
|
-
if (result.length > 0) {
|
|
102
|
-
let targetIdx = result.length - 1;
|
|
103
|
-
while (targetIdx >= 0) {
|
|
104
|
-
const checkLine = result[targetIdx].trim();
|
|
105
|
-
if (checkLine && !checkLine.startsWith('//') && !checkLine.startsWith('/*')) {
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
targetIdx--;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (targetIdx >= 0) {
|
|
112
|
-
const prevLine = result[targetIdx];
|
|
113
|
-
const prevTrimmed = prevLine.trim();
|
|
114
|
-
if (prevTrimmed && !prevTrimmed.endsWith(';') && !prevTrimmed.endsWith('{')) {
|
|
115
|
-
result[targetIdx] = prevLine + ';';
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Wrap just the {...} part
|
|
121
|
-
const objPart = trimmed.substring(trimmed.indexOf('{'));
|
|
122
|
-
const prefix = trimmed.substring(0, trimmed.indexOf('{'));
|
|
123
|
-
line = prefix + '(' + objPart + ')';
|
|
124
|
-
result.push(line);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Check if this line starts a block that looks like an object literal
|
|
130
|
-
// BUT: don't wrap if previous line is a function declaration
|
|
131
|
-
if (trimmed === '{' && i < lines.length - 1) {
|
|
132
|
-
// Check if previous line is a function declaration
|
|
133
|
-
if (result.length > 0) {
|
|
134
|
-
const prevTrimmed = result[result.length - 1].trim();
|
|
135
|
-
if (prevTrimmed.match(/^\s*(async\s+)?function\s*\w*\s*\([^)]*\)\s*$/) ||
|
|
136
|
-
prevTrimmed.match(/\)\s*=>\s*$/) ||
|
|
137
|
-
prevTrimmed.match(/\s+(if|else|for|while|do|try|catch|finally|switch)\s*\([^)]*\)\s*$/)) {
|
|
138
|
-
// This is a function/control structure body, not an object literal
|
|
139
|
-
result.push(line);
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Look ahead to see if block contains colons and commas (object literal pattern)
|
|
145
|
-
let blockLines = [line];
|
|
146
|
-
let j = i + 1;
|
|
147
|
-
let braceCount = 1;
|
|
148
|
-
let hasColon = false;
|
|
149
|
-
let hasComma = false;
|
|
150
|
-
|
|
151
|
-
// Collect the block
|
|
152
|
-
while (j < lines.length && braceCount > 0) {
|
|
153
|
-
const nextLine = lines[j];
|
|
154
|
-
blockLines.push(nextLine);
|
|
155
|
-
|
|
156
|
-
// Count braces (simple heuristic, not perfect but good enough)
|
|
157
|
-
for (const char of nextLine) {
|
|
158
|
-
if (char === '{') braceCount++;
|
|
159
|
-
if (char === '}') braceCount--;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Check for object literal indicators
|
|
163
|
-
if (nextLine.includes(':')) hasColon = true;
|
|
164
|
-
if (nextLine.includes(',')) hasComma = true;
|
|
165
|
-
|
|
166
|
-
j++;
|
|
167
|
-
if (braceCount === 0) break;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// If block looks like object literal (has both : and ,), wrap in parens
|
|
171
|
-
if (hasColon && hasComma) {
|
|
172
|
-
// Check if previous line needs a semicolon to avoid ASI issues
|
|
173
|
-
// (wrapping in parens can cause previous expression to be treated as function call)
|
|
174
|
-
// Skip back over empty lines and comments to find the last real statement
|
|
175
|
-
if (result.length > 0) {
|
|
176
|
-
let targetIdx = result.length - 1;
|
|
177
|
-
while (targetIdx >= 0) {
|
|
178
|
-
const checkLine = result[targetIdx].trim();
|
|
179
|
-
if (checkLine && !checkLine.startsWith('//') && !checkLine.startsWith('/*')) {
|
|
180
|
-
// Found a non-empty, non-comment line
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
targetIdx--;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (targetIdx >= 0) {
|
|
187
|
-
const targetLine = result[targetIdx];
|
|
188
|
-
const targetTrimmed = targetLine.trim();
|
|
189
|
-
// Add semicolon if line doesn't end with one
|
|
190
|
-
// Note: Even if it ends with }, we need semicolon (could be object literal)
|
|
191
|
-
// Only skip if it ends with ; or { (block statement opener)
|
|
192
|
-
if (targetTrimmed && !targetTrimmed.endsWith(';') && !targetTrimmed.endsWith('{')) {
|
|
193
|
-
result[targetIdx] = targetLine + ';';
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
result.push('(' + blockLines[0]);
|
|
199
|
-
for (let k = 1; k < blockLines.length - 1; k++) {
|
|
200
|
-
result.push(blockLines[k]);
|
|
201
|
-
}
|
|
202
|
-
// Add closing paren after closing brace
|
|
203
|
-
const lastLine = blockLines[blockLines.length - 1];
|
|
204
|
-
result.push(lastLine.replace('}', '})'));
|
|
205
|
-
|
|
206
|
-
// Skip the lines we already processed
|
|
207
|
-
i = j - 1;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
result.push(line);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return result.join('\n');
|
|
216
|
-
}
|
|
217
26
|
|
|
218
27
|
export function parse(code, options = {}) {
|
|
219
|
-
// Pre-process code to handle patterns that fail in strict mode
|
|
220
|
-
const processedCode = preprocessCode(code);
|
|
221
|
-
|
|
222
28
|
// Determine sourceType: use 'module' ONLY if explicitly requested or if code has imports/exports
|
|
223
29
|
// Default to 'script' for better compatibility with labeled statements
|
|
224
30
|
let sourceType = options.sourceType || 'script';
|
|
225
|
-
if (!options.sourceType && containsModuleSyntax(
|
|
31
|
+
if (!options.sourceType && containsModuleSyntax(code)) {
|
|
226
32
|
sourceType = 'module';
|
|
227
33
|
}
|
|
228
34
|
|
|
229
35
|
// Parse with Acorn
|
|
230
36
|
try {
|
|
231
|
-
return acornParse(
|
|
37
|
+
return acornParse(code, {
|
|
232
38
|
ecmaVersion: 2022, // Support ES2022 features (including top-level await)
|
|
233
39
|
sourceType: sourceType,
|
|
234
40
|
locations: true, // Track source locations for better error messages
|
|
@@ -331,7 +137,6 @@ export function createEnvironment() {
|
|
|
331
137
|
}
|
|
332
138
|
|
|
333
139
|
// Export utility functions for CLI tools
|
|
334
|
-
export { preprocessCode };
|
|
335
140
|
export const isTopLevelAwait = containsModuleSyntax;
|
|
336
141
|
|
|
337
142
|
export { Interpreter } from './interpreter/interpreter.js';
|
|
@@ -35,9 +35,10 @@ export class Interpreter {
|
|
|
35
35
|
|
|
36
36
|
// For block statements, evaluate each statement async
|
|
37
37
|
if (node.type === 'BlockStatement') {
|
|
38
|
+
const blockEnv = new Environment(env);
|
|
38
39
|
let result = undefined;
|
|
39
40
|
for (const statement of node.body) {
|
|
40
|
-
result = await this.evaluateAsync(statement,
|
|
41
|
+
result = await this.evaluateAsync(statement, blockEnv);
|
|
41
42
|
if (result instanceof ReturnValue || result instanceof ThrowSignal ||
|
|
42
43
|
result instanceof BreakSignal || result instanceof ContinueSignal) {
|
|
43
44
|
return result;
|
|
@@ -254,8 +255,360 @@ export class Interpreter {
|
|
|
254
255
|
return result;
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
// For
|
|
258
|
-
|
|
258
|
+
// For ForStatement with async body
|
|
259
|
+
if (node.type === 'ForStatement') {
|
|
260
|
+
const forEnv = new Environment(env);
|
|
261
|
+
if (node.init) {
|
|
262
|
+
await this.evaluateAsync(node.init, forEnv);
|
|
263
|
+
}
|
|
264
|
+
while (!node.test || await this.evaluateAsync(node.test, forEnv)) {
|
|
265
|
+
const result = await this.evaluateAsync(node.body, forEnv);
|
|
266
|
+
if (result instanceof BreakSignal) {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
if (result instanceof ContinueSignal) {
|
|
270
|
+
if (node.update) {
|
|
271
|
+
await this.evaluateAsync(node.update, forEnv);
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (result instanceof ReturnValue || result instanceof ThrowSignal) {
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
if (node.update) {
|
|
279
|
+
await this.evaluateAsync(node.update, forEnv);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// For ForOfStatement with async body
|
|
286
|
+
if (node.type === 'ForOfStatement') {
|
|
287
|
+
const forEnv = new Environment(env);
|
|
288
|
+
const iterable = await this.evaluateAsync(node.right, forEnv);
|
|
289
|
+
const declarator = node.left.declarations[0];
|
|
290
|
+
const isConst = node.left.kind === 'const';
|
|
291
|
+
|
|
292
|
+
for (const value of iterable) {
|
|
293
|
+
const iterEnv = forEnv.extend();
|
|
294
|
+
if (declarator.id.type === 'Identifier') {
|
|
295
|
+
iterEnv.define(declarator.id.name, value, isConst);
|
|
296
|
+
} else if (declarator.id.type === 'ArrayPattern') {
|
|
297
|
+
this.bindArrayPattern(declarator.id, value, iterEnv, isConst);
|
|
298
|
+
} else if (declarator.id.type === 'ObjectPattern') {
|
|
299
|
+
this.bindObjectPattern(declarator.id, value, iterEnv, isConst);
|
|
300
|
+
}
|
|
301
|
+
const result = await this.evaluateAsync(node.body, iterEnv);
|
|
302
|
+
if (result instanceof BreakSignal) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
if (result instanceof ContinueSignal) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (result instanceof ReturnValue || result instanceof ThrowSignal) {
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// For ForInStatement with async body
|
|
316
|
+
if (node.type === 'ForInStatement') {
|
|
317
|
+
const forEnv = new Environment(env);
|
|
318
|
+
const obj = await this.evaluateAsync(node.right, forEnv);
|
|
319
|
+
if (obj === null || obj === undefined) {
|
|
320
|
+
throw new TypeError(`Cannot use 'in' operator to iterate over ${obj}`);
|
|
321
|
+
}
|
|
322
|
+
const varName = node.left.declarations[0].id.name;
|
|
323
|
+
forEnv.define(varName, undefined);
|
|
324
|
+
|
|
325
|
+
for (const key in obj) {
|
|
326
|
+
forEnv.set(varName, key);
|
|
327
|
+
const result = await this.evaluateAsync(node.body, forEnv);
|
|
328
|
+
if (result instanceof BreakSignal) {
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
if (result instanceof ContinueSignal) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (result instanceof ReturnValue || result instanceof ThrowSignal) {
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// For WhileStatement with async body
|
|
342
|
+
if (node.type === 'WhileStatement') {
|
|
343
|
+
while (await this.evaluateAsync(node.test, env)) {
|
|
344
|
+
const result = await this.evaluateAsync(node.body, env);
|
|
345
|
+
if (result instanceof BreakSignal) {
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
if (result instanceof ContinueSignal) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (result instanceof ReturnValue || result instanceof ThrowSignal) {
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// For DoWhileStatement with async body
|
|
359
|
+
if (node.type === 'DoWhileStatement') {
|
|
360
|
+
do {
|
|
361
|
+
const result = await this.evaluateAsync(node.body, env);
|
|
362
|
+
if (result instanceof BreakSignal) {
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
if (result instanceof ContinueSignal) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (result instanceof ReturnValue || result instanceof ThrowSignal) {
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
} while (await this.evaluateAsync(node.test, env));
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// For IfStatement with async branches
|
|
376
|
+
if (node.type === 'IfStatement') {
|
|
377
|
+
const test = await this.evaluateAsync(node.test, env);
|
|
378
|
+
if (test) {
|
|
379
|
+
return await this.evaluateAsync(node.consequent, env);
|
|
380
|
+
} else if (node.alternate) {
|
|
381
|
+
return await this.evaluateAsync(node.alternate, env);
|
|
382
|
+
}
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// For SwitchStatement with async cases
|
|
387
|
+
if (node.type === 'SwitchStatement') {
|
|
388
|
+
const discriminant = await this.evaluateAsync(node.discriminant, env);
|
|
389
|
+
let matched = false;
|
|
390
|
+
|
|
391
|
+
for (const switchCase of node.cases) {
|
|
392
|
+
if (!matched && switchCase.test) {
|
|
393
|
+
const testValue = await this.evaluateAsync(switchCase.test, env);
|
|
394
|
+
if (testValue === discriminant) {
|
|
395
|
+
matched = true;
|
|
396
|
+
}
|
|
397
|
+
} else if (!switchCase.test) {
|
|
398
|
+
matched = true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (matched) {
|
|
402
|
+
for (const statement of switchCase.consequent) {
|
|
403
|
+
const result = await this.evaluateAsync(statement, env);
|
|
404
|
+
if (result instanceof BreakSignal) {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
if (result instanceof ReturnValue || result instanceof ThrowSignal) {
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// For ConditionalExpression (ternary) with async operands
|
|
417
|
+
if (node.type === 'ConditionalExpression') {
|
|
418
|
+
const test = await this.evaluateAsync(node.test, env);
|
|
419
|
+
return test
|
|
420
|
+
? await this.evaluateAsync(node.consequent, env)
|
|
421
|
+
: await this.evaluateAsync(node.alternate, env);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// For AssignmentExpression with async value
|
|
425
|
+
if (node.type === 'AssignmentExpression') {
|
|
426
|
+
const value = await this.evaluateAsync(node.right, env);
|
|
427
|
+
|
|
428
|
+
if (node.left.type === 'Identifier') {
|
|
429
|
+
const name = node.left.name;
|
|
430
|
+
if (node.operator === '=') {
|
|
431
|
+
if (env.has(name)) {
|
|
432
|
+
env.set(name, value);
|
|
433
|
+
} else {
|
|
434
|
+
env.define(name, value);
|
|
435
|
+
}
|
|
436
|
+
return value;
|
|
437
|
+
} else {
|
|
438
|
+
const current = env.get(name);
|
|
439
|
+
const newValue = this.applyCompoundAssignment(node.operator, current, value);
|
|
440
|
+
env.set(name, newValue);
|
|
441
|
+
return newValue;
|
|
442
|
+
}
|
|
443
|
+
} else if (node.left.type === 'MemberExpression') {
|
|
444
|
+
const obj = await this.evaluateAsync(node.left.object, env);
|
|
445
|
+
const prop = node.left.computed
|
|
446
|
+
? await this.evaluateAsync(node.left.property, env)
|
|
447
|
+
: node.left.property.name;
|
|
448
|
+
|
|
449
|
+
if (node.operator === '=') {
|
|
450
|
+
obj[prop] = value;
|
|
451
|
+
return value;
|
|
452
|
+
} else {
|
|
453
|
+
const newValue = this.applyCompoundAssignment(node.operator, obj[prop], value);
|
|
454
|
+
obj[prop] = newValue;
|
|
455
|
+
return newValue;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
throw new Error('Invalid assignment target');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// For UnaryExpression with async argument
|
|
462
|
+
if (node.type === 'UnaryExpression') {
|
|
463
|
+
if (node.operator === 'delete' && node.argument.type === 'MemberExpression') {
|
|
464
|
+
const obj = await this.evaluateAsync(node.argument.object, env);
|
|
465
|
+
const prop = node.argument.computed
|
|
466
|
+
? await this.evaluateAsync(node.argument.property, env)
|
|
467
|
+
: node.argument.property.name;
|
|
468
|
+
return delete obj[prop];
|
|
469
|
+
}
|
|
470
|
+
const argument = await this.evaluateAsync(node.argument, env);
|
|
471
|
+
switch (node.operator) {
|
|
472
|
+
case '+': return +argument;
|
|
473
|
+
case '-': return -argument;
|
|
474
|
+
case '!': return !argument;
|
|
475
|
+
case '~': return ~argument;
|
|
476
|
+
case 'typeof':
|
|
477
|
+
if (argument && argument.__isFunction) {
|
|
478
|
+
return 'function';
|
|
479
|
+
}
|
|
480
|
+
return typeof argument;
|
|
481
|
+
case 'void': return undefined;
|
|
482
|
+
case 'delete': return true;
|
|
483
|
+
default:
|
|
484
|
+
throw new Error(`Unknown unary operator: ${node.operator}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// For UpdateExpression with async member access
|
|
489
|
+
if (node.type === 'UpdateExpression') {
|
|
490
|
+
if (node.argument.type === 'Identifier') {
|
|
491
|
+
const name = node.argument.name;
|
|
492
|
+
const current = env.get(name);
|
|
493
|
+
const numericCurrent = (current === null || current === undefined) ? 0 : Number(current);
|
|
494
|
+
const newValue = node.operator === '++' ? numericCurrent + 1 : numericCurrent - 1;
|
|
495
|
+
env.set(name, newValue);
|
|
496
|
+
return node.prefix ? newValue : numericCurrent;
|
|
497
|
+
} else if (node.argument.type === 'MemberExpression') {
|
|
498
|
+
const obj = await this.evaluateAsync(node.argument.object, env);
|
|
499
|
+
if (obj === null || obj === undefined) {
|
|
500
|
+
throw new TypeError(
|
|
501
|
+
`Cannot read properties of ${obj} (reading '${
|
|
502
|
+
node.argument.computed
|
|
503
|
+
? await this.evaluateAsync(node.argument.property, env)
|
|
504
|
+
: node.argument.property.name
|
|
505
|
+
}')`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
const prop = node.argument.computed
|
|
509
|
+
? await this.evaluateAsync(node.argument.property, env)
|
|
510
|
+
: node.argument.property.name;
|
|
511
|
+
let current = obj[prop];
|
|
512
|
+
const numericCurrent = (current === null || current === undefined) ? 0 : Number(current);
|
|
513
|
+
const newValue = node.operator === '++' ? numericCurrent + 1 : numericCurrent - 1;
|
|
514
|
+
obj[prop] = newValue;
|
|
515
|
+
return node.prefix ? newValue : numericCurrent;
|
|
516
|
+
}
|
|
517
|
+
throw new Error('Invalid update expression target');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// For ArrayExpression with async elements
|
|
521
|
+
if (node.type === 'ArrayExpression') {
|
|
522
|
+
const result = [];
|
|
523
|
+
for (const elem of node.elements) {
|
|
524
|
+
if (!elem) {
|
|
525
|
+
result.push(undefined);
|
|
526
|
+
} else if (elem.type === 'SpreadElement') {
|
|
527
|
+
const spreadValue = await this.evaluateAsync(elem.argument, env);
|
|
528
|
+
if (Array.isArray(spreadValue)) {
|
|
529
|
+
result.push(...spreadValue);
|
|
530
|
+
} else if (typeof spreadValue[Symbol.iterator] === 'function') {
|
|
531
|
+
result.push(...spreadValue);
|
|
532
|
+
} else {
|
|
533
|
+
throw new TypeError('Spread syntax requires an iterable');
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
result.push(await this.evaluateAsync(elem, env));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// For ObjectExpression with async values
|
|
543
|
+
if (node.type === 'ObjectExpression') {
|
|
544
|
+
const obj = {};
|
|
545
|
+
for (const prop of node.properties) {
|
|
546
|
+
if (prop.type === 'SpreadElement') {
|
|
547
|
+
const spreadValue = await this.evaluateAsync(prop.argument, env);
|
|
548
|
+
if (typeof spreadValue === 'object' && spreadValue !== null) {
|
|
549
|
+
Object.assign(obj, spreadValue);
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
const key = prop.key.type === 'Identifier' && !prop.computed
|
|
553
|
+
? prop.key.name
|
|
554
|
+
: await this.evaluateAsync(prop.key, env);
|
|
555
|
+
const value = prop.value ? await this.evaluateAsync(prop.value, env) : env.get(key);
|
|
556
|
+
if (prop.method && prop.value.type === 'FunctionExpression') {
|
|
557
|
+
obj[key] = (...args) => {
|
|
558
|
+
const funcValue = this.evaluate(prop.value, env);
|
|
559
|
+
return this.callUserFunction(funcValue, args, env);
|
|
560
|
+
};
|
|
561
|
+
} else {
|
|
562
|
+
obj[key] = value;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return obj;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// For SequenceExpression with async expressions
|
|
570
|
+
if (node.type === 'SequenceExpression') {
|
|
571
|
+
let result;
|
|
572
|
+
for (const expr of node.expressions) {
|
|
573
|
+
result = await this.evaluateAsync(expr, env);
|
|
574
|
+
}
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// For ThrowStatement with async argument
|
|
579
|
+
if (node.type === 'ThrowStatement') {
|
|
580
|
+
return new ThrowSignal(await this.evaluateAsync(node.argument, env));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// For FunctionDeclaration - define in environment
|
|
584
|
+
if (node.type === 'FunctionDeclaration') {
|
|
585
|
+
return this.evaluateFunctionDeclaration(node, env);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// For FunctionExpression/ArrowFunctionExpression - create function
|
|
589
|
+
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
|
590
|
+
return this.evaluateFunctionExpression(node, env);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// For ClassDeclaration
|
|
594
|
+
if (node.type === 'ClassDeclaration') {
|
|
595
|
+
return this.evaluateClassDeclaration(node, env);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// For ClassExpression
|
|
599
|
+
if (node.type === 'ClassExpression') {
|
|
600
|
+
return this.evaluateClassExpression(node, env);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Only leaf nodes should fall through to sync evaluate
|
|
604
|
+
// These have no sub-expressions that could contain await
|
|
605
|
+
if (['Literal', 'Identifier', 'BreakStatement', 'ContinueStatement',
|
|
606
|
+
'EmptyStatement', 'ThisExpression', 'Super'].includes(node.type)) {
|
|
607
|
+
return this.evaluate(node, env);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Safety check - if we get here, we missed a node type
|
|
611
|
+
throw new Error(`Unhandled node type in evaluateAsync: ${node.type}`);
|
|
259
612
|
}
|
|
260
613
|
|
|
261
614
|
evaluate(node, env) {
|
package/src/validator/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* WangValidator - Syntax validation for Wang/JSLike code
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { parse
|
|
5
|
+
import { parse } from '../index.js';
|
|
6
6
|
|
|
7
7
|
export class WangValidator {
|
|
8
8
|
/**
|
|
@@ -14,11 +14,8 @@ export class WangValidator {
|
|
|
14
14
|
*/
|
|
15
15
|
validate(code, options = {}) {
|
|
16
16
|
try {
|
|
17
|
-
// Preprocess code (handles ASI, etc.)
|
|
18
|
-
const processed = preprocessCode(code);
|
|
19
|
-
|
|
20
17
|
// Parse the code
|
|
21
|
-
const ast = parse(
|
|
18
|
+
const ast = parse(code);
|
|
22
19
|
|
|
23
20
|
return {
|
|
24
21
|
valid: true,
|