rigortype 0.4.1 → 0.4.3

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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  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/11-sig-gen.md +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -1
@@ -11,6 +11,7 @@ require_relative "dynamic_origin"
11
11
  require_relative "flow_tracer"
12
12
  require_relative "void_tail_summary"
13
13
  require_relative "method_dispatcher/call_context"
14
+ require_relative "method_dispatcher/argument_nodes"
14
15
  require_relative "method_dispatcher/constant_folding"
15
16
  require_relative "method_dispatcher/literal_string_folding"
16
17
  require_relative "method_dispatcher/shape_dispatch"
@@ -18,6 +19,7 @@ require_relative "method_dispatcher/data_folding"
18
19
  require_relative "method_dispatcher/struct_folding"
19
20
  require_relative "method_dispatcher/rbs_dispatch"
20
21
  require_relative "method_dispatcher/iterator_dispatch"
22
+ require_relative "method_dispatcher/arithmetic_sequence_elements"
21
23
  require_relative "method_dispatcher/reduce_folding"
22
24
  require_relative "method_dispatcher/block_folding"
23
25
  require_relative "method_dispatcher/array_to_h_folding"
@@ -38,6 +40,7 @@ require_relative "method_dispatcher/kernel_dispatch"
38
40
  require_relative "method_dispatcher/universal_object_dispatch"
39
41
  require_relative "method_dispatcher/singleton_mixin_dispatch"
40
42
  require_relative "method_dispatcher/method_folding"
43
+ require_relative "method_dispatcher/optional_count"
41
44
 
42
45
  module Rigor
43
46
  module Inference
@@ -101,9 +104,19 @@ module Rigor
101
104
  # the tier commits the buffer only if it goes on to answer. See {#try_composite_receiver}.
102
105
  def resolve(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
103
106
  block_type: nil, environment: nil,
104
- call_node: nil, scope: nil, plugin_typed_sink: nil)
107
+ call_node: nil, scope: nil, plugin_typed_sink: nil, skip_project_patched: false)
105
108
  return nil if receiver_type.nil?
106
109
 
110
+ # Issue #1393 — a `nil` count on `min` / `max` / `min_by` / `max_by` is the no-count call.
111
+ nil_count = OptionalCount.split(receiver_type, method_name, arg_types, block: !block_type.nil?)
112
+ if nil_count
113
+ return resolve_nil_count(nil_count.first, receiver_type: receiver_type, method_name: method_name,
114
+ block_type: block_type, environment: environment,
115
+ call_node: call_node, scope: scope,
116
+ plugin_typed_sink: plugin_typed_sink,
117
+ skip_project_patched: skip_project_patched)
118
+ end
119
+
107
120
  # Build the call context once and thread it — unchanged — through every tier
108
121
  # (`_DispatchTier#try_dispatch`). The dispatcher's own private fallback tiers still read
109
122
  # the positional locals below; only the tier modules consume the context object.
@@ -205,7 +218,10 @@ module Rigor
205
218
  # the pre-pass populated `ProjectPatchedMethods` with the `(class, method, kind)` triple;
206
219
  # this tier surfaces it as `Dynamic[top]` so the patched call resolves cross-file without
207
220
  # `call.undefined-method`.
208
- patched_result = try_project_patched_method(receiver_type, method_name, environment)
221
+ patched_result =
222
+ unless skip_project_patched
223
+ try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
224
+ end
209
225
  if patched_result
210
226
  scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
211
227
  return patched_result
@@ -284,6 +300,20 @@ module Rigor
284
300
  try_composite_receiver(context)
285
301
  end
286
302
 
303
+ # The `nil` part of an optional count dispatches as the no-argument call, the rest as written; a count
304
+ # that is both answers the union of the two, or declines when either declines. See {OptionalCount}.
305
+ def resolve_nil_count(rest, **common)
306
+ return resolve(arg_types: [], **common) if rest.nil?
307
+
308
+ # The counted leg runs first: a leg's plugin answer records on the scope (or buffers in the sink, #1101),
309
+ # and a declining counted leg must not leave the no-argument leg's record behind for a call that answers nil.
310
+ counted = resolve(arg_types: [rest], **common)
311
+ return nil if counted.nil?
312
+
313
+ without = resolve(arg_types: [], **common)
314
+ without && Type::Combinator.union(without, counted)
315
+ end
316
+
287
317
  # ADR-100 WD4 — records the transitive `-> void` provenance when the called method is a
288
318
  # discovered project `def` whose {VoidTailSummary} chain ends in an author-declared `-> void`
289
319
  # leaf on the same owner. Guards cheapest-first: the internal dispatcher callers nil `scope` /
@@ -614,7 +644,7 @@ module Rigor
614
644
  # `def to_url; "hello"; end` patched onto `String` now resolves `s.to_url` to
615
645
  # `Dynamic[Nominal[String]]` instead of the pre-3a `Dynamic[Top]`. Falls back to
616
646
  # `Dynamic[Top]` when the heuristic declined (non-literal tail expression).
617
- def try_project_patched_method(receiver_type, method_name, environment)
647
+ def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
618
648
  registry = environment&.project_patched_methods
619
649
  return nil if registry.nil? || registry.empty?
620
650
 
@@ -624,9 +654,25 @@ module Rigor
624
654
  kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
625
655
  entry = registry.lookup(class_name: class_name, method_name: method_name, kind: kind)
626
656
  return nil if entry.nil?
627
- return Type::Combinator.untyped if entry.return_type.nil?
657
+ return Type::Combinator.dynamic(entry.return_type) if entry.return_type
658
+
659
+ aliased = project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
660
+ aliased ? Type::Combinator.dynamic(aliased) : Type::Combinator.untyped
661
+ end
628
662
 
629
- Type::Combinator.dynamic(entry.return_type)
663
+ # Issue #1702 — an alias in a `pre_eval:` patch whose old name no earlier patch binding defines
664
+ # (`alias old_plus +` on Integer, or `alias_method :orig_succ, :succ` ahead of the patch's own `def succ`)
665
+ # names the method the class had before the patch, so it answers what that method answers for the same
666
+ # arguments, behind the `Dynamic` every patched method carries. The re-entry skips this tier: a later patch
667
+ # `def` of the old name is not the body the alias copied, and the skip is also what ends an alias cycle. It
668
+ # carries no call node or scope, so it answers a type and records nothing at the call site.
669
+ def project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
670
+ return nil if entry.alias_of.nil?
671
+
672
+ resolve(
673
+ receiver_type: receiver_type, method_name: entry.alias_of, arg_types: arg_types,
674
+ block_type: block_type, environment: environment, skip_project_patched: true
675
+ )
630
676
  end
631
677
 
632
678
  # ADR-10 slice 2b-ii. Consults the per-run `Analysis::DependencySourceInference::Index`
@@ -847,7 +893,8 @@ module Rigor
847
893
  private_constant :STDLIB_SINGLETON_FOLDERS
848
894
 
849
895
  PRECISE_TIERS_TAIL = Ractor.make_shareable([
850
- MethodFolding, ReduceFolding, ArrayToHFolding, HashTransformKeysFolding, BlockFolding
896
+ MethodFolding, ReduceFolding, ArrayToHFolding, HashTransformKeysFolding, BlockFolding,
897
+ ArithmeticSequenceElements
851
898
  ].freeze)
852
899
  private_constant :PRECISE_TIERS_TAIL
853
900
 
@@ -1104,14 +1151,23 @@ module Rigor
1104
1151
  # `instance_of?(self)` later as the rule catalogue grows; for now only `class` is handled.
1105
1152
  def try_meta_introspection(receiver_type, method_name, arg_types = [], context = nil)
1106
1153
  case method_name
1107
- when :class then meta_class(receiver_type)
1154
+ when :class then meta_class(receiver_type, context&.environment)
1108
1155
  when :new then meta_new(receiver_type, arg_types, context)
1109
1156
  end
1110
1157
  end
1111
1158
 
1112
- def meta_class(receiver_type)
1159
+ def meta_class(receiver_type, environment = nil)
1113
1160
  case receiver_type
1114
- when Type::Nominal then Type::Combinator.singleton_of(receiver_type.class_name)
1161
+ when Type::Nominal
1162
+ # An instance of module `M` is an instance of some class that includes `M`, never of `M`
1163
+ # itself: `self.class` in `M`'s instance method is that includer, which no RBS type names.
1164
+ # `Dynamic[Class]` keeps the surface every class shares (`name`, `ancestors`) and answers
1165
+ # nothing for the includer's own singleton methods.
1166
+ if environment&.rbs_module?(receiver_type.class_name)
1167
+ return Type::Combinator.dynamic(Type::Combinator.nominal_of("Class"))
1168
+ end
1169
+
1170
+ Type::Combinator.singleton_of(receiver_type.class_name)
1115
1171
  when Type::Constant then constant_metaclass(receiver_type.value)
1116
1172
  end
1117
1173
  end
@@ -1140,7 +1196,7 @@ module Rigor
1140
1196
  set_lift = set_new_lift(receiver_type.class_name, arg_types)
1141
1197
  return set_lift if set_lift
1142
1198
 
1143
- hash_lift = hash_new_lift(receiver_type.class_name, arg_types)
1199
+ hash_lift = hash_new_lift(receiver_type.class_name, arg_types, context&.call_node)
1144
1200
  return hash_lift if hash_lift
1145
1201
 
1146
1202
  regexp_lift = regexp_new_lift(receiver_type.class_name, arg_types)
@@ -1415,9 +1471,15 @@ module Rigor
1415
1471
  # form (`Hash.new { |h, k| … }`) keep the bare `Nominal[Hash]` answer — the block's value
1416
1472
  # type is not available at this `:new` dispatch site, and conservatively leaving the read
1417
1473
  # as today's behaviour is precision-additive.
1418
- def hash_new_lift(class_name, arg_types)
1474
+ #
1475
+ # Issue #1812 — a brace-less keyword hash is `Hash#initialize`'s `capacity:` keyword, not the
1476
+ # default (`Hash.new(capacity: 10).default` is `nil`), so it is dropped before counting the
1477
+ # positionals. A braced hash (`Hash.new({ capacity: 10 })`) is a positional and stays the default.
1478
+ def hash_new_lift(class_name, arg_types, call_node = nil)
1419
1479
  return nil unless class_name == "Hash"
1420
- return nil unless arg_types.size == 1
1480
+
1481
+ arg_types = hash_new_positionals(arg_types, call_node)
1482
+ return nil unless arg_types&.size == 1
1421
1483
 
1422
1484
  default = arg_types.first
1423
1485
  return nil if default.nil?
@@ -1426,6 +1488,23 @@ module Rigor
1426
1488
  Type::Combinator.nominal_of("Hash", type_args: [Type::Combinator.untyped, value])
1427
1489
  end
1428
1490
 
1491
+ # `Hash.new`'s positional argument types: `arg_types` without a trailing brace-less keyword hash. A literal
1492
+ # send (`Hash.send(:new, capacity: 1)`) dispatches with the outer call node, whose arguments are the
1493
+ # selector symbol followed by `arg_types`' own, and is read through that offset. When the node's arguments
1494
+ # correspond otherwise, a sole hash argument may be the keyword hash, so the answer is nil (decline) and the
1495
+ # bare `Hash` stands; any other argument cannot be a keyword hash and is kept.
1496
+ def hash_new_positionals(arg_types, call_node)
1497
+ return arg_types[0...-1] if ArgumentNodes.keyword_hash_last?(call_node, arg_types)
1498
+
1499
+ arguments = call_node.respond_to?(:arguments) ? call_node.arguments&.arguments : nil
1500
+ return arg_types if arguments.nil? || arguments.size == arg_types.size
1501
+ if arguments.size == arg_types.size + 1 && arguments.first.is_a?(Prism::SymbolNode)
1502
+ return arguments.last.is_a?(Prism::KeywordHashNode) ? arg_types[0...-1] : arg_types
1503
+ end
1504
+
1505
+ arg_types.last.is_a?(Type::HashShape) ? nil : arg_types
1506
+ end
1507
+
1429
1508
  # `Range.new(b, e)` / `Range.new(b, e, excl)` — folds to `Constant[Range]` when both
1430
1509
  # endpoints are `Constant[Integer]` or both are `Constant[String]`, and the optional third
1431
1510
  # argument is a `Constant[true/false]`. Nil endpoints (beginless / endless ranges) are not
@@ -1555,13 +1634,16 @@ module Rigor
1555
1634
  # method definition, or selected overload does not provide statically declared block
1556
1635
  # parameter types. Callers MUST treat the empty array as "no information"; the binder
1557
1636
  # falls back to `Dynamic[Top]` for every parameter slot in that case.
1637
+ #
1638
+ # `call_node` lets the overload selector read the call's keyword arguments as keywords (#1727, #1737), as
1639
+ # the return path does; without it they are read positionally.
1558
1640
  def expected_block_param_types(receiver_type:, method_name:, arg_types:, environment: nil,
1559
- scope: nil)
1641
+ scope: nil, call_node: nil)
1560
1642
  return [] if receiver_type.nil?
1561
1643
 
1562
1644
  context = CallContext.build(
1563
1645
  receiver: receiver_type, method_name: method_name,
1564
- args: arg_types, environment: environment, scope: scope
1646
+ args: arg_types, environment: environment, scope: scope, call_node: call_node
1565
1647
  )
1566
1648
  iterator_result = IteratorDispatch.block_param_types(context)
1567
1649
  return iterator_result if iterator_result
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
+ require_relative "../source/literals"
4
5
  require_relative "../source/node_children"
5
6
 
6
7
  module Rigor
@@ -204,7 +205,7 @@ module Rigor
204
205
 
205
206
  # The Symbol a `:name` or `"name"` literal argument names, or nil.
206
207
  def symbol_argument_name(arg)
207
- arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
208
+ Source::Literals.symbol_or_string(arg)
208
209
  end
209
210
 
210
211
  private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name
@@ -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"
@@ -1273,7 +1274,7 @@ module Rigor
1273
1274
  end
1274
1275
 
1275
1276
  def simple_dispatch_name?(name)
1276
- %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?
1277
1278
  none? respond_to? nan? finite?].include?(name)
1278
1279
  end
1279
1280
 
@@ -1286,7 +1287,7 @@ module Rigor
1286
1287
  when :=== then analyse_case_equality_predicate(node, scope)
1287
1288
  when :=~, :!~ then dispatch_regex_match_predicate(node, scope, name)
1288
1289
  when :match? then analyse_whole_regex_match_predicate(node, scope)
1289
- 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)
1290
1291
  when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
1291
1292
  when :respond_to? then analyse_respond_to_predicate(node, scope)
1292
1293
  when :nan?, :finite? then analyse_float_class_predicate(node, scope, name)
@@ -1462,14 +1463,16 @@ module Rigor
1462
1463
  # `h[:foo]` nil-typed). The falsey edge is left unchanged — "key absent" is the
1463
1464
  # conservative no-op. Only literal `Symbol`/`String` arguments and
1464
1465
  # `LocalVariableReadNode`/`InstanceVariableReadNode` receivers narrow; everything else
1465
- # (Dynamic, `Nominal[Hash]`, method-chain receivers, dynamic keys) bails to no
1466
- # 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).
1467
1468
  def analyse_key_presence_predicate(node, scope)
1468
1469
  return nil if node.arguments.nil?
1469
1470
  return nil unless node.arguments.arguments.size == 1
1470
1471
 
1471
- key = static_hash_key(node.arguments.arguments.first)
1472
- 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?
1473
1476
 
1474
1477
  case node.receiver
1475
1478
  when Prism::LocalVariableReadNode
@@ -1479,6 +1482,17 @@ module Rigor
1479
1482
  end
1480
1483
  end
1481
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
+
1482
1496
  def key_presence_scopes(name, key, scope, reader:, writer:)
1483
1497
  current = scope.public_send(reader, name)
1484
1498
  return nil if current.nil?
@@ -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
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+
7
+ module Rigor
8
+ module Inference
9
+ module ProjectPatchedScanner
10
+ # Finds the `def`s of a `pre_eval:` file written under a definedness guard naming the same method
11
+ # (`def foo = 1 unless method_defined?(:foo)`): polyfills that run only when nothing bound the name before.
12
+ module DefinednessGuard
13
+ DEFINEDNESS_PREDICATES = %i[
14
+ method_defined? private_method_defined? public_method_defined? protected_method_defined? respond_to?
15
+ ].freeze
16
+ INSTANCE_METHOD_LISTS = %i[
17
+ instance_methods public_instance_methods private_instance_methods protected_instance_methods
18
+ ].freeze
19
+ private_constant :DEFINEDNESS_PREDICATES, :INSTANCE_METHOD_LISTS
20
+
21
+ module_function
22
+
23
+ # The `def` nodes (by identity) that sit on the "name not yet defined" side of a definedness guard naming the
24
+ # same method: `def foo = 1 unless method_defined?(:foo)`, or the same inside an `unless` / `if !` block.
25
+ def defs(root)
26
+ found = {}.compare_by_identity
27
+ collect_guarded(root, found)
28
+ found
29
+ end
30
+
31
+ def collect_guarded(node, found)
32
+ return unless node.is_a?(Prism::Node)
33
+
34
+ if node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
35
+ name, negated = definedness_guard(node.predicate)
36
+ mark_guarded_defs(undefined_branch(node, negated), name, found) if name
37
+ end
38
+ node.rigor_each_child { |child| collect_guarded(child, found) }
39
+ end
40
+ private_class_method :collect_guarded
41
+
42
+ # `[method_name, negated]` for a predicate like `method_defined?(:m)` / `!respond_to?(:m)`, else nil.
43
+ def definedness_guard(predicate)
44
+ predicate = predicate.body if predicate.is_a?(Prism::ParenthesesNode) && predicate.body
45
+ predicate = predicate.body.first if predicate.is_a?(Prism::StatementsNode) && predicate.body.size == 1
46
+ if predicate.is_a?(Prism::CallNode) && predicate.name == :! && predicate.receiver
47
+ name, negated = definedness_guard(predicate.receiver)
48
+ return name && [name, !negated]
49
+ end
50
+ name = guard_method_name(predicate)
51
+ name && [name, false]
52
+ end
53
+ private_class_method :definedness_guard
54
+
55
+ def guard_method_name(call)
56
+ return unless call.is_a?(Prism::CallNode)
57
+
58
+ listed = %i[include? member?].include?(call.name)
59
+ listed &&= call.receiver.is_a?(Prism::CallNode) && INSTANCE_METHOD_LISTS.include?(call.receiver.name)
60
+ return unless listed || DEFINEDNESS_PREDICATES.include?(call.name)
61
+
62
+ arg = call.arguments&.arguments&.first
63
+ arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
64
+ end
65
+ private_class_method :guard_method_name
66
+
67
+ # The branch that runs when the guarded name is NOT defined yet.
68
+ def undefined_branch(node, negated)
69
+ branch_when_true = node.is_a?(Prism::IfNode) ? node.statements : node.else_clause
70
+ branch_when_false = node.is_a?(Prism::IfNode) ? node.subsequent : node.statements
71
+ # The predicate is true when the name is defined (unless negated).
72
+ negated ? branch_when_true : branch_when_false
73
+ end
74
+ private_class_method :undefined_branch
75
+
76
+ def mark_guarded_defs(node, name, found)
77
+ return unless node.is_a?(Prism::Node)
78
+
79
+ found[node] = true if node.is_a?(Prism::DefNode) && node.name == name
80
+ node.rigor_each_child { |child| mark_guarded_defs(child, name, found) } unless node.is_a?(Prism::DefNode)
81
+ end
82
+ private_class_method :mark_guarded_defs
83
+ end
84
+ end
85
+ end
86
+ end
@@ -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
@@ -31,14 +36,14 @@ module Rigor
31
36
  attr_reader :by_key, :write_census
32
37
 
33
38
  # @param entries — flat list of declarations observed during the pre-pass.
34
- # First-write-wins on `(class_name, method_name, kind)` duplicates so the
39
+ # Last-write-wins on `(class_name, method_name, kind)` duplicates, as at runtime (ADR-17); the
35
40
  # `pre-eval.duplicate-declaration` diagnostic emission stays decoupled from registry behaviour.
36
41
  # @param write_census — issue #1367: the `pre_eval:` files' {GlobalWriteCensus}, which the `global.*` write
37
42
  # rules join with the project's, since a patch file is loaded ahead of the code it patches.
38
43
  def initialize(entries: [], write_census: GlobalWriteCensus::EMPTY)
39
44
  @by_key = entries.each_with_object({}) do |entry, acc|
40
45
  key = [entry.class_name, entry.method_name, entry.kind]
41
- acc[key] ||= entry
46
+ acc[key] = entry
42
47
  end.freeze
43
48
  @write_census = write_census.frozen? ? write_census : write_census.dup.freeze
44
49
  freeze