From bde0ea5c3768a408bbc6b7eea4f2477066d2ae7e Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Thu, 13 Nov 2025 13:09:05 +0100 Subject: [PATCH] Parser: Use location-based detection for ERB control types --- src/analyze.c | 146 ++++++++++++++-- src/analyze_helpers.c | 16 +- test/analyze/block_test.rb | 30 ++++ test/analyze/if_test.rb | 8 + test/parser/erb_test.rb | 51 ++++++ ...error_71c9c1b58aa58b284c179d494b481660.txt | 36 ++++ ...error_ce5658e89dd99cb6740835d0d0666353.txt | 55 ++++++ ...block_5795056538d87c6366da710856a9f7e2.txt | 16 ++ ...ected_6af38da3c127d941bc2ca73254434f84.txt | 36 ++++ ...ition_4070aeafc603aa162d1c39baf6f6f451.txt | 27 +++ ...mple)_6c3b80e7864615f8a75a7ec3f037b94f.txt | 165 ++++++++++++++++++ ...ition_ba20822ea8182f5e4fb7bf8515cfa1a3.txt | 40 +++++ ...ition_6e7724ebffff1929458931e07e94b5fc.txt | 40 +++++ ...rors)_10f348784fe47c985ab245e92368d3b8.txt | 61 +++++++ ...ition_039c95173fbba491fbdcde41740cd7eb.txt | 48 +++++ 15 files changed, 760 insertions(+), 15 deletions(-) create mode 100644 test/snapshots/analyze/block_test/test_0019_unclosed_brace_block_should_error_71c9c1b58aa58b284c179d494b481660.txt create mode 100644 test/snapshots/analyze/block_test/test_0020_unclosed_brace_block_with_end_should_error_ce5658e89dd99cb6740835d0d0666353.txt create mode 100644 test/snapshots/analyze/block_test/test_0021_closed_brace_block_in_single_tag_is_not_a_block_5795056538d87c6366da710856a9f7e2.txt create mode 100644 test/snapshots/analyze/block_test/test_0022_do_end_block_works_as_expected_6af38da3c127d941bc2ca73254434f84.txt create mode 100644 test/snapshots/analyze/if_test/test_0014_if_with_yield_in_condition_4070aeafc603aa162d1c39baf6f6f451.txt create mode 100644 test/snapshots/parser/erb_test/test_0047_if_then_else_with_trimming_and_nested_output_tags_(real-world_RDoc_example)_6c3b80e7864615f8a75a7ec3f037b94f.txt create mode 100644 test/snapshots/parser/erb_test/test_0048_if_elsif_with_block_syntax_in_condition_ba20822ea8182f5e4fb7bf8515cfa1a3.txt create mode 100644 test/snapshots/parser/erb_test/test_0049_if_elsif_with_symbol_to_proc_in_condition_6e7724ebffff1929458931e07e94b5fc.txt create mode 100644 test/snapshots/parser/erb_test/test_0050_if_elsif_else_with_multiple_block_conditions_and_output_(real-world_form_errors)_10f348784fe47c985ab245e92368d3b8.txt create mode 100644 test/snapshots/parser/erb_test/test_0051_if_elsif_else_with_assignment_and_block_in_condition_039c95173fbba491fbdcde41740cd7eb.txt diff --git a/src/analyze.c b/src/analyze.c index 61cf86463..57f325e6d 100644 --- a/src/analyze.c +++ b/src/analyze.c @@ -90,6 +90,138 @@ static size_t process_subsequent_block( control_type_t parent_type ); +typedef struct { + control_type_t type; + uint32_t offset; + bool found; +} earliest_control_keyword_t; + +typedef struct { + earliest_control_keyword_t* result; + const uint8_t* source_start; +} location_walker_context_t; + +static bool find_earliest_control_keyword_walker(const pm_node_t* node, void* data) { + if (!node) { return true; } + + location_walker_context_t* context = (location_walker_context_t*) data; + earliest_control_keyword_t* result = context->result; + + control_type_t current_type = CONTROL_TYPE_UNKNOWN; + uint32_t keyword_offset = UINT32_MAX; + + switch (node->type) { + case PM_IF_NODE: { + pm_if_node_t* if_node = (pm_if_node_t*) node; + current_type = CONTROL_TYPE_IF; + keyword_offset = (uint32_t) (if_node->if_keyword_loc.start - context->source_start); + break; + } + + case PM_UNLESS_NODE: { + pm_unless_node_t* unless_node = (pm_unless_node_t*) node; + current_type = CONTROL_TYPE_UNLESS; + keyword_offset = (uint32_t) (unless_node->keyword_loc.start - context->source_start); + break; + } + + case PM_CASE_NODE: { + pm_case_node_t* case_node = (pm_case_node_t*) node; + current_type = CONTROL_TYPE_CASE; + keyword_offset = (uint32_t) (case_node->case_keyword_loc.start - context->source_start); + break; + } + + case PM_CASE_MATCH_NODE: { + pm_case_match_node_t* case_match_node = (pm_case_match_node_t*) node; + current_type = CONTROL_TYPE_CASE_MATCH; + keyword_offset = (uint32_t) (case_match_node->case_keyword_loc.start - context->source_start); + break; + } + + case PM_WHILE_NODE: { + pm_while_node_t* while_node = (pm_while_node_t*) node; + current_type = CONTROL_TYPE_WHILE; + keyword_offset = (uint32_t) (while_node->keyword_loc.start - context->source_start); + break; + } + + case PM_UNTIL_NODE: { + pm_until_node_t* until_node = (pm_until_node_t*) node; + current_type = CONTROL_TYPE_UNTIL; + keyword_offset = (uint32_t) (until_node->keyword_loc.start - context->source_start); + break; + } + + case PM_FOR_NODE: { + pm_for_node_t* for_node = (pm_for_node_t*) node; + current_type = CONTROL_TYPE_FOR; + keyword_offset = (uint32_t) (for_node->for_keyword_loc.start - context->source_start); + break; + } + + case PM_BEGIN_NODE: { + pm_begin_node_t* begin_node = (pm_begin_node_t*) node; + current_type = CONTROL_TYPE_BEGIN; + + if (begin_node->begin_keyword_loc.start != NULL) { + keyword_offset = (uint32_t) (begin_node->begin_keyword_loc.start - context->source_start); + } else { + keyword_offset = (uint32_t) (node->location.start - context->source_start); + } + break; + } + + case PM_YIELD_NODE: { + current_type = CONTROL_TYPE_YIELD; + keyword_offset = (uint32_t) (node->location.start - context->source_start); + break; + } + + case PM_CALL_NODE: { + pm_call_node_t* call = (pm_call_node_t*) node; + + if (call->block != NULL) { + current_type = CONTROL_TYPE_BLOCK; + keyword_offset = (uint32_t) (node->location.start - context->source_start); + } + break; + } + + case PM_NEXT_NODE: + case PM_BREAK_NODE: + case PM_RETURN_NODE: { + current_type = CONTROL_TYPE_UNKNOWN; + keyword_offset = (uint32_t) (node->location.start - context->source_start); + break; + } + + default: break; + } + + if (keyword_offset != UINT32_MAX) { + if (!result->found || keyword_offset < result->offset) { + result->type = current_type; + result->offset = keyword_offset; + result->found = true; + } + } + + return true; +} + +static control_type_t find_earliest_control_keyword(pm_node_t* root, const uint8_t* source_start) { + if (!root) { return CONTROL_TYPE_UNKNOWN; } + + earliest_control_keyword_t result = { .type = CONTROL_TYPE_UNKNOWN, .offset = UINT32_MAX, .found = false }; + + location_walker_context_t context = { .result = &result, .source_start = source_start }; + + pm_visit_node(root, find_earliest_control_keyword_walker, &context); + + return result.found ? result.type : CONTROL_TYPE_UNKNOWN; +} + static control_type_t detect_control_type(AST_ERB_CONTENT_NODE_T* erb_node) { if (!erb_node || erb_node->base.type != AST_ERB_CONTENT_NODE) { return CONTROL_TYPE_UNKNOWN; } @@ -99,26 +231,18 @@ static control_type_t detect_control_type(AST_ERB_CONTENT_NODE_T* erb_node) { if (ruby->valid) { return CONTROL_TYPE_UNKNOWN; } - if (has_block_node(ruby)) { return CONTROL_TYPE_BLOCK; } - if (has_if_node(ruby)) { return CONTROL_TYPE_IF; } + pm_node_t* root = ruby->root; + if (has_elsif_node(ruby)) { return CONTROL_TYPE_ELSIF; } if (has_else_node(ruby)) { return CONTROL_TYPE_ELSE; } if (has_end(ruby)) { return CONTROL_TYPE_END; } - if (has_case_node(ruby)) { return CONTROL_TYPE_CASE; } - if (has_case_match_node(ruby)) { return CONTROL_TYPE_CASE_MATCH; } if (has_when_node(ruby)) { return CONTROL_TYPE_WHEN; } if (has_in_node(ruby)) { return CONTROL_TYPE_IN; } - if (has_begin_node(ruby)) { return CONTROL_TYPE_BEGIN; } if (has_rescue_node(ruby)) { return CONTROL_TYPE_RESCUE; } if (has_ensure_node(ruby)) { return CONTROL_TYPE_ENSURE; } - if (has_unless_node(ruby)) { return CONTROL_TYPE_UNLESS; } - if (has_while_node(ruby)) { return CONTROL_TYPE_WHILE; } - if (has_until_node(ruby)) { return CONTROL_TYPE_UNTIL; } - if (has_for_node(ruby)) { return CONTROL_TYPE_FOR; } if (has_block_closing(ruby)) { return CONTROL_TYPE_BLOCK_CLOSE; } - if (has_yield_node(ruby)) { return CONTROL_TYPE_YIELD; } - return CONTROL_TYPE_UNKNOWN; + return find_earliest_control_keyword(root, ruby->parser.start); } static bool is_subsequent_type(control_type_t parent_type, control_type_t child_type) { diff --git a/src/analyze_helpers.c b/src/analyze_helpers.c index 36117367c..6580a5966 100644 --- a/src/analyze_helpers.c +++ b/src/analyze_helpers.c @@ -110,12 +110,20 @@ bool search_block_nodes(const pm_node_t* node, void* data) { analyzed_ruby_T* analyzed = (analyzed_ruby_T*) data; if (node->type == PM_BLOCK_NODE) { - analyzed->has_block_node = true; - return true; - } else { - pm_visit_child_nodes(node, search_block_nodes, analyzed); + pm_block_node_t* block_node = (pm_block_node_t*) node; + + size_t opening_length = block_node->opening_loc.end - block_node->opening_loc.start; + + if ((opening_length == 2 && block_node->opening_loc.start[0] == 'd' && block_node->opening_loc.start[1] == 'o') + || (opening_length == 1 && block_node->opening_loc.start[0] == '{')) { + analyzed->has_block_node = true; + + return true; + } } + pm_visit_child_nodes(node, search_block_nodes, analyzed); + return false; } diff --git a/test/analyze/block_test.rb b/test/analyze/block_test.rb index 5d4dd7326..05cde2cdd 100644 --- a/test/analyze/block_test.rb +++ b/test/analyze/block_test.rb @@ -164,5 +164,35 @@ class BlockTest < Minitest::Spec <%= yield(:header) if content_for?(:header) %> HTML end + + test "unclosed brace block should error" do + assert_parsed_snapshot(<<~HTML) + <% items.each { |item| %> + <%= item %> + <% } %> + HTML + end + + test "unclosed brace block with end should error" do + assert_parsed_snapshot(<<~HTML) + <% items.each { |item| %> + <%= item %> + <% end %> + HTML + end + + test "closed brace block in single tag is not a block" do + assert_parsed_snapshot(<<~HTML) + <% items.map { |item| item.name } %> + HTML + end + + test "do/end block works as expected" do + assert_parsed_snapshot(<<~HTML) + <% items.each do |item| %> + <%= item %> + <% end %> + HTML + end end end diff --git a/test/analyze/if_test.rb b/test/analyze/if_test.rb index 7902ffc18..48f0ac305 100644 --- a/test/analyze/if_test.rb +++ b/test/analyze/if_test.rb @@ -134,6 +134,14 @@ class IfTest < Minitest::Spec HTML end + test "if with yield in condition" do + assert_parsed_snapshot(<<~HTML) + <% if yield(:a) %> + content + <% end %> + HTML + end + test "conditional attribute value" do skip diff --git a/test/parser/erb_test.rb b/test/parser/erb_test.rb index 4306ec697..285b0d8e3 100644 --- a/test/parser/erb_test.rb +++ b/test/parser/erb_test.rb @@ -258,5 +258,56 @@ class ERBTest < Minitest::Spec more %> <% code %> HTML end + + test "if/then/else with trimming and nested output tags (real-world RDoc example)" do + assert_parsed_snapshot(<<~'HTML') + <%- if @options.main_page and main_page = @files.find { |f| f.full_name == @options.main_page } then %> + "> + <%- else %> + + <%- end %> + HTML + end + + test "if/elsif with block syntax in condition" do + assert_parsed_snapshot(<<~HTML) + <% if value %> + + <% elsif items.any? { |item| item.true? } %> + + <% end %> + HTML + end + + test "if/elsif with symbol to proc in condition" do + assert_parsed_snapshot(<<~HTML) + <% if value %> + + <% elsif items.any?(&:true?) %> + + <% end %> + HTML + end + + test "if/elsif/else with multiple block conditions and output (real-world form errors)" do + assert_parsed_snapshot(<<~HTML) + <% if f.object.errors.any? { |e| e.type == :blank } %> + Name is required. + <% elsif f.object.errors.any? { |e| e.type == :taken } %> + Coffee bag with this name and roast date already exists on this roaster. + <% else %> + <%= f.object.errors.first.message %> + <% end %> + HTML + end + + test "if/elsif/else with assignment and block in condition" do + assert_parsed_snapshot(<<~HTML) + <% if something = @some.find { |t| t.id == 1 } %> + <% elsif other_condition %> + <% else %> + <% end %> + HTML + end end end diff --git a/test/snapshots/analyze/block_test/test_0019_unclosed_brace_block_should_error_71c9c1b58aa58b284c179d494b481660.txt b/test/snapshots/analyze/block_test/test_0019_unclosed_brace_block_should_error_71c9c1b58aa58b284c179d494b481660.txt new file mode 100644 index 000000000..10177aa24 --- /dev/null +++ b/test/snapshots/analyze/block_test/test_0019_unclosed_brace_block_should_error_71c9c1b58aa58b284c179d494b481660.txt @@ -0,0 +1,36 @@ +--- +source: "Analyze::BlockTest#test_0019_unclosed brace block should error" +input: |2- +<% items.each { |item| %> + <%= item %> +<% } %> +--- +@ DocumentNode (location: (1:0)-(4:0)) +└── children: (2 items) + ├── @ ERBBlockNode (location: (1:0)-(3:7)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " items.each { |item| " (location: (1:2)-(1:23)) + │ ├── tag_closing: "%>" (location: (1:23)-(1:25)) + │ ├── body: (3 items) + │ │ ├── @ HTMLTextNode (location: (1:25)-(2:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBContentNode (location: (2:2)-(2:13)) + │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5)) + │ │ │ ├── content: " item " (location: (2:5)-(2:11)) + │ │ │ ├── tag_closing: "%>" (location: (2:11)-(2:13)) + │ │ │ ├── parsed: true + │ │ │ └── valid: true + │ │ │ + │ │ └── @ HTMLTextNode (location: (2:13)-(3:0)) + │ │ └── content: "\n" + │ │ + │ └── end_node: + │ └── @ ERBEndNode (location: (3:0)-(3:7)) + │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ ├── content: " } " (location: (3:2)-(3:5)) + │ └── tag_closing: "%>" (location: (3:5)-(3:7)) + │ + │ + └── @ HTMLTextNode (location: (3:7)-(4:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/block_test/test_0020_unclosed_brace_block_with_end_should_error_ce5658e89dd99cb6740835d0d0666353.txt b/test/snapshots/analyze/block_test/test_0020_unclosed_brace_block_with_end_should_error_ce5658e89dd99cb6740835d0d0666353.txt new file mode 100644 index 000000000..c3504979f --- /dev/null +++ b/test/snapshots/analyze/block_test/test_0020_unclosed_brace_block_with_end_should_error_ce5658e89dd99cb6740835d0d0666353.txt @@ -0,0 +1,55 @@ +--- +source: "Analyze::BlockTest#test_0020_unclosed brace block with end should error" +input: |2- +<% items.each { |item| %> + <%= item %> +<% end %> +--- +@ DocumentNode (location: (1:0)-(4:0)) +├── errors: (3 errors) +│ ├── @ RubyParseError (location: (3:3)-(3:6)) +│ │ ├── message: "unexpected_token_ignore: unexpected 'end', ignoring it" +│ │ ├── error_message: "unexpected 'end', ignoring it" +│ │ ├── diagnostic_id: "unexpected_token_ignore" +│ │ └── level: "syntax" +│ │ +│ ├── @ RubyParseError (location: (3:9)-(4:0)) +│ │ ├── 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" +│ │ +│ └── @ RubyParseError (location: (4:0)-(4:0)) +│ ├── message: "block_term_brace: expected a block beginning with `{` to end with `}`" +│ ├── error_message: "expected a block beginning with `{` to end with `}`" +│ ├── diagnostic_id: "block_term_brace" +│ └── level: "syntax" +│ +└── children: (2 items) + ├── @ ERBBlockNode (location: (1:0)-(3:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " items.each { |item| " (location: (1:2)-(1:23)) + │ ├── tag_closing: "%>" (location: (1:23)-(1:25)) + │ ├── body: (3 items) + │ │ ├── @ HTMLTextNode (location: (1:25)-(2:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBContentNode (location: (2:2)-(2:13)) + │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5)) + │ │ │ ├── content: " item " (location: (2:5)-(2:11)) + │ │ │ ├── tag_closing: "%>" (location: (2:11)-(2:13)) + │ │ │ ├── parsed: true + │ │ │ └── valid: true + │ │ │ + │ │ └── @ HTMLTextNode (location: (2:13)-(3:0)) + │ │ └── content: "\n" + │ │ + │ └── end_node: + │ └── @ ERBEndNode (location: (3:0)-(3:9)) + │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ ├── content: " end " (location: (3:2)-(3:7)) + │ └── tag_closing: "%>" (location: (3:7)-(3:9)) + │ + │ + └── @ HTMLTextNode (location: (3:9)-(4:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/block_test/test_0021_closed_brace_block_in_single_tag_is_not_a_block_5795056538d87c6366da710856a9f7e2.txt b/test/snapshots/analyze/block_test/test_0021_closed_brace_block_in_single_tag_is_not_a_block_5795056538d87c6366da710856a9f7e2.txt new file mode 100644 index 000000000..8b1dddfe4 --- /dev/null +++ b/test/snapshots/analyze/block_test/test_0021_closed_brace_block_in_single_tag_is_not_a_block_5795056538d87c6366da710856a9f7e2.txt @@ -0,0 +1,16 @@ +--- +source: "Analyze::BlockTest#test_0021_closed brace block in single tag is not a block" +input: |2- +<% items.map { |item| item.name } %> +--- +@ DocumentNode (location: (1:0)-(2:0)) +└── children: (2 items) + ├── @ ERBContentNode (location: (1:0)-(1:36)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " items.map { |item| item.name } " (location: (1:2)-(1:34)) + │ ├── tag_closing: "%>" (location: (1:34)-(1:36)) + │ ├── parsed: true + │ └── valid: true + │ + └── @ HTMLTextNode (location: (1:36)-(2:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/block_test/test_0022_do_end_block_works_as_expected_6af38da3c127d941bc2ca73254434f84.txt b/test/snapshots/analyze/block_test/test_0022_do_end_block_works_as_expected_6af38da3c127d941bc2ca73254434f84.txt new file mode 100644 index 000000000..253da4566 --- /dev/null +++ b/test/snapshots/analyze/block_test/test_0022_do_end_block_works_as_expected_6af38da3c127d941bc2ca73254434f84.txt @@ -0,0 +1,36 @@ +--- +source: "Analyze::BlockTest#test_0022_do/end block works as expected" +input: |2- +<% items.each do |item| %> + <%= item %> +<% end %> +--- +@ DocumentNode (location: (1:0)-(4:0)) +└── children: (2 items) + ├── @ ERBBlockNode (location: (1:0)-(3:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " items.each do |item| " (location: (1:2)-(1:24)) + │ ├── tag_closing: "%>" (location: (1:24)-(1:26)) + │ ├── body: (3 items) + │ │ ├── @ HTMLTextNode (location: (1:26)-(2:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ ERBContentNode (location: (2:2)-(2:13)) + │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5)) + │ │ │ ├── content: " item " (location: (2:5)-(2:11)) + │ │ │ ├── tag_closing: "%>" (location: (2:11)-(2:13)) + │ │ │ ├── parsed: true + │ │ │ └── valid: true + │ │ │ + │ │ └── @ HTMLTextNode (location: (2:13)-(3:0)) + │ │ └── content: "\n" + │ │ + │ └── end_node: + │ └── @ ERBEndNode (location: (3:0)-(3:9)) + │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ ├── content: " end " (location: (3:2)-(3:7)) + │ └── tag_closing: "%>" (location: (3:7)-(3:9)) + │ + │ + └── @ HTMLTextNode (location: (3:9)-(4:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/analyze/if_test/test_0014_if_with_yield_in_condition_4070aeafc603aa162d1c39baf6f6f451.txt b/test/snapshots/analyze/if_test/test_0014_if_with_yield_in_condition_4070aeafc603aa162d1c39baf6f6f451.txt new file mode 100644 index 000000000..3ee603fd9 --- /dev/null +++ b/test/snapshots/analyze/if_test/test_0014_if_with_yield_in_condition_4070aeafc603aa162d1c39baf6f6f451.txt @@ -0,0 +1,27 @@ +--- +source: "Analyze::IfTest#test_0014_if with yield in condition" +input: |2- +<% if yield(:a) %> + content +<% end %> +--- +@ DocumentNode (location: (1:0)-(4:0)) +└── children: (2 items) + ├── @ ERBIfNode (location: (1:0)-(3:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " if yield(:a) " (location: (1:2)-(1:16)) + │ ├── tag_closing: "%>" (location: (1:16)-(1:18)) + │ ├── statements: (1 item) + │ │ └── @ HTMLTextNode (location: (1:18)-(3:0)) + │ │ └── content: "\n content\n" + │ │ + │ ├── subsequent: ∅ + │ └── end_node: + │ └── @ ERBEndNode (location: (3:0)-(3:9)) + │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ ├── content: " end " (location: (3:2)-(3:7)) + │ └── tag_closing: "%>" (location: (3:7)-(3:9)) + │ + │ + └── @ HTMLTextNode (location: (3:9)-(4:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/parser/erb_test/test_0047_if_then_else_with_trimming_and_nested_output_tags_(real-world_RDoc_example)_6c3b80e7864615f8a75a7ec3f037b94f.txt b/test/snapshots/parser/erb_test/test_0047_if_then_else_with_trimming_and_nested_output_tags_(real-world_RDoc_example)_6c3b80e7864615f8a75a7ec3f037b94f.txt new file mode 100644 index 000000000..8c2e17c58 --- /dev/null +++ b/test/snapshots/parser/erb_test/test_0047_if_then_else_with_trimming_and_nested_output_tags_(real-world_RDoc_example)_6c3b80e7864615f8a75a7ec3f037b94f.txt @@ -0,0 +1,165 @@ +--- +source: "Parser::ERBTest#test_0047_if/then/else with trimming and nested output tags (real-world RDoc example)" +input: |2- +<%- if @options.main_page and main_page = @files.find { |f| f.full_name == @options.main_page } then %> + "> +<%- else %> + +<%- end %> +--- +@ DocumentNode (location: (1:0)-(6:0)) +└── children: (2 items) + ├── @ ERBIfNode (location: (1:0)-(5:10)) + │ ├── tag_opening: "<%-" (location: (1:0)-(1:3)) + │ ├── content: " if @options.main_page and main_page = @files.find { |f| f.full_name == @options.main_page } then " (location: (1:3)-(1:101)) + │ ├── tag_closing: "%>" (location: (1:101)-(1:103)) + │ ├── statements: (3 items) + │ │ ├── @ HTMLTextNode (location: (1:103)-(2:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ HTMLElementNode (location: (2:2)-(2:89)) + │ │ │ ├── open_tag: + │ │ │ │ └── @ HTMLOpenTagNode (location: (2:2)-(2:89)) + │ │ │ │ ├── tag_opening: "<" (location: (2:2)-(2:3)) + │ │ │ │ ├── tag_name: "meta" (location: (2:3)-(2:7)) + │ │ │ │ ├── tag_closing: ">" (location: (2:88)-(2:89)) + │ │ │ │ ├── children: (2 items) + │ │ │ │ │ ├── @ HTMLAttributeNode (location: (2:8)-(2:26)) + │ │ │ │ │ │ ├── name: + │ │ │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (2:8)-(2:12)) + │ │ │ │ │ │ │ └── children: (1 item) + │ │ │ │ │ │ │ └── @ LiteralNode (location: (2:8)-(2:12)) + │ │ │ │ │ │ │ └── content: "name" + │ │ │ │ │ │ │ + │ │ │ │ │ │ │ + │ │ │ │ │ │ ├── equals: "=" (location: (2:12)-(2:13)) + │ │ │ │ │ │ └── value: + │ │ │ │ │ │ └── @ HTMLAttributeValueNode (location: (2:13)-(2:26)) + │ │ │ │ │ │ ├── open_quote: """ (location: (2:13)-(2:14)) + │ │ │ │ │ │ ├── children: (1 item) + │ │ │ │ │ │ │ └── @ LiteralNode (location: (2:14)-(2:25)) + │ │ │ │ │ │ │ └── content: "description" + │ │ │ │ │ │ │ + │ │ │ │ │ │ ├── close_quote: """ (location: (2:25)-(2:26)) + │ │ │ │ │ │ └── quoted: true + │ │ │ │ │ │ + │ │ │ │ │ │ + │ │ │ │ │ └── @ HTMLAttributeNode (location: (2:27)-(2:88)) + │ │ │ │ │ ├── name: + │ │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (2:27)-(2:34)) + │ │ │ │ │ │ └── children: (1 item) + │ │ │ │ │ │ └── @ LiteralNode (location: (2:27)-(2:34)) + │ │ │ │ │ │ └── content: "content" + │ │ │ │ │ │ + │ │ │ │ │ │ + │ │ │ │ │ ├── equals: "=" (location: (2:34)-(2:35)) + │ │ │ │ │ └── value: + │ │ │ │ │ └── @ HTMLAttributeValueNode (location: (2:35)-(2:88)) + │ │ │ │ │ ├── open_quote: """ (location: (2:35)-(2:36)) + │ │ │ │ │ ├── children: (1 item) + │ │ │ │ │ │ └── @ ERBContentNode (location: (2:36)-(2:87)) + │ │ │ │ │ │ ├── tag_opening: "<%=" (location: (2:36)-(2:39)) + │ │ │ │ │ │ ├── content: " h "#{@title}: #{excerpt(main_page.comment)}" " (location: (2:39)-(2:85)) + │ │ │ │ │ │ ├── tag_closing: "%>" (location: (2:85)-(2:87)) + │ │ │ │ │ │ ├── parsed: true + │ │ │ │ │ │ └── valid: true + │ │ │ │ │ │ + │ │ │ │ │ ├── close_quote: """ (location: (2:87)-(2:88)) + │ │ │ │ │ └── quoted: true + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ └── is_void: false + │ │ │ │ + │ │ │ ├── tag_name: "meta" (location: (2:3)-(2:7)) + │ │ │ ├── body: [] + │ │ │ ├── close_tag: ∅ + │ │ │ ├── is_void: true + │ │ │ └── source: "HTML" + │ │ │ + │ │ └── @ HTMLTextNode (location: (2:89)-(3:0)) + │ │ └── content: "\n" + │ │ + │ ├── subsequent: + │ │ └── @ ERBElseNode (location: (3:0)-(5:0)) + │ │ ├── tag_opening: "<%-" (location: (3:0)-(3:3)) + │ │ ├── content: " else " (location: (3:3)-(3:9)) + │ │ ├── tag_closing: "%>" (location: (3:9)-(3:11)) + │ │ └── statements: (3 items) + │ │ ├── @ HTMLTextNode (location: (3:11)-(4:2)) + │ │ │ └── content: "\n " + │ │ │ + │ │ ├── @ HTMLElementNode (location: (4:2)-(4:71)) + │ │ │ ├── open_tag: + │ │ │ │ └── @ HTMLOpenTagNode (location: (4:2)-(4:71)) + │ │ │ │ ├── tag_opening: "<" (location: (4:2)-(4:3)) + │ │ │ │ ├── tag_name: "meta" (location: (4:3)-(4:7)) + │ │ │ │ ├── tag_closing: ">" (location: (4:70)-(4:71)) + │ │ │ │ ├── children: (2 items) + │ │ │ │ │ ├── @ HTMLAttributeNode (location: (4:8)-(4:26)) + │ │ │ │ │ │ ├── name: + │ │ │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (4:8)-(4:12)) + │ │ │ │ │ │ │ └── children: (1 item) + │ │ │ │ │ │ │ └── @ LiteralNode (location: (4:8)-(4:12)) + │ │ │ │ │ │ │ └── content: "name" + │ │ │ │ │ │ │ + │ │ │ │ │ │ │ + │ │ │ │ │ │ ├── equals: "=" (location: (4:12)-(4:13)) + │ │ │ │ │ │ └── value: + │ │ │ │ │ │ └── @ HTMLAttributeValueNode (location: (4:13)-(4:26)) + │ │ │ │ │ │ ├── open_quote: """ (location: (4:13)-(4:14)) + │ │ │ │ │ │ ├── children: (1 item) + │ │ │ │ │ │ │ └── @ LiteralNode (location: (4:14)-(4:25)) + │ │ │ │ │ │ │ └── content: "description" + │ │ │ │ │ │ │ + │ │ │ │ │ │ ├── close_quote: """ (location: (4:25)-(4:26)) + │ │ │ │ │ │ └── quoted: true + │ │ │ │ │ │ + │ │ │ │ │ │ + │ │ │ │ │ └── @ HTMLAttributeNode (location: (4:27)-(4:70)) + │ │ │ │ │ ├── name: + │ │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (4:27)-(4:34)) + │ │ │ │ │ │ └── children: (1 item) + │ │ │ │ │ │ └── @ LiteralNode (location: (4:27)-(4:34)) + │ │ │ │ │ │ └── content: "content" + │ │ │ │ │ │ + │ │ │ │ │ │ + │ │ │ │ │ ├── equals: "=" (location: (4:34)-(4:35)) + │ │ │ │ │ └── value: + │ │ │ │ │ └── @ HTMLAttributeValueNode (location: (4:35)-(4:70)) + │ │ │ │ │ ├── open_quote: """ (location: (4:35)-(4:36)) + │ │ │ │ │ ├── children: (2 items) + │ │ │ │ │ │ ├── @ LiteralNode (location: (4:36)-(4:54)) + │ │ │ │ │ │ │ └── content: "Documentation for " + │ │ │ │ │ │ │ + │ │ │ │ │ │ └── @ ERBContentNode (location: (4:54)-(4:69)) + │ │ │ │ │ │ ├── tag_opening: "<%=" (location: (4:54)-(4:57)) + │ │ │ │ │ │ ├── content: " h @title " (location: (4:57)-(4:67)) + │ │ │ │ │ │ ├── tag_closing: "%>" (location: (4:67)-(4:69)) + │ │ │ │ │ │ ├── parsed: true + │ │ │ │ │ │ └── valid: true + │ │ │ │ │ │ + │ │ │ │ │ ├── close_quote: """ (location: (4:69)-(4:70)) + │ │ │ │ │ └── quoted: true + │ │ │ │ │ + │ │ │ │ │ + │ │ │ │ └── is_void: false + │ │ │ │ + │ │ │ ├── tag_name: "meta" (location: (4:3)-(4:7)) + │ │ │ ├── body: [] + │ │ │ ├── close_tag: ∅ + │ │ │ ├── is_void: true + │ │ │ └── source: "HTML" + │ │ │ + │ │ └── @ HTMLTextNode (location: (4:71)-(5:0)) + │ │ └── content: "\n" + │ │ + │ │ + │ └── end_node: + │ └── @ ERBEndNode (location: (5:0)-(5:10)) + │ ├── tag_opening: "<%-" (location: (5:0)-(5:3)) + │ ├── content: " end " (location: (5:3)-(5:8)) + │ └── tag_closing: "%>" (location: (5:8)-(5:10)) + │ + │ + └── @ HTMLTextNode (location: (5:10)-(6:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/parser/erb_test/test_0048_if_elsif_with_block_syntax_in_condition_ba20822ea8182f5e4fb7bf8515cfa1a3.txt b/test/snapshots/parser/erb_test/test_0048_if_elsif_with_block_syntax_in_condition_ba20822ea8182f5e4fb7bf8515cfa1a3.txt new file mode 100644 index 000000000..9ba485285 --- /dev/null +++ b/test/snapshots/parser/erb_test/test_0048_if_elsif_with_block_syntax_in_condition_ba20822ea8182f5e4fb7bf8515cfa1a3.txt @@ -0,0 +1,40 @@ +--- +source: "Parser::ERBTest#test_0048_if/elsif with block syntax in condition" +input: |2- +<% if value %> + +<% elsif items.any? { |item| item.true? } %> + +<% end %> +--- +@ DocumentNode (location: (1:0)-(6:0)) +└── children: (2 items) + ├── @ ERBIfNode (location: (1:0)-(5:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " if value " (location: (1:2)-(1:12)) + │ ├── tag_closing: "%>" (location: (1:12)-(1:14)) + │ ├── statements: (1 item) + │ │ └── @ HTMLTextNode (location: (1:14)-(3:0)) + │ │ └── content: "\n\n" + │ │ + │ ├── subsequent: + │ │ └── @ ERBIfNode (location: (3:0)-(5:0)) + │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ │ ├── content: " elsif items.any? { |item| item.true? } " (location: (3:2)-(3:42)) + │ │ ├── tag_closing: "%>" (location: (3:42)-(3:44)) + │ │ ├── statements: (1 item) + │ │ │ └── @ HTMLTextNode (location: (3:44)-(5:0)) + │ │ │ └── content: "\n\n" + │ │ │ + │ │ ├── subsequent: ∅ + │ │ └── end_node: ∅ + │ │ + │ └── end_node: + │ └── @ ERBEndNode (location: (5:0)-(5:9)) + │ ├── tag_opening: "<%" (location: (5:0)-(5:2)) + │ ├── content: " end " (location: (5:2)-(5:7)) + │ └── tag_closing: "%>" (location: (5:7)-(5:9)) + │ + │ + └── @ HTMLTextNode (location: (5:9)-(6:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/parser/erb_test/test_0049_if_elsif_with_symbol_to_proc_in_condition_6e7724ebffff1929458931e07e94b5fc.txt b/test/snapshots/parser/erb_test/test_0049_if_elsif_with_symbol_to_proc_in_condition_6e7724ebffff1929458931e07e94b5fc.txt new file mode 100644 index 000000000..547351476 --- /dev/null +++ b/test/snapshots/parser/erb_test/test_0049_if_elsif_with_symbol_to_proc_in_condition_6e7724ebffff1929458931e07e94b5fc.txt @@ -0,0 +1,40 @@ +--- +source: "Parser::ERBTest#test_0049_if/elsif with symbol to proc in condition" +input: |2- +<% if value %> + +<% elsif items.any?(&:true?) %> + +<% end %> +--- +@ DocumentNode (location: (1:0)-(6:0)) +└── children: (2 items) + ├── @ ERBIfNode (location: (1:0)-(5:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " if value " (location: (1:2)-(1:12)) + │ ├── tag_closing: "%>" (location: (1:12)-(1:14)) + │ ├── statements: (1 item) + │ │ └── @ HTMLTextNode (location: (1:14)-(3:0)) + │ │ └── content: "\n\n" + │ │ + │ ├── subsequent: + │ │ └── @ ERBIfNode (location: (3:0)-(5:0)) + │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ │ ├── content: " elsif items.any?(&:true?) " (location: (3:2)-(3:29)) + │ │ ├── tag_closing: "%>" (location: (3:29)-(3:31)) + │ │ ├── statements: (1 item) + │ │ │ └── @ HTMLTextNode (location: (3:31)-(5:0)) + │ │ │ └── content: "\n\n" + │ │ │ + │ │ ├── subsequent: ∅ + │ │ └── end_node: ∅ + │ │ + │ └── end_node: + │ └── @ ERBEndNode (location: (5:0)-(5:9)) + │ ├── tag_opening: "<%" (location: (5:0)-(5:2)) + │ ├── content: " end " (location: (5:2)-(5:7)) + │ └── tag_closing: "%>" (location: (5:7)-(5:9)) + │ + │ + └── @ HTMLTextNode (location: (5:9)-(6:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/parser/erb_test/test_0050_if_elsif_else_with_multiple_block_conditions_and_output_(real-world_form_errors)_10f348784fe47c985ab245e92368d3b8.txt b/test/snapshots/parser/erb_test/test_0050_if_elsif_else_with_multiple_block_conditions_and_output_(real-world_form_errors)_10f348784fe47c985ab245e92368d3b8.txt new file mode 100644 index 000000000..148687af5 --- /dev/null +++ b/test/snapshots/parser/erb_test/test_0050_if_elsif_else_with_multiple_block_conditions_and_output_(real-world_form_errors)_10f348784fe47c985ab245e92368d3b8.txt @@ -0,0 +1,61 @@ +--- +source: "Parser::ERBTest#test_0050_if/elsif/else with multiple block conditions and output (real-world form errors)" +input: |2- +<% if f.object.errors.any? { |e| e.type == :blank } %> + Name is required. +<% elsif f.object.errors.any? { |e| e.type == :taken } %> + Coffee bag with this name and roast date already exists on this roaster. +<% else %> + <%= f.object.errors.first.message %> +<% end %> +--- +@ DocumentNode (location: (1:0)-(8:0)) +└── children: (2 items) + ├── @ ERBIfNode (location: (1:0)-(7:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " if f.object.errors.any? { |e| e.type == :blank } " (location: (1:2)-(1:52)) + │ ├── tag_closing: "%>" (location: (1:52)-(1:54)) + │ ├── statements: (1 item) + │ │ └── @ HTMLTextNode (location: (1:54)-(3:0)) + │ │ └── content: "\n Name is required.\n" + │ │ + │ ├── subsequent: + │ │ └── @ ERBIfNode (location: (3:0)-(5:0)) + │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ │ ├── content: " elsif f.object.errors.any? { |e| e.type == :taken } " (location: (3:2)-(3:55)) + │ │ ├── tag_closing: "%>" (location: (3:55)-(3:57)) + │ │ ├── statements: (1 item) + │ │ │ └── @ HTMLTextNode (location: (3:57)-(5:0)) + │ │ │ └── content: "\n Coffee bag with this name and roast date already exists on this roaster.\n" + │ │ │ + │ │ ├── subsequent: + │ │ │ └── @ ERBElseNode (location: (5:0)-(7:0)) + │ │ │ ├── tag_opening: "<%" (location: (5:0)-(5:2)) + │ │ │ ├── content: " else " (location: (5:2)-(5:8)) + │ │ │ ├── tag_closing: "%>" (location: (5:8)-(5:10)) + │ │ │ └── statements: (3 items) + │ │ │ ├── @ HTMLTextNode (location: (5:10)-(6:2)) + │ │ │ │ └── content: "\n " + │ │ │ │ + │ │ │ ├── @ ERBContentNode (location: (6:2)-(6:38)) + │ │ │ │ ├── tag_opening: "<%=" (location: (6:2)-(6:5)) + │ │ │ │ ├── content: " f.object.errors.first.message " (location: (6:5)-(6:36)) + │ │ │ │ ├── tag_closing: "%>" (location: (6:36)-(6:38)) + │ │ │ │ ├── parsed: true + │ │ │ │ └── valid: true + │ │ │ │ + │ │ │ └── @ HTMLTextNode (location: (6:38)-(7:0)) + │ │ │ └── content: "\n" + │ │ │ + │ │ │ + │ │ └── end_node: ∅ + │ │ + │ └── end_node: + │ └── @ ERBEndNode (location: (7:0)-(7:9)) + │ ├── tag_opening: "<%" (location: (7:0)-(7:2)) + │ ├── content: " end " (location: (7:2)-(7:7)) + │ └── tag_closing: "%>" (location: (7:7)-(7:9)) + │ + │ + └── @ HTMLTextNode (location: (7:9)-(8:0)) + └── content: "\n" \ No newline at end of file diff --git a/test/snapshots/parser/erb_test/test_0051_if_elsif_else_with_assignment_and_block_in_condition_039c95173fbba491fbdcde41740cd7eb.txt b/test/snapshots/parser/erb_test/test_0051_if_elsif_else_with_assignment_and_block_in_condition_039c95173fbba491fbdcde41740cd7eb.txt new file mode 100644 index 000000000..cddde86ad --- /dev/null +++ b/test/snapshots/parser/erb_test/test_0051_if_elsif_else_with_assignment_and_block_in_condition_039c95173fbba491fbdcde41740cd7eb.txt @@ -0,0 +1,48 @@ +--- +source: "Parser::ERBTest#test_0051_if/elsif/else with assignment and block in condition" +input: |2- +<% if something = @some.find { |t| t.id == 1 } %> +<% elsif other_condition %> +<% else %> +<% end %> +--- +@ DocumentNode (location: (1:0)-(5:0)) +└── children: (2 items) + ├── @ ERBIfNode (location: (1:0)-(4:9)) + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " if something = @some.find { |t| t.id == 1 } " (location: (1:2)-(1:47)) + │ ├── tag_closing: "%>" (location: (1:47)-(1:49)) + │ ├── statements: (1 item) + │ │ └── @ HTMLTextNode (location: (1:49)-(2:0)) + │ │ └── content: "\n" + │ │ + │ ├── subsequent: + │ │ └── @ ERBIfNode (location: (2:0)-(3:0)) + │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2)) + │ │ ├── content: " elsif other_condition " (location: (2:2)-(2:25)) + │ │ ├── tag_closing: "%>" (location: (2:25)-(2:27)) + │ │ ├── statements: (1 item) + │ │ │ └── @ HTMLTextNode (location: (2:27)-(3:0)) + │ │ │ └── content: "\n" + │ │ │ + │ │ ├── subsequent: + │ │ │ └── @ ERBElseNode (location: (3:0)-(4:0)) + │ │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ │ │ ├── content: " else " (location: (3:2)-(3:8)) + │ │ │ ├── tag_closing: "%>" (location: (3:8)-(3:10)) + │ │ │ └── statements: (1 item) + │ │ │ └── @ HTMLTextNode (location: (3:10)-(4:0)) + │ │ │ └── content: "\n" + │ │ │ + │ │ │ + │ │ └── end_node: ∅ + │ │ + │ └── end_node: + │ └── @ ERBEndNode (location: (4:0)-(4:9)) + │ ├── tag_opening: "<%" (location: (4:0)-(4:2)) + │ ├── content: " end " (location: (4:2)-(4:7)) + │ └── tag_closing: "%>" (location: (4:7)-(4:9)) + │ + │ + └── @ HTMLTextNode (location: (4:9)-(5:0)) + └── content: "\n" \ No newline at end of file