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
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "project_patched_methods"
6
6
  require_relative "../analysis/dependency_source_inference/return_type_heuristic"
7
+ require_relative "../source/alias_names"
7
8
  require_relative "../source/constant_path"
8
9
  require_relative "../source/node_children"
9
10
 
@@ -11,7 +12,8 @@ module Rigor
11
12
  module Inference
12
13
  # ADR-17 slice 2 — pre-pass scanner. Walks every file the user listed under `pre_eval:` and
13
14
  # harvests every `def` / `def self.` declaration inside a class / module body into a
14
- # {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier.
15
+ # {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier, together with every
16
+ # `alias new old` / `alias_method :new, :old` there (issue #1702, read through {Source::AliasNames}).
15
17
  #
16
18
  # The walker is intentionally a strict subset of {Rigor::Inference::ScopeIndexer}'s machinery: it
17
19
  # only needs `class C; def m; end; end` shape recognition, not full inference. Parse errors degrade
@@ -29,6 +31,10 @@ module Rigor
29
31
  end
30
32
  end
31
33
 
34
+ # An alias the walk met, before {resolve_aliases} knows whether its old name is one the patch files define.
35
+ PendingAlias = Data.define(:class_name, :new_name, :old_name, :kind, :source_path, :source_line)
36
+ private_constant :PendingAlias
37
+
32
38
  module_function
33
39
 
34
40
  # @param paths — absolute paths to the pre-eval files. The runner has already
@@ -39,10 +45,11 @@ module Rigor
39
45
  # users editing a monkey-patch file see the in-flight version in their analysis.
40
46
  # @return the populated registry plus any per-file warnings.
41
47
  def scan(paths, buffer: nil)
42
- entries = []
48
+ collected = []
43
49
  diagnostics = []
44
50
  census = Set.new
45
- paths.each { |path| scan_file(path, entries, diagnostics, buffer, census) }
51
+ paths.each { |path| scan_file(path, collected, diagnostics, buffer, census) }
52
+ entries = resolve_aliases(collected)
46
53
  diagnostics.concat(duplicate_declaration_diagnostics(entries))
47
54
  Result.new(
48
55
  registry: ProjectPatchedMethods.new(entries: entries, write_census: census.freeze),
@@ -78,6 +85,46 @@ module Rigor
78
85
  end
79
86
  private_class_method :duplicate_declaration_diagnostics
80
87
 
88
+ # Turns each {PendingAlias} into the entry it publishes, walking the bindings in configuration-then-source
89
+ # order as Ruby runs them. An alias binds to the body its old name has WHEN IT RUNS, so only a `def` or alias
90
+ # earlier in that order may supply its return type: `alias to_m to_modint` after `def to_modint` answers what
91
+ # `to_modint` does, while `alias_method :orig_succ, :succ` ahead of a patch's `def succ` is the method
92
+ # Integer already had. An alias whose old name nothing earlier binds records that name as `alias_of`, for the
93
+ # dispatcher to answer with the class's existing method (`alias old_plus +` on Integer), or with
94
+ # `Dynamic[top]` when nothing knows the name: the alias is still published, since a call to it is not the
95
+ # undefined method a dropped entry would report.
96
+ #
97
+ # Within one file the later binding of a name replaces the earlier, as at runtime: the alias-method-chain
98
+ # idiom (`def foo`, `alias_method :foo_without_x, :foo`, `alias_method :foo, :foo_with_x`) leaves `foo` the
99
+ # alias. Bindings of one name in DIFFERENT files are what `pre-eval.duplicate-declaration` reports, and the
100
+ # registry keeps the first of those, as it always has for `def`s.
101
+ def resolve_aliases(collected)
102
+ bound = {}
103
+ per_file = collected.group_by(&:source_path).values.map do |items|
104
+ final = {}
105
+ items.each do |item|
106
+ entry = item.is_a?(PendingAlias) ? alias_entry(item, bound) : item
107
+ key = [entry.class_name, entry.method_name, entry.kind]
108
+ bound[key] = entry
109
+ final.delete(key)
110
+ final[key] = entry
111
+ end
112
+ final.values
113
+ end
114
+ per_file.flatten(1)
115
+ end
116
+ private_class_method :resolve_aliases
117
+
118
+ def alias_entry(item, bound)
119
+ target = bound[[item.class_name, item.old_name, item.kind]]
120
+ ProjectPatchedMethods::Entry.new(
121
+ class_name: item.class_name, method_name: item.new_name, kind: item.kind,
122
+ source_path: item.source_path, source_line: item.source_line,
123
+ return_type: target&.return_type, alias_of: target ? target.alias_of : item.old_name
124
+ )
125
+ end
126
+ private_class_method :alias_entry
127
+
81
128
  def scan_file(path, entries, diagnostics, buffer = nil, census = Set.new)
82
129
  physical = buffer ? buffer.resolve(path) : path
83
130
  parse_result =
@@ -139,6 +186,13 @@ module Rigor
139
186
  descend_singleton_class(node, qualified_prefix, source_path, entries)
140
187
  when Prism::DefNode
141
188
  record_def_node(node, qualified_prefix, in_singleton_class, source_path, entries)
189
+ when Prism::AliasMethodNode
190
+ record_alias(node, Source::AliasNames.keyword_names(node), qualified_prefix, in_singleton_class,
191
+ source_path, entries)
192
+ when Prism::CallNode
193
+ record_alias(node, Source::AliasNames.alias_method_call_names(node), qualified_prefix,
194
+ in_singleton_class, source_path, entries)
195
+ walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
142
196
  else
143
197
  walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
144
198
  end
@@ -186,6 +240,20 @@ module Rigor
186
240
  )
187
241
  end
188
242
  private_class_method :record_def_node
243
+
244
+ # `names` is the alias's `[new_name, old_name]`, or nil for a computed name or a call that is not an alias.
245
+ # The side follows the body it is written in, as a `def`'s does: `alias` and `alias_method` inside
246
+ # `class << self` both bind on the singleton.
247
+ def record_alias(node, names, qualified_prefix, in_singleton_class, source_path, entries)
248
+ return if names.nil? || qualified_prefix.empty?
249
+
250
+ entries << PendingAlias.new(
251
+ class_name: qualified_prefix.join("::"), new_name: names.first, old_name: names.last,
252
+ kind: in_singleton_class ? :singleton : :instance,
253
+ source_path: source_path, source_line: node.location&.start_line || 1
254
+ )
255
+ end
256
+ private_class_method :record_alias
189
257
  end
190
258
  end
191
259
  end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/alias_names"
6
+ require_relative "../source/node_children"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # ADR-121 WD7 (issues #1796, #1799) — what a `refine` body tells the refinement table, read the one way the project
11
+ # walk (`ScopeIndexer#record_refinement_defs`) and the gem source walker
12
+ # (`Analysis::DependencySourceInference::Walker#walk_refine_body`) both use, so the two cannot drift. The table is
13
+ # `{refined class => {method => [refining modules]}}`; `Scope::DiscoveryIndex::REFINEMENT_WILDCARD` stands for
14
+ # what could not be read.
15
+ #
16
+ # A body is fully read when every call on its own `self` (the refinement module) is one that defines a name the
17
+ # walk can spell, or one that defines nothing: a `def`, an `alias` or `alias_method` of two literal names, a
18
+ # `define_method` of a literal name, an `attr_*` of literal names, an `undef`, a visibility call, or
19
+ # `Refinement#target`. Anything
20
+ # else (`import_methods`, `send`, a computed `define_method`, `include`, a nested `refine`, any other call on
21
+ # `self`) may define a name the walk cannot spell, so the body's names are incomplete and the table gets a
22
+ # names-wildcard row for each target. A call on another receiver cannot define a method on the refinement, and
23
+ # a call inside a `def` or `define_method` body runs on an instance of the refined class, so neither counts.
24
+ module RefineCensus
25
+ # `names`, every name the body is read to define, in textual order; `complete`, whether those are all of them;
26
+ # `defs`, the typed refined arm's bodies in textual order: `[name, Prism::DefNode]` for a `def` or an alias of
27
+ # an earlier `def`, and `[name, nil]` for any other definer (`define_method`, `attr_*`, `undef`, an alias of a
28
+ # name the body did not define earlier). A later definer replaces an earlier one, as Ruby's method table
29
+ # keeps the last, so a name whose last definer has no `DefNode` types as `Dynamic[top]`.
30
+ BodyReading = Struct.new(:names, :complete, :defs)
31
+
32
+ # Calls on the refinement that define nothing: visibility, with no argument, literal names, a `def`, or an
33
+ # allowlisted definer call.
34
+ VISIBILITY_CALLS = Set[:private, :public, :protected, :module_function, :private_constant].freeze
35
+ # `Refinement#target`, a reader of the refined class (the other `Refinement` method on Ruby 4.0.5 is
36
+ # `import_methods`).
37
+ READER_CALLS = Set[:target].freeze
38
+ # `attr_*` and whether each defines the reader and the writer.
39
+ ATTR_CALLS = {
40
+ attr_reader: [true, false].freeze, attr_writer: [false, true].freeze, attr_accessor: [true, true].freeze,
41
+ attr: [true, false].freeze
42
+ }.freeze
43
+
44
+ # A3 — the calls whose String argument may hold `refine` as code or as a method name, and the two families
45
+ # whose `self` is their receiver (so `M.send(:refine, …)` runs `refine` on `M`).
46
+ SEND_CALLS = Set[:send, :__send__, :public_send].freeze
47
+ SELF_EVAL_CALLS = Set[:instance_eval, :class_eval, :module_eval, :instance_exec, :class_exec, :module_exec].freeze
48
+ NAMING_CALLS = Set[
49
+ :method, :public_method, :singleton_method, :instance_method, :public_instance_method, :define_method,
50
+ :define_singleton_method, :alias_method
51
+ ].freeze
52
+ REFINE_WORD = /\brefine\b/
53
+
54
+ private_constant :VISIBILITY_CALLS, :READER_CALLS, :ATTR_CALLS, :REFINE_WORD
55
+
56
+ module_function
57
+
58
+ def wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
59
+
60
+ # §2.2 — what `body` (a refine block's body, or nil) defines on the refined class.
61
+ def read_body(body)
62
+ reading = BodyReading.new([], true, [])
63
+ scan_body(body, reading, {}, true) if body
64
+ reading.names.uniq!
65
+ reading
66
+ end
67
+
68
+ # Records `reading`'s names for each of `targets` under `refining` into `table` (nil starts one), plus a
69
+ # names-wildcard row per target when the body's names are incomplete. Returns the table.
70
+ def record_rows(table, targets, reading, refining)
71
+ table ||= {}
72
+ targets.each do |class_name|
73
+ reading.names.each { |name| add_row(table, class_name, name, refining) }
74
+ add_row(table, class_name, wildcard, refining) unless reading.complete
75
+ end
76
+ table
77
+ end
78
+
79
+ # One `{class_name => {method_key => [refining]}}` entry, `refining` once.
80
+ def add_row(table, class_name, method_key, refining)
81
+ modules = ((table[class_name] ||= {})[method_key] ||= [])
82
+ modules << refining unless modules.include?(refining)
83
+ table
84
+ end
85
+
86
+ # A3 — a `:refine` Symbol literal: it may name `Module#refine` for `send`, `alias_method`, `method`, ….
87
+ def refine_symbol?(node) = node.is_a?(Prism::SymbolNode) && node.unescaped == "refine"
88
+
89
+ # A3 — a String literal (or a heredoc's literal part) holding the word `refine`, as code an eval runs or as a
90
+ # method name.
91
+ def refine_string?(node)
92
+ case node
93
+ # As bytes: a literal whose bytes are not valid in its encoding (`"\xff …"`) must not raise.
94
+ when Prism::StringNode then REFINE_WORD.match?(node.unescaped.b)
95
+ when Prism::InterpolatedStringNode then node.parts.any? { |part| refine_string?(part) }
96
+ else false
97
+ end
98
+ end
99
+
100
+ # A3 — does a String argument of `call` count: an eval, `send` or method-naming call?
101
+ def string_literal_call?(call)
102
+ name = call.name
103
+ name == :eval || SELF_EVAL_CALLS.include?(name) || SEND_CALLS.include?(name) || NAMING_CALLS.include?(name)
104
+ end
105
+
106
+ # A3 — the `self` a refine literal argument of `call` runs `refine` on: `:receiver` for a `send` or `*_eval`
107
+ # / `*_exec` call (the receiver, or the caller's `self` when it has none or it is `self`), `:unknown` for an
108
+ # `eval` on a binding, `:self` for everything else (the caller's `self`).
109
+ def literal_target(call)
110
+ receiver = call.receiver
111
+ self_receiver = receiver.nil? || receiver.is_a?(Prism::SelfNode)
112
+ if SEND_CALLS.include?(call.name) || SELF_EVAL_CALLS.include?(call.name)
113
+ self_receiver ? :self : :receiver
114
+ elsif call.name == :eval
115
+ self_receiver ? :self : :unknown
116
+ else
117
+ :self
118
+ end
119
+ end
120
+
121
+ # An implicit- or `self`-receiver call: one whose `self` is the walk's `self`.
122
+ def self_call?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
123
+
124
+ # `classify` is false inside a `define_method` block or its arguments, whose calls run on an instance of the
125
+ # refined class (or before the method exists), so they define nothing on the refinement. `latest` maps each
126
+ # name to the last `def` the body wrote for it so far, which an alias copies.
127
+ def scan_body(node, reading, latest, classify)
128
+ case node
129
+ when Prism::DefNode
130
+ record_def(node, reading, latest) if node.receiver.nil?
131
+ return
132
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
133
+ return
134
+ when Prism::AliasMethodNode
135
+ return record_alias(Source::AliasNames.keyword_names(node), reading, latest)
136
+ when Prism::UndefNode
137
+ return record_undef(node, reading, latest)
138
+ when Prism::CallNode
139
+ if classify && self_call?(node)
140
+ classify_call(node, reading, latest)
141
+ classify = node.name != :define_method
142
+ end
143
+ end
144
+
145
+ node.rigor_each_child { |child| scan_body(child, reading, latest, classify) }
146
+ end
147
+
148
+ def record_def(node, reading, latest)
149
+ reading.names << node.name
150
+ reading.defs << [node.name, node]
151
+ latest[node.name] = node
152
+ end
153
+
154
+ # `alias new old` / `alias_method :new, :old`: `new` is defined; it copies `old`'s body when the refine body
155
+ # wrote `old` earlier (Ruby copies at the alias, so a later `def old` does not move it), and otherwise it
156
+ # copies the refined class's own method, whose body the walk does not hold.
157
+ def record_alias(pair, reading, latest)
158
+ return reading.complete = false if pair.nil?
159
+
160
+ new_name, old_name = pair
161
+ reading.names << new_name
162
+ def_node = latest[old_name]
163
+ return record_bodiless(new_name, reading, latest) if def_node.nil?
164
+
165
+ reading.defs << [new_name, def_node]
166
+ latest[new_name] = def_node
167
+ end
168
+
169
+ # A name a definer with no `DefNode` defines: it replaces any earlier `def` of the name, so the typed arm
170
+ # finds no body for it (`refine(String) { def center(a) = 1; define_method(:center) { |a| "dm" } }` answers
171
+ # `"dm"` on Ruby 4.0.5), and an alias of it copies no body either.
172
+ def record_bodiless(name, reading, latest)
173
+ reading.names << name
174
+ reading.defs << [name, nil]
175
+ latest.delete(name)
176
+ end
177
+
178
+ def record_undef(node, reading, latest)
179
+ node.names.each do |name|
180
+ symbol = literal_name(name)
181
+ symbol ? record_bodiless(symbol, reading, latest) : reading.complete = false
182
+ end
183
+ end
184
+
185
+ # Classifies a call on the refinement.
186
+ def classify_call(node, reading, latest)
187
+ name = node.name
188
+ arguments = node.arguments&.arguments || []
189
+ if (attr = ATTR_CALLS[name])
190
+ record_attr(arguments, attr, reading, latest)
191
+ elsif name == :define_method
192
+ record_define_method(arguments, reading, latest)
193
+ elsif name == :alias_method
194
+ record_alias(Source::AliasNames.alias_method_call_names(node), reading, latest)
195
+ else
196
+ reading.complete = false unless defines_nothing?(node, arguments)
197
+ end
198
+ end
199
+
200
+ # A visibility call with no argument, literal names, a `def` or a call; `Refinement#target` with none.
201
+ def defines_nothing?(node, arguments)
202
+ return arguments.all? { |argument| visibility_argument?(argument) } if VISIBILITY_CALLS.include?(node.name)
203
+
204
+ READER_CALLS.include?(node.name) && arguments.empty? && node.block.nil?
205
+ end
206
+
207
+ def record_attr(arguments, (reader, writer), reading, latest)
208
+ arguments.each do |argument|
209
+ name = literal_name(argument)
210
+ next reading.complete = false if name.nil?
211
+
212
+ record_bodiless(name, reading, latest) if reader
213
+ record_bodiless(:"#{name}=", reading, latest) if writer
214
+ end
215
+ end
216
+
217
+ def record_define_method(arguments, reading, latest)
218
+ name = arguments.first && literal_name(arguments.first)
219
+ name ? record_bodiless(name, reading, latest) : reading.complete = false
220
+ end
221
+
222
+ # `private`, `private :a, "b"`, `private def x`, `private attr_reader :x`: the nested call is classified on
223
+ # its own when the walk descends into it.
224
+ def visibility_argument?(argument)
225
+ argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode) || argument.is_a?(Prism::DefNode) ||
226
+ argument.is_a?(Prism::CallNode)
227
+ end
228
+
229
+ # A literal's name, or nil for none and for bytes no Symbol holds: `define_method("\xff")` raises
230
+ # EncodingError on Ruby 4.0.5, so the body is not read.
231
+ def literal_name(node)
232
+ return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
233
+
234
+ name = node.unescaped
235
+ name.to_sym if name&.valid_encoding?
236
+ end
237
+ end
238
+ end
239
+ end