index.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. /**
  2. * call-matcher:
  3. * ECMAScript CallExpression matcher made from function/method signature
  4. *
  5. * https://github.com/twada/call-matcher
  6. *
  7. * Copyright (c) 2015-2018 Takuto Wada
  8. * Licensed under the MIT license.
  9. * https://github.com/twada/call-matcher/blob/master/MIT-LICENSE.txt
  10. */
  11. 'use strict';
  12. /* jshint -W024 */
  13. var estraverse = require('estraverse');
  14. var espurify = require('espurify');
  15. var syntax = estraverse.Syntax;
  16. var hasOwn = Object.prototype.hasOwnProperty;
  17. var forEach = require('core-js/library/fn/array/for-each');
  18. var map = require('core-js/library/fn/array/map');
  19. var filter = require('core-js/library/fn/array/filter');
  20. var reduce = require('core-js/library/fn/array/reduce');
  21. var indexOf = require('core-js/library/fn/array/index-of');
  22. var deepEqual = require('deep-equal');
  23. var notCallExprMessage = 'Argument should be in the form of CallExpression';
  24. var duplicatedArgMessage = 'Duplicate argument name: ';
  25. var invalidFormMessage = 'Argument should be in the form of `name` or `[name]`';
  26. function CallMatcher (signatureAst, options) {
  27. validateApiExpression(signatureAst);
  28. options = options || {};
  29. this.visitorKeys = options.visitorKeys || estraverse.VisitorKeys;
  30. if (options.astWhiteList) {
  31. this.purifyAst = espurify.cloneWithWhitelist(options.astWhiteList);
  32. } else {
  33. this.purifyAst = espurify;
  34. }
  35. this.signatureAst = signatureAst;
  36. this.signatureCalleeDepth = astDepth(signatureAst.callee, this.visitorKeys);
  37. this.numMaxArgs = this.signatureAst.arguments.length;
  38. this.numMinArgs = filter(this.signatureAst.arguments, identifiers).length;
  39. }
  40. CallMatcher.prototype.test = function (currentNode) {
  41. var calleeMatched = this.isCalleeMatched(currentNode);
  42. var numArgs;
  43. if (calleeMatched) {
  44. numArgs = currentNode.arguments.length;
  45. return this.numMinArgs <= numArgs && numArgs <= this.numMaxArgs;
  46. }
  47. return false;
  48. };
  49. CallMatcher.prototype.matchArgument = function (currentNode, parentNode) {
  50. if (isCalleeOfParent(currentNode, parentNode)) {
  51. return null;
  52. }
  53. if (this.test(parentNode)) {
  54. var indexOfCurrentArg = indexOf(parentNode.arguments, currentNode);
  55. var numOptional = parentNode.arguments.length - this.numMinArgs;
  56. var matchedSignatures = reduce(this.argumentSignatures(), function (accum, argSig) {
  57. if (argSig.kind === 'mandatory') {
  58. accum.push(argSig);
  59. }
  60. if (argSig.kind === 'optional' && 0 < numOptional) {
  61. numOptional -= 1;
  62. accum.push(argSig);
  63. }
  64. return accum;
  65. }, []);
  66. return matchedSignatures[indexOfCurrentArg];
  67. }
  68. return null;
  69. };
  70. CallMatcher.prototype.calleeAst = function () {
  71. return this.purifyAst(this.signatureAst.callee);
  72. };
  73. CallMatcher.prototype.argumentSignatures = function () {
  74. return map(this.signatureAst.arguments, toArgumentSignature);
  75. };
  76. CallMatcher.prototype.isCalleeMatched = function (node) {
  77. if (!isCallExpression(node)) {
  78. return false;
  79. }
  80. if (!this.isSameDepthAsSignatureCallee(node.callee)) {
  81. return false;
  82. }
  83. return deepEqual(this.purifyAst(this.signatureAst.callee), this.purifyAst(node.callee));
  84. };
  85. CallMatcher.prototype.isSameDepthAsSignatureCallee = function (ast) {
  86. var depth = this.signatureCalleeDepth;
  87. var currentDepth = 0;
  88. estraverse.traverse(ast, {
  89. keys: this.visitorKeys,
  90. enter: function (currentNode, parentNode) {
  91. var path = this.path();
  92. var pathDepth = path ? path.length : 0;
  93. if (currentDepth < pathDepth) {
  94. currentDepth = pathDepth;
  95. }
  96. if (depth < currentDepth) {
  97. this['break']();
  98. }
  99. }
  100. });
  101. return (depth === currentDepth);
  102. };
  103. function toArgumentSignature (argSignatureNode, idx) {
  104. switch(argSignatureNode.type) {
  105. case syntax.Identifier:
  106. return {
  107. index: idx,
  108. name: argSignatureNode.name,
  109. kind: 'mandatory'
  110. };
  111. case syntax.ArrayExpression:
  112. return {
  113. index: idx,
  114. name: argSignatureNode.elements[0].name,
  115. kind: 'optional'
  116. };
  117. default:
  118. return null;
  119. }
  120. }
  121. function astDepth (ast, visitorKeys) {
  122. var maxDepth = 0;
  123. estraverse.traverse(ast, {
  124. keys: visitorKeys,
  125. enter: function (currentNode, parentNode) {
  126. var path = this.path();
  127. var pathDepth = path ? path.length : 0;
  128. if (maxDepth < pathDepth) {
  129. maxDepth = pathDepth;
  130. }
  131. }
  132. });
  133. return maxDepth;
  134. }
  135. function isCallExpression (node) {
  136. return node && node.type === syntax.CallExpression;
  137. }
  138. function isCalleeOfParent(currentNode, parentNode) {
  139. return parentNode && currentNode &&
  140. parentNode.type === syntax.CallExpression &&
  141. parentNode.callee === currentNode;
  142. }
  143. function identifiers (node) {
  144. return node.type === syntax.Identifier;
  145. }
  146. function validateApiExpression (callExpression) {
  147. if (!callExpression || !callExpression.type) {
  148. throw new Error(notCallExprMessage);
  149. }
  150. if (callExpression.type !== syntax.CallExpression) {
  151. throw new Error(notCallExprMessage);
  152. }
  153. var names = {};
  154. forEach(callExpression.arguments, function (arg) {
  155. var name = validateArg(arg);
  156. if (hasOwn.call(names, name)) {
  157. throw new Error(duplicatedArgMessage + name);
  158. } else {
  159. names[name] = name;
  160. }
  161. });
  162. }
  163. function validateArg (arg) {
  164. var inner;
  165. switch(arg.type) {
  166. case syntax.Identifier:
  167. return arg.name;
  168. case syntax.ArrayExpression:
  169. if (arg.elements.length !== 1) {
  170. throw new Error(invalidFormMessage);
  171. }
  172. inner = arg.elements[0];
  173. if (inner.type !== syntax.Identifier) {
  174. throw new Error(invalidFormMessage);
  175. }
  176. return inner.name;
  177. default:
  178. throw new Error(invalidFormMessage);
  179. }
  180. }
  181. module.exports = CallMatcher;