jsxpression 0.1.0

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.
Files changed (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +368 -0
  3. package/lib/analyze/analysis-error.d.ts.map +1 -0
  4. package/lib/analyze/analysis-report.d.ts +33 -0
  5. package/lib/analyze/analysis-report.d.ts.map +1 -0
  6. package/lib/analyze/analysis-report.js +194 -0
  7. package/lib/analyze/analysis-report.js.map +1 -0
  8. package/lib/analyze/analyze.d.ts.map +1 -0
  9. package/lib/analyze/analyze.test.d.ts.map +1 -0
  10. package/lib/analyze/analyze.test.js +765 -0
  11. package/lib/analyze/analyze.test.js.map +1 -0
  12. package/lib/analyze/data/analyze.d.ts.map +1 -0
  13. package/lib/analyze/data/data-access.d.ts.map +1 -0
  14. package/lib/analyze/data/index.d.ts.map +1 -0
  15. package/lib/analyze/data/method-calls.d.ts.map +1 -0
  16. package/lib/analyze/data/utils.d.ts.map +1 -0
  17. package/lib/analyze/index.d.ts.map +1 -0
  18. package/lib/analyze/jsx/analyze.d.ts.map +1 -0
  19. package/lib/analyze/jsx/element-attributes.d.ts.map +1 -0
  20. package/lib/analyze/jsx/element-attributes.js +89 -0
  21. package/lib/analyze/jsx/element-attributes.js.map +1 -0
  22. package/lib/analyze/jsx/element-children.d.ts.map +1 -0
  23. package/lib/analyze/jsx/element-tags.d.ts.map +1 -0
  24. package/lib/analyze/jsx/index.d.ts.map +1 -0
  25. package/lib/analyze/security/analyze.d.ts.map +1 -0
  26. package/lib/analyze/security/call-expressions.d.ts.map +1 -0
  27. package/lib/analyze/security/declarations.d.ts.map +1 -0
  28. package/lib/analyze/security/expressions.d.ts.map +1 -0
  29. package/lib/analyze/security/identifiers.d.ts.map +1 -0
  30. package/lib/analyze/security/index.d.ts.map +1 -0
  31. package/lib/analyze/security/member-access.d.ts.map +1 -0
  32. package/lib/builtins.d.ts +50 -0
  33. package/lib/builtins.d.ts.map +1 -0
  34. package/lib/builtins.js +1065 -0
  35. package/lib/builtins.js.map +1 -0
  36. package/lib/codegen/builtin-types.d.ts +11 -0
  37. package/lib/codegen/builtin-types.d.ts.map +1 -0
  38. package/lib/codegen/builtin-types.js +151 -0
  39. package/lib/codegen/builtin-types.js.map +1 -0
  40. package/lib/codegen/builtins/builtin-types.d.ts +2 -0
  41. package/lib/codegen/builtins/builtin-types.d.ts.map +1 -0
  42. package/lib/codegen/builtins/builtin-types.js +143 -0
  43. package/lib/codegen/builtins/builtin-types.js.map +1 -0
  44. package/lib/codegen/builtins/index.d.ts +2 -0
  45. package/lib/codegen/builtins/index.d.ts.map +1 -0
  46. package/lib/codegen/builtins/index.js +6 -0
  47. package/lib/codegen/builtins/index.js.map +1 -0
  48. package/lib/codegen/declare-types.d.ts +20 -0
  49. package/lib/codegen/declare-types.d.ts.map +1 -0
  50. package/lib/codegen/declare-types.js +299 -0
  51. package/lib/codegen/declare-types.js.map +1 -0
  52. package/lib/codegen/generate-typescript-definitions.d.ts +31 -0
  53. package/lib/codegen/generate-typescript-definitions.d.ts.map +1 -0
  54. package/lib/codegen/generate-typescript-definitions.js +37 -0
  55. package/lib/codegen/generate-typescript-definitions.js.map +1 -0
  56. package/lib/codegen/generate-typescript-definitions.test.d.ts +2 -0
  57. package/lib/codegen/generate-typescript-definitions.test.d.ts.map +1 -0
  58. package/lib/codegen/generate-typescript-definitions.test.js +587 -0
  59. package/lib/codegen/generate-typescript-definitions.test.js.map +1 -0
  60. package/lib/codegen/index.d.ts +2 -0
  61. package/lib/codegen/index.d.ts.map +1 -0
  62. package/lib/codegen/index.js +6 -0
  63. package/lib/codegen/index.js.map +1 -0
  64. package/lib/codegen/schema/component-types.d.ts +3 -0
  65. package/lib/codegen/schema/component-types.d.ts.map +1 -0
  66. package/lib/codegen/schema/component-types.js +118 -0
  67. package/lib/codegen/schema/component-types.js.map +1 -0
  68. package/lib/codegen/schema/data-types.d.ts +3 -0
  69. package/lib/codegen/schema/data-types.d.ts.map +1 -0
  70. package/lib/codegen/schema/data-types.js +98 -0
  71. package/lib/codegen/schema/data-types.js.map +1 -0
  72. package/lib/codegen/schema/index.d.ts +3 -0
  73. package/lib/codegen/schema/index.d.ts.map +1 -0
  74. package/lib/codegen/schema/index.js +8 -0
  75. package/lib/codegen/schema/index.js.map +1 -0
  76. package/lib/codegen/schema/utils.d.ts +4 -0
  77. package/lib/codegen/schema/utils.d.ts.map +1 -0
  78. package/lib/codegen/schema/utils.js +103 -0
  79. package/lib/codegen/schema/utils.js.map +1 -0
  80. package/lib/codegen/schema-to-types.d.ts +14 -0
  81. package/lib/codegen/schema-to-types.d.ts.map +1 -0
  82. package/lib/codegen/schema-to-types.js +203 -0
  83. package/lib/codegen/schema-to-types.js.map +1 -0
  84. package/lib/compile/compile.d.ts.map +1 -0
  85. package/lib/compile/compile.js +245 -0
  86. package/lib/compile/compile.js.map +1 -0
  87. package/lib/compile/compile.test.d.ts.map +1 -0
  88. package/lib/compile/compile.test.js +148 -0
  89. package/lib/compile/compile.test.js.map +1 -0
  90. package/lib/evaluate/evaluate.d.ts +26 -0
  91. package/lib/evaluate/evaluate.d.ts.map +1 -0
  92. package/lib/evaluate/evaluate.js +40 -0
  93. package/lib/evaluate/evaluation-error.d.ts +4 -0
  94. package/lib/evaluate/evaluation-error.d.ts.map +1 -0
  95. package/lib/evaluate/evaluation-error.js +6 -0
  96. package/lib/index.d.ts +8 -0
  97. package/lib/index.d.ts.map +1 -0
  98. package/lib/index.js +16 -0
  99. package/lib/index.js.map +1 -0
  100. package/lib/render.d.ts +58 -0
  101. package/lib/render.d.ts.map +1 -0
  102. package/lib/render.js +52 -0
  103. package/lib/render.test.d.ts.map +1 -0
  104. package/lib/schema.d.ts +68 -0
  105. package/lib/schema.d.ts.map +1 -0
  106. package/lib/schema.js +106 -0
  107. package/lib/validate.d.ts +41 -0
  108. package/lib/validate.d.ts.map +1 -0
  109. package/lib/validate.js +42 -0
  110. package/package.json +27 -0
  111. package/src/analyze/analysis-error.ts +26 -0
  112. package/src/analyze/analysis-report.ts +228 -0
  113. package/src/analyze/analyze.test.ts +829 -0
  114. package/src/analyze/analyze.ts +10 -0
  115. package/src/analyze/data/analyze.ts +15 -0
  116. package/src/analyze/data/data-access.ts +349 -0
  117. package/src/analyze/data/index.ts +1 -0
  118. package/src/analyze/data/method-calls.ts +96 -0
  119. package/src/analyze/data/utils.ts +329 -0
  120. package/src/analyze/index.ts +3 -0
  121. package/src/analyze/jsx/analyze.ts +17 -0
  122. package/src/analyze/jsx/element-attributes.ts +143 -0
  123. package/src/analyze/jsx/element-children.ts +97 -0
  124. package/src/analyze/jsx/element-tags.ts +55 -0
  125. package/src/analyze/jsx/index.ts +1 -0
  126. package/src/analyze/security/analyze.ts +21 -0
  127. package/src/analyze/security/call-expressions.ts +41 -0
  128. package/src/analyze/security/declarations.ts +67 -0
  129. package/src/analyze/security/expressions.ts +100 -0
  130. package/src/analyze/security/identifiers.ts +113 -0
  131. package/src/analyze/security/index.ts +1 -0
  132. package/src/analyze/security/member-access.ts +35 -0
  133. package/src/analyze/utils.ts +118 -0
  134. package/src/analyze/validation-context.ts +32 -0
  135. package/src/builtins.ts +1149 -0
  136. package/src/codegen/builtins/builtin-types.ts +174 -0
  137. package/src/codegen/builtins/index.ts +1 -0
  138. package/src/codegen/generate-typescript-definitions.test.ts +676 -0
  139. package/src/codegen/generate-typescript-definitions.ts +35 -0
  140. package/src/codegen/index.ts +1 -0
  141. package/src/codegen/schema/component-types.ts +140 -0
  142. package/src/codegen/schema/data-types.ts +109 -0
  143. package/src/codegen/schema/index.ts +2 -0
  144. package/src/codegen/schema/utils.ts +111 -0
  145. package/src/compile/compilation-error.ts +37 -0
  146. package/src/compile/compile.test.ts +180 -0
  147. package/src/compile/compile.ts +282 -0
  148. package/src/compile/index.ts +2 -0
  149. package/src/evaluate/evaluate.test.ts +316 -0
  150. package/src/evaluate/evaluate.ts +81 -0
  151. package/src/evaluate/evaluation-error.ts +6 -0
  152. package/src/evaluate/index.ts +2 -0
  153. package/src/index.ts +10 -0
  154. package/src/jsx.ts +161 -0
  155. package/src/parse/index.ts +2 -0
  156. package/src/parse/parse-error.ts +14 -0
  157. package/src/parse/parse.test.ts +99 -0
  158. package/src/parse/parse.ts +40 -0
  159. package/src/render.test.ts +425 -0
  160. package/src/render.ts +75 -0
  161. package/src/schema.ts +195 -0
  162. package/src/traverse.test.ts +104 -0
  163. package/src/traverse.ts +78 -0
  164. package/src/types.ts +17 -0
  165. package/src/validate.ts +59 -0
@@ -0,0 +1,329 @@
1
+ import type { MemberExpression, Program } from "acorn";
2
+ import {
3
+ isAllowedOnArray,
4
+ isAllowedOnString,
5
+ getAvailableArrayMembers,
6
+ getAvailableStringMembers,
7
+ } from "../../builtins";
8
+ import { Schema, ArrayPropertySchema, PropertySchema } from "../../schema";
9
+ import { AnalysisReport } from "../analysis-report";
10
+ import { getNodeRange } from "../utils";
11
+ import { ValidationContext } from "../validation-context";
12
+
13
+ export function isSimpleDataAccess(node: MemberExpression): boolean {
14
+ if (node.computed) {
15
+ return node.property.type === "Literal" && typeof node.property.value === "number";
16
+ }
17
+ if (node.property.type !== "Identifier") {
18
+ return false;
19
+ }
20
+
21
+ let current: MemberExpression["object"] = node.object;
22
+
23
+ while (current) {
24
+ if (current.type === "Identifier") {
25
+ return current.name === "data";
26
+ }
27
+
28
+ if (current.type === "MemberExpression") {
29
+ if (current.computed) {
30
+ if (!(current.property.type === "Literal" && typeof current.property.value === "number")) {
31
+ return false;
32
+ }
33
+ } else if (current.property.type !== "Identifier") {
34
+ return false;
35
+ }
36
+ current = current.object;
37
+ } else {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ return false;
43
+ }
44
+
45
+ export function extractPath(node: MemberExpression): string[] {
46
+ const path: string[] = [];
47
+
48
+ if (node.property.type === "Identifier") {
49
+ path.unshift(node.property.name);
50
+ }
51
+
52
+ let current: MemberExpression["object"] = node.object;
53
+
54
+ while (current) {
55
+ if (current.type === "Identifier") {
56
+ path.unshift(current.name);
57
+ break;
58
+ }
59
+
60
+ if (current.type === "MemberExpression") {
61
+ if (current.property.type === "Identifier") {
62
+ path.unshift(current.property.name);
63
+ }
64
+ current = current.object;
65
+ } else {
66
+ break;
67
+ }
68
+ }
69
+
70
+ return path;
71
+ }
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
+ export function getAvailablePropsAtPath(path: string[], depth: number, schemaData: any): string[] {
103
+ if (!schemaData || depth < 0) {
104
+ return schemaData ? Object.keys(schemaData) : [];
105
+ }
106
+
107
+ if (depth === 0) {
108
+ return Object.keys(schemaData);
109
+ }
110
+
111
+ let current = schemaData[path[0]];
112
+
113
+ for (let i = 1; i <= depth; i++) {
114
+ if (!current || current.type !== "object" || !current.shape) {
115
+ return [];
116
+ }
117
+ current = current.shape[path[i]];
118
+ }
119
+
120
+ if (current && current.type === "object" && current.shape) {
121
+ return Object.keys(current.shape);
122
+ }
123
+
124
+ return [];
125
+ }
126
+
127
+ export function validateSchemaPath(
128
+ path: string[],
129
+ schema: Schema,
130
+ analysisReport: AnalysisReport,
131
+ node: MemberExpression,
132
+ validationContext: ValidationContext
133
+ ): void {
134
+ const schemaData = schema.data;
135
+ if (!schemaData || path.length === 0) {
136
+ analysisReport.addIssue(
137
+ "INVALID_DATA_ACCESS",
138
+ "Invalid data access - no schema data available",
139
+ getNodeRange(node),
140
+ validationContext.getSnapshot(),
141
+ schemaData ? Object.keys(schemaData) : undefined
142
+ );
143
+ return;
144
+ }
145
+
146
+ let current = schemaData[path[0]];
147
+
148
+ if (!current) {
149
+ analysisReport.addIssue(
150
+ "INVALID_DATA_ACCESS",
151
+ `Property 'data.${path[0]}' does not exist in schema`,
152
+ getNodeRange(node),
153
+ validationContext.getSnapshot(),
154
+ Object.keys(schemaData)
155
+ );
156
+ return;
157
+ }
158
+
159
+ const elementAccessFlags = getElementAccessFlags(node, path.length);
160
+
161
+ for (let i = 1; i < path.length; i++) {
162
+ const prop = path[i];
163
+ const isElementAccess = elementAccessFlags[i];
164
+
165
+ if (current.type === "array") {
166
+ if (!isElementAccess) {
167
+ if (isAllowedOnArray(prop)) {
168
+ return;
169
+ }
170
+
171
+ const accessedPath = ["data", ...path.slice(0, i), prop].join(".");
172
+ analysisReport.addIssue(
173
+ "INVALID_DATA_ACCESS",
174
+ `Property '${accessedPath}' does not exist on array type`,
175
+ getNodeRange(node),
176
+ validationContext.getSnapshot(),
177
+ getAvailableArrayMembers()
178
+ );
179
+ return;
180
+ }
181
+
182
+ const elementType = current.shape || null;
183
+
184
+ if (!elementType) {
185
+ analysisReport.addIssue(
186
+ "INVALID_DATA_ACCESS",
187
+ `Cannot access property '${prop}' on unknown array element type`,
188
+ getNodeRange(node),
189
+ validationContext.getSnapshot(),
190
+ getAvailableArrayMembers()
191
+ );
192
+ return;
193
+ }
194
+
195
+ if (elementType.type === "object" && elementType.shape) {
196
+ const next = elementType.shape[prop];
197
+
198
+ if (!next) {
199
+ const paths = path.slice(0, i + 1);
200
+ const suggestions = Object.keys(elementType.shape);
201
+
202
+ analysisReport.addIssue(
203
+ "INVALID_DATA_ACCESS",
204
+ `Property 'data.${paths.join(".")}' does not exist in schema`,
205
+ getNodeRange(node),
206
+ validationContext.getSnapshot(),
207
+ suggestions
208
+ );
209
+ return;
210
+ }
211
+
212
+ current = next;
213
+ continue;
214
+ }
215
+
216
+ if (elementType.type === "array") {
217
+ if (isAllowedOnArray(prop)) {
218
+ return;
219
+ }
220
+
221
+ const accessedPath = ["data", ...path.slice(0, i), prop].join(".");
222
+ analysisReport.addIssue(
223
+ "INVALID_DATA_ACCESS",
224
+ `Property '${accessedPath}' does not exist on array type`,
225
+ getNodeRange(node),
226
+ validationContext.getSnapshot(),
227
+ getAvailableArrayMembers()
228
+ );
229
+ return;
230
+ }
231
+
232
+ if (elementType.type === "string") {
233
+ if (isAllowedOnString(prop)) {
234
+ return;
235
+ }
236
+
237
+ const accessedPath = ["data", ...path.slice(0, i), prop].join(".");
238
+ analysisReport.addIssue(
239
+ "INVALID_DATA_ACCESS",
240
+ `Property '${accessedPath}' does not exist on string type`,
241
+ getNodeRange(node),
242
+ validationContext.getSnapshot(),
243
+ getAvailableStringMembers()
244
+ );
245
+ return;
246
+ }
247
+
248
+ analysisReport.addIssue(
249
+ "INVALID_DATA_ACCESS",
250
+ `Cannot access property '${prop}' on ${elementType.type || "undefined"} type array element`,
251
+ getNodeRange(node),
252
+ validationContext.getSnapshot()
253
+ );
254
+ return;
255
+ }
256
+
257
+ if (current.type === "string") {
258
+ if (isAllowedOnString(prop)) {
259
+ return;
260
+ }
261
+
262
+ const accessedPath = ["data", ...path.slice(0, i), prop].join(".");
263
+ analysisReport.addIssue(
264
+ "INVALID_DATA_ACCESS",
265
+ `Property '${accessedPath}' does not exist on string type`,
266
+ getNodeRange(node),
267
+ validationContext.getSnapshot(),
268
+ getAvailableStringMembers()
269
+ );
270
+ return;
271
+ }
272
+
273
+ if (current.type !== "object" || !current.shape) {
274
+ analysisReport.addIssue(
275
+ "INVALID_DATA_ACCESS",
276
+ `Cannot access property '${prop}' on ${current.type || "undefined"} type`,
277
+ getNodeRange(node),
278
+ validationContext.getSnapshot()
279
+ );
280
+ return;
281
+ }
282
+
283
+ current = current.shape[prop];
284
+
285
+ if (!current) {
286
+ const availableProps = getAvailablePropsAtPath(path, i - 1, schemaData);
287
+ const paths = path.slice(0, i + 1);
288
+
289
+ analysisReport.addIssue(
290
+ "INVALID_DATA_ACCESS",
291
+ `Property 'data.${paths.join(".")}' does not exist in schema`,
292
+ getNodeRange(node),
293
+ validationContext.getSnapshot(),
294
+ availableProps
295
+ );
296
+ }
297
+ }
298
+ }
299
+
300
+ function getElementAccessFlags(node: MemberExpression, pathLength: number): boolean[] {
301
+ const flags = Array(pathLength).fill(false);
302
+ const segments: Array<{ property: any; computed: boolean }> = [];
303
+
304
+ let current: any = node;
305
+ while (current && current.type === "MemberExpression") {
306
+ segments.unshift({ property: current.property, computed: current.computed });
307
+ current = current.object;
308
+ }
309
+
310
+ let schemaIndex = -1;
311
+ let lastWasComputedNumeric = false;
312
+
313
+ for (const segment of segments) {
314
+ if (segment.property?.type === "Identifier") {
315
+ schemaIndex += 1;
316
+ if (schemaIndex < pathLength) {
317
+ flags[schemaIndex] = lastWasComputedNumeric;
318
+ }
319
+ }
320
+
321
+ if (segment.computed && segment.property?.type === "Literal" && typeof segment.property.value === "number") {
322
+ lastWasComputedNumeric = true;
323
+ } else {
324
+ lastWasComputedNumeric = false;
325
+ }
326
+ }
327
+
328
+ return flags;
329
+ }
@@ -0,0 +1,3 @@
1
+ export { analyze } from "./analyze";
2
+ export { AnalysisError } from "./analysis-error";
3
+ export { AnalysisReport } from "./analysis-report";
@@ -0,0 +1,17 @@
1
+ import { Program } from "acorn";
2
+ import { Schema } from "../../schema";
3
+ import { AnalysisReport } from "../analysis-report";
4
+ import { ValidationContext } from "../validation-context";
5
+ import { analyzeElementAttributes } from "./element-attributes";
6
+ import { analyzeElementChildren } from "./element-children";
7
+ import { analyzeElementTags } from "./element-tags";
8
+
9
+ export function analyzeJsx(ast: Program, schema: Schema): AnalysisReport {
10
+ const validationContext = new ValidationContext();
11
+
12
+ return new AnalysisReport().merge(
13
+ analyzeElementTags(ast, schema, validationContext),
14
+ analyzeElementAttributes(ast, schema, validationContext),
15
+ analyzeElementChildren(ast, schema, validationContext)
16
+ );
17
+ }
@@ -0,0 +1,143 @@
1
+ import type { Program } from "acorn";
2
+ import { traverse } from "../../traverse";
3
+ import {
4
+ type Schema,
5
+ isAttributeAllowed,
6
+ getAllowedAttributes,
7
+ getRequiredAttributes,
8
+ getAttributeSchema,
9
+ getEnumValues,
10
+ } from "../../schema";
11
+ import { isJsxAttribute, isJsxExpressionContainer, type JSXAttribute } from "../../jsx";
12
+ import { ValidationContext } from "../validation-context";
13
+ import { AnalysisReport } from "../analysis-report";
14
+
15
+ import { getNodeRange, getAttributeSimilarityMatchers, getBestSimilarityMatcherSuggestion } from "../utils";
16
+
17
+ export function analyzeElementAttributes(
18
+ ast: Program,
19
+ schema: Schema,
20
+ validationContext: ValidationContext
21
+ ): AnalysisReport {
22
+ const analysisReport = new AnalysisReport();
23
+
24
+ traverse(ast, {
25
+ JSXElement(node) {
26
+ const tagName = node.openingElement.name.name;
27
+
28
+ validationContext.enterElement(tagName);
29
+ const currentContext = validationContext.getSnapshot();
30
+
31
+ const allowedAttributes = getAllowedAttributes(schema, tagName);
32
+ const requiredAttributes = getRequiredAttributes(schema, tagName);
33
+
34
+ for (const attribute of node.openingElement.attributes) {
35
+ if (!isJsxAttribute(attribute)) {
36
+ continue;
37
+ }
38
+
39
+ const attributeName = attribute.name.name;
40
+ const range = getNodeRange(attribute);
41
+
42
+ if (!isAttributeAllowed(schema, tagName, attributeName)) {
43
+ const suggestions = getAttributeSimilarityMatchers(attributeName, allowedAttributes);
44
+ const suggestionMatch = getBestSimilarityMatcherSuggestion(attributeName, allowedAttributes);
45
+
46
+ let message = `Attribute "${attributeName}" not allowed on <${tagName}>`;
47
+
48
+ if (suggestionMatch) {
49
+ message += `. Did you mean "${suggestionMatch}"?`;
50
+ } else if (allowedAttributes.length > 0) {
51
+ const displayAttrs = allowedAttributes.slice(0, 5);
52
+ message += `. Available attributes: ${displayAttrs.join(", ")}`;
53
+ if (allowedAttributes.length > 5) {
54
+ message += ` (and ${allowedAttributes.length - 5} more)`;
55
+ }
56
+ }
57
+
58
+ analysisReport.addIssue(
59
+ "INVALID_ATTRIBUTE",
60
+ message,
61
+ range,
62
+ currentContext,
63
+ suggestions.map((suggestion) => suggestion.suggestion)
64
+ );
65
+ continue;
66
+ }
67
+
68
+ const attributeSchema = getAttributeSchema(schema, tagName, attributeName);
69
+ const enumValues = attributeSchema ? getEnumValues(attributeSchema) : undefined;
70
+
71
+ if (enumValues && enumValues.length > 0) {
72
+ const literalValue = getAttributeLiteralValue(attribute);
73
+
74
+ if (isEnumComparableValue(literalValue) && !enumValues.includes(literalValue)) {
75
+ const stringEnumValues = enumValues.map((value) => String(value));
76
+ const suggestionMatch = getBestSimilarityMatcherSuggestion(String(literalValue), stringEnumValues);
77
+
78
+ let message = `Value ${JSON.stringify(
79
+ literalValue
80
+ )} is not allowed for attribute "${attributeName}" on <${tagName}>`;
81
+
82
+ if (suggestionMatch) {
83
+ message += `. Did you mean "${suggestionMatch}"?`;
84
+ } else {
85
+ message += `. Allowed values: ${enumValues.map((value) => JSON.stringify(value)).join(", ")}`;
86
+ }
87
+
88
+ analysisReport.addIssue(
89
+ "INVALID_ATTRIBUTE_VALUE",
90
+ message,
91
+ range,
92
+ currentContext,
93
+ suggestionMatch ? [suggestionMatch] : stringEnumValues
94
+ );
95
+ }
96
+ }
97
+ }
98
+
99
+ // Check for missing required attributes
100
+ const providedAttributes = node.openingElement.attributes.filter(isJsxAttribute).map((attr) => attr.name.name);
101
+
102
+ for (const requiredAttribute of requiredAttributes) {
103
+ if (!providedAttributes.includes(requiredAttribute)) {
104
+ analysisReport.addIssue(
105
+ "MISSING_REQUIRED_ATTRIBUTE",
106
+ `Required attribute "${requiredAttribute}" missing on <${tagName}>`,
107
+ getNodeRange(node.openingElement),
108
+ currentContext,
109
+ [requiredAttribute]
110
+ );
111
+ }
112
+ }
113
+
114
+ validationContext.exitElement();
115
+ },
116
+ });
117
+
118
+ return analysisReport;
119
+ }
120
+
121
+ function isEnumComparableValue(value: unknown): value is string | number | boolean {
122
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
123
+ }
124
+
125
+ function getAttributeLiteralValue(attribute: JSXAttribute): string | number | boolean | null | undefined {
126
+ if (!attribute.value) {
127
+ return true;
128
+ }
129
+
130
+ if (attribute.value.type === "Literal") {
131
+ return attribute.value.value as string | number | boolean | null | undefined;
132
+ }
133
+
134
+ if (isJsxExpressionContainer(attribute.value)) {
135
+ const expression = attribute.value.expression as { type?: string; value?: unknown };
136
+
137
+ if (expression?.type === "Literal") {
138
+ return expression.value as string | number | boolean | null | undefined;
139
+ }
140
+ }
141
+
142
+ return undefined;
143
+ }
@@ -0,0 +1,97 @@
1
+ import type { Program } from "acorn";
2
+ import { traverse } from "../../traverse";
3
+ import { type Schema, getAllowedChildren, isSelfClosing } from "../../schema";
4
+ import { isJsxElement, isJsxFragment, isJsxText, isJsxLeafNode } from "../../jsx";
5
+ import { ValidationContext } from "../validation-context";
6
+ import { AnalysisReport } from "../analysis-report";
7
+ import { getNodeRange, getElementSimilarityMatchers, getBestSimilarityMatcherSuggestion } from "../utils";
8
+
9
+ export function analyzeElementChildren(
10
+ ast: Program,
11
+ schema: Schema,
12
+ validationContext: ValidationContext
13
+ ): AnalysisReport {
14
+ const analysisReport = new AnalysisReport();
15
+
16
+ traverse(ast, {
17
+ JSXElement(node) {
18
+ const parentTag = node.openingElement.name.name;
19
+
20
+ validationContext.enterElement(parentTag);
21
+
22
+ const currentContext = validationContext.getSnapshot();
23
+
24
+ const allowedChildren = getAllowedChildren(schema, parentTag);
25
+
26
+ if (!allowedChildren) {
27
+ validationContext.exitElement();
28
+ return;
29
+ }
30
+
31
+ if (isSelfClosing(schema, parentTag) && node.children.length > 0) {
32
+ const hasNonWhitespaceContent = node.children.some((child) => {
33
+ if (isJsxText(child)) {
34
+ return child.value.trim().length > 0;
35
+ }
36
+ return !isJsxLeafNode(child) || !isJsxText(child);
37
+ });
38
+
39
+ if (hasNonWhitespaceContent) {
40
+ const firstMeaningfulChild = node.children.find((child) => {
41
+ if (isJsxText(child)) {
42
+ return child.value.trim().length > 0;
43
+ }
44
+ return true;
45
+ });
46
+
47
+ analysisReport.addIssue(
48
+ "SELF_CLOSING_WITH_CHILDREN",
49
+ `Element <${parentTag}> cannot have children. It is self-closing.`,
50
+ getNodeRange(firstMeaningfulChild || node.children[0]),
51
+ currentContext
52
+ );
53
+ }
54
+ }
55
+
56
+ for (const child of node.children) {
57
+ if (isJsxElement(child)) {
58
+ const childTag = child.openingElement.name.name;
59
+
60
+ if (!allowedChildren.includes(childTag)) {
61
+ const elementSimilarityMatchers = getElementSimilarityMatchers(childTag, allowedChildren);
62
+ const bestSimilarityMatcherSuggestion = getBestSimilarityMatcherSuggestion(childTag, allowedChildren);
63
+
64
+ let message = `Element <${childTag}> not allowed in <${parentTag}>`;
65
+
66
+ if (bestSimilarityMatcherSuggestion) {
67
+ message += `. Did you mean <${bestSimilarityMatcherSuggestion}>?`;
68
+ }
69
+
70
+ message += `. Allowed children: ${allowedChildren.join(", ")}`;
71
+
72
+ analysisReport.addIssue(
73
+ "INVALID_CHILD_ELEMENT",
74
+ message,
75
+ getNodeRange(child),
76
+ currentContext,
77
+ elementSimilarityMatchers.map((similarityMatcher) => similarityMatcher.suggestion)
78
+ );
79
+ }
80
+ } else if (isJsxFragment(child)) {
81
+ // TODO: Recursively validate fragment children
82
+ // For now, we allow fragments but should add more specific validation in the future
83
+ } else if (isJsxText(child)) {
84
+ const hasContent = child.value.trim().length > 0;
85
+ if (!hasContent) {
86
+ // Allow whitespace-only text nodes (very common in JSX formatting)
87
+ continue;
88
+ }
89
+ }
90
+ }
91
+
92
+ validationContext.exitElement();
93
+ },
94
+ });
95
+
96
+ return analysisReport;
97
+ }
@@ -0,0 +1,55 @@
1
+ import type { Program } from "acorn";
2
+ import { traverse } from "../../traverse";
3
+ import { type Schema, isElementAllowed, getAllElements } from "../../schema";
4
+ import { ValidationContext } from "../validation-context";
5
+ import { AnalysisReport } from "../analysis-report";
6
+ import { getNodeRange, getElementSimilarityMatchers, getBestSimilarityMatcherSuggestion } from "../utils";
7
+
8
+ export function analyzeElementTags(ast: Program, schema: Schema, validationContext: ValidationContext): AnalysisReport {
9
+ const analysisReport = new AnalysisReport();
10
+
11
+ traverse(ast, {
12
+ JSXElement(node) {
13
+ const tagName = node.openingElement.name.name;
14
+
15
+ validationContext.enterElement(tagName);
16
+
17
+ const snapshot = validationContext.getSnapshot();
18
+ const range = getNodeRange(node);
19
+
20
+ if (!isElementAllowed(schema, tagName)) {
21
+ const availableElements = getAllElements(schema);
22
+ const elementSimilarityMatchers = getElementSimilarityMatchers(tagName, availableElements);
23
+ const bestSuggestionMatch = getBestSimilarityMatcherSuggestion(tagName, availableElements);
24
+
25
+ let message = `Element <${tagName}> not allowed`;
26
+
27
+ if (snapshot.elements.length > 1) {
28
+ message += ` in ${validationContext.currentPath}`;
29
+ }
30
+
31
+ if (bestSuggestionMatch) {
32
+ message += `. Did you mean <${bestSuggestionMatch}>?`;
33
+ } else if (availableElements.length > 0) {
34
+ const displayElements = availableElements.slice(0, 5);
35
+ message += `. Available elements: ${displayElements.join(", ")}`;
36
+ if (availableElements.length > 5) {
37
+ message += ` (and ${availableElements.length - 5} more)`;
38
+ }
39
+ }
40
+
41
+ analysisReport.addIssue(
42
+ "INVALID_ELEMENT",
43
+ message,
44
+ range,
45
+ snapshot,
46
+ elementSimilarityMatchers.map((similarityMatcher) => similarityMatcher.suggestion)
47
+ );
48
+ }
49
+
50
+ validationContext.exitElement();
51
+ },
52
+ });
53
+
54
+ return analysisReport;
55
+ }
@@ -0,0 +1 @@
1
+ export { analyzeJsx } from "./analyze";
@@ -0,0 +1,21 @@
1
+ import { Program } from "acorn";
2
+ import { Schema } from "../../schema";
3
+ import { AnalysisReport } from "../analysis-report";
4
+ import { ValidationContext } from "../validation-context";
5
+ import { analyzeCallExpressions } from "./call-expressions";
6
+ import { analyzeDeclarations } from "./declarations";
7
+ import { analyzeExpressions } from "./expressions";
8
+ import { analyzeIdentifiers } from "./identifiers";
9
+ import { analyzeMemberAccess } from "./member-access";
10
+
11
+ export function analyzeSecurity(ast: Program, schema: Schema): AnalysisReport {
12
+ const validationContext = new ValidationContext();
13
+
14
+ return new AnalysisReport().merge(
15
+ analyzeDeclarations(ast, schema, validationContext),
16
+ analyzeIdentifiers(ast, schema, validationContext),
17
+ analyzeExpressions(ast, schema, validationContext),
18
+ analyzeMemberAccess(ast, schema, validationContext),
19
+ analyzeCallExpressions(ast, schema, validationContext)
20
+ );
21
+ }