Skip to content

Commit 734d466

Browse files
authored
Parser: Support Action View tag helpers with inline blocks (#1404)
This pull request adds support for detecting Action View tag helpers using inline brace block syntax (`{ }`) in addition to the existing `do...end` block syntax. Previously, the parser only recognized multi-line block forms like: ```erb <%= content_tag :div do %> Content <% end %> ``` With this change, inline block forms are now also detected and transformed into `HTMLElementNode` representations: ```erb <%= content_tag(:div) { "Content" } %> ``` Resolves #1369
1 parent 5942e12 commit 734d466

File tree

22 files changed

+782
-31
lines changed

22 files changed

+782
-31
lines changed

src/analyze/action_view/content_tag.c

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,28 @@ char* extract_content_tag_name(pm_call_node_t* call_node, pm_parser_t* parser, h
3838
char* extract_content_tag_content(pm_call_node_t* call_node, pm_parser_t* parser, hb_allocator_T* allocator) {
3939
(void) parser;
4040

41-
if (!call_node || !call_node->arguments) { return NULL; }
41+
if (!call_node) { return NULL; }
4242

43-
pm_arguments_node_t* arguments = call_node->arguments;
44-
if (arguments->arguments.size < 2) { return NULL; }
43+
if (call_node->arguments) {
44+
pm_arguments_node_t* arguments = call_node->arguments;
4545

46-
pm_node_t* second_argument = arguments->arguments.nodes[1];
46+
if (arguments->arguments.size >= 2) {
47+
pm_node_t* second_argument = arguments->arguments.nodes[1];
4748

48-
if (second_argument->type == PM_KEYWORD_HASH_NODE) { return NULL; }
49+
if (second_argument->type != PM_KEYWORD_HASH_NODE) {
50+
if (second_argument->type == PM_STRING_NODE) {
51+
pm_string_node_t* string_node = (pm_string_node_t*) second_argument;
52+
size_t length = pm_string_length(&string_node->unescaped);
53+
return hb_allocator_strndup(allocator, (const char*) pm_string_source(&string_node->unescaped), length);
54+
}
4955

50-
if (second_argument->type == PM_STRING_NODE) {
51-
pm_string_node_t* string_node = (pm_string_node_t*) second_argument;
52-
size_t length = pm_string_length(&string_node->unescaped);
53-
return hb_allocator_strndup(allocator, (const char*) pm_string_source(&string_node->unescaped), length);
56+
size_t source_length = second_argument->location.end - second_argument->location.start;
57+
return hb_allocator_strndup(allocator, (const char*) second_argument->location.start, source_length);
58+
}
59+
}
5460
}
5561

56-
size_t source_length = second_argument->location.end - second_argument->location.start;
57-
return hb_allocator_strndup(allocator, (const char*) second_argument->location.start, source_length);
62+
return extract_inline_block_content(call_node, allocator);
5863
}
5964

6065
bool content_tag_supports_block(void) {

src/analyze/action_view/link_to.c

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ char* extract_link_to_tag_name(pm_call_node_t* call_node, pm_parser_t* parser, h
5656
char* extract_link_to_content(pm_call_node_t* call_node, pm_parser_t* parser, hb_allocator_T* allocator) {
5757
(void) parser;
5858

59-
if (!call_node || !call_node->arguments) { return NULL; }
59+
if (!call_node) { return NULL; }
60+
61+
char* block_content = extract_inline_block_content(call_node, allocator);
62+
if (block_content) { return block_content; }
63+
64+
if (!call_node->arguments) { return NULL; }
6065

6166
pm_arguments_node_t* arguments = call_node->arguments;
6267
if (!arguments->arguments.size) { return NULL; }
@@ -96,6 +101,25 @@ char* extract_link_to_href(pm_call_node_t* call_node, pm_parser_t* parser, hb_al
96101
if (!call_node || !call_node->arguments) { return NULL; }
97102

98103
pm_arguments_node_t* arguments = call_node->arguments;
104+
bool has_inline_block = call_node->block && call_node->block->type == PM_BLOCK_NODE;
105+
106+
if (has_inline_block && arguments->arguments.size >= 1) {
107+
pm_node_t* first_argument = arguments->arguments.nodes[0];
108+
109+
if (first_argument->type == PM_STRING_NODE) {
110+
pm_string_node_t* string_node = (pm_string_node_t*) first_argument;
111+
size_t length = pm_string_length(&string_node->unescaped);
112+
return hb_allocator_strndup(allocator, (const char*) pm_string_source(&string_node->unescaped), length);
113+
}
114+
115+
size_t source_length = first_argument->location.end - first_argument->location.start;
116+
117+
if (is_route_helper_node(first_argument, parser)) {
118+
return hb_allocator_strndup(allocator, (const char*) first_argument->location.start, source_length);
119+
}
120+
121+
return wrap_in_url_for((const char*) first_argument->location.start, source_length, allocator);
122+
}
99123

100124
// Format: "url_for(<expression>)"
101125
if (arguments->arguments.size == 1) {

src/analyze/action_view/registry.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,26 @@ tag_helper_handler_T* get_tag_helper_handlers(void) {
5858
size_t get_tag_helper_handlers_count(void) {
5959
return handlers_count;
6060
}
61+
62+
char* extract_inline_block_content(pm_call_node_t* call_node, hb_allocator_T* allocator) {
63+
if (!call_node || !call_node->block || call_node->block->type != PM_BLOCK_NODE) { return NULL; }
64+
65+
pm_block_node_t* block_node = (pm_block_node_t*) call_node->block;
66+
67+
if (!block_node->body || block_node->body->type != PM_STATEMENTS_NODE) { return NULL; }
68+
69+
pm_statements_node_t* statements = (pm_statements_node_t*) block_node->body;
70+
71+
if (statements->body.size != 1) { return NULL; }
72+
73+
pm_node_t* statement = statements->body.nodes[0];
74+
75+
if (statement->type == PM_STRING_NODE) {
76+
pm_string_node_t* string_node = (pm_string_node_t*) statement;
77+
size_t length = pm_string_length(&string_node->unescaped);
78+
return hb_allocator_strndup(allocator, (const char*) pm_string_source(&string_node->unescaped), length);
79+
}
80+
81+
size_t source_length = statement->location.end - statement->location.start;
82+
return hb_allocator_strndup(allocator, (const char*) statement->location.start, source_length);
83+
}

src/analyze/action_view/tag.c

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,23 @@ char* extract_tag_dot_name(pm_call_node_t* call_node, pm_parser_t* parser, hb_al
3636
char* extract_tag_dot_content(pm_call_node_t* call_node, pm_parser_t* parser, hb_allocator_T* allocator) {
3737
(void) parser;
3838

39-
if (!call_node || !call_node->arguments) { return NULL; }
39+
if (!call_node) { return NULL; }
4040

41-
pm_arguments_node_t* arguments = call_node->arguments;
42-
if (!arguments->arguments.size) { return NULL; }
41+
if (call_node->arguments) {
42+
pm_arguments_node_t* arguments = call_node->arguments;
4343

44-
pm_node_t* first_argument = arguments->arguments.nodes[0];
44+
if (arguments->arguments.size) {
45+
pm_node_t* first_argument = arguments->arguments.nodes[0];
4546

46-
if (first_argument->type == PM_STRING_NODE) {
47-
pm_string_node_t* string_node = (pm_string_node_t*) first_argument;
48-
size_t length = pm_string_length(&string_node->unescaped);
49-
return hb_allocator_strndup(allocator, (const char*) pm_string_source(&string_node->unescaped), length);
47+
if (first_argument->type == PM_STRING_NODE) {
48+
pm_string_node_t* string_node = (pm_string_node_t*) first_argument;
49+
size_t length = pm_string_length(&string_node->unescaped);
50+
return hb_allocator_strndup(allocator, (const char*) pm_string_source(&string_node->unescaped), length);
51+
}
52+
}
5053
}
5154

52-
return NULL;
55+
return extract_inline_block_content(call_node, allocator);
5356
}
5457

5558
bool tag_dot_supports_block(void) {

src/analyze/action_view/tag_helpers.c

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,29 @@ static AST_NODE_T* transform_tag_helper_with_attributes(
262262
if (parse_context->info->call_node && handler->extract_content) {
263263
helper_content = handler->extract_content(parse_context->info->call_node, &parse_context->parser, allocator);
264264

265-
if (helper_content && parse_context->info->call_node->arguments) {
266-
if (strcmp(handler->name, "content_tag") == 0 && parse_context->info->call_node->arguments->arguments.size >= 2) {
267-
content_is_ruby_expression =
268-
(parse_context->info->call_node->arguments->arguments.nodes[1]->type != PM_STRING_NODE);
269-
} else if (parse_context->info->call_node->arguments->arguments.size >= 1) {
270-
content_is_ruby_expression =
271-
(parse_context->info->call_node->arguments->arguments.nodes[0]->type != PM_STRING_NODE);
265+
if (helper_content) {
266+
pm_call_node_t* call = parse_context->info->call_node;
267+
268+
if (call->arguments) {
269+
if (strcmp(handler->name, "content_tag") == 0 && call->arguments->arguments.size >= 2
270+
&& call->arguments->arguments.nodes[1]->type != PM_KEYWORD_HASH_NODE) {
271+
content_is_ruby_expression = (call->arguments->arguments.nodes[1]->type != PM_STRING_NODE);
272+
} else if (strcmp(handler->name, "content_tag") != 0 && call->arguments->arguments.size >= 1
273+
&& call->arguments->arguments.nodes[0]->type != PM_KEYWORD_HASH_NODE) {
274+
content_is_ruby_expression = (call->arguments->arguments.nodes[0]->type != PM_STRING_NODE);
275+
}
276+
}
277+
278+
if (!content_is_ruby_expression && call->block && call->block->type == PM_BLOCK_NODE) {
279+
pm_block_node_t* block_node = (pm_block_node_t*) call->block;
280+
281+
if (block_node->body && block_node->body->type == PM_STATEMENTS_NODE) {
282+
pm_statements_node_t* statements = (pm_statements_node_t*) block_node->body;
283+
284+
if (statements->body.size == 1) {
285+
content_is_ruby_expression = (statements->body.nodes[0]->type != PM_STRING_NODE);
286+
}
287+
}
272288
}
273289
}
274290
}
@@ -492,7 +508,9 @@ static AST_NODE_T* transform_link_to_helper(
492508

493509
hb_array_T* attributes = NULL;
494510
pm_arguments_node_t* link_arguments = info->call_node->arguments;
495-
bool keyword_hash_is_url = link_arguments && link_arguments->arguments.size == 2
511+
bool has_inline_block = info->call_node->block && info->call_node->block->type == PM_BLOCK_NODE;
512+
513+
bool keyword_hash_is_url = !has_inline_block && link_arguments && link_arguments->arguments.size == 2
496514
&& link_arguments->arguments.nodes[1]->type == PM_KEYWORD_HASH_NODE;
497515

498516
if (!keyword_hash_is_url) {
@@ -549,7 +567,12 @@ static AST_NODE_T* transform_link_to_helper(
549567
pm_arguments_node_t* arguments = info->call_node->arguments;
550568
pm_node_t* href_argument = NULL;
551569

552-
if (arguments->arguments.size >= 2) {
570+
if (has_inline_block) {
571+
if (arguments->arguments.size >= 1) {
572+
href_argument = arguments->arguments.nodes[0];
573+
href_is_ruby_expression = (href_argument->type != PM_STRING_NODE);
574+
}
575+
} else if (arguments->arguments.size >= 2) {
553576
href_argument = arguments->arguments.nodes[1];
554577
href_is_ruby_expression = (href_argument->type != PM_STRING_NODE);
555578
} else if (arguments->arguments.size == 1) {
@@ -602,7 +625,17 @@ static AST_NODE_T* transform_link_to_helper(
602625
if (info->content) {
603626
bool content_is_ruby_expression = false;
604627

605-
if (info->call_node && info->call_node->arguments && info->call_node->arguments->arguments.size >= 1) {
628+
if (has_inline_block && info->call_node->block && info->call_node->block->type == PM_BLOCK_NODE) {
629+
pm_block_node_t* block_node = (pm_block_node_t*) info->call_node->block;
630+
631+
if (block_node->body && block_node->body->type == PM_STATEMENTS_NODE) {
632+
pm_statements_node_t* statements = (pm_statements_node_t*) block_node->body;
633+
634+
if (statements->body.size == 1) {
635+
content_is_ruby_expression = (statements->body.nodes[0]->type != PM_STRING_NODE);
636+
}
637+
}
638+
} else if (info->call_node && info->call_node->arguments && info->call_node->arguments->arguments.size >= 1) {
606639
pm_node_t* first_argument = info->call_node->arguments->arguments.nodes[0];
607640
content_is_ruby_expression = (first_argument->type != PM_STRING_NODE);
608641
}

src/include/analyze/action_view/tag_helper_handler.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,6 @@ void tag_helper_info_free(tag_helper_info_T** info);
3838
tag_helper_handler_T* get_tag_helper_handlers(void);
3939
size_t get_tag_helper_handlers_count(void);
4040

41+
char* extract_inline_block_content(pm_call_node_t* call_node, hb_allocator_T* allocator);
42+
4143
#endif

test/analyze/action_view/tag_helper/content_tag_test.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,29 @@ class ContentTagTest < Minitest::Spec
149149
<% end %>
150150
HTML
151151
end
152+
153+
test "content_tag with inline block" do
154+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
155+
<%= content_tag(:details) { "Some content" } %>
156+
HTML
157+
end
158+
159+
test "content_tag with inline block and attributes" do
160+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
161+
<%= content_tag(:div, class: "container") { "Hello" } %>
162+
HTML
163+
end
164+
165+
test "content_tag with inline block and ruby expression" do
166+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
167+
<%= content_tag(:p) { @user.name } %>
168+
HTML
169+
end
170+
171+
test "content_tag with inline block and symbol tag name" do
172+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
173+
<%= content_tag(:span) { "Text" } %>
174+
HTML
175+
end
152176
end
153177
end

test/analyze/action_view/tag_helper/tag_test.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,29 @@ class TagTest < Minitest::Spec
151151
<% end %>
152152
HTML
153153
end
154+
155+
test "tag.details with inline block" do
156+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
157+
<%= tag.details { "Some content" } %>
158+
HTML
159+
end
160+
161+
test "tag.div with inline block and attributes" do
162+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
163+
<%= tag.div(class: "container") { "Hello" } %>
164+
HTML
165+
end
166+
167+
test "tag.p with inline block and ruby expression" do
168+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
169+
<%= tag.p { @user.name } %>
170+
HTML
171+
end
172+
173+
test "tag.span with inline block" do
174+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
175+
<%= tag.span { "Text" } %>
176+
HTML
177+
end
154178
end
155179
end

test/analyze/action_view/url_helper/link_to_test.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,5 +221,35 @@ class LinkToTest < Minitest::Spec
221221
<% end %>
222222
HTML
223223
end
224+
225+
test "link_to with inline block" do
226+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
227+
<%= link_to("#") { "Click me" } %>
228+
HTML
229+
end
230+
231+
test "link_to with inline block and attributes" do
232+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
233+
<%= link_to("/about", class: "btn") { "About" } %>
234+
HTML
235+
end
236+
237+
test "link_to with inline block and data attributes" do
238+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
239+
<%= link_to("/profile", data: { turbo_method: "delete" }) { "Delete" } %>
240+
HTML
241+
end
242+
243+
test "link_to with inline block and path helper" do
244+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
245+
<%= link_to(root_path) { "Home" } %>
246+
HTML
247+
end
248+
249+
test "link_to with inline block and ruby expression" do
250+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
251+
<%= link_to("#") { @user.name } %>
252+
HTML
253+
end
224254
end
225255
end

test/snapshots/analyze/action_view/tag_helper/content_tag_test/test_0021_content_tag_with_inline_block_c40e688f117ff3e3a9124d87f9addbc9-ef4af315cb33925c38d24ea3c2e8a1cd.txt

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)