rigortype 0.4.0 → 0.4.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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -10,6 +10,7 @@ require_relative "../rbs_extended"
10
10
  require_relative "../analysis/fact_store"
11
11
  require_relative "../builtins/regex_refinement"
12
12
  require_relative "guard_rebinding"
13
+ require_relative "key_presence_guard"
13
14
  require_relative "last_line"
14
15
  require_relative "operand_effects"
15
16
  require_relative "optimistic_origin"
@@ -440,6 +441,20 @@ module Rigor
440
441
  Source::ConstantPath.rooted?(node) ? "::#{name}" : name
441
442
  end
442
443
 
444
+ # Issue #1446 — the classes a class guard narrowed the receiver `node` to where the guard left it `bot`
445
+ # ({Scope#bot_guard_classes_for}), or nil: a node that is not a local, instance-variable, global or constant
446
+ # read, or one no such guard narrowed.
447
+ def bot_guard_classes_of(node, scope)
448
+ case node
449
+ when Prism::LocalVariableReadNode then scope.bot_guard_classes_for(:local, node.name)
450
+ when Prism::InstanceVariableReadNode then scope.bot_guard_classes_for(:ivar, node.name)
451
+ when Prism::GlobalVariableReadNode then scope.bot_guard_classes_for(:global, node.name)
452
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
453
+ key = constant_key(node)
454
+ key && scope.bot_guard_classes_for(:constant, key)
455
+ end
456
+ end
457
+
443
458
  # Public predicate analyser. Returns `[truthy_scope, falsey_scope]`, always; when no
444
459
  # narrowing rule matches the predicate node both entries are the receiver scope unchanged.
445
460
  def predicate_scopes(node, scope)
@@ -504,15 +519,20 @@ module Rigor
504
519
  # a different regex. Applied even when the subject is not a narrowable local read.
505
520
  body_scope = apply_when_regex_globals(conditions, scope)
506
521
 
507
- # Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does.
522
+ # Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does, and so
523
+ # does an instance variable (#1446).
508
524
  slot = receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
509
525
  return [body_scope, scope] if slot.nil?
510
526
 
511
527
  truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
512
- [narrow_slot(body_scope, slot, truthy_type), narrow_slot(scope, slot, falsey_type)]
528
+ classes = conditions.map do |condition|
529
+ lexical_class_name(condition, scope) || case_equality_target_class(condition)
530
+ end
531
+ body_scope = record_bot_guard(narrow_guarded_slot(body_scope, slot, truthy_type), slot, truthy_type, classes)
532
+ [body_scope, narrow_guarded_slot(scope, slot, falsey_type)]
513
533
  end
514
534
 
515
- CASE_SUBJECT_KINDS = %i[local global constant].freeze
535
+ CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
516
536
  private_constant :CASE_SUBJECT_KINDS
517
537
 
518
538
  # When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
@@ -979,8 +999,9 @@ module Rigor
979
999
  end
980
1000
 
981
1001
  GLOBAL_AND_CONSTANT = %i[global constant].freeze
1002
+ CLASS_GUARD_SLOT_KINDS = %i[ivar global constant].freeze
982
1003
  ALL_SLOT_KINDS = %i[local ivar global constant].freeze
983
- private_constant :GLOBAL_AND_CONSTANT, :ALL_SLOT_KINDS
1004
+ private_constant :GLOBAL_AND_CONSTANT, :CLASS_GUARD_SLOT_KINDS, :ALL_SLOT_KINDS
984
1005
 
985
1006
  # The {ReceiverSlot} `node` reads, among `kinds`, or nil. A local or instance variable needs a binding. A
986
1007
  # global the scope does not bind is read as it reads unbound, and a constant reference as it resolves
@@ -1029,6 +1050,25 @@ module Rigor
1029
1050
  end
1030
1051
  end
1031
1052
 
1053
+ # Issue #1446 — {#narrow_slot} for a class guard: an instance variable narrows with its pre-guard type
1054
+ # recorded, as a global does, since code that may reach `self` may rebind it. The other guards on an instance
1055
+ # variable keep {#narrow_slot}'s unrecorded binding.
1056
+ def narrow_guarded_slot(scope, slot, type)
1057
+ return narrow_slot(scope, slot, type) unless slot.kind == :ivar
1058
+ return scope if type == slot.current
1059
+
1060
+ scope.with_guarded_ivar(slot.name, type, slot.current)
1061
+ end
1062
+
1063
+ # Issue #1446 — `scope`, the guard's truthy edge, with `class_names` recorded for `slot` when the guard left
1064
+ # it `bot` ({Scope#with_bot_guard_classes}): the value that passes it is one of those classes. Nothing is
1065
+ # recorded when a condition names no class.
1066
+ def record_bot_guard(scope, slot, type, class_names)
1067
+ return scope unless type.is_a?(Type::Bot) && !class_names.empty? && class_names.none?(&:nil?)
1068
+
1069
+ scope.with_bot_guard_classes(slot.kind, slot.name, class_names)
1070
+ end
1071
+
1032
1072
  def narrow_global_slot(scope, slot, type)
1033
1073
  return scope if type == slot.current
1034
1074
 
@@ -1234,7 +1274,7 @@ module Rigor
1234
1274
  end
1235
1275
 
1236
1276
  def simple_dispatch_name?(name)
1237
- %i[nil? ! is_a? kind_of? instance_of? == != === =~ match? key? has_key? empty? any?
1277
+ %i[nil? ! is_a? kind_of? instance_of? == != === =~ !~ match? key? has_key? include? member? empty? any?
1238
1278
  none? respond_to? nan? finite?].include?(name)
1239
1279
  end
1240
1280
 
@@ -1245,15 +1285,19 @@ module Rigor
1245
1285
  when :instance_of? then analyse_class_predicate(node, scope, exact: true)
1246
1286
  when :==, :!= then analyse_equality_predicate(node, scope, equality: name)
1247
1287
  when :=== then analyse_case_equality_predicate(node, scope)
1248
- when :=~ then analyse_regex_match_predicate(node, scope)
1288
+ when :=~, :!~ then dispatch_regex_match_predicate(node, scope, name)
1249
1289
  when :match? then analyse_whole_regex_match_predicate(node, scope)
1250
- when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
1290
+ when :key?, :has_key?, :include?, :member? then analyse_key_presence_predicate(node, scope)
1251
1291
  when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
1252
1292
  when :respond_to? then analyse_respond_to_predicate(node, scope)
1253
1293
  when :nan?, :finite? then analyse_float_class_predicate(node, scope, name)
1254
1294
  end
1255
1295
  end
1256
1296
 
1297
+ def dispatch_regex_match_predicate(node, scope, name)
1298
+ name == :=~ ? analyse_regex_match_predicate(node, scope) : analyse_regex_not_match_predicate(node, scope)
1299
+ end
1300
+
1257
1301
  # T3 (template-corpora survey) — `recv.respond_to?(sym)` truthy edge narrows `recv`
1258
1302
  # non-nil. `nil.respond_to?(m)` is `false` for every method `m` that `NilClass` does not
1259
1303
  # define, so a truthy `respond_to?` proves the receiver was not `nil` UNLESS the queried
@@ -1419,14 +1463,16 @@ module Rigor
1419
1463
  # `h[:foo]` nil-typed). The falsey edge is left unchanged — "key absent" is the
1420
1464
  # conservative no-op. Only literal `Symbol`/`String` arguments and
1421
1465
  # `LocalVariableReadNode`/`InstanceVariableReadNode` receivers narrow; everything else
1422
- # (Dynamic, `Nominal[Hash]`, method-chain receivers, dynamic keys) bails to no
1423
- # narrowing.
1466
+ # (Dynamic, `Nominal[Hash]`, method-chain receivers) bails to no narrowing. A non-literal
1467
+ # key goes to {#analyse_computed_key_presence_predicate} (issue #1703).
1424
1468
  def analyse_key_presence_predicate(node, scope)
1425
1469
  return nil if node.arguments.nil?
1426
1470
  return nil unless node.arguments.arguments.size == 1
1427
1471
 
1428
- key = static_hash_key(node.arguments.arguments.first)
1429
- return nil if key.nil?
1472
+ # A literal key promotes a shape's optional key only for `key?` / `has_key?`; `include?` / `member?` reach
1473
+ # the computed-key guard alone, so a String or Array receiver's `include?` keeps its own narrowings.
1474
+ key = static_hash_key(node.arguments.arguments.first) if %i[key? has_key?].include?(node.name)
1475
+ return analyse_computed_key_presence_predicate(node, scope) if key.nil?
1430
1476
 
1431
1477
  case node.receiver
1432
1478
  when Prism::LocalVariableReadNode
@@ -1436,6 +1482,17 @@ module Rigor
1436
1482
  end
1437
1483
  end
1438
1484
 
1485
+ # Issue #1703 — `H.key?(k)` / `has_key?` / `include?` / `member?` with a non-literal key (a local, an ivar,
1486
+ # or a reader chain such as `prop.column_type`) on a closed hash shape held in a local, an ivar or a
1487
+ # constant. The truthy edge records the guard, and a later `H[k]` of the same receiver and structurally the
1488
+ # same key drops the miss `nil` ({KeyPresenceGuard}). The falsey edge is the no-op: an
1489
+ # absent computed key says nothing a shape can record. Nil when the guard does not apply, so a collection's
1490
+ # `include?` still reaches the membership and String predicates.
1491
+ def analyse_computed_key_presence_predicate(node, scope)
1492
+ truthy = KeyPresenceGuard.record(node, scope)
1493
+ truthy && [truthy, scope]
1494
+ end
1495
+
1439
1496
  def key_presence_scopes(name, key, scope, reader:, writer:)
1440
1497
  current = scope.public_send(reader, name)
1441
1498
  return nil if current.nil?
@@ -1532,7 +1589,9 @@ module Rigor
1532
1589
  #
1533
1590
  # Returns nil (no narrowing) when the receiver / argument pair does not resolve to exactly one
1534
1591
  # regex pattern we can count (see {#regex_match_pattern}).
1535
- def analyse_regex_match_predicate(node, scope)
1592
+ #
1593
+ # `refine: false` leaves out the whole-receiver refinement, which names a String.
1594
+ def analyse_regex_match_predicate(node, scope, refine: true)
1536
1595
  return nil if node.arguments.nil?
1537
1596
  return nil unless node.arguments.arguments.size == 1
1538
1597
 
@@ -1552,10 +1611,96 @@ module Rigor
1552
1611
  # match-global logic above. A successful `str =~ /\A\d+\z/` proves the WHOLE string
1553
1612
  # matched, so the string operand narrows to the imported refinement the anchored pattern
1554
1613
  # names. The falsey edge is untouched: a failed match proves nothing about the shape.
1555
- truthy = apply_whole_receiver_refinement(truthy, node.receiver, arg, pattern.source, scope)
1614
+ truthy = apply_whole_receiver_refinement(truthy, node.receiver, arg, pattern.source, scope) if refine
1556
1615
  [truthy, falsey]
1557
1616
  end
1558
1617
 
1618
+ # Issue #1379 — `subject !~ pattern` is `Kernel#!~`, which returns `!(subject =~ pattern)`. It runs the same
1619
+ # match in the caller's frame, so its edges are {#analyse_regex_match_predicate}'s swapped: the falsey edge is
1620
+ # the match edge (`$~` a `MatchData`, an unconditional `$N` a `String`, and the whole-receiver refinement), the
1621
+ # truthy edge the no-match edge (`$~` and `$N` nil). The same patterns are recognised and the same ones
1622
+ # declined (a non-regex operand, an ambiguous constant, `//x`).
1623
+ #
1624
+ # Unlike `=~`'s, an edge here is narrowed only when the receiver proves what the frame holds on it
1625
+ # ({#not_match_receiver_edges}). An edge it cannot vouch for keeps the match globals forgotten, since the call
1626
+ # may still have rebound them. The refinement is kept only when the local it would narrow is known to be a
1627
+ # String on the match edge ({#refinable_operand?}).
1628
+ def analyse_regex_not_match_predicate(node, scope)
1629
+ edges = analyse_regex_match_predicate(node, scope, refine: refinable_operand?(node, scope))
1630
+ return nil if edges.nil?
1631
+
1632
+ match, no_match = edges
1633
+ forgotten = scope.forget_match_globals
1634
+ case not_match_receiver_edges(node, scope)
1635
+ when :both then [no_match, match]
1636
+ when :match then [forgotten, match]
1637
+ else [forgotten, forgotten]
1638
+ end
1639
+ end
1640
+
1641
+ # Which edges of `recv !~ pattern` the receiver lets {#analyse_regex_not_match_predicate} narrow. `Kernel#!~`
1642
+ # dispatches `=~` on the receiver:
1643
+ #
1644
+ # - A Regexp receiver (the pattern on the left) runs `Regexp#=~`, which sets `$~` on both outcomes, a `nil`
1645
+ # argument included: `:both`.
1646
+ # - `String#=~` and `Symbol#=~` do the same: `:both` when every member of the receiver's type is a String or a
1647
+ # Symbol.
1648
+ # - `NilClass#=~` answers nil and leaves `$~` alone, so a nil receiver reaches the no-match edge with the
1649
+ # previous match still bound; it never reaches the match edge. A `Dynamic` receiver may be nil at run time
1650
+ # whatever its static facet says: `:match` when every member is a String, a Symbol, nil, or a `Dynamic`
1651
+ # that is untyped or whose facet holds only those.
1652
+ # - Any other receiver may define `=~` in Ruby, which binds `$~` in its own frame, not the caller's: nil.
1653
+ #
1654
+ # A safe-navigation `recv&.!~(pattern)` answers nil without matching on a nil receiver, so it declines too.
1655
+ def not_match_receiver_edges(node, scope)
1656
+ return nil if node.receiver.nil? || node.safe_navigation?
1657
+ return :both if regex_operand_pattern(node.receiver, scope).is_a?(RegexMatchPattern)
1658
+
1659
+ kinds = not_match_member_kinds(scope.type_of(node.receiver), scope)
1660
+ return :both if kinds.all? { |kind| %i[string symbol].include?(kind) }
1661
+
1662
+ :match if kinds.all? { |kind| %i[string symbol nil dynamic].include?(kind) }
1663
+ end
1664
+
1665
+ # Whether the local operand {#apply_whole_receiver_refinement} would narrow holds only Strings and nil, which
1666
+ # the match edge excludes. The refinement names a String, so a member that may be a Symbol — a Symbol, or an
1667
+ # untyped `Dynamic` that `Kernel#!~` accepts as the receiver or `Regexp#=~` as the argument — keeps it out:
1668
+ # `return if raw !~ /\A\d+\z/` on an untyped `raw` must not make `raw.to_proc` an undefined method.
1669
+ def refinable_operand?(node, scope)
1670
+ operand = [node.receiver, *node.arguments&.arguments].find { |o| o.is_a?(Prism::LocalVariableReadNode) }
1671
+ type = operand && scope.local(operand.name)
1672
+ return true if type.nil?
1673
+
1674
+ not_match_member_kinds(type, scope).all? { |kind| %i[string nil].include?(kind) }
1675
+ end
1676
+
1677
+ def not_match_member_kinds(type, scope)
1678
+ (type.is_a?(Type::Union) ? type.members : [type]).map { |member| not_match_member_kind(member, scope) }
1679
+ end
1680
+
1681
+ # `:string` or `:symbol` for a member {.narrow_not_class} proves a String or a Symbol (a value or refinement of
1682
+ # either, or a class the environment orders below it; a project subclass it cannot order reads as neither),
1683
+ # `:nil`, `:dynamic` for a `Dynamic` whose facet is untyped or holds only Strings, Symbols and nil, or nil for
1684
+ # anything else.
1685
+ def not_match_member_kind(member, scope)
1686
+ return not_match_dynamic_kind(member, scope) if member.is_a?(Type::Dynamic)
1687
+ return :nil if narrow_nil(member) == member && !member.is_a?(Type::Bot)
1688
+
1689
+ environment = scope.environment
1690
+ { "String" => :string, "Symbol" => :symbol }.each do |class_name, kind|
1691
+ return kind if narrow_not_class(member, class_name, environment: environment, scope: scope).is_a?(Type::Bot)
1692
+ end
1693
+ nil
1694
+ end
1695
+
1696
+ def not_match_dynamic_kind(member, scope)
1697
+ facet = member.static_facet
1698
+ return :dynamic if facet.is_a?(Type::Top)
1699
+
1700
+ facet_kinds = not_match_member_kinds(facet, scope)
1701
+ :dynamic if facet_kinds.all? { |kind| %i[string symbol nil].include?(kind) }
1702
+ end
1703
+
1559
1704
  # `str.match?(/\A\d+\z/)` — `String#match?` is a pure boolean predicate whose truthy edge
1560
1705
  # proves a property of the ENTIRE receiver. When the argument is a fully `\A…\z`-anchored
1561
1706
  # single-char-class pattern recognised by {RegexRefinement.for_whole_pattern}, narrow the
@@ -2239,9 +2384,10 @@ module Rigor
2239
2384
  end
2240
2385
 
2241
2386
  # Issue #1429 — `$stdout.is_a?(StringIO)`, `STDOUT.kind_of?(StringIO)`: a global or constant receiver
2242
- # narrows as a local does ({#class_predicate_scopes}), with the pre-guard type recorded ({#narrow_slot}).
2387
+ # narrows as a local does ({#class_predicate_scopes}), with the pre-guard type recorded ({#narrow_slot}). So
2388
+ # does an instance variable (#1446, {#narrow_guarded_slot}).
2243
2389
  def analyse_class_predicate_on_slot(receiver, scope, class_name, exact)
2244
- slot = receiver_slot(receiver, scope, kinds: GLOBAL_AND_CONSTANT)
2390
+ slot = receiver_slot(receiver, scope, kinds: CLASS_GUARD_SLOT_KINDS)
2245
2391
  return nil if slot.nil?
2246
2392
 
2247
2393
  class_slot_scopes(scope, slot, class_name, exact: exact)
@@ -2250,11 +2396,9 @@ module Rigor
2250
2396
  def class_slot_scopes(scope, slot, class_name, exact:)
2251
2397
  environment = scope.environment
2252
2398
  truthy = narrow_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
2253
- [
2254
- narrow_slot(scope, slot, truthy),
2255
- narrow_slot(scope, slot, narrow_not_class(slot.current, class_name, exact: exact,
2256
- environment: environment, scope: scope))
2257
- ]
2399
+ falsey = narrow_not_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
2400
+ truthy_scope = record_bot_guard(narrow_guarded_slot(scope, slot, truthy), slot, truthy, [class_name])
2401
+ [truthy_scope, narrow_guarded_slot(scope, slot, falsey)]
2258
2402
  end
2259
2403
 
2260
2404
  # The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
@@ -2429,12 +2573,12 @@ module Rigor
2429
2573
  end
2430
2574
 
2431
2575
  # Issue #1429 — `StringIO === $stdout`, `StringIO === STDOUT`: the class-constant receiver form on a global or
2432
- # constant argument. The Range / Regexp receivers stay local-only.
2576
+ # constant argument, and on an instance variable (#1446). The Range / Regexp receivers stay local-only.
2433
2577
  def analyse_case_equality_on_slot(receiver, arg, scope)
2434
2578
  class_name = lexical_class_name(receiver, scope)
2435
2579
  return nil if class_name.nil?
2436
2580
 
2437
- slot = receiver_slot(arg, scope, kinds: GLOBAL_AND_CONSTANT)
2581
+ slot = receiver_slot(arg, scope, kinds: CLASS_GUARD_SLOT_KINDS)
2438
2582
  return nil if slot.nil?
2439
2583
 
2440
2584
  class_slot_scopes(scope, slot, class_name, exact: false)
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../reflection"
4
+ require_relative "../analysis/dependency_recorder"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # Issue #1697 — the modules the project mixes into `Object`, and what they say about a method name.
9
+ #
10
+ # A top-level `include M` is `main.include`, which is `Object.include`, so M's instance methods exist on
11
+ # every object: bare top-level calls (`main` is an `Object`) and calls on any receiver whose class
12
+ # descends from `Object`. `ScopeIndexer` records that statement under its own owner key
13
+ # ({TOPLEVEL_INCLUDE_KEY}), and an `include M` inside `class Object ... end` on `Object`. The RBS
14
+ # environment knows nothing of either: `Object`'s RBS ancestry is core's, so a direct lookup on an
15
+ # RBS-known receiver misses every one of M's methods. This module is the one place that asks the
16
+ # project's `Object` edges instead, for the
17
+ # `call.unresolved-toplevel` and `call.undefined-method` silences in `Analysis::CheckRules`. Nothing
18
+ # types a call through these edges yet: what Ruby reaches first, even for a bare top-level call, depends
19
+ # on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def` on `main`),
20
+ # so typing is issue #1715's.
21
+ #
22
+ # The edges are read through `Scope::ResolutionChain`, the two owners' instance chains, whose search
23
+ # files the ADR-46 class edge of every project entry it passes, the declaring files among them (a file
24
+ # that wrote a top-level `include` is one), so a consumer re-checks when the include that answered for
25
+ # it is edited away. A project that never reopens `Object` and writes no top-level `include` returns
26
+ # before reading anything. The question is a union over the chain, not "which definer does Ruby call":
27
+ # any entry that may define the name answers, so the chain's `settle` verdict is not asked.
28
+ module ObjectMixins
29
+ OWNER = "Object"
30
+ # The owner key `ScopeIndexer` files a top-level `include` under, beside `Object`'s own key, so that
31
+ # no ancestry walk that types a call reaches it (see `ScopeIndexer#toplevel_include_owner`).
32
+ TOPLEVEL_INCLUDE_KEY = "<toplevel-include>"
33
+
34
+ module_function
35
+
36
+ # How the project's `Object` mixins answer `method_name`, nearest edge first:
37
+ #
38
+ # - `[:rbs, module_name, definition]` — an RBS-declared module declares it; `definition` is its
39
+ # `RBS::Definition::Method`. `:private_rbs` is the same for a private declaration, which a bare call
40
+ # reaches and a call with a receiver does not.
41
+ # - `[:source, module_name]` — a project module (or `Object` itself) defines it.
42
+ # - `[:singleton_function, module_name]` — an RBS-declared module declares it only as a singleton method.
43
+ # Core declares `Math`'s functions that way (`def self.sqrt`) although they are `module_function`s,
44
+ # whose private instance copies a bare call reaches after `include Math`. RBS cannot tell such a
45
+ # function from a plain `def self.`, so the answer silences a bare call only.
46
+ # - `[:opaque, module_name]` — a module neither RBS nor the project declares. Its surface cannot be
47
+ # enumerated, so it may define the name.
48
+ # - `[:truncated, "Object"]` — the chain was cut at its budget with no answer before the cut: a definer
49
+ # may sit past it, which is uncertainty, not absence.
50
+ # - `nil` — no edge, or every edge names a module known not to declare it.
51
+ #
52
+ # `skip:` names the kinds a caller cannot use; an entry of one of them is passed over and the search
53
+ # goes on to the next, so the answer is a union over the chain for those callers rather than the
54
+ # nearest entry only.
55
+ def answer(scope, method_name, skip: NO_SKIP)
56
+ return nil if scope.nil?
57
+
58
+ # ADR-46 — the answer depends on whether a top-level `include` exists anywhere, so a consumer checked
59
+ # while none did must re-check when one appears. The key appears as a declared "class" of the file that
60
+ # writes it (`discovered_class_sources`), which is what this name-keyed existence edge listens for.
61
+ if Analysis::DependencyRecorder.active?
62
+ Analysis::DependencyRecorder.read_last_segment(:class, TOPLEVEL_INCLUDE_KEY)
63
+ end
64
+ EDGE_OWNERS.each do |owner|
65
+ next unless scope.known_user_class?(owner)
66
+
67
+ found = answer_on_chain(scope, owner, method_name, skip)
68
+ return found if found
69
+ end
70
+ nil
71
+ end
72
+
73
+ # Where `Object`'s project edges are recorded: a top-level `include` under the indexer's own key, and
74
+ # `Object` itself for `include M` in `class Object` (and `Object`'s own definitions).
75
+ EDGE_OWNERS = [TOPLEVEL_INCLUDE_KEY, OWNER].freeze
76
+ private_constant :EDGE_OWNERS
77
+
78
+ def answer_on_chain(scope, owner, method_name, skip)
79
+ chain = Scope::ResolutionChain.for(scope, owner, :instance, :methods)
80
+ found = chain.search(scope, side: :instance) do |entry|
81
+ entry_answer = answer_for_entry(scope, entry, method_name)
82
+ entry_answer unless entry_answer.nil? || skip.include?(entry_answer.first)
83
+ end
84
+ return found if found
85
+
86
+ chain.truncated? ? [:truncated, owner] : nil
87
+ end
88
+ private_class_method :answer_on_chain
89
+
90
+ NO_SKIP = [].freeze
91
+ # An explicit receiver reaches neither a private method (a `module_function`'s instance copy among
92
+ # them) nor, on #746's
93
+ # reading of an ancestor's unknown include (the receiver's own includes only), a module whose surface
94
+ # is unknown: one top-level `include` of an undeclared module would otherwise silence
95
+ # `call.undefined-method` on every receiver in the project.
96
+ EXPLICIT_RECEIVER_SKIP = %i[private_rbs singleton_function opaque].freeze
97
+ private_constant :NO_SKIP, :EXPLICIT_RECEIVER_SKIP
98
+
99
+ # Whether some `Object` mixin may define `method_name`: the question the silences ask. A bare
100
+ # top-level call (`receiver: :implicit`, `call.unresolved-toplevel`) counts every answer, an unknown
101
+ # module's included: such an include is the likeliest source of a bare name, and silencing a typo
102
+ # there is the cheaper error. A call with a receiver (`receiver: :explicit`, `call.undefined-method`)
103
+ # counts only a declared, public definition.
104
+ def may_define?(scope, method_name, receiver:)
105
+ skip = receiver == :explicit ? EXPLICIT_RECEIVER_SKIP : NO_SKIP
106
+ !answer(scope, method_name, skip: skip).nil?
107
+ end
108
+
109
+ # Whether a receiver of `class_name` reaches `Object`'s mixins at all: `Object` itself, or an RBS
110
+ # class the environment orders below it. A `BasicObject` subclass does not; a class the environment
111
+ # cannot order is not claimed.
112
+ def reaches_object?(scope, class_name)
113
+ name = class_name.to_s.delete_prefix("::")
114
+ return true if name == OWNER
115
+
116
+ environment = scope&.environment
117
+ !environment.nil? && environment.class_ordering(name, OWNER) == :subclass
118
+ end
119
+
120
+ # A project entry (`Object` itself, or a project module mixed into it) answers when it defines the name;
121
+ # an external one when RBS declares the name on one of the names its spelling can denote (as an instance
122
+ # method, or as a module's singleton function), and is opaque when none of those names is declared at
123
+ # all.
124
+ def answer_for_entry(scope, entry, method_name)
125
+ unless entry.external?
126
+ return source_defines?(scope, entry.name, method_name) ? [:source, entry.name] : nil
127
+ end
128
+
129
+ entry.candidates.each do |candidate|
130
+ definition = Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
131
+ return [private_definition?(definition) ? :private_rbs : :rbs, candidate, definition] if definition
132
+ return [:singleton_function, candidate] if singleton_function?(scope, candidate, method_name)
133
+ end
134
+ return nil if entry.candidates.any? { |candidate| declared?(scope, candidate) }
135
+
136
+ [:opaque, entry.raw.to_s]
137
+ end
138
+ private_class_method :answer_for_entry
139
+
140
+ def private_definition?(definition)
141
+ definition.respond_to?(:accessibility) && definition.accessibility == :private
142
+ end
143
+ private_class_method :private_definition?
144
+
145
+ def singleton_function?(scope, name, method_name)
146
+ environment = scope.environment
147
+ return false if environment.nil? || !environment.rbs_module?(name)
148
+
149
+ definition = Rigor::Reflection.singleton_method_definition(name, method_name, scope: scope)
150
+ # Declared on the module itself: its singleton class also answers every `Module` method
151
+ # (`include`, `name`), which no `include` of it brings to the top level.
152
+ !definition.nil? && definition.defined_in.to_s.delete_prefix("::") == name
153
+ end
154
+ private_class_method :singleton_function?
155
+
156
+ # Asked of both tables a project method lands in, as `RbsDispatch#source_declares_through_ancestors?`
157
+ # does: `discovered_methods` withholds a plain `def` written in another file, which is exactly where a
158
+ # top-level `include` usually finds its module. The chain already expands each module's own ancestry, so
159
+ # one entry's own tables are asked.
160
+ def source_defines?(scope, name, method_name)
161
+ scope.discovered_method?(name, method_name, :instance) || !scope.user_def_for(name, method_name).nil?
162
+ end
163
+ private_class_method :source_defines?
164
+
165
+ # The same "is this mixin's surface enumerable" reading `Analysis::CheckRules#known_mixin?` gives a
166
+ # class's own includes (#746): a project class or module, or an RBS declaration Rigor did not
167
+ # synthesize to keep a broken signature buildable.
168
+ def declared?(scope, name)
169
+ return true if scope.discovered_classes.key?(name)
170
+ return false unless Rigor::Reflection.rbs_class_known?(name, scope: scope)
171
+
172
+ loader = scope.environment&.rbs_loader
173
+ return true if loader.nil? || !loader.respond_to?(:synthesized_type_names)
174
+
175
+ !loader.synthesized_type_names.include?(name)
176
+ end
177
+ private_class_method :declared?
178
+ end
179
+ end
180
+ end
@@ -245,13 +245,16 @@ module Rigor
245
245
  tables[:discovered_classes] = classes unless classes.empty?
246
246
  DISCOVERY_FIELD.each do |index_key, field|
247
247
  table = def_index.fetch(index_key)
248
- tables[field] = table unless table.empty?
248
+ tables[field] = table unless table.empty? && !Scope::DiscoveryIndex.paired?(field)
249
249
  end
250
- tables
251
- rescue StandardError
250
+ # ADR-119 WD1 — the pairs travel together: a pair is dropped only when both halves are empty. A sibling whose
251
+ # member this collector does not seed completes to that member's empty value, which the base scope holds.
252
+ Scope::DiscoveryIndex.compact_pairs(tables.merge(def_index.fetch(:siblings)))
253
+ rescue StandardError => e
252
254
  # Discovery is best-effort; a malformed corner of the project must not crash the
253
- # protection scan. Without discovery the collector simply resolves fewer call sites.
254
- {}
255
+ # protection scan. Without discovery the collector simply resolves fewer call sites. A broken
256
+ # walk contract is not such a corner, and an empty seed would hide it.
257
+ e.is_a?(DeclarationWalk::ContractError) ? raise : {}
255
258
  end
256
259
 
257
260
  DISCOVERY_FIELD = {
@@ -260,7 +263,7 @@ module Rigor
260
263
  def_sources: :discovered_def_sources,
261
264
  superclasses: :discovered_superclasses,
262
265
  header_nestings: :discovered_header_nestings,
263
- includes: :discovered_includes,
266
+ includes: :discovered_includes, unpositioned_mixins: :unpositioned_mixins,
264
267
  class_sources: :discovered_class_sources,
265
268
  method_visibilities: :discovered_method_visibilities,
266
269
  methods: :discovered_methods,
@@ -359,9 +362,7 @@ module Rigor
359
362
  end
360
363
 
361
364
  def lookup_def(scope, class_name, method, kind)
362
- table = kind == :singleton ? scope.discovered_singleton_def_nodes : scope.discovered_def_nodes
363
- per_class = table[class_name]
364
- per_class && per_class[method]
365
+ scope.def_node_slot(class_name, method, kind)
365
366
  end
366
367
 
367
368
  # The leading required-positional parameters — the ones a call's leading positional arguments
@@ -7,9 +7,9 @@ module Rigor
7
7
  # ADR-17 § "Inference contract" — project-wide patched-method registry populated by the pre-eval
8
8
  # pre-pass (slice 2) from the user's `.rigor.yml` `pre_eval:` list.
9
9
  #
10
- # Each entry records one `def` declaration the pre-pass observed inside a class / module body. The
11
- # dispatcher's `try_project_patched_method` tier consults this registry between the plugin tier and
12
- # the dependency-source tier so project-side `lib/core_ext/string_extensions.rb` patches are visible
10
+ # Each entry records one `def`, `alias` or `alias_method` declaration the pre-pass observed inside a class /
11
+ # module body. The dispatcher's `try_project_patched_method` tier consults this registry between the plugin
12
+ # tier and the dependency-source tier so project-side `lib/core_ext/string_extensions.rb` patches are visible
13
13
  # to cross-file dispatch.
14
14
  #
15
15
  # The dispatcher answers `Dynamic[T]` (with a heuristic static facet) when `Entry#return_type` is
@@ -22,8 +22,13 @@ module Rigor
22
22
  # the {Analysis::DependencySourceInference::ReturnTypeHeuristic}-extracted static facet (a
23
23
  # `Rigor::Type::*`) or `nil` when the heuristic declined. The dispatcher wraps a non-nil
24
24
  # `return_type` in `Dynamic[T]`; a `nil` `return_type` falls back to `Dynamic[top]`.
25
- Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type) do
26
- def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil)
25
+ #
26
+ # Issue #1702 — an `alias new old` / `alias_method :new, :old` in a patch is recorded too. When a `def` or
27
+ # alias earlier in the `pre_eval:` files binds `old`, the alias's entry copies that entry's `return_type` (and
28
+ # its `alias_of`); otherwise `alias_of` names `old`, a method the class had before the patch, and the
29
+ # dispatcher answers the alias with what that method answers. `alias_of` is nil for a `def`.
30
+ Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type, :alias_of) do
31
+ def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil, alias_of: nil)
27
32
  super
28
33
  end
29
34
  end