assertion-visitor.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. 'use strict';
  2. var estraverse = require('estraverse');
  3. var escodegen = require('escodegen');
  4. var espurify = require('espurify');
  5. var espurifyWithRaw = espurify.customize({extra: 'raw'});
  6. var syntax = estraverse.Syntax;
  7. var EspowerLocationDetector = require('espower-location-detector');
  8. var EspowerError = require('./espower-error');
  9. var toBeSkipped = require('./rules/to-be-skipped');
  10. var toBeCaptured = require('./rules/to-be-captured');
  11. var canonicalCodeOptions = {
  12. format: {
  13. indent: {
  14. style: ''
  15. },
  16. newline: ''
  17. },
  18. verbatim: 'x-verbatim-espower'
  19. };
  20. var recorderClassAst = require('./power-assert-recorder.json');
  21. function AssertionVisitor (matcher, options) {
  22. this.matcher = matcher;
  23. this.options = options;
  24. this.valueRecorder = null;
  25. this.locationDetector = new EspowerLocationDetector(this.options);
  26. this.currentArgumentPath = null;
  27. this.argumentModified = false;
  28. }
  29. AssertionVisitor.prototype.enter = function (controller) {
  30. this.assertionPath = [].concat(controller.path());
  31. var currentNode = controller.current();
  32. this.canonicalCode = this.generateCanonicalCode(currentNode);
  33. this.location = this.locationDetector.locationFor(currentNode);
  34. var enclosingFunc = findEnclosingFunction(controller.parents());
  35. this.withinGenerator = enclosingFunc && enclosingFunc.generator;
  36. this.withinAsync = enclosingFunc && enclosingFunc.async;
  37. };
  38. AssertionVisitor.prototype.enterArgument = function (controller) {
  39. var currentNode = controller.current();
  40. var parentNode = getParentNode(controller);
  41. var argMatchResult = this.matcher.matchArgument(currentNode, parentNode);
  42. if (!argMatchResult) {
  43. return undefined;
  44. }
  45. if (argMatchResult.name === 'message' && argMatchResult.kind === 'optional') {
  46. // skip optional message argument
  47. return undefined;
  48. }
  49. this.verifyNotInstrumented(currentNode);
  50. // create recorder per argument
  51. this.valueRecorder = this.createNewRecorder(controller);
  52. // entering target argument
  53. this.currentArgumentPath = [].concat(controller.path());
  54. return undefined;
  55. };
  56. AssertionVisitor.prototype.leave = function (controller) {
  57. // nothing to do now
  58. };
  59. AssertionVisitor.prototype.leaveArgument = function (resultTree) {
  60. try {
  61. return this.argumentModified ? this.captureArgument(resultTree) : resultTree;
  62. } finally {
  63. this.currentArgumentPath = null;
  64. this.argumentModified = false;
  65. this.valueRecorder = null;
  66. }
  67. };
  68. AssertionVisitor.prototype.captureNode = function (controller) {
  69. this.argumentModified = true;
  70. var currentNode = controller.current();
  71. var path = controller.path();
  72. var n = newNodeWithLocationCopyOf(currentNode);
  73. var relativeEsPath = path.slice(this.assertionPath.length);
  74. return n({
  75. type: syntax.CallExpression,
  76. callee: n({
  77. type: syntax.MemberExpression,
  78. computed: false,
  79. object: this.valueRecorder,
  80. property: n({
  81. type: syntax.Identifier,
  82. name: '_capt'
  83. })
  84. }),
  85. arguments: [
  86. currentNode,
  87. n({
  88. type: syntax.Literal,
  89. value: relativeEsPath.join('/')
  90. })
  91. ]
  92. });
  93. };
  94. AssertionVisitor.prototype.toBeSkipped = function (controller) {
  95. var currentNode = controller.current();
  96. var parentNode = getParentNode(controller);
  97. var currentKey = getCurrentKey(controller);
  98. return toBeSkipped(currentNode, parentNode, currentKey);
  99. };
  100. AssertionVisitor.prototype.toBeCaptured = function (controller) {
  101. var currentNode = controller.current();
  102. var parentNode = getParentNode(controller);
  103. var currentKey = getCurrentKey(controller);
  104. return toBeCaptured(currentNode, parentNode, currentKey);
  105. };
  106. AssertionVisitor.prototype.isCapturingArgument = function () {
  107. return !!this.currentArgumentPath;
  108. };
  109. AssertionVisitor.prototype.isLeavingAssertion = function (controller) {
  110. return isPathIdentical(this.assertionPath, controller.path());
  111. };
  112. AssertionVisitor.prototype.isLeavingArgument = function (controller) {
  113. return isPathIdentical(this.currentArgumentPath, controller.path());
  114. };
  115. // internal
  116. AssertionVisitor.prototype.generateCanonicalCode = function (node) {
  117. var visitorKeys = this.options.visitorKeys;
  118. var ast = espurifyWithRaw(node);
  119. var visitor = {
  120. leave: function (currentNode, parentNode) {
  121. if (currentNode.type === syntax.Literal && typeof currentNode.raw !== 'undefined') {
  122. currentNode['x-verbatim-espower'] = {
  123. content : currentNode.raw,
  124. precedence : escodegen.Precedence.Primary
  125. };
  126. return currentNode;
  127. } else {
  128. return undefined;
  129. }
  130. }
  131. };
  132. if (visitorKeys) {
  133. visitor.keys = visitorKeys;
  134. }
  135. estraverse.replace(ast, visitor);
  136. return escodegen.generate(ast, canonicalCodeOptions);
  137. };
  138. AssertionVisitor.prototype.captureArgument = function (node) {
  139. var n = newNodeWithLocationCopyOf(node);
  140. var props = [];
  141. addLiteralTo(props, n, 'content', this.canonicalCode);
  142. addLiteralTo(props, n, 'filepath', this.location.source);
  143. addLiteralTo(props, n, 'line', this.location.line);
  144. if (this.withinAsync) {
  145. addLiteralTo(props, n, 'async', true);
  146. }
  147. if (this.withinGenerator) {
  148. addLiteralTo(props, n, 'generator', true);
  149. }
  150. return n({
  151. type: syntax.CallExpression,
  152. callee: n({
  153. type: syntax.MemberExpression,
  154. computed: false,
  155. object: this.valueRecorder,
  156. property: n({
  157. type: syntax.Identifier,
  158. name: '_expr'
  159. })
  160. }),
  161. arguments: [node].concat(n({
  162. type: syntax.ObjectExpression,
  163. properties: props
  164. }))
  165. });
  166. };
  167. AssertionVisitor.prototype.verifyNotInstrumented = function (currentNode) {
  168. if (currentNode.type !== syntax.CallExpression) {
  169. return;
  170. }
  171. if (currentNode.callee.type !== syntax.MemberExpression) {
  172. return;
  173. }
  174. var prop = currentNode.callee.property;
  175. if (prop.type === syntax.Identifier && prop.name === '_expr') {
  176. var errorMessage = 'Attempted to transform AST twice.';
  177. if (this.options.path) {
  178. errorMessage += ' path: ' + this.options.path;
  179. }
  180. throw new EspowerError(errorMessage, this.verifyNotInstrumented);
  181. }
  182. };
  183. AssertionVisitor.prototype.createNewRecorder = function (controller) {
  184. var currentBlock = findBlockedScope(this.options.scopeStack).block;
  185. var scopeBlockEspath = findEspathOfAncestorNode(currentBlock, controller);
  186. var recorderConstructorName = this.getRecorderConstructorName(controller);
  187. var recorderVariableName = this.options.transformation.generateUniqueName('rec');
  188. var currentNode = controller.current();
  189. var createNode = newNodeWithLocationCopyOf(currentNode);
  190. var ident = createNode({
  191. type: syntax.Identifier,
  192. name: recorderVariableName
  193. });
  194. var init = this.createNewExpression(createNode, recorderConstructorName);
  195. var decl = this.createVariableDeclaration(createNode, ident, init);
  196. this.options.transformation.register(scopeBlockEspath, function (matchNode) {
  197. var body;
  198. if (/Function/.test(matchNode.type)) {
  199. var blockStatement = matchNode.body;
  200. body = blockStatement.body;
  201. } else {
  202. body = matchNode.body;
  203. }
  204. insertAfterUseStrictDirective(decl, body);
  205. });
  206. return ident;
  207. };
  208. AssertionVisitor.prototype.getRecorderConstructorName = function (controller) {
  209. var ctorName = this.options.storage.powerAssertRecorderConstructorName;
  210. if (!ctorName) {
  211. ctorName = this.createRecorderClass(controller);
  212. }
  213. return ctorName;
  214. };
  215. AssertionVisitor.prototype.createRecorderClass = function (controller) {
  216. var globalScope = this.options.globalScope;
  217. var globalScopeBlockEspath = findEspathOfAncestorNode(globalScope.block, controller);
  218. var createNode = newNodeWithLocationCopyOf(globalScope.block);
  219. var ctorName = this.options.transformation.generateUniqueName('PowerAssertRecorder');
  220. var ident = createNode({
  221. type: syntax.Identifier,
  222. name: ctorName
  223. });
  224. var classDef = updateLocRecursively(espurify(recorderClassAst), createNode, this.options.visitorKeys);
  225. var decl = this.createVariableDeclaration(createNode, ident, classDef);
  226. this.options.transformation.register(globalScopeBlockEspath, function (matchNode) {
  227. insertAfterUseStrictDirective(decl, matchNode.body);
  228. });
  229. this.options.storage.powerAssertRecorderConstructorName = ctorName;
  230. return ctorName;
  231. };
  232. AssertionVisitor.prototype.createVariableDeclaration = function (createNode, ident, init) {
  233. return createNode({
  234. type: syntax.VariableDeclaration,
  235. declarations: [
  236. createNode({
  237. type: syntax.VariableDeclarator,
  238. id: ident,
  239. init: init
  240. })
  241. ],
  242. kind: 'var'
  243. });
  244. };
  245. AssertionVisitor.prototype.createNewExpression = function (createNode, constructorName) {
  246. return createNode({
  247. type: syntax.NewExpression,
  248. callee: createNode({
  249. type: syntax.Identifier,
  250. name: constructorName
  251. }),
  252. arguments: []
  253. });
  254. };
  255. function addLiteralTo (props, createNode, name, value) {
  256. if (typeof value !== 'undefined') {
  257. addToProps(props, createNode, name, createNode({
  258. type: syntax.Literal,
  259. value: value
  260. }));
  261. }
  262. }
  263. function addToProps (props, createNode, name, value) {
  264. props.push(createNode({
  265. type: syntax.Property,
  266. key: createNode({
  267. type: syntax.Identifier,
  268. name: name
  269. }),
  270. value: value,
  271. method: false,
  272. shorthand: false,
  273. computed: false,
  274. kind: 'init'
  275. }));
  276. }
  277. function updateLocRecursively (node, n, visitorKeys) {
  278. var visitor = {
  279. leave: function (currentNode, parentNode) {
  280. return n(currentNode);
  281. }
  282. };
  283. if (visitorKeys) {
  284. visitor.keys = visitorKeys;
  285. }
  286. estraverse.replace(node, visitor);
  287. return node;
  288. }
  289. function isPathIdentical (path1, path2) {
  290. if (!path1 || !path2) {
  291. return false;
  292. }
  293. return path1.join('/') === path2.join('/');
  294. }
  295. function newNodeWithLocationCopyOf (original) {
  296. return function (newNode) {
  297. if (typeof original.loc !== 'undefined') {
  298. var newLoc = {
  299. start: {
  300. line: original.loc.start.line,
  301. column: original.loc.start.column
  302. },
  303. end: {
  304. line: original.loc.end.line,
  305. column: original.loc.end.column
  306. }
  307. };
  308. if (typeof original.loc.source !== 'undefined') {
  309. newLoc.source = original.loc.source;
  310. }
  311. newNode.loc = newLoc;
  312. }
  313. if (Array.isArray(original.range)) {
  314. newNode.range = [original.range[0], original.range[1]];
  315. }
  316. return newNode;
  317. };
  318. }
  319. function findBlockedScope (scopeStack) {
  320. var lastIndex = scopeStack.length - 1;
  321. var scope = scopeStack[lastIndex];
  322. if (!scope.block || isArrowFunctionWithConciseBody(scope.block)) {
  323. return findBlockedScope(scopeStack.slice(0, lastIndex));
  324. }
  325. return scope;
  326. }
  327. function isArrowFunctionWithConciseBody (node) {
  328. return node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement';
  329. }
  330. function findEspathOfAncestorNode (targetNode, controller) {
  331. // iterate child to root
  332. var child, parent;
  333. var path = controller.path();
  334. var parents = controller.parents();
  335. var popUntilParent = function (key) {
  336. if (parent[key] !== undefined) {
  337. return;
  338. }
  339. popUntilParent(path.pop());
  340. };
  341. for (var i = parents.length - 1; i >= 0; i--) {
  342. parent = parents[i];
  343. if (child) {
  344. popUntilParent(path.pop());
  345. }
  346. if (parent === targetNode) {
  347. return path.join('/');
  348. }
  349. child = parent;
  350. }
  351. return null;
  352. }
  353. function insertAfterUseStrictDirective (decl, body) {
  354. var firstBody = body[0];
  355. if (firstBody.type === syntax.ExpressionStatement) {
  356. var expression = firstBody.expression;
  357. if (expression.type === syntax.Literal && expression.value === 'use strict') {
  358. body.splice(1,0, decl);
  359. return;
  360. }
  361. }
  362. body.unshift(decl);
  363. }
  364. function isFunction (node) {
  365. switch(node.type) {
  366. case syntax.FunctionDeclaration:
  367. case syntax.FunctionExpression:
  368. case syntax.ArrowFunctionExpression:
  369. return true;
  370. }
  371. return false;
  372. }
  373. function findEnclosingFunction (parents) {
  374. for (var i = parents.length - 1; i >= 0; i--) {
  375. if (isFunction(parents[i])) {
  376. return parents[i];
  377. }
  378. }
  379. return null;
  380. }
  381. function getParentNode (controller) {
  382. var parents = controller.parents();
  383. return parents[parents.length - 1];
  384. }
  385. function getCurrentKey (controller) {
  386. var path = controller.path();
  387. return path ? path[path.length - 1] : null;
  388. }
  389. module.exports = AssertionVisitor;