diff --git a/src/acorn/src/expression.js b/src/acorn/src/expression.js index b820a6bf64..08c4b26084 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 (!noCalls && 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..08e53b6d5a --- /dev/null +++ b/src/babel/transformation/transformers/es7/function-bind.js @@ -0,0 +1,41 @@ +// https://github.com/zenparsing/es-function-bind + +import * as t from "../../../types"; + +export var metadata = { + optional: true, + stage: 0 +}; + +function getTempId(scope) { + var id = scope.path.getData("functionBind"); + if (id) return id; + id = scope.generateTemp("context"); + return scope.path.setData("functionBind", id); +} + +function inferBindContext(bind, scope) { + var tempId = getTempId(scope); + if (bind.object) { + bind.callee = t.sequenceExpression([ + t.assignmentExpression("=", tempId, bind.object), + bind.callee + ]); + } else { + bind.callee.object = t.assignmentExpression("=", tempId, bind.callee.object); + } + return tempId; +} + +export function CallExpression(node, parent, scope, file) { + var bind = node.callee; + if (!t.isBindExpression(bind)) return; + var context = inferBindContext(bind, scope); + node.callee = t.memberExpression(bind.callee, t.identifier("call")); + node.arguments.unshift(context); +} + +export function BindExpression(node, parent, scope, file) { + var context = inferBindContext(node, scope); + return t.callExpression(t.memberExpression(node.callee, t.identifier("bind")), [context]); +} 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/generation/types/BindExpression/actual.js b/test/core/fixtures/generation/types/BindExpression/actual.js new file mode 100644 index 0000000000..c78d72da44 --- /dev/null +++ b/test/core/fixtures/generation/types/BindExpression/actual.js @@ -0,0 +1,5 @@ +::foo.bar.foo; +::foo.bar["foo"]; + +ctx::foo.bar.foo; +ctx::foo.bar["foo"]; diff --git a/test/core/fixtures/generation/types/BindExpression/expected.js b/test/core/fixtures/generation/types/BindExpression/expected.js new file mode 100644 index 0000000000..c78d72da44 --- /dev/null +++ b/test/core/fixtures/generation/types/BindExpression/expected.js @@ -0,0 +1,5 @@ +::foo.bar.foo; +::foo.bar["foo"]; + +ctx::foo.bar.foo; +ctx::foo.bar["foo"]; 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..822d9a79e9 --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/bind/actual.js @@ -0,0 +1,3 @@ +var f = ctx::ns.obj.func; +var g = ::ns.obj.func; +var h = new X::y; 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..22ae9099a5 --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/bind/expected.js @@ -0,0 +1,7 @@ +"use strict"; + +var _context; + +var f = (_context = ctx, ns.obj.func).bind(_context); +var g = (_context = ns.obj).func.bind(_context); +var h = (_context = new X(), y).bind(_context); 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..b48a0bdb3e --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/call/expected.js @@ -0,0 +1,8 @@ +"use strict"; + +var _context; + +(_context = ctx, ns.obj.func).call(_context); +(_context = ns.obj).func.call(_context); + +(_context = ns.obj2, ns.obj1.func).call(_context); 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/exec.js b/test/core/fixtures/transformation/es7.function-bind/complex-call/exec.js new file mode 100644 index 0000000000..5ee5f3f7d7 --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/complex-call/exec.js @@ -0,0 +1,27 @@ +var operations = []; + +var lib = {}; + +for (let key of ['f', 'g', 'h']) { + let func = () => operations.push(`lib.${key}()`); + Object.defineProperty(lib, key, { + get() { + operations.push(`get lib.${key}`); + return func; + } + }); +} + +({prop:'value'}) +::lib.f() +::lib.g() +::lib.h(); + +assert.deepEqual(operations, [ + 'get lib.f', + 'lib.f()', + 'get lib.g', + 'lib.g()', + 'get lib.h', + 'lib.h()' +]); 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..b15ae7d381 --- /dev/null +++ b/test/core/fixtures/transformation/es7.function-bind/complex-call/expected.js @@ -0,0 +1,13 @@ +"use strict"; + +var _context; + +var _iterlib = require("iterlib"); + +(_context = (_context = (_context = getPlayers(), _iterlib.map).call(_context, function (x) { + return x.character(); +}), _iterlib.takeWhile).call(_context, function (x) { + return x.strength > 100; +}), _iterlib.forEach).call(_context, function (x) { + return console.log(x); +}); 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" +} diff --git a/test/core/generation.js b/test/core/generation.js index fa4dd81017..b1f08a2908 100644 --- a/test/core/generation.js +++ b/test/core/generation.js @@ -34,7 +34,8 @@ _.each(helper.get("generation"), function (testSuite) { features: { "es7.comprehensions": true, "es7.asyncFunctions": true, - "es7.exportExtensions": true + "es7.exportExtensions": true, + "es7.functionBind": true } }); var actualCode = generate(actualAst, task.options, actual.code).code;