jsxpression 0.1.32 → 0.1.33
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/LICENSE +21 -0
- package/lib/analyze/analyze.test.d.ts +2 -0
- package/lib/analyze/analyze.test.d.ts.map +1 -0
- package/lib/analyze/analyze.test.js +763 -0
- package/lib/analyze/analyze.test.js.map +1 -0
- package/lib/analyze/jsx/element-children.d.ts.map +1 -1
- package/lib/analyze/jsx/element-children.js +1 -0
- package/lib/analyze/jsx/element-children.js.map +1 -1
- package/lib/analyze/security/member-access.d.ts +6 -0
- package/lib/codegen/generate-typescript-definitions.test.d.ts +2 -0
- package/lib/codegen/generate-typescript-definitions.test.d.ts.map +1 -0
- package/lib/codegen/generate-typescript-definitions.test.js +649 -0
- package/lib/codegen/generate-typescript-definitions.test.js.map +1 -0
- package/lib/compile/compile.test.d.ts +2 -0
- package/lib/compile/compile.test.d.ts.map +1 -0
- package/lib/compile/compile.test.js +146 -0
- package/lib/compile/compile.test.js.map +1 -0
- package/lib/evaluate/evaluate.test.d.ts +2 -0
- package/lib/evaluate/evaluate.test.d.ts.map +1 -0
- package/lib/evaluate/evaluate.test.js +260 -0
- package/lib/evaluate/evaluate.test.js.map +1 -0
- package/lib/parse/parse.test.d.ts +2 -0
- package/lib/parse/parse.test.d.ts.map +1 -0
- package/lib/parse/parse.test.js +80 -0
- package/lib/parse/parse.test.js.map +1 -0
- package/lib/render.test.d.ts +2 -0
- package/lib/render.test.d.ts.map +1 -0
- package/lib/render.test.js +301 -0
- package/lib/render.test.js.map +1 -0
- package/lib/traverse.d.ts.map +1 -1
- package/lib/traverse.js +1 -0
- package/lib/traverse.js.map +1 -1
- package/lib/traverse.test.d.ts +2 -0
- package/lib/traverse.test.d.ts.map +1 -0
- package/lib/traverse.test.js +84 -0
- package/lib/traverse.test.js.map +1 -0
- package/lib/types.d.ts +12 -0
- package/package.json +2 -3
- package/src/analyze/jsx/element-children.ts +1 -0
- package/src/traverse.ts +1 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { analyze } from "./analyze.js";
|
|
3
|
+
import { parse } from "../parse/index.js";
|
|
4
|
+
import { getBuiltins } from "../builtins.js";
|
|
5
|
+
describe("analyze - consolidated tests", () => {
|
|
6
|
+
const baseSchema = {
|
|
7
|
+
data: {
|
|
8
|
+
user: {
|
|
9
|
+
type: "object",
|
|
10
|
+
shape: {
|
|
11
|
+
name: { type: "string" },
|
|
12
|
+
age: { type: "number" },
|
|
13
|
+
profile: {
|
|
14
|
+
type: "object",
|
|
15
|
+
shape: {
|
|
16
|
+
avatar: { type: "string" },
|
|
17
|
+
bio: { type: "string" },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
text: { type: "string" },
|
|
23
|
+
items: {
|
|
24
|
+
type: "array",
|
|
25
|
+
shape: { type: "string" },
|
|
26
|
+
},
|
|
27
|
+
numbers: {
|
|
28
|
+
type: "array",
|
|
29
|
+
shape: { type: "number" },
|
|
30
|
+
},
|
|
31
|
+
products: {
|
|
32
|
+
type: "array",
|
|
33
|
+
shape: {
|
|
34
|
+
type: "object",
|
|
35
|
+
shape: {
|
|
36
|
+
name: { type: "string" },
|
|
37
|
+
price: { type: "number" },
|
|
38
|
+
tags: {
|
|
39
|
+
type: "array",
|
|
40
|
+
shape: { type: "string" },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
departments: {
|
|
46
|
+
type: "array",
|
|
47
|
+
shape: {
|
|
48
|
+
type: "object",
|
|
49
|
+
shape: {
|
|
50
|
+
name: { type: "string" },
|
|
51
|
+
employees: {
|
|
52
|
+
type: "array",
|
|
53
|
+
shape: {
|
|
54
|
+
type: "object",
|
|
55
|
+
shape: {
|
|
56
|
+
name: { type: "string" },
|
|
57
|
+
role: { type: "string" },
|
|
58
|
+
salary: { type: "number" },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
elements: {
|
|
67
|
+
Text: { props: {} },
|
|
68
|
+
div: { props: {} },
|
|
69
|
+
span: { props: {} },
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
describe("data access validation", () => {
|
|
73
|
+
describe("basic data access", () => {
|
|
74
|
+
it("should allow valid data access", () => {
|
|
75
|
+
const ast = parse("<Text>{data.user.name}</Text>");
|
|
76
|
+
const result = analyze(ast, baseSchema);
|
|
77
|
+
expect(result.hasErrors).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
it("should allow nested object access", () => {
|
|
80
|
+
const ast = parse("<Text>{data.user.profile.avatar}</Text>");
|
|
81
|
+
const result = analyze(ast, baseSchema);
|
|
82
|
+
expect(result.hasErrors).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
it("should block invalid data properties", () => {
|
|
85
|
+
const ast = parse("<Text>{data.user.invalid}</Text>");
|
|
86
|
+
const result = analyze(ast, baseSchema);
|
|
87
|
+
expect(result.hasErrors).toBe(true);
|
|
88
|
+
expect(result.errors[0].message).toContain("Property 'data.user.invalid' does not exist in schema");
|
|
89
|
+
});
|
|
90
|
+
it("should block invalid nested properties", () => {
|
|
91
|
+
const ast = parse("<Text>{data.user.profile.invalid}</Text>");
|
|
92
|
+
const result = analyze(ast, baseSchema);
|
|
93
|
+
expect(result.hasErrors).toBe(true);
|
|
94
|
+
expect(result.errors[0].message).toContain("Property 'data.user.profile.invalid' does not exist in schema");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe("array access", () => {
|
|
98
|
+
it("should allow array access with valid methods", () => {
|
|
99
|
+
const ast = parse("<Text>{data.items.map(item => item)}</Text>");
|
|
100
|
+
const result = analyze(ast, baseSchema);
|
|
101
|
+
expect(result.hasErrors).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
it("should allow numeric array index access", () => {
|
|
104
|
+
const ast = parse("<Text>{data.items[0]}</Text>");
|
|
105
|
+
const result = analyze(ast, baseSchema);
|
|
106
|
+
expect(result.hasErrors).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
it("should allow various numeric array indices", () => {
|
|
109
|
+
const ast = parse("<Text>{data.items[42]} and {data.numbers[1]}</Text>");
|
|
110
|
+
const result = analyze(ast, baseSchema);
|
|
111
|
+
expect(result.hasErrors).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
it("should allow nested array access with indices", () => {
|
|
114
|
+
const ast = parse("<Text>{data.products[0].tags[1]}</Text>");
|
|
115
|
+
const result = analyze(ast, baseSchema);
|
|
116
|
+
expect(result.hasErrors).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it("should require array indices for nested object access", () => {
|
|
119
|
+
const ast = parse("<Text>{data.products.tags}</Text>");
|
|
120
|
+
const result = analyze(ast, baseSchema);
|
|
121
|
+
expect(result.hasErrors).toBe(true);
|
|
122
|
+
expect(result.errors[0].message).toContain("data.products.tags");
|
|
123
|
+
});
|
|
124
|
+
it("should detect typos on array properties", () => {
|
|
125
|
+
const ast = parse("<Text>{data.departments.lengdth}</Text>");
|
|
126
|
+
const result = analyze(ast, baseSchema);
|
|
127
|
+
expect(result.hasErrors).toBe(true);
|
|
128
|
+
expect(result.errors[0].code).toBe("INVALID_DATA_ACCESS");
|
|
129
|
+
expect(result.errors[0].suggestions).toContain("length");
|
|
130
|
+
});
|
|
131
|
+
it("should allow string members on array elements", () => {
|
|
132
|
+
const ast = parse("<Text>{data.items[0].length}</Text>");
|
|
133
|
+
const result = analyze(ast, baseSchema);
|
|
134
|
+
expect(result.hasErrors).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
it("should allow array.at() method with positive index", () => {
|
|
137
|
+
const ast = parse("<Text>{data.items.at(0)}</Text>");
|
|
138
|
+
const result = analyze(ast, baseSchema);
|
|
139
|
+
expect(result.hasErrors).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
it("should allow array.at() method with negative index", () => {
|
|
142
|
+
const ast = parse("<Text>{data.items.at(-1)}</Text>");
|
|
143
|
+
const result = analyze(ast, baseSchema);
|
|
144
|
+
expect(result.hasErrors).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
it("should allow array.at() method in complex expressions", () => {
|
|
147
|
+
const ast = parse("<Text>{data.products.at(-1).name}</Text>");
|
|
148
|
+
const result = analyze(ast, baseSchema);
|
|
149
|
+
expect(result.hasErrors).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
it("should block dynamic array access with variables", () => {
|
|
152
|
+
const ast = parse("<Text>{data.items[variable]}</Text>");
|
|
153
|
+
const result = analyze(ast, baseSchema);
|
|
154
|
+
expect(result.hasErrors).toBe(true);
|
|
155
|
+
expect(result.errors.some((e) => e.message.includes("variable") || e.message.includes("not allowed"))).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
it("should block string-based computed access on objects", () => {
|
|
158
|
+
const ast = parse("<Text>{data.user['name']}</Text>");
|
|
159
|
+
const result = analyze(ast, baseSchema);
|
|
160
|
+
expect(result.hasErrors).toBe(true);
|
|
161
|
+
expect(result.errors.some((e) => e.message.includes("computed member access not allowed"))).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
it("should block mutating array methods", () => {
|
|
164
|
+
const ast = parse("<Text>{data.items.push('new')}</Text>");
|
|
165
|
+
const result = analyze(ast, baseSchema);
|
|
166
|
+
expect(result.hasErrors).toBe(true);
|
|
167
|
+
expect(result.errors[0].message).toContain("method .push not allowed");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe("arrow function parameter validation", () => {
|
|
171
|
+
it("should validate arrow function parameters - valid access", () => {
|
|
172
|
+
const ast = parse("<Text>{data.products.map((product, i) => product.name)}</Text>");
|
|
173
|
+
const result = analyze(ast, baseSchema);
|
|
174
|
+
expect(result.hasErrors).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
it("should validate arrow function parameters - invalid property", () => {
|
|
177
|
+
const ast = parse("<Text>{data.products.map((product, i) => product.invalid)}</Text>");
|
|
178
|
+
const result = analyze(ast, baseSchema);
|
|
179
|
+
expect(result.hasErrors).toBe(true);
|
|
180
|
+
expect(result.errors[0].message).toContain("Property 'invalid' does not exist on parameter 'product'");
|
|
181
|
+
});
|
|
182
|
+
it("should validate nested arrow functions", () => {
|
|
183
|
+
const ast = parse("<Text>{data.products.map(product => product.tags.map(tag => tag))}</Text>");
|
|
184
|
+
const result = analyze(ast, baseSchema);
|
|
185
|
+
expect(result.hasErrors).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
it("should catch errors in nested arrow functions", () => {
|
|
188
|
+
const ast = parse("<Text>{data.products.map(product => product.tags.map(tag => tag.invalid))}</Text>");
|
|
189
|
+
const result = analyze(ast, baseSchema);
|
|
190
|
+
expect(result.hasErrors).toBe(true);
|
|
191
|
+
expect(result.errors[0].message).toContain("Cannot access property 'invalid' on string type");
|
|
192
|
+
});
|
|
193
|
+
it("should validate index parameters", () => {
|
|
194
|
+
const ast = parse("<Text>{data.items.map((item, i) => i + 1)}</Text>");
|
|
195
|
+
const result = analyze(ast, baseSchema);
|
|
196
|
+
expect(result.hasErrors).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
it("should catch invalid index parameter access", () => {
|
|
199
|
+
const ast = parse("<Text>{data.items.map((item, i) => i.invalid)}</Text>");
|
|
200
|
+
const result = analyze(ast, baseSchema);
|
|
201
|
+
expect(result.hasErrors).toBe(true);
|
|
202
|
+
expect(result.errors[0].message).toContain("Cannot access property 'invalid' on number type");
|
|
203
|
+
});
|
|
204
|
+
it("should handle complex nested scenarios", () => {
|
|
205
|
+
const ast = parse("<Text>{data.departments.map((dept) => dept.employees.map((employee) => employee.salary))}</Text>");
|
|
206
|
+
const result = analyze(ast, baseSchema);
|
|
207
|
+
expect(result.hasErrors).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
it("should detect typos in complex nested scenarios", () => {
|
|
210
|
+
const ast = parse("<Text>{data.departments.map((dept) => dept.employees.map((employee) => employee.saladry))}</Text>");
|
|
211
|
+
const result = analyze(ast, baseSchema);
|
|
212
|
+
expect(result.hasErrors).toBe(true);
|
|
213
|
+
expect(result.errors[0].message).toContain("Property 'saladry' does not exist on parameter 'employee'");
|
|
214
|
+
expect(result.errors[0].suggestions).toEqual(["name", "role", "salary"]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe("method calls validation", () => {
|
|
219
|
+
// Dynamic builtin schema tests - generates tests for all methods in BUILTINS
|
|
220
|
+
describe("builtin methods - dynamic validation", () => {
|
|
221
|
+
const builtins = getBuiltins();
|
|
222
|
+
// Test each builtin object (Math, Array.prototype, String.prototype)
|
|
223
|
+
Object.entries(builtins).forEach(([objectName, schema]) => {
|
|
224
|
+
if (!schema.methods)
|
|
225
|
+
return;
|
|
226
|
+
describe(`${objectName} methods`, () => {
|
|
227
|
+
Object.entries(schema.methods).forEach(([methodName, methodDef]) => {
|
|
228
|
+
const displayName = objectName === "Math" ? `Math.${methodName}` : methodName;
|
|
229
|
+
it(`should allow ${displayName} with valid parameters`, () => {
|
|
230
|
+
let testExpression;
|
|
231
|
+
if (objectName === "Math") {
|
|
232
|
+
// Math methods
|
|
233
|
+
if (methodDef.params[0]?.variadic) {
|
|
234
|
+
// Variadic methods like max/min
|
|
235
|
+
testExpression = `<Text>{Math.${methodName}(1, 2, 3)}</Text>`;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Fixed parameter methods like floor/ceil
|
|
239
|
+
testExpression = `<Text>{Math.${methodName}(3.14)}</Text>`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (objectName === "Array.prototype") {
|
|
243
|
+
// Array methods
|
|
244
|
+
if (methodName === "map" || methodName === "filter") {
|
|
245
|
+
testExpression = `<Text>{data.items.${methodName}(x => x)}</Text>`;
|
|
246
|
+
}
|
|
247
|
+
else if (methodName === "reduce") {
|
|
248
|
+
testExpression = `<Text>{data.numbers.${methodName}((a, b) => a + b, 0)}</Text>`;
|
|
249
|
+
}
|
|
250
|
+
else if (methodName === "slice") {
|
|
251
|
+
testExpression = `<Text>{data.items.${methodName}(0, 2)}</Text>`;
|
|
252
|
+
}
|
|
253
|
+
else if (methodName === "join") {
|
|
254
|
+
testExpression = `<Text>{data.items.${methodName}(", ")}</Text>`;
|
|
255
|
+
}
|
|
256
|
+
else if (methodName === "includes" || methodName === "indexOf") {
|
|
257
|
+
testExpression = `<Text>{data.items.${methodName}("test")}</Text>`;
|
|
258
|
+
}
|
|
259
|
+
else if (methodName === "find" || methodName === "some" || methodName === "every") {
|
|
260
|
+
// These methods need proper boolean-returning callbacks
|
|
261
|
+
testExpression = `<Text>{data.items.${methodName}(x => x === "test")}</Text>`;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// Other array methods with basic usage
|
|
265
|
+
testExpression = `<Text>{data.items.${methodName}(x => x.length > 0)}</Text>`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (objectName === "String.prototype") {
|
|
269
|
+
// String methods
|
|
270
|
+
if (methodName === "charAt" || methodName === "charCodeAt") {
|
|
271
|
+
testExpression = `<Text>{data.text.${methodName}(0)}</Text>`;
|
|
272
|
+
}
|
|
273
|
+
else if (methodName === "slice" || methodName === "substring") {
|
|
274
|
+
testExpression = `<Text>{data.text.${methodName}(0, 5)}</Text>`;
|
|
275
|
+
}
|
|
276
|
+
else if (methodName === "indexOf" || methodName === "lastIndexOf") {
|
|
277
|
+
testExpression = `<Text>{data.text.${methodName}("test")}</Text>`;
|
|
278
|
+
}
|
|
279
|
+
else if (methodName === "split") {
|
|
280
|
+
testExpression = `<Text>{data.text.${methodName}(",")}</Text>`;
|
|
281
|
+
}
|
|
282
|
+
else if (methodName === "concat") {
|
|
283
|
+
testExpression = `<Text>{data.text.${methodName}(" world")}</Text>`;
|
|
284
|
+
}
|
|
285
|
+
else if (methodName === "includes" || methodName === "startsWith" || methodName === "endsWith") {
|
|
286
|
+
testExpression = `<Text>{data.text.${methodName}("test")}</Text>`;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// Methods with no parameters like trim, toLowerCase
|
|
290
|
+
testExpression = `<Text>{data.text.${methodName}()}</Text>`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Fallback
|
|
295
|
+
testExpression = `<Text>{${objectName}.${methodName}()}</Text>`;
|
|
296
|
+
}
|
|
297
|
+
const ast = parse(testExpression);
|
|
298
|
+
const result = analyze(ast, baseSchema);
|
|
299
|
+
expect(result.hasErrors).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
// Test parameter validation for methods that have parameter constraints
|
|
302
|
+
const requiredParams = methodDef.params.filter((p) => p.required);
|
|
303
|
+
const nonVariadicParams = methodDef.params.filter((p) => !p.variadic);
|
|
304
|
+
if (requiredParams.length > 0) {
|
|
305
|
+
it(`should validate ${displayName} minimum parameters`, () => {
|
|
306
|
+
let testExpression;
|
|
307
|
+
if (objectName === "Math") {
|
|
308
|
+
testExpression = `<Text>{Math.${methodName}()}</Text>`;
|
|
309
|
+
}
|
|
310
|
+
else if (objectName === "Array.prototype") {
|
|
311
|
+
testExpression = `<Text>{data.items.${methodName}()}</Text>`;
|
|
312
|
+
}
|
|
313
|
+
else if (objectName === "String.prototype") {
|
|
314
|
+
testExpression = `<Text>{data.text.${methodName}()}</Text>`;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
testExpression = `<Text>{${objectName}.${methodName}()}</Text>`;
|
|
318
|
+
}
|
|
319
|
+
const ast = parse(testExpression);
|
|
320
|
+
const result = analyze(ast, baseSchema);
|
|
321
|
+
expect(result.hasWarnings).toBe(true);
|
|
322
|
+
expect(result.warnings[0].message).toContain(`${displayName} expects at least ${requiredParams.length} parameter`);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
// Test maximum parameters for non-variadic methods
|
|
326
|
+
if (nonVariadicParams.length === methodDef.params.length && methodDef.params.length > 0) {
|
|
327
|
+
it(`should validate ${displayName} maximum parameters`, () => {
|
|
328
|
+
// Create a call with too many parameters
|
|
329
|
+
const maxParams = methodDef.params.length;
|
|
330
|
+
const extraParams = Array(maxParams + 2)
|
|
331
|
+
.fill("1")
|
|
332
|
+
.join(", ");
|
|
333
|
+
let testExpression;
|
|
334
|
+
if (objectName === "Math") {
|
|
335
|
+
testExpression = `<Text>{Math.${methodName}(${extraParams})}</Text>`;
|
|
336
|
+
}
|
|
337
|
+
else if (objectName === "Array.prototype") {
|
|
338
|
+
if (methodName === "map" || methodName === "filter") {
|
|
339
|
+
testExpression = `<Text>{data.items.${methodName}(x => x, {}, ${extraParams})}</Text>`;
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
testExpression = `<Text>{data.items.${methodName}(${extraParams})}</Text>`;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else if (objectName === "String.prototype") {
|
|
346
|
+
testExpression = `<Text>{data.text.${methodName}(${extraParams})}</Text>`;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
testExpression = `<Text>{${objectName}.${methodName}(${extraParams})}</Text>`;
|
|
350
|
+
}
|
|
351
|
+
const ast = parse(testExpression);
|
|
352
|
+
const result = analyze(ast, baseSchema);
|
|
353
|
+
expect(result.hasWarnings).toBe(true);
|
|
354
|
+
expect(result.warnings[0].message).toContain(`${displayName} expects at most ${maxParams} parameter`);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
describe("builtin properties - dynamic validation", () => {
|
|
362
|
+
const builtins = getBuiltins();
|
|
363
|
+
Object.entries(builtins).forEach(([objectName, schema]) => {
|
|
364
|
+
if (!schema.properties)
|
|
365
|
+
return;
|
|
366
|
+
describe(`${objectName} properties`, () => {
|
|
367
|
+
Object.entries(schema.properties).forEach(([propName, propDef]) => {
|
|
368
|
+
it(`should allow ${objectName}.${propName} property access`, () => {
|
|
369
|
+
let testExpression;
|
|
370
|
+
if (objectName === "Array.prototype") {
|
|
371
|
+
testExpression = `<Text>{data.items.${propName}}</Text>`;
|
|
372
|
+
}
|
|
373
|
+
else if (objectName === "String.prototype") {
|
|
374
|
+
testExpression = `<Text>{data.text.${propName}}</Text>`;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
testExpression = `<Text>{${objectName}.${propName}}</Text>`;
|
|
378
|
+
}
|
|
379
|
+
const ast = parse(testExpression);
|
|
380
|
+
const result = analyze(ast, baseSchema);
|
|
381
|
+
expect(result.hasErrors).toBe(false);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
describe("blocked methods", () => {
|
|
388
|
+
it("should block mutating array methods", () => {
|
|
389
|
+
const blockedMethods = ["push", "pop", "shift", "unshift", "splice", "reverse", "sort"];
|
|
390
|
+
blockedMethods.forEach((method) => {
|
|
391
|
+
const ast = parse(`<Text>{data.items.${method}()}</Text>`);
|
|
392
|
+
const result = analyze(ast, baseSchema);
|
|
393
|
+
expect(result.hasErrors).toBe(true);
|
|
394
|
+
expect(result.errors[0].message).toContain(`method .${method} not allowed`);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
it("should block Math.random (non-deterministic)", () => {
|
|
398
|
+
const ast = parse("<Text>{Math.random()}</Text>");
|
|
399
|
+
const result = analyze(ast, baseSchema);
|
|
400
|
+
expect(result.hasErrors).toBe(true);
|
|
401
|
+
expect(result.errors[0].message).toContain("method Math.random not allowed");
|
|
402
|
+
});
|
|
403
|
+
it("should block unknown methods", () => {
|
|
404
|
+
const ast = parse("<Text>{Math.unknownMethod()}</Text>");
|
|
405
|
+
const result = analyze(ast, baseSchema);
|
|
406
|
+
expect(result.hasErrors).toBe(true);
|
|
407
|
+
expect(result.errors[0].message).toContain("method Math.unknownMethod not allowed");
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
describe("custom builtin schema", () => {
|
|
411
|
+
it("should work with custom builtin schema", () => {
|
|
412
|
+
const customSchema = {
|
|
413
|
+
data: {
|
|
414
|
+
value: { type: "number" },
|
|
415
|
+
},
|
|
416
|
+
elements: {
|
|
417
|
+
Text: { props: {} },
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
// Math.max should work with default builtins
|
|
421
|
+
const ast1 = parse("<Text>{Math.max(data.value, 10)}</Text>");
|
|
422
|
+
const result1 = analyze(ast1, customSchema);
|
|
423
|
+
expect(result1.hasErrors).toBe(false);
|
|
424
|
+
// Math.min should also work since we use BUILTINS
|
|
425
|
+
const ast2 = parse("<Text>{Math.min(data.value, 10)}</Text>");
|
|
426
|
+
const result2 = analyze(ast2, customSchema);
|
|
427
|
+
expect(result2.hasErrors).toBe(false);
|
|
428
|
+
});
|
|
429
|
+
it("should allow custom String methods in builtin schema", () => {
|
|
430
|
+
const customSchema = {
|
|
431
|
+
data: {
|
|
432
|
+
text: { type: "string" },
|
|
433
|
+
},
|
|
434
|
+
elements: {
|
|
435
|
+
Text: { props: {} },
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
const ast1 = parse("<Text>{data.text.toUpperCase()}</Text>");
|
|
439
|
+
const result1 = analyze(ast1, customSchema);
|
|
440
|
+
expect(result1.hasErrors).toBe(false);
|
|
441
|
+
const ast2 = parse("<Text>{data.text.toLowerCase()}</Text>");
|
|
442
|
+
const result2 = analyze(ast2, customSchema);
|
|
443
|
+
expect(result2.hasErrors).toBe(false);
|
|
444
|
+
});
|
|
445
|
+
it("should fall back to default builtin schema when not specified", () => {
|
|
446
|
+
const schemaWithoutBuiltins = {
|
|
447
|
+
data: {
|
|
448
|
+
x: { type: "number" },
|
|
449
|
+
},
|
|
450
|
+
elements: {
|
|
451
|
+
Text: { props: {} },
|
|
452
|
+
},
|
|
453
|
+
// No builtins specified - should use default
|
|
454
|
+
};
|
|
455
|
+
// Should work with default builtin schema
|
|
456
|
+
const ast = parse("<Text>{Math.max(data.x, 10)}</Text>");
|
|
457
|
+
const result = analyze(ast, schemaWithoutBuiltins);
|
|
458
|
+
expect(result.hasErrors).toBe(false);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
describe("JSX validation", () => {
|
|
463
|
+
describe("elements", () => {
|
|
464
|
+
it("should allow valid elements", () => {
|
|
465
|
+
const ast = parse("<Text>Hello</Text>");
|
|
466
|
+
const result = analyze(ast, baseSchema);
|
|
467
|
+
expect(result.hasErrors).toBe(false);
|
|
468
|
+
});
|
|
469
|
+
it("should accept enum-constrained attribute values", () => {
|
|
470
|
+
const schema = {
|
|
471
|
+
...baseSchema,
|
|
472
|
+
elements: {
|
|
473
|
+
...baseSchema.elements,
|
|
474
|
+
Badge: {
|
|
475
|
+
props: {
|
|
476
|
+
variant: { type: "string", enum: ["primary", "secondary"] },
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
const ast = parse('<Badge variant="primary" />');
|
|
482
|
+
const result = analyze(ast, schema);
|
|
483
|
+
expect(result.hasWarnings).toBe(false);
|
|
484
|
+
expect(result.hasErrors).toBe(false);
|
|
485
|
+
});
|
|
486
|
+
it("should report invalid enum attribute values", () => {
|
|
487
|
+
const schema = {
|
|
488
|
+
...baseSchema,
|
|
489
|
+
elements: {
|
|
490
|
+
...baseSchema.elements,
|
|
491
|
+
Badge: {
|
|
492
|
+
props: {
|
|
493
|
+
variant: { type: "string", enum: ["primary", "secondary"] },
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
const ast = parse('<Badge variant="danger" />');
|
|
499
|
+
const result = analyze(ast, schema);
|
|
500
|
+
expect(result.hasErrors).toBe(true);
|
|
501
|
+
expect(result.errors[0].code).toBe("INVALID_ATTRIBUTE_VALUE");
|
|
502
|
+
expect(result.errors[0].suggestions).toEqual(["primary", "secondary"]);
|
|
503
|
+
});
|
|
504
|
+
it("should block invalid elements", () => {
|
|
505
|
+
const ast = parse("<InvalidElement>Hello</InvalidElement>");
|
|
506
|
+
const result = analyze(ast, baseSchema);
|
|
507
|
+
expect(result.hasErrors).toBe(true);
|
|
508
|
+
expect(result.errors[0].message).toContain("Element <InvalidElement> not allowed");
|
|
509
|
+
});
|
|
510
|
+
it("should allow nested valid elements", () => {
|
|
511
|
+
const ast = parse(`
|
|
512
|
+
<div>
|
|
513
|
+
<Text>Hello</Text>
|
|
514
|
+
<span>World</span>
|
|
515
|
+
</div>
|
|
516
|
+
`);
|
|
517
|
+
const result = analyze(ast, baseSchema);
|
|
518
|
+
expect(result.hasErrors).toBe(false);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
describe("spread attributes", () => {
|
|
522
|
+
it("should allow spread with data object", () => {
|
|
523
|
+
const schema = {
|
|
524
|
+
data: {
|
|
525
|
+
props: {
|
|
526
|
+
type: "object",
|
|
527
|
+
shape: {
|
|
528
|
+
title: { type: "string" },
|
|
529
|
+
count: { type: "number" },
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
elements: {
|
|
534
|
+
Card: {
|
|
535
|
+
props: {
|
|
536
|
+
title: { type: "string" },
|
|
537
|
+
count: { type: "number" },
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
const ast = parse("<Card {...data.props} />");
|
|
543
|
+
const result = analyze(ast, schema);
|
|
544
|
+
expect(result.hasErrors).toBe(false);
|
|
545
|
+
});
|
|
546
|
+
it("should allow spread with nested data object", () => {
|
|
547
|
+
const schema = {
|
|
548
|
+
data: {
|
|
549
|
+
user: {
|
|
550
|
+
type: "object",
|
|
551
|
+
shape: {
|
|
552
|
+
attributes: {
|
|
553
|
+
type: "object",
|
|
554
|
+
shape: {
|
|
555
|
+
style: { type: "string" },
|
|
556
|
+
className: { type: "string" },
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
elements: {
|
|
563
|
+
Text: {
|
|
564
|
+
props: {
|
|
565
|
+
style: { type: "string" },
|
|
566
|
+
className: { type: "string" },
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
const ast = parse("<Text {...data.user.attributes} />");
|
|
572
|
+
const result = analyze(ast, schema);
|
|
573
|
+
expect(result.hasErrors).toBe(false);
|
|
574
|
+
});
|
|
575
|
+
it("should allow spread with arrow function parameter", () => {
|
|
576
|
+
const schema = {
|
|
577
|
+
data: {
|
|
578
|
+
items: {
|
|
579
|
+
type: "array",
|
|
580
|
+
shape: {
|
|
581
|
+
type: "object",
|
|
582
|
+
shape: {
|
|
583
|
+
name: { type: "string" },
|
|
584
|
+
props: {
|
|
585
|
+
type: "object",
|
|
586
|
+
shape: {
|
|
587
|
+
color: { type: "string" },
|
|
588
|
+
size: { type: "number" },
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
elements: {
|
|
596
|
+
Item: {
|
|
597
|
+
props: {
|
|
598
|
+
color: { type: "string" },
|
|
599
|
+
size: { type: "number" },
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
div: {
|
|
603
|
+
props: {},
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
const ast = parse("<div>{data.items.map(item => <Item {...item.props} />)}</div>");
|
|
608
|
+
const result = analyze(ast, schema);
|
|
609
|
+
expect(result.hasErrors).toBe(false);
|
|
610
|
+
});
|
|
611
|
+
it("should allow spread with the parameter itself", () => {
|
|
612
|
+
const schema = {
|
|
613
|
+
data: {
|
|
614
|
+
items: {
|
|
615
|
+
type: "array",
|
|
616
|
+
shape: {
|
|
617
|
+
type: "object",
|
|
618
|
+
shape: {
|
|
619
|
+
name: { type: "string" },
|
|
620
|
+
value: { type: "number" },
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
elements: {
|
|
626
|
+
Item: {
|
|
627
|
+
props: {
|
|
628
|
+
name: { type: "string" },
|
|
629
|
+
value: { type: "number" },
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
div: {
|
|
633
|
+
props: {},
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
const ast = parse("<div>{data.items.map(item => <Item {...item} />)}</div>");
|
|
638
|
+
const result = analyze(ast, schema);
|
|
639
|
+
expect(result.hasErrors).toBe(false);
|
|
640
|
+
});
|
|
641
|
+
it("should allow spread combined with regular attributes", () => {
|
|
642
|
+
const schema = {
|
|
643
|
+
data: {
|
|
644
|
+
props: {
|
|
645
|
+
type: "object",
|
|
646
|
+
shape: {
|
|
647
|
+
title: { type: "string" },
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
elements: {
|
|
652
|
+
Card: {
|
|
653
|
+
props: {
|
|
654
|
+
title: { type: "string" },
|
|
655
|
+
highlighted: { type: "boolean" },
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
const ast = parse("<Card {...data.props} highlighted={true} />");
|
|
661
|
+
const result = analyze(ast, schema);
|
|
662
|
+
expect(result.hasErrors).toBe(false);
|
|
663
|
+
});
|
|
664
|
+
it("should allow multiple spreads", () => {
|
|
665
|
+
const schema = {
|
|
666
|
+
data: {
|
|
667
|
+
baseProps: {
|
|
668
|
+
type: "object",
|
|
669
|
+
shape: {
|
|
670
|
+
title: { type: "string" },
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
extraProps: {
|
|
674
|
+
type: "object",
|
|
675
|
+
shape: {
|
|
676
|
+
count: { type: "number" },
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
elements: {
|
|
681
|
+
Card: {
|
|
682
|
+
props: {
|
|
683
|
+
title: { type: "string" },
|
|
684
|
+
count: { type: "number" },
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
const ast = parse("<Card {...data.baseProps} {...data.extraProps} />");
|
|
690
|
+
const result = analyze(ast, schema);
|
|
691
|
+
expect(result.hasErrors).toBe(false);
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
describe("empty expressions", () => {
|
|
695
|
+
it("should allow empty JSX expressions {} in children", () => {
|
|
696
|
+
const ast = parse("<Text>{}</Text>");
|
|
697
|
+
const result = analyze(ast, baseSchema);
|
|
698
|
+
expect(result.hasErrors).toBe(false);
|
|
699
|
+
});
|
|
700
|
+
it("should allow multiple empty expressions in children", () => {
|
|
701
|
+
const ast = parse("<Text>{}{}{}</Text>");
|
|
702
|
+
const result = analyze(ast, baseSchema);
|
|
703
|
+
expect(result.hasErrors).toBe(false);
|
|
704
|
+
});
|
|
705
|
+
it("should allow empty expressions mixed with content in children", () => {
|
|
706
|
+
const ast = parse("<Text>Hello {}{data.user.name}{} World</Text>");
|
|
707
|
+
const result = analyze(ast, baseSchema);
|
|
708
|
+
expect(result.hasErrors).toBe(false);
|
|
709
|
+
});
|
|
710
|
+
it("should handle empty expressions without compile errors", () => {
|
|
711
|
+
const ast = parse("<Text>{}</Text>");
|
|
712
|
+
const result = analyze(ast, baseSchema);
|
|
713
|
+
expect(result.hasErrors).toBe(false);
|
|
714
|
+
expect(ast).toBeDefined();
|
|
715
|
+
expect(ast.body).toHaveLength(1);
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
describe("security validation", () => {
|
|
720
|
+
describe("disallowed identifiers", () => {
|
|
721
|
+
it("should block window access", () => {
|
|
722
|
+
const ast = parse("<Text>{window.alert('hack')}</Text>");
|
|
723
|
+
const result = analyze(ast, baseSchema);
|
|
724
|
+
expect(result.hasErrors).toBe(true);
|
|
725
|
+
expect(result.errors[0].message).toContain('Identifier "window" not allowed');
|
|
726
|
+
});
|
|
727
|
+
it("should block document access", () => {
|
|
728
|
+
const ast = parse("<Text>{document.body}</Text>");
|
|
729
|
+
const result = analyze(ast, baseSchema);
|
|
730
|
+
expect(result.hasErrors).toBe(true);
|
|
731
|
+
expect(result.errors[0].message).toContain('Identifier "document" not allowed');
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
describe("dangerous expressions", () => {
|
|
735
|
+
it("should block eval", () => {
|
|
736
|
+
const ast = parse("<Text>{eval('alert(1)')}</Text>");
|
|
737
|
+
const result = analyze(ast, baseSchema);
|
|
738
|
+
expect(result.hasErrors).toBe(true);
|
|
739
|
+
expect(result.errors[0].message).toContain('Identifier "eval" not allowed');
|
|
740
|
+
});
|
|
741
|
+
it("should block Function constructor", () => {
|
|
742
|
+
const ast = parse("<Text>{new Function('return 1')()}</Text>");
|
|
743
|
+
const result = analyze(ast, baseSchema);
|
|
744
|
+
expect(result.hasErrors).toBe(true);
|
|
745
|
+
expect(result.errors[0].message).toContain('Identifier "Function" not allowed');
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
describe("integration scenarios", () => {
|
|
750
|
+
it("should catch multiple errors in complex expressions", () => {
|
|
751
|
+
const ast = parse(`
|
|
752
|
+
<InvalidElement>
|
|
753
|
+
<Text>{data.user.invalid}</Text>
|
|
754
|
+
<span>{window.location}</span>
|
|
755
|
+
</InvalidElement>
|
|
756
|
+
`);
|
|
757
|
+
const result = analyze(ast, baseSchema);
|
|
758
|
+
expect(result.hasErrors).toBe(true);
|
|
759
|
+
expect(result.errors.length).toBeGreaterThan(1);
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
//# sourceMappingURL=analyze.test.js.map
|