rigortype 0.4.1 → 0.4.2

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 (182) 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 +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -243,7 +243,8 @@ module Rigor
243
243
  method_name: context.method_name,
244
244
  args: context.args,
245
245
  environment: environment,
246
- scope: context.scope
246
+ scope: context.scope,
247
+ call_node: context.call_node
247
248
  )
248
249
  end
249
250
 
@@ -277,6 +278,11 @@ module Rigor
277
278
 
278
279
  def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil, # rubocop:disable Metrics/ParameterLists
279
280
  public_only: false, scope: nil, call_node: nil)
281
+ # Issue #1794 — a sequence's inherited Enumerable surface reads its Rigor-private element argument, with
282
+ # `self` still the sequence.
283
+ element = ArithmeticSequenceElements.element_receiver(receiver, method_name, environment)
284
+ self_type_override ||= receiver if element
285
+ receiver = element || receiver
280
286
  descriptor = receiver_descriptor(receiver)
281
287
  return nil unless descriptor
282
288
 
@@ -1006,41 +1012,147 @@ module Rigor
1006
1012
  return override if override
1007
1013
 
1008
1014
  instance_type = Type::Combinator.nominal_of(class_name)
1009
- resolved_self_type =
1010
- case kind
1011
- when :singleton then Type::Combinator.singleton_of(class_name)
1012
- else instance_type
1013
- end
1014
1015
  # `self_type_override` lets the user-class fallback path preserve the ORIGINAL receiver as the
1015
1016
  # substitute for `Bases::Self` — so `Kernel#dup: () -> self` resolved through the Object
1016
1017
  # fallback returns the caller's type, not Object. `dispatch_one` also routes the receiver's
1017
1018
  # type-argument-bearing projection through it ({SelfSubstitute}, #1092).
1018
- self_type = self_type_override || resolved_self_type
1019
+ self_type = self_type_override ||
1020
+ (kind == :singleton ? Type::Combinator.singleton_of(class_name) : instance_type)
1021
+
1022
+ keywords_last = keyword_arguments_last?(call_node, args)
1023
+ call_site = [class_name, method_name, kind]
1024
+ if keywords_last && args.last.is_a?(Type::HashShape)
1025
+ distributed = distributed_keyword_return(method_definition, self_type, instance_type, type_vars, args,
1026
+ block_type, scope, call_node, call_site, environment)
1027
+ return distributed unless distributed.equal?(NOT_DISTRIBUTED)
1028
+ end
1019
1029
 
1020
1030
  candidates = OverloadSelector.select_candidates(
1021
1031
  method_definition,
1022
1032
  arg_types: args,
1033
+ keywords_last: keywords_last, splats: splat_indices(call_node, args),
1023
1034
  # A `Dynamic` self (#1092) is a return-side answer; overload selection and ReceiverAffinity
1024
1035
  # read the static facet, as they did before the substitute carried the wrapping.
1025
1036
  self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
1026
- instance_type: instance_type,
1027
- type_vars: type_vars,
1037
+ instance_type: instance_type, type_vars: type_vars,
1028
1038
  block_required: !block_type.nil?,
1029
1039
  environment: environment
1030
1040
  )
1031
1041
  return nil if candidates.empty?
1032
1042
 
1033
- call_site = [class_name, method_name, kind]
1034
1043
  record_dispatch_provenance(method_definition, candidates.first, scope, call_node, call_site)
1035
1044
  join_candidate_returns(
1036
1045
  candidates,
1037
1046
  method_definition: method_definition,
1038
1047
  self_type: self_type, instance_type: instance_type, type_vars: type_vars,
1039
1048
  args: args, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
1040
- alias_expander: environment.rbs_loader
1049
+ alias_expander: environment.rbs_loader, keywords_last: keywords_last
1041
1050
  )
1042
1051
  end
1043
1052
 
1053
+ # Issue #1746 — a keyword hash holding a union value (`headers: flag` with `flag : bool`) selects per member
1054
+ # at runtime, so the return reads one overload per {KeywordArguments.distributions} list and joins their
1055
+ # returns, as the block probe does for its parameters (#1737). Selecting once for the whole value took one
1056
+ # member's overload (`Symbol` for `(headers: true) -> Symbol | (?headers: false) -> Integer`). Each list
1057
+ # counts only a genuine match: a member no overload takes, or more lists than the limit, answers
1058
+ # `Dynamic[top]` rather than lend the first-overload fallback to the join. A precise union value joins the
1059
+ # returns as a union; a `Dynamic` value keeps the answer `Dynamic`, as the #521 join does, since the split
1060
+ # dropped the value's `nil` and the untyped input behind it. A keyword hash with no union value answers
1061
+ # {NOT_DISTRIBUTED}, and the call selects as before; so does one whose values do not split (#1779) and that no
1062
+ # overload genuinely takes.
1063
+ NOT_DISTRIBUTED = Object.new.freeze
1064
+ private_constant :NOT_DISTRIBUTED
1065
+
1066
+ # rubocop:disable Metrics/ParameterLists
1067
+ def distributed_keyword_return(method_definition, self_type, instance_type, type_vars, args, block_type,
1068
+ scope, call_node, call_site, environment)
1069
+ # rubocop:enable Metrics/ParameterLists
1070
+ # One overload is the only one any member can select, and a wide union value (`receiver_type: Type::t`)
1071
+ # spelled out member by member would only pass the distribution limit.
1072
+ method_types = method_definition.method_types
1073
+ return NOT_DISTRIBUTED if method_types.size < 2
1074
+ # A keyword hash no overload takes as keywords is a positional `Hash` throughout (#1737).
1075
+ return NOT_DISTRIBUTED unless KeywordArguments.any_declares?(method_types)
1076
+
1077
+ keywords = args.last
1078
+ return NOT_DISTRIBUTED if keywords.pairs.none? { |_name, value| KeywordArguments.union_members(value) }
1079
+
1080
+ selector_self = self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type
1081
+ per_list = distributed_keyword_matches(method_definition, args, selector_self, instance_type, type_vars,
1082
+ !block_type.nil?, environment, splat_indices(call_node, args))
1083
+ return NOT_DISTRIBUTED if per_list.equal?(NOT_DISTRIBUTED)
1084
+ return Type::Combinator.untyped if per_list.nil?
1085
+
1086
+ overloads = per_list.flat_map(&:last).uniq
1087
+ if overloads.size == 1
1088
+ record_dispatch_provenance(method_definition, overloads.first, scope, call_node, call_site)
1089
+ end
1090
+ returns = per_list.map do |arg_types, matches|
1091
+ join_candidate_returns(
1092
+ matches,
1093
+ method_definition: method_definition,
1094
+ self_type: self_type, instance_type: instance_type, type_vars: type_vars,
1095
+ args: arg_types, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
1096
+ alias_expander: environment.rbs_loader, keywords_last: true
1097
+ )
1098
+ end
1099
+ join_distributed_returns(returns, keywords)
1100
+ end
1101
+
1102
+ # Each distribution list with its genuine matches, or nil past the distribution limit or when a list has
1103
+ # none. When no value split (no union value's key discriminates between the overloads, #1779), the one list
1104
+ # is the call as given, and one with no genuine match answers {NOT_DISTRIBUTED}: the call then selects as it
1105
+ # did before #1746, first-overload fallback included (`1.step(10, by: (c ? 1 : 2.0))` is
1106
+ # `Enumerator::ArithmeticSequence` through the incomplete stdlib RBS).
1107
+ # rubocop:disable Metrics/ParameterLists
1108
+ def distributed_keyword_matches(method_definition, args, selector_self, instance_type, type_vars,
1109
+ block_required, environment, splats)
1110
+ # rubocop:enable Metrics/ParameterLists
1111
+ distributions = keyword_distributions(method_definition, args, selector_self, instance_type, type_vars,
1112
+ environment, splats)
1113
+ return nil if distributions.nil?
1114
+
1115
+ distributions.map do |arg_types|
1116
+ matches = keyword_matches(method_definition, arg_types, selector_self, instance_type, type_vars,
1117
+ block_required, environment, splats)
1118
+ return arg_types.equal?(args) ? NOT_DISTRIBUTED : nil if matches.empty?
1119
+
1120
+ [arg_types, matches]
1121
+ end
1122
+ end
1123
+
1124
+ def join_distributed_returns(returns, keywords)
1125
+ return nil if returns.any?(&:nil?)
1126
+
1127
+ joined = Type::Combinator.union(*returns.uniq)
1128
+ dynamic_value = keywords.pairs.any? { |_name, value| value.is_a?(Type::Dynamic) }
1129
+ dynamic_value && !joined.is_a?(Type::Dynamic) ? Type::Combinator.dynamic(joined) : joined
1130
+ end
1131
+
1132
+ # Issue #1727 — whether `args`' last entry is the call's keyword hash (`f(a, k: 1)`), which the overload
1133
+ # selector then matches against an overload's keywords. Only the AST tells a keyword hash from a
1134
+ # braced positional one (`f(a, { k: 1 })`), so a dispatch without the call node, or one whose
1135
+ # arguments are not the node's own (a forwarded `...`, a `send` re-dispatch), answers false and keeps
1136
+ # reading every argument positionally.
1137
+ def keyword_arguments_last?(call_node, args)
1138
+ return false if args.empty? || !call_node.respond_to?(:arguments)
1139
+
1140
+ arguments = call_node.arguments&.arguments
1141
+ !arguments.nil? && arguments.size == args.size && arguments.last.is_a?(Prism::KeywordHashNode)
1142
+ end
1143
+
1144
+ # Issue #1801 — the indices of `args` the call passes as splat arguments (`f(*xs, k: 1)`), whose positional
1145
+ # count is not known statically, or nil without one. As for {#keyword_arguments_last?}, only a call whose
1146
+ # node's arguments are exactly `args` answers.
1147
+ def splat_indices(call_node, args)
1148
+ return nil if args.empty? || !call_node.respond_to?(:arguments)
1149
+
1150
+ arguments = call_node.arguments&.arguments
1151
+ return nil if arguments.nil? || arguments.size != args.size || arguments.none?(Prism::SplatNode)
1152
+
1153
+ arguments.each_index.select { |index| arguments[index].is_a?(Prism::SplatNode) }
1154
+ end
1155
+
1044
1156
  # The two provenance side-tables the return-typing tier is the last place able to populate, recorded
1045
1157
  # together because they share one gate (`scope` && `call_node`) and one call site.
1046
1158
  #
@@ -1074,10 +1186,16 @@ module Rigor
1074
1186
  # A candidate whose return does not translate leaves the join incomplete — decline (fail-soft
1075
1187
  # to Dynamic downstream) rather than answer a join missing an arm the runtime can take.
1076
1188
  # rubocop:disable-next Metrics/ParameterLists
1189
+ # rubocop:disable Metrics/ParameterLists
1077
1190
  def join_candidate_returns(candidates, method_definition:, self_type:, instance_type:, type_vars:, args:,
1078
- block_type:, scope:, call_node:, call_site:, alias_expander: nil)
1191
+ block_type:, scope:, call_node:, call_site:, alias_expander: nil,
1192
+ keywords_last: false)
1193
+ # rubocop:enable Metrics/ParameterLists
1079
1194
  returns = candidates.map do |method_type|
1080
- full_type_vars = compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
1195
+ # An overload that takes the keyword hash as its keywords binds no positional parameter to it.
1196
+ positional = KeywordArguments.positional(method_type, args, keywords_last)
1197
+ full_type_vars = compose_type_vars(method_type, type_vars, args, positional, block_type, scope,
1198
+ call_node, call_site)
1081
1199
  returned = RbsTypeTranslator.translate(
1082
1200
  method_type.type.return_type,
1083
1201
  self_type: self_type,
@@ -1087,7 +1205,7 @@ module Rigor
1087
1205
  )
1088
1206
  next returned unless combining_overload?(method_definition, method_type, call_site)
1089
1207
 
1090
- class_level_sum(returned, method_type, args)
1208
+ class_level_sum(returned, method_type, positional)
1091
1209
  end
1092
1210
  return returns.first if returns.size == 1
1093
1211
  return nil if returns.any?(&:nil?)
@@ -1098,11 +1216,17 @@ module Rigor
1098
1216
  Type::Combinator.dynamic(Type::Combinator.union(*distinct))
1099
1217
  end
1100
1218
 
1101
- def compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
1219
+ # `positional` is `args` less a keyword hash the overload takes as its keywords (#1727). The block-return
1220
+ # rule reads every argument, the keyword hash included: a keyword parameter naming the block's variable
1221
+ # (`[T] (?default: T) { () -> T } -> T`) is an argument reaching it. The positional binding reads only the
1222
+ # arguments that land in positional parameters.
1223
+ # rubocop:disable Metrics/ParameterLists
1224
+ def compose_type_vars(method_type, type_vars, args, positional, block_type, scope, call_node, call_site)
1225
+ # rubocop:enable Metrics/ParameterLists
1102
1226
  vars = compose_block_type_vars(method_type, type_vars, block_type, args,
1103
1227
  scope: scope, call_node: call_node, call_site: call_site)
1104
- compose_arg_type_vars(method_type, vars, args, scope: scope, call_node: call_node,
1105
- call_site: call_site)
1228
+ compose_arg_type_vars(method_type, vars, positional, scope: scope, call_node: call_node,
1229
+ call_site: call_site)
1106
1230
  end
1107
1231
 
1108
1232
  # Whether the overload is one `Enumerable` declares for `sum`, whose declared return the tier reads
@@ -1268,7 +1392,8 @@ module Rigor
1268
1392
  # Whether an argument the call passes may land in a parameter whose type names `name`, anywhere
1269
1393
  # inside it (`hash[_Key, K2]` names `K2`). The argument count is not matched against the
1270
1394
  # parameter list: a `*splat` argument stands for any number of arguments, and keyword arguments
1271
- # arrive as one more entry in `args`, so any argument counts as reaching every parameter.
1395
+ # arrive as one more entry in `args` (untrimmed, #1727), so any argument counts as reaching every
1396
+ # parameter.
1272
1397
  def argument_reaches_variable?(method_type, name, args)
1273
1398
  return false if args.empty?
1274
1399
 
@@ -1602,11 +1727,15 @@ module Rigor
1602
1727
 
1603
1728
  # ----- block parameter probe (Phase C sub-phase 1) -----
1604
1729
 
1605
- def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil)
1730
+ def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil, call_node: nil)
1606
1731
  args ||= []
1732
+ keywords_last = keyword_arguments_last?(call_node, args)
1733
+ splat = splat_indices(call_node, args)
1607
1734
  case receiver
1608
- when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment, scope)
1609
- else probe_block_param_types_one(receiver, method_name, args, environment, scope)
1735
+ when Type::Union
1736
+ probe_block_param_types_union(receiver, method_name, args, environment, scope, keywords_last, splat)
1737
+ else
1738
+ probe_block_param_types_one(receiver, method_name, args, environment, scope, keywords_last, splat)
1610
1739
  end
1611
1740
  end
1612
1741
 
@@ -1614,9 +1743,9 @@ module Rigor
1614
1743
  # member resolves the same arity and types (otherwise the call sites would have to thread
1615
1744
  # per-member binders, which the slice does not support yet). Mismatches degrade to the empty
1616
1745
  # array so the binder defaults all params to Dynamic[Top].
1617
- def probe_block_param_types_union(receiver, method_name, args, environment, scope)
1746
+ def probe_block_param_types_union(receiver, method_name, args, environment, scope, keywords_last, splat)
1618
1747
  results = receiver.members.map do |member|
1619
- probe_block_param_types_one(member, method_name, args, environment, scope)
1748
+ probe_block_param_types_one(member, method_name, args, environment, scope, keywords_last, splat)
1620
1749
  end
1621
1750
  return [] if results.empty?
1622
1751
  return [] unless results.all? { |r| r == results.first }
@@ -1624,7 +1753,9 @@ module Rigor
1624
1753
  results.first
1625
1754
  end
1626
1755
 
1627
- def probe_block_param_types_one(receiver, method_name, args, environment, scope)
1756
+ def probe_block_param_types_one(receiver, method_name, args, environment, scope, keywords_last, splat)
1757
+ receiver = ArithmeticSequenceElements.element_receiver(receiver, method_name, environment, block: true) ||
1758
+ receiver
1628
1759
  descriptor = receiver_descriptor(receiver)
1629
1760
  return [] unless descriptor
1630
1761
 
@@ -1642,7 +1773,8 @@ module Rigor
1642
1773
  environment: environment,
1643
1774
  receiver: receiver,
1644
1775
  receiver_args: receiver_args,
1645
- method_name: method_name
1776
+ method_name: method_name,
1777
+ keywords_last: keywords_last, positional_splat: splat
1646
1778
  )
1647
1779
  rescue StandardError
1648
1780
  []
@@ -1651,7 +1783,7 @@ module Rigor
1651
1783
  # rubocop:disable Metrics/ParameterLists
1652
1784
  def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:,
1653
1785
  environment: nil, receiver: nil, receiver_args: [],
1654
- method_name: nil)
1786
+ method_name: nil, keywords_last: false, positional_splat: nil)
1655
1787
  # rubocop:enable Metrics/ParameterLists
1656
1788
  instance_type = Type::Combinator.nominal_of(class_name)
1657
1789
  self_type =
@@ -1672,12 +1804,25 @@ module Rigor
1672
1804
  substitute = SelfSubstitute.for(receiver, receiver_args, method_name, args, nil)
1673
1805
  self_type = Type::Combinator.nominal_of(class_name, type_args: receiver_args) if substitute
1674
1806
 
1807
+ selector_self = self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type
1808
+ if keywords_last
1809
+ return agreed_keyword_block_params(method_definition, args, self_type, selector_self, instance_type,
1810
+ type_vars, environment, positional_splat)
1811
+ end
1812
+
1813
+ # A splat (#1801) leaves the positional count open, so every overload some count reaches must agree.
1814
+ if positional_splat || FacetDistribution.faceted?(args)
1815
+ matches = facet_matches(method_definition, args, selector_self, instance_type, type_vars, true,
1816
+ environment, false, positional_splat)
1817
+ return agreed_block_params(matches, self_type, instance_type, type_vars, environment)
1818
+ end
1819
+
1675
1820
  method_type = OverloadSelector.select(
1676
1821
  method_definition,
1677
1822
  arg_types: args,
1678
1823
  # Overload selection reads a `Dynamic` self's static facet, mirroring the return path;
1679
1824
  # the substitution verdict here is built from the receiver's type arguments alone.
1680
- self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
1825
+ self_type: selector_self,
1681
1826
  instance_type: instance_type,
1682
1827
  type_vars: type_vars,
1683
1828
  block_required: true,
@@ -1685,8 +1830,20 @@ module Rigor
1685
1830
  )
1686
1831
  return [] unless method_type
1687
1832
 
1833
+ block_params_of(method_type, self_type, instance_type, type_vars, environment) || []
1834
+ end
1835
+
1836
+ # The block parameters every overload in `matches` declares alike, or no information (`[]`).
1837
+ def agreed_block_params(matches, self_type, instance_type, type_vars, environment)
1838
+ answers = matches.uniq.map do |method_type|
1839
+ block_params_of(method_type, self_type, instance_type, type_vars, environment)
1840
+ end
1841
+ answers.uniq.size == 1 && answers.first ? answers.first : []
1842
+ end
1843
+
1844
+ def block_params_of(method_type, self_type, instance_type, type_vars, environment)
1688
1845
  block = method_type.respond_to?(:block) ? method_type.block : nil
1689
- return [] unless block
1846
+ return nil unless block
1690
1847
 
1691
1848
  translate_block_positional_params(
1692
1849
  block,
@@ -1697,6 +1854,80 @@ module Rigor
1697
1854
  )
1698
1855
  end
1699
1856
 
1857
+ # {KeywordArguments.distributions} for the call's keyword hash. An overload that declares no keywords may read
1858
+ # the hash as a positional `Hash`, and then every union value splits: with
1859
+ # `(a: Integer) -> Integer | (Hash[Symbol, String]) -> String`, `a: Integer | String`'s `String` member takes
1860
+ # the second overload and its `Integer` member the first. A parameter that cannot take a `Hash` (`(String)`)
1861
+ # leaves the split to the keyword declarations.
1862
+ # `splats`: a splat argument hides how many positional arguments precede the hash.
1863
+ def keyword_distributions(method_definition, args, selector_self, instance_type, type_vars, environment,
1864
+ splats)
1865
+ hash = Type::Combinator.nominal_of("Hash")
1866
+ count = splats ? nil : args.size
1867
+ KeywordArguments.distributions(args, true, method_definition.method_types, count) do |param|
1868
+ param_type = RbsTypeTranslator.translate(
1869
+ param.type, self_type: selector_self, instance_type: instance_type, type_vars: type_vars,
1870
+ alias_expander: environment.rbs_loader
1871
+ )
1872
+ !param_type.accepts(hash, mode: :gradual).no?
1873
+ end
1874
+ end
1875
+
1876
+ # rubocop:disable Metrics/ParameterLists
1877
+ def agreed_keyword_block_params(method_definition, args, self_type, selector_self, instance_type, type_vars,
1878
+ environment, splats)
1879
+ # rubocop:enable Metrics/ParameterLists
1880
+ distributions = keyword_distributions(method_definition, args, selector_self, instance_type, type_vars,
1881
+ environment, splats)
1882
+ return [] if distributions.nil?
1883
+
1884
+ candidates = []
1885
+ distributions.each do |arg_types|
1886
+ # Genuine matches only (`member` true): a member no overload takes must not lend the first-overload
1887
+ # fallback to the agreement, which bound `headers: (true | nil)`'s block to the `true` arm.
1888
+ matches = keyword_matches(method_definition, arg_types, selector_self, instance_type, type_vars, true,
1889
+ environment, splats)
1890
+ return [] if matches.empty?
1891
+
1892
+ candidates.concat(matches)
1893
+ end
1894
+ agreed_block_params(candidates, self_type, instance_type, type_vars, environment)
1895
+ end
1896
+
1897
+ # One distribution's genuine matches. A faceted positional argument (a `Dynamic` with sealed facet members) is
1898
+ # read member-wise as `OverloadSelector.select_candidates` reads it, but every list answers only a genuine
1899
+ # match: the first-overload fallback must not count toward the block probe's agreement or the return path's
1900
+ # join.
1901
+ # rubocop:disable Metrics/ParameterLists
1902
+ def keyword_matches(method_definition, arg_types, selector_self, instance_type, type_vars, block_required,
1903
+ environment, splats)
1904
+ # rubocop:enable Metrics/ParameterLists
1905
+ unless FacetDistribution.faceted?(arg_types)
1906
+ return OverloadSelector.select_declared(method_definition, arg_types, selector_self, instance_type,
1907
+ type_vars, block_required, environment, true, true, splats)
1908
+ end
1909
+
1910
+ facet_matches(method_definition, arg_types, selector_self, instance_type, type_vars, block_required,
1911
+ environment, true, splats)
1912
+ end
1913
+
1914
+ # Issue #1750 — the overloads a faceted argument list may reach, read member-wise with genuine matches only.
1915
+ # Where `FacetDistribution.select` falls back to the arguments as given (overloads not provable, more lists
1916
+ # than its cap, or a member no overload takes), the bare `Dynamic` gradually matches every arm and the
1917
+ # strict pass's pick is the first by position, so the answer is every gradual match
1918
+ # (`OverloadSelector.select_declared`'s `:gradual`) and the caller binds only what they all agree on.
1919
+ # rubocop:disable Metrics/ParameterLists
1920
+ def facet_matches(method_definition, arg_types, selector_self, instance_type, type_vars, block_required,
1921
+ environment, keywords_last, splats = nil)
1922
+ # rubocop:enable Metrics/ParameterLists
1923
+ FacetDistribution.select(arg_types, method_definition, member_wise: true, environment:) do |list, member|
1924
+ # The fallback list (`member` false) answers every gradual match rather than the strict pass's pick.
1925
+ OverloadSelector.select_declared(method_definition, list, selector_self, instance_type, type_vars,
1926
+ block_required, environment, member || :gradual, keywords_last,
1927
+ splats)
1928
+ end
1929
+ end
1930
+
1700
1931
  # `RBS::Types::Block#type` is normally an `RBS::Types::Function` carrying the block's parameter
1701
1932
  # list; some signatures use `RBS::Types::UntypedFunction` (a `(?)` block) which exposes no
1702
1933
  # parameter types -- we treat it as "no information" and return an empty array so the binder
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ module MethodDispatcher
6
+ # How `OverloadSelector` reads a call whose positional arguments include a splat (`f(*xs, a: v)`), #1801.
7
+ #
8
+ # A splat stands for any number of arguments, so the call's positional count is not known statically. The
9
+ # selector sees each splat as one untyped entry of the argument list, and counted that way it ruled out every
10
+ # overload whose arity the splat may still meet: `ph(*xs, a: "s")` skipped `(Hash[Symbol, String]) -> String`,
11
+ # which takes `{ a: "s" }` when `xs` is empty (`def ph(h) = h; ph(*[], a: "s")` is `{ a: "s" }`). Instead, an
12
+ # overload matches when some count of the splats' elements fits its arity and every argument, the splats'
13
+ # untyped elements included, is accepted at the parameter that count binds it to.
14
+ module SplatArity
15
+ # The most argument lists {.expansions} spells out; past it, the selector only asks that every other argument
16
+ # fit some positional parameter of the overload.
17
+ LIMIT = 32
18
+ private_constant :LIMIT
19
+
20
+ module_function
21
+
22
+ # The argument lists `arg_types` stands for against `fun`, each splat entry (at `splats`, indices into
23
+ # `arg_types`) repeated as often as `fun`'s positional parameters can take, from none upward; nil past
24
+ # {LIMIT}. An untyped `(?)` parameter list takes any count, so the list as given stands for all of them.
25
+ def expansions(fun, arg_types, splats)
26
+ return [arg_types] unless fun.respond_to?(:required_positionals)
27
+
28
+ indices = splats.select { |index| index < arg_types.size }
29
+ return [arg_types] if indices.empty?
30
+
31
+ bound = element_bound(fun, arg_types.size - indices.size)
32
+ counts = counts(indices.size, bound)
33
+ return nil if counts.nil?
34
+
35
+ counts.map { |count| expand(arg_types, indices, count) }
36
+ end
37
+
38
+ # The most splat elements a call with `fixed` other positional arguments can pass to `fun`: up to its maximum
39
+ # arity, or, with a `*rest`, one past filling every other parameter, since further elements land in the rest
40
+ # parameter alike.
41
+ def element_bound(fun, fixed)
42
+ declared = fun.required_positionals.size + fun.optional_positionals.size + fun.trailing_positionals.size
43
+ return declared + 1 if fun.rest_positionals
44
+
45
+ [declared - fixed, 0].max
46
+ end
47
+
48
+ # Every way to give `splats` splats at most `bound` elements in all, or nil past {LIMIT}.
49
+ def counts(splats, bound)
50
+ combinations = [[]]
51
+ splats.times do
52
+ combinations = combinations.flat_map do |partial|
53
+ (0..(bound - partial.sum)).map { |count| partial + [count] }
54
+ end
55
+ return nil if combinations.size > LIMIT
56
+ end
57
+ combinations
58
+ end
59
+
60
+ def expand(arg_types, indices, counts)
61
+ expanded = []
62
+ arg_types.each_with_index do |arg, index|
63
+ position = indices.index(index)
64
+ position ? expanded.concat([arg] * counts[position]) : expanded << arg
65
+ end
66
+ expanded
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -18,6 +18,7 @@ require_relative "method_dispatcher/data_folding"
18
18
  require_relative "method_dispatcher/struct_folding"
19
19
  require_relative "method_dispatcher/rbs_dispatch"
20
20
  require_relative "method_dispatcher/iterator_dispatch"
21
+ require_relative "method_dispatcher/arithmetic_sequence_elements"
21
22
  require_relative "method_dispatcher/reduce_folding"
22
23
  require_relative "method_dispatcher/block_folding"
23
24
  require_relative "method_dispatcher/array_to_h_folding"
@@ -101,7 +102,7 @@ module Rigor
101
102
  # the tier commits the buffer only if it goes on to answer. See {#try_composite_receiver}.
102
103
  def resolve(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
103
104
  block_type: nil, environment: nil,
104
- call_node: nil, scope: nil, plugin_typed_sink: nil)
105
+ call_node: nil, scope: nil, plugin_typed_sink: nil, skip_project_patched: false)
105
106
  return nil if receiver_type.nil?
106
107
 
107
108
  # Build the call context once and thread it — unchanged — through every tier
@@ -205,7 +206,10 @@ module Rigor
205
206
  # the pre-pass populated `ProjectPatchedMethods` with the `(class, method, kind)` triple;
206
207
  # this tier surfaces it as `Dynamic[top]` so the patched call resolves cross-file without
207
208
  # `call.undefined-method`.
208
- patched_result = try_project_patched_method(receiver_type, method_name, environment)
209
+ patched_result =
210
+ unless skip_project_patched
211
+ try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
212
+ end
209
213
  if patched_result
210
214
  scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
211
215
  return patched_result
@@ -614,7 +618,7 @@ module Rigor
614
618
  # `def to_url; "hello"; end` patched onto `String` now resolves `s.to_url` to
615
619
  # `Dynamic[Nominal[String]]` instead of the pre-3a `Dynamic[Top]`. Falls back to
616
620
  # `Dynamic[Top]` when the heuristic declined (non-literal tail expression).
617
- def try_project_patched_method(receiver_type, method_name, environment)
621
+ def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
618
622
  registry = environment&.project_patched_methods
619
623
  return nil if registry.nil? || registry.empty?
620
624
 
@@ -624,9 +628,25 @@ module Rigor
624
628
  kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
625
629
  entry = registry.lookup(class_name: class_name, method_name: method_name, kind: kind)
626
630
  return nil if entry.nil?
627
- return Type::Combinator.untyped if entry.return_type.nil?
631
+ return Type::Combinator.dynamic(entry.return_type) if entry.return_type
632
+
633
+ aliased = project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
634
+ aliased ? Type::Combinator.dynamic(aliased) : Type::Combinator.untyped
635
+ end
628
636
 
629
- Type::Combinator.dynamic(entry.return_type)
637
+ # Issue #1702 — an alias in a `pre_eval:` patch whose old name no earlier patch binding defines
638
+ # (`alias old_plus +` on Integer, or `alias_method :orig_succ, :succ` ahead of the patch's own `def succ`)
639
+ # names the method the class had before the patch, so it answers what that method answers for the same
640
+ # arguments, behind the `Dynamic` every patched method carries. The re-entry skips this tier: a later patch
641
+ # `def` of the old name is not the body the alias copied, and the skip is also what ends an alias cycle. It
642
+ # carries no call node or scope, so it answers a type and records nothing at the call site.
643
+ def project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
644
+ return nil if entry.alias_of.nil?
645
+
646
+ resolve(
647
+ receiver_type: receiver_type, method_name: entry.alias_of, arg_types: arg_types,
648
+ block_type: block_type, environment: environment, skip_project_patched: true
649
+ )
630
650
  end
631
651
 
632
652
  # ADR-10 slice 2b-ii. Consults the per-run `Analysis::DependencySourceInference::Index`
@@ -847,7 +867,8 @@ module Rigor
847
867
  private_constant :STDLIB_SINGLETON_FOLDERS
848
868
 
849
869
  PRECISE_TIERS_TAIL = Ractor.make_shareable([
850
- MethodFolding, ReduceFolding, ArrayToHFolding, HashTransformKeysFolding, BlockFolding
870
+ MethodFolding, ReduceFolding, ArrayToHFolding, HashTransformKeysFolding, BlockFolding,
871
+ ArithmeticSequenceElements
851
872
  ].freeze)
852
873
  private_constant :PRECISE_TIERS_TAIL
853
874
 
@@ -1104,14 +1125,23 @@ module Rigor
1104
1125
  # `instance_of?(self)` later as the rule catalogue grows; for now only `class` is handled.
1105
1126
  def try_meta_introspection(receiver_type, method_name, arg_types = [], context = nil)
1106
1127
  case method_name
1107
- when :class then meta_class(receiver_type)
1128
+ when :class then meta_class(receiver_type, context&.environment)
1108
1129
  when :new then meta_new(receiver_type, arg_types, context)
1109
1130
  end
1110
1131
  end
1111
1132
 
1112
- def meta_class(receiver_type)
1133
+ def meta_class(receiver_type, environment = nil)
1113
1134
  case receiver_type
1114
- when Type::Nominal then Type::Combinator.singleton_of(receiver_type.class_name)
1135
+ when Type::Nominal
1136
+ # An instance of module `M` is an instance of some class that includes `M`, never of `M`
1137
+ # itself: `self.class` in `M`'s instance method is that includer, which no RBS type names.
1138
+ # `Dynamic[Class]` keeps the surface every class shares (`name`, `ancestors`) and answers
1139
+ # nothing for the includer's own singleton methods.
1140
+ if environment&.rbs_module?(receiver_type.class_name)
1141
+ return Type::Combinator.dynamic(Type::Combinator.nominal_of("Class"))
1142
+ end
1143
+
1144
+ Type::Combinator.singleton_of(receiver_type.class_name)
1115
1145
  when Type::Constant then constant_metaclass(receiver_type.value)
1116
1146
  end
1117
1147
  end
@@ -1555,13 +1585,16 @@ module Rigor
1555
1585
  # method definition, or selected overload does not provide statically declared block
1556
1586
  # parameter types. Callers MUST treat the empty array as "no information"; the binder
1557
1587
  # falls back to `Dynamic[Top]` for every parameter slot in that case.
1588
+ #
1589
+ # `call_node` lets the overload selector read the call's keyword arguments as keywords (#1727, #1737), as
1590
+ # the return path does; without it they are read positionally.
1558
1591
  def expected_block_param_types(receiver_type:, method_name:, arg_types:, environment: nil,
1559
- scope: nil)
1592
+ scope: nil, call_node: nil)
1560
1593
  return [] if receiver_type.nil?
1561
1594
 
1562
1595
  context = CallContext.build(
1563
1596
  receiver: receiver_type, method_name: method_name,
1564
- args: arg_types, environment: environment, scope: scope
1597
+ args: arg_types, environment: environment, scope: scope, call_node: call_node
1565
1598
  )
1566
1599
  iterator_result = IteratorDispatch.block_param_types(context)
1567
1600
  return iterator_result if iterator_result
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
+ require_relative "../source/literals"
4
5
  require_relative "../source/node_children"
5
6
 
6
7
  module Rigor
@@ -204,7 +205,7 @@ module Rigor
204
205
 
205
206
  # The Symbol a `:name` or `"name"` literal argument names, or nil.
206
207
  def symbol_argument_name(arg)
207
- arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
208
+ Source::Literals.symbol_or_string(arg)
208
209
  end
209
210
 
210
211
  private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name