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
@@ -34,6 +34,7 @@ module Rigor
34
34
  :discovered_includes,
35
35
  :discovered_prepends,
36
36
  :discovered_extends,
37
+ :unpositioned_mixins,
37
38
  :discovered_class_sources,
38
39
  :constant_sources,
39
40
  :constant_writers,
@@ -49,7 +50,14 @@ module Rigor
49
50
  :patched_line_readers,
50
51
  :clears_last_status,
51
52
  :defines_case_equality,
52
- :implicit_self_evidence
53
+ :implicit_self_evidence,
54
+ :in_effect_refinements,
55
+ :possible_discovered_methods,
56
+ :possible_discovered_deferred_ranges,
57
+ :contested_discovered_def_nodes,
58
+ :contested_discovered_singleton_def_nodes,
59
+ :contested_discovered_method_visibilities,
60
+ :contested_discovered_parameter_envelopes
53
61
  )
54
62
 
55
63
  class DiscoveryIndex
@@ -58,6 +66,115 @@ module Rigor
58
66
  EMPTY_NAME_SET = Set.new.freeze
59
67
  private_constant :EMPTY_NODE_TABLE, :EMPTY_TABLE, :EMPTY_NAME_SET
60
68
 
69
+ # ADR-119 WD1 — each member that may one day admit `possible` or `contested` facts, and the sibling that
70
+ # holds them. Every copy path reads this one declaration, so a member never travels without its sibling:
71
+ # {#with} raises on a half pair and {.compact_pairs} drops a pair only when BOTH halves are empty. No
72
+ # producer fills a sibling yet (the lane-1 carriage), so every sibling is empty and each member keeps its
73
+ # meaning, the union of its certain and possible facts.
74
+ #
75
+ # - `possible_*` has its member's shape and is a subset of it (a `discovered_methods` pair is a
76
+ # `{class => {name => kind}}` table).
77
+ # - `contested_*` is a Set of key paths into its member, one per entry whose value depends on a possible
78
+ # fact: `[class, method]`, or `[class, [kind, method]]` for an envelope.
79
+ #
80
+ # The members without a sibling here (`discovered_def_sources`, `discovered_singleton_def_sources`) are site
81
+ # tables (dependency edges, positions, fingerprints): no read takes a definer, visibility, arity or type from
82
+ # them, and certainty does not move a site. The reads whose answer depends on the site are
83
+ # `Scope#same_file_top_level_def?` (file identity, independent of certainty), `#singleton_def_shadows_call?`
84
+ # and `#instance_def_shadows_call?` (a conditional def's site answers "shadows"). Where "shadows" narrows an
85
+ # answer, the caller pairs it with {#possible_method?}: existence is the method table, certainty is the possible
86
+ # table alone (ADR-119 C1d-c).
87
+ SIBLINGS = {
88
+ discovered_methods: :possible_discovered_methods,
89
+ discovered_deferred_ranges: :possible_discovered_deferred_ranges,
90
+ discovered_def_nodes: :contested_discovered_def_nodes,
91
+ discovered_singleton_def_nodes: :contested_discovered_singleton_def_nodes,
92
+ discovered_method_visibilities: :contested_discovered_method_visibilities,
93
+ discovered_parameter_envelopes: :contested_discovered_parameter_envelopes
94
+ }.freeze
95
+
96
+ # Either half of a pair to the other.
97
+ PARTNERS = SIBLINGS.flat_map { |member, sibling| [[member, sibling], [sibling, member]] }.to_h.freeze
98
+ private_constant :PARTNERS
99
+
100
+ # [#1507](https://github.com/rigortype/rigor/issues/1507) — the kind of fact each member holds, which decides
101
+ # what a reader may conclude from an entry and which gate covers the member (ADR-119 WD4, proposed). Each
102
+ # reason describes the table as the code builds it today. `spec/rigor/declaration_facts/member_classes_spec.rb`
103
+ # fails on a member that is unclassified or classified twice, and checks each class's shape on an index built
104
+ # from a fixture project; it pins the few misfilings those checks accept.
105
+ #
106
+ # - `:set_valued` — names, edges, files or rows whose presence is the fact. A reader asks whether an entry is
107
+ # there.
108
+ # - `:single_valued` — one value per key, which readers use; disagreeing contributions fold by a fixed rule.
109
+ # - `:typed` — the values are types, which already carry uncertainty in the type lattice.
110
+ # - `:syntactic` — read off the parse of the analysed file alone, which is their reference.
111
+ # - `:run_state` — the run's own state, not a fact about the program.
112
+ # - `:sibling` — the `possible_*` / `contested_*` half of a pair in {SIBLINGS}, which travels with its member.
113
+ MEMBER_CLASSES = {
114
+ set_valued: {
115
+ discovered_classes: "the class and module names the project declares; the value is the name's singleton",
116
+ discovered_methods: "which names each class answers, and on which side",
117
+ discovered_refinements: "the methods a refine block adds, keyed by the refined class",
118
+ discovered_global_write_census: "the names any file aliases, defines or mixes in at the top level",
119
+ discovered_includes: "the modules each class includes, prepends among them",
120
+ discovered_prepends: "the modules each class prepends",
121
+ discovered_extends: "the modules each singleton mixes in; the module itself for module_function",
122
+ unpositioned_mixins: "per side, the mixin edges each class records whose order is not a fact, or \"*\"",
123
+ discovered_class_sources: "the files that declare each class, reopenings included",
124
+ discovered_deferred_ranges: "each file's def and block ranges; def rows add owner and module_function side",
125
+ constant_sources: "the files that write each constant",
126
+ constant_writers: "the constant names some write binds, by last segment",
127
+ constant_shadowers: "the constant names a non-memo write assigns, by last segment",
128
+ published_constant_names: "the last segments of the constants the project publishes",
129
+ local_constant_names: "the last segments of the constants the analysed file assigns",
130
+ published_constant_alias_names: "the file's constants copied straight from a published one",
131
+ published_constant_ivars: "the ivars each class seeds from a published constant"
132
+ }.freeze,
133
+ single_valued: {
134
+ discovered_def_nodes: "the def node each instance method resolves to",
135
+ discovered_singleton_def_nodes: "the def node each singleton method resolves to",
136
+ discovered_def_sources: "the first-seen path:line of each instance method, which readers use",
137
+ discovered_singleton_def_sources: "the first-seen path:line of each singleton method, which readers use",
138
+ discovered_method_visibilities: "each method's visibility",
139
+ discovered_parameter_envelopes: "each method's parameter shape, opaque when contributions disagree",
140
+ discovered_superclasses: "each class's superclass as written",
141
+ discovered_header_nestings: "the cref each ancestor name resolves in, an alternatives list when ambiguous",
142
+ data_member_layouts: "each Data class's member layout",
143
+ struct_member_layouts: "each Struct class's member layout"
144
+ }.freeze,
145
+ typed: {
146
+ declared_types: "the type each declaration node binds",
147
+ class_ivars: "the joined type of each ivar's writes, per class",
148
+ class_cvars: "the joined type of each cvar's writes, per class",
149
+ program_globals: "the joined type of each global's writes",
150
+ program_global_seeds: "a declared global's signature type joined with its writes",
151
+ in_source_constants: "the type each constant's write binds",
152
+ param_inferred_types: "the union of each parameter's call-site argument types"
153
+ }.freeze,
154
+ syntactic: {
155
+ discovered_def_nestings: "the Module.nesting each def is written in, keyed by node identity",
156
+ patched_line_readers: "the gets / readline names the file defines through the define_method family",
157
+ clears_last_status: "whether the analysed file holds a call that may set $? to nil",
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",
160
+ implicit_self_evidence: "where the file's implicit-self readers sit; built lazily from its tree, never seeded"
161
+ }.freeze,
162
+ run_state: {
163
+ run_generation: "the identity token of the current run"
164
+ }.freeze,
165
+ sibling: {
166
+ possible_discovered_methods: "the names and sides of discovered_methods that only a possible definer gives",
167
+ possible_discovered_deferred_ranges: "the rows of discovered_deferred_ranges that only a possible fact gives",
168
+ contested_discovered_def_nodes: "the key paths of discovered_def_nodes that a possible definer may change",
169
+ contested_discovered_singleton_def_nodes:
170
+ "the key paths of discovered_singleton_def_nodes that a possible definer may change",
171
+ contested_discovered_method_visibilities:
172
+ "the key paths of discovered_method_visibilities that a possible fact may change",
173
+ contested_discovered_parameter_envelopes:
174
+ "the key paths of discovered_parameter_envelopes that a possible fact may change"
175
+ }.freeze
176
+ }.freeze
177
+
61
178
  # The third value a `discovered_methods` entry can hold, beside `:instance` and `:singleton`. One name may
62
179
  # legitimately be defined on both sides of the same class (`def helper` plus a `class << self` twin), and the
63
180
  # table is keyed by name alone — so before this existed the second `def` overwrote the first's kind and
@@ -89,6 +206,9 @@ module Rigor
89
206
  # `Source::ParameterEnvelope::OPAQUE`, so a bucket folds under the one join every other entry does.
90
207
  # Symbols, because every per-method key is an Array and neither can collide with the other.
91
208
  #
209
+ # The class marks below and {ENVELOPE_PROJECT_WIDE} are presence facts read only to decline a read; they are
210
+ # never a contested path. Contested envelope paths are always `[class, [kind, name]]`.
211
+ #
92
212
  # - {ENVELOPE_MODULE_MARK}: the name is declared with `module` (or `Const = Module.new do … end`) somewhere
93
213
  # in the project. An instance of it is an instance of an unknown includer.
94
214
  # - {ENVELOPE_DYNAMIC_MARK}: the class body rewrites its method table in a way no literal argument names
@@ -99,12 +219,36 @@ module Rigor
99
219
  # - {ENVELOPE_REFINED_MARK}: a `refine` block refines the class, which redefines its methods wherever a `using`
100
220
  # is in effect. Issue #1367 splits it from {ENVELOPE_DYNAMIC_MARK}, which it used to share: an implicit
101
221
  # conversion (`to_str`, `to_int`) ignores a refinement, so the `global.*` rules must tell the two apart.
102
- # 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>"
103
234
  ENVELOPE_MODULE_MARK = :"<module>"
104
235
  ENVELOPE_DYNAMIC_MARK = :"<dynamic>"
105
236
  ENVELOPE_OBJECT_EXTENDED_MARK = :"<object-extended>"
106
237
  ENVELOPE_REFINED_MARK = :"<refined>"
107
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
+
108
252
  # Whether an envelope bucket says the project rewrites the class's method table beyond its literal names: a
109
253
  # dynamic rewrite, or a refinement.
110
254
  def self.rewritten_surface?(bucket)
@@ -118,6 +262,14 @@ module Rigor
118
262
  # present. Not a constant character, so no class can be named it.
119
263
  ENVELOPE_PROJECT_WIDE = "<project-wide>"
120
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
+
121
273
  # The shared all-empty index `Scope.empty` (and every scope that never sees a seeding pass) points at — one
122
274
  # allocation per process.
123
275
  EMPTY = new(
@@ -210,6 +362,24 @@ module Rigor
210
362
  # the table inside the indexer and threw it away; `Narrowing` needs it to survive onto the scope,
211
363
  # because `Singleton[C]`'s ancestry is exactly what `extend` writes and nothing else records it.
212
364
  discovered_extends: EMPTY_TABLE,
365
+ # `{qualified class or module name => {include: [names], extend: [names]}}` — the mixin edges of the three
366
+ # tables above whose ORDER is not a fact, per side (`:include` is the instance side, `include` and
367
+ # `prepend`; `:extend` the singleton side). An edge is listed when it is written anywhere but as a direct
368
+ # statement of the class's own body (or its `class << self` body) in a declaration that is itself a
369
+ # direct statement — inside a `def`, a block, a conditional, or a conditional or deferred declaration —
370
+ # or in the receiver form, and when it shares an argument list with a name the walk could not record
371
+ # (`Inference::ScopeIndexer::MixinAccumulator`). A mixin call the walk cannot record at all (`include
372
+ # helper`, `include(*MODS)`, `send(:include, M)`, `self.include M`, `C.include(M)`,
373
+ # `singleton_class.include M`, a call on a `self.included(base)`-style hook's parameter, an `*_eval` block on
374
+ # one or on an opaque receiver whose body mixes a module in) lists the name
375
+ # `"*"` on the side it reshapes: that side of the owner has no known order. An edge written both ways
376
+ # is listed. Folded by union. A reader that depends on the order of a class's ancestors MUST decline
377
+ # when the owner it asks about, or any ancestor it walks through, lists anything on the side it reads;
378
+ # the three tables themselves are unchanged.
379
+ unpositioned_mixins: EMPTY_TABLE,
380
+ # The files that declare each class, reopenings included. Seeded on every run: the ADR-46 recording
381
+ # accessors read it for their class edges, and an ancestor-order reader reads whether a class is
382
+ # declared in more than one file, where the order of its mixins across the files is load order.
213
383
  discovered_class_sources: EMPTY_TABLE,
214
384
  # Issue #644 — `{qualified constant name => Set[declaring file]}`, the write attribution behind the
215
385
  # cross-file value-constant table. Read only by `Scope#record_constant_dependency` during ADR-46
@@ -280,8 +450,93 @@ module Rigor
280
450
  # (`Inference::LastLine::SelfEvidence`, read on the first ask), which `Inference::LastLine.reads_line?` reads
281
451
  # them by. Filled by `Inference::ScopeIndexer.index` from the file's own tree only; nil, where no file was
282
452
  # indexed, declines every such reader.
283
- implicit_self_evidence: nil
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,
459
+ # ADR-119 WD1 — the siblings of the members {SIBLINGS} pairs, empty until a producer admits a possible fact.
460
+ possible_discovered_methods: EMPTY_TABLE,
461
+ possible_discovered_deferred_ranges: EMPTY_TABLE,
462
+ contested_discovered_def_nodes: EMPTY_NAME_SET,
463
+ contested_discovered_singleton_def_nodes: EMPTY_NAME_SET,
464
+ contested_discovered_method_visibilities: EMPTY_NAME_SET,
465
+ contested_discovered_parameter_envelopes: EMPTY_NAME_SET
284
466
  )
467
+
468
+ # The siblings of an all-empty index, `{sibling name => empty value}`, frozen: what a def-index with no
469
+ # possible fact carries, and a Runner's initial state. Copy it (`dup`) before mutating.
470
+ EMPTY_SIBLINGS = SIBLINGS.each_value.to_h { |sibling| [sibling, EMPTY.public_send(sibling)] }.freeze
471
+
472
+ # The all-empty value of a paired member or sibling.
473
+ def self.empty_value(name)
474
+ EMPTY.public_send(name)
475
+ end
476
+
477
+ def self.empty_siblings = EMPTY_SIBLINGS
478
+
479
+ # ADR-119 WD1 — `tables` (a `with(**)` argument Hash) with every pair either complete or absent: a pair whose
480
+ # halves are both missing, nil or empty is dropped, any other pair carries BOTH halves (the half that was
481
+ # absent or nil becomes its empty value). Dropping members one at a time would strand a non-empty sibling,
482
+ # and passing a half would make {#with} raise. Entries outside {SIBLINGS} pass through untouched.
483
+ def self.compact_pairs(tables)
484
+ tables.each_with_object({}) do |(name, table), out|
485
+ partner = PARTNERS[name]
486
+ next out[name] = table if partner.nil?
487
+ next if out.key?(name)
488
+
489
+ other = tables[partner]
490
+ next if blank_table?(table) && blank_table?(other)
491
+
492
+ out[name] = table.nil? ? empty_value(name) : table
493
+ out[partner] = other.nil? ? empty_value(partner) : other
494
+ end
495
+ end
496
+
497
+ # Whether `name` is a member or sibling of {SIBLINGS}.
498
+ def self.paired?(name)
499
+ PARTNERS.key?(name)
500
+ end
501
+
502
+ def self.blank_table?(table)
503
+ table.nil? || table.empty?
504
+ end
505
+ private_class_method :blank_table?
506
+
507
+ # The six siblings as `{sibling name => table}`, for a caller that rebuilds the paired members and carries
508
+ # their siblings over unchanged.
509
+ def sibling_tables
510
+ SIBLINGS.each_value.to_h { |sibling| [sibling, public_send(sibling)] }
511
+ end
512
+
513
+ # ADR-119 WD1/WD2 — whether `class_name` answers `method_name` on `kind` only through a `possible` fact
514
+ # (`possible_discovered_methods`, empty until a producer admits one). Internal to the candidate-set read.
515
+ def possible_method?(class_name, method_name, kind)
516
+ recorded = possible_discovered_methods[class_name.to_s]&.[](method_name.to_sym)
517
+ recorded == kind || (!recorded.nil? && recorded == METHOD_KIND_BOTH)
518
+ end
519
+
520
+ # ADR-119 WD1 — whether `path` (`[class, name]`, or `[class, [kind, name]]` for an envelope) is a contested
521
+ # entry of `member`, one of the single-valued members {SIBLINGS} pairs. Internal to the candidate-set read.
522
+ def contested?(member, path)
523
+ public_send(SIBLINGS.fetch(member)).include?(path)
524
+ end
525
+
526
+ # Data's `with`, except that a paired member and its sibling are accepted only TOGETHER (ADR-119 WD1): a
527
+ # caller that rebuilt one without the other would leave the sibling describing the old table, so it
528
+ # raises instead. A call with no change returns the receiver.
529
+ def with(**changes)
530
+ return self if changes.empty?
531
+
532
+ changes.each_key do |name|
533
+ partner = PARTNERS[name]
534
+ next if partner.nil? || changes.key?(partner)
535
+
536
+ raise ArgumentError, "DiscoveryIndex#with: #{name} and #{partner} change together; #{partner} is missing"
537
+ end
538
+ super
539
+ end
285
540
  end
286
541
  end
287
542
  end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Scope
5
+ class ResolutionChain
6
+ # ADR-119 WD2 — per-name relevance, the narrowing of an unsettled chain (#1591). A {Mark} says a node's mixin
7
+ # edges have no known order; the chain still answers a question about ONE method name when the module the
8
+ # mark names cannot answer that name, because with no fork every entry reached the chain by a single route,
9
+ # so a module of unknown presence or position adds or removes only its own closure's entries and moves no
10
+ # other entry's first occurrence. A closure that cannot answer the name therefore cannot move its first
11
+ # definer. {ResolutionChain#settle} asks it under `unknown_for:`; nothing else does.
12
+ #
13
+ # - A named entry `Q` (not `"*"`) is discharged for `n` when every entry of `Q`'s own instance chain (its
14
+ # closure) is clean: (i) a project entry, or an external one RBS knows whose declaration lacks `n`
15
+ # (the test `SourceArity#external_mixin_lacks_method?` applies); (ii) no project entry carries the dynamic
16
+ # mark, lists `"*"` on the instance side or the mark's, or records `method_missing`; (iii) no project
17
+ # entry records `n` in `discovered_methods` (either kind) or in `discovered_method_visibilities`.
18
+ # - A multi-file mark names no entry: it is discharged when at most one of the node's edges' closures is
19
+ # not clean. A mark that is both listed and multi-file must pass both rules.
20
+ # - An ambiguous spelling (`:arity` only: several project modules a compact-header collision left as the
21
+ # spelling's meaning, #986) is discharged when every one of them is clean; any candidate that fails fails it.
22
+ # - `"*"` and a truncated closure are never discharged.
23
+ #
24
+ # The verdict and the edges it read are memoised per `[mark, name]` in the flavor's bucket and the edges are
25
+ # replayed on every call while a dependency recording is active (ADR-46): per tested project entry its class
26
+ # edge and the negative method edge on `Owner#name`, per tested external the negative class edge on the last
27
+ # segment of its spelling, so a new file declaring the module re-checks the consumer.
28
+ module Relevance
29
+ WILDCARD = "*"
30
+
31
+ # The singleton hooks Ruby calls while a module enters (or is asked to enter) another's ancestry. A module
32
+ # that defines one can add methods to the includer, extender or prepender with nothing the tables record
33
+ # on the module (`def self.included(base) = base.attr_reader(:foo)`), so no closure holding one can be
34
+ # said not to answer a name.
35
+ HOOKS = %i[included extended prepended inherited append_features extend_object prepend_features].freeze
36
+
37
+ module_function
38
+
39
+ def discharged?(scope, chain, mark, name)
40
+ name = name.to_sym
41
+ memo = ResolutionChain.relevance_memo(scope, chain.flavor)
42
+ verdict, edges = memo[[mark, name]] ||= compute(scope, chain.flavor, mark, name)
43
+ replay(scope, edges) if Analysis::DependencyRecorder.active?
44
+ verdict
45
+ end
46
+
47
+ def compute(scope, flavor, mark, name)
48
+ context = Context.new(scope, flavor, mark, name)
49
+ verdict = !mark.listed.include?(WILDCARD) && listed_discharged?(context) && multi_file_discharged?(context)
50
+ [verdict, context.edges.uniq.freeze].freeze
51
+ end
52
+
53
+ def listed_discharged?(context)
54
+ mark = context.mark
55
+ mark.listed.all? { |raw| context.closure_clean?(raw) }
56
+ end
57
+
58
+ def multi_file_discharged?(context)
59
+ mark = context.mark
60
+ return true unless mark.multi_file
61
+
62
+ edges = context.edge_names
63
+ edges.count { |raw| !context.closure_clean?(raw) } <= 1
64
+ end
65
+
66
+ def replay(scope, edges)
67
+ edges.each do |kind, value|
68
+ case kind
69
+ when :class then ResolutionChain.record_class(scope, value)
70
+ when :method then Analysis::DependencyRecorder.read_missing(:method, value)
71
+ when :external then Analysis::DependencyRecorder.read_missing(:class, value)
72
+ end
73
+ end
74
+ end
75
+
76
+ # True only for an ancestor the project does not declare that RBS knows and whose declaration lacks `name`
77
+ # (`SourceArity#external_mixin_lacks_method?`); an unknown one, or one that has it, may answer.
78
+ def external_lacks?(scope, candidates, name)
79
+ known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
80
+ return false if known.nil?
81
+
82
+ Rigor::Reflection.instance_method_definition(known, name, scope: scope).nil?
83
+ rescue StandardError
84
+ false
85
+ end
86
+
87
+ # One verdict's working state: the scope, the resolver, and the edges the tests read.
88
+ class Context
89
+ attr_reader :mark, :edges
90
+
91
+ def initialize(scope, flavor, mark, name)
92
+ @scope = scope
93
+ @flavor = flavor
94
+ @mark = mark
95
+ @name = name
96
+ @resolver = ResolutionChain.resolver_for(scope, flavor)
97
+ @edges = []
98
+ end
99
+
100
+ # The raw names of the node's own edges on the side the mark covers.
101
+ def edge_names
102
+ discovery = @scope.discovery
103
+ table = @mark.kind == :extend ? discovery.discovered_extends : discovery.discovered_includes
104
+ table[@mark.node] || EMPTY_NAMES
105
+ end
106
+
107
+ # Whether the closure of the module `raw` names from the marked node cannot answer the name.
108
+ def closure_clean?(raw)
109
+ resolved = @resolver.resolve(@mark.node, raw)
110
+ case resolved
111
+ when String then project_closure_clean?(resolved)
112
+ when Array then resolved.all? { |name| project_closure_clean?(name) }
113
+ else external_clean?(@resolver.candidates(@mark.node, raw), raw)
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def project_closure_clean?(module_name)
120
+ chain = ResolutionChain.for(@scope, module_name, :instance, @flavor)
121
+ return false if chain.truncated?
122
+
123
+ chain.entries.all? do |entry|
124
+ entry.external? ? external_clean?(entry.candidates, entry.raw) : project_clean?(entry)
125
+ end
126
+ end
127
+
128
+ def project_clean?(entry)
129
+ owner = entry.name
130
+ @edges << [:class, owner] << [:method, "#{owner}##{@name}"]
131
+ return false if Scope::DiscoveryIndex.rewritten_surface?(@scope.parameter_envelopes_of(owner))
132
+ return false if wildcard_listed?(owner)
133
+ return false if @scope.discovered_method?(owner, :method_missing, :instance)
134
+ return false if HOOKS.any? { |hook| @scope.discovered_method?(owner, hook, :singleton) }
135
+
136
+ !records_name?(owner)
137
+ end
138
+
139
+ def wildcard_listed?(owner)
140
+ sides = @scope.discovery.unpositioned_mixins[owner]
141
+ return false if sides.nil?
142
+
143
+ [:include, @mark.kind].uniq.any? { |side| sides[side]&.include?(WILDCARD) }
144
+ end
145
+
146
+ def records_name?(owner)
147
+ @scope.discovered_method?(owner, @name, :instance) || @scope.discovered_method?(owner, @name, :singleton) ||
148
+ !@scope.discovered_method_visibility(owner, @name).nil?
149
+ end
150
+
151
+ # An ancestor the project does not declare: only a module RBS knows, whose declaration lacks the name,
152
+ # is evidence it does not define it (`SourceArity#external_mixin_lacks_method?`).
153
+ def external_clean?(candidates, raw)
154
+ @edges << [:external, raw.to_s.split("::").last]
155
+ Relevance.external_lacks?(@scope, candidates, @name)
156
+ end
157
+ end
158
+ private_constant :Context
159
+
160
+ EMPTY_NAMES = [].freeze
161
+ private_constant :EMPTY_NAMES
162
+ end
163
+ end
164
+ end
165
+ end