Source: extract/VariableExtractor.js

import {Dsl} from '../config/Dsl.js';
import {DiffConfig} from '../config/DiffConfig.js';

/**
 * Extractor for retrieving and caching the sets of read and written variables
 * in a node's code.
 * @implements {ExtractorInterface<{writtenVariables: Set<String>,
 *     readVariables: Set<Node>}>}
 */
export class VariableExtractor {
  /**
   * @inheritDoc
   * @type {Map<Node,{writtenVariables: Set<String>,
   *     readVariables: Set<Node>}>}
   * @protected
   */
  _memo;

  /**
   * Extract the sets of read and written variables from a node's content and
   * cache it.
   * @param {Node} node
   * @protected
   */
  _extract(node) {
    this._memo.set(node, {
      writtenVariables: this.#getWrittenVariables(node),
      readVariables: this.#getReadVariables(node),
    });
  }

  /**
   * Get the cached sets of read and written variables.
   * If they are not cached, compute and cache them first.
   * @param {Node} node
   * @return {{writtenVariables: Set<String>,
   *     readVariables: Set<Node>}}
   */
  get(node) {
    if (!this._memo.has(node)) {
      this._extract(node);
    }
    return this._memo.get(node);
  }

  /**
   * An extractor for the properties of <call> nodes.
   * @type {CallPropertyExtractor}
   * @const
   */
  callPropertyExtractor;

  /**
   * Create a new VariableExtractor instance.
   * @param {CallPropertyExtractor} callPropertyExtractor The existing call
   *     property extractor
   */
  constructor(callPropertyExtractor) {
    this._memo = new Map();
    this.callPropertyExtractor = callPropertyExtractor;
  }

  /**
   * Get the set of read variables for a node.
   * @param {Node} node
   * @return {Set<String>}
   * @private
   */
  #getReadVariables(node) {
    let code;
    if (node.isInnerNode()) {
      // May be null, we will check later
      code = node.attributes.get(Dsl.INNER_PROPERTIES.CONDITION.label);
    } else if (node.isScript()) {
      code = node.text;
    } else if (node.isCall()) {
      // Also consider text content of call arguments as code
      const argsCode = this.callPropertyExtractor.get(node).argVals.join(' ');
      code = argsCode + ' ' + this.callPropertyExtractor.get(node).code;
    }
    const readVariables = new Set();
    if (code != null) {
      for (const readVar of this.#readVarsFromString(code)) {
        readVariables.add(readVar);
      }
    }
    return readVariables;
  }

  /**
   * Get the set of written variables for a node.
   * @param {Node} node
   * @return {Set<String>}
   * @private
   */
  #getWrittenVariables(node) {
    let code;
    if (node.isScript()) {
      code = node.text;
    } else if (node.isCall()) {
      code = this.callPropertyExtractor.get(node).code;
    }
    let writtenVariables = new Set();
    if (code != null) {
      writtenVariables = new Set(this.#writtenVarsFromString(code));
    }
    return writtenVariables;
  }

  /**
   * Get an array of read variables contained in a String of code.
   * @param {String} str
   * @return {Array<String>}
   */
  #readVarsFromString(str) {
    // Cannot keep raw dots in variable prefix
    const prefix = DiffConfig.VARIABLE_PREFIX.replaceAll('.', '\\.');
    // Negative lookahead for assignment operators and positive lookbehind for
    // Data element prefix. Also, a positive lookahead for any non-word
    // character is necessary to avoid matching a partial variable descriptor.
    const regex = new RegExp('(?<=' + prefix + ')' +
        '[a-zA-Z$_](\\w|\\$)*(?=($|\\s*[^\\w_$]))(?!\\s*=[^=])', 'g');
    const matches = str.match(regex);
    return matches == null ? [] : matches;
  }

  /**
   * Get an array of written variables contained in a String of code.
   * @param {String} str
   * @return {Array<String>}
   */
  #writtenVarsFromString(str) {
    // Cannot keep raw dots in variable prefix
    const prefix = DiffConfig.VARIABLE_PREFIX.replaceAll('.', '\\.');
    // Positive lookahead for assignment operators and positive lookbehind for
    // data element prefix.
    const regex = new RegExp('(?<=' + prefix + ')' +
        '[a-zA-Z$_](\\w|\\$)*(?=\\s*(=[^=]|\\+=|\\+\\+|-=|--|\\*=|\\/=))', 'g');
    const matches = str.match(regex);
    return matches == null ? [] : matches;
  }
}