diff --git a/javascript/packages/linter/test/rules/parser-no-errors.test.ts b/javascript/packages/linter/test/rules/parser-no-errors.test.ts index b8d52b03a..e214756c4 100644 --- a/javascript/packages/linter/test/rules/parser-no-errors.test.ts +++ b/javascript/packages/linter/test/rules/parser-no-errors.test.ts @@ -56,7 +56,8 @@ describe("ParserNoErrorsRule", () => { }) test("should report Ruby parse errors in ERB tags", () => { - expectError("expect_expression_after_operator: unexpected ';'; expected an expression after the operator (`RUBY_PARSE_ERROR`)") + expectError("expect_expression_after_operator: unexpected end-of-input; expected an expression after the operator (`RUBY_PARSE_ERROR`)") + expectError("unexpected_token_close_context: unexpected end-of-input, assuming it is closing the parent top level context (`RUBY_PARSE_ERROR`)") assertOffenses(`<%= 1 + %>`) }) diff --git a/src/extract.c b/src/extract.c index 16a6740ad..8f88fd4f5 100644 --- a/src/extract.c +++ b/src/extract.c @@ -10,6 +10,7 @@ void herb_extract_ruby_to_buffer_with_semicolons(const char* source, hb_buffer_T* output) { hb_array_T* tokens = herb_lex(source); bool skip_erb_content = false; + bool is_comment_tag = false; for (size_t i = 0; i < hb_array_size(tokens); i++) { const token_T* token = hb_array_get(tokens, i); @@ -21,8 +22,15 @@ void herb_extract_ruby_to_buffer_with_semicolons(const char* source, hb_buffer_T } case TOKEN_ERB_START: { - if (strcmp(token->value, "<%#") == 0 || strcmp(token->value, "<%%") == 0 || strcmp(token->value, "<%%=") == 0) { + if (strcmp(token->value, "<%#") == 0) { + skip_erb_content = true; + is_comment_tag = true; + } else if (strcmp(token->value, "<%%") == 0 || strcmp(token->value, "<%%=") == 0) { skip_erb_content = true; + is_comment_tag = false; + } else { + skip_erb_content = false; + is_comment_tag = false; } hb_buffer_append_whitespace(output, range_length(token->range)); @@ -40,11 +48,36 @@ void herb_extract_ruby_to_buffer_with_semicolons(const char* source, hb_buffer_T } case TOKEN_ERB_END: { + bool was_comment = is_comment_tag; skip_erb_content = false; + is_comment_tag = false; + + if (was_comment) { + hb_buffer_append_whitespace(output, range_length(token->range)); + break; + } + + bool needs_semicolon = false; + uint32_t current_line = token->location.end.line; - hb_buffer_append_char(output, ' '); - hb_buffer_append_char(output, ';'); - hb_buffer_append_whitespace(output, range_length(token->range) - 2); + for (size_t j = i + 1; j < hb_array_size(tokens); j++) { + const token_T* next_token = hb_array_get(tokens, j); + + if (next_token->type == TOKEN_NEWLINE) { break; } + + if (next_token->type == TOKEN_ERB_START && next_token->location.start.line == current_line) { + needs_semicolon = true; + break; + } + } + + if (needs_semicolon) { + hb_buffer_append_char(output, ' '); + hb_buffer_append_char(output, ';'); + hb_buffer_append_whitespace(output, range_length(token->range) - 2); + } else { + hb_buffer_append_whitespace(output, range_length(token->range)); + } break; } diff --git a/test/c/main.c b/test/c/main.c index ef4238806..9ec6e06bf 100644 --- a/test/c/main.c +++ b/test/c/main.c @@ -11,6 +11,7 @@ TCase *io_tests(void); TCase *lex_tests(void); TCase *token_tests(void); TCase *util_tests(void); +TCase *extract_tests(void); Suite *herb_suite(void) { Suite *suite = suite_create("Herb Suite"); @@ -25,6 +26,7 @@ Suite *herb_suite(void) { suite_add_tcase(suite, lex_tests()); suite_add_tcase(suite, token_tests()); suite_add_tcase(suite, util_tests()); + suite_add_tcase(suite, extract_tests()); return suite; } diff --git a/test/c/test_extract.c b/test/c/test_extract.c new file mode 100644 index 000000000..07a9d27ce --- /dev/null +++ b/test/c/test_extract.c @@ -0,0 +1,106 @@ +#include "include/test.h" + +#include "../../src/include/herb.h" +#include "../../src/include/extract.h" +#include "../../src/include/util/hb_buffer.h" + +TEST(extract_ruby_single_erb_no_semicolon) + char* source = "<% if %>\n<% end %>"; + char* result = herb_extract_ruby_with_semicolons(source); + + char expected[] = " if \n end "; + ck_assert_str_eq(result, expected); + + free(result); +END + +TEST(extract_ruby_multiple_erb_same_line_with_semicolon) + char* source = "<% x = 1 %> <% y = 2 %>"; + char* result = herb_extract_ruby_with_semicolons(source); + + ck_assert_str_eq(result, " x = 1 ; y = 2 "); + + free(result); +END + +TEST(extract_ruby_three_erb_same_line_with_semicolons) + char* source = "<% a = 1 %> <% b = 2 %> <% c = 3 %>"; + char* result = herb_extract_ruby_with_semicolons(source); + + ck_assert_str_eq(result, " a = 1 ; b = 2 ; c = 3 "); + + free(result); +END + +TEST(extract_ruby_different_lines_no_semicolons) + char* source = "<% x = 1 %>\n<% y = 2 %>"; + char* result = herb_extract_ruby_with_semicolons(source); + + char expected[] = " x = 1 \n y = 2 "; + ck_assert_str_eq(result, expected); + + free(result); +END + +TEST(extract_ruby_mixed_lines) + char* source = "<% a = 1 %> <% b = 2 %>\n<% c = 3 %>"; + char* result = herb_extract_ruby_with_semicolons(source); + + char expected[] = " a = 1 ; b = 2 \n c = 3 "; + ck_assert_str_eq(result, expected); + + free(result); +END + +TEST(extract_ruby_output_tags_same_line) + char* source = "<%= x %> <%= y %>"; + char* result = herb_extract_ruby_with_semicolons(source); + + ck_assert_str_eq(result, " x ; y "); + + free(result); +END + +TEST(extract_ruby_empty_erb_same_line) + char* source = "<% %> <% %>"; + char* result = herb_extract_ruby_with_semicolons(source); + + ck_assert_str_eq(result, " ; "); + + free(result); +END + +TEST(extract_ruby_comments_skipped) + char* source = "<%# comment %> <% code %>"; + char* result = herb_extract_ruby_with_semicolons(source); + + ck_assert_str_eq(result, " code "); + + free(result); +END + +TEST(extract_ruby_issue_135_if_without_condition) + char* source = "<% if %>\n<% end %>"; + char* result = herb_extract_ruby_with_semicolons(source); + + char expected[] = " if \n end "; + ck_assert_str_eq(result, expected); + + free(result); +END + +TCase *extract_tests(void) { + TCase *extract = tcase_create("Extract"); + + tcase_add_test(extract, extract_ruby_single_erb_no_semicolon); + tcase_add_test(extract, extract_ruby_multiple_erb_same_line_with_semicolon); + tcase_add_test(extract, extract_ruby_three_erb_same_line_with_semicolons); + tcase_add_test(extract, extract_ruby_different_lines_no_semicolons); + tcase_add_test(extract, extract_ruby_mixed_lines); + tcase_add_test(extract, extract_ruby_output_tags_same_line); + tcase_add_test(extract, extract_ruby_empty_erb_same_line); + tcase_add_test(extract, extract_ruby_comments_skipped); + tcase_add_test(extract, extract_ruby_issue_135_if_without_condition); + + return extract; +} diff --git a/test/parser/erb_test.rb b/test/parser/erb_test.rb index 3dabb4c1d..623272aa3 100644 --- a/test/parser/erb_test.rb +++ b/test/parser/erb_test.rb @@ -218,8 +218,15 @@ class ERBTest < Minitest::Spec assert_parsed_snapshot(%(<% content %> %>)) end - test "TODO" do + test "incomplete erb tag" do assert_parsed_snapshot(%(<%= 1 + %>)) end + + test "if without condition" do + assert_parsed_snapshot(<<~HTML) + <% if %> + <% end %> + HTML + end end end diff --git a/test/snapshots/parser/erb_test/test_0039_TODO_d7de40920545c14e5afcda3a2e9f0eb9.txt b/test/snapshots/parser/erb_test/test_0039_TODO_d7de40920545c14e5afcda3a2e9f0eb9.txt deleted file mode 100644 index 59a382d5f..000000000 --- a/test/snapshots/parser/erb_test/test_0039_TODO_d7de40920545c14e5afcda3a2e9f0eb9.txt +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: "Parser::ERBTest#test_0039_TODO" -input: "<%= 1 + %>" ---- -@ DocumentNode (location: (1:0)-(1:10)) -├── errors: (1 error) -│ └── @ RubyParseError (location: (1:9)-(1:10)) -│ ├── message: "expect_expression_after_operator: unexpected ';'; expected an expression after the operator" -│ ├── error_message: "unexpected ';'; expected an expression after the operator" -│ ├── diagnostic_id: "expect_expression_after_operator" -│ └── level: "syntax" -│ -└── children: (1 item) - └── @ ERBContentNode (location: (1:0)-(1:10)) - ├── tag_opening: "<%=" (location: (1:0)-(1:3)) - ├── content: " 1 + " (location: (1:3)-(1:8)) - ├── tag_closing: "%>" (location: (1:8)-(1:10)) - ├── parsed: true - └── valid: false \ No newline at end of file diff --git a/test/snapshots/parser/erb_test/test_0039_incomplete_erb_tag_d7de40920545c14e5afcda3a2e9f0eb9.txt b/test/snapshots/parser/erb_test/test_0039_incomplete_erb_tag_d7de40920545c14e5afcda3a2e9f0eb9.txt new file mode 100644 index 000000000..b79785e60 --- /dev/null +++ b/test/snapshots/parser/erb_test/test_0039_incomplete_erb_tag_d7de40920545c14e5afcda3a2e9f0eb9.txt @@ -0,0 +1,25 @@ +--- +source: "Parser::ERBTest#test_0039_incomplete erb tag" +input: "<%= 1 + %>" +--- +@ DocumentNode (location: (1:0)-(1:10)) +├── errors: (2 errors) +│ ├── @ RubyParseError (location: (1:10)-(1:10)) +│ │ ├── message: "expect_expression_after_operator: unexpected end-of-input; expected an expression after the operator" +│ │ ├── error_message: "unexpected end-of-input; expected an expression after the operator" +│ │ ├── diagnostic_id: "expect_expression_after_operator" +│ │ └── level: "syntax" +│ │ +│ └── @ RubyParseError (location: (1:10)-(1:10)) +│ ├── message: "unexpected_token_close_context: unexpected end-of-input, assuming it is closing the parent top level context" +│ ├── error_message: "unexpected end-of-input, assuming it is closing the parent top level context" +│ ├── diagnostic_id: "unexpected_token_close_context" +│ └── level: "syntax" +│ +└── children: (1 item) + └── @ ERBContentNode (location: (1:0)-(1:10)) + ├── tag_opening: "<%=" (location: (1:0)-(1:3)) + ├── content: " 1 + " (location: (1:3)-(1:8)) + ├── tag_closing: "%>" (location: (1:8)-(1:10)) + ├── parsed: true + └── valid: false \ No newline at end of file diff --git a/test/snapshots/parser/erb_test/test_0040_if_without_condition_ee03fbe2e88a2b56ca5c044200fe19bf.txt b/test/snapshots/parser/erb_test/test_0040_if_without_condition_ee03fbe2e88a2b56ca5c044200fe19bf.txt new file mode 100644 index 000000000..f90424832 --- /dev/null +++ b/test/snapshots/parser/erb_test/test_0040_if_without_condition_ee03fbe2e88a2b56ca5c044200fe19bf.txt @@ -0,0 +1,39 @@ +--- +source: "Parser::ERBTest#test_0040_if without condition" +input: |2- +<% if %> +<% end %> +--- +@ DocumentNode (location: (1:0)-(3:0)) +├── errors: (2 errors) +│ ├── @ RubyParseError (location: (1:3)-(1:5)) +│ │ ├── message: "conditional_if_predicate: expected a predicate expression for the `if` statement" +│ │ ├── error_message: "expected a predicate expression for the `if` statement" +│ │ ├── diagnostic_id: "conditional_if_predicate" +│ │ └── level: "syntax" +│ │ +│ └── @ RubyParseError (location: (2:3)-(2:6)) +│ ├── message: "conditional_predicate_term: expected `then` or `;` or '\\n'" +│ ├── error_message: "expected `then` or `;` or '\\n'" +│ ├── diagnostic_id: "conditional_predicate_term" +│ └── level: "syntax" +│ +└── children: (2 items) + ├── @ ERBIfNode (location: (1:0)-(2:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " if " (location: (1:2)-(1:6)) + │ ├── tag_closing: "%>" (location: (1:6)-(1:8)) + │ ├── statements: (1 item) + │ │ └── @ HTMLTextNode (location: (1:8)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── subsequent: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (2:0)-(2:9)) + │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ ├── content: " end " (location: (2:2)-(2:7)) + │ └── tag_closing: "%>" (location: (2:7)-(2:9)) + │ + │ + └── @ HTMLTextNode (location: (2:9)-(3:0)) + └── content: "\n" \ No newline at end of file