From 73b9d329505a49a4024997299f90dc20190d65cc Mon Sep 17 00:00:00 2001 From: "William C. Johnson" Date: Sun, 16 Jul 2017 15:31:50 -0400 Subject: [PATCH] Initial implementation of syntactic placeholders --- src/parser/expression.js | 19 ++++-- src/plugins/syntacticPlaceholder.js | 51 +++++++++++++++ src/plugins/tildeCall.js | 2 +- src/registerPlugins.js | 5 ++ .../basic/basic/actual.js | 1 + .../basic/basic/expected.json | 64 ++++++++++++++++++ .../basic/indexed/actual.js | 1 + .../basic/indexed/expected.json | 65 +++++++++++++++++++ .../syntactic-placeholder/options.json | 8 +++ 9 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 src/plugins/syntacticPlaceholder.js create mode 100644 test/fixtures/syntactic-placeholder/basic/basic/actual.js create mode 100644 test/fixtures/syntactic-placeholder/basic/basic/expected.json create mode 100644 test/fixtures/syntactic-placeholder/basic/indexed/actual.js create mode 100644 test/fixtures/syntactic-placeholder/basic/indexed/expected.json create mode 100644 test/fixtures/syntactic-placeholder/options.json diff --git a/src/parser/expression.js b/src/parser/expression.js index 4a05f1c992..1211ba5d0a 100644 --- a/src/parser/expression.js +++ b/src/parser/expression.js @@ -412,7 +412,7 @@ pp.parseSubscripts = function (base, startPos, startLoc, noCalls) { node.property = this.parseExprAtom(); node.computed = true; } else { - node.property = this.parseIdentifier(true); + node.property = this.parseIdentifierOrPlaceholder(true); node.computed = false; } base = this.finishNode(node, "MemberExpression"); @@ -431,7 +431,7 @@ pp.parseSubscripts = function (base, startPos, startLoc, noCalls) { node.property = this.parseLiteral(this.state.value, "NumericLiteral"); node.computed = true; } else { - node.property = this.parseIdentifier(true); + node.property = this.parseIdentifierOrPlaceholder(true); node.computed = false; } } else if (op === "?[") { @@ -651,7 +651,7 @@ pp.parseExprAtom = function (refShorthandDefaultPos) { node = this.startNode(); const allowAwait = this.state.value === "await" && this.state.inAsync; const allowYield = this.shouldAllowYieldIdentifier(); - const id = this.parseIdentifier(allowAwait || allowYield); + const id = this.parseIdentifierOrPlaceholder(allowAwait || allowYield); if (id.name === "await") { if (this.state.inAsync || this.inModule) { @@ -661,7 +661,7 @@ pp.parseExprAtom = function (refShorthandDefaultPos) { this.next(); return this.parseFunction(node, false, false, true); } else if (canBeArrow && id.name === "async" && this.match(tt.name)) { - const params = [this.parseIdentifier()]; + const params = [this.parseIdentifierOrPlaceholder()]; this.check(tt.arrow); // let foo = bar => {}; return this.parseArrowExpression(node, params, true); @@ -1134,6 +1134,7 @@ pp.parseObj = function (isPattern, refShorthandDefaultPos) { if (!isPattern && this.isContextual("async")) { if (isGenerator) this.unexpected(); + // TODO: syntacticPlaceholder: is a placeholder legal here? const asyncId = this.parseIdentifier(); if (this.match(tt.colon) || this.match(tt.parenL) || this.match(tt.braceR) || this.match(tt.eq) || this.match(tt.comma) || (this.hasPlugin("lightscript") && this.isLineBreak())) { prop.key = asyncId; @@ -1262,6 +1263,7 @@ pp.parsePropertyName = function (prop) { prop.computed = false; const oldInPropertyName = this.state.inPropertyName; this.state.inPropertyName = true; + // TODO: syntacticPlaceholder: is a placeholder legal here? prop.key = (this.match(tt.num) || this.match(tt.string)) ? this.parseExprAtom() : this.parseIdentifier(true); this.state.inPropertyName = oldInPropertyName; } @@ -1465,6 +1467,15 @@ pp.parseIdentifier = function (liberal) { return this.finishNode(node, "Identifier"); }; +// Syntactic placeholders: shunt based on plugin status +pp.parseIdentifierOrPlaceholder = function(liberal) { + if (this.hasPlugin("syntacticPlaceholder")) { + return this._parseIdentifierOrPlaceholder(liberal); + } else { + return this.parseIdentifier(liberal); + } +}; + pp.checkReservedWord = function (word, startLoc, checkKeywords, isBinding) { if (this.isReservedWord(word) || (checkKeywords && this.isKeyword(word))) { this.raise(startLoc, word + " is a reserved word"); diff --git a/src/plugins/syntacticPlaceholder.js b/src/plugins/syntacticPlaceholder.js new file mode 100644 index 0000000000..e54b5a0103 --- /dev/null +++ b/src/plugins/syntacticPlaceholder.js @@ -0,0 +1,51 @@ +import Parser from "../parser"; +import { types as tt } from "../tokenizer/types"; +const pp = Parser.prototype; + +export default function(parser) { + if (parser.__syntacticPlaceholderPluginInstalled) return; + parser.__syntacticPlaceholderPluginInstalled = true; + + const ph = parser.options.placeholder || "_"; + const quotedPh = (ph + "").replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); + const phRegex = new RegExp("^" + quotedPh + "([0-9]*)$"); + + pp.isPlaceholderName = function(name) { + return phRegex.test(name); + }; + + // c/p parseIdentifier + pp._parseIdentifierOrPlaceholder = function(liberal) { + const node = this.startNode(); + if (!liberal) { + this.checkReservedWord(this.state.value, this.state.start, !!this.state.type.keyword, false); + } + + let name; + if (this.match(tt.name)) { + name = this.state.value; + } else if (this.state.type.keyword) { + name = this.state.type.keyword; + } else { + this.unexpected(); + } + + const matches = phRegex.exec(name); + if (matches) { + if (matches[1]) node.index = parseInt(matches[1]); + this.next(); + return this.finishNode(node, "PlaceholderExpression"); + } + + node.name = name; + + if (!liberal && node.name === "await" && this.state.inAsync) { + this.raise(node.start, "invalid use of await inside of an async function"); + } + + node.loc.identifierName = node.name; + + this.next(); + return this.finishNode(node, "Identifier"); + }; +} diff --git a/src/plugins/tildeCall.js b/src/plugins/tildeCall.js index 55fd3db574..6274e75f48 100644 --- a/src/plugins/tildeCall.js +++ b/src/plugins/tildeCall.js @@ -13,7 +13,7 @@ export default function(parser) { node.left = left; // allow `this`, Identifier or MemberExpression, but not calls - const right = this.match(tt._this) ? this.parseExprAtom() : this.parseIdentifier(); + const right = this.match(tt._this) ? this.parseExprAtom() : this.parseIdentifierOrPlaceholder(); node.right = this.parseSubscripts(right, this.state.start, this.state.startLoc, true); // Allow safe tilde calls (a~b?(c)) diff --git a/src/registerPlugins.js b/src/registerPlugins.js index 8ca0b97c39..7461a802ff 100644 --- a/src/registerPlugins.js +++ b/src/registerPlugins.js @@ -7,6 +7,7 @@ import safeCallExistentialPlugin from "./plugins/safeCallExistential"; import bangCallPlugin from "./plugins/bangCall"; import significantWhitespacePlugin from "./plugins/significantWhitespace"; import enhancedComprehensionPlugin from "./plugins/enhancedComprehension"; +import syntacticPlaceholderPlugin from "./plugins/syntacticPlaceholder"; import { matchCoreSyntax, match } from "./plugins/match"; function noncePlugin() {} @@ -79,4 +80,8 @@ export default function registerPlugins(plugins, metadata) { registerPlugin("objectBlockAmbiguity_preferObject", noncePlugin, { dependencies: ["lightscript"] }); + + // Parse identifiers beginning with `_` or another user-chosen symbol + // as PlaceholderExpressions. + registerPlugin("syntacticPlaceholder", syntacticPlaceholderPlugin); } diff --git a/test/fixtures/syntactic-placeholder/basic/basic/actual.js b/test/fixtures/syntactic-placeholder/basic/basic/actual.js new file mode 100644 index 0000000000..31354ec138 --- /dev/null +++ b/test/fixtures/syntactic-placeholder/basic/basic/actual.js @@ -0,0 +1 @@ +_ diff --git a/test/fixtures/syntactic-placeholder/basic/basic/expected.json b/test/fixtures/syntactic-placeholder/basic/basic/expected.json new file mode 100644 index 0000000000..dd674cdc43 --- /dev/null +++ b/test/fixtures/syntactic-placeholder/basic/basic/expected.json @@ -0,0 +1,64 @@ +{ + "type": "File", + "start": 0, + "end": 1, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 1, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + } + }, + "sourceType": "script", + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 1, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + } + }, + "expression": { + "type": "PlaceholderExpression", + "start": 0, + "end": 1, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + } + } + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/test/fixtures/syntactic-placeholder/basic/indexed/actual.js b/test/fixtures/syntactic-placeholder/basic/indexed/actual.js new file mode 100644 index 0000000000..77d26c76e0 --- /dev/null +++ b/test/fixtures/syntactic-placeholder/basic/indexed/actual.js @@ -0,0 +1 @@ +_1 diff --git a/test/fixtures/syntactic-placeholder/basic/indexed/expected.json b/test/fixtures/syntactic-placeholder/basic/indexed/expected.json new file mode 100644 index 0000000000..c0ab205a92 --- /dev/null +++ b/test/fixtures/syntactic-placeholder/basic/indexed/expected.json @@ -0,0 +1,65 @@ +{ + "type": "File", + "start": 0, + "end": 2, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 2 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 2, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 2 + } + }, + "sourceType": "script", + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 2, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 2 + } + }, + "expression": { + "type": "PlaceholderExpression", + "start": 0, + "end": 2, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 2 + } + }, + "index": 1 + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/test/fixtures/syntactic-placeholder/options.json b/test/fixtures/syntactic-placeholder/options.json new file mode 100644 index 0000000000..60bbeba2ff --- /dev/null +++ b/test/fixtures/syntactic-placeholder/options.json @@ -0,0 +1,8 @@ +{ + "alternatives": { + "all": { + "allPlugins": true, + "excludePlugins": ["estree"] + } + } +}