rigortype 0.4.0 → 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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  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/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require_relative "../cache/value_digest"
5
+
6
+ module Rigor
7
+ class Environment
8
+ # ADR-32 WD5 — one plugin's `source_rbs_synthesizer` output for one source file, cached per (file content,
9
+ # plugin entry, engine source). Two callers read it, and sharing the function is the point:
10
+ #
11
+ # - {Environment.collect_virtual_rbs} hands each output to the loader as a `virtual:` buffer;
12
+ # - {.digest} reduces every loaded synthesizer's output for a file to one comparable value, which the
13
+ # incremental session (ADR-89 WD1, issue #1536) keeps on the file's seed bundle and compares across an
14
+ # edit.
15
+ #
16
+ # Because the digest reads the very function (and the very cache entries) the loader is fed from, "the
17
+ # digest did not move" means "the loader read the same bytes for this file", not "some model of what
18
+ # the plugin reads did not move". That is why it is taken from the output rather than from the file's
19
+ # comment lines: rbs-inline binds an annotation by adjacency, so a plain comment or a blank line can move
20
+ # one to another declaration, and in an annotated file every `def`, `attr`, constant and mixin gets a
21
+ # skeleton whether or not it carries an annotation of its own.
22
+ module SourceRbsSynthesis
23
+ PRODUCER_ID = "plugin.source_rbs_synthesizer"
24
+
25
+ # The digest of a file no loaded synthesizer contributes anything for — the answer for every file of a
26
+ # project without an annotation, and for every file when no synthesizer is loaded at all. Not a hex
27
+ # digest, so it can never collide with one.
28
+ NO_CONTRIBUTION = "none"
29
+
30
+ module_function
31
+
32
+ # The cache stores the empty string `""` as the "no contribution" sentinel because `Cache::Store`
33
+ # treats `nil` as a cache miss. Error tuples are stored as the canonical `[:error, message_string]`
34
+ # Array so the same wrapper short-circuits subsequent runs against unchanged broken input.
35
+ def output_for(plugin, callable, path, cache_store)
36
+ return invoke_safely(callable, path) if cache_store.nil?
37
+ return invoke_safely(callable, path) unless File.file?(path)
38
+
39
+ descriptor = cache_descriptor(plugin, path)
40
+ return invoke_safely(callable, path) if descriptor.nil?
41
+
42
+ cache_store.fetch_or_compute(
43
+ producer_id: PRODUCER_ID,
44
+ params: {},
45
+ descriptor: descriptor,
46
+ generation_cap: generation_cap
47
+ ) { invoke_safely(callable, path) || "" }
48
+ end
49
+
50
+ # Issue #1536 — every synthesizer's output for `path`, reduced to one value: {NO_CONTRIBUTION} when none
51
+ # of them contributes anything, a SHA-256 hex digest of each plugin's id and {.contribution} otherwise,
52
+ # and nil when an output could not be read at all.
53
+ #
54
+ # nil means "unknown", and a caller must treat it as moved. Every failure is per file: one plugin that
55
+ # cannot answer for one file leaves every other file's digest intact.
56
+ #
57
+ # @param synthesizers — `Plugin::Registry#source_rbs_synthesizers` pairs.
58
+ def digest(synthesizers, path, cache_store)
59
+ sha = Digest::SHA256.new
60
+ contributed = false
61
+ synthesizers.each do |plugin, callable|
62
+ tag, text = contribution(output_for(plugin, callable, path, cache_store))
63
+ next if tag.nil?
64
+
65
+ contributed = true
66
+ sha << plugin.manifest.id.to_s.b << "\x00" << tag << "\x00" << text.to_s.b << "\x01"
67
+ end
68
+ contributed ? sha.hexdigest : NO_CONTRIBUTION
69
+ rescue StandardError
70
+ nil
71
+ end
72
+
73
+ # What one output puts into the environment, as a `[tag, text]` pair, or nil for nothing: exactly the
74
+ # RBS {Environment.interpret_synthesizer_outcome} hands the loader. The ADR-32 WD6 / WD12 notices are left
75
+ # out. They reach only the run-level `source-rbs-*` rows, which every run regenerates and the per-file
76
+ # cache never serves, and they quote line numbers, so a line shift in a file carrying a malformed `#:`
77
+ # would otherwise read as a moved contribution. A failed synthesis contributes nothing to the loader but
78
+ # is kept as one stable value of its own, so a file flipping between "no annotation" and "broken
79
+ # annotation" still reads as moved. Any shape the contract does not name is digested whole, by value: the
80
+ # output is computed on one run and served from its cache entry on the next, and a digest of its
81
+ # `Marshal.dump` bytes can differ between the two (issue #1574).
82
+ #
83
+ # The RBS text itself is digested whole, comments included. RBS reads a `# resolve-type-names: false`
84
+ # magic comment at the start of a buffer, rbs-inline copies a `.rb` file's first comment line there, and
85
+ # a string-literal type may span lines that start with `#`, so no line-based stripping is sound (#1549).
86
+ def contribution(output)
87
+ return nil if output.nil? || output == ""
88
+ return ["rbs", output] if output.is_a?(String)
89
+ return ["raw", Cache::ValueDigest.hexdigest(output)] unless output.is_a?(Array)
90
+
91
+ case output[0]
92
+ when :error then ["error", nil]
93
+ when :ok then contribution(output[1])
94
+ else ["raw", Cache::ValueDigest.hexdigest(output)]
95
+ end
96
+ end
97
+
98
+ # One entry per (plugin, source file), all of them live for as long as the file is in the project — a
99
+ # generation count says nothing about staleness here, so this producer declares itself out of
100
+ # `Cache::Store#evict!`'s compaction pass and is bounded only by the size-based LRU pass. A method
101
+ # rather than a constant: `Cache::Store` is not loaded yet when this file is.
102
+ def generation_cap
103
+ Cache::Store::UNBOUNDED_GENERATIONS
104
+ end
105
+
106
+ # The key composes the file's content SHA with the plugin's `PluginEntry` (id + version + config_hash),
107
+ # so a config change or a content change invalidates the entry, and with the engine's source identity
108
+ # (issue #1009): a plugin's manifest version does not move when a checkout edits its synthesizer, and
109
+ # a stale string here also keeps the `rbs.virtual_rbs` env key warm, so the new build's rules would
110
+ # read the old build's RBS.
111
+ def cache_descriptor(plugin, path)
112
+ Cache::Descriptor.new(
113
+ files: [Cache::Descriptor::FileEntry.new(
114
+ path: path.to_s,
115
+ comparator: :digest,
116
+ value: input_digest(path)
117
+ )],
118
+ plugins: [plugin.plugin_entry],
119
+ configs: Cache::EngineSource.key_config_entries
120
+ )
121
+ rescue Cache::EngineSource::Unavailable
122
+ # An engine that cannot be identified must not be keyed by its inputs alone (issue #1009): nil runs the
123
+ # synthesizer uncached rather than serving an entry another build may have written.
124
+ nil
125
+ end
126
+
127
+ def input_digest(path)
128
+ Digest::SHA256.hexdigest(File.binread(path))
129
+ rescue ::SystemCallError
130
+ # Unreadable file → key on the path alone; the synthesizer's File.file?/File.read will see the same
131
+ # failure and return nil.
132
+ Digest::SHA256.hexdigest(path.to_s)
133
+ end
134
+
135
+ def invoke_safely(callable, path)
136
+ callable.call(path.to_s)
137
+ rescue StandardError
138
+ # WD6 fail-soft — a synthesizer that raises does NOT crash analysis. Unlike the `[:error, msg]`
139
+ # return path (which the runner surfaces as `source-rbs-synthesis-failed`), an unhandled raise is
140
+ # swallowed silently; the unexamined-raise channel is deliberately silent per WD6.
141
+ nil
142
+ end
143
+
144
+ private_class_method :contribution, :generation_cap, :cache_descriptor, :input_digest, :invoke_safely
145
+ end
146
+ end
147
+ end
@@ -3,6 +3,7 @@
3
3
  require "digest"
4
4
 
5
5
  require_relative "environment/default_libraries"
6
+ require_relative "environment/required_features"
6
7
  require_relative "environment/class_registry"
7
8
  require_relative "environment/rbs_loader"
8
9
  require_relative "environment/reflection"
@@ -15,6 +16,7 @@ require_relative "environment/bundle_sig_discovery"
15
16
  require_relative "environment/lockfile_resolver"
16
17
  require_relative "environment/installed_gem_set"
17
18
  require_relative "environment/rbs_collection_discovery"
19
+ require_relative "environment/source_rbs_synthesis"
18
20
  require_relative "plugin/isolation"
19
21
  require_relative "cache/engine_source"
20
22
  require_relative "environment/rbs_coverage_report"
@@ -274,8 +276,8 @@ module Rigor
274
276
  bundler_lockfile: nil,
275
277
  rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
276
278
  synthetic_method_index: nil, project_patched_methods: nil,
277
- source_files: [], locked_gems: nil)
278
- resolved_paths = signature_paths || default_signature_paths(root)
279
+ source_files: [], locked_gems: nil, required_features: nil)
280
+ resolved_paths = project_signature_roots(signature_paths, root: root)
279
281
  # O4 MVP — append per-gem `sig/` directories discovered under the target project's bundler install
280
282
  # root. Empty array when neither an explicit path nor auto-detection finds a bundle. Order: user
281
283
  # `signature_paths:` win first (semantic precedence inside `RbsLoader.build_env_for`); gem-shipped
@@ -313,8 +315,10 @@ module Rigor
313
315
  # and the `data/vendored_gem_sigs/` bundle (`redis`, `nokogiri`, `pg`, …). `skip_gem_names:` passes
314
316
  # both sets so the collection copy doesn't double-declare against rigor's bundled RBS (the
315
317
  # `RBS::DuplicatedDeclarationError` hazard).
316
- merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
317
- skip_gem_names = merged_libraries + RbsLoader.vendored_gem_names
318
+ # Issue #1700 — plus the gated vendored directories this run's source activates ({#feature_tokens}).
319
+ merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s) +
320
+ feature_tokens(required_features, source_files, libraries)).uniq
321
+ skip_gem_names = collection_skip_gem_names(merged_libraries)
318
322
  collection_paths = RbsCollectionDiscovery.discover(
319
323
  lockfile_path: rbs_collection_lockfile,
320
324
  project_root: root,
@@ -390,8 +394,30 @@ module Rigor
390
394
  )
391
395
  end
392
396
 
397
+ # The project-owned `sig/` roots a run reads: the explicit `signature_paths:` when given, else the
398
+ # auto-detected `<root>/sig` (or none). Public so {Cache::IncrementalSnapshot} digests exactly the
399
+ # directories the environment loads, including the auto-detected one a `nil` `signature_paths:` hides.
400
+ def project_signature_roots(signature_paths, root: Dir.pwd)
401
+ signature_paths || default_signature_paths(root)
402
+ end
403
+
393
404
  private
394
405
 
406
+ # Issue #1700 — the `RequiredFeatures` library tokens for the gated vendored directories this run asks for:
407
+ # each feature its source requires and each one the configuration's `libraries:` names. `required_features`
408
+ # is the scan a caller already took; nil scans `source_files`. Whether a directory then loads is decided
409
+ # by `RbsLoader.build_env_for` against the environment it builds.
410
+ def feature_tokens(required_features, source_files, libraries)
411
+ RequiredFeatures.tokens(required_features || RequiredFeatures.scan(source_files), libraries)
412
+ end
413
+
414
+ # The gem names an `rbs collection` copy must not double-declare against: the library list and every
415
+ # vendored directory except a `RequiredFeatures`-gated one. A gated directory stands down for any other
416
+ # copy of its declarations (`GatedSignatureGuard`), so the collection's copy is kept and wins.
417
+ def collection_skip_gem_names(library_names)
418
+ library_names + RbsLoader.vendored_gem_names.reject { |name| RequiredFeatures.gated_dir?(name) }
419
+ end
420
+
395
421
  def default_signature_paths(root)
396
422
  sig = Pathname(root) / DEFAULT_PROJECT_SIG_DIR
397
423
  sig.directory? ? [sig] : []
@@ -496,11 +522,9 @@ module Rigor
496
522
  # empty and this method short-circuits to `[]` without walking the file list.
497
523
  #
498
524
  # WD5 — when `cache_store` is supplied, each (file, plugin) synthesizer call is memoised through
499
- # `Cache::Store`. The cache key composes the file's content SHA with the plugin's `PluginEntry` (id +
500
- # version + config_hash) so a config change or content change invalidates the entry automatically, and
501
- # with the engine's source identity (issue #1009): a plugin's manifest version does not move when a
502
- # checkout edits its synthesizer, and a stale string here also keeps the `rbs.virtual_rbs` env key warm,
503
- # so the new build's rules would read the old build's RBS.
525
+ # `Cache::Store` ({SourceRbsSynthesis.output_for}, which owns the key). The incremental session's
526
+ # per-file digest (issue #1536) reads the same function, so the two can never disagree on what a file
527
+ # contributed.
504
528
  def collect_virtual_rbs(plugin_registry, source_files, cache_store, reporter)
505
529
  return [] if plugin_registry.nil?
506
530
 
@@ -511,7 +535,7 @@ module Rigor
511
535
  result = []
512
536
  source_files.each do |path|
513
537
  synthesizers.each do |plugin, callable|
514
- outcome = synthesizer_output_for(plugin, callable, path, cache_store)
538
+ outcome = SourceRbsSynthesis.output_for(plugin, callable, path, cache_store)
515
539
  outcome = interpret_synthesizer_outcome(outcome, plugin, path, reporter)
516
540
  next if outcome.nil? || outcome.empty?
517
541
 
@@ -522,25 +546,6 @@ module Rigor
522
546
  result
523
547
  end
524
548
 
525
- # ADR-32 WD5 — cache wrapper around a single (plugin, file) invocation. The cache stores the empty
526
- # string `""` as the "no contribution" sentinel because `Cache::Store` treats `nil` as a cache miss.
527
- # Error tuples are stored as the canonical `[:error, message_string]` Array so the same wrapper
528
- # short-circuits subsequent runs against unchanged broken input.
529
- def synthesizer_output_for(plugin, callable, path, cache_store)
530
- return invoke_synthesizer_safely(callable, path) if cache_store.nil?
531
- return invoke_synthesizer_safely(callable, path) unless File.file?(path)
532
-
533
- descriptor = build_synthesizer_cache_descriptor(plugin, path)
534
- return invoke_synthesizer_safely(callable, path) if descriptor.nil?
535
-
536
- cache_store.fetch_or_compute(
537
- producer_id: SYNTHESIZER_CACHE_PRODUCER_ID,
538
- params: {},
539
- descriptor: descriptor,
540
- generation_cap: synthesizer_generation_cap
541
- ) { invoke_synthesizer_safely(callable, path) || "" }
542
- end
543
-
544
549
  # ADR-32 WD6 / WD12 — route a synthesizer return value through the per-run reporter. The
545
550
  # synthesizer's contract (declared in
546
551
  # `plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb`) admits four return shapes:
@@ -661,50 +666,6 @@ module Rigor
661
666
  prefix = "#{root}#{File::SEPARATOR}"
662
667
  path.to_s.start_with?(prefix) ? path.to_s.delete_prefix(prefix) : path.to_s
663
668
  end
664
-
665
- SYNTHESIZER_CACHE_PRODUCER_ID = "plugin.source_rbs_synthesizer"
666
- private_constant :SYNTHESIZER_CACHE_PRODUCER_ID
667
-
668
- # One entry per (plugin, source file), all of them live for as long as the file is in the project — a
669
- # generation count says nothing about staleness here, so this producer declares itself out of
670
- # `Cache::Store#evict!`'s compaction pass and is bounded only by the size-based LRU pass. A method
671
- # rather than a constant: `Cache::Store` is not loaded yet when this class body runs.
672
- def synthesizer_generation_cap
673
- Cache::Store::UNBOUNDED_GENERATIONS
674
- end
675
-
676
- def build_synthesizer_cache_descriptor(plugin, path)
677
- Cache::Descriptor.new(
678
- files: [Cache::Descriptor::FileEntry.new(
679
- path: path.to_s,
680
- comparator: :digest,
681
- value: synthesizer_input_digest(path)
682
- )],
683
- plugins: [plugin.plugin_entry],
684
- configs: Cache::EngineSource.key_config_entries
685
- )
686
- rescue Cache::EngineSource::Unavailable
687
- # An engine that cannot be identified must not be keyed by its inputs alone (issue #1009): nil runs the
688
- # synthesizer uncached rather than serving an entry another build may have written.
689
- nil
690
- end
691
-
692
- def synthesizer_input_digest(path)
693
- Digest::SHA256.hexdigest(File.binread(path))
694
- rescue ::SystemCallError
695
- # Unreadable file → key on the path alone; the synthesizer's File.file?/File.read will see the same
696
- # failure and return nil.
697
- Digest::SHA256.hexdigest(path.to_s)
698
- end
699
-
700
- def invoke_synthesizer_safely(callable, path)
701
- callable.call(path.to_s)
702
- rescue StandardError
703
- # WD6 fail-soft — a synthesizer that raises does NOT crash analysis. Unlike the `[:error, msg]`
704
- # return path (which the runner surfaces as `source-rbs-synthesis-failed`), an unhandled raise is
705
- # swallowed silently; the unexamined-raise channel is deliberately silent per WD6.
706
- nil
707
- end
708
669
  end
709
670
 
710
671
  # Resolves a constant name to a Rigor::Type::Nominal (the *instance* type carrier). Consults the static
@@ -486,7 +486,17 @@ module Rigor
486
486
  )
487
487
  end
488
488
 
489
- nil
489
+ project_sequence_to_enumerator(self_type, other_type)
490
+ end
491
+
492
+ # `Enumerator::ArithmeticSequence[E]` carries only the element of its `Enumerator[Numeric, void]` superclass
493
+ # (#1794), so against a two-argument `Enumerator` it projects to `Enumerator[E, top]`, `top` being what RBS
494
+ # `void` translates to: `Enumerator[Integer, void]` and `[Integer, untyped]` accept it, as RBS would.
495
+ def project_sequence_to_enumerator(self_type, other_type)
496
+ return nil unless self_type.class_name == "Enumerator" && self_type.type_args.size == 2
497
+ return nil unless other_type.class_name == "Enumerator::ArithmeticSequence" && other_type.type_args.size == 1
498
+
499
+ Type::Combinator.nominal_of("Enumerator", type_args: [other_type.type_args.first, Type::Combinator.top])
490
500
  end
491
501
 
492
502
  def project_tuple_to_nominal(tuple)
@@ -963,11 +973,14 @@ module Rigor
963
973
  end
964
974
  end
965
975
 
966
- # Constant[v] accepts only Constant[v'] with structurally equal value. Any other type is rejected
967
- # (modulo the universal Bot/Dynamic short-circuits already applied upstream).
976
+ # Constant[v] accepts Constant[v'] with structurally equal value, and the plain nominal of a class whose
977
+ # only instance is `v`. Any other type is rejected (modulo the universal Bot/Dynamic short-circuits
978
+ # already applied upstream).
968
979
  def accepts_constant(self_type, other_type, mode)
969
980
  if other_type.is_a?(Type::Constant) && self_type == other_type
970
981
  Type::AcceptsResult.yes(mode: mode, reasons: "structural literal match")
982
+ elsif sole_instance_nominal?(self_type, other_type)
983
+ Type::AcceptsResult.yes(mode: mode, reasons: "#{other_type.class_name} has no instance but the literal")
971
984
  else
972
985
  Type::AcceptsResult.no(
973
986
  mode: mode,
@@ -976,6 +989,18 @@ module Rigor
976
989
  end
977
990
  end
978
991
 
992
+ # `true`, `false` and `nil` are the only instances of their classes, so `FalseClass` holds exactly the
993
+ # values `false` holds. Read apart, `Array.new(n, false)`, typed `Array[FalseClass]` through the
994
+ # `[T] (Integer, T)` binding, failed a declared `-> Array[bool]` and fired `def.return-type-mismatch` on
995
+ # correct code.
996
+ SOLE_INSTANCE_CLASSES = { true => "TrueClass", false => "FalseClass", nil => "NilClass" }.freeze
997
+ private_constant :SOLE_INSTANCE_CLASSES
998
+
999
+ def sole_instance_nominal?(constant, other_type)
1000
+ other_type.is_a?(Type::Nominal) && other_type.type_args.empty? &&
1001
+ SOLE_INSTANCE_CLASSES.key?(constant.value) && SOLE_INSTANCE_CLASSES[constant.value] == other_type.class_name
1002
+ end
1003
+
979
1004
  # Tuple[A1..An] accepts:
980
1005
  # - Tuple[B1..Bn] when arities match and each Ai accepts Bi (covariant per-position).
981
1006
  # - Anything else: no (we cannot prove the arity from a generic nominal alone).
@@ -54,14 +54,16 @@ module Rigor
54
54
  reset_per_bind_state
55
55
  end
56
56
 
57
- # The names the last {#bind} bound optimistically nil-free (see the class comment).
57
+ # The names the last {#bind} bound optimistically nil-free (see the class comment). Read-only: it is a
58
+ # shared frozen empty when the bind marked nothing.
58
59
  attr_reader :optimistic
59
60
 
60
61
  # Binds the block's parameters into `scope`: {#bind}'s types through `Scope#with_local`, then the
61
- # optimistic mark for every name in {#optimistic}.
62
+ # optimistic mark for every name in {#optimistic}. It applies them through
63
+ # {MultiTargetBinder.apply_bindings} directly rather than through a `Result`, which every block entry
64
+ # built only to apply at once ([#1504](https://github.com/rigortype/rigor/issues/1504)).
62
65
  def bind_onto(block_node, scope)
63
- types = bind(block_node, scope: scope)
64
- MultiTargetBinder::Result.new(types: types, optimistic: @optimistic.dup.freeze).apply_to(scope)
66
+ MultiTargetBinder.apply_bindings(scope, bind(block_node, scope: scope), @optimistic)
65
67
  end
66
68
 
67
69
  # @return ordered map from parameter name to bound type. Anonymous
@@ -90,29 +92,40 @@ module Rigor
90
92
 
91
93
  private
92
94
 
95
+ NO_POSITIONS = [].freeze
96
+ NO_NAMES = [].freeze
97
+ private_constant :NO_POSITIONS, :NO_NAMES
98
+
93
99
  # {#apply_auto_splat} rewrites the positional table for the block it is binding, so every {#bind} starts
94
100
  # from the declared types again; a binder reused across blocks must not see the previous block's splat.
101
+ # The two lists start as shared frozen empties, since most blocks mark nothing ({#mark_optimistic}).
95
102
  def reset_per_bind_state
96
103
  @expected_param_types = @declared_param_types
97
104
  @splat_rest_type = nil
98
- @optimistic_positions = []
99
- @optimistic = []
105
+ @optimistic_positions = NO_POSITIONS
106
+ @optimistic = NO_NAMES
100
107
  @scope = nil
101
108
  end
102
109
 
110
+ # Records `name` as bound optimistically, allocating {#optimistic} on the first mark of a {#bind}.
111
+ def mark_optimistic(name)
112
+ @optimistic = [] if @optimistic.equal?(NO_NAMES)
113
+ @optimistic << name
114
+ end
115
+
103
116
  # `|_1, _2|` numbered-parameter form. Prism exposes the implicit count through
104
117
  # `NumberedParametersNode#maximum` (the highest `_N` referenced in the body); we materialise bindings
105
118
  # for `:_1` through `:_maximum`, auto-splatting exactly as the explicit list of that many required
106
119
  # positionals would, so the body's `LocalVariableReadNode` lookups see the same types and marks.
107
120
  def bind_numbered_parameters(numbered_node)
108
121
  arity = numbered_node.maximum
109
- apply_auto_splat(BlockAutoSplat::ParameterShape.of_arity(arity))
122
+ apply_auto_splat { BlockAutoSplat::ParameterShape.of_arity(arity) }
110
123
 
111
124
  bindings = {}
112
125
  arity.times do |i|
113
126
  name = :"_#{i + 1}"
114
127
  bindings[name] = positional_type_at(i)
115
- @optimistic << name if @optimistic_positions.include?(i)
128
+ mark_optimistic(name) if @optimistic_positions.include?(i)
116
129
  end
117
130
  bindings
118
131
  end
@@ -127,7 +140,7 @@ module Rigor
127
140
  params_node = params_root.parameters
128
141
  return {} if params_node.nil?
129
142
 
130
- apply_auto_splat(BlockAutoSplat::ParameterShape.of(params_node))
143
+ apply_auto_splat { BlockAutoSplat::ParameterShape.of(params_node) }
131
144
 
132
145
  bindings = {}
133
146
  bind_positionals(params_node, bindings, 0)
@@ -152,9 +165,12 @@ module Rigor
152
165
  # and {BlockAutoSplat} answers a table for that value and `shape`. A multi-arg yield (e.g.
153
166
  # `each_with_index`'s `(element, index)` pair) is NOT auto-splatted — matching Ruby semantics where a
154
167
  # multi-arg yield to a `|a, b, c|` block fills the extra slot with nil rather than splatting any
155
- # element.
156
- def apply_auto_splat(shape)
168
+ # element. The block answers the parameter list's shape, built only past that test
169
+ # ([#1504](https://github.com/rigortype/rigor/issues/1504)).
170
+ def apply_auto_splat
157
171
  return unless @expected_param_types.size == 1
172
+
173
+ shape = yield
158
174
  return unless shape.splats?
159
175
 
160
176
  table = BlockAutoSplat.for(shape, @expected_param_types[0])
@@ -242,12 +258,12 @@ module Rigor
242
258
  case param
243
259
  when Prism::RequiredParameterNode
244
260
  bindings[param.name] = positional_type_at(cursor)
245
- @optimistic << param.name if @optimistic_positions.include?(cursor)
261
+ mark_optimistic(param.name) if @optimistic_positions.include?(cursor)
246
262
  when Prism::MultiTargetNode
247
263
  nested = MultiTargetBinder.bind_marked(param, positional_type_at(cursor),
248
264
  optimistic: @optimistic_positions.include?(cursor), scope: @scope)
249
265
  bindings.merge!(nested.types)
250
- @optimistic.concat(nested.optimistic)
266
+ nested.optimistic.each { |name| mark_optimistic(name) }
251
267
  end
252
268
  end
253
269
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "method_catalog"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module Builtins
9
+ # Issue #1740 — which instance methods CRuby itself defines on a core class or module, read from the offline
10
+ # catalogues under `data/builtins/ruby_core/` (`tool/extract_builtin_catalog.rb` records each `rb_define_method`
11
+ # and prelude `def` of an `Init_*` function on the class it names, aliases included). A registration inside an
12
+ # `#if 0` block CRuby keeps for RDoc carries `rdoc_only: true` and is left out: `random.c`'s registers
13
+ # `Random#seed` on `Random`, though it runs on `Random::Base`.
14
+ # `spec/rigor/inference/builtins/c_ruby_definers_spec.rb` checks every listed method the running Ruby has
15
+ # against its runtime owner. RBS is not that record: core RBS redeclares some inherited methods on a subclass
16
+ # (`Integer#quo` and `File#to_path`, whose CRuby owners are `Numeric` and `IO`), and declares a few CRuby no
17
+ # longer defines (`Process::Status#&`). A class no catalogue covers answers false, as does a method its
18
+ # catalogue does not list: the caller reads both as "not proven". The catalogue is one CRuby snapshot.
19
+ module CRubyDefiners
20
+ module_function
21
+
22
+ # Does the catalogue prove CRuby defines `method_name` on `class_name` itself?
23
+ def defines?(class_name, method_name)
24
+ names = INDEX[class_name.to_s.delete_prefix("::")]
25
+ !names.nil? && names.include?(method_name.to_s)
26
+ end
27
+
28
+ def build_index
29
+ index = Hash.new { |hash, key| hash[key] = Set.new }
30
+ MethodCatalog.topic_paths.each do |path|
31
+ data = YAML.safe_load_file(path, permitted_classes: [Symbol])
32
+ classes = data.is_a?(Hash) ? data["classes"] : nil
33
+ next unless classes.is_a?(Hash)
34
+
35
+ classes.each do |name, klass|
36
+ next if klass["rdoc_only"]
37
+
38
+ names = index[name]
39
+ names.merge(defined_names(klass["instance_methods"]))
40
+ names.merge(defined_names(klass["aliases"]))
41
+ end
42
+ end
43
+ Ractor.make_shareable(index.to_h { |name, names| [name, names.freeze] })
44
+ rescue Psych::SyntaxError, SystemCallError
45
+ Ractor.make_shareable({})
46
+ end
47
+
48
+ # The names of `entries` CRuby defines at runtime: one the catalogue marks `rdoc_only` was registered only for
49
+ # RDoc, inside an `#if 0` block.
50
+ def defined_names(entries)
51
+ return [] unless entries.is_a?(Hash)
52
+
53
+ entries.filter_map { |method, entry| method unless entry.is_a?(Hash) && entry["rdoc_only"] }
54
+ end
55
+
56
+ INDEX = build_index
57
+ private_constant :INDEX
58
+ end
59
+ end
60
+ end
61
+ end
@@ -48,6 +48,9 @@ module Rigor
48
48
  new(path: File.join(DATA_ROOT, "#{topic}.yml"), mutating_selectors: mutating_selectors)
49
49
  end
50
50
 
51
+ # Every catalogue file under {DATA_ROOT}, in a stable order.
52
+ def self.topic_paths = Dir[File.join(DATA_ROOT, "*.yml")]
53
+
51
54
  def initialize(path:, mutating_selectors: {})
52
55
  @path = path
53
56
  @mutating_selectors = mutating_selectors.transform_values(&:freeze).freeze
@@ -413,10 +413,16 @@ module Rigor
413
413
  NESTED_SCOPE_NODES = Set[Prism::BlockNode, Prism::LambdaNode].freeze
414
414
  private_constant :NESTED_SCOPE_NODES
415
415
 
416
+ # The index arm spells {INDEX_STORE_NODES} out rather than splatting it: `when *ARRAY` copies the array
417
+ # on every evaluation, and this runs once per node of every scanned block body, which cost `rigor check`
418
+ # about 126K allocations over the v0.3.9 `lib` ([#1502](https://github.com/rigortype/rigor/issues/1502),
419
+ # the shape `ScopeIndexer::META_CONSTANT_WRITE_NODES` records). Keep the arm and the list in step.
416
420
  def mutated_receiver(node)
417
421
  case node
418
422
  when Prism::CallNode then node.receiver if MutationWidening::SHAPE_MUTATORS.include?(node.name)
419
- when *INDEX_STORE_NODES then node.receiver
423
+ when Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode,
424
+ Prism::IndexTargetNode
425
+ node.receiver
420
426
  end
421
427
  end
422
428