Skip to content

Commit 8af7d3b

Browse files
Copilotna-trium-144
andcommitted
Extract replLikeEval/checkSyntax into packages/jsEval workspace with tests
Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com>
1 parent 57e89f2 commit 8af7d3b

8 files changed

Lines changed: 331 additions & 61 deletions

File tree

.github/workflows/node.js.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,17 @@ jobs:
3232
cache: 'npm'
3333
- run: npm ci
3434
- run: npm run tsc
35+
36+
test-js-eval:
37+
runs-on: ubuntu-latest
38+
strategy:
39+
matrix:
40+
node-version: [22.x]
41+
steps:
42+
- uses: actions/checkout@v4
43+
- uses: actions/setup-node@v4
44+
with:
45+
node-version: ${{ matrix.node-version }}
46+
cache: 'npm'
47+
- run: npm ci
48+
- run: npm test --workspace=packages/jsEval

app/terminal/worker/jsEval.worker.ts

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { expose } from "comlink";
44
import type { ReplOutput } from "../repl";
55
import type { WorkerCapabilities } from "./runtime";
66
import inspect from "object-inspect";
7+
import { replLikeEval, checkSyntax } from "@my-code/js-eval";
78

89
function format(...args: unknown[]): string {
910
// TODO: console.logの第1引数はフォーマット指定文字列を取ることができる
@@ -34,41 +35,6 @@ async function init(/*_interruptBuffer?: Uint8Array*/): Promise<{
3435
return { capabilities: { interrupt: "restart" } };
3536
}
3637

37-
async function replLikeEval(code: string): Promise<unknown> {
38-
// eval()の中でconst,letを使って変数を作成した場合、
39-
// 次に実行するコマンドはスコープ外扱いでありアクセスできなくなってしまうので、
40-
// varに置き換えている
41-
if (code.trim().startsWith("const ")) {
42-
code = "var " + code.trim().slice(6);
43-
} else if (code.trim().startsWith("let ")) {
44-
code = "var " + code.trim().slice(4);
45-
}
46-
// eval()の中でclassを作成した場合も同様
47-
const classRegExp = /^\s*class\s+(\w+)/;
48-
if (classRegExp.test(code)) {
49-
code = code.replace(classRegExp, "var $1 = class $1");
50-
}
51-
52-
if (code.trim().startsWith("{") && code.trim().endsWith("}")) {
53-
// オブジェクトは ( ) で囲わなければならない
54-
try {
55-
return self.eval(`(${code})`);
56-
} catch (e) {
57-
if (e instanceof SyntaxError) {
58-
// オブジェクトではなくブロックだった場合、再度普通に実行
59-
return self.eval(code);
60-
} else {
61-
throw e;
62-
}
63-
}
64-
} else if (/^\s*await\W/.test(code)) {
65-
// promiseをawaitする場合は、promiseの部分だけをevalし、それを外からawaitする
66-
return await self.eval(code.trim().slice(5));
67-
} else {
68-
return self.eval(code);
69-
}
70-
}
71-
7238
async function runCode(
7339
code: string,
7440
onOutput: (output: ReplOutput) => void
@@ -129,32 +95,6 @@ function runFile(
12995
return { updatedFiles: {} as Record<string, string> };
13096
}
13197

132-
async function checkSyntax(
133-
code: string
134-
): Promise<{ status: "complete" | "incomplete" | "invalid" }> {
135-
try {
136-
// Try to create a Function to check syntax
137-
// new Function(code); // <- not working
138-
self.eval(`() => {${code}}`);
139-
return { status: "complete" };
140-
} catch (e) {
141-
// Check if it's a syntax error or if more input is expected
142-
if (e instanceof SyntaxError) {
143-
// Simple heuristic: check for "Unexpected end of input"
144-
if (
145-
e.message.includes("Unexpected token '}'") ||
146-
e.message.includes("Unexpected end of input")
147-
) {
148-
return { status: "incomplete" };
149-
} else {
150-
return { status: "invalid" };
151-
}
152-
} else {
153-
return { status: "invalid" };
154-
}
155-
}
156-
}
157-
15898
async function restoreState(commands: string[]): Promise<object> {
15999
// Re-execute all previously successful commands to restore state
160100
for (const command of commands) {

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"version": "0.1.0",
44
"private": true,
55
"type": "module",
6+
"workspaces": [
7+
"packages/*"
8+
],
69
"scripts": {
710
"dev": "npm run cf-typegen && npm run generateLanguages && npm run generateSections && npm run copyAllDTSFiles && npm run removeHinting && next dev",
811
"build": "npm run cf-typegen && npm run generateLanguages && npm run generateSections && npm run copyAllDTSFiles && npm run removeHinting && next build",

packages/jsEval/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "@my-code/js-eval",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"exports": {
7+
".": "./src/index.ts"
8+
},
9+
"scripts": {
10+
"test": "node --import tsx/esm --test src/index.test.ts"
11+
},
12+
"devDependencies": {
13+
"tsx": "*",
14+
"typescript": "*"
15+
}
16+
}

packages/jsEval/src/index.test.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { describe, it } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { replLikeEval, checkSyntax } from "./index.ts";
4+
5+
// ---------------------------------------------------------------------------
6+
// replLikeEval
7+
// ---------------------------------------------------------------------------
8+
9+
describe("replLikeEval", () => {
10+
describe("var declaration", () => {
11+
it("evaluates var and returns undefined", async () => {
12+
const result = await replLikeEval("var x = 42");
13+
assert.strictEqual(result, undefined);
14+
});
15+
});
16+
17+
describe("const declaration", () => {
18+
it("converts const to var and returns undefined", async () => {
19+
const result = await replLikeEval("const constVar = 1");
20+
assert.strictEqual(result, undefined);
21+
});
22+
});
23+
24+
describe("let declaration", () => {
25+
it("converts let to var and returns undefined", async () => {
26+
const result = await replLikeEval("let letVar = 2");
27+
assert.strictEqual(result, undefined);
28+
});
29+
});
30+
31+
describe("undeclared variable assignment", () => {
32+
it("assigns to global and returns the assigned value", async () => {
33+
const result = await replLikeEval("undeclaredVar = 99");
34+
assert.strictEqual(result, 99);
35+
});
36+
});
37+
38+
describe("expression evaluation", () => {
39+
it("returns numeric result", async () => {
40+
assert.strictEqual(await replLikeEval("1 + 2"), 3);
41+
});
42+
43+
it("returns string result", async () => {
44+
assert.strictEqual(await replLikeEval('"hello"'), "hello");
45+
});
46+
47+
it("returns boolean result", async () => {
48+
assert.strictEqual(await replLikeEval("true"), true);
49+
});
50+
});
51+
52+
describe("function declaration", () => {
53+
it("declares a function and returns undefined", async () => {
54+
const result = await replLikeEval("function greet() { return 'hi'; }");
55+
assert.strictEqual(result, undefined);
56+
});
57+
});
58+
59+
describe("class declaration", () => {
60+
it("converts class to var-assigned class expression and returns undefined", async () => {
61+
const result = await replLikeEval("class MyClass { constructor() {} }");
62+
assert.strictEqual(result, undefined);
63+
});
64+
});
65+
66+
describe("array literal", () => {
67+
it("returns an array", async () => {
68+
const result = await replLikeEval("[1, 2, 3]");
69+
assert.deepStrictEqual(result, [1, 2, 3]);
70+
});
71+
72+
it("returns an empty array", async () => {
73+
assert.deepStrictEqual(await replLikeEval("[]"), []);
74+
});
75+
});
76+
77+
describe("object literal", () => {
78+
it("returns an object for { a: 1 }", async () => {
79+
const result = await replLikeEval("{ a: 1 }");
80+
assert.deepStrictEqual(result, { a: 1 });
81+
});
82+
83+
it("returns an empty object for {}", async () => {
84+
assert.deepStrictEqual(await replLikeEval("{}"), {});
85+
});
86+
});
87+
88+
describe("block that looks like an object", () => {
89+
it("executes a labelled statement block and returns undefined", async () => {
90+
// { x: 1 } is ambiguous – first tried as object; if that fails as
91+
// SyntaxError it falls back to block execution. A true label statement
92+
// `{ label: expr; }` is a block and eval returns the last expression.
93+
// { a: 1 } is valid as both, so replLikeEval returns the object.
94+
const result = await replLikeEval("{ x: 1 }");
95+
// Treated as object literal when it is valid JSON-like
96+
assert.deepStrictEqual(result, { x: 1 });
97+
});
98+
99+
it("executes pure block statement when object parse fails", async () => {
100+
// A block containing a statement that is invalid as an object expression
101+
// forces the fallback path.
102+
const result = await replLikeEval("{ let tmp = 5; tmp; }");
103+
assert.strictEqual(result, 5);
104+
});
105+
});
106+
107+
describe("await expression", () => {
108+
it("awaits a resolved promise", async () => {
109+
const result = await replLikeEval("await Promise.resolve(7)");
110+
assert.strictEqual(result, 7);
111+
});
112+
});
113+
114+
describe("error propagation", () => {
115+
it("throws ReferenceError for undefined identifier", async () => {
116+
await assert.rejects(
117+
() => replLikeEval("notDefinedAtAllXyz"),
118+
ReferenceError
119+
);
120+
});
121+
});
122+
});
123+
124+
// ---------------------------------------------------------------------------
125+
// checkSyntax
126+
// ---------------------------------------------------------------------------
127+
128+
describe("checkSyntax", () => {
129+
describe("complete inputs", () => {
130+
it("simple expression is complete", async () => {
131+
assert.deepStrictEqual(await checkSyntax("1 + 2"), {
132+
status: "complete",
133+
});
134+
});
135+
136+
it("function declaration is complete", async () => {
137+
assert.deepStrictEqual(
138+
await checkSyntax("function f() { return 1; }"),
139+
{ status: "complete" }
140+
);
141+
});
142+
143+
it("if-else block is complete", async () => {
144+
assert.deepStrictEqual(
145+
await checkSyntax("if (true) { 1; } else { 2; }"),
146+
{ status: "complete" }
147+
);
148+
});
149+
150+
it("for loop is complete", async () => {
151+
assert.deepStrictEqual(
152+
await checkSyntax("for (let i = 0; i < 3; i++) {}"),
153+
{ status: "complete" }
154+
);
155+
});
156+
157+
it("empty string is complete", async () => {
158+
assert.deepStrictEqual(await checkSyntax(""), { status: "complete" });
159+
});
160+
});
161+
162+
describe("incomplete inputs", () => {
163+
it("if(1){ is incomplete", async () => {
164+
assert.deepStrictEqual(await checkSyntax("if(1){"), {
165+
status: "incomplete",
166+
});
167+
});
168+
169+
it("function f() { is incomplete", async () => {
170+
assert.deepStrictEqual(await checkSyntax("function f() {"), {
171+
status: "incomplete",
172+
});
173+
});
174+
175+
it("open array bracket is incomplete", async () => {
176+
assert.deepStrictEqual(await checkSyntax("[1, 2,"), {
177+
status: "incomplete",
178+
});
179+
});
180+
181+
it("open object brace is incomplete", async () => {
182+
assert.deepStrictEqual(await checkSyntax("({ a:"), {
183+
status: "incomplete",
184+
});
185+
});
186+
});
187+
188+
describe("invalid inputs", () => {
189+
it("extra closing brace after complete block returns incomplete (extra } matches wrapper)", async () => {
190+
// The implementation wraps code in `() => {<code>}`.
191+
// An extra } is interpreted as closing the wrapper early, so the
192+
// engine reports "Unexpected token '}'" – which the heuristic maps to
193+
// "incomplete". This is a known limitation of the wrapper approach.
194+
assert.deepStrictEqual(await checkSyntax("if(1){}}"), {
195+
status: "incomplete",
196+
});
197+
});
198+
199+
it("syntax error expression is invalid", async () => {
200+
assert.deepStrictEqual(await checkSyntax("1 +* 2"), {
201+
status: "invalid",
202+
});
203+
});
204+
});
205+
});

0 commit comments

Comments
 (0)