rigortype 0.4.4 → 0.4.6

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 (233) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +100 -18
  4. data/docs/manual/02-cli-reference.md +85 -16
  5. data/docs/manual/03-configuration.md +12 -9
  6. data/docs/manual/04-diagnostics.md +17 -6
  7. data/docs/manual/05-inspecting-types.md +2 -2
  8. data/docs/manual/06-baseline.md +1 -1
  9. data/docs/manual/07-plugins.md +2 -1
  10. data/docs/manual/08-skills.md +7 -0
  11. data/docs/manual/10-mcp-server.md +17 -2
  12. data/docs/manual/12-caching.md +8 -1
  13. data/docs/manual/18-removing-dead-code.md +18 -7
  14. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  15. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  16. data/docs/manual/plugins/rigor-devise.md +15 -0
  17. data/docs/manual/plugins/rigor-rspec.md +102 -1
  18. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  19. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  20. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  21. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  22. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
  23. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
  24. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  25. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  26. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  27. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  28. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  29. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
  30. data/lib/rigor/analysis/check_rules.rb +319 -21
  31. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/edit_replay.rb +67 -0
  34. data/lib/rigor/analysis/incremental.rb +86 -0
  35. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  36. data/lib/rigor/analysis/incremental_session.rb +336 -46
  37. data/lib/rigor/analysis/path_expansion.rb +44 -1
  38. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  39. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  40. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  41. data/lib/rigor/analysis/rule_catalog.rb +22 -5
  42. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  43. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  44. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  48. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  49. data/lib/rigor/analysis/runner.rb +222 -111
  50. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  51. data/lib/rigor/analysis/worker_session.rb +24 -1
  52. data/lib/rigor/cache/descriptor.rb +40 -7
  53. data/lib/rigor/cache/engine_source.rb +3 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +49 -8
  55. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  56. data/lib/rigor/cache/store.rb +4 -3
  57. data/lib/rigor/cli/check_command.rb +93 -4
  58. data/lib/rigor/cli/command.rb +3 -1
  59. data/lib/rigor/cli/diff_command.rb +2 -1
  60. data/lib/rigor/cli/docs_command.rb +3 -2
  61. data/lib/rigor/cli/doctor_command.rb +1 -1
  62. data/lib/rigor/cli/explain_command.rb +3 -2
  63. data/lib/rigor/cli/lens_command.rb +5 -5
  64. data/lib/rigor/cli/mcp_command.rb +2 -1
  65. data/lib/rigor/cli/options.rb +20 -3
  66. data/lib/rigor/cli/plugin_command.rb +2 -1
  67. data/lib/rigor/cli/plugins_command.rb +7 -1
  68. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  69. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  70. data/lib/rigor/cli/sig_gen_command.rb +73 -13
  71. data/lib/rigor/cli/skill_command.rb +2 -1
  72. data/lib/rigor/cli/skill_describe.rb +3 -2
  73. data/lib/rigor/cli/type_of_command.rb +2 -1
  74. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  75. data/lib/rigor/cli/unused_command.rb +10 -7
  76. data/lib/rigor/cli.rb +3 -2
  77. data/lib/rigor/config_audit.rb +72 -16
  78. data/lib/rigor/configuration/severity_profile.rb +20 -2
  79. data/lib/rigor/configuration.rb +43 -7
  80. data/lib/rigor/effects/signature_sources.rb +3 -1
  81. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  82. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  83. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  84. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  85. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  86. data/lib/rigor/environment/rbs_loader.rb +136 -7
  87. data/lib/rigor/environment/required_features.rb +136 -29
  88. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  89. data/lib/rigor/environment.rb +4 -2
  90. data/lib/rigor/inference/block_call_timing.rb +7 -1
  91. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  92. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  93. data/lib/rigor/inference/captured_locals.rb +44 -13
  94. data/lib/rigor/inference/clause_widening.rb +90 -0
  95. data/lib/rigor/inference/deep_erasure.rb +138 -0
  96. data/lib/rigor/inference/def_return_typer.rb +159 -9
  97. data/lib/rigor/inference/definer_resolution.rb +101 -20
  98. data/lib/rigor/inference/error_info.rb +17 -3
  99. data/lib/rigor/inference/expression_typer.rb +97 -18
  100. data/lib/rigor/inference/global_write_census.rb +36 -1
  101. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  102. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  103. data/lib/rigor/inference/loop_value_record.rb +53 -0
  104. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  105. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  106. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
  107. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  108. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  109. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  110. data/lib/rigor/inference/method_dispatcher.rb +113 -19
  111. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  112. data/lib/rigor/inference/module_function_state.rb +149 -42
  113. data/lib/rigor/inference/mutation_widening.rb +5 -2
  114. data/lib/rigor/inference/narrowing.rb +116 -11
  115. data/lib/rigor/inference/object_mixins.rb +13 -1
  116. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  117. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  118. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  119. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  120. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  121. data/lib/rigor/inference/project_seed_tables.rb +121 -0
  122. data/lib/rigor/inference/project_yielder.rb +128 -0
  123. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  124. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  125. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  126. data/lib/rigor/inference/return_barrier.rb +16 -0
  127. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  128. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  129. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  130. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  131. data/lib/rigor/inference/scope_indexer.rb +1144 -197
  132. data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
  133. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  134. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  135. data/lib/rigor/inference/statement_evaluator.rb +1088 -213
  136. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  137. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  138. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  139. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  140. data/lib/rigor/language_server/buffer_table.rb +6 -0
  141. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  142. data/lib/rigor/language_server/project_context.rb +59 -3
  143. data/lib/rigor/language_server/server.rb +5 -3
  144. data/lib/rigor/lens/contract_reader.rb +45 -2
  145. data/lib/rigor/lens/document.rb +2 -1
  146. data/lib/rigor/lens/file_reader.rb +12 -2
  147. data/lib/rigor/lens/members.rb +16 -2
  148. data/lib/rigor/lens/run.rb +34 -7
  149. data/lib/rigor/mcp/server.rb +11 -3
  150. data/lib/rigor/plugin/base.rb +29 -13
  151. data/lib/rigor/plugin/io_boundary.rb +42 -6
  152. data/lib/rigor/plugin/isolation.rb +4 -3
  153. data/lib/rigor/plugin/manifest.rb +29 -3
  154. data/lib/rigor/plugin/registry.rb +38 -0
  155. data/lib/rigor/protection/discovery_seed.rb +64 -36
  156. data/lib/rigor/protection/mutation_cache.rb +8 -2
  157. data/lib/rigor/rooted_glob.rb +36 -0
  158. data/lib/rigor/scope/discovery_index.rb +5 -0
  159. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  160. data/lib/rigor/scope/resolution_chain.rb +40 -5
  161. data/lib/rigor/scope.rb +59 -7
  162. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  163. data/lib/rigor/sig_gen/classification.rb +4 -1
  164. data/lib/rigor/sig_gen/generator.rb +465 -49
  165. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  166. data/lib/rigor/sig_gen/observation_collector.rb +141 -12
  167. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  168. data/lib/rigor/sig_gen/renderer.rb +16 -155
  169. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  170. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  171. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  172. data/lib/rigor/sig_gen/write_result.rb +3 -1
  173. data/lib/rigor/sig_gen/writer.rb +5 -3
  174. data/lib/rigor/signature_path_audit.rb +4 -2
  175. data/lib/rigor/type/combinator.rb +4 -3
  176. data/lib/rigor/type/constant.rb +54 -3
  177. data/lib/rigor/type/hash_shape.rb +14 -9
  178. data/lib/rigor/type/struct_instance.rb +3 -2
  179. data/lib/rigor/version.rb +1 -1
  180. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  181. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
  182. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  183. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  184. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  185. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  186. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  187. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  188. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  189. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  190. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  191. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  192. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  193. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  194. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  195. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  196. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  197. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  198. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  199. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  200. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  201. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  202. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  203. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  204. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  205. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  206. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  207. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  208. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  209. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  210. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  211. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  212. data/sig/rigor/analysis/baseline.rbs +3 -2
  213. data/sig/rigor/effects/file_collection.rbs +5 -6
  214. data/sig/rigor/effects/registry.rbs +8 -6
  215. data/sig/rigor/effects/summary.rbs +6 -5
  216. data/sig/rigor/environment.rbs +5 -3
  217. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  218. data/sig/rigor/inference.rbs +6 -0
  219. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  220. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  221. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  222. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  223. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  224. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  225. data/sig/rigor/plugin/manifest.rbs +16 -18
  226. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  227. data/sig/rigor/plugin/registry.rbs +1 -1
  228. data/sig/rigor/rooted_glob.rbs +7 -0
  229. data/sig/rigor/scope.rbs +34 -52
  230. data/sig/rigor/type.rbs +0 -6
  231. data/skills/rigor-doctor/references/01-checks.md +23 -11
  232. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  233. metadata +32 -1
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Analysis
7
+ module CheckRules
8
+ module RebindingBlockCallees
9
+ # Which method a block call reaches, and on what receiver: `callee_may_rebind?` reads the callee name, a
10
+ # callable receiver, and whether Ruby can reach `method_missing` from here.
11
+ module Dispatch
12
+ # Issue #2186 — a block given to `call` / `bind_call` (or `yield` / `===` / `[]`) on a literal `method(:x)`
13
+ # chain goes to the method it names: `Ext.method(:me).call { … }` is `Ext.me { … }`. On a receiver typed as
14
+ # one of CALLABLE_CLASSES it goes to a callable Rigor cannot follow; on any other receiver it is a call by
15
+ # that name.
16
+ METHOD_OBJECT_CALLS = %i[call yield === [] bind_call].freeze
17
+ METHOD_READERS = %i[method public_method singleton_method instance_method public_instance_method].freeze
18
+ # The readers whose receiver is the object the named method runs on.
19
+ BOUND_METHOD_READERS = %i[method public_method singleton_method].freeze
20
+ METHOD_WRAPPERS = %i[bind unbind to_proc curry].freeze
21
+ CALLABLE_CLASSES = %w[Method UnboundMethod Proc].freeze
22
+ private_constant :METHOD_READERS, :BOUND_METHOD_READERS, :METHOD_WRAPPERS, :CALLABLE_CLASSES
23
+
24
+ module_function
25
+
26
+ # `[name, receiver]` of the method the block reaches: the call's own name, the literal name a `send` family
27
+ # call dispatches to, or the method a literal `method(:name)` chain names. The receiver is a node, `:implicit`
28
+ # for a receiverless call, or nil where Rigor cannot say (an `instance_method` chain). The name is nil when a
29
+ # `send` names its method with anything but a valid literal.
30
+ def dispatch(call)
31
+ reader = method_reader(call.receiver) if METHOD_OBJECT_CALLS.include?(call.name)
32
+ if reader
33
+ receiver = reader.receiver || :implicit if BOUND_METHOD_READERS.include?(reader.name)
34
+ return [RebindingBlockCallees.literal_name(reader.arguments&.arguments&.first), receiver]
35
+ end
36
+ [RebindingBlockCallees.send_target(call), call.receiver || :implicit]
37
+ end
38
+
39
+ def method_reader(receiver)
40
+ receiver = receiver.receiver while wrapper?(receiver)
41
+ receiver if receiver.is_a?(Prism::CallNode) && METHOD_READERS.include?(receiver.name)
42
+ end
43
+
44
+ # Whether `receiver` may be a Method, UnboundMethod or Proc, whose `call` runs a callable Rigor cannot follow.
45
+ def callable?(receiver, scope, scope_index)
46
+ return false unless receiver.is_a?(Prism::Node)
47
+
48
+ type = receiver_type(receiver, scope, scope_index)
49
+ members = type.is_a?(Type::Union) ? type.members : [type]
50
+ members.any? do |member|
51
+ member.is_a?(Type::BoundMethod) ||
52
+ (member.is_a?(Type::Nominal) && CALLABLE_CLASSES.include?(member.class_name.to_s))
53
+ end
54
+ end
55
+
56
+ def wrapper?(node) = node.is_a?(Prism::CallNode) && METHOD_WRAPPERS.include?(node.name)
57
+
58
+ # A `call` (or the like) on a receiver that may be a Method, UnboundMethod or Proc.
59
+ def callable_call?(name, receiver, scope, scope_index)
60
+ METHOD_OBJECT_CALLS.include?(name) && callable?(receiver, scope, scope_index)
61
+ end
62
+
63
+ def receiver_type(receiver, scope, scope_index)
64
+ (scope_index&.[](receiver) || scope).type_of(receiver)
65
+ rescue StandardError
66
+ nil
67
+ end
68
+
69
+ # Whether the receiver's type names a class whose RBS or project table gives it `name`, on every member of a
70
+ # union. A receiverless call at the top level asks `Object`, `main`'s class. Any other type cannot say.
71
+ def resolves_on_receiver?(receiver, name, scope, scope_index)
72
+ return scope.toplevel? && class_defines?("Object", :instance, name, scope) if receiver == :implicit
73
+ return false if receiver.nil?
74
+
75
+ type = receiver_type(receiver, scope, scope_index)
76
+ !type.nil? && type_resolves?(type, name, scope)
77
+ rescue StandardError
78
+ false
79
+ end
80
+
81
+ def type_resolves?(type, name, scope)
82
+ case type
83
+ when Type::Union then type.members.all? { |member| type_resolves?(member, name, scope) }
84
+ when Type::Singleton then class_defines?(type.class_name, :singleton, name, scope)
85
+ else
86
+ class_name = receiver_class_name(type)
87
+ !class_name.nil? && class_defines?(class_name, :instance, name, scope)
88
+ end
89
+ end
90
+
91
+ def receiver_class_name(type)
92
+ case type
93
+ when Type::Nominal, Type::StructInstance, Type::DataInstance then type.class_name&.to_s
94
+ when Type::Tuple then "Array"
95
+ when Type::HashShape then "Hash"
96
+ when Type::Constant then type.value.class.name
97
+ end
98
+ end
99
+
100
+ def class_defines?(class_name, side, name, scope)
101
+ rbs = if side == :singleton
102
+ Rigor::Reflection.singleton_method_definition(class_name, name, scope: scope)
103
+ else
104
+ Rigor::Reflection.instance_method_definition(class_name, name, scope: scope)
105
+ end
106
+ !rbs.nil? || scope.discovered_method?(class_name, name, side)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,394 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../dependency_recorder"
6
+ require_relative "../../inference/scope_indexer"
7
+
8
+ module Rigor
9
+ module Analysis
10
+ module CheckRules
11
+ # Issue #1817 — `call.unresolved-toplevel` declines inside a block handed to a project method that may run it
12
+ # under another `self`. `def self.define(&blk) = module_eval(&blk)` makes `Ext.define { refine(String) { … } }`
13
+ # call `refine` on `Ext`, not on `main`, so the call is not a top-level one even though the block sits at the
14
+ # top of the file.
15
+ #
16
+ # The reading is by method NAME across every project definition, never by the receiver's type: any project
17
+ # `def` of the block's callee name that may hand the block to something else declines the rule. That
18
+ # over-reads (an unrelated class's same-named method declines too), which only loses a report; resolving the
19
+ # receiver would read one class and could miss the definition Ruby actually dispatches to, which reports on
20
+ # correct code. A definition "may hand the block on" unless every use of it is provably a plain call:
21
+ #
22
+ # - its body only `yield`s, or reads the block parameter only as the receiver of `call` / `()` / `[]` /
23
+ # `yield` / `===` / `arity` / `lambda?` / `parameters` / `nil?` / `!`, or as an `if` / `unless` / `while` /
24
+ # `until` predicate or the left side of `&&` — each of which runs the block with its own `self` or not at
25
+ # all;
26
+ # - any other read (`module_eval(&blk)`, `other(&blk)`, `@blk = blk`, `return blk`), an anonymous `&` or a
27
+ # `...` forwarded anywhere, a `super` that passes the block on implicitly, or a `binding` / `eval` that
28
+ # could reach it, may rebind `self`. Where in the body it sits does not matter: an eval under an `if` may
29
+ # run.
30
+ #
31
+ # A callee name the project gives a method without a readable `def` (`attr_*`, `alias_method`,
32
+ # `define_method`, `define_singleton_method` on any receiver, a `def` written inside a block), or patches
33
+ # through `pre_eval:`, cannot be read and declines too. So does a `send` / `__send__` / `public_send` whose
34
+ # method name is not a literal. Issue #2184 — a project `method_missing` that may hand its block on answers
35
+ # for every name, since Ruby reaches it for any name the receiver lacks; and the block of a `define_method` /
36
+ # `define_singleton_method` call is a method body, run under whatever receives the method. A callee no project
37
+ # file defines keeps the rule's old answer otherwise: Rigor reads only the project's own bodies here.
38
+ module RebindingBlockCallees
39
+ SEND_CALLS = %i[send __send__ public_send].freeze
40
+ # Reading the block through these runs it with its own `self`, or does not run it.
41
+ PLAIN_PROC_CALLS = %i[call yield [] === ! arity lambda? parameters nil?].freeze
42
+ # Calls, on any receiver, that can reach a local by name: a `Binding`, a string `eval`, and the string forms of
43
+ # the `*_eval` family (with an argument), whose code runs in the caller's scope.
44
+ BINDING_CALLS = %i[binding eval local_variable_get].freeze
45
+ STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].freeze
46
+ private_constant :STRING_EVAL_CALLS
47
+ PREDICATE_PARENTS = [Prism::IfNode, Prism::UnlessNode, Prism::WhileNode, Prism::UntilNode].freeze
48
+ # `proc { … }`, `lambda { … }` and `Proc.new { … }` passed with `&` are the block of the call they go to.
49
+ PROC_LITERAL_CALLS = %i[proc lambda].freeze
50
+ # Core methods that run their block under another `self`: as a method body, or under the receiver (#2186:
51
+ # also when the block is a proc passed through a local, `Ext.instance_exec(&blk)`).
52
+ REBINDING_CORE_CALLS = %i[
53
+ define_method define_singleton_method
54
+ instance_eval instance_exec class_eval class_exec module_eval module_exec
55
+ ].freeze
56
+ private_constant :REBINDING_CORE_CALLS
57
+ private_constant :SEND_CALLS, :PLAIN_PROC_CALLS, :BINDING_CALLS, :PREDICATE_PARENTS, :PROC_LITERAL_CALLS
58
+
59
+ # The calls in one file that pass a literal block, each with the offsets that block's body spans, plus the
60
+ # file's memo of callee answers. Built on the first ask, so a file whose would-fire calls all decline earlier
61
+ # never walks. One file is one ADR-46 consumer, so an answer's dependency edges, recorded on its first ask,
62
+ # hold for every later call in the file that reuses it.
63
+ #
64
+ # Issue #2186 — a proc literal that is a value rather than a call's block (`blk = proc { … }`) adds a row for
65
+ # each call that receives it as `&blk`, when every other read of that local only calls or tests it. A literal
66
+ # called in place (`proc { … }.call`) runs with its own `self` and adds none. Any other proc value (stored,
67
+ # passed positionally, read another way, or in a method (or the code around it) that may read locals by name
68
+ # through `binding` / `eval` / `local_variable_get` or a computed `send`) cannot be followed to the call that
69
+ # runs it, so its row is {UNKNOWN}, which declines. A heredoc that opens inside a block adds a row over its
70
+ # body: Prism puts the body after the block's closing brace, but Ruby runs its interpolation inside the block.
71
+ class Sites
72
+ # The target of a row whose block Rigor cannot follow to the call that runs it.
73
+ UNKNOWN = :unknown
74
+
75
+ attr_reader :answers
76
+
77
+ def initialize(root)
78
+ @root = root
79
+ @rows = nil
80
+ @answers = {}
81
+ @def_rows = nil
82
+ end
83
+
84
+ # The block-passing calls (or {UNKNOWN}) whose block contains `node`. Rows are sorted by start, so the scan
85
+ # stops at the first block that starts after `node`.
86
+ def enclosing(node)
87
+ build
88
+ start = node.location.start_offset
89
+ stop = node.location.end_offset
90
+ found = []
91
+ @rows.each do |from, to, call|
92
+ break if from > start
93
+
94
+ found << call if stop <= to
95
+ end
96
+ found
97
+ end
98
+
99
+ # `{name => {[owner, side] => rows}}` over the project's named `def` rows, built once per file.
100
+ def def_rows(scope)
101
+ @def_rows ||= RebindingBlockCallees.def_row_counts(scope)
102
+ end
103
+
104
+ private
105
+
106
+ def build
107
+ return if @rows
108
+
109
+ @rows = []
110
+ @bodies = []
111
+ @heredocs = []
112
+ @proc_values = []
113
+ @local_reads = Hash.new { |hash, name| hash[name] = [] }
114
+ @block_passes = Hash.new { |hash, name| hash[name] = [] }
115
+ @scopes_reading_locals = Set.new.compare_by_identity
116
+ walk unless @root.nil?
117
+ # A block call chained on another (`m { }.then { }`) is met after the call whose block starts later.
118
+ @rows.sort_by!(&:first)
119
+ end
120
+
121
+ def walk
122
+ collect(@root, nil, @root)
123
+ @proc_values.each do |body, parent, local_scope|
124
+ value_targets(parent, local_scope).each { |target| @bodies << [body, target] }
125
+ end
126
+ @heredocs.sort_by!(&:first)
127
+ @bodies.each { |body, target| add_rows(body, target) }
128
+ end
129
+
130
+ def collect(node, parent, local_scope)
131
+ heredoc = RebindingBlockCallees.heredoc_range(node)
132
+ @heredocs << heredoc if heredoc
133
+ note(node, parent, local_scope)
134
+ # A `def` opens a new local-variable scope; blocks and lambdas share the enclosing one. A class body is
135
+ # read as part of the scope around it, which only widens the set a `binding` there may reach.
136
+ inner = node.is_a?(Prism::DefNode) ? node : local_scope
137
+ node.compact_child_nodes.each { |child| collect(child, node, inner) }
138
+ end
139
+
140
+ def note(node, parent, local_scope)
141
+ case node
142
+ when Prism::CallNode then note_call(node, parent, local_scope)
143
+ when Prism::LambdaNode then note_value(node, node, parent, local_scope)
144
+ when Prism::LocalVariableReadNode
145
+ @local_reads[node.name] << [node, parent] unless parent.is_a?(Prism::BlockArgumentNode)
146
+ end
147
+ end
148
+
149
+ def note_call(node, parent, local_scope)
150
+ block = node.block
151
+ body = RebindingBlockCallees.block_body(block)
152
+ @bodies << [body, node] if body
153
+ if block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::LocalVariableReadNode)
154
+ @block_passes[block.expression.name] << node
155
+ end
156
+ value = RebindingBlockCallees.proc_literal(node)
157
+ note_value(node, value, parent, local_scope) if value
158
+ target = RebindingBlockCallees.send_target(node)
159
+ @scopes_reading_locals << local_scope if target.nil? || BINDING_CALLS.include?(target)
160
+ end
161
+
162
+ # `literal` is the proc expression and `body` its block. Passed with `&`, it is that call's block.
163
+ def note_value(literal, body, parent, local_scope)
164
+ return if parent.is_a?(Prism::BlockArgumentNode) || RebindingBlockCallees.plain_read?(literal, parent)
165
+
166
+ @proc_values << [body, parent, local_scope]
167
+ end
168
+
169
+ # The calls a proc value assigned to a local reaches as `&local`, or {UNKNOWN}.
170
+ def value_targets(parent, local_scope)
171
+ return [UNKNOWN] if @scopes_reading_locals.include?(local_scope)
172
+ return [UNKNOWN] unless parent.is_a?(Prism::LocalVariableWriteNode)
173
+
174
+ name = parent.name
175
+ followed = @local_reads[name].all? do |read, read_parent|
176
+ RebindingBlockCallees.plain_read?(read, read_parent)
177
+ end
178
+ followed ? @block_passes[name] : [UNKNOWN]
179
+ end
180
+
181
+ # The block's own row, plus one over the body of each heredoc that opens inside it.
182
+ def add_rows(body, target)
183
+ from = body.location.start_offset
184
+ to = body.location.end_offset
185
+ @rows << [from, to, target]
186
+ index = @heredocs.bsearch_index { |(opening, _start, _stop)| opening >= from }
187
+ return if index.nil?
188
+
189
+ while index < @heredocs.size && @heredocs[index].first < to
190
+ _opening, start, stop = @heredocs[index]
191
+ @rows << [start, stop, target]
192
+ index += 1
193
+ end
194
+ end
195
+ end
196
+
197
+ module_function
198
+
199
+ # Does `call_node` sit inside a block that some project definition of its callee may run under another `self`?
200
+ def inside_rebound_block?(call_node, scope, sites, scope_index = nil)
201
+ sites.enclosing(call_node).any? do |block_call|
202
+ block_call == Sites::UNKNOWN || callee_may_rebind?(block_call, scope, sites, scope_index)
203
+ end
204
+ end
205
+
206
+ # `[opening, body start, end]` offsets of a heredoc `node` opens, or nil.
207
+ def heredoc_range(node)
208
+ opening = node.opening_loc if node.respond_to?(:opening_loc)
209
+ closing = node.closing_loc if node.respond_to?(:closing_loc)
210
+ return nil unless opening&.slice&.start_with?("<<") && closing
211
+
212
+ start = case node
213
+ when Prism::StringNode, Prism::XStringNode then node.content_loc.start_offset
214
+ when Prism::InterpolatedStringNode, Prism::InterpolatedXStringNode
215
+ node.parts.first&.location&.start_offset
216
+ end
217
+ [opening.start_offset, start || closing.start_offset, closing.end_offset]
218
+ end
219
+
220
+ # The node whose span is the block `call` passes: its literal block, or a lambda / proc literal passed with `&`.
221
+ def block_body(block)
222
+ case block
223
+ when Prism::BlockNode then block
224
+ when Prism::BlockArgumentNode then proc_literal(block.expression)
225
+ end
226
+ end
227
+
228
+ def proc_literal(node)
229
+ case node
230
+ when Prism::LambdaNode then node
231
+ when Prism::CallNode
232
+ node.block if node.block.is_a?(Prism::BlockNode) && proc_literal_call?(node)
233
+ end
234
+ end
235
+
236
+ def proc_literal_call?(node)
237
+ return PROC_LITERAL_CALLS.include?(node.name) if node.receiver.nil?
238
+
239
+ node.name == :new && node.receiver.is_a?(Prism::ConstantReadNode) && node.receiver.name == :Proc
240
+ end
241
+
242
+ def callee_may_rebind?(block_call, scope, sites, scope_index = nil)
243
+ name, receiver = Dispatch.dispatch(block_call)
244
+ return true if name.nil? || REBINDING_CORE_CALLS.include?(name)
245
+ return true if Dispatch.callable_call?(name, receiver, scope, scope_index)
246
+ return true if answer(name, scope, sites)
247
+ # Ruby reaches `method_missing` only for a name the receiver lacks.
248
+ return false if Dispatch.resolves_on_receiver?(receiver, name, scope, scope_index)
249
+
250
+ answer(:method_missing, scope, sites)
251
+ end
252
+
253
+ def answer(name, scope, sites)
254
+ answers = sites.answers
255
+ return answers[name] if answers.key?(name)
256
+
257
+ answers[name] = project_may_rebind?(name, scope, sites)
258
+ end
259
+
260
+ # The call's own name, or the literal name a `send` family call dispatches to (nil when it is not one).
261
+ def send_target(call)
262
+ return call.name unless SEND_CALLS.include?(call.name)
263
+
264
+ literal_name(call.arguments&.arguments&.first)
265
+ end
266
+
267
+ def literal_name(first)
268
+ case first
269
+ when Prism::SymbolNode, Prism::StringNode
270
+ text = first.unescaped
271
+ text.to_sym if text.valid_encoding?
272
+ end
273
+ end
274
+
275
+ # Reads every project table for `name`, so it records a `defines:<name>` edge whichever way it answers (a file
276
+ # that starts or stops defining the name re-checks this one), plus each read body's own method edge through the
277
+ # scope accessors (an edit of that body re-checks this one).
278
+ def project_may_rebind?(name, scope, sites)
279
+ DependencyRecorder.read_missing(:defines, name) if DependencyRecorder.active?
280
+ return true if patched_anywhere?(name, scope)
281
+
282
+ slots = Set.new
283
+ readers = {
284
+ [scope.discovered_def_nodes, :instance] => ->(owner) { instance_def(scope, owner, name) },
285
+ [scope.discovered_singleton_def_nodes, :singleton] => ->(owner) { scope.singleton_def_for(owner, name) }
286
+ }
287
+ readers.each do |(tables, side), reader|
288
+ tables.each do |owner, table|
289
+ next unless table.key?(name)
290
+
291
+ slots << [owner, side]
292
+ node = reader.call(owner)
293
+ return true if !node.is_a?(Prism::DefNode) || def_may_rebind?(node)
294
+ end
295
+ end
296
+ unread_definition?(sites.def_rows(scope)[name], slots) || bodiless_definition?(name, scope)
297
+ end
298
+
299
+ def instance_def(scope, owner, name)
300
+ return scope.top_level_def_for(name) if owner == Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
301
+
302
+ scope.user_def_for(owner, name)
303
+ end
304
+
305
+ # A def table keeps one body per class and side, so a second `def` of the name for the same slot (in a
306
+ # sibling file, whose order is not load order, or in the other arm of an `if`) is one the tables never
307
+ # showed, and so is a `def` whose class and side the tables hold no slot for. Every `def` outside a deferred
308
+ # body has a named row in the deferred-range table; a `module_function` row stands for both sides. A row with
309
+ # no side (a `def` inside a block, a `define_singleton_method` name) counts under a nil side, which no slot has.
310
+ def unread_definition?(rows, slots)
311
+ return false if rows.nil?
312
+
313
+ rows.any? { |slot, count| count > 1 || !slots.include?(slot) }
314
+ end
315
+
316
+ def def_row_counts(scope)
317
+ counts = Hash.new { |hash, name| hash[name] = Hash.new(0) }
318
+ scope.discovered_deferred_ranges.each_value do |list|
319
+ list.each do |(_start, _stop, name, kind, owner)|
320
+ next if name.nil?
321
+
322
+ key = owner || Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
323
+ sides = kind == :both ? %i[instance singleton] : [kind]
324
+ sides.each { |side| counts[name][[key, side]] += 1 }
325
+ end
326
+ end
327
+ counts.default_proc = nil
328
+ counts
329
+ end
330
+
331
+ # A name some class gives a method with no `def` body Rigor holds (`attr_*`, `alias_method`, `define_method`).
332
+ def bodiless_definition?(name, scope)
333
+ scope.discovered_methods.any? do |owner, table|
334
+ table.key?(name) &&
335
+ !scope.discovered_def_nodes[owner]&.key?(name) &&
336
+ !scope.discovered_singleton_def_nodes[owner]&.key?(name)
337
+ end
338
+ end
339
+
340
+ def patched_anywhere?(name, scope)
341
+ patched = scope.environment&.project_patched_methods
342
+ !patched.nil? && patched.by_key.any? { |(_owner, patched_name, _kind), _entry| patched_name == name }
343
+ end
344
+
345
+ def def_may_rebind?(def_node)
346
+ return false if def_node.body.nil?
347
+
348
+ block = def_node.parameters&.block
349
+ escapes?(def_node.body, nil, block&.name)
350
+ end
351
+
352
+ # `block_name` is the def's named `&` parameter, or nil.
353
+ def escapes?(node, parent, block_name)
354
+ return true if hands_block_on?(node, parent, block_name)
355
+
356
+ node.compact_child_nodes.any? { |child| escapes?(child, node, block_name) }
357
+ end
358
+
359
+ def hands_block_on?(node, parent, block_name)
360
+ case node
361
+ when Prism::ForwardingSuperNode then !node.block.is_a?(Prism::BlockNode)
362
+ when Prism::SuperNode then node.block.nil?
363
+ when Prism::BlockArgumentNode then node.expression.nil?
364
+ when Prism::ForwardingArgumentsNode then true
365
+ when Prism::LocalVariableReadNode then node.name == block_name && !plain_read?(node, parent)
366
+ when Prism::CallNode then reaches_locals?(node)
367
+ else false
368
+ end
369
+ end
370
+
371
+ # Issue #2186 — a call that can reach the block local by name, on any receiver and through `send`:
372
+ # `Kernel.eval("module_eval(&b)")`, `send(:binding).local_variable_get(:b)`, `instance_eval("module_eval(&b)")`.
373
+ # A `send` whose method name is not a literal may be any of them.
374
+ def reaches_locals?(call)
375
+ name = send_target(call)
376
+ return true if name.nil? || BINDING_CALLS.include?(name)
377
+
378
+ STRING_EVAL_CALLS.include?(name) && !call.arguments.nil?
379
+ end
380
+
381
+ def plain_read?(node, parent)
382
+ case parent
383
+ when Prism::CallNode then parent.receiver.equal?(node) && PLAIN_PROC_CALLS.include?(parent.name)
384
+ when Prism::AndNode then parent.left.equal?(node)
385
+ when *PREDICATE_PARENTS then parent.predicate.equal?(node)
386
+ else false
387
+ end
388
+ end
389
+ end
390
+ end
391
+ end
392
+ end
393
+
394
+ require_relative "rebinding_block_callees/dispatch"
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "rule_ids"
6
+ require_relative "../dependency_recorder"
6
7
  require_relative "../diagnostic"
7
8
  require_relative "../../inference/version_guard"
8
9
  require_relative "../../source/constant_path"
@@ -243,7 +244,13 @@ module Rigor
243
244
 
244
245
  # Whether a project or `pre_eval:` file defines a method called `name` anywhere — on any class, on either
245
246
  # side, or at the top level. Asked once per name per file.
247
+ #
248
+ # Issue #2133 — the answer reads every project file's tables whole, so it records a `defines:<name>` edge
249
+ # whichever way it answers, as {Inference::BlockCallTiming.project_defines_anywhere?} does: an edit that makes
250
+ # a file start or stop defining the name re-checks the call. A `pre_eval:` patch moves the snapshot's own
251
+ # fingerprint.
246
252
  def project_defines?(scope, name)
253
+ Analysis::DependencyRecorder.read_missing(:defines, name) if Analysis::DependencyRecorder.active?
247
254
  return true if scope.discovered_methods.any? { |_owner, table| table.key?(name) }
248
255
  return true if scope.discovered_def_nodes.any? { |_owner, table| table.key?(name) }
249
256
 
@@ -168,7 +168,9 @@ module Rigor
168
168
  # Bare (dot-less) diagnostic ids the engine emits outside the catalogue (see the `rule:` literals in
169
169
  # `Analysis::Runner` / `Runner::DiagnosticAggregator`). A token equal to one of these is treated as
170
170
  # known by `suppression.unknown-rule` even though it carries no family prefix; extend the list when
171
- # the runner grows a new bare id.
171
+ # the runner grows a new bare id. Ids a marker or `disable:` cannot suppress (the `--explain`
172
+ # `fallback` row, the `contribution_merge` conflict reasons) stay out: they are
173
+ # `ConfigAudit::SEVERITY_ONLY_IDS`, known to the `severity_overrides:` audit alone.
172
174
  NON_CHECK_DIAGNOSTIC_IDS = %w[
173
175
  configuration-error load-error pool-degraded runtime-error source-rbs-synthesis-failed
174
176
  source-rbs-annotation-not-honoured
@@ -101,6 +101,19 @@ module Rigor
101
101
  # {#decline_unless_recorded_nowhere}): it settles by definitions, as an `ABSENT` read does.
102
102
  return settle_by_walk if @unknown
103
103
 
104
+ # Issue #1833 — a level the `:arity` resolver took to be the project's on a mark alone (a namespace
105
+ # candidate a marks-only file rewrites) stands only while that mark does, and no definition appearing or
106
+ # vanishing can overturn it. Only such levels file the edge: the walk's other mark reads would re-check
107
+ # the call whenever an unrelated namespace reuses an ancestor's last segment.
108
+ @levels.each do |level|
109
+ ([level.class_name] + level.modules).each do |name|
110
+ next if @scope.known_user_class?(name)
111
+ next unless Scope::DiscoveryIndex.declares_surface?(@scope.discovered_parameter_envelopes[name])
112
+
113
+ Analysis::DependencyRecorder.read_last_segment(:mark, name)
114
+ end
115
+ end
116
+
104
117
  @owner_entries.each do |name, kind|
105
118
  if kind == :singleton
106
119
  @scope.user_singleton_def_site_for(name, @method_name)
@@ -121,7 +134,8 @@ module Rigor
121
134
  # Stage 2, for the class {#owner_envelope} last answered.
122
135
  def authoritative?(class_name)
123
136
  !load_order_dependent?(class_name) && !object_extension_may_shadow? &&
124
- @levels.all? { |level| clean_level?(level) && public_at?(level.class_name) } &&
137
+ @levels.all? { |level| clean_level?(level) && (@kind == :singleton || public_at?(level.class_name)) } &&
138
+ !(@kind == :singleton && singleton_private?(class_name)) &&
125
139
  chain_free_of_hooks?(class_name) &&
126
140
  subclasses_agree?(class_name, @envelope)
127
141
  end
@@ -245,8 +259,10 @@ module Rigor
245
259
  end
246
260
 
247
261
  # A module some method body hands to `obj.extend` sits ahead of the object's class, so if it defines
248
- # the name at all, that object answers with the module's definition.
262
+ # the name at all, that object answers with the module's definition. Every bucket is read, so the answer
263
+ # records `mark:*`, which an object-extended mark appearing or vanishing anywhere fires (issue #1833).
249
264
  def object_extension_may_shadow?
265
+ Analysis::DependencyRecorder.read_name(:mark, "*") if Analysis::DependencyRecorder.active?
250
266
  @scope.discovered_parameter_envelopes.any? do |_name, bucket|
251
267
  bucket.key?(Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK) && bucket.key?([:instance, @method_name])
252
268
  end
@@ -291,7 +307,9 @@ module Rigor
291
307
  end
292
308
 
293
309
  # A bucket of class marks alone (issue #1820: `x.singleton_class.prepend(Comparable)`) does not make a name
294
- # the project's.
310
+ # the project's. The bucket is read directly rather than through `parameter_envelopes_of`: a `mark:<last>`
311
+ # edge recorded here fires on marks a correctly-fitting call does not depend on, so the dependency on
312
+ # a mark that could flip this resolution is kept at `settle_by_definitions` instead (issue #1833).
295
313
  def project_class?(name)
296
314
  @scope.known_user_class?(name) ||
297
315
  Scope::DiscoveryIndex.declares_surface?(@scope.discovered_parameter_envelopes[name])
@@ -348,6 +366,17 @@ module Rigor
348
366
  false
349
367
  end
350
368
 
369
+ # Issue #2059 — a call Ruby rejects as private on the singleton side (`private_class_method :two`, a private
370
+ # method of a module that extends itself) is `def.method-visibility-mismatch`'s, which reads the same definer;
371
+ # the arity is not checked. A read that declines leaves the arity to the other conditions.
372
+ def singleton_private?(class_name)
373
+ case Inference::DefinerResolution.resolve(@scope, class_name, @method_name, :singleton, question: :visibility)
374
+ in Inference::DefinerResolution::Known(answer:, owner: _owner) then answer == :private
375
+ in Inference::DefinerResolution::UNKNOWN then false
376
+ in Inference::DefinerResolution::ABSENT then false # rubocop:disable Lint/DuplicateBranch
377
+ end
378
+ end
379
+
351
380
  def public_at?(class_name)
352
381
  visibility = @scope.discovered_method_visibility(class_name, @method_name)
353
382
  visibility.nil? || visibility == :public