rigortype 0.4.1 → 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 (182) 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 +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "rule_ids"
6
+ require_relative "../diagnostic"
7
+ require_relative "../../inference/version_guard"
8
+ require_relative "../../source/constant_path"
9
+
10
+ module Rigor
11
+ module Analysis
12
+ module CheckRules
13
+ # Issue #1692 (the ADR-47 WD5 amendment) — `call.deprecated-ruby2-keywords`: a call to a method of the
14
+ # `ruby2_keywords` family, which Ruby 4.1 deprecates (Feature #22205). `Module#ruby2_keywords`, top-level
15
+ # `ruby2_keywords` and `Proc#ruby2_keywords` are removed in Ruby 4.4, `Hash.ruby2_keywords_hash?` and
16
+ # `Hash.ruby2_keywords_hash` in Ruby 4.5.
17
+ #
18
+ # The rule runs only when `.rigor.yml` states the runtime: `target_ruby` set explicitly to 4.1 or later
19
+ # ({Configuration#stated_runtime_ruby}). On Ruby 4.0 the same calls are correct and silent, so the default
20
+ # `target_ruby` fires nothing. The file walk is paid only by a project that states such a target.
21
+ #
22
+ # A call counts only where it can reach the core method and nothing else:
23
+ #
24
+ # - `ruby2_keywords` with no receiver (or `self`), in a class or module body, a `class << self` body or a
25
+ # singleton method, where `self` is a Module. At the top level outside any block, it is the `main`
26
+ # method (a top-level `def` body included). Inside a top-level block, inside any `instance_eval` /
27
+ # `instance_exec` block, and in an instance method of a class or module, it is not judged.
28
+ # - `ruby2_keywords` on a receiver typed `Proc`.
29
+ # - `ruby2_keywords_hash?` / `ruby2_keywords_hash` on `Hash` or a class RBS knows as its subclass, or with
30
+ # no receiver where `self` is one.
31
+ # - The same names through `send` / `__send__` with a literal Symbol, which also reach the private
32
+ # `Module#ruby2_keywords` on an explicit Module receiver.
33
+ #
34
+ # A project or `pre_eval:` file that defines a method by the same name anywhere silences that name, since
35
+ # the call may then reach the project's method. A call in an arm of a version guard that cannot run on the
36
+ # stated Ruby, or whose guard cannot be decided, is not reported, nor are the statements after a guard
37
+ # that jumps away (`return if RUBY_VERSION < "2.7"`).
38
+ module RubyDeprecations
39
+ module_function
40
+
41
+ # The first Ruby that deprecates the family.
42
+ DEPRECATED_SINCE = "4.1"
43
+
44
+ MODULE_FORM = ["Module#ruby2_keywords", "4.4"].freeze
45
+ TOPLEVEL_FORM = ["top-level ruby2_keywords", "4.4"].freeze
46
+ PROC_FORM = ["Proc#ruby2_keywords", "4.4"].freeze
47
+ HASH_FORMS = {
48
+ ruby2_keywords_hash?: ["Hash.ruby2_keywords_hash?", "4.5"].freeze,
49
+ ruby2_keywords_hash: ["Hash.ruby2_keywords_hash", "4.5"].freeze
50
+ }.freeze
51
+ NAMES = (%i[ruby2_keywords] + HASH_FORMS.keys).freeze
52
+ SEND_NAMES = %i[send __send__].freeze
53
+ INSTANCE_EVAL_NAMES = %i[instance_eval instance_exec].freeze
54
+ private_constant :MODULE_FORM, :TOPLEVEL_FORM, :PROC_FORM, :HASH_FORMS, :NAMES, :SEND_NAMES,
55
+ :INSTANCE_EVAL_NAMES
56
+
57
+ # The walk's lexical context: whether a block hides `self`, and whether a version guard makes the node
58
+ # unreportable.
59
+ Context = Data.define(:in_block, :in_instance_eval, :guarded)
60
+ ROOT_CONTEXT = Context.new(in_block: false, in_instance_eval: false, guarded: false)
61
+ private_constant :Context, :ROOT_CONTEXT
62
+
63
+ # @param stated_ruby — {Configuration#stated_runtime_ruby}, or nil
64
+ # @return whether the rule runs at all for this target
65
+ def active?(stated_ruby)
66
+ return false if stated_ruby.nil? || !::Gem::Version.correct?(stated_ruby)
67
+
68
+ ::Gem::Version.new(stated_ruby) >= ::Gem::Version.new(DEPRECATED_SINCE)
69
+ end
70
+
71
+ def diagnostics(path, root, scope_index, stated_ruby)
72
+ return [] unless active?(stated_ruby)
73
+
74
+ guard_ruby = guard_version(stated_ruby)
75
+ results = []
76
+ shadowed = {}
77
+ walk(root, ROOT_CONTEXT, guard_ruby) do |call, context|
78
+ form = deprecated_form(call, context, scope_index, shadowed)
79
+ results << build(path, call, form, stated_ruby) if form
80
+ end
81
+ results
82
+ end
83
+
84
+ # `RUBY_VERSION` reads `"4.1.0"` on Ruby 4.1, so a stated `"4.1"` is padded before a guard compares it
85
+ # as a String.
86
+ def guard_version(stated_ruby)
87
+ segments = stated_ruby.split(".")
88
+ (segments + (["0"] * (3 - segments.size))).join(".")
89
+ end
90
+
91
+ def walk(node, context, guard_ruby, &)
92
+ # Nothing under a guarded node is reported, so the walk does not descend into it.
93
+ return if !node.is_a?(Prism::Node) || context.guarded
94
+
95
+ yield node, context if node.is_a?(Prism::CallNode) && candidate?(node)
96
+
97
+ jumped = false
98
+ node.rigor_each_child do |child|
99
+ child_context = child_context(node, child, context, guard_ruby)
100
+ child_context = child_context.with(guarded: true) if jumped
101
+ walk(child, child_context, guard_ruby, &)
102
+ jumped ||= node.is_a?(Prism::StatementsNode) && jumping_version_guard?(child)
103
+ end
104
+ end
105
+
106
+ def candidate?(call)
107
+ return true if NAMES.include?(call.name)
108
+
109
+ SEND_NAMES.include?(call.name) && NAMES.include?(send_target(call))
110
+ end
111
+
112
+ # The Symbol a `send` names as its first argument, or nil.
113
+ def send_target(call)
114
+ first = call.arguments&.arguments&.first
115
+ first.is_a?(Prism::SymbolNode) ? first.unescaped.to_sym : nil
116
+ end
117
+
118
+ def child_context(node, child, context, guard_ruby)
119
+ if node.is_a?(Prism::CallNode) && child.equal?(node.block) && child.is_a?(Prism::BlockNode)
120
+ instance_eval = context.in_instance_eval || INSTANCE_EVAL_NAMES.include?(node.name)
121
+ context = context.with(in_block: true, in_instance_eval: instance_eval)
122
+ end
123
+ return context if context.guarded
124
+
125
+ version_guard_live?(node, child, guard_ruby) ? context : context.with(guarded: true)
126
+ end
127
+
128
+ # Whether `child` stays reportable under `node`'s condition. An `if` / `unless` arm is, when its guard is
129
+ # decided on the stated Ruby and selects it. Any other branch of a conditional whose condition reads
130
+ # `RUBY_VERSION` is not, because the stated version is the lowest one the project runs on and an
131
+ # undecided guard may deselect the call on it.
132
+ def version_guard_live?(node, child, guard_ruby)
133
+ case node
134
+ when Prism::IfNode, Prism::UnlessNode
135
+ return true if child.equal?(node.predicate) || !reads_ruby_version?(node.predicate)
136
+
137
+ arm_live?(node, child, guard_ruby)
138
+ when Prism::CaseNode, Prism::CaseMatchNode, Prism::WhileNode, Prism::UntilNode
139
+ condition = node.predicate
140
+ condition.nil? || child.equal?(condition) || !reads_ruby_version?(condition)
141
+ when Prism::AndNode, Prism::OrNode
142
+ child.equal?(node.left) || !reads_ruby_version?(node.left)
143
+ else
144
+ true
145
+ end
146
+ end
147
+
148
+ def arm_live?(node, child, guard_ruby)
149
+ verdict = Inference::VersionGuard.verdict(node.predicate, stated_ruby: guard_ruby)
150
+ return false if verdict.nil?
151
+
152
+ then_live = verdict == (node.is_a?(Prism::IfNode) ? :truthy : :falsey)
153
+ child.equal?(node.statements) ? then_live : !then_live
154
+ end
155
+
156
+ # `return if RUBY_VERSION < "2.7"` (or `raise` / `next` / `break` under such a guard): the statements
157
+ # after it run only on the Rubies the guard lets through.
158
+ def jumping_version_guard?(node)
159
+ return false unless node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
160
+ return false unless reads_ruby_version?(node.predicate)
161
+
162
+ other_arm = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
163
+ contains_jump?(node.statements) || contains_jump?(other_arm)
164
+ end
165
+
166
+ JUMP_CALLS = %i[raise fail exit exit! abort].freeze
167
+ private_constant :JUMP_CALLS
168
+
169
+ def contains_jump?(node)
170
+ return false unless node.is_a?(Prism::Node)
171
+
172
+ case node
173
+ when Prism::ReturnNode, Prism::NextNode, Prism::BreakNode then return true
174
+ when Prism::CallNode then return true if node.receiver.nil? && JUMP_CALLS.include?(node.name)
175
+ end
176
+ found = false
177
+ node.rigor_each_child { |child| found ||= contains_jump?(child) }
178
+ found
179
+ end
180
+
181
+ def reads_ruby_version?(node)
182
+ return false unless node.is_a?(Prism::Node)
183
+ return true if (node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)) &&
184
+ Source::ConstantPath.qualified_name_or_nil(node) == "RUBY_VERSION"
185
+
186
+ found = false
187
+ node.rigor_each_child { |child| found ||= reads_ruby_version?(child) }
188
+ found
189
+ end
190
+
191
+ # @return `[display, removal]` for a call that reaches a deprecated method, or nil
192
+ def deprecated_form(call, context, scope_index, shadowed)
193
+ scope = scope_index[call]
194
+ return nil if scope.nil?
195
+
196
+ via_send = SEND_NAMES.include?(call.name)
197
+ name = via_send ? send_target(call) : call.name
198
+ shadowed[name] = project_defines?(scope, name) unless shadowed.key?(name)
199
+ return nil if shadowed[name]
200
+
201
+ receiver = call.receiver
202
+ if receiver.nil? || receiver.is_a?(Prism::SelfNode)
203
+ self_form(name, scope, context)
204
+ else
205
+ receiver_form(name, scope.type_of(receiver), scope, via_send)
206
+ end
207
+ end
208
+
209
+ def self_form(name, scope, context)
210
+ self_type = scope.self_type
211
+ if self_type.nil?
212
+ return nil if context.in_block || name != :ruby2_keywords
213
+
214
+ return TOPLEVEL_FORM
215
+ end
216
+ return nil if context.in_instance_eval || !self_type.is_a?(Type::Singleton)
217
+
218
+ name == :ruby2_keywords ? MODULE_FORM : hash_form(name, self_type, scope)
219
+ end
220
+
221
+ def receiver_form(name, type, scope, via_send)
222
+ if name == :ruby2_keywords
223
+ return PROC_FORM if type.is_a?(Type::Nominal) && class_at_or_below?(type.class_name, "Proc", scope)
224
+
225
+ # `Module#ruby2_keywords` is private: only `send` reaches it on an explicit receiver.
226
+ return via_send && type.is_a?(Type::Singleton) ? MODULE_FORM : nil
227
+ end
228
+ type.is_a?(Type::Singleton) ? hash_form(name, type, scope) : nil
229
+ end
230
+
231
+ def hash_form(name, singleton, scope)
232
+ return nil unless HASH_FORMS.key?(name)
233
+
234
+ class_at_or_below?(singleton.class_name, "Hash", scope) ? HASH_FORMS.fetch(name) : nil
235
+ end
236
+
237
+ def class_at_or_below?(class_name, ancestor, scope)
238
+ return true if class_name == ancestor
239
+ return false if scope.environment.nil?
240
+
241
+ %i[equal subclass].include?(Rigor::Reflection.class_ordering(class_name, ancestor, scope: scope))
242
+ end
243
+
244
+ # Whether a project or `pre_eval:` file defines a method called `name` anywhere — on any class, on either
245
+ # side, or at the top level. Asked once per name per file.
246
+ def project_defines?(scope, name)
247
+ return true if scope.discovered_methods.any? { |_owner, table| table.key?(name) }
248
+ return true if scope.discovered_def_nodes.any? { |_owner, table| table.key?(name) }
249
+
250
+ registry = scope.environment&.project_patched_methods
251
+ return false if registry.nil? || registry.empty?
252
+
253
+ registry.by_key.each_key.any? { |(_owner, method_name, _kind)| method_name == name }
254
+ end
255
+
256
+ def build(path, call, form, stated_ruby)
257
+ display, removal = form
258
+ tail = if ::Gem::Version.new(stated_ruby) >= ::Gem::Version.new(removal)
259
+ "was removed in Ruby #{removal}"
260
+ else
261
+ "is deprecated since Ruby 4.1 and will be removed in Ruby #{removal}"
262
+ end
263
+ Diagnostic.from_message_loc(
264
+ call,
265
+ rule: RULE_DEPRECATED_RUBY2_KEYWORDS,
266
+ path: path,
267
+ message: "`#{display}' #{tail} (target_ruby: #{stated_ruby.inspect})",
268
+ severity: :warning,
269
+ method_name: display
270
+ )
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
@@ -78,6 +78,9 @@ module Rigor
78
78
  # (`SpecialGlobalSetters`), not the global's RBS declaration.
79
79
  RULE_GLOBAL_WRITE_TYPE_MISMATCH = "global.write-type-mismatch"
80
80
  RULE_GLOBAL_READONLY_WRITE = "global.readonly-write"
81
+ # Issue #1692 (the ADR-47 WD5 amendment) — a call to the `ruby2_keywords` family Ruby 4.1 deprecates. Runs only
82
+ # under an explicit `target_ruby` of 4.1 or later; see {RubyDeprecations}.
83
+ RULE_DEPRECATED_RUBY2_KEYWORDS = "call.deprecated-ruby2-keywords"
81
84
 
82
85
  ALL_RULES = [
83
86
  RULE_UNDEFINED_METHOD,
@@ -87,6 +90,7 @@ module Rigor
87
90
  RULE_ARGUMENT_TYPE,
88
91
  RULE_NIL_RECEIVER,
89
92
  RULE_RAISE_NON_EXCEPTION,
93
+ RULE_DEPRECATED_RUBY2_KEYWORDS,
90
94
  RULE_DUMP_TYPE,
91
95
  RULE_ASSERT_TYPE,
92
96
  RULE_ALWAYS_RAISES,
@@ -290,8 +290,11 @@ module Rigor
290
290
  bucket[raw] = @scope.ancestor_name_candidates(owner, raw).find { |name| project_class?(name) }
291
291
  end
292
292
 
293
+ # A bucket of class marks alone (issue #1820: `x.singleton_class.prepend(Comparable)`) does not make a name
294
+ # the project's.
293
295
  def project_class?(name)
294
- @scope.known_user_class?(name) || @scope.discovered_parameter_envelopes.key?(name)
296
+ @scope.known_user_class?(name) ||
297
+ Scope::DiscoveryIndex.declares_surface?(@scope.discovered_parameter_envelopes[name])
295
298
  end
296
299
 
297
300
  # A module's methods reach the receiver as instance methods whichever side the receiver is on.
@@ -306,8 +309,13 @@ module Rigor
306
309
  level.externals.all? { |candidates| external_mixin_lacks_method?(candidates) }
307
310
  end
308
311
 
312
+ # Issue #1797 — a class whose body rewrites its method table in a way no literal names. A `refine` of the
313
+ # class (`ENVELOPE_REFINED_MARK`) is not one here: whether a refinement answers a call is decided per call
314
+ # site, name and `using` by `CheckRules#refined_redefinition_in_effect?`, which also declines where a
315
+ # refinement of a subclass of the receiver's class may answer. Counting the mark silenced every name on
316
+ # every subclass of a refined class, with or without a `using`.
309
317
  def dynamic_surface?(class_name)
310
- Scope::DiscoveryIndex.rewritten_surface?(@scope.parameter_envelopes_of(class_name))
318
+ @scope.parameter_envelopes_of(class_name).key?(Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
311
319
  end
312
320
 
313
321
  def project_patched?(class_name)
@@ -401,8 +409,12 @@ module Rigor
401
409
 
402
410
  def first_level(class_name) = chain_levels(arity_chain(class_name)).first.first
403
411
 
412
+ # ADR-46 — every class whose subclasses are enumerated records `subclass_of:<last segment>`, which a class
413
+ # declaration with that superclass appearing or vanishing fires (issue #1820): a new subclass with a
414
+ # different envelope re-checks the call.
404
415
  def each_subclass(class_name)
405
416
  children = children_by_parent
417
+ record_subclasses_read(class_name)
406
418
  queue = children.fetch(class_name.to_s, []).dup
407
419
  seen = {}
408
420
  until queue.empty?
@@ -410,11 +422,18 @@ module Rigor
410
422
  next if seen[subclass]
411
423
 
412
424
  seen[subclass] = true
425
+ record_subclasses_read(subclass)
413
426
  yield subclass
414
427
  queue.concat(children.fetch(subclass, []))
415
428
  end
416
429
  end
417
430
 
431
+ def record_subclasses_read(class_name)
432
+ return unless Analysis::DependencyRecorder.active?
433
+
434
+ Analysis::DependencyRecorder.read_last_segment(:subclass_of, class_name.to_s)
435
+ end
436
+
418
437
  def children_by_parent
419
438
  @scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
420
439
  parent = resolve(child, raw_parent)