rigortype 0.4.1 → 0.4.3

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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -1
@@ -3,7 +3,9 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "project_patched_methods"
6
+ require_relative "project_patched_definedness_guard"
6
7
  require_relative "../analysis/dependency_source_inference/return_type_heuristic"
8
+ require_relative "../source/alias_names"
7
9
  require_relative "../source/constant_path"
8
10
  require_relative "../source/node_children"
9
11
 
@@ -11,7 +13,8 @@ module Rigor
11
13
  module Inference
12
14
  # ADR-17 slice 2 — pre-pass scanner. Walks every file the user listed under `pre_eval:` and
13
15
  # harvests every `def` / `def self.` declaration inside a class / module body into a
14
- # {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier.
16
+ # {ProjectPatchedMethods} registry the dispatcher consults below the plugin tier, together with every
17
+ # `alias new old` / `alias_method :new, :old` there (issue #1702, read through {Source::AliasNames}).
15
18
  #
16
19
  # The walker is intentionally a strict subset of {Rigor::Inference::ScopeIndexer}'s machinery: it
17
20
  # only needs `class C; def m; end; end` shape recognition, not full inference. Parse errors degrade
@@ -29,6 +32,17 @@ module Rigor
29
32
  end
30
33
  end
31
34
 
35
+ # An alias the walk met, before {resolve_aliases} knows whether its old name is one the patch files define.
36
+ PendingAlias = Data.define(:class_name, :new_name, :old_name, :kind, :source_path, :source_line)
37
+ private_constant :PendingAlias
38
+
39
+ # A `def` written under a definedness guard (`def foo ... unless method_defined?(:foo)`): a polyfill that runs
40
+ # only when nothing bound the name before it, so it never replaces an earlier binding.
41
+ GuardedDef = Data.define(:entry) do
42
+ def source_path = entry.source_path
43
+ end
44
+ private_constant :GuardedDef
45
+
32
46
  module_function
33
47
 
34
48
  # @param paths — absolute paths to the pre-eval files. The runner has already
@@ -39,10 +53,11 @@ module Rigor
39
53
  # users editing a monkey-patch file see the in-flight version in their analysis.
40
54
  # @return the populated registry plus any per-file warnings.
41
55
  def scan(paths, buffer: nil)
42
- entries = []
56
+ collected = []
43
57
  diagnostics = []
44
58
  census = Set.new
45
- paths.each { |path| scan_file(path, entries, diagnostics, buffer, census) }
59
+ paths.each { |path| scan_file(path, collected, diagnostics, buffer, census) }
60
+ entries = resolve_aliases(collected)
46
61
  diagnostics.concat(duplicate_declaration_diagnostics(entries))
47
62
  Result.new(
48
63
  registry: ProjectPatchedMethods.new(entries: entries, write_census: census.freeze),
@@ -51,33 +66,75 @@ module Rigor
51
66
  end
52
67
 
53
68
  # ADR-17 § "Failure modes" — when two pre-eval entries declare the same `(class_name, method_name,
54
- # kind)` triple, emit one `:info` `pre-eval.duplicate-declaration` diagnostic per collision. The
55
- # registry's first-write-wins behaviour is unchanged; the diagnostic just makes the shadowing
56
- # visible so users notice when a later patch is silently masked.
69
+ # kind)` triple, the last-listed one wins, as at runtime where the later `def` replaces the earlier. Emit
70
+ # one `:info` `pre-eval.duplicate-declaration` diagnostic per SHADOWED entry, reported on that earlier
71
+ # declaration (the declaration that never takes effect) and naming the
72
+ # winning declaration it yields to.
57
73
  def duplicate_declaration_diagnostics(entries)
58
- seen = {}
59
- entries.each_with_object([]) do |entry, acc|
60
- key = [entry.class_name, entry.method_name, entry.kind]
61
- if (first = seen[key])
62
- acc << build_diagnostic(
63
- path: entry.source_path,
64
- line: entry.source_line,
65
- column: 1,
66
- severity: :info,
67
- rule: "pre-eval.duplicate-declaration",
68
- message: "pre-eval duplicate declaration: " \
69
- "#{entry.class_name}##{entry.method_name} " \
70
- "(#{entry.kind}) is already declared at " \
71
- "#{first.source_path}:#{first.source_line}. " \
72
- "The first declaration wins; this entry is shadowed."
73
- )
74
- else
75
- seen[key] = entry
76
- end
74
+ winners = entries.to_h { |entry| [[entry.class_name, entry.method_name, entry.kind], entry] }
75
+ entries.filter_map do |entry|
76
+ winner = winners[[entry.class_name, entry.method_name, entry.kind]]
77
+ next if winner.equal?(entry)
78
+
79
+ build_diagnostic(
80
+ path: entry.source_path,
81
+ line: entry.source_line,
82
+ column: 1,
83
+ severity: :info,
84
+ rule: "pre-eval.duplicate-declaration",
85
+ message: "pre-eval duplicate declaration: " \
86
+ "#{entry.class_name}##{entry.method_name} " \
87
+ "(#{entry.kind}) is declared again at " \
88
+ "#{winner.source_path}:#{winner.source_line}. " \
89
+ "The last-listed declaration wins; this entry is shadowed."
90
+ )
77
91
  end
78
92
  end
79
93
  private_class_method :duplicate_declaration_diagnostics
80
94
 
95
+ # Turns each {PendingAlias} into the entry it publishes, walking the bindings in configuration-then-source
96
+ # order as Ruby runs them. An alias binds to the body its old name has WHEN IT RUNS, so only a `def` or alias
97
+ # earlier in that order may supply its return type: `alias to_m to_modint` after `def to_modint` answers what
98
+ # `to_modint` does, while `alias_method :orig_succ, :succ` ahead of a patch's `def succ` is the method
99
+ # Integer already had. An alias whose old name nothing earlier binds records that name as `alias_of`, for the
100
+ # dispatcher to answer with the class's existing method (`alias old_plus +` on Integer), or with
101
+ # `Dynamic[top]` when nothing knows the name: the alias is still published, since a call to it is not the
102
+ # undefined method a dropped entry would report.
103
+ #
104
+ # Within one file the later binding of a name replaces the earlier, as at runtime: the alias-method-chain
105
+ # idiom (`def foo`, `alias_method :foo_without_x, :foo`, `alias_method :foo, :foo_with_x`) leaves `foo` the
106
+ # alias. Bindings of one name in DIFFERENT files are what `pre-eval.duplicate-declaration` reports, and the
107
+ # registry keeps the last of those, as Ruby does.
108
+ def resolve_aliases(collected)
109
+ bound = {}
110
+ per_file = collected.group_by(&:source_path).values.map do |items|
111
+ final = {}
112
+ items.each do |item|
113
+ entry = item.is_a?(PendingAlias) ? alias_entry(item, bound) : item
114
+ entry = entry.entry if entry.is_a?(GuardedDef)
115
+ key = [entry.class_name, entry.method_name, entry.kind]
116
+ next if item.is_a?(GuardedDef) && bound.key?(key)
117
+
118
+ bound[key] = entry
119
+ final.delete(key)
120
+ final[key] = entry
121
+ end
122
+ final.values
123
+ end
124
+ per_file.flatten(1)
125
+ end
126
+ private_class_method :resolve_aliases
127
+
128
+ def alias_entry(item, bound)
129
+ target = bound[[item.class_name, item.old_name, item.kind]]
130
+ ProjectPatchedMethods::Entry.new(
131
+ class_name: item.class_name, method_name: item.new_name, kind: item.kind,
132
+ source_path: item.source_path, source_line: item.source_line,
133
+ return_type: target&.return_type, alias_of: target ? target.alias_of : item.old_name
134
+ )
135
+ end
136
+ private_class_method :alias_entry
137
+
81
138
  def scan_file(path, entries, diagnostics, buffer = nil, census = Set.new)
82
139
  physical = buffer ? buffer.resolve(path) : path
83
140
  parse_result =
@@ -91,6 +148,7 @@ module Rigor
91
148
  return
92
149
  end
93
150
 
151
+ Thread.current[:rigor_guarded_defs] = DefinednessGuard.defs(parse_result.value)
94
152
  walk_node(parse_result.value, [], false, path, entries)
95
153
  # Issue #1367 — the file's `global.*` write facts: a patch file may alias a special or give a class the
96
154
  # method a setter asks for, as any project file may.
@@ -104,6 +162,8 @@ module Rigor
104
162
  "#{e.class}: #{e.message}. Pre-evaluation skipped for this file; " \
105
163
  "the rest of the run proceeds."
106
164
  )
165
+ ensure
166
+ Thread.current[:rigor_guarded_defs] = nil
107
167
  end
108
168
  private_class_method :scan_file
109
169
 
@@ -139,6 +199,13 @@ module Rigor
139
199
  descend_singleton_class(node, qualified_prefix, source_path, entries)
140
200
  when Prism::DefNode
141
201
  record_def_node(node, qualified_prefix, in_singleton_class, source_path, entries)
202
+ when Prism::AliasMethodNode
203
+ record_alias(node, Source::AliasNames.keyword_names(node), qualified_prefix, in_singleton_class,
204
+ source_path, entries)
205
+ when Prism::CallNode
206
+ record_alias(node, Source::AliasNames.alias_method_call_names(node), qualified_prefix,
207
+ in_singleton_class, source_path, entries)
208
+ walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
142
209
  else
143
210
  walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
144
211
  end
@@ -179,13 +246,28 @@ module Rigor
179
246
  kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
180
247
  line = node.location&.start_line || 1
181
248
  return_type = Analysis::DependencySourceInference::ReturnTypeHeuristic.extract(node)
182
- entries << ProjectPatchedMethods::Entry.new(
249
+ entry = ProjectPatchedMethods::Entry.new(
183
250
  class_name: class_name, method_name: node.name, kind: kind,
184
251
  source_path: source_path, source_line: line,
185
252
  return_type: return_type
186
253
  )
254
+ entries << (Thread.current[:rigor_guarded_defs]&.key?(node) ? GuardedDef.new(entry: entry) : entry)
187
255
  end
188
256
  private_class_method :record_def_node
257
+
258
+ # `names` is the alias's `[new_name, old_name]`, or nil for a computed name or a call that is not an alias.
259
+ # The side follows the body it is written in, as a `def`'s does: `alias` and `alias_method` inside
260
+ # `class << self` both bind on the singleton.
261
+ def record_alias(node, names, qualified_prefix, in_singleton_class, source_path, entries)
262
+ return if names.nil? || qualified_prefix.empty?
263
+
264
+ entries << PendingAlias.new(
265
+ class_name: qualified_prefix.join("::"), new_name: names.first, old_name: names.last,
266
+ kind: in_singleton_class ? :singleton : :instance,
267
+ source_path: source_path, source_line: node.location&.start_line || 1
268
+ )
269
+ end
270
+ private_class_method :record_alias
189
271
  end
190
272
  end
191
273
  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