SimpliField/jsub

View on GitHub
src/index.js

Summary

Maintainability
D
1 day
Test Coverage
'use strict';

var YError = require('yerror');
var debug = require('debug')('jsub');

function jsub(options, ast) {
  return checkExpression([], options.conditions || [], ast);
}

function checkExpression(errors, conditions, expression) {
  var expressionErrors = [];

  debug('Checking expression:', expression);
  // Check alls conditions
  conditions.some(function checkConditions(condition, index) {
    var conditionErrors = checkCondition(condition, index, expression);

    debug(
      'Condition check of:',
      condition,
      ' led to ',
      conditionErrors.length,
      ' errors.'
    );
    if (conditionErrors.length) {
      expressionErrors = expressionErrors.concat(conditionErrors);
      return false;
    }
    expressionErrors = conditionErrors;
    return true;
  });
  if (expressionErrors.length) {
    errors.push(
      new YError(
        'E_BAD_EXPRESSION',
        expression,
        expressionErrors.length,
        expressionErrors
      )
    );
  }
  // By-pass child expressions checking when having a custom parser
  // Warning: Using this feature may cause security issues!
  if (
    conditions.some(function(condition) {
      return condition.type === expression.type && condition.$_;
    })
  ) {
    debug('Child expression cancelled with a custom parser:', expression);
    return errors;
  }
  // Check child expressions
  if ('Program' === expression.type) {
    errors = expression.body.reduce(function processProgram(
      errors,
      expression
    ) {
      return errors.concat(checkExpression(errors, conditions, expression));
    },
    errors);
  } else if ('ExpressionStatement' === expression.type) {
    errors = checkExpression(errors, conditions, expression.expression);
  } else if ('BinaryExpression' === expression.type) {
    errors = checkExpression(errors, conditions, expression.left);
    errors = checkExpression(errors, conditions, expression.right);
  } else if ('LogicalExpression' === expression.type) {
    errors = checkExpression(errors, conditions, expression.left);
    errors = checkExpression(errors, conditions, expression.right);
  } else if ('UnaryExpression' === expression.type) {
    errors = checkExpression(errors, conditions, expression.argument);
  } else if ('ConditionalExpression' === expression.type) {
    errors = checkExpression(errors, conditions, expression.test);
    errors = checkExpression(errors, conditions, expression.consequent);
    errors = checkExpression(errors, conditions, expression.alternate);
  } else if ('NewExpression' === expression.type) {
    errors = checkExpression(errors, conditions, expression.callee);
    errors = expression.arguments.reduce(function(errors, expression) {
      return errors.concat(checkExpression(errors, conditions, expression));
    }, errors);
  } else if ('MemberExpression' === expression.type) {
    errors = checkExpression(errors, conditions, expression.object);
    errors = checkExpression(errors, conditions, expression.property);
  } else if ('CallExpression' === expression.type) {
    errors = checkExpression(errors, conditions, expression.callee);
    errors = expression.arguments.reduce(function(errors, expression) {
      return errors.concat(checkExpression(errors, conditions, expression));
    }, errors);
  } else if (-1 === ['Identifier', 'Literal'].indexOf(expression.type)) {
    errors = [new YError('E_UNHANDLED_EXPRESSION', expression.type)];
  }
  return errors;
}

function checkCondition(condition, index, expression) {
  var errors = [];

  if (!condition.type) {
    throw new Error('E_NO_CONDITION_TYPE');
  }

  // Always check type first
  errors = checkConditionKey(condition, expression, errors, 'type');

  // If type doesn't match, do not parse the all condition keys
  if (errors.length) {
    return errors;
  }

  // Add other checks
  errors = Object.keys(condition)
    .filter(function(key) {
      return 'type' !== key;
    })
    .reduce(checkConditionKey.bind(null, condition, expression), errors);

  return errors;
}

function checkConditionKey(condition, expression, errors, property) {
  // Simple value based condition
  if (
    -1 !== ['boolean', 'string', 'number'].indexOf(typeof condition[property])
  ) {
    if (condition[property] !== expression[property]) {
      errors.push(
        new YError(
          'E_DIFFERENT_VALUES',
          condition[property],
          expression[property]
        )
      );
    }
    // RegExp based condition
  } else if (condition[property] instanceof RegExp) {
    if (!condition[property].test(expression[property])) {
      errors.push(
        new YError('E_NO_MATCH', condition[property], expression[property])
      );
    }
    // Array based condition
  } else if (condition[property] instanceof Array) {
    if (-1 === condition[property].indexOf(expression[property])) {
      errors.push(
        new YError('E_NO_MATCH', condition[property], expression[property])
      );
    }
    // Expression check based condition
  } else if ('$_' === property) {
    if (!(condition[property] instanceof Function)) {
      throw new YError(
        'E_BAD_EXPRESSION_CHECKER',
        property,
        condition[property]
      );
    }
    errors = errors.concat(condition[property](expression));
    // Bad condition property
  } else {
    throw new YError(
      'E_BAD_CONDITION_PROPERTY',
      condition,
      property,
      condition[property]
    );
  }
  if (errors.length) {
    debug('Property check:', property, 'led to', errors);
  }
  return errors;
}

module.exports = jsub;