rigortype 0.4.1 → 0.4.2

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 (182) 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 +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -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) &&
@@ -53,16 +53,37 @@ module Rigor
53
53
  # ADR-93 auto-wired `rigor-rbs-inline` entry when the library is resolvable).
54
54
  # @param source_files — the file(s) the command inspects. Threaded so each loaded plugin's
55
55
  # `source_rbs_synthesizer` runs over them at env-build time; an empty list contributes no synthesized RBS.
56
- def build(configuration:, source_files:)
56
+ # @param feature_sources — path => source text the required-feature scan reads in place of that file (an
57
+ # editor buffer bound with `--tmp-file` / `--instead-of`).
58
+ def build(configuration:, source_files:, feature_sources: {})
59
+ features = required_features(configuration, source_files, sources: feature_sources)
57
60
  Environment.for_project(
58
61
  libraries: configuration.libraries,
59
62
  signature_paths: configuration.signature_paths,
60
63
  plugin_registry: load_plugin_registry(configuration),
61
64
  source_files: source_files,
65
+ required_features: features,
62
66
  **dependency_discovery_options(configuration)
63
67
  )
64
68
  rescue StandardError
65
- bare(configuration)
69
+ bare(configuration, features)
70
+ end
71
+
72
+ # Issue #1700 — the {Environment::RequiredFeatures} the environment gates on: the configured paths plus
73
+ # `extra_files` (the file a probe inspects, which may lie outside them), the same set `rigor check` scans
74
+ # whichever files it is given, so a position types as `rigor check` types it.
75
+ def required_features(configuration, extra_files = [], sources: {})
76
+ Environment::RequiredFeatures.for_configuration(configuration, extra_files, sources: sources)
77
+ end
78
+
79
+ # The required-feature scan's view of an editor buffer (`--tmp-file` / `--instead-of`): its logical path
80
+ # read from its physical file. Empty without a buffer or when the file cannot be read.
81
+ def buffer_sources(buffer)
82
+ return {} if buffer.nil?
83
+
84
+ { buffer.logical_path => File.binread(buffer.physical_path) }
85
+ rescue SystemCallError
86
+ {}
66
87
  end
67
88
 
68
89
  # The dependency-discovery axes `check` reads off the configuration, as a keyword hash. Extracted so the
@@ -116,22 +137,24 @@ module Rigor
116
137
 
117
138
  # The first fail-soft floor: no plugin tier and no synthesized RBS, but still the project's own dependency
118
139
  # sources — dropping those is what issue #821 was, so a plugin-loading failure must not cost them.
119
- def bare(configuration)
140
+ def bare(configuration, features = required_features(configuration))
120
141
  Environment.for_project(
121
142
  libraries: configuration.libraries,
122
143
  signature_paths: configuration.signature_paths,
144
+ required_features: features,
123
145
  **dependency_discovery_options(configuration)
124
146
  )
125
147
  rescue StandardError
126
- minimal(configuration)
148
+ minimal(configuration, features)
127
149
  end
128
150
 
129
151
  # The last floor: RBS core plus the project's own signature paths, which cannot fail on anything outside
130
152
  # the project itself.
131
- def minimal(configuration)
153
+ def minimal(configuration, features = [])
132
154
  Environment.for_project(
133
155
  libraries: configuration.libraries,
134
- signature_paths: configuration.signature_paths
156
+ signature_paths: configuration.signature_paths,
157
+ required_features: features
135
158
  )
136
159
  end
137
160
  end
@@ -220,7 +220,7 @@ module Rigor
220
220
  def patched_rows(patched)
221
221
  patched.by_key.map do |(class_name, method_name, kind), entry|
222
222
  [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]
223
+ entry.return_type&.describe(:short).to_s, entry.alias_of.to_s]
224
224
  end.sort
225
225
  end
226
226
 
@@ -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
 
@@ -51,6 +51,7 @@ module Rigor
51
51
  :clears_last_status,
52
52
  :defines_case_equality,
53
53
  :implicit_self_evidence,
54
+ :in_effect_refinements,
54
55
  :possible_discovered_methods,
55
56
  :possible_discovered_deferred_ranges,
56
57
  :contested_discovered_def_nodes,
@@ -155,6 +156,7 @@ module Rigor
155
156
  patched_line_readers: "the gets / readline names the file defines through the define_method family",
156
157
  clears_last_status: "whether the analysed file holds a call that may set $? to nil",
157
158
  defines_case_equality: "whether the analysed file holds a define_method naming ===",
159
+ in_effect_refinements: "the file's ordered in-effect refinements; built lazily from its tree, never seeded",
158
160
  implicit_self_evidence: "where the file's implicit-self readers sit; built lazily from its tree, never seeded"
159
161
  }.freeze,
160
162
  run_state: {
@@ -217,12 +219,36 @@ module Rigor
217
219
  # - {ENVELOPE_REFINED_MARK}: a `refine` block refines the class, which redefines its methods wherever a `using`
218
220
  # is in effect. Issue #1367 splits it from {ENVELOPE_DYNAMIC_MARK}, which it used to share: an implicit
219
221
  # conversion (`to_str`, `to_int`) ignores a refinement, so the `global.*` rules must tell the two apart.
220
- # Every reader that asks whether a surface is rewritten reads both ({.rewritten_surface?}).
222
+ # The readers that ask whether a surface is rewritten read both ({.rewritten_surface?}), except
223
+ # `call.wrong-arity`'s source envelope (`CheckRules::SourceArity`), which leaves a refinement to the per-call
224
+ # decline that knows the name and the `using` (issue #1797).
225
+ # Issue #1820 — the kind of a `[ENVELOPE_REMOVED_KIND, method_name]` bucket entry: the project removes the name
226
+ # from the class with a literal `undef`, `remove_method` or `undef_method`, whichever side. A removal is not
227
+ # otherwise distinguishable from an `attr_*` or `define_method`, whose envelope is opaque too, and the
228
+ # refinement own-definer proof must not take a removed method as the one Ruby finds.
229
+ ENVELOPE_REMOVED_KIND = :removed
230
+ # Issue #1820 — a `K = Class.new(Parent)` constant write: the class's superclass is not in the superclass table
231
+ # (the meta-new walk does not route `Parent` through ancestor discovery), so a reader that must place `K` in a
232
+ # hierarchy reads its superclass as unknown, as it reads the `nil` the table records for `class K < <expr>`.
233
+ ENVELOPE_META_SUPERCLASS_MARK = :"<meta-superclass>"
221
234
  ENVELOPE_MODULE_MARK = :"<module>"
222
235
  ENVELOPE_DYNAMIC_MARK = :"<dynamic>"
223
236
  ENVELOPE_OBJECT_EXTENDED_MARK = :"<object-extended>"
224
237
  ENVELOPE_REFINED_MARK = :"<refined>"
225
238
 
239
+ # Issue #1820 — whether an envelope bucket says the project declares, or rewrites, the name's method table: a
240
+ # per-method entry, the {ENVELOPE_MODULE_MARK} a `module` declaration records, or the {ENVELOPE_DYNAMIC_MARK}
241
+ # (`Comparable.module_eval { define_method(computed) }` gives a module a method no table names, so a reader
242
+ # must keep it on its chain rather than read RBS's surface for it). A bucket that holds only the refined,
243
+ # object-extended or meta-superclass marks is evidence about a name the project may only mention, such as
244
+ # `Comparable` in `x.singleton_class.prepend(Comparable)`.
245
+ DECLARING_MARKS = [ENVELOPE_MODULE_MARK, ENVELOPE_DYNAMIC_MARK].freeze
246
+ private_constant :DECLARING_MARKS
247
+
248
+ def self.declares_surface?(bucket)
249
+ !bucket.nil? && bucket.any? { |key, _| !key.is_a?(Symbol) || DECLARING_MARKS.include?(key) }
250
+ end
251
+
226
252
  # Whether an envelope bucket says the project rewrites the class's method table beyond its literal names: a
227
253
  # dynamic rewrite, or a refinement.
228
254
  def self.rewritten_surface?(bucket)
@@ -236,6 +262,14 @@ module Rigor
236
262
  # present. Not a constant character, so no class can be named it.
237
263
  ENVELOPE_PROJECT_WIDE = "<project-wide>"
238
264
 
265
+ # ADR-121 WD7 (issues #1796, #1799) — the wildcard of `discovered_refinements`, standing for what the walk could
266
+ # not read. As a refined-class key it is a class the walk cannot name (`refine(k)`, `refine(self)`); as a method
267
+ # key, names a refine body defines that the walk cannot enumerate (`import_methods`, a computed
268
+ # `define_method`); as a refining module, a module the walk cannot name (a `refine` in an instance method,
269
+ # which any module extending it may run). `{"*" => {"*" => [M]}}` makes `M` opaque: some `refine` of `M` is
270
+ # unreadable. No constant can be named `*`, and method keys are Symbols, so neither axis collides.
271
+ REFINEMENT_WILDCARD = "*"
272
+
239
273
  # The shared all-empty index `Scope.empty` (and every scope that never sees a seeding pass) points at — one
240
274
  # allocation per process.
241
275
  EMPTY = new(
@@ -417,6 +451,11 @@ module Rigor
417
451
  # them by. Filled by `Inference::ScopeIndexer.index` from the file's own tree only; nil, where no file was
418
452
  # indexed, declines every such reader.
419
453
  implicit_self_evidence: nil,
454
+ # Issue #1673 (ADR-121 WD1) — the file's in-effect refinements (`Inference::InEffectRefinements`, walked on the
455
+ # first ask), which `Scope#in_effect_refinements` reads for the typer and the check rules read off the root's
456
+ # scope. Filled by `Inference::ScopeIndexer.index` from the file's own tree only; nil, where no file was
457
+ # indexed, answers no lexical refinement.
458
+ in_effect_refinements: nil,
420
459
  # ADR-119 WD1 — the siblings of the members {SIBLINGS} pairs, empty until a producer admits a possible fact.
421
460
  possible_discovered_methods: EMPTY_TABLE,
422
461
  possible_discovered_deferred_ranges: EMPTY_TABLE,