rigortype 0.4.3 → 0.4.5

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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +104 -15
  5. data/docs/manual/02-cli-reference.md +43 -12
  6. data/docs/manual/03-configuration.md +20 -7
  7. data/docs/manual/04-diagnostics.md +56 -3
  8. data/docs/manual/05-inspecting-types.md +2 -2
  9. data/docs/manual/06-baseline.md +1 -1
  10. data/docs/manual/07-plugins.md +3 -2
  11. data/docs/manual/08-skills.md +7 -0
  12. data/docs/manual/10-mcp-server.md +40 -4
  13. data/docs/manual/18-removing-dead-code.md +23 -2
  14. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  15. data/docs/manual/plugins/rigor-rspec.md +101 -1
  16. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  17. data/exe/rigor +12 -17
  18. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  19. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
  20. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
  21. data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
  22. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  23. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
  24. data/lib/rigor/analysis/check_rules.rb +274 -51
  25. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  26. data/lib/rigor/analysis/incremental.rb +61 -0
  27. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  28. data/lib/rigor/analysis/incremental_session.rb +270 -28
  29. data/lib/rigor/analysis/path_expansion.rb +3 -1
  30. data/lib/rigor/analysis/reachability/graph.rb +111 -14
  31. data/lib/rigor/analysis/reachability/scan.rb +305 -23
  32. data/lib/rigor/analysis/rule_catalog.rb +22 -4
  33. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  34. data/lib/rigor/analysis/run_cache_probe.rb +5 -3
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
  36. data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
  37. data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
  38. data/lib/rigor/analysis/runner.rb +102 -72
  39. data/lib/rigor/analysis/severity_stamp.rb +45 -1
  40. data/lib/rigor/analysis/worker_session.rb +69 -2
  41. data/lib/rigor/cache/descriptor.rb +79 -12
  42. data/lib/rigor/cache/engine_source.rb +3 -2
  43. data/lib/rigor/cache/incremental_snapshot.rb +39 -5
  44. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  45. data/lib/rigor/cache/store.rb +4 -3
  46. data/lib/rigor/cli/command.rb +3 -1
  47. data/lib/rigor/cli/docs_command.rb +3 -2
  48. data/lib/rigor/cli/doctor_command.rb +1 -1
  49. data/lib/rigor/cli/explain_command.rb +3 -2
  50. data/lib/rigor/cli/mcp_command.rb +1 -1
  51. data/lib/rigor/cli/plugin_command.rb +2 -1
  52. data/lib/rigor/cli/plugins_command.rb +2 -1
  53. data/lib/rigor/cli/sig_gen_command.rb +32 -11
  54. data/lib/rigor/cli/skill_command.rb +2 -1
  55. data/lib/rigor/cli/skill_describe.rb +3 -2
  56. data/lib/rigor/cli/unused_command.rb +10 -7
  57. data/lib/rigor/cli.rb +3 -2
  58. data/lib/rigor/config_audit.rb +163 -22
  59. data/lib/rigor/configuration/dependencies.rb +19 -11
  60. data/lib/rigor/configuration/severity_profile.rb +20 -2
  61. data/lib/rigor/configuration.rb +29 -3
  62. data/lib/rigor/effects/signature_sources.rb +3 -1
  63. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  64. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  65. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  66. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  67. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  68. data/lib/rigor/environment/rbs_loader.rb +67 -11
  69. data/lib/rigor/inference/block_call_timing.rb +12 -1
  70. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  71. data/lib/rigor/inference/captured_locals.rb +67 -13
  72. data/lib/rigor/inference/clause_widening.rb +90 -0
  73. data/lib/rigor/inference/definer_resolution.rb +101 -25
  74. data/lib/rigor/inference/expression_typer.rb +94 -9
  75. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  76. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  77. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  78. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  79. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
  80. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  81. data/lib/rigor/inference/method_dispatcher.rb +67 -3
  82. data/lib/rigor/inference/module_function_state.rb +213 -51
  83. data/lib/rigor/inference/narrowing.rb +235 -75
  84. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  85. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  86. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  87. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  88. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  89. data/lib/rigor/inference/project_seed_tables.rb +120 -0
  90. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  91. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  92. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  93. data/lib/rigor/inference/repeated_or_writes.rb +414 -1
  94. data/lib/rigor/inference/return_barrier.rb +16 -0
  95. data/lib/rigor/inference/scope_indexer.rb +1061 -122
  96. data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
  97. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  98. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  99. data/lib/rigor/inference/statement_evaluator.rb +512 -150
  100. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  101. data/lib/rigor/lens/anchor.rb +3 -3
  102. data/lib/rigor/lens/contract_reader.rb +60 -2
  103. data/lib/rigor/lens/document.rb +4 -3
  104. data/lib/rigor/lens/file_reader.rb +452 -73
  105. data/lib/rigor/lens/members.rb +81 -17
  106. data/lib/rigor/lens/run.rb +73 -4
  107. data/lib/rigor/lens.rb +2 -1
  108. data/lib/rigor/mcp/server.rb +63 -22
  109. data/lib/rigor/plugin/base.rb +3 -2
  110. data/lib/rigor/plugin/box_probe.rb +154 -7
  111. data/lib/rigor/plugin/io_boundary.rb +42 -6
  112. data/lib/rigor/plugin/isolation.rb +4 -3
  113. data/lib/rigor/project_environment.rb +6 -3
  114. data/lib/rigor/protection/discovery_seed.rb +9 -36
  115. data/lib/rigor/protection/mutation_cache.rb +8 -2
  116. data/lib/rigor/rooted_glob.rb +27 -0
  117. data/lib/rigor/scope/discovery_index.rb +5 -0
  118. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  119. data/lib/rigor/scope/resolution_chain.rb +40 -5
  120. data/lib/rigor/scope.rb +56 -8
  121. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  122. data/lib/rigor/sig_gen/classification.rb +4 -1
  123. data/lib/rigor/sig_gen/generator.rb +167 -22
  124. data/lib/rigor/sig_gen/layout_index.rb +40 -1
  125. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  126. data/lib/rigor/sig_gen/observation_collector.rb +137 -11
  127. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  128. data/lib/rigor/sig_gen/renderer.rb +13 -155
  129. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  130. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  131. data/lib/rigor/sig_gen/write_result.rb +3 -1
  132. data/lib/rigor/sig_gen/writer.rb +50 -35
  133. data/lib/rigor/signature_path_audit.rb +4 -2
  134. data/lib/rigor/type/constant.rb +10 -1
  135. data/lib/rigor/version.rb +1 -1
  136. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  137. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
  138. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  139. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  141. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  142. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  143. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  146. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  147. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  148. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  149. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  150. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  151. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
  152. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  153. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  154. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  155. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  156. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  157. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  158. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  159. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  160. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  164. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  166. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  167. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  168. data/sig/rigor/rooted_glob.rbs +7 -0
  169. data/sig/rigor/scope.rbs +4 -3
  170. data/skills/rigor-doctor/references/01-checks.md +23 -11
  171. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  172. metadata +16 -1
@@ -8,6 +8,7 @@ require_relative "parameters"
8
8
  require_relative "../inference/def_return_typer"
9
9
  require_relative "../inference/origin_lookup"
10
10
  require_relative "../sig_gen/type_elaborator"
11
+ require_relative "../source/alias_names"
11
12
 
12
13
  module Rigor
13
14
  module Lens
@@ -22,7 +23,10 @@ module Rigor
22
23
  # walk cannot place (under an `if`, a `begin`, a block) leaves that default unknown.
23
24
  # A `detached` body stands for code that is not the class body (a block, a method body, a superclass
24
25
  # expression, an omitted class): its toggles and named calls stay local and change no site.
25
- Body = Struct.new(:current, :uncertain, :detached)
26
+ # `kind` is :module, :class or :singleton. `module_function` is :off, :on (a bare `module_function` written
27
+ # directly in a module body covers the later direct defs) or :unknown (the reading Ruby gives and the one
28
+ # `check`'s tables give could differ, so the later defs are omissions).
29
+ Body = Struct.new(:current, :uncertain, :detached, :kind, :module_function)
26
30
 
27
31
  ATTRIBUTE_CALLS = { attr_reader: "reader", attr_writer: "writer", attr_accessor: "accessor" }.freeze
28
32
  OMITTED_CALLS = {
@@ -30,6 +34,8 @@ module Rigor
30
34
  refine: "refine"
31
35
  }.freeze
32
36
  VISIBILITY_CALLS = %i[private protected public].freeze
37
+ # Instance methods Ruby makes private whatever the visibility they are defined or aliased with.
38
+ FORCED_PRIVATE = %w[initialize initialize_copy initialize_clone initialize_dup respond_to_missing?].freeze
33
39
  CLASS_VISIBILITY_CALLS = { private_class_method: "private", public_class_method: "public" }.freeze
34
40
  BLOCK_NODES = [Prism::BlockNode, Prism::LambdaNode].freeze
35
41
  CONSTANT_WRITES = [
@@ -66,10 +72,11 @@ module Rigor
66
72
  when Prism::SingletonClassNode then walk_singleton_class(node, nesting, singleton, in_block)
67
73
  when Prism::DefNode then read_def(node, nesting, singleton, in_block)
68
74
  when Prism::CallNode then walk_call(node, nesting, singleton, in_block)
69
- when Prism::AliasMethodNode
70
- omit("alias", node.new_name.slice.delete_prefix(":"), node, owner: nesting.last, singleton: singleton)
75
+ when Prism::AliasMethodNode then read_alias(node, nesting, singleton, in_block)
71
76
  when Prism::MultiWriteNode then walk_multi_write(node, nesting, singleton, in_block)
72
- when *CONSTANT_WRITES then walk_constant(node, nesting)
77
+ when Prism::ForNode then walk_assigning(node, node.index, nesting, singleton, in_block)
78
+ when Prism::RescueNode then walk_assigning(node, node.reference, nesting, singleton, in_block)
79
+ when *CONSTANT_WRITES then walk_constant(node, nesting, in_block)
73
80
  else walk_children(node, nesting, singleton, in_block)
74
81
  end
75
82
  end
@@ -85,7 +92,7 @@ module Rigor
85
92
  end
86
93
 
87
94
  def detached
88
- @bodies.push(Body.new("public", false, true))
95
+ @bodies.push(Body.new("public", false, true, :detached, :off))
89
96
  yield
90
97
  ensure
91
98
  @bodies.pop
@@ -97,16 +104,16 @@ module Rigor
97
104
  if node.is_a?(Prism::ClassNode) && node.superclass
98
105
  detached { walk(node.superclass, nesting, singleton: false, in_block: true) }
99
106
  end
100
- if in_block || name.nil?
101
- omit(kind, name || node.constant_path.slice, node)
107
+ if in_block || name.nil? || constant_scope_unknown?
108
+ omit(kind, constant_scope_unknown? ? node.constant_path.slice : (name || node.constant_path.slice), node)
102
109
  detached { walk(node.body, nesting, singleton: false, in_block: true) } if node.body
103
110
  return
104
111
  end
105
112
 
106
113
  add_site(node, { "kind" => kind, "name" => name, "owner" => nil, "scope" => nil, "attribute" => nil,
107
- "members" => [], "visibility" => nil, "params" => nil, "block" => nil,
108
- "body_return" => nil })
109
- walk_body(node.body, nesting + [name], singleton: false)
114
+ "members" => [], "module_function" => false, "visibility" => nil, "params" => nil,
115
+ "block" => nil, "body_return" => nil })
116
+ walk_body(node.body, nesting + [name], singleton: false, kind: kind.to_sym)
110
117
  end
111
118
 
112
119
  # `class << self` directly in a class or module body opens its singleton class. Any other `class << x`, one
@@ -117,33 +124,79 @@ module Rigor
117
124
  return detached { walk(node.body, nesting, singleton: singleton, in_block: true) } if node.body
118
125
  end
119
126
 
120
- walk_body(node.body, nesting, singleton: true)
127
+ walk_body(node.body, nesting, singleton: true, kind: :singleton)
121
128
  end
122
129
 
123
- # A class body's statements, tracking the running visibility and `module_function`. A bare `module_function`
124
- # makes every later `def` of the body a method on both sides: one site, two members, which revision 1 does
125
- # not model.
126
- def walk_body(body, nesting, singleton:)
130
+ # A class body's statements, tracking the running visibility and `module_function`.
131
+ def walk_body(body, nesting, singleton:, kind:)
127
132
  return if body.nil?
128
133
 
129
- @bodies.push(Body.new("public", false, false))
134
+ @bodies.push(Body.new("public", false, false, kind, :off))
130
135
  statements = body.is_a?(Prism::StatementsNode) ? body.body : [body]
131
- module_function_on = false
132
- statements.each do |child|
133
- if module_function_on && child.is_a?(Prism::DefNode)
134
- omit("module_function", child.name.to_s, child, owner: nesting.last, singleton: singleton)
135
- next
136
- end
137
- module_function_on = true if bare_call?(child, :module_function)
138
- next if toggle_visibility?(child)
139
-
140
- visibility_call(child, nesting, singleton, direct: true) if visibility_call?(child)
141
- walk(child, nesting, singleton: singleton, in_block: false)
142
- end
136
+ statements.each { |child| walk_body_statement(child, nesting, singleton) }
143
137
  ensure
144
138
  @bodies.pop if body
145
139
  end
146
140
 
141
+ def walk_body_statement(child, nesting, singleton)
142
+ return bare_module_function if bare_call?(child, :module_function)
143
+ return end_module_function if toggle_visibility?(child)
144
+ return named_module_function(child, nesting, singleton) if named_module_function?(child)
145
+ return constant_visibility(child, nesting, direct: true) if constant_visibility_call?(child)
146
+
147
+ visibility_call(child, nesting, singleton, direct: true) if visibility_call?(child)
148
+ @direct_statement = child
149
+ walk(child, nesting, singleton: singleton, in_block: false)
150
+ ensure
151
+ @direct_statement = nil
152
+ end
153
+
154
+ # A bare `module_function` directly in a module body turns the mode on for the later direct defs. In a class
155
+ # (where Ruby raises) or after the mode was left unknown, the later defs stay omissions. A second bare call
156
+ # after a toggle also stays unknown, though Ruby turns the mode on again: the safe side.
157
+ def bare_module_function
158
+ body = @bodies.last
159
+ body.module_function = body.kind == :module && body.module_function != :unknown ? :on : :unknown
160
+ end
161
+
162
+ # A bare `private` / `public` / `protected` ends the mode in Ruby, but `check`'s tables keep it on. The two
163
+ # readings differ for the later defs, so they are left as omissions.
164
+ def end_module_function
165
+ body = @bodies.last
166
+ body.module_function = :unknown if body.module_function == :on
167
+ end
168
+
169
+ def named_module_function?(child)
170
+ child.is_a?(Prism::CallNode) && child.receiver.nil? && child.name == :module_function && child.arguments
171
+ end
172
+
173
+ # A named `module_function` (`module_function :a`, `module_function %i[a]`, `module_function def a`) is an
174
+ # omission: `check`'s singleton table reads it differently from Ruby (a def nested in the body, a reopened
175
+ # module, a redefinition after the call, a wrapped def). The instance methods it names turn private in Ruby,
176
+ # so their visibility reads unknown, and the omission makes each named member contested.
177
+ def named_module_function(node, nesting, singleton)
178
+ owner = nesting.last
179
+ node.arguments.arguments.each do |argument|
180
+ # The copy replaces any `def self.x` already there, so the singleton side is contested as well.
181
+ names = argument.is_a?(Prism::DefNode) ? [argument.name.to_s] : argument_names(argument)
182
+ if names.nil?
183
+ [false, true].each do |side|
184
+ unknown_visibility(owner, scope_name(side), default_too: false)
185
+ omit("module_function", nil, argument, owner: owner, singleton: side, any_member: true)
186
+ end
187
+ else
188
+ names.product([false, true]).each do |name, side|
189
+ name_visibility(owner, scope_name(side), name, nil)
190
+ omit("module_function", name, argument.is_a?(Prism::DefNode) ? argument : node, owner: owner,
191
+ singleton: side)
192
+ end
193
+ end
194
+ if argument.is_a?(Prism::DefNode) && argument.body
195
+ detached { walk(argument.body, nesting, singleton: singleton, in_block: true) }
196
+ end
197
+ end
198
+ end
199
+
147
200
  # A bare `private` / `protected` / `public` written directly in the body sets the default for later defs.
148
201
  def toggle_visibility?(child)
149
202
  return false unless VISIBILITY_CALLS.any? { |name| bare_call?(child, name) }
@@ -157,20 +210,32 @@ module Rigor
157
210
  if node.receiver.nil? && ATTRIBUTE_CALLS.key?(node.name)
158
211
  read_attribute(node, nesting, singleton, in_block)
159
212
  elsif node.receiver.nil? && OMITTED_CALLS.key?(node.name)
160
- return if bare_call?(node, :module_function)
161
-
162
- omit(OMITTED_CALLS.fetch(node.name), first_argument_name(node), node,
163
- owner: node.name == :refine ? nil : nesting.last, singleton: singleton)
164
- walk_children(node, nesting, singleton, true) unless node.name == :refine
213
+ omitted_call(node, nesting, singleton, in_block)
214
+ elsif constant_visibility_call?(node)
215
+ constant_visibility(node, nesting, direct: false)
165
216
  else
166
- @visited_visibility_calls ||= {}.compare_by_identity
167
- if visibility_call?(node) && !@visited_visibility_calls.key?(node)
168
- visibility_call(node, nesting, singleton, direct: false)
169
- end
217
+ unplaced_visibility_call(node, nesting, singleton)
170
218
  walk_children(node, nesting, singleton, in_block)
171
219
  end
172
220
  end
173
221
 
222
+ # A visibility call the body walk did not already read directly.
223
+ def unplaced_visibility_call(node, nesting, singleton)
224
+ @visited_visibility_calls ||= {}.compare_by_identity
225
+ return unless visibility_call?(node) && !@visited_visibility_calls.key?(node)
226
+
227
+ visibility_call(node, nesting, singleton, direct: false)
228
+ end
229
+
230
+ def omitted_call(node, nesting, singleton, in_block)
231
+ return unplaced_module_function if bare_call?(node, :module_function)
232
+ return read_alias(node, nesting, singleton, in_block) if node.name == :alias_method
233
+
234
+ omit(OMITTED_CALLS.fetch(node.name), first_argument_name(node), node,
235
+ owner: node.name == :refine ? nil : nesting.last, singleton: singleton)
236
+ walk_children(node, nesting, singleton, true) unless node.name == :refine
237
+ end
238
+
174
239
  def visibility_call?(node)
175
240
  node.is_a?(Prism::CallNode) && node.receiver.nil? &&
176
241
  (VISIBILITY_CALLS.include?(node.name) || CLASS_VISIBILITY_CALLS.key?(node.name))
@@ -235,63 +300,364 @@ module Rigor
235
300
  end
236
301
  end
237
302
 
238
- def unknown_visibility(owner, scope)
303
+ # `default_too: false` leaves the body's running default alone: `module_function(*NAMES)` changes only the
304
+ # members it names, while `private *NAMES` is read as making the default unknown too, the safe side.
305
+ def unknown_visibility(owner, scope, default_too: true)
239
306
  @sites.each do |site|
240
- site.data["visibility"] = nil if site.data["owner"] == owner && site.data["scope"] == scope
307
+ data = site.data
308
+ next unless data["owner"] == owner
309
+
310
+ data["visibility"] = nil if data["scope"] == scope
311
+ data["singleton_visibility"] = nil if scope == "singleton" && data["module_function"]
241
312
  end
242
- mark_uncertain
313
+ mark_uncertain if default_too
243
314
  end
244
315
 
245
316
  # `private :x` changes the visibility of the members already defined under that name in this file.
246
317
  def name_visibility(owner, scope, name, visibility)
247
318
  @sites.each do |site|
248
319
  data = site.data
249
- next unless data["owner"] == owner && data["scope"] == scope && data["members"].include?(name)
320
+ next unless data["owner"] == owner && data["members"].include?(name)
250
321
 
251
- data["visibility"] = data["members"] == [name] ? visibility : nil
322
+ if data["scope"] == scope
323
+ data["visibility"] = data["members"] == [name] ? visibility : nil
324
+ elsif scope == "singleton" && data["module_function"]
325
+ # `private_class_method :x` reaches a module function's singleton copy.
326
+ data["singleton_visibility"] = visibility
327
+ end
252
328
  end
253
329
  end
254
330
 
331
+ # `a, B = …`: each constant target is an omission. The targets are walked too, since an index or call target
332
+ # (`a[expr], b = …`) or a constant path's parent can hold a declaration; the value is walked last.
255
333
  def walk_multi_write(node, nesting, singleton, in_block)
256
- (node.lefts + [node.rest&.expression].compact + node.rights).each do |target|
257
- next unless target.is_a?(Prism::ConstantTargetNode) || target.is_a?(Prism::ConstantPathTargetNode)
334
+ omit_constant_targets(node, nesting)
335
+ targets_of(node).each { |target| walk(target, nesting, singleton: singleton, in_block: in_block) }
336
+ walk(node.value, nesting, singleton: singleton, in_block: in_block)
337
+ end
338
+
339
+ # A `for A in …` index or a `rescue => E` reference assigns the constants it names, like a multiple
340
+ # assignment does.
341
+ def walk_assigning(node, target, nesting, singleton, in_block)
342
+ omit_constant_targets(target, nesting) if target
343
+ walk_children(node, nesting, singleton, in_block)
344
+ end
258
345
 
259
- omit("constant", qualify(nesting, target) || target.slice, target)
346
+ def omit_constant_targets(target, nesting)
347
+ constant_targets(target).each do |constant|
348
+ omit("constant", qualify(nesting, constant) || constant.slice, constant)
349
+ end
350
+ end
351
+
352
+ # The constants a target assigns: itself when it is one, and those inside a nested `(a, B)` group or a
353
+ # `*Rest` splat. A group's rest slot is a `SplatNode` (`a, *B = …`, or a bare `*`) or an `ImplicitRestNode`
354
+ # (the trailing comma of `a, = …`), which names nothing.
355
+ def constant_targets(target)
356
+ case target
357
+ when Prism::ConstantTargetNode, Prism::ConstantPathTargetNode then [target]
358
+ when Prism::MultiWriteNode, Prism::MultiTargetNode then targets_of(target).flat_map { |t| constant_targets(t) }
359
+ when Prism::SplatNode then target.expression ? constant_targets(target.expression) : []
360
+ else []
260
361
  end
261
- walk(node.value, nesting, singleton: singleton, in_block: in_block)
262
362
  end
263
363
 
264
- def walk_constant(node, nesting)
364
+ def targets_of(node)
365
+ node.lefts + [node.rest].compact + node.rights
366
+ end
367
+
368
+ def walk_constant(node, nesting, in_block)
265
369
  target = node.respond_to?(:target) ? node.target : node
266
370
  name = node.respond_to?(:target) ? qualify(nesting, node.target) : qualify_name(nesting, node.name.to_s)
267
- kind = case data_or_struct(node.value)
268
- when :data then "data_class"
269
- when :struct then "struct_class"
270
- else "constant"
271
- end
272
- omit(kind, name || target.slice, node)
371
+ builder = data_or_struct(node.value)
372
+ if modelled_write?(node, name, in_block)
373
+ fields = builder && builder_fields(node.value, builder)
374
+ return read_builder_class(node, name, nesting, builder, fields) if fields
375
+ # A path write (`Foo::BAR = 1`, `::BAR = 1`) lands on the path's namespace, which `check` resolves
376
+ # through the nesting and Rigor does not name the way it does: an omission.
377
+ return read_constant(node, name, nesting) if builder.nil? && node.is_a?(Prism::ConstantWriteNode)
378
+ end
379
+
380
+ kind = { data: "data_class", struct: "struct_class" }.fetch(builder, "constant")
381
+ # Under a builder block or `class << self` the nesting does not name where the constant lands: as written.
382
+ written = node.respond_to?(:target) ? target.slice : node.name.to_s
383
+ omit(kind, constant_scope_unknown? ? written : (name || written), node)
384
+ walk(node.value, nesting, singleton: false, in_block: true)
385
+ end
386
+
387
+ # `X = value` written directly in a namespace body (or at the top level): a `constant` site (capability
388
+ # `constant`). Its `value` slot is filled from the run's tables ({Run#read_constant_value}). A declaration
389
+ # inside the value (a lambda body, a `Class.new` block) is an omission, as anywhere in a block.
390
+ def read_constant(node, name, nesting)
391
+ owner = nesting.last || "Object"
392
+ add_site(node, { "kind" => "constant", "name" => name, "owner" => owner, "scope" => nil,
393
+ "attribute" => nil, "members" => [], "module_function" => false,
394
+ "visibility" => reassigned_visibility(name, owner), "params" => nil, "block" => nil,
395
+ "body_return" => nil, "value" => Slot.unavailable("not_computed") })
273
396
  walk(node.value, nesting, singleton: false, in_block: true)
274
397
  end
275
398
 
276
- def read_def(node, nesting, singleton, in_block)
399
+ # Reassigning a constant keeps its visibility in Ruby: a `private_constant` above still holds.
400
+ def reassigned_visibility(name, owner)
401
+ earlier = @sites.rfind do |site|
402
+ site.data["kind"] == "constant" && site.data["owner"] == owner && site.data["name"] == name
403
+ end
404
+ earlier ? earlier.data["visibility"] : "public"
405
+ end
406
+
407
+ # `Foo = Data.define(:a, :b) do … end` and `Foo = Struct.new(:a, keyword_init: true) do … end`: a class site
408
+ # with `builder` set, an attribute site per field (a reader for `Data`, an accessor for `Struct`), and the block
409
+ # read as the class body, which Ruby `class_eval`s on the new class. `check`'s discovery keys the block's defs
410
+ # and the fields under the same constant name.
411
+ def read_builder_class(node, name, nesting, builder, fields)
412
+ add_site(node, { "kind" => "class", "name" => name, "owner" => nil, "scope" => nil, "attribute" => nil,
413
+ "members" => [], "module_function" => false, "builder" => builder.to_s,
414
+ "visibility" => nil, "params" => nil, "block" => nil, "body_return" => nil })
415
+ attribute = builder == :data ? "reader" : "accessor"
416
+ start_line = node.location.start_line
417
+ fields.each do |field, field_node|
418
+ # rbs-inline reads a `#:` trailing a field on its own line of a multi-line call as that field's type.
419
+ trailing = field_node.location.start_line != start_line
420
+ members = attribute_members(field, attribute)
421
+ visibility = builder_field_visibility(members)
422
+ add_site(field_node, { "kind" => "attribute", "name" => field, "owner" => name, "scope" => "instance",
423
+ "attribute" => attribute, "members" => members,
424
+ "module_function" => false, "visibility" => visibility, "params" => nil,
425
+ "block" => nil, "body_return" => nil }, contracts: trailing ? :trailing : false)
426
+ end
427
+ block = node.value.block
428
+ walk_body(block.body, nesting + [name], singleton: false, kind: :builder) if block&.body
429
+ end
430
+
431
+ # Ruby forces a field reader named `initialize_copy` (say) private, but not its `Struct` writer: a `Data`
432
+ # field reads `private`, a `Struct` one, whose site covers both, `null`.
433
+ def builder_field_visibility(members)
434
+ return "public" unless members.intersect?(FORCED_PRIVATE)
435
+
436
+ members.all? { |member| FORCED_PRIVATE.include?(member) } ? "private" : nil
437
+ end
438
+
439
+ # Whether a constant, class or module written here lands somewhere the nesting does not name: a builder
440
+ # block, which Ruby `class_eval`s (defs go to the new class, but constants stay with the lexical scope), or a
441
+ # `class << self` body (constants go to the singleton class). Such a declaration is an omission.
442
+ def constant_scope_unknown?
443
+ @bodies.any? { |body| %i[builder singleton].include?(body.kind) }
444
+ end
445
+
446
+ def constant_visibility_call?(node)
447
+ node.is_a?(Prism::CallNode) && node.receiver.nil? && %i[private_constant public_constant].include?(node.name) &&
448
+ node.arguments
449
+ end
450
+
451
+ # `private_constant :X` written directly in a namespace body makes the constants it names, already assigned
452
+ # above it in this body's namespace, private. Written anywhere else (an `if`, a block, a method) it runs at a
453
+ # time Rigor does not know, so those constants read unknown; a non-literal argument leaves every constant of
454
+ # the namespace unknown. Under a builder block or `class << self` it targets the new class or the singleton
455
+ # class, which hold none of the constants listed here.
456
+ def constant_visibility(node, nesting, direct:)
457
+ return if constant_scope_unknown?
458
+
459
+ direct &&= !@bodies.last&.detached
460
+ visibility = node.name == :private_constant ? "private" : "public"
461
+ node.arguments.arguments.each do |argument|
462
+ names = argument_names(argument)
463
+ named_constants(nesting, names).each { |site| site.data["visibility"] = names && direct ? visibility : nil }
464
+ end
465
+ end
466
+
467
+ # The namespace's constant sites so far that `names` names, or all of them when `names` is nil.
468
+ def named_constants(nesting, names)
469
+ owner = nesting.last || "Object"
470
+ qualified = names&.map { |name| qualify_name(nesting, name) }
471
+ @sites.select do |site|
472
+ site.data["kind"] == "constant" && site.data["owner"] == owner &&
473
+ (qualified.nil? || qualified.include?(site.data["name"]))
474
+ end
475
+ end
476
+
477
+ # A plain constant assignment whose name Rigor can qualify, outside any block or a body whose constants land
478
+ # somewhere the nesting does not name.
479
+ def modelled_write?(node, name, in_block)
480
+ name && !in_block && plain_write?(node) && !constant_scope_unknown?
481
+ end
482
+
483
+ def plain_write?(node)
484
+ node.is_a?(Prism::ConstantWriteNode) || node.is_a?(Prism::ConstantPathWriteNode)
485
+ end
486
+
487
+ # `[[name, node], …]` for a builder call whose fields are all symbols, or nil. `Struct.new` may end with
488
+ # `keyword_init:` alone. Not modelled: a string field (`check`'s discovery reads only symbols, and
489
+ # `Struct.new("Name", …)` defines `Struct::Name`), a `&block` argument, and a duplicate field or a writer-like
490
+ # name (`a=`), which Ruby rejects at load time.
491
+ def builder_fields(call, builder)
492
+ return nil if call.block.is_a?(Prism::BlockArgumentNode)
493
+
494
+ arguments = call.arguments&.arguments || []
495
+ arguments = arguments[0...-1] if builder == :struct && keyword_init_only?(arguments.last)
496
+ return nil unless arguments.all?(Prism::SymbolNode)
497
+
498
+ names = arguments.map(&:unescaped)
499
+ return nil if names.uniq.size != names.size || names.any? { |name| name.end_with?("=") }
500
+
501
+ names.zip(arguments)
502
+ end
503
+
504
+ def keyword_init_only?(argument)
505
+ return false unless argument.is_a?(Prism::KeywordHashNode)
506
+
507
+ argument.elements.all? do |element|
508
+ element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode) &&
509
+ element.key.unescaped == "keyword_init"
510
+ end
511
+ end
512
+
513
+ # A bare `module_function` the walk cannot place in the body's statement list (under an `if`, say) leaves the
514
+ # mode unknown for the body's later defs.
515
+ def unplaced_module_function
516
+ body = @bodies.last
517
+ body.module_function = :unknown if body && !body.detached
518
+ end
519
+
520
+ def read_def(node, nesting, singleton, in_block, module_function: false)
277
521
  scope = def_scope(node, nesting, singleton)
522
+ module_function ||= module_function_state(node, singleton)
278
523
  if in_block || scope.nil?
279
524
  omit("method", node.name.to_s, node)
525
+ elsif module_function.nil?
526
+ omit("module_function", node.name.to_s, node, owner: nesting.last, singleton: singleton)
280
527
  else
281
- owner = nesting.last || "Object"
282
- data = { "kind" => "method", "name" => node.name.to_s, "owner" => owner, "scope" => scope.to_s,
283
- "attribute" => nil, "members" => [node.name.to_s],
284
- "visibility" => visibility(node, nesting, scope) }
285
- params, block, = Parameters.read(node.parameters, yields: Parameters.yields?(node.body))
286
- shape_params, shape_block = Parameters.shape(params, block)
287
- data.merge!("params" => shape_params, "block" => shape_block,
288
- "body_return" => body_return(node, owner, scope))
289
- add_site(node, data)
528
+ add_site(node, method_site(node, nesting, scope, module_function))
290
529
  end
291
530
  # A declaration inside a method body runs only when the method does: not modelled.
292
531
  detached { walk(node.body, nesting, singleton: singleton, in_block: true) } if node.body
293
532
  end
294
533
 
534
+ def method_site(node, nesting, scope, module_function)
535
+ owner = nesting.last || "Object"
536
+ params, block, = Parameters.read(node.parameters, yields: Parameters.yields?(node.body))
537
+ shape_params, shape_block = Parameters.shape(params, block)
538
+ returns = body_return(node, owner, scope)
539
+ data = { "kind" => "method", "name" => node.name.to_s, "owner" => owner, "scope" => scope.to_s,
540
+ "attribute" => nil, "members" => [node.name.to_s], "module_function" => module_function,
541
+ "visibility" => module_function ? "private" : visibility(node, nesting, scope),
542
+ "params" => shape_params, "block" => shape_block, "body_return" => returns }
543
+ return data unless module_function
544
+
545
+ # Internal to the member pass: the singleton copy is public until `private_class_method` says otherwise,
546
+ # and its body runs with the module as `self`. The instance-scope typing read `self` as an instance of the
547
+ # owner, so a return that may depend on `self` is not the copy's.
548
+ copy = self_dependent?(node.body, returns, owner) ? Slot.unavailable("not_computed") : returns
549
+ data.merge("singleton_visibility" => "public", "singleton_body_return" => copy)
550
+ end
551
+
552
+ # Whether the instance-scope return may differ from the singleton copy's: the body reads `self`, instance or
553
+ # class state, or `super` (nested defs excluded, blocks included, which over-declines safely), or the return
554
+ # names the owner's type, which is where a `self` that flowed through `itself`, `tap` or another module
555
+ # function shows up.
556
+ def self_dependent?(body, returns, owner)
557
+ instance_state?(body) || names_owner?(returns, owner)
558
+ end
559
+
560
+ def instance_state?(node)
561
+ return false if node.nil?
562
+ if node.is_a?(Prism::SelfNode) || node.is_a?(Prism::SuperNode) || node.is_a?(Prism::ForwardingSuperNode)
563
+ return true
564
+ end
565
+ return true if node.type.to_s.start_with?("instance_variable_", "class_variable_")
566
+ return false if node.is_a?(Prism::DefNode)
567
+
568
+ node.compact_child_nodes.any? { |child| instance_state?(child) }
569
+ end
570
+
571
+ # Whether a spelled type mentions the owner, by its qualified name or its last segment. A heuristic, not a proof:
572
+ # a self-dependent body whose instance-scope type folds to something that names no owner slips through.
573
+ def names_owner?(slot, owner)
574
+ type = slot["type"]
575
+ return false unless type
576
+
577
+ [owner, owner.split("::").last].uniq.any? do |name|
578
+ type.match?(/(?<![\w:])(?:::)?#{Regexp.escape(name)}(?!\w)/)
579
+ end
580
+ end
581
+
582
+ # Whether a receiverless instance `def` is a module function: true when the body's mode is on and the def is
583
+ # one of the body's direct statements, nil when the mode is on or unknown but Rigor cannot say (a def nested
584
+ # in the body, or after the mode became unknown), false otherwise.
585
+ def module_function_state(node, singleton)
586
+ body = @bodies.last
587
+ return false if body.nil? || body.detached || singleton || node.receiver
588
+ return false if body.module_function == :off
589
+
590
+ body.module_function == :on && node.equal?(@direct_statement) ? true : nil
591
+ end
592
+
593
+ # `alias new old` or `alias_method :new, :old` (literal names) written as a statement of a class, module or
594
+ # builder body: a `method` site named `new` with `alias_of` set (capability `alias`). Ruby copies the method
595
+ # `old` names at that moment, visibility included, so the site takes the visibility of the last `old` site of
596
+ # the owner above it, and its parameters and body return when that is the only `old` site above it and a
597
+ # `def`. Whether that is the `def` `check` reads (the final one) is the member pass's question. Anywhere else
598
+ # (a block, a method, `class << self`, the top level, wrapped in a visibility call or nested under an `if`),
599
+ # with a computed name, or while `module_function` is on, it is an omission.
600
+ def read_alias(node, nesting, singleton, in_block)
601
+ names = alias_names(node)
602
+ if names && modelled_alias?(node, nesting, singleton, in_block)
603
+ return add_site(node, alias_site(names.map(&:to_s), nesting.last))
604
+ end
605
+
606
+ omit("alias", names&.first&.to_s || alias_written_name(node), node, owner: nesting.last, singleton: singleton)
607
+ walk_children(node, nesting, singleton, true) if node.is_a?(Prism::CallNode)
608
+ end
609
+
610
+ def alias_names(node)
611
+ if node.is_a?(Prism::AliasMethodNode)
612
+ Source::AliasNames.keyword_names(node)
613
+ else
614
+ node.block.nil? ? Source::AliasNames.alias_method_call_names(node) : nil
615
+ end
616
+ end
617
+
618
+ def modelled_alias?(node, nesting, singleton, in_block)
619
+ body = @bodies.last
620
+ !in_block && !singleton && !nesting.empty? && body && !body.detached &&
621
+ body.module_function == :off && node.equal?(@direct_statement)
622
+ end
623
+
624
+ def alias_written_name(node)
625
+ node.is_a?(Prism::AliasMethodNode) ? node.new_name.slice.delete_prefix(":") : first_argument_name(node)
626
+ end
627
+
628
+ def alias_site(names, owner)
629
+ new_name, old_name = names
630
+ last, copy = alias_original(owner, old_name)
631
+ visibility = FORCED_PRIVATE.include?(new_name) ? "private" : last&.data&.fetch("visibility")
632
+ # `initialize` reads `void` by its name, which neither side of an alias across it keeps.
633
+ returns = copy && ![new_name, old_name].include?("initialize") ? copy.data["body_return"] : nil
634
+ { "kind" => "method", "name" => new_name, "owner" => owner, "scope" => "instance", "attribute" => nil,
635
+ "members" => [new_name], "module_function" => false, "visibility" => visibility,
636
+ "params" => copy&.data&.fetch("params"), "block" => copy&.data&.fetch("block"),
637
+ "body_return" => returns || Slot.unavailable("not_computed"),
638
+ "alias_of" => old_name, "aliased_node" => copy && aliased_node(copy) }
639
+ end
640
+
641
+ # `[last, copy]`: the last site of the owner above the alias defining `old` (nil when an omission names
642
+ # `old` too), and that site when it is the file's only one and a `def`.
643
+ def alias_original(owner, old_name)
644
+ olds = @sites.select do |site|
645
+ site.data["owner"] == owner && site.data["scope"] == "instance" && site.data["members"].include?(old_name)
646
+ end
647
+ unplaced = @omissions.any? do |omission|
648
+ [omission["owner"], omission["scope"]] == [owner, "instance"] && [old_name, nil].include?(omission["name"])
649
+ end
650
+ return [nil, nil] if unplaced
651
+
652
+ last = olds.last
653
+ [last, olds.size == 1 && last.data["kind"] == "method" ? last : nil]
654
+ end
655
+
656
+ # The `def` a method site reads: its own, or for an alias the one it copied.
657
+ def aliased_node(site)
658
+ site.data["alias_of"] ? site.data["aliased_node"] : site.node
659
+ end
660
+
295
661
  def read_attribute(node, nesting, singleton, in_block)
296
662
  arguments = node.arguments&.arguments || []
297
663
  names = arguments.map { |arg| arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode) ? arg.unescaped : nil }
@@ -303,28 +669,40 @@ module Rigor
303
669
  scope = singleton ? :singleton : :instance
304
670
  attribute = ATTRIBUTE_CALLS.fetch(node.name)
305
671
  names.each_with_index do |name, ordinal|
672
+ members = attribute_members(name, attribute)
306
673
  add_site(node, { "kind" => "attribute", "name" => name, "owner" => owner, "scope" => scope.to_s,
307
- "attribute" => attribute, "members" => attribute_members(name, attribute),
308
- "visibility" => visibility(node, nesting, scope),
674
+ "attribute" => attribute, "members" => members,
675
+ "module_function" => false,
676
+ "visibility" => attribute_visibility(node, nesting, scope, members),
309
677
  "params" => nil, "block" => nil, "body_return" => nil }, ordinal: ordinal)
310
678
  end
311
679
  end
312
680
 
313
- def add_site(node, data, ordinal: 0)
681
+ # `attr_reader :initialize` is forced private while its writer is not: not modelled.
682
+ def attribute_visibility(node, nesting, scope, members)
683
+ return nil if scope == :instance && members.intersect?(FORCED_PRIVATE)
684
+
685
+ visibility(node, nesting, scope)
686
+ end
687
+
688
+ # A builder's field reads no leading contract comments, which belong to the constant: `contracts: :trailing`
689
+ # reads only a `#:` trailing it (rbs-inline's per-field form on its own line), and `false` reads none.
690
+ def add_site(node, data, ordinal: 0, contracts: true)
314
691
  location = node.location
315
- anchor = @anchor.for_span(location.start_offset, location.end_offset, location.start_line,
316
- location.end_line)
692
+ anchor = @anchor.for_span(location.start_offset, location.end_offset, location.start_line, location.end_line,
693
+ leading: contracts == true, trailing: contracts != false)
317
694
  key = "#{@path}:#{location.start_offset}-#{location.end_offset}:#{ordinal}"
318
695
  tail = { "ordinal" => ordinal, "span" => Lens.span(location),
319
696
  "contract_span" => Lens.contract_span(anchor.contract_comments), "anchor" => anchor.token,
320
697
  "contract" => nil }
321
- @sites << Site.new(data: { "key" => key }.merge(data).merge(tail), node: node,
322
- contract_comments: anchor.contract_comments)
698
+ head = { "key" => key, "builder" => nil, "value" => nil, "alias_of" => nil }
699
+ @sites << Site.new(data: head.merge(data).merge(tail), node: node, contract_comments: anchor.contract_comments)
323
700
  end
324
701
 
325
- # `owner` / `singleton` name the member the declaration would define, when Rigor can tell.
326
- def omit(kind, name, node, owner: nil, singleton: false)
327
- member = owner && name
702
+ # `owner` / `singleton` name the member the declaration would define, when Rigor can tell. `any_member` keeps
703
+ # them with a null name: the declaration may define any member of that owner and scope.
704
+ def omit(kind, name, node, owner: nil, singleton: false, any_member: false)
705
+ member = owner && (name || any_member)
328
706
  @omissions << { "kind" => kind, "owner" => member ? owner : nil,
329
707
  "scope" => member ? scope_name(singleton) : nil, "name" => name,
330
708
  "span" => Lens.span(node.location), "reason" => "unsupported_kind" }
@@ -349,6 +727,7 @@ module Rigor
349
727
  def visibility(node, nesting, scope)
350
728
  return "private" if nesting.empty?
351
729
  return @node_visibility[node] if @node_visibility.key?(node)
730
+ return "private" if scope == :instance && node.is_a?(Prism::DefNode) && FORCED_PRIVATE.include?(node.name.to_s)
352
731
  return "public" if scope == :singleton && node.is_a?(Prism::DefNode) && node.receiver
353
732
 
354
733
  body = @bodies.last