@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,1400 @@
1
+ import { JEMPL_BINARY_OP, JEMPL_NODE, JEMPL_UNARY_OP, parseJemplForCompiler } from "./parsers.js";
2
+ import { toCamelCase, toKebabCase } from "../utils/case.js";
3
+ import { collectKnownExpressionRoots } from "./semantic.js";
4
+
5
+ const CONTROL_PREFIXES = ["$if", "$elif", "$else", "$for"];
6
+
7
+ const RESERVED_WORDS = new Set([
8
+ "if",
9
+ "else",
10
+ "for",
11
+ "while",
12
+ "switch",
13
+ "case",
14
+ "break",
15
+ "continue",
16
+ "return",
17
+ "throw",
18
+ "try",
19
+ "catch",
20
+ "finally",
21
+ "new",
22
+ "typeof",
23
+ "instanceof",
24
+ "in",
25
+ "void",
26
+ "delete",
27
+ "await",
28
+ "async",
29
+ "true",
30
+ "false",
31
+ "null",
32
+ "undefined",
33
+ ]);
34
+
35
+ const stripStringLiterals = (source = "") => {
36
+ return source.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/gu, " ");
37
+ };
38
+
39
+ const extractExpressionRootIdentifiersRegexFallback = (expression = "") => {
40
+ const source = stripStringLiterals(String(expression));
41
+ const roots = new Set();
42
+ const regex = /(?:^|[^.\w$])([A-Za-z_$][A-Za-z0-9_$]*)/g;
43
+ let match = regex.exec(source);
44
+
45
+ while (match) {
46
+ const candidate = match[1];
47
+ if (!candidate || RESERVED_WORDS.has(candidate)) {
48
+ match = regex.exec(source);
49
+ continue;
50
+ }
51
+ roots.add(candidate);
52
+ match = regex.exec(source);
53
+ }
54
+
55
+ return [...roots];
56
+ };
57
+
58
+ const toPathRootIdentifier = (pathExpression = "") => {
59
+ const match = String(pathExpression || "").trim().match(/^([A-Za-z_$][A-Za-z0-9_$]*)/u);
60
+ if (!match) {
61
+ return null;
62
+ }
63
+ return match[1];
64
+ };
65
+
66
+ const extractExpressionRootIdentifiersFromAst = (expressionAst) => {
67
+ const roots = new Set();
68
+
69
+ const visit = (node) => {
70
+ if (Array.isArray(node)) {
71
+ node.forEach((item) => visit(item));
72
+ return;
73
+ }
74
+ if (!node || typeof node !== "object") {
75
+ return;
76
+ }
77
+
78
+ if (node.type === JEMPL_NODE.PATH && typeof node.path === "string") {
79
+ const root = toPathRootIdentifier(node.path);
80
+ if (root && !RESERVED_WORDS.has(root)) {
81
+ roots.add(root);
82
+ }
83
+ }
84
+
85
+ Object.values(node).forEach((value) => visit(value));
86
+ };
87
+
88
+ visit(expressionAst);
89
+ return [...roots];
90
+ };
91
+
92
+ const createLineOffsets = (source = "") => {
93
+ const offsets = [0];
94
+ for (let index = 0; index < source.length; index += 1) {
95
+ if (source[index] === "\n") {
96
+ offsets.push(index + 1);
97
+ }
98
+ }
99
+ return offsets;
100
+ };
101
+
102
+ const toRangeWithLength = (range = {}) => {
103
+ const offset = Number.isInteger(range.offset) ? range.offset : undefined;
104
+ const endOffset = Number.isInteger(range.endOffset) ? range.endOffset : undefined;
105
+ const length = (
106
+ Number.isInteger(offset)
107
+ && Number.isInteger(endOffset)
108
+ && endOffset >= offset
109
+ )
110
+ ? endOffset - offset
111
+ : undefined;
112
+
113
+ return {
114
+ line: Number.isInteger(range.line) ? range.line : undefined,
115
+ column: Number.isInteger(range.column) ? range.column : undefined,
116
+ endLine: Number.isInteger(range.endLine) ? range.endLine : undefined,
117
+ endColumn: Number.isInteger(range.endColumn) ? range.endColumn : undefined,
118
+ offset,
119
+ endOffset,
120
+ length,
121
+ };
122
+ };
123
+
124
+ const offsetToLineColumn = ({ lineOffsets = [], offset = 0 }) => {
125
+ if (!Array.isArray(lineOffsets) || lineOffsets.length === 0) {
126
+ return { line: 1, column: 1 };
127
+ }
128
+
129
+ let low = 0;
130
+ let high = lineOffsets.length - 1;
131
+ while (low <= high) {
132
+ const mid = Math.floor((low + high) / 2);
133
+ const start = lineOffsets[mid];
134
+ const nextStart = lineOffsets[mid + 1] ?? Number.POSITIVE_INFINITY;
135
+ if (offset >= start && offset < nextStart) {
136
+ return {
137
+ line: mid + 1,
138
+ column: offset - start + 1,
139
+ };
140
+ }
141
+ if (offset < start) {
142
+ high = mid - 1;
143
+ } else {
144
+ low = mid + 1;
145
+ }
146
+ }
147
+
148
+ const lastLine = Math.max(1, lineOffsets.length);
149
+ const lastStart = lineOffsets[lastLine - 1] || 0;
150
+ return {
151
+ line: lastLine,
152
+ column: Math.max(1, offset - lastStart + 1),
153
+ };
154
+ };
155
+
156
+ const escapeRegexChar = (char = "") => {
157
+ return char.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
158
+ };
159
+
160
+ const buildWhitespaceFlexiblePattern = (needle = "") => {
161
+ const trimmedNeedle = String(needle || "").trim();
162
+ if (!trimmedNeedle || !/\s/u.test(trimmedNeedle)) {
163
+ return null;
164
+ }
165
+
166
+ let pattern = "";
167
+ let whitespacePending = false;
168
+ for (const char of trimmedNeedle) {
169
+ if (/\s/u.test(char)) {
170
+ whitespacePending = true;
171
+ continue;
172
+ }
173
+ if (whitespacePending) {
174
+ pattern += "\\s+";
175
+ whitespacePending = false;
176
+ }
177
+ pattern += escapeRegexChar(char);
178
+ }
179
+
180
+ return pattern || null;
181
+ };
182
+
183
+ const createRangeLocator = (viewText = "") => {
184
+ const lineOffsets = createLineOffsets(viewText);
185
+ const cursorByNeedle = new Map();
186
+ const findLineAtOffset = (offset = 0) => offsetToLineColumn({ lineOffsets, offset }).line;
187
+
188
+ const toRangeFromOffsets = ({ startOffset = 0, endOffset = 0 } = {}) => {
189
+ const safeStartOffset = Math.max(0, Number(startOffset) || 0);
190
+ const safeEndOffset = Math.max(safeStartOffset + 1, Number(endOffset) || safeStartOffset + 1);
191
+ const start = offsetToLineColumn({ lineOffsets, offset: safeStartOffset });
192
+ const end = offsetToLineColumn({ lineOffsets, offset: safeEndOffset - 1 });
193
+
194
+ return {
195
+ line: start.line,
196
+ column: start.column,
197
+ endLine: end.line,
198
+ endColumn: end.column,
199
+ offset: safeStartOffset,
200
+ endOffset: safeEndOffset,
201
+ length: safeEndOffset - safeStartOffset,
202
+ };
203
+ };
204
+
205
+ const findNeedleExact = ({ needle, fromIndex = 0, preferredLine } = {}) => {
206
+ if (!needle) {
207
+ return null;
208
+ }
209
+
210
+ let bestFound = null;
211
+ let bestDistance = Number.POSITIVE_INFINITY;
212
+ let cursor = Math.max(0, fromIndex);
213
+ while (cursor <= viewText.length) {
214
+ const foundIndex = viewText.indexOf(needle, cursor);
215
+ if (foundIndex === -1) {
216
+ break;
217
+ }
218
+ const foundMatch = {
219
+ startOffset: foundIndex,
220
+ endOffset: foundIndex + needle.length,
221
+ };
222
+ if (!Number.isInteger(preferredLine)) {
223
+ return foundMatch;
224
+ }
225
+ const foundLine = findLineAtOffset(foundIndex);
226
+ if (foundLine === preferredLine) {
227
+ return foundMatch;
228
+ }
229
+ const distance = Math.abs(foundLine - preferredLine);
230
+ if (
231
+ distance < bestDistance
232
+ || (
233
+ distance === bestDistance
234
+ && foundLine > (bestFound?.line || 0)
235
+ )
236
+ ) {
237
+ bestFound = {
238
+ ...foundMatch,
239
+ line: foundLine,
240
+ };
241
+ bestDistance = distance;
242
+ }
243
+ cursor = foundIndex + 1;
244
+ }
245
+
246
+ return bestFound;
247
+ };
248
+
249
+ const findNeedleFlexible = ({ needle, fromIndex = 0, preferredLine } = {}) => {
250
+ const pattern = buildWhitespaceFlexiblePattern(needle);
251
+ if (!pattern) {
252
+ return null;
253
+ }
254
+
255
+ const regex = new RegExp(pattern, "gu");
256
+ regex.lastIndex = Math.max(0, fromIndex);
257
+ let bestFound = null;
258
+ let bestDistance = Number.POSITIVE_INFINITY;
259
+ let match = regex.exec(viewText);
260
+ while (match) {
261
+ const foundLine = findLineAtOffset(match.index);
262
+ const foundMatch = {
263
+ startOffset: match.index,
264
+ endOffset: match.index + match[0].length,
265
+ };
266
+ if (!Number.isInteger(preferredLine) || foundLine === preferredLine) {
267
+ return foundMatch;
268
+ }
269
+ const distance = Math.abs(foundLine - preferredLine);
270
+ if (
271
+ distance < bestDistance
272
+ || (
273
+ distance === bestDistance
274
+ && foundLine > (bestFound?.line || 0)
275
+ )
276
+ ) {
277
+ bestFound = {
278
+ ...foundMatch,
279
+ line: foundLine,
280
+ };
281
+ bestDistance = distance;
282
+ }
283
+ match = regex.exec(viewText);
284
+ }
285
+
286
+ return bestFound;
287
+ };
288
+
289
+ const locate = ({ expression, preferredLine } = {}) => {
290
+ if (!expression) {
291
+ return {};
292
+ }
293
+ const normalizedExpression = String(expression).trim();
294
+ if (!normalizedExpression) {
295
+ return {};
296
+ }
297
+
298
+ const needles = [
299
+ {
300
+ needle: `\${${normalizedExpression}}`,
301
+ expressionStartOffset: 2,
302
+ expressionEndTrim: 1,
303
+ },
304
+ {
305
+ needle: `#{${normalizedExpression}}`,
306
+ expressionStartOffset: 2,
307
+ expressionEndTrim: 1,
308
+ },
309
+ {
310
+ needle: `{{${normalizedExpression}}}`,
311
+ expressionStartOffset: 2,
312
+ expressionEndTrim: 2,
313
+ },
314
+ {
315
+ needle: normalizedExpression,
316
+ expressionStartOffset: 0,
317
+ expressionEndTrim: 0,
318
+ },
319
+ ];
320
+
321
+ for (let index = 0; index < needles.length; index += 1) {
322
+ const needleEntry = needles[index];
323
+ const needle = needleEntry.needle;
324
+ const cursor = cursorByNeedle.get(needle) || 0;
325
+ let foundMatch = findNeedleExact({ needle, fromIndex: cursor, preferredLine });
326
+ if (!foundMatch) {
327
+ foundMatch = findNeedleFlexible({ needle, fromIndex: cursor, preferredLine });
328
+ }
329
+ if (!foundMatch) {
330
+ foundMatch = findNeedleExact({ needle, preferredLine });
331
+ }
332
+ if (!foundMatch) {
333
+ foundMatch = findNeedleFlexible({ needle, preferredLine });
334
+ }
335
+ if (!foundMatch) {
336
+ continue;
337
+ }
338
+
339
+ cursorByNeedle.set(needle, foundMatch.endOffset);
340
+ return toRangeFromOffsets({
341
+ startOffset: foundMatch.startOffset + needleEntry.expressionStartOffset,
342
+ endOffset: Math.max(
343
+ foundMatch.startOffset + needleEntry.expressionStartOffset + 1,
344
+ foundMatch.endOffset - needleEntry.expressionEndTrim,
345
+ ),
346
+ });
347
+ }
348
+
349
+ return {
350
+ line: Number.isInteger(preferredLine) ? preferredLine : undefined,
351
+ };
352
+ };
353
+
354
+ const sliceRange = (range = {}) => {
355
+ const normalizedRange = toRangeWithLength(range);
356
+ if (!Number.isInteger(normalizedRange.offset) || !Number.isInteger(normalizedRange.endOffset)) {
357
+ return "";
358
+ }
359
+ return viewText.slice(normalizedRange.offset, normalizedRange.endOffset);
360
+ };
361
+
362
+ return { locate, toRangeFromOffsets, sliceRange };
363
+ };
364
+
365
+ const isControlKey = (key = "") => {
366
+ return CONTROL_PREFIXES.some((prefix) => key.startsWith(prefix));
367
+ };
368
+
369
+ const contextFromAttrSourceType = (sourceType = "") => {
370
+ if (sourceType === "boolean-attr") return "attr-boolean";
371
+ if (sourceType === "prop") return "attr-prop";
372
+ if (sourceType === "event") return "attr-event";
373
+ return "attr";
374
+ };
375
+
376
+ const collectSchemaRootMap = (model) => {
377
+ const map = new Map();
378
+ const normalizedProps = model?.schema?.normalized?.props;
379
+ if (normalizedProps?.aliasToCanonical instanceof Map && normalizedProps?.byName instanceof Map) {
380
+ normalizedProps.aliasToCanonical.forEach((canonicalName, alias) => {
381
+ const schema = normalizedProps.byName.get(canonicalName);
382
+ if (!schema || !alias || map.has(alias)) {
383
+ return;
384
+ }
385
+ map.set(alias, {
386
+ schema,
387
+ canonicalName,
388
+ });
389
+ });
390
+ if (map.size > 0) {
391
+ return map;
392
+ }
393
+ }
394
+
395
+ const properties = model?.schema?.yaml?.propsSchema?.properties;
396
+ if (properties && typeof properties === "object" && !Array.isArray(properties)) {
397
+ Object.entries(properties).forEach(([propName, schema]) => {
398
+ if (!propName) {
399
+ return;
400
+ }
401
+ [propName, toCamelCase(propName), toKebabCase(propName)].forEach((alias) => {
402
+ if (!map.has(alias)) {
403
+ map.set(alias, {
404
+ schema,
405
+ canonicalName: propName,
406
+ });
407
+ }
408
+ });
409
+ });
410
+ }
411
+
412
+ return map;
413
+ };
414
+
415
+ const collectLocalSymbols = (scopeStack = []) => {
416
+ const symbols = new Set();
417
+ scopeStack.forEach((scope) => {
418
+ if (!(scope?.symbols instanceof Set)) {
419
+ return;
420
+ }
421
+ scope.symbols.forEach((symbol) => symbols.add(symbol));
422
+ });
423
+ return symbols;
424
+ };
425
+
426
+ const collectLocalSchemaTypes = (scopeStack = []) => {
427
+ const symbolTypes = new Map();
428
+ scopeStack.forEach((scope) => {
429
+ if (!(scope?.symbolTypes instanceof Map)) {
430
+ return;
431
+ }
432
+ scope.symbolTypes.forEach((schemaType, symbolName) => {
433
+ symbolTypes.set(symbolName, schemaType);
434
+ });
435
+ });
436
+ return symbolTypes;
437
+ };
438
+
439
+ const JEMPL_BINARY_OPERATOR_SYMBOL_BY_OP = new Map([
440
+ [JEMPL_BINARY_OP.EQ, "=="],
441
+ [JEMPL_BINARY_OP.NEQ, "!="],
442
+ [JEMPL_BINARY_OP.GT, ">"],
443
+ [JEMPL_BINARY_OP.LT, "<"],
444
+ [JEMPL_BINARY_OP.GTE, ">="],
445
+ [JEMPL_BINARY_OP.LTE, "<="],
446
+ [JEMPL_BINARY_OP.AND, "&&"],
447
+ [JEMPL_BINARY_OP.OR, "||"],
448
+ [JEMPL_BINARY_OP.IN, "in"],
449
+ [JEMPL_BINARY_OP.ADD, "+"],
450
+ [JEMPL_BINARY_OP.SUBTRACT, "-"],
451
+ ]);
452
+
453
+ const JEMPL_UNARY_OPERATOR_SYMBOL_BY_OP = new Map([
454
+ [JEMPL_UNARY_OP.NOT, "!"],
455
+ ]);
456
+
457
+ const stringifyJemplExpression = (node) => {
458
+ if (!node || typeof node !== "object") {
459
+ return null;
460
+ }
461
+
462
+ if (node.type === JEMPL_NODE.PATH && typeof node.path === "string") {
463
+ return node.path;
464
+ }
465
+
466
+ if (node.type === JEMPL_NODE.LITERAL) {
467
+ const value = node.value;
468
+ if (typeof value === "string") {
469
+ return JSON.stringify(value);
470
+ }
471
+ if (typeof value === "number" || typeof value === "boolean") {
472
+ return String(value);
473
+ }
474
+ if (value === null) {
475
+ return "null";
476
+ }
477
+ return null;
478
+ }
479
+
480
+ if (node.type === JEMPL_NODE.UNARY) {
481
+ const operator = JEMPL_UNARY_OPERATOR_SYMBOL_BY_OP.get(node.op);
482
+ const operand = stringifyJemplExpression(node.operand);
483
+ if (!operator || !operand) {
484
+ return null;
485
+ }
486
+ return `${operator}${operand}`;
487
+ }
488
+
489
+ if (node.type === JEMPL_NODE.BINARY) {
490
+ const operator = JEMPL_BINARY_OPERATOR_SYMBOL_BY_OP.get(node.op);
491
+ const left = stringifyJemplExpression(node.left);
492
+ const right = stringifyJemplExpression(node.right);
493
+ if (!operator || !left || !right) {
494
+ return null;
495
+ }
496
+ return `${left} ${operator} ${right}`;
497
+ }
498
+
499
+ return null;
500
+ };
501
+
502
+ const trimSource = (source = "") => {
503
+ const raw = String(source || "");
504
+ const leadingTrim = raw.length - raw.trimStart().length;
505
+ const trailingTrim = raw.length - raw.trimEnd().length;
506
+ const trimmed = raw.trim();
507
+ return {
508
+ raw,
509
+ trimmed,
510
+ leadingTrim,
511
+ trailingTrim,
512
+ };
513
+ };
514
+
515
+ const isIdentifierChar = (char = "") => /[A-Za-z0-9_$]/u.test(char);
516
+
517
+ const isWordBoundary = (char = "") => !isIdentifierChar(char);
518
+
519
+ const isEscapedAt = (source = "", index = 0) => {
520
+ let escapeCount = 0;
521
+ for (let cursor = index - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) {
522
+ escapeCount += 1;
523
+ }
524
+ return escapeCount % 2 === 1;
525
+ };
526
+
527
+ const matchesTopLevelOperatorAt = ({
528
+ source = "",
529
+ index = 0,
530
+ operator = "",
531
+ }) => {
532
+ if (!operator || !source.startsWith(operator, index)) {
533
+ return false;
534
+ }
535
+
536
+ if (operator === "in") {
537
+ const previousChar = source[index - 1] || "";
538
+ const nextChar = source[index + operator.length] || "";
539
+ return isWordBoundary(previousChar) && isWordBoundary(nextChar);
540
+ }
541
+
542
+ if (operator === ">" && source[index + 1] === "=") {
543
+ return false;
544
+ }
545
+ if (operator === "<" && source[index + 1] === "=") {
546
+ return false;
547
+ }
548
+
549
+ return true;
550
+ };
551
+
552
+ const findTopLevelBinaryOperatorIndex = ({
553
+ source = "",
554
+ operator = "",
555
+ }) => {
556
+ if (!source || !operator) {
557
+ return -1;
558
+ }
559
+
560
+ let quote = null;
561
+ let parenDepth = 0;
562
+ let bracketDepth = 0;
563
+ let braceDepth = 0;
564
+ for (let index = 0; index <= source.length - operator.length; index += 1) {
565
+ const char = source[index];
566
+
567
+ if (quote) {
568
+ if (char === quote && !isEscapedAt(source, index)) {
569
+ quote = null;
570
+ }
571
+ continue;
572
+ }
573
+
574
+ if ((char === "\"" || char === "'" || char === "`") && !isEscapedAt(source, index)) {
575
+ quote = char;
576
+ continue;
577
+ }
578
+
579
+ if (char === "(") {
580
+ parenDepth += 1;
581
+ continue;
582
+ }
583
+ if (char === ")" && parenDepth > 0) {
584
+ parenDepth -= 1;
585
+ continue;
586
+ }
587
+ if (char === "[") {
588
+ bracketDepth += 1;
589
+ continue;
590
+ }
591
+ if (char === "]" && bracketDepth > 0) {
592
+ bracketDepth -= 1;
593
+ continue;
594
+ }
595
+ if (char === "{") {
596
+ braceDepth += 1;
597
+ continue;
598
+ }
599
+ if (char === "}" && braceDepth > 0) {
600
+ braceDepth -= 1;
601
+ continue;
602
+ }
603
+
604
+ if (parenDepth !== 0 || bracketDepth !== 0 || braceDepth !== 0) {
605
+ continue;
606
+ }
607
+
608
+ if (!matchesTopLevelOperatorAt({ source, index, operator })) {
609
+ continue;
610
+ }
611
+
612
+ const left = source.slice(0, index).trim();
613
+ const right = source.slice(index + operator.length).trim();
614
+ if (!left || !right) {
615
+ continue;
616
+ }
617
+
618
+ return index;
619
+ }
620
+
621
+ return -1;
622
+ };
623
+
624
+ const toRangeFromAbsoluteOffsets = ({
625
+ rangeLocator,
626
+ startOffset,
627
+ endOffset,
628
+ }) => {
629
+ if (!rangeLocator || typeof rangeLocator.toRangeFromOffsets !== "function") {
630
+ return toRangeWithLength({
631
+ offset: startOffset,
632
+ endOffset,
633
+ });
634
+ }
635
+ return toRangeWithLength(
636
+ rangeLocator.toRangeFromOffsets({
637
+ startOffset,
638
+ endOffset,
639
+ }),
640
+ );
641
+ };
642
+
643
+ const annotateJemplExpressionNodeRanges = ({
644
+ node,
645
+ source,
646
+ baseOffset,
647
+ rangeLocator,
648
+ }) => {
649
+ if (!node || typeof node !== "object") {
650
+ return;
651
+ }
652
+
653
+ if (!Number.isInteger(baseOffset)) {
654
+ return;
655
+ }
656
+
657
+ const sourceSlice = trimSource(source);
658
+ const nodeStartOffset = baseOffset + sourceSlice.leadingTrim;
659
+ const nodeEndOffset = Math.max(
660
+ nodeStartOffset + 1,
661
+ nodeStartOffset + Math.max(sourceSlice.trimmed.length, 0),
662
+ );
663
+ node.range = toRangeFromAbsoluteOffsets({
664
+ rangeLocator,
665
+ startOffset: nodeStartOffset,
666
+ endOffset: nodeEndOffset,
667
+ });
668
+
669
+ if (!sourceSlice.trimmed) {
670
+ return;
671
+ }
672
+
673
+ if (node.type === JEMPL_NODE.UNARY) {
674
+ const unaryOperator = JEMPL_UNARY_OPERATOR_SYMBOL_BY_OP.get(node.op);
675
+ if (!unaryOperator) {
676
+ return;
677
+ }
678
+ let operandStartIndex = 0;
679
+ if (sourceSlice.trimmed.startsWith(unaryOperator)) {
680
+ operandStartIndex = unaryOperator.length;
681
+ } else {
682
+ const operatorIndex = sourceSlice.trimmed.indexOf(unaryOperator);
683
+ if (operatorIndex !== -1) {
684
+ operandStartIndex = operatorIndex + unaryOperator.length;
685
+ }
686
+ }
687
+ const operandSourceRaw = sourceSlice.trimmed.slice(operandStartIndex);
688
+ const operandSlice = trimSource(operandSourceRaw);
689
+ annotateJemplExpressionNodeRanges({
690
+ node: node.operand,
691
+ source: operandSlice.trimmed,
692
+ baseOffset: nodeStartOffset + operandStartIndex + operandSlice.leadingTrim,
693
+ rangeLocator,
694
+ });
695
+ return;
696
+ }
697
+
698
+ if (node.type === JEMPL_NODE.BINARY) {
699
+ const binaryOperator = JEMPL_BINARY_OPERATOR_SYMBOL_BY_OP.get(node.op);
700
+ if (!binaryOperator) {
701
+ return;
702
+ }
703
+
704
+ let operatorIndex = findTopLevelBinaryOperatorIndex({
705
+ source: sourceSlice.trimmed,
706
+ operator: binaryOperator,
707
+ });
708
+ if (operatorIndex === -1) {
709
+ const leftExpression = stringifyJemplExpression(node.left);
710
+ if (leftExpression) {
711
+ operatorIndex = sourceSlice.trimmed.indexOf(leftExpression) + leftExpression.length;
712
+ }
713
+ }
714
+ if (operatorIndex === -1) {
715
+ return;
716
+ }
717
+
718
+ const leftSourceRaw = sourceSlice.trimmed.slice(0, operatorIndex);
719
+ const rightSourceRaw = sourceSlice.trimmed.slice(operatorIndex + binaryOperator.length);
720
+ const leftSlice = trimSource(leftSourceRaw);
721
+ const rightSlice = trimSource(rightSourceRaw);
722
+
723
+ annotateJemplExpressionNodeRanges({
724
+ node: node.left,
725
+ source: leftSlice.trimmed,
726
+ baseOffset: nodeStartOffset + leftSlice.leadingTrim,
727
+ rangeLocator,
728
+ });
729
+ annotateJemplExpressionNodeRanges({
730
+ node: node.right,
731
+ source: rightSlice.trimmed,
732
+ baseOffset: nodeStartOffset + operatorIndex + binaryOperator.length + rightSlice.leadingTrim,
733
+ rangeLocator,
734
+ });
735
+ }
736
+ };
737
+
738
+ const annotateExpressionAstRanges = ({
739
+ expressionAst,
740
+ expression,
741
+ range,
742
+ rangeLocator,
743
+ }) => {
744
+ if (!expressionAst || typeof expressionAst !== "object") {
745
+ return expressionAst;
746
+ }
747
+
748
+ const normalizedRange = toRangeWithLength(range);
749
+ if (!Number.isInteger(normalizedRange.offset) || !Number.isInteger(normalizedRange.endOffset)) {
750
+ if (!expressionAst.range) {
751
+ expressionAst.range = normalizedRange;
752
+ }
753
+ return expressionAst;
754
+ }
755
+
756
+ const sourceFromRange = typeof rangeLocator?.sliceRange === "function"
757
+ ? rangeLocator.sliceRange(normalizedRange)
758
+ : "";
759
+ const source = sourceFromRange || (
760
+ typeof expression === "string"
761
+ && expression.trim()
762
+ ? expression
763
+ : ""
764
+ );
765
+
766
+ annotateJemplExpressionNodeRanges({
767
+ node: expressionAst,
768
+ source: source || "",
769
+ baseOffset: normalizedRange.offset,
770
+ rangeLocator,
771
+ });
772
+
773
+ if (!expressionAst.range) {
774
+ expressionAst.range = normalizedRange;
775
+ }
776
+
777
+ return expressionAst;
778
+ };
779
+
780
+ const pushReference = ({
781
+ references,
782
+ expression,
783
+ context,
784
+ source,
785
+ localSymbols,
786
+ localSchemaTypes,
787
+ expressionAst,
788
+ parseExpressionAst,
789
+ rangeLocator,
790
+ range = {},
791
+ }) => {
792
+ if (!expression || typeof expression !== "string") {
793
+ return;
794
+ }
795
+ const normalizedRange = toRangeWithLength(range);
796
+ const resolvedExpressionAst = (
797
+ expressionAst && typeof expressionAst === "object"
798
+ )
799
+ ? expressionAst
800
+ : (
801
+ typeof parseExpressionAst === "function"
802
+ ? parseExpressionAst(expression)
803
+ : undefined
804
+ );
805
+ const clonedExpressionAst = resolvedExpressionAst && typeof resolvedExpressionAst === "object"
806
+ ? JSON.parse(JSON.stringify(resolvedExpressionAst))
807
+ : undefined;
808
+ if (clonedExpressionAst) {
809
+ annotateExpressionAstRanges({
810
+ expressionAst: clonedExpressionAst,
811
+ expression,
812
+ range: normalizedRange,
813
+ rangeLocator,
814
+ });
815
+ }
816
+
817
+ const roots = (() => {
818
+ const astRoots = extractExpressionRootIdentifiersFromAst(resolvedExpressionAst);
819
+ if (astRoots.length > 0) {
820
+ return astRoots;
821
+ }
822
+ return extractExpressionRootIdentifiersRegexFallback(expression);
823
+ })();
824
+
825
+ references.push({
826
+ expression,
827
+ roots,
828
+ context,
829
+ source,
830
+ localSymbols: new Set(localSymbols || []),
831
+ localSchemaTypes: localSchemaTypes instanceof Map
832
+ ? new Map(localSchemaTypes)
833
+ : new Map(localSchemaTypes || []),
834
+ expressionAst: clonedExpressionAst,
835
+ range: normalizedRange,
836
+ line: normalizedRange.line,
837
+ column: normalizedRange.column,
838
+ endLine: normalizedRange.endLine,
839
+ endColumn: normalizedRange.endColumn,
840
+ offset: normalizedRange.offset,
841
+ endOffset: normalizedRange.endOffset,
842
+ length: normalizedRange.length,
843
+ });
844
+ };
845
+
846
+ const attachAttrExpressionReferences = ({
847
+ model,
848
+ references,
849
+ localBindingContextByElementOccurrence,
850
+ parseExpressionAst,
851
+ rangeLocator,
852
+ }) => {
853
+ const nodes = Array.isArray(model?.view?.templateAst?.nodes) ? model.view.templateAst.nodes : [];
854
+
855
+ nodes.forEach((node) => {
856
+ const occurrenceKey = `${node?.range?.line || 0}::${node?.rawKey || ""}`;
857
+ const localBindingContext = localBindingContextByElementOccurrence.get(occurrenceKey) || {};
858
+ const localSymbols = localBindingContext.localSymbols || [];
859
+ const localSchemaTypes = localBindingContext.localSchemaTypes || [];
860
+ const attributes = Array.isArray(node?.attributes) ? node.attributes : [];
861
+ attributes.forEach((attribute) => {
862
+ if (String(attribute?.sourceType || "") === "event") {
863
+ return;
864
+ }
865
+ const expressionNodes = Array.isArray(attribute?.expressionNodes) ? attribute.expressionNodes : [];
866
+ if (expressionNodes.length > 0) {
867
+ expressionNodes.forEach((expressionNode) => {
868
+ pushReference({
869
+ references,
870
+ expression: expressionNode?.expression,
871
+ context: contextFromAttrSourceType(String(attribute?.sourceType || "")),
872
+ source: "template-attr",
873
+ localSymbols,
874
+ localSchemaTypes,
875
+ parseExpressionAst,
876
+ rangeLocator,
877
+ range: expressionNode?.range || attribute?.range || node?.range || {},
878
+ });
879
+ });
880
+ return;
881
+ }
882
+
883
+ const expressions = Array.isArray(attribute?.expressions) ? attribute.expressions : [];
884
+ expressions.forEach((expression) => {
885
+ pushReference({
886
+ references,
887
+ expression,
888
+ context: contextFromAttrSourceType(String(attribute?.sourceType || "")),
889
+ source: "template-attr",
890
+ localSymbols,
891
+ localSchemaTypes,
892
+ parseExpressionAst,
893
+ rangeLocator,
894
+ range: attribute?.range || node?.range || {},
895
+ });
896
+ });
897
+ });
898
+ });
899
+ };
900
+
901
+ const visitJemplNode = ({
902
+ model,
903
+ node,
904
+ scopeStack,
905
+ references,
906
+ elementEntries,
907
+ rangeLocator,
908
+ parseExpressionAst,
909
+ }) => {
910
+ if (!node || typeof node !== "object") {
911
+ return;
912
+ }
913
+
914
+ if (node.type === JEMPL_NODE.PATH && typeof node.path === "string") {
915
+ pushReference({
916
+ references,
917
+ expression: node.path,
918
+ context: "template-value",
919
+ source: "jempl-path",
920
+ localSymbols: collectLocalSymbols(scopeStack),
921
+ localSchemaTypes: collectLocalSchemaTypes(scopeStack),
922
+ parseExpressionAst,
923
+ range: node?.range || rangeLocator.locate({ expression: node.path }),
924
+ });
925
+ return;
926
+ }
927
+
928
+ if (node.type === JEMPL_NODE.LOOP) {
929
+ const currentSymbols = collectLocalSymbols(scopeStack);
930
+ const currentLocalSchemaTypes = collectLocalSchemaTypes(scopeStack);
931
+ if (node?.iterable?.type === JEMPL_NODE.PATH && typeof node.iterable.path === "string") {
932
+ pushReference({
933
+ references,
934
+ expression: node.iterable.path,
935
+ context: "loop-iterable",
936
+ source: "jempl-loop",
937
+ localSymbols: currentSymbols,
938
+ localSchemaTypes: currentLocalSchemaTypes,
939
+ parseExpressionAst,
940
+ range: node?.iterable?.range || rangeLocator.locate({ expression: node.iterable.path }),
941
+ });
942
+ } else {
943
+ visitJemplNode({
944
+ model,
945
+ node: node.iterable,
946
+ scopeStack,
947
+ references,
948
+ elementEntries,
949
+ rangeLocator,
950
+ parseExpressionAst,
951
+ });
952
+ }
953
+
954
+ const loopSymbols = new Set();
955
+ const loopSymbolTypes = new Map();
956
+ const iterablePathType = (node?.iterable?.type === JEMPL_NODE.PATH && typeof node.iterable.path === "string")
957
+ ? resolveExpressionPathType({
958
+ model,
959
+ expression: node.iterable.path,
960
+ localSchemaTypes: currentLocalSchemaTypes,
961
+ })
962
+ : null;
963
+ const iterableItemSchemaType = iterablePathType?.resolved?.type === "array" ? iterablePathType.resolved.items : null;
964
+ if (typeof node.itemVar === "string" && node.itemVar) {
965
+ loopSymbols.add(node.itemVar);
966
+ if (iterableItemSchemaType && typeof iterableItemSchemaType === "object" && !Array.isArray(iterableItemSchemaType)) {
967
+ loopSymbolTypes.set(node.itemVar, iterableItemSchemaType);
968
+ }
969
+ }
970
+ if (typeof node.indexVar === "string" && node.indexVar) {
971
+ loopSymbols.add(node.indexVar);
972
+ loopSymbolTypes.set(node.indexVar, { type: "number" });
973
+ }
974
+
975
+ visitJemplNode({
976
+ model,
977
+ node: node.body,
978
+ scopeStack: [...scopeStack, { kind: "loop", symbols: loopSymbols, symbolTypes: loopSymbolTypes }],
979
+ references,
980
+ elementEntries,
981
+ rangeLocator,
982
+ parseExpressionAst,
983
+ });
984
+ return;
985
+ }
986
+
987
+ if (node.type === JEMPL_NODE.CONDITIONAL) {
988
+ const conditions = Array.isArray(node.conditions) ? node.conditions : [];
989
+ conditions.forEach((condition) => {
990
+ const conditionExpression = stringifyJemplExpression(condition);
991
+ if (conditionExpression) {
992
+ const conditionRange = rangeLocator.locate({ expression: conditionExpression });
993
+ annotateExpressionAstRanges({
994
+ expressionAst: condition,
995
+ expression: conditionExpression,
996
+ range: conditionRange,
997
+ rangeLocator,
998
+ });
999
+ pushReference({
1000
+ references,
1001
+ expression: conditionExpression,
1002
+ context: "condition",
1003
+ source: "jempl-condition",
1004
+ localSymbols: collectLocalSymbols(scopeStack),
1005
+ localSchemaTypes: collectLocalSchemaTypes(scopeStack),
1006
+ expressionAst: condition,
1007
+ parseExpressionAst,
1008
+ rangeLocator,
1009
+ range: conditionRange,
1010
+ });
1011
+ }
1012
+ visitJemplNode({
1013
+ model,
1014
+ node: condition,
1015
+ scopeStack,
1016
+ references,
1017
+ elementEntries,
1018
+ rangeLocator,
1019
+ parseExpressionAst,
1020
+ });
1021
+ });
1022
+
1023
+ const bodies = Array.isArray(node.bodies) ? node.bodies : [];
1024
+ bodies.forEach((body) => {
1025
+ visitJemplNode({
1026
+ model,
1027
+ node: body,
1028
+ scopeStack: [...scopeStack, { kind: "branch", symbols: new Set(), symbolTypes: new Map() }],
1029
+ references,
1030
+ elementEntries,
1031
+ rangeLocator,
1032
+ parseExpressionAst,
1033
+ });
1034
+ });
1035
+ return;
1036
+ }
1037
+
1038
+ if (node.type === JEMPL_NODE.OBJECT && Array.isArray(node.properties)) {
1039
+ node.properties.forEach((property) => {
1040
+ if (!property || typeof property !== "object") {
1041
+ return;
1042
+ }
1043
+
1044
+ const key = typeof property.key === "string" ? property.key.trim() : "";
1045
+ if (!key) {
1046
+ visitJemplNode({
1047
+ model,
1048
+ node: property.value,
1049
+ scopeStack,
1050
+ references,
1051
+ elementEntries,
1052
+ rangeLocator,
1053
+ parseExpressionAst,
1054
+ });
1055
+ return;
1056
+ }
1057
+
1058
+ if (key === "children" || isControlKey(key)) {
1059
+ visitJemplNode({
1060
+ model,
1061
+ node: property.value,
1062
+ scopeStack,
1063
+ references,
1064
+ elementEntries,
1065
+ rangeLocator,
1066
+ parseExpressionAst,
1067
+ });
1068
+ return;
1069
+ }
1070
+
1071
+ const locals = collectLocalSymbols(scopeStack);
1072
+ const localSchemaTypes = collectLocalSchemaTypes(scopeStack);
1073
+ elementEntries.push({
1074
+ key,
1075
+ localSymbols: [...locals],
1076
+ localSchemaTypes: [...localSchemaTypes.entries()],
1077
+ });
1078
+
1079
+ visitJemplNode({
1080
+ model,
1081
+ node: property.value,
1082
+ scopeStack,
1083
+ references,
1084
+ elementEntries,
1085
+ rangeLocator,
1086
+ parseExpressionAst,
1087
+ });
1088
+ });
1089
+ return;
1090
+ }
1091
+
1092
+ if (node.type === JEMPL_NODE.ARRAY && Array.isArray(node.items)) {
1093
+ node.items.forEach((item) => {
1094
+ visitJemplNode({
1095
+ model,
1096
+ node: item,
1097
+ scopeStack,
1098
+ references,
1099
+ elementEntries,
1100
+ rangeLocator,
1101
+ parseExpressionAst,
1102
+ });
1103
+ });
1104
+ return;
1105
+ }
1106
+
1107
+ Object.values(node).forEach((value) => {
1108
+ if (!value || typeof value !== "object") {
1109
+ return;
1110
+ }
1111
+ if (Array.isArray(value)) {
1112
+ value.forEach((item) => {
1113
+ visitJemplNode({
1114
+ model,
1115
+ node: item,
1116
+ scopeStack,
1117
+ references,
1118
+ elementEntries,
1119
+ rangeLocator,
1120
+ parseExpressionAst,
1121
+ });
1122
+ });
1123
+ return;
1124
+ }
1125
+ visitJemplNode({
1126
+ model,
1127
+ node: value,
1128
+ scopeStack,
1129
+ references,
1130
+ elementEntries,
1131
+ rangeLocator,
1132
+ parseExpressionAst,
1133
+ });
1134
+ });
1135
+ };
1136
+
1137
+ const mapElementLocalSymbolsByRawKey = ({ model, elementEntries = [] }) => {
1138
+ const nodes = Array.isArray(model?.view?.templateAst?.nodes) ? model.view.templateAst.nodes : [];
1139
+ const keyToQueues = new Map();
1140
+ elementEntries.forEach((entry) => {
1141
+ if (!keyToQueues.has(entry.key)) {
1142
+ keyToQueues.set(entry.key, []);
1143
+ }
1144
+ keyToQueues.get(entry.key).push({
1145
+ localSymbols: entry.localSymbols || [],
1146
+ localSchemaTypes: entry.localSchemaTypes || [],
1147
+ });
1148
+ });
1149
+
1150
+ const result = new Map();
1151
+ nodes.forEach((node) => {
1152
+ const queue = keyToQueues.get(node.rawKey);
1153
+ if (Array.isArray(queue) && queue.length > 0) {
1154
+ const occurrenceKey = `${node?.range?.line || 0}::${node?.rawKey || ""}`;
1155
+ result.set(occurrenceKey, queue.shift());
1156
+ }
1157
+ });
1158
+
1159
+ return result;
1160
+ };
1161
+
1162
+ const SIMPLE_PATH_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\[[0-9]+\]|\.[A-Za-z_$][A-Za-z0-9_$]*)*$/u;
1163
+
1164
+ const splitSimplePath = (expression = "") => {
1165
+ const trimmed = String(expression || "").trim();
1166
+ if (!SIMPLE_PATH_REGEX.test(trimmed)) {
1167
+ return null;
1168
+ }
1169
+ return trimmed
1170
+ .replace(/\[([0-9]+)\]/g, ".$1")
1171
+ .split(".")
1172
+ .filter(Boolean);
1173
+ };
1174
+
1175
+ const resolvePropertyTypeAtPath = ({ schemaNode, segments = [] }) => {
1176
+ let node = schemaNode;
1177
+ for (let index = 0; index < segments.length; index += 1) {
1178
+ const segment = segments[index];
1179
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
1180
+ return null;
1181
+ }
1182
+
1183
+ if (node.type === "array" && node.items) {
1184
+ if (/^[0-9]+$/.test(segment)) {
1185
+ node = node.items;
1186
+ continue;
1187
+ }
1188
+ return null;
1189
+ }
1190
+
1191
+ const properties = node.properties;
1192
+ if (!properties || typeof properties !== "object" || Array.isArray(properties)) {
1193
+ return null;
1194
+ }
1195
+ const candidateNames = [segment, toCamelCase(segment), toKebabCase(segment)];
1196
+ let next = null;
1197
+ for (let i = 0; i < candidateNames.length; i += 1) {
1198
+ const candidate = candidateNames[i];
1199
+ if (Object.prototype.hasOwnProperty.call(properties, candidate)) {
1200
+ next = properties[candidate];
1201
+ break;
1202
+ }
1203
+ }
1204
+ if (!next) {
1205
+ return null;
1206
+ }
1207
+ node = next;
1208
+ }
1209
+
1210
+ return node;
1211
+ };
1212
+
1213
+ export const resolveSchemaPathType = ({ model, expression = "" }) => {
1214
+ return resolveExpressionPathType({
1215
+ model,
1216
+ expression,
1217
+ });
1218
+ };
1219
+
1220
+ export const resolveExpressionPathType = ({
1221
+ model,
1222
+ expression = "",
1223
+ localSchemaTypes = new Map(),
1224
+ }) => {
1225
+ const segments = splitSimplePath(expression);
1226
+ if (!segments || segments.length === 0) {
1227
+ return null;
1228
+ }
1229
+
1230
+ const root = segments[0];
1231
+ const localRootSchema = localSchemaTypes instanceof Map
1232
+ ? localSchemaTypes.get(root)
1233
+ : undefined;
1234
+ if (localRootSchema && typeof localRootSchema === "object" && !Array.isArray(localRootSchema)) {
1235
+ if (segments.length === 1) {
1236
+ return {
1237
+ root,
1238
+ rootKind: "local",
1239
+ canonicalRoot: root,
1240
+ resolved: localRootSchema,
1241
+ missingSegment: null,
1242
+ };
1243
+ }
1244
+
1245
+ const resolvedLocalType = resolvePropertyTypeAtPath({
1246
+ schemaNode: localRootSchema,
1247
+ segments: segments.slice(1),
1248
+ });
1249
+ if (resolvedLocalType) {
1250
+ return {
1251
+ root,
1252
+ rootKind: "local",
1253
+ canonicalRoot: root,
1254
+ resolved: resolvedLocalType,
1255
+ missingSegment: null,
1256
+ };
1257
+ }
1258
+
1259
+ return {
1260
+ root,
1261
+ rootKind: "local",
1262
+ canonicalRoot: root,
1263
+ resolved: null,
1264
+ missingSegment: segments[segments.length - 1],
1265
+ };
1266
+ }
1267
+
1268
+ const schemaRootMap = collectSchemaRootMap(model);
1269
+ const rootSchema = schemaRootMap.get(root);
1270
+ if (!rootSchema) {
1271
+ return null;
1272
+ }
1273
+
1274
+ if (segments.length === 1) {
1275
+ return {
1276
+ root,
1277
+ rootKind: "schema",
1278
+ canonicalRoot: rootSchema.canonicalName,
1279
+ resolved: rootSchema.schema,
1280
+ missingSegment: null,
1281
+ };
1282
+ }
1283
+
1284
+ const resolved = resolvePropertyTypeAtPath({
1285
+ schemaNode: rootSchema.schema,
1286
+ segments: segments.slice(1),
1287
+ });
1288
+
1289
+ if (resolved) {
1290
+ return {
1291
+ root,
1292
+ rootKind: "schema",
1293
+ canonicalRoot: rootSchema.canonicalName,
1294
+ resolved,
1295
+ missingSegment: null,
1296
+ };
1297
+ }
1298
+
1299
+ return {
1300
+ root,
1301
+ rootKind: "schema",
1302
+ canonicalRoot: rootSchema.canonicalName,
1303
+ resolved: null,
1304
+ missingSegment: segments[segments.length - 1],
1305
+ };
1306
+ };
1307
+
1308
+ export const buildComponentScopeGraph = (model) => {
1309
+ const viewText = String(model?.view?.text || "");
1310
+ const rangeLocator = createRangeLocator(viewText);
1311
+ const references = [];
1312
+ const elementEntries = [];
1313
+ const globalSymbols = collectKnownExpressionRoots(model);
1314
+ const expressionAstCache = new Map();
1315
+ const parseExpressionAst = (expression = "") => {
1316
+ if (typeof expression !== "string" || !expression.trim()) {
1317
+ return undefined;
1318
+ }
1319
+ if (expressionAstCache.has(expression)) {
1320
+ return expressionAstCache.get(expression);
1321
+ }
1322
+
1323
+ const parsedExpression = parseJemplForCompiler({
1324
+ source: [{ [`$if ${expression}`]: null }],
1325
+ });
1326
+ const ast = parsedExpression?.ast?.items?.[0]?.properties?.[0]?.value?.conditions?.[0];
1327
+ expressionAstCache.set(expression, ast);
1328
+ return ast;
1329
+ };
1330
+
1331
+ const template = model?.view?.yaml?.template;
1332
+ if (template !== undefined) {
1333
+ const parsedTemplate = parseJemplForCompiler({ source: template });
1334
+ if (parsedTemplate.ast) {
1335
+ visitJemplNode({
1336
+ model,
1337
+ node: parsedTemplate.ast,
1338
+ scopeStack: [],
1339
+ references,
1340
+ elementEntries,
1341
+ rangeLocator,
1342
+ parseExpressionAst,
1343
+ });
1344
+ }
1345
+ }
1346
+
1347
+ const localBindingContextByElementOccurrence = mapElementLocalSymbolsByRawKey({
1348
+ model,
1349
+ elementEntries,
1350
+ });
1351
+ attachAttrExpressionReferences({
1352
+ model,
1353
+ references,
1354
+ localBindingContextByElementOccurrence,
1355
+ parseExpressionAst,
1356
+ rangeLocator,
1357
+ });
1358
+
1359
+ const refListeners = Array.isArray(model?.view?.refListeners) ? model.view.refListeners : [];
1360
+ refListeners.forEach((listener) => {
1361
+ const payloadExpressionRaw = listener?.eventConfig?.payload;
1362
+ if (typeof payloadExpressionRaw !== "string" || !payloadExpressionRaw.trim()) {
1363
+ return;
1364
+ }
1365
+ const payloadExpression = payloadExpressionRaw.trim();
1366
+
1367
+ const parsedPayload = parseJemplForCompiler({ source: payloadExpression });
1368
+ if (parsedPayload.ast) {
1369
+ visitJemplNode({
1370
+ model,
1371
+ node: parsedPayload.ast,
1372
+ scopeStack: [],
1373
+ references,
1374
+ elementEntries: [],
1375
+ rangeLocator,
1376
+ parseExpressionAst,
1377
+ });
1378
+ }
1379
+
1380
+ pushReference({
1381
+ references,
1382
+ expression: payloadExpression,
1383
+ context: "listener-payload",
1384
+ source: "listener",
1385
+ localSymbols: [],
1386
+ localSchemaTypes: [],
1387
+ parseExpressionAst,
1388
+ rangeLocator,
1389
+ range: rangeLocator.locate({
1390
+ expression: payloadExpression,
1391
+ preferredLine: listener?.optionLines?.payload || listener?.line,
1392
+ }),
1393
+ });
1394
+ });
1395
+
1396
+ return {
1397
+ globalSymbols,
1398
+ references,
1399
+ };
1400
+ };