twada/espower

View on GitHub
templates/argument-recorder.js

Summary

Maintainability
C
7 hrs
Test Coverage
'use strict';
module.exports = function () {
  const isPromiseLike = (o) => o !== null &&
                typeof o === 'object' &&
                typeof o.then === 'function' &&
                typeof o.catch === 'function';

  const mark = (_this, s) => {
    return function () {
      const args = Array.from(arguments);
      _this.status = s;
      _this.value = (args.length === 1) ? args[0] : args;
    };
  };

  class $Promise$ {
    constructor (prms) {
      this.status = 'pending';
      prms.then(mark(this, 'resolved'), mark(this, 'rejected'));
    }
  }

  const wrap = (v) => isPromiseLike(v) ? new $Promise$(v) : v;

  class ArgumentRecorder {
    /**
     * @typedef {Object} AssertionMetadata
     * @property {string} content
     * @property {string} filepath
     * @property {number} line
     * @property {number} version
     * @property {string} pattern
     * @property {array} params
     * @property {boolean} [async] - true if enclosed in async function
     * @property {boolean} [generator] - true if enclosed in generator function
     * @property {string} [ast] - stringified AST (a part of API used in AVA)
     * @property {string} [tokens] - stringified tokens
     * @property {string} [visitorKeys] - stringified visitorKeys
     */

    /**
     * record argument value and metadata silently
     * @param {function} callee - callee of target argument
     * @param {AssertionMetadata} am - generated metadata for target assertion
     * @param {number} matchIndex - index of matched parameter
     */
    constructor (callee, am, matchIndex) {
      this._callee = callee;
      this._am = am;
      this._logs = [];
      this._recorded = null;
      this._val = null;
      this._idx = matchIndex;
      const conf = am.params[matchIndex];
      this._isBlock = !!conf.block;
    }

    /**
     * @return {AssertionMetadata} - AssertionMetadata for target assertion
     */
    metadata () {
      return this._am;
    }

    /**
     * @return {number} - index of matched parameter
     */
    matchIndex () {
      return this._idx;
    }

    /**
     * @return {*} - recorded actual value of target argument
     */
    val () {
      return this._val;
    }

    /**
     * tap capturable node value with its espath then store them as a Log
     * @param {*} value - actual value of target node
     * @param {string} espath - espath of target node in AST
     * @return {*} - the original value
     */
    _tap (value, espath) {
      this._logs.push({
        value: wrap(value), // API (used in AVA)
        espath // API (used in AVA)
      });
      return value;
    }

    /**
     * record argument value silently then clear captured logs
     * optionally, proxy block argument then store its result as a Log
     * @param {*} value - actual value of target argument
     * @param {string} [espath] - espath of target node in AST
     * @return {*|ArgumentRecorder} - ArgumentRecorder or actual value of target argument
     */
    _rec (value, espath) {
      const empowered = this._callee && this._callee._empowered;
      try {
        if (!empowered) return value;
        if (!espath) return this;

        const log = {
          value: wrap(value), // API (used in AVA)
          espath // API (used in AVA)
        };
        this._logs.push(log);

        if (this._isBlock && empowered && typeof value === 'function') {
          value = new Proxy(value, {
            apply (target, thisArg, args) {
              try {
                const ret = target.apply(thisArg, args);
                log.value = wrap(ret); // API (used in AVA)
                return ret;
              } catch (e) {
                log.value = e; // API (used in AVA)
                throw e;
              }
            }
          });
        }

        return this;
      } finally {
        if (empowered) {
          this._recorded = {
            value,
            logs: [].concat(this._logs)
          };
        }
        this._val = value; // actual value of target argument
        this._logs = []; // clear logs
      }
    }

    /**
     * @typedef {Object} Log
     * @property {*} value - recorded actual value of target node
     * @property {string} espath - espath of target node in AST
     */

    /**
     * @typedef {Object} RecordedData
     * @property {*} value - recorded actual value of target argument
     * @property {Log[]} logs - recorded Logs
     */

    /**
     * return RecordedData then clear cache
     * @return {RecordedData} - captured value and metadata of target argument
     */
    eject () {
      const ret = this._recorded;
      this._recorded = null;
      this._val = null;
      return ret;
    }
  }

  return ArgumentRecorder;
};