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
@@ -54,7 +54,7 @@ module Rigor
54
54
  # by `rigor plugins` (ADR-90) to probe {available?} at activation time — so a standalone install
55
55
  # where inflection silently degrades reports the degradation instead of an unqualified `[OK]`. When
56
56
  # a new plugin adopts the helper, add its manifest id here.
57
- CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord factorybot rails-routes].freeze
57
+ CONSUMER_PLUGIN_IDS = %w[actionmailer actionpack activerecord alba factorybot rails-routes].freeze
58
58
 
59
59
  # Raised when `ActiveSupport::Inflector` is required for an inflection but cannot be loaded. Caught by
60
60
  # the per-plugin isolation boundary, so it surfaces as "this plugin produced no diagnostics" rather than
@@ -199,6 +199,44 @@ module Rigor
199
199
  entries
200
200
  end
201
201
 
202
+ # Issue #1652 — the MEMBERSHIP fingerprint of a `Dir.glob` pattern under `root`, `**` included. Returns the
203
+ # matching paths and records one `:names` {Cache::Descriptor::GlobEntry} over `root` and `pattern`: the row
204
+ # reads stale when a match appears or disappears, in a subdirectory created after the read too, and NOT
205
+ # when a match's bytes or stat tuple move. A caller whose answer depends on a match's content MUST
206
+ # {#read_file} each match it reads; that row carries the edit. A stat signature here would read stale on
207
+ # every `touch`, checkout or restored CI cache, though the content the caller read is unchanged (#979's
208
+ # reasoning). {#list_directory} keeps its `:stat` listing row.
209
+ #
210
+ # The pattern must stay under `root`, since the policy is checked against `root` alone: a `..` segment, a
211
+ # brace (`{..,x}`) or a backslash escape (`.\\./`) raises {AccessDeniedError} (`:read_outside_scope`).
212
+ # The last two are refused outright because `Dir.glob` can spell `..` with them. Out of policy, `root` is
213
+ # globbed truthfully and contributes no row, as in {#list_directory}.
214
+ #
215
+ # @param root — project directory; relative paths expand against the working directory
216
+ # @param pattern — a `Dir.glob` pattern relative to `root`, such as `"ext/**/extconf.rb"`
217
+ # @return the absolute matching paths in `Dir.glob` order
218
+ def glob(root, pattern)
219
+ absolute = File.absolute_path(root.to_s)
220
+ pattern = pattern.to_s
221
+ if pattern.match?(/[{\\]/) || pattern.split("/").include?("..")
222
+ raise AccessDeniedError.new(
223
+ "plugin #{@plugin_id.inspect} cannot glob #{pattern.inspect} under #{absolute.inspect}: " \
224
+ "a `..` segment, a brace or a backslash can leave the root the trusted-read scope was checked against",
225
+ reason: :read_outside_scope,
226
+ resource: File.join(absolute, pattern)
227
+ )
228
+ end
229
+
230
+ entries = Dir.glob(File.join(absolute, pattern))
231
+ unless @policy.allow_read?(absolute)
232
+ record_refusal(absolute)
233
+ return entries
234
+ end
235
+
236
+ record_glob_entry(absolute, pattern, mode: :names)
237
+ entries
238
+ end
239
+
202
240
  # Fetches the URL when the policy permits it. Returns the response body. Raises {AccessDeniedError} when
203
241
  # the policy is `:disabled`, the URL scheme is not `https`, the host is not on the allowlist, the
204
242
  # response is non-2xx, the body exceeds {URL_MAX_BYTES}, or the request times out
@@ -352,11 +390,12 @@ module Rigor
352
390
  self.class.attribute(entry)
353
391
  end
354
392
 
355
- # ADR-45 WD1c (#629) — the listing row for {#list_directory}, deduplicated per (root, pattern) slot.
393
+ # ADR-45 WD1c (#629) — the listing row for {#list_directory} (and the `:names` row for {#glob}),
394
+ # deduplicated per (root, pattern, mode) slot.
356
395
  # Recomputed on each call rather than `||=`d, so a directory listed again after a mid-run mutation
357
396
  # carries the signature of the state the LAST reader saw.
358
- def record_glob_entry(root, pattern)
359
- entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern)
397
+ def record_glob_entry(root, pattern, mode: :stat)
398
+ entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern, mode: mode)
360
399
  @mutex.synchronize { @glob_entries[entry.slot_key] = entry }
361
400
  self.class.attribute(entry)
362
401
  end
@@ -42,8 +42,20 @@ module Rigor
42
42
  # `Nominal[Grape::Endpoint]` (the block is `instance_eval`'d on an instance of that class, e.g. a
43
43
  # route body on `Grape::Endpoint`), and `"singleton(Grape::API::Instance)"` binds
44
44
  # `Singleton[Grape::API::Instance]` (the block is `instance_eval`'d on that *class object*, e.g.
45
- # a `namespace` body on `Grape::API::Instance`). Reserved Symbol names (`:receiver_singleton`,
46
- # `:dsl_recorder`) remain unaccepted.
45
+ # a `namespace` body on `Grape::API::Instance`). `:lexical` (issue #1667) leaves `self` as the caller's:
46
+ # the method runs the block where it was written (`block.refined(M).call`), so the entry only declares
47
+ # `refinements:`. Reserved Symbol names (`:receiver_singleton`, `:dsl_recorder`) remain unaccepted.
48
+ # - `refinements` — Array of fully-qualified module-name Strings (default `[]`, issue #1667, ADR-121 WD5).
49
+ # The method runs the block under `Proc#refined` with these modules (`instance_exec(&block.refined(M))`),
50
+ # so they are appended to the block body's in-effect refinements after its lexical list, in the declared
51
+ # order (the last one wins). Nested blocks inherit them. A module that refines nothing the project or a
52
+ # gem's source inference makes visible contributes nothing.
53
+ #
54
+ # ## Matching
55
+ #
56
+ # A `:receiver_instance` or `singleton(...)` entry matches `Singleton[X]` receivers only (a class-level DSL
57
+ # call). A named instance binding (`"Foo::Bar"`) and a `:lexical` entry also match `Nominal[X]` receivers:
58
+ # a `:lexical` method is as often an instance method (`ActiveRecord::Relation#where`) as a class-level one.
47
59
  #
48
60
  # ## Ractor-shareability
49
61
  #
@@ -51,7 +63,8 @@ module Rigor
51
63
  # mutable array does not leak into the value. `Ractor.shareable?` returns true after `#initialize`.
52
64
  class BlockAsMethod
53
65
  SELF_TYPE_RECEIVER_INSTANCE = :receiver_instance
54
- VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE].freeze
66
+ SELF_TYPE_LEXICAL = :lexical
67
+ VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE, SELF_TYPE_LEXICAL].freeze
55
68
 
56
69
  CLASS_NAME_PATTERN = /[A-Z]\w*(?:::[A-Z]\w*)*/
57
70
  # `self_type: "Foo::Bar"` — the block is `instance_eval`'d on an instance of `Foo::Bar`.
@@ -60,16 +73,18 @@ module Rigor
60
73
  # object itself (Grape's `namespace`/`route_param` bodies run on `Grape::API::Instance`).
61
74
  SINGLETON_SELF_TYPE_PATTERN = /\Asingleton\((#{CLASS_NAME_PATTERN})\)\z/
62
75
 
63
- attr_reader :receiver_constraint, :method_names, :self_type
76
+ attr_reader :receiver_constraint, :method_names, :self_type, :refinements
64
77
 
65
- def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
78
+ def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE, refinements: [])
66
79
  validate_receiver_constraint!(receiver_constraint)
67
80
  validate_method_names!(method_names)
68
81
  validate_self_type!(self_type)
82
+ validate_refinements!(refinements)
69
83
 
70
84
  @receiver_constraint = receiver_constraint.dup.freeze
71
85
  @method_names = method_names.map(&:to_sym).freeze
72
86
  @self_type = self_type.is_a?(String) ? self_type.dup.freeze : self_type
87
+ @refinements = refinements.map { |name| name.dup.freeze }.freeze
73
88
  freeze
74
89
  end
75
90
 
@@ -77,7 +92,8 @@ module Rigor
77
92
  {
78
93
  "receiver_constraint" => receiver_constraint,
79
94
  "method_names" => method_names.map(&:to_s),
80
- "self_type" => self_type.to_s
95
+ "self_type" => self_type.to_s,
96
+ "refinements" => refinements
81
97
  }
82
98
  end
83
99
 
@@ -85,12 +101,18 @@ module Rigor
85
101
  other.is_a?(BlockAsMethod) &&
86
102
  receiver_constraint == other.receiver_constraint &&
87
103
  method_names == other.method_names &&
88
- self_type == other.self_type
104
+ self_type == other.self_type &&
105
+ refinements == other.refinements
89
106
  end
90
107
  alias eql? ==
91
108
 
92
109
  def hash
93
- [receiver_constraint, method_names, self_type].hash
110
+ [receiver_constraint, method_names, self_type, refinements].hash
111
+ end
112
+
113
+ # Whether the block keeps the caller's `self` (`self_type: :lexical`).
114
+ def lexical_self?
115
+ self_type == SELF_TYPE_LEXICAL
94
116
  end
95
117
 
96
118
  # The class name a String `self_type` binds `self` to — `"Foo::Bar"` for both the plain and the
@@ -118,6 +140,12 @@ module Rigor
118
140
  self_type.is_a?(String) && !singleton_binding?
119
141
  end
120
142
 
143
+ # Whether `Nominal[X]` receivers match, beside `Singleton[X]` ones: a named instance binding, or a
144
+ # `:lexical` entry.
145
+ def matches_instance_receivers?
146
+ named_instance_binding? || lexical_self?
147
+ end
148
+
121
149
  private
122
150
 
123
151
  def validate_receiver_constraint!(value)
@@ -143,6 +171,21 @@ module Rigor
143
171
  end
144
172
  end
145
173
 
174
+ def validate_refinements!(refinements)
175
+ unless refinements.is_a?(Array)
176
+ raise ArgumentError,
177
+ "Plugin::Macro::BlockAsMethod#refinements must be an Array, got #{refinements.inspect}"
178
+ end
179
+
180
+ refinements.each do |name|
181
+ next if name.is_a?(String) && name.match?(NAMED_SELF_TYPE_PATTERN)
182
+
183
+ raise ArgumentError,
184
+ "Plugin::Macro::BlockAsMethod#refinements entries must be module-name Strings ('Foo::Bar'), " \
185
+ "got #{name.inspect}"
186
+ end
187
+ end
188
+
146
189
  def validate_self_type!(self_type)
147
190
  return if VALID_SELF_TYPES.include?(self_type)
148
191
  return if self_type.is_a?(String) &&
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "environment"
4
+ require_relative "inference/project_patched_scanner"
4
5
  require_relative "plugin"
5
6
  require_relative "plugin/loader"
6
7
  require_relative "plugin/services"
@@ -53,16 +54,55 @@ module Rigor
53
54
  # ADR-93 auto-wired `rigor-rbs-inline` entry when the library is resolvable).
54
55
  # @param source_files — the file(s) the command inspects. Threaded so each loaded plugin's
55
56
  # `source_rbs_synthesizer` runs over them at env-build time; an empty list contributes no synthesized RBS.
56
- def build(configuration:, source_files:)
57
+ # @param feature_sources — path => source text the required-feature scan reads in place of that file (an
58
+ # editor buffer bound with `--tmp-file` / `--instead-of`).
59
+ # @param project_patched_methods — the `pre_eval:` registry (see {.project_patched_methods}); nil keeps
60
+ # the unpatched universe.
61
+ def build(configuration:, source_files:, feature_sources: {}, project_patched_methods: nil)
62
+ features = required_features(configuration, source_files, sources: feature_sources)
57
63
  Environment.for_project(
64
+ project_patched_methods: project_patched_methods,
58
65
  libraries: configuration.libraries,
59
66
  signature_paths: configuration.signature_paths,
60
67
  plugin_registry: load_plugin_registry(configuration),
61
68
  source_files: source_files,
69
+ required_features: features,
62
70
  **dependency_discovery_options(configuration)
63
71
  )
64
72
  rescue StandardError
65
- bare(configuration)
73
+ bare(configuration, features)
74
+ end
75
+
76
+ # The patched-method registry `rigor check` types against for the project's `pre_eval:` files. It depends
77
+ # on those files alone, so it is built by the same {Inference::ProjectPatchedScanner} the Runner's project
78
+ # pre-pass calls, over the same resolved `configuration.pre_eval` entries (glob-expanded at config load,
79
+ # missing files skipped), without the pre-pass's plugin load / `#prepare` / dependency-source work. An
80
+ # editor `buffer` is honoured, so a `--tmp-file` edit of a pre_eval file is read in flight. Nil when the
81
+ # project lists no `pre_eval:` entry, so a project without one stays byte-identical. Fail-soft.
82
+ def project_patched_methods(configuration, buffer: nil)
83
+ return nil if configuration.pre_eval.empty?
84
+
85
+ existing = configuration.pre_eval.select { |path| File.file?(path) }
86
+ Inference::ProjectPatchedScanner.scan(existing, buffer: buffer).registry
87
+ rescue StandardError
88
+ nil
89
+ end
90
+
91
+ # Issue #1700 — the {Environment::RequiredFeatures} the environment gates on: the configured paths plus
92
+ # `extra_files` (the file a probe inspects, which may lie outside them), the same set `rigor check` scans
93
+ # whichever files it is given, so a position types as `rigor check` types it.
94
+ def required_features(configuration, extra_files = [], sources: {})
95
+ Environment::RequiredFeatures.for_configuration(configuration, extra_files, sources: sources)
96
+ end
97
+
98
+ # The required-feature scan's view of an editor buffer (`--tmp-file` / `--instead-of`): its logical path
99
+ # read from its physical file. Empty without a buffer or when the file cannot be read.
100
+ def buffer_sources(buffer)
101
+ return {} if buffer.nil?
102
+
103
+ { buffer.logical_path => File.binread(buffer.physical_path) }
104
+ rescue SystemCallError
105
+ {}
66
106
  end
67
107
 
68
108
  # The dependency-discovery axes `check` reads off the configuration, as a keyword hash. Extracted so the
@@ -116,22 +156,24 @@ module Rigor
116
156
 
117
157
  # The first fail-soft floor: no plugin tier and no synthesized RBS, but still the project's own dependency
118
158
  # sources — dropping those is what issue #821 was, so a plugin-loading failure must not cost them.
119
- def bare(configuration)
159
+ def bare(configuration, features = required_features(configuration))
120
160
  Environment.for_project(
121
161
  libraries: configuration.libraries,
122
162
  signature_paths: configuration.signature_paths,
163
+ required_features: features,
123
164
  **dependency_discovery_options(configuration)
124
165
  )
125
166
  rescue StandardError
126
- minimal(configuration)
167
+ minimal(configuration, features)
127
168
  end
128
169
 
129
170
  # The last floor: RBS core plus the project's own signature paths, which cannot fail on anything outside
130
171
  # the project itself.
131
- def minimal(configuration)
172
+ def minimal(configuration, features = [])
132
173
  Environment.for_project(
133
174
  libraries: configuration.libraries,
134
- signature_paths: configuration.signature_paths
175
+ signature_paths: configuration.signature_paths,
176
+ required_features: features
135
177
  )
136
178
  end
137
179
  end
@@ -84,7 +84,8 @@ module Rigor
84
84
 
85
85
  # Human-readable reasons the caller prints. Each names something the operator can act on.
86
86
  NO_SNAPSHOT = "no reusable `rigor check --incremental` snapshot"
87
- OPAQUE_PLUGIN = "a plugin contributes types with no incremental fingerprint surface"
87
+ OPAQUE_PLUGIN = "a plugin is opaque to the incremental fingerprint " \
88
+ "(no fingerprint surface, or a value it cannot digest)"
88
89
  UNDIGESTIBLE = "the project-scan tables could not be digested"
89
90
  UNIDENTIFIED_ENGINE = "the engine's own source tree could not be digested"
90
91
 
@@ -220,7 +221,7 @@ module Rigor
220
221
  def patched_rows(patched)
221
222
  patched.by_key.map do |(class_name, method_name, kind), entry|
222
223
  [class_name.to_s, method_name.to_s, kind.to_s, entry.source_path.to_s, entry.source_line,
223
- entry.return_type&.describe(:short).to_s]
224
+ entry.return_type&.describe(:short).to_s, entry.alias_of.to_s]
224
225
  end.sort
225
226
  end
226
227
 
@@ -366,7 +366,7 @@ module Rigor
366
366
  end
367
367
 
368
368
  def snippet(loc)
369
- text = loc.slice.gsub(/\s+/, " ")
369
+ text = loc.slice.scrub.gsub(/\s+/, " ")
370
370
  text.length > 30 ? "#{text[0, 27]}..." : text
371
371
  end
372
372
  end
@@ -27,11 +27,13 @@ module Rigor
27
27
  # ancestors are `[C, A, M, Base]`. It reads the chain's `:constants` flavor, whose predicate for a
28
28
  # project class also admits a module that holds only constants.
29
29
  #
30
- # Only PROJECT ancestors appear: an as-written name that resolves to no discovered class or module
31
- # is an external entry on the chain and contributes nothing here, so a `class Foo <
32
- # ActiveRecord::Base` adds no candidate and a constant owned by an RBS-known ancestor still resolves
33
- # only if the bare name reaches it at step 3. That gap is deliberate for this slice: widening to the
34
- # RBS ancestor graph is a separate question with its own FP surface.
30
+ # Project ancestors appear, and (issue #1698) a module the class body `include`s or `prepend`s that
31
+ # only RBS or the class registry declares: `include AcLibraryRb` puts `AcLibraryRb` at its Ruby
32
+ # position, so `Segtree` inside the class reads `AcLibraryRb::Segtree` as Ruby does. Such an entry
33
+ # contributes its OWN constants only; the RBS ancestry behind it is not expanded, as for every external
34
+ # entry. An external SUPERCLASS (`class Foo < ActiveRecord::Base`) still contributes nothing, and a
35
+ # constant it owns resolves only if the bare name reaches it at step 3: widening to the superclass's
36
+ # RBS graph is a separate question with its own FP surface. See {.external_constant_owner}.
35
37
  #
36
38
  # Memoised per run because step 2 runs on every constant reference whose lexical candidates all
37
39
  # miss — which is the common case for a core-class reference (`String` inside `class Foo`). The
@@ -52,7 +54,7 @@ module Rigor
52
54
  scopes, chain = ancestor_constant_worlds(class_name, scope)
53
55
  owner, hit = first_ancestor_hit(scopes, &)
54
56
  verdict = chain.settle(scope, owner, owner: owner) do |retro|
55
- first_ancestor_hit(constant_scopes_of(retro, class_name), &)&.first
57
+ first_ancestor_hit(constant_scopes_of(retro, class_name, scope), &)&.first
56
58
  end
57
59
  return hit if verdict == :chain
58
60
 
@@ -109,7 +111,9 @@ module Rigor
109
111
  worlds = bucket[class_name]
110
112
  return bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) if worlds.nil?
111
113
 
112
- Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants).record(scope)
114
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
115
+ chain.record(scope)
116
+ record_external_owner_edges(chain)
113
117
  worlds
114
118
  end
115
119
  private_class_method :recorded_ancestor_constant_scopes
@@ -118,22 +122,134 @@ module Rigor
118
122
  def compute_ancestor_constant_scopes(class_name, scope)
119
123
  chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
120
124
  chain.record(scope)
121
- scopes = constant_scopes_of(chain, class_name)
125
+ record_external_owner_edges(chain)
126
+ scopes = constant_scopes_of(chain, class_name, scope)
122
127
  [scopes, chain].freeze
123
128
  end
124
129
  private_class_method :compute_ancestor_constant_scopes
125
130
 
126
- # The chain's project entries in order, each once, without `class_name` itself.
127
- def constant_scopes_of(chain, class_name)
131
+ # The chain's project entries in order, each once, without `class_name` itself, with each mixin entry
132
+ # the project does not declare standing as the module {.external_constant_owner} names.
133
+ def constant_scopes_of(chain, class_name, scope)
128
134
  out = []
129
135
  chain.entries.each do |entry|
130
- name = entry.name
136
+ name = entry.external? ? external_constant_owner(entry, scope) : entry.name
131
137
  out << name unless name.nil? || name == class_name || out.include?(name)
132
138
  end
133
139
  out.freeze
134
140
  end
135
141
  private_class_method :constant_scopes_of
136
142
 
143
+ # Issue #1698 — the module an external `include` / `prepend` entry names, when the constant ladder may
144
+ # read its constants, or nil. Ruby resolved the spelling at the `include` lexically, so the first of the
145
+ # entry's candidates (most qualified first) that exists is the module. The answer is the first candidate
146
+ # RBS or the class registry knows, and nil when a nearer candidate is a constant the project binds
147
+ # ({.project_written_candidate?}): Ruby's module is then that constant, whose constants nothing here
148
+ # describes. The candidates are the entry's own, behind the namespace of the body that wrote the edge
149
+ # (`entry.owner`), which Ruby searches first and the chain's resolver does not consult. An ambiguous
150
+ # spelling has no candidates and answers nil. A superclass edge answers nil (see
151
+ # {.ancestor_constant_scopes}).
152
+ def external_constant_owner(entry, scope)
153
+ return nil if entry.superclass_edge || entry.candidates.empty?
154
+
155
+ env = scope.environment
156
+ external_owner_candidates(entry).each do |candidate|
157
+ return nil if project_written_candidate?(candidate, scope)
158
+ return candidate if env.class_known?(candidate)
159
+ end
160
+ nil
161
+ end
162
+ private_class_method :external_constant_owner
163
+
164
+ def external_owner_candidates(entry)
165
+ return entry.candidates if entry.owner.nil? || entry.raw.start_with?("::")
166
+
167
+ ["#{entry.owner}::#{entry.raw}", *entry.candidates]
168
+ end
169
+ private_class_method :external_owner_candidates
170
+
171
+ # ADR-46 — the name edges of {.external_constant_owner}'s guard, for every mixin entry it read: a file that
172
+ # starts writing or declaring a nearer spelling of the module's name (`User::AcLibraryRb = …` in a file the
173
+ # reader has no other edge to) moves the answer. Keyed on the spelling's last segment, as the ladder's own
174
+ # `constant:` and `class:` edges are. A memo hit replays them beside the chain's own edges.
175
+ def record_external_owner_edges(chain)
176
+ return unless Analysis::DependencyRecorder.active?
177
+
178
+ chain.entries.each do |entry|
179
+ next unless entry.external? && !entry.superclass_edge
180
+
181
+ Analysis::DependencyRecorder.read_last_segment(:constant, entry.raw)
182
+ Analysis::DependencyRecorder.read_last_segment(:class, entry.raw)
183
+ end
184
+ end
185
+ private_class_method :record_external_owner_edges
186
+
187
+ # Issue #1698 — whether the walk must stop at `owner` because a module its RBS ancestry holds may own the name
188
+ # (`candidate` answers for it). Ruby searches that ancestry right after `owner`, before any later entry of
189
+ # the chain and before the top level, and the chain does not expand it. Where no module of it owns the
190
+ # name, where they sit does not matter to this name and the walk goes on.
191
+ #
192
+ # `owner` is an RBS-only mixin (`external`), or a project entry RBS also declares: a project file that
193
+ # reopens an RBS module without restating its `include`s makes it a project entry whose chain lists only
194
+ # the edges the project writes, so its ancestry is read less what that chain lists. The ancestry comes from
195
+ # the declarations (`RbsLoader#declared_ancestry`), so a definition that fails to build still yields what
196
+ # it declares. Where it cannot be read whole, {.ancestry_unknown?} keeps the later RBS-only mixins out
197
+ # instead of stopping here.
198
+ def mixin_ancestry_stop?(owner, scope, external)
199
+ loader = rbs_loader_for(scope, nil)
200
+ return false if loader.nil? || (!external && !loader.class_known?(owner))
201
+
202
+ names, = loader.declared_ancestry(owner)
203
+ listed = nil
204
+ names.any? do |ancestor|
205
+ next false if IMPLICIT_ANCESTORS.include?(ancestor)
206
+
207
+ listed ||= external ? EMPTY_NAMES : project_chain_names(owner, scope)
208
+ !listed.include?(ancestor) && yield(ancestor)
209
+ end
210
+ end
211
+ private_class_method :mixin_ancestry_stop?
212
+
213
+ # Issue #1698 — whether an entry ahead of `entry` on `class_name`'s list has an RBS ancestry that cannot be
214
+ # read whole ({.mixin_ancestry_stop?}): a module it hides may own the name, so `entry`, an RBS-only mixin,
215
+ # MUST NOT answer. Only those entries are kept out; project entries, the superclass chain and the top level
216
+ # answer as they did before #1698.
217
+ def ancestry_unknown_before?(class_name, entry, scope)
218
+ loader = rbs_loader_for(scope, nil)
219
+ return true if loader.nil?
220
+
221
+ ancestor_constant_scopes(class_name, scope).each do |earlier|
222
+ break if earlier == entry
223
+ next unless loader.class_known?(earlier)
224
+
225
+ return true unless loader.declared_ancestry(earlier)[1]
226
+ end
227
+ false
228
+ end
229
+ private_class_method :ancestry_unknown_before?
230
+
231
+ # Ruby places these after every entry of a chain, so a constant they own is the top level's (step 4).
232
+ IMPLICIT_ANCESTORS = %w[Object Kernel BasicObject].freeze
233
+ private_constant :IMPLICIT_ANCESTORS
234
+
235
+ EMPTY_NAMES = [].freeze
236
+ private_constant :EMPTY_NAMES
237
+
238
+ # Every name `owner`'s own instance chain lists: its project entries and each external entry's candidates.
239
+ def project_chain_names(owner, scope)
240
+ Scope::ResolutionChain.for(scope, owner, :instance, :constants).entries.flat_map do |entry|
241
+ entry.external? ? entry.candidates : [entry.name]
242
+ end
243
+ end
244
+ private_class_method :project_chain_names
245
+
246
+ # Whether the project binds `candidate` itself: a namespace under the `:constants` flavor's tables, or a
247
+ # constant write the census records (#1290), typed or not.
248
+ def project_written_candidate?(candidate, scope)
249
+ known_project_namespace?(candidate, scope) || scope.shadowing_constant_names(candidate).include?(candidate)
250
+ end
251
+ private_class_method :project_written_candidate?
252
+
137
253
  # Whether `name` is a namespace the project declares — the predicate the chain's `:constants` flavor
138
254
  # resolves ancestor names with, and the path walk's (`reflection/constant_path.rb`) test for a head.
139
255
  def known_project_namespace?(name, scope)
@@ -118,11 +118,15 @@ module Rigor
118
118
 
119
119
  # {.agreed_ancestor_hit} over the budget-bounded scope lists: the first answer where
120
120
  # `Scope::ResolutionChain#settle` lets the chain stand, and otherwise the breadth-first order's.
121
+ #
122
+ # Issue #1698 — the path walk reads the project entries only: an RBS-only mixin's own ancestry is not on the
123
+ # chain, so the rung that reads such a mixin also stops behind it ({.mixin_ancestry_stop?}), and this
124
+ # walk keeps the answer it gave before.
121
125
  def bounded_agreed_hit(class_name, scope, &)
122
126
  scopes, chain = ancestor_constant_worlds(class_name, scope)
123
- owner, hit = first_ancestor_hit(bounded(scopes), &)
127
+ owner, hit = first_ancestor_hit(bounded(project_scopes(scopes, scope)), &)
124
128
  verdict = chain.settle(scope, owner, owner: owner) do |retro|
125
- first_ancestor_hit(bounded(constant_scopes_of(retro, class_name)), &)&.first
129
+ first_ancestor_hit(bounded(project_scopes(constant_scopes_of(retro, class_name, scope), scope)), &)&.first
126
130
  end
127
131
  return hit if verdict == :chain
128
132
 
@@ -130,6 +134,13 @@ module Rigor
130
134
  end
131
135
  private_class_method :bounded_agreed_hit
132
136
 
137
+ def project_scopes(scopes, scope)
138
+ return scopes if scopes.all? { |name| known_project_namespace?(name, scope) }
139
+
140
+ scopes.select { |name| known_project_namespace?(name, scope) }
141
+ end
142
+ private_class_method :project_scopes
143
+
133
144
  # How many of {.ancestor_constant_scopes}' entries one segment may consult, under
134
145
  # `Scope::ANCESTOR_WALK_LIMIT` — the budget `Scope`'s method lookup already spends on this same
135
146
  # graph, reused rather than re-chosen so one pathological ancestry costs one bound. The list
@@ -50,6 +50,11 @@ module Rigor
50
50
  WRITTEN_CANDIDATE = Object.new.freeze
51
51
  private_constant :WRITTEN_CANDIDATE
52
52
 
53
+ # Issue #1698 — what the constant ladder's ancestor rung answers where an entry's RBS ancestry may
54
+ # own the name ({.mixin_ancestry_stop?}). Never a type: {.lexical_constant_type} replaces it.
55
+ MIXIN_ANCESTRY_STOP = Object.new.freeze
56
+ private_constant :MIXIN_ANCESTRY_STOP
57
+
53
58
  module_function
54
59
 
55
60
  def class_known?(class_name, scope: Scope.empty)
@@ -197,10 +202,19 @@ module Rigor
197
202
  # `P::A < P::Base` is `P::Base::B`, and step 3's `A::B` is a real but different class.
198
203
  #
199
204
  # Step 3 — the bare name (top level). It takes no `shadows`: nothing sits below it for a stop to replace.
200
- first_constant_hit(lexical_nesting_chain(scope), name, scope, shadows) ||
201
- ancestor_constant_type(name, scope, enclosing_class_path(scope), shadows) ||
202
- constant_path_type(name, scope, shadows: shadows) ||
203
- constant_type_at(name, scope)
205
+ #
206
+ # Issue #1698 — step 2 answers {MIXIN_ANCESTRY_STOP} where an RBS-only mixin's own ancestry may own the name
207
+ # ({.mixin_ancestry_stop?}). Ruby reads that ancestry before any later ancestor or the top level, and
208
+ # the chain does not list it, so a lower rung's answer is replaced by `Dynamic[top]`, as #1290's stop is.
209
+ lexical = first_constant_hit(lexical_nesting_chain(scope), name, scope, shadows)
210
+ return lexical if lexical
211
+
212
+ ancestor = ancestor_constant_type(name, scope, enclosing_class_path(scope), shadows)
213
+ stopped = ancestor.equal?(MIXIN_ANCESTRY_STOP)
214
+ return ancestor if ancestor && !stopped
215
+
216
+ below = constant_path_type(name, scope, shadows: shadows) || constant_type_at(name, scope)
217
+ stopped && below ? Type::Combinator.untyped : below
204
218
  end
205
219
  private_class_method :lexical_constant_type
206
220
 
@@ -216,11 +230,28 @@ module Rigor
216
230
  end
217
231
  private_class_method :first_constant_hit
218
232
 
219
- # Step 2's rung on its own: nil when there is no enclosing class path to take ancestors of.
233
+ # Step 2's rung on its own: nil when there is no enclosing class path to take ancestors of, and
234
+ # {MIXIN_ANCESTRY_STOP} where an RBS-only mixin's ancestry may own the name.
235
+ #
236
+ # Issue #1698 — an entry that stands for an RBS-only mixin ({.external_constant_owner}) is skipped where `self`
237
+ # is a class object: a `class << self` body and a `def` in it run under the singleton class's cref, whose
238
+ # ancestors are not the class's ([#1305](https://github.com/rigortype/rigor/issues/1305)), and the scope does
239
+ # not tell such a body from a `def self.m` or the class body. Skipping keeps the answer this rung gave before
240
+ # #1698 there.
220
241
  def ancestor_constant_type(name, scope, prefix, shadows)
221
242
  return nil if prefix.nil? || prefix.empty?
222
243
 
223
- agreed_ancestor_hit(prefix, scope) { |entry| constant_type_at("#{entry}::#{name}", scope, shadows) }
244
+ class_object_self = scope.self_type.is_a?(Type::Singleton)
245
+ agreed_ancestor_hit(prefix, scope) do |entry|
246
+ external = !known_project_namespace?(entry, scope)
247
+ next nil if external && (class_object_self || ancestry_unknown_before?(prefix, entry, scope))
248
+
249
+ candidate = ->(owner) { constant_type_at("#{owner}::#{name}", scope, shadows) }
250
+ hit = candidate.call(entry)
251
+ next hit if hit
252
+
253
+ MIXIN_ANCESTRY_STOP if mixin_ancestry_stop?(entry, scope, external, &candidate)
254
+ end
224
255
  end
225
256
  private_class_method :ancestor_constant_type
226
257
 
@@ -264,8 +295,9 @@ module Rigor
264
295
  prefix = enclosing_class_path(scope)
265
296
  return nil if prefix.nil? || prefix.empty?
266
297
 
267
- first_constant_hit(peeled_nesting(prefix), name, scope, shadows) ||
268
- ancestor_constant_type(name, scope, prefix, shadows)
298
+ hit = first_constant_hit(peeled_nesting(prefix), name, scope, shadows) ||
299
+ ancestor_constant_type(name, scope, prefix, shadows)
300
+ hit.equal?(MIXIN_ANCESTRY_STOP) ? nil : hit
269
301
  end
270
302
  private_class_method :caller_derived_constant_type
271
303