rigortype 0.3.6 → 0.3.7

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 (213) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/date.yml +6 -3
  4. data/data/builtins/ruby_core/enumerable.yml +1 -0
  5. data/data/builtins/ruby_core/exception.yml +2 -1
  6. data/data/builtins/ruby_core/file.yml +28 -14
  7. data/data/builtins/ruby_core/hash.yml +5 -2
  8. data/data/builtins/ruby_core/io.yml +33 -16
  9. data/data/builtins/ruby_core/random.yml +2 -1
  10. data/data/builtins/ruby_core/re.yml +6 -3
  11. data/data/builtins/ruby_core/struct.yml +2 -0
  12. data/data/builtins/ruby_core/time.yml +2 -1
  13. data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
  14. data/docs/manual/02-cli-reference.md +16 -7
  15. data/docs/manual/05-inspecting-types.md +20 -8
  16. data/docs/manual/07-plugins.md +4 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  18. data/docs/manual/plugins/rigor-activerecord.md +74 -1
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  20. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  21. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  22. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  24. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  25. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  26. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  28. data/lib/rigor/analysis/check_rules.rb +383 -48
  29. data/lib/rigor/analysis/crash_signature.rb +116 -0
  30. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  31. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  32. data/lib/rigor/analysis/incremental.rb +28 -0
  33. data/lib/rigor/analysis/incremental_session.rb +44 -9
  34. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  35. data/lib/rigor/analysis/reachability/graph.rb +14 -5
  36. data/lib/rigor/analysis/reachability/scan.rb +7 -3
  37. data/lib/rigor/analysis/result.rb +32 -0
  38. data/lib/rigor/analysis/run_cache_key.rb +41 -1
  39. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
  40. data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
  41. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  42. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
  43. data/lib/rigor/analysis/runner.rb +147 -10
  44. data/lib/rigor/analysis/worker_session.rb +10 -1
  45. data/lib/rigor/bleeding_edge.rb +7 -6
  46. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  47. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  48. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  49. data/lib/rigor/cache/descriptor.rb +42 -2
  50. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  51. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
  52. data/lib/rigor/cache/store.rb +10 -1
  53. data/lib/rigor/cli/coverage_command.rb +23 -18
  54. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  55. data/lib/rigor/cli/coverage_scan.rb +47 -6
  56. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  57. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  58. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  59. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  60. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  61. data/lib/rigor/cli/sig_gen_command.rb +18 -0
  62. data/lib/rigor/cli/type_of_command.rb +188 -41
  63. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  64. data/lib/rigor/cli/type_scan_command.rb +10 -1
  65. data/lib/rigor/cli.rb +1 -1
  66. data/lib/rigor/configuration.rb +7 -4
  67. data/lib/rigor/effects/plugin_facts.rb +1 -1
  68. data/lib/rigor/effects/scanner.rb +2 -3
  69. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  70. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  71. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  72. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  73. data/lib/rigor/environment/rbs_loader.rb +525 -46
  74. data/lib/rigor/environment.rb +38 -15
  75. data/lib/rigor/inference/acceptance.rb +172 -20
  76. data/lib/rigor/inference/captured_locals.rb +62 -0
  77. data/lib/rigor/inference/content_join.rb +347 -0
  78. data/lib/rigor/inference/def_handle.rb +15 -3
  79. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  80. data/lib/rigor/inference/expression_typer.rb +1020 -141
  81. data/lib/rigor/inference/fork_map.rb +6 -1
  82. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  83. data/lib/rigor/inference/hkt_registry.rb +46 -0
  84. data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
  85. data/lib/rigor/inference/index_write_widening.rb +48 -0
  86. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  88. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  89. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  90. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  91. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  93. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  94. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  95. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  96. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  97. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  98. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  99. data/lib/rigor/inference/mutation_widening.rb +244 -174
  100. data/lib/rigor/inference/narrowing.rb +331 -27
  101. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  102. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  103. data/lib/rigor/inference/precision_scanner.rb +5 -2
  104. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  105. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  106. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  107. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  108. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  109. data/lib/rigor/inference/version_guard.rb +229 -0
  110. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  111. data/lib/rigor/plugin/base.rb +3 -2
  112. data/lib/rigor/plugin/inflector.rb +14 -5
  113. data/lib/rigor/plugin/io_boundary.rb +107 -4
  114. data/lib/rigor/plugin/loader.rb +14 -0
  115. data/lib/rigor/protection/analysis_guard.rb +60 -0
  116. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  117. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  118. data/lib/rigor/protection/discovery_seed.rb +1 -1
  119. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  120. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  121. data/lib/rigor/protection/mutator.rb +121 -18
  122. data/lib/rigor/reflection.rb +182 -30
  123. data/lib/rigor/scope/discovery_index.rb +39 -1
  124. data/lib/rigor/scope.rb +385 -4
  125. data/lib/rigor/sig_gen/classification.rb +12 -1
  126. data/lib/rigor/sig_gen/generator.rb +254 -5
  127. data/lib/rigor/sig_gen/renderer.rb +1 -6
  128. data/lib/rigor/sig_gen/writer.rb +3 -0
  129. data/lib/rigor/source/constant_path.rb +79 -2
  130. data/lib/rigor/triage/catalogue.rb +1 -1
  131. data/lib/rigor/type/combinator.rb +10 -0
  132. data/lib/rigor/type/maybe.rb +47 -0
  133. data/lib/rigor/type/refined.rb +1 -2
  134. data/lib/rigor/type/result.rb +53 -0
  135. data/lib/rigor/type.rb +2 -0
  136. data/lib/rigor/version.rb +1 -1
  137. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  138. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  139. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  140. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  141. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  142. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  143. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  144. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  145. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  146. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  147. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  148. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  149. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  150. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  151. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  152. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  153. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  154. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  156. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  157. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  158. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  159. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  160. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  163. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  164. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  165. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  166. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  167. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  168. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  169. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  170. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  171. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  172. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  173. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  174. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  175. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  176. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  177. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  178. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  179. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  180. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  181. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  182. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  183. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  184. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  185. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  186. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  187. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  188. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  189. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  190. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  191. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  192. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  193. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  194. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  195. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  196. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  197. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  198. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  199. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  200. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  201. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  202. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  203. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  204. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  205. data/sig/rigor/analysis/baseline.rbs +1 -1
  206. data/sig/rigor/environment.rbs +3 -0
  207. data/sig/rigor/inference.rbs +4 -1
  208. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  209. data/sig/rigor/reflection.rbs +3 -1
  210. data/sig/rigor/scope.rbs +25 -1
  211. data/sig/rigor/type.rbs +95 -1
  212. data/sig/rigor.rbs +16 -0
  213. metadata +69 -3
@@ -42,11 +42,16 @@ module Rigor
42
42
  #
43
43
  # A child ends at {.run_worker}'s `exit!`, which skips `at_exit` AND object finalizers, so a `Tempfile`
44
44
  # a child creates is never reclaimed by the mechanism that reclaims the parent's (issue #330 —
45
- # `Protection::ClosureKillOracle`'s per-process mutant file leaked one file per worker per
45
+ # `Protection::ClosureKillOracle`'s mutant file leaked one file per worker per
46
46
  # `rigor coverage --protection --mutation` run). Answering with the directory the parent already removes
47
47
  # once every child has been collected makes the child's scratch state the parent's problem, which is the
48
48
  # only place it can be solved: the names are random, so nothing outside the child could find them.
49
49
  #
50
+ # Still load-bearing after issue #572 gave that oracle a block-scoped temp file. The block unwinds on
51
+ # both paths a child can take on its own — a normal return and the `StandardError` {.run_worker}
52
+ # rescues — but a child killed by a signal unwinds nothing at all, and this directory is what reclaims
53
+ # what it left behind.
54
+ #
50
55
  # The directory check is the belt to {.run_worker}'s braces: the offer is only worth taking while the
51
56
  # parent still has the directory open, and answering `nil` once it is gone degrades to `Dir.tmpdir`
52
57
  # instead of raising `Errno::ENOENT` somewhere far from here.
@@ -85,7 +85,7 @@ module Rigor
85
85
  Type::Combinator.union(*reduced)
86
86
  when HktBody::NominalApp
87
87
  reduced_args = node.args.map { |arg| walk(arg, bindings: bindings, state: state) }
88
- Type::Combinator.nominal_of(node.class_name, type_args: reduced_args)
88
+ reduce_nominal_app(node, reduced_args)
89
89
  when HktBody::AppRef
90
90
  reduced_args = node.args.map { |arg| walk(arg, bindings: bindings, state: state) }
91
91
  reduce_app_ref(node.uri, reduced_args, state: state)
@@ -96,6 +96,17 @@ module Rigor
96
96
  end
97
97
  end
98
98
 
99
+ def reduce_nominal_app(node, reduced_args)
100
+ normalized_name = node.class_name.sub(/\A::/, "")
101
+ if %w[Result Dry::Monads::Result].include?(normalized_name) && reduced_args.size == 2
102
+ Type::Combinator.result_of(reduced_args[0], reduced_args[1])
103
+ elsif %w[Maybe Dry::Monads::Maybe].include?(normalized_name) && reduced_args.size == 1
104
+ Type::Combinator.maybe_of(reduced_args[0])
105
+ else
106
+ Type::Combinator.nominal_of(node.class_name, type_args: reduced_args)
107
+ end
108
+ end
109
+
99
110
  # ADR-20 § D3 conditional reduction. Resolves the test against the current bindings and
100
111
  # picks a branch:
101
112
  #
@@ -160,6 +160,8 @@ module Rigor
160
160
  # typically `untyped` or stdlib classes.
161
161
  # @param reporter [#record, nil] same fail-soft reporter contract the other RBS-extended
162
162
  # parsers use.
163
+
164
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
163
165
  def self.scan_rbs_loader(rbs_loader, base: EMPTY, name_scope: nil, reporter: nil)
164
166
  return base if rbs_loader.nil?
165
167
 
@@ -182,6 +184,50 @@ module Rigor
182
184
  definitions << defn if defn
183
185
  end
184
186
 
187
+ require_relative "hkt_sugar_translator"
188
+ if rbs_loader.respond_to?(:each_type_alias_decl)
189
+ rbs_loader.each_type_alias_decl do |type_name, decl_entry|
190
+ decl = decl_entry.decl
191
+ next if decl.type_params.empty?
192
+
193
+ uri_str = type_name.to_s.sub(/\A::/, "")
194
+ next unless uri_str.include?("::")
195
+
196
+ uri = uri_str.to_sym
197
+
198
+ params = decl.type_params.map(&:name)
199
+ params_set = params.to_set
200
+
201
+ translator = HktSugarTranslator.new(uri: uri, params_set: params_set, name_scope: name_scope)
202
+ body_tree = translator.translate(decl.type)
203
+
204
+ next unless translator.recursive
205
+
206
+ variance = decl.type_params.map do |p|
207
+ case p.variance
208
+ when :covariant then :out
209
+ when :contravariant then :in
210
+ else :inv
211
+ end
212
+ end
213
+
214
+ registrations << Registration.new(
215
+ uri: uri,
216
+ arity: params.size,
217
+ variance: variance,
218
+ bound: Type::Combinator.untyped
219
+ )
220
+ definitions << Definition.new(
221
+ uri: uri,
222
+ params: params,
223
+ body: "",
224
+ body_tree: body_tree,
225
+ source_path: decl.location&.buffer&.name,
226
+ source_line: decl.location&.start_line
227
+ )
228
+ end
229
+ end
230
+
185
231
  return base if registrations.empty? && definitions.empty?
186
232
 
187
233
  overlay = new(registrations: registrations, definitions: definitions)
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+ require_relative "hkt_body"
5
+
6
+ module Rigor
7
+ module Inference
8
+ class HktSugarTranslator
9
+ class << self
10
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
11
+ def translate(rbs_type, uri:, params_set:, name_scope:)
12
+ new(uri: uri, params_set: params_set, name_scope: name_scope).translate(rbs_type)
13
+ end
14
+ end
15
+
16
+ attr_reader :recursive
17
+
18
+ def initialize(uri:, params_set:, name_scope:)
19
+ @uri = uri
20
+ @params_set = params_set
21
+ @name_scope = name_scope
22
+ @recursive = false
23
+ end
24
+
25
+ def translate(type)
26
+ case type
27
+ when RBS::Types::Alias
28
+ if type.name.to_s.sub(/\A::/, "") == @uri.to_s
29
+ @recursive = true
30
+ args = type.args.map { |a| translate(a) }
31
+ HktBody::AppRef.new(uri: @uri, args: args)
32
+ else
33
+ # Re-wrap other aliases as NominalApp or TypeLeaf?
34
+ # Actually, we can just translate them to TypeLeaf if they have no args,
35
+ # but Rigor handles aliases via RbsLoader.
36
+ # If we don't expand them here, they will just be RbsTypeTranslator.translate'd later?
37
+ # Wait, HktBody doesn't have an AliasRef node. HktBody uses NominalApp for things with args,
38
+ # or TypeLeaf for concrete Rigor::Type.
39
+
40
+ # Since Rigor doesn't support generic aliases outside of HKT AppRef,
41
+ # we can just translate to TypeLeaf.
42
+ fallback_to_type_leaf(type)
43
+ end
44
+ when RBS::Types::ClassInstance
45
+ class_name = type.name.to_s.sub(/\A::/, "")
46
+ if type.args.empty?
47
+ HktBody::TypeLeaf.new(type: Rigor::Type::Nominal.new(class_name))
48
+ else
49
+ args = type.args.map { |a| translate(a) }
50
+ HktBody::NominalApp.new(class_name: class_name, args: args)
51
+ end
52
+ when RBS::Types::Variable
53
+ if @params_set.include?(type.name)
54
+ HktBody::Param.new(name: type.name)
55
+ else
56
+ fallback_to_type_leaf(type)
57
+ end
58
+ when RBS::Types::Union
59
+ HktBody::Union.new(arms: type.types.map { |t| translate(t) })
60
+ when RBS::Types::Optional
61
+ HktBody::Union.new(arms: [
62
+ translate(type.type),
63
+ HktBody::TypeLeaf.new(type: Rigor::Type::Constant.new(nil))
64
+ ])
65
+ when RBS::Types::Bases::Nil
66
+ HktBody::TypeLeaf.new(type: Rigor::Type::Constant.new(nil))
67
+ when RBS::Types::Bases::Bool
68
+ HktBody::TypeLeaf.new(type: Rigor::Type::Combinator.union(Rigor::Type::Constant.new(true), Rigor::Type::Constant.new(false)))
69
+ when RBS::Types::Bases::Any
70
+ HktBody::TypeLeaf.new(type: Rigor::Type::Combinator.untyped)
71
+ else
72
+ fallback_to_type_leaf(type)
73
+ end
74
+ end
75
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
76
+
77
+ private
78
+
79
+ def fallback_to_type_leaf(type)
80
+ require_relative "rbs_type_translator" unless defined?(RbsTypeTranslator)
81
+ rigor_type = RbsTypeTranslator.translate(
82
+ type,
83
+ alias_expander: nil,
84
+ type_vars: {},
85
+ name_scope: @name_scope,
86
+ self_type: Rigor::Type::Combinator.untyped,
87
+ instance_type: Rigor::Type::Combinator.untyped
88
+ )
89
+ HktBody::TypeLeaf.new(type: rigor_type)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "mutation_widening"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # `h[k] ||= v`, `h[k] &&= v` and `h[k] += v` store through `[]=`, but Prism gives each its own node class rather
10
+ # than a `[]=` `CallNode`. None of them therefore reached {MutationWidening.widen_after_call}, and a literal-shape
11
+ # binding outlived its justification: an `@h = {}` written only through `@h[k] ||= {}` kept its empty `HashShape`,
12
+ # so `@h.empty?` folded to `Constant[true]` and `return nil if x.nil? || @h.empty?` drew a false
13
+ # `flow.always-truthy-condition` on a hash the class fills.
14
+ #
15
+ # Two consumers need the same recognition and neither had it: {StatementEvaluator} for the straight-line write, and
16
+ # {ScopeIndexer}'s class-ivar pre-pass for the cross-method one (an ivar written in `add` and read in `probe`).
17
+ # `||=` / `&&=` are conditional at runtime, but a widening may only LOSE precision, so answering on the branch that
18
+ # does not store is safe.
19
+ #
20
+ # `Prism::IndexTargetNode` is deliberately absent: it is a multi-assign TARGET, and the `MultiWriteNode` that owns
21
+ # it is where that write is observed.
22
+ module IndexWriteWidening
23
+ NODE_CLASSES = [Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode].freeze
24
+
25
+ # The method these forms store through — the name the mutator tables are keyed on.
26
+ MUTATOR = :[]=
27
+
28
+ module_function
29
+
30
+ # @param node [Prism::Node]
31
+ def index_write?(node)
32
+ NODE_CLASSES.any? { |klass| node.is_a?(klass) }
33
+ end
34
+
35
+ # `arg_types` is the `[key, stored_value]` pair the caller typed, shaped like a `[]=` call's argument list so
36
+ # the widening joins the stored value into the carrier's content evidence exactly as a real `[]=` does
37
+ # (issue #560). Empty means "no evidence" and widens without joining.
38
+ #
39
+ # @param node [Prism::Node] one of {NODE_CLASSES}
40
+ # @param current_scope [Rigor::Scope]
41
+ # @param arg_types [Array<Rigor::Type::Base>]
42
+ # @return [Rigor::Scope]
43
+ def widen(node:, current_scope:, arg_types: MutationWidening::NO_ARG_TYPES)
44
+ MutationWidening.widen_receiver_aliases(node.receiver, MUTATOR, current_scope, arg_types: arg_types)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -83,6 +83,20 @@ module Rigor
83
83
  [receiver.first, receiver.last, key]
84
84
  end
85
85
 
86
+ # Issue #544 — whether the RECEIVER's type is precise enough for a recorded slot value to be a
87
+ # fact. A `Dynamic` / `Top` constituent means the collection can hold a caller-supplied value at
88
+ # the slot that `||=` keeps, so recording the default alone would invent a fact (mail's
89
+ # `options[:count] ||= :all` folded a reachable `count: 1` path away). Unions walk their members;
90
+ # everything else — the tracked `HashShape` / `Tuple` carriers this feature was built for, and
91
+ # plain nominals whose RBS read already answers honestly — passes.
92
+ def fully_tracked_receiver_type?(type)
93
+ case type
94
+ when Type::Dynamic, Type::Top then false
95
+ when Type::Union then type.members.all? { |m| fully_tracked_receiver_type?(m) }
96
+ else true
97
+ end
98
+ end
99
+
86
100
  # Looks up a recorded narrowing for `receiver[key]` against `scope`, returning the narrowed
87
101
  # type or nil when no entry applies. Used by ExpressionTyper's `[]` dispatch to refine the
88
102
  # result of a stable indexed read.
@@ -136,7 +136,7 @@ module Rigor
136
136
  end
137
137
 
138
138
  # @return [:always_true, :always_false, :bool, nil]
139
- # rubocop:disable Metrics/CyclomaticComplexity
139
+ # rubocop:disable-next Metrics/CyclomaticComplexity
140
140
  def predicate_decision(method_name, truthiness, emptiness)
141
141
  case method_name
142
142
  when :all?
@@ -159,7 +159,6 @@ module Rigor
159
159
  :bool
160
160
  end
161
161
  end
162
- # rubocop:enable Metrics/CyclomaticComplexity
163
162
 
164
163
  def bool_union
165
164
  Type::Combinator.union(
@@ -185,10 +184,13 @@ module Rigor
185
184
  end
186
185
 
187
186
  def constant_emptiness(value)
188
- # Only `Range` constants reach these folds: `Type::Constant` rejects Array / Hash literals (they
189
- # become `Tuple` / `HashShape` carriers), and the remaining scalar constants (Integer / Float /
190
- # Symbol / String / ) are not Enumerable receivers for the filter or predicate methods folded here.
187
+ # `Range` constants and folded `Set` values (SetFolding wraps a real frozen ::Set) are the
188
+ # Enumerable receivers that reach these folds: `Type::Constant` rejects Array / Hash literals
189
+ # (they become `Tuple` / `HashShape` carriers), and the remaining scalar constants (Integer /
190
+ # Float / Symbol / String / …) are not Enumerable receivers for the filter or predicate methods
191
+ # folded here.
191
192
  return range_emptiness(value) if value.is_a?(Range)
193
+ return (value.empty? ? :empty : :non_empty) if value.is_a?(::Set)
192
194
 
193
195
  :unknown
194
196
  end
@@ -220,6 +220,16 @@ module Rigor
220
220
  method_name = context.method_name
221
221
  args = context.args
222
222
 
223
+ # Issue #539 — every fold in this tier executes the BLOCKLESS form of the method against the
224
+ # constant value. A call site that passes a block (literal or `&arg`) has different semantics —
225
+ # `SET.any? { |n| lookup(n) }` folded to the blockless `Set#any?`'s `true` on any non-empty set,
226
+ # a wrong type the flow rules then amplified (`flow.always-truthy-condition` on our own lib).
227
+ # The catalog's `block_dependent` classification covers the C body's own iterator, not the call
228
+ # site's block, so the guard belongs here. Internal per-element dispatches hand a non-CallNode
229
+ # (e.g. the `&:sym` BlockArgumentNode) as `call_node` — those carry no call-site block.
230
+ call_node = context.call_node
231
+ return nil if call_node.is_a?(Prism::CallNode) && call_node.block
232
+
223
233
  if REFLECTIVE_SEND_METHODS.include?(method_name)
224
234
  first_arg = args.first
225
235
  return nil unless first_arg.is_a?(Type::Constant) && first_arg.value.is_a?(Symbol)
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+ require_relative "singleton_folding"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module MethodDispatcher
9
+ # Folds `JSON.generate` / `JSON.pretty_generate` to their precise return type.
10
+ #
11
+ # Both methods always return a `String` or raise (`JSON::GeneratorError`, `JSON::NestingError`, …) —
12
+ # never `nil`, never a non-`String` value. Upstream RBS (`stdlib/json/0/json.rbs`) already declares
13
+ # `generate` as `-> String`, but `pretty_generate` is declared `-> untyped`, so every
14
+ # `JSON.pretty_generate` call widens to `Dynamic[top]` and every downstream method call on the result
15
+ # loses precision with it. This tier closes that gap by construction rather than waiting on an
16
+ # upstream RBS fix, and folds `generate` alongside it for symmetry — a `Nominal[String]` answer here
17
+ # is monotone over both today's `untyped` (`pretty_generate`) and today's already-precise `String`
18
+ # (`generate`).
19
+ #
20
+ # Unlike `CGIFolding` / `ShellwordsFolding`, this tier does NOT evaluate the argument. `JSON.generate`
21
+ # is not a pure function whose *result* can be computed at analysis time in general — the object graph
22
+ # can be arbitrarily large, cyclic, or rely on a user-defined `#to_json` — so only the *return type* is
23
+ # folded here, never the *return value*. A future literal-argument constant fold (mirroring
24
+ # `ConstantFolding`'s treatment of other pure stdlib calls) can layer on top of this tier without
25
+ # changing it.
26
+ #
27
+ # === Supported methods
28
+ #
29
+ # * `generate(obj)` / `generate(obj, opts)` — returns `Nominal[String]`.
30
+ # * `pretty_generate(obj)` / `pretty_generate(obj, opts)` — returns `Nominal[String]`.
31
+ #
32
+ # === Unsupported cases
33
+ #
34
+ # Returns `nil` (deferring to the next dispatcher tier) when:
35
+ # - the receiver is not `Singleton[JSON]`,
36
+ # - the method is not `generate` / `pretty_generate` (e.g. `JSON.parse`, `JSON.dump`),
37
+ # - the call has the wrong arity (not 1 or 2 positional arguments).
38
+ module JSONFolding
39
+ JSON_GENERATE_METHODS = Set[:generate, :pretty_generate].freeze
40
+ private_constant :JSON_GENERATE_METHODS
41
+
42
+ module_function
43
+
44
+ # @return [Rigor::Type, nil] folded result, or nil to defer.
45
+ def try_dispatch(context)
46
+ receiver = context.receiver
47
+ method_name = context.method_name
48
+ args = context.args
49
+ return nil unless SingletonFolding.receiver?(receiver, "JSON")
50
+ return nil unless JSON_GENERATE_METHODS.include?(method_name)
51
+ return nil unless args.size.between?(1, 2)
52
+
53
+ Type::Combinator.nominal_of("String")
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -80,10 +80,22 @@ module Rigor
80
80
  # declaration.
81
81
  # @return [RBS::MethodType, nil] the chosen overload, or nil when the definition has no method
82
82
  # types at all.
83
- def select(method_definition, arg_types:, self_type:, instance_type:, type_vars: {}, block_required: false,
84
- environment: nil)
83
+ def select(method_definition, **) = select_candidates(method_definition, **).first
84
+
85
+ # Issue #521 — like {.select}, but when a `Dynamic[Top]` argument reaches the gradual pass it
86
+ # returns EVERY gradually-matching overload instead of pinning the first. An untyped argument
87
+ # accepts every param indiscriminately, so "first gradual match" is decided by overload-list
88
+ # position, not by types — `[true] * n` with an untyped `n` pinned `Array#*(string) -> String`
89
+ # and answered a wrong precise type the runtime can contradict. The caller unions the candidates'
90
+ # returns, which contains the truth whichever overload the runtime takes. Every other path (strict,
91
+ # alias-resolved, typed-gradual, arity fallback) still yields exactly one candidate, so `select`
92
+ # keeps its historical single answer.
93
+ #
94
+ # @return [Array<RBS::MethodType>] matching overloads; empty when the definition declares none.
95
+ def select_candidates(method_definition, arg_types:, self_type:, instance_type:, type_vars: {},
96
+ block_required: false, environment: nil)
85
97
  overloads = method_definition.method_types
86
- return nil if overloads.empty?
98
+ return [] if overloads.empty?
87
99
 
88
100
  # `rigor:v1:param: <name> <refinement>` annotations on this method override the RBS-declared
89
101
  # parameter type at the matching name. The map is consumed inside `accepts_param?` so overload
@@ -96,15 +108,18 @@ module Rigor
96
108
  # an arbitrary sibling-class arm that only wins by overload-list position.
97
109
  overloads = ReceiverAffinity.reorder(overloads, self_type: self_type, environment: environment)
98
110
 
111
+ alias_expander = environment&.rbs_loader
99
112
  passes = lambda do |require_block|
100
113
  run_selection_passes(
101
- overloads, arg_types: arg_types, self_type: self_type, instance_type: instance_type,
102
- type_vars: type_vars, block_required: require_block, param_overrides: param_overrides
114
+ overloads,
115
+ { arg_types: arg_types, self_type: self_type, instance_type: instance_type,
116
+ type_vars: type_vars, block_required: require_block, param_overrides: param_overrides,
117
+ alias_expander: alias_expander }
103
118
  )
104
119
  end
105
120
 
106
- match = passes.call(block_required)
107
- return match if match
121
+ matches = passes.call(block_required)
122
+ return matches unless matches.empty?
108
123
 
109
124
  # A block at the call site that no block-declaring overload matched: Ruby ignores a block handed
110
125
  # to a method that never yields it, so retry treating the block as ignorable rather than failing
@@ -112,8 +127,8 @@ module Rigor
112
127
  # `define_command(:x) do … end` against `def define_command: (Symbol) -> Symbol`) degraded to
113
128
  # `Dynamic[Top]` — and on a self-send suppressed the whole method's return type.
114
129
  if block_required
115
- match = passes.call(false)
116
- return match if match
130
+ matches = passes.call(false)
131
+ return matches unless matches.empty?
117
132
  end
118
133
 
119
134
  # No (usable) block at the call site: prefer an overload that does not REQUIRE a block over
@@ -121,7 +136,7 @@ module Rigor
121
136
  # overload first (`() { ... } -> Array[Elem]`) and the bare-call overload second
122
137
  # (`() -> Enumerator[...]`). Without this, a no-block `[1, 2].filter` would adopt the block
123
138
  # overload's `Array[Elem]` return when the call actually yields an `Enumerator`.
124
- overloads.find { |mt| !overload_requires_block?(mt) } || overloads.first
139
+ [overloads.find { |mt| !overload_requires_block?(mt) } || overloads.first]
125
140
  end
126
141
 
127
142
  def overload_has_block?(method_type)
@@ -147,38 +162,58 @@ module Rigor
147
162
  # `Array#*(int)` wins over the `Array#*(string) -> String` overload for Integer args.
148
163
  # - Pass 2 (gradual): the original gradual matcher so overloads that legitimately rely on
149
164
  # duck-typed params still resolve when nothing stricter applies.
150
- def run_selection_passes(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
151
- param_overrides:)
152
- shared = {
153
- arg_types: arg_types, self_type: self_type, instance_type: instance_type,
154
- type_vars: type_vars, block_required: block_required, param_overrides: param_overrides
155
- }
156
- find_matching_overload(overloads, **shared, strict: true) ||
157
- find_matching_overload_via_aliases(overloads, arg_types: arg_types, block_required: block_required) ||
158
- find_matching_overload(overloads, **shared, strict: false)
165
+ # `shared` is the caller-assembled keyword bundle for `find_matching_overload` — hash-shaped
166
+ # because the pass pipeline forwards it twice and RuboCop's parameter-list budget is real.
167
+ def run_selection_passes(overloads, shared)
168
+ strict = find_matching_overload(overloads, **shared, strict: true)
169
+ return strict unless strict.empty?
170
+
171
+ alias_hit = find_matching_overload_via_aliases(
172
+ overloads, arg_types: shared[:arg_types], block_required: shared[:block_required]
173
+ )
174
+ return [alias_hit] if alias_hit
175
+
176
+ # Pass 2, array-valued. With every argument carrying real type information the first gradual
177
+ # match keeps its historical single-winner contract. With a `Dynamic[Top]` argument in play the
178
+ # matches are indistinguishable by types — position alone would pick — so ALL of them come back
179
+ # and the dispatch layer unions their returns (#521).
180
+ matches = find_matching_overload(overloads, **shared, strict: false)
181
+ return matches.first(1) unless shared[:arg_types].any? { |t| untyped_arg?(t) }
182
+
183
+ matches
159
184
  end
160
185
 
161
- # rubocop:disable Metrics/ParameterLists
186
+ # rubocop:disable-next Metrics/ParameterLists
162
187
  def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
163
- param_overrides:, strict:)
164
- return nil if strict && arg_types.any? { |t| untyped_arg?(t) }
188
+ param_overrides:, strict:, alias_expander: nil)
189
+ return [] if strict && arg_types.any? { |t| untyped_arg?(t) }
165
190
 
166
- overloads.find do |method_type|
167
- next false if block_required && !OverloadSelector.overload_has_block?(method_type)
168
- next false if !block_required && OverloadSelector.overload_requires_block?(method_type)
191
+ predicate = lambda do |method_type|
192
+ next false unless engages_block_shape?(method_type, block_required)
169
193
  next false if strict && !strictly_typed_params?(method_type, arg_types.size)
170
194
 
171
195
  matches?(
172
- method_type,
173
- arg_types,
174
- self_type: self_type,
175
- instance_type: instance_type,
176
- type_vars: type_vars,
177
- param_overrides: param_overrides
196
+ method_type, arg_types,
197
+ self_type: self_type, instance_type: instance_type,
198
+ type_vars: type_vars, param_overrides: param_overrides,
199
+ alias_expander: alias_expander
178
200
  )
179
201
  end
202
+ # Strict keeps its historical first-match short-circuit (a dispatch hot path); the gradual
203
+ # pass needs the full candidate list for the #521 union.
204
+ return [overloads.find(&predicate)].compact if strict
205
+
206
+ overloads.select(&predicate)
207
+ end
208
+
209
+ # Whether the overload's block clause is compatible with the call site's block shape: a
210
+ # block-bearing call engages only block-declaring overloads; a block-less call skips overloads
211
+ # that REQUIRE one.
212
+ def engages_block_shape?(method_type, block_required)
213
+ return OverloadSelector.overload_has_block?(method_type) if block_required
214
+
215
+ !OverloadSelector.overload_requires_block?(method_type)
180
216
  end
181
- # rubocop:enable Metrics/ParameterLists
182
217
 
183
218
  # Treats the literal `untyped` carrier (`Dynamic[Top]`) as too imprecise to drive a strict-pass
184
219
  # match. Other `Dynamic`-wrapped types with a concrete static facet carry enough information to
@@ -194,9 +229,13 @@ module Rigor
194
229
  # to `Dynamic[Top]` at the param level. Only fires when EVERY positional param has a known
195
230
  # alias-or-strict shape; otherwise gradual matching takes over.
196
231
  def find_matching_overload_via_aliases(overloads, arg_types:, block_required:)
232
+ # Issue #521 — an untyped argument "maybe"-accepts EVERY alias's strict arm, so it cannot
233
+ # discriminate between overloads here any more than in the strict pass; without this guard a
234
+ # Dynamic arg pinned `Array#*(string) -> String` purely by declaration order.
235
+ return nil if arg_types.any? { |t| untyped_arg?(t) }
236
+
197
237
  overloads.find do |method_type|
198
- next false if block_required && !OverloadSelector.overload_has_block?(method_type)
199
- next false if !block_required && OverloadSelector.overload_requires_block?(method_type)
238
+ next false unless engages_block_shape?(method_type, block_required)
200
239
 
201
240
  fun = method_type.type
202
241
  next false unless arity_compatible?(fun, arg_types.size)
@@ -278,7 +317,8 @@ module Rigor
278
317
  end
279
318
  end
280
319
 
281
- def matches?(method_type, arg_types, self_type:, instance_type:, type_vars:, param_overrides:)
320
+ def matches?(method_type, arg_types, self_type:, instance_type:, type_vars:, param_overrides:,
321
+ alias_expander: nil)
282
322
  return false if method_type.respond_to?(:type_params) && rejects_keyword_required?(method_type)
283
323
 
284
324
  fun = method_type.type
@@ -292,7 +332,8 @@ module Rigor
292
332
  self_type: self_type,
293
333
  instance_type: instance_type,
294
334
  type_vars: type_vars,
295
- param_overrides: param_overrides
335
+ param_overrides: param_overrides,
336
+ alias_expander: alias_expander
296
337
  )
297
338
  end
298
339
  end
@@ -346,12 +387,14 @@ module Rigor
346
387
  head
347
388
  end
348
389
 
349
- def accepts_param?(param, arg, self_type:, instance_type:, type_vars:, param_overrides:)
390
+ def accepts_param?(param, arg, self_type:, instance_type:, type_vars:, param_overrides:,
391
+ alias_expander: nil)
350
392
  param_type = param_overrides[param.name] || RbsTypeTranslator.translate(
351
393
  param.type,
352
394
  self_type: self_type,
353
395
  instance_type: instance_type,
354
- type_vars: type_vars
396
+ type_vars: type_vars,
397
+ alias_expander: alias_expander
355
398
  )
356
399
  # An `untyped` arg gradually accepts against every param, so a value-pinning param would be
357
400
  # "matched" with zero evidence and its value-precise return (`(nil) -> []`) would beat broader