From 2cb371a79f0b36b17c2f5120e3b7e07e25b32e07 Mon Sep 17 00:00:00 2001 From: Jannis Borgers Date: Tue, 6 Aug 2024 23:36:45 +0200 Subject: [PATCH 1/2] feat: add support for arrow functions as filter arguments (map, filter, reduce) --- .eslintrc.yml | 1 + CHANGELOG.md | 3 ++ src/melody/melody-parser/Lexer.js | 8 ++++ src/melody/melody-parser/Parser.js | 56 ++++++++++++++++++++++++++ src/melody/melody-parser/TokenTypes.js | 1 + src/melody/melody-types/index.js | 15 +++++++ src/print/ArrowFunction.js | 37 +++++++++++++++++ src/printer.js | 2 + 8 files changed, 123 insertions(+) create mode 100644 src/print/ArrowFunction.js diff --git a/.eslintrc.yml b/.eslintrc.yml index e9c0cee7..30e82d34 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -18,6 +18,7 @@ rules: - error - devDependencies: ["tests*/**", "scripts/**"] no-else-return: error + no-fallthrough: off no-inner-declarations: error no-unneeded-ternary: error no-debugger: off diff --git a/CHANGELOG.md b/CHANGELOG.md index 4382d96d..5664ddc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## unreleased +### Features +- Add support for arrow function inside `filter, map, reduce` filter + ### Internals - Optimize test runner by defining where to look for test files - NPM script alias to run `prettier` has been removed diff --git a/src/melody/melody-parser/Lexer.js b/src/melody/melody-parser/Lexer.js index 7138b089..1dd28284 100644 --- a/src/melody/melody-parser/Lexer.js +++ b/src/melody/melody-parser/Lexer.js @@ -44,6 +44,7 @@ const CHAR_TO_TOKEN = { "|": TokenTypes.PIPE, ",": TokenTypes.COMMA, "?": TokenTypes.QUESTION_MARK, + "=>": TokenTypes.ARROW, "=": TokenTypes.ASSIGNMENT, //'<': TokenTypes.ELEMENT_START, //'>': TokenTypes.ELEMENT_END, @@ -345,6 +346,13 @@ export default class Lexer { this.pushState(State.STRING_DOUBLE); input.next(); return this.createToken(TokenTypes.STRING_START, pos); + case "=": + // Lookahead for '>' + if (input.la(1) === ">") { + input.next(); // Advance to '=' + input.next(); // Advance to '>' + return this.createToken(TokenTypes.ARROW, pos); + } default: { if (isDigit(input.lac(0))) { input.next(); diff --git a/src/melody/melody-parser/Parser.js b/src/melody/melody-parser/Parser.js index baec8012..71f29ab1 100644 --- a/src/melody/melody-parser/Parser.js +++ b/src/melody/melody-parser/Parser.js @@ -926,9 +926,18 @@ export default class Parser { tokens.expect(Types.LPAREN); while (!tokens.test(Types.RPAREN) && !tokens.test(Types.EOF)) { if ( + tokens.test(Types.LPAREN) || + (tokens.test(Types.SYMBOL) && tokens.lat(1) === Types.ARROW) + ) { + // OPTION 1: filter argument with arrow function + const arrowFunction = this.matchArrowFunction(); + copyEnd(arrowFunction, arrowFunction.body); + args.push(arrowFunction); + } else if ( tokens.test(Types.SYMBOL) && tokens.lat(1) === Types.ASSIGNMENT ) { + // OPTION 2: named filter argument(s) const name = tokens.next(); tokens.next(); const value = this.matchExpression(); @@ -939,16 +948,63 @@ export default class Parser { copyEnd(arg, value); args.push(arg); } else { + // OPTION 3: unnamed filter argument(s) args.push(this.matchExpression()); } + // No comma means end of filter arguments, return filter arguments to matchFilterExpression() if (!tokens.test(Types.COMMA)) { tokens.expect(Types.RPAREN); return args; } + // Otherwise, expect a comma and run again tokens.expect(Types.COMMA); } + // End of arguments tokens.expect(Types.RPAREN); return args; } + + matchArrowFunction() { + const tokens = this.tokens; + + // Arrow arguments + const arrowArguments = []; + + if (tokens.test(Types.LPAREN)) { + // OPTION 1: Multiple arguments in parentheses, e.g. (value, key) => expression + tokens.next(); // Consume the LPAREN + + while (!tokens.test(Types.EOF) && !tokens.test(Types.RPAREN)) { + const arg = this.matchExpression(); // Adjust this line to match arguments properly + arrowArguments.push(arg); + if (tokens.test(Types.COMMA)) { + tokens.next(); // Consume the comma + } + } + + if (tokens.test(Types.RPAREN)) { + tokens.next(); // Consume the RPAREN + } + } else { + // OPTION 2: Single argument, e.g. item => expression + const arg = this.matchExpression(); // Adjust this line to match arguments properly + arrowArguments.push(arg); + } + + // Skip arrow + if (tokens.test(Types.ARROW)) { + tokens.next(); + } + + // Body + const arrowBody = this.matchExpression(); + + const result = new n.ArrowFunction( + arrowArguments, + arrowBody.length === 1 ? arrowBody[0] : arrowBody // If single expression, return it directly + ); + + return result; + } } diff --git a/src/melody/melody-parser/TokenTypes.js b/src/melody/melody-parser/TokenTypes.js index bc316336..074ece6a 100644 --- a/src/melody/melody-parser/TokenTypes.js +++ b/src/melody/melody-parser/TokenTypes.js @@ -44,6 +44,7 @@ export const COMMA = ","; export const DOT = "."; export const PIPE = "|"; export const QUESTION_MARK = "?"; +export const ARROW = "=>"; export const ASSIGNMENT = "="; export const ELEMENT_START = "<"; export const SLASH = "/"; diff --git a/src/melody/melody-types/index.js b/src/melody/melody-types/index.js index 4b70777b..60762b3a 100644 --- a/src/melody/melody-types/index.js +++ b/src/melody/melody-types/index.js @@ -323,6 +323,21 @@ type(NamedArgumentExpression, "NamedArgumentExpression"); alias(NamedArgumentExpression, "Expression"); visitor(NamedArgumentExpression, "name", "value"); +export class ArrowFunction extends Node { + /** + * @param {Array} args + * @param {Node} body + */ + constructor(args, body) { + super(); + this.args = args; + this.body = body; + } +} +type(ArrowFunction, "ArrowFunction"); +alias(ArrowFunction, "Expression"); +visitor(ArrowFunction, "args", "body"); + export class ObjectExpression extends Node { /** * @param {Array} properties diff --git a/src/print/ArrowFunction.js b/src/print/ArrowFunction.js new file mode 100644 index 00000000..63032f38 --- /dev/null +++ b/src/print/ArrowFunction.js @@ -0,0 +1,37 @@ +import { doc } from "prettier"; +const { group, indent, join, line, softline } = doc.builders; + +const p = (node, path, print) => { + const args = node.args; + const body = path.call(print, "body"); + + const parts = []; + + // Args + if (args.length > 1) { + // Multiple args + parts.push( + group([ + "(", + join( + ", ", + args.map(arg => arg.name) + ), + ")" + ]) + ); + } else { + // Single arg + parts.push(args[0].name); + } + + // Arrow + parts.push(" => "); + + // Body + parts.push(body); + + return group(parts); +}; + +export { p as printArrowFunction }; diff --git a/src/printer.js b/src/printer.js index c4aa04cc..94641caa 100644 --- a/src/printer.js +++ b/src/printer.js @@ -7,6 +7,7 @@ import { printIdentifier } from "./print/Identifier.js"; import { printExpressionStatement } from "./print/ExpressionStatement.js"; import { printMemberExpression } from "./print/MemberExpression.js"; import { printFilterExpression } from "./print/FilterExpression.js"; +import { printArrowFunction } from "./print/ArrowFunction.js"; import { printObjectExpression } from "./print/ObjectExpression.js"; import { printObjectProperty } from "./print/ObjectProperty.js"; import { printCallExpression } from "./print/CallExpression.js"; @@ -194,6 +195,7 @@ printFunctions["PrintTextStatement"] = printTextStatement; printFunctions["PrintExpressionStatement"] = printExpressionStatement; printFunctions["MemberExpression"] = printMemberExpression; printFunctions["FilterExpression"] = printFilterExpression; +printFunctions["ArrowFunction"] = printArrowFunction; printFunctions["ObjectExpression"] = printObjectExpression; printFunctions["ObjectProperty"] = printObjectProperty; From ee47de75660cfa712a9f1f7dc91ada01d642c7f5 Mon Sep 17 00:00:00 2001 From: Jannis Borgers Date: Tue, 6 Aug 2024 23:42:24 +0200 Subject: [PATCH 2/2] test: add test file for arrow functions --- .../__snapshots__/jsfmt.spec.js.snap | 21 +++++++++++++++++++ tests/Expressions/arrowFunctions.twig | 8 +++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/Expressions/arrowFunctions.twig diff --git a/tests/Expressions/__snapshots__/jsfmt.spec.js.snap b/tests/Expressions/__snapshots__/jsfmt.spec.js.snap index 08e05966..c14312d8 100644 --- a/tests/Expressions/__snapshots__/jsfmt.spec.js.snap +++ b/tests/Expressions/__snapshots__/jsfmt.spec.js.snap @@ -42,6 +42,27 @@ exports[`arrayExpression.twig - twig-verify > arrayExpression.twig 1`] = ` `; +exports[`arrowFunctions.twig - twig-verify > arrowFunctions.twig 1`] = ` +Arrow function with one argument: +{{ someArray|filter(item => item.amount > 5) }} + +Arrow function with multiple arguments: +{{ someArray|map((value, key) => "key #{key} with value #{value}") }} + +Arrow function with multiple arguments and second filter argument: +{{ numbers|reduce((carry, v, k) => carry + v * k, 10) }} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Arrow function with one argument: +{{ someArray|filter(item => item.amount > 5) }} + +Arrow function with multiple arguments: +{{ someArray|map((value, key) => "key #{key} with value #{value}") }} + +Arrow function with multiple arguments and second filter argument: +{{ numbers|reduce((carry, v, k) => carry + v * k, 10) }} + +`; + exports[`binaryExpressions.twig - twig-verify > binaryExpressions.twig 1`] = ` {% set highlightValueForMoney = isFeatureEnabled('vFMV5') or isCTestActive('WEB-48935') or isCTestActive('WEB-48956') or isCTestActive('WEB-48955')%} diff --git a/tests/Expressions/arrowFunctions.twig b/tests/Expressions/arrowFunctions.twig new file mode 100644 index 00000000..697a7574 --- /dev/null +++ b/tests/Expressions/arrowFunctions.twig @@ -0,0 +1,8 @@ +Arrow function with one argument: +{{ someArray|filter(item => item.amount > 5) }} + +Arrow function with multiple arguments: +{{ someArray|map((value, key) => "key #{key} with value #{value}") }} + +Arrow function with multiple arguments and second filter argument: +{{ numbers|reduce((carry, v, k) => carry + v * k, 10) }}