'use strict';
|
|
var estraverse = require('estraverse');
|
var syntax = estraverse.Syntax;
|
var escope = require('escope');
|
var escallmatch = require('escallmatch');
|
var AssertionVisitor = require('./assertion-visitor');
|
var Transformation = require('./transformation');
|
var EspowerError = require('./espower-error');
|
var typeName = require('type-name');
|
var find = require('array-find');
|
var isSpreadElement = function (node) {
|
return node.type === 'SpreadElement';
|
};
|
|
|
function Instrumentor (options) {
|
verifyOptionPrerequisites(options);
|
this.options = options;
|
this.matchers = options.patterns.map(function (pattern) { return escallmatch(pattern, options); });
|
}
|
|
Instrumentor.prototype.instrument = function (ast) {
|
return estraverse.replace(ast, this.createVisitor(ast));
|
};
|
|
Instrumentor.prototype.createVisitor = function (ast) {
|
verifyAstPrerequisites(ast, this.options);
|
var that = this;
|
var assertionVisitor;
|
var storage = {};
|
var skipping = false;
|
var escopeOptions = {
|
ecmaVersion: this.options.ecmaVersion,
|
sourceType: this.options.sourceType
|
};
|
if (this.options.visitorKeys) {
|
escopeOptions.childVisitorKeys = this.options.visitorKeys;
|
}
|
var scopeManager = escope.analyze(ast, escopeOptions);
|
var globalScope = scopeManager.acquire(ast);
|
var scopeStack = [];
|
scopeStack.push(globalScope);
|
var transformation = new Transformation();
|
var visitor = {
|
enter: function (currentNode, parentNode) {
|
if (/Function/.test(currentNode.type)) {
|
scopeStack.push(scopeManager.acquire(currentNode));
|
}
|
var controller = this;
|
var path = controller.path();
|
var currentKey = path ? path[path.length - 1] : null;
|
if (assertionVisitor) {
|
if (assertionVisitor.toBeSkipped(controller)) {
|
skipping = true;
|
return controller.skip();
|
}
|
if (!assertionVisitor.isCapturingArgument() && !isCalleeOfParentCallExpression(parentNode, currentKey)) {
|
return assertionVisitor.enterArgument(controller);
|
}
|
} else if (currentNode.type === syntax.CallExpression) {
|
var matcher = find(that.matchers, function (matcher) { return matcher.test(currentNode); });
|
if (matcher) {
|
// skip modifying argument if SpreadElement appears immediately beneath assert
|
if (currentNode.arguments.some(isSpreadElement)) {
|
skipping = true;
|
return controller.skip();
|
}
|
// entering target assertion
|
assertionVisitor = new AssertionVisitor(matcher, Object.assign({
|
storage: storage,
|
transformation: transformation,
|
globalScope: globalScope,
|
scopeStack: scopeStack
|
}, that.options));
|
assertionVisitor.enter(controller);
|
return undefined;
|
}
|
}
|
return undefined;
|
},
|
leave: function (currentNode, parentNode) {
|
try {
|
var controller = this;
|
var resultTree = currentNode;
|
var path = controller.path();
|
var espath = path ? path.join('/') : '';
|
if (transformation.isTarget(espath)) {
|
transformation.apply(espath, resultTree);
|
return resultTree;
|
}
|
if (!assertionVisitor) {
|
return undefined;
|
}
|
if (skipping) {
|
skipping = false;
|
return undefined;
|
}
|
if (assertionVisitor.isLeavingAssertion(controller)) {
|
assertionVisitor.leave(controller);
|
assertionVisitor = null;
|
return undefined;
|
}
|
if (!assertionVisitor.isCapturingArgument()) {
|
return undefined;
|
}
|
if (assertionVisitor.toBeCaptured(controller)) {
|
resultTree = assertionVisitor.captureNode(controller);
|
}
|
if (assertionVisitor.isLeavingArgument(controller)) {
|
return assertionVisitor.leaveArgument(resultTree);
|
}
|
return resultTree;
|
} finally {
|
if (/Function/.test(currentNode.type)) {
|
scopeStack.pop();
|
}
|
}
|
}
|
};
|
if (this.options.visitorKeys) {
|
visitor.keys = this.options.visitorKeys;
|
}
|
return visitor;
|
};
|
|
function isCalleeOfParentCallExpression (parentNode, currentKey) {
|
return parentNode.type === syntax.CallExpression && currentKey === 'callee';
|
}
|
|
function verifyAstPrerequisites (ast, options) {
|
var errorMessage;
|
if (typeof ast.loc === 'undefined') {
|
errorMessage = 'ECMAScript AST should contain location information.';
|
if (options.path) {
|
errorMessage += ' path: ' + options.path;
|
}
|
throw new EspowerError(errorMessage, verifyAstPrerequisites);
|
}
|
}
|
|
function verifyOptionPrerequisites (options) {
|
if (options.destructive === false) {
|
throw new EspowerError('options.destructive is deprecated and always treated as destructive:true', verifyOptionPrerequisites);
|
}
|
if (typeName(options.patterns) !== 'Array') {
|
throw new EspowerError('options.patterns should be an array.', verifyOptionPrerequisites);
|
}
|
}
|
|
module.exports = Instrumentor;
|