rigortype 0.2.0 → 0.2.2

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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -20
  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/docs/handbook/01-getting-started.md +311 -0
  27. data/docs/handbook/02-everyday-types.md +337 -0
  28. data/docs/handbook/03-narrowing.md +359 -0
  29. data/docs/handbook/04-tuples-and-shapes.md +321 -0
  30. data/docs/handbook/05-methods-and-blocks.md +339 -0
  31. data/docs/handbook/06-classes.md +305 -0
  32. data/docs/handbook/07-rbs-and-extended.md +427 -0
  33. data/docs/handbook/08-understanding-errors.md +373 -0
  34. data/docs/handbook/09-plugins.md +241 -0
  35. data/docs/handbook/10-sorbet.md +347 -0
  36. data/docs/handbook/11-sig-gen.md +312 -0
  37. data/docs/handbook/12-lightweight-hkt.md +333 -0
  38. data/docs/handbook/README.md +275 -0
  39. data/docs/handbook/appendix-elixir.md +370 -0
  40. data/docs/handbook/appendix-go.md +399 -0
  41. data/docs/handbook/appendix-java-csharp.md +470 -0
  42. data/docs/handbook/appendix-liskov.md +580 -0
  43. data/docs/handbook/appendix-mypy.md +370 -0
  44. data/docs/handbook/appendix-phpstan.md +338 -0
  45. data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
  46. data/docs/handbook/appendix-rust.md +446 -0
  47. data/docs/handbook/appendix-steep.md +336 -0
  48. data/docs/handbook/appendix-type-theory.md +1662 -0
  49. data/docs/handbook/appendix-typeprof.md +416 -0
  50. data/docs/handbook/appendix-typescript.md +332 -0
  51. data/docs/install.md +189 -0
  52. data/docs/llms.txt +72 -0
  53. data/docs/manual/01-installation.md +342 -0
  54. data/docs/manual/02-cli-reference.md +557 -0
  55. data/docs/manual/03-configuration.md +152 -0
  56. data/docs/manual/04-diagnostics.md +206 -0
  57. data/docs/manual/05-inspecting-types.md +109 -0
  58. data/docs/manual/06-baseline.md +104 -0
  59. data/docs/manual/07-plugins.md +92 -0
  60. data/docs/manual/08-skills.md +143 -0
  61. data/docs/manual/09-editor-integration.md +245 -0
  62. data/docs/manual/10-mcp-server.md +532 -0
  63. data/docs/manual/11-ci.md +274 -0
  64. data/docs/manual/12-caching.md +116 -0
  65. data/docs/manual/13-troubleshooting.md +120 -0
  66. data/docs/manual/14-rails-quickstart.md +332 -0
  67. data/docs/manual/15-type-protection-coverage.md +204 -0
  68. data/docs/manual/16-rbs-extended-annotations.md +190 -0
  69. data/docs/manual/17-driving-improvement.md +160 -0
  70. data/docs/manual/README.md +87 -0
  71. data/docs/manual/ci-templates/README.md +58 -0
  72. data/docs/manual/plugins/README.md +86 -0
  73. data/docs/manual/plugins/rigor-actioncable.md +78 -0
  74. data/docs/manual/plugins/rigor-actionmailer.md +74 -0
  75. data/docs/manual/plugins/rigor-actionpack.md +80 -0
  76. data/docs/manual/plugins/rigor-activejob.md +58 -0
  77. data/docs/manual/plugins/rigor-activerecord.md +102 -0
  78. data/docs/manual/plugins/rigor-activestorage.md +74 -0
  79. data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
  80. data/docs/manual/plugins/rigor-devise.md +70 -0
  81. data/docs/manual/plugins/rigor-dry-schema.md +56 -0
  82. data/docs/manual/plugins/rigor-dry-struct.md +60 -0
  83. data/docs/manual/plugins/rigor-dry-types.md +59 -0
  84. data/docs/manual/plugins/rigor-dry-validation.md +62 -0
  85. data/docs/manual/plugins/rigor-factorybot.md +76 -0
  86. data/docs/manual/plugins/rigor-graphql.md +89 -0
  87. data/docs/manual/plugins/rigor-hanami.md +83 -0
  88. data/docs/manual/plugins/rigor-mangrove.md +73 -0
  89. data/docs/manual/plugins/rigor-minitest.md +86 -0
  90. data/docs/manual/plugins/rigor-pundit.md +72 -0
  91. data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
  92. data/docs/manual/plugins/rigor-rails-routes.md +94 -0
  93. data/docs/manual/plugins/rigor-rails.md +44 -0
  94. data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
  95. data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
  96. data/docs/manual/plugins/rigor-rspec.md +86 -0
  97. data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
  98. data/docs/manual/plugins/rigor-sidekiq.md +78 -0
  99. data/docs/manual/plugins/rigor-sinatra.md +61 -0
  100. data/docs/manual/plugins/rigor-sorbet.md +63 -0
  101. data/docs/manual/plugins/rigor-statesman.md +75 -0
  102. data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
  103. data/exe/rigor +1 -1
  104. data/lib/rigor/analysis/incremental_session.rb +4 -2
  105. data/lib/rigor/analysis/run_stats.rb +13 -1
  106. data/lib/rigor/analysis/runner.rb +54 -12
  107. data/lib/rigor/cli/check_command.rb +26 -3
  108. data/lib/rigor/cli/coverage_command.rb +67 -92
  109. data/lib/rigor/cli/coverage_mutation.rb +149 -0
  110. data/lib/rigor/cli/docs_command.rb +248 -0
  111. data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
  112. data/lib/rigor/cli/fused_protection_report.rb +76 -0
  113. data/lib/rigor/cli/skill_command.rb +103 -41
  114. data/lib/rigor/cli/skill_describe.rb +346 -0
  115. data/lib/rigor/cli.rb +25 -3
  116. data/lib/rigor/config_audit.rb +152 -0
  117. data/lib/rigor/configuration.rb +12 -0
  118. data/lib/rigor/environment/rbs_loader.rb +27 -0
  119. data/lib/rigor/environment.rb +49 -1
  120. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
  121. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  122. data/lib/rigor/inference/scope_indexer.rb +87 -89
  123. data/lib/rigor/inference/statement_evaluator.rb +27 -0
  124. data/lib/rigor/plugin/isolation.rb +5 -5
  125. data/lib/rigor/plugin/loader.rb +4 -2
  126. data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
  127. data/lib/rigor/protection/mutation_scanner.rb +98 -38
  128. data/lib/rigor/protection/mutator.rb +21 -0
  129. data/lib/rigor/protection/test_suite_oracle.rb +68 -0
  130. data/lib/rigor/signature_path_audit.rb +92 -0
  131. data/lib/rigor/version.rb +1 -1
  132. data/skills/rigor-ask/SKILL.md +172 -0
  133. data/skills/rigor-doctor/SKILL.md +87 -0
  134. data/skills/rigor-editor-setup/SKILL.md +114 -0
  135. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  136. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  137. data/skills/rigor-next-steps/SKILL.md +113 -0
  138. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  139. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  140. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  141. data/skills/rigor-upgrade/SKILL.md +79 -0
  142. metadata +120 -1
@@ -88,6 +88,7 @@ module Rigor
88
88
  to_h: :tuple_to_h,
89
89
  zip: :tuple_zip,
90
90
  :[] => :tuple_index,
91
+ slice: :tuple_index,
91
92
  fetch: :tuple_index,
92
93
  dig: :tuple_dig,
93
94
  values_at: :tuple_values_at,
@@ -842,7 +843,10 @@ module Rigor
842
843
 
843
844
  # `tuple.min` / `tuple.max` — fold when every element is
844
845
  # a `Constant` whose values share a Ruby-comparable
845
- # domain. Empty tuples fold to `Constant[nil]`.
846
+ # domain. Empty tuples fold to `Constant[nil]`. The 1-arg
847
+ # `min(n)` / `max(n)` form folds to a `Tuple` of the n
848
+ # edge-most values in Ruby's order (`min(n)` ascending,
849
+ # `max(n)` descending) — the n-arg sibling of `first(n)`.
846
850
  def tuple_min(tuple, _method_name, args)
847
851
  tuple_minmax(tuple, args, :min)
848
852
  end
@@ -852,7 +856,7 @@ module Rigor
852
856
  end
853
857
 
854
858
  def tuple_minmax(tuple, args, edge)
855
- return nil unless args.empty?
859
+ return tuple_minmax_n(tuple, args.first, edge) unless args.empty?
856
860
  return Type::Combinator.constant_of(nil) if tuple.elements.empty?
857
861
 
858
862
  values = constant_values(tuple.elements)
@@ -864,6 +868,25 @@ module Rigor
864
868
  nil
865
869
  end
866
870
 
871
+ # `tuple.min(n)` / `tuple.max(n)` — a `Tuple` of the n
872
+ # edge-most element values, delegating to Ruby's
873
+ # `Array#min` / `#max` for the ordering. Declines on a
874
+ # non-static / negative count or non-Constant elements.
875
+ # The result is bounded by the tuple's known arity, so no
876
+ # extra size cap is needed.
877
+ def tuple_minmax_n(tuple, arg, edge)
878
+ return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
879
+ return nil if arg.value.negative?
880
+
881
+ values = constant_values(tuple.elements)
882
+ return nil if values.nil?
883
+
884
+ picked = values.public_send(edge, arg.value)
885
+ Type::Combinator.tuple_of(*picked.map { |v| Type::Combinator.constant_of(v) })
886
+ rescue StandardError
887
+ nil
888
+ end
889
+
867
890
  # `tuple.minmax` — the `[min, max]` pair as a 2-slot
868
891
  # `Tuple[Constant[min], Constant[max]]`, mirroring the
869
892
  # `Range#minmax` fold. Every element must be a `Constant`
@@ -1201,6 +1224,13 @@ module Rigor
1201
1224
  # indices still fall through because the same handler serves
1202
1225
  # `fetch`, while statically nil slices can be represented
1203
1226
  # precisely for `[]`.
1227
+ # `[]` and its exact alias `slice` share the index / Range /
1228
+ # start-length folding. `fetch` routes here too but stays
1229
+ # integer-index-only: the Range and start-length branches gate
1230
+ # on this selector set, which `fetch` is deliberately not in.
1231
+ SLICE_SELECTORS = Set[:[], :slice].freeze
1232
+ private_constant :SLICE_SELECTORS
1233
+
1204
1234
  def tuple_index(tuple, method_name, args)
1205
1235
  case args.size
1206
1236
  when 1 then tuple_single_index(tuple, method_name, args.first)
@@ -1211,17 +1241,18 @@ module Rigor
1211
1241
  def tuple_single_index(tuple, method_name, arg)
1212
1242
  return nil unless arg.is_a?(Type::Constant)
1213
1243
 
1214
- return tuple_range_slice(tuple, arg.value) if method_name == :[] && arg.value.is_a?(Range)
1215
- return nil unless arg.value.is_a?(Integer)
1244
+ value = arg.value
1245
+ return tuple_range_slice(tuple, value) if SLICE_SELECTORS.include?(method_name) && value.is_a?(Range)
1246
+ return nil unless value.is_a?(Integer)
1216
1247
 
1217
- idx = normalise_index(arg.value, tuple.elements.size)
1248
+ idx = normalise_index(value, tuple.elements.size)
1218
1249
  return nil unless idx
1219
1250
 
1220
1251
  tuple.elements[idx]
1221
1252
  end
1222
1253
 
1223
1254
  def tuple_start_length_slice(tuple, method_name, args)
1224
- return nil unless method_name == :[]
1255
+ return nil unless SLICE_SELECTORS.include?(method_name)
1225
1256
 
1226
1257
  start, length = args
1227
1258
  return nil unless start.is_a?(Type::Constant) && length.is_a?(Type::Constant)
@@ -118,10 +118,12 @@ module Rigor
118
118
  # table to suppress false positives for methods the
119
119
  # user has defined but no RBS sig describes. Merged
120
120
  # UNDER the cross-file pre-pass seed; details: merge_project_method_indexes.
121
- discovered_methods = deep_merge_class_methods(
122
- default_scope.discovered_methods, build_discovered_methods(root)
123
- )
124
- seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
121
+ # One combined descent yields both the discovered-methods existence
122
+ # table and the instance def-node table — see
123
+ # {#build_methods_and_def_nodes}. `seed_discovered_methods` seeds the
124
+ # former onto the scope and returns the def-node table for
125
+ # `merge_project_method_indexes` below.
126
+ seeded_scope, file_def_nodes = seed_discovered_methods(seeded_scope, default_scope, root)
125
127
 
126
128
  # v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
127
129
  # def nodes, the class -> superclass map, and the
@@ -134,7 +136,7 @@ module Rigor
134
136
  # table. Seeded inside `merge_project_method_indexes` so the
135
137
  # per-file visibilities merge OVER the cross-file project seed
136
138
  # rather than overwriting it.
137
- seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root)
139
+ seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
138
140
 
139
141
  table = {}.compare_by_identity
140
142
  table.default = seeded_scope
@@ -160,6 +162,19 @@ module Rigor
160
162
  table
161
163
  end
162
164
 
165
+ # Runs the combined methods/def-nodes descent (one walk of the file),
166
+ # seeds the discovered-methods existence table onto `seeded_scope`
167
+ # (merged UNDER the cross-file pre-pass seed `default_scope` carries),
168
+ # and returns `[scope, file_def_nodes]` so the caller can thread the
169
+ # def-node table into {#merge_project_method_indexes} without walking
170
+ # the file a second time.
171
+ def seed_discovered_methods(seeded_scope, default_scope, root)
172
+ file_methods, file_def_nodes = build_methods_and_def_nodes(root)
173
+ discovered_methods = deep_merge_class_methods(default_scope.discovered_methods, file_methods)
174
+ scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
175
+ [scope, file_def_nodes]
176
+ end
177
+
163
178
  # ADR-48 Struct slice 3 — installs the top-level fold-safe-local set
164
179
  # ({Inference::StructFoldSafety}). Struct member layouts of constant
165
180
  # receivers are resolved through the side-table the seeded scope carries.
@@ -179,9 +194,9 @@ module Rigor
179
194
  # `discovered_def_index_for_paths` seed carried on
180
195
  # `default_scope` — same-file declarations win per entry,
181
196
  # the cross-file seed supplies sibling-file ancestors.
182
- def merge_project_method_indexes(seeded_scope, default_scope, root)
197
+ def merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
183
198
  def_nodes = default_scope.discovered_def_nodes.merge(
184
- build_discovered_def_nodes(root)
199
+ file_def_nodes
185
200
  ) { |_class, cross_file, per_file| cross_file.merge(per_file) }
186
201
  singleton_def_nodes = default_scope.discovered_singleton_def_nodes.merge(
187
202
  build_discovered_singleton_def_nodes(root)
@@ -1406,16 +1421,29 @@ module Rigor
1406
1421
  Type::Combinator.singleton_of(full)
1407
1422
  end
1408
1423
 
1409
- # Slice 7 phase 12 — in-source method discovery pre-pass.
1410
- # Walks every class/module body and records the methods
1411
- # introduced via `Prism::DefNode` (instance + singleton)
1412
- # and via recognised `define_method(:name) { ... }` calls.
1413
- # The returned table maps qualified class name to a
1414
- # `Hash[Symbol, :instance | :singleton]`.
1415
- def build_discovered_methods(root)
1416
- accumulator = {}
1417
- walk_methods(root, [], false, accumulator)
1418
- accumulator.transform_values(&:freeze).freeze
1424
+ # Slice 7 phase 12 — in-source method discovery pre-pass, fused with
1425
+ # the instance-method def-node pre-pass (v0.0.2 #5). One descent
1426
+ # produces BOTH tables the per-file `index` and the cross-file
1427
+ # pre-pass each need together:
1428
+ #
1429
+ # - `methods` : `{class_name => {method => :instance | :singleton}}`
1430
+ # for every `def` / `define_method(:name)` / `attr_*` / `alias` /
1431
+ # Data/Struct-member reader (the undefined-method existence table).
1432
+ # - `def_nodes` : `{class_name => {method => Prism::DefNode}}` for
1433
+ # every instance-side `def` (the inter-procedural return-inference
1434
+ # table; singleton defs and `define_method` are intentionally
1435
+ # skipped — `record_def_node` filters them).
1436
+ #
1437
+ # `walk_methods` and `walk_def_nodes` had byte-identical class /
1438
+ # module / singleton / meta-block descents (both stop at `DefNode`),
1439
+ # so a single combined walk records both accumulators at once instead
1440
+ # of traversing every file twice.
1441
+ def build_methods_and_def_nodes(root)
1442
+ methods = {}
1443
+ def_nodes = {}
1444
+ walk_methods_and_def_nodes(root, [], false, methods, def_nodes)
1445
+ apply_alias_def_nodes(root, def_nodes)
1446
+ [methods.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze]
1419
1447
  end
1420
1448
 
1421
1449
  # Merges two `class_name => { method => kind }` tables, unioning
@@ -1431,7 +1459,14 @@ module Rigor
1431
1459
  end
1432
1460
 
1433
1461
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
1434
- def walk_methods(node, qualified_prefix, in_singleton_class, accumulator)
1462
+ # Combined `walk_methods` + `walk_def_nodes` descent. The two walks
1463
+ # had identical class / module / singleton-class / meta-block
1464
+ # traversals and both stopped at `DefNode`; the only divergences are
1465
+ # leaf actions (recorded into the right accumulator) and the original
1466
+ # `walk_methods` returning at `AliasMethodNode` (its symbol-only
1467
+ # children carry no def / class node, so not descending them is
1468
+ # byte-identical for `def_nodes` too). See {#build_methods_and_def_nodes}.
1469
+ def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
1435
1470
  return unless node.is_a?(Prism::Node)
1436
1471
 
1437
1472
  case node
@@ -1439,40 +1474,40 @@ module Rigor
1439
1474
  name = Source::ConstantPath.qualified_name(node.constant_path)
1440
1475
  if name
1441
1476
  child_prefix = qualified_prefix + [name]
1442
- record_meta_superclass_members(node, child_prefix, accumulator) if node.is_a?(Prism::ClassNode)
1443
- walk_methods(node.body, child_prefix, false, accumulator) if node.body
1477
+ record_meta_superclass_members(node, child_prefix, methods_acc) if node.is_a?(Prism::ClassNode)
1478
+ walk_methods_and_def_nodes(node.body, child_prefix, false, methods_acc, def_nodes_acc) if node.body
1444
1479
  return
1445
1480
  end
1446
1481
  when Prism::SingletonClassNode
1447
1482
  if node.body
1448
1483
  singleton_prefix = singleton_class_prefix(node, qualified_prefix)
1449
1484
  if singleton_prefix
1450
- walk_methods(node.body, singleton_prefix, true, accumulator)
1485
+ walk_methods_and_def_nodes(node.body, singleton_prefix, true, methods_acc, def_nodes_acc)
1451
1486
  return
1452
1487
  end
1453
1488
  end
1454
1489
  when Prism::ConstantWriteNode
1455
1490
  if meta_new_block_body(node)
1456
1491
  child_prefix = qualified_prefix + [node.name.to_s]
1457
- walk_methods(meta_new_block_body(node), child_prefix, false, accumulator)
1492
+ walk_methods_and_def_nodes(meta_new_block_body(node), child_prefix, false, methods_acc, def_nodes_acc)
1458
1493
  return
1459
1494
  end
1460
1495
  when Prism::DefNode
1461
- record_def_method(node, qualified_prefix, in_singleton_class, accumulator)
1496
+ record_def_method(node, qualified_prefix, in_singleton_class, methods_acc)
1497
+ record_def_node(node, qualified_prefix, in_singleton_class, def_nodes_acc)
1462
1498
  return
1463
1499
  when Prism::AliasMethodNode
1464
- record_alias_method(node, qualified_prefix, in_singleton_class, accumulator)
1500
+ record_alias_method(node, qualified_prefix, in_singleton_class, methods_acc)
1465
1501
  return
1466
1502
  when Prism::CallNode
1467
- record_define_method(node, qualified_prefix, in_singleton_class, accumulator) if node.name == :define_method
1503
+ record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
1468
1504
  if ATTR_MACROS.include?(node.name)
1469
- record_attr_methods(node, qualified_prefix, in_singleton_class,
1470
- accumulator)
1505
+ record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc)
1471
1506
  end
1472
1507
  end
1473
1508
 
1474
1509
  node.compact_child_nodes.each do |child|
1475
- walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
1510
+ walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
1476
1511
  end
1477
1512
  end
1478
1513
 
@@ -1606,57 +1641,6 @@ module Rigor
1606
1641
  end
1607
1642
  end
1608
1643
 
1609
- # v0.0.2 #5 — instance-side def-node recording. Walks
1610
- # class bodies the same way as `build_discovered_methods`
1611
- # but records the actual `Prism::DefNode` for each
1612
- # **instance** method so `ExpressionTyper` can re-type
1613
- # the body at the call site for inter-procedural return
1614
- # inference. Singleton methods and `define_method` calls
1615
- # are intentionally skipped: the inference path needs a
1616
- # statically introspectable body, and singleton dispatch
1617
- # has its own complications (Class / Module ancestry)
1618
- # the first-iteration rule does not yet model.
1619
- def build_discovered_def_nodes(root)
1620
- accumulator = {}
1621
- walk_def_nodes(root, [], false, accumulator)
1622
- apply_alias_def_nodes(root, accumulator)
1623
- accumulator.transform_values(&:freeze).freeze
1624
- end
1625
-
1626
- def walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
1627
- return unless node.is_a?(Prism::Node)
1628
-
1629
- case node
1630
- when Prism::ClassNode, Prism::ModuleNode
1631
- name = Source::ConstantPath.qualified_name(node.constant_path)
1632
- if name
1633
- child_prefix = qualified_prefix + [name]
1634
- walk_def_nodes(node.body, child_prefix, false, accumulator) if node.body
1635
- return
1636
- end
1637
- when Prism::SingletonClassNode
1638
- if node.body
1639
- singleton_prefix = singleton_class_prefix(node, qualified_prefix)
1640
- if singleton_prefix
1641
- walk_def_nodes(node.body, singleton_prefix, true, accumulator)
1642
- return
1643
- end
1644
- end
1645
- when Prism::ConstantWriteNode
1646
- if meta_new_block_body(node)
1647
- child_prefix = qualified_prefix + [node.name.to_s]
1648
- walk_def_nodes(meta_new_block_body(node), child_prefix, false, accumulator)
1649
- return
1650
- end
1651
- when Prism::DefNode
1652
- record_def_node(node, qualified_prefix, in_singleton_class, accumulator)
1653
- return
1654
- end
1655
-
1656
- node.compact_child_nodes.each do |child|
1657
- walk_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
1658
- end
1659
- end
1660
1644
  # v0.0.3 A — sentinel key under which `record_def_node`
1661
1645
  # files DefNodes that live outside any class / module
1662
1646
  # body (top-level helpers, `def`s nested inside DSL
@@ -2385,7 +2369,13 @@ module Rigor
2385
2369
  # the override-visibility-reduced rule can read an ancestor's
2386
2370
  # visibility declared in a sibling file.
2387
2371
  def accumulate_project_index(acc, path, root)
2388
- merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, root)
2372
+ # One combined descent yields both the methods existence table and
2373
+ # the def-node table; the latter is also consumed by
2374
+ # `record_class_sources`, so a def-dense file is walked once here
2375
+ # instead of three times (methods + def-nodes ×2). See
2376
+ # {#build_methods_and_def_nodes}.
2377
+ file_methods, file_def_nodes = build_methods_and_def_nodes(root)
2378
+ merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
2389
2379
  build_discovered_singleton_def_nodes(root).each do |class_name, methods|
2390
2380
  (acc[:singleton_def_nodes][class_name] ||= {}).merge!(methods)
2391
2381
  end
@@ -2395,20 +2385,28 @@ module Rigor
2395
2385
  includes.each do |class_name, mods|
2396
2386
  acc[:includes][class_name] = ((acc[:includes][class_name] || []) + mods).uniq
2397
2387
  end
2398
- record_class_sources(acc[:class_sources], path, root, superclasses, includes)
2399
- merge_class_keyed_index_tables(acc, root)
2388
+ record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes)
2389
+ merge_class_keyed_index_tables(acc, root, file_methods)
2390
+ merge_member_layout_tables(acc, root)
2391
+ end
2392
+
2393
+ # Folds one file's Data + Struct member-layout tables into the
2394
+ # cross-file accumulator (kept out of {#accumulate_project_index} to
2395
+ # hold its ABC budget).
2396
+ def merge_member_layout_tables(acc, root)
2400
2397
  acc[:data_member_layouts].merge!(build_data_member_layouts(root))
2401
2398
  acc[:struct_member_layouts].merge!(build_struct_member_layouts(root))
2402
2399
  end
2403
2400
 
2404
2401
  # Folds the per-class method-visibility and method-existence tables of
2405
2402
  # one file into the cross-file accumulator (kept out of
2406
- # {#accumulate_project_index} to hold its ABC budget).
2407
- def merge_class_keyed_index_tables(acc, root)
2403
+ # {#accumulate_project_index} to hold its ABC budget). `file_methods`
2404
+ # is the existence table from the combined methods/def-nodes descent.
2405
+ def merge_class_keyed_index_tables(acc, root, file_methods)
2408
2406
  build_discovered_method_visibilities(root).each do |class_name, table|
2409
2407
  (acc[:method_visibilities][class_name] ||= {}).merge!(table)
2410
2408
  end
2411
- build_discovered_methods(root).each do |class_name, table|
2409
+ file_methods.each do |class_name, table|
2412
2410
  (acc[:methods][class_name] ||= {}).merge!(table)
2413
2411
  end
2414
2412
  end
@@ -2423,13 +2421,13 @@ module Rigor
2423
2421
  # dependency recording (ADR-46). The class-declaration walk
2424
2422
  # (`collect_class_decls`) catches bodyless / def-less reopenings the
2425
2423
  # other three builders miss.
2426
- def record_class_sources(class_sources, path, root, superclasses, includes)
2424
+ def record_class_sources(class_sources, path, root, superclasses, includes, file_def_nodes)
2427
2425
  names = Set.new
2428
2426
  collect_class_decls(root, [], decls = {})
2429
2427
  names.merge(decls.keys)
2430
2428
  names.merge(superclasses.keys)
2431
2429
  names.merge(includes.keys)
2432
- names.merge(build_discovered_def_nodes(root).keys)
2430
+ names.merge(file_def_nodes.keys)
2433
2431
  names.each { |name| (class_sources[name] ||= Set.new) << path }
2434
2432
  end
2435
2433
 
@@ -2438,8 +2436,8 @@ module Rigor
2438
2436
  # seen `"path:line"` definition site in `def_sources` (ADR-17 —
2439
2437
  # the un-registered-project-patch signal `call.undefined-method`
2440
2438
  # and `rigor triage` key on).
2441
- def merge_discovered_defs(def_nodes, def_sources, path, root)
2442
- build_discovered_def_nodes(root).each do |class_name, methods|
2439
+ def merge_discovered_defs(def_nodes, def_sources, path, file_def_nodes)
2440
+ file_def_nodes.each do |class_name, methods|
2443
2441
  (def_nodes[class_name] ||= {}).merge!(methods)
2444
2442
  sources = (def_sources[class_name] ||= {})
2445
2443
  methods.each do |method_name, def_node|
@@ -2274,6 +2274,11 @@ module Rigor
2274
2274
  body = block_node.body
2275
2275
  return post_scope if body.nil?
2276
2276
 
2277
+ # Transitive case first: the body may content-mutate a captured local
2278
+ # through a self-call rather than a direct `local[k] = v` write, which
2279
+ # `collect_content_mutations` cannot see (see below).
2280
+ post_scope = floor_block_body_callee_escaped_args(body, post_scope)
2281
+
2277
2282
  mutations = collect_content_mutations(body)
2278
2283
  return post_scope if mutations.empty?
2279
2284
 
@@ -2283,6 +2288,28 @@ module Rigor
2283
2288
  end
2284
2289
  end
2285
2290
 
2291
+ # Inside an ESCAPING block body, a captured outer local can be content-
2292
+ # mutated transitively: the body is (or contains) a self-call that
2293
+ # escape-mutates one of its arguments. The canonical shape is the CLI's
2294
+ # own `OptionParser.new { |opts| define_options(opts, options) }` — the
2295
+ # block body is a bare `define_options(opts, options)` whose `options`
2296
+ # parameter is escape-mutated inside ITS nested `opts.on { options[:k] =
2297
+ # v }` blocks. `collect_content_mutations` only sees direct `local[k] =
2298
+ # v` writes in THIS body, so it misses the transitive write and the
2299
+ # captured Hash keeps its literal-false seed (folding the caller's
2300
+ # `options[:mutation]` guard to an always-falsey constant). Reuse the
2301
+ # cross-method-boundary callee-escaped-argument floor (the same gate the
2302
+ # receiver-chain path uses at the call site) on every self-call in the
2303
+ # body. Sound — only ever floors a captured local passed as an argument
2304
+ # to a callee that demonstrably escape-mutates the matching parameter.
2305
+ def floor_block_body_callee_escaped_args(body, post_scope)
2306
+ acc = post_scope
2307
+ Source::NodeWalker.each(body) do |descendant|
2308
+ acc = floor_callee_escaped_args_for_call(descendant, acc) if descendant.is_a?(Prism::CallNode)
2309
+ end
2310
+ acc
2311
+ end
2312
+
2286
2313
  # The Dynamic-floor carrier for a content-mutated escaping capture, or
2287
2314
  # nil when the pre-state is not a recognised mutable collection (leave
2288
2315
  # it alone — e.g. an already-`Dynamic` binding or an unknown shape).
@@ -12,13 +12,13 @@ module Rigor
12
12
  # `exe/rigor` launcher maps `.rigor.yml`'s `plugins_isolation:` onto it
13
13
  # before re-exec). Three backends behind one interface:
14
14
  #
15
- # - `none` (**default**) — load into the main space and call directly.
16
- # Lowest cost; no isolation. Fine for the common case because the
17
- # invoked library is trusted + pure.
15
+ # - `none` — load into the main space and call directly.
16
+ # Lowest cost; no isolation. Used as the fallback where fork is
17
+ # unavailable; fine because the invoked library is trusted + pure.
18
18
  # - `ruby_box` — call inside a {Box} (`Ruby::Box`, `RUBY_BOX=1`). Isolates
19
19
  # core-class monkey-patches + lets gem versions coexist, but a native
20
20
  # crash in the boxed work still takes the process down (in-process).
21
- # - `process` — call in a forked worker ({Process}); returns data over a
21
+ # - `process` (**default**) — call in a forked worker ({Process}); returns data over a
22
22
  # pipe. The strongest: a child crash (even `SIGSEGV`) is contained —
23
23
  # the parent survives and declines. Higher cost (fork + IPC).
24
24
  #
@@ -73,7 +73,7 @@ module Rigor
73
73
  end
74
74
 
75
75
  # `none` — load the trusted library into the main space and call it
76
- # directly. No isolation; lowest cost; the current default behaviour.
76
+ # directly. No isolation; lowest cost; the fork-unavailable fallback.
77
77
  module Direct
78
78
  module_function
79
79
 
@@ -183,8 +183,10 @@ module Rigor
183
183
  Plugin.registered_for(newly_registered.first)
184
184
  else
185
185
  raise LoadError.new(
186
- "plugin gem #{entry[:gem].inspect} registered multiple plugins " \
187
- "(#{newly_registered.sort.inspect}); disambiguate with an explicit `id:` field",
186
+ "plugin gem #{entry[:gem].inspect} bundles #{newly_registered.size} plugins " \
187
+ "(#{newly_registered.sort.inspect}) and cannot be activated as a single `plugins:` entry — " \
188
+ "it is a convenience meta-gem. List the individual plugin gems you want in `plugins:` " \
189
+ "(e.g. `rigor-#{newly_registered.min}`), or select one with an explicit `id:` field.",
188
190
  plugin_ref: entry[:gem]
189
191
  )
190
192
  end
@@ -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