From 65f0a44b3de0c2b237e5cb32c19c79a77199e8c3 Mon Sep 17 00:00:00 2001 From: George Holborn Date: Mon, 2 Feb 2026 17:15:57 +0000 Subject: [PATCH 1/2] Fix segfault when Ruby coverage is enabled with Rails 8.1 ERB templates v4.2.0 introduced a fix for incorrect line numbers in stack traces on Rails 8.1 by using negative lineno values (-1) in class_eval. However, negative line numbers cause segmentation faults when Ruby's Coverage module is enabled (e.g., when using SimpleCov in CI). This is a known Ruby bug: https://bugs.ruby-lang.org/issues/19363 The fix detects whether coverage is running via Coverage.running? and adapts the lineno value accordingly: - Coverage OFF: Use -1 (correct line numbers in stack traces) - Coverage ON: Use 1 (avoids segfault, line numbers off by ~2) Note: lineno=0 was also tested but causes the same segfault, so 1 is the minimum safe value when coverage is enabled. Co-Authored-By: Claude Opus 4.5 --- lib/view_component/template.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/view_component/template.rb b/lib/view_component/template.rb index 206865556..c34bc11a9 100644 --- a/lib/view_component/template.rb +++ b/lib/view_component/template.rb @@ -21,11 +21,13 @@ def initialize(component:, details:, lineno: nil, path: nil) class File < Template def initialize(component:, details:, path:) - # Rails 8.1 added a newline to the compiled ERB output in - # https://github.com/rails/rails/pull/53731 + # Rails 8.1 added a newline to compiled ERB output (rails/rails#53731). + # Use -1 to compensate for correct line numbers in stack traces. + # However, negative line numbers cause segfaults when Ruby's coverage + # is enabled (bugs.ruby-lang.org/issues/19363), so use 1 in that case. lineno = if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb - - 1 + coverage_running? ? 1 : -1 else 0 end @@ -54,11 +56,13 @@ class Inline < Template def initialize(component:, inline_template:) details = ActionView::TemplateDetails.new(nil, inline_template.language.to_sym, nil, nil) - # Rails 8.1 added a newline to the compiled ERB output in - # https://github.com/rails/rails/pull/53731 + # Rails 8.1 added a newline to compiled ERB output (rails/rails#53731). + # Subtract 1 to compensate for correct line numbers in stack traces. + # However, negative line numbers cause segfaults when Ruby's coverage + # is enabled (bugs.ruby-lang.org/issues/19363), so skip the adjustment in that case. lineno = if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb - inline_template.lineno - 1 + coverage_running? ? inline_template.lineno : inline_template.lineno - 1 else inline_template.lineno end @@ -126,6 +130,10 @@ def #{call_method_name} @component.define_method(safe_method_name, @component.instance_method(@call_method_name)) end + def coverage_running? + defined?(Coverage) && Coverage.running? + end + def safe_method_name_call m = safe_method_name proc { send(m) } From 324f46073bb5cdee384074e637cde9af777492a9 Mon Sep 17 00:00:00 2001 From: George Holborn Date: Mon, 9 Feb 2026 19:20:45 +0000 Subject: [PATCH 2/2] Address review feedback: strip annotation line instead of adjusting lineno - Remove coverage_running? check from Inline class (inline templates start at line 2+, so subtracting 1 won't result in negative line numbers) - For File templates, strip the first line of compiled source when coverage is running AND annotations are enabled, instead of using lineno=1 - Add regression tests for coverage segfault fix Co-Authored-By: Claude Opus 4.5 --- lib/view_component/template.rb | 28 ++++++++++--- test/sandbox/test/inline_template_test.rb | 50 +++++++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/lib/view_component/template.rb b/lib/view_component/template.rb index c34bc11a9..afa1f8aec 100644 --- a/lib/view_component/template.rb +++ b/lib/view_component/template.rb @@ -21,13 +21,21 @@ def initialize(component:, details:, lineno: nil, path: nil) class File < Template def initialize(component:, details:, path:) + @strip_annotation_line = false + # Rails 8.1 added a newline to compiled ERB output (rails/rails#53731). # Use -1 to compensate for correct line numbers in stack traces. # However, negative line numbers cause segfaults when Ruby's coverage - # is enabled (bugs.ruby-lang.org/issues/19363), so use 1 in that case. + # is enabled (bugs.ruby-lang.org/issues/19363). In that case, strip the + # annotation line from compiled source instead. lineno = if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb - coverage_running? ? 1 : -1 + if coverage_running? && ActionView::Base.annotate_rendered_view_with_filenames + @strip_annotation_line = true + 0 + else + -1 + end else 0 end @@ -48,6 +56,16 @@ def type def source ::File.read(@path) end + + private + + def compiled_source + result = super + # Strip the annotation line to maintain correct line numbers when coverage + # is running (avoids segfault from negative lineno) + result = result.sub(/\A[^\n]*\n/, "") if @strip_annotation_line + result + end end class Inline < Template @@ -58,11 +76,11 @@ def initialize(component:, inline_template:) # Rails 8.1 added a newline to compiled ERB output (rails/rails#53731). # Subtract 1 to compensate for correct line numbers in stack traces. - # However, negative line numbers cause segfaults when Ruby's coverage - # is enabled (bugs.ruby-lang.org/issues/19363), so skip the adjustment in that case. + # Inline templates start at line 2+ (defined inside a class), so this + # won't result in negative line numbers that cause segfaults with coverage. lineno = if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb - coverage_running? ? inline_template.lineno : inline_template.lineno - 1 + inline_template.lineno - 1 else inline_template.lineno end diff --git a/test/sandbox/test/inline_template_test.rb b/test/sandbox/test/inline_template_test.rb index 5f380bebf..60fa38adf 100644 --- a/test/sandbox/test/inline_template_test.rb +++ b/test/sandbox/test/inline_template_test.rb @@ -189,4 +189,54 @@ class InlineComponentDerivedFromComponentSupportingVariants < Level2Component assert_selector(".greeting-container h1", text: "Hello, Fox Mulder!") end + + # Regression test for https://github.com/ViewComponent/view_component/issues/2540 + # Negative lineno values in class_eval cause segfaults when Ruby's Coverage module + # is enabled. This test verifies that components can be compiled and rendered when + # coverage is running. + test "file-based templates compile without segfault when coverage is running" do + skip unless Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 + + with_new_cache do + with_coverage_running do + # Force recompilation with coverage "enabled" + ViewComponent::CompileCache.cache.delete(ErbComponent) + + # This would segfault before the fix due to negative lineno + render_inline(ErbComponent.new(message: "Foo bar")) + + assert_selector("div", text: "Foo bar") + end + end + end + + test "inline templates compile without segfault when coverage is running" do + skip unless Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 + + with_new_cache do + with_coverage_running do + # Force recompilation with coverage "enabled" + ViewComponent::CompileCache.cache.delete(InlineRaiseErbComponent) + + # Inline templates should still work (lineno is 2+, so -1 won't be negative) + error = assert_raises ArgumentError do + render_inline(InlineRaiseErbComponent.new("Fox Mulder")) + end + + # Verify backtrace still points to correct line + assert_match %r{test/sandbox/test/inline_template_test.rb:22}, error.backtrace[0] + end + end + end + + private + + def with_coverage_running + require "coverage" + already_running = Coverage.running? + Coverage.start unless already_running + yield + ensure + Coverage.result unless already_running + end end