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 3 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)."
}
36 changes: 30 additions & 6 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -1204,6 +1204,30 @@ export default class ExpressionParser extends LValParser {
return this.parseTopicReferenceThenEqualsSign(tt.bitwiseXOR, "^");
}

case tt.doubleCaret: {
const pipeProposal = this.getPluginOption(
"pipelineOperator",
"proposal",
);
const pluginTopicToken = this.getPluginOption(
"pipelineOperator",
"topicToken",
);

// The `^^` token is valid only when:
// the pipe-operator proposal is active,
// its "pipeProposal" is configured as "hack",
// and "topicToken" is configured as "^^".
// If the pipe-operator proposal settles on a token that is not ^^,
// then this token type may be removed.
if (pipeProposal === "hack" && pluginTopicToken === "^^") {
return this.parseTopicReference(pipeProposal);
} else {
throw this.unexpected();
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
}
}

case tt.doubleAt:
case tt.bitwiseXOR:
case tt.modulo:
case tt.hash: {
Expand Down Expand Up @@ -1306,10 +1330,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 +1348,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
38 changes: 36 additions & 2 deletions packages/babel-parser/src/tokenizer/index.js
Expand Up @@ -715,6 +715,10 @@ export default class Tokenizer extends ParserErrors {

readToken_caret(): void {
const next = this.input.charCodeAt(this.state.pos + 1);
const pipeProposal = this.getPluginOption("pipelineOperator", "proposal");
const topicToken = this.getPluginOption("pipelineOperator", "topicToken");
const hackPipeWithDoubleCaretIsActive =
pipeProposal === "hack" && topicToken === "^^";

// '^='
if (next === charCodes.equalsTo && !this.state.inType) {
Expand All @@ -723,12 +727,43 @@ export default class Tokenizer extends ParserErrors {
// it can be merged with tt.assign.
this.finishOp(tt.xorAssign, 2);
}
// '^^'
else if (hackPipeWithDoubleCaretIsActive && next === charCodes.caret) {
js-choi marked this conversation as resolved.
Show resolved Hide resolved
// `tt.doubleCaret` is only needed to support ^^
// as a Hack-pipe topic token.
// If the proposal ends up choosing a different token,
// it may be removed.
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) {
// `tt.doubleAt` is only needed to support @@
// as a Hack-pipe topic token.
// If the proposal ends up choosing a different token,
// it may be removed.
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 +1053,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": "@@" }]]
}