rigortype 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/date.yml +6 -3
  4. data/data/builtins/ruby_core/enumerable.yml +1 -0
  5. data/data/builtins/ruby_core/exception.yml +2 -1
  6. data/data/builtins/ruby_core/file.yml +28 -14
  7. data/data/builtins/ruby_core/hash.yml +5 -2
  8. data/data/builtins/ruby_core/io.yml +33 -16
  9. data/data/builtins/ruby_core/random.yml +2 -1
  10. data/data/builtins/ruby_core/re.yml +6 -3
  11. data/data/builtins/ruby_core/struct.yml +2 -0
  12. data/data/builtins/ruby_core/time.yml +2 -1
  13. data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
  14. data/docs/handbook/11-sig-gen.md +24 -14
  15. data/docs/manual/02-cli-reference.md +23 -7
  16. data/docs/manual/04-diagnostics.md +1 -1
  17. data/docs/manual/05-inspecting-types.md +20 -8
  18. data/docs/manual/07-plugins.md +4 -0
  19. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  20. data/docs/manual/plugins/rigor-activerecord.md +74 -1
  21. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  22. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  24. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  25. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  27. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  28. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  29. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  30. data/lib/rigor/analysis/check_rules.rb +383 -48
  31. data/lib/rigor/analysis/crash_signature.rb +188 -0
  32. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  33. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  34. data/lib/rigor/analysis/incremental.rb +28 -0
  35. data/lib/rigor/analysis/incremental_session.rb +63 -16
  36. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  37. data/lib/rigor/analysis/reachability/graph.rb +14 -5
  38. data/lib/rigor/analysis/reachability/scan.rb +7 -3
  39. data/lib/rigor/analysis/result.rb +32 -0
  40. data/lib/rigor/analysis/run_cache_key.rb +41 -1
  41. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +233 -16
  42. data/lib/rigor/analysis/runner/pool_coordinator.rb +266 -47
  43. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  44. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -2
  45. data/lib/rigor/analysis/runner.rb +212 -16
  46. data/lib/rigor/analysis/worker_session.rb +37 -3
  47. data/lib/rigor/bleeding_edge.rb +7 -6
  48. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  49. data/lib/rigor/builtins/imported_refinements.rb +11 -9
  50. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  51. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  52. data/lib/rigor/cache/annotation_location.rb +72 -0
  53. data/lib/rigor/cache/descriptor.rb +42 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  55. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +70 -8
  56. data/lib/rigor/cache/store.rb +17 -1
  57. data/lib/rigor/cli/coverage_command.rb +23 -18
  58. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  59. data/lib/rigor/cli/coverage_scan.rb +47 -6
  60. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  61. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  62. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  63. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  64. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  65. data/lib/rigor/cli/sig_gen_command.rb +47 -0
  66. data/lib/rigor/cli/type_of_command.rb +188 -41
  67. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  68. data/lib/rigor/cli/type_scan_command.rb +10 -1
  69. data/lib/rigor/cli.rb +1 -1
  70. data/lib/rigor/configuration.rb +7 -4
  71. data/lib/rigor/effects/plugin_facts.rb +1 -1
  72. data/lib/rigor/effects/scanner.rb +2 -3
  73. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  74. data/lib/rigor/environment/failure_slot.rb +28 -0
  75. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  76. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  77. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  78. data/lib/rigor/environment/rbs_loader.rb +743 -93
  79. data/lib/rigor/environment.rb +125 -25
  80. data/lib/rigor/inference/acceptance.rb +172 -20
  81. data/lib/rigor/inference/captured_locals.rb +62 -0
  82. data/lib/rigor/inference/content_join.rb +347 -0
  83. data/lib/rigor/inference/def_handle.rb +15 -3
  84. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  85. data/lib/rigor/inference/expression_typer.rb +1020 -141
  86. data/lib/rigor/inference/fork_map.rb +6 -1
  87. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  88. data/lib/rigor/inference/hkt_registry.rb +61 -3
  89. data/lib/rigor/inference/hkt_sugar_translator.rb +87 -0
  90. data/lib/rigor/inference/index_write_widening.rb +48 -0
  91. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  92. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  93. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  94. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  95. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  96. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  97. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  98. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  99. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  100. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  101. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  103. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  104. data/lib/rigor/inference/mutation_widening.rb +244 -174
  105. data/lib/rigor/inference/narrowing.rb +331 -27
  106. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  107. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  108. data/lib/rigor/inference/precision_scanner.rb +5 -2
  109. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  110. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  111. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  112. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  113. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  114. data/lib/rigor/inference/version_guard.rb +229 -0
  115. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  116. data/lib/rigor/plugin/base.rb +3 -2
  117. data/lib/rigor/plugin/inflector.rb +14 -5
  118. data/lib/rigor/plugin/io_boundary.rb +107 -4
  119. data/lib/rigor/plugin/loader.rb +14 -0
  120. data/lib/rigor/plugin/registry.rb +25 -2
  121. data/lib/rigor/protection/analysis_guard.rb +60 -0
  122. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  123. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  124. data/lib/rigor/protection/discovery_seed.rb +1 -1
  125. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  126. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  127. data/lib/rigor/protection/mutator.rb +121 -18
  128. data/lib/rigor/rbs_extended/envelope_scanner.rb +8 -7
  129. data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
  130. data/lib/rigor/rbs_extended/reporter.rb +93 -13
  131. data/lib/rigor/rbs_extended.rb +6 -1
  132. data/lib/rigor/reflection.rb +182 -30
  133. data/lib/rigor/scope/discovery_index.rb +39 -1
  134. data/lib/rigor/scope.rb +385 -4
  135. data/lib/rigor/sig_gen/classification.rb +12 -1
  136. data/lib/rigor/sig_gen/generator.rb +320 -85
  137. data/lib/rigor/sig_gen/renderer.rb +14 -12
  138. data/lib/rigor/sig_gen/writer.rb +3 -0
  139. data/lib/rigor/source/constant_path.rb +79 -2
  140. data/lib/rigor/triage/catalogue.rb +1 -1
  141. data/lib/rigor/type/combinator.rb +10 -0
  142. data/lib/rigor/type/maybe.rb +47 -0
  143. data/lib/rigor/type/refined.rb +1 -2
  144. data/lib/rigor/type/result.rb +53 -0
  145. data/lib/rigor/type.rb +2 -0
  146. data/lib/rigor/version.rb +1 -1
  147. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  148. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  149. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  150. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  151. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  152. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  154. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  157. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  158. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  159. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  160. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  161. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  162. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  163. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  164. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  165. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  166. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  167. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  168. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  169. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  170. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  171. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  172. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  173. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  174. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  175. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  176. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  177. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  178. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  179. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  180. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  181. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  182. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  183. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  184. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  185. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  186. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  187. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  188. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  189. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  190. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  191. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  192. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  193. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  194. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  195. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  196. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  197. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  198. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  199. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  200. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  201. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  202. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  203. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  204. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  205. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  206. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  207. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  208. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  209. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  210. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  211. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  212. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  213. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  214. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  215. data/sig/rigor/analysis/baseline.rbs +1 -1
  216. data/sig/rigor/environment.rbs +9 -0
  217. data/sig/rigor/inference.rbs +4 -1
  218. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  219. data/sig/rigor/reflection.rbs +3 -1
  220. data/sig/rigor/scope.rbs +25 -1
  221. data/sig/rigor/type.rbs +95 -1
  222. data/sig/rigor.rbs +16 -0
  223. metadata +71 -3
@@ -37,7 +37,7 @@ module Rigor
37
37
  #
38
38
  # See docs/internal-spec/inference-engine.md (Narrowing) and
39
39
  # docs/type-specification/control-flow-analysis.md for the binding contract.
40
- # rubocop:disable Metrics/ModuleLength
40
+ # rubocop:disable-next Metrics/ModuleLength
41
41
  module Narrowing
42
42
  TRUSTED_EQUALITY_LITERAL_CLASSES = [String, Symbol, Integer, TrueClass, FalseClass, NilClass].freeze
43
43
  SINGLETON_LITERAL_CLASSES = [TrueClass, FalseClass, NilClass].freeze
@@ -439,7 +439,7 @@ module Rigor
439
439
  end
440
440
  end
441
441
 
442
- # rubocop:disable Metrics/ClassLength
442
+ # rubocop:disable-next Metrics/ClassLength
443
443
  class << self
444
444
  private
445
445
 
@@ -897,6 +897,30 @@ module Rigor
897
897
  # the conservative no-op (falsey could mean nil receiver OR a falsey method result).
898
898
  safe_nav_result = analyse_safe_nav_receiver(node, scope)
899
899
  return safe_nav_result if safe_nav_result
900
+
901
+ # Issue #606 slice 1 — the same proof one call further out, where the `&.` result is
902
+ # fed to a suffix (`custom&.value.present?`, `backup&.user.nil?`) or compared against
903
+ # a non-nil operand. Sits after every pre-existing NARROWING path so those keep first claim
904
+ # on the node (the RBS-extended contribution hook still runs later and can be pre-empted —
905
+ # no live collision today since arg-free chains carry no argument facts):
906
+ # each of them returns nil for these shapes today (the predicate analysers require a
907
+ # local-read receiver, which a chain is not), so this only ever runs on nodes that
908
+ # were otherwise about to narrow nothing.
909
+ chain_result =
910
+ if %i[== !=].include?(node.name)
911
+ analyse_safe_nav_chain_equality(node, scope)
912
+ else
913
+ analyse_safe_nav_chain(node, scope)
914
+ end
915
+ return chain_result if chain_result
916
+
917
+ # Issue #606 slice 2 — membership. Runs after `analyse_string_predicate`, which owns
918
+ # the `str.include?(literal)` shape; this one wants the mirror image (a collection
919
+ # receiver, a bound argument), so the two never contend for the same node.
920
+ if node.name == :include?
921
+ membership_result = analyse_membership_predicate(node, scope)
922
+ return membership_result if membership_result
923
+ end
900
924
  end
901
925
 
902
926
  # Slice 7 phase 15 — RBS::Extended predicate effects. When the method's RBS signature
@@ -1311,7 +1335,8 @@ module Rigor
1311
1335
  name = Source::ConstantPath.qualified_name_or_nil(node)
1312
1336
  return nil if name.nil?
1313
1337
 
1314
- type = Reflection.resolve_constant_type(name, scope: scope)
1338
+ type = Reflection.resolve_constant_type(name, scope: scope,
1339
+ rooted: Source::ConstantPath.rooted?(node))
1315
1340
  return :ambiguous if type.is_a?(Type::Union)
1316
1341
  return nil unless type.is_a?(Type::Constant)
1317
1342
 
@@ -1764,7 +1789,8 @@ module Rigor
1764
1789
  return nil if node.arguments.nil?
1765
1790
  return nil unless node.arguments.arguments.size == 1
1766
1791
 
1767
- bare_name = static_class_name(node.arguments.arguments.first)
1792
+ argument = node.arguments.arguments.first
1793
+ bare_name = static_class_name(argument)
1768
1794
  return nil if bare_name.nil?
1769
1795
 
1770
1796
  # Resolve `bare_name` through the lexical-scope chain so a name shadowed by the
@@ -1773,7 +1799,15 @@ module Rigor
1773
1799
  # `Rigor::Type::Singleton#==`, `is_a?(Singleton)` should resolve to
1774
1800
  # `Rigor::Type::Singleton`, not the top-level stdlib `Singleton` mixin (which would
1775
1801
  # surface as a spurious `undefined-method` on subsequent `other.class_name` calls).
1776
- class_name = resolve_class_name_lexically(bare_name, scope)
1802
+ #
1803
+ # A leading `::` opts out of that walk (#614): `static_class_name` de-roots the
1804
+ # argument, so without the marker the guard answered the shadow while the READ
1805
+ # `::Foo.new` answered the top-level class — the guard and the value it guards
1806
+ # disagreeing about the same spelling. When no top-level class owns the name we
1807
+ # DECLINE (nil) rather than pass the unknown name down: keeping the pre-state is the
1808
+ # conservative answer, and narrowing to the shadow is the one answer that is wrong.
1809
+ class_name = rooted_class_predicate_name(argument, bare_name, scope)
1810
+ return nil if class_name.nil?
1777
1811
 
1778
1812
  case node.receiver
1779
1813
  when Prism::LocalVariableReadNode
@@ -1783,6 +1817,15 @@ module Rigor
1783
1817
  end
1784
1818
  end
1785
1819
 
1820
+ # The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
1821
+ # `bare_name` when the reference is written `::Foo` (nil when nothing declares it there),
1822
+ # the lexical resolution otherwise.
1823
+ def rooted_class_predicate_name(argument, bare_name, scope)
1824
+ return resolve_class_name_lexically(bare_name, scope) unless Source::ConstantPath.rooted?(argument)
1825
+
1826
+ class_known_to_scope?(scope, bare_name) ? bare_name : nil
1827
+ end
1828
+
1786
1829
  def analyse_class_predicate_on_local(node, scope, class_name, exact)
1787
1830
  current = scope.local(node.receiver.name)
1788
1831
  return nil if current.nil?
@@ -1844,9 +1887,27 @@ module Rigor
1844
1887
  # `<prefix>::<bare_name>` (or bare `<bare_name>` at the top level) that the environment
1845
1888
  # recognises. Falls back to `bare_name` itself when nothing in the chain resolves; the
1846
1889
  # downstream `narrow_class` then yields the conservative answer for unknown receivers.
1890
+ #
1891
+ # A MULTI-SEGMENT spelling is relative too and gets the same walk (#635). `Type::Nominal`
1892
+ # written inside `module Rigor` is not "already qualified": Ruby anchors the path's FIRST
1893
+ # segment through `Module.nesting`, so the constant it names is `Rigor::Type::Nominal`.
1894
+ # Treating any `::` as absolute left every such guard narrowing to a name no environment
1895
+ # knows, and each subsequent reader call on the narrowed receiver degraded to
1896
+ # `Dynamic[top]` — ~300 sites in Rigor's own `lib/` alone. Trying the whole path under
1897
+ # each prefix is the same rule the single-segment case already applies, and it adopts a
1898
+ # candidate only when the scope actually knows it, so a genuinely absolute spelling still
1899
+ # falls through to `bare_name` unchanged.
1900
+ #
1901
+ # The chain IS `Module.nesting` wherever a declaration walk recorded one (#652): a COMPACT
1902
+ # `class Admin::UsersController` contributes the single entry `["Admin::UsersController"]`, so a
1903
+ # bare `User` guard there narrows to the top-level `::User` exactly as Ruby resolves it, while
1904
+ # the nested `module Admin; class UsersController` spelling still reaches `Admin::User`. Peeling
1905
+ # `scope.self_type.class_name` could not tell the two apart — the string is identical — and
1906
+ # answered `Admin::User` for both, reporting `undefined method` on correct code across all four
1907
+ # shapes at once. Because the repair lives in the shared derivation, `is_a?`, `case`/`when`, `===`
1908
+ # and the constant typer were fixed together rather than one guard shape at a time. A scope no
1909
+ # declaration walk built still falls back to the peel; see {Reflection.lexical_nesting_chain}.
1847
1910
  def resolve_class_name_lexically(bare_name, scope)
1848
- return bare_name if bare_name.include?("::") # Already qualified.
1849
-
1850
1911
  chain = lexical_nesting_for(scope)
1851
1912
  chain.each do |prefix|
1852
1913
  candidate = "#{prefix}::#{bare_name}"
@@ -1864,20 +1925,12 @@ module Rigor
1864
1925
  scope.discovered_classes.key?(candidate)
1865
1926
  end
1866
1927
 
1867
- # Approximates `Module.nesting` from the inferable `self_type`. Today's implementation
1868
- # handles the common case: when the surrounding method is a regular instance method
1869
- # (`self_type = Nominal[T]`) or a class-body / singleton (`self_type = Singleton[T]`),
1870
- # the chain is `T`'s namespace path — `Foo::Bar::Baz` → `["Foo::Bar::Baz", "Foo::Bar",
1871
- # "Foo"]`. Returns an empty array when `self_type` is unknown.
1928
+ # `Module.nesting` for the guard's body, innermost first. Delegates to
1929
+ # {Reflection.lexical_nesting_chain} the SINGLE owner of the question, shared with the constant
1930
+ # typer's step-1 ladder, so `User` read as a value and `x.is_a?(User)` written beside it cannot
1931
+ # resolve to two different classes ([#652](https://github.com/rigortype/rigor/issues/652)).
1872
1932
  def lexical_nesting_for(scope)
1873
- self_type = scope.self_type
1874
- base = case self_type
1875
- when Type::Nominal, Type::Singleton then self_type.class_name
1876
- end
1877
- return [] if base.nil? || base.empty?
1878
-
1879
- parts = base.split("::")
1880
- parts.each_index.map { |i| parts[0..-(i + 1)].join("::") }
1933
+ Reflection.lexical_nesting_chain(scope)
1881
1934
  end
1882
1935
 
1883
1936
  def class_predicate_scopes(scope, name, current, class_name, exact:)
@@ -1934,7 +1987,7 @@ module Rigor
1934
1987
  # Only static class/module receivers narrow here — the Range / Regexp literal receivers
1935
1988
  # (`(1..10) === x.foo`) are not a common method-chain shape and stay deferred.
1936
1989
  def analyse_case_equality_on_chain(receiver, chain_arg, scope)
1937
- class_name = static_class_name(receiver)
1990
+ class_name = lexical_class_name(receiver, scope)
1938
1991
  return nil if class_name.nil?
1939
1992
 
1940
1993
  address = stable_chain_address(chain_arg)
@@ -1952,7 +2005,7 @@ module Rigor
1952
2005
  end
1953
2006
 
1954
2007
  def analyse_case_equality_receiver(receiver, scope, local_name, current)
1955
- if (class_name = static_class_name(receiver))
2008
+ if (class_name = lexical_class_name(receiver, scope))
1956
2009
  return class_predicate_scopes(scope, local_name, current, class_name, exact: false)
1957
2010
  end
1958
2011
 
@@ -2192,7 +2245,7 @@ module Rigor
2192
2245
  return integer_literal_when_result(current, int_literal, falsey_acc)
2193
2246
  end
2194
2247
 
2195
- target = static_class_name(condition) || case_equality_target_class(condition)
2248
+ target = lexical_class_name(condition, scope) || case_equality_target_class(condition)
2196
2249
  return class_when_result(scope, current, target, falsey_acc) if target
2197
2250
 
2198
2251
  nil
@@ -2304,6 +2357,25 @@ module Rigor
2304
2357
  end
2305
2358
  end
2306
2359
 
2360
+ # The class a constant reference denotes at THIS point in the source (#635). `case t when
2361
+ # Type::Union` inside `module Rigor` names `Rigor::Type::Union`, and narrowing to the
2362
+ # source spelling instead left the guarded receiver an unknown nominal whose every reader
2363
+ # dispatched to `Dynamic[top]`. The resolution APPROXIMATES Ruby's — see
2364
+ # {#resolve_class_name_lexically} for where the derived nesting chain diverges (#652).
2365
+ #
2366
+ # Shared by the case-equality shapes (`Class === x`, `case/when`) so one spelling narrows
2367
+ # to one name whichever guard it is written in; `is_a?` / `kind_of?` / `instance_of?` go
2368
+ # through {#rooted_class_predicate_name}, which layers #614's extra decline on top of the
2369
+ # same walk. A rooted `::Foo` names the top level and never a lexically nearer shadow, so
2370
+ # it keeps the un-walked spelling. nil for any non-constant shape, as before.
2371
+ def lexical_class_name(node, scope)
2372
+ bare_name = static_class_name(node)
2373
+ return nil if bare_name.nil?
2374
+ return bare_name if Source::ConstantPath.rooted?(node)
2375
+
2376
+ resolve_class_name_lexically(bare_name, scope)
2377
+ end
2378
+
2307
2379
  # ----- narrow_class / narrow_not_class helpers -----
2308
2380
 
2309
2381
  # Polarity-aware dispatch table for {.narrow_class} / {.narrow_not_class}. Avoids
@@ -2364,9 +2436,34 @@ module Rigor
2364
2436
  context.polarity == :positive ? narrow_class_other(type, class_name) : type
2365
2437
  end
2366
2438
 
2439
+ # Issue #657 — `Bot` is the assertion "this can never match", and only a KNOWN-disjoint ordering
2440
+ # supports it. `subclass_of?` folds `:disjoint` and `:unknown` into one `false`, which is safe on
2441
+ # the negative edge (where the pre-state survives) and an over-claim on the positive one:
2442
+ #
2443
+ # module Taggable; def tag = "t"; end
2444
+ # class Integer; include Taggable; end
2445
+ # x = 1
2446
+ # case x
2447
+ # when Taggable then x.tag # reported unreachable; `1.is_a?(Taggable)` is true
2448
+ # end
2449
+ #
2450
+ # The ordering is `:unknown` because an in-source `include` into a CORE class never reaches the
2451
+ # environment's class ordering — the include is discovered in project source, `Integer`'s ancestry
2452
+ # comes from RBS, and the two are not joined (the precision half, left in #657).
2453
+ #
2454
+ # `:unknown` joins to Dynamic, the same answer {#narrow_nominal_to_class} gives and for the same
2455
+ # reason: keeping the old bound licenses `call.undefined-method` on the guard-proven branch. Keeping
2456
+ # the literal was tried first and did exactly that — `s = "str"` under `when Unrelated` stayed
2457
+ # `Constant["str"]` and reported `undefined method 'other'`, trading one false positive for
2458
+ # another. The value is lost only on the guarded arm, which is where the guard just told us the
2459
+ # environment's picture was incomplete.
2367
2460
  def narrow_constant_to_class(constant, class_name, context)
2368
2461
  rigor_class = constant.value.class.name
2369
- subclass_of?(rigor_class, class_name, context) ? constant : Type::Combinator.bot
2462
+ return constant if subclass_of?(rigor_class, class_name, context)
2463
+ return Type::Combinator.untyped if !context.exact &&
2464
+ class_ordering(rigor_class, class_name, context) == :unknown
2465
+
2466
+ Type::Combinator.bot
2370
2467
  end
2371
2468
 
2372
2469
  def narrow_constant_not_class(constant, class_name, context)
@@ -2387,7 +2484,13 @@ module Rigor
2387
2484
  case class_ordering(nominal.class_name, class_name, context)
2388
2485
  when :superclass then Type::Combinator.nominal_of(class_name)
2389
2486
  when :disjoint then Type::Combinator.bot
2390
- else nominal # :subclass preserves the bound; :unknown stays conservative
2487
+ when :subclass then nominal
2488
+ else
2489
+ # :unknown — the guard proved membership in a class the environment cannot order against the
2490
+ # bound (an RBS-less gem class, typically). Keeping the old bound licenses `undefined-method`
2491
+ # on the guard-proven branch (concurrent-ruby's `done.is_a?(Concurrent::Maybe)` kept `Array`
2492
+ # and fired on `.value`); the honest join is Dynamic — the guard destroyed the old knowledge.
2493
+ Type::Combinator.untyped
2391
2494
  end
2392
2495
  end
2393
2496
 
@@ -2606,6 +2709,209 @@ module Rigor
2606
2709
  [scope.public_send(writer, receiver.name, non_nil), scope]
2607
2710
  end
2608
2711
 
2712
+ # Issue #606 slice 1 — the SAFE-NAV CHAIN forms. {.analyse_safe_nav_receiver} above proves
2713
+ # the receiver non-nil when the `&.` call is itself the predicate (`if v&.foo`). It cannot
2714
+ # see the far more common shape where the `&.` result is fed to one more call:
2715
+ #
2716
+ # if custom&.value.present? # mastodon extended_description.rb / privacy_policy.rb
2717
+ # return true if backup&.user.nil? # mastodon backup_worker.rb (the FALL-THROUGH proves it)
2718
+ #
2719
+ # Both were measured firing `call.possible-nil-receiver` on correct code (#574's matrix).
2720
+ #
2721
+ # ## Why a truthy chain proves the root non-nil, and when it does not
2722
+ #
2723
+ # If the root IS nil, `root&.y` is nil and every suffix runs ON nil. Reaching the guarded
2724
+ # branch then requires each of them to have returned — and the outermost to have returned
2725
+ # TRUTHY. Two things can stop that, and only these two are sound to rely on:
2726
+ #
2727
+ # 1. `NilClass` does not define the method. The call raises, so control never reaches the
2728
+ # branch at all, and narrowing the branch body is vacuously correct.
2729
+ # 2. The method is defined on `NilClass` and documented falsey there — {NIL_FALSEY_SUFFIXES}.
2730
+ # ActiveSupport's `nil.present?` is `false`, so a truthy answer excludes the nil root.
2731
+ #
2732
+ # Anything else must decline, and the temptation to wave this through is exactly where the
2733
+ # bug would be: most of what `NilClass` DOES define returns something truthy. `nil.to_s` is
2734
+ # `""`, `nil.to_a` is `[]`, `nil.to_i` is `0`, `nil.inspect` is `"nil"` — all truthy — and
2735
+ # `nil.nil?` and `nil.blank?` are literally `true`. Narrowing on `x&.y.nil?`'s TRUTHY edge
2736
+ # would invert the guard's meaning. `nil?` therefore gets the opposite edge instead: a
2737
+ # FALSEY `x&.y.nil?` says `x&.y` is not nil, which no nil root can produce.
2738
+ #
2739
+ # Scoped to chains that actually contain `&.`. The same raise-based argument would license
2740
+ # narrowing a plain `x.y.present?` chain, but there `x.y` is itself the latent nil bug this
2741
+ # rule exists to report, and suppressing it is the opposite of the intent.
2742
+ NIL_FALSEY_SUFFIXES = %i[present?].freeze
2743
+ private_constant :NIL_FALSEY_SUFFIXES
2744
+
2745
+ def analyse_safe_nav_chain(node, scope)
2746
+ chain = safe_nav_chain(node)
2747
+ return nil if chain.nil?
2748
+
2749
+ receiver, suffixes = chain
2750
+ edge = safe_nav_chain_edge(suffixes, scope)
2751
+ return nil if edge.nil?
2752
+
2753
+ narrowed = safe_nav_chain_narrowed_scope(receiver, scope)
2754
+ return nil if narrowed.nil?
2755
+
2756
+ edge == :truthy ? [narrowed, scope] : [scope, narrowed]
2757
+ end
2758
+
2759
+ # Walks a call chain down to its `&.` link, collecting the suffix method names applied
2760
+ # above it (outermost first). Declines unless the chain bottoms out in a `&.` call on a
2761
+ # direct local / ivar read — those are the two bindings a scope can rewrite — and unless
2762
+ # every suffix is a bare reader or predicate, since an argument or a block could make the
2763
+ # answer depend on something other than the receiver.
2764
+ #
2765
+ # `allow_bare:` false keeps the zero-suffix case (`if v&.foo`) with
2766
+ # {.analyse_safe_nav_receiver}, which already owns it; the equality form passes true
2767
+ # because there the `&.` call IS the comparison's receiver.
2768
+ def safe_nav_chain(node, allow_bare: false)
2769
+ suffixes = []
2770
+ current = node
2771
+ while current.is_a?(Prism::CallNode) && !current.safe_navigation?
2772
+ return nil unless argument_free?(current) && current.block.nil?
2773
+
2774
+ suffixes << current.name
2775
+ current = current.receiver
2776
+ end
2777
+ return nil unless current.is_a?(Prism::CallNode) && current.safe_navigation?
2778
+ return nil if suffixes.empty? && !allow_bare
2779
+
2780
+ receiver = current.receiver
2781
+ return nil unless receiver.is_a?(Prism::LocalVariableReadNode) ||
2782
+ receiver.is_a?(Prism::InstanceVariableReadNode)
2783
+
2784
+ [receiver, suffixes]
2785
+ end
2786
+
2787
+ # Which edge — if either — a chain's outcome narrows on. The inner suffixes (everything
2788
+ # below the outermost) only ever have to NOT return, so they are held to the raise test
2789
+ # alone; the outermost decides the edge per the table in {NIL_FALSEY_SUFFIXES}'s comment.
2790
+ def safe_nav_chain_edge(suffixes, scope)
2791
+ outermost, *inner = suffixes
2792
+ return nil unless inner.all? { |name| !nilclass_method?(name, scope) }
2793
+ return :falsey if outermost == :nil?
2794
+ return :truthy if NIL_FALSEY_SUFFIXES.include?(outermost)
2795
+ return :truthy unless nilclass_method?(outermost, scope)
2796
+
2797
+ nil
2798
+ end
2799
+
2800
+ def safe_nav_chain_narrowed_scope(receiver, scope)
2801
+ reader, writer =
2802
+ case receiver
2803
+ when Prism::LocalVariableReadNode then %i[local with_local]
2804
+ when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
2805
+ else return nil
2806
+ end
2807
+
2808
+ current = scope.public_send(reader, receiver.name)
2809
+ return nil if current.nil?
2810
+
2811
+ non_nil = narrow_non_nil(current)
2812
+ return nil if non_nil.equal?(current)
2813
+
2814
+ scope.public_send(writer, receiver.name, non_nil)
2815
+ end
2816
+
2817
+ # Issue #606 slice 1, second form — a safe-nav chain COMPARED to a value that cannot be
2818
+ # nil: `next unless status&.account_id == @account.id`. A nil root makes the left side nil,
2819
+ # and `nil == <non-nil>` is false, so a true comparison excludes it.
2820
+ #
2821
+ # The whole form turns on proving the OTHER operand non-nil, and `x&.y == nil` is the
2822
+ # counter-example that must never narrow — there a nil root makes the comparison TRUE.
2823
+ # {.provably_non_nil_type?} is deliberately a whitelist rather than "not obviously nil":
2824
+ # an operand typed `Dynamic` may well be nil at runtime, so it declines. That costs the
2825
+ # third corpus site (`@account.id` types opaque there), which is the honest price of not
2826
+ # guessing.
2827
+ #
2828
+ # `!=` takes the opposite edge, for the same reason `nil?` did above: a nil root makes
2829
+ # `nil != <non-nil>` true, so it is the FALSEY edge that proves the root non-nil.
2830
+ def analyse_safe_nav_chain_equality(node, scope)
2831
+ return nil if node.arguments.nil? || node.arguments.arguments.size != 1
2832
+
2833
+ chain = safe_nav_chain(node.receiver, allow_bare: true)
2834
+ return nil if chain.nil?
2835
+
2836
+ receiver, suffixes = chain
2837
+ return nil unless suffixes.all? { |name| !nilclass_method?(name, scope) }
2838
+ return nil unless provably_non_nil_type?(scope.type_of(node.arguments.arguments.first))
2839
+
2840
+ narrowed = safe_nav_chain_narrowed_scope(receiver, scope)
2841
+ return nil if narrowed.nil?
2842
+
2843
+ node.name == :== ? [narrowed, scope] : [scope, narrowed]
2844
+ end
2845
+
2846
+ # Whitelist: true only for types every inhabitant of which is non-nil. `Dynamic` / `Top`
2847
+ # answer FALSE — an untyped operand is exactly the case where nil cannot be excluded —
2848
+ # and so does `Bot`, which has no inhabitants to reason about.
2849
+ def provably_non_nil_type?(type)
2850
+ case type
2851
+ when Type::Constant then !type.value.nil?
2852
+ when Type::Nominal then type.class_name != "NilClass"
2853
+ when Type::Singleton, Type::Tuple, Type::HashShape then true
2854
+ when Type::Union then type.members.all? { |member| provably_non_nil_type?(member) }
2855
+ end
2856
+ end
2857
+
2858
+ # Issue #606 slice 2 — MEMBERSHIP. `collection.include?(x)` answering true proves `x` is one
2859
+ # of the collection's elements, so if none of those can be nil, neither can `x`:
2860
+ #
2861
+ # unless Redmine::Reaction::REACTABLE_TYPES.include?(object_type) # redmine reactions_controller.rb
2862
+ # render_403
2863
+ # return
2864
+ # end
2865
+ # @object = object_type.constantize.find(...) # ← fired before this
2866
+ #
2867
+ # The proof lives entirely in the collection, which is why {.nil_free_collection?} answers
2868
+ # only for shapes whose element types are actually known — a literal / `Tuple`, or an
2869
+ # `Array` / `Set` carrying an element type. An untyped collection declines: `Dynamic` may
2870
+ # hold nil, and "we do not know" must not read as "no nil here".
2871
+ #
2872
+ # Only the truthy edge narrows. A false `include?` says nothing about `x` — a nil `x` is
2873
+ # simply not in the list, which is exactly what false means.
2874
+ MEMBERSHIP_COLLECTION_CLASSES = %w[Array Set].freeze
2875
+ private_constant :MEMBERSHIP_COLLECTION_CLASSES
2876
+
2877
+ def analyse_membership_predicate(node, scope)
2878
+ return nil if node.receiver.nil? || node.arguments.nil?
2879
+ return nil unless node.arguments.arguments.size == 1
2880
+
2881
+ argument = node.arguments.arguments.first
2882
+ reader, writer =
2883
+ case argument
2884
+ when Prism::LocalVariableReadNode then %i[local with_local]
2885
+ when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
2886
+ else return nil
2887
+ end
2888
+ return nil unless nil_free_collection?(scope.type_of(node.receiver))
2889
+
2890
+ current = scope.public_send(reader, argument.name)
2891
+ return nil if current.nil?
2892
+
2893
+ non_nil = narrow_non_nil(current)
2894
+ return nil if non_nil.equal?(current)
2895
+
2896
+ [scope.public_send(writer, argument.name, non_nil), scope]
2897
+ end
2898
+
2899
+ # True only when every inhabitant the collection can yield is non-nil. Restricted to
2900
+ # sequence shapes on purpose: `Hash#include?` asks about KEYS and `Range#include?` about
2901
+ # bounds, so neither element story is the one this rule reasons about.
2902
+ def nil_free_collection?(type)
2903
+ case type
2904
+ when Type::Tuple
2905
+ !type.elements.empty? && type.elements.all? { |element| provably_non_nil_type?(element) }
2906
+ when Type::Constant
2907
+ type.value.is_a?(Array) && !type.value.empty? && type.value.none?(&:nil?)
2908
+ when Type::Nominal
2909
+ MEMBERSHIP_COLLECTION_CLASSES.include?(type.class_name) &&
2910
+ type.type_args.size == 1 &&
2911
+ provably_non_nil_type?(type.type_args.first)
2912
+ end
2913
+ end
2914
+
2609
2915
  # Layers the safe-nav non-nil truthy narrowing over the edges an existing predicate path
2610
2916
  # already produced, so a safe-nav string predicate (`v&.start_with?(x)`) keeps its
2611
2917
  # relational fact AND proves `v` non-nil on the truthy edge. Re-runs the predicate's
@@ -2645,8 +2951,6 @@ module Rigor
2645
2951
  [truthy_a.join(truthy_b), falsey_b]
2646
2952
  end
2647
2953
  end
2648
- # rubocop:enable Metrics/ClassLength
2649
2954
  end
2650
- # rubocop:enable Metrics/ModuleLength
2651
2955
  end
2652
2956
  end
@@ -260,6 +260,7 @@ module Rigor
260
260
  singleton_def_nodes: :discovered_singleton_def_nodes,
261
261
  def_sources: :discovered_def_sources,
262
262
  superclasses: :discovered_superclasses,
263
+ header_nestings: :discovered_header_nestings,
263
264
  includes: :discovered_includes,
264
265
  class_sources: :discovered_class_sources,
265
266
  method_visibilities: :discovered_method_visibilities,
@@ -54,6 +54,21 @@ module Rigor
54
54
  # Value-pinning a *provably* frozen single-write literal cross-file is a strictly later question; it needs
55
55
  # its own FP measurement and is deliberately not attempted here.
56
56
  #
57
+ # **Issue #644 asked that later question and answered it.** The discovery pre-pass now publishes a
58
+ # project-wide table of syntactic frozen-scalar literal assignments (Symbol / Integer / Float / boolean,
59
+ # written once, in one file), with the FP measurement this paragraph asked for. Where that table and this
60
+ # one both answer a name, THAT TABLE WINS and the value is not erased — a listed `TIMEOUT = 30` reads
61
+ # `30`, not `Integer`. Nothing above is wrong; its scope is narrower than it reads. The widening exists
62
+ # for the rvalues whose basis this collector cannot fully see, and every hazard it names — the closed
63
+ # `HashShape`, the `Tuple` displacing an RBS overload, the mutable String — is a form #644's table
64
+ # DECLINES. So the two producers do not overlap where the widening earns its cost, and where they do
65
+ # overlap the narrower, purely syntactic basis is the better-founded answer. Normative in
66
+ # `docs/internal-spec/inference-engine.md` § "Cross-file value constants"; pinned by
67
+ # `spec/rigor/analysis/pre_eval_constants_spec.rb`.
68
+ #
69
+ # What this collector still owns alone: every rvalue #644 declines — a String, an Array, a Hash, a class
70
+ # alias, and anything that needs the rvalue TYPED rather than read off the syntax.
71
+ #
57
72
  # ## The multi-file write rule — widen on conflict, all the way to `Dynamic[top]`
58
73
  #
59
74
  # Within one file, `ScopeIndexer#record_constant_write` unions repeated writes; that union is widened as a
@@ -153,9 +153,12 @@ module Rigor
153
153
  when Type::Bot then :bot
154
154
  when Type::Top then :top
155
155
  when Type::Constant then :constant
156
- when Type::Nominal, Type::Singleton then :nominal
156
+ when Type::Nominal, Type::Singleton,
157
+ Type::DataClass, Type::StructClass then :nominal
157
158
  when Type::Tuple, Type::HashShape,
158
- Type::IntegerRange, Type::App then :shaped
159
+ Type::IntegerRange, Type::App,
160
+ Type::DataInstance, Type::StructInstance,
161
+ Type::BoundMethod then :shaped
159
162
  when Type::Refined then :refined
160
163
  when Type::Dynamic then classify_dynamic(type)
161
164
  when Type::Union then worst_of(type.members)
@@ -151,7 +151,8 @@ module Rigor
151
151
  def descend_class_or_module(node, qualified_prefix, in_singleton_class, source_path, entries)
152
152
  name = Source::ConstantPath.qualified_name_or_nil(node.constant_path)
153
153
  if name && node.body
154
- walk_node(node.body, qualified_prefix + [name], in_singleton_class, source_path, entries)
154
+ child_prefix = Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
155
+ walk_node(node.body, child_prefix, in_singleton_class, source_path, entries)
155
156
  else
156
157
  walk_children(node, qualified_prefix, in_singleton_class, source_path, entries)
157
158
  end