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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../source/node_children"
4
+
3
5
  module Rigor
4
6
  module Analysis
5
7
  # Issues #665 / #674 / #683 / #686 / #696 — the ONE definition of "an internal failure made this run
@@ -104,12 +106,52 @@ module Rigor
104
106
  # into. Built here, not at either rescue site, so the two twins cannot drift (issue #665) — and so
105
107
  # the appended crash frame is derived identically on the sequential and pooled paths. Keeps the
106
108
  # {CHECK_RULE_MESSAGE_PREFIX} prefix every consumer matches on; the frame is a trailing hint.
107
- def check_rule_message(error)
109
+ def check_rule_message(error, root = nil)
110
+ return stack_overflow_message(error, root) if error.is_a?(SystemStackError)
111
+
108
112
  base = "#{CHECK_RULE_MESSAGE_PREFIX}: #{error.class}: #{error.message}"
109
113
  frame = crash_frame(error)
110
114
  frame ? "#{base} (#{frame})" : base
111
115
  end
112
116
 
117
+ # The crash-row message for a rescued error, for the per-file rescues of `Runner` and `WorkerSession`. Re-raises a
118
+ # `SystemStackError` raised from C (no backtrace): that one leaves no stack to recover on and a second would kill
119
+ # the process with a signal, so it keeps the run-level abort ({CLI::FATAL_ERRORS}, exit 70). `parse_result` may be
120
+ # nil when the error predates the parse.
121
+ def contained_message!(error, parse_result)
122
+ raise error if error.is_a?(SystemStackError) && error.backtrace.nil?
123
+
124
+ check_rule_message(error, parse_result&.value)
125
+ end
126
+
127
+ # #2455 — the AST depth above which a `SystemStackError` is read as the file's own nesting rather than a Rigor
128
+ # recursion defect. The recursive walkers overflow from about 700 nested `if`s (twice as many AST levels) and
129
+ # 1,300 nested array literals or chained calls, so a file past this bound is one they cannot be expected to walk.
130
+ DEEP_NESTING_BOUND = 500
131
+
132
+ DEEP_NESTING_DETAIL = "this file is nested too deeply to analyse (stack level too deep); " \
133
+ "split the deeply nested expression"
134
+ STACK_DEFECT_DETAIL = "stack level too deep; this is a defect in Rigor, please report it at " \
135
+ "https://github.com/rigortype/rigor/issues"
136
+
137
+ # No crash frame: the frame that overflowed is whichever the stack ran out in, which differs by mode and cache and
138
+ # would make the row unstable. `root` is nil when the file never parsed, which reads as the defect wording.
139
+ def stack_overflow_message(error, root)
140
+ deep = !root.nil? && Source::NodeChildren.nested_deeper_than?(root, DEEP_NESTING_BOUND)
141
+ "#{CHECK_RULE_MESSAGE_PREFIX}: #{error.class}: #{deep ? DEEP_NESTING_DETAIL : STACK_DEFECT_DETAIL}"
142
+ end
143
+
144
+ # ADR-129 WD5 (#2785) — `diagnostics` (one file's) with a crash row for each of `errors`, raised errors the
145
+ # typing path may have swallowed under a `rescue StandardError` (`MemoAudit`'s mismatches), so a swallowed one
146
+ # is reported exactly as one no rescue caught. An error that did reach the file's crash row is not repeated.
147
+ def with_swallowed(path, diagnostics, errors)
148
+ return diagnostics if errors.empty?
149
+
150
+ seen = diagnostics.map(&:message)
151
+ rows = errors.map { |error| check_rule_message(error) }.uniq.reject { |message| seen.include?(message) }
152
+ diagnostics + rows.map { |message| Diagnostic.new(path: path, line: 1, column: 1, message: message) }
153
+ end
154
+
113
155
  # The first `lib/rigor/` backtrace frame — the raise site, path made repo-relative so it reads the
114
156
  # same whether Rigor runs from a checkout or an installed gem. A LOCATION, not an attribution: a
115
157
  # bundled or third-party plugin also lives under `lib/rigor/<plugin>/`, so a frame here says where
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../typing_state"
4
+
3
5
  module Rigor
4
6
  module Analysis
5
7
  # ADR-46 slice 1 — records, per analyzed file, which OTHER source files its analysis read declarations /
@@ -12,9 +14,67 @@ module Rigor
12
14
  #
13
15
  # Modelled on {Inference::BudgetTrace}: process-thread-local state, a cheap disabled fast path, and a
14
16
  # frozen snapshot for consumers.
15
- module DependencyRecorder
16
- KEY = :__rigor_dependency_recorder__
17
- private_constant :KEY
17
+ module DependencyRecorder # rubocop:disable Metrics/ModuleLength
18
+ # ADR-129 WD1 — the installed accumulator is a Frame whose contents are a Resource for its suspension count, the
19
+ # one mutation that touches ({record_for}); its filings are entries of the bound Table {READS} and are journaled
20
+ # instead. WD4 shuts the `type_of` memo under recording only until these reads are journaled, which they now are.
21
+ # The activation count below is a process switch, outside the typing state (WD5).
22
+ RECORDER = TypingState.frame(
23
+ :dependency_recorder,
24
+ contents: :resource, reason: "the consumer's accumulator a recording installs; each read files into it",
25
+ # G1′ witness: the dependency reads are a journaled Table (WD4), so the memo may run under recording, and typing
26
+ # reads the accumulator (`read_site`, `read_name`) inside `type_of`.
27
+ witness: "spec/fixtures/typing_state_witnesses/dependency_recorder.rb"
28
+ )
29
+ private_constant :RECORDER
30
+
31
+ # ADR-129 WD1 — the recorded dependency reads are a bound Table: the installed {Accumulator} and each open
32
+ # {Capture} are its stores, keyed on the event tuples they deduplicate on (a frozen `[path, symbol_or_nil]` pair
33
+ # for a positive edge, a `"kind:name"` String for a name-keyed one). Each event that files into a store logs a
34
+ # read of its key (`true` when the store already held it, else {TypingState::ABSENT}) and, when it adds, a write
35
+ # of `true`, while a capture is open, so a later `type_of` memo can validate the reads and replay the writes.
36
+ # The keys are built only then: a run with no journal capture open allocates nothing for the journal, whether
37
+ # or not an ADR-84 capture is open.
38
+ # ADR-129 WD3 — {READS}'s entry protocol over an {Accumulator} or a {Capture}: a positive edge's
39
+ # `[path, symbol_or_nil]` pair is held in the accumulator's symbol or ancestry table (a capture's `reads`), a
40
+ # name-keyed `"kind:name"` String in `missing`. Every entry is `true`; a replayed write files the edge the way the
41
+ # event that logged it did.
42
+ module Entries
43
+ module_function
44
+
45
+ def entry(store, key)
46
+ held = if key.is_a?(String) then store.missing.include?(key)
47
+ elsif store.is_a?(Capture) then store.reads.include?(key)
48
+ else store.holds_read?(key[0], key[1])
49
+ end
50
+ held ? true : TypingState::ABSENT
51
+ end
52
+
53
+ def put(store, key, _value)
54
+ if key.is_a?(String) then store.missing.add(key)
55
+ elsif store.is_a?(Capture) then store.reads.add(key)
56
+ else store.add_read(key[0], key[1])
57
+ end
58
+ end
59
+
60
+ def delete(store, key)
61
+ if key.is_a?(String) then store.missing.delete(key)
62
+ elsif store.is_a?(Capture) then store.reads.delete(key)
63
+ else store.remove_read(key[0], key[1])
64
+ end
65
+ end
66
+
67
+ def container(_state, _slot)
68
+ raise ArgumentError, "dependency reads are a bound table: a store is an accumulator or a capture"
69
+ end
70
+ end
71
+
72
+ READS = TypingState.table(
73
+ :dependency_reads,
74
+ bound: true, entries: Entries,
75
+ reason: "event tuple => true per accumulator and per open capture; set-only, in first-filing order"
76
+ )
77
+ private_constant :READS
18
78
 
19
79
  # ADR-84 WD2 — thread-local Array of active {Capture}s (nil when none). A capture observes-and-forwards:
20
80
  # every read recorded while it is active flows into the current consumer's accumulator exactly as before
@@ -27,14 +87,24 @@ module Rigor
27
87
  # each outer one's: the inner capture was pushed after the outer, and everything added since reached both.
28
88
  # {withhold} swaps in a fresh stack and restores this one untouched, so it keeps the property too.
29
89
  # {tee_read} and {tee_missing} rely on it to stop at the first capture that already holds an event.
30
- CAPTURE_KEY = :__rigor_dependency_capture_stack__
31
- private_constant :CAPTURE_KEY
90
+ #
91
+ # ADR-129 WD3 — the stack is an id-determined Resource: every change of it ({capture}'s push and pop,
92
+ # {withhold}'s swap and restore) takes a fresh state id, so reading it on the event paths is exempt.
93
+ CAPTURES = TypingState.resource(
94
+ :dependency_capture_stack,
95
+ read_exemption: :id_determined,
96
+ reason: "the open capture windows, pushed per window and swapped by withhold"
97
+ )
98
+ private_constant :CAPTURES
32
99
 
33
100
  # The read sets an {Accumulator} or {Capture} already holds in full, by identity: replayed into it, or
34
101
  # captured by a window it saw every event of ({capture}). Both only ever grow and a {ReadSet} is frozen,
35
102
  # so replaying such a set into the same target again adds nothing. Recording replays the same stored
36
103
  # read set many times over: on Rigor's own `lib`, 71% of the replays into a consumer repeated one
37
104
  # already made into it, carrying 65% of the replayed events.
105
+ #
106
+ # ADR-129 — the held marks are an extensional dedupe cache over the store's own {READS} entries (every event of
107
+ # a held set is already in the store), so marking and probing them log nothing; {ChainMemory} likewise.
38
108
  module ReplayMemory
39
109
  def holds?(read_set)
40
110
  !@held.nil? && @held.key?(read_set)
@@ -90,6 +160,39 @@ module Rigor
90
160
  @suspensions += 1
91
161
  end
92
162
 
163
+ # The positive-edge aggregation shared by {DependencyRecorder.read_site}, {DependencyRecorder.replay} and a
164
+ # replayed {READS} write. `sources` is the union of the other two tables' paths, and this is the only writer of
165
+ # all three, so a read the finer table already holds is already in `sources` and costs one Set probe instead
166
+ # of two. Answers whether the read was new (truthy) or already held (nil), which is its {READS} filing.
167
+ def add_read(path, symbol)
168
+ added = symbol ? @symbol_sources[path].add?(symbol) : @ancestry_sources.add?(path)
169
+ @sources << path if added
170
+ added
171
+ end
172
+
173
+ # Undoes an {#add_read} that filed a new edge (G1's stage-4 form): `sources` keeps `path` while another table
174
+ # still holds it, and a `symbol_sources` set left empty goes, as no read had made it.
175
+ def remove_read(path, symbol)
176
+ if symbol
177
+ symbols = @symbol_sources.key?(path) ? @symbol_sources[path] : nil
178
+ if symbols
179
+ symbols.delete(symbol)
180
+ @symbol_sources.delete(path) if symbols.empty?
181
+ end
182
+ else
183
+ @ancestry_sources.delete(path)
184
+ end
185
+ @sources.delete(path) unless @ancestry_sources.include?(path) || @symbol_sources.key?(path)
186
+ end
187
+
188
+ # Whether the edge {#add_read} files for `path` and `symbol` is held. Reads `symbol_sources` without its
189
+ # default block, so asking adds no empty set.
190
+ def holds_read?(path, symbol)
191
+ return @ancestry_sources.include?(path) unless symbol
192
+
193
+ @symbol_sources.key?(path) && @symbol_sources[path].include?(symbol)
194
+ end
195
+
93
196
  def snapshot
94
197
  frozen_sym = @symbol_sources.transform_values(&:freeze).freeze
95
198
  Record.new(
@@ -154,16 +257,22 @@ module Rigor
154
257
  # the frozen {Record}. Nests safely (the inner consumer's reads do not leak to the outer one); restores
155
258
  # the previous recorder on exit.
156
259
  def record_for(consumer)
157
- previous = Thread.current[KEY]
158
- previous&.suspend!
260
+ state = TypingState.current
261
+ previous = state[RECORDER]
262
+ if previous
263
+ state.touch(RECORDER)
264
+ previous.suspend!
265
+ end
159
266
  accumulator = Accumulator.new(consumer.to_s)
160
- Thread.current[KEY] = accumulator
161
- @mutex.synchronize { @active_count += 1 }
162
- yield
163
- accumulator.snapshot
164
- ensure
165
- Thread.current[KEY] = previous
166
- @mutex.synchronize { @active_count -= 1 }
267
+ state.with(RECORDER, accumulator) do
268
+ @mutex.synchronize { @active_count += 1 }
269
+ begin
270
+ yield
271
+ accumulator.snapshot
272
+ ensure
273
+ @mutex.synchronize { @active_count -= 1 }
274
+ end
275
+ end
167
276
  end
168
277
 
169
278
  # Plain integer read (GVL-atomic) — no `Thread.current` lookup on the disabled fast path.
@@ -175,25 +284,26 @@ module Rigor
175
284
  # window. {active?} answers for the process and is the cheap gate; this is the exact one, for a caller
176
285
  # that must know whether an empty {capture} means "nothing to record" or "nothing was watching".
177
286
  def recording?
178
- !Thread.current[KEY].nil?
287
+ !TypingState.current[RECORDER].nil?
179
288
  end
180
289
 
181
290
  # ADR-84 WD2 — runs the block under an observe-and-forward capture window and returns
182
291
  # `[block_result, read_set]`. Reads recorded during the window reach the current consumer's accumulator
183
292
  # unchanged (forwarding) and are additionally collected into the returned frozen {ReadSet} (observing),
184
- # which {replay} can later apply to a different consumer. Nests (see CAPTURE_KEY). Callers guard on
293
+ # which {replay} can later apply to a different consumer. Nests (see CAPTURES). Callers guard on
185
294
  # {active?}; opening a capture with no accumulator active would collect reads no run is recording.
186
295
  def capture
187
- captures = (Thread.current[CAPTURE_KEY] ||= [])
188
- accumulator = Thread.current[KEY]
296
+ state = TypingState.current
297
+ accumulator = state[RECORDER]
189
298
  suspensions = accumulator&.suspensions
190
299
  capture = Capture.new
191
- captures.push(capture)
300
+ state.push(CAPTURES, capture)
192
301
  begin
193
302
  result = yield
194
303
  ensure
195
- captures.pop
304
+ state.pop(CAPTURES)
196
305
  end
306
+ captures = state[CAPTURES]
197
307
  read_set = capture.snapshot
198
308
  note_captured(read_set, captures, accumulator, suspensions)
199
309
  [result, read_set]
@@ -209,7 +319,7 @@ module Rigor
209
319
  return if read_set.equal?(EMPTY_READ_SET)
210
320
 
211
321
  captures.last&.hold!(read_set)
212
- return unless accumulator && Thread.current[KEY].equal?(accumulator)
322
+ return unless accumulator && TypingState.current[RECORDER].equal?(accumulator)
213
323
  return unless accumulator.suspensions == suspensions
214
324
 
215
325
  accumulator.hold!(read_set)
@@ -223,18 +333,22 @@ module Rigor
223
333
  # edge would make every correct call on a project class re-check on any edit to the class's files.
224
334
  # Returns a nil read set when no recording is active.
225
335
  def withhold
226
- previous = Thread.current[KEY]
336
+ state = TypingState.current
337
+ previous = state[RECORDER]
227
338
  return [yield, nil] if previous.nil?
228
339
 
229
- previous_captures = Thread.current[CAPTURE_KEY]
340
+ previous_captures = state[CAPTURES]
230
341
  capture = Capture.new
231
- Thread.current[KEY] = Accumulator.new(previous.consumer)
232
- Thread.current[CAPTURE_KEY] = [capture]
233
- begin
234
- result = yield
235
- ensure
236
- Thread.current[KEY] = previous
237
- Thread.current[CAPTURE_KEY] = previous_captures
342
+ result = state.with(RECORDER, Accumulator.new(previous.consumer)) do
343
+ # A swap of the whole stack: a write, then a rewrite, so the swapped-in stack is read under a fresh id.
344
+ state.write(CAPTURES, [capture])
345
+ state.rewrite(CAPTURES)
346
+ begin
347
+ yield
348
+ ensure
349
+ state.write(CAPTURES, previous_captures)
350
+ state.rewrite(CAPTURES)
351
+ end
238
352
  end
239
353
  [result, capture.snapshot]
240
354
  end
@@ -243,11 +357,12 @@ module Rigor
243
357
  # here, applying the same per-consumer self-read filter as {read_site}, and tees into every active
244
358
  # capture so an enclosing capture window stays transitive when a memo hit substitutes for a body walk.
245
359
  def replay(read_set)
246
- accumulator = Thread.current[KEY]
360
+ state = TypingState.current
361
+ accumulator = state[RECORDER]
247
362
  return if accumulator.nil? || read_set.nil? || read_set.equal?(EMPTY_READ_SET)
248
363
 
249
- captures = Thread.current[CAPTURE_KEY]
250
- tee_read_set(captures, read_set) if captures && !captures.empty?
364
+ captures = state[CAPTURES]
365
+ tee_read_set(state, captures, read_set) if captures && !captures.empty?
251
366
  # Each set keeps its own insertion order whichever target is filled first, and a skipped replay is one
252
367
  # that could only have re-added what the target already holds.
253
368
  return if accumulator.holds?(read_set)
@@ -255,25 +370,29 @@ module Rigor
255
370
  consumer = accumulator.consumer
256
371
  read_set.reads.each do |pair|
257
372
  path, symbol = pair
258
- accumulate_read(accumulator, path, symbol) unless path == consumer
373
+ next if path == consumer
374
+
375
+ added = accumulate_read(accumulator, path, symbol)
376
+ log_filing(state, accumulator, pair, added) if state.capturing?
259
377
  end
260
378
  missing = accumulator.missing
261
- read_set.missing.each { |entry| missing << entry }
379
+ read_set.missing.each { |entry| file_missing(state, accumulator, missing, entry) }
262
380
  accumulator.hold!(read_set)
263
381
  end
264
382
 
265
383
  # #1590 — runs the block, which files a resolution chain's edges from entry `start` (nil: the root and the
266
384
  # whole chain) against `discovery`, unless the current consumer's accumulator AND the innermost open capture
267
385
  # already hold that filing; the block files the same edges every time, so skipping it there changes nothing.
268
- # Every event reaches every capture open at the time (see CAPTURE_KEY), so the innermost capture holding the
386
+ # Every event reaches every capture open at the time (see CAPTURES), so the innermost capture holding the
269
387
  # filing means every open capture does. A filing the accumulator holds but a capture opened since does not is
270
388
  # filed again, so that capture's read set, replayed elsewhere, still carries it; and {withhold} starts a fresh
271
389
  # accumulator and capture, which hold nothing. The marks are made only once the block has run, as {hold!}'s are.
272
390
  def file_chain_once(chain, start, discovery)
273
- accumulator = Thread.current[KEY]
391
+ state = TypingState.current
392
+ accumulator = state[RECORDER]
274
393
  return if accumulator.nil?
275
394
 
276
- captures = Thread.current[CAPTURE_KEY]
395
+ captures = state[CAPTURES]
277
396
  open = captures && !captures.empty?
278
397
  if accumulator.chain_filed?(chain, start, discovery) &&
279
398
  (!open || captures.last.chain_filed?(chain, start, discovery))
@@ -286,33 +405,78 @@ module Rigor
286
405
  end
287
406
 
288
407
  # {replay}'s share for the open captures. The innermost capture having seen `read_set` already means every
289
- # open capture holds all of it (see CAPTURE_KEY).
290
- def tee_read_set(captures, read_set)
408
+ # open capture holds all of it (see CAPTURES).
409
+ def tee_read_set(state, captures, read_set)
291
410
  innermost = captures.last
292
411
  return if innermost.holds?(read_set)
293
412
 
294
- read_set.reads.each { |pair| tee_read(captures, pair) }
295
- read_set.missing.each { |entry| tee_missing(captures, entry) }
413
+ read_set.reads.each { |pair| tee_read(state, captures, pair) }
414
+ read_set.missing.each { |entry| tee_missing(state, captures, entry) }
296
415
  innermost.hold!(read_set)
297
416
  end
298
417
  private_class_method :tee_read_set
299
418
 
300
419
  # Adds a read pair to the open captures, innermost first, and stops at the first capture that already holds
301
- # it: every capture outside that one holds it too (see CAPTURE_KEY). The capture sets end up exactly as
302
- # adding it to every capture would leave them, in the same insertion order.
303
- def tee_read(captures, pair)
304
- index = captures.size - 1
305
- index -= 1 while index >= 0 && captures[index].reads.add?(pair)
420
+ # it: every capture outside that one holds it too (see CAPTURES). The capture sets end up exactly as
421
+ # adding it to every capture would leave them, in the same insertion order. Each capture probed logs its
422
+ # filing; the ones outside the first holder are not probed, and hold the key by the same property.
423
+ def tee_read(state, captures, pair)
424
+ if state.capturing?
425
+ tee_logged(state, captures, pair, :reads)
426
+ else
427
+ index = captures.size - 1
428
+ index -= 1 while index >= 0 && captures[index].reads.add?(pair)
429
+ end
306
430
  end
307
431
  private_class_method :tee_read
308
432
 
309
433
  # {tee_read} for a name-keyed `missing` entry.
310
- def tee_missing(captures, entry)
311
- index = captures.size - 1
312
- index -= 1 while index >= 0 && captures[index].missing.add?(entry)
434
+ def tee_missing(state, captures, entry)
435
+ if state.capturing?
436
+ tee_logged(state, captures, entry, :missing)
437
+ else
438
+ index = captures.size - 1
439
+ index -= 1 while index >= 0 && captures[index].missing.add?(entry)
440
+ end
313
441
  end
314
442
  private_class_method :tee_missing
315
443
 
444
+ # {tee_read} / {tee_missing} while a capture is open: the same walk, logging each capture's filing.
445
+ def tee_logged(state, captures, key, set)
446
+ index = captures.size - 1
447
+ while index >= 0
448
+ capture = captures[index]
449
+ added = capture.public_send(set).add?(key)
450
+ log_filing(state, capture, key, added)
451
+ break unless added
452
+
453
+ index -= 1
454
+ end
455
+ end
456
+ private_class_method :tee_logged
457
+
458
+ # Logs one filing of `key` into `store` (an {Accumulator} or a {Capture}): a read of the key, which the store held
459
+ # unless the filing `added` it, and the write when it did. Callers log only while a capture is open.
460
+ def log_filing(state, store, key, added)
461
+ state.log_bound_read(READS, store, key, added ? TypingState::ABSENT : true)
462
+ state.log_bound_write(READS, store, key, true, TypingState::ABSENT) if added
463
+ end
464
+ private_class_method :log_filing
465
+
466
+ # A positive edge filed into the consumer's accumulator ({Accumulator#add_read}): truthy when it was new.
467
+ def accumulate_read(accumulator, path, symbol) = accumulator.add_read(path, symbol)
468
+ private_class_method :accumulate_read
469
+
470
+ # Files a name-keyed entry into an accumulator's `missing` set, logging the filing while a capture is open.
471
+ def file_missing(state, accumulator, missing, entry)
472
+ if state.capturing?
473
+ log_filing(state, accumulator, entry, missing.add?(entry))
474
+ else
475
+ missing << entry
476
+ end
477
+ end
478
+ private_class_method :file_missing
479
+
316
480
  # Records that the current consumer read a declaration / body whose definition site is `path_line` (a
317
481
  # `"path:line"` String, or nil). When `symbol` is given (a `"ClassName#method"` String), the read is a
318
482
  # method-call edge and is recorded at symbol granularity in `symbol_sources` in addition to the coarse
@@ -321,17 +485,20 @@ module Rigor
321
485
  # (ADR-84 WD2) observe every valid-path read UNfiltered — the self-read filter is per-consumer and is
322
486
  # re-applied by {replay} against whichever consumer a replay serves.
323
487
  def read_site(path_line, symbol = nil)
324
- accumulator = Thread.current[KEY]
488
+ state = TypingState.current
489
+ accumulator = state[RECORDER]
325
490
  return if accumulator.nil? || path_line.nil?
326
491
 
327
492
  path = site_path(path_line)
328
493
  return unless path
329
494
 
330
- captures = Thread.current[CAPTURE_KEY]
331
- tee_read(captures, [path, symbol].freeze) if captures && !captures.empty?
495
+ captures = state[CAPTURES]
496
+ pair = [path, symbol].freeze if (captures && !captures.empty?) || state.capturing?
497
+ tee_read(state, captures, pair) if captures && !captures.empty?
332
498
  return if path == accumulator.consumer
333
499
 
334
- accumulate_read(accumulator, path, symbol)
500
+ added = accumulate_read(accumulator, path, symbol)
501
+ log_filing(state, accumulator, pair, added) if state.capturing?
335
502
  end
336
503
 
337
504
  # The path part of a `"path:line"` site: what `path_line.split(":", 2).first` answers, without the Array
@@ -355,13 +522,14 @@ module Rigor
355
522
  # currently wins: a second declarer appearing in a file the reader has no edge to must drop the name to
356
523
  # `Dynamic` at that reader, and only a name-keyed edge can see it.
357
524
  def read_name(kind, name)
358
- accumulator = Thread.current[KEY]
525
+ state = TypingState.current
526
+ accumulator = state[RECORDER]
359
527
  return if accumulator.nil?
360
528
 
361
529
  entry = "#{kind}:#{name}"
362
- captures = Thread.current[CAPTURE_KEY]
363
- tee_missing(captures, entry) if captures && !captures.empty?
364
- accumulator.missing.add(entry)
530
+ captures = state[CAPTURES]
531
+ tee_missing(state, captures, entry) if captures && !captures.empty?
532
+ file_missing(state, accumulator, accumulator.missing, entry)
365
533
  end
366
534
 
367
535
  # {read_name} keyed on the LAST SEGMENT of a constant path, `name.delete_prefix("::").split("::").last`,
@@ -370,7 +538,7 @@ module Rigor
370
538
  # objects a call over half a million calls. A name the scan could read differently from `split` (one
371
539
  # that is not ASCII, or carries a run of colons or a trailing one) takes `split`'s answer.
372
540
  def read_last_segment(kind, name)
373
- return if Thread.current[KEY].nil?
541
+ return unless recording?
374
542
 
375
543
  segment = last_segment(name)
376
544
  read_name(kind, segment) if segment
@@ -394,15 +562,16 @@ module Rigor
394
562
  # {read_name} for prebuilt, frozen `"kind:name"` keys: a caller that files the same keys on every read
395
563
  # (a resolution chain's hook edges, #1641) builds them once and adds them here without a String apiece.
396
564
  def read_keys(keys)
397
- accumulator = Thread.current[KEY]
565
+ state = TypingState.current
566
+ accumulator = state[RECORDER]
398
567
  return if accumulator.nil?
399
568
 
400
- captures = Thread.current[CAPTURE_KEY]
569
+ captures = state[CAPTURES]
401
570
  open = captures && !captures.empty?
402
571
  missing = accumulator.missing
403
572
  keys.each do |entry|
404
- tee_missing(captures, entry) if open
405
- missing.add(entry)
573
+ tee_missing(state, captures, entry) if open
574
+ file_missing(state, accumulator, missing, entry)
406
575
  end
407
576
  end
408
577
 
@@ -412,7 +581,7 @@ module Rigor
412
581
  # the name private passes `"*"` as the class; the key fires when any entry of the name moves. A wildcard
413
582
  # slot (`[:singleton, :*]`) is `*` as the method name.
414
583
  def read_visibility(class_name, method_name)
415
- return if Thread.current[KEY].nil?
584
+ return unless recording?
416
585
 
417
586
  read_name(:visibility, "#{class_name}##{method_name}")
418
587
  end
@@ -423,18 +592,6 @@ module Rigor
423
592
  def read_missing(kind, name)
424
593
  read_name(kind, name)
425
594
  end
426
-
427
- # The positive-edge aggregation shared by {read_site} and {replay}. `sources` is the union of the other
428
- # two tables' paths, and this is the only writer of all three, so a read the finer table already holds is
429
- # already in `sources` and costs one Set probe instead of two.
430
- def accumulate_read(accumulator, path, symbol)
431
- if symbol
432
- accumulator.sources << path if accumulator.symbol_sources[path].add?(symbol)
433
- elsif accumulator.ancestry_sources.add?(path)
434
- accumulator.sources << path
435
- end
436
- end
437
- private_class_method :accumulate_read
438
595
  end
439
596
  end
440
597
  end
@@ -256,9 +256,19 @@ module Rigor
256
256
  end
257
257
 
258
258
  def class_mark?(key)
259
- key.is_a?(Symbol) || (key.is_a?(Array) && key.first == Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND)
259
+ key.is_a?(Symbol) || (key.is_a?(Array) && ENTRY_MARK_KINDS.include?(key.first))
260
260
  end
261
261
 
262
+ # Issue #2901 — a class-body macro call or computed definer can be filed on a class another file declares
263
+ # (`X.class_eval { my_attr :a }`), so they diff as marks too. Issue #2998 — a singleton prepend diffs as one so a
264
+ # new file that prepends to a class object's singleton fires the `mark:` edge a singleton chain search files.
265
+ ENTRY_MARK_KINDS = [Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND, Scope::DiscoveryIndex::ENVELOPE_BODY_CALL_KIND,
266
+ Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND,
267
+ Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND,
268
+ Scope::DiscoveryIndex::ENVELOPE_SAME_CLASS_KIND,
269
+ Scope::DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND].freeze
270
+ private_constant :ENTRY_MARK_KINDS
271
+
262
272
  # Issue #1661 — the names of the classes and modules whose unpositioned mixin edges
263
273
  # (`Scope::DiscoveryIndex#unpositioned_mixins`, `"*"` included) appeared or vanished in the files an edit
264
274
  # touched. A file can taint a module it does not otherwise change the chain of (`module Comparable;
@@ -78,7 +78,8 @@ module Rigor
78
78
  fingerprint
79
79
 
80
80
  lockfiles = lockfile_paths(configuration)
81
- named = Array(roots) + configuration.pre_eval + (configuration.signature_paths || ["sig"])
81
+ named = Array(roots) + configuration.pre_eval +
82
+ Environment.project_signature_root_candidates(configuration.signature_paths)
82
83
  presence = (lockfiles + named.map { |path| File.absolute_path(path.to_s) }).to_h do |path|
83
84
  [path, Cache::Descriptor::FileEntry.kind_of(path)]
84
85
  end