jsxpression 0.1.2 → 0.1.3
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/lib/analyze/analysis-error.js +1 -5
- package/lib/analyze/analysis-error.js.map +1 -1
- package/lib/analyze/analysis-report.js +1 -5
- package/lib/analyze/analysis-report.js.map +1 -1
- package/lib/analyze/analyze.d.ts +1 -1
- package/lib/analyze/analyze.d.ts.map +1 -1
- package/lib/analyze/analyze.js +6 -9
- package/lib/analyze/analyze.js.map +1 -1
- package/lib/analyze/analyze.test.js +312 -314
- package/lib/analyze/analyze.test.js.map +1 -1
- package/lib/analyze/data/analyze.d.ts +1 -1
- package/lib/analyze/data/analyze.d.ts.map +1 -1
- package/lib/analyze/data/analyze.js +7 -10
- package/lib/analyze/data/analyze.js.map +1 -1
- package/lib/analyze/data/data-access.d.ts +1 -1
- package/lib/analyze/data/data-access.d.ts.map +1 -1
- package/lib/analyze/data/data-access.js +20 -24
- package/lib/analyze/data/data-access.js.map +1 -1
- package/lib/analyze/data/index.d.ts +1 -1
- package/lib/analyze/data/index.d.ts.map +1 -1
- package/lib/analyze/data/index.js +1 -5
- package/lib/analyze/data/index.js.map +1 -1
- package/lib/analyze/data/method-calls.d.ts +1 -1
- package/lib/analyze/data/method-calls.d.ts.map +1 -1
- package/lib/analyze/data/method-calls.js +17 -20
- package/lib/analyze/data/method-calls.js.map +1 -1
- package/lib/analyze/data/utils.d.ts +1 -1
- package/lib/analyze/data/utils.d.ts.map +1 -1
- package/lib/analyze/data/utils.js +22 -29
- package/lib/analyze/data/utils.js.map +1 -1
- package/lib/analyze/index.d.ts +1 -1
- package/lib/analyze/index.d.ts.map +1 -1
- package/lib/analyze/index.js +3 -9
- package/lib/analyze/index.js.map +1 -1
- package/lib/analyze/jsx/analyze.d.ts +1 -1
- package/lib/analyze/jsx/analyze.d.ts.map +1 -1
- package/lib/analyze/jsx/analyze.js +8 -11
- package/lib/analyze/jsx/analyze.js.map +1 -1
- package/lib/analyze/jsx/element-attributes.d.ts +1 -1
- package/lib/analyze/jsx/element-attributes.d.ts.map +1 -1
- package/lib/analyze/jsx/element-attributes.js +21 -24
- package/lib/analyze/jsx/element-attributes.js.map +1 -1
- package/lib/analyze/jsx/element-children.d.ts +1 -1
- package/lib/analyze/jsx/element-children.d.ts.map +1 -1
- package/lib/analyze/jsx/element-children.js +20 -23
- package/lib/analyze/jsx/element-children.js.map +1 -1
- package/lib/analyze/jsx/element-tags.d.ts +1 -1
- package/lib/analyze/jsx/element-tags.d.ts.map +1 -1
- package/lib/analyze/jsx/element-tags.js +12 -15
- package/lib/analyze/jsx/element-tags.js.map +1 -1
- package/lib/analyze/jsx/index.d.ts +1 -1
- package/lib/analyze/jsx/index.d.ts.map +1 -1
- package/lib/analyze/jsx/index.js +1 -5
- package/lib/analyze/jsx/index.js.map +1 -1
- package/lib/analyze/security/analyze.d.ts +1 -1
- package/lib/analyze/security/analyze.d.ts.map +1 -1
- package/lib/analyze/security/analyze.js +10 -13
- package/lib/analyze/security/analyze.js.map +1 -1
- package/lib/analyze/security/call-expressions.d.ts +1 -1
- package/lib/analyze/security/call-expressions.d.ts.map +1 -1
- package/lib/analyze/security/call-expressions.js +8 -11
- package/lib/analyze/security/call-expressions.js.map +1 -1
- package/lib/analyze/security/declarations.d.ts +1 -1
- package/lib/analyze/security/declarations.d.ts.map +1 -1
- package/lib/analyze/security/declarations.js +12 -15
- package/lib/analyze/security/declarations.js.map +1 -1
- package/lib/analyze/security/expressions.d.ts +1 -1
- package/lib/analyze/security/expressions.d.ts.map +1 -1
- package/lib/analyze/security/expressions.js +14 -17
- package/lib/analyze/security/expressions.js.map +1 -1
- package/lib/analyze/security/identifiers.d.ts +1 -1
- package/lib/analyze/security/identifiers.d.ts.map +1 -1
- package/lib/analyze/security/identifiers.js +10 -13
- package/lib/analyze/security/identifiers.js.map +1 -1
- package/lib/analyze/security/index.d.ts +1 -1
- package/lib/analyze/security/index.d.ts.map +1 -1
- package/lib/analyze/security/index.js +1 -5
- package/lib/analyze/security/index.js.map +1 -1
- package/lib/analyze/security/member-access.d.ts +1 -1
- package/lib/analyze/security/member-access.d.ts.map +1 -1
- package/lib/analyze/security/member-access.js +7 -10
- package/lib/analyze/security/member-access.js.map +1 -1
- package/lib/analyze/utils.js +4 -10
- package/lib/analyze/utils.js.map +1 -1
- package/lib/analyze/validation-context.js +1 -5
- package/lib/analyze/validation-context.js.map +1 -1
- package/lib/builtins.js +9 -20
- package/lib/builtins.js.map +1 -1
- package/lib/codegen/builtins/builtin-types.js +3 -6
- package/lib/codegen/builtins/builtin-types.js.map +1 -1
- package/lib/codegen/builtins/index.js +1 -5
- package/lib/codegen/builtins/index.js.map +1 -1
- package/lib/codegen/generate-typescript-definitions.js +4 -7
- package/lib/codegen/generate-typescript-definitions.js.map +1 -1
- package/lib/codegen/generate-typescript-definitions.test.js +190 -192
- package/lib/codegen/generate-typescript-definitions.test.js.map +1 -1
- package/lib/codegen/index.js +1 -5
- package/lib/codegen/index.js.map +1 -1
- package/lib/codegen/schema/component-types.js +3 -6
- package/lib/codegen/schema/component-types.js.map +1 -1
- package/lib/codegen/schema/data-types.js +3 -6
- package/lib/codegen/schema/data-types.js.map +1 -1
- package/lib/codegen/schema/index.js +2 -7
- package/lib/codegen/schema/index.js.map +1 -1
- package/lib/codegen/schema/utils.js +3 -8
- package/lib/codegen/schema/utils.js.map +1 -1
- package/lib/compile/compilation-error.js +1 -5
- package/lib/compile/compilation-error.js.map +1 -1
- package/lib/compile/compile.d.ts +1 -1
- package/lib/compile/compile.d.ts.map +1 -1
- package/lib/compile/compile.js +20 -25
- package/lib/compile/compile.js.map +1 -1
- package/lib/compile/compile.test.js +110 -112
- package/lib/compile/compile.test.js.map +1 -1
- package/lib/compile/index.d.ts +1 -1
- package/lib/compile/index.d.ts.map +1 -1
- package/lib/compile/index.js +2 -7
- package/lib/compile/index.js.map +1 -1
- package/lib/evaluate/evaluate.js +5 -8
- package/lib/evaluate/evaluate.js.map +1 -1
- package/lib/evaluate/evaluate.test.js +56 -58
- package/lib/evaluate/evaluate.test.js.map +1 -1
- package/lib/evaluate/evaluation-error.js +1 -5
- package/lib/evaluate/evaluation-error.js.map +1 -1
- package/lib/evaluate/index.d.ts +1 -1
- package/lib/evaluate/index.d.ts.map +1 -1
- package/lib/evaluate/index.js +2 -7
- package/lib/evaluate/index.js.map +1 -1
- package/lib/index.js +6 -15
- package/lib/index.js.map +1 -1
- package/lib/jsx.d.ts +1 -1
- package/lib/jsx.d.ts.map +1 -1
- package/lib/jsx.js +10 -22
- package/lib/jsx.js.map +1 -1
- package/lib/parse/index.d.ts +1 -1
- package/lib/parse/index.d.ts.map +1 -1
- package/lib/parse/index.js +2 -7
- package/lib/parse/index.js.map +1 -1
- package/lib/parse/parse-error.js +1 -5
- package/lib/parse/parse-error.js.map +1 -1
- package/lib/parse/parse.d.ts +1 -1
- package/lib/parse/parse.d.ts.map +1 -1
- package/lib/parse/parse.js +7 -13
- package/lib/parse/parse.js.map +1 -1
- package/lib/parse/parse.test.js +56 -58
- package/lib/parse/parse.test.js.map +1 -1
- package/lib/render.d.ts +1 -1
- package/lib/render.d.ts.map +1 -1
- package/lib/render.js +9 -12
- package/lib/render.js.map +1 -1
- package/lib/render.test.js +62 -64
- package/lib/render.test.js.map +1 -1
- package/lib/schema.js +18 -38
- package/lib/schema.js.map +1 -1
- package/lib/traverse.d.ts +3 -3
- package/lib/traverse.d.ts.map +1 -1
- package/lib/traverse.js +8 -11
- package/lib/traverse.js.map +1 -1
- package/lib/traverse.test.js +34 -36
- package/lib/traverse.test.js.map +1 -1
- package/lib/types.js +1 -2
- package/lib/validate.js +7 -10
- package/lib/validate.js.map +1 -1
- package/package.json +1 -1
- package/src/compile/compile.ts +3 -4
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const builtins_js_1 = require("../builtins.js");
|
|
7
|
-
(0, vitest_js_1.describe)("analyze - consolidated tests", () => {
|
|
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", () => {
|
|
8
6
|
const baseSchema = {
|
|
9
7
|
data: {
|
|
10
8
|
user: {
|
|
@@ -71,164 +69,164 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
71
69
|
span: { props: {} },
|
|
72
70
|
},
|
|
73
71
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const ast =
|
|
78
|
-
const result =
|
|
79
|
-
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const ast =
|
|
83
|
-
const result =
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const ast =
|
|
88
|
-
const result =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const ast =
|
|
94
|
-
const result =
|
|
95
|
-
|
|
96
|
-
|
|
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");
|
|
97
95
|
});
|
|
98
96
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const ast =
|
|
102
|
-
const result =
|
|
103
|
-
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const ast =
|
|
107
|
-
const result =
|
|
108
|
-
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const ast =
|
|
112
|
-
const result =
|
|
113
|
-
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const ast =
|
|
117
|
-
const result =
|
|
118
|
-
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const ast =
|
|
122
|
-
const result =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const ast =
|
|
128
|
-
const result =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const ast =
|
|
135
|
-
const result =
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const ast =
|
|
140
|
-
const result =
|
|
141
|
-
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const ast =
|
|
145
|
-
const result =
|
|
146
|
-
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const ast =
|
|
150
|
-
const result =
|
|
151
|
-
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const ast =
|
|
155
|
-
const result =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const ast =
|
|
161
|
-
const result =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const ast =
|
|
167
|
-
const result =
|
|
168
|
-
|
|
169
|
-
|
|
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");
|
|
170
168
|
});
|
|
171
169
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const ast =
|
|
175
|
-
const result =
|
|
176
|
-
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
const ast =
|
|
180
|
-
const result =
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const ast =
|
|
186
|
-
const result =
|
|
187
|
-
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const ast =
|
|
191
|
-
const result =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
const ast =
|
|
197
|
-
const result =
|
|
198
|
-
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const ast =
|
|
202
|
-
const result =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const ast =
|
|
208
|
-
const result =
|
|
209
|
-
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
const ast =
|
|
213
|
-
const result =
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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"]);
|
|
217
215
|
});
|
|
218
216
|
});
|
|
219
217
|
});
|
|
220
|
-
|
|
218
|
+
describe("method calls validation", () => {
|
|
221
219
|
// Dynamic builtin schema tests - generates tests for all methods in BUILTINS
|
|
222
|
-
|
|
223
|
-
const builtins =
|
|
220
|
+
describe("builtin methods - dynamic validation", () => {
|
|
221
|
+
const builtins = getBuiltins();
|
|
224
222
|
// Test each builtin object (Math, Array.prototype, String.prototype)
|
|
225
223
|
Object.entries(builtins).forEach(([objectName, schema]) => {
|
|
226
224
|
if (!schema.methods)
|
|
227
225
|
return;
|
|
228
|
-
|
|
226
|
+
describe(`${objectName} methods`, () => {
|
|
229
227
|
Object.entries(schema.methods).forEach(([methodName, methodDef]) => {
|
|
230
228
|
const displayName = objectName === "Math" ? `Math.${methodName}` : methodName;
|
|
231
|
-
|
|
229
|
+
it(`should allow ${displayName} with valid parameters`, () => {
|
|
232
230
|
let testExpression;
|
|
233
231
|
if (objectName === "Math") {
|
|
234
232
|
// Math methods
|
|
@@ -296,15 +294,15 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
296
294
|
// Fallback
|
|
297
295
|
testExpression = `<Text>{${objectName}.${methodName}()}</Text>`;
|
|
298
296
|
}
|
|
299
|
-
const ast =
|
|
300
|
-
const result =
|
|
301
|
-
|
|
297
|
+
const ast = parse(testExpression);
|
|
298
|
+
const result = analyze(ast, baseSchema);
|
|
299
|
+
expect(result.hasErrors).toBe(false);
|
|
302
300
|
});
|
|
303
301
|
// Test parameter validation for methods that have parameter constraints
|
|
304
302
|
const requiredParams = methodDef.params.filter((p) => p.required);
|
|
305
303
|
const nonVariadicParams = methodDef.params.filter((p) => !p.variadic);
|
|
306
304
|
if (requiredParams.length > 0) {
|
|
307
|
-
|
|
305
|
+
it(`should validate ${displayName} minimum parameters`, () => {
|
|
308
306
|
let testExpression;
|
|
309
307
|
if (objectName === "Math") {
|
|
310
308
|
testExpression = `<Text>{Math.${methodName}()}</Text>`;
|
|
@@ -318,15 +316,15 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
318
316
|
else {
|
|
319
317
|
testExpression = `<Text>{${objectName}.${methodName}()}</Text>`;
|
|
320
318
|
}
|
|
321
|
-
const ast =
|
|
322
|
-
const result =
|
|
323
|
-
|
|
324
|
-
|
|
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`);
|
|
325
323
|
});
|
|
326
324
|
}
|
|
327
325
|
// Test maximum parameters for non-variadic methods
|
|
328
326
|
if (nonVariadicParams.length === methodDef.params.length && methodDef.params.length > 0) {
|
|
329
|
-
|
|
327
|
+
it(`should validate ${displayName} maximum parameters`, () => {
|
|
330
328
|
// Create a call with too many parameters
|
|
331
329
|
const maxParams = methodDef.params.length;
|
|
332
330
|
const extraParams = Array(maxParams + 2)
|
|
@@ -350,24 +348,24 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
350
348
|
else {
|
|
351
349
|
testExpression = `<Text>{${objectName}.${methodName}(${extraParams})}</Text>`;
|
|
352
350
|
}
|
|
353
|
-
const ast =
|
|
354
|
-
const result =
|
|
355
|
-
|
|
356
|
-
|
|
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`);
|
|
357
355
|
});
|
|
358
356
|
}
|
|
359
357
|
});
|
|
360
358
|
});
|
|
361
359
|
});
|
|
362
360
|
});
|
|
363
|
-
|
|
364
|
-
const builtins =
|
|
361
|
+
describe("builtin properties - dynamic validation", () => {
|
|
362
|
+
const builtins = getBuiltins();
|
|
365
363
|
Object.entries(builtins).forEach(([objectName, schema]) => {
|
|
366
364
|
if (!schema.properties)
|
|
367
365
|
return;
|
|
368
|
-
|
|
366
|
+
describe(`${objectName} properties`, () => {
|
|
369
367
|
Object.entries(schema.properties).forEach(([propName, propDef]) => {
|
|
370
|
-
|
|
368
|
+
it(`should allow ${objectName}.${propName} property access`, () => {
|
|
371
369
|
let testExpression;
|
|
372
370
|
if (objectName === "Array.prototype") {
|
|
373
371
|
testExpression = `<Text>{data.items.${propName}}</Text>`;
|
|
@@ -378,39 +376,39 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
378
376
|
else {
|
|
379
377
|
testExpression = `<Text>{${objectName}.${propName}}</Text>`;
|
|
380
378
|
}
|
|
381
|
-
const ast =
|
|
382
|
-
const result =
|
|
383
|
-
|
|
379
|
+
const ast = parse(testExpression);
|
|
380
|
+
const result = analyze(ast, baseSchema);
|
|
381
|
+
expect(result.hasErrors).toBe(false);
|
|
384
382
|
});
|
|
385
383
|
});
|
|
386
384
|
});
|
|
387
385
|
});
|
|
388
386
|
});
|
|
389
|
-
|
|
390
|
-
|
|
387
|
+
describe("blocked methods", () => {
|
|
388
|
+
it("should block mutating array methods", () => {
|
|
391
389
|
const blockedMethods = ["push", "pop", "shift", "unshift", "splice", "reverse", "sort"];
|
|
392
390
|
blockedMethods.forEach((method) => {
|
|
393
|
-
const ast =
|
|
394
|
-
const result =
|
|
395
|
-
|
|
396
|
-
|
|
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`);
|
|
397
395
|
});
|
|
398
396
|
});
|
|
399
|
-
|
|
400
|
-
const ast =
|
|
401
|
-
const result =
|
|
402
|
-
|
|
403
|
-
|
|
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");
|
|
404
402
|
});
|
|
405
|
-
|
|
406
|
-
const ast =
|
|
407
|
-
const result =
|
|
408
|
-
|
|
409
|
-
|
|
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");
|
|
410
408
|
});
|
|
411
409
|
});
|
|
412
|
-
|
|
413
|
-
|
|
410
|
+
describe("custom builtin schema", () => {
|
|
411
|
+
it("should work with custom builtin schema", () => {
|
|
414
412
|
const customSchema = {
|
|
415
413
|
data: {
|
|
416
414
|
value: { type: "number" },
|
|
@@ -420,15 +418,15 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
420
418
|
},
|
|
421
419
|
};
|
|
422
420
|
// Math.max should work with default builtins
|
|
423
|
-
const ast1 =
|
|
424
|
-
const result1 =
|
|
425
|
-
|
|
421
|
+
const ast1 = parse("<Text>{Math.max(data.value, 10)}</Text>");
|
|
422
|
+
const result1 = analyze(ast1, customSchema);
|
|
423
|
+
expect(result1.hasErrors).toBe(false);
|
|
426
424
|
// Math.min should also work since we use BUILTINS
|
|
427
|
-
const ast2 =
|
|
428
|
-
const result2 =
|
|
429
|
-
|
|
425
|
+
const ast2 = parse("<Text>{Math.min(data.value, 10)}</Text>");
|
|
426
|
+
const result2 = analyze(ast2, customSchema);
|
|
427
|
+
expect(result2.hasErrors).toBe(false);
|
|
430
428
|
});
|
|
431
|
-
|
|
429
|
+
it("should allow custom String methods in builtin schema", () => {
|
|
432
430
|
const customSchema = {
|
|
433
431
|
data: {
|
|
434
432
|
text: { type: "string" },
|
|
@@ -437,14 +435,14 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
437
435
|
Text: { props: {} },
|
|
438
436
|
},
|
|
439
437
|
};
|
|
440
|
-
const ast1 =
|
|
441
|
-
const result1 =
|
|
442
|
-
|
|
443
|
-
const ast2 =
|
|
444
|
-
const result2 =
|
|
445
|
-
|
|
446
|
-
});
|
|
447
|
-
|
|
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", () => {
|
|
448
446
|
const schemaWithoutBuiltins = {
|
|
449
447
|
data: {
|
|
450
448
|
x: { type: "number" },
|
|
@@ -455,20 +453,20 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
455
453
|
// No builtins specified - should use default
|
|
456
454
|
};
|
|
457
455
|
// Should work with default builtin schema
|
|
458
|
-
const ast =
|
|
459
|
-
const result =
|
|
460
|
-
|
|
456
|
+
const ast = parse("<Text>{Math.max(data.x, 10)}</Text>");
|
|
457
|
+
const result = analyze(ast, schemaWithoutBuiltins);
|
|
458
|
+
expect(result.hasErrors).toBe(false);
|
|
461
459
|
});
|
|
462
460
|
});
|
|
463
461
|
});
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const ast =
|
|
468
|
-
const result =
|
|
469
|
-
|
|
470
|
-
});
|
|
471
|
-
|
|
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", () => {
|
|
472
470
|
const schema = {
|
|
473
471
|
...baseSchema,
|
|
474
472
|
elements: {
|
|
@@ -480,12 +478,12 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
480
478
|
},
|
|
481
479
|
},
|
|
482
480
|
};
|
|
483
|
-
const ast =
|
|
484
|
-
const result =
|
|
485
|
-
|
|
486
|
-
|
|
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);
|
|
487
485
|
});
|
|
488
|
-
|
|
486
|
+
it("should report invalid enum attribute values", () => {
|
|
489
487
|
const schema = {
|
|
490
488
|
...baseSchema,
|
|
491
489
|
elements: {
|
|
@@ -497,31 +495,31 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
497
495
|
},
|
|
498
496
|
},
|
|
499
497
|
};
|
|
500
|
-
const ast =
|
|
501
|
-
const result =
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
const ast =
|
|
508
|
-
const result =
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
const ast =
|
|
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(`
|
|
514
512
|
<div>
|
|
515
513
|
<Text>Hello</Text>
|
|
516
514
|
<span>World</span>
|
|
517
515
|
</div>
|
|
518
516
|
`);
|
|
519
|
-
const result =
|
|
520
|
-
|
|
517
|
+
const result = analyze(ast, baseSchema);
|
|
518
|
+
expect(result.hasErrors).toBe(false);
|
|
521
519
|
});
|
|
522
520
|
});
|
|
523
|
-
|
|
524
|
-
|
|
521
|
+
describe("spread attributes", () => {
|
|
522
|
+
it("should allow spread with data object", () => {
|
|
525
523
|
const schema = {
|
|
526
524
|
data: {
|
|
527
525
|
props: {
|
|
@@ -541,11 +539,11 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
541
539
|
},
|
|
542
540
|
},
|
|
543
541
|
};
|
|
544
|
-
const ast =
|
|
545
|
-
const result =
|
|
546
|
-
|
|
542
|
+
const ast = parse("<Card {...data.props} />");
|
|
543
|
+
const result = analyze(ast, schema);
|
|
544
|
+
expect(result.hasErrors).toBe(false);
|
|
547
545
|
});
|
|
548
|
-
|
|
546
|
+
it("should allow spread with nested data object", () => {
|
|
549
547
|
const schema = {
|
|
550
548
|
data: {
|
|
551
549
|
user: {
|
|
@@ -570,11 +568,11 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
570
568
|
},
|
|
571
569
|
},
|
|
572
570
|
};
|
|
573
|
-
const ast =
|
|
574
|
-
const result =
|
|
575
|
-
|
|
571
|
+
const ast = parse("<Text {...data.user.attributes} />");
|
|
572
|
+
const result = analyze(ast, schema);
|
|
573
|
+
expect(result.hasErrors).toBe(false);
|
|
576
574
|
});
|
|
577
|
-
|
|
575
|
+
it("should allow spread with arrow function parameter", () => {
|
|
578
576
|
const schema = {
|
|
579
577
|
data: {
|
|
580
578
|
items: {
|
|
@@ -606,11 +604,11 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
606
604
|
},
|
|
607
605
|
},
|
|
608
606
|
};
|
|
609
|
-
const ast =
|
|
610
|
-
const result =
|
|
611
|
-
|
|
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);
|
|
612
610
|
});
|
|
613
|
-
|
|
611
|
+
it("should allow spread with the parameter itself", () => {
|
|
614
612
|
const schema = {
|
|
615
613
|
data: {
|
|
616
614
|
items: {
|
|
@@ -636,11 +634,11 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
636
634
|
},
|
|
637
635
|
},
|
|
638
636
|
};
|
|
639
|
-
const ast =
|
|
640
|
-
const result =
|
|
641
|
-
|
|
637
|
+
const ast = parse("<div>{data.items.map(item => <Item {...item} />)}</div>");
|
|
638
|
+
const result = analyze(ast, schema);
|
|
639
|
+
expect(result.hasErrors).toBe(false);
|
|
642
640
|
});
|
|
643
|
-
|
|
641
|
+
it("should allow spread combined with regular attributes", () => {
|
|
644
642
|
const schema = {
|
|
645
643
|
data: {
|
|
646
644
|
props: {
|
|
@@ -659,11 +657,11 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
659
657
|
},
|
|
660
658
|
},
|
|
661
659
|
};
|
|
662
|
-
const ast =
|
|
663
|
-
const result =
|
|
664
|
-
|
|
660
|
+
const ast = parse("<Card {...data.props} highlighted={true} />");
|
|
661
|
+
const result = analyze(ast, schema);
|
|
662
|
+
expect(result.hasErrors).toBe(false);
|
|
665
663
|
});
|
|
666
|
-
|
|
664
|
+
it("should allow multiple spreads", () => {
|
|
667
665
|
const schema = {
|
|
668
666
|
data: {
|
|
669
667
|
baseProps: {
|
|
@@ -688,77 +686,77 @@ const builtins_js_1 = require("../builtins.js");
|
|
|
688
686
|
},
|
|
689
687
|
},
|
|
690
688
|
};
|
|
691
|
-
const ast =
|
|
692
|
-
const result =
|
|
693
|
-
|
|
689
|
+
const ast = parse("<Card {...data.baseProps} {...data.extraProps} />");
|
|
690
|
+
const result = analyze(ast, schema);
|
|
691
|
+
expect(result.hasErrors).toBe(false);
|
|
694
692
|
});
|
|
695
693
|
});
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const ast =
|
|
699
|
-
const result =
|
|
700
|
-
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
const ast =
|
|
704
|
-
const result =
|
|
705
|
-
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
const ast =
|
|
709
|
-
const result =
|
|
710
|
-
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
const ast =
|
|
714
|
-
const result =
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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);
|
|
718
716
|
});
|
|
719
717
|
});
|
|
720
718
|
});
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const ast =
|
|
725
|
-
const result =
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
const ast =
|
|
731
|
-
const result =
|
|
732
|
-
|
|
733
|
-
|
|
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');
|
|
734
732
|
});
|
|
735
733
|
});
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const ast =
|
|
739
|
-
const result =
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
const ast =
|
|
745
|
-
const result =
|
|
746
|
-
|
|
747
|
-
|
|
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');
|
|
748
746
|
});
|
|
749
747
|
});
|
|
750
748
|
});
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const ast =
|
|
749
|
+
describe("integration scenarios", () => {
|
|
750
|
+
it("should catch multiple errors in complex expressions", () => {
|
|
751
|
+
const ast = parse(`
|
|
754
752
|
<InvalidElement>
|
|
755
753
|
<Text>{data.user.invalid}</Text>
|
|
756
754
|
<span>{window.location}</span>
|
|
757
755
|
</InvalidElement>
|
|
758
756
|
`);
|
|
759
|
-
const result =
|
|
760
|
-
|
|
761
|
-
|
|
757
|
+
const result = analyze(ast, baseSchema);
|
|
758
|
+
expect(result.hasErrors).toBe(true);
|
|
759
|
+
expect(result.errors.length).toBeGreaterThan(1);
|
|
762
760
|
});
|
|
763
761
|
});
|
|
764
762
|
});
|