diff --git a/javascript/packages/linter/test/rules/html-no-unknown-tag.test.ts b/javascript/packages/linter/test/rules/html-no-unknown-tag.test.ts
index f0a73f29a..a610bc5b3 100644
--- a/javascript/packages/linter/test/rules/html-no-unknown-tag.test.ts
+++ b/javascript/packages/linter/test/rules/html-no-unknown-tag.test.ts
@@ -500,5 +500,30 @@ describe("html-no-unknown-tag", () => {
test("tag.send for custom element passes", () => {
expectNoOffenses(`<%= tag.send("turbo-frame", id: "frame") do %>content<% end %>`)
})
+
+ test("tag shadowed by a block argument passes", () => {
+ expectNoOffenses(`
+ <% @tags.each do |tag| %>
+ <%= tag.name %>
+ <% end %>
+ `)
+ })
+
+ test("tag shadowed by a local variable assignment passes", () => {
+ expectNoOffenses(`
+ <% tag = Tag.new(name: "Name") %>
+
+
<%= tag.name %>
+ `)
+ })
+
+ test("tag.hello is still flagged after an unrelated local variable assignment", () => {
+ expectWarning('Unknown HTML tag ``. This is not a standard HTML element.')
+
+ assertOffenses(`
+ <% label = "Name" %>
+ <%= tag.hello do %><%= label %><% end %>
+ `)
+ })
})
})
diff --git a/src/analyze/action_view/tag_helpers.c b/src/analyze/action_view/tag_helpers.c
index 507ab24fd..248661a2c 100644
--- a/src/analyze/action_view/tag_helpers.c
+++ b/src/analyze/action_view/tag_helpers.c
@@ -48,29 +48,86 @@ typedef struct {
size_t erb_content_offset;
} tag_helper_parse_context_T;
-static bool build_scope_options_from_context(analyze_ruby_context_T* context, pm_options_t* options) {
- size_t locals_count = context && context->tag_helper_locals ? hb_array_size(context->tag_helper_locals) : 0;
+typedef struct {
+ tag_helper_scope_T* scope;
+ hb_array_T* constants;
+ size_t from;
+ size_t to;
+} local_read_search_T;
- if (locals_count == 0) { return false; }
- if (!pm_options_scopes_init(options, 1)) { return false; }
+static void append_local_read_constant(local_read_search_T* search, pm_constant_id_t constant_id) {
+ pm_constant_t* constant = pm_constant_pool_id_to_constant(&search->scope->parser.constant_pool, constant_id);
- pm_options_scope_t* scope = &options->scopes[0];
+ if (!constant || constant->length == 0) { return; }
- if (!pm_options_scope_init(scope, locals_count)) {
- pm_options_free(options);
+ for (size_t index = 0; index < hb_array_size(search->constants); index++) {
+ pm_constant_t* existing = hb_array_get(search->constants, index);
- return false;
+ if (existing && existing->length == constant->length
+ && memcmp(existing->start, constant->start, constant->length) == 0) {
+ return;
+ }
}
- for (size_t index = 0; index < locals_count; index++) {
- const char* local_name = hb_array_get(context->tag_helper_locals, index);
+ hb_array_append(search->constants, constant);
+}
+
+static bool search_local_variable_reads(const pm_node_t* node, void* data) {
+ local_read_search_T* search = (local_read_search_T*) data;
+ const uint8_t* base = (const uint8_t*) search->scope->buffer.value;
+
+ size_t start = (size_t) (node->location.start - base);
+ size_t end = (size_t) (node->location.end - base);
+
+ if (end <= search->from || start >= search->to) { return false; }
- pm_string_constant_init(&scope->locals[index], local_name, strlen(local_name));
+ if (PM_NODE_TYPE(node) == PM_LOCAL_VARIABLE_READ_NODE && start >= search->from && end <= search->to) {
+ append_local_read_constant(search, ((pm_local_variable_read_node_t*) node)->name);
}
return true;
}
+static bool build_scope_options_from_context(
+ analyze_ruby_context_T* context,
+ pm_options_t* options,
+ size_t from,
+ size_t to
+) {
+ if (!context || !context->tag_helper_scope || !context->tag_helper_scope->root) { return false; }
+ if (to <= from) { return false; }
+
+ local_read_search_T search = { .scope = context->tag_helper_scope,
+ .constants = hb_array_init(4, context->allocator),
+ .from = from,
+ .to = to };
+
+ pm_visit_node(context->tag_helper_scope->root, search_local_variable_reads, &search);
+
+ size_t locals_count = hb_array_size(search.constants);
+ bool built = false;
+
+ if (locals_count > 0 && pm_options_scopes_init(options, 1)) {
+ pm_options_scope_t* scope = &options->scopes[0];
+
+ if (pm_options_scope_init(scope, locals_count)) {
+ for (size_t index = 0; index < locals_count; index++) {
+ pm_constant_t* constant = hb_array_get(search.constants, index);
+
+ pm_string_constant_init(&scope->locals[index], (const char*) constant->start, constant->length);
+ }
+
+ built = true;
+ } else {
+ pm_options_free(options);
+ }
+ }
+
+ hb_array_free(&search.constants);
+
+ return built;
+}
+
static tag_helper_parse_context_T* parse_tag_helper_content(
const char* content_string,
const char* original_source,
@@ -88,13 +145,16 @@ static tag_helper_parse_context_T* parse_tag_helper_content(
parse_context->original_source = original_source;
parse_context->erb_content_offset = erb_content_offset;
+ size_t content_length = strlen(parse_context->content_string);
+
pm_options_t options = { 0 };
- bool has_scope_options = build_scope_options_from_context(context, &options);
+ bool has_scope_options =
+ build_scope_options_from_context(context, &options, erb_content_offset, erb_content_offset + content_length);
pm_parser_init(
&parse_context->parser,
parse_context->prism_source,
- strlen(parse_context->content_string),
+ content_length,
has_scope_options ? &options : NULL
);
parse_context->root = pm_parse(&parse_context->parser);
@@ -1774,67 +1834,12 @@ void transform_tag_helper_blocks(const AST_NODE_T* node, analyze_ruby_context_T*
}
}
-static size_t push_tag_helper_local_scope(const AST_NODE_T* node, analyze_ruby_context_T* context) {
- if (!node || !context || !context->tag_helper_locals) { return 0; }
-
- hb_array_T* block_arguments = NULL;
-
- if (node->type == AST_ERB_BLOCK_NODE) {
- block_arguments = ((AST_ERB_BLOCK_NODE_T*) node)->block_arguments;
- } else if (node->type == AST_ERB_RENDER_NODE) {
- block_arguments = ((AST_ERB_RENDER_NODE_T*) node)->block_arguments;
- }
-
- if (!block_arguments) { return 0; }
-
- size_t pushed = 0;
-
- for (size_t index = 0; index < hb_array_size(block_arguments); index++) {
- AST_NODE_T* argument = hb_array_get(block_arguments, index);
-
- if (!argument || argument->type != AST_RUBY_PARAMETER_NODE) { continue; }
-
- AST_RUBY_PARAMETER_NODE_T* parameter = (AST_RUBY_PARAMETER_NODE_T*) argument;
-
- if (!parameter->name || hb_string_is_empty(parameter->name->value)) { continue; }
-
- char* name = hb_allocator_strndup(context->allocator, parameter->name->value.data, parameter->name->value.length);
-
- if (name && hb_array_append(context->tag_helper_locals, name)) {
- pushed++;
- } else if (name) {
- hb_allocator_dealloc(context->allocator, name);
- }
- }
-
- return pushed;
-}
-
-static void pop_tag_helper_local_scope(analyze_ruby_context_T* context, size_t count) {
- if (!context || !context->tag_helper_locals) { return; }
-
- for (size_t index = 0; index < count; index++) {
- size_t size = hb_array_size(context->tag_helper_locals);
-
- if (size == 0) { break; }
-
- char* name = hb_array_get(context->tag_helper_locals, size - 1);
- hb_array_remove(context->tag_helper_locals, size - 1);
-
- if (name) { hb_allocator_dealloc(context->allocator, name); }
- }
-}
-
bool transform_tag_helper_nodes(const AST_NODE_T* node, void* data) {
analyze_ruby_context_T* context = (analyze_ruby_context_T*) data;
- size_t pushed_locals = push_tag_helper_local_scope(node, context);
-
transform_tag_helper_blocks(node, context);
herb_visit_child_nodes(node, transform_tag_helper_nodes, data);
- pop_tag_helper_local_scope(context, pushed_locals);
-
return false;
}
diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c
index 043ec0375..8ffbced00 100644
--- a/src/analyze/analyze.c
+++ b/src/analyze/analyze.c
@@ -16,8 +16,10 @@
#include "../include/ast/ast_node.h"
#include "../include/ast/ast_nodes.h"
#include "../include/errors.h"
+#include "../include/extract.h"
#include "../include/lexer/token_struct.h"
#include "../include/lib/hb_array.h"
+#include "../include/lib/hb_buffer.h"
#include "../include/lib/hb_string.h"
#include "../include/lib/string.h"
#include "../include/location/location.h"
@@ -1016,6 +1018,63 @@ hb_array_T* rewrite_node_array(AST_NODE_T* node, hb_array_T* array, analyze_ruby
return new_array;
}
+static tag_helper_scope_T* tag_helper_scope_init(const char* source, hb_allocator_T* allocator) {
+ if (!source) { return NULL; }
+
+ tag_helper_scope_T* scope = hb_allocator_alloc(allocator, sizeof(tag_helper_scope_T));
+
+ if (!scope) { return NULL; }
+
+ memset(scope, 0, sizeof(tag_helper_scope_T));
+
+ if (!hb_buffer_init(&scope->buffer, strlen(source), allocator)) {
+ hb_allocator_dealloc(allocator, scope);
+
+ return NULL;
+ }
+
+ herb_extract_ruby_options_T extract_options = {
+ .semicolons = true,
+ .comments = false,
+ .preserve_positions = true,
+ };
+
+ herb_extract_ruby_to_buffer_with_options(source, &scope->buffer, &extract_options, allocator);
+
+ if (!scope->buffer.value || scope->buffer.length == 0) {
+ hb_buffer_free(&scope->buffer);
+ hb_allocator_dealloc(allocator, scope);
+
+ return NULL;
+ }
+
+ pm_options_partial_script_set(&scope->options, true);
+ pm_parser_init(&scope->parser, (const uint8_t*) scope->buffer.value, scope->buffer.length, &scope->options);
+
+ scope->root = pm_parse(&scope->parser);
+
+ if (!scope->root) {
+ pm_parser_free(&scope->parser);
+ pm_options_free(&scope->options);
+ hb_buffer_free(&scope->buffer);
+ hb_allocator_dealloc(allocator, scope);
+
+ return NULL;
+ }
+
+ return scope;
+}
+
+static void tag_helper_scope_free(tag_helper_scope_T* scope, hb_allocator_T* allocator) {
+ if (!scope) { return; }
+
+ pm_node_destroy(&scope->parser, scope->root);
+ pm_parser_free(&scope->parser);
+ pm_options_free(&scope->options);
+ hb_buffer_free(&scope->buffer);
+ hb_allocator_dealloc(allocator, scope);
+}
+
void herb_analyze_parse_tree(
AST_DOCUMENT_NODE_T* document,
const char* source,
@@ -1029,7 +1088,7 @@ void herb_analyze_parse_tree(
.document = document,
.parent = NULL,
.ruby_context_stack = hb_array_init(8, allocator),
- .tag_helper_locals = hb_array_init(8, allocator),
+ .tag_helper_scope = NULL,
.allocator = allocator,
.source = source,
};
@@ -1051,7 +1110,12 @@ void herb_analyze_parse_tree(
}
if (options && options->action_view_helpers) {
+ context.tag_helper_scope = tag_helper_scope_init(source, allocator);
+
herb_visit_node((AST_NODE_T*) document, transform_tag_helper_nodes, &context);
+
+ tag_helper_scope_free(context.tag_helper_scope, allocator);
+ context.tag_helper_scope = NULL;
}
herb_transform_conditional_elements(document, allocator);
@@ -1069,11 +1133,5 @@ void herb_analyze_parse_tree(
herb_parser_match_html_tags_post_analyze(document, options, allocator);
- for (size_t index = hb_array_size(context.tag_helper_locals); index > 0; index--) {
- char* local_name = hb_array_get(context.tag_helper_locals, index - 1);
- if (local_name) { hb_allocator_dealloc(allocator, local_name); }
- }
-
- hb_array_free(&context.tag_helper_locals);
hb_array_free(&context.ruby_context_stack);
}
diff --git a/src/include/analyze/analyze.h b/src/include/analyze/analyze.h
index b0a0b2dfa..d0c242f28 100644
--- a/src/include/analyze/analyze.h
+++ b/src/include/analyze/analyze.h
@@ -4,14 +4,22 @@
#include "../ast/ast_nodes.h"
#include "../lib/hb_allocator.h"
#include "../lib/hb_array.h"
+#include "../lib/hb_buffer.h"
#include "../parser/parser.h"
#include "analyzed_ruby.h"
+typedef struct TAG_HELPER_SCOPE_STRUCT {
+ hb_buffer_T buffer;
+ pm_options_t options;
+ pm_parser_t parser;
+ pm_node_t* root;
+} tag_helper_scope_T;
+
typedef struct ANALYZE_RUBY_CONTEXT_STRUCT {
AST_DOCUMENT_NODE_T* document;
AST_NODE_T* parent;
hb_array_T* ruby_context_stack;
- hb_array_T* tag_helper_locals;
+ tag_helper_scope_T* tag_helper_scope;
hb_allocator_T* allocator;
const char* source;
bool found_strict_locals;
diff --git a/test/analyze/action_view/tag_helper/tag_test.rb b/test/analyze/action_view/tag_helper/tag_test.rb
index e98c250bd..fd784bf34 100644
--- a/test/analyze/action_view/tag_helper/tag_test.rb
+++ b/test/analyze/action_view/tag_helper/tag_test.rb
@@ -644,5 +644,109 @@ class TagTest < Minitest::Spec
<%= tag.hr %>
HTML
end
+
+ test "tag shadowed by local variable assignment is not treated as tag helper" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <% tag = Tag.new(name: "Name") %>
+
+ <%= tag.name %>
+ HTML
+ end
+
+ test "tag shadowed by multiple assignment is not treated as tag helper" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <% label, tag = @pair %>
+ <%= tag.name %>
+ HTML
+ end
+
+ test "tag shadowed by or-assignment is not treated as tag helper" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <% tag ||= Tag.new %>
+ <%= tag.name %>
+ HTML
+ end
+
+ test "tag shadowed by local variable assignment inside an if statement is not treated as tag helper" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <% if @condition %>
+ <% tag = Tag.new %>
+ <%= tag.name %>
+ <% end %>
+ HTML
+ end
+
+ test "real tag helper is still transformed before the local variable assignment" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <%= tag.hr %>
+ <% tag = Tag.new %>
+ <%= tag.name %>
+ HTML
+ end
+
+ test "tag shadowed by a for loop variable is not treated as tag helper" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <% for tag in @tags %>
+ <%= tag.name %>
+ <% end %>
+ HTML
+ end
+
+ test "tag shadowed by a local variable assigned in a block opening is not treated as tag helper" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <% tag = capture do %>
+ content
+ <% end %>
+ <%= tag.name %>
+ HTML
+ end
+
+ test "tag shadowed by block argument is not treated as tag helper with iteration nodes" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true, iteration_nodes: true)
+ <% @tags.each do |tag| %>
+ <%= tag.name %>
+ <% end %>
+ HTML
+ end
+
+ test "tag shadowed by a rescue reference is not treated as tag helper" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <% begin %>
+ <%= render "thing" %>
+ <% rescue => tag %>
+ <%= tag.name %>
+ <% end %>
+ HTML
+ end
+
+ test "tag shadowed by a rescue reference with an exception class is not treated as tag helper" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <% begin %>
+ <%= render "thing" %>
+ <% rescue StandardError => tag %>
+ <%= tag.name %>
+ <% end %>
+ HTML
+ end
+
+ test "real tag helper is still transformed outside the rescue branch" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <% begin %>
+ <%= tag.hr %>
+ <% rescue => tag %>
+ rescued
+ <% end %>
+ HTML
+ end
+
+ test "local variable assigned inside a block doesn't shadow the tag helper outside of it" do
+ assert_parsed_snapshot(<<~HTML, action_view_helpers: true)
+ <% @groups.each do |group| %>
+ <% tag = group.tag %>
+ <%= tag.name %>
+ <% end %>
+ <%= tag.hr %>
+ HTML
+ end
end
end
diff --git a/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0090_tag_shadowed_by_local_variable_assignment_is_not_treated_as_tag_helper_3002414cbbbee566b64e16fa573856d2-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0090_tag_shadowed_by_local_variable_assignment_is_not_treated_as_tag_helper_3002414cbbbee566b64e16fa573856d2-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..7922e83f7
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0090_tag_shadowed_by_local_variable_assignment_is_not_treated_as_tag_helper_3002414cbbbee566b64e16fa573856d2-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,50 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0090_tag shadowed by local variable assignment is not treated as tag helper"
+input: |2-
+<% tag = Tag.new(name: "Name") %>
+
+<%= tag.name %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(4:0))
+└── children: (4 items)
+ ├── @ ERBContentNode (location: (1:0)-(1:33))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " tag = Tag.new(name: "Name") " (location: (1:2)-(1:31))
+ │ ├── tag_closing: "%>" (location: (1:31)-(1:33))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ ├── @ HTMLTextNode (location: (1:33)-(3:0))
+ │ └── content: "\n\n"
+ │
+ ├── @ HTMLElementNode (location: (3:0)-(3:24))
+ │ ├── open_tag:
+ │ │ └── @ HTMLOpenTagNode (location: (3:0)-(3:4))
+ │ │ ├── tag_opening: "<" (location: (3:0)-(3:1))
+ │ │ ├── tag_name: "li" (location: (3:1)-(3:3))
+ │ │ ├── tag_closing: ">" (location: (3:3)-(3:4))
+ │ │ ├── children: []
+ │ │ └── is_void: false
+ │ │
+ │ ├── tag_name: "li" (location: (3:1)-(3:3))
+ │ ├── body: (1 item)
+ │ │ └── @ ERBContentNode (location: (3:4)-(3:19))
+ │ │ ├── tag_opening: "<%=" (location: (3:4)-(3:7))
+ │ │ ├── content: " tag.name " (location: (3:7)-(3:17))
+ │ │ ├── tag_closing: "%>" (location: (3:17)-(3:19))
+ │ │ ├── parsed: true
+ │ │ └── valid: true
+ │ │
+ │ ├── close_tag:
+ │ │ └── @ HTMLCloseTagNode (location: (3:19)-(3:24))
+ │ │ ├── tag_opening: "" (location: (3:19)-(3:21))
+ │ │ ├── tag_name: "li" (location: (3:21)-(3:23))
+ │ │ ├── children: []
+ │ │ └── tag_closing: ">" (location: (3:23)-(3:24))
+ │ │
+ │ ├── is_void: false
+ │ └── element_source: "HTML"
+ │
+ └── @ HTMLTextNode (location: (3:24)-(4:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0091_tag_shadowed_by_multiple_assignment_is_not_treated_as_tag_helper_8f98b0bc3c23693e5a742622bc5f89c8-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0091_tag_shadowed_by_multiple_assignment_is_not_treated_as_tag_helper_8f98b0bc3c23693e5a742622bc5f89c8-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..f62badc39
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0091_tag_shadowed_by_multiple_assignment_is_not_treated_as_tag_helper_8f98b0bc3c23693e5a742622bc5f89c8-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,28 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0091_tag shadowed by multiple assignment is not treated as tag helper"
+input: |2-
+<% label, tag = @pair %>
+<%= tag.name %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(3:0))
+└── children: (4 items)
+ ├── @ ERBContentNode (location: (1:0)-(1:24))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " label, tag = @pair " (location: (1:2)-(1:22))
+ │ ├── tag_closing: "%>" (location: (1:22)-(1:24))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ ├── @ HTMLTextNode (location: (1:24)-(2:0))
+ │ └── content: "\n"
+ │
+ ├── @ ERBContentNode (location: (2:0)-(2:15))
+ │ ├── tag_opening: "<%=" (location: (2:0)-(2:3))
+ │ ├── content: " tag.name " (location: (2:3)-(2:13))
+ │ ├── tag_closing: "%>" (location: (2:13)-(2:15))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ └── @ HTMLTextNode (location: (2:15)-(3:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0092_tag_shadowed_by_or-assignment_is_not_treated_as_tag_helper_3d1f98b12ea5f8f5b7ee2f0a716e882f-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0092_tag_shadowed_by_or-assignment_is_not_treated_as_tag_helper_3d1f98b12ea5f8f5b7ee2f0a716e882f-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..edca0e733
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0092_tag_shadowed_by_or-assignment_is_not_treated_as_tag_helper_3d1f98b12ea5f8f5b7ee2f0a716e882f-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,28 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0092_tag shadowed by or-assignment is not treated as tag helper"
+input: |2-
+<% tag ||= Tag.new %>
+<%= tag.name %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(3:0))
+└── children: (4 items)
+ ├── @ ERBContentNode (location: (1:0)-(1:21))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " tag ||= Tag.new " (location: (1:2)-(1:19))
+ │ ├── tag_closing: "%>" (location: (1:19)-(1:21))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ ├── @ HTMLTextNode (location: (1:21)-(2:0))
+ │ └── content: "\n"
+ │
+ ├── @ ERBContentNode (location: (2:0)-(2:15))
+ │ ├── tag_opening: "<%=" (location: (2:0)-(2:3))
+ │ ├── content: " tag.name " (location: (2:3)-(2:13))
+ │ ├── tag_closing: "%>" (location: (2:13)-(2:15))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ └── @ HTMLTextNode (location: (2:15)-(3:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0093_tag_shadowed_by_local_variable_assignment_inside_an_if_statement_is_not_treated_as_tag_helper_6dda2a09159cfdd43944aa70a2559b86-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0093_tag_shadowed_by_local_variable_assignment_inside_an_if_statement_is_not_treated_as_tag_helper_6dda2a09159cfdd43944aa70a2559b86-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..2d11b957c
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0093_tag_shadowed_by_local_variable_assignment_inside_an_if_statement_is_not_treated_as_tag_helper_6dda2a09159cfdd43944aa70a2559b86-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,50 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0093_tag shadowed by local variable assignment inside an if statement is not treated as tag helper"
+input: |2-
+<% if @condition %>
+ <% tag = Tag.new %>
+ <%= tag.name %>
+<% end %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(5:0))
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(4:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " if @condition " (location: (1:2)-(1:17))
+ │ ├── tag_closing: "%>" (location: (1:17)-(1:19))
+ │ ├── then_keyword: ∅
+ │ ├── statements: (5 items)
+ │ │ ├── @ HTMLTextNode (location: (1:19)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:21))
+ │ │ │ ├── tag_opening: "<%" (location: (2:2)-(2:4))
+ │ │ │ ├── content: " tag = Tag.new " (location: (2:4)-(2:19))
+ │ │ │ ├── tag_closing: "%>" (location: (2:19)-(2:21))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (2:21)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (3:2)-(3:17))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " tag.name " (location: (3:5)-(3:15))
+ │ │ │ ├── tag_closing: "%>" (location: (3:15)-(3:17))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (3:17)-(4:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── subsequent: ∅
+ │ └── 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
diff --git a/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0094_real_tag_helper_is_still_transformed_before_the_local_variable_assignment_e37a17bb86a915b47cf78ec20e6da603-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0094_real_tag_helper_is_still_transformed_before_the_local_variable_assignment_e37a17bb86a915b47cf78ec20e6da603-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..7fbd18825
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0094_real_tag_helper_is_still_transformed_before_the_local_variable_assignment_e37a17bb86a915b47cf78ec20e6da603-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,47 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0094_real tag helper is still transformed before the local variable assignment"
+input: |2-
+<%= tag.hr %>
+<% tag = Tag.new %>
+<%= tag.name %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(4:0))
+└── children: (6 items)
+ ├── @ HTMLElementNode (location: (1:0)-(1:13))
+ │ ├── open_tag:
+ │ │ └── @ ERBOpenTagNode (location: (1:0)-(1:13))
+ │ │ ├── tag_opening: "<%=" (location: (1:0)-(1:3))
+ │ │ ├── content: " tag.hr " (location: (1:3)-(1:11))
+ │ │ ├── tag_closing: "%>" (location: (1:11)-(1:13))
+ │ │ ├── tag_name: "hr" (location: (1:8)-(1:10))
+ │ │ └── children: []
+ │ │
+ │ ├── tag_name: "hr" (location: (1:8)-(1:10))
+ │ ├── body: []
+ │ ├── close_tag: ∅
+ │ ├── is_void: true
+ │ └── element_source: "ActionView::Helpers::TagHelper#tag"
+ │
+ ├── @ HTMLTextNode (location: (1:13)-(2:0))
+ │ └── content: "\n"
+ │
+ ├── @ ERBContentNode (location: (2:0)-(2:19))
+ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ ├── content: " tag = Tag.new " (location: (2:2)-(2:17))
+ │ ├── tag_closing: "%>" (location: (2:17)-(2:19))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ ├── @ HTMLTextNode (location: (2:19)-(3:0))
+ │ └── content: "\n"
+ │
+ ├── @ ERBContentNode (location: (3:0)-(3:15))
+ │ ├── tag_opening: "<%=" (location: (3:0)-(3:3))
+ │ ├── content: " tag.name " (location: (3:3)-(3:13))
+ │ ├── tag_closing: "%>" (location: (3:13)-(3:15))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ └── @ HTMLTextNode (location: (3:15)-(4:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0095_tag_shadowed_by_a_for_loop_variable_is_not_treated_as_tag_helper_279f26fe3a8ac4cc08f89e40f017fac2-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0095_tag_shadowed_by_a_for_loop_variable_is_not_treated_as_tag_helper_279f26fe3a8ac4cc08f89e40f017fac2-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..5652e9d92
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0095_tag_shadowed_by_a_for_loop_variable_is_not_treated_as_tag_helper_279f26fe3a8ac4cc08f89e40f017fac2-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,37 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0095_tag shadowed by a for loop variable is not treated as tag helper"
+input: |2-
+<% for tag in @tags %>
+ <%= tag.name %>
+<% end %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(4:0))
+└── children: (2 items)
+ ├── @ ERBForNode (location: (1:0)-(3:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " for tag in @tags " (location: (1:2)-(1:20))
+ │ ├── tag_closing: "%>" (location: (1:20)-(1:22))
+ │ ├── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:22)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:17))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " tag.name " (location: (2:5)-(2:15))
+ │ │ │ ├── tag_closing: "%>" (location: (2:15)-(2:17))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:17)-(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/action_view/tag_helper/tag_test/test_0096_tag_shadowed_by_a_local_variable_assigned_in_a_block_opening_is_not_treated_as_tag_helper_2f6fbc8168699f810c5858be4739f6b5-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0096_tag_shadowed_by_a_local_variable_assigned_in_a_block_opening_is_not_treated_as_tag_helper_2f6fbc8168699f810c5858be4739f6b5-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..2a91ca541
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0096_tag_shadowed_by_a_local_variable_assigned_in_a_block_opening_is_not_treated_as_tag_helper_2f6fbc8168699f810c5858be4739f6b5-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,42 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0096_tag shadowed by a local variable assigned in a block opening is not treated as tag helper"
+input: |2-
+<% tag = capture do %>
+ content
+<% end %>
+<%= tag.name %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(5:0))
+└── children: (4 items)
+ ├── @ ERBBlockNode (location: (1:0)-(3:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " tag = capture do " (location: (1:2)-(1:20))
+ │ ├── tag_closing: "%>" (location: (1:20)-(1:22))
+ │ ├── body: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:22)-(3:0))
+ │ │ └── content: "\n content\n"
+ │ │
+ │ ├── block_arguments: []
+ │ ├── rescue_clause: ∅
+ │ ├── else_clause: ∅
+ │ ├── ensure_clause: ∅
+ │ └── 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"
+ │
+ ├── @ ERBContentNode (location: (4:0)-(4:15))
+ │ ├── tag_opening: "<%=" (location: (4:0)-(4:3))
+ │ ├── content: " tag.name " (location: (4:3)-(4:13))
+ │ ├── tag_closing: "%>" (location: (4:13)-(4:15))
+ │ ├── parsed: true
+ │ └── valid: true
+ │
+ └── @ HTMLTextNode (location: (4:15)-(5:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0097_tag_shadowed_by_block_argument_is_not_treated_as_tag_helper_with_iteration_nodes_c7f0d997281ad7650587e6260b8f6282-4c63ec08b6ab0e9cb1d6ac3f296576f6.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0097_tag_shadowed_by_block_argument_is_not_treated_as_tag_helper_with_iteration_nodes_c7f0d997281ad7650587e6260b8f6282-4c63ec08b6ab0e9cb1d6ac3f296576f6.txt
new file mode 100644
index 000000000..c50501407
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0097_tag_shadowed_by_block_argument_is_not_treated_as_tag_helper_with_iteration_nodes_c7f0d997281ad7650587e6260b8f6282-4c63ec08b6ab0e9cb1d6ac3f296576f6.txt
@@ -0,0 +1,52 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0097_tag shadowed by block argument is not treated as tag helper with iteration nodes"
+input: |2-
+<% @tags.each do |tag| %>
+ <%= tag.name %>
+<% end %>
+options: {action_view_helpers: true, iteration_nodes: true}
+---
+@ DocumentNode (location: (1:0)-(4:0))
+└── children: (2 items)
+ ├── @ ERBIterationBlockNode (location: (1:0)-(3:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " @tags.each do |tag| " (location: (1:2)-(1:23))
+ │ ├── tag_closing: "%>" (location: (1:23)-(1:25))
+ │ ├── receiver: "@tags" (location: (1:3)-(1:8))
+ │ ├── call_operator: "." (location: (1:8)-(1:9))
+ │ ├── message: "each" (location: (1:9)-(1:13))
+ │ ├── arguments: []
+ │ ├── block_opening: "do" (location: (1:14)-(1:16))
+ │ ├── body: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:25)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:17))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " tag.name " (location: (2:5)-(2:15))
+ │ │ │ ├── tag_closing: "%>" (location: (2:15)-(2:17))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:17)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── block_arguments: (1 item)
+ │ │ └── @ RubyParameterNode (location: (1:18)-(1:21))
+ │ │ ├── name: "tag" (location: (1:18)-(1:21))
+ │ │ ├── default_value: ∅
+ │ │ ├── kind: "positional"
+ │ │ └── required: true
+ │ │
+ │ ├── rescue_clause: ∅
+ │ ├── else_clause: ∅
+ │ ├── ensure_clause: ∅
+ │ └── 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/action_view/tag_helper/tag_test/test_0098_tag_shadowed_by_a_rescue_reference_is_not_treated_as_tag_helper_c29cf7876303aa20dc206b4a579aea5d-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0098_tag_shadowed_by_a_rescue_reference_is_not_treated_as_tag_helper_c29cf7876303aa20dc206b4a579aea5d-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..a6f9a3065
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0098_tag_shadowed_by_a_rescue_reference_is_not_treated_as_tag_helper_c29cf7876303aa20dc206b4a579aea5d-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,62 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0098_tag shadowed by a rescue reference is not treated as tag helper"
+input: |2-
+<% begin %>
+ <%= render "thing" %>
+<% rescue => tag %>
+ <%= tag.name %>
+<% end %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(6:0))
+└── children: (2 items)
+ ├── @ ERBBeginNode (location: (1:0)-(5:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " begin " (location: (1:2)-(1:9))
+ │ ├── tag_closing: "%>" (location: (1:9)-(1:11))
+ │ ├── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:11)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:23))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " render "thing" " (location: (2:5)-(2:21))
+ │ │ │ ├── tag_closing: "%>" (location: (2:21)-(2:23))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:23)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── rescue_clause:
+ │ │ └── @ ERBRescueNode (location: (3:0)-(5:0))
+ │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ │ ├── content: " rescue => tag " (location: (3:2)-(3:17))
+ │ │ ├── tag_closing: "%>" (location: (3:17)-(3:19))
+ │ │ ├── statements: (3 items)
+ │ │ │ ├── @ HTMLTextNode (location: (3:19)-(4:2))
+ │ │ │ │ └── content: "\n "
+ │ │ │ │
+ │ │ │ ├── @ ERBContentNode (location: (4:2)-(4:17))
+ │ │ │ │ ├── tag_opening: "<%=" (location: (4:2)-(4:5))
+ │ │ │ │ ├── content: " tag.name " (location: (4:5)-(4:15))
+ │ │ │ │ ├── tag_closing: "%>" (location: (4:15)-(4:17))
+ │ │ │ │ ├── parsed: true
+ │ │ │ │ └── valid: true
+ │ │ │ │
+ │ │ │ └── @ HTMLTextNode (location: (4:17)-(5:0))
+ │ │ │ └── content: "\n"
+ │ │ │
+ │ │ └── subsequent: ∅
+ │ │
+ │ ├── else_clause: ∅
+ │ ├── ensure_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/analyze/action_view/tag_helper/tag_test/test_0099_tag_shadowed_by_a_rescue_reference_with_an_exception_class_is_not_treated_as_tag_helper_2dd895016b10bcff0c9264c5f8cc8692-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0099_tag_shadowed_by_a_rescue_reference_with_an_exception_class_is_not_treated_as_tag_helper_2dd895016b10bcff0c9264c5f8cc8692-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..f9e219a48
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0099_tag_shadowed_by_a_rescue_reference_with_an_exception_class_is_not_treated_as_tag_helper_2dd895016b10bcff0c9264c5f8cc8692-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,62 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0099_tag shadowed by a rescue reference with an exception class is not treated as tag helper"
+input: |2-
+<% begin %>
+ <%= render "thing" %>
+<% rescue StandardError => tag %>
+ <%= tag.name %>
+<% end %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(6:0))
+└── children: (2 items)
+ ├── @ ERBBeginNode (location: (1:0)-(5:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " begin " (location: (1:2)-(1:9))
+ │ ├── tag_closing: "%>" (location: (1:9)-(1:11))
+ │ ├── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:11)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:23))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " render "thing" " (location: (2:5)-(2:21))
+ │ │ │ ├── tag_closing: "%>" (location: (2:21)-(2:23))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:23)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── rescue_clause:
+ │ │ └── @ ERBRescueNode (location: (3:0)-(5:0))
+ │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ │ ├── content: " rescue StandardError => tag " (location: (3:2)-(3:31))
+ │ │ ├── tag_closing: "%>" (location: (3:31)-(3:33))
+ │ │ ├── statements: (3 items)
+ │ │ │ ├── @ HTMLTextNode (location: (3:33)-(4:2))
+ │ │ │ │ └── content: "\n "
+ │ │ │ │
+ │ │ │ ├── @ ERBContentNode (location: (4:2)-(4:17))
+ │ │ │ │ ├── tag_opening: "<%=" (location: (4:2)-(4:5))
+ │ │ │ │ ├── content: " tag.name " (location: (4:5)-(4:15))
+ │ │ │ │ ├── tag_closing: "%>" (location: (4:15)-(4:17))
+ │ │ │ │ ├── parsed: true
+ │ │ │ │ └── valid: true
+ │ │ │ │
+ │ │ │ └── @ HTMLTextNode (location: (4:17)-(5:0))
+ │ │ │ └── content: "\n"
+ │ │ │
+ │ │ └── subsequent: ∅
+ │ │
+ │ ├── else_clause: ∅
+ │ ├── ensure_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/analyze/action_view/tag_helper/tag_test/test_0100_real_tag_helper_is_still_transformed_outside_the_rescue_branch_8c92964ce90030b9e384d89cfc1e5c8e-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0100_real_tag_helper_is_still_transformed_outside_the_rescue_branch_8c92964ce90030b9e384d89cfc1e5c8e-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..85ec88835
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0100_real_tag_helper_is_still_transformed_outside_the_rescue_branch_8c92964ce90030b9e384d89cfc1e5c8e-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,60 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0100_real tag helper is still transformed outside the rescue branch"
+input: |2-
+<% begin %>
+ <%= tag.hr %>
+<% rescue => tag %>
+ rescued
+<% end %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(6:0))
+└── children: (2 items)
+ ├── @ ERBBeginNode (location: (1:0)-(5:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " begin " (location: (1:2)-(1:9))
+ │ ├── tag_closing: "%>" (location: (1:9)-(1:11))
+ │ ├── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:11)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ HTMLElementNode (location: (2:2)-(2:15))
+ │ │ │ ├── open_tag:
+ │ │ │ │ └── @ ERBOpenTagNode (location: (2:2)-(2:15))
+ │ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ │ ├── content: " tag.hr " (location: (2:5)-(2:13))
+ │ │ │ │ ├── tag_closing: "%>" (location: (2:13)-(2:15))
+ │ │ │ │ ├── tag_name: "hr" (location: (2:10)-(2:12))
+ │ │ │ │ └── children: []
+ │ │ │ │
+ │ │ │ ├── tag_name: "hr" (location: (2:10)-(2:12))
+ │ │ │ ├── body: []
+ │ │ │ ├── close_tag: ∅
+ │ │ │ ├── is_void: true
+ │ │ │ └── element_source: "ActionView::Helpers::TagHelper#tag"
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:15)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── rescue_clause:
+ │ │ └── @ ERBRescueNode (location: (3:0)-(5:0))
+ │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ │ ├── content: " rescue => tag " (location: (3:2)-(3:17))
+ │ │ ├── tag_closing: "%>" (location: (3:17)-(3:19))
+ │ │ ├── statements: (1 item)
+ │ │ │ └── @ HTMLTextNode (location: (3:19)-(5:0))
+ │ │ │ └── content: "\n rescued\n"
+ │ │ │
+ │ │ └── subsequent: ∅
+ │ │
+ │ ├── else_clause: ∅
+ │ ├── ensure_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/analyze/action_view/tag_helper/tag_test/test_0101_local_variable_assigned_inside_a_block_doesn't_shadow_the_tag_helper_outside_of_it_b8b55ad908011b311a1dcbc684e8cd59-ef4af315cb33925c38d24ea3c2e8a1cd.txt b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0101_local_variable_assigned_inside_a_block_doesn't_shadow_the_tag_helper_outside_of_it_b8b55ad908011b311a1dcbc684e8cd59-ef4af315cb33925c38d24ea3c2e8a1cd.txt
new file mode 100644
index 000000000..41b5872f2
--- /dev/null
+++ b/test/snapshots/analyze/action_view/tag_helper/tag_test/test_0101_local_variable_assigned_inside_a_block_doesn't_shadow_the_tag_helper_outside_of_it_b8b55ad908011b311a1dcbc684e8cd59-ef4af315cb33925c38d24ea3c2e8a1cd.txt
@@ -0,0 +1,77 @@
+---
+source: "Analyze::ActionView::TagHelper::TagTest#test_0101_local variable assigned inside a block doesn't shadow the tag helper outside of it"
+input: |2-
+<% @groups.each do |group| %>
+ <% tag = group.tag %>
+ <%= tag.name %>
+<% end %>
+<%= tag.hr %>
+options: {action_view_helpers: true}
+---
+@ DocumentNode (location: (1:0)-(6:0))
+└── children: (4 items)
+ ├── @ ERBBlockNode (location: (1:0)-(4:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " @groups.each do |group| " (location: (1:2)-(1:27))
+ │ ├── tag_closing: "%>" (location: (1:27)-(1:29))
+ │ ├── body: (5 items)
+ │ │ ├── @ HTMLTextNode (location: (1:29)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:23))
+ │ │ │ ├── tag_opening: "<%" (location: (2:2)-(2:4))
+ │ │ │ ├── content: " tag = group.tag " (location: (2:4)-(2:21))
+ │ │ │ ├── tag_closing: "%>" (location: (2:21)-(2:23))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (2:23)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (3:2)-(3:17))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " tag.name " (location: (3:5)-(3:15))
+ │ │ │ ├── tag_closing: "%>" (location: (3:15)-(3:17))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (3:17)-(4:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── block_arguments: (1 item)
+ │ │ └── @ RubyParameterNode (location: (1:20)-(1:25))
+ │ │ ├── name: "group" (location: (1:20)-(1:25))
+ │ │ ├── default_value: ∅
+ │ │ ├── kind: "positional"
+ │ │ └── required: true
+ │ │
+ │ ├── rescue_clause: ∅
+ │ ├── else_clause: ∅
+ │ ├── ensure_clause: ∅
+ │ └── 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"
+ │
+ ├── @ HTMLElementNode (location: (5:0)-(5:13))
+ │ ├── open_tag:
+ │ │ └── @ ERBOpenTagNode (location: (5:0)-(5:13))
+ │ │ ├── tag_opening: "<%=" (location: (5:0)-(5:3))
+ │ │ ├── content: " tag.hr " (location: (5:3)-(5:11))
+ │ │ ├── tag_closing: "%>" (location: (5:11)-(5:13))
+ │ │ ├── tag_name: "hr" (location: (5:8)-(5:10))
+ │ │ └── children: []
+ │ │
+ │ ├── tag_name: "hr" (location: (5:8)-(5:10))
+ │ ├── body: []
+ │ ├── close_tag: ∅
+ │ ├── is_void: true
+ │ └── element_source: "ActionView::Helpers::TagHelper#tag"
+ │
+ └── @ HTMLTextNode (location: (5:13)-(6:0))
+ └── content: "\n"
\ No newline at end of file