diff options
Diffstat (limited to 'node_modules/babel-plugin-espower/lib/babel-assertion-visitor.js')
-rw-r--r-- | node_modules/babel-plugin-espower/lib/babel-assertion-visitor.js | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/node_modules/babel-plugin-espower/lib/babel-assertion-visitor.js b/node_modules/babel-plugin-espower/lib/babel-assertion-visitor.js new file mode 100644 index 000000000..6135f5766 --- /dev/null +++ b/node_modules/babel-plugin-espower/lib/babel-assertion-visitor.js @@ -0,0 +1,341 @@ +'use strict'; + +var EspowerLocationDetector = require('espower-location-detector'); +var estraverse = require('estraverse'); +var cloneWithWhitelist = require('espurify').cloneWithWhitelist; +var babelgen = require('babel-generator'); +var define = require('./define-properties'); +var toBeCaptured = require('./to-be-captured'); +var toBeSkipped = require('./to-be-skipped'); +var fs = require('fs'); +var helperCode = '(' + + fs.readFileSync(require.resolve('./power-assert-recorder.js'), 'utf8') + .split('\n') + .slice(2) + .join('\n') + + ')()'; + +function BabelAssertionVisitor (babel, matcher, options) { + this.babel = babel; + this.matcher = matcher; + this.options = options; + this.currentArgumentNodePath = null; + this.argumentModified = false; + this.valueRecorder = null; + this.locationDetector = new EspowerLocationDetector(this.options); + var babelTemplate = babel.template; + this.helperTemplate = babelTemplate(helperCode); + var whiteListWithRange = Object.keys(options.astWhiteList).reduce(function (acc, key) { + acc[key] = options.astWhiteList[key].concat(['range']); + return acc; + }, {}); + this.purifyAst = cloneWithWhitelist(whiteListWithRange); +} + +BabelAssertionVisitor.prototype.enter = function (nodePath) { + this.assertionNodePath = nodePath; + var currentNode = nodePath.node; + this.location = this.locationDetector.locationFor(currentNode); + var enclosingFunc = this.findEnclosingFunction(nodePath); + this.withinGenerator = enclosingFunc && enclosingFunc.generator; + this.withinAsync = enclosingFunc && enclosingFunc.async; + this.generateCanonicalCode(nodePath, currentNode); // should be next to enclosingFunc detection + // store original espath for each node + var visitorKeys = this.options.visitorKeys; + estraverse.traverse(currentNode, { + keys: visitorKeys, + enter: function (node) { + if (this.path()) { + var espath = this.path().join('/'); + define(node, { _espowerEspath: espath }); + } + } + }); +}; + +BabelAssertionVisitor.prototype.enterArgument = function (nodePath) { + var currentNode = nodePath.node; + var parentNode = nodePath.parent; + var argMatchResult = this.matcher.matchArgument(currentNode, parentNode); + if (!argMatchResult) { + return; + } + if (argMatchResult.name === 'message' && argMatchResult.kind === 'optional') { + // skip optional message argument + return; + } + this.verifyNotInstrumented(currentNode); + // create recorder per argument + this.valueRecorder = this.createNewRecorder(nodePath); + // entering target argument + this.currentArgumentNodePath = nodePath; +}; + +BabelAssertionVisitor.prototype.leave = function (nodePath) { + var currentNode = nodePath.node; + var visitorKeys = this.options.visitorKeys; + estraverse.traverse(currentNode, { + keys: visitorKeys, + enter: function (node) { + delete node._espowerEspath; + } + }); +}; + +BabelAssertionVisitor.prototype.leaveArgument = function (resultTree) { + try { + return this.argumentModified ? this.captureArgument(resultTree) : resultTree; + } finally { + this.currentArgumentNodePath = null; + this.argumentModified = false; + this.valueRecorder = null; + } +}; + +BabelAssertionVisitor.prototype.captureNode = function (nodePath) { + var currentNode = nodePath.node; + var t = this.babel.types; + this.argumentModified = true; + var relativeEsPath = currentNode._espowerEspath; + var newNode = t.callExpression( + t.memberExpression(this.valueRecorder, t.identifier('_capt')), + [ + currentNode, + t.valueToNode(relativeEsPath) + ]); + define(newNode, { _generatedByEspower: true }); + return newNode; +}; + +BabelAssertionVisitor.prototype.toBeSkipped = function (nodePath) { + return toBeSkipped(this.babel.types, nodePath); +}; + +BabelAssertionVisitor.prototype.toBeCaptured = function (nodePath) { + return toBeCaptured(this.babel.types, nodePath); +}; + +BabelAssertionVisitor.prototype.isArgumentModified = function () { + return !!this.argumentModified; +}; + +BabelAssertionVisitor.prototype.isCapturingArgument = function () { + return !!this.currentArgumentNodePath; +}; + +BabelAssertionVisitor.prototype.isLeavingAssertion = function (nodePath) { + return this.assertionNodePath === nodePath; +}; + +BabelAssertionVisitor.prototype.isLeavingArgument = function (nodePath) { + return this.currentArgumentNodePath === nodePath; +}; + +BabelAssertionVisitor.prototype.isGeneratedNode = function (nodePath) { + var currentNode = nodePath.node; + return !!currentNode._generatedByEspower; +}; + +// internal + +BabelAssertionVisitor.prototype.generateCanonicalCode = function (nodePath, node) { + var file = nodePath.hub.file; + var gen = new babelgen.CodeGenerator(node, { concise: true, comments: false }); + var output = gen.generate(); + this.canonicalCode = output.code; + if (!this.options.embedAst) { + return; + } + var astAndTokens = this.parseCanonicalCode(file, this.canonicalCode); + this.ast = JSON.stringify(this.purifyAst(astAndTokens.expression)); + this.tokens = JSON.stringify(astAndTokens.tokens); + var _this = this; + var types = this.babel.types; + this.visitorKeys = this.getOrCreateNode(nodePath, 'powerAssertVisitorKeys', function () { + return types.stringLiteral(JSON.stringify(_this.options.visitorKeys)); + }); +}; + +BabelAssertionVisitor.prototype.parseCanonicalCode = function (file, code) { + var ast, tokens; + + function doParse(wrapper) { + var content = wrapper ? wrapper(code) : code; + var output = file.parse(content); + if (wrapper) { + ast = output.program.body[0].body; + tokens = output.tokens.slice(6, -2); + } else { + ast = output.program; + tokens = output.tokens.slice(0, -1); + } + } + + if (this.withinAsync) { + doParse(wrappedInAsync); + } else if (this.withinGenerator) { + doParse(wrappedInGenerator); + } else { + doParse(); + } + + var exp = ast.body[0].expression; + var columnOffset = exp.loc.start.column; + var offsetTree = estraverse.replace(exp, { + keys: this.options.visitorKeys, + enter: function (eachNode) { + eachNode.range = [ + eachNode.loc.start.column - columnOffset, + eachNode.loc.end.column - columnOffset + ]; + delete eachNode.loc; + return eachNode; + } + }); + + return { + tokens: offsetAndSlimDownTokens(tokens), + expression: offsetTree + }; +}; + +function wrappedInGenerator (jsCode) { + return 'function *wrapper() { ' + jsCode + ' }'; +} + +function wrappedInAsync (jsCode) { + return 'async function wrapper() { ' + jsCode + ' }'; +} + +function offsetAndSlimDownTokens (tokens) { + var i, token, newToken, result = []; + var columnOffset; + for(i = 0; i < tokens.length; i += 1) { + token = tokens[i]; + if (i === 0) { + columnOffset = token.loc.start.column; + } + newToken = { + type: { + label: token.type.label + } + }; + if (typeof token.value !== 'undefined') { + newToken.value = token.value; + } + newToken.range = [ + token.loc.start.column - columnOffset, + token.loc.end.column - columnOffset + ]; + result.push(newToken); + } + return result; +} + +BabelAssertionVisitor.prototype.captureArgument = function (node) { + var t = this.babel.types; + var props = { + content: this.canonicalCode, + filepath: this.location.source, + line: this.location.line + }; + if (this.withinAsync) { + props.async = true; + } + if (this.withinGenerator) { + props.generator = true; + } + if (this.ast) { + props.ast = this.ast; + } + if (this.tokens) { + props.tokens = this.tokens; + } + var propsNode = t.valueToNode(props); + if (this.visitorKeys) { + var visitorKeysNode = t.objectProperty(t.identifier('visitorKeys'), this.visitorKeys); + propsNode.properties.push(visitorKeysNode); + } + var newNode = t.callExpression( + t.memberExpression(this.valueRecorder, t.identifier('_expr')), + [ + node, + propsNode + ] + ); + define(newNode, { _generatedByEspower: true }); + return newNode; +}; + +BabelAssertionVisitor.prototype.verifyNotInstrumented = function (currentNode) { + var types = this.babel.types; + if (!types.isCallExpression(currentNode)) { + return; + } + if (!types.isMemberExpression(currentNode.callee)) { + return; + } + var prop = currentNode.callee.property; + if (types.isIdentifier(prop) && prop.name === '_expr') { + var errorMessage = '[espower] Attempted to transform AST twice.'; + if (this.options.path) { + errorMessage += ' path: ' + this.options.path; + } + throw new Error(errorMessage); + } +}; + +BabelAssertionVisitor.prototype.createNewRecorder = function (nodePath) { + var _this = this; + var types = this.babel.types; + var helperNameNode = this.getOrCreateNode(nodePath, 'powerAssertRecorder', function () { + return types.toExpression(_this.helperTemplate()); + }); + var recorderIdent = nodePath.scope.generateUidIdentifier('rec'); + define(recorderIdent, { _generatedByEspower: true }); + var init = types.newExpression(helperNameNode, []); + define(init, { _generatedByEspower: true }); + nodePath.scope.push({ id: recorderIdent, init: init }); + return recorderIdent; +}; + +BabelAssertionVisitor.prototype.getOrCreateNode = function (nodePath, keyName, generateNode) { + var file = nodePath.hub.file; + var ident = file.get(keyName); + if (!ident) { + ident = this.createNode(nodePath, keyName, generateNode); + // helperNameNode = file.addImport('power-assert-runtime/recorder', 'default', 'recorder'); + } + return ident; +}; + +BabelAssertionVisitor.prototype.createNode = function (nodePath, keyName, generateNode) { + var file = nodePath.hub.file; + var programScope = nodePath.scope.getProgramParent(); + var ident = programScope.generateUidIdentifier(keyName); + define(ident, { _generatedByEspower: true }); + file.set(keyName, ident); + var generatedNode = generateNode(); + var visitorKeys = this.options.visitorKeys; + estraverse.traverse(generatedNode, { + keys: visitorKeys, + enter: function (node) { + define(node, { _generatedByEspower: true }); + } + }); + generatedNode._compact = true; + programScope.push({ id: ident, init: generatedNode }); + return ident; +}; + +BabelAssertionVisitor.prototype.findEnclosingFunction = function (nodePath) { + if (!nodePath) { + return null; + } + if (this.babel.types.isFunction(nodePath.node)) { + return nodePath.node; + } + return this.findEnclosingFunction(nodePath.parentPath); +}; + +module.exports = BabelAssertionVisitor; |