rigortype 0.1.2 → 0.1.4

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +135 -31
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +113 -0
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +72 -0
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +139 -0
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +200 -0
  9. data/lib/rigor/analysis/dependency_source_inference.rb +38 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +11 -3
  12. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  13. data/lib/rigor/analysis/runner.rb +206 -6
  14. data/lib/rigor/builtins/imported_refinements.rb +360 -55
  15. data/lib/rigor/cache/descriptor.rb +59 -6
  16. data/lib/rigor/cache/store.rb +1 -1
  17. data/lib/rigor/cli/diff_command.rb +1 -1
  18. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  19. data/lib/rigor/cli/type_of_command.rb +1 -1
  20. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  21. data/lib/rigor/cli/type_scan_report.rb +2 -2
  22. data/lib/rigor/cli.rb +9 -1
  23. data/lib/rigor/configuration/dependencies.rb +235 -0
  24. data/lib/rigor/configuration.rb +45 -11
  25. data/lib/rigor/environment.rb +47 -4
  26. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  27. data/lib/rigor/flow_contribution/element.rb +1 -1
  28. data/lib/rigor/flow_contribution/fact.rb +1 -1
  29. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  30. data/lib/rigor/flow_contribution/merger.rb +7 -3
  31. data/lib/rigor/flow_contribution.rb +2 -2
  32. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  33. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  34. data/lib/rigor/inference/expression_typer.rb +67 -11
  35. data/lib/rigor/inference/fallback.rb +1 -1
  36. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  37. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
  38. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  39. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  40. data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
  41. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
  42. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  43. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
  44. data/lib/rigor/inference/method_dispatcher.rb +233 -2
  45. data/lib/rigor/inference/method_parameter_binder.rb +1 -3
  46. data/lib/rigor/inference/narrowing.rb +2 -4
  47. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  48. data/lib/rigor/inference/scope_indexer.rb +14 -9
  49. data/lib/rigor/inference/statement_evaluator.rb +70 -6
  50. data/lib/rigor/plugin/io_boundary.rb +0 -2
  51. data/lib/rigor/plugin/loader.rb +2 -2
  52. data/lib/rigor/plugin/manifest.rb +49 -7
  53. data/lib/rigor/plugin/registry.rb +11 -0
  54. data/lib/rigor/plugin/services.rb +1 -1
  55. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  56. data/lib/rigor/plugin.rb +1 -0
  57. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  58. data/lib/rigor/rbs_extended.rb +131 -32
  59. data/lib/rigor/scope.rb +25 -8
  60. data/lib/rigor/sig_gen/classification.rb +36 -0
  61. data/lib/rigor/sig_gen/generator.rb +1048 -0
  62. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  63. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  64. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  65. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  66. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  67. data/lib/rigor/sig_gen/renderer.rb +157 -0
  68. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  69. data/lib/rigor/sig_gen/write_result.rb +48 -0
  70. data/lib/rigor/sig_gen/writer.rb +530 -0
  71. data/lib/rigor/sig_gen.rb +25 -0
  72. data/lib/rigor/type/bound_method.rb +79 -0
  73. data/lib/rigor/type/combinator.rb +195 -2
  74. data/lib/rigor/type/constant.rb +13 -0
  75. data/lib/rigor/type/hash_shape.rb +0 -2
  76. data/lib/rigor/type/union.rb +20 -1
  77. data/lib/rigor/type.rb +1 -0
  78. data/lib/rigor/type_node/generic.rb +62 -0
  79. data/lib/rigor/type_node/identifier.rb +30 -0
  80. data/lib/rigor/type_node/indexed_access.rb +41 -0
  81. data/lib/rigor/type_node/integer_literal.rb +29 -0
  82. data/lib/rigor/type_node/name_scope.rb +52 -0
  83. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  84. data/lib/rigor/type_node/string_literal.rb +29 -0
  85. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  86. data/lib/rigor/type_node/union.rb +42 -0
  87. data/lib/rigor/type_node.rb +29 -0
  88. data/lib/rigor/version.rb +1 -1
  89. data/lib/rigor.rb +2 -0
  90. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  91. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  92. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  93. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  94. data/sig/rigor/cli/diff_command.rbs +4 -0
  95. data/sig/rigor/cli/explain_command.rbs +4 -0
  96. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  97. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  98. data/sig/rigor/environment.rbs +6 -2
  99. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  100. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  101. data/sig/rigor/inference/builtins.rbs +2 -0
  102. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  103. data/sig/rigor/plugin/base.rbs +6 -0
  104. data/sig/rigor/plugin/fact_store.rbs +11 -0
  105. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  106. data/sig/rigor/plugin/load_error.rbs +6 -0
  107. data/sig/rigor/plugin/loader.rbs +20 -0
  108. data/sig/rigor/plugin/manifest.rbs +9 -0
  109. data/sig/rigor/plugin/registry.rbs +3 -0
  110. data/sig/rigor/plugin/services.rbs +3 -0
  111. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  112. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  113. data/sig/rigor/plugin.rbs +8 -0
  114. data/sig/rigor/scope.rbs +4 -2
  115. data/sig/rigor/type.rbs +28 -6
  116. metadata +58 -1
@@ -28,7 +28,7 @@ module Rigor
28
28
  role
29
29
  ].freeze
30
30
 
31
- Element = Data.define(:target, :edge, :kind, :payload, :provenance) do
31
+ class Element < Data.define(:target, :edge, :kind, :payload, :provenance)
32
32
  def initialize(target:, edge:, kind:, payload:, provenance:)
33
33
  unless ELEMENT_VALID_EDGES.include?(edge)
34
34
  raise ArgumentError,
@@ -55,7 +55,7 @@ module Rigor
55
55
  # land in the same merge bucket.
56
56
  FACT_VALID_TARGET_KINDS = %i[parameter self].freeze
57
57
 
58
- Fact = Data.define(:target_kind, :target_name, :type, :negative) do
58
+ class Fact < Data.define(:target_kind, :target_name, :type, :negative)
59
59
  def initialize(target_kind:, target_name:, type:, negative: false)
60
60
  unless FACT_VALID_TARGET_KINDS.include?(target_kind)
61
61
  raise ArgumentError,
@@ -42,7 +42,7 @@ module Rigor
42
42
  !@conflicts.empty?
43
43
  end
44
44
 
45
- def empty? # rubocop:disable Metrics/CyclomaticComplexity
45
+ def empty?
46
46
  @return_type.nil? && @truthy_facts.empty? && @falsey_facts.empty? &&
47
47
  @post_return_facts.empty? && @mutations.empty? && @invalidations.empty? &&
48
48
  @exceptional.nil? && @role_conformance.empty?
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "conflict"
4
+ require_relative "fact"
5
+ require_relative "merge_result"
6
+
3
7
  module Rigor
4
8
  class FlowContribution
5
9
  # Composes any number of {FlowContribution} bundles into a
@@ -51,7 +55,7 @@ module Rigor
51
55
  # In every conflict case the result keeps the higher-tier value
52
56
  # for that slot, records a {Conflict} with both provenances, and
53
57
  # continues processing the remaining slots / contributions.
54
- module Merger # rubocop:disable Metrics/ModuleLength
58
+ module Merger
55
59
  AUTHORITY_TIERS = {
56
60
  builtin: 0,
57
61
  rbs_extended: 1,
@@ -108,7 +112,7 @@ module Rigor
108
112
  fold_role_conformance(state, contribution)
109
113
  end
110
114
 
111
- def fold_return_type(state, contribution, tier) # rubocop:disable Metrics/AbcSize
115
+ def fold_return_type(state, contribution, tier)
112
116
  incoming = contribution.return_type
113
117
  return if incoming.nil?
114
118
 
@@ -198,7 +202,7 @@ module Rigor
198
202
  end
199
203
  end
200
204
 
201
- def build_conflict(target:, edge:, kind:, reason:, provenances:, message:) # rubocop:disable Metrics/ParameterLists
205
+ def build_conflict(target:, edge:, kind:, reason:, provenances:, message:)
202
206
  Conflict.new(target: target, edge: edge, kind: kind, reason: reason,
203
207
  provenances: provenances, message: message)
204
208
  end
@@ -32,7 +32,7 @@ module Rigor
32
32
  # `descriptor` is the {Rigor::Cache::Descriptor} this
33
33
  # contribution attaches to (or `nil` when the contribution does
34
34
  # not need its own cache slice).
35
- Provenance = Data.define(:source_family, :plugin_id, :node, :descriptor) do
35
+ class Provenance < Data.define(:source_family, :plugin_id, :node, :descriptor)
36
36
  def self.builtin
37
37
  new(source_family: :builtin, plugin_id: nil, node: nil, descriptor: nil)
38
38
  end
@@ -122,7 +122,7 @@ module Rigor
122
122
  # | role_conformance | normal | role | (per-role target) |
123
123
  #
124
124
  # @return [Array<Element>]
125
- def to_element_list # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
125
+ def to_element_list # rubocop:disable Metrics/AbcSize
126
126
  elements = []
127
127
  elements << element_for(:return, :normal, :return_type, return_type) unless return_type.nil?
128
128
  Array(truthy_facts).each { |fact| elements << element_for(fact_target(fact), :truthy, :truthy_fact, fact) }
@@ -45,7 +45,6 @@ module Rigor
45
45
  # scope MUST NOT observe them and the binder leaves them unbound.
46
46
  #
47
47
  # See docs/internal-spec/inference-engine.md for the binding contract.
48
- # rubocop:disable Metrics/ClassLength
49
48
  class BlockParameterBinder
50
49
  # @param expected_param_types [Array<Rigor::Type>] positional block
51
50
  # parameter types in order. Indices the binder cannot fill from
@@ -208,6 +207,5 @@ module Rigor
208
207
  @expected_param_types[index] || Type::Combinator.untyped
209
208
  end
210
209
  end
211
- # rubocop:enable Metrics/ClassLength
212
210
  end
213
211
  end
@@ -24,7 +24,7 @@ module Rigor
24
24
  # hot inference path: it allocates a tracer per visited node and discards
25
25
  # the inferred type values.
26
26
  class CoverageScanner
27
- Result = Data.define(:visits, :unrecognized, :events) do
27
+ class Result < Data.define(:visits, :unrecognized, :events)
28
28
  # @return [Integer] sum of all visits across node classes.
29
29
  def visited_count
30
30
  visits.values.sum
@@ -111,7 +111,21 @@ module Rigor
111
111
  Prism::IndexOrWriteNode => :type_of_assignment_write,
112
112
  Prism::IndexAndWriteNode => :type_of_assignment_write,
113
113
  Prism::MultiWriteNode => :type_of_assignment_write,
114
+ # LHS-only target nodes (destructuring assignment, pattern matching,
115
+ # `for x in xs`, block parameter `|a, (b, c)|`). They have no value
116
+ # to extract — the type-of pass acknowledges the node class so the
117
+ # coverage scanner stops flagging it; binding the inner names back
118
+ # into the scope is the StatementEvaluator / MultiTargetBinder /
119
+ # BlockParameterBinder side's concern.
114
120
  Prism::LocalVariableTargetNode => :type_of_non_value,
121
+ Prism::MultiTargetNode => :type_of_non_value,
122
+ Prism::InstanceVariableTargetNode => :type_of_non_value,
123
+ Prism::ClassVariableTargetNode => :type_of_non_value,
124
+ Prism::GlobalVariableTargetNode => :type_of_non_value,
125
+ Prism::ConstantTargetNode => :type_of_non_value,
126
+ Prism::ConstantPathTargetNode => :type_of_non_value,
127
+ Prism::CallTargetNode => :type_of_non_value,
128
+ Prism::IndexTargetNode => :type_of_non_value,
115
129
  # Hashes and interpolation
116
130
  Prism::HashNode => :type_of_hash,
117
131
  Prism::KeywordHashNode => :type_of_hash,
@@ -931,7 +945,6 @@ module Rigor
931
945
  # for the CallNode itself (the inner type_of calls already record
932
946
  # their own fallbacks for unrecognised receivers/args, so the tracer
933
947
  # captures both the immediate dispatch miss and the deeper cause).
934
- # rubocop:disable Metrics/CyclomaticComplexity
935
948
  def call_type_for(node)
936
949
  receiver = call_receiver_type_for(node)
937
950
  arg_types = call_arg_types(node)
@@ -1004,7 +1017,6 @@ module Rigor
1004
1017
 
1005
1018
  fallback_for(node, family: :prism)
1006
1019
  end
1007
- # rubocop:enable Metrics/CyclomaticComplexity
1008
1020
 
1009
1021
  # v0.0.2 #5 — re-types the body of a user-defined
1010
1022
  # instance method with the call site's argument types
@@ -1160,9 +1172,20 @@ module Rigor
1160
1172
  # when typing the body raises (defensive against malformed
1161
1173
  # subtrees); the dispatcher then runs in its no-block-aware
1162
1174
  # path.
1175
+ #
1176
+ # ADR-14 gap-#3 (d): a `Prism::BlockArgumentNode` carrying
1177
+ # `&:symbol` (the Symbol#to_proc shorthand) is treated as
1178
+ # a block. The block's return type is computed by
1179
+ # dispatching `:symbol` on the expected block param type
1180
+ # (per `Symbol#to_proc`'s `{ |x| x.symbol }` semantics).
1181
+ # A precise inner dispatch produces the right return; any
1182
+ # failure step falls back to `Dynamic[Top]` so the
1183
+ # dispatcher still SEES a block — selecting the block-
1184
+ # bearing overload of e.g. `Hash#transform_values` over
1185
+ # the no-block overload that returns `Enumerator`.
1163
1186
  def block_return_type_for(call_node, receiver_type, arg_types)
1164
- block_node = call_node.block
1165
- return nil unless block_node.is_a?(Prism::BlockNode)
1187
+ block_arg = call_node.block
1188
+ return nil if block_arg.nil?
1166
1189
  return nil if receiver_type.nil?
1167
1190
 
1168
1191
  expected = MethodDispatcher.expected_block_param_types(
@@ -1171,13 +1194,50 @@ module Rigor
1171
1194
  arg_types: arg_types,
1172
1195
  environment: scope.environment
1173
1196
  )
1174
- bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
1175
- block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1176
- type_block_body(block_node, block_scope)
1197
+ block_return_for(block_arg, expected)
1177
1198
  rescue StandardError
1178
1199
  nil
1179
1200
  end
1180
1201
 
1202
+ def block_return_for(block_arg, expected)
1203
+ case block_arg
1204
+ when Prism::BlockNode
1205
+ bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
1206
+ block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1207
+ type_block_body(block_arg, block_scope)
1208
+ when Prism::BlockArgumentNode
1209
+ symbol_block_return_type(block_arg, expected)
1210
+ end
1211
+ end
1212
+
1213
+ # `&:symbol` desugars to a one-arg Proc that dispatches
1214
+ # `symbol` against its argument. When the param type is
1215
+ # known and the resulting inner dispatch is precise,
1216
+ # this returns the precise carrier; otherwise it
1217
+ # returns `Dynamic[Top]` (still non-nil) so the outer
1218
+ # dispatcher selects the block-bearing overload.
1219
+ # `&proc_local` / `&method(:foo)` and friends — anything
1220
+ # not a bare SymbolNode — still resolve to
1221
+ # `Dynamic[Top]` for the same block-presence signal.
1222
+ def symbol_block_return_type(block_arg, expected_param_types)
1223
+ expression = block_arg.expression
1224
+ return dynamic_top unless expression.is_a?(Prism::SymbolNode)
1225
+
1226
+ param_type = expected_param_types&.first
1227
+ return dynamic_top if param_type.nil?
1228
+
1229
+ result = MethodDispatcher.dispatch(
1230
+ receiver_type: param_type,
1231
+ method_name: expression.unescaped.to_sym,
1232
+ arg_types: [],
1233
+ block_type: nil,
1234
+ environment: scope.environment,
1235
+ call_node: block_arg,
1236
+ scope: scope
1237
+ )
1238
+ result || dynamic_top
1239
+ end
1240
+
1181
1241
  def type_block_body(block_node, block_scope)
1182
1242
  body = block_node.body
1183
1243
  return Type::Combinator.constant_of(nil) if body.nil?
@@ -1213,7 +1273,6 @@ module Rigor
1213
1273
  PER_ELEMENT_RANGE_LIMIT = 8
1214
1274
  private_constant :PER_ELEMENT_RANGE_LIMIT
1215
1275
 
1216
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1217
1276
  def try_per_element_block_fold(call_node, receiver_type)
1218
1277
  return nil unless PER_ELEMENT_TUPLE_METHODS.include?(call_node.name)
1219
1278
  return nil if find_family_with_args?(call_node)
@@ -1231,7 +1290,6 @@ module Rigor
1231
1290
 
1232
1291
  assemble_per_element_result(call_node.name, per_position, element_types)
1233
1292
  end
1234
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1235
1293
 
1236
1294
  # Returns the per-position element types for a finite,
1237
1295
  # statically-known receiver shape — or nil when the
@@ -1254,7 +1312,6 @@ module Rigor
1254
1312
  end
1255
1313
  end
1256
1314
 
1257
- # rubocop:disable Metrics/CyclomaticComplexity
1258
1315
  def constant_range_elements(value)
1259
1316
  return nil unless value.is_a?(Range)
1260
1317
  return nil unless value.begin.is_a?(Integer) && value.end.is_a?(Integer)
@@ -1264,7 +1321,6 @@ module Rigor
1264
1321
 
1265
1322
  value.to_a.map { |v| Type::Combinator.constant_of(v) }
1266
1323
  end
1267
- # rubocop:enable Metrics/CyclomaticComplexity
1268
1324
 
1269
1325
  # `index(value)` and `find_index(value)` carry a positional
1270
1326
  # argument and search by `==` rather than running the block.
@@ -20,7 +20,7 @@ module Rigor
20
20
  # - inner_type: the Rigor::Type returned to the caller (currently
21
21
  # always Dynamic[Top]; later slices may carry richer fallback
22
22
  # types).
23
- Fallback = Data.define(:node_class, :location, :family, :inner_type) do
23
+ class Fallback < Data.define(:node_class, :location, :family, :inner_type)
24
24
  def initialize(node_class:, location:, family:, inner_type:)
25
25
  raise ArgumentError, "node_class must be a Class, got #{node_class.class}" unless node_class.is_a?(Class)
26
26
 
@@ -37,7 +37,7 @@ module Rigor
37
37
  # tuple — element-wise block re-evaluation against
38
38
  # `Constant<Array>` receivers (the `map` / `filter_map` /
39
39
  # `flat_map` precision tier) is reserved for a later slice.
40
- module BlockFolding # rubocop:disable Metrics/ModuleLength
40
+ module BlockFolding
41
41
  module_function
42
42
 
43
43
  FILTER_KEEP_ON_TRUTHY = Set[:select, :filter, :take_while].freeze
@@ -69,7 +69,6 @@ module Rigor
69
69
  # the call's block. `nil` means "no block at the call site"
70
70
  # and disqualifies every rule here.
71
71
  # @return [Rigor::Type, nil]
72
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
73
72
  def try_fold(receiver:, method_name:, args:, block_type:)
74
73
  return nil if receiver.nil? || block_type.nil?
75
74
 
@@ -86,7 +85,6 @@ module Rigor
86
85
  fold_count(receiver, truthiness, args)
87
86
  end
88
87
  end
89
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
88
 
91
89
  def filter_method?(method_name)
92
90
  FILTER_KEEP_ON_TRUTHY.include?(method_name) ||
@@ -154,7 +152,7 @@ module Rigor
154
152
  end
155
153
 
156
154
  # @return [:always_true, :always_false, :bool, nil]
157
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
155
+ # rubocop:disable Metrics/CyclomaticComplexity
158
156
  def predicate_decision(method_name, truthiness, emptiness)
159
157
  case method_name
160
158
  when :all?
@@ -177,7 +175,7 @@ module Rigor
177
175
  :bool
178
176
  end
179
177
  end
180
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
178
+ # rubocop:enable Metrics/CyclomaticComplexity
181
179
 
182
180
  def bool_union
183
181
  Type::Combinator.union(
@@ -149,7 +149,6 @@ module Rigor
149
149
  # and runs the format. Symbol keys are kept as
150
150
  # Symbols (matching Ruby's `%{key}` resolution).
151
151
  # Anything else declines so the RBS tier widens.
152
- # rubocop:disable Metrics/CyclomaticComplexity
153
152
  def try_fold_string_format(receiver, method_name, args)
154
153
  return nil unless method_name == :%
155
154
  return nil unless args.size == 1
@@ -166,7 +165,6 @@ module Rigor
166
165
  rescue StandardError
167
166
  nil
168
167
  end
169
- # rubocop:enable Metrics/CyclomaticComplexity
170
168
 
171
169
  def format_argument_value(arg)
172
170
  case arg
@@ -275,7 +273,6 @@ module Rigor
275
273
  [result]
276
274
  end
277
275
 
278
- # rubocop:disable Metrics/CyclomaticComplexity
279
276
  def try_fold_unary_set(receiver_values, method_name)
280
277
  range_lift = try_fold_range_constant_unary(receiver_values, method_name)
281
278
  return range_lift if range_lift
@@ -299,8 +296,6 @@ module Rigor
299
296
  end
300
297
  build_constant_type(results, source: receiver_values)
301
298
  end
302
- # rubocop:enable Metrics/CyclomaticComplexity
303
-
304
299
  # v0.0.7 — `Constant<Range>#to_a` and the no-arg
305
300
  # `first` / `last` / `min` / `max` short-circuit through a
306
301
  # Range-specific arm that catalog dispatch cannot reach:
@@ -353,7 +348,6 @@ module Rigor
353
348
  Type::Combinator.constant_of(edge == :first ? values.first : values.last)
354
349
  end
355
350
 
356
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
357
351
  def try_fold_binary_set(receiver_values, method_name, arg_values)
358
352
  string_lift = try_fold_string_array_binary(receiver_values, method_name, arg_values)
359
353
  return string_lift if string_lift
@@ -369,8 +363,6 @@ module Rigor
369
363
  end
370
364
  build_constant_type(results, source: receiver_values + arg_values)
371
365
  end
372
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
373
-
374
366
  # v0.0.7 — `Constant<String>#chars` / `bytes` / `lines` /
375
367
  # `split` (no-arg) return a Ruby Array of foldable
376
368
  # scalars; `foldable_constant_value?` rejects Array
@@ -425,7 +417,6 @@ module Rigor
425
417
  nil
426
418
  end
427
419
 
428
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
429
420
  def try_fold_pathname_binary(receiver_values, method_name, arg_values)
430
421
  return nil unless PATHNAME_PURE_BINARY.include?(method_name)
431
422
  return nil unless receiver_values.size == 1 && arg_values.size == 1
@@ -442,7 +433,6 @@ module Rigor
442
433
  rescue StandardError
443
434
  nil
444
435
  end
445
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
446
436
 
447
437
  def try_fold_string_array_unary(receiver_values, method_name)
448
438
  return nil unless STRING_ARRAY_UNARY_METHODS.include?(method_name)
@@ -459,7 +449,6 @@ module Rigor
459
449
  # `Constant<String>#split(arg)` / `#scan(arg)` — lift the
460
450
  # Array result to a Tuple when both sides are statically
461
451
  # known and the cardinality fits.
462
- # rubocop:disable Metrics/CyclomaticComplexity
463
452
  def try_fold_string_array_binary(receiver_values, method_name, arg_values)
464
453
  return nil unless STRING_ARRAY_BINARY_METHODS.include?(method_name)
465
454
  return nil unless receiver_values.size == 1 && arg_values.size == 1
@@ -473,7 +462,6 @@ module Rigor
473
462
  rescue StandardError
474
463
  nil
475
464
  end
476
- # rubocop:enable Metrics/CyclomaticComplexity
477
465
 
478
466
  def lift_array_result(result)
479
467
  return nil unless result.is_a?(Array)
@@ -27,12 +27,11 @@ module Rigor
27
27
  # - `a.downto(b) { |i| … }` yields the same domain `[b, a]`,
28
28
  # just iterated in reverse. Lower bound from the
29
29
  # argument, upper bound from the receiver.
30
- module IteratorDispatch # rubocop:disable Metrics/ModuleLength
30
+ module IteratorDispatch
31
31
  module_function
32
32
 
33
33
  # @return [Array<Rigor::Type>, nil] block-param types, or
34
34
  # nil to fall through to the next tier.
35
- # rubocop:disable Metrics/CyclomaticComplexity
36
35
  def block_param_types(receiver:, method_name:, args:)
37
36
  case method_name
38
37
  when :times then times_block_params(receiver)
@@ -45,7 +44,6 @@ module Rigor
45
44
  when :each_slice, :each_cons then slice_block_params(receiver)
46
45
  end
47
46
  end
48
- # rubocop:enable Metrics/CyclomaticComplexity
49
47
 
50
48
  def times_block_params(receiver)
51
49
  return nil unless integer_rooted?(receiver)
@@ -74,7 +74,7 @@ module Rigor
74
74
  private_constant :CONCAT_METHODS, :FORMAT_METHODS,
75
75
  :LITERAL_PRESERVING_METHODS, :WIDTH_PADDING_METHODS
76
76
 
77
- def try_dispatch(receiver:, method_name:, args:, **) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
77
+ def try_dispatch(receiver:, method_name:, args:, **)
78
78
  return fold_array_join(receiver, args) if method_name == :join
79
79
  return fold_format(args) if FORMAT_METHODS.include?(method_name)
80
80
 
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # `Method` (and friends) precision tier.
9
+ #
10
+ # Two folds make a `Method` carrier round-trip with its
11
+ # binding visible:
12
+ #
13
+ # 1. **Forward** — `<receiver>.method(:sym)` (or
14
+ # `.method("sym")`) lifts to {Type::BoundMethod}
15
+ # carrying the receiver type AND the resolved Symbol.
16
+ # Calling with a non-literal symbol-shaped argument
17
+ # declines so the RBS tier still answers
18
+ # `Nominal[Method]`.
19
+ # 2. **Backward** — `Type::BoundMethod#call(...)` /
20
+ # `#()` (Prism lowers `.()` into a CallNode whose
21
+ # `name` is `:call`) / `#[](...)` substitutes the
22
+ # bound `(receiver_type, method_name)` and recurses
23
+ # back into `MethodDispatcher.dispatch`. The
24
+ # re-entrant call lets the substituted dispatch
25
+ # consume every tier the original call site would
26
+ # have — constant folding, shape dispatch, RBS,
27
+ # plugin contributions, etc. The original block_type
28
+ # / environment / call_node / scope are threaded
29
+ # through unchanged so capture-sensitive tiers (the
30
+ # block fold) keep working.
31
+ #
32
+ # Lives ABOVE the standard precision-tier chain so the
33
+ # RBS tier never sees a `BoundMethod` receiver — `Method`
34
+ # erasure means RBS would otherwise return
35
+ # `Method#call: (*untyped) -> untyped`, which is exactly
36
+ # the precision loss the carrier exists to avoid.
37
+ module MethodFolding
38
+ module_function
39
+
40
+ # Forward fold. Returns a {Type::BoundMethod} when the
41
+ # call shape is `<receiver>.method(:name)` /
42
+ # `.method("name")` with a precisely-known Symbol /
43
+ # String argument. Declines on every other shape so
44
+ # the RBS tier still answers `Method` for non-folding
45
+ # cases.
46
+ #
47
+ # @param receiver [Rigor::Type] caller's receiver
48
+ # @param method_name [Symbol] the method being
49
+ # dispatched on `receiver` — only `:method` triggers
50
+ # the fold.
51
+ # @param args [Array<Rigor::Type>] caller's argument
52
+ # types in order. Only the single-argument case
53
+ # matches; other arities decline.
54
+ def try_forward(receiver:, method_name:, args:)
55
+ return nil unless method_name == :method
56
+ return nil if args.size != 1
57
+
58
+ bound_name = symbol_name_of(args.first)
59
+ return nil if bound_name.nil?
60
+
61
+ Type::Combinator.bound_method_of(receiver, bound_name)
62
+ end
63
+
64
+ # Backward fold. Recurses into `MethodDispatcher.dispatch`
65
+ # with the bound `(receiver_type, method_name)`. The
66
+ # `block_type` / `environment` / `call_node` / `scope`
67
+ # are forwarded so every downstream tier (constant
68
+ # folding, shape dispatch, plugin contributions, …)
69
+ # keeps the original call site's context. Returns
70
+ # `Dynamic[top]` rather than `nil` when the recursive
71
+ # dispatch declines so the call site still ends in a
72
+ # well-defined type (the gradual-safety net mirrors
73
+ # the engine's "BoundMethod erases to `Method`,
74
+ # `Method#call: (*untyped) -> untyped`" RBS fallback).
75
+ def try_backward(receiver:, method_name:, args:, block_type:, environment:, call_node:, scope:)
76
+ return nil unless receiver.is_a?(Type::BoundMethod)
77
+ return nil unless backward_method?(method_name)
78
+
79
+ MethodDispatcher.dispatch(
80
+ receiver_type: receiver.receiver_type,
81
+ method_name: receiver.method_name,
82
+ arg_types: args,
83
+ block_type: block_type,
84
+ environment: environment,
85
+ call_node: call_node,
86
+ scope: scope
87
+ ) || Type::Combinator.untyped
88
+ end
89
+ # `Method#call` / `Method#()` and `Method#[]` are the
90
+ # invocation entry points on the `Method` API; the
91
+ # alias `===` is also `call` semantically but is more
92
+ # commonly used as a case-equality predicate, so we
93
+ # do NOT fold through it (the case/when narrowing path
94
+ # already special-cases `===` for branch typing).
95
+ BACKWARD_METHOD_NAMES = %i[call []].freeze
96
+ private_constant :BACKWARD_METHOD_NAMES
97
+
98
+ def backward_method?(method_name)
99
+ BACKWARD_METHOD_NAMES.include?(method_name)
100
+ end
101
+
102
+ # `Object#method` accepts both Symbol and String at
103
+ # runtime (the latter coerced via `to_sym`). The
104
+ # `Constant<String>` form is rare in production code
105
+ # but cheap to support and matches Ruby's documented
106
+ # contract.
107
+ def symbol_name_of(arg)
108
+ return nil unless arg.is_a?(Type::Constant)
109
+
110
+ case arg.value
111
+ when Symbol then arg.value
112
+ when String then arg.value.to_sym
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -41,7 +41,7 @@ module Rigor
41
41
  # (instance vs singleton). Both kinds share the same arity and
42
42
  # acceptance shape; the difference is only in which `Definition`
43
43
  # the caller fetched.
44
- module OverloadSelector # rubocop:disable Metrics/ModuleLength
44
+ module OverloadSelector
45
45
  module_function
46
46
 
47
47
  # @param method_definition [RBS::Definition::Method]
@@ -64,8 +64,8 @@ module Rigor
64
64
  # back to the first declaration.
65
65
  # @return [RBS::MethodType, nil] the chosen overload, or nil
66
66
  # when the definition has no method types at all.
67
- # rubocop:disable Metrics/ParameterLists
68
- def select(method_definition, arg_types:, self_type:, instance_type:, type_vars: {}, block_required: false)
67
+ def select(method_definition, arg_types:, self_type:, instance_type:, type_vars: {}, block_required: false,
68
+ environment: nil)
69
69
  overloads = method_definition.method_types
70
70
  return nil if overloads.empty?
71
71
 
@@ -75,7 +75,7 @@ module Rigor
75
75
  # `accepts_param?` so overload selection sees the
76
76
  # tighter type when filtering candidates by argument
77
77
  # compatibility.
78
- param_overrides = RbsExtended.param_type_override_map(method_definition)
78
+ param_overrides = RbsExtended.param_type_override_map(method_definition, environment: environment)
79
79
 
80
80
  # Pass 1: prefer overloads whose param types stay strict —
81
81
  # no translator-induced `Dynamic[Top]` from Alias /
@@ -113,7 +113,6 @@ module Rigor
113
113
 
114
114
  overloads.first
115
115
  end
116
- # rubocop:enable Metrics/ParameterLists
117
116
 
118
117
  def overload_has_block?(method_type)
119
118
  method_type.respond_to?(:block) && method_type.block
@@ -122,7 +121,7 @@ module Rigor
122
121
  class << self
123
122
  private
124
123
 
125
- # rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
124
+ # rubocop:disable Metrics/ParameterLists
126
125
  def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
127
126
  param_overrides:, strict:)
128
127
  return nil if strict && arg_types.any? { |t| untyped_arg?(t) }
@@ -141,7 +140,7 @@ module Rigor
141
140
  )
142
141
  end
143
142
  end
144
- # rubocop:enable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
143
+ # rubocop:enable Metrics/ParameterLists
145
144
 
146
145
  # Treats the literal `untyped` carrier (`Dynamic[Top]`)
147
146
  # as too imprecise to drive a strict-pass match. Other
@@ -194,7 +193,6 @@ module Rigor
194
193
  end
195
194
  end
196
195
 
197
- # rubocop:disable Metrics/ParameterLists
198
196
  def matches?(method_type, arg_types, self_type:, instance_type:, type_vars:, param_overrides:)
199
197
  return false if method_type.respond_to?(:type_params) && rejects_keyword_required?(method_type)
200
198
 
@@ -213,7 +211,6 @@ module Rigor
213
211
  )
214
212
  end
215
213
  end
216
- # rubocop:enable Metrics/ParameterLists
217
214
 
218
215
  # Slice 4 phase 2c does not pass keyword arguments through the
219
216
  # call site (caller passes only positional `arg_types`). An
@@ -258,7 +255,6 @@ module Rigor
258
255
  head
259
256
  end
260
257
 
261
- # rubocop:disable Metrics/ParameterLists
262
258
  def accepts_param?(param, arg, self_type:, instance_type:, type_vars:, param_overrides:)
263
259
  param_type = param_overrides[param.name] || RbsTypeTranslator.translate(
264
260
  param.type,
@@ -269,7 +265,6 @@ module Rigor
269
265
  result = param_type.accepts(arg, mode: :gradual)
270
266
  result.yes? || result.maybe?
271
267
  end
272
- # rubocop:enable Metrics/ParameterLists
273
268
  end
274
269
  end
275
270
  end