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

Feature request: add support for arrow functions in filters #19

Closed
rellafella opened this issue Apr 12, 2024 · 5 comments
Closed

Feature request: add support for arrow functions in filters #19

rellafella opened this issue Apr 12, 2024 · 5 comments
Labels
Feature: Request Help Wanted Please send PR, I don't know what to do Priority: High
Milestone

Comments

@rellafella
Copy link
Collaborator

Twig has some included filters that can take an arrow function
https://twig.symfony.com/doc/3.x/filters/map.html

Not sure if it would be best to build this out as a plugin or built into the core but the arrow completely breaks the parser.

@zackad
Copy link
Owner

zackad commented Apr 12, 2024

This is also pain point I face when using this plugin. It force me to create custom twig filter to circumvent this limitation. Definitely will be added.

@zackad zackad added this to the Version 1.0 milestone May 8, 2024
@fvwanja
Copy link

fvwanja commented May 22, 2024

This would be soooo super awesome to have!

It also does not work with your prettier-ignore-start syntax:

Screenshot 2024-05-22 at 11 32 46

@marcwieland95
Copy link

marcwieland95 commented Jul 22, 2024

Same issue to me. I was formatting a full code base and the only place I needed to refactor something which made the code worse was with the map filter. It would be very handy to have support for that as well.

@zackad zackad added the Help Wanted Please send PR, I don't know what to do label Aug 3, 2024
@jannisborgers
Copy link

jannisborgers commented Aug 6, 2024

Thought I could give it a try and added PR #42 to hopefully resolve this issue.

Here are my notes. For anyone interested, and to maybe document how to add new features in the future.

Step 1: Let lexer tokenize arrow functions

  • Add new token type:

  • Identify and tokenize arrow function correctly instead of making it an assignment (=) and an operator (>) (this is the error we all see right now):


case "=":
if (input.la(1) === ">") { // Lookahead for '>'
input.next(); // Advance to '='
input.next(); // Advance to '>'
return this.createToken(TokenTypes.ARROW, pos);
} else {
input.next();
return this.createToken(TokenTypes.ASSIGNMENT, pos);
}

Step 2: Let parser identify arrow function in addition to other filter arguments

matchFilterExpression() and matchArguments() are called to collect the arguments of the filter that is applied to a variable.

  • Already supported scenarios:
    • no arguments: someString|raw
    • some arguments, comma-separated: someArray|batch(3, 'No item')
    • named arguments via assignments: someDate|data_uri(mime="text/html", parameters={charset: "ascii"})
  • Newly supported scenarios:
    • arrow function with one argument: someArray|filter(item => item.amount > 4)
    • arrow function with multiple arguments: someArray|map((value, key) => item.firstName ~ " " ~ item.lastName)
    • arrow function, plus other filter arguments, e.g. numbers|reduce((carry, v, k) => carry + v * k, 10), where 10 is the second filter argument and the arrow function is the callback. This works because comma-checks are in place for multiple filter arguments already and matchArguments() is called recursively for each filter argument.

matchArguments() {
const tokens = this.tokens;
const args = [];
tokens.expect(Types.LPAREN);
while (!tokens.test(Types.EOF) && !tokens.test(Types.RPAREN)) {
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();
const arg = new n.NamedArgumentExpression(
createNode(n.Identifier, name, name.text),
value
);
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;
}

Step 3: Add node type:

Used to identify the arrow function as a whole

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");

Step 4: Parse arrow function itself

Arguments is everything between the filter’s opening parenthesis ( and arrow =>

  • Possible scenarios:
  • option 1: single argument, e.g. item =>
  • option 2: multiple arguments inside their own parenthesis, e.g.(value, key) =>

Body is everything after the arrow => up until the closing filter parenthesis )

  • A million scenarios, but each is an expression, which is already covered by the plugin

matchArrowFunction() {
const tokens = this.tokens;
// Arrow arguments
let 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)) {
let 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
let arg = this.matchExpression(); // Adjust this line to match arguments properly
arrowArguments.push(arg);
}
// Skip arrow
if (tokens.test(Types.ARROW)) {
tokens.next();
}
// Body
let arrowBody = this.matchExpression();
const result = new n.ArrowFunction(
arrowArguments,
arrowBody.length === 1 ? arrowBody[0] : arrowBody // If single expression, return it directly
);
return result;
}

Step 5: Add ArrowFunction printer

After the arrow function as a whole is added as a node, it has props for params and body, which can be used inside the printer for formatting.

This is where proper formatting can be achieved by defining the output of the printer.

I’m working on another PR which allows more fine-grained control over line-breaks for filters (and objects), because I think needless indentation of single arguments or properties, or multiple short ones, is not helping code legibility.

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");
let 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 };

Additional scenarios:

I have only tested arrow functions in tags {% %} as opposed to expressions {{ }} briefly, but they seem to work as well now (?)

Scope

As far as I know, arrow functions are only available in map, filter and reduce filters in Twig 3. While there is a Craft plugin to make it work everywhere else, and that plugin got a lot of thumbs up for being native Twig 4 functionality, I think making it work with the three above will improve this plugin a lot.

@zackad zackad closed this as completed in d481de6 Aug 9, 2024
@marcwieland95
Copy link

Thanks @jannisborgers and @zackad. Amazing work 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature: Request Help Wanted Please send PR, I don't know what to do Priority: High
Projects
None yet
Development

No branches or pull requests

5 participants