rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -168,7 +168,7 @@ module Rigor
168
168
  end
169
169
 
170
170
  def classify_dynamic(type)
171
- type.static_facet.is_a?(Type::Top) ? :dynamic_top : :dynamic_specific
171
+ Type::Combinator.untyped?(type) ? :dynamic_top : :dynamic_specific
172
172
  end
173
173
 
174
174
  def worst_of(members)
@@ -2,7 +2,8 @@
2
2
 
3
3
  require_relative "../type"
4
4
  require_relative "external_ancestor_resolution"
5
- require_relative "../run_scoped_slots"
5
+ require_relative "../typing_state"
6
+ require_relative "../memo_audit"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -20,8 +21,14 @@ module Rigor
20
21
  NO_TARGETS = [].freeze
21
22
  private_constant :NO_TARGETS
22
23
 
23
- MEMO_KEY = RunScopedSlots.register(:__rigor_project_method_ownership_memo__)
24
- private_constant :MEMO_KEY
24
+ # ADR-129 WD1 — a run-scoped Memo keyed on the identity of the discovery index and the environment, the only
25
+ # inputs the buckets' answers read.
26
+ MEMO = TypingState.memo(
27
+ :project_method_ownership,
28
+ run_scoped: true, audit: ["project method defines", "block runs", "ancestry non-escaping"],
29
+ reason: "answer buckets keyed on the discovery index and environment identities"
30
+ )
31
+ private_constant :MEMO
25
32
 
26
33
  module_function
27
34
 
@@ -62,9 +69,15 @@ module Rigor
62
69
  return false if scope.nil?
63
70
 
64
71
  by_method = (memo(scope)[kind][class_name] ||= {})
65
- return by_method[method_sym] if by_method.key?(method_sym)
72
+ if by_method.key?(method_sym)
73
+ return MemoAudit.check("project method defines", by_method[method_sym], class_name, method_sym) do
74
+ compute_defines?(class_name, method_sym, kind, scope)
75
+ end
76
+ end
66
77
 
67
- by_method[method_sym] = compute_defines?(class_name, method_sym, kind, scope)
78
+ by_method[method_sym] = MemoAudit.key_audit("project method defines") do
79
+ compute_defines?(class_name, method_sym, kind, scope)
80
+ end
68
81
  end
69
82
 
70
83
  # One slot per thread, keyed on the identity of the scope's frozen discovery index and its environment —
@@ -72,17 +85,19 @@ module Rigor
72
85
  # changes: the shape of `ExpressionTyper#class_graph_buckets`, whose rationale applies unchanged. A
73
86
  # `Scope` merges each file's discovery with the project pre-pass, so the slot turns over per file, and
74
87
  # an ADR-46 dependency the first lookup recorded belongs to the file every later hit serves. Pool
75
- # workers are separate processes, and a Ractor has its own `Thread.current`. The `:ancestry` bucket
88
+ # workers are separate processes, and a Ractor has its own {TypingState}. The `:ancestry` bucket
76
89
  # belongs to {ClosureEscapeAnalyzer}'s ancestry step. The `:block_runs` bucket belongs to
77
90
  # {BlockRunCount}.
78
91
  def memo(scope)
92
+ return new_buckets if MemoAudit.enabled? && MemoAudit.suspended?
93
+
79
94
  discovery = scope.discovery
80
95
  environment = scope.environment
81
- slot = Thread.current[MEMO_KEY]
96
+ state = TypingState.current
97
+ slot = state[MEMO]
82
98
  unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
83
- buckets = { instance: {}, singleton: {}, ancestry: {}, block_runs: {}.compare_by_identity }
84
- slot = [discovery, environment, buckets]
85
- Thread.current[MEMO_KEY] = slot
99
+ slot = [discovery, environment, new_buckets]
100
+ state[MEMO] = slot
86
101
  end
87
102
  slot[2]
88
103
  end
@@ -90,6 +105,9 @@ module Rigor
90
105
  class << self
91
106
  private
92
107
 
108
+ # Empty buckets: a new slot's, and a throwaway set while an audit recomputation bypasses the memo.
109
+ def new_buckets = { instance: {}, singleton: {}, ancestry: {}, block_runs: {}.compare_by_identity }
110
+
93
111
  def class_target(class_name, kind)
94
112
  class_name.nil? ? nil : [[class_name.to_s, kind]]
95
113
  end
@@ -36,6 +36,9 @@ module Rigor
36
36
  # `farther` is set only on an entry {#lookup_through_ancestors} returns: the other definers of the name farther
37
37
  # on the walk (entries, or {SOURCE_DEFINER} for a project-source `def`), which {#callable?} reads before it
38
38
  # refuses the call.
39
+ #
40
+ # `kind` stays truthful on a copy {#singleton_copy_entry} returns: the `module_function` singleton copy is a
41
+ # `:singleton` method even though the body it copied is an instance `def`.
39
42
  Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type, :alias_of,
40
43
  :visibility, :farther) do
41
44
  def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil, alias_of: nil,
@@ -49,7 +52,7 @@ module Rigor
49
52
  # written, with no lexical resolution, as a patch's class name is.
50
53
  Mixin = Data.define(:owner, :module_name, :kind)
51
54
 
52
- attr_reader :by_key, :write_census, :mixins, :overrides
55
+ attr_reader :by_key, :write_census, :mixins, :overrides, :singleton_copies
53
56
 
54
57
  # Each owner's prepended and included modules, the most recent first.
55
58
  def self.mixins_by_owner(mixins)
@@ -69,7 +72,12 @@ module Rigor
69
72
  # @param overrides — issue #1708: `[class_name, method_name, kind] => visibility` for a named visibility call
70
73
  # (`public :kp` in `class Integer`) on a method the class inherits rather than binds, which Ruby copies onto
71
74
  # the class with that visibility.
72
- def initialize(entries: [], write_census: GlobalWriteCensus::EMPTY, mixins: [], overrides: {})
75
+ # @param singleton_copies — issue #2108: `[class_name, method_name]` pairs a `module_function :x` wrote for a
76
+ # name the class inherits rather than binds — Ruby's public singleton copy of the instance method the name
77
+ # resolves to. {#lookup_through_ancestors} answers a singleton-side call for one through
78
+ # {#singleton_copy_entry}.
79
+ def initialize(entries: [], write_census: GlobalWriteCensus::EMPTY, mixins: [], overrides: {},
80
+ singleton_copies: [])
73
81
  @by_key = entries.each_with_object({}) do |entry, acc|
74
82
  key = [entry.class_name, entry.method_name, entry.kind]
75
83
  acc[key] = entry
@@ -78,6 +86,7 @@ module Rigor
78
86
  @mixins = mixins.dup.freeze
79
87
  @mixins_of = ProjectPatchedMethods.mixins_by_owner(@mixins)
80
88
  @overrides = overrides.dup.freeze
89
+ @singleton_copies = singleton_copies.to_set.freeze
81
90
  freeze
82
91
  end
83
92
 
@@ -121,10 +130,18 @@ module Rigor
121
130
  end
122
131
 
123
132
  entry = @by_key[[owner, method_name, side]]
133
+ if entry.nil? && found.nil? && side == :singleton
134
+ copy = singleton_copy_entry(owner, method_name, rbs_loader, source_defines)
135
+ # Issue #2108 — a singleton copy is a real method on `#<Class:owner>` at this exact ancestor
136
+ # position: Ruby's lookup ends there, so nothing farther on the walk can answer `owner.x`.
137
+ # Return it now — collecting `farther` would only make {#callable?} decline the refusal Ruby
138
+ # makes of a private or protected copy.
139
+ return apply_override(copy, override) if copy
140
+ end
124
141
  if found
125
142
  farther << entry if entry
126
143
  elsif entry
127
- found = override ? entry.with(class_name: override.first, visibility: override.last) : entry
144
+ found = apply_override(entry, override)
128
145
  elsif (visibility = @overrides[[owner, method_name, side]])
129
146
  override ||= [owner, visibility]
130
147
  end
@@ -137,9 +154,12 @@ module Rigor
137
154
 
138
155
  # Issue #1708 — whether Ruby lets a call reach `entry`. A public method answers any call; a private one only a
139
156
  # call with no receiver or with `self` as it (`explicit` is false for those); a protected one also a call from
140
- # a method whose `self` is an instance of the entry's owner. `self_type` is the caller's `self` (`nil` at the
141
- # top level, where it is `main`, an Object); a `self` this cannot name (the default) may be one, so a
142
- # protected entry answers it.
157
+ # a method whose `self` is an instance of the entry's owner — for a `:singleton` entry, the owner itself or a
158
+ # class whose singleton chain holds it (`Kernel.a1` under `class << self; protected :a1` is refused from
159
+ # `main`, an Object, and from another class's singleton method, and answered from Kernel's own and from a
160
+ # `module_function` def of Kernel's, whose self is typed as an instance of Kernel though the copy runs with
161
+ # self = Kernel). `self_type` is the caller's `self` (`nil` at the top level, where it is `main`, an Object);
162
+ # a `self` this cannot name (the default) may be one, so a protected entry answers it.
143
163
  #
144
164
  # It never refuses a call some other definer of the name may answer: the walk's order is an approximation
145
165
  # (a module two owners mix in is spliced at the nearer one, where Ruby keeps the first include and skips the
@@ -152,7 +172,7 @@ module Rigor
152
172
  allowed =
153
173
  case entry.visibility
154
174
  when :private, :module_function then false
155
- when :protected then self_kind_of?(self_type, entry.class_name, rbs_loader)
175
+ when :protected then self_kind_of?(self_type, entry.class_name, entry.kind, rbs_loader)
156
176
  else true
157
177
  end
158
178
  return true if allowed
@@ -171,18 +191,50 @@ module Rigor
171
191
 
172
192
  private
173
193
 
174
- # Whether `self_type` may be an instance of `owner`: false only when the ancestors of a class RBS knows,
175
- # with the patches' mixins, do not hold it.
176
- def self_kind_of?(self_type, owner, rbs_loader)
177
- name, kind =
194
+ # Whether `self_type` may reach `owner`'s `entry_kind` method: false only when the ancestors of a class RBS
195
+ # knows, with the patches' mixins, do not hold `[owner, entry_kind]` — an instance of the owner for an
196
+ # instance method, the owner itself or a class whose singleton chain holds it for a singleton one. For a
197
+ # singleton entry a self whose class IS the owner answers too: a `module_function` / `extend self` def is
198
+ # typed `Nominal[owner]` though its singleton copy runs with self = owner, so `name == owner` may be the
199
+ # owner object itself (issue #3056).
200
+ def self_kind_of?(self_type, owner, entry_kind, rbs_loader)
201
+ name, side =
178
202
  if self_type.nil? then ["Object", :instance]
179
203
  elsif self_type.respond_to?(:class_name) && self_type.class_name
180
204
  [self_type.class_name.to_s, self_type.is_a?(Type::Singleton) ? :singleton : :instance]
181
205
  end
182
206
  return true if name.nil? || rbs_loader.nil?
183
207
 
184
- keys = ancestor_keys(name, kind, rbs_loader)
185
- keys == [[name, kind]] || keys.include?([owner, :instance])
208
+ keys = ancestor_keys(name, side, rbs_loader)
209
+ keys == [[name, side]] || keys.include?([owner, entry_kind]) ||
210
+ (entry_kind == :singleton && side == :instance && name == owner)
211
+ end
212
+
213
+ # `entry` as the walk's recorded `override` ([owner, visibility], from a nearer row's named
214
+ # visibility call) leaves it: the override's owner as `class_name` and its visibility — or `entry`
215
+ # itself when the walk passed no override on the way.
216
+ def apply_override(entry, override)
217
+ override ? entry.with(class_name: override.first, visibility: override.last) : entry
218
+ end
219
+
220
+ # Issue #2108 — the entry a `module_function :x` on a name `owner` inherits answers `owner.x` with. Ruby
221
+ # copies the instance method the name resolves to onto the owner's singleton as a public method, so the
222
+ # copy is the entry this walk finds for the name on the owner's INSTANCE ancestors, re-attributed to the
223
+ # owner, re-kinded `:singleton` and made public — unless a later named visibility call (`private_class_method
224
+ # :x`, or `private :x` in `class << self`) recorded the pair's singleton-side override, which Ruby applies to
225
+ # the copy. The instance walk's `farther` definers are dropped: the copy is a real method on
226
+ # `#<Class:owner>` that ends the singleton-side lookup, so no instance-chain definer can answer `owner.x`,
227
+ # and keeping them would only make {#callable?} decline a refusal Ruby makes. nil when no copy is
228
+ # recorded for `[owner, method_name]`, or when the instance walk finds no patch entry — an RBS or
229
+ # project-source method the registry does not hold, where the singleton walk then answers whatever else
230
+ # it reaches, as before.
231
+ def singleton_copy_entry(owner, method_name, rbs_loader, source_defines)
232
+ return nil unless @singleton_copies.include?([owner, method_name])
233
+
234
+ copy = lookup_through_ancestors(class_name: owner, method_name: method_name, kind: :instance,
235
+ rbs_loader: rbs_loader, source_defines: source_defines)
236
+ copy&.with(class_name: owner, kind: :singleton, farther: nil,
237
+ visibility: @overrides[[owner, method_name, :singleton]] || :public)
186
238
  end
187
239
 
188
240
  def ancestor_keys(class_name, kind, rbs_loader)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "prism"
4
4
 
5
+ require_relative "../typing_state"
5
6
  require_relative "project_patched_methods"
6
7
  require_relative "project_patched_definedness_guard"
7
8
  require_relative "project_patched_visibility"
@@ -45,6 +46,38 @@ module Rigor
45
46
  end
46
47
  private_constant :GuardedDef
47
48
 
49
+ # A `module_function` singleton copy a `def`-form produced (`module_function def x`, or a `def` under a
50
+ # bare `module_function`): `entry` is the copy to bind and `def_entry` the instance entry it was copied
51
+ # from, so {resolve_aliases} records which def the singleton binding answers — a later named
52
+ # `module_function :x` keeps the copy only while that def is proved unchanged (issue #3056).
53
+ SingletonCopy = Data.define(:entry, :def_entry) do
54
+ def source_path = entry.source_path
55
+ end
56
+ private_constant :SingletonCopy
57
+
58
+ # A statement the walk does not read that may rebind a method: any call other than a visibility call, a
59
+ # literal-named `alias_method`, or a receiverless `include` / `extend`, plus an `alias` with a computed
60
+ # name and an `undef`. `define_method(:x)`, `class_eval "def x"`, `send(:define_method, ...)` and
61
+ # `attr_reader :x` all make Ruby's next `module_function :x` copy a fresh public body, so after one
62
+ # {resolve_aliases} no longer assumes a copy is unchanged (issue #3056).
63
+ Unread = Data.define(:source_path)
64
+ private_constant :Unread
65
+
66
+ # The receiverless calls the walk reads as not rebinding a method of the body's own class.
67
+ MIXIN_CALLS = %i[include extend].freeze
68
+ private_constant :MIXIN_CALLS
69
+
70
+ # ADR-129 WD1 — the two per-file inputs of the walk, each a Frame {.walk_file} installs for the file's walk and
71
+ # restores on the way out: the `def`s on the undefined side of a definedness guard, and each `def`'s visibility.
72
+ # Both are frozen once built and read only by {.walk_node}'s callees.
73
+ #
74
+ # G1′: the walk is a Prism traversal that never types, so no `type_of` runs while either is installed.
75
+ GUARDED_DEFS = TypingState.frame(:patched_scanner_guarded_defs, reason: "installed per scanned file's walk",
76
+ typing_unreachable: true)
77
+ DEF_VISIBILITIES = TypingState.frame(:patched_scanner_def_visibilities, reason: "installed per scanned file",
78
+ typing_unreachable: true)
79
+ private_constant :GUARDED_DEFS, :DEF_VISIBILITIES
80
+
48
81
  module_function
49
82
 
50
83
  # @param paths — absolute paths to the pre-eval files. The runner has already
@@ -61,11 +94,13 @@ module Rigor
61
94
  paths.each { |path| scan_file(path, collected, diagnostics, buffer, census) }
62
95
  pending_mixins, collected = collected.partition { |item| item.is_a?(Mixins::Pending) }
63
96
  overrides = {}
64
- entries = resolve_aliases(collected, overrides)
97
+ singleton_copies = Set.new
98
+ entries = resolve_aliases(collected, overrides, singleton_copies)
65
99
  diagnostics.concat(duplicate_declaration_diagnostics(entries))
66
100
  Result.new(
67
101
  registry: ProjectPatchedMethods.new(entries: entries, write_census: census.freeze, overrides: overrides,
68
- mixins: Mixins.resolve(pending_mixins, entries)),
102
+ mixins: Mixins.resolve(pending_mixins, entries),
103
+ singleton_copies: singleton_copies),
69
104
  diagnostics: diagnostics
70
105
  )
71
106
  end
@@ -114,23 +149,37 @@ module Rigor
114
149
  # A {Visibility::Pending} (issue #1708) changes the visibility of the binding its name has when it runs, in the
115
150
  # file that bound it, so it is not a second declaration; a name no earlier patch binds on the class is one it
116
151
  # inherits, recorded in `overrides` ({Visibility.apply}). `module_function :x` also binds the public singleton
117
- # copy.
118
- def resolve_aliases(collected, overrides = {})
152
+ # copy: an entry {.applied} yields for a bound name, or a `[class_name, name]` pair in `singleton_copies`
153
+ # (issue #2108) for an inherited one, whose body the scan cannot name.
154
+ #
155
+ # `bound_defs` holds each key's def / alias-produced entry — the def identity a visibility change does not
156
+ # move — and `copy_sources` each singleton key's `module_function` copy source, the pair
157
+ # {Visibility.apply} reads to keep a repeated `module_function :x` from re-publicing a copy
158
+ # `private_class_method` made private (issue #3056).
159
+ def resolve_aliases(collected, overrides = {}, singleton_copies = Set.new)
119
160
  bound = {}
120
161
  homes = {}
162
+ bound_defs = {}
163
+ copy_sources = {}
121
164
  per_file = collected.group_by(&:source_path).values.map do |items|
122
165
  final = {}
123
166
  items.each do |item|
124
167
  if item.is_a?(Visibility::Pending)
125
- next Visibility.apply(item, bound, homes, overrides) { |copy| bind(copy, bound, homes, final) }
168
+ next Visibility.apply(item, bound, homes, overrides, singleton_copies, bound_defs, copy_sources) do |copy|
169
+ bind(copy, bound, homes, final)
170
+ end
126
171
  end
172
+ next if copy_provenance_item?(item, bound, homes, final, copy_sources)
127
173
 
128
174
  entry = item.is_a?(PendingAlias) ? alias_entry(item, bound) : item
129
175
  entry = entry.entry if entry.is_a?(GuardedDef)
130
176
  key = [entry.class_name, entry.method_name, entry.kind]
131
- next if item.is_a?(GuardedDef) && bound.key?(key)
177
+ # A guarded def dropped here may still run (`method_defined?` ignores a private method), so the
178
+ # copy it would rebind is no longer known to be unchanged.
179
+ next copy_sources.delete([key[0], key[1], :singleton]) if item.is_a?(GuardedDef) && bound.key?(key)
132
180
 
133
181
  bind(entry, bound, homes, final)
182
+ record_binding(key, entry, bound_defs, copy_sources)
134
183
  end
135
184
  final
136
185
  end
@@ -138,6 +187,33 @@ module Rigor
138
187
  end
139
188
  private_class_method :resolve_aliases
140
189
 
190
+ # The bookkeeping {resolve_aliases} keeps beside `bound`: `bound_defs` holds the def / alias
191
+ # entry `key` binds — the def identity a visibility change does not move, which
192
+ # {Visibility.apply}'s `module_function` re-copy check compares — and a singleton-side binding
193
+ # clears the copy provenance, since a `def self.x` between two `module_function :x` calls makes
194
+ # the next copy fresh (issue #3056).
195
+ def record_binding(key, entry, bound_defs, copy_sources)
196
+ bound_defs[key] = entry
197
+ copy_sources.delete([entry.class_name, entry.method_name, :singleton]) if entry.kind == :singleton
198
+ end
199
+ private_class_method :record_binding
200
+
201
+ # The two items that only move the `module_function` copy provenance {Visibility.apply}'s re-copy
202
+ # check compares (issue #3056), handled here and answered true: a {SingletonCopy} binds the copy and
203
+ # records which instance def entry it was made from, and an {Unread} statement forgets every record,
204
+ # since it may have rebound any name.
205
+ def copy_provenance_item?(item, bound, homes, final, copy_sources)
206
+ case item
207
+ when SingletonCopy
208
+ bind(item.entry, bound, homes, final)
209
+ copy_sources[[item.entry.class_name, item.entry.method_name, :singleton]] = item.def_entry
210
+ when Unread then copy_sources.clear
211
+ else return false
212
+ end
213
+ true
214
+ end
215
+ private_class_method :copy_provenance_item?
216
+
141
217
  def bind(entry, bound, homes, final)
142
218
  key = [entry.class_name, entry.method_name, entry.kind]
143
219
  bound[key] = entry
@@ -184,18 +260,16 @@ module Rigor
184
260
  "#{e.class}: #{e.message}. Pre-evaluation skipped for this file; " \
185
261
  "the rest of the run proceeds."
186
262
  )
187
- ensure
188
- Thread.current[:rigor_guarded_defs] = nil
189
- Thread.current[:rigor_def_visibilities] = nil
190
263
  end
191
264
  private_class_method :scan_file
192
265
 
193
266
  # Walks one parsed file with its definedness guards and `def` visibilities (issue #1708) in reach of
194
267
  # {record_def_node}.
195
268
  def walk_file(root, path, entries)
196
- Thread.current[:rigor_guarded_defs] = DefinednessGuard.defs(root)
197
- Thread.current[:rigor_def_visibilities] = Visibility.defs(root)
198
- walk_node(root, [], false, path, entries)
269
+ TypingState.current.with_pair(GUARDED_DEFS, DefinednessGuard.defs(root).freeze,
270
+ DEF_VISIBILITIES, Visibility.defs(root).freeze) do
271
+ walk_node(root, [], false, path, entries)
272
+ end
199
273
  end
200
274
  private_class_method :walk_file
201
275
 
@@ -231,17 +305,10 @@ module Rigor
231
305
  descend_singleton_class(node, qualified_prefix, source_path, entries)
232
306
  when Prism::DefNode
233
307
  record_def_node(node, qualified_prefix, in_singleton_class, source_path, entries)
234
- when Prism::AliasMethodNode
235
- record_alias(node, Source::AliasNames.keyword_names(node), qualified_prefix, in_singleton_class,
236
- source_path, entries)
308
+ when Prism::AliasMethodNode, Prism::UndefNode
309
+ record_keyword(node, qualified_prefix, in_singleton_class, source_path, entries)
237
310
  when Prism::CallNode
238
- record_alias(node, Source::AliasNames.alias_method_call_names(node), qualified_prefix,
239
- in_singleton_class, source_path, entries)
240
- if Thread.current[:rigor_def_visibilities]&.[](node).equal?(Visibility::STATEMENT)
241
- pending = Visibility.pending(node, qualified_prefix, in_singleton_class, source_path)
242
- entries << pending if pending
243
- entries.concat(Mixins.pending(node, qualified_prefix, in_singleton_class))
244
- end
311
+ record_call(node, qualified_prefix, in_singleton_class, source_path, entries)
245
312
  walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
246
313
  else
247
314
  walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
@@ -249,6 +316,34 @@ module Rigor
249
316
  end
250
317
  private_class_method :walk_node
251
318
 
319
+ # An `alias` with literal names binds as {record_alias} reads it; a computed-name `alias` or an `undef`
320
+ # is {Unread}.
321
+ def record_keyword(node, qualified_prefix, in_singleton_class, source_path, entries)
322
+ names = node.is_a?(Prism::AliasMethodNode) ? Source::AliasNames.keyword_names(node) : nil
323
+ entries << Unread.new(source_path: source_path) if names.nil?
324
+ record_alias(node, names, qualified_prefix, in_singleton_class, source_path, entries)
325
+ end
326
+ private_class_method :record_keyword
327
+
328
+ def record_call(node, qualified_prefix, in_singleton_class, source_path, entries)
329
+ names = Source::AliasNames.alias_method_call_names(node)
330
+ entries << Unread.new(source_path: source_path) if names.nil? && unread_call?(node)
331
+ record_alias(node, names, qualified_prefix, in_singleton_class, source_path, entries)
332
+ return unless TypingState.current[DEF_VISIBILITIES]&.[](node).equal?(Visibility::STATEMENT)
333
+
334
+ pending = Visibility.pending(node, qualified_prefix, in_singleton_class, source_path)
335
+ entries << pending if pending
336
+ entries.concat(Mixins.pending(node, qualified_prefix, in_singleton_class))
337
+ end
338
+ private_class_method :record_call
339
+
340
+ def unread_call?(node)
341
+ return false if Visibility.call(node)
342
+
343
+ !(MIXIN_CALLS.include?(node.name) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)))
344
+ end
345
+ private_class_method :unread_call?
346
+
252
347
  def walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
253
348
  node.rigor_each_child do |child|
254
349
  walk_node(child, qualified_prefix, in_singleton_class, source_path, entries)
@@ -288,9 +383,9 @@ module Rigor
288
383
  source_path: source_path, source_line: line,
289
384
  return_type: return_type
290
385
  )
291
- entry, *copies = Visibility.applied(entry, Thread.current[:rigor_def_visibilities]&.[](node) || :public)
292
- entries << (Thread.current[:rigor_guarded_defs]&.key?(node) ? GuardedDef.new(entry: entry) : entry)
293
- entries.concat(copies)
386
+ entry, *copies = Visibility.applied(entry, TypingState.current[DEF_VISIBILITIES]&.[](node) || :public)
387
+ entries << (TypingState.current[GUARDED_DEFS]&.key?(node) ? GuardedDef.new(entry: entry) : entry)
388
+ entries.concat(copies.map { |copy| SingletonCopy.new(entry: copy, def_entry: entry) })
294
389
  end
295
390
  private_class_method :record_def_node
296
391
 
@@ -98,20 +98,56 @@ module Rigor
98
98
  # second declaration. A name the class does not bind is one it inherits (`public :kp` in `class Integer` over
99
99
  # `module Kernel; private def kp`), which Ruby copies onto the class with the new visibility: `overrides` gets
100
100
  # `key => visibility` for {ProjectPatchedMethods#lookup_through_ancestors} to apply to the body it finds farther
101
- # on. Yields each copy that binds (`module_function :x`'s singleton), at the call's line, for the caller to
101
+ # on. `module_function :x` also binds the public singleton copy: for a name the class binds, {.applied} yields
102
+ # it to bind here; for an inherited one (issue #2108) `singleton_copies` records `[class_name, name]`, which
103
+ # {ProjectPatchedMethods#singleton_copy_entry} resolves through the owner's instance ancestors at call time
104
+ # (an approximation: Ruby snapshots the body when module_function runs)
105
+ # — the copy's body is whichever method the name reaches there, which the scan cannot name.
106
+ # Yields each copy that binds (`module_function :x`'s singleton), at the call's line, for the caller to
102
107
  # bind.
103
- def apply(pending, bound, homes, overrides)
108
+ #
109
+ # `bound_defs` maps each bound key to the entry a `def` / `alias` last produced for it — the def
110
+ # identity a visibility change (`private :x`) does not move — and `copy_sources` maps a singleton key
111
+ # to the instance def entry its `module_function` copy was made from. Ruby re-copies the instance
112
+ # method onto the singleton only when that def changed since the last copy (issue #3056): a
113
+ # `private_class_method :x` between two `module_function :x` calls keeps the copy private, while a
114
+ # `def x` redefinition, or any statement the scan does not read, makes the next copy public again.
115
+ def apply(pending, bound, homes, overrides, singleton_copies, bound_defs, copy_sources)
104
116
  pending.names.each do |name|
105
117
  key = [pending.class_name, name, pending.kind]
106
118
  entry = bound[key]
107
- next overrides[key] = pending.visibility if entry.nil?
119
+ if entry.nil?
120
+ overrides[key] = pending.visibility
121
+ singleton_copies << [pending.class_name, name] if pending.visibility == :module_function
122
+ next
123
+ end
108
124
 
109
125
  changed, *copies = applied(entry, pending.visibility)
126
+ copies = [] if unchanged_module_function_copy?(pending, key, bound_defs, copy_sources)
110
127
  bound[key] = homes[key][key] = changed
111
128
  copies.each { |copy| yield copy.with(source_path: pending.source_path, source_line: pending.source_line) }
112
129
  end
113
130
  end
114
131
 
132
+ # Issue #3056 — true only when `pending` is a `module_function` named call whose singleton copy is
133
+ # proved to answer the same instance def: the copy's recorded source is still the def / alias
134
+ # binding `key` has. `resolve_aliases` drops that record whenever something may have rebound the
135
+ # name (a singleton-side `def`, a guarded def it dropped, or any statement the walk does not read),
136
+ # and then Ruby's re-copy is public, the reading that refuses no call Ruby runs. `bound_defs[key]` is
137
+ # the def entry `key` binds — a visibility change does not move it — and `copy_sources[skey]` the
138
+ # instance def entry the singleton's current copy was made from; either way the source is
139
+ # (re)recorded for the next call.
140
+ def unchanged_module_function_copy?(pending, key, bound_defs, copy_sources)
141
+ return false unless pending.visibility == :module_function
142
+
143
+ skey = [key[0], key[1], :singleton]
144
+ def_entry = bound_defs[key]
145
+ source = copy_sources[skey]
146
+ copy_sources[skey] = def_entry
147
+ !source.nil? && source.equal?(def_entry)
148
+ end
149
+ private_class_method :unchanged_module_function_copy?
150
+
115
151
  # `entry` as `visibility` leaves it, then any copy that binds: `module_function` makes an instance method
116
152
  # private and binds a public singleton copy, as Ruby does.
117
153
  def applied(entry, visibility)