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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "type"
4
+ require_relative "analysis/dependency_recorder"
4
5
 
5
6
  module Rigor
6
7
  # Read-side facade over Rigor's three reflection sources:
@@ -76,6 +77,21 @@ module Rigor
76
77
  loader.class_known?(class_name)
77
78
  end
78
79
 
80
+ # Issue #735 — true when the class's RBS declaration is the PROJECT's own (a file under
81
+ # `signature_paths:`) rather than bundled core / stdlib / gem RBS. The two carry different authority:
82
+ # a bundled signature describes a class the project does not own, so a project `def` on it is the
83
+ # ADR-17 monkey-patch the analyzer reports; a project sidecar describes the very source being
84
+ # analysed, and a `def` in another of the project's files is that class's own definition.
85
+ #
86
+ # False whenever the question cannot be answered (no loader, no environment, an environment cached
87
+ # before #725 preserved buffer names), which is the pre-#735 reading in every case.
88
+ def project_declared_class?(class_name, scope: nil, environment: nil)
89
+ loader = rbs_loader_for(scope, environment)
90
+ return false if loader.nil?
91
+
92
+ loader.project_declared_class?(class_name)
93
+ end
94
+
79
95
  # @return [Symbol] one of `:equal`, `:subclass`, `:superclass`,
80
96
  # `:disjoint`, `:unknown`.
81
97
  def class_ordering(lhs, rhs, scope: Scope.empty)
@@ -118,22 +134,35 @@ module Rigor
118
134
  # RBS because the user's source is authoritative for its own constants. Returns the matched
119
135
  # `Rigor::Type`, or nil when no source knows the constant.
120
136
  #
137
+ # `rooted:` reports that the reference was written with a leading `::` (`::Foo`,
138
+ # `::Foo::Bar`). Ruby's absolute form names the TOP-LEVEL constant unconditionally, so the
139
+ # lexical ladder (steps 1 and 2) MUST be skipped and only the bare candidate consulted. The
140
+ # name itself stays un-rooted — every discovered-constant table is keyed that way — so the
141
+ # marker travels beside it rather than inside it (#614). When no top-level constant answers,
142
+ # the result is nil exactly as for an unknown constant: a lexically nearer shadow is never the
143
+ # answer to `::Foo`, and falling back to it was the bug (`::Rails` inside `module MyApp` typed
144
+ # as `MyApp::Rails`).
145
+ #
121
146
  # This is the shared owner of the lexical-constant resolution: `Inference::ExpressionTyper`
122
147
  # reads it to type a constant read, and `Inference::Narrowing` reads it to recognise a
123
148
  # value-pinned `Constant[Regexp]` match-predicate operand.
124
- def resolve_constant_type(name, scope: Scope.empty)
125
- prefix = enclosing_class_path(scope)
149
+ def resolve_constant_type(name, scope: Scope.empty, rooted: false)
150
+ # Issue #644 — the ADR-46 name edge, recorded ONCE PER REFERENCE and BEFORE the ladder, so it does not
151
+ # depend on which resolver answers. A reference that resolves through RBS (`Math::PI`) or through the
152
+ # class registry is exactly as dependent on the project's constant write set as one that resolves
153
+ # through `in_source_constants`: a project file that later assigns the name wins over the RBS answer,
154
+ # and without this edge the reader keeps serving the pre-assignment type. That staleness is NEW with
155
+ # the cross-file publication — before it, a project value write never won at a reader, so nothing could
156
+ # go stale. Over-recording is ADR-46's safe direction.
157
+ record_constant_reference(name) if Analysis::DependencyRecorder.active?
158
+ return constant_type_at(name, scope) if rooted
159
+ # Issue #716 — a body whose `Module.nesting` was RECORDED EMPTY is definitively top level, and Ruby
160
+ # resolves its constants there no matter which namespace calls it. Steps 1 and 2 both derive from the
161
+ # caller and are both wrong for it, so it takes its own ladder.
162
+ return toplevel_first_constant_type(name, scope) if recorded_toplevel_nesting?(scope)
126
163
 
127
164
  # Step 1 — `Module.nesting`, innermost first. Each entry contributes only its OWN constants.
128
- walker = prefix
129
- while walker && !walker.empty?
130
- hit = constant_type_at("#{walker}::#{name}", scope)
131
- return hit if hit
132
-
133
- idx = walker.rindex("::")
134
- walker = idx ? walker[0, idx] : nil
135
- end
136
-
165
+ #
137
166
  # Step 2 (#354) — the ancestors of the innermost cresting scope, which Ruby consults BEFORE
138
167
  # falling back to the top level. Skipping this step did not merely lose a resolution: when the
139
168
  # same name also exists at top level, step 3 answered a lookup Ruby gives to the ancestor, so
@@ -141,16 +170,67 @@ module Rigor
141
170
  # wrong type on correct code, which outranks any worst-case reading (AGENTS.md
142
171
  # § "Implementation Guidelines"). Only project ancestors are walked; an RBS-known superclass
143
172
  # contributes no name here (see {.ancestor_constant_scopes}).
144
- if prefix
145
- ancestor_constant_scopes(prefix, scope).each do |ancestor|
146
- hit = constant_type_at("#{ancestor}::#{name}", scope)
147
- return hit if hit
148
- end
173
+ #
174
+ # Step 3 — the bare name (top level).
175
+ first_constant_hit(lexical_nesting_chain(scope), name, scope) ||
176
+ ancestor_constant_type(name, scope, enclosing_class_path(scope)) ||
177
+ constant_type_at(name, scope)
178
+ end
179
+
180
+ # The first candidate `<entry>::<name>` any source knows, walking `entries` in order, or nil. The one
181
+ # place a qualifying prefix is joined to a name, so every rung of the ladder consults its candidates
182
+ # identically.
183
+ def first_constant_hit(entries, name, scope)
184
+ entries.each do |entry|
185
+ hit = constant_type_at("#{entry}::#{name}", scope)
186
+ return hit if hit
149
187
  end
188
+ nil
189
+ end
190
+ private_class_method :first_constant_hit
150
191
 
151
- # Step 3 the bare name (top level).
152
- constant_type_at(name, scope)
192
+ # Step 2's rung on its own: nil when there is no enclosing class path to take ancestors of.
193
+ def ancestor_constant_type(name, scope, prefix)
194
+ return nil if prefix.nil? || prefix.empty?
195
+
196
+ first_constant_hit(ancestor_constant_scopes(prefix, scope), name, scope)
153
197
  end
198
+ private_class_method :ancestor_constant_type
199
+
200
+ # Issue #716 — whether the scope carries a RECORDED empty `Module.nesting`, which is the declaration
201
+ # walk's positive statement that the body is written at the top level (`Inference::ScopeIndexer#
202
+ # walk_def_nestings`). Distinct from `nil`, which still means "no declaration walk built this scope" and
203
+ # keeps the peel ({.lexical_nesting_chain}); the two are only distinguishable because the table is
204
+ # populated by that walk alone.
205
+ def recorded_toplevel_nesting?(scope) = scope.lexical_nesting.is_a?(Array) && scope.lexical_nesting.empty?
206
+ private_class_method :recorded_toplevel_nesting?
207
+
208
+ # Issue #716 — the ladder for a top-level body: the top level FIRST, then the caller-derived rungs
209
+ # unchanged. The candidate SET is exactly what {.resolve_constant_type}'s three steps consult for the
210
+ # same scope — only the order moves — so no read that resolves today stops resolving, and any read where
211
+ # the two disagree is one where the top level is Ruby's answer and the caller's namespace never was.
212
+ #
213
+ # Keeping the caller-derived rungs at all is deliberate and unsound against Ruby: they cover reads whose
214
+ # name exists ONLY under some namespace, where Ruby raises `NameError` (486 such reads on gitlab, 33 on
215
+ # dependabot-core — `docs/notes/20260905-toplevel-def-cref-movable-sites.md`). Rigor reports nothing
216
+ # about them either way, so retracting them would trade a silent wrong answer for a silent absent one at
217
+ # no gain. Making them FIRE is a separate question with its own false-positive budget.
218
+ def toplevel_first_constant_type(name, scope)
219
+ constant_type_at(name, scope) || caller_derived_constant_type(name, scope)
220
+ end
221
+ private_class_method :toplevel_first_constant_type
222
+
223
+ # The peel and its ancestors — {.resolve_constant_type}'s steps 1 and 2 as they answer for a scope with
224
+ # no recorded chain, factored out so the top-level ladder above can consult them BELOW the top level
225
+ # instead of above it.
226
+ def caller_derived_constant_type(name, scope)
227
+ prefix = enclosing_class_path(scope)
228
+ return nil if prefix.nil? || prefix.empty?
229
+
230
+ first_constant_hit(peeled_nesting(prefix), name, scope) ||
231
+ ancestor_constant_type(name, scope, prefix)
232
+ end
233
+ private_class_method :caller_derived_constant_type
154
234
 
155
235
  # One candidate name, consulted in source-precedence order: the class registry (yielding a
156
236
  # `Singleton[C]`), source-discovered classes, in-source value constants, then RBS-side
@@ -166,12 +246,27 @@ module Rigor
166
246
  return in_source_class if in_source_class
167
247
 
168
248
  in_source_value = scope.in_source_constants[candidate]
169
- return in_source_value if in_source_value
249
+ if in_source_value
250
+ scope.record_constant_dependency(candidate) if Analysis::DependencyRecorder.active?
251
+ return in_source_value
252
+ end
170
253
 
171
254
  env.constant_for_name(candidate)
172
255
  end
173
256
  private_class_method :constant_type_at
174
257
 
258
+ # Issue #644 — the load-bearing ADR-46 edge for a constant reference: `constant:<last segment>`, keyed on
259
+ # the NAME rather than on a file. The published value is a function of the whole project's write set for
260
+ # the name, so it moves when a SECOND file starts or stops assigning it — a file this reader has no other
261
+ # relationship with, and which no positive edge could reach. Matched against the publication diff
262
+ # ({Analysis::Incremental.changed_constant_publications}). Gated by the caller on the recorder being
263
+ # active, so an ordinary run pays one integer read.
264
+ def record_constant_reference(name)
265
+ segment = name.delete_prefix("::").split("::").last
266
+ Analysis::DependencyRecorder.read_name(:constant, segment) if segment
267
+ end
268
+ private_class_method :record_constant_reference
269
+
175
270
  # #354 — the project classes and modules whose own constants `class_name` inherits, in Ruby's
176
271
  # ancestor order: included / prepended modules before the superclass (Ruby places mixins nearer),
177
272
  # transitively, breadth-first. `class_name` itself is excluded — step 1 already covered it.
@@ -235,17 +330,14 @@ module Rigor
235
330
  end
236
331
  private_class_method :compute_ancestor_constant_scopes
237
332
 
238
- # Resolves an ancestor name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) against the
239
- # subclass's lexical nesting, innermost first — the same walk
240
- # `ExpressionTyper#compute_ancestor_class_name` performs for method lookup. Returns nil when no
241
- # candidate names a discovered project class or module.
333
+ # Resolves an ancestor name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) against the nesting in
334
+ # force where the subclass's header is written`Scope#ancestor_name_candidates`, the single
335
+ # owner of that order, which `Scope#enqueue_ancestors` reads for method lookup and the
336
+ # override-visibility rule reads for its own walk. Returns nil when no candidate names a
337
+ # discovered project class or module.
242
338
  def resolve_ancestor_name(subclass_qualified, raw, scope)
243
- segments = subclass_qualified.split("::")
244
- (segments.length - 1).downto(0) do |i|
245
- candidate = (segments[0, i] + [raw]).join("::")
246
- return candidate if known_project_namespace?(candidate, scope)
247
- end
248
- nil
339
+ scope.ancestor_name_candidates(subclass_qualified, raw)
340
+ .find { |candidate| known_project_namespace?(candidate, scope) }
249
341
  end
250
342
  private_class_method :resolve_ancestor_name
251
343
 
@@ -266,6 +358,49 @@ module Rigor
266
358
  end
267
359
  private_class_method :enclosing_class_path
268
360
 
361
+ # Ruby's `Module.nesting` for the body `scope` sits in, innermost first — the chain step 1 of
362
+ # {.resolve_constant_type} walks, and the chain `Inference::Narrowing` walks to resolve a class-guard
363
+ # name. The single owner of the question, so a constant read and the `is_a?` / `case`-`when` / `===`
364
+ # guard over the same spelling cannot disagree about which constant it names.
365
+ #
366
+ # The chain the analyzer RECORDED at declaration time wins ({Scope#with_lexical_nesting}). It is not
367
+ # recoverable from `self_type`: `class Admin::UsersController` and `module Admin; class
368
+ # UsersController` produce the identical `class_name`, while Ruby's nesting is
369
+ # `[Admin::UsersController]` for the first and `[Admin::UsersController, Admin]` for the second — so a
370
+ # bare `User` in the compact form names `::User`, and peeling the name reached `Admin::User` and
371
+ # reported `undefined method` on correct code ([#652](https://github.com/rigortype/rigor/issues/652)).
372
+ #
373
+ # The peel survives only as the FALLBACK, for a scope no declaration walk built (a callee body
374
+ # re-entered through `Scope#evaluate`, a plugin-constructed scope). A `class << expr` body is NOT
375
+ # one of them — not even `class << SomeOtherConstant`, whose class-frame stack the evaluator
376
+ # resets: the body inherits the enclosing chain, because Ruby pushes no `Module.nesting` entry
377
+ # there either. Answering an empty chain there would retract resolutions the
378
+ # engine makes today and turn correct code into `Dynamic`; a stale-but-gradual rung costs precision
379
+ # only, which is the direction AGENTS.md § "Implementation Guidelines" mandates.
380
+ #
381
+ # Issue #716 — the recorded value is three-valued, and a RECORDED `[]` is not the absence of one: it is
382
+ # the declaration walk saying "top level", and this returns it verbatim so `Module.nesting` reads what
383
+ # Ruby reads. {.resolve_constant_type} keeps the peel for that case as a rung BELOW the top level rather
384
+ # than through this reader, because a chain is not where an out-of-order fallback belongs.
385
+ def lexical_nesting_chain(scope)
386
+ recorded = scope.lexical_nesting
387
+ return recorded if recorded
388
+
389
+ base = enclosing_class_path(scope)
390
+ return [] if base.nil? || base.empty?
391
+
392
+ peeled_nesting(base)
393
+ end
394
+
395
+ # The peel itself: a qualified name split into the chain the NESTED spelling would have produced
396
+ # (`"Admin::Users::Show"` → `["Admin::Users::Show", "Admin::Users", "Admin"]`). Shared by the fallback
397
+ # above and by issue #716's below-the-top-level rung, so the two cannot drift apart.
398
+ def peeled_nesting(base)
399
+ parts = base.split("::")
400
+ parts.each_index.map { |i| parts[0..-(i + 1)].join("::") }
401
+ end
402
+ private_class_method :peeled_nesting
403
+
269
404
  # Returns the RBS `RBS::Definition::Method` for the instance method, or nil when the
270
405
  # class or method is not in RBS. The source-side discovered-method facts are reachable
271
406
  # through {.discovered_method?}; a future slice will unify the two under a
@@ -340,8 +475,25 @@ module Rigor
340
475
  # @param kind [:instance, :singleton]
341
476
  # @return [Boolean] true when the ScopeIndexer recorded a `def` for the given method on
342
477
  # the given class with the matching kind.
478
+ #
479
+ # ADR-46 — a MISS records a negative cross-file dependency, so a consumer whose analysis
480
+ # turned on "the project does not define this method" is re-checked once a later edit
481
+ # defines it. The engine's own dispatch gets that edge from
482
+ # `Scope#record_cross_file_method`, but this facade is what a PLUGIN gate reads, and a
483
+ # contribution tier answers before dispatch reaches the engine's recording accessors —
484
+ # rigor-railties declines its `Rails.logger` typing exactly on this probe. Without the
485
+ # edge a warm `--incremental` run kept serving the plugin's answer after the project
486
+ # added `def self.logger`. The key grammar is the one `IncrementalSession#negative_key_for`
487
+ # inverts against (`Class#method` instance-side, `Class.method` singleton-side), which is
488
+ # also what `Incremental.appeared_symbols` emits for a newly-defined method.
343
489
  def discovered_method?(class_name, method_name, kind: :instance, scope: Scope.empty)
344
- scope.discovered_method?(class_name, method_name, kind)
490
+ return true if scope.discovered_method?(class_name, method_name, kind)
491
+
492
+ if Analysis::DependencyRecorder.active?
493
+ symbol = "#{class_name}#{kind == :singleton ? '.' : '#'}#{method_name}"
494
+ Analysis::DependencyRecorder.read_missing(:method, symbol)
495
+ end
496
+ false
345
497
  end
346
498
  end
347
499
  end
@@ -19,13 +19,18 @@ module Rigor
19
19
  :in_source_constants,
20
20
  :discovered_methods,
21
21
  :discovered_def_nodes,
22
+ :discovered_def_nestings,
22
23
  :discovered_singleton_def_nodes,
23
24
  :discovered_def_sources,
24
25
  :discovered_singleton_def_sources,
25
26
  :discovered_method_visibilities,
26
27
  :discovered_superclasses,
28
+ :discovered_header_nestings,
27
29
  :discovered_includes,
28
30
  :discovered_class_sources,
31
+ :constant_sources,
32
+ :published_constant_names,
33
+ :local_constant_names,
29
34
  :data_member_layouts,
30
35
  :struct_member_layouts,
31
36
  :param_inferred_types,
@@ -35,7 +40,8 @@ module Rigor
35
40
  class DiscoveryIndex
36
41
  EMPTY_NODE_TABLE = {}.compare_by_identity.freeze
37
42
  EMPTY_TABLE = {}.freeze
38
- private_constant :EMPTY_NODE_TABLE, :EMPTY_TABLE
43
+ EMPTY_NAME_SET = Set.new.freeze
44
+ private_constant :EMPTY_NODE_TABLE, :EMPTY_TABLE, :EMPTY_NAME_SET
39
45
 
40
46
  # The third value a `discovered_methods` entry can hold, beside `:instance` and `:singleton`. One name may
41
47
  # legitimately be defined on both sides of the same class (`def helper` plus a `class << self` twin), and the
@@ -57,13 +63,45 @@ module Rigor
57
63
  in_source_constants: EMPTY_TABLE,
58
64
  discovered_methods: EMPTY_TABLE,
59
65
  discovered_def_nodes: EMPTY_TABLE,
66
+ # Issue #681 — `{Prism::DefNode => Module.nesting}` for every `def` the declaration walk reaches,
67
+ # recorded by `Inference::ScopeIndexer` where the declaration is indexed. Read by
68
+ # `Inference::ExpressionTyper#build_user_method_body_scope`, which rebuilds a callee's body scope
69
+ # from the receiver's type alone and so has no prefix of its own to derive one from. Keyed by node
70
+ # IDENTITY, because an inherited body is re-walked with the subclass as receiver while its
71
+ # constants resolve under the declaration that owns it.
72
+ #
73
+ # Issue #716 — PRESENT with `[]` and ABSENT are different facts, and only this table can tell them
74
+ # apart: it is populated by the declaration walk alone, so `[]` means "walked, and written at the top
75
+ # level" (Ruby's `Module.nesting` there) while an absent entry still means "not recorded" and
76
+ # `Reflection.lexical_nesting_chain` keeps its peel fallback for it.
77
+ discovered_def_nestings: EMPTY_NODE_TABLE,
60
78
  discovered_singleton_def_nodes: EMPTY_TABLE,
61
79
  discovered_def_sources: EMPTY_TABLE,
62
80
  discovered_singleton_def_sources: EMPTY_TABLE,
63
81
  discovered_method_visibilities: EMPTY_TABLE,
64
82
  discovered_superclasses: EMPTY_TABLE,
83
+ # Issue #682 — `{qualified class name => Module.nesting where its declaration HEADER is written}`,
84
+ # innermost first and EXCLUDING the declaration's own entry. Read by `Scope#ancestor_name_candidates`,
85
+ # which resolves a superclass / include name in that cref instead of peeling the subclass's qualified
86
+ # name — the peel is the nested spelling's answer, and it searched an `Admin::Base` that a compact
87
+ # `class Admin::Widget < Base` written at the top level never reaches. An absent entry means "not
88
+ # recorded" and keeps the peel, so a scope that never saw a declaration walk is unchanged.
89
+ discovered_header_nestings: EMPTY_TABLE,
65
90
  discovered_includes: EMPTY_TABLE,
66
91
  discovered_class_sources: EMPTY_TABLE,
92
+ # Issue #644 — `{qualified constant name => Set[declaring file]}`, the write attribution behind the
93
+ # cross-file value-constant table. Read only by `Scope#record_constant_dependency` during ADR-46
94
+ # dependency recording, so the runner seeds it only on a recording run; every other run leaves it
95
+ # empty and the edge costs one nil check.
96
+ constant_sources: EMPTY_TABLE,
97
+ # Issue #644 — the two halves of `Scope#published_constant?`, the question
98
+ # {Analysis::CheckRules::PublishedConstantGuard} asks. `published_constant_names` is the LAST
99
+ # SEGMENTS of the project-wide published table (run-wide, seeded by the runner);
100
+ # `local_constant_names` is the last segments the ANALYSED FILE itself assigns (per file, seeded by
101
+ # `ScopeIndexer.index`). A name in the first and not the second was declared somewhere the reader's
102
+ # author cannot see. Both empty outside a runner-seeded scope, which makes the guard a no-op there.
103
+ published_constant_names: EMPTY_NAME_SET,
104
+ local_constant_names: EMPTY_NAME_SET,
67
105
  data_member_layouts: EMPTY_TABLE,
68
106
  struct_member_layouts: EMPTY_TABLE,
69
107
  # ADR-67 WD3 — the call-site parameter-inference table, keyed by `[class_name, method_name, kind]` (the