Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 471x 471x 471x 471x 471x 471x 471x 534x 534x 534x 534x 534x 471x 471x 3297x 3297x 3297x 65x 65x 65x 65x 65x 65x 65x 65x 65x 65x 65x 65x 65x 1x 1x 64x 64x 64x 64x 64x 64x 64x 64x 65x 3297x 471x 471x 471x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 471x 471x 471x 471x 471x 471x 471x 471x 471x 471x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 471x 471x 471x 36x 36x 36x 471x 471x 598x 1x 1x 598x 471x 471x 471x | /**
* @file validateExpressions.js
* @description Checks template expressions at build time, not at render time.
*
* Template interpolations, computed values and directive bindings are evaluated
* by Avenx's own expression evaluator, which covers the expression language and
* refuses anything outside it. Without this pass a developer finds that out
* when the component renders — possibly in production, possibly on a branch a
* test never took.
*
* The compiler already holds every one of those expressions as source text.
* Parsing them here means an unsupported expression fails the build, with the
* file, the line and the reason, instead of becoming an AVX_R32 at runtime.
*
* Action bodies are deliberately not checked. They are statement JavaScript and
* always have been; `if`, `for` and `return` are not expressions, and the
* runtime runs them as statements.
* @module lib/compiler/validateExpressions
*/
import { compileExpression, describeParseFailure } from '../core/expression/compile.js';
import { createInterpolationRegex } from '../core/utils/templateUtils.js';
import { createLineIndex } from './parser/tokenizer.js';
import { AvenxErrorCodes } from '../core/runtime/AvenxError.js';
import { TemplateValidationError } from './errors/TemplateValidationError.js';
/**
* Attributes whose value is an expression rather than a statement.
*
* `data-ax-event` holds handler statements and is excluded: a handler may
* legitimately be a statement sequence.
* @type {string[]}
*/
const EXPRESSION_ATTRIBUTES = [
'data-ax-for',
'data-ax-key',
'data-ax-show',
'data-ax-html',
'data-ax-class',
'data-ax-style',
'data-ax-if',
];
/**
* Finds every expression in a template, with its source offset.
*
* Interpolations and directive attribute values only. Reading them out of the
* rewritten template rather than the original source means the offsets point at
* a string the developer never wrote, so the location is reported against the
* original file by searching for the expression text — imprecise for a repeated
* expression, and honest about it: the message names the expression itself,
* which is what a developer searches for.
* @param {string} template - The rewritten template.
* @returns {Array<{source: string, kind: string}>} The expressions found.
*/
export function collectTemplateExpressions(template) {
/** @type {Array<{source: string, kind: string}>} */
const found = [];
if (typeof template !== 'string' || template === '') {
return found;
}
const interpolation = createInterpolationRegex();
let match;
while ((match = interpolation.exec(template)) !== null) {
const source = (match[1] !== undefined ? match[1] : match[2] || '').trim();
if (source) {
found.push({ source, kind: 'interpolation' });
}
}
for (const name of EXPRESSION_ATTRIBUTES) {
const attribute = new RegExp(`${name}="([^"]*)"`, 'g');
let attrMatch;
while ((attrMatch = attribute.exec(template)) !== null) {
const raw = attrMatch[1];
// A directive value may be written as an interpolation --
// data-ax-style="{{ { fontWeight: x } }}" -- in which case the braces are
// the interpolation, not the expression. Those are already collected by
// the interpolation pass above; parsing the wrapper as an expression
// would reject perfectly valid markup.
//
// `{% %}` is the same thing inside a loop body, where the escaping pass
// rewrites `{{ }}` so the enclosing render does not evaluate it per
// template instead of per item. The interpolation pass does not see
// those, so skipping them here is what stops a valid directive inside a
// `<@for>` being reported as a malformed expression.
if (raw.includes('{{') || raw.includes('{%')) {
continue;
}
const source = raw
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/&/g, '&')
.trim();
if (source) {
found.push({ source, kind: name });
}
}
}
return found;
}
/**
* Validates every expression a component evaluates as an expression.
* @param {object} unit - The component being compiled.
* @param {string} unit.name - The component name.
* @param {string} unit.filePath - Absolute path to the component file.
* @param {string} unit.content - The original source, for locating errors.
* @param {string} unit.template - The rewritten template.
* @param {Object<string, string>} unit.computed - Computed expressions by name.
* @returns {TemplateValidationError[]} One error per unsupported expression.
*/
export function validateComponentExpressions({ name, filePath, content, template, computed }) {
/** @type {TemplateValidationError[]} */
const errors = [];
const lines = content ? createLineIndex(content) : null;
/**
* Records an error for one unsupported expression.
* @param {string} source - The expression source.
* @param {string} where - What kind of binding it is.
*/
const reject = (source, where) => {
const error = new TemplateValidationError(
AvenxErrorCodes.EXPRESSION_UNSUPPORTED,
source,
`${describeParseFailure(source)} (in ${where} of <${name}>)`,
);
if (content && lines) {
const index = content.indexOf(source);
if (index >= 0) {
error.setLocation({ source: content, index, filename: filePath });
}
}
errors.push(error);
};
for (const [key, expression] of Object.entries(computed || {})) {
if (typeof expression !== 'string' || expression.trim() === '') continue;
if (!compileExpression(expression)) {
reject(expression, `<computed name="${key}">`);
}
}
for (const { source, kind } of collectTemplateExpressions(template)) {
if (!compileExpression(source)) {
reject(source, kind === 'interpolation' ? 'a template interpolation' : `a ${kind} binding`);
}
}
return errors;
}
|