'use strict';
|
|
var estraverse = require('estraverse');
|
var escodegen = require('escodegen');
|
var espurify = require('espurify');
|
var espurifyWithRaw = espurify.customize({extra: 'raw'});
|
var syntax = estraverse.Syntax;
|
var EspowerLocationDetector = require('espower-location-detector');
|
var EspowerError = require('./espower-error');
|
var toBeSkipped = require('./rules/to-be-skipped');
|
var toBeCaptured = require('./rules/to-be-captured');
|
var canonicalCodeOptions = {
|
format: {
|
indent: {
|
style: ''
|
},
|
newline: ''
|
},
|
verbatim: 'x-verbatim-espower'
|
};
|
var recorderClassAst = require('./power-assert-recorder.json');
|
|
function AssertionVisitor (matcher, options) {
|
this.matcher = matcher;
|
this.options = options;
|
this.valueRecorder = null;
|
this.locationDetector = new EspowerLocationDetector(this.options);
|
this.currentArgumentPath = null;
|
this.argumentModified = false;
|
}
|
|
AssertionVisitor.prototype.enter = function (controller) {
|
this.assertionPath = [].concat(controller.path());
|
var currentNode = controller.current();
|
this.canonicalCode = this.generateCanonicalCode(currentNode);
|
this.location = this.locationDetector.locationFor(currentNode);
|
var enclosingFunc = findEnclosingFunction(controller.parents());
|
this.withinGenerator = enclosingFunc && enclosingFunc.generator;
|
this.withinAsync = enclosingFunc && enclosingFunc.async;
|
};
|
|
AssertionVisitor.prototype.enterArgument = function (controller) {
|
var currentNode = controller.current();
|
var parentNode = getParentNode(controller);
|
var argMatchResult = this.matcher.matchArgument(currentNode, parentNode);
|
if (!argMatchResult) {
|
return undefined;
|
}
|
if (argMatchResult.name === 'message' && argMatchResult.kind === 'optional') {
|
// skip optional message argument
|
return undefined;
|
}
|
this.verifyNotInstrumented(currentNode);
|
// create recorder per argument
|
this.valueRecorder = this.createNewRecorder(controller);
|
// entering target argument
|
this.currentArgumentPath = [].concat(controller.path());
|
return undefined;
|
};
|
|
AssertionVisitor.prototype.leave = function (controller) {
|
// nothing to do now
|
};
|
|
AssertionVisitor.prototype.leaveArgument = function (resultTree) {
|
try {
|
return this.argumentModified ? this.captureArgument(resultTree) : resultTree;
|
} finally {
|
this.currentArgumentPath = null;
|
this.argumentModified = false;
|
this.valueRecorder = null;
|
}
|
};
|
|
AssertionVisitor.prototype.captureNode = function (controller) {
|
this.argumentModified = true;
|
var currentNode = controller.current();
|
var path = controller.path();
|
var n = newNodeWithLocationCopyOf(currentNode);
|
var relativeEsPath = path.slice(this.assertionPath.length);
|
return n({
|
type: syntax.CallExpression,
|
callee: n({
|
type: syntax.MemberExpression,
|
computed: false,
|
object: this.valueRecorder,
|
property: n({
|
type: syntax.Identifier,
|
name: '_capt'
|
})
|
}),
|
arguments: [
|
currentNode,
|
n({
|
type: syntax.Literal,
|
value: relativeEsPath.join('/')
|
})
|
]
|
});
|
};
|
|
AssertionVisitor.prototype.toBeSkipped = function (controller) {
|
var currentNode = controller.current();
|
var parentNode = getParentNode(controller);
|
var currentKey = getCurrentKey(controller);
|
return toBeSkipped(currentNode, parentNode, currentKey);
|
};
|
|
AssertionVisitor.prototype.toBeCaptured = function (controller) {
|
var currentNode = controller.current();
|
var parentNode = getParentNode(controller);
|
var currentKey = getCurrentKey(controller);
|
return toBeCaptured(currentNode, parentNode, currentKey);
|
};
|
|
AssertionVisitor.prototype.isCapturingArgument = function () {
|
return !!this.currentArgumentPath;
|
};
|
|
AssertionVisitor.prototype.isLeavingAssertion = function (controller) {
|
return isPathIdentical(this.assertionPath, controller.path());
|
};
|
|
AssertionVisitor.prototype.isLeavingArgument = function (controller) {
|
return isPathIdentical(this.currentArgumentPath, controller.path());
|
};
|
|
// internal
|
|
AssertionVisitor.prototype.generateCanonicalCode = function (node) {
|
var visitorKeys = this.options.visitorKeys;
|
var ast = espurifyWithRaw(node);
|
var visitor = {
|
leave: function (currentNode, parentNode) {
|
if (currentNode.type === syntax.Literal && typeof currentNode.raw !== 'undefined') {
|
currentNode['x-verbatim-espower'] = {
|
content : currentNode.raw,
|
precedence : escodegen.Precedence.Primary
|
};
|
return currentNode;
|
} else {
|
return undefined;
|
}
|
}
|
};
|
if (visitorKeys) {
|
visitor.keys = visitorKeys;
|
}
|
estraverse.replace(ast, visitor);
|
return escodegen.generate(ast, canonicalCodeOptions);
|
};
|
|
AssertionVisitor.prototype.captureArgument = function (node) {
|
var n = newNodeWithLocationCopyOf(node);
|
var props = [];
|
addLiteralTo(props, n, 'content', this.canonicalCode);
|
addLiteralTo(props, n, 'filepath', this.location.source);
|
addLiteralTo(props, n, 'line', this.location.line);
|
if (this.withinAsync) {
|
addLiteralTo(props, n, 'async', true);
|
}
|
if (this.withinGenerator) {
|
addLiteralTo(props, n, 'generator', true);
|
}
|
return n({
|
type: syntax.CallExpression,
|
callee: n({
|
type: syntax.MemberExpression,
|
computed: false,
|
object: this.valueRecorder,
|
property: n({
|
type: syntax.Identifier,
|
name: '_expr'
|
})
|
}),
|
arguments: [node].concat(n({
|
type: syntax.ObjectExpression,
|
properties: props
|
}))
|
});
|
};
|
|
AssertionVisitor.prototype.verifyNotInstrumented = function (currentNode) {
|
if (currentNode.type !== syntax.CallExpression) {
|
return;
|
}
|
if (currentNode.callee.type !== syntax.MemberExpression) {
|
return;
|
}
|
var prop = currentNode.callee.property;
|
if (prop.type === syntax.Identifier && prop.name === '_expr') {
|
var errorMessage = 'Attempted to transform AST twice.';
|
if (this.options.path) {
|
errorMessage += ' path: ' + this.options.path;
|
}
|
throw new EspowerError(errorMessage, this.verifyNotInstrumented);
|
}
|
};
|
|
AssertionVisitor.prototype.createNewRecorder = function (controller) {
|
var currentBlock = findBlockedScope(this.options.scopeStack).block;
|
var scopeBlockEspath = findEspathOfAncestorNode(currentBlock, controller);
|
var recorderConstructorName = this.getRecorderConstructorName(controller);
|
var recorderVariableName = this.options.transformation.generateUniqueName('rec');
|
|
var currentNode = controller.current();
|
var createNode = newNodeWithLocationCopyOf(currentNode);
|
var ident = createNode({
|
type: syntax.Identifier,
|
name: recorderVariableName
|
});
|
var init = this.createNewExpression(createNode, recorderConstructorName);
|
var decl = this.createVariableDeclaration(createNode, ident, init);
|
this.options.transformation.register(scopeBlockEspath, function (matchNode) {
|
var body;
|
if (/Function/.test(matchNode.type)) {
|
var blockStatement = matchNode.body;
|
body = blockStatement.body;
|
} else {
|
body = matchNode.body;
|
}
|
insertAfterUseStrictDirective(decl, body);
|
});
|
return ident;
|
};
|
|
AssertionVisitor.prototype.getRecorderConstructorName = function (controller) {
|
var ctorName = this.options.storage.powerAssertRecorderConstructorName;
|
if (!ctorName) {
|
ctorName = this.createRecorderClass(controller);
|
}
|
return ctorName;
|
};
|
|
AssertionVisitor.prototype.createRecorderClass = function (controller) {
|
var globalScope = this.options.globalScope;
|
var globalScopeBlockEspath = findEspathOfAncestorNode(globalScope.block, controller);
|
var createNode = newNodeWithLocationCopyOf(globalScope.block);
|
var ctorName = this.options.transformation.generateUniqueName('PowerAssertRecorder');
|
var ident = createNode({
|
type: syntax.Identifier,
|
name: ctorName
|
});
|
var classDef = updateLocRecursively(espurify(recorderClassAst), createNode, this.options.visitorKeys);
|
var decl = this.createVariableDeclaration(createNode, ident, classDef);
|
this.options.transformation.register(globalScopeBlockEspath, function (matchNode) {
|
insertAfterUseStrictDirective(decl, matchNode.body);
|
});
|
this.options.storage.powerAssertRecorderConstructorName = ctorName;
|
return ctorName;
|
};
|
|
AssertionVisitor.prototype.createVariableDeclaration = function (createNode, ident, init) {
|
return createNode({
|
type: syntax.VariableDeclaration,
|
declarations: [
|
createNode({
|
type: syntax.VariableDeclarator,
|
id: ident,
|
init: init
|
})
|
],
|
kind: 'var'
|
});
|
};
|
|
AssertionVisitor.prototype.createNewExpression = function (createNode, constructorName) {
|
return createNode({
|
type: syntax.NewExpression,
|
callee: createNode({
|
type: syntax.Identifier,
|
name: constructorName
|
}),
|
arguments: []
|
});
|
};
|
|
function addLiteralTo (props, createNode, name, value) {
|
if (typeof value !== 'undefined') {
|
addToProps(props, createNode, name, createNode({
|
type: syntax.Literal,
|
value: value
|
}));
|
}
|
}
|
|
function addToProps (props, createNode, name, value) {
|
props.push(createNode({
|
type: syntax.Property,
|
key: createNode({
|
type: syntax.Identifier,
|
name: name
|
}),
|
value: value,
|
method: false,
|
shorthand: false,
|
computed: false,
|
kind: 'init'
|
}));
|
}
|
|
function updateLocRecursively (node, n, visitorKeys) {
|
var visitor = {
|
leave: function (currentNode, parentNode) {
|
return n(currentNode);
|
}
|
};
|
if (visitorKeys) {
|
visitor.keys = visitorKeys;
|
}
|
estraverse.replace(node, visitor);
|
return node;
|
}
|
|
function isPathIdentical (path1, path2) {
|
if (!path1 || !path2) {
|
return false;
|
}
|
return path1.join('/') === path2.join('/');
|
}
|
|
function newNodeWithLocationCopyOf (original) {
|
return function (newNode) {
|
if (typeof original.loc !== 'undefined') {
|
var newLoc = {
|
start: {
|
line: original.loc.start.line,
|
column: original.loc.start.column
|
},
|
end: {
|
line: original.loc.end.line,
|
column: original.loc.end.column
|
}
|
};
|
if (typeof original.loc.source !== 'undefined') {
|
newLoc.source = original.loc.source;
|
}
|
newNode.loc = newLoc;
|
}
|
if (Array.isArray(original.range)) {
|
newNode.range = [original.range[0], original.range[1]];
|
}
|
return newNode;
|
};
|
}
|
|
function findBlockedScope (scopeStack) {
|
var lastIndex = scopeStack.length - 1;
|
var scope = scopeStack[lastIndex];
|
if (!scope.block || isArrowFunctionWithConciseBody(scope.block)) {
|
return findBlockedScope(scopeStack.slice(0, lastIndex));
|
}
|
return scope;
|
}
|
|
function isArrowFunctionWithConciseBody (node) {
|
return node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement';
|
}
|
|
function findEspathOfAncestorNode (targetNode, controller) {
|
// iterate child to root
|
var child, parent;
|
var path = controller.path();
|
var parents = controller.parents();
|
var popUntilParent = function (key) {
|
if (parent[key] !== undefined) {
|
return;
|
}
|
popUntilParent(path.pop());
|
};
|
for (var i = parents.length - 1; i >= 0; i--) {
|
parent = parents[i];
|
if (child) {
|
popUntilParent(path.pop());
|
}
|
if (parent === targetNode) {
|
return path.join('/');
|
}
|
child = parent;
|
}
|
return null;
|
}
|
|
function insertAfterUseStrictDirective (decl, body) {
|
var firstBody = body[0];
|
if (firstBody.type === syntax.ExpressionStatement) {
|
var expression = firstBody.expression;
|
if (expression.type === syntax.Literal && expression.value === 'use strict') {
|
body.splice(1,0, decl);
|
return;
|
}
|
}
|
body.unshift(decl);
|
}
|
|
function isFunction (node) {
|
switch(node.type) {
|
case syntax.FunctionDeclaration:
|
case syntax.FunctionExpression:
|
case syntax.ArrowFunctionExpression:
|
return true;
|
}
|
return false;
|
}
|
|
function findEnclosingFunction (parents) {
|
for (var i = parents.length - 1; i >= 0; i--) {
|
if (isFunction(parents[i])) {
|
return parents[i];
|
}
|
}
|
return null;
|
}
|
|
function getParentNode (controller) {
|
var parents = controller.parents();
|
return parents[parents.length - 1];
|
}
|
|
function getCurrentKey (controller) {
|
var path = controller.path();
|
return path ? path[path.length - 1] : null;
|
}
|
|
module.exports = AssertionVisitor;
|