Skip to content

An instance variable read inside a jbuilder partial is rewritten to a bare local that nothing defines #540

Description

@eddygarcas

Probed at main 23b21b7, Linux x86-64, gcc 16.2.1.

Fixture = fixtures/tiny-api from roundhouse's own repo, with gadgets_controller.rb added as
app/controllers/gadgets_controller.rb, show.json.jbuilder and _gadget.json.jbuilder added
under app/views/gadgets/, and routes.rb replacing config/routes.rb (adds resources :gadgets, only: %i[show], otherwise unchanged). The locals key here deliberately matches the partial's own basename (gadget),
so the parameter-naming issue from #539 does not also fire — this fixture isolates the ivar read:

# app/controllers/gadgets_controller.rb
def show
  @widget = Widget.find_by(id: params[:id])
  @note = "hello"
end

# app/views/gadgets/show.json.jbuilder
json.partial!("gadgets/gadget", gadget: @widget)

# app/views/gadgets/_gadget.json.jbuilder
json.name(gadget.name)
json.note(@note) if @note

Real Rails renders this as {"name":"Sprocket","note":"hello"} — Rails partials see the controller's instance
variables (@note reaches _gadget.json.jbuilder the same way it reaches any other view/partial in the same
request), with no need to pass it as a local.

roundhouse --target spinel (and --target ruby) refuse to emit at all, with no --allow-unsupported:

app/views/gadgets/_gadget.json.jbuilder:2:21: error[ivar_unresolved]: @note has no known type
app/views/gadgets/_gadget.json.jbuilder:2:11: error[ivar_unresolved]: @note has no known type

With --allow-unsupported both targets emit (app/views/gadgets/_gadget_json.rb):

module Views
  module Gadgets
    def self.gadget_json(gadget)
      io = String.new
      io << "{"
      io << "\"name\":"
      io << JsonBuilder.encode_value(gadget.name)
      if note
        io << ","
        io << "\"note\":"
        io << JsonBuilder.encode_value(note)
      end
      io << "}"
      io
    end
  end
end

@note became a bare note that the method never binds — not a parameter, not a local assignment anywhere in
the body. Running the --target ruby tree against a seeded widget row (REQUEST_METHOD=GET PATH_INFO=/gadgets/<id> ruby main.rb) raises on the first request that reaches the if note line:

app/views/gadgets/_gadget_json.rb:8:in 'Views::Gadgets.gadget_json': undefined local variable or method 'note' for module Views::Gadgets (NameError)
      if note
         ^^^^
Did you mean?  not
	from app/views/gadgets/show_json.rb:7:in 'Views::Gadgets.show_json'
	from app/controllers/gadgets_controller.rb:21:in 'GadgetsController#show'

Expected: @note resolves inside the partial the same way it would in Rails — either genuinely plumbed through
(the controller's ivar reaching the partial method, e.g. as an extra parameter the same way index/show templates
already receive their ivar-derived args) or, short of that, a survey/unsupported diagnostic at transpile
time instead of a confident wrong emit that only fails at request time.

Cause: src/lower/jbuilder_to_library/mod.rs, jbuilder_method_parts (around line 315):

let ivar_params: Vec<Symbol> = if is_partial {
    Vec::new()
} else {
    crate::lower::view_to_library::view_read_ivars(&view.body)
};

The surrounding comment states the assumption driving this: "A PARTIAL keeps the convention: its record arrives
positionally from the parent's collection, and it reads no ivar at all." For a non-partial template,
view_read_ivars walks the body and turns every @ivar read into a parameter; for a partial this is
unconditionally skipped, so any @ivar a partial body does read is still rewritten to a bare local by
rewrite_ivars_to_locals (the same ivar→local pass runs regardless of is_partial) but is never added as a
parameter anywhere — the two passes disagree about whether partials can read ivars at all. The strict-mode
error[ivar_unresolved] is the type checker catching the output of that disagreement, not a separate bug.

Duplicate search: gh search issues -R rubys/roundhouse for "jbuilder partial instance variable", "jbuilder ivar undefined", "partial @note", "jbuilder partial ivar scope", "json.partial! @" — no match other than
the unrelated json.partial! @record positional-record case (#322, closed, fixed by merged PR #356) and #360 /
#323 (open, different jbuilder shapes: a runtime-built partial path, and a collection+block/array!/
helper_method cluster). None covers an ivar read inside a partial's own body. PRs touching
jbuilder_to_library (#355, #359, #361, #367, #356 — all merged) address collection blocks, template locals,
conditionals, rescue blocks and the @record positional case; none touches ivar visibility inside partials.
Not a duplicate.

Fixture files

_gadget.json.jbuilder:

json.name(gadget.name)
json.note(@note) if @note

gadgets_controller.rb:

class GadgetsController < ApplicationController
  def show
    @widget = Widget.find_by(id: params[:id])
    @note = "hello"
  end
end

routes.rb:

Rails.application.routes.draw do
  resources :widgets, only: %i[index show create update]
  resources :gadgets, only: %i[show]
  get "/feed.json", to: "formats#literal"
  get "/formats", to: "formats#index"
  get "/formats/:id", to: "formats#show"
end

show.json.jbuilder:

json.partial!("gadgets/gadget", gadget: @widget)

Found while compiling a Rails API app with --target spinel. Happy to send a PR for this if the direction above sounds right.

Activity

  1. eddygarcas commented on Oct 8, 2026

    @eddygarcas
    CollaboratorAuthor

    I'll take this one. It sits in the same function as #539 (jbuilder_method_parts), so I'll send both as one PR, one commit each with its own regression test. If someone already has it in progress, tell me and I'll drop it.

  2. added a commit that references this issue on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions