|
| 1 | +import { describe, expect, test } from "bun:test" |
| 2 | +import { Effect, Schema } from "effect" |
| 3 | +import { CodeMode, Tool } from "../src/index.js" |
| 4 | + |
| 5 | +// `new` is supported syntax; only the callee decides whether construction succeeds. A callee without |
| 6 | +// construction support is a TypeError naming it, like JS, rather than an unsupported-syntax diagnostic |
| 7 | +// that would suggest `new` itself is unavailable. |
| 8 | +const tools = { |
| 9 | + echo: Tool.make({ |
| 10 | + description: "Echo", |
| 11 | + input: Schema.Struct({}), |
| 12 | + output: Schema.Struct({}), |
| 13 | + execute: () => Effect.succeed({}), |
| 14 | + }), |
| 15 | +} |
| 16 | +const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools })) |
| 17 | +const value = async (code: string) => { |
| 18 | + const result = await run(code) |
| 19 | + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) |
| 20 | + return result.value |
| 21 | +} |
| 22 | +const error = async (code: string) => { |
| 23 | + const result = await run(code) |
| 24 | + if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`) |
| 25 | + return result.error |
| 26 | +} |
| 27 | + |
| 28 | +describe("new on a non-constructible callee", () => { |
| 29 | + test("built-in functions without construction point at the plain call", async () => { |
| 30 | + // Number is a real constructor in JS, so the message must not claim otherwise. |
| 31 | + const failure = await error(`return new Number(42)`) |
| 32 | + expect(failure.kind).toBe("ExecutionFailure") |
| 33 | + expect(failure.message).toStartWith("new Number(...) is not supported; call Number(...) without new instead.") |
| 34 | + expect(failure.suggestions).toBeUndefined() |
| 35 | + expect((await error(`return new String("a")`)).message).toStartWith("new String(...) is not supported") |
| 36 | + expect((await error(`return new Math.abs(1)`)).message).toStartWith( |
| 37 | + "new Math.abs(...) is not supported; call Math.abs(...) without new instead.", |
| 38 | + ) |
| 39 | + }) |
| 40 | + |
| 41 | + test("non-callable values are not constructors", async () => { |
| 42 | + expect((await error(`return new tools.echo()`)).message).toStartWith("tools.echo is not a constructor.") |
| 43 | + expect((await error(`return new (1)()`)).message).toStartWith("The called value is not a constructor.") |
| 44 | + expect((await error(`const Date = 5; return new Date()`)).message).toStartWith("Date is not a constructor.") |
| 45 | + }) |
| 46 | + |
| 47 | + test("user-defined functions explain the documented gap", async () => { |
| 48 | + const failure = await error(`function Point(x) { return { x } }; return new Point(1)`) |
| 49 | + expect(failure.message).toStartWith( |
| 50 | + "Point cannot be constructed: user-defined constructors and classes are not supported. Call it as a function that returns a plain object instead.", |
| 51 | + ) |
| 52 | + expect((await error(`const make = () => ({}); return new make()`)).message).toStartWith( |
| 53 | + "make cannot be constructed", |
| 54 | + ) |
| 55 | + }) |
| 56 | + |
| 57 | + test("the failure is a catchable TypeError", async () => { |
| 58 | + expect( |
| 59 | + await value(` |
| 60 | + try { new Number(1) } catch (error) { return [error.name, error instanceof TypeError] } |
| 61 | + `), |
| 62 | + ).toEqual(["TypeError", true]) |
| 63 | + }) |
| 64 | + |
| 65 | + test("an undeclared callee still fails as an unknown identifier", async () => { |
| 66 | + expect((await error(`return new Function("return 1")`)).message).toContain("Function") |
| 67 | + expect((await error(`return new Function("return 1")`)).message).not.toContain("not a constructor") |
| 68 | + }) |
| 69 | + |
| 70 | + test("classes remain unsupported syntax", async () => { |
| 71 | + const failure = await error(`class A {}; return new A()`) |
| 72 | + expect(failure.kind).toBe("UnsupportedSyntax") |
| 73 | + expect(failure.message).toStartWith("Syntax 'ClassDeclaration' is not supported. Programs run a JavaScript subset") |
| 74 | + expect(failure.message).toContain("Classes, this, getters/setters, tagged templates, BigInt, and custom Symbols") |
| 75 | + }) |
| 76 | +}) |
0 commit comments