instrumentor.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. 'use strict';
  2. var estraverse = require('estraverse');
  3. var syntax = estraverse.Syntax;
  4. var escope = require('escope');
  5. var escallmatch = require('escallmatch');
  6. var AssertionVisitor = require('./assertion-visitor');
  7. var Transformation = require('./transformation');
  8. var EspowerError = require('./espower-error');
  9. var typeName = require('type-name');
  10. var find = require('array-find');
  11. var isSpreadElement = function (node) {
  12. return node.type === 'SpreadElement';
  13. };
  14. function Instrumentor (options) {
  15. verifyOptionPrerequisites(options);
  16. this.options = options;
  17. this.matchers = options.patterns.map(function (pattern) { return escallmatch(pattern, options); });
  18. }
  19. Instrumentor.prototype.instrument = function (ast) {
  20. return estraverse.replace(ast, this.createVisitor(ast));
  21. };
  22. Instrumentor.prototype.createVisitor = function (ast) {
  23. verifyAstPrerequisites(ast, this.options);
  24. var that = this;
  25. var assertionVisitor;
  26. var storage = {};
  27. var skipping = false;
  28. var escopeOptions = {
  29. ecmaVersion: this.options.ecmaVersion,
  30. sourceType: this.options.sourceType
  31. };
  32. if (this.options.visitorKeys) {
  33. escopeOptions.childVisitorKeys = this.options.visitorKeys;
  34. }
  35. var scopeManager = escope.analyze(ast, escopeOptions);
  36. var globalScope = scopeManager.acquire(ast);
  37. var scopeStack = [];
  38. scopeStack.push(globalScope);
  39. var transformation = new Transformation();
  40. var visitor = {
  41. enter: function (currentNode, parentNode) {
  42. if (/Function/.test(currentNode.type)) {
  43. scopeStack.push(scopeManager.acquire(currentNode));
  44. }
  45. var controller = this;
  46. var path = controller.path();
  47. var currentKey = path ? path[path.length - 1] : null;
  48. if (assertionVisitor) {
  49. if (assertionVisitor.toBeSkipped(controller)) {
  50. skipping = true;
  51. return controller.skip();
  52. }
  53. if (!assertionVisitor.isCapturingArgument() && !isCalleeOfParentCallExpression(parentNode, currentKey)) {
  54. return assertionVisitor.enterArgument(controller);
  55. }
  56. } else if (currentNode.type === syntax.CallExpression) {
  57. var matcher = find(that.matchers, function (matcher) { return matcher.test(currentNode); });
  58. if (matcher) {
  59. // skip modifying argument if SpreadElement appears immediately beneath assert
  60. if (currentNode.arguments.some(isSpreadElement)) {
  61. skipping = true;
  62. return controller.skip();
  63. }
  64. // entering target assertion
  65. assertionVisitor = new AssertionVisitor(matcher, Object.assign({
  66. storage: storage,
  67. transformation: transformation,
  68. globalScope: globalScope,
  69. scopeStack: scopeStack
  70. }, that.options));
  71. assertionVisitor.enter(controller);
  72. return undefined;
  73. }
  74. }
  75. return undefined;
  76. },
  77. leave: function (currentNode, parentNode) {
  78. try {
  79. var controller = this;
  80. var resultTree = currentNode;
  81. var path = controller.path();
  82. var espath = path ? path.join('/') : '';
  83. if (transformation.isTarget(espath)) {
  84. transformation.apply(espath, resultTree);
  85. return resultTree;
  86. }
  87. if (!assertionVisitor) {
  88. return undefined;
  89. }
  90. if (skipping) {
  91. skipping = false;
  92. return undefined;
  93. }
  94. if (assertionVisitor.isLeavingAssertion(controller)) {
  95. assertionVisitor.leave(controller);
  96. assertionVisitor = null;
  97. return undefined;
  98. }
  99. if (!assertionVisitor.isCapturingArgument()) {
  100. return undefined;
  101. }
  102. if (assertionVisitor.toBeCaptured(controller)) {
  103. resultTree = assertionVisitor.captureNode(controller);
  104. }
  105. if (assertionVisitor.isLeavingArgument(controller)) {
  106. return assertionVisitor.leaveArgument(resultTree);
  107. }
  108. return resultTree;
  109. } finally {
  110. if (/Function/.test(currentNode.type)) {
  111. scopeStack.pop();
  112. }
  113. }
  114. }
  115. };
  116. if (this.options.visitorKeys) {
  117. visitor.keys = this.options.visitorKeys;
  118. }
  119. return visitor;
  120. };
  121. function isCalleeOfParentCallExpression (parentNode, currentKey) {
  122. return parentNode.type === syntax.CallExpression && currentKey === 'callee';
  123. }
  124. function verifyAstPrerequisites (ast, options) {
  125. var errorMessage;
  126. if (typeof ast.loc === 'undefined') {
  127. errorMessage = 'ECMAScript AST should contain location information.';
  128. if (options.path) {
  129. errorMessage += ' path: ' + options.path;
  130. }
  131. throw new EspowerError(errorMessage, verifyAstPrerequisites);
  132. }
  133. }
  134. function verifyOptionPrerequisites (options) {
  135. if (options.destructive === false) {
  136. throw new EspowerError('options.destructive is deprecated and always treated as destructive:true', verifyOptionPrerequisites);
  137. }
  138. if (typeName(options.patterns) !== 'Array') {
  139. throw new EspowerError('options.patterns should be an array.', verifyOptionPrerequisites);
  140. }
  141. }
  142. module.exports = Instrumentor;