123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- /**
- * 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;
|