diff --git a/src/acorn/src/expression.js b/src/acorn/src/expression.js index b820a6bf64..086a01c58a 100755 --- a/src/acorn/src/expression.js +++ b/src/acorn/src/expression.js @@ -214,7 +214,12 @@ pp.parseExprSubscripts = function(refShorthandDefaultPos) { } pp.parseSubscripts = function(base, start, noCalls) { - if (this.eat(tt.dot)) { + if (this.eat(tt.doubleColon)) { + let node = this.startNodeAt(start) + node.object = base + node.callee = this.parseNoCallExpr() + return this.parseSubscripts(this.finishNode(node, "BindExpression"), start, noCalls) + } else if (this.eat(tt.dot)) { let node = this.startNodeAt(start) node.object = base node.property = this.parseIdent(true) @@ -240,6 +245,13 @@ pp.parseSubscripts = function(base, start, noCalls) { } return base } +// Parse a no-call expression (like argument of `new` or `::` operators). + +pp.parseNoCallExpr = function() { + let start = this.markPosition() + return this.parseSubscripts(this.parseExprAtom(), start, true) +} + // Parse an atomic expression — either a single token that is an // expression, an expression started by a keyword like `function` or // `new`, or an expression wrapped in punctuation like `()`, `[]`, @@ -363,6 +375,15 @@ pp.parseExprAtom = function(refShorthandDefaultPos) { case tt.backQuote: return this.parseTemplate() + case tt.doubleColon: + node = this.startNode() + this.next() + node.object = null + let callee = node.callee = this.parseNoCallExpr() + if (callee.type !== "MemberExpression") + this.raise(callee.start, "Binding should be performed on object property.") + return this.finishNode(node, "BindExpression") + default: this.unexpected() } @@ -472,8 +493,7 @@ pp.parseNew = function() { this.raise(node.property.start, "The only valid meta property for new is new.target") return this.finishNode(node, "MetaProperty") } - let start = this.markPosition() - node.callee = this.parseSubscripts(this.parseExprAtom(), start, true) + node.callee = this.parseNoCallExpr() if (this.eat(tt.parenL)) node.arguments = this.parseExprList( tt.parenR, this.options.features["es7.trailingFunctionCommas"] diff --git a/src/acorn/src/tokenize.js b/src/acorn/src/tokenize.js index e99fc74080..fc9d706bb8 100755 --- a/src/acorn/src/tokenize.js +++ b/src/acorn/src/tokenize.js @@ -320,7 +320,13 @@ pp.getTokenFromCode = function(code) { case 93: ++this.pos; return this.finishToken(tt.bracketR) case 123: ++this.pos; return this.finishToken(tt.braceL) case 125: ++this.pos; return this.finishToken(tt.braceR) - case 58: ++this.pos; return this.finishToken(tt.colon) + + case 58: + if (this.options.features["es7.functionBind"] && this.input.charCodeAt(this.pos + 1) === 58) + return this.finishOp(tt.doubleColon, 2) + ++this.pos + return this.finishToken(tt.colon) + case 63: ++this.pos; return this.finishToken(tt.question) case 64: ++this.pos; return this.finishToken(tt.at) diff --git a/src/acorn/src/tokentype.js b/src/acorn/src/tokentype.js index 609e508583..05d1d53186 100755 --- a/src/acorn/src/tokentype.js +++ b/src/acorn/src/tokentype.js @@ -54,6 +54,7 @@ export const types = { comma: new TokenType(",", beforeExpr), semi: new TokenType(";", beforeExpr), colon: new TokenType(":", beforeExpr), + doubleColon: new TokenType("::", beforeExpr), dot: new TokenType("."), question: new TokenType("?", beforeExpr), arrow: new TokenType("=>", beforeExpr), diff --git a/src/babel/generation/generators/expressions.js b/src/babel/generation/generators/expressions.js index afa481d11d..e08aeb168e 100644 --- a/src/babel/generation/generators/expressions.js +++ b/src/babel/generation/generators/expressions.js @@ -133,6 +133,12 @@ export function AssignmentExpression(node, print) { print(node.right); } +export function BindExpression(node, print) { + print(node.object); + this.push("::"); + print(node.callee); +} + export { AssignmentExpression as BinaryExpression, AssignmentExpression as LogicalExpression, diff --git a/src/babel/patch.js b/src/babel/patch.js index f8f5dcf95e..4d329a6b3c 100644 --- a/src/babel/patch.js +++ b/src/babel/patch.js @@ -72,4 +72,10 @@ def("ExportAllDeclaration") .field("exported", def("Identifier")) .field("source", def("Literal")); +def("BindExpression") + .bases("Expression") + .build("object", "callee") + .field("object", or(def("Expression"), null)) + .field("callee", def("Expression")); + types.finalize(); diff --git a/src/babel/transformation/transformers/es7/function-bind.js b/src/babel/transformation/transformers/es7/function-bind.js new file mode 100644 index 0000000000..dc3af46ecd --- /dev/null +++ b/src/babel/transformation/transformers/es7/function-bind.js @@ -0,0 +1,37 @@ +// https://github.com/zenparsing/es-function-bind + +import * as t from "../../../types"; + +export var metadata = { + optional: true, + stage: 0 +}; + +var CALL = t.identifier("call"); +var BIND = t.identifier("bind"); + +function inferBindContext(bindExpr, scope) { + // nothing to infer + if (bindExpr.object) return bindExpr.object; + + var id = scope.path.getData("functionBind"); + if (!id) { + id = scope.generateTemp("context"); + scope.path.setData("functionBind", id); + } + bindExpr.callee.object = t.assignmentExpression("=", id, bindExpr.callee.object); + return id; +} + +export function CallExpression(node, parent, scope, file) { + var bindExpr = node.callee; + if (!t.isBindExpression(bindExpr)) return; + var bindCtx = inferBindContext(bindExpr, scope); + node.callee = t.memberExpression(bindExpr.callee, CALL, false); + node.arguments.unshift(bindCtx); +} + +export function BindExpression(node, parent, scope, file) { + var bindCtx = inferBindContext(node, scope); + return t.callExpression(t.memberExpression(node.callee, BIND, false), [bindCtx]); +} diff --git a/src/babel/transformation/transformers/index.js b/src/babel/transformation/transformers/index.js index e6231da863..b44ba74e3a 100644 --- a/src/babel/transformation/transformers/index.js +++ b/src/babel/transformation/transformers/index.js @@ -51,6 +51,7 @@ export default { "spec.protoToAssign": require("./spec/proto-to-assign"), "es7.doExpressions": require("./es7/do-expressions"), "es6.spec.symbols": require("./es6/spec.symbols"), + "es7.functionBind": require("./es7/function-bind"), "spec.undefinedToVoid": require("./spec/undefined-to-void"), jscript: require("./other/jscript"), flow: require("./other/flow"), diff --git a/src/babel/types/builder-keys.json b/src/babel/types/builder-keys.json index bae45903a7..8213e78235 100644 --- a/src/babel/types/builder-keys.json +++ b/src/babel/types/builder-keys.json @@ -20,6 +20,11 @@ "right": null }, + "BindExpression": { + "object": null, + "callee": null + }, + "BlockStatement": { "body": null }, diff --git a/src/babel/types/visitor-keys.json b/src/babel/types/visitor-keys.json index c0d52cab62..0dc2d97b6e 100644 --- a/src/babel/types/visitor-keys.json +++ b/src/babel/types/visitor-keys.json @@ -6,6 +6,7 @@ "AssignmentPattern": ["left", "right"], "AwaitExpression": ["argument"], "BinaryExpression": ["left", "right"], + "BindExpression": ["object", "callee"], "BlockStatement": ["body"], "BreakStatement": ["label"], "CallExpression": ["callee", "arguments"], diff --git a/test/core/fixtures/transformation/es7.function-bind/bind/actual.js b/test/core/fixtures/transformation/es7.function-bind/bind/actual.js new file mode 100644 index 0000000000..95e7aa13cd --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/bind/actual.js @@ -0,0 +1,2 @@ +var f = ctx::ns.obj.func; +var g = ::ns.obj.func; diff --git a/test/core/fixtures/transformation/es7.function-bind/bind/expected.js b/test/core/fixtures/transformation/es7.function-bind/bind/expected.js new file mode 100644 index 0000000000..41321e2ecb --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/bind/expected.js @@ -0,0 +1,6 @@ +"use strict"; + +var _context; + +var f = ns.obj.func.bind(ctx); +var g = (_context = ns.obj).func.bind(_context); \ No newline at end of file diff --git a/test/core/fixtures/transformation/es7.function-bind/call/actual.js b/test/core/fixtures/transformation/es7.function-bind/call/actual.js new file mode 100644 index 0000000000..c2c18d6370 --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/call/actual.js @@ -0,0 +1,4 @@ +ctx::ns.obj.func(); +::ns.obj.func(); + +ns.obj2::ns.obj1.func(); diff --git a/test/core/fixtures/transformation/es7.function-bind/call/expected.js b/test/core/fixtures/transformation/es7.function-bind/call/expected.js new file mode 100644 index 0000000000..75cc530bff --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/call/expected.js @@ -0,0 +1,8 @@ +"use strict"; + +var _context; + +ns.obj.func.call(ctx); +(_context = ns.obj).func.call(_context); + +ns.obj1.func.call(ns.obj2); \ No newline at end of file diff --git a/test/core/fixtures/transformation/es7.function-bind/complex-call/actual.js b/test/core/fixtures/transformation/es7.function-bind/complex-call/actual.js new file mode 100644 index 0000000000..b681cb546b --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/complex-call/actual.js @@ -0,0 +1,6 @@ +import { map, takeWhile, forEach } from "iterlib"; + +getPlayers() +::map(x => x.character()) +::takeWhile(x => x.strength > 100) +::forEach(x => console.log(x)); diff --git a/test/core/fixtures/transformation/es7.function-bind/complex-call/expected.js b/test/core/fixtures/transformation/es7.function-bind/complex-call/expected.js new file mode 100644 index 0000000000..e1482fbd94 --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/complex-call/expected.js @@ -0,0 +1,11 @@ +"use strict"; + +var _iterlib = require("iterlib"); + +_iterlib.forEach.call(_iterlib.takeWhile.call(_iterlib.map.call(getPlayers(), function (x) { + return x.character(); +}), function (x) { + return x.strength > 100; +}), function (x) { + return console.log(x); +}); \ No newline at end of file diff --git a/test/core/fixtures/transformation/es7.function-bind/options.json b/test/core/fixtures/transformation/es7.function-bind/options.json new file mode 100644 index 0000000000..bdc665fe51 --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/options.json @@ -0,0 +1,3 @@ +{ + "optional": "es7.functionBind" +}