diff --git a/src/program/types/ArrayExpression.js b/src/program/types/ArrayExpression.js index bf363bd7..6565f59f 100644 --- a/src/program/types/ArrayExpression.js +++ b/src/program/types/ArrayExpression.js @@ -1,5 +1,5 @@ import Node from '../Node.js'; -import spread, { isArguments } from '../../utils/spread.js'; +import spread, { isArguments, inlineSpreads } from '../../utils/spread.js'; export default class ArrayExpression extends Node { initialise(transforms) { @@ -26,6 +26,7 @@ export default class ArrayExpression extends Node { super.transpile(code, transforms); if (transforms.spreadRest) { + inlineSpreads(code, this, this.elements); // erase trailing comma after last array element if not an array hole if (this.elements.length) { const lastElement = this.elements[this.elements.length - 1]; diff --git a/src/program/types/CallExpression.js b/src/program/types/CallExpression.js index 3671f526..1e0eee4a 100644 --- a/src/program/types/CallExpression.js +++ b/src/program/types/CallExpression.js @@ -1,5 +1,5 @@ import Node from '../Node.js'; -import spread, { isArguments } from '../../utils/spread.js'; +import spread, { isArguments, inlineSpreads } from '../../utils/spread.js'; import removeTrailingComma from '../../utils/removeTrailingComma.js'; export default class CallExpression extends Node { @@ -20,6 +20,11 @@ export default class CallExpression extends Node { } transpile(code, transforms) { + if (transforms.spreadRest && this.arguments.length) { + inlineSpreads(code, this, this.arguments); + // this.arguments.length may have changed, must retest. + } + if (transforms.spreadRest && this.arguments.length) { let hasSpreadElements = false; let context; diff --git a/src/program/types/NewExpression.js b/src/program/types/NewExpression.js index ccf9b8da..747749d7 100644 --- a/src/program/types/NewExpression.js +++ b/src/program/types/NewExpression.js @@ -1,5 +1,5 @@ import Node from '../Node.js'; -import spread, { isArguments } from '../../utils/spread.js'; +import spread, { isArguments, inlineSpreads } from '../../utils/spread.js'; import removeTrailingComma from '../../utils/removeTrailingComma.js'; export default class NewExpression extends Node { @@ -23,6 +23,11 @@ export default class NewExpression extends Node { transpile(code, transforms) { super.transpile(code, transforms); + if (transforms.spreadRest && this.arguments.length) { + inlineSpreads(code, this, this.arguments); + // this.arguments.length may have changed, must retest. + } + if (transforms.spreadRest && this.arguments.length) { const firstArgument = this.arguments[0]; const isNew = true; diff --git a/src/program/types/ObjectExpression.js b/src/program/types/ObjectExpression.js index 06a09faa..151de7e9 100644 --- a/src/program/types/ObjectExpression.js +++ b/src/program/types/ObjectExpression.js @@ -14,8 +14,35 @@ export default class ObjectExpression extends Node { for (let i = 0; i < this.properties.length; ++i) { const prop = this.properties[i]; if (prop.type === 'SpreadElement') { - spreadPropertyCount += 1; - if (firstSpreadProperty === null) firstSpreadProperty = i; + // First see if we can inline the spread, to save needing objectAssign. + const argument = prop.argument; + if ( + argument.type === 'ObjectExpression' || ( + argument.type === 'Literal' && + typeof argument.value !== 'string' + ) + ) { + if (argument.type === 'ObjectExpression' && argument.properties.length > 0) { + // Strip the `...{` and the `}` with a possible trailing comma before it, + // leaving just the possible trailing comma after it. + code.remove(prop.start, argument.properties[0].start); + code.remove(argument.properties[argument.properties.length - 1].end, prop.end); + this.properties.splice(i, 1, ...argument.properties); + i--; + } else { + // An empty object, boolean, null, undefined, number or regexp (but NOT + // string) will spread to nothing, so just remove the element altogether, + // including a possible trailing comma. + code.remove(prop.start, i === this.properties.length - 1 + ? prop.end + : this.properties[i + 1].start); + this.properties.splice(i, 1); + i--; + } + } else { + spreadPropertyCount += 1; + if (firstSpreadProperty === null) firstSpreadProperty = i; + } } else if (prop.computed && transforms.computedProperty) { computedPropertyCount += 1; if (firstComputedProperty === null) firstComputedProperty = i; diff --git a/src/utils/spread.js b/src/utils/spread.js index ce2a10cc..bfca294f 100644 --- a/src/utils/spread.js +++ b/src/utils/spread.js @@ -2,6 +2,59 @@ export function isArguments(node) { return node.type === 'Identifier' && node.name === 'arguments'; } +export function inlineSpreads( + code, + node, + elements +) { + let i = elements.length; + + while (i--) { + const element = elements[i]; + if (!element || element.type !== 'SpreadElement') { + continue; + } + const argument = element.argument; + if (argument.type !== 'ArrayExpression') { + continue; + } + const subelements = argument.elements; + if (subelements.some(subelement => subelement === null)) { + // Not even going to try inlining spread arrays with holes. + // It's a lot of work (got to be VERY careful in comma counting for + // ArrayExpression, and turn blanks into undefined for + // CallExpression and NewExpression), and probably literally no one + // would ever benefit from it. + continue; + } + // We can inline it: drop the `...[` and `]` and sort out any commas. + const isLast = i === elements.length - 1; + if (subelements.length === 0) { + code.remove( + isLast && i !== 0 + ? elements[i - 1].end // Take the previous comma too + : element.start, + isLast + ? node.end - 1 // Must remove trailing comma; element.end wouldn’t + : elements[i + 1].start); + } else { + // Strip the `...[` and the `]` with a possible trailing comma before it, + // leaving just the possible trailing comma after it. + code.remove(element.start, subelements[0].start); + code.remove( + // Strip a possible trailing comma after the last element + subelements[subelements.length - 1].end, + // And also a possible trailing comma after the spread + isLast + ? node.end - 1 + : element.end + ); + } + elements.splice(i, 1, ...subelements); + i += subelements.length; + } +} + export default function spread( code, elements, diff --git a/test/samples/computed-properties.js b/test/samples/computed-properties.js index e0c55ede..b0955d38 100644 --- a/test/samples/computed-properties.js +++ b/test/samples/computed-properties.js @@ -238,7 +238,7 @@ module.exports = [ let a = { [foo] (x, y) { return { - ...{abc: '123'} + ...c }; }, }; @@ -246,7 +246,7 @@ module.exports = [ output: ` var a = {}; a[foo] = function (x, y) { - return Object.assign({}, {abc: '123'}); + return Object.assign({}, c); }; ` }, diff --git a/test/samples/object-rest-spread.js b/test/samples/object-rest-spread.js index abc319ec..6716d618 100644 --- a/test/samples/object-rest-spread.js +++ b/test/samples/object-rest-spread.js @@ -277,6 +277,61 @@ for( var a = c.a, rest = objectWithoutProperties( c, ["a"] ), b = rest;; ) {}` input: `for( var {...b} = c;; ) {}`, output: `function objectWithoutProperties (obj, exclude) { var target = {}; for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k) && exclude.indexOf(k) === -1) target[k] = obj[k]; return target; } for( var rest = objectWithoutProperties( c, [] ), b = rest;; ) {}` - } + }, + + { + description: 'inlines object spread with one object', + input: `var obj = {...{a: 1}};`, + output: `var obj = {a: 1};` + }, + + { + description: 'inlines object spread with two objects', + input: `var obj = {...{a: 1}, ...{b: 2}};`, + output: `var obj = {a: 1, b: 2};` + }, + { + description: 'inlines object spread with regular keys in between', + input: `var obj = { ...{a: 1}, b: 2, c: 3 };`, + output: `var obj = { a: 1, b: 2, c: 3 };` + }, + + { + description: 'inlines object spread mixed', + input: `var obj = { ...{a: 1}, b: 2, ...{c: 3}, e};`, + output: `var obj = { a: 1, b: 2, c: 3, e: e};` + }, + + { + description: 'inlines object spread very mixed', + options: { + objectAssign: 'Object.assign' + }, + input: `var obj = { ...{a: 1}, b: 2, ...c, e};`, + output: `var obj = Object.assign({}, {a: 1, b: 2}, c, {e: e});` + }, + + { + description: 'inlines object spread without extraneous trailing commas', + options: { + objectAssign: 'Object.assign' + }, + input: ` + var obj = { ...{a: 1,}, b: 2, ...{c: 3,}, ...d, e, ...{f: 6,},}; + obj = { a: 1, b: 2, }; + obj = { a: 1, ...{b: 2} }; + obj = { a: 1, ...{b: 2,} }; + obj = { a: 1, ...{b: 2}, }; + obj = { a: 1, ...{b: 2,}, }; + `, + output: ` + var obj = Object.assign({}, {a: 1, b: 2, c: 3}, d, {e: e, f: 6}); + obj = { a: 1, b: 2, }; + obj = { a: 1, b: 2 }; + obj = { a: 1, b: 2 }; + obj = { a: 1, b: 2, }; + obj = { a: 1, b: 2, }; + ` + } ]; diff --git a/test/samples/spread-operator.js b/test/samples/spread-operator.js index 6cda801a..3adb3d8e 100644 --- a/test/samples/spread-operator.js +++ b/test/samples/spread-operator.js @@ -435,6 +435,25 @@ module.exports = [ console.log(JSON.stringify(this.a)); } var obj = { Test }; + var a = [1, 2]; + var b = [3, 4]; + var c = [7, 8]; + + new Test(...a); + new obj.Test(...a); + new (null || obj).Test(...a); + + new Test(0, ...a); + new obj.Test(0, ...a); + new (null || obj).Test(0, ...a); + + new Test(...a, ...b, 5); + new obj.Test(...a, ...b, 5); + new (null || obj).Test(...a, ...b, 5); + + new Test(...a, new Test(...c), ...b, 5); + new obj.Test(...a, new Test(...c), ...b, 5); + new (null || obj).Test(...a, new Test(...c), ...b, 5); new Test(...[1, 2]); new obj.Test(...[1, 2]); @@ -471,22 +490,41 @@ module.exports = [ console.log(JSON.stringify(this.a)); } var obj = { Test: Test }; + var a = [1, 2]; + var b = [3, 4]; + var c = [7, 8]; + + new (Function.prototype.bind.apply( Test, [ null ].concat( a) )); + new (Function.prototype.bind.apply( obj.Test, [ null ].concat( a) )); + new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( a) )); + + new (Function.prototype.bind.apply( Test, [ null ].concat( [0], a) )); + new (Function.prototype.bind.apply( obj.Test, [ null ].concat( [0], a) )); + new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( [0], a) )); - new (Function.prototype.bind.apply( Test, [ null ].concat( [1, 2]) )); - new (Function.prototype.bind.apply( obj.Test, [ null ].concat( [1, 2]) )); - new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( [1, 2]) )); + new (Function.prototype.bind.apply( Test, [ null ].concat( a, b, [5]) )); + new (Function.prototype.bind.apply( obj.Test, [ null ].concat( a, b, [5]) )); + new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( a, b, [5]) )); - new (Function.prototype.bind.apply( Test, [ null ].concat( [0], [1, 2]) )); - new (Function.prototype.bind.apply( obj.Test, [ null ].concat( [0], [1, 2]) )); - new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( [0], [1, 2]) )); + new (Function.prototype.bind.apply( Test, [ null ].concat( a, [new (Function.prototype.bind.apply( Test, [ null ].concat( c) ))], b, [5]) )); + new (Function.prototype.bind.apply( obj.Test, [ null ].concat( a, [new (Function.prototype.bind.apply( Test, [ null ].concat( c) ))], b, [5]) )); + new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( a, [new (Function.prototype.bind.apply( Test, [ null ].concat( c) ))], b, [5]) )); - new (Function.prototype.bind.apply( Test, [ null ].concat( [1, 2], [3, 4], [5]) )); - new (Function.prototype.bind.apply( obj.Test, [ null ].concat( [1, 2], [3, 4], [5]) )); - new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( [1, 2], [3, 4], [5]) )); + new Test(1, 2); + new obj.Test(1, 2); + new (null || obj).Test(1, 2); - new (Function.prototype.bind.apply( Test, [ null ].concat( [1, 2], [new (Function.prototype.bind.apply( Test, [ null ].concat( [7, 8]) ))], [3, 4], [5]) )); - new (Function.prototype.bind.apply( obj.Test, [ null ].concat( [1, 2], [new (Function.prototype.bind.apply( Test, [ null ].concat( [7, 8]) ))], [3, 4], [5]) )); - new (Function.prototype.bind.apply( (null || obj).Test, [ null ].concat( [1, 2], [new (Function.prototype.bind.apply( Test, [ null ].concat( [7, 8]) ))], [3, 4], [5]) )); + new Test(0, 1, 2); + new obj.Test(0, 1, 2); + new (null || obj).Test(0, 1, 2); + + new Test(1, 2, 3, 4, 5); + new obj.Test(1, 2, 3, 4, 5); + new (null || obj).Test(1, 2, 3, 4, 5); + + new Test(1, 2, new Test(7, 8), 3, 4, 5); + new obj.Test(1, 2, new Test(7, 8), 3, 4, 5); + new (null || obj).Test(1, 2, new Test(7, 8), 3, 4, 5); (function () { var i = arguments.length, argsArray = Array(i); @@ -617,5 +655,231 @@ module.exports = [ input: `new X(...A, () => 'B')`, output: `new (Function.prototype.bind.apply( X, [ null ].concat( A, [function () { return 'B'; }]) ))` - } + }, + + { + description: 'inlines unreasonably deep spreads', + input: ` + [...[...[...[1, ...[...[...[2, 3]], 4]]]]]; + f(...[...[...[1, ...[...[...[2, 3]], 4]]]]); + new f(...[...[...[1, ...[...[...[2, 3]], 4]]]]); + `, + output: ` + [1, 2, 3, 4]; + f(1, 2, 3, 4); + new f(1, 2, 3, 4); + ` + }, + + { + description: 'does not (yet) inline spread arrays with holes', + input: ` + [...[,]]; + f(...[,]); + new f(...[,]); + `, + output: ` + [].concat( [,] ); + f.apply(void 0, [,]); + new (Function.prototype.bind.apply( f, [ null ].concat( [,]) )); + ` + }, + + { + description: 'inlines array spreads without extraneous trailing commas', + input: ` + [...[]]; + [...[],]; + [...[x]]; + [...[x,]]; + [...[x, y]]; + [...[x, y,]]; + [...[x, y],]; + [...[x, y,],]; + + [w, ...[]]; + [w, ...[],]; + [w, ...[x]]; + [w, ...[x,]]; + [w, ...[x, y]]; + [w, ...[x, y,]]; + [w, ...[x, y],]; + [w, ...[x, y,],]; + + [...[], z]; + [...[x], z]; + [...[x,], z]; + [...[x, y], z]; + [...[x, y,], z]; + + [w, ...[], z]; + [w, ...[x], z]; + [w, ...[x,], z]; + [w, ...[x, y], z]; + [w, ...[x, y,], z]; + `, + output: ` + []; + []; + [x]; + [x ]; + [x, y]; + [x, y ]; + [x, y ]; + [x, y ]; + + [w ]; + [w ]; + [w, x]; + [w, x ]; + [w, x, y]; + [w, x, y ]; + [w, x, y ]; + [w, x, y ]; + + [z]; + [x, z]; + [x, z]; + [x, y, z]; + [x, y, z]; + + [w, z]; + [w, x, z]; + [w, x, z]; + [w, x, y, z]; + [w, x, y, z]; + ` + }, + + { + description: 'inlines call spreads without extraneous trailing commas', + input: ` + f(...[]); + f(...[],); + f(...[x]); + f(...[x,]); + f(...[x, y]); + f(...[x, y,]); + f(...[x, y],); + f(...[x, y,],); + + f(w, ...[]); + f(w, ...[],); + f(w, ...[x]); + f(w, ...[x,]); + f(w, ...[x, y]); + f(w, ...[x, y,]); + f(w, ...[x, y],); + f(w, ...[x, y,],); + + f(...[], z); + f(...[x], z); + f(...[x,], z); + f(...[x, y], z); + f(...[x, y,], z); + + f(w, ...[], z); + f(w, ...[x], z); + f(w, ...[x,], z); + f(w, ...[x, y], z); + f(w, ...[x, y,], z); + `, + output: ` + f(); + f(); + f(x); + f(x); + f(x, y); + f(x, y); + f(x, y); + f(x, y); + + f(w); + f(w); + f(w, x); + f(w, x); + f(w, x, y); + f(w, x, y); + f(w, x, y); + f(w, x, y); + + f(z); + f(x, z); + f(x, z); + f(x, y, z); + f(x, y, z); + + f(w, z); + f(w, x, z); + f(w, x, z); + f(w, x, y, z); + f(w, x, y, z); + ` + }, + + { + description: 'inlines new call spreads without extraneous trailing commas', + input: ` + new f(...[]); + new f(...[],); + new f(...[x]); + new f(...[x,]); + new f(...[x, y]); + new f(...[x, y,]); + new f(...[x, y],); + new f(...[x, y,],); + + new f(w, ...[]); + new f(w, ...[],); + new f(w, ...[x]); + new f(w, ...[x,]); + new f(w, ...[x, y]); + new f(w, ...[x, y,]); + new f(w, ...[x, y],); + new f(w, ...[x, y,],); + + new f(...[], z); + new f(...[x], z); + new f(...[x,], z); + new f(...[x, y], z); + new f(...[x, y,], z); + + new f(w, ...[], z); + new f(w, ...[x], z); + new f(w, ...[x,], z); + new f(w, ...[x, y], z); + new f(w, ...[x, y,], z); + `, + output: ` + new f(); + new f(); + new f(x); + new f(x); + new f(x, y); + new f(x, y); + new f(x, y); + new f(x, y); + + new f(w); + new f(w); + new f(w, x); + new f(w, x); + new f(w, x, y); + new f(w, x, y); + new f(w, x, y); + new f(w, x, y); + + new f(z); + new f(x, z); + new f(x, z); + new f(x, y, z); + new f(x, y, z); + + new f(w, z); + new f(w, x, z); + new f(w, x, z); + new f(w, x, y, z); + new f(w, x, y, z); + ` + }, ];