Skip to content

Analysis: Number a node the way SlotVisitor and SubtreeCompiler do - #2279

Merged
marcoroth merged 1 commit into
mainfrom
slots/node-path
Aug 18, 2026
Merged

Analysis: Number a node the way SlotVisitor and SubtreeCompiler do#2279
marcoroth merged 1 commit into
mainfrom
slots/node-path

Conversation

@marcoroth

@marcoroth marcoroth commented Aug 18, 2026

Copy link
Copy Markdown
Owner

This pull request makes the three implementations of the dependency analysis number a node the same way, so a node_path from any of them names the same node.

SubtreeCompiler says the target it renders is named by "the same path SlotVisitor records for a slot and TemplateDependencies reports for a node, so a caller that has one from either has one that works here". That is the join the slot work depends on.

SlotVisitor walks an element's body and SubtreeCompiler indexes :body, while the analysis collector walked child_nodes, which is [open_tag, *body, close_tag]. Every element level shifted the index by one:

<div><h1><%= @title %></h1></div>

An attribute was addressed by descending into the open tag, where a path does not go, so it reported [0, 0, 1] where the visitor recorded [0] for the element carrying it. The bodies of blocks and loops were walked without pushing a path at all, so nothing inside a loop was addressable.

@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 f2276ea

Command Status Duration Result
nx run-many -t test --all --parallel --exclude=... ✅ Succeeded 3m 6s View ↗
nx run-many -t build --all --exclude=herb-langu... ✅ Succeeded 15s 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 16:23:43 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 f2276ea


✅ 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@2279
npx https://pkg.pr.new/@herb-tools/language-server@2279
npx https://pkg.pr.new/@herb-tools/linter@2279

commit: f2276ea

@marcoroth marcoroth added the reactivity Reactive ERB templates: diff and re-render only what changed label Aug 18, 2026
@marcoroth
marcoroth merged commit 028139e into main Aug 18, 2026
37 of 46 checks passed
@marcoroth
marcoroth deleted the slots/node-path branch August 18, 2026 17:05
marcoroth added a commit that referenced this pull request Aug 19, 2026
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.

```erb
<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:
```erb
<%= render partial: "posts/card", collection: @posts %>
```

not traced:
```erb
<% @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.
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