Replace virtus with regular accessors + testing type-check shim#574
Replace virtus with regular accessors + testing type-check shim#574pnomolos wants to merge 1 commit into
Conversation
|
Hey @pnomolos, I like the idea of removing a dependency; I'm always all for it. Well, not always, it depends, haha. I dug into this and noticed only a small set of classes use virtus. Your instinct is right, and your module faithfully keeps virtus's behavior. But what if we go a bit further and just use plain old attr_accessors instead? The one thing virtus gave us beyond accessors was type coercion. I checked every place these typed attributes get assigned, and the value is always already the right type (complexity/coverage are Floats, duplication += node.mass is integer + integer, and status is always a Symbol). So coercion never actually converts anything here, which means we can drop it without changing behavior. Smell would look like: attr_accessor :context, :cost, :locations, :message,
:score, :status, :type, :analyser
def initialize(attributes = {})
@locations = []
@status = :new
attributes.each { |name, value| public_send("#{name}=", value) }
endAnd AnalysedModule: attr_accessor :coverage, :name, :pathname, :smells, :churn,
:committed_at, :complexity, :duplication, :methods_count
def initialize(attributes = {})
@coverage = 0.0
@smells = []
@complexity = Float::INFINITY
@duplication = 0
attributes.each { |name, value| public_send("#{name}=", value) }
endCleanup bonus baked in: smells_count, file_location, file_name, and line_count were declared as attributes and then immediately overridden by real methods. They just vanish from the accessor list, no longer declared twice. @faisal I'd like to know your opinion on this, if you can. |
I like the thinking here. My only concern is if this is introducing an assumption for the future. What gives us confidence the types will continue to be correct? Would it make sense add tests that confirm type alignment, so we'd catch a mismatch at development time without adding runtime overhead? |
|
I don't know what thoughts are around these parts in regards to Steep or Sorbet, but this screams at me as a prime candidate for that sort of thing. If you're not interested, though, I can see about adding tests in order to confirm alignment without affecting the runtime 🤔 |
|
For now, I think we can keep it simple, and I second adding tests to catch mismatched types. But feel free to open a small PR for Steep or Sorbet, then we can check what maintainers think about it btw, I see Claude is coauthoring these changes. I'm not sure if rubycritic's maintainers have an opinion on that |
4af3db0 to
8760b53
Compare
The `virtus` gem is unmaintained and drags in a chain of abandoned transitive dependencies: axiom-types, coercible, descendants_tracker, ice_nine, and the deprecated thread_safe (superseded by concurrent-ruby). RubyCritic only used a tiny slice of virtus -- an `attribute` DSL on two classes -- so carrying that whole tree was not justified. Introduce a small internal `RubyCritic::Attributes` module that reproduces only the used subset: an `attribute` class macro with reader/writer generation, type coercion (Float/Integer/Array/Symbol), per-instance duped mutable defaults, and a hash-accepting initializer. `Smell` and `AnalysedModule` now include it instead of `Virtus.model`. Drop the `virtus` runtime dependency and move `ostruct` to a development dependency (only referenced in tests). Update `.reek.yml` and the changelog accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> refactor: simplify Smell/AnalysedModule and add test-only type checking Replace the internal attributes module (from the previous commit) with plain `attr_accessor` plus a hash-accepting initializer, per PR feedback to go simpler. This keeps the gem free of virtus and its abandoned transitive dependencies without introducing any custom attribute DSL. Add a test-only "micro-Sorbet" helper (test/support/type_check.rb) that wraps the writers of these plain accessors and raises on unexpected types, recovering the lightweight type safety virtus used to provide. It is loaded only through test_helper, never required by production code, and never shipped (the gemspec packages lib only). A spec value may be a single class or a union, nil is always permitted, and late-bound class names (e.g. FakeFS::Pathname) are matched by name. Adjust .reek.yml/.rubocop.yml for the plain accessors (writable-attribute and initializer smells) and remove the now-stale entries that referenced the deleted attributes module. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8760b53 to
a642b76
Compare
|
I've updated this with plain accessors. For verifying the types, I chose a "micro-Sorbet" approach of a wrapper that wraps the accessors in the test environment and checks every write for the As far as I can tell there's no guidance towards not using an LLM for this repository, but I'm open to rewriting the implementation (it's pretty close to what I would have built, anyway) if there's any pushback :) |
|
That does suggest we should actually have guidelines ... #576 |
Why
virtusis unmaintained and pulls in a chain of abandoned gems (axiom-types,coercible,descendants_tracker,ice_nine) plus the deprecatedthread_safe(superseded byconcurrent-ruby). RubyCritic only used a tiny slice of virtus — anattributeDSL on two classes — so carrying that whole dependency tree wasn't justified.Direct accessor were chosen after the original PR and some deliberation. A custom module was originally over dry-rb deliberately:
dry-structwould just trade one transitive tree for another (dry-core,dry-types,dry-logic,zeitwerk...), which runs counter to the goal and to the codebase's recent direction of shedding/leaning out dependencies (e.g. preferringprismover theparsergem, dropping cucumber).What changed
SmellandAnalysedModuleto plain accessorsSmellandAnalysedModuleonly in testing to guarantee types. If the project is interested in adopting Sorbet or Steep this can (and should) be dropped.virtusruntime dependency removed;ostructmoved to a development dependency (only used in tests)..reek.ymlandCHANGELOG.mdupdated accordingly.Verification
rakesuite is green: tests, reek (0 warnings), and rubocop (no offenses).Float::INFINITY/Symbol defaults preserved, coercion applied on writers, and method overrides intact.Checklist
🤖 Generated with Claude Code