Source: lib/compiler/expressionParser.js

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.
   * @returns {object} The extracted state object.
   */
  parseState(content, config = null) {
    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;
    }
    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;