es6-comprehensions
Version:
Transforms ES6 Array Comprehensions to ES5 compliant equivalent.
185 lines (161 loc) • 4.52 kB
JavaScript
;
var esprima = require('esprima-fb')
, recast = require('recast')
, astUtil = require('ast-util')
, types = recast.types
, b = types.builders
, nt = types.namedTypes
, NodePath = types.NodePath
, es6ForOf = require('es6-for-of');
/**
* Replaces a comprehension block `for...of` loop
* with a regular `for` loop.
*
* @param {Object} scope IIFE scope
* @param {Object} block Comprehension block
* @param {Number} idx Block index
* @param {Object} forBody Body of `for` loop
* @return {Object} ForStatement
*/
function replaceComprehensionBlock(scope, block, idx, forBody) {
if (!nt.BlockStatement.check(forBody)) {
forBody = b.blockStatement([forBody]);
}
return b.forOfStatement(
b.variableDeclaration(
'var',
[b.variableDeclarator(
b.identifier(block.left.name),
null
)]
),
block.right,
forBody
);
}
/**
* Create `arr.push(arg)` expression.
*
* @param {Object} body Argument of `push` method
* @param {Object} identifier Identifier on which call `push`
* @return {Object} Expression Statement
*/
function createPushExpression(body, identifier) {
return b.expressionStatement(
b.callExpression(
b.memberExpression(
identifier,
b.identifier('push'),
false
),
[body]
)
);
}
function visitNode(node) {
if (!nt.ComprehensionExpression.check(node)) {
return;
}
var self = this;
var iife = b.functionExpression(
null, // id
[], // params
b.blockStatement([]), // body
false, // is a generator
false // is an expression
);
var iifeScope = new NodePath(iife, this).scope;
var resultIdentifier = astUtil.uniqueIdentifier(self.scope, 'result');
var pushResultExpr = createPushExpression(node.body, resultIdentifier);
var body =
node.filter
? b.ifStatement(
node.filter, // test
b.blockStatement([pushResultExpr]) // consequent
)
: pushResultExpr;
// Explanation based on:
// http://people.mozilla.org/~jorendorff/es6-draft.html#sec-array-comprehension
//
// Array comprehension consists of body, blocks and filter.
// Body is an actual transformation performed on items.
// Blocks are for...of loops which takes items from arrays/iterator.
// Filter is the last part of the whole expression and selectes
// only items that match the conditions.
//
// All blocks (for...of loops) are transformed into nested for loops.
// The filter is checked in the innermost function.
var lastFor = null;
var blocks = node.blocks.slice().reverse();
blocks.forEach(function (block, idx) {
lastFor = replaceComprehensionBlock(
iifeScope,
block,
blocks.length - 1 - idx,
lastFor || body
);
});
// Update function body.
iife.body = b.blockStatement([
b.variableDeclaration(
'var', [
b.variableDeclarator(
resultIdentifier,
b.arrayExpression([])
),
]
),
lastFor,
b.returnStatement(
resultIdentifier
)
]);
es6ForOf.transform(new NodePath(iife, this));
// The whole array comprehension is replaced with IIFE
// that returns array.
this.replace(b.callExpression(
b.callExpression(
b.memberExpression(
iife, // function expression
b.identifier('bind'),
false
),
[b.thisExpression()]
),
[] // arguments
));
}
/**
* Transform an ES6 Esprima AST to the ES5 equivalent
* by replacing ComprehensionExpression.
*
* @param {Object} ast Esprima AST to transform
* @return {Object} Transformed AST
*/
function transform(ast) {
return types.traverse(ast, visitNode);
}
/**
* Transform JavaScript ES6 code to ES5 compliant one
* by replacing Array Comprehensions with `for` loops.
*
* @param {String} source Source code
* @param {Object} [mapOpts={}] Source map options
* @return {String}
*/
function compile(source, mapOpts) {
mapOpts || (mapOpts = {});
var recastOptions = {
esprima: esprima, // using harmony branch
sourceFileName: mapOpts.sourceFileName,
sourceMapName: mapOpts.sourceMapName,
tabWidth: require('./utils').guessTabWidth(source)
};
var ast = recast.parse(source, recastOptions);
return recast.print(transform(ast), recastOptions).code;
}
/**
* Export public API.
*/
module.exports.compile = compile;
module.exports.transform = transform;