jsxpression 0.1.28 → 0.1.29
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/README.md +155 -188
- package/lib/analyze/analysis-report.d.ts +2 -2
- package/lib/analyze/analysis-report.d.ts.map +1 -1
- package/lib/analyze/analysis-report.js +4 -0
- package/lib/analyze/analysis-report.js.map +1 -1
- package/lib/analyze/data/data-access.d.ts.map +1 -1
- package/lib/analyze/data/data-access.js +45 -82
- package/lib/analyze/data/data-access.js.map +1 -1
- package/lib/analyze/data/method-calls.d.ts.map +1 -1
- package/lib/analyze/data/method-calls.js +4 -3
- package/lib/analyze/data/method-calls.js.map +1 -1
- package/lib/analyze/data/utils.d.ts +2 -3
- package/lib/analyze/data/utils.d.ts.map +1 -1
- package/lib/analyze/data/utils.js +10 -36
- package/lib/analyze/data/utils.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 +13 -3
- 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 +16 -5
- 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 -2
- package/lib/analyze/jsx/element-tags.js.map +1 -1
- package/lib/analyze/security/analyze.d.ts.map +1 -1
- package/lib/analyze/security/analyze.js +2 -4
- package/lib/analyze/security/analyze.js.map +1 -1
- package/lib/analyze/security/call-expressions.d.ts +2 -2
- package/lib/analyze/security/call-expressions.d.ts.map +1 -1
- package/lib/analyze/security/call-expressions.js +21 -5
- 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 +22 -2
- package/lib/analyze/security/declarations.js.map +1 -1
- package/lib/analyze/security/expressions.d.ts.map +1 -1
- package/lib/analyze/security/expressions.js +10 -2
- package/lib/analyze/security/expressions.js.map +1 -1
- package/lib/analyze/security/identifiers.d.ts +2 -2
- package/lib/analyze/security/identifiers.d.ts.map +1 -1
- package/lib/analyze/security/identifiers.js +159 -19
- package/lib/analyze/security/identifiers.js.map +1 -1
- package/lib/builtins.d.ts.map +1 -1
- package/lib/builtins.js +5 -0
- package/lib/builtins.js.map +1 -1
- package/lib/compile/compile.d.ts.map +1 -1
- package/lib/compile/compile.js +131 -61
- package/lib/compile/compile.js.map +1 -1
- package/lib/evaluate/evaluate.d.ts +3 -3
- package/lib/evaluate/evaluate.d.ts.map +1 -1
- package/lib/evaluate/evaluate.js +28 -7
- package/lib/evaluate/evaluate.js.map +1 -1
- package/lib/jsx.d.ts +2 -19
- package/lib/jsx.d.ts.map +1 -1
- package/lib/jsx.js +0 -6
- package/lib/jsx.js.map +1 -1
- package/lib/parse/parse.d.ts.map +1 -1
- package/lib/parse/parse.js +4 -3
- package/lib/parse/parse.js.map +1 -1
- package/lib/render.d.ts +4 -4
- package/lib/render.d.ts.map +1 -1
- package/lib/render.js +7 -9
- package/lib/render.js.map +1 -1
- package/lib/schema.d.ts +2 -7
- package/lib/schema.d.ts.map +1 -1
- package/lib/schema.js +0 -43
- package/lib/schema.js.map +1 -1
- package/lib/traverse.d.ts +4 -4
- package/lib/traverse.d.ts.map +1 -1
- package/lib/traverse.js +118 -40
- package/lib/traverse.js.map +1 -1
- package/lib/validate.d.ts +1 -1
- package/lib/validate.js +1 -1
- package/package.json +4 -3
- package/src/analyze/analysis-report.ts +6 -2
- package/src/analyze/analyze.test.ts +108 -69
- package/src/analyze/data/data-access.ts +45 -90
- package/src/analyze/data/method-calls.ts +4 -3
- package/src/analyze/data/utils.ts +11 -40
- package/src/analyze/jsx/element-attributes.ts +24 -9
- package/src/analyze/jsx/element-children.ts +20 -6
- package/src/analyze/jsx/element-tags.ts +15 -3
- package/src/analyze/security/analyze.ts +1 -3
- package/src/analyze/security/call-expressions.ts +37 -17
- package/src/analyze/security/declarations.ts +35 -9
- package/src/analyze/security/expressions.ts +17 -3
- package/src/analyze/security/identifiers.ts +172 -28
- package/src/builtins.ts +6 -0
- package/src/codegen/generate-typescript-definitions.test.ts +16 -23
- package/src/compile/compile.test.ts +153 -30
- package/src/compile/compile.ts +163 -73
- package/src/evaluate/evaluate.test.ts +276 -14
- package/src/evaluate/evaluate.ts +34 -10
- package/src/jsx.ts +2 -31
- package/src/parse/parse.test.ts +4 -4
- package/src/parse/parse.ts +4 -3
- package/src/render.test.ts +476 -27
- package/src/render.ts +7 -14
- package/src/schema.ts +1 -55
- package/src/traverse.ts +135 -49
- package/src/validate.ts +1 -1
- package/LICENSE +0 -21
- package/lib/analyze/analyze.test.d.ts +0 -2
- package/lib/analyze/analyze.test.d.ts.map +0 -1
- package/lib/analyze/analyze.test.js +0 -763
- package/lib/analyze/analyze.test.js.map +0 -1
- package/lib/analyze/security/member-access.d.ts +0 -6
- package/lib/codegen/generate-typescript-definitions.test.d.ts +0 -2
- package/lib/codegen/generate-typescript-definitions.test.d.ts.map +0 -1
- package/lib/codegen/generate-typescript-definitions.test.js +0 -649
- package/lib/codegen/generate-typescript-definitions.test.js.map +0 -1
- package/lib/compile/compile.test.d.ts +0 -2
- package/lib/compile/compile.test.d.ts.map +0 -1
- package/lib/compile/compile.test.js +0 -146
- package/lib/compile/compile.test.js.map +0 -1
- package/lib/evaluate/evaluate.test.d.ts +0 -2
- package/lib/evaluate/evaluate.test.d.ts.map +0 -1
- package/lib/evaluate/evaluate.test.js +0 -260
- package/lib/evaluate/evaluate.test.js.map +0 -1
- package/lib/parse/parse.test.d.ts +0 -2
- package/lib/parse/parse.test.d.ts.map +0 -1
- package/lib/parse/parse.test.js +0 -80
- package/lib/parse/parse.test.js.map +0 -1
- package/lib/render.test.d.ts +0 -2
- package/lib/render.test.d.ts.map +0 -1
- package/lib/render.test.js +0 -301
- package/lib/render.test.js.map +0 -1
- package/lib/traverse.test.d.ts +0 -2
- package/lib/traverse.test.d.ts.map +0 -1
- package/lib/traverse.test.js +0 -84
- package/lib/traverse.test.js.map +0 -1
- package/lib/types.d.ts +0 -12
- package/src/analyze/security/member-access.ts +0 -35
- package/src/types.ts +0 -17
|
@@ -3,12 +3,13 @@ import { traverse } from "../../traverse.js";
|
|
|
3
3
|
import type { ArrayPropertySchema, PropertySchema, Schema } from "../../schema.js";
|
|
4
4
|
import { ValidationContext } from "../validation-context.js";
|
|
5
5
|
import { AnalysisReport } from "../analysis-report.js";
|
|
6
|
-
import { isSimpleDataAccess, extractPath,
|
|
6
|
+
import { isSimpleDataAccess, extractPath, validateSchemaPath } from "./utils.js";
|
|
7
7
|
import { getNodeRange } from "../utils.js";
|
|
8
8
|
|
|
9
9
|
export function analyzeDataAccess(ast: Program, schema: Schema, validationContext: ValidationContext): AnalysisReport {
|
|
10
10
|
const analysisReport = new AnalysisReport();
|
|
11
11
|
|
|
12
|
+
const dataKeys = new Set(Object.keys(schema.data ?? {}));
|
|
12
13
|
const dataPaths = new Set<string>();
|
|
13
14
|
const arrowFunctionContexts = new Map<any, Map<string, any>>();
|
|
14
15
|
|
|
@@ -71,9 +72,9 @@ export function analyzeDataAccess(ast: Program, schema: Schema, validationContex
|
|
|
71
72
|
|
|
72
73
|
traverse(ast, {
|
|
73
74
|
MemberExpression(node) {
|
|
74
|
-
if (isSimpleDataAccess(node)) {
|
|
75
|
+
if (isSimpleDataAccess(node, dataKeys)) {
|
|
75
76
|
const path = extractPath(node);
|
|
76
|
-
if (path[0]
|
|
77
|
+
if (dataKeys.has(path[0])) {
|
|
77
78
|
dataPaths.add(path.join("."));
|
|
78
79
|
}
|
|
79
80
|
}
|
|
@@ -83,6 +84,8 @@ export function analyzeDataAccess(ast: Program, schema: Schema, validationContex
|
|
|
83
84
|
// Collect all arrow function contexts including nested ones
|
|
84
85
|
collectArrowFunctionContexts(ast);
|
|
85
86
|
|
|
87
|
+
const parentMap = buildParentMap(ast);
|
|
88
|
+
|
|
86
89
|
const validatablePaths = new Set<string>();
|
|
87
90
|
|
|
88
91
|
for (const path of dataPaths) {
|
|
@@ -101,13 +104,13 @@ export function analyzeDataAccess(ast: Program, schema: Schema, validationContex
|
|
|
101
104
|
// Second pass: validate with context awareness
|
|
102
105
|
traverse(ast, {
|
|
103
106
|
MemberExpression(node) {
|
|
104
|
-
const parent =
|
|
107
|
+
const parent = parentMap.get(node);
|
|
105
108
|
if (parent && parent.type === "CallExpression" && parent.callee === node) {
|
|
106
109
|
return;
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
// Check if this is inside an arrow function with parameter context
|
|
110
|
-
const arrowFunctionContext =
|
|
113
|
+
const arrowFunctionContext = findEnclosingArrowContext(node, arrowFunctionContexts);
|
|
111
114
|
|
|
112
115
|
if (arrowFunctionContext && node.object.type === "Identifier") {
|
|
113
116
|
const paramType = arrowFunctionContext.get(node.object.name);
|
|
@@ -117,15 +120,14 @@ export function analyzeDataAccess(ast: Program, schema: Schema, validationContex
|
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
if (isSimpleDataAccess(node)) {
|
|
123
|
+
if (isSimpleDataAccess(node, dataKeys)) {
|
|
121
124
|
const path = extractPath(node);
|
|
122
125
|
|
|
123
|
-
if (path[0]
|
|
126
|
+
if (dataKeys.has(path[0])) {
|
|
124
127
|
const pathString = path.join(".");
|
|
125
128
|
|
|
126
129
|
if (validatablePaths.has(pathString)) {
|
|
127
|
-
|
|
128
|
-
validateSchemaPath(schemaPath, schema, analysisReport, node, validationContext);
|
|
130
|
+
validateSchemaPath(path, schema, analysisReport, node, validationContext);
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
}
|
|
@@ -135,48 +137,20 @@ export function analyzeDataAccess(ast: Program, schema: Schema, validationContex
|
|
|
135
137
|
return analysisReport;
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
function
|
|
140
|
+
function findEnclosingArrowContext(
|
|
139
141
|
targetNode: any,
|
|
140
|
-
ast: Program,
|
|
141
142
|
arrowFunctionContexts: Map<any, Map<string, any>>
|
|
142
143
|
): Map<string, any> | null {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const result = findInNode(node.body, depth + 1);
|
|
150
|
-
if (result?.found) {
|
|
151
|
-
return { arrowFunction: node, depth: result.depth };
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
for (const key of Object.keys(node)) {
|
|
156
|
-
const value = node[key];
|
|
157
|
-
if (value && typeof value === "object") {
|
|
158
|
-
if (Array.isArray(value)) {
|
|
159
|
-
for (const child of value) {
|
|
160
|
-
if (child && typeof child === "object" && child.type) {
|
|
161
|
-
const result = findInNode(child, depth);
|
|
162
|
-
if (result?.found || result?.arrowFunction) {
|
|
163
|
-
return result;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
} else if (value.type) {
|
|
168
|
-
const result = findInNode(value, depth);
|
|
169
|
-
if (result?.found || result?.arrowFunction) {
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
144
|
+
let best: { arrowFn: any; size: number } | null = null;
|
|
145
|
+
for (const arrowFn of arrowFunctionContexts.keys()) {
|
|
146
|
+
if (targetNode.start >= arrowFn.start && targetNode.end <= arrowFn.end) {
|
|
147
|
+
const size = arrowFn.end - arrowFn.start;
|
|
148
|
+
if (!best || size < best.size) {
|
|
149
|
+
best = { arrowFn, size };
|
|
173
150
|
}
|
|
174
151
|
}
|
|
175
|
-
return null;
|
|
176
152
|
}
|
|
177
|
-
|
|
178
|
-
const result = findInNode(ast);
|
|
179
|
-
return result?.arrowFunction ? arrowFunctionContexts.get(result.arrowFunction) || null : null;
|
|
153
|
+
return best ? arrowFunctionContexts.get(best.arrowFn) || null : null;
|
|
180
154
|
}
|
|
181
155
|
|
|
182
156
|
function getArrayElementTypeFromCall(
|
|
@@ -185,15 +159,16 @@ function getArrayElementTypeFromCall(
|
|
|
185
159
|
arrowFunctionContexts: Map<any, Map<string, any>>
|
|
186
160
|
): any {
|
|
187
161
|
const { callee } = node;
|
|
162
|
+
const dataKeys = new Set(Object.keys(schema.data ?? {}));
|
|
188
163
|
|
|
189
164
|
if (callee.type !== "MemberExpression") {
|
|
190
165
|
return null;
|
|
191
166
|
}
|
|
192
167
|
|
|
193
|
-
if (isSimpleDataAccess(callee)) {
|
|
168
|
+
if (isSimpleDataAccess(callee, dataKeys)) {
|
|
194
169
|
const path = extractPath(callee);
|
|
195
|
-
if (path[0]
|
|
196
|
-
const schemaPath = path.slice(
|
|
170
|
+
if (dataKeys.has(path[0])) {
|
|
171
|
+
const schemaPath = path.slice(0, -1); // Remove method name
|
|
197
172
|
const arraySchema = getSchemaAtPath(schemaPath, schema);
|
|
198
173
|
|
|
199
174
|
if (arraySchema?.type === "array") {
|
|
@@ -219,14 +194,6 @@ function getArrayElementTypeFromCall(
|
|
|
219
194
|
return getArrayElementType(propertySchema);
|
|
220
195
|
}
|
|
221
196
|
}
|
|
222
|
-
|
|
223
|
-
// Fallback: try to find any matching property in schema
|
|
224
|
-
if (propertyName === "items" || propertyName === "employees") {
|
|
225
|
-
const itemsArraySchema = findArrayPropertyInSchema(schema, propertyName);
|
|
226
|
-
if (itemsArraySchema) {
|
|
227
|
-
return getArrayElementType(itemsArraySchema);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
197
|
}
|
|
231
198
|
}
|
|
232
199
|
|
|
@@ -258,40 +225,6 @@ function isNodeInsideArrowFunction(targetNode: any, arrowFunction: any): boolean
|
|
|
258
225
|
return false;
|
|
259
226
|
}
|
|
260
227
|
|
|
261
|
-
function findArrayPropertyInSchema(schema: Schema, propertyName: string): any {
|
|
262
|
-
function searchInObject(obj: any): any {
|
|
263
|
-
if (!obj || typeof obj !== "object") return null;
|
|
264
|
-
|
|
265
|
-
if (obj.type === "object" && obj.shape) {
|
|
266
|
-
// Check if this object has the property we're looking for
|
|
267
|
-
if (obj.shape[propertyName] && obj.shape[propertyName].type === "array") {
|
|
268
|
-
return obj.shape[propertyName];
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Recursively search in nested objects
|
|
272
|
-
for (const key of Object.keys(obj.shape)) {
|
|
273
|
-
const result = searchInObject(obj.shape[key]);
|
|
274
|
-
if (result) return result;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (obj.type === "array" && obj.shape) {
|
|
279
|
-
return searchInObject(obj.shape);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (!schema.data) return null;
|
|
286
|
-
|
|
287
|
-
for (const key of Object.keys(schema.data)) {
|
|
288
|
-
const result = searchInObject(schema.data[key]);
|
|
289
|
-
if (result) return result;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
228
|
function getSchemaAtPath(path: string[], schema: Schema): any {
|
|
296
229
|
if (!schema.data || path.length === 0) {
|
|
297
230
|
return schema.data;
|
|
@@ -347,3 +280,25 @@ function validateParameterAccess(
|
|
|
347
280
|
export function getArrayElementType(schema: ArrayPropertySchema): PropertySchema | null {
|
|
348
281
|
return schema.shape || null;
|
|
349
282
|
}
|
|
283
|
+
|
|
284
|
+
function buildParentMap(ast: Program): Map<any, any> {
|
|
285
|
+
const map = new Map<any, any>();
|
|
286
|
+
function visit(node: any, parent: any): void {
|
|
287
|
+
if (!node || typeof node !== "object" || !node.type) return;
|
|
288
|
+
map.set(node, parent);
|
|
289
|
+
for (const key of Object.keys(node)) {
|
|
290
|
+
const value = node[key];
|
|
291
|
+
if (value && typeof value === "object") {
|
|
292
|
+
if (Array.isArray(value)) {
|
|
293
|
+
for (const child of value) {
|
|
294
|
+
visit(child, node);
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
visit(value, node);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
visit(ast, null);
|
|
303
|
+
return map;
|
|
304
|
+
}
|
|
@@ -9,6 +9,7 @@ import { getNodeRange } from "../utils.js";
|
|
|
9
9
|
|
|
10
10
|
export function analyzeMethodCalls(ast: Program, schema: Schema, validationContext: ValidationContext): AnalysisReport {
|
|
11
11
|
const analysisReport = new AnalysisReport();
|
|
12
|
+
const dataKeys = new Set(Object.keys(schema.data ?? {}));
|
|
12
13
|
|
|
13
14
|
traverse(ast, {
|
|
14
15
|
CallExpression(node) {
|
|
@@ -17,10 +18,10 @@ export function analyzeMethodCalls(ast: Program, schema: Schema, validationConte
|
|
|
17
18
|
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier") {
|
|
18
19
|
const method = callee.property.name;
|
|
19
20
|
|
|
20
|
-
if (isSimpleDataAccess(callee)) {
|
|
21
|
+
if (isSimpleDataAccess(callee, dataKeys)) {
|
|
21
22
|
const path = extractPath(callee);
|
|
22
|
-
if (path[0]
|
|
23
|
-
const schemaPath = path.slice(
|
|
23
|
+
if (dataKeys.has(path[0])) {
|
|
24
|
+
const schemaPath = path.slice(0, -1);
|
|
24
25
|
if (schemaPath.length > 0) {
|
|
25
26
|
validateSchemaPath(schemaPath, schema, analysisReport, callee, validationContext);
|
|
26
27
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MemberExpression
|
|
1
|
+
import type { MemberExpression } from "acorn";
|
|
2
2
|
import {
|
|
3
3
|
isAllowedOnArray,
|
|
4
4
|
isAllowedOnString,
|
|
@@ -10,7 +10,7 @@ import { AnalysisReport } from "../analysis-report.js";
|
|
|
10
10
|
import { getNodeRange } from "../utils.js";
|
|
11
11
|
import { ValidationContext } from "../validation-context.js";
|
|
12
12
|
|
|
13
|
-
export function isSimpleDataAccess(node: MemberExpression): boolean {
|
|
13
|
+
export function isSimpleDataAccess(node: MemberExpression, dataKeys: ReadonlySet<string>): boolean {
|
|
14
14
|
if (node.computed) {
|
|
15
15
|
return node.property.type === "Literal" && typeof node.property.value === "number";
|
|
16
16
|
}
|
|
@@ -22,7 +22,7 @@ export function isSimpleDataAccess(node: MemberExpression): boolean {
|
|
|
22
22
|
|
|
23
23
|
while (current) {
|
|
24
24
|
if (current.type === "Identifier") {
|
|
25
|
-
return current.name
|
|
25
|
+
return dataKeys.has(current.name);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
if (current.type === "MemberExpression") {
|
|
@@ -70,35 +70,6 @@ export function extractPath(node: MemberExpression): string[] {
|
|
|
70
70
|
return path;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export function getParentNode(targetNode: any, ast: Program): any | null {
|
|
74
|
-
let parent: any = null;
|
|
75
|
-
|
|
76
|
-
function traverseForParent(node: any, currentParent: any = null): void {
|
|
77
|
-
if (node === targetNode) {
|
|
78
|
-
parent = currentParent;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
for (const key of Object.keys(node)) {
|
|
83
|
-
const value = node[key];
|
|
84
|
-
if (value && typeof value === "object") {
|
|
85
|
-
if (Array.isArray(value)) {
|
|
86
|
-
value.forEach((child) => {
|
|
87
|
-
if (child && typeof child === "object" && child.type) {
|
|
88
|
-
traverseForParent(child, node);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
} else if (value.type) {
|
|
92
|
-
traverseForParent(value, node);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
traverseForParent(ast);
|
|
99
|
-
return parent;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
73
|
export function getAvailablePropsAtPath(path: string[], depth: number, schemaData: any): string[] {
|
|
103
74
|
if (!schemaData || depth < 0) {
|
|
104
75
|
return schemaData ? Object.keys(schemaData) : [];
|
|
@@ -148,7 +119,7 @@ export function validateSchemaPath(
|
|
|
148
119
|
if (!current) {
|
|
149
120
|
analysisReport.addIssue(
|
|
150
121
|
"INVALID_DATA_ACCESS",
|
|
151
|
-
`Property '
|
|
122
|
+
`Property '${path[0]}' does not exist in schema`,
|
|
152
123
|
getNodeRange(node),
|
|
153
124
|
validationContext.getSnapshot(),
|
|
154
125
|
Object.keys(schemaData)
|
|
@@ -160,7 +131,7 @@ export function validateSchemaPath(
|
|
|
160
131
|
|
|
161
132
|
for (let i = 1; i < path.length; i++) {
|
|
162
133
|
const prop = path[i];
|
|
163
|
-
const isElementAccess = elementAccessFlags[i];
|
|
134
|
+
const isElementAccess = elementAccessFlags[i - 1];
|
|
164
135
|
|
|
165
136
|
if (current.type === "array") {
|
|
166
137
|
if (!isElementAccess) {
|
|
@@ -168,7 +139,7 @@ export function validateSchemaPath(
|
|
|
168
139
|
return;
|
|
169
140
|
}
|
|
170
141
|
|
|
171
|
-
const accessedPath = [
|
|
142
|
+
const accessedPath = [...path.slice(0, i), prop].join(".");
|
|
172
143
|
analysisReport.addIssue(
|
|
173
144
|
"INVALID_DATA_ACCESS",
|
|
174
145
|
`Property '${accessedPath}' does not exist on array type`,
|
|
@@ -201,7 +172,7 @@ export function validateSchemaPath(
|
|
|
201
172
|
|
|
202
173
|
analysisReport.addIssue(
|
|
203
174
|
"INVALID_DATA_ACCESS",
|
|
204
|
-
`Property '
|
|
175
|
+
`Property '${paths.join(".")}' does not exist in schema`,
|
|
205
176
|
getNodeRange(node),
|
|
206
177
|
validationContext.getSnapshot(),
|
|
207
178
|
suggestions
|
|
@@ -218,7 +189,7 @@ export function validateSchemaPath(
|
|
|
218
189
|
return;
|
|
219
190
|
}
|
|
220
191
|
|
|
221
|
-
const accessedPath = [
|
|
192
|
+
const accessedPath = [...path.slice(0, i), prop].join(".");
|
|
222
193
|
analysisReport.addIssue(
|
|
223
194
|
"INVALID_DATA_ACCESS",
|
|
224
195
|
`Property '${accessedPath}' does not exist on array type`,
|
|
@@ -234,7 +205,7 @@ export function validateSchemaPath(
|
|
|
234
205
|
return;
|
|
235
206
|
}
|
|
236
207
|
|
|
237
|
-
const accessedPath = [
|
|
208
|
+
const accessedPath = [...path.slice(0, i), prop].join(".");
|
|
238
209
|
analysisReport.addIssue(
|
|
239
210
|
"INVALID_DATA_ACCESS",
|
|
240
211
|
`Property '${accessedPath}' does not exist on string type`,
|
|
@@ -259,7 +230,7 @@ export function validateSchemaPath(
|
|
|
259
230
|
return;
|
|
260
231
|
}
|
|
261
232
|
|
|
262
|
-
const accessedPath = [
|
|
233
|
+
const accessedPath = [...path.slice(0, i), prop].join(".");
|
|
263
234
|
analysisReport.addIssue(
|
|
264
235
|
"INVALID_DATA_ACCESS",
|
|
265
236
|
`Property '${accessedPath}' does not exist on string type`,
|
|
@@ -288,7 +259,7 @@ export function validateSchemaPath(
|
|
|
288
259
|
|
|
289
260
|
analysisReport.addIssue(
|
|
290
261
|
"INVALID_DATA_ACCESS",
|
|
291
|
-
`Property '
|
|
262
|
+
`Property '${paths.join(".")}' does not exist in schema`,
|
|
292
263
|
getNodeRange(node),
|
|
293
264
|
validationContext.getSnapshot(),
|
|
294
265
|
availableProps
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import type { Program } from "acorn";
|
|
2
|
-
import {
|
|
2
|
+
import { isJsxAttribute, isJsxExpressionContainer, type JSXAttribute } from "../../jsx.js";
|
|
3
3
|
import {
|
|
4
|
-
type Schema,
|
|
5
|
-
isAttributeAllowed,
|
|
6
4
|
getAllowedAttributes,
|
|
7
|
-
getRequiredAttributes,
|
|
8
5
|
getAttributeSchema,
|
|
9
6
|
getEnumValues,
|
|
7
|
+
getRequiredAttributes,
|
|
8
|
+
isAttributeAllowed,
|
|
9
|
+
type Schema,
|
|
10
10
|
} from "../../schema.js";
|
|
11
|
-
import {
|
|
12
|
-
import { ValidationContext } from "../validation-context.js";
|
|
11
|
+
import { traverse } from "../../traverse.js";
|
|
13
12
|
import { AnalysisReport } from "../analysis-report.js";
|
|
14
|
-
|
|
15
|
-
import {
|
|
13
|
+
import { getAttributeSimilarityMatchers, getBestSimilarityMatcherSuggestion, getNodeRange } from "../utils.js";
|
|
14
|
+
import type { ValidationContext } from "../validation-context.js";
|
|
16
15
|
|
|
17
16
|
export function analyzeElementAttributes(
|
|
18
17
|
ast: Program,
|
|
@@ -21,11 +20,24 @@ export function analyzeElementAttributes(
|
|
|
21
20
|
): AnalysisReport {
|
|
22
21
|
const analysisReport = new AnalysisReport();
|
|
23
22
|
|
|
23
|
+
const localFunctionNames = new Set<string>();
|
|
24
|
+
for (const stmt of ast.body) {
|
|
25
|
+
if (stmt.type === "FunctionDeclaration" && stmt.id) {
|
|
26
|
+
localFunctionNames.add(stmt.id.name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
traverse(ast, {
|
|
25
31
|
JSXElement(node) {
|
|
26
32
|
const tagName = node.openingElement.name.name;
|
|
27
33
|
|
|
28
34
|
validationContext.enterElement(tagName);
|
|
35
|
+
|
|
36
|
+
if (localFunctionNames.has(tagName)) {
|
|
37
|
+
validationContext.exitElement();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
const currentContext = validationContext.getSnapshot();
|
|
30
42
|
|
|
31
43
|
const allowedAttributes = getAllowedAttributes(schema, tagName);
|
|
@@ -132,7 +144,10 @@ function getAttributeLiteralValue(attribute: JSXAttribute): string | number | bo
|
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
if (isJsxExpressionContainer(attribute.value)) {
|
|
135
|
-
const expression = attribute.value.expression as {
|
|
147
|
+
const expression = attribute.value.expression as {
|
|
148
|
+
type?: string;
|
|
149
|
+
value?: unknown;
|
|
150
|
+
};
|
|
136
151
|
|
|
137
152
|
if (expression?.type === "Literal") {
|
|
138
153
|
return expression.value as string | number | boolean | null | undefined;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Program } from "acorn";
|
|
2
|
+
import { isJsxElement, isJsxFragment, isJsxLeafNode, isJsxText } from "../../jsx.js";
|
|
3
|
+
import { getAllowedChildren, isSelfClosing, type Schema } from "../../schema.js";
|
|
2
4
|
import { traverse } from "../../traverse.js";
|
|
3
|
-
import { type Schema, getAllowedChildren, isSelfClosing } from "../../schema.js";
|
|
4
|
-
import { isJsxElement, isJsxFragment, isJsxText, isJsxLeafNode } from "../../jsx.js";
|
|
5
|
-
import { ValidationContext } from "../validation-context.js";
|
|
6
5
|
import { AnalysisReport } from "../analysis-report.js";
|
|
7
|
-
import {
|
|
6
|
+
import { getBestSimilarityMatcherSuggestion, getElementSimilarityMatchers, getNodeRange } from "../utils.js";
|
|
7
|
+
import type { ValidationContext } from "../validation-context.js";
|
|
8
8
|
|
|
9
9
|
export function analyzeElementChildren(
|
|
10
10
|
ast: Program,
|
|
@@ -13,12 +13,24 @@ export function analyzeElementChildren(
|
|
|
13
13
|
): AnalysisReport {
|
|
14
14
|
const analysisReport = new AnalysisReport();
|
|
15
15
|
|
|
16
|
+
const localFunctionNames = new Set<string>();
|
|
17
|
+
for (const stmt of ast.body) {
|
|
18
|
+
if (stmt.type === "FunctionDeclaration" && stmt.id) {
|
|
19
|
+
localFunctionNames.add(stmt.id.name);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
traverse(ast, {
|
|
17
24
|
JSXElement(node) {
|
|
18
25
|
const parentTag = node.openingElement.name.name;
|
|
19
26
|
|
|
20
27
|
validationContext.enterElement(parentTag);
|
|
21
28
|
|
|
29
|
+
if (localFunctionNames.has(parentTag)) {
|
|
30
|
+
validationContext.exitElement();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
const currentContext = validationContext.getSnapshot();
|
|
23
35
|
|
|
24
36
|
const allowedChildren = getAllowedChildren(schema, parentTag);
|
|
@@ -57,6 +69,10 @@ export function analyzeElementChildren(
|
|
|
57
69
|
if (isJsxElement(child)) {
|
|
58
70
|
const childTag = child.openingElement.name.name;
|
|
59
71
|
|
|
72
|
+
if (localFunctionNames.has(childTag)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
if (!allowedChildren.includes(childTag)) {
|
|
61
77
|
const elementSimilarityMatchers = getElementSimilarityMatchers(childTag, allowedChildren);
|
|
62
78
|
const bestSimilarityMatcherSuggestion = getBestSimilarityMatcherSuggestion(childTag, allowedChildren);
|
|
@@ -83,8 +99,6 @@ export function analyzeElementChildren(
|
|
|
83
99
|
} else if (isJsxText(child)) {
|
|
84
100
|
const hasContent = child.value.trim().length > 0;
|
|
85
101
|
if (!hasContent) {
|
|
86
|
-
// Allow whitespace-only text nodes (very common in JSX formatting)
|
|
87
|
-
continue;
|
|
88
102
|
}
|
|
89
103
|
}
|
|
90
104
|
}
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import type { Program } from "acorn";
|
|
2
|
+
import { getAllElements, isElementAllowed, type Schema } from "../../schema.js";
|
|
2
3
|
import { traverse } from "../../traverse.js";
|
|
3
|
-
import { type Schema, isElementAllowed, getAllElements } from "../../schema.js";
|
|
4
|
-
import { ValidationContext } from "../validation-context.js";
|
|
5
4
|
import { AnalysisReport } from "../analysis-report.js";
|
|
6
|
-
import {
|
|
5
|
+
import { getBestSimilarityMatcherSuggestion, getElementSimilarityMatchers, getNodeRange } from "../utils.js";
|
|
6
|
+
import type { ValidationContext } from "../validation-context.js";
|
|
7
7
|
|
|
8
8
|
export function analyzeElementTags(ast: Program, schema: Schema, validationContext: ValidationContext): AnalysisReport {
|
|
9
9
|
const analysisReport = new AnalysisReport();
|
|
10
10
|
|
|
11
|
+
const localFunctionNames = new Set<string>();
|
|
12
|
+
for (const stmt of ast.body) {
|
|
13
|
+
if (stmt.type === "FunctionDeclaration" && stmt.id) {
|
|
14
|
+
localFunctionNames.add(stmt.id.name);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
traverse(ast, {
|
|
12
19
|
JSXElement(node) {
|
|
13
20
|
const tagName = node.openingElement.name.name;
|
|
14
21
|
|
|
15
22
|
validationContext.enterElement(tagName);
|
|
16
23
|
|
|
24
|
+
if (localFunctionNames.has(tagName)) {
|
|
25
|
+
validationContext.exitElement();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
const snapshot = validationContext.getSnapshot();
|
|
18
30
|
const range = getNodeRange(node);
|
|
19
31
|
|
|
@@ -6,16 +6,14 @@ import { analyzeCallExpressions } from "./call-expressions.js";
|
|
|
6
6
|
import { analyzeDeclarations } from "./declarations.js";
|
|
7
7
|
import { analyzeExpressions } from "./expressions.js";
|
|
8
8
|
import { analyzeIdentifiers } from "./identifiers.js";
|
|
9
|
-
import { analyzeMemberAccess } from "./member-access.js";
|
|
10
9
|
|
|
11
10
|
export function analyzeSecurity(ast: Program, schema: Schema): AnalysisReport {
|
|
12
11
|
const validationContext = new ValidationContext();
|
|
13
12
|
|
|
14
13
|
return new AnalysisReport().merge(
|
|
15
14
|
analyzeDeclarations(ast, schema, validationContext),
|
|
16
|
-
|
|
15
|
+
analyzeIdentifiers(ast, schema, validationContext),
|
|
17
16
|
analyzeExpressions(ast, schema, validationContext),
|
|
18
|
-
analyzeMemberAccess(ast, schema, validationContext),
|
|
19
17
|
analyzeCallExpressions(ast, schema, validationContext)
|
|
20
18
|
);
|
|
21
19
|
}
|
|
@@ -1,39 +1,59 @@
|
|
|
1
1
|
import type { Program } from "acorn";
|
|
2
|
-
import {
|
|
2
|
+
import { getBuiltinGlobals } from "../../builtins.js";
|
|
3
3
|
import type { Schema } from "../../schema.js";
|
|
4
|
-
import {
|
|
4
|
+
import { traverse } from "../../traverse.js";
|
|
5
5
|
import { AnalysisReport } from "../analysis-report.js";
|
|
6
6
|
import { getNodeRange } from "../utils.js";
|
|
7
|
+
import type { ValidationContext } from "../validation-context.js";
|
|
7
8
|
|
|
8
9
|
export function analyzeCallExpressions(
|
|
9
10
|
ast: Program,
|
|
10
|
-
|
|
11
|
+
schema: Schema,
|
|
11
12
|
validationContext: ValidationContext
|
|
12
13
|
): AnalysisReport {
|
|
13
14
|
const analysisReport = new AnalysisReport();
|
|
14
15
|
|
|
16
|
+
const localFunctionNames = new Set<string>();
|
|
17
|
+
for (const stmt of ast.body) {
|
|
18
|
+
if (stmt.type === "FunctionDeclaration" && stmt.id) {
|
|
19
|
+
localFunctionNames.add(stmt.id.name);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const schemaFunctionNames = new Set(Object.keys(schema.functions ?? {}));
|
|
23
|
+
const builtinGlobals = new Set(getBuiltinGlobals());
|
|
24
|
+
|
|
15
25
|
traverse(ast, {
|
|
16
26
|
CallExpression(node) {
|
|
17
27
|
const { callee } = node;
|
|
18
28
|
|
|
19
|
-
if (callee.type
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
if (callee.type === "MemberExpression") {
|
|
30
|
+
if (callee.property.type !== "Identifier" || callee.computed) {
|
|
31
|
+
analysisReport.addIssue(
|
|
32
|
+
"DYNAMIC_METHOD_NOT_ALLOWED",
|
|
33
|
+
"only explicitly allowed methods are permitted",
|
|
34
|
+
getNodeRange(callee),
|
|
35
|
+
validationContext.getSnapshot()
|
|
36
|
+
);
|
|
37
|
+
}
|
|
26
38
|
return;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
if (callee.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
if (callee.type === "Identifier") {
|
|
42
|
+
if (
|
|
43
|
+
localFunctionNames.has(callee.name) ||
|
|
44
|
+
schemaFunctionNames.has(callee.name) ||
|
|
45
|
+
builtinGlobals.has(callee.name)
|
|
46
|
+
) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
36
49
|
}
|
|
50
|
+
|
|
51
|
+
analysisReport.addIssue(
|
|
52
|
+
"DIRECT_CALL_NOT_ALLOWED",
|
|
53
|
+
"only member calls allowed",
|
|
54
|
+
getNodeRange(node),
|
|
55
|
+
validationContext.getSnapshot()
|
|
56
|
+
);
|
|
37
57
|
},
|
|
38
58
|
});
|
|
39
59
|
|