Skip to content

Commit f2d2c2f

Browse files
authored
Linter: Improve no-void-element-content to detect void blocks (#1513)
This pull request updates the `actionview-no-void-element-content` linter rule (and the parser itself) to also detect Action View tag builder that use a block for building an HTML void element. The following is now flagged by the rule: ```erb <%= tag.input type: "text" do %> input doesn't accept content <% end %> ``` with: ``` Void element `input` cannot have content. `tag.input` does not accept a block for content. ```
1 parent 0f90fc4 commit f2d2c2f

17 files changed

Lines changed: 457 additions & 10 deletions

File tree

‎config.yml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,10 +460,11 @@ errors:
460460

461461
- name: VoidElementContentError
462462
message:
463-
template: "Void element `%s` cannot have content. `%s` does not accept a positional argument for content."
463+
template: "Void element `%s` cannot have content. `%s` does not accept %s for content."
464464
arguments:
465465
- tag_name->value
466466
- helper_name
467+
- content_type
467468

468469
fields:
469470
- name: tag_name
@@ -472,6 +473,9 @@ errors:
472473
- name: helper_name
473474
type: string
474475

476+
- name: content_type
477+
type: string
478+
475479
- name: DotNotationCasingError
476480
message:
477481
template: "Dot-notation component tags require the first segment to start with an uppercase letter. `%s` does not start with an uppercase letter."

‎javascript/packages/linter/test/rules/actionview-no-void-element-content.test.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,56 @@ describe("ActionViewNoVoidElementContentRule", () => {
9191
<%= content_tag :br, "hello" %>
9292
`)
9393
})
94+
95+
test("tag.img with do...end block is not allowed", () => {
96+
expectError(`Void element \`img\` cannot have content. \`tag.img\` does not accept a block for content.`)
97+
98+
assertOffenses(dedent`
99+
<%= tag.img alt: "hello" do %>
100+
a
101+
<% end %>
102+
`)
103+
})
104+
105+
test("tag.br with inline block is not allowed", () => {
106+
expectError(`Void element \`br\` cannot have content. \`tag.br\` does not accept a block for content.`)
107+
108+
assertOffenses(dedent`
109+
<%= tag.br { "content" } %>
110+
`)
111+
})
112+
113+
test("tag.input with do...end block is not allowed", () => {
114+
expectError(`Void element \`input\` cannot have content. \`tag.input\` does not accept a block for content.`)
115+
116+
assertOffenses(dedent`
117+
<%= tag.input do %>
118+
content
119+
<% end %>
120+
`)
121+
})
122+
123+
test("content_tag :input with do...end block is not allowed", () => {
124+
expectError(`Void element \`input\` cannot have content. \`content_tag :input\` does not accept a block for content.`)
125+
126+
assertOffenses(dedent`
127+
<%= content_tag :input do %>
128+
content
129+
<% end %>
130+
`)
131+
})
132+
133+
test("tag.div with do...end block is allowed (non-void element)", () => {
134+
expectNoOffenses(dedent`
135+
<%= tag.div do %>
136+
content
137+
<% end %>
138+
`)
139+
})
140+
141+
test("tag.span with inline block is allowed (non-void element)", () => {
142+
expectNoOffenses(dedent`
143+
<%= tag.span { "content" } %>
144+
`)
145+
})
94146
})

‎sig/herb/errors.rbs‎

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

‎src/analyze/action_view/tag_helpers.c‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -586,9 +586,14 @@ static AST_NODE_T* transform_tag_helper_with_attributes(
586586
}
587587
}
588588

589+
pm_call_node_t* void_call = parse_context->info->call_node;
590+
bool content_from_block = void_call && void_call->block && void_call->block->type == PM_BLOCK_NODE;
591+
hb_string_T content_type = content_from_block ? hb_string("a block") : hb_string("a positional argument");
592+
589593
append_void_element_content_error(
590594
tag_name_token,
591595
helper_name,
596+
content_type,
592597
content_start,
593598
content_end,
594599
allocator,
@@ -946,9 +951,42 @@ static AST_NODE_T* transform_erb_block_to_tag_helper(
946951
);
947952

948953
hb_array_T* body = block_node->body ? block_node->body : hb_array_init(0, allocator);
954+
hb_array_T* element_errors = hb_array_init(0, allocator);
949955
AST_NODE_T* close_tag = (AST_NODE_T*) block_node->end_node;
950956
position_T element_end = block_node->base.location.end;
951957

958+
bool is_void = tag_name && is_void_element(hb_string_from_c_string(tag_name)) && parse_context->matched_handler
959+
&& parse_context->matched_handler->name
960+
&& (string_equals(parse_context->matched_handler->name, "tag")
961+
|| string_equals(parse_context->matched_handler->name, "content_tag")
962+
|| string_equals(parse_context->matched_handler->name, "image_tag"));
963+
964+
if (is_void) {
965+
hb_buffer_T helper_name_buffer;
966+
hb_buffer_init(&helper_name_buffer, 64, allocator);
967+
968+
if (string_equals(parse_context->matched_handler->name, "tag")) {
969+
hb_buffer_append(&helper_name_buffer, "tag.");
970+
hb_buffer_append(&helper_name_buffer, tag_name);
971+
} else {
972+
hb_buffer_append(&helper_name_buffer, parse_context->matched_handler->name);
973+
hb_buffer_append(&helper_name_buffer, " :");
974+
hb_buffer_append(&helper_name_buffer, tag_name);
975+
}
976+
977+
hb_string_T helper_name = hb_string_from_c_string(hb_buffer_value(&helper_name_buffer));
978+
979+
append_void_element_content_error(
980+
tag_name_token,
981+
helper_name,
982+
hb_string("a block"),
983+
block_node->base.location.start,
984+
block_node->base.location.end,
985+
allocator,
986+
element_errors
987+
);
988+
}
989+
952990
if (tag_name && parser_is_foreign_content_tag(hb_string_from_c_string(tag_name)) && context->source
953991
&& block_node->body && hb_array_size(block_node->body) > 0) {
954992
size_t start_offset = block_node->tag_closing->range.to;
@@ -999,12 +1037,12 @@ static AST_NODE_T* transform_erb_block_to_tag_helper(
9991037
(AST_NODE_T*) open_tag_node,
10001038
tag_name_token,
10011039
body,
1002-
close_tag,
1003-
false,
1040+
is_void ? NULL : close_tag,
1041+
is_void,
10041042
parse_context->matched_handler->source,
10051043
block_node->base.location.start,
10061044
element_end,
1007-
hb_array_init(0, allocator),
1045+
element_errors,
10081046
allocator
10091047
);
10101048

‎test/analyze/action_view/tag_helper/content_tag_test.rb‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,5 +284,27 @@ class ContentTagTest < Minitest::Spec
284284
<%= content_tag(:div, content_tag(:p, "Hello world!"), class: "strong") %>
285285
HTML
286286
end
287+
288+
test "content_tag :input with do...end block reports void element content error" do
289+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
290+
<%= content_tag :input do %>
291+
content
292+
<% end %>
293+
HTML
294+
end
295+
296+
test "content_tag :img with do...end block reports void element content error" do
297+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
298+
<%= content_tag :img do %>
299+
content
300+
<% end %>
301+
HTML
302+
end
303+
304+
test "content_tag :br with inline block reports void element content error" do
305+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
306+
<%= content_tag(:br) { "content" } %>
307+
HTML
308+
end
287309
end
288310
end

‎test/analyze/action_view/tag_helper/tag_test.rb‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,5 +573,35 @@ class TagTest < Minitest::Spec
573573
<% end %>
574574
HTML
575575
end
576+
577+
test "tag.img with do...end block reports void element content error" do
578+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
579+
<%= tag.img alt: "hello" do %>
580+
a
581+
<% end %>
582+
HTML
583+
end
584+
585+
test "tag.br with inline block reports void element content error" do
586+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
587+
<%= tag.br { "content" } %>
588+
HTML
589+
end
590+
591+
test "tag.input with do...end block reports void element content error" do
592+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
593+
<%= tag.input do %>
594+
content
595+
<% end %>
596+
HTML
597+
end
598+
599+
test "tag.hr with do...end block reports void element content error" do
600+
assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
601+
<%= tag.hr class: "divider" do %>
602+
content
603+
<% end %>
604+
HTML
605+
end
576606
end
577607
end

‎test/snapshots/analyze/action_view/tag_helper/content_tag_test/test_0037_content_tag_with_void_element_img_and_content_argument_reports_error_5bdb8a95466d4783db7ad280fd6d71ca-ef4af315cb33925c38d24ea3c2e8a1cd.txt‎

Lines changed: 2 additions & 1 deletion
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_0038_content_tag_with_void_element_br_and_content_argument_reports_error_29c54479d2c6354145cc9b90d440d7b8-ef4af315cb33925c38d24ea3c2e8a1cd.txt‎

Lines changed: 2 additions & 1 deletion
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_0041_content_tag_input_with_do...end_block_reports_void_element_content_error_5db23c1fe50e00c4a562ffb47419c21a-ef4af315cb33925c38d24ea3c2e8a1cd.txt‎

Lines changed: 37 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_0042_content_tag_img_with_do...end_block_reports_void_element_content_error_7273bf89d79ff3b4b1e8d1877b550a72-ef4af315cb33925c38d24ea3c2e8a1cd.txt‎

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