Skip to content

Commit

Permalink
Merge branch 'jannisborgers:master' into master
Browse files Browse the repository at this point in the history
Close GH-19

Add new feature to handle arrow function inside `filter, map, reduce`
filter.
  • Loading branch information
zackad committed Aug 9, 2024
2 parents a5b4136 + ee47de7 commit d481de6
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/melody/melody-parser/Lexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
56 changes: 56 additions & 0 deletions src/melody/melody-parser/Parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}
}
1 change: 1 addition & 0 deletions src/melody/melody-parser/TokenTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "/";
Expand Down
15 changes: 15 additions & 0 deletions src/melody/melody-types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,21 @@ type(NamedArgumentExpression, "NamedArgumentExpression");
alias(NamedArgumentExpression, "Expression");
visitor(NamedArgumentExpression, "name", "value");

export class ArrowFunction extends Node {
/**
* @param {Array<Node>} 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<ObjectProperty>} properties
Expand Down
37 changes: 37 additions & 0 deletions src/print/ArrowFunction.js
Original file line number Diff line number Diff line change
@@ -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 };
2 changes: 2 additions & 0 deletions src/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;

Expand Down
21 changes: 21 additions & 0 deletions tests/Expressions/__snapshots__/jsfmt.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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')%}
Expand Down
8 changes: 8 additions & 0 deletions tests/Expressions/arrowFunctions.twig
Original file line number Diff line number Diff line change
@@ -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) }}

0 comments on commit d481de6

Please sign in to comment.