rigortype 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/array.yml +11 -24
  4. data/data/builtins/ruby_core/enumerable.yml +4 -8
  5. data/data/builtins/ruby_core/exception.yml +6 -9
  6. data/data/builtins/ruby_core/hash.yml +13 -20
  7. data/data/builtins/ruby_core/io.yml +6 -9
  8. data/data/builtins/ruby_core/numeric.yml +2 -3
  9. data/data/builtins/ruby_core/proc.yml +4 -8
  10. data/data/builtins/ruby_core/re.yml +6 -9
  11. data/data/builtins/ruby_core/set.yml +2 -3
  12. data/data/builtins/ruby_core/string.yml +4 -7
  13. data/data/builtins/ruby_core/struct.yml +0 -6
  14. data/data/builtins/ruby_core/time.yml +3 -6
  15. data/data/effects/core.yml +843 -0
  16. data/data/effects/registry.yml +67 -0
  17. data/docs/handbook/07-rbs-and-extended.md +106 -5
  18. data/docs/manual/02-cli-reference.md +311 -1
  19. data/docs/manual/03-configuration.md +84 -1
  20. data/docs/manual/04-diagnostics.md +14 -2
  21. data/docs/manual/16-rbs-extended-annotations.md +77 -0
  22. data/docs/manual/18-removing-dead-code.md +326 -0
  23. data/docs/manual/README.md +5 -0
  24. data/docs/manual/plugins/rigor-actionmailer.md +5 -0
  25. data/docs/manual/plugins/rigor-activejob.md +47 -0
  26. data/docs/manual/plugins/rigor-factorybot.md +16 -0
  27. data/docs/manual/plugins/rigor-pundit.md +26 -0
  28. data/docs/manual/plugins/rigor-rails-routes.md +41 -2
  29. data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
  30. data/docs/manual/plugins/rigor-rspec.md +8 -0
  31. data/docs/manual/plugins/rigor-sidekiq.md +54 -0
  32. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
  33. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
  34. data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
  35. data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
  36. data/lib/rigor/analysis/check_rules.rb +62 -22
  37. data/lib/rigor/analysis/incremental_session.rb +140 -6
  38. data/lib/rigor/analysis/reachability/graph.rb +244 -0
  39. data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
  40. data/lib/rigor/analysis/reachability/project_files.rb +58 -0
  41. data/lib/rigor/analysis/reachability/scan.rb +241 -0
  42. data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
  43. data/lib/rigor/analysis/rule_catalog.rb +163 -1
  44. data/lib/rigor/analysis/run_cache_key.rb +16 -0
  45. data/lib/rigor/analysis/runner/declaration_position.rb +54 -0
  46. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
  47. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
  48. data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
  49. data/lib/rigor/analysis/runner/pool_coordinator.rb +68 -10
  50. data/lib/rigor/analysis/runner.rb +420 -10
  51. data/lib/rigor/analysis/severity_stamp.rb +2 -1
  52. data/lib/rigor/analysis/worker_session.rb +58 -2
  53. data/lib/rigor/bleeding_edge.rb +22 -0
  54. data/lib/rigor/cache/incremental_snapshot.rb +24 -5
  55. data/lib/rigor/cli/check_command.rb +13 -1
  56. data/lib/rigor/cli/check_runner_factory.rb +4 -1
  57. data/lib/rigor/cli/effects_command.rb +131 -0
  58. data/lib/rigor/cli/effects_diff_renderer.rb +123 -0
  59. data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
  60. data/lib/rigor/cli/effects_renderer.rb +54 -0
  61. data/lib/rigor/cli/effects_report.rb +53 -0
  62. data/lib/rigor/cli/effects_snapshot_command.rb +256 -0
  63. data/lib/rigor/cli/unused_command.rb +288 -0
  64. data/lib/rigor/cli.rb +16 -0
  65. data/lib/rigor/configuration/severity_profile.rb +23 -0
  66. data/lib/rigor/configuration.rb +296 -14
  67. data/lib/rigor/effects/attribution.rb +76 -0
  68. data/lib/rigor/effects/catalog.rb +275 -0
  69. data/lib/rigor/effects/collector.rb +182 -0
  70. data/lib/rigor/effects/config_envelopes.rb +185 -0
  71. data/lib/rigor/effects/discharge.rb +69 -0
  72. data/lib/rigor/effects/effect_table.rb +92 -0
  73. data/lib/rigor/effects/entry_points.rb +93 -0
  74. data/lib/rigor/effects/envelope.rb +86 -0
  75. data/lib/rigor/effects/envelope_check.rb +172 -0
  76. data/lib/rigor/effects/envelope_index.rb +157 -0
  77. data/lib/rigor/effects/file_collection.rb +160 -0
  78. data/lib/rigor/effects/framework_units.rb +167 -0
  79. data/lib/rigor/effects/identity.rb +104 -0
  80. data/lib/rigor/effects/label.rb +77 -0
  81. data/lib/rigor/effects/label_intent.rb +73 -0
  82. data/lib/rigor/effects/label_set.rb +136 -0
  83. data/lib/rigor/effects/liskov_check.rb +167 -0
  84. data/lib/rigor/effects/local_ownership.rb +132 -0
  85. data/lib/rigor/effects/method_key.rb +40 -0
  86. data/lib/rigor/effects/mutation_classifier.rb +92 -0
  87. data/lib/rigor/effects/narrowing.rb +202 -0
  88. data/lib/rigor/effects/origin.rb +66 -0
  89. data/lib/rigor/effects/path_finder.rb +89 -0
  90. data/lib/rigor/effects/plugin_facts.rb +335 -0
  91. data/lib/rigor/effects/propagator.rb +275 -0
  92. data/lib/rigor/effects/registry.rb +193 -0
  93. data/lib/rigor/effects/scanner.rb +290 -0
  94. data/lib/rigor/effects/signature_sources.rb +74 -0
  95. data/lib/rigor/effects/snapshot.rb +380 -0
  96. data/lib/rigor/effects/snapshot_diff.rb +265 -0
  97. data/lib/rigor/effects/summary.rb +154 -0
  98. data/lib/rigor/effects/taint_cause.rb +38 -0
  99. data/lib/rigor/effects/unit_scan.rb +572 -0
  100. data/lib/rigor/effects/unknown_label_check.rb +86 -0
  101. data/lib/rigor/effects/unknown_label_report.rb +59 -0
  102. data/lib/rigor/effects.rb +47 -0
  103. data/lib/rigor/environment/rbs_loader.rb +34 -0
  104. data/lib/rigor/environment.rb +8 -1
  105. data/lib/rigor/flow_contribution/element.rb +1 -0
  106. data/lib/rigor/flow_contribution/merge_result.rb +5 -3
  107. data/lib/rigor/flow_contribution/merger.rb +16 -1
  108. data/lib/rigor/flow_contribution.rb +20 -4
  109. data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
  110. data/lib/rigor/inference/expression_typer.rb +74 -17
  111. data/lib/rigor/inference/fork_map.rb +45 -11
  112. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +147 -17
  113. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +97 -2
  114. data/lib/rigor/inference/method_dispatcher.rb +41 -10
  115. data/lib/rigor/inference/optimistic_origin.rb +68 -3
  116. data/lib/rigor/inference/pre_eval_constants.rb +189 -0
  117. data/lib/rigor/inference/scope_indexer.rb +134 -31
  118. data/lib/rigor/inference/singleton_object_constant.rb +63 -0
  119. data/lib/rigor/inference/statement_evaluator.rb +25 -12
  120. data/lib/rigor/plugin/base.rb +24 -0
  121. data/lib/rigor/plugin/effect_attribution.rb +208 -0
  122. data/lib/rigor/plugin/effect_edge.rb +101 -0
  123. data/lib/rigor/plugin/effect_entry_points.rb +51 -0
  124. data/lib/rigor/plugin/first_party.rb +57 -0
  125. data/lib/rigor/plugin/isolation.rb +4 -1
  126. data/lib/rigor/plugin/manifest.rb +103 -3
  127. data/lib/rigor/plugin/node_rule_walk.rb +6 -0
  128. data/lib/rigor/plugin/registry.rb +48 -0
  129. data/lib/rigor/plugin.rb +1 -0
  130. data/lib/rigor/protection/closure_kill_oracle.rb +20 -1
  131. data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
  132. data/lib/rigor/rbs_extended.rb +168 -0
  133. data/lib/rigor/reflection.rb +131 -30
  134. data/lib/rigor/scope.rb +62 -2
  135. data/lib/rigor/source/node_walker.rb +12 -0
  136. data/lib/rigor/type/anonymous_class_name.rb +40 -0
  137. data/lib/rigor/type/nominal.rb +5 -2
  138. data/lib/rigor/type/singleton.rb +4 -1
  139. data/lib/rigor/version.rb +1 -1
  140. data/lib/rigor.rb +1 -0
  141. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
  142. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
  143. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
  144. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
  145. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
  146. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
  147. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
  148. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
  149. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
  150. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
  151. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
  152. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
  153. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
  154. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
  155. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
  156. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
  157. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +385 -28
  158. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
  159. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
  160. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
  161. data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
  162. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
  163. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
  164. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
  165. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
  166. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
  167. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
  168. data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
  169. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
  170. data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
  171. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
  172. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +56 -3
  173. data/sig/rigor/analysis/fact_store.rbs +1 -0
  174. data/sig/rigor/inference.rbs +2 -0
  175. data/sig/rigor/rbs_extended.rbs +6 -0
  176. data/sig/rigor/scope.rbs +4 -0
  177. data/sig/rigor.rbs +21 -1
  178. data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
  179. metadata +83 -1
@@ -61,6 +61,11 @@ module Rigor
61
61
  # slice's cross-file reads, marshalled back into `#collected_dependencies` for the runner's graph.
62
62
  @record_dependencies = record_dependencies
63
63
  @collected_dependencies = {}
64
+ # ADR-103 WD13 — effect collection, derived from the configuration exactly as `Runner` and
65
+ # `WorkerSession` derive it. A worker's per-file collections marshal back into
66
+ # `#collected_effects`, so a pooled run's effect graph is the sequential run's graph.
67
+ @record_effects = configuration.effects_enabled?
68
+ @collected_effects = {}
64
69
  @collect_stats = collect_stats
65
70
  @buffer = buffer
66
71
  @environment_override = environment_override
@@ -81,6 +86,11 @@ module Rigor
81
86
  # so the incremental session's dependency graph is refreshed whether analysis ran sequential or pooled.
82
87
  attr_reader :collected_dependencies
83
88
 
89
+ # ADR-103 WD12 — the per-file {Effects::FileCollection}s the fork pool captured this run (empty
90
+ # unless the configuration carries an `effects:` block and a pool run occurred). The runner merges
91
+ # these with its own sequential records before the post-pool fixpoint.
92
+ attr_reader :collected_effects
93
+
84
94
  # ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`. Editor mode (`buffer:` non-nil)
85
95
  # silently overrides pool mode to sequential: per design § "Ractor pool mode", the pool's warm-up
86
96
  # cost dominates one-file wall time, so the pool gains nothing on a per-buffer invocation. The
@@ -179,8 +189,12 @@ module Rigor
179
189
  # per-worker dependency records back, and the Ractor path additionally lacks the cross-file
180
190
  # `project_scope_seed` its worker would need to record complete edges. Without `fork` (Windows) a
181
191
  # recording run degrades to sequential, which records correctly through `@analyze_file`.
192
+ #
193
+ # An effects run (ADR-103 WD13) is pinned for exactly the same reason — the Ractor messages carry
194
+ # no side-table channel — and degrades the same way. The degrade is sound rather than merely safe:
195
+ # the sequential fallback still collects, so the effect graph is complete either way.
182
196
  def dispatch_pool(files)
183
- if @record_dependencies
197
+ if @record_dependencies || @record_effects
184
198
  return analyze_files_in_fork_pool(files) if Process.respond_to?(:fork)
185
199
 
186
200
  return analyze_files_sequentially_fallback(
@@ -250,6 +264,14 @@ module Rigor
250
264
  # replayed into the runner-side accumulators via their `record_*` APIs, which dedupe on the same
251
265
  # keys as a single-session run would. Net result: reporter state is identical to the sequential
252
266
  # path.
267
+ #
268
+ # **This backend cannot analyse a file under rbs 4.x, and the blocker is upstream** (#414).
269
+ # `RBS::Namespace.[]` interns every namespace through a process-wide flyweight cache held in
270
+ # module ivars (`@intern_trie_absolute` / `@intern_trie_relative`, mutable by design), and reading
271
+ # an unshareable module ivar from a non-main Ractor is `Ractor::IsolationError` — so every file a
272
+ # worker touches comes back as an internal analyzer error. What the code below fixes is the
273
+ # failure MODE, not the backend: the run used to hang forever instead of saying anything. Reviving
274
+ # the backend needs an upstream change, which is why `pool_backend` keeps `fork` as the default.
253
275
  def analyze_files_in_pool(files) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
254
276
  # Pre-warm class-level lazy memos on the MAIN Ractor. `Environment::ClassRegistry.default` is the
255
277
  # default kwarg threaded through `Environment.new` inside each worker session; lazy-initialising
@@ -303,15 +325,30 @@ module Rigor
303
325
  end
304
326
  end
305
327
 
328
+ # #414 — every worker is monitored BEFORE any file is dispatched, so a worker that dies at
329
+ # construction is still observed. `Ractor#monitor` posts `:exited` / `:aborted` to this
330
+ # Ractor's default port, which is the same mailbox the workers push their results to.
331
+ pool.each { |worker| worker.monitor(Ractor.current.default_port) }
332
+
306
333
  files.each_with_index { |path, index| pool[index % pool.size].send(path) }
307
334
  pool.each { |worker| worker.send(nil) }
308
335
 
309
336
  prepare_diagnostics = nil
310
337
  results_by_path = {}
311
- done_count = 0
312
-
313
- while done_count < pool.size
338
+ terminated = 0
339
+
340
+ # Bounded by TERMINATION, not by `:done`. A worker that dies never says `:done`, and the loop
341
+ # that counted `:done` blocked on `Ractor.receive` for as long as the process lived — the whole
342
+ # backend hung that way once `Environment.for_project` started raising `Ractor::IsolationError`
343
+ # inside `WorkerSession#initialize` (#414). Monitor notices are bare Symbols; worker messages
344
+ # are Arrays, so the two never collide.
345
+ while terminated < pool.size
314
346
  message = Ractor.receive
347
+ if message.is_a?(Symbol)
348
+ terminated += 1
349
+ next
350
+ end
351
+
315
352
  case message.first
316
353
  when :prepare
317
354
  prepare_diagnostics ||= message.last
@@ -319,13 +356,27 @@ module Rigor
319
356
  results_by_path[message[1]] = message[2]
320
357
  when :done
321
358
  merge_worker_reporters(message.last)
322
- done_count += 1
323
359
  end
324
360
  end
325
361
 
326
- pool.each(&:join)
362
+ # `join` re-raises an abnormal worker's exception as `Ractor::RemoteError`; the degrade below is
363
+ # this path's answer to that, so the raise carries no information the coordinator acts on.
364
+ pool.each do |worker|
365
+ worker.join
366
+ rescue Ractor::Error
367
+ nil
368
+ end
369
+
370
+ # The files no worker reported — every file of a worker that died, and any file in flight when
371
+ # it did. Re-analysed in process, exactly as the fork backend re-analyses a dead child's slice.
372
+ degraded = files.reject { |path| results_by_path.key?(path) }
373
+ unless degraded.empty?
374
+ environment = build_runner_environment(source_files: files)
375
+ degraded.each { |path| results_by_path[path] = @analyze_file.call(path, environment) }
376
+ end
327
377
 
328
- Array(prepare_diagnostics) + files.flat_map { |path| results_by_path.fetch(path, []) }
378
+ diagnostics = Array(prepare_diagnostics) + files.flat_map { |path| results_by_path.fetch(path, []) }
379
+ degraded.empty? ? diagnostics : diagnostics.unshift(pool_degraded_diagnostic(degraded.size, "ractor"))
329
380
  end
330
381
 
331
382
  # ADR-15 Amendment (2026-05-20) — fork-based worker pool, the active backend for `workers > 0`.
@@ -378,11 +429,12 @@ module Rigor
378
429
  # The degraded slice was re-analysed on the parent session, so its dependency records live there,
379
430
  # not in a child payload — fold them in alongside the successful children's.
380
431
  @collected_dependencies.merge!(session.drain_dependencies) if @record_dependencies
432
+ @collected_effects.merge!(session.drain_effects) if @record_effects
381
433
  end
382
434
 
383
435
  diagnostics = Array(session.prepare_diagnostics) +
384
436
  files.flat_map { |path| results_by_path.fetch(path, []) }
385
- degraded.empty? ? diagnostics : diagnostics.unshift(fork_degraded_diagnostic(degraded.size))
437
+ degraded.empty? ? diagnostics : diagnostics.unshift(pool_degraded_diagnostic(degraded.size, "fork"))
386
438
  end
387
439
 
388
440
  # Child-process body for {#analyze_files_in_fork_pool}. Analyses the slice with the
@@ -399,6 +451,9 @@ module Rigor
399
451
  results = slice.to_h { |path| [path, session.analyze(path)] }
400
452
  payload = { results: results, reporters: session.drain_reporters,
401
453
  dependencies: session.drain_dependencies }
454
+ # ADR-103 WD13 — the effects slot exists only on a collecting run, so a normal `--workers N` run
455
+ # marshals exactly the payload it marshalled before the feature existed.
456
+ payload[:effects] = session.drain_effects if @record_effects
402
457
  File.binwrite(out_path, Marshal.dump(payload))
403
458
  exit!(0)
404
459
  rescue StandardError
@@ -430,6 +485,7 @@ module Rigor
430
485
  results_by_path.merge!(payload.fetch(:results))
431
486
  merge_worker_reporters(payload.fetch(:reporters))
432
487
  @collected_dependencies.merge!(payload.fetch(:dependencies, {})) if @record_dependencies
488
+ @collected_effects.merge!(payload.fetch(:effects, {})) if @record_effects
433
489
  else
434
490
  degraded.concat(child[:slice])
435
491
  end
@@ -448,10 +504,12 @@ module Rigor
448
504
  nil
449
505
  end
450
506
 
451
- def fork_degraded_diagnostic(count)
507
+ # Both pool backends degrade the same way — the files a dead worker owed are re-analysed in
508
+ # process — so they say so the same way, naming which backend it was (#414).
509
+ def pool_degraded_diagnostic(count, backend)
452
510
  Diagnostic.new(
453
511
  path: ".rigor.yml", line: 1, column: 1,
454
- message: "fork pool degraded: #{count} file(s) re-analysed in-process " \
512
+ message: "#{backend} pool degraded: #{count} file(s) re-analysed in-process " \
455
513
  "after a worker exited abnormally",
456
514
  severity: :warning, rule: "pool-degraded", source_family: :builtin
457
515
  )