From 5833092b5bcc42c890d3882212ee8710f0da4f35 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Mon, 30 Mar 2026 13:28:53 -0600 Subject: [PATCH 1/7] fix changelog typo --- docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 38e8dd8c6..bec75f44d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -22,7 +22,7 @@ nav_order: 6 * Return `html_safe` empty string from `render_in` when `render?` is false. - *Copilot* + *GitHub Copilot* ## 4.5.0 From 85efdd8a3a63543e6c17d3bf9170ada22041d8ec Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Wed, 1 Apr 2026 11:55:40 -0600 Subject: [PATCH 2/7] fix inheritance edge case with formatless templates --- docs/CHANGELOG.md | 4 ++++ lib/view_component/template.rb | 7 +++++++ .../app/components/format_less_child_component.erb | 1 + .../app/components/format_less_child_component.rb | 4 ++++ .../app/components/format_less_parent_component.erb | 1 + .../app/components/format_less_parent_component.rb | 4 ++++ test/sandbox/test/rendering_test.rb | 12 ++++++++++++ 7 files changed, 33 insertions(+) create mode 100644 test/sandbox/app/components/format_less_child_component.erb create mode 100644 test/sandbox/app/components/format_less_child_component.rb create mode 100644 test/sandbox/app/components/format_less_parent_component.erb create mode 100644 test/sandbox/app/components/format_less_parent_component.rb diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bec75f44d..c917653d3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,10 @@ nav_order: 6 ## main +* Fix bug where inheritance of components with formatless templates improperly raised a NoMethodError. + + *GitHub Copilot*, *Joel Hawksley*, *Cameron Dutro* + ## 4.6.0 * Add `view_identifier` to the `render.view_component` instrumentation event payload, containing the path to the component's template file (e.g. `app/components/my_component.html.erb`). For components using inline render methods, `view_identifier` will be `nil`. diff --git a/lib/view_component/template.rb b/lib/view_component/template.rb index 23ca0c5ee..a05803a28 100644 --- a/lib/view_component/template.rb +++ b/lib/view_component/template.rb @@ -21,6 +21,13 @@ def initialize(component:, details:, lineno: nil, path: nil) class File < Template def initialize(component:, details:, path:) + # If the template file has no format (e.g. .erb instead of .html.erb), + # assume the default format (html). + if details.format.nil? + Kernel.warn("Template format for #{path} is missing, defaulting to :html.") + details = ActionView::TemplateDetails.new(details.locale, details.handler, DEFAULT_FORMAT, details.variant) + end + @strip_annotation_line = false # Rails 8.1 added a newline to compiled ERB output (rails/rails#53731). diff --git a/test/sandbox/app/components/format_less_child_component.erb b/test/sandbox/app/components/format_less_child_component.erb new file mode 100644 index 000000000..3b6bbb6e7 --- /dev/null +++ b/test/sandbox/app/components/format_less_child_component.erb @@ -0,0 +1 @@ +
<%= content %>
diff --git a/test/sandbox/app/components/format_less_child_component.rb b/test/sandbox/app/components/format_less_child_component.rb new file mode 100644 index 000000000..6e86972eb --- /dev/null +++ b/test/sandbox/app/components/format_less_child_component.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class FormatLessChildComponent < FormatLessParentComponent +end diff --git a/test/sandbox/app/components/format_less_parent_component.erb b/test/sandbox/app/components/format_less_parent_component.erb new file mode 100644 index 000000000..34bdb107d --- /dev/null +++ b/test/sandbox/app/components/format_less_parent_component.erb @@ -0,0 +1 @@ +
<%= content %>
diff --git a/test/sandbox/app/components/format_less_parent_component.rb b/test/sandbox/app/components/format_less_parent_component.rb new file mode 100644 index 000000000..7ec22277a --- /dev/null +++ b/test/sandbox/app/components/format_less_parent_component.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class FormatLessParentComponent < ViewComponent::Base +end diff --git a/test/sandbox/test/rendering_test.rb b/test/sandbox/test/rendering_test.rb index b394bba4d..ee98488ae 100644 --- a/test/sandbox/test/rendering_test.rb +++ b/test/sandbox/test/rendering_test.rb @@ -852,6 +852,18 @@ def test_inherited_component_overrides_inherits_template assert_selector("div", text: "hello, my own template") end + def test_inherited_component_with_format_less_template + # Reproduces https://github.com/ViewComponent/view_component/issues/2573 + # When a parent component uses a format-less template (e.g. .erb or .slim + # instead of .html.erb or .html.slim), the child component crashes with + # "undefined method 'upcase' for nil" during compilation. + render_inline(FormatLessChildComponent.new) do + "Hello World" + end + + assert_selector("div.child", text: "Hello World") + end + def test_inherited_inline_component_inherits_inline_method render_inline(InlineInheritedComponent.new) From 7fcc7f7d939cd5d2e72e8c13cc3d2f761389a00c Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Wed, 1 Apr 2026 13:15:59 -0600 Subject: [PATCH 3/7] clean up tests --- ...nent.erb => content_security_policy_nonce_component.html.erb} | 0 ...component.erb => invalid_named_parameters_component.html.erb} | 0 ...eters_component.erb => invalid_parameters_component.html.erb} | 0 test/sandbox/config/environments/test.rb | 1 + 4 files changed, 1 insertion(+) rename test/sandbox/app/components/{content_security_policy_nonce_component.erb => content_security_policy_nonce_component.html.erb} (100%) rename test/sandbox/app/components/{invalid_named_parameters_component.erb => invalid_named_parameters_component.html.erb} (100%) rename test/sandbox/app/components/{invalid_parameters_component.erb => invalid_parameters_component.html.erb} (100%) diff --git a/test/sandbox/app/components/content_security_policy_nonce_component.erb b/test/sandbox/app/components/content_security_policy_nonce_component.html.erb similarity index 100% rename from test/sandbox/app/components/content_security_policy_nonce_component.erb rename to test/sandbox/app/components/content_security_policy_nonce_component.html.erb diff --git a/test/sandbox/app/components/invalid_named_parameters_component.erb b/test/sandbox/app/components/invalid_named_parameters_component.html.erb similarity index 100% rename from test/sandbox/app/components/invalid_named_parameters_component.erb rename to test/sandbox/app/components/invalid_named_parameters_component.html.erb diff --git a/test/sandbox/app/components/invalid_parameters_component.erb b/test/sandbox/app/components/invalid_parameters_component.html.erb similarity index 100% rename from test/sandbox/app/components/invalid_parameters_component.erb rename to test/sandbox/app/components/invalid_parameters_component.html.erb diff --git a/test/sandbox/config/environments/test.rb b/test/sandbox/config/environments/test.rb index 96e43b2b3..6d55d4557 100644 --- a/test/sandbox/config/environments/test.rb +++ b/test/sandbox/config/environments/test.rb @@ -9,6 +9,7 @@ end Warning.ignore(/warning: parser\/current/) +Warning.ignore(/(format_less_parent|format_less_child|no_format)_component.erb is missing, defaulting to :html/) Sandbox::Application.configure do # Settings specified here will take precedence over those in config/application.rb From 8d5a4e9197febb32834e3108fbcdc0cf74642fce Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Wed, 1 Apr 2026 13:21:22 -0600 Subject: [PATCH 4/7] erb_lint --- .erb_lint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.erb_lint.yml b/.erb_lint.yml index 530f46bdc..6e01701b8 100644 --- a/.erb_lint.yml +++ b/.erb_lint.yml @@ -2,6 +2,7 @@ EnableDefaultLinters: true exclude: - '**/vendor/**/*' + - 'test/sandbox/app/components/content_security_policy_nonce_component.html.erb' linters: ErbSafety: enabled: true From 40b7a7c123acfcc63afd6079f6eb05b297c8803b Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Wed, 1 Apr 2026 13:23:53 -0600 Subject: [PATCH 5/7] try this warning suppression approach --- lib/view_component/template.rb | 2 +- test/sandbox/config/environments/test.rb | 1 - test/test_helper.rb | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/view_component/template.rb b/lib/view_component/template.rb index a05803a28..8cc330075 100644 --- a/lib/view_component/template.rb +++ b/lib/view_component/template.rb @@ -24,7 +24,7 @@ def initialize(component:, details:, path:) # If the template file has no format (e.g. .erb instead of .html.erb), # assume the default format (html). if details.format.nil? - Kernel.warn("Template format for #{path} is missing, defaulting to :html.") + Kernel.warn("WARNING: Template format for #{path} is missing, defaulting to :html.") details = ActionView::TemplateDetails.new(details.locale, details.handler, DEFAULT_FORMAT, details.variant) end diff --git a/test/sandbox/config/environments/test.rb b/test/sandbox/config/environments/test.rb index 6d55d4557..96e43b2b3 100644 --- a/test/sandbox/config/environments/test.rb +++ b/test/sandbox/config/environments/test.rb @@ -9,7 +9,6 @@ end Warning.ignore(/warning: parser\/current/) -Warning.ignore(/(format_less_parent|format_less_child|no_format)_component.erb is missing, defaulting to :html/) Sandbox::Application.configure do # Settings specified here will take precedence over those in config/application.rb diff --git a/test/test_helper.rb b/test/test_helper.rb index 13353dbd6..f6ddbd2e5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -14,6 +14,7 @@ module Warning def self.warn(message) called_by = caller_locations(1, 1).first.path return super unless called_by&.start_with?(PROJECT_ROOT) && !called_by.start_with?("#{PROJECT_ROOT}/vendor") + return super if message.include?("Template format for") raise "Warning: #{message}" end From bab5bd0a27603bc74bee026c66397bf848be51b9 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Wed, 1 Apr 2026 13:33:50 -0600 Subject: [PATCH 6/7] fix flaky test --- test/sandbox/test/inline_template_test.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/sandbox/test/inline_template_test.rb b/test/sandbox/test/inline_template_test.rb index dcb5741ec..67acc680c 100644 --- a/test/sandbox/test/inline_template_test.rb +++ b/test/sandbox/test/inline_template_test.rb @@ -216,15 +216,17 @@ class InlineComponentDerivedFromComponentSupportingVariants < Level2Component skip unless Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 without_template_annotations do - with_coverage_running do - # Force recompilation with coverage "enabled" but annotations disabled - ViewComponent::CompileCache.cache.delete(ErbComponent) + with_new_cache do + with_coverage_running do + # Force recompilation with coverage "enabled" but annotations disabled + ViewComponent::CompileCache.cache.delete(ErbComponent) - # This would segfault in v4.3.0 because it only avoided -1 lineno - # when annotations were enabled - render_inline(ErbComponent.new(message: "Foo bar")) + # This would segfault in v4.3.0 because it only avoided -1 lineno + # when annotations were enabled + render_inline(ErbComponent.new(message: "Foo bar")) - assert_selector("div", text: "Foo bar") + assert_selector("div", text: "Foo bar") + end end end end From a54a0c4dc14f12effe89991a2872213dfcc01093 Mon Sep 17 00:00:00 2001 From: Joel Hawksley Date: Wed, 1 Apr 2026 13:42:31 -0600 Subject: [PATCH 7/7] attempt flaky fix --- test/sandbox/test/inline_template_test.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/sandbox/test/inline_template_test.rb b/test/sandbox/test/inline_template_test.rb index 67acc680c..dcb5741ec 100644 --- a/test/sandbox/test/inline_template_test.rb +++ b/test/sandbox/test/inline_template_test.rb @@ -216,17 +216,15 @@ class InlineComponentDerivedFromComponentSupportingVariants < Level2Component skip unless Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 without_template_annotations do - with_new_cache do - with_coverage_running do - # Force recompilation with coverage "enabled" but annotations disabled - ViewComponent::CompileCache.cache.delete(ErbComponent) + with_coverage_running do + # Force recompilation with coverage "enabled" but annotations disabled + ViewComponent::CompileCache.cache.delete(ErbComponent) - # This would segfault in v4.3.0 because it only avoided -1 lineno - # when annotations were enabled - render_inline(ErbComponent.new(message: "Foo bar")) + # This would segfault in v4.3.0 because it only avoided -1 lineno + # when annotations were enabled + render_inline(ErbComponent.new(message: "Foo bar")) - assert_selector("div", text: "Foo bar") - end + assert_selector("div", text: "Foo bar") end end end