1import { assert, describe, it } from "@effect/vitest"
2import { Schema, SchemaParser } from "effect"
3import * as Registry from "effect/internal/schema/compilerRegistry"
4// oxlint-disable-next-line no-unassigned-import
5import "effect/schema/SchemaJITCompiler/enable"
6import { assertSchemaIssueError, deepStrictEqual, throws } from "../utils/assert.ts"
7
8const strict = { onExcessProperty: "error" } as const
9
10const compiled = (schema: Schema.Top) => {
11 const { decode, is } = Registry.resolve(schema.ast)
12 assert.ok(decode, "Expected a compiled decoder")
13 assert.ok(is, "Expected a compiled type guard")
14 return { decode, is }
15}
16
17describe("SchemaJITCompiler excess properties", () => {
18 it("ignores non-enumerable own properties without fallback or getter access", () => {
19 const schema = Schema.Struct({ a: Schema.String })
20 const { decode, is } = compiled(schema)
21 const input = Object.assign(Object.create(null), { a: "a" })
22 for (const key of ["stack", "propertyIsEnumerable", Symbol("hidden")]) {
23 Object.defineProperty(input, key, {
24 get() {
25 throw new Error("Non-enumerable excess properties must not be read")
26 },
27 enumerable: false
28 })
29 }
30 deepStrictEqual(decode(input, strict), { a: "a" })
31 assert.isTrue(is(input, strict))
32 })
33
34 it("rejects enumerable excess properties", () => {
35 const schema = Schema.Struct({ a: Schema.String })
36 const compiledParser = compiled(schema)
37 for (const key of ["b", Symbol("b")]) {
38 const input = { a: "a", [key]: "b" }
39 assert.strictEqual(compiledParser.decode(input, strict), Registry.invalid)
40 assert.isFalse(compiledParser.is(input, strict))
41 }
42 const decode = SchemaParser.decodeUnknownSync(schema, strict)
43 throws(
44 () => decode({ a: "a", b: "b" }),
45 (error) => {
46 assertSchemaIssueError(error, `Expected no excess property\n at ["b"]`)
47 }
48 )
49 })
50
51 it("symbol index signatures ignore non-enumerable properties in decode and encode", () => {
52 const visible = Symbol("visible")
53 const hidden = Symbol("hidden")
54 const input = { [visible]: 1 }
55 Object.defineProperty(input, hidden, {
56 get() {
57 throw new Error("Non-enumerable record entries must not be read")
58 },
59 enumerable: false
60 })
61 const expected = { [visible]: 1 }
62 const schema = Schema.Record(Schema.Symbol, Schema.Number)
63
64 const { decode, is } = compiled(schema)
65 for (const options of [{}, strict]) {
66 deepStrictEqual(decode(input, options), expected)
67 assert.isTrue(is(input, options))
68 assert.strictEqual(decode({ [visible]: "invalid" }, options), Registry.invalid)
69 assert.isFalse(is({ [visible]: "invalid" }, options))
70 deepStrictEqual(SchemaParser.encodeUnknownSync(schema, options)(input), expected)
71 }
72 })
73
74 it("validates non-enumerable declared string and symbol properties", () => {
75 const symbol = Symbol("declared")
76 const schema = Schema.Struct({ a: Schema.Number, [symbol]: Schema.String })
77 const { decode, is } = compiled(schema)
78 const input = Object.defineProperties({}, {
79 a: { value: 1, enumerable: false },
80 [symbol]: { value: "a", enumerable: false }
81 })
82 deepStrictEqual(decode(input, strict), { a: 1, [symbol]: "a" })
83 assert.isTrue(is(input, strict))
84 deepStrictEqual(SchemaParser.encodeUnknownSync(schema, strict)(input), { a: 1, [symbol]: "a" })
85 for (const key of ["a", symbol]) {
86 const invalid = Object.defineProperty({ a: 1, [symbol]: "a" }, key, { value: null, enumerable: false })
87 assert.strictEqual(decode(invalid, strict), Registry.invalid)
88 assert.isFalse(is(invalid, strict))
89 }
90 })
91
92 it("handles fixed fields and multiple index signatures", () => {
93 const symbol = Symbol("visible")
94 const schema = Schema.StructWithRest(Schema.Struct({ fixed: Schema.String }), [
95 Schema.Record(Schema.TemplateLiteral(["field-", Schema.Number]), Schema.Number),
96 Schema.Record(Schema.Symbol, Schema.Number)
97 ])
98 const { decode, is } = compiled(schema)
99 const expected = { fixed: "a", "field-1": 1, [symbol]: 2 }
100 const input = Object.defineProperties({ ...expected }, {
101 "field-2": { value: "invalid", enumerable: false },
102 [Symbol("hidden")]: { value: "invalid", enumerable: false }
103 })
104 deepStrictEqual(decode(input, strict), expected)
105 assert.isTrue(is(input, strict))
106 assert.strictEqual(decode({ ...input, extra: 1 }, strict), Registry.invalid)
107 assert.isFalse(is({ ...input, extra: 1 }, strict))
108 assert.strictEqual(decode({ ...input, [symbol]: "invalid" }, strict), Registry.invalid)
109 deepStrictEqual(decode({ fixed: "b", "field-3": 3 }, strict), { fixed: "b", "field-3": 3 })
110 })
111
112 it("reuses the strict key snapshot before reading declared getters", () => {
113 const visible = Symbol("visible")
114 const added = Symbol("added")
115 const schema = Schema.StructWithRest(Schema.Struct({ fixed: Schema.Boolean }), [
116 Schema.Record(Schema.Symbol, Schema.Number)
117 ])
118 const { decode, is } = compiled(schema)
119 const input = () => {
120 const record: Record<PropertyKey, unknown> = { [visible]: 1 }
121 Object.defineProperty(record, "fixed", {
122 enumerable: true,
123 get() {
124 record[added] = "added after excess checking"
125 return true
126 }
127 })
128 return record
129 }
130 deepStrictEqual(decode(input(), strict), { fixed: true, [visible]: 1 })
131 assert.isTrue(is(input(), strict))
132 assert.strictEqual(decode(input(), {}), Registry.invalid)
133 assert.isFalse(is(input(), {}))
134 })
135
136 it("encodes tagged errors carrying runtime internals", () => {
137 class NotFound extends Schema.TaggedError<NotFound>()("NotFound", { id: Schema.Number }) {}
138 const encode = SchemaParser.encodeUnknownSync(NotFound, strict)
139 deepStrictEqual(encode(new NotFound({ id: 1 })), { _tag: "NotFound", id: 1 })
140 })
141})