diff --git a/visitors/__tests__/es6-spread-operator-visitors-test.js b/visitors/__tests__/es6-spread-operator-visitors-test.js new file mode 100644 index 0000000..241a40a --- /dev/null +++ b/visitors/__tests__/es6-spread-operator-visitors-test.js @@ -0,0 +1,252 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @emails dmitrys@fb.com javascript@lists.facebook.com + */ + +/*jshint evil:true, unused: false*/ +/* global describe, beforeEach, expect, it*/ + + + +describe('es6-spread-operator-visitors', function() { + var fs = require('fs'), path = require('path'), + runtime = fs.readFileSync(path.join(__dirname, '..', 'es6-spread-operator-runtime.js'), 'utf-8'); + + var visitors, + transformFn; + + beforeEach(function() { + visitors = require('../es6-spread-operator-visitors').visitorList; + transformFn = require('../../src/jstransform').transform; + }); + + function transform(code, options) { + return transformFn(visitors, code, options).code; + } + + function expectTransform(code, result, options) { + expect(transform(code, options)).toEqual(result); + } + + describe('runtime', function () { + it('should be included if the options \'includeSpreadRuntime \' is set to true ', function () { + expectTransform('', runtime, { includeSpreadRuntime: true }); + }); + + it('should not be included otherwise', function () { + expectTransform('', '', {}); + }); + }); + + + describe('within array', function () { + it('should create an array concatanation of each object in array, and each parameters ', function () { + expect(eval(transform('[1, 2, ...[3, 4]]', { includeSpreadRuntime: true }))).toEqual([1, 2, 3, 4]); + }); + + it('should works with only spread', function () { + expect(eval(transform('[...[1, 2]]', { includeSpreadRuntime: true }))).toEqual([1, 2]); + }); + + it('should throws an error if spread a non object ', function () { + expect(function () { + eval(transform('[1, 2, ...undefined]')); + }).toThrow(); + }); + + it('should throws an error if passing a non array', function () { + expect(function () { + eval(transform('[1, 2, ...{ a: 5 }]')); + }).toThrow(); + }); + + it('should accept anything that resolve to an array', function () { + expect(eval(transform('[1, 2, ...(function () { return [3, 4] })()]', { includeSpreadRuntime: true }))).toEqual([1, 2, 3, 4]); + }); + + it('it should not spread elements without spread operator', function () { + expect(eval(transform('[[1,2], ...[3, 4]]', { includeSpreadRuntime: true }))).toEqual([[1, 2], 3, 4]); + }); + + it('should ouput the following code source', function () { + expectTransform( + '[1, 2, ...[3, 4]]', + [ + 'Array.prototype.concat.call(', + '[1, 2], ____JSTRANSFORM_SPREAD_RUNTIME____.assertSpreadElement([3, 4])', + ')', + ].join('')); + }); + + + it('should keep lines break and comments', function () { + expectTransform( + ['[1 /*mycomments*/, 2,', + '...[3,', + ' 4]]' + ].join('\n'), + [ + 'Array.prototype.concat.call([1 /*mycomments*/, 2],', + '____JSTRANSFORM_SPREAD_RUNTIME____.assertSpreadElement([3,', + ' 4]))' + ].join('\n')); + }); + }); + + + describe('within call expression', function () { + + function returnArgs () { + return Array.prototype.slice.call(arguments); + } + + + it('should pass spread array as parameters', function () { + expect(eval(transform('returnArgs(1, 2, ...[3, 4])', { includeSpreadRuntime: true }))).toEqual([1, 2, 3, 4]); + }); + + it('should ouput the following code source', function () { + expectTransform( + 'returnArgs(1, 2, ...[3, 4])', + [ + 'returnArgs.apply(undefined, Array.prototype.concat.call(', + '[1, 2], ____JSTRANSFORM_SPREAD_RUNTIME____.assertSpreadElement([3, 4])', + '))' + ].join('')); + }); + + + it('should keep lines break and comments', function () { + expectTransform( + [ + 'returnArgs /*comments*/(', + ' 1, 2,', + ' ...[3, 4]', + ')' + ].join('\n'), + [ + 'returnArgs.apply(undefined, /*comments*/Array.prototype.concat.call(', + ' [1, 2],', + ' ____JSTRANSFORM_SPREAD_RUNTIME____.assertSpreadElement([3, 4])', + '))' + ].join('\n')); + }); + + it('should keep intact comments with \'(\' && \')\' ', function () { + expectTransform( + [ + 'returnArgs /*comments (*/( 1, 2,', + '...[3, 4] //comments )', + ')' + ].join('\n'), + [ + 'returnArgs.apply(undefined, /*comments (*/Array.prototype.concat.call( [1, 2],', + '____JSTRANSFORM_SPREAD_RUNTIME____.assertSpreadElement([3, 4]) //comments )', + '))' + ].join('\n')); + }); + }); + + + describe('within method call expression', function () { + + var object = { + returnArgsAndThis: function() { + return Array.prototype.slice.call(arguments).concat(this); + } + }; + + + it('should keep the \'this\' context in case of method call ', function () { + expect(eval(transform('object.returnArgsAndThis(1, 2, ...[3, 4])', { includeSpreadRuntime: true }))).toEqual([1, 2, 3, 4, object]); + }); + + it('should keep the \'this\' context in case of computed method call ', function () { + expect(eval(transform('object[\'return\'+\'ArgsAndThis\'](1, 2, ...[3, 4])', { includeSpreadRuntime: true }))).toEqual([1, 2, 3, 4, object]); + }); + + + it('should ouput the following code source', function () { + var transformedCode = transform('object.returnArgsAndThis(1, 2,...[3, 4])'); + transformedCode = transformedCode.replace(/_this\d*/g, '_this'); + expect(transformedCode).toBe([ + '(function() { ', + 'var _this = object; ', + 'return _this.returnArgsAndThis.apply(_this, Array.prototype.concat.call(', + '[1, 2],____JSTRANSFORM_SPREAD_RUNTIME____.assertSpreadElement([3, 4])', + '))', + '})()' + ].join('')); + }); + + + }); + + + describe('within new expression', function () { + + function MyClass(a, b) { + this.a = a; + this.b = b; + } + + function FakeClass(a) { + return a; + } + + it('should pass spread array as arguments of the construtor, and produce an object instance of called function', function () { + var result = eval(transform('new MyClass(...[1, 2])', { includeSpreadRuntime: true })); + expect(result).toEqual({ + a: 1, + b: 2 + }); + expect(result instanceof MyClass).toBe(true); + }); + + it('should return the function return value if the function has one', function () { + expect(eval(transform('new FakeClass(...[1, 2])', { includeSpreadRuntime: true }))).toBe(1); + expect(eval(transform('new FakeClass(...[null])', { includeSpreadRuntime: true }))).toBe(null); + }); + + + it('should ouput the following code source', function () { + var transformedCode = transform('new MyClass(...[1, 2])'); + transformedCode = transformedCode.replace(/_result\d*/g, '_result'); + transformedCode = transformedCode.replace(/_class\d*/g, '_class'); + expect(transformedCode).toBe([ + '____JSTRANSFORM_SPREAD_RUNTIME____.executeNewExpression(MyClass, ', + 'Array.prototype.concat.call(____JSTRANSFORM_SPREAD_RUNTIME____.assertSpreadElement([1, 2]))', + ')' + ].join('')); + }); + + it('should keep new lines and comments', function () { + var transformedCode = transform('/*hello world (*/ new /*hello*/\nMyClass(\n /*comments*/ ...[1//comment\n, 2])'); + transformedCode = transformedCode.replace(/_result\d*/g, '_result'); + transformedCode = transformedCode.replace(/_class\d*/g, '_class'); + expect(transformedCode).toBe([ + '/*hello world (*/ /*hello*/', + '____JSTRANSFORM_SPREAD_RUNTIME____.executeNewExpression(MyClass, Array.prototype.concat.call(', + ' /*comments*/ ____JSTRANSFORM_SPREAD_RUNTIME____.assertSpreadElement([1//comment', + ', 2])))' + ].join('\n')); + }); + }); +}); + + diff --git a/visitors/es6-spread-operator-runtime.js b/visitors/es6-spread-operator-runtime.js new file mode 100644 index 0000000..2176fc0 --- /dev/null +++ b/visitors/es6-spread-operator-runtime.js @@ -0,0 +1,29 @@ +(function (global) { + + function assertSpreadElement(array) { + if (Array.isArray(array)) { + return array; + } + throw new TypeError(array + ' is not an array'); + } + + function executeNewExpression(func, args) { + var result = Object.create(func.prototype); + var funcResult = func.apply(result, args); + return typeof funcResult === 'undefined' ? result : funcResult; + } + + global.____JSTRANSFORM_SPREAD_RUNTIME____ = { + assertSpreadElement: assertSpreadElement, + executeNewExpression: executeNewExpression + }; +})((function () { + if (typeof window !== 'undefined') { + return window; + } else if (typeof global !== 'undefined') { + return global; + } else if (typeof self !== 'undefined') { + return self; + } + return this; +})()); diff --git a/visitors/es6-spread-operator-visitors.js b/visitors/es6-spread-operator-visitors.js new file mode 100644 index 0000000..931c255 --- /dev/null +++ b/visitors/es6-spread-operator-visitors.js @@ -0,0 +1,189 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*jslint node:true*/ + +/** + * Desugars ES6 spread operator into ES5 (and ES3 with ES5-shim) equivalent expression + * + * [1, 2, 3, ...[4, 5]] + * is transformed into an expression equivalent to : + * [1, 2, 3, 4, 5] + * + * myFunction(...[1, 2]) + * is transformed in an expression equivalent to : + * myFunction(1, 2) + * + * myObject.myMethod(…[1, 2]) + * is transformed in an expression equivalent to : + * myObject.myMethod(1, 2) + * + * new MyClass(...[1, 2]) + * is transformed in an expression equivalent to : + * new MyClass(1, 2) + * + * + * works only with arrays (no 'iterable object') + */ +var Syntax = require('esprima-fb').Syntax; +var utils = require('../src/utils'); +var fs = require('fs'); +var path = require('path'); +var runtimeCode = fs.readFileSync(path.join(__dirname, 'es6-spread-operator-runtime.js'), 'utf-8'); +var runtime = '____JSTRANSFORM_SPREAD_RUNTIME____'; + +function hasSpread(elements) { + return elements && + elements.some(function (node) { + return node.type === Syntax.SpreadElement; + }); +} + +function generateIdent(base) { + return base + (Math.random() * 1e9 >>> 0); +} + +function replaceInNonComments(search, replace) { + return function (source) { + var result = '', inBlockComment = false, inLineComment = false; + while (source) { + var char = source.charAt(0); + source = source.substr(1); + if (inBlockComment) { + if (char === '*' && source.charAt(0) === '/') { + inBlockComment = false; + } + } else if (inLineComment) { + if (char === '\n') { + inLineComment = false; + } + } else if (char === '/') { + var next = source.charAt(0); + if (next === '*') { + inBlockComment = true; + } else if (next === '/') { + inLineComment = true; + } + } + + if(char === search && !inBlockComment && !inLineComment) { + result += replace; + } else { + result += char; + } + } + return result; + }; +} + +function insertElementsWithSpread(elements, state) { + var insideBrackets = false; + elements.forEach(function (node) { + if (node.type === Syntax.SpreadElement) { + if (insideBrackets) { + utils.append(']', state); + insideBrackets = false; + } + utils.catchup(node.range[0], state); + utils.append(runtime + '.assertSpreadElement(', state); + utils.move(node.range[0] + 3, state); // remove ... + utils.catchup(node.range[1], state); + utils.append(')', state); + } else { + if (!insideBrackets) { + utils.append('[', state); + insideBrackets = true; + } + utils.catchup(node.range[1], state); + } + }); + if (insideBrackets) { + utils.append(']', state); + } +} + + +function visitProgram(traverse, node, path, state) { + if (state.g.opts.includeSpreadRuntime) { + utils.append(runtimeCode, state); + } +} +visitProgram.test = function(node) { + return node.type === Syntax.Program; +}; + +function visitArrayExpressionWithSpreadElement(traverse, node, path, state) { + utils.catchup(node.elements[0].range[0], state, + replaceInNonComments('[', 'Array.prototype.concat.call(')); + insertElementsWithSpread(node.elements, state); + utils.catchup(node.range[1], state, replaceInNonComments(']', ')')); +} + +visitArrayExpressionWithSpreadElement.test = function (node) { + return node.type === Syntax.ArrayExpression && hasSpread(node.elements); +}; + + +function visitFunctionCallWithSpreadElement(traverse, node, path, state) { + var thisIdent = 'undefined'; + if (node.callee.type === Syntax.MemberExpression) { + thisIdent = generateIdent('_this'); + utils.append('(function() { ', state); + utils.append('var ' + thisIdent + ' = ', state); + utils.catchup(node.callee.object.range[1], state); + utils.append('; return '+ thisIdent , state); + } + + utils.catchup(node.callee.range[1], state); + utils.append('.apply(' + thisIdent + ', ', state); + + utils.catchup(node.arguments[0].range[0], state, replaceInNonComments('(', 'Array.prototype.concat.call(')); + insertElementsWithSpread(node.arguments, state); + utils.catchup(node.range[1], state); + + utils.append(')', state); + + if (node.callee.type === Syntax.MemberExpression) { + utils.append('})()', state); + } +} + +visitFunctionCallWithSpreadElement.test = function (node) { + return node.type === Syntax.CallExpression && hasSpread(node.arguments); +}; + + +function visitNewExpressionWithSpreadElement(traverse, node, path, state) { + utils.move(node.range[0] + 4 , state); //remove 'new ' + utils.catchup(node.callee.range[0], state); + utils.append(runtime + '.executeNewExpression(', state); + utils.catchup(node.callee.range[1], state); + utils.catchup(node.arguments[0].range[0], state, replaceInNonComments('(', ', Array.prototype.concat.call(')); + insertElementsWithSpread(node.arguments, state); + utils.catchup(node.range[1], state); + utils.append(')', state); +} + +visitNewExpressionWithSpreadElement.test = function (node) { + return node.type === Syntax.NewExpression && hasSpread(node.arguments); +}; + +exports.visitorList = [ + visitProgram, + visitArrayExpressionWithSpreadElement, + visitFunctionCallWithSpreadElement, + visitNewExpressionWithSpreadElement +];