rigortype 0.1.19 → 0.2.1

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 (197) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -6
  3. data/data/core_overlay/numeric.rbs +33 -0
  4. data/data/core_overlay/pathname.rbs +25 -0
  5. data/data/core_overlay/string_scanner.rbs +28 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
  7. data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
  8. data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
  9. data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
  10. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
  11. data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
  12. data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
  13. data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
  14. data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
  15. data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
  16. data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
  17. data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
  18. data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
  19. data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
  20. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
  21. data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
  22. data/data/vendored_gem_sigs/redis/future.rbs +5 -0
  23. data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
  24. data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
  25. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
  26. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
  27. data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
  28. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  29. data/lib/rigor/analysis/check_rules.rb +492 -71
  30. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  31. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  32. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  33. data/lib/rigor/analysis/fact_store.rb +5 -4
  34. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
  36. data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
  37. data/lib/rigor/analysis/runner.rb +17 -6
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  39. data/lib/rigor/analysis/worker_session.rb +10 -14
  40. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  41. data/lib/rigor/cache/store.rb +5 -3
  42. data/lib/rigor/cli/annotate_command.rb +28 -7
  43. data/lib/rigor/cli/baseline_command.rb +4 -3
  44. data/lib/rigor/cli/check_command.rb +138 -16
  45. data/lib/rigor/cli/coverage_command.rb +138 -31
  46. data/lib/rigor/cli/coverage_mutation.rb +149 -0
  47. data/lib/rigor/cli/coverage_scan.rb +57 -0
  48. data/lib/rigor/cli/explain_command.rb +2 -0
  49. data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
  50. data/lib/rigor/cli/fused_protection_report.rb +76 -0
  51. data/lib/rigor/cli/lsp_command.rb +3 -7
  52. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  53. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  54. data/lib/rigor/cli/options.rb +9 -0
  55. data/lib/rigor/cli/plugins_command.rb +2 -1
  56. data/lib/rigor/cli/protection_renderer.rb +63 -0
  57. data/lib/rigor/cli/protection_report.rb +68 -0
  58. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  59. data/lib/rigor/cli/trace_command.rb +2 -1
  60. data/lib/rigor/cli/triage_command.rb +2 -1
  61. data/lib/rigor/cli/type_of_command.rb +1 -1
  62. data/lib/rigor/cli/type_scan_command.rb +2 -1
  63. data/lib/rigor/cli.rb +3 -2
  64. data/lib/rigor/config_audit.rb +152 -0
  65. data/lib/rigor/configuration/dependencies.rb +2 -4
  66. data/lib/rigor/configuration.rb +57 -7
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  68. data/lib/rigor/environment/class_registry.rb +4 -3
  69. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  70. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  71. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  72. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  73. data/lib/rigor/environment/rbs_loader.rb +76 -5
  74. data/lib/rigor/environment.rb +66 -8
  75. data/lib/rigor/flow_contribution/fact.rb +1 -1
  76. data/lib/rigor/flow_contribution.rb +3 -5
  77. data/lib/rigor/inference/acceptance.rb +17 -9
  78. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  79. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  80. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  81. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  82. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  83. data/lib/rigor/inference/expression_typer.rb +20 -28
  84. data/lib/rigor/inference/hkt_body.rb +8 -11
  85. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  86. data/lib/rigor/inference/hkt_registry.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  88. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +169 -24
  89. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  90. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  91. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  92. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  93. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  94. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  95. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  96. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
  97. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  98. data/lib/rigor/inference/method_dispatcher.rb +40 -48
  99. data/lib/rigor/inference/mutation_widening.rb +5 -11
  100. data/lib/rigor/inference/narrowing.rb +14 -16
  101. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  102. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  103. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  104. data/lib/rigor/inference/protection_scanner.rb +86 -0
  105. data/lib/rigor/inference/scope_indexer.rb +129 -55
  106. data/lib/rigor/inference/statement_evaluator.rb +271 -114
  107. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  108. data/lib/rigor/inference/synthetic_method.rb +7 -7
  109. data/lib/rigor/language_server/completion_provider.rb +6 -12
  110. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  111. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  112. data/lib/rigor/language_server/hover_provider.rb +2 -3
  113. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  114. data/lib/rigor/language_server/server.rb +9 -17
  115. data/lib/rigor/language_server.rb +4 -5
  116. data/lib/rigor/plugin/base.rb +10 -8
  117. data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
  118. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  119. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  120. data/lib/rigor/plugin/macro.rb +4 -5
  121. data/lib/rigor/plugin/manifest.rb +45 -66
  122. data/lib/rigor/plugin/registry.rb +6 -7
  123. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  124. data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
  125. data/lib/rigor/protection/mutation_scanner.rb +180 -0
  126. data/lib/rigor/protection/mutator.rb +267 -0
  127. data/lib/rigor/protection/test_suite_oracle.rb +68 -0
  128. data/lib/rigor/rbs_extended.rb +24 -36
  129. data/lib/rigor/reflection.rb +4 -7
  130. data/lib/rigor/scope/discovery_index.rb +14 -2
  131. data/lib/rigor/scope.rb +54 -11
  132. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  133. data/lib/rigor/sig_gen/writer.rb +40 -2
  134. data/lib/rigor/signature_path_audit.rb +92 -0
  135. data/lib/rigor/source/constant_path.rb +62 -0
  136. data/lib/rigor/source.rb +1 -0
  137. data/lib/rigor/type/bound_method.rb +2 -11
  138. data/lib/rigor/type/combinator.rb +16 -3
  139. data/lib/rigor/type/constant.rb +2 -11
  140. data/lib/rigor/type/data_class.rb +2 -11
  141. data/lib/rigor/type/data_instance.rb +2 -11
  142. data/lib/rigor/type/hash_shape.rb +2 -11
  143. data/lib/rigor/type/integer_range.rb +2 -11
  144. data/lib/rigor/type/intersection.rb +2 -11
  145. data/lib/rigor/type/nominal.rb +2 -11
  146. data/lib/rigor/type/plain_lattice.rb +37 -0
  147. data/lib/rigor/type/refined.rb +72 -13
  148. data/lib/rigor/type/singleton.rb +2 -11
  149. data/lib/rigor/type/struct_class.rb +75 -0
  150. data/lib/rigor/type/struct_instance.rb +93 -0
  151. data/lib/rigor/type/tuple.rb +5 -15
  152. data/lib/rigor/type.rb +2 -0
  153. data/lib/rigor/version.rb +1 -1
  154. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  155. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  156. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
  157. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  158. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  159. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
  160. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  161. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  162. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
  163. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  164. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
  165. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  166. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  167. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  168. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  171. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  172. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
  173. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  174. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  175. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
  176. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  179. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  182. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  184. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  185. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  186. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  189. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  190. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  191. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
  195. data/sig/rigor/scope.rbs +9 -1
  196. data/sig/rigor/type.rbs +36 -1
  197. metadata +49 -1
@@ -32,9 +32,9 @@ module Rigor
32
32
  # its `#prepare(services)` hook. `consumes:` lists the
33
33
  # `(plugin_id, name)` pairs this plugin reads from
34
34
  # `services.fact_store`. The loader uses both for
35
- # topological sort + missing-producer detection (slice 5);
36
- # slice 4 carries the declarations on the manifest but the
37
- # loader does not yet enforce them.
35
+ # topological sort + missing-producer detection; slice 4
36
+ # added the declarations; slice 5 (`Loader#topo_sort_plugins`)
37
+ # enforces ordering and missing-producer validation.
38
38
  class Consumption < Data.define(:plugin_id, :name, :optional)
39
39
  def initialize(plugin_id:, name:, optional: false)
40
40
  super(plugin_id: plugin_id.to_s, name: name.to_sym, optional: optional ? true : false)
@@ -277,10 +277,18 @@ module Rigor
277
277
  end
278
278
  end
279
279
 
280
- def validate_produces!(produces)
281
- return if produces.is_a?(Array) && produces.all? { |p| p.is_a?(Symbol) || p.is_a?(String) }
280
+ # Shared shape check for the Array-of-X manifest fields. Every entry
281
+ # must satisfy the block; otherwise raise the uniform "must be an
282
+ # Array of <label>" message. Centralises the message format the
283
+ # field validators below share so it cannot drift between them.
284
+ def validate_array_of!(field, value, label, &)
285
+ return if value.is_a?(Array) && value.all?(&)
282
286
 
283
- raise ArgumentError, "plugin manifest produces must be an Array of Symbol/String, got #{produces.inspect}"
287
+ raise ArgumentError, "plugin manifest #{field} must be an Array of #{label}, got #{value.inspect}"
288
+ end
289
+
290
+ def validate_produces!(produces)
291
+ validate_array_of!("produces", produces, "Symbol/String") { |p| p.is_a?(Symbol) || p.is_a?(String) }
284
292
  end
285
293
 
286
294
  # ADR-10 5a — `owns_receivers:` declares the class names
@@ -291,11 +299,7 @@ module Rigor
291
299
  # so plugin contributions stay authoritative for those
292
300
  # types.
293
301
  def validate_owns_receivers!(owns_receivers)
294
- return if owns_receivers.is_a?(Array) && owns_receivers.all? { |c| c.is_a?(String) && !c.empty? }
295
-
296
- raise ArgumentError,
297
- "plugin manifest owns_receivers must be an Array of non-empty String, " \
298
- "got #{owns_receivers.inspect}"
302
+ validate_array_of!("owns_receivers", owns_receivers, "non-empty String") { |c| c.is_a?(String) && !c.empty? }
299
303
  end
300
304
 
301
305
  # ADR-26 — `open_receivers:` declares the class names this
@@ -309,11 +313,7 @@ module Rigor
309
313
  # `owns_receivers:` (which routes dispatch); this one only
310
314
  # suppresses the diagnostic.
311
315
  def validate_open_receivers!(open_receivers)
312
- return if open_receivers.is_a?(Array) && open_receivers.all? { |c| c.is_a?(String) && !c.empty? }
313
-
314
- raise ArgumentError,
315
- "plugin manifest open_receivers must be an Array of non-empty String, " \
316
- "got #{open_receivers.inspect}"
316
+ validate_array_of!("open_receivers", open_receivers, "non-empty String") { |c| c.is_a?(String) && !c.empty? }
317
317
  end
318
318
 
319
319
  # ADR-13 slice 2 — `type_node_resolvers:` declares the
@@ -325,11 +325,9 @@ module Rigor
325
325
  # integration that actually drives the chain lands in
326
326
  # slice 3.
327
327
  def validate_type_node_resolvers!(resolvers)
328
- return if resolvers.is_a?(Array) && resolvers.all?(TypeNodeResolver)
329
-
330
- raise ArgumentError,
331
- "plugin manifest type_node_resolvers must be an Array of " \
332
- "Rigor::Plugin::TypeNodeResolver instances, got #{resolvers.inspect}"
328
+ validate_array_of!("type_node_resolvers", resolvers, "Rigor::Plugin::TypeNodeResolver instances") do |r|
329
+ r.is_a?(TypeNodeResolver)
330
+ end
333
331
  end
334
332
 
335
333
  # ADR-16 slice 1a — `block_as_methods:` declares the Tier A
@@ -338,11 +336,9 @@ module Rigor
338
336
  # actually narrows `Scope#self_type` for matching blocks
339
337
  # arrives in a subsequent slice.
340
338
  def validate_block_as_methods!(entries)
341
- return if entries.is_a?(Array) && entries.all?(Macro::BlockAsMethod)
342
-
343
- raise ArgumentError,
344
- "plugin manifest block_as_methods must be an Array of " \
345
- "Rigor::Plugin::Macro::BlockAsMethod instances, got #{entries.inspect}"
339
+ validate_array_of!("block_as_methods", entries, "Rigor::Plugin::Macro::BlockAsMethod instances") do |e|
340
+ e.is_a?(Macro::BlockAsMethod)
341
+ end
346
342
  end
347
343
 
348
344
  # ADR-16 slice 2a — `heredoc_templates:` declares the Tier C
@@ -351,11 +347,9 @@ module Rigor
351
347
  # manifest; the pre-pass + `SyntheticMethodIndex` that actually
352
348
  # emit synthetic methods arrive in slice 2b.
353
349
  def validate_heredoc_templates!(entries)
354
- return if entries.is_a?(Array) && entries.all?(Macro::HeredocTemplate)
355
-
356
- raise ArgumentError,
357
- "plugin manifest heredoc_templates must be an Array of " \
358
- "Rigor::Plugin::Macro::HeredocTemplate instances, got #{entries.inspect}"
350
+ validate_array_of!("heredoc_templates", entries, "Rigor::Plugin::Macro::HeredocTemplate instances") do |e|
351
+ e.is_a?(Macro::HeredocTemplate)
352
+ end
359
353
  end
360
354
 
361
355
  # ADR-36 — `nested_class_templates:` declares the
@@ -365,11 +359,10 @@ module Rigor
365
359
  # subclasses + their `#inner` reader through the existing
366
360
  # `SyntheticMethodIndex` primitive.
367
361
  def validate_nested_class_templates!(entries)
368
- return if entries.is_a?(Array) && entries.all?(Macro::NestedClassTemplate)
369
-
370
- raise ArgumentError,
371
- "plugin manifest nested_class_templates must be an Array of " \
372
- "Rigor::Plugin::Macro::NestedClassTemplate instances, got #{entries.inspect}"
362
+ validate_array_of!("nested_class_templates", entries,
363
+ "Rigor::Plugin::Macro::NestedClassTemplate instances") do |e|
364
+ e.is_a?(Macro::NestedClassTemplate)
365
+ end
373
366
  end
374
367
 
375
368
  # ADR-16 slice 3a — `trait_registries:` declares the Tier B
@@ -379,11 +372,9 @@ module Rigor
379
372
  # `SyntheticMethodIndex` (slice 2b primitive) arrives in
380
373
  # slice 3b.
381
374
  def validate_trait_registries!(entries)
382
- return if entries.is_a?(Array) && entries.all?(Macro::TraitRegistry)
383
-
384
- raise ArgumentError,
385
- "plugin manifest trait_registries must be an Array of " \
386
- "Rigor::Plugin::Macro::TraitRegistry instances, got #{entries.inspect}"
375
+ validate_array_of!("trait_registries", entries, "Rigor::Plugin::Macro::TraitRegistry instances") do |e|
376
+ e.is_a?(Macro::TraitRegistry)
377
+ end
387
378
  end
388
379
 
389
380
  # ADR-20 slice 6 — `hkt_registrations:` declares the
@@ -398,11 +389,9 @@ module Rigor
398
389
  # user `.rbs` overlays merge on top of plugin entries
399
390
  # last-write-wins.
400
391
  def validate_hkt_registrations!(entries)
401
- return if entries.is_a?(Array) && entries.all?(Inference::HktRegistry::Registration)
402
-
403
- raise ArgumentError,
404
- "plugin manifest hkt_registrations must be an Array of " \
405
- "Rigor::Inference::HktRegistry::Registration instances, got #{entries.inspect}"
392
+ validate_array_of!("hkt_registrations", entries, "Rigor::Inference::HktRegistry::Registration instances") do |e|
393
+ e.is_a?(Inference::HktRegistry::Registration)
394
+ end
406
395
  end
407
396
 
408
397
  # ADR-20 slice 6 — `hkt_definitions:` declares the
@@ -415,11 +404,9 @@ module Rigor
415
404
  # via {Rigor::Inference::HktBody}'s node-constructor API
416
405
  # without parsing a string.
417
406
  def validate_hkt_definitions!(entries)
418
- return if entries.is_a?(Array) && entries.all?(Inference::HktRegistry::Definition)
419
-
420
- raise ArgumentError,
421
- "plugin manifest hkt_definitions must be an Array of " \
422
- "Rigor::Inference::HktRegistry::Definition instances, got #{entries.inspect}"
407
+ validate_array_of!("hkt_definitions", entries, "Rigor::Inference::HktRegistry::Definition instances") do |e|
408
+ e.is_a?(Inference::HktRegistry::Definition)
409
+ end
423
410
  end
424
411
 
425
412
  # ADR-25 — `signature_paths:` declares the RBS signature
@@ -429,11 +416,7 @@ module Rigor
429
416
  # loader validates each exists and `Environment.for_project`
430
417
  # merges the resolved set into the RBS environment.
431
418
  def validate_signature_paths!(paths)
432
- return if paths.is_a?(Array) && paths.all? { |p| p.is_a?(String) && !p.empty? }
433
-
434
- raise ArgumentError,
435
- "plugin manifest signature_paths must be an Array of non-empty String, " \
436
- "got #{paths.inspect}"
419
+ validate_array_of!("signature_paths", paths, "non-empty String") { |p| p.is_a?(String) && !p.empty? }
437
420
  end
438
421
 
439
422
  # ADR-28 — `protocol_contracts:` declares the path-scoped
@@ -449,11 +432,9 @@ module Rigor
449
432
  # MAY override `Plugin::Base#protocol_contracts` to fold in
450
433
  # per-project config (e.g. a custom convention path).
451
434
  def validate_protocol_contracts!(entries)
452
- return if entries.is_a?(Array) && entries.all?(ProtocolContract)
453
-
454
- raise ArgumentError,
455
- "plugin manifest protocol_contracts must be an Array of " \
456
- "Rigor::Plugin::ProtocolContract instances, got #{entries.inspect}"
435
+ validate_array_of!("protocol_contracts", entries, "Rigor::Plugin::ProtocolContract instances") do |e|
436
+ e.is_a?(ProtocolContract)
437
+ end
457
438
  end
458
439
 
459
440
  # ADR-38 — `additional_initializers:` declares the
@@ -465,11 +446,9 @@ module Rigor
465
446
  # loaded plugins; `Inference::ScopeIndexer` consults the set at
466
447
  # its single gate.
467
448
  def validate_additional_initializers!(entries)
468
- return if entries.is_a?(Array) && entries.all?(AdditionalInitializer)
469
-
470
- raise ArgumentError,
471
- "plugin manifest additional_initializers must be an Array of " \
472
- "Rigor::Plugin::AdditionalInitializer instances, got #{entries.inspect}"
449
+ validate_array_of!("additional_initializers", entries, "Rigor::Plugin::AdditionalInitializer instances") do |e|
450
+ e.is_a?(AdditionalInitializer)
451
+ end
473
452
  end
474
453
 
475
454
  # ADR-32 WD4 — `source_rbs_synthesizer:` declares a callable
@@ -317,13 +317,12 @@ module Rigor
317
317
  !load_errors.empty?
318
318
  end
319
319
 
320
- # ADR-13 slice 2 — flat ordered list of every loaded
321
- # plugin's manifest-declared {TypeNodeResolver} instances,
322
- # in plugin registration order. Slice 3 wires this into
323
- # the parser's resolver chain; until then the method is a
324
- # read-side aggregator only. The first non-nil
325
- # `#resolve(node, scope)` return wins per ADR-13 WD3 / WD5
326
- # — registration order is the user's lever.
320
+ # ADR-13 — flat ordered list of every loaded plugin's
321
+ # manifest-declared {TypeNodeResolver} instances, in plugin
322
+ # registration order. `Environment#build_name_scope` builds a
323
+ # `TypeNode::ResolverChain` from this list (environment.rb).
324
+ # The first non-nil `#resolve(node, scope)` return wins per
325
+ # ADR-13 WD3 / WD5 — registration order is the user's lever.
327
326
  def type_node_resolvers
328
327
  plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
329
328
  end
@@ -24,14 +24,12 @@ module Rigor
24
24
  # )
25
25
  # end
26
26
  #
27
- # Slice 2 of the ADR-13 envelope (this file) ships the base
28
- # class + manifest hook + registry aggregation. The parser-
29
- # side wiring that actually consults the resolver chain
30
- # arrives in slice 3, when {Rigor::TypeNode::NameScope} and
31
- # the dispatcher between {Rigor::Builtins::ImportedRefinements::Parser}
32
- # and the chain land. Until then resolvers can be unit-tested
33
- # in isolation but never run for a real `%a{rigor:v1:...}`
34
- # payload.
27
+ # ADR-13 base class, manifest hook, and registry aggregation
28
+ # for plugin-contributed type-node resolvers. Resolvers declared
29
+ # via `manifest(type_node_resolvers:)` run for every real
30
+ # `%a{rigor:v1:...}` payload through `TypeNode::ResolverChain`
31
+ # (built by `Environment#build_name_scope` from
32
+ # `Plugin::Registry#type_node_resolvers`).
35
33
  #
36
34
  # Resolvers SHOULD be stateless and re-entrant; the registry
37
35
  # builds the chain once per `Analysis::Runner.run` and may
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../analysis/runner"
4
+
5
+ module Rigor
6
+ module Protection
7
+ # ADR-69 Seam 1 — the **kill oracle** Rigor's analyzer-teeth measurement uses:
8
+ # a mutant is killed iff re-analysing it introduces a diagnostic absent from
9
+ # the clean baseline. This is exactly the behaviour ADR-62/63 shipped, lifted
10
+ # out of {MutationScanner} so a {TestSuiteOracle} (ADR-70) — which kills by
11
+ # *running tests* rather than by re-analysis — can sit beside it without the
12
+ # scanner baking in either assumption.
13
+ #
14
+ # The expensive builds (RBS environment + the whole-project pre-pass scan) are
15
+ # paid once by the caller and threaded in; each mutant reuses them through
16
+ # `Runner.new(prebuilt:)#run_source` (in-memory overlay, no disk write).
17
+ # Passing `prebuilt:` disables the run-result cache (whose key digests the
18
+ # *disk* file), so a mutant is never served a stale clean hit.
19
+ class DiagnosticOracle
20
+ def initialize(configuration:, environment:, project_scan:)
21
+ @configuration = configuration
22
+ @environment = environment
23
+ @project_scan = project_scan
24
+ end
25
+
26
+ # The clean per-file baseline: the diagnostic signatures a mutant must add
27
+ # to count as killed. Computed once per file by the caller.
28
+ def baseline(source:, path:)
29
+ analyse(source, path).to_set { |d| sig(d) }
30
+ end
31
+
32
+ # Killed iff the mutant introduces a diagnostic not in `baseline`.
33
+ def killed?(mutant_source:, path:, baseline:)
34
+ analyse(mutant_source, path).any? { |d| !baseline.include?(sig(d)) }
35
+ end
36
+
37
+ private
38
+
39
+ def analyse(source, path)
40
+ Rigor::Analysis::Runner.new(
41
+ configuration: @configuration, environment: @environment, prebuilt: @project_scan,
42
+ cache_store: nil, collect_stats: false
43
+ ).run_source(source: source, path: path).diagnostics
44
+ end
45
+
46
+ def sig(diagnostic)
47
+ [diagnostic.rule, diagnostic.path, diagnostic.line, diagnostic.column, diagnostic.message]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "mutator"
6
+ require_relative "diagnostic_oracle"
7
+
8
+ module Rigor
9
+ module Protection
10
+ # ADR-63 Tier 2 — the mutation *effectiveness* tier (the truth tier behind
11
+ # Tier 1's static {Inference::ProtectionScanner} proxy). For one file it
12
+ # answers the question Tier 1 only bounds: when a type-visible bug is
13
+ # introduced at a dispatch site, does Rigor actually catch it?
14
+ #
15
+ # Mechanism (the ADR-62 warm loop, narrowed to per-file measurement):
16
+ # generate the type-visible mutations ({Mutator}), keep only those whose
17
+ # receiver Rigor holds a concrete type for (the type-aware filter — the
18
+ # FP-safe meaning-maker; an unresolved receiver is kept), then for each ask
19
+ # the **kill oracle** whether the mutant is caught. The oracle is the ADR-69
20
+ # seam: {#scan_file} uses the {DiagnosticOracle} (a *new Rigor diagnostic* =
21
+ # a kill); {#scan_file_fused} additionally consults a {TestSuiteOracle} on the
22
+ # type-survivors (ADR-70 — the dynamic protection axis).
23
+ #
24
+ # The expensive builds (RBS environment + the whole-project pre-pass scan)
25
+ # are paid ONCE by the caller and threaded into the {DiagnosticOracle}; each
26
+ # mutant reuses them through `Runner.new(prebuilt:)#run_source` (in-memory
27
+ # overlay, no disk write).
28
+ class MutationScanner
29
+ # A surviving mutation site — a breakage Rigor did not catch.
30
+ SurvivingSite = Data.define(:line, :receiver, :method_name, :operator)
31
+
32
+ FileResult = Data.define(:path, :killed, :survived, :sites) do
33
+ # Mutations actually analysed (parse-invalid mutants are not counted).
34
+ def total = killed + survived
35
+
36
+ # Effectiveness ratio; a file with no type-relevant mutation is
37
+ # vacuously fully effective (no breakage was available to miss).
38
+ def ratio = total.zero? ? 1.0 : killed.to_f / total
39
+ end
40
+
41
+ # ADR-70 — one type-survivor classified by the dynamic (test) axis.
42
+ # `protection` is `:test` (a test caught it) or `:none` (unprotected — the
43
+ # "add a type OR a test here" sites).
44
+ FusedSite = Data.define(:line, :receiver, :method_name, :operator, :protection)
45
+
46
+ # ADR-70 — the per-file fused classification. The gradual short-circuit
47
+ # collapses the conceptual "doubly-protected" bucket into `type_killed`:
48
+ # a mutant the type checker already kills never reaches the suite, because
49
+ # the static net already suffices and re-running the suite to learn a test
50
+ # *would also* catch it is wasted work. So the observed buckets are three.
51
+ FusedFileResult = Data.define(:path, :type_killed, :test_killed, :sites) do
52
+ # The unprotected sites (neither a type nor a test caught the breakage).
53
+ def unprotected = sites.size
54
+ def total = type_killed + test_killed + unprotected
55
+
56
+ # Fused protected ratio — caught by *either* axis.
57
+ def ratio = total.zero? ? 1.0 : (type_killed + test_killed).to_f / total
58
+ end
59
+
60
+ # @param configuration [Rigor::Configuration]
61
+ # @param environment [Rigor::Environment] pre-built once by the caller
62
+ # @param project_scan [Rigor::Analysis::ProjectScan] pre-built once
63
+ # @param limit [Integer, nil] optional per-file mutation cap (sampled with
64
+ # `seed`); nil analyses every type-relevant mutation (deterministic).
65
+ # @param seed [Integer] RNG seed for the optional sample.
66
+ # @param oracle [#baseline, #killed?, nil] the kill oracle (ADR-69 Seam 1);
67
+ # defaults to the {DiagnosticOracle} (the ADR-62/63 behaviour).
68
+ # @param site_selector [:biteable, :all] which sites to mutate (ADR-69
69
+ # Seam 2). `:biteable` (default) keeps only concrete-type sites Rigor can
70
+ # bite; `:all` also mutates Dynamic-receiver dispatch sites — use only
71
+ # with a {TestSuiteOracle} (the fused overlay), never the diagnostic path.
72
+ def initialize(configuration:, environment:, project_scan:, limit: nil, seed: 1, oracle: nil,
73
+ site_selector: :biteable)
74
+ @environment = environment
75
+ @limit = limit
76
+ @seed = seed
77
+ @site_selector = site_selector
78
+ @oracle = oracle || DiagnosticOracle.new(
79
+ configuration: configuration, environment: environment, project_scan: project_scan
80
+ )
81
+ end
82
+
83
+ # @param path [String] the file to measure (used as the in-memory bind path)
84
+ # @param source [String, nil] the file's source; read from disk when nil
85
+ # @return [FileResult]
86
+ def scan_file(path, source: nil)
87
+ source ||= File.read(path, encoding: Encoding::UTF_8)
88
+ kept = kept_mutations(source, path)
89
+ return FileResult.new(path: path, killed: 0, survived: 0, sites: []) if kept.empty?
90
+
91
+ baseline = @oracle.baseline(source: source, path: path)
92
+ killed = 0
93
+ sites = []
94
+ kept.each do |mut|
95
+ case classify(source, path, mut, baseline)
96
+ when :killed then killed += 1
97
+ when :survived then sites << surviving_site(mut)
98
+ # :invalid — a parse-broken mutant; not a measurement, skip it.
99
+ end
100
+ end
101
+ FileResult.new(path: path, killed: killed, survived: sites.size, sites: sites)
102
+ end
103
+
104
+ # ADR-70 — the fused static∪dynamic measurement. Runs the type pass
105
+ # (the {DiagnosticOracle}); for every mutant the type checker did **not**
106
+ # kill, asks `test_oracle` whether the project's test suite catches it.
107
+ # The expensive suite run is paid only for type-survivors (the gradual
108
+ # short-circuit), so the cost is proportional to the protection hole.
109
+ # @param test_oracle [TestSuiteOracle]
110
+ # @return [FusedFileResult]
111
+ def scan_file_fused(path, test_oracle:, source: nil)
112
+ source ||= File.read(path, encoding: Encoding::UTF_8)
113
+ kept = kept_mutations(source, path)
114
+ return FusedFileResult.new(path: path, type_killed: 0, test_killed: 0, sites: []) if kept.empty?
115
+
116
+ baseline = @oracle.baseline(source: source, path: path)
117
+ type_killed = 0
118
+ test_killed = 0
119
+ sites = []
120
+ kept.each do |mut|
121
+ case classify(source, path, mut, baseline)
122
+ when :killed then type_killed += 1
123
+ when :survived
124
+ if test_oracle.killed?(path: path, original: source, mutant_source: mut.apply(source))
125
+ test_killed += 1
126
+ else
127
+ sites << fused_site(mut, :none)
128
+ end
129
+ # :invalid — a parse-broken mutant; not a measurement, skip it.
130
+ end
131
+ end
132
+ FusedFileResult.new(path: path, type_killed: type_killed, test_killed: test_killed, sites: sites)
133
+ end
134
+
135
+ private
136
+
137
+ # The mutations to measure: the biteable filter (concrete-type sites only;
138
+ # the FP-safe default — an unresolved receiver is kept) or, under the
139
+ # `:all` selector (ADR-69 Seam 2), every dispatch site including Dynamic
140
+ # receivers. Optionally sampled.
141
+ def kept_mutations(source, path)
142
+ mutator = Mutator.new(source)
143
+ muts = mutator.mutations
144
+ kept =
145
+ if @site_selector == :all
146
+ mutator.dispatch_site_mutations(muts, environment: @environment, path: path)
147
+ else
148
+ mutator.filter_by_type(muts, environment: @environment, path: path).first
149
+ end
150
+ sample(kept)
151
+ end
152
+
153
+ def classify(source, path, mut, baseline)
154
+ mutant_source = mut.apply(source)
155
+ return :invalid unless Prism.parse(mutant_source).success?
156
+
157
+ @oracle.killed?(mutant_source: mutant_source, path: path, baseline: baseline) ? :killed : :survived
158
+ rescue StandardError
159
+ # A harness-level failure on one mutant must not abort the file.
160
+ :invalid
161
+ end
162
+
163
+ def sample(mutations)
164
+ return mutations unless @limit
165
+
166
+ mutations.sample(@limit, random: Random.new(@seed))
167
+ end
168
+
169
+ def surviving_site(mut)
170
+ SurvivingSite.new(line: mut.line, receiver: mut.anchor_type,
171
+ method_name: mut.method_name, operator: mut.operator.to_s)
172
+ end
173
+
174
+ def fused_site(mut, protection)
175
+ FusedSite.new(line: mut.line, receiver: mut.anchor_type, method_name: mut.method_name,
176
+ operator: mut.operator.to_s, protection: protection)
177
+ end
178
+ end
179
+ end
180
+ end