Skip to content

Rust: Implement herb-analysis crate based on Rubydex - #1993

Merged
marcoroth merged 81 commits into
mainfrom
herb-analysis-rust
Aug 13, 2026
Merged

marcoroth merged 81 commits into
mainfrom
herb-analysis-rust

Conversation

@marcoroth

@marcoroth marcoroth commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

This pull request adds a herb-analysis crate that brings Herb's cross-file static analysis to Rust, backed by rubydex.

It started as a spike to evaluate rubydex as a resolution backend and has grown into a full port of the Herb::Analysis namespace, including an actionview command set that mirrors the Ruby CLI subcommands.

Motivation

#1667 implements Action View template dependency analysis in Ruby. Its weakest part is helper and constant resolution: scan_helpers! flat-scans every def in app/helpers/**/*.rb, so it misses gem-provided helpers, misses helpers reaching the view through included concerns, and records constants as a naive "#{receiver}.#{name}" with no namespace resolution. That drives the unknown_calls warnings.

rubydex indexes the analysed app and the gems in its Gemfile.lock, and walks real ancestor chains. The question worth answering was what that enables in Rust that Ruby cannot reach, and the answer turned out to be most of the warning noise.

What the crate provides

The binary exposes two command families. The first are rubydex-backed introspection commands:

helpers    [path] [--only app,gem,rails,route] [--roots A,B]   what a template can call, grouped by origin
audit      [path] [--gem G]                                    cross-check the Action View helper registry
ancestors  <Name> [path] [--built-ins]                         ancestor chain and completeness
constants  [NAME] [--nesting A::B] [path]                      list constants, or resolve one
stats      [path]                                              counts and per-phase timings

helpers reconstructs everything a template in an arbitrary Rails app can call. It resolves gems from Gemfile.lock across all three source types (registry, git checkouts including monorepo subdirectories, and path sources), discovers helper modules by the include_all_helpers convention that no static index can infer from code, walks ActionView::Base for the built-ins, reads helper_method declarations off controllers, and derives route helpers from config/routes.rb.

The second family ports the Ruby herb actionview subcommands:

check         [path]           check render calls and flag unresolved partials
graph         [path]           show the render graph for a project
dependencies  <file>           show a template's dependency manifest
flow          <file> <state>   trace one piece of state through every partial it reaches
context       <partial>        show what a partial is rendered inside of
signature     <partial>        show the strict locals a partial receives

Behind them sit Rust counterparts for PartialResolution, PartialIndex, PartialDeclaration, RenderGraph, ProjectIndex, TemplateDependencies, StateFlow, and RubyLocalsIndex, so the three implementations now share one architecture.

Where Rust goes further than Ruby

Both check commands print the same sections, in the same order, with the same wording. The counts differ, and that difference is the point, Rust resolves names Ruby's scan cannot see.

Ruby Rust
Checked 232 files 232 files
Renders 515 total, 477 with partial, 4 dynamic, 38 other same
Partials 265 on disk, 4 unresolved 265 on disk, 4 unresolved
Unknown calls 108 45

Measured against rubyevents. The remaining Rust figure is dominated by four ui_* helpers that a component library generates by convention.

check output gained three things on both sides:

  • Locals that cannot be inferred is its own section. When no call site contributes a single local name, either because nothing renders the partial or because its callers pass locals opaquely through **locals, there is no signature to learn from. Reporting those as missing methods was misleading.
  • Unknown method calls end with a tally of the most frequent names, since the same name usually recurs across many templates.
  • The summary breaks the warning categories out and reports how many component templates were skipped.
 Summary:
  Checked      232 files
  Renders      515 total | 477 with partial | 4 dynamic | 38 other
  Partials     265 on disk | 4 unresolved | 14 unused
  Warnings     47 undeclared locals | 44 uninferable locals | 1 instance variable | 45 unknown calls
  Ignored      6 component templates in app/components/

Related #1667

@github-actions github-actions Bot added rust Rust bindings and the Herb Rust crate action-view-helpers Action View helper support and metadata labels Aug 4, 2026
marcoroth and others added 2 commits August 4, 2026 14:39
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: Marco Roth <marco.roth@intergga.ch>
@github-actions

github-actions Bot commented Aug 4, 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 cc89a4a


✅ Preview deployment has been cleaned up.

@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026 •

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

commit: cc89a4a

@marcoroth marcoroth added analysis reactivity Reactive ERB templates: diff and re-render only what changed labels Aug 12, 2026
@github-actions github-actions Bot added ruby Ruby source for the gem and its libraries rubygem The herb RubyGem and its packaging labels Aug 13, 2026
@marcoroth
marcoroth marked this pull request as ready for review August 13, 2026 03:15
@github-actions github-actions Bot added the rbs RBS type signatures in sig/ label Aug 13, 2026
@marcoroth
marcoroth enabled auto-merge (squash) August 13, 2026 03:41
@marcoroth
marcoroth merged commit eb99ae5 into main Aug 13, 2026
32 checks passed
@marcoroth
marcoroth deleted the herb-analysis-rust branch August 13, 2026 03:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action-view-helpers Action View helper support and metadata analysis 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants