Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

embedAst option to embed assertion's AST and tokens to make runtime side parser unnecessary #13

Merged
merged 18 commits into from
May 27, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a49334c
test(babel-plugin-espower): add testcase for React testing
twada Mar 7, 2016
246dc8d
feat(babel-plugin-espower): embed assertion's AST and tokens to make …
twada Mar 7, 2016
d06c107
feat(babel-plugin-espower): embed AST visitor keys to make runtime si…
twada Mar 7, 2016
fd530cb
test(babel-plugin-espower): add expected output for React testing hav…
twada Mar 7, 2016
1be6b6e
refactor(babel-plugin-espower): embed visitorKeys once per file
twada Mar 15, 2016
bdcbedd
refactor(babel-plugin-espower): eliminate duplication
twada Mar 17, 2016
83c6b41
refactor(babel-plugin-espower): eliminate duplication
twada Mar 17, 2016
fdd33a4
refactor(babel-plugin-espower): remove unused variables
twada Mar 17, 2016
28ad187
feat(babel-plugin-espower): make locations of embedded ast and tokens…
twada Mar 20, 2016
86dc6a7
refactor(babel-plugin-espower): inline slimDown function
twada Mar 20, 2016
2af72c8
test(babel-plugin-espower): update expected output for YieldExpressio…
twada Mar 21, 2016
8498304
chore(package): update espurify to 1.5.1, the faster version
twada Mar 29, 2016
a14b940
test(babel-plugin-espower): maint whole test suite
twada Apr 19, 2016
9d971fe
feat(babel-plugin-espower): introduce `embedAst` option to enable emb…
twada May 21, 2016
0c4ff3a
test(babel-plugin-espower): maint tests of `embedAst: false` cases
twada May 21, 2016
baa9329
docs(README): about `embedAst` option
twada May 21, 2016
c3310bb
test(babel-plugin-espower): test for Object Rest/Spread Properties
twada May 21, 2016
1bc56ee
chore(babel-plugin-espower): dealing with undefined token value
twada May 24, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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