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
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # `Integer#step`'s block parameter, reached through {IteratorDispatch.block_param_types} (issue #1783).
9
+ #
10
+ # `ruby/rbs` declares every `Numeric#step` overload with a `{ (Numeric) -> void }` block and `Integer` does
11
+ # not redeclare `step`, so `1.step(n, 2) { |i| i.even? }` reported `even?` on correct code. The block form
12
+ # yields Integer only while the limit and the step are Integer too: a Float step or a finite Float limit
13
+ # makes CRuby's `ruby_float_step` yield Floats (`1.step(10, 0.5)`, `1.step(10.0)`; an infinite limit with
14
+ # an Integer step still yields Integers, which this rule leaves on the RBS binding), and a Rational step yields
15
+ # Rationals after the first value. So:
16
+ #
17
+ # - every operand provably Integer (an absent one is the Integer default; a nil limit means "no limit")
18
+ # binds `Integer`;
19
+ # - an operand that is `Dynamic` (an untyped parameter, `1.step(n, 2)`) could be either, so the binding is
20
+ # `Dynamic[Numeric]`: Integer is unsound for `n = 0.5`, and the RBS `Numeric` reports `even?` on correct
21
+ # Integer code, so the answer that holds for both declines the check;
22
+ # - anything else (a Float, a Rational, a union) falls through to the RBS `Numeric` binding, which a Float
23
+ # or Rational receiver also keeps.
24
+ #
25
+ # The keyword forms (`step(by: 2, to: 10)`, `step(10, by: 2)`) arrive as a trailing hash shape.
26
+ module IntegerStepBlockParams
27
+ module_function
28
+
29
+ KEYWORD_ROLES = { to: :limit, by: :step }.freeze
30
+ private_constant :KEYWORD_ROLES
31
+
32
+ # @return the block-param types, or nil to fall through to the RBS tier.
33
+ def block_param_types(receiver, args)
34
+ element = element_type(receiver, args)
35
+ element && [element]
36
+ end
37
+
38
+ # The type of every value the call yields, or nil when the rule does not decide it. The block-less form
39
+ # shares it: the `Enumerator::ArithmeticSequence` it returns yields the same values as the block would,
40
+ # except past an infinite Float limit, which this rule already declines ({ArithmeticSequenceElements}).
41
+ def element_type(receiver, args)
42
+ return nil unless IteratorDispatch.integer_rooted?(receiver)
43
+
44
+ operands = operands(args)
45
+ return nil if operands.nil?
46
+
47
+ verdicts = operands.map { |role, type| verdict(role, type) }
48
+ return nil if verdicts.include?(:decline)
49
+ return Type::Combinator.nominal_of("Integer") if verdicts.all?(:integer)
50
+
51
+ Type::Combinator.dynamic(Type::Combinator.nominal_of("Numeric"))
52
+ end
53
+
54
+ # The `[role, type]` pairs of the call's limit and step operands, or nil when the argument list is not
55
+ # one of `step`'s shapes.
56
+ def operands(args)
57
+ positional = args
58
+ keywords = {}
59
+ if args.last.is_a?(Type::HashShape)
60
+ positional = args[0...-1]
61
+ keywords = args.last.pairs
62
+ return nil unless keywords.keys.all? { |key| KEYWORD_ROLES.key?(key) }
63
+ end
64
+ return nil if positional.size > 2
65
+
66
+ pairs = positional.zip(%i[limit step]).map { |type, role| [role, type] }
67
+ keywords.each { |key, type| pairs << [KEYWORD_ROLES.fetch(key), type] }
68
+ pairs
69
+ end
70
+
71
+ def verdict(role, type)
72
+ return :integer if IteratorDispatch.integer_rooted?(type)
73
+ return :integer if role == :limit && type.is_a?(Type::Constant) && type.value.nil?
74
+ return :unknown if type.is_a?(Type::Dynamic)
75
+
76
+ :decline
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "../../type"
4
4
  require_relative "singleton_folding"
5
5
  require_relative "hash_transform_keys_folding"
6
+ require_relative "integer_step_block_params"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -23,6 +24,10 @@ module Rigor
23
24
  # bound from the argument.
24
25
  # - `a.downto(b) { |i| … }` yields the same domain `[b, a]`, just iterated in reverse. Lower bound
25
26
  # from the argument, upper bound from the receiver.
27
+ # - `a.step(limit, by) { |i| … }` (and the `by:` / `to:` keyword forms) yields `Integer` when the receiver,
28
+ # the limit and the step are all Integer; any other operand keeps the RBS `(Numeric)` binding
29
+ # (`1.step(10, 0.5)` yields Floats). RBS declares every `Numeric#step` block as `(Numeric)`, which reports
30
+ # `even?` on correct Integer loops (issue #1783); see {IntegerStepBlockParams}.
26
31
  # - `h.transform_keys(mapping) { |k| … }` yields the receiver's keys, a rule the RBS probe does not state for
27
32
  # a union of receiver shapes ({HashTransformKeysFolding.block_param_types}).
28
33
  module IteratorDispatch
@@ -38,6 +43,7 @@ module Rigor
38
43
  when :times then times_block_params(receiver)
39
44
  when :upto then upto_block_params(receiver, args.first)
40
45
  when :downto then downto_block_params(receiver, args.first)
46
+ when :step then IntegerStepBlockParams.block_param_types(receiver, args)
41
47
  when :each_with_index then each_with_index_block_params(receiver)
42
48
  when :each_with_object then each_with_object_block_params(receiver, args.first)
43
49
  when :inject, :reduce then inject_block_params(receiver, args)
@@ -3,6 +3,7 @@
3
3
  require_relative "../../type"
4
4
  require_relative "constant_folding"
5
5
  require_relative "random_folding"
6
+ require_relative "unit_denominator_rational"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -346,8 +347,8 @@ module Rigor
346
347
  # is a numeric Constant. The actual Ruby constructor runs at fold time (host-side), so the result
347
348
  # respects Ruby's normalisation (`Rational(2, 4)` → `Rational(1, 2)`).
348
349
  def try_numeric_constructor(method_name, args)
349
- return nil unless [1, 2].include?(args.size)
350
- return nil unless args.all? { |arg| numeric_constant?(arg) }
350
+ foldable = [1, 2].include?(args.size) && args.all? { |arg| numeric_constant?(arg) }
351
+ return UnitDenominatorRational.answer(method_name, args) unless foldable
351
352
 
352
353
  values = args.map(&:value)
353
354
  result = NUMERIC_CONSTRUCTORS[method_name].call(*values)
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # How `OverloadSelector` reads a call's keyword arguments (#1727).
9
+ #
10
+ # A call's arguments reach the selector as one positional list, the keyword hash (`f(a, k: 1)`) typed as its
11
+ # last entry. Only the AST tells it from a braced positional hash (`f(a, { k: 1 })`), so the caller says which
12
+ # it is (`keywords_last`). An overload that declares keywords takes that hash as its keywords; one that
13
+ # declares none reads it as a trailing positional `Hash`, as Ruby passes it.
14
+ module KeywordArguments
15
+ NO_PARAMS = [].freeze
16
+ private_constant :NO_PARAMS
17
+
18
+ # The most argument lists {.distributions} spells out; past it, the block probe answers no information.
19
+ DISTRIBUTION_LIMIT = 8
20
+ private_constant :DISTRIBUTION_LIMIT
21
+
22
+ module_function
23
+
24
+ # The actuals `method_type`'s positional parameters read: every argument, less the trailing keyword hash
25
+ # when the call passes one and the overload declares keywords to take it.
26
+ def positional(method_type, arg_types, keywords_last)
27
+ return arg_types unless keywords_last && !arg_types.empty? && declares?(method_type.type)
28
+
29
+ arg_types[0...-1]
30
+ end
31
+
32
+ # {.positional} for the selector's `shared` bundle, computed once per selection: every overload that takes the
33
+ # keyword hash reads the same trimmed list. A bundle rebuilt with other arguments must reset the cache.
34
+ def selection_positional(method_type, shared)
35
+ return shared[:arg_types] unless shared[:keywords_last] && declares?(method_type.type)
36
+
37
+ shared[:positional_arguments] ||= positional(method_type, shared[:arg_types], true)
38
+ end
39
+
40
+ # The argument lists a call with a keyword hash stands for, one per combination of the members of its
41
+ # union-typed keyword values (`headers: bool` stands for `headers: true` and `headers: false`, and so does a
42
+ # `Dynamic[bool]`, which the #521 join produces), or nil when that is more than {DISTRIBUTION_LIMIT} lists. A
43
+ # precise union value selects per member at runtime, so an answer that holds for the call must hold for every
44
+ # member (#1737). Only a key that discriminates between `method_types` ({.discriminating?}) splits (#1779):
45
+ # every overload reads any other key's members alike, so splitting it only multiplies the lists toward the
46
+ # limit (`(a: Symbol, b: Symbol)` with two three-member values made nine). An overload that declares no
47
+ # keywords but may take the hash as a positional `Hash` ({.positional_hash_reader?}, whose block says whether
48
+ # a positional parameter may accept a `Hash`) reads every value against that parameter, so then every union
49
+ # value splits. `count` is the call's argument count, nil when a splat hides it. Without a keyword hash, or
50
+ # with no discriminating union value, the one list, `arg_types` itself.
51
+ def distributions(arg_types, keywords_last, method_types, count = arg_types.size, &hash_param)
52
+ keywords = keywords_last && arg_types.last
53
+ return [arg_types] unless keywords.is_a?(Type::HashShape)
54
+
55
+ choices = split_choices(keywords, method_types) do
56
+ hash_param && positional_hash_reader?(method_types, count, &hash_param)
57
+ end
58
+ return [arg_types] if choices.nil?
59
+ return nil if choices.reduce(1) { |count, (_, members)| count * members.size } > DISTRIBUTION_LIMIT
60
+
61
+ combinations(choices).map do |pairs|
62
+ shape = Type::HashShape.new(
63
+ pairs, required_keys: keywords.required_keys, optional_keys: keywords.optional_keys,
64
+ read_only_keys: keywords.read_only_keys, extra_keys: keywords.extra_keys
65
+ )
66
+ arg_types[0...-1] + [shape]
67
+ end
68
+ end
69
+
70
+ # Each key of `keywords` with the values its lists take: a discriminating union value's members, any other
71
+ # value whole. Nil when no value splits. The block answers whether every union value splits (read once).
72
+ def split_choices(keywords, method_types)
73
+ split = false
74
+ every = nil
75
+ choices = keywords.pairs.map do |name, value|
76
+ members = union_members(value)
77
+ next [name, [value]] unless members
78
+
79
+ unless discriminating?(name, method_types)
80
+ every = yield ? true : false if every.nil?
81
+ next [name, [value]] unless every
82
+ end
83
+
84
+ split = true
85
+ [name, members]
86
+ end
87
+ choices if split
88
+ end
89
+
90
+ # Whether the members of `name`'s value may select different overloads among `method_types`: the overloads
91
+ # that take the key (by name or through `**rest`) declare it with different types (`headers: true` in one,
92
+ # `?headers: false` in another). Where they all declare one type, a member it rejects is rejected by every
93
+ # overload, so the split only adds lists: four `flag: bool` keywords alone made sixteen. An overload that
94
+ # declares others but not this key rejects it whatever its value. One that declares no keywords reads the
95
+ # hash positionally, which {.positional_hash_reader?} decides for every key at once.
96
+ def discriminating?(name, method_types)
97
+ first = nil
98
+ method_types.any? do |method_type|
99
+ fun = method_type.type
100
+ next false unless declares?(fun)
101
+
102
+ type = (fun.required_keywords[name] || fun.optional_keywords[name] || fun.rest_keywords)&.type
103
+ next false if type.nil?
104
+
105
+ first ||= type
106
+ type != first
107
+ end
108
+ end
109
+
110
+ # Whether an overload of `method_types` that declares no keywords takes `count` arguments and may read the last,
111
+ # the keyword hash, as a positional `Hash`: the block answers for the RBS parameter at that position (the
112
+ # `(String)` of `(a: Symbol) | (String)` cannot take one, the `(Hash[Symbol, String])` of
113
+ # `(a: Integer) | (Hash[Symbol, String])` can, and its members then select per value), and an untyped `(?)`
114
+ # parameter list may take anything. A nil `count` (a splat hides how many arguments precede the hash, and
115
+ # `ph(*[], a: v)` passes `{ a: v }` to a one-parameter `(Hash)`) asks about every positional parameter.
116
+ def positional_hash_reader?(method_types, count, &)
117
+ method_types.any? do |method_type|
118
+ fun = method_type.type
119
+ next false if declares?(fun)
120
+ next true unless fun.respond_to?(:required_positionals)
121
+ next positional_params(fun).any?(&) if count.nil?
122
+
123
+ param = positional_param_at(fun, count, count - 1)
124
+ !param.nil? && yield(param)
125
+ end
126
+ end
127
+
128
+ def positional_params(fun)
129
+ params = fun.required_positionals + fun.optional_positionals + fun.trailing_positionals
130
+ fun.rest_positionals ? params + [fun.rest_positionals] : params
131
+ end
132
+
133
+ # The positional parameter `fun` binds the argument at `index` to when called with `count` arguments, or nil
134
+ # when the count does not fit its arity.
135
+ def positional_param_at(fun, count, index)
136
+ required = fun.required_positionals
137
+ trailing = fun.trailing_positionals
138
+ optional = fun.optional_positionals
139
+ minimum = required.size + trailing.size
140
+ return nil if count < minimum || (fun.rest_positionals.nil? && count > minimum + optional.size)
141
+ return required[index] if index < required.size
142
+ return trailing[index - (count - trailing.size)] if index >= count - trailing.size
143
+
144
+ optional[index - required.size] || fun.rest_positionals
145
+ end
146
+
147
+ # The members a keyword value splits into: a union's, or those of a `Dynamic` whose static facet is a union,
148
+ # less `nil`, which the #521 join usually carries from an arm no call takes (as `FacetDistribution` reads a
149
+ # facet). One member left stands in for the value (`Dynamic[false | nil]` reads as `false`), since the bare
150
+ # `Dynamic` would gradually match every overload's keyword. A plain union keeps `nil`: there it is a value
151
+ # the call may pass.
152
+ def union_members(value)
153
+ return value.members if value.is_a?(Type::Union)
154
+ return nil unless value.is_a?(Type::Dynamic) && value.static_facet.is_a?(Type::Union)
155
+
156
+ members = value.static_facet.members.reject { |member| nil_member?(member) }
157
+ members.empty? ? nil : members
158
+ end
159
+
160
+ def nil_member?(member)
161
+ return member.value.nil? if member.is_a?(Type::Constant)
162
+
163
+ member.is_a?(Type::Nominal) && member.class_name == "NilClass"
164
+ end
165
+
166
+ def combinations(choices)
167
+ choices.reduce([{}]) do |partials, (name, members)|
168
+ partials.flat_map { |partial| members.map { |member| partial.merge(name => member) } }
169
+ end
170
+ end
171
+
172
+ def declares?(fun)
173
+ return false unless fun.respond_to?(:required_keywords)
174
+
175
+ !fun.required_keywords.empty? || !fun.optional_keywords.empty? || !fun.rest_keywords.nil?
176
+ end
177
+
178
+ # The keyword hash `fun` takes as its keywords, or nil when the call passes none or `fun` declares none.
179
+ def keyword_hash(fun, arg_types, keywords_last)
180
+ return nil unless keywords_last && !arg_types.empty? && declares?(fun)
181
+
182
+ arg_types.last
183
+ end
184
+
185
+ # {.accepted?} for the selector's `shared` bundle: the keyword hash `fun` takes, if any, against its keywords.
186
+ def accepted_by?(fun, shared, strict, &)
187
+ accepted?(fun, keyword_hash(fun, shared[:arg_types], shared[:keywords_last]), strict, &)
188
+ end
189
+
190
+ def any_declares?(method_types) = method_types.any? { |method_type| declares?(method_type.type) }
191
+
192
+ # Issue #1800 — a keyword hash an overload declares and takes as its keywords is no positional argument of
193
+ # another. Both RBS (the first overload in declared order that takes the call) and Ruby (a method that accepts
194
+ # keywords binds `a: 1` to them) read `(a: Integer) -> Integer | (Object) -> String`'s `ob(a: 1)` as the
195
+ # keyword overload, but `ReceiverAffinity` moves `(Object)` first and the strict pass took it, reading the hash
196
+ # positionally. `matches` is a selection pass's answer for a call with a keyword hash; the block answers the
197
+ # keyword-declaring overloads' proven (`:yes`, every pair accepted with a `yes`) or gradual (`false`) matches.
198
+ # A `maybe` proves nothing: a source-only class is a `maybe` instance of every declared class, so
199
+ # `(a: Integer) | (Object)` would have taken `i(a: Bar.new)` as `Integer`. When one is proven, a
200
+ # positional reader declared after it is dropped. One declared before it is what RBS takes, while a method
201
+ # accepting keywords still binds them as keywords, so both stay and the caller joins their returns. A keyword
202
+ # overload that only gradually takes the hash (an unshaped `**opts`, an imprecise value) proves nothing either
203
+ # way, so it joins the positional readers rather than replace them.
204
+ def prefer_takers(declared, matches)
205
+ readers = matches.reject { |method_type| declares?(method_type.type) }
206
+ return matches if readers.empty?
207
+
208
+ takers = declared.select { |method_type| declares?(method_type.type) }
209
+ proven = yield(takers, :yes).first
210
+ unless proven
211
+ joined = matches | yield(takers, false)
212
+ return joined.size == matches.size ? matches : in_declared_order(declared, joined)
213
+ end
214
+
215
+ position = declared.index(proven)
216
+ earlier = readers.select { |method_type| declared.index(method_type) < position }
217
+ kept = matches.select { |method_type| declares?(method_type.type) }
218
+ in_declared_order(declared, kept | [proven] | earlier)
219
+ end
220
+
221
+ def in_declared_order(declared, method_types)
222
+ method_types.sort_by { |method_type| declared.index(method_type) }
223
+ end
224
+
225
+ # Whether `fun`'s keywords take `keywords` (nil for a call without a keyword hash). Without one, an
226
+ # overload that requires a keyword is not viable. With one, a closed shape must carry every required
227
+ # keyword, and every key it carries must be declared (or taken by `**rest`) with a value the block accepts
228
+ # against the declaration. An unshaped keyword hash (`f(**opts)` over a `Hash`) or a shape that may lack a
229
+ # required key is a gradual match only, never a strict one.
230
+ #
231
+ # @yieldparam param — the keyword's `RBS::Types::Function::Param`.
232
+ # @yieldparam value — the type the call passes for it.
233
+ def accepted?(fun, keywords, strict)
234
+ return true unless fun.respond_to?(:required_keywords)
235
+ return fun.required_keywords.empty? if keywords.nil?
236
+ return !strict unless keywords.is_a?(Type::HashShape)
237
+
238
+ required_present?(fun, keywords, strict) &&
239
+ keywords.pairs.all? do |name, value|
240
+ param = fun.required_keywords[name] || fun.optional_keywords[name] || fun.rest_keywords
241
+ !param.nil? && yield(param, value)
242
+ end
243
+ end
244
+
245
+ # The declarations the keys of the call's keyword hash land in, for the selector's `shared` bundle; empty
246
+ # without a shaped keyword hash `fun` takes (a key nothing declares lands nowhere).
247
+ def passed_params(fun, shared)
248
+ keywords = keyword_hash(fun, shared[:arg_types], shared[:keywords_last])
249
+ return NO_PARAMS unless keywords.is_a?(Type::HashShape)
250
+
251
+ keywords.pairs.each_key.filter_map do |name|
252
+ fun.required_keywords[name] || fun.optional_keywords[name] || fun.rest_keywords
253
+ end
254
+ end
255
+
256
+ def required_present?(fun, keywords, strict)
257
+ fun.required_keywords.each_key.all? do |name|
258
+ next true if keywords.pairs.key?(name) && !keywords.optional_key?(name)
259
+
260
+ # Possibly absent: an optional key, or a key an open shape may carry unseen.
261
+ !strict && (keywords.pairs.key?(name) || keywords.open?)
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
@@ -35,8 +35,8 @@ module Rigor
35
35
  # beginless),
36
36
  # - the slice is empty.
37
37
  #
38
- # K is read from `$1`..`$9` only: `Scope#forget_match_globals` does not clear `$10` and above (#1384), so a
39
- # binding there may belong to an earlier match.
38
+ # K is read from `$1`..`$9` only: the fold declines an index above nine rather than widen its reach.
39
+ # `Scope#forget_match_globals` clears `$10` and above too (#1384), so a binding there is never stale.
40
40
  module MatchDataFolding
41
41
  MAX_GROUP_GLOBAL = 9
42
42
  private_constant :MAX_GROUP_GLOBAL
@@ -12,9 +12,9 @@ module Rigor
12
12
  # The member-shape projections shared by {DataFolding} and {StructFolding}. A `DataInstance` and a
13
13
  # `StructInstance` expose the same surface — an ordered `members` map, `member_names`, and a
14
14
  # `class_name` — so the value projections off that surface (`[]` / `to_h` / `deconstruct` /
15
- # `deconstruct_keys` / `members` / `with`) and the reader-redefinition guard are identical between the
16
- # two folders. Only `#with`'s carrier constructor differs (`data_instance_of` vs
17
- # `struct_instance_of`), so it takes a block that builds the new instance from the merged member map.
15
+ # `deconstruct_keys` / `members`) and the reader-redefinition guard are identical between the two
16
+ # folders. `#with` is a projection of `Data` only (`Struct` has no `#with`, #1335); it takes a block that
17
+ # builds the new instance from the merged member map, so the carrier constructor stays with the caller.
18
18
  #
19
19
  # Both folders `extend` this module so the projections resolve as their own module functions
20
20
  # (matching their `module_function` style).
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../source/symbol_name"
3
4
  require_relative "../../type"
4
5
 
5
6
  module Rigor
@@ -94,7 +95,7 @@ module Rigor
94
95
 
95
96
  case arg.value
96
97
  when Symbol then arg.value
97
- when String then arg.value.to_sym
98
+ when String then Source::SymbolName.of(arg.value)
98
99
  end
99
100
  end
100
101
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Issue #1393 — `min` / `max` / `min_by` / `max_by` take an OPTIONAL count, and Ruby reads a `nil` count as
9
+ # "no count" (except blockless `Range#min` / `Range#max`, which raise; those are left alone):
10
+ # `[1, 2].min_by(nil) { |s| s }` is `1`, not `[1]`, and `[1, 2].max(nil)` is `2`. The RBS
11
+ # declares only `() ...` and `(Integer) ...` arms, so a `nil` argument matched none of them and the selector
12
+ # fell back to the blockless `Enumerator` arm — a confidently wrong type, and a false `call.undefined-method`
13
+ # on `xs.min_by(limit) { … }.succ` where `limit` may be `nil`.
14
+ #
15
+ # This splits the count argument. A `nil` part is dispatched as the no-argument call (so every no-count
16
+ # answer, including the #1333 non-empty fold, applies); a non-`nil` part is dispatched as written; a union
17
+ # of both answers the union of the two results. A count that has no `nil` part is left to the caller
18
+ # untouched. Only the core receivers whose methods these are (`Array`, `Hash`, `Range`, `Set`, and their
19
+ # shape and constant carriers) are rewritten, so a user class's own `min(limit)` is not reinterpreted.
20
+ module OptionalCount
21
+ module_function
22
+
23
+ METHODS = Set[:min, :max, :min_by, :max_by].freeze
24
+ # `Range` defines its own blockless `min` / `max`, which raise `TypeError` for a `nil` count in Ruby 4.0;
25
+ # only with a block does it fall through to `Enumerable`'s, which read `nil` as no count.
26
+ COUNTED_BY_ENUMERABLE_ONLY = Set[:min, :max].freeze
27
+ RECEIVER_CLASSES = Set["Array", "Hash", "Range", "Set"].freeze
28
+
29
+ # @return nil when the call is not a nil-count call; otherwise a one-element list holding the non-`nil`
30
+ # part of the count (itself nil when the count is only `nil`).
31
+ def split(receiver, method_name, args, block: true)
32
+ return nil unless METHODS.include?(method_name) && args.size == 1 && core_receiver?(receiver)
33
+ return nil if !block && COUNTED_BY_ENUMERABLE_ONLY.include?(method_name) && range_receiver?(receiver)
34
+
35
+ arg = args.first
36
+ members = arg.is_a?(Type::Union) ? arg.members : [arg]
37
+ return nil unless members.any? { |m| nil_constant?(m) }
38
+
39
+ rest = members.reject { |m| nil_constant?(m) }
40
+ [rest.empty? ? nil : Type::Combinator.union(*rest)]
41
+ end
42
+
43
+ def range_receiver?(receiver)
44
+ (receiver.is_a?(Type::Constant) && receiver.value.is_a?(Range)) ||
45
+ (receiver.is_a?(Type::Nominal) && receiver.class_name == "Range")
46
+ end
47
+
48
+ def nil_constant?(type)
49
+ type.is_a?(Type::Constant) && type.value.nil?
50
+ end
51
+
52
+ def core_receiver?(receiver)
53
+ case receiver
54
+ when Type::Tuple, Type::HashShape then true
55
+ when Type::Constant then receiver.value.is_a?(Range) || receiver.value.is_a?(::Set)
56
+ when Type::Nominal then RECEIVER_CLASSES.include?(receiver.class_name)
57
+ else false
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end