From 7a448bf3470f24615e3597ec48ca16e4f4b6ed85 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Mon, 19 Oct 2026 15:08:40 -0400 Subject: [PATCH 1/2] actionpack: type abstract render_to_body untyped AbstractController::Rendering#render_to_body is an empty hook: the including class supplies the body through ActionView::Rendering, ActionController::Renderers and ActionController::Rendering, and ActionController::Rendering never returns nil (`super || ... || " "`). The generated `-> nil` described only the empty default, so tools that type an override from the method it overrides typed every controller render_to_body as nil. Streaming renders return a body object rather than a String, so the hook returns untyped. The declaration and its Rails doc move to actioncontroller.rbs beside render_to_string. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn --- gems/actionpack/7.2/_test/test.rb | 9 +++++++++ gems/actionpack/7.2/_test/test.rbs | 6 ++++++ gems/actionpack/7.2/actioncontroller.rbs | 3 +++ gems/actionpack/7.2/actionpack-generated.rbs | 3 --- gems/actionpack/8.1/_test/test.rb | 9 +++++++++ gems/actionpack/8.1/_test/test.rbs | 6 ++++++ gems/actionpack/8.1/actioncontroller.rbs | 3 +++ gems/actionpack/8.1/actionpack-generated.rbs | 3 --- 8 files changed, 36 insertions(+), 6 deletions(-) diff --git a/gems/actionpack/7.2/_test/test.rb b/gems/actionpack/7.2/_test/test.rb index edda1d0d..0ebd9746 100644 --- a/gems/actionpack/7.2/_test/test.rb +++ b/gems/actionpack/7.2/_test/test.rb @@ -31,3 +31,12 @@ def show request_http_digest_authentication("Admin", "Denied").length end end + +# render_to_body returns what the including class renders, so it can be truthy +class BodyRenderer + include AbstractController::Rendering + + def show + render_to_body(template: "show") ? "rendered" : "empty" + end +end diff --git a/gems/actionpack/7.2/_test/test.rbs b/gems/actionpack/7.2/_test/test.rbs index 18bcbc7c..c562c903 100644 --- a/gems/actionpack/7.2/_test/test.rbs +++ b/gems/actionpack/7.2/_test/test.rbs @@ -10,3 +10,9 @@ end class AuthController < ActionController::Base def show: () -> void end + +class BodyRenderer + include AbstractController::Rendering + + def show: () -> String +end diff --git a/gems/actionpack/7.2/actioncontroller.rbs b/gems/actionpack/7.2/actioncontroller.rbs index 3a515900..fcf46584 100644 --- a/gems/actionpack/7.2/actioncontroller.rbs +++ b/gems/actionpack/7.2/actioncontroller.rbs @@ -291,6 +291,9 @@ end module AbstractController module Rendering + # Performs the actual template rendering. + def render_to_body: (?::Hash[untyped, untyped] options) -> untyped + def render_to_string: (*untyped args) ?{ () -> untyped } -> ::String end end diff --git a/gems/actionpack/7.2/actionpack-generated.rbs b/gems/actionpack/7.2/actionpack-generated.rbs index 7136f58c..0b733120 100644 --- a/gems/actionpack/7.2/actionpack-generated.rbs +++ b/gems/actionpack/7.2/actionpack-generated.rbs @@ -511,9 +511,6 @@ module AbstractController # sticks the result in self.response_body. def render: (*untyped args) { () -> untyped } -> untyped - # Performs the actual template rendering. - def render_to_body: (?::Hash[untyped, untyped] options) -> nil - # Returns Content-Type of rendered content. def rendered_format: () -> untyped diff --git a/gems/actionpack/8.1/_test/test.rb b/gems/actionpack/8.1/_test/test.rb index db5a44c6..d2adbe53 100644 --- a/gems/actionpack/8.1/_test/test.rb +++ b/gems/actionpack/8.1/_test/test.rb @@ -117,3 +117,12 @@ def show request_http_digest_authentication("Admin", "Denied").length end end + +# render_to_body returns what the including class renders, so it can be truthy +class BodyRenderer + include AbstractController::Rendering + + def show + render_to_body(template: "show") ? "rendered" : "empty" + end +end diff --git a/gems/actionpack/8.1/_test/test.rbs b/gems/actionpack/8.1/_test/test.rbs index d568eb0a..7a351c1c 100644 --- a/gems/actionpack/8.1/_test/test.rbs +++ b/gems/actionpack/8.1/_test/test.rbs @@ -24,3 +24,9 @@ end class AuthController < ActionController::Base def show: () -> void end + +class BodyRenderer + include AbstractController::Rendering + + def show: () -> String +end diff --git a/gems/actionpack/8.1/actioncontroller.rbs b/gems/actionpack/8.1/actioncontroller.rbs index bd99d005..379d6247 100644 --- a/gems/actionpack/8.1/actioncontroller.rbs +++ b/gems/actionpack/8.1/actioncontroller.rbs @@ -317,6 +317,9 @@ end module AbstractController module Rendering + # Performs the actual template rendering. + def render_to_body: (?::Hash[untyped, untyped] options) -> untyped + def render_to_string: (*untyped args) ?{ () -> untyped } -> ::String end end diff --git a/gems/actionpack/8.1/actionpack-generated.rbs b/gems/actionpack/8.1/actionpack-generated.rbs index c1aa5d0e..7b3b2ffe 100644 --- a/gems/actionpack/8.1/actionpack-generated.rbs +++ b/gems/actionpack/8.1/actionpack-generated.rbs @@ -527,9 +527,6 @@ module AbstractController # Supported options depend on the underlying `render_to_body` implementation. def render: (*untyped args) ?{ (?) -> untyped } -> untyped - # Performs the actual template rendering. - def render_to_body: (?::Hash[untyped, untyped] options) -> nil - # Returns `Content-Type` of rendered content. def rendered_format: () -> untyped From 0900ca822ccc1e7faf6cdc2fb41debc2f859cfcd Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Thu, 29 Oct 2026 19:23:17 -0400 Subject: [PATCH 2/2] Move render_to_body tests to end of test files The tests sat next to the ones ruby/gem_rbs_collection#1115 adds, so whichever merged second would conflict. At the end of each file the two PRs touch separate hunks. --- gems/actionpack/7.2/_test/test.rb | 18 +++++++++--------- gems/actionpack/7.2/_test/test.rbs | 8 ++++---- gems/actionpack/8.1/_test/test.rb | 18 +++++++++--------- gems/actionpack/8.1/_test/test.rbs | 8 ++++---- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/gems/actionpack/7.2/_test/test.rb b/gems/actionpack/7.2/_test/test.rb index cad98d02..88020fed 100644 --- a/gems/actionpack/7.2/_test/test.rb +++ b/gems/actionpack/7.2/_test/test.rb @@ -32,15 +32,6 @@ def show end end -# render_to_body returns what the including class renders, so it can be truthy -class BodyRenderer - include AbstractController::Rendering - - def show - render_to_body(template: "show") ? "rendered" : "empty" - end -end - # Routing DSL forms from the Rails routing guide class AdminConstraint def matches?(request) @@ -91,3 +82,12 @@ def matches?(request) mount Rack::Head.new(->(_env) { [200, { "content-type" => "text/plain" }, ["OK"]] }), at: "/head", as: :head with_default_scope(module: "v1") { get "ping", to: "ping#show" }.has_named_route?(:ping) end + +# render_to_body returns what the including class renders, so it can be truthy +class BodyRenderer + include AbstractController::Rendering + + def show + render_to_body(template: "show") ? "rendered" : "empty" + end +end diff --git a/gems/actionpack/7.2/_test/test.rbs b/gems/actionpack/7.2/_test/test.rbs index b43b54d7..d704bb01 100644 --- a/gems/actionpack/7.2/_test/test.rbs +++ b/gems/actionpack/7.2/_test/test.rbs @@ -11,12 +11,12 @@ class AuthController < ActionController::Base def show: () -> void end +class AdminConstraint + def matches?: (ActionDispatch::Request request) -> bool +end + class BodyRenderer include AbstractController::Rendering def show: () -> String end - -class AdminConstraint - def matches?: (ActionDispatch::Request request) -> bool -end diff --git a/gems/actionpack/8.1/_test/test.rb b/gems/actionpack/8.1/_test/test.rb index 87cdfa77..e21c064d 100644 --- a/gems/actionpack/8.1/_test/test.rb +++ b/gems/actionpack/8.1/_test/test.rb @@ -118,15 +118,6 @@ def show end end -# render_to_body returns what the including class renders, so it can be truthy -class BodyRenderer - include AbstractController::Rendering - - def show - render_to_body(template: "show") ? "rendered" : "empty" - end -end - # Routing DSL forms from the Rails routing guide class AdminConstraint def matches?(request) @@ -177,3 +168,12 @@ def matches?(request) mount Rack::Head.new(->(_env) { [200, { "content-type" => "text/plain" }, ["OK"]] }), at: "/head", as: :head with_default_scope(module: "v1") { get "ping", to: "ping#show" }.has_named_route?(:ping) end + +# render_to_body returns what the including class renders, so it can be truthy +class BodyRenderer + include AbstractController::Rendering + + def show + render_to_body(template: "show") ? "rendered" : "empty" + end +end diff --git a/gems/actionpack/8.1/_test/test.rbs b/gems/actionpack/8.1/_test/test.rbs index 06eb3595..6cf28590 100644 --- a/gems/actionpack/8.1/_test/test.rbs +++ b/gems/actionpack/8.1/_test/test.rbs @@ -25,12 +25,12 @@ class AuthController < ActionController::Base def show: () -> void end +class AdminConstraint + def matches?: (ActionDispatch::Request request) -> bool +end + class BodyRenderer include AbstractController::Rendering def show: () -> String end - -class AdminConstraint - def matches?: (ActionDispatch::Request request) -> bool -end