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
@@ -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,
@@ -83,10 +83,12 @@ module Rigor
83
83
  # One ancestor on a chain. A project entry names a class or module (`name`) on `side` `:instance` (its
84
84
  # instance methods) or `:singleton` (the class object's singleton). An external entry has no `name`: it
85
85
  # keeps the spelling (`raw`) and its candidate names (`candidates`, most qualified first, empty for an
86
- # ambiguous spelling), and `superclass_edge` says it was reached through a superclass edge.
86
+ # ambiguous spelling), and `superclass_edge` says it was reached through a superclass edge. `owner` names
87
+ # the class or module whose edge an external entry is (#1698: Ruby resolves an `include`'s argument with
88
+ # that body as the innermost cref, a namespace `candidates` leaves out); it is nil on a project entry.
87
89
  # `last_segment` is the unqualified name a project entry files its negative class edge under, computed once
88
90
  # when the entry is interned.
89
- Entry = Data.define(:name, :side, :raw, :candidates, :superclass_edge, :last_segment) do
91
+ Entry = Data.define(:name, :side, :raw, :candidates, :superclass_edge, :last_segment, :owner) do
90
92
  def external? = name.nil?
91
93
  end
92
94
 
@@ -98,7 +100,9 @@ module Rigor
98
100
  # module whose mixin edges' order is not a fact, `kind` the side read (`:include` or `:extend`), `listed`
99
101
  # the entries `DiscoveryIndex#unpositioned_mixins` lists for it (`"*"` included), and `multi_file` whether
100
102
  # the multi-file rule fired (two or more files declare it and it has two or more edges on that side).
101
- # Interned per `[node, kind]` in the flavor's bucket, so equal marks are one object.
103
+ # Interned per `[node, kind]` in the flavor's bucket, so equal marks are one object. Kind `:extend_hook`
104
+ # (#1730, listing `"*"`) marks a node a module it extends may mix into through a hook; it is not an order mark
105
+ # ({#order_marks}).
102
106
  Mark = Data.define(:node, :kind, :listed, :multi_file)
103
107
 
104
108
  NO_MARKS = [].freeze
@@ -135,7 +139,20 @@ module Rigor
135
139
 
136
140
  # True when a class on the chain has mixin edges whose order the tables cannot vouch for
137
141
  # (`DiscoveryIndex#unpositioned_mixins`, or a class declared in several files with several edges).
138
- def unsettled? = !@marks.empty?
142
+ def unsettled? = !order_marks.empty?
143
+
144
+ # The marks that make the chain's ORDER doubtful. An `:extend_hook` mark (#1730) says only that an extended
145
+ # module's hook may have mixed something in that the chain does not list: it makes a candidate-set read
146
+ # `:unknown` and {#wildcard_mixin?} true, and leaves every other reader on the chain's own order. Sending
147
+ # those readers to master's order would answer a constant lookup breadth-first, which is wrong for the
148
+ # #1567 shape on every class that extends such a module.
149
+ def order_marks
150
+ @memo[:order_marks] ||= @marks.reject { |mark| mark.kind == :extend_hook }.freeze
151
+ end
152
+
153
+ # True when a class on the chain records a mixin the tables cannot name (`"*"`: an `include` of a
154
+ # non-constant, or a call the walk cannot record), so the chain may hold a module it does not list.
155
+ def wildcard_mixin? = @marks.any? { |mark| mark.listed.include?(Relevance::WILDCARD) }
139
156
 
140
157
  # The ONE decision every first-definer reader makes: does `answer`, read off this chain, stand, or does
141
158
  # the reader answer what the walk this chain replaced answered ({MasterOrder})? Returns `:chain` or
@@ -163,7 +180,7 @@ module Rigor
163
180
  # goes; `search` files only the entries ahead of an answer. `owner` names the entry that answered.
164
181
  record_beyond(scope, owner, unknown_for.nil?)
165
182
  return settle_unknown(scope, answer, unknown_for, &) unless unknown_for.nil?
166
- return :master unless @marks.empty?
183
+ return :master unless order_marks.empty?
167
184
 
168
185
  case @forks
169
186
  when 0 then :chain
@@ -208,7 +225,10 @@ module Rigor
208
225
  def search(scope, start = 0, stop = nil, side: nil)
209
226
  recording = Analysis::DependencyRecorder.active?
210
227
  stop ||= @entries.size
211
- record_head(scope, start, side) if recording
228
+ if recording
229
+ record_head(scope, start, side)
230
+ record_extend_hooks(scope)
231
+ end
212
232
  index = start
213
233
  while index < stop
214
234
  entry = @entries[index]
@@ -228,6 +248,7 @@ module Rigor
228
248
 
229
249
  ResolutionChain.record_class(scope, @root)
230
250
  (stop || @entries.size).times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
251
+ record_extend_hooks(scope)
231
252
  end
232
253
 
233
254
  # What a {#settle} verdict adds to what the search already filed. The search files the root and every entry
@@ -241,7 +262,8 @@ module Rigor
241
262
  # it `:unknown`, so the rest of the chain is filed as for any other answer;
242
263
  # - otherwise every entry after the answer (the whole chain when `owner` names none) files its class edge
243
264
  # and, for a project entry, the negative class edge on its unqualified name, so a NEW file declaring or
244
- # reopening it re-checks the reader. An external entry files only the sites of the names it can denote.
265
+ # reopening it re-checks the reader. An external entry files only the sites of the names it can denote,
266
+ # and every external on the chain files its hook edges ({#record_hooks}, #1641).
245
267
  #
246
268
  # What is filed is a function of this chain, the entry the filing starts from and the discovery index the
247
269
  # sites are read from, so a consumer that settles the same chain from the same entry again (the common case:
@@ -253,6 +275,8 @@ module Rigor
253
275
 
254
276
  start = owner.nil? ? nil : @entries.index { |entry| entry.name == owner }
255
277
  Analysis::DependencyRecorder.file_chain_once(self, start, scope.discovery) { file_beyond(scope, start) }
278
+ record_hooks(scope)
279
+ record_extend_hooks(scope)
256
280
  end
257
281
  private :record_beyond
258
282
 
@@ -268,6 +292,113 @@ module Rigor
268
292
  end
269
293
  private :file_beyond
270
294
 
295
+ # #1641 — the hook edges of every EXTERNAL entry on the chain, ahead of the answer or past it. A project file
296
+ # that gives an external module a hook (`module Comparable; def self.included(base) = base.prepend(P); end`)
297
+ # can put a definer ahead of the answer wherever the module sits, and a candidate-set read declines on it
298
+ # (the hook's mixin lists `"*"` on the module). The entry's class edges are the sites of the names it can
299
+ # denote, which a NEW file is not among, and a negative class edge on its name would re-check every reader of
300
+ # `Kernel` or `Enumerable` on any reopening. So each candidate name files the negative method edge of every
301
+ # {Relevance::HOOKS} name on its singleton side (`method:Comparable.included`, the key a new `def
302
+ # self.included` satisfies); a file adding any other method to the module re-checks nothing, and once the
303
+ # module is declared its sites carry later edits and the removal. The edges are a function of the chain alone,
304
+ # so a consumer files them once per chain ({Analysis::DependencyRecorder.file_chain_once}, #1590). A hook
305
+ # written another way — an instance `def included` in a module extended onto the external, a
306
+ # `define_singleton_method(:included)` — satisfies no such key and is not seen.
307
+ def record_hooks(scope)
308
+ externals = (@memo[:externals] ||= @entries.select(&:external?).freeze)
309
+ return if externals.empty?
310
+
311
+ Analysis::DependencyRecorder.file_chain_once(self, :hooks, scope.discovery) do
312
+ externals.each do |entry|
313
+ entry.candidates.each { |name| Analysis::DependencyRecorder.read_keys(ResolutionChain.hook_keys(name)) }
314
+ end
315
+ end
316
+ end
317
+ private :record_hooks
318
+
319
+ # #1730 — the edges of the {Builder#extend_hook_taint?} decision: an instance chain's project entries may carry
320
+ # a `"*"` mark because a module one of them `extend`s has a hook that mixes into the extender, and the
321
+ # module is often declared in a file the chain records nothing of. Each such module the decision read files
322
+ # its class edges (an edit to its hook moves its declaration signature) and the negative method edges of
323
+ # the hooks the decision reads ({ResolutionChain.extend_hook_keys}), which a NEW file reopening the module
324
+ # to add a hook satisfies; a spelling no project module answers files the singleton hook keys of each name
325
+ # it can denote, as {#record_hooks} does. Filed once per consumer and chain.
326
+ def record_extend_hooks(scope)
327
+ return unless @side == :instance
328
+
329
+ targets = (@memo[:extend_targets] ||= ResolutionChain.extend_targets(scope, @flavor, @entries))
330
+ return if targets.empty?
331
+
332
+ Analysis::DependencyRecorder.file_chain_once(self, :extend_hooks, scope.discovery) do
333
+ targets.each do |raw, resolved, candidates|
334
+ Analysis::DependencyRecorder.read_last_segment(:class, raw)
335
+ if resolved
336
+ ResolutionChain.record_class(scope, resolved)
337
+ Analysis::DependencyRecorder.read_keys(ResolutionChain.extend_hook_keys(resolved))
338
+ else
339
+ candidates.each { |name| Analysis::DependencyRecorder.read_keys(ResolutionChain.hook_keys(name)) }
340
+ end
341
+ end
342
+ end
343
+ end
344
+ private :record_extend_hooks
345
+
346
+ # The hooks that let a module `extend`ed onto N reshape N's INSTANCE side (#1730): its singleton `extended` /
347
+ # `extend_object` run against N at the `extend` and may mix into N; its instance `included` /
348
+ # `append_features` / `prepended` / `prepend_features` / `inherited` become N's own singleton hooks and run
349
+ # against N's includers, prependers and subclasses, which the mark on N reaches through their chains.
350
+ SINGLETON_EXTEND_HOOKS = %i[extended extend_object].freeze
351
+ INSTANCE_EXTEND_HOOKS = %i[included append_features prepended prepend_features inherited].freeze
352
+
353
+ @extend_hook_keys = {}
354
+
355
+ # The frozen negative method keys of `name`'s {SINGLETON_EXTEND_HOOKS} and {INSTANCE_EXTEND_HOOKS}.
356
+ def self.extend_hook_keys(name)
357
+ @extend_hook_keys[name] ||= (
358
+ SINGLETON_EXTEND_HOOKS.map { |hook| "method:#{name}.#{hook}".freeze } +
359
+ INSTANCE_EXTEND_HOOKS.map { |hook| "method:#{name}##{hook}".freeze }
360
+ ).freeze
361
+ end
362
+
363
+ # Whether extending `name` may mix modules into the extender's instance side, or into its includers' (#1730):
364
+ # `name` records a mixin it cannot name on its own instance side (`"*"`, which a hook's `base.include X` or
365
+ # `base.class_eval { … }` lists on the hook's module) and defines one of the hooks that would run it there.
366
+ def self.extend_reshapes_instance?(scope, name)
367
+ listed = scope.discovery.unpositioned_mixins[name]&.dig(:include)
368
+ return false unless listed&.include?(Relevance::WILDCARD)
369
+
370
+ defines_any?(scope.discovered_singleton_def_nodes[name], SINGLETON_EXTEND_HOOKS) ||
371
+ defines_any?(scope.discovered_def_nodes[name], INSTANCE_EXTEND_HOOKS)
372
+ end
373
+
374
+ # Read off the def-node tables: the existence table does not carry a module's own instance hook once the
375
+ # extend fold has copied it onto the extender's singleton.
376
+ def self.defines_any?(defs, hooks)
377
+ !defs.nil? && hooks.any? { |hook| defs.key?(hook) }
378
+ end
379
+
380
+ # `[[spelling, resolved name or nil, candidate names], …]`: every module the project entries of an instance chain
381
+ # `extend`, as {Builder#extend_hook_taint?} resolved them.
382
+ def self.extend_targets(scope, flavor, entries)
383
+ resolver = resolver_for(scope, flavor)
384
+ extends = scope.discovery.discovered_extends
385
+ entries.each_with_object([]) do |entry, out|
386
+ next if entry.external? || entry.side != :instance
387
+
388
+ (extends[entry.name] || EMPTY_NAMES).each do |raw|
389
+ resolved = resolver.resolve_one(entry.name, raw)
390
+ out << [raw, resolved, resolved ? EMPTY_NAMES : resolver.candidates(entry.name, raw)].freeze
391
+ end
392
+ end.uniq.freeze
393
+ end
394
+
395
+ @hook_keys = {}
396
+
397
+ # The frozen `method:Name.hook` negative keys of `name`'s {Relevance::HOOKS}, built once per name.
398
+ def self.hook_keys(name)
399
+ @hook_keys[name] ||= Relevance::HOOKS.map { |hook| "method:#{name}.#{hook}".freeze }.freeze
400
+ end
401
+
271
402
  def record_head(scope, start, side)
272
403
  ResolutionChain.record_class(scope, @root) unless start.zero? && @entries.first&.name == @root
273
404
  start.times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
@@ -397,7 +528,8 @@ module Rigor
397
528
  @discovery.discovered_superclasses.key?(name) || @discovery.discovered_includes.key?(name) ||
398
529
  @discovery.discovered_classes.key?(name)
399
530
  when :arity
400
- @scope.known_user_class?(name) || @discovery.discovered_parameter_envelopes.key?(name)
531
+ @scope.known_user_class?(name) ||
532
+ DiscoveryIndex.declares_surface?(@discovery.discovered_parameter_envelopes[name])
401
533
  else
402
534
  @scope.known_user_class?(name)
403
535
  end
@@ -601,7 +733,8 @@ module Rigor
601
733
  # The chain for `root`, carrying its retro world when the skip rule skipped anything on the way.
602
734
  def chain(root, side)
603
735
  entries, starts, classes, forks, marks, retro_ok = lin_for(root, side)
604
- retro = build_retro(root, side) if forks == 1 && retro_ok && marks.empty? && !@retro
736
+ order_marked = marks.any? { |mark| mark.kind != :extend_hook }
737
+ retro = build_retro(root, side) if forks == 1 && retro_ok && !order_marked && !@retro
605
738
  cut_at = cut_position(entries)
606
739
  return whole_chain(root, side, entries, starts, classes, [forks, marks], retro) if cut_at.nil?
607
740
 
@@ -701,6 +834,7 @@ module Rigor
701
834
  def compute_instance(name, depth)
702
835
  # `discovered_includes` already carries the prepended names, so it counts every instance-side edge once.
703
836
  mark_unsettled(name, :include, (@discovery.discovered_includes[name] || EMPTY).size)
837
+ @frames.last.add_marks(mark_for(name, :extend_hook, WILDCARD_LIST, false)) if extend_hook_taint?(name)
704
838
  entries = [project_entry(name, :instance)]
705
839
  super_lin = superclass_lin(name, :instance, depth)
706
840
  entries.concat(super_lin[0])
@@ -743,6 +877,20 @@ module Rigor
743
877
  multi)].freeze
744
878
  end
745
879
 
880
+ WILDCARD_LIST = ["*"].freeze # Relevance::WILDCARD, which loads after this file
881
+ private_constant :WILDCARD_LIST
882
+
883
+ # #1730 — whether a module `name` extends has a hook that may mix into `name`'s instance side or its
884
+ # includers' ({ResolutionChain.extend_reshapes_instance?}). Ruby runs it at the `extend`, so the mixin is
885
+ # not a fact of the tables and `name`'s instance side is unpositioned. A spelling no project module
886
+ # answers taints nothing: the project cannot see its hooks.
887
+ def extend_hook_taint?(name)
888
+ (@discovery.discovered_extends[name] || EMPTY).any? do |raw|
889
+ resolved = @resolver.resolve_one(name, raw)
890
+ !resolved.nil? && ResolutionChain.extend_reshapes_instance?(@scope, resolved)
891
+ end
892
+ end
893
+
746
894
  def multi_file?(name)
747
895
  sites = @discovery.discovered_class_sources[name]
748
896
  !sites.nil? && sites.size >= 2
@@ -866,14 +1014,14 @@ module Rigor
866
1014
 
867
1015
  def project_entry(name, side)
868
1016
  by_side = (@bucket[:interned][side] ||= {})
869
- by_side[name] ||= Entry.new(name, side, nil, nil, false, name.to_s.split("::").last.freeze)
1017
+ by_side[name] ||= Entry.new(name, side, nil, nil, false, name.to_s.split("::").last.freeze, nil)
870
1018
  end
871
1019
 
872
1020
  def external_entry(owner, raw, side, superclass_edge)
873
1021
  by_edge = ((@bucket[:externals][side] ||= {})[superclass_edge] ||= {})
874
1022
  by_owner = (by_edge[owner] ||= {})
875
1023
  by_owner[raw] ||= Entry.new(nil, side, raw, @scope.ancestor_name_candidates(owner, raw).freeze,
876
- superclass_edge, nil)
1024
+ superclass_edge, nil, owner)
877
1025
  end
878
1026
  end
879
1027
  private_constant :Builder