rigortype 0.4.4 → 0.4.6

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 (233) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +100 -18
  4. data/docs/manual/02-cli-reference.md +85 -16
  5. data/docs/manual/03-configuration.md +12 -9
  6. data/docs/manual/04-diagnostics.md +17 -6
  7. data/docs/manual/05-inspecting-types.md +2 -2
  8. data/docs/manual/06-baseline.md +1 -1
  9. data/docs/manual/07-plugins.md +2 -1
  10. data/docs/manual/08-skills.md +7 -0
  11. data/docs/manual/10-mcp-server.md +17 -2
  12. data/docs/manual/12-caching.md +8 -1
  13. data/docs/manual/18-removing-dead-code.md +18 -7
  14. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  15. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  16. data/docs/manual/plugins/rigor-devise.md +15 -0
  17. data/docs/manual/plugins/rigor-rspec.md +102 -1
  18. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  19. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  20. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  21. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  22. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
  23. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
  24. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  25. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  26. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  27. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  28. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  29. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
  30. data/lib/rigor/analysis/check_rules.rb +319 -21
  31. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/edit_replay.rb +67 -0
  34. data/lib/rigor/analysis/incremental.rb +86 -0
  35. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  36. data/lib/rigor/analysis/incremental_session.rb +336 -46
  37. data/lib/rigor/analysis/path_expansion.rb +44 -1
  38. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  39. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  40. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  41. data/lib/rigor/analysis/rule_catalog.rb +22 -5
  42. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  43. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  44. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  48. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  49. data/lib/rigor/analysis/runner.rb +222 -111
  50. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  51. data/lib/rigor/analysis/worker_session.rb +24 -1
  52. data/lib/rigor/cache/descriptor.rb +40 -7
  53. data/lib/rigor/cache/engine_source.rb +3 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +49 -8
  55. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  56. data/lib/rigor/cache/store.rb +4 -3
  57. data/lib/rigor/cli/check_command.rb +93 -4
  58. data/lib/rigor/cli/command.rb +3 -1
  59. data/lib/rigor/cli/diff_command.rb +2 -1
  60. data/lib/rigor/cli/docs_command.rb +3 -2
  61. data/lib/rigor/cli/doctor_command.rb +1 -1
  62. data/lib/rigor/cli/explain_command.rb +3 -2
  63. data/lib/rigor/cli/lens_command.rb +5 -5
  64. data/lib/rigor/cli/mcp_command.rb +2 -1
  65. data/lib/rigor/cli/options.rb +20 -3
  66. data/lib/rigor/cli/plugin_command.rb +2 -1
  67. data/lib/rigor/cli/plugins_command.rb +7 -1
  68. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  69. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  70. data/lib/rigor/cli/sig_gen_command.rb +73 -13
  71. data/lib/rigor/cli/skill_command.rb +2 -1
  72. data/lib/rigor/cli/skill_describe.rb +3 -2
  73. data/lib/rigor/cli/type_of_command.rb +2 -1
  74. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  75. data/lib/rigor/cli/unused_command.rb +10 -7
  76. data/lib/rigor/cli.rb +3 -2
  77. data/lib/rigor/config_audit.rb +72 -16
  78. data/lib/rigor/configuration/severity_profile.rb +20 -2
  79. data/lib/rigor/configuration.rb +43 -7
  80. data/lib/rigor/effects/signature_sources.rb +3 -1
  81. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  82. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  83. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  84. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  85. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  86. data/lib/rigor/environment/rbs_loader.rb +136 -7
  87. data/lib/rigor/environment/required_features.rb +136 -29
  88. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  89. data/lib/rigor/environment.rb +4 -2
  90. data/lib/rigor/inference/block_call_timing.rb +7 -1
  91. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  92. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  93. data/lib/rigor/inference/captured_locals.rb +44 -13
  94. data/lib/rigor/inference/clause_widening.rb +90 -0
  95. data/lib/rigor/inference/deep_erasure.rb +138 -0
  96. data/lib/rigor/inference/def_return_typer.rb +159 -9
  97. data/lib/rigor/inference/definer_resolution.rb +101 -20
  98. data/lib/rigor/inference/error_info.rb +17 -3
  99. data/lib/rigor/inference/expression_typer.rb +97 -18
  100. data/lib/rigor/inference/global_write_census.rb +36 -1
  101. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  102. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  103. data/lib/rigor/inference/loop_value_record.rb +53 -0
  104. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  105. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  106. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
  107. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  108. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  109. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  110. data/lib/rigor/inference/method_dispatcher.rb +113 -19
  111. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  112. data/lib/rigor/inference/module_function_state.rb +149 -42
  113. data/lib/rigor/inference/mutation_widening.rb +5 -2
  114. data/lib/rigor/inference/narrowing.rb +116 -11
  115. data/lib/rigor/inference/object_mixins.rb +13 -1
  116. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  117. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  118. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  119. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  120. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  121. data/lib/rigor/inference/project_seed_tables.rb +121 -0
  122. data/lib/rigor/inference/project_yielder.rb +128 -0
  123. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  124. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  125. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  126. data/lib/rigor/inference/return_barrier.rb +16 -0
  127. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  128. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  129. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  130. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  131. data/lib/rigor/inference/scope_indexer.rb +1144 -197
  132. data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
  133. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  134. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  135. data/lib/rigor/inference/statement_evaluator.rb +1088 -213
  136. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  137. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  138. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  139. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  140. data/lib/rigor/language_server/buffer_table.rb +6 -0
  141. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  142. data/lib/rigor/language_server/project_context.rb +59 -3
  143. data/lib/rigor/language_server/server.rb +5 -3
  144. data/lib/rigor/lens/contract_reader.rb +45 -2
  145. data/lib/rigor/lens/document.rb +2 -1
  146. data/lib/rigor/lens/file_reader.rb +12 -2
  147. data/lib/rigor/lens/members.rb +16 -2
  148. data/lib/rigor/lens/run.rb +34 -7
  149. data/lib/rigor/mcp/server.rb +11 -3
  150. data/lib/rigor/plugin/base.rb +29 -13
  151. data/lib/rigor/plugin/io_boundary.rb +42 -6
  152. data/lib/rigor/plugin/isolation.rb +4 -3
  153. data/lib/rigor/plugin/manifest.rb +29 -3
  154. data/lib/rigor/plugin/registry.rb +38 -0
  155. data/lib/rigor/protection/discovery_seed.rb +64 -36
  156. data/lib/rigor/protection/mutation_cache.rb +8 -2
  157. data/lib/rigor/rooted_glob.rb +36 -0
  158. data/lib/rigor/scope/discovery_index.rb +5 -0
  159. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  160. data/lib/rigor/scope/resolution_chain.rb +40 -5
  161. data/lib/rigor/scope.rb +59 -7
  162. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  163. data/lib/rigor/sig_gen/classification.rb +4 -1
  164. data/lib/rigor/sig_gen/generator.rb +465 -49
  165. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  166. data/lib/rigor/sig_gen/observation_collector.rb +141 -12
  167. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  168. data/lib/rigor/sig_gen/renderer.rb +16 -155
  169. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  170. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  171. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  172. data/lib/rigor/sig_gen/write_result.rb +3 -1
  173. data/lib/rigor/sig_gen/writer.rb +5 -3
  174. data/lib/rigor/signature_path_audit.rb +4 -2
  175. data/lib/rigor/type/combinator.rb +4 -3
  176. data/lib/rigor/type/constant.rb +54 -3
  177. data/lib/rigor/type/hash_shape.rb +14 -9
  178. data/lib/rigor/type/struct_instance.rb +3 -2
  179. data/lib/rigor/version.rb +1 -1
  180. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  181. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
  182. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  183. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  184. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  185. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  186. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  187. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  188. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  189. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  190. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  191. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  192. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  193. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  194. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  195. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  196. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  197. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  198. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  199. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  200. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  201. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  202. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  203. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  204. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  205. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  206. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  207. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  208. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  209. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  210. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  211. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  212. data/sig/rigor/analysis/baseline.rbs +3 -2
  213. data/sig/rigor/effects/file_collection.rbs +5 -6
  214. data/sig/rigor/effects/registry.rbs +8 -6
  215. data/sig/rigor/effects/summary.rbs +6 -5
  216. data/sig/rigor/environment.rbs +5 -3
  217. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  218. data/sig/rigor/inference.rbs +6 -0
  219. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  220. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  221. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  222. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  223. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  224. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  225. data/sig/rigor/plugin/manifest.rbs +16 -18
  226. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  227. data/sig/rigor/plugin/registry.rbs +1 -1
  228. data/sig/rigor/rooted_glob.rbs +7 -0
  229. data/sig/rigor/scope.rbs +34 -52
  230. data/sig/rigor/type.rbs +0 -6
  231. data/skills/rigor-doctor/references/01-checks.md +23 -11
  232. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  233. metadata +32 -1
@@ -190,6 +190,44 @@ 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
+
222
+ # #2125 — a String result past `STRING_FOLD_BYTE_LIMIT`, or an operand that would build one. Unlike a
223
+ # `WidenedResult` it declines the WHOLE fold, every other member of a union receiver included: dropping
224
+ # only the oversized member would answer the rest (`(c ? "a" : big) * 2` read `"aa"`). The next tier then
225
+ # answers the call — `LiteralStringFolding`'s `literal-string`, or RBS's `String`, never less than the
226
+ # `Nominal[String]` the fold could have widened to.
227
+ OversizedString = Data.define
228
+ OVERSIZED_STRING = [OversizedString.new].freeze
229
+ private_constant :OversizedString, :OVERSIZED_STRING
230
+
193
231
  # Input cartesian product hard cap. Keeps fold cost bounded even when the receiver and argument are
194
232
  # both `Union[Constant…]`. 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-facing payoff
195
233
  # (a precise small enum) drops off fast past this range and CRuby method invocation cost adds up.
@@ -302,6 +340,7 @@ module Rigor
302
340
  arg = args.first
303
341
  ruby_arg = format_argument_value(arg)
304
342
  return nil if ruby_arg.nil?
343
+ return nil if format_blow_up?(receiver.value, ruby_arg)
305
344
 
306
345
  result = receiver.value % ruby_arg
307
346
  return nil unless foldable_constant_value?(result)
@@ -649,45 +688,41 @@ module Rigor
649
688
  def range_constant_unary(range, method_name)
650
689
  case method_name
651
690
  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)
691
+ # #2115 — `Range#min` / `#max` / `#minmax` / `#size` are O(1) on Integer endpoints in CRuby, so
692
+ # none of these materialises the range. The no-arg `first` / `last` read the endpoints, not the
693
+ # elements: `first` is `begin` and `last` is `end` whether the range is empty, reversed or
694
+ # exclusive (`(5..1).first` is 5, `(1...10).last` is 10). Only `min` / `max` depend on emptiness
695
+ # and exclusivity (`(5..1).min` is nil, `(1...10).max` is 9).
696
+ when :first then Type::Combinator.constant_of(range.begin)
697
+ when :last then Type::Combinator.constant_of(range.end)
698
+ when :min then Type::Combinator.constant_of(range.min)
699
+ when :max then Type::Combinator.constant_of(range.max)
700
+ when :count, :size, :length then Type::Combinator.constant_of(range.size)
701
+ when :minmax then Type::Combinator.tuple_of(*range.minmax.map { |v| Type::Combinator.constant_of(v) })
656
702
  # `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)
703
+ # yields a single Integer — no materialisation; only `INTEGER_FOLD_BIT_LIMIT` applies. Endless
704
+ # ranges are already excluded by the Integer-endpoint guard in the caller.
705
+ when :sum then range_sum_constant(range)
660
706
  end
661
707
  end
662
708
 
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
709
+ # The Gauss sum of a range of large endpoints can still pass `INTEGER_FOLD_BIT_LIMIT`; it is then
710
+ # an `Integer` (both endpoints are).
711
+ def range_sum_constant(range)
712
+ sum = range.sum
713
+ return Type::Combinator.nominal_of("Integer") if oversized_number?(sum)
667
714
 
668
- Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
715
+ Type::Combinator.constant_of(sum)
669
716
  end
670
717
 
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
718
+ def range_to_a_tuple(range)
719
+ # `Range#size` is O(1) on Integer endpoints: check the cardinality before materialising.
720
+ return nil if range.size > RANGE_TO_A_LIMIT
677
721
 
678
- def range_minmax_tuple(range)
679
722
  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
723
+ return Type::Combinator.tuple_of if values.empty?
686
724
 
687
- Type::Combinator.tuple_of(
688
- Type::Combinator.constant_of(values.first),
689
- Type::Combinator.constant_of(values.last)
690
- )
725
+ Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
691
726
  end
692
727
 
693
728
  # `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head / tail forms. `first`/`last` already
@@ -1175,9 +1210,23 @@ module Rigor
1175
1210
  # "Integer-ness" was already implied by the inputs (so the widening fallback only fires for
1176
1211
  # arithmetic over integers).
1177
1212
  def build_constant_type(values, source: nil)
1213
+ return nil if values.any?(OversizedString)
1214
+
1215
+ widened, values = values.partition { |value| value.is_a?(WidenedResult) }
1216
+ return build_scalar_constant_type(values, source) if widened.empty?
1217
+
1218
+ nominals = widened.map(&:class_name).uniq.map { |name| Type::Combinator.nominal_of(name) }
1219
+ return Type::Combinator.union(*nominals) if values.empty?
1220
+
1221
+ constants = build_scalar_constant_type(values, source)
1222
+ constants && Type::Combinator.union(constants, *nominals)
1223
+ end
1224
+
1225
+ def build_scalar_constant_type(values, source)
1178
1226
  return nil if values.empty?
1179
1227
 
1180
- unique = values.uniq
1228
+ # #1513 — keyed so the possible results `0.0` and `-0.0` stay two members, as `Constant#==` keeps them.
1229
+ unique = values.uniq { |value| Type::Constant.value_key(value) }
1181
1230
  return collapse_constants(unique) if unique.size <= UNION_FOLD_OUTPUT_LIMIT
1182
1231
 
1183
1232
  widen_to_integer_range(unique, source) ||
@@ -1607,9 +1656,13 @@ module Rigor
1607
1656
  # `flat_map`.
1608
1657
  def invoke_binary(receiver_value, method_name, arg_value)
1609
1658
  return nil unless safe?(receiver_value, method_name, arg_value)
1659
+ return OVERSIZED_STRING if string_blow_up?(receiver_value, method_name, arg_value)
1660
+ return nil if range_enumeration_blow_up?(receiver_value, method_name)
1610
1661
 
1611
- result = receiver_value.public_send(method_name, arg_value)
1612
- foldable_constant_value?(result) ? [result] : nil
1662
+ widened = numeric_blow_up_class(receiver_value, method_name, arg_value)
1663
+ return [WidenedResult.new(widened)] if widened
1664
+
1665
+ fold_result(receiver_value.public_send(method_name, arg_value))
1613
1666
  rescue StandardError
1614
1667
  nil
1615
1668
  end
@@ -1619,9 +1672,9 @@ module Rigor
1619
1672
  # `false`/`nil` folds.
1620
1673
  def invoke_ternary(receiver_value, method_name, av0, av1)
1621
1674
  return nil unless ternary_method_allowed?(receiver_value, method_name)
1675
+ return OVERSIZED_STRING if string_ternary_blow_up?(receiver_value, method_name, av0, av1)
1622
1676
 
1623
- result = receiver_value.public_send(method_name, av0, av1)
1624
- foldable_constant_value?(result) ? [result] : nil
1677
+ fold_result(receiver_value.public_send(method_name, av0, av1))
1625
1678
  rescue StandardError
1626
1679
  nil
1627
1680
  end
@@ -1629,14 +1682,23 @@ module Rigor
1629
1682
  # Returns `[value]` on success, `nil` to signal "skip". See `invoke_binary` for why we wrap.
1630
1683
  def invoke_unary(receiver_value, method_name)
1631
1684
  return nil unless unary_safe?(receiver_value, method_name)
1632
- return nil if string_unary_blow_up?(receiver_value, method_name)
1685
+ return OVERSIZED_STRING if string_unary_blow_up?(receiver_value, method_name)
1686
+ return nil if range_enumeration_blow_up?(receiver_value, method_name)
1633
1687
 
1634
- result = receiver_value.public_send(method_name)
1635
- foldable_constant_value?(result) ? [result] : nil
1688
+ fold_result(receiver_value.public_send(method_name))
1636
1689
  rescue StandardError
1637
1690
  nil
1638
1691
  end
1639
1692
 
1693
+ # `[result]` for a foldable value, `[WidenedResult]` for a number past `INTEGER_FOLD_BIT_LIMIT`,
1694
+ # `OVERSIZED_STRING` for a String past `STRING_FOLD_BYTE_LIMIT`, nil for anything else.
1695
+ def fold_result(result)
1696
+ return [WidenedResult.new(result.class.name)] if oversized_number?(result)
1697
+ return OVERSIZED_STRING if oversized_string?(result)
1698
+
1699
+ foldable_constant_value?(result) ? [result] : nil
1700
+ end
1701
+
1640
1702
  def unary_safe?(receiver_value, method_name)
1641
1703
  return true if unary_ops_for(receiver_value).include?(method_name)
1642
1704
 
@@ -1775,7 +1837,54 @@ module Rigor
1775
1837
  # with the widened class.
1776
1838
  return false if value.is_a?(Float) && value.nan?
1777
1839
  return false if value.is_a?(Complex) && complex_nan?(value)
1840
+ return false if oversized_number?(value)
1841
+ return false if oversized_string?(value)
1842
+
1843
+ true
1844
+ end
1845
+
1846
+ # #2125 — the result gate every String-producing fold passes through, the String twin of
1847
+ # `oversized_number?`. The operand prechecks (`string_blow_up?`, `string_ternary_blow_up?`,
1848
+ # `format_blow_up?`) keep the costly cases from being built at all; this catches the rest after the
1849
+ # fact (`tr` into multibyte characters, `dump`, `Integer#to_s(2)`, whose receiver
1850
+ # `INTEGER_FOLD_BIT_LIMIT` already bounds, so its rendering is at most 13 KB).
1851
+ def oversized_string?(value)
1852
+ value.is_a?(String) && value.bytesize > STRING_FOLD_BYTE_LIMIT
1853
+ end
1854
+
1855
+ # True when an Integer, or an Integer part of a Rational / Complex, is past `INTEGER_FOLD_BIT_LIMIT`.
1856
+ def oversized_number?(value)
1857
+ case value
1858
+ when Integer then value.bit_length > INTEGER_FOLD_BIT_LIMIT
1859
+ when Rational then oversized_number?(value.numerator) || oversized_number?(value.denominator)
1860
+ when Complex then oversized_number?(value.real) || oversized_number?(value.imaginary)
1861
+ else false
1862
+ end
1863
+ end
1864
+
1865
+ # #2115 — true when `receiver_value` is a `Range` the method would walk element by element and the
1866
+ # range holds more than `RANGE_ENUMERATION_LIMIT` elements (an endless one never stops).
1867
+ def range_enumeration_blow_up?(receiver_value, method_name)
1868
+ return false unless receiver_value.is_a?(Range)
1869
+ return false if RANGE_ENDPOINT_METHODS.include?(method_name)
1870
+ return false if RANGE_COVER_METHODS.include?(method_name) && numeric_range?(receiver_value)
1871
+
1872
+ !range_within_enumeration_budget?(receiver_value)
1873
+ end
1874
+
1875
+ def numeric_range?(range)
1876
+ range.begin.is_a?(Numeric) || range.end.is_a?(Numeric)
1877
+ end
1778
1878
 
1879
+ # Takes at most `RANGE_ENUMERATION_LIMIT + 1` elements (`Range#first(n)` stops there, endless or
1880
+ # not). A range that cannot iterate at all (a Float or Time begin) raises, and so does every
1881
+ # enumerating method on it before doing any work, so it counts as within budget: the invoke's own
1882
+ # rescue declines an enumerating fold, and `(t0..t1).include?(t)`, which CRuby answers by `cover?`,
1883
+ # keeps folding. A beginless range raises `RangeError` here, which the invoke's own rescue turns into
1884
+ # a declined fold, as the enumerating method itself would raise.
1885
+ def range_within_enumeration_budget?(range)
1886
+ range.first(RANGE_ENUMERATION_LIMIT + 1).size <= RANGE_ENUMERATION_LIMIT
1887
+ rescue TypeError
1779
1888
  true
1780
1889
  end
1781
1890
 
@@ -1791,11 +1900,62 @@ module Rigor
1791
1900
  catalog_allows?(receiver_value, method_name)
1792
1901
  return false unless allowed
1793
1902
  return false if integer_division_by_zero?(receiver_value, method_name, arg_value)
1794
- return false if string_blow_up?(receiver_value, method_name, arg_value)
1795
1903
 
1796
1904
  true
1797
1905
  end
1798
1906
 
1907
+ # #2115 — the result's class name when `**` / `pow` / `<<` / `>>` would produce a number past
1908
+ # `INTEGER_FOLD_BIT_LIMIT`, read from the operands so the huge value is never computed; nil
1909
+ # otherwise. `fold_result` applies the same bound to every other result after the fact.
1910
+ def numeric_blow_up_class(receiver_value, method_name, arg_value)
1911
+ return nil unless arg_value.is_a?(Integer)
1912
+
1913
+ case method_name
1914
+ when :**, :pow then power_blow_up_class(receiver_value, arg_value)
1915
+ when :<<, :>> then "Integer" if shift_blow_up?(receiver_value, method_name, arg_value)
1916
+ end
1917
+ end
1918
+
1919
+ # An Integer base to a negative power is a Rational (`2 ** -3 == (1/8)`); a Rational or Complex
1920
+ # base keeps its class.
1921
+ def power_blow_up_class(base, exponent)
1922
+ return nil unless power_blow_up?(base, exponent)
1923
+ return base.class.name unless base.is_a?(Integer)
1924
+
1925
+ exponent.negative? ? "Rational" : "Integer"
1926
+ end
1927
+
1928
+ # A lower bound on the result's bit length is `floor(log2 |base|) * |exponent|`; past the limit the
1929
+ # result is declined anyway. A Float base (or Float part) stays in Float arithmetic and never grows.
1930
+ def power_blow_up?(base, exponent)
1931
+ bits = power_base_bits(base)
1932
+ !bits.nil? && bits * exponent.abs > INTEGER_FOLD_BIT_LIMIT
1933
+ end
1934
+
1935
+ # `floor(log2 |base|)` for an Integer, the larger of its terms' for a Rational. A Complex part
1936
+ # counts one bit more: `(1+1i) ** n` grows although neither part is past 1.
1937
+ def power_base_bits(base)
1938
+ case base
1939
+ when Integer then base.abs.bit_length - 1
1940
+ when Rational then [base.numerator.abs.bit_length, base.denominator.bit_length].max - 1
1941
+ when Complex then complex_power_base_bits(base)
1942
+ end
1943
+ end
1944
+
1945
+ def complex_power_base_bits(base)
1946
+ parts = [base.real, base.imaginary]
1947
+ return nil unless parts.all? { |part| part.is_a?(Integer) || part.is_a?(Rational) }
1948
+
1949
+ parts.map { |part| power_base_bits(part) + 1 }.max
1950
+ end
1951
+
1952
+ def shift_blow_up?(receiver_value, method_name, arg_value)
1953
+ return false unless receiver_value.is_a?(Integer) && !receiver_value.zero?
1954
+
1955
+ shift = method_name == :<< ? arg_value : -arg_value
1956
+ receiver_value.bit_length + shift > INTEGER_FOLD_BIT_LIMIT
1957
+ end
1958
+
1799
1959
  def ops_for(receiver_value)
1800
1960
  case receiver_value
1801
1961
  when Integer, Float then NUMERIC_BINARY
@@ -1825,16 +1985,91 @@ module Rigor
1825
1985
  case method_name
1826
1986
  when :+ then string_concat_blow_up?(receiver_value, arg_value)
1827
1987
  when :* then string_repeat_blow_up?(receiver_value, arg_value)
1988
+ when :% then format_blow_up?(receiver_value, arg_value)
1828
1989
  when :center, :ljust, :rjust then string_pad_blow_up?(arg_value)
1829
1990
  else false
1830
1991
  end
1831
1992
  end
1832
1993
 
1994
+ # #2125 — the two-argument String folds that can grow past the input: the padded `center` / `ljust`
1995
+ # / `rjust` (width, pad) and `sub` / `gsub` (pattern, replacement).
1996
+ def string_ternary_blow_up?(receiver_value, method_name, av0, av1)
1997
+ return false unless receiver_value.is_a?(String)
1998
+
1999
+ case method_name
2000
+ when :center, :ljust, :rjust then string_pad_blow_up?(av0)
2001
+ when :sub, :gsub then string_substitution_blow_up?(receiver_value, method_name, av1)
2002
+ else false
2003
+ end
2004
+ end
2005
+
2006
+ # An upper bound on a `sub` / `gsub` result with a String replacement: `gsub` matches at most once per
2007
+ # position plus once at the end, and each replacement is the replacement's bytes plus, for every
2008
+ # backslash in it (`\0`, `\1`, `\k<name>`, `` \` ``, `\'`), at most the receiver's bytes again.
2009
+ def string_substitution_blow_up?(receiver_value, method_name, replacement)
2010
+ return false unless replacement.is_a?(String)
2011
+
2012
+ size = receiver_value.bytesize
2013
+ matches = method_name == :gsub ? size + 1 : 1
2014
+ per_match = replacement.bytesize + (replacement.count("\\") * size)
2015
+ size + (matches * per_match) > STRING_FOLD_BYTE_LIMIT
2016
+ end
2017
+
2018
+ # #2125 — `String#%` / `Kernel#format` pad to a field width and render to a precision the format
2019
+ # string spells (`"%02000000000d"`, `"%.100000f"`) or an argument supplies (`"%*d" % [n, 1]`).
2020
+ # Formatting builds the whole padded string before any result gate can look at it — two billion
2021
+ # bytes took 13 s and 4 GB — so the numbers are read from the directives first. A `*` width counts
2022
+ # every Integer argument as a possible width, and a `%%` escape matches as a whole, carrying no digits.
2023
+ FORMAT_DIRECTIVE = /%%|%(?:<[^>]*>|\{[^}]*\}|[-+ 0#*$.\d])*/
2024
+ private_constant :FORMAT_DIRECTIVE
2025
+
2026
+ def format_blow_up?(format_string, arguments)
2027
+ star = false
2028
+ format_string.scan(FORMAT_DIRECTIVE) do |directive|
2029
+ spec = directive.gsub(/<[^>]*>|\{[^}]*\}|\d+\$/, "")
2030
+ return true if spec.scan(/\d+/).any? { |digits| digits.to_i > STRING_FOLD_BYTE_LIMIT }
2031
+
2032
+ star ||= spec.include?("*")
2033
+ end
2034
+ star && format_argument_values(arguments).any? do |value|
2035
+ width = integer_operand(value)
2036
+ !width.nil? && width.abs > STRING_FOLD_BYTE_LIMIT
2037
+ end
2038
+ end
2039
+
2040
+ def format_argument_values(arguments)
2041
+ case arguments
2042
+ when Hash then arguments.values
2043
+ when Array then arguments
2044
+ else [arguments]
2045
+ end
2046
+ end
2047
+
1833
2048
  # `"x".center(width)` / `#ljust` / `#rjust` produce a string of `max(width, len)` characters. A
1834
2049
  # literal `width` far larger than the receiver would materialise a huge Constant; cap it at the same
1835
2050
  # byte limit the concat / repeat paths use.
1836
2051
  def string_pad_blow_up?(arg_value)
1837
- arg_value.is_a?(Integer) && arg_value > STRING_FOLD_BYTE_LIMIT
2052
+ width = integer_operand(arg_value)
2053
+ !width.nil? && width > STRING_FOLD_BYTE_LIMIT
2054
+ end
2055
+
2056
+ # The Integer a width or count operand stands for. Ruby converts a Float or Rational one with `NUM2LONG`
2057
+ # (`"x" * 2.9` is `"xx"`, `"x".ljust(2e9)` pads to two billion), and a Complex one whose imaginary part is an
2058
+ # exact zero through its real part (`"x" * Complex(3, 0)` is `"xxx"`; `Complex(3, 0.0)` raises), so the
2059
+ # prechecks read it the same way. Those are every Numeric class a `Constant` carries. A non-finite Float
2060
+ # raises in Ruby, and any other operand is left to the call, so both answer nil.
2061
+ def integer_operand(value)
2062
+ case value
2063
+ when Integer then value
2064
+ when Float then value.finite? ? value.to_i : nil
2065
+ when Rational then value.to_i
2066
+ when Complex then exact_real_operand(value)
2067
+ end
2068
+ end
2069
+
2070
+ def exact_real_operand(value)
2071
+ imaginary = value.imaginary
2072
+ imaginary.is_a?(Integer) && imaginary.zero? ? integer_operand(value.real) : nil
1838
2073
  end
1839
2074
 
1840
2075
  def string_concat_blow_up?(receiver_value, arg_value)
@@ -1843,10 +2078,11 @@ module Rigor
1843
2078
  end
1844
2079
 
1845
2080
  def string_repeat_blow_up?(receiver_value, arg_value)
1846
- return false unless arg_value.is_a?(Integer)
1847
- return true if arg_value.negative?
2081
+ # A negative count raises (`ArgumentError`), so the call itself skips that member, as any raising one is.
2082
+ count = integer_operand(arg_value)
2083
+ return false if count.nil? || count.negative?
1848
2084
 
1849
- receiver_value.bytesize * arg_value > STRING_FOLD_BYTE_LIMIT
2085
+ receiver_value.bytesize * count > STRING_FOLD_BYTE_LIMIT
1850
2086
  end
1851
2087
  end
1852
2088
  end
@@ -191,12 +191,16 @@ module Rigor
191
191
  # Runs the real `Kernel#format` when everything is value-pinned. A malformed directive or an
192
192
  # argument-count mismatch raises at fold time; the handler declines so the literal-string
193
193
  # lift (or the RBS `String` envelope) answers instead. Results larger than the shared
194
- # `ConstantFolding::STRING_FOLD_BYTE_LIMIT` decline too, keeping the carrier-size convention.
194
+ # `ConstantFolding::STRING_FOLD_BYTE_LIMIT` decline too, keeping the carrier-size convention,
195
+ # and a field width or precision past it declines before the string is built (#2125).
195
196
  def fold_format_constant(args)
196
197
  return nil unless args.all?(Type::Constant)
197
198
  return nil unless args.first.value.is_a?(String)
198
199
 
199
- result = format(args.first.value, *args.drop(1).map(&:value))
200
+ values = args.drop(1).map(&:value)
201
+ return nil if ConstantFolding.format_blow_up?(args.first.value, values)
202
+
203
+ result = format(args.first.value, *values)
200
204
  return nil if result.bytesize > ConstantFolding::STRING_FOLD_BYTE_LIMIT
201
205
 
202
206
  Type::Combinator.constant_of(result)
@@ -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.
@@ -35,43 +35,76 @@ module Rigor
35
35
  return nil unless SingletonFolding.receiver?(receiver, "Set")
36
36
 
37
37
  case method_name
38
- when :[] then fold_bracket(args)
39
- when :new then fold_new(args)
38
+ when :[] then fold_bracket(args, context.call_node)
39
+ when :new then fold_new(args, context.call_node, context.block_type)
40
40
  end
41
41
  end
42
42
 
43
- # `Set["a", "b", "c"]` — all positional args must be Constant.
44
- def fold_bracket(args)
45
- values = args.map do |a|
46
- return nil unless a.is_a?(Type::Constant)
43
+ # `Set["a", "b", "c"]` — all positional args must be Constant to fold to a value. Otherwise the
44
+ # element type is the union of the arguments, widened off their literal pins (#2278).
45
+ def fold_bracket(args, call_node = nil)
46
+ return nil if splatted?(call_node)
47
47
 
48
- a.value
49
- end
48
+ return Type::Combinator.constant_of(::Set.new(args.map(&:value))) if args.all?(Type::Constant)
50
49
 
51
- Type::Combinator.constant_of(::Set.new(values))
50
+ typed_set(args)
52
51
  rescue StandardError
53
52
  nil
54
53
  end
55
54
 
56
- # `Set.new` / `Set.new(tuple_of_constants)`.
57
- def fold_new(args)
55
+ # `Set.new` / `Set.new(enum)`. A call with a block declines: the block maps each element
56
+ # (`Set.new([1, 2], &:to_s)` holds `"1"` and `"2"`), so the argument's elements are not the set's.
57
+ def fold_new(args, call_node = nil, block_type = nil)
58
+ return nil if block_type || block_given?(call_node) || splatted?(call_node)
58
59
  return Type::Combinator.constant_of(::Set.new) if args.empty?
59
60
  return nil if args.size > 1
60
61
 
61
62
  arg = args.first
62
- values = case arg
63
- when Type::Tuple
64
- return nil unless arg.elements.all?(Type::Constant)
65
-
66
- arg.elements.map(&:value)
67
- else
68
- return nil
69
- end
63
+ return fold_tuple(arg) if arg.is_a?(Type::Tuple)
70
64
 
71
- Type::Combinator.constant_of(::Set.new(values))
65
+ element = nominal_element(arg)
66
+ element ? typed_set([element]) : nil
72
67
  rescue StandardError
73
68
  nil
74
69
  end
70
+
71
+ def fold_tuple(tuple)
72
+ if tuple.elements.all?(Type::Constant)
73
+ return Type::Combinator.constant_of(::Set.new(tuple.elements.map(&:value)))
74
+ end
75
+
76
+ typed_set(tuple.elements)
77
+ end
78
+
79
+ # The element of an `Array[T]` / `Set[T]` argument, or nil for any other carrier.
80
+ def nominal_element(arg)
81
+ return nil unless arg.is_a?(Type::Nominal) && %w[Array Set].include?(arg.class_name)
82
+ return nil unless arg.type_args.size == 1
83
+
84
+ arg.type_args.first
85
+ end
86
+
87
+ # `Nominal[Set, [E]]`, `E` the members widened as an `Array.new` fill is (value pins dropped, a literal
88
+ # container widened to its nominal) joined with a `Dynamic[top]` floor. The floor is the conservative half:
89
+ # nothing widens a Set's element when the program mutates it (`add`, `<<`, `merge`, `replace`, `clear`
90
+ # then `<<`, a callee that fills it), so the classes the set was built with are not a claim about what it
91
+ # holds later, and a read of the element must not report on them (#2278).
92
+ def typed_set(types)
93
+ return nil if types.empty?
94
+
95
+ members = types.map { |t| MethodDispatcher.array_new_element(t) }
96
+ Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.union(*members, Type::Combinator.untyped)])
97
+ end
98
+
99
+ def block_given?(call_node)
100
+ call_node.respond_to?(:block) && !call_node.block.nil?
101
+ end
102
+
103
+ def splatted?(call_node)
104
+ return false unless call_node.respond_to?(:arguments)
105
+
106
+ (call_node.arguments&.arguments || []).any?(Prism::SplatNode)
107
+ end
75
108
  end
76
109
  end
77
110
  end