Module: Shirobai::Inject

Defined in:
lib/shirobai/inject.rb

Class Method Summary collapse

Class Method Details

.activate!Object

Badge replacement rides on Registry#clear_enrollment_queue being last-write-wins: whoever is enlisted later owns the badge. Up to RuboCop 1.88 every stock cop was loaded (and enlisted) at require "rubocop", so wrappers defined afterwards always won.

RuboCop 1.89 registers stock cops lazily: the registry maps each badge to a constant-name String and the class only loads when the constant is resolved (autoload). Loading enlists the stock class through Base.inherited, so a replaced cop whose stock file loads AFTER the wrapper — Registry#load_all_lazy_cops, a plugin gem referencing the constant (rubocop-rails prepends onto Style/RedundantSelf), or our own stock_counterpart — would steal the badge back on the next flush.

claim_badges! closes every such path at require time: resolve each replaced cop's stock constant first (consuming the autoload, so the stock class can never be enlisted again later), then re-enlist the wrapper so the queue flushes with the wrapper last. Unreplaced cops keep their lazy registration — shirobai must not force the whole registry to load.



24
25
26
27
# File 'lib/shirobai/inject.rb', line 24

def self.activate!
  claim_badges!
  align_autocorrect_incompatibilities!
end

.align_autocorrect_incompatibilities!(extra_cops = []) ⇒ Object

Team#each_corrector skips a cop's corrector for the round when an earlier-merged cop's autocorrect_incompatible_with includes the cop's CLASS. Badge replacement breaks that identity check in both directions: a wrapper copying stock's list still names the dismissed stock class, and a stock cop that stays stock (Style/SymbolProc) names a class whose registry slot a wrapper took over. Either way skips.include?(cop.class) misses, the skip never fires, and the -a round applies corrections stock would have dropped — the corrected trees then drift (fluentd types.rb / log.rb: SpaceInsideBlockBraces lists BlockDelimiters).

Rewrite every active cop's list through the stock-to-shirobai map: wrappers get stock's list (the drop-in truth) translated, and remaining stock cops get their own list translated. Runs after all core wrapper classes are defined (each auto-enlists via Base.inherited), again at each shirobai plugin gem's require (their stock departments — e.g. Rails/SafeNavigation listing Style::RedundantSelf — enlist between aligner runs), and per config over the enabled set (third-party plugins like rubocop-capybara load during config resolution, after every shirobai require; see align_for and Dispatch.bundle_token).

The rewritten methods return a FRESH copy per call, like stock's per-call array literals: rubocop-performance and rubocop-capybara prepend singleton modules that do super.push(...), so a shared (or frozen) array would accumulate duplicates across calls (or raise FrozenError).

The wrapper pass runs unconditionally (the stock counterparts are loaded by claim_badges!). The non-wrapper pass only covers extra_cops — enumerating the whole registry would force every lazy stock cop to load, so align_for passes the enabled set of the config actually being run, which is exactly the set Team#each_corrector can consult. Idempotent: an already-translated list maps to itself and is skipped.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/shirobai/inject.rb', line 85

def self.align_autocorrect_incompatibilities!(extra_cops = [])
  replacements = {}
  wrapper_cops.each do |cop|
    stock = stock_counterpart(cop)
    replacements[stock] = cop if stock
  end

  replacements.each do |stock, wrapper|
    list = stock.autocorrect_incompatible_with
                .map { |klass| replacements.fetch(klass, klass) }.freeze
    wrapper.define_singleton_method(:autocorrect_incompatible_with) { list.dup }
  end
  extra_cops.each do |cop|
    next if replacements.value?(cop)

    list = base_incompatible_list(cop)
    mapped = list.map { |klass| replacements.fetch(klass, klass) }
    next if mapped == list

    mapped.freeze
    cop.define_singleton_method(:autocorrect_incompatible_with) { mapped.dup }
  end
end

.align_for(config) ⇒ Object

Per-config re-alignment for cops that load AFTER every shirobai require: rubocop resolves plugins: / require: gems while loading the corpus config, and those gems (rubocop-capybara et al.) may list a replaced class or enlist new cops. Called from Dispatch.bundle_token on each new config, which happens after config resolution and before any correction round. Memoized per (config identity, registry badge count): a plugin load only ever ADDS badges, so a size change re-runs the alignment mid-run. Registry#length only counts badges — it never loads a lazy cop — and Registry#enabled loads exactly the cops the run's team is built from.



132
133
134
135
136
137
138
139
140
# File 'lib/shirobai/inject.rb', line 132

def self.align_for(config)
  registry = RuboCop::Cop::Registry.global
  size = registry.length
  @aligned ||= {}.compare_by_identity
  return if @aligned[config] == size

  @aligned[config] = size
  align_autocorrect_incompatibilities!(registry.enabled(config))
end

.base_incompatible_list(cop) ⇒ Object

The cop's own autocorrect_incompatible_with, bypassing modules prepended onto its singleton class (rubocop-performance's super.push(...) pattern). The rewritten method sits UNDER those prepends, so snapshotting the full call would bake their pushes into the base and the prepend would then add them again on every call.



114
115
116
117
118
119
120
# File 'lib/shirobai/inject.rb', line 114

def self.base_incompatible_list(cop)
  singleton = cop.singleton_class
  prepends = singleton.ancestors.take_while { |mod| !mod.equal?(singleton) }
  meth = cop.method(:autocorrect_incompatible_with)
  meth = meth.super_method while meth && prepends.include?(meth.owner)
  meth ? meth.call : []
end

.claim_badges!Object



29
30
31
32
33
34
35
36
# File 'lib/shirobai/inject.rb', line 29

def self.claim_badges!
  registry = RuboCop::Cop::Registry.global
  wrapper_cops.each do |wrapper|
    next unless stock_counterpart(wrapper) # consume the stock autoload

    registry.enlist(wrapper)
  end
end

.stock_counterpart(klass) ⇒ Object

The stock class whose badge klass took over, or nil when the stock constant does not exist (a shirobai-only cop).



144
145
146
147
148
149
# File 'lib/shirobai/inject.rb', line 144

def self.stock_counterpart(klass)
  department, name = klass.cop_name.split("/")
  RuboCop::Cop.const_get(department, false).const_get(name, false)
rescue NameError
  nil
end

.wrapper_copsObject

All wrapper classes defined so far, from the core departments and any loaded plugin gem's department (RSpec / Rails / Performance).



40
41
42
43
44
45
46
47
48
49
50
# File 'lib/shirobai/inject.rb', line 40

def self.wrapper_cops
  Shirobai::Cop.constants(false).flat_map do |department|
    mod = Shirobai::Cop.const_get(department)
    next [] unless mod.is_a?(Module) && !mod.is_a?(Class)

    mod.constants(false).filter_map do |cop_name|
      klass = mod.const_get(cop_name)
      klass if klass.is_a?(Class) && klass < RuboCop::Cop::Base
    end
  end
end