rigortype 0.4.3 → 0.4.5

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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +104 -15
  5. data/docs/manual/02-cli-reference.md +43 -12
  6. data/docs/manual/03-configuration.md +20 -7
  7. data/docs/manual/04-diagnostics.md +56 -3
  8. data/docs/manual/05-inspecting-types.md +2 -2
  9. data/docs/manual/06-baseline.md +1 -1
  10. data/docs/manual/07-plugins.md +3 -2
  11. data/docs/manual/08-skills.md +7 -0
  12. data/docs/manual/10-mcp-server.md +40 -4
  13. data/docs/manual/18-removing-dead-code.md +23 -2
  14. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  15. data/docs/manual/plugins/rigor-rspec.md +101 -1
  16. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  17. data/exe/rigor +12 -17
  18. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  19. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
  20. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
  21. data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
  22. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  23. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
  24. data/lib/rigor/analysis/check_rules.rb +274 -51
  25. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  26. data/lib/rigor/analysis/incremental.rb +61 -0
  27. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  28. data/lib/rigor/analysis/incremental_session.rb +270 -28
  29. data/lib/rigor/analysis/path_expansion.rb +3 -1
  30. data/lib/rigor/analysis/reachability/graph.rb +111 -14
  31. data/lib/rigor/analysis/reachability/scan.rb +305 -23
  32. data/lib/rigor/analysis/rule_catalog.rb +22 -4
  33. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  34. data/lib/rigor/analysis/run_cache_probe.rb +5 -3
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
  36. data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
  37. data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
  38. data/lib/rigor/analysis/runner.rb +102 -72
  39. data/lib/rigor/analysis/severity_stamp.rb +45 -1
  40. data/lib/rigor/analysis/worker_session.rb +69 -2
  41. data/lib/rigor/cache/descriptor.rb +79 -12
  42. data/lib/rigor/cache/engine_source.rb +3 -2
  43. data/lib/rigor/cache/incremental_snapshot.rb +39 -5
  44. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  45. data/lib/rigor/cache/store.rb +4 -3
  46. data/lib/rigor/cli/command.rb +3 -1
  47. data/lib/rigor/cli/docs_command.rb +3 -2
  48. data/lib/rigor/cli/doctor_command.rb +1 -1
  49. data/lib/rigor/cli/explain_command.rb +3 -2
  50. data/lib/rigor/cli/mcp_command.rb +1 -1
  51. data/lib/rigor/cli/plugin_command.rb +2 -1
  52. data/lib/rigor/cli/plugins_command.rb +2 -1
  53. data/lib/rigor/cli/sig_gen_command.rb +32 -11
  54. data/lib/rigor/cli/skill_command.rb +2 -1
  55. data/lib/rigor/cli/skill_describe.rb +3 -2
  56. data/lib/rigor/cli/unused_command.rb +10 -7
  57. data/lib/rigor/cli.rb +3 -2
  58. data/lib/rigor/config_audit.rb +163 -22
  59. data/lib/rigor/configuration/dependencies.rb +19 -11
  60. data/lib/rigor/configuration/severity_profile.rb +20 -2
  61. data/lib/rigor/configuration.rb +29 -3
  62. data/lib/rigor/effects/signature_sources.rb +3 -1
  63. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  64. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  65. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  66. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  67. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  68. data/lib/rigor/environment/rbs_loader.rb +67 -11
  69. data/lib/rigor/inference/block_call_timing.rb +12 -1
  70. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  71. data/lib/rigor/inference/captured_locals.rb +67 -13
  72. data/lib/rigor/inference/clause_widening.rb +90 -0
  73. data/lib/rigor/inference/definer_resolution.rb +101 -25
  74. data/lib/rigor/inference/expression_typer.rb +94 -9
  75. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  76. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  77. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  78. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  79. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
  80. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  81. data/lib/rigor/inference/method_dispatcher.rb +67 -3
  82. data/lib/rigor/inference/module_function_state.rb +213 -51
  83. data/lib/rigor/inference/narrowing.rb +235 -75
  84. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  85. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  86. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  87. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  88. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  89. data/lib/rigor/inference/project_seed_tables.rb +120 -0
  90. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  91. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  92. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  93. data/lib/rigor/inference/repeated_or_writes.rb +414 -1
  94. data/lib/rigor/inference/return_barrier.rb +16 -0
  95. data/lib/rigor/inference/scope_indexer.rb +1061 -122
  96. data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
  97. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  98. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  99. data/lib/rigor/inference/statement_evaluator.rb +512 -150
  100. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  101. data/lib/rigor/lens/anchor.rb +3 -3
  102. data/lib/rigor/lens/contract_reader.rb +60 -2
  103. data/lib/rigor/lens/document.rb +4 -3
  104. data/lib/rigor/lens/file_reader.rb +452 -73
  105. data/lib/rigor/lens/members.rb +81 -17
  106. data/lib/rigor/lens/run.rb +73 -4
  107. data/lib/rigor/lens.rb +2 -1
  108. data/lib/rigor/mcp/server.rb +63 -22
  109. data/lib/rigor/plugin/base.rb +3 -2
  110. data/lib/rigor/plugin/box_probe.rb +154 -7
  111. data/lib/rigor/plugin/io_boundary.rb +42 -6
  112. data/lib/rigor/plugin/isolation.rb +4 -3
  113. data/lib/rigor/project_environment.rb +6 -3
  114. data/lib/rigor/protection/discovery_seed.rb +9 -36
  115. data/lib/rigor/protection/mutation_cache.rb +8 -2
  116. data/lib/rigor/rooted_glob.rb +27 -0
  117. data/lib/rigor/scope/discovery_index.rb +5 -0
  118. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  119. data/lib/rigor/scope/resolution_chain.rb +40 -5
  120. data/lib/rigor/scope.rb +56 -8
  121. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  122. data/lib/rigor/sig_gen/classification.rb +4 -1
  123. data/lib/rigor/sig_gen/generator.rb +167 -22
  124. data/lib/rigor/sig_gen/layout_index.rb +40 -1
  125. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  126. data/lib/rigor/sig_gen/observation_collector.rb +137 -11
  127. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  128. data/lib/rigor/sig_gen/renderer.rb +13 -155
  129. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  130. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  131. data/lib/rigor/sig_gen/write_result.rb +3 -1
  132. data/lib/rigor/sig_gen/writer.rb +50 -35
  133. data/lib/rigor/signature_path_audit.rb +4 -2
  134. data/lib/rigor/type/constant.rb +10 -1
  135. data/lib/rigor/version.rb +1 -1
  136. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  137. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
  138. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  139. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  141. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  142. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  143. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  146. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  147. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  148. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  149. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  150. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  151. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
  152. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  153. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  154. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  155. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  156. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  157. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  158. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  159. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  160. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  164. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  166. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  167. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  168. data/sig/rigor/rooted_glob.rbs +7 -0
  169. data/sig/rigor/scope.rbs +4 -3
  170. data/skills/rigor-doctor/references/01-checks.md +23 -11
  171. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  172. metadata +16 -1
@@ -190,6 +190,35 @@ module Rigor
190
190
 
191
191
  STRING_FOLD_BYTE_LIMIT = 4096
192
192
 
193
+ # #2115 — the numeric twin of `STRING_FOLD_BYTE_LIMIT`. A folded Integer (and each Integer part of a
194
+ # folded Rational / Complex) is at most this many bits, the largest bit length whose decimal
195
+ # rendering still fits in `STRING_FOLD_BYTE_LIMIT` digits: `2 ** 13_605` folds, `2 ** 13_606`
196
+ # (bit length 13_607) does not. Computing `2 ** 100_000_000` takes Ruby milliseconds, but
197
+ # rendering it in decimal — which describing, sorting and printing the `Constant` does — takes
198
+ # minutes. `**` / `pow` / `<<` / `>>` also check their operands before invoking, so the big value
199
+ # is never computed.
200
+ INTEGER_FOLD_BIT_LIMIT = (STRING_FOLD_BYTE_LIMIT * Math.log2(10)).floor
201
+
202
+ # #2115 — the most elements a fold walks out of a `Constant<Range>` receiver. A method that
203
+ # enumerates the range (`to_a`, `to_set`, `all?`, `include?` on a String range, …) declines when
204
+ # the range holds more elements, or is endless (`1..Float::INFINITY`); a method that reads only
205
+ # the endpoints (`RANGE_ENDPOINT_METHODS`) still folds whatever the range's size.
206
+ RANGE_ENUMERATION_LIMIT = 1024
207
+
208
+ # The `Range` methods whose CRuby body reads the endpoints without walking the elements, so they
209
+ # stay foldable on a range past `RANGE_ENUMERATION_LIMIT`. `include?` / `member?` join them on a
210
+ # numeric range, where CRuby answers them by `cover?`.
211
+ RANGE_ENDPOINT_METHODS = Set[
212
+ :begin, :end, :exclude_end?, :cover?, :===, :size, :to_s, :inspect, :==, :eql?, :hash
213
+ ].freeze
214
+ RANGE_COVER_METHODS = Set[:include?, :member?].freeze
215
+ private_constant :RANGE_ENDPOINT_METHODS, :RANGE_COVER_METHODS
216
+
217
+ # A result `INTEGER_FOLD_BIT_LIMIT` keeps from becoming a `Constant`: the fold still knows its class,
218
+ # and answers `Nominal[class_name]` for it. Falling through to RBS instead would read `Integer#**`'s
219
+ # declared `Numeric`, on which `(2 ** 20_000).even?` is an undefined method.
220
+ WidenedResult = Data.define(:class_name)
221
+
193
222
  # Input cartesian product hard cap. Keeps fold cost bounded even when the receiver and argument are
194
223
  # both `Union[Constant…]`. 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-facing payoff
195
224
  # (a precise small enum) drops off fast past this range and CRuby method invocation cost adds up.
@@ -649,45 +678,41 @@ module Rigor
649
678
  def range_constant_unary(range, method_name)
650
679
  case method_name
651
680
  when :to_a, :entries then range_to_a_tuple(range)
652
- when :first, :min then range_endpoint_constant(range, :first)
653
- when :last, :max then range_endpoint_constant(range, :last)
654
- when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
655
- when :minmax then range_minmax_tuple(range)
681
+ # #2115 — `Range#min` / `#max` / `#minmax` / `#size` are O(1) on Integer endpoints in CRuby, so
682
+ # none of these materialises the range. The no-arg `first` / `last` read the endpoints, not the
683
+ # elements: `first` is `begin` and `last` is `end` whether the range is empty, reversed or
684
+ # exclusive (`(5..1).first` is 5, `(1...10).last` is 10). Only `min` / `max` depend on emptiness
685
+ # and exclusivity (`(5..1).min` is nil, `(1...10).max` is 9).
686
+ when :first then Type::Combinator.constant_of(range.begin)
687
+ when :last then Type::Combinator.constant_of(range.end)
688
+ when :min then Type::Combinator.constant_of(range.min)
689
+ when :max then Type::Combinator.constant_of(range.max)
690
+ when :count, :size, :length then Type::Combinator.constant_of(range.size)
691
+ when :minmax then Type::Combinator.tuple_of(*range.minmax.map { |v| Type::Combinator.constant_of(v) })
656
692
  # `range.sum` is closed-form (Gauss) for an integer range, so a huge range still costs O(1) and
657
- # yields a single Integer — no materialisation, no cap needed. Endless ranges are already excluded
658
- # by the Integer-endpoint guard in the caller.
659
- when :sum then Type::Combinator.constant_of(range.sum)
693
+ # yields a single Integer — no materialisation; only `INTEGER_FOLD_BIT_LIMIT` applies. Endless
694
+ # ranges are already excluded by the Integer-endpoint guard in the caller.
695
+ when :sum then range_sum_constant(range)
660
696
  end
661
697
  end
662
698
 
663
- def range_to_a_tuple(range)
664
- values = range.to_a
665
- return Type::Combinator.tuple_of if values.empty?
666
- return nil if values.size > RANGE_TO_A_LIMIT
699
+ # The Gauss sum of a range of large endpoints can still pass `INTEGER_FOLD_BIT_LIMIT`; it is then
700
+ # an `Integer` (both endpoints are).
701
+ def range_sum_constant(range)
702
+ sum = range.sum
703
+ return Type::Combinator.nominal_of("Integer") if oversized_number?(sum)
667
704
 
668
- Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
705
+ Type::Combinator.constant_of(sum)
669
706
  end
670
707
 
671
- def range_endpoint_constant(range, edge)
672
- values = range.to_a
673
- return Type::Combinator.constant_of(nil) if values.empty?
674
-
675
- Type::Combinator.constant_of(edge == :first ? values.first : values.last)
676
- end
708
+ def range_to_a_tuple(range)
709
+ # `Range#size` is O(1) on Integer endpoints: check the cardinality before materialising.
710
+ return nil if range.size > RANGE_TO_A_LIMIT
677
711
 
678
- def range_minmax_tuple(range)
679
712
  values = range.to_a
680
- if values.empty?
681
- return Type::Combinator.tuple_of(
682
- Type::Combinator.constant_of(nil),
683
- Type::Combinator.constant_of(nil)
684
- )
685
- end
713
+ return Type::Combinator.tuple_of if values.empty?
686
714
 
687
- Type::Combinator.tuple_of(
688
- Type::Combinator.constant_of(values.first),
689
- Type::Combinator.constant_of(values.last)
690
- )
715
+ Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
691
716
  end
692
717
 
693
718
  # `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head / tail forms. `first`/`last` already
@@ -1175,6 +1200,17 @@ module Rigor
1175
1200
  # "Integer-ness" was already implied by the inputs (so the widening fallback only fires for
1176
1201
  # arithmetic over integers).
1177
1202
  def build_constant_type(values, source: nil)
1203
+ widened, values = values.partition { |value| value.is_a?(WidenedResult) }
1204
+ return build_scalar_constant_type(values, source) if widened.empty?
1205
+
1206
+ nominals = widened.map(&:class_name).uniq.map { |name| Type::Combinator.nominal_of(name) }
1207
+ return Type::Combinator.union(*nominals) if values.empty?
1208
+
1209
+ constants = build_scalar_constant_type(values, source)
1210
+ constants && Type::Combinator.union(constants, *nominals)
1211
+ end
1212
+
1213
+ def build_scalar_constant_type(values, source)
1178
1214
  return nil if values.empty?
1179
1215
 
1180
1216
  unique = values.uniq
@@ -1607,9 +1643,12 @@ module Rigor
1607
1643
  # `flat_map`.
1608
1644
  def invoke_binary(receiver_value, method_name, arg_value)
1609
1645
  return nil unless safe?(receiver_value, method_name, arg_value)
1646
+ return nil if range_enumeration_blow_up?(receiver_value, method_name)
1610
1647
 
1611
- result = receiver_value.public_send(method_name, arg_value)
1612
- foldable_constant_value?(result) ? [result] : nil
1648
+ widened = numeric_blow_up_class(receiver_value, method_name, arg_value)
1649
+ return [WidenedResult.new(widened)] if widened
1650
+
1651
+ fold_result(receiver_value.public_send(method_name, arg_value))
1613
1652
  rescue StandardError
1614
1653
  nil
1615
1654
  end
@@ -1620,8 +1659,7 @@ module Rigor
1620
1659
  def invoke_ternary(receiver_value, method_name, av0, av1)
1621
1660
  return nil unless ternary_method_allowed?(receiver_value, method_name)
1622
1661
 
1623
- result = receiver_value.public_send(method_name, av0, av1)
1624
- foldable_constant_value?(result) ? [result] : nil
1662
+ fold_result(receiver_value.public_send(method_name, av0, av1))
1625
1663
  rescue StandardError
1626
1664
  nil
1627
1665
  end
@@ -1630,13 +1668,21 @@ module Rigor
1630
1668
  def invoke_unary(receiver_value, method_name)
1631
1669
  return nil unless unary_safe?(receiver_value, method_name)
1632
1670
  return nil if string_unary_blow_up?(receiver_value, method_name)
1671
+ return nil if range_enumeration_blow_up?(receiver_value, method_name)
1633
1672
 
1634
- result = receiver_value.public_send(method_name)
1635
- foldable_constant_value?(result) ? [result] : nil
1673
+ fold_result(receiver_value.public_send(method_name))
1636
1674
  rescue StandardError
1637
1675
  nil
1638
1676
  end
1639
1677
 
1678
+ # `[result]` for a foldable value, `[WidenedResult]` for a number past `INTEGER_FOLD_BIT_LIMIT`, nil
1679
+ # for anything else.
1680
+ def fold_result(result)
1681
+ return [WidenedResult.new(result.class.name)] if oversized_number?(result)
1682
+
1683
+ foldable_constant_value?(result) ? [result] : nil
1684
+ end
1685
+
1640
1686
  def unary_safe?(receiver_value, method_name)
1641
1687
  return true if unary_ops_for(receiver_value).include?(method_name)
1642
1688
 
@@ -1775,7 +1821,44 @@ module Rigor
1775
1821
  # with the widened class.
1776
1822
  return false if value.is_a?(Float) && value.nan?
1777
1823
  return false if value.is_a?(Complex) && complex_nan?(value)
1824
+ return false if oversized_number?(value)
1825
+
1826
+ true
1827
+ end
1828
+
1829
+ # True when an Integer, or an Integer part of a Rational / Complex, is past `INTEGER_FOLD_BIT_LIMIT`.
1830
+ def oversized_number?(value)
1831
+ case value
1832
+ when Integer then value.bit_length > INTEGER_FOLD_BIT_LIMIT
1833
+ when Rational then oversized_number?(value.numerator) || oversized_number?(value.denominator)
1834
+ when Complex then oversized_number?(value.real) || oversized_number?(value.imaginary)
1835
+ else false
1836
+ end
1837
+ end
1838
+
1839
+ # #2115 — true when `receiver_value` is a `Range` the method would walk element by element and the
1840
+ # range holds more than `RANGE_ENUMERATION_LIMIT` elements (an endless one never stops).
1841
+ def range_enumeration_blow_up?(receiver_value, method_name)
1842
+ return false unless receiver_value.is_a?(Range)
1843
+ return false if RANGE_ENDPOINT_METHODS.include?(method_name)
1844
+ return false if RANGE_COVER_METHODS.include?(method_name) && numeric_range?(receiver_value)
1845
+
1846
+ !range_within_enumeration_budget?(receiver_value)
1847
+ end
1778
1848
 
1849
+ def numeric_range?(range)
1850
+ range.begin.is_a?(Numeric) || range.end.is_a?(Numeric)
1851
+ end
1852
+
1853
+ # Takes at most `RANGE_ENUMERATION_LIMIT + 1` elements (`Range#first(n)` stops there, endless or
1854
+ # not). A range that cannot iterate at all (a Float or Time begin) raises, and so does every
1855
+ # enumerating method on it before doing any work, so it counts as within budget: the invoke's own
1856
+ # rescue declines an enumerating fold, and `(t0..t1).include?(t)`, which CRuby answers by `cover?`,
1857
+ # keeps folding. A beginless range raises `RangeError` here, which the invoke's own rescue turns into
1858
+ # a declined fold, as the enumerating method itself would raise.
1859
+ def range_within_enumeration_budget?(range)
1860
+ range.first(RANGE_ENUMERATION_LIMIT + 1).size <= RANGE_ENUMERATION_LIMIT
1861
+ rescue TypeError
1779
1862
  true
1780
1863
  end
1781
1864
 
@@ -1796,6 +1879,58 @@ module Rigor
1796
1879
  true
1797
1880
  end
1798
1881
 
1882
+ # #2115 — the result's class name when `**` / `pow` / `<<` / `>>` would produce a number past
1883
+ # `INTEGER_FOLD_BIT_LIMIT`, read from the operands so the huge value is never computed; nil
1884
+ # otherwise. `fold_result` applies the same bound to every other result after the fact.
1885
+ def numeric_blow_up_class(receiver_value, method_name, arg_value)
1886
+ return nil unless arg_value.is_a?(Integer)
1887
+
1888
+ case method_name
1889
+ when :**, :pow then power_blow_up_class(receiver_value, arg_value)
1890
+ when :<<, :>> then "Integer" if shift_blow_up?(receiver_value, method_name, arg_value)
1891
+ end
1892
+ end
1893
+
1894
+ # An Integer base to a negative power is a Rational (`2 ** -3 == (1/8)`); a Rational or Complex
1895
+ # base keeps its class.
1896
+ def power_blow_up_class(base, exponent)
1897
+ return nil unless power_blow_up?(base, exponent)
1898
+ return base.class.name unless base.is_a?(Integer)
1899
+
1900
+ exponent.negative? ? "Rational" : "Integer"
1901
+ end
1902
+
1903
+ # A lower bound on the result's bit length is `floor(log2 |base|) * |exponent|`; past the limit the
1904
+ # result is declined anyway. A Float base (or Float part) stays in Float arithmetic and never grows.
1905
+ def power_blow_up?(base, exponent)
1906
+ bits = power_base_bits(base)
1907
+ !bits.nil? && bits * exponent.abs > INTEGER_FOLD_BIT_LIMIT
1908
+ end
1909
+
1910
+ # `floor(log2 |base|)` for an Integer, the larger of its terms' for a Rational. A Complex part
1911
+ # counts one bit more: `(1+1i) ** n` grows although neither part is past 1.
1912
+ def power_base_bits(base)
1913
+ case base
1914
+ when Integer then base.abs.bit_length - 1
1915
+ when Rational then [base.numerator.abs.bit_length, base.denominator.bit_length].max - 1
1916
+ when Complex then complex_power_base_bits(base)
1917
+ end
1918
+ end
1919
+
1920
+ def complex_power_base_bits(base)
1921
+ parts = [base.real, base.imaginary]
1922
+ return nil unless parts.all? { |part| part.is_a?(Integer) || part.is_a?(Rational) }
1923
+
1924
+ parts.map { |part| power_base_bits(part) + 1 }.max
1925
+ end
1926
+
1927
+ def shift_blow_up?(receiver_value, method_name, arg_value)
1928
+ return false unless receiver_value.is_a?(Integer) && !receiver_value.zero?
1929
+
1930
+ shift = method_name == :<< ? arg_value : -arg_value
1931
+ receiver_value.bit_length + shift > INTEGER_FOLD_BIT_LIMIT
1932
+ end
1933
+
1799
1934
  def ops_for(receiver_value)
1800
1935
  case receiver_value
1801
1936
  when Integer, Float then NUMERIC_BINARY
@@ -526,12 +526,60 @@ module Rigor
526
526
  registry = environment&.plugin_registry
527
527
  each_source_ancestor_candidate(scope, class_name) do |candidate|
528
528
  return nil if scope.discovered_method?(candidate, method_name, kind)
529
- return candidate if ALLOWED_RBS_COMPLETE_ANCESTORS.include?(candidate) ||
530
- registry&.rbs_complete_ancestor?(candidate)
529
+ next unless ALLOWED_RBS_COMPLETE_ANCESTORS.include?(candidate) ||
530
+ registry&.rbs_complete_ancestor?(candidate)
531
+
532
+ return shadowed_in_ruby_order?(scope, class_name, kind, method_name, candidate) ? nil : candidate
531
533
  end
532
534
  nil
533
535
  end
534
536
 
537
+ # Issue #1572 — the walk above is breadth-first, so it reaches the superclass before a module an included
538
+ # module includes, which Ruby's order puts ahead of it (`class P < Rigor::Plugin::Base; include A` with `A`
539
+ # including `M` calls `M#prepare`, not `Base#prepare`), and it passes over RBS-known modules. True when the
540
+ # bridged `ancestor` is not what Ruby calls: on `class_name`'s {Scope::ResolutionChain} in Ruby's order, a
541
+ # project entry ahead of it records the name ({#project_records_name?}), an external entry ahead of it is a
542
+ # module RBS knows and declares the name in, or the chain does not hold it. An RBS-unknown external ahead is
543
+ # passed over, as the breadth-first walk always did. An unsettled chain withholds too: a mixin edge the
544
+ # tables cannot place (a receiver-form `Demo.include(M)` after the body, an edge in a conditional, a block or
545
+ # a hook) may put a definer ahead of the ancestor.
546
+ def shadowed_in_ruby_order?(scope, class_name, kind, method_name, ancestor)
547
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods)
548
+ return true if chain.unsettled?
549
+
550
+ entries = chain.entries
551
+ stop = entries.index do |entry|
552
+ entry.external? ? entry.candidates.include?(ancestor) : entry.name == ancestor
553
+ end
554
+ return true if stop.nil?
555
+
556
+ entries.first(stop).any? do |entry|
557
+ if entry.external?
558
+ rbs_declares_ahead?(scope, entry, method_name)
559
+ else
560
+ project_records_name?(scope, entry.name, method_name, entry.side)
561
+ end
562
+ end
563
+ end
564
+
565
+ # Whether the project entry `owner` holds the name on `kind`'s side. `discovered_methods` carries the
566
+ # `attr_*` and `define_method` rows project-wide, but at dispatch it has no row for a `def` in a module
567
+ # declared in another file, so a `def` is also asked of the project-wide def tables. ADR-46:
568
+ # `user_def_for` / `singleton_def_for` file the method edge, positive or negative, so a file that adds or
569
+ # removes the `def` on an entry ahead of the ancestor re-checks the call.
570
+ def project_records_name?(scope, owner, method_name, kind)
571
+ return true if scope.discovered_method?(owner, method_name, kind)
572
+
573
+ return !scope.singleton_def_for(owner, method_name).nil? if kind == :singleton
574
+
575
+ !scope.user_def_for(owner, method_name).nil?
576
+ end
577
+
578
+ def rbs_declares_ahead?(scope, entry, method_name)
579
+ entry.candidates.any? { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) } &&
580
+ !Scope::ResolutionChain::Relevance.external_lacks_on?(scope, entry.candidates, entry.side, method_name)
581
+ end
582
+
535
583
  # Issue #527 slice 1 — the RBS instance definition a Ruby-source class inherits from a CORE or
536
584
  # STDLIB ancestor, or nil. `Oj::EasyHash < Hash` answering `Dynamic[top]` to `has_key?` while
537
585
  # `{}.has_key?` folds was the largest single opacity family in the 2026-09-01 corpus sweep.
@@ -220,7 +220,8 @@ module Rigor
220
220
  # `call.undefined-method`.
221
221
  patched_result =
222
222
  unless skip_project_patched
223
- try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
223
+ try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment,
224
+ call_node: call_node, scope: scope)
224
225
  end
225
226
  if patched_result
226
227
  scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
@@ -258,6 +259,22 @@ module Rigor
258
259
  return discovered_result
259
260
  end
260
261
 
262
+ # Issue #1708 — a `pre_eval:` patch on one of the receiver's ancestors (`class Object; def zz`,
263
+ # `module Enumerable; def sum_by`). Below the RBS tier, whose decline means no nearer RBS declares the
264
+ # method, and below the tiers that answer the receiver's own patch or a gem catalogue entry. A nearer
265
+ # `def` in the project's source (on an RBS-known receiver, which the discovered-method tier leaves to
266
+ # user-method inference) stops the walk: see {#project_source_defines?}. That probe reads the scope's def
267
+ # index, so a single-file scope (`type-of`, `annotate`, an LSP buffer) cannot see a `def` in another file.
268
+ ancestor_patched =
269
+ if !skip_project_patched && scope
270
+ try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment,
271
+ call_node: call_node, scope: scope, through_ancestors: true)
272
+ end
273
+ if ancestor_patched
274
+ scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
275
+ return ancestor_patched
276
+ end
277
+
261
278
  # ADR-5 robustness — synthesized-stub-type tier. When the receiver is a type Rigor
262
279
  # invented to make an otherwise-unbuildable project signature resolve (a
263
280
  # missing-namespace module, or a stub for a referenced-but-undeclared type like an
@@ -644,7 +661,13 @@ module Rigor
644
661
  # `def to_url; "hello"; end` patched onto `String` now resolves `s.to_url` to
645
662
  # `Dynamic[Nominal[String]]` instead of the pre-3a `Dynamic[Top]`. Falls back to
646
663
  # `Dynamic[Top]` when the heuristic declined (non-literal tail expression).
647
- def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
664
+ #
665
+ # `through_ancestors:` (issue #1708), with a scope, searches the receiver's ancestors as well, through
666
+ # {ProjectPatchedMethods#lookup_through_ancestors}, stopping at a nearer project-source `def`. A private or
667
+ # protected patch Ruby would not let the call reach ({ProjectPatchedMethods#callable?}) answers nothing, so
668
+ # `1.kp` on `module Kernel; private def kp` reads as the undefined call it is.
669
+ def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment, # rubocop:disable Metrics/ParameterLists
670
+ call_node: nil, scope: nil, through_ancestors: false)
648
671
  registry = environment&.project_patched_methods
649
672
  return nil if registry.nil? || registry.empty?
650
673
 
@@ -652,14 +675,55 @@ module Rigor
652
675
  return nil if class_name.nil?
653
676
 
654
677
  kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
655
- entry = registry.lookup(class_name: class_name, method_name: method_name, kind: kind)
678
+ entry = project_patched_entry(registry, class_name, method_name, kind, environment,
679
+ scope: scope, through_ancestors: through_ancestors)
656
680
  return nil if entry.nil?
681
+
682
+ verdict = registry.callable?(entry, explicit: explicit_non_self_receiver?(call_node),
683
+ self_type: scope ? scope.self_type : :unknown,
684
+ rbs_loader: environment.rbs_loader)
685
+ return nil if verdict == false
686
+ return Type::Combinator.untyped if verdict.nil?
657
687
  return Type::Combinator.dynamic(entry.return_type) if entry.return_type
658
688
 
659
689
  aliased = project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
660
690
  aliased ? Type::Combinator.dynamic(aliased) : Type::Combinator.untyped
661
691
  end
662
692
 
693
+ def project_patched_entry(registry, class_name, method_name, kind, environment, scope:, through_ancestors:)
694
+ return registry.lookup(class_name: class_name, method_name: method_name, kind: kind) unless through_ancestors
695
+
696
+ registry.lookup_through_ancestors(
697
+ class_name: class_name, method_name: method_name, kind: kind, rbs_loader: environment.rbs_loader,
698
+ source_defines: ->(owner, side) { project_source_defines?(scope, owner, method_name, side) }
699
+ )
700
+ end
701
+
702
+ # Issue #1708 — does the project's own source define `method_name` on `owner` (`side` as the ancestor walk
703
+ # reaches it), or on a module the project prepends to or includes into it? Ruby runs that `def` ahead of a
704
+ # patch on a farther ancestor: `class Integer; def nn = "s"` beats `class Object; def nn = nil` in a
705
+ # `pre_eval:` file. The owner's level of its {Scope::ResolutionChain} (its prepends, itself, its includes
706
+ # and their own mixins, or a class object's singleton and its extends) holds those definers; whether one
707
+ # of them defines the name does not depend on their order, so the chain is searched without settling it.
708
+ def project_source_defines?(scope, owner, method_name, side)
709
+ return false unless scope.known_user_class?(owner)
710
+
711
+ chain = Scope::ResolutionChain.for(scope, owner, side, :methods)
712
+ own_level_end = chain.level_starts[1] || chain.entries.size
713
+ found = chain.search(scope, 0, own_level_end) do |entry|
714
+ !entry.external? && source_def?(scope, entry.name, method_name, entry.side)
715
+ end
716
+ !found.nil?
717
+ end
718
+
719
+ def source_def?(scope, owner, method_name, side)
720
+ if side == :singleton
721
+ scope.discovered_method?(owner, method_name, :singleton) || !scope.singleton_def_for(owner, method_name).nil?
722
+ else
723
+ scope.discovered_method?(owner, method_name, :instance) || !scope.user_def_for(owner, method_name).nil?
724
+ end
725
+ end
726
+
663
727
  # Issue #1702 — an alias in a `pre_eval:` patch whose old name no earlier patch binding defines
664
728
  # (`alias old_plus +` on Integer, or `alias_method :orig_succ, :succ` ahead of the patch's own `def succ`)
665
729
  # names the method the class had before the patch, so it answers what that method answers for the same