From 9228d2c55f2e29e458429b661d611ed70ca8844c Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 09:57:16 -0400 Subject: [PATCH 01/20] Replace air's tree-sitter-r parser with aether's Copying the aether parser in, directly overwriting the air one. This turned out to be the easiest approach for now, and we can split back out later if needed. --- .gitattributes | 1 - Cargo.lock | 76 +- Cargo.toml | 6 + crates/air_r_parser/Cargo.toml | 12 +- crates/air_r_parser/src/diagnostics.rs | 223 ++ crates/air_r_parser/src/lexer.rs | 877 +++++ crates/air_r_parser/src/lexer/tests.rs | 176 + .../air_r_parser/src/lexer/tests/backticks.rs | 49 + crates/air_r_parser/src/lexer/tests/bom.rs | 30 + .../air_r_parser/src/lexer/tests/brackets.rs | 112 + .../src/lexer/tests/checkpoint.rs | 94 + .../air_r_parser/src/lexer/tests/comments.rs | 33 + .../air_r_parser/src/lexer/tests/coverage.rs | 66 + crates/air_r_parser/src/lexer/tests/dots.rs | 64 + .../src/lexer/tests/error_token.rs | 56 + .../src/lexer/tests/identifiers.rs | 43 + .../air_r_parser/src/lexer/tests/keywords.rs | 60 + .../src/lexer/tests/line_breaks.rs | 58 + .../air_r_parser/src/lexer/tests/numbers.rs | 105 + crates/air_r_parser/src/lexer/tests/puncts.rs | 111 + .../src/lexer/tests/raw_strings.rs | 72 + .../air_r_parser/src/lexer/tests/specials.rs | 53 + .../air_r_parser/src/lexer/tests/strings.rs | 78 + .../src/lexer/tests/whitespace.rs | 30 + crates/air_r_parser/src/lib.rs | 14 +- crates/air_r_parser/src/parse.rs | 1559 +------- crates/air_r_parser/src/parser.rs | 118 + crates/air_r_parser/src/syntax.rs | 20 + .../air_r_parser/src/syntax/associativity.rs | 22 + crates/air_r_parser/src/syntax/binary.rs | 27 + .../air_r_parser/src/syntax/binding_power.rs | 34 + crates/air_r_parser/src/syntax/body.rs | 15 + .../src/syntax/braced_statements.rs | 17 + crates/air_r_parser/src/syntax/call.rs | 199 + crates/air_r_parser/src/syntax/expr.rs | 239 ++ crates/air_r_parser/src/syntax/extract.rs | 21 + crates/air_r_parser/src/syntax/function.rs | 125 + .../air_r_parser/src/syntax/if_statement.rs | 46 + crates/air_r_parser/src/syntax/literal.rs | 15 + .../src/syntax/loop_statements.rs | 90 + crates/air_r_parser/src/syntax/namespace.rs | 43 + .../src/syntax/parenthesized_expression.rs | 21 + crates/air_r_parser/src/syntax/root.rs | 13 + crates/air_r_parser/src/syntax/selector.rs | 32 + .../air_r_parser/src/syntax/statement_list.rs | 83 + crates/air_r_parser/src/syntax/unary.rs | 20 + crates/air_r_parser/src/token_source.rs | 118 + crates/air_r_parser/src/token_source/tests.rs | 211 ++ crates/air_r_parser/src/treesitter.rs | 196 - .../error/call/side_by_side_arguments.R | 2 - .../error/call/side_by_side_arguments.R.snap | 51 - .../call_lhs_double_colon.R | 1 - .../call_lhs_double_colon.R.snap | 49 - .../call_lhs_triple_colon.R | 1 - .../call_lhs_triple_colon.R.snap | 49 - .../chained_double_colon.R | 1 - .../chained_double_colon.R.snap | 49 - .../chained_triple_colon.R | 1 - .../chained_triple_colon.R.snap | 49 - .../error/parenthesized_expression/empty.R | 1 - .../parenthesized_expression/empty.R.snap | 49 - .../error/parenthesized_expression/multiple.R | 4 - .../parenthesized_expression/multiple.R.snap | 52 - .../snapshots/ok/braced_expressions.R.snap | 193 - .../air_r_parser/tests/snapshots/ok/calls.R | 58 - .../tests/snapshots/ok/calls.R.snap | 1259 ------- .../tests/snapshots/ok/comments.R | 3 - .../tests/snapshots/ok/comments.R.snap | 54 - .../ok/crlf/multiline_string_value.R | 2 - .../ok/crlf/multiline_string_value.R.snap | 43 - .../snapshots/ok/crlf/trailing_comments.R | 3 - .../ok/crlf/trailing_comments.R.snap | 54 - .../tests/snapshots/ok/dot_dot_i.R | 10 - .../tests/snapshots/ok/dot_dot_i.R.snap | 153 - crates/air_r_parser/tests/snapshots/ok/dots.R | 7 - .../tests/snapshots/ok/dots.R.snap | 95 - .../tests/snapshots/ok/extract_expression.R | 26 - .../snapshots/ok/extract_expression.R.snap | 485 --- .../tests/snapshots/ok/for_statement.R | 13 - .../tests/snapshots/ok/for_statement.R.snap | 137 - .../tests/snapshots/ok/function_definition.R | 17 - .../snapshots/ok/function_definition.R.snap | 680 ---- .../tests/snapshots/ok/if_statement.R | 51 - .../tests/snapshots/ok/if_statement.R.snap | 599 --- .../tests/snapshots/ok/namespace_expression.R | 32 - .../snapshots/ok/namespace_expression.R.snap | 571 --- .../snapshots/ok/parenthesized_expression.R | 13 - .../ok/parenthesized_expression.R.snap | 144 - .../tests/snapshots/ok/repeat_statement.R | 8 - .../snapshots/ok/repeat_statement.R.snap | 113 - .../ok/semicolons/semicolon-end-of-file-01.R | 2 - .../semicolon-end-of-file-01.R.snap | 43 - .../ok/semicolons/semicolon-end-of-file-02.R | 1 - .../semicolon-end-of-file-02.R.snap | 42 - .../ok/semicolons/semicolon-end-of-file-03.R | 3 - .../semicolon-end-of-file-03.R.snap | 44 - .../semicolons/semicolon-start-of-file-01.R | 2 - .../semicolon-start-of-file-01.R.snap | 43 - .../semicolons/semicolon-start-of-file-02.R | 1 - .../semicolon-start-of-file-02.R.snap | 42 - .../snapshots/ok/semicolons/semicolons.R | 16 - .../snapshots/ok/semicolons/semicolons.R.snap | 187 - .../air_r_parser/tests/snapshots/ok/subset2.R | 41 - .../tests/snapshots/ok/unary_expressions.R | 18 - .../snapshots/ok/unary_expressions.R.snap | 204 -- .../tests/snapshots/ok/value/complex_value.R | 7 - .../snapshots/ok/value/complex_value.R.snap | 88 - .../tests/snapshots/ok/value/double_value.R | 37 - .../snapshots/ok/value/double_value.R.snap | 337 -- .../tests/snapshots/ok/value/integer_value.R | 35 - .../snapshots/ok/value/integer_value.R.snap | 315 -- .../tests/snapshots/ok/value/string_value.R | 33 - .../snapshots/ok/value/string_value.R.snap | 354 -- .../tests/snapshots/ok/while_statement.R | 11 - .../tests/snapshots/ok/while_statement.R.snap | 162 - .../undefined/extract_expression_error.R | 29 - .../undefined/extract_expression_error.R.snap | 440 --- .../undefined/namespace_expression_error.R | 29 - .../namespace_expression_error.R.snap | 440 --- crates/air_r_parser/tests/spec_test.rs | 141 - crates/air_r_parser/tests/spec_tests.rs | 9 - crates/air_r_parser_tests/Cargo.toml | 28 + crates/air_r_parser_tests/build.rs | 4 + crates/air_r_parser_tests/src/check.rs | 187 + crates/air_r_parser_tests/src/corpus.rs | 74 + crates/air_r_parser_tests/src/document.rs | 540 +++ crates/air_r_parser_tests/src/lib.rs | 14 + crates/air_r_parser_tests/src/runner.rs | 96 + .../tests/corpus/control/for.md | 69 + .../tests/corpus/control/for.snap.md | 538 +++ .../tests/corpus/control/if.md | 259 ++ .../tests/corpus/control/if.snap.md | 1663 +++++++++ .../tests/corpus/control/repeat.md | 22 + .../tests/corpus/control/repeat.snap.md | 112 + .../tests/corpus/control/while.md | 43 + .../tests/corpus/control/while.snap.md | 274 ++ .../tests/corpus/expressions/assignment.md | 12 + .../corpus/expressions/assignment.snap.md | 147 + .../tests/corpus/expressions/binary.md} | 42 + .../tests/corpus/expressions/binary.snap.md} | 550 ++- .../tests/corpus/expressions/call.md | 244 ++ .../tests/corpus/expressions/call.snap.md | 3220 +++++++++++++++++ .../tests/corpus/expressions/colon.md | 6 + .../tests/corpus/expressions/colon.snap.md | 54 + .../tests/corpus/expressions/extract.md | 134 + .../tests/corpus/expressions/extract.snap.md | 1713 +++++++++ .../tests/corpus/expressions/formula.md | 6 + .../tests/corpus/expressions/formula.snap.md | 38 + .../tests/corpus/expressions/help.md | 7 + .../tests/corpus/expressions/help.snap.md | 57 + .../tests/corpus/expressions/namespace.md | 134 + .../corpus/expressions/namespace.snap.md | 1475 ++++++++ .../tests/corpus/expressions/parenthesized.md | 47 + .../corpus/expressions/parenthesized.snap.md | 379 ++ .../tests/corpus/expressions/precedence.md | 42 + .../corpus/expressions/precedence.snap.md | 626 ++++ .../tests/corpus/expressions/specials.md | 46 + .../tests/corpus/expressions/specials.snap.md | 463 +++ .../tests/corpus/expressions/subset.md} | 32 + .../tests/corpus/expressions/subset.snap.md} | 984 +++-- .../tests/corpus/expressions/subset2.md | 96 + .../tests/corpus/expressions/subset2.snap.md} | 1056 ++++-- .../tests/corpus/expressions/unary.md | 36 + .../tests/corpus/expressions/unary.snap.md | 257 ++ .../tests/corpus/functions/definition.md | 106 + .../tests/corpus/functions/definition.snap.md | 1173 ++++++ .../tests/corpus/functions/lambda.md | 18 + .../tests/corpus/functions/lambda.snap.md | 385 ++ .../tests/corpus/functions/parameters.md | 91 + .../tests/corpus/functions/parameters.snap.md | 1114 ++++++ .../tests/corpus/literals/comments.md | 26 + .../tests/corpus/literals/comments.snap.md | 85 + .../tests/corpus/literals/constants.md} | 4 + .../tests/corpus/literals/constants.snap.md} | 74 +- .../tests/corpus/literals/identifiers.md | 81 + .../tests/corpus/literals/identifiers.snap.md | 429 +++ .../tests/corpus/literals/numbers.md | 131 + .../tests/corpus/literals/numbers.snap.md | 679 ++++ .../tests/corpus/literals/strings.md | 186 + .../tests/corpus/literals/strings.snap.md | 1017 ++++++ .../corpus/recovery/unmatched-delimiters.md | 70 + .../recovery/unmatched-delimiters.snap.md | 595 +++ .../tests/corpus/statements/braces.md} | 24 +- .../tests/corpus/statements/braces.snap.md | 217 ++ .../tests/corpus/statements/newlines.md | 37 + .../tests/corpus/statements/newlines.snap.md | 217 ++ .../tests/corpus/statements/semicolons.md | 134 + .../corpus/statements/semicolons.snap.md | 817 +++++ crates/air_r_parser_tests/tests/main.rs | 6 + crates/air_r_parser_tests/tests/quick_test.rs | 20 + crates/air_r_syntax/src/lib.rs | 2 + crates/air_r_syntax/src/string_ext.rs | 153 + crates/air_r_syntax/src/syntax_node.rs | 3 +- crates/air_r_test_macros/Cargo.toml | 23 + crates/air_r_test_macros/src/lib.rs | 153 + crates/biome_ungrammar/Cargo.lock | 21 - .../snapshots/crates__tests__crate_names.snap | 2 + plans/aether.md | 26 +- 198 files changed, 26781 insertions(+), 11285 deletions(-) create mode 100644 crates/air_r_parser/src/diagnostics.rs create mode 100644 crates/air_r_parser/src/lexer.rs create mode 100644 crates/air_r_parser/src/lexer/tests.rs create mode 100644 crates/air_r_parser/src/lexer/tests/backticks.rs create mode 100644 crates/air_r_parser/src/lexer/tests/bom.rs create mode 100644 crates/air_r_parser/src/lexer/tests/brackets.rs create mode 100644 crates/air_r_parser/src/lexer/tests/checkpoint.rs create mode 100644 crates/air_r_parser/src/lexer/tests/comments.rs create mode 100644 crates/air_r_parser/src/lexer/tests/coverage.rs create mode 100644 crates/air_r_parser/src/lexer/tests/dots.rs create mode 100644 crates/air_r_parser/src/lexer/tests/error_token.rs create mode 100644 crates/air_r_parser/src/lexer/tests/identifiers.rs create mode 100644 crates/air_r_parser/src/lexer/tests/keywords.rs create mode 100644 crates/air_r_parser/src/lexer/tests/line_breaks.rs create mode 100644 crates/air_r_parser/src/lexer/tests/numbers.rs create mode 100644 crates/air_r_parser/src/lexer/tests/puncts.rs create mode 100644 crates/air_r_parser/src/lexer/tests/raw_strings.rs create mode 100644 crates/air_r_parser/src/lexer/tests/specials.rs create mode 100644 crates/air_r_parser/src/lexer/tests/strings.rs create mode 100644 crates/air_r_parser/src/lexer/tests/whitespace.rs create mode 100644 crates/air_r_parser/src/parser.rs create mode 100644 crates/air_r_parser/src/syntax.rs create mode 100644 crates/air_r_parser/src/syntax/associativity.rs create mode 100644 crates/air_r_parser/src/syntax/binary.rs create mode 100644 crates/air_r_parser/src/syntax/binding_power.rs create mode 100644 crates/air_r_parser/src/syntax/body.rs create mode 100644 crates/air_r_parser/src/syntax/braced_statements.rs create mode 100644 crates/air_r_parser/src/syntax/call.rs create mode 100644 crates/air_r_parser/src/syntax/expr.rs create mode 100644 crates/air_r_parser/src/syntax/extract.rs create mode 100644 crates/air_r_parser/src/syntax/function.rs create mode 100644 crates/air_r_parser/src/syntax/if_statement.rs create mode 100644 crates/air_r_parser/src/syntax/literal.rs create mode 100644 crates/air_r_parser/src/syntax/loop_statements.rs create mode 100644 crates/air_r_parser/src/syntax/namespace.rs create mode 100644 crates/air_r_parser/src/syntax/parenthesized_expression.rs create mode 100644 crates/air_r_parser/src/syntax/root.rs create mode 100644 crates/air_r_parser/src/syntax/selector.rs create mode 100644 crates/air_r_parser/src/syntax/statement_list.rs create mode 100644 crates/air_r_parser/src/syntax/unary.rs create mode 100644 crates/air_r_parser/src/token_source.rs create mode 100644 crates/air_r_parser/src/token_source/tests.rs delete mode 100644 crates/air_r_parser/src/treesitter.rs delete mode 100644 crates/air_r_parser/tests/snapshots/error/call/side_by_side_arguments.R delete mode 100644 crates/air_r_parser/tests/snapshots/error/call/side_by_side_arguments.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_double_colon.R delete mode 100644 crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_double_colon.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_triple_colon.R delete mode 100644 crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_triple_colon.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_double_colon.R delete mode 100644 crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_double_colon.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_triple_colon.R delete mode 100644 crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_triple_colon.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/error/parenthesized_expression/empty.R delete mode 100644 crates/air_r_parser/tests/snapshots/error/parenthesized_expression/empty.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/error/parenthesized_expression/multiple.R delete mode 100644 crates/air_r_parser/tests/snapshots/error/parenthesized_expression/multiple.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/braced_expressions.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/calls.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/calls.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/comments.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/comments.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/crlf/multiline_string_value.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/crlf/multiline_string_value.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/crlf/trailing_comments.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/crlf/trailing_comments.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/dot_dot_i.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/dot_dot_i.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/dots.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/dots.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/extract_expression.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/extract_expression.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/for_statement.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/for_statement.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/function_definition.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/function_definition.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/if_statement.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/if_statement.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/namespace_expression.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/namespace_expression.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/parenthesized_expression.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/parenthesized_expression.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/repeat_statement.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/repeat_statement.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-01.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-01.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-02.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-02.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-03.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-03.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-01.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-01.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-02.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-02.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolons.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/semicolons/semicolons.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/subset2.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/unary_expressions.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/unary_expressions.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/value/complex_value.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/value/complex_value.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/value/double_value.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/value/double_value.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/value/integer_value.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/value/integer_value.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/value/string_value.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/value/string_value.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/ok/while_statement.R delete mode 100644 crates/air_r_parser/tests/snapshots/ok/while_statement.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/undefined/extract_expression_error.R delete mode 100644 crates/air_r_parser/tests/snapshots/undefined/extract_expression_error.R.snap delete mode 100644 crates/air_r_parser/tests/snapshots/undefined/namespace_expression_error.R delete mode 100644 crates/air_r_parser/tests/snapshots/undefined/namespace_expression_error.R.snap delete mode 100644 crates/air_r_parser/tests/spec_test.rs delete mode 100644 crates/air_r_parser/tests/spec_tests.rs create mode 100644 crates/air_r_parser_tests/Cargo.toml create mode 100644 crates/air_r_parser_tests/build.rs create mode 100644 crates/air_r_parser_tests/src/check.rs create mode 100644 crates/air_r_parser_tests/src/corpus.rs create mode 100644 crates/air_r_parser_tests/src/document.rs create mode 100644 crates/air_r_parser_tests/src/lib.rs create mode 100644 crates/air_r_parser_tests/src/runner.rs create mode 100644 crates/air_r_parser_tests/tests/corpus/control/for.md create mode 100644 crates/air_r_parser_tests/tests/corpus/control/for.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/control/if.md create mode 100644 crates/air_r_parser_tests/tests/corpus/control/if.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/control/repeat.md create mode 100644 crates/air_r_parser_tests/tests/corpus/control/repeat.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/control/while.md create mode 100644 crates/air_r_parser_tests/tests/corpus/control/while.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/assignment.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/assignment.snap.md rename crates/{air_r_parser/tests/snapshots/ok/binary_expressions.R => air_r_parser_tests/tests/corpus/expressions/binary.md} (51%) rename crates/{air_r_parser/tests/snapshots/ok/binary_expressions.R.snap => air_r_parser_tests/tests/corpus/expressions/binary.snap.md} (59%) create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/call.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/call.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/colon.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/colon.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/extract.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/extract.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/formula.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/formula.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/help.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/help.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/namespace.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/namespace.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/parenthesized.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/parenthesized.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/precedence.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/precedence.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/specials.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/specials.snap.md rename crates/{air_r_parser/tests/snapshots/ok/subset.R => air_r_parser_tests/tests/corpus/expressions/subset.md} (58%) rename crates/{air_r_parser/tests/snapshots/ok/subset.R.snap => air_r_parser_tests/tests/corpus/expressions/subset.snap.md} (50%) create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/subset2.md rename crates/{air_r_parser/tests/snapshots/ok/subset2.R.snap => air_r_parser_tests/tests/corpus/expressions/subset2.snap.md} (50%) create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/unary.md create mode 100644 crates/air_r_parser_tests/tests/corpus/expressions/unary.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/functions/definition.md create mode 100644 crates/air_r_parser_tests/tests/corpus/functions/definition.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/functions/lambda.md create mode 100644 crates/air_r_parser_tests/tests/corpus/functions/lambda.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/functions/parameters.md create mode 100644 crates/air_r_parser_tests/tests/corpus/functions/parameters.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/literals/comments.md create mode 100644 crates/air_r_parser_tests/tests/corpus/literals/comments.snap.md rename crates/{air_r_parser/tests/snapshots/ok/keyword.R => air_r_parser_tests/tests/corpus/literals/constants.md} (79%) rename crates/{air_r_parser/tests/snapshots/ok/keyword.R.snap => air_r_parser_tests/tests/corpus/literals/constants.snap.md} (59%) create mode 100644 crates/air_r_parser_tests/tests/corpus/literals/identifiers.md create mode 100644 crates/air_r_parser_tests/tests/corpus/literals/identifiers.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/literals/numbers.md create mode 100644 crates/air_r_parser_tests/tests/corpus/literals/numbers.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/literals/strings.md create mode 100644 crates/air_r_parser_tests/tests/corpus/literals/strings.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/recovery/unmatched-delimiters.md create mode 100644 crates/air_r_parser_tests/tests/corpus/recovery/unmatched-delimiters.snap.md rename crates/{air_r_parser/tests/snapshots/ok/braced_expressions.R => air_r_parser_tests/tests/corpus/statements/braces.md} (55%) create mode 100644 crates/air_r_parser_tests/tests/corpus/statements/braces.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/statements/newlines.md create mode 100644 crates/air_r_parser_tests/tests/corpus/statements/newlines.snap.md create mode 100644 crates/air_r_parser_tests/tests/corpus/statements/semicolons.md create mode 100644 crates/air_r_parser_tests/tests/corpus/statements/semicolons.snap.md create mode 100644 crates/air_r_parser_tests/tests/main.rs create mode 100644 crates/air_r_parser_tests/tests/quick_test.rs create mode 100644 crates/air_r_syntax/src/string_ext.rs create mode 100644 crates/air_r_test_macros/Cargo.toml create mode 100644 crates/air_r_test_macros/src/lib.rs delete mode 100644 crates/biome_ungrammar/Cargo.lock diff --git a/.gitattributes b/.gitattributes index 2fc638e7..9a5b8767 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,3 @@ # Windows specific test files where we need CRLF endings crates/air/fixtures/crlf/*.R text eol=crlf -crates/air_r_parser/tests/snapshots/ok/crlf/*.R text eol=crlf diff --git a/Cargo.lock b/Cargo.lock index fde560ed..400f0647 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,14 +109,24 @@ version = "0.0.0" dependencies = [ "air_r_factory", "air_r_syntax", + "biome_diagnostics", "biome_parser", "biome_rowan", - "biome_unicode_table", - "insta", - "tests_macros", - "tracing", - "tree-sitter", - "tree-sitter-r", + "unicode-ident", +] + +[[package]] +name = "air_r_parser_tests" +version = "0.0.0" +dependencies = [ + "air_r_parser", + "air_r_syntax", + "air_r_test_macros", + "biome_console", + "biome_diagnostics", + "biome_parser", + "biome_rowan", + "similar", ] [[package]] @@ -127,6 +137,14 @@ dependencies = [ "serde", ] +[[package]] +name = "air_r_test_macros" +version = "0.0.0" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "anstream" version = "0.6.17" @@ -1654,18 +1672,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" version = "0.4.8" @@ -1963,12 +1969,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" - [[package]] name = "strsim" version = "0.11.1" @@ -2379,34 +2379,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tree-sitter" -version = "0.24.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75" -dependencies = [ - "cc", - "regex", - "regex-syntax", - "streaming-iterator", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-language" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ddffe35a0e5eeeadf13ff7350af564c6e73993a24db62caee1822b185c2600" - -[[package]] -name = "tree-sitter-r" -version = "1.2.0" -source = "git+https://github.com/r-lib/tree-sitter-r?rev=d459b97a9a11dd81643ab674938bede2874663b0#d459b97a9a11dd81643ab674938bede2874663b0" -dependencies = [ - "cc", - "tree-sitter-language", -] - [[package]] name = "typeid" version = "1.0.3" diff --git a/Cargo.toml b/Cargo.toml index bf7a40de..6f50e4d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,13 @@ air_formatter_test = { path = "./crates/air_formatter_test" } air_r_factory = { path = "./crates/air_r_factory" } air_r_formatter = { path = "./crates/air_r_formatter" } air_r_parser = { path = "./crates/air_r_parser" } +air_r_parser_tests = { path = "./crates/air_r_parser_tests" } air_r_syntax = { path = "./crates/air_r_syntax" } +air_r_test_macros = { path = "./crates/air_r_test_macros" } anyhow = "1.0.89" assert_matches = "1.5.0" +biome_console = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } +biome_diagnostics = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } biome_formatter = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } biome_line_index = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } biome_parser = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } @@ -60,6 +64,7 @@ schemars = "0.9.0" serde = "1.0.215" serde_json = "1.0.132" settings = { path = "./crates/settings" } +similar = "2.6" similar-asserts = "1.6.0" struct-field-names-as-array = "0.3.0" strum = "0.26" @@ -76,6 +81,7 @@ tracing = { version = "0.1.40", default-features = false, features = ["std"] } tracing-subscriber = "0.3.19" tree-sitter = "0.24.7" tree-sitter-r = { git = "https://github.com/r-lib/tree-sitter-r", rev = "d459b97a9a11dd81643ab674938bede2874663b0" } +unicode-ident = "1.0" url = "2.5.3" uuid = { version = "1.11.0", features = ["v4"] } workspace = { path = "./crates/workspace" } diff --git a/crates/air_r_parser/Cargo.toml b/crates/air_r_parser/Cargo.toml index fced5a63..5a566a40 100644 --- a/crates/air_r_parser/Cargo.toml +++ b/crates/air_r_parser/Cargo.toml @@ -12,23 +12,15 @@ keywords.workspace = true categories.workspace = true publish = false -# cargo-workspaces metadata -[package.metadata.workspaces] -independent = true - [dependencies] air_r_factory = { workspace = true } air_r_syntax = { workspace = true } biome_parser = { workspace = true } biome_rowan = { workspace = true } -biome_unicode_table = { workspace = true } -tracing = { workspace = true } -tree-sitter = { workspace = true } -tree-sitter-r = { workspace = true } +unicode-ident = { workspace = true } [dev-dependencies] -insta = { workspace = true } -tests_macros = { workspace = true } +biome_diagnostics = { workspace = true } [lints] workspace = true diff --git a/crates/air_r_parser/src/diagnostics.rs b/crates/air_r_parser/src/diagnostics.rs new file mode 100644 index 00000000..1466feef --- /dev/null +++ b/crates/air_r_parser/src/diagnostics.rs @@ -0,0 +1,223 @@ +//! Constructors for every diagnostic the parser emits + +use crate::parser::RParser; +use air_r_syntax::RSyntaxKind; +use biome_parser::Parser; +use biome_parser::diagnostic::ParseDiagnostic; +use biome_parser::token_source::TokenSource; +use biome_rowan::TextLen; +use biome_rowan::TextRange; +use std::borrow::Cow; + +/// A backtick-quoted name that runs to the end of the file +pub(crate) fn unterminated_backtick_identifier(open: TextRange, eof: TextRange) -> ParseDiagnostic { + ParseDiagnostic::new("Unterminated backtick-quoted name.", open) + .with_detail(eof, "Reached the end of the file looking for a closing `.") +} + +/// A `%...%` operator cut short by a newline or the end of the file +pub(crate) fn unterminated_special(open: TextRange) -> ParseDiagnostic { + ParseDiagnostic::new("Unterminated `%` operator.", open) + .with_hint("A `%...%` operator must be closed by a second `%` on the same line.") +} + +/// A `0x` with no body, as in `0x` or `0xg` +pub(crate) fn unterminated_hexadecimal(range: TextRange) -> ParseDiagnostic { + ParseDiagnostic::new("Unterminated hexadecimal literal.", range) + .with_hint("`0x` must be followed by hexadecimal digits, a `.`, or both.") +} + +/// A `p` exponent with no digits, as in `0x1p` or `0x1p+` +pub(crate) fn unterminated_hexadecimal_binary_exponent(range: TextRange) -> ParseDiagnostic { + ParseDiagnostic::new("Unterminated binary exponent.", range) + .with_hint("A `p` exponent must be followed by decimal digits.") +} + +/// An `e` exponent with no digits, as in `1e` or `1e+` +pub(crate) fn unterminated_exponent(range: TextRange) -> ParseDiagnostic { + ParseDiagnostic::new("Unterminated exponent.", range) + .with_hint("An `e` exponent must be followed by decimal digits.") +} + +/// A character that starts no known R token +pub(crate) fn unexpected_character(range: TextRange, character: char) -> ParseDiagnostic { + ParseDiagnostic::new(format!("Unexpected character `{character}`."), range) +} + +/// A string that runs to the end of the file +pub(crate) fn unterminated_string(open: TextRange, eof: TextRange) -> ParseDiagnostic { + ParseDiagnostic::new("Unterminated string.", open).with_detail( + eof, + "Reached the end of the file looking for a closing delimiter.", + ) +} + +pub(crate) fn expected_expression(p: &RParser, range: TextRange) -> ParseDiagnostic { + expected_node("expression", range, p) +} + +/// The name on the right of `$`, `@`, `::`, or `:::` is missing +pub(crate) fn expected_rhs_selector( + operator: RSyntaxKind, + operator_range: TextRange, + range: TextRange, +) -> ParseDiagnostic { + ParseDiagnostic::new("Expected a name.", range).with_detail( + operator_range, + format!("`{}` requires a name on the right.", text_of(operator)), + ) +} + +/// The name on the left of `::` or `:::` is not a name +pub(crate) fn expected_lhs_selector( + operator: RSyntaxKind, + operator_range: TextRange, + range: TextRange, +) -> ParseDiagnostic { + ParseDiagnostic::new("Expected a name.", range).with_detail( + operator_range, + format!("`{}` requires a name on the left.", text_of(operator)), + ) +} + +/// A newline between `::` or `:::` and the name it needs +pub(crate) fn namespace_rhs_selector_after_line_break( + operator: RSyntaxKind, + operator_range: TextRange, +) -> ParseDiagnostic { + let operator = text_of(operator); + + ParseDiagnostic::new( + format!("A newline is not allowed between `{operator}` and the name that follows."), + operator_range, + ) +} + +/// The whole `(...)` group of a function definition is missing, as in `function 1` +pub(crate) fn expected_parameters(p: &RParser, range: TextRange) -> ParseDiagnostic { + expected_node("parameter list", range, p) +} + +/// A parameter with no name, as in `function(a,,b)` or `function(1)` +pub(crate) fn expected_parameter_name(p: &RParser, range: TextRange) -> ParseDiagnostic { + expected_node("parameter name", range, p) +} + +/// The condition of an `if` or `while` is missing +pub(crate) fn expected_condition(p: &RParser, range: TextRange) -> ParseDiagnostic { + expected_node("condition", range, p) +} + +/// The variable of a `for` loop is missing or is not a name, as in `for (1 in x) y` +pub(crate) fn expected_for_variable(p: &RParser, range: TextRange) -> ParseDiagnostic { + expected_node("loop variable", range, p) +} + +/// The sequence a `for` loop iterates over is missing +pub(crate) fn expected_for_sequence(p: &RParser, range: TextRange) -> ParseDiagnostic { + expected_node("loop sequence", range, p) +} + +/// The body of a `for`, `while`, or `repeat` loop is missing +pub(crate) fn expected_loop_body(p: &RParser, range: TextRange) -> ParseDiagnostic { + expected_node("loop body", range, p) +} + +/// The body of a function definition is missing, as in `function(x)` +pub(crate) fn expected_function_body(p: &RParser, range: TextRange) -> ParseDiagnostic { + expected_node("function body", range, p) +} + +pub(crate) fn expected_if_body(p: &RParser, range: TextRange) -> ParseDiagnostic { + expected_node("if body", range, p) +} + +pub(crate) fn expected_else_body(p: &RParser, range: TextRange) -> ParseDiagnostic { + expected_node("else body", range, p) +} + +/// Two expressions with nothing between them, as in `1 2` +pub(crate) fn expected_statement_separator( + previous: TextRange, + range: TextRange, +) -> ParseDiagnostic { + ParseDiagnostic::new( + "Expected a newline or a `;` between two expressions.", + range, + ) + .with_detail(previous, "The first expression ends here.") +} + +/// A `;` with no expression before it on the same line, as in `;1` or `1\n;` +pub(crate) fn unexpected_semicolon(range: TextRange) -> ParseDiagnostic { + ParseDiagnostic::new("Unexpected `;`.", range) + .with_hint("At top level, a `;` must directly follow an expression.") +} + +/// Like [biome_parser::diagnostic::expected_node()], but without the duplicative hint +fn expected_node(name: &str, range: TextRange, p: &RParser) -> ParseDiagnostic { + let article = article_for(name); + + let message = if p.source().text().text_len() <= range.start() { + format!("Expected {article} {name} but instead found the end of the file.") + } else { + format!( + "Expected {article} {name} but instead found '{}'.", + truncate(p.text(range)) + ) + }; + + ParseDiagnostic::new(message, range) +} + +fn truncate(text: &str) -> Cow<'_, str> { + const MAX_LEN: usize = 40; + + if text.chars().count() <= MAX_LEN { + Cow::Borrowed(text) + } else { + let mut truncated: String = text.chars().take(MAX_LEN).collect(); + truncated.push_str("..."); + Cow::Owned(truncated) + } +} + +fn article_for(name: &str) -> &'static str { + match name.bytes().next() { + Some(b'a' | b'e' | b'i' | b'o' | b'u') => "an", + _ => "a", + } +} + +fn text_of(operator: RSyntaxKind) -> &'static str { + operator.to_string().unwrap_or("operator") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn truncate_keeps_short_text_untouched() { + assert!(matches!(truncate("abc"), Cow::Borrowed("abc"))); + } + + #[test] + fn truncate_keeps_exactly_max_len_untouched() { + let text = "a".repeat(40); + assert!(matches!(truncate(&text), Cow::Borrowed(borrowed) if borrowed == text)); + } + + #[test] + fn truncate_cuts_and_appends_dots_past_max_len() { + let text = "a".repeat(41); + let expected = format!("{}...", "a".repeat(40)); + assert_eq!(&*truncate(&text), expected); + } + + #[test] + fn truncate_counts_chars_not_bytes() { + let text = "é".repeat(41); + assert_eq!(&*truncate(&text), format!("{}...", "é".repeat(40))); + } +} diff --git a/crates/air_r_parser/src/lexer.rs b/crates/air_r_parser/src/lexer.rs new file mode 100644 index 00000000..a413ef01 --- /dev/null +++ b/crates/air_r_parser/src/lexer.rs @@ -0,0 +1,877 @@ +#[cfg(test)] +mod tests; + +use crate::diagnostics::unexpected_character; +use crate::diagnostics::unterminated_backtick_identifier; +use crate::diagnostics::unterminated_exponent; +use crate::diagnostics::unterminated_hexadecimal; +use crate::diagnostics::unterminated_hexadecimal_binary_exponent; +use crate::diagnostics::unterminated_special; +use crate::diagnostics::unterminated_string; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxKind::*; +use air_r_syntax::T; +use biome_parser::diagnostic::ParseDiagnostic; +use biome_parser::lexer::LexContext; +use biome_parser::lexer::Lexer; +use biome_parser::lexer::LexerCheckpoint; +use biome_parser::lexer::LexerWithCheckpoint; +use biome_parser::lexer::ReLexer; +use biome_parser::lexer::TokenFlags; +use biome_rowan::SyntaxKind; +use biome_rowan::TextRange; +use biome_rowan::TextSize; +use unicode_ident::is_xid_continue; +use unicode_ident::is_xid_start; + +/// Lex context for R +/// +/// There aren't any special contexts for R, so we just have a single `Regular` +/// context that everything is lexed in. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) enum RLexContext { + #[default] + Regular, +} + +impl LexContext for RLexContext { + fn is_regular(&self) -> bool { + // Because we only have `Regular`! + true + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RStringDelimiter { + Single, + Double, + /// `r"---[...]---"`, closed by the matching bracket, the same number of + /// hyphens, and the same quote + Raw { + quote: u8, + hyphens: u8, + bracket: RawStringBracket, + }, +} + +impl RStringDelimiter { + /// The raw string delimiter opening at the start of `text`, or `None` when + /// none does + /// + /// Reads no further than the delimiter itself + fn try_from_raw_string_open(text: &str) -> Option { + let bytes = text.as_bytes(); + + if !matches!(bytes.first()?, b'r' | b'R') { + return None; + } + + let quote = match bytes.get(1)? { + quote @ (b'\'' | b'"') => *quote, + _ => return None, + }; + + let hyphens = bytes[2..].iter().take_while(|byte| **byte == b'-').count(); + + // Bail on pathological 256 hyphens + let hyphens = u8::try_from(hyphens).ok()?; + let bracket = RawStringBracket::from_open(*bytes.get(2 + usize::from(hyphens))?)?; + + Some(Self::Raw { + quote, + hyphens, + bracket, + }) + } + + fn kind(self) -> RSyntaxKind { + match self { + Self::Single => R_SINGLE_QUOTED_STRING_LITERAL, + Self::Double => R_DOUBLE_QUOTED_STRING_LITERAL, + Self::Raw { .. } => R_RAW_STRING_LITERAL, + } + } + + /// The string quote character + fn quote(self) -> u8 { + match self { + Self::Single => b'\'', + Self::Double => b'"', + Self::Raw { quote, .. } => quote, + } + } + + /// The byte length of the opening delimiter + fn open_length(self) -> usize { + match self { + Self::Single | Self::Double => 1, + // `r`, the quote, the hyphens, and the bracket + Self::Raw { hyphens, .. } => 3 + usize::from(hyphens), + } + } + + /// The byte length of the closing delimiter + fn close_length(self) -> usize { + match self { + Self::Single | Self::Double => 1, + // The bracket, the hyphens, and the quote + Self::Raw { hyphens, .. } => 2 + usize::from(hyphens), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RawStringBracket { + Paren, + Bracket, + Brace, + Pipe, +} + +impl RawStringBracket { + fn from_open(byte: u8) -> Option { + match byte { + b'(' => Some(Self::Paren), + b'[' => Some(Self::Bracket), + b'{' => Some(Self::Brace), + b'|' => Some(Self::Pipe), + _ => None, + } + } + + fn close(self) -> u8 { + match self { + Self::Paren => b')', + Self::Bracket => b']', + Self::Brace => b'}', + Self::Pipe => b'|', + } + } +} + +/// The re-lexing requests the parser can make +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RReLexContext { + /// Re-lex `]` as `]]` when the very next byte is another `]`. A no-op on any + /// other current token. + JoinBrackets, +} + +impl RLexer<'_> { + fn re_lex_join_brackets(&mut self) -> RSyntaxKind { + match self.current_kind { + T![']'] if self.peek_byte() == Some(b']') => self.eat_bytes(2, T!["]]"]), + kind => kind, + } + } +} + +pub(crate) struct RLexer<'src> { + source: &'src str, + + /// Byte offset of the next token + position: usize, + + /// Byte offset of the current token. Its range is `current_start..position`. + current_start: TextSize, + + current_kind: RSyntaxKind, + + current_flags: TokenFlags, + + /// Sticky across `next_token` calls: set by `consume_newline_or_whitespaces` + /// and cleared only once a non-trivia token has been stamped with it. This + /// is what lets the lexer report a preceding line break even though + /// `NEWLINE` is a token in its own right, and it is exactly + /// what `biome_js_parser` does. + after_line_break: bool, + + /// Byte length of the BOM, if the source starts with one + /// + /// Nothing in R reads this. It exists because [`LexerCheckpoint`] carries + /// it, and setting it from `consume_potential_bom` costs one line where a + /// hardcoded `0` would be a lie the next reader has to re-derive. + unicode_bom_length: usize, + + diagnostics: Vec, +} + +impl<'src> Lexer<'src> for RLexer<'src> { + const NEWLINE: Self::Kind = NEWLINE; + const WHITESPACE: Self::Kind = WHITESPACE; + + type Kind = RSyntaxKind; + type LexContext = RLexContext; + type ReLexContext = RReLexContext; + + fn source(&self) -> &'src str { + self.source + } + + fn current(&self) -> Self::Kind { + self.current_kind + } + + fn current_start(&self) -> TextSize { + self.current_start + } + + /// There's only one "Regular" context for R + fn next_token(&mut self, _context: Self::LexContext) -> Self::Kind { + self.current_start = self.text_position(); + self.current_flags = TokenFlags::empty(); + + let kind = if self.is_eof() { EOF } else { self.lex_token() }; + + // Stamped after lexing so that the lex functions are free to set other + // flags first + self.current_flags + .set(TokenFlags::PRECEDING_LINE_BREAK, self.after_line_break); + self.current_kind = kind; + + // The bit survives across trivia, so `1\n # c\n + 2` reports the break + // on `+` rather than on the whitespace that happens to precede it + if !kind.is_trivia() { + self.after_line_break = false; + } + + debug_assert!(self.current_start <= self.text_position()); + kind + } + + fn has_preceding_line_break(&self) -> bool { + self.current_flags.has_preceding_line_break() + } + + /// R has no token-level unicode escapes + fn has_unicode_escape(&self) -> bool { + false + } + + fn rewind(&mut self, checkpoint: LexerCheckpoint) { + let LexerCheckpoint { + position, + current_start, + current_kind, + current_flags, + after_line_break, + unicode_bom_length, + diagnostics_pos, + } = checkpoint; + + self.position = u32::from(position) as usize; + self.current_start = current_start; + self.current_kind = current_kind; + self.current_flags = current_flags; + self.after_line_break = after_line_break; + self.unicode_bom_length = unicode_bom_length; + self.diagnostics.truncate(diagnostics_pos as usize); + } + + fn finish(self) -> Vec { + self.diagnostics + } + + fn current_flags(&self) -> TokenFlags { + self.current_flags + } + + fn position(&self) -> usize { + self.position + } + + fn push_diagnostic(&mut self, diagnostic: ParseDiagnostic) { + self.diagnostics.push(diagnostic); + } + + fn advance(&mut self, n: usize) { + self.position += n; + } + + fn advance_char_unchecked(&mut self) { + let char = self.current_char_unchecked(); + self.position += char.len_utf8(); + } + + /// Overridden to update `after_line_break`. The default cannot since this is + /// a custom field. `biome_js_parser` overrides it for the same reason. + /// + /// Inheriting the trait's `consume_newline` also gets `\r\n` treated as a + /// single newline for free. + fn consume_newline_or_whitespaces(&mut self) -> Self::Kind { + if self.consume_newline() { + self.after_line_break = true; + NEWLINE + } else { + self.consume_whitespaces(); + WHITESPACE + } + } + + /// R's whitespace is ASCII: space, tab, vertical tab, and form feed + /// + /// The trait's body is written for CSS and diagnoses vertical tab and form + /// feed, which are ordinary whitespace here. + fn consume_whitespaces(&mut self) { + while let Some(b'\t' | b' ' | b'\x0B' | b'\x0C') = self.current_byte() { + self.advance(1); + } + } +} + +impl<'src> ReLexer<'src> for RLexer<'src> { + fn re_lex(&mut self, context: Self::ReLexContext) -> Self::Kind { + let old_position = self.position; + self.position = u32::from(self.current_start) as usize; + + let new_kind = match context { + RReLexContext::JoinBrackets => self.re_lex_join_brackets(), + }; + + if self.current_kind == new_kind { + // Nothing was re-lexed. Return the existing token untouched. + self.position = old_position; + } else { + self.current_kind = new_kind; + } + + new_kind + } +} + +impl<'src> LexerWithCheckpoint<'src> for RLexer<'src> { + fn checkpoint(&self) -> LexerCheckpoint { + LexerCheckpoint { + position: self.text_position(), + current_start: self.current_start, + current_kind: self.current_kind, + current_flags: self.current_flags, + after_line_break: self.after_line_break, + unicode_bom_length: self.unicode_bom_length, + diagnostics_pos: self.diagnostics.len() as u32, + } + } +} + +impl<'src> RLexer<'src> { + pub(crate) fn from_str(source: &'src str) -> Self { + Self { + source, + position: 0, + current_start: TextSize::from(0), + current_kind: TOMBSTONE, + current_flags: TokenFlags::empty(), + after_line_break: false, + unicode_bom_length: 0, + diagnostics: Vec::new(), + } + } + + fn lex_token(&mut self) -> RSyntaxKind { + debug_assert!(!self.is_eof()); + + match self.current_byte_unchecked() { + b'\n' | b'\r' | b'\t' | b' ' | b'\x0B' | b'\x0C' => { + self.consume_newline_or_whitespaces() + } + + b'#' => self.consume_comment(), + + b'"' => self.consume_string(RStringDelimiter::Double), + b'\'' => self.consume_string(RStringDelimiter::Single), + b'`' => self.consume_backtick_identifier(), + + // Ahead of the digit arm, which would otherwise take the `0` + b'0' if matches!(self.peek_byte(), Some(b'x' | b'X')) => self.consume_hexadecimal(), + b'0'..=b'9' => self.consume_number(), + + // `.` is overloaded four ways: `...`, `..1`, `.5`, and `.foo` + b'.' => self.consume_dot(), + + b'%' => self.consume_special(), + + // Raw strings share this arm with every other name + // rather than taking one of their own, which would split the letter + // range and cost the whole match its jump table for `s` onwards. + b'_' | b'a'..=b'z' | b'A'..=b'Z' => { + match RStringDelimiter::try_from_raw_string_open(&self.source[self.position..]) { + Some(delimiter) => self.consume_string(delimiter), + None => self.consume_identifier_or_keyword(), + } + } + + b';' => self.eat_byte(T![;]), + b',' => self.eat_byte(T![,]), + b'{' => self.eat_byte(T!['{']), + b'}' => self.eat_byte(T!['}']), + b'(' => self.eat_byte(T!['(']), + b')' => self.eat_byte(T![')']), + b'?' => self.eat_byte(T![?]), + b'~' => self.eat_byte(T![~]), + b'+' => self.eat_byte(T![+]), + b'/' => self.eat_byte(T![/]), + b'^' => self.eat_byte(T![^]), + b'$' => self.eat_byte(T![$]), + b'@' => self.eat_byte(T![@]), + b'\\' => self.eat_byte(T!["\\"]), + + // Lexer emits `]` but never `]]`. Only the parser can decide if we + // are in a [`RReLexContext::JoinBrackets`] context or not, at which + // point re-lexing will join contiguous brackets. + b']' => self.eat_byte(T![']']), + + // `[[` is greedy in R's own `gram.y` and in tree-sitter-r's scanner. + // Two adjacent opening brackets are always a subset2. + b'[' => match self.peek_byte() { + Some(b'[') => self.eat_bytes(2, T!["[["]), + _ => self.eat_byte(T!['[']), + }, + b'<' => match (self.peek_byte(), self.byte_at(2)) { + (Some(b'<'), Some(b'-')) => self.eat_bytes(3, T![<<-]), + (Some(b'-'), _) => self.eat_bytes(2, T![<-]), + (Some(b'='), _) => self.eat_bytes(2, T![<=]), + _ => self.eat_byte(T![<]), + }, + b'-' => match (self.peek_byte(), self.byte_at(2)) { + (Some(b'>'), Some(b'>')) => self.eat_bytes(3, T![->>]), + (Some(b'>'), _) => self.eat_bytes(2, T![->]), + _ => self.eat_byte(T![-]), + }, + b':' => match (self.peek_byte(), self.byte_at(2)) { + (Some(b':'), Some(b':')) => self.eat_bytes(3, T![:::]), + (Some(b':'), _) => self.eat_bytes(2, T![::]), + (Some(b'='), _) => self.eat_bytes(2, T![:=]), + _ => self.eat_byte(T![:]), + }, + b'>' => match self.peek_byte() { + Some(b'=') => self.eat_bytes(2, T![>=]), + _ => self.eat_byte(T![>]), + }, + b'=' => match self.peek_byte() { + Some(b'=') => self.eat_bytes(2, T![==]), + _ => self.eat_byte(T![=]), + }, + b'!' => match self.peek_byte() { + Some(b'=') => self.eat_bytes(2, T![!=]), + _ => self.eat_byte(T![!]), + }, + b'&' => match self.peek_byte() { + Some(b'&') => self.eat_bytes(2, T![&&]), + _ => self.eat_byte(T![&]), + }, + b'|' => match self.peek_byte() { + Some(b'|') => self.eat_bytes(2, T![||]), + Some(b'>') => self.eat_bytes(2, T![|>]), + _ => self.eat_byte(T![|]), + }, + b'*' => match self.peek_byte() { + Some(b'*') => self.eat_bytes(2, T![**]), + _ => self.eat_byte(T![*]), + }, + + byte if byte >= 128 => self.consume_unicode(), + + _ => self.consume_unexpected(), + } + } + + fn current_byte_unchecked(&self) -> u8 { + debug_assert!(!self.is_eof()); + self.source.as_bytes()[self.position] + } + + fn eat_byte(&mut self, kind: RSyntaxKind) -> RSyntaxKind { + self.advance(1); + kind + } + + fn eat_bytes(&mut self, n: usize, kind: RSyntaxKind) -> RSyntaxKind { + self.advance(n); + kind + } + + /// `#` to the end of the line, exclusive of the line break itself + fn consume_comment(&mut self) -> RSyntaxKind { + self.assert_byte(b'#'); + self.advance(1); + + // We're just scanning for the next `\n` or `\r`, so we don't need to worry about multi + // byte UTF-8 characters here. This is faster than calling `advance_byte_or_char()` in a loop. + let rest = &self.source.as_bytes()[self.position..]; + let len = rest + .iter() + .position(|byte| matches!(byte, b'\n' | b'\r')) + .unwrap_or(rest.len()); + self.advance(len); + + COMMENT + } + + // ----------------------------------------------------------------------- + // Identifiers and keywords + + /// Consumes an identifier mimicking `[\p{XID_Start}_][\p{XID_Continue}.]*`. + /// If the consumed identifier maps to a keyword, that is returned instead. + fn consume_identifier_or_keyword(&mut self) -> RSyntaxKind { + let start = self.position; + // `XID_Start` or `_` + self.advance_char_unchecked(); + self.consume_xid_continue_or_dot(); + + Self::keyword_kind(&self.source[start..self.position]).unwrap_or(IDENT) + } + + /// `[\p{XID_Continue}.]*` + fn consume_xid_continue_or_dot(&mut self) { + loop { + match self.current_byte() { + Some(b'.' | b'_' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z') => self.advance(1), + Some(byte) if byte >= 128 => { + if !is_xid_continue(self.current_char_unchecked()) { + break; + } + self.advance_char_unchecked(); + } + _ => break, + } + } + } + + /// R's keywords and constants, spelled as they appear in source + fn keyword_kind(text: &str) -> Option { + Some(match text { + "function" => T![function], + "for" => T![for], + "in" => T![in], + "while" => T![while], + "repeat" => T![repeat], + "if" => T![if], + "else" => T![else], + "next" => T![next], + "break" => T![break], + "TRUE" => T![true], + "FALSE" => T![false], + "NULL" => T![null], + "Inf" => T![inf], + "NaN" => T![nan], + "NA" => T![na_logical], + "NA_integer_" => T![na_integer], + "NA_real_" => T![na_double], + "NA_complex_" => T![na_complex], + "NA_character_" => T![na_character], + _ => return None, + }) + } + + fn consume_backtick_identifier(&mut self) -> RSyntaxKind { + self.assert_byte(b'`'); + let open = TextRange::at(self.text_position(), TextSize::from(1)); + self.advance(1); + + while let Some(byte) = self.current_byte() { + match byte { + b'`' => { + self.advance(1); + return IDENT; + } + b'\\' => { + self.advance(1); + if let Some(escaped) = self.current_byte() { + self.advance_byte_or_char(escaped); + } + } + byte => self.advance_byte_or_char(byte), + } + } + + let eof = TextRange::empty(self.text_position()); + self.push_diagnostic(unterminated_backtick_identifier(open, eof)); + ERROR_TOKEN + } + + /// The `byte >= 128` arm, which is also where the BOM is resolved + fn consume_unicode(&mut self) -> RSyntaxKind { + // A BOM is only a BOM at offset 0. Elsewhere it falls through as unexpected. + if self.position == 0 + && let Some((bom, length)) = self.consume_potential_bom(UNICODE_BOM) + { + self.unicode_bom_length = length; + return bom; + } + + if is_xid_start(self.current_char_unchecked()) { + self.consume_identifier_or_keyword() + } else { + self.consume_unexpected() + } + } + + // ----------------------------------------------------------------------- + // Dots + + /// `...`, `..1`, `.5`, and `.foo` all start with a `.` + /// + /// R decides between a number and a name on the single character after the + /// `.` (`typeofnext()` in `src/main/gram.y`), then consumes one whole name + /// and classifies its text. `...` and `..i` are not tokens of their own + /// there — `"..."` is a row in R's keyword table, and `..1` is an ordinary + /// symbol. + fn consume_dot(&mut self) -> RSyntaxKind { + self.assert_byte(b'.'); + + // A digit after the `.` always makes a number, never a name, otherwise + // `.1i` and `.1L` lex as identifiers (r-lib/tree-sitter-r#190). + if matches!(self.peek_byte(), Some(b'0'..=b'9')) { + return self.consume_number(); + } + + let start = self.position; + self.advance(1); + self.consume_xid_continue_or_dot(); + + Self::dot_kind(&self.source[start..self.position]) + } + + /// The two names the parser has to tell apart from any other identifier + fn dot_kind(text: &str) -> RSyntaxKind { + match text.as_bytes() { + b"..." => T![...], + [b'.', b'.', digits @ ..] + if !digits.is_empty() && digits.iter().all(u8::is_ascii_digit) => + { + DOTDOTI + } + _ => IDENT, + } + } + + // ----------------------------------------------------------------------- + // Numbers + + /// `(?:(?:\d+(?:\.\d*)?)|(?:\.\d+))(?:[eE][+-]?\d+)?` + fn consume_number(&mut self) -> RSyntaxKind { + let start = self.text_position(); + self.consume_digits(); + + if self.current_byte() == Some(b'.') { + self.advance(1); + self.consume_digits(); + } + + if matches!(self.current_byte(), Some(b'e' | b'E')) { + self.advance(1); + + if !self.consume_exponent() { + let range = TextRange::new(start, self.text_position()); + self.push_diagnostic(unterminated_exponent(range)); + } + } + + self.consume_suffix() + } + + /// `0[xX](([0-9a-fA-F]+(\.[0-9a-fA-F]*)?)|(\.[0-9a-fA-F]*))([pP][+-]?[0-9]+)?` + fn consume_hexadecimal(&mut self) -> RSyntaxKind { + let start = self.text_position(); + + // The `0x` + self.advance(2); + + let has_body = self + .current_byte() + .is_some_and(|byte| byte == b'.' || byte.is_ascii_hexdigit()); + + if !has_body { + let range = TextRange::new(start, self.text_position()); + self.push_diagnostic(unterminated_hexadecimal(range)); + return self.consume_suffix(); + } + + self.consume_hex_digits(); + + if self.current_byte() == Some(b'.') { + self.advance(1); + self.consume_hex_digits(); + } + + if matches!(self.current_byte(), Some(b'p' | b'P')) { + self.advance(1); + + if !self.consume_exponent() { + let range = TextRange::new(start, self.text_position()); + self.push_diagnostic(unterminated_hexadecimal_binary_exponent(range)); + } + } + + self.consume_suffix() + } + + /// The `L` and `i` that turn the literal before them into an integer or a + /// complex + fn consume_suffix(&mut self) -> RSyntaxKind { + match self.current_byte() { + Some(b'L') => self.eat_byte(R_INTEGER_LITERAL), + Some(b'i') => self.eat_byte(R_COMPLEX_LITERAL), + _ => R_DOUBLE_LITERAL, + } + } + + /// An optional sign and the exponent's digits, reporting whether the digits + /// were there + fn consume_exponent(&mut self) -> bool { + if matches!(self.current_byte(), Some(b'+' | b'-')) { + self.advance(1); + } + + self.consume_digits() + } + + /// Whether any digit was consumed + fn consume_digits(&mut self) -> bool { + let start = self.position; + while matches!(self.current_byte(), Some(byte) if byte.is_ascii_digit()) { + self.advance(1); + } + self.position != start + } + + /// Whether any digit was consumed + fn consume_hex_digits(&mut self) -> bool { + let start = self.position; + while matches!(self.current_byte(), Some(byte) if byte.is_ascii_hexdigit()) { + self.advance(1); + } + self.position != start + } + + // ----------------------------------------------------------------------- + // Special operators + + /// `/%[^%\n]*%/`, i.e. `%%`, `%/%`, `%in%`, `%o%`, and friends + /// + /// Unlike strings and backticked names, a `%...%` may not span a newline. + /// The partial `%foo` is kept as a `SPECIAL` carrying the diagnostic. + fn consume_special(&mut self) -> RSyntaxKind { + self.assert_byte(b'%'); + let open = TextRange::at(self.text_position(), TextSize::from(1)); + + // The `%` + self.advance(1); + + while let Some(byte) = self.current_byte() { + match byte { + b'%' => { + self.advance(1); + return SPECIAL; + } + b'\n' | b'\r' => break, + byte => self.advance_byte_or_char(byte), + } + } + + self.push_diagnostic(unterminated_special(open)); + SPECIAL + } + + // ----------------------------------------------------------------------- + // Strings + + fn consume_string(&mut self, delimiter: RStringDelimiter) -> RSyntaxKind { + let open = TextRange::at( + self.text_position(), + TextSize::from(delimiter.open_length() as u32), + ); + self.advance(delimiter.open_length()); + + let terminated = match delimiter { + RStringDelimiter::Single | RStringDelimiter::Double => { + self.consume_quoted_string_content(delimiter.quote()) + } + RStringDelimiter::Raw { + quote, + hyphens, + bracket, + } => self.consume_raw_string_content(quote, hyphens, bracket), + }; + + if !terminated { + let eof = TextRange::empty(self.text_position()); + self.push_diagnostic(unterminated_string(open, eof)); + return ERROR_TOKEN; + } + + self.advance(delimiter.close_length()); + delimiter.kind() + } + + /// Everything up to the closing quote + fn consume_quoted_string_content(&mut self, quote: u8) -> bool { + while let Some(byte) = self.current_byte() { + match byte { + b'\\' => { + self.advance(1); + if let Some(escaped) = self.current_byte() { + self.advance_byte_or_char(escaped); + } + } + byte if byte == quote => return true, + byte => self.advance_byte_or_char(byte), + } + } + + // Unterminated string + false + } + + /// Everything up to the closing sequence + /// + /// The subtle part is the closing bracket that turns out not to start one. + /// It belongs to the body, and the scan carries on from the byte after it, + /// or `r"(())"` and `r"-())-"` never find their close + /// (r-lib/tree-sitter-r#162). + fn consume_raw_string_content( + &mut self, + quote: u8, + hyphens: u8, + bracket: RawStringBracket, + ) -> bool { + while let Some(byte) = self.current_byte() { + if self.at_raw_string_close(quote, hyphens, bracket) { + return true; + } + self.advance_byte_or_char(byte); + } + + // Unterminated string + false + } + + /// Whether a whole closing sequence — the bracket, the hyphens, and the + /// quote — starts at the current position + fn at_raw_string_close(&self, quote: u8, hyphens: u8, bracket: RawStringBracket) -> bool { + if self.current_byte() != Some(bracket.close()) { + return false; + } + + let hyphens = usize::from(hyphens); + (1..=hyphens).all(|offset| self.byte_at(offset) == Some(b'-')) + && self.byte_at(hyphens + 1) == Some(quote) + } + + // ----------------------------------------------------------------------- + // Errors + + /// A character that starts no token + /// + /// Exactly one character is consumed and passed on as `ERROR_TOKEN`, which + /// is what `biome_js_parser` also does. + fn consume_unexpected(&mut self) -> RSyntaxKind { + let start = self.text_position(); + let character = self.current_char_unchecked(); + self.advance(character.len_utf8()); + + let range = TextRange::new(start, self.text_position()); + self.push_diagnostic(unexpected_character(range, character)); + ERROR_TOKEN + } +} diff --git a/crates/air_r_parser/src/lexer/tests.rs b/crates/air_r_parser/src/lexer/tests.rs new file mode 100644 index 00000000..b04aeccb --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests.rs @@ -0,0 +1,176 @@ +//! Lexer test infrastructure + +mod backticks; +mod bom; +mod brackets; +mod checkpoint; +mod comments; +mod coverage; +mod dots; +mod error_token; +mod identifiers; +mod keywords; +mod line_breaks; +mod numbers; +mod puncts; +mod raw_strings; +mod specials; +mod strings; +mod whitespace; + +use super::*; +use biome_diagnostics::PrintDescription; + +#[derive(Debug)] +pub(super) struct LexedToken { + kind: RSyntaxKind, + range: TextRange, + preceding_line_break: bool, +} + +/// Lex `source` to completion +pub(super) fn lex(source: &str) -> (Vec, Vec) { + let mut lexer = RLexer::from_str(source); + let mut tokens = Vec::new(); + + loop { + let kind = lexer.next_token(RLexContext::Regular); + if kind == EOF { + break; + } + + tokens.push(LexedToken { + kind, + range: lexer.current_range(), + preceding_line_break: lexer.has_preceding_line_break(), + }); + } + + (tokens, lexer.finish()) +} + +/// Assert the exact token stream, and that the lexer emitted no diagnostics +macro_rules! assert_lex { + ($source:expr, $($kind:ident : $length:expr),* $(,)?) => { + $crate::lexer::tests::check_lex($source, &[$(($kind, $length)),*], &[]) + }; +} +// `macro_rules!` scoping is textual, so the three macros reach the submodules +// declared above them by path rather than by falling into scope +pub(super) use assert_lex; + +/// [`assert_lex!`] plus the diagnostics expected, in order +macro_rules! assert_lex_err { + ($source:expr, $($kind:ident : $length:expr),* $(,)? => $($message:literal),+ $(,)?) => { + $crate::lexer::tests::check_lex($source, &[$(($kind, $length)),*], &[$($message),+]) + }; +} +pub(super) use assert_lex_err; + +/// Assert `PRECEDING_LINE_BREAK` on each non-trivia token, in order +macro_rules! assert_line_breaks { + ($source:expr, $($kind:ident : $preceded:literal),* $(,)?) => { + $crate::lexer::tests::check_line_breaks($source, &[$(($kind, $preceded)),*]) + }; +} +pub(super) use assert_line_breaks; + +#[track_caller] +pub(super) fn check_lex(source: &str, expected: &[(RSyntaxKind, u32)], messages: &[&str]) { + let (tokens, diagnostics) = lex(source); + let render = render(source, &tokens); + + assert_eq!( + tokens.len(), + expected.len(), + "expected {} tokens, lexed {}:\n{render}", + expected.len(), + tokens.len(), + ); + + // The ranges must cover the whole source + let mut offset = TextSize::from(0); + for (index, (token, (want_kind, want_length))) in tokens.iter().zip(expected).enumerate() { + assert_eq!(token.kind, *want_kind, "token {index}:\n{render}"); + assert_eq!( + token.range.len(), + TextSize::from(*want_length), + "token {index} has the wrong length:\n{render}", + ); + assert_eq!( + token.range.start(), + offset, + "token {index} leaves a gap:\n{render}", + ); + offset = token.range.end(); + } + assert_eq!( + usize::from(offset), + source.len(), + "the tokens stop short of the end of the source:\n{render}", + ); + + let actual: Vec = diagnostics + .iter() + .map(|diagnostic| PrintDescription(diagnostic).to_string()) + .collect(); + + assert_eq!( + actual.len(), + messages.len(), + "expected {} diagnostics, got:\n{actual:#?}", + messages.len(), + ); + for (actual, expected) in actual.iter().zip(messages) { + assert!( + actual.contains(expected), + "expected {expected:?} in {actual:?}" + ); + } +} + +#[track_caller] +pub(super) fn check_line_breaks(source: &str, expected: &[(RSyntaxKind, bool)]) { + let (tokens, _) = lex(source); + let render = render(source, &tokens); + + let actual: Vec<_> = tokens + .iter() + .filter(|token| !token.kind.is_trivia()) + .collect(); + + assert_eq!( + actual.len(), + expected.len(), + "expected {} non-trivia tokens, lexed {}:\n{render}", + expected.len(), + actual.len(), + ); + for (index, (token, (want_kind, want_break))) in actual.iter().zip(expected).enumerate() { + assert_eq!(token.kind, *want_kind, "token {index}:\n{render}"); + assert_eq!( + token.preceding_line_break, *want_break, + "token {index} reports the wrong preceding line break:\n{render}", + ); + } +} + +/// The whole stream, for any failure. A message naming only the offending token +/// sends you back to re-run with `--no-capture` and print the rest yourself. +fn render(source: &str, tokens: &[LexedToken]) -> String { + tokens + .iter() + .map(|token| { + let break_marker = if token.preceding_line_break { + " nl" + } else { + "" + }; + format!( + " {:?}@{:?} {:?}{break_marker}", + token.kind, token.range, &source[token.range] + ) + }) + .collect::>() + .join("\n") +} diff --git a/crates/air_r_parser/src/lexer/tests/backticks.rs b/crates/air_r_parser/src/lexer/tests/backticks.rs new file mode 100644 index 00000000..1969c942 --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/backticks.rs @@ -0,0 +1,49 @@ +use super::*; + +#[test] +fn one_token_spanning_the_delimiters() { + assert_lex!("`a b`", IDENT: 5); + assert_lex!("`a b` <- 1", IDENT: 5, WHITESPACE: 1, ASSIGN: 2, WHITESPACE: 1, R_DOUBLE_LITERAL: 1); +} + +#[test] +fn empty_name() { + assert_lex!("``", IDENT: 2); +} + +/// An escaped backtick does not close the name +#[test] +fn escapes_apply() { + assert_lex!("`a\\`b`", IDENT: 6); + assert_lex!("`a\\\\`", IDENT: 5); +} + +#[test] +fn names_may_span_newlines() { + assert_lex!( + "`my\nvar` <- 1", + IDENT: 8, + WHITESPACE: 1, + ASSIGN: 2, + WHITESPACE: 1, + R_DOUBLE_LITERAL: 1, + ); +} + +/// There is no line-based early stop available, so an unterminated name really +/// does run to the end of the file, exactly as in R and in tree-sitter-r. What +/// keeps the error useful is that it points at the opening delimiter rather than +/// highlighting the rest of the file. +#[test] +fn unterminated_runs_to_the_end_of_the_file() { + assert_lex_err!( + "`a\nb <- 1", + ERROR_TOKEN: 9 + => "Unterminated backtick-quoted name" + ); +} + +#[test] +fn a_trailing_escape_does_not_run_past_the_end() { + assert_lex_err!("`a\\", ERROR_TOKEN: 3 => "Unterminated backtick-quoted name"); +} diff --git a/crates/air_r_parser/src/lexer/tests/bom.rs b/crates/air_r_parser/src/lexer/tests/bom.rs new file mode 100644 index 00000000..90c42b5f --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/bom.rs @@ -0,0 +1,30 @@ +use super::*; + +#[test] +fn bare_bom() { + assert_lex!("\u{FEFF}", UNICODE_BOM: 3); +} + +#[test] +fn bom_before_a_token() { + assert_lex!("\u{FEFF}1", UNICODE_BOM: 3, R_DOUBLE_LITERAL: 1); +} + +#[test] +fn a_bom_is_only_a_bom_at_offset_zero() { + // U+FEFF is category Cf, so it is neither XID_Start nor XID_Continue and + // cannot join the identifier either + assert_lex_err!( + "a\u{FEFF}", + IDENT: 1, ERROR_TOKEN: 3 + => "Unexpected character" + ); +} + +// `consume_potential_bom` matches on `source().get(0..3)`, so a source shorter +// than three bytes has to fall through rather than panic or mis-slice +#[test] +fn source_shorter_than_a_bom() { + assert_lex!("a", IDENT: 1); + assert_lex!("é", IDENT: 2); +} diff --git a/crates/air_r_parser/src/lexer/tests/brackets.rs b/crates/air_r_parser/src/lexer/tests/brackets.rs new file mode 100644 index 00000000..631924a7 --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/brackets.rs @@ -0,0 +1,112 @@ +//! `[[` is lexed greedily, `]]` is not + +use super::*; + +#[test] +fn adjacent_open_brackets_are_one_token() { + assert_lex!("[[", L_BRACK2: 2); +} + +#[test] +fn adjacent_close_brackets_are_not() { + assert_lex!("]]", R_BRACK: 1, R_BRACK: 1); +} + +#[test] +fn separated_open_brackets_are_not() { + assert_lex!( + "x[ [1] ]", + IDENT: 1, + L_BRACK: 1, + WHITESPACE: 1, + L_BRACK: 1, + R_DOUBLE_LITERAL: 1, + R_BRACK: 1, + WHITESPACE: 1, + R_BRACK: 1, + ); +} + +#[test] +fn a_third_open_bracket_stands_alone() { + assert_lex!("[[[", L_BRACK2: 2, L_BRACK: 1); +} + +#[test] +fn join_takes_both_brackets() { + let mut lexer = RLexer::from_str("]]a"); + + assert_eq!(lexer.next_token(RLexContext::Regular), R_BRACK); + assert_eq!(lexer.re_lex(RReLexContext::JoinBrackets), R_BRACK2); + assert_eq!(lexer.current_range(), TextRange::new(0.into(), 2.into())); + + assert_eq!(lexer.next_token(RLexContext::Regular), IDENT); + assert_eq!(lexer.current_range(), TextRange::new(2.into(), 3.into())); +} + +/// `x[[a[[b]]]]` — the outer subset2 joins the pair the inner one left behind +#[test] +fn a_joined_pair_leaves_the_rest_of_the_run() { + let mut lexer = RLexer::from_str("]]]]"); + + assert_eq!(lexer.next_token(RLexContext::Regular), R_BRACK); + assert_eq!(lexer.re_lex(RReLexContext::JoinBrackets), R_BRACK2); + + assert_eq!(lexer.next_token(RLexContext::Regular), R_BRACK); + assert_eq!(lexer.re_lex(RReLexContext::JoinBrackets), R_BRACK2); + assert_eq!(lexer.current_range(), TextRange::new(2.into(), 4.into())); +} + +#[test] +fn join_is_idempotent() { + let mut lexer = RLexer::from_str("]]"); + + assert_eq!(lexer.next_token(RLexContext::Regular), R_BRACK); + assert_eq!(lexer.re_lex(RReLexContext::JoinBrackets), R_BRACK2); + assert_eq!(lexer.re_lex(RReLexContext::JoinBrackets), R_BRACK2); + assert_eq!(lexer.current_range(), TextRange::new(0.into(), 2.into())); +} + +/// `x[[1] ]` and issue #177's `x[[1]\n]` both land here: the brackets must be +/// adjacent, and a no-op join must leave the trivia between them intact +#[test] +fn join_needs_the_brackets_adjacent() { + for source in ["] ]", "]\n]"] { + let mut lexer = RLexer::from_str(source); + + assert_eq!(lexer.next_token(RLexContext::Regular), R_BRACK); + assert_eq!(lexer.re_lex(RReLexContext::JoinBrackets), R_BRACK); + assert_eq!(lexer.current_range(), TextRange::new(0.into(), 1.into())); + + assert!(matches!( + lexer.next_token(RLexContext::Regular), + WHITESPACE | NEWLINE + )); + assert_eq!(lexer.next_token(RLexContext::Regular), R_BRACK); + assert_eq!(lexer.next_token(RLexContext::Regular), EOF); + } +} + +#[test] +fn join_is_a_no_op_at_the_end_of_the_source() { + let mut lexer = RLexer::from_str("]"); + + assert_eq!(lexer.next_token(RLexContext::Regular), R_BRACK); + assert_eq!(lexer.re_lex(RReLexContext::JoinBrackets), R_BRACK); + assert_eq!(lexer.current_range(), TextRange::new(0.into(), 1.into())); +} + +/// A join rewinds to the start of the current token, so it must not lose the +/// line break flag +#[test] +fn join_preserves_the_preceding_line_break() { + let mut lexer = RLexer::from_str("1\n]]"); + + assert_eq!(lexer.next_token(RLexContext::Regular), R_DOUBLE_LITERAL); + assert_eq!(lexer.next_token(RLexContext::Regular), NEWLINE); + assert_eq!(lexer.next_token(RLexContext::Regular), R_BRACK); + assert!(lexer.has_preceding_line_break()); + + assert_eq!(lexer.re_lex(RReLexContext::JoinBrackets), R_BRACK2); + assert!(lexer.has_preceding_line_break()); +} diff --git a/crates/air_r_parser/src/lexer/tests/checkpoint.rs b/crates/air_r_parser/src/lexer/tests/checkpoint.rs new file mode 100644 index 00000000..476aeb9f --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/checkpoint.rs @@ -0,0 +1,94 @@ +use super::*; + +#[test] +fn rewind_restores_the_current_token() { + let mut lexer = RLexer::from_str("a + b"); + + assert_eq!(lexer.next_token(RLexContext::Regular), IDENT); + let checkpoint = lexer.checkpoint(); + + assert_eq!(lexer.next_token(RLexContext::Regular), WHITESPACE); + assert_eq!(lexer.next_token(RLexContext::Regular), PLUS); + + lexer.rewind(checkpoint); + assert_eq!(lexer.current(), IDENT); + assert_eq!(lexer.current_range(), TextRange::new(0.into(), 1.into())); + assert_eq!(lexer.next_token(RLexContext::Regular), WHITESPACE); +} + +#[test] +fn rewind_restores_the_sticky_line_break() { + let mut lexer = RLexer::from_str("1\n2"); + + assert_eq!(lexer.next_token(RLexContext::Regular), R_DOUBLE_LITERAL); + assert_eq!(lexer.next_token(RLexContext::Regular), NEWLINE); + assert!(lexer.after_line_break); + + // Checkpointed with the break still pending, and consumed straight after + let checkpoint = lexer.checkpoint(); + assert_eq!(lexer.next_token(RLexContext::Regular), R_DOUBLE_LITERAL); + assert!(lexer.has_preceding_line_break()); + assert!(!lexer.after_line_break); + + lexer.rewind(checkpoint); + assert!(lexer.after_line_break); + assert_eq!(lexer.next_token(RLexContext::Regular), R_DOUBLE_LITERAL); + assert!(lexer.has_preceding_line_break()); +} + +#[test] +fn rewind_restores_the_current_flags() { + let mut lexer = RLexer::from_str("1\n2 3"); + + assert_eq!(lexer.next_token(RLexContext::Regular), R_DOUBLE_LITERAL); + assert_eq!(lexer.next_token(RLexContext::Regular), NEWLINE); + assert_eq!(lexer.next_token(RLexContext::Regular), R_DOUBLE_LITERAL); + let checkpoint = lexer.checkpoint(); + + assert_eq!(lexer.next_token(RLexContext::Regular), WHITESPACE); + assert_eq!(lexer.next_token(RLexContext::Regular), R_DOUBLE_LITERAL); + assert!(!lexer.has_preceding_line_break()); + + lexer.rewind(checkpoint); + assert!(lexer.has_preceding_line_break()); +} + +#[test] +fn rewind_restores_the_bom_length() { + let mut lexer = RLexer::from_str("\u{FEFF}1"); + + assert_eq!(lexer.next_token(RLexContext::Regular), UNICODE_BOM); + assert_eq!(lexer.unicode_bom_length, 3); + let checkpoint = lexer.checkpoint(); + + assert_eq!(lexer.next_token(RLexContext::Regular), R_DOUBLE_LITERAL); + + lexer.rewind(checkpoint); + assert_eq!(lexer.unicode_bom_length, 3); +} + +/// Diagnostics raised after the checkpoint are dropped, so a rewind cannot leave +/// the parser reporting an error about text it decided not to lex that way +#[test] +fn rewind_truncates_diagnostics() { + let mut lexer = RLexer::from_str("a %in"); + + assert_eq!(lexer.next_token(RLexContext::Regular), IDENT); + assert_eq!(lexer.next_token(RLexContext::Regular), WHITESPACE); + let checkpoint = lexer.checkpoint(); + + assert_eq!(lexer.next_token(RLexContext::Regular), SPECIAL); + assert_eq!(lexer.diagnostics.len(), 1); + + lexer.rewind(checkpoint); + assert_eq!(lexer.diagnostics.len(), 0); +} + +/// The `None` that sends an `r` down the identifier arm instead +#[test] +fn a_name_is_not_a_raw_string_open() { + assert_eq!(RStringDelimiter::try_from_raw_string_open(""), None); + assert_eq!(RStringDelimiter::try_from_raw_string_open("rnorm"), None); + assert_eq!(RStringDelimiter::try_from_raw_string_open("r\"-x("), None); + assert_eq!(RStringDelimiter::try_from_raw_string_open("\""), None); +} diff --git a/crates/air_r_parser/src/lexer/tests/comments.rs b/crates/air_r_parser/src/lexer/tests/comments.rs new file mode 100644 index 00000000..03c4e446 --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/comments.rs @@ -0,0 +1,33 @@ +use super::*; + +#[test] +fn to_end_of_line() { + assert_lex!("# comment\n1", COMMENT: 9, NEWLINE: 1, R_DOUBLE_LITERAL: 1); +} + +#[test] +fn ending_the_file_without_a_newline() { + assert_lex!("1 # comment", R_DOUBLE_LITERAL: 1, WHITESPACE: 1, COMMENT: 9); +} + +#[test] +fn empty_comment() { + assert_lex!("#", COMMENT: 1); +} + +/// The `\r` belongs to the `NEWLINE`, not to the comment +#[test] +fn crlf_stays_out_of_the_comment() { + assert_lex!("# x\r\n", COMMENT: 3, NEWLINE: 2); +} + +#[test] +fn non_ascii_inside_a_comment() { + assert_lex!("# héllo", COMMENT: 8); +} + +/// Nothing inside a comment is lexed, however token-shaped it looks +#[test] +fn contents_are_inert() { + assert_lex!("# \"a\" `b` %in% 1L", COMMENT: 17); +} diff --git a/crates/air_r_parser/src/lexer/tests/coverage.rs b/crates/air_r_parser/src/lexer/tests/coverage.rs new file mode 100644 index 00000000..29f94008 --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/coverage.rs @@ -0,0 +1,66 @@ +//! Coverage test to ensure every token has lexer coverage + +use super::*; +use biome_rowan::RawSyntaxKind; +use std::collections::HashSet; + +/// Well-formed sources, which between them must produce every lexable kind but +/// `ERROR_TOKEN` and `R_BRACK2`. The test names the gaps for you. +const SOURCES: &[&str] = &[ + "\u{FEFF}", + ";,{}()?~$@\\+/^", + "[ ] [[ ]]", + "<- <<- := -> ->> - = == != < <= > >=", + "| || & && |> * ** : :: ::: !", + "function for in while repeat if else next break", + "TRUE FALSE NULL Inf NaN NA NA_integer_ NA_real_ NA_complex_ NA_character_", + "foo ... ..1 1 1L 1i %in%\n# comment\n", + "\"a\" 'b' r\"(c)\"", +]; + +/// Sources for the kinds no well-formed source can reach. `ERROR_TOKEN` is the +/// only one, and it comes with a diagnostic by construction, so it needs a list +/// of its own rather than a hole in [`SOURCES`]. +const ERROR_SOURCES: &[&str] = &["\u{1}"]; + +/// The kinds only a re-lex can produce. +/// +/// `R_BRACK2` is the only one +fn re_lexed_kinds() -> HashSet { + let mut lexer = RLexer::from_str("]]"); + lexer.next_token(RLexContext::Regular); + HashSet::from([lexer.re_lex(RReLexContext::JoinBrackets)]) +} + +fn is_lexable(kind: RSyntaxKind) -> bool { + !matches!(kind, TOMBSTONE | EOF | __LAST) && (kind as u16) < (R_ROOT as u16) +} + +#[test] +fn every_lexable_kind_is_produced() { + let mut produced = HashSet::new(); + for source in SOURCES { + let (tokens, diagnostics) = lex(source); + assert!( + diagnostics.is_empty(), + "coverage sources are well-formed R; {source:?} lexed with diagnostics", + ); + produced.extend(tokens.into_iter().map(|token| token.kind)); + } + for source in ERROR_SOURCES { + let (tokens, diagnostics) = lex(source); + assert!( + !diagnostics.is_empty(), + "{source:?} is in ERROR_SOURCES but lexed cleanly", + ); + produced.extend(tokens.into_iter().map(|token| token.kind)); + } + produced.extend(re_lexed_kinds()); + + let missing: Vec<_> = (0..RSyntaxKind::__LAST as u16) + .map(|raw| RSyntaxKind::from_raw(RawSyntaxKind(raw))) + .filter(|kind| is_lexable(*kind) && !produced.contains(kind)) + .collect(); + + assert!(missing.is_empty(), "no test lexes: {missing:#?}"); +} diff --git a/crates/air_r_parser/src/lexer/tests/dots.rs b/crates/air_r_parser/src/lexer/tests/dots.rs new file mode 100644 index 00000000..4b65eeac --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/dots.rs @@ -0,0 +1,64 @@ +use super::*; + +#[test] +fn dispatch() { + assert_lex!( + "... ..1 .5 .foo . ..", + DOTS: 3, + WHITESPACE: 1, + DOTDOTI: 3, + WHITESPACE: 1, + R_DOUBLE_LITERAL: 2, + WHITESPACE: 1, + IDENT: 4, + WHITESPACE: 1, + IDENT: 1, + WHITESPACE: 1, + IDENT: 2, + ); +} + +#[test] +fn dot_dot_i_takes_every_digit() { + assert_lex!("..1", DOTDOTI: 3); + assert_lex!("..12", DOTDOTI: 4); +} + +/// Anything the name scanner picks up beyond the `...` or the digits leaves an +/// ordinary identifier, however dot-like the start of it looks +#[test] +fn a_name_that_continues_is_an_identifier() { + assert_lex!("....", IDENT: 4); + assert_lex!("...a", IDENT: 4); + assert_lex!("...1", IDENT: 4); + assert_lex!("..1a", IDENT: 4); + assert_lex!("..1.", IDENT: 4); + assert_lex!("..1e5", IDENT: 5); +} + +/// A digit after the `.` always makes a number, never a name, in R and in +/// tree-sitter-r alike: otherwise `.1L` and `.1i` lex as identifiers +/// (r-lib/tree-sitter-r#190) +#[test] +fn a_digit_after_the_dot_makes_a_number() { + assert_lex!(".5", R_DOUBLE_LITERAL: 2); + assert_lex!(".1L", R_INTEGER_LITERAL: 3); + assert_lex!(".1i", R_COMPLEX_LITERAL: 3); + // The number stops at the first character it cannot use, and a name starts + // there + assert_lex!(".5abc", R_DOUBLE_LITERAL: 2, IDENT: 3); +} + +#[test] +fn dots_in_context() { + assert_lex!( + "f(..., ..1)", + IDENT: 1, + L_PAREN: 1, + DOTS: 3, + COMMA: 1, + WHITESPACE: 1, + DOTDOTI: 3, + R_PAREN: 1, + ); +} diff --git a/crates/air_r_parser/src/lexer/tests/error_token.rs b/crates/air_r_parser/src/lexer/tests/error_token.rs new file mode 100644 index 00000000..bf06a194 --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/error_token.rs @@ -0,0 +1,56 @@ +use super::*; + +/// The token spans the whole character, not its first byte. An off-by-one here +/// would split a multi-byte character and break round-tripping. +#[test] +fn spans_the_whole_character() { + assert_lex_err!("\u{1}", ERROR_TOKEN: 1 => "Unexpected character"); + assert_lex_err!("£", ERROR_TOKEN: 2 => "Unexpected character"); + assert_lex_err!("€", ERROR_TOKEN: 3 => "Unexpected character"); + assert_lex_err!("🎉", ERROR_TOKEN: 4 => "Unexpected character"); +} + +/// One token and one diagnostic per character, each naming its own character. +/// Merging a run into a single token would be cheaper to report and worse to +/// read, and it would hide the second character from the parser entirely. +#[test] +fn each_character_is_its_own_token() { + assert_lex_err!( + "€£", + ERROR_TOKEN: 3, ERROR_TOKEN: 2 + => "Unexpected character `€`", "Unexpected character `£`" + ); +} + +/// The lexer does not stop, and the tokens after the error are the ones a clean +/// source would have produced +#[test] +fn lexing_continues_afterwards() { + assert_lex_err!( + "1 + \u{1} + 2", + R_DOUBLE_LITERAL: 1, WHITESPACE: 1, PLUS: 1, WHITESPACE: 1, + ERROR_TOKEN: 1, + WHITESPACE: 1, PLUS: 1, WHITESPACE: 1, R_DOUBLE_LITERAL: 1 + => "Unexpected character" + ); +} + +#[test] +fn breaks_an_identifier_in_two() { + assert_lex_err!( + "a€b", + IDENT: 1, ERROR_TOKEN: 3, IDENT: 1 + => "Unexpected character" + ); +} + +/// An error token is not trivia, so it takes the sticky break flag and clears it +/// rather than passing it along to the next real token +#[test] +fn carries_a_preceding_line_break() { + assert_line_breaks!( + "\n\u{1}1", + ERROR_TOKEN: true, + R_DOUBLE_LITERAL: false, + ); +} diff --git a/crates/air_r_parser/src/lexer/tests/identifiers.rs b/crates/air_r_parser/src/lexer/tests/identifiers.rs new file mode 100644 index 00000000..3822e46f --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/identifiers.rs @@ -0,0 +1,43 @@ +use super::*; + +#[test] +fn plain_names() { + assert_lex!("foo", IDENT: 3); + assert_lex!("a1", IDENT: 2); + assert_lex!("foo.bar", IDENT: 7); + assert_lex!("x_1.y", IDENT: 5); +} + +/// `_` is not a valid way to start an R name, but tree-sitter-r is deliberately +/// lax: a lone `_` is the native pipe placeholder, and lexing `_foo` as one name +/// is what lets a downstream consumer report it as an invalid one. Splitting it +/// into `_` and `foo` would make that check impossible (r-lib/tree-sitter-r#71). +/// +/// TODO: We may consider making this stricter now that we don't rely on tree sitter +#[test] +fn leading_underscore_is_one_name() { + assert_lex!("_", IDENT: 1); + assert_lex!("_foo", IDENT: 4); +} + +#[test] +fn names_may_start_with_a_dot() { + assert_lex!(".", IDENT: 1); + assert_lex!("..", IDENT: 2); + assert_lex!(".foo", IDENT: 4); + assert_lex!("._foo", IDENT: 5); +} + +#[test] +fn unicode_starts_and_continues() { + assert_lex!("café", IDENT: 5); + assert_lex!("μ", IDENT: 2); + assert_lex!("日本語", IDENT: 9); + assert_lex!("aμ1", IDENT: 4); +} + +/// A character that is neither XID_Start nor punctuation starts no token at all +#[test] +fn a_character_that_is_not_an_identifier_start() { + assert_lex_err!("€", ERROR_TOKEN: 3 => "Unexpected character"); +} diff --git a/crates/air_r_parser/src/lexer/tests/keywords.rs b/crates/air_r_parser/src/lexer/tests/keywords.rs new file mode 100644 index 00000000..f44e58aa --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/keywords.rs @@ -0,0 +1,60 @@ +use super::*; + +#[test] +fn control_flow_keywords() { + assert_lex!("function", FUNCTION_KW: 8); + assert_lex!("for", FOR_KW: 3); + assert_lex!("in", IN_KW: 2); + assert_lex!("while", WHILE_KW: 5); + assert_lex!("repeat", REPEAT_KW: 6); + assert_lex!("if", IF_KW: 2); + assert_lex!("else", ELSE_KW: 4); + assert_lex!("next", NEXT_KW: 4); + assert_lex!("break", BREAK_KW: 5); +} + +#[test] +fn constants() { + assert_lex!("TRUE", TRUE_KW: 4); + assert_lex!("FALSE", FALSE_KW: 5); + assert_lex!("NULL", NULL_KW: 4); + assert_lex!("Inf", INF_KW: 3); + assert_lex!("NaN", NAN_KW: 3); +} + +#[test] +fn missing_values() { + assert_lex!("NA", NA_LOGICAL_KW: 2); + assert_lex!("NA_integer_", NA_INTEGER_KW: 11); + assert_lex!("NA_real_", NA_DOUBLE_KW: 8); + assert_lex!("NA_complex_", NA_COMPLEX_KW: 11); + assert_lex!("NA_character_", NA_CHARACTER_KW: 13); +} + +#[test] +fn keywords_are_case_sensitive() { + assert_lex!("If", IDENT: 2); + assert_lex!("Function", IDENT: 8); + assert_lex!("true", IDENT: 4); + assert_lex!("false", IDENT: 5); + assert_lex!("null", IDENT: 4); + assert_lex!("na", IDENT: 2); + assert_lex!("INF", IDENT: 3); +} + +/// Lexing the whole name before consulting the keyword table is what gets these +/// right for free. tree-sitter-r had to fix `elseμ` specifically (r-lib/tree-sitter-r#200). +#[test] +fn a_keyword_prefix_is_not_a_keyword() { + assert_lex!("elseif", IDENT: 6); + assert_lex!("else_idx", IDENT: 8); + assert_lex!("elseμ", IDENT: 6); + assert_lex!("NA_real", IDENT: 7); + assert_lex!("NA_real__", IDENT: 9); +} + +#[test] +fn backticked_keywords_stay_identifiers() { + assert_lex!("`if`", IDENT: 4); + assert_lex!("`TRUE`", IDENT: 6); +} diff --git a/crates/air_r_parser/src/lexer/tests/line_breaks.rs b/crates/air_r_parser/src/lexer/tests/line_breaks.rs new file mode 100644 index 00000000..0216d01a --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/line_breaks.rs @@ -0,0 +1,58 @@ +//! The sticky `PRECEDING_LINE_BREAK` flag. +//! +//! The lexer answers "was there a line break here", which is a lexical fact. The +//! parser decides whether that fact means anything, which in R is statement +//! termination and `else` attachment. + +use super::*; + +#[test] +fn the_first_token_has_no_preceding_break() { + assert_line_breaks!("1", R_DOUBLE_LITERAL: false); + assert_line_breaks!("1 + 2", R_DOUBLE_LITERAL: false, PLUS: false, R_DOUBLE_LITERAL: false); +} + +#[test] +fn a_leading_newline_still_counts() { + assert_line_breaks!("\n1", R_DOUBLE_LITERAL: true); +} + +#[test] +fn set_by_a_newline_and_cleared_by_the_next_real_token() { + assert_line_breaks!( + "1\n2 3", + R_DOUBLE_LITERAL: false, + R_DOUBLE_LITERAL: true, + R_DOUBLE_LITERAL: false, + ); +} + +#[test] +fn survives_whitespace_and_comments() { + // The break is reported on `+`, not on the whitespace that precedes it and + // not on the `2` that follows it + assert_line_breaks!( + "1\n # c\n + 2", + R_DOUBLE_LITERAL: false, + PLUS: true, + R_DOUBLE_LITERAL: false, + ); +} + +#[test] +fn a_trailing_comment_does_not_set_it() { + assert_line_breaks!( + "1 # c\n2", + R_DOUBLE_LITERAL: false, + R_DOUBLE_LITERAL: true, + ); +} + +#[test] +fn strings_report_the_break() { + assert_line_breaks!( + "1\n\"a\"", + R_DOUBLE_LITERAL: false, + R_DOUBLE_QUOTED_STRING_LITERAL: true, + ); +} diff --git a/crates/air_r_parser/src/lexer/tests/numbers.rs b/crates/air_r_parser/src/lexer/tests/numbers.rs new file mode 100644 index 00000000..aea80795 --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/numbers.rs @@ -0,0 +1,105 @@ +use super::*; + +#[test] +fn decimals() { + assert_lex!("1", R_DOUBLE_LITERAL: 1); + assert_lex!("123", R_DOUBLE_LITERAL: 3); + assert_lex!("1.", R_DOUBLE_LITERAL: 2); + assert_lex!("1.5", R_DOUBLE_LITERAL: 3); + assert_lex!(".5", R_DOUBLE_LITERAL: 2); +} + +#[test] +fn suffixes() { + assert_lex!("1L", R_INTEGER_LITERAL: 2); + assert_lex!("1i", R_COMPLEX_LITERAL: 2); + assert_lex!("1.5L", R_INTEGER_LITERAL: 4); + assert_lex!("1e5L", R_INTEGER_LITERAL: 4); + assert_lex!("1L(", R_INTEGER_LITERAL: 2, L_PAREN: 1); +} + +/// Suffix is taken greedily +#[test] +fn a_suffix_is_always_taken() { + assert_lex!("1Li", R_INTEGER_LITERAL: 2, IDENT: 1); + assert_lex!("1LL", R_INTEGER_LITERAL: 2, IDENT: 1); + assert_lex!("1iL", R_COMPLEX_LITERAL: 2, IDENT: 1); + assert_lex!("1L_", R_INTEGER_LITERAL: 2, IDENT: 1); + assert_lex!("1LForever", R_INTEGER_LITERAL: 2, IDENT: 7); + assert_lex!("1e5iL", R_COMPLEX_LITERAL: 4, IDENT: 1); + // `.` starts a name of its own here, rather than continuing the literal + assert_lex!("1L.", R_INTEGER_LITERAL: 2, IDENT: 1); + assert_lex!(".1L.", R_INTEGER_LITERAL: 3, IDENT: 1); +} + +#[test] +fn exponents() { + assert_lex!("1e5", R_DOUBLE_LITERAL: 3); + assert_lex!("1e+3", R_DOUBLE_LITERAL: 4); + assert_lex!("1e-3", R_DOUBLE_LITERAL: 4); + assert_lex!("1.e5", R_DOUBLE_LITERAL: 4); +} + +/// An `e` commits to an exponent, so its digits are required +#[test] +fn a_decimal_exponent_with_no_digits() { + assert_lex_err!("1e", R_DOUBLE_LITERAL: 2 => "Unterminated exponent"); + assert_lex_err!("1e+", R_DOUBLE_LITERAL: 3 => "Unterminated exponent"); + assert_lex_err!("1e-", R_DOUBLE_LITERAL: 3 => "Unterminated exponent"); + // The literal still stops where it stopped, so the name after it is intact + assert_lex_err!("1east", R_DOUBLE_LITERAL: 2, IDENT: 3 => "Unterminated exponent"); + assert_lex_err!("1e-a", R_DOUBLE_LITERAL: 3, IDENT: 1 => "Unterminated exponent"); + // A space does not rescue it either, in R or here + assert_lex_err!( + "1e + 2", + R_DOUBLE_LITERAL: 2, WHITESPACE: 1, PLUS: 1, WHITESPACE: 1, R_DOUBLE_LITERAL: 1 + => "Unterminated exponent" + ); +} + +#[test] +fn hexadecimals() { + assert_lex!("0xFF", R_DOUBLE_LITERAL: 4); + assert_lex!("0XFF", R_DOUBLE_LITERAL: 4); + assert_lex!("0xFFL", R_INTEGER_LITERAL: 5); + assert_lex!("0x1.8", R_DOUBLE_LITERAL: 5); + // R accepts a hexadecimal with nothing after the dot, surprisingly + assert_lex!("0x.", R_DOUBLE_LITERAL: 3); + assert_lex!("0x.8", R_DOUBLE_LITERAL: 4); + // `e` is a hexadecimal digit, not the start of a decimal exponent + assert_lex!("0x1e", R_DOUBLE_LITERAL: 4); +} + +#[test] +fn binary_exponents() { + assert_lex!("0x1p3", R_DOUBLE_LITERAL: 5); + assert_lex!("0x.8p1", R_DOUBLE_LITERAL: 6); + assert_lex!("0x1p+3", R_DOUBLE_LITERAL: 6); + assert_lex!("0x1p-3", R_DOUBLE_LITERAL: 6); + assert_lex!("0x.p3", R_DOUBLE_LITERAL: 5); + assert_lex!("0x1.8p3", R_DOUBLE_LITERAL: 7); +} + +/// A `p` commits to a binary exponent +#[test] +fn a_binary_exponent_with_no_digits() { + assert_lex_err!("0x1p", R_DOUBLE_LITERAL: 4 => "Unterminated binary exponent"); + assert_lex_err!("0x1p+", R_DOUBLE_LITERAL: 5 => "Unterminated binary exponent"); + assert_lex_err!("0x1p-", R_DOUBLE_LITERAL: 5 => "Unterminated binary exponent"); + assert_lex_err!("0x.p", R_DOUBLE_LITERAL: 4 => "Unterminated binary exponent"); + assert_lex_err!("0x.8p", R_DOUBLE_LITERAL: 5 => "Unterminated binary exponent"); +} + +/// `0x` commits to a hexadecimal +#[test] +fn a_hexadecimal_with_no_body() { + assert_lex_err!("0x", R_DOUBLE_LITERAL: 2 => "Unterminated hexadecimal"); + assert_lex_err!("0xg", R_DOUBLE_LITERAL: 2, IDENT: 1 => "Unterminated hexadecimal"); + // The body may not start with the binary exponent either + assert_lex_err!("0xp3", R_DOUBLE_LITERAL: 2, IDENT: 2 => "Unterminated hexadecimal"); +} + +#[test] +fn a_second_dot_starts_a_second_literal() { + assert_lex!("1.5.3", R_DOUBLE_LITERAL: 3, R_DOUBLE_LITERAL: 2); +} diff --git a/crates/air_r_parser/src/lexer/tests/puncts.rs b/crates/air_r_parser/src/lexer/tests/puncts.rs new file mode 100644 index 00000000..ded9127b --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/puncts.rs @@ -0,0 +1,111 @@ +use super::*; + +/// None of these is a prefix of a longer token, so they can be written adjacent +#[test] +fn single_byte_puncts() { + assert_lex!( + ";,{}()?~$@\\+/^", + SEMICOLON: 1, + COMMA: 1, + L_CURLY: 1, + R_CURLY: 1, + L_PAREN: 1, + R_PAREN: 1, + WAT: 1, + TILDE: 1, + DOLLAR: 1, + AT: 1, + BACKSLASH: 1, + PLUS: 1, + DIVIDE: 1, + EXPONENTIATE: 1, + ); +} + +#[test] +fn less_than() { + assert_lex!("<", LESS_THAN: 1); + assert_lex!("<=", LESS_THAN_OR_EQUAL_TO: 2); + assert_lex!("<-", ASSIGN: 2); + assert_lex!("<<-", SUPER_ASSIGN: 3); + assert_lex!("<<", LESS_THAN: 1, LESS_THAN: 1); +} + +#[test] +fn greater_than() { + assert_lex!(">", GREATER_THAN: 1); + assert_lex!(">=", GREATER_THAN_OR_EQUAL_TO: 2); + assert_lex!(">>", GREATER_THAN: 1, GREATER_THAN: 1); +} + +#[test] +fn minus() { + assert_lex!("-", MINUS: 1); + assert_lex!("->", ASSIGN_RIGHT: 2); + assert_lex!("->>", SUPER_ASSIGN_RIGHT: 3); +} + +#[test] +fn colon() { + assert_lex!(":", COLON: 1); + assert_lex!("::", COLON2: 2); + assert_lex!(":::", COLON3: 3); + assert_lex!("::::", COLON3: 3, COLON: 1); + assert_lex!(":=", WALRUS: 2); +} + +#[test] +fn star() { + assert_lex!("*", MULTIPLY: 1); + assert_lex!("**", EXPONENTIATE2: 2); + assert_lex!("***", EXPONENTIATE2: 2, MULTIPLY: 1); +} + +#[test] +fn pipes() { + assert_lex!("|", OR: 1); + assert_lex!("||", OR2: 2); + assert_lex!("|>", PIPE: 2); +} + +#[test] +fn ampersands() { + assert_lex!("&", AND: 1); + assert_lex!("&&", AND2: 2); +} + +#[test] +fn equals() { + assert_lex!("=", EQUAL: 1); + assert_lex!("==", EQUAL2: 2); + assert_lex!("===", EQUAL2: 2, EQUAL: 1); +} + +#[test] +fn bang() { + assert_lex!("!", BANG: 1); + assert_lex!("!=", NOT_EQUAL: 2); +} + +#[test] +fn adjacency_changes_the_meaning() { + assert_lex!("1<-2", R_DOUBLE_LITERAL: 1, ASSIGN: 2, R_DOUBLE_LITERAL: 1); + assert_lex!( + "1 < -2", + R_DOUBLE_LITERAL: 1, + WHITESPACE: 1, + LESS_THAN: 1, + WHITESPACE: 1, + MINUS: 1, + R_DOUBLE_LITERAL: 1, + ); +} + +#[test] +fn a_byte_that_starts_no_token() { + assert_lex_err!( + "1\u{1}", + R_DOUBLE_LITERAL: 1, ERROR_TOKEN: 1 + => "Unexpected character" + ); +} diff --git a/crates/air_r_parser/src/lexer/tests/raw_strings.rs b/crates/air_r_parser/src/lexer/tests/raw_strings.rs new file mode 100644 index 00000000..bd428b5a --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/raw_strings.rs @@ -0,0 +1,72 @@ +use super::*; + +#[test] +fn every_bracket_pair() { + assert_lex!("r\"(abc)\"", R_RAW_STRING_LITERAL: 8); + assert_lex!("r\"[abc]\"", R_RAW_STRING_LITERAL: 8); + assert_lex!("r\"{abc}\"", R_RAW_STRING_LITERAL: 8); + assert_lex!("r\"|abc|\"", R_RAW_STRING_LITERAL: 8); +} + +#[test] +fn hyphen_counts() { + assert_lex!("r\"(a)\"", R_RAW_STRING_LITERAL: 6); + assert_lex!("r\"-(a)-\"", R_RAW_STRING_LITERAL: 8); + assert_lex!("r\"---(a)---\"", R_RAW_STRING_LITERAL: 12); + assert_lex!("r\"--|a|--\"", R_RAW_STRING_LITERAL: 10); +} + +#[test] +fn uppercase_prefix_and_single_quotes() { + assert_lex!("R'---[x]---'", R_RAW_STRING_LITERAL: 12); + assert_lex!("R\"(x)\"", R_RAW_STRING_LITERAL: 6); + assert_lex!("R'|x|'", R_RAW_STRING_LITERAL: 6); +} + +/// A closing bracket that turns out not to start the closing sequence is +/// content, and the scan resumes from where the attempt stopped rather than from +/// the byte after the bracket. Getting that wrong makes both of these +/// unterminated (r-lib/tree-sitter-r#162). +#[test] +fn a_bracket_that_does_not_close() { + assert_lex!("r\"(())\"", R_RAW_STRING_LITERAL: 7); + assert_lex!("r\"-())-\"", R_RAW_STRING_LITERAL: 8); + assert_lex!("r\"|||\"", R_RAW_STRING_LITERAL: 6); + assert_lex!("r\"|a|b|\"", R_RAW_STRING_LITERAL: 8); + assert_lex!("r\"-|a|-|-\"", R_RAW_STRING_LITERAL: 10); +} + +/// The bracket matches but the hyphen count does not, so the string never +/// closes and the content runs to the end of the file +#[test] +fn the_hyphen_count_has_to_match() { + assert_lex_err!("r\"-(a)\"", ERROR_TOKEN: 7 => "Unterminated string"); +} + +#[test] +fn unterminated_runs_to_the_end_of_the_file() { + assert_lex_err!("r\"(abc", ERROR_TOKEN: 6 => "Unterminated string"); + assert_lex_err!("r\"|abc", ERROR_TOKEN: 6 => "Unterminated string"); +} + +/// The prefix has to be complete, so anything short of it falls through to the +/// identifier and punctuation arms +#[test] +fn an_incomplete_prefix_is_just_a_name() { + assert_lex!("rnorm", IDENT: 5); + assert_lex!("r + 1", IDENT: 1, WHITESPACE: 1, PLUS: 1, WHITESPACE: 1, R_DOUBLE_LITERAL: 1); + // A quote with no bracket after it: an ordinary name and an ordinary string + assert_lex!("r\"abc\"", IDENT: 1, R_DOUBLE_QUOTED_STRING_LITERAL: 5); + assert_lex!("r\"-abc\"", IDENT: 1, R_DOUBLE_QUOTED_STRING_LITERAL: 6); +} + +/// Escapes are not special inside a raw string +#[test] +fn backslashes_are_literal() { + assert_lex!("r\"(a\\)\"", R_RAW_STRING_LITERAL: 7); +} + +#[test] +fn raw_strings_may_span_newlines() { + assert_lex!("r\"(a\nb)\"", R_RAW_STRING_LITERAL: 8); +} diff --git a/crates/air_r_parser/src/lexer/tests/specials.rs b/crates/air_r_parser/src/lexer/tests/specials.rs new file mode 100644 index 00000000..1fec96e3 --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/specials.rs @@ -0,0 +1,53 @@ +use super::*; + +#[test] +fn special_operators() { + assert_lex!("%%", SPECIAL: 2); + assert_lex!("%/%", SPECIAL: 3); + assert_lex!("%in%", SPECIAL: 4); + assert_lex!("%o%", SPECIAL: 3); + assert_lex!("%>%", SPECIAL: 3); +} + +#[test] +fn in_context() { + assert_lex!( + "a %in% b", + IDENT: 1, + WHITESPACE: 1, + SPECIAL: 4, + WHITESPACE: 1, + IDENT: 1, + ); +} + +/// `%foo` is still a `SPECIAL` carrying a diagnostic, not an `ERROR_TOKEN`: the +/// text is a well-formed operator that was cut short, so the parser can still +/// use it. `ERROR_TOKEN` is for characters that start nothing at all. +#[test] +fn stops_at_a_newline() { + assert_lex_err!( + "%foo\n1", + SPECIAL: 4, NEWLINE: 1, R_DOUBLE_LITERAL: 1 + => "Unterminated" + ); +} + +#[test] +fn unterminated_at_end_of_file() { + assert_lex_err!("x %in", IDENT: 1, WHITESPACE: 1, SPECIAL: 3 => "Unterminated"); +} + +/// R's `SpecialValue` does not exclude backslash, so `%a\b%` is a valid operator +/// in R +#[test] +fn a_backslash_is_ordinary_text() { + assert_lex!( + "a %a\\b% b", + IDENT: 1, + WHITESPACE: 1, + SPECIAL: 5, + WHITESPACE: 1, + IDENT: 1, + ); +} diff --git a/crates/air_r_parser/src/lexer/tests/strings.rs b/crates/air_r_parser/src/lexer/tests/strings.rs new file mode 100644 index 00000000..8fc7d136 --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/strings.rs @@ -0,0 +1,78 @@ +use super::*; + +#[test] +fn double_quoted() { + assert_lex!("\"a\"", R_DOUBLE_QUOTED_STRING_LITERAL: 3); +} + +#[test] +fn single_quoted() { + assert_lex!("'abc'", R_SINGLE_QUOTED_STRING_LITERAL: 5); +} + +#[test] +fn an_empty_string_is_one_token() { + assert_lex!("\"\"", R_DOUBLE_QUOTED_STRING_LITERAL: 2); + assert_lex!("''", R_SINGLE_QUOTED_STRING_LITERAL: 2); + assert_lex!("r\"()\"", R_RAW_STRING_LITERAL: 5); +} + +#[test] +fn escapes_stay_inside_the_string() { + assert_lex!("\"a\\\"b\"", R_DOUBLE_QUOTED_STRING_LITERAL: 6); + assert_lex!("\"a\\\\\"", R_DOUBLE_QUOTED_STRING_LITERAL: 5); + assert_lex!("'a\\'b'", R_SINGLE_QUOTED_STRING_LITERAL: 6); +} + +#[test] +fn strings_may_span_newlines() { + assert_lex!("\"foo\nbar\"", R_DOUBLE_QUOTED_STRING_LITERAL: 9); +} + +#[test] +fn a_crlf_inside_a_string_is_content() { + assert_lex!( + "\"multiline\r\nstring\"\r\n", + R_DOUBLE_QUOTED_STRING_LITERAL: 19, + NEWLINE: 2 + ); +} + +#[test] +fn the_other_quote_is_inert() { + assert_lex!("\"it's\"", R_DOUBLE_QUOTED_STRING_LITERAL: 6); + assert_lex!("'say \"hi\"'", R_SINGLE_QUOTED_STRING_LITERAL: 10); +} + +#[test] +fn comments_and_operators_are_inert() { + assert_lex!("\"# %in% `a`\"", R_DOUBLE_QUOTED_STRING_LITERAL: 12); +} + +/// There is no closing delimiter, so the token cannot claim to be a string. What +/// keeps the error useful is that it points at the opening delimiter rather than +/// highlighting the rest of the file. +#[test] +fn unterminated_runs_to_the_end_of_the_file() { + assert_lex_err!("\"abc", ERROR_TOKEN: 4 => "Unterminated string"); + assert_lex_err!("\"", ERROR_TOKEN: 1 => "Unterminated string"); + assert_lex_err!("'a\nb", ERROR_TOKEN: 4 => "Unterminated string"); +} + +/// A backslash at the very end of the file must not read past it +#[test] +fn a_trailing_escape_does_not_run_past_the_end() { + assert_lex_err!("\"a\\", ERROR_TOKEN: 3 => "Unterminated string"); +} + +#[test] +fn in_context() { + assert_lex!( + "x <- \"a\"", + IDENT: 1, + WHITESPACE: 1, + ASSIGN: 2, + WHITESPACE: 1, + R_DOUBLE_QUOTED_STRING_LITERAL: 3, + ); +} diff --git a/crates/air_r_parser/src/lexer/tests/whitespace.rs b/crates/air_r_parser/src/lexer/tests/whitespace.rs new file mode 100644 index 00000000..c90b26f4 --- /dev/null +++ b/crates/air_r_parser/src/lexer/tests/whitespace.rs @@ -0,0 +1,30 @@ +use super::*; + +#[test] +fn runs_coalesce() { + assert_lex!(" ", WHITESPACE: 3); + assert_lex!(" \t\u{0B}\u{0C} ", WHITESPACE: 5); +} + +#[test] +fn whitespace_stops_at_a_line_break() { + assert_lex!(" \n ", WHITESPACE: 2, NEWLINE: 1, WHITESPACE: 2); +} + +#[test] +fn line_feed_and_lone_carriage_return() { + assert_lex!("\n", NEWLINE: 1); + assert_lex!("\r", NEWLINE: 1); +} + +#[test] +fn crlf_is_one_newline() { + assert_lex!("\r\n", NEWLINE: 2); +} + +#[test] +fn each_line_break_is_its_own_token() { + assert_lex!("\n\n", NEWLINE: 1, NEWLINE: 1); + // The first `\r` stands alone; only the second pairs with the `\n` + assert_lex!("\r\r\n", NEWLINE: 1, NEWLINE: 2); +} diff --git a/crates/air_r_parser/src/lib.rs b/crates/air_r_parser/src/lib.rs index 84d72fda..3968be31 100644 --- a/crates/air_r_parser/src/lib.rs +++ b/crates/air_r_parser/src/lib.rs @@ -1,14 +1,16 @@ +mod diagnostics; +mod lexer; mod options; mod parse; +mod parser; +mod syntax; +mod token_source; -#[allow(unused)] -mod treesitter; +pub use crate::options::RParserOptions; +pub use crate::parse::RParse; +pub use crate::parse::parse; use air_r_factory::RSyntaxFactory; -pub use options::RParserOptions; -pub use parse::RParse; -pub use parse::parse; - use air_r_syntax::RLanguage; use biome_parser::tree_sink::LosslessTreeSink; diff --git a/crates/air_r_parser/src/parse.rs b/crates/air_r_parser/src/parse.rs index 95d2ba18..681bfbf6 100644 --- a/crates/air_r_parser/src/parse.rs +++ b/crates/air_r_parser/src/parse.rs @@ -1,24 +1,13 @@ +use crate::RLosslessTreeSink; +use crate::options::RParserOptions; +use crate::parser::RParser; use air_r_syntax::RRoot; -use air_r_syntax::RSyntaxKind; use air_r_syntax::RSyntaxNode; use biome_parser::AnyParse; use biome_parser::diagnostic::ParseDiagnostic; -use biome_parser::event::Event; -use biome_parser::prelude::Trivia; use biome_rowan::AstNode; use biome_rowan::NodeCache; use biome_rowan::SendNode; -use biome_rowan::TextRange; -use biome_rowan::TextSize; -use biome_rowan::TriviaPieceKind; -use biome_unicode_table::Dispatch; -use tree_sitter::Tree; - -use crate::RLosslessTreeSink; -use crate::RParserOptions; -use crate::treesitter::NodeTypeExt; -use crate::treesitter::Preorder; -use crate::treesitter::WalkEvent; /// A utility struct for managing the result of a parser job /// @@ -93,1522 +82,86 @@ impl From for AnyParse { /// Parses `text` pub fn parse(text: &str, options: RParserOptions) -> RParse { let mut cache = NodeCache::default(); - let (events, trivia, diagnostics) = parse_text(text, options); - build_tree(text, events, trivia, diagnostics, &mut cache) -} - -fn build_tree( - text: &str, - events: Vec>, - trivia: Vec, - diagnostics: Vec, - cache: &mut NodeCache, -) -> RParse { - tracing::debug_span!("parse").in_scope(move || { - let mut tree_sink = RLosslessTreeSink::with_cache(text, &trivia, cache); - biome_parser::event::process(&mut tree_sink, events, diagnostics); - let (green, diagnostics) = tree_sink.finish(); - - RParse::new(green, diagnostics) - }) -} - -fn parse_text( - text: &str, - _options: RParserOptions, -) -> (Vec>, Vec, Vec) { - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_r::LANGUAGE.into()) - .unwrap(); - - let ast = parser.parse(text, None).unwrap(); - parse_from_treesitter(&ast, text) + parse_with_cache(text, options, &mut cache) } -fn parse_from_treesitter( - ast: &Tree, - text: &str, -) -> (Vec>, Vec, Vec) { - if ast.root_node().has_error() { - // TODO: In the long term we want an error resiliant parser. - // This would probably only be able to happen if we swap out tree sitter - // for a hand written recursive descent pratt parser using the Biome infra. - return parse_failure(text); - } - - let mut walker = RWalk::new(text); +/// Parses `text`, structurally sharing what it can with the trees already in +/// `cache` +fn parse_with_cache(text: &str, options: RParserOptions, cache: &mut NodeCache) -> RParse { + let mut parser = RParser::new(text, options); + crate::syntax::parse_root(&mut parser); + let (events, trivia, diagnostics) = parser.finish(); - let root = ast.root_node(); - let mut iter = root.preorder(); - walker.walk(&mut iter); + let mut tree_sink = RLosslessTreeSink::with_cache(text, &trivia, cache); + biome_parser::event::process(&mut tree_sink, events, diagnostics); + let (green, diagnostics) = tree_sink.finish(); - let (events, trivia) = walker.events.drain(); - - (events, trivia, Vec::new()) -} - -fn parse_failure(text: &str) -> (Vec>, Vec, Vec) { - // Safety: We don't allow files longer than a `u32` can support. - let end = - TextSize::try_from(text.len()).expect("`text` can't be longer than `TextSize` allows."); - - // Must provide a valid root node on failures, otherwise `tree_sink.finish()` fails. - // An `R_ROOT` must have an `R_STATEMENT_LIST`, and we force that to hold 1 expression - // statement with a bogus expression that spans the whole file. This allows the AST printer to - // work correctly in error tests, and during debugging. - let events = vec![ - Event::Start { - kind: RSyntaxKind::R_ROOT, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_STATEMENT_LIST, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_EXPRESSION_STATEMENT, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_BOGUS_EXPRESSION, - forward_parent: None, - }, - Event::Token { - kind: RSyntaxKind::R_BOGUS, - end, - }, - Event::Finish, - Event::Finish, - Event::Finish, - Event::Finish, - ]; - - // No trivia - let trivia = vec![]; - - // Single whole-file diagnostic for now - let span: Option = None; - let diagnostic = ParseDiagnostic::new("Failed to parse due to syntax errors.", span); - - (events, trivia, vec![diagnostic]) -} - -/// Given an ast with absolutely no ERROR or MISSING nodes, let's walk that tree -/// and collect our `trivia` and `events`. -struct RWalk<'src> { - text: &'src str, - events: RWalkEvents, - last_end: TextSize, -} - -impl<'src> RWalk<'src> { - fn new(text: &'src str) -> Self { - Self { - text, - events: RWalkEvents::new(), - last_end: TextSize::from(0), - } - } - - fn walk(&mut self, iter: &mut Preorder) { - while let Some(event) = iter.next() { - match event { - WalkEvent::Enter(node) => self.handle_enter(node, node.syntax_kind(), iter), - WalkEvent::Leave(node) => self.handle_leave(node, node.syntax_kind()), - } - } - } - - /// Walk only the upcoming node, including its subtree - /// - /// If the `next()` event is an `Enter`, `walk_next()` walks the - /// `node` returned in the `Enter` event until hitting the `Leave` event - /// of that `node`. - fn walk_next(&mut self, iter: &mut Preorder) { - let end = match iter.peek() { - Some(end) => match end { - WalkEvent::Enter(end) => *end, - WalkEvent::Leave(_) => return, - }, - None => return, - }; - - while let Some(event) = iter.next() { - match event { - WalkEvent::Enter(node) => self.handle_enter(node, node.syntax_kind(), iter), - WalkEvent::Leave(node) => { - self.handle_leave(node, node.syntax_kind()); - if node == end { - break; - } - } - } - } - } - - fn handle_enter(&mut self, node: tree_sitter::Node, kind: RSyntaxKind, iter: &mut Preorder) { - match kind { - RSyntaxKind::R_ROOT => self.handle_root_enter(node, iter), - - RSyntaxKind::R_UNARY_EXPRESSION - | RSyntaxKind::R_BINARY_EXPRESSION - | RSyntaxKind::R_FUNCTION_DEFINITION - | RSyntaxKind::R_FOR_STATEMENT - | RSyntaxKind::R_WHILE_STATEMENT - | RSyntaxKind::R_REPEAT_STATEMENT - | RSyntaxKind::R_PARENTHESIZED_EXPRESSION - | RSyntaxKind::R_EXTRACT_EXPRESSION - | RSyntaxKind::R_NAMESPACE_EXPRESSION - | RSyntaxKind::R_NA_EXPRESSION => self.handle_node_enter(kind), - - RSyntaxKind::R_CALL => self.handle_call_like_enter( - RSyntaxKind::R_CALL, - RSyntaxKind::R_CALL_ARGUMENTS, - node, - iter, - ), - RSyntaxKind::R_SUBSET => self.handle_call_like_enter( - RSyntaxKind::R_SUBSET, - RSyntaxKind::R_SUBSET_ARGUMENTS, - node, - iter, - ), - RSyntaxKind::R_SUBSET2 => self.handle_call_like_enter( - RSyntaxKind::R_SUBSET2, - RSyntaxKind::R_SUBSET2_ARGUMENTS, - node, - iter, - ), - RSyntaxKind::R_PARAMETERS => self.handle_parameters_enter(node, iter), - RSyntaxKind::R_PARAMETER => self.handle_parameter_enter(node, iter), - RSyntaxKind::R_IF_STATEMENT => self.handle_if_statement_enter(node, iter), - RSyntaxKind::R_ARGUMENT => self.handle_argument_enter(node, iter), - RSyntaxKind::R_BRACED_STATEMENTS => self.handle_braced_statements_enter(node, iter), - - // Literals / wrapped keywords - RSyntaxKind::R_DOUBLE_VALUE - | RSyntaxKind::R_IDENTIFIER - | RSyntaxKind::R_DOT_DOT_I - | RSyntaxKind::R_DOTS - | RSyntaxKind::R_NEXT_EXPRESSION - | RSyntaxKind::R_BREAK_EXPRESSION - | RSyntaxKind::R_TRUE_EXPRESSION - | RSyntaxKind::R_FALSE_EXPRESSION - | RSyntaxKind::R_NULL_EXPRESSION - | RSyntaxKind::R_INF_EXPRESSION - | RSyntaxKind::R_NAN_EXPRESSION => self.handle_value_enter(kind), - - RSyntaxKind::R_INTEGER_VALUE => self.handle_integer_value_enter(iter), - RSyntaxKind::R_COMPLEX_VALUE => self.handle_complex_value_enter(iter), - RSyntaxKind::R_STRING_VALUE => self.handle_string_value_enter(iter), - - // Tokens are no-ops on `Enter`, handled on `Leave` - RSyntaxKind::SEMICOLON - | RSyntaxKind::COMMA - | RSyntaxKind::TILDE - | RSyntaxKind::ASSIGN - | RSyntaxKind::SUPER_ASSIGN - | RSyntaxKind::WALRUS - | RSyntaxKind::ASSIGN_RIGHT - | RSyntaxKind::SUPER_ASSIGN_RIGHT - | RSyntaxKind::EQUAL - | RSyntaxKind::OR - | RSyntaxKind::AND - | RSyntaxKind::OR2 - | RSyntaxKind::AND2 - | RSyntaxKind::LESS_THAN - | RSyntaxKind::LESS_THAN_OR_EQUAL_TO - | RSyntaxKind::GREATER_THAN - | RSyntaxKind::GREATER_THAN_OR_EQUAL_TO - | RSyntaxKind::EQUAL2 - | RSyntaxKind::NOT_EQUAL - | RSyntaxKind::PLUS - | RSyntaxKind::MINUS - | RSyntaxKind::MULTIPLY - | RSyntaxKind::DIVIDE - | RSyntaxKind::EXPONENTIATE - | RSyntaxKind::EXPONENTIATE2 - | RSyntaxKind::PIPE - | RSyntaxKind::SPECIAL - | RSyntaxKind::COLON - | RSyntaxKind::COLON2 - | RSyntaxKind::COLON3 - | RSyntaxKind::DOLLAR - | RSyntaxKind::AT - | RSyntaxKind::BANG - | RSyntaxKind::WAT - | RSyntaxKind::BACKSLASH - | RSyntaxKind::FUNCTION_KW - | RSyntaxKind::FOR_KW - | RSyntaxKind::IN_KW - | RSyntaxKind::WHILE_KW - | RSyntaxKind::REPEAT_KW - | RSyntaxKind::IF_KW - | RSyntaxKind::ELSE_KW - | RSyntaxKind::NA_LOGICAL_KW - | RSyntaxKind::NA_INTEGER_KW - | RSyntaxKind::NA_DOUBLE_KW - | RSyntaxKind::NA_COMPLEX_KW - | RSyntaxKind::NA_CHARACTER_KW - | RSyntaxKind::L_PAREN - | RSyntaxKind::R_PAREN - | RSyntaxKind::L_BRACK - | RSyntaxKind::R_BRACK - | RSyntaxKind::L_BRACK2 - | RSyntaxKind::R_BRACK2 - | RSyntaxKind::L_CURLY - | RSyntaxKind::R_CURLY => (), - - // Comments - RSyntaxKind::COMMENT => self.handle_comment_enter(), - - // Unreachable - RSyntaxKind::R_ELSE_CLAUSE - | RSyntaxKind::R_CALL_ARGUMENTS - | RSyntaxKind::R_SUBSET_ARGUMENTS - | RSyntaxKind::R_SUBSET2_ARGUMENTS - | RSyntaxKind::R_ARGUMENT_NAME_CLAUSE - | RSyntaxKind::R_PARAMETER_LIST - | RSyntaxKind::R_ARGUMENT_LIST - | RSyntaxKind::R_STATEMENT_LIST - | RSyntaxKind::R_EXPRESSION_STATEMENT - | RSyntaxKind::R_EMPTY_STATEMENT - | RSyntaxKind::R_PARAMETER_DEFAULT - | RSyntaxKind::EOF - | RSyntaxKind::UNICODE_BOM - | RSyntaxKind::DOTS - | RSyntaxKind::R_INTEGER_LITERAL - | RSyntaxKind::R_DOUBLE_LITERAL - | RSyntaxKind::R_COMPLEX_LITERAL - | RSyntaxKind::R_SINGLE_QUOTED_STRING_LITERAL - | RSyntaxKind::R_DOUBLE_QUOTED_STRING_LITERAL - | RSyntaxKind::R_RAW_STRING_LITERAL - | RSyntaxKind::NEWLINE - | RSyntaxKind::WHITESPACE - | RSyntaxKind::IDENT - | RSyntaxKind::DOTDOTI - | RSyntaxKind::ERROR_TOKEN - | RSyntaxKind::NEXT_KW - | RSyntaxKind::BREAK_KW - | RSyntaxKind::TRUE_KW - | RSyntaxKind::FALSE_KW - | RSyntaxKind::NULL_KW - | RSyntaxKind::INF_KW - | RSyntaxKind::NAN_KW - | RSyntaxKind::R_BOGUS - | RSyntaxKind::R_BOGUS_VALUE - | RSyntaxKind::R_BOGUS_EXPRESSION - | RSyntaxKind::R_BOGUS_SELECTOR - | RSyntaxKind::R_BOGUS_PARAMETER_NAME - | RSyntaxKind::R_BOGUS_FOR_VARIABLE - | RSyntaxKind::TOMBSTONE - | RSyntaxKind::__LAST => unreachable!("{kind:?}"), - } - } - - fn handle_leave(&mut self, node: tree_sitter::Node, kind: RSyntaxKind) { - match kind { - RSyntaxKind::R_ROOT => self.handle_root_leave(node), - - RSyntaxKind::R_UNARY_EXPRESSION - | RSyntaxKind::R_BINARY_EXPRESSION - | RSyntaxKind::R_FUNCTION_DEFINITION - | RSyntaxKind::R_FOR_STATEMENT - | RSyntaxKind::R_WHILE_STATEMENT - | RSyntaxKind::R_REPEAT_STATEMENT - | RSyntaxKind::R_PARENTHESIZED_EXPRESSION - | RSyntaxKind::R_EXTRACT_EXPRESSION - | RSyntaxKind::R_NAMESPACE_EXPRESSION - | RSyntaxKind::R_NA_EXPRESSION => self.handle_node_leave(kind), - - RSyntaxKind::R_CALL => self.handle_call_like_leave(kind), - RSyntaxKind::R_SUBSET => self.handle_call_like_leave(kind), - RSyntaxKind::R_SUBSET2 => self.handle_call_like_leave(kind), - RSyntaxKind::R_PARAMETERS => self.handle_parameters_leave(), - RSyntaxKind::R_PARAMETER => self.handle_parameter_leave(), - RSyntaxKind::R_IF_STATEMENT => self.handle_if_statement_leave(), - RSyntaxKind::R_ARGUMENT => self.handle_argument_leave(), - RSyntaxKind::R_BRACED_STATEMENTS => self.handle_braced_statements_leave(), - - // Literals / wrapped keywords - RSyntaxKind::R_DOUBLE_VALUE => { - self.handle_value_leave(node, kind, RSyntaxKind::R_DOUBLE_LITERAL) - } - RSyntaxKind::R_IDENTIFIER => self.handle_value_leave(node, kind, RSyntaxKind::IDENT), - RSyntaxKind::R_DOTS => self.handle_value_leave(node, kind, RSyntaxKind::DOTS), - RSyntaxKind::R_DOT_DOT_I => self.handle_value_leave(node, kind, RSyntaxKind::DOTDOTI), - RSyntaxKind::R_NEXT_EXPRESSION => { - self.handle_value_leave(node, kind, RSyntaxKind::NEXT_KW) - } - RSyntaxKind::R_BREAK_EXPRESSION => { - self.handle_value_leave(node, kind, RSyntaxKind::BREAK_KW) - } - RSyntaxKind::R_TRUE_EXPRESSION => { - self.handle_value_leave(node, kind, RSyntaxKind::TRUE_KW) - } - RSyntaxKind::R_FALSE_EXPRESSION => { - self.handle_value_leave(node, kind, RSyntaxKind::FALSE_KW) - } - RSyntaxKind::R_NULL_EXPRESSION => { - self.handle_value_leave(node, kind, RSyntaxKind::NULL_KW) - } - RSyntaxKind::R_INF_EXPRESSION => { - self.handle_value_leave(node, kind, RSyntaxKind::INF_KW) - } - RSyntaxKind::R_NAN_EXPRESSION => { - self.handle_value_leave(node, kind, RSyntaxKind::NAN_KW) - } - RSyntaxKind::R_INTEGER_VALUE => self.handle_integer_value_leave(node), - RSyntaxKind::R_COMPLEX_VALUE => self.handle_complex_value_leave(node), - RSyntaxKind::R_STRING_VALUE => self.handle_string_value_leave(node), - - // Tokens - RSyntaxKind::SEMICOLON - | RSyntaxKind::COMMA - | RSyntaxKind::TILDE - | RSyntaxKind::ASSIGN - | RSyntaxKind::SUPER_ASSIGN - | RSyntaxKind::WALRUS - | RSyntaxKind::ASSIGN_RIGHT - | RSyntaxKind::SUPER_ASSIGN_RIGHT - | RSyntaxKind::EQUAL - | RSyntaxKind::OR - | RSyntaxKind::AND - | RSyntaxKind::OR2 - | RSyntaxKind::AND2 - | RSyntaxKind::LESS_THAN - | RSyntaxKind::LESS_THAN_OR_EQUAL_TO - | RSyntaxKind::GREATER_THAN - | RSyntaxKind::GREATER_THAN_OR_EQUAL_TO - | RSyntaxKind::EQUAL2 - | RSyntaxKind::NOT_EQUAL - | RSyntaxKind::PLUS - | RSyntaxKind::MINUS - | RSyntaxKind::MULTIPLY - | RSyntaxKind::DIVIDE - | RSyntaxKind::EXPONENTIATE - | RSyntaxKind::EXPONENTIATE2 - | RSyntaxKind::PIPE - | RSyntaxKind::SPECIAL - | RSyntaxKind::COLON - | RSyntaxKind::COLON2 - | RSyntaxKind::COLON3 - | RSyntaxKind::DOLLAR - | RSyntaxKind::AT - | RSyntaxKind::BANG - | RSyntaxKind::WAT - | RSyntaxKind::BACKSLASH - | RSyntaxKind::FUNCTION_KW - | RSyntaxKind::FOR_KW - | RSyntaxKind::IN_KW - | RSyntaxKind::WHILE_KW - | RSyntaxKind::REPEAT_KW - | RSyntaxKind::IF_KW - | RSyntaxKind::ELSE_KW - | RSyntaxKind::NA_LOGICAL_KW - | RSyntaxKind::NA_INTEGER_KW - | RSyntaxKind::NA_DOUBLE_KW - | RSyntaxKind::NA_COMPLEX_KW - | RSyntaxKind::NA_CHARACTER_KW - | RSyntaxKind::L_PAREN - | RSyntaxKind::R_PAREN - | RSyntaxKind::L_BRACK - | RSyntaxKind::R_BRACK - | RSyntaxKind::L_BRACK2 - | RSyntaxKind::R_BRACK2 - | RSyntaxKind::L_CURLY - | RSyntaxKind::R_CURLY => self.handle_token(node, kind), - - // Comments - RSyntaxKind::COMMENT => self.handle_comment_leave(node), - - // Unreachable directly - RSyntaxKind::R_ELSE_CLAUSE - | RSyntaxKind::R_CALL_ARGUMENTS - | RSyntaxKind::R_SUBSET_ARGUMENTS - | RSyntaxKind::R_SUBSET2_ARGUMENTS - | RSyntaxKind::R_ARGUMENT_NAME_CLAUSE - | RSyntaxKind::R_PARAMETER_LIST - | RSyntaxKind::R_ARGUMENT_LIST - | RSyntaxKind::R_STATEMENT_LIST - | RSyntaxKind::R_EXPRESSION_STATEMENT - | RSyntaxKind::R_EMPTY_STATEMENT - | RSyntaxKind::R_PARAMETER_DEFAULT - | RSyntaxKind::EOF - | RSyntaxKind::UNICODE_BOM - | RSyntaxKind::DOTS - | RSyntaxKind::DOTDOTI - | RSyntaxKind::R_INTEGER_LITERAL - | RSyntaxKind::R_DOUBLE_LITERAL - | RSyntaxKind::R_COMPLEX_LITERAL - | RSyntaxKind::R_SINGLE_QUOTED_STRING_LITERAL - | RSyntaxKind::R_DOUBLE_QUOTED_STRING_LITERAL - | RSyntaxKind::R_RAW_STRING_LITERAL - | RSyntaxKind::NEWLINE - | RSyntaxKind::WHITESPACE - | RSyntaxKind::IDENT - | RSyntaxKind::ERROR_TOKEN - | RSyntaxKind::NEXT_KW - | RSyntaxKind::BREAK_KW - | RSyntaxKind::TRUE_KW - | RSyntaxKind::FALSE_KW - | RSyntaxKind::NULL_KW - | RSyntaxKind::INF_KW - | RSyntaxKind::NAN_KW - | RSyntaxKind::R_BOGUS - | RSyntaxKind::R_BOGUS_VALUE - | RSyntaxKind::R_BOGUS_EXPRESSION - | RSyntaxKind::R_BOGUS_SELECTOR - | RSyntaxKind::R_BOGUS_PARAMETER_NAME - | RSyntaxKind::R_BOGUS_FOR_VARIABLE - | RSyntaxKind::TOMBSTONE - | RSyntaxKind::__LAST => unreachable!("{kind:?}"), - } - } - - fn handle_node_enter(&mut self, kind: RSyntaxKind) { - self.events.start(kind); - } - - // `_kind` is nice to see at call sites as it makes the code more self-expanatory - fn handle_node_leave(&mut self, _kind: RSyntaxKind) { - self.events.finish(); - } - - fn handle_value_enter(&mut self, kind: RSyntaxKind) { - self.handle_node_enter(kind); - } - - fn handle_value_leave( - &mut self, - node: tree_sitter::Node, - kind: RSyntaxKind, - literal_kind: RSyntaxKind, - ) { - // Push the token for the literal - self.handle_token(node, literal_kind); - - // Then close the node - self.handle_node_leave(kind); - } - - fn handle_root_enter(&mut self, node: tree_sitter::Node, iter: &mut Preorder) { - // Start the overarching root - self.handle_node_enter(RSyntaxKind::R_ROOT); - - // TODO: Handle optional BOM? - - self.handle_node_enter(RSyntaxKind::R_STATEMENT_LIST); - - // Root contains a list of `statements`, so uses the same - // style loop as `handle_braced_statements_enter()` - while let Some(event) = iter.peek() { - match event { - WalkEvent::Enter(next) => match next.syntax_kind() { - RSyntaxKind::COMMENT => self.walk_next(iter), - _ => self.handle_statement(iter), - }, - WalkEvent::Leave(next) => { - if node != *next { - panic!("Expected next `Leave` event to be for `node`."); - } - break; - } - } - } - } - - fn handle_root_leave(&mut self, node: tree_sitter::Node) { - // Finish statement list - self.handle_node_leave(RSyntaxKind::R_STATEMENT_LIST); - - // Safety: tree-sitter uses `u32` internally and upcasts to `usize` in - // `end_byte()`. `TextSize` also uses `u32` internally, so we are safe. - let this_end = TextSize::try_from(node.end_byte()).unwrap(); - let gap = &self.text[usize::from(self.last_end)..usize::from(this_end)]; - - // Derive trivia between last token and end of document. Leading - // trivia here belongs to the `EOF` token, which `TreeSink` adds for us. - self.events.derive_trivia(gap, self.last_end); - - self.handle_node_leave(RSyntaxKind::R_ROOT); - } - - fn handle_token(&mut self, node: tree_sitter::Node, kind: RSyntaxKind) { - // Safety: tree-sitter uses `u32` internally and upcasts to `usize` in - // `start/end_byte()`. `TextSize` also uses `u32` internally, so we are safe. - let this_start = TextSize::try_from(node.start_byte()).unwrap(); - let this_end = TextSize::try_from(node.end_byte()).unwrap(); - let gap = &self.text[usize::from(self.last_end)..usize::from(this_start)]; - - self.events.derive_trivia(gap, self.last_end); - - self.events.token(kind, this_end); - - self.last_end = this_end; - } - - fn handle_comment_enter(&mut self) { - // Nothing, handled on `Leave` - } - - fn handle_comment_leave(&mut self, node: tree_sitter::Node) { - let this_start = TextSize::try_from(node.start_byte()).unwrap(); - let this_end = TextSize::try_from(node.end_byte()).unwrap(); - let gap = &self.text[usize::from(self.last_end)..usize::from(this_start)]; - - self.events.derive_trivia(gap, self.last_end); - - self.events.comment(TextRange::new(this_start, this_end)); - - self.last_end = this_end; - } - - fn handle_parameters_enter(&mut self, node: tree_sitter::Node, iter: &mut Preorder) { - self.handle_node_enter(RSyntaxKind::R_PARAMETERS); - - // Regardless of whether or not we see an `R_PARAMETER`, we have to - // open and close the required `R_PARAMETER_LIST` - while let Some(event) = iter.peek() { - match event { - WalkEvent::Enter(next) => match next.syntax_kind() { - RSyntaxKind::L_PAREN => { - self.walk_next(iter); - self.handle_node_enter(RSyntaxKind::R_PARAMETER_LIST); - } - RSyntaxKind::R_PAREN => { - self.handle_node_leave(RSyntaxKind::R_PARAMETER_LIST); - self.walk_next(iter); - } - RSyntaxKind::R_PARAMETER | RSyntaxKind::COMMA | RSyntaxKind::COMMENT => { - self.walk_next(iter) - } - kind => unreachable!("{kind:?}"), - }, - WalkEvent::Leave(next) => { - if node != *next { - panic!("Expected next `Leave` event to be for `node`."); - } - break; - } - } - } - } - - fn handle_parameters_leave(&mut self) { - self.handle_node_leave(RSyntaxKind::R_PARAMETERS); - } - - fn handle_parameter_enter(&mut self, node: tree_sitter::Node, iter: &mut Preorder) { - self.handle_node_enter(RSyntaxKind::R_PARAMETER); - - // Seeing an `=` causes us to open an `R_PARAMETER_DEFAULT` node which - // we push the `=` keyword under, along with the remaining `value` for - // the default, and any comments that appear there. We then close the - // `R_PARAMETER_DEFAULT` on the way out. - let mut used_equal = false; - - while let Some(event) = iter.peek() { - match event { - WalkEvent::Enter(next) => match next.syntax_kind() { - RSyntaxKind::EQUAL => { - used_equal = true; - self.handle_node_enter(RSyntaxKind::R_PARAMETER_DEFAULT); - self.walk_next(iter); - } - // Main loop handles everything else - _ => self.walk_next(iter), - }, - WalkEvent::Leave(next) => { - if node != *next { - panic!("Expected next `Leave` event to be for `node`."); - } - break; - } - } - } - - if used_equal { - self.handle_node_leave(RSyntaxKind::R_PARAMETER_DEFAULT); - } - } - - fn handle_parameter_leave(&mut self) { - self.handle_node_leave(RSyntaxKind::R_PARAMETER); - } - - fn handle_integer_value_enter(&mut self, iter: &mut Preorder) { - // Skip subtree, we don't want to separate the literal from the `L`. - // Can't have comments between the value and the `L`. - iter.skip_subtree(); - self.handle_value_enter(RSyntaxKind::R_INTEGER_VALUE); - } - - fn handle_integer_value_leave(&mut self, node: tree_sitter::Node) { - self.handle_value_leave( - node, - RSyntaxKind::R_INTEGER_VALUE, - RSyntaxKind::R_INTEGER_LITERAL, - ); - } - - fn handle_complex_value_enter(&mut self, iter: &mut Preorder) { - // Skip subtree, we don't want to separate the literal from the `i`. - // Can't have comments between the value and the `i`. - iter.skip_subtree(); - self.handle_value_enter(RSyntaxKind::R_COMPLEX_VALUE); - } - - fn handle_complex_value_leave(&mut self, node: tree_sitter::Node) { - self.handle_value_leave( - node, - RSyntaxKind::R_COMPLEX_VALUE, - RSyntaxKind::R_COMPLEX_LITERAL, - ); - } - - fn handle_string_value_enter(&mut self, iter: &mut Preorder) { - // Skip subtree, the delimiters and content are one token. - // Can't have comments in a string subtree. - iter.skip_subtree(); - self.handle_value_enter(RSyntaxKind::R_STRING_VALUE); - } - - fn handle_string_value_leave(&mut self, node: tree_sitter::Node) { - let literal_kind = match self.text.as_bytes()[node.start_byte()] { - b'"' => RSyntaxKind::R_DOUBLE_QUOTED_STRING_LITERAL, - b'\'' => RSyntaxKind::R_SINGLE_QUOTED_STRING_LITERAL, - b'r' | b'R' => RSyntaxKind::R_RAW_STRING_LITERAL, - byte => unreachable!("Unexpected string start {:?}", char::from(byte)), - }; - self.handle_value_leave(node, RSyntaxKind::R_STRING_VALUE, literal_kind); - } - - fn handle_if_statement_enter(&mut self, node: tree_sitter::Node, iter: &mut Preorder) { - self.handle_node_enter(RSyntaxKind::R_IF_STATEMENT); - - // Seeing an `else` causes us to open an `R_ELSE_CLAUSE` node which - // we push the `else` keyword under, along with the `alternative`, - // and any comments that appear there. We then close the `R_ELSE_CLAUSE` - // on the way out. - let mut used_else = false; - - while let Some(event) = iter.peek() { - match event { - WalkEvent::Enter(next) => match next.syntax_kind() { - RSyntaxKind::IF_KW => self.walk_next(iter), - RSyntaxKind::L_PAREN => self.walk_next(iter), - RSyntaxKind::R_PAREN => self.walk_next(iter), - RSyntaxKind::ELSE_KW => { - used_else = true; - self.handle_node_enter(RSyntaxKind::R_ELSE_CLAUSE); - self.walk_next(iter); - } - RSyntaxKind::COMMENT => self.walk_next(iter), - // i.e. the `condition`, `consequence`, and `alternative` - _ => self.walk_next(iter), - }, - WalkEvent::Leave(next) => { - if node != *next { - panic!("Expected next `Leave` event to be for `node`."); - } - break; - } - } - } - - if used_else { - self.handle_node_leave(RSyntaxKind::R_ELSE_CLAUSE); - } - } - - fn handle_if_statement_leave(&mut self) { - self.handle_node_leave(RSyntaxKind::R_IF_STATEMENT); - } - - fn handle_braced_statements_enter(&mut self, node: tree_sitter::Node, iter: &mut Preorder) { - self.handle_node_enter(RSyntaxKind::R_BRACED_STATEMENTS); - - while let Some(event) = iter.peek() { - match event { - WalkEvent::Enter(next) => match next.syntax_kind() { - RSyntaxKind::L_CURLY => { - self.walk_next(iter); - self.handle_node_enter(RSyntaxKind::R_STATEMENT_LIST); - } - RSyntaxKind::R_CURLY => { - self.handle_node_leave(RSyntaxKind::R_STATEMENT_LIST); - self.walk_next(iter); - } - RSyntaxKind::COMMENT => self.walk_next(iter), - _ => self.handle_statement(iter), - }, - WalkEvent::Leave(next) => { - if node != *next { - panic!("Expected next `Leave` event to be for `node`."); - } - break; - } - } - } - } - - fn handle_braced_statements_leave(&mut self) { - self.handle_node_leave(RSyntaxKind::R_BRACED_STATEMENTS); - } - - /// tree-sitter-r hides `;`, so the optional `;` slot of the statement is - /// never filled, and we never produce an `R_EMPTY_STATEMENT` - fn handle_statement(&mut self, iter: &mut Preorder) { - self.handle_node_enter(RSyntaxKind::R_EXPRESSION_STATEMENT); - self.walk_next(iter); - self.handle_node_leave(RSyntaxKind::R_EXPRESSION_STATEMENT); - } - - fn handle_call_like_enter( - &mut self, - kind: RSyntaxKind, - arguments_kind: RSyntaxKind, - node: tree_sitter::Node, - iter: &mut Preorder, - ) { - self.handle_node_enter(kind); - - while let Some(event) = iter.peek() { - match event { - WalkEvent::Enter(_) => match iter.peek_field_name() { - Some("arguments") => self.handle_arguments(arguments_kind, iter), - // `"function"` field and comments - _ => self.walk_next(iter), - }, - WalkEvent::Leave(next) => { - if node != *next { - panic!("Expected next `Leave` event to be for `node`."); - } - break; - } - } - } - } - - fn handle_call_like_leave(&mut self, kind: RSyntaxKind) { - self.handle_node_leave(kind); - } - - fn handle_arguments(&mut self, kind: RSyntaxKind, iter: &mut Preorder) { - // `Enter` event for arguments - let event = iter.next().unwrap(); - let WalkEvent::Enter(node) = event else { - panic!("Expected to `Enter` arguments"); - }; - - self.handle_arguments_enter(kind, node, iter); - - // `Leave` event for arguments - let event = iter.next().unwrap(); - assert_eq!( - event, - WalkEvent::Leave(node), - "Expected to `Leave` arguments" - ); - - self.handle_arguments_leave(kind); - } - - fn handle_arguments_enter( - &mut self, - kind: RSyntaxKind, - node: tree_sitter::Node, - iter: &mut Preorder, - ) { - let (open, close) = match kind { - RSyntaxKind::R_CALL_ARGUMENTS => (RSyntaxKind::L_PAREN, RSyntaxKind::R_PAREN), - RSyntaxKind::R_SUBSET_ARGUMENTS => (RSyntaxKind::L_BRACK, RSyntaxKind::R_BRACK), - RSyntaxKind::R_SUBSET2_ARGUMENTS => (RSyntaxKind::L_BRACK2, RSyntaxKind::R_BRACK2), - _ => unreachable!("Found unexpected kind '{kind:?}'."), - }; - - self.handle_node_enter(kind); - - let mut last_kind = RSyntaxKind::R_BOGUS; - - while let Some(event) = iter.peek() { - match event { - WalkEvent::Enter(next) => match next.syntax_kind() { - kind if kind == open => { - self.walk_next(iter); - self.handle_node_enter(RSyntaxKind::R_ARGUMENT_LIST); - last_kind = open; - } - kind if kind == close => { - self.handle_hole_before_close(last_kind); - self.handle_node_leave(RSyntaxKind::R_ARGUMENT_LIST); - self.walk_next(iter); - last_kind = close; - } - RSyntaxKind::COMMA => { - self.handle_hole_before_comma(last_kind, open); - self.walk_next(iter); - last_kind = RSyntaxKind::COMMA; - } - RSyntaxKind::R_ARGUMENT => { - self.walk_next(iter); - last_kind = RSyntaxKind::R_ARGUMENT; - } - RSyntaxKind::COMMENT => { - self.walk_next(iter); - // Not setting `last_kind` here! - } - kind => unreachable!("Found {kind:?} in arguments"), - }, - WalkEvent::Leave(next) => { - if node != *next { - panic!("Expected next `Leave` event to be for `node`."); - } - break; - } - } - } - } - - fn handle_arguments_leave(&mut self, kind: RSyntaxKind) { - self.handle_node_leave(kind); - } - - fn handle_argument_enter(&mut self, node: tree_sitter::Node, iter: &mut Preorder) { - self.handle_node_enter(RSyntaxKind::R_ARGUMENT); - - // A complication with arguments is that tree-sitter-r doesn't differentiate - // between named and unnamed arguments at the node level. Named arguments - // have a `"name"` field node (identifier/string/dots/dotdoti/null) followed - // by an `=` sign. If we see that `"name"` field, we open the name clause - // node and close it after we see the `=`. - let mut seen_equal = false; - - while let Some(event) = iter.peek() { - match event { - WalkEvent::Enter(next) => match next.syntax_kind() { - RSyntaxKind::R_IDENTIFIER - | RSyntaxKind::R_STRING_VALUE - | RSyntaxKind::R_DOTS - | RSyntaxKind::R_DOT_DOT_I - | RSyntaxKind::R_NULL_EXPRESSION => { - if !seen_equal - && let Some(next_field_name) = iter.peek_field_name() - && next_field_name == "name" - { - self.handle_node_enter(RSyntaxKind::R_ARGUMENT_NAME_CLAUSE) - } - self.walk_next(iter); - } - RSyntaxKind::EQUAL => { - seen_equal = true; - self.walk_next(iter); - self.handle_node_leave(RSyntaxKind::R_ARGUMENT_NAME_CLAUSE); - } - // Comments, values, etc - _ => self.walk_next(iter), - }, - WalkEvent::Leave(next) => { - if node != *next { - panic!("Expected next `Leave` event to be for `node`."); - } - break; - } - } - } - } - - fn handle_argument_leave(&mut self) { - self.handle_node_leave(RSyntaxKind::R_ARGUMENT); - } - - /// Is there a hole before this `)`, `]`, or `]]`? - /// - /// ```r - /// fn(,) - /// - /// fn( - /// x, - /// # comment - /// - /// ) - /// ``` - fn handle_hole_before_close(&mut self, last_kind: RSyntaxKind) { - if last_kind == RSyntaxKind::COMMA { - self.handle_node_enter(RSyntaxKind::R_ARGUMENT); - self.handle_node_leave(RSyntaxKind::R_ARGUMENT); - } - } - - /// Is there a hole before this `,`? - /// - /// ```r - /// fn(,) - /// - /// fn(x, ,) - /// - /// fn( - /// x, - /// # comment - /// , - /// ) - /// ``` - fn handle_hole_before_comma(&mut self, last_kind: RSyntaxKind, open: RSyntaxKind) { - if last_kind == RSyntaxKind::COMMA || last_kind == open { - self.handle_node_enter(RSyntaxKind::R_ARGUMENT); - self.handle_node_leave(RSyntaxKind::R_ARGUMENT); - } - } -} - -struct RWalkEvents { - events: Vec>, - trivia: Vec, - - /// Whether trivia trails the last token. Set by a token, cleared by a - /// newline. - /// - /// TODO(aether): Temporary field that is removed on the swap to aether - trailing: bool, -} - -impl RWalkEvents { - fn new() -> Self { - Self { - events: Vec::new(), - trivia: Vec::new(), - trailing: false, - } - } - - fn start(&mut self, kind: RSyntaxKind) { - self.push_event(Event::Start { - kind, - forward_parent: None, - }); - } - - fn token(&mut self, kind: RSyntaxKind, end: TextSize) { - self.push_event(Event::Token { kind, end }); - self.trailing = true; - } - - fn finish(&mut self) { - self.push_event(Event::Finish); - } - - fn comment(&mut self, range: TextRange) { - // Comments are "single line" even if they are consecutive - self.push_trivia(TriviaPieceKind::SingleLineComment, range); - } - - fn push_event(&mut self, event: Event) { - self.events.push(event); - } - - fn push_trivia(&mut self, kind: TriviaPieceKind, range: TextRange) { - if kind.is_newline() { - self.trailing = false; - } - self.trivia.push(Trivia::new(kind, range, self.trailing)); - } - - fn drain(self) -> (Vec>, Vec) { - (self.events, self.trivia) - } - - /// Given: - /// - A slice of `text` starting at byte `start` - /// - Which only contains whitespace or newlines - /// - And represents a "gap" between two tokens - /// - /// Derive the implied stream of trivia that exists in that gap - /// - /// TODO(aether): Trivia trails the token before it until a newline is seen, - /// and leads the token after it from the newline onward. Before the first - /// token of the file, everything leads. This matches Biome's parsers, and the - /// new aether parser. - /// - /// TODO(semicolon): tree-sitter-r does not emit semicolons in the parse tree, so they - /// technically also appear here in `text`. We hackily patch over this for now by - /// consuming semicolons as whitespace, which ends up getting ignored by the - /// formatter. It would be nice to have official semicolon handling, and then in the - /// formatter we'd just use `format_removed()` to officially remove semicolons. - /// - /// SAFETY: `last_end <= next_start` - /// SAFETY: `last_end` and `next_start` must define a range within `text` - fn derive_trivia(&mut self, text: &str, mut start: TextSize) { - let mut iter = text.as_bytes().iter().peekable(); - let mut end = start; - - while let Some(byte) = iter.next() { - end += TextSize::from(1); - - if Self::is_whitespace(*byte) { - // Finish out stream of whitespace - while iter.next_if(|byte| Self::is_whitespace(**byte)).is_some() { - end += TextSize::from(1); - } - let range = TextRange::new(start, end); - self.push_trivia(TriviaPieceKind::Whitespace, range); - start = end; - continue; - } - - // A lone `\r` not attached to an `\n` should not happen in a - // well-formed file (unless inside a string token), so we just - // treat it as a `\r\n` line ending. - if let b'\r' = byte { - match iter.next_if(|byte| **byte == b'\n') { - Some(_) => { - // Finish out `\r\n` - end += TextSize::from(1); - let range = TextRange::new(start, end); - self.push_trivia(TriviaPieceKind::Newline, range); - start = end; - } - None => { - // Finish out `\r` - let range = TextRange::new(start, end); - self.push_trivia(TriviaPieceKind::Newline, range); - start = end; - } - } - continue; - } - - if let b'\n' = byte { - // Finish out `\n` - let range = TextRange::new(start, end); - self.push_trivia(TriviaPieceKind::Newline, range); - start = end; - continue; - } - - unreachable!("Detected non trivia character!"); - } - } - - fn is_whitespace(byte: u8) -> bool { - // `WHS` maps newlines as "whitespace" but we handle that specially - match biome_unicode_table::lookup_byte(byte) { - Dispatch::WHS => byte != b'\r' && byte != b'\n', - // TODO(semicolon): Mapping semicolons as whitespace until we handle them officially - Dispatch::SEM => true, - _ => false, - } - } + RParse::new(green, diagnostics) } #[cfg(test)] mod tests { use super::*; + use air_r_syntax::RSyntaxKind; + use biome_diagnostics::PrintDescription; + use biome_rowan::AstNodeList; - enum Pos { - Leading, - Trailing, - } - - fn trivia(text: &str) -> Vec { - let (_events, trivia, _errors) = parse_text(text, RParserOptions::default()); - trivia - } - - fn ws(start: u32, end: u32, position: Pos) -> Trivia { - Trivia::new( - TriviaPieceKind::Whitespace, - TextRange::new(TextSize::from(start), TextSize::from(end)), - matches!(position, Pos::Trailing), - ) - } - - fn nl(start: u32, end: u32) -> Trivia { - Trivia::new( - TriviaPieceKind::Newline, - TextRange::new(TextSize::from(start), TextSize::from(end)), - false, - ) - } - - fn cmt(start: u32, end: u32, position: Pos) -> Trivia { - Trivia::new( - TriviaPieceKind::SingleLineComment, - TextRange::new(TextSize::from(start), TextSize::from(end)), - matches!(position, Pos::Trailing), - ) - } - - // TODO: It would be great if `biome_parser::token_source::Trivia` - // implemented `PartialEq`, maybe we should ask for that. - fn assert_eq_trivia(lhs: Vec, rhs: Vec) { - assert_eq!(lhs.len(), rhs.len()); - - for (i, (lhs, rhs)) in lhs.iter().zip(rhs.iter()).enumerate() { - let message = format!("In event {i} with:\nlhs {lhs:?}\nrhs {rhs:?}"); - assert_eq!(lhs.kind(), rhs.kind(), "{message}"); - assert_eq!(lhs.text_range(), rhs.text_range(), "{message}"); - assert_eq!(lhs.trailing(), rhs.trailing(), "{message}"); - } - } - - // TODO: It would be great if `biome_parser::token_source::Trivia` - // implemented `PartialEq`, maybe we should ask for that. - fn assert_eq_events(lhs: Vec>, rhs: Vec>) { - assert_eq!(lhs.len(), rhs.len(), "With:\nlhs {lhs:?}\nrhs {rhs:?}"); - - for (i, (lhs, rhs)) in lhs.iter().zip(rhs.iter()).enumerate() { - let message = format!("In event {i} with:\nlhs {lhs:?}\nrhs {rhs:?}"); - - match lhs { - Event::Start { - kind: lhs_kind, - forward_parent: lhs_forward_parent, - } => match rhs { - Event::Start { - kind: rhs_kind, - forward_parent: rhs_forward_parent, - } => { - assert_eq!(lhs_kind, rhs_kind, "{message}"); - assert_eq!(lhs_forward_parent, rhs_forward_parent, "{message}"); - } - Event::Finish => panic!("{message}"), - Event::Token { .. } => panic!("{message}"), - }, - Event::Token { - kind: lhs_kind, - end: lhs_end, - } => match rhs { - Event::Token { - kind: rhs_kind, - end: rhs_end, - } => { - assert_eq!(lhs_kind, rhs_kind, "{message}"); - assert_eq!(lhs_end, rhs_end, "{message}"); - } - Event::Start { .. } => panic!("{message}"), - Event::Finish => panic!("{message}"), - }, - Event::Finish => match rhs { - Event::Finish => (), - Event::Start { .. } => panic!("{message}"), - Event::Token { .. } => panic!("{message}"), - }, - } - } - } - - #[test] - fn test_parse_trivia_smoke_test() { - assert_eq_trivia( - trivia("1 + 1"), - vec![ws(1, 2, Pos::Trailing), ws(3, 4, Pos::Trailing)], - ); - } - - #[test] - fn test_parse_trivia_tab_test() { - assert_eq_trivia( - trivia("1\t+\t\n\t1"), - vec![ - ws(1, 2, Pos::Trailing), - ws(3, 4, Pos::Trailing), - nl(4, 5), - ws(5, 6, Pos::Leading), - ], - ); - } - - #[test] - fn test_parse_trivia_trailing_test() { - assert_eq_trivia( - trivia("1 + \n1"), - vec![ws(1, 2, Pos::Trailing), ws(3, 4, Pos::Trailing), nl(4, 5)], - ); - } - - #[test] - fn test_parse_trivia_trailing_trivia_test() { - // Trivia after the newline leads the `EOF` token that `TreeSink` adds - assert_eq_trivia( - trivia("1 \n "), - vec![ws(1, 3, Pos::Trailing), nl(3, 4), ws(4, 5, Pos::Leading)], - ); - } - - #[test] - fn test_parse_trivia_trailing_crlf_test() { - assert_eq_trivia( - trivia("1 + \r\n1"), - vec![ws(1, 2, Pos::Trailing), ws(3, 4, Pos::Trailing), nl(4, 6)], - ); - } - - #[test] - fn test_parse_trivia_before_first_token() { - assert_eq_trivia(trivia(" \n1"), vec![ws(0, 2, Pos::Leading), nl(2, 3)]); - } - + /// Air's LSP currently needs this #[test] - fn test_parse_trivia_comment_test() { - assert_eq_trivia( - trivia("1 #"), - vec![ws(1, 2, Pos::Trailing), cmt(2, 3, Pos::Trailing)], - ); + fn parse_is_send() { + fn assert_send() {} + assert_send::(); } #[test] - fn test_parse_trivia_comment_crlf_test() { - // Trailing comment: `1 # hi\r\n` (r-lib/tree-sitter-r#184) - assert_eq_trivia( - trivia("1 # hi\r\n"), - vec![ws(1, 2, Pos::Trailing), cmt(2, 6, Pos::Trailing), nl(6, 8)], - ); + fn parse_empty() { + let parse = parse("", RParserOptions::default()); + assert!(parse.diagnostics().is_empty()); + assert_eq!(parse.syntax().to_string(), ""); - // Own-line comment: `# hi\r\n1` - assert_eq_trivia(trivia("# hi\r\n1"), vec![cmt(0, 4, Pos::Leading), nl(4, 6)]); + let root = parse.tree(); + assert!(root.statements().is_empty()); + assert_eq!(root.eof_token().unwrap().kind(), RSyntaxKind::EOF); } + /// Trivia with no token to trail lands on `EOF` #[test] - fn test_parse_trivia_crlf_leading_and_trailing_comments() { - assert_eq_trivia( - trivia("# Leading\r\n1 + 1 # Trailing\r\n# Leading\r\n"), - vec![ - cmt(0, 9, Pos::Leading), - nl(9, 11), - ws(12, 13, Pos::Trailing), - ws(14, 15, Pos::Trailing), - ws(16, 17, Pos::Trailing), - cmt(17, 27, Pos::Trailing), - nl(27, 29), - cmt(29, 38, Pos::Leading), - nl(38, 40), - ], - ); - } + fn parse_trivia_only() { + let source = " \n# hi\n"; + let parse = parse(source, RParserOptions::default()); + assert!(parse.diagnostics().is_empty()); + assert_eq!(parse.syntax().to_string(), source); - #[test] - fn test_parse_trivia_comment_nothing_else_test() { - assert_eq_trivia(trivia("#"), vec![cmt(0, 1, Pos::Leading)]); + let eof = parse.tree().eof_token().unwrap(); + assert_eq!(eof.leading_trivia().text(), source); + assert_eq!(eof.trailing_trivia().text(), ""); } + /// A BOM is a token of `RRoot`, not trivia #[test] - fn test_parse_trivia_comment_end_of_document_test() { - assert_eq_trivia(trivia("1\n#"), vec![nl(1, 2), cmt(2, 3, Pos::Leading)]); - } + fn parse_bom() { + let source = "\u{feff}"; + let with_bom = parse(source, RParserOptions::default()); + assert!(with_bom.diagnostics().is_empty()); + assert_eq!(with_bom.syntax().to_string(), source); + assert!(with_bom.tree().bom_token().is_some()); - #[test] - fn test_parse_trivia_whitespace_between_comments_test() { - let text = " -1 # -# -2 -" - .trim(); - assert_eq_trivia( - trivia(text), - vec![ - ws(1, 2, Pos::Trailing), - cmt(2, 3, Pos::Trailing), - nl(3, 4), - cmt(4, 5, Pos::Leading), - nl(5, 6), - ], - ); + let without_bom = parse("", RParserOptions::default()); + assert!(without_bom.tree().bom_token().is_none()); } + /// The lexer's diagnostics reach `RParse` through the tree sink #[test] - fn test_parse_trivia_comment_beginning_of_document_test() { - assert_eq_trivia(trivia("#\n1"), vec![cmt(0, 1, Pos::Leading), nl(1, 2)]); - } + fn parse_reports_lexer_diagnostics() { + let source = "`a"; + let parse = parse(source, RParserOptions::default()); + assert_eq!(parse.syntax().to_string(), source); - #[test] - fn test_parse_trivia_comment_beginning_of_document_with_whitespace_test() { - assert_eq_trivia( - trivia(" \n \n#"), - vec![ - ws(0, 1, Pos::Leading), - nl(1, 2), - ws(2, 3, Pos::Leading), - nl(3, 4), - cmt(4, 5, Pos::Leading), - ], + assert!(parse.has_errors()); + assert_eq!(parse.diagnostics().len(), 1); + assert_eq!( + PrintDescription(&parse.diagnostics()[0]).to_string(), + "Unterminated backtick-quoted name." ); } - - #[test] - fn test_parse_smoke_test() { - let (events, trivia, _errors) = parse_text("1+1", RParserOptions::default()); - - let expect = vec![ - Event::Start { - kind: RSyntaxKind::R_ROOT, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_STATEMENT_LIST, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_EXPRESSION_STATEMENT, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_BINARY_EXPRESSION, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_DOUBLE_VALUE, - forward_parent: None, - }, - Event::Token { - kind: RSyntaxKind::R_DOUBLE_LITERAL, - end: TextSize::from(1), - }, - Event::Finish, - Event::Token { - kind: RSyntaxKind::PLUS, - end: TextSize::from(2), - }, - Event::Start { - kind: RSyntaxKind::R_DOUBLE_VALUE, - forward_parent: None, - }, - Event::Token { - kind: RSyntaxKind::R_DOUBLE_LITERAL, - end: TextSize::from(3), - }, - Event::Finish, - Event::Finish, - Event::Finish, - Event::Finish, - Event::Finish, - ]; - - assert_eq_events(events, expect); - assert!(trivia.is_empty()); - } - - #[test] - fn test_parse_function_definition() { - let (events, trivia, _errors) = parse_text("function() 1", RParserOptions::default()); - - let expect = vec![ - Event::Start { - kind: RSyntaxKind::R_ROOT, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_STATEMENT_LIST, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_EXPRESSION_STATEMENT, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_FUNCTION_DEFINITION, - forward_parent: None, - }, - Event::Token { - kind: RSyntaxKind::FUNCTION_KW, - end: TextSize::from(8), - }, - Event::Start { - kind: RSyntaxKind::R_PARAMETERS, - forward_parent: None, - }, - Event::Token { - kind: RSyntaxKind::L_PAREN, - end: TextSize::from(9), - }, - Event::Start { - kind: RSyntaxKind::R_PARAMETER_LIST, - forward_parent: None, - }, - Event::Finish, // R_PARAMETER_LIST - Event::Token { - kind: RSyntaxKind::R_PAREN, - end: TextSize::from(10), - }, - Event::Finish, // R_PARAMETERS - Event::Start { - kind: RSyntaxKind::R_DOUBLE_VALUE, - forward_parent: None, - }, - Event::Token { - kind: RSyntaxKind::R_DOUBLE_LITERAL, - end: TextSize::from(12), - }, - Event::Finish, // R_DOUBLE_VALUE - Event::Finish, // R_FUNCTION_DEFINITION - Event::Finish, // R_EXPRESSION_STATEMENT - Event::Finish, // R_STATEMENT_LIST - Event::Finish, // R_ROOT - ]; - assert_eq_events(events, expect); - - let expect = vec![ws(10, 11, Pos::Trailing)]; - assert_eq_trivia(trivia, expect); - } - - #[test] - fn test_parse_call() { - let (events, trivia, _errors) = parse_text("fn()", RParserOptions::default()); - - let expect = vec![ - Event::Start { - kind: RSyntaxKind::R_ROOT, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_STATEMENT_LIST, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_EXPRESSION_STATEMENT, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_CALL, - forward_parent: None, - }, - Event::Start { - kind: RSyntaxKind::R_IDENTIFIER, - forward_parent: None, - }, - Event::Token { - kind: RSyntaxKind::IDENT, - end: TextSize::from(2), - }, - Event::Finish, // R_IDENTIFIER - Event::Start { - kind: RSyntaxKind::R_CALL_ARGUMENTS, - forward_parent: None, - }, - Event::Token { - kind: RSyntaxKind::L_PAREN, - end: TextSize::from(3), - }, - Event::Start { - kind: RSyntaxKind::R_ARGUMENT_LIST, - forward_parent: None, - }, - Event::Finish, // R_ARGUMENT_LIST - Event::Token { - kind: RSyntaxKind::R_PAREN, - end: TextSize::from(4), - }, - Event::Finish, // R_CALL_ARGUMENTS - Event::Finish, // R_CALL - Event::Finish, // R_EXPRESSION_STATEMENT - Event::Finish, // R_STATEMENT_LIST - Event::Finish, // R_ROOT - ]; - assert_eq_events(events, expect); - - let expect = vec![]; - assert_eq_trivia(trivia, expect); - } } diff --git a/crates/air_r_parser/src/parser.rs b/crates/air_r_parser/src/parser.rs new file mode 100644 index 00000000..5251b226 --- /dev/null +++ b/crates/air_r_parser/src/parser.rs @@ -0,0 +1,118 @@ +use crate::lexer::RReLexContext; +use crate::options::RParserOptions; +use crate::token_source::RTokenSource; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::T; +use biome_parser::ParserContext; +use biome_parser::diagnostic::ParseDiagnostic; +use biome_parser::diagnostic::merge_diagnostics; +use biome_parser::event::Event; +use biome_parser::prelude::*; + +pub(crate) struct RParser<'src> { + context: ParserContext, + source: RTokenSource<'src>, + options: RParserOptions, + scope: Scope, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) enum Scope { + #[default] + TopLevel, + Brace, + Parenthesis, + Bracket, + Bracket2, +} + +impl Scope { + /// A line break ends a statement at top level and inside `{`, and means + /// nothing inside `(`, `[`, or `[[` + pub(crate) fn newlines_are_significant(self) -> bool { + match self { + Self::TopLevel | Self::Brace => true, + Self::Parenthesis | Self::Bracket | Self::Bracket2 => false, + } + } +} + +impl<'src> RParser<'src> { + pub(crate) fn new(source: &'src str, options: RParserOptions) -> Self { + Self { + context: ParserContext::default(), + source: RTokenSource::from_str(source), + options, + scope: Scope::default(), + } + } + + #[expect(dead_code, reason = "`RParserOptions` is still empty")] + pub(crate) fn options(&self) -> &RParserOptions { + &self.options + } + + /// Runs `f` with `scope` in effect, restoring the previous value after + pub(crate) fn with_scope(&mut self, scope: Scope, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + let saved = std::mem::replace(&mut self.scope, scope); + let result = f(self); + self.scope = saved; + result + } + + pub(crate) fn scope(&self) -> Scope { + self.scope + } + + pub(crate) fn re_lex(&mut self, context: RReLexContext) -> RSyntaxKind { + self.source_mut().re_lex(context) + } + + pub(crate) fn finish(self) -> (Vec>, Vec, Vec) { + let (trivia, lexer_diagnostics) = self.source.finish(); + let (events, parse_diagnostics) = self.context.finish(); + let diagnostics = merge_diagnostics(lexer_diagnostics, parse_diagnostics); + (events, trivia, diagnostics) + } +} + +impl<'src> Parser for RParser<'src> { + type Kind = RSyntaxKind; + type Source = RTokenSource<'src>; + + fn context(&self) -> &ParserContext { + &self.context + } + + fn context_mut(&mut self) -> &mut ParserContext { + &mut self.context + } + + fn source(&self) -> &Self::Source { + &self.source + } + + fn source_mut(&mut self) -> &mut Self::Source { + &mut self.source + } + + /// Overridden so that every bump checks for a potential `]]` re-lex + fn do_bump(&mut self, kind: Self::Kind) { + let end = self.cur_range().end(); + self.context_mut().push_token(kind, end); + + if self.context().is_skipping() { + self.source_mut().skip_as_trivia(); + } else { + self.source_mut().bump(); + } + + // Joins an adjacent `] ]` into the `]]` the lexer never emits + if self.scope == Scope::Bracket2 && self.cur() == T![']'] { + self.re_lex(RReLexContext::JoinBrackets); + } + } +} diff --git a/crates/air_r_parser/src/syntax.rs b/crates/air_r_parser/src/syntax.rs new file mode 100644 index 00000000..fa45bcdb --- /dev/null +++ b/crates/air_r_parser/src/syntax.rs @@ -0,0 +1,20 @@ +mod associativity; +mod binary; +mod binding_power; +mod body; +mod braced_statements; +mod call; +mod expr; +mod extract; +mod function; +mod if_statement; +mod literal; +mod loop_statements; +mod namespace; +mod parenthesized_expression; +mod root; +mod selector; +mod statement_list; +mod unary; + +pub(crate) use crate::syntax::root::parse_root; diff --git a/crates/air_r_parser/src/syntax/associativity.rs b/crates/air_r_parser/src/syntax/associativity.rs new file mode 100644 index 00000000..00517adb --- /dev/null +++ b/crates/air_r_parser/src/syntax/associativity.rs @@ -0,0 +1,22 @@ +/// Binary operator associativity, taken from `gram.y` +#[derive(Debug, Clone, Copy)] +pub(crate) enum Associativity { + Left, + Right, +} + +impl Associativity { + pub(crate) const BINARY_HELP: Self = Self::Left; + pub(crate) const BINARY_LEFT_ASSIGN: Self = Self::Right; + pub(crate) const BINARY_EQUALS_ASSIGN: Self = Self::Right; + pub(crate) const BINARY_RIGHT_ASSIGN: Self = Self::Left; + pub(crate) const BINARY_TILDE: Self = Self::Left; + pub(crate) const BINARY_OR: Self = Self::Left; + pub(crate) const BINARY_AND: Self = Self::Left; + pub(crate) const BINARY_COMPARISON: Self = Self::Left; + pub(crate) const BINARY_PLUS_MINUS: Self = Self::Left; + pub(crate) const BINARY_MULTIPLY_DIVIDE: Self = Self::Left; + pub(crate) const BINARY_SPECIAL_OR_PIPE: Self = Self::Left; + pub(crate) const BINARY_COLON: Self = Self::Left; + pub(crate) const BINARY_EXPONENTIATE: Self = Self::Right; +} diff --git a/crates/air_r_parser/src/syntax/binary.rs b/crates/air_r_parser/src/syntax/binary.rs new file mode 100644 index 00000000..39067d38 --- /dev/null +++ b/crates/air_r_parser/src/syntax/binary.rs @@ -0,0 +1,27 @@ +use crate::diagnostics; +use crate::parser::RParser; +use crate::syntax::associativity::Associativity; +use crate::syntax::binding_power::BindingPower; +use crate::syntax::expr::parse_expression_with_binding_power; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxKind::R_BINARY_EXPRESSION; +use biome_parser::CompletedMarker; +use biome_parser::Parser; + +pub(crate) fn parse_binary_expression( + p: &mut RParser, + lhs: CompletedMarker, + kind: RSyntaxKind, + binding_power: BindingPower, + associativity: Associativity, +) -> CompletedMarker { + let binding_power = match associativity { + Associativity::Left => binding_power.increment(), + Associativity::Right => binding_power, + }; + let m = lhs.precede(p); + p.bump(kind); + parse_expression_with_binding_power(p, binding_power) + .or_add_diagnostic(p, diagnostics::expected_expression); + m.complete(p, R_BINARY_EXPRESSION) +} diff --git a/crates/air_r_parser/src/syntax/binding_power.rs b/crates/air_r_parser/src/syntax/binding_power.rs new file mode 100644 index 00000000..c84aa4f9 --- /dev/null +++ b/crates/air_r_parser/src/syntax/binding_power.rs @@ -0,0 +1,34 @@ +/// How tightly an operator binds. Higher wins! +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct BindingPower(u8); + +impl BindingPower { + pub(crate) const LOWEST: Self = Self(0); + + // Taken from R's `gram.y` + pub(crate) const BINARY_HELP: Self = Self(1); + pub(crate) const UNARY_HELP: Self = Self(1); + pub(crate) const FUNCTION_OR_LOOP_OR_IF: Self = Self(2); + pub(crate) const BINARY_LEFT_ASSIGN: Self = Self(3); + pub(crate) const BINARY_EQUALS_ASSIGN: Self = Self(4); + pub(crate) const BINARY_RIGHT_ASSIGN: Self = Self(5); + pub(crate) const BINARY_TILDE: Self = Self(6); + pub(crate) const UNARY_TILDE: Self = Self(6); + pub(crate) const BINARY_OR: Self = Self(7); + pub(crate) const BINARY_AND: Self = Self(8); + pub(crate) const UNARY_NOT: Self = Self(9); + pub(crate) const BINARY_COMPARISON: Self = Self(10); + pub(crate) const BINARY_PLUS_MINUS: Self = Self(11); + pub(crate) const BINARY_MULTIPLY_DIVIDE: Self = Self(12); + pub(crate) const BINARY_SPECIAL_OR_PIPE: Self = Self(13); + pub(crate) const BINARY_COLON: Self = Self(14); + pub(crate) const UNARY_PLUS_MINUS: Self = Self(15); + pub(crate) const BINARY_EXPONENTIATE: Self = Self(16); + + pub(crate) const HIGHEST: Self = Self(17); + + /// Used by left associative constructs + pub(crate) fn increment(self) -> Self { + Self(self.0 + 1) + } +} diff --git a/crates/air_r_parser/src/syntax/body.rs b/crates/air_r_parser/src/syntax/body.rs new file mode 100644 index 00000000..26076bf1 --- /dev/null +++ b/crates/air_r_parser/src/syntax/body.rs @@ -0,0 +1,15 @@ +use crate::parser::RParser; +use crate::syntax::binding_power::BindingPower; +use crate::syntax::expr::parse_expression_with_binding_power; +use biome_parser::parsed_syntax::ParsedSyntax; + +/// Parse a function, loop, or if body +/// +/// Generally treated like a normal expression, but R's precedence table puts +/// `?` at a lower binding power than a function, loop, or if body, so `function() a ? b` +/// needs to parse as `(function () a) ? b`. +pub(crate) fn parse_body_expression(p: &mut RParser) -> ParsedSyntax { + // Treated as left associative + let binding_power = BindingPower::FUNCTION_OR_LOOP_OR_IF.increment(); + parse_expression_with_binding_power(p, binding_power) +} diff --git a/crates/air_r_parser/src/syntax/braced_statements.rs b/crates/air_r_parser/src/syntax/braced_statements.rs new file mode 100644 index 00000000..fcb48229 --- /dev/null +++ b/crates/air_r_parser/src/syntax/braced_statements.rs @@ -0,0 +1,17 @@ +use crate::parser::RParser; +use crate::parser::Scope; +use crate::syntax::statement_list::parse_statement_list; +use air_r_syntax::RSyntaxKind::R_BRACED_STATEMENTS; +use air_r_syntax::T; +use biome_parser::CompletedMarker; +use biome_parser::Parser; + +pub(crate) fn parse_braced_statements(p: &mut RParser) -> CompletedMarker { + let m = p.start(); + p.with_scope(Scope::Brace, |p| { + p.bump(T!['{']); + parse_statement_list(p, T!['}']); + }); + p.expect(T!['}']); + m.complete(p, R_BRACED_STATEMENTS) +} diff --git a/crates/air_r_parser/src/syntax/call.rs b/crates/air_r_parser/src/syntax/call.rs new file mode 100644 index 00000000..eef38fb0 --- /dev/null +++ b/crates/air_r_parser/src/syntax/call.rs @@ -0,0 +1,199 @@ +use crate::diagnostics; +use crate::parser::RParser; +use crate::parser::Scope; +use crate::syntax::expr::parse_expression; +use crate::syntax::literal::parse_literal_expression; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxKind::DOTDOTI; +use air_r_syntax::RSyntaxKind::DOTS; +use air_r_syntax::RSyntaxKind::IDENT; +use air_r_syntax::RSyntaxKind::R_ARGUMENT; +use air_r_syntax::RSyntaxKind::R_ARGUMENT_LIST; +use air_r_syntax::RSyntaxKind::R_ARGUMENT_NAME_CLAUSE; +use air_r_syntax::RSyntaxKind::R_BOGUS_EXPRESSION; +use air_r_syntax::RSyntaxKind::R_CALL; +use air_r_syntax::RSyntaxKind::R_CALL_ARGUMENTS; +use air_r_syntax::RSyntaxKind::R_DOT_DOT_I; +use air_r_syntax::RSyntaxKind::R_DOTS; +use air_r_syntax::RSyntaxKind::R_DOUBLE_QUOTED_STRING_LITERAL; +use air_r_syntax::RSyntaxKind::R_IDENTIFIER; +use air_r_syntax::RSyntaxKind::R_NULL_EXPRESSION; +use air_r_syntax::RSyntaxKind::R_RAW_STRING_LITERAL; +use air_r_syntax::RSyntaxKind::R_SINGLE_QUOTED_STRING_LITERAL; +use air_r_syntax::RSyntaxKind::R_STRING_VALUE; +use air_r_syntax::RSyntaxKind::R_SUBSET; +use air_r_syntax::RSyntaxKind::R_SUBSET_ARGUMENTS; +use air_r_syntax::RSyntaxKind::R_SUBSET2; +use air_r_syntax::RSyntaxKind::R_SUBSET2_ARGUMENTS; +use air_r_syntax::T; +use biome_parser::CompletedMarker; +use biome_parser::Parser; +use biome_parser::ParserProgress; +use biome_parser::TokenSet; +use biome_parser::parse_recovery::ParseRecoveryTokenSet; +use biome_parser::token_set; + +pub(crate) fn parse_call_expression(p: &mut RParser, lhs: CompletedMarker) -> CompletedMarker { + let m = lhs.precede(p); + + let arguments = p.start(); + p.with_scope(Scope::Parenthesis, |p| { + p.bump(T!['(']); + parse_argument_list(p); + }); + p.expect(T![')']); + arguments.complete(p, R_CALL_ARGUMENTS); + + m.complete(p, R_CALL) +} + +pub(crate) fn parse_subset_expression(p: &mut RParser, lhs: CompletedMarker) -> CompletedMarker { + let m = lhs.precede(p); + + let arguments = p.start(); + p.with_scope(Scope::Bracket, |p| { + p.bump(T!['[']); + parse_argument_list(p); + }); + p.expect(T![']']); + arguments.complete(p, R_SUBSET_ARGUMENTS); + + m.complete(p, R_SUBSET) +} + +pub(crate) fn parse_subset2_expression(p: &mut RParser, lhs: CompletedMarker) -> CompletedMarker { + let m = lhs.precede(p); + + let arguments = p.start(); + p.with_scope(Scope::Bracket2, |p| { + // Purposefully bump inside `with_scope()`. Bumping lexes the next + // token after this one, and the context for how two back to back `]` + // `]` are lexed depends on whether we are in a Bracket2 scope or not. + // If we bump before `with_scope()` then an empty `x[[]]` parses + // incorrectly! + p.bump(T!["[["]); + parse_argument_list(p); + }); + // Similarly, we `expect()` (bump + assert) outside the scope so that a + // potential future `]` can't be affected by a now closed Bracket2 scope + p.expect(T!["]]"]); + arguments.complete(p, R_SUBSET2_ARGUMENTS); + + m.complete(p, R_SUBSET2) +} + +/// What ends an argument list +/// +/// Any closing delimiter, not just the one the caller opened. We don't consume +/// it, we return from here. The caller's `expect()` on the closing delimiter +/// reports a diagnostic, which still doesn't consume, and the delimiter +/// survives (ideally to be consumed by its actual owner). +const ARGUMENT_LIST_ENDS: TokenSet = + token_set![T![EOF], T![')'], T![']'], T!["]]"], T!['}']]; + +/// Where an argument may stop: at the `,` that starts the next one, or at an end +const ARGUMENT_LIST_BOUNDARIES: TokenSet = ARGUMENT_LIST_ENDS.union(token_set![T![,]]); + +fn parse_argument_list(p: &mut RParser) { + let argument_list = p.start(); + let mut progress = ParserProgress::default(); + let mut first = true; + + while !p.at_ts(ARGUMENT_LIST_ENDS) { + if first { + first = false; + } else { + // Either a `,` that introduces the next argument, or a missing one + // that we report without consuming and let the next argument start + // where we already are + p.expect(T![,]); + } + + progress.assert_progressing(p); + parse_argument(p); + } + + argument_list.complete(p, R_ARGUMENT_LIST); +} + +/// One argument: `[name =] [value]`, both halves optional +/// +/// An empty argument opens and immediately closes its marker, pushing no +/// children. That is the technique `r.ungram` documents, and it is what makes +/// `f(,,a,,b,,)` representable. +fn parse_argument(p: &mut RParser) { + let m = p.start(); + + // Optional name clause + parse_argument_name_clause(p); + + // Optional value + parse_argument_value(p); + + m.complete(p, R_ARGUMENT); +} + +/// Value can be missing as long as the next token is a boundary, like +/// `f(a,)` where the next token is `)` or `f(a,,)` where the next +/// token is `,`, but not with `f(* x)` where `* x` simply isn't an expression. +/// That should be a parse error! +fn parse_argument_value(p: &mut RParser) { + if p.at_ts(ARGUMENT_LIST_BOUNDARIES) { + return; + } + + parse_expression(p) + .or_recover_with_token_set( + p, + &ParseRecoveryTokenSet::new(R_BOGUS_EXPRESSION, ARGUMENT_LIST_BOUNDARIES), + diagnostics::expected_expression, + ) + .ok(); +} + +/// `name =`, where the `name` is an identifier, a string, `...`, `..i`, or `NULL` +/// +/// `foo` and `foo =` start the same way, so one token of lookahead is what tells +/// a name from the head of a value +/// +/// `NULL` is accepted here and nowhere else, and has been since the +/// beginning of R (r-lib/tree-sitter-r#164). +fn parse_argument_name_clause(p: &mut RParser) { + match p.cur() { + IDENT if p.nth_at(1, T![=]) => { + let clause = p.start(); + parse_literal_expression(p, IDENT, R_IDENTIFIER); + p.bump(T![=]); + clause.complete(p, R_ARGUMENT_NAME_CLAUSE); + } + DOTS if p.nth_at(1, T![=]) => { + let clause = p.start(); + parse_literal_expression(p, DOTS, R_DOTS); + p.bump(T![=]); + clause.complete(p, R_ARGUMENT_NAME_CLAUSE); + } + DOTDOTI if p.nth_at(1, T![=]) => { + let clause = p.start(); + parse_literal_expression(p, DOTDOTI, R_DOT_DOT_I); + p.bump(T![=]); + clause.complete(p, R_ARGUMENT_NAME_CLAUSE); + } + T![null] if p.nth_at(1, T![=]) => { + let clause = p.start(); + parse_literal_expression(p, T![null], R_NULL_EXPRESSION); + p.bump(T![=]); + clause.complete(p, R_ARGUMENT_NAME_CLAUSE); + } + kind @ (R_SINGLE_QUOTED_STRING_LITERAL + | R_DOUBLE_QUOTED_STRING_LITERAL + | R_RAW_STRING_LITERAL) + if p.nth_at(1, T![=]) => + { + let clause = p.start(); + parse_literal_expression(p, kind, R_STRING_VALUE); + p.bump(T![=]); + clause.complete(p, R_ARGUMENT_NAME_CLAUSE); + } + _ => {} + } +} diff --git a/crates/air_r_parser/src/syntax/expr.rs b/crates/air_r_parser/src/syntax/expr.rs new file mode 100644 index 00000000..0a563ac7 --- /dev/null +++ b/crates/air_r_parser/src/syntax/expr.rs @@ -0,0 +1,239 @@ +//! Pratt parser core +//! +//! https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html +//! https://kevinushey.github.io/blog/2016/02/12/top-down-operator-precedence-parsing-with-r/ + +use crate::parser::RParser; +use crate::syntax::associativity::Associativity; +use crate::syntax::binary::parse_binary_expression; +use crate::syntax::binding_power::BindingPower; +use crate::syntax::braced_statements::parse_braced_statements; +use crate::syntax::call::parse_call_expression; +use crate::syntax::call::parse_subset_expression; +use crate::syntax::call::parse_subset2_expression; +use crate::syntax::extract::parse_extract_expression; +use crate::syntax::function::parse_function_definition; +use crate::syntax::if_statement::parse_if_statement; +use crate::syntax::literal::parse_literal_expression; +use crate::syntax::loop_statements::parse_for_statement; +use crate::syntax::loop_statements::parse_repeat_statement; +use crate::syntax::loop_statements::parse_while_statement; +use crate::syntax::namespace::parse_namespace_expression; +use crate::syntax::parenthesized_expression::parse_parenthesized_expression; +use crate::syntax::unary::parse_unary_expression; +use air_r_syntax::RSyntaxKind::*; +use air_r_syntax::T; +use biome_parser::Parser; +use biome_parser::ParserProgress; +use biome_parser::parsed_syntax::ParsedSyntax; +use biome_parser::parsed_syntax::ParsedSyntax::Absent; +use biome_parser::parsed_syntax::ParsedSyntax::Present; + +pub(crate) fn parse_expression(p: &mut RParser) -> ParsedSyntax { + parse_expression_with_binding_power(p, BindingPower::LOWEST) +} + +/// The Pratt loop +pub(crate) fn parse_expression_with_binding_power( + p: &mut RParser, + current_binding_power: BindingPower, +) -> ParsedSyntax { + debug_assert!( + current_binding_power <= BindingPower::HIGHEST, + "{current_binding_power:?} must not exceed {:?}.", + BindingPower::HIGHEST + ); + + let mut lhs = match p.cur() { + kind @ T![?] => parse_unary_expression(p, kind, BindingPower::UNARY_HELP), + kind @ T![~] => parse_unary_expression(p, kind, BindingPower::UNARY_TILDE), + kind @ T![!] => parse_unary_expression(p, kind, BindingPower::UNARY_NOT), + kind @ (T![+] | T![-]) => parse_unary_expression(p, kind, BindingPower::UNARY_PLUS_MINUS), + R_INTEGER_LITERAL => parse_literal_expression(p, R_INTEGER_LITERAL, R_INTEGER_VALUE), + R_DOUBLE_LITERAL => parse_literal_expression(p, R_DOUBLE_LITERAL, R_DOUBLE_VALUE), + R_COMPLEX_LITERAL => parse_literal_expression(p, R_COMPLEX_LITERAL, R_COMPLEX_VALUE), + IDENT => parse_literal_expression(p, IDENT, R_IDENTIFIER), + DOTS => parse_literal_expression(p, DOTS, R_DOTS), + DOTDOTI => parse_literal_expression(p, DOTDOTI, R_DOT_DOT_I), + T![next] => parse_literal_expression(p, T![next], R_NEXT_EXPRESSION), + T![break] => parse_literal_expression(p, T![break], R_BREAK_EXPRESSION), + T![true] => parse_literal_expression(p, T![true], R_TRUE_EXPRESSION), + T![false] => parse_literal_expression(p, T![false], R_FALSE_EXPRESSION), + T![null] => parse_literal_expression(p, T![null], R_NULL_EXPRESSION), + T![inf] => parse_literal_expression(p, T![inf], R_INF_EXPRESSION), + T![nan] => parse_literal_expression(p, T![nan], R_NAN_EXPRESSION), + kind @ (T![na_logical] + | T![na_integer] + | T![na_double] + | T![na_complex] + | T![na_character]) => parse_literal_expression(p, kind, R_NA_EXPRESSION), + // Every ERROR_TOKEN already carries a lexer diagnostic, from + // `unexpected_character()`, `unterminated_string()`, or + // `unterminated_backtick_identifier()` + ERROR_TOKEN => parse_literal_expression(p, ERROR_TOKEN, R_BOGUS_EXPRESSION), + kind @ (R_SINGLE_QUOTED_STRING_LITERAL + | R_DOUBLE_QUOTED_STRING_LITERAL + | R_RAW_STRING_LITERAL) => parse_literal_expression(p, kind, R_STRING_VALUE), + kind @ (T![function] | T!["\\"]) => parse_function_definition(p, kind), + T![if] => parse_if_statement(p), + T![for] => parse_for_statement(p), + T![while] => parse_while_statement(p), + T![repeat] => parse_repeat_statement(p), + T!['('] => parse_parenthesized_expression(p), + T!['{'] => parse_braced_statements(p), + // Doesn't start an expression + _ => return Absent, + }; + + let mut progress = ParserProgress::default(); + + loop { + progress.assert_progressing(p); + + if p.scope().newlines_are_significant() && p.has_preceding_line_break() { + break; + } + + lhs = match p.cur() { + T!['('] => parse_call_expression(p, lhs), + T!['['] => parse_subset_expression(p, lhs), + T!["[["] => parse_subset2_expression(p, lhs), + kind @ (T![$] | T![@]) => parse_extract_expression(p, lhs, kind), + kind @ (T![::] | T![:::]) => parse_namespace_expression(p, lhs, kind), + kind @ T![?] if BindingPower::BINARY_HELP >= current_binding_power => { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_HELP, + Associativity::BINARY_HELP, + ) + } + kind @ (T![<-] | T![<<-] | T![:=]) + if BindingPower::BINARY_LEFT_ASSIGN >= current_binding_power => + { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_LEFT_ASSIGN, + Associativity::BINARY_LEFT_ASSIGN, + ) + } + kind @ T![=] if BindingPower::BINARY_EQUALS_ASSIGN >= current_binding_power => { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_EQUALS_ASSIGN, + Associativity::BINARY_EQUALS_ASSIGN, + ) + } + kind @ (T![->] | T![->>]) + if BindingPower::BINARY_RIGHT_ASSIGN >= current_binding_power => + { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_RIGHT_ASSIGN, + Associativity::BINARY_RIGHT_ASSIGN, + ) + } + kind @ T![~] if BindingPower::BINARY_TILDE >= current_binding_power => { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_TILDE, + Associativity::BINARY_TILDE, + ) + } + kind @ (T![|] | T![||]) if BindingPower::BINARY_OR >= current_binding_power => { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_OR, + Associativity::BINARY_OR, + ) + } + kind @ (T![&] | T![&&]) if BindingPower::BINARY_AND >= current_binding_power => { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_AND, + Associativity::BINARY_AND, + ) + } + kind @ (T![<] | T![<=] | T![>] | T![>=] | T![==] | T![!=]) + if BindingPower::BINARY_COMPARISON >= current_binding_power => + { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_COMPARISON, + Associativity::BINARY_COMPARISON, + ) + } + kind @ (T![+] | T![-]) if BindingPower::BINARY_PLUS_MINUS >= current_binding_power => { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_PLUS_MINUS, + Associativity::BINARY_PLUS_MINUS, + ) + } + kind @ (T![*] | T![/]) + if BindingPower::BINARY_MULTIPLY_DIVIDE >= current_binding_power => + { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_MULTIPLY_DIVIDE, + Associativity::BINARY_MULTIPLY_DIVIDE, + ) + } + kind @ (SPECIAL | T![|>]) + if BindingPower::BINARY_SPECIAL_OR_PIPE >= current_binding_power => + { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_SPECIAL_OR_PIPE, + Associativity::BINARY_SPECIAL_OR_PIPE, + ) + } + kind @ T![:] if BindingPower::BINARY_COLON >= current_binding_power => { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_COLON, + Associativity::BINARY_COLON, + ) + } + kind @ (T![^] | T![**]) + if BindingPower::BINARY_EXPONENTIATE >= current_binding_power => + { + parse_binary_expression( + p, + lhs, + kind, + BindingPower::BINARY_EXPONENTIATE, + Associativity::BINARY_EXPONENTIATE, + ) + } + // No expression to continue, or upcoming one binds looser than + // current binding power + _ => break, + }; + } + + Present(lhs) +} diff --git a/crates/air_r_parser/src/syntax/extract.rs b/crates/air_r_parser/src/syntax/extract.rs new file mode 100644 index 00000000..0631c731 --- /dev/null +++ b/crates/air_r_parser/src/syntax/extract.rs @@ -0,0 +1,21 @@ +use crate::diagnostics; +use crate::parser::RParser; +use crate::syntax::selector::parse_selector; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxKind::R_EXTRACT_EXPRESSION; +use biome_parser::CompletedMarker; +use biome_parser::Parser; + +pub(crate) fn parse_extract_expression( + p: &mut RParser, + lhs: CompletedMarker, + kind: RSyntaxKind, +) -> CompletedMarker { + let kind_range = p.cur_range(); + let m = lhs.precede(p); + p.bump(kind); + parse_selector(p).or_add_diagnostic(p, |_, range| { + diagnostics::expected_rhs_selector(kind, kind_range, range) + }); + m.complete(p, R_EXTRACT_EXPRESSION) +} diff --git a/crates/air_r_parser/src/syntax/function.rs b/crates/air_r_parser/src/syntax/function.rs new file mode 100644 index 00000000..f42c48e9 --- /dev/null +++ b/crates/air_r_parser/src/syntax/function.rs @@ -0,0 +1,125 @@ +use crate::diagnostics; +use crate::parser::RParser; +use crate::parser::Scope; +use crate::syntax::body::parse_body_expression; +use crate::syntax::expr::parse_expression; +use crate::syntax::literal::parse_literal_expression; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxKind::DOTDOTI; +use air_r_syntax::RSyntaxKind::DOTS; +use air_r_syntax::RSyntaxKind::IDENT; +use air_r_syntax::RSyntaxKind::R_BOGUS_PARAMETER_NAME; +use air_r_syntax::RSyntaxKind::R_DOT_DOT_I; +use air_r_syntax::RSyntaxKind::R_DOTS; +use air_r_syntax::RSyntaxKind::R_FUNCTION_DEFINITION; +use air_r_syntax::RSyntaxKind::R_IDENTIFIER; +use air_r_syntax::RSyntaxKind::R_PARAMETER; +use air_r_syntax::RSyntaxKind::R_PARAMETER_DEFAULT; +use air_r_syntax::RSyntaxKind::R_PARAMETER_LIST; +use air_r_syntax::RSyntaxKind::R_PARAMETERS; +use air_r_syntax::T; +use biome_parser::CompletedMarker; +use biome_parser::Parser; +use biome_parser::ParserProgress; +use biome_parser::TokenSet; +use biome_parser::parse_recovery::ParseRecoveryTokenSet; +use biome_parser::parsed_syntax::ParsedSyntax; +use biome_parser::parsed_syntax::ParsedSyntax::Absent; +use biome_parser::parsed_syntax::ParsedSyntax::Present; +use biome_parser::token_set; + +pub(crate) fn parse_function_definition(p: &mut RParser, kind: RSyntaxKind) -> CompletedMarker { + let m = p.start(); + p.bump(kind); + + if p.at(T!['(']) { + parse_parameters(p); + } else { + p.error(diagnostics::expected_parameters(p, p.cur_range())); + } + + parse_body_expression(p).or_add_diagnostic(p, diagnostics::expected_function_body); + + m.complete(p, R_FUNCTION_DEFINITION) +} + +fn parse_parameters(p: &mut RParser) { + let m = p.start(); + p.with_scope(Scope::Parenthesis, |p| { + p.bump(T!['(']); + parse_parameter_list(p); + }); + p.expect(T![')']); + m.complete(p, R_PARAMETERS); +} + +/// What ends a parameter list +/// +/// Any closing delimiter, not just the one the caller opened, for the reason +/// [crate::syntax::call]'s `ARGUMENT_LIST_ENDS` gives. +const PARAMETER_LIST_ENDS: TokenSet = + token_set![T![EOF], T![')'], T![']'], T!["]]"], T!['}']]; + +/// Where a parameter may stop: at the `=` of its own default, at the `,` that +/// starts the next one, or at an end +const PARAMETER_LIST_BOUNDARIES: TokenSet = + PARAMETER_LIST_ENDS.union(token_set![T![,], T![=]]); + +fn parse_parameter_list(p: &mut RParser) { + let parameter_list = p.start(); + let mut progress = ParserProgress::default(); + let mut first = true; + + while !p.at_ts(PARAMETER_LIST_ENDS) { + if first { + first = false; + } else { + // Either a `,` that introduces the next parameter, or a missing one + // that we report without consuming and let the next parameter start + // where we already are + p.expect(T![,]); + } + + progress.assert_progressing(p); + parse_parameter(p); + } + + parameter_list.complete(p, R_PARAMETER_LIST); +} + +/// One parameter: `name [= default]` +fn parse_parameter(p: &mut RParser) { + let m = p.start(); + + parse_parameter_name(p) + .or_recover_with_token_set( + p, + &ParseRecoveryTokenSet::new(R_BOGUS_PARAMETER_NAME, PARAMETER_LIST_BOUNDARIES), + diagnostics::expected_parameter_name, + ) + .ok(); + + if p.at(T![=]) { + parse_parameter_default(p); + } + + m.complete(p, R_PARAMETER); +} + +/// identifier | `...` | `..i` +fn parse_parameter_name(p: &mut RParser) -> ParsedSyntax { + match p.cur() { + IDENT => Present(parse_literal_expression(p, IDENT, R_IDENTIFIER)), + DOTS => Present(parse_literal_expression(p, DOTS, R_DOTS)), + DOTDOTI => Present(parse_literal_expression(p, DOTDOTI, R_DOT_DOT_I)), + _ => Absent, + } +} + +/// `= value`, where the value is required (r-lib/tree-sitter-r#161) +fn parse_parameter_default(p: &mut RParser) { + let m = p.start(); + p.bump(T![=]); + parse_expression(p).or_add_diagnostic(p, diagnostics::expected_expression); + m.complete(p, R_PARAMETER_DEFAULT); +} diff --git a/crates/air_r_parser/src/syntax/if_statement.rs b/crates/air_r_parser/src/syntax/if_statement.rs new file mode 100644 index 00000000..99af961c --- /dev/null +++ b/crates/air_r_parser/src/syntax/if_statement.rs @@ -0,0 +1,46 @@ +use crate::diagnostics; +use crate::parser::RParser; +use crate::parser::Scope; +use crate::syntax::body::parse_body_expression; +use crate::syntax::expr::parse_expression; +use air_r_syntax::RSyntaxKind::R_ELSE_CLAUSE; +use air_r_syntax::RSyntaxKind::R_IF_STATEMENT; +use air_r_syntax::T; +use biome_parser::CompletedMarker; +use biome_parser::Parser; + +pub(crate) fn parse_if_statement(p: &mut RParser) -> CompletedMarker { + let m = p.start(); + p.bump(T![if]); + + p.with_scope(Scope::Parenthesis, |p| { + p.expect(T!['(']); + parse_expression(p).or_add_diagnostic(p, diagnostics::expected_condition); + }); + p.expect(T![')']); + + // The consequence and the alternative have the same limitation as a loop + // body: treated like any other expression, but `?` comes before them in the + // precedence table. In gram.y a loop body is left associative and an `if` + // body is right associative one rank up, but they shake out the same + parse_body_expression(p).or_add_diagnostic(p, diagnostics::expected_if_body); + parse_else_clause(p); + + m.complete(p, R_IF_STATEMENT) +} + +fn parse_else_clause(p: &mut RParser) { + if !p.at(T![else]) { + return; + } + + // A newline before `else` ends the `if` at top level and nowhere else + if p.scope() == Scope::TopLevel && p.has_preceding_line_break() { + return; + } + + let m = p.start(); + p.bump(T![else]); + parse_body_expression(p).or_add_diagnostic(p, diagnostics::expected_else_body); + m.complete(p, R_ELSE_CLAUSE); +} diff --git a/crates/air_r_parser/src/syntax/literal.rs b/crates/air_r_parser/src/syntax/literal.rs new file mode 100644 index 00000000..30dd231b --- /dev/null +++ b/crates/air_r_parser/src/syntax/literal.rs @@ -0,0 +1,15 @@ +use crate::parser::RParser; +use air_r_syntax::RSyntaxKind; +use biome_parser::CompletedMarker; +use biome_parser::Parser; + +/// Bumps a single token and wraps it in `kind_complete` +pub(crate) fn parse_literal_expression( + p: &mut RParser, + kind_bump: RSyntaxKind, + kind_complete: RSyntaxKind, +) -> CompletedMarker { + let m = p.start(); + p.bump(kind_bump); + m.complete(p, kind_complete) +} diff --git a/crates/air_r_parser/src/syntax/loop_statements.rs b/crates/air_r_parser/src/syntax/loop_statements.rs new file mode 100644 index 00000000..91570c52 --- /dev/null +++ b/crates/air_r_parser/src/syntax/loop_statements.rs @@ -0,0 +1,90 @@ +use crate::diagnostics; +use crate::parser::RParser; +use crate::parser::Scope; +use crate::syntax::body::parse_body_expression; +use crate::syntax::expr::parse_expression; +use crate::syntax::literal::parse_literal_expression; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxKind::DOTDOTI; +use air_r_syntax::RSyntaxKind::DOTS; +use air_r_syntax::RSyntaxKind::IDENT; +use air_r_syntax::RSyntaxKind::R_BOGUS_FOR_VARIABLE; +use air_r_syntax::RSyntaxKind::R_DOT_DOT_I; +use air_r_syntax::RSyntaxKind::R_DOTS; +use air_r_syntax::RSyntaxKind::R_FOR_STATEMENT; +use air_r_syntax::RSyntaxKind::R_IDENTIFIER; +use air_r_syntax::RSyntaxKind::R_REPEAT_STATEMENT; +use air_r_syntax::RSyntaxKind::R_WHILE_STATEMENT; +use air_r_syntax::T; +use biome_parser::CompletedMarker; +use biome_parser::Parser; +use biome_parser::TokenSet; +use biome_parser::parse_recovery::ParseRecoveryTokenSet; +use biome_parser::parsed_syntax::ParsedSyntax; +use biome_parser::parsed_syntax::ParsedSyntax::Absent; +use biome_parser::parsed_syntax::ParsedSyntax::Present; +use biome_parser::token_set; + +/// Where a `for` variable may stop: at the `in` that follows it, or at an end +/// +/// Any closing delimiter, not just the one the caller opened, for the reason +/// [crate::syntax::call]'s `ARGUMENT_LIST_ENDS` gives. +const FOR_VARIABLE_ENDS: TokenSet = + token_set![T![EOF], T![in], T![')'], T![']'], T!["]]"], T!['}']]; + +pub(crate) fn parse_for_statement(p: &mut RParser) -> CompletedMarker { + let m = p.start(); + p.bump(T![for]); + + p.with_scope(Scope::Parenthesis, |p| { + p.expect(T!['(']); + parse_for_variable(p) + .or_recover_with_token_set( + p, + &ParseRecoveryTokenSet::new(R_BOGUS_FOR_VARIABLE, FOR_VARIABLE_ENDS), + diagnostics::expected_for_variable, + ) + .ok(); + p.expect(T![in]); + parse_expression(p).or_add_diagnostic(p, diagnostics::expected_for_sequence); + }); + p.expect(T![')']); + + parse_body_expression(p).or_add_diagnostic(p, diagnostics::expected_loop_body); + + m.complete(p, R_FOR_STATEMENT) +} + +/// identifier | `...` | `..i` +fn parse_for_variable(p: &mut RParser) -> ParsedSyntax { + match p.cur() { + IDENT => Present(parse_literal_expression(p, IDENT, R_IDENTIFIER)), + DOTS => Present(parse_literal_expression(p, DOTS, R_DOTS)), + DOTDOTI => Present(parse_literal_expression(p, DOTDOTI, R_DOT_DOT_I)), + _ => Absent, + } +} + +pub(crate) fn parse_while_statement(p: &mut RParser) -> CompletedMarker { + let m = p.start(); + p.bump(T![while]); + + p.with_scope(Scope::Parenthesis, |p| { + p.expect(T!['(']); + parse_expression(p).or_add_diagnostic(p, diagnostics::expected_condition); + }); + p.expect(T![')']); + + parse_body_expression(p).or_add_diagnostic(p, diagnostics::expected_loop_body); + + m.complete(p, R_WHILE_STATEMENT) +} + +pub(crate) fn parse_repeat_statement(p: &mut RParser) -> CompletedMarker { + let m = p.start(); + p.bump(T![repeat]); + + parse_body_expression(p).or_add_diagnostic(p, diagnostics::expected_loop_body); + + m.complete(p, R_REPEAT_STATEMENT) +} diff --git a/crates/air_r_parser/src/syntax/namespace.rs b/crates/air_r_parser/src/syntax/namespace.rs new file mode 100644 index 00000000..4e9d34f3 --- /dev/null +++ b/crates/air_r_parser/src/syntax/namespace.rs @@ -0,0 +1,43 @@ +use crate::diagnostics; +use crate::parser::RParser; +use crate::syntax::selector::parse_selector; +use air_r_syntax::AnyRSelector; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxKind::R_BOGUS_SELECTOR; +use air_r_syntax::RSyntaxKind::R_NAMESPACE_EXPRESSION; +use biome_parser::CompletedMarker; +use biome_parser::Parser; +use biome_rowan::AstNode; + +pub(crate) fn parse_namespace_expression( + p: &mut RParser, + mut lhs: CompletedMarker, + kind: RSyntaxKind, +) -> CompletedMarker { + let kind_range = p.cur_range(); + + // `::` and `:::` restrict their left operand as well, and it is already parsed + if !AnyRSelector::can_cast(lhs.kind(p)) { + p.error(diagnostics::expected_lhs_selector( + kind, + kind_range, + lhs.range(p), + )); + lhs = lhs.precede(p).complete(p, R_BOGUS_SELECTOR); + } + + let m = lhs.precede(p); + p.bump(kind); + + if p.scope().newlines_are_significant() && p.has_preceding_line_break() { + p.error(diagnostics::namespace_rhs_selector_after_line_break( + kind, kind_range, + )); + } else { + parse_selector(p).or_add_diagnostic(p, |_, range| { + diagnostics::expected_rhs_selector(kind, kind_range, range) + }); + } + + m.complete(p, R_NAMESPACE_EXPRESSION) +} diff --git a/crates/air_r_parser/src/syntax/parenthesized_expression.rs b/crates/air_r_parser/src/syntax/parenthesized_expression.rs new file mode 100644 index 00000000..2199ecf0 --- /dev/null +++ b/crates/air_r_parser/src/syntax/parenthesized_expression.rs @@ -0,0 +1,21 @@ +use crate::diagnostics; +use crate::parser::RParser; +use crate::parser::Scope; +use crate::syntax::expr::parse_expression; +use air_r_syntax::RSyntaxKind::R_PARENTHESIZED_EXPRESSION; +use air_r_syntax::T; +use biome_parser::CompletedMarker; +use biome_parser::Parser; + +pub(crate) fn parse_parenthesized_expression(p: &mut RParser) -> CompletedMarker { + debug_assert!(p.at(T!['('])); + + let m = p.start(); + p.with_scope(Scope::Parenthesis, |p| { + p.bump(T!['(']); + parse_expression(p).or_add_diagnostic(p, diagnostics::expected_expression); + }); + p.expect(T![')']); + + m.complete(p, R_PARENTHESIZED_EXPRESSION) +} diff --git a/crates/air_r_parser/src/syntax/root.rs b/crates/air_r_parser/src/syntax/root.rs new file mode 100644 index 00000000..3aa9b302 --- /dev/null +++ b/crates/air_r_parser/src/syntax/root.rs @@ -0,0 +1,13 @@ +use crate::parser::RParser; +use crate::syntax::statement_list::parse_statement_list; +use air_r_syntax::RSyntaxKind::R_ROOT; +use air_r_syntax::T; +use biome_parser::Parser; + +pub(crate) fn parse_root(p: &mut RParser) { + let m = p.start(); + p.eat(T![UNICODE_BOM]); + parse_statement_list(p, T![EOF]); + p.expect(T![EOF]); + m.complete(p, R_ROOT); +} diff --git a/crates/air_r_parser/src/syntax/selector.rs b/crates/air_r_parser/src/syntax/selector.rs new file mode 100644 index 00000000..9c075fc7 --- /dev/null +++ b/crates/air_r_parser/src/syntax/selector.rs @@ -0,0 +1,32 @@ +use crate::parser::RParser; +use crate::syntax::literal::parse_literal_expression; +use air_r_syntax::RSyntaxKind::DOTDOTI; +use air_r_syntax::RSyntaxKind::DOTS; +use air_r_syntax::RSyntaxKind::IDENT; +use air_r_syntax::RSyntaxKind::R_DOT_DOT_I; +use air_r_syntax::RSyntaxKind::R_DOTS; +use air_r_syntax::RSyntaxKind::R_DOUBLE_QUOTED_STRING_LITERAL; +use air_r_syntax::RSyntaxKind::R_IDENTIFIER; +use air_r_syntax::RSyntaxKind::R_RAW_STRING_LITERAL; +use air_r_syntax::RSyntaxKind::R_SINGLE_QUOTED_STRING_LITERAL; +use air_r_syntax::RSyntaxKind::R_STRING_VALUE; +use biome_parser::Parser; +use biome_parser::parsed_syntax::ParsedSyntax; +use biome_parser::parsed_syntax::ParsedSyntax::Absent; +use biome_parser::parsed_syntax::ParsedSyntax::Present; + +/// identifier | string | `...` | `..i` +/// +/// Used by [crate::syntax::extract::parse_extract_expression()] and +/// [crate::syntax::namespace::parse_namespace_expression()] +pub(crate) fn parse_selector(p: &mut RParser) -> ParsedSyntax { + match p.cur() { + IDENT => Present(parse_literal_expression(p, IDENT, R_IDENTIFIER)), + DOTS => Present(parse_literal_expression(p, DOTS, R_DOTS)), + DOTDOTI => Present(parse_literal_expression(p, DOTDOTI, R_DOT_DOT_I)), + kind @ (R_SINGLE_QUOTED_STRING_LITERAL + | R_DOUBLE_QUOTED_STRING_LITERAL + | R_RAW_STRING_LITERAL) => Present(parse_literal_expression(p, kind, R_STRING_VALUE)), + _ => Absent, + } +} diff --git a/crates/air_r_parser/src/syntax/statement_list.rs b/crates/air_r_parser/src/syntax/statement_list.rs new file mode 100644 index 00000000..5e62aa6b --- /dev/null +++ b/crates/air_r_parser/src/syntax/statement_list.rs @@ -0,0 +1,83 @@ +use crate::diagnostics; +use crate::parser::RParser; +use crate::parser::Scope; +use crate::syntax::expr::parse_expression; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxKind::R_BOGUS_EXPRESSION; +use air_r_syntax::RSyntaxKind::R_EMPTY_STATEMENT; +use air_r_syntax::RSyntaxKind::R_EXPRESSION_STATEMENT; +use air_r_syntax::RSyntaxKind::R_STATEMENT_LIST; +use air_r_syntax::T; +use biome_parser::Parser; +use biome_parser::ParserProgress; +use biome_parser::parsed_syntax::ParsedSyntax::Absent; +use biome_parser::parsed_syntax::ParsedSyntax::Present; + +/// Runs to `close` and never stops short of it +/// +/// Used by [crate::syntax::root::parse_root()] and +/// [crate::syntax::braced_statements::parse_braced_statements()] +pub(crate) fn parse_statement_list(p: &mut RParser, close: RSyntaxKind) { + let statement_list = p.start(); + let mut progress = ParserProgress::default(); + + while !p.at(T![EOF]) && !p.at(close) { + progress.assert_progressing(p); + + let statement = p.start(); + + match p.cur() { + T![;] => { + if p.scope() == Scope::TopLevel { + // At top level, a semicolon must be preceded by an + // expression that ends on the same line, i.e. a lone `;` at + // top level isn't legal. Inside braces, semicolons may + // appear even without an expression, so `{ ; }`, `{ ;; }`, + // and `{ 1\n; }` are fine. + p.error(diagnostics::unexpected_semicolon(p.cur_range())); + } + p.bump(T![;]); + statement.complete(p, R_EMPTY_STATEMENT); + } + _ => { + match parse_expression(p) { + Present(expression) => { + if !p.at(T![EOF]) + && !p.at(close) + && !p.at(T![;]) + && !p.has_preceding_line_break() + { + p.error(diagnostics::expected_statement_separator( + expression.range(p), + p.cur_range(), + )); + } + } + Absent => { + // Consume everything until next statement separator as + // bogus, and always at least one token so that the + // statement list keeps progressing + let bogus = p.start(); + p.bump_any(); + while !p.at(T![EOF]) + && !p.at(close) + && !p.at(T![;]) + && !p.has_preceding_line_break() + { + p.bump_any(); + } + let bogus = bogus.complete(p, R_BOGUS_EXPRESSION); + p.error(diagnostics::expected_expression(p, bogus.range(p))); + } + } + + if !p.has_preceding_line_break() { + p.eat(T![;]); + } + statement.complete(p, R_EXPRESSION_STATEMENT); + } + } + } + + statement_list.complete(p, R_STATEMENT_LIST); +} diff --git a/crates/air_r_parser/src/syntax/unary.rs b/crates/air_r_parser/src/syntax/unary.rs new file mode 100644 index 00000000..51ae3f18 --- /dev/null +++ b/crates/air_r_parser/src/syntax/unary.rs @@ -0,0 +1,20 @@ +use crate::diagnostics; +use crate::parser::RParser; +use crate::syntax::binding_power::BindingPower; +use crate::syntax::expr::parse_expression_with_binding_power; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxKind::R_UNARY_EXPRESSION; +use biome_parser::CompletedMarker; +use biome_parser::Parser; + +pub(crate) fn parse_unary_expression( + p: &mut RParser, + kind: RSyntaxKind, + binding_power: BindingPower, +) -> CompletedMarker { + let m = p.start(); + p.bump(kind); + parse_expression_with_binding_power(p, binding_power) + .or_add_diagnostic(p, diagnostics::expected_expression); + m.complete(p, R_UNARY_EXPRESSION) +} diff --git a/crates/air_r_parser/src/token_source.rs b/crates/air_r_parser/src/token_source.rs new file mode 100644 index 00000000..c8882bf9 --- /dev/null +++ b/crates/air_r_parser/src/token_source.rs @@ -0,0 +1,118 @@ +#[cfg(test)] +mod tests; + +use crate::lexer::RLexContext; +use crate::lexer::RLexer; +use crate::lexer::RReLexContext; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxKind::EOF; +use biome_parser::diagnostic::ParseDiagnostic; +use biome_parser::lexer::BufferedLexer; +use biome_parser::prelude::TokenSource; +use biome_parser::prelude::Trivia; +use biome_parser::token_source::TokenSourceWithBufferedLexer; +use biome_rowan::TextRange; +use biome_rowan::TriviaPieceKind; + +pub(crate) struct RTokenSource<'src> { + lexer: BufferedLexer>, + + /// The trivia the CST is rebuilt from, in source order + trivia: Vec, +} + +impl<'src> RTokenSource<'src> { + pub(crate) fn from_str(source: &'src str) -> Self { + let lexer = BufferedLexer::new(RLexer::from_str(source)); + + let mut source = Self { + lexer, + trivia: Vec::new(), + }; + + source.next_non_trivia_token(true); + source + } + + /// Advances to the next non-trivia token, collecting the trivia it skips + /// + /// Trivia trails the token before it until a newline is seen, and leads the + /// token after it from the newline onward. Before the first token of the + /// file, everything leads. That aligns with Biome's parsers and is both + /// very logical and very practical because it doesn't require any + /// lookahead. + fn next_non_trivia_token(&mut self, first: bool) { + let mut trailing = !first; + + loop { + let kind = self.lexer.next_token(RLexContext::Regular); + + let Ok(trivia_kind) = TriviaPieceKind::try_from(kind) else { + break; + }; + + // We've now seen a newline, so this newline and everything after it + // is now considered leading trivia of the next token + if trivia_kind.is_newline() { + trailing = false; + } + + self.trivia + .push(Trivia::new(trivia_kind, self.current_range(), trailing)); + } + } + + /// Re-lexes the current token. Buffered lookahead is cleared if the kind + /// changes. + pub(crate) fn re_lex(&mut self, context: RReLexContext) -> RSyntaxKind { + self.lexer.re_lex(context) + } +} + +impl TokenSource for RTokenSource<'_> { + type Kind = RSyntaxKind; + + fn current(&self) -> Self::Kind { + self.lexer.current() + } + + fn current_range(&self) -> TextRange { + self.lexer.current_range() + } + + fn text(&self) -> &str { + self.lexer.source() + } + + fn has_preceding_line_break(&self) -> bool { + self.lexer.has_preceding_line_break() + } + + fn bump(&mut self) { + if self.current() != EOF { + self.next_non_trivia_token(false); + } + } + + fn skip_as_trivia(&mut self) { + if self.current() != EOF { + self.trivia.push(Trivia::new( + TriviaPieceKind::Skipped, + self.current_range(), + false, + )); + + self.next_non_trivia_token(true); + } + } + + fn finish(self) -> (Vec, Vec) { + (self.trivia, self.lexer.finish()) + } +} + +impl<'src> TokenSourceWithBufferedLexer> for RTokenSource<'src> { + fn lexer(&mut self) -> &mut BufferedLexer> { + &mut self.lexer + } +} diff --git a/crates/air_r_parser/src/token_source/tests.rs b/crates/air_r_parser/src/token_source/tests.rs new file mode 100644 index 00000000..897cf391 --- /dev/null +++ b/crates/air_r_parser/src/token_source/tests.rs @@ -0,0 +1,211 @@ +use super::*; +use air_r_syntax::RSyntaxKind::R_BRACK; +use air_r_syntax::RSyntaxKind::R_BRACK2; +use air_r_syntax::RSyntaxKind::R_DOUBLE_LITERAL; +use biome_parser::prelude::NthToken; +use biome_rowan::TextSize; + +enum Pos { + Leading, + Trailing, +} + +/// Drive the token source to `EOF` and hand back the trivia it collected +fn trivia(source: &str) -> Vec { + drain(RTokenSource::from_str(source)) +} + +fn drain(mut source: RTokenSource) -> Vec { + while source.current() != EOF { + source.bump(); + } + + source.finish().0 +} + +fn ws(start: u32, end: u32, position: Pos) -> Trivia { + piece(TriviaPieceKind::Whitespace, start, end, position) +} + +fn nl(start: u32, end: u32) -> Trivia { + piece(TriviaPieceKind::Newline, start, end, Pos::Leading) +} + +fn cmt(start: u32, end: u32, position: Pos) -> Trivia { + piece(TriviaPieceKind::SingleLineComment, start, end, position) +} + +fn skipped(start: u32, end: u32) -> Trivia { + piece(TriviaPieceKind::Skipped, start, end, Pos::Leading) +} + +fn piece(kind: TriviaPieceKind, start: u32, end: u32, position: Pos) -> Trivia { + let range = TextRange::new(TextSize::from(start), TextSize::from(end)); + Trivia::new(kind, range, matches!(position, Pos::Trailing)) +} + +#[track_caller] +fn assert_eq_trivia(actual: Vec, expected: Vec) { + // `Trivia` implements neither `PartialEq` nor a useful `Debug` + let key = |trivia: &Trivia| (trivia.kind(), trivia.text_range(), trivia.trailing()); + let actual: Vec<_> = actual.iter().map(key).collect(); + let expected: Vec<_> = expected.iter().map(key).collect(); + + assert_eq!(actual, expected); +} + +#[test] +fn test_parse_trivia_smoke_test() { + assert_eq_trivia( + trivia("1 + 1"), + vec![ws(1, 2, Pos::Trailing), ws(3, 4, Pos::Trailing)], + ); +} + +#[test] +fn test_parse_trivia_tab_test() { + assert_eq_trivia( + trivia("1\t+\t\n\t1"), + vec![ + ws(1, 2, Pos::Trailing), + ws(3, 4, Pos::Trailing), + nl(4, 5), + ws(5, 6, Pos::Leading), + ], + ); +} + +#[test] +fn test_parse_trivia_trailing_test() { + assert_eq_trivia( + trivia("1 + \n1"), + vec![ws(1, 2, Pos::Trailing), ws(3, 4, Pos::Trailing), nl(4, 5)], + ); + + assert_eq_trivia( + trivia("1 \n "), + vec![ws(1, 3, Pos::Trailing), nl(3, 4), ws(4, 5, Pos::Leading)], + ); + + assert_eq_trivia( + trivia("1 + \r\n1"), + vec![ws(1, 2, Pos::Trailing), ws(3, 4, Pos::Trailing), nl(4, 6)], + ); +} + +#[test] +fn test_parse_trivia_before_first_token() { + assert_eq_trivia(trivia(" \n1"), vec![ws(0, 2, Pos::Leading), nl(2, 3)]); +} + +#[test] +fn test_parse_trivia_comment_test() { + assert_eq_trivia( + trivia("1 #"), + vec![ws(1, 2, Pos::Trailing), cmt(2, 3, Pos::Trailing)], + ); +} + +#[test] +fn test_parse_trivia_comment_crlf_test() { + // Trailing comment: `1 # hi\r\n` (r-lib/tree-sitter-r#184) + assert_eq_trivia( + trivia("1 # hi\r\n"), + vec![ws(1, 2, Pos::Trailing), cmt(2, 6, Pos::Trailing), nl(6, 8)], + ); + + // Own-line comment: `# hi\r\n1` + assert_eq_trivia(trivia("# hi\r\n1"), vec![cmt(0, 4, Pos::Leading), nl(4, 6)]); +} + +#[test] +fn test_parse_trivia_crlf_leading_and_trailing_comments() { + assert_eq_trivia( + trivia("# Leading\r\n1 + 1 # Trailing\r\n# Leading\r\n"), + vec![ + cmt(0, 9, Pos::Leading), + nl(9, 11), + ws(12, 13, Pos::Trailing), + ws(14, 15, Pos::Trailing), + ws(16, 17, Pos::Trailing), + cmt(17, 27, Pos::Trailing), + nl(27, 29), + cmt(29, 38, Pos::Leading), + nl(38, 40), + ], + ); +} + +#[test] +fn test_parse_trivia_comment_nothing_else_test() { + assert_eq_trivia(trivia("#"), vec![cmt(0, 1, Pos::Leading)]); +} + +#[test] +fn test_parse_trivia_comment_end_of_document_test() { + assert_eq_trivia(trivia("1\n#"), vec![nl(1, 2), cmt(2, 3, Pos::Leading)]); +} + +#[test] +fn test_parse_trivia_whitespace_between_comments_test() { + assert_eq_trivia( + trivia("1 #\n#\n2"), + vec![ + ws(1, 2, Pos::Trailing), + cmt(2, 3, Pos::Trailing), + nl(3, 4), + cmt(4, 5, Pos::Leading), + nl(5, 6), + ], + ); +} + +#[test] +fn test_parse_trivia_comment_beginning_of_document_test() { + assert_eq_trivia(trivia("#\n1"), vec![cmt(0, 1, Pos::Leading), nl(1, 2)]); +} + +/// A skipped token becomes a `Skipped` piece, and everything up to the next +/// token leads that token rather than trailing what was skipped +#[test] +fn test_skip_as_trivia() { + let mut source = RTokenSource::from_str("1 2"); + source.skip_as_trivia(); + + assert_eq!(source.current(), R_DOUBLE_LITERAL); + assert_eq!(source.current_range(), TextRange::new(2.into(), 3.into())); + + assert_eq_trivia(drain(source), vec![skipped(0, 1), ws(1, 2, Pos::Leading)]); +} + +/// Lookahead drives the inner lexer past the current token, so using +/// [`BufferedLexer`] is useful to allow us to still answer for the token the +/// parser is actually on. +#[test] +fn test_line_break_survives_lookahead() { + let mut source = RTokenSource::from_str("1\n2"); + assert!(!source.has_preceding_line_break()); + + assert_eq!(source.nth(1), R_DOUBLE_LITERAL); + assert!(source.has_nth_preceding_line_break(1)); + + assert_eq!(source.current_range(), TextRange::new(0.into(), 1.into())); + assert!(!source.has_preceding_line_break()); + + source.bump(); + assert!(source.has_preceding_line_break()); +} + +/// `parse_argument()`'s lookahead buffers the token after an argument name, +/// which inside `[[` is the `]` that the next bump asks to join +#[test] +fn test_re_lex_after_lookahead() { + let mut source = RTokenSource::from_str("a]]"); + assert_eq!(source.nth(1), R_BRACK); + + source.bump(); + assert_eq!(source.current(), R_BRACK); + + assert_eq!(source.re_lex(RReLexContext::JoinBrackets), R_BRACK2); + assert_eq!(source.current_range(), TextRange::new(1.into(), 3.into())); +} diff --git a/crates/air_r_parser/src/treesitter.rs b/crates/air_r_parser/src/treesitter.rs deleted file mode 100644 index fccc82d2..00000000 --- a/crates/air_r_parser/src/treesitter.rs +++ /dev/null @@ -1,196 +0,0 @@ -use air_r_syntax::RSyntaxKind; -use tree_sitter::{Node, TreeCursor}; - -/// `WalkEvent` describes tree walking process. -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum WalkEvent { - /// Fired before traversing the node. - Enter(T), - /// Fired after the node is traversed. - Leave(T), -} - -impl WalkEvent { - pub fn map U, U>(self, f: F) -> WalkEvent { - match self { - WalkEvent::Enter(it) => WalkEvent::Enter(f(it)), - WalkEvent::Leave(it) => WalkEvent::Leave(f(it)), - } - } -} - -// TODO: Assign iterator to rowan -pub struct Preorder<'tree> { - cursor: TreeCursor<'tree>, - next: Option>>, -} - -impl<'tree> Preorder<'tree> { - fn new(node: Node) -> Preorder { - let cursor = node.walk(); - let next = Some(WalkEvent::Enter(node)); - Preorder { cursor, next } - } - - /// Peek at the upcoming node without advancing to it - /// - /// NOTE: Effectively free, since we track this anyways - pub fn peek(&self) -> &Option>> { - &self.next - } - - /// Peek at the upcoming node's field name - /// - /// NOTE: The `self.cursor` is kept in sync with `self.next` and is always - /// pointing to the upcoming node, so using the cursor to extract the field - /// name is always valid. - pub fn peek_field_name(&self) -> Option<&'static str> { - self.cursor.field_name() - } - - pub fn skip_subtree(&mut self) { - let next = self.next.take(); - self.next = next.as_ref().and_then(|next| { - Some(match next { - WalkEvent::Enter(_first_child) => match self.cursor.goto_parent() { - true => WalkEvent::Leave(self.cursor.node()), - false => return None, - }, - WalkEvent::Leave(parent) => WalkEvent::Leave(*parent), - }) - }); - } -} - -impl<'tree> Iterator for Preorder<'tree> { - type Item = WalkEvent>; - - fn next(&mut self) -> Option { - let next = self.next.take(); - self.next = next.as_ref().and_then(|next| { - Some(match next { - WalkEvent::Enter(node) => match self.cursor.goto_first_child() { - true => WalkEvent::Enter(self.cursor.node()), - false => WalkEvent::Leave(*node), - }, - WalkEvent::Leave(node) => match self.cursor.goto_next_sibling() { - true => WalkEvent::Enter(self.cursor.node()), - false => match self.cursor.goto_parent() { - true => WalkEvent::Leave(self.cursor.node()), - false => return None, - }, - }, - }) - }); - next - } -} - -fn node_syntax_kind(x: &Node) -> RSyntaxKind { - match x.kind() { - "program" => RSyntaxKind::R_ROOT, - "unary_operator" => RSyntaxKind::R_UNARY_EXPRESSION, - "binary_operator" => RSyntaxKind::R_BINARY_EXPRESSION, - "extract_operator" => RSyntaxKind::R_EXTRACT_EXPRESSION, - "namespace_operator" => RSyntaxKind::R_NAMESPACE_EXPRESSION, - "function_definition" => RSyntaxKind::R_FUNCTION_DEFINITION, - "parameters" => RSyntaxKind::R_PARAMETERS, - "parameter" => RSyntaxKind::R_PARAMETER, - "if_statement" => RSyntaxKind::R_IF_STATEMENT, - "for_statement" => RSyntaxKind::R_FOR_STATEMENT, - "while_statement" => RSyntaxKind::R_WHILE_STATEMENT, - "repeat_statement" => RSyntaxKind::R_REPEAT_STATEMENT, - "braced_expression" => RSyntaxKind::R_BRACED_STATEMENTS, - "parenthesized_expression" => RSyntaxKind::R_PARENTHESIZED_EXPRESSION, - "call" => RSyntaxKind::R_CALL, - "subset" => RSyntaxKind::R_SUBSET, - "subset2" => RSyntaxKind::R_SUBSET2, - "argument" => RSyntaxKind::R_ARGUMENT, - "identifier" => RSyntaxKind::R_IDENTIFIER, - "integer" => RSyntaxKind::R_INTEGER_VALUE, - "float" => RSyntaxKind::R_DOUBLE_VALUE, - "complex" => RSyntaxKind::R_COMPLEX_VALUE, - "string" => RSyntaxKind::R_STRING_VALUE, - "next" => RSyntaxKind::R_NEXT_EXPRESSION, - "break" => RSyntaxKind::R_BREAK_EXPRESSION, - "true" => RSyntaxKind::R_TRUE_EXPRESSION, - "false" => RSyntaxKind::R_FALSE_EXPRESSION, - "null" => RSyntaxKind::R_NULL_EXPRESSION, - "inf" => RSyntaxKind::R_INF_EXPRESSION, - "nan" => RSyntaxKind::R_NAN_EXPRESSION, - "na" => RSyntaxKind::R_NA_EXPRESSION, - "NA" => RSyntaxKind::NA_LOGICAL_KW, - "NA_integer_" => RSyntaxKind::NA_INTEGER_KW, - "NA_real_" => RSyntaxKind::NA_DOUBLE_KW, - "NA_complex_" => RSyntaxKind::NA_COMPLEX_KW, - "NA_character_" => RSyntaxKind::NA_CHARACTER_KW, - "{" => RSyntaxKind::L_CURLY, - "}" => RSyntaxKind::R_CURLY, - "[" => RSyntaxKind::L_BRACK, - "]" => RSyntaxKind::R_BRACK, - "[[" => RSyntaxKind::L_BRACK2, - "]]" => RSyntaxKind::R_BRACK2, - "(" => RSyntaxKind::L_PAREN, - ")" => RSyntaxKind::R_PAREN, - "?" => RSyntaxKind::WAT, - "~" => RSyntaxKind::TILDE, - "<-" => RSyntaxKind::ASSIGN, - "<<-" => RSyntaxKind::SUPER_ASSIGN, - ":=" => RSyntaxKind::WALRUS, - "->" => RSyntaxKind::ASSIGN_RIGHT, - "->>" => RSyntaxKind::SUPER_ASSIGN_RIGHT, - "=" => RSyntaxKind::EQUAL, - "|" => RSyntaxKind::OR, - "&" => RSyntaxKind::AND, - "||" => RSyntaxKind::OR2, - "&&" => RSyntaxKind::AND2, - "<" => RSyntaxKind::LESS_THAN, - "<=" => RSyntaxKind::LESS_THAN_OR_EQUAL_TO, - ">" => RSyntaxKind::GREATER_THAN, - ">=" => RSyntaxKind::GREATER_THAN_OR_EQUAL_TO, - "==" => RSyntaxKind::EQUAL2, - "!=" => RSyntaxKind::NOT_EQUAL, - "+" => RSyntaxKind::PLUS, - "-" => RSyntaxKind::MINUS, - "*" => RSyntaxKind::MULTIPLY, - "/" => RSyntaxKind::DIVIDE, - "^" => RSyntaxKind::EXPONENTIATE, - "**" => RSyntaxKind::EXPONENTIATE2, - "|>" => RSyntaxKind::PIPE, - "special" => RSyntaxKind::SPECIAL, - ":" => RSyntaxKind::COLON, - "::" => RSyntaxKind::COLON2, - ":::" => RSyntaxKind::COLON3, - "$" => RSyntaxKind::DOLLAR, - "@" => RSyntaxKind::AT, - "!" => RSyntaxKind::BANG, - "function" => RSyntaxKind::FUNCTION_KW, - "\\" => RSyntaxKind::BACKSLASH, - "if" => RSyntaxKind::IF_KW, - "else" => RSyntaxKind::ELSE_KW, - "for" => RSyntaxKind::FOR_KW, - "in" => RSyntaxKind::IN_KW, - "while" => RSyntaxKind::WHILE_KW, - "repeat" => RSyntaxKind::REPEAT_KW, - "comma" => RSyntaxKind::COMMA, - "dots" => RSyntaxKind::R_DOTS, - "dot_dot_i" => RSyntaxKind::R_DOT_DOT_I, - "comment" => RSyntaxKind::COMMENT, - kind => unreachable!("Not implemented: '{kind}'."), - } -} - -pub trait NodeTypeExt: Sized { - fn syntax_kind(&self) -> RSyntaxKind; - fn preorder(&self) -> Preorder<'_>; -} - -impl NodeTypeExt for Node<'_> { - fn syntax_kind(&self) -> RSyntaxKind { - node_syntax_kind(self) - } - - fn preorder(&self) -> Preorder<'_> { - Preorder::new(*self) - } -} diff --git a/crates/air_r_parser/tests/snapshots/error/call/side_by_side_arguments.R b/crates/air_r_parser/tests/snapshots/error/call/side_by_side_arguments.R deleted file mode 100644 index 20434796..00000000 --- a/crates/air_r_parser/tests/snapshots/error/call/side_by_side_arguments.R +++ /dev/null @@ -1,2 +0,0 @@ -# Can't have two arguments side by side without a delimiter -fn(a b) diff --git a/crates/air_r_parser/tests/snapshots/error/call/side_by_side_arguments.R.snap b/crates/air_r_parser/tests/snapshots/error/call/side_by_side_arguments.R.snap deleted file mode 100644 index fa360239..00000000 --- a/crates/air_r_parser/tests/snapshots/error/call/side_by_side_arguments.R.snap +++ /dev/null @@ -1,51 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -# Can't have two arguments side by side without a delimiter -fn(a b) - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RBogusExpression { - items: [ - R_BOGUS@0..68 "# Can't have two arguments side by side without a delimiter\nfn(a b)\n" [] [], - ], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@68..68 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..68 - 0: (empty) - 1: R_STATEMENT_LIST@0..68 - 0: R_EXPRESSION_STATEMENT@0..68 - 0: R_BOGUS_EXPRESSION@0..68 - 0: R_BOGUS@0..68 "# Can't have two arguments side by side without a delimiter\nfn(a b)\n" [] [] - 1: (empty) - 2: EOF@68..68 "" [] [] - -``` - -## Errors - -``` -Failed to parse due to syntax errors. -``` diff --git a/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_double_colon.R b/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_double_colon.R deleted file mode 100644 index 66b9b916..00000000 --- a/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_double_colon.R +++ /dev/null @@ -1 +0,0 @@ -a()::b \ No newline at end of file diff --git a/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_double_colon.R.snap b/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_double_colon.R.snap deleted file mode 100644 index c44c3722..00000000 --- a/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_double_colon.R.snap +++ /dev/null @@ -1,49 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -a()::b -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RBogusExpression { - items: [ - R_BOGUS@0..6 "a()::b" [] [], - ], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@6..6 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..6 - 0: (empty) - 1: R_STATEMENT_LIST@0..6 - 0: R_EXPRESSION_STATEMENT@0..6 - 0: R_BOGUS_EXPRESSION@0..6 - 0: R_BOGUS@0..6 "a()::b" [] [] - 1: (empty) - 2: EOF@6..6 "" [] [] - -``` - -## Errors - -``` -Failed to parse due to syntax errors. -``` diff --git a/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_triple_colon.R b/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_triple_colon.R deleted file mode 100644 index 3d1a4ea8..00000000 --- a/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_triple_colon.R +++ /dev/null @@ -1 +0,0 @@ -a():::b \ No newline at end of file diff --git a/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_triple_colon.R.snap b/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_triple_colon.R.snap deleted file mode 100644 index e4776c06..00000000 --- a/crates/air_r_parser/tests/snapshots/error/namespace_expression/call_lhs_triple_colon.R.snap +++ /dev/null @@ -1,49 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -a():::b -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RBogusExpression { - items: [ - R_BOGUS@0..7 "a():::b" [] [], - ], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@7..7 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..7 - 0: (empty) - 1: R_STATEMENT_LIST@0..7 - 0: R_EXPRESSION_STATEMENT@0..7 - 0: R_BOGUS_EXPRESSION@0..7 - 0: R_BOGUS@0..7 "a():::b" [] [] - 1: (empty) - 2: EOF@7..7 "" [] [] - -``` - -## Errors - -``` -Failed to parse due to syntax errors. -``` diff --git a/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_double_colon.R b/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_double_colon.R deleted file mode 100644 index e657d86d..00000000 --- a/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_double_colon.R +++ /dev/null @@ -1 +0,0 @@ -a::b::c \ No newline at end of file diff --git a/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_double_colon.R.snap b/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_double_colon.R.snap deleted file mode 100644 index 2806b5ca..00000000 --- a/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_double_colon.R.snap +++ /dev/null @@ -1,49 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -a::b::c -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RBogusExpression { - items: [ - R_BOGUS@0..7 "a::b::c" [] [], - ], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@7..7 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..7 - 0: (empty) - 1: R_STATEMENT_LIST@0..7 - 0: R_EXPRESSION_STATEMENT@0..7 - 0: R_BOGUS_EXPRESSION@0..7 - 0: R_BOGUS@0..7 "a::b::c" [] [] - 1: (empty) - 2: EOF@7..7 "" [] [] - -``` - -## Errors - -``` -Failed to parse due to syntax errors. -``` diff --git a/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_triple_colon.R b/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_triple_colon.R deleted file mode 100644 index 280e1e53..00000000 --- a/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_triple_colon.R +++ /dev/null @@ -1 +0,0 @@ -a:::b:::c \ No newline at end of file diff --git a/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_triple_colon.R.snap b/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_triple_colon.R.snap deleted file mode 100644 index 6c076682..00000000 --- a/crates/air_r_parser/tests/snapshots/error/namespace_expression/chained_triple_colon.R.snap +++ /dev/null @@ -1,49 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -a:::b:::c -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RBogusExpression { - items: [ - R_BOGUS@0..9 "a:::b:::c" [] [], - ], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@9..9 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..9 - 0: (empty) - 1: R_STATEMENT_LIST@0..9 - 0: R_EXPRESSION_STATEMENT@0..9 - 0: R_BOGUS_EXPRESSION@0..9 - 0: R_BOGUS@0..9 "a:::b:::c" [] [] - 1: (empty) - 2: EOF@9..9 "" [] [] - -``` - -## Errors - -``` -Failed to parse due to syntax errors. -``` diff --git a/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/empty.R b/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/empty.R deleted file mode 100644 index dd626a0f..00000000 --- a/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/empty.R +++ /dev/null @@ -1 +0,0 @@ -() \ No newline at end of file diff --git a/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/empty.R.snap b/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/empty.R.snap deleted file mode 100644 index f06d3f6b..00000000 --- a/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/empty.R.snap +++ /dev/null @@ -1,49 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -() -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RBogusExpression { - items: [ - R_BOGUS@0..2 "()" [] [], - ], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@2..2 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..2 - 0: (empty) - 1: R_STATEMENT_LIST@0..2 - 0: R_EXPRESSION_STATEMENT@0..2 - 0: R_BOGUS_EXPRESSION@0..2 - 0: R_BOGUS@0..2 "()" [] [] - 1: (empty) - 2: EOF@2..2 "" [] [] - -``` - -## Errors - -``` -Failed to parse due to syntax errors. -``` diff --git a/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/multiple.R b/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/multiple.R deleted file mode 100644 index 8d2dedd1..00000000 --- a/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/multiple.R +++ /dev/null @@ -1,4 +0,0 @@ -( - a - b -) \ No newline at end of file diff --git a/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/multiple.R.snap b/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/multiple.R.snap deleted file mode 100644 index 59a67800..00000000 --- a/crates/air_r_parser/tests/snapshots/error/parenthesized_expression/multiple.R.snap +++ /dev/null @@ -1,52 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -( - a - b -) -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RBogusExpression { - items: [ - R_BOGUS@0..11 "(\n a\n b\n)" [] [], - ], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@11..11 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..11 - 0: (empty) - 1: R_STATEMENT_LIST@0..11 - 0: R_EXPRESSION_STATEMENT@0..11 - 0: R_BOGUS_EXPRESSION@0..11 - 0: R_BOGUS@0..11 "(\n a\n b\n)" [] [] - 1: (empty) - 2: EOF@11..11 "" [] [] - -``` - -## Errors - -``` -Failed to parse due to syntax errors. -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/braced_expressions.R.snap b/crates/air_r_parser/tests/snapshots/ok/braced_expressions.R.snap deleted file mode 100644 index a7a874c9..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/braced_expressions.R.snap +++ /dev/null @@ -1,193 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -{ } - -{ - -} - -{ 1 } - -{ - 1 - 2 + 2 - 3 + - 4 -} - -# comment1 -{ # comment2 - 1 # comment3 -} # comment4 - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@0..2 "{" [] [Whitespace(" ")], - statements: RStatementList [], - r_curly_token: R_CURLY@2..3 "}" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@3..6 "{" [Newline("\n"), Newline("\n")] [], - statements: RStatementList [], - r_curly_token: R_CURLY@6..9 "}" [Newline("\n"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@9..13 "{" [Newline("\n"), Newline("\n")] [Whitespace(" ")], - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@13..15 "1" [] [Whitespace(" ")], - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@15..16 "}" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@16..19 "{" [Newline("\n"), Newline("\n")] [], - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@19..23 "1" [Newline("\n"), Whitespace(" ")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBinaryExpression { - left: RDoubleValue { - value_token: R_DOUBLE_LITERAL@23..28 "2" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: PLUS@28..30 "+" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@30..31 "2" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBinaryExpression { - left: RDoubleValue { - value_token: R_DOUBLE_LITERAL@31..36 "3" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: PLUS@36..37 "+" [] [], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@37..43 "4" [Newline("\n"), Whitespace(" ")] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@43..45 "}" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@45..70 "{" [Newline("\n"), Newline("\n"), Comments("# comment1"), Newline("\n")] [Whitespace(" "), Comments("# comment2")], - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@70..85 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")], - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@85..98 "}" [Newline("\n")] [Whitespace(" "), Comments("# comment4")], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@98..99 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..99 - 0: (empty) - 1: R_STATEMENT_LIST@0..98 - 0: R_EXPRESSION_STATEMENT@0..3 - 0: R_BRACED_STATEMENTS@0..3 - 0: L_CURLY@0..2 "{" [] [Whitespace(" ")] - 1: R_STATEMENT_LIST@2..2 - 2: R_CURLY@2..3 "}" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@3..9 - 0: R_BRACED_STATEMENTS@3..9 - 0: L_CURLY@3..6 "{" [Newline("\n"), Newline("\n")] [] - 1: R_STATEMENT_LIST@6..6 - 2: R_CURLY@6..9 "}" [Newline("\n"), Newline("\n")] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@9..16 - 0: R_BRACED_STATEMENTS@9..16 - 0: L_CURLY@9..13 "{" [Newline("\n"), Newline("\n")] [Whitespace(" ")] - 1: R_STATEMENT_LIST@13..15 - 0: R_EXPRESSION_STATEMENT@13..15 - 0: R_DOUBLE_VALUE@13..15 - 0: R_DOUBLE_LITERAL@13..15 "1" [] [Whitespace(" ")] - 1: (empty) - 2: R_CURLY@15..16 "}" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@16..45 - 0: R_BRACED_STATEMENTS@16..45 - 0: L_CURLY@16..19 "{" [Newline("\n"), Newline("\n")] [] - 1: R_STATEMENT_LIST@19..43 - 0: R_EXPRESSION_STATEMENT@19..23 - 0: R_DOUBLE_VALUE@19..23 - 0: R_DOUBLE_LITERAL@19..23 "1" [Newline("\n"), Whitespace(" ")] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@23..31 - 0: R_BINARY_EXPRESSION@23..31 - 0: R_DOUBLE_VALUE@23..28 - 0: R_DOUBLE_LITERAL@23..28 "2" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: PLUS@28..30 "+" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@30..31 - 0: R_DOUBLE_LITERAL@30..31 "2" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@31..43 - 0: R_BINARY_EXPRESSION@31..43 - 0: R_DOUBLE_VALUE@31..36 - 0: R_DOUBLE_LITERAL@31..36 "3" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: PLUS@36..37 "+" [] [] - 2: R_DOUBLE_VALUE@37..43 - 0: R_DOUBLE_LITERAL@37..43 "4" [Newline("\n"), Whitespace(" ")] [] - 1: (empty) - 2: R_CURLY@43..45 "}" [Newline("\n")] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@45..98 - 0: R_BRACED_STATEMENTS@45..98 - 0: L_CURLY@45..70 "{" [Newline("\n"), Newline("\n"), Comments("# comment1"), Newline("\n")] [Whitespace(" "), Comments("# comment2")] - 1: R_STATEMENT_LIST@70..85 - 0: R_EXPRESSION_STATEMENT@70..85 - 0: R_DOUBLE_VALUE@70..85 - 0: R_DOUBLE_LITERAL@70..85 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")] - 1: (empty) - 2: R_CURLY@85..98 "}" [Newline("\n")] [Whitespace(" "), Comments("# comment4")] - 1: (empty) - 2: EOF@98..99 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/calls.R b/crates/air_r_parser/tests/snapshots/ok/calls.R deleted file mode 100644 index 2e5a47b2..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/calls.R +++ /dev/null @@ -1,58 +0,0 @@ -fn() -fn(a) -fn(a, b) -fn(...) -fn(a, ..., b) - -fn(a = 1) -fn(a = ) - -fn(a = 1, b = 2) -fn(a = , b = 2) - -fn(... = 1) -fn(... =) - -fn(..1 = 2) -fn(..1 =) - -fn("arg" = 1) -fn("arg" =) - -# Comma tests -fn(,) -fn(,,,) -fn(,,a,,b,,) - -# Comment tests -fn( - # comment -) -fn(, - # comment -,) -fn( - a, # comment1 - b # comment2 -) -fn( - a # comment1 - = # comment2 - 1 # comment3 -) - -{expr}(a = 1) - -# `NULL` argument name (r-lib/tree-sitter-r#164) -fn( - NULL = 1 -) -fn( - NULL = -) -fn( - NULL = , -) - -# Standard call as of https://github.com/r-lib/tree-sitter-r/pull/197 -return() \ No newline at end of file diff --git a/crates/air_r_parser/tests/snapshots/ok/calls.R.snap b/crates/air_r_parser/tests/snapshots/ok/calls.R.snap deleted file mode 100644 index feeea444..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/calls.R.snap +++ /dev/null @@ -1,1259 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -fn() -fn(a) -fn(a, b) -fn(...) -fn(a, ..., b) - -fn(a = 1) -fn(a = ) - -fn(a = 1, b = 2) -fn(a = , b = 2) - -fn(... = 1) -fn(... =) - -fn(..1 = 2) -fn(..1 =) - -fn("arg" = 1) -fn("arg" =) - -# Comma tests -fn(,) -fn(,,,) -fn(,,a,,b,,) - -# Comment tests -fn( - # comment -) -fn(, - # comment -,) -fn( - a, # comment1 - b # comment2 -) -fn( - a # comment1 - = # comment2 - 1 # comment3 -) - -{expr}(a = 1) - -# `NULL` argument name (r-lib/tree-sitter-r#164) -fn( - NULL = 1 -) -fn( - NULL = -) -fn( - NULL = , -) - -# Standard call as of https://github.com/r-lib/tree-sitter-r/pull/197 -return() -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@0..2 "fn" [] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@2..3 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@3..4 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@4..7 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@7..8 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@8..9 "a" [] [], - }, - }, - ], - r_paren_token: R_PAREN@9..10 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@10..13 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@13..14 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@14..15 "a" [] [], - }, - }, - COMMA@15..17 "," [] [Whitespace(" ")], - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@17..18 "b" [] [], - }, - }, - ], - r_paren_token: R_PAREN@18..19 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@19..22 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@22..23 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RDots { - value_token: DOTS@23..26 "..." [] [], - }, - }, - ], - r_paren_token: R_PAREN@26..27 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@27..30 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@30..31 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@31..32 "a" [] [], - }, - }, - COMMA@32..34 "," [] [Whitespace(" ")], - RArgument { - name_clause: missing (optional), - value: RDots { - value_token: DOTS@34..37 "..." [] [], - }, - }, - COMMA@37..39 "," [] [Whitespace(" ")], - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@39..40 "b" [] [], - }, - }, - ], - r_paren_token: R_PAREN@40..41 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@41..45 "fn" [Newline("\n"), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@45..46 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RIdentifier { - name_token: IDENT@46..48 "a" [] [Whitespace(" ")], - }, - eq_token: EQUAL@48..50 "=" [] [Whitespace(" ")], - }, - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@50..51 "1" [] [], - }, - }, - ], - r_paren_token: R_PAREN@51..52 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@52..55 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@55..56 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RIdentifier { - name_token: IDENT@56..58 "a" [] [Whitespace(" ")], - }, - eq_token: EQUAL@58..60 "=" [] [Whitespace(" ")], - }, - value: missing (optional), - }, - ], - r_paren_token: R_PAREN@60..61 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@61..65 "fn" [Newline("\n"), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@65..66 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RIdentifier { - name_token: IDENT@66..68 "a" [] [Whitespace(" ")], - }, - eq_token: EQUAL@68..70 "=" [] [Whitespace(" ")], - }, - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@70..71 "1" [] [], - }, - }, - COMMA@71..73 "," [] [Whitespace(" ")], - RArgument { - name_clause: RArgumentNameClause { - name: RIdentifier { - name_token: IDENT@73..75 "b" [] [Whitespace(" ")], - }, - eq_token: EQUAL@75..77 "=" [] [Whitespace(" ")], - }, - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@77..78 "2" [] [], - }, - }, - ], - r_paren_token: R_PAREN@78..79 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@79..82 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@82..83 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RIdentifier { - name_token: IDENT@83..85 "a" [] [Whitespace(" ")], - }, - eq_token: EQUAL@85..87 "=" [] [Whitespace(" ")], - }, - value: missing (optional), - }, - COMMA@87..89 "," [] [Whitespace(" ")], - RArgument { - name_clause: RArgumentNameClause { - name: RIdentifier { - name_token: IDENT@89..91 "b" [] [Whitespace(" ")], - }, - eq_token: EQUAL@91..93 "=" [] [Whitespace(" ")], - }, - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@93..94 "2" [] [], - }, - }, - ], - r_paren_token: R_PAREN@94..95 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@95..99 "fn" [Newline("\n"), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@99..100 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RDots { - value_token: DOTS@100..104 "..." [] [Whitespace(" ")], - }, - eq_token: EQUAL@104..106 "=" [] [Whitespace(" ")], - }, - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@106..107 "1" [] [], - }, - }, - ], - r_paren_token: R_PAREN@107..108 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@108..111 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@111..112 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RDots { - value_token: DOTS@112..116 "..." [] [Whitespace(" ")], - }, - eq_token: EQUAL@116..117 "=" [] [], - }, - value: missing (optional), - }, - ], - r_paren_token: R_PAREN@117..118 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@118..122 "fn" [Newline("\n"), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@122..123 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RDotDotI { - value_token: DOTDOTI@123..127 "..1" [] [Whitespace(" ")], - }, - eq_token: EQUAL@127..129 "=" [] [Whitespace(" ")], - }, - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@129..130 "2" [] [], - }, - }, - ], - r_paren_token: R_PAREN@130..131 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@131..134 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@134..135 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RDotDotI { - value_token: DOTDOTI@135..139 "..1" [] [Whitespace(" ")], - }, - eq_token: EQUAL@139..140 "=" [] [], - }, - value: missing (optional), - }, - ], - r_paren_token: R_PAREN@140..141 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@141..145 "fn" [Newline("\n"), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@145..146 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@146..152 "\"arg\"" [] [Whitespace(" ")], - }, - eq_token: EQUAL@152..154 "=" [] [Whitespace(" ")], - }, - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@154..155 "1" [] [], - }, - }, - ], - r_paren_token: R_PAREN@155..156 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@156..159 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@159..160 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@160..166 "\"arg\"" [] [Whitespace(" ")], - }, - eq_token: EQUAL@166..167 "=" [] [], - }, - value: missing (optional), - }, - ], - r_paren_token: R_PAREN@167..168 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@168..186 "fn" [Newline("\n"), Newline("\n"), Comments("# Comma tests"), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@186..187 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - COMMA@187..188 "," [] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - ], - r_paren_token: R_PAREN@188..189 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@189..192 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@192..193 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - COMMA@193..194 "," [] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - COMMA@194..195 "," [] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - COMMA@195..196 "," [] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - ], - r_paren_token: R_PAREN@196..197 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@197..200 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@200..201 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - COMMA@201..202 "," [] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - COMMA@202..203 "," [] [], - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@203..204 "a" [] [], - }, - }, - COMMA@204..205 "," [] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - COMMA@205..206 "," [] [], - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@206..207 "b" [] [], - }, - }, - COMMA@207..208 "," [] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - COMMA@208..209 "," [] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - ], - r_paren_token: R_PAREN@209..210 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@210..230 "fn" [Newline("\n"), Newline("\n"), Comments("# Comment tests"), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@230..231 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@231..245 ")" [Newline("\n"), Whitespace(" "), Comments("# comment"), Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@245..248 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@248..249 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - COMMA@249..250 "," [] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - COMMA@250..264 "," [Newline("\n"), Whitespace(" "), Comments("# comment"), Newline("\n")] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - ], - r_paren_token: R_PAREN@264..265 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@265..268 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@268..269 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@269..273 "a" [Newline("\n"), Whitespace(" ")] [], - }, - }, - COMMA@273..285 "," [] [Whitespace(" "), Comments("# comment1")], - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@285..300 "b" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")], - }, - }, - ], - r_paren_token: R_PAREN@300..302 ")" [Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@302..305 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@305..306 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RIdentifier { - name_token: IDENT@306..321 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment1")], - }, - eq_token: EQUAL@321..336 "=" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")], - }, - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@336..351 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")], - }, - }, - ], - r_paren_token: R_PAREN@351..353 ")" [Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RBracedStatements { - l_curly_token: L_CURLY@353..356 "{" [Newline("\n"), Newline("\n")] [], - statements: RStatementList [ - RExpressionStatement { - expression: RIdentifier { - name_token: IDENT@356..360 "expr" [] [], - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@360..361 "}" [] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@361..362 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RIdentifier { - name_token: IDENT@362..364 "a" [] [Whitespace(" ")], - }, - eq_token: EQUAL@364..366 "=" [] [Whitespace(" ")], - }, - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@366..367 "1" [] [], - }, - }, - ], - r_paren_token: R_PAREN@367..368 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@368..421 "fn" [Newline("\n"), Newline("\n"), Comments("# `NULL` argument nam ..."), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@421..422 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RNullExpression { - null_token: NULL_KW@422..430 "NULL" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - eq_token: EQUAL@430..432 "=" [] [Whitespace(" ")], - }, - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@432..433 "1" [] [], - }, - }, - ], - r_paren_token: R_PAREN@433..435 ")" [Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@435..438 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@438..439 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RNullExpression { - null_token: NULL_KW@439..447 "NULL" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - eq_token: EQUAL@447..449 "=" [] [Whitespace(" ")], - }, - value: missing (optional), - }, - ], - r_paren_token: R_PAREN@449..451 ")" [Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@451..454 "fn" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@454..455 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: RArgumentNameClause { - name: RNullExpression { - null_token: NULL_KW@455..463 "NULL" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - eq_token: EQUAL@463..465 "=" [] [Whitespace(" ")], - }, - value: missing (optional), - }, - COMMA@465..466 "," [] [], - RArgument { - name_clause: missing (optional), - value: missing (optional), - }, - ], - r_paren_token: R_PAREN@466..468 ")" [Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RIdentifier { - name_token: IDENT@468..546 "return" [Newline("\n"), Newline("\n"), Comments("# Standard call as of ..."), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@546..547 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@547..548 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@548..548 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..548 - 0: (empty) - 1: R_STATEMENT_LIST@0..548 - 0: R_EXPRESSION_STATEMENT@0..4 - 0: R_CALL@0..4 - 0: R_IDENTIFIER@0..2 - 0: IDENT@0..2 "fn" [] [] - 1: R_CALL_ARGUMENTS@2..4 - 0: L_PAREN@2..3 "(" [] [] - 1: R_ARGUMENT_LIST@3..3 - 2: R_PAREN@3..4 ")" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@4..10 - 0: R_CALL@4..10 - 0: R_IDENTIFIER@4..7 - 0: IDENT@4..7 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@7..10 - 0: L_PAREN@7..8 "(" [] [] - 1: R_ARGUMENT_LIST@8..9 - 0: R_ARGUMENT@8..9 - 0: (empty) - 1: R_IDENTIFIER@8..9 - 0: IDENT@8..9 "a" [] [] - 2: R_PAREN@9..10 ")" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@10..19 - 0: R_CALL@10..19 - 0: R_IDENTIFIER@10..13 - 0: IDENT@10..13 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@13..19 - 0: L_PAREN@13..14 "(" [] [] - 1: R_ARGUMENT_LIST@14..18 - 0: R_ARGUMENT@14..15 - 0: (empty) - 1: R_IDENTIFIER@14..15 - 0: IDENT@14..15 "a" [] [] - 1: COMMA@15..17 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@17..18 - 0: (empty) - 1: R_IDENTIFIER@17..18 - 0: IDENT@17..18 "b" [] [] - 2: R_PAREN@18..19 ")" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@19..27 - 0: R_CALL@19..27 - 0: R_IDENTIFIER@19..22 - 0: IDENT@19..22 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@22..27 - 0: L_PAREN@22..23 "(" [] [] - 1: R_ARGUMENT_LIST@23..26 - 0: R_ARGUMENT@23..26 - 0: (empty) - 1: R_DOTS@23..26 - 0: DOTS@23..26 "..." [] [] - 2: R_PAREN@26..27 ")" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@27..41 - 0: R_CALL@27..41 - 0: R_IDENTIFIER@27..30 - 0: IDENT@27..30 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@30..41 - 0: L_PAREN@30..31 "(" [] [] - 1: R_ARGUMENT_LIST@31..40 - 0: R_ARGUMENT@31..32 - 0: (empty) - 1: R_IDENTIFIER@31..32 - 0: IDENT@31..32 "a" [] [] - 1: COMMA@32..34 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@34..37 - 0: (empty) - 1: R_DOTS@34..37 - 0: DOTS@34..37 "..." [] [] - 3: COMMA@37..39 "," [] [Whitespace(" ")] - 4: R_ARGUMENT@39..40 - 0: (empty) - 1: R_IDENTIFIER@39..40 - 0: IDENT@39..40 "b" [] [] - 2: R_PAREN@40..41 ")" [] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@41..52 - 0: R_CALL@41..52 - 0: R_IDENTIFIER@41..45 - 0: IDENT@41..45 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@45..52 - 0: L_PAREN@45..46 "(" [] [] - 1: R_ARGUMENT_LIST@46..51 - 0: R_ARGUMENT@46..51 - 0: R_ARGUMENT_NAME_CLAUSE@46..50 - 0: R_IDENTIFIER@46..48 - 0: IDENT@46..48 "a" [] [Whitespace(" ")] - 1: EQUAL@48..50 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@50..51 - 0: R_DOUBLE_LITERAL@50..51 "1" [] [] - 2: R_PAREN@51..52 ")" [] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@52..61 - 0: R_CALL@52..61 - 0: R_IDENTIFIER@52..55 - 0: IDENT@52..55 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@55..61 - 0: L_PAREN@55..56 "(" [] [] - 1: R_ARGUMENT_LIST@56..60 - 0: R_ARGUMENT@56..60 - 0: R_ARGUMENT_NAME_CLAUSE@56..60 - 0: R_IDENTIFIER@56..58 - 0: IDENT@56..58 "a" [] [Whitespace(" ")] - 1: EQUAL@58..60 "=" [] [Whitespace(" ")] - 1: (empty) - 2: R_PAREN@60..61 ")" [] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@61..79 - 0: R_CALL@61..79 - 0: R_IDENTIFIER@61..65 - 0: IDENT@61..65 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@65..79 - 0: L_PAREN@65..66 "(" [] [] - 1: R_ARGUMENT_LIST@66..78 - 0: R_ARGUMENT@66..71 - 0: R_ARGUMENT_NAME_CLAUSE@66..70 - 0: R_IDENTIFIER@66..68 - 0: IDENT@66..68 "a" [] [Whitespace(" ")] - 1: EQUAL@68..70 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@70..71 - 0: R_DOUBLE_LITERAL@70..71 "1" [] [] - 1: COMMA@71..73 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@73..78 - 0: R_ARGUMENT_NAME_CLAUSE@73..77 - 0: R_IDENTIFIER@73..75 - 0: IDENT@73..75 "b" [] [Whitespace(" ")] - 1: EQUAL@75..77 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@77..78 - 0: R_DOUBLE_LITERAL@77..78 "2" [] [] - 2: R_PAREN@78..79 ")" [] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@79..95 - 0: R_CALL@79..95 - 0: R_IDENTIFIER@79..82 - 0: IDENT@79..82 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@82..95 - 0: L_PAREN@82..83 "(" [] [] - 1: R_ARGUMENT_LIST@83..94 - 0: R_ARGUMENT@83..87 - 0: R_ARGUMENT_NAME_CLAUSE@83..87 - 0: R_IDENTIFIER@83..85 - 0: IDENT@83..85 "a" [] [Whitespace(" ")] - 1: EQUAL@85..87 "=" [] [Whitespace(" ")] - 1: (empty) - 1: COMMA@87..89 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@89..94 - 0: R_ARGUMENT_NAME_CLAUSE@89..93 - 0: R_IDENTIFIER@89..91 - 0: IDENT@89..91 "b" [] [Whitespace(" ")] - 1: EQUAL@91..93 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@93..94 - 0: R_DOUBLE_LITERAL@93..94 "2" [] [] - 2: R_PAREN@94..95 ")" [] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@95..108 - 0: R_CALL@95..108 - 0: R_IDENTIFIER@95..99 - 0: IDENT@95..99 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@99..108 - 0: L_PAREN@99..100 "(" [] [] - 1: R_ARGUMENT_LIST@100..107 - 0: R_ARGUMENT@100..107 - 0: R_ARGUMENT_NAME_CLAUSE@100..106 - 0: R_DOTS@100..104 - 0: DOTS@100..104 "..." [] [Whitespace(" ")] - 1: EQUAL@104..106 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@106..107 - 0: R_DOUBLE_LITERAL@106..107 "1" [] [] - 2: R_PAREN@107..108 ")" [] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@108..118 - 0: R_CALL@108..118 - 0: R_IDENTIFIER@108..111 - 0: IDENT@108..111 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@111..118 - 0: L_PAREN@111..112 "(" [] [] - 1: R_ARGUMENT_LIST@112..117 - 0: R_ARGUMENT@112..117 - 0: R_ARGUMENT_NAME_CLAUSE@112..117 - 0: R_DOTS@112..116 - 0: DOTS@112..116 "..." [] [Whitespace(" ")] - 1: EQUAL@116..117 "=" [] [] - 1: (empty) - 2: R_PAREN@117..118 ")" [] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@118..131 - 0: R_CALL@118..131 - 0: R_IDENTIFIER@118..122 - 0: IDENT@118..122 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@122..131 - 0: L_PAREN@122..123 "(" [] [] - 1: R_ARGUMENT_LIST@123..130 - 0: R_ARGUMENT@123..130 - 0: R_ARGUMENT_NAME_CLAUSE@123..129 - 0: R_DOT_DOT_I@123..127 - 0: DOTDOTI@123..127 "..1" [] [Whitespace(" ")] - 1: EQUAL@127..129 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@129..130 - 0: R_DOUBLE_LITERAL@129..130 "2" [] [] - 2: R_PAREN@130..131 ")" [] [] - 1: (empty) - 12: R_EXPRESSION_STATEMENT@131..141 - 0: R_CALL@131..141 - 0: R_IDENTIFIER@131..134 - 0: IDENT@131..134 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@134..141 - 0: L_PAREN@134..135 "(" [] [] - 1: R_ARGUMENT_LIST@135..140 - 0: R_ARGUMENT@135..140 - 0: R_ARGUMENT_NAME_CLAUSE@135..140 - 0: R_DOT_DOT_I@135..139 - 0: DOTDOTI@135..139 "..1" [] [Whitespace(" ")] - 1: EQUAL@139..140 "=" [] [] - 1: (empty) - 2: R_PAREN@140..141 ")" [] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@141..156 - 0: R_CALL@141..156 - 0: R_IDENTIFIER@141..145 - 0: IDENT@141..145 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@145..156 - 0: L_PAREN@145..146 "(" [] [] - 1: R_ARGUMENT_LIST@146..155 - 0: R_ARGUMENT@146..155 - 0: R_ARGUMENT_NAME_CLAUSE@146..154 - 0: R_STRING_VALUE@146..152 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@146..152 "\"arg\"" [] [Whitespace(" ")] - 1: EQUAL@152..154 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@154..155 - 0: R_DOUBLE_LITERAL@154..155 "1" [] [] - 2: R_PAREN@155..156 ")" [] [] - 1: (empty) - 14: R_EXPRESSION_STATEMENT@156..168 - 0: R_CALL@156..168 - 0: R_IDENTIFIER@156..159 - 0: IDENT@156..159 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@159..168 - 0: L_PAREN@159..160 "(" [] [] - 1: R_ARGUMENT_LIST@160..167 - 0: R_ARGUMENT@160..167 - 0: R_ARGUMENT_NAME_CLAUSE@160..167 - 0: R_STRING_VALUE@160..166 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@160..166 "\"arg\"" [] [Whitespace(" ")] - 1: EQUAL@166..167 "=" [] [] - 1: (empty) - 2: R_PAREN@167..168 ")" [] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@168..189 - 0: R_CALL@168..189 - 0: R_IDENTIFIER@168..186 - 0: IDENT@168..186 "fn" [Newline("\n"), Newline("\n"), Comments("# Comma tests"), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@186..189 - 0: L_PAREN@186..187 "(" [] [] - 1: R_ARGUMENT_LIST@187..188 - 0: R_ARGUMENT@187..187 - 0: (empty) - 1: (empty) - 1: COMMA@187..188 "," [] [] - 2: R_ARGUMENT@188..188 - 0: (empty) - 1: (empty) - 2: R_PAREN@188..189 ")" [] [] - 1: (empty) - 16: R_EXPRESSION_STATEMENT@189..197 - 0: R_CALL@189..197 - 0: R_IDENTIFIER@189..192 - 0: IDENT@189..192 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@192..197 - 0: L_PAREN@192..193 "(" [] [] - 1: R_ARGUMENT_LIST@193..196 - 0: R_ARGUMENT@193..193 - 0: (empty) - 1: (empty) - 1: COMMA@193..194 "," [] [] - 2: R_ARGUMENT@194..194 - 0: (empty) - 1: (empty) - 3: COMMA@194..195 "," [] [] - 4: R_ARGUMENT@195..195 - 0: (empty) - 1: (empty) - 5: COMMA@195..196 "," [] [] - 6: R_ARGUMENT@196..196 - 0: (empty) - 1: (empty) - 2: R_PAREN@196..197 ")" [] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@197..210 - 0: R_CALL@197..210 - 0: R_IDENTIFIER@197..200 - 0: IDENT@197..200 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@200..210 - 0: L_PAREN@200..201 "(" [] [] - 1: R_ARGUMENT_LIST@201..209 - 0: R_ARGUMENT@201..201 - 0: (empty) - 1: (empty) - 1: COMMA@201..202 "," [] [] - 2: R_ARGUMENT@202..202 - 0: (empty) - 1: (empty) - 3: COMMA@202..203 "," [] [] - 4: R_ARGUMENT@203..204 - 0: (empty) - 1: R_IDENTIFIER@203..204 - 0: IDENT@203..204 "a" [] [] - 5: COMMA@204..205 "," [] [] - 6: R_ARGUMENT@205..205 - 0: (empty) - 1: (empty) - 7: COMMA@205..206 "," [] [] - 8: R_ARGUMENT@206..207 - 0: (empty) - 1: R_IDENTIFIER@206..207 - 0: IDENT@206..207 "b" [] [] - 9: COMMA@207..208 "," [] [] - 10: R_ARGUMENT@208..208 - 0: (empty) - 1: (empty) - 11: COMMA@208..209 "," [] [] - 12: R_ARGUMENT@209..209 - 0: (empty) - 1: (empty) - 2: R_PAREN@209..210 ")" [] [] - 1: (empty) - 18: R_EXPRESSION_STATEMENT@210..245 - 0: R_CALL@210..245 - 0: R_IDENTIFIER@210..230 - 0: IDENT@210..230 "fn" [Newline("\n"), Newline("\n"), Comments("# Comment tests"), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@230..245 - 0: L_PAREN@230..231 "(" [] [] - 1: R_ARGUMENT_LIST@231..231 - 2: R_PAREN@231..245 ")" [Newline("\n"), Whitespace(" "), Comments("# comment"), Newline("\n")] [] - 1: (empty) - 19: R_EXPRESSION_STATEMENT@245..265 - 0: R_CALL@245..265 - 0: R_IDENTIFIER@245..248 - 0: IDENT@245..248 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@248..265 - 0: L_PAREN@248..249 "(" [] [] - 1: R_ARGUMENT_LIST@249..264 - 0: R_ARGUMENT@249..249 - 0: (empty) - 1: (empty) - 1: COMMA@249..250 "," [] [] - 2: R_ARGUMENT@250..250 - 0: (empty) - 1: (empty) - 3: COMMA@250..264 "," [Newline("\n"), Whitespace(" "), Comments("# comment"), Newline("\n")] [] - 4: R_ARGUMENT@264..264 - 0: (empty) - 1: (empty) - 2: R_PAREN@264..265 ")" [] [] - 1: (empty) - 20: R_EXPRESSION_STATEMENT@265..302 - 0: R_CALL@265..302 - 0: R_IDENTIFIER@265..268 - 0: IDENT@265..268 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@268..302 - 0: L_PAREN@268..269 "(" [] [] - 1: R_ARGUMENT_LIST@269..300 - 0: R_ARGUMENT@269..273 - 0: (empty) - 1: R_IDENTIFIER@269..273 - 0: IDENT@269..273 "a" [Newline("\n"), Whitespace(" ")] [] - 1: COMMA@273..285 "," [] [Whitespace(" "), Comments("# comment1")] - 2: R_ARGUMENT@285..300 - 0: (empty) - 1: R_IDENTIFIER@285..300 - 0: IDENT@285..300 "b" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")] - 2: R_PAREN@300..302 ")" [Newline("\n")] [] - 1: (empty) - 21: R_EXPRESSION_STATEMENT@302..353 - 0: R_CALL@302..353 - 0: R_IDENTIFIER@302..305 - 0: IDENT@302..305 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@305..353 - 0: L_PAREN@305..306 "(" [] [] - 1: R_ARGUMENT_LIST@306..351 - 0: R_ARGUMENT@306..351 - 0: R_ARGUMENT_NAME_CLAUSE@306..336 - 0: R_IDENTIFIER@306..321 - 0: IDENT@306..321 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment1")] - 1: EQUAL@321..336 "=" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")] - 1: R_DOUBLE_VALUE@336..351 - 0: R_DOUBLE_LITERAL@336..351 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")] - 2: R_PAREN@351..353 ")" [Newline("\n")] [] - 1: (empty) - 22: R_EXPRESSION_STATEMENT@353..368 - 0: R_CALL@353..368 - 0: R_BRACED_STATEMENTS@353..361 - 0: L_CURLY@353..356 "{" [Newline("\n"), Newline("\n")] [] - 1: R_STATEMENT_LIST@356..360 - 0: R_EXPRESSION_STATEMENT@356..360 - 0: R_IDENTIFIER@356..360 - 0: IDENT@356..360 "expr" [] [] - 1: (empty) - 2: R_CURLY@360..361 "}" [] [] - 1: R_CALL_ARGUMENTS@361..368 - 0: L_PAREN@361..362 "(" [] [] - 1: R_ARGUMENT_LIST@362..367 - 0: R_ARGUMENT@362..367 - 0: R_ARGUMENT_NAME_CLAUSE@362..366 - 0: R_IDENTIFIER@362..364 - 0: IDENT@362..364 "a" [] [Whitespace(" ")] - 1: EQUAL@364..366 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@366..367 - 0: R_DOUBLE_LITERAL@366..367 "1" [] [] - 2: R_PAREN@367..368 ")" [] [] - 1: (empty) - 23: R_EXPRESSION_STATEMENT@368..435 - 0: R_CALL@368..435 - 0: R_IDENTIFIER@368..421 - 0: IDENT@368..421 "fn" [Newline("\n"), Newline("\n"), Comments("# `NULL` argument nam ..."), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@421..435 - 0: L_PAREN@421..422 "(" [] [] - 1: R_ARGUMENT_LIST@422..433 - 0: R_ARGUMENT@422..433 - 0: R_ARGUMENT_NAME_CLAUSE@422..432 - 0: R_NULL_EXPRESSION@422..430 - 0: NULL_KW@422..430 "NULL" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: EQUAL@430..432 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@432..433 - 0: R_DOUBLE_LITERAL@432..433 "1" [] [] - 2: R_PAREN@433..435 ")" [Newline("\n")] [] - 1: (empty) - 24: R_EXPRESSION_STATEMENT@435..451 - 0: R_CALL@435..451 - 0: R_IDENTIFIER@435..438 - 0: IDENT@435..438 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@438..451 - 0: L_PAREN@438..439 "(" [] [] - 1: R_ARGUMENT_LIST@439..449 - 0: R_ARGUMENT@439..449 - 0: R_ARGUMENT_NAME_CLAUSE@439..449 - 0: R_NULL_EXPRESSION@439..447 - 0: NULL_KW@439..447 "NULL" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: EQUAL@447..449 "=" [] [Whitespace(" ")] - 1: (empty) - 2: R_PAREN@449..451 ")" [Newline("\n")] [] - 1: (empty) - 25: R_EXPRESSION_STATEMENT@451..468 - 0: R_CALL@451..468 - 0: R_IDENTIFIER@451..454 - 0: IDENT@451..454 "fn" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@454..468 - 0: L_PAREN@454..455 "(" [] [] - 1: R_ARGUMENT_LIST@455..466 - 0: R_ARGUMENT@455..465 - 0: R_ARGUMENT_NAME_CLAUSE@455..465 - 0: R_NULL_EXPRESSION@455..463 - 0: NULL_KW@455..463 "NULL" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: EQUAL@463..465 "=" [] [Whitespace(" ")] - 1: (empty) - 1: COMMA@465..466 "," [] [] - 2: R_ARGUMENT@466..466 - 0: (empty) - 1: (empty) - 2: R_PAREN@466..468 ")" [Newline("\n")] [] - 1: (empty) - 26: R_EXPRESSION_STATEMENT@468..548 - 0: R_CALL@468..548 - 0: R_IDENTIFIER@468..546 - 0: IDENT@468..546 "return" [Newline("\n"), Newline("\n"), Comments("# Standard call as of ..."), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@546..548 - 0: L_PAREN@546..547 "(" [] [] - 1: R_ARGUMENT_LIST@547..547 - 2: R_PAREN@547..548 ")" [] [] - 1: (empty) - 2: EOF@548..548 "" [] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/comments.R b/crates/air_r_parser/tests/snapshots/ok/comments.R deleted file mode 100644 index 9ed0cb0d..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/comments.R +++ /dev/null @@ -1,3 +0,0 @@ -# Leading -1 + 1 # Trailing -# Leading diff --git a/crates/air_r_parser/tests/snapshots/ok/comments.R.snap b/crates/air_r_parser/tests/snapshots/ok/comments.R.snap deleted file mode 100644 index a3159931..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/comments.R.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -# Leading -1 + 1 # Trailing -# Leading - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RBinaryExpression { - left: RDoubleValue { - value_token: R_DOUBLE_LITERAL@0..12 "1" [Comments("# Leading"), Newline("\n")] [Whitespace(" ")], - }, - operator: PLUS@12..14 "+" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@14..26 "1" [] [Whitespace(" "), Comments("# Trailing")], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@26..37 "" [Newline("\n"), Comments("# Leading"), Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..37 - 0: (empty) - 1: R_STATEMENT_LIST@0..26 - 0: R_EXPRESSION_STATEMENT@0..26 - 0: R_BINARY_EXPRESSION@0..26 - 0: R_DOUBLE_VALUE@0..12 - 0: R_DOUBLE_LITERAL@0..12 "1" [Comments("# Leading"), Newline("\n")] [Whitespace(" ")] - 1: PLUS@12..14 "+" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@14..26 - 0: R_DOUBLE_LITERAL@14..26 "1" [] [Whitespace(" "), Comments("# Trailing")] - 1: (empty) - 2: EOF@26..37 "" [Newline("\n"), Comments("# Leading"), Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/crlf/multiline_string_value.R b/crates/air_r_parser/tests/snapshots/ok/crlf/multiline_string_value.R deleted file mode 100644 index 198f3f21..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/crlf/multiline_string_value.R +++ /dev/null @@ -1,2 +0,0 @@ -"multiline -string" diff --git a/crates/air_r_parser/tests/snapshots/ok/crlf/multiline_string_value.R.snap b/crates/air_r_parser/tests/snapshots/ok/crlf/multiline_string_value.R.snap deleted file mode 100644 index a52f18f7..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/crlf/multiline_string_value.R.snap +++ /dev/null @@ -1,43 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -"multiline -string" - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@0..19 "\"multiline\r\nstring\"" [] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@19..21 "" [Newline("\r\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..21 - 0: (empty) - 1: R_STATEMENT_LIST@0..19 - 0: R_EXPRESSION_STATEMENT@0..19 - 0: R_STRING_VALUE@0..19 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@0..19 "\"multiline\r\nstring\"" [] [] - 1: (empty) - 2: EOF@19..21 "" [Newline("\r\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/crlf/trailing_comments.R b/crates/air_r_parser/tests/snapshots/ok/crlf/trailing_comments.R deleted file mode 100644 index 9ed0cb0d..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/crlf/trailing_comments.R +++ /dev/null @@ -1,3 +0,0 @@ -# Leading -1 + 1 # Trailing -# Leading diff --git a/crates/air_r_parser/tests/snapshots/ok/crlf/trailing_comments.R.snap b/crates/air_r_parser/tests/snapshots/ok/crlf/trailing_comments.R.snap deleted file mode 100644 index 52fa8bf2..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/crlf/trailing_comments.R.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -# Leading -1 + 1 # Trailing -# Leading - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RBinaryExpression { - left: RDoubleValue { - value_token: R_DOUBLE_LITERAL@0..13 "1" [Comments("# Leading"), Newline("\r\n")] [Whitespace(" ")], - }, - operator: PLUS@13..15 "+" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@15..27 "1" [] [Whitespace(" "), Comments("# Trailing")], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@27..40 "" [Newline("\r\n"), Comments("# Leading"), Newline("\r\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..40 - 0: (empty) - 1: R_STATEMENT_LIST@0..27 - 0: R_EXPRESSION_STATEMENT@0..27 - 0: R_BINARY_EXPRESSION@0..27 - 0: R_DOUBLE_VALUE@0..13 - 0: R_DOUBLE_LITERAL@0..13 "1" [Comments("# Leading"), Newline("\r\n")] [Whitespace(" ")] - 1: PLUS@13..15 "+" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@15..27 - 0: R_DOUBLE_LITERAL@15..27 "1" [] [Whitespace(" "), Comments("# Trailing")] - 1: (empty) - 2: EOF@27..40 "" [Newline("\r\n"), Comments("# Leading"), Newline("\r\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/dot_dot_i.R b/crates/air_r_parser/tests/snapshots/ok/dot_dot_i.R deleted file mode 100644 index 8a0b30ec..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/dot_dot_i.R +++ /dev/null @@ -1,10 +0,0 @@ -..1 -..2 -..10 - -function(...) { - ..1 + ..2 -} - -# Parses as a function -..1() diff --git a/crates/air_r_parser/tests/snapshots/ok/dot_dot_i.R.snap b/crates/air_r_parser/tests/snapshots/ok/dot_dot_i.R.snap deleted file mode 100644 index d241a295..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/dot_dot_i.R.snap +++ /dev/null @@ -1,153 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -..1 -..2 -..10 - -function(...) { - ..1 + ..2 -} - -# Parses as a function -..1() - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RDotDotI { - value_token: DOTDOTI@0..3 "..1" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDotDotI { - value_token: DOTDOTI@3..7 "..2" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDotDotI { - value_token: DOTDOTI@7..12 "..10" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@12..22 "function" [Newline("\n"), Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@22..23 "(" [] [], - items: RParameterList [ - RParameter { - name: RDots { - value_token: DOTS@23..26 "..." [] [], - }, - default: missing (optional), - }, - ], - r_paren_token: R_PAREN@26..28 ")" [] [Whitespace(" ")], - }, - body: RBracedStatements { - l_curly_token: L_CURLY@28..29 "{" [] [], - statements: RStatementList [ - RExpressionStatement { - expression: RBinaryExpression { - left: RDotDotI { - value_token: DOTDOTI@29..36 "..1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: PLUS@36..38 "+" [] [Whitespace(" ")], - right: RDotDotI { - value_token: DOTDOTI@38..41 "..2" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@41..43 "}" [Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RDotDotI { - value_token: DOTDOTI@43..71 "..1" [Newline("\n"), Newline("\n"), Comments("# Parses as a function"), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@71..72 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@72..73 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@73..74 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..74 - 0: (empty) - 1: R_STATEMENT_LIST@0..73 - 0: R_EXPRESSION_STATEMENT@0..3 - 0: R_DOT_DOT_I@0..3 - 0: DOTDOTI@0..3 "..1" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@3..7 - 0: R_DOT_DOT_I@3..7 - 0: DOTDOTI@3..7 "..2" [Newline("\n")] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@7..12 - 0: R_DOT_DOT_I@7..12 - 0: DOTDOTI@7..12 "..10" [Newline("\n")] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@12..43 - 0: R_FUNCTION_DEFINITION@12..43 - 0: FUNCTION_KW@12..22 "function" [Newline("\n"), Newline("\n")] [] - 1: R_PARAMETERS@22..28 - 0: L_PAREN@22..23 "(" [] [] - 1: R_PARAMETER_LIST@23..26 - 0: R_PARAMETER@23..26 - 0: R_DOTS@23..26 - 0: DOTS@23..26 "..." [] [] - 1: (empty) - 2: R_PAREN@26..28 ")" [] [Whitespace(" ")] - 2: R_BRACED_STATEMENTS@28..43 - 0: L_CURLY@28..29 "{" [] [] - 1: R_STATEMENT_LIST@29..41 - 0: R_EXPRESSION_STATEMENT@29..41 - 0: R_BINARY_EXPRESSION@29..41 - 0: R_DOT_DOT_I@29..36 - 0: DOTDOTI@29..36 "..1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: PLUS@36..38 "+" [] [Whitespace(" ")] - 2: R_DOT_DOT_I@38..41 - 0: DOTDOTI@38..41 "..2" [] [] - 1: (empty) - 2: R_CURLY@41..43 "}" [Newline("\n")] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@43..73 - 0: R_CALL@43..73 - 0: R_DOT_DOT_I@43..71 - 0: DOTDOTI@43..71 "..1" [Newline("\n"), Newline("\n"), Comments("# Parses as a function"), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@71..73 - 0: L_PAREN@71..72 "(" [] [] - 1: R_ARGUMENT_LIST@72..72 - 2: R_PAREN@72..73 ")" [] [] - 1: (empty) - 2: EOF@73..74 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/dots.R b/crates/air_r_parser/tests/snapshots/ok/dots.R deleted file mode 100644 index 77d7b3c8..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/dots.R +++ /dev/null @@ -1,7 +0,0 @@ -# Parses, but fails at runtime -... - -{ ... } - -# An actual base R function -...() diff --git a/crates/air_r_parser/tests/snapshots/ok/dots.R.snap b/crates/air_r_parser/tests/snapshots/ok/dots.R.snap deleted file mode 100644 index 58fc50bd..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/dots.R.snap +++ /dev/null @@ -1,95 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -# Parses, but fails at runtime -... - -{ ... } - -# An actual base R function -...() - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RDots { - value_token: DOTS@0..34 "..." [Comments("# Parses, but fails a ..."), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@34..38 "{" [Newline("\n"), Newline("\n")] [Whitespace(" ")], - statements: RStatementList [ - RExpressionStatement { - expression: RDots { - value_token: DOTS@38..42 "..." [] [Whitespace(" ")], - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@42..43 "}" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RDots { - value_token: DOTS@43..76 "..." [Newline("\n"), Newline("\n"), Comments("# An actual base R fu ..."), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@76..77 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@77..78 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@78..79 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..79 - 0: (empty) - 1: R_STATEMENT_LIST@0..78 - 0: R_EXPRESSION_STATEMENT@0..34 - 0: R_DOTS@0..34 - 0: DOTS@0..34 "..." [Comments("# Parses, but fails a ..."), Newline("\n")] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@34..43 - 0: R_BRACED_STATEMENTS@34..43 - 0: L_CURLY@34..38 "{" [Newline("\n"), Newline("\n")] [Whitespace(" ")] - 1: R_STATEMENT_LIST@38..42 - 0: R_EXPRESSION_STATEMENT@38..42 - 0: R_DOTS@38..42 - 0: DOTS@38..42 "..." [] [Whitespace(" ")] - 1: (empty) - 2: R_CURLY@42..43 "}" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@43..78 - 0: R_CALL@43..78 - 0: R_DOTS@43..76 - 0: DOTS@43..76 "..." [Newline("\n"), Newline("\n"), Comments("# An actual base R fu ..."), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@76..78 - 0: L_PAREN@76..77 "(" [] [] - 1: R_ARGUMENT_LIST@77..77 - 2: R_PAREN@77..78 ")" [] [] - 1: (empty) - 2: EOF@78..79 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/extract_expression.R b/crates/air_r_parser/tests/snapshots/ok/extract_expression.R deleted file mode 100644 index 87796fe4..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/extract_expression.R +++ /dev/null @@ -1,26 +0,0 @@ -a$b -a@b - -a$`b` -a@`b` - -a$"b" -a@"b" - -a()$b -a()@b - -a$b$c -a@b@c - -a$b() -a@b() - -a$... -a@... - -a$..1 -a@..1 - -a$..10 -a@..10 diff --git a/crates/air_r_parser/tests/snapshots/ok/extract_expression.R.snap b/crates/air_r_parser/tests/snapshots/ok/extract_expression.R.snap deleted file mode 100644 index 7a01d851..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/extract_expression.R.snap +++ /dev/null @@ -1,485 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -a$b -a@b - -a$`b` -a@`b` - -a$"b" -a@"b" - -a()$b -a()@b - -a$b$c -a@b@c - -a$b() -a@b() - -a$... -a@... - -a$..1 -a@..1 - -a$..10 -a@..10 - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@0..1 "a" [] [], - }, - operator: DOLLAR@1..2 "$" [] [], - right: RIdentifier { - name_token: IDENT@2..3 "b" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@3..5 "a" [Newline("\n")] [], - }, - operator: AT@5..6 "@" [] [], - right: RIdentifier { - name_token: IDENT@6..7 "b" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@7..10 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: DOLLAR@10..11 "$" [] [], - right: RIdentifier { - name_token: IDENT@11..14 "`b`" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@14..16 "a" [Newline("\n")] [], - }, - operator: AT@16..17 "@" [] [], - right: RIdentifier { - name_token: IDENT@17..20 "`b`" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@20..23 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: DOLLAR@23..24 "$" [] [], - right: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@24..27 "\"b\"" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@27..29 "a" [Newline("\n")] [], - }, - operator: AT@29..30 "@" [] [], - right: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@30..33 "\"b\"" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RCall { - function: RIdentifier { - name_token: IDENT@33..36 "a" [Newline("\n"), Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@36..37 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@37..38 ")" [] [], - }, - }, - operator: DOLLAR@38..39 "$" [] [], - right: RIdentifier { - name_token: IDENT@39..40 "b" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RCall { - function: RIdentifier { - name_token: IDENT@40..42 "a" [Newline("\n")] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@42..43 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@43..44 ")" [] [], - }, - }, - operator: AT@44..45 "@" [] [], - right: RIdentifier { - name_token: IDENT@45..46 "b" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RExtractExpression { - left: RIdentifier { - name_token: IDENT@46..49 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: DOLLAR@49..50 "$" [] [], - right: RIdentifier { - name_token: IDENT@50..51 "b" [] [], - }, - }, - operator: DOLLAR@51..52 "$" [] [], - right: RIdentifier { - name_token: IDENT@52..53 "c" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RExtractExpression { - left: RIdentifier { - name_token: IDENT@53..55 "a" [Newline("\n")] [], - }, - operator: AT@55..56 "@" [] [], - right: RIdentifier { - name_token: IDENT@56..57 "b" [] [], - }, - }, - operator: AT@57..58 "@" [] [], - right: RIdentifier { - name_token: IDENT@58..59 "c" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RExtractExpression { - left: RIdentifier { - name_token: IDENT@59..62 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: DOLLAR@62..63 "$" [] [], - right: RIdentifier { - name_token: IDENT@63..64 "b" [] [], - }, - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@64..65 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@65..66 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RExtractExpression { - left: RIdentifier { - name_token: IDENT@66..68 "a" [Newline("\n")] [], - }, - operator: AT@68..69 "@" [] [], - right: RIdentifier { - name_token: IDENT@69..70 "b" [] [], - }, - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@70..71 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@71..72 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@72..75 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: DOLLAR@75..76 "$" [] [], - right: RDots { - value_token: DOTS@76..79 "..." [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@79..81 "a" [Newline("\n")] [], - }, - operator: AT@81..82 "@" [] [], - right: RDots { - value_token: DOTS@82..85 "..." [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@85..88 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: DOLLAR@88..89 "$" [] [], - right: RDotDotI { - value_token: DOTDOTI@89..92 "..1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@92..94 "a" [Newline("\n")] [], - }, - operator: AT@94..95 "@" [] [], - right: RDotDotI { - value_token: DOTDOTI@95..98 "..1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@98..101 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: DOLLAR@101..102 "$" [] [], - right: RDotDotI { - value_token: DOTDOTI@102..106 "..10" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@106..108 "a" [Newline("\n")] [], - }, - operator: AT@108..109 "@" [] [], - right: RDotDotI { - value_token: DOTDOTI@109..113 "..10" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@113..114 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..114 - 0: (empty) - 1: R_STATEMENT_LIST@0..113 - 0: R_EXPRESSION_STATEMENT@0..3 - 0: R_EXTRACT_EXPRESSION@0..3 - 0: R_IDENTIFIER@0..1 - 0: IDENT@0..1 "a" [] [] - 1: DOLLAR@1..2 "$" [] [] - 2: R_IDENTIFIER@2..3 - 0: IDENT@2..3 "b" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@3..7 - 0: R_EXTRACT_EXPRESSION@3..7 - 0: R_IDENTIFIER@3..5 - 0: IDENT@3..5 "a" [Newline("\n")] [] - 1: AT@5..6 "@" [] [] - 2: R_IDENTIFIER@6..7 - 0: IDENT@6..7 "b" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@7..14 - 0: R_EXTRACT_EXPRESSION@7..14 - 0: R_IDENTIFIER@7..10 - 0: IDENT@7..10 "a" [Newline("\n"), Newline("\n")] [] - 1: DOLLAR@10..11 "$" [] [] - 2: R_IDENTIFIER@11..14 - 0: IDENT@11..14 "`b`" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@14..20 - 0: R_EXTRACT_EXPRESSION@14..20 - 0: R_IDENTIFIER@14..16 - 0: IDENT@14..16 "a" [Newline("\n")] [] - 1: AT@16..17 "@" [] [] - 2: R_IDENTIFIER@17..20 - 0: IDENT@17..20 "`b`" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@20..27 - 0: R_EXTRACT_EXPRESSION@20..27 - 0: R_IDENTIFIER@20..23 - 0: IDENT@20..23 "a" [Newline("\n"), Newline("\n")] [] - 1: DOLLAR@23..24 "$" [] [] - 2: R_STRING_VALUE@24..27 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@24..27 "\"b\"" [] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@27..33 - 0: R_EXTRACT_EXPRESSION@27..33 - 0: R_IDENTIFIER@27..29 - 0: IDENT@27..29 "a" [Newline("\n")] [] - 1: AT@29..30 "@" [] [] - 2: R_STRING_VALUE@30..33 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@30..33 "\"b\"" [] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@33..40 - 0: R_EXTRACT_EXPRESSION@33..40 - 0: R_CALL@33..38 - 0: R_IDENTIFIER@33..36 - 0: IDENT@33..36 "a" [Newline("\n"), Newline("\n")] [] - 1: R_CALL_ARGUMENTS@36..38 - 0: L_PAREN@36..37 "(" [] [] - 1: R_ARGUMENT_LIST@37..37 - 2: R_PAREN@37..38 ")" [] [] - 1: DOLLAR@38..39 "$" [] [] - 2: R_IDENTIFIER@39..40 - 0: IDENT@39..40 "b" [] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@40..46 - 0: R_EXTRACT_EXPRESSION@40..46 - 0: R_CALL@40..44 - 0: R_IDENTIFIER@40..42 - 0: IDENT@40..42 "a" [Newline("\n")] [] - 1: R_CALL_ARGUMENTS@42..44 - 0: L_PAREN@42..43 "(" [] [] - 1: R_ARGUMENT_LIST@43..43 - 2: R_PAREN@43..44 ")" [] [] - 1: AT@44..45 "@" [] [] - 2: R_IDENTIFIER@45..46 - 0: IDENT@45..46 "b" [] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@46..53 - 0: R_EXTRACT_EXPRESSION@46..53 - 0: R_EXTRACT_EXPRESSION@46..51 - 0: R_IDENTIFIER@46..49 - 0: IDENT@46..49 "a" [Newline("\n"), Newline("\n")] [] - 1: DOLLAR@49..50 "$" [] [] - 2: R_IDENTIFIER@50..51 - 0: IDENT@50..51 "b" [] [] - 1: DOLLAR@51..52 "$" [] [] - 2: R_IDENTIFIER@52..53 - 0: IDENT@52..53 "c" [] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@53..59 - 0: R_EXTRACT_EXPRESSION@53..59 - 0: R_EXTRACT_EXPRESSION@53..57 - 0: R_IDENTIFIER@53..55 - 0: IDENT@53..55 "a" [Newline("\n")] [] - 1: AT@55..56 "@" [] [] - 2: R_IDENTIFIER@56..57 - 0: IDENT@56..57 "b" [] [] - 1: AT@57..58 "@" [] [] - 2: R_IDENTIFIER@58..59 - 0: IDENT@58..59 "c" [] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@59..66 - 0: R_CALL@59..66 - 0: R_EXTRACT_EXPRESSION@59..64 - 0: R_IDENTIFIER@59..62 - 0: IDENT@59..62 "a" [Newline("\n"), Newline("\n")] [] - 1: DOLLAR@62..63 "$" [] [] - 2: R_IDENTIFIER@63..64 - 0: IDENT@63..64 "b" [] [] - 1: R_CALL_ARGUMENTS@64..66 - 0: L_PAREN@64..65 "(" [] [] - 1: R_ARGUMENT_LIST@65..65 - 2: R_PAREN@65..66 ")" [] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@66..72 - 0: R_CALL@66..72 - 0: R_EXTRACT_EXPRESSION@66..70 - 0: R_IDENTIFIER@66..68 - 0: IDENT@66..68 "a" [Newline("\n")] [] - 1: AT@68..69 "@" [] [] - 2: R_IDENTIFIER@69..70 - 0: IDENT@69..70 "b" [] [] - 1: R_CALL_ARGUMENTS@70..72 - 0: L_PAREN@70..71 "(" [] [] - 1: R_ARGUMENT_LIST@71..71 - 2: R_PAREN@71..72 ")" [] [] - 1: (empty) - 12: R_EXPRESSION_STATEMENT@72..79 - 0: R_EXTRACT_EXPRESSION@72..79 - 0: R_IDENTIFIER@72..75 - 0: IDENT@72..75 "a" [Newline("\n"), Newline("\n")] [] - 1: DOLLAR@75..76 "$" [] [] - 2: R_DOTS@76..79 - 0: DOTS@76..79 "..." [] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@79..85 - 0: R_EXTRACT_EXPRESSION@79..85 - 0: R_IDENTIFIER@79..81 - 0: IDENT@79..81 "a" [Newline("\n")] [] - 1: AT@81..82 "@" [] [] - 2: R_DOTS@82..85 - 0: DOTS@82..85 "..." [] [] - 1: (empty) - 14: R_EXPRESSION_STATEMENT@85..92 - 0: R_EXTRACT_EXPRESSION@85..92 - 0: R_IDENTIFIER@85..88 - 0: IDENT@85..88 "a" [Newline("\n"), Newline("\n")] [] - 1: DOLLAR@88..89 "$" [] [] - 2: R_DOT_DOT_I@89..92 - 0: DOTDOTI@89..92 "..1" [] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@92..98 - 0: R_EXTRACT_EXPRESSION@92..98 - 0: R_IDENTIFIER@92..94 - 0: IDENT@92..94 "a" [Newline("\n")] [] - 1: AT@94..95 "@" [] [] - 2: R_DOT_DOT_I@95..98 - 0: DOTDOTI@95..98 "..1" [] [] - 1: (empty) - 16: R_EXPRESSION_STATEMENT@98..106 - 0: R_EXTRACT_EXPRESSION@98..106 - 0: R_IDENTIFIER@98..101 - 0: IDENT@98..101 "a" [Newline("\n"), Newline("\n")] [] - 1: DOLLAR@101..102 "$" [] [] - 2: R_DOT_DOT_I@102..106 - 0: DOTDOTI@102..106 "..10" [] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@106..113 - 0: R_EXTRACT_EXPRESSION@106..113 - 0: R_IDENTIFIER@106..108 - 0: IDENT@106..108 "a" [Newline("\n")] [] - 1: AT@108..109 "@" [] [] - 2: R_DOT_DOT_I@109..113 - 0: DOTDOTI@109..113 "..10" [] [] - 1: (empty) - 2: EOF@113..114 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/for_statement.R b/crates/air_r_parser/tests/snapshots/ok/for_statement.R deleted file mode 100644 index 51c9caa6..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/for_statement.R +++ /dev/null @@ -1,13 +0,0 @@ -for (i in 1) i - -for ( - i # comment - in # comment2 - 1 # comment3 -) i - -for # dangling1 - ( # dangling2 - a in 1 - ) # dangling3 - a diff --git a/crates/air_r_parser/tests/snapshots/ok/for_statement.R.snap b/crates/air_r_parser/tests/snapshots/ok/for_statement.R.snap deleted file mode 100644 index 40e99dfa..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/for_statement.R.snap +++ /dev/null @@ -1,137 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -for (i in 1) i - -for ( - i # comment - in # comment2 - 1 # comment3 -) i - -for # dangling1 - ( # dangling2 - a in 1 - ) # dangling3 - a - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RForStatement { - for_token: FOR_KW@0..4 "for" [] [Whitespace(" ")], - l_paren_token: L_PAREN@4..5 "(" [] [], - variable: RIdentifier { - name_token: IDENT@5..7 "i" [] [Whitespace(" ")], - }, - in_token: IN_KW@7..10 "in" [] [Whitespace(" ")], - sequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@10..11 "1" [] [], - }, - r_paren_token: R_PAREN@11..13 ")" [] [Whitespace(" ")], - body: RIdentifier { - name_token: IDENT@13..14 "i" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RForStatement { - for_token: FOR_KW@14..20 "for" [Newline("\n"), Newline("\n")] [Whitespace(" ")], - l_paren_token: L_PAREN@20..21 "(" [] [], - variable: RIdentifier { - name_token: IDENT@21..35 "i" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment")], - }, - in_token: IN_KW@35..51 "in" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")], - sequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@51..66 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")], - }, - r_paren_token: R_PAREN@66..69 ")" [Newline("\n")] [Whitespace(" ")], - body: RIdentifier { - name_token: IDENT@69..70 "i" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RForStatement { - for_token: FOR_KW@70..87 "for" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# dangling1")], - l_paren_token: L_PAREN@87..103 "(" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# dangling2")], - variable: RIdentifier { - name_token: IDENT@103..110 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - in_token: IN_KW@110..113 "in" [] [Whitespace(" ")], - sequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@113..114 "1" [] [], - }, - r_paren_token: R_PAREN@114..130 ")" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# dangling3")], - body: RIdentifier { - name_token: IDENT@130..134 "a" [Newline("\n"), Whitespace(" ")] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@134..135 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..135 - 0: (empty) - 1: R_STATEMENT_LIST@0..134 - 0: R_EXPRESSION_STATEMENT@0..14 - 0: R_FOR_STATEMENT@0..14 - 0: FOR_KW@0..4 "for" [] [Whitespace(" ")] - 1: L_PAREN@4..5 "(" [] [] - 2: R_IDENTIFIER@5..7 - 0: IDENT@5..7 "i" [] [Whitespace(" ")] - 3: IN_KW@7..10 "in" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@10..11 - 0: R_DOUBLE_LITERAL@10..11 "1" [] [] - 5: R_PAREN@11..13 ")" [] [Whitespace(" ")] - 6: R_IDENTIFIER@13..14 - 0: IDENT@13..14 "i" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@14..70 - 0: R_FOR_STATEMENT@14..70 - 0: FOR_KW@14..20 "for" [Newline("\n"), Newline("\n")] [Whitespace(" ")] - 1: L_PAREN@20..21 "(" [] [] - 2: R_IDENTIFIER@21..35 - 0: IDENT@21..35 "i" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment")] - 3: IN_KW@35..51 "in" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")] - 4: R_DOUBLE_VALUE@51..66 - 0: R_DOUBLE_LITERAL@51..66 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")] - 5: R_PAREN@66..69 ")" [Newline("\n")] [Whitespace(" ")] - 6: R_IDENTIFIER@69..70 - 0: IDENT@69..70 "i" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@70..134 - 0: R_FOR_STATEMENT@70..134 - 0: FOR_KW@70..87 "for" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# dangling1")] - 1: L_PAREN@87..103 "(" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# dangling2")] - 2: R_IDENTIFIER@103..110 - 0: IDENT@103..110 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 3: IN_KW@110..113 "in" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@113..114 - 0: R_DOUBLE_LITERAL@113..114 "1" [] [] - 5: R_PAREN@114..130 ")" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# dangling3")] - 6: R_IDENTIFIER@130..134 - 0: IDENT@130..134 "a" [Newline("\n"), Whitespace(" ")] [] - 1: (empty) - 2: EOF@134..135 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/function_definition.R b/crates/air_r_parser/tests/snapshots/ok/function_definition.R deleted file mode 100644 index eef66550..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/function_definition.R +++ /dev/null @@ -1,17 +0,0 @@ -function() 1 -function(a) a -function(a, b) a + b -function( - a # important! -) a -function(...) 1 -function(a, ..., b) 1 -function(a = 1, ..., b = 2) a -function(..1, ..2) get("..1") + get("..2") -function(..1 = 1, ..2 = 2) get("..1") + get("..2") -function( - x = # important! - 4 -) 1 - -\(x, y) 1 diff --git a/crates/air_r_parser/tests/snapshots/ok/function_definition.R.snap b/crates/air_r_parser/tests/snapshots/ok/function_definition.R.snap deleted file mode 100644 index aefa6ef9..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/function_definition.R.snap +++ /dev/null @@ -1,680 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -function() 1 -function(a) a -function(a, b) a + b -function( - a # important! -) a -function(...) 1 -function(a, ..., b) 1 -function(a = 1, ..., b = 2) a -function(..1, ..2) get("..1") + get("..2") -function(..1 = 1, ..2 = 2) get("..1") + get("..2") -function( - x = # important! - 4 -) 1 - -\(x, y) 1 - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@0..8 "function" [] [], - parameters: RParameters { - l_paren_token: L_PAREN@8..9 "(" [] [], - items: RParameterList [], - r_paren_token: R_PAREN@9..11 ")" [] [Whitespace(" ")], - }, - body: RDoubleValue { - value_token: R_DOUBLE_LITERAL@11..12 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@12..21 "function" [Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@21..22 "(" [] [], - items: RParameterList [ - RParameter { - name: RIdentifier { - name_token: IDENT@22..23 "a" [] [], - }, - default: missing (optional), - }, - ], - r_paren_token: R_PAREN@23..25 ")" [] [Whitespace(" ")], - }, - body: RIdentifier { - name_token: IDENT@25..26 "a" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@26..35 "function" [Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@35..36 "(" [] [], - items: RParameterList [ - RParameter { - name: RIdentifier { - name_token: IDENT@36..37 "a" [] [], - }, - default: missing (optional), - }, - COMMA@37..39 "," [] [Whitespace(" ")], - RParameter { - name: RIdentifier { - name_token: IDENT@39..40 "b" [] [], - }, - default: missing (optional), - }, - ], - r_paren_token: R_PAREN@40..42 ")" [] [Whitespace(" ")], - }, - body: RBinaryExpression { - left: RIdentifier { - name_token: IDENT@42..44 "a" [] [Whitespace(" ")], - }, - operator: PLUS@44..46 "+" [] [Whitespace(" ")], - right: RIdentifier { - name_token: IDENT@46..47 "b" [] [], - }, - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@47..56 "function" [Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@56..57 "(" [] [], - items: RParameterList [ - RParameter { - name: RIdentifier { - name_token: IDENT@57..74 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# important!")], - }, - default: missing (optional), - }, - ], - r_paren_token: R_PAREN@74..77 ")" [Newline("\n")] [Whitespace(" ")], - }, - body: RIdentifier { - name_token: IDENT@77..78 "a" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@78..87 "function" [Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@87..88 "(" [] [], - items: RParameterList [ - RParameter { - name: RDots { - value_token: DOTS@88..91 "..." [] [], - }, - default: missing (optional), - }, - ], - r_paren_token: R_PAREN@91..93 ")" [] [Whitespace(" ")], - }, - body: RDoubleValue { - value_token: R_DOUBLE_LITERAL@93..94 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@94..103 "function" [Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@103..104 "(" [] [], - items: RParameterList [ - RParameter { - name: RIdentifier { - name_token: IDENT@104..105 "a" [] [], - }, - default: missing (optional), - }, - COMMA@105..107 "," [] [Whitespace(" ")], - RParameter { - name: RDots { - value_token: DOTS@107..110 "..." [] [], - }, - default: missing (optional), - }, - COMMA@110..112 "," [] [Whitespace(" ")], - RParameter { - name: RIdentifier { - name_token: IDENT@112..113 "b" [] [], - }, - default: missing (optional), - }, - ], - r_paren_token: R_PAREN@113..115 ")" [] [Whitespace(" ")], - }, - body: RDoubleValue { - value_token: R_DOUBLE_LITERAL@115..116 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@116..125 "function" [Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@125..126 "(" [] [], - items: RParameterList [ - RParameter { - name: RIdentifier { - name_token: IDENT@126..128 "a" [] [Whitespace(" ")], - }, - default: RParameterDefault { - eq_token: EQUAL@128..130 "=" [] [Whitespace(" ")], - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@130..131 "1" [] [], - }, - }, - }, - COMMA@131..133 "," [] [Whitespace(" ")], - RParameter { - name: RDots { - value_token: DOTS@133..136 "..." [] [], - }, - default: missing (optional), - }, - COMMA@136..138 "," [] [Whitespace(" ")], - RParameter { - name: RIdentifier { - name_token: IDENT@138..140 "b" [] [Whitespace(" ")], - }, - default: RParameterDefault { - eq_token: EQUAL@140..142 "=" [] [Whitespace(" ")], - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@142..143 "2" [] [], - }, - }, - }, - ], - r_paren_token: R_PAREN@143..145 ")" [] [Whitespace(" ")], - }, - body: RIdentifier { - name_token: IDENT@145..146 "a" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@146..155 "function" [Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@155..156 "(" [] [], - items: RParameterList [ - RParameter { - name: RDotDotI { - value_token: DOTDOTI@156..159 "..1" [] [], - }, - default: missing (optional), - }, - COMMA@159..161 "," [] [Whitespace(" ")], - RParameter { - name: RDotDotI { - value_token: DOTDOTI@161..164 "..2" [] [], - }, - default: missing (optional), - }, - ], - r_paren_token: R_PAREN@164..166 ")" [] [Whitespace(" ")], - }, - body: RBinaryExpression { - left: RCall { - function: RIdentifier { - name_token: IDENT@166..169 "get" [] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@169..170 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@170..175 "\"..1\"" [] [], - }, - }, - ], - r_paren_token: R_PAREN@175..177 ")" [] [Whitespace(" ")], - }, - }, - operator: PLUS@177..179 "+" [] [Whitespace(" ")], - right: RCall { - function: RIdentifier { - name_token: IDENT@179..182 "get" [] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@182..183 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@183..188 "\"..2\"" [] [], - }, - }, - ], - r_paren_token: R_PAREN@188..189 ")" [] [], - }, - }, - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@189..198 "function" [Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@198..199 "(" [] [], - items: RParameterList [ - RParameter { - name: RDotDotI { - value_token: DOTDOTI@199..203 "..1" [] [Whitespace(" ")], - }, - default: RParameterDefault { - eq_token: EQUAL@203..205 "=" [] [Whitespace(" ")], - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@205..206 "1" [] [], - }, - }, - }, - COMMA@206..208 "," [] [Whitespace(" ")], - RParameter { - name: RDotDotI { - value_token: DOTDOTI@208..212 "..2" [] [Whitespace(" ")], - }, - default: RParameterDefault { - eq_token: EQUAL@212..214 "=" [] [Whitespace(" ")], - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@214..215 "2" [] [], - }, - }, - }, - ], - r_paren_token: R_PAREN@215..217 ")" [] [Whitespace(" ")], - }, - body: RBinaryExpression { - left: RCall { - function: RIdentifier { - name_token: IDENT@217..220 "get" [] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@220..221 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@221..226 "\"..1\"" [] [], - }, - }, - ], - r_paren_token: R_PAREN@226..228 ")" [] [Whitespace(" ")], - }, - }, - operator: PLUS@228..230 "+" [] [Whitespace(" ")], - right: RCall { - function: RIdentifier { - name_token: IDENT@230..233 "get" [] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@233..234 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@234..239 "\"..2\"" [] [], - }, - }, - ], - r_paren_token: R_PAREN@239..240 ")" [] [], - }, - }, - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: FUNCTION_KW@240..249 "function" [Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@249..250 "(" [] [], - items: RParameterList [ - RParameter { - name: RIdentifier { - name_token: IDENT@250..255 "x" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - default: RParameterDefault { - eq_token: EQUAL@255..269 "=" [] [Whitespace(" "), Comments("# important!")], - value: RDoubleValue { - value_token: R_DOUBLE_LITERAL@269..273 "4" [Newline("\n"), Whitespace(" ")] [], - }, - }, - }, - ], - r_paren_token: R_PAREN@273..276 ")" [Newline("\n")] [Whitespace(" ")], - }, - body: RDoubleValue { - value_token: R_DOUBLE_LITERAL@276..277 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RFunctionDefinition { - name: BACKSLASH@277..280 "\\" [Newline("\n"), Newline("\n")] [], - parameters: RParameters { - l_paren_token: L_PAREN@280..281 "(" [] [], - items: RParameterList [ - RParameter { - name: RIdentifier { - name_token: IDENT@281..282 "x" [] [], - }, - default: missing (optional), - }, - COMMA@282..284 "," [] [Whitespace(" ")], - RParameter { - name: RIdentifier { - name_token: IDENT@284..285 "y" [] [], - }, - default: missing (optional), - }, - ], - r_paren_token: R_PAREN@285..287 ")" [] [Whitespace(" ")], - }, - body: RDoubleValue { - value_token: R_DOUBLE_LITERAL@287..288 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@288..289 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..289 - 0: (empty) - 1: R_STATEMENT_LIST@0..288 - 0: R_EXPRESSION_STATEMENT@0..12 - 0: R_FUNCTION_DEFINITION@0..12 - 0: FUNCTION_KW@0..8 "function" [] [] - 1: R_PARAMETERS@8..11 - 0: L_PAREN@8..9 "(" [] [] - 1: R_PARAMETER_LIST@9..9 - 2: R_PAREN@9..11 ")" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@11..12 - 0: R_DOUBLE_LITERAL@11..12 "1" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@12..26 - 0: R_FUNCTION_DEFINITION@12..26 - 0: FUNCTION_KW@12..21 "function" [Newline("\n")] [] - 1: R_PARAMETERS@21..25 - 0: L_PAREN@21..22 "(" [] [] - 1: R_PARAMETER_LIST@22..23 - 0: R_PARAMETER@22..23 - 0: R_IDENTIFIER@22..23 - 0: IDENT@22..23 "a" [] [] - 1: (empty) - 2: R_PAREN@23..25 ")" [] [Whitespace(" ")] - 2: R_IDENTIFIER@25..26 - 0: IDENT@25..26 "a" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@26..47 - 0: R_FUNCTION_DEFINITION@26..47 - 0: FUNCTION_KW@26..35 "function" [Newline("\n")] [] - 1: R_PARAMETERS@35..42 - 0: L_PAREN@35..36 "(" [] [] - 1: R_PARAMETER_LIST@36..40 - 0: R_PARAMETER@36..37 - 0: R_IDENTIFIER@36..37 - 0: IDENT@36..37 "a" [] [] - 1: (empty) - 1: COMMA@37..39 "," [] [Whitespace(" ")] - 2: R_PARAMETER@39..40 - 0: R_IDENTIFIER@39..40 - 0: IDENT@39..40 "b" [] [] - 1: (empty) - 2: R_PAREN@40..42 ")" [] [Whitespace(" ")] - 2: R_BINARY_EXPRESSION@42..47 - 0: R_IDENTIFIER@42..44 - 0: IDENT@42..44 "a" [] [Whitespace(" ")] - 1: PLUS@44..46 "+" [] [Whitespace(" ")] - 2: R_IDENTIFIER@46..47 - 0: IDENT@46..47 "b" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@47..78 - 0: R_FUNCTION_DEFINITION@47..78 - 0: FUNCTION_KW@47..56 "function" [Newline("\n")] [] - 1: R_PARAMETERS@56..77 - 0: L_PAREN@56..57 "(" [] [] - 1: R_PARAMETER_LIST@57..74 - 0: R_PARAMETER@57..74 - 0: R_IDENTIFIER@57..74 - 0: IDENT@57..74 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# important!")] - 1: (empty) - 2: R_PAREN@74..77 ")" [Newline("\n")] [Whitespace(" ")] - 2: R_IDENTIFIER@77..78 - 0: IDENT@77..78 "a" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@78..94 - 0: R_FUNCTION_DEFINITION@78..94 - 0: FUNCTION_KW@78..87 "function" [Newline("\n")] [] - 1: R_PARAMETERS@87..93 - 0: L_PAREN@87..88 "(" [] [] - 1: R_PARAMETER_LIST@88..91 - 0: R_PARAMETER@88..91 - 0: R_DOTS@88..91 - 0: DOTS@88..91 "..." [] [] - 1: (empty) - 2: R_PAREN@91..93 ")" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@93..94 - 0: R_DOUBLE_LITERAL@93..94 "1" [] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@94..116 - 0: R_FUNCTION_DEFINITION@94..116 - 0: FUNCTION_KW@94..103 "function" [Newline("\n")] [] - 1: R_PARAMETERS@103..115 - 0: L_PAREN@103..104 "(" [] [] - 1: R_PARAMETER_LIST@104..113 - 0: R_PARAMETER@104..105 - 0: R_IDENTIFIER@104..105 - 0: IDENT@104..105 "a" [] [] - 1: (empty) - 1: COMMA@105..107 "," [] [Whitespace(" ")] - 2: R_PARAMETER@107..110 - 0: R_DOTS@107..110 - 0: DOTS@107..110 "..." [] [] - 1: (empty) - 3: COMMA@110..112 "," [] [Whitespace(" ")] - 4: R_PARAMETER@112..113 - 0: R_IDENTIFIER@112..113 - 0: IDENT@112..113 "b" [] [] - 1: (empty) - 2: R_PAREN@113..115 ")" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@115..116 - 0: R_DOUBLE_LITERAL@115..116 "1" [] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@116..146 - 0: R_FUNCTION_DEFINITION@116..146 - 0: FUNCTION_KW@116..125 "function" [Newline("\n")] [] - 1: R_PARAMETERS@125..145 - 0: L_PAREN@125..126 "(" [] [] - 1: R_PARAMETER_LIST@126..143 - 0: R_PARAMETER@126..131 - 0: R_IDENTIFIER@126..128 - 0: IDENT@126..128 "a" [] [Whitespace(" ")] - 1: R_PARAMETER_DEFAULT@128..131 - 0: EQUAL@128..130 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@130..131 - 0: R_DOUBLE_LITERAL@130..131 "1" [] [] - 1: COMMA@131..133 "," [] [Whitespace(" ")] - 2: R_PARAMETER@133..136 - 0: R_DOTS@133..136 - 0: DOTS@133..136 "..." [] [] - 1: (empty) - 3: COMMA@136..138 "," [] [Whitespace(" ")] - 4: R_PARAMETER@138..143 - 0: R_IDENTIFIER@138..140 - 0: IDENT@138..140 "b" [] [Whitespace(" ")] - 1: R_PARAMETER_DEFAULT@140..143 - 0: EQUAL@140..142 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@142..143 - 0: R_DOUBLE_LITERAL@142..143 "2" [] [] - 2: R_PAREN@143..145 ")" [] [Whitespace(" ")] - 2: R_IDENTIFIER@145..146 - 0: IDENT@145..146 "a" [] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@146..189 - 0: R_FUNCTION_DEFINITION@146..189 - 0: FUNCTION_KW@146..155 "function" [Newline("\n")] [] - 1: R_PARAMETERS@155..166 - 0: L_PAREN@155..156 "(" [] [] - 1: R_PARAMETER_LIST@156..164 - 0: R_PARAMETER@156..159 - 0: R_DOT_DOT_I@156..159 - 0: DOTDOTI@156..159 "..1" [] [] - 1: (empty) - 1: COMMA@159..161 "," [] [Whitespace(" ")] - 2: R_PARAMETER@161..164 - 0: R_DOT_DOT_I@161..164 - 0: DOTDOTI@161..164 "..2" [] [] - 1: (empty) - 2: R_PAREN@164..166 ")" [] [Whitespace(" ")] - 2: R_BINARY_EXPRESSION@166..189 - 0: R_CALL@166..177 - 0: R_IDENTIFIER@166..169 - 0: IDENT@166..169 "get" [] [] - 1: R_CALL_ARGUMENTS@169..177 - 0: L_PAREN@169..170 "(" [] [] - 1: R_ARGUMENT_LIST@170..175 - 0: R_ARGUMENT@170..175 - 0: (empty) - 1: R_STRING_VALUE@170..175 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@170..175 "\"..1\"" [] [] - 2: R_PAREN@175..177 ")" [] [Whitespace(" ")] - 1: PLUS@177..179 "+" [] [Whitespace(" ")] - 2: R_CALL@179..189 - 0: R_IDENTIFIER@179..182 - 0: IDENT@179..182 "get" [] [] - 1: R_CALL_ARGUMENTS@182..189 - 0: L_PAREN@182..183 "(" [] [] - 1: R_ARGUMENT_LIST@183..188 - 0: R_ARGUMENT@183..188 - 0: (empty) - 1: R_STRING_VALUE@183..188 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@183..188 "\"..2\"" [] [] - 2: R_PAREN@188..189 ")" [] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@189..240 - 0: R_FUNCTION_DEFINITION@189..240 - 0: FUNCTION_KW@189..198 "function" [Newline("\n")] [] - 1: R_PARAMETERS@198..217 - 0: L_PAREN@198..199 "(" [] [] - 1: R_PARAMETER_LIST@199..215 - 0: R_PARAMETER@199..206 - 0: R_DOT_DOT_I@199..203 - 0: DOTDOTI@199..203 "..1" [] [Whitespace(" ")] - 1: R_PARAMETER_DEFAULT@203..206 - 0: EQUAL@203..205 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@205..206 - 0: R_DOUBLE_LITERAL@205..206 "1" [] [] - 1: COMMA@206..208 "," [] [Whitespace(" ")] - 2: R_PARAMETER@208..215 - 0: R_DOT_DOT_I@208..212 - 0: DOTDOTI@208..212 "..2" [] [Whitespace(" ")] - 1: R_PARAMETER_DEFAULT@212..215 - 0: EQUAL@212..214 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@214..215 - 0: R_DOUBLE_LITERAL@214..215 "2" [] [] - 2: R_PAREN@215..217 ")" [] [Whitespace(" ")] - 2: R_BINARY_EXPRESSION@217..240 - 0: R_CALL@217..228 - 0: R_IDENTIFIER@217..220 - 0: IDENT@217..220 "get" [] [] - 1: R_CALL_ARGUMENTS@220..228 - 0: L_PAREN@220..221 "(" [] [] - 1: R_ARGUMENT_LIST@221..226 - 0: R_ARGUMENT@221..226 - 0: (empty) - 1: R_STRING_VALUE@221..226 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@221..226 "\"..1\"" [] [] - 2: R_PAREN@226..228 ")" [] [Whitespace(" ")] - 1: PLUS@228..230 "+" [] [Whitespace(" ")] - 2: R_CALL@230..240 - 0: R_IDENTIFIER@230..233 - 0: IDENT@230..233 "get" [] [] - 1: R_CALL_ARGUMENTS@233..240 - 0: L_PAREN@233..234 "(" [] [] - 1: R_ARGUMENT_LIST@234..239 - 0: R_ARGUMENT@234..239 - 0: (empty) - 1: R_STRING_VALUE@234..239 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@234..239 "\"..2\"" [] [] - 2: R_PAREN@239..240 ")" [] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@240..277 - 0: R_FUNCTION_DEFINITION@240..277 - 0: FUNCTION_KW@240..249 "function" [Newline("\n")] [] - 1: R_PARAMETERS@249..276 - 0: L_PAREN@249..250 "(" [] [] - 1: R_PARAMETER_LIST@250..273 - 0: R_PARAMETER@250..273 - 0: R_IDENTIFIER@250..255 - 0: IDENT@250..255 "x" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: R_PARAMETER_DEFAULT@255..273 - 0: EQUAL@255..269 "=" [] [Whitespace(" "), Comments("# important!")] - 1: R_DOUBLE_VALUE@269..273 - 0: R_DOUBLE_LITERAL@269..273 "4" [Newline("\n"), Whitespace(" ")] [] - 2: R_PAREN@273..276 ")" [Newline("\n")] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@276..277 - 0: R_DOUBLE_LITERAL@276..277 "1" [] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@277..288 - 0: R_FUNCTION_DEFINITION@277..288 - 0: BACKSLASH@277..280 "\\" [Newline("\n"), Newline("\n")] [] - 1: R_PARAMETERS@280..287 - 0: L_PAREN@280..281 "(" [] [] - 1: R_PARAMETER_LIST@281..285 - 0: R_PARAMETER@281..282 - 0: R_IDENTIFIER@281..282 - 0: IDENT@281..282 "x" [] [] - 1: (empty) - 1: COMMA@282..284 "," [] [Whitespace(" ")] - 2: R_PARAMETER@284..285 - 0: R_IDENTIFIER@284..285 - 0: IDENT@284..285 "y" [] [] - 1: (empty) - 2: R_PAREN@285..287 ")" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@287..288 - 0: R_DOUBLE_LITERAL@287..288 "1" [] [] - 1: (empty) - 2: EOF@288..289 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/if_statement.R b/crates/air_r_parser/tests/snapshots/ok/if_statement.R deleted file mode 100644 index 5d54652c..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/if_statement.R +++ /dev/null @@ -1,51 +0,0 @@ -if (a) 1 -if (a) 1 else 2 -if (a) 1 else if (b) 2 else 3 - -if # comment1 - (a) # comment2 - 1 else # comment3 - 2 # comment4 - -# ------------------------------------------------------------------------------ -# `else`-like identifier is not treated as `else` (#499) - -# ASCII letter -{ - if (TRUE) 1 - elseidx <- 1 -} - -# ASCII number -{ - if (TRUE) 1 - else1idx <- 1 -} - -# ASCII `_` -{ - if (TRUE) 1 - else_idx <- 1 -} - -# ASCII `.` -{ - if (TRUE) 1 - else.idx <- 1 -} - -# Non-ASCII, but considered `iswalnum()` in R with UTF-8 locale. -# Not considered `iswalnum()` in our scanner with C locale, but -# our overapproximation with `>= 128` allows this and aligns with R. -{ - if (TRUE) 1 - elseμ <- 1 -} - -# Non-ASCII (U+00B7), rejected by R as syntax error since `iswalnum()` returns false. -# Our overapproximation with `>= 128` allows this, but that's ok since the R -# side behavior is a syntax error. -{ - if (TRUE) 1 - else· <- 1 -} \ No newline at end of file diff --git a/crates/air_r_parser/tests/snapshots/ok/if_statement.R.snap b/crates/air_r_parser/tests/snapshots/ok/if_statement.R.snap deleted file mode 100644 index a820a35a..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/if_statement.R.snap +++ /dev/null @@ -1,599 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -if (a) 1 -if (a) 1 else 2 -if (a) 1 else if (b) 2 else 3 - -if # comment1 - (a) # comment2 - 1 else # comment3 - 2 # comment4 - -# ------------------------------------------------------------------------------ -# `else`-like identifier is not treated as `else` (#499) - -# ASCII letter -{ - if (TRUE) 1 - elseidx <- 1 -} - -# ASCII number -{ - if (TRUE) 1 - else1idx <- 1 -} - -# ASCII `_` -{ - if (TRUE) 1 - else_idx <- 1 -} - -# ASCII `.` -{ - if (TRUE) 1 - else.idx <- 1 -} - -# Non-ASCII, but considered `iswalnum()` in R with UTF-8 locale. -# Not considered `iswalnum()` in our scanner with C locale, but -# our overapproximation with `>= 128` allows this and aligns with R. -{ - if (TRUE) 1 - elseμ <- 1 -} - -# Non-ASCII (U+00B7), rejected by R as syntax error since `iswalnum()` returns false. -# Our overapproximation with `>= 128` allows this, but that's ok since the R -# side behavior is a syntax error. -{ - if (TRUE) 1 - else· <- 1 -} -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RIfStatement { - if_token: IF_KW@0..3 "if" [] [Whitespace(" ")], - l_paren_token: L_PAREN@3..4 "(" [] [], - condition: RIdentifier { - name_token: IDENT@4..5 "a" [] [], - }, - r_paren_token: R_PAREN@5..7 ")" [] [Whitespace(" ")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@7..8 "1" [] [], - }, - else_clause: missing (optional), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIfStatement { - if_token: IF_KW@8..12 "if" [Newline("\n")] [Whitespace(" ")], - l_paren_token: L_PAREN@12..13 "(" [] [], - condition: RIdentifier { - name_token: IDENT@13..14 "a" [] [], - }, - r_paren_token: R_PAREN@14..16 ")" [] [Whitespace(" ")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@16..18 "1" [] [Whitespace(" ")], - }, - else_clause: RElseClause { - else_token: ELSE_KW@18..23 "else" [] [Whitespace(" ")], - alternative: RDoubleValue { - value_token: R_DOUBLE_LITERAL@23..24 "2" [] [], - }, - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIfStatement { - if_token: IF_KW@24..28 "if" [Newline("\n")] [Whitespace(" ")], - l_paren_token: L_PAREN@28..29 "(" [] [], - condition: RIdentifier { - name_token: IDENT@29..30 "a" [] [], - }, - r_paren_token: R_PAREN@30..32 ")" [] [Whitespace(" ")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@32..34 "1" [] [Whitespace(" ")], - }, - else_clause: RElseClause { - else_token: ELSE_KW@34..39 "else" [] [Whitespace(" ")], - alternative: RIfStatement { - if_token: IF_KW@39..42 "if" [] [Whitespace(" ")], - l_paren_token: L_PAREN@42..43 "(" [] [], - condition: RIdentifier { - name_token: IDENT@43..44 "b" [] [], - }, - r_paren_token: R_PAREN@44..46 ")" [] [Whitespace(" ")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@46..48 "2" [] [Whitespace(" ")], - }, - else_clause: RElseClause { - else_token: ELSE_KW@48..53 "else" [] [Whitespace(" ")], - alternative: RDoubleValue { - value_token: R_DOUBLE_LITERAL@53..54 "3" [] [], - }, - }, - }, - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIfStatement { - if_token: IF_KW@54..69 "if" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# comment1")], - l_paren_token: L_PAREN@69..73 "(" [Newline("\n"), Whitespace(" ")] [], - condition: RIdentifier { - name_token: IDENT@73..74 "a" [] [], - }, - r_paren_token: R_PAREN@74..86 ")" [] [Whitespace(" "), Comments("# comment2")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@86..91 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - else_clause: RElseClause { - else_token: ELSE_KW@91..106 "else" [] [Whitespace(" "), Comments("# comment3")], - alternative: RDoubleValue { - value_token: R_DOUBLE_LITERAL@106..121 "2" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment4")], - }, - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@121..278 "{" [Newline("\n"), Newline("\n"), Comments("# ------------------- ..."), Newline("\n"), Comments("# `else`-like identif ..."), Newline("\n"), Newline("\n"), Comments("# ASCII letter"), Newline("\n")] [], - statements: RStatementList [ - RExpressionStatement { - expression: RIfStatement { - if_token: IF_KW@278..284 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - l_paren_token: L_PAREN@284..285 "(" [] [], - condition: RTrueExpression { - true_token: TRUE_KW@285..289 "TRUE" [] [], - }, - r_paren_token: R_PAREN@289..291 ")" [] [Whitespace(" ")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@291..292 "1" [] [], - }, - else_clause: missing (optional), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBinaryExpression { - left: RIdentifier { - name_token: IDENT@292..303 "elseidx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: ASSIGN@303..306 "<-" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@306..307 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@307..309 "}" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@309..327 "{" [Newline("\n"), Newline("\n"), Comments("# ASCII number"), Newline("\n")] [], - statements: RStatementList [ - RExpressionStatement { - expression: RIfStatement { - if_token: IF_KW@327..333 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - l_paren_token: L_PAREN@333..334 "(" [] [], - condition: RTrueExpression { - true_token: TRUE_KW@334..338 "TRUE" [] [], - }, - r_paren_token: R_PAREN@338..340 ")" [] [Whitespace(" ")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@340..341 "1" [] [], - }, - else_clause: missing (optional), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBinaryExpression { - left: RIdentifier { - name_token: IDENT@341..353 "else1idx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: ASSIGN@353..356 "<-" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@356..357 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@357..359 "}" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@359..374 "{" [Newline("\n"), Newline("\n"), Comments("# ASCII `_`"), Newline("\n")] [], - statements: RStatementList [ - RExpressionStatement { - expression: RIfStatement { - if_token: IF_KW@374..380 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - l_paren_token: L_PAREN@380..381 "(" [] [], - condition: RTrueExpression { - true_token: TRUE_KW@381..385 "TRUE" [] [], - }, - r_paren_token: R_PAREN@385..387 ")" [] [Whitespace(" ")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@387..388 "1" [] [], - }, - else_clause: missing (optional), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBinaryExpression { - left: RIdentifier { - name_token: IDENT@388..400 "else_idx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: ASSIGN@400..403 "<-" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@403..404 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@404..406 "}" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@406..421 "{" [Newline("\n"), Newline("\n"), Comments("# ASCII `.`"), Newline("\n")] [], - statements: RStatementList [ - RExpressionStatement { - expression: RIfStatement { - if_token: IF_KW@421..427 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - l_paren_token: L_PAREN@427..428 "(" [] [], - condition: RTrueExpression { - true_token: TRUE_KW@428..432 "TRUE" [] [], - }, - r_paren_token: R_PAREN@432..434 ")" [] [Whitespace(" ")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@434..435 "1" [] [], - }, - else_clause: missing (optional), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBinaryExpression { - left: RIdentifier { - name_token: IDENT@435..447 "else.idx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: ASSIGN@447..450 "<-" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@450..451 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@451..453 "}" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@453..654 "{" [Newline("\n"), Newline("\n"), Comments("# Non-ASCII, but cons ..."), Newline("\n"), Comments("# Not considered `isw ..."), Newline("\n"), Comments("# our overapproximati ..."), Newline("\n")] [], - statements: RStatementList [ - RExpressionStatement { - expression: RIfStatement { - if_token: IF_KW@654..660 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - l_paren_token: L_PAREN@660..661 "(" [] [], - condition: RTrueExpression { - true_token: TRUE_KW@661..665 "TRUE" [] [], - }, - r_paren_token: R_PAREN@665..667 ")" [] [Whitespace(" ")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@667..668 "1" [] [], - }, - else_clause: missing (optional), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBinaryExpression { - left: RIdentifier { - name_token: IDENT@668..678 "elseμ" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: ASSIGN@678..681 "<-" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@681..682 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@682..684 "}" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@684..885 "{" [Newline("\n"), Newline("\n"), Comments("# Non-ASCII (U+00B7), ..."), Newline("\n"), Comments("# Our overapproximati ..."), Newline("\n"), Comments("# side behavior is a ..."), Newline("\n")] [], - statements: RStatementList [ - RExpressionStatement { - expression: RIfStatement { - if_token: IF_KW@885..891 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - l_paren_token: L_PAREN@891..892 "(" [] [], - condition: RTrueExpression { - true_token: TRUE_KW@892..896 "TRUE" [] [], - }, - r_paren_token: R_PAREN@896..898 ")" [] [Whitespace(" ")], - consequence: RDoubleValue { - value_token: R_DOUBLE_LITERAL@898..899 "1" [] [], - }, - else_clause: missing (optional), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBinaryExpression { - left: RIdentifier { - name_token: IDENT@899..909 "else·" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: ASSIGN@909..912 "<-" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@912..913 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@913..915 "}" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@915..915 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..915 - 0: (empty) - 1: R_STATEMENT_LIST@0..915 - 0: R_EXPRESSION_STATEMENT@0..8 - 0: R_IF_STATEMENT@0..8 - 0: IF_KW@0..3 "if" [] [Whitespace(" ")] - 1: L_PAREN@3..4 "(" [] [] - 2: R_IDENTIFIER@4..5 - 0: IDENT@4..5 "a" [] [] - 3: R_PAREN@5..7 ")" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@7..8 - 0: R_DOUBLE_LITERAL@7..8 "1" [] [] - 5: (empty) - 1: (empty) - 1: R_EXPRESSION_STATEMENT@8..24 - 0: R_IF_STATEMENT@8..24 - 0: IF_KW@8..12 "if" [Newline("\n")] [Whitespace(" ")] - 1: L_PAREN@12..13 "(" [] [] - 2: R_IDENTIFIER@13..14 - 0: IDENT@13..14 "a" [] [] - 3: R_PAREN@14..16 ")" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@16..18 - 0: R_DOUBLE_LITERAL@16..18 "1" [] [Whitespace(" ")] - 5: R_ELSE_CLAUSE@18..24 - 0: ELSE_KW@18..23 "else" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@23..24 - 0: R_DOUBLE_LITERAL@23..24 "2" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@24..54 - 0: R_IF_STATEMENT@24..54 - 0: IF_KW@24..28 "if" [Newline("\n")] [Whitespace(" ")] - 1: L_PAREN@28..29 "(" [] [] - 2: R_IDENTIFIER@29..30 - 0: IDENT@29..30 "a" [] [] - 3: R_PAREN@30..32 ")" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@32..34 - 0: R_DOUBLE_LITERAL@32..34 "1" [] [Whitespace(" ")] - 5: R_ELSE_CLAUSE@34..54 - 0: ELSE_KW@34..39 "else" [] [Whitespace(" ")] - 1: R_IF_STATEMENT@39..54 - 0: IF_KW@39..42 "if" [] [Whitespace(" ")] - 1: L_PAREN@42..43 "(" [] [] - 2: R_IDENTIFIER@43..44 - 0: IDENT@43..44 "b" [] [] - 3: R_PAREN@44..46 ")" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@46..48 - 0: R_DOUBLE_LITERAL@46..48 "2" [] [Whitespace(" ")] - 5: R_ELSE_CLAUSE@48..54 - 0: ELSE_KW@48..53 "else" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@53..54 - 0: R_DOUBLE_LITERAL@53..54 "3" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@54..121 - 0: R_IF_STATEMENT@54..121 - 0: IF_KW@54..69 "if" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# comment1")] - 1: L_PAREN@69..73 "(" [Newline("\n"), Whitespace(" ")] [] - 2: R_IDENTIFIER@73..74 - 0: IDENT@73..74 "a" [] [] - 3: R_PAREN@74..86 ")" [] [Whitespace(" "), Comments("# comment2")] - 4: R_DOUBLE_VALUE@86..91 - 0: R_DOUBLE_LITERAL@86..91 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 5: R_ELSE_CLAUSE@91..121 - 0: ELSE_KW@91..106 "else" [] [Whitespace(" "), Comments("# comment3")] - 1: R_DOUBLE_VALUE@106..121 - 0: R_DOUBLE_LITERAL@106..121 "2" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment4")] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@121..309 - 0: R_BRACED_STATEMENTS@121..309 - 0: L_CURLY@121..278 "{" [Newline("\n"), Newline("\n"), Comments("# ------------------- ..."), Newline("\n"), Comments("# `else`-like identif ..."), Newline("\n"), Newline("\n"), Comments("# ASCII letter"), Newline("\n")] [] - 1: R_STATEMENT_LIST@278..307 - 0: R_EXPRESSION_STATEMENT@278..292 - 0: R_IF_STATEMENT@278..292 - 0: IF_KW@278..284 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: L_PAREN@284..285 "(" [] [] - 2: R_TRUE_EXPRESSION@285..289 - 0: TRUE_KW@285..289 "TRUE" [] [] - 3: R_PAREN@289..291 ")" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@291..292 - 0: R_DOUBLE_LITERAL@291..292 "1" [] [] - 5: (empty) - 1: (empty) - 1: R_EXPRESSION_STATEMENT@292..307 - 0: R_BINARY_EXPRESSION@292..307 - 0: R_IDENTIFIER@292..303 - 0: IDENT@292..303 "elseidx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: ASSIGN@303..306 "<-" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@306..307 - 0: R_DOUBLE_LITERAL@306..307 "1" [] [] - 1: (empty) - 2: R_CURLY@307..309 "}" [Newline("\n")] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@309..359 - 0: R_BRACED_STATEMENTS@309..359 - 0: L_CURLY@309..327 "{" [Newline("\n"), Newline("\n"), Comments("# ASCII number"), Newline("\n")] [] - 1: R_STATEMENT_LIST@327..357 - 0: R_EXPRESSION_STATEMENT@327..341 - 0: R_IF_STATEMENT@327..341 - 0: IF_KW@327..333 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: L_PAREN@333..334 "(" [] [] - 2: R_TRUE_EXPRESSION@334..338 - 0: TRUE_KW@334..338 "TRUE" [] [] - 3: R_PAREN@338..340 ")" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@340..341 - 0: R_DOUBLE_LITERAL@340..341 "1" [] [] - 5: (empty) - 1: (empty) - 1: R_EXPRESSION_STATEMENT@341..357 - 0: R_BINARY_EXPRESSION@341..357 - 0: R_IDENTIFIER@341..353 - 0: IDENT@341..353 "else1idx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: ASSIGN@353..356 "<-" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@356..357 - 0: R_DOUBLE_LITERAL@356..357 "1" [] [] - 1: (empty) - 2: R_CURLY@357..359 "}" [Newline("\n")] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@359..406 - 0: R_BRACED_STATEMENTS@359..406 - 0: L_CURLY@359..374 "{" [Newline("\n"), Newline("\n"), Comments("# ASCII `_`"), Newline("\n")] [] - 1: R_STATEMENT_LIST@374..404 - 0: R_EXPRESSION_STATEMENT@374..388 - 0: R_IF_STATEMENT@374..388 - 0: IF_KW@374..380 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: L_PAREN@380..381 "(" [] [] - 2: R_TRUE_EXPRESSION@381..385 - 0: TRUE_KW@381..385 "TRUE" [] [] - 3: R_PAREN@385..387 ")" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@387..388 - 0: R_DOUBLE_LITERAL@387..388 "1" [] [] - 5: (empty) - 1: (empty) - 1: R_EXPRESSION_STATEMENT@388..404 - 0: R_BINARY_EXPRESSION@388..404 - 0: R_IDENTIFIER@388..400 - 0: IDENT@388..400 "else_idx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: ASSIGN@400..403 "<-" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@403..404 - 0: R_DOUBLE_LITERAL@403..404 "1" [] [] - 1: (empty) - 2: R_CURLY@404..406 "}" [Newline("\n")] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@406..453 - 0: R_BRACED_STATEMENTS@406..453 - 0: L_CURLY@406..421 "{" [Newline("\n"), Newline("\n"), Comments("# ASCII `.`"), Newline("\n")] [] - 1: R_STATEMENT_LIST@421..451 - 0: R_EXPRESSION_STATEMENT@421..435 - 0: R_IF_STATEMENT@421..435 - 0: IF_KW@421..427 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: L_PAREN@427..428 "(" [] [] - 2: R_TRUE_EXPRESSION@428..432 - 0: TRUE_KW@428..432 "TRUE" [] [] - 3: R_PAREN@432..434 ")" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@434..435 - 0: R_DOUBLE_LITERAL@434..435 "1" [] [] - 5: (empty) - 1: (empty) - 1: R_EXPRESSION_STATEMENT@435..451 - 0: R_BINARY_EXPRESSION@435..451 - 0: R_IDENTIFIER@435..447 - 0: IDENT@435..447 "else.idx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: ASSIGN@447..450 "<-" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@450..451 - 0: R_DOUBLE_LITERAL@450..451 "1" [] [] - 1: (empty) - 2: R_CURLY@451..453 "}" [Newline("\n")] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@453..684 - 0: R_BRACED_STATEMENTS@453..684 - 0: L_CURLY@453..654 "{" [Newline("\n"), Newline("\n"), Comments("# Non-ASCII, but cons ..."), Newline("\n"), Comments("# Not considered `isw ..."), Newline("\n"), Comments("# our overapproximati ..."), Newline("\n")] [] - 1: R_STATEMENT_LIST@654..682 - 0: R_EXPRESSION_STATEMENT@654..668 - 0: R_IF_STATEMENT@654..668 - 0: IF_KW@654..660 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: L_PAREN@660..661 "(" [] [] - 2: R_TRUE_EXPRESSION@661..665 - 0: TRUE_KW@661..665 "TRUE" [] [] - 3: R_PAREN@665..667 ")" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@667..668 - 0: R_DOUBLE_LITERAL@667..668 "1" [] [] - 5: (empty) - 1: (empty) - 1: R_EXPRESSION_STATEMENT@668..682 - 0: R_BINARY_EXPRESSION@668..682 - 0: R_IDENTIFIER@668..678 - 0: IDENT@668..678 "elseμ" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: ASSIGN@678..681 "<-" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@681..682 - 0: R_DOUBLE_LITERAL@681..682 "1" [] [] - 1: (empty) - 2: R_CURLY@682..684 "}" [Newline("\n")] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@684..915 - 0: R_BRACED_STATEMENTS@684..915 - 0: L_CURLY@684..885 "{" [Newline("\n"), Newline("\n"), Comments("# Non-ASCII (U+00B7), ..."), Newline("\n"), Comments("# Our overapproximati ..."), Newline("\n"), Comments("# side behavior is a ..."), Newline("\n")] [] - 1: R_STATEMENT_LIST@885..913 - 0: R_EXPRESSION_STATEMENT@885..899 - 0: R_IF_STATEMENT@885..899 - 0: IF_KW@885..891 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: L_PAREN@891..892 "(" [] [] - 2: R_TRUE_EXPRESSION@892..896 - 0: TRUE_KW@892..896 "TRUE" [] [] - 3: R_PAREN@896..898 ")" [] [Whitespace(" ")] - 4: R_DOUBLE_VALUE@898..899 - 0: R_DOUBLE_LITERAL@898..899 "1" [] [] - 5: (empty) - 1: (empty) - 1: R_EXPRESSION_STATEMENT@899..913 - 0: R_BINARY_EXPRESSION@899..913 - 0: R_IDENTIFIER@899..909 - 0: IDENT@899..909 "else·" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: ASSIGN@909..912 "<-" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@912..913 - 0: R_DOUBLE_LITERAL@912..913 "1" [] [] - 1: (empty) - 2: R_CURLY@913..915 "}" [Newline("\n")] [] - 1: (empty) - 2: EOF@915..915 "" [] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/namespace_expression.R b/crates/air_r_parser/tests/snapshots/ok/namespace_expression.R deleted file mode 100644 index bbe9e5b3..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/namespace_expression.R +++ /dev/null @@ -1,32 +0,0 @@ -a::b -a:::b - -a::`b` -a:::`b` - -a::"b" -a:::"b" - -a::b() -a:::b() - -a::b$c() -a:::b@c() - -a::... -a:::... - -...::a -...:::a - -...::... -...:::... - -a::..1 -a:::..1 - -..1::a -..1:::a - -..1::..1 -..1:::..1 diff --git a/crates/air_r_parser/tests/snapshots/ok/namespace_expression.R.snap b/crates/air_r_parser/tests/snapshots/ok/namespace_expression.R.snap deleted file mode 100644 index ac79f309..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/namespace_expression.R.snap +++ /dev/null @@ -1,571 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -a::b -a:::b - -a::`b` -a:::`b` - -a::"b" -a:::"b" - -a::b() -a:::b() - -a::b$c() -a:::b@c() - -a::... -a:::... - -...::a -...:::a - -...::... -...:::... - -a::..1 -a:::..1 - -..1::a -..1:::a - -..1::..1 -..1:::..1 - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@0..1 "a" [] [], - }, - operator: COLON2@1..3 "::" [] [], - right: RIdentifier { - name_token: IDENT@3..4 "b" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@4..6 "a" [Newline("\n")] [], - }, - operator: COLON3@6..9 ":::" [] [], - right: RIdentifier { - name_token: IDENT@9..10 "b" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@10..13 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@13..15 "::" [] [], - right: RIdentifier { - name_token: IDENT@15..18 "`b`" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@18..20 "a" [Newline("\n")] [], - }, - operator: COLON3@20..23 ":::" [] [], - right: RIdentifier { - name_token: IDENT@23..26 "`b`" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@26..29 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@29..31 "::" [] [], - right: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@31..34 "\"b\"" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@34..36 "a" [Newline("\n")] [], - }, - operator: COLON3@36..39 ":::" [] [], - right: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@39..42 "\"b\"" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@42..45 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@45..47 "::" [] [], - right: RIdentifier { - name_token: IDENT@47..48 "b" [] [], - }, - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@48..49 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@49..50 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@50..52 "a" [Newline("\n")] [], - }, - operator: COLON3@52..55 ":::" [] [], - right: RIdentifier { - name_token: IDENT@55..56 "b" [] [], - }, - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@56..57 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@57..58 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RExtractExpression { - left: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@58..61 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@61..63 "::" [] [], - right: RIdentifier { - name_token: IDENT@63..64 "b" [] [], - }, - }, - operator: DOLLAR@64..65 "$" [] [], - right: RIdentifier { - name_token: IDENT@65..66 "c" [] [], - }, - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@66..67 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@67..68 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RExtractExpression { - left: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@68..70 "a" [Newline("\n")] [], - }, - operator: COLON3@70..73 ":::" [] [], - right: RIdentifier { - name_token: IDENT@73..74 "b" [] [], - }, - }, - operator: AT@74..75 "@" [] [], - right: RIdentifier { - name_token: IDENT@75..76 "c" [] [], - }, - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@76..77 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@77..78 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@78..81 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@81..83 "::" [] [], - right: RDots { - value_token: DOTS@83..86 "..." [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@86..88 "a" [Newline("\n")] [], - }, - operator: COLON3@88..91 ":::" [] [], - right: RDots { - value_token: DOTS@91..94 "..." [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RDots { - value_token: DOTS@94..99 "..." [Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@99..101 "::" [] [], - right: RIdentifier { - name_token: IDENT@101..102 "a" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RDots { - value_token: DOTS@102..106 "..." [Newline("\n")] [], - }, - operator: COLON3@106..109 ":::" [] [], - right: RIdentifier { - name_token: IDENT@109..110 "a" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RDots { - value_token: DOTS@110..115 "..." [Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@115..117 "::" [] [], - right: RDots { - value_token: DOTS@117..120 "..." [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RDots { - value_token: DOTS@120..124 "..." [Newline("\n")] [], - }, - operator: COLON3@124..127 ":::" [] [], - right: RDots { - value_token: DOTS@127..130 "..." [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@130..133 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@133..135 "::" [] [], - right: RDotDotI { - value_token: DOTDOTI@135..138 "..1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@138..140 "a" [Newline("\n")] [], - }, - operator: COLON3@140..143 ":::" [] [], - right: RDotDotI { - value_token: DOTDOTI@143..146 "..1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RDotDotI { - value_token: DOTDOTI@146..151 "..1" [Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@151..153 "::" [] [], - right: RIdentifier { - name_token: IDENT@153..154 "a" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RDotDotI { - value_token: DOTDOTI@154..158 "..1" [Newline("\n")] [], - }, - operator: COLON3@158..161 ":::" [] [], - right: RIdentifier { - name_token: IDENT@161..162 "a" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RDotDotI { - value_token: DOTDOTI@162..167 "..1" [Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@167..169 "::" [] [], - right: RDotDotI { - value_token: DOTDOTI@169..172 "..1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RDotDotI { - value_token: DOTDOTI@172..176 "..1" [Newline("\n")] [], - }, - operator: COLON3@176..179 ":::" [] [], - right: RDotDotI { - value_token: DOTDOTI@179..182 "..1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@182..183 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..183 - 0: (empty) - 1: R_STATEMENT_LIST@0..182 - 0: R_EXPRESSION_STATEMENT@0..4 - 0: R_NAMESPACE_EXPRESSION@0..4 - 0: R_IDENTIFIER@0..1 - 0: IDENT@0..1 "a" [] [] - 1: COLON2@1..3 "::" [] [] - 2: R_IDENTIFIER@3..4 - 0: IDENT@3..4 "b" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@4..10 - 0: R_NAMESPACE_EXPRESSION@4..10 - 0: R_IDENTIFIER@4..6 - 0: IDENT@4..6 "a" [Newline("\n")] [] - 1: COLON3@6..9 ":::" [] [] - 2: R_IDENTIFIER@9..10 - 0: IDENT@9..10 "b" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@10..18 - 0: R_NAMESPACE_EXPRESSION@10..18 - 0: R_IDENTIFIER@10..13 - 0: IDENT@10..13 "a" [Newline("\n"), Newline("\n")] [] - 1: COLON2@13..15 "::" [] [] - 2: R_IDENTIFIER@15..18 - 0: IDENT@15..18 "`b`" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@18..26 - 0: R_NAMESPACE_EXPRESSION@18..26 - 0: R_IDENTIFIER@18..20 - 0: IDENT@18..20 "a" [Newline("\n")] [] - 1: COLON3@20..23 ":::" [] [] - 2: R_IDENTIFIER@23..26 - 0: IDENT@23..26 "`b`" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@26..34 - 0: R_NAMESPACE_EXPRESSION@26..34 - 0: R_IDENTIFIER@26..29 - 0: IDENT@26..29 "a" [Newline("\n"), Newline("\n")] [] - 1: COLON2@29..31 "::" [] [] - 2: R_STRING_VALUE@31..34 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@31..34 "\"b\"" [] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@34..42 - 0: R_NAMESPACE_EXPRESSION@34..42 - 0: R_IDENTIFIER@34..36 - 0: IDENT@34..36 "a" [Newline("\n")] [] - 1: COLON3@36..39 ":::" [] [] - 2: R_STRING_VALUE@39..42 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@39..42 "\"b\"" [] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@42..50 - 0: R_CALL@42..50 - 0: R_NAMESPACE_EXPRESSION@42..48 - 0: R_IDENTIFIER@42..45 - 0: IDENT@42..45 "a" [Newline("\n"), Newline("\n")] [] - 1: COLON2@45..47 "::" [] [] - 2: R_IDENTIFIER@47..48 - 0: IDENT@47..48 "b" [] [] - 1: R_CALL_ARGUMENTS@48..50 - 0: L_PAREN@48..49 "(" [] [] - 1: R_ARGUMENT_LIST@49..49 - 2: R_PAREN@49..50 ")" [] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@50..58 - 0: R_CALL@50..58 - 0: R_NAMESPACE_EXPRESSION@50..56 - 0: R_IDENTIFIER@50..52 - 0: IDENT@50..52 "a" [Newline("\n")] [] - 1: COLON3@52..55 ":::" [] [] - 2: R_IDENTIFIER@55..56 - 0: IDENT@55..56 "b" [] [] - 1: R_CALL_ARGUMENTS@56..58 - 0: L_PAREN@56..57 "(" [] [] - 1: R_ARGUMENT_LIST@57..57 - 2: R_PAREN@57..58 ")" [] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@58..68 - 0: R_CALL@58..68 - 0: R_EXTRACT_EXPRESSION@58..66 - 0: R_NAMESPACE_EXPRESSION@58..64 - 0: R_IDENTIFIER@58..61 - 0: IDENT@58..61 "a" [Newline("\n"), Newline("\n")] [] - 1: COLON2@61..63 "::" [] [] - 2: R_IDENTIFIER@63..64 - 0: IDENT@63..64 "b" [] [] - 1: DOLLAR@64..65 "$" [] [] - 2: R_IDENTIFIER@65..66 - 0: IDENT@65..66 "c" [] [] - 1: R_CALL_ARGUMENTS@66..68 - 0: L_PAREN@66..67 "(" [] [] - 1: R_ARGUMENT_LIST@67..67 - 2: R_PAREN@67..68 ")" [] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@68..78 - 0: R_CALL@68..78 - 0: R_EXTRACT_EXPRESSION@68..76 - 0: R_NAMESPACE_EXPRESSION@68..74 - 0: R_IDENTIFIER@68..70 - 0: IDENT@68..70 "a" [Newline("\n")] [] - 1: COLON3@70..73 ":::" [] [] - 2: R_IDENTIFIER@73..74 - 0: IDENT@73..74 "b" [] [] - 1: AT@74..75 "@" [] [] - 2: R_IDENTIFIER@75..76 - 0: IDENT@75..76 "c" [] [] - 1: R_CALL_ARGUMENTS@76..78 - 0: L_PAREN@76..77 "(" [] [] - 1: R_ARGUMENT_LIST@77..77 - 2: R_PAREN@77..78 ")" [] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@78..86 - 0: R_NAMESPACE_EXPRESSION@78..86 - 0: R_IDENTIFIER@78..81 - 0: IDENT@78..81 "a" [Newline("\n"), Newline("\n")] [] - 1: COLON2@81..83 "::" [] [] - 2: R_DOTS@83..86 - 0: DOTS@83..86 "..." [] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@86..94 - 0: R_NAMESPACE_EXPRESSION@86..94 - 0: R_IDENTIFIER@86..88 - 0: IDENT@86..88 "a" [Newline("\n")] [] - 1: COLON3@88..91 ":::" [] [] - 2: R_DOTS@91..94 - 0: DOTS@91..94 "..." [] [] - 1: (empty) - 12: R_EXPRESSION_STATEMENT@94..102 - 0: R_NAMESPACE_EXPRESSION@94..102 - 0: R_DOTS@94..99 - 0: DOTS@94..99 "..." [Newline("\n"), Newline("\n")] [] - 1: COLON2@99..101 "::" [] [] - 2: R_IDENTIFIER@101..102 - 0: IDENT@101..102 "a" [] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@102..110 - 0: R_NAMESPACE_EXPRESSION@102..110 - 0: R_DOTS@102..106 - 0: DOTS@102..106 "..." [Newline("\n")] [] - 1: COLON3@106..109 ":::" [] [] - 2: R_IDENTIFIER@109..110 - 0: IDENT@109..110 "a" [] [] - 1: (empty) - 14: R_EXPRESSION_STATEMENT@110..120 - 0: R_NAMESPACE_EXPRESSION@110..120 - 0: R_DOTS@110..115 - 0: DOTS@110..115 "..." [Newline("\n"), Newline("\n")] [] - 1: COLON2@115..117 "::" [] [] - 2: R_DOTS@117..120 - 0: DOTS@117..120 "..." [] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@120..130 - 0: R_NAMESPACE_EXPRESSION@120..130 - 0: R_DOTS@120..124 - 0: DOTS@120..124 "..." [Newline("\n")] [] - 1: COLON3@124..127 ":::" [] [] - 2: R_DOTS@127..130 - 0: DOTS@127..130 "..." [] [] - 1: (empty) - 16: R_EXPRESSION_STATEMENT@130..138 - 0: R_NAMESPACE_EXPRESSION@130..138 - 0: R_IDENTIFIER@130..133 - 0: IDENT@130..133 "a" [Newline("\n"), Newline("\n")] [] - 1: COLON2@133..135 "::" [] [] - 2: R_DOT_DOT_I@135..138 - 0: DOTDOTI@135..138 "..1" [] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@138..146 - 0: R_NAMESPACE_EXPRESSION@138..146 - 0: R_IDENTIFIER@138..140 - 0: IDENT@138..140 "a" [Newline("\n")] [] - 1: COLON3@140..143 ":::" [] [] - 2: R_DOT_DOT_I@143..146 - 0: DOTDOTI@143..146 "..1" [] [] - 1: (empty) - 18: R_EXPRESSION_STATEMENT@146..154 - 0: R_NAMESPACE_EXPRESSION@146..154 - 0: R_DOT_DOT_I@146..151 - 0: DOTDOTI@146..151 "..1" [Newline("\n"), Newline("\n")] [] - 1: COLON2@151..153 "::" [] [] - 2: R_IDENTIFIER@153..154 - 0: IDENT@153..154 "a" [] [] - 1: (empty) - 19: R_EXPRESSION_STATEMENT@154..162 - 0: R_NAMESPACE_EXPRESSION@154..162 - 0: R_DOT_DOT_I@154..158 - 0: DOTDOTI@154..158 "..1" [Newline("\n")] [] - 1: COLON3@158..161 ":::" [] [] - 2: R_IDENTIFIER@161..162 - 0: IDENT@161..162 "a" [] [] - 1: (empty) - 20: R_EXPRESSION_STATEMENT@162..172 - 0: R_NAMESPACE_EXPRESSION@162..172 - 0: R_DOT_DOT_I@162..167 - 0: DOTDOTI@162..167 "..1" [Newline("\n"), Newline("\n")] [] - 1: COLON2@167..169 "::" [] [] - 2: R_DOT_DOT_I@169..172 - 0: DOTDOTI@169..172 "..1" [] [] - 1: (empty) - 21: R_EXPRESSION_STATEMENT@172..182 - 0: R_NAMESPACE_EXPRESSION@172..182 - 0: R_DOT_DOT_I@172..176 - 0: DOTDOTI@172..176 "..1" [Newline("\n")] [] - 1: COLON3@176..179 ":::" [] [] - 2: R_DOT_DOT_I@179..182 - 0: DOTDOTI@179..182 "..1" [] [] - 1: (empty) - 2: EOF@182..183 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/parenthesized_expression.R b/crates/air_r_parser/tests/snapshots/ok/parenthesized_expression.R deleted file mode 100644 index aee6d4c6..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/parenthesized_expression.R +++ /dev/null @@ -1,13 +0,0 @@ -(1) - -({ - 1 + 1 -}) - -((1)) - -( # comment1 - # comment2 - 1 # comment3 - # comment4 -) # comment5 diff --git a/crates/air_r_parser/tests/snapshots/ok/parenthesized_expression.R.snap b/crates/air_r_parser/tests/snapshots/ok/parenthesized_expression.R.snap deleted file mode 100644 index 921cf8c2..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/parenthesized_expression.R.snap +++ /dev/null @@ -1,144 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -(1) - -({ - 1 + 1 -}) - -((1)) - -( # comment1 - # comment2 - 1 # comment3 - # comment4 -) # comment5 - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RParenthesizedExpression { - l_paren_token: L_PAREN@0..1 "(" [] [], - body: RDoubleValue { - value_token: R_DOUBLE_LITERAL@1..2 "1" [] [], - }, - r_paren_token: R_PAREN@2..3 ")" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RParenthesizedExpression { - l_paren_token: L_PAREN@3..6 "(" [Newline("\n"), Newline("\n")] [], - body: RBracedStatements { - l_curly_token: L_CURLY@6..7 "{" [] [], - statements: RStatementList [ - RExpressionStatement { - expression: RBinaryExpression { - left: RDoubleValue { - value_token: R_DOUBLE_LITERAL@7..12 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: PLUS@12..14 "+" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@14..15 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@15..17 "}" [Newline("\n")] [], - }, - r_paren_token: R_PAREN@17..18 ")" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RParenthesizedExpression { - l_paren_token: L_PAREN@18..21 "(" [Newline("\n"), Newline("\n")] [], - body: RParenthesizedExpression { - l_paren_token: L_PAREN@21..22 "(" [] [], - body: RDoubleValue { - value_token: R_DOUBLE_LITERAL@22..23 "1" [] [], - }, - r_paren_token: R_PAREN@23..24 ")" [] [], - }, - r_paren_token: R_PAREN@24..25 ")" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RParenthesizedExpression { - l_paren_token: L_PAREN@25..39 "(" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# comment1")], - body: RDoubleValue { - value_token: R_DOUBLE_LITERAL@39..67 "1" [Newline("\n"), Whitespace(" "), Comments("# comment2"), Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")], - }, - r_paren_token: R_PAREN@67..93 ")" [Newline("\n"), Whitespace(" "), Comments("# comment4"), Newline("\n")] [Whitespace(" "), Comments("# comment5")], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@93..94 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..94 - 0: (empty) - 1: R_STATEMENT_LIST@0..93 - 0: R_EXPRESSION_STATEMENT@0..3 - 0: R_PARENTHESIZED_EXPRESSION@0..3 - 0: L_PAREN@0..1 "(" [] [] - 1: R_DOUBLE_VALUE@1..2 - 0: R_DOUBLE_LITERAL@1..2 "1" [] [] - 2: R_PAREN@2..3 ")" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@3..18 - 0: R_PARENTHESIZED_EXPRESSION@3..18 - 0: L_PAREN@3..6 "(" [Newline("\n"), Newline("\n")] [] - 1: R_BRACED_STATEMENTS@6..17 - 0: L_CURLY@6..7 "{" [] [] - 1: R_STATEMENT_LIST@7..15 - 0: R_EXPRESSION_STATEMENT@7..15 - 0: R_BINARY_EXPRESSION@7..15 - 0: R_DOUBLE_VALUE@7..12 - 0: R_DOUBLE_LITERAL@7..12 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: PLUS@12..14 "+" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@14..15 - 0: R_DOUBLE_LITERAL@14..15 "1" [] [] - 1: (empty) - 2: R_CURLY@15..17 "}" [Newline("\n")] [] - 2: R_PAREN@17..18 ")" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@18..25 - 0: R_PARENTHESIZED_EXPRESSION@18..25 - 0: L_PAREN@18..21 "(" [Newline("\n"), Newline("\n")] [] - 1: R_PARENTHESIZED_EXPRESSION@21..24 - 0: L_PAREN@21..22 "(" [] [] - 1: R_DOUBLE_VALUE@22..23 - 0: R_DOUBLE_LITERAL@22..23 "1" [] [] - 2: R_PAREN@23..24 ")" [] [] - 2: R_PAREN@24..25 ")" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@25..93 - 0: R_PARENTHESIZED_EXPRESSION@25..93 - 0: L_PAREN@25..39 "(" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# comment1")] - 1: R_DOUBLE_VALUE@39..67 - 0: R_DOUBLE_LITERAL@39..67 "1" [Newline("\n"), Whitespace(" "), Comments("# comment2"), Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")] - 2: R_PAREN@67..93 ")" [Newline("\n"), Whitespace(" "), Comments("# comment4"), Newline("\n")] [Whitespace(" "), Comments("# comment5")] - 1: (empty) - 2: EOF@93..94 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/repeat_statement.R b/crates/air_r_parser/tests/snapshots/ok/repeat_statement.R deleted file mode 100644 index 0d184b0e..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/repeat_statement.R +++ /dev/null @@ -1,8 +0,0 @@ -repeat 1 - -repeat { -} - -repeat { - 1 + 1 -} diff --git a/crates/air_r_parser/tests/snapshots/ok/repeat_statement.R.snap b/crates/air_r_parser/tests/snapshots/ok/repeat_statement.R.snap deleted file mode 100644 index cd17c760..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/repeat_statement.R.snap +++ /dev/null @@ -1,113 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -repeat 1 - -repeat { -} - -repeat { - 1 + 1 -} - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RRepeatStatement { - repeat_token: REPEAT_KW@0..7 "repeat" [] [Whitespace(" ")], - body: RDoubleValue { - value_token: R_DOUBLE_LITERAL@7..8 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RRepeatStatement { - repeat_token: REPEAT_KW@8..17 "repeat" [Newline("\n"), Newline("\n")] [Whitespace(" ")], - body: RBracedStatements { - l_curly_token: L_CURLY@17..18 "{" [] [], - statements: RStatementList [], - r_curly_token: R_CURLY@18..20 "}" [Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RRepeatStatement { - repeat_token: REPEAT_KW@20..29 "repeat" [Newline("\n"), Newline("\n")] [Whitespace(" ")], - body: RBracedStatements { - l_curly_token: L_CURLY@29..30 "{" [] [], - statements: RStatementList [ - RExpressionStatement { - expression: RBinaryExpression { - left: RDoubleValue { - value_token: R_DOUBLE_LITERAL@30..35 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], - }, - operator: PLUS@35..37 "+" [] [Whitespace(" ")], - right: RDoubleValue { - value_token: R_DOUBLE_LITERAL@37..38 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@38..40 "}" [Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@40..41 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..41 - 0: (empty) - 1: R_STATEMENT_LIST@0..40 - 0: R_EXPRESSION_STATEMENT@0..8 - 0: R_REPEAT_STATEMENT@0..8 - 0: REPEAT_KW@0..7 "repeat" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@7..8 - 0: R_DOUBLE_LITERAL@7..8 "1" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@8..20 - 0: R_REPEAT_STATEMENT@8..20 - 0: REPEAT_KW@8..17 "repeat" [Newline("\n"), Newline("\n")] [Whitespace(" ")] - 1: R_BRACED_STATEMENTS@17..20 - 0: L_CURLY@17..18 "{" [] [] - 1: R_STATEMENT_LIST@18..18 - 2: R_CURLY@18..20 "}" [Newline("\n")] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@20..40 - 0: R_REPEAT_STATEMENT@20..40 - 0: REPEAT_KW@20..29 "repeat" [Newline("\n"), Newline("\n")] [Whitespace(" ")] - 1: R_BRACED_STATEMENTS@29..40 - 0: L_CURLY@29..30 "{" [] [] - 1: R_STATEMENT_LIST@30..38 - 0: R_EXPRESSION_STATEMENT@30..38 - 0: R_BINARY_EXPRESSION@30..38 - 0: R_DOUBLE_VALUE@30..35 - 0: R_DOUBLE_LITERAL@30..35 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")] - 1: PLUS@35..37 "+" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@37..38 - 0: R_DOUBLE_LITERAL@37..38 "1" [] [] - 1: (empty) - 2: R_CURLY@38..40 "}" [Newline("\n")] [] - 1: (empty) - 2: EOF@40..41 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-01.R b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-01.R deleted file mode 100644 index 106a966b..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-01.R +++ /dev/null @@ -1,2 +0,0 @@ -# https://github.com/posit-dev/air/issues/238 -1; diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-01.R.snap b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-01.R.snap deleted file mode 100644 index b27be4c2..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-01.R.snap +++ /dev/null @@ -1,43 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -# https://github.com/posit-dev/air/issues/238 -1; - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@0..48 "1" [Comments("# https://github.com/ ..."), Newline("\n")] [Whitespace(";")], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@48..49 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..49 - 0: (empty) - 1: R_STATEMENT_LIST@0..48 - 0: R_EXPRESSION_STATEMENT@0..48 - 0: R_DOUBLE_VALUE@0..48 - 0: R_DOUBLE_LITERAL@0..48 "1" [Comments("# https://github.com/ ..."), Newline("\n")] [Whitespace(";")] - 1: (empty) - 2: EOF@48..49 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-02.R b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-02.R deleted file mode 100644 index bdf81218..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-02.R +++ /dev/null @@ -1 +0,0 @@ -1;; diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-02.R.snap b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-02.R.snap deleted file mode 100644 index b91cb0c1..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-02.R.snap +++ /dev/null @@ -1,42 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -1;; - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@0..3 "1" [] [Whitespace(";;")], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@3..4 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..4 - 0: (empty) - 1: R_STATEMENT_LIST@0..3 - 0: R_EXPRESSION_STATEMENT@0..3 - 0: R_DOUBLE_VALUE@0..3 - 0: R_DOUBLE_LITERAL@0..3 "1" [] [Whitespace(";;")] - 1: (empty) - 2: EOF@3..4 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-03.R b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-03.R deleted file mode 100644 index 5c099ac5..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-03.R +++ /dev/null @@ -1,3 +0,0 @@ -1 - -; diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-03.R.snap b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-03.R.snap deleted file mode 100644 index ba315843..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-end-of-file-03.R.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -1 - -; - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@0..1 "1" [] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@1..5 "" [Newline("\n"), Newline("\n"), Whitespace(";"), Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..5 - 0: (empty) - 1: R_STATEMENT_LIST@0..1 - 0: R_EXPRESSION_STATEMENT@0..1 - 0: R_DOUBLE_VALUE@0..1 - 0: R_DOUBLE_LITERAL@0..1 "1" [] [] - 1: (empty) - 2: EOF@1..5 "" [Newline("\n"), Newline("\n"), Whitespace(";"), Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-01.R b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-01.R deleted file mode 100644 index c203e438..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-01.R +++ /dev/null @@ -1,2 +0,0 @@ -; -1 diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-01.R.snap b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-01.R.snap deleted file mode 100644 index f7db41e7..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-01.R.snap +++ /dev/null @@ -1,43 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -; -1 - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@0..3 "1" [Whitespace(";"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@3..4 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..4 - 0: (empty) - 1: R_STATEMENT_LIST@0..3 - 0: R_EXPRESSION_STATEMENT@0..3 - 0: R_DOUBLE_VALUE@0..3 - 0: R_DOUBLE_LITERAL@0..3 "1" [Whitespace(";"), Newline("\n")] [] - 1: (empty) - 2: EOF@3..4 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-02.R b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-02.R deleted file mode 100644 index 17d762e3..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-02.R +++ /dev/null @@ -1 +0,0 @@ -;1 diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-02.R.snap b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-02.R.snap deleted file mode 100644 index ec97c801..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolon-start-of-file-02.R.snap +++ /dev/null @@ -1,42 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -;1 - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@0..2 "1" [Whitespace(";")] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@2..3 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..3 - 0: (empty) - 1: R_STATEMENT_LIST@0..2 - 0: R_EXPRESSION_STATEMENT@0..2 - 0: R_DOUBLE_VALUE@0..2 - 0: R_DOUBLE_LITERAL@0..2 "1" [Whitespace(";")] [] - 1: (empty) - 2: EOF@2..3 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolons.R b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolons.R deleted file mode 100644 index 9acbe439..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolons.R +++ /dev/null @@ -1,16 +0,0 @@ -1;2 - -1 ; 2 - -1;;2 - -{ - 1; -} - -{ - 1; 2 -} - -1;# comment -1; # comment diff --git a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolons.R.snap b/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolons.R.snap deleted file mode 100644 index 66d34985..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/semicolons/semicolons.R.snap +++ /dev/null @@ -1,187 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -1;2 - -1 ; 2 - -1;;2 - -{ - 1; -} - -{ - 1; 2 -} - -1;# comment -1; # comment - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@0..2 "1" [] [Whitespace(";")], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@2..3 "2" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@3..9 "1" [Newline("\n"), Newline("\n")] [Whitespace(" ; ")], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@9..10 "2" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@10..15 "1" [Newline("\n"), Newline("\n")] [Whitespace(";;")], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@15..16 "2" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@16..19 "{" [Newline("\n"), Newline("\n")] [], - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@19..26 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(";")], - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@26..28 "}" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@28..31 "{" [Newline("\n"), Newline("\n")] [], - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@31..39 "1" [Newline("\n"), Whitespace(" ")] [Whitespace("; ")], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@39..40 "2" [] [], - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@40..42 "}" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@42..55 "1" [Newline("\n"), Newline("\n")] [Whitespace(";"), Comments("# comment")], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@55..68 "1" [Newline("\n")] [Whitespace("; "), Comments("# comment")], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@68..69 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..69 - 0: (empty) - 1: R_STATEMENT_LIST@0..68 - 0: R_EXPRESSION_STATEMENT@0..2 - 0: R_DOUBLE_VALUE@0..2 - 0: R_DOUBLE_LITERAL@0..2 "1" [] [Whitespace(";")] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@2..3 - 0: R_DOUBLE_VALUE@2..3 - 0: R_DOUBLE_LITERAL@2..3 "2" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@3..9 - 0: R_DOUBLE_VALUE@3..9 - 0: R_DOUBLE_LITERAL@3..9 "1" [Newline("\n"), Newline("\n")] [Whitespace(" ; ")] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@9..10 - 0: R_DOUBLE_VALUE@9..10 - 0: R_DOUBLE_LITERAL@9..10 "2" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@10..15 - 0: R_DOUBLE_VALUE@10..15 - 0: R_DOUBLE_LITERAL@10..15 "1" [Newline("\n"), Newline("\n")] [Whitespace(";;")] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@15..16 - 0: R_DOUBLE_VALUE@15..16 - 0: R_DOUBLE_LITERAL@15..16 "2" [] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@16..28 - 0: R_BRACED_STATEMENTS@16..28 - 0: L_CURLY@16..19 "{" [Newline("\n"), Newline("\n")] [] - 1: R_STATEMENT_LIST@19..26 - 0: R_EXPRESSION_STATEMENT@19..26 - 0: R_DOUBLE_VALUE@19..26 - 0: R_DOUBLE_LITERAL@19..26 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(";")] - 1: (empty) - 2: R_CURLY@26..28 "}" [Newline("\n")] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@28..42 - 0: R_BRACED_STATEMENTS@28..42 - 0: L_CURLY@28..31 "{" [Newline("\n"), Newline("\n")] [] - 1: R_STATEMENT_LIST@31..40 - 0: R_EXPRESSION_STATEMENT@31..39 - 0: R_DOUBLE_VALUE@31..39 - 0: R_DOUBLE_LITERAL@31..39 "1" [Newline("\n"), Whitespace(" ")] [Whitespace("; ")] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@39..40 - 0: R_DOUBLE_VALUE@39..40 - 0: R_DOUBLE_LITERAL@39..40 "2" [] [] - 1: (empty) - 2: R_CURLY@40..42 "}" [Newline("\n")] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@42..55 - 0: R_DOUBLE_VALUE@42..55 - 0: R_DOUBLE_LITERAL@42..55 "1" [Newline("\n"), Newline("\n")] [Whitespace(";"), Comments("# comment")] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@55..68 - 0: R_DOUBLE_VALUE@55..68 - 0: R_DOUBLE_LITERAL@55..68 "1" [Newline("\n")] [Whitespace("; "), Comments("# comment")] - 1: (empty) - 2: EOF@68..69 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/subset2.R b/crates/air_r_parser/tests/snapshots/ok/subset2.R deleted file mode 100644 index e9e13a0c..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/subset2.R +++ /dev/null @@ -1,41 +0,0 @@ -fn -fn[[a]] -fn[[a, b]] -fn[[...]] -fn[[a, ..., b]] - -fn[[a = 1]] -fn[[a = ]] - -fn[[a = 1, b = 2]] -fn[[a = , b = 2]] - -fn[[... = 1]] -fn[[... =]] - -fn[["arg" = 1]] -fn[["arg" =]] - -# Comma tests -fn[[,]] -fn[[,,,]] -fn[[,,a,,b,,]] - -# Comment tests -fn[[ - # comment -]] -fn[[, - # comment -,]] -fn[[ - a, # comment1 - b # comment2 -]] -fn[[ - a # comment1 - = # comment2 - 1 # comment3 -]] - -{expr}[[a = 1]] diff --git a/crates/air_r_parser/tests/snapshots/ok/unary_expressions.R b/crates/air_r_parser/tests/snapshots/ok/unary_expressions.R deleted file mode 100644 index f1af856f..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/unary_expressions.R +++ /dev/null @@ -1,18 +0,0 @@ -+1 --1 -~1 -?1 -!1 - -+ -1 - -+ # Comment -1 - -+ -# Comment -+ # Comment -1 - -1 + ++1 diff --git a/crates/air_r_parser/tests/snapshots/ok/unary_expressions.R.snap b/crates/air_r_parser/tests/snapshots/ok/unary_expressions.R.snap deleted file mode 100644 index 8ccb35aa..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/unary_expressions.R.snap +++ /dev/null @@ -1,204 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -+1 --1 -~1 -?1 -!1 - -+ -1 - -+ # Comment -1 - -+ -# Comment -+ # Comment -1 - -1 + ++1 - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RUnaryExpression { - operator: PLUS@0..1 "+" [] [], - argument: RDoubleValue { - value_token: R_DOUBLE_LITERAL@1..2 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RUnaryExpression { - operator: MINUS@2..4 "-" [Newline("\n")] [], - argument: RDoubleValue { - value_token: R_DOUBLE_LITERAL@4..5 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RUnaryExpression { - operator: TILDE@5..7 "~" [Newline("\n")] [], - argument: RDoubleValue { - value_token: R_DOUBLE_LITERAL@7..8 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RUnaryExpression { - operator: WAT@8..10 "?" [Newline("\n")] [], - argument: RDoubleValue { - value_token: R_DOUBLE_LITERAL@10..11 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RUnaryExpression { - operator: BANG@11..13 "!" [Newline("\n")] [], - argument: RDoubleValue { - value_token: R_DOUBLE_LITERAL@13..14 "1" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RUnaryExpression { - operator: PLUS@14..17 "+" [Newline("\n"), Newline("\n")] [], - argument: RDoubleValue { - value_token: R_DOUBLE_LITERAL@17..19 "1" [Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RUnaryExpression { - operator: PLUS@19..32 "+" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# Comment")], - argument: RDoubleValue { - value_token: R_DOUBLE_LITERAL@32..34 "1" [Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RUnaryExpression { - operator: PLUS@34..37 "+" [Newline("\n"), Newline("\n")] [], - argument: RUnaryExpression { - operator: PLUS@37..59 "+" [Newline("\n"), Comments("# Comment"), Newline("\n")] [Whitespace(" "), Comments("# Comment")], - argument: RDoubleValue { - value_token: R_DOUBLE_LITERAL@59..61 "1" [Newline("\n")] [], - }, - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBinaryExpression { - left: RDoubleValue { - value_token: R_DOUBLE_LITERAL@61..65 "1" [Newline("\n"), Newline("\n")] [Whitespace(" ")], - }, - operator: PLUS@65..67 "+" [] [Whitespace(" ")], - right: RUnaryExpression { - operator: PLUS@67..68 "+" [] [], - argument: RUnaryExpression { - operator: PLUS@68..69 "+" [] [], - argument: RDoubleValue { - value_token: R_DOUBLE_LITERAL@69..70 "1" [] [], - }, - }, - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@70..71 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..71 - 0: (empty) - 1: R_STATEMENT_LIST@0..70 - 0: R_EXPRESSION_STATEMENT@0..2 - 0: R_UNARY_EXPRESSION@0..2 - 0: PLUS@0..1 "+" [] [] - 1: R_DOUBLE_VALUE@1..2 - 0: R_DOUBLE_LITERAL@1..2 "1" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@2..5 - 0: R_UNARY_EXPRESSION@2..5 - 0: MINUS@2..4 "-" [Newline("\n")] [] - 1: R_DOUBLE_VALUE@4..5 - 0: R_DOUBLE_LITERAL@4..5 "1" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@5..8 - 0: R_UNARY_EXPRESSION@5..8 - 0: TILDE@5..7 "~" [Newline("\n")] [] - 1: R_DOUBLE_VALUE@7..8 - 0: R_DOUBLE_LITERAL@7..8 "1" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@8..11 - 0: R_UNARY_EXPRESSION@8..11 - 0: WAT@8..10 "?" [Newline("\n")] [] - 1: R_DOUBLE_VALUE@10..11 - 0: R_DOUBLE_LITERAL@10..11 "1" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@11..14 - 0: R_UNARY_EXPRESSION@11..14 - 0: BANG@11..13 "!" [Newline("\n")] [] - 1: R_DOUBLE_VALUE@13..14 - 0: R_DOUBLE_LITERAL@13..14 "1" [] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@14..19 - 0: R_UNARY_EXPRESSION@14..19 - 0: PLUS@14..17 "+" [Newline("\n"), Newline("\n")] [] - 1: R_DOUBLE_VALUE@17..19 - 0: R_DOUBLE_LITERAL@17..19 "1" [Newline("\n")] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@19..34 - 0: R_UNARY_EXPRESSION@19..34 - 0: PLUS@19..32 "+" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# Comment")] - 1: R_DOUBLE_VALUE@32..34 - 0: R_DOUBLE_LITERAL@32..34 "1" [Newline("\n")] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@34..61 - 0: R_UNARY_EXPRESSION@34..61 - 0: PLUS@34..37 "+" [Newline("\n"), Newline("\n")] [] - 1: R_UNARY_EXPRESSION@37..61 - 0: PLUS@37..59 "+" [Newline("\n"), Comments("# Comment"), Newline("\n")] [Whitespace(" "), Comments("# Comment")] - 1: R_DOUBLE_VALUE@59..61 - 0: R_DOUBLE_LITERAL@59..61 "1" [Newline("\n")] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@61..70 - 0: R_BINARY_EXPRESSION@61..70 - 0: R_DOUBLE_VALUE@61..65 - 0: R_DOUBLE_LITERAL@61..65 "1" [Newline("\n"), Newline("\n")] [Whitespace(" ")] - 1: PLUS@65..67 "+" [] [Whitespace(" ")] - 2: R_UNARY_EXPRESSION@67..70 - 0: PLUS@67..68 "+" [] [] - 1: R_UNARY_EXPRESSION@68..70 - 0: PLUS@68..69 "+" [] [] - 1: R_DOUBLE_VALUE@69..70 - 0: R_DOUBLE_LITERAL@69..70 "1" [] [] - 1: (empty) - 2: EOF@70..71 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/value/complex_value.R b/crates/air_r_parser/tests/snapshots/ok/value/complex_value.R deleted file mode 100644 index 212a47e9..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/value/complex_value.R +++ /dev/null @@ -1,7 +0,0 @@ -1i -2.5i -1e6i -0x123Fi - -# https://github.com/r-lib/tree-sitter-r/pull/198 -.1i diff --git a/crates/air_r_parser/tests/snapshots/ok/value/complex_value.R.snap b/crates/air_r_parser/tests/snapshots/ok/value/complex_value.R.snap deleted file mode 100644 index 389e1451..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/value/complex_value.R.snap +++ /dev/null @@ -1,88 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -1i -2.5i -1e6i -0x123Fi - -# https://github.com/r-lib/tree-sitter-r/pull/198 -.1i - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RComplexValue { - value_token: R_COMPLEX_LITERAL@0..2 "1i" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RComplexValue { - value_token: R_COMPLEX_LITERAL@2..7 "2.5i" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RComplexValue { - value_token: R_COMPLEX_LITERAL@7..12 "1e6i" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RComplexValue { - value_token: R_COMPLEX_LITERAL@12..20 "0x123Fi" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RComplexValue { - value_token: R_COMPLEX_LITERAL@20..75 ".1i" [Newline("\n"), Newline("\n"), Comments("# https://github.com/ ..."), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@75..76 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..76 - 0: (empty) - 1: R_STATEMENT_LIST@0..75 - 0: R_EXPRESSION_STATEMENT@0..2 - 0: R_COMPLEX_VALUE@0..2 - 0: R_COMPLEX_LITERAL@0..2 "1i" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@2..7 - 0: R_COMPLEX_VALUE@2..7 - 0: R_COMPLEX_LITERAL@2..7 "2.5i" [Newline("\n")] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@7..12 - 0: R_COMPLEX_VALUE@7..12 - 0: R_COMPLEX_LITERAL@7..12 "1e6i" [Newline("\n")] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@12..20 - 0: R_COMPLEX_VALUE@12..20 - 0: R_COMPLEX_LITERAL@12..20 "0x123Fi" [Newline("\n")] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@20..75 - 0: R_COMPLEX_VALUE@20..75 - 0: R_COMPLEX_LITERAL@20..75 ".1i" [Newline("\n"), Newline("\n"), Comments("# https://github.com/ ..."), Newline("\n")] [] - 1: (empty) - 2: EOF@75..76 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/value/double_value.R b/crates/air_r_parser/tests/snapshots/ok/value/double_value.R deleted file mode 100644 index c26d2c4e..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/value/double_value.R +++ /dev/null @@ -1,37 +0,0 @@ -1 -2.5 -1e6 -0x123F - -# https://github.com/r-lib/tree-sitter-r/pull/198 -.1 - -# Hexadecimal -# `x` vs `X` -0x123 -0X123 -# Numbers and letters -0xDEAD -0XDEAD -0x1f2F3 -0X1f2F3 -# `p` vs `P` -0x0p0 -0x0P0 -0x0p123 -0x0P123 -# `+` and `-` -0x0p+0 -0x0p-0 -0x0p+123 -0x0p-123 -# Decimal point -0x. -0x1. -0x.1 -0x1.1 -# Decimal point with exponent -0x.p1 -0x1.p1 -0x.1p1 -0x1.1p1 \ No newline at end of file diff --git a/crates/air_r_parser/tests/snapshots/ok/value/double_value.R.snap b/crates/air_r_parser/tests/snapshots/ok/value/double_value.R.snap deleted file mode 100644 index ebc3d42e..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/value/double_value.R.snap +++ /dev/null @@ -1,337 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -1 -2.5 -1e6 -0x123F - -# https://github.com/r-lib/tree-sitter-r/pull/198 -.1 - -# Hexadecimal -# `x` vs `X` -0x123 -0X123 -# Numbers and letters -0xDEAD -0XDEAD -0x1f2F3 -0X1f2F3 -# `p` vs `P` -0x0p0 -0x0P0 -0x0p123 -0x0P123 -# `+` and `-` -0x0p+0 -0x0p-0 -0x0p+123 -0x0p-123 -# Decimal point -0x. -0x1. -0x.1 -0x1.1 -# Decimal point with exponent -0x.p1 -0x1.p1 -0x.1p1 -0x1.1p1 -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@0..1 "1" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@1..5 "2.5" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@5..9 "1e6" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@9..16 "0x123F" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@16..70 ".1" [Newline("\n"), Newline("\n"), Comments("# https://github.com/ ..."), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@70..104 "0x123" [Newline("\n"), Newline("\n"), Comments("# Hexadecimal"), Newline("\n"), Comments("# `x` vs `X`"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@104..110 "0X123" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@110..139 "0xDEAD" [Newline("\n"), Comments("# Numbers and letters"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@139..146 "0XDEAD" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@146..154 "0x1f2F3" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@154..162 "0X1f2F3" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@162..181 "0x0p0" [Newline("\n"), Comments("# `p` vs `P`"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@181..187 "0x0P0" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@187..195 "0x0p123" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@195..203 "0x0P123" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@203..224 "0x0p+0" [Newline("\n"), Comments("# `+` and `-`"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@224..231 "0x0p-0" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@231..240 "0x0p+123" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@240..249 "0x0p-123" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@249..269 "0x." [Newline("\n"), Comments("# Decimal point"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@269..274 "0x1." [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@274..279 "0x.1" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@279..285 "0x1.1" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@285..321 "0x.p1" [Newline("\n"), Comments("# Decimal point with ..."), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@321..328 "0x1.p1" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@328..335 "0x.1p1" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@335..343 "0x1.1p1" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@343..343 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..343 - 0: (empty) - 1: R_STATEMENT_LIST@0..343 - 0: R_EXPRESSION_STATEMENT@0..1 - 0: R_DOUBLE_VALUE@0..1 - 0: R_DOUBLE_LITERAL@0..1 "1" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@1..5 - 0: R_DOUBLE_VALUE@1..5 - 0: R_DOUBLE_LITERAL@1..5 "2.5" [Newline("\n")] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@5..9 - 0: R_DOUBLE_VALUE@5..9 - 0: R_DOUBLE_LITERAL@5..9 "1e6" [Newline("\n")] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@9..16 - 0: R_DOUBLE_VALUE@9..16 - 0: R_DOUBLE_LITERAL@9..16 "0x123F" [Newline("\n")] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@16..70 - 0: R_DOUBLE_VALUE@16..70 - 0: R_DOUBLE_LITERAL@16..70 ".1" [Newline("\n"), Newline("\n"), Comments("# https://github.com/ ..."), Newline("\n")] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@70..104 - 0: R_DOUBLE_VALUE@70..104 - 0: R_DOUBLE_LITERAL@70..104 "0x123" [Newline("\n"), Newline("\n"), Comments("# Hexadecimal"), Newline("\n"), Comments("# `x` vs `X`"), Newline("\n")] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@104..110 - 0: R_DOUBLE_VALUE@104..110 - 0: R_DOUBLE_LITERAL@104..110 "0X123" [Newline("\n")] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@110..139 - 0: R_DOUBLE_VALUE@110..139 - 0: R_DOUBLE_LITERAL@110..139 "0xDEAD" [Newline("\n"), Comments("# Numbers and letters"), Newline("\n")] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@139..146 - 0: R_DOUBLE_VALUE@139..146 - 0: R_DOUBLE_LITERAL@139..146 "0XDEAD" [Newline("\n")] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@146..154 - 0: R_DOUBLE_VALUE@146..154 - 0: R_DOUBLE_LITERAL@146..154 "0x1f2F3" [Newline("\n")] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@154..162 - 0: R_DOUBLE_VALUE@154..162 - 0: R_DOUBLE_LITERAL@154..162 "0X1f2F3" [Newline("\n")] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@162..181 - 0: R_DOUBLE_VALUE@162..181 - 0: R_DOUBLE_LITERAL@162..181 "0x0p0" [Newline("\n"), Comments("# `p` vs `P`"), Newline("\n")] [] - 1: (empty) - 12: R_EXPRESSION_STATEMENT@181..187 - 0: R_DOUBLE_VALUE@181..187 - 0: R_DOUBLE_LITERAL@181..187 "0x0P0" [Newline("\n")] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@187..195 - 0: R_DOUBLE_VALUE@187..195 - 0: R_DOUBLE_LITERAL@187..195 "0x0p123" [Newline("\n")] [] - 1: (empty) - 14: R_EXPRESSION_STATEMENT@195..203 - 0: R_DOUBLE_VALUE@195..203 - 0: R_DOUBLE_LITERAL@195..203 "0x0P123" [Newline("\n")] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@203..224 - 0: R_DOUBLE_VALUE@203..224 - 0: R_DOUBLE_LITERAL@203..224 "0x0p+0" [Newline("\n"), Comments("# `+` and `-`"), Newline("\n")] [] - 1: (empty) - 16: R_EXPRESSION_STATEMENT@224..231 - 0: R_DOUBLE_VALUE@224..231 - 0: R_DOUBLE_LITERAL@224..231 "0x0p-0" [Newline("\n")] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@231..240 - 0: R_DOUBLE_VALUE@231..240 - 0: R_DOUBLE_LITERAL@231..240 "0x0p+123" [Newline("\n")] [] - 1: (empty) - 18: R_EXPRESSION_STATEMENT@240..249 - 0: R_DOUBLE_VALUE@240..249 - 0: R_DOUBLE_LITERAL@240..249 "0x0p-123" [Newline("\n")] [] - 1: (empty) - 19: R_EXPRESSION_STATEMENT@249..269 - 0: R_DOUBLE_VALUE@249..269 - 0: R_DOUBLE_LITERAL@249..269 "0x." [Newline("\n"), Comments("# Decimal point"), Newline("\n")] [] - 1: (empty) - 20: R_EXPRESSION_STATEMENT@269..274 - 0: R_DOUBLE_VALUE@269..274 - 0: R_DOUBLE_LITERAL@269..274 "0x1." [Newline("\n")] [] - 1: (empty) - 21: R_EXPRESSION_STATEMENT@274..279 - 0: R_DOUBLE_VALUE@274..279 - 0: R_DOUBLE_LITERAL@274..279 "0x.1" [Newline("\n")] [] - 1: (empty) - 22: R_EXPRESSION_STATEMENT@279..285 - 0: R_DOUBLE_VALUE@279..285 - 0: R_DOUBLE_LITERAL@279..285 "0x1.1" [Newline("\n")] [] - 1: (empty) - 23: R_EXPRESSION_STATEMENT@285..321 - 0: R_DOUBLE_VALUE@285..321 - 0: R_DOUBLE_LITERAL@285..321 "0x.p1" [Newline("\n"), Comments("# Decimal point with ..."), Newline("\n")] [] - 1: (empty) - 24: R_EXPRESSION_STATEMENT@321..328 - 0: R_DOUBLE_VALUE@321..328 - 0: R_DOUBLE_LITERAL@321..328 "0x1.p1" [Newline("\n")] [] - 1: (empty) - 25: R_EXPRESSION_STATEMENT@328..335 - 0: R_DOUBLE_VALUE@328..335 - 0: R_DOUBLE_LITERAL@328..335 "0x.1p1" [Newline("\n")] [] - 1: (empty) - 26: R_EXPRESSION_STATEMENT@335..343 - 0: R_DOUBLE_VALUE@335..343 - 0: R_DOUBLE_LITERAL@335..343 "0x1.1p1" [Newline("\n")] [] - 1: (empty) - 2: EOF@343..343 "" [] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/value/integer_value.R b/crates/air_r_parser/tests/snapshots/ok/value/integer_value.R deleted file mode 100644 index 7bd3aa3c..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/value/integer_value.R +++ /dev/null @@ -1,35 +0,0 @@ -1L -1e5L - -# https://github.com/r-lib/tree-sitter-r/pull/198 -.1L - -# Hexadecimal -# `x` vs `X` -0x123L -0X123L -# Numbers and letters -0xDEADL -0XDEADL -0x1f2F3L -0X1f2F3L -# `p` vs `P` -0x0p0L -0x0P0L -0x0p123L -0x0P123L -# `+` and `-` -0x0p+0L -0x0p-0L -0x0p+123L -0x0p-123L -# Decimal point -0x.L -0x1.L -0x.1L -0x1.1L -# Decimal point with exponent -0x.p1L -0x1.p1L -0x.1p1L -0x1.1p1L \ No newline at end of file diff --git a/crates/air_r_parser/tests/snapshots/ok/value/integer_value.R.snap b/crates/air_r_parser/tests/snapshots/ok/value/integer_value.R.snap deleted file mode 100644 index 2df3acf3..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/value/integer_value.R.snap +++ /dev/null @@ -1,315 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -1L -1e5L - -# https://github.com/r-lib/tree-sitter-r/pull/198 -.1L - -# Hexadecimal -# `x` vs `X` -0x123L -0X123L -# Numbers and letters -0xDEADL -0XDEADL -0x1f2F3L -0X1f2F3L -# `p` vs `P` -0x0p0L -0x0P0L -0x0p123L -0x0P123L -# `+` and `-` -0x0p+0L -0x0p-0L -0x0p+123L -0x0p-123L -# Decimal point -0x.L -0x1.L -0x.1L -0x1.1L -# Decimal point with exponent -0x.p1L -0x1.p1L -0x.1p1L -0x1.1p1L -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@0..2 "1L" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@2..7 "1e5L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@7..62 ".1L" [Newline("\n"), Newline("\n"), Comments("# https://github.com/ ..."), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@62..97 "0x123L" [Newline("\n"), Newline("\n"), Comments("# Hexadecimal"), Newline("\n"), Comments("# `x` vs `X`"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@97..104 "0X123L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@104..134 "0xDEADL" [Newline("\n"), Comments("# Numbers and letters"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@134..142 "0XDEADL" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@142..151 "0x1f2F3L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@151..160 "0X1f2F3L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@160..180 "0x0p0L" [Newline("\n"), Comments("# `p` vs `P`"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@180..187 "0x0P0L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@187..196 "0x0p123L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@196..205 "0x0P123L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@205..227 "0x0p+0L" [Newline("\n"), Comments("# `+` and `-`"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@227..235 "0x0p-0L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@235..245 "0x0p+123L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@245..255 "0x0p-123L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@255..276 "0x.L" [Newline("\n"), Comments("# Decimal point"), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@276..282 "0x1.L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@282..288 "0x.1L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@288..295 "0x1.1L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@295..332 "0x.p1L" [Newline("\n"), Comments("# Decimal point with ..."), Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@332..340 "0x1.p1L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@340..348 "0x.1p1L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RIntegerValue { - value_token: R_INTEGER_LITERAL@348..357 "0x1.1p1L" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@357..357 "" [] [], -} -``` - -## CST - -``` -0: R_ROOT@0..357 - 0: (empty) - 1: R_STATEMENT_LIST@0..357 - 0: R_EXPRESSION_STATEMENT@0..2 - 0: R_INTEGER_VALUE@0..2 - 0: R_INTEGER_LITERAL@0..2 "1L" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@2..7 - 0: R_INTEGER_VALUE@2..7 - 0: R_INTEGER_LITERAL@2..7 "1e5L" [Newline("\n")] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@7..62 - 0: R_INTEGER_VALUE@7..62 - 0: R_INTEGER_LITERAL@7..62 ".1L" [Newline("\n"), Newline("\n"), Comments("# https://github.com/ ..."), Newline("\n")] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@62..97 - 0: R_INTEGER_VALUE@62..97 - 0: R_INTEGER_LITERAL@62..97 "0x123L" [Newline("\n"), Newline("\n"), Comments("# Hexadecimal"), Newline("\n"), Comments("# `x` vs `X`"), Newline("\n")] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@97..104 - 0: R_INTEGER_VALUE@97..104 - 0: R_INTEGER_LITERAL@97..104 "0X123L" [Newline("\n")] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@104..134 - 0: R_INTEGER_VALUE@104..134 - 0: R_INTEGER_LITERAL@104..134 "0xDEADL" [Newline("\n"), Comments("# Numbers and letters"), Newline("\n")] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@134..142 - 0: R_INTEGER_VALUE@134..142 - 0: R_INTEGER_LITERAL@134..142 "0XDEADL" [Newline("\n")] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@142..151 - 0: R_INTEGER_VALUE@142..151 - 0: R_INTEGER_LITERAL@142..151 "0x1f2F3L" [Newline("\n")] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@151..160 - 0: R_INTEGER_VALUE@151..160 - 0: R_INTEGER_LITERAL@151..160 "0X1f2F3L" [Newline("\n")] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@160..180 - 0: R_INTEGER_VALUE@160..180 - 0: R_INTEGER_LITERAL@160..180 "0x0p0L" [Newline("\n"), Comments("# `p` vs `P`"), Newline("\n")] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@180..187 - 0: R_INTEGER_VALUE@180..187 - 0: R_INTEGER_LITERAL@180..187 "0x0P0L" [Newline("\n")] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@187..196 - 0: R_INTEGER_VALUE@187..196 - 0: R_INTEGER_LITERAL@187..196 "0x0p123L" [Newline("\n")] [] - 1: (empty) - 12: R_EXPRESSION_STATEMENT@196..205 - 0: R_INTEGER_VALUE@196..205 - 0: R_INTEGER_LITERAL@196..205 "0x0P123L" [Newline("\n")] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@205..227 - 0: R_INTEGER_VALUE@205..227 - 0: R_INTEGER_LITERAL@205..227 "0x0p+0L" [Newline("\n"), Comments("# `+` and `-`"), Newline("\n")] [] - 1: (empty) - 14: R_EXPRESSION_STATEMENT@227..235 - 0: R_INTEGER_VALUE@227..235 - 0: R_INTEGER_LITERAL@227..235 "0x0p-0L" [Newline("\n")] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@235..245 - 0: R_INTEGER_VALUE@235..245 - 0: R_INTEGER_LITERAL@235..245 "0x0p+123L" [Newline("\n")] [] - 1: (empty) - 16: R_EXPRESSION_STATEMENT@245..255 - 0: R_INTEGER_VALUE@245..255 - 0: R_INTEGER_LITERAL@245..255 "0x0p-123L" [Newline("\n")] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@255..276 - 0: R_INTEGER_VALUE@255..276 - 0: R_INTEGER_LITERAL@255..276 "0x.L" [Newline("\n"), Comments("# Decimal point"), Newline("\n")] [] - 1: (empty) - 18: R_EXPRESSION_STATEMENT@276..282 - 0: R_INTEGER_VALUE@276..282 - 0: R_INTEGER_LITERAL@276..282 "0x1.L" [Newline("\n")] [] - 1: (empty) - 19: R_EXPRESSION_STATEMENT@282..288 - 0: R_INTEGER_VALUE@282..288 - 0: R_INTEGER_LITERAL@282..288 "0x.1L" [Newline("\n")] [] - 1: (empty) - 20: R_EXPRESSION_STATEMENT@288..295 - 0: R_INTEGER_VALUE@288..295 - 0: R_INTEGER_LITERAL@288..295 "0x1.1L" [Newline("\n")] [] - 1: (empty) - 21: R_EXPRESSION_STATEMENT@295..332 - 0: R_INTEGER_VALUE@295..332 - 0: R_INTEGER_LITERAL@295..332 "0x.p1L" [Newline("\n"), Comments("# Decimal point with ..."), Newline("\n")] [] - 1: (empty) - 22: R_EXPRESSION_STATEMENT@332..340 - 0: R_INTEGER_VALUE@332..340 - 0: R_INTEGER_LITERAL@332..340 "0x1.p1L" [Newline("\n")] [] - 1: (empty) - 23: R_EXPRESSION_STATEMENT@340..348 - 0: R_INTEGER_VALUE@340..348 - 0: R_INTEGER_LITERAL@340..348 "0x.1p1L" [Newline("\n")] [] - 1: (empty) - 24: R_EXPRESSION_STATEMENT@348..357 - 0: R_INTEGER_VALUE@348..357 - 0: R_INTEGER_LITERAL@348..357 "0x1.1p1L" [Newline("\n")] [] - 1: (empty) - 2: EOF@357..357 "" [] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/value/string_value.R b/crates/air_r_parser/tests/snapshots/ok/value/string_value.R deleted file mode 100644 index 9343f245..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/value/string_value.R +++ /dev/null @@ -1,33 +0,0 @@ -"hi there!" -"\"" -'hi there!' -'\'' -"'" -"multiline -string" -r"(")" -r"("")" -r"(')" -r"('')" -r"-(-)-" -r"-()-)-" -r"--()-")--" -r"( () )" -r"(())" -r"( ())" -r"(() )" -r"-())-" -r"-(())-)-" -r"(raw -string -)" -r"("some raw string -business")" -R"(upper)" -r'(single)' -R'(upper single)' -r"[brackets]" -r"{braces}" -"" -'' -r"()" diff --git a/crates/air_r_parser/tests/snapshots/ok/value/string_value.R.snap b/crates/air_r_parser/tests/snapshots/ok/value/string_value.R.snap deleted file mode 100644 index 8ad6040d..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/value/string_value.R.snap +++ /dev/null @@ -1,354 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -"hi there!" -"\"" -'hi there!' -'\'' -"'" -"multiline -string" -r"(")" -r"("")" -r"(')" -r"('')" -r"-(-)-" -r"-()-)-" -r"--()-")--" -r"( () )" -r"(())" -r"( ())" -r"(() )" -r"-())-" -r"-(())-)-" -r"(raw -string -)" -r"("some raw string -business")" -R"(upper)" -r'(single)' -R'(upper single)' -r"[brackets]" -r"{braces}" -"" -'' -r"()" - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@0..11 "\"hi there!\"" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@11..16 "\"\\\"\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_SINGLE_QUOTED_STRING_LITERAL@16..28 "'hi there!'" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_SINGLE_QUOTED_STRING_LITERAL@28..33 "'\\''" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@33..37 "\"'\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@37..56 "\"multiline\nstring\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@56..63 "r\"(\")\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@63..71 "r\"(\"\")\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@71..78 "r\"(')\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@78..86 "r\"('')\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@86..95 "r\"-(-)-\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@95..105 "r\"-()-)-\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@105..118 "r\"--()-\")--\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@118..128 "r\"( () )\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@128..136 "r\"(())\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@136..145 "r\"( ())\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@145..154 "r\"(() )\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@154..163 "r\"-())-\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@163..175 "r\"-(())-)-\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@175..192 "r\"(raw\nstring\n)\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@192..224 "r\"(\"some raw string\nbusiness\")\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@224..235 "R\"(upper)\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@235..247 "r'(single)'" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@247..265 "R'(upper single)'" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@265..279 "r\"[brackets]\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@279..291 "r\"{braces}\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_DOUBLE_QUOTED_STRING_LITERAL@291..294 "\"\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_SINGLE_QUOTED_STRING_LITERAL@294..297 "''" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RStringValue { - value: R_RAW_STRING_LITERAL@297..303 "r\"()\"" [Newline("\n")] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@303..304 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..304 - 0: (empty) - 1: R_STATEMENT_LIST@0..303 - 0: R_EXPRESSION_STATEMENT@0..11 - 0: R_STRING_VALUE@0..11 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@0..11 "\"hi there!\"" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@11..16 - 0: R_STRING_VALUE@11..16 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@11..16 "\"\\\"\"" [Newline("\n")] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@16..28 - 0: R_STRING_VALUE@16..28 - 0: R_SINGLE_QUOTED_STRING_LITERAL@16..28 "'hi there!'" [Newline("\n")] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@28..33 - 0: R_STRING_VALUE@28..33 - 0: R_SINGLE_QUOTED_STRING_LITERAL@28..33 "'\\''" [Newline("\n")] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@33..37 - 0: R_STRING_VALUE@33..37 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@33..37 "\"'\"" [Newline("\n")] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@37..56 - 0: R_STRING_VALUE@37..56 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@37..56 "\"multiline\nstring\"" [Newline("\n")] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@56..63 - 0: R_STRING_VALUE@56..63 - 0: R_RAW_STRING_LITERAL@56..63 "r\"(\")\"" [Newline("\n")] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@63..71 - 0: R_STRING_VALUE@63..71 - 0: R_RAW_STRING_LITERAL@63..71 "r\"(\"\")\"" [Newline("\n")] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@71..78 - 0: R_STRING_VALUE@71..78 - 0: R_RAW_STRING_LITERAL@71..78 "r\"(')\"" [Newline("\n")] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@78..86 - 0: R_STRING_VALUE@78..86 - 0: R_RAW_STRING_LITERAL@78..86 "r\"('')\"" [Newline("\n")] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@86..95 - 0: R_STRING_VALUE@86..95 - 0: R_RAW_STRING_LITERAL@86..95 "r\"-(-)-\"" [Newline("\n")] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@95..105 - 0: R_STRING_VALUE@95..105 - 0: R_RAW_STRING_LITERAL@95..105 "r\"-()-)-\"" [Newline("\n")] [] - 1: (empty) - 12: R_EXPRESSION_STATEMENT@105..118 - 0: R_STRING_VALUE@105..118 - 0: R_RAW_STRING_LITERAL@105..118 "r\"--()-\")--\"" [Newline("\n")] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@118..128 - 0: R_STRING_VALUE@118..128 - 0: R_RAW_STRING_LITERAL@118..128 "r\"( () )\"" [Newline("\n")] [] - 1: (empty) - 14: R_EXPRESSION_STATEMENT@128..136 - 0: R_STRING_VALUE@128..136 - 0: R_RAW_STRING_LITERAL@128..136 "r\"(())\"" [Newline("\n")] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@136..145 - 0: R_STRING_VALUE@136..145 - 0: R_RAW_STRING_LITERAL@136..145 "r\"( ())\"" [Newline("\n")] [] - 1: (empty) - 16: R_EXPRESSION_STATEMENT@145..154 - 0: R_STRING_VALUE@145..154 - 0: R_RAW_STRING_LITERAL@145..154 "r\"(() )\"" [Newline("\n")] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@154..163 - 0: R_STRING_VALUE@154..163 - 0: R_RAW_STRING_LITERAL@154..163 "r\"-())-\"" [Newline("\n")] [] - 1: (empty) - 18: R_EXPRESSION_STATEMENT@163..175 - 0: R_STRING_VALUE@163..175 - 0: R_RAW_STRING_LITERAL@163..175 "r\"-(())-)-\"" [Newline("\n")] [] - 1: (empty) - 19: R_EXPRESSION_STATEMENT@175..192 - 0: R_STRING_VALUE@175..192 - 0: R_RAW_STRING_LITERAL@175..192 "r\"(raw\nstring\n)\"" [Newline("\n")] [] - 1: (empty) - 20: R_EXPRESSION_STATEMENT@192..224 - 0: R_STRING_VALUE@192..224 - 0: R_RAW_STRING_LITERAL@192..224 "r\"(\"some raw string\nbusiness\")\"" [Newline("\n")] [] - 1: (empty) - 21: R_EXPRESSION_STATEMENT@224..235 - 0: R_STRING_VALUE@224..235 - 0: R_RAW_STRING_LITERAL@224..235 "R\"(upper)\"" [Newline("\n")] [] - 1: (empty) - 22: R_EXPRESSION_STATEMENT@235..247 - 0: R_STRING_VALUE@235..247 - 0: R_RAW_STRING_LITERAL@235..247 "r'(single)'" [Newline("\n")] [] - 1: (empty) - 23: R_EXPRESSION_STATEMENT@247..265 - 0: R_STRING_VALUE@247..265 - 0: R_RAW_STRING_LITERAL@247..265 "R'(upper single)'" [Newline("\n")] [] - 1: (empty) - 24: R_EXPRESSION_STATEMENT@265..279 - 0: R_STRING_VALUE@265..279 - 0: R_RAW_STRING_LITERAL@265..279 "r\"[brackets]\"" [Newline("\n")] [] - 1: (empty) - 25: R_EXPRESSION_STATEMENT@279..291 - 0: R_STRING_VALUE@279..291 - 0: R_RAW_STRING_LITERAL@279..291 "r\"{braces}\"" [Newline("\n")] [] - 1: (empty) - 26: R_EXPRESSION_STATEMENT@291..294 - 0: R_STRING_VALUE@291..294 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@291..294 "\"\"" [Newline("\n")] [] - 1: (empty) - 27: R_EXPRESSION_STATEMENT@294..297 - 0: R_STRING_VALUE@294..297 - 0: R_SINGLE_QUOTED_STRING_LITERAL@294..297 "''" [Newline("\n")] [] - 1: (empty) - 28: R_EXPRESSION_STATEMENT@297..303 - 0: R_STRING_VALUE@297..303 - 0: R_RAW_STRING_LITERAL@297..303 "r\"()\"" [Newline("\n")] [] - 1: (empty) - 2: EOF@303..304 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/ok/while_statement.R b/crates/air_r_parser/tests/snapshots/ok/while_statement.R deleted file mode 100644 index badffae8..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/while_statement.R +++ /dev/null @@ -1,11 +0,0 @@ -while(TRUE) FALSE - -while(TRUE) {} - -while(a) { - -} - -while(fn()) { - -} diff --git a/crates/air_r_parser/tests/snapshots/ok/while_statement.R.snap b/crates/air_r_parser/tests/snapshots/ok/while_statement.R.snap deleted file mode 100644 index 7b25e202..00000000 --- a/crates/air_r_parser/tests/snapshots/ok/while_statement.R.snap +++ /dev/null @@ -1,162 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -while(TRUE) FALSE - -while(TRUE) {} - -while(a) { - -} - -while(fn()) { - -} - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RWhileStatement { - while_token: WHILE_KW@0..5 "while" [] [], - l_paren_token: L_PAREN@5..6 "(" [] [], - condition: RTrueExpression { - true_token: TRUE_KW@6..10 "TRUE" [] [], - }, - r_paren_token: R_PAREN@10..12 ")" [] [Whitespace(" ")], - body: RFalseExpression { - false_token: FALSE_KW@12..17 "FALSE" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RWhileStatement { - while_token: WHILE_KW@17..24 "while" [Newline("\n"), Newline("\n")] [], - l_paren_token: L_PAREN@24..25 "(" [] [], - condition: RTrueExpression { - true_token: TRUE_KW@25..29 "TRUE" [] [], - }, - r_paren_token: R_PAREN@29..31 ")" [] [Whitespace(" ")], - body: RBracedStatements { - l_curly_token: L_CURLY@31..32 "{" [] [], - statements: RStatementList [], - r_curly_token: R_CURLY@32..33 "}" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RWhileStatement { - while_token: WHILE_KW@33..40 "while" [Newline("\n"), Newline("\n")] [], - l_paren_token: L_PAREN@40..41 "(" [] [], - condition: RIdentifier { - name_token: IDENT@41..42 "a" [] [], - }, - r_paren_token: R_PAREN@42..44 ")" [] [Whitespace(" ")], - body: RBracedStatements { - l_curly_token: L_CURLY@44..45 "{" [] [], - statements: RStatementList [], - r_curly_token: R_CURLY@45..48 "}" [Newline("\n"), Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RWhileStatement { - while_token: WHILE_KW@48..55 "while" [Newline("\n"), Newline("\n")] [], - l_paren_token: L_PAREN@55..56 "(" [] [], - condition: RCall { - function: RIdentifier { - name_token: IDENT@56..58 "fn" [] [], - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@58..59 "(" [] [], - items: RArgumentList [], - r_paren_token: R_PAREN@59..60 ")" [] [], - }, - }, - r_paren_token: R_PAREN@60..62 ")" [] [Whitespace(" ")], - body: RBracedStatements { - l_curly_token: L_CURLY@62..63 "{" [] [], - statements: RStatementList [], - r_curly_token: R_CURLY@63..66 "}" [Newline("\n"), Newline("\n")] [], - }, - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@66..67 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..67 - 0: (empty) - 1: R_STATEMENT_LIST@0..66 - 0: R_EXPRESSION_STATEMENT@0..17 - 0: R_WHILE_STATEMENT@0..17 - 0: WHILE_KW@0..5 "while" [] [] - 1: L_PAREN@5..6 "(" [] [] - 2: R_TRUE_EXPRESSION@6..10 - 0: TRUE_KW@6..10 "TRUE" [] [] - 3: R_PAREN@10..12 ")" [] [Whitespace(" ")] - 4: R_FALSE_EXPRESSION@12..17 - 0: FALSE_KW@12..17 "FALSE" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@17..33 - 0: R_WHILE_STATEMENT@17..33 - 0: WHILE_KW@17..24 "while" [Newline("\n"), Newline("\n")] [] - 1: L_PAREN@24..25 "(" [] [] - 2: R_TRUE_EXPRESSION@25..29 - 0: TRUE_KW@25..29 "TRUE" [] [] - 3: R_PAREN@29..31 ")" [] [Whitespace(" ")] - 4: R_BRACED_STATEMENTS@31..33 - 0: L_CURLY@31..32 "{" [] [] - 1: R_STATEMENT_LIST@32..32 - 2: R_CURLY@32..33 "}" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@33..48 - 0: R_WHILE_STATEMENT@33..48 - 0: WHILE_KW@33..40 "while" [Newline("\n"), Newline("\n")] [] - 1: L_PAREN@40..41 "(" [] [] - 2: R_IDENTIFIER@41..42 - 0: IDENT@41..42 "a" [] [] - 3: R_PAREN@42..44 ")" [] [Whitespace(" ")] - 4: R_BRACED_STATEMENTS@44..48 - 0: L_CURLY@44..45 "{" [] [] - 1: R_STATEMENT_LIST@45..45 - 2: R_CURLY@45..48 "}" [Newline("\n"), Newline("\n")] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@48..66 - 0: R_WHILE_STATEMENT@48..66 - 0: WHILE_KW@48..55 "while" [Newline("\n"), Newline("\n")] [] - 1: L_PAREN@55..56 "(" [] [] - 2: R_CALL@56..60 - 0: R_IDENTIFIER@56..58 - 0: IDENT@56..58 "fn" [] [] - 1: R_CALL_ARGUMENTS@58..60 - 0: L_PAREN@58..59 "(" [] [] - 1: R_ARGUMENT_LIST@59..59 - 2: R_PAREN@59..60 ")" [] [] - 3: R_PAREN@60..62 ")" [] [Whitespace(" ")] - 4: R_BRACED_STATEMENTS@62..66 - 0: L_CURLY@62..63 "{" [] [] - 1: R_STATEMENT_LIST@63..63 - 2: R_CURLY@63..66 "}" [Newline("\n"), Newline("\n")] [] - 1: (empty) - 2: EOF@66..67 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/undefined/extract_expression_error.R b/crates/air_r_parser/tests/snapshots/undefined/extract_expression_error.R deleted file mode 100644 index 4a4ca3be..00000000 --- a/crates/air_r_parser/tests/snapshots/undefined/extract_expression_error.R +++ /dev/null @@ -1,29 +0,0 @@ -# TODO: tree-sitter-r currently allows an `optional()` RHS, but our grammar -# requires a RHS, so we get `missing (required)`. These should eventually be -# parse errors if we can switch tree-sitter-r away from using `optional()`. -# For example, `a$1` gives: - -#> RExtractExpression { -#> left: RDoubleValue { -#> value_token: R_DOUBLE_LITERAL@10..16 "1" [Newline("\n"), Whitespace(" ")] [], -#> }, -#> operator: DOLLAR@16..17 "$" [] [], -#> right: missing (required), -#> }, -#> RDoubleValue { -#> value_token: R_DOUBLE_LITERAL@17..18 "2" [] [], -#> }, - -a$1 -a$NA -a$NULL -a$TRUE -a$(b) -a${ b } - -a@1 -a@NA -a@NULL -a@TRUE -a@(b) -a@{ b } diff --git a/crates/air_r_parser/tests/snapshots/undefined/extract_expression_error.R.snap b/crates/air_r_parser/tests/snapshots/undefined/extract_expression_error.R.snap deleted file mode 100644 index 1f105c8a..00000000 --- a/crates/air_r_parser/tests/snapshots/undefined/extract_expression_error.R.snap +++ /dev/null @@ -1,440 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -# TODO: tree-sitter-r currently allows an `optional()` RHS, but our grammar -# requires a RHS, so we get `missing (required)`. These should eventually be -# parse errors if we can switch tree-sitter-r away from using `optional()`. -# For example, `a$1` gives: - -#> RExtractExpression { -#> left: RDoubleValue { -#> value_token: R_DOUBLE_LITERAL@10..16 "1" [Newline("\n"), Whitespace(" ")] [], -#> }, -#> operator: DOLLAR@16..17 "$" [] [], -#> right: missing (required), -#> }, -#> RDoubleValue { -#> value_token: R_DOUBLE_LITERAL@17..18 "2" [] [], -#> }, - -a$1 -a$NA -a$NULL -a$TRUE -a$(b) -a${ b } - -a@1 -a@NA -a@NULL -a@TRUE -a@(b) -a@{ b } - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@0..575 "a" [Comments("# TODO: tree-sitter-r ..."), Newline("\n"), Comments("# requires a RHS, so ..."), Newline("\n"), Comments("# parse errors if we ..."), Newline("\n"), Comments("# For example, `a$1` ..."), Newline("\n"), Newline("\n"), Comments("#> RExtractExpression {"), Newline("\n"), Comments("#> left: RDoubleV ..."), Newline("\n"), Comments("#> value_toke ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Comments("#> operator: DOLL ..."), Newline("\n"), Comments("#> right: missing ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Comments("#> RDoubleValue {"), Newline("\n"), Comments("#> value_token: R ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Newline("\n")] [], - }, - operator: DOLLAR@575..576 "$" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@576..577 "1" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@577..579 "a" [Newline("\n")] [], - }, - operator: DOLLAR@579..580 "$" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNaExpression { - value: NA_LOGICAL_KW@580..582 "NA" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@582..584 "a" [Newline("\n")] [], - }, - operator: DOLLAR@584..585 "$" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNullExpression { - null_token: NULL_KW@585..589 "NULL" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@589..591 "a" [Newline("\n")] [], - }, - operator: DOLLAR@591..592 "$" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RTrueExpression { - true_token: TRUE_KW@592..596 "TRUE" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RExtractExpression { - left: RIdentifier { - name_token: IDENT@596..598 "a" [Newline("\n")] [], - }, - operator: DOLLAR@598..599 "$" [] [], - right: missing (required), - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@599..600 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@600..601 "b" [] [], - }, - }, - ], - r_paren_token: R_PAREN@601..602 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@602..604 "a" [Newline("\n")] [], - }, - operator: DOLLAR@604..605 "$" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@605..607 "{" [] [Whitespace(" ")], - statements: RStatementList [ - RExpressionStatement { - expression: RIdentifier { - name_token: IDENT@607..609 "b" [] [Whitespace(" ")], - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@609..610 "}" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@610..613 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: AT@613..614 "@" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@614..615 "1" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@615..617 "a" [Newline("\n")] [], - }, - operator: AT@617..618 "@" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNaExpression { - value: NA_LOGICAL_KW@618..620 "NA" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@620..622 "a" [Newline("\n")] [], - }, - operator: AT@622..623 "@" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNullExpression { - null_token: NULL_KW@623..627 "NULL" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@627..629 "a" [Newline("\n")] [], - }, - operator: AT@629..630 "@" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RTrueExpression { - true_token: TRUE_KW@630..634 "TRUE" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RExtractExpression { - left: RIdentifier { - name_token: IDENT@634..636 "a" [Newline("\n")] [], - }, - operator: AT@636..637 "@" [] [], - right: missing (required), - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@637..638 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@638..639 "b" [] [], - }, - }, - ], - r_paren_token: R_PAREN@639..640 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RExtractExpression { - left: RIdentifier { - name_token: IDENT@640..642 "a" [Newline("\n")] [], - }, - operator: AT@642..643 "@" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@643..645 "{" [] [Whitespace(" ")], - statements: RStatementList [ - RExpressionStatement { - expression: RIdentifier { - name_token: IDENT@645..647 "b" [] [Whitespace(" ")], - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@647..648 "}" [] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@648..649 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..649 - 0: (empty) - 1: R_STATEMENT_LIST@0..648 - 0: R_EXPRESSION_STATEMENT@0..576 - 0: R_EXTRACT_EXPRESSION@0..576 - 0: R_IDENTIFIER@0..575 - 0: IDENT@0..575 "a" [Comments("# TODO: tree-sitter-r ..."), Newline("\n"), Comments("# requires a RHS, so ..."), Newline("\n"), Comments("# parse errors if we ..."), Newline("\n"), Comments("# For example, `a$1` ..."), Newline("\n"), Newline("\n"), Comments("#> RExtractExpression {"), Newline("\n"), Comments("#> left: RDoubleV ..."), Newline("\n"), Comments("#> value_toke ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Comments("#> operator: DOLL ..."), Newline("\n"), Comments("#> right: missing ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Comments("#> RDoubleValue {"), Newline("\n"), Comments("#> value_token: R ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Newline("\n")] [] - 1: DOLLAR@575..576 "$" [] [] - 2: (empty) - 1: (empty) - 1: R_EXPRESSION_STATEMENT@576..577 - 0: R_DOUBLE_VALUE@576..577 - 0: R_DOUBLE_LITERAL@576..577 "1" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@577..580 - 0: R_EXTRACT_EXPRESSION@577..580 - 0: R_IDENTIFIER@577..579 - 0: IDENT@577..579 "a" [Newline("\n")] [] - 1: DOLLAR@579..580 "$" [] [] - 2: (empty) - 1: (empty) - 3: R_EXPRESSION_STATEMENT@580..582 - 0: R_NA_EXPRESSION@580..582 - 0: NA_LOGICAL_KW@580..582 "NA" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@582..585 - 0: R_EXTRACT_EXPRESSION@582..585 - 0: R_IDENTIFIER@582..584 - 0: IDENT@582..584 "a" [Newline("\n")] [] - 1: DOLLAR@584..585 "$" [] [] - 2: (empty) - 1: (empty) - 5: R_EXPRESSION_STATEMENT@585..589 - 0: R_NULL_EXPRESSION@585..589 - 0: NULL_KW@585..589 "NULL" [] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@589..592 - 0: R_EXTRACT_EXPRESSION@589..592 - 0: R_IDENTIFIER@589..591 - 0: IDENT@589..591 "a" [Newline("\n")] [] - 1: DOLLAR@591..592 "$" [] [] - 2: (empty) - 1: (empty) - 7: R_EXPRESSION_STATEMENT@592..596 - 0: R_TRUE_EXPRESSION@592..596 - 0: TRUE_KW@592..596 "TRUE" [] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@596..602 - 0: R_CALL@596..602 - 0: R_EXTRACT_EXPRESSION@596..599 - 0: R_IDENTIFIER@596..598 - 0: IDENT@596..598 "a" [Newline("\n")] [] - 1: DOLLAR@598..599 "$" [] [] - 2: (empty) - 1: R_CALL_ARGUMENTS@599..602 - 0: L_PAREN@599..600 "(" [] [] - 1: R_ARGUMENT_LIST@600..601 - 0: R_ARGUMENT@600..601 - 0: (empty) - 1: R_IDENTIFIER@600..601 - 0: IDENT@600..601 "b" [] [] - 2: R_PAREN@601..602 ")" [] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@602..605 - 0: R_EXTRACT_EXPRESSION@602..605 - 0: R_IDENTIFIER@602..604 - 0: IDENT@602..604 "a" [Newline("\n")] [] - 1: DOLLAR@604..605 "$" [] [] - 2: (empty) - 1: (empty) - 10: R_EXPRESSION_STATEMENT@605..610 - 0: R_BRACED_STATEMENTS@605..610 - 0: L_CURLY@605..607 "{" [] [Whitespace(" ")] - 1: R_STATEMENT_LIST@607..609 - 0: R_EXPRESSION_STATEMENT@607..609 - 0: R_IDENTIFIER@607..609 - 0: IDENT@607..609 "b" [] [Whitespace(" ")] - 1: (empty) - 2: R_CURLY@609..610 "}" [] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@610..614 - 0: R_EXTRACT_EXPRESSION@610..614 - 0: R_IDENTIFIER@610..613 - 0: IDENT@610..613 "a" [Newline("\n"), Newline("\n")] [] - 1: AT@613..614 "@" [] [] - 2: (empty) - 1: (empty) - 12: R_EXPRESSION_STATEMENT@614..615 - 0: R_DOUBLE_VALUE@614..615 - 0: R_DOUBLE_LITERAL@614..615 "1" [] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@615..618 - 0: R_EXTRACT_EXPRESSION@615..618 - 0: R_IDENTIFIER@615..617 - 0: IDENT@615..617 "a" [Newline("\n")] [] - 1: AT@617..618 "@" [] [] - 2: (empty) - 1: (empty) - 14: R_EXPRESSION_STATEMENT@618..620 - 0: R_NA_EXPRESSION@618..620 - 0: NA_LOGICAL_KW@618..620 "NA" [] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@620..623 - 0: R_EXTRACT_EXPRESSION@620..623 - 0: R_IDENTIFIER@620..622 - 0: IDENT@620..622 "a" [Newline("\n")] [] - 1: AT@622..623 "@" [] [] - 2: (empty) - 1: (empty) - 16: R_EXPRESSION_STATEMENT@623..627 - 0: R_NULL_EXPRESSION@623..627 - 0: NULL_KW@623..627 "NULL" [] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@627..630 - 0: R_EXTRACT_EXPRESSION@627..630 - 0: R_IDENTIFIER@627..629 - 0: IDENT@627..629 "a" [Newline("\n")] [] - 1: AT@629..630 "@" [] [] - 2: (empty) - 1: (empty) - 18: R_EXPRESSION_STATEMENT@630..634 - 0: R_TRUE_EXPRESSION@630..634 - 0: TRUE_KW@630..634 "TRUE" [] [] - 1: (empty) - 19: R_EXPRESSION_STATEMENT@634..640 - 0: R_CALL@634..640 - 0: R_EXTRACT_EXPRESSION@634..637 - 0: R_IDENTIFIER@634..636 - 0: IDENT@634..636 "a" [Newline("\n")] [] - 1: AT@636..637 "@" [] [] - 2: (empty) - 1: R_CALL_ARGUMENTS@637..640 - 0: L_PAREN@637..638 "(" [] [] - 1: R_ARGUMENT_LIST@638..639 - 0: R_ARGUMENT@638..639 - 0: (empty) - 1: R_IDENTIFIER@638..639 - 0: IDENT@638..639 "b" [] [] - 2: R_PAREN@639..640 ")" [] [] - 1: (empty) - 20: R_EXPRESSION_STATEMENT@640..643 - 0: R_EXTRACT_EXPRESSION@640..643 - 0: R_IDENTIFIER@640..642 - 0: IDENT@640..642 "a" [Newline("\n")] [] - 1: AT@642..643 "@" [] [] - 2: (empty) - 1: (empty) - 21: R_EXPRESSION_STATEMENT@643..648 - 0: R_BRACED_STATEMENTS@643..648 - 0: L_CURLY@643..645 "{" [] [Whitespace(" ")] - 1: R_STATEMENT_LIST@645..647 - 0: R_EXPRESSION_STATEMENT@645..647 - 0: R_IDENTIFIER@645..647 - 0: IDENT@645..647 "b" [] [Whitespace(" ")] - 1: (empty) - 2: R_CURLY@647..648 "}" [] [] - 1: (empty) - 2: EOF@648..649 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/snapshots/undefined/namespace_expression_error.R b/crates/air_r_parser/tests/snapshots/undefined/namespace_expression_error.R deleted file mode 100644 index 6cd3b8ba..00000000 --- a/crates/air_r_parser/tests/snapshots/undefined/namespace_expression_error.R +++ /dev/null @@ -1,29 +0,0 @@ -# TODO: tree-sitter-r currently allows an `optional()` RHS, but our grammar -# requires a RHS, so we get `missing (required)`. These should eventually be -# parse errors if we can switch tree-sitter-r away from using `optional()`. -# For example, `a::1` gives: - -#> RNamespaceExpression { -#> left: RIdentifier { -#> name_token: IDENT@0..6 "a" [Newline("\n"), Whitespace(" ")] [], -#> }, -#> operator: COLON2@6..8 "::" [] [], -#> right: missing (required), -#> }, -#> RDoubleValue { -#> value_token: R_DOUBLE_LITERAL@8..9 "1" [] [], -#> }, - -a::1 -a::NA -a::NULL -a::TRUE -a::(b) -a::{ b } - -a:::1 -a:::NA -a:::NULL -a:::TRUE -a:::(b) -a:::{ b } diff --git a/crates/air_r_parser/tests/snapshots/undefined/namespace_expression_error.R.snap b/crates/air_r_parser/tests/snapshots/undefined/namespace_expression_error.R.snap deleted file mode 100644 index 37ea9316..00000000 --- a/crates/air_r_parser/tests/snapshots/undefined/namespace_expression_error.R.snap +++ /dev/null @@ -1,440 +0,0 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input - -```R -# TODO: tree-sitter-r currently allows an `optional()` RHS, but our grammar -# requires a RHS, so we get `missing (required)`. These should eventually be -# parse errors if we can switch tree-sitter-r away from using `optional()`. -# For example, `a::1` gives: - -#> RNamespaceExpression { -#> left: RIdentifier { -#> name_token: IDENT@0..6 "a" [Newline("\n"), Whitespace(" ")] [], -#> }, -#> operator: COLON2@6..8 "::" [] [], -#> right: missing (required), -#> }, -#> RDoubleValue { -#> value_token: R_DOUBLE_LITERAL@8..9 "1" [] [], -#> }, - -a::1 -a::NA -a::NULL -a::TRUE -a::(b) -a::{ b } - -a:::1 -a:::NA -a:::NULL -a:::TRUE -a:::(b) -a:::{ b } - -``` - - -## AST - -``` -RRoot { - bom_token: missing (optional), - statements: RStatementList [ - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@0..560 "a" [Comments("# TODO: tree-sitter-r ..."), Newline("\n"), Comments("# requires a RHS, so ..."), Newline("\n"), Comments("# parse errors if we ..."), Newline("\n"), Comments("# For example, `a::1` ..."), Newline("\n"), Newline("\n"), Comments("#> RNamespaceExpressi ..."), Newline("\n"), Comments("#> left: RIdentif ..."), Newline("\n"), Comments("#> name_token ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Comments("#> operator: COLO ..."), Newline("\n"), Comments("#> right: missing ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Comments("#> RDoubleValue {"), Newline("\n"), Comments("#> value_token: R ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Newline("\n")] [], - }, - operator: COLON2@560..562 "::" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@562..563 "1" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@563..565 "a" [Newline("\n")] [], - }, - operator: COLON2@565..567 "::" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNaExpression { - value: NA_LOGICAL_KW@567..569 "NA" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@569..571 "a" [Newline("\n")] [], - }, - operator: COLON2@571..573 "::" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNullExpression { - null_token: NULL_KW@573..577 "NULL" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@577..579 "a" [Newline("\n")] [], - }, - operator: COLON2@579..581 "::" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RTrueExpression { - true_token: TRUE_KW@581..585 "TRUE" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@585..587 "a" [Newline("\n")] [], - }, - operator: COLON2@587..589 "::" [] [], - right: missing (required), - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@589..590 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@590..591 "b" [] [], - }, - }, - ], - r_paren_token: R_PAREN@591..592 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@592..594 "a" [Newline("\n")] [], - }, - operator: COLON2@594..596 "::" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@596..598 "{" [] [Whitespace(" ")], - statements: RStatementList [ - RExpressionStatement { - expression: RIdentifier { - name_token: IDENT@598..600 "b" [] [Whitespace(" ")], - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@600..601 "}" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@601..604 "a" [Newline("\n"), Newline("\n")] [], - }, - operator: COLON3@604..607 ":::" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RDoubleValue { - value_token: R_DOUBLE_LITERAL@607..608 "1" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@608..610 "a" [Newline("\n")] [], - }, - operator: COLON3@610..613 ":::" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNaExpression { - value: NA_LOGICAL_KW@613..615 "NA" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@615..617 "a" [Newline("\n")] [], - }, - operator: COLON3@617..620 ":::" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNullExpression { - null_token: NULL_KW@620..624 "NULL" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@624..626 "a" [Newline("\n")] [], - }, - operator: COLON3@626..629 ":::" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RTrueExpression { - true_token: TRUE_KW@629..633 "TRUE" [] [], - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RCall { - function: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@633..635 "a" [Newline("\n")] [], - }, - operator: COLON3@635..638 ":::" [] [], - right: missing (required), - }, - arguments: RCallArguments { - l_paren_token: L_PAREN@638..639 "(" [] [], - items: RArgumentList [ - RArgument { - name_clause: missing (optional), - value: RIdentifier { - name_token: IDENT@639..640 "b" [] [], - }, - }, - ], - r_paren_token: R_PAREN@640..641 ")" [] [], - }, - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RNamespaceExpression { - left: RIdentifier { - name_token: IDENT@641..643 "a" [Newline("\n")] [], - }, - operator: COLON3@643..646 ":::" [] [], - right: missing (required), - }, - semicolon_token: missing (optional), - }, - RExpressionStatement { - expression: RBracedStatements { - l_curly_token: L_CURLY@646..648 "{" [] [Whitespace(" ")], - statements: RStatementList [ - RExpressionStatement { - expression: RIdentifier { - name_token: IDENT@648..650 "b" [] [Whitespace(" ")], - }, - semicolon_token: missing (optional), - }, - ], - r_curly_token: R_CURLY@650..651 "}" [] [], - }, - semicolon_token: missing (optional), - }, - ], - eof_token: EOF@651..652 "" [Newline("\n")] [], -} -``` - -## CST - -``` -0: R_ROOT@0..652 - 0: (empty) - 1: R_STATEMENT_LIST@0..651 - 0: R_EXPRESSION_STATEMENT@0..562 - 0: R_NAMESPACE_EXPRESSION@0..562 - 0: R_IDENTIFIER@0..560 - 0: IDENT@0..560 "a" [Comments("# TODO: tree-sitter-r ..."), Newline("\n"), Comments("# requires a RHS, so ..."), Newline("\n"), Comments("# parse errors if we ..."), Newline("\n"), Comments("# For example, `a::1` ..."), Newline("\n"), Newline("\n"), Comments("#> RNamespaceExpressi ..."), Newline("\n"), Comments("#> left: RIdentif ..."), Newline("\n"), Comments("#> name_token ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Comments("#> operator: COLO ..."), Newline("\n"), Comments("#> right: missing ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Comments("#> RDoubleValue {"), Newline("\n"), Comments("#> value_token: R ..."), Newline("\n"), Comments("#> },"), Newline("\n"), Newline("\n")] [] - 1: COLON2@560..562 "::" [] [] - 2: (empty) - 1: (empty) - 1: R_EXPRESSION_STATEMENT@562..563 - 0: R_DOUBLE_VALUE@562..563 - 0: R_DOUBLE_LITERAL@562..563 "1" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@563..567 - 0: R_NAMESPACE_EXPRESSION@563..567 - 0: R_IDENTIFIER@563..565 - 0: IDENT@563..565 "a" [Newline("\n")] [] - 1: COLON2@565..567 "::" [] [] - 2: (empty) - 1: (empty) - 3: R_EXPRESSION_STATEMENT@567..569 - 0: R_NA_EXPRESSION@567..569 - 0: NA_LOGICAL_KW@567..569 "NA" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@569..573 - 0: R_NAMESPACE_EXPRESSION@569..573 - 0: R_IDENTIFIER@569..571 - 0: IDENT@569..571 "a" [Newline("\n")] [] - 1: COLON2@571..573 "::" [] [] - 2: (empty) - 1: (empty) - 5: R_EXPRESSION_STATEMENT@573..577 - 0: R_NULL_EXPRESSION@573..577 - 0: NULL_KW@573..577 "NULL" [] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@577..581 - 0: R_NAMESPACE_EXPRESSION@577..581 - 0: R_IDENTIFIER@577..579 - 0: IDENT@577..579 "a" [Newline("\n")] [] - 1: COLON2@579..581 "::" [] [] - 2: (empty) - 1: (empty) - 7: R_EXPRESSION_STATEMENT@581..585 - 0: R_TRUE_EXPRESSION@581..585 - 0: TRUE_KW@581..585 "TRUE" [] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@585..592 - 0: R_CALL@585..592 - 0: R_NAMESPACE_EXPRESSION@585..589 - 0: R_IDENTIFIER@585..587 - 0: IDENT@585..587 "a" [Newline("\n")] [] - 1: COLON2@587..589 "::" [] [] - 2: (empty) - 1: R_CALL_ARGUMENTS@589..592 - 0: L_PAREN@589..590 "(" [] [] - 1: R_ARGUMENT_LIST@590..591 - 0: R_ARGUMENT@590..591 - 0: (empty) - 1: R_IDENTIFIER@590..591 - 0: IDENT@590..591 "b" [] [] - 2: R_PAREN@591..592 ")" [] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@592..596 - 0: R_NAMESPACE_EXPRESSION@592..596 - 0: R_IDENTIFIER@592..594 - 0: IDENT@592..594 "a" [Newline("\n")] [] - 1: COLON2@594..596 "::" [] [] - 2: (empty) - 1: (empty) - 10: R_EXPRESSION_STATEMENT@596..601 - 0: R_BRACED_STATEMENTS@596..601 - 0: L_CURLY@596..598 "{" [] [Whitespace(" ")] - 1: R_STATEMENT_LIST@598..600 - 0: R_EXPRESSION_STATEMENT@598..600 - 0: R_IDENTIFIER@598..600 - 0: IDENT@598..600 "b" [] [Whitespace(" ")] - 1: (empty) - 2: R_CURLY@600..601 "}" [] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@601..607 - 0: R_NAMESPACE_EXPRESSION@601..607 - 0: R_IDENTIFIER@601..604 - 0: IDENT@601..604 "a" [Newline("\n"), Newline("\n")] [] - 1: COLON3@604..607 ":::" [] [] - 2: (empty) - 1: (empty) - 12: R_EXPRESSION_STATEMENT@607..608 - 0: R_DOUBLE_VALUE@607..608 - 0: R_DOUBLE_LITERAL@607..608 "1" [] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@608..613 - 0: R_NAMESPACE_EXPRESSION@608..613 - 0: R_IDENTIFIER@608..610 - 0: IDENT@608..610 "a" [Newline("\n")] [] - 1: COLON3@610..613 ":::" [] [] - 2: (empty) - 1: (empty) - 14: R_EXPRESSION_STATEMENT@613..615 - 0: R_NA_EXPRESSION@613..615 - 0: NA_LOGICAL_KW@613..615 "NA" [] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@615..620 - 0: R_NAMESPACE_EXPRESSION@615..620 - 0: R_IDENTIFIER@615..617 - 0: IDENT@615..617 "a" [Newline("\n")] [] - 1: COLON3@617..620 ":::" [] [] - 2: (empty) - 1: (empty) - 16: R_EXPRESSION_STATEMENT@620..624 - 0: R_NULL_EXPRESSION@620..624 - 0: NULL_KW@620..624 "NULL" [] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@624..629 - 0: R_NAMESPACE_EXPRESSION@624..629 - 0: R_IDENTIFIER@624..626 - 0: IDENT@624..626 "a" [Newline("\n")] [] - 1: COLON3@626..629 ":::" [] [] - 2: (empty) - 1: (empty) - 18: R_EXPRESSION_STATEMENT@629..633 - 0: R_TRUE_EXPRESSION@629..633 - 0: TRUE_KW@629..633 "TRUE" [] [] - 1: (empty) - 19: R_EXPRESSION_STATEMENT@633..641 - 0: R_CALL@633..641 - 0: R_NAMESPACE_EXPRESSION@633..638 - 0: R_IDENTIFIER@633..635 - 0: IDENT@633..635 "a" [Newline("\n")] [] - 1: COLON3@635..638 ":::" [] [] - 2: (empty) - 1: R_CALL_ARGUMENTS@638..641 - 0: L_PAREN@638..639 "(" [] [] - 1: R_ARGUMENT_LIST@639..640 - 0: R_ARGUMENT@639..640 - 0: (empty) - 1: R_IDENTIFIER@639..640 - 0: IDENT@639..640 "b" [] [] - 2: R_PAREN@640..641 ")" [] [] - 1: (empty) - 20: R_EXPRESSION_STATEMENT@641..646 - 0: R_NAMESPACE_EXPRESSION@641..646 - 0: R_IDENTIFIER@641..643 - 0: IDENT@641..643 "a" [Newline("\n")] [] - 1: COLON3@643..646 ":::" [] [] - 2: (empty) - 1: (empty) - 21: R_EXPRESSION_STATEMENT@646..651 - 0: R_BRACED_STATEMENTS@646..651 - 0: L_CURLY@646..648 "{" [] [Whitespace(" ")] - 1: R_STATEMENT_LIST@648..650 - 0: R_EXPRESSION_STATEMENT@648..650 - 0: R_IDENTIFIER@648..650 - 0: IDENT@648..650 "b" [] [Whitespace(" ")] - 1: (empty) - 2: R_CURLY@650..651 "}" [] [] - 1: (empty) - 2: EOF@651..652 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser/tests/spec_test.rs b/crates/air_r_parser/tests/spec_test.rs deleted file mode 100644 index 39660490..00000000 --- a/crates/air_r_parser/tests/spec_test.rs +++ /dev/null @@ -1,141 +0,0 @@ -use air_r_parser::{RParserOptions, parse}; -use air_r_syntax::RSyntaxNode; -use biome_rowan::SyntaxKind; -use biome_rowan::SyntaxNode; -use biome_rowan::SyntaxSlot; -use std::fmt::Write; -use std::fs; -use std::path::Path; - -#[derive(Copy, Clone)] -pub enum ExpectedOutcome { - Pass, - Fail, - Undefined, -} - -#[ignore] -#[test] -fn quick_test() { - let code = " - 1$foo - 1$2"; - let options = RParserOptions::default(); - let parsed = parse(code, options); - let root = parsed.tree(); - println!("{root:#?}"); -} - -pub fn run(test_case: &str, _snapshot_name: &str, test_directory: &str, outcome_str: &str) { - let outcome = match outcome_str { - "ok" => ExpectedOutcome::Pass, - "error" => ExpectedOutcome::Fail, - "undefined" => ExpectedOutcome::Undefined, - _ => panic!("Invalid expected outcome {outcome_str}"), - }; - - let test_case_path = Path::new(test_case); - - let file_name = test_case_path - .file_name() - .expect("Expected test to have a file name") - .to_str() - .expect("File name to be valid UTF8"); - - let content = fs::read_to_string(test_case_path) - .expect("Expected test path to be a readable file in UTF8 encoding"); - - let options = RParserOptions::default(); - let parsed = parse(&content, options); - let root = parsed.tree(); - let formatted_ast = format!("{:#?}", root); - - let mut snapshot = String::new(); - writeln!(snapshot, "\n## Input\n\n```R\n{content}\n```\n\n").unwrap(); - - writeln!( - snapshot, - r#"## AST - -``` -{formatted_ast} -``` - -## CST - -``` -{:#?} -``` -"#, - parsed.syntax() - ) - .unwrap(); - - let diagnostics = parsed.diagnostics(); - - if !diagnostics.is_empty() { - if matches!(outcome, ExpectedOutcome::Pass) { - panic!( - "Expected no errors to be present in a test case that is expected to pass but the following errors are present:\n{diagnostics:#?}" - ) - } - writeln!(snapshot, "## Errors\n\n```").unwrap(); - for diagnostic in diagnostics { - writeln!(snapshot, "{}", diagnostic.message).unwrap(); - } - writeln!(snapshot, "```\n").unwrap(); - } - - match outcome { - ExpectedOutcome::Pass => { - let missing_required = formatted_ast.contains("missing (required)"); - if missing_required - || parsed - .syntax() - .descendants() - .any(|node: RSyntaxNode| node.kind().is_bogus()) - { - panic!( - "Parsed tree of a 'OK' test case should not contain any missing required children or bogus nodes: \n {formatted_ast:#?} \n\n {formatted_ast}" - ); - } - - let syntax = parsed.syntax(); - if has_bogus_nodes_or_empty_slots(&syntax) { - panic!("modified tree has bogus nodes or empty slots:\n{syntax:#?} \n\n {syntax}") - } - } - ExpectedOutcome::Fail => { - if !parsed.has_errors() { - panic!("Failing test must have an error"); - } - } - _ => {} - } - - insta::with_settings!({ - prepend_module_to_snapshot => false, - snapshot_path => &test_directory, - }, { - insta::assert_snapshot!(file_name, snapshot); - }); -} - -/// This check is used in the parser test to ensure it doesn't emit -/// bogus nodes without diagnostics. -pub fn has_bogus_nodes_or_empty_slots(node: &SyntaxNode) -> bool { - node.descendants().any(|descendant| { - let kind = descendant.kind(); - if kind.is_bogus() { - return true; - } - - if kind.is_list() { - return descendant - .slots() - .any(|slot| matches!(slot, SyntaxSlot::Empty { .. })); - } - - false - }) -} diff --git a/crates/air_r_parser/tests/spec_tests.rs b/crates/air_r_parser/tests/spec_tests.rs deleted file mode 100644 index 37eeb04e..00000000 --- a/crates/air_r_parser/tests/spec_tests.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod spec_test; - -mod tests { - tests_macros::gen_tests! {"tests/snapshots/ok/**/*.R", crate::spec_test::run, "ok"} - - tests_macros::gen_tests! {"tests/snapshots/error/**/*.R", crate::spec_test::run, "error"} - - tests_macros::gen_tests! {"tests/snapshots/undefined/**/*.R", crate::spec_test::run, "undefined"} -} diff --git a/crates/air_r_parser_tests/Cargo.toml b/crates/air_r_parser_tests/Cargo.toml new file mode 100644 index 00000000..fc24e40c --- /dev/null +++ b/crates/air_r_parser_tests/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "air_r_parser_tests" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +description = "The markdown tests and the harness that runs them" +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[dependencies] +air_r_parser = { workspace = true } +air_r_syntax = { workspace = true } +biome_console = { workspace = true } +biome_diagnostics = { workspace = true } +biome_parser = { workspace = true } +biome_rowan = { workspace = true } +similar = { workspace = true } + +[dev-dependencies] +air_r_test_macros = { workspace = true } + +[lints] +workspace = true diff --git a/crates/air_r_parser_tests/build.rs b/crates/air_r_parser_tests/build.rs new file mode 100644 index 00000000..06517ce9 --- /dev/null +++ b/crates/air_r_parser_tests/build.rs @@ -0,0 +1,4 @@ +fn main() { + // Rebuild when a new test file is added + println!("cargo::rerun-if-changed=tests/corpus"); +} diff --git a/crates/air_r_parser_tests/src/check.rs b/crates/air_r_parser_tests/src/check.rs new file mode 100644 index 00000000..f7df5fd5 --- /dev/null +++ b/crates/air_r_parser_tests/src/check.rs @@ -0,0 +1,187 @@ +use crate::document::Block; +use crate::document::Cell; +use crate::document::Expectation; +use air_r_parser::RParse; +use air_r_parser::RParserOptions; +use air_r_parser::parse; +use air_r_syntax::RSyntaxNode; +use air_r_syntax::TextRange; +use biome_console::fmt::Formatter; +use biome_console::fmt::Termcolor; +use biome_console::markup; +use biome_diagnostics::Diagnostic; +use biome_diagnostics::DiagnosticExt; +use biome_diagnostics::PrintDiagnostic; +use biome_diagnostics::termcolor::NoColor; +use biome_parser::diagnostic::ParseDiagnostic; +use biome_rowan::SyntaxKind; +use biome_rowan::SyntaxNode; + +#[derive(Debug)] +pub enum CheckError { + RoundTrip { expected: String, got: String }, + ExpectedError, + UnexpectedError(String), + BogusNodes(String), + UndiagnosedBogusNode { ranges: Vec, ast: String }, +} + +/// One code block to check +/// +/// Emits CST and diagnostic cells to write in the snapshot +pub fn check(block: &Block, expectation: Expectation) -> Result, CheckError> { + let parsed = parse(&block.source, RParserOptions::default()); + let ast = format!("{:#?}", parsed.tree()); + let root = parsed.syntax(); + + let round_tripped = root.to_string(); + if round_tripped != block.source { + return Err(CheckError::RoundTrip { + expected: block.source.clone(), + got: round_tripped, + }); + } + + let has_errors = parsed.has_errors(); + let has_bogus = has_bogus_nodes_or_missing_required_children(&ast, &root); + + match expectation { + Expectation::Error => { + if !has_errors { + return Err(CheckError::ExpectedError); + } + } + Expectation::Clean => { + if has_errors { + return Err(CheckError::UnexpectedError(render_diagnostics( + &parsed, + &block.source, + ))); + } + + if has_bogus { + return Err(CheckError::BogusNodes(ast)); + } + } + } + + let unexplained = undiagnosed_bogus_nodes(&root, parsed.diagnostics()); + if !unexplained.is_empty() { + return Err(CheckError::UndiagnosedBogusNode { + ranges: unexplained + .iter() + .map(SyntaxNode::text_trimmed_range) + .collect(), + ast, + }); + } + + let mut cells = vec![Cell::Cst(ast)]; + if !parsed.diagnostics().is_empty() { + cells.push(Cell::Diagnostics(render_diagnostics( + &parsed, + &block.source, + ))); + } + + Ok(cells) +} + +fn undiagnosed_bogus_nodes( + root: &RSyntaxNode, + diagnostics: &[ParseDiagnostic], +) -> Vec { + let spans: Vec = diagnostics + .iter() + .filter_map(|diagnostic| diagnostic.location().span) + .collect(); + + root.descendants() + .filter(|node| node.kind().is_bogus()) + .filter(|node| { + let range = node.text_trimmed_range(); + !spans.iter().any(|span| span.intersect(range).is_some()) + }) + .collect() +} + +fn has_bogus_nodes_or_missing_required_children(ast: &str, root: &RSyntaxNode) -> bool { + ast.contains("missing (required)") || root.descendants().any(|node| node.kind().is_bogus()) +} + +fn render_diagnostics(parse: &RParse, source: &str) -> String { + let mut out = Vec::new(); + + for diagnostic in parse.diagnostics() { + let diagnostic = diagnostic + .clone() + .with_file_path("test.R") + .with_file_source_code(source); + + Formatter::new(&mut Termcolor(NoColor::new(&mut out))) + .write_markup(markup! { {PrintDiagnostic::verbose(&diagnostic)} }) + .expect("writing a diagnostic to a string cannot fail"); + } + + let rendered = String::from_utf8(out).expect("rendered diagnostics are UTF-8"); + + // Biome pads its frames out to the terminal width. Committed snapshots have + // to survive an editor that strips trailing whitespace. + let mut trimmed = String::with_capacity(rendered.len()); + for line in rendered.lines() { + trimmed.push_str(line.trim_end()); + trimmed.push('\n'); + } + + trimmed.truncate(trimmed.trim_end().len()); + trimmed.push('\n'); + trimmed +} + +#[cfg(test)] +mod tests { + use super::*; + + const CLEAN: &str = "# a comment\n"; + + /// Causes a lexer diagnostic + const ERRORS: &str = "`a\n"; + + fn block(source: &str) -> Block { + Block { + source: source.to_string(), + generated: Vec::new(), + } + } + + #[test] + fn a_clean_block_gets_a_cst_cell_and_no_diagnostics_cell() { + let cells = check(&block(CLEAN), Expectation::Clean).expect("parses cleanly"); + + assert!(matches!(cells.as_slice(), [Cell::Cst(_)])); + } + + #[test] + fn an_error_block_gets_both_cells() { + let cells = check(&block(ERRORS), Expectation::Error).expect("errors as marked"); + + let [Cell::Cst(_), Cell::Diagnostics(diagnostics)] = cells.as_slice() else { + panic!("expected a cst cell and a diagnostics cell, got {cells:?}"); + }; + assert!(diagnostics.contains("Unterminated backtick-quoted name.")); + } + + #[test] + fn an_unmarked_block_may_not_error() { + let error = check(&block(ERRORS), Expectation::Clean).expect_err("errors"); + + assert!(matches!(error, CheckError::UnexpectedError(_))); + } + + #[test] + fn an_error_block_must_error() { + let error = check(&block(CLEAN), Expectation::Error).expect_err("parses cleanly"); + + assert!(matches!(error, CheckError::ExpectedError)); + } +} diff --git a/crates/air_r_parser_tests/src/corpus.rs b/crates/air_r_parser_tests/src/corpus.rs new file mode 100644 index 00000000..930e6c1d --- /dev/null +++ b/crates/air_r_parser_tests/src/corpus.rs @@ -0,0 +1,74 @@ +use std::path::Path; +use std::path::PathBuf; + +pub fn dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus") +} + +/// Every human-authored `*.md`, sorted +pub fn input_files() -> Vec { + files() + .into_iter() + .filter(|path| !is_snapshot(path)) + .collect() +} + +/// Every generated `*.snap.md`, sorted +pub fn snapshot_files() -> Vec { + files() + .into_iter() + .filter(|path| is_snapshot(path)) + .collect() +} + +/// The snapshot beside an input file +pub fn snapshot_of(input: &Path) -> PathBuf { + input.with_extension("snap.md") +} + +/// The path from the corpus root, which is what a snapshot banner records and +/// what a failure message should show +pub fn relative(path: &Path) -> String { + let mut parts: Vec = path + .components() + .rev() + .take_while(|component| component.as_os_str() != "corpus") + .map(|component| component.as_os_str().to_string_lossy().into_owned()) + .collect(); + + parts.reverse(); + parts.join("/") +} + +fn is_snapshot(path: &Path) -> bool { + name(path).ends_with(".snap.md") +} + +fn name(path: &Path) -> &str { + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or("") +} + +fn files() -> Vec { + let mut files = Vec::new(); + let mut directories = vec![dir()]; + + while let Some(directory) = directories.pop() { + let entries = std::fs::read_dir(&directory) + .unwrap_or_else(|err| panic!("cannot read {}: {err}", directory.display())); + + for entry in entries { + let path = entry.expect("cannot read directory entry").path(); + + if path.is_dir() { + directories.push(path); + } else if name(&path).ends_with(".md") { + files.push(path); + } + } + } + + files.sort(); + files +} diff --git a/crates/air_r_parser_tests/src/document.rs b/crates/air_r_parser_tests/src/document.rs new file mode 100644 index 00000000..7725e42b --- /dev/null +++ b/crates/air_r_parser_tests/src/document.rs @@ -0,0 +1,540 @@ +use std::fmt::Write as _; + +/// One corpus test file +/// +/// Covers both human created input files and generated snapshot files. The +/// only difference is that snapshot files have non empty `generated` cells. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Document { + pub sections: Vec
, +} + +/// One `##` heading and everything under it +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Section { + pub name: String, + pub expectation: Expectation, + + pub elements: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Element { + /// Copied verbatim into the snapshot, never parsed + Prose(String), + Block(Block), +} + +/// One R code cell and the CST and diagnostic cells generated from it +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Block { + pub source: String, + pub generated: Vec, +} + +/// A cell the harness produces, in snapshot files only +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Cell { + /// The CST view + Cst(String), + /// Rendered diagnostics + Diagnostics(String), +} + +/// What a heading's blocks must do when parsed +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum Expectation { + /// Every block must parse with zero diagnostics and no bogus nodes + #[default] + Clean, + /// Every block must produce at least one error diagnostic + Error, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DocumentError { + pub line: usize, + pub message: String, +} + +impl std::fmt::Display for DocumentError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "line {}: {}", self.line, self.message) + } +} + +impl std::error::Error for DocumentError {} + +impl Expectation { + fn parse(text: &str, line: usize) -> Result { + match text.trim() { + "error" => Ok(Expectation::Error), + other => Err(DocumentError { + line, + message: format!("unknown marker `{other}`, expected `error`"), + }), + } + } + + fn render(self) -> String { + match self { + Expectation::Clean => String::new(), + Expectation::Error => " [error]".to_string(), + } + } +} + +impl Section { + pub fn blocks(&self) -> impl DoubleEndedIterator { + self.elements.iter().filter_map(|element| match element { + Element::Block(block) => Some(block), + Element::Prose(_) => None, + }) + } + + pub fn blocks_mut(&mut self) -> impl DoubleEndedIterator { + self.elements + .iter_mut() + .filter_map(|element| match element { + Element::Block(block) => Some(block), + Element::Prose(_) => None, + }) + } +} + +impl Document { + pub fn parse(text: &str) -> Result { + Parser::new(text).parse() + } + + pub fn render_snapshot(&self) -> String { + let mut out = String::new(); + writeln!(out, "").unwrap(); + + for section in &self.sections { + writeln!(out, "\n## {}{}", section.name, section.expectation.render()).unwrap(); + + for element in §ion.elements { + match element { + Element::Prose(prose) => writeln!(out, "\n{prose}").unwrap(), + Element::Block(block) => render_block(&mut out, block), + } + } + } + + out + } + + /// A comparable shape for this section, to ensure nothing is dropped when + /// writing the snapshot file + pub fn shape(&self) -> Vec<(&str, Expectation, usize)> { + self.sections + .iter() + .map(|section| { + ( + section.name.as_str(), + section.expectation, + section.blocks().count(), + ) + }) + .collect() + } +} + +fn render_block(out: &mut String, block: &Block) { + render_fence(out, "r", &block.source); + + for cell in &block.generated { + match cell { + Cell::Cst(content) => render_fence(out, "cst", content), + Cell::Diagnostics(content) => render_fence(out, "diagnostics", content), + } + } +} + +fn render_fence(out: &mut String, language: &str, content: &str) { + out.push('\n'); + out.push_str("```"); + out.push_str(language); + out.push('\n'); + out.push_str(content); + // A cell whose content does not end its line would swallow the fence + if !content.is_empty() && !content.ends_with('\n') { + out.push('\n'); + } + out.push_str("```\n"); +} + +struct Parser<'src> { + lines: Vec<&'src str>, + index: usize, + sections: Vec
, + prose: Vec<&'src str>, +} + +impl<'src> Parser<'src> { + fn new(text: &'src str) -> Self { + // `split_inclusive` keeps each line's terminator, so a file that does + // not end in a newline is not silently given one + let lines = text.split_inclusive('\n').collect(); + + Self { + lines, + index: 0, + sections: Vec::new(), + prose: Vec::new(), + } + } + + fn parse(mut self) -> Result { + while let Some(line) = self.next_line() { + let trimmed = trim_newline(line); + + if let Some(heading) = trimmed.strip_prefix("## ") { + self.finish_prose()?; + let (name, expectation) = parse_heading(heading, self.index)?; + self.push_section(name, expectation)?; + } else if let Some(language) = fence_language(trimmed) { + let content = self.read_fenced_content()?; + self.push_cell(language, content)?; + } else if self.sections.is_empty() && trimmed.starts_with("") { + return; + } + + while let Some(line) = self.next_line() { + if trim_newline(line).ends_with("-->") { + return; + } + } + } + + fn read_fenced_content(&mut self) -> Result { + let opened = self.index; + let mut content = String::new(); + + while let Some(line) = self.next_line() { + if trim_newline(line) == "```" { + return Ok(content); + } + content.push_str(line); + } + + Err(DocumentError { + line: opened, + message: "unterminated fenced cell".to_string(), + }) + } + + fn push_section( + &mut self, + name: String, + expectation: Expectation, + ) -> Result<(), DocumentError> { + if self.sections.iter().any(|section| section.name == name) { + return Err(DocumentError { + line: self.index, + message: format!( + "duplicate test name `{name}`; names must be unique within a file" + ), + }); + } + + self.sections.push(Section { + name, + expectation, + elements: Vec::new(), + }); + + Ok(()) + } + + fn push_cell(&mut self, language: &str, content: String) -> Result<(), DocumentError> { + self.finish_prose()?; + + if language == "r" { + let block = Block { + source: content, + generated: Vec::new(), + }; + return self.push_element(Element::Block(block)); + } + + let cell = match language { + "cst" => Cell::Cst(content), + "diagnostics" => Cell::Diagnostics(content), + other => { + return Err(DocumentError { + line: self.index, + message: format!( + "unknown cell language `{other}`, expected `r`, `cst` or `diagnostics`" + ), + }); + } + }; + + self.push_generated(cell) + } + + fn push_generated(&mut self, cell: Cell) -> Result<(), DocumentError> { + self.finish_prose()?; + + let line = self.index; + let block = self + .sections + .last_mut() + .and_then(|section| section.blocks_mut().next_back()); + + match block { + Some(block) => { + block.generated.push(cell); + Ok(()) + } + None => Err(DocumentError { + line, + message: "generated cell with no ```r block before it".to_string(), + }), + } + } + + fn push_element(&mut self, element: Element) -> Result<(), DocumentError> { + match self.sections.last_mut() { + Some(section) => { + section.elements.push(element); + Ok(()) + } + None => Err(DocumentError { + line: self.index, + message: "content before the first `## ` heading".to_string(), + }), + } + } + + /// Flushes accumulated prose, dropping its boundary blank lines + /// + /// The trim is what makes rendering idempotent: loosely spaced input + /// converges after one `just accept` instead of gaining a blank line on + /// every pass. + fn finish_prose(&mut self) -> Result<(), DocumentError> { + let lines = std::mem::take(&mut self.prose); + let prose = lines.concat(); + let prose = prose.trim_matches('\n'); + + if prose.is_empty() { + return Ok(()); + } + + self.push_element(Element::Prose(prose.to_string())) + } +} + +fn parse_heading(heading: &str, line: usize) -> Result<(String, Expectation), DocumentError> { + let heading = heading.trim(); + + // The marker is the trailing bracketed group, so a name may itself contain + // brackets — `Unsupported Subset2 With `]]` Over Multiple Lines` does + let Some(open) = heading.strip_suffix(']').and_then(|rest| rest.rfind('[')) else { + return Ok((heading.to_string(), Expectation::default())); + }; + + let inside = &heading[open + 1..heading.len() - 1]; + if inside.contains('[') || inside.contains(']') { + return Ok((heading.to_string(), Expectation::default())); + } + + let expectation = Expectation::parse(inside, line)?; + Ok((heading[..open].trim().to_string(), expectation)) +} + +fn fence_language(line: &str) -> Option<&str> { + let language = line.strip_prefix("```")?; + + if language.is_empty() || language.contains(' ') { + None + } else { + Some(language) + } +} + +fn trim_newline(line: &str) -> &str { + line.trim_end_matches('\n').trim_end_matches('\r') +} + +#[cfg(test)] +mod tests { + use super::*; + + fn document(text: &str) -> Document { + Document::parse(text).expect("the document parses") + } + + fn error(text: &str) -> String { + Document::parse(text) + .expect_err("the document is rejected") + .message + } + + const INPUT: &str = "\ +## Numbers + +```r +1 +``` + +## Dollar [error] + +Why this test exists. + +```r +a$ +``` + +```r +b$ +``` +"; + + #[test] + fn parses_headings_markers_and_blocks() { + let document = document(INPUT); + + assert_eq!( + document.shape(), + vec![ + ("Numbers", Expectation::Clean, 1), + ("Dollar", Expectation::Error, 2), + ] + ); + + let sources: Vec<_> = document.sections[1] + .blocks() + .map(|block| block.source.as_str()) + .collect(); + + assert_eq!(sources, vec!["a$\n", "b$\n"]); + } + + /// Prose keeps its place in the element sequence, so rendering can put each + /// block's cells beside it + #[test] + fn prose_interleaves_with_blocks() { + let document = document(INPUT); + + assert!(matches!( + document.sections[1].elements.as_slice(), + [ + Element::Prose(prose), + Element::Block(_), + Element::Block(_) + ] if prose == "Why this test exists." + )); + } + + /// Textual idempotence is the contract, since CI regenerates and asserts a + /// clean tree. A cell whose content does not end its line gains a newline, so + /// the structural round-trip holds only for content that already ends in one. + #[test] + fn round_trips_through_a_snapshot() { + let mut document = document(INPUT); + document.sections[0].blocks_mut().next().unwrap().generated = + vec![Cell::Cst("RRoot { }\n".to_string())]; + document.sections[1].blocks_mut().for_each(|block| { + block.generated = vec![ + Cell::Cst("RBogusExpression { }\n".to_string()), + Cell::Diagnostics("error\n".to_string()), + ] + }); + + let rendered = document.render_snapshot(); + assert_eq!(Document::parse(&rendered).unwrap(), document); + assert_eq!( + Document::parse(&rendered).unwrap().render_snapshot(), + rendered + ); + } + + /// Loosely spaced input must converge after one pass rather than gaining a + /// blank line on every one + #[test] + fn rendering_is_idempotent_over_prose_spacing() { + let loose = "## A\n\n\n\nSpaced out.\n\n\n\n```r\n1\n```\n"; + let rendered = document(loose).render_snapshot(); + + assert_eq!(document(&rendered).render_snapshot(), rendered); + } + + #[test] + fn an_empty_block_survives() { + let document = document("## A\n\n```r\n```\n"); + assert_eq!(document.sections[0].blocks().next().unwrap().source, ""); + + let rendered = document.render_snapshot(); + assert_eq!(Document::parse(&rendered).unwrap(), document); + } + + /// A name may hold brackets of its own; only a trailing group is a marker + #[test] + fn markers_are_the_trailing_bracket_group() { + let document = document("## Subset2 With `]]` (#177) [error]\n\n```r\n1\n```\n"); + + assert_eq!( + document.shape(), + vec![("Subset2 With `]]` (#177)", Expectation::Error, 1)] + ); + } + + #[test] + fn rejects_content_before_the_first_heading() { + assert!(error("Stray prose.\n\n## A\n\n```r\n1\n```\n").contains("before the first")); + } + + #[test] + fn rejects_a_heading_with_no_block() { + assert!(error("## A\n\nJust prose.\n").contains("no ```r block")); + } + + #[test] + fn rejects_a_duplicate_name() { + let text = "## A\n\n```r\n1\n```\n\n## A\n\n```r\n2\n```\n"; + assert!(error(text).contains("duplicate test name")); + } + + #[test] + fn rejects_an_unknown_marker() { + assert!(error("## A [wip]\n\n```r\n1\n```\n").contains("unknown marker")); + } + + #[test] + fn rejects_an_unterminated_cell() { + assert!(error("## A\n\n```r\n1\n").contains("unterminated")); + } +} diff --git a/crates/air_r_parser_tests/src/lib.rs b/crates/air_r_parser_tests/src/lib.rs new file mode 100644 index 00000000..3d843a2f --- /dev/null +++ b/crates/air_r_parser_tests/src/lib.rs @@ -0,0 +1,14 @@ +pub mod check; +pub mod corpus; +pub mod document; +pub mod runner; + +pub use crate::check::check; +pub use crate::document::Block; +pub use crate::document::Cell; +pub use crate::document::Document; +pub use crate::document::DocumentError; +pub use crate::document::Element; +pub use crate::document::Expectation; +pub use crate::document::Section; +pub use crate::runner::run_file; diff --git a/crates/air_r_parser_tests/src/runner.rs b/crates/air_r_parser_tests/src/runner.rs new file mode 100644 index 00000000..d6928ec3 --- /dev/null +++ b/crates/air_r_parser_tests/src/runner.rs @@ -0,0 +1,96 @@ +use crate::check::CheckError; +use crate::check::check; +use crate::corpus; +use crate::document::Document; +use std::fmt::Write as _; +use std::fs; +use std::path::Path; + +pub fn run_file(input_path: &Path) { + let display = corpus::relative(input_path); + let snapshot_path = corpus::snapshot_of(input_path); + + let input = + fs::read_to_string(input_path).unwrap_or_else(|err| panic!("cannot read {display}: {err}")); + let input = Document::parse(&input).unwrap_or_else(|err| panic!("{display}: {err}")); + + let committed = fs::read_to_string(&snapshot_path) + .unwrap_or_else(|_| panic!("{display} has no snapshot. Run `just accept`.")); + + // Marker violations fail before any diffing, so a parser bug is not + // reported as a confusing snapshot mismatch + let mut generated = input.clone(); + let mut failures = Vec::new(); + + for section in &mut generated.sections { + let expectation = section.expectation; + let name = section.name.clone(); + + // A heading may hold several blocks, so the name alone would not say + // which one failed + for (index, block) in section.blocks_mut().enumerate() { + match check(block, expectation) { + Ok(cells) => block.generated = cells, + Err(error) => failures.push((name.clone(), index, error)), + } + } + } + + if !failures.is_empty() { + panic!("{}", render_failures(&display, &failures)); + } + + // Check that the input and the snapshot are structurally the same + let snapshot = Document::parse(&committed) + .unwrap_or_else(|err| panic!("{}: {err}", corpus::relative(&snapshot_path))); + + assert_eq!( + input.shape(), + snapshot.shape(), + "{display} and its snapshot have drifted apart — run `just accept`", + ); + + let rendered = generated.render_snapshot(); + if rendered != committed { + let diff = similar::TextDiff::from_lines(&committed, &rendered); + panic!( + "{}\n{} is out of date. Run `just accept`.", + diff.unified_diff().header("committed", "generated"), + corpus::relative(&snapshot_path), + ); + } +} + +fn render_failures(display: &str, failures: &[(String, usize, CheckError)]) -> String { + let mut out = format!("{} failing block(s) in {display}\n", failures.len()); + + for (name, index, error) in failures { + write!(out, "\n## {name} (block {})\n\n", index + 1).unwrap(); + out.push_str(&render_failure(error)); + } + + out +} + +fn render_failure(error: &CheckError) -> String { + match error { + CheckError::RoundTrip { expected, got } => format!( + "The parse did not round-trip, so a token was dropped or invented.\n\ + input: {expected:?}\n\ + output: {got:?}\n" + ), + CheckError::ExpectedError => { + "Marked `[error]` but produced no error diagnostic.\n".to_string() + } + CheckError::UnexpectedError(diagnostics) => { + format!("Expected a clean parse, but:\n\n{diagnostics}") + } + CheckError::BogusNodes(ast) => { + format!("Bogus nodes in a clean test:\n\n{ast}\n") + } + CheckError::UndiagnosedBogusNode { ranges, ast } => format!( + "Bogus nodes that no diagnostic points at, at {ranges:?}.\n\ + {ast}\n" + ), + } +} diff --git a/crates/air_r_parser_tests/tests/corpus/control/for.md b/crates/air_r_parser_tests/tests/corpus/control/for.md new file mode 100644 index 00000000..95638620 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/control/for.md @@ -0,0 +1,69 @@ +## For + +```r +for (x in y) + f + +for (x in 5:6) { + for (y in ys) { + z + } +} + +for (x in y) for (y in z) x + y + +for (x in y + +) 1 + 1 +``` + +## For no body [error] + +```r +for (i in 1:5) +``` + +## For with comments + +```r +for (i in 1) i + +for ( + i # comment + in # comment2 + 1 # comment3 +) i + +for # dangling1 + ( # dangling2 + a in 1 + ) # dangling3 + a +``` + +## `...` and `..i` as the `for` variable + +Technically allowed! + +```r +for (... in 1:2) print(get("...")) +for (..1 in 1:2) print(get("..1")) +``` + +## A `for` variable that is not a name [error] + +```r +for (1 in x) y +``` + +## A `for` with no variable [error] + +```r +for (in x) y +``` + +## A `for` with no sequence [error] + +```r +for (i in) y +``` diff --git a/crates/air_r_parser_tests/tests/corpus/control/for.snap.md b/crates/air_r_parser_tests/tests/corpus/control/for.snap.md new file mode 100644 index 00000000..4bffa836 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/control/for.snap.md @@ -0,0 +1,538 @@ + + +## For + +```r +for (x in y) + f + +for (x in 5:6) { + for (y in ys) { + z + } +} + +for (x in y) for (y in z) x + y + +for (x in y + +) 1 + 1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@0..4 "for" [] [Whitespace(" ")], + l_paren_token: L_PAREN@4..5 "(" [] [], + variable: RIdentifier { + name_token: IDENT@5..7 "x" [] [Whitespace(" ")], + }, + in_token: IN_KW@7..10 "in" [] [Whitespace(" ")], + sequence: RIdentifier { + name_token: IDENT@10..11 "y" [] [], + }, + r_paren_token: R_PAREN@11..12 ")" [] [], + body: RIdentifier { + name_token: IDENT@12..16 "f" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@16..22 "for" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@22..23 "(" [] [], + variable: RIdentifier { + name_token: IDENT@23..25 "x" [] [Whitespace(" ")], + }, + in_token: IN_KW@25..28 "in" [] [Whitespace(" ")], + sequence: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@28..29 "5" [] [], + }, + operator: COLON@29..30 ":" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@30..31 "6" [] [], + }, + }, + r_paren_token: R_PAREN@31..33 ")" [] [Whitespace(" ")], + body: RBracedStatements { + l_curly_token: L_CURLY@33..34 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@34..41 "for" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@41..42 "(" [] [], + variable: RIdentifier { + name_token: IDENT@42..44 "y" [] [Whitespace(" ")], + }, + in_token: IN_KW@44..47 "in" [] [Whitespace(" ")], + sequence: RIdentifier { + name_token: IDENT@47..49 "ys" [] [], + }, + r_paren_token: R_PAREN@49..51 ")" [] [Whitespace(" ")], + body: RBracedStatements { + l_curly_token: L_CURLY@51..52 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@52..58 "z" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@58..62 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@62..64 "}" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@64..70 "for" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@70..71 "(" [] [], + variable: RIdentifier { + name_token: IDENT@71..73 "x" [] [Whitespace(" ")], + }, + in_token: IN_KW@73..76 "in" [] [Whitespace(" ")], + sequence: RIdentifier { + name_token: IDENT@76..77 "y" [] [], + }, + r_paren_token: R_PAREN@77..79 ")" [] [Whitespace(" ")], + body: RForStatement { + for_token: FOR_KW@79..83 "for" [] [Whitespace(" ")], + l_paren_token: L_PAREN@83..84 "(" [] [], + variable: RIdentifier { + name_token: IDENT@84..86 "y" [] [Whitespace(" ")], + }, + in_token: IN_KW@86..89 "in" [] [Whitespace(" ")], + sequence: RIdentifier { + name_token: IDENT@89..90 "z" [] [], + }, + r_paren_token: R_PAREN@90..92 ")" [] [Whitespace(" ")], + body: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@92..94 "x" [] [Whitespace(" ")], + }, + operator: PLUS@94..96 "+" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@96..97 "y" [] [], + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@97..103 "for" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@103..104 "(" [] [], + variable: RIdentifier { + name_token: IDENT@104..106 "x" [] [Whitespace(" ")], + }, + in_token: IN_KW@106..109 "in" [] [Whitespace(" ")], + sequence: RIdentifier { + name_token: IDENT@109..110 "y" [] [], + }, + r_paren_token: R_PAREN@110..114 ")" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + body: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@114..116 "1" [] [Whitespace(" ")], + }, + operator: PLUS@116..118 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@118..119 "1" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@119..120 "" [Newline("\n")] [], +} +``` + +## For no body [error] + +```r +for (i in 1:5) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@0..4 "for" [] [Whitespace(" ")], + l_paren_token: L_PAREN@4..5 "(" [] [], + variable: RIdentifier { + name_token: IDENT@5..7 "i" [] [Whitespace(" ")], + }, + in_token: IN_KW@7..10 "in" [] [Whitespace(" ")], + sequence: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@10..11 "1" [] [], + }, + operator: COLON@11..12 ":" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@12..13 "5" [] [], + }, + }, + r_paren_token: R_PAREN@13..14 ")" [] [], + body: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@14..15 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a loop body but instead found the end of the file. + + 1 │ for (i in 1:5) + > 2 │ + │ +``` + +## For with comments + +```r +for (i in 1) i + +for ( + i # comment + in # comment2 + 1 # comment3 +) i + +for # dangling1 + ( # dangling2 + a in 1 + ) # dangling3 + a +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@0..4 "for" [] [Whitespace(" ")], + l_paren_token: L_PAREN@4..5 "(" [] [], + variable: RIdentifier { + name_token: IDENT@5..7 "i" [] [Whitespace(" ")], + }, + in_token: IN_KW@7..10 "in" [] [Whitespace(" ")], + sequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@10..11 "1" [] [], + }, + r_paren_token: R_PAREN@11..13 ")" [] [Whitespace(" ")], + body: RIdentifier { + name_token: IDENT@13..14 "i" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@14..20 "for" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@20..21 "(" [] [], + variable: RIdentifier { + name_token: IDENT@21..35 "i" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment")], + }, + in_token: IN_KW@35..51 "in" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")], + sequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@51..66 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")], + }, + r_paren_token: R_PAREN@66..69 ")" [Newline("\n")] [Whitespace(" ")], + body: RIdentifier { + name_token: IDENT@69..70 "i" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@70..87 "for" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# dangling1")], + l_paren_token: L_PAREN@87..103 "(" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# dangling2")], + variable: RIdentifier { + name_token: IDENT@103..110 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + in_token: IN_KW@110..113 "in" [] [Whitespace(" ")], + sequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@113..114 "1" [] [], + }, + r_paren_token: R_PAREN@114..130 ")" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# dangling3")], + body: RIdentifier { + name_token: IDENT@130..134 "a" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@134..135 "" [Newline("\n")] [], +} +``` + +## `...` and `..i` as the `for` variable + +Technically allowed! + +```r +for (... in 1:2) print(get("...")) +for (..1 in 1:2) print(get("..1")) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@0..4 "for" [] [Whitespace(" ")], + l_paren_token: L_PAREN@4..5 "(" [] [], + variable: RDots { + value_token: DOTS@5..9 "..." [] [Whitespace(" ")], + }, + in_token: IN_KW@9..12 "in" [] [Whitespace(" ")], + sequence: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@12..13 "1" [] [], + }, + operator: COLON@13..14 ":" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@14..15 "2" [] [], + }, + }, + r_paren_token: R_PAREN@15..17 ")" [] [Whitespace(" ")], + body: RCall { + function: RIdentifier { + name_token: IDENT@17..22 "print" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@22..23 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RCall { + function: RIdentifier { + name_token: IDENT@23..26 "get" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@26..27 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@27..32 "\"...\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@32..33 ")" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@33..34 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@34..39 "for" [Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@39..40 "(" [] [], + variable: RDotDotI { + value_token: DOTDOTI@40..44 "..1" [] [Whitespace(" ")], + }, + in_token: IN_KW@44..47 "in" [] [Whitespace(" ")], + sequence: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@47..48 "1" [] [], + }, + operator: COLON@48..49 ":" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@49..50 "2" [] [], + }, + }, + r_paren_token: R_PAREN@50..52 ")" [] [Whitespace(" ")], + body: RCall { + function: RIdentifier { + name_token: IDENT@52..57 "print" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@57..58 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RCall { + function: RIdentifier { + name_token: IDENT@58..61 "get" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@61..62 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@62..67 "\"..1\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@67..68 ")" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@68..69 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@69..70 "" [Newline("\n")] [], +} +``` + +## A `for` variable that is not a name [error] + +```r +for (1 in x) y +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@0..4 "for" [] [Whitespace(" ")], + l_paren_token: L_PAREN@4..5 "(" [] [], + variable: RBogusForVariable { + items: [ + R_DOUBLE_LITERAL@5..7 "1" [] [Whitespace(" ")], + ], + }, + in_token: IN_KW@7..10 "in" [] [Whitespace(" ")], + sequence: RIdentifier { + name_token: IDENT@10..11 "x" [] [], + }, + r_paren_token: R_PAREN@11..13 ")" [] [Whitespace(" ")], + body: RIdentifier { + name_token: IDENT@13..14 "y" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@14..15 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a loop variable but instead found '1'. + + > 1 │ for (1 in x) y + │ ^ + 2 │ +``` + +## A `for` with no variable [error] + +```r +for (in x) y +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@0..4 "for" [] [Whitespace(" ")], + l_paren_token: L_PAREN@4..5 "(" [] [], + variable: missing (required), + in_token: IN_KW@5..8 "in" [] [Whitespace(" ")], + sequence: RIdentifier { + name_token: IDENT@8..9 "x" [] [], + }, + r_paren_token: R_PAREN@9..11 ")" [] [Whitespace(" ")], + body: RIdentifier { + name_token: IDENT@11..12 "y" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@12..13 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a loop variable but instead found 'in'. + + > 1 │ for (in x) y + │ ^^ + 2 │ +``` + +## A `for` with no sequence [error] + +```r +for (i in) y +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@0..4 "for" [] [Whitespace(" ")], + l_paren_token: L_PAREN@4..5 "(" [] [], + variable: RIdentifier { + name_token: IDENT@5..7 "i" [] [Whitespace(" ")], + }, + in_token: IN_KW@7..9 "in" [] [], + sequence: missing (required), + r_paren_token: R_PAREN@9..11 ")" [] [Whitespace(" ")], + body: RIdentifier { + name_token: IDENT@11..12 "y" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@12..13 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:10 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a loop sequence but instead found ')'. + + > 1 │ for (i in) y + │ ^ + 2 │ +``` diff --git a/crates/air_r_parser_tests/tests/corpus/control/if.md b/crates/air_r_parser_tests/tests/corpus/control/if.md new file mode 100644 index 00000000..c9a5aa03 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/control/if.md @@ -0,0 +1,259 @@ +## If + +```r +if (x) + log(y) + +if (a.b) { + log(c) + d +} + +if ( + x < + y +) { + z +} +``` + +## If-else + +```r +if (x) + y else if (a) + b + +if (x) + y else if (a) + b else d + +if (a) { + c + d +} else { + e +} +``` + +## Complex if expression [error] + +```r +# Invalid at top level due to newline before `else`, so not a real if statement +if (TRUE) { + 1 +} +else { + 2 +} + +# Invalid for same reason as above +if (TRUE) + 1 +else + 2 +``` + +## Complex if expression - 2 + +```r +# Valid inside `{` only due to special `else` handling with newlines +{ + if (TRUE) { + 1 + } + else { + 2 + } +} + +# Valid with comments in special newline territory +{ + if (TRUE) { + 1 + } + # hi there + + # another one! + + else { + 2 + } +} + +# Valid. This test ensures we handle the newline after `1 + 1` correctly (r-lib/tree-sitter-r#125). +{ + if (TRUE) + + 1 + 1 +} + +# Valid. Newlines are allowed between the `else` and the `alternative`, even at top level (r-lib/tree-sitter-r#141). +if (TRUE) { + 1 +} else +{ + 2 +} + +# Valid. Same as above but in `{ }` so it is valid no matter where the newlines are. +{ + if (TRUE) { + 1 + } else + { + 2 + } +} + +# Valid. Newlines and comments are allowed between the `else` and the `alternative`, even at top level. +if (TRUE) { + 1 +} else + +# do this alternative +{ + 2 +} + +# Valid. Newlines are allowed between the `else` and the `alternative`, even at top level. +if (TRUE) 1 else + 2 +``` + +## Identifiers starting with `else` that are not the `else` keyword (r-lib/tree-sitter-r#200, r-lib/tree-sitter-r#499) + +```r +# ASCII letter +{ + if (TRUE) 1 + elseidx <- 1 +} + +# ASCII number +{ + if (TRUE) 1 + else1idx <- 1 +} + +# ASCII `_` +{ + if (TRUE) 1 + else_idx <- 1 +} + +# ASCII `.` +{ + if (TRUE) 1 + else.idx <- 1 +} + +# Non-ASCII, but considered `iswalnum()` in R with UTF-8 locale. +# Our lexer uses `XID_Continue`, which allows this and aligns with R. +{ + if (TRUE) 1 + elseμ <- 1 +} + +# Non-ASCII (U+00B7), rejected by R as syntax error since `iswalnum()` returns false. +# `XID_Continue` allows this, but that's ok since the R side behavior is a +# syntax error. +{ + if (TRUE) 1 + else· <- 1 +} +``` + +## Identifiers starting with `else` that are not the `else` keyword - 2 (r-lib/tree-sitter-r#200) [error] + +```r +# Correctly treats `else.x` as a single identifier, not as `else` followed by `.x`, +# which also throws a diagnostic about a missing statement separator. +# See r-lib/tree-sitter-r#194. +if (TRUE) { + 1 +} else.x +``` + +## If with comments + +```r +if # comment1 + (a) # comment2 + 1 else # comment3 + 2 # comment4 +``` + +## If and else + +```r +if (a) 1 +if (a) 1 else 2 +if (a) 1 else if (b) 2 else 3 +``` + +An `else` binds to the nearest `if`. + +```r +if (a) if (b) c else d +``` + +## A consequence and an alternative stop at the binary `?` + +Both share the rank a function or loop body has, so `if (a) b ? c` must parse as +`(if (a) b) ? c`. + +```r +if (a) b ? c +if (a) b else c ? d +``` + +An assignment binds tighter than either, so the consequence swallows it. + +```r +if (a) b <- c else d +1 + if (a) b +``` + +## A newline before `else` at top level [error] + +Fatal here. The `if` ends at the consequence and the `else` is left over. + +```r +if (a) 1 +else 2 +``` + +## A newline before `else` inside a delimiter + +Newlines carry no meaning inside `(`, `[`, or `[[` + +```r +f(if (a) 1 + else 2) +``` + +```r +x[if (a) 1 + else 2] +``` + +```r +x[[if (a) 1 + else 2]] +``` + +## A newline between `else` and its alternative + +Allowed even at top level, unlike a newline before the `else` itself (r-lib/tree-sitter-r#141). + +```r +if (a) 1 else + 2 +``` + +## An `if` with no condition [error] + +```r +if () 1 +``` diff --git a/crates/air_r_parser_tests/tests/corpus/control/if.snap.md b/crates/air_r_parser_tests/tests/corpus/control/if.snap.md new file mode 100644 index 00000000..143867ec --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/control/if.snap.md @@ -0,0 +1,1663 @@ + + +## If + +```r +if (x) + log(y) + +if (a.b) { + log(c) + d +} + +if ( + x < + y +) { + z +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..3 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@3..4 "(" [] [], + condition: RIdentifier { + name_token: IDENT@4..5 "x" [] [], + }, + r_paren_token: R_PAREN@5..6 ")" [] [], + consequence: RCall { + function: RIdentifier { + name_token: IDENT@6..12 "log" [Newline("\n"), Whitespace(" ")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@12..13 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@13..14 "y" [] [], + }, + }, + ], + r_paren_token: R_PAREN@14..15 ")" [] [], + }, + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@15..20 "if" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@20..21 "(" [] [], + condition: RIdentifier { + name_token: IDENT@21..24 "a.b" [] [], + }, + r_paren_token: R_PAREN@24..26 ")" [] [Whitespace(" ")], + consequence: RBracedStatements { + l_curly_token: L_CURLY@26..27 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@27..33 "log" [Newline("\n"), Whitespace(" ")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@33..34 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@34..35 "c" [] [], + }, + }, + ], + r_paren_token: R_PAREN@35..36 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@36..40 "d" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@40..42 "}" [Newline("\n")] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@42..47 "if" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@47..48 "(" [] [], + condition: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@48..53 "x" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: LESS_THAN@53..54 "<" [] [], + right: RIdentifier { + name_token: IDENT@54..58 "y" [Newline("\n"), Whitespace(" ")] [], + }, + }, + r_paren_token: R_PAREN@58..61 ")" [Newline("\n")] [Whitespace(" ")], + consequence: RBracedStatements { + l_curly_token: L_CURLY@61..62 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@62..66 "z" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@66..68 "}" [Newline("\n")] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@68..69 "" [Newline("\n")] [], +} +``` + +## If-else + +```r +if (x) + y else if (a) + b + +if (x) + y else if (a) + b else d + +if (a) { + c + d +} else { + e +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..3 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@3..4 "(" [] [], + condition: RIdentifier { + name_token: IDENT@4..5 "x" [] [], + }, + r_paren_token: R_PAREN@5..6 ")" [] [], + consequence: RIdentifier { + name_token: IDENT@6..11 "y" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@11..16 "else" [] [Whitespace(" ")], + alternative: RIfStatement { + if_token: IF_KW@16..19 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@19..20 "(" [] [], + condition: RIdentifier { + name_token: IDENT@20..21 "a" [] [], + }, + r_paren_token: R_PAREN@21..22 ")" [] [], + consequence: RIdentifier { + name_token: IDENT@22..26 "b" [Newline("\n"), Whitespace(" ")] [], + }, + else_clause: missing (optional), + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@26..31 "if" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@31..32 "(" [] [], + condition: RIdentifier { + name_token: IDENT@32..33 "x" [] [], + }, + r_paren_token: R_PAREN@33..34 ")" [] [], + consequence: RIdentifier { + name_token: IDENT@34..39 "y" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@39..44 "else" [] [Whitespace(" ")], + alternative: RIfStatement { + if_token: IF_KW@44..47 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@47..48 "(" [] [], + condition: RIdentifier { + name_token: IDENT@48..49 "a" [] [], + }, + r_paren_token: R_PAREN@49..50 ")" [] [], + consequence: RIdentifier { + name_token: IDENT@50..55 "b" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@55..60 "else" [] [Whitespace(" ")], + alternative: RIdentifier { + name_token: IDENT@60..61 "d" [] [], + }, + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@61..66 "if" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@66..67 "(" [] [], + condition: RIdentifier { + name_token: IDENT@67..68 "a" [] [], + }, + r_paren_token: R_PAREN@68..70 ")" [] [Whitespace(" ")], + consequence: RBracedStatements { + l_curly_token: L_CURLY@70..71 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@71..75 "c" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@75..79 "d" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@79..82 "}" [Newline("\n")] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@82..87 "else" [] [Whitespace(" ")], + alternative: RBracedStatements { + l_curly_token: L_CURLY@87..88 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@88..92 "e" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@92..94 "}" [Newline("\n")] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@94..95 "" [Newline("\n")] [], +} +``` + +## Complex if expression [error] + +```r +# Invalid at top level due to newline before `else`, so not a real if statement +if (TRUE) { + 1 +} +else { + 2 +} + +# Invalid for same reason as above +if (TRUE) + 1 +else + 2 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..83 "if" [Comments("# Invalid at top leve ..."), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@83..84 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@84..88 "TRUE" [] [], + }, + r_paren_token: R_PAREN@88..90 ")" [] [Whitespace(" ")], + consequence: RBracedStatements { + l_curly_token: L_CURLY@90..91 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@91..95 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@95..97 "}" [Newline("\n")] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + ELSE_KW@97..103 "else" [Newline("\n")] [Whitespace(" ")], + L_CURLY@103..104 "{" [] [], + ], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@104..108 "2" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_CURLY@108..110 "}" [Newline("\n")] [], + ], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@110..150 "if" [Newline("\n"), Newline("\n"), Comments("# Invalid for same re ..."), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@150..151 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@151..155 "TRUE" [] [], + }, + r_paren_token: R_PAREN@155..156 ")" [] [], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@156..160 "1" [Newline("\n"), Whitespace(" ")] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + ELSE_KW@160..165 "else" [Newline("\n")] [], + ], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@165..169 "2" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@169..170 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:5:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found 'else {'. + + 3 │ 1 + 4 │ } + > 5 │ else { + │ ^^^^^^ + 6 │ 2 + 7 │ } + +test.R:7:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '}'. + + 5 │ else { + 6 │ 2 + > 7 │ } + │ ^ + 8 │ + 9 │ # Invalid for same reason as above + +test.R:12:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found 'else'. + + 10 │ if (TRUE) + 11 │ 1 + > 12 │ else + │ ^^^^ + 13 │ 2 + 14 │ +``` + +## Complex if expression - 2 + +```r +# Valid inside `{` only due to special `else` handling with newlines +{ + if (TRUE) { + 1 + } + else { + 2 + } +} + +# Valid with comments in special newline territory +{ + if (TRUE) { + 1 + } + # hi there + + # another one! + + else { + 2 + } +} + +# Valid. This test ensures we handle the newline after `1 + 1` correctly (r-lib/tree-sitter-r#125). +{ + if (TRUE) + + 1 + 1 +} + +# Valid. Newlines are allowed between the `else` and the `alternative`, even at top level (r-lib/tree-sitter-r#141). +if (TRUE) { + 1 +} else +{ + 2 +} + +# Valid. Same as above but in `{ }` so it is valid no matter where the newlines are. +{ + if (TRUE) { + 1 + } else + { + 2 + } +} + +# Valid. Newlines and comments are allowed between the `else` and the `alternative`, even at top level. +if (TRUE) { + 1 +} else + +# do this alternative +{ + 2 +} + +# Valid. Newlines are allowed between the `else` and the `alternative`, even at top level. +if (TRUE) 1 else + 2 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..70 "{" [Comments("# Valid inside `{` on ..."), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@70..76 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@76..77 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@77..81 "TRUE" [] [], + }, + r_paren_token: R_PAREN@81..83 ")" [] [Whitespace(" ")], + consequence: RBracedStatements { + l_curly_token: L_CURLY@83..84 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@84..90 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@90..94 "}" [Newline("\n"), Whitespace(" ")] [], + }, + else_clause: RElseClause { + else_token: ELSE_KW@94..102 "else" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + alternative: RBracedStatements { + l_curly_token: L_CURLY@102..103 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@103..109 "2" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@109..113 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@113..115 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@115..169 "{" [Newline("\n"), Newline("\n"), Comments("# Valid with comments ..."), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@169..175 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@175..176 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@176..180 "TRUE" [] [], + }, + r_paren_token: R_PAREN@180..182 ")" [] [Whitespace(" ")], + consequence: RBracedStatements { + l_curly_token: L_CURLY@182..183 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@183..189 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@189..193 "}" [Newline("\n"), Whitespace(" ")] [], + }, + else_clause: RElseClause { + else_token: ELSE_KW@193..233 "else" [Newline("\n"), Whitespace(" "), Comments("# hi there"), Newline("\n"), Newline("\n"), Whitespace(" "), Comments("# another one!"), Newline("\n"), Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + alternative: RBracedStatements { + l_curly_token: L_CURLY@233..234 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@234..240 "2" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@240..244 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@244..246 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@246..349 "{" [Newline("\n"), Newline("\n"), Comments("# Valid. This test en ..."), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@349..355 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@355..356 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@356..360 "TRUE" [] [], + }, + r_paren_token: R_PAREN@360..361 ")" [] [], + consequence: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@361..369 "1" [Newline("\n"), Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: PLUS@369..371 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@371..372 "1" [] [], + }, + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@372..374 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@374..496 "if" [Newline("\n"), Newline("\n"), Comments("# Valid. Newlines are ..."), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@496..497 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@497..501 "TRUE" [] [], + }, + r_paren_token: R_PAREN@501..503 ")" [] [Whitespace(" ")], + consequence: RBracedStatements { + l_curly_token: L_CURLY@503..504 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@504..508 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@508..511 "}" [Newline("\n")] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@511..515 "else" [] [], + alternative: RBracedStatements { + l_curly_token: L_CURLY@515..517 "{" [Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@517..521 "2" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@521..523 "}" [Newline("\n")] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@523..611 "{" [Newline("\n"), Newline("\n"), Comments("# Valid. Same as abov ..."), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@611..617 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@617..618 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@618..622 "TRUE" [] [], + }, + r_paren_token: R_PAREN@622..624 ")" [] [Whitespace(" ")], + consequence: RBracedStatements { + l_curly_token: L_CURLY@624..625 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@625..631 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@631..636 "}" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@636..640 "else" [] [], + alternative: RBracedStatements { + l_curly_token: L_CURLY@640..644 "{" [Newline("\n"), Whitespace(" ")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@644..650 "2" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@650..654 "}" [Newline("\n"), Whitespace(" ")] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@654..656 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@656..765 "if" [Newline("\n"), Newline("\n"), Comments("# Valid. Newlines and ..."), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@765..766 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@766..770 "TRUE" [] [], + }, + r_paren_token: R_PAREN@770..772 ")" [] [Whitespace(" ")], + consequence: RBracedStatements { + l_curly_token: L_CURLY@772..773 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@773..777 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@777..780 "}" [Newline("\n")] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@780..784 "else" [] [], + alternative: RBracedStatements { + l_curly_token: L_CURLY@784..809 "{" [Newline("\n"), Newline("\n"), Comments("# do this alternative"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@809..813 "2" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@813..815 "}" [Newline("\n")] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@815..911 "if" [Newline("\n"), Newline("\n"), Comments("# Valid. Newlines are ..."), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@911..912 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@912..916 "TRUE" [] [], + }, + r_paren_token: R_PAREN@916..918 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@918..920 "1" [] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@920..924 "else" [] [], + alternative: RDoubleValue { + value_token: R_DOUBLE_LITERAL@924..928 "2" [Newline("\n"), Whitespace(" ")] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@928..929 "" [Newline("\n")] [], +} +``` + +## Identifiers starting with `else` that are not the `else` keyword (r-lib/tree-sitter-r#200, r-lib/tree-sitter-r#499) + +```r +# ASCII letter +{ + if (TRUE) 1 + elseidx <- 1 +} + +# ASCII number +{ + if (TRUE) 1 + else1idx <- 1 +} + +# ASCII `_` +{ + if (TRUE) 1 + else_idx <- 1 +} + +# ASCII `.` +{ + if (TRUE) 1 + else.idx <- 1 +} + +# Non-ASCII, but considered `iswalnum()` in R with UTF-8 locale. +# Our lexer uses `XID_Continue`, which allows this and aligns with R. +{ + if (TRUE) 1 + elseμ <- 1 +} + +# Non-ASCII (U+00B7), rejected by R as syntax error since `iswalnum()` returns false. +# `XID_Continue` allows this, but that's ok since the R side behavior is a +# syntax error. +{ + if (TRUE) 1 + else· <- 1 +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..16 "{" [Comments("# ASCII letter"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@16..22 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@22..23 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@23..27 "TRUE" [] [], + }, + r_paren_token: R_PAREN@27..29 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@29..30 "1" [] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@30..41 "elseidx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: ASSIGN@41..44 "<-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@44..45 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@45..47 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@47..65 "{" [Newline("\n"), Newline("\n"), Comments("# ASCII number"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@65..71 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@71..72 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@72..76 "TRUE" [] [], + }, + r_paren_token: R_PAREN@76..78 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@78..79 "1" [] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@79..91 "else1idx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: ASSIGN@91..94 "<-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@94..95 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@95..97 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@97..112 "{" [Newline("\n"), Newline("\n"), Comments("# ASCII `_`"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@112..118 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@118..119 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@119..123 "TRUE" [] [], + }, + r_paren_token: R_PAREN@123..125 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@125..126 "1" [] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@126..138 "else_idx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: ASSIGN@138..141 "<-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@141..142 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@142..144 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@144..159 "{" [Newline("\n"), Newline("\n"), Comments("# ASCII `.`"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@159..165 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@165..166 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@166..170 "TRUE" [] [], + }, + r_paren_token: R_PAREN@170..172 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@172..173 "1" [] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@173..185 "else.idx" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: ASSIGN@185..188 "<-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@188..189 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@189..191 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@191..329 "{" [Newline("\n"), Newline("\n"), Comments("# Non-ASCII, but cons ..."), Newline("\n"), Comments("# Our lexer uses `XID ..."), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@329..335 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@335..336 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@336..340 "TRUE" [] [], + }, + r_paren_token: R_PAREN@340..342 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@342..343 "1" [] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@343..353 "elseμ" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: ASSIGN@353..356 "<-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@356..357 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@357..359 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@359..539 "{" [Newline("\n"), Newline("\n"), Comments("# Non-ASCII (U+00B7), ..."), Newline("\n"), Comments("# `XID_Continue` allo ..."), Newline("\n"), Comments("# syntax error."), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@539..545 "if" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + l_paren_token: L_PAREN@545..546 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@546..550 "TRUE" [] [], + }, + r_paren_token: R_PAREN@550..552 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@552..553 "1" [] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@553..563 "else·" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: ASSIGN@563..566 "<-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@566..567 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@567..569 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@569..570 "" [Newline("\n")] [], +} +``` + +## Identifiers starting with `else` that are not the `else` keyword - 2 (r-lib/tree-sitter-r#200) [error] + +```r +# Correctly treats `else.x` as a single identifier, not as `else` followed by `.x`, +# which also throws a diagnostic about a missing statement separator. +# See r-lib/tree-sitter-r#194. +if (TRUE) { + 1 +} else.x +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..188 "if" [Comments("# Correctly treats `e ..."), Newline("\n"), Comments("# which also throws a ..."), Newline("\n"), Comments("# See r-lib/tree-sitt ..."), Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@188..189 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@189..193 "TRUE" [] [], + }, + r_paren_token: R_PAREN@193..195 ")" [] [Whitespace(" ")], + consequence: RBracedStatements { + l_curly_token: L_CURLY@195..196 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@196..200 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@200..203 "}" [Newline("\n")] [Whitespace(" ")], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@203..209 "else.x" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@209..210 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:6:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a newline or a `;` between two expressions. + + 4 │ if (TRUE) { + 5 │ 1 + > 6 │ } else.x + │ ^^^^^^ + 7 │ + + i The first expression ends here. + + 2 │ # which also throws a diagnostic about a missing statement separator. + 3 │ # See r-lib/tree-sitter-r#194. + > 4 │ if (TRUE) { + │ ^^^^^^^^^^^ + > 5 │ 1 + > 6 │ } else.x + │ ^ + 7 │ +``` + +## If with comments + +```r +if # comment1 + (a) # comment2 + 1 else # comment3 + 2 # comment4 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..13 "if" [] [Whitespace(" "), Comments("# comment1")], + l_paren_token: L_PAREN@13..17 "(" [Newline("\n"), Whitespace(" ")] [], + condition: RIdentifier { + name_token: IDENT@17..18 "a" [] [], + }, + r_paren_token: R_PAREN@18..30 ")" [] [Whitespace(" "), Comments("# comment2")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@30..35 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@35..50 "else" [] [Whitespace(" "), Comments("# comment3")], + alternative: RDoubleValue { + value_token: R_DOUBLE_LITERAL@50..65 "2" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment4")], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@65..66 "" [Newline("\n")] [], +} +``` + +## If and else + +```r +if (a) 1 +if (a) 1 else 2 +if (a) 1 else if (b) 2 else 3 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..3 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@3..4 "(" [] [], + condition: RIdentifier { + name_token: IDENT@4..5 "a" [] [], + }, + r_paren_token: R_PAREN@5..7 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@7..8 "1" [] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@8..12 "if" [Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@12..13 "(" [] [], + condition: RIdentifier { + name_token: IDENT@13..14 "a" [] [], + }, + r_paren_token: R_PAREN@14..16 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@16..18 "1" [] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@18..23 "else" [] [Whitespace(" ")], + alternative: RDoubleValue { + value_token: R_DOUBLE_LITERAL@23..24 "2" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@24..28 "if" [Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@28..29 "(" [] [], + condition: RIdentifier { + name_token: IDENT@29..30 "a" [] [], + }, + r_paren_token: R_PAREN@30..32 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@32..34 "1" [] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@34..39 "else" [] [Whitespace(" ")], + alternative: RIfStatement { + if_token: IF_KW@39..42 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@42..43 "(" [] [], + condition: RIdentifier { + name_token: IDENT@43..44 "b" [] [], + }, + r_paren_token: R_PAREN@44..46 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@46..48 "2" [] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@48..53 "else" [] [Whitespace(" ")], + alternative: RDoubleValue { + value_token: R_DOUBLE_LITERAL@53..54 "3" [] [], + }, + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@54..55 "" [Newline("\n")] [], +} +``` + +An `else` binds to the nearest `if`. + +```r +if (a) if (b) c else d +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..3 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@3..4 "(" [] [], + condition: RIdentifier { + name_token: IDENT@4..5 "a" [] [], + }, + r_paren_token: R_PAREN@5..7 ")" [] [Whitespace(" ")], + consequence: RIfStatement { + if_token: IF_KW@7..10 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@10..11 "(" [] [], + condition: RIdentifier { + name_token: IDENT@11..12 "b" [] [], + }, + r_paren_token: R_PAREN@12..14 ")" [] [Whitespace(" ")], + consequence: RIdentifier { + name_token: IDENT@14..16 "c" [] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@16..21 "else" [] [Whitespace(" ")], + alternative: RIdentifier { + name_token: IDENT@21..22 "d" [] [], + }, + }, + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@22..23 "" [Newline("\n")] [], +} +``` + +## A consequence and an alternative stop at the binary `?` + +Both share the rank a function or loop body has, so `if (a) b ? c` must parse as +`(if (a) b) ? c`. + +```r +if (a) b ? c +if (a) b else c ? d +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIfStatement { + if_token: IF_KW@0..3 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@3..4 "(" [] [], + condition: RIdentifier { + name_token: IDENT@4..5 "a" [] [], + }, + r_paren_token: R_PAREN@5..7 ")" [] [Whitespace(" ")], + consequence: RIdentifier { + name_token: IDENT@7..9 "b" [] [Whitespace(" ")], + }, + else_clause: missing (optional), + }, + operator: WAT@9..11 "?" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@11..12 "c" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIfStatement { + if_token: IF_KW@12..16 "if" [Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@16..17 "(" [] [], + condition: RIdentifier { + name_token: IDENT@17..18 "a" [] [], + }, + r_paren_token: R_PAREN@18..20 ")" [] [Whitespace(" ")], + consequence: RIdentifier { + name_token: IDENT@20..22 "b" [] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@22..27 "else" [] [Whitespace(" ")], + alternative: RIdentifier { + name_token: IDENT@27..29 "c" [] [Whitespace(" ")], + }, + }, + }, + operator: WAT@29..31 "?" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@31..32 "d" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@32..33 "" [Newline("\n")] [], +} +``` + +An assignment binds tighter than either, so the consequence swallows it. + +```r +if (a) b <- c else d +1 + if (a) b +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..3 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@3..4 "(" [] [], + condition: RIdentifier { + name_token: IDENT@4..5 "a" [] [], + }, + r_paren_token: R_PAREN@5..7 ")" [] [Whitespace(" ")], + consequence: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@7..9 "b" [] [Whitespace(" ")], + }, + operator: ASSIGN@9..12 "<-" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@12..14 "c" [] [Whitespace(" ")], + }, + }, + else_clause: RElseClause { + else_token: ELSE_KW@14..19 "else" [] [Whitespace(" ")], + alternative: RIdentifier { + name_token: IDENT@19..20 "d" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@20..23 "1" [Newline("\n")] [Whitespace(" ")], + }, + operator: PLUS@23..25 "+" [] [Whitespace(" ")], + right: RIfStatement { + if_token: IF_KW@25..28 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@28..29 "(" [] [], + condition: RIdentifier { + name_token: IDENT@29..30 "a" [] [], + }, + r_paren_token: R_PAREN@30..32 ")" [] [Whitespace(" ")], + consequence: RIdentifier { + name_token: IDENT@32..33 "b" [] [], + }, + else_clause: missing (optional), + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@33..34 "" [Newline("\n")] [], +} +``` + +## A newline before `else` at top level [error] + +Fatal here. The `if` ends at the consequence and the `else` is left over. + +```r +if (a) 1 +else 2 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..3 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@3..4 "(" [] [], + condition: RIdentifier { + name_token: IDENT@4..5 "a" [] [], + }, + r_paren_token: R_PAREN@5..7 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@7..8 "1" [] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + ELSE_KW@8..14 "else" [Newline("\n")] [Whitespace(" ")], + R_DOUBLE_LITERAL@14..15 "2" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@15..16 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found 'else 2'. + + 1 │ if (a) 1 + > 2 │ else 2 + │ ^^^^^^ + 3 │ +``` + +## A newline before `else` inside a delimiter + +Newlines carry no meaning inside `(`, `[`, or `[[` + +```r +f(if (a) 1 + else 2) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..1 "f" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@1..2 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIfStatement { + if_token: IF_KW@2..5 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@5..6 "(" [] [], + condition: RIdentifier { + name_token: IDENT@6..7 "a" [] [], + }, + r_paren_token: R_PAREN@7..9 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@9..10 "1" [] [], + }, + else_clause: RElseClause { + else_token: ELSE_KW@10..18 "else" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + alternative: RDoubleValue { + value_token: R_DOUBLE_LITERAL@18..19 "2" [] [], + }, + }, + }, + }, + ], + r_paren_token: R_PAREN@19..20 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@20..21 "" [Newline("\n")] [], +} +``` + +```r +x[if (a) 1 + else 2] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@1..2 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIfStatement { + if_token: IF_KW@2..5 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@5..6 "(" [] [], + condition: RIdentifier { + name_token: IDENT@6..7 "a" [] [], + }, + r_paren_token: R_PAREN@7..9 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@9..10 "1" [] [], + }, + else_clause: RElseClause { + else_token: ELSE_KW@10..18 "else" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + alternative: RDoubleValue { + value_token: R_DOUBLE_LITERAL@18..19 "2" [] [], + }, + }, + }, + }, + ], + r_brack_token: R_BRACK@19..20 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@20..21 "" [Newline("\n")] [], +} +``` + +```r +x[[if (a) 1 + else 2]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@1..3 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIfStatement { + if_token: IF_KW@3..6 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@6..7 "(" [] [], + condition: RIdentifier { + name_token: IDENT@7..8 "a" [] [], + }, + r_paren_token: R_PAREN@8..10 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@10..11 "1" [] [], + }, + else_clause: RElseClause { + else_token: ELSE_KW@11..19 "else" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + alternative: RDoubleValue { + value_token: R_DOUBLE_LITERAL@19..20 "2" [] [], + }, + }, + }, + }, + ], + r_brack2_token: R_BRACK2@20..22 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@22..23 "" [Newline("\n")] [], +} +``` + +## A newline between `else` and its alternative + +Allowed even at top level, unlike a newline before the `else` itself (r-lib/tree-sitter-r#141). + +```r +if (a) 1 else + 2 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..3 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@3..4 "(" [] [], + condition: RIdentifier { + name_token: IDENT@4..5 "a" [] [], + }, + r_paren_token: R_PAREN@5..7 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@7..9 "1" [] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@9..13 "else" [] [], + alternative: RDoubleValue { + value_token: R_DOUBLE_LITERAL@13..17 "2" [Newline("\n"), Whitespace(" ")] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@17..18 "" [Newline("\n")] [], +} +``` + +## An `if` with no condition [error] + +```r +if () 1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@0..3 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@3..4 "(" [] [], + condition: missing (required), + r_paren_token: R_PAREN@4..6 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@6..7 "1" [] [], + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a condition but instead found ')'. + + > 1 │ if () 1 + │ ^ + 2 │ +``` diff --git a/crates/air_r_parser_tests/tests/corpus/control/repeat.md b/crates/air_r_parser_tests/tests/corpus/control/repeat.md new file mode 100644 index 00000000..bcf576dc --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/control/repeat.md @@ -0,0 +1,22 @@ +## Repeat + +```r +repeat 1 +``` + +## Repeat no body [error] + +```r +repeat +``` + +## Repeat with braces + +```r +repeat { +} + +repeat { + 1 + 1 +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/control/repeat.snap.md b/crates/air_r_parser_tests/tests/corpus/control/repeat.snap.md new file mode 100644 index 00000000..4925c399 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/control/repeat.snap.md @@ -0,0 +1,112 @@ + + +## Repeat + +```r +repeat 1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RRepeatStatement { + repeat_token: REPEAT_KW@0..7 "repeat" [] [Whitespace(" ")], + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@7..8 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@8..9 "" [Newline("\n")] [], +} +``` + +## Repeat no body [error] + +```r +repeat +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RRepeatStatement { + repeat_token: REPEAT_KW@0..6 "repeat" [] [], + body: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@6..7 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a loop body but instead found the end of the file. + + 1 │ repeat + > 2 │ + │ +``` + +## Repeat with braces + +```r +repeat { +} + +repeat { + 1 + 1 +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RRepeatStatement { + repeat_token: REPEAT_KW@0..7 "repeat" [] [Whitespace(" ")], + body: RBracedStatements { + l_curly_token: L_CURLY@7..8 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@8..10 "}" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RRepeatStatement { + repeat_token: REPEAT_KW@10..19 "repeat" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + body: RBracedStatements { + l_curly_token: L_CURLY@19..20 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@20..25 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: PLUS@25..27 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@27..28 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@28..30 "}" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@30..31 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/control/while.md b/crates/air_r_parser_tests/tests/corpus/control/while.md new file mode 100644 index 00000000..06b1b5b7 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/control/while.md @@ -0,0 +1,43 @@ +## While + +```r +while(TRUE) + bar + +while(x > 0) + x <- x - 1 + +while(TRUE) + break + +while(TRUE) + next +``` + +## While no body [error] + +```r +while (a < b) +``` + +## While with braces + +```r +while(TRUE) FALSE + +while(TRUE) {} + +while(a) { + +} + +while(fn()) { + +} +``` + +## A `while` with no condition [error] + +```r +while () b +``` diff --git a/crates/air_r_parser_tests/tests/corpus/control/while.snap.md b/crates/air_r_parser_tests/tests/corpus/control/while.snap.md new file mode 100644 index 00000000..ab5ba8d2 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/control/while.snap.md @@ -0,0 +1,274 @@ + + +## While + +```r +while(TRUE) + bar + +while(x > 0) + x <- x - 1 + +while(TRUE) + break + +while(TRUE) + next +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@0..5 "while" [] [], + l_paren_token: L_PAREN@5..6 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@6..10 "TRUE" [] [], + }, + r_paren_token: R_PAREN@10..11 ")" [] [], + body: RIdentifier { + name_token: IDENT@11..17 "bar" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@17..24 "while" [Newline("\n"), Newline("\n")] [], + l_paren_token: L_PAREN@24..25 "(" [] [], + condition: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@25..27 "x" [] [Whitespace(" ")], + }, + operator: GREATER_THAN@27..29 ">" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@29..30 "0" [] [], + }, + }, + r_paren_token: R_PAREN@30..31 ")" [] [], + body: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@31..36 "x" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: ASSIGN@36..39 "<-" [] [Whitespace(" ")], + right: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@39..41 "x" [] [Whitespace(" ")], + }, + operator: MINUS@41..43 "-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@43..44 "1" [] [], + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@44..51 "while" [Newline("\n"), Newline("\n")] [], + l_paren_token: L_PAREN@51..52 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@52..56 "TRUE" [] [], + }, + r_paren_token: R_PAREN@56..57 ")" [] [], + body: RBreakExpression { + break_token: BREAK_KW@57..65 "break" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@65..72 "while" [Newline("\n"), Newline("\n")] [], + l_paren_token: L_PAREN@72..73 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@73..77 "TRUE" [] [], + }, + r_paren_token: R_PAREN@77..78 ")" [] [], + body: RNextExpression { + next_token: NEXT_KW@78..85 "next" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@85..86 "" [Newline("\n")] [], +} +``` + +## While no body [error] + +```r +while (a < b) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@0..6 "while" [] [Whitespace(" ")], + l_paren_token: L_PAREN@6..7 "(" [] [], + condition: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@7..9 "a" [] [Whitespace(" ")], + }, + operator: LESS_THAN@9..11 "<" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@11..12 "b" [] [], + }, + }, + r_paren_token: R_PAREN@12..13 ")" [] [], + body: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@13..14 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a loop body but instead found the end of the file. + + 1 │ while (a < b) + > 2 │ + │ +``` + +## While with braces + +```r +while(TRUE) FALSE + +while(TRUE) {} + +while(a) { + +} + +while(fn()) { + +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@0..5 "while" [] [], + l_paren_token: L_PAREN@5..6 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@6..10 "TRUE" [] [], + }, + r_paren_token: R_PAREN@10..12 ")" [] [Whitespace(" ")], + body: RFalseExpression { + false_token: FALSE_KW@12..17 "FALSE" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@17..24 "while" [Newline("\n"), Newline("\n")] [], + l_paren_token: L_PAREN@24..25 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@25..29 "TRUE" [] [], + }, + r_paren_token: R_PAREN@29..31 ")" [] [Whitespace(" ")], + body: RBracedStatements { + l_curly_token: L_CURLY@31..32 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@32..33 "}" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@33..40 "while" [Newline("\n"), Newline("\n")] [], + l_paren_token: L_PAREN@40..41 "(" [] [], + condition: RIdentifier { + name_token: IDENT@41..42 "a" [] [], + }, + r_paren_token: R_PAREN@42..44 ")" [] [Whitespace(" ")], + body: RBracedStatements { + l_curly_token: L_CURLY@44..45 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@45..48 "}" [Newline("\n"), Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@48..55 "while" [Newline("\n"), Newline("\n")] [], + l_paren_token: L_PAREN@55..56 "(" [] [], + condition: RCall { + function: RIdentifier { + name_token: IDENT@56..58 "fn" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@58..59 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@59..60 ")" [] [], + }, + }, + r_paren_token: R_PAREN@60..62 ")" [] [Whitespace(" ")], + body: RBracedStatements { + l_curly_token: L_CURLY@62..63 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@63..66 "}" [Newline("\n"), Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@66..67 "" [Newline("\n")] [], +} +``` + +## A `while` with no condition [error] + +```r +while () b +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@0..6 "while" [] [Whitespace(" ")], + l_paren_token: L_PAREN@6..7 "(" [] [], + condition: missing (required), + r_paren_token: R_PAREN@7..9 ")" [] [Whitespace(" ")], + body: RIdentifier { + name_token: IDENT@9..10 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@10..11 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:8 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a condition but instead found ')'. + + > 1 │ while () b + │ ^ + 2 │ +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/assignment.md b/crates/air_r_parser_tests/tests/corpus/expressions/assignment.md new file mode 100644 index 00000000..028d2bc8 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/assignment.md @@ -0,0 +1,12 @@ +## Assignment + +```r +x <- 1 +x = 1 +x := 1 +x <<- 1 +1 ->> x +1 -> x +x <- y(1) +y(1) -> x +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/assignment.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/assignment.snap.md new file mode 100644 index 00000000..8567d5bd --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/assignment.snap.md @@ -0,0 +1,147 @@ + + +## Assignment + +```r +x <- 1 +x = 1 +x := 1 +x <<- 1 +1 ->> x +1 -> x +x <- y(1) +y(1) -> x +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "x" [] [Whitespace(" ")], + }, + operator: ASSIGN@2..5 "<-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..6 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@6..9 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: EQUAL@9..11 "=" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@11..12 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@12..15 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: WALRUS@15..18 ":=" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@18..19 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@19..22 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: SUPER_ASSIGN@22..26 "<<-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@26..27 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@27..30 "1" [Newline("\n")] [Whitespace(" ")], + }, + operator: SUPER_ASSIGN_RIGHT@30..34 "->>" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@34..35 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@35..38 "1" [Newline("\n")] [Whitespace(" ")], + }, + operator: ASSIGN_RIGHT@38..41 "->" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@41..42 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@42..45 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: ASSIGN@45..48 "<-" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@48..49 "y" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@49..50 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@50..51 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@51..52 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RCall { + function: RIdentifier { + name_token: IDENT@52..54 "y" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@54..55 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@55..56 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@56..58 ")" [] [Whitespace(" ")], + }, + }, + operator: ASSIGN_RIGHT@58..61 "->" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@61..62 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@62..63 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser/tests/snapshots/ok/binary_expressions.R b/crates/air_r_parser_tests/tests/corpus/expressions/binary.md similarity index 51% rename from crates/air_r_parser/tests/snapshots/ok/binary_expressions.R rename to crates/air_r_parser_tests/tests/corpus/expressions/binary.md index c5295b90..8333baf7 100644 --- a/crates/air_r_parser/tests/snapshots/ok/binary_expressions.R +++ b/crates/air_r_parser_tests/tests/corpus/expressions/binary.md @@ -1,3 +1,44 @@ +## Relational + +```r +a == b +a > b +a < b +a >= b +a <= b +a != b + +a == + b +``` + +## Arithmetic operator + +```r +a + b +a - b +a * b +a / b +a ^ b +a ** b + +a + + b + +a * + b +``` + +## Not an arithmetic operator + +```r +a + + b +``` + +## Binary operators + +```r 1 ? 2 1 ~ 2 1 <- 2 @@ -38,3 +79,4 @@ # comment1 2 # comment2 +``` diff --git a/crates/air_r_parser/tests/snapshots/ok/binary_expressions.R.snap b/crates/air_r_parser_tests/tests/corpus/expressions/binary.snap.md similarity index 59% rename from crates/air_r_parser/tests/snapshots/ok/binary_expressions.R.snap rename to crates/air_r_parser_tests/tests/corpus/expressions/binary.snap.md index 517ac16b..365f56b3 100644 --- a/crates/air_r_parser/tests/snapshots/ok/binary_expressions.R.snap +++ b/crates/air_r_parser_tests/tests/corpus/expressions/binary.snap.md @@ -1,10 +1,268 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input + -```R +## Relational + +```r +a == b +a > b +a < b +a >= b +a <= b +a != b + +a == + b +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "a" [] [Whitespace(" ")], + }, + operator: EQUAL2@2..5 "==" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@5..6 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@6..9 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: GREATER_THAN@9..11 ">" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@11..12 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@12..15 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: LESS_THAN@15..17 "<" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@17..18 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@18..21 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: GREATER_THAN_OR_EQUAL_TO@21..24 ">=" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@24..25 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@25..28 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: LESS_THAN_OR_EQUAL_TO@28..31 "<=" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@31..32 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@32..35 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: NOT_EQUAL@35..38 "!=" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@38..39 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@39..43 "a" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + }, + operator: EQUAL2@43..45 "==" [] [], + right: RIdentifier { + name_token: IDENT@45..49 "b" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@49..50 "" [Newline("\n")] [], +} +``` + +## Arithmetic operator + +```r +a + b +a - b +a * b +a / b +a ^ b +a ** b + +a + + b + +a * + b +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "a" [] [Whitespace(" ")], + }, + operator: PLUS@2..4 "+" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@4..5 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@5..8 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: MINUS@8..10 "-" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@10..11 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@11..14 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: MULTIPLY@14..16 "*" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@16..17 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@17..20 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: DIVIDE@20..22 "/" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@22..23 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@23..26 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: EXPONENTIATE@26..28 "^" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@28..29 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@29..32 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: EXPONENTIATE2@32..35 "**" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@35..36 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@36..40 "a" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + }, + operator: PLUS@40..41 "+" [] [], + right: RIdentifier { + name_token: IDENT@41..45 "b" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@45..49 "a" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + }, + operator: MULTIPLY@49..50 "*" [] [], + right: RIdentifier { + name_token: IDENT@50..54 "b" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@54..55 "" [Newline("\n")] [], +} +``` + +## Not an arithmetic operator + +```r +a + + b +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@0..1 "a" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: PLUS@1..6 "+" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + argument: RIdentifier { + name_token: IDENT@6..7 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` + +## Binary operators + +```r 1 ? 2 1 ~ 2 1 <- 2 @@ -45,13 +303,9 @@ expression: snapshot # comment1 2 # comment2 - ``` - -## AST - -``` +```cst RRoot { bom_token: missing (optional), statements: RStatementList [ @@ -455,277 +709,3 @@ RRoot { eof_token: EOF@258..259 "" [Newline("\n")] [], } ``` - -## CST - -``` -0: R_ROOT@0..259 - 0: (empty) - 1: R_STATEMENT_LIST@0..258 - 0: R_EXPRESSION_STATEMENT@0..5 - 0: R_BINARY_EXPRESSION@0..5 - 0: R_DOUBLE_VALUE@0..2 - 0: R_DOUBLE_LITERAL@0..2 "1" [] [Whitespace(" ")] - 1: WAT@2..4 "?" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@4..5 - 0: R_DOUBLE_LITERAL@4..5 "2" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@5..11 - 0: R_BINARY_EXPRESSION@5..11 - 0: R_DOUBLE_VALUE@5..8 - 0: R_DOUBLE_LITERAL@5..8 "1" [Newline("\n")] [Whitespace(" ")] - 1: TILDE@8..10 "~" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@10..11 - 0: R_DOUBLE_LITERAL@10..11 "2" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@11..18 - 0: R_BINARY_EXPRESSION@11..18 - 0: R_DOUBLE_VALUE@11..14 - 0: R_DOUBLE_LITERAL@11..14 "1" [Newline("\n")] [Whitespace(" ")] - 1: ASSIGN@14..17 "<-" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@17..18 - 0: R_DOUBLE_LITERAL@17..18 "2" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@18..26 - 0: R_BINARY_EXPRESSION@18..26 - 0: R_DOUBLE_VALUE@18..21 - 0: R_DOUBLE_LITERAL@18..21 "1" [Newline("\n")] [Whitespace(" ")] - 1: SUPER_ASSIGN@21..25 "<<-" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@25..26 - 0: R_DOUBLE_LITERAL@25..26 "2" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@26..33 - 0: R_BINARY_EXPRESSION@26..33 - 0: R_DOUBLE_VALUE@26..29 - 0: R_DOUBLE_LITERAL@26..29 "1" [Newline("\n")] [Whitespace(" ")] - 1: WALRUS@29..32 ":=" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@32..33 - 0: R_DOUBLE_LITERAL@32..33 "2" [] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@33..40 - 0: R_BINARY_EXPRESSION@33..40 - 0: R_DOUBLE_VALUE@33..36 - 0: R_DOUBLE_LITERAL@33..36 "1" [Newline("\n")] [Whitespace(" ")] - 1: ASSIGN_RIGHT@36..39 "->" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@39..40 - 0: R_DOUBLE_LITERAL@39..40 "2" [] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@40..48 - 0: R_BINARY_EXPRESSION@40..48 - 0: R_DOUBLE_VALUE@40..43 - 0: R_DOUBLE_LITERAL@40..43 "1" [Newline("\n")] [Whitespace(" ")] - 1: SUPER_ASSIGN_RIGHT@43..47 "->>" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@47..48 - 0: R_DOUBLE_LITERAL@47..48 "2" [] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@48..54 - 0: R_BINARY_EXPRESSION@48..54 - 0: R_DOUBLE_VALUE@48..51 - 0: R_DOUBLE_LITERAL@48..51 "1" [Newline("\n")] [Whitespace(" ")] - 1: EQUAL@51..53 "=" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@53..54 - 0: R_DOUBLE_LITERAL@53..54 "2" [] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@54..60 - 0: R_BINARY_EXPRESSION@54..60 - 0: R_DOUBLE_VALUE@54..57 - 0: R_DOUBLE_LITERAL@54..57 "1" [Newline("\n")] [Whitespace(" ")] - 1: OR@57..59 "|" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@59..60 - 0: R_DOUBLE_LITERAL@59..60 "2" [] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@60..66 - 0: R_BINARY_EXPRESSION@60..66 - 0: R_DOUBLE_VALUE@60..63 - 0: R_DOUBLE_LITERAL@60..63 "1" [Newline("\n")] [Whitespace(" ")] - 1: AND@63..65 "&" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@65..66 - 0: R_DOUBLE_LITERAL@65..66 "2" [] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@66..73 - 0: R_BINARY_EXPRESSION@66..73 - 0: R_DOUBLE_VALUE@66..69 - 0: R_DOUBLE_LITERAL@66..69 "1" [Newline("\n")] [Whitespace(" ")] - 1: OR2@69..72 "||" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@72..73 - 0: R_DOUBLE_LITERAL@72..73 "2" [] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@73..80 - 0: R_BINARY_EXPRESSION@73..80 - 0: R_DOUBLE_VALUE@73..76 - 0: R_DOUBLE_LITERAL@73..76 "1" [Newline("\n")] [Whitespace(" ")] - 1: AND2@76..79 "&&" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@79..80 - 0: R_DOUBLE_LITERAL@79..80 "2" [] [] - 1: (empty) - 12: R_EXPRESSION_STATEMENT@80..86 - 0: R_BINARY_EXPRESSION@80..86 - 0: R_DOUBLE_VALUE@80..83 - 0: R_DOUBLE_LITERAL@80..83 "1" [Newline("\n")] [Whitespace(" ")] - 1: LESS_THAN@83..85 "<" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@85..86 - 0: R_DOUBLE_LITERAL@85..86 "2" [] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@86..93 - 0: R_BINARY_EXPRESSION@86..93 - 0: R_DOUBLE_VALUE@86..89 - 0: R_DOUBLE_LITERAL@86..89 "1" [Newline("\n")] [Whitespace(" ")] - 1: LESS_THAN_OR_EQUAL_TO@89..92 "<=" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@92..93 - 0: R_DOUBLE_LITERAL@92..93 "2" [] [] - 1: (empty) - 14: R_EXPRESSION_STATEMENT@93..99 - 0: R_BINARY_EXPRESSION@93..99 - 0: R_DOUBLE_VALUE@93..96 - 0: R_DOUBLE_LITERAL@93..96 "1" [Newline("\n")] [Whitespace(" ")] - 1: GREATER_THAN@96..98 ">" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@98..99 - 0: R_DOUBLE_LITERAL@98..99 "2" [] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@99..106 - 0: R_BINARY_EXPRESSION@99..106 - 0: R_DOUBLE_VALUE@99..102 - 0: R_DOUBLE_LITERAL@99..102 "1" [Newline("\n")] [Whitespace(" ")] - 1: GREATER_THAN_OR_EQUAL_TO@102..105 ">=" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@105..106 - 0: R_DOUBLE_LITERAL@105..106 "2" [] [] - 1: (empty) - 16: R_EXPRESSION_STATEMENT@106..113 - 0: R_BINARY_EXPRESSION@106..113 - 0: R_DOUBLE_VALUE@106..109 - 0: R_DOUBLE_LITERAL@106..109 "1" [Newline("\n")] [Whitespace(" ")] - 1: EQUAL2@109..112 "==" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@112..113 - 0: R_DOUBLE_LITERAL@112..113 "2" [] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@113..120 - 0: R_BINARY_EXPRESSION@113..120 - 0: R_DOUBLE_VALUE@113..116 - 0: R_DOUBLE_LITERAL@113..116 "1" [Newline("\n")] [Whitespace(" ")] - 1: NOT_EQUAL@116..119 "!=" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@119..120 - 0: R_DOUBLE_LITERAL@119..120 "2" [] [] - 1: (empty) - 18: R_EXPRESSION_STATEMENT@120..126 - 0: R_BINARY_EXPRESSION@120..126 - 0: R_DOUBLE_VALUE@120..123 - 0: R_DOUBLE_LITERAL@120..123 "1" [Newline("\n")] [Whitespace(" ")] - 1: PLUS@123..125 "+" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@125..126 - 0: R_DOUBLE_LITERAL@125..126 "2" [] [] - 1: (empty) - 19: R_EXPRESSION_STATEMENT@126..132 - 0: R_BINARY_EXPRESSION@126..132 - 0: R_DOUBLE_VALUE@126..129 - 0: R_DOUBLE_LITERAL@126..129 "1" [Newline("\n")] [Whitespace(" ")] - 1: MINUS@129..131 "-" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@131..132 - 0: R_DOUBLE_LITERAL@131..132 "2" [] [] - 1: (empty) - 20: R_EXPRESSION_STATEMENT@132..138 - 0: R_BINARY_EXPRESSION@132..138 - 0: R_DOUBLE_VALUE@132..135 - 0: R_DOUBLE_LITERAL@132..135 "1" [Newline("\n")] [Whitespace(" ")] - 1: MULTIPLY@135..137 "*" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@137..138 - 0: R_DOUBLE_LITERAL@137..138 "2" [] [] - 1: (empty) - 21: R_EXPRESSION_STATEMENT@138..144 - 0: R_BINARY_EXPRESSION@138..144 - 0: R_DOUBLE_VALUE@138..141 - 0: R_DOUBLE_LITERAL@138..141 "1" [Newline("\n")] [Whitespace(" ")] - 1: DIVIDE@141..143 "/" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@143..144 - 0: R_DOUBLE_LITERAL@143..144 "2" [] [] - 1: (empty) - 22: R_EXPRESSION_STATEMENT@144..151 - 0: R_BINARY_EXPRESSION@144..151 - 0: R_DOUBLE_VALUE@144..147 - 0: R_DOUBLE_LITERAL@144..147 "1" [Newline("\n")] [Whitespace(" ")] - 1: EXPONENTIATE2@147..150 "**" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@150..151 - 0: R_DOUBLE_LITERAL@150..151 "2" [] [] - 1: (empty) - 23: R_EXPRESSION_STATEMENT@151..157 - 0: R_BINARY_EXPRESSION@151..157 - 0: R_DOUBLE_VALUE@151..154 - 0: R_DOUBLE_LITERAL@151..154 "1" [Newline("\n")] [Whitespace(" ")] - 1: EXPONENTIATE@154..156 "^" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@156..157 - 0: R_DOUBLE_LITERAL@156..157 "2" [] [] - 1: (empty) - 24: R_EXPRESSION_STATEMENT@157..164 - 0: R_BINARY_EXPRESSION@157..164 - 0: R_DOUBLE_VALUE@157..160 - 0: R_DOUBLE_LITERAL@157..160 "1" [Newline("\n")] [Whitespace(" ")] - 1: PIPE@160..163 "|>" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@163..164 - 0: R_DOUBLE_LITERAL@163..164 "2" [] [] - 1: (empty) - 25: R_EXPRESSION_STATEMENT@164..172 - 0: R_BINARY_EXPRESSION@164..172 - 0: R_DOUBLE_VALUE@164..167 - 0: R_DOUBLE_LITERAL@164..167 "1" [Newline("\n")] [Whitespace(" ")] - 1: SPECIAL@167..171 "%>%" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@171..172 - 0: R_DOUBLE_LITERAL@171..172 "2" [] [] - 1: (empty) - 26: R_EXPRESSION_STATEMENT@172..176 - 0: R_BINARY_EXPRESSION@172..176 - 0: R_DOUBLE_VALUE@172..174 - 0: R_DOUBLE_LITERAL@172..174 "1" [Newline("\n")] [] - 1: COLON@174..175 ":" [] [] - 2: R_DOUBLE_VALUE@175..176 - 0: R_DOUBLE_LITERAL@175..176 "2" [] [] - 1: (empty) - 27: R_EXPRESSION_STATEMENT@176..191 - 0: R_BINARY_EXPRESSION@176..191 - 0: R_BINARY_EXPRESSION@176..188 - 0: R_BINARY_EXPRESSION@176..184 - 0: R_DOUBLE_VALUE@176..180 - 0: R_DOUBLE_LITERAL@176..180 "1" [Newline("\n"), Newline("\n")] [Whitespace(" ")] - 1: PLUS@180..182 "+" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@182..184 - 0: R_DOUBLE_LITERAL@182..184 "2" [] [Whitespace(" ")] - 1: PLUS@184..186 "+" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@186..188 - 0: R_DOUBLE_LITERAL@186..188 "3" [] [Whitespace(" ")] - 1: PLUS@188..190 "+" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@190..191 - 0: R_DOUBLE_LITERAL@190..191 "4" [] [] - 1: (empty) - 28: R_EXPRESSION_STATEMENT@191..204 - 0: R_BINARY_EXPRESSION@191..204 - 0: R_BINARY_EXPRESSION@191..201 - 0: R_DOUBLE_VALUE@191..195 - 0: R_DOUBLE_LITERAL@191..195 "1" [Newline("\n"), Newline("\n")] [Whitespace(" ")] - 1: PLUS@195..197 "+" [] [Whitespace(" ")] - 2: R_BINARY_EXPRESSION@197..201 - 0: R_DOUBLE_VALUE@197..198 - 0: R_DOUBLE_LITERAL@197..198 "2" [] [] - 1: EXPONENTIATE@198..199 "^" [] [] - 2: R_DOUBLE_VALUE@199..201 - 0: R_DOUBLE_LITERAL@199..201 "3" [] [Whitespace(" ")] - 1: PLUS@201..203 "+" [] [Whitespace(" ")] - 2: R_DOUBLE_VALUE@203..204 - 0: R_DOUBLE_LITERAL@203..204 "4" [] [] - 1: (empty) - 29: R_EXPRESSION_STATEMENT@204..223 - 0: R_BINARY_EXPRESSION@204..223 - 0: R_DOUBLE_VALUE@204..208 - 0: R_DOUBLE_LITERAL@204..208 "1" [Newline("\n"), Newline("\n")] [Whitespace(" ")] - 1: PLUS@208..219 "+" [] [Whitespace(" "), Comments("# comment")] - 2: R_DOUBLE_VALUE@219..223 - 0: R_DOUBLE_LITERAL@219..223 "2" [Newline("\n"), Whitespace(" ")] [] - 1: (empty) - 30: R_EXPRESSION_STATEMENT@223..258 - 0: R_BINARY_EXPRESSION@223..258 - 0: R_DOUBLE_VALUE@223..227 - 0: R_DOUBLE_LITERAL@223..227 "1" [Newline("\n"), Newline("\n")] [Whitespace(" ")] - 1: PLUS@227..228 "+" [] [] - 2: R_DOUBLE_VALUE@228..258 - 0: R_DOUBLE_LITERAL@228..258 "2" [Newline("\n"), Newline("\n"), Whitespace(" "), Comments("# comment1"), Newline("\n"), Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")] - 1: (empty) - 2: EOF@258..259 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/call.md b/crates/air_r_parser_tests/tests/corpus/expressions/call.md new file mode 100644 index 00000000..19e67888 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/call.md @@ -0,0 +1,244 @@ +## Calls + +```r +f() +f(,) +f(,,) +f(x,) +f(x,,) +f(,x) +f(,,x) +f(x,,y) +f(,,x,,y,,) +f(x=,) +f("x"=,) +f(,y=) + +f(x) +f(1+1) +f(1 ~ 1) +f(x, y) +f(x, y = 2) +f(x = 1 + 1) +f(x, y =) +f(f2(x, y)) + +# Dots as unnamed and named argument +f(...) +f(, ..., 1) +f(... = 1) +f(... = ,) +f(... = ...) + +# `..i` as unnamed and named argument +f(..1) +f(, ..1, 1) +f(..1 = 1) +f(..1 = ,) +f(..1 = ..1) +``` + +## Calls with NULL as argument name + +```r +fn(NULL =) +fn(NULL =,) +fn(NULL = 1) + +# This was likely added to R to support this case for `switch()` +switch(typeof(x), NULL = this, integer = that) +``` + +## Calls with `return` as argument name + +```r +# `return` is not a reserved word in R (it's not in `?Reserved`), so it is +# allowed as an argument name (r-lib/tree-sitter-r#189). + +fn(return = TRUE) +fn(return = ) + +# One specific example from r-lib/tree-sitter-r#189 +switch( + x, + return = , +) +``` + +## `return` is just an identifier + +```r +# `return` is a regular symbol in R, not a keyword (r-lib/tree-sitter-r#189). A `return()` +# call is parsed the same as any other call, with `return` as the +# identifier. A bare `return` is also parsed as an identifier. + +function(x) return(x) +return(x) +return +``` + +## Switch + +```r +switch(foo, + x = 1, + "y" = 2, + z = , + 3 +) +``` + +## Calls with NA as argument name [error] + +```r +# Unlike `NULL`, other keywords like `NA` aren't allowed here +fn(NA = ) +``` + +## Not a call, subset, or subset2 [error] + +```r +f +(x) + +foo +[bar] + +foo +[[x]] +``` + +## Not a call, subset, or subset2 due to sequential arguments [error] + +```r +f(x y) +foo[x y] +foo[[x y]] + +f(x, y z) +foo[x, y z] +foo[[x, y z]] + +f(, x y) +foo[, x y] +foo[[, x y]] +``` + +## Call arguments + +```r +fn() +fn(a) +fn(a, b) +fn(...) +fn(a, ..., b) + +fn(a = 1) +fn(a = ) + +fn(a = 1, b = 2) +fn(a = , b = 2) + +fn(... = 1) +fn(... =) + +fn(..1 = 2) +fn(..1 =) + +fn("arg" = 1) +fn("arg" =) + +# Comma tests +fn(,) +fn(,,,) +fn(,,a,,b,,) + +# Comment tests +fn( + # comment +) +fn(, + # comment +,) +fn( + a, # comment1 + b # comment2 +) +fn( + a # comment1 + = # comment2 + 1 # comment3 +) + +{expr}(a = 1) + +# `NULL` argument name (r-lib/tree-sitter-r#164) +fn( + NULL = 1 +) +fn( + NULL = +) +fn( + NULL = , +) + +# A standard call, not a special node (r-lib/tree-sitter-r#197) +return() +``` + +## Side by side arguments [error] + +```r +fn(a b) +fn(a b c) +``` + +## An argument value that starts no expression [error] + +Diagnostic should be about the faulty expression, not a comma + +```r +fn(* 1) +fn(a, * 1) +fn(a = * 1) +fn(,, * 1) +``` + +Same for `[` and `[[` + +```r +x[* 1] +x[[* 1]] +``` + +## Closing delimiter belonging to someone else [error] + +The argument list ends at any closing delimiter, not just its own, so the `}` +survives for its owner rather than being eaten as an argument. Nothing was +written between the `(` and the `}`, so the list is empty rather than holding +one empty argument. + +```r +fn(} +``` + +```r +fn(a} +``` + +## A newline or comment between a name and its `=` + +`parse_argument()` looks one token past the name to find the `=`, and that +lookahead skips trivia to reach it + +```r +f(a + = 1) + +f(a # c + = 1) + +f("a" # c + = 1) +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/call.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/call.snap.md new file mode 100644 index 00000000..d98cc8f6 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/call.snap.md @@ -0,0 +1,3220 @@ + + +## Calls + +```r +f() +f(,) +f(,,) +f(x,) +f(x,,) +f(,x) +f(,,x) +f(x,,y) +f(,,x,,y,,) +f(x=,) +f("x"=,) +f(,y=) + +f(x) +f(1+1) +f(1 ~ 1) +f(x, y) +f(x, y = 2) +f(x = 1 + 1) +f(x, y =) +f(f2(x, y)) + +# Dots as unnamed and named argument +f(...) +f(, ..., 1) +f(... = 1) +f(... = ,) +f(... = ...) + +# `..i` as unnamed and named argument +f(..1) +f(, ..1, 1) +f(..1 = 1) +f(..1 = ,) +f(..1 = ..1) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..1 "f" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@1..2 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@2..3 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@3..5 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@5..6 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@6..7 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@7..8 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@8..10 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@10..11 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@11..12 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@12..13 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@13..14 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@14..16 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@16..17 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@17..18 "x" [] [], + }, + }, + COMMA@18..19 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@19..20 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@20..22 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@22..23 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@23..24 "x" [] [], + }, + }, + COMMA@24..25 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@25..26 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@26..27 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@27..29 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@29..30 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@30..31 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@31..32 "x" [] [], + }, + }, + ], + r_paren_token: R_PAREN@32..33 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@33..35 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@35..36 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@36..37 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@37..38 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@38..39 "x" [] [], + }, + }, + ], + r_paren_token: R_PAREN@39..40 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@40..42 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@42..43 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@43..44 "x" [] [], + }, + }, + COMMA@44..45 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@45..46 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@46..47 "y" [] [], + }, + }, + ], + r_paren_token: R_PAREN@47..48 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@48..50 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@50..51 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@51..52 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@52..53 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@53..54 "x" [] [], + }, + }, + COMMA@54..55 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@55..56 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@56..57 "y" [] [], + }, + }, + COMMA@57..58 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@58..59 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@59..60 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@60..62 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@62..63 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@63..64 "x" [] [], + }, + eq_token: EQUAL@64..65 "=" [] [], + }, + value: missing (optional), + }, + COMMA@65..66 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@66..67 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@67..69 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@69..70 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@70..73 "\"x\"" [] [], + }, + eq_token: EQUAL@73..74 "=" [] [], + }, + value: missing (optional), + }, + COMMA@74..75 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@75..76 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@76..78 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@78..79 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@79..80 "," [] [], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@80..81 "y" [] [], + }, + eq_token: EQUAL@81..82 "=" [] [], + }, + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@82..83 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@83..86 "f" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@86..87 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@87..88 "x" [] [], + }, + }, + ], + r_paren_token: R_PAREN@88..89 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@89..91 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@91..92 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@92..93 "1" [] [], + }, + operator: PLUS@93..94 "+" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@94..95 "1" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@95..96 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@96..98 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@98..99 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@99..101 "1" [] [Whitespace(" ")], + }, + operator: TILDE@101..103 "~" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@103..104 "1" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@104..105 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@105..107 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@107..108 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@108..109 "x" [] [], + }, + }, + COMMA@109..111 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@111..112 "y" [] [], + }, + }, + ], + r_paren_token: R_PAREN@112..113 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@113..115 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@115..116 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@116..117 "x" [] [], + }, + }, + COMMA@117..119 "," [] [Whitespace(" ")], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@119..121 "y" [] [Whitespace(" ")], + }, + eq_token: EQUAL@121..123 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@123..124 "2" [] [], + }, + }, + ], + r_paren_token: R_PAREN@124..125 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@125..127 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@127..128 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@128..130 "x" [] [Whitespace(" ")], + }, + eq_token: EQUAL@130..132 "=" [] [Whitespace(" ")], + }, + value: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@132..134 "1" [] [Whitespace(" ")], + }, + operator: PLUS@134..136 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@136..137 "1" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@137..138 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@138..140 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@140..141 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@141..142 "x" [] [], + }, + }, + COMMA@142..144 "," [] [Whitespace(" ")], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@144..146 "y" [] [Whitespace(" ")], + }, + eq_token: EQUAL@146..147 "=" [] [], + }, + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@147..148 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@148..150 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@150..151 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RCall { + function: RIdentifier { + name_token: IDENT@151..153 "f2" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@153..154 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@154..155 "x" [] [], + }, + }, + COMMA@155..157 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@157..158 "y" [] [], + }, + }, + ], + r_paren_token: R_PAREN@158..159 ")" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@159..160 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@160..200 "f" [Newline("\n"), Newline("\n"), Comments("# Dots as unnamed and ..."), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@200..201 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDots { + value_token: DOTS@201..204 "..." [] [], + }, + }, + ], + r_paren_token: R_PAREN@204..205 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@205..207 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@207..208 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@208..210 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RDots { + value_token: DOTS@210..213 "..." [] [], + }, + }, + COMMA@213..215 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@215..216 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@216..217 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@217..219 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@219..220 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RDots { + value_token: DOTS@220..224 "..." [] [Whitespace(" ")], + }, + eq_token: EQUAL@224..226 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@226..227 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@227..228 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@228..230 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@230..231 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RDots { + value_token: DOTS@231..235 "..." [] [Whitespace(" ")], + }, + eq_token: EQUAL@235..237 "=" [] [Whitespace(" ")], + }, + value: missing (optional), + }, + COMMA@237..238 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@238..239 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@239..241 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@241..242 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RDots { + value_token: DOTS@242..246 "..." [] [Whitespace(" ")], + }, + eq_token: EQUAL@246..248 "=" [] [Whitespace(" ")], + }, + value: RDots { + value_token: DOTS@248..251 "..." [] [], + }, + }, + ], + r_paren_token: R_PAREN@251..252 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@252..293 "f" [Newline("\n"), Newline("\n"), Comments("# `..i` as unnamed an ..."), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@293..294 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDotDotI { + value_token: DOTDOTI@294..297 "..1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@297..298 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@298..300 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@300..301 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@301..303 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RDotDotI { + value_token: DOTDOTI@303..306 "..1" [] [], + }, + }, + COMMA@306..308 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@308..309 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@309..310 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@310..312 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@312..313 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RDotDotI { + value_token: DOTDOTI@313..317 "..1" [] [Whitespace(" ")], + }, + eq_token: EQUAL@317..319 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@319..320 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@320..321 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@321..323 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@323..324 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RDotDotI { + value_token: DOTDOTI@324..328 "..1" [] [Whitespace(" ")], + }, + eq_token: EQUAL@328..330 "=" [] [Whitespace(" ")], + }, + value: missing (optional), + }, + COMMA@330..331 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@331..332 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@332..334 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@334..335 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RDotDotI { + value_token: DOTDOTI@335..339 "..1" [] [Whitespace(" ")], + }, + eq_token: EQUAL@339..341 "=" [] [Whitespace(" ")], + }, + value: RDotDotI { + value_token: DOTDOTI@341..344 "..1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@344..345 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@345..346 "" [Newline("\n")] [], +} +``` + +## Calls with NULL as argument name + +```r +fn(NULL =) +fn(NULL =,) +fn(NULL = 1) + +# This was likely added to R to support this case for `switch()` +switch(typeof(x), NULL = this, integer = that) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..2 "fn" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@2..3 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RNullExpression { + null_token: NULL_KW@3..8 "NULL" [] [Whitespace(" ")], + }, + eq_token: EQUAL@8..9 "=" [] [], + }, + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@9..10 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@10..13 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@13..14 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RNullExpression { + null_token: NULL_KW@14..19 "NULL" [] [Whitespace(" ")], + }, + eq_token: EQUAL@19..20 "=" [] [], + }, + value: missing (optional), + }, + COMMA@20..21 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@21..22 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@22..25 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@25..26 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RNullExpression { + null_token: NULL_KW@26..31 "NULL" [] [Whitespace(" ")], + }, + eq_token: EQUAL@31..33 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@33..34 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@34..35 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@35..108 "switch" [Newline("\n"), Newline("\n"), Comments("# This was likely add ..."), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@108..109 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RCall { + function: RIdentifier { + name_token: IDENT@109..115 "typeof" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@115..116 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@116..117 "x" [] [], + }, + }, + ], + r_paren_token: R_PAREN@117..118 ")" [] [], + }, + }, + }, + COMMA@118..120 "," [] [Whitespace(" ")], + RArgument { + name_clause: RArgumentNameClause { + name: RNullExpression { + null_token: NULL_KW@120..125 "NULL" [] [Whitespace(" ")], + }, + eq_token: EQUAL@125..127 "=" [] [Whitespace(" ")], + }, + value: RIdentifier { + name_token: IDENT@127..131 "this" [] [], + }, + }, + COMMA@131..133 "," [] [Whitespace(" ")], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@133..141 "integer" [] [Whitespace(" ")], + }, + eq_token: EQUAL@141..143 "=" [] [Whitespace(" ")], + }, + value: RIdentifier { + name_token: IDENT@143..147 "that" [] [], + }, + }, + ], + r_paren_token: R_PAREN@147..148 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@148..149 "" [Newline("\n")] [], +} +``` + +## Calls with `return` as argument name + +```r +# `return` is not a reserved word in R (it's not in `?Reserved`), so it is +# allowed as an argument name (r-lib/tree-sitter-r#189). + +fn(return = TRUE) +fn(return = ) + +# One specific example from r-lib/tree-sitter-r#189 +switch( + x, + return = , +) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..135 "fn" [Comments("# `return` is not a r ..."), Newline("\n"), Comments("# allowed as an argum ..."), Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@135..136 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@136..143 "return" [] [Whitespace(" ")], + }, + eq_token: EQUAL@143..145 "=" [] [Whitespace(" ")], + }, + value: RTrueExpression { + true_token: TRUE_KW@145..149 "TRUE" [] [], + }, + }, + ], + r_paren_token: R_PAREN@149..150 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@150..153 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@153..154 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@154..161 "return" [] [Whitespace(" ")], + }, + eq_token: EQUAL@161..163 "=" [] [Whitespace(" ")], + }, + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@163..164 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@164..224 "switch" [Newline("\n"), Newline("\n"), Comments("# One specific exampl ..."), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@224..225 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@225..229 "x" [Newline("\n"), Whitespace(" ")] [], + }, + }, + COMMA@229..230 "," [] [], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@230..240 "return" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + eq_token: EQUAL@240..242 "=" [] [Whitespace(" ")], + }, + value: missing (optional), + }, + COMMA@242..243 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@243..245 ")" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@245..246 "" [Newline("\n")] [], +} +``` + +## `return` is just an identifier + +```r +# `return` is a regular symbol in R, not a keyword (r-lib/tree-sitter-r#189). A `return()` +# call is parsed the same as any other call, with `return` as the +# identifier. A bare `return` is also parsed as an identifier. + +function(x) return(x) +return(x) +return +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..229 "function" [Comments("# `return` is a regul ..."), Newline("\n"), Comments("# call is parsed the ..."), Newline("\n"), Comments("# identifier. A bare ..."), Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@229..230 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@230..231 "x" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@231..233 ")" [] [Whitespace(" ")], + }, + body: RCall { + function: RIdentifier { + name_token: IDENT@233..239 "return" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@239..240 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@240..241 "x" [] [], + }, + }, + ], + r_paren_token: R_PAREN@241..242 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@242..249 "return" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@249..250 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@250..251 "x" [] [], + }, + }, + ], + r_paren_token: R_PAREN@251..252 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@252..259 "return" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@259..260 "" [Newline("\n")] [], +} +``` + +## Switch + +```r +switch(foo, + x = 1, + "y" = 2, + z = , + 3 +) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..6 "switch" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@6..7 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@7..10 "foo" [] [], + }, + }, + COMMA@10..11 "," [] [], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@11..16 "x" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + eq_token: EQUAL@16..18 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@18..19 "1" [] [], + }, + }, + COMMA@19..20 "," [] [], + RArgument { + name_clause: RArgumentNameClause { + name: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@20..27 "\"y\"" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + eq_token: EQUAL@27..29 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@29..30 "2" [] [], + }, + }, + COMMA@30..31 "," [] [], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@31..36 "z" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + eq_token: EQUAL@36..38 "=" [] [Whitespace(" ")], + }, + value: missing (optional), + }, + COMMA@38..39 "," [] [], + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@39..43 "3" [Newline("\n"), Whitespace(" ")] [], + }, + }, + ], + r_paren_token: R_PAREN@43..45 ")" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@45..46 "" [Newline("\n")] [], +} +``` + +## Calls with NA as argument name [error] + +```r +# Unlike `NULL`, other keywords like `NA` aren't allowed here +fn(NA = ) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..64 "fn" [Comments("# Unlike `NULL`, othe ..."), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@64..65 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RBinaryExpression { + left: RNaExpression { + value: NA_LOGICAL_KW@65..68 "NA" [] [Whitespace(" ")], + }, + operator: EQUAL@68..70 "=" [] [Whitespace(" ")], + right: missing (required), + }, + }, + ], + r_paren_token: R_PAREN@70..71 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@71..72 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:9 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ')'. + + 1 │ # Unlike `NULL`, other keywords like `NA` aren't allowed here + > 2 │ fn(NA = ) + │ ^ + 3 │ +``` + +## Not a call, subset, or subset2 [error] + +```r +f +(x) + +foo +[bar] + +foo +[[x]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@0..1 "f" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@1..3 "(" [Newline("\n")] [], + body: RIdentifier { + name_token: IDENT@3..4 "x" [] [], + }, + r_paren_token: R_PAREN@4..5 ")" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@5..10 "foo" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + L_BRACK@10..12 "[" [Newline("\n")] [], + IDENT@12..15 "bar" [] [], + R_BRACK@15..16 "]" [] [], + ], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@16..21 "foo" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + L_BRACK2@21..24 "[[" [Newline("\n")] [], + IDENT@24..25 "x" [] [], + R_BRACK@25..26 "]" [] [], + R_BRACK@26..27 "]" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@27..28 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:5:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '[bar]'. + + 4 │ foo + > 5 │ [bar] + │ ^^^^^ + 6 │ + 7 │ foo + +test.R:8:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '[[x]]'. + + 7 │ foo + > 8 │ [[x]] + │ ^^^^^ + 9 │ +``` + +## Not a call, subset, or subset2 due to sequential arguments [error] + +```r +f(x y) +foo[x y] +foo[[x y]] + +f(x, y z) +foo[x, y z] +foo[[x, y z]] + +f(, x y) +foo[, x y] +foo[[, x y]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..1 "f" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@1..2 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@2..4 "x" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@4..5 "y" [] [], + }, + }, + ], + r_paren_token: R_PAREN@5..6 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@6..10 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@10..11 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@11..13 "x" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@13..14 "y" [] [], + }, + }, + ], + r_brack_token: R_BRACK@14..15 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@15..19 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@19..21 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@21..23 "x" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@23..24 "y" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@24..26 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@26..29 "f" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@29..30 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@30..31 "x" [] [], + }, + }, + COMMA@31..33 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@33..35 "y" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@35..36 "z" [] [], + }, + }, + ], + r_paren_token: R_PAREN@36..37 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@37..41 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@41..42 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@42..43 "x" [] [], + }, + }, + COMMA@43..45 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@45..47 "y" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@47..48 "z" [] [], + }, + }, + ], + r_brack_token: R_BRACK@48..49 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@49..53 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@53..55 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@55..56 "x" [] [], + }, + }, + COMMA@56..58 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@58..60 "y" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@60..61 "z" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@61..63 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@63..66 "f" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@66..67 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@67..69 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@69..71 "x" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@71..72 "y" [] [], + }, + }, + ], + r_paren_token: R_PAREN@72..73 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@73..77 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@77..78 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@78..80 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@80..82 "x" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@82..83 "y" [] [], + }, + }, + ], + r_brack_token: R_BRACK@83..84 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@84..88 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@88..90 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@90..92 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@92..94 "x" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@94..95 "y" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@95..97 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@97..98 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `y` + + > 1 │ f(x y) + │ ^ + 2 │ foo[x y] + 3 │ foo[[x y]] + + i Remove y + +test.R:2:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `y` + + 1 │ f(x y) + > 2 │ foo[x y] + │ ^ + 3 │ foo[[x y]] + 4 │ + + i Remove y + +test.R:3:8 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `y` + + 1 │ f(x y) + 2 │ foo[x y] + > 3 │ foo[[x y]] + │ ^ + 4 │ + 5 │ f(x, y z) + + i Remove y + +test.R:5:8 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `z` + + 3 │ foo[[x y]] + 4 │ + > 5 │ f(x, y z) + │ ^ + 6 │ foo[x, y z] + 7 │ foo[[x, y z]] + + i Remove z + +test.R:6:10 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `z` + + 5 │ f(x, y z) + > 6 │ foo[x, y z] + │ ^ + 7 │ foo[[x, y z]] + 8 │ + + i Remove z + +test.R:7:11 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `z` + + 5 │ f(x, y z) + 6 │ foo[x, y z] + > 7 │ foo[[x, y z]] + │ ^ + 8 │ + 9 │ f(, x y) + + i Remove z + +test.R:9:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `y` + + 7 │ foo[[x, y z]] + 8 │ + > 9 │ f(, x y) + │ ^ + 10 │ foo[, x y] + 11 │ foo[[, x y]] + + i Remove y + +test.R:10:9 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `y` + + 9 │ f(, x y) + > 10 │ foo[, x y] + │ ^ + 11 │ foo[[, x y]] + 12 │ + + i Remove y + +test.R:11:10 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `y` + + 9 │ f(, x y) + 10 │ foo[, x y] + > 11 │ foo[[, x y]] + │ ^ + 12 │ + + i Remove y +``` + +## Call arguments + +```r +fn() +fn(a) +fn(a, b) +fn(...) +fn(a, ..., b) + +fn(a = 1) +fn(a = ) + +fn(a = 1, b = 2) +fn(a = , b = 2) + +fn(... = 1) +fn(... =) + +fn(..1 = 2) +fn(..1 =) + +fn("arg" = 1) +fn("arg" =) + +# Comma tests +fn(,) +fn(,,,) +fn(,,a,,b,,) + +# Comment tests +fn( + # comment +) +fn(, + # comment +,) +fn( + a, # comment1 + b # comment2 +) +fn( + a # comment1 + = # comment2 + 1 # comment3 +) + +{expr}(a = 1) + +# `NULL` argument name (r-lib/tree-sitter-r#164) +fn( + NULL = 1 +) +fn( + NULL = +) +fn( + NULL = , +) + +# A standard call, not a special node (r-lib/tree-sitter-r#197) +return() +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..2 "fn" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@2..3 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@3..4 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@4..7 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@7..8 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@8..9 "a" [] [], + }, + }, + ], + r_paren_token: R_PAREN@9..10 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@10..13 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@13..14 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@14..15 "a" [] [], + }, + }, + COMMA@15..17 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@17..18 "b" [] [], + }, + }, + ], + r_paren_token: R_PAREN@18..19 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@19..22 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@22..23 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDots { + value_token: DOTS@23..26 "..." [] [], + }, + }, + ], + r_paren_token: R_PAREN@26..27 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@27..30 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@30..31 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@31..32 "a" [] [], + }, + }, + COMMA@32..34 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RDots { + value_token: DOTS@34..37 "..." [] [], + }, + }, + COMMA@37..39 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@39..40 "b" [] [], + }, + }, + ], + r_paren_token: R_PAREN@40..41 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@41..45 "fn" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@45..46 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@46..48 "a" [] [Whitespace(" ")], + }, + eq_token: EQUAL@48..50 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@50..51 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@51..52 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@52..55 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@55..56 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@56..58 "a" [] [Whitespace(" ")], + }, + eq_token: EQUAL@58..60 "=" [] [Whitespace(" ")], + }, + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@60..61 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@61..65 "fn" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@65..66 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@66..68 "a" [] [Whitespace(" ")], + }, + eq_token: EQUAL@68..70 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@70..71 "1" [] [], + }, + }, + COMMA@71..73 "," [] [Whitespace(" ")], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@73..75 "b" [] [Whitespace(" ")], + }, + eq_token: EQUAL@75..77 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@77..78 "2" [] [], + }, + }, + ], + r_paren_token: R_PAREN@78..79 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@79..82 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@82..83 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@83..85 "a" [] [Whitespace(" ")], + }, + eq_token: EQUAL@85..87 "=" [] [Whitespace(" ")], + }, + value: missing (optional), + }, + COMMA@87..89 "," [] [Whitespace(" ")], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@89..91 "b" [] [Whitespace(" ")], + }, + eq_token: EQUAL@91..93 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@93..94 "2" [] [], + }, + }, + ], + r_paren_token: R_PAREN@94..95 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@95..99 "fn" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@99..100 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RDots { + value_token: DOTS@100..104 "..." [] [Whitespace(" ")], + }, + eq_token: EQUAL@104..106 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@106..107 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@107..108 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@108..111 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@111..112 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RDots { + value_token: DOTS@112..116 "..." [] [Whitespace(" ")], + }, + eq_token: EQUAL@116..117 "=" [] [], + }, + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@117..118 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@118..122 "fn" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@122..123 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RDotDotI { + value_token: DOTDOTI@123..127 "..1" [] [Whitespace(" ")], + }, + eq_token: EQUAL@127..129 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@129..130 "2" [] [], + }, + }, + ], + r_paren_token: R_PAREN@130..131 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@131..134 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@134..135 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RDotDotI { + value_token: DOTDOTI@135..139 "..1" [] [Whitespace(" ")], + }, + eq_token: EQUAL@139..140 "=" [] [], + }, + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@140..141 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@141..145 "fn" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@145..146 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@146..152 "\"arg\"" [] [Whitespace(" ")], + }, + eq_token: EQUAL@152..154 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@154..155 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@155..156 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@156..159 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@159..160 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@160..166 "\"arg\"" [] [Whitespace(" ")], + }, + eq_token: EQUAL@166..167 "=" [] [], + }, + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@167..168 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@168..186 "fn" [Newline("\n"), Newline("\n"), Comments("# Comma tests"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@186..187 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@187..188 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@188..189 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@189..192 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@192..193 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@193..194 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@194..195 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@195..196 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@196..197 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@197..200 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@200..201 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@201..202 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@202..203 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@203..204 "a" [] [], + }, + }, + COMMA@204..205 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@205..206 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@206..207 "b" [] [], + }, + }, + COMMA@207..208 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@208..209 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@209..210 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@210..230 "fn" [Newline("\n"), Newline("\n"), Comments("# Comment tests"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@230..231 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@231..245 ")" [Newline("\n"), Whitespace(" "), Comments("# comment"), Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@245..248 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@248..249 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@249..250 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@250..264 "," [Newline("\n"), Whitespace(" "), Comments("# comment"), Newline("\n")] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@264..265 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@265..268 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@268..269 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@269..273 "a" [Newline("\n"), Whitespace(" ")] [], + }, + }, + COMMA@273..285 "," [] [Whitespace(" "), Comments("# comment1")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@285..300 "b" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")], + }, + }, + ], + r_paren_token: R_PAREN@300..302 ")" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@302..305 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@305..306 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@306..321 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment1")], + }, + eq_token: EQUAL@321..336 "=" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@336..351 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")], + }, + }, + ], + r_paren_token: R_PAREN@351..353 ")" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RBracedStatements { + l_curly_token: L_CURLY@353..356 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@356..360 "expr" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@360..361 "}" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@361..362 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@362..364 "a" [] [Whitespace(" ")], + }, + eq_token: EQUAL@364..366 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@366..367 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@367..368 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@368..421 "fn" [Newline("\n"), Newline("\n"), Comments("# `NULL` argument nam ..."), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@421..422 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RNullExpression { + null_token: NULL_KW@422..430 "NULL" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + eq_token: EQUAL@430..432 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@432..433 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@433..435 ")" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@435..438 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@438..439 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RNullExpression { + null_token: NULL_KW@439..447 "NULL" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + eq_token: EQUAL@447..449 "=" [] [Whitespace(" ")], + }, + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@449..451 ")" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@451..454 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@454..455 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RNullExpression { + null_token: NULL_KW@455..463 "NULL" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + eq_token: EQUAL@463..465 "=" [] [Whitespace(" ")], + }, + value: missing (optional), + }, + COMMA@465..466 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_paren_token: R_PAREN@466..468 ")" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@468..540 "return" [Newline("\n"), Newline("\n"), Comments("# A standard call, no ..."), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@540..541 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@541..542 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@542..543 "" [Newline("\n")] [], +} +``` + +## Side by side arguments [error] + +```r +fn(a b) +fn(a b c) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..2 "fn" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@2..3 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@3..5 "a" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@5..6 "b" [] [], + }, + }, + ], + r_paren_token: R_PAREN@6..7 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@7..10 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@10..11 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@11..13 "a" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@13..15 "b" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@15..16 "c" [] [], + }, + }, + ], + r_paren_token: R_PAREN@16..17 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@17..18 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `b` + + > 1 │ fn(a b) + │ ^ + 2 │ fn(a b c) + 3 │ + + i Remove b + +test.R:2:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `b` + + 1 │ fn(a b) + > 2 │ fn(a b c) + │ ^ + 3 │ + + i Remove b + +test.R:2:8 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `c` + + 1 │ fn(a b) + > 2 │ fn(a b c) + │ ^ + 3 │ + + i Remove c +``` + +## An argument value that starts no expression [error] + +Diagnostic should be about the faulty expression, not a comma + +```r +fn(* 1) +fn(a, * 1) +fn(a = * 1) +fn(,, * 1) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..2 "fn" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@2..3 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RBogusExpression { + items: [ + MULTIPLY@3..5 "*" [] [Whitespace(" ")], + R_DOUBLE_LITERAL@5..6 "1" [] [], + ], + }, + }, + ], + r_paren_token: R_PAREN@6..7 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@7..10 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@10..11 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@11..12 "a" [] [], + }, + }, + COMMA@12..14 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RBogusExpression { + items: [ + MULTIPLY@14..16 "*" [] [Whitespace(" ")], + R_DOUBLE_LITERAL@16..17 "1" [] [], + ], + }, + }, + ], + r_paren_token: R_PAREN@17..18 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@18..21 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@21..22 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@22..24 "a" [] [Whitespace(" ")], + }, + eq_token: EQUAL@24..26 "=" [] [Whitespace(" ")], + }, + value: RBogusExpression { + items: [ + MULTIPLY@26..28 "*" [] [Whitespace(" ")], + R_DOUBLE_LITERAL@28..29 "1" [] [], + ], + }, + }, + ], + r_paren_token: R_PAREN@29..30 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@30..33 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@33..34 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@34..35 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@35..37 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RBogusExpression { + items: [ + MULTIPLY@37..39 "*" [] [Whitespace(" ")], + R_DOUBLE_LITERAL@39..40 "1" [] [], + ], + }, + }, + ], + r_paren_token: R_PAREN@40..41 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@41..42 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '* 1'. + + > 1 │ fn(* 1) + │ ^^^ + 2 │ fn(a, * 1) + 3 │ fn(a = * 1) + +test.R:2:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '* 1'. + + 1 │ fn(* 1) + > 2 │ fn(a, * 1) + │ ^^^ + 3 │ fn(a = * 1) + 4 │ fn(,, * 1) + +test.R:3:8 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '* 1'. + + 1 │ fn(* 1) + 2 │ fn(a, * 1) + > 3 │ fn(a = * 1) + │ ^^^ + 4 │ fn(,, * 1) + 5 │ + +test.R:4:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '* 1'. + + 2 │ fn(a, * 1) + 3 │ fn(a = * 1) + > 4 │ fn(,, * 1) + │ ^^^ + 5 │ +``` + +Same for `[` and `[[` + +```r +x[* 1] +x[[* 1]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@1..2 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RBogusExpression { + items: [ + MULTIPLY@2..4 "*" [] [Whitespace(" ")], + R_DOUBLE_LITERAL@4..5 "1" [] [], + ], + }, + }, + ], + r_brack_token: R_BRACK@5..6 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@6..8 "x" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@8..10 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RBogusExpression { + items: [ + MULTIPLY@10..12 "*" [] [Whitespace(" ")], + R_DOUBLE_LITERAL@12..13 "1" [] [], + ], + }, + }, + ], + r_brack2_token: R_BRACK2@13..15 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@15..16 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '* 1'. + + > 1 │ x[* 1] + │ ^^^ + 2 │ x[[* 1]] + 3 │ + +test.R:2:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '* 1'. + + 1 │ x[* 1] + > 2 │ x[[* 1]] + │ ^^^ + 3 │ +``` + +## Closing delimiter belonging to someone else [error] + +The argument list ends at any closing delimiter, not just its own, so the `}` +survives for its owner rather than being eaten as an argument. Nothing was +written between the `(` and the `}`, so the list is empty rather than holding +one empty argument. + +```r +fn(} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..2 "fn" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@2..3 "(" [] [], + items: RArgumentList [], + r_paren_token: missing (required), + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_CURLY@3..4 "}" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@4..5 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `)` but instead found `}` + + > 1 │ fn(} + │ ^ + 2 │ + + i Remove } +``` + +```r +fn(a} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..2 "fn" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@2..3 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@3..4 "a" [] [], + }, + }, + ], + r_paren_token: missing (required), + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_CURLY@4..5 "}" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@5..6 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `)` but instead found `}` + + > 1 │ fn(a} + │ ^ + 2 │ + + i Remove } +``` + +## A newline or comment between a name and its `=` + +`parse_argument()` looks one token past the name to find the `=`, and that +lookahead skips trivia to reach it + +```r +f(a + = 1) + +f(a # c + = 1) + +f("a" # c + = 1) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..1 "f" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@1..2 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@2..3 "a" [] [], + }, + eq_token: EQUAL@3..8 "=" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@8..9 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@9..10 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@10..13 "f" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@13..14 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@14..19 "a" [] [Whitespace(" "), Comments("# c")], + }, + eq_token: EQUAL@19..24 "=" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@24..25 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@25..26 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@26..29 "f" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@29..30 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@30..37 "\"a\"" [] [Whitespace(" "), Comments("# c")], + }, + eq_token: EQUAL@37..42 "=" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@42..43 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@43..44 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@44..45 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/colon.md b/crates/air_r_parser_tests/tests/corpus/expressions/colon.md new file mode 100644 index 00000000..b6e94518 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/colon.md @@ -0,0 +1,6 @@ +## Colon + +```r +1:2 +(1 + 1):-5 +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/colon.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/colon.snap.md new file mode 100644 index 00000000..c8bccaf3 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/colon.snap.md @@ -0,0 +1,54 @@ + + +## Colon + +```r +1:2 +(1 + 1):-5 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..1 "1" [] [], + }, + operator: COLON@1..2 ":" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@2..3 "2" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RParenthesizedExpression { + l_paren_token: L_PAREN@3..5 "(" [Newline("\n")] [], + body: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..7 "1" [] [Whitespace(" ")], + }, + operator: PLUS@7..9 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@9..10 "1" [] [], + }, + }, + r_paren_token: R_PAREN@10..11 ")" [] [], + }, + operator: COLON@11..12 ":" [] [], + right: RUnaryExpression { + operator: MINUS@12..13 "-" [] [], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@13..14 "5" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@14..15 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/extract.md b/crates/air_r_parser_tests/tests/corpus/expressions/extract.md new file mode 100644 index 00000000..9ed11613 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/extract.md @@ -0,0 +1,134 @@ +## Dollar + +```r +foo$bar +foo$bar$baz +foo$bar@baz +foo$bar() +foo$"bar" +foo$bar()$baz[[1]]$bam +foo$... +foo$..1 +``` + +## Slot + +```r +foo@bar +foo@bar$baz +foo@bar() +foo@"bar" +foo@... +foo@..1 +``` + +## Dollar, at, namespace, namespace internal with expression RHS [error] + +```r +# The RHS of these nodes must be a string/identifier, so we nicely get a true +# node here alongside the braces. Even if that's not parsable R code, it's +# useful for completions and highlighting. +foo${bar} +foo@{bar} +foo::{bar} +foo:::{bar} +``` + +## Dollar no RHS [error] + +```r +foo$ +``` + +## Slot no RHS [error] + +```r +foo@ +``` + +## Dollar, at, namespace, namespace internal with `if` RHS [error] + +```r +# The RHS's of these nodes are restricted to strings and identifiers, so the RHS +# is left empty and the `if` goes on to be its own statement, which then errors +# because it doesn't have a `(`. This is actually pretty decent behavior. +foo$if +foo@if +foo::if +foo:::if +``` + +## Dollar, at, namespace, namespace internal with newline before operator [error] + +```r +# Newlines are not allowed before the operator, so none of these are valid +x +$y + +x +@y + +x +::y + +x +:::y +``` + +## Extract operators + +```r +a$b +a@b + +a$`b` +a@`b` + +a$"b" +a@"b" + +a()$b +a()@b + +a$b$c +a@b@c + +a$b() +a@b() + +a$... +a@... + +a$..1 +a@..1 + +a$..10 +a@..10 +``` + +## Extract with a literal RHS [error] + +The RHS slot is left missing rather than swallowing the invalid right-hand side. + +```r +a$1 +a$NA +a$NULL +a$TRUE +a$(b) +a${ b } + +a@1 +a@NA +a@NULL +a@TRUE +a@(b) +a@{ b } +``` + +## Dollar tolerates a newline + +```r +df$ + x +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/extract.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/extract.snap.md new file mode 100644 index 00000000..c7bba777 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/extract.snap.md @@ -0,0 +1,1713 @@ + + +## Dollar + +```r +foo$bar +foo$bar$baz +foo$bar@baz +foo$bar() +foo$"bar" +foo$bar()$baz[[1]]$bam +foo$... +foo$..1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + operator: DOLLAR@3..4 "$" [] [], + right: RIdentifier { + name_token: IDENT@4..7 "bar" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RExtractExpression { + left: RIdentifier { + name_token: IDENT@7..11 "foo" [Newline("\n")] [], + }, + operator: DOLLAR@11..12 "$" [] [], + right: RIdentifier { + name_token: IDENT@12..15 "bar" [] [], + }, + }, + operator: DOLLAR@15..16 "$" [] [], + right: RIdentifier { + name_token: IDENT@16..19 "baz" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RExtractExpression { + left: RIdentifier { + name_token: IDENT@19..23 "foo" [Newline("\n")] [], + }, + operator: DOLLAR@23..24 "$" [] [], + right: RIdentifier { + name_token: IDENT@24..27 "bar" [] [], + }, + }, + operator: AT@27..28 "@" [] [], + right: RIdentifier { + name_token: IDENT@28..31 "baz" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RExtractExpression { + left: RIdentifier { + name_token: IDENT@31..35 "foo" [Newline("\n")] [], + }, + operator: DOLLAR@35..36 "$" [] [], + right: RIdentifier { + name_token: IDENT@36..39 "bar" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@39..40 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@40..41 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@41..45 "foo" [Newline("\n")] [], + }, + operator: DOLLAR@45..46 "$" [] [], + right: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@46..51 "\"bar\"" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RSubset2 { + function: RExtractExpression { + left: RCall { + function: RExtractExpression { + left: RIdentifier { + name_token: IDENT@51..55 "foo" [Newline("\n")] [], + }, + operator: DOLLAR@55..56 "$" [] [], + right: RIdentifier { + name_token: IDENT@56..59 "bar" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@59..60 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@60..61 ")" [] [], + }, + }, + operator: DOLLAR@61..62 "$" [] [], + right: RIdentifier { + name_token: IDENT@62..65 "baz" [] [], + }, + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@65..67 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@67..68 "1" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@68..70 "]]" [] [], + }, + }, + operator: DOLLAR@70..71 "$" [] [], + right: RIdentifier { + name_token: IDENT@71..74 "bam" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@74..78 "foo" [Newline("\n")] [], + }, + operator: DOLLAR@78..79 "$" [] [], + right: RDots { + value_token: DOTS@79..82 "..." [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@82..86 "foo" [Newline("\n")] [], + }, + operator: DOLLAR@86..87 "$" [] [], + right: RDotDotI { + value_token: DOTDOTI@87..90 "..1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@90..91 "" [Newline("\n")] [], +} +``` + +## Slot + +```r +foo@bar +foo@bar$baz +foo@bar() +foo@"bar" +foo@... +foo@..1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + operator: AT@3..4 "@" [] [], + right: RIdentifier { + name_token: IDENT@4..7 "bar" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RExtractExpression { + left: RIdentifier { + name_token: IDENT@7..11 "foo" [Newline("\n")] [], + }, + operator: AT@11..12 "@" [] [], + right: RIdentifier { + name_token: IDENT@12..15 "bar" [] [], + }, + }, + operator: DOLLAR@15..16 "$" [] [], + right: RIdentifier { + name_token: IDENT@16..19 "baz" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RExtractExpression { + left: RIdentifier { + name_token: IDENT@19..23 "foo" [Newline("\n")] [], + }, + operator: AT@23..24 "@" [] [], + right: RIdentifier { + name_token: IDENT@24..27 "bar" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@27..28 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@28..29 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@29..33 "foo" [Newline("\n")] [], + }, + operator: AT@33..34 "@" [] [], + right: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@34..39 "\"bar\"" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@39..43 "foo" [Newline("\n")] [], + }, + operator: AT@43..44 "@" [] [], + right: RDots { + value_token: DOTS@44..47 "..." [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@47..51 "foo" [Newline("\n")] [], + }, + operator: AT@51..52 "@" [] [], + right: RDotDotI { + value_token: DOTDOTI@52..55 "..1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@55..56 "" [Newline("\n")] [], +} +``` + +## Dollar, at, namespace, namespace internal with expression RHS [error] + +```r +# The RHS of these nodes must be a string/identifier, so we nicely get a true +# node here alongside the braces. Even if that's not parsable R code, it's +# useful for completions and highlighting. +foo${bar} +foo@{bar} +foo::{bar} +foo:::{bar} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@0..199 "foo" [Comments("# The RHS of these no ..."), Newline("\n"), Comments("# node here alongside ..."), Newline("\n"), Comments("# useful for completi ..."), Newline("\n")] [], + }, + operator: DOLLAR@199..200 "$" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@200..201 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@201..204 "bar" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@204..205 "}" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@205..209 "foo" [Newline("\n")] [], + }, + operator: AT@209..210 "@" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@210..211 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@211..214 "bar" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@214..215 "}" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@215..219 "foo" [Newline("\n")] [], + }, + operator: COLON2@219..221 "::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@221..222 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@222..225 "bar" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@225..226 "}" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@226..230 "foo" [Newline("\n")] [], + }, + operator: COLON3@230..233 ":::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@233..234 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@234..237 "bar" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@237..238 "}" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@238..239 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:4:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 2 │ # node here alongside the braces. Even if that's not parsable R code, it's + 3 │ # useful for completions and highlighting. + > 4 │ foo${bar} + │ ^ + 5 │ foo@{bar} + 6 │ foo::{bar} + + i `$` requires a name on the right. + + 2 │ # node here alongside the braces. Even if that's not parsable R code, it's + 3 │ # useful for completions and highlighting. + > 4 │ foo${bar} + │ ^ + 5 │ foo@{bar} + 6 │ foo::{bar} + +test.R:5:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 3 │ # useful for completions and highlighting. + 4 │ foo${bar} + > 5 │ foo@{bar} + │ ^ + 6 │ foo::{bar} + 7 │ foo:::{bar} + + i `@` requires a name on the right. + + 3 │ # useful for completions and highlighting. + 4 │ foo${bar} + > 5 │ foo@{bar} + │ ^ + 6 │ foo::{bar} + 7 │ foo:::{bar} + +test.R:6:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 4 │ foo${bar} + 5 │ foo@{bar} + > 6 │ foo::{bar} + │ ^ + 7 │ foo:::{bar} + 8 │ + + i `::` requires a name on the right. + + 4 │ foo${bar} + 5 │ foo@{bar} + > 6 │ foo::{bar} + │ ^^ + 7 │ foo:::{bar} + 8 │ + +test.R:7:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 5 │ foo@{bar} + 6 │ foo::{bar} + > 7 │ foo:::{bar} + │ ^ + 8 │ + + i `:::` requires a name on the right. + + 5 │ foo@{bar} + 6 │ foo::{bar} + > 7 │ foo:::{bar} + │ ^^^ + 8 │ +``` + +## Dollar no RHS [error] + +```r +foo$ +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + operator: DOLLAR@3..4 "$" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@4..5 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 1 │ foo$ + > 2 │ + │ + + i `$` requires a name on the right. + + > 1 │ foo$ + │ ^ + 2 │ +``` + +## Slot no RHS [error] + +```r +foo@ +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + operator: AT@3..4 "@" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@4..5 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 1 │ foo@ + > 2 │ + │ + + i `@` requires a name on the right. + + > 1 │ foo@ + │ ^ + 2 │ +``` + +## Dollar, at, namespace, namespace internal with `if` RHS [error] + +```r +# The RHS's of these nodes are restricted to strings and identifiers, so the RHS +# is left empty and the `if` goes on to be its own statement, which then errors +# because it doesn't have a `(`. This is actually pretty decent behavior. +foo$if +foo@if +foo::if +foo:::if +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@0..238 "foo" [Comments("# The RHS's of these ..."), Newline("\n"), Comments("# is left empty and t ..."), Newline("\n"), Comments("# because it doesn't ..."), Newline("\n")] [], + }, + operator: DOLLAR@238..239 "$" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@239..241 "if" [] [], + l_paren_token: missing (required), + condition: RExtractExpression { + left: RIdentifier { + name_token: IDENT@241..245 "foo" [Newline("\n")] [], + }, + operator: AT@245..246 "@" [] [], + right: missing (required), + }, + r_paren_token: missing (required), + consequence: RIfStatement { + if_token: IF_KW@246..248 "if" [] [], + l_paren_token: missing (required), + condition: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@248..252 "foo" [Newline("\n")] [], + }, + operator: COLON2@252..254 "::" [] [], + right: missing (required), + }, + r_paren_token: missing (required), + consequence: RIfStatement { + if_token: IF_KW@254..256 "if" [] [], + l_paren_token: missing (required), + condition: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@256..260 "foo" [Newline("\n")] [], + }, + operator: COLON3@260..263 ":::" [] [], + right: missing (required), + }, + r_paren_token: missing (required), + consequence: RIfStatement { + if_token: IF_KW@263..265 "if" [] [], + l_paren_token: missing (required), + condition: missing (required), + r_paren_token: missing (required), + consequence: missing (required), + else_clause: missing (optional), + }, + else_clause: missing (optional), + }, + else_clause: missing (optional), + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@265..266 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:4:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 2 │ # is left empty and the `if` goes on to be its own statement, which then errors + 3 │ # because it doesn't have a `(`. This is actually pretty decent behavior. + > 4 │ foo$if + │ ^^ + 5 │ foo@if + 6 │ foo::if + + i `$` requires a name on the right. + + 2 │ # is left empty and the `if` goes on to be its own statement, which then errors + 3 │ # because it doesn't have a `(`. This is actually pretty decent behavior. + > 4 │ foo$if + │ ^ + 5 │ foo@if + 6 │ foo::if + +test.R:5:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `(` but instead found `foo` + + 3 │ # because it doesn't have a `(`. This is actually pretty decent behavior. + 4 │ foo$if + > 5 │ foo@if + │ ^^^ + 6 │ foo::if + 7 │ foo:::if + + i Remove foo + +test.R:5:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 3 │ # because it doesn't have a `(`. This is actually pretty decent behavior. + 4 │ foo$if + > 5 │ foo@if + │ ^^ + 6 │ foo::if + 7 │ foo:::if + + i `@` requires a name on the right. + + 3 │ # because it doesn't have a `(`. This is actually pretty decent behavior. + 4 │ foo$if + > 5 │ foo@if + │ ^ + 6 │ foo::if + 7 │ foo:::if + +test.R:6:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `(` but instead found `foo` + + 4 │ foo$if + 5 │ foo@if + > 6 │ foo::if + │ ^^^ + 7 │ foo:::if + 8 │ + + i Remove foo + +test.R:6:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 4 │ foo$if + 5 │ foo@if + > 6 │ foo::if + │ ^^ + 7 │ foo:::if + 8 │ + + i `::` requires a name on the right. + + 4 │ foo$if + 5 │ foo@if + > 6 │ foo::if + │ ^^ + 7 │ foo:::if + 8 │ + +test.R:7:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `(` but instead found `foo` + + 5 │ foo@if + 6 │ foo::if + > 7 │ foo:::if + │ ^^^ + 8 │ + + i Remove foo + +test.R:7:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 5 │ foo@if + 6 │ foo::if + > 7 │ foo:::if + │ ^^ + 8 │ + + i `:::` requires a name on the right. + + 5 │ foo@if + 6 │ foo::if + > 7 │ foo:::if + │ ^^^ + 8 │ + +test.R:8:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `(` but instead the file ends + + 6 │ foo::if + 7 │ foo:::if + > 8 │ + │ + + i the file ends here + + 6 │ foo::if + 7 │ foo:::if + > 8 │ + │ +``` + +## Dollar, at, namespace, namespace internal with newline before operator [error] + +```r +# Newlines are not allowed before the operator, so none of these are valid +x +$y + +x +@y + +x +::y + +x +:::y +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@0..76 "x" [Comments("# Newlines are not al ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + DOLLAR@76..78 "$" [Newline("\n")] [], + IDENT@78..79 "y" [] [], + ], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@79..82 "x" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + AT@82..84 "@" [Newline("\n")] [], + IDENT@84..85 "y" [] [], + ], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@85..88 "x" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + COLON2@88..91 "::" [Newline("\n")] [], + IDENT@91..92 "y" [] [], + ], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@92..95 "x" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + COLON3@95..99 ":::" [Newline("\n")] [], + IDENT@99..100 "y" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@100..101 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '$y'. + + 1 │ # Newlines are not allowed before the operator, so none of these are valid + 2 │ x + > 3 │ $y + │ ^^ + 4 │ + 5 │ x + +test.R:6:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '@y'. + + 5 │ x + > 6 │ @y + │ ^^ + 7 │ + 8 │ x + +test.R:9:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '::y'. + + 8 │ x + > 9 │ ::y + │ ^^^ + 10 │ + 11 │ x + +test.R:12:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ':::y'. + + 11 │ x + > 12 │ :::y + │ ^^^^ + 13 │ +``` + +## Extract operators + +```r +a$b +a@b + +a$`b` +a@`b` + +a$"b" +a@"b" + +a()$b +a()@b + +a$b$c +a@b@c + +a$b() +a@b() + +a$... +a@... + +a$..1 +a@..1 + +a$..10 +a@..10 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@0..1 "a" [] [], + }, + operator: DOLLAR@1..2 "$" [] [], + right: RIdentifier { + name_token: IDENT@2..3 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@3..5 "a" [Newline("\n")] [], + }, + operator: AT@5..6 "@" [] [], + right: RIdentifier { + name_token: IDENT@6..7 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@7..10 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: DOLLAR@10..11 "$" [] [], + right: RIdentifier { + name_token: IDENT@11..14 "`b`" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@14..16 "a" [Newline("\n")] [], + }, + operator: AT@16..17 "@" [] [], + right: RIdentifier { + name_token: IDENT@17..20 "`b`" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@20..23 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: DOLLAR@23..24 "$" [] [], + right: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@24..27 "\"b\"" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@27..29 "a" [Newline("\n")] [], + }, + operator: AT@29..30 "@" [] [], + right: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@30..33 "\"b\"" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RCall { + function: RIdentifier { + name_token: IDENT@33..36 "a" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@36..37 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@37..38 ")" [] [], + }, + }, + operator: DOLLAR@38..39 "$" [] [], + right: RIdentifier { + name_token: IDENT@39..40 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RCall { + function: RIdentifier { + name_token: IDENT@40..42 "a" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@42..43 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@43..44 ")" [] [], + }, + }, + operator: AT@44..45 "@" [] [], + right: RIdentifier { + name_token: IDENT@45..46 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RExtractExpression { + left: RIdentifier { + name_token: IDENT@46..49 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: DOLLAR@49..50 "$" [] [], + right: RIdentifier { + name_token: IDENT@50..51 "b" [] [], + }, + }, + operator: DOLLAR@51..52 "$" [] [], + right: RIdentifier { + name_token: IDENT@52..53 "c" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RExtractExpression { + left: RIdentifier { + name_token: IDENT@53..55 "a" [Newline("\n")] [], + }, + operator: AT@55..56 "@" [] [], + right: RIdentifier { + name_token: IDENT@56..57 "b" [] [], + }, + }, + operator: AT@57..58 "@" [] [], + right: RIdentifier { + name_token: IDENT@58..59 "c" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RExtractExpression { + left: RIdentifier { + name_token: IDENT@59..62 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: DOLLAR@62..63 "$" [] [], + right: RIdentifier { + name_token: IDENT@63..64 "b" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@64..65 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@65..66 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RExtractExpression { + left: RIdentifier { + name_token: IDENT@66..68 "a" [Newline("\n")] [], + }, + operator: AT@68..69 "@" [] [], + right: RIdentifier { + name_token: IDENT@69..70 "b" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@70..71 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@71..72 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@72..75 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: DOLLAR@75..76 "$" [] [], + right: RDots { + value_token: DOTS@76..79 "..." [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@79..81 "a" [Newline("\n")] [], + }, + operator: AT@81..82 "@" [] [], + right: RDots { + value_token: DOTS@82..85 "..." [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@85..88 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: DOLLAR@88..89 "$" [] [], + right: RDotDotI { + value_token: DOTDOTI@89..92 "..1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@92..94 "a" [Newline("\n")] [], + }, + operator: AT@94..95 "@" [] [], + right: RDotDotI { + value_token: DOTDOTI@95..98 "..1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@98..101 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: DOLLAR@101..102 "$" [] [], + right: RDotDotI { + value_token: DOTDOTI@102..106 "..10" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@106..108 "a" [Newline("\n")] [], + }, + operator: AT@108..109 "@" [] [], + right: RDotDotI { + value_token: DOTDOTI@109..113 "..10" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@113..114 "" [Newline("\n")] [], +} +``` + +## Extract with a literal RHS [error] + +The RHS slot is left missing rather than swallowing the invalid right-hand side. + +```r +a$1 +a$NA +a$NULL +a$TRUE +a$(b) +a${ b } + +a@1 +a@NA +a@NULL +a@TRUE +a@(b) +a@{ b } +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@0..1 "a" [] [], + }, + operator: DOLLAR@1..2 "$" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@2..3 "1" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@3..5 "a" [Newline("\n")] [], + }, + operator: DOLLAR@5..6 "$" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNaExpression { + value: NA_LOGICAL_KW@6..8 "NA" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@8..10 "a" [Newline("\n")] [], + }, + operator: DOLLAR@10..11 "$" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNullExpression { + null_token: NULL_KW@11..15 "NULL" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@15..17 "a" [Newline("\n")] [], + }, + operator: DOLLAR@17..18 "$" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RTrueExpression { + true_token: TRUE_KW@18..22 "TRUE" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RExtractExpression { + left: RIdentifier { + name_token: IDENT@22..24 "a" [Newline("\n")] [], + }, + operator: DOLLAR@24..25 "$" [] [], + right: missing (required), + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@25..26 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@26..27 "b" [] [], + }, + }, + ], + r_paren_token: R_PAREN@27..28 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@28..30 "a" [Newline("\n")] [], + }, + operator: DOLLAR@30..31 "$" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@31..33 "{" [] [Whitespace(" ")], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@33..35 "b" [] [Whitespace(" ")], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@35..36 "}" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@36..39 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: AT@39..40 "@" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@40..41 "1" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@41..43 "a" [Newline("\n")] [], + }, + operator: AT@43..44 "@" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNaExpression { + value: NA_LOGICAL_KW@44..46 "NA" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@46..48 "a" [Newline("\n")] [], + }, + operator: AT@48..49 "@" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNullExpression { + null_token: NULL_KW@49..53 "NULL" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@53..55 "a" [Newline("\n")] [], + }, + operator: AT@55..56 "@" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RTrueExpression { + true_token: TRUE_KW@56..60 "TRUE" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RExtractExpression { + left: RIdentifier { + name_token: IDENT@60..62 "a" [Newline("\n")] [], + }, + operator: AT@62..63 "@" [] [], + right: missing (required), + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@63..64 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@64..65 "b" [] [], + }, + }, + ], + r_paren_token: R_PAREN@65..66 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@66..68 "a" [Newline("\n")] [], + }, + operator: AT@68..69 "@" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@69..71 "{" [] [Whitespace(" ")], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@71..73 "b" [] [Whitespace(" ")], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@73..74 "}" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@74..75 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + > 1 │ a$1 + │ ^ + 2 │ a$NA + 3 │ a$NULL + + i `$` requires a name on the right. + + > 1 │ a$1 + │ ^ + 2 │ a$NA + 3 │ a$NULL + +test.R:2:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 1 │ a$1 + > 2 │ a$NA + │ ^^ + 3 │ a$NULL + 4 │ a$TRUE + + i `$` requires a name on the right. + + 1 │ a$1 + > 2 │ a$NA + │ ^ + 3 │ a$NULL + 4 │ a$TRUE + +test.R:3:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 1 │ a$1 + 2 │ a$NA + > 3 │ a$NULL + │ ^^^^ + 4 │ a$TRUE + 5 │ a$(b) + + i `$` requires a name on the right. + + 1 │ a$1 + 2 │ a$NA + > 3 │ a$NULL + │ ^ + 4 │ a$TRUE + 5 │ a$(b) + +test.R:4:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 2 │ a$NA + 3 │ a$NULL + > 4 │ a$TRUE + │ ^^^^ + 5 │ a$(b) + 6 │ a${ b } + + i `$` requires a name on the right. + + 2 │ a$NA + 3 │ a$NULL + > 4 │ a$TRUE + │ ^ + 5 │ a$(b) + 6 │ a${ b } + +test.R:5:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 3 │ a$NULL + 4 │ a$TRUE + > 5 │ a$(b) + │ ^ + 6 │ a${ b } + 7 │ + + i `$` requires a name on the right. + + 3 │ a$NULL + 4 │ a$TRUE + > 5 │ a$(b) + │ ^ + 6 │ a${ b } + 7 │ + +test.R:6:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 4 │ a$TRUE + 5 │ a$(b) + > 6 │ a${ b } + │ ^ + 7 │ + 8 │ a@1 + + i `$` requires a name on the right. + + 4 │ a$TRUE + 5 │ a$(b) + > 6 │ a${ b } + │ ^ + 7 │ + 8 │ a@1 + +test.R:8:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 6 │ a${ b } + 7 │ + > 8 │ a@1 + │ ^ + 9 │ a@NA + 10 │ a@NULL + + i `@` requires a name on the right. + + 6 │ a${ b } + 7 │ + > 8 │ a@1 + │ ^ + 9 │ a@NA + 10 │ a@NULL + +test.R:9:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 8 │ a@1 + > 9 │ a@NA + │ ^^ + 10 │ a@NULL + 11 │ a@TRUE + + i `@` requires a name on the right. + + 8 │ a@1 + > 9 │ a@NA + │ ^ + 10 │ a@NULL + 11 │ a@TRUE + +test.R:10:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 8 │ a@1 + 9 │ a@NA + > 10 │ a@NULL + │ ^^^^ + 11 │ a@TRUE + 12 │ a@(b) + + i `@` requires a name on the right. + + 8 │ a@1 + 9 │ a@NA + > 10 │ a@NULL + │ ^ + 11 │ a@TRUE + 12 │ a@(b) + +test.R:11:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 9 │ a@NA + 10 │ a@NULL + > 11 │ a@TRUE + │ ^^^^ + 12 │ a@(b) + 13 │ a@{ b } + + i `@` requires a name on the right. + + 9 │ a@NA + 10 │ a@NULL + > 11 │ a@TRUE + │ ^ + 12 │ a@(b) + 13 │ a@{ b } + +test.R:12:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 10 │ a@NULL + 11 │ a@TRUE + > 12 │ a@(b) + │ ^ + 13 │ a@{ b } + 14 │ + + i `@` requires a name on the right. + + 10 │ a@NULL + 11 │ a@TRUE + > 12 │ a@(b) + │ ^ + 13 │ a@{ b } + 14 │ + +test.R:13:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 11 │ a@TRUE + 12 │ a@(b) + > 13 │ a@{ b } + │ ^ + 14 │ + + i `@` requires a name on the right. + + 11 │ a@TRUE + 12 │ a@(b) + > 13 │ a@{ b } + │ ^ + 14 │ +``` + +## Dollar tolerates a newline + +```r +df$ + x +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RExtractExpression { + left: RIdentifier { + name_token: IDENT@0..2 "df" [] [], + }, + operator: DOLLAR@2..3 "$" [] [], + right: RIdentifier { + name_token: IDENT@3..7 "x" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/formula.md b/crates/air_r_parser_tests/tests/corpus/expressions/formula.md new file mode 100644 index 00000000..6375013b --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/formula.md @@ -0,0 +1,6 @@ +## Formulas + +```r +~x +y~x +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/formula.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/formula.snap.md new file mode 100644 index 00000000..c5fbc3b1 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/formula.snap.md @@ -0,0 +1,38 @@ + + +## Formulas + +```r +~x +y~x +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RUnaryExpression { + operator: TILDE@0..1 "~" [] [], + argument: RIdentifier { + name_token: IDENT@1..2 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@2..4 "y" [Newline("\n")] [], + }, + operator: TILDE@4..5 "~" [] [], + right: RIdentifier { + name_token: IDENT@5..6 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@6..7 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/help.md b/crates/air_r_parser_tests/tests/corpus/expressions/help.md new file mode 100644 index 00000000..e59a717d --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/help.md @@ -0,0 +1,7 @@ +## Help operator + +```r +a ? b +a ? b <- 1 +?a +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/help.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/help.snap.md new file mode 100644 index 00000000..7fe5ea89 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/help.snap.md @@ -0,0 +1,57 @@ + + +## Help operator + +```r +a ? b +a ? b <- 1 +?a +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "a" [] [Whitespace(" ")], + }, + operator: WAT@2..4 "?" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@4..5 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@5..8 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: WAT@8..10 "?" [] [Whitespace(" ")], + right: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@10..12 "b" [] [Whitespace(" ")], + }, + operator: ASSIGN@12..15 "<-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@15..16 "1" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: WAT@16..18 "?" [Newline("\n")] [], + argument: RIdentifier { + name_token: IDENT@18..19 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@19..20 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/namespace.md b/crates/air_r_parser_tests/tests/corpus/expressions/namespace.md new file mode 100644 index 00000000..cc9807cc --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/namespace.md @@ -0,0 +1,134 @@ +## Namespace get + +```r +foo::bar +foo::bar(1) +foo::... +foo::..1 +...::foo +..1::foo +``` + +## Namespace get internal + +```r +foo:::bar +foo:::bar(1) +foo:::... +foo:::..1 +...:::foo +..1:::foo +``` + +## Namespace with missing RHS [error] + +```r +# It's nice that `::` enforces that its RHS can only be a string or identifier, +# so this gives us a pretty clean tree even though it is invalid R code. +# r-lib/tree-sitter-r#65 +library(dplyr::) + +library() + +1 +2 +3 +``` + +## Namespace with missing RHS - 2 [error] + +```r +foo:: +``` + +```r +foo::: +``` + +## Namespace operators + +```r +a::b +a:::b + +a::`b` +a:::`b` + +a::"b" +a:::"b" + +a::b() +a:::b() + +a::b$c() +a:::b@c() + +a::... +a:::... + +...::a +...:::a + +...::... +...:::... + +a::..1 +a:::..1 + +..1::a +..1:::a + +..1::..1 +..1:::..1 +``` + +## Namespace with a literal RHS [error] + +The RHS slot is left missing rather than swallowing the invalid right-hand side. + +```r +a::1 +a::NA +a::NULL +a::TRUE +a::(b) +a::{ b } + +a:::1 +a:::NA +a:::NULL +a:::TRUE +a:::(b) +a:::{ b } +``` + +## Chained `::` [error] + +```r +a::b::c +``` + +## Chained `:::` [error] + +```r +a:::b:::c +``` + +## Call on the left of `::` [error] + +```r +a()::b +``` + +## Call on the left of `:::` [error] + +```r +a():::b +``` + +## Namespace newline is significant only outside parens + +```r +f(stats:: + median) +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/namespace.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/namespace.snap.md new file mode 100644 index 00000000..21d2ae89 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/namespace.snap.md @@ -0,0 +1,1475 @@ + + +## Namespace get + +```r +foo::bar +foo::bar(1) +foo::... +foo::..1 +...::foo +..1::foo +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + operator: COLON2@3..5 "::" [] [], + right: RIdentifier { + name_token: IDENT@5..8 "bar" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@8..12 "foo" [Newline("\n")] [], + }, + operator: COLON2@12..14 "::" [] [], + right: RIdentifier { + name_token: IDENT@14..17 "bar" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@17..18 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@18..19 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@19..20 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@20..24 "foo" [Newline("\n")] [], + }, + operator: COLON2@24..26 "::" [] [], + right: RDots { + value_token: DOTS@26..29 "..." [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@29..33 "foo" [Newline("\n")] [], + }, + operator: COLON2@33..35 "::" [] [], + right: RDotDotI { + value_token: DOTDOTI@35..38 "..1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDots { + value_token: DOTS@38..42 "..." [Newline("\n")] [], + }, + operator: COLON2@42..44 "::" [] [], + right: RIdentifier { + name_token: IDENT@44..47 "foo" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDotDotI { + value_token: DOTDOTI@47..51 "..1" [Newline("\n")] [], + }, + operator: COLON2@51..53 "::" [] [], + right: RIdentifier { + name_token: IDENT@53..56 "foo" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@56..57 "" [Newline("\n")] [], +} +``` + +## Namespace get internal + +```r +foo:::bar +foo:::bar(1) +foo:::... +foo:::..1 +...:::foo +..1:::foo +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + operator: COLON3@3..6 ":::" [] [], + right: RIdentifier { + name_token: IDENT@6..9 "bar" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@9..13 "foo" [Newline("\n")] [], + }, + operator: COLON3@13..16 ":::" [] [], + right: RIdentifier { + name_token: IDENT@16..19 "bar" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@19..20 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@20..21 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@21..22 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@22..26 "foo" [Newline("\n")] [], + }, + operator: COLON3@26..29 ":::" [] [], + right: RDots { + value_token: DOTS@29..32 "..." [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@32..36 "foo" [Newline("\n")] [], + }, + operator: COLON3@36..39 ":::" [] [], + right: RDotDotI { + value_token: DOTDOTI@39..42 "..1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDots { + value_token: DOTS@42..46 "..." [Newline("\n")] [], + }, + operator: COLON3@46..49 ":::" [] [], + right: RIdentifier { + name_token: IDENT@49..52 "foo" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDotDotI { + value_token: DOTDOTI@52..56 "..1" [Newline("\n")] [], + }, + operator: COLON3@56..59 ":::" [] [], + right: RIdentifier { + name_token: IDENT@59..62 "foo" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@62..63 "" [Newline("\n")] [], +} +``` + +## Namespace with missing RHS [error] + +```r +# It's nice that `::` enforces that its RHS can only be a string or identifier, +# so this gives us a pretty clean tree even though it is invalid R code. +# r-lib/tree-sitter-r#65 +library(dplyr::) + +library() + +1 +2 +3 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..185 "library" [Comments("# It's nice that `::` ..."), Newline("\n"), Comments("# so this gives us a ..."), Newline("\n"), Comments("# r-lib/tree-sitter-r#65"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@185..186 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@186..191 "dplyr" [] [], + }, + operator: COLON2@191..193 "::" [] [], + right: missing (required), + }, + }, + ], + r_paren_token: R_PAREN@193..194 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@194..203 "library" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@203..204 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@204..205 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@205..208 "1" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@208..210 "2" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@210..212 "3" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@212..213 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:4:16 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 2 │ # so this gives us a pretty clean tree even though it is invalid R code. + 3 │ # r-lib/tree-sitter-r#65 + > 4 │ library(dplyr::) + │ ^ + 5 │ + 6 │ library() + + i `::` requires a name on the right. + + 2 │ # so this gives us a pretty clean tree even though it is invalid R code. + 3 │ # r-lib/tree-sitter-r#65 + > 4 │ library(dplyr::) + │ ^^ + 5 │ + 6 │ library() +``` + +## Namespace with missing RHS - 2 [error] + +```r +foo:: +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + operator: COLON2@3..5 "::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@5..6 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × A newline is not allowed between `::` and the name that follows. + + > 1 │ foo:: + │ ^^ + 2 │ +``` + +```r +foo::: +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + operator: COLON3@3..6 ":::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@6..7 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × A newline is not allowed between `:::` and the name that follows. + + > 1 │ foo::: + │ ^^^ + 2 │ +``` + +## Namespace operators + +```r +a::b +a:::b + +a::`b` +a:::`b` + +a::"b" +a:::"b" + +a::b() +a:::b() + +a::b$c() +a:::b@c() + +a::... +a:::... + +...::a +...:::a + +...::... +...:::... + +a::..1 +a:::..1 + +..1::a +..1:::a + +..1::..1 +..1:::..1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@0..1 "a" [] [], + }, + operator: COLON2@1..3 "::" [] [], + right: RIdentifier { + name_token: IDENT@3..4 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@4..6 "a" [Newline("\n")] [], + }, + operator: COLON3@6..9 ":::" [] [], + right: RIdentifier { + name_token: IDENT@9..10 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@10..13 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: COLON2@13..15 "::" [] [], + right: RIdentifier { + name_token: IDENT@15..18 "`b`" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@18..20 "a" [Newline("\n")] [], + }, + operator: COLON3@20..23 ":::" [] [], + right: RIdentifier { + name_token: IDENT@23..26 "`b`" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@26..29 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: COLON2@29..31 "::" [] [], + right: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@31..34 "\"b\"" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@34..36 "a" [Newline("\n")] [], + }, + operator: COLON3@36..39 ":::" [] [], + right: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@39..42 "\"b\"" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@42..45 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: COLON2@45..47 "::" [] [], + right: RIdentifier { + name_token: IDENT@47..48 "b" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@48..49 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@49..50 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@50..52 "a" [Newline("\n")] [], + }, + operator: COLON3@52..55 ":::" [] [], + right: RIdentifier { + name_token: IDENT@55..56 "b" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@56..57 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@57..58 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RExtractExpression { + left: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@58..61 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: COLON2@61..63 "::" [] [], + right: RIdentifier { + name_token: IDENT@63..64 "b" [] [], + }, + }, + operator: DOLLAR@64..65 "$" [] [], + right: RIdentifier { + name_token: IDENT@65..66 "c" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@66..67 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@67..68 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RExtractExpression { + left: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@68..70 "a" [Newline("\n")] [], + }, + operator: COLON3@70..73 ":::" [] [], + right: RIdentifier { + name_token: IDENT@73..74 "b" [] [], + }, + }, + operator: AT@74..75 "@" [] [], + right: RIdentifier { + name_token: IDENT@75..76 "c" [] [], + }, + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@76..77 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@77..78 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@78..81 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: COLON2@81..83 "::" [] [], + right: RDots { + value_token: DOTS@83..86 "..." [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@86..88 "a" [Newline("\n")] [], + }, + operator: COLON3@88..91 ":::" [] [], + right: RDots { + value_token: DOTS@91..94 "..." [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDots { + value_token: DOTS@94..99 "..." [Newline("\n"), Newline("\n")] [], + }, + operator: COLON2@99..101 "::" [] [], + right: RIdentifier { + name_token: IDENT@101..102 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDots { + value_token: DOTS@102..106 "..." [Newline("\n")] [], + }, + operator: COLON3@106..109 ":::" [] [], + right: RIdentifier { + name_token: IDENT@109..110 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDots { + value_token: DOTS@110..115 "..." [Newline("\n"), Newline("\n")] [], + }, + operator: COLON2@115..117 "::" [] [], + right: RDots { + value_token: DOTS@117..120 "..." [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDots { + value_token: DOTS@120..124 "..." [Newline("\n")] [], + }, + operator: COLON3@124..127 ":::" [] [], + right: RDots { + value_token: DOTS@127..130 "..." [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@130..133 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: COLON2@133..135 "::" [] [], + right: RDotDotI { + value_token: DOTDOTI@135..138 "..1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@138..140 "a" [Newline("\n")] [], + }, + operator: COLON3@140..143 ":::" [] [], + right: RDotDotI { + value_token: DOTDOTI@143..146 "..1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDotDotI { + value_token: DOTDOTI@146..151 "..1" [Newline("\n"), Newline("\n")] [], + }, + operator: COLON2@151..153 "::" [] [], + right: RIdentifier { + name_token: IDENT@153..154 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDotDotI { + value_token: DOTDOTI@154..158 "..1" [Newline("\n")] [], + }, + operator: COLON3@158..161 ":::" [] [], + right: RIdentifier { + name_token: IDENT@161..162 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDotDotI { + value_token: DOTDOTI@162..167 "..1" [Newline("\n"), Newline("\n")] [], + }, + operator: COLON2@167..169 "::" [] [], + right: RDotDotI { + value_token: DOTDOTI@169..172 "..1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RDotDotI { + value_token: DOTDOTI@172..176 "..1" [Newline("\n")] [], + }, + operator: COLON3@176..179 ":::" [] [], + right: RDotDotI { + value_token: DOTDOTI@179..182 "..1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@182..183 "" [Newline("\n")] [], +} +``` + +## Namespace with a literal RHS [error] + +The RHS slot is left missing rather than swallowing the invalid right-hand side. + +```r +a::1 +a::NA +a::NULL +a::TRUE +a::(b) +a::{ b } + +a:::1 +a:::NA +a:::NULL +a:::TRUE +a:::(b) +a:::{ b } +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@0..1 "a" [] [], + }, + operator: COLON2@1..3 "::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@3..4 "1" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@4..6 "a" [Newline("\n")] [], + }, + operator: COLON2@6..8 "::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNaExpression { + value: NA_LOGICAL_KW@8..10 "NA" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@10..12 "a" [Newline("\n")] [], + }, + operator: COLON2@12..14 "::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNullExpression { + null_token: NULL_KW@14..18 "NULL" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@18..20 "a" [Newline("\n")] [], + }, + operator: COLON2@20..22 "::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RTrueExpression { + true_token: TRUE_KW@22..26 "TRUE" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@26..28 "a" [Newline("\n")] [], + }, + operator: COLON2@28..30 "::" [] [], + right: missing (required), + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@30..31 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@31..32 "b" [] [], + }, + }, + ], + r_paren_token: R_PAREN@32..33 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@33..35 "a" [Newline("\n")] [], + }, + operator: COLON2@35..37 "::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@37..39 "{" [] [Whitespace(" ")], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@39..41 "b" [] [Whitespace(" ")], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@41..42 "}" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@42..45 "a" [Newline("\n"), Newline("\n")] [], + }, + operator: COLON3@45..48 ":::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@48..49 "1" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@49..51 "a" [Newline("\n")] [], + }, + operator: COLON3@51..54 ":::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNaExpression { + value: NA_LOGICAL_KW@54..56 "NA" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@56..58 "a" [Newline("\n")] [], + }, + operator: COLON3@58..61 ":::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNullExpression { + null_token: NULL_KW@61..65 "NULL" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@65..67 "a" [Newline("\n")] [], + }, + operator: COLON3@67..70 ":::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RTrueExpression { + true_token: TRUE_KW@70..74 "TRUE" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@74..76 "a" [Newline("\n")] [], + }, + operator: COLON3@76..79 ":::" [] [], + right: missing (required), + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@79..80 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@80..81 "b" [] [], + }, + }, + ], + r_paren_token: R_PAREN@81..82 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@82..84 "a" [Newline("\n")] [], + }, + operator: COLON3@84..87 ":::" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@87..89 "{" [] [Whitespace(" ")], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@89..91 "b" [] [Whitespace(" ")], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@91..92 "}" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@92..93 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + > 1 │ a::1 + │ ^ + 2 │ a::NA + 3 │ a::NULL + + i `::` requires a name on the right. + + > 1 │ a::1 + │ ^^ + 2 │ a::NA + 3 │ a::NULL + +test.R:2:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 1 │ a::1 + > 2 │ a::NA + │ ^^ + 3 │ a::NULL + 4 │ a::TRUE + + i `::` requires a name on the right. + + 1 │ a::1 + > 2 │ a::NA + │ ^^ + 3 │ a::NULL + 4 │ a::TRUE + +test.R:3:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 1 │ a::1 + 2 │ a::NA + > 3 │ a::NULL + │ ^^^^ + 4 │ a::TRUE + 5 │ a::(b) + + i `::` requires a name on the right. + + 1 │ a::1 + 2 │ a::NA + > 3 │ a::NULL + │ ^^ + 4 │ a::TRUE + 5 │ a::(b) + +test.R:4:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 2 │ a::NA + 3 │ a::NULL + > 4 │ a::TRUE + │ ^^^^ + 5 │ a::(b) + 6 │ a::{ b } + + i `::` requires a name on the right. + + 2 │ a::NA + 3 │ a::NULL + > 4 │ a::TRUE + │ ^^ + 5 │ a::(b) + 6 │ a::{ b } + +test.R:5:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 3 │ a::NULL + 4 │ a::TRUE + > 5 │ a::(b) + │ ^ + 6 │ a::{ b } + 7 │ + + i `::` requires a name on the right. + + 3 │ a::NULL + 4 │ a::TRUE + > 5 │ a::(b) + │ ^^ + 6 │ a::{ b } + 7 │ + +test.R:6:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 4 │ a::TRUE + 5 │ a::(b) + > 6 │ a::{ b } + │ ^ + 7 │ + 8 │ a:::1 + + i `::` requires a name on the right. + + 4 │ a::TRUE + 5 │ a::(b) + > 6 │ a::{ b } + │ ^^ + 7 │ + 8 │ a:::1 + +test.R:8:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 6 │ a::{ b } + 7 │ + > 8 │ a:::1 + │ ^ + 9 │ a:::NA + 10 │ a:::NULL + + i `:::` requires a name on the right. + + 6 │ a::{ b } + 7 │ + > 8 │ a:::1 + │ ^^^ + 9 │ a:::NA + 10 │ a:::NULL + +test.R:9:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 8 │ a:::1 + > 9 │ a:::NA + │ ^^ + 10 │ a:::NULL + 11 │ a:::TRUE + + i `:::` requires a name on the right. + + 8 │ a:::1 + > 9 │ a:::NA + │ ^^^ + 10 │ a:::NULL + 11 │ a:::TRUE + +test.R:10:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 8 │ a:::1 + 9 │ a:::NA + > 10 │ a:::NULL + │ ^^^^ + 11 │ a:::TRUE + 12 │ a:::(b) + + i `:::` requires a name on the right. + + 8 │ a:::1 + 9 │ a:::NA + > 10 │ a:::NULL + │ ^^^ + 11 │ a:::TRUE + 12 │ a:::(b) + +test.R:11:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 9 │ a:::NA + 10 │ a:::NULL + > 11 │ a:::TRUE + │ ^^^^ + 12 │ a:::(b) + 13 │ a:::{ b } + + i `:::` requires a name on the right. + + 9 │ a:::NA + 10 │ a:::NULL + > 11 │ a:::TRUE + │ ^^^ + 12 │ a:::(b) + 13 │ a:::{ b } + +test.R:12:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 10 │ a:::NULL + 11 │ a:::TRUE + > 12 │ a:::(b) + │ ^ + 13 │ a:::{ b } + 14 │ + + i `:::` requires a name on the right. + + 10 │ a:::NULL + 11 │ a:::TRUE + > 12 │ a:::(b) + │ ^^^ + 13 │ a:::{ b } + 14 │ + +test.R:13:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + 11 │ a:::TRUE + 12 │ a:::(b) + > 13 │ a:::{ b } + │ ^ + 14 │ + + i `:::` requires a name on the right. + + 11 │ a:::TRUE + 12 │ a:::(b) + > 13 │ a:::{ b } + │ ^^^ + 14 │ +``` + +## Chained `::` [error] + +```r +a::b::c +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RNamespaceExpression { + left: RBogusSelector { + items: [ + RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@0..1 "a" [] [], + }, + operator: COLON2@1..3 "::" [] [], + right: RIdentifier { + name_token: IDENT@3..4 "b" [] [], + }, + }, + ], + }, + operator: COLON2@4..6 "::" [] [], + right: RIdentifier { + name_token: IDENT@6..7 "c" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + > 1 │ a::b::c + │ ^^^^ + 2 │ + + i `::` requires a name on the left. + + > 1 │ a::b::c + │ ^^ + 2 │ +``` + +## Chained `:::` [error] + +```r +a:::b:::c +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RNamespaceExpression { + left: RBogusSelector { + items: [ + RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@0..1 "a" [] [], + }, + operator: COLON3@1..4 ":::" [] [], + right: RIdentifier { + name_token: IDENT@4..5 "b" [] [], + }, + }, + ], + }, + operator: COLON3@5..8 ":::" [] [], + right: RIdentifier { + name_token: IDENT@8..9 "c" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@9..10 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + > 1 │ a:::b:::c + │ ^^^^^ + 2 │ + + i `:::` requires a name on the left. + + > 1 │ a:::b:::c + │ ^^^ + 2 │ +``` + +## Call on the left of `::` [error] + +```r +a()::b +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RNamespaceExpression { + left: RBogusSelector { + items: [ + RCall { + function: RIdentifier { + name_token: IDENT@0..1 "a" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@1..2 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@2..3 ")" [] [], + }, + }, + ], + }, + operator: COLON2@3..5 "::" [] [], + right: RIdentifier { + name_token: IDENT@5..6 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@6..7 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + > 1 │ a()::b + │ ^^^ + 2 │ + + i `::` requires a name on the left. + + > 1 │ a()::b + │ ^^ + 2 │ +``` + +## Call on the left of `:::` [error] + +```r +a():::b +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RNamespaceExpression { + left: RBogusSelector { + items: [ + RCall { + function: RIdentifier { + name_token: IDENT@0..1 "a" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@1..2 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@2..3 ")" [] [], + }, + }, + ], + }, + operator: COLON3@3..6 ":::" [] [], + right: RIdentifier { + name_token: IDENT@6..7 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a name. + + > 1 │ a():::b + │ ^^^ + 2 │ + + i `:::` requires a name on the left. + + > 1 │ a():::b + │ ^^^ + 2 │ +``` + +## Namespace newline is significant only outside parens + +```r +f(stats:: + median) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..1 "f" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@1..2 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@2..7 "stats" [] [], + }, + operator: COLON2@7..9 "::" [] [], + right: RIdentifier { + name_token: IDENT@9..18 "median" [Newline("\n"), Whitespace(" ")] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@18..19 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@19..20 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/parenthesized.md b/crates/air_r_parser_tests/tests/corpus/expressions/parenthesized.md new file mode 100644 index 00000000..e5b7e2e2 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/parenthesized.md @@ -0,0 +1,47 @@ +## Parenthesized expression + +```r +(1) +((1)) +(1 + 1) +(fn(a, b)) +fn((a), (b)) +(function() { + body +}) +``` + +## Not a parenthesized expression 1 [error] + +```r +() +``` + +## Not a parenthesized expression 2 [error] + +```r +( + 1 + 2 +) +``` + +## Not a parenthesized expression 3 [error] + +```r +(1; 2) +``` + +## Parentheses with comments + +```r +({ + 1 + 1 +}) + +( # comment1 + # comment2 + 1 # comment3 + # comment4 +) # comment5 +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/parenthesized.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/parenthesized.snap.md new file mode 100644 index 00000000..907a4a64 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/parenthesized.snap.md @@ -0,0 +1,379 @@ + + +## Parenthesized expression + +```r +(1) +((1)) +(1 + 1) +(fn(a, b)) +fn((a), (b)) +(function() { + body +}) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@0..1 "(" [] [], + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@1..2 "1" [] [], + }, + r_paren_token: R_PAREN@2..3 ")" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@3..5 "(" [Newline("\n")] [], + body: RParenthesizedExpression { + l_paren_token: L_PAREN@5..6 "(" [] [], + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@6..7 "1" [] [], + }, + r_paren_token: R_PAREN@7..8 ")" [] [], + }, + r_paren_token: R_PAREN@8..9 ")" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@9..11 "(" [Newline("\n")] [], + body: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@11..13 "1" [] [Whitespace(" ")], + }, + operator: PLUS@13..15 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@15..16 "1" [] [], + }, + }, + r_paren_token: R_PAREN@16..17 ")" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@17..19 "(" [Newline("\n")] [], + body: RCall { + function: RIdentifier { + name_token: IDENT@19..21 "fn" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@21..22 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@22..23 "a" [] [], + }, + }, + COMMA@23..25 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@25..26 "b" [] [], + }, + }, + ], + r_paren_token: R_PAREN@26..27 ")" [] [], + }, + }, + r_paren_token: R_PAREN@27..28 ")" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@28..31 "fn" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@31..32 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RParenthesizedExpression { + l_paren_token: L_PAREN@32..33 "(" [] [], + body: RIdentifier { + name_token: IDENT@33..34 "a" [] [], + }, + r_paren_token: R_PAREN@34..35 ")" [] [], + }, + }, + COMMA@35..37 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RParenthesizedExpression { + l_paren_token: L_PAREN@37..38 "(" [] [], + body: RIdentifier { + name_token: IDENT@38..39 "b" [] [], + }, + r_paren_token: R_PAREN@39..40 ")" [] [], + }, + }, + ], + r_paren_token: R_PAREN@40..41 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@41..43 "(" [Newline("\n")] [], + body: RFunctionDefinition { + name: FUNCTION_KW@43..51 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@51..52 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@52..54 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@54..55 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@55..62 "body" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@62..64 "}" [Newline("\n")] [], + }, + }, + r_paren_token: R_PAREN@64..65 ")" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@65..66 "" [Newline("\n")] [], +} +``` + +## Not a parenthesized expression 1 [error] + +```r +() +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@0..1 "(" [] [], + body: missing (required), + r_paren_token: R_PAREN@1..2 ")" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@2..3 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ')'. + + > 1 │ () + │ ^ + 2 │ +``` + +## Not a parenthesized expression 2 [error] + +```r +( + 1 + 2 +) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@0..1 "(" [] [], + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@1..5 "1" [Newline("\n"), Whitespace(" ")] [], + }, + r_paren_token: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..9 "2" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_PAREN@9..11 ")" [Newline("\n")] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@11..12 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:3:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `)` but instead found `2` + + 1 │ ( + 2 │ 1 + > 3 │ 2 + │ ^ + 4 │ ) + 5 │ + + i Remove 2 + +test.R:4:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ')'. + + 2 │ 1 + 3 │ 2 + > 4 │ ) + │ ^ + 5 │ +``` + +## Not a parenthesized expression 3 [error] + +```r +(1; 2) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@0..1 "(" [] [], + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@1..2 "1" [] [], + }, + r_paren_token: missing (required), + }, + semicolon_token: SEMICOLON@2..4 ";" [] [Whitespace(" ")], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@4..5 "2" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_PAREN@5..6 ")" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@6..7 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `)` but instead found `;` + + > 1 │ (1; 2) + │ ^ + 2 │ + + i Remove ; + +test.R:1:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a newline or a `;` between two expressions. + + > 1 │ (1; 2) + │ ^ + 2 │ + + i The first expression ends here. + + > 1 │ (1; 2) + │ ^ + 2 │ +``` + +## Parentheses with comments + +```r +({ + 1 + 1 +}) + +( # comment1 + # comment2 + 1 # comment3 + # comment4 +) # comment5 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@0..1 "(" [] [], + body: RBracedStatements { + l_curly_token: L_CURLY@1..2 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@2..7 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: PLUS@7..9 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@9..10 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@10..12 "}" [Newline("\n")] [], + }, + r_paren_token: R_PAREN@12..13 ")" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@13..27 "(" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# comment1")], + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@27..55 "1" [Newline("\n"), Whitespace(" "), Comments("# comment2"), Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")], + }, + r_paren_token: R_PAREN@55..81 ")" [Newline("\n"), Whitespace(" "), Comments("# comment4"), Newline("\n")] [Whitespace(" "), Comments("# comment5")], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@81..82 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/precedence.md b/crates/air_r_parser_tests/tests/corpus/expressions/precedence.md new file mode 100644 index 00000000..080e7cc0 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/precedence.md @@ -0,0 +1,42 @@ +## Operator precedence + +```r +2+a*2 +2+a+2 +!a + !b +a <= 2 && 2 >= d +a[1] <- foo || bar +a && b(c) && d +val <- foo %>% bar(1) %>% baz() +``` + +## Precedence tests + +```r +A$"B"^NA +a::b$c +a$b?c +``` + +## Complex expressions + +```r +repeat if (1) TRUE else repeat 42 +if (TRUE) if (FALSE) 2 else NULL +a::b$c[[d]] <- e +TRUE ~ FALSE ~ NULL ? NA ? NaN +(if (TRUE) FALSE +else NA) +a = TRUE ? FALSE +TRUE <- FALSE = NA +TRUE <- FALSE ? NA +TRUE = FALSE ? NA +TRUE ? FALSE = NA +``` + +## Subset and subset2 precedence + +```r +a[[b[1]]] +a[b[[1]]] +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/precedence.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/precedence.snap.md new file mode 100644 index 00000000..42ec3e05 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/precedence.snap.md @@ -0,0 +1,626 @@ + + +## Operator precedence + +```r +2+a*2 +2+a+2 +!a + !b +a <= 2 && 2 >= d +a[1] <- foo || bar +a && b(c) && d +val <- foo %>% bar(1) %>% baz() +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..1 "2" [] [], + }, + operator: PLUS@1..2 "+" [] [], + right: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@2..3 "a" [] [], + }, + operator: MULTIPLY@3..4 "*" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@4..5 "2" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..7 "2" [Newline("\n")] [], + }, + operator: PLUS@7..8 "+" [] [], + right: RIdentifier { + name_token: IDENT@8..9 "a" [] [], + }, + }, + operator: PLUS@9..10 "+" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@10..11 "2" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: BANG@11..13 "!" [Newline("\n")] [], + argument: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@13..15 "a" [] [Whitespace(" ")], + }, + operator: PLUS@15..17 "+" [] [Whitespace(" ")], + right: RUnaryExpression { + operator: BANG@17..18 "!" [] [], + argument: RIdentifier { + name_token: IDENT@18..19 "b" [] [], + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@19..22 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: LESS_THAN_OR_EQUAL_TO@22..25 "<=" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@25..27 "2" [] [Whitespace(" ")], + }, + }, + operator: AND2@27..30 "&&" [] [Whitespace(" ")], + right: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@30..32 "2" [] [Whitespace(" ")], + }, + operator: GREATER_THAN_OR_EQUAL_TO@32..35 ">=" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@35..36 "d" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RSubset { + function: RIdentifier { + name_token: IDENT@36..38 "a" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@38..39 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@39..40 "1" [] [], + }, + }, + ], + r_brack_token: R_BRACK@40..42 "]" [] [Whitespace(" ")], + }, + }, + operator: ASSIGN@42..45 "<-" [] [Whitespace(" ")], + right: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@45..49 "foo" [] [Whitespace(" ")], + }, + operator: OR2@49..52 "||" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@52..55 "bar" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@55..58 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: AND2@58..61 "&&" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@61..62 "b" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@62..63 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@63..64 "c" [] [], + }, + }, + ], + r_paren_token: R_PAREN@64..66 ")" [] [Whitespace(" ")], + }, + }, + }, + operator: AND2@66..69 "&&" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@69..70 "d" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@70..75 "val" [Newline("\n")] [Whitespace(" ")], + }, + operator: ASSIGN@75..78 "<-" [] [Whitespace(" ")], + right: RBinaryExpression { + left: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@78..82 "foo" [] [Whitespace(" ")], + }, + operator: SPECIAL@82..86 "%>%" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@86..89 "bar" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@89..90 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@90..91 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@91..93 ")" [] [Whitespace(" ")], + }, + }, + }, + operator: SPECIAL@93..97 "%>%" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@97..100 "baz" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@100..101 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@101..102 ")" [] [], + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@102..103 "" [Newline("\n")] [], +} +``` + +## Precedence tests + +```r +A$"B"^NA +a::b$c +a$b?c +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RExtractExpression { + left: RIdentifier { + name_token: IDENT@0..1 "A" [] [], + }, + operator: DOLLAR@1..2 "$" [] [], + right: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@2..5 "\"B\"" [] [], + }, + }, + operator: EXPONENTIATE@5..6 "^" [] [], + right: RNaExpression { + value: NA_LOGICAL_KW@6..8 "NA" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RExtractExpression { + left: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@8..10 "a" [Newline("\n")] [], + }, + operator: COLON2@10..12 "::" [] [], + right: RIdentifier { + name_token: IDENT@12..13 "b" [] [], + }, + }, + operator: DOLLAR@13..14 "$" [] [], + right: RIdentifier { + name_token: IDENT@14..15 "c" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RExtractExpression { + left: RIdentifier { + name_token: IDENT@15..17 "a" [Newline("\n")] [], + }, + operator: DOLLAR@17..18 "$" [] [], + right: RIdentifier { + name_token: IDENT@18..19 "b" [] [], + }, + }, + operator: WAT@19..20 "?" [] [], + right: RIdentifier { + name_token: IDENT@20..21 "c" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@21..22 "" [Newline("\n")] [], +} +``` + +## Complex expressions + +```r +repeat if (1) TRUE else repeat 42 +if (TRUE) if (FALSE) 2 else NULL +a::b$c[[d]] <- e +TRUE ~ FALSE ~ NULL ? NA ? NaN +(if (TRUE) FALSE +else NA) +a = TRUE ? FALSE +TRUE <- FALSE = NA +TRUE <- FALSE ? NA +TRUE = FALSE ? NA +TRUE ? FALSE = NA +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RRepeatStatement { + repeat_token: REPEAT_KW@0..7 "repeat" [] [Whitespace(" ")], + body: RIfStatement { + if_token: IF_KW@7..10 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@10..11 "(" [] [], + condition: RDoubleValue { + value_token: R_DOUBLE_LITERAL@11..12 "1" [] [], + }, + r_paren_token: R_PAREN@12..14 ")" [] [Whitespace(" ")], + consequence: RTrueExpression { + true_token: TRUE_KW@14..19 "TRUE" [] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@19..24 "else" [] [Whitespace(" ")], + alternative: RRepeatStatement { + repeat_token: REPEAT_KW@24..31 "repeat" [] [Whitespace(" ")], + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@31..33 "42" [] [], + }, + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@33..37 "if" [Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@37..38 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@38..42 "TRUE" [] [], + }, + r_paren_token: R_PAREN@42..44 ")" [] [Whitespace(" ")], + consequence: RIfStatement { + if_token: IF_KW@44..47 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@47..48 "(" [] [], + condition: RFalseExpression { + false_token: FALSE_KW@48..53 "FALSE" [] [], + }, + r_paren_token: R_PAREN@53..55 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@55..57 "2" [] [Whitespace(" ")], + }, + else_clause: RElseClause { + else_token: ELSE_KW@57..62 "else" [] [Whitespace(" ")], + alternative: RNullExpression { + null_token: NULL_KW@62..66 "NULL" [] [], + }, + }, + }, + else_clause: missing (optional), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RSubset2 { + function: RExtractExpression { + left: RNamespaceExpression { + left: RIdentifier { + name_token: IDENT@66..68 "a" [Newline("\n")] [], + }, + operator: COLON2@68..70 "::" [] [], + right: RIdentifier { + name_token: IDENT@70..71 "b" [] [], + }, + }, + operator: DOLLAR@71..72 "$" [] [], + right: RIdentifier { + name_token: IDENT@72..73 "c" [] [], + }, + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@73..75 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@75..76 "d" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@76..79 "]]" [] [Whitespace(" ")], + }, + }, + operator: ASSIGN@79..82 "<-" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@82..83 "e" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RBinaryExpression { + left: RBinaryExpression { + left: RTrueExpression { + true_token: TRUE_KW@83..89 "TRUE" [Newline("\n")] [Whitespace(" ")], + }, + operator: TILDE@89..91 "~" [] [Whitespace(" ")], + right: RFalseExpression { + false_token: FALSE_KW@91..97 "FALSE" [] [Whitespace(" ")], + }, + }, + operator: TILDE@97..99 "~" [] [Whitespace(" ")], + right: RNullExpression { + null_token: NULL_KW@99..104 "NULL" [] [Whitespace(" ")], + }, + }, + operator: WAT@104..106 "?" [] [Whitespace(" ")], + right: RNaExpression { + value: NA_LOGICAL_KW@106..109 "NA" [] [Whitespace(" ")], + }, + }, + operator: WAT@109..111 "?" [] [Whitespace(" ")], + right: RNanExpression { + nan_token: NAN_KW@111..114 "NaN" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@114..116 "(" [Newline("\n")] [], + body: RIfStatement { + if_token: IF_KW@116..119 "if" [] [Whitespace(" ")], + l_paren_token: L_PAREN@119..120 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@120..124 "TRUE" [] [], + }, + r_paren_token: R_PAREN@124..126 ")" [] [Whitespace(" ")], + consequence: RFalseExpression { + false_token: FALSE_KW@126..131 "FALSE" [] [], + }, + else_clause: RElseClause { + else_token: ELSE_KW@131..137 "else" [Newline("\n")] [Whitespace(" ")], + alternative: RNaExpression { + value: NA_LOGICAL_KW@137..139 "NA" [] [], + }, + }, + }, + r_paren_token: R_PAREN@139..140 ")" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@140..143 "a" [Newline("\n")] [Whitespace(" ")], + }, + operator: EQUAL@143..145 "=" [] [Whitespace(" ")], + right: RTrueExpression { + true_token: TRUE_KW@145..150 "TRUE" [] [Whitespace(" ")], + }, + }, + operator: WAT@150..152 "?" [] [Whitespace(" ")], + right: RFalseExpression { + false_token: FALSE_KW@152..157 "FALSE" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RTrueExpression { + true_token: TRUE_KW@157..163 "TRUE" [Newline("\n")] [Whitespace(" ")], + }, + operator: ASSIGN@163..166 "<-" [] [Whitespace(" ")], + right: RBinaryExpression { + left: RFalseExpression { + false_token: FALSE_KW@166..172 "FALSE" [] [Whitespace(" ")], + }, + operator: EQUAL@172..174 "=" [] [Whitespace(" ")], + right: RNaExpression { + value: NA_LOGICAL_KW@174..176 "NA" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RTrueExpression { + true_token: TRUE_KW@176..182 "TRUE" [Newline("\n")] [Whitespace(" ")], + }, + operator: ASSIGN@182..185 "<-" [] [Whitespace(" ")], + right: RFalseExpression { + false_token: FALSE_KW@185..191 "FALSE" [] [Whitespace(" ")], + }, + }, + operator: WAT@191..193 "?" [] [Whitespace(" ")], + right: RNaExpression { + value: NA_LOGICAL_KW@193..195 "NA" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RTrueExpression { + true_token: TRUE_KW@195..201 "TRUE" [Newline("\n")] [Whitespace(" ")], + }, + operator: EQUAL@201..203 "=" [] [Whitespace(" ")], + right: RFalseExpression { + false_token: FALSE_KW@203..209 "FALSE" [] [Whitespace(" ")], + }, + }, + operator: WAT@209..211 "?" [] [Whitespace(" ")], + right: RNaExpression { + value: NA_LOGICAL_KW@211..213 "NA" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RTrueExpression { + true_token: TRUE_KW@213..219 "TRUE" [Newline("\n")] [Whitespace(" ")], + }, + operator: WAT@219..221 "?" [] [Whitespace(" ")], + right: RBinaryExpression { + left: RFalseExpression { + false_token: FALSE_KW@221..227 "FALSE" [] [Whitespace(" ")], + }, + operator: EQUAL@227..229 "=" [] [Whitespace(" ")], + right: RNaExpression { + value: NA_LOGICAL_KW@229..231 "NA" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@231..232 "" [Newline("\n")] [], +} +``` + +## Subset and subset2 precedence + +```r +a[[b[1]]] +a[b[[1]]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@0..1 "a" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@1..3 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset { + function: RIdentifier { + name_token: IDENT@3..4 "b" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@4..5 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..6 "1" [] [], + }, + }, + ], + r_brack_token: R_BRACK@6..7 "]" [] [], + }, + }, + }, + ], + r_brack2_token: R_BRACK2@7..9 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@9..11 "a" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@11..12 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset2 { + function: RIdentifier { + name_token: IDENT@12..13 "b" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@13..15 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@15..16 "1" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@16..18 "]]" [] [], + }, + }, + }, + ], + r_brack_token: R_BRACK@18..19 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@19..20 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/specials.md b/crates/air_r_parser_tests/tests/corpus/expressions/specials.md new file mode 100644 index 00000000..87fe3735 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/specials.md @@ -0,0 +1,46 @@ +## Specials + +```r +x %% y +x %/% y +x %+% y +x %>% y +x %>% 2 %>% z +x %some text% y +x %//% y +``` + +## A backslash inside a special is ordinary text + +R's `SpecialValue` excludes only the newline, so the whole operator is one token. + +```r +x %\% y +``` + +## An unterminated special [error] + +The `%%` closes first, leaving a `%` with no partner before the newline. + +```r +x %%% y +``` + +## Pipe + +```r +x |> print() + +x |> foo() %>% bar() |> baz() + +x |> foo() |> bar() + baz() + +x |> {function(x) x}() +``` + +## Pipe placeholder + +```r +foo |> bar(x, y = _) +foo |> bar() |> baz(data = _) +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/specials.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/specials.snap.md new file mode 100644 index 00000000..1713760b --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/specials.snap.md @@ -0,0 +1,463 @@ + + +## Specials + +```r +x %% y +x %/% y +x %+% y +x %>% y +x %>% 2 %>% z +x %some text% y +x %//% y +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "x" [] [Whitespace(" ")], + }, + operator: SPECIAL@2..5 "%%" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@5..6 "y" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@6..9 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: SPECIAL@9..13 "%/%" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@13..14 "y" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@14..17 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: SPECIAL@17..21 "%+%" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@21..22 "y" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@22..25 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: SPECIAL@25..29 "%>%" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@29..30 "y" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@30..33 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: SPECIAL@33..37 "%>%" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@37..39 "2" [] [Whitespace(" ")], + }, + }, + operator: SPECIAL@39..43 "%>%" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@43..44 "z" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@44..47 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: SPECIAL@47..59 "%some text%" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@59..60 "y" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@60..63 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: SPECIAL@63..68 "%//%" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@68..69 "y" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@69..70 "" [Newline("\n")] [], +} +``` + +## A backslash inside a special is ordinary text + +R's `SpecialValue` excludes only the newline, so the whole operator is one token. + +```r +x %\% y +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "x" [] [Whitespace(" ")], + }, + operator: SPECIAL@2..6 "%\\%" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@6..7 "y" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` + +## An unterminated special [error] + +The `%%` closes first, leaving a `%` with no partner before the newline. + +```r +x %%% y +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "x" [] [Whitespace(" ")], + }, + operator: SPECIAL@2..4 "%%" [] [], + right: missing (required), + }, + operator: SPECIAL@4..7 "% y" [] [], + right: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unterminated `%` operator. + + > 1 │ x %%% y + │ ^ + 2 │ + + i A `%...%` operator must be closed by a second `%` on the same line. + +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found the end of the file. + + 1 │ x %%% y + > 2 │ + │ +``` + +## Pipe + +```r +x |> print() + +x |> foo() %>% bar() |> baz() + +x |> foo() |> bar() + baz() + +x |> {function(x) x}() +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "x" [] [Whitespace(" ")], + }, + operator: PIPE@2..5 "|>" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@5..10 "print" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@10..11 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@11..12 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@12..16 "x" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + }, + operator: PIPE@16..19 "|>" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@19..22 "foo" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@22..23 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@23..25 ")" [] [Whitespace(" ")], + }, + }, + }, + operator: SPECIAL@25..29 "%>%" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@29..32 "bar" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@32..33 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@33..35 ")" [] [Whitespace(" ")], + }, + }, + }, + operator: PIPE@35..38 "|>" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@38..41 "baz" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@41..42 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@42..43 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@43..47 "x" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + }, + operator: PIPE@47..50 "|>" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@50..53 "foo" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@53..54 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@54..56 ")" [] [Whitespace(" ")], + }, + }, + }, + operator: PIPE@56..59 "|>" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@59..62 "bar" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@62..63 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@63..65 ")" [] [Whitespace(" ")], + }, + }, + }, + operator: PLUS@65..67 "+" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@67..70 "baz" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@70..71 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@71..72 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@72..76 "x" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + }, + operator: PIPE@76..79 "|>" [] [Whitespace(" ")], + right: RCall { + function: RBracedStatements { + l_curly_token: L_CURLY@79..80 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@80..88 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@88..89 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@89..90 "x" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@90..92 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@92..93 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@93..94 "}" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@94..95 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@95..96 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@96..97 "" [Newline("\n")] [], +} +``` + +## Pipe placeholder + +```r +foo |> bar(x, y = _) +foo |> bar() |> baz(data = _) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..4 "foo" [] [Whitespace(" ")], + }, + operator: PIPE@4..7 "|>" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@7..10 "bar" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@10..11 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@11..12 "x" [] [], + }, + }, + COMMA@12..14 "," [] [Whitespace(" ")], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@14..16 "y" [] [Whitespace(" ")], + }, + eq_token: EQUAL@16..18 "=" [] [Whitespace(" ")], + }, + value: RIdentifier { + name_token: IDENT@18..19 "_" [] [], + }, + }, + ], + r_paren_token: R_PAREN@19..20 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@20..25 "foo" [Newline("\n")] [Whitespace(" ")], + }, + operator: PIPE@25..28 "|>" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@28..31 "bar" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@31..32 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@32..34 ")" [] [Whitespace(" ")], + }, + }, + }, + operator: PIPE@34..37 "|>" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@37..40 "baz" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@40..41 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@41..46 "data" [] [Whitespace(" ")], + }, + eq_token: EQUAL@46..48 "=" [] [Whitespace(" ")], + }, + value: RIdentifier { + name_token: IDENT@48..49 "_" [] [], + }, + }, + ], + r_paren_token: R_PAREN@49..50 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@50..51 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser/tests/snapshots/ok/subset.R b/crates/air_r_parser_tests/tests/corpus/expressions/subset.md similarity index 58% rename from crates/air_r_parser/tests/snapshots/ok/subset.R rename to crates/air_r_parser_tests/tests/corpus/expressions/subset.md index 1ce21345..4361eb0e 100644 --- a/crates/air_r_parser/tests/snapshots/ok/subset.R +++ b/crates/air_r_parser_tests/tests/corpus/expressions/subset.md @@ -1,3 +1,34 @@ +## Subset + +```r +foo[] +foo[,] +foo[,,] +foo[x,] +foo[x,,] +foo[,x] +foo[,,x] +foo[x,,y] +foo[,,x,,y,,] + +foo[bar] +foo[1, 2] +foo[x=1,,y=3,4] +``` + +## Nested brackets + +```r +x[a[b]] +x[[a[b]]] +x[a[[b]]] +x[[a[[b]]]] +x[y[z[[a]]]] +``` + +## Subset arguments + +```r fn fn[a] fn[a, b] @@ -39,3 +70,4 @@ fn[ ] {expr}[a = 1] +``` diff --git a/crates/air_r_parser/tests/snapshots/ok/subset.R.snap b/crates/air_r_parser_tests/tests/corpus/expressions/subset.snap.md similarity index 50% rename from crates/air_r_parser/tests/snapshots/ok/subset.R.snap rename to crates/air_r_parser_tests/tests/corpus/expressions/subset.snap.md index 8829b926..a6a92214 100644 --- a/crates/air_r_parser/tests/snapshots/ok/subset.R.snap +++ b/crates/air_r_parser_tests/tests/corpus/expressions/subset.snap.md @@ -1,10 +1,592 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input + -```R +## Subset + +```r +foo[] +foo[,] +foo[,,] +foo[x,] +foo[x,,] +foo[,x] +foo[,,x] +foo[x,,y] +foo[,,x,,y,,] + +foo[bar] +foo[1, 2] +foo[x=1,,y=3,4] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@3..4 "[" [] [], + items: RArgumentList [], + r_brack_token: R_BRACK@4..5 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@5..9 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@9..10 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@10..11 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_brack_token: R_BRACK@11..12 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@12..16 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@16..17 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@17..18 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@18..19 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_brack_token: R_BRACK@19..20 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@20..24 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@24..25 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@25..26 "x" [] [], + }, + }, + COMMA@26..27 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_brack_token: R_BRACK@27..28 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@28..32 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@32..33 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@33..34 "x" [] [], + }, + }, + COMMA@34..35 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@35..36 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_brack_token: R_BRACK@36..37 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@37..41 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@41..42 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@42..43 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@43..44 "x" [] [], + }, + }, + ], + r_brack_token: R_BRACK@44..45 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@45..49 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@49..50 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@50..51 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@51..52 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@52..53 "x" [] [], + }, + }, + ], + r_brack_token: R_BRACK@53..54 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@54..58 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@58..59 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@59..60 "x" [] [], + }, + }, + COMMA@60..61 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@61..62 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@62..63 "y" [] [], + }, + }, + ], + r_brack_token: R_BRACK@63..64 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@64..68 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@68..69 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@69..70 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@70..71 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@71..72 "x" [] [], + }, + }, + COMMA@72..73 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@73..74 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@74..75 "y" [] [], + }, + }, + COMMA@75..76 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@76..77 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_brack_token: R_BRACK@77..78 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@78..83 "foo" [Newline("\n"), Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@83..84 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@84..87 "bar" [] [], + }, + }, + ], + r_brack_token: R_BRACK@87..88 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@88..92 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@92..93 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@93..94 "1" [] [], + }, + }, + COMMA@94..96 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@96..97 "2" [] [], + }, + }, + ], + r_brack_token: R_BRACK@97..98 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@98..102 "foo" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@102..103 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@103..104 "x" [] [], + }, + eq_token: EQUAL@104..105 "=" [] [], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@105..106 "1" [] [], + }, + }, + COMMA@106..107 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@107..108 "," [] [], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@108..109 "y" [] [], + }, + eq_token: EQUAL@109..110 "=" [] [], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@110..111 "3" [] [], + }, + }, + COMMA@111..112 "," [] [], + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@112..113 "4" [] [], + }, + }, + ], + r_brack_token: R_BRACK@113..114 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@114..115 "" [Newline("\n")] [], +} +``` + +## Nested brackets + +```r +x[a[b]] +x[[a[b]]] +x[a[[b]]] +x[[a[[b]]]] +x[y[z[[a]]]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@1..2 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset { + function: RIdentifier { + name_token: IDENT@2..3 "a" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@3..4 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@4..5 "b" [] [], + }, + }, + ], + r_brack_token: R_BRACK@5..6 "]" [] [], + }, + }, + }, + ], + r_brack_token: R_BRACK@6..7 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@7..9 "x" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@9..11 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset { + function: RIdentifier { + name_token: IDENT@11..12 "a" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@12..13 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@13..14 "b" [] [], + }, + }, + ], + r_brack_token: R_BRACK@14..15 "]" [] [], + }, + }, + }, + ], + r_brack2_token: R_BRACK2@15..17 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@17..19 "x" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@19..20 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset2 { + function: RIdentifier { + name_token: IDENT@20..21 "a" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@21..23 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@23..24 "b" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@24..26 "]]" [] [], + }, + }, + }, + ], + r_brack_token: R_BRACK@26..27 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@27..29 "x" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@29..31 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset2 { + function: RIdentifier { + name_token: IDENT@31..32 "a" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@32..34 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@34..35 "b" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@35..37 "]]" [] [], + }, + }, + }, + ], + r_brack2_token: R_BRACK2@37..39 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@39..41 "x" [Newline("\n")] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@41..42 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset { + function: RIdentifier { + name_token: IDENT@42..43 "y" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@43..44 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset2 { + function: RIdentifier { + name_token: IDENT@44..45 "z" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@45..47 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@47..48 "a" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@48..50 "]]" [] [], + }, + }, + }, + ], + r_brack_token: R_BRACK@50..51 "]" [] [], + }, + }, + }, + ], + r_brack_token: R_BRACK@51..52 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@52..53 "" [Newline("\n")] [], +} +``` + +## Subset arguments + +```r fn fn[a] fn[a, b] @@ -46,13 +628,9 @@ fn[ ] {expr}[a = 1] - ``` - -## AST - -``` +```cst RRoot { bom_token: missing (optional), statements: RStatementList [ @@ -618,387 +1196,3 @@ RRoot { eof_token: EOF@343..344 "" [Newline("\n")] [], } ``` - -## CST - -``` -0: R_ROOT@0..344 - 0: (empty) - 1: R_STATEMENT_LIST@0..343 - 0: R_EXPRESSION_STATEMENT@0..2 - 0: R_IDENTIFIER@0..2 - 0: IDENT@0..2 "fn" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@2..8 - 0: R_SUBSET@2..8 - 0: R_IDENTIFIER@2..5 - 0: IDENT@2..5 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@5..8 - 0: L_BRACK@5..6 "[" [] [] - 1: R_ARGUMENT_LIST@6..7 - 0: R_ARGUMENT@6..7 - 0: (empty) - 1: R_IDENTIFIER@6..7 - 0: IDENT@6..7 "a" [] [] - 2: R_BRACK@7..8 "]" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@8..17 - 0: R_SUBSET@8..17 - 0: R_IDENTIFIER@8..11 - 0: IDENT@8..11 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@11..17 - 0: L_BRACK@11..12 "[" [] [] - 1: R_ARGUMENT_LIST@12..16 - 0: R_ARGUMENT@12..13 - 0: (empty) - 1: R_IDENTIFIER@12..13 - 0: IDENT@12..13 "a" [] [] - 1: COMMA@13..15 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@15..16 - 0: (empty) - 1: R_IDENTIFIER@15..16 - 0: IDENT@15..16 "b" [] [] - 2: R_BRACK@16..17 "]" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@17..25 - 0: R_SUBSET@17..25 - 0: R_IDENTIFIER@17..20 - 0: IDENT@17..20 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@20..25 - 0: L_BRACK@20..21 "[" [] [] - 1: R_ARGUMENT_LIST@21..24 - 0: R_ARGUMENT@21..24 - 0: (empty) - 1: R_DOTS@21..24 - 0: DOTS@21..24 "..." [] [] - 2: R_BRACK@24..25 "]" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@25..39 - 0: R_SUBSET@25..39 - 0: R_IDENTIFIER@25..28 - 0: IDENT@25..28 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@28..39 - 0: L_BRACK@28..29 "[" [] [] - 1: R_ARGUMENT_LIST@29..38 - 0: R_ARGUMENT@29..30 - 0: (empty) - 1: R_IDENTIFIER@29..30 - 0: IDENT@29..30 "a" [] [] - 1: COMMA@30..32 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@32..35 - 0: (empty) - 1: R_DOTS@32..35 - 0: DOTS@32..35 "..." [] [] - 3: COMMA@35..37 "," [] [Whitespace(" ")] - 4: R_ARGUMENT@37..38 - 0: (empty) - 1: R_IDENTIFIER@37..38 - 0: IDENT@37..38 "b" [] [] - 2: R_BRACK@38..39 "]" [] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@39..50 - 0: R_SUBSET@39..50 - 0: R_IDENTIFIER@39..43 - 0: IDENT@39..43 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@43..50 - 0: L_BRACK@43..44 "[" [] [] - 1: R_ARGUMENT_LIST@44..49 - 0: R_ARGUMENT@44..49 - 0: R_ARGUMENT_NAME_CLAUSE@44..48 - 0: R_IDENTIFIER@44..46 - 0: IDENT@44..46 "a" [] [Whitespace(" ")] - 1: EQUAL@46..48 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@48..49 - 0: R_DOUBLE_LITERAL@48..49 "1" [] [] - 2: R_BRACK@49..50 "]" [] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@50..59 - 0: R_SUBSET@50..59 - 0: R_IDENTIFIER@50..53 - 0: IDENT@50..53 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@53..59 - 0: L_BRACK@53..54 "[" [] [] - 1: R_ARGUMENT_LIST@54..58 - 0: R_ARGUMENT@54..58 - 0: R_ARGUMENT_NAME_CLAUSE@54..58 - 0: R_IDENTIFIER@54..56 - 0: IDENT@54..56 "a" [] [Whitespace(" ")] - 1: EQUAL@56..58 "=" [] [Whitespace(" ")] - 1: (empty) - 2: R_BRACK@58..59 "]" [] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@59..77 - 0: R_SUBSET@59..77 - 0: R_IDENTIFIER@59..63 - 0: IDENT@59..63 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@63..77 - 0: L_BRACK@63..64 "[" [] [] - 1: R_ARGUMENT_LIST@64..76 - 0: R_ARGUMENT@64..69 - 0: R_ARGUMENT_NAME_CLAUSE@64..68 - 0: R_IDENTIFIER@64..66 - 0: IDENT@64..66 "a" [] [Whitespace(" ")] - 1: EQUAL@66..68 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@68..69 - 0: R_DOUBLE_LITERAL@68..69 "1" [] [] - 1: COMMA@69..71 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@71..76 - 0: R_ARGUMENT_NAME_CLAUSE@71..75 - 0: R_IDENTIFIER@71..73 - 0: IDENT@71..73 "b" [] [Whitespace(" ")] - 1: EQUAL@73..75 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@75..76 - 0: R_DOUBLE_LITERAL@75..76 "2" [] [] - 2: R_BRACK@76..77 "]" [] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@77..93 - 0: R_SUBSET@77..93 - 0: R_IDENTIFIER@77..80 - 0: IDENT@77..80 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@80..93 - 0: L_BRACK@80..81 "[" [] [] - 1: R_ARGUMENT_LIST@81..92 - 0: R_ARGUMENT@81..85 - 0: R_ARGUMENT_NAME_CLAUSE@81..85 - 0: R_IDENTIFIER@81..83 - 0: IDENT@81..83 "a" [] [Whitespace(" ")] - 1: EQUAL@83..85 "=" [] [Whitespace(" ")] - 1: (empty) - 1: COMMA@85..87 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@87..92 - 0: R_ARGUMENT_NAME_CLAUSE@87..91 - 0: R_IDENTIFIER@87..89 - 0: IDENT@87..89 "b" [] [Whitespace(" ")] - 1: EQUAL@89..91 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@91..92 - 0: R_DOUBLE_LITERAL@91..92 "2" [] [] - 2: R_BRACK@92..93 "]" [] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@93..106 - 0: R_SUBSET@93..106 - 0: R_IDENTIFIER@93..97 - 0: IDENT@93..97 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@97..106 - 0: L_BRACK@97..98 "[" [] [] - 1: R_ARGUMENT_LIST@98..105 - 0: R_ARGUMENT@98..105 - 0: R_ARGUMENT_NAME_CLAUSE@98..104 - 0: R_DOTS@98..102 - 0: DOTS@98..102 "..." [] [Whitespace(" ")] - 1: EQUAL@102..104 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@104..105 - 0: R_DOUBLE_LITERAL@104..105 "1" [] [] - 2: R_BRACK@105..106 "]" [] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@106..116 - 0: R_SUBSET@106..116 - 0: R_IDENTIFIER@106..109 - 0: IDENT@106..109 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@109..116 - 0: L_BRACK@109..110 "[" [] [] - 1: R_ARGUMENT_LIST@110..115 - 0: R_ARGUMENT@110..115 - 0: R_ARGUMENT_NAME_CLAUSE@110..115 - 0: R_DOTS@110..114 - 0: DOTS@110..114 "..." [] [Whitespace(" ")] - 1: EQUAL@114..115 "=" [] [] - 1: (empty) - 2: R_BRACK@115..116 "]" [] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@116..131 - 0: R_SUBSET@116..131 - 0: R_IDENTIFIER@116..120 - 0: IDENT@116..120 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@120..131 - 0: L_BRACK@120..121 "[" [] [] - 1: R_ARGUMENT_LIST@121..130 - 0: R_ARGUMENT@121..130 - 0: R_ARGUMENT_NAME_CLAUSE@121..129 - 0: R_STRING_VALUE@121..127 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@121..127 "\"arg\"" [] [Whitespace(" ")] - 1: EQUAL@127..129 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@129..130 - 0: R_DOUBLE_LITERAL@129..130 "1" [] [] - 2: R_BRACK@130..131 "]" [] [] - 1: (empty) - 12: R_EXPRESSION_STATEMENT@131..143 - 0: R_SUBSET@131..143 - 0: R_IDENTIFIER@131..134 - 0: IDENT@131..134 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@134..143 - 0: L_BRACK@134..135 "[" [] [] - 1: R_ARGUMENT_LIST@135..142 - 0: R_ARGUMENT@135..142 - 0: R_ARGUMENT_NAME_CLAUSE@135..142 - 0: R_STRING_VALUE@135..141 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@135..141 "\"arg\"" [] [Whitespace(" ")] - 1: EQUAL@141..142 "=" [] [] - 1: (empty) - 2: R_BRACK@142..143 "]" [] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@143..164 - 0: R_SUBSET@143..164 - 0: R_IDENTIFIER@143..161 - 0: IDENT@143..161 "fn" [Newline("\n"), Newline("\n"), Comments("# Comma tests"), Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@161..164 - 0: L_BRACK@161..162 "[" [] [] - 1: R_ARGUMENT_LIST@162..163 - 0: R_ARGUMENT@162..162 - 0: (empty) - 1: (empty) - 1: COMMA@162..163 "," [] [] - 2: R_ARGUMENT@163..163 - 0: (empty) - 1: (empty) - 2: R_BRACK@163..164 "]" [] [] - 1: (empty) - 14: R_EXPRESSION_STATEMENT@164..172 - 0: R_SUBSET@164..172 - 0: R_IDENTIFIER@164..167 - 0: IDENT@164..167 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@167..172 - 0: L_BRACK@167..168 "[" [] [] - 1: R_ARGUMENT_LIST@168..171 - 0: R_ARGUMENT@168..168 - 0: (empty) - 1: (empty) - 1: COMMA@168..169 "," [] [] - 2: R_ARGUMENT@169..169 - 0: (empty) - 1: (empty) - 3: COMMA@169..170 "," [] [] - 4: R_ARGUMENT@170..170 - 0: (empty) - 1: (empty) - 5: COMMA@170..171 "," [] [] - 6: R_ARGUMENT@171..171 - 0: (empty) - 1: (empty) - 2: R_BRACK@171..172 "]" [] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@172..185 - 0: R_SUBSET@172..185 - 0: R_IDENTIFIER@172..175 - 0: IDENT@172..175 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@175..185 - 0: L_BRACK@175..176 "[" [] [] - 1: R_ARGUMENT_LIST@176..184 - 0: R_ARGUMENT@176..176 - 0: (empty) - 1: (empty) - 1: COMMA@176..177 "," [] [] - 2: R_ARGUMENT@177..177 - 0: (empty) - 1: (empty) - 3: COMMA@177..178 "," [] [] - 4: R_ARGUMENT@178..179 - 0: (empty) - 1: R_IDENTIFIER@178..179 - 0: IDENT@178..179 "a" [] [] - 5: COMMA@179..180 "," [] [] - 6: R_ARGUMENT@180..180 - 0: (empty) - 1: (empty) - 7: COMMA@180..181 "," [] [] - 8: R_ARGUMENT@181..182 - 0: (empty) - 1: R_IDENTIFIER@181..182 - 0: IDENT@181..182 "b" [] [] - 9: COMMA@182..183 "," [] [] - 10: R_ARGUMENT@183..183 - 0: (empty) - 1: (empty) - 11: COMMA@183..184 "," [] [] - 12: R_ARGUMENT@184..184 - 0: (empty) - 1: (empty) - 2: R_BRACK@184..185 "]" [] [] - 1: (empty) - 16: R_EXPRESSION_STATEMENT@185..220 - 0: R_SUBSET@185..220 - 0: R_IDENTIFIER@185..205 - 0: IDENT@185..205 "fn" [Newline("\n"), Newline("\n"), Comments("# Comment tests"), Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@205..220 - 0: L_BRACK@205..206 "[" [] [] - 1: R_ARGUMENT_LIST@206..206 - 2: R_BRACK@206..220 "]" [Newline("\n"), Whitespace(" "), Comments("# comment"), Newline("\n")] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@220..240 - 0: R_SUBSET@220..240 - 0: R_IDENTIFIER@220..223 - 0: IDENT@220..223 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@223..240 - 0: L_BRACK@223..224 "[" [] [] - 1: R_ARGUMENT_LIST@224..239 - 0: R_ARGUMENT@224..224 - 0: (empty) - 1: (empty) - 1: COMMA@224..225 "," [] [] - 2: R_ARGUMENT@225..225 - 0: (empty) - 1: (empty) - 3: COMMA@225..239 "," [Newline("\n"), Whitespace(" "), Comments("# comment"), Newline("\n")] [] - 4: R_ARGUMENT@239..239 - 0: (empty) - 1: (empty) - 2: R_BRACK@239..240 "]" [] [] - 1: (empty) - 18: R_EXPRESSION_STATEMENT@240..277 - 0: R_SUBSET@240..277 - 0: R_IDENTIFIER@240..243 - 0: IDENT@240..243 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@243..277 - 0: L_BRACK@243..244 "[" [] [] - 1: R_ARGUMENT_LIST@244..275 - 0: R_ARGUMENT@244..248 - 0: (empty) - 1: R_IDENTIFIER@244..248 - 0: IDENT@244..248 "a" [Newline("\n"), Whitespace(" ")] [] - 1: COMMA@248..260 "," [] [Whitespace(" "), Comments("# comment1")] - 2: R_ARGUMENT@260..275 - 0: (empty) - 1: R_IDENTIFIER@260..275 - 0: IDENT@260..275 "b" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")] - 2: R_BRACK@275..277 "]" [Newline("\n")] [] - 1: (empty) - 19: R_EXPRESSION_STATEMENT@277..328 - 0: R_SUBSET@277..328 - 0: R_IDENTIFIER@277..280 - 0: IDENT@277..280 "fn" [Newline("\n")] [] - 1: R_SUBSET_ARGUMENTS@280..328 - 0: L_BRACK@280..281 "[" [] [] - 1: R_ARGUMENT_LIST@281..326 - 0: R_ARGUMENT@281..326 - 0: R_ARGUMENT_NAME_CLAUSE@281..311 - 0: R_IDENTIFIER@281..296 - 0: IDENT@281..296 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment1")] - 1: EQUAL@296..311 "=" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")] - 1: R_DOUBLE_VALUE@311..326 - 0: R_DOUBLE_LITERAL@311..326 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")] - 2: R_BRACK@326..328 "]" [Newline("\n")] [] - 1: (empty) - 20: R_EXPRESSION_STATEMENT@328..343 - 0: R_SUBSET@328..343 - 0: R_BRACED_STATEMENTS@328..336 - 0: L_CURLY@328..331 "{" [Newline("\n"), Newline("\n")] [] - 1: R_STATEMENT_LIST@331..335 - 0: R_EXPRESSION_STATEMENT@331..335 - 0: R_IDENTIFIER@331..335 - 0: IDENT@331..335 "expr" [] [] - 1: (empty) - 2: R_CURLY@335..336 "}" [] [] - 1: R_SUBSET_ARGUMENTS@336..343 - 0: L_BRACK@336..337 "[" [] [] - 1: R_ARGUMENT_LIST@337..342 - 0: R_ARGUMENT@337..342 - 0: R_ARGUMENT_NAME_CLAUSE@337..341 - 0: R_IDENTIFIER@337..339 - 0: IDENT@337..339 "a" [] [Whitespace(" ")] - 1: EQUAL@339..341 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@341..342 - 0: R_DOUBLE_LITERAL@341..342 "1" [] [] - 2: R_BRACK@342..343 "]" [] [] - 1: (empty) - 2: EOF@343..344 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/subset2.md b/crates/air_r_parser_tests/tests/corpus/expressions/subset2.md new file mode 100644 index 00000000..66d6a55b --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/subset2.md @@ -0,0 +1,96 @@ +## Subset2 + +```r +foo[[]] +foo[[,]] +foo[[,,]] +foo[[x,]] +foo[[x,,]] +foo[[,x]] +foo[[,,x]] +foo[[x,,y]] +foo[[,,x,,y,,]] + +foo[[x]] +foo[[x, y]] +foo[[1, 2]] +foo[[x=1,,y=3,4]] +``` + +## Unsupported subset2 with `]]` over multiple lines (r-lib/tree-sitter-r#177) [error] + +```r +x[["a"] +] +``` + +## Empty subset2 joins its `]]` + +Nothing stands between the `[[` and the `]]`, so the join has to happen on the +bump of the `[[` itself. + +```r +x[[]] +``` + +## Side by side arguments still join the `]]` [error] + +```r +x[[a b]] +``` + +## Lookahead for `=` survives the `]]` join + +Looking one token past an argument name buffers whatever follows it, which for +`x[[a]]` is the very `]` the next bump has to re-lex into `]]`. + +```r +x[[a = 1]] +x[[a = x[[b = 1]]]] +``` + +## Subset2 arguments + +```r +fn +fn[[a]] +fn[[a, b]] +fn[[...]] +fn[[a, ..., b]] + +fn[[a = 1]] +fn[[a = ]] + +fn[[a = 1, b = 2]] +fn[[a = , b = 2]] + +fn[[... = 1]] +fn[[... =]] + +fn[["arg" = 1]] +fn[["arg" =]] + +# Comma tests +fn[[,]] +fn[[,,,]] +fn[[,,a,,b,,]] + +# Comment tests +fn[[ + # comment +]] +fn[[, + # comment +,]] +fn[[ + a, # comment1 + b # comment2 +]] +fn[[ + a # comment1 + = # comment2 + 1 # comment3 +]] + +{expr}[[a = 1]] +``` diff --git a/crates/air_r_parser/tests/snapshots/ok/subset2.R.snap b/crates/air_r_parser_tests/tests/corpus/expressions/subset2.snap.md similarity index 50% rename from crates/air_r_parser/tests/snapshots/ok/subset2.R.snap rename to crates/air_r_parser_tests/tests/corpus/expressions/subset2.snap.md index e68e7980..75a4bc35 100644 --- a/crates/air_r_parser/tests/snapshots/ok/subset2.R.snap +++ b/crates/air_r_parser_tests/tests/corpus/expressions/subset2.snap.md @@ -1,10 +1,664 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input + -```R +## Subset2 + +```r +foo[[]] +foo[[,]] +foo[[,,]] +foo[[x,]] +foo[[x,,]] +foo[[,x]] +foo[[,,x]] +foo[[x,,y]] +foo[[,,x,,y,,]] + +foo[[x]] +foo[[x, y]] +foo[[1, 2]] +foo[[x=1,,y=3,4]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@3..5 "[[" [] [], + items: RArgumentList [], + r_brack2_token: R_BRACK2@5..7 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@7..11 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@11..13 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@13..14 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_brack2_token: R_BRACK2@14..16 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@16..20 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@20..22 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@22..23 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@23..24 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_brack2_token: R_BRACK2@24..26 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@26..30 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@30..32 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@32..33 "x" [] [], + }, + }, + COMMA@33..34 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_brack2_token: R_BRACK2@34..36 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@36..40 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@40..42 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@42..43 "x" [] [], + }, + }, + COMMA@43..44 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@44..45 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_brack2_token: R_BRACK2@45..47 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@47..51 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@51..53 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@53..54 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@54..55 "x" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@55..57 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@57..61 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@61..63 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@63..64 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@64..65 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@65..66 "x" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@66..68 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@68..72 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@72..74 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@74..75 "x" [] [], + }, + }, + COMMA@75..76 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@76..77 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@77..78 "y" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@78..80 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@80..84 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@84..86 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@86..87 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@87..88 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@88..89 "x" [] [], + }, + }, + COMMA@89..90 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@90..91 "," [] [], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@91..92 "y" [] [], + }, + }, + COMMA@92..93 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@93..94 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + ], + r_brack2_token: R_BRACK2@94..96 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@96..101 "foo" [Newline("\n"), Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@101..103 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@103..104 "x" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@104..106 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@106..110 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@110..112 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@112..113 "x" [] [], + }, + }, + COMMA@113..115 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@115..116 "y" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@116..118 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@118..122 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@122..124 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@124..125 "1" [] [], + }, + }, + COMMA@125..127 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@127..128 "2" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@128..130 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@130..134 "foo" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@134..136 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@136..137 "x" [] [], + }, + eq_token: EQUAL@137..138 "=" [] [], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@138..139 "1" [] [], + }, + }, + COMMA@139..140 "," [] [], + RArgument { + name_clause: missing (optional), + value: missing (optional), + }, + COMMA@140..141 "," [] [], + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@141..142 "y" [] [], + }, + eq_token: EQUAL@142..143 "=" [] [], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@143..144 "3" [] [], + }, + }, + COMMA@144..145 "," [] [], + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@145..146 "4" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@146..148 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@148..149 "" [Newline("\n")] [], +} +``` + +## Unsupported subset2 with `]]` over multiple lines (r-lib/tree-sitter-r#177) [error] + +```r +x[["a"] +] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@1..3 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@3..6 "\"a\"" [] [], + }, + }, + ], + r_brack2_token: missing (required), + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_BRACK@6..7 "]" [] [], + ], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_BRACK@7..9 "]" [Newline("\n")] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@9..10 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `]]` but instead found `]` + + > 1 │ x[["a"] + │ ^ + 2 │ ] + 3 │ + + i Remove ] + +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ']'. + + 1 │ x[["a"] + > 2 │ ] + │ ^ + 3 │ +``` + +## Empty subset2 joins its `]]` + +Nothing stands between the `[[` and the `]]`, so the join has to happen on the +bump of the `[[` itself. + +```r +x[[]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@1..3 "[[" [] [], + items: RArgumentList [], + r_brack2_token: R_BRACK2@3..5 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@5..6 "" [Newline("\n")] [], +} +``` + +## Side by side arguments still join the `]]` [error] + +```r +x[[a b]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@1..3 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@3..5 "a" [] [Whitespace(" ")], + }, + }, + missing separator, + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@5..6 "b" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@6..8 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@8..9 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `b` + + > 1 │ x[[a b]] + │ ^ + 2 │ + + i Remove b +``` + +## Lookahead for `=` survives the `]]` join + +Looking one token past an argument name buffers whatever follows it, which for +`x[[a]]` is the very `]` the next bump has to re-lex into `]]`. + +```r +x[[a = 1]] +x[[a = x[[b = 1]]]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@1..3 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@3..5 "a" [] [Whitespace(" ")], + }, + eq_token: EQUAL@5..7 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@7..8 "1" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@8..10 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@10..12 "x" [Newline("\n")] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@12..14 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@14..16 "a" [] [Whitespace(" ")], + }, + eq_token: EQUAL@16..18 "=" [] [Whitespace(" ")], + }, + value: RSubset2 { + function: RIdentifier { + name_token: IDENT@18..19 "x" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@19..21 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@21..23 "b" [] [Whitespace(" ")], + }, + eq_token: EQUAL@23..25 "=" [] [Whitespace(" ")], + }, + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@25..26 "1" [] [], + }, + }, + ], + r_brack2_token: R_BRACK2@26..28 "]]" [] [], + }, + }, + }, + ], + r_brack2_token: R_BRACK2@28..30 "]]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@30..31 "" [Newline("\n")] [], +} +``` + +## Subset2 arguments + +```r fn fn[[a]] fn[[a, b]] @@ -46,13 +700,9 @@ fn[[ ]] {expr}[[a = 1]] - ``` - -## AST - -``` +```cst RRoot { bom_token: missing (optional), statements: RStatementList [ @@ -618,387 +1268,3 @@ RRoot { eof_token: EOF@383..384 "" [Newline("\n")] [], } ``` - -## CST - -``` -0: R_ROOT@0..384 - 0: (empty) - 1: R_STATEMENT_LIST@0..383 - 0: R_EXPRESSION_STATEMENT@0..2 - 0: R_IDENTIFIER@0..2 - 0: IDENT@0..2 "fn" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@2..10 - 0: R_SUBSET2@2..10 - 0: R_IDENTIFIER@2..5 - 0: IDENT@2..5 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@5..10 - 0: L_BRACK2@5..7 "[[" [] [] - 1: R_ARGUMENT_LIST@7..8 - 0: R_ARGUMENT@7..8 - 0: (empty) - 1: R_IDENTIFIER@7..8 - 0: IDENT@7..8 "a" [] [] - 2: R_BRACK2@8..10 "]]" [] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@10..21 - 0: R_SUBSET2@10..21 - 0: R_IDENTIFIER@10..13 - 0: IDENT@10..13 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@13..21 - 0: L_BRACK2@13..15 "[[" [] [] - 1: R_ARGUMENT_LIST@15..19 - 0: R_ARGUMENT@15..16 - 0: (empty) - 1: R_IDENTIFIER@15..16 - 0: IDENT@15..16 "a" [] [] - 1: COMMA@16..18 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@18..19 - 0: (empty) - 1: R_IDENTIFIER@18..19 - 0: IDENT@18..19 "b" [] [] - 2: R_BRACK2@19..21 "]]" [] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@21..31 - 0: R_SUBSET2@21..31 - 0: R_IDENTIFIER@21..24 - 0: IDENT@21..24 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@24..31 - 0: L_BRACK2@24..26 "[[" [] [] - 1: R_ARGUMENT_LIST@26..29 - 0: R_ARGUMENT@26..29 - 0: (empty) - 1: R_DOTS@26..29 - 0: DOTS@26..29 "..." [] [] - 2: R_BRACK2@29..31 "]]" [] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@31..47 - 0: R_SUBSET2@31..47 - 0: R_IDENTIFIER@31..34 - 0: IDENT@31..34 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@34..47 - 0: L_BRACK2@34..36 "[[" [] [] - 1: R_ARGUMENT_LIST@36..45 - 0: R_ARGUMENT@36..37 - 0: (empty) - 1: R_IDENTIFIER@36..37 - 0: IDENT@36..37 "a" [] [] - 1: COMMA@37..39 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@39..42 - 0: (empty) - 1: R_DOTS@39..42 - 0: DOTS@39..42 "..." [] [] - 3: COMMA@42..44 "," [] [Whitespace(" ")] - 4: R_ARGUMENT@44..45 - 0: (empty) - 1: R_IDENTIFIER@44..45 - 0: IDENT@44..45 "b" [] [] - 2: R_BRACK2@45..47 "]]" [] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@47..60 - 0: R_SUBSET2@47..60 - 0: R_IDENTIFIER@47..51 - 0: IDENT@47..51 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@51..60 - 0: L_BRACK2@51..53 "[[" [] [] - 1: R_ARGUMENT_LIST@53..58 - 0: R_ARGUMENT@53..58 - 0: R_ARGUMENT_NAME_CLAUSE@53..57 - 0: R_IDENTIFIER@53..55 - 0: IDENT@53..55 "a" [] [Whitespace(" ")] - 1: EQUAL@55..57 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@57..58 - 0: R_DOUBLE_LITERAL@57..58 "1" [] [] - 2: R_BRACK2@58..60 "]]" [] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@60..71 - 0: R_SUBSET2@60..71 - 0: R_IDENTIFIER@60..63 - 0: IDENT@60..63 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@63..71 - 0: L_BRACK2@63..65 "[[" [] [] - 1: R_ARGUMENT_LIST@65..69 - 0: R_ARGUMENT@65..69 - 0: R_ARGUMENT_NAME_CLAUSE@65..69 - 0: R_IDENTIFIER@65..67 - 0: IDENT@65..67 "a" [] [Whitespace(" ")] - 1: EQUAL@67..69 "=" [] [Whitespace(" ")] - 1: (empty) - 2: R_BRACK2@69..71 "]]" [] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@71..91 - 0: R_SUBSET2@71..91 - 0: R_IDENTIFIER@71..75 - 0: IDENT@71..75 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@75..91 - 0: L_BRACK2@75..77 "[[" [] [] - 1: R_ARGUMENT_LIST@77..89 - 0: R_ARGUMENT@77..82 - 0: R_ARGUMENT_NAME_CLAUSE@77..81 - 0: R_IDENTIFIER@77..79 - 0: IDENT@77..79 "a" [] [Whitespace(" ")] - 1: EQUAL@79..81 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@81..82 - 0: R_DOUBLE_LITERAL@81..82 "1" [] [] - 1: COMMA@82..84 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@84..89 - 0: R_ARGUMENT_NAME_CLAUSE@84..88 - 0: R_IDENTIFIER@84..86 - 0: IDENT@84..86 "b" [] [Whitespace(" ")] - 1: EQUAL@86..88 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@88..89 - 0: R_DOUBLE_LITERAL@88..89 "2" [] [] - 2: R_BRACK2@89..91 "]]" [] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@91..109 - 0: R_SUBSET2@91..109 - 0: R_IDENTIFIER@91..94 - 0: IDENT@91..94 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@94..109 - 0: L_BRACK2@94..96 "[[" [] [] - 1: R_ARGUMENT_LIST@96..107 - 0: R_ARGUMENT@96..100 - 0: R_ARGUMENT_NAME_CLAUSE@96..100 - 0: R_IDENTIFIER@96..98 - 0: IDENT@96..98 "a" [] [Whitespace(" ")] - 1: EQUAL@98..100 "=" [] [Whitespace(" ")] - 1: (empty) - 1: COMMA@100..102 "," [] [Whitespace(" ")] - 2: R_ARGUMENT@102..107 - 0: R_ARGUMENT_NAME_CLAUSE@102..106 - 0: R_IDENTIFIER@102..104 - 0: IDENT@102..104 "b" [] [Whitespace(" ")] - 1: EQUAL@104..106 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@106..107 - 0: R_DOUBLE_LITERAL@106..107 "2" [] [] - 2: R_BRACK2@107..109 "]]" [] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@109..124 - 0: R_SUBSET2@109..124 - 0: R_IDENTIFIER@109..113 - 0: IDENT@109..113 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@113..124 - 0: L_BRACK2@113..115 "[[" [] [] - 1: R_ARGUMENT_LIST@115..122 - 0: R_ARGUMENT@115..122 - 0: R_ARGUMENT_NAME_CLAUSE@115..121 - 0: R_DOTS@115..119 - 0: DOTS@115..119 "..." [] [Whitespace(" ")] - 1: EQUAL@119..121 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@121..122 - 0: R_DOUBLE_LITERAL@121..122 "1" [] [] - 2: R_BRACK2@122..124 "]]" [] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@124..136 - 0: R_SUBSET2@124..136 - 0: R_IDENTIFIER@124..127 - 0: IDENT@124..127 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@127..136 - 0: L_BRACK2@127..129 "[[" [] [] - 1: R_ARGUMENT_LIST@129..134 - 0: R_ARGUMENT@129..134 - 0: R_ARGUMENT_NAME_CLAUSE@129..134 - 0: R_DOTS@129..133 - 0: DOTS@129..133 "..." [] [Whitespace(" ")] - 1: EQUAL@133..134 "=" [] [] - 1: (empty) - 2: R_BRACK2@134..136 "]]" [] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@136..153 - 0: R_SUBSET2@136..153 - 0: R_IDENTIFIER@136..140 - 0: IDENT@136..140 "fn" [Newline("\n"), Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@140..153 - 0: L_BRACK2@140..142 "[[" [] [] - 1: R_ARGUMENT_LIST@142..151 - 0: R_ARGUMENT@142..151 - 0: R_ARGUMENT_NAME_CLAUSE@142..150 - 0: R_STRING_VALUE@142..148 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@142..148 "\"arg\"" [] [Whitespace(" ")] - 1: EQUAL@148..150 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@150..151 - 0: R_DOUBLE_LITERAL@150..151 "1" [] [] - 2: R_BRACK2@151..153 "]]" [] [] - 1: (empty) - 12: R_EXPRESSION_STATEMENT@153..167 - 0: R_SUBSET2@153..167 - 0: R_IDENTIFIER@153..156 - 0: IDENT@153..156 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@156..167 - 0: L_BRACK2@156..158 "[[" [] [] - 1: R_ARGUMENT_LIST@158..165 - 0: R_ARGUMENT@158..165 - 0: R_ARGUMENT_NAME_CLAUSE@158..165 - 0: R_STRING_VALUE@158..164 - 0: R_DOUBLE_QUOTED_STRING_LITERAL@158..164 "\"arg\"" [] [Whitespace(" ")] - 1: EQUAL@164..165 "=" [] [] - 1: (empty) - 2: R_BRACK2@165..167 "]]" [] [] - 1: (empty) - 13: R_EXPRESSION_STATEMENT@167..190 - 0: R_SUBSET2@167..190 - 0: R_IDENTIFIER@167..185 - 0: IDENT@167..185 "fn" [Newline("\n"), Newline("\n"), Comments("# Comma tests"), Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@185..190 - 0: L_BRACK2@185..187 "[[" [] [] - 1: R_ARGUMENT_LIST@187..188 - 0: R_ARGUMENT@187..187 - 0: (empty) - 1: (empty) - 1: COMMA@187..188 "," [] [] - 2: R_ARGUMENT@188..188 - 0: (empty) - 1: (empty) - 2: R_BRACK2@188..190 "]]" [] [] - 1: (empty) - 14: R_EXPRESSION_STATEMENT@190..200 - 0: R_SUBSET2@190..200 - 0: R_IDENTIFIER@190..193 - 0: IDENT@190..193 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@193..200 - 0: L_BRACK2@193..195 "[[" [] [] - 1: R_ARGUMENT_LIST@195..198 - 0: R_ARGUMENT@195..195 - 0: (empty) - 1: (empty) - 1: COMMA@195..196 "," [] [] - 2: R_ARGUMENT@196..196 - 0: (empty) - 1: (empty) - 3: COMMA@196..197 "," [] [] - 4: R_ARGUMENT@197..197 - 0: (empty) - 1: (empty) - 5: COMMA@197..198 "," [] [] - 6: R_ARGUMENT@198..198 - 0: (empty) - 1: (empty) - 2: R_BRACK2@198..200 "]]" [] [] - 1: (empty) - 15: R_EXPRESSION_STATEMENT@200..215 - 0: R_SUBSET2@200..215 - 0: R_IDENTIFIER@200..203 - 0: IDENT@200..203 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@203..215 - 0: L_BRACK2@203..205 "[[" [] [] - 1: R_ARGUMENT_LIST@205..213 - 0: R_ARGUMENT@205..205 - 0: (empty) - 1: (empty) - 1: COMMA@205..206 "," [] [] - 2: R_ARGUMENT@206..206 - 0: (empty) - 1: (empty) - 3: COMMA@206..207 "," [] [] - 4: R_ARGUMENT@207..208 - 0: (empty) - 1: R_IDENTIFIER@207..208 - 0: IDENT@207..208 "a" [] [] - 5: COMMA@208..209 "," [] [] - 6: R_ARGUMENT@209..209 - 0: (empty) - 1: (empty) - 7: COMMA@209..210 "," [] [] - 8: R_ARGUMENT@210..211 - 0: (empty) - 1: R_IDENTIFIER@210..211 - 0: IDENT@210..211 "b" [] [] - 9: COMMA@211..212 "," [] [] - 10: R_ARGUMENT@212..212 - 0: (empty) - 1: (empty) - 11: COMMA@212..213 "," [] [] - 12: R_ARGUMENT@213..213 - 0: (empty) - 1: (empty) - 2: R_BRACK2@213..215 "]]" [] [] - 1: (empty) - 16: R_EXPRESSION_STATEMENT@215..252 - 0: R_SUBSET2@215..252 - 0: R_IDENTIFIER@215..235 - 0: IDENT@215..235 "fn" [Newline("\n"), Newline("\n"), Comments("# Comment tests"), Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@235..252 - 0: L_BRACK2@235..237 "[[" [] [] - 1: R_ARGUMENT_LIST@237..237 - 2: R_BRACK2@237..252 "]]" [Newline("\n"), Whitespace(" "), Comments("# comment"), Newline("\n")] [] - 1: (empty) - 17: R_EXPRESSION_STATEMENT@252..274 - 0: R_SUBSET2@252..274 - 0: R_IDENTIFIER@252..255 - 0: IDENT@252..255 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@255..274 - 0: L_BRACK2@255..257 "[[" [] [] - 1: R_ARGUMENT_LIST@257..272 - 0: R_ARGUMENT@257..257 - 0: (empty) - 1: (empty) - 1: COMMA@257..258 "," [] [] - 2: R_ARGUMENT@258..258 - 0: (empty) - 1: (empty) - 3: COMMA@258..272 "," [Newline("\n"), Whitespace(" "), Comments("# comment"), Newline("\n")] [] - 4: R_ARGUMENT@272..272 - 0: (empty) - 1: (empty) - 2: R_BRACK2@272..274 "]]" [] [] - 1: (empty) - 18: R_EXPRESSION_STATEMENT@274..313 - 0: R_SUBSET2@274..313 - 0: R_IDENTIFIER@274..277 - 0: IDENT@274..277 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@277..313 - 0: L_BRACK2@277..279 "[[" [] [] - 1: R_ARGUMENT_LIST@279..310 - 0: R_ARGUMENT@279..283 - 0: (empty) - 1: R_IDENTIFIER@279..283 - 0: IDENT@279..283 "a" [Newline("\n"), Whitespace(" ")] [] - 1: COMMA@283..295 "," [] [Whitespace(" "), Comments("# comment1")] - 2: R_ARGUMENT@295..310 - 0: (empty) - 1: R_IDENTIFIER@295..310 - 0: IDENT@295..310 "b" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")] - 2: R_BRACK2@310..313 "]]" [Newline("\n")] [] - 1: (empty) - 19: R_EXPRESSION_STATEMENT@313..366 - 0: R_SUBSET2@313..366 - 0: R_IDENTIFIER@313..316 - 0: IDENT@313..316 "fn" [Newline("\n")] [] - 1: R_SUBSET2_ARGUMENTS@316..366 - 0: L_BRACK2@316..318 "[[" [] [] - 1: R_ARGUMENT_LIST@318..363 - 0: R_ARGUMENT@318..363 - 0: R_ARGUMENT_NAME_CLAUSE@318..348 - 0: R_IDENTIFIER@318..333 - 0: IDENT@318..333 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment1")] - 1: EQUAL@333..348 "=" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment2")] - 1: R_DOUBLE_VALUE@348..363 - 0: R_DOUBLE_LITERAL@348..363 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")] - 2: R_BRACK2@363..366 "]]" [Newline("\n")] [] - 1: (empty) - 20: R_EXPRESSION_STATEMENT@366..383 - 0: R_SUBSET2@366..383 - 0: R_BRACED_STATEMENTS@366..374 - 0: L_CURLY@366..369 "{" [Newline("\n"), Newline("\n")] [] - 1: R_STATEMENT_LIST@369..373 - 0: R_EXPRESSION_STATEMENT@369..373 - 0: R_IDENTIFIER@369..373 - 0: IDENT@369..373 "expr" [] [] - 1: (empty) - 2: R_CURLY@373..374 "}" [] [] - 1: R_SUBSET2_ARGUMENTS@374..383 - 0: L_BRACK2@374..376 "[[" [] [] - 1: R_ARGUMENT_LIST@376..381 - 0: R_ARGUMENT@376..381 - 0: R_ARGUMENT_NAME_CLAUSE@376..380 - 0: R_IDENTIFIER@376..378 - 0: IDENT@376..378 "a" [] [Whitespace(" ")] - 1: EQUAL@378..380 "=" [] [Whitespace(" ")] - 1: R_DOUBLE_VALUE@380..381 - 0: R_DOUBLE_LITERAL@380..381 "1" [] [] - 2: R_BRACK2@381..383 "]]" [] [] - 1: (empty) - 2: EOF@383..384 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/unary.md b/crates/air_r_parser_tests/tests/corpus/expressions/unary.md new file mode 100644 index 00000000..99381a50 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/unary.md @@ -0,0 +1,36 @@ +## Unary operators + +```r +!a ++a +-a +foo(!a, +b) +foo(-a, bar) + +! + a +- b +``` + +## Unary operators across lines + +```r ++1 +-1 +~1 +?1 +!1 + ++ +1 + ++ # Comment +1 + ++ +# Comment ++ # Comment +1 + +1 + ++1 +``` diff --git a/crates/air_r_parser_tests/tests/corpus/expressions/unary.snap.md b/crates/air_r_parser_tests/tests/corpus/expressions/unary.snap.md new file mode 100644 index 00000000..421d5c63 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/expressions/unary.snap.md @@ -0,0 +1,257 @@ + + +## Unary operators + +```r +!a ++a +-a +foo(!a, +b) +foo(-a, bar) + +! + a +- b +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RUnaryExpression { + operator: BANG@0..1 "!" [] [], + argument: RIdentifier { + name_token: IDENT@1..2 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: PLUS@2..4 "+" [Newline("\n")] [], + argument: RIdentifier { + name_token: IDENT@4..5 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: MINUS@5..7 "-" [Newline("\n")] [], + argument: RIdentifier { + name_token: IDENT@7..8 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@8..12 "foo" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@12..13 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RUnaryExpression { + operator: BANG@13..14 "!" [] [], + argument: RIdentifier { + name_token: IDENT@14..15 "a" [] [], + }, + }, + }, + COMMA@15..17 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RUnaryExpression { + operator: PLUS@17..18 "+" [] [], + argument: RIdentifier { + name_token: IDENT@18..19 "b" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@19..20 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@20..24 "foo" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@24..25 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RUnaryExpression { + operator: MINUS@25..26 "-" [] [], + argument: RIdentifier { + name_token: IDENT@26..27 "a" [] [], + }, + }, + }, + COMMA@27..29 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@29..32 "bar" [] [], + }, + }, + ], + r_paren_token: R_PAREN@32..33 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: BANG@33..36 "!" [Newline("\n"), Newline("\n")] [], + argument: RIdentifier { + name_token: IDENT@36..40 "a" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: MINUS@40..44 "-" [Newline("\n")] [Whitespace(" ")], + argument: RIdentifier { + name_token: IDENT@44..45 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@45..46 "" [Newline("\n")] [], +} +``` + +## Unary operators across lines + +```r ++1 +-1 +~1 +?1 +!1 + ++ +1 + ++ # Comment +1 + ++ +# Comment ++ # Comment +1 + +1 + ++1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RUnaryExpression { + operator: PLUS@0..1 "+" [] [], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@1..2 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: MINUS@2..4 "-" [Newline("\n")] [], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@4..5 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: TILDE@5..7 "~" [Newline("\n")] [], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@7..8 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: WAT@8..10 "?" [Newline("\n")] [], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@10..11 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: BANG@11..13 "!" [Newline("\n")] [], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@13..14 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: PLUS@14..17 "+" [Newline("\n"), Newline("\n")] [], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@17..19 "1" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: PLUS@19..32 "+" [Newline("\n"), Newline("\n")] [Whitespace(" "), Comments("# Comment")], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@32..34 "1" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: PLUS@34..37 "+" [Newline("\n"), Newline("\n")] [], + argument: RUnaryExpression { + operator: PLUS@37..59 "+" [Newline("\n"), Comments("# Comment"), Newline("\n")] [Whitespace(" "), Comments("# Comment")], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@59..61 "1" [Newline("\n")] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@61..65 "1" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + }, + operator: PLUS@65..67 "+" [] [Whitespace(" ")], + right: RUnaryExpression { + operator: PLUS@67..68 "+" [] [], + argument: RUnaryExpression { + operator: PLUS@68..69 "+" [] [], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@69..70 "1" [] [], + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@70..71 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/functions/definition.md b/crates/air_r_parser_tests/tests/corpus/functions/definition.md new file mode 100644 index 00000000..c93207df --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/functions/definition.md @@ -0,0 +1,106 @@ +## Functions + +```r +function() 1 +function() {} +function(arg1, arg2) { + arg2 +} + +function(arg1, arg2 = 2) {} + +function(x, + y, + z = 3) { + +} + +function() + + + 1 + +function +() 1 + 1 + +function + +() 1 + 1 + +function + +# this important comment + +() 1 + 1 + +function() function() {} + +function(x = function() {}) {} + +# With no intermediate `{` scope +function() for(i in 1:5) i +``` + +## Function no body [error] + +```r +function(x, y) +``` + +## Function no body with assignment [error] + +```r +x <- function(x, y) +``` + +## Function no body inside another function [error] + +```r +function(x = function()) {} +``` + +## Function definitions + +```r +function(a) a +function(a, b) a + b +function( + a # important! +) a +function(...) 1 +function(a, ..., b) 1 +function(a = 1, ..., b = 2) a +function(..1, ..2) get("..1") + get("..2") +function(..1 = 1, ..2 = 2) get("..1") + get("..2") +function( + x = # important! + 4 +) 1 + +\(x, y) 1 +``` + +## A body stops at the binary `?` + +Weirdly in R, help is precedence 1 and function and loop bodies are precedence 2. +So `function() x ? y` must parse as `(function () x) ? y`. + +```r +function() a ? b +function() a ? b ? c +?function() a +``` + +```r +function() a <- b +function() a = b +1 + function(x) x + 2 +``` + +## A definition with no parameter list [error] + +```r +function 1 +\ 1 +function +``` diff --git a/crates/air_r_parser_tests/tests/corpus/functions/definition.snap.md b/crates/air_r_parser_tests/tests/corpus/functions/definition.snap.md new file mode 100644 index 00000000..38f2a637 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/functions/definition.snap.md @@ -0,0 +1,1173 @@ + + +## Functions + +```r +function() 1 +function() {} +function(arg1, arg2) { + arg2 +} + +function(arg1, arg2 = 2) {} + +function(x, + y, + z = 3) { + +} + +function() + + + 1 + +function +() 1 + 1 + +function + +() 1 + 1 + +function + +# this important comment + +() 1 + 1 + +function() function() {} + +function(x = function() {}) {} + +# With no intermediate `{` scope +function() for(i in 1:5) i +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@9..11 ")" [] [Whitespace(" ")], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@11..12 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@12..21 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@21..22 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@22..24 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@24..25 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@25..26 "}" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@26..35 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@35..36 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@36..40 "arg1" [] [], + }, + default: missing (optional), + }, + COMMA@40..42 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@42..46 "arg2" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@46..48 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@48..49 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@49..56 "arg2" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@56..58 "}" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@58..68 "function" [Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@68..69 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@69..73 "arg1" [] [], + }, + default: missing (optional), + }, + COMMA@73..75 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@75..80 "arg2" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@80..82 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@82..83 "2" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@83..85 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@85..86 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@86..87 "}" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@87..97 "function" [Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@97..98 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@98..99 "x" [] [], + }, + default: missing (optional), + }, + COMMA@99..100 "," [] [], + RParameter { + name: RIdentifier { + name_token: IDENT@100..111 "y" [Newline("\n"), Whitespace(" ")] [], + }, + default: missing (optional), + }, + COMMA@111..112 "," [] [], + RParameter { + name: RIdentifier { + name_token: IDENT@112..124 "z" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@124..126 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@126..127 "3" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@127..129 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@129..130 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@130..133 "}" [Newline("\n"), Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@133..143 "function" [Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@143..144 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@144..145 ")" [] [], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@145..151 "1" [Newline("\n"), Newline("\n"), Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@151..161 "function" [Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@161..163 "(" [Newline("\n")] [], + items: RParameterList [], + r_paren_token: R_PAREN@163..165 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@165..167 "1" [] [Whitespace(" ")], + }, + operator: PLUS@167..169 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@169..170 "1" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@170..180 "function" [Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@180..183 "(" [Newline("\n"), Newline("\n")] [], + items: RParameterList [], + r_paren_token: R_PAREN@183..185 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@185..187 "1" [] [Whitespace(" ")], + }, + operator: PLUS@187..189 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@189..190 "1" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@190..200 "function" [Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@200..229 "(" [Newline("\n"), Newline("\n"), Comments("# this important comment"), Newline("\n"), Newline("\n")] [], + items: RParameterList [], + r_paren_token: R_PAREN@229..231 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@231..233 "1" [] [Whitespace(" ")], + }, + operator: PLUS@233..235 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@235..236 "1" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@236..246 "function" [Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@246..247 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@247..249 ")" [] [Whitespace(" ")], + }, + body: RFunctionDefinition { + name: FUNCTION_KW@249..257 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@257..258 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@258..260 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@260..261 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@261..262 "}" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@262..272 "function" [Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@272..273 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@273..275 "x" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@275..277 "=" [] [Whitespace(" ")], + value: RFunctionDefinition { + name: FUNCTION_KW@277..285 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@285..286 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@286..288 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@288..289 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@289..290 "}" [] [], + }, + }, + }, + }, + ], + r_paren_token: R_PAREN@290..292 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@292..293 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@293..294 "}" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@294..337 "function" [Newline("\n"), Newline("\n"), Comments("# With no intermediat ..."), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@337..338 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@338..340 ")" [] [Whitespace(" ")], + }, + body: RForStatement { + for_token: FOR_KW@340..343 "for" [] [], + l_paren_token: L_PAREN@343..344 "(" [] [], + variable: RIdentifier { + name_token: IDENT@344..346 "i" [] [Whitespace(" ")], + }, + in_token: IN_KW@346..349 "in" [] [Whitespace(" ")], + sequence: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@349..350 "1" [] [], + }, + operator: COLON@350..351 ":" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@351..352 "5" [] [], + }, + }, + r_paren_token: R_PAREN@352..354 ")" [] [Whitespace(" ")], + body: RIdentifier { + name_token: IDENT@354..355 "i" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@355..356 "" [Newline("\n")] [], +} +``` + +## Function no body [error] + +```r +function(x, y) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@9..10 "x" [] [], + }, + default: missing (optional), + }, + COMMA@10..12 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@12..13 "y" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@13..14 ")" [] [], + }, + body: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@14..15 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a function body but instead found the end of the file. + + 1 │ function(x, y) + > 2 │ + │ +``` + +## Function no body with assignment [error] + +```r +x <- function(x, y) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "x" [] [Whitespace(" ")], + }, + operator: ASSIGN@2..5 "<-" [] [Whitespace(" ")], + right: RFunctionDefinition { + name: FUNCTION_KW@5..13 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@13..14 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@14..15 "x" [] [], + }, + default: missing (optional), + }, + COMMA@15..17 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@17..18 "y" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@18..19 ")" [] [], + }, + body: missing (required), + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@19..20 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a function body but instead found the end of the file. + + 1 │ x <- function(x, y) + > 2 │ + │ +``` + +## Function no body inside another function [error] + +```r +function(x = function()) {} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@9..11 "x" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@11..13 "=" [] [Whitespace(" ")], + value: RFunctionDefinition { + name: FUNCTION_KW@13..21 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@21..22 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@22..23 ")" [] [], + }, + body: missing (required), + }, + }, + }, + ], + r_paren_token: R_PAREN@23..25 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@25..26 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@26..27 "}" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@27..28 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:24 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a function body but instead found ')'. + + > 1 │ function(x = function()) {} + │ ^ + 2 │ +``` + +## Function definitions + +```r +function(a) a +function(a, b) a + b +function( + a # important! +) a +function(...) 1 +function(a, ..., b) 1 +function(a = 1, ..., b = 2) a +function(..1, ..2) get("..1") + get("..2") +function(..1 = 1, ..2 = 2) get("..1") + get("..2") +function( + x = # important! + 4 +) 1 + +\(x, y) 1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@9..10 "a" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@10..12 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@12..13 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@13..22 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@22..23 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@23..24 "a" [] [], + }, + default: missing (optional), + }, + COMMA@24..26 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@26..27 "b" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@27..29 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@29..31 "a" [] [Whitespace(" ")], + }, + operator: PLUS@31..33 "+" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@33..34 "b" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@34..43 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@43..44 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@44..61 "a" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# important!")], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@61..64 ")" [Newline("\n")] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@64..65 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@65..74 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@74..75 "(" [] [], + items: RParameterList [ + RParameter { + name: RDots { + value_token: DOTS@75..78 "..." [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@78..80 ")" [] [Whitespace(" ")], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@80..81 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@81..90 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@90..91 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@91..92 "a" [] [], + }, + default: missing (optional), + }, + COMMA@92..94 "," [] [Whitespace(" ")], + RParameter { + name: RDots { + value_token: DOTS@94..97 "..." [] [], + }, + default: missing (optional), + }, + COMMA@97..99 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@99..100 "b" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@100..102 ")" [] [Whitespace(" ")], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@102..103 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@103..112 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@112..113 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@113..115 "a" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@115..117 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@117..118 "1" [] [], + }, + }, + }, + COMMA@118..120 "," [] [Whitespace(" ")], + RParameter { + name: RDots { + value_token: DOTS@120..123 "..." [] [], + }, + default: missing (optional), + }, + COMMA@123..125 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@125..127 "b" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@127..129 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@129..130 "2" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@130..132 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@132..133 "a" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@133..142 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@142..143 "(" [] [], + items: RParameterList [ + RParameter { + name: RDotDotI { + value_token: DOTDOTI@143..146 "..1" [] [], + }, + default: missing (optional), + }, + COMMA@146..148 "," [] [Whitespace(" ")], + RParameter { + name: RDotDotI { + value_token: DOTDOTI@148..151 "..2" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@151..153 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RCall { + function: RIdentifier { + name_token: IDENT@153..156 "get" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@156..157 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@157..162 "\"..1\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@162..164 ")" [] [Whitespace(" ")], + }, + }, + operator: PLUS@164..166 "+" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@166..169 "get" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@169..170 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@170..175 "\"..2\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@175..176 ")" [] [], + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@176..185 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@185..186 "(" [] [], + items: RParameterList [ + RParameter { + name: RDotDotI { + value_token: DOTDOTI@186..190 "..1" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@190..192 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@192..193 "1" [] [], + }, + }, + }, + COMMA@193..195 "," [] [Whitespace(" ")], + RParameter { + name: RDotDotI { + value_token: DOTDOTI@195..199 "..2" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@199..201 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@201..202 "2" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@202..204 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RCall { + function: RIdentifier { + name_token: IDENT@204..207 "get" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@207..208 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@208..213 "\"..1\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@213..215 ")" [] [Whitespace(" ")], + }, + }, + operator: PLUS@215..217 "+" [] [Whitespace(" ")], + right: RCall { + function: RIdentifier { + name_token: IDENT@217..220 "get" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@220..221 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@221..226 "\"..2\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@226..227 ")" [] [], + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@227..236 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@236..237 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@237..242 "x" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@242..256 "=" [] [Whitespace(" "), Comments("# important!")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@256..260 "4" [Newline("\n"), Whitespace(" ")] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@260..263 ")" [Newline("\n")] [Whitespace(" ")], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@263..264 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: BACKSLASH@264..267 "\\" [Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@267..268 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@268..269 "x" [] [], + }, + default: missing (optional), + }, + COMMA@269..271 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@271..272 "y" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@272..274 ")" [] [Whitespace(" ")], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@274..275 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@275..276 "" [Newline("\n")] [], +} +``` + +## A body stops at the binary `?` + +Weirdly in R, help is precedence 1 and function and loop bodies are precedence 2. +So `function() x ? y` must parse as `(function () x) ? y`. + +```r +function() a ? b +function() a ? b ? c +?function() a +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@9..11 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@11..13 "a" [] [Whitespace(" ")], + }, + }, + operator: WAT@13..15 "?" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@15..16 "b" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RFunctionDefinition { + name: FUNCTION_KW@16..25 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@25..26 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@26..28 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@28..30 "a" [] [Whitespace(" ")], + }, + }, + operator: WAT@30..32 "?" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@32..34 "b" [] [Whitespace(" ")], + }, + }, + operator: WAT@34..36 "?" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@36..37 "c" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RUnaryExpression { + operator: WAT@37..39 "?" [Newline("\n")] [], + argument: RFunctionDefinition { + name: FUNCTION_KW@39..47 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@47..48 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@48..50 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@50..51 "a" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@51..52 "" [Newline("\n")] [], +} +``` + +```r +function() a <- b +function() a = b +1 + function(x) x + 2 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@9..11 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@11..13 "a" [] [Whitespace(" ")], + }, + operator: ASSIGN@13..16 "<-" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@16..17 "b" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@17..26 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@26..27 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@27..29 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@29..31 "a" [] [Whitespace(" ")], + }, + operator: EQUAL@31..33 "=" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@33..34 "b" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@34..37 "1" [Newline("\n")] [Whitespace(" ")], + }, + operator: PLUS@37..39 "+" [] [Whitespace(" ")], + right: RFunctionDefinition { + name: FUNCTION_KW@39..47 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@47..48 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@48..49 "x" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@49..51 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@51..53 "x" [] [Whitespace(" ")], + }, + operator: PLUS@53..55 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@55..56 "2" [] [], + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@56..57 "" [Newline("\n")] [], +} +``` + +## A definition with no parameter list [error] + +```r +function 1 +\ 1 +function +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..9 "function" [] [Whitespace(" ")], + parameters: missing (required), + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@9..10 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: BACKSLASH@10..13 "\\" [Newline("\n")] [Whitespace(" ")], + parameters: missing (required), + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@13..14 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@14..23 "function" [Newline("\n")] [], + parameters: missing (required), + body: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@23..24 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:10 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter list but instead found '1'. + + > 1 │ function 1 + │ ^ + 2 │ \ 1 + 3 │ function + +test.R:2:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter list but instead found '1'. + + 1 │ function 1 + > 2 │ \ 1 + │ ^ + 3 │ function + 4 │ + +test.R:4:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter list but instead found the end of the file. + + 2 │ \ 1 + 3 │ function + > 4 │ + │ +``` diff --git a/crates/air_r_parser_tests/tests/corpus/functions/lambda.md b/crates/air_r_parser_tests/tests/corpus/functions/lambda.md new file mode 100644 index 00000000..50ad688f --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/functions/lambda.md @@ -0,0 +1,18 @@ +## Lambda function + +```r +a <- \(arg) arg +b <- \(arg1, arg2) paste0(arg1, arg2) +c <- \(fun, ...) fun(...) +1:3 |> {\(x, y = 1) x + y}() |> {\(x) sum(x)}() +{\(a = 1) a + 1}() +\() 1 + 2 +\() + + 1 + 2 + +# Not currently allowed by R's parser, but we think it will be and is just an oversight. +# `'\\'` would need to be included alongside `FUNCTION` here https://github.com/wch/r-source/blob/802121c877837926a6bc2a930b3da749b537258b/src/main/gram.y#L3898-L3901. +\ +() 1 + 1 +``` diff --git a/crates/air_r_parser_tests/tests/corpus/functions/lambda.snap.md b/crates/air_r_parser_tests/tests/corpus/functions/lambda.snap.md new file mode 100644 index 00000000..4aa9d302 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/functions/lambda.snap.md @@ -0,0 +1,385 @@ + + +## Lambda function + +```r +a <- \(arg) arg +b <- \(arg1, arg2) paste0(arg1, arg2) +c <- \(fun, ...) fun(...) +1:3 |> {\(x, y = 1) x + y}() |> {\(x) sum(x)}() +{\(a = 1) a + 1}() +\() 1 + 2 +\() + + 1 + 2 + +# Not currently allowed by R's parser, but we think it will be and is just an oversight. +# `'\\'` would need to be included alongside `FUNCTION` here https://github.com/wch/r-source/blob/802121c877837926a6bc2a930b3da749b537258b/src/main/gram.y#L3898-L3901. +\ +() 1 + 1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "a" [] [Whitespace(" ")], + }, + operator: ASSIGN@2..5 "<-" [] [Whitespace(" ")], + right: RFunctionDefinition { + name: BACKSLASH@5..6 "\\" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@6..7 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@7..10 "arg" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@10..12 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@12..15 "arg" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@15..18 "b" [Newline("\n")] [Whitespace(" ")], + }, + operator: ASSIGN@18..21 "<-" [] [Whitespace(" ")], + right: RFunctionDefinition { + name: BACKSLASH@21..22 "\\" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@22..23 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@23..27 "arg1" [] [], + }, + default: missing (optional), + }, + COMMA@27..29 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@29..33 "arg2" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@33..35 ")" [] [Whitespace(" ")], + }, + body: RCall { + function: RIdentifier { + name_token: IDENT@35..41 "paste0" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@41..42 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@42..46 "arg1" [] [], + }, + }, + COMMA@46..48 "," [] [Whitespace(" ")], + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@48..52 "arg2" [] [], + }, + }, + ], + r_paren_token: R_PAREN@52..53 ")" [] [], + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@53..56 "c" [Newline("\n")] [Whitespace(" ")], + }, + operator: ASSIGN@56..59 "<-" [] [Whitespace(" ")], + right: RFunctionDefinition { + name: BACKSLASH@59..60 "\\" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@60..61 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@61..64 "fun" [] [], + }, + default: missing (optional), + }, + COMMA@64..66 "," [] [Whitespace(" ")], + RParameter { + name: RDots { + value_token: DOTS@66..69 "..." [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@69..71 ")" [] [Whitespace(" ")], + }, + body: RCall { + function: RIdentifier { + name_token: IDENT@71..74 "fun" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@74..75 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDots { + value_token: DOTS@75..78 "..." [] [], + }, + }, + ], + r_paren_token: R_PAREN@78..79 ")" [] [], + }, + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RBinaryExpression { + left: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@79..81 "1" [Newline("\n")] [], + }, + operator: COLON@81..82 ":" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@82..84 "3" [] [Whitespace(" ")], + }, + }, + operator: PIPE@84..87 "|>" [] [Whitespace(" ")], + right: RCall { + function: RBracedStatements { + l_curly_token: L_CURLY@87..88 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: BACKSLASH@88..89 "\\" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@89..90 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@90..91 "x" [] [], + }, + default: missing (optional), + }, + COMMA@91..93 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@93..95 "y" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@95..97 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@97..98 "1" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@98..100 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@100..102 "x" [] [Whitespace(" ")], + }, + operator: PLUS@102..104 "+" [] [Whitespace(" ")], + right: RIdentifier { + name_token: IDENT@104..105 "y" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@105..106 "}" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@106..107 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@107..109 ")" [] [Whitespace(" ")], + }, + }, + }, + operator: PIPE@109..112 "|>" [] [Whitespace(" ")], + right: RCall { + function: RBracedStatements { + l_curly_token: L_CURLY@112..113 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: BACKSLASH@113..114 "\\" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@114..115 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@115..116 "x" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@116..118 ")" [] [Whitespace(" ")], + }, + body: RCall { + function: RIdentifier { + name_token: IDENT@118..121 "sum" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@121..122 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@122..123 "x" [] [], + }, + }, + ], + r_paren_token: R_PAREN@123..124 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@124..125 "}" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@125..126 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@126..127 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RBracedStatements { + l_curly_token: L_CURLY@127..129 "{" [Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: BACKSLASH@129..130 "\\" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@130..131 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@131..133 "a" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@133..135 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@135..136 "1" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@136..138 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@138..140 "a" [] [Whitespace(" ")], + }, + operator: PLUS@140..142 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@142..143 "1" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@143..144 "}" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@144..145 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@145..146 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: BACKSLASH@146..148 "\\" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@148..149 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@149..151 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@151..153 "1" [] [Whitespace(" ")], + }, + operator: PLUS@153..155 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@155..156 "2" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: BACKSLASH@156..158 "\\" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@158..159 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@159..160 ")" [] [], + }, + body: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@160..166 "1" [Newline("\n"), Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: PLUS@166..168 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@168..169 "2" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: BACKSLASH@169..429 "\\" [Newline("\n"), Newline("\n"), Comments("# Not currently allow ..."), Newline("\n"), Comments("# `'\\\\'` would need t ..."), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@429..431 "(" [Newline("\n")] [], + items: RParameterList [], + r_paren_token: R_PAREN@431..433 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@433..435 "1" [] [Whitespace(" ")], + }, + operator: PLUS@435..437 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@437..438 "1" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@438..439 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/functions/parameters.md b/crates/air_r_parser_tests/tests/corpus/functions/parameters.md new file mode 100644 index 00000000..0b517779 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/functions/parameters.md @@ -0,0 +1,91 @@ +## Functions with `return` as parameter name + +```r +# `return` is not a reserved word in R (it's not in `?Reserved`), so it is +# allowed as a parameter name (r-lib/tree-sitter-r#189). + +function(return) return +function(return = 1) return +``` + +## Function dots and dot dot i + +```r +# Dots as parameter without default +function(...) { list(...) } + +# Dots as parameter with default +function(... = 1) { get("...") } + +# `..i` as parameter without default +function(..1) { get("..1") } + +# `..i` as parameter with default +function(..1 = 1) { get("..1") } + +# Miscellaneous +function(x, ...) f(...) +function(x, ...) ..1 + ..2 +``` + +## A hole in a parameter list [error] + +```r +function(a,,b) 1 +function(a,) 1 +function(,) 1 +``` + +## A parameter that is not a name [error] + +```r +function(1) x +function("a") x +function(a$b) x +``` + +Recovery stops at the `=`, so a bad name costs the name and nothing else. + +```r +function(1 = 2) x +function("a" = 1) x +function(a, 1 = 2, b) x +function(= 1) x +``` + +## A missing comma between parameters [error] + +A name is a single token, so the token after one may well be another parameter. +Reporting the separator without consuming anything keeps both potential parameters. + +```r +function(a b) x +``` + +## A default with no value [error] + +The default is required once an `=` is written (r-lib/tree-sitter-r#161). + +```r +function(x = ) 1 +``` + +## A parameter list with no closing parenthesis [error] + +The list runs to the end of the file and the `)` is reported missing, leaving +`RParameters` and the definition around it whole. + +```r +function(a +``` + +```r +\(x +``` + +A closer that belongs to someone else ends the list without being consumed + +```r +function(a} 1 +function(a] 1 +``` diff --git a/crates/air_r_parser_tests/tests/corpus/functions/parameters.snap.md b/crates/air_r_parser_tests/tests/corpus/functions/parameters.snap.md new file mode 100644 index 00000000..c667afac --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/functions/parameters.snap.md @@ -0,0 +1,1114 @@ + + +## Functions with `return` as parameter name + +```r +# `return` is not a reserved word in R (it's not in `?Reserved`), so it is +# allowed as a parameter name (r-lib/tree-sitter-r#189). + +function(return) return +function(return = 1) return +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..141 "function" [Comments("# `return` is not a r ..."), Newline("\n"), Comments("# allowed as a parame ..."), Newline("\n"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@141..142 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@142..148 "return" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@148..150 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@150..156 "return" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@156..165 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@165..166 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@166..173 "return" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@173..175 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@175..176 "1" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@176..178 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@178..184 "return" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@184..185 "" [Newline("\n")] [], +} +``` + +## Function dots and dot dot i + +```r +# Dots as parameter without default +function(...) { list(...) } + +# Dots as parameter with default +function(... = 1) { get("...") } + +# `..i` as parameter without default +function(..1) { get("..1") } + +# `..i` as parameter with default +function(..1 = 1) { get("..1") } + +# Miscellaneous +function(x, ...) f(...) +function(x, ...) ..1 + ..2 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..44 "function" [Comments("# Dots as parameter w ..."), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@44..45 "(" [] [], + items: RParameterList [ + RParameter { + name: RDots { + value_token: DOTS@45..48 "..." [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@48..50 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@50..52 "{" [] [Whitespace(" ")], + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@52..56 "list" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@56..57 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDots { + value_token: DOTS@57..60 "..." [] [], + }, + }, + ], + r_paren_token: R_PAREN@60..62 ")" [] [Whitespace(" ")], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@62..63 "}" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@63..106 "function" [Newline("\n"), Newline("\n"), Comments("# Dots as parameter w ..."), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@106..107 "(" [] [], + items: RParameterList [ + RParameter { + name: RDots { + value_token: DOTS@107..111 "..." [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@111..113 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@113..114 "1" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@114..116 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@116..118 "{" [] [Whitespace(" ")], + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@118..121 "get" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@121..122 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@122..127 "\"...\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@127..129 ")" [] [Whitespace(" ")], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@129..130 "}" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@130..177 "function" [Newline("\n"), Newline("\n"), Comments("# `..i` as parameter ..."), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@177..178 "(" [] [], + items: RParameterList [ + RParameter { + name: RDotDotI { + value_token: DOTDOTI@178..181 "..1" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@181..183 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@183..185 "{" [] [Whitespace(" ")], + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@185..188 "get" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@188..189 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@189..194 "\"..1\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@194..196 ")" [] [Whitespace(" ")], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@196..197 "}" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@197..241 "function" [Newline("\n"), Newline("\n"), Comments("# `..i` as parameter ..."), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@241..242 "(" [] [], + items: RParameterList [ + RParameter { + name: RDotDotI { + value_token: DOTDOTI@242..246 "..1" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@246..248 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@248..249 "1" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@249..251 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@251..253 "{" [] [Whitespace(" ")], + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@253..256 "get" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@256..257 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@257..262 "\"..1\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@262..264 ")" [] [Whitespace(" ")], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@264..265 "}" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@265..291 "function" [Newline("\n"), Newline("\n"), Comments("# Miscellaneous"), Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@291..292 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@292..293 "x" [] [], + }, + default: missing (optional), + }, + COMMA@293..295 "," [] [Whitespace(" ")], + RParameter { + name: RDots { + value_token: DOTS@295..298 "..." [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@298..300 ")" [] [Whitespace(" ")], + }, + body: RCall { + function: RIdentifier { + name_token: IDENT@300..301 "f" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@301..302 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDots { + value_token: DOTS@302..305 "..." [] [], + }, + }, + ], + r_paren_token: R_PAREN@305..306 ")" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@306..315 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@315..316 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@316..317 "x" [] [], + }, + default: missing (optional), + }, + COMMA@317..319 "," [] [Whitespace(" ")], + RParameter { + name: RDots { + value_token: DOTS@319..322 "..." [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@322..324 ")" [] [Whitespace(" ")], + }, + body: RBinaryExpression { + left: RDotDotI { + value_token: DOTDOTI@324..328 "..1" [] [Whitespace(" ")], + }, + operator: PLUS@328..330 "+" [] [Whitespace(" ")], + right: RDotDotI { + value_token: DOTDOTI@330..333 "..2" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@333..334 "" [Newline("\n")] [], +} +``` + +## A hole in a parameter list [error] + +```r +function(a,,b) 1 +function(a,) 1 +function(,) 1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@9..10 "a" [] [], + }, + default: missing (optional), + }, + COMMA@10..11 "," [] [], + RParameter { + name: missing (required), + default: missing (optional), + }, + COMMA@11..12 "," [] [], + RParameter { + name: RIdentifier { + name_token: IDENT@12..13 "b" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@13..15 ")" [] [Whitespace(" ")], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@15..16 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@16..25 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@25..26 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@26..27 "a" [] [], + }, + default: missing (optional), + }, + COMMA@27..28 "," [] [], + RParameter { + name: missing (required), + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@28..30 ")" [] [Whitespace(" ")], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@30..31 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@31..40 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@40..41 "(" [] [], + items: RParameterList [ + RParameter { + name: missing (required), + default: missing (optional), + }, + COMMA@41..42 "," [] [], + RParameter { + name: missing (required), + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@42..44 ")" [] [Whitespace(" ")], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@44..45 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@45..46 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:12 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter name but instead found ','. + + > 1 │ function(a,,b) 1 + │ ^ + 2 │ function(a,) 1 + 3 │ function(,) 1 + +test.R:2:12 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter name but instead found ')'. + + 1 │ function(a,,b) 1 + > 2 │ function(a,) 1 + │ ^ + 3 │ function(,) 1 + 4 │ + +test.R:3:10 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter name but instead found ','. + + 1 │ function(a,,b) 1 + 2 │ function(a,) 1 + > 3 │ function(,) 1 + │ ^ + 4 │ + +test.R:3:11 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter name but instead found ')'. + + 1 │ function(a,,b) 1 + 2 │ function(a,) 1 + > 3 │ function(,) 1 + │ ^ + 4 │ +``` + +## A parameter that is not a name [error] + +```r +function(1) x +function("a") x +function(a$b) x +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RBogusParameterName { + items: [ + R_DOUBLE_LITERAL@9..10 "1" [] [], + ], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@10..12 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@12..13 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@13..22 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@22..23 "(" [] [], + items: RParameterList [ + RParameter { + name: RBogusParameterName { + items: [ + R_DOUBLE_QUOTED_STRING_LITERAL@23..26 "\"a\"" [] [], + ], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@26..28 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@28..29 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@29..38 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@38..39 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@39..40 "a" [] [], + }, + default: missing (optional), + }, + missing separator, + RParameter { + name: RBogusParameterName { + items: [ + DOLLAR@40..41 "$" [] [], + IDENT@41..42 "b" [] [], + ], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@42..44 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@44..45 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@45..46 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:10 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter name but instead found '1'. + + > 1 │ function(1) x + │ ^ + 2 │ function("a") x + 3 │ function(a$b) x + +test.R:2:10 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter name but instead found '"a"'. + + 1 │ function(1) x + > 2 │ function("a") x + │ ^^^ + 3 │ function(a$b) x + 4 │ + +test.R:3:11 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `$` + + 1 │ function(1) x + 2 │ function("a") x + > 3 │ function(a$b) x + │ ^ + 4 │ + + i Remove $ +``` + +Recovery stops at the `=`, so a bad name costs the name and nothing else. + +```r +function(1 = 2) x +function("a" = 1) x +function(a, 1 = 2, b) x +function(= 1) x +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RBogusParameterName { + items: [ + R_DOUBLE_LITERAL@9..11 "1" [] [Whitespace(" ")], + ], + }, + default: RParameterDefault { + eq_token: EQUAL@11..13 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@13..14 "2" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@14..16 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@16..17 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@17..26 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@26..27 "(" [] [], + items: RParameterList [ + RParameter { + name: RBogusParameterName { + items: [ + R_DOUBLE_QUOTED_STRING_LITERAL@27..31 "\"a\"" [] [Whitespace(" ")], + ], + }, + default: RParameterDefault { + eq_token: EQUAL@31..33 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@33..34 "1" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@34..36 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@36..37 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@37..46 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@46..47 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@47..48 "a" [] [], + }, + default: missing (optional), + }, + COMMA@48..50 "," [] [Whitespace(" ")], + RParameter { + name: RBogusParameterName { + items: [ + R_DOUBLE_LITERAL@50..52 "1" [] [Whitespace(" ")], + ], + }, + default: RParameterDefault { + eq_token: EQUAL@52..54 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@54..55 "2" [] [], + }, + }, + }, + COMMA@55..57 "," [] [Whitespace(" ")], + RParameter { + name: RIdentifier { + name_token: IDENT@57..58 "b" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@58..60 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@60..61 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@61..70 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@70..71 "(" [] [], + items: RParameterList [ + RParameter { + name: missing (required), + default: RParameterDefault { + eq_token: EQUAL@71..73 "=" [] [Whitespace(" ")], + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@73..74 "1" [] [], + }, + }, + }, + ], + r_paren_token: R_PAREN@74..76 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@76..77 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@77..78 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:10 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter name but instead found '1'. + + > 1 │ function(1 = 2) x + │ ^ + 2 │ function("a" = 1) x + 3 │ function(a, 1 = 2, b) x + +test.R:2:10 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter name but instead found '"a"'. + + 1 │ function(1 = 2) x + > 2 │ function("a" = 1) x + │ ^^^ + 3 │ function(a, 1 = 2, b) x + 4 │ function(= 1) x + +test.R:3:13 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter name but instead found '1'. + + 1 │ function(1 = 2) x + 2 │ function("a" = 1) x + > 3 │ function(a, 1 = 2, b) x + │ ^ + 4 │ function(= 1) x + 5 │ + +test.R:4:10 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a parameter name but instead found '='. + + 2 │ function("a" = 1) x + 3 │ function(a, 1 = 2, b) x + > 4 │ function(= 1) x + │ ^ + 5 │ +``` + +## A missing comma between parameters [error] + +A name is a single token, so the token after one may well be another parameter. +Reporting the separator without consuming anything keeps both potential parameters. + +```r +function(a b) x +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@9..11 "a" [] [Whitespace(" ")], + }, + default: missing (optional), + }, + missing separator, + RParameter { + name: RIdentifier { + name_token: IDENT@11..12 "b" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@12..14 ")" [] [Whitespace(" ")], + }, + body: RIdentifier { + name_token: IDENT@14..15 "x" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@15..16 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:12 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `,` but instead found `b` + + > 1 │ function(a b) x + │ ^ + 2 │ + + i Remove b +``` + +## A default with no value [error] + +The default is required once an `=` is written (r-lib/tree-sitter-r#161). + +```r +function(x = ) 1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@9..11 "x" [] [Whitespace(" ")], + }, + default: RParameterDefault { + eq_token: EQUAL@11..13 "=" [] [Whitespace(" ")], + value: missing (required), + }, + }, + ], + r_paren_token: R_PAREN@13..15 ")" [] [Whitespace(" ")], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@15..16 "1" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@16..17 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:14 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ')'. + + > 1 │ function(x = ) 1 + │ ^ + 2 │ +``` + +## A parameter list with no closing parenthesis [error] + +The list runs to the end of the file and the `)` is reported missing, leaving +`RParameters` and the definition around it whole. + +```r +function(a +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@9..10 "a" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: missing (required), + }, + body: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@10..11 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `)` but instead the file ends + + 1 │ function(a + > 2 │ + │ + + i the file ends here + + 1 │ function(a + > 2 │ + │ +``` + +```r +\(x +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: BACKSLASH@0..1 "\\" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@1..2 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@2..3 "x" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: missing (required), + }, + body: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@3..4 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `)` but instead the file ends + + 1 │ \(x + > 2 │ + │ + + i the file ends here + + 1 │ \(x + > 2 │ + │ +``` + +A closer that belongs to someone else ends the list without being consumed + +```r +function(a} 1 +function(a] 1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@9..10 "a" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: missing (required), + }, + body: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_CURLY@10..12 "}" [] [Whitespace(" ")], + R_DOUBLE_LITERAL@12..13 "1" [] [], + ], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@13..22 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@22..23 "(" [] [], + items: RParameterList [ + RParameter { + name: RIdentifier { + name_token: IDENT@23..24 "a" [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: missing (required), + }, + body: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_BRACK@24..26 "]" [] [Whitespace(" ")], + R_DOUBLE_LITERAL@26..27 "1" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@27..28 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:11 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `)` but instead found `}` + + > 1 │ function(a} 1 + │ ^ + 2 │ function(a] 1 + 3 │ + + i Remove } + +test.R:2:11 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `)` but instead found `]` + + 1 │ function(a} 1 + > 2 │ function(a] 1 + │ ^ + 3 │ + + i Remove ] +``` diff --git a/crates/air_r_parser_tests/tests/corpus/literals/comments.md b/crates/air_r_parser_tests/tests/corpus/literals/comments.md new file mode 100644 index 00000000..17ce57b8 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/literals/comments.md @@ -0,0 +1,26 @@ +## Comments + +```r +# a comment' + +'# not a comment' + + +' +# still not a comment' +``` + +## Leading and trailing comments + +```r +# Leading +1 + 1 # Trailing +# Leading +``` + +## Multi-byte Unicode in a comment + +```r +# héllo wörld 日本語 emoji 🎉 comment +1 +``` diff --git a/crates/air_r_parser_tests/tests/corpus/literals/comments.snap.md b/crates/air_r_parser_tests/tests/corpus/literals/comments.snap.md new file mode 100644 index 00000000..62a11725 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/literals/comments.snap.md @@ -0,0 +1,85 @@ + + +## Comments + +```r +# a comment' + +'# not a comment' + + +' +# still not a comment' +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RStringValue { + value: R_SINGLE_QUOTED_STRING_LITERAL@0..31 "'# not a comment'" [Comments("# a comment'"), Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_SINGLE_QUOTED_STRING_LITERAL@31..58 "'\n# still not a comment'" [Newline("\n"), Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@58..59 "" [Newline("\n")] [], +} +``` + +## Leading and trailing comments + +```r +# Leading +1 + 1 # Trailing +# Leading +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..12 "1" [Comments("# Leading"), Newline("\n")] [Whitespace(" ")], + }, + operator: PLUS@12..14 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@14..26 "1" [] [Whitespace(" "), Comments("# Trailing")], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@26..37 "" [Newline("\n"), Comments("# Leading"), Newline("\n")] [], +} +``` + +## Multi-byte Unicode in a comment + +```r +# héllo wörld 日本語 emoji 🎉 comment +1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..46 "1" [Comments("# héllo wörld 日本 ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@46..47 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser/tests/snapshots/ok/keyword.R b/crates/air_r_parser_tests/tests/corpus/literals/constants.md similarity index 79% rename from crates/air_r_parser/tests/snapshots/ok/keyword.R rename to crates/air_r_parser_tests/tests/corpus/literals/constants.md index d2107707..c2da4714 100644 --- a/crates/air_r_parser/tests/snapshots/ok/keyword.R +++ b/crates/air_r_parser_tests/tests/corpus/literals/constants.md @@ -1,3 +1,6 @@ +## Keywords + +```r break next TRUE @@ -10,3 +13,4 @@ NA_integer_ NA_real_ NA_complex_ NA_character_ +``` diff --git a/crates/air_r_parser/tests/snapshots/ok/keyword.R.snap b/crates/air_r_parser_tests/tests/corpus/literals/constants.snap.md similarity index 59% rename from crates/air_r_parser/tests/snapshots/ok/keyword.R.snap rename to crates/air_r_parser_tests/tests/corpus/literals/constants.snap.md index 9edcbf29..f3644bb2 100644 --- a/crates/air_r_parser/tests/snapshots/ok/keyword.R.snap +++ b/crates/air_r_parser_tests/tests/corpus/literals/constants.snap.md @@ -1,10 +1,8 @@ ---- -source: crates/air_r_parser/tests/spec_test.rs -expression: snapshot ---- -## Input + -```R +## Keywords + +```r break next TRUE @@ -17,13 +15,9 @@ NA_integer_ NA_real_ NA_complex_ NA_character_ - ``` - -## AST - -``` +```cst RRoot { bom_token: missing (optional), statements: RStatementList [ @@ -103,61 +97,3 @@ RRoot { eof_token: EOF@84..85 "" [Newline("\n")] [], } ``` - -## CST - -``` -0: R_ROOT@0..85 - 0: (empty) - 1: R_STATEMENT_LIST@0..84 - 0: R_EXPRESSION_STATEMENT@0..5 - 0: R_BREAK_EXPRESSION@0..5 - 0: BREAK_KW@0..5 "break" [] [] - 1: (empty) - 1: R_EXPRESSION_STATEMENT@5..10 - 0: R_NEXT_EXPRESSION@5..10 - 0: NEXT_KW@5..10 "next" [Newline("\n")] [] - 1: (empty) - 2: R_EXPRESSION_STATEMENT@10..15 - 0: R_TRUE_EXPRESSION@10..15 - 0: TRUE_KW@10..15 "TRUE" [Newline("\n")] [] - 1: (empty) - 3: R_EXPRESSION_STATEMENT@15..21 - 0: R_FALSE_EXPRESSION@15..21 - 0: FALSE_KW@15..21 "FALSE" [Newline("\n")] [] - 1: (empty) - 4: R_EXPRESSION_STATEMENT@21..26 - 0: R_NULL_EXPRESSION@21..26 - 0: NULL_KW@21..26 "NULL" [Newline("\n")] [] - 1: (empty) - 5: R_EXPRESSION_STATEMENT@26..30 - 0: R_INF_EXPRESSION@26..30 - 0: INF_KW@26..30 "Inf" [Newline("\n")] [] - 1: (empty) - 6: R_EXPRESSION_STATEMENT@30..34 - 0: R_NAN_EXPRESSION@30..34 - 0: NAN_KW@30..34 "NaN" [Newline("\n")] [] - 1: (empty) - 7: R_EXPRESSION_STATEMENT@34..37 - 0: R_NA_EXPRESSION@34..37 - 0: NA_LOGICAL_KW@34..37 "NA" [Newline("\n")] [] - 1: (empty) - 8: R_EXPRESSION_STATEMENT@37..49 - 0: R_NA_EXPRESSION@37..49 - 0: NA_INTEGER_KW@37..49 "NA_integer_" [Newline("\n")] [] - 1: (empty) - 9: R_EXPRESSION_STATEMENT@49..58 - 0: R_NA_EXPRESSION@49..58 - 0: NA_DOUBLE_KW@49..58 "NA_real_" [Newline("\n")] [] - 1: (empty) - 10: R_EXPRESSION_STATEMENT@58..70 - 0: R_NA_EXPRESSION@58..70 - 0: NA_COMPLEX_KW@58..70 "NA_complex_" [Newline("\n")] [] - 1: (empty) - 11: R_EXPRESSION_STATEMENT@70..84 - 0: R_NA_EXPRESSION@70..84 - 0: NA_CHARACTER_KW@70..84 "NA_character_" [Newline("\n")] [] - 1: (empty) - 2: EOF@84..85 "" [Newline("\n")] [] - -``` diff --git a/crates/air_r_parser_tests/tests/corpus/literals/identifiers.md b/crates/air_r_parser_tests/tests/corpus/literals/identifiers.md new file mode 100644 index 00000000..10152dec --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/literals/identifiers.md @@ -0,0 +1,81 @@ +## Identifiers + +```r +foo +foo2 +foo.bar +.foo.bar +.__NAMESPACE__. +. +.. +._ +._. +foo_bar +`_foo` +`a "literal"` +`another +literal \` foo` +`backslash followed by newline \ +` +`\`` +# Pipe placeholder +_ +# Recognized as a single `_foo` identifier, even if invalid R code (r-lib/tree-sitter-r#71). +_foo +__foo +_foo_ +``` + +## Unicode identifiers + +```r +你好 +.你.好. +.你_好. +``` + +## Invalid identifiers [error] + +```r +# R does not allow identifiers to start with a `.` followed by a digit, as this +# would cause ambiguity with `.1L` and `.1i`. R itself rejects this as a syntax +# error, we currently parse this as `.1` followed by `foo`, just like we do with +# `1foo` (r-lib/tree-sitter-r#190). +.1foo +``` + +## Dot dot i + +```r +..1 +..10 +``` + +## Unclosed backtick [error] + +```r +`\` +``` + +## Dots + +```r +# Parses, but fails at runtime +... + +{ ... } + +# An actual base R function +...() +``` + +## Dot dot i in functions and calls + +```r +function(...) { + ..1 + ..2 +} + +# Parses as a function +..1() +``` diff --git a/crates/air_r_parser_tests/tests/corpus/literals/identifiers.snap.md b/crates/air_r_parser_tests/tests/corpus/literals/identifiers.snap.md new file mode 100644 index 00000000..e2e40564 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/literals/identifiers.snap.md @@ -0,0 +1,429 @@ + + +## Identifiers + +```r +foo +foo2 +foo.bar +.foo.bar +.__NAMESPACE__. +. +.. +._ +._. +foo_bar +`_foo` +`a "literal"` +`another +literal \` foo` +`backslash followed by newline \ +` +`\`` +# Pipe placeholder +_ +# Recognized as a single `_foo` identifier, even if invalid R code (r-lib/tree-sitter-r#71). +_foo +__foo +_foo_ +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@0..3 "foo" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@3..8 "foo2" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@8..16 "foo.bar" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@16..25 ".foo.bar" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@25..41 ".__NAMESPACE__." [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@41..43 "." [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@43..46 ".." [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@46..49 "._" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@49..53 "._." [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@53..61 "foo_bar" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@61..68 "`_foo`" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@68..82 "`a \"literal\"`" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@82..107 "`another\nliteral \\` foo`" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@107..142 "`backslash followed by newline \\\n`" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@142..147 "`\\``" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@147..168 "_" [Newline("\n"), Comments("# Pipe placeholder"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@168..266 "_foo" [Newline("\n"), Comments("# Recognized as a sin ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@266..272 "__foo" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@272..278 "_foo_" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@278..279 "" [Newline("\n")] [], +} +``` + +## Unicode identifiers + +```r +你好 +.你.好. +.你_好. +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@0..6 "你好" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@6..16 ".你.好." [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@16..26 ".你_好." [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@26..27 "" [Newline("\n")] [], +} +``` + +## Invalid identifiers [error] + +```r +# R does not allow identifiers to start with a `.` followed by a digit, as this +# would cause ambiguity with `.1L` and `.1i`. R itself rejects this as a syntax +# error, we currently parse this as `.1` followed by `foo`, just like we do with +# `1foo` (r-lib/tree-sitter-r#190). +.1foo +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..279 ".1" [Comments("# R does not allow id ..."), Newline("\n"), Comments("# would cause ambigui ..."), Newline("\n"), Comments("# error, we currently ..."), Newline("\n"), Comments("# `1foo` (r-lib/tree- ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@279..282 "foo" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@282..283 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:5:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a newline or a `;` between two expressions. + + 3 │ # error, we currently parse this as `.1` followed by `foo`, just like we do with + 4 │ # `1foo` (r-lib/tree-sitter-r#190). + > 5 │ .1foo + │ ^^^ + 6 │ + + i The first expression ends here. + + 3 │ # error, we currently parse this as `.1` followed by `foo`, just like we do with + 4 │ # `1foo` (r-lib/tree-sitter-r#190). + > 5 │ .1foo + │ ^^ + 6 │ +``` + +## Dot dot i + +```r +..1 +..10 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDotDotI { + value_token: DOTDOTI@0..3 "..1" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDotDotI { + value_token: DOTDOTI@3..8 "..10" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@8..9 "" [Newline("\n")] [], +} +``` + +## Unclosed backtick [error] + +```r +`\` +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBogusExpression { + items: [ + ERROR_TOKEN@0..4 "`\\`\n" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@4..4 "" [] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unterminated backtick-quoted name. + + > 1 │ `\` + │ ^ + 2 │ + + i Reached the end of the file looking for a closing `. + + 1 │ `\` + > 2 │ + │ +``` + +## Dots + +```r +# Parses, but fails at runtime +... + +{ ... } + +# An actual base R function +...() +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDots { + value_token: DOTS@0..34 "..." [Comments("# Parses, but fails a ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@34..38 "{" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + statements: RStatementList [ + RExpressionStatement { + expression: RDots { + value_token: DOTS@38..42 "..." [] [Whitespace(" ")], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@42..43 "}" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RDots { + value_token: DOTS@43..76 "..." [Newline("\n"), Newline("\n"), Comments("# An actual base R fu ..."), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@76..77 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@77..78 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@78..79 "" [Newline("\n")] [], +} +``` + +## Dot dot i in functions and calls + +```r +function(...) { + ..1 + ..2 +} + +# Parses as a function +..1() +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@0..8 "function" [] [], + parameters: RParameters { + l_paren_token: L_PAREN@8..9 "(" [] [], + items: RParameterList [ + RParameter { + name: RDots { + value_token: DOTS@9..12 "..." [] [], + }, + default: missing (optional), + }, + ], + r_paren_token: R_PAREN@12..14 ")" [] [Whitespace(" ")], + }, + body: RBracedStatements { + l_curly_token: L_CURLY@14..15 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RDotDotI { + value_token: DOTDOTI@15..22 "..1" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: PLUS@22..24 "+" [] [Whitespace(" ")], + right: RDotDotI { + value_token: DOTDOTI@24..27 "..2" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@27..29 "}" [Newline("\n")] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RDotDotI { + value_token: DOTDOTI@29..57 "..1" [Newline("\n"), Newline("\n"), Comments("# Parses as a function"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@57..58 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@58..59 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@59..60 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/literals/numbers.md b/crates/air_r_parser_tests/tests/corpus/literals/numbers.md new file mode 100644 index 00000000..868823ca --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/literals/numbers.md @@ -0,0 +1,131 @@ +## Integers + +```r +12332L +0L +12L +1e1L +1L +1e5L +# Technically, R parses these as floats with a warning, but for our purposes this is good enough +0.1L +# r-lib/tree-sitter-r#198 +.1L +``` + +## Floats + +```r +.66 +.11 +123.4123 +.1234 +x <- -.66 +``` + +## Hexadecimal + +```r +# `x` vs `X` +0x123 +0X123 + +# Numbers and letters +0xDEAD +0XDEAD +0x1f2F3 +0X1f2F3 + +# `p` vs `P` +0x0p0 +0x0P0 +0x0p123 +0x0P123 + +# `+` and `-` +0x0p+0 +0x0p-0 +0x0p+123 +0x0p-123 + +# Decimal point +0x. +0x1. +0x.1 +0x1.1 + +# Decimal point with exponent +0x.p1 +0x1.p1 +0x.1p1 +0x1.1p1 + +# As integers +0x123L +0X123L +0xDEADL +0XDEADL +0x1f2F3L +0X1f2F3L +0x0p0L +0x0P0L +0x0p123L +0x0P123L +0x0p+0L +0x0p-0L +0x0p+123L +0x0p-123L +0x.L +0x1.L +0x.1L +0x1.1L +0x.p1L +0x1.p1L +0x.1p1L +0x1.1p1L +``` + +## Scientific notation floats + +```r +1e322 +1e-3 +1e+3 +1.8e10 +1.e10 +1e10 +``` + +## Complex + +```r +1i +0i +1.1i +# r-lib/tree-sitter-r#198 +.1i +1e10i +.1e10i +0x1i +0x.1i +``` + +## Double values + +```r +1 +2.5 +1e6 +0x123F + +# r-lib/tree-sitter-r#198 +.1 +``` + +## Complex values + +```r +2.5i +1e6i +0x123Fi +``` diff --git a/crates/air_r_parser_tests/tests/corpus/literals/numbers.snap.md b/crates/air_r_parser_tests/tests/corpus/literals/numbers.snap.md new file mode 100644 index 00000000..0f006e14 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/literals/numbers.snap.md @@ -0,0 +1,679 @@ + + +## Integers + +```r +12332L +0L +12L +1e1L +1L +1e5L +# Technically, R parses these as floats with a warning, but for our purposes this is good enough +0.1L +# r-lib/tree-sitter-r#198 +.1L +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@0..6 "12332L" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@6..9 "0L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@9..13 "12L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@13..18 "1e1L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@18..21 "1L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@21..26 "1e5L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@26..128 "0.1L" [Newline("\n"), Comments("# Technically, R pars ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@128..158 ".1L" [Newline("\n"), Comments("# r-lib/tree-sitter-r ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@158..159 "" [Newline("\n")] [], +} +``` + +## Floats + +```r +.66 +.11 +123.4123 +.1234 +x <- -.66 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..3 ".66" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@3..7 ".11" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@7..16 "123.4123" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@16..22 ".1234" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@22..25 "x" [Newline("\n")] [Whitespace(" ")], + }, + operator: ASSIGN@25..28 "<-" [] [Whitespace(" ")], + right: RUnaryExpression { + operator: MINUS@28..29 "-" [] [], + argument: RDoubleValue { + value_token: R_DOUBLE_LITERAL@29..32 ".66" [] [], + }, + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@32..33 "" [Newline("\n")] [], +} +``` + +## Hexadecimal + +```r +# `x` vs `X` +0x123 +0X123 + +# Numbers and letters +0xDEAD +0XDEAD +0x1f2F3 +0X1f2F3 + +# `p` vs `P` +0x0p0 +0x0P0 +0x0p123 +0x0P123 + +# `+` and `-` +0x0p+0 +0x0p-0 +0x0p+123 +0x0p-123 + +# Decimal point +0x. +0x1. +0x.1 +0x1.1 + +# Decimal point with exponent +0x.p1 +0x1.p1 +0x.1p1 +0x1.1p1 + +# As integers +0x123L +0X123L +0xDEADL +0XDEADL +0x1f2F3L +0X1f2F3L +0x0p0L +0x0P0L +0x0p123L +0x0P123L +0x0p+0L +0x0p-0L +0x0p+123L +0x0p-123L +0x.L +0x1.L +0x.1L +0x1.1L +0x.p1L +0x1.p1L +0x.1p1L +0x1.1p1L +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..18 "0x123" [Comments("# `x` vs `X`"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@18..24 "0X123" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@24..54 "0xDEAD" [Newline("\n"), Newline("\n"), Comments("# Numbers and letters"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@54..61 "0XDEAD" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@61..69 "0x1f2F3" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@69..77 "0X1f2F3" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@77..97 "0x0p0" [Newline("\n"), Newline("\n"), Comments("# `p` vs `P`"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@97..103 "0x0P0" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@103..111 "0x0p123" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@111..119 "0x0P123" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@119..141 "0x0p+0" [Newline("\n"), Newline("\n"), Comments("# `+` and `-`"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@141..148 "0x0p-0" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@148..157 "0x0p+123" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@157..166 "0x0p-123" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@166..187 "0x." [Newline("\n"), Newline("\n"), Comments("# Decimal point"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@187..192 "0x1." [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@192..197 "0x.1" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@197..203 "0x1.1" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@203..240 "0x.p1" [Newline("\n"), Newline("\n"), Comments("# Decimal point with ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@240..247 "0x1.p1" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@247..254 "0x.1p1" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@254..262 "0x1.1p1" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@262..284 "0x123L" [Newline("\n"), Newline("\n"), Comments("# As integers"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@284..291 "0X123L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@291..299 "0xDEADL" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@299..307 "0XDEADL" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@307..316 "0x1f2F3L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@316..325 "0X1f2F3L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@325..332 "0x0p0L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@332..339 "0x0P0L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@339..348 "0x0p123L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@348..357 "0x0P123L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@357..365 "0x0p+0L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@365..373 "0x0p-0L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@373..383 "0x0p+123L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@383..393 "0x0p-123L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@393..398 "0x.L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@398..404 "0x1.L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@404..410 "0x.1L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@410..417 "0x1.1L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@417..424 "0x.p1L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@424..432 "0x1.p1L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@432..440 "0x.1p1L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RIntegerValue { + value_token: R_INTEGER_LITERAL@440..449 "0x1.1p1L" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@449..450 "" [Newline("\n")] [], +} +``` + +## Scientific notation floats + +```r +1e322 +1e-3 +1e+3 +1.8e10 +1.e10 +1e10 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..5 "1e322" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..10 "1e-3" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@10..15 "1e+3" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@15..22 "1.8e10" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@22..28 "1.e10" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@28..33 "1e10" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@33..34 "" [Newline("\n")] [], +} +``` + +## Complex + +```r +1i +0i +1.1i +# r-lib/tree-sitter-r#198 +.1i +1e10i +.1e10i +0x1i +0x.1i +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@0..2 "1i" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@2..5 "0i" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@5..10 "1.1i" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@10..40 ".1i" [Newline("\n"), Comments("# r-lib/tree-sitter-r ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@40..46 "1e10i" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@46..53 ".1e10i" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@53..58 "0x1i" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@58..64 "0x.1i" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@64..65 "" [Newline("\n")] [], +} +``` + +## Double values + +```r +1 +2.5 +1e6 +0x123F + +# r-lib/tree-sitter-r#198 +.1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..1 "1" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@1..5 "2.5" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..9 "1e6" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@9..16 "0x123F" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@16..46 ".1" [Newline("\n"), Newline("\n"), Comments("# r-lib/tree-sitter-r ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@46..47 "" [Newline("\n")] [], +} +``` + +## Complex values + +```r +2.5i +1e6i +0x123Fi +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@0..4 "2.5i" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@4..9 "1e6i" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RComplexValue { + value_token: R_COMPLEX_LITERAL@9..17 "0x123Fi" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@17..18 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/literals/strings.md b/crates/air_r_parser_tests/tests/corpus/literals/strings.md new file mode 100644 index 00000000..1fa0c0ec --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/literals/strings.md @@ -0,0 +1,186 @@ +## Strings + +```r +"foo" +"foo +bar" +"#" +"," +"}" +'foo' +'foo +bar' +'#' +',' +'}' +``` + +## Strings without content + +```r +"" +'' +``` + +## Strings with escape sequences + +```r +"\\" +"\ " +"\n" +"\t" +"\r" + +"\0" +"\1" +"\7" +"\12" +"\123" +"\1234" + +"\x1" +"\x01" +"\x012" + +"\u1" +"\u01" +"\u001" +"\u0001" +"\u00011" + +"\u{1}" +"\u{01}" +"\u{001}" +"\u{0001}" + +"\U{1}" +"\U{01}" +"\U{001}" +"\U{0001}" +"\U{00001}" +"\U{000001}" +"\U{0000001}" +"\U{00000001}" + +"\U1" +"\U01" +"\U001" +"\U0001" +"\U00001" +"\U000001" +"\U0000001" +"\U00000001" +"\U000000011" + +"foo\"bar" +"foo\ " +"foo\ +" +"\"" +'\'' +``` + +## Raw strings + +```r +r"(raw string)" +R"{another raw string}" +R"--[yet another ]- raw string]--" + +r"(")" +r"("")" +r"(')" +r"('')" + +r"-(-)-" +r"-()-)-" +r"--()-")--" + +r"( () )" +r"(())" +r"( ())" +r"(() )" +r"-())-" +r"-(())-)-" + +r"(raw +string +)" + +# Empty raw strings +r"()" +r'()' +R"{}" +r"--[]--" + +# Leading whitespace and newlines after open +r"( hello)" +r"(\nhello)" +r"( +hello +)" +f(r"(\nhello\n)") +f(r"( +hello +)") + +# Single quoted raw strings +r'(a)' +r'(')' +r'(")' + +r() +``` + +## Raw strings with `|` + +R accepts `|` as a raw string delimiter, though `?Quotes` doesn't say so. + +```r +r"|raw string|" +R'|raw string|' +r"--|a|--" + +r"||" +r"|||" +r"|a|b|" +r"-|a|-|-" + +r"|(|" +r"(|)" +``` + +## Unclosed double quote [error] + +```r +"\" +``` + +## Unclosed single quote [error] + +```r +'\' +``` + +## Unclosed raw string [error] + +```r +r"( +" +``` + +```r +r"|" +``` + +## String values + +```r +"hi there!" +'hi there!' +"'" +"multiline +string" +r"("some raw string +business")" +``` diff --git a/crates/air_r_parser_tests/tests/corpus/literals/strings.snap.md b/crates/air_r_parser_tests/tests/corpus/literals/strings.snap.md new file mode 100644 index 00000000..882917b6 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/literals/strings.snap.md @@ -0,0 +1,1017 @@ + + +## Strings + +```r +"foo" +"foo +bar" +"#" +"," +"}" +'foo' +'foo +bar' +'#' +',' +'}' +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@0..5 "\"foo\"" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@5..15 "\"foo\nbar\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@15..19 "\"#\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@19..23 "\",\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@23..27 "\"}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_SINGLE_QUOTED_STRING_LITERAL@27..33 "'foo'" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_SINGLE_QUOTED_STRING_LITERAL@33..43 "'foo\nbar'" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_SINGLE_QUOTED_STRING_LITERAL@43..47 "'#'" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_SINGLE_QUOTED_STRING_LITERAL@47..51 "','" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_SINGLE_QUOTED_STRING_LITERAL@51..55 "'}'" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@55..56 "" [Newline("\n")] [], +} +``` + +## Strings without content + +```r +"" +'' +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@0..2 "\"\"" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_SINGLE_QUOTED_STRING_LITERAL@2..5 "''" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@5..6 "" [Newline("\n")] [], +} +``` + +## Strings with escape sequences + +```r +"\\" +"\ " +"\n" +"\t" +"\r" + +"\0" +"\1" +"\7" +"\12" +"\123" +"\1234" + +"\x1" +"\x01" +"\x012" + +"\u1" +"\u01" +"\u001" +"\u0001" +"\u00011" + +"\u{1}" +"\u{01}" +"\u{001}" +"\u{0001}" + +"\U{1}" +"\U{01}" +"\U{001}" +"\U{0001}" +"\U{00001}" +"\U{000001}" +"\U{0000001}" +"\U{00000001}" + +"\U1" +"\U01" +"\U001" +"\U0001" +"\U00001" +"\U000001" +"\U0000001" +"\U00000001" +"\U000000011" + +"foo\"bar" +"foo\ " +"foo\ +" +"\"" +'\'' +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@0..4 "\"\\\\\"" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@4..9 "\"\\ \"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@9..14 "\"\\n\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@14..19 "\"\\t\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@19..24 "\"\\r\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@24..30 "\"\\0\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@30..35 "\"\\1\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@35..40 "\"\\7\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@40..46 "\"\\12\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@46..53 "\"\\123\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@53..61 "\"\\1234\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@61..68 "\"\\x1\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@68..75 "\"\\x01\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@75..83 "\"\\x012\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@83..90 "\"\\u1\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@90..97 "\"\\u01\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@97..105 "\"\\u001\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@105..114 "\"\\u0001\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@114..124 "\"\\u00011\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@124..133 "\"\\u{1}\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@133..142 "\"\\u{01}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@142..152 "\"\\u{001}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@152..163 "\"\\u{0001}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@163..172 "\"\\U{1}\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@172..181 "\"\\U{01}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@181..191 "\"\\U{001}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@191..202 "\"\\U{0001}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@202..214 "\"\\U{00001}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@214..227 "\"\\U{000001}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@227..241 "\"\\U{0000001}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@241..256 "\"\\U{00000001}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@256..263 "\"\\U1\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@263..270 "\"\\U01\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@270..278 "\"\\U001\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@278..287 "\"\\U0001\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@287..297 "\"\\U00001\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@297..308 "\"\\U000001\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@308..320 "\"\\U0000001\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@320..333 "\"\\U00000001\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@333..347 "\"\\U000000011\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@347..359 "\"foo\\\"bar\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@359..367 "\"foo\\ \"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@367..375 "\"foo\\\n\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@375..380 "\"\\\"\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_SINGLE_QUOTED_STRING_LITERAL@380..385 "'\\''" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@385..386 "" [Newline("\n")] [], +} +``` + +## Raw strings + +```r +r"(raw string)" +R"{another raw string}" +R"--[yet another ]- raw string]--" + +r"(")" +r"("")" +r"(')" +r"('')" + +r"-(-)-" +r"-()-)-" +r"--()-")--" + +r"( () )" +r"(())" +r"( ())" +r"(() )" +r"-())-" +r"-(())-)-" + +r"(raw +string +)" + +# Empty raw strings +r"()" +r'()' +R"{}" +r"--[]--" + +# Leading whitespace and newlines after open +r"( hello)" +r"(\nhello)" +r"( +hello +)" +f(r"(\nhello\n)") +f(r"( +hello +)") + +# Single quoted raw strings +r'(a)' +r'(')' +r'(")' + +r() +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@0..15 "r\"(raw string)\"" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@15..39 "R\"{another raw string}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@39..74 "R\"--[yet another ]- raw string]--\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@74..82 "r\"(\")\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@82..90 "r\"(\"\")\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@90..97 "r\"(')\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@97..105 "r\"('')\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@105..115 "r\"-(-)-\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@115..125 "r\"-()-)-\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@125..138 "r\"--()-\")--\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@138..149 "r\"( () )\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@149..157 "r\"(())\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@157..166 "r\"( ())\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@166..175 "r\"(() )\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@175..184 "r\"-())-\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@184..196 "r\"-(())-)-\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@196..214 "r\"(raw\nstring\n)\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@214..241 "r\"()\"" [Newline("\n"), Newline("\n"), Comments("# Empty raw strings"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@241..247 "r'()'" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@247..253 "R\"{}\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@253..263 "r\"--[]--\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@263..322 "r\"( hello)\"" [Newline("\n"), Newline("\n"), Comments("# Leading whitespace ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@322..335 "r\"(\\nhello)\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@335..348 "r\"(\nhello\n)\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@348..350 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@350..351 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_RAW_STRING_LITERAL@351..365 "r\"(\\nhello\\n)\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@365..366 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@366..368 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@368..369 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RStringValue { + value: R_RAW_STRING_LITERAL@369..381 "r\"(\nhello\n)\"" [] [], + }, + }, + ], + r_paren_token: R_PAREN@381..382 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@382..418 "r'(a)'" [Newline("\n"), Newline("\n"), Comments("# Single quoted raw s ..."), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@418..425 "r'(')'" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@425..432 "r'(\")'" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@432..435 "r" [Newline("\n"), Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@435..436 "(" [] [], + items: RArgumentList [], + r_paren_token: R_PAREN@436..437 ")" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@437..438 "" [Newline("\n")] [], +} +``` + +## Raw strings with `|` + +R accepts `|` as a raw string delimiter, though `?Quotes` doesn't say so. + +```r +r"|raw string|" +R'|raw string|' +r"--|a|--" + +r"||" +r"|||" +r"|a|b|" +r"-|a|-|-" + +r"|(|" +r"(|)" +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@0..15 "r\"|raw string|\"" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@15..31 "R'|raw string|'" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@31..42 "r\"--|a|--\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@42..49 "r\"||\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@49..56 "r\"|||\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@56..65 "r\"|a|b|\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@65..76 "r\"-|a|-|-\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@76..84 "r\"|(|\"" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@84..91 "r\"(|)\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@91..92 "" [Newline("\n")] [], +} +``` + +## Unclosed double quote [error] + +```r +"\" +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBogusExpression { + items: [ + ERROR_TOKEN@0..4 "\"\\\"\n" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@4..4 "" [] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unterminated string. + + > 1 │ "\" + │ ^ + 2 │ + + i Reached the end of the file looking for a closing delimiter. + + 1 │ "\" + > 2 │ + │ +``` + +## Unclosed single quote [error] + +```r +'\' +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBogusExpression { + items: [ + ERROR_TOKEN@0..4 "'\\'\n" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@4..4 "" [] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unterminated string. + + > 1 │ '\' + │ ^ + 2 │ + + i Reached the end of the file looking for a closing delimiter. + + 1 │ '\' + > 2 │ + │ +``` + +## Unclosed raw string [error] + +```r +r"( +" +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBogusExpression { + items: [ + ERROR_TOKEN@0..6 "r\"(\n\"\n" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@6..6 "" [] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unterminated string. + + > 1 │ r"( + │ ^^^ + 2 │ " + 3 │ + + i Reached the end of the file looking for a closing delimiter. + + 1 │ r"( + 2 │ " + > 3 │ + │ +``` + +```r +r"|" +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBogusExpression { + items: [ + ERROR_TOKEN@0..5 "r\"|\"\n" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@5..5 "" [] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unterminated string. + + > 1 │ r"|" + │ ^^^ + 2 │ + + i Reached the end of the file looking for a closing delimiter. + + 1 │ r"|" + > 2 │ + │ +``` + +## String values + +```r +"hi there!" +'hi there!' +"'" +"multiline +string" +r"("some raw string +business")" +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@0..11 "\"hi there!\"" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_SINGLE_QUOTED_STRING_LITERAL@11..23 "'hi there!'" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@23..27 "\"'\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_DOUBLE_QUOTED_STRING_LITERAL@27..46 "\"multiline\nstring\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RStringValue { + value: R_RAW_STRING_LITERAL@46..78 "r\"(\"some raw string\nbusiness\")\"" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@78..79 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/recovery/unmatched-delimiters.md b/crates/air_r_parser_tests/tests/corpus/recovery/unmatched-delimiters.md new file mode 100644 index 00000000..673dd627 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/recovery/unmatched-delimiters.md @@ -0,0 +1,70 @@ +## Closing brace [error] + +```r +} +``` + +## Closing parenthesis [error] + +```r +) +``` + +## Closing bracket [error] + +```r +] +``` + +## Opening brace, closing parenthesis [error] + +```r +# Parenthesis is "not valid" so it isn't matched as a closer for the `{` +{) +``` + +## Opening parenthesis, closing brace [error] + +```r +(} +``` + +## Opening parenthesis, closing bracket [error] + +```r +(] +``` + +## Opening bracket2, unmatched closing bracket [error] + +```r +x[[2] +``` + +## Opening bracket and bracket2, unmatched closing bracket [error] + +```r +x[y[[2] +``` + +## Opening bracket2 and bracket, matched closing bracket [error] + +```r +x[[y[2] +``` + +## Opening bracket2 and bracket, unmatched closing bracket [error] + +```r +x[[y[2]] +``` + +## An argument name cut off by the end of the file [error] + +```r +f(a +``` + +```r +f(a= +``` diff --git a/crates/air_r_parser_tests/tests/corpus/recovery/unmatched-delimiters.snap.md b/crates/air_r_parser_tests/tests/corpus/recovery/unmatched-delimiters.snap.md new file mode 100644 index 00000000..28181d25 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/recovery/unmatched-delimiters.snap.md @@ -0,0 +1,595 @@ + + +## Closing brace [error] + +```r +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_CURLY@0..1 "}" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@1..2 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '}'. + + > 1 │ } + │ ^ + 2 │ +``` + +## Closing parenthesis [error] + +```r +) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_PAREN@0..1 ")" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@1..2 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ')'. + + > 1 │ ) + │ ^ + 2 │ +``` + +## Closing bracket [error] + +```r +] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_BRACK@0..1 "]" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@1..2 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ']'. + + > 1 │ ] + │ ^ + 2 │ +``` + +## Opening brace, closing parenthesis [error] + +```r +# Parenthesis is "not valid" so it isn't matched as a closer for the `{` +{) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..74 "{" [Comments("# Parenthesis is \"not ..."), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_PAREN@74..75 ")" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: missing (required), + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@75..76 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ')'. + + 1 │ # Parenthesis is "not valid" so it isn't matched as a closer for the `{` + > 2 │ {) + │ ^ + 3 │ + +test.R:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `}` but instead the file ends + + 1 │ # Parenthesis is "not valid" so it isn't matched as a closer for the `{` + 2 │ {) + > 3 │ + │ + + i the file ends here + + 1 │ # Parenthesis is "not valid" so it isn't matched as a closer for the `{` + 2 │ {) + > 3 │ + │ +``` + +## Opening parenthesis, closing brace [error] + +```r +(} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@0..1 "(" [] [], + body: missing (required), + r_paren_token: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_CURLY@1..2 "}" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@2..3 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found '}'. + + > 1 │ (} + │ ^ + 2 │ +``` + +## Opening parenthesis, closing bracket [error] + +```r +(] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@0..1 "(" [] [], + body: missing (required), + r_paren_token: missing (required), + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_BRACK@1..2 "]" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@2..3 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ']'. + + > 1 │ (] + │ ^ + 2 │ +``` + +## Opening bracket2, unmatched closing bracket [error] + +```r +x[[2] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@1..3 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@3..4 "2" [] [], + }, + }, + ], + r_brack2_token: missing (required), + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_BRACK@4..5 "]" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@5..6 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `]]` but instead found `]` + + > 1 │ x[[2] + │ ^ + 2 │ + + i Remove ] +``` + +## Opening bracket and bracket2, unmatched closing bracket [error] + +```r +x[y[[2] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@1..2 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset2 { + function: RIdentifier { + name_token: IDENT@2..3 "y" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@3..5 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..6 "2" [] [], + }, + }, + ], + r_brack2_token: missing (required), + }, + }, + }, + ], + r_brack_token: R_BRACK@6..7 "]" [] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `]]` but instead found `]` + + > 1 │ x[y[[2] + │ ^ + 2 │ + + i Remove ] +``` + +## Opening bracket2 and bracket, matched closing bracket [error] + +```r +x[[y[2] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@1..3 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset { + function: RIdentifier { + name_token: IDENT@3..4 "y" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@4..5 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..6 "2" [] [], + }, + }, + ], + r_brack_token: R_BRACK@6..7 "]" [] [], + }, + }, + }, + ], + r_brack2_token: missing (required), + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `]]` but instead the file ends + + 1 │ x[[y[2] + > 2 │ + │ + + i the file ends here + + 1 │ x[[y[2] + > 2 │ + │ +``` + +## Opening bracket2 and bracket, unmatched closing bracket [error] + +```r +x[[y[2]] +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RSubset2 { + function: RIdentifier { + name_token: IDENT@0..1 "x" [] [], + }, + arguments: RSubset2Arguments { + l_brack2_token: L_BRACK2@1..3 "[[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RSubset { + function: RIdentifier { + name_token: IDENT@3..4 "y" [] [], + }, + arguments: RSubsetArguments { + l_brack_token: L_BRACK@4..5 "[" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..6 "2" [] [], + }, + }, + ], + r_brack_token: R_BRACK@6..7 "]" [] [], + }, + }, + }, + ], + r_brack2_token: missing (required), + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBogusExpression { + items: [ + R_BRACK@7..8 "]" [] [], + ], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@8..9 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:8 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `]]` but instead found `]` + + > 1 │ x[[y[2]] + │ ^ + 2 │ + + i Remove ] +``` + +## An argument name cut off by the end of the file [error] + +```r +f(a +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..1 "f" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@1..2 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@2..3 "a" [] [], + }, + }, + ], + r_paren_token: missing (required), + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@3..4 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `)` but instead the file ends + + 1 │ f(a + > 2 │ + │ + + i the file ends here + + 1 │ f(a + > 2 │ + │ +``` + +```r +f(a= +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@0..1 "f" [] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@1..2 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: RArgumentNameClause { + name: RIdentifier { + name_token: IDENT@2..3 "a" [] [], + }, + eq_token: EQUAL@3..4 "=" [] [], + }, + value: missing (optional), + }, + ], + r_paren_token: missing (required), + }, + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@4..5 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `)` but instead the file ends + + 1 │ f(a= + > 2 │ + │ + + i the file ends here + + 1 │ f(a= + > 2 │ + │ +``` diff --git a/crates/air_r_parser/tests/snapshots/ok/braced_expressions.R b/crates/air_r_parser_tests/tests/corpus/statements/braces.md similarity index 55% rename from crates/air_r_parser/tests/snapshots/ok/braced_expressions.R rename to crates/air_r_parser_tests/tests/corpus/statements/braces.md index e5fe4a66..ecb250ee 100644 --- a/crates/air_r_parser/tests/snapshots/ok/braced_expressions.R +++ b/crates/air_r_parser_tests/tests/corpus/statements/braces.md @@ -1,10 +1,27 @@ -{ } +## Braced expressions -{ +```r +{} + +{1} + +{1; 2} + +{1; +2} + +{1 +2 +} +{ +1 +2 } -{ 1 } +{ + +} { 1 @@ -17,3 +34,4 @@ { # comment2 1 # comment3 } # comment4 +``` diff --git a/crates/air_r_parser_tests/tests/corpus/statements/braces.snap.md b/crates/air_r_parser_tests/tests/corpus/statements/braces.snap.md new file mode 100644 index 00000000..e3612fd8 --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/statements/braces.snap.md @@ -0,0 +1,217 @@ + + +## Braced expressions + +```r +{} + +{1} + +{1; 2} + +{1; +2} + +{1 +2 +} + +{ +1 +2 +} + +{ + +} + +{ + 1 + 2 + 2 + 3 + + 4 +} + +# comment1 +{ # comment2 + 1 # comment3 +} # comment4 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..1 "{" [] [], + statements: RStatementList [], + r_curly_token: R_CURLY@1..2 "}" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@2..5 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..6 "1" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@6..7 "}" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@7..10 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@10..11 "1" [] [], + }, + semicolon_token: SEMICOLON@11..13 ";" [] [Whitespace(" ")], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@13..14 "2" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@14..15 "}" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@15..18 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@18..19 "1" [] [], + }, + semicolon_token: SEMICOLON@19..20 ";" [] [], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@20..22 "2" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@22..23 "}" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@23..26 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@26..27 "1" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@27..29 "2" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@29..31 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@31..34 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@34..36 "1" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@36..38 "2" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@38..40 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@40..43 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [], + r_curly_token: R_CURLY@43..46 "}" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@46..49 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@49..53 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@53..58 "2" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: PLUS@58..60 "+" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@60..61 "2" [] [], + }, + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBinaryExpression { + left: RDoubleValue { + value_token: R_DOUBLE_LITERAL@61..66 "3" [Newline("\n"), Whitespace(" ")] [Whitespace(" ")], + }, + operator: PLUS@66..67 "+" [] [], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@67..73 "4" [Newline("\n"), Whitespace(" ")] [], + }, + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@73..75 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@75..100 "{" [Newline("\n"), Newline("\n"), Comments("# comment1"), Newline("\n")] [Whitespace(" "), Comments("# comment2")], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@100..115 "1" [Newline("\n"), Whitespace(" ")] [Whitespace(" "), Comments("# comment3")], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@115..128 "}" [Newline("\n")] [Whitespace(" "), Comments("# comment4")], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@128..129 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/statements/newlines.md b/crates/air_r_parser_tests/tests/corpus/statements/newlines.md new file mode 100644 index 00000000..3801d11e --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/statements/newlines.md @@ -0,0 +1,37 @@ +## Newlines + +```r +apple +(banana) + +{ + apple + (banana) +} + +( + apple + (banana) +) +``` + +## Two expressions on one line [error] + +```r +1 2 +``` + +## Two expressions on one line inside braces [error] + +```r +{ 1 2 } +``` + +## A comment does not hide a line break + +Lexer's `after_line_break` flag is sticky across trivia + +```r +1 # c +2 +``` diff --git a/crates/air_r_parser_tests/tests/corpus/statements/newlines.snap.md b/crates/air_r_parser_tests/tests/corpus/statements/newlines.snap.md new file mode 100644 index 00000000..007e257f --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/statements/newlines.snap.md @@ -0,0 +1,217 @@ + + +## Newlines + +```r +apple +(banana) + +{ + apple + (banana) +} + +( + apple + (banana) +) +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@0..5 "apple" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@5..7 "(" [Newline("\n")] [], + body: RIdentifier { + name_token: IDENT@7..13 "banana" [] [], + }, + r_paren_token: R_PAREN@13..14 ")" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@14..17 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RIdentifier { + name_token: IDENT@17..25 "apple" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@25..29 "(" [Newline("\n"), Whitespace(" ")] [], + body: RIdentifier { + name_token: IDENT@29..35 "banana" [] [], + }, + r_paren_token: R_PAREN@35..36 ")" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@36..38 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@38..41 "(" [Newline("\n"), Newline("\n")] [], + body: RCall { + function: RIdentifier { + name_token: IDENT@41..49 "apple" [Newline("\n"), Whitespace(" ")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@49..53 "(" [Newline("\n"), Whitespace(" ")] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RIdentifier { + name_token: IDENT@53..59 "banana" [] [], + }, + }, + ], + r_paren_token: R_PAREN@59..60 ")" [] [], + }, + }, + r_paren_token: R_PAREN@60..62 ")" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@62..63 "" [Newline("\n")] [], +} +``` + +## Two expressions on one line [error] + +```r +1 2 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..2 "1" [] [Whitespace(" ")], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@2..3 "2" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@3..4 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a newline or a `;` between two expressions. + + > 1 │ 1 2 + │ ^ + 2 │ + + i The first expression ends here. + + > 1 │ 1 2 + │ ^ + 2 │ +``` + +## Two expressions on one line inside braces [error] + +```r +{ 1 2 } +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..2 "{" [] [Whitespace(" ")], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@2..4 "1" [] [Whitespace(" ")], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@4..6 "2" [] [Whitespace(" ")], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@6..7 "}" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a newline or a `;` between two expressions. + + > 1 │ { 1 2 } + │ ^ + 2 │ + + i The first expression ends here. + + > 1 │ { 1 2 } + │ ^ + 2 │ +``` + +## A comment does not hide a line break + +Lexer's `after_line_break` flag is sticky across trivia + +```r +1 # c +2 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..5 "1" [] [Whitespace(" "), Comments("# c")], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..7 "2" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/corpus/statements/semicolons.md b/crates/air_r_parser_tests/tests/corpus/statements/semicolons.md new file mode 100644 index 00000000..d75f925b --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/statements/semicolons.md @@ -0,0 +1,134 @@ +## Semicolons + +```r +1;2 + +1 ; 2 + +{ + 1; +} + +{ + 1; 2 +} + +1;# comment +1; # comment +``` + +## Semicolon at start of file [error] + +```r +; +1 +``` + +## Semicolon at end of file + +```r +# posit-dev/air#238 +1; +``` + +## Semicolon on its own line at end of file [error] + +```r +1 + +; +``` + +## A semicolon with no expression is an error at top level [error] + +```r +; +``` + +```r +;; +``` + +```r +;1 +``` + +```r +1;; +``` + +```r +1;;2 +``` + +```r +1 +; +``` + +The comment has no expression to attach to, so it lands on the `;`. + +```r +;# comment +``` + +## A semicolon with no expression is allowed inside braces + +```r +{ + ; +} +``` + +```r +{ + ;; +} +``` + +```r +{ + ;1 +} +``` + +```r +{ + 1;; +} +``` + +```r +{ + 1 + ; +} +``` + +```r +{ + 1 + + ; +} +``` + +```r +{ + ;# comment +} +``` + +## A semicolon terminates any kind of statement + +```r +x <- 1; +f(1); +(1); +{1}; +if (TRUE) 1; +for (i in 1) i; +while (FALSE) 1; +repeat break; +function() 1; +``` diff --git a/crates/air_r_parser_tests/tests/corpus/statements/semicolons.snap.md b/crates/air_r_parser_tests/tests/corpus/statements/semicolons.snap.md new file mode 100644 index 00000000..354da76e --- /dev/null +++ b/crates/air_r_parser_tests/tests/corpus/statements/semicolons.snap.md @@ -0,0 +1,817 @@ + + +## Semicolons + +```r +1;2 + +1 ; 2 + +{ + 1; +} + +{ + 1; 2 +} + +1;# comment +1; # comment +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..1 "1" [] [], + }, + semicolon_token: SEMICOLON@1..2 ";" [] [], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@2..3 "2" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@3..7 "1" [Newline("\n"), Newline("\n")] [Whitespace(" ")], + }, + semicolon_token: SEMICOLON@7..9 ";" [] [Whitespace(" ")], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@9..10 "2" [] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@10..13 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@13..19 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: SEMICOLON@19..20 ";" [] [], + }, + ], + r_curly_token: R_CURLY@20..22 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@22..25 "{" [Newline("\n"), Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@25..31 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: SEMICOLON@31..33 ";" [] [Whitespace(" ")], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@33..34 "2" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@34..36 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@36..39 "1" [Newline("\n"), Newline("\n")] [], + }, + semicolon_token: SEMICOLON@39..49 ";" [] [Comments("# comment")], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@49..51 "1" [Newline("\n")] [], + }, + semicolon_token: SEMICOLON@51..62 ";" [] [Whitespace(" "), Comments("# comment")], + }, + ], + eof_token: EOF@62..63 "" [Newline("\n")] [], +} +``` + +## Semicolon at start of file [error] + +```r +; +1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + REmptyStatement { + semicolon_token: SEMICOLON@0..1 ";" [] [], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@1..3 "1" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@3..4 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected `;`. + + > 1 │ ; + │ ^ + 2 │ 1 + 3 │ + + i At top level, a `;` must directly follow an expression. +``` + +## Semicolon at end of file + +```r +# posit-dev/air#238 +1; +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..21 "1" [Comments("# posit-dev/air#238"), Newline("\n")] [], + }, + semicolon_token: SEMICOLON@21..22 ";" [] [], + }, + ], + eof_token: EOF@22..23 "" [Newline("\n")] [], +} +``` + +## Semicolon on its own line at end of file [error] + +```r +1 + +; +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..1 "1" [] [], + }, + semicolon_token: missing (optional), + }, + REmptyStatement { + semicolon_token: SEMICOLON@1..4 ";" [Newline("\n"), Newline("\n")] [], + }, + ], + eof_token: EOF@4..5 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected `;`. + + 1 │ 1 + 2 │ + > 3 │ ; + │ ^ + 4 │ + + i At top level, a `;` must directly follow an expression. +``` + +## A semicolon with no expression is an error at top level [error] + +```r +; +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + REmptyStatement { + semicolon_token: SEMICOLON@0..1 ";" [] [], + }, + ], + eof_token: EOF@1..2 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected `;`. + + > 1 │ ; + │ ^ + 2 │ + + i At top level, a `;` must directly follow an expression. +``` + +```r +;; +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + REmptyStatement { + semicolon_token: SEMICOLON@0..1 ";" [] [], + }, + REmptyStatement { + semicolon_token: SEMICOLON@1..2 ";" [] [], + }, + ], + eof_token: EOF@2..3 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected `;`. + + > 1 │ ;; + │ ^ + 2 │ + + i At top level, a `;` must directly follow an expression. + +test.R:1:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected `;`. + + > 1 │ ;; + │ ^ + 2 │ + + i At top level, a `;` must directly follow an expression. +``` + +```r +;1 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + REmptyStatement { + semicolon_token: SEMICOLON@0..1 ";" [] [], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@1..2 "1" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@2..3 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected `;`. + + > 1 │ ;1 + │ ^ + 2 │ + + i At top level, a `;` must directly follow an expression. +``` + +```r +1;; +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..1 "1" [] [], + }, + semicolon_token: SEMICOLON@1..2 ";" [] [], + }, + REmptyStatement { + semicolon_token: SEMICOLON@2..3 ";" [] [], + }, + ], + eof_token: EOF@3..4 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected `;`. + + > 1 │ 1;; + │ ^ + 2 │ + + i At top level, a `;` must directly follow an expression. +``` + +```r +1;;2 +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..1 "1" [] [], + }, + semicolon_token: SEMICOLON@1..2 ";" [] [], + }, + REmptyStatement { + semicolon_token: SEMICOLON@2..3 ";" [] [], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@3..4 "2" [] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@4..5 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected `;`. + + > 1 │ 1;;2 + │ ^ + 2 │ + + i At top level, a `;` must directly follow an expression. +``` + +```r +1 +; +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@0..1 "1" [] [], + }, + semicolon_token: missing (optional), + }, + REmptyStatement { + semicolon_token: SEMICOLON@1..3 ";" [Newline("\n")] [], + }, + ], + eof_token: EOF@3..4 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected `;`. + + 1 │ 1 + > 2 │ ; + │ ^ + 3 │ + + i At top level, a `;` must directly follow an expression. +``` + +The comment has no expression to attach to, so it lands on the `;`. + +```r +;# comment +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + REmptyStatement { + semicolon_token: SEMICOLON@0..10 ";" [] [Comments("# comment")], + }, + ], + eof_token: EOF@10..11 "" [Newline("\n")] [], +} +``` + +```diagnostics +test.R:1:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected `;`. + + > 1 │ ;# comment + │ ^ + 2 │ + + i At top level, a `;` must directly follow an expression. +``` + +## A semicolon with no expression is allowed inside braces + +```r +{ + ; +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..1 "{" [] [], + statements: RStatementList [ + REmptyStatement { + semicolon_token: SEMICOLON@1..5 ";" [Newline("\n"), Whitespace(" ")] [], + }, + ], + r_curly_token: R_CURLY@5..7 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@7..8 "" [Newline("\n")] [], +} +``` + +```r +{ + ;; +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..1 "{" [] [], + statements: RStatementList [ + REmptyStatement { + semicolon_token: SEMICOLON@1..5 ";" [Newline("\n"), Whitespace(" ")] [], + }, + REmptyStatement { + semicolon_token: SEMICOLON@5..6 ";" [] [], + }, + ], + r_curly_token: R_CURLY@6..8 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@8..9 "" [Newline("\n")] [], +} +``` + +```r +{ + ;1 +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..1 "{" [] [], + statements: RStatementList [ + REmptyStatement { + semicolon_token: SEMICOLON@1..5 ";" [Newline("\n"), Whitespace(" ")] [], + }, + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..6 "1" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@6..8 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@8..9 "" [Newline("\n")] [], +} +``` + +```r +{ + 1;; +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..1 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@1..5 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: SEMICOLON@5..6 ";" [] [], + }, + REmptyStatement { + semicolon_token: SEMICOLON@6..7 ";" [] [], + }, + ], + r_curly_token: R_CURLY@7..9 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@9..10 "" [Newline("\n")] [], +} +``` + +```r +{ + 1 + ; +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..1 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@1..5 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + REmptyStatement { + semicolon_token: SEMICOLON@5..9 ";" [Newline("\n"), Whitespace(" ")] [], + }, + ], + r_curly_token: R_CURLY@9..11 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@11..12 "" [Newline("\n")] [], +} +``` + +```r +{ + 1 + + ; +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..1 "{" [] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@1..5 "1" [Newline("\n"), Whitespace(" ")] [], + }, + semicolon_token: missing (optional), + }, + REmptyStatement { + semicolon_token: SEMICOLON@5..10 ";" [Newline("\n"), Newline("\n"), Whitespace(" ")] [], + }, + ], + r_curly_token: R_CURLY@10..12 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@12..13 "" [Newline("\n")] [], +} +``` + +```r +{ + ;# comment +} +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@0..1 "{" [] [], + statements: RStatementList [ + REmptyStatement { + semicolon_token: SEMICOLON@1..14 ";" [Newline("\n"), Whitespace(" ")] [Comments("# comment")], + }, + ], + r_curly_token: R_CURLY@14..16 "}" [Newline("\n")] [], + }, + semicolon_token: missing (optional), + }, + ], + eof_token: EOF@16..17 "" [Newline("\n")] [], +} +``` + +## A semicolon terminates any kind of statement + +```r +x <- 1; +f(1); +(1); +{1}; +if (TRUE) 1; +for (i in 1) i; +while (FALSE) 1; +repeat break; +function() 1; +``` + +```cst +RRoot { + bom_token: missing (optional), + statements: RStatementList [ + RExpressionStatement { + expression: RBinaryExpression { + left: RIdentifier { + name_token: IDENT@0..2 "x" [] [Whitespace(" ")], + }, + operator: ASSIGN@2..5 "<-" [] [Whitespace(" ")], + right: RDoubleValue { + value_token: R_DOUBLE_LITERAL@5..6 "1" [] [], + }, + }, + semicolon_token: SEMICOLON@6..7 ";" [] [], + }, + RExpressionStatement { + expression: RCall { + function: RIdentifier { + name_token: IDENT@7..9 "f" [Newline("\n")] [], + }, + arguments: RCallArguments { + l_paren_token: L_PAREN@9..10 "(" [] [], + items: RArgumentList [ + RArgument { + name_clause: missing (optional), + value: RDoubleValue { + value_token: R_DOUBLE_LITERAL@10..11 "1" [] [], + }, + }, + ], + r_paren_token: R_PAREN@11..12 ")" [] [], + }, + }, + semicolon_token: SEMICOLON@12..13 ";" [] [], + }, + RExpressionStatement { + expression: RParenthesizedExpression { + l_paren_token: L_PAREN@13..15 "(" [Newline("\n")] [], + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@15..16 "1" [] [], + }, + r_paren_token: R_PAREN@16..17 ")" [] [], + }, + semicolon_token: SEMICOLON@17..18 ";" [] [], + }, + RExpressionStatement { + expression: RBracedStatements { + l_curly_token: L_CURLY@18..20 "{" [Newline("\n")] [], + statements: RStatementList [ + RExpressionStatement { + expression: RDoubleValue { + value_token: R_DOUBLE_LITERAL@20..21 "1" [] [], + }, + semicolon_token: missing (optional), + }, + ], + r_curly_token: R_CURLY@21..22 "}" [] [], + }, + semicolon_token: SEMICOLON@22..23 ";" [] [], + }, + RExpressionStatement { + expression: RIfStatement { + if_token: IF_KW@23..27 "if" [Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@27..28 "(" [] [], + condition: RTrueExpression { + true_token: TRUE_KW@28..32 "TRUE" [] [], + }, + r_paren_token: R_PAREN@32..34 ")" [] [Whitespace(" ")], + consequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@34..35 "1" [] [], + }, + else_clause: missing (optional), + }, + semicolon_token: SEMICOLON@35..36 ";" [] [], + }, + RExpressionStatement { + expression: RForStatement { + for_token: FOR_KW@36..41 "for" [Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@41..42 "(" [] [], + variable: RIdentifier { + name_token: IDENT@42..44 "i" [] [Whitespace(" ")], + }, + in_token: IN_KW@44..47 "in" [] [Whitespace(" ")], + sequence: RDoubleValue { + value_token: R_DOUBLE_LITERAL@47..48 "1" [] [], + }, + r_paren_token: R_PAREN@48..50 ")" [] [Whitespace(" ")], + body: RIdentifier { + name_token: IDENT@50..51 "i" [] [], + }, + }, + semicolon_token: SEMICOLON@51..52 ";" [] [], + }, + RExpressionStatement { + expression: RWhileStatement { + while_token: WHILE_KW@52..59 "while" [Newline("\n")] [Whitespace(" ")], + l_paren_token: L_PAREN@59..60 "(" [] [], + condition: RFalseExpression { + false_token: FALSE_KW@60..65 "FALSE" [] [], + }, + r_paren_token: R_PAREN@65..67 ")" [] [Whitespace(" ")], + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@67..68 "1" [] [], + }, + }, + semicolon_token: SEMICOLON@68..69 ";" [] [], + }, + RExpressionStatement { + expression: RRepeatStatement { + repeat_token: REPEAT_KW@69..77 "repeat" [Newline("\n")] [Whitespace(" ")], + body: RBreakExpression { + break_token: BREAK_KW@77..82 "break" [] [], + }, + }, + semicolon_token: SEMICOLON@82..83 ";" [] [], + }, + RExpressionStatement { + expression: RFunctionDefinition { + name: FUNCTION_KW@83..92 "function" [Newline("\n")] [], + parameters: RParameters { + l_paren_token: L_PAREN@92..93 "(" [] [], + items: RParameterList [], + r_paren_token: R_PAREN@93..95 ")" [] [Whitespace(" ")], + }, + body: RDoubleValue { + value_token: R_DOUBLE_LITERAL@95..96 "1" [] [], + }, + }, + semicolon_token: SEMICOLON@96..97 ";" [] [], + }, + ], + eof_token: EOF@97..98 "" [Newline("\n")] [], +} +``` diff --git a/crates/air_r_parser_tests/tests/main.rs b/crates/air_r_parser_tests/tests/main.rs new file mode 100644 index 00000000..ae36c2c8 --- /dev/null +++ b/crates/air_r_parser_tests/tests/main.rs @@ -0,0 +1,6 @@ +//! The corpus. One `#[test]` per input file, so `nextest` filters read as +//! `corpus::expressions::extract`. + +mod corpus { + air_r_test_macros::gen_corpus_tests!("tests/corpus", air_r_parser_tests::run_file); +} diff --git a/crates/air_r_parser_tests/tests/quick_test.rs b/crates/air_r_parser_tests/tests/quick_test.rs new file mode 100644 index 00000000..240b66e0 --- /dev/null +++ b/crates/air_r_parser_tests/tests/quick_test.rs @@ -0,0 +1,20 @@ +//! Scratchpad. Run it with `just test-quick-parser`. + +use air_r_parser::RParserOptions; +use air_r_parser::parse; + +#[ignore] +#[test] +fn quick_test() { + let source = r#" +1$foo +1$2 +"#; + + let parsed = parse(source, RParserOptions::default()); + println!("{:#?}", parsed.tree()); + + for diagnostic in parsed.diagnostics() { + println!("{diagnostic:?}"); + } +} diff --git a/crates/air_r_syntax/src/lib.rs b/crates/air_r_syntax/src/lib.rs index 23e78a88..3dc12ba2 100644 --- a/crates/air_r_syntax/src/lib.rs +++ b/crates/air_r_syntax/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] mod generated; pub mod argument_ext; +pub mod string_ext; mod syntax_node; pub use self::generated::*; @@ -8,6 +9,7 @@ pub use biome_rowan::TextLen; pub use biome_rowan::TextRange; pub use biome_rowan::TextSize; pub use biome_rowan::TokenAtOffset; +pub use biome_rowan::TokenText; pub use biome_rowan::TriviaPieceKind; pub use biome_rowan::WalkEvent; pub use syntax_node::*; diff --git a/crates/air_r_syntax/src/string_ext.rs b/crates/air_r_syntax/src/string_ext.rs new file mode 100644 index 00000000..a9f827a5 --- /dev/null +++ b/crates/air_r_syntax/src/string_ext.rs @@ -0,0 +1,153 @@ +use crate::RStringValue; +use crate::RSyntaxKind::R_DOUBLE_QUOTED_STRING_LITERAL; +use crate::RSyntaxKind::R_RAW_STRING_LITERAL; +use crate::RSyntaxKind::R_SINGLE_QUOTED_STRING_LITERAL; +use crate::RSyntaxToken; +use biome_rowan::SyntaxResult; +use biome_rowan::TextRange; +use biome_rowan::TextSize; +use biome_rowan::TokenText; + +impl RStringValue { + /// The text between the delimiters + /// + /// `"foo"` and `r"---(foo)---"` both give `foo` + pub fn inner_string_text(&self) -> SyntaxResult { + let token = self.value()?; + Ok(token.token_text_trimmed().slice(inner_string_range(&token))) + } +} + +/// Range of the inner text +/// +/// # Safety +/// +/// String literals always carry both of their delimiters. Broken strings are +/// returned from the lexer as `ERROR_TOKEN`s, making this safe on any string we +/// see. +fn inner_string_range(token: &RSyntaxToken) -> TextRange { + let text = token.text_trimmed(); + + let (open, close) = match token.kind() { + R_SINGLE_QUOTED_STRING_LITERAL | R_DOUBLE_QUOTED_STRING_LITERAL => (1, 1), + // `r`, the quote, the hyphens, and the bracket + R_RAW_STRING_LITERAL => { + let hyphens = text.as_bytes()[2..] + .iter() + .take_while(|byte| **byte == b'-') + .count(); + (3 + hyphens, 2 + hyphens) + } + kind => unreachable!("`{kind:?}` is not a string"), + }; + + TextRange::new( + TextSize::from(open as u32), + TextSize::from((text.len() - close) as u32), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::RSyntaxKind; + use crate::RSyntaxKind::R_STRING_VALUE; + use crate::RSyntaxNode; + use biome_rowan::AstNode; + use biome_rowan::TriviaPiece; + + /// The parser cannot be reached from here, so the token is built by hand. + /// `lexer/tests/strings.rs` is what pins that the lexer really produces it. + fn string_value(kind: RSyntaxKind, text: &str, leading: usize) -> RStringValue { + let leading = (leading > 0).then(|| TriviaPiece::whitespace(leading as u32)); + let token = RSyntaxToken::new_detached(kind, text, leading, []); + let node = RSyntaxNode::new_detached(R_STRING_VALUE, [Some(token.into())]); + + RStringValue::cast(node).expect("a string value") + } + + #[track_caller] + fn assert_inner(kind: RSyntaxKind, text: &str, expected: &str, range: TextRange) { + let value = string_value(kind, text, 0); + + assert_eq!(value.inner_string_text().unwrap().text(), expected); + assert_eq!(inner_string_range(&value.value().unwrap()), range); + assert_eq!(&text[range], expected); + } + + fn range(start: u32, end: u32) -> TextRange { + TextRange::new(TextSize::from(start), TextSize::from(end)) + } + + #[test] + fn quoted_strings() { + assert_inner( + R_DOUBLE_QUOTED_STRING_LITERAL, + "\"foo\"", + "foo", + range(1, 4), + ); + assert_inner(R_SINGLE_QUOTED_STRING_LITERAL, "'foo'", "foo", range(1, 4)); + } + + #[test] + fn empty_strings() { + assert_inner(R_SINGLE_QUOTED_STRING_LITERAL, "''", "", range(1, 1)); + assert_inner(R_DOUBLE_QUOTED_STRING_LITERAL, "\"\"", "", range(1, 1)); + assert_inner(R_RAW_STRING_LITERAL, "r\"()\"", "", range(3, 3)); + } + + #[test] + fn raw_strings() { + assert_inner(R_RAW_STRING_LITERAL, "r\"(a)\"", "a", range(3, 4)); + assert_inner(R_RAW_STRING_LITERAL, "r\"---(a)---\"", "a", range(6, 7)); + assert_inner(R_RAW_STRING_LITERAL, "R'--[a]--'", "a", range(5, 6)); + assert_inner(R_RAW_STRING_LITERAL, "r\"-|a|-\"", "a", range(4, 5)); + } + + /// A closing bracket that does not close belongs to the content + #[test] + fn a_raw_string_containing_its_own_bracket() { + assert_inner(R_RAW_STRING_LITERAL, "r\"(())\"", "()", range(3, 5)); + assert_inner(R_RAW_STRING_LITERAL, "r\"|a|b|\"", "a|b", range(3, 6)); + } + + #[test] + fn multiline_strings() { + assert_inner( + R_DOUBLE_QUOTED_STRING_LITERAL, + "\"a\nb\"", + "a\nb", + range(1, 4), + ); + assert_inner(R_RAW_STRING_LITERAL, "r\"(a\nb)\"", "a\nb", range(3, 6)); + } + + /// Escapes are handed back exactly as written + #[test] + fn nothing_is_unescaped() { + assert_inner( + R_DOUBLE_QUOTED_STRING_LITERAL, + "\"a\\nb\"", + "a\\nb", + range(1, 5), + ); + assert_inner( + R_DOUBLE_QUOTED_STRING_LITERAL, + "\"a\\\"b\"", + "a\\\"b", + range(1, 5), + ); + } + + /// The range is off the trimmed token, so leading trivia does not shift it + #[test] + fn trivia_is_not_content() { + let value = string_value(R_DOUBLE_QUOTED_STRING_LITERAL, " \"foo\"", 2); + let token = value.value().unwrap(); + + assert_eq!(token.text_trimmed(), "\"foo\""); + assert_eq!(value.inner_string_text().unwrap().text(), "foo"); + assert_eq!(inner_string_range(&token), range(1, 4)); + } +} diff --git a/crates/air_r_syntax/src/syntax_node.rs b/crates/air_r_syntax/src/syntax_node.rs index ddcce232..4ae311c5 100644 --- a/crates/air_r_syntax/src/syntax_node.rs +++ b/crates/air_r_syntax/src/syntax_node.rs @@ -5,7 +5,8 @@ //! //! This is a simple wrapper around the `rowan` crate which does most of the heavy lifting and is language agnostic. -use crate::{RRoot, RSyntaxKind}; +use crate::RRoot; +use crate::RSyntaxKind; use biome_rowan::Language; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] diff --git a/crates/air_r_test_macros/Cargo.toml b/crates/air_r_test_macros/Cargo.toml new file mode 100644 index 00000000..1eedb51e --- /dev/null +++ b/crates/air_r_test_macros/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "air_r_test_macros" +version = "0.0.0" +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +description = "Test generation macros for the R corpus" +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } + +[lints] +workspace = true diff --git a/crates/air_r_test_macros/src/lib.rs b/crates/air_r_test_macros/src/lib.rs new file mode 100644 index 00000000..30201470 --- /dev/null +++ b/crates/air_r_test_macros/src/lib.rs @@ -0,0 +1,153 @@ +use proc_macro::TokenStream; +use proc_macro2::Ident; +use proc_macro2::Span; +use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::TokenTree; +use quote::quote; +use std::fs; +use std::path::Path; +use std::path::PathBuf; + +/// Expands to one `#[test] fn` per `*.md` under `directory` +/// +/// ```ignore +/// gen_corpus_tests!("tests/corpus", crate::run_file); +/// ``` +#[proc_macro] +pub fn gen_corpus_tests(input: TokenStream) -> TokenStream { + let (directory, runner) = parse_arguments(input.into()); + + // Cargo sets this to the manifest directory of the crate being compiled, + // which is the caller rather than this crate + let manifest = std::env::var("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR"); + let root = PathBuf::from(manifest).join(directory); + + let mut module = Module::default(); + collect(&root, &mut module); + module.generate(&runner).into() +} + +fn parse_arguments(input: TokenStream2) -> (String, TokenStream2) { + let mut tokens = input.into_iter(); + + let directory = match tokens.next() { + Some(TokenTree::Literal(literal)) => literal.to_string().trim_matches('"').to_string(), + _ => panic!("expected a string literal naming the corpus directory"), + }; + + match tokens.next() { + Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => {} + _ => panic!("expected `,` after the corpus directory"), + } + + (directory, tokens.collect()) +} + +#[derive(Default)] +struct Module { + children: Vec<(String, Module)>, + tests: Vec<(String, String)>, +} + +impl Module { + fn generate(self, runner: &TokenStream2) -> TokenStream2 { + let modules = self.children.into_iter().map(|(name, child)| { + let name = Ident::new(&name, Span::call_site()); + let body = child.generate(runner); + quote! { mod #name { #body } } + }); + + let tests = self.tests.into_iter().map(|(name, path)| { + let name = Ident::new(&name, Span::call_site()); + quote! { + #[test] + fn #name() { + #runner(std::path::Path::new(#path)); + } + } + }); + + quote! { #(#modules)* #(#tests)* } + } +} + +fn collect(directory: &Path, module: &mut Module) { + let mut entries: Vec = fs::read_dir(directory) + .unwrap_or_else(|err| panic!("cannot read {}: {err}", directory.display())) + .map(|entry| entry.expect("cannot read directory entry").path()) + .collect(); + + // read_dir() order is not stable across filesystems + entries.sort(); + + for path in entries { + let name = path + .file_name() + .and_then(|name| name.to_str()) + .expect("corpus paths are UTF-8"); + + if path.is_dir() { + let mut child = Module::default(); + let name = identifier(name); + collect(&path, &mut child); + module.children.push((name, child)); + } else if name.ends_with(".md") && !name.ends_with(".snap.md") { + let test = identifier(name.trim_end_matches(".md")); + module.tests.push((test, path.display().to_string())); + } + } +} + +/// A file or directory name as a Rust identifier +fn identifier(name: &str) -> String { + let mut identifier: String = name + .chars() + .map(|c| if c.is_alphanumeric() { c } else { '_' }) + .collect(); + + if identifier.starts_with(|c: char| c.is_ascii_digit()) || is_keyword(&identifier) { + identifier.push('_'); + } + + identifier +} + +fn is_keyword(name: &str) -> bool { + matches!( + name, + "as" | "break" + | "const" + | "continue" + | "crate" + | "dyn" + | "else" + | "enum" + | "extern" + | "false" + | "fn" + | "for" + | "if" + | "impl" + | "in" + | "let" + | "loop" + | "match" + | "mod" + | "move" + | "mut" + | "pub" + | "ref" + | "return" + | "self" + | "static" + | "struct" + | "super" + | "trait" + | "true" + | "type" + | "unsafe" + | "use" + | "where" + | "while" + ) +} diff --git a/crates/biome_ungrammar/Cargo.lock b/crates/biome_ungrammar/Cargo.lock deleted file mode 100644 index 9ee01fa8..00000000 --- a/crates/biome_ungrammar/Cargo.lock +++ /dev/null @@ -1,21 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "ungrammar" -version = "1.16.1" - -[[package]] -name = "ungrammar2json" -version = "1.0.0" -dependencies = [ - "ungrammar", - "write-json", -] - -[[package]] -name = "write-json" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f6174b2566cc4a74f95e1367ec343e7fa80c93cc8087f5c4a3d6a1088b2118" diff --git a/crates/crates/src/snapshots/crates__tests__crate_names.snap b/crates/crates/src/snapshots/crates__tests__crate_names.snap index 30b3240c..a767380e 100644 --- a/crates/crates/src/snapshots/crates__tests__crate_names.snap +++ b/crates/crates/src/snapshots/crates__tests__crate_names.snap @@ -9,7 +9,9 @@ expression: AIR_CRATE_NAMES "air_r_factory", "air_r_formatter", "air_r_parser", + "air_r_parser_tests", "air_r_syntax", + "air_r_test_macros", "biome_ungrammar", "comments", "crates", diff --git a/plans/aether.md b/plans/aether.md index 629d8d4f..d1c206b8 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -96,9 +96,10 @@ The swap PR adds two more checks to the same workflow. ### The swap PR -Each bullet is roughly one commit. +Each bullet is roughly one commit. Each bullet adds the workspace dependencies +it needs, so every commit builds. -- [ ] Copy aether's crates in, renaming `aether` to `air` in crate names, +- [x] Copy aether's crates in, renaming `aether` to `air` in crate names, paths, idents, and descriptions: - `aether_r_parser` replaces `crates/air_r_parser` wholesale, including deleting `treesitter.rs`, the old spec harness, and the 78 fixtures under @@ -108,21 +109,25 @@ Each bullet is roughly one commit. - `aether_r_parser_tests` arrives as `crates/air_r_parser_tests`. - `aether_r_test_macros` arrives as `crates/air_r_test_macros`. - `biome_ungrammar` replaces air's copy, dropping the stray `Cargo.lock`. + - Add `air_r_parser_tests`, `air_r_test_macros`, `biome_console`, + `biome_diagnostics`, `similar`, and `unicode-ident` to the workspace + dependencies. + - Update the `crates/crates` crate list snapshot, which gains + `air_r_parser_tests` and `air_r_test_macros`. - [ ] `xtask`: aether's `xtask` replaces air's, renamed. Add `"xtask"` to - workspace `members`, since it now has a binary. + workspace `members`, since it now has a binary. Add `bpaf` to the workspace + dependencies. - [ ] `xtask/codegen`: start from aether's crate, then re-add air's `formatter.rs` unchanged (`git2` included), the `Formatter` and `All` commands, and `formatter_crate_name()`. Run `just gen-grammar` and `just gen-formatter`. Both should produce no diff. -- [ ] Workspace `Cargo.toml`: - - Add `air_r_parser_tests`, `air_r_test_macros`, `biome_console`, - `biome_diagnostics`, `bpaf`, `similar`, and `unicode-ident`. +- [ ] Workspace `Cargo.toml`, for what the earlier bullets didn't need: - Give `serde` the `derive` feature, as aether does. - - Remove `tree-sitter` and `tree-sitter-r`, plus `biome_unicode_table` if - nothing else uses it. + - Remove `tree-sitter`, `tree-sitter-r`, and `biome_unicode_table`, which + nothing uses after the copy. - [ ] `.config/nextest.toml`: take aether's, verbatim. @@ -154,8 +159,9 @@ Each bullet is roughly one commit. - [ ] Range formatting across `;` in the LSP tests. -- [ ] Snapshots: the `crates/crates` crate list and the LSP `document_syntax` - snapshots. +- [ ] Snapshots: the LSP `document_syntax` snapshots. They pass unchanged + after the copy, so this is only a check that the later bullets leave them + alone. - [ ] `CLAUDE.md`: - Root: update the crate layout, say that the parser is hand-written, and From 369d96ba0f7c2d06e405055690a7567b42baf551 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 12:26:27 -0400 Subject: [PATCH 02/20] Pull over accept --- Cargo.lock | 2 ++ Cargo.toml | 3 ++- xtask/Cargo.toml | 2 ++ xtask/src/accept.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++ xtask/src/lib.rs | 30 ++++++++++++++++++++------- xtask/src/main.rs | 9 ++++++++ 6 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 xtask/src/accept.rs create mode 100644 xtask/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 400f0647..fce7f359 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2793,7 +2793,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" name = "xtask" version = "0.0.0" dependencies = [ + "air_r_parser_tests", "anyhow", + "bpaf", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6f50e4d0..a5aa2673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["crates/*", "editors/zed", "xtask/codegen", "xtask/json_schema"] +members = ["crates/*", "editors/zed", "xtask", "xtask/codegen", "xtask/json_schema"] [workspace.package] authors = ["Posit Software, PBC"] @@ -33,6 +33,7 @@ biome_string_case = { git = "https://github.com/lionel-/biome", rev = "a1296ea6b biome_text_size = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } biome_ungrammar = { path = "./crates/biome_ungrammar" } biome_unicode_table = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } +bpaf = { version = "0.9.15", features = ["derive"] } bytes = "1.8.0" cargo_metadata = "0.20.0" case = "1.0.0" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 650e6343..6edbb881 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -5,7 +5,9 @@ edition = "2021" publish = false [dependencies] +air_r_parser_tests = { workspace = true } anyhow = { workspace = true } +bpaf = { workspace = true } [lints] workspace = true diff --git a/xtask/src/accept.rs b/xtask/src/accept.rs new file mode 100644 index 00000000..979e2e52 --- /dev/null +++ b/xtask/src/accept.rs @@ -0,0 +1,50 @@ +use crate::Result; +use air_r_parser_tests::{check, corpus, Document}; +use std::fs; + +pub fn accept() -> Result<()> { + for input_path in corpus::input_files() { + let display = corpus::relative(&input_path); + let mut document = Document::parse(&fs::read_to_string(&input_path)?) + .map_err(|err| crate::anyhow!("{display}: {err}"))?; + + for section in &mut document.sections { + let expectation = section.expectation; + for block in section.blocks_mut() { + // On failure write what we can and let the test run report it: + // a regeneration should be maximally informative + block.generated = check(block, expectation).unwrap_or_default(); + } + } + + let snapshot_path = corpus::snapshot_of(&input_path); + let rendered = document.render_snapshot(); + + if fs::read_to_string(&snapshot_path).ok().as_deref() != Some(&rendered) { + fs::write(&snapshot_path, rendered)?; + println!("updated {}", corpus::relative(&snapshot_path)); + } + } + + remove_orphan_snapshots() +} + +/// A deleted input file leaves its snapshot behind +fn remove_orphan_snapshots() -> Result<()> { + for snapshot in corpus::snapshot_files() { + let name = snapshot + .file_name() + .and_then(|name| name.to_str()) + .expect("corpus paths are UTF-8"); + + if !snapshot + .with_file_name(name.replace(".snap.md", ".md")) + .exists() + { + fs::remove_file(&snapshot)?; + println!("removed orphan {}", corpus::relative(&snapshot)); + } + } + + Ok(()) +} diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs index 39790ced..d02c4817 100644 --- a/xtask/src/lib.rs +++ b/xtask/src/lib.rs @@ -1,13 +1,23 @@ //! Codegen tools mostly used to generate ast and syntax definitions. Adapted from rust analyzer's codegen +pub mod accept; pub mod glue; +use bpaf::Bpaf; use std::{ env, fmt::Display, path::{Path, PathBuf}, }; +#[derive(Debug, Clone, Bpaf)] +#[bpaf(options)] +pub enum TaskCommand { + /// Regenerates every corpus snapshot + #[bpaf(command)] + Accept, +} + pub use crate::glue::{pushd, pushenv}; pub use anyhow::{anyhow, bail, ensure, Context as _, Error, Result}; @@ -19,13 +29,19 @@ pub enum Mode { } pub fn project_root() -> PathBuf { - Path::new( - &env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()), - ) - .ancestors() - .nth(2) - .unwrap() - .to_path_buf() + let manifest_dir = + env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()); + + // Searched rather than counted, because `xtask` and `xtask/codegen` sit at + // different depths + Path::new(&manifest_dir) + .ancestors() + .find(|directory| { + std::fs::read_to_string(directory.join("Cargo.toml")) + .is_ok_and(|manifest| manifest.contains("[workspace]")) + }) + .expect("a crate in this workspace runs this") + .to_path_buf() } pub fn reformat(text: impl Display) -> Result { diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..00a80826 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,9 @@ +use xtask::{accept, project_root, pushd, Result}; + +fn main() -> Result<()> { + let _d = pushd(project_root()); + + match xtask::task_command().fallback_to_usage().run() { + xtask::TaskCommand::Accept => accept::accept(), + } +} From 4a1335cb53ddc13f5c1f7a9d73638a39b9efc33e Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 12:32:23 -0400 Subject: [PATCH 03/20] Rework as more native `xtask/accept/` subcrate Matching our existing tasks --- Cargo.lock | 10 ++++++++-- Cargo.toml | 3 +-- plans/aether.md | 19 ++++++++++++------- xtask/Cargo.toml | 2 -- xtask/accept/Cargo.toml | 12 ++++++++++++ xtask/{src/accept.rs => accept/src/main.rs} | 20 ++++++++++++++++---- xtask/src/lib.rs | 13 +------------ xtask/src/main.rs | 9 --------- 8 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 xtask/accept/Cargo.toml rename xtask/{src/accept.rs => accept/src/main.rs} (79%) delete mode 100644 xtask/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index fce7f359..63de1943 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2793,9 +2793,15 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" name = "xtask" version = "0.0.0" dependencies = [ - "air_r_parser_tests", "anyhow", - "bpaf", +] + +[[package]] +name = "xtask_accept" +version = "0.0.0" +dependencies = [ + "air_r_parser_tests", + "xtask", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a5aa2673..b573a0ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["crates/*", "editors/zed", "xtask", "xtask/codegen", "xtask/json_schema"] +members = ["crates/*", "editors/zed", "xtask/accept", "xtask/codegen", "xtask/json_schema"] [workspace.package] authors = ["Posit Software, PBC"] @@ -33,7 +33,6 @@ biome_string_case = { git = "https://github.com/lionel-/biome", rev = "a1296ea6b biome_text_size = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } biome_ungrammar = { path = "./crates/biome_ungrammar" } biome_unicode_table = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } -bpaf = { version = "0.9.15", features = ["derive"] } bytes = "1.8.0" cargo_metadata = "0.20.0" case = "1.0.0" diff --git a/plans/aether.md b/plans/aether.md index d1c206b8..a1011298 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -115,14 +115,16 @@ it needs, so every commit builds. - Update the `crates/crates` crate list snapshot, which gains `air_r_parser_tests` and `air_r_test_macros`. -- [ ] `xtask`: aether's `xtask` replaces air's, renamed. Add `"xtask"` to - workspace `members`, since it now has a binary. Add `bpaf` to the workspace - dependencies. +- [x] `xtask`: take aether's `project_root()`, which searches for + `[workspace]`. Aether's `accept` becomes its own crate, `xtask/accept` + (`xtask_accept`), mirroring `xtask/codegen` and `xtask/json_schema`. Add + `"xtask/accept"` to workspace `members`. - [ ] `xtask/codegen`: start from aether's crate, then re-add air's `formatter.rs` unchanged (`git2` included), the `Formatter` and `All` - commands, and `formatter_crate_name()`. Run `just gen-grammar` and - `just gen-formatter`. Both should produce no diff. + commands, and `formatter_crate_name()`. Add `bpaf` to the workspace + dependencies, as aether's codegen uses it from there. Run `just gen-grammar` + and `just gen-formatter`. Both should produce no diff. - [ ] Workspace `Cargo.toml`, for what the earlier bullets didn't need: - Give `serde` the `derive` feature, as aether does. @@ -134,13 +136,13 @@ it needs, so every commit builds. - [ ] `justfile`: - `gen-grammar` stays, and runs the merged codegen. - Aether's `test` is dropped. Air's `test *ARGS` stays. - - Aether's `accept` is added as-is. + - Aether's `accept` is added, running `cargo run -p xtask_accept`. - Aether's `quick` is added as `test-quick-parser`. - Air's `test-quick` becomes `test-quick-formatter`. - [ ] `test-health.yml`: add aether's two checks, which are `just gen-grammar && git diff --exit-code` and - `cargo run -p xtask -- accept && git diff --exit-code`. + `cargo run -p xtask_accept && git diff --exit-code`. - [ ] Formatter specs for semicolons, replacing the hand-built `TODO(aether)` tests in `r/auxiliary/empty_statement.rs`. New `statements/semicolons.R` @@ -200,6 +202,9 @@ it needs, so every commit builds. - `formatter.rs` comes over unchanged, `git2` and all. +- `accept` lives in its own `xtask_accept` crate rather than in `xtask`, so + `xtask_codegen` doesn't build the parser it generates code for. + - Aether's `.config/nextest.toml` is taken verbatim, including the 3×10s terminate. Any LSP test that trips it gets fixed. diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 6edbb881..650e6343 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -5,9 +5,7 @@ edition = "2021" publish = false [dependencies] -air_r_parser_tests = { workspace = true } anyhow = { workspace = true } -bpaf = { workspace = true } [lints] workspace = true diff --git a/xtask/accept/Cargo.toml b/xtask/accept/Cargo.toml new file mode 100644 index 00000000..6cd4c4c9 --- /dev/null +++ b/xtask/accept/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xtask_accept" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +air_r_parser_tests = { workspace = true } +xtask = { version = "0.0", path = "../" } + +[lints] +workspace = true diff --git a/xtask/src/accept.rs b/xtask/accept/src/main.rs similarity index 79% rename from xtask/src/accept.rs rename to xtask/accept/src/main.rs index 979e2e52..e4aa617f 100644 --- a/xtask/src/accept.rs +++ b/xtask/accept/src/main.rs @@ -1,12 +1,24 @@ -use crate::Result; -use air_r_parser_tests::{check, corpus, Document}; use std::fs; -pub fn accept() -> Result<()> { +use air_r_parser_tests::check; +use air_r_parser_tests::corpus; +use air_r_parser_tests::Document; +use xtask::anyhow; +use xtask::project_root; +use xtask::pushd; +use xtask::Result; + +fn main() -> Result<()> { + let _d = pushd(project_root()); + accept() +} + +/// Regenerates every corpus snapshot +fn accept() -> Result<()> { for input_path in corpus::input_files() { let display = corpus::relative(&input_path); let mut document = Document::parse(&fs::read_to_string(&input_path)?) - .map_err(|err| crate::anyhow!("{display}: {err}"))?; + .map_err(|err| anyhow!("{display}: {err}"))?; for section in &mut document.sections { let expectation = section.expectation; diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs index d02c4817..c28158d4 100644 --- a/xtask/src/lib.rs +++ b/xtask/src/lib.rs @@ -1,23 +1,13 @@ //! Codegen tools mostly used to generate ast and syntax definitions. Adapted from rust analyzer's codegen -pub mod accept; pub mod glue; -use bpaf::Bpaf; use std::{ env, fmt::Display, path::{Path, PathBuf}, }; -#[derive(Debug, Clone, Bpaf)] -#[bpaf(options)] -pub enum TaskCommand { - /// Regenerates every corpus snapshot - #[bpaf(command)] - Accept, -} - pub use crate::glue::{pushd, pushenv}; pub use anyhow::{anyhow, bail, ensure, Context as _, Error, Result}; @@ -32,8 +22,7 @@ pub fn project_root() -> PathBuf { let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()); - // Searched rather than counted, because `xtask` and `xtask/codegen` sit at - // different depths + // Searched rather than counted, so it holds wherever the calling crate sits Path::new(&manifest_dir) .ancestors() .find(|directory| { diff --git a/xtask/src/main.rs b/xtask/src/main.rs deleted file mode 100644 index 00a80826..00000000 --- a/xtask/src/main.rs +++ /dev/null @@ -1,9 +0,0 @@ -use xtask::{accept, project_root, pushd, Result}; - -fn main() -> Result<()> { - let _d = pushd(project_root()); - - match xtask::task_command().fallback_to_usage().run() { - xtask::TaskCommand::Accept => accept::accept(), - } -} From 63196e1f57586374f91653586cdf4b320a30331f Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 12:35:58 -0400 Subject: [PATCH 04/20] Move bpaf up into workspace `Cargo.toml` --- Cargo.toml | 1 + plans/aether.md | 2 +- xtask/codegen/Cargo.toml | 2 +- xtask/codegen/README.md | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b573a0ec..81fc51a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ biome_string_case = { git = "https://github.com/lionel-/biome", rev = "a1296ea6b biome_text_size = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } biome_ungrammar = { path = "./crates/biome_ungrammar" } biome_unicode_table = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } +bpaf = { version = "0.9.15", features = ["derive"] } bytes = "1.8.0" cargo_metadata = "0.20.0" case = "1.0.0" diff --git a/plans/aether.md b/plans/aether.md index a1011298..65351ebf 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -120,7 +120,7 @@ it needs, so every commit builds. (`xtask_accept`), mirroring `xtask/codegen` and `xtask/json_schema`. Add `"xtask/accept"` to workspace `members`. -- [ ] `xtask/codegen`: start from aether's crate, then re-add air's +- [x] `xtask/codegen`: start from aether's crate, then re-add air's `formatter.rs` unchanged (`git2` included), the `Formatter` and `All` commands, and `formatter_crate_name()`. Add `bpaf` to the workspace dependencies, as aether's codegen uses it from there. Run `just gen-grammar` diff --git a/xtask/codegen/Cargo.toml b/xtask/codegen/Cargo.toml index dfc12e62..12f2cc9c 100644 --- a/xtask/codegen/Cargo.toml +++ b/xtask/codegen/Cargo.toml @@ -8,7 +8,7 @@ publish = false anyhow = { workspace = true } biome_string_case = { workspace = true } biome_ungrammar = { workspace = true } -bpaf = { version = "0.9.15", features = ["derive"] } +bpaf = { workspace = true } git2 = { version = "0.21.0", default-features = false } proc-macro2 = { workspace = true, features = ["span-locations"] } quote = { workspace = true } diff --git a/xtask/codegen/README.md b/xtask/codegen/README.md index ebbe7cb4..a90a292f 100644 --- a/xtask/codegen/README.md +++ b/xtask/codegen/README.md @@ -2,7 +2,7 @@ This crate contains local commands used to auto-generate source code. -## `cargo codegen grammar` +## `just gen-grammar` This command transforms the `*.ungram` files into the `air_*_syntax` and `air_*_factory` crates. From 6826554014cf395848384430ce621894e785f8c7 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 12:40:20 -0400 Subject: [PATCH 05/20] Drop unused crates, set serde features correctly --- Cargo.toml | 3 --- crates/lsp/Cargo.toml | 2 +- crates/lsp_test/Cargo.toml | 2 +- crates/settings/Cargo.toml | 4 ++-- plans/aether.md | 9 +++++++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 81fc51a6..d48f60ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ biome_rowan = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8 biome_string_case = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } biome_text_size = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } biome_ungrammar = { path = "./crates/biome_ungrammar" } -biome_unicode_table = { git = "https://github.com/lionel-/biome", rev = "a1296ea6ba363d8b8d8f02181b2a4ce9315c5ef9" } bpaf = { version = "0.9.15", features = ["derive"] } bytes = "1.8.0" cargo_metadata = "0.20.0" @@ -80,8 +79,6 @@ toml = "0.8.19" tower-lsp = { branch = "bugfix/patches", git = "https://github.com/lionel-/tower-lsp" } tracing = { version = "0.1.40", default-features = false, features = ["std"] } tracing-subscriber = "0.3.19" -tree-sitter = "0.24.7" -tree-sitter-r = { git = "https://github.com/r-lib/tree-sitter-r", rev = "d459b97a9a11dd81643ab674938bede2874663b0" } unicode-ident = "1.0" url = "2.5.3" uuid = { version = "1.11.0", features = ["v4"] } diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 427065d1..a2b5db26 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -27,7 +27,7 @@ dissimilar.workspace = true futures.workspace = true itertools.workspace = true line_ending.workspace = true -serde.workspace = true +serde = { workspace = true, features = ["derive"] } serde_json.workspace = true settings.workspace = true struct-field-names-as-array.workspace = true diff --git a/crates/lsp_test/Cargo.toml b/crates/lsp_test/Cargo.toml index e5111cf2..19ce62be 100644 --- a/crates/lsp_test/Cargo.toml +++ b/crates/lsp_test/Cargo.toml @@ -17,7 +17,7 @@ futures.workspace = true futures-util.workspace = true httparse.workspace = true memchr.workspace = true -serde.workspace = true +serde = { workspace = true, features = ["derive"] } serde_json.workspace = true tokio = { workspace = true, features = ["full"] } tokio-util.workspace = true diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 4a639f74..22f56c32 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -14,13 +14,13 @@ publish = false [dependencies] biome_formatter = { workspace = true, optional = true } schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] anyhow = { workspace = true } biome_formatter = { workspace = true } insta = { workspace = true } -serde = { workspace = true } +serde = { workspace = true, features = ["derive"] } # Self dependency to activate optional features for tests. # `cargo shear` will try and incorrectly remove this. settings = { features = ["biome", "serde"], path = "./" } diff --git a/plans/aether.md b/plans/aether.md index 65351ebf..c016d716 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -126,8 +126,10 @@ it needs, so every commit builds. dependencies, as aether's codegen uses it from there. Run `just gen-grammar` and `just gen-formatter`. Both should produce no diff. -- [ ] Workspace `Cargo.toml`, for what the earlier bullets didn't need: - - Give `serde` the `derive` feature, as aether does. +- [x] Workspace `Cargo.toml`, for what the earlier bullets didn't need: + - Keep `serde` without `derive`, unlike aether. Instead, `lsp`, + `lsp_test`, and `settings` ask for `derive` themselves, as every other + crate that derives already does. - Remove `tree-sitter`, `tree-sitter-r`, and `biome_unicode_table`, which nothing uses after the copy. @@ -205,6 +207,9 @@ it needs, so every commit builds. - `accept` lives in its own `xtask_accept` crate rather than in `xtask`, so `xtask_codegen` doesn't build the parser it generates code for. +- The workspace `serde` stays without `derive`. Each crate that derives asks + for the feature itself. + - Aether's `.config/nextest.toml` is taken verbatim, including the 3×10s terminate. Any LSP test that trips it gets fixed. From 80d4d15426f0ecb1a02780c033a5e08642207d57 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 12:42:48 -0400 Subject: [PATCH 06/20] Move git2 into workspace `Cargo.toml` as well --- Cargo.toml | 1 + plans/aether.md | 2 ++ xtask/codegen/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d48f60ab..2ef291e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ etcetera = "0.11.0" fs = { path = "./crates/fs" } futures = "0.3.31" futures-util = "0.3.31" +git2 = { version = "0.21.0", default-features = false } globwalk = "0.9.1" httparse = "1.9.5" ignore = "0.4.23" diff --git a/plans/aether.md b/plans/aether.md index c016d716..419a0ad9 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -132,6 +132,8 @@ it needs, so every commit builds. crate that derives already does. - Remove `tree-sitter`, `tree-sitter-r`, and `biome_unicode_table`, which nothing uses after the copy. + - Move `git2` into the workspace dependencies, so `xtask_codegen` inherits + it like everything else. - [ ] `.config/nextest.toml`: take aether's, verbatim. diff --git a/xtask/codegen/Cargo.toml b/xtask/codegen/Cargo.toml index 12f2cc9c..14ef4dfb 100644 --- a/xtask/codegen/Cargo.toml +++ b/xtask/codegen/Cargo.toml @@ -9,7 +9,7 @@ anyhow = { workspace = true } biome_string_case = { workspace = true } biome_ungrammar = { workspace = true } bpaf = { workspace = true } -git2 = { version = "0.21.0", default-features = false } +git2 = { workspace = true } proc-macro2 = { workspace = true, features = ["span-locations"] } quote = { workspace = true } xtask = { version = "0.0", path = "../" } From 494081d83be9d719af81b4dd246dd134df9fa9f8 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 12:47:05 -0400 Subject: [PATCH 07/20] Pull over aether's `.config/nextest.toml` --- .config/nextest.toml | 17 ++++++++++------- plans/aether.md | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 18bc23b5..ad7b9ec5 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,11 +1,14 @@ -[profile.ci] +[profile.default] +fail-fast = false +# Try up to 3 times with 10 second timeout each time. +# Helps detect deadlocks without hanging infinitely. +slow-timeout = { period = "10s", terminate-after = 3 } # Print out output for failing tests as soon as they fail, and also at the end # of the run (for easy scrollability). failure-output = "immediate-final" +status-level = "fail" -# Do not cancel the test run on the first failure. -fail-fast = false - -# Mark tests that take longer than 60s as slow. -# Terminate after 60s as a stop-gap measure to terminate on deadlock. -slow-timeout = { period = "60s", terminate-after = 1 } +[profile.ci] +inherits = "default" +retries = 0 +final-status-level = "slow" diff --git a/plans/aether.md b/plans/aether.md index 419a0ad9..bff01621 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -135,7 +135,7 @@ it needs, so every commit builds. - Move `git2` into the workspace dependencies, so `xtask_codegen` inherits it like everything else. -- [ ] `.config/nextest.toml`: take aether's, verbatim. +- [x] `.config/nextest.toml`: take aether's, verbatim. - [ ] `justfile`: - `gen-grammar` stays, and runs the merged codegen. From 5dc8d8c7170618da61689f7280f0c56ecba24e70 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 12:51:36 -0400 Subject: [PATCH 08/20] Update `justfile` with new commands --- CONTRIBUTING.md | 2 +- justfile | 13 +++++++++++-- plans/aether.md | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 431ffb73..6b9b509d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -187,6 +187,6 @@ We use [nextest](https://nexte.st/) for testing rather than a standard `cargo te Install the nextest cli tool using a [prebuilt binary](https://nexte.st/docs/installation/pre-built-binaries/). -Run tests locally with `just test`, which calls `cargo nextest run`. Run insta snapshot tests in "update" mode with `just test-insta`. +Run tests locally with `just test`, which calls `cargo nextest run`. Run insta snapshot tests in "update" mode with `just test-insta`. Regenerate the parser's markdown snapshots with `just test-snapshot-accept`. On CI we use the nextest profile found in `.config/nextest.toml`. diff --git a/justfile b/justfile index ab2662d5..af3d5cf6 100644 --- a/justfile +++ b/justfile @@ -20,14 +20,23 @@ test *ARGS: test-verbose *ARGS: cargo nextest run --no-capture {{ARGS}} -# Run the insta tests in update mode +# Run the formatter's insta tests in update mode test-insta *ARGS: cargo insta test --test-runner nextest {{ARGS}} +# Regenerate the parser's markdown snapshots, then verify they are stable +test-snapshot-accept: + cargo run -p xtask_accept + cargo nextest run --workspace + # Run the quick formatter test -test-quick: +test-quick-formatter: cargo test --package air_r_formatter --test quick_test -- quick_test --exact --show-output --ignored +# Run the quick parser test +test-quick-parser: + cargo nextest run -p air_r_parser_tests --run-ignored all -E 'test(quick_test)' --no-capture + # Install the air binary to `~/.cargo/bin/air`. # Note that a `~/.local/bin/air` installed another way may shadow this. install-binary: diff --git a/plans/aether.md b/plans/aether.md index bff01621..0dcc3486 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -137,7 +137,7 @@ it needs, so every commit builds. - [x] `.config/nextest.toml`: take aether's, verbatim. -- [ ] `justfile`: +- [x] `justfile`: - `gen-grammar` stays, and runs the merged codegen. - Aether's `test` is dropped. Air's `test *ARGS` stays. - Aether's `accept` is added, running `cargo run -p xtask_accept`. From 48d715e0efe69c687c98e591473fb11d2be707d1 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 12:53:56 -0400 Subject: [PATCH 09/20] Add codegen and snapshot checks to `test-health.yml` --- .github/workflows/test-health.yml | 11 +++++++++++ plans/aether.md | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-health.yml b/.github/workflows/test-health.yml index bf24a6dd..4725e6a7 100644 --- a/.github/workflows/test-health.yml +++ b/.github/workflows/test-health.yml @@ -20,8 +20,19 @@ jobs: - name: Rust cache uses: Swatinem/rust-cache@v2 + - name: Install just + uses: taiki-e/install-action@v2 + with: + tool: just + - name: Check formatting run: cargo fmt --all --check - name: Clippy run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Codegen is up to date + run: just gen-grammar && git diff --exit-code + + - name: Parser markdown snapshots are up to date + run: cargo run -p xtask_accept && git diff --exit-code diff --git a/plans/aether.md b/plans/aether.md index 0dcc3486..0a6bc149 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -144,7 +144,7 @@ it needs, so every commit builds. - Aether's `quick` is added as `test-quick-parser`. - Air's `test-quick` becomes `test-quick-formatter`. -- [ ] `test-health.yml`: add aether's two checks, which are +- [x] `test-health.yml`: add aether's two checks, which are `just gen-grammar && git diff --exit-code` and `cargo run -p xtask_accept && git diff --exit-code`. From fe45c3b31ca44281a2848063686036fb21bccd36 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 13:04:34 -0400 Subject: [PATCH 10/20] Transition to actual semicolon snapshot tests! --- Cargo.lock | 2 - crates/air_r_formatter/Cargo.toml | 2 - .../src/r/auxiliary/empty_statement.rs | 1185 ----------------- .../tests/specs/r/statements/semicolons.R | 238 ++++ .../specs/r/statements/semicolons.R.snap | 496 +++++++ plans/aether.md | 2 +- 6 files changed, 735 insertions(+), 1190 deletions(-) create mode 100644 crates/air_r_formatter/tests/specs/r/statements/semicolons.R create mode 100644 crates/air_r_formatter/tests/specs/r/statements/semicolons.R.snap diff --git a/Cargo.lock b/Cargo.lock index 63de1943..410d4527 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,10 +87,8 @@ name = "air_r_formatter" version = "0.0.0" dependencies = [ "air_formatter_test", - "air_r_factory", "air_r_parser", "air_r_syntax", - "anyhow", "biome_formatter", "biome_parser", "biome_rowan", diff --git a/crates/air_r_formatter/Cargo.toml b/crates/air_r_formatter/Cargo.toml index a77be538..5edc1900 100644 --- a/crates/air_r_formatter/Cargo.toml +++ b/crates/air_r_formatter/Cargo.toml @@ -24,9 +24,7 @@ tracing = { workspace = true } [dev-dependencies] air_formatter_test = { workspace = true } -air_r_factory = { workspace = true } air_r_parser = { workspace = true } -anyhow = { workspace = true } biome_parser = { workspace = true } tests_macros = { workspace = true } workspace = { workspace = true } diff --git a/crates/air_r_formatter/src/r/auxiliary/empty_statement.rs b/crates/air_r_formatter/src/r/auxiliary/empty_statement.rs index c47f3916..9b3d9807 100644 --- a/crates/air_r_formatter/src/r/auxiliary/empty_statement.rs +++ b/crates/air_r_formatter/src/r/auxiliary/empty_statement.rs @@ -19,1188 +19,3 @@ impl FormatNodeRule for FormatREmptyStatement { write!(f, [format_removed(&semicolon_token?)]) } } - -// TODO(aether): Replace with `statements/semicolons.R` spec tests on the swap to aether -#[cfg(test)] -mod test { - use air_r_factory::r_argument; - use air_r_factory::r_argument_list; - use air_r_factory::r_braced_statements; - use air_r_factory::r_call; - use air_r_factory::r_call_arguments; - use air_r_factory::r_double_value; - use air_r_factory::r_empty_statement; - use air_r_factory::r_expression_statement; - use air_r_factory::r_function_definition; - use air_r_factory::r_identifier; - use air_r_factory::r_if_statement; - use air_r_factory::r_parameter_list; - use air_r_factory::r_parameters; - use air_r_factory::r_root; - use air_r_factory::r_statement_list; - use air_r_parser::RParserOptions; - use air_r_syntax::AnyRExpression; - use air_r_syntax::AnyRStatement; - use air_r_syntax::RRoot; - use air_r_syntax::RSyntaxKind; - use air_r_syntax::RSyntaxKind::*; - use air_r_syntax::RSyntaxToken; - use biome_rowan::AstNode; - use biome_rowan::TextSize; - use biome_rowan::TriviaPiece; - use biome_rowan::TriviaPieceKind; - - use crate::context::RFormatOptions; - use crate::format_node; - - #[derive(Clone, Copy)] - enum Trivia { - Newline, - Whitespace(&'static str), - Comment(&'static str), - } - - use Trivia::Comment; - use Trivia::Newline; - - const INDENT: Trivia = Trivia::Whitespace(" "); - const SPACE: Trivia = Trivia::Whitespace(" "); - - impl Trivia { - fn text(self) -> &'static str { - match self { - Trivia::Newline => "\n", - Trivia::Whitespace(text) | Trivia::Comment(text) => text, - } - } - - fn piece(self) -> TriviaPiece { - let kind = match self { - Trivia::Newline => TriviaPieceKind::Newline, - Trivia::Whitespace(_) => TriviaPieceKind::Whitespace, - Trivia::Comment(_) => TriviaPieceKind::SingleLineComment, - }; - TriviaPiece::new(kind, TextSize::of(self.text())) - } - } - - fn token( - kind: RSyntaxKind, - leading: &[Trivia], - text: &str, - trailing: &[Trivia], - ) -> RSyntaxToken { - let text: String = leading - .iter() - .map(|trivia| trivia.text()) - .chain([text]) - .chain(trailing.iter().map(|trivia| trivia.text())) - .collect(); - - RSyntaxToken::new_detached( - kind, - &text, - leading.iter().map(|trivia| trivia.piece()), - trailing.iter().map(|trivia| trivia.piece()), - ) - } - - fn root( - statements: impl IntoIterator, - ) -> RRoot { - r_root(r_statement_list(statements), token(EOF, &[], "", &[])).build() - } - - fn braces( - l_curly_token: RSyntaxToken, - statements: impl IntoIterator, - r_curly_token: RSyntaxToken, - ) -> AnyRExpression { - r_braced_statements(l_curly_token, r_statement_list(statements), r_curly_token).into() - } - - fn statement(expression: AnyRExpression) -> AnyRStatement { - r_expression_statement(expression).build().into() - } - - fn statement_with_semicolon( - expression: AnyRExpression, - semicolon_token: RSyntaxToken, - ) -> AnyRStatement { - r_expression_statement(expression) - .with_semicolon_token(semicolon_token) - .build() - .into() - } - - fn empty(semicolon_token: RSyntaxToken) -> AnyRStatement { - r_empty_statement(semicolon_token).into() - } - - fn double(value_token: RSyntaxToken) -> AnyRExpression { - AnyRExpression::AnyRValue(r_double_value(value_token).into()) - } - - fn identifier(name_token: RSyntaxToken) -> AnyRExpression { - r_identifier(name_token).into() - } - - fn call(name_token: RSyntaxToken, value: AnyRExpression) -> AnyRExpression { - let argument = r_argument().with_value(value).build(); - let arguments = r_call_arguments( - token(L_PAREN, &[], "(", &[]), - r_argument_list([argument], []), - token(R_PAREN, &[], ")", &[]), - ); - r_call(identifier(name_token), arguments).into() - } - - /// Format a hand-built `root` - /// - /// Checks that `root` was transcribed from `source` correctly, and that the - /// output is stable when parsed and formatted again. - fn format(root: RRoot, source: &str) -> anyhow::Result { - assert_eq!(root.syntax().to_string(), source); - - let output = format_node(RFormatOptions::default(), root.syntax())? - .print()? - .into_code(); - - // tree-sitter hides `;`, so we can't parse a kept `;` back - if output.contains(';') { - return Ok(output); - } - - let reparsed = air_r_parser::parse(&output, RParserOptions::default()); - assert!(!reparsed.has_errors()); - let reformatted = format_node(RFormatOptions::default(), &reparsed.syntax())? - .print()? - .into_code(); - assert_eq!(output, reformatted); - - Ok(output) - } - - #[test] - fn test_semicolons_between_statements() -> anyhow::Result<()> { - let source = "\ -{ - 1;; - 2 -}"; - let expected = "\ -{ - 1 - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement_with_semicolon( - double(token(R_DOUBLE_LITERAL, &[Newline, INDENT], "1", &[])), - token(SEMICOLON, &[], ";", &[]), - ), - empty(token(SEMICOLON, &[], ";", &[])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - #[test] - fn test_braces_with_only_semicolons() -> anyhow::Result<()> { - let source = "\ -{;}"; - let expected = "\ -{} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [empty(token(SEMICOLON, &[], ";", &[]))], - token(R_CURLY, &[], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{;;}"; - let expected = "\ -{} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - empty(token(SEMICOLON, &[], ";", &[])), - empty(token(SEMICOLON, &[], ";", &[])), - ], - token(R_CURLY, &[], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - Ok(()) - } - - #[test] - fn test_empty_line_before_lone_semicolon_is_kept() -> anyhow::Result<()> { - let source = "\ -{ - 1 - - ; - 2 -}"; - let expected = "\ -{ - 1 - - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token(SEMICOLON, &[Newline, Newline, INDENT], ";", &[])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - #[test] - fn test_empty_line_before_leading_lone_semicolon_is_removed() -> anyhow::Result<()> { - let source = "\ -{ - - ; - 1 -}"; - let expected = "\ -{ - 1 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - empty(token(SEMICOLON, &[Newline, Newline, INDENT], ";", &[])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - #[test] - fn test_lone_semicolon_does_not_add_empty_line() -> anyhow::Result<()> { - let source = "\ -{ - 1 - ; - 2 -}"; - let expected = "\ -{ - 1 - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - #[test] - fn test_comment_before_lone_semicolon() -> anyhow::Result<()> { - let source = "\ -{ - 1 # c - ; -}"; - let expected = "\ -{ - 1 # c -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[SPACE, Comment("# c")], - ))), - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[])), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{1 # c -;}"; - let expected = "\ -{ - 1 # c -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[], - "1", - &[SPACE, Comment("# c")], - ))), - empty(token(SEMICOLON, &[Newline], ";", &[])), - ], - token(R_CURLY, &[], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - Ok(()) - } - - #[test] - fn test_comment_after_lone_semicolon_trails_previous_statement() -> anyhow::Result<()> { - let source = "\ -{ - 1 - ; # c -}"; - let expected = "\ -{ - 1 # c -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token( - SEMICOLON, - &[Newline, INDENT], - ";", - &[SPACE, Comment("# c")], - )), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - #[test] - fn test_comment_after_lone_semicolon_leads_next_statement() -> anyhow::Result<()> { - let source = "\ -{ - ; # c - 1 -}"; - let expected = "\ -{ - # c - 1 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - empty(token( - SEMICOLON, - &[Newline, INDENT], - ";", - &[SPACE, Comment("# c")], - )), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - #[test] - fn test_comment_after_lone_semicolon_dangles() -> anyhow::Result<()> { - let source = "\ -{;# c -}"; - let expected = "\ -{ - # c -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [empty(token(SEMICOLON, &[], ";", &[Comment("# c")]))], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - #[test] - fn test_comments_around_lone_semicolon_dangle() -> anyhow::Result<()> { - let source = "\ -{ - # a - ; # b - # c -}"; - let expected = "\ -{ - # a - # b - # c -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [empty(token( - SEMICOLON, - &[Newline, INDENT, Comment("# a"), Newline, INDENT], - ";", - &[SPACE, Comment("# b")], - ))], - token( - R_CURLY, - &[Newline, INDENT, Comment("# c"), Newline], - "}", - &[], - ), - ))]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - #[test] - fn test_comments_around_lone_semicolon_keep_order() -> anyhow::Result<()> { - let source = "\ -{ - 1 - # a - ; # b - 2 -}"; - let expected = "\ -{ - 1 - # a - # b - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token( - SEMICOLON, - &[Newline, INDENT, Comment("# a"), Newline, INDENT], - ";", - &[SPACE, Comment("# b")], - )), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - /// Handles every variant of [crate::comments::handle_empty_statement_comment] - #[test] - fn test_handle_empty_statement_comment() -> anyhow::Result<()> { - const TRAILS: Trivia = Comment("# Trails preceding"); - const LEADS: Trivia = Comment("# Leads following"); - const DANGLING: Trivia = Comment("# Dangling on enclosing"); - - // `CommentTextPosition::EndOfLine` - - let source = "\ -{ - 1; ; # Trails preceding - 2 -}"; - let expected = "\ -{ - 1 # Trails preceding - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement_with_semicolon( - double(token(R_DOUBLE_LITERAL, &[Newline, INDENT], "1", &[])), - token(SEMICOLON, &[], ";", &[]), - ), - empty(token(SEMICOLON, &[SPACE], ";", &[SPACE, TRAILS])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - 1; ; ; # Trails preceding - 2 -}"; - let expected = "\ -{ - 1 # Trails preceding - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement_with_semicolon( - double(token(R_DOUBLE_LITERAL, &[Newline, INDENT], "1", &[])), - token(SEMICOLON, &[], ";", &[]), - ), - empty(token(SEMICOLON, &[SPACE], ";", &[])), - empty(token(SEMICOLON, &[SPACE], ";", &[SPACE, TRAILS])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - 1 - ; ; # Leads following - 2 -}"; - let expected = "\ -{ - 1 - # Leads following - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[])), - empty(token(SEMICOLON, &[SPACE], ";", &[SPACE, LEADS])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - 1 - ; # Leads following - 2 -}"; - let expected = "\ -{ - 1 - # Leads following - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[SPACE, LEADS])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - 1 - ; # Trails preceding -}"; - let expected = "\ -{ - 1 # Trails preceding -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[SPACE, TRAILS])), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - ; # Leads following - 2 -}"; - let expected = "\ -{ - # Leads following - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[SPACE, LEADS])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - ; # Dangling on enclosing -}"; - let expected = "\ -{ - # Dangling on enclosing -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [empty(token( - SEMICOLON, - &[Newline, INDENT], - ";", - &[SPACE, DANGLING], - ))], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - // `CommentTextPosition::OwnLine` - - let source = "\ -{ - 1 - # Leads following - ; - 2 -}"; - let expected = "\ -{ - 1 - # Leads following - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token( - SEMICOLON, - &[Newline, INDENT, LEADS, Newline, INDENT], - ";", - &[], - )), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - 1 - ; - # Leads following - 2 -}"; - let expected = "\ -{ - 1 - # Leads following - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT, LEADS, Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - # Leads following - ; - 2 -}"; - let expected = "\ -{ - # Leads following - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - empty(token( - SEMICOLON, - &[Newline, INDENT, LEADS, Newline, INDENT], - ";", - &[], - )), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - ; - # Leads following - 2 -}"; - let expected = "\ -{ - # Leads following - 2 -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT, LEADS, Newline, INDENT], - "2", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - 1 - # Trails preceding - ; -}"; - let expected = "\ -{ - 1 - # Trails preceding -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token( - SEMICOLON, - &[Newline, INDENT, TRAILS, Newline, INDENT], - ";", - &[], - )), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - 1 - ; - # Trails preceding -}"; - let expected = "\ -{ - 1 - # Trails preceding -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[])), - ], - token(R_CURLY, &[Newline, INDENT, TRAILS, Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - # Dangling on enclosing - ; -}"; - let expected = "\ -{ - # Dangling on enclosing -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [empty(token( - SEMICOLON, - &[Newline, INDENT, DANGLING, Newline, INDENT], - ";", - &[], - ))], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -{ - ; - # Dangling on enclosing -}"; - let expected = "\ -{ - # Dangling on enclosing -} -"; - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [empty(token(SEMICOLON, &[Newline, INDENT], ";", &[]))], - token(R_CURLY, &[Newline, INDENT, DANGLING, Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - - Ok(()) - } - - #[test] - fn test_function_body_comment_skips_lone_semicolon() -> anyhow::Result<()> { - fn function(body: AnyRExpression) -> AnyRExpression { - let parameters = r_parameters( - token(L_PAREN, &[], "(", &[]), - r_parameter_list([], []), - token(R_PAREN, &[], ")", &[SPACE, Comment("# c")]), - ); - r_function_definition(token(FUNCTION_KW, &[], "function", &[]), parameters, body).into() - } - - let source = "\ -function() # c -{ - ; - 1 -}"; - let expected = "\ -function() { - # c - 1 -} -"; - let tree = root([statement(function(braces( - token(L_CURLY, &[Newline], "{", &[]), - [ - empty(token(SEMICOLON, &[Newline, INDENT], ";", &[])), - statement(double(token( - R_DOUBLE_LITERAL, - &[Newline, INDENT], - "1", - &[], - ))), - ], - token(R_CURLY, &[Newline], "}", &[]), - )))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -function() # c -{;}"; - let expected = "\ -function() { - # c -} -"; - let tree = root([statement(function(braces( - token(L_CURLY, &[Newline], "{", &[]), - [empty(token(SEMICOLON, &[], ";", &[]))], - token(R_CURLY, &[], "}", &[]), - )))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -function() # c -{ - ; -}"; - let expected = "\ -function() { - # c -} -"; - let tree = root([statement(function(braces( - token(L_CURLY, &[Newline], "{", &[]), - [empty(token(SEMICOLON, &[Newline, INDENT], ";", &[]))], - token(R_CURLY, &[Newline], "}", &[]), - )))]); - assert_eq!(format(tree, source)?, expected); - - Ok(()) - } - - #[test] - fn test_trailing_semicolons_keep_value_position() -> anyhow::Result<()> { - let source = "\ -{ - if (a) 1;; -}"; - let expected = "\ -{ - if (a) 1 -} -"; - let if_statement = r_if_statement( - token(IF_KW, &[Newline, INDENT], "if", &[SPACE]), - token(L_PAREN, &[], "(", &[]), - identifier(token(IDENT, &[], "a", &[])), - token(R_PAREN, &[], ")", &[SPACE]), - double(token(R_DOUBLE_LITERAL, &[], "1", &[])), - ) - .build(); - let tree = root([statement(braces( - token(L_CURLY, &[], "{", &[]), - [ - statement_with_semicolon(if_statement.into(), token(SEMICOLON, &[], ";", &[])), - empty(token(SEMICOLON, &[], ";", &[])), - ], - token(R_CURLY, &[Newline], "}", &[]), - ))]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - #[test] - fn test_curly_curly_removes_semicolon() -> anyhow::Result<()> { - let source = "\ -fn({{ x }; })"; - let expected = "\ -fn({{ x }}) -"; - let inner = braces( - token(L_CURLY, &[], "{", &[SPACE]), - [statement(identifier(token(IDENT, &[], "x", &[SPACE])))], - token(R_CURLY, &[], "}", &[]), - ); - let outer = braces( - token(L_CURLY, &[], "{", &[]), - [statement_with_semicolon( - inner, - token(SEMICOLON, &[], ";", &[SPACE]), - )], - token(R_CURLY, &[], "}", &[]), - ); - let tree = root([statement(call(token(IDENT, &[], "fn", &[]), outer))]); - assert_eq!(format(tree, source)?, expected); - - let source = "\ -fn({{ x; }})"; - let expected = "\ -fn({{ x }}) -"; - let inner = braces( - token(L_CURLY, &[], "{", &[SPACE]), - [statement_with_semicolon( - identifier(token(IDENT, &[], "x", &[])), - token(SEMICOLON, &[], ";", &[SPACE]), - )], - token(R_CURLY, &[], "}", &[]), - ); - let outer = braces( - token(L_CURLY, &[], "{", &[]), - [statement(inner)], - token(R_CURLY, &[], "}", &[]), - ); - let tree = root([statement(call(token(IDENT, &[], "fn", &[]), outer))]); - assert_eq!(format(tree, source)?, expected); - - Ok(()) - } - - #[test] - fn test_skip_keeps_semicolon() -> anyhow::Result<()> { - let source = "\ -# fmt: skip -x;"; - let expected = "\ -# fmt: skip -x; -"; - let tree = root([statement_with_semicolon( - identifier(token(IDENT, &[Comment("# fmt: skip"), Newline], "x", &[])), - token(SEMICOLON, &[], ";", &[]), - )]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } - - #[test] - fn test_comment_after_semicolon() -> anyhow::Result<()> { - let source = "\ -1; # c"; - let expected = "\ -1 # c -"; - let tree = root([statement_with_semicolon( - double(token(R_DOUBLE_LITERAL, &[], "1", &[])), - token(SEMICOLON, &[], ";", &[SPACE, Comment("# c")]), - )]); - assert_eq!(format(tree, source)?, expected); - Ok(()) - } -} diff --git a/crates/air_r_formatter/tests/specs/r/statements/semicolons.R b/crates/air_r_formatter/tests/specs/r/statements/semicolons.R new file mode 100644 index 00000000..bdc8ab8f --- /dev/null +++ b/crates/air_r_formatter/tests/specs/r/statements/semicolons.R @@ -0,0 +1,238 @@ +# ------------------------------------------------------------------------ +# Semicolons between statements + +1;2 + +1; + +{1;;2} + +{;1} + +{ + 1;; + 2 +} + +# Trailing semicolons keep value position +{ + if (a) 1;; +} + +# ------------------------------------------------------------------------ +# Braces with only semicolons + +{;} + +{;;} + +# ------------------------------------------------------------------------ +# Empty lines around a lone semicolon + +# Empty line before lone semicolon is kept +{ + 1 + + ; + 2 +} + +# Empty line after lone semicolon is kept +{ + 1 + ; + + 2 +} + +# Empty line before leading lone semicolon is removed +{ + + ; + 1 +} + +# Lone semicolon does not add empty line +{ + 1 + ; + 2 +} + +# ------------------------------------------------------------------------ +# Comments after a semicolon + +1; # c + +1 ; # c + +# ------------------------------------------------------------------------ +# Comment before a lone semicolon + +{ + 1 # c + ; +} + +{1 # c +;} + +# ------------------------------------------------------------------------ +# Comment after a lone semicolon + +# Trails previous statement +{ + 1 + ; # c +} + +# Leads next statement +{ + ; # c + 1 +} + +# Dangles +{;# c +} + +# ------------------------------------------------------------------------ +# Comments around a lone semicolon + +# Dangle +{ + # a + ; # b + # c +} + +# Keep order +{ + 1 + # a + ; # b + 2 +} + +# ------------------------------------------------------------------------ +# Every variant of `handle_empty_statement_comment()`, end of line + +{ + 1; ; # Trails preceding + 2 +} + +{ + 1; ; ; # Trails preceding + 2 +} + +{ + 1 + ; ; # Leads following + 2 +} + +{ + 1 + ; # Leads following + 2 +} + +{ + 1 + ; # Trails preceding +} + +{ + ; # Leads following + 2 +} + +{ + ; # Dangling on enclosing +} + +# ------------------------------------------------------------------------ +# Every variant of `handle_empty_statement_comment()`, own line + +{ + 1 + # Leads following + ; + 2 +} + +{ + 1 + ; + # Leads following + 2 +} + +{ + # Leads following + ; + 2 +} + +{ + ; + # Leads following + 2 +} + +{ + 1 + # Trails preceding + ; +} + +{ + 1 + ; + # Trails preceding +} + +{ + # Dangling on enclosing + ; +} + +{ + ; + # Dangling on enclosing +} + +# ------------------------------------------------------------------------ +# Function body comment skips semicolons + +# Due to `place_leading_or_dangling_body_comment()` finding the first +# non-empy statement, and placing the comment as leading on that. + +function() # Leads 1 +{ + ; + 1 +} + +function() # Dangles braces +{;} + +function() # Dangles braces +{ + ; +} + +# ------------------------------------------------------------------------ +# Curly-curly removes semicolon + +fn({{ x }; }) + +fn({{ x; }}) + +# ------------------------------------------------------------------------ +# `# fmt: skip` keeps semicolon + +# fmt: skip +x; diff --git a/crates/air_r_formatter/tests/specs/r/statements/semicolons.R.snap b/crates/air_r_formatter/tests/specs/r/statements/semicolons.R.snap new file mode 100644 index 00000000..090d0a64 --- /dev/null +++ b/crates/air_r_formatter/tests/specs/r/statements/semicolons.R.snap @@ -0,0 +1,496 @@ +--- +source: crates/air_formatter_test/src/snapshot_builder.rs +info: r/statements/semicolons.R +--- +# Input + +```R +# ------------------------------------------------------------------------ +# Semicolons between statements + +1;2 + +1; + +{1;;2} + +{;1} + +{ + 1;; + 2 +} + +# Trailing semicolons keep value position +{ + if (a) 1;; +} + +# ------------------------------------------------------------------------ +# Braces with only semicolons + +{;} + +{;;} + +# ------------------------------------------------------------------------ +# Empty lines around a lone semicolon + +# Empty line before lone semicolon is kept +{ + 1 + + ; + 2 +} + +# Empty line after lone semicolon is kept +{ + 1 + ; + + 2 +} + +# Empty line before leading lone semicolon is removed +{ + + ; + 1 +} + +# Lone semicolon does not add empty line +{ + 1 + ; + 2 +} + +# ------------------------------------------------------------------------ +# Comments after a semicolon + +1; # c + +1 ; # c + +# ------------------------------------------------------------------------ +# Comment before a lone semicolon + +{ + 1 # c + ; +} + +{1 # c +;} + +# ------------------------------------------------------------------------ +# Comment after a lone semicolon + +# Trails previous statement +{ + 1 + ; # c +} + +# Leads next statement +{ + ; # c + 1 +} + +# Dangles +{;# c +} + +# ------------------------------------------------------------------------ +# Comments around a lone semicolon + +# Dangle +{ + # a + ; # b + # c +} + +# Keep order +{ + 1 + # a + ; # b + 2 +} + +# ------------------------------------------------------------------------ +# Every variant of `handle_empty_statement_comment()`, end of line + +{ + 1; ; # Trails preceding + 2 +} + +{ + 1; ; ; # Trails preceding + 2 +} + +{ + 1 + ; ; # Leads following + 2 +} + +{ + 1 + ; # Leads following + 2 +} + +{ + 1 + ; # Trails preceding +} + +{ + ; # Leads following + 2 +} + +{ + ; # Dangling on enclosing +} + +# ------------------------------------------------------------------------ +# Every variant of `handle_empty_statement_comment()`, own line + +{ + 1 + # Leads following + ; + 2 +} + +{ + 1 + ; + # Leads following + 2 +} + +{ + # Leads following + ; + 2 +} + +{ + ; + # Leads following + 2 +} + +{ + 1 + # Trails preceding + ; +} + +{ + 1 + ; + # Trails preceding +} + +{ + # Dangling on enclosing + ; +} + +{ + ; + # Dangling on enclosing +} + +# ------------------------------------------------------------------------ +# Function body comment skips semicolons + +# Due to `place_leading_or_dangling_body_comment()` finding the first +# non-empy statement, and placing the comment as leading on that. + +function() # Leads 1 +{ + ; + 1 +} + +function() # Dangles braces +{;} + +function() # Dangles braces +{ + ; +} + +# ------------------------------------------------------------------------ +# Curly-curly removes semicolon + +fn({{ x }; }) + +fn({{ x; }}) + +# ------------------------------------------------------------------------ +# `# fmt: skip` keeps semicolon + +# fmt: skip +x; + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Space +Indent width: 2 +Line ending: LF +Line width: 80 +Persistent line breaks: Respect +Assignment style: Arrow +Table: fcase, rowwiseDT, tribble +----- + +```R +# ------------------------------------------------------------------------ +# Semicolons between statements + +1 +2 + +1 + +{ + 1 + 2 +} + +{ + 1 +} + +{ + 1 + 2 +} + +# Trailing semicolons keep value position +{ + if (a) 1 +} + +# ------------------------------------------------------------------------ +# Braces with only semicolons + +{} + +{} + +# ------------------------------------------------------------------------ +# Empty lines around a lone semicolon + +# Empty line before lone semicolon is kept +{ + 1 + + 2 +} + +# Empty line after lone semicolon is kept +{ + 1 + + 2 +} + +# Empty line before leading lone semicolon is removed +{ + 1 +} + +# Lone semicolon does not add empty line +{ + 1 + 2 +} + +# ------------------------------------------------------------------------ +# Comments after a semicolon + +1 # c + +1 # c + +# ------------------------------------------------------------------------ +# Comment before a lone semicolon + +{ + 1 # c +} + +{ + 1 # c +} + +# ------------------------------------------------------------------------ +# Comment after a lone semicolon + +# Trails previous statement +{ + 1 # c +} + +# Leads next statement +{ + # c + 1 +} + +# Dangles +{ + # c +} + +# ------------------------------------------------------------------------ +# Comments around a lone semicolon + +# Dangle +{ + # a + # b + # c +} + +# Keep order +{ + 1 + # a + # b + 2 +} + +# ------------------------------------------------------------------------ +# Every variant of `handle_empty_statement_comment()`, end of line + +{ + 1 # Trails preceding + 2 +} + +{ + 1 # Trails preceding + 2 +} + +{ + 1 + # Leads following + 2 +} + +{ + 1 + # Leads following + 2 +} + +{ + 1 # Trails preceding +} + +{ + # Leads following + 2 +} + +{ + # Dangling on enclosing +} + +# ------------------------------------------------------------------------ +# Every variant of `handle_empty_statement_comment()`, own line + +{ + 1 + # Leads following + 2 +} + +{ + 1 + # Leads following + 2 +} + +{ + # Leads following + 2 +} + +{ + # Leads following + 2 +} + +{ + 1 + # Trails preceding +} + +{ + 1 + # Trails preceding +} + +{ + # Dangling on enclosing +} + +{ + # Dangling on enclosing +} + +# ------------------------------------------------------------------------ +# Function body comment skips semicolons + +# Due to `place_leading_or_dangling_body_comment()` finding the first +# non-empy statement, and placing the comment as leading on that. + +function() { + # Leads 1 + 1 +} + +function() { + # Dangles braces +} + +function() { + # Dangles braces +} + +# ------------------------------------------------------------------------ +# Curly-curly removes semicolon + +fn({{ x }}) + +fn({{ x }}) + +# ------------------------------------------------------------------------ +# `# fmt: skip` keeps semicolon + +# fmt: skip +x; +``` diff --git a/plans/aether.md b/plans/aether.md index 0a6bc149..52a6dca6 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -148,7 +148,7 @@ it needs, so every commit builds. `just gen-grammar && git diff --exit-code` and `cargo run -p xtask_accept && git diff --exit-code`. -- [ ] Formatter specs for semicolons, replacing the hand-built `TODO(aether)` +- [x] Formatter specs for semicolons, replacing the hand-built `TODO(aether)` tests in `r/auxiliary/empty_statement.rs`. New `statements/semicolons.R` covers: - `1;2`, `1;`, `1;;2` in braces, `{;}`, `{;1}`. From 3458259f08cb6d9bd1bb477473809574b0bab969 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 13:09:37 -0400 Subject: [PATCH 11/20] Add formatter tests for raw strings with `|` and `SPECIAL` operations with slashes Both of which came with aether --- .../tests/specs/r/binary_expression.R | 1 + .../tests/specs/r/binary_expression.R.snap | 28 ++++++++++--------- .../tests/specs/r/value/string_value.R | 2 ++ .../tests/specs/r/value/string_value.R.snap | 4 +++ plans/aether.md | 10 +++++-- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/crates/air_r_formatter/tests/specs/r/binary_expression.R b/crates/air_r_formatter/tests/specs/r/binary_expression.R index 6a7974e5..7ef7152b 100644 --- a/crates/air_r_formatter/tests/specs/r/binary_expression.R +++ b/crates/air_r_formatter/tests/specs/r/binary_expression.R @@ -42,6 +42,7 @@ argument_that_is_quite_long + argument_that_is_quite_long^argument_that_is_quite 1 / 2 1 |> 2 1 %>% 2 +1 %a\b% 2 # The following expressions should not have spaces 1 ? 2 diff --git a/crates/air_r_formatter/tests/specs/r/binary_expression.R.snap b/crates/air_r_formatter/tests/specs/r/binary_expression.R.snap index 90b4784f..b5413b73 100644 --- a/crates/air_r_formatter/tests/specs/r/binary_expression.R.snap +++ b/crates/air_r_formatter/tests/specs/r/binary_expression.R.snap @@ -49,6 +49,7 @@ argument_that_is_quite_long + argument_that_is_quite_long^argument_that_is_quite 1 / 2 1 |> 2 1 %>% 2 +1 %a\b% 2 # The following expressions should not have spaces 1 ? 2 @@ -753,6 +754,7 @@ argument_that_is_quite_long + 1 / 2 1 |> 2 1 %>% 2 +1 %a\b% 2 # The following expressions should not have spaces 1?2 @@ -1524,17 +1526,17 @@ y ~ x1 + # Lines exceeding max width of 80 characters ``` - 206: # TODO: Do we really want these chained? It seems like no good can come of that even though they have same precedence - 582: a_really_really_really_really_really_really_long_thing_here ~ a_really_really_really_really_really_really_long_thing_here2 - 583: a_really_really_really_really_really_really_long_thing_here = a_really_really_really_really_really_really_long_thing_here2 - 584: a_really_really_really_really_really_really_long_thing_here := a_really_really_really_really_really_really_long_thing_here2 - 585: a_really_really_really_really_really_really_long_thing_here <- a_really_really_really_really_really_really_long_thing_here2 - 586: a_really_really_really_really_really_really_long_thing_here -> a_really_really_really_really_really_really_long_thing_here2 - 587: a_really_really_really_really_really_really_long_thing_here <<- a_really_really_really_really_really_really_long_thing_here2 - 588: a_really_really_really_really_really_really_long_thing_here ->> a_really_really_really_really_really_really_long_thing_here2 - 700: a_really_really_long_thing_here1 ~ a_really_really_long_thing_here2 ~ a_really_really_long_thing_here3 - 701: a_really_really_long_thing_here1 ~ a_really_really_really_really_really_really_long_thing_here2 ~ a_really_really_long_thing_here3 - 702: (a_really_really_long_thing_here1 ~ a_really_really_long_thing_here2) ~ a_really_really_long_thing_here3 - 703: a_really_really_long_thing_here1 ~ (a_really_really_long_thing_here2 ~ a_really_really_long_thing_here3) - 756: # https://github.com/posit-dev/positron/discussions/6095#discussioncomment-13112983 + 207: # TODO: Do we really want these chained? It seems like no good can come of that even though they have same precedence + 583: a_really_really_really_really_really_really_long_thing_here ~ a_really_really_really_really_really_really_long_thing_here2 + 584: a_really_really_really_really_really_really_long_thing_here = a_really_really_really_really_really_really_long_thing_here2 + 585: a_really_really_really_really_really_really_long_thing_here := a_really_really_really_really_really_really_long_thing_here2 + 586: a_really_really_really_really_really_really_long_thing_here <- a_really_really_really_really_really_really_long_thing_here2 + 587: a_really_really_really_really_really_really_long_thing_here -> a_really_really_really_really_really_really_long_thing_here2 + 588: a_really_really_really_really_really_really_long_thing_here <<- a_really_really_really_really_really_really_long_thing_here2 + 589: a_really_really_really_really_really_really_long_thing_here ->> a_really_really_really_really_really_really_long_thing_here2 + 701: a_really_really_long_thing_here1 ~ a_really_really_long_thing_here2 ~ a_really_really_long_thing_here3 + 702: a_really_really_long_thing_here1 ~ a_really_really_really_really_really_really_long_thing_here2 ~ a_really_really_long_thing_here3 + 703: (a_really_really_long_thing_here1 ~ a_really_really_long_thing_here2) ~ a_really_really_long_thing_here3 + 704: a_really_really_long_thing_here1 ~ (a_really_really_long_thing_here2 ~ a_really_really_long_thing_here3) + 757: # https://github.com/posit-dev/positron/discussions/6095#discussioncomment-13112983 ``` diff --git a/crates/air_r_formatter/tests/specs/r/value/string_value.R b/crates/air_r_formatter/tests/specs/r/value/string_value.R index ddf6c14a..19fd4e3f 100644 --- a/crates/air_r_formatter/tests/specs/r/value/string_value.R +++ b/crates/air_r_formatter/tests/specs/r/value/string_value.R @@ -7,6 +7,8 @@ string" r"("some raw string business")" +r"|some raw string|" +r"-|some raw string|-" # ------------------------------------------------------------------------ # Comments diff --git a/crates/air_r_formatter/tests/specs/r/value/string_value.R.snap b/crates/air_r_formatter/tests/specs/r/value/string_value.R.snap index 3a3f57ab..b99f3714 100644 --- a/crates/air_r_formatter/tests/specs/r/value/string_value.R.snap +++ b/crates/air_r_formatter/tests/specs/r/value/string_value.R.snap @@ -14,6 +14,8 @@ info: r/value/string_value.R string" r"("some raw string business")" +r"|some raw string|" +r"-|some raw string|-" # ------------------------------------------------------------------------ # Comments @@ -61,6 +63,8 @@ Table: fcase, rowwiseDT, tribble string" r"("some raw string business")" +r"|some raw string|" +r"-|some raw string|-" # ------------------------------------------------------------------------ # Comments diff --git a/plans/aether.md b/plans/aether.md index 52a6dca6..c6dab280 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -160,8 +160,10 @@ it needs, so every commit builds. to `fn({{ x }})`. - Every case from the hand-built tests. -- [ ] Formatter specs for input only aether can parse: `r"-|a|-"` raw strings, - `%a\b%`, and `for (... in x)`. +- [x] Formatter specs for input only aether can parse: `r"|a|"` and + `r"-|a|-"` raw strings in `value/string_value.R`, and `%a\b%` in + `binary_expression.R`. `for (... in x)` already parsed with tree-sitter and + is covered by `for_statement.R`. - [ ] Range formatting across `;` in the LSP tests. @@ -182,7 +184,9 @@ it needs, so every commit builds. - [ ] `CHANGELOG.md`: - Files R rejects are now rejected: two expressions on one line, a stray top-level `;`, `pkg::` followed by a newline, `x$1`. - - `r"-|...|-"`, `%a\b%`, and `for (... in x)` now parse. + - `%a\b%` now parses. + - `r"|...|"` and `r"-|...|-"` are now kept as raw strings. Tree-sitter read + them as `r` followed by a string, which air split onto two lines. - Building air no longer needs a C compiler. - Performance, if the benchmark shows a win. From 518d7aacd24f113eac6a877d18b9946f801861f6 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 13:11:02 -0400 Subject: [PATCH 12/20] Move `semicolons.R` up a level --- .../air_r_formatter/tests/specs/r/{statements => }/semicolons.R | 0 .../tests/specs/r/{statements => }/semicolons.R.snap | 2 +- plans/aether.md | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename crates/air_r_formatter/tests/specs/r/{statements => }/semicolons.R (100%) rename crates/air_r_formatter/tests/specs/r/{statements => }/semicolons.R.snap (99%) diff --git a/crates/air_r_formatter/tests/specs/r/statements/semicolons.R b/crates/air_r_formatter/tests/specs/r/semicolons.R similarity index 100% rename from crates/air_r_formatter/tests/specs/r/statements/semicolons.R rename to crates/air_r_formatter/tests/specs/r/semicolons.R diff --git a/crates/air_r_formatter/tests/specs/r/statements/semicolons.R.snap b/crates/air_r_formatter/tests/specs/r/semicolons.R.snap similarity index 99% rename from crates/air_r_formatter/tests/specs/r/statements/semicolons.R.snap rename to crates/air_r_formatter/tests/specs/r/semicolons.R.snap index 090d0a64..514d8472 100644 --- a/crates/air_r_formatter/tests/specs/r/statements/semicolons.R.snap +++ b/crates/air_r_formatter/tests/specs/r/semicolons.R.snap @@ -1,6 +1,6 @@ --- source: crates/air_formatter_test/src/snapshot_builder.rs -info: r/statements/semicolons.R +info: r/semicolons.R --- # Input diff --git a/plans/aether.md b/plans/aether.md index c6dab280..d78db3c3 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -149,7 +149,7 @@ it needs, so every commit builds. `cargo run -p xtask_accept && git diff --exit-code`. - [x] Formatter specs for semicolons, replacing the hand-built `TODO(aether)` - tests in `r/auxiliary/empty_statement.rs`. New `statements/semicolons.R` + tests in `r/auxiliary/empty_statement.rs`. New `semicolons.R` covers: - `1;2`, `1;`, `1;;2` in braces, `{;}`, `{;1}`. - `1; # c` and `1 ; # c`. From bac7bff92d3a9a1a3fcbfec8d76984017b811ab4 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 13:20:25 -0400 Subject: [PATCH 13/20] With range selection, widen statement selection across semicolons Otherwise `<< 1; >> 2` could reformat as `1 2` which is invalid R code --- crates/lsp/src/handlers_format.rs | 140 +++++++++++++++++- ...at__tests__format_range_semicolons-10.snap | 6 + ...at__tests__format_range_semicolons-11.snap | 6 + ...mat__tests__format_range_semicolons-2.snap | 6 + ...mat__tests__format_range_semicolons-3.snap | 6 + ...mat__tests__format_range_semicolons-4.snap | 7 + ...mat__tests__format_range_semicolons-5.snap | 6 + ...mat__tests__format_range_semicolons-6.snap | 7 + ...mat__tests__format_range_semicolons-7.snap | 6 + ...mat__tests__format_range_semicolons-8.snap | 6 + ...mat__tests__format_range_semicolons-9.snap | 8 + ...ormat__tests__format_range_semicolons.snap | 6 + plans/aether.md | 5 +- 13 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-10.snap create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-11.snap create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-2.snap create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-3.snap create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-4.snap create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-5.snap create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-6.snap create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-7.snap create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-8.snap create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-9.snap create mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons.snap diff --git a/crates/lsp/src/handlers_format.rs b/crates/lsp/src/handlers_format.rs index 465ee56a..9476261f 100644 --- a/crates/lsp/src/handlers_format.rs +++ b/crates/lsp/src/handlers_format.rs @@ -237,12 +237,28 @@ fn find_deepest_enclosing_logical_lines(node: RSyntaxNode, range: TextRange) -> // ```rust // .filter(|node| range.contains_range(node.text_trimmed_range())) // ``` - let logical_lines: Vec = iter + let mut logical_lines: Vec = iter .map(|statement| statement.into_syntax()) .skip_while(|node| !node.text_range_with_trivia().contains(range.start())) - .take_while(|node| node.text_trimmed_range().start() <= range.end()) .collect(); + let mut len = logical_lines + .iter() + .take_while(|node| node.text_trimmed_range().start() <= range.end()) + .count(); + + // Formatting drops `;`, so a statement sharing a line with the last one must + // come along or the two would be joined + if len > 0 { + while logical_lines + .get(len) + .is_some_and(|node| !node.has_leading_newline()) + { + len += 1; + } + } + + logical_lines.truncate(len); logical_lines } @@ -546,6 +562,126 @@ mod tests { insta::assert_snapshot!(output); } + // Formatting drops `;`, so a range ending before another statement on the + // same line widens to include it + #[tokio::test] + async fn test_format_range_semicolons() { + let mut client = new_test_client().await; + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<>; b +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<> b +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<> ; b +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<>; b; c +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"a; <>; c +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<>; c +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<> +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"{ <>; b } +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"if (x) <>; b +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + + // A `;` before a line break is dropped without widening + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<> +b +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = Document::doodle_and_range( +"<>; # c +b +", + ); + let output = client + .format_document_range(&doc, FileName::Random, range) + .await; + insta::assert_snapshot!(output); + } + #[tokio::test] async fn test_format_range_multiple_lines() { let mut client = new_test_client().await; diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-10.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-10.snap new file mode 100644 index 00000000..bdd92c8e --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-10.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +a +b diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-11.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-11.snap new file mode 100644 index 00000000..7bbdbc05 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-11.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +a # c +b diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-2.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-2.snap new file mode 100644 index 00000000..bdd92c8e --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-2.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +a +b diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-3.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-3.snap new file mode 100644 index 00000000..bdd92c8e --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-3.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +a +b diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-4.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-4.snap new file mode 100644 index 00000000..dad228a7 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-4.snap @@ -0,0 +1,7 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +a +b +c diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-5.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-5.snap new file mode 100644 index 00000000..c09784b9 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-5.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +a; b +c diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-6.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-6.snap new file mode 100644 index 00000000..dad228a7 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-6.snap @@ -0,0 +1,7 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +a +b +c diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-7.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-7.snap new file mode 100644 index 00000000..bdd92c8e --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-7.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +a +b diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-8.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-8.snap new file mode 100644 index 00000000..5ace5545 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-8.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +{ a +b } diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-9.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-9.snap new file mode 100644 index 00000000..33ddae60 --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons-9.snap @@ -0,0 +1,8 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +if (x) { + a +} +b diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons.snap new file mode 100644 index 00000000..bdd92c8e --- /dev/null +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_semicolons.snap @@ -0,0 +1,6 @@ +--- +source: crates/lsp/src/handlers_format.rs +expression: output +--- +a +b diff --git a/plans/aether.md b/plans/aether.md index d78db3c3..d7dffab7 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -165,7 +165,10 @@ it needs, so every commit builds. `binary_expression.R`. `for (... in x)` already parsed with tree-sitter and is covered by `for_statement.R`. -- [ ] Range formatting across `;` in the LSP tests. +- [x] Range formatting across `;` in the LSP tests. A range ending before + another statement on the same line joined the two, as in `<>; b` to + `a b`, since formatting drops the `;`. Such a range now widens to the end of + the line. - [ ] Snapshots: the LSP `document_syntax` snapshots. They pass unchanged after the copy, so this is only a check that the later bullets leave them From e78e32801569785702a46b0b383752774d6c8fe4 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 13:21:30 -0400 Subject: [PATCH 14/20] Double check LSP snapshots --- plans/aether.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plans/aether.md b/plans/aether.md index d7dffab7..fcfb24de 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -170,9 +170,9 @@ it needs, so every commit builds. `a b`, since formatting drops the `;`. Such a range now widens to the end of the line. -- [ ] Snapshots: the LSP `document_syntax` snapshots. They pass unchanged +- [x] Snapshots: the LSP `document_syntax` snapshots. They pass unchanged after the copy, so this is only a check that the later bullets leave them - alone. + alone. They are byte-identical to `main`'s, so there is no commit. - [ ] `CLAUDE.md`: - Root: update the crate layout, say that the parser is hand-written, and From 77138d9a63eb2fa8c497a42ee0cf131e3777bc33 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 13:29:21 -0400 Subject: [PATCH 15/20] Pull over aether's `CLAUDE.md` for the parser --- CLAUDE.md | 7 +++-- crates/air_r_parser/CLAUDE.md | 54 +++++++++++++++++++++++++++++++++++ plans/aether.md | 2 +- 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 crates/air_r_parser/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index 8e8ada76..6f7436ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,8 +37,11 @@ Air is an R language server and formatter written in Rust. Workspace crate layou ### Running Tests - Use `just test` to run tests via nextest, NOT `cargo test` -- Example: `just test -p workspace --lib discovery::test` -- Passes arguments through to `cargo nextest run` + - Example: `just test -p workspace --lib discovery::test` + - Passes arguments through to `cargo nextest run` +- `just test-snapshot-accept` regenerates the parser's markdown snapshots +- `just test-insta` regenerates the formatter's insta snapshots, `cargo insta review` to accept them +- `just test-quick-parser` and `just test-quick-formatter` run the `quick_test.rs` scratchpads ### Unit Tests diff --git a/crates/air_r_parser/CLAUDE.md b/crates/air_r_parser/CLAUDE.md new file mode 100644 index 00000000..c04b6cc5 --- /dev/null +++ b/crates/air_r_parser/CLAUDE.md @@ -0,0 +1,54 @@ +# air_r_parser + +A hand-written Rust parser for R, built on Biome's parser infrastructure. + +## Invariants, do not break these + +1. `parse(src).syntax().to_string() == src` for ALL inputs. Always. + +2. Bogus nodes always come with at least one diagnostic. + +3. The parser never panics and never loops forever. + +## Test corpus + +The corpus lives in `crates/air_r_parser_tests/tests/corpus/**/*.md`. One file +per concept, one test per `##` heading. + +Each concept has two files. You write `X.md`, which holds headings, prose, and +\`\`\`r blocks: + +````md +## Colon + +```r +1:2 +``` + +## Closing brace [error] + +A stray `}` is reported and skipped, and the parse keeps going. + +```r +} +``` +```` + +`just test-snapshot-accept` generates `X.snap.md` from it. The snapshot repeats +the heading, the prose, and each source, and adds a cst and a diagnostics block +after each source. Never hand-edit a snapshot. To change a test, edit `X.md` and +re-run `just test-snapshot-accept`. + +Prose between blocks is copied to the snapshot and otherwise ignored. Use it to +say why a test exists. Do not use an R comment for that, since a comment is +input and changes the tree. + +The heading marker sets what the blocks under it must do: + +- `[error]`: each block produces at least one error diagnostic. + +- No marker: each block parses with zero diagnostics and no bogus nodes. + +## Reference implementations + +- Biome's JS/CSS parsers. When in doubt, do what `biome_js_parser` does. diff --git a/plans/aether.md b/plans/aether.md index fcfb24de..ece2fbb1 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -174,7 +174,7 @@ it needs, so every commit builds. after the copy, so this is only a check that the later bullets leave them alone. They are byte-identical to `main`'s, so there is no commit. -- [ ] `CLAUDE.md`: +- [x] `CLAUDE.md`: - Root: update the crate layout, say that the parser is hand-written, and document `just accept` and `just test-quick-parser`. - New nested `crates/air_r_parser/CLAUDE.md`, a trimmed copy of aether's. It From 03376439474ae75bcbf445e2045b955bdf930327 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 13:33:42 -0400 Subject: [PATCH 16/20] Clean up a few outdated docs --- CONTRIBUTING.md | 2 +- plans/aether.md | 2 +- xtask/README.md | 2 +- xtask/codegen/README.md | 6 ++++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b9b509d..573c82b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -189,4 +189,4 @@ Install the nextest cli tool using a [prebuilt binary](https://nexte.st/docs/ins Run tests locally with `just test`, which calls `cargo nextest run`. Run insta snapshot tests in "update" mode with `just test-insta`. Regenerate the parser's markdown snapshots with `just test-snapshot-accept`. -On CI we use the nextest profile found in `.config/nextest.toml`. +The nextest profiles are in `.config/nextest.toml`. Local runs use `default`, and CI uses `ci`. diff --git a/plans/aether.md b/plans/aether.md index ece2fbb1..1565c792 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -182,7 +182,7 @@ it needs, so every commit builds. implementations. It drops the `deviations.md` references, since that file doesn't come over. -- [ ] Docs: `CONTRIBUTING.md`, `xtask/README.md`, `xtask/codegen/README.md`. +- [x] Docs: `CONTRIBUTING.md`, `xtask/README.md`, `xtask/codegen/README.md`. - [ ] `CHANGELOG.md`: - Files R rejects are now rejected: two expressions on one line, a stray diff --git a/xtask/README.md b/xtask/README.md index 5f286591..31238766 100644 --- a/xtask/README.md +++ b/xtask/README.md @@ -1,5 +1,5 @@ # xtask -This directory contains Rust scripts for code generation. +This directory contains Rust scripts for development tasks. Check the `justfile` for usage. diff --git a/xtask/codegen/README.md b/xtask/codegen/README.md index a90a292f..6662ea49 100644 --- a/xtask/codegen/README.md +++ b/xtask/codegen/README.md @@ -11,3 +11,9 @@ The project uses a fork of [`ungrammar`](https://github.com/rust-analyzer/ungram `ungrammar` uses a DSL to define and parse the grammar of a language. Once the library parses the DSL files, some custom logic generates the AST APIs. + +## `just gen-formatter` + +This command creates a stub formatting rule in `air_r_formatter` for each node that doesn't have one yet, and regenerates `air_r_formatter/src/generated.rs`. Existing rules are never overwritten. + +It stages the files it writes, and stops with an error if a file it would write has uncommitted changes. From a90f505d75ee277fe58cf7b74939a5b9702fc634 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 13:47:51 -0400 Subject: [PATCH 17/20] Update CHANGELOG.md --- CHANGELOG.md | 30 +++++++++++++++++++++++++++++- plans/aether.md | 2 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fcfc8e2..6f273c45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,35 @@ # Development version -- `for` loops with `...` or `..1` as the loop variable, like `for (... in x)`, are now supported. +- Air no longer utilizes [tree-sitter-r](https://github.com/r-lib/tree-sitter-r) for parsing R files. Instead, Air now ships with a completely custom R parser written entirely in Rust. This comes with a number of benefits! + + - Air is now roughly 2x faster. Formatting all of dplyr has gone from 300ms to 150ms on an M2 Mac. The actual parsing stage is around 7x faster. + + - Air now emits precise error messages when a file fails to parse due to syntax errors. + + - Air no longer has C in its dependency chain. This means that we'll be able to add WASM support for Air much easier now. + + If you are building a Rust based project that needs to parse R files, we encourage you to use our `air_r_parser` crate (pinned to a git SHA since we haven't put it on crates.io yet). + +- We've fixed a number of minor issues through the new parser: + + - Air more accurately accepts certain syntax: + + - `r"|...|"`, which are raw strings using the undocumented `|` delimiter + + - `%a\b%` where previously the `\` failed to parse + + - `for` loops with `...` or `..1` as the loop variable, like `for (... in x)` + + - Air more accurately rejects certain syntax errors: + + - `1 2`, which are two expressions side by side without a semicolon or newline + + - Stray top-level `;` + + - `pkg::` without a right-hand side + + - `x$1` where the right-hand side is not an identifier # 0.12.0 diff --git a/plans/aether.md b/plans/aether.md index 1565c792..92a308b9 100644 --- a/plans/aether.md +++ b/plans/aether.md @@ -184,7 +184,7 @@ it needs, so every commit builds. - [x] Docs: `CONTRIBUTING.md`, `xtask/README.md`, `xtask/codegen/README.md`. -- [ ] `CHANGELOG.md`: +- [x] `CHANGELOG.md`: - Files R rejects are now rejected: two expressions on one line, a stray top-level `;`, `pkg::` followed by a newline, `x$1`. - `%a\b%` now parses. From c4aa5b4f46c28116c3a7dbf10b5a38fe3dd27492 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 14:17:48 -0400 Subject: [PATCH 18/20] Fix the crate snapshot --- crates/crates/src/snapshots/crates__tests__crate_names.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/crates/src/snapshots/crates__tests__crate_names.snap b/crates/crates/src/snapshots/crates__tests__crate_names.snap index a767380e..65f8d208 100644 --- a/crates/crates/src/snapshots/crates__tests__crate_names.snap +++ b/crates/crates/src/snapshots/crates__tests__crate_names.snap @@ -23,6 +23,7 @@ expression: AIR_CRATE_NAMES "tests_macros", "workspace", "xtask", + "xtask_accept", "xtask_codegen", "xtask_json_schema", "zed_air", From 662961e8c77ee7ec1f6c53acfbefb19f775f3d60 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 15:14:03 -0400 Subject: [PATCH 19/20] Add diagnostics plan --- plans/diagnostics.md | 309 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 plans/diagnostics.md diff --git a/plans/diagnostics.md b/plans/diagnostics.md new file mode 100644 index 00000000..01dffaec --- /dev/null +++ b/plans/diagnostics.md @@ -0,0 +1,309 @@ +# Parse diagnostics in the CLI + +Status: draft, ready for review. + +## Goal + +When `air format` can't parse a file, show the user where and why, with a code +frame, instead of a single opaque line. + +Today: + +``` +ERROR Failed to format R/foo.R: Failed to parse due to syntax errors. +``` + +After: + +``` +R/foo.R:3:8 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression but instead found ')'. + + 1 │ f <- function(x) { + 2 │ y <- 1 + > 3 │ g(x, ) + │ ^ + 4 │ } + 5 │ + +ERROR Failed to parse 1 file. +``` + +This resolves the `TODO(aether)` in `crates/workspace/src/format.rs`. + +## Where things stand + +- `air_r_parser::parse()` returns an `RParse` whose `diagnostics()` are + `biome_parser::diagnostic::ParseDiagnostic`s. Each carries a `TextRange` + span, a message, and optional detail advices with their own spans, as in + "Reached the end of the file looking for a closing delimiter". + +- `workspace::format::format_source()` already hands them back, in + `FormatSourceError::Parse(Vec)`. The CLI only ever prints + that variant's `Display`, so the diagnostics are dropped. + +- `format_path()` in `crates/air/src/commands/format/paths.rs` and + `format_stdin()` in `stdin.rs` both drop the source text on the error + path. The renderer needs that text to turn byte offsets into lines and + columns. + +- `air_r_parser_tests` already renders these diagnostics into the corpus + snapshots, using `biome_diagnostics::PrintDiagnostic::verbose()` with + `with_file_path()` and `with_file_source_code()`. The CLI takes the same + path. Both move to `simple()`, which prints the same bytes as `verbose()` + for parse diagnostics today, so users see what the corpus snapshots show. + +- How air decides on color today is inconsistent: + + - `colored` checks whether stdout is a terminal, but everything it colors + goes to stderr. + + - `tracing-subscriber` only checks `NO_COLOR`, and never checks for a + terminal. + + So `air format . 2>log` run in a terminal writes ANSI codes into `log`. + +- The LSP never calls `format_source()`. It checks `has_errors()` itself and + is unaffected. + +## Renderer + +Use `biome_diagnostics`, which is already a workspace dependency. Things +worth knowing about it: + +- Columns are counted in characters, not bytes, so non-ASCII R code reports + the right column. Lines split on `\n`, `\r\n`, and a lone `\r`, and the + frame trims line endings, so CRLF files render cleanly. This matters + because the CLI doesn't normalize line endings. + +- Frames show two lines of context, and frames longer than 8 lines get an + ellipsis. + +- Tabs are printed as tabs, and carets are padded by display width, so + markers line up under tabs and wide characters. + +- The `━` rule after the header is padded to 100 columns in debug builds and + to the terminal width in release builds. It measures the terminal on + stdout, with a fallback of 100. Integration tests run a debug binary, so + snapshots are stable. + +- The header changes with the terminal: under `TERM_PROGRAM=vscode` it skips + the hyperlink, under JetBrains it prefixes ` at `, and otherwise an + absolute path becomes an OSC 8 `file://` link. We pass relative paths + whenever the file is under the working directory, so in practice only + files outside it get links. This is acceptable as is. + +- On Windows, `biome_console` swaps a few emoji for ASCII. `×` is already + ASCII-safe, and the corpus snapshots pass on Windows CI today. + +## Commits + +1. Decide color once, based on stderr. This fixes the existing inconsistency + on its own, before any diagnostics work. + +2. Render parse diagnostics. + +## Design + +### Output streams + +stdout carries only formatted code, in stdin mode. Everything else goes to +stderr, as it does today. That includes the code frames, the `ERROR` lines, +the summary, and `Would reformat:`. + +For comparison: + +- biome sends diagnostics to stderr, and the run summary to stdout. + +- `ruff check` and `ty check` send diagnostics to stdout, as the command's + result. + +- `ruff format` puts parse errors on stdout as code frames under `--check`. + In write mode and stdin mode, it falls back to one-line `error!` messages on + stderr, because stdout is taken. + +Air can't use stdout for diagnostics, because in stdin mode stdout is the +formatted file. Using stderr everywhere gives full code frames in every mode, +including the editor's stdin path. Moving `Would reformat:` to stdout to match +ruff would be a breaking change, and is out of scope. + +### Color + +Commit 1, in `crates/air/src/logging.rs`, or wherever `run()` sets up +logging: + +- Compute one `bool` at startup: + `!no_color && NO_COLOR is unset or empty && stderr().is_terminal()`. + +- Apply it everywhere, with `colored::control::set_override(color)` and + `layer.with_ansi(color)`. This replaces `turn_off_colors()`. + `set_override(true)` is only reached once `NO_COLOR` has already been + checked, so it never overrides it. + +- Every later caller reads `colored::control::SHOULD_COLORIZE.should_colorize()`, + as ruff does, so there is one source of truth and nothing to pass around. + +- `CLICOLOR` and `CLICOLOR_FORCE` stop having an effect. `colored` only + respected them by accident, and they are not documented in `--help`. + `FORCE_COLOR` is not added. + +Commit 2 maps `should_colorize()` to `ColorChoice::Always` or +`ColorChoice::Never` for the `termcolor::StandardStream::stderr()` the +diagnostics are written to. + +### `workspace` + +- Delete the `TODO(aether)` comment. `FormatSourceError::Parse` keeps its + payload and its `Display`, which stays the fallback for any caller that + doesn't render diagnostics. + +### `air` crate + +- Add `biome_console` and `biome_diagnostics` as dependencies. + `biome_diagnostics` re-exports `termcolor`, so nothing else is needed. + +- New module `crates/air/src/commands/format/diagnostics.rs`, shared by + `paths.rs` and `stdin.rs`, with two functions: + + - A `ParseFailure` struct, holding a display path, the source text, and + the `Vec`. + + - `print_parse_failures(failures: &[ParseFailure], writer: &mut impl WriteColor)`, + which renders every diagnostic of every failure, in order. + +- Switch `render_diagnostics()` in `crates/air_r_parser_tests/src/check.rs` + from `verbose()` to `simple()`, so the corpus snapshots keep showing exactly + what the CLI prints. `ParseDiagnostic` has no verbose advices, so no + snapshot changes, and `just accept` should produce no diff. + +- Each diagnostic is rendered as biome's terminal reporter does: + `diagnostic.with_file_path(path).with_file_source_code(source)`, then + `PrintDiagnostic::simple()` through + `biome_console::fmt::Formatter::new(&mut Termcolor(writer))`. Attaching the + source copies it once per diagnostic. That's fine, since this only runs + when parsing fails. + +- Print every diagnostic. There is no cap and no `--max-diagnostics` flag. + +### `paths.rs` + +- `FormatPathError` gains a `Parse(ParseFailure)` variant. `format_path()` + matches `FormatSourceError::Parse(diagnostics)` and moves `old` and the + relativized path into it. All other `FormatSourceError` variants keep + going to `FormatPathError::Format`. + +- In both `FormatMode::Write` and `FormatMode::Check`, `format()` splits the + collected errors into parse failures and everything else, then: + + 1. Sorts the parse failures by path. Discovery runs in parallel, so without + sorting the output order wouldn't be deterministic. + + 2. Prints them with `print_parse_failures()` to a locked + `termcolor::StandardStream::stderr()`. + + 3. Logs the other errors with `tracing::error!`, as today. + + 4. If any files failed to parse, logs one summary through + `tracing::error!`: `Failed to parse 1 file.` or + `Failed to parse 3 files.`. + + 5. In check mode, prints the `Would reformat:` lines, as today. + +- The exit status doesn't change. Any error still gives `ExitStatus::Error`. + +### `stdin.rs` + +- `FormatStdinError` gains `Parse(ParseFailure)`. `format_stdin()` takes + the `--stdin-file-path` path, which is required for stdin, and uses + `relativize_path()` on it for the header, so editors that pipe through + stdin show a real file location. + +- When parsing fails, it prints the failure and then + `Failed to parse 1 file.`, matching the paths output. + +### Log level + +- The diagnostic frames are written straight to stderr, not through + `tracing`, so `--log-level` doesn't filter them. They are the command's + output, like `Would reformat:`. The summary line does go through + `tracing`, so it does respect the log level. + +## Tests + +All as integration snapshots in `crates/air/tests/integration/format.rs`, +run with `--no-color`: + +- One file, one error, in write mode. Replaces the snapshot for + `stdin_errors_on_parse_error`, which moves to the new output. + +- One file with several errors, to check that every one prints in source + order. + +- An error with a detail advice, like an unterminated string, to check that + the second frame renders. + +- Two broken files and one good file, passed in reverse alphabetical order, + to check the sorted output and `Failed to parse 2 files.`. + +- `--check` with one broken file and one that needs formatting, to check that + the diagnostics come before `Would reformat:` and the exit code is an + error. + +- A CRLF file, to check that the frame has no stray `\r`. + +- An error after a non-ASCII character on the same line, to check that the + column counts characters. + +- Stdin with `--stdin-file-path R/foo.R`, to check that the header shows + `R/foo.R`. + +- Use `.normalize_os_path_separator()` where a header has a nested path. + +- Commit 1: a test with stderr piped and no `--no-color`, checking that the + `ERROR` label and `Would reformat:` contain no ANSI codes. Integration + tests always pipe stderr, so every existing snapshot already exercises the + case without a terminal. + +## CHANGELOG + +- Under the CLI section: "Files that fail to parse now report each syntax + error with its location and a code frame, instead of 'Failed to parse due + to syntax errors.'" + +- Under the CLI section: "Colored output is now turned off when stderr is not + a terminal, so redirecting stderr to a file no longer writes ANSI codes." + +## Decisions + +- Render with `biome_diagnostics`, not `annotate-snippets`, so the CLI matches + the corpus snapshots. + +- Print every diagnostic, with no cap. + +- Print the frames, then an `ERROR Failed to parse N file(s).` summary. The + old per-file `ERROR Failed to format …` line is dropped for parse errors. + +- Under stdin, the header shows the path from `--stdin-file-path`. + +- Everything goes to stderr except formatted code. That includes the code + frames, even under `--check`. + +- Color is decided once, from stderr, `--no-color`, and `NO_COLOR`, then + pushed into `colored` and `tracing-subscriber`. + +- Use `PrintDiagnostic::simple()`, as biome's CLI does, in both the CLI and + the corpus harness. + +- Copy the source per diagnostic with `with_file_source_code()`, rather than + writing a borrowing `Diagnostic` wrapper. It's simpler, and this only + runs on failure. + +## Out of scope + +- Publishing parse diagnostics from the LSP. + +- Formatting around local parse errors. + +- A `--max-diagnostics` flag. If cascades from error recovery turn out to be + noisy in practice, this is where we'd revisit it. From 10af38dcab24dbbcadefefb81db09a07b9773d1d Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 22 Oct 2026 15:14:20 -0400 Subject: [PATCH 20/20] Delete aether plan! --- plans/aether.md | 244 ------------------------------------------------ 1 file changed, 244 deletions(-) delete mode 100644 plans/aether.md diff --git a/plans/aether.md b/plans/aether.md deleted file mode 100644 index 92a308b9..00000000 --- a/plans/aether.md +++ /dev/null @@ -1,244 +0,0 @@ -# Moving air onto aether - -Status: ready to start the swap. One open item at the bottom. - -## Goal - -Bring all of aether into air under the `air_` prefix. Aether's crates replace -air's crates of the same role, and aether's test crates come along unchanged. -After the swap, air's parser is aether's hand-written parser, and tree-sitter is -gone. - -This is a plain copy, not a dependency. Air does not pin aether by git, and -there is no `aether_r_*` rename across air's code. If the parser ever needs to -live on its own again, it can be split back out. - -Success means: - -- `crates/air_r_parser` is aether's parser, and `tree-sitter` and - `tree-sitter-r` are gone from the workspace. - -- `air_r_parser_tests` and `air_r_test_macros` exist, and `just accept` and - `just test-quick-parser` work. - -- Every formatter spec snapshot is unchanged, except for new specs covering - input that only aether can parse. - -## Rule for collisions - -When air and aether disagree on a file, crate, setting, or dependency, aether -wins. Exceptions are listed under Decisions. - -## Where the two repos stand - -The prep work (P1 to P7, all merged) has brought air's tree-sitter walker in -line with aether's tree shape. What's left to reconcile is small: - -- `r.ungram`: byte-identical. - -- `air_r_syntax` vs `aether_r_syntax`, after swapping the prefix: aether adds - `string_ext.rs` and `pub use biome_rowan::TokenText`. It also splits one - `use crate::{RRoot, RSyntaxKind}` into two lines. - -- `air_r_factory` vs `aether_r_factory`: identical after swapping the prefix. - -- `biome_ungrammar`: identical, except air has a stray `Cargo.lock`. - -- `air_r_parser` vs `aether_r_parser`: same public API (`RParse`, `parse()`, - `RParserOptions`, `has_errors()`, `diagnostics()`, `into_diagnostics()`, - `From for AnyParse`). The internals are fully replaced. - -- `xtask`: aether's is a superset of air's. It adds `accept.rs`, a `main.rs`, - a `bpaf` `TaskCommand`, and a `project_root()` that searches for - `[workspace]`. - -- `xtask/codegen`: aether's is air's minus `formatter.rs`, the `Formatter` and - `All` commands, and `LanguageKind::formatter_crate_name()`. - -- Air's `tests_macros` stays. The formatter spec harness still uses it. - `air_r_test_macros` is a different, smaller macro for the markdown corpus. - -## The stack - -Two prep PRs on `main`, then one swap PR. - -### Prep 1: drop the tree-sitter LSP view - -Self-contained, and it can land now. - -- `crates/lsp/src/handlers_ext.rs`: remove `ViewFileKind::TreeSitter` and - `format_ts_node()`. - -- `crates/lsp/Cargo.toml`: drop `tree-sitter` and `tree-sitter-r`. - -- `editors/code`: remove `air.viewTreeSitter` from `commands.ts`, - `package.json`, and the `"TreeSitter"` member of `ViewFileKind` in - `request/viewFile.ts`. - -- Changelog bullet for the VS Code extension. - -### Prep 2: `test-health.yml` with fmt and clippy - -Air's CI runs neither today. - -- New `.github/workflows/test-health.yml`, wired into `test.yml`. It runs - `cargo fmt --all --check` and - `cargo clippy --workspace --all-targets -- -D warnings` on Linux. - -- Fix whatever they flag in air's code, in its own commit. - -- Take aether's `rust-toolchain.toml`, which adds - `components = ["rustfmt", "clippy"]`. - -- Rename the clippy lint `empty_enum` to `empty_enums`, as aether does. - -The swap PR adds two more checks to the same workflow. - -### The swap PR - -Each bullet is roughly one commit. Each bullet adds the workspace dependencies -it needs, so every commit builds. - -- [x] Copy aether's crates in, renaming `aether` to `air` in crate names, - paths, idents, and descriptions: - - `aether_r_parser` replaces `crates/air_r_parser` wholesale, including - deleting `treesitter.rs`, the old spec harness, and the 78 fixtures under - `tests/snapshots/`. Aether's corpus replaces them. - - `aether_r_syntax` replaces `crates/air_r_syntax`. - - `aether_r_factory` replaces `crates/air_r_factory`. - - `aether_r_parser_tests` arrives as `crates/air_r_parser_tests`. - - `aether_r_test_macros` arrives as `crates/air_r_test_macros`. - - `biome_ungrammar` replaces air's copy, dropping the stray `Cargo.lock`. - - Add `air_r_parser_tests`, `air_r_test_macros`, `biome_console`, - `biome_diagnostics`, `similar`, and `unicode-ident` to the workspace - dependencies. - - Update the `crates/crates` crate list snapshot, which gains - `air_r_parser_tests` and `air_r_test_macros`. - -- [x] `xtask`: take aether's `project_root()`, which searches for - `[workspace]`. Aether's `accept` becomes its own crate, `xtask/accept` - (`xtask_accept`), mirroring `xtask/codegen` and `xtask/json_schema`. Add - `"xtask/accept"` to workspace `members`. - -- [x] `xtask/codegen`: start from aether's crate, then re-add air's - `formatter.rs` unchanged (`git2` included), the `Formatter` and `All` - commands, and `formatter_crate_name()`. Add `bpaf` to the workspace - dependencies, as aether's codegen uses it from there. Run `just gen-grammar` - and `just gen-formatter`. Both should produce no diff. - -- [x] Workspace `Cargo.toml`, for what the earlier bullets didn't need: - - Keep `serde` without `derive`, unlike aether. Instead, `lsp`, - `lsp_test`, and `settings` ask for `derive` themselves, as every other - crate that derives already does. - - Remove `tree-sitter`, `tree-sitter-r`, and `biome_unicode_table`, which - nothing uses after the copy. - - Move `git2` into the workspace dependencies, so `xtask_codegen` inherits - it like everything else. - -- [x] `.config/nextest.toml`: take aether's, verbatim. - -- [x] `justfile`: - - `gen-grammar` stays, and runs the merged codegen. - - Aether's `test` is dropped. Air's `test *ARGS` stays. - - Aether's `accept` is added, running `cargo run -p xtask_accept`. - - Aether's `quick` is added as `test-quick-parser`. - - Air's `test-quick` becomes `test-quick-formatter`. - -- [x] `test-health.yml`: add aether's two checks, which are - `just gen-grammar && git diff --exit-code` and - `cargo run -p xtask_accept && git diff --exit-code`. - -- [x] Formatter specs for semicolons, replacing the hand-built `TODO(aether)` - tests in `r/auxiliary/empty_statement.rs`. New `semicolons.R` - covers: - - `1;2`, `1;`, `1;;2` in braces, `{;}`, `{;1}`. - - `1; # c` and `1 ; # c`. - - A comment before a `;` on the next line, as in `{1 # c` then `;}`. - - Blank lines around a lone `;` in braces. - - `# fmt: skip` on a statement ending in `;` (kept verbatim, `;` included). - - Curly-curly with a `;`: `fn({{ x }; })` and `fn({{ x; }})` both format - to `fn({{ x }})`. - - Every case from the hand-built tests. - -- [x] Formatter specs for input only aether can parse: `r"|a|"` and - `r"-|a|-"` raw strings in `value/string_value.R`, and `%a\b%` in - `binary_expression.R`. `for (... in x)` already parsed with tree-sitter and - is covered by `for_statement.R`. - -- [x] Range formatting across `;` in the LSP tests. A range ending before - another statement on the same line joined the two, as in `<>; b` to - `a b`, since formatting drops the `;`. Such a range now widens to the end of - the line. - -- [x] Snapshots: the LSP `document_syntax` snapshots. They pass unchanged - after the copy, so this is only a check that the later bullets leave them - alone. They are byte-identical to `main`'s, so there is no commit. - -- [x] `CLAUDE.md`: - - Root: update the crate layout, say that the parser is hand-written, and - document `just accept` and `just test-quick-parser`. - - New nested `crates/air_r_parser/CLAUDE.md`, a trimmed copy of aether's. It - keeps the invariants, the corpus test format, and the reference - implementations. It drops the `deviations.md` references, since that file - doesn't come over. - -- [x] Docs: `CONTRIBUTING.md`, `xtask/README.md`, `xtask/codegen/README.md`. - -- [x] `CHANGELOG.md`: - - Files R rejects are now rejected: two expressions on one line, a stray - top-level `;`, `pkg::` followed by a newline, `x$1`. - - `%a\b%` now parses. - - `r"|...|"` and `r"-|...|-"` are now kept as raw strings. Tree-sitter read - them as `r` followed by a string, which air split onto two lines. - - Building air no longer needs a C compiler. - - Performance, if the benchmark shows a win. - -- [ ] Validation before merge: - - Ecosystem diff: format a fixed list of GitHub repos, cloned at pinned SHAs, - with `main` and with the branch, then diff. Every formatting difference must - be explained, and every file that newly fails to parse must be one R also - rejects. - - CLI benchmark on the same corpus. - -## Decisions - -- Plain copy, not a git subtree. Blame for parser files starts at the import - commit. - -- One swap PR after two prep PRs, not a longer stack. - -- Aether's `docs/deviations.md` does not come over. - -- Aether's `CLAUDE.md` becomes a nested `crates/air_r_parser/CLAUDE.md`. - -- `formatter.rs` comes over unchanged, `git2` and all. - -- `accept` lives in its own `xtask_accept` crate rather than in `xtask`, so - `xtask_codegen` doesn't build the parser it generates code for. - -- The workspace `serde` stays without `derive`. Each crate that derives asks - for the feature itself. - -- Aether's `.config/nextest.toml` is taken verbatim, including the 3×10s - terminate. Any LSP test that trips it gets fixed. - -- The CLI keeps today's one-line parse error. Rich diagnostics are a follow-up. - -- What happens to the aether repo is out of scope. - -## Follow-ups after the swap - -- Print parse diagnostics with locations from the CLI, instead of "Failed to - parse due to syntax errors." This is the `TODO(aether)` in - `crates/workspace/src/format.rs`. - -- Consider publishing parse diagnostics from the LSP, now that they carry - ranges. - -- Consider formatting around local errors, since the parser keeps the rest of - the tree intact. Air refuses today, and that stays true in this plan. - -## Open - -1. The repo list for the ecosystem diff. It needs to go here before the swap PR - is opened.