You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
6.0 KiB
JavaScript

/**
* call-matcher:
* ECMAScript CallExpression matcher made from function/method signature
*
* https://github.com/twada/call-matcher
*
* Copyright (c) 2015-2018 Takuto Wada
* Licensed under the MIT license.
* https://github.com/twada/call-matcher/blob/master/MIT-LICENSE.txt
*/
'use strict';
/* jshint -W024 */
var estraverse = require('estraverse');
var espurify = require('espurify');
var syntax = estraverse.Syntax;
var hasOwn = Object.prototype.hasOwnProperty;
var forEach = require('core-js/library/fn/array/for-each');
var map = require('core-js/library/fn/array/map');
var filter = require('core-js/library/fn/array/filter');
var reduce = require('core-js/library/fn/array/reduce');
var indexOf = require('core-js/library/fn/array/index-of');
var deepEqual = require('deep-equal');
var notCallExprMessage = 'Argument should be in the form of CallExpression';
var duplicatedArgMessage = 'Duplicate argument name: ';
var invalidFormMessage = 'Argument should be in the form of `name` or `[name]`';
function CallMatcher (signatureAst, options) {
validateApiExpression(signatureAst);
options = options || {};
this.visitorKeys = options.visitorKeys || estraverse.VisitorKeys;
if (options.astWhiteList) {
this.purifyAst = espurify.cloneWithWhitelist(options.astWhiteList);
} else {
this.purifyAst = espurify;
}
this.signatureAst = signatureAst;
this.signatureCalleeDepth = astDepth(signatureAst.callee, this.visitorKeys);
this.numMaxArgs = this.signatureAst.arguments.length;
this.numMinArgs = filter(this.signatureAst.arguments, identifiers).length;
}
CallMatcher.prototype.test = function (currentNode) {
var calleeMatched = this.isCalleeMatched(currentNode);
var numArgs;
if (calleeMatched) {
numArgs = currentNode.arguments.length;
return this.numMinArgs <= numArgs && numArgs <= this.numMaxArgs;
}
return false;
};
CallMatcher.prototype.matchArgument = function (currentNode, parentNode) {
if (isCalleeOfParent(currentNode, parentNode)) {
return null;
}
if (this.test(parentNode)) {
var indexOfCurrentArg = indexOf(parentNode.arguments, currentNode);
var numOptional = parentNode.arguments.length - this.numMinArgs;
var matchedSignatures = reduce(this.argumentSignatures(), function (accum, argSig) {
if (argSig.kind === 'mandatory') {
accum.push(argSig);
}
if (argSig.kind === 'optional' && 0 < numOptional) {
numOptional -= 1;
accum.push(argSig);
}
return accum;
}, []);
return matchedSignatures[indexOfCurrentArg];
}
return null;
};
CallMatcher.prototype.calleeAst = function () {
return this.purifyAst(this.signatureAst.callee);
};
CallMatcher.prototype.argumentSignatures = function () {
return map(this.signatureAst.arguments, toArgumentSignature);
};
CallMatcher.prototype.isCalleeMatched = function (node) {
if (!isCallExpression(node)) {
return false;
}
if (!this.isSameDepthAsSignatureCallee(node.callee)) {
return false;
}
return deepEqual(this.purifyAst(this.signatureAst.callee), this.purifyAst(node.callee));
};
CallMatcher.prototype.isSameDepthAsSignatureCallee = function (ast) {
var depth = this.signatureCalleeDepth;
var currentDepth = 0;
estraverse.traverse(ast, {
keys: this.visitorKeys,
enter: function (currentNode, parentNode) {
var path = this.path();
var pathDepth = path ? path.length : 0;
if (currentDepth < pathDepth) {
currentDepth = pathDepth;
}
if (depth < currentDepth) {
this['break']();
}
}
});
return (depth === currentDepth);
};
function toArgumentSignature (argSignatureNode, idx) {
switch(argSignatureNode.type) {
case syntax.Identifier:
return {
index: idx,
name: argSignatureNode.name,
kind: 'mandatory'
};
case syntax.ArrayExpression:
return {
index: idx,
name: argSignatureNode.elements[0].name,
kind: 'optional'
};
default:
return null;
}
}
function astDepth (ast, visitorKeys) {
var maxDepth = 0;
estraverse.traverse(ast, {
keys: visitorKeys,
enter: function (currentNode, parentNode) {
var path = this.path();
var pathDepth = path ? path.length : 0;
if (maxDepth < pathDepth) {
maxDepth = pathDepth;
}
}
});
return maxDepth;
}
function isCallExpression (node) {
return node && node.type === syntax.CallExpression;
}
function isCalleeOfParent(currentNode, parentNode) {
return parentNode && currentNode &&
parentNode.type === syntax.CallExpression &&
parentNode.callee === currentNode;
}
function identifiers (node) {
return node.type === syntax.Identifier;
}
function validateApiExpression (callExpression) {
if (!callExpression || !callExpression.type) {
throw new Error(notCallExprMessage);
}
if (callExpression.type !== syntax.CallExpression) {
throw new Error(notCallExprMessage);
}
var names = {};
forEach(callExpression.arguments, function (arg) {
var name = validateArg(arg);
if (hasOwn.call(names, name)) {
throw new Error(duplicatedArgMessage + name);
} else {
names[name] = name;
}
});
}
function validateArg (arg) {
var inner;
switch(arg.type) {
case syntax.Identifier:
return arg.name;
case syntax.ArrayExpression:
if (arg.elements.length !== 1) {
throw new Error(invalidFormMessage);
}
inner = arg.elements[0];
if (inner.type !== syntax.Identifier) {
throw new Error(invalidFormMessage);
}
return inner.name;
default:
throw new Error(invalidFormMessage);
}
}
module.exports = CallMatcher;