@rettangoli/check 0.0.1

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 (42) hide show
  1. package/README.md +295 -0
  2. package/ROADMAP.md +175 -0
  3. package/package.json +46 -0
  4. package/src/cli/bin.js +325 -0
  5. package/src/cli/check.js +232 -0
  6. package/src/cli/index.js +1 -0
  7. package/src/core/analyze.js +227 -0
  8. package/src/core/discovery.js +83 -0
  9. package/src/core/exportedFunctions.js +235 -0
  10. package/src/core/model.js +898 -0
  11. package/src/core/parsers.js +2726 -0
  12. package/src/core/registry.js +779 -0
  13. package/src/core/schema.js +161 -0
  14. package/src/core/scopeGraph.js +1400 -0
  15. package/src/core/semantic.js +329 -0
  16. package/src/diagnostics/autofix.js +191 -0
  17. package/src/diagnostics/catalog.js +89 -0
  18. package/src/index.js +2 -0
  19. package/src/reporters/index.js +13 -0
  20. package/src/reporters/json.js +42 -0
  21. package/src/reporters/sarif.js +213 -0
  22. package/src/reporters/text.js +145 -0
  23. package/src/rules/compatibility.js +318 -0
  24. package/src/rules/constants.js +22 -0
  25. package/src/rules/crossFileSymbols.js +108 -0
  26. package/src/rules/expression.js +338 -0
  27. package/src/rules/feParity.js +65 -0
  28. package/src/rules/index.js +39 -0
  29. package/src/rules/jempl.js +80 -0
  30. package/src/rules/lifecycle.js +4 -0
  31. package/src/rules/listenerConfig.js +556 -0
  32. package/src/rules/listenerSymbols.js +49 -0
  33. package/src/rules/methods.js +117 -0
  34. package/src/rules/refs.js +20 -0
  35. package/src/rules/schema.js +118 -0
  36. package/src/rules/shared.js +20 -0
  37. package/src/rules/yahtmlAttrs.js +238 -0
  38. package/src/semantic/engine.js +778 -0
  39. package/src/semantic/index.js +9 -0
  40. package/src/types/lattice.js +281 -0
  41. package/src/utils/case.js +9 -0
  42. package/src/utils/fs.js +30 -0
@@ -0,0 +1,108 @@
1
+ import {
2
+ getListenerSymbols,
3
+ isValidHandlerSymbol,
4
+ resolveListenerLine,
5
+ } from "./listenerSymbols.js";
6
+ import { collectInvalidRefKeys } from "./refs.js";
7
+ import { getModelFilePath, getYamlPathLine } from "./shared.js";
8
+
9
+ const getSchemaMethodNames = (model) => {
10
+ const normalizedMethodNames = model?.schema?.normalized?.methods?.names;
11
+ if (Array.isArray(normalizedMethodNames)) {
12
+ return normalizedMethodNames;
13
+ }
14
+
15
+ const methodProps = model?.schema?.yaml?.methods?.properties;
16
+ if (!methodProps || typeof methodProps !== "object" || Array.isArray(methodProps)) {
17
+ return [];
18
+ }
19
+ return Object.keys(methodProps);
20
+ };
21
+
22
+ export const runCrossFileSymbolRules = ({ models = [] }) => {
23
+ const diagnostics = [];
24
+
25
+ models.forEach((model) => {
26
+ const viewFilePath = getModelFilePath({ model, fileType: "view" });
27
+ const refs = model?.view?.yaml?.refs;
28
+ const invalidRefKeys = collectInvalidRefKeys(refs);
29
+
30
+ model.view.refListeners.forEach(({ refKey, eventType, eventConfig, line, optionLines }) => {
31
+ if (invalidRefKeys.has(refKey)) {
32
+ return;
33
+ }
34
+
35
+ const listenerSymbols = getListenerSymbols(eventConfig);
36
+ const handlerSymbol = listenerSymbols.handler;
37
+ const actionSymbol = listenerSymbols.action;
38
+
39
+ if (handlerSymbol.isValid && isValidHandlerSymbol(handlerSymbol.value)) {
40
+ if (!model.handlers.exports.has(handlerSymbol.value)) {
41
+ diagnostics.push({
42
+ code: "RTGL-CHECK-SYMBOL-001",
43
+ severity: "error",
44
+ filePath: viewFilePath,
45
+ line: resolveListenerLine({
46
+ listenerLine: line,
47
+ optionLines,
48
+ preferredKeys: ["handler"],
49
+ }),
50
+ message: `${model.componentKey}: handler '${handlerSymbol.value}' for event '${eventType}' on ref '${refKey}' is missing in .handlers.js exports.`,
51
+ });
52
+ }
53
+ }
54
+
55
+ if (actionSymbol.isValid) {
56
+ if (!model.store.exports.has(actionSymbol.value)) {
57
+ diagnostics.push({
58
+ code: "RTGL-CHECK-SYMBOL-002",
59
+ severity: "error",
60
+ filePath: viewFilePath,
61
+ line: resolveListenerLine({
62
+ listenerLine: line,
63
+ optionLines,
64
+ preferredKeys: ["action"],
65
+ }),
66
+ message: `${model.componentKey}: action '${actionSymbol.value}' for event '${eventType}' on ref '${refKey}' is missing in .store.js exports.`,
67
+ });
68
+ }
69
+ }
70
+ });
71
+
72
+ const declaredMethods = getSchemaMethodNames(model);
73
+ declaredMethods.forEach((methodName) => {
74
+ if (!model.methods.exports.has(methodName)) {
75
+ diagnostics.push({
76
+ code: "RTGL-CHECK-SYMBOL-003",
77
+ severity: "error",
78
+ filePath: getModelFilePath({ model, fileType: "schema" }),
79
+ line: getYamlPathLine(model.schema.yamlKeyPathLines, ["methods", "properties", methodName]),
80
+ message: `${model.componentKey}: method '${methodName}' is declared in schema but missing in .methods.js exports.`,
81
+ });
82
+ }
83
+ });
84
+
85
+ model.methods.exports.forEach((methodName) => {
86
+ if (methodName === "default") {
87
+ diagnostics.push({
88
+ code: "RTGL-CHECK-SYMBOL-004",
89
+ severity: "error",
90
+ filePath: getModelFilePath({ model, fileType: "methods" }),
91
+ message: `${model.componentKey}: method name 'default' is not supported. Use named exports only.`,
92
+ });
93
+ return;
94
+ }
95
+
96
+ if (!declaredMethods.includes(methodName)) {
97
+ diagnostics.push({
98
+ code: "RTGL-CHECK-SYMBOL-005",
99
+ severity: "warn",
100
+ filePath: getModelFilePath({ model, fileType: "methods" }),
101
+ message: `${model.componentKey}: method '${methodName}' is exported in .methods.js but not documented in schema.methods.properties.`,
102
+ });
103
+ }
104
+ });
105
+ });
106
+
107
+ return diagnostics;
108
+ };
@@ -0,0 +1,338 @@
1
+ import { buildComponentScopeGraph, resolveExpressionPathType } from "../core/scopeGraph.js";
2
+ import { JEMPL_BINARY_OP, JEMPL_NODE, JEMPL_UNARY_OP } from "../core/parsers.js";
3
+ import { collectSelectViewDataRoots } from "../core/semantic.js";
4
+ import { areTypesCompatible, inferSchemaNodePrimitiveType } from "../types/lattice.js";
5
+ import { getModelFilePath } from "./shared.js";
6
+
7
+ const JEMPL_BINARY_OPERATOR_SYMBOL_BY_OP = new Map([
8
+ [JEMPL_BINARY_OP.EQ, "=="],
9
+ [JEMPL_BINARY_OP.NEQ, "!="],
10
+ [JEMPL_BINARY_OP.GT, ">"],
11
+ [JEMPL_BINARY_OP.LT, "<"],
12
+ [JEMPL_BINARY_OP.GTE, ">="],
13
+ [JEMPL_BINARY_OP.LTE, "<="],
14
+ [JEMPL_BINARY_OP.AND, "&&"],
15
+ [JEMPL_BINARY_OP.OR, "||"],
16
+ [JEMPL_BINARY_OP.IN, "in"],
17
+ [JEMPL_BINARY_OP.ADD, "+"],
18
+ [JEMPL_BINARY_OP.SUBTRACT, "-"],
19
+ ]);
20
+
21
+ const isUnknownExpressionRoot = ({ root = "", globalSymbols = new Set(), localSymbols = new Set() }) => {
22
+ if (!root) {
23
+ return false;
24
+ }
25
+ if (globalSymbols.has(root)) {
26
+ return false;
27
+ }
28
+ if (localSymbols.has(root)) {
29
+ return false;
30
+ }
31
+ if (/^[-+]?(?:\d+\.?\d*|\d*\.?\d+)$/u.test(root)) {
32
+ return false;
33
+ }
34
+ return true;
35
+ };
36
+
37
+ const isEqualityCompatibleTypePair = ({ leftType, rightType }) => {
38
+ if (!leftType || !rightType) {
39
+ return true;
40
+ }
41
+
42
+ if (leftType === rightType) {
43
+ return true;
44
+ }
45
+
46
+ return leftType === "null" || rightType === "null";
47
+ };
48
+
49
+ const inferExpressionAstType = ({
50
+ model,
51
+ node,
52
+ localSchemaTypes,
53
+ viewDataRoots,
54
+ issues,
55
+ }) => {
56
+ if (!node || typeof node !== "object") {
57
+ return null;
58
+ }
59
+
60
+ if (node.type === JEMPL_NODE.LITERAL) {
61
+ if (typeof node.value === "boolean") return "boolean";
62
+ if (typeof node.value === "number") return "number";
63
+ if (typeof node.value === "string") return "string";
64
+ if (node.value === null) return "null";
65
+ return null;
66
+ }
67
+
68
+ if (node.type === JEMPL_NODE.PATH && typeof node.path === "string") {
69
+ const pathType = resolveExpressionPathType({
70
+ model,
71
+ expression: node.path,
72
+ localSchemaTypes,
73
+ });
74
+ if (!pathType || !pathType.resolved) {
75
+ return null;
76
+ }
77
+ if (pathType.rootKind === "schema" && viewDataRoots.has(pathType.root)) {
78
+ return null;
79
+ }
80
+ return inferSchemaNodePrimitiveType(pathType.resolved);
81
+ }
82
+
83
+ if (node.type === JEMPL_NODE.UNARY) {
84
+ const operandType = inferExpressionAstType({
85
+ model,
86
+ node: node.operand,
87
+ localSchemaTypes,
88
+ viewDataRoots,
89
+ issues,
90
+ });
91
+
92
+ if (node.op === JEMPL_UNARY_OP.NOT) {
93
+ if (operandType && operandType !== "boolean") {
94
+ issues.push(`operator '!' expects a boolean operand but resolved '${operandType}'.`);
95
+ }
96
+ return "boolean";
97
+ }
98
+ return null;
99
+ }
100
+
101
+ if (node.type === JEMPL_NODE.BINARY) {
102
+ const leftType = inferExpressionAstType({
103
+ model,
104
+ node: node.left,
105
+ localSchemaTypes,
106
+ viewDataRoots,
107
+ issues,
108
+ });
109
+ const rightType = inferExpressionAstType({
110
+ model,
111
+ node: node.right,
112
+ localSchemaTypes,
113
+ viewDataRoots,
114
+ issues,
115
+ });
116
+ const operator = JEMPL_BINARY_OPERATOR_SYMBOL_BY_OP.get(node.op) || "unknown";
117
+
118
+ if (node.op === JEMPL_BINARY_OP.AND || node.op === JEMPL_BINARY_OP.OR) {
119
+ if (leftType && rightType && (leftType !== "boolean" || rightType !== "boolean")) {
120
+ issues.push(`operator '${operator}' expects boolean operands but resolved '${leftType}' and '${rightType}'.`);
121
+ }
122
+ return "boolean";
123
+ }
124
+
125
+ if (
126
+ node.op === JEMPL_BINARY_OP.GT
127
+ || node.op === JEMPL_BINARY_OP.GTE
128
+ || node.op === JEMPL_BINARY_OP.LT
129
+ || node.op === JEMPL_BINARY_OP.LTE
130
+ ) {
131
+ const comparable = new Set(["number", "string"]);
132
+ if (leftType && rightType) {
133
+ if (!comparable.has(leftType) || !comparable.has(rightType) || leftType !== rightType) {
134
+ issues.push(`operator '${operator}' expects both operands to be 'number' or both 'string', but resolved '${leftType}' and '${rightType}'.`);
135
+ }
136
+ }
137
+ return "boolean";
138
+ }
139
+
140
+ if (node.op === JEMPL_BINARY_OP.IN) {
141
+ if (leftType && rightType && !["array", "string", "object"].includes(rightType)) {
142
+ issues.push(`operator 'in' expects right operand type 'array', 'string', or 'object', but resolved '${rightType}'.`);
143
+ }
144
+ return "boolean";
145
+ }
146
+
147
+ if (node.op === JEMPL_BINARY_OP.ADD) {
148
+ if (leftType && rightType) {
149
+ if (leftType === "number" && rightType === "number") {
150
+ return "number";
151
+ }
152
+ if (leftType === "string" && rightType === "string") {
153
+ return "string";
154
+ }
155
+ issues.push(`operator '+' expects both operands to be 'number' or both 'string', but resolved '${leftType}' and '${rightType}'.`);
156
+ }
157
+ return null;
158
+ }
159
+
160
+ if (node.op === JEMPL_BINARY_OP.SUBTRACT) {
161
+ if (leftType && rightType && (leftType !== "number" || rightType !== "number")) {
162
+ issues.push(`operator '-' expects number operands but resolved '${leftType}' and '${rightType}'.`);
163
+ }
164
+ return leftType && rightType ? "number" : null;
165
+ }
166
+
167
+ if (
168
+ node.op === JEMPL_BINARY_OP.EQ
169
+ || node.op === JEMPL_BINARY_OP.NEQ
170
+ ) {
171
+ if (!isEqualityCompatibleTypePair({ leftType, rightType })) {
172
+ issues.push(`operator '${operator}' expects compatible operand types, but resolved '${leftType}' and '${rightType}'.`);
173
+ }
174
+ return "boolean";
175
+ }
176
+ }
177
+
178
+ return null;
179
+ };
180
+
181
+ const collectPathNodesFromExpressionAst = (node, rows = []) => {
182
+ if (Array.isArray(node)) {
183
+ node.forEach((entry) => collectPathNodesFromExpressionAst(entry, rows));
184
+ return rows;
185
+ }
186
+
187
+ if (!node || typeof node !== "object") {
188
+ return rows;
189
+ }
190
+
191
+ if (node.type === JEMPL_NODE.PATH && typeof node.path === "string") {
192
+ rows.push({
193
+ path: node.path,
194
+ range: node.range || {},
195
+ });
196
+ }
197
+
198
+ Object.values(node).forEach((value) => {
199
+ collectPathNodesFromExpressionAst(value, rows);
200
+ });
201
+
202
+ return rows;
203
+ };
204
+
205
+ export const runExpressionRules = ({ models = [] }) => {
206
+ const diagnostics = [];
207
+
208
+ models.forEach((model) => {
209
+ const viewPath = getModelFilePath({ model, fileType: "view" });
210
+ const scopeGraph = model?.semanticGraph || buildComponentScopeGraph(model);
211
+ const viewDataRoots = collectSelectViewDataRoots(
212
+ model?.store?.sourceText || "",
213
+ model?.store?.filePath || "unknown.store.js",
214
+ );
215
+ const globalSymbols = scopeGraph?.globalSymbols instanceof Set
216
+ ? scopeGraph.globalSymbols
217
+ : new Set();
218
+ const references = Array.isArray(scopeGraph?.references) ? scopeGraph.references : [];
219
+
220
+ references.forEach((reference) => {
221
+ const localSymbols = reference?.localSymbols instanceof Set
222
+ ? reference.localSymbols
223
+ : new Set(reference?.localSymbols || []);
224
+
225
+ const unknownRoots = (reference?.roots || []).filter((root) => isUnknownExpressionRoot({
226
+ root,
227
+ globalSymbols,
228
+ localSymbols,
229
+ }));
230
+ if (unknownRoots.length > 0) {
231
+ diagnostics.push({
232
+ code: "RTGL-CHECK-EXPR-001",
233
+ severity: "error",
234
+ filePath: viewPath,
235
+ line: reference.line,
236
+ column: reference.column,
237
+ endLine: reference.endLine,
238
+ endColumn: reference.endColumn,
239
+ message: `${model.componentKey}: unresolved expression root(s) [${unknownRoots.join(", ")}] in '${reference.expression}'.`,
240
+ });
241
+ }
242
+
243
+ const localSchemaTypes = reference?.localSchemaTypes instanceof Map
244
+ ? reference.localSchemaTypes
245
+ : new Map(reference?.localSchemaTypes || []);
246
+ const schemaPathType = resolveExpressionPathType({
247
+ model,
248
+ expression: reference?.expression || "",
249
+ localSchemaTypes,
250
+ });
251
+ if (reference?.context === "condition" && reference?.expressionAst) {
252
+ const pathNodes = collectPathNodesFromExpressionAst(reference.expressionAst);
253
+ pathNodes.forEach((pathNode) => {
254
+ const pathType = resolveExpressionPathType({
255
+ model,
256
+ expression: pathNode.path,
257
+ localSchemaTypes,
258
+ });
259
+ if (!pathType || pathType.resolved || !pathType.missingSegment) {
260
+ return;
261
+ }
262
+
263
+ diagnostics.push({
264
+ code: "RTGL-CHECK-EXPR-005",
265
+ severity: "error",
266
+ filePath: viewPath,
267
+ line: pathNode?.range?.line || reference.line,
268
+ column: pathNode?.range?.column || reference.column,
269
+ endLine: pathNode?.range?.endLine || reference.endLine,
270
+ endColumn: pathNode?.range?.endColumn || reference.endColumn,
271
+ message: `${model.componentKey}: unknown schema path segment '${pathType.missingSegment}' in condition path '${pathNode.path}'.`,
272
+ });
273
+ });
274
+ }
275
+
276
+ if (!schemaPathType) {
277
+ if (reference?.context === "condition" && reference?.expressionAst) {
278
+ const issues = [];
279
+ inferExpressionAstType({
280
+ model,
281
+ node: reference.expressionAst,
282
+ localSchemaTypes,
283
+ viewDataRoots,
284
+ issues,
285
+ });
286
+ issues.forEach((issue) => {
287
+ diagnostics.push({
288
+ code: "RTGL-CHECK-EXPR-004",
289
+ severity: "error",
290
+ filePath: viewPath,
291
+ line: reference.line,
292
+ column: reference.column,
293
+ endLine: reference.endLine,
294
+ endColumn: reference.endColumn,
295
+ message: `${model.componentKey}: invalid condition expression '${reference.expression}': ${issue}`,
296
+ });
297
+ });
298
+ }
299
+ return;
300
+ }
301
+ if (schemaPathType.rootKind === "schema" && viewDataRoots.has(schemaPathType.root)) {
302
+ return;
303
+ }
304
+
305
+ if (reference?.context !== "condition" && !schemaPathType.resolved && schemaPathType.missingSegment) {
306
+ diagnostics.push({
307
+ code: "RTGL-CHECK-EXPR-002",
308
+ severity: "error",
309
+ filePath: viewPath,
310
+ line: reference.line,
311
+ column: reference.column,
312
+ endLine: reference.endLine,
313
+ endColumn: reference.endColumn,
314
+ message: `${model.componentKey}: unknown schema path segment '${schemaPathType.missingSegment}' in expression '${reference.expression}'.`,
315
+ });
316
+ return;
317
+ }
318
+
319
+ if (reference?.context === "attr-boolean") {
320
+ const resolvedType = inferSchemaNodePrimitiveType(schemaPathType.resolved);
321
+ if (!areTypesCompatible({ expected: "boolean", actual: resolvedType })) {
322
+ diagnostics.push({
323
+ code: "RTGL-CHECK-EXPR-003",
324
+ severity: "error",
325
+ filePath: viewPath,
326
+ line: reference.line,
327
+ column: reference.column,
328
+ endLine: reference.endLine,
329
+ endColumn: reference.endColumn,
330
+ message: `${model.componentKey}: boolean binding expects a boolean expression but '${reference.expression}' resolves to '${resolvedType}'.`,
331
+ });
332
+ }
333
+ }
334
+ });
335
+ });
336
+
337
+ return diagnostics;
338
+ };
@@ -0,0 +1,65 @@
1
+ import { getModelFilePath, isObjectRecord } from "./shared.js";
2
+
3
+ const FORBIDDEN_VIEW_KEYS = [
4
+ "elementName",
5
+ "viewDataSchema",
6
+ "propsSchema",
7
+ "events",
8
+ "methods",
9
+ "attrsSchema",
10
+ ];
11
+
12
+ export const runFeParityRules = ({ models = [] }) => {
13
+ const diagnostics = [];
14
+
15
+ models.forEach((model) => {
16
+ const viewPath = getModelFilePath({ model, fileType: "view" });
17
+
18
+ if (!model.files.schema) {
19
+ diagnostics.push({
20
+ code: "RTGL-CONTRACT-001",
21
+ severity: "error",
22
+ filePath: viewPath,
23
+ message: `${model.componentKey}: missing required .schema.yaml file.`,
24
+ });
25
+ }
26
+
27
+ const viewYaml = model?.view?.yaml;
28
+ if (isObjectRecord(viewYaml)) {
29
+ FORBIDDEN_VIEW_KEYS.forEach((forbiddenKey) => {
30
+ if (Object.prototype.hasOwnProperty.call(viewYaml, forbiddenKey)) {
31
+ diagnostics.push({
32
+ code: "RTGL-CONTRACT-002",
33
+ severity: "error",
34
+ filePath: viewPath,
35
+ line: model.view.topLevelKeyLines?.get(forbiddenKey),
36
+ message: `${model.componentKey}: '${forbiddenKey}' is not allowed in .view.yaml. Move API metadata to .schema.yaml.`,
37
+ });
38
+ }
39
+ });
40
+ }
41
+
42
+ if (model?.view?.hasLegacyDotPropBinding) {
43
+ diagnostics.push({
44
+ code: "RTGL-CONTRACT-003",
45
+ severity: "error",
46
+ filePath: viewPath,
47
+ line: model.view.legacyDotPropBindingLine,
48
+ message: `${model.componentKey}: legacy '.prop=' binding is not supported. Use ':prop=' in .view.yaml.`,
49
+ fix: {
50
+ kind: "line-regex-replace",
51
+ filePath: viewPath,
52
+ line: model.view.legacyDotPropBindingLine,
53
+ pattern: "\\.([a-zA-Z][\\w-]*)\\s*=",
54
+ replacement: ":$1=",
55
+ flags: "g",
56
+ description: "Convert legacy '.prop=' binding to ':prop='.",
57
+ safe: true,
58
+ confidence: 0.98,
59
+ },
60
+ });
61
+ }
62
+ });
63
+
64
+ return diagnostics;
65
+ };
@@ -0,0 +1,39 @@
1
+ import { runConstantsRules } from "./constants.js";
2
+ import { runCompatibilityRules } from "./compatibility.js";
3
+ import { runCrossFileSymbolRules } from "./crossFileSymbols.js";
4
+ import { runExpressionRules } from "./expression.js";
5
+ import { runFeParityRules } from "./feParity.js";
6
+ import { runJemplRules } from "./jempl.js";
7
+ import { runLifecycleRules } from "./lifecycle.js";
8
+ import { runListenerConfigRules } from "./listenerConfig.js";
9
+ import { runMethodRules } from "./methods.js";
10
+ import { runSchemaRules } from "./schema.js";
11
+ import { runYahtmlAttrRules } from "./yahtmlAttrs.js";
12
+
13
+ export const runRules = ({
14
+ models = [],
15
+ registry = new Map(),
16
+ includeYahtml = true,
17
+ includeExpression = false,
18
+ }) => {
19
+ const diagnostics = [];
20
+
21
+ diagnostics.push(...runFeParityRules({ models }));
22
+ diagnostics.push(...runSchemaRules({ models }));
23
+ diagnostics.push(...runConstantsRules({ models }));
24
+ diagnostics.push(...runJemplRules({ models }));
25
+ diagnostics.push(...runListenerConfigRules({ models }));
26
+ diagnostics.push(...runCrossFileSymbolRules({ models }));
27
+ diagnostics.push(...runMethodRules({ models }));
28
+ diagnostics.push(...runLifecycleRules({ models }));
29
+ if (includeExpression) {
30
+ diagnostics.push(...runExpressionRules({ models }));
31
+ }
32
+ diagnostics.push(...runCompatibilityRules({ models, registry }));
33
+
34
+ if (includeYahtml) {
35
+ diagnostics.push(...runYahtmlAttrRules({ models, registry }));
36
+ }
37
+
38
+ return diagnostics;
39
+ };
@@ -0,0 +1,80 @@
1
+ import { normalizeJemplErrorMessage, parseJemplForCompiler } from "../core/parsers.js";
2
+ import { resolveListenerLine } from "./listenerSymbols.js";
3
+ import { getModelFilePath, isObjectRecord } from "./shared.js";
4
+
5
+ export const runJemplRules = ({ models = [] }) => {
6
+ const diagnostics = [];
7
+
8
+ models.forEach((model) => {
9
+ const viewPath = getModelFilePath({ model, fileType: "view" });
10
+ const viewYaml = model?.view?.yaml;
11
+
12
+ if (!isObjectRecord(viewYaml)) {
13
+ return;
14
+ }
15
+
16
+ if (Object.prototype.hasOwnProperty.call(viewYaml, "template")) {
17
+ const templateLine = model?.view?.topLevelKeyLines?.get("template");
18
+ const templateParse = parseJemplForCompiler({
19
+ source: viewYaml.template,
20
+ viewText: model?.view?.text || "",
21
+ fallbackLine: templateLine,
22
+ strictControlDirectives: true,
23
+ });
24
+
25
+ if (templateParse.parseError) {
26
+ diagnostics.push({
27
+ code: "RTGL-CHECK-JEMPL-001",
28
+ severity: "error",
29
+ filePath: viewPath,
30
+ line: templateParse.parseError.line,
31
+ message: `${model.componentKey}: invalid Jempl in view template. ${templateParse.parseError.message}`,
32
+ });
33
+ }
34
+
35
+ templateParse.controlDiagnostics.forEach((controlDiagnostic) => {
36
+ diagnostics.push({
37
+ code: "RTGL-CHECK-JEMPL-003",
38
+ severity: "error",
39
+ filePath: viewPath,
40
+ line: controlDiagnostic.line,
41
+ message: `${model.componentKey}: ${normalizeJemplErrorMessage({
42
+ message: controlDiagnostic.message,
43
+ }, "Invalid Jempl control directive")}`,
44
+ });
45
+ });
46
+ }
47
+
48
+ model.view.refListeners.forEach(({ refKey, eventType, eventConfig, line, optionLines }) => {
49
+ if (!isObjectRecord(eventConfig)) {
50
+ return;
51
+ }
52
+
53
+ if (!Object.prototype.hasOwnProperty.call(eventConfig, "payload")) {
54
+ return;
55
+ }
56
+
57
+ const payloadLine = resolveListenerLine({
58
+ listenerLine: line,
59
+ optionLines,
60
+ preferredKeys: ["payload"],
61
+ });
62
+ const payloadParse = parseJemplForCompiler({
63
+ source: eventConfig.payload,
64
+ fallbackLine: payloadLine,
65
+ });
66
+
67
+ if (payloadParse.parseError) {
68
+ diagnostics.push({
69
+ code: "RTGL-CHECK-JEMPL-002",
70
+ severity: "error",
71
+ filePath: viewPath,
72
+ line: payloadParse.parseError.line,
73
+ message: `${model.componentKey}: invalid Jempl in listener payload for event '${eventType}' on ref '${refKey}'. ${payloadParse.parseError.message}`,
74
+ });
75
+ }
76
+ });
77
+ });
78
+
79
+ return diagnostics;
80
+ };
@@ -0,0 +1,4 @@
1
+ export const runLifecycleRules = ({ models = [] } = {}) => {
2
+ void models;
3
+ return [];
4
+ };