From 86cc7fb813796d674e0c7342919d3bc2e13d0dc2 Mon Sep 17 00:00:00 2001 From: Alon Gubkin Date: Fri, 14 Nov 2014 23:17:03 +0200 Subject: [PATCH] splats in function invocation --- lib/ast.js | 1 + lib/ast/expressions/CallExpression.js | 92 ++++++++-- .../expressions/NullCheckCallExpression.js | 5 + lib/ast/expressions/SplatExpression.js | 49 ++++++ lib/parser.js | 161 +++++++++++------- lib/spider.pegjs | 9 +- test/spider_test.js | 38 +++++ 7 files changed, 281 insertions(+), 74 deletions(-) create mode 100644 lib/ast/expressions/SplatExpression.js diff --git a/lib/ast.js b/lib/ast.js index a8cf148..729ea63 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -20,6 +20,7 @@ module.exports = { NewExpression: require('./ast/expressions/NewExpression').NewExpression, ThisExpression: require('./ast/expressions/ThisExpression').ThisExpression, SuperExpression: require('./ast/expressions/SuperExpression').SuperExpression, + SplatExpression: require('./ast/expressions/SplatExpression').SplatExpression, BlockStatement: require('./ast/statements/BlockStatement').BlockStatement, ExpressionStatement: require('./ast/statements/ExpressionStatement').ExpressionStatement, IfStatement: require('./ast/statements/IfStatement').IfStatement, diff --git a/lib/ast/expressions/CallExpression.js b/lib/ast/expressions/CallExpression.js index 58dbfe3..6c328ca 100644 --- a/lib/ast/expressions/CallExpression.js +++ b/lib/ast/expressions/CallExpression.js @@ -32,18 +32,82 @@ exports.CallExpression.prototype.codegen = function () { } var args = this.arguments; - args.forEach(function (arg, i) { - if (!args[i].codeGenerated) { + var splatPositions = []; + + args.forEach(function (arg, i) { + if (args[i].type === "SplatExpression" || args[i].__splat) { + splatPositions.push(i); + } + + if (!args[i].codeGenerated) { args[i] = arg.codegen(); } }); + if (splatPositions.length > 0) { + var argsClone = args.slice(0); + args.length = 0; + args.push({ + "type": "Literal", + "value": null + }); + + if (argsClone.length === 1) { + args.push(argsClone[0].arguments[0]); + } else { + args.push({ + "type": "CallExpression", + "callee": { + "type": "MemberExpression", + "computed": false, + "object": splatPositions[0] === 0 ? argsClone[0] : { + "type": "ArrayExpression", + "elements": argsClone.slice(0, splatPositions[0]) + }, + "property": { + "type": "Identifier", + "name": "concat" + } + }, + "arguments": argsClone.slice(splatPositions[0] === 0 ? 1 : splatPositions[0]) + .map(function (arg, i) { + if (splatPositions.indexOf(i + (splatPositions[0] === 0 ? 1 : splatPositions[0])) !== -1) { + return arg; + } + + return { + "type": "ArrayExpression", + "elements": [arg] + }; + }) + }); + } + } + // If we are null propagating (?.), then turn this // into a condition and add the null propagating condition. if (this.callee.type === 'ConditionalExpression' && (calleeType === 'NullPropagatingExpression' || calleeType === 'MemberExpression')) { var parent = this.parent; + var consequent = { + type: 'CallExpression', + callee: this.callee.consequent, + arguments: this.arguments + }; + + if (splatPositions.length > 0) { + this.callee.consequent = { + "type": "MemberExpression", + "computed": false, + "object": this.callee.consequent, + "property": { + "type": "Identifier", + "name": "apply" + } + }; + } + // If we're inside a statement, then turn this into // a normal if statement. if (parent.type === 'ExpressionStatement') { @@ -54,26 +118,28 @@ exports.CallExpression.prototype.codegen = function () { body: [ { type: 'ExpressionStatement', - expression: { - type: 'CallExpression', - callee: this.callee.consequent, - arguments: this.arguments - } + expression: consequent } ] }; parent.alternate = null; } else { // Otherwise, it should be a conditional expression (?:). - this.type = 'ConditionalExpression'; + this.type = 'ConditionalExpression'; this.test = this.callee.test; - this.consequent = { - type: 'CallExpression', - callee: this.callee.consequent, - arguments: this.arguments - }; + this.consequent = consequent; this.alternate = this.callee.alternate; } + } else if (splatPositions.length > 0) { + this.callee = { + "type": "MemberExpression", + "computed": false, + "object": this.callee, + "property": { + "type": "Identifier", + "name": "apply" + } + }; } return this; diff --git a/lib/ast/expressions/NullCheckCallExpression.js b/lib/ast/expressions/NullCheckCallExpression.js index 2ad5e10..ef80303 100644 --- a/lib/ast/expressions/NullCheckCallExpression.js +++ b/lib/ast/expressions/NullCheckCallExpression.js @@ -30,8 +30,13 @@ exports.NullCheckCallExpression.prototype.codegen = function () { var args = this.args; args.forEach(function (arg, i) { + var isSplat = args[i].type === "SplatExpression"; args[i] = arg.codegen(); args[i].codeGenerated = true; + + if (isSplat) { + args[i].__splat = true; + } }); // If the callee has a function call (e.g: a().b) diff --git a/lib/ast/expressions/SplatExpression.js b/lib/ast/expressions/SplatExpression.js new file mode 100644 index 0000000..2e8de6d --- /dev/null +++ b/lib/ast/expressions/SplatExpression.js @@ -0,0 +1,49 @@ +var Node = require('../Node').Node; + +exports.SplatExpression = function (expression) { + Node.call(this); + + this.type = 'SplatExpression'; + + this.expression = expression; + this.expression.parent = this; +}; + +exports.SplatExpression.prototype = Object.create(Node); + +exports.SplatExpression.prototype.codegen = function () { + if (!Node.prototype.codegen.call(this)) { + return; + } + + this.expression = this.expression.codegen(); + + return { + "type": "CallExpression", + "callee": { + "type": "MemberExpression", + "computed": false, + "object": { + "type": "MemberExpression", + "computed": false, + "object": { + "type": "ArrayExpression", + "elements": [] + }, + "property": { + "type": "Identifier", + "name": "slice" + } + }, + "property": { + "type": "Identifier", + "name": "call" + } + }, + "arguments": [this.expression] + }; +}; + +exports.SplatExpression.prototype.hasCallExpression = function () { + return true; +}; \ No newline at end of file diff --git a/lib/parser.js b/lib/parser.js index 1c5e6dc..5e603bf 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -560,7 +560,10 @@ module.exports = (function() { peg$c341 = function(args) { return optionalList(extractOptional(args, 0)); }, - peg$c342 = function(id, params, inheritsFrom, body) { + peg$c342 = function(expression) { + return insertLocationData(new ast.SplatExpression(expression), text(), line(), column()); + }, + peg$c343 = function(id, params, inheritsFrom, body) { return new ast.FunctionExpression( extractOptional(id, 0), optionalList(extractOptional(params, 0)), @@ -568,64 +571,64 @@ module.exports = (function() { inheritsFrom ); }, - peg$c343 = function(params, body) { + peg$c344 = function(params, body) { return new ast.FunctionExpression( null, optionalList(extractOptional(params, 0)), body ); }, - peg$c344 = function(params, body) { + peg$c345 = function(params, body) { return new ast.FunctionExpression( null, optionalList(extractOptional(params, 0)), body ); }, - peg$c345 = "->", - peg$c346 = { type: "literal", value: "->", description: "\"->\"" }, - peg$c347 = "=>", - peg$c348 = { type: "literal", value: "=>", description: "\"=>\"" }, - peg$c349 = function(id) { return id.asGlobal(); }, - peg$c350 = function() { + peg$c346 = "->", + peg$c347 = { type: "literal", value: "->", description: "\"->\"" }, + peg$c348 = "=>", + peg$c349 = { type: "literal", value: "=>", description: "\"=>\"" }, + peg$c350 = function(id) { return id.asGlobal(); }, + peg$c351 = function() { return insertLocationData(new ast.ThisExpression(), text(), line(), column()); }, - peg$c351 = function() { + peg$c352 = function() { return insertLocationData(new ast.SuperExpression(), text(), line(), column()); }, - peg$c352 = function(range) { + peg$c353 = function(range) { return range; }, - peg$c353 = function(from, operator, to) { + peg$c354 = function(from, operator, to) { return insertLocationData(new ast.Range(from, operator, to), text(), line(), column()); }, - peg$c354 = "..", - peg$c355 = { type: "literal", value: "..", description: "\"..\"" }, - peg$c356 = function() { return ".."; }, - peg$c357 = function(elision) { + peg$c355 = "..", + peg$c356 = { type: "literal", value: "..", description: "\"..\"" }, + peg$c357 = function() { return ".."; }, + peg$c358 = function(elision) { return insertLocationData(new ast.ArrayExpression(optionalList(extractOptional(elision, 0))), text(), line(), column()); }, - peg$c358 = function(elements) { + peg$c359 = function(elements) { return insertLocationData(new ast.ArrayExpression(elements), text(), line(), column()); }, - peg$c359 = function(elements, elision) { + peg$c360 = function(elements, elision) { return insertLocationData(new ast.ArrayExpression(elements.concat(optionalList(extractOptional(elision, 0)))), text(), line(), column()); }, - peg$c360 = function(elision, element) { + peg$c361 = function(elision, element) { return optionalList(extractOptional(elision, 0)).concat(element); }, - peg$c361 = function(first, rest) { return Array.prototype.concat.apply(first, rest); }, - peg$c362 = function(commas) { return filledArray(commas.length + 1, null); }, - peg$c363 = function() { + peg$c362 = function(first, rest) { return Array.prototype.concat.apply(first, rest); }, + peg$c363 = function(commas) { return filledArray(commas.length + 1, null); }, + peg$c364 = function() { return new ast.ObjectExpression([]); }, - peg$c364 = function(properties) { + peg$c365 = function(properties) { return insertLocationData(new ast.ObjectExpression(properties), text(), line(), column()); }, - peg$c365 = function(key, value) { + peg$c366 = function(key, value) { return insertLocationData(new ast.Property(key, value), text(), line(), column()); }, - peg$c366 = function(id) { return [id]; }, + peg$c367 = function(id) { return [id]; }, peg$currPos = 0, peg$reportedPos = 0, @@ -9191,7 +9194,7 @@ module.exports = (function() { var s0, s1, s2, s3, s4, s5, s6, s7; s0 = peg$currPos; - s1 = peg$parseAssignmentExpression(); + s1 = peg$parseArgument(); if (s1 !== peg$FAILED) { s2 = []; s3 = peg$currPos; @@ -9207,7 +9210,7 @@ module.exports = (function() { if (s5 !== peg$FAILED) { s6 = peg$parse__(); if (s6 !== peg$FAILED) { - s7 = peg$parseAssignmentExpression(); + s7 = peg$parseArgument(); if (s7 !== peg$FAILED) { s4 = [s4, s5, s6, s7]; s3 = s4; @@ -9242,7 +9245,7 @@ module.exports = (function() { if (s5 !== peg$FAILED) { s6 = peg$parse__(); if (s6 !== peg$FAILED) { - s7 = peg$parseAssignmentExpression(); + s7 = peg$parseArgument(); if (s7 !== peg$FAILED) { s4 = [s4, s5, s6, s7]; s3 = s4; @@ -9279,6 +9282,44 @@ module.exports = (function() { return s0; } + function peg$parseArgument() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parseAssignmentExpression(); + if (s1 !== peg$FAILED) { + s2 = peg$parse__(); + if (s2 !== peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c205) { + s3 = peg$c205; + peg$currPos += 3; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c206); } + } + if (s3 !== peg$FAILED) { + peg$reportedPos = s0; + s1 = peg$c342(s1); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$c0; + } + } else { + peg$currPos = s0; + s0 = peg$c0; + } + } else { + peg$currPos = s0; + s0 = peg$c0; + } + if (s0 === peg$FAILED) { + s0 = peg$parseAssignmentExpression(); + } + + return s0; + } + function peg$parseFunctionExpression() { var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12; @@ -9357,7 +9398,7 @@ module.exports = (function() { s12 = peg$parse__(); if (s12 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c342(s3, s6, s9, s11); + s1 = peg$c343(s3, s6, s9, s11); s0 = s1; } else { peg$currPos = s0; @@ -9457,7 +9498,7 @@ module.exports = (function() { s9 = peg$parse__(); if (s9 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c343(s3, s8); + s1 = peg$c344(s3, s8); s0 = s1; } else { peg$currPos = s0; @@ -9545,7 +9586,7 @@ module.exports = (function() { s9 = peg$parse__(); if (s9 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c344(s3, s8); + s1 = peg$c345(s3, s8); s0 = s1; } else { peg$currPos = s0; @@ -9595,20 +9636,20 @@ module.exports = (function() { function peg$parseFunctionExpressionOperator() { var s0; - if (input.substr(peg$currPos, 2) === peg$c345) { - s0 = peg$c345; + if (input.substr(peg$currPos, 2) === peg$c346) { + s0 = peg$c346; peg$currPos += 2; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c346); } + if (peg$silentFails === 0) { peg$fail(peg$c347); } } if (s0 === peg$FAILED) { - if (input.substr(peg$currPos, 2) === peg$c347) { - s0 = peg$c347; + if (input.substr(peg$currPos, 2) === peg$c348) { + s0 = peg$c348; peg$currPos += 2; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c348); } + if (peg$silentFails === 0) { peg$fail(peg$c349); } } } @@ -9632,7 +9673,7 @@ module.exports = (function() { s3 = peg$parseIdentifier(); if (s3 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c349(s3); + s1 = peg$c350(s3); s0 = s1; } else { peg$currPos = s0; @@ -9734,7 +9775,7 @@ module.exports = (function() { s1 = peg$parseThisToken(); if (s1 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c350(); + s1 = peg$c351(); } s0 = s1; @@ -9748,7 +9789,7 @@ module.exports = (function() { s1 = peg$parseSuperToken(); if (s1 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c351(); + s1 = peg$c352(); } s0 = s1; @@ -9782,7 +9823,7 @@ module.exports = (function() { } if (s5 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c352(s3); + s1 = peg$c353(s3); s0 = s1; } else { peg$currPos = s0; @@ -9823,7 +9864,7 @@ module.exports = (function() { s5 = peg$parseExpression(); if (s5 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c353(s1, s3, s5); + s1 = peg$c354(s1, s3, s5); s0 = s1; } else { peg$currPos = s0; @@ -9870,7 +9911,7 @@ module.exports = (function() { } if (s5 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c353(s1, s3, s5); + s1 = peg$c354(s1, s3, s5); s0 = s1; } else { peg$currPos = s0; @@ -9900,12 +9941,12 @@ module.exports = (function() { var s0, s1, s2, s3; s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c354) { - s1 = peg$c354; + if (input.substr(peg$currPos, 2) === peg$c355) { + s1 = peg$c355; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c355); } + if (peg$silentFails === 0) { peg$fail(peg$c356); } } if (s1 !== peg$FAILED) { s2 = peg$currPos; @@ -9926,7 +9967,7 @@ module.exports = (function() { } if (s2 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c356(); + s1 = peg$c357(); s0 = s1; } else { peg$currPos = s0; @@ -10005,7 +10046,7 @@ module.exports = (function() { } if (s4 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c357(s3); + s1 = peg$c358(s3); s0 = s1; } else { peg$currPos = s0; @@ -10048,7 +10089,7 @@ module.exports = (function() { } if (s5 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c358(s3); + s1 = peg$c359(s3); s0 = s1; } else { peg$currPos = s0; @@ -10124,7 +10165,7 @@ module.exports = (function() { } if (s8 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c359(s3, s7); + s1 = peg$c360(s3, s7); s0 = s1; } else { peg$currPos = s0; @@ -10191,7 +10232,7 @@ module.exports = (function() { s3 = peg$parseAssignmentExpression(); if (s3 !== peg$FAILED) { peg$reportedPos = s1; - s2 = peg$c360(s2, s3); + s2 = peg$c361(s2, s3); s1 = s2; } else { peg$currPos = s1; @@ -10238,7 +10279,7 @@ module.exports = (function() { s8 = peg$parseAssignmentExpression(); if (s8 !== peg$FAILED) { peg$reportedPos = s3; - s4 = peg$c360(s7, s8); + s4 = peg$c361(s7, s8); s3 = s4; } else { peg$currPos = s3; @@ -10297,7 +10338,7 @@ module.exports = (function() { s8 = peg$parseAssignmentExpression(); if (s8 !== peg$FAILED) { peg$reportedPos = s3; - s4 = peg$c360(s7, s8); + s4 = peg$c361(s7, s8); s3 = s4; } else { peg$currPos = s3; @@ -10322,7 +10363,7 @@ module.exports = (function() { } if (s2 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c361(s1, s2); + s1 = peg$c362(s1, s2); s0 = s1; } else { peg$currPos = s0; @@ -10396,7 +10437,7 @@ module.exports = (function() { } if (s2 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c362(s2); + s1 = peg$c363(s2); s0 = s1; } else { peg$currPos = s0; @@ -10433,7 +10474,7 @@ module.exports = (function() { } if (s3 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c363(); + s1 = peg$c364(); s0 = s1; } else { peg$currPos = s0; @@ -10472,7 +10513,7 @@ module.exports = (function() { } if (s5 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c364(s3); + s1 = peg$c365(s3); s0 = s1; } else { peg$currPos = s0; @@ -10529,7 +10570,7 @@ module.exports = (function() { } if (s7 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c364(s3); + s1 = peg$c365(s3); s0 = s1; } else { peg$currPos = s0; @@ -10678,7 +10719,7 @@ module.exports = (function() { s5 = peg$parseAssignmentExpression(); if (s5 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c365(s1, s5); + s1 = peg$c366(s1, s5); s0 = s1; } else { peg$currPos = s0; @@ -10725,7 +10766,7 @@ module.exports = (function() { s1 = peg$parseIdentifier(); if (s1 !== peg$FAILED) { peg$reportedPos = s0; - s1 = peg$c366(s1); + s1 = peg$c367(s1); } s0 = s1; diff --git a/lib/spider.pegjs b/lib/spider.pegjs index 2847c5b..061ddcd 100644 --- a/lib/spider.pegjs +++ b/lib/spider.pegjs @@ -908,10 +908,17 @@ Arguments } ArgumentList - = first:AssignmentExpression rest:(__ "," __ AssignmentExpression)* { + = first:Argument rest:(__ "," __ Argument)* { return buildList(first, rest, 3); } +Argument + = expression:AssignmentExpression __ "..." { + return insertLocationData(new ast.SplatExpression(expression), text(), line(), column()); + } + / AssignmentExpression + + FunctionExpression = FuncToken __ id:(Identifier __)? "(" __ params:(FormalParameterList __)? ")" __ diff --git a/test/spider_test.js b/test/spider_test.js index 4efc5cb..459ed0b 100644 --- a/test/spider_test.js +++ b/test/spider_test.js @@ -1492,4 +1492,42 @@ describe('splat in function declaration:', function () { it('multiple splats in func decl', generateErrorTest('func f(a..., b...) {}', [{ type: "MultipleSplatsDisallowed" }])); +}); + +describe('splat in call expressions:', function () { + it('call expression with a splat', + generateTest('f(a...);', 'f.apply(null, a);')); + + it('call expression with splat, argument', + generateTest('f(a..., b);', 'f.apply(null, [].slice.call(a).concat([b]));')); + + it('call expression with argument, splat', + generateTest('f(a, b...);', 'f.apply(null, [a].concat([].slice.call(b)));')); + + it('call expression with splat, argument, argument', + generateTest('f(a..., b, c);', 'f.apply(null, [].slice.call(a).concat([b], [c]));')); + + it('call expression with argument, splat, argument', + generateTest('f(a, b..., c);', 'f.apply(null, [a].concat([].slice.call(b), [c]));')); + + it('call expression with argument, argument, splat, argument', + generateTest('f(a, b, c..., d);', 'f.apply(null, [\n a,\n b\n].concat([].slice.call(c), [d]));')); + + it('null check call expression with a splat', + generateTest('f?(a...);', 'if (typeof f === \"function\") {\n f.apply(null, a);\n}')); + + it('null check call expression with splat, argument', + generateTest('f?(a..., b);', 'if (typeof f === \"function\") {\n f.apply(null, [].slice.call(a).concat([b]));\n}')); + + it('null check call expression with argument, splat', + generateTest('f?(a, b...);', 'if (typeof f === \"function\") {\n f.apply(null, [a].concat([].slice.call(b)));\n}')); + + it('null check call expression with splat, argument, argument', + generateTest('f?(a..., b, c);', 'if (typeof f === \"function\") {\n f.apply(null, [].slice.call(a).concat([b], [c]));\n}')); + + it('null check call expression with argument, splat, argument', + generateTest('f?(a, b..., c);', 'if (typeof f === \"function\") {\n f.apply(null, [a].concat([].slice.call(b), [c]));\n}')); + + it('null check call expression with argument, argument, splat, argument', + generateTest('f?(a, b, c..., d);', 'if (typeof f === \"function\") {\n f.apply(null, [\n a,\n b\n ].concat([].slice.call(c), [d]));\n}')); }); \ No newline at end of file