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.
This commit is contained in:
Michael Davis 2022-04-11 19:03:34 -05:00
parent c68c4ad72f
commit 662426cd85
No known key found for this signature in database
GPG Key ID: 25D3AFE4BA2A0C49
3 changed files with 52 additions and 7 deletions

View File

@ -159,6 +159,16 @@ module.exports = grammar({
// * stab arguments item in `arg1, left when right ->` // * stab arguments item in `arg1, left when right ->`
[$.binary_operator, $._stab_clause_arguments_without_parentheses], [$.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: // Given `((arg1, arg2 • ,`, `arg3` expression can be either:
// * stab parenthesised arguments item in `((arg1, arg2, arg3) ->)` // * stab parenthesised arguments item in `((arg1, arg2, arg3) ->)`
// * stab non-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: ($) => stab_clause: ($) =>
// Right precedence, because we want to consume body if any choice($._stab_clause_with_body, $._stab_clause_without_body),
prec.right(
seq( _stab_clause_with_body: ($) =>
optional(field("left", $._stab_clause_left)), seq(
field("operator", "->"), optional(field("left", $._stab_clause_left)),
optional(field("right", $.body)) field("operator", "->"),
) field("right", $.body)
),
_stab_clause_without_body: ($) =>
seq(
optional(field("left", $._stab_clause_left)),
field("operator", "->"),
optional($._terminator)
), ),
_stab_clause_left: ($) => _stab_clause_left: ($) =>

View File

@ -683,6 +683,28 @@ end
(body (body
(integer)))))) (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 pattern matching
===================================== =====================================

View File

@ -41,6 +41,12 @@ fn x, y, z ->
end end
end end
fn ->
# <- keyword
# ^ operator
end
# <- keyword
&Set.put(&1, &2) &Set.put(&1, &2)
# <- operator # <- operator
# ^ module # ^ module