rigortype 0.1.16 → 0.1.18

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
  4. data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
  5. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
  6. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
  7. data/lib/rigor/analysis/check_rules.rb +180 -73
  8. data/lib/rigor/analysis/dependency_recorder.rb +122 -0
  9. data/lib/rigor/analysis/diagnostic.rb +18 -0
  10. data/lib/rigor/analysis/incremental.rb +162 -0
  11. data/lib/rigor/analysis/incremental_session.rb +337 -0
  12. data/lib/rigor/analysis/rule_catalog.rb +48 -0
  13. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
  14. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  15. data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
  16. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  17. data/lib/rigor/analysis/runner.rb +477 -1110
  18. data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
  19. data/lib/rigor/analysis/worker_session.rb +47 -8
  20. data/lib/rigor/builtins/static_return_refinements.rb +7 -1
  21. data/lib/rigor/cache/descriptor.rb +50 -49
  22. data/lib/rigor/cache/incremental_snapshot.rb +153 -0
  23. data/lib/rigor/cache/rbs_cache_producer.rb +34 -0
  24. data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
  25. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
  26. data/lib/rigor/cache/rbs_constant_table.rb +2 -8
  27. data/lib/rigor/cache/rbs_environment.rb +2 -8
  28. data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
  29. data/lib/rigor/cache/store.rb +145 -14
  30. data/lib/rigor/cli/annotate_command.rb +2 -7
  31. data/lib/rigor/cli/baseline_command.rb +2 -7
  32. data/lib/rigor/cli/check_command.rb +705 -0
  33. data/lib/rigor/cli/ci_detector.rb +94 -0
  34. data/lib/rigor/cli/command.rb +47 -0
  35. data/lib/rigor/cli/coverage_command.rb +3 -23
  36. data/lib/rigor/cli/coverage_renderer.rb +3 -8
  37. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  38. data/lib/rigor/cli/diff_command.rb +3 -7
  39. data/lib/rigor/cli/explain_command.rb +2 -7
  40. data/lib/rigor/cli/lsp_command.rb +3 -7
  41. data/lib/rigor/cli/mcp_command.rb +3 -7
  42. data/lib/rigor/cli/options.rb +57 -0
  43. data/lib/rigor/cli/plugin_command.rb +3 -7
  44. data/lib/rigor/cli/plugins_command.rb +2 -7
  45. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  46. data/lib/rigor/cli/renderable.rb +26 -0
  47. data/lib/rigor/cli/sig_gen_command.rb +2 -7
  48. data/lib/rigor/cli/skill_command.rb +3 -7
  49. data/lib/rigor/cli/trace_command.rb +143 -0
  50. data/lib/rigor/cli/trace_renderer.rb +310 -0
  51. data/lib/rigor/cli/triage_command.rb +2 -7
  52. data/lib/rigor/cli/type_of_command.rb +5 -38
  53. data/lib/rigor/cli/type_of_renderer.rb +4 -9
  54. data/lib/rigor/cli/type_scan_command.rb +3 -23
  55. data/lib/rigor/cli/type_scan_renderer.rb +4 -9
  56. data/lib/rigor/cli.rb +15 -532
  57. data/lib/rigor/configuration/dependencies.rb +18 -1
  58. data/lib/rigor/configuration/severity_profile.rb +22 -3
  59. data/lib/rigor/configuration.rb +16 -3
  60. data/lib/rigor/environment/rbs_loader.rb +129 -71
  61. data/lib/rigor/environment.rb +1 -1
  62. data/lib/rigor/inference/acceptance.rb +10 -0
  63. data/lib/rigor/inference/block_parameter_binder.rb +1 -2
  64. data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
  65. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
  66. data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
  67. data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
  68. data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
  69. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
  70. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
  71. data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
  72. data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
  73. data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
  74. data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
  75. data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
  76. data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
  77. data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
  78. data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
  79. data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
  80. data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
  81. data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
  82. data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
  83. data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
  84. data/lib/rigor/inference/expression_typer.rb +149 -63
  85. data/lib/rigor/inference/flow_tracer.rb +180 -0
  86. data/lib/rigor/inference/macro_block_self_type.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
  88. data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
  89. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
  90. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
  91. data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
  92. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
  93. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  94. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
  95. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
  96. data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
  97. data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
  98. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  99. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
  100. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
  101. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
  102. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
  103. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
  104. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
  105. data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
  106. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
  107. data/lib/rigor/inference/method_dispatcher.rb +185 -84
  108. data/lib/rigor/inference/narrowing.rb +262 -5
  109. data/lib/rigor/inference/scope_indexer.rb +208 -21
  110. data/lib/rigor/inference/statement_evaluator.rb +110 -48
  111. data/lib/rigor/language_server/buffer_resolution.rb +33 -0
  112. data/lib/rigor/language_server/completion_provider.rb +4 -4
  113. data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
  114. data/lib/rigor/language_server/folding_range_provider.rb +4 -4
  115. data/lib/rigor/language_server/hover_provider.rb +4 -4
  116. data/lib/rigor/language_server/selection_range_provider.rb +4 -4
  117. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  118. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  119. data/lib/rigor/plugin/base.rb +302 -45
  120. data/lib/rigor/plugin/node_rule_walk.rb +147 -0
  121. data/lib/rigor/plugin/registry.rb +281 -15
  122. data/lib/rigor/plugin.rb +1 -0
  123. data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
  124. data/lib/rigor/rbs_extended.rb +39 -0
  125. data/lib/rigor/scope/discovery_index.rb +58 -0
  126. data/lib/rigor/scope.rb +150 -167
  127. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  128. data/lib/rigor/source/literals.rb +14 -0
  129. data/lib/rigor/type/acceptance_router.rb +19 -0
  130. data/lib/rigor/type/accepts_result.rb +3 -10
  131. data/lib/rigor/type/app.rb +3 -7
  132. data/lib/rigor/type/bot.rb +2 -3
  133. data/lib/rigor/type/bound_method.rb +5 -12
  134. data/lib/rigor/type/combinator.rb +22 -0
  135. data/lib/rigor/type/constant.rb +2 -3
  136. data/lib/rigor/type/data_class.rb +80 -0
  137. data/lib/rigor/type/data_instance.rb +100 -0
  138. data/lib/rigor/type/difference.rb +5 -10
  139. data/lib/rigor/type/dynamic.rb +5 -10
  140. data/lib/rigor/type/hash_shape.rb +5 -15
  141. data/lib/rigor/type/integer_range.rb +5 -10
  142. data/lib/rigor/type/intersection.rb +5 -10
  143. data/lib/rigor/type/nominal.rb +5 -10
  144. data/lib/rigor/type/refined.rb +5 -10
  145. data/lib/rigor/type/singleton.rb +5 -10
  146. data/lib/rigor/type/top.rb +2 -3
  147. data/lib/rigor/type/tuple.rb +5 -10
  148. data/lib/rigor/type/union.rb +5 -10
  149. data/lib/rigor/type.rb +2 -0
  150. data/lib/rigor/value_semantics.rb +77 -0
  151. data/lib/rigor/version.rb +1 -1
  152. data/lib/rigor.rb +1 -1
  153. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
  156. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  157. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
  158. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  159. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  160. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  161. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  163. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  164. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  166. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  167. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  168. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
  169. data/sig/rigor/cache.rbs +19 -0
  170. data/sig/rigor/environment.rbs +0 -2
  171. data/sig/rigor/inference.rbs +27 -0
  172. data/sig/rigor/plugin/base.rbs +1 -2
  173. data/sig/rigor/rbs_extended.rbs +2 -0
  174. data/sig/rigor/scope.rbs +42 -25
  175. data/sig/rigor/source.rbs +1 -0
  176. data/sig/rigor/type.rbs +58 -1
  177. data/sig/rigor.rbs +6 -1
  178. data/skills/rigor-ci-setup/SKILL.md +319 -0
  179. metadata +36 -2
  180. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../type"
4
+ require_relative "singleton_folding"
4
5
 
5
6
  module Rigor
6
7
  module Inference
@@ -32,7 +33,10 @@ module Rigor
32
33
 
33
34
  # @return [Array<Rigor::Type>, nil] block-param types, or
34
35
  # nil to fall through to the next tier.
35
- def block_param_types(receiver:, method_name:, args:)
36
+ def block_param_types(context)
37
+ receiver = context.receiver
38
+ method_name = context.method_name
39
+ args = context.args
36
40
  case method_name
37
41
  when :times then times_block_params(receiver)
38
42
  when :upto then upto_block_params(receiver, args.first)
@@ -66,7 +70,7 @@ module Rigor
66
70
  end
67
71
 
68
72
  def class_metaclass_receiver?(type)
69
- type.is_a?(Type::Singleton) && type.class_name == "Class"
73
+ SingletonFolding.receiver?(type, "Class")
70
74
  end
71
75
 
72
76
  def times_block_params(receiver)
@@ -53,7 +53,10 @@ module Rigor
53
53
  INTEGER_REFINEMENT_PREDICATES = Set[:decimal_int, :numeric].freeze
54
54
  private_constant :INTEGER_REFINEMENT_PREDICATES
55
55
 
56
- def try_dispatch(receiver:, method_name:, args:)
56
+ def try_dispatch(context)
57
+ receiver = context.receiver
58
+ method_name = context.method_name
59
+ args = context.args
57
60
  return nil if receiver.nil?
58
61
  return try_array(args) if method_name == :Array
59
62
  return try_numeric_constructor(method_name, args) if NUMERIC_CONSTRUCTORS.key?(method_name)
@@ -80,7 +80,10 @@ module Rigor
80
80
  :LITERAL_PRESERVING_METHODS, :NON_EMPTY_LITERAL_PRESERVING_METHODS,
81
81
  :WIDTH_PADDING_METHODS
82
82
 
83
- def try_dispatch(receiver:, method_name:, args:, **)
83
+ def try_dispatch(context)
84
+ receiver = context.receiver
85
+ method_name = context.method_name
86
+ args = context.args
84
87
  return fold_array_join(receiver, args) if method_name == :join
85
88
  return fold_format(args) if FORMAT_METHODS.include?(method_name)
86
89
  return nil unless Type::Combinator.literal_string_compatible?(receiver)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../type"
4
+ require_relative "singleton_folding"
4
5
 
5
6
  module Rigor
6
7
  module Inference
@@ -57,8 +58,11 @@ module Rigor
57
58
  module_function
58
59
 
59
60
  # @return [Rigor::Type, nil] folded result, or nil to defer.
60
- def try_dispatch(receiver:, method_name:, args:)
61
- return nil unless dispatch_target?(receiver)
61
+ def try_dispatch(context)
62
+ receiver = context.receiver
63
+ method_name = context.method_name
64
+ args = context.args
65
+ return nil unless SingletonFolding.receiver?(receiver, "Math")
62
66
 
63
67
  # `log` is variadic (1 or 2 args), so it cannot live in the
64
68
  # fixed-arity sets above.
@@ -70,10 +74,6 @@ module Rigor
70
74
  nil
71
75
  end
72
76
 
73
- def dispatch_target?(receiver)
74
- receiver.is_a?(Type::Singleton) && receiver.class_name == "Math"
75
- end
76
-
77
77
  # Unwraps a numeric `Constant` argument to its Ruby value.
78
78
  # Returns nil for any non-`Constant` or non-`Numeric` carrier.
79
79
  def numeric_constant(arg)
@@ -51,7 +51,10 @@ module Rigor
51
51
  # @param args [Array<Rigor::Type>] caller's argument
52
52
  # types in order. Only the single-argument case
53
53
  # matches; other arities decline.
54
- def try_forward(receiver:, method_name:, args:)
54
+ def try_dispatch(context)
55
+ receiver = context.receiver
56
+ method_name = context.method_name
57
+ args = context.args
55
58
  return nil unless method_name == :method
56
59
  return nil if args.size != 1
57
60
 
@@ -72,7 +75,9 @@ module Rigor
72
75
  # well-defined type (the gradual-safety net mirrors
73
76
  # the engine's "BoundMethod erases to `Method`,
74
77
  # `Method#call: (*untyped) -> untyped`" RBS fallback).
75
- def try_backward(receiver:, method_name:, args:, block_type:, environment:, call_node:, scope:)
78
+ def try_backward(context)
79
+ receiver = context.receiver
80
+ method_name = context.method_name
76
81
  return nil unless receiver.is_a?(Type::BoundMethod)
77
82
  return nil unless backward_method?(method_name)
78
83
 
@@ -94,11 +99,11 @@ module Rigor
94
99
  MethodDispatcher.dispatch(
95
100
  receiver_type: receiver.receiver_type,
96
101
  method_name: receiver.method_name,
97
- arg_types: args,
98
- block_type: block_type,
99
- environment: environment,
100
- call_node: call_node,
101
- scope: scope
102
+ arg_types: context.args,
103
+ block_type: context.block_type,
104
+ environment: context.environment,
105
+ call_node: context.call_node,
106
+ scope: context.scope
102
107
  ) || Type::Combinator.untyped
103
108
  end
104
109
  # `Method#call` / `Method#()` and `Method#[]` are the
@@ -32,7 +32,16 @@ module Rigor
32
32
  # matches, accept the first arity-and-gradual-accept match
33
33
  # (the v0.1.1 behaviour). Alias / Interface / Intersection
34
34
  # params still reach this pass, so call sites whose only
35
- # candidate IS an alias-typed overload keep working.
35
+ # candidate IS an alias-typed overload keep working. One
36
+ # exclusion: an `untyped` argument does NOT gradually match
37
+ # a value-pinning param (`nil` / literal types — carriers
38
+ # that admit only specific values). Those overloads carry
39
+ # value-precise returns (`Kernel#Array: (nil) -> []`,
40
+ # `Regexp#=~: (nil) -> nil`) that would otherwise win purely
41
+ # by list position and inject false constants into the flow;
42
+ # they remain selectable when the argument PROVES the value
43
+ # (strict pass) or when no other overload matches (step 4's
44
+ # fallback picks the first overload regardless).
36
45
  # 4. If no overload matches at all, fall back to
37
46
  # `method_types.first` so existing call sites keep their
38
47
  # phase 1 / 2b behavior. This preserves the fail-soft
@@ -393,9 +402,32 @@ module Rigor
393
402
  instance_type: instance_type,
394
403
  type_vars: type_vars
395
404
  )
405
+ # An `untyped` arg gradually accepts against every param,
406
+ # so a value-pinning param would be "matched" with zero
407
+ # evidence and its value-precise return (`(nil) -> []`)
408
+ # would beat broader overloads purely by list position.
409
+ # Decline the pair; only the strict pass (where the arg
410
+ # proves the value) or the final first-overload fallback
411
+ # may select such an overload. (Pass 1 already skips
412
+ # untyped args entirely, so this only engages pass 2.)
413
+ return false if untyped_arg?(arg) && value_pinning?(param_type)
414
+
396
415
  result = param_type.accepts(arg, mode: :gradual)
397
416
  result.yes? || result.maybe?
398
417
  end
418
+
419
+ # A type that admits only specific VALUES rather than a
420
+ # class of values: a `Constant` carrier (RBS `nil` and
421
+ # literal types both translate to one) or a union made up
422
+ # entirely of them (`true | false`, `1 | 2`, `nil?`-style
423
+ # optionals of literals).
424
+ def value_pinning?(type)
425
+ case type
426
+ when Type::Constant then true
427
+ when Type::Union then type.members.all? { |member| value_pinning?(member) }
428
+ else false
429
+ end
430
+ end
399
431
  end
400
432
  end
401
433
  end
@@ -118,20 +118,20 @@ module Rigor
118
118
  # `Dynamic[Top]`, which is the false-positive-safe default
119
119
  # for the open hierarchies (`< ActionController::Base`, …)
120
120
  # the allow-list deliberately excludes.
121
- def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil, self_type_override: nil, # rubocop:disable Metrics/ParameterLists
122
- public_only: false, scope: nil)
121
+ def try_dispatch(context)
122
+ environment = context.environment
123
123
  return nil if environment.nil?
124
124
  return nil unless environment.rbs_loader
125
125
 
126
126
  dispatch_for(
127
- receiver: receiver,
128
- method_name: method_name,
129
- args: args,
127
+ receiver: context.receiver,
128
+ method_name: context.method_name,
129
+ args: context.args,
130
130
  environment: environment,
131
- block_type: block_type,
132
- self_type_override: self_type_override,
133
- public_only: public_only,
134
- scope: scope
131
+ block_type: context.block_type,
132
+ self_type_override: context.self_type_override,
133
+ public_only: context.public_only,
134
+ scope: context.scope
135
135
  )
136
136
  end
137
137
 
@@ -157,14 +157,15 @@ module Rigor
157
157
  # block" from "the block is untyped"; the binder treats both
158
158
  # the same way (every parameter defaults to `Dynamic[Top]`).
159
159
  # @return [Array<Rigor::Type>] positional block parameter types.
160
- def block_param_types(receiver:, method_name:, args:, environment:)
160
+ def block_param_types(context)
161
+ environment = context.environment
161
162
  return [] if environment.nil?
162
163
  return [] unless environment.rbs_loader
163
164
 
164
165
  probe_block_param_types(
165
- receiver: receiver,
166
- method_name: method_name,
167
- args: args,
166
+ receiver: context.receiver,
167
+ method_name: context.method_name,
168
+ args: context.args,
168
169
  environment: environment
169
170
  )
170
171
  end
@@ -248,6 +249,15 @@ module Rigor
248
249
  ["Array", :instance, tuple_type_args(receiver)]
249
250
  when Type::HashShape
250
251
  ["Hash", :instance, hash_shape_type_args(receiver)]
252
+ when Type::DataInstance
253
+ # ADR-48 — project a member-instance carrier to its tagging
254
+ # class (or the `Data` supertype) so non-member calls
255
+ # (`inspect`, `==`, `frozen?`, ...) resolve through RBS
256
+ # rather than mis-firing undefined-method. Member reads were
257
+ # already folded by DataFolding above this tier.
258
+ [receiver.class_name || "Data", :instance, []]
259
+ when Type::DataClass
260
+ [receiver.class_name || "Data", :singleton, []]
251
261
  when Type::BoundMethod
252
262
  # `BoundMethod` is a precision-bearing alias for
253
263
  # `Nominal[Method]`: it carries the
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../type"
4
+ require_relative "singleton_folding"
4
5
 
5
6
  module Rigor
6
7
  module Inference
@@ -30,26 +31,25 @@ module Rigor
30
31
  module_function
31
32
 
32
33
  # @return [Rigor::Type, nil] folded result, or nil to defer.
33
- def try_dispatch(receiver:, method_name:, args:)
34
- return nil unless dispatch_target?(receiver)
34
+ def try_dispatch(context)
35
+ receiver = context.receiver
36
+ method_name = context.method_name
37
+ args = context.args
38
+ return nil unless SingletonFolding.receiver?(receiver, "Regexp")
35
39
  return fold_escape(args) if REGEXP_ESCAPE_METHODS.include?(method_name)
36
40
  return fold_new(args) if method_name == :new
37
41
 
38
42
  nil
39
43
  end
40
44
 
41
- def dispatch_target?(receiver)
42
- receiver.is_a?(Type::Singleton) && receiver.class_name == "Regexp"
43
- end
44
-
45
45
  # `Regexp.escape(str)` / `.quote(str)` — one String arg.
46
46
  def fold_escape(args)
47
47
  return nil unless args.size == 1
48
48
 
49
- arg = args.first
50
- return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
49
+ str = SingletonFolding.constant_string(args.first)
50
+ return nil if str.nil?
51
51
 
52
- Type::Combinator.constant_of(Regexp.escape(arg.value))
52
+ Type::Combinator.constant_of(Regexp.escape(str))
53
53
  end
54
54
 
55
55
  # `Regexp.new(pattern)` / `Regexp.new(pattern, opts)` — constructs
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../type"
4
+ require_relative "singleton_folding"
4
5
 
5
6
  module Rigor
6
7
  module Inference
@@ -30,8 +31,11 @@ module Rigor
30
31
  module_function
31
32
 
32
33
  # @return [Rigor::Type, nil] folded result, or nil to defer.
33
- def try_dispatch(receiver:, method_name:, args:)
34
- return nil unless dispatch_target?(receiver)
34
+ def try_dispatch(context)
35
+ receiver = context.receiver
36
+ method_name = context.method_name
37
+ args = context.args
38
+ return nil unless SingletonFolding.receiver?(receiver, "Set")
35
39
 
36
40
  case method_name
37
41
  when :[] then fold_bracket(args)
@@ -39,10 +43,6 @@ module Rigor
39
43
  end
40
44
  end
41
45
 
42
- def dispatch_target?(receiver)
43
- receiver.is_a?(Type::Singleton) && receiver.class_name == "Set"
44
- end
45
-
46
46
  # `Set["a", "b", "c"]` — all positional args must be Constant.
47
47
  def fold_bracket(args)
48
48
  values = args.map do |a|
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../type"
4
+ require_relative "call_context"
4
5
 
5
6
  module Rigor
6
7
  module Inference
@@ -91,7 +92,13 @@ module Rigor
91
92
  values_at: :tuple_values_at,
92
93
  :+ => :tuple_concat,
93
94
  compact: :tuple_compact,
94
- take: :tuple_take
95
+ take: :tuple_take,
96
+ drop: :tuple_drop,
97
+ rotate: :tuple_rotate,
98
+ uniq: :tuple_uniq,
99
+ index: :tuple_find_index,
100
+ find_index: :tuple_find_index,
101
+ rindex: :tuple_rindex
95
102
  }.freeze
96
103
 
97
104
  HASH_SHAPE_HANDLERS = {
@@ -122,6 +129,7 @@ module Rigor
122
129
  values_at: :hash_values_at,
123
130
  fetch_values: :hash_fetch_values,
124
131
  assoc: :hash_assoc,
132
+ rassoc: :hash_rassoc,
125
133
  key: :hash_key,
126
134
  has_key?: :hash_has_key?,
127
135
  key?: :hash_has_key?,
@@ -167,7 +175,10 @@ module Rigor
167
175
  }.freeze
168
176
  private_constant :TO_S_BASE_REFINEMENTS
169
177
 
170
- def try_dispatch(receiver:, method_name:, args:)
178
+ def try_dispatch(context)
179
+ receiver = context.receiver
180
+ method_name = context.method_name
181
+ args = context.args
171
182
  args ||= []
172
183
  handler = RECEIVER_HANDLERS[receiver.class]
173
184
  return nil unless handler
@@ -545,7 +556,9 @@ module Rigor
545
556
  # falls through to the next dispatcher tier.
546
557
  def dispatch_intersection(intersection, method_name, args)
547
558
  results = intersection.members.filter_map do |member|
548
- ShapeDispatch.try_dispatch(receiver: member, method_name: method_name, args: args)
559
+ ShapeDispatch.try_dispatch(
560
+ CallContext.build(receiver: member, method_name: method_name, args: args)
561
+ )
549
562
  end
550
563
 
551
564
  case results.size
@@ -580,6 +593,10 @@ module Rigor
580
593
  Type::Combinator.integer_range(min, max)
581
594
  end
582
595
 
596
+ # `first` (no arg) → the first element (or `Constant[nil]` when
597
+ # empty). The `first(n)` arg-form is deliberately left to RBS
598
+ # overload selection (see the overload-selection specs) — folding
599
+ # it here would change that documented `Array[Elem]` contract.
583
600
  def tuple_first(tuple, _method_name, args)
584
601
  return nil unless args.empty?
585
602
  return Type::Combinator.constant_of(nil) if tuple.elements.empty?
@@ -832,21 +849,95 @@ module Rigor
832
849
  Type::Combinator.tuple_of(*kept)
833
850
  end
834
851
 
852
+ # `uniq` (no block) → `Tuple` of the first occurrence of each
853
+ # distinct value. Folds only when every element is a `Constant`
854
+ # so value equality is decidable; the block form defers.
855
+ def tuple_uniq(tuple, _method_name, args)
856
+ return nil unless args.empty?
857
+ return nil unless tuple.elements.all?(Type::Constant)
858
+
859
+ seen = []
860
+ kept = tuple.elements.each_with_object([]) do |element, acc|
861
+ next if seen.include?(element.value)
862
+
863
+ seen << element.value
864
+ acc << element
865
+ end
866
+ Type::Combinator.tuple_of(*kept)
867
+ end
868
+
869
+ # `index(obj)` / `find_index(obj)` → `Constant[Integer]` of the
870
+ # first element equal to `obj`, `Constant[nil]` when none match.
871
+ # Folds only for the argument form (the block form defers) when
872
+ # every element AND the argument are `Constant` (decidable
873
+ # equality).
874
+ def tuple_find_index(tuple, _method_name, args)
875
+ constant_index(tuple, args) { |elements, value| elements.index { |e| e.value == value } }
876
+ end
877
+
878
+ # `rindex(obj)` → the LAST matching index, same decidability gate.
879
+ def tuple_rindex(tuple, _method_name, args)
880
+ constant_index(tuple, args) { |elements, value| elements.rindex { |e| e.value == value } }
881
+ end
882
+
883
+ def constant_index(tuple, args)
884
+ return nil unless args.size == 1
885
+
886
+ needle = args.first
887
+ return nil unless needle.is_a?(Type::Constant)
888
+ return nil unless tuple.elements.all?(Type::Constant)
889
+
890
+ Type::Combinator.constant_of(yield(tuple.elements, needle.value))
891
+ end
892
+
835
893
  # `tuple.take(n)` — returns the first n elements as a
836
894
  # new Tuple. The argument must be a `Constant[Integer]`.
837
895
  # n <= 0 returns the empty Tuple; n >= size returns the
838
896
  # full receiver.
839
897
  def tuple_take(tuple, _method_name, args)
898
+ n = non_negative_count_arg(args)
899
+ return nil if n.nil?
900
+
901
+ Type::Combinator.tuple_of(*tuple.elements.take(n))
902
+ end
903
+
904
+ # `drop(n)` → `Tuple` of every element after the first `n`
905
+ # (mirror of `take`; `n >= size` → empty Tuple).
906
+ def tuple_drop(tuple, _method_name, args)
907
+ n = non_negative_count_arg(args)
908
+ return nil if n.nil?
909
+
910
+ Type::Combinator.tuple_of(*tuple.elements.drop(n))
911
+ end
912
+
913
+ # `rotate` (no arg → 1) / `rotate(n)` → `Tuple` of the elements
914
+ # cyclically shifted left by `n` (`Array#rotate` handles negative
915
+ # and out-of-range `n` by modulo, so any Integer arg folds).
916
+ def tuple_rotate(tuple, _method_name, args)
917
+ count =
918
+ if args.empty?
919
+ 1
920
+ else
921
+ arg = args.size == 1 ? args.first : nil
922
+ return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
923
+
924
+ arg.value
925
+ end
926
+ Type::Combinator.tuple_of(*tuple.elements.rotate(count))
927
+ end
928
+
929
+ # Unwraps a single non-negative `Constant[Integer]` count argument
930
+ # (the `take` / `drop` / `first(n)` / `last(n)` shape). Returns the
931
+ # Integer, or nil to defer (wrong arity, non-constant, non-Integer,
932
+ # or negative — `Array#take`/`#drop` raise on negative counts).
933
+ def non_negative_count_arg(args)
840
934
  return nil unless args.size == 1
841
935
 
842
936
  arg = args.first
843
- return nil unless arg.is_a?(Type::Constant)
844
- return nil unless arg.value.is_a?(Integer)
845
-
846
- n = arg.value
847
- return Type::Combinator.tuple_of if n <= 0
937
+ return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
938
+ return nil if arg.value.negative?
848
939
 
849
- Type::Combinator.tuple_of(*tuple.elements.take(n))
940
+ arg.value
850
941
  end
851
942
 
852
943
  # Returns `true` / `false` if every element's truthiness
@@ -1083,6 +1174,26 @@ module Rigor
1083
1174
  Type::Combinator.tuple_of(Type::Combinator.constant_of(key), shape.pairs[key])
1084
1175
  end
1085
1176
 
1177
+ # `shape.rassoc(value)` — reverse of `assoc`: returns
1178
+ # `Tuple[Constant[k], V]` for the first key whose VALUE equals
1179
+ # the argument, `Constant[nil]` when none match. Folds when every
1180
+ # value is a `Constant` so equality is decidable (mirrors
1181
+ # `hash_key`, which returns only the key).
1182
+ def hash_rassoc(shape, _method_name, args)
1183
+ return nil unless args.size == 1
1184
+ return nil unless shape.closed?
1185
+ return nil unless shape.optional_keys.empty?
1186
+ return nil unless shape.pairs.values.all?(Type::Constant)
1187
+
1188
+ arg = args.first
1189
+ return nil unless arg.is_a?(Type::Constant)
1190
+
1191
+ pair = shape.pairs.find { |_k, v| v.value == arg.value }
1192
+ return Type::Combinator.constant_of(nil) if pair.nil?
1193
+
1194
+ Type::Combinator.tuple_of(Type::Combinator.constant_of(pair.first), pair.last)
1195
+ end
1196
+
1086
1197
  # `shape.key(value)` — reverse lookup. Folds when every
1087
1198
  # value is a `Constant` so equality is decidable: returns
1088
1199
  # `Constant[k]` for the first matching key, `Constant[nil]`
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "shellwords"
4
4
  require_relative "../../type"
5
+ require_relative "singleton_folding"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -64,8 +65,11 @@ module Rigor
64
65
  module_function
65
66
 
66
67
  # @return [Rigor::Type, nil] folded result, or nil to defer.
67
- def try_dispatch(receiver:, method_name:, args:)
68
- return nil unless dispatch_target?(receiver)
68
+ def try_dispatch(context)
69
+ receiver = context.receiver
70
+ method_name = context.method_name
71
+ args = context.args
72
+ return nil unless SingletonFolding.receiver?(receiver, "Shellwords")
69
73
  return nil unless SHELLWORDS_ALL_METHODS.include?(method_name)
70
74
 
71
75
  if SHELLWORDS_ESCAPE_METHODS.include?(method_name)
@@ -77,18 +81,14 @@ module Rigor
77
81
  end
78
82
  end
79
83
 
80
- def dispatch_target?(receiver)
81
- receiver.is_a?(Type::Singleton) && receiver.class_name == "Shellwords"
82
- end
83
-
84
84
  # `Shellwords.escape(str)` / `.shellescape(str)` — one String arg.
85
85
  def fold_escape(args)
86
86
  return nil unless args.size == 1
87
87
 
88
- arg = args.first
89
- return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
88
+ str = SingletonFolding.constant_string(args.first)
89
+ return nil if str.nil?
90
90
 
91
- Type::Combinator.constant_of(Shellwords.escape(arg.value))
91
+ Type::Combinator.constant_of(Shellwords.escape(str))
92
92
  end
93
93
 
94
94
  # `Shellwords.split(line)` / `.shellsplit` / `.shellwords` —
@@ -96,10 +96,10 @@ module Rigor
96
96
  def fold_split(args)
97
97
  return nil unless args.size == 1
98
98
 
99
- arg = args.first
100
- return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
99
+ str = SingletonFolding.constant_string(args.first)
100
+ return nil if str.nil?
101
101
 
102
- tokens = Shellwords.split(arg.value)
102
+ tokens = Shellwords.split(str)
103
103
  return nil if tokens.size > SHELLWORDS_SPLIT_LIMIT
104
104
 
105
105
  Type::Combinator.tuple_of(*tokens.map { |t| Type::Combinator.constant_of(t) })
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Shared helpers for the per-singleton folding tiers (CGI, URI,
9
+ # Shellwords, Math, Time, Regexp, Set, File, …).
10
+ #
11
+ # Each of those tiers folds a pure module-/class-function call on
12
+ # `Constant` receivers, and every one of them opens with the same
13
+ # two questions:
14
+ #
15
+ # 1. "Is the receiver the `Singleton[X]` I handle?" — a one-line
16
+ # `is_a?(Type::Singleton) && class_name == "X"` that used to be
17
+ # copied verbatim into nine modules.
18
+ # 2. "Is this argument a foldable `Constant[String]`?" — the
19
+ # `is_a?(Type::Constant) && value.is_a?(String)` unwrap the
20
+ # string-folding tiers (CGI / URI / Shellwords / Regexp) gate
21
+ # on before evaluating the literal.
22
+ #
23
+ # Centralising both here keeps the receiver-shape and
24
+ # constant-unwrap knowledge in one place: if `Type::Singleton` or
25
+ # `Type::Constant` ever changes shape, there is a single edit
26
+ # rather than nine. The per-tier fold bodies stay where they are —
27
+ # only the shared gate moves.
28
+ module SingletonFolding
29
+ module_function
30
+
31
+ # True when `receiver` is the class/module singleton named
32
+ # `class_name` (e.g. `Singleton[Math]`).
33
+ def receiver?(receiver, class_name)
34
+ receiver.is_a?(Type::Singleton) && receiver.class_name == class_name
35
+ end
36
+
37
+ # The `String` value carried by a `Constant[String]` argument, or
38
+ # `nil` for any other carrier. Callers fold when this is non-nil
39
+ # and decline (return nil, deferring to the RBS tier) otherwise.
40
+ def constant_string(arg)
41
+ return nil unless arg.is_a?(Type::Constant)
42
+
43
+ value = arg.value
44
+ value.is_a?(String) ? value : nil
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../type"
4
+ require_relative "singleton_folding"
4
5
 
5
6
  module Rigor
6
7
  module Inference
@@ -33,8 +34,11 @@ module Rigor
33
34
  module_function
34
35
 
35
36
  # @return [Rigor::Type, nil] folded result, or nil to defer.
36
- def try_dispatch(receiver:, method_name:, args:)
37
- return nil unless dispatch_target?(receiver)
37
+ def try_dispatch(context)
38
+ receiver = context.receiver
39
+ method_name = context.method_name
40
+ args = context.args
41
+ return nil unless SingletonFolding.receiver?(receiver, "Time")
38
42
  return nil unless TIME_UTC_METHODS.include?(method_name)
39
43
  return nil unless args.size.between?(1, MAX_TIME_ARITY)
40
44
  return nil unless args.all?(Type::Constant)
@@ -46,10 +50,6 @@ module Rigor
46
50
  rescue StandardError
47
51
  nil
48
52
  end
49
-
50
- def dispatch_target?(receiver)
51
- receiver.is_a?(Type::Singleton) && receiver.class_name == "Time"
52
- end
53
53
  end
54
54
  end
55
55
  end