From ac9c312e0f7ebaba96e95b5c6036c2c1f40eab58 Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Sat, 31 Jan 2026 20:51:05 +0700 Subject: [PATCH 1/2] Add error for case/when on same line in ERB tag This adds test cases and a custom error to handle code like: ```erb <% case variable when "a" %> A <% when "b" %> B <% else %> C <% end %> <% case value in 1 %> One <% in 2 %> Two <% end %> ``` While this is valid ERB, it poses issues for the parser, as it would mean we'd need to turn a single statement into multiple nodes. Those same blocks can be rewritten as: ```erb <% case variable %> <% when "a" %> A <% when "b" %> B <% else %> C <% end %> <% case value %> <% in 1 %> One <% in 2 %> Two <% end %> ``` Which is unambiguous to parse, and arguably more legible. This adds a new ERBCaseWithConditionsInTagError to detect this pattern and direct users to split into separate tags. --- config.yml | 11 +++++ src/analyze.c | 10 +++++ src/analyze_helpers.c | 12 +++++ src/analyzed_ruby.c | 1 + src/include/analyzed_ruby.h | 1 + test/parser/multiple_control_flow_test.rb | 20 +++++++++ ...B_tag_82797abe9bddf29a367f8ebcc420eaba.txt | 45 +++++++++++++++++++ ...B_tag_5655a054015224a72ca2f5d378f9b966.txt | 45 +++++++++++++++++++ 8 files changed, 145 insertions(+) create mode 100644 test/snapshots/parser/multiple_control_flow_test/test_0041_case_and_when_in_same_ERB_tag_82797abe9bddf29a367f8ebcc420eaba.txt create mode 100644 test/snapshots/parser/multiple_control_flow_test/test_0042_case_in_pattern_in_same_ERB_tag_5655a054015224a72ca2f5d378f9b966.txt diff --git a/config.yml b/config.yml index 22376e286..48ee6fb14 100644 --- a/config.yml +++ b/config.yml @@ -178,6 +178,17 @@ errors: fields: [] + - name: ERBCaseWithConditionsInTagError + message: + template: "A `case` statement with a `%s` clause in a single ERB tag cannot be split across multiple tags. Use separate `<%% case ... %%>` and `<%% %s ... %%>` tags." + arguments: + - keyword + - keyword + + fields: + - name: keyword + type: string + warnings: fields: [] types: [] diff --git a/src/analyze.c b/src/analyze.c index dcb49cd9a..0d1753942 100644 --- a/src/analyze.c +++ b/src/analyze.c @@ -72,6 +72,16 @@ static bool analyze_erb_content(const AST_NODE_T* node, void* data) { erb_content_node->base.errors ); } + + if (analyzed->has_case_with_inline_conditions) { + const char* keyword = analyzed->case_match_node_count > 0 ? "in" : "when"; + append_erb_case_with_conditions_in_tag_error( + keyword, + erb_content_node->base.location.start, + erb_content_node->base.location.end, + erb_content_node->base.errors + ); + } } else { erb_content_node->parsed = false; erb_content_node->valid = true; diff --git a/src/analyze_helpers.c b/src/analyze_helpers.c index 27a5ca7f2..91c811e3e 100644 --- a/src/analyze_helpers.c +++ b/src/analyze_helpers.c @@ -406,6 +406,10 @@ bool search_unclosed_control_flows(const pm_node_t* node, void* data) { if (has_location(case_node->case_keyword_loc) && !is_end_keyword(case_node->end_keyword_loc)) { analyzed->unclosed_control_flow_count++; + + if (case_node->conditions.size > 0) { + analyzed->has_case_with_inline_conditions = true; + } } break; @@ -416,6 +420,14 @@ bool search_unclosed_control_flows(const pm_node_t* node, void* data) { if (has_location(case_match_node->case_keyword_loc) && !is_end_keyword(case_match_node->end_keyword_loc)) { analyzed->unclosed_control_flow_count++; + + if (case_match_node->conditions.size > 0) { + analyzed->has_case_with_inline_conditions = true; + } + + if (case_match_node->predicate != NULL && case_match_node->predicate->type == PM_MATCH_PREDICATE_NODE) { + analyzed->has_case_with_inline_conditions = true; + } } break; diff --git a/src/analyzed_ruby.c b/src/analyzed_ruby.c index 4d0a7ac84..0a7cc5b0d 100644 --- a/src/analyzed_ruby.c +++ b/src/analyzed_ruby.c @@ -32,6 +32,7 @@ analyzed_ruby_T* init_analyzed_ruby(hb_string_T source) { analyzed->yield_node_count = 0; analyzed->then_keyword_count = 0; analyzed->unclosed_control_flow_count = 0; + analyzed->has_case_with_inline_conditions = false; return analyzed; } diff --git a/src/include/analyzed_ruby.h b/src/include/analyzed_ruby.h index 9956871da..40c9d08f0 100644 --- a/src/include/analyzed_ruby.h +++ b/src/include/analyzed_ruby.h @@ -31,6 +31,7 @@ typedef struct ANALYZED_RUBY_STRUCT { int yield_node_count; int then_keyword_count; int unclosed_control_flow_count; + bool has_case_with_inline_conditions; } analyzed_ruby_T; analyzed_ruby_T* init_analyzed_ruby(hb_string_T source); diff --git a/test/parser/multiple_control_flow_test.rb b/test/parser/multiple_control_flow_test.rb index 1573278e0..c4914bbc8 100644 --- a/test/parser/multiple_control_flow_test.rb +++ b/test/parser/multiple_control_flow_test.rb @@ -361,5 +361,25 @@ class MultipleControlFlowTest < Minitest::Spec %> ERB end + + test "case and when in same ERB tag" do + assert_parsed_snapshot(<<~ERB) + <% case variable when "a" %> + A + <% when "b" %> + B + <% end %> + ERB + end + + test "case in pattern in same ERB tag" do + assert_parsed_snapshot(<<~ERB) + <% case value in 1 %> + One + <% in 2 %> + Two + <% end %> + ERB + end end end diff --git a/test/snapshots/parser/multiple_control_flow_test/test_0041_case_and_when_in_same_ERB_tag_82797abe9bddf29a367f8ebcc420eaba.txt b/test/snapshots/parser/multiple_control_flow_test/test_0041_case_and_when_in_same_ERB_tag_82797abe9bddf29a367f8ebcc420eaba.txt new file mode 100644 index 000000000..07acbaa81 --- /dev/null +++ b/test/snapshots/parser/multiple_control_flow_test/test_0041_case_and_when_in_same_ERB_tag_82797abe9bddf29a367f8ebcc420eaba.txt @@ -0,0 +1,45 @@ +--- +source: "Parser::MultipleControlFlowTest#test_0041_case and when in same ERB tag" +input: |2- +<% case variable when "a" %> + A +<% when "b" %> + B +<% end %> +--- +@ DocumentNode (location: (1:0)-(6:0)) +└── children: (2 items) + ├── @ ERBCaseNode (location: (1:0)-(5:9)) + │ ├── errors: (1 error) + │ │ └── @ ERBCaseWithConditionsInTagError (location: (1:0)-(1:28)) + │ │ ├── message: "A `case` statement with a `when` clause in a single ERB tag cannot be split across multiple tags. Use separate `<% case ... %>` and `<% when ... %>` tags." + │ │ └── keyword: "when" + │ │ + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case variable when "a" " (location: (1:2)-(1:26)) + │ ├── tag_closing: "%>" (location: (1:26)-(1:28)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:28)-(3:0)) + │ │ └── content: "\n A\n" + │ │ + │ ├── conditions: (1 item) + │ │ └── @ ERBWhenNode (location: (3:0)-(3:14)) + │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ │ ├── content: " when "b" " (location: (3:2)-(3:12)) + │ │ ├── tag_closing: "%>" (location: (3:12)-(3:14)) + │ │ ├── then_keyword: ∅ + │ │ └── statements: (1 item) + │ │ └── @ HTMLTextNode (location: (3:14)-(5:0)) + │ │ └── content: "\n B\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── 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/multiple_control_flow_test/test_0042_case_in_pattern_in_same_ERB_tag_5655a054015224a72ca2f5d378f9b966.txt b/test/snapshots/parser/multiple_control_flow_test/test_0042_case_in_pattern_in_same_ERB_tag_5655a054015224a72ca2f5d378f9b966.txt new file mode 100644 index 000000000..409e653da --- /dev/null +++ b/test/snapshots/parser/multiple_control_flow_test/test_0042_case_in_pattern_in_same_ERB_tag_5655a054015224a72ca2f5d378f9b966.txt @@ -0,0 +1,45 @@ +--- +source: "Parser::MultipleControlFlowTest#test_0042_case in pattern in same ERB tag" +input: |2- +<% case value in 1 %> + One +<% in 2 %> + Two +<% end %> +--- +@ DocumentNode (location: (1:0)-(6:0)) +└── children: (2 items) + ├── @ ERBCaseMatchNode (location: (1:0)-(5:9)) + │ ├── errors: (1 error) + │ │ └── @ ERBCaseWithConditionsInTagError (location: (1:0)-(1:21)) + │ │ ├── message: "A `case` statement with a `in` clause in a single ERB tag cannot be split across multiple tags. Use separate `<% case ... %>` and `<% in ... %>` tags." + │ │ └── keyword: "in" + │ │ + │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) + │ ├── content: " case value in 1 " (location: (1:2)-(1:19)) + │ ├── tag_closing: "%>" (location: (1:19)-(1:21)) + │ ├── children: (1 item) + │ │ └── @ HTMLTextNode (location: (1:21)-(3:0)) + │ │ └── content: "\n One\n" + │ │ + │ ├── conditions: (1 item) + │ │ └── @ ERBInNode (location: (3:0)-(3:10)) + │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2)) + │ │ ├── content: " in 2 " (location: (3:2)-(3:8)) + │ │ ├── tag_closing: "%>" (location: (3:8)-(3:10)) + │ │ ├── then_keyword: ∅ + │ │ └── statements: (1 item) + │ │ └── @ HTMLTextNode (location: (3:10)-(5:0)) + │ │ └── content: "\n Two\n" + │ │ + │ │ + │ ├── else_clause: ∅ + │ └── 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 From 8aecb22f2ea847dc8690c7a9f71c20a22e726d18 Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Tue, 3 Feb 2026 09:48:39 +0700 Subject: [PATCH 2/2] Rename ERBCaseWithConditionsError Also use `inline_conditionals_count` instead of `has_case_with_inline_conditions`. --- config.yml | 12 ++++-------- src/analyze.c | 6 ++---- src/analyze_helpers.c | 10 ++++------ src/analyzed_ruby.c | 2 +- src/include/analyzed_ruby.h | 2 +- ...same_ERB_tag_82797abe9bddf29a367f8ebcc420eaba.txt | 5 ++--- ...same_ERB_tag_5655a054015224a72ca2f5d378f9b966.txt | 5 ++--- 7 files changed, 16 insertions(+), 26 deletions(-) diff --git a/config.yml b/config.yml index 48ee6fb14..34c161742 100644 --- a/config.yml +++ b/config.yml @@ -178,16 +178,12 @@ errors: fields: [] - - name: ERBCaseWithConditionsInTagError + - name: ERBCaseWithConditionsError message: - template: "A `case` statement with a `%s` clause in a single ERB tag cannot be split across multiple tags. Use separate `<%% case ... %%>` and `<%% %s ... %%>` tags." - arguments: - - keyword - - keyword + template: "A `case` statement with `when`/`in` in a single ERB tag cannot be formatted. Use separate tags for `case` and its conditions." + arguments: [] - fields: - - name: keyword - type: string + fields: [] warnings: fields: [] diff --git a/src/analyze.c b/src/analyze.c index 0d1753942..48bdccf7f 100644 --- a/src/analyze.c +++ b/src/analyze.c @@ -73,10 +73,8 @@ static bool analyze_erb_content(const AST_NODE_T* node, void* data) { ); } - if (analyzed->has_case_with_inline_conditions) { - const char* keyword = analyzed->case_match_node_count > 0 ? "in" : "when"; - append_erb_case_with_conditions_in_tag_error( - keyword, + if (analyzed->inline_conditionals_count > 0) { + append_erb_case_with_conditions_error( erb_content_node->base.location.start, erb_content_node->base.location.end, erb_content_node->base.errors diff --git a/src/analyze_helpers.c b/src/analyze_helpers.c index 91c811e3e..3d21bf7d4 100644 --- a/src/analyze_helpers.c +++ b/src/analyze_helpers.c @@ -406,9 +406,8 @@ bool search_unclosed_control_flows(const pm_node_t* node, void* data) { if (has_location(case_node->case_keyword_loc) && !is_end_keyword(case_node->end_keyword_loc)) { analyzed->unclosed_control_flow_count++; - if (case_node->conditions.size > 0) { - analyzed->has_case_with_inline_conditions = true; + analyzed->inline_conditionals_count++; } } @@ -420,13 +419,12 @@ bool search_unclosed_control_flows(const pm_node_t* node, void* data) { if (has_location(case_match_node->case_keyword_loc) && !is_end_keyword(case_match_node->end_keyword_loc)) { analyzed->unclosed_control_flow_count++; - if (case_match_node->conditions.size > 0) { - analyzed->has_case_with_inline_conditions = true; + analyzed->inline_conditionals_count++; } - if (case_match_node->predicate != NULL && case_match_node->predicate->type == PM_MATCH_PREDICATE_NODE) { - analyzed->has_case_with_inline_conditions = true; + if (case_match_node->predicate && case_match_node->predicate->type == PM_MATCH_PREDICATE_NODE) { + analyzed->inline_conditionals_count++; } } diff --git a/src/analyzed_ruby.c b/src/analyzed_ruby.c index 0a7cc5b0d..9c8f3c2bb 100644 --- a/src/analyzed_ruby.c +++ b/src/analyzed_ruby.c @@ -22,6 +22,7 @@ analyzed_ruby_T* init_analyzed_ruby(hb_string_T source) { analyzed->case_match_node_count = 0; analyzed->when_node_count = 0; analyzed->in_node_count = 0; + analyzed->inline_conditionals_count = 0; analyzed->for_node_count = 0; analyzed->while_node_count = 0; analyzed->until_node_count = 0; @@ -32,7 +33,6 @@ analyzed_ruby_T* init_analyzed_ruby(hb_string_T source) { analyzed->yield_node_count = 0; analyzed->then_keyword_count = 0; analyzed->unclosed_control_flow_count = 0; - analyzed->has_case_with_inline_conditions = false; return analyzed; } diff --git a/src/include/analyzed_ruby.h b/src/include/analyzed_ruby.h index 40c9d08f0..245291905 100644 --- a/src/include/analyzed_ruby.h +++ b/src/include/analyzed_ruby.h @@ -21,6 +21,7 @@ typedef struct ANALYZED_RUBY_STRUCT { int case_match_node_count; int when_node_count; int in_node_count; + int inline_conditionals_count; int for_node_count; int while_node_count; int until_node_count; @@ -31,7 +32,6 @@ typedef struct ANALYZED_RUBY_STRUCT { int yield_node_count; int then_keyword_count; int unclosed_control_flow_count; - bool has_case_with_inline_conditions; } analyzed_ruby_T; analyzed_ruby_T* init_analyzed_ruby(hb_string_T source); diff --git a/test/snapshots/parser/multiple_control_flow_test/test_0041_case_and_when_in_same_ERB_tag_82797abe9bddf29a367f8ebcc420eaba.txt b/test/snapshots/parser/multiple_control_flow_test/test_0041_case_and_when_in_same_ERB_tag_82797abe9bddf29a367f8ebcc420eaba.txt index 07acbaa81..58b9869c4 100644 --- a/test/snapshots/parser/multiple_control_flow_test/test_0041_case_and_when_in_same_ERB_tag_82797abe9bddf29a367f8ebcc420eaba.txt +++ b/test/snapshots/parser/multiple_control_flow_test/test_0041_case_and_when_in_same_ERB_tag_82797abe9bddf29a367f8ebcc420eaba.txt @@ -11,9 +11,8 @@ input: |2- └── children: (2 items) ├── @ ERBCaseNode (location: (1:0)-(5:9)) │ ├── errors: (1 error) - │ │ └── @ ERBCaseWithConditionsInTagError (location: (1:0)-(1:28)) - │ │ ├── message: "A `case` statement with a `when` clause in a single ERB tag cannot be split across multiple tags. Use separate `<% case ... %>` and `<% when ... %>` tags." - │ │ └── keyword: "when" + │ │ └── @ ERBCaseWithConditionsError (location: (1:0)-(1:28)) + │ │ └── message: "A `case` statement with `when`/`in` in a single ERB tag cannot be formatted. Use separate tags for `case` and its conditions." │ │ │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) │ ├── content: " case variable when "a" " (location: (1:2)-(1:26)) diff --git a/test/snapshots/parser/multiple_control_flow_test/test_0042_case_in_pattern_in_same_ERB_tag_5655a054015224a72ca2f5d378f9b966.txt b/test/snapshots/parser/multiple_control_flow_test/test_0042_case_in_pattern_in_same_ERB_tag_5655a054015224a72ca2f5d378f9b966.txt index 409e653da..a8fbbd0a2 100644 --- a/test/snapshots/parser/multiple_control_flow_test/test_0042_case_in_pattern_in_same_ERB_tag_5655a054015224a72ca2f5d378f9b966.txt +++ b/test/snapshots/parser/multiple_control_flow_test/test_0042_case_in_pattern_in_same_ERB_tag_5655a054015224a72ca2f5d378f9b966.txt @@ -11,9 +11,8 @@ input: |2- └── children: (2 items) ├── @ ERBCaseMatchNode (location: (1:0)-(5:9)) │ ├── errors: (1 error) - │ │ └── @ ERBCaseWithConditionsInTagError (location: (1:0)-(1:21)) - │ │ ├── message: "A `case` statement with a `in` clause in a single ERB tag cannot be split across multiple tags. Use separate `<% case ... %>` and `<% in ... %>` tags." - │ │ └── keyword: "in" + │ │ └── @ ERBCaseWithConditionsError (location: (1:0)-(1:21)) + │ │ └── message: "A `case` statement with `when`/`in` in a single ERB tag cannot be formatted. Use separate tags for `case` and its conditions." │ │ │ ├── tag_opening: "<%" (location: (1:0)-(1:2)) │ ├── content: " case value in 1 " (location: (1:2)-(1:19))