From 662426cd857e57c9c618059542305e8c86b4d30f Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 11 Apr 2022 19:03:34 -0500 Subject: [PATCH] handle stab clauses without right-hand-sides Currently a stab clause without a right-hand-side is parsed as an error: ```elixir Enum.map(xs, fn x -> end) ``` And the `end` token ends up not being highlighted as a keyword. The compiler gives a warning about this syntax but it comes up pretty often when editing (writing a `case` block for example). Implementation-wise, this might be a bug in tree-sitter? `prec.right` seems to fight with error recovery when the rightmost token(s) are `optional`. ```elixir fn -> end ``` gets parsed as ```scm (anonymous_function (stab_clause right: (body (identifier))) (MISSING "end")) ``` although the `optional` should allow this case. I've seen this in other grammars and it seems like the way around it is to replace the `prec.right` with a conflict. --- grammar.js | 31 ++++++++++++++++++++++++------- test/corpus/do_end.txt | 22 ++++++++++++++++++++++ test/highlight/anonymous.ex | 6 ++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/grammar.js b/grammar.js index 69cf600..0e7f517 100644 --- a/grammar.js +++ b/grammar.js @@ -159,6 +159,16 @@ module.exports = grammar({ // * stab arguments item in `arg1, left when right ->` [$.binary_operator, $._stab_clause_arguments_without_parentheses], + // Given `( -> • \n`, the newline could be either: + // * stab clause without a body + // * stab clause with a body + [$._stab_clause_without_body, $._stab_clause_with_body], + + // Given `( -> • /`, `/` token could be either: + // * stab clause with a body + // * -> as an operator followed by `/` + [$._stab_clause_with_body, $.operator_identifier], + // Given `((arg1, arg2 • ,`, `arg3` expression can be either: // * stab parenthesised arguments item in `((arg1, arg2, arg3) ->)` // * stab non-parenthesised arguments item in `((arg1, arg2, arg3 ->))` @@ -729,13 +739,20 @@ module.exports = grammar({ ), stab_clause: ($) => - // Right precedence, because we want to consume body if any - prec.right( - seq( - optional(field("left", $._stab_clause_left)), - field("operator", "->"), - optional(field("right", $.body)) - ) + choice($._stab_clause_with_body, $._stab_clause_without_body), + + _stab_clause_with_body: ($) => + seq( + optional(field("left", $._stab_clause_left)), + field("operator", "->"), + field("right", $.body) + ), + + _stab_clause_without_body: ($) => + seq( + optional(field("left", $._stab_clause_left)), + field("operator", "->"), + optional($._terminator) ), _stab_clause_left: ($) => diff --git a/test/corpus/do_end.txt b/test/corpus/do_end.txt index 8fc1527..1fe5b57 100644 --- a/test/corpus/do_end.txt +++ b/test/corpus/do_end.txt @@ -683,6 +683,28 @@ end (body (integer)))))) +===================================== +stab clause / edge cases / empty right-hand-sides +===================================== + +fun do + x -> + y -> +end + +--- + +(source + (call + (identifier) + (do_block + (stab_clause + (arguments + (identifier))) + (stab_clause + (arguments + (identifier)))))) + ===================================== pattern matching ===================================== diff --git a/test/highlight/anonymous.ex b/test/highlight/anonymous.ex index 0adfde4..760c8ba 100644 --- a/test/highlight/anonymous.ex +++ b/test/highlight/anonymous.ex @@ -41,6 +41,12 @@ fn x, y, z -> end end +fn -> +# <- keyword +# ^ operator +end +# <- keyword + &Set.put(&1, &2) # <- operator # ^ module