rigortype 0.4.1 → 0.4.3

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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -1
data/lib/rigor/scope.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "environment"
5
5
  require_relative "scope/discovery_index"
6
6
  require_relative "scope/resolution_chain"
7
7
  require_relative "inference/definer_resolution"
8
+ require_relative "inference/in_effect_refinements"
8
9
  require_relative "analysis/fact_store"
9
10
  require_relative "analysis/dependency_recorder"
10
11
  require_relative "inference/expression_typer"
@@ -26,12 +27,12 @@ module Rigor
26
27
  :indexed_narrowings, :method_chain_narrowings,
27
28
  :declaration_sourced, :published_constant_sourced,
28
29
  :source_path, :discovery, :struct_fold_safe_locals,
29
- :opaque_block_self, :singleton_class_body, :lexical_nesting,
30
+ :opaque_block_self, :block_self_unknown, :singleton_class_body, :lexical_nesting,
30
31
  :dynamic_origins, :local_origins, :ivar_origins,
31
32
  :void_origins, :plugin_typed_calls,
32
33
  :optimistic_origins, :optimistic_locals, :optimistic_ivars,
33
34
  :repeated_or_writes, :match_frame,
34
- :constant_narrowings, :guard_records, :bot_guard_classes
35
+ :constant_narrowings, :guard_records, :bot_guard_classes, :declared_refinements
35
36
 
36
37
  # ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
37
38
  # reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
@@ -62,6 +63,20 @@ module Rigor
62
63
  # Issue #1120 — `{refined class name => {method name => [refining module names]}}`: what each
63
64
  # `refine X do … end` block defines, visible only where a `using` of its module is in effect.
64
65
  def discovered_refinements = @discovery.discovered_refinements
66
+
67
+ # Issue #1673 (ADR-121 WD1) — the in-effect refinements at `node`: the refining-module names whose refinements
68
+ # Ruby applies there, ordered so a later activation comes later (the later one wins), each once at its first
69
+ # position, each `using`'d module expanded through its includes and prepends in CRuby's activation order.
70
+ # `declared` is a block source's modules, appended and expanded on the same terms; it defaults to this scope's
71
+ # {#declared_refinements}, a plugin-declared refined block's (#1667). An entry may be
72
+ # `Inference::InEffectRefinements::UNKNOWN`, which means any refinement may be in effect. A node another file
73
+ # wrote (a callee body typed under this file's scope) answers `declared` alone. Reads include edges, so it records
74
+ # the dependencies `Inference::InEffectRefinements.activated_modules` names; a consumer that asks for a method
75
+ # name also records `refinement:<name>` (`Inference::InEffectRefinements.refining_modules`).
76
+ def in_effect_refinements(node, declared = @declared_refinements)
77
+ Inference::InEffectRefinements.for_node(self, node, declared)
78
+ end
79
+
65
80
  # Issue #1367 — the project's `Inference::GlobalWriteCensus`, a `Set` of frozen entries.
66
81
  def discovered_global_write_census = @discovery.discovered_global_write_census
67
82
  def discovered_includes = @discovery.discovered_includes
@@ -182,7 +197,9 @@ module Rigor
182
197
  # today.
183
198
  # - `receiver_name` is the variable's Symbol.
184
199
  # - `key` is the Ruby value of the literal index (Symbol / String / Integer). Non-literal keys
185
- # (`params[field]`) are not recorded; they have no stable address.
200
+ # (`params[field]`) have no stable address for a recorded value. A `key?` guard with a non-literal key
201
+ # (issue #1703) records an {Inference::KeyPresenceGuard::KeyExpr} here instead, with `receiver_kind`
202
+ # `:const` allowed; its entry marks presence only.
186
203
  IndexedKey = Data.define(:receiver_kind, :receiver_name, :key)
187
204
 
188
205
  # Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain per A1
@@ -234,11 +251,11 @@ module Rigor
234
251
  # class in a project that publishes nothing) allocates none.
235
252
  EMPTY_PUBLISHED_CONSTANT_IVARS = Set.new.freeze
236
253
  # The index `||=` sites of a repeating block body whose slot an earlier run of the body may have filled,
237
- # keyed by node identity and laid by a block-return pass at the body's entry
238
- # ({#with_repeated_or_writes}; `Inference::RepeatedOrWrites` decides which). The memoizing `||=` reading
239
- # (`StatementEvaluator#index_compound_write_value`) is withheld at such a site. The mark belongs to the
240
- # site, so no rebind or narrowing of a variable drops it, and a join keeps a site either arm holds: the
241
- # mark only ever withholds that reading, whose answer is the narrower one.
254
+ # keyed by node identity and laid at the body's entry by a block-return pass or by `StatementEvaluator`'s
255
+ # statement and write-back passes ({#with_repeated_or_writes}; `Inference::RepeatedOrWrites` decides which).
256
+ # The memoizing `||=` reading (`StatementEvaluator#index_compound_write_value`) is withheld at such a site.
257
+ # The mark belongs to the site, so no rebind or narrowing of a variable drops it, and a join keeps a site
258
+ # either arm holds: the mark only ever withholds that reading, whose answer is the narrower one.
242
259
  EMPTY_REPEATED_OR_WRITES = {}.compare_by_identity.freeze
243
260
  # Issue #1429 — the narrowing a guard leaves on a constant reference (`STDOUT.is_a?(StringIO)`, `if CONFIG`),
244
261
  # keyed by the reference's spelling ({Inference::Narrowing.constant_key}: `"STDOUT"`, `"::Foo::BAR"`). A constant
@@ -320,6 +337,7 @@ module Rigor
320
337
  source_path: nil,
321
338
  struct_fold_safe_locals: EMPTY_FOLD_SAFE,
322
339
  opaque_block_self: false,
340
+ block_self_unknown: false,
323
341
  singleton_class_body: false,
324
342
  lexical_nesting: nil,
325
343
  dynamic_origins: {}.compare_by_identity,
@@ -334,7 +352,8 @@ module Rigor
334
352
  match_frame: nil,
335
353
  constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
336
354
  guard_records: EMPTY_GUARD_RECORDS,
337
- bot_guard_classes: EMPTY_BOT_GUARD_CLASSES
355
+ bot_guard_classes: EMPTY_BOT_GUARD_CLASSES,
356
+ declared_refinements: Inference::InEffectRefinements::EMPTY
338
357
  )
339
358
  @environment = environment
340
359
  @locals = locals
@@ -351,6 +370,7 @@ module Rigor
351
370
  @source_path = source_path
352
371
  @struct_fold_safe_locals = struct_fold_safe_locals
353
372
  @opaque_block_self = opaque_block_self
373
+ @block_self_unknown = block_self_unknown
354
374
  @singleton_class_body = singleton_class_body
355
375
  @lexical_nesting = lexical_nesting
356
376
  @dynamic_origins = dynamic_origins
@@ -366,6 +386,7 @@ module Rigor
366
386
  @constant_narrowings = constant_narrowings
367
387
  @guard_records = guard_records
368
388
  @bot_guard_classes = bot_guard_classes
389
+ @declared_refinements = declared_refinements
369
390
  freeze
370
391
  end
371
392
 
@@ -378,6 +399,14 @@ module Rigor
378
399
  self
379
400
  end
380
401
 
402
+ # Issue #1703 — this scope with private copies of the identity-keyed side tables (dynamic, void and optimistic
403
+ # origins, plugin-typed calls), so a second walk from it — the `key?`-guarded re-walk — records into its own
404
+ # tables and never into the ones the file's analysis reads.
405
+ def with_isolated_side_tables
406
+ rebuild(dynamic_origins: @dynamic_origins.dup, void_origins: @void_origins.dup,
407
+ plugin_typed_calls: @plugin_typed_calls.dup, optimistic_origins: @optimistic_origins.dup)
408
+ end
409
+
381
410
  def optimistic_local(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_locals[name.to_sym])
382
411
  def optimistic_ivar(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_ivars[name.to_sym])
383
412
 
@@ -428,6 +457,13 @@ module Rigor
428
457
  @locals[name.to_sym]
429
458
  end
430
459
 
460
+ # Issue #1840 — replaces the joined bindings of locals a branch merge only narrowed with their pre-branch
461
+ # binding ({Inference::UntypedRejoin}). Unlike {#with_local} this is not a write: the value is the one the
462
+ # branches started from, so every fact, narrowing and mark the join kept about it still holds.
463
+ def with_rejoined_locals(bindings)
464
+ rebuild(locals: @locals.merge(bindings).freeze)
465
+ end
466
+
431
467
  def with_local(name, type)
432
468
  bind_local(name, type, keep_marks: false)
433
469
  end
@@ -476,6 +512,39 @@ module Rigor
476
512
  rebuild(self_type: type)
477
513
  end
478
514
 
515
+ # Issue #1717 — installs the `self` a block body runs with when the engine KNOWS it: an ADR-16
516
+ # `block_as_methods:` match, `define_method`, or a `Class.new` / `refine` body (#319). Like
517
+ # {#with_self_type} it leaves the #316 mark alone; it also sets {#block_self_unknown?} to `keeps_unknown`,
518
+ # which a caller passes from {#block_self_narrowing_unknown?}. A nil `type` narrows nothing and keeps it.
519
+ #
520
+ # Issue #1695 — instance variables belong to `self`, so a narrowing onto a DIFFERENT `self` than the enclosing
521
+ # scope's also drops every instance-variable binding the body would otherwise inherit ({#without_ivar_bindings}):
522
+ # a `define_method` block in a class body runs on an instance, and the class body's `@label = nil` is the class
523
+ # object's ivar, not the one the block reads. The body's reads answer `Dynamic` until it writes them. A narrowing
524
+ # that keeps the caller's `self` (an ADR-16 `:lexical` entry) keeps them.
525
+ def with_block_self_type(type, keeps_unknown: false)
526
+ unknown = type.nil? ? @block_self_unknown : keeps_unknown
527
+ return rebuild(self_type: type, block_self_unknown: unknown) if type.nil? || type == @self_type
528
+
529
+ without_ivar_bindings(type, unknown)
530
+ end
531
+
532
+ # Issue #1717 — whether a block-self narrowing for `call_node`, asked from this (the CALLER's) scope, is
533
+ # built on an unknown `self`. `define_method` and an implicit- or `self`-receiver `block_as_methods:` match
534
+ # read the lexical `self`, which inside a block of unknown `self` is the enclosing method's, not the
535
+ # block's; a narrowing keyed on an explicit receiver (`Grape::API.namespace do`) does not depend on it.
536
+ #
537
+ # Issue #1759 — one narrowing is not: a `define_method` written directly in a `refine X` block
538
+ # ({REFINEMENT_BLOCK_SELF}) defines a method of the refinement, which runs on an instance of X, and the
539
+ # `Nominal[X]` it reads off the block's lexical `Singleton[X]` is that `self`.
540
+ def block_self_narrowing_unknown?(call_node)
541
+ return false unless @block_self_unknown
542
+ return false if @block_self_unknown == REFINEMENT_BLOCK_SELF && call_node.name == :define_method
543
+
544
+ receiver = call_node.receiver
545
+ receiver.nil? || receiver.is_a?(Prism::SelfNode)
546
+ end
547
+
479
548
  # Issue #652 — installs the body's REAL `Module.nesting`, innermost first, as recorded at declaration
480
549
  # time by `Inference::StatementEvaluator` (`["Admin::UsersController"]` for a compact
481
550
  # `class Admin::UsersController`, `["Admin::UsersController", "Admin"]` for the nested spelling). A
@@ -518,15 +587,52 @@ module Rigor
518
587
  # `Array#each` without knowing the callee. The flag is set at every block entry that leaves `self_type`
519
588
  # unnarrowed and is inherited by every scope derived inside the block; it never leaks past the block,
520
589
  # because `eval_call` returns the caller's scope unchanged.
590
+ #
591
+ # Entering also sets {#block_self_unknown?} (issue #1717), the separate mark `call.undefined-method` reads.
521
592
  def entering_opaque_block
522
- return self if @opaque_block_self
593
+ return self if @opaque_block_self && @block_self_unknown == true
523
594
 
524
- rebuild(opaque_block_self: true)
595
+ rebuild(opaque_block_self: true, block_self_unknown: true)
525
596
  end
526
597
 
527
598
  # True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
528
599
  def opaque_block_self? = @opaque_block_self
529
600
 
601
+ # Issue #1717 — true when this scope sits in a block whose `self` the engine does not know, so an explicit
602
+ # `self.m` there is no evidence `m` is missing: the yielding method may `instance_exec` the block on another
603
+ # object. Read ONLY by `call.undefined-method`'s explicit-`self` exemption, and kept apart from
604
+ # {#opaque_block_self?} so that mark's reach (the top-level-def bind gate) is untouched. Set with
605
+ # {#entering_opaque_block} on the evaluator's block entries and by the indexer's walk of an unentered block
606
+ # ({#with_block_self_unknown}); cleared where a block's `self` is narrowed ({#with_block_self_type}) and, on
607
+ # the indexer's walk, at a `def` / `class` / `module` body, which the evaluator starts from a fresh scope.
608
+ def block_self_unknown? = @block_self_unknown ? true : false
609
+
610
+ # Issue #1759 — the {#block_self_unknown?} value a `refine X do … end` block body is entered with
611
+ # (`with_block_self_type(Singleton[X], keeps_unknown: REFINEMENT_BLOCK_SELF)`). It reads as unknown everywhere
612
+ # `true` does; what it adds is that the body's lexical `self` is X's singleton, which a `define_method` in it
613
+ # reads correctly ({#block_self_narrowing_unknown?}). A block nested in the body takes plain `true`.
614
+ REFINEMENT_BLOCK_SELF = :refinement
615
+
616
+ # Issue #1763 — true directly in a `refine X do … end` block body ({REFINEMENT_BLOCK_SELF}). The body's
617
+ # `self_type` is the `Singleton[X]` its implicit-self calls and `define_method` read, but the block runs on the
618
+ # refinement module, so a `self` read as a value (`x = self`) types as `Dynamic[top]` there.
619
+ def refinement_block_self? = @block_self_unknown == REFINEMENT_BLOCK_SELF
620
+
621
+ # The join of two arms' {#block_self_unknown?} values: `true` if either has it, else either's mark.
622
+ def merged_block_self_unknown(other)
623
+ return true if @block_self_unknown == true || other == true
624
+
625
+ @block_self_unknown || other
626
+ end
627
+ private :merged_block_self_unknown
628
+
629
+ # Issue #1717 — sets {#block_self_unknown?} to `flag`, leaving every other field (the #316 mark included).
630
+ def with_block_self_unknown(flag)
631
+ return self if @block_self_unknown == flag
632
+
633
+ rebuild(block_self_unknown: flag)
634
+ end
635
+
530
636
  # Issue #963 — marks the body of a `class << ...` as such. Inside it `self` is the SINGLETON class, which
531
637
  # Rigor models with the same `Singleton[X]` carrier a `class X` body gets, so the carrier alone cannot say
532
638
  # which of the two a scope came from. The distinction decides what `define_method` does: in a `class << self`
@@ -544,6 +650,19 @@ module Rigor
544
650
  rebuild(singleton_class_body: flag)
545
651
  end
546
652
 
653
+ # Issue #1667 (ADR-121 WD5) — this scope with `names` appended to {#declared_refinements}, each once at its first
654
+ # position: the modules a plugin-declared refined block (`block_as_methods:` `refinements:`) runs under. Stamped
655
+ # at block entry and inherited by every scope derived inside the body, nested blocks included, and by a `def` or
656
+ # class body written in it (`StatementEvaluator#build_fresh_body_scope`), as CRuby's cref is.
657
+ def with_declared_refinements(names)
658
+ return self if names.empty?
659
+
660
+ merged = (@declared_refinements | names).freeze
661
+ return self if merged == @declared_refinements
662
+
663
+ rebuild(declared_refinements: merged)
664
+ end
665
+
547
666
  # True when this scope IS a `class << ...` body (not merely inside one lexically — a `def` reached from it
548
667
  # answers false).
549
668
  def singleton_class_body? = @singleton_class_body
@@ -600,6 +719,45 @@ module Rigor
600
719
  end
601
720
  private :bind_ivar
602
721
 
722
+ # Issue #1695 — this scope with no instance-variable binding and none of the per-ivar state that rides on one:
723
+ # the narrowings keyed on or rooted at an ivar, its ADR-58 and #667 marks, ADR-82 origins, #286 optimistic marks
724
+ # and #1446 guard records, the state {#bind_ivar} drops for one name. It installs {#with_block_self_type}'s
725
+ # `self_type` and unknown-`self` mark in the same rebuild.
726
+ def without_ivar_bindings(self_type, block_self_unknown)
727
+ rebuild(
728
+ self_type: self_type,
729
+ block_self_unknown: block_self_unknown,
730
+ ivars: EMPTY_VAR_BINDINGS,
731
+ indexed_narrowings: reject_kept(@indexed_narrowings) { |k, _| ivar_indexed_key?(k) },
732
+ method_chain_narrowings: reject_kept(@method_chain_narrowings) { |k, _| k.receiver_kind == :ivar },
733
+ declaration_sourced: reject_kept(@declaration_sourced) { |ref| ref[0] == :ivar },
734
+ published_constant_sourced: reject_kept(@published_constant_sourced) { |ref| ref[0] == :ivar },
735
+ ivar_origins: EMPTY_ORIGINS,
736
+ optimistic_ivars: EMPTY_ORIGINS,
737
+ guard_records: reject_kept(@guard_records) { |key, _| key[0] == :ivar },
738
+ bot_guard_classes: reject_kept(@bot_guard_classes) { |key, _| key[0] == :ivar }
739
+ )
740
+ end
741
+ private :without_ivar_bindings
742
+
743
+ # `collection` without the entries the block selects, or `collection` itself when it selects none. It keeps the
744
+ # collection's class: `Set#reject` answers an Array, which a Set carrier's readers (`merge`, `|`, `==`) reject.
745
+ def reject_kept(collection, &)
746
+ return collection if collection.empty? || collection.none?(&)
747
+
748
+ collection.dup.delete_if(&).freeze
749
+ end
750
+ private :reject_kept
751
+
752
+ # An indexed narrowing of an ivar, or of a `key?` guard's key read from one ({#key_guard_rooted_at?}).
753
+ def ivar_indexed_key?(indexed_key)
754
+ return true if indexed_key.receiver_kind == :ivar
755
+
756
+ key = indexed_key.key
757
+ key.is_a?(Inference::KeyPresenceGuard::KeyExpr) && key.root[0] == :ivar
758
+ end
759
+ private :ivar_indexed_key?
760
+
603
761
  # ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index.
604
762
  # Unlike `with_ivar` this binds the type AND records the declaration-sourced mark in one transition.
605
763
  def seed_declaration_sourced_ivar(name, type)
@@ -904,20 +1062,22 @@ module Rigor
904
1062
  @repeated_or_writes.key?(node)
905
1063
  end
906
1064
 
907
- # Regex match-data globals (`$~`, `$&`, `$1..$9`, the pre/post-match and last-paren back-references). Narrowed
1065
+ # Regex match-data globals (`$~`, `$&`, `$1..$N`, the pre/post-match and last-paren back-references). Narrowed
908
1066
  # on a successful-`=~` / `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`). They live in
909
1067
  # the method frame's special-variable slot, which the method's blocks and closures share (issue #1358), so a
910
1068
  # later call that may run a match in this frame — a match-capable call, a block that may match, a call once
911
1069
  # the frame has made a closure that may — rebinds every one of them, and `eval_call` forgets the narrowed
912
1070
  # facts here. Always safe — only drops facts, so a subsequent read falls back to the default `String | nil`.
913
1071
  # Program-level `$GLOBAL = ...` seeds use other names and are untouched.
914
- MATCH_DATA_GLOBALS = %i[$~ $& $` $' $+ $1 $2 $3 $4 $5 $6 $7 $8 $9].freeze
915
- private_constant :MATCH_DATA_GLOBALS
1072
+ # Issue #1384 — the numbered groups are matched by name, not listed: a regex may carry more than nine captures, and
1073
+ # `$10` and above are the same slot as `$1`..`$9`, so forgetting must clear them too.
1074
+ MATCH_DATA_GLOBAL_NAME = /\A\$(?:~|&|`|'|\+|[1-9]\d*)\z/
1075
+ private_constant :MATCH_DATA_GLOBAL_NAME
916
1076
 
917
1077
  def forget_match_globals
918
1078
  return self unless match_globals_bound?
919
1079
 
920
- rebuild(globals: @globals.except(*MATCH_DATA_GLOBALS).freeze)
1080
+ rebuild(globals: @globals.reject { |name, _| MATCH_DATA_GLOBAL_NAME.match?(name.name) }.freeze)
921
1081
  end
922
1082
 
923
1083
  # Issue #1361 — this scope with every bound match-data global rebound to `Dynamic[top]`, and an unbound one left
@@ -928,14 +1088,16 @@ module Rigor
928
1088
  return self unless match_globals_bound?
929
1089
 
930
1090
  untyped = Type::Combinator.untyped
931
- rebound = MATCH_DATA_GLOBALS.each_with_object({}) { |name, acc| acc[name] = untyped if @globals.key?(name) }
1091
+ rebound = @globals.each_with_object({}) do |(name, _), acc|
1092
+ acc[name] = untyped if MATCH_DATA_GLOBAL_NAME.match?(name.name)
1093
+ end
932
1094
  rebuild(globals: @globals.merge(rebound).freeze)
933
1095
  end
934
1096
 
935
1097
  # True when any match-data global holds a binding, which a match edge or {#untyped_match_globals} makes: the only
936
1098
  # state {#forget_match_globals} can drop, and so the gate on every scan that decides whether to.
937
1099
  def match_globals_bound?
938
- !@globals.empty? && MATCH_DATA_GLOBALS.any? { |name| @globals.key?(name) }
1100
+ !@globals.empty? && @globals.any? { |name, _| MATCH_DATA_GLOBAL_NAME.match?(name.name) }
939
1101
  end
940
1102
 
941
1103
  # Issue #1359 — the last line read, `$_`, lives in the same frame slot as the match globals, and so on the same
@@ -1954,6 +2116,7 @@ module Rigor
1954
2116
  source_path: @source_path,
1955
2117
  struct_fold_safe_locals: @struct_fold_safe_locals,
1956
2118
  opaque_block_self: @opaque_block_self,
2119
+ block_self_unknown: @block_self_unknown,
1957
2120
  singleton_class_body: @singleton_class_body,
1958
2121
  lexical_nesting: @lexical_nesting,
1959
2122
  dynamic_origins: @dynamic_origins,
@@ -1968,7 +2131,8 @@ module Rigor
1968
2131
  match_frame: @match_frame,
1969
2132
  constant_narrowings: @constant_narrowings,
1970
2133
  guard_records: @guard_records,
1971
- bot_guard_classes: @bot_guard_classes
2134
+ bot_guard_classes: @bot_guard_classes,
2135
+ declared_refinements: @declared_refinements
1972
2136
  )
1973
2137
  self.class.new(
1974
2138
  environment: environment, locals: locals,
@@ -1982,6 +2146,7 @@ module Rigor
1982
2146
  source_path: source_path,
1983
2147
  struct_fold_safe_locals: struct_fold_safe_locals,
1984
2148
  opaque_block_self: opaque_block_self,
2149
+ block_self_unknown: block_self_unknown,
1985
2150
  singleton_class_body: singleton_class_body,
1986
2151
  lexical_nesting: lexical_nesting,
1987
2152
  dynamic_origins: dynamic_origins,
@@ -1996,7 +2161,8 @@ module Rigor
1996
2161
  match_frame: match_frame,
1997
2162
  constant_narrowings: constant_narrowings,
1998
2163
  guard_records: guard_records,
1999
- bot_guard_classes: bot_guard_classes
2164
+ bot_guard_classes: bot_guard_classes,
2165
+ declared_refinements: declared_refinements
2000
2166
  )
2001
2167
  end
2002
2168
 
@@ -2014,7 +2180,8 @@ module Rigor
2014
2180
  result.freeze
2015
2181
  end
2016
2182
 
2017
- def build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
2183
+ # One keyword per Scope field, each with its join rule beside it; the size is the field count.
2184
+ def build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
2018
2185
  self.class.new(
2019
2186
  environment: @environment,
2020
2187
  locals: joined_locals.freeze,
@@ -2062,6 +2229,9 @@ module Rigor
2062
2229
  # block entry by `entering_opaque_block` and inherited through `rebuild`), so both arms usually carry
2063
2230
  # the same value and the `||` is that value.
2064
2231
  opaque_block_self: @opaque_block_self || other.opaque_block_self,
2232
+ # Issue #1717 — the same body property, and `||` keeps the exemption it grants (the FP-safe side).
2233
+ # A plain `true` outranks the refine body's {REFINEMENT_BLOCK_SELF} for the same reason.
2234
+ block_self_unknown: merged_block_self_unknown(other.block_self_unknown),
2065
2235
  # Issue #963 — a body property like the two above, so both arms of an in-body merge carry the identical
2066
2236
  # value and the `||` is that value. `||` is also the safe direction on its own terms: keeping the mark
2067
2237
  # declines the `define_method` narrowing, which is the pre-#963 answer.
@@ -2086,12 +2256,24 @@ module Rigor
2086
2256
  # of a merge inside one body carry the same one; `||` keeps it should either arm lack it, since dropping
2087
2257
  # it only loses the frame's resets.
2088
2258
  match_frame: @match_frame || other.match_frame,
2259
+ # Issue #1667 — a body property stamped at block entry like the frame above, so both arms of a merge carry
2260
+ # the same list; the union keeps either arm's modules, the declining direction.
2261
+ declared_refinements: join_declared_refinements(other),
2089
2262
  # Issue #1429 — the guard narrowings of globals and constants and their pre-guard records, and of instance
2090
2263
  # variables (#1446).
2091
2264
  **join_guard_narrowings(other, joined_globals, joined_ivars)
2092
2265
  )
2093
2266
  end
2094
2267
 
2268
+ def join_declared_refinements(other)
2269
+ mine = @declared_refinements
2270
+ theirs = other.declared_refinements
2271
+ return mine if theirs.equal?(mine) || theirs.empty?
2272
+ return theirs if mine.empty?
2273
+
2274
+ (mine | theirs).freeze
2275
+ end
2276
+
2095
2277
  def join_guard_narrowings(other, joined_globals, joined_ivars)
2096
2278
  joined_constants = join_constant_narrowings(other)
2097
2279
  joined = { global: joined_globals, ivar: joined_ivars, constant: joined_constants }
@@ -2246,11 +2428,19 @@ module Rigor
2246
2428
  sym_kind = receiver_kind.to_sym
2247
2429
  sym_name = receiver_name.to_sym
2248
2430
  filtered = @indexed_narrowings.reject do |k, _|
2249
- k.receiver_kind == sym_kind && k.receiver_name == sym_name
2431
+ (k.receiver_kind == sym_kind && k.receiver_name == sym_name) || key_guard_rooted_at?(k.key, sym_kind, sym_name)
2250
2432
  end
2251
2433
  filtered.size == @indexed_narrowings.size ? @indexed_narrowings : filtered.freeze
2252
2434
  end
2253
2435
 
2436
+ # Issue #1703 — whether `key` is a `key?` guard's key that is the variable `kind`/`name`, or a chain read from it.
2437
+ def key_guard_rooted_at?(key, kind, name)
2438
+ return false unless key.is_a?(Inference::KeyPresenceGuard::KeyExpr)
2439
+
2440
+ root = key.root
2441
+ root[0] == kind && root[1] == name
2442
+ end
2443
+
2254
2444
  # ADR-58 WD1 — set/clear the declaration-sourced provenance mark.
2255
2445
  # Issue #667 — the #667 carrier's add/drop pair. Deliberately not reusing
2256
2446
  # {#add_declaration_sourced} / {#drop_declaration_sourced_for}: the two sets are joined by opposite
@@ -57,7 +57,10 @@ module Rigor
57
57
  inline_generic_class: "sig.skipped.inline-generic-class",
58
58
  # ADR-112 WD4 — the member is declared inline and in `sig/`, and the two disagree. A refusal, not a
59
59
  # skip: `--write` and `--check` exit 1 until a person reconciles them or passes `--overwrite`.
60
- inline_differs: "sig.skipped.inline-differs"
60
+ inline_differs: "sig.skipped.inline-differs",
61
+ # #1676 — a plain top-level `def` defines a private `Object` method and has no class to write a
62
+ # declaration under, so it is reported rather than silently dropped. Whether to emit it stays open.
63
+ top_level_def: "sig.skipped.top-level-def"
61
64
  }.freeze
62
65
  end
63
66
  end
@@ -57,7 +57,8 @@ module Rigor
57
57
  # a parameter-only inline `# @rbs` annotation) is the ABSENCE of a return statement, not the author's
58
58
  # word about it — the opposite of `void`. It is treated like no declaration at all: the inferred return
59
59
  # is proposed as `tighter-return`, carrying `untyped` as the declared spelling so `--diff` still shows a
60
- # declaration existed (#995; see {#declared_untyped?}).
60
+ # declaration existed (#995; see {#declared_untyped?}). A value literal the body proves (Integer, String,
61
+ # Symbol) is widened to its class there unless the `untyped` is only the inline reader's synthesis (#1489).
61
62
  # - A `tighter-return` over the class's own declaration replaces the return and nothing else: the rest of
62
63
  # the line is the declaration's, spelled as written, so `--overwrite` never changes which calls the
63
64
  # signature accepts. A declaration with more than one overload, or one spelled as an `alias`, is left
@@ -100,6 +101,9 @@ module Rigor
100
101
  @class_shells = Set.new
101
102
  @class_superclasses = {}
102
103
  @meta_layouts = {}
104
+ # Issue #1763 — the walked file's tree, and its refine-call query, built only when a `refine` call is walked.
105
+ @walk_root = nil
106
+ @in_effect_refinements = nil
103
107
  # Whole-run, NOT per-file: a rendering defect is reported once at the end of the run.
104
108
  @unrenderable = []
105
109
  # Issue #735 — whole-run too: `{ class name => the superclass token that resolves nowhere }`.
@@ -475,7 +479,7 @@ module Rigor
475
479
  # `rigor sig-gen` run. The `check` path recovers each *file* this way (worker_session.rb); sig-gen
476
480
  # recovers per-def so the rest of the file's candidates still emit.
477
481
 
478
- classify_def(path, def_node, class_name, kind, scope_index)
482
+ classify_collected_def(path, def_node, class_name, kind, scope_index)
479
483
  rescue StandardError
480
484
  nil
481
485
  end
@@ -486,8 +490,9 @@ module Rigor
486
490
 
487
491
  # Walks the AST collecting `(def_node, class_name, kind)` tuples for every `def` Rigor can re-type. Slice 1
488
492
  # covered instance `def foo` methods inside a nameable `class` / `module` body. Slice 4 extends this to
489
- # singleton-side methods via `def self.foo` and `class << self; def foo; end`; top-level / DSL-block defs
490
- # still degrade silently (no nameable receiver).
493
+ # singleton-side methods via `def self.foo` and `class << self; def foo; end`. A plain top-level `def` is
494
+ # collected with a nil class and reported as skipped (#1676); DSL-block defs still degrade silently (no
495
+ # nameable receiver).
491
496
  #
492
497
  # ADR-14 gap-#3 follow-up tracks two extra pieces during the same walk so the Writer can emit kind-correct
493
498
  # RBS without guessing:
@@ -500,7 +505,10 @@ module Rigor
500
505
  # RBS spelling that matches the dual instance + singleton dispatch the runtime produces.
501
506
  def collect_method_definitions(root, scope_index)
502
507
  @declared_types = scope_index[root].declared_types
508
+ @walk_root = root
509
+ @in_effect_refinements = nil
503
510
  out = []
511
+ @block_depth = 0
504
512
  walk_defs(root, [], false, false, out)
505
513
  out
506
514
  end
@@ -530,9 +538,22 @@ module Rigor
530
538
  return
531
539
  end
532
540
 
541
+ walk_def_children(node, prefix, in_singleton_class, module_function_active, out)
542
+ end
543
+
544
+ # #1676 — a block body is a DSL scope, not the file's top level, so a `def` inside one is not reported
545
+ # as a top-level def. Neither is a `def` in `class << obj` (a non-`self` singleton body reaches here,
546
+ # the `self` one returns above): it defines a singleton method on `obj`, not a private `Object` method.
547
+ def walk_def_children(node, prefix, in_singleton_class, module_function_active, out)
548
+ return if refine_block_call?(node)
549
+
550
+ in_block = node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode) ||
551
+ node.is_a?(Prism::SingletonClassNode)
552
+ @block_depth += 1 if in_block
533
553
  node.rigor_each_child do |child|
534
554
  walk_defs(child, prefix, in_singleton_class, module_function_active, out)
535
555
  end
556
+ @block_depth -= 1 if in_block
536
557
  end
537
558
 
538
559
  # [#722](https://github.com/rigortype/rigor/issues/722) residue 3 — the qualified name is built by
@@ -559,6 +580,20 @@ module Rigor
559
580
  true
560
581
  end
561
582
 
583
+ # Issue #1763 — a `def` (or an `attr_*`) in a `refine X do … end` block defines a method of the refinement
584
+ # module, not of the namespace the block is written in, and RBS has no declaration for a refinement, so both
585
+ # walks skip the whole block: writing `def shout: () -> String` under the enclosing module declared a method
586
+ # that module does not have. Statement and value position alike, since the walks descend every child. The
587
+ # skip is silent, as for a top-level `def`: a `sig.skipped.*` row would name the enclosing class, the very
588
+ # misattribution this avoids. A `refine` call the engine reads as a class's own method (`Class` undefines
589
+ # `refine`, #1689) walks as any other block, as the engine treats it.
590
+ def refine_block_call?(node)
591
+ return false unless node.is_a?(Prism::CallNode) && Inference::ScopeIndexer.refine_call?(node)
592
+
593
+ @in_effect_refinements ||= Inference::InEffectRefinements.new(@walk_root)
594
+ !@in_effect_refinements.class_body_refine?(node)
595
+ end
596
+
562
597
  def declined_self_header?(node)
563
598
  path = node.constant_path
564
599
  path.is_a?(Prism::ConstantPathNode) && path.parent.is_a?(Prism::SelfNode) &&
@@ -661,7 +696,8 @@ module Rigor
661
696
  return nil unless node.value.is_a?(Prism::CallNode)
662
697
  return nil unless @meta_layouts.key?((prefix + [node.name.to_s]).join("::"))
663
698
 
664
- node.value.block&.body
699
+ block = node.value.block
700
+ block.body if block.is_a?(Prism::BlockNode)
665
701
  end
666
702
 
667
703
  # Module / class bodies are walked through the `walk_statements` path so `module_function` (no-args)
@@ -684,7 +720,13 @@ module Rigor
684
720
  end
685
721
 
686
722
  def collect_def_node(node, prefix, in_singleton_class, module_function_active, out)
687
- return if prefix.empty?
723
+ if prefix.empty?
724
+ # #1676 — a plain top-level `def` defines a private `Object` method. It is surfaced as a skipped
725
+ # candidate (no class to write it under) rather than dropped; `def self.x` and block-body defs stay
726
+ # unreported.
727
+ out << [node, nil, :instance] if node.receiver.nil? && !in_singleton_class && @block_depth.zero?
728
+ return
729
+ end
688
730
 
689
731
  kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
690
732
  class_name = prefix.join("::")
@@ -915,6 +957,13 @@ module Rigor
915
957
  end
916
958
  end
917
959
 
960
+ # #1676 — a top-level `def` is collected with no class; it is reported, never typed.
961
+ def classify_collected_def(path, def_node, class_name, kind, scope_index)
962
+ return skipped(path, def_node, class_name, kind, :top_level_def) if class_name.nil?
963
+
964
+ classify_def(path, def_node, class_name, kind, scope_index)
965
+ end
966
+
918
967
  def classify_def(path, def_node, class_name, kind, scope_index)
919
968
  return nil if visibility_excludes?(def_node, class_name, kind, scope_index)
920
969
  return nil if initialize_excludes?(def_node, kind)
@@ -1368,14 +1417,53 @@ module Rigor
1368
1417
  declared.is_a?(Type::Dynamic)
1369
1418
  end
1370
1419
 
1371
- # Proposed without {#compare_against_declared}'s tightening guards: a declared `untyped` return carries no
1372
- # information to weigh {#tighter?} or {#literal_decline?} against, the same position a method with no
1373
- # declaration at all is in. The line is built as for any tightening ({#tighter_return_line}), and
1374
- # `declared_return_rbs` carries `"untyped"` so the `[tighter, was: untyped]` print tag says one existed.
1420
+ # Proposed without {#tighter?}: a declared `untyped` return carries no information to weigh it against, the
1421
+ # same position a method with no declaration at all is in. Over a declaration the author wrote, though, a
1422
+ # VALUE literal the body proves (`-> 1`, `-> "x"`, `-> :k`) is an implementation detail the `untyped` was
1423
+ # abstracting over, so it is widened to its class (`1 | 2` to `Integer`, `"S" | nil` to `(String | nil)`; #1489).
1424
+ # `nil`, `true` and `false` are honest types and stay, as does a computed `Float`. An `untyped` that is only
1425
+ # the inline reader's synthesis ({#synthesized_declaration?}) is no word of the author's and keeps the
1426
+ # strictest carrier. `declared_return_rbs` carries `"untyped"` so the `[tighter, was: untyped]` print tag
1427
+ # says a declaration existed.
1375
1428
  def declared_untyped_candidate(path, def_node, class_name, kind, inferred, method_def, rendered: nil)
1429
+ unless synthesized_declaration?(method_def)
1430
+ widened = widen_value_literals(inferred)
1431
+ unless widened.equal?(inferred)
1432
+ # A `rendered` line was spelled from the unwidened type; there is nothing to rebuild it from.
1433
+ return equivalent(path, def_node, class_name, kind, inferred, "untyped") if rendered
1434
+
1435
+ inferred = widened
1436
+ end
1437
+ end
1438
+
1376
1439
  tighter_return_candidate(path, def_node, class_name, kind, inferred, method_def, "untyped", rendered)
1377
1440
  end
1378
1441
 
1442
+ VALUE_LITERAL_CLASSES = [Integer, String, Symbol].freeze
1443
+ private_constant :VALUE_LITERAL_CLASSES
1444
+
1445
+ # The same object back when nothing needed widening.
1446
+ def widen_value_literals(type)
1447
+ case type
1448
+ when Type::Constant
1449
+ return type unless VALUE_LITERAL_CLASSES.any? { |klass| type.value.instance_of?(klass) }
1450
+
1451
+ Type::Combinator.nominal_of(type.value.class.name)
1452
+ when Type::Union
1453
+ members = type.members.map { |member| widen_value_literals(member) }
1454
+ members.each_with_index.all? { |m, i| m.equal?(type.members[i]) } ? type : Type::Combinator.union(*members)
1455
+ else
1456
+ type
1457
+ end
1458
+ end
1459
+
1460
+ # A declaration only the inline reader's synthesis (a `virtual:` buffer) supplies — ADR-93 gives every `def` an
1461
+ # `untyped` member, annotated or not — is no word of the author's about the return, so it keeps the proposal
1462
+ # a method with no declaration at all gets: the strictest carrier the body proves.
1463
+ def synthesized_declaration?(method_def)
1464
+ method_def.defs.all? { |type_def| type_def.member.location&.buffer&.name.to_s.start_with?("virtual:") }
1465
+ end
1466
+
1379
1467
  def build_declared_return(method_def)
1380
1468
  translated = method_def.method_types.filter_map { |mt| translate_method_type_return(mt) }
1381
1469
  return nil if translated.empty?
@@ -1712,6 +1800,8 @@ module Rigor
1712
1800
  return
1713
1801
  end
1714
1802
  when Prism::CallNode
1803
+ return if refine_block_call?(node)
1804
+
1715
1805
  collect_attr_call(node, prefix, in_singleton_class, ctx)
1716
1806
  end
1717
1807
 
@@ -11,8 +11,8 @@ module Rigor
11
11
  # applicable to that classification.
12
12
  #
13
13
  # - `path` — the source `.rb` file the def came from.
14
- # - `class_name` — qualified receiver class name (e.g. `"Foo::Bar"`). `nil` for top-level / DSL-block defs
15
- # the MVP skips.
14
+ # - `class_name` — qualified receiver class name (e.g. `"Foo::Bar"`). `nil` for a top-level `def`, which is only
15
+ # ever `skipped` (`sig.skipped.top-level-def`, #1676).
16
16
  # - `method_name` — the def's `Symbol` name.
17
17
  # - `kind` — `:instance` or `:singleton`.
18
18
  # - `inferred_return` — `Rigor::Type` instance (or `nil` when the inference pass disqualified the def).