rigortype 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +100 -18
  4. data/docs/manual/02-cli-reference.md +85 -16
  5. data/docs/manual/03-configuration.md +12 -9
  6. data/docs/manual/04-diagnostics.md +17 -6
  7. data/docs/manual/05-inspecting-types.md +2 -2
  8. data/docs/manual/06-baseline.md +1 -1
  9. data/docs/manual/07-plugins.md +2 -1
  10. data/docs/manual/08-skills.md +7 -0
  11. data/docs/manual/10-mcp-server.md +17 -2
  12. data/docs/manual/12-caching.md +8 -1
  13. data/docs/manual/18-removing-dead-code.md +18 -7
  14. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  15. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  16. data/docs/manual/plugins/rigor-devise.md +15 -0
  17. data/docs/manual/plugins/rigor-rspec.md +102 -1
  18. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  19. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  20. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  21. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  22. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
  23. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
  24. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  25. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  26. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  27. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  28. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  29. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
  30. data/lib/rigor/analysis/check_rules.rb +319 -21
  31. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/edit_replay.rb +67 -0
  34. data/lib/rigor/analysis/incremental.rb +86 -0
  35. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  36. data/lib/rigor/analysis/incremental_session.rb +336 -46
  37. data/lib/rigor/analysis/path_expansion.rb +44 -1
  38. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  39. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  40. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  41. data/lib/rigor/analysis/rule_catalog.rb +22 -5
  42. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  43. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  44. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  48. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  49. data/lib/rigor/analysis/runner.rb +222 -111
  50. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  51. data/lib/rigor/analysis/worker_session.rb +24 -1
  52. data/lib/rigor/cache/descriptor.rb +40 -7
  53. data/lib/rigor/cache/engine_source.rb +3 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +49 -8
  55. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  56. data/lib/rigor/cache/store.rb +4 -3
  57. data/lib/rigor/cli/check_command.rb +93 -4
  58. data/lib/rigor/cli/command.rb +3 -1
  59. data/lib/rigor/cli/diff_command.rb +2 -1
  60. data/lib/rigor/cli/docs_command.rb +3 -2
  61. data/lib/rigor/cli/doctor_command.rb +1 -1
  62. data/lib/rigor/cli/explain_command.rb +3 -2
  63. data/lib/rigor/cli/lens_command.rb +5 -5
  64. data/lib/rigor/cli/mcp_command.rb +2 -1
  65. data/lib/rigor/cli/options.rb +20 -3
  66. data/lib/rigor/cli/plugin_command.rb +2 -1
  67. data/lib/rigor/cli/plugins_command.rb +7 -1
  68. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  69. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  70. data/lib/rigor/cli/sig_gen_command.rb +73 -13
  71. data/lib/rigor/cli/skill_command.rb +2 -1
  72. data/lib/rigor/cli/skill_describe.rb +3 -2
  73. data/lib/rigor/cli/type_of_command.rb +2 -1
  74. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  75. data/lib/rigor/cli/unused_command.rb +10 -7
  76. data/lib/rigor/cli.rb +3 -2
  77. data/lib/rigor/config_audit.rb +72 -16
  78. data/lib/rigor/configuration/severity_profile.rb +20 -2
  79. data/lib/rigor/configuration.rb +43 -7
  80. data/lib/rigor/effects/signature_sources.rb +3 -1
  81. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  82. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  83. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  84. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  85. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  86. data/lib/rigor/environment/rbs_loader.rb +136 -7
  87. data/lib/rigor/environment/required_features.rb +136 -29
  88. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  89. data/lib/rigor/environment.rb +4 -2
  90. data/lib/rigor/inference/block_call_timing.rb +7 -1
  91. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  92. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  93. data/lib/rigor/inference/captured_locals.rb +44 -13
  94. data/lib/rigor/inference/clause_widening.rb +90 -0
  95. data/lib/rigor/inference/deep_erasure.rb +138 -0
  96. data/lib/rigor/inference/def_return_typer.rb +159 -9
  97. data/lib/rigor/inference/definer_resolution.rb +101 -20
  98. data/lib/rigor/inference/error_info.rb +17 -3
  99. data/lib/rigor/inference/expression_typer.rb +97 -18
  100. data/lib/rigor/inference/global_write_census.rb +36 -1
  101. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  102. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  103. data/lib/rigor/inference/loop_value_record.rb +53 -0
  104. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  105. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  106. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
  107. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  108. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  109. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  110. data/lib/rigor/inference/method_dispatcher.rb +113 -19
  111. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  112. data/lib/rigor/inference/module_function_state.rb +149 -42
  113. data/lib/rigor/inference/mutation_widening.rb +5 -2
  114. data/lib/rigor/inference/narrowing.rb +116 -11
  115. data/lib/rigor/inference/object_mixins.rb +13 -1
  116. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  117. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  118. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  119. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  120. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  121. data/lib/rigor/inference/project_seed_tables.rb +121 -0
  122. data/lib/rigor/inference/project_yielder.rb +128 -0
  123. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  124. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  125. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  126. data/lib/rigor/inference/return_barrier.rb +16 -0
  127. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  128. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  129. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  130. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  131. data/lib/rigor/inference/scope_indexer.rb +1144 -197
  132. data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
  133. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  134. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  135. data/lib/rigor/inference/statement_evaluator.rb +1088 -213
  136. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  137. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  138. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  139. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  140. data/lib/rigor/language_server/buffer_table.rb +6 -0
  141. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  142. data/lib/rigor/language_server/project_context.rb +59 -3
  143. data/lib/rigor/language_server/server.rb +5 -3
  144. data/lib/rigor/lens/contract_reader.rb +45 -2
  145. data/lib/rigor/lens/document.rb +2 -1
  146. data/lib/rigor/lens/file_reader.rb +12 -2
  147. data/lib/rigor/lens/members.rb +16 -2
  148. data/lib/rigor/lens/run.rb +34 -7
  149. data/lib/rigor/mcp/server.rb +11 -3
  150. data/lib/rigor/plugin/base.rb +29 -13
  151. data/lib/rigor/plugin/io_boundary.rb +42 -6
  152. data/lib/rigor/plugin/isolation.rb +4 -3
  153. data/lib/rigor/plugin/manifest.rb +29 -3
  154. data/lib/rigor/plugin/registry.rb +38 -0
  155. data/lib/rigor/protection/discovery_seed.rb +64 -36
  156. data/lib/rigor/protection/mutation_cache.rb +8 -2
  157. data/lib/rigor/rooted_glob.rb +36 -0
  158. data/lib/rigor/scope/discovery_index.rb +5 -0
  159. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  160. data/lib/rigor/scope/resolution_chain.rb +40 -5
  161. data/lib/rigor/scope.rb +59 -7
  162. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  163. data/lib/rigor/sig_gen/classification.rb +4 -1
  164. data/lib/rigor/sig_gen/generator.rb +465 -49
  165. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  166. data/lib/rigor/sig_gen/observation_collector.rb +141 -12
  167. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  168. data/lib/rigor/sig_gen/renderer.rb +16 -155
  169. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  170. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  171. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  172. data/lib/rigor/sig_gen/write_result.rb +3 -1
  173. data/lib/rigor/sig_gen/writer.rb +5 -3
  174. data/lib/rigor/signature_path_audit.rb +4 -2
  175. data/lib/rigor/type/combinator.rb +4 -3
  176. data/lib/rigor/type/constant.rb +54 -3
  177. data/lib/rigor/type/hash_shape.rb +14 -9
  178. data/lib/rigor/type/struct_instance.rb +3 -2
  179. data/lib/rigor/version.rb +1 -1
  180. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  181. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
  182. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  183. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  184. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  185. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  186. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  187. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  188. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  189. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  190. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  191. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  192. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  193. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  194. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  195. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  196. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  197. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  198. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  199. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  200. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  201. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  202. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  203. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  204. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  205. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  206. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  207. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  208. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  209. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  210. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  211. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  212. data/sig/rigor/analysis/baseline.rbs +3 -2
  213. data/sig/rigor/effects/file_collection.rbs +5 -6
  214. data/sig/rigor/effects/registry.rbs +8 -6
  215. data/sig/rigor/effects/summary.rbs +6 -5
  216. data/sig/rigor/environment.rbs +5 -3
  217. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  218. data/sig/rigor/inference.rbs +6 -0
  219. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  220. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  221. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  222. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  223. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  224. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  225. data/sig/rigor/plugin/manifest.rbs +16 -18
  226. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  227. data/sig/rigor/plugin/registry.rbs +1 -1
  228. data/sig/rigor/rooted_glob.rbs +7 -0
  229. data/sig/rigor/scope.rbs +34 -52
  230. data/sig/rigor/type.rbs +0 -6
  231. data/skills/rigor-doctor/references/01-checks.md +23 -11
  232. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  233. metadata +32 -1
@@ -220,7 +220,8 @@ module Rigor
220
220
  # `call.undefined-method`.
221
221
  patched_result =
222
222
  unless skip_project_patched
223
- try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
223
+ try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment,
224
+ call_node: call_node, scope: scope)
224
225
  end
225
226
  if patched_result
226
227
  scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
@@ -258,6 +259,22 @@ module Rigor
258
259
  return discovered_result
259
260
  end
260
261
 
262
+ # Issue #1708 — a `pre_eval:` patch on one of the receiver's ancestors (`class Object; def zz`,
263
+ # `module Enumerable; def sum_by`). Below the RBS tier, whose decline means no nearer RBS declares the
264
+ # method, and below the tiers that answer the receiver's own patch or a gem catalogue entry. A nearer
265
+ # `def` in the project's source (on an RBS-known receiver, which the discovered-method tier leaves to
266
+ # user-method inference) stops the walk: see {#project_source_defines?}. That probe reads the scope's def
267
+ # index, so a single-file scope (`type-of`, `annotate`, an LSP buffer) cannot see a `def` in another file.
268
+ ancestor_patched =
269
+ if !skip_project_patched && scope
270
+ try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment,
271
+ call_node: call_node, scope: scope, through_ancestors: true)
272
+ end
273
+ if ancestor_patched
274
+ scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
275
+ return ancestor_patched
276
+ end
277
+
261
278
  # ADR-5 robustness — synthesized-stub-type tier. When the receiver is a type Rigor
262
279
  # invented to make an otherwise-unbuildable project signature resolve (a
263
280
  # missing-namespace module, or a stub for a referenced-but-undeclared type like an
@@ -518,10 +535,9 @@ module Rigor
518
535
  # The question is asked HERE, of the same two tiers `resolve` consults and in the same order, rather
519
536
  # than reconstructed in the veto: a name this reports as answered is a name the dispatch really would
520
537
  # resolve, and no plugin knowledge moves into the engine's veto. The tiers are the gated
521
- # `dynamic_return` walk and the ADR-16 synthetic-method index (Tier C `heredoc_templates` emissions
522
- # today; the Tier B `Plugin::Macro::TraitRegistry` explosions share that table but cannot populate it
523
- # until #476 threads a real environment into the pre-pass that builds it, so no trait member reaches
524
- # this predicate in production yet — it will when #476 lands, with no change here).
538
+ # `dynamic_return` walk and the ADR-16 synthetic-method index (Tier C `heredoc_templates` emissions and
539
+ # the Tier B `Plugin::Macro::TraitRegistry` explosions, which share that table; since #476 the pre-pass
540
+ # explodes a trait module whenever the project's RBS declares it).
525
541
  #
526
542
  # It is a predicate, not a type: the veto's job is to stop the top-level bind, after which the call
527
543
  # falls through to the ordinary dispatch chain and the tier that answered here answers there too. The
@@ -644,7 +660,13 @@ module Rigor
644
660
  # `def to_url; "hello"; end` patched onto `String` now resolves `s.to_url` to
645
661
  # `Dynamic[Nominal[String]]` instead of the pre-3a `Dynamic[Top]`. Falls back to
646
662
  # `Dynamic[Top]` when the heuristic declined (non-literal tail expression).
647
- def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment)
663
+ #
664
+ # `through_ancestors:` (issue #1708), with a scope, searches the receiver's ancestors as well, through
665
+ # {ProjectPatchedMethods#lookup_through_ancestors}, stopping at a nearer project-source `def`. A private or
666
+ # protected patch Ruby would not let the call reach ({ProjectPatchedMethods#callable?}) answers nothing, so
667
+ # `1.kp` on `module Kernel; private def kp` reads as the undefined call it is.
668
+ def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment, # rubocop:disable Metrics/ParameterLists
669
+ call_node: nil, scope: nil, through_ancestors: false)
648
670
  registry = environment&.project_patched_methods
649
671
  return nil if registry.nil? || registry.empty?
650
672
 
@@ -652,14 +674,55 @@ module Rigor
652
674
  return nil if class_name.nil?
653
675
 
654
676
  kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
655
- entry = registry.lookup(class_name: class_name, method_name: method_name, kind: kind)
677
+ entry = project_patched_entry(registry, class_name, method_name, kind, environment,
678
+ scope: scope, through_ancestors: through_ancestors)
656
679
  return nil if entry.nil?
680
+
681
+ verdict = registry.callable?(entry, explicit: explicit_non_self_receiver?(call_node),
682
+ self_type: scope ? scope.self_type : :unknown,
683
+ rbs_loader: environment.rbs_loader)
684
+ return nil if verdict == false
685
+ return Type::Combinator.untyped if verdict.nil?
657
686
  return Type::Combinator.dynamic(entry.return_type) if entry.return_type
658
687
 
659
688
  aliased = project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
660
689
  aliased ? Type::Combinator.dynamic(aliased) : Type::Combinator.untyped
661
690
  end
662
691
 
692
+ def project_patched_entry(registry, class_name, method_name, kind, environment, scope:, through_ancestors:)
693
+ return registry.lookup(class_name: class_name, method_name: method_name, kind: kind) unless through_ancestors
694
+
695
+ registry.lookup_through_ancestors(
696
+ class_name: class_name, method_name: method_name, kind: kind, rbs_loader: environment.rbs_loader,
697
+ source_defines: ->(owner, side) { project_source_defines?(scope, owner, method_name, side) }
698
+ )
699
+ end
700
+
701
+ # Issue #1708 — does the project's own source define `method_name` on `owner` (`side` as the ancestor walk
702
+ # reaches it), or on a module the project prepends to or includes into it? Ruby runs that `def` ahead of a
703
+ # patch on a farther ancestor: `class Integer; def nn = "s"` beats `class Object; def nn = nil` in a
704
+ # `pre_eval:` file. The owner's level of its {Scope::ResolutionChain} (its prepends, itself, its includes
705
+ # and their own mixins, or a class object's singleton and its extends) holds those definers; whether one
706
+ # of them defines the name does not depend on their order, so the chain is searched without settling it.
707
+ def project_source_defines?(scope, owner, method_name, side)
708
+ return false unless scope.known_user_class?(owner)
709
+
710
+ chain = Scope::ResolutionChain.for(scope, owner, side, :methods)
711
+ own_level_end = chain.level_starts[1] || chain.entries.size
712
+ found = chain.search(scope, 0, own_level_end) do |entry|
713
+ !entry.external? && source_def?(scope, entry.name, method_name, entry.side)
714
+ end
715
+ !found.nil?
716
+ end
717
+
718
+ def source_def?(scope, owner, method_name, side)
719
+ if side == :singleton
720
+ scope.discovered_method?(owner, method_name, :singleton) || !scope.singleton_def_for(owner, method_name).nil?
721
+ else
722
+ scope.discovered_method?(owner, method_name, :instance) || !scope.user_def_for(owner, method_name).nil?
723
+ end
724
+ end
725
+
663
726
  # Issue #1702 — an alias in a `pre_eval:` patch whose old name no earlier patch binding defines
664
727
  # (`alias old_plus +` on Integer, or `alias_method :orig_succ, :succ` ahead of the patch's own `def succ`)
665
728
  # names the method the class had before the patch, so it answers what that method answers for the same
@@ -967,6 +1030,7 @@ module Rigor
967
1030
 
968
1031
  fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
969
1032
  return nil if fallback_receiver.nil?
1033
+ return nil if declared_by_external_ancestor?(receiver_type, environment, context)
970
1034
 
971
1035
  # Preserve the ORIGINAL receiver type as the `self` substitution so `Kernel#dup: () ->
972
1036
  # self` and other `self`-returning methods route through Object's RBS while still
@@ -991,6 +1055,45 @@ module Rigor
991
1055
  )
992
1056
  end
993
1057
 
1058
+ # Issue #2196 — a project class whose ancestry reaches a core or stdlib class before `Object` (`class FL <
1059
+ # Array`) answers a name that class declares from that class, not from `Object` / `Kernel`: Ruby dispatches
1060
+ # `select { … }` on an `FL` to `Array#select`, never to the private `Kernel#select` (`-> Array[String]`). The
1061
+ # ancestor tiers decline such a name where they cannot type it (`RbsDispatch#returns_the_walked_ancestry?`),
1062
+ # and the `Object` fallback then fabricated `Kernel#select`'s answer, so `fields.detect { |f| f.errors }` read
1063
+ # `f` as `String`. The fallback declines the name instead, which leaves the call `Dynamic[top]`.
1064
+ # {ExternalAncestorResolution.resolve} answers nil for a name only `Object`, `Kernel` or `BasicObject` declares,
1065
+ # so `puts` and `raise` keep resolving.
1066
+ def declared_by_external_ancestor?(receiver_type, environment, context)
1067
+ return false unless receiver_type.is_a?(Type::Nominal)
1068
+
1069
+ scope = context.scope
1070
+ return false if scope.nil?
1071
+
1072
+ memo = external_ancestor_memo(scope, environment)
1073
+ key = [receiver_type.class_name, context.method_name]
1074
+ return memo[key] if memo.key?(key)
1075
+
1076
+ memo[key] = !ExternalAncestorResolution.resolve(receiver_type.class_name, context.method_name, :instance,
1077
+ scope: scope, environment: environment,
1078
+ record_dependencies: false).nil?
1079
+ end
1080
+
1081
+ EXTERNAL_ANCESTOR_MEMO_KEY = :__rigor_user_class_fallback_external_ancestor__
1082
+ private_constant :EXTERNAL_ANCESTOR_MEMO_KEY
1083
+
1084
+ # One slot per discovery index and environment, as `RbsDispatch#ancestor_memo_slot` keeps its own: the answer
1085
+ # reads only the frozen discovery tables and the RBS environment, and its walk is withheld from dependency
1086
+ # recording, so it may be served again for the same pair.
1087
+ def external_ancestor_memo(scope, environment)
1088
+ discovery = scope.discovery
1089
+ slot = Thread.current[EXTERNAL_ANCESTOR_MEMO_KEY]
1090
+ unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
1091
+ slot = [discovery, environment, {}]
1092
+ Thread.current[EXTERNAL_ANCESTOR_MEMO_KEY] = slot
1093
+ end
1094
+ slot[2]
1095
+ end
1096
+
994
1097
  # True when the call node carries an explicit receiver that is not the literal `self`.
995
1098
  # Such a call cannot legally dispatch to a private method, so the user-class fallback must
996
1099
  # skip private signatures rather than return a confidently-wrong type. Returns false for
@@ -1193,7 +1296,7 @@ module Rigor
1193
1296
  range_lift = range_new_lift(receiver_type.class_name, arg_types)
1194
1297
  return range_lift if range_lift
1195
1298
 
1196
- set_lift = set_new_lift(receiver_type.class_name, arg_types)
1299
+ set_lift = set_new_lift(receiver_type.class_name, arg_types, context)
1197
1300
  return set_lift if set_lift
1198
1301
 
1199
1302
  hash_lift = hash_new_lift(receiver_type.class_name, arg_types, context&.call_node)
@@ -1549,19 +1652,10 @@ module Rigor
1549
1652
  # are given or the single argument is a `Tuple` whose every element is a `Constant[T]`.
1550
1653
  # Mirrors `SetFolding#fold_new` but lives here so the `:new` path in
1551
1654
  # `try_meta_introspection` can reach it before the RBS tier answers `Nominal[Set]`.
1552
- def set_new_lift(class_name, arg_types)
1655
+ def set_new_lift(class_name, arg_types, context = nil)
1553
1656
  return nil unless class_name == "Set"
1554
- return Type::Combinator.constant_of(::Set.new) if arg_types.empty?
1555
- return nil if arg_types.size > 1
1556
-
1557
- arg = arg_types.first
1558
- return nil unless arg.is_a?(Type::Tuple)
1559
- return nil unless arg.elements.all?(Type::Constant)
1560
1657
 
1561
- values = arg.elements.map(&:value)
1562
- Type::Combinator.constant_of(::Set.new(values))
1563
- rescue StandardError
1564
- nil
1658
+ SetFolding.fold_new(arg_types, context&.call_node, context&.block_type)
1565
1659
  end
1566
1660
 
1567
1661
  # `Regexp.new(pattern)` / `Regexp.new(pattern, opts)` — folds to `Constant[Regexp]` when
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../../source/literals"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module ModuleFunctionState
9
+ # Issue #2209 — the `module_function` spellings other than the receiverless call that the deferred-ranges
10
+ # prescan ({ModuleFunctionState.prescan_deferred}) reads: `self.module_function(...)`, and
11
+ # `send(:module_function, ...)` or `__send__(:module_function, ...)` on no receiver or `self`, which Ruby runs
12
+ # as the receiverless call. `public_send` raises on a private method such as `module_function`, so it is not
13
+ # one.
14
+ #
15
+ # A literal name gives the `:singleton` row and a `def` argument the `:both` or `:singleton` row a receiverless
16
+ # call gives. The bare form, and an argument that is neither (`*names`, a local), gives an {ANY_ROW_NAME} row
17
+ # over the call: the prescan reads neither the toggle these spellings set nor such a name, so every singleton
18
+ # copy of the owner is uncertain (`DefinerResolution.module_function_row?`).
19
+ module Spellings
20
+ SEND_CALLS = %i[send __send__].freeze
21
+ private_constant :SEND_CALLS
22
+
23
+ module_function
24
+
25
+ # The arguments the spelling `node` passes `module_function`, or nil for any other call.
26
+ def arguments(node)
27
+ arguments = node.arguments&.arguments || []
28
+ return arguments if node.name == :module_function && node.receiver.is_a?(Prism::SelfNode)
29
+ return nil unless SEND_CALLS.include?(node.name)
30
+ return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
31
+ return nil unless Source::Literals.symbol_or_string(arguments.first) == :module_function
32
+
33
+ arguments.drop(1)
34
+ end
35
+
36
+ # Appends the rows of the spelling `node` with `arguments` ({.arguments}) to `ranges`, and prescans the rest
37
+ # as {ModuleFunctionState.prescan_deferred} prescans a receiverless call's.
38
+ def prescan(node, arguments, qualified_prefix, in_singleton_class, offsets, ranges)
39
+ owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
40
+ call_span = [node.location.start_offset, node.location.end_offset]
41
+ any = arguments.empty?
42
+ arguments.each do |arg|
43
+ if arg.is_a?(Prism::DefNode)
44
+ kind = ScopeIndexer.def_singleton?(arg, qualified_prefix, in_singleton_class) ? :singleton : :both
45
+ ranges << [arg.location.start_offset, arg.location.end_offset, arg.name, kind, owner]
46
+ elsif (name = Source::Literals.symbol_or_string(arg))
47
+ ranges << [*call_span, name, :singleton, owner]
48
+ else
49
+ any = true
50
+ nested_prescan(arg, qualified_prefix, in_singleton_class, offsets, ranges)
51
+ end
52
+ end
53
+ ranges << [*call_span, ANY_ROW_NAME, :singleton, owner] if any
54
+ nested_prescan(node.block, qualified_prefix, in_singleton_class, offsets, ranges) if node.block
55
+ end
56
+
57
+ def nested_prescan(node, qualified_prefix, in_singleton_class, offsets, ranges)
58
+ ModuleFunctionState.prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges,
59
+ nested: true)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -9,24 +9,36 @@ module Rigor
9
9
  # The one home for what a receiverless `module_function` call does to the methods around it
10
10
  # ([#1507](https://github.com/rigortype/rigor/issues/1507),
11
11
  # [#1197](https://github.com/rigortype/rigor/issues/1197)). Four readers used to compute it in four
12
- # places. They disagree with each other and, in places, with Ruby. Each entry point below reproduces
13
- # one reader's answer exactly: this module gives the readings one place to live, and does not yet make
14
- # them agree. A change to one reading lands here, beside the other three. An example is the reset a bare
15
- # `public`, `private` or `protected` makes.
12
+ # places, and the method-visibility table reads it too. They disagree with each other and, in places, with
13
+ # Ruby. Each entry point below reproduces one reader's answer exactly: this module gives the readings one
14
+ # place to live, and does not yet make them agree. A change to one reading lands here, beside the others.
15
+ #
16
+ # In Ruby a bare `module_function` call turns the body's module_function mode on, and a later bare
17
+ # `public`, `private` or `protected` ({.visibility_reset?}) turns it off again (#1968). A named call
18
+ # (`public :x`, `private def y`) leaves the mode alone, and each `module` body starts with it off. A reset
19
+ # written in the body's own statements, or in a plain `begin` or `ensure` there, runs. One nested anywhere
20
+ # else (control flow, a block, a `rescue` clause, an argument) may not, so the mode after it is uncertain.
21
+ # A reset in a nested `class`, `module`, `class <<` or `def` body, a `*_eval` / `*_exec` block or a
22
+ # class-creating block (`Class.new do`, `Struct.new do`, ...) belongs to another scope and does not count.
16
23
  #
17
24
  # - The singleton def-node table (`ScopeIndexer#walk_singleton_body`) reads
18
25
  # {.each_singleton_sibling} and {.each_singleton_copy}. A bare call covers the later direct
19
- # statements of its own body and is never reset.
26
+ # statements of its own body, up to a reset; after an uncertain one the copy is possible.
20
27
  # - The deferred ranges (`ScopeIndexer#walk_deferred_body`) read {.prescan_deferred} and
21
- # {.deferred_module_function?}. A bare call covers every later def by offset, and a call inside
22
- # control flow or a block counts.
28
+ # {.deferred_module_function?}. A bare call covers every later def by offset, up to a later reset,
29
+ # and a call inside control flow or a block counts. An uncertain reset changes nothing here.
23
30
  # - The extends table (`ScopeIndexer#record_extend_call`) reads {.extends_self?}. A bare call covers
24
- # every instance def of the module, in any order.
31
+ # every instance def of the module, in any order, and is never reset.
25
32
  # - sig-gen (`SigGen::Generator#walk_statements`) reads {.each_sig_gen_statement} and
26
33
  # {.sig_gen_module_function?}. A bare call covers the later statements of its own statement list and
27
- # what nests in them.
34
+ # what nests in them, up to a reset; after an uncertain one {.sig_gen_uncertain?} says so.
35
+ # - The method-visibility table (`ScopeIndexer#walk_method_visibilities`) reads {.instance_visibility}. It
36
+ # applies a `module_function` call as it applies a `private` one, bare or named: the call makes the instance
37
+ # copy private (#1569), and a later bare `public`, `private` or `protected` sets the running default as it
38
+ # would anyway, so the reset needs no reading of its own. A call or reset that may not run leaves the slots
39
+ # after it contested, as any visibility call there does.
28
40
  #
29
- # A fifth reader does not come here yet: `rigor-activerecord`'s `ModelDiscoverer#table_name_decorators`
41
+ # Another reader does not come here yet: `rigor-activerecord`'s `ModelDiscoverer#table_name_decorators`
30
42
  # (`plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb`) reads a module body's
31
43
  # direct statements for its table-name readers. It also accepts `self.module_function`, and there a bare
32
44
  # call covers the instance methods written before it as well as after it, while a named call takes only a
@@ -47,23 +59,60 @@ module Rigor
47
59
  node.name == :module_function && node.receiver.nil?
48
60
  end
49
61
 
62
+ # The visibility a `module_function` call gives the instance method of each name it covers, bare or named:
63
+ # Ruby makes the instance copy private and adds a public singleton copy, which the instance-side
64
+ # method-visibility table does not hold (#1569).
65
+ def instance_visibility = :private
66
+
50
67
  # The toggle form: a `module_function` call with no arguments.
51
68
  def bare?(node)
52
69
  node.arguments.nil? || node.arguments.arguments.empty?
53
70
  end
54
71
 
72
+ # A bare `public`, `private` or `protected` call, receiverless or on `self`: Ruby ends the module_function
73
+ # mode there (#1968). A call with arguments sets the named methods' visibility and leaves the mode alone.
74
+ def visibility_reset?(node)
75
+ %i[public private protected].include?(node.name) && bare?(node) &&
76
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
77
+ end
78
+
79
+ # The mode after `stmt`, a direct statement of a body, for the readers that walk the statements: `mode`
80
+ # is `true`, `false` or `:uncertain` before it. A {.visibility_reset?} the statement runs ends the mode,
81
+ # and one it may not run (`:maybe_off`, below {.prescan_deferred}) makes an on mode uncertain. A
82
+ # `module_function` nested in the statement is not read here, as before. `nested` says the statement
83
+ # itself may not run, so every reset in it may not either.
84
+ def mode_after(mode, stmt, nested: false)
85
+ return mode unless mode
86
+
87
+ events = []
88
+ prescan_deferred(stmt, EMPTY_PREFIX, false, events, [], nested: nested)
89
+ events.sort_by(&:first).each do |_offset, event|
90
+ case event
91
+ when :off then mode = false
92
+ when :maybe_off then mode = :uncertain if mode == true
93
+ end
94
+ end
95
+ mode
96
+ end
97
+
98
+ # The owner prefix {.mode_after} hands the prescan, which records no row a reset needs.
99
+ EMPTY_PREFIX = [].freeze
100
+
55
101
  # The singleton def-node table's reading, over one class-ish body's direct statements in source
56
102
  # order. `ScopeIndexer#statements_of` builds that list, folding a body-level `rescue`, `else` or
57
103
  # `ensure` clause into it. Yields `stmt, module_function_on, named_call` for every statement except a
58
104
  # bare call:
59
105
  #
60
- # - `module_function_on` says whether a bare call precedes the statement. The toggle never switches
61
- # off, although a later bare `public`, `private` or `protected` resets it in Ruby.
106
+ # - `module_function_on` is `true` when a bare call precedes the statement with no reset between them,
107
+ # `:uncertain` when a reset that may not run is between them ({.mode_after}), and `false` otherwise.
62
108
  # - `named_call` is true for a named call (`module_function :a` or `module_function def x`), which
63
109
  # {.each_singleton_copy} resolves; the caller records nothing else for it.
64
110
  #
65
- # A call nested in control flow or a block is not seen.
66
- def each_singleton_sibling(statements)
111
+ # A `module_function` call nested in control flow or a block is not seen. `conditional` lists the
112
+ # statements of a body-level `rescue` or `else` clause ({.conditional_clause_statements}), which may not
113
+ # run, so a reset there makes the mode uncertain rather than ending it.
114
+ def each_singleton_sibling(statements, conditional = [])
115
+ conditional = Set.new(conditional).compare_by_identity
67
116
  module_function_on = false
68
117
  statements.each do |stmt|
69
118
  if stmt.is_a?(Prism::CallNode) && call?(stmt)
@@ -75,7 +124,24 @@ module Rigor
75
124
  next
76
125
  end
77
126
  yield stmt, module_function_on, false
127
+ module_function_on = mode_after(module_function_on, stmt, nested: conditional.include?(stmt))
128
+ end
129
+ end
130
+
131
+ # The statements of a body-level `rescue` or `else` clause, which `ScopeIndexer#statements_of` folds into
132
+ # the body's direct statements although they run only on an exception, or only without one. An `ensure`
133
+ # clause runs either way and is not listed.
134
+ def conditional_clause_statements(body)
135
+ return [] unless body.is_a?(Prism::BeginNode)
136
+
137
+ statements = []
138
+ clause = body.rescue_clause
139
+ while clause
140
+ statements.concat(ScopeIndexer.statements_of(clause.statements))
141
+ clause = clause.subsequent
78
142
  end
143
+ statements.concat(ScopeIndexer.statements_of(body.else_clause.statements)) if body.else_clause
144
+ statements
79
145
  end
80
146
 
81
147
  # The defs a named call copies onto the module's singleton, for the singleton def-node table. Ruby
@@ -121,10 +187,11 @@ module Rigor
121
187
  alias_method define_method attr attr_reader attr_writer attr_accessor remove_method undef_method
122
188
  ].freeze
123
189
 
124
- # The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets` the start of
125
- # every bare call; each def that starts after one is a module function
126
- # ({.deferred_module_function?}). Appends to `ranges` one row per named call, in the table's
127
- # `[start, end, name, kind, owner]` shape:
190
+ # The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets` an
191
+ # `[offset, :on]` event for the start of every bare call, and for every {.visibility_reset?} an
192
+ # `[offset, :off]` event where it runs or a `[offset, :maybe_off]` one where it may not: `nested` is true
193
+ # once the walk has left the body's statements, a plain `begin` and an `ensure`. Appends to `ranges` one
194
+ # row per named call, in the table's `[start, end, name, kind, owner]` shape:
128
195
  #
129
196
  # - a `:name` argument gives a `:singleton` row over the call itself, because the copy happens at
130
197
  # the call and not at the def;
@@ -134,33 +201,36 @@ module Rigor
134
201
  # The prescan looks through control flow and blocks, because a call that ran there still flips the
135
202
  # later defs. It stops at a nested class, module, `class <<` or def body, where the call targets
136
203
  # another module. It skips an `END` body, which runs after every def, and a `*_eval` / `*_exec`
137
- # block, whose self is the receiver's module; that block gets its own prescan as a body. Nothing
138
- # resets it.
204
+ # block, whose self is the receiver's module, and a class-creating block (`Class.new do`, `Struct.new do`,
205
+ # `Data.define do`, `Module.new do`), whose self is the new class; such a block gets its own prescan as a
206
+ # body where the walk opens one.
139
207
  #
140
208
  # `qualified_prefix` names the owner, and `in_singleton_class` says whether the body is a
141
209
  # `class <<` body; both feed `ScopeIndexer.def_singleton?`.
142
- def prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges)
210
+ def prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges, nested: false)
143
211
  return unless node.is_a?(Prism::Node)
144
212
  return if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) ||
145
213
  node.is_a?(Prism::SingletonClassNode) || node.is_a?(Prism::DefNode) ||
146
214
  node.is_a?(Prism::PostExecutionNode)
215
+ return if node.is_a?(Prism::CallNode) &&
216
+ prescanned_call?(node, qualified_prefix, in_singleton_class, offsets, ranges, nested)
147
217
 
148
- if node.is_a?(Prism::CallNode)
149
- return prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges) if call?(node)
150
- if ScopeIndexer.receiver_eval_call?(node)
151
- return prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
152
- end
153
- end
154
-
218
+ nested ||= !RUN_IN_PLACE.include?(node.class)
155
219
  node.rigor_each_child do |child|
156
- prescan_deferred(child, qualified_prefix, in_singleton_class, offsets, ranges)
220
+ prescan_deferred(child, qualified_prefix, in_singleton_class, offsets, ranges, nested: nested)
157
221
  end
158
222
  end
159
223
 
160
- # Whether the deferred-ranges reading makes a def starting at `start` a module function: a bare call
161
- # starts before it, at any depth the prescan entered.
224
+ # The nodes whose statements run where they are written, so a reset there runs ({.prescan_deferred}).
225
+ RUN_IN_PLACE = [Prism::StatementsNode, Prism::BeginNode, Prism::EnsureNode].freeze
226
+
227
+ # Whether the deferred-ranges reading makes a def starting at `start` a module function: the last
228
+ # `:on` or `:off` event {.prescan_deferred} recorded before it, at any depth the prescan entered, is
229
+ # `:on`. A `:maybe_off` leaves the answer as it was: the row stays `:both`, the reading that counts the
230
+ # def as run on the singleton side too, since the table has no possible row for it.
162
231
  def deferred_module_function?(offsets, start)
163
- offsets.any? { |offset| offset < start }
232
+ last = offsets.select { |offset, event| offset < start && event != :maybe_off }.max_by(&:first)
233
+ last ? last[1] == :on : false
164
234
  end
165
235
 
166
236
  # The extends table's reading. A bare call makes the module extend itself, so every instance def the
@@ -174,10 +244,12 @@ module Rigor
174
244
  end
175
245
 
176
246
  # sig-gen's reading, over one statement list. Yields every statement except a bare call, with
177
- # whether a bare call came before it in this list or `active` was already true on entry. The
247
+ # whether a bare call came before it in this list or `active` was already on at entry: `true`, `false`
248
+ # or `:uncertain` after a reset that may not run ({.mode_after}). The
178
249
  # generator passes the enclosing list's answer into an `if`, a block or a `begin`, but not into a
179
250
  # nested class, module or `class << self`. A bare call in a nested list covers only that list's
180
- # later statements. Nothing resets it, and a named call is not a directive at all.
251
+ # later statements. A reset ends it for the statements after it, and a named call is not a directive at
252
+ # all.
181
253
  def each_sig_gen_statement(statements, active)
182
254
  statements.each do |stmt|
183
255
  if sig_gen_directive?(stmt)
@@ -185,6 +257,7 @@ module Rigor
185
257
  next
186
258
  end
187
259
  yield stmt, active
260
+ active = mode_after(active, stmt)
188
261
  end
189
262
  end
190
263
 
@@ -196,7 +269,13 @@ module Rigor
196
269
  # Whether sig-gen renders a def as `def self?.name`: an instance-side def under an active directive,
197
270
  # in a class or a module alike. `kind` is `:singleton` for `def self.x` and a `class << self` body.
198
271
  def sig_gen_module_function?(active, kind)
199
- active && kind == :instance
272
+ active == true && kind == :instance
273
+ end
274
+
275
+ # Whether sig-gen cannot tell a def's kind: an instance-side def after a reset that may not run, which
276
+ # is a module function only if the reset did not run.
277
+ def sig_gen_uncertain?(active, kind)
278
+ active == :uncertain && kind == :instance
200
279
  end
201
280
 
202
281
  # Yields each instance-method name `node` may rebind on the module whose body holds it, or
@@ -239,10 +318,34 @@ module Rigor
239
318
  end
240
319
  end
241
320
 
321
+ # Issue #2209 — the row name of a deferred-ranges row that stands for every name of its owner
322
+ # ({Spellings}).
323
+ ANY_ROW_NAME = :*
324
+
325
+ # Issue #2209 — the deferred-ranges row names that make `method_name` a module function on their owner: its
326
+ # own and {ANY_ROW_NAME}.
327
+ def row_names(method_name) = [method_name.to_sym, ANY_ROW_NAME]
328
+
329
+ # The prescan's arms for a call; true when the arm took the call and its subtree.
330
+ def prescanned_call?(node, qualified_prefix, in_singleton_class, offsets, ranges, nested)
331
+ if call?(node)
332
+ prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
333
+ elsif (arguments = Spellings.arguments(node))
334
+ Spellings.prescan(node, arguments, qualified_prefix, in_singleton_class, offsets, ranges)
335
+ elsif visibility_reset?(node)
336
+ offsets << [node.location.start_offset, nested ? :maybe_off : :off]
337
+ elsif ScopeIndexer.receiver_eval_call?(node) || (node.block && ScopeIndexer.meta_new_constant_rvalue?(node))
338
+ prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
339
+ else
340
+ return false
341
+ end
342
+ true
343
+ end
344
+
242
345
  # The prescan's call arm.
243
346
  def prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
244
347
  if bare?(node)
245
- offsets << node.location.start_offset
348
+ offsets << [node.location.start_offset, :on]
246
349
  else
247
350
  owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
248
351
  node.arguments&.arguments&.each do |arg|
@@ -252,20 +355,22 @@ module Rigor
252
355
  elsif (name = symbol_argument_name(arg))
253
356
  ranges << [node.location.start_offset, node.location.end_offset, name, :singleton, owner]
254
357
  else
255
- prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
358
+ prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
256
359
  end
257
360
  end
258
361
  end
259
362
  return unless node.block
260
363
 
261
- prescan_deferred(node.block, qualified_prefix, in_singleton_class, offsets, ranges)
364
+ prescan_deferred(node.block, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
262
365
  end
263
366
 
264
367
  # The prescan's eval arm: only the call's receiver and arguments keep this module's context.
265
368
  def prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
266
- prescan_deferred(node.receiver, qualified_prefix, in_singleton_class, offsets, ranges) if node.receiver
369
+ if node.receiver
370
+ prescan_deferred(node.receiver, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
371
+ end
267
372
  node.arguments&.arguments&.each do |arg|
268
- prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
373
+ prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges, nested: true)
269
374
  end
270
375
  end
271
376
 
@@ -274,8 +379,10 @@ module Rigor
274
379
  Source::Literals.symbol_or_string(arg)
275
380
  end
276
381
 
277
- private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name, :each_rebound_name,
278
- :each_own_rebound_name, :each_rebinding_call_name
382
+ private_class_method :prescan_call, :prescanned_call?, :prescan_eval_call, :symbol_argument_name,
383
+ :each_rebound_name, :each_own_rebound_name, :each_rebinding_call_name
279
384
  end
280
385
  end
281
386
  end
387
+
388
+ require_relative "module_function_state/spellings"
@@ -7,6 +7,7 @@ require_relative "../source/node_children"
7
7
  require_relative "content_join"
8
8
  require_relative "hash_lookup_mutation"
9
9
  require_relative "mutation_rejoin"
10
+ require_relative "deep_erasure"
10
11
  require_relative "receiver_alias"
11
12
  require_relative "refinement_mutation"
12
13
  require_relative "rewrite_mutation"
@@ -200,12 +201,15 @@ module Rigor
200
201
  # `Prism::BlockNode`s — each block is processed by its own `eval_call`.
201
202
  def widen_after_block(call_node:, outer_scope:)
202
203
  block = call_node.block
204
+ return DeepErasure.widen_symbol_proc(call_node, outer_scope) if DeepErasure.symbol_proc_mutation?(block)
203
205
  return outer_scope unless block.is_a?(Prism::BlockNode)
204
206
 
205
207
  body = block.body
206
208
  return outer_scope if body.nil?
207
209
 
208
- walk_for_outer_mutations(body, outer_scope)
210
+ scope = walk_for_outer_mutations(body, outer_scope)
211
+ names = DeepErasure.parameter_names(block.parameters)
212
+ DeepErasure.widen_element_aliases(call_node.receiver, block.body, names, scope)
209
213
  end
210
214
 
211
215
  def walk_for_outer_mutations(node, scope)
@@ -257,7 +261,6 @@ module Rigor
257
261
  Prism::ClassVariableReadNode => %i[cvar with_cvar],
258
262
  Prism::GlobalVariableReadNode => %i[global with_global]
259
263
  })
260
- private_constant :ALIAS_ACCESSORS
261
264
 
262
265
  def widen_alias_read(method_name, read, scope, values: :widen, arg_types: NO_ARG_TYPES)
263
266
  getter, builder = ALIAS_ACCESSORS[read.class]