rigortype 0.4.0 → 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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  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/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../source/constant_path"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module DeclarationWalk
8
+ # What `self`, the cref and `Module.nesting` are at one node of a {DeclarationWalk} (ADR-116 WD5). The walk
9
+ # derives each child's context from its parent's through the transitions below, so every collector reads
10
+ # the same answer for the same node and none of them recomputes it.
11
+ #
12
+ # - `prefix` — the qualified-name segments of the innermost `class` / `module` a name reaches, which is
13
+ # what the tables key lexical facts under: `["Admin", "User"]` inside `module Admin; class User`, `[]`
14
+ # at the top level. It is not always the cref. Directly inside `class << self` the cref is the
15
+ # singleton class, which has no name, and `prefix` stays the enclosing class's while `singleton_cref`
16
+ # says so; below a bare header there (`class D` opens `#<Class:C>::D`, which no name spells) it is
17
+ # `[]`.
18
+ # - `self_owner` — the rebound `self` a `self::` header, write target or eval receiver anchors on. nil
19
+ # while `self` is the lexical class; a prefix while a meta-new or eval-family block rebinds it; `[]`
20
+ # while it names nothing (a `class <<` body, an anonymous factory block, an eval receiver no name
21
+ # reaches).
22
+ # - `singleton_cref` — whether the lexical cref is unnameable. `class <<` sets it, and a header that
23
+ # still names its class re-anchors it.
24
+ # - `nesting` — `Module.nesting` as the ancestry tables record it (issue #682): a nameable
25
+ # `class`/`module` keyword pushes, a `self::` header pushes the prefix its rebound self gives it, and
26
+ # nothing else pushes. nil when the caller does not track it, or when it was lost below a header that
27
+ # renders no name ({#lost_header_body}). The context keeps the two apart: a `self::` header below a
28
+ # rebound self grows a lost chain again, while an untracked one stays nil everywhere.
29
+ # - `scope` — the census scope a collector types an rvalue under, carrying the chain
30
+ # {ScopeIndexer.scope_entering_declaration} stamps at each header. nil when the caller has none.
31
+ # - `source_path` — the path of the file being walked, which an anonymous class's synthetic name carries
32
+ # ({AnonymousMetaClass.name_for}). nil when the caller has none. Read it through
33
+ # {#anonymous_class_path}, which answers each variant of that rule.
34
+ # - `class_body` — whether a `class` / `module` body, a meta-new write's body or an eval-family block's
35
+ # body encloses the node. A `class <<` body and a bare factory block do not set it. It exists for the
36
+ # `anonymous_class_path` variant {#anonymous_class_path} documents, and nothing else reads it.
37
+ #
38
+ # Nameability is derived, not stored: {#unnameable_self?} answers it from the fields above.
39
+ #
40
+ # `nesting` and the chain on `scope` are two fields because the walks that threaded them disagree. The
41
+ # scope's chain is pushed at EVERY header, an unnameable one and the lenient render of a `self::` header
42
+ # included (`class self::D` pushes `Outer::D`); the ancestry chain is not. A move keeps both answers
43
+ # (ADR-116's variant rule); the scope's is wrong below `class <<` (#1520), and converging the two is
44
+ # #1521's.
45
+ #
46
+ # The rules themselves stay in {ScopeIndexer} ({ScopeIndexer.decl_body_context},
47
+ # {ScopeIndexer.meta_new_block_split}, {ScopeIndexer.eval_block_split}), where the walkers not yet ported
48
+ # still call them; this class is the one place a ported collector reaches them from. That is why the
49
+ # entry point is `rigor/inference/declaration_walk`, which loads `ScopeIndexer` and, through it, this file.
50
+ class Context
51
+ EMPTY_PREFIX = [].freeze
52
+
53
+ # The chain below a header that renders no name, held in place of it so that a `self::` header can
54
+ # tell it from an untracked one. {#nesting} answers nil for it; only its identity is read. Not an empty
55
+ # Array: Ruby folds every `[].freeze` literal into one shared object, so the sentinel would be the
56
+ # top level's `[]` chain.
57
+ LOST_NESTING = Object.new.freeze
58
+ private_constant :LOST_NESTING
59
+
60
+ attr_reader :prefix, :self_owner, :singleton_cref, :scope, :source_path, :class_body
61
+
62
+ # A file's top level: `self` is `main`, which the lexical-class convention (a nil `self_owner`)
63
+ # already answers.
64
+ def self.root(scope: nil, nesting: nil, source_path: nil)
65
+ new(EMPTY_PREFIX, nil, false, nesting, scope, source_path, false)
66
+ end
67
+
68
+ def initialize(prefix, self_owner, singleton_cref, nesting, scope, source_path, class_body)
69
+ @prefix = prefix
70
+ @self_owner = self_owner
71
+ @singleton_cref = singleton_cref
72
+ @nesting = nesting
73
+ @scope = scope
74
+ @source_path = source_path
75
+ @class_body = class_body
76
+ freeze
77
+ end
78
+
79
+ def nesting
80
+ @nesting unless @nesting.equal?(LOST_NESTING)
81
+ end
82
+
83
+ # Whether a bare, `self` or `self::` reference here names nothing a table can key on: inside a
84
+ # `class <<` body, under a `self` an eval or factory block left unnamed, or below an unnameable cref
85
+ # that no rebound self re-anchors.
86
+ def unnameable_self?
87
+ ScopeIndexer.unnameable_eval_self?(false, self_owner, prefix, singleton_cref)
88
+ end
89
+
90
+ # The file path an anonymous class created at this node carries in its synthetic name, under the
91
+ # variant of that rule the asking collector follows (see {DeclarationWalk::RULE_VARIANTS}):
92
+ #
93
+ # - `:whole_file` — the walk's rule: the path everywhere, as the evaluator, the dispatcher and the
94
+ # methods walker name the class.
95
+ # - `:outside_class_bodies` — `walk_class_superclasses`' rule, kept so the superclass table stays
96
+ # byte-identical: nil once {#class_body} is set, so `Class.new(P) { }` inside `class C` is keyed
97
+ # `#<Class:L:C>` while the other tables name it `#<Class:path:L:C>` (#1521 item 11).
98
+ def anonymous_class_path(variant)
99
+ case variant
100
+ when :whole_file then source_path
101
+ when :outside_class_bodies then class_body ? nil : source_path
102
+ else raise ArgumentError, "no anonymous_class_path variant #{variant.inspect}"
103
+ end
104
+ end
105
+
106
+ # The context a `class` / `module` header gives its body, or nil when the header renders no prefix
107
+ # (a parsed header always renders one; nil keeps the transition total). `self` is the class again,
108
+ # so the rebound self resets.
109
+ def declaration_body(node)
110
+ self_decl, child_prefix, child_cref =
111
+ ScopeIndexer.decl_body_context(node, prefix, self_owner, singleton_cref)
112
+ return nil unless child_prefix
113
+
114
+ Context.new(child_cref ? EMPTY_PREFIX : child_prefix, nil, child_cref,
115
+ declaration_nesting(node, self_decl, child_cref),
116
+ ScopeIndexer.scope_entering_declaration(scope, node.constant_path), source_path, true)
117
+ end
118
+
119
+ # The context of a `class << expr` body. `self` is the singleton class, which no `self::` path names,
120
+ # and the cref is unnameable until a nameable header re-anchors it. The rung Ruby pushes names nothing,
121
+ # so neither chain moves.
122
+ def singleton_class_body
123
+ Context.new(prefix, EMPTY_PREFIX, true, @nesting, scope, source_path, class_body)
124
+ end
125
+
126
+ # The body of a `K = Class.new { … }`-shaped write: `self` is the class the write names (`[]` when it
127
+ # names none). The cref and both chains stay lexical.
128
+ def meta_new_body(owner)
129
+ Context.new(prefix, owner, singleton_cref, @nesting, scope, source_path, true)
130
+ end
131
+
132
+ # The body of an eval-family block: `self` is the receiver (`[]` when no name reaches it). The cref and
133
+ # both chains stay lexical.
134
+ def eval_body(owner)
135
+ Context.new(prefix, owner, singleton_cref, @nesting, scope, source_path, true)
136
+ end
137
+
138
+ # The body of a bare factory block under the walk's rule: `self` is an anonymous class no name reaches.
139
+ # The cref and both chains stay lexical.
140
+ def factory_body
141
+ Context.new(prefix, EMPTY_PREFIX, singleton_cref, @nesting, scope, source_path, class_body)
142
+ end
143
+
144
+ # The body of a `class` / `module` whose header renders no name (`class foo`, `module` followed by a
145
+ # `def`: parse-error shapes), under the `:body_with_lost_nesting` variant of the `unrendered_header`
146
+ # rule (see {DeclarationWalk::RULE_VARIANTS}). `self` is the class again; the chain is kept below an
147
+ # unnameable cref and lost otherwise, because nothing can be pushed for a header with no name. An
148
+ # untracked chain stays untracked.
149
+ def lost_header_body
150
+ lost = singleton_cref || @nesting.nil? ? @nesting : LOST_NESTING
151
+ Context.new(prefix, nil, singleton_cref, lost, scope, source_path, true)
152
+ end
153
+
154
+ # The prefix the meta-new and eval-family splits resolve against, under the variant of the
155
+ # `lexical_prefix` rule the asking collectors follow: `:prefix` is the walk's rule; `:nesting_head` is
156
+ # `walk_def_nestings`' — the innermost `Module.nesting` entry split into segments, or `[]`. The two
157
+ # differ below a compact header (`["Admin::Widget"]` against `["Admin", "Widget"]`, which resolve an
158
+ # eval receiver through different chains) and below an unnameable cref (`[]` against the enclosing
159
+ # entry) (#1521).
160
+ def lexical_prefix(variant = :prefix)
161
+ case variant
162
+ when :prefix then prefix
163
+ when :nesting_head then (head = nesting&.first) ? head.split("::") : EMPTY_PREFIX
164
+ else raise UnknownVariant, "no lexical_prefix variant #{variant.inspect}"
165
+ end
166
+ end
167
+
168
+ # `[enclosing_parts, body, body_self]` for a `K = Class.new { … }`-shaped write, or nil when its
169
+ # rvalue is not the idiom ({ScopeIndexer.meta_new_block_split}).
170
+ def meta_new_split(node, variant = :prefix)
171
+ ScopeIndexer.meta_new_block_split(node, lexical_prefix(variant), self_owner, singleton_cref)
172
+ end
173
+
174
+ # `[enclosing_parts, body, eval_self]` for an eval-family call with a block, or nil
175
+ # ({ScopeIndexer.eval_block_split}).
176
+ def eval_split(node, variant = :prefix)
177
+ ScopeIndexer.eval_block_split(node, lexical_prefix(variant), self_owner, singleton_cref)
178
+ end
179
+
180
+ private
181
+
182
+ # An untracked chain stays untracked. A `self::` header pushes the prefix its rebound self gives it even
183
+ # below a lost chain, which is how `walk_def_nestings` has always read `[joined, *nil]`; any other
184
+ # header keeps a lost chain lost.
185
+ def declaration_nesting(node, self_decl, child_cref)
186
+ return @nesting if child_cref || @nesting.nil?
187
+ return [self_decl.join("::"), *nesting].freeze if self_decl
188
+ return @nesting if @nesting.equal?(LOST_NESTING)
189
+
190
+ Source::ConstantPath.pushed_nesting(@nesting, node.constant_path) || @nesting
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ module DeclarationWalk
6
+ # A failure of the walk's contract rather than of the file being walked: a shadow-harness divergence
7
+ # ({Shadow::Divergence}) or a collector naming a variant no rule has ({UnknownVariant}). The discovery
8
+ # passes skip a file they cannot read or parse, and the run-result cache path falls back to an uncached
9
+ # run on an error; both let this through, because skipping the file would hide the failure and drop the
10
+ # file from the project index. In a file's own index the per-file rescue still reports it as an error
11
+ # row on the file.
12
+ class ContractError < StandardError; end
13
+
14
+ # A collector's `VARIANTS` names a rule or a variant {RULE_VARIANTS} does not have.
15
+ class UnknownVariant < ContractError; end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../analysis/shadow_harness"
6
+ require_relative "errors"
7
+
8
+ module Rigor
9
+ module Inference
10
+ module DeclarationWalk
11
+ # ADR-53 Track B's shadow harness, extended from the rule collectors to the discovery tables (ADR-116
12
+ # WD5). With `RIGOR_SHADOW_RULE_WALK` set, a table a {DeclarationWalk} collector builds is compared with
13
+ # the one the legacy walker it replaces builds from the same input, and the first difference raises
14
+ # {Divergence}. Unset, the legacy walker never runs.
15
+ #
16
+ # Equality is stricter than `Hash#==`: key ORDER counts, and so do a container's frozenness and
17
+ # `compare_by_identity`. A table is iterated when it is merged, folded and written to a seed bundle, so
18
+ # an order change is a change in what later stages see even where `==` holds. Not compared yet: a
19
+ # `Set`'s order and frozenness (a `Set` compares by `==` alone), a Hash's `default` / `default_proc`,
20
+ # and the fields inside a `Data` or `Struct` value beyond its `==`. No ported table holds one yet; a
21
+ # port whose table does extends {#first_difference} first.
22
+ #
23
+ # ## Running it
24
+ #
25
+ # Set the variable for the whole `rigor check` process, pool workers included: `RIGOR_SHADOW_RULE_WALK=1
26
+ # rigor check …`. Where a divergence surfaces depends on which pass built the table:
27
+ #
28
+ # - in a file's own index ({ScopeIndexer.index}), the raise lands in the per-file rescue and reports
29
+ # as an error diagnostic on the file, carrying the message, the same way a rule-walk divergence does;
30
+ # - in the cross-file project pre-pass (the superclass tables are built there too), the raise aborts
31
+ # the run with the message. {Divergence} is a {ContractError}, which the pre-pass loops, the
32
+ # run-result cache fallback and the parameter-inference discovery seed all let through (see
33
+ # {ContractError}).
34
+ #
35
+ # A run that completes with no such row found no divergence. The rule-walk harness reads the same
36
+ # variable ({Analysis::ShadowHarness}).
37
+ #
38
+ # A warm cache cannot answer for a run the harness did not check. The ADR-45 run-result cache (and the
39
+ # ADR-87 boot-slim probe and the effects sidecar, which share its key) and the ADR-46 incremental
40
+ # snapshot, seed bundles included, key on {Analysis::ShadowHarness.cache_identity}. So a run with the
41
+ # variable never replays a result computed without it, and a divergence row cached under it never
42
+ # replays into a run without it. A warm hit WITH the variable replays what the same engine computed on
43
+ # the same inputs with the harness on, divergence rows included; `--no-cache` sends every file through
44
+ # the check again.
45
+ module Shadow
46
+ ENV_KEY = Analysis::ShadowHarness::ENV_KEY
47
+
48
+ class Divergence < ContractError; end
49
+
50
+ module_function
51
+
52
+ def enabled?
53
+ Analysis::ShadowHarness.enabled?
54
+ end
55
+
56
+ # `walk`, after checking it against the table the block builds when the harness is on. The block is
57
+ # the legacy walker, so a disabled harness never pays for it.
58
+ def verified(table, path, walk)
59
+ verify!(table, path, walk, yield) if enabled?
60
+ walk
61
+ end
62
+
63
+ def verify!(table, path, walk, legacy)
64
+ difference = first_difference(legacy, walk, "")
65
+ return if difference.nil?
66
+
67
+ raise Divergence,
68
+ "#{ENV_KEY} divergence: discovery table `#{table}` for #{path || '(no path)'}: #{difference}"
69
+ end
70
+
71
+ # A description of the first place `walk` differs from `legacy`, or nil when they are the same.
72
+ # `at` is the key path walked so far, rendered as Ruby index syntax.
73
+ def first_difference(legacy, walk, at)
74
+ if legacy.is_a?(Hash) && walk.is_a?(Hash)
75
+ hash_difference(legacy, walk, at)
76
+ elsif legacy.is_a?(Array) && walk.is_a?(Array)
77
+ array_difference(legacy, walk, at)
78
+ elsif legacy.class != walk.class || legacy != walk
79
+ "#{location(at)}: legacy #{render(legacy)}, declaration walk #{render(walk)}"
80
+ end
81
+ end
82
+
83
+ def hash_difference(legacy, walk, at)
84
+ container_difference(legacy, walk, at) ||
85
+ key_difference(legacy, walk, at) ||
86
+ legacy.each_key.lazy.filter_map do |key|
87
+ first_difference(legacy[key], walk[key], "#{at}[#{render_key(key)}]")
88
+ end.first
89
+ end
90
+
91
+ def array_difference(legacy, walk, at)
92
+ container_difference(legacy, walk, at) ||
93
+ (legacy.size == walk.size ? nil : size_difference(legacy, walk, at)) ||
94
+ legacy.each_index.lazy.filter_map do |index|
95
+ first_difference(legacy[index], walk[index], "#{at}[#{index}]")
96
+ end.first
97
+ end
98
+
99
+ def container_difference(legacy, walk, at)
100
+ if legacy.frozen? != walk.frozen?
101
+ "#{location(at)}: legacy frozen=#{legacy.frozen?}, declaration walk frozen=#{walk.frozen?}"
102
+ elsif legacy.is_a?(Hash) && legacy.compare_by_identity? != walk.compare_by_identity?
103
+ "#{location(at)}: legacy compare_by_identity=#{legacy.compare_by_identity?}, " \
104
+ "declaration walk compare_by_identity=#{walk.compare_by_identity?}"
105
+ end
106
+ end
107
+
108
+ def key_difference(legacy, walk, at)
109
+ missing = legacy.each_key.find { |key| !walk.key?(key) }
110
+ return "#{location(at)}: key #{render_key(missing)} only in legacy" if missing
111
+
112
+ extra = walk.each_key.find { |key| !legacy.key?(key) }
113
+ return "#{location(at)}: key #{render_key(extra)} only in declaration walk" if extra
114
+
115
+ index = legacy.keys.zip(walk.keys).index { |ours, theirs| !ours.eql?(theirs) }
116
+ return nil if index.nil?
117
+
118
+ "#{location(at)}: key order differs at position #{index}: legacy #{render_key(legacy.keys[index])}, " \
119
+ "declaration walk #{render_key(walk.keys[index])}"
120
+ end
121
+
122
+ def size_difference(legacy, walk, at)
123
+ "#{location(at)}: legacy has #{legacy.size} elements, declaration walk #{walk.size}"
124
+ end
125
+
126
+ def location(at)
127
+ at.empty? ? "the table" : at
128
+ end
129
+
130
+ # A key as it inspects, except a Prism node, which keys the identity tables and would inspect as its
131
+ # whole subtree: that renders as its class, its name where it has one, and where it starts.
132
+ def render_key(key)
133
+ return key.inspect unless key.is_a?(Prism::Node)
134
+
135
+ name = key.respond_to?(:name) ? " #{key.name}" : ""
136
+ start = key.location
137
+ "#<#{key.class.name.delete_prefix('Prism::')}#{name} at #{start.start_line}:#{start.start_column}>"
138
+ end
139
+
140
+ # A Rigor type renders as it describes itself; anything else as it inspects.
141
+ def render(value)
142
+ value.respond_to?(:describe) ? value.describe : value.inspect
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end