Replace virtus with an internal attributes module#574
Conversation
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>
|
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 🤔 |
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.A custom module was chosen 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
RubyCritic::Attributesmodule reproducing only the used virtus subset: anattributemacro, type coercion, per-instance duped defaults, and a hash-accepting initializer.SmellandAnalysedModulenowinclude RubyCritic::Attributesinstead ofVirtus.model.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