Skip to content

Commit

Permalink
Merge pull request #13 from power-assert-js/embedded-ast
Browse files Browse the repository at this point in the history
`embedAst` option to embed assertion's AST and tokens to make runtime side parser unnecessary
  • Loading branch information
twada committed May 27, 2016
2 parents e44cec4 + 1bc56ee commit 7f7219b
Show file tree
Hide file tree
Showing 33 changed files with 911 additions and 160 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ var transformed = babel.transform(jsCode, {
presets: [...],
plugins: [
createEspowerPlugin(babel, {
embedAst: true,
patterns: [
'assert.isNull(object, [message])',
'assert.same(actual, expected, [message])',
Expand All @@ -327,6 +328,7 @@ require('babel-register')({
presets: [...],
plugins: [
createEspowerPlugin(babel, {
embedAst: true,
patterns: [
'assert.isNull(object, [message])',
'assert.same(actual, expected, [message])',
Expand All @@ -343,7 +345,7 @@ require('babel-register')({
|:---------|:--------------------|
| `object` | objects shown below |
Configuration options for `babel-plugin-espower`. If not passed, default options will be used (return value of `defaultOptions()` with default `visitorKeys`, `astWhiteList`, `path`, `sourceRoot` and `sourceMap`. `visitorKeys` is value of `babel.types.VISITOR_KEYS`. `astWhiteList` is value of `babel.types.BUILDER_KEYS`. `path` is filename passed to babel. `sourceRoot` is be return value of `process.cwd()`, `sourceMap` is babel's internal SourceMap object).
Configuration options for `babel-plugin-espower`. If not passed, default options will be used (return value of `defaultOptions()` with default `embedAst`, `visitorKeys`, `astWhiteList`, `path`, `sourceRoot` and `sourceMap`. `visitorKeys` is value of `babel.types.VISITOR_KEYS`. `astWhiteList` is value of `babel.types.BUILDER_KEYS`. `path` is filename passed to babel. `sourceRoot` is be return value of `process.cwd()`, `sourceMap` is babel's internal SourceMap object).
```javascript
{
Expand All @@ -359,6 +361,7 @@ Configuration options for `babel-plugin-espower`. If not passed, default options
'assert.deepStrictEqual(actual, expected, [message])',
'assert.notDeepStrictEqual(actual, expected, [message])'
],
embedAst: false,
visitorKeys: babel.types.VISITOR_KEYS,
astWhiteList: babel.types.BUILDER_KEYS,
sourceRoot: process.cwd(),
Expand Down
148 changes: 128 additions & 20 deletions lib/babel-assertion-visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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');
Expand All @@ -24,16 +25,21 @@ function BabelAssertionVisitor (babel, matcher, options) {
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.canonicalCode = this.generateCanonicalCode(currentNode);
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, {
Expand Down Expand Up @@ -132,11 +138,100 @@ BabelAssertionVisitor.prototype.isGeneratedNode = function (nodePath) {

// internal

BabelAssertionVisitor.prototype.generateCanonicalCode = function (node) {
BabelAssertionVisitor.prototype.generateCanonicalCode = function (nodePath, node) {
var file = nodePath.hub.file;
var gen = new babelgen.CodeGenerator(node, { concise: true, comments: false });
return gen.generate().code;
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 = {
Expand All @@ -150,11 +245,22 @@ BabelAssertionVisitor.prototype.captureArgument = function (node) {
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,
t.valueToNode(props)
propsNode
]
);
define(newNode, { _generatedByEspower: true });
Expand All @@ -180,8 +286,11 @@ BabelAssertionVisitor.prototype.verifyNotInstrumented = function (currentNode) {
};

BabelAssertionVisitor.prototype.createNewRecorder = function (nodePath) {
var _this = this;
var types = this.babel.types;
var helperNameNode = this.getRecordHelperNameNode(nodePath);
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, []);
Expand All @@ -190,34 +299,33 @@ BabelAssertionVisitor.prototype.createNewRecorder = function (nodePath) {
return recorderIdent;
};

BabelAssertionVisitor.prototype.getRecordHelperNameNode = function (nodePath) {
BabelAssertionVisitor.prototype.getOrCreateNode = function (nodePath, keyName, generateNode) {
var file = nodePath.hub.file;
var helperNameNode = file.get('powerAssertRecordHelper');
if (!helperNameNode) {
helperNameNode = this.createHelperNameNode(nodePath);
var ident = file.get(keyName);
if (!ident) {
ident = this.createNode(nodePath, keyName, generateNode);
// helperNameNode = file.addImport('power-assert-runtime/recorder', 'default', 'recorder');
}
return helperNameNode;
return ident;
};

BabelAssertionVisitor.prototype.createHelperNameNode = function (nodePath) {
var types = this.babel.types;
BabelAssertionVisitor.prototype.createNode = function (nodePath, keyName, generateNode) {
var file = nodePath.hub.file;
var programScope = nodePath.scope.getProgramParent();
var helperNameNode = programScope.generateUidIdentifier('powerAssertRecorder');
define(helperNameNode, { _generatedByEspower: true });
file.set('powerAssertRecordHelper', helperNameNode);
var helperFunctionNode = types.toExpression(this.helperTemplate());
var ident = programScope.generateUidIdentifier(keyName);
define(ident, { _generatedByEspower: true });
file.set(keyName, ident);
var generatedNode = generateNode();
var visitorKeys = this.options.visitorKeys;
estraverse.traverse(helperFunctionNode, {
estraverse.traverse(generatedNode, {
keys: visitorKeys,
enter: function (node) {
define(node, { _generatedByEspower: true });
}
});
helperFunctionNode._compact = true;
programScope.push({ id: helperNameNode, init: helperFunctionNode });
return helperNameNode;
generatedNode._compact = true;
programScope.push({ id: ident, init: generatedNode });
return ident;
};

BabelAssertionVisitor.prototype.findEnclosingFunction = function (nodePath) {
Expand Down
2 changes: 0 additions & 2 deletions lib/babel-espower-visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ BabelEspowerVisitor.prototype.enter = function (nodePath) {
var currentNode = nodePath.node;
var file = nodePath.hub.file;
var assertionVisitor = file.get('espowerAssertionVisitor');
var types = this.babel.types;
if (assertionVisitor) {
if (assertionVisitor.isGeneratedNode(nodePath) || assertionVisitor.toBeSkipped(nodePath)) {
// skipping this Node
Expand Down Expand Up @@ -52,7 +51,6 @@ BabelEspowerVisitor.prototype.exit = function (nodePath) {
var resultTree = currentNode;
var file = nodePath.hub.file;
var assertionVisitor = file.get('espowerAssertionVisitor');
var types = this.babel.types;
if (!assertionVisitor) {
return;
}
Expand Down
1 change: 1 addition & 0 deletions lib/default-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

module.exports = function defaultOptions () {
return {
embedAst: false,
patterns: [
'assert(value, [message])',
'assert.ok(value, [message])',
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
"call-matcher": "^0.1.0",
"define-properties": "^1.1.2",
"espower-location-detector": "^0.1.1",
"espurify": "^1.5.1",
"estraverse": "^4.1.1",
"xtend": "^4.0.0"
},
"devDependencies": {
"babel-core": "^6.1.0",
"babel-preset-es2015": "^6.1.2",
"babel-preset-stage-3": "^6.1.2",
"babel-preset-react": "^6.1.2",
"babel-preset-stage-2": "^6.1.2",
"mocha": "^2.2.4"
},
"directories": {
Expand Down
9 changes: 7 additions & 2 deletions test/fixtures-with-presets-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ function testTransform (fixtureName, extraSuffix, extraOptions) {
var result = babel.transformFileSync(fixtureFilepath, extend({
presets: [
'es2015',
'stage-3'
'stage-2',
'react'
],
plugins: [
createEspowerPlugin(babel)
createEspowerPlugin(babel, {
embedAst: true
})
]
}, extraOptions));
var actual = result.code + '\n';
Expand Down Expand Up @@ -54,4 +57,6 @@ describe('babel-plugin-espower with presets', function () {
testTransform('Property', 'presets-es2015');
testTransform('YieldExpression', 'presets-es2015');
testTransform('AwaitExpression', 'presets-stage-3');
testTransform('ObjectRestSpread', 'presets-stage-2');
testTransform('React', 'presets-react');
});
Loading

0 comments on commit 7f7219b

Please sign in to comment.