Module: Shirobai::Dispatch

Defined in:
lib/shirobai/dispatch.rb

Overview

Per-file coordinator: computes every cop's offenses in ONE bundled ext call (Shirobai.check_all), memoized by source. The first participating cop on a file triggers the bundled run; the rest read their slice from the cache.

The cache key is the raw_source identity (equal?, not ==), so the autocorrect loop (which re-investigates a freshly built ProcessedSource) naturally recomputes, and a different file never reuses stale results.

A cop whose per-investigation config/state cannot be represented in the bundle (see each wrapper's bundle_eligible?) skips this coordinator and calls its standalone entry point directly instead.

Constant Summary collapse

ORIGINS =

Origin order of the Shirobai.check_all result Array and of the packed config segments: outer index 0 is the core batch, 1 the shirobai-performance plugin, 2 the shirobai-rspec plugin, 3 the shirobai-rails plugin, and so on. Mirrors the origin constants in crates/shirobai-core/src/rules/bundle.rs (ORIGIN_* / N_ORIGINS); adding a plugin means one entry here and one constant there — that pair of one-line edits is the only place plugin batches can conflict.

%i[core performance rspec rails].freeze
SLOTS =

[origin, rule] slot pairs into the Shirobai.check_all result (result[origin][rule]). The rule order within each origin mirrors the order documented on BundleConfig and built by check_all in ext/shirobai/src/lib.rs; each slot carries the same shape as the cop's standalone Shirobai.check_* return value. Wrappers never index the result themselves — they go through offenses_for.

{
  debugger: [0, 0].freeze,
  block_length: [0, 1].freeze,
  block_nesting: [0, 2].freeze,
  complexity: [0, 3].freeze,
  variable_number: [0, 4].freeze,
  method_name: [0, 5].freeze,
  safe_navigation_chain: [0, 6].freeze,
  multiline_operation: [0, 7].freeze,
  multiline_method_call: [0, 8].freeze,
  dot_position: [0, 9].freeze,
  line_length: [0, 10].freeze,
  line_length_breakables: [0, 11].freeze,
  line_end_concatenation: [0, 12].freeze,
  argument_alignment: [0, 13].freeze,
  first_argument_indentation: [0, 14].freeze,
  redundant_self: [0, 15].freeze,
  indentation_width: [0, 16].freeze,
  predicate_prefix: [0, 17].freeze,
  closing_parenthesis_indentation: [0, 18].freeze,
  first_array_element_indentation: [0, 19].freeze,
  hash_each_methods: [0, 20].freeze,
  void: [0, 21].freeze,
  useless_access_modifier: [0, 22].freeze,
  empty_lines_around_method_body: [0, 23].freeze,
  empty_lines_around_class_body: [0, 24].freeze,
  empty_lines_around_module_body: [0, 25].freeze,
  empty_lines_around_block_body: [0, 26].freeze,
  empty_lines_around_begin_body: [0, 27].freeze,
  empty_lines_around_exception_handling_keywords: [0, 28].freeze,
  block_delimiters: [0, 29].freeze,
  abc_size: [0, 30].freeze,
  indentation_consistency: [0, 31].freeze,
  empty_line_between_defs: [0, 32].freeze,
  end_alignment: [0, 33].freeze,
  block_alignment: [0, 34].freeze,
  else_alignment: [0, 35].freeze,
  first_hash_element_indentation: [0, 36].freeze,
  hash_alignment: [0, 37].freeze,
  empty_lines_around_arguments: [0, 38].freeze,
  hash_syntax: [0, 39].freeze,
  string_literals: [0, 40].freeze,
  trailing_comma_in_arguments: [0, 41].freeze,
  string_literals_in_interpolation: [0, 42].freeze,
  trailing_empty_lines: [0, 43].freeze,
  space_around_method_call_operator: [0, 44].freeze,
  space_around_keyword: [0, 45].freeze,
  space_inside_block_braces: [0, 46].freeze,
  method_length: [0, 47].freeze,
  def_end_alignment: [0, 48].freeze,
  require_parentheses: [0, 49].freeze,
  self_assignment: [0, 50].freeze,
  nested_parenthesized_calls: [0, 51].freeze,
  parentheses_as_grouped_expression: [0, 52].freeze,
  percent_literal_delimiters: [0, 53].freeze,
  multiline_method_call_brace_layout: [0, 54].freeze,
  access_modifier_indentation: [0, 55].freeze,
  assignment_indentation: [0, 56].freeze,
  redundant_self_assignment: [0, 57].freeze,
  colon_method_call: [0, 58].freeze,
  stabby_lambda_parentheses: [0, 59].freeze,
  unreachable_code: [0, 60].freeze,
  hash_transform_keys: [0, 61].freeze,
  ambiguous_block_association: [0, 62].freeze,
  empty_line_after_guard_clause: [0, 63].freeze,
  empty_comment: [0, 64].freeze,
  empty_line_after_magic_comment: [0, 65].freeze,
  empty_lines: [0, 66].freeze,
  leading_empty_lines: [0, 67].freeze,
  class_length: [0, 68].freeze,
  module_length: [0, 69].freeze,
  trailing_comma_in_hash_literal: [0, 70].freeze,
  trailing_comma_in_array_literal: [0, 71].freeze,
  space_inside_hash_literal_braces: [0, 72].freeze,
  space_inside_array_literal_brackets: [0, 73].freeze,
  space_before_block_braces: [0, 74].freeze,
  if_unless_modifier: [0, 75].freeze,
  space_before_comma: [0, 76].freeze,
  space_after_comma: [0, 77].freeze,
  space_before_semicolon: [0, 78].freeze,
  space_after_semicolon: [0, 79].freeze,
  space_after_colon: [0, 80].freeze,
  space_before_comment: [0, 81].freeze,
  space_inside_parens: [0, 82].freeze,
  space_inside_reference_brackets: [0, 83].freeze,
  space_before_first_arg: [0, 84].freeze,
  duplicate_magic_comment: [0, 85].freeze,
  duplicate_methods: [0, 86].freeze,
  array_alignment: [0, 87].freeze,
  file_null: [0, 88].freeze,
  semicolon: [0, 89].freeze,
  redundant_freeze: [0, 90].freeze,
  frozen_string_literal_comment: [0, 91].freeze,
  arguments_forwarding: [0, 92].freeze,
  # shirobai-performance plugin slots (origin 1). Always present in the
  # wire format; the Rust side leaves them empty unless the plugin gem
  # registered its packed segment (`Dispatch.register_plugin_packer`).
  perf_detect: [1, 0].freeze,
  perf_string_include: [1, 1].freeze,
  perf_end_with: [1, 2].freeze,
  perf_start_with: [1, 3].freeze,
  perf_times_map: [1, 4].freeze,
  # shirobai-rspec plugin slots (origin 2), all filled by the single
  # RSpecDispatcherRule. The rspec origin is additionally gated per
  # file (see `register_plugin_packer`'s `gate`): the RSpec department
  # only runs on spec files, so other files use a dormant-rspec token.
  rspec_variable_name: [2, 0].freeze,
  rspec_let_setup: [2, 1].freeze,
  rspec_variable_definition: [2, 2].freeze,
  rspec_multiple_memoized_helpers: [2, 3].freeze,
  rspec_repeated_description: [2, 4].freeze,
  rspec_repeated_example: [2, 5].freeze,
  rspec_named_subject: [2, 6].freeze,
  # R2 metadata family (rspec origin slots 7-12). Slots 9-12 (the four
  # Metadata-mixin cops) read the same shared metadata-anchor list.
  rspec_focus: [2, 7].freeze,
  rspec_pending_without_reason: [2, 8].freeze,
  rspec_metadata_style: [2, 9].freeze,
  rspec_duplicated_metadata: [2, 10].freeze,
  rspec_empty_metadata: [2, 11].freeze,
  rspec_sort_metadata: [2, 12].freeze,
  # R2 empty-line family (rspec origin slots 13-17), all filled by the
  # single RSpecEmptyLineRule.
  rspec_empty_line_after_example: [2, 13].freeze,
  rspec_empty_line_after_example_group: [2, 14].freeze,
  rspec_empty_line_after_final_let: [2, 15].freeze,
  rspec_empty_line_after_hook: [2, 16].freeze,
  rspec_empty_line_after_subject: [2, 17].freeze,
  rspec_empty_example_group: [2, 18].freeze,
  rspec_described_class: [2, 19].freeze,
  rspec_scattered_setup: [2, 20].freeze,
  # shirobai-rails plugin slots (origin 3), all filled by the single
  # RailsAppVisitor. The rails origin is NOT per-file gated: the
  # Application* cops run on every Ruby file, so it is always awake once
  # the plugin gem is loaded.
  rails_application_record: [3, 0].freeze,
  rails_application_controller: [3, 1].freeze,
  rails_application_mailer: [3, 2].freeze,
  rails_application_job: [3, 3].freeze,
  # send/block-table cluster (rails origin slots 4-5), each its own rule.
  rails_unknown_env: [3, 4].freeze,
  rails_dynamic_find_by: [3, 5].freeze,
  # Rails Architecture-B cops (origin 3 slots 6-7): the Rust side emits
  # candidate send ranges here, and the wrapper relocates the parser node
  # and runs stock detection + autocorrect verbatim (same shape as the
  # rspec send-candidate cops). The 3-point positional sync (this map,
  # the `BundleResult` field order in `crates/.../bundle.rs`, the ext
  # `check_all` push order) moves together.
  rails_http_positional_arguments: [3, 6].freeze,
  rails_deprecated_active_model_errors_methods: [3, 7].freeze,
  rails_pluck: [3, 8].freeze
}.freeze
DORMANT_SEGMENTS =

Dormant packed-config segment per plugin origin: the enable flag (first num) is 0 and the cop settings are placeholders (see the BundleConfig docs for each segment's field order). Packed whenever the plugin gem has not registered a packer — or, for a gated origin, whenever the origin's gate says the current file is not relevant; the Rust side then skips that origin's rules and leaves its slots empty.

{
  performance: [[0, 0, 0].freeze, [[].freeze].freeze].freeze,
  # rspec: enable flag + per-cop nums (VariableName style,
  # VariableDefinition style, MMH Max, MMH AllowSubject, NamedSubject
  # style, NamedSubject IgnoreSharedExamples, EmptyLineAfterExample
  # AllowConsecutiveOneLiners, EmptyLineAfterHook
  # AllowConsecutiveOneLiners), then the sixteen
  # RSpec/Language role lists.
  rspec: [[0, 0, 0, 0, 0, 0, 0, 0, 0].freeze, ([[].freeze] * 16).freeze].freeze,
  # rails: wake-up flag + UnknownEnv supports_local; four lists
  # (UnknownEnv Environments + DynamicFindBy AllowedMethods /
  # AllowedReceivers / Whitelist).
  rails: [[0, 0].freeze, [[].freeze, [].freeze, [].freeze, [].freeze].freeze].freeze
}.freeze

Class Method Summary collapse

Class Method Details

.bundle_token(config, inactive = EMPTY_INACTIVE) ⇒ Object

The Rust-side token for config with the given gated-off origins, registering its packed bundle config on first sight. Memoized per (config object identity, inactive origin set): a lint run shares one Config object across all cops in the team, so a run registers O(#distinct configs x #distinct gate outcomes) entries (each spec example registers one; entries are small and never evicted).



264
265
266
267
268
269
# File 'lib/shirobai/dispatch.rb', line 264

def bundle_token(config, inactive = EMPTY_INACTIVE)
  @bundle_tokens ||= {}.compare_by_identity
  per_config = (@bundle_tokens[config] ||= {})
  per_config[inactive] ||=
    Shirobai.register_bundle_config(*packed_config(config, inactive))
end

.offenses_for(processed_source, config, cop_key) ⇒ Object

Returns the raw Rust result for cop_key on this source, or nil when cop_key belongs to a gated origin that is inactive for this file (the safety net: the wrapper then takes its standalone entry point, so a gate/relevant_file? disagreement can only cost speed, never offenses).



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/shirobai/dispatch.rb', line 242

def offenses_for(processed_source, config, cop_key)
  src = processed_source.raw_source
  unless defined?(@cached_source) && @cached_source.equal?(src) && @cached_config.equal?(config)
    inactive = inactive_origins(config, processed_source)
    result = Shirobai.check_all(src, bundle_token(config, inactive))
    @cached_source = src
    @cached_config = config
    @cached_result = result
    @cached_inactive = inactive
  end
  origin, rule = SLOTS.fetch(cop_key)
  return nil if @cached_inactive.include?(ORIGINS.fetch(origin))

  @cached_result.fetch(origin).fetch(rule)
end

.plugin_gatesObject



233
234
235
# File 'lib/shirobai/dispatch.rb', line 233

def plugin_gates
  @plugin_gates ||= {}
end

.plugin_packersObject



229
230
231
# File 'lib/shirobai/dispatch.rb', line 229

def plugin_packers
  @plugin_packers ||= {}
end

.register_plugin_packer(origin, gate: nil, &packer) ⇒ Object

Registration point for plugin gems: origin is the ORIGINS key and the block is a callable (config) -> [nums, lists] producing that origin's segment documented on BundleConfig (enable flag first). Called at plugin require time, BEFORE any lint run packs a config. Configs packed earlier keep their dormant tokens (token memoization is per Config identity), so requiring a plugin mid-run is not supported.

gate (optional) makes the origin per-file: a callable (config, processed_source) -> bool answering "does any of this origin's cops run on this file?". Department-scoped plugins (rubocop-rspec's Include: **/*_spec.rb) pass one so non-relevant files use a token whose segment is dormant and never pay for the origin's rules. The gate must be a superset of the wrapper cops' relevant_file? — a wrapper that runs on a gated-off file falls back to its standalone entry point (offenses_for returns nil).

Raises:

  • (ArgumentError)


222
223
224
225
226
227
# File 'lib/shirobai/dispatch.rb', line 222

def register_plugin_packer(origin, gate: nil, &packer)
  raise ArgumentError, "unknown origin #{origin.inspect}" unless ORIGINS.include?(origin)

  plugin_packers[origin] = packer
  plugin_gates[origin] = gate if gate
end