rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ # Not required on its own: `rigor/inference/declaration_walk` loads it once the walk exists, and
6
+ # {Reflection.class_declarations_in} loads that entry point on first use. Keeping the walk out of `Reflection`'s own
7
+ # load keeps the engine-free paths engine-free (ADR-104).
8
+ module Rigor
9
+ module Reflection
10
+ # The walk's collector behind {.class_declarations_in}; the plugin never sees it.
11
+ class DeclarationCollector
12
+ include Inference::DeclarationWalk::Collector
13
+
14
+ EMPTY_NESTING = [].freeze
15
+
16
+ attr_reader :classes
17
+
18
+ def initialize
19
+ @classes = []
20
+ end
21
+
22
+ def on_declaration(node, context, body_context)
23
+ if node.is_a?(Prism::ClassNode)
24
+ name = body_context.prefix.join("::")
25
+ @classes << ClassDeclaration.new(name: name, nesting: frozen_nesting(context), node: node) unless name.empty?
26
+ end
27
+ Inference::DeclarationWalk::DESCEND
28
+ end
29
+
30
+ private
31
+
32
+ # The walk's chain, frozen: the root's `[]` is one array shared by every top-level declaration.
33
+ def frozen_nesting(context)
34
+ nesting = context.nesting || EMPTY_NESTING
35
+ nesting.frozen? ? nesting : nesting.dup.freeze
36
+ end
37
+ end
38
+
39
+ # {DeclarationCollector} that also forwards every call. A subclass, because the walk dispatches only the events a
40
+ # collector's class overrides, so a classes-only run pays for no call event.
41
+ class DeclarationCallCollector < DeclarationCollector
42
+ def initialize(on_call)
43
+ super()
44
+ @on_call = on_call
45
+ end
46
+
47
+ def on_call(node, context)
48
+ owner = context.prefix.join("::")
49
+ @on_call.call(DeclaredCall.new(node: node, owner: owner.empty? ? nil : owner))
50
+ Inference::DeclarationWalk::DESCEND
51
+ end
52
+ end
53
+ private_constant :DeclarationCollector, :DeclarationCallCollector
54
+ end
55
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ # {Reflection}'s per-file declaration walk for plugins (ADR-128, #2708): {.class_declarations_in} answers which
7
+ # classes a parsed file declares under their `Module.nesting`-correct names, and hands each call the walk visits
8
+ # to a plugin together with the class or module its position reaches. It replaces a plugin's own
9
+ # `DeclarationWalk::Collector`, so the walk's context, its control constants and its collector protocol stay
10
+ # internal.
11
+ module Reflection
12
+ # One `class` header of a file, as {.class_declarations_in} reports it.
13
+ #
14
+ # - `name` — the qualified name the header's body is keyed under (`"Admin::User"` for `class User` in
15
+ # `module Admin`, and for `class Admin::User`); never empty.
16
+ # - `nesting` — `Module.nesting` where the header is written, innermost first, not counting the class itself
17
+ # (`["Admin"]` for `class User` in `module Admin`; `[]` at the top level). Frozen.
18
+ # - `node` — the `Prism::ClassNode`, for its `superclass` and `body`.
19
+ ClassDeclaration = Data.define(:name, :nesting, :node)
20
+
21
+ # One call the walk visited, as {.class_declarations_in} yields it.
22
+ #
23
+ # - `node` — the `Prism::CallNode`.
24
+ # - `owner` — the qualified name of the innermost `class` / `module` the call is lexically inside, or nil where
25
+ # none is (the top level, or below a bare header in a `class << self` body).
26
+ DeclaredCall = Data.define(:node, :owner)
27
+
28
+ module_function
29
+
30
+ # ADR-128, #2708 — the classes a parsed file declares, in the order their headers are written. A class
31
+ # reopened in the file appears once per header. A `module`, a `class << self` and a class no name reaches
32
+ # (`Class.new { … }`, a header below `class << self`) are not reported.
33
+ #
34
+ # With a block, the walk also yields a {DeclaredCall} for the calls the engine's declaration walk visits, in
35
+ # source order, outer calls before the calls inside them. Those are the calls in statements and in the bodies of
36
+ # `class` / `module` / `class << self` / blocks, at any depth. The owner is lexical, not `self`: a call in
37
+ # `class << self` or in a `Class.new { … }` block reports the class around it.
38
+ #
39
+ # The block does not see every call node of the file. The walk does not enter, so the block never sees:
40
+ #
41
+ # - a class or module header's own expressions: the superclass (`class Foo < Struct.new(:a)`) and the constant
42
+ # path (`class foo::Bar`);
43
+ # - the factory call a constant write binds (`K = Class.new { … }`), whose receiver, arguments and block body the
44
+ # walk does enter;
45
+ # - the tail of such a write (`K = Class.new { … }.freeze` yields what is inside the block, not `freeze`), and
46
+ # the constant-path parent of a write target (`P` in `P::K = …`).
47
+ #
48
+ # `root` is a `Prism::Node`, typically `Prism.parse(source).value`.
49
+ def class_declarations_in(root, &on_call)
50
+ # Loaded on first use, not with `Reflection`: the walk heads the analysis engine, which the engine-free
51
+ # paths (a warm `rigor effects`) load `Reflection` without. The walk's entry point loads the collectors.
52
+ require_relative "../inference/declaration_walk"
53
+ collector = on_call ? DeclarationCallCollector.new(on_call) : DeclarationCollector.new
54
+ Inference::DeclarationWalk.run(root, [collector], Inference::DeclarationWalk::Context.root(nesting: []))
55
+ collector.classes
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ # {Reflection}'s ancestor facade for plugins (ADR-128 WD1), split out for the reader rather than for the
5
+ # loader: {.project_method_defined} turns the two `Scope` ancestor walks into one `Trinary` answer. Reopening
6
+ # the module keeps it behind the facade its callers already read.
7
+ module Reflection
8
+ module_function
9
+
10
+ PROJECT_METHOD_KINDS = %i[instance singleton].freeze
11
+ PROJECT_METHOD_SOURCES = %i[def discovered either].freeze
12
+ private_constant :PROJECT_METHOD_KINDS, :PROJECT_METHOD_SOURCES
13
+
14
+ # ADR-128 WD1 — does the project define `method_name` on `class_name` or on an ancestor the project itself
15
+ # declares? Answers a {Trinary}: `yes` where the walk found a definer, `no` where it found none and its
16
+ # `ResolutionChain` was not truncated, and `maybe` where the chain was truncated at its budget before it found one
17
+ # (ADR-41 WD4), so a definer past the cut cannot be ruled out. It replaces a plugin's direct call to the
18
+ # two `Scope` walks, whose budget answers differ and are not a promise to plugin authors.
19
+ #
20
+ # - `kind:` is `:instance` or `:singleton`; each walks its own chain.
21
+ # - `source:` names the walk outright, with that walk's order and settling:
22
+ # - `:def` — the first project `def` (`Scope#user_def_through_ancestors`, or `#singleton_def_through_ancestors`
23
+ # for `kind: :singleton`). It does not count `attr_*` or `define_method`.
24
+ # - `:discovered` — the discovery table (`Scope#discovered_method_through_ancestors?`), which also counts
25
+ # `attr_*` and `define_method`.
26
+ # - `:either` — their union as a three-valued OR: `yes` wins, then `maybe`, then `no`.
27
+ #
28
+ # A site that tested the `:def` walk's old answer tests `.yes?` (that walk read a cut as absent); a site that
29
+ # tested the `:discovered` walk's tests `!no?` (that walk read a cut as present).
30
+ def project_method_defined(class_name, method_name, kind:, source:, scope:)
31
+ raise ArgumentError, "unknown kind #{kind.inspect}" unless PROJECT_METHOD_KINDS.include?(kind)
32
+ raise ArgumentError, "unknown source #{source.inspect}" unless PROJECT_METHOD_SOURCES.include?(source)
33
+
34
+ case source
35
+ when :def then project_def_defined(class_name, method_name, kind, scope)
36
+ when :discovered then project_discovered_defined(class_name, method_name, kind, scope)
37
+ else project_either_defined(class_name, method_name, kind, scope)
38
+ end
39
+ end
40
+
41
+ # Does RBS declare `method_name` on `class_name` or on an ancestor the project's own ancestry reaches? A
42
+ # project class (`class ArticlesController < ActionController::Base`) is rarely in RBS itself; its RBS-known
43
+ # ancestors are the EXTERNAL entries of its `ResolutionChain`, which the RBS-only reads
44
+ # ({.instance_method_definition}) cannot reach from the project class's name. This walks the chain and asks RBS
45
+ # of each entry (every candidate spelling of an external one), on the side the entry sits.
46
+ #
47
+ # Answers a {Trinary}: `yes` when an entry's RBS declares the method, `no` when none does and the chain was
48
+ # not truncated, `maybe` when the chain was cut at its budget (ADR-41 WD4) before one did. `kind:` is
49
+ # `:instance` or `:singleton`; a `nil` `class_name` answers `no`.
50
+ def rbs_method_defined(class_name, method_name, kind:, scope:)
51
+ chain = ancestry_chain(class_name, kind, scope)
52
+ return Trinary.no if chain.nil?
53
+
54
+ found = chain.search(scope) do |entry|
55
+ chain_entry_names(entry).any? { |name| !rbs_method_on_side(name, method_name, entry.side, scope).nil? }
56
+ end
57
+ chain_answer(found, chain)
58
+ end
59
+
60
+ # Does the project ancestry of `class_name` reach any of `ancestor_names` (qualified names, a leading `::`
61
+ # ignored)? An external ancestor counts by any candidate spelling, so a superclass the project names but
62
+ # does not declare (`ActionController::Base`, with no RBS loaded) is found. Same `Trinary` and `kind:` as
63
+ # {.rbs_method_defined}.
64
+ def ancestry_reaches(class_name, ancestor_names, kind:, scope:)
65
+ chain = ancestry_chain(class_name, kind, scope)
66
+ return Trinary.no if chain.nil?
67
+
68
+ wanted = ancestor_names.map { |name| name.to_s.delete_prefix("::") }
69
+ found = chain.search(scope) do |entry|
70
+ chain_entry_names(entry).any? { |name| wanted.include?(name.to_s.delete_prefix("::")) }
71
+ end
72
+ chain_answer(found, chain)
73
+ end
74
+
75
+ def ancestry_chain(class_name, kind, scope)
76
+ raise ArgumentError, "unknown kind #{kind.inspect}" unless PROJECT_METHOD_KINDS.include?(kind)
77
+ return nil if class_name.nil?
78
+
79
+ Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods)
80
+ end
81
+ private_class_method :ancestry_chain
82
+
83
+ def chain_answer(found, chain)
84
+ return Trinary.yes if found
85
+
86
+ chain.truncated? ? Trinary.maybe : Trinary.no
87
+ end
88
+ private_class_method :chain_answer
89
+
90
+ def chain_entry_names(entry)
91
+ entry.external? ? entry.candidates : [entry.name]
92
+ end
93
+ private_class_method :chain_entry_names
94
+
95
+ def rbs_method_on_side(class_name, method_name, side, scope)
96
+ if side == :singleton
97
+ singleton_method_definition(class_name, method_name, scope: scope)
98
+ else
99
+ instance_method_definition(class_name, method_name, scope: scope)
100
+ end
101
+ end
102
+ private_class_method :rbs_method_on_side
103
+
104
+ # `yes` wins, then `maybe`, then `no`. The early return is a short-circuit and carries no answer of its own:
105
+ # `yes.or(x)` is `yes`, so removing it changes only which walks run, and so which dependencies they read.
106
+ def project_either_defined(class_name, method_name, kind, scope)
107
+ by_def = project_def_defined(class_name, method_name, kind, scope)
108
+ return by_def if by_def.yes?
109
+
110
+ by_def.or(project_discovered_defined(class_name, method_name, kind, scope))
111
+ end
112
+ private_class_method :project_either_defined
113
+
114
+ # The def walk answers `[node, owner]` or `[nil, nil]`, and `[nil, nil]` both when the chain holds no definer
115
+ # and when it was cut; the chain (memoised, so this is the one the walk read) tells the two apart.
116
+ def project_def_defined(class_name, method_name, kind, scope)
117
+ found = if kind == :singleton
118
+ scope.singleton_def_through_ancestors(class_name, method_name)
119
+ else
120
+ scope.user_def_through_ancestors(class_name, method_name)
121
+ end
122
+ return Trinary.yes if found.first
123
+
124
+ project_chain_truncated?(class_name, kind, scope) ? Trinary.maybe : Trinary.no
125
+ end
126
+ private_class_method :project_def_defined
127
+
128
+ # The discovery walk answers `true` both for a definer and for a cut chain, so a `true` on a cut chain is
129
+ # settled by searching the chain's entries for a definer, as the walk does.
130
+ #
131
+ # Issue #2968 — where the walk finds none, a class on the chain whose side is open (an open-names mark: a
132
+ # splatted `Struct.new(*FIELDS)`, `attr_accessor(*NAMES)`, a `define_method` under a computed name) answers
133
+ # `maybe`: it defines methods under names no table holds. Issue #2994 — so does an instance side whose chain mixes
134
+ # in a module no table names (`include(*MODS)`, `ResolutionChain#unnamed_mixin?`); the #618 top-level-`def` veto
135
+ # (`ExpressionTyper#instance_self_answers?`) reads the same mark, so a plugin that stands down here does not hand
136
+ # the call to a top-level `def`.
137
+ def project_discovered_defined(class_name, method_name, kind, scope)
138
+ unless scope.discovered_method_through_ancestors?(class_name, method_name, kind)
139
+ return open_side?(class_name, kind, scope) ? Trinary.maybe : Trinary.no
140
+ end
141
+ # A shortcut, not a decision: on an untruncated chain the search below finds the same definer.
142
+ return Trinary.yes unless project_chain_truncated?(class_name, kind, scope)
143
+
144
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods)
145
+ found = chain.search(scope) do |entry|
146
+ # A guard, not a decision: it mirrors `Scope#discovered_method_through_ancestors?`, whose search skips
147
+ # an external entry (no project name to look up). `discovered_method?(nil, ...)` is false anyway.
148
+ next false if entry.external?
149
+
150
+ scope.discovered_method?(entry.name, method_name, discovered_entry_kind(entry, kind))
151
+ end
152
+ found ? Trinary.yes : Trinary.maybe
153
+ end
154
+ private_class_method :project_discovered_defined
155
+
156
+ def open_side?(class_name, kind, scope)
157
+ return true if Scope::DiscoveryIndex.open_names_on_chain?(scope, class_name, kind)
158
+
159
+ kind == :instance && Scope::DiscoveryIndex.unnamed_mixin_on_chain?(scope, class_name)
160
+ end
161
+ private_class_method :open_side?
162
+
163
+ # A singleton-side entry reads the singleton table; a module on the singleton chain answers with its
164
+ # INSTANCE methods; on the instance chain the caller's kind stands (`Scope#kind_for`).
165
+ def discovered_entry_kind(entry, kind)
166
+ return :singleton if entry.side == :singleton
167
+
168
+ kind == :singleton ? :instance : kind
169
+ end
170
+ private_class_method :discovered_entry_kind
171
+
172
+ def project_chain_truncated?(class_name, kind, scope)
173
+ Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods).truncated?
174
+ end
175
+ private_class_method :project_chain_truncated?
176
+ end
177
+ end
@@ -511,3 +511,5 @@ end
511
511
  # private candidate lookups, so the facade has to exist first.
512
512
  require_relative "reflection/constant_ancestors"
513
513
  require_relative "reflection/constant_path"
514
+ require_relative "reflection/project_method"
515
+ require_relative "reflection/declarations"
@@ -38,13 +38,30 @@ module Rigor
38
38
  # it always did. An absolute one is split into its own directory and a literal basename: `File.join(".", "/abs")`
39
39
  # reads `./abs`, so an absolute path joined under `"."` matched nothing.
40
40
  #
41
+ # A `~`-prefixed path is expanded before the split (#2586): the readers apply `File.expand_path`, so `~/x.yml`
42
+ # reads `$HOME/x.yml` and the watch must be split from that spelling — `[".", "~/x.yml"]` would watch
43
+ # `./~/x.yml` and never fire. Only the `~` form is expanded; expanding every path would change how relative
44
+ # entries are watched.
45
+ #
41
46
  # @param path — a file path, taken literally
42
47
  # @return `[root, pattern]`: `"."` and the literal path when relative, else the directory and the literal basename
43
48
  def split_file(path)
44
- path = path.to_s
49
+ path = expand_home(path.to_s)
45
50
  return [".", literal(path)] unless File.absolute_path?(path)
46
51
 
47
52
  [File.dirname(path), literal(File.basename(path))]
48
53
  end
54
+
55
+ # `File.expand_path`'s `~` handling, kept literal on failure: `~nouser/x` (or an unset HOME) names no home
56
+ # directory and raises `ArgumentError`; the readers fail on the same path, so the unexpanded spelling —
57
+ # watched under `"."` like any other relative entry — is the consistent answer.
58
+ def expand_home(path)
59
+ return path unless path.start_with?("~")
60
+
61
+ File.expand_path(path)
62
+ rescue ArgumentError
63
+ path
64
+ end
65
+ private_class_method :expand_home
49
66
  end
50
67
  end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "English"
4
+ require_relative "typing_state"
5
+ require_relative "memo_audit"
6
+
3
7
  module Rigor
4
- # Issues #2412 and #2399 — the registry of per-thread memo slots whose contents belong to one analysis run. An
5
- # owner registers its `Thread.current` key where it names it (`KEY = RunScopedSlots.register(:key)`), and each
6
- # entry point that runs an analysis calls {.release} in its `ensure`, so a finished run, or one that raised,
7
- # leaves none of its scopes, discovery tables or environment behind on the thread.
8
+ # Issues #2412 and #2399 — the run-end hook for per-thread state whose contents belong to one analysis run. An
9
+ # owner declares such a slot in {TypingState} with `run_scoped: true` where it names it, and each entry point that
10
+ # runs an analysis calls {.release} in its `ensure`, so a finished run, or one that raised, leaves none of its
11
+ # scopes, discovery tables or environment behind on the thread.
8
12
  #
9
13
  # A slot that holds its run until the next run overwrites it retains one run per thread. A slot whose value
10
14
  # reaches its own key retains every run, as `LoopValueRecord`'s `ObjectSpace::WeakKeyMap` did: the map holds
@@ -12,26 +16,22 @@ module Rigor
12
16
  # bounds both. Inside a run nothing changes: every memo keeps its key and its hit rate, and a slot read outside
13
17
  # a run (a language-server hover on the main thread) refills on its first miss.
14
18
  #
15
- # Only memo slots belong here. A slot a `with_*` frame installs and restores around a block is already empty
19
+ # Only Tables, Memos and Resources are run-scoped. A Frame a `with_*` block installs and restores is already empty
16
20
  # when the frame returns, and clearing it from inside a frame would break the frame.
17
21
  #
18
- # The registry is a module-level array filled at require time, so it is for the main Ractor only.
22
+ # ADR-129 — run-scoped state is a `run_scoped: true` {TypingState} slot, and nothing else registers: this module
23
+ # keeps no key list of its own.
19
24
  module RunScopedSlots
20
- @keys = []
21
-
22
- # Adds `key` to the registry and returns it, so the owner's constant keeps its value.
23
- def self.register(key)
24
- @keys << key unless @keys.include?(key)
25
- key
26
- end
27
-
28
- # Every registered key, in registration order.
29
- def self.keys = @keys.dup.freeze
30
-
31
- # Empties every registered slot on the calling thread.
25
+ # Empties every run-scoped {TypingState} slot on the calling thread.
26
+ #
27
+ # ADR-129 WD5 (#2785) — with the memo audit on, a mismatch no file drained (one raised by typing outside the
28
+ # per-file loop: return summaries, return re-evaluation, sig-gen) would be released here unreported, so it is
29
+ # raised after the release, unless an exception is already leaving the run.
32
30
  def self.release
33
- thread = Thread.current
34
- @keys.each { |key| thread[key] = nil }
31
+ undrained = Inference::MemoAudit.enabled? ? Inference::MemoAudit.drain : nil
32
+ TypingState.release_run_scoped
33
+ raise undrained.first if undrained && !undrained.empty? && $ERROR_INFO.nil?
34
+
35
35
  nil
36
36
  end
37
37
  end
@@ -60,7 +60,7 @@ module Rigor
60
60
  :contested_discovered_parameter_envelopes
61
61
  )
62
62
 
63
- class DiscoveryIndex
63
+ class DiscoveryIndex # rubocop:disable Metrics/ClassLength -- the envelope kinds and marks are one constant each
64
64
  # Issue #2330 — identity as a hash key. `Data` hashes and compares structurally, which over these project-sized
65
65
  # tables is both costly and wrong for a per-thread memo keyed by "this very index" (an `ObjectSpace::WeakKeyMap`
66
66
  # holds the memo while the index lives and drops it with it). `==` stays structural.
@@ -247,6 +247,100 @@ module Rigor
247
247
  ENVELOPE_DYNAMIC_MARK = :"<dynamic>"
248
248
  ENVELOPE_OBJECT_EXTENDED_MARK = :"<object-extended>"
249
249
  ENVELOPE_REFINED_MARK = :"<refined>"
250
+ # Issue #2793 — a class-body macro defines methods under names its syntax does not spell
251
+ # (`def_delegators :@x, *NAMES`), so no table can enumerate the class's members: `call.undefined-method`
252
+ # treats the class, and every project class that inherits or mixes it in, as open, as it does a source
253
+ # `method_missing`. Not a {.rewritten_surface?} mark: the macro adds names and replaces no method body
254
+ # Rigor reads.
255
+ ENVELOPE_OPEN_NAMES_MARK = :"<open-names>"
256
+ # Issue #2793 — the sided twins of {ENVELOPE_OPEN_NAMES_MARK}: a `define_method` whose name is not a literal
257
+ # (`%i[a b].each { |m| define_method(m) { } }`) where `self` is the class defines instance methods no table can
258
+ # name, and one in a `class << self` body singleton methods. `call.undefined-method` reads the instance mark
259
+ # for an instance receiver and the singleton mark for the class object, each beside the sideless mark.
260
+ ENVELOPE_OPEN_INSTANCE_NAMES_MARK = :"<open-instance-names>"
261
+ ENVELOPE_OPEN_SINGLETON_NAMES_MARK = :"<open-singleton-names>"
262
+ # Issue #2901 — the two halves of a class macro that defines methods under computed names. A
263
+ # `[ENVELOPE_COMPUTED_DEFINER_KIND, side, name]` entry says the class or module's own method `name`, on `side`
264
+ # (`:singleton` or `:instance`, #2975), runs a `define_method` on its `self` whose name is not a literal
265
+ # (`def self.my_attr(*n) = n.each { |x| define_method(x) {} }`, or a module's instance method an `extend` makes a
266
+ # class macro). A
267
+ # `[ENVELOPE_BODY_CALL_KIND, name]` entry says the class body calls `name` on `self` (`my_attr :foo`).
268
+ # `call.undefined-method` reads a class as instance-open when a body call of it resolves, on its singleton
269
+ # chain, to a computed definer. Neither entry names a member, so neither declares a surface.
270
+ ENVELOPE_COMPUTED_DEFINER_KIND = :computed_definer
271
+ ENVELOPE_BODY_CALL_KIND = :body_call
272
+ # Issue #2975 — `[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods"]` says the module's `self.included(base)` (or
273
+ # `self.prepended(base)`) hook runs `base.extend(ClassMethods)`, the constant as written: an includer's
274
+ # singleton chain then reaches `ClassMethods`, whose computed definers its body calls may resolve to.
275
+ #
276
+ # Issue #2986 — an `extend ActiveSupport::Concern` module records `[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods",
277
+ # :const_get]`: `Concern#append_features` extends its includer with `const_get(:ClassMethods)`, which searches the
278
+ # module, its ancestors and `Object`, not the lexical scope. A `[ENVELOPE_HOOK_BODY_CALL_KIND, hook, name]` entry
279
+ # says its `included do … end` (`hook` `:included`) or `prepended do … end` (`:prepended`) block calls `name` on
280
+ # `self`, which Ruby runs in the class body of a class that includes (or prepends) the module directly. It is
281
+ # filed only on the module whose body the walk is in, so it is not one of `Incremental`'s diffed class-mark kinds.
282
+ #
283
+ # Issue #1592 — a hook's own entry names the hook as its third element (`[ENVELOPE_HOOK_EXTEND_KIND, "M",
284
+ # :included]`, `:prepended` for `self.prepended` or a `prepended do … end` block), and an `extend` on `self` in a
285
+ # concern's `included do … end` / `prepended do … end` block records one too. The includer's singleton resolution
286
+ # chain inserts each such module for the edge its hook runs on. A `[ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]` entry
287
+ # says that hook may extend its includer with a module no entry names, or one it may not extend at all (an
288
+ # `extend` that is not a direct statement of the hook body, or whose argument is not a constant).
289
+ ENVELOPE_HOOK_EXTEND_KIND = :hook_extend
290
+ ENVELOPE_HOOK_EXTEND_OPEN_KIND = :hook_extend_open
291
+ # Issue #1592 — `[ENVELOPE_HOOK_DEF_KIND, hook, source]`: one definition of the module's `self.included` /
292
+ # `self.prepended` hook, by its source text. Two entries for one hook mean the hook is defined twice, and which
293
+ # definition Ruby keeps is load order.
294
+ ENVELOPE_HOOK_DEF_KIND = :hook_def
295
+ ENVELOPE_HOOK_BODY_CALL_KIND = :hook_body_call
296
+ # Issue #2992 — `[ENVELOPE_CONCERN_DEPENDENCY_KIND, "Dep"]`: the module's body runs `include Dep` (or `prepend
297
+ # Dep`) after its own `extend ActiveSupport::Concern`, so a concern `Dep` is a dependency of the module, which
298
+ # ActiveSupport includes into the module's final includer instead. Filed only on the module whose body the walk
299
+ # is in, as the hook-body-call entry is.
300
+ ENVELOPE_CONCERN_DEPENDENCY_KIND = :concern_dependency
301
+ # Issue #3005 — the hook a hook-extend, open or hook-body-call entry names when it comes from an `included do …
302
+ # end` / `prepended do … end` block in a body that does not run `extend ActiveSupport::Concern` itself. The
303
+ # block is ActiveSupport's only when some body of the module records the concern entry; a reader takes the
304
+ # gated hook as the plain one then, and as no hook otherwise ({ResolutionChain.effective_hook}).
305
+ CONCERN_BLOCK_HOOKS = { included: :concern_included, prepended: :concern_prepended }.freeze
306
+ # Issue #3005 — the last segment of the module a concern block's `def self.x`s are filed on (`Owner::<included>`),
307
+ # and the spelling its hook-extend entry names.
308
+ HOOK_BLOCK_SEGMENTS = %w[<included> <prepended>].freeze
309
+ # Issue #2979 — `[ENVELOPE_SAME_CLASS_KIND, "Struct::Named"]` on `K`'s bucket, and its mirror on
310
+ # `Struct::Named`'s, say that `K = Struct.new("Named", …)` binds both constants to one class object: the method
311
+ # chain of either name reads the other's tables at its own position ({.same_class_names}). It names no member.
312
+ ENVELOPE_SAME_CLASS_KIND = :same_class
313
+ # Issue #2998 — `[ENVELOPE_SINGLETON_PREPEND_KIND, "P"]` on `K`'s bucket says one of `K`'s `discovered_extends`
314
+ # entries spelt `"P"` is a prepend to `K`'s singleton class (`class << self; prepend P; end`,
315
+ # `singleton_class.prepend P`, `singleton_class.send(:prepend, P)` in `K`'s body), which sits ahead of the class
316
+ # object's own methods where an `extend` sits behind them. The singleton resolution chain reads it.
317
+ ENVELOPE_SINGLETON_PREPEND_KIND = :singleton_prepend
318
+ # Issue #3018 — `[ENVELOPE_OBJECT_SINGLETON_KIND, name]` says a definer written on the constant's object from
319
+ # outside its body defines `name` there: `def K.name`, a literal `K.define_singleton_method(:name)`, or a `def
320
+ # name` in a `K.singleton_class.class_eval` block. Each also files the opaque `[:singleton, name]` entry, which
321
+ # name-rewriting class-body macros (`memoize :name`) file too; this entry is the one the class-object path of
322
+ # `call.undefined-method` reads as evidence the singleton method exists.
323
+ ENVELOPE_OBJECT_SINGLETON_KIND = :object_singleton
324
+ MACRO_KINDS = [ENVELOPE_BODY_CALL_KIND, ENVELOPE_COMPUTED_DEFINER_KIND, ENVELOPE_HOOK_EXTEND_KIND,
325
+ ENVELOPE_HOOK_EXTEND_OPEN_KIND, ENVELOPE_HOOK_DEF_KIND, ENVELOPE_HOOK_BODY_CALL_KIND,
326
+ ENVELOPE_CONCERN_DEPENDENCY_KIND, ENVELOPE_SAME_CLASS_KIND,
327
+ ENVELOPE_SINGLETON_PREPEND_KIND].freeze
328
+ private_constant :MACRO_KINDS
329
+
330
+ NO_NAMES = [].freeze
331
+ private_constant :NO_NAMES
332
+
333
+ # Issue #2979 — the other names an envelope bucket says its class object is bound to
334
+ # ({ENVELOPE_SAME_CLASS_KIND}), in recording order; empty for nearly every bucket, and allocates nothing then.
335
+ def self.same_class_names(bucket)
336
+ return NO_NAMES if bucket.nil?
337
+
338
+ names = nil
339
+ bucket.each_key do |key|
340
+ (names ||= []) << key.last if key.is_a?(Array) && key.first == ENVELOPE_SAME_CLASS_KIND
341
+ end
342
+ names || NO_NAMES
343
+ end
250
344
 
251
345
  # Issue #1820 — whether an envelope bucket says the project declares, or rewrites, the name's method table: a
252
346
  # per-method entry, the {ENVELOPE_MODULE_MARK} a `module` declaration records, or the {ENVELOPE_DYNAMIC_MARK}
@@ -258,7 +352,60 @@ module Rigor
258
352
  private_constant :DECLARING_MARKS
259
353
 
260
354
  def self.declares_surface?(bucket)
261
- !bucket.nil? && bucket.any? { |key, _| !key.is_a?(Symbol) || DECLARING_MARKS.include?(key) }
355
+ !bucket.nil? && bucket.any? { |key, _| declaring_key?(key) }
356
+ end
357
+
358
+ # Issue #2901 — a per-method entry declares a member; the two macro entries ({ENVELOPE_BODY_CALL_KIND},
359
+ # {ENVELOPE_COMPUTED_DEFINER_KIND}) do not.
360
+ def self.declaring_key?(key)
361
+ return DECLARING_MARKS.include?(key) if key.is_a?(Symbol)
362
+
363
+ !(key.is_a?(Array) && MACRO_KINDS.include?(key.first))
364
+ end
365
+
366
+ # Issue #2968 — the open-names marks of one side: {ENVELOPE_OPEN_NAMES_MARK} and the side's own twin.
367
+ OPEN_NAMES_MARKS = {
368
+ instance: [ENVELOPE_OPEN_NAMES_MARK, ENVELOPE_OPEN_INSTANCE_NAMES_MARK].freeze,
369
+ singleton: [ENVELOPE_OPEN_NAMES_MARK, ENVELOPE_OPEN_SINGLETON_NAMES_MARK].freeze
370
+ }.freeze
371
+ private_constant :OPEN_NAMES_MARKS
372
+
373
+ # Issue #2968 — whether an entry of `class_name`'s `kind` chain carries that side's open-names mark: a splatted
374
+ # `Struct.new(*FIELDS)`, `attr_accessor(*NAMES)`, a `define_method` under a computed name, so the side defines
375
+ # methods no table names. A project class with no `def` of its own is an external entry (#2094), so its candidate
376
+ # names are asked too. Read by `Reflection.project_method_defined` and by the implicit-self top-level-`def` veto
377
+ # (`ExpressionTyper#instance_self_answers?`), beside `call.undefined-method`'s own reader.
378
+ def self.open_names_on_chain?(scope, class_name, kind)
379
+ marks = OPEN_NAMES_MARKS.fetch(kind)
380
+ chain = ResolutionChain.for(scope, class_name.to_s, kind, :methods)
381
+ found = chain.search(scope) do |entry|
382
+ names = entry.external? ? entry.candidates : [entry.name]
383
+ names.any? do |name|
384
+ bucket = scope.parameter_envelopes_of(name)
385
+ marks.any? { |mark| bucket.key?(mark) }
386
+ end
387
+ end
388
+ !found.nil?
389
+ end
390
+
391
+ # Issue #2994 — whether `class_name`'s instance-side chain mixes in a module no table names
392
+ # (`ResolutionChain#unnamed_mixin?`: `include(*MODS)`, `include helper`, a hook's `base.include`). A project class
393
+ # with no `def` of its own is an external entry (#2094) whose ancestry the chain does not walk, so each of its
394
+ # candidate names that the project declares is asked on its own chain. Read beside {.open_names_on_chain?} by
395
+ # `Reflection.project_method_defined` and by the implicit-self top-level-`def` veto.
396
+ def self.unnamed_mixin_on_chain?(scope, class_name)
397
+ chain = ResolutionChain.for(scope, class_name.to_s, :instance, :methods)
398
+ return true if chain.unnamed_mixin?
399
+
400
+ found = chain.search(scope) do |entry|
401
+ next false unless entry.external?
402
+
403
+ entry.candidates.any? do |name|
404
+ scope.discovered_classes.key?(name) &&
405
+ ResolutionChain.for(scope, name, :instance, :methods).unnamed_mixin?
406
+ end
407
+ end
408
+ !found.nil?
262
409
  end
263
410
 
264
411
  # Whether an envelope bucket says the project rewrites the class's method table beyond its literal names: a
@@ -27,6 +27,8 @@ module Rigor
27
27
  # segment of its spelling, so a new file declaring the module re-checks the consumer.
28
28
  module Relevance
29
29
  WILDCARD = "*"
30
+ # Issue #2991 — listed beside {WILDCARD} when the side mixes in a module no name spells.
31
+ UNNAMED = "?"
30
32
 
31
33
  # The singleton hooks Ruby calls while a module enters (or is asked to enter) another's ancestry. A module
32
34
  # that defines one can add methods to the includer, extender or prepender with nothing the tables record
@@ -39,7 +41,18 @@ module Rigor
39
41
  def discharged?(scope, chain, mark, name)
40
42
  name = name.to_sym
41
43
  memo = ResolutionChain.relevance_memo(scope, chain.flavor)
42
- verdict, edges = memo[[mark, name]] ||= compute(scope, chain.flavor, mark, name)
44
+ key = [mark, name]
45
+ entry = memo[key]
46
+ verdict, edges =
47
+ if entry.nil?
48
+ memo[key] = Inference::MemoAudit.key_audit("resolution chain relevance") do
49
+ compute(scope, chain.flavor, mark, name)
50
+ end
51
+ else
52
+ Inference::MemoAudit.check("resolution chain relevance", entry, mark, name) do
53
+ Analysis::DependencyRecorder.withhold { compute(scope, chain.flavor, mark, name) }.first
54
+ end
55
+ end
43
56
  replay(scope, edges) if Analysis::DependencyRecorder.active?
44
57
  verdict
45
58
  end