Skip to content

Commit 5ac1b52

Browse files
authored
Parser: Fix Action View Helpers with positional arguments and block (#1406)
This pull request fixes how the parser handles Action View tag helpers when both positional arguments and an inline block are present. Previously, `content_tag` and `tag.*` would use the positional content argument even when a block was given. Rails always prefers the block content, so now the parser matches that behavior: ```html+erb <%= content_tag(:div, "ignored") { "Block wins" } %> <!-- Rails renders: <div>Block wins</div> --> ``` This also fixes attribute extraction for `link_to` when the second argument is an explicit hash literal or a variable: ```html+erb <%= link_to("/about", { class: "btn" }) { "About" } %> <!-- Now correctly extracts: href="/about", class="btn", body="About" --> ``` ```html+erb <%= link_to("/about", html_opts) { "About" } %> <!-- Variable options are now represented as RubyHTMLAttributesSplatNode --> ``` Follow up on #1404
1 parent 936e6ae commit 5ac1b52

File tree

22 files changed

+962
-5
lines changed

22 files changed

+962
-5
lines changed

javascript/packages/rewriter/test/action-view-tag-helper-to-html.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,12 @@ describe("ActionViewTagHelperToHTMLRewriter", () => {
179179
)
180180
})
181181

182+
test("tag.div with content argument and block prefers block content", () => {
183+
expect(transform('<%= tag.div("argument") { "Block" } %>')).toBe(
184+
"<div>Block</div>"
185+
)
186+
})
187+
182188
test("tag.div with splat attributes", () => {
183189
const input = dedent`
184190
<%= tag.div class: "content", **attributes do %>
@@ -214,6 +220,12 @@ describe("ActionViewTagHelperToHTMLRewriter", () => {
214220
"<p><%= @user.name %></p>"
215221
)
216222
})
223+
224+
test("content_tag with content argument and block prefers block content", () => {
225+
expect(transform('<%= content_tag(:div, "argument") { "Block" } %>')).toBe(
226+
"<div>Block</div>"
227+
)
228+
})
217229
})
218230

219231
describe("nested helpers", () => {
@@ -270,6 +282,12 @@ describe("ActionViewTagHelperToHTMLRewriter", () => {
270282
'<a href="#"><%= @user.name %></a>'
271283
)
272284
})
285+
286+
test("link_to with content argument and block prefers block content", () => {
287+
expect(transform('<%= link_to("#", "argument") { "Block" } %>')).toBe(
288+
`<a href="#">Block</a>`
289+
)
290+
})
273291
})
274292

275293
describe("turbo_frame_tag helpers", () => {

src/analyze/action_view/attribute_extraction_helpers.c

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ bool has_html_attributes_in_call(pm_call_node_t* call_node) {
265265

266266
pm_node_t* last_argument = arguments->arguments.nodes[arguments->arguments.size - 1];
267267

268-
return last_argument && last_argument->type == PM_KEYWORD_HASH_NODE;
268+
return last_argument && (last_argument->type == PM_KEYWORD_HASH_NODE || last_argument->type == PM_HASH_NODE);
269269
}
270270

271271
hb_array_T* extract_html_attributes_from_call_node(
@@ -280,6 +280,19 @@ hb_array_T* extract_html_attributes_from_call_node(
280280
pm_arguments_node_t* arguments = call_node->arguments;
281281
pm_node_t* last_argument = arguments->arguments.nodes[arguments->arguments.size - 1];
282282

283+
if (last_argument->type == PM_HASH_NODE) {
284+
pm_hash_node_t* hash_node = (pm_hash_node_t*) last_argument;
285+
pm_keyword_hash_node_t synthetic = { .base = hash_node->base, .elements = hash_node->elements };
286+
287+
return extract_html_attributes_from_keyword_hash(
288+
&synthetic,
289+
source,
290+
original_source,
291+
erb_content_offset,
292+
allocator
293+
);
294+
}
295+
283296
return extract_html_attributes_from_keyword_hash(
284297
(pm_keyword_hash_node_t*) last_argument,
285298
source,

src/analyze/action_view/content_tag.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ char* extract_content_tag_content(pm_call_node_t* call_node, pm_parser_t* parser
4040

4141
if (!call_node) { return NULL; }
4242

43+
char* block_content = extract_inline_block_content(call_node, allocator);
44+
if (block_content) { return block_content; }
45+
4346
if (call_node->arguments) {
4447
pm_arguments_node_t* arguments = call_node->arguments;
4548

@@ -59,7 +62,7 @@ char* extract_content_tag_content(pm_call_node_t* call_node, pm_parser_t* parser
5962
}
6063
}
6164

62-
return extract_inline_block_content(call_node, allocator);
65+
return NULL;
6366
}
6467

6568
bool content_tag_supports_block(void) {

src/analyze/action_view/tag.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ char* extract_tag_dot_content(pm_call_node_t* call_node, pm_parser_t* parser, hb
3838

3939
if (!call_node) { return NULL; }
4040

41+
char* block_content = extract_inline_block_content(call_node, allocator);
42+
if (block_content) { return block_content; }
43+
4144
if (call_node->arguments) {
4245
pm_arguments_node_t* arguments = call_node->arguments;
4346

@@ -52,7 +55,7 @@ char* extract_tag_dot_content(pm_call_node_t* call_node, pm_parser_t* parser, hb
5255
}
5356
}
5457

55-
return extract_inline_block_content(call_node, allocator);
58+
return NULL;
5659
}
5760

5861
bool tag_dot_supports_block(void) {

src/analyze/action_view/tag_helpers.c

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,10 @@ static AST_NODE_T* transform_link_to_helper(
510510
pm_arguments_node_t* link_arguments = info->call_node->arguments;
511511
bool has_inline_block = info->call_node->block && info->call_node->block->type == PM_BLOCK_NODE;
512512

513-
bool keyword_hash_is_url = !has_inline_block && link_arguments && link_arguments->arguments.size == 2
514-
&& link_arguments->arguments.nodes[1]->type == PM_KEYWORD_HASH_NODE;
513+
bool second_arg_is_hash = link_arguments && link_arguments->arguments.size == 2
514+
&& (link_arguments->arguments.nodes[1]->type == PM_KEYWORD_HASH_NODE
515+
|| link_arguments->arguments.nodes[1]->type == PM_HASH_NODE);
516+
bool keyword_hash_is_url = !has_inline_block && second_arg_is_hash;
515517

516518
if (!keyword_hash_is_url) {
517519
attributes = extract_html_attributes_from_call_node(
@@ -525,6 +527,38 @@ static AST_NODE_T* transform_link_to_helper(
525527

526528
if (!attributes) { attributes = hb_array_init(4, allocator); }
527529

530+
if (has_inline_block && link_arguments && link_arguments->arguments.size >= 2) {
531+
pm_node_t* second_arg = link_arguments->arguments.nodes[1];
532+
533+
if (second_arg->type != PM_KEYWORD_HASH_NODE && second_arg->type != PM_HASH_NODE
534+
&& second_arg->type != PM_STRING_NODE && second_arg->type != PM_SYMBOL_NODE) {
535+
size_t source_length = second_arg->location.end - second_arg->location.start;
536+
char* content = hb_allocator_strndup(allocator, (const char*) second_arg->location.start, source_length);
537+
538+
if (content) {
539+
position_T position = prism_location_to_position_with_offset(
540+
&second_arg->location,
541+
parse_context->original_source,
542+
parse_context->erb_content_offset,
543+
parse_context->prism_source
544+
);
545+
546+
AST_RUBY_HTML_ATTRIBUTES_SPLAT_NODE_T* splat_node = ast_ruby_html_attributes_splat_node_init(
547+
hb_string_from_c_string(content),
548+
HB_STRING_EMPTY,
549+
position,
550+
position,
551+
hb_array_init(0, allocator),
552+
allocator
553+
);
554+
555+
if (splat_node) { hb_array_append(attributes, (AST_NODE_T*) splat_node); }
556+
557+
hb_allocator_dealloc(allocator, content);
558+
}
559+
}
560+
}
561+
528562
// `method:` implies `rel="nofollow"`
529563
bool has_data_method = false;
530564
hb_string_T data_method_string = hb_string("data-method");

test/analyze/action_view/tag_helper/content_tag_test.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,5 +173,29 @@ class ContentTagTest < Minitest::Spec
173173
<%= content_tag(:span) { "Text" } %>
174174
HTML
175175
end
176+
177+
test "content_tag with content argument and block prefers block content" do
178+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
179+
<%= content_tag(:div, "argument") { "Block" } %>
180+
HTML
181+
end
182+
183+
test "content_tag with inline block and data attributes" do
184+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
185+
<%= content_tag(:div, data: { controller: "example" }) { "Hello" } %>
186+
HTML
187+
end
188+
189+
test "content_tag with content argument and attributes and block prefers block" do
190+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
191+
<%= content_tag(:div, "Content", class: "box") { "Block" } %>
192+
HTML
193+
end
194+
195+
test "content_tag with content argument and multiple attributes and block prefers block" do
196+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
197+
<%= content_tag(:div, "Content", id: "main", class: "box") { "Block" } %>
198+
HTML
199+
end
176200
end
177201
end

test/analyze/action_view/tag_helper/tag_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,23 @@ class TagTest < Minitest::Spec
175175
<%= tag.span { "Text" } %>
176176
HTML
177177
end
178+
179+
test "tag.div with content argument and block prefers block content" do
180+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
181+
<%= tag.div("argument") { "Block" } %>
182+
HTML
183+
end
184+
185+
test "tag.div with inline block and data attributes" do
186+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
187+
<%= tag.div(data: { controller: "example" }) { "Hello" } %>
188+
HTML
189+
end
190+
191+
test "tag.div with content argument and attributes and block prefers block" do
192+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
193+
<%= tag.div("Content", class: "box") { "Block" } %>
194+
HTML
195+
end
178196
end
179197
end

test/analyze/action_view/url_helper/link_to_test.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,5 +251,47 @@ class LinkToTest < Minitest::Spec
251251
<%= link_to("#") { @user.name } %>
252252
HTML
253253
end
254+
255+
test "link_to with inline block and multiple data attributes" do
256+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
257+
<%= link_to("/profile", data: { turbo_method: "delete", turbo_confirm: "Sure?" }) { "Delete" } %>
258+
HTML
259+
end
260+
261+
test "link_to with inline block and string url" do
262+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
263+
<%= link_to("/about") { "About Us" } %>
264+
HTML
265+
end
266+
267+
test "link_to with inline block and path helper and attributes" do
268+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
269+
<%= link_to(root_path, class: "btn", data: { turbo_method: "delete" }) { "Home" } %>
270+
HTML
271+
end
272+
273+
test "link_to with inline block and explicit hash options" do
274+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
275+
<%= link_to("/about", { class: "btn" }) { "x" } %>
276+
HTML
277+
end
278+
279+
test "link_to with inline block and variable options" do
280+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
281+
<%= link_to("/about", html_opts) { "About" } %>
282+
HTML
283+
end
284+
285+
test "link_to with inline block and path helper with variable options" do
286+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
287+
<%= link_to(root_path, options) { "Home" } %>
288+
HTML
289+
end
290+
291+
test "link_to with inline block and string as second argument" do
292+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
293+
<%= link_to("#", "argument") { "Block" } %>
294+
HTML
295+
end
254296
end
255297
end

test/snapshots/analyze/action_view/tag_helper/content_tag_test/test_0025_content_tag_with_content_argument_and_block_prefers_block_content_0725c6c7a921d29df07af10be989013f-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.

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

Lines changed: 51 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)