feat(generator): ship the docs-kit RuboCop cops from the gem (#22)#38
Merged
Conversation
## Summary
Both consumer sites carried a byte-identical hand-copied cop
(`DocsKit/RenderComponentPreferred`). Copy-paste cops drift and new sites
never got them. This upstreams the cops into the gem so a consuming site
requires them by gem path (two lines, scaffolded by the install generator)
and deletes its copies.
- Move `RenderComponentPreferred` into `lib/rubocop/cop/docs_kit/` and add
one new cop, `EscapedInterpolationInHeredoc` (steer `\#{...}` escapes in a
double-quoted heredoc to a single-quoted delimiter — the recurring escape
tax every audited site paid).
- `lib/docs_kit/rubocop.rb` is the require entry point; it loads `rubocop`
LAZILY, so RuboCop stays a development-time dependency of the host, never a
runtime dependency of docs-kit. Added to the gemspec dev deps + zeitwerk
`loader.ignore` (the file defines `RuboCop::Cop::DocsKit::*`, not a
`DocsKit::Rubocop` constant).
- Ship `config/rubocop/docs_kit.yml` (both cops enabled, scoped to
`app/views/docs/**/*`).
- Install generator + `docs-kit new` inject `require: docs_kit/rubocop` and
`inherit_gem: { docs-kit: config/rubocop/docs_kit.yml }` into the site's
`.rubocop.yml`, idempotently (created minimal when absent; merged — not
clobbered — into an existing omakase config).
- README section + CHANGELOG entry.
## Autocorrect-safety fixes (found by adversarial verification)
- `EscapedInterpolationInHeredoc`: Ruby interpolates `#@ivar`/`#$global` too,
not only `#{...}`. Live-detection now treats any non-`str` dstr child as
live (was `begin`-only), so a delimiter swap can no longer freeze a live
`#@name` into literal text. De-escaping now strips the backslash from every
escaped form (`\#{`, `\#@`, `\#$`), not just `\#{`.
- `RenderComponentPreferred`: a kit render nested inside another kit render's
block no longer produces overlapping corrections (Parser::ClobberingError).
The correction now replaces only the `render X.new(...)` send, leaving the
block in place, so the inner render corrects independently.
## Test Coverage
- 16 cop specs (expect_offense/expect_correction) incl. sigil-interpolation
live/de-escape cases and the nested-render clobber case.
- 6 generator specs: `.rubocop.yml` created when absent, merged into omakase,
appends to an existing require list, idempotent on re-run.
- Zeitwerk-safety specs: gem boots, no `DocsKit::Rubocop` autoload, cops load
under the RuboCop namespace when required.
- Scoped the rubocop-rspec harness to the cop specs only (a global
`require "rubocop/rspec/support"` had shadowed `DocsKit::Registry` specs via
`CopHelper#registry` under random ordering).
## Verification
- [x] bundle exec rake (rspec + rubocop) — 489 examples, 0 failures, 96% cov
- [x] End-to-end: cops fire, autocorrect, and scope correctly on a fixture site
- [x] Real `rubocop -A`: live `#@ivar` reported "fix by hand", file unchanged;
nested render corrects cleanly with no clobbering error
Claude-Session: https://claude.ai/code/session_01FPQb6z3YwcKRMbvoJhdxnX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #22.
Problem
Both consumer sites carried a byte-identical hand-copied cop
(
DocsKit/RenderComponentPreferred). Copy-paste cops drift and new sites nevergot them. This upstreams the cops into the gem so a consuming site requires them
by gem path (two lines, scaffolded by the install generator) and deletes its
copies.
What ships
lib/rubocop/cop/docs_kit/render_component_preferred.rbDocsUI::Code(...)overrender DocsUI::Code.new(...)lib/rubocop/cop/docs_kit/escaped_interpolation_in_heredoc.rb\#{...}escapes in a double-quoted heredoc to a single-quoted delimiter (the recurring "escape tax")lib/docs_kit/rubocop.rbrubocoplazily (dev-time dep of the host, never a runtime dep of docs-kit)config/rubocop/docs_kit.ymlapp/views/docs/**/*by defaultWiring (automatic)
The install generator and
docs-kit newinject two lines into the site's.rubocop.yml, idempotently — created minimal when absent, merged (notclobbered) into an existing
rails newomakase config:RuboCop stays a development-time dependency of the host app. Every generated
site already has it via
rubocop-rails-omakase.Autocorrect-safety fixes (found by adversarial verification)
A multi-agent verification pass (probe against adversarial fixtures → independent
skeptical judges) surfaced three real defects, all now fixed and regression-tested:
EscapedInterpolationInHeredoc— live sigil interpolation (HIGH). Rubyinterpolates
#@ivarand#$globalin a double-quoted string too, not only#{...}. Live-detection wasbegin-child-only, so a heredoc whose liveinterpolation was
#@namegot its delimiter swapped and the interpolationsilently frozen to literal text. Now any non-
strdstr child counts aslive. Verified on the real CLI: reports "fix by hand", leaves the file unchanged.
EscapedInterpolationInHeredoc— inconsistent de-escaping (HIGH).Autocorrect stripped the backslash only from
\#{while switching to asingle-quoted delimiter (which makes all backslashes literal), so a
\#@namekept a backslash it used to consume. Now strips every escaped form
(
\#{,\#@,\#$).RenderComponentPreferred— overlapping corrections (MEDIUM). A kit rendernested inside another kit render's block produced overlapping rewrite ranges
(
Parser::ClobberingError; the real CLI emitted a "this is a RuboCop bug"error). The correction now replaces only the
render X.new(...)send, leavingthe block in place so the inner render corrects independently.
Zeitwerk safety
lib/rubocop/**is outside the gem's loader dirs, andlib/docs_kit/rubocop.rb(which defines
RuboCop::Cop::DocsKit::*, not aDocsKit::Rubocopconstant) isloader.ignore-d. Specs assert the gem boots with no autoload registered forDocsKit::Rubocop.Test plan
bundle exec rake— 489 examples, 0 failures, 96% coverage, RuboCop clean.expect_offense/expect_correction) including the sigil-interplive/de-escape cases and the nested-render clobber case.
.rubocop.ymlcreated when absent, merged into omakase,appends to an existing
requirelist, idempotent on re-run.docs/app: cops fire,autocorrect, and respect the
Includescope.Out of scope
repos, a follow-up).
https://claude.ai/code/session_01FPQb6z3YwcKRMbvoJhdxnX