Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal-pipe: Add support for ^^ and @@ topics #13973

Merged
merged 4 commits into from Feb 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions eslint/babel-eslint-parser/src/convert/convertTokens.cjs
Expand Up @@ -140,6 +140,8 @@ function convertToken(token, source, tl) {
label === tl.bracketHashL ||
label === tl.bracketBarL ||
label === tl.bracketBarR ||
label === tl.doubleCaret ||
label === tl.doubleAt ||
type.isAssign
) {
token.type = "Punctuator";
Expand Down
25 changes: 25 additions & 0 deletions eslint/babel-eslint-parser/test/index.js
Expand Up @@ -691,6 +691,31 @@ describe("Babel and Espree", () => {
expect(babylonAST.tokens[16]).toMatchObject(topicToken);
});

it.each(["^", "%", "^^", "@@"])("pipeline %s topic token", tok => {
const code = `
x |> ${tok}
y |> ${tok}[0]
`;

const babylonAST = parseForESLint(code, {
eslintVisitorKeys: true,
eslintScopeManager: true,
babelOptions: {
filename: "test.js",
parserOpts: {
plugins: [
["pipelineOperator", { proposal: "hack", topicToken: tok }],
],
tokens: true,
},
},
}).ast;

const topicToken = { type: "Punctuator", value: tok };
expect(babylonAST.tokens[2]).toMatchObject(topicToken);
expect(babylonAST.tokens[5]).toMatchObject(topicToken);
});

it("empty program with line comment", () => {
parseAndAssertSame("// single comment");
});
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-generator/src/generators/types.ts
Expand Up @@ -238,7 +238,7 @@ export function DecimalLiteral(this: Printer, node: t.DecimalLiteral) {
}

// Hack pipe operator
const validTopicTokenSet = new Set(["^", "%", "#"]);
const validTopicTokenSet = new Set(["^^", "@@", "^", "%", "#"]);
export function TopicReference(this: Printer) {
const { topicToken } = this.format;

Expand Down
2 changes: 1 addition & 1 deletion packages/babel-generator/src/index.ts
Expand Up @@ -203,7 +203,7 @@ export interface GeneratorOptions {
* For use with the Hack-style pipe operator.
* Changes what token is used for pipe bodies’ topic references.
*/
topicToken?: "^" | "%" | "#";
topicToken?: "^^" | "@@" | "^" | "%" | "#";
}

export interface GeneratorResult {
Expand Down
@@ -0,0 +1 @@
2 + 3 |> @@.toString(16);
@@ -0,0 +1,4 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "@@" }]],
"topicToken": "@@"
}
@@ -0,0 +1 @@
2 + 3 |> @@.toString(16);
@@ -0,0 +1 @@
2 + 3 |> ^^.toString(16);
@@ -0,0 +1,4 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "^^" }]],
"topicToken": "^^"
}
@@ -0,0 +1 @@
2 + 3 |> ^^.toString(16);
@@ -1,5 +1,5 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "#" }]],
"topicToken": "invalid",
"throws": "The \"topicToken\" generator option must be one of \"^\", \"%\", \"#\" (\"invalid\" received instead)."
"throws": "The \"topicToken\" generator option must be one of \"^^\", \"@@\", \"^\", \"%\", \"#\" (\"invalid\" received instead)."
}
@@ -1,4 +1,4 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "#" }]],
"throws": "The \"topicToken\" generator option must be one of \"^\", \"%\", \"#\" (undefined received instead)."
"throws": "The \"topicToken\" generator option must be one of \"^^\", \"@@\", \"^\", \"%\", \"#\" (undefined received instead)."
}
17 changes: 11 additions & 6 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -1204,6 +1204,11 @@ export default class ExpressionParser extends LValParser {
return this.parseTopicReferenceThenEqualsSign(tt.bitwiseXOR, "^");
}

case tt.doubleCaret:
case tt.doubleAt: {
return this.parseTopicReference("hack");
}

case tt.bitwiseXOR:
case tt.modulo:
case tt.hash: {
Expand Down Expand Up @@ -1306,10 +1311,10 @@ export default class ExpressionParser extends LValParser {
// that is followed by an equals sign.
// See <https://github.com/js-choi/proposal-hack-pipes>.
// If we find ^= or %= in an expression position
// (i.e., the tt.moduloAssign or tt.xorAssign token types),
// and if the Hack-pipes proposal is active with ^ or % as its topicToken,
// then the ^ or % could be the topic token (e.g., in x |> ^==y or x |> ^===y),
// and so we reparse the current token as ^ or %.
// (i.e., the tt.moduloAssign or tt.xorAssign token types), and if the
// Hack-pipes proposal is active with ^ or % as its topicToken, then the ^ or
// % could be the topic token (e.g., in x |> ^==y or x |> ^===y), and so we
// reparse the current token as ^ or %.
// Otherwise, this throws an unexpected-token error.
parseTopicReferenceThenEqualsSign(
topicTokenType: TokenType,
Expand All @@ -1324,8 +1329,8 @@ export default class ExpressionParser extends LValParser {
// will consume that “topic token”.
this.state.type = topicTokenType;
this.state.value = topicTokenValue;
// Rewind the tokenizer to the end of the “topic token”,
// so that the following token starts at the equals sign after that topic token.
// Rewind the tokenizer to the end of the “topic token”, so that the
// following token starts at the equals sign after that topic token.
this.state.pos--;
this.state.end--;
// This is safe to do since the preceding character was either ^ or %, and
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-parser/src/plugin-utils.js
Expand Up @@ -67,7 +67,7 @@ export function getPluginOption(
}

const PIPELINE_PROPOSALS = ["minimal", "fsharp", "hack", "smart"];
const TOPIC_TOKENS = ["^", "%", "#"];
const TOPIC_TOKENS = ["^^", "@@", "^", "%", "#"];
const RECORD_AND_TUPLE_SYNTAX_TYPES = ["hash", "bar"];

export function validatePlugins(plugins: PluginList) {
Expand Down
4 changes: 3 additions & 1 deletion packages/babel-parser/src/plugins/flow/index.js
Expand Up @@ -2256,7 +2256,9 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}
// allow double nullable types in Flow: ??string
return this.finishOp(tt.question, 1);
} else if (isIteratorStart(code, next)) {
} else if (
isIteratorStart(code, next, this.input.charCodeAt(this.state.pos + 2))
) {
this.state.pos += 2; // eat "@@"
return this.readIterator();
} else {
Expand Down
40 changes: 38 additions & 2 deletions packages/babel-parser/src/tokenizer/index.js
Expand Up @@ -723,12 +723,49 @@ export default class Tokenizer extends ParserErrors {
// it can be merged with tt.assign.
this.finishOp(tt.xorAssign, 2);
}
// '^^'
else if (
next === charCodes.caret &&
// If the ^^ token is not enabled, we don't throw but parse two single ^s
// because it could be a ^ hack token followed by a ^ binary operator.
this.hasPlugin([
"pipelineOperator",
{ proposal: "hack", topicToken: "^^" },
])
) {
this.finishOp(tt.doubleCaret, 2);

// `^^^` is forbidden and must be separated by a space.
const lookaheadCh = this.input.codePointAt(this.state.pos);
if (lookaheadCh === charCodes.caret) {
throw this.unexpected();
}
}
// '^'
else {
this.finishOp(tt.bitwiseXOR, 1);
}
}

readToken_atSign(): void {
const next = this.input.charCodeAt(this.state.pos + 1);

// '@@'
if (
next === charCodes.atSign &&
this.hasPlugin([
"pipelineOperator",
{ proposal: "hack", topicToken: "@@" },
])
) {
this.finishOp(tt.doubleAt, 2);
}
// '@'
else {
this.finishOp(tt.at, 1);
}
}

readToken_plus_min(code: number): void {
// '+-'
const next = this.input.charCodeAt(this.state.pos + 1);
Expand Down Expand Up @@ -1018,8 +1055,7 @@ export default class Tokenizer extends ParserErrors {
return;

case charCodes.atSign:
++this.state.pos;
this.finishToken(tt.at);
this.readToken_atSign();
return;

case charCodes.numberSign:
Expand Down
11 changes: 9 additions & 2 deletions packages/babel-parser/src/tokenizer/types.js
Expand Up @@ -186,15 +186,22 @@ export const tt: { [name: string]: TokenType } = {
eq: createToken("=", { beforeExpr, isAssign }),
assign: createToken("_=", { beforeExpr, isAssign }),
slashAssign: createToken("_=", { beforeExpr, isAssign }),
// These are only needed to support % and ^ as a Hack-pipe topic token. When the
// proposal settles on a token, the others can be merged with tt.assign.
// These are only needed to support % and ^ as a Hack-pipe topic token.
// When the proposal settles on a token, the others can be merged with
// tt.assign.
xorAssign: createToken("_=", { beforeExpr, isAssign }),
moduloAssign: createToken("_=", { beforeExpr, isAssign }),
// end: isAssign

incDec: createToken("++/--", { prefix, postfix, startsExpr }),
bang: createToken("!", { beforeExpr, prefix, startsExpr }),
tilde: createToken("~", { beforeExpr, prefix, startsExpr }),

// More possible topic tokens.
// When the proposal settles on a token, at least one of these may be removed.
doubleCaret: createToken("^^", { startsExpr }),
doubleAt: createToken("@@", { startsExpr }),

// start: isBinop
pipeline: createBinop("|>", 0),
nullishCoalescing: createBinop("??", 1),
Expand Down
13 changes: 11 additions & 2 deletions packages/babel-parser/src/util/identifier.js
Expand Up @@ -3,6 +3,7 @@
// @flow

import * as charCodes from "charcodes";
import { isIdentifierStart } from "@babel/helper-validator-identifier";

export {
isIdentifierStart,
Expand All @@ -18,8 +19,16 @@ export const keywordRelationalOperator = /^in(stanceof)?$/;

// Test whether a current state character code and next character code is @

export function isIteratorStart(current: number, next: number): boolean {
return current === charCodes.atSign && next === charCodes.atSign;
export function isIteratorStart(
current: number,
next: number,
next2: number,
): boolean {
return (
current === charCodes.atSign &&
next === charCodes.atSign &&
isIdentifierStart(next2)
);
}

// This is the comprehensive set of JavaScript reserved words
Expand Down
@@ -0,0 +1 @@
value |> @@ + 1
@@ -0,0 +1,3 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "@@" }]]
}
@@ -0,0 +1,45 @@
{
"type": "File",
"start":0,"end":15,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":15}},
"program": {
"type": "Program",
"start":0,"end":15,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":15}},
"sourceType": "script",
"interpreter": null,
"body": [
{
"type": "ExpressionStatement",
"start":0,"end":15,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":15}},
"expression": {
"type": "BinaryExpression",
"start":0,"end":15,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":15}},
"left": {
"type": "Identifier",
"start":0,"end":5,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":5},"identifierName":"value"},
"name": "value"
},
"operator": "|>",
"right": {
"type": "BinaryExpression",
"start":9,"end":15,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":15}},
"left": {
"type": "TopicReference",
"start":9,"end":11,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":11}}
},
"operator": "+",
"right": {
"type": "NumericLiteral",
"start":14,"end":15,"loc":{"start":{"line":1,"column":14},"end":{"line":1,"column":15}},
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
}
}
}
}
],
"directives": []
}
}
@@ -0,0 +1 @@
value |> 1 + @@
@@ -0,0 +1,3 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "@@" }]]
}
@@ -0,0 +1,45 @@
{
"type": "File",
"start":0,"end":15,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":15}},
"program": {
"type": "Program",
"start":0,"end":15,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":15}},
"sourceType": "script",
"interpreter": null,
"body": [
{
"type": "ExpressionStatement",
"start":0,"end":15,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":15}},
"expression": {
"type": "BinaryExpression",
"start":0,"end":15,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":15}},
"left": {
"type": "Identifier",
"start":0,"end":5,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":5},"identifierName":"value"},
"name": "value"
},
"operator": "|>",
"right": {
"type": "BinaryExpression",
"start":9,"end":15,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":15}},
"left": {
"type": "NumericLiteral",
"start":9,"end":10,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":10}},
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
},
"operator": "+",
"right": {
"type": "TopicReference",
"start":13,"end":15,"loc":{"start":{"line":1,"column":13},"end":{"line":1,"column":15}}
}
}
}
}
],
"directives": []
}
}
@@ -0,0 +1 @@
value |> a + b
@@ -0,0 +1,3 @@
{
"plugins": [["pipelineOperator", { "proposal": "hack", "topicToken": "@@" }]]
}