rigor-rspec
Validates RSpec let / subject declarations within each
describe / context scope. It is deliberately small: the two
checks it ships have the lowest false-positive risk of the
proposed RSpec surface, run in pure syntactic-walk mode, and
catch real bugs that rspec / rubocop-rspec don’t always
surface clearly. No RSpec runtime dependency.
It ships bundled in rigortype. Activate it under plugins::
plugins: - rigor-rspecWhat it checks
Section titled “What it checks”RSpec.describe "User" do let(:user) { :alice } let(:user) { :bob } # ← warning: duplicate `let(:user)`
let(:tags) { tags.map(&:up) } # ← error: self-referencing let
context "when admin" do let(:user) { :admin } # ← OK: different scope endendspec/user_spec.rb:5:3: warning: duplicate `let(:user)` in this scope (first declared at line 4); the last declaration wins at runtimespec/user_spec.rb:7:3: error: `let(:tags)` references its own name `tags` — this will infinite-loop at runtime- Duplicate
let/subjectdeclarations within the same scope —warning. RSpec’s runtime lets the last declaration win, so the first is silently shadowed; the message names the line of the first declaration. - Self-referencing
let/subject— calling the declared name from inside its own block body —error. At runtime this infinite-loops.
The walker recognises RSpec.describe … do (root), nested
describe / context … do, let(:name) / let!(:name), and
subject(:name) / bare subject (the implicit :subject).
Configuration
Section titled “Configuration”No configuration knobs. The plugin walks every file on the
project’s paths: for RSpec.describe … do blocks; files with
no recognised describe block are silently skipped, so it is safe
to enable project-wide alongside non-spec files.
Limitations
Section titled “Limitations”- No let-typo detection in
itbodies. Flagging a misspelledletname inside anitblock needs a much heavier walker (matcher DSL, helper methods, the let scope chain) — queued. - No mock-target validation.
expect(x).to receive(:nme)againstx’s methods is a separate slice. - No shared-context resolution.
include_context,shared_context, andit_behaves_likeare ignored. - Self-reference detection is intra-block only. An indirect
loop (
let(:user) { foo }wherefoocalls back touser) is not flagged. - Constant validation (
RSpec.describe SomeClass) is the engine’s job, not this plugin’s.
Related plugins
Section titled “Related plugins”rspec-rails and shoulda-matchers matchers are mostly
behavioral (they assert runtime state, not a static type), so
they are out of scope here; the queued rigor-rspec-rails and
rigor-shoulda-matchers plugins would emit domain-specific
diagnostics for them. The README covers that boundary in detail.
Plugin internals
Section titled “Plugin internals”The scope-walker / analyzer layout, how to run the demo, the
contract surfaces this plugin exercises, and the future-direction
slices are in the
plugin’s README. To
write a plugin, see examples/
and the rigor-plugin-author skill.
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.