Skip to content

Analysis: Follow state through a block parameter - #2288

Merged
marcoroth merged 1 commit into
mainfrom
analysis/rust-block-aliases
Aug 19, 2026
Merged

Analysis: Follow state through a block parameter#2288
marcoroth merged 1 commit into
mainfrom
analysis/rust-block-aliases

Conversation

@marcoroth

Copy link
Copy Markdown
Owner

This pull request teaches the dependency analysis that a block's parameters carry whatever that block iterates, in one template and across a render call, and in all three languages that implement it.

<ul>
  <% @items.each do |item| %>
    <li><%= item.name %></li>
  <% end %>
</ul>

item is @items, one at a time. Matching is by name, so nothing connected the two, and item.name was attributed to nothing.

Ruby learned this within a single template in #2280. This finishes it: the two ports learn the same thing, and the Ruby trace learns it across a render boundary, which is where it was answering a user-facing question incorrectly.

affected_templates and state_flow back herb dependencies and herb actionview flow, and their whole product is the list of templates a piece of state reaches. One shape was missing from it.

traced:

<%= render partial: "posts/card", collection: @posts %>

not traced:

<% @posts.each do |post| %>
  <%= render "posts/card", card: post %>
<% end %>

For the second, affected_templates(entry, "@posts") returned ["index.html.erb"] and omitted _card.html.erb, and state_flow returned a tree with no child. trace_state asks whether a render call's local carries the state, and card: post names a block parameter that nothing tied to the collection that bound it.

The collector now records the blocks open around a render call, with the names each binds, and a local whose value is one of those names carries what its block iterates.

analyze now parses with iteration_nodes, so a block running per item and a block running once are different node types. Without that, form_with model: @post do |f| looks like a collection and f carries @post into every partial rendered inside the form.

Once the trace reaches _card.html.erb, its slots become addressable, and they came back as an identity, which is a client writing one value into every card. A render inside an iterating block is now per item the same way collection: is, and the modes below a block that runs once are left alone.

Follow up on #2279, #2280, #2281, #2282 and #2283.

@github-actions github-actions Bot added ruby Ruby source for the gem and its libraries typescript TypeScript source across the javascript/ packages rbs RBS type signatures in sig/ engine Herb engine and Rails template compilation rubygem The herb RubyGem and its packaging rust Rust bindings and the Herb Rust crate analysis labels Aug 18, 2026
@nx-cloud

nx-cloud Bot commented Aug 18, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit af3768c

Command Status Duration Result
nx run-many -t test --all --parallel --exclude=... ✅ Succeeded 3m 9s View ↗
nx run-many -t build --all --exclude=herb-langu... ✅ Succeeded 16s View ↗
nx build @herb-tools/tailwind-class-sorter ✅ Succeeded 1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-08-18 23:54:02 UTC

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

🌿 Interactive Playground and Documentation Preview

A preview deployment has been built for this pull request. Try out the changes live in the interactive playground:


🌱 Grown from commit b337dfe


✅ Preview deployment has been cleaned up.

@pkg-pr-new

pkg-pr-new Bot commented Aug 18, 2026

Copy link
Copy Markdown
npx https://pkg.pr.new/@herb-tools/formatter@2288
npx https://pkg.pr.new/@herb-tools/language-server@2288
npx https://pkg.pr.new/@herb-tools/linter@2288

commit: b337dfe

@marcoroth marcoroth added the reactivity Reactive ERB templates: diff and re-render only what changed label Aug 18, 2026
@marcoroth
marcoroth merged commit 20b200a into main Aug 19, 2026
34 checks passed
@marcoroth
marcoroth deleted the analysis/rust-block-aliases branch August 19, 2026 00:02
marcoroth added a commit that referenced this pull request Aug 19, 2026
This pull request teaches the dependency analysis that a local assigned
from a piece of state carries that state.

```erb
<% total = @items.size %>
<p><%= total %></p>
```

`total` is `@items`, counted. Matching is by name, so nothing connected
the two and `total` was attributed to nothing, which meant `herb
dependencies` and `herb actionview flow` left the paragraph out of what
`@items` reaches.

Follow up on #2288, which did the same for the names a block binds. This
is the other way a state gets a second name.

An assignment binds the name only when its right hand side reads the
state, and the name carries it for as long as the assignment is in
scope.

```erb
<% a = @items.size %>
<% b = a * 2 %>
<p><%= b %></p>
```

`a` carries `@items` and `b` carries `a`, so `b` reaches back to
`@items` through the chain.

Three cases that look like assignments and are not:

- `<% other = 5 %>` binds nothing, because nothing on the right reads
the state.
- `<% if total == @items.size %>` is a comparison, and `total` is left
alone.
- A local assigned inside a block stops carrying at the end of that
block, the same rule the block parameters follow.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

analysis engine Herb engine and Rails template compilation rbs RBS type signatures in sig/ reactivity Reactive ERB templates: diff and re-render only what changed ruby Ruby source for the gem and its libraries rubygem The herb RubyGem and its packaging rust Rust bindings and the Herb Rust crate typescript TypeScript source across the javascript/ packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant