rigortype 0.1.18 → 0.2.0

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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -224
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
  8. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  9. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
  10. data/lib/rigor/analysis/check_rules.rb +756 -132
  11. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  12. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  13. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  14. data/lib/rigor/analysis/diagnostic.rb +8 -0
  15. data/lib/rigor/analysis/fact_store.rb +5 -4
  16. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  17. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
  18. data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
  19. data/lib/rigor/analysis/runner.rb +75 -27
  20. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  21. data/lib/rigor/analysis/worker_session.rb +31 -25
  22. data/lib/rigor/bleeding_edge.rb +123 -0
  23. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  24. data/lib/rigor/cache/descriptor.rb +86 -8
  25. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  26. data/lib/rigor/cache/store.rb +5 -3
  27. data/lib/rigor/cli/annotate_command.rb +122 -16
  28. data/lib/rigor/cli/baseline_command.rb +4 -3
  29. data/lib/rigor/cli/check_command.rb +118 -16
  30. data/lib/rigor/cli/coverage_command.rb +148 -16
  31. data/lib/rigor/cli/coverage_scan.rb +57 -0
  32. data/lib/rigor/cli/explain_command.rb +2 -0
  33. data/lib/rigor/cli/lsp_command.rb +3 -7
  34. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  35. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  36. data/lib/rigor/cli/options.rb +9 -0
  37. data/lib/rigor/cli/plugins_command.rb +4 -5
  38. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  39. data/lib/rigor/cli/protection_renderer.rb +63 -0
  40. data/lib/rigor/cli/protection_report.rb +68 -0
  41. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  42. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  43. data/lib/rigor/cli/trace_command.rb +2 -1
  44. data/lib/rigor/cli/triage_command.rb +8 -4
  45. data/lib/rigor/cli/triage_renderer.rb +15 -1
  46. data/lib/rigor/cli/type_of_command.rb +1 -1
  47. data/lib/rigor/cli/type_scan_command.rb +2 -1
  48. data/lib/rigor/cli.rb +12 -3
  49. data/lib/rigor/configuration/dependencies.rb +2 -4
  50. data/lib/rigor/configuration/severity_profile.rb +13 -1
  51. data/lib/rigor/configuration.rb +100 -6
  52. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  53. data/lib/rigor/environment/class_registry.rb +4 -3
  54. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  55. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  56. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  57. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  58. data/lib/rigor/environment/rbs_loader.rb +74 -5
  59. data/lib/rigor/environment.rb +17 -7
  60. data/lib/rigor/flow_contribution/fact.rb +1 -1
  61. data/lib/rigor/flow_contribution.rb +3 -5
  62. data/lib/rigor/inference/acceptance.rb +17 -9
  63. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  64. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  65. data/lib/rigor/inference/budget_trace.rb +29 -2
  66. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  67. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  68. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  69. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  70. data/lib/rigor/inference/expression_typer.rb +1072 -71
  71. data/lib/rigor/inference/hkt_body.rb +8 -11
  72. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  73. data/lib/rigor/inference/hkt_registry.rb +10 -11
  74. data/lib/rigor/inference/macro_block_self_type.rb +2 -2
  75. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  76. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  77. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
  78. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  79. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  80. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  81. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  82. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  83. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  84. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  85. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  86. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  87. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
  88. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  89. data/lib/rigor/inference/method_dispatcher.rb +112 -49
  90. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  91. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  92. data/lib/rigor/inference/mutation_widening.rb +147 -11
  93. data/lib/rigor/inference/narrowing.rb +284 -53
  94. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  95. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  97. data/lib/rigor/inference/protection_scanner.rb +86 -0
  98. data/lib/rigor/inference/scope_indexer.rb +821 -76
  99. data/lib/rigor/inference/statement_evaluator.rb +1179 -102
  100. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  101. data/lib/rigor/inference/synthetic_method.rb +7 -7
  102. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  103. data/lib/rigor/language_server/completion_provider.rb +6 -12
  104. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  105. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  106. data/lib/rigor/language_server/hover_provider.rb +2 -3
  107. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  108. data/lib/rigor/language_server/server.rb +9 -17
  109. data/lib/rigor/language_server.rb +4 -5
  110. data/lib/rigor/plugin/base.rb +245 -87
  111. data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
  112. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  113. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  114. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  115. data/lib/rigor/plugin/macro.rb +6 -8
  116. data/lib/rigor/plugin/manifest.rb +49 -90
  117. data/lib/rigor/plugin/node_rule_walk.rb +59 -14
  118. data/lib/rigor/plugin/registry.rb +18 -18
  119. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  120. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  121. data/lib/rigor/protection/mutator.rb +246 -0
  122. data/lib/rigor/rbs_extended.rb +24 -36
  123. data/lib/rigor/reflection.rb +4 -7
  124. data/lib/rigor/scope/discovery_index.rb +16 -2
  125. data/lib/rigor/scope.rb +185 -16
  126. data/lib/rigor/sig_gen/generator.rb +8 -0
  127. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  128. data/lib/rigor/sig_gen/writer.rb +40 -2
  129. data/lib/rigor/source/constant_path.rb +62 -0
  130. data/lib/rigor/source.rb +1 -0
  131. data/lib/rigor/triage/catalogue.rb +4 -19
  132. data/lib/rigor/triage.rb +69 -1
  133. data/lib/rigor/type/bound_method.rb +2 -11
  134. data/lib/rigor/type/combinator.rb +45 -3
  135. data/lib/rigor/type/constant.rb +2 -11
  136. data/lib/rigor/type/data_class.rb +2 -11
  137. data/lib/rigor/type/data_instance.rb +2 -11
  138. data/lib/rigor/type/hash_shape.rb +2 -11
  139. data/lib/rigor/type/integer_range.rb +2 -11
  140. data/lib/rigor/type/intersection.rb +2 -11
  141. data/lib/rigor/type/nominal.rb +2 -11
  142. data/lib/rigor/type/plain_lattice.rb +37 -0
  143. data/lib/rigor/type/refined.rb +72 -13
  144. data/lib/rigor/type/singleton.rb +2 -11
  145. data/lib/rigor/type/struct_class.rb +75 -0
  146. data/lib/rigor/type/struct_instance.rb +93 -0
  147. data/lib/rigor/type/tuple.rb +5 -15
  148. data/lib/rigor/type.rb +2 -0
  149. data/lib/rigor/version.rb +1 -1
  150. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  151. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  152. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  154. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
  157. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  158. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  159. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  160. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
  163. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  164. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  165. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  166. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  167. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  168. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
  171. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  172. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  173. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
  174. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  175. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  176. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
  179. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
  182. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  184. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  185. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  186. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  189. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  190. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  191. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  195. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  196. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  197. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
  198. data/sig/rigor/analysis/fact_store.rbs +3 -0
  199. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  200. data/sig/rigor/plugin/base.rbs +5 -2
  201. data/sig/rigor/plugin/manifest.rbs +1 -2
  202. data/sig/rigor/scope.rbs +18 -1
  203. data/sig/rigor/type.rbs +37 -1
  204. data/sig/rigor.rbs +1 -1
  205. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  206. data/skills/rigor-plugin-author/SKILL.md +6 -4
  207. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  208. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  209. metadata +25 -2
  210. data/lib/rigor/plugin/macro/external_file.rb +0 -143
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../../type"
4
4
  require_relative "singleton_folding"
5
+ require_relative "member_shape_projection"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -30,6 +31,10 @@ module Rigor
30
31
  module DataFolding
31
32
  module_function
32
33
 
34
+ # The `[]` / `to_h` / `deconstruct` / `members` / `with` projections
35
+ # and the reader-redefinition guard are shared with {StructFolding}.
36
+ extend MemberShapeProjection
37
+
33
38
  # @return [Rigor::Type, nil] the folded result, or nil to defer.
34
39
  def try_dispatch(context)
35
40
  receiver = context.receiver
@@ -165,81 +170,12 @@ module Rigor
165
170
  when :deconstruct then instance_deconstruct(instance)
166
171
  when :deconstruct_keys then instance_deconstruct_keys(instance, args)
167
172
  when :members then instance_members(instance)
168
- when :with then instance_with(instance, args)
173
+ when :with
174
+ instance_with(instance, args) do |members, class_name|
175
+ Type::Combinator.data_instance_of(members: members, class_name: class_name)
176
+ end
169
177
  end
170
178
  end
171
-
172
- # A `Data.define` class body (the `class Point < Data.define(:x);
173
- # def x; …; end; end` subclass body, or a `Const = Data.define(:x) do
174
- # def x; …; end; end` block) can redefine a member's synthesised
175
- # reader. When it does, `inst.x` runs that `def`, NOT the member, so
176
- # folding the read to the member type would be unsound (a downstream
177
- # FP). Both named forms register the override as a real `def` node
178
- # under the class name, so an entry in the project def-node table is
179
- # the discriminator (the synthesised reader has no def node). The
180
- # value accessors `[]` / `to_h` / `deconstruct` bypass the reader and
181
- # stay foldable, so this gate is on the bare member read only.
182
- def reader_overridden?(instance, method_name, scope)
183
- class_name = instance.class_name
184
- return false if class_name.nil? || scope.nil?
185
-
186
- !scope.user_def_for(class_name, method_name).nil?
187
- end
188
-
189
- def instance_index(instance, args)
190
- return nil unless args.size == 1
191
-
192
- arg = args.first
193
- return nil unless arg.is_a?(Type::Constant)
194
-
195
- key = arg.value
196
- case key
197
- when Symbol
198
- instance.members[key]
199
- when Integer
200
- values = instance.members.values
201
- idx = key.negative? ? key + values.size : key
202
- values[idx] if idx && idx >= 0 && idx < values.size
203
- end
204
- end
205
-
206
- def instance_to_h(instance)
207
- Type::Combinator.hash_shape_of(instance.members.dup)
208
- end
209
-
210
- def instance_deconstruct(instance)
211
- Type::Combinator.tuple_of(*instance.members.values)
212
- end
213
-
214
- # `deconstruct_keys(nil)` / `deconstruct_keys([:x])` both yield a
215
- # subset of the member map; the conservative, always-correct answer
216
- # is the full closed member shape.
217
- def instance_deconstruct_keys(instance, args)
218
- return nil unless args.size <= 1
219
-
220
- Type::Combinator.hash_shape_of(instance.members.dup)
221
- end
222
-
223
- def instance_members(instance)
224
- Type::Combinator.tuple_of(*instance.member_names.map { |name| Type::Combinator.constant_of(name) })
225
- end
226
-
227
- # `Data#with(x: 9)` returns a new frozen copy with the named members
228
- # overridden. Only a closed keyword `HashShape` whose keys are a
229
- # subset of the members folds; anything else defers (RBS resolves
230
- # `with` to `self`, returning the unchanged instance type).
231
- def instance_with(instance, args)
232
- return instance if args.empty?
233
- return nil unless args.size == 1
234
-
235
- shape = args.first
236
- return nil unless shape.is_a?(Type::HashShape) && shape.closed?
237
- return nil unless shape.optional_keys.empty?
238
- return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
239
-
240
- merged = instance.members.merge(shape.pairs)
241
- Type::Combinator.data_instance_of(members: merged, class_name: instance.class_name)
242
- end
243
179
  end
244
180
  end
245
181
  end
@@ -54,11 +54,10 @@ module Rigor
54
54
  # *correctness-preservingly* proved" excludes Constants whose
55
55
  # value is host-specific.
56
56
  module FileFolding
57
- # File class methods that the analyzer can fold *when the
58
- # fold is platform-safe to perform*. Today every entry is
59
- # platform-sensitive (every one observes `File::SEPARATOR`
60
- # or `File::ALT_SEPARATOR`); the gate below requires the
61
- # opt-in flag for any of them to fire.
57
+ # File class methods the analyzer can fold when the opt-in
58
+ # flag is set. Currently identical to PLATFORM_DEPENDENT_METHODS
59
+ # — separated for a future non-platform-sensitive tier that
60
+ # can fold without the opt-in flag.
62
61
  FILE_PURE_CLASS_METHODS = Set[
63
62
  :basename,
64
63
  :dirname,
@@ -72,8 +71,8 @@ module Rigor
72
71
  # Methods whose result depends on host directory-separator
73
72
  # semantics (`/` on POSIX, `/` AND `\` on Windows, drive
74
73
  # letters, UNC paths). Folding these would bake the
75
- # analyzer-host's platform into the inferred type. The opt-
76
- # in flag below controls whether to do it anyway.
74
+ # analyzer-host's platform into the inferred type. The opt-in
75
+ # flag below controls whether to do it anyway.
77
76
  PLATFORM_DEPENDENT_METHODS = Set[
78
77
  :basename, :dirname, :extname, :join, :split, :absolute_path?
79
78
  ].freeze
@@ -175,24 +175,18 @@ module Rigor
175
175
  type.is_a?(Type::Constant) && type.value.is_a?(Symbol)
176
176
  end
177
177
 
178
- # Element-yielding Enumerable methods covered as a v0.0.5
179
- # placeholder. RBS already binds the block parameter
180
- # correctly for plain `Array[T]` / `Set[T]` / `Range[T]`
181
- # receivers via generic substitution; this tier exists so
182
- # Tuple- and HashShape-shaped receivers reach the block
183
- # body with the precise per-position element union /
184
- # `Tuple[K, V]` pair rather than the projected
178
+ # Element-yielding Enumerable methods covered as a placeholder.
179
+ # RBS already binds the block parameter correctly for plain
180
+ # `Array[T]` / `Set[T]` / `Range[T]` receivers via generic
181
+ # substitution; this tier exists so Tuple- and HashShape-shaped
182
+ # receivers reach the block body with the precise per-position
183
+ # element union / `Tuple[K, V]` pair rather than the projected
185
184
  # `Array[union]` / `Hash[K, V]` widening.
186
185
  #
187
- # NOTE (v0.0.5): the per-method coverage here (group_by,
188
- # partition, each_slice, each_cons) is intentionally
189
- # narrow. The longer-term direction is to move
190
- # Enumerable-aware projections into a plugin tier modelled
191
- # after PHPStan's extension API (ADR-2). The placeholders
192
- # below stay until the plugin surface is in place; once it
193
- # ships, this dispatcher loses these arms and the
194
- # equivalent rules move into a built-in plugin loaded at
195
- # boot.
186
+ # NOTE: `Plugin::NodeRuleWalk` (ADR-52 WD4) is now in place as
187
+ # the intended migration target for these Enumerable projections.
188
+ # The four methods (group_by, partition, each_slice, each_cons)
189
+ # remain here pending that migration.
196
190
  def single_element_block_params(receiver)
197
191
  element = element_type_of(receiver)
198
192
  return nil if element.nil?
@@ -43,14 +43,21 @@ module Rigor
43
43
  private_constant :NUMERIC_CONSTRUCTORS
44
44
 
45
45
  # `Kernel#Integer(s)` predicate-aware refinement set
46
- # (v0.1.1 Track 1 slice 2b). Both `decimal-int-string` and
47
- # `numeric-string` describe digit-only ASCII strings, so
48
- # `Integer(s)` is total over the carrier domain and the
49
- # result is `>= 0`. The default `base: 10` invocation
50
- # accepts the same shape `String#to_i` does for these
51
- # predicates; the `Integer(s, base)` overload is left for
52
- # a later slice.
53
- INTEGER_REFINEMENT_PREDICATES = Set[:decimal_int, :numeric].freeze
46
+ # (v0.1.1 Track 1 slice 2b). `decimal-int-string` is the
47
+ # only string refinement whose every inhabitant `Integer(s)`
48
+ # parses without remainder, so the result is a plain
49
+ # `Integer` but NOT `non-negative-int`: the predicate
50
+ # `/\A-?\d+\z/` admits a leading sign, so `"-7"` is a valid
51
+ # decimal-int-string and `Integer("-7") == -7 < 0`. The
52
+ # narrowing is total (every inhabitant parses) but not `>= 0`,
53
+ # so it lands on `universal_int`. `numeric-string` is
54
+ # deliberately NOT in this set at all: since it was widened to
55
+ # the full Ruby numeric-literal grammar (floats, hex, rational,
56
+ # imaginary, signs), `Integer(numeric_string)` would raise for
57
+ # a `"1.5"` / `"2i"` inhabitant — not even total — so it falls
58
+ # through to RBS `Integer`. The `Integer(s, base)` overload is
59
+ # left for a later slice.
60
+ INTEGER_REFINEMENT_PREDICATES = Set[:decimal_int].freeze
54
61
  private_constant :INTEGER_REFINEMENT_PREDICATES
55
62
 
56
63
  def try_dispatch(context)
@@ -70,7 +77,7 @@ module Rigor
70
77
  # paths, tried in order:
71
78
  #
72
79
  # 1. A `Refined[String, predicate]` argument whose predicate
73
- # is a digit-only carrier narrows to `non-negative-int`
80
+ # is a total-parse carrier narrows to `universal_int`
74
81
  # (see {try_integer_from_refinement}).
75
82
  # 2. A `Constant` String or Numeric argument — optionally
76
83
  # with a `Constant[Integer]` base — runs the actual
@@ -120,9 +127,14 @@ module Rigor
120
127
  # `Kernel#Integer(s)` over a `Refined[String, predicate]`
121
128
  # whose predicate is in {INTEGER_REFINEMENT_PREDICATES}.
122
129
  # Mirrors the `String#to_i` projection in `ShapeDispatch`
123
- # (v0.1.1 slice 2a) — the result is always
124
- # `non-negative-int`. Returns nil for any other arg shape
125
- # so the RBS tier handles the generic `Integer(arg)` case.
130
+ # (v0.1.1 slice 2a) — the result is `universal_int`, NOT
131
+ # `non-negative-int`: a decimal-int-string admits a leading
132
+ # sign (`"-7"`), so the parsed Integer can be negative. The
133
+ # carrier stays an `IntegerRange` (rather than declining to
134
+ # the RBS `Nominal[Integer]`) so downstream range narrowing
135
+ # still has a range to intersect. Returns nil for any other
136
+ # arg shape so the RBS tier handles the generic `Integer(arg)`
137
+ # case.
126
138
  def try_integer_from_refinement(args)
127
139
  return nil unless args.size == 1
128
140
 
@@ -133,7 +145,7 @@ module Rigor
133
145
  return nil unless base.is_a?(Type::Nominal) && base.class_name == "String"
134
146
  return nil unless INTEGER_REFINEMENT_PREDICATES.include?(arg.predicate_id)
135
147
 
136
- Type::Combinator.non_negative_int
148
+ Type::Combinator.universal_int
137
149
  end
138
150
 
139
151
  def try_array(args)
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # The member-shape projections shared by {DataFolding} and
9
+ # {StructFolding}. A `DataInstance` and a `StructInstance` expose the
10
+ # same surface — an ordered `members` map, `member_names`, and a
11
+ # `class_name` — so the value projections off that surface
12
+ # (`[]` / `to_h` / `deconstruct` / `deconstruct_keys` / `members` /
13
+ # `with`) and the reader-redefinition guard are identical between the
14
+ # two folders. Only `#with`'s carrier constructor differs
15
+ # (`data_instance_of` vs `struct_instance_of`), so it takes a block
16
+ # that builds the new instance from the merged member map.
17
+ #
18
+ # Both folders `extend` this module so the projections resolve as
19
+ # their own module functions (matching their `module_function` style).
20
+ module MemberShapeProjection
21
+ # A Data/Struct subclass body can redefine a member's synthesised
22
+ # reader (`def x`); when it does, `inst.x` runs that `def`, not the
23
+ # member, so folding the read would be unsound. A real `def` node
24
+ # under the class name is the discriminator (the synthesised reader
25
+ # has none), so an entry in the project def-node table gates the
26
+ # bare member read off.
27
+ def reader_overridden?(instance, method_name, scope)
28
+ class_name = instance.class_name
29
+ return false if class_name.nil? || scope.nil?
30
+
31
+ !scope.user_def_for(class_name, method_name).nil?
32
+ end
33
+
34
+ def instance_index(instance, args)
35
+ return nil unless args.size == 1
36
+
37
+ arg = args.first
38
+ return nil unless arg.is_a?(Type::Constant)
39
+
40
+ key = arg.value
41
+ case key
42
+ when Symbol
43
+ instance.members[key]
44
+ when Integer
45
+ values = instance.members.values
46
+ idx = key.negative? ? key + values.size : key
47
+ values[idx] if idx && idx >= 0 && idx < values.size
48
+ end
49
+ end
50
+
51
+ def instance_to_h(instance)
52
+ Type::Combinator.hash_shape_of(instance.members.dup)
53
+ end
54
+
55
+ def instance_deconstruct(instance)
56
+ Type::Combinator.tuple_of(*instance.members.values)
57
+ end
58
+
59
+ # `deconstruct_keys(nil)` / `deconstruct_keys([:x])` both yield a
60
+ # subset of the member map; the conservative, always-correct answer
61
+ # is the full closed member shape.
62
+ def instance_deconstruct_keys(instance, args)
63
+ return nil unless args.size <= 1
64
+
65
+ Type::Combinator.hash_shape_of(instance.members.dup)
66
+ end
67
+
68
+ def instance_members(instance)
69
+ Type::Combinator.tuple_of(*instance.member_names.map { |name| Type::Combinator.constant_of(name) })
70
+ end
71
+
72
+ # `#with(x: 9)` returns a new copy with the named members
73
+ # overridden. Only a closed keyword `HashShape` whose keys are a
74
+ # subset of the members folds; anything else defers (RBS resolves
75
+ # `with` to `self`, returning the unchanged instance type). The
76
+ # carrier constructor differs per folder, so the caller supplies it
77
+ # as a block taking the merged member map and the class name.
78
+ def instance_with(instance, args)
79
+ return instance if args.empty?
80
+ return nil unless args.size == 1
81
+
82
+ shape = args.first
83
+ return nil unless shape.is_a?(Type::HashShape) && shape.closed?
84
+ return nil unless shape.optional_keys.empty?
85
+ return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
86
+
87
+ merged = instance.members.merge(shape.pairs)
88
+ yield(merged, instance.class_name)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -25,9 +25,7 @@ module Rigor
25
25
  # so without this preference, an alias-typed overload like
26
26
  # `Array#[](::int) -> Elem` would beat the strict
27
27
  # `Array#[](Range) -> Array[Elem]?` overload for a Range
28
- # argument. (Surfaced during v0.1.1 self-analysis; see the
29
- # "Interface-strictness on overload selection" item in
30
- # `docs/ROADMAP.md`.)
28
+ # argument.
31
29
  # 3. **Pass 2 — gradual fall-back.** If no fully strict overload
32
30
  # matches, accept the first arity-and-gradual-accept match
33
31
  # (the v0.1.1 behaviour). Alias / Interface / Intersection
@@ -45,8 +45,9 @@ module Rigor
45
45
  # machinery works without duplication: `Tuple[Integer, String]`
46
46
  # dispatches as `Array[Integer | String]`, and
47
47
  # `HashShape{a: Integer}` dispatches as `Hash[Symbol, Integer]`.
48
- # Tuple-aware refinements (e.g., `tuple[0]` returning the precise
49
- # member) are deferred to Slice 5 phase 2.
48
+ # Tuple/HashShape element precision (e.g., `tuple[0]` returning
49
+ # the precise member) is handled by the preceding `ShapeDispatch`
50
+ # tier.
50
51
  #
51
52
  # Remaining limitations:
52
53
  #
@@ -107,17 +108,11 @@ module Rigor
107
108
  # @return [Rigor::Type, nil] inferred return type, or `nil`
108
109
  # when no rule resolves (no class name, no method, dispatch
109
110
  # on a Top/Dynamic[Top] receiver, etc.).
110
- # @param scope [Rigor::Scope, nil] when supplied, enables
111
- # ADR-43 RBS-complete-ancestor resolution: a call on a
112
- # Ruby-source subclass not known to RBS, whose discovered
113
- # superclass chain reaches an allow-listed RBS-complete
114
- # ancestor (e.g. `Rigor::Plugin::Base`), resolves against
115
- # that ancestor's RBS. `nil` (the default for every caller
116
- # that does not thread a scope) keeps the legacy behaviour —
117
- # such an inherited call stays unresolved and degrades to
118
- # `Dynamic[Top]`, which is the false-positive-safe default
119
- # for the open hierarchies (`< ActionController::Base`, …)
120
- # the allow-list deliberately excludes.
111
+ # @param scope [Rigor::Scope, nil] when supplied, enables ADR-43
112
+ # RBS-complete-ancestor resolution against
113
+ # `ALLOWED_RBS_COMPLETE_ANCESTORS`. `nil` keeps inherited calls
114
+ # unresolved (`Dynamic[Top]`) the FP-safe default for open
115
+ # hierarchies (`< ActionController::Base`, …).
121
116
  def try_dispatch(context)
122
117
  environment = context.environment
123
118
  return nil if environment.nil?
@@ -249,15 +244,8 @@ module Rigor
249
244
  ["Array", :instance, tuple_type_args(receiver)]
250
245
  when Type::HashShape
251
246
  ["Hash", :instance, hash_shape_type_args(receiver)]
252
- when Type::DataInstance
253
- # ADR-48 — project a member-instance carrier to its tagging
254
- # class (or the `Data` supertype) so non-member calls
255
- # (`inspect`, `==`, `frozen?`, ...) resolve through RBS
256
- # rather than mis-firing undefined-method. Member reads were
257
- # already folded by DataFolding above this tier.
258
- [receiver.class_name || "Data", :instance, []]
259
- when Type::DataClass
260
- [receiver.class_name || "Data", :singleton, []]
247
+ when Type::DataInstance, Type::DataClass, Type::StructInstance, Type::StructClass
248
+ member_carrier_descriptor(receiver)
261
249
  when Type::BoundMethod
262
250
  # `BoundMethod` is a precision-bearing alias for
263
251
  # `Nominal[Method]`: it carries the
@@ -275,6 +263,20 @@ module Rigor
275
263
  end
276
264
  end
277
265
 
266
+ # ADR-48 — project a `Data`/`Struct` member carrier to its tagging
267
+ # class (or the `Data`/`Struct` supertype) so non-member calls
268
+ # (`inspect`, `==`, `frozen?`, ...) resolve through RBS rather than
269
+ # mis-firing undefined-method. Precise member reads were already
270
+ # folded by DataFolding / StructFolding above this tier.
271
+ def member_carrier_descriptor(receiver)
272
+ case receiver
273
+ when Type::DataInstance then [receiver.class_name || "Data", :instance, []]
274
+ when Type::DataClass then [receiver.class_name || "Data", :singleton, []]
275
+ when Type::StructInstance then [receiver.class_name || "Struct", :instance, []]
276
+ when Type::StructClass then [receiver.class_name || "Struct", :singleton, []]
277
+ end
278
+ end
279
+
278
280
  def tuple_type_args(tuple)
279
281
  return [] if tuple.elements.empty?
280
282