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 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 3960x 3960x 3960x 455x 455x 455x 3960x 3960x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 403x 403x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 512x 512x 512x 512x 3x 3x 3x 3x 3x 3x 511x 511x 511x 550x 550x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 550x 511x 512x 325x 325x 325x 325x 325x 325x 325x 490x 490x 42x 42x 490x 490x 325x 325x 325x 325x 325x 325x 325x 495x 495x 279x 279x 495x 495x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 325x 484x 484x 484x 484x 267x 52x 267x 1x 1x 1x 1x 1x 1x 1x 1x 1x 51x 51x 267x 267x 483x 483x 484x 325x 325x 325x 325x 325x 325x 325x 494x 494x 12x 12x 12x 494x 494x 325x 325x 325x 325x 325x 325x 325x 325x 3x 3x 3x 2x 2x 2x 2x 2x 3x 325x 325x 325x 325x 325x 325x 325x 325x 490x 490x 325x 325x 325x | import path from 'path';
import { AvenxErrorCodes } from '../core/runtime/AvenxError.js';
import { TemplateValidationError } from './errors/TemplateValidationError.js';
import { reportWarning } from './utils/warningReporter.js';
import { parseDeclarations } from './parser/declarations.js';
import { LruCache } from '../core/utils/LruCache.js';
/**
* Declaration sets keyed by source text.
*
* Every `parseX` method needs the same scan, and `ComponentParser` calls six of
* them per file. Scanning once per unique source keeps that a single pass
* without changing any method's signature.
* @type {LruCache}
*/
const declarationCache = new LruCache(64);
/**
* Returns the declaration set for a source, scanning it at most once.
* @param {string} content - The component source.
* @returns {DeclarationSet} The declarations.
*/
export function readDeclarations(content) {
const source = typeof content === 'string' ? content : '';
let parsed = declarationCache.get(source);
if (!parsed) {
parsed = parseDeclarations(source);
declarationCache.set(source, parsed);
}
return parsed;
}
/**
* Conflict policies an `<action atomic>` may select.
*
* Mirrors `rewind.onConflict` in avenx.config.json, which supplies the default
* when an action does not name one.
* @type {string[]}
*/
export const CONFLICT_POLICIES = ['safe', 'force', 'abort'];
/**
* ExpressionParser is responsible for extracting state, computed properties,
* and methods from Avenx component source code.
*/
class ExpressionParser {
/**
* @param {object} [config] - Project configuration object.
*/
constructor(config = null) {
this.config = config;
}
/**
* Extracts the initial state from <state /> tags.
* @param {string} content - The component source code.
* @param {object} [config] - Optional override configuration object.
* @param {{name?: string, filePath?: string}} [unit] - The component being
* parsed, for diagnostics.
* @returns {object} The extracted state object.
*/
parseState(content, config = null, unit = {}) {
const activeConfig = config || this.config;
const declarations = readDeclarations(content);
if (declarations.stateTagCount > 1) {
const err = new TemplateValidationError(AvenxErrorCodes.COMPILER_MULTIPLE_STATE_TAGS);
if (declarations.secondStateTagOffset >= 0) {
err.setLocation({ source: content, index: declarations.secondStateTagOffset });
}
reportWarning(AvenxErrorCodes.COMPILER_MULTIPLE_STATE_TAGS, err, activeConfig);
}
const state = {};
for (const entry of declarations.state) {
state[entry.name] = entry.value;
if (entry.notLiteral) {
// The file is in the message, not only in the location: a warning is
// rendered from its message alone, so a developer reading "State x in
// <Card>" across forty components had no way to tell which file, and
// `avenx check --json` had nothing to attribute it to.
const err = new TemplateValidationError(
AvenxErrorCodes.COMPILER_STATE_NOT_LITERAL,
unit.name || 'Component',
entry.name,
entry.notLiteral,
JSON.stringify(entry.value),
unit.filePath ? path.basename(unit.filePath) : 'this component',
);
err.setLocation({ source: content, line: entry.line, column: entry.column, filename: unit.filePath });
reportWarning(AvenxErrorCodes.COMPILER_STATE_NOT_LITERAL, err, activeConfig);
}
}
return state;
}
/**
* Extracts computed property definitions from <computed /> tags.
* @param {string} content - The component source code.
* @returns {object} A map of computed property names to their expressions.
*/
parseComputed(content) {
const computed = {};
for (const entry of readDeclarations(content).computed) {
computed[entry.name] = entry.expression;
}
return computed;
}
/**
* Extracts method definitions from <action /> tags.
* @param {string} content - The component source code.
* @returns {object} A map of method names to their source code.
*/
parseMethods(content) {
const methods = {};
for (const action of readDeclarations(content).actions) {
methods[action.name] = action.body;
}
return methods;
}
/**
* Extracts Avenx Rewind modifiers from `<action>` tags.
*
* Deliberately a second pass rather than a wider return type on
* {@link ExpressionParser#parseMethods}: that method's `{name: body}` shape
* is consumed by the code generator, by Atlas and by a dozen tests, and
* widening it to carry attributes would ripple through all of them for the
* sake of two optional flags.
*
* `atomic` is a bare boolean in the same style as `<contract static pure />`.
* `onConflict` selects what a rewind does when it finds a value the
* transaction did not write; omitting it falls back to the project's
* `rewind.onConflict`, which is why an absent value is left undefined here
* rather than defaulted.
* @param {string} content - The component source code.
* @returns {Object<string, {atomic: boolean, onConflict: string=}>} Modifiers
* by action name. Only actions that declare one appear.
* @throws {TemplateValidationError} When `onConflict` names an unknown policy.
*/
parseActionModifiers(content) {
/** @type {Object<string, {atomic: boolean, onConflict: string=}>} */
const modifiers = {};
for (const action of readDeclarations(content).actions) {
if (!action.atomic) continue;
if (action.onConflict !== undefined && !CONFLICT_POLICIES.includes(action.onConflict)) {
const err = new TemplateValidationError(
AvenxErrorCodes.COMPILER_CONTRACT_INVALID_DECLARATION,
`onConflict="${action.onConflict}"`,
`<action name="${action.name}" atomic>`,
`expected one of ${CONFLICT_POLICIES.map((policy) => `"${policy}"`).join(', ')}`,
);
err.setLocation({ source: content, index: action.tagOffset });
throw err;
}
modifiers[action.name] =
action.onConflict === undefined ? { atomic: true } : { atomic: true, onConflict: action.onConflict };
}
return modifiers;
}
/**
* Extracts resource definitions from <resource /> tags.
* @param {string} content - The component source code.
* @returns {object} A map of resource names to their handler expressions.
*/
parseResources(content) {
const resources = {};
for (const entry of readDeclarations(content).resources) {
resources[entry.name] =
entry.pollInterval !== null ? { handler: entry.handler, pollInterval: entry.pollInterval } : entry.handler;
}
return resources;
}
/**
* Parses dynamic attribute name binding expressions matching ^:\[(.*)\]$.
* @param {string} attrName - The attribute name (e.g. ":[dynamicAttr]").
* @param {string} [attrValue] - The attribute value expression.
* @returns {object|null} Metadata object containing expression details or null if not a dynamic attribute.
*/
parseDynamicAttribute(attrName, attrValue = '') {
if (!attrName) return null;
const match = attrName.match(/^:\[(.*)\]$/);
if (!match) return null;
return {
isDynamicName: true,
nameExpr: match[1],
valueExpr: attrValue,
};
}
/**
* Extracts declared component-level compiler contracts from <contract /> tags.
* Supports attributes: static, pure, deterministic, isolated (boolean or valueless).
* @param {string} content - The component source code.
* @returns {Set<string>} The set of active contracts for the component.
*/
parseContracts(content) {
return new Set(readDeclarations(content).contracts);
}
}
export default ExpressionParser;
|