rigortype 0.4.4 → 0.4.6

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 (233) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +100 -18
  4. data/docs/manual/02-cli-reference.md +85 -16
  5. data/docs/manual/03-configuration.md +12 -9
  6. data/docs/manual/04-diagnostics.md +17 -6
  7. data/docs/manual/05-inspecting-types.md +2 -2
  8. data/docs/manual/06-baseline.md +1 -1
  9. data/docs/manual/07-plugins.md +2 -1
  10. data/docs/manual/08-skills.md +7 -0
  11. data/docs/manual/10-mcp-server.md +17 -2
  12. data/docs/manual/12-caching.md +8 -1
  13. data/docs/manual/18-removing-dead-code.md +18 -7
  14. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  15. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  16. data/docs/manual/plugins/rigor-devise.md +15 -0
  17. data/docs/manual/plugins/rigor-rspec.md +102 -1
  18. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  19. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  20. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  21. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  22. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
  23. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
  24. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  25. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  26. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  27. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  28. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  29. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
  30. data/lib/rigor/analysis/check_rules.rb +319 -21
  31. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/edit_replay.rb +67 -0
  34. data/lib/rigor/analysis/incremental.rb +86 -0
  35. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  36. data/lib/rigor/analysis/incremental_session.rb +336 -46
  37. data/lib/rigor/analysis/path_expansion.rb +44 -1
  38. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  39. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  40. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  41. data/lib/rigor/analysis/rule_catalog.rb +22 -5
  42. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  43. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  44. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  48. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  49. data/lib/rigor/analysis/runner.rb +222 -111
  50. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  51. data/lib/rigor/analysis/worker_session.rb +24 -1
  52. data/lib/rigor/cache/descriptor.rb +40 -7
  53. data/lib/rigor/cache/engine_source.rb +3 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +49 -8
  55. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  56. data/lib/rigor/cache/store.rb +4 -3
  57. data/lib/rigor/cli/check_command.rb +93 -4
  58. data/lib/rigor/cli/command.rb +3 -1
  59. data/lib/rigor/cli/diff_command.rb +2 -1
  60. data/lib/rigor/cli/docs_command.rb +3 -2
  61. data/lib/rigor/cli/doctor_command.rb +1 -1
  62. data/lib/rigor/cli/explain_command.rb +3 -2
  63. data/lib/rigor/cli/lens_command.rb +5 -5
  64. data/lib/rigor/cli/mcp_command.rb +2 -1
  65. data/lib/rigor/cli/options.rb +20 -3
  66. data/lib/rigor/cli/plugin_command.rb +2 -1
  67. data/lib/rigor/cli/plugins_command.rb +7 -1
  68. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  69. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  70. data/lib/rigor/cli/sig_gen_command.rb +73 -13
  71. data/lib/rigor/cli/skill_command.rb +2 -1
  72. data/lib/rigor/cli/skill_describe.rb +3 -2
  73. data/lib/rigor/cli/type_of_command.rb +2 -1
  74. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  75. data/lib/rigor/cli/unused_command.rb +10 -7
  76. data/lib/rigor/cli.rb +3 -2
  77. data/lib/rigor/config_audit.rb +72 -16
  78. data/lib/rigor/configuration/severity_profile.rb +20 -2
  79. data/lib/rigor/configuration.rb +43 -7
  80. data/lib/rigor/effects/signature_sources.rb +3 -1
  81. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  82. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  83. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  84. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  85. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  86. data/lib/rigor/environment/rbs_loader.rb +136 -7
  87. data/lib/rigor/environment/required_features.rb +136 -29
  88. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  89. data/lib/rigor/environment.rb +4 -2
  90. data/lib/rigor/inference/block_call_timing.rb +7 -1
  91. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  92. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  93. data/lib/rigor/inference/captured_locals.rb +44 -13
  94. data/lib/rigor/inference/clause_widening.rb +90 -0
  95. data/lib/rigor/inference/deep_erasure.rb +138 -0
  96. data/lib/rigor/inference/def_return_typer.rb +159 -9
  97. data/lib/rigor/inference/definer_resolution.rb +101 -20
  98. data/lib/rigor/inference/error_info.rb +17 -3
  99. data/lib/rigor/inference/expression_typer.rb +97 -18
  100. data/lib/rigor/inference/global_write_census.rb +36 -1
  101. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  102. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  103. data/lib/rigor/inference/loop_value_record.rb +53 -0
  104. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  105. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  106. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
  107. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  108. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  109. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  110. data/lib/rigor/inference/method_dispatcher.rb +113 -19
  111. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  112. data/lib/rigor/inference/module_function_state.rb +149 -42
  113. data/lib/rigor/inference/mutation_widening.rb +5 -2
  114. data/lib/rigor/inference/narrowing.rb +116 -11
  115. data/lib/rigor/inference/object_mixins.rb +13 -1
  116. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  117. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  118. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  119. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  120. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  121. data/lib/rigor/inference/project_seed_tables.rb +121 -0
  122. data/lib/rigor/inference/project_yielder.rb +128 -0
  123. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  124. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  125. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  126. data/lib/rigor/inference/return_barrier.rb +16 -0
  127. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  128. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  129. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  130. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  131. data/lib/rigor/inference/scope_indexer.rb +1144 -197
  132. data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
  133. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  134. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  135. data/lib/rigor/inference/statement_evaluator.rb +1088 -213
  136. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  137. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  138. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  139. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  140. data/lib/rigor/language_server/buffer_table.rb +6 -0
  141. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  142. data/lib/rigor/language_server/project_context.rb +59 -3
  143. data/lib/rigor/language_server/server.rb +5 -3
  144. data/lib/rigor/lens/contract_reader.rb +45 -2
  145. data/lib/rigor/lens/document.rb +2 -1
  146. data/lib/rigor/lens/file_reader.rb +12 -2
  147. data/lib/rigor/lens/members.rb +16 -2
  148. data/lib/rigor/lens/run.rb +34 -7
  149. data/lib/rigor/mcp/server.rb +11 -3
  150. data/lib/rigor/plugin/base.rb +29 -13
  151. data/lib/rigor/plugin/io_boundary.rb +42 -6
  152. data/lib/rigor/plugin/isolation.rb +4 -3
  153. data/lib/rigor/plugin/manifest.rb +29 -3
  154. data/lib/rigor/plugin/registry.rb +38 -0
  155. data/lib/rigor/protection/discovery_seed.rb +64 -36
  156. data/lib/rigor/protection/mutation_cache.rb +8 -2
  157. data/lib/rigor/rooted_glob.rb +36 -0
  158. data/lib/rigor/scope/discovery_index.rb +5 -0
  159. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  160. data/lib/rigor/scope/resolution_chain.rb +40 -5
  161. data/lib/rigor/scope.rb +59 -7
  162. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  163. data/lib/rigor/sig_gen/classification.rb +4 -1
  164. data/lib/rigor/sig_gen/generator.rb +465 -49
  165. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  166. data/lib/rigor/sig_gen/observation_collector.rb +141 -12
  167. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  168. data/lib/rigor/sig_gen/renderer.rb +16 -155
  169. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  170. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  171. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  172. data/lib/rigor/sig_gen/write_result.rb +3 -1
  173. data/lib/rigor/sig_gen/writer.rb +5 -3
  174. data/lib/rigor/signature_path_audit.rb +4 -2
  175. data/lib/rigor/type/combinator.rb +4 -3
  176. data/lib/rigor/type/constant.rb +54 -3
  177. data/lib/rigor/type/hash_shape.rb +14 -9
  178. data/lib/rigor/type/struct_instance.rb +3 -2
  179. data/lib/rigor/version.rb +1 -1
  180. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  181. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
  182. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  183. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  184. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  185. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  186. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  187. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  188. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  189. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  190. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  191. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  192. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  193. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  194. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  195. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  196. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  197. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  198. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  199. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  200. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  201. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  202. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  203. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  204. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  205. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  206. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  207. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  208. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  209. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  210. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  211. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  212. data/sig/rigor/analysis/baseline.rbs +3 -2
  213. data/sig/rigor/effects/file_collection.rbs +5 -6
  214. data/sig/rigor/effects/registry.rbs +8 -6
  215. data/sig/rigor/effects/summary.rbs +6 -5
  216. data/sig/rigor/environment.rbs +5 -3
  217. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  218. data/sig/rigor/inference.rbs +6 -0
  219. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  220. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  221. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  222. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  223. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  224. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  225. data/sig/rigor/plugin/manifest.rbs +16 -18
  226. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  227. data/sig/rigor/plugin/registry.rbs +1 -1
  228. data/sig/rigor/rooted_glob.rbs +7 -0
  229. data/sig/rigor/scope.rbs +34 -52
  230. data/sig/rigor/type.rbs +0 -6
  231. data/skills/rigor-doctor/references/01-checks.md +23 -11
  232. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  233. metadata +32 -1
@@ -406,6 +406,17 @@ module Rigor
406
406
  end
407
407
  end
408
408
 
409
+ # Issue #2134 — {read_name} for a read of `class_name`'s visibility slots for `method_name`, on either side, or
410
+ # of their contest: `visibility:<class>#<method>`. `IncrementalSession#visibility_name_affected` fires the key
411
+ # when a file's visibility entry or contest for the slot moves. A reader that asks whether ANY class records
412
+ # the name private passes `"*"` as the class; the key fires when any entry of the name moves. A wildcard
413
+ # slot (`[:singleton, :*]`) is `*` as the method name.
414
+ def read_visibility(class_name, method_name)
415
+ return if Thread.current[KEY].nil?
416
+
417
+ read_name(:visibility, "#{class_name}##{method_name}")
418
+ end
419
+
409
420
  # The miss-only spelling of {read_name}: a cross-file lookup of `name` (kind `:method` / `:class` /
410
421
  # `:toplevel` / …) that resolved to nothing. Kept as the name the internal spec's negative-edge
411
422
  # contract uses.
@@ -6,6 +6,7 @@ require_relative "return_type_heuristic"
6
6
  require_relative "../../source/constant_path"
7
7
  require_relative "../../source/node_children"
8
8
  require_relative "../../inference/scope_indexer"
9
+ require_relative "../../rooted_glob"
9
10
 
10
11
  module Rigor
11
12
  module Analysis
@@ -127,7 +128,7 @@ module Rigor
127
128
  def walk_root(root_dir, harvest, budget) # rubocop:disable Naming/PredicateMethod
128
129
  return false unless File.directory?(root_dir)
129
130
 
130
- Dir.glob(File.join(root_dir, "**", "*.rb")).each do |path|
131
+ RootedGlob.glob(root_dir, "**/*.rb").each do |path|
131
132
  harvest_file(path, harvest, budget)
132
133
  return true if harvest.catalog.size >= budget
133
134
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Analysis
7
+ # Issue #1967 — the edits `rigor check --verify-incremental-edits` replays on each sampled file, as the text an
8
+ # editor buffer stands in for it with ({IncrementalSession#replay_edit}).
9
+ #
10
+ # - `emptied` — the file with nothing in it. Every declaration and definition it makes disappears, so the replay
11
+ # exercises the edges a removed declaration fires.
12
+ # - `skeleton` — the file's `class` / `module` declarations, nested as written, with every body statement dropped
13
+ # (methods, `refine` blocks, visibility calls, mixins, constants). The declarations stay, so the class-level keys
14
+ # a removed declaration fires stay quiet, and a finer edge such as #2163's `refinement:` is what puts a reader
15
+ # into the closure. Emptying alone does not exercise that one: the broader key it fires re-checks the reader
16
+ # anyway. Not every finer edge is reached: dropping a body still moves a broader key for some of them. A
17
+ # `visibility:` edge is one (#2161's `module_function :two` to `private :two` swap passes both edits with the
18
+ # edge removed), and only an edit that changes a body while keeping it (#2277) would test it.
19
+ #
20
+ # Neither replays an addition or a body-changing edit. A skeleton identical to the file, or with no declaration
21
+ # (the emptied file again, or a file that does not parse), is not replayed.
22
+ module EditReplay
23
+ Edit = Data.define(:label, :source)
24
+
25
+ module_function
26
+
27
+ # The edits to replay for `source`, in order.
28
+ def edits(source)
29
+ emptied = Edit.new(label: "emptying", source: "")
30
+ reduced = skeleton(source)
31
+ return [emptied] if reduced.empty? || reduced == source
32
+
33
+ [emptied, Edit.new(label: "reducing to its class/module skeleton", source: reduced)]
34
+ end
35
+
36
+ # `source`'s `class` / `module` declarations, nested, with their bodies dropped; "" when it does not parse or
37
+ # declares nothing.
38
+ def skeleton(source)
39
+ result = Prism.parse(source)
40
+ return "" unless result.success?
41
+
42
+ lines = declarations(result.value.statements, 0)
43
+ lines.empty? ? "" : "#{lines.join("\n")}\n"
44
+ end
45
+
46
+ # The skeleton lines of the declarations directly in `statements`, indented `depth` levels.
47
+ def declarations(statements, depth)
48
+ return [] unless statements.is_a?(Prism::StatementsNode)
49
+
50
+ statements.body.flat_map do |node|
51
+ next [] unless node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
52
+
53
+ indent = " " * depth
54
+ ["#{indent}#{header_of(node)}", *declarations(node.body, depth + 1), "#{indent}end"]
55
+ end
56
+ end
57
+
58
+ def header_of(node)
59
+ return "module #{node.constant_path.slice}" if node.is_a?(Prism::ModuleNode)
60
+
61
+ superclass = node.superclass
62
+ superclass ? "class #{node.constant_path.slice} < #{superclass.slice}" : "class #{node.constant_path.slice}"
63
+ end
64
+ private_class_method :declarations, :header_of
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../inference/global_write_census"
4
+
3
5
  module Rigor
4
6
  module Analysis
5
7
  # ADR-46 slice 2 — the pure set-algebra core of the incremental step, kept side-effect-free and
@@ -132,6 +134,41 @@ module Rigor
132
134
  moved.freeze
133
135
  end
134
136
 
137
+ # Issue #2158 — the `global_census:` keys whose entries moved in this edit: one for every entry that appeared in
138
+ # or vanished from a file's `global_write_census` ({Inference::GlobalWriteCensus.dependency_key}). `paths` MUST
139
+ # include removed files, whose whole census vanished. `before` / `after` map a path to that file's census Set (nil
140
+ # when it records nothing).
141
+ def changed_global_census_keys(paths, before, after)
142
+ moved = Set.new
143
+ paths.each do |path|
144
+ ((before[path] || Set.new) ^ (after[path] || Set.new)).each do |entry|
145
+ moved << "global_census:#{Inference::GlobalWriteCensus.dependency_key(entry)}"
146
+ end
147
+ end
148
+ moved.freeze
149
+ end
150
+
151
+ # The {Inference::ScopeIndexer.visibility_entries} entries that carry a leading marker before `class, slot`.
152
+ MARKED_VISIBILITY_ENTRIES = %i[contested module_function].freeze
153
+
154
+ # Issue #2134 — the `<class>#<method>` slots whose visibility entries or contests moved in this edit: one for
155
+ # every entry that appeared in or vanished from a file's {Inference::ScopeIndexer.visibility_entries}, a
156
+ # singleton slot's `[:singleton, name]` read as `name` (the wildcard slot as `*`), plus `*#<method>`, which the
157
+ # "is the name private anywhere" readers record. That key fires for an entry of any visibility: the merge is
158
+ # last-writer-wins, so dropping a `public :foo` can uncover another file's `private :foo`. `paths` MUST include
159
+ # removed files. Issue #2161 — a `[:module_function, owner, name]` entry fires `<owner>#<name>` the same way.
160
+ def changed_visibility_slots(paths, before, after)
161
+ moved = Set.new
162
+ paths.each do |path|
163
+ ((before[path] || Set.new) ^ (after[path] || Set.new)).each do |entry|
164
+ class_name, slot = MARKED_VISIBILITY_ENTRIES.include?(entry.first) ? entry.drop(1) : entry
165
+ name = slot.is_a?(Array) ? slot.last : slot
166
+ moved << "#{class_name}##{name}" << "*##{name}"
167
+ end
168
+ end
169
+ moved.freeze
170
+ end
171
+
135
172
  # Issue #1120 — the method names whose refinements moved in this edit. A consumer whose answer consulted the
136
173
  # refinement table recorded a `refinement:<method>` edge whichever way it answered, because the answer is a
137
174
  # function of the whole project's refinements of that name. `paths` MUST include removed files, whose whole
@@ -194,6 +231,55 @@ module Rigor
194
231
  moved.freeze
195
232
  end
196
233
 
234
+ # Issue #1833 — the `[class name, mark]` pairs that appeared in or vanished from the class marks of the files an
235
+ # edit touched: a Symbol key of a `discovered_parameter_envelopes` bucket (the class-wide marks of
236
+ # `Scope::DiscoveryIndex`) or a `[ENVELOPE_REMOVED_KIND, name]` entry. A file may mark a class it does not declare
237
+ # (`x.singleton_class.prepend(Comparable)`, `o.extend(Helper)` in a method body), so neither the class-declaration
238
+ # diff nor a reader's edge to the declaring files sees such a mark appear or vanish.
239
+ #
240
+ # `before` and `after` are lists of envelope tables (`{class name => bucket}`): the touched files' seed-bundle
241
+ # tables before the edit, and the merged table of the re-parsed files after it. The diff is of their unions, which
242
+ # moves whenever the project's union of marks moves (the untouched files contribute the same marks to both).
243
+ # Returns a frozen Set of frozen `[class name, mark]` pairs.
244
+ def changed_class_marks(before, after)
245
+ (class_marks(before) ^ class_marks(after)).freeze
246
+ end
247
+
248
+ def class_marks(tables)
249
+ marks = Set.new
250
+ tables.each do |table|
251
+ table&.each do |class_name, bucket|
252
+ bucket.each_key { |key| marks << [class_name, key].freeze if class_mark?(key) }
253
+ end
254
+ end
255
+ marks
256
+ end
257
+
258
+ def class_mark?(key)
259
+ key.is_a?(Symbol) || (key.is_a?(Array) && key.first == Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND)
260
+ end
261
+
262
+ # Issue #1661 — the names of the classes and modules whose unpositioned mixin edges
263
+ # (`Scope::DiscoveryIndex#unpositioned_mixins`, `"*"` included) appeared or vanished in the files an edit
264
+ # touched. A file can taint a module it does not otherwise change the chain of (`module Comparable;
265
+ # include(*MODS); end`, `Comparable.include(x) if c`), and an external chain entry has no declaring file
266
+ # for a reader's edge to name. `before` and `after` are lists of `{class => {side => [names]}}` tables,
267
+ # diffed as unions like {changed_class_marks}. Returns a frozen Set of class names.
268
+ def changed_unpositioned_owners(before, after)
269
+ moved = unpositioned_edges(before) ^ unpositioned_edges(after)
270
+ moved.to_set { |(owner, _side, _name)| owner }.freeze
271
+ end
272
+
273
+ def unpositioned_edges(tables)
274
+ edges = Set.new
275
+ tables.each do |table|
276
+ table&.each do |owner, sides|
277
+ sides.each { |side, names| names.each { |name| edges << [owner, side, name].freeze } }
278
+ end
279
+ end
280
+ edges
281
+ end
282
+
197
283
  # Issue #644 — the qualified constant names whose PUBLICATION could have moved in this edit: the names
198
284
  # whose per-file census descriptor differs between the before- and after-state of any file in `paths`.
199
285
  # The value-constant twin of {appeared_classes}, and deliberately NOT its analogue — a class either
@@ -7,6 +7,7 @@ require_relative "../incremental_run_slot"
7
7
  require_relative "../dependency_source_inference/gem_resolver"
8
8
  require_relative "../../cache/file_digest"
9
9
  require_relative "../../cache/incremental_snapshot"
10
+ require_relative "../../rooted_glob"
10
11
 
11
12
  module Rigor
12
13
  module Analysis
@@ -66,8 +67,8 @@ module Rigor
66
67
  # opted-in gems' resolved versions, the key's one input that is neither a file nor the configuration (issue
67
68
  # #1814), are read at the mark and watched the same way.
68
69
  #
69
- # The mark also records which of the paths an existence row can name are present: the lockfiles, the
70
- # analysis roots, the `pre_eval:` entries and the signature roots.
70
+ # The mark also records what each path a presence row can name is ({Cache::Descriptor::FileEntry.kind_of}):
71
+ # the lockfiles, the analysis roots, the `pre_eval:` entries and the signature roots.
71
72
  def self.start(configuration:, roots:, cache_root:, fingerprint:)
72
73
  return nil if Gem.win_platform?
73
74
 
@@ -79,7 +80,7 @@ module Rigor
79
80
  lockfiles = lockfile_paths(configuration)
80
81
  named = Array(roots) + configuration.pre_eval + (configuration.signature_paths || ["sig"])
81
82
  presence = (lockfiles + named.map { |path| File.absolute_path(path.to_s) }).to_h do |path|
82
- [path, File.exist?(path)]
83
+ [path, Cache::Descriptor::FileEntry.kind_of(path)]
83
84
  end
84
85
  rows = dependency_rows(configuration)
85
86
  return nil if rows.nil?
@@ -174,11 +175,12 @@ module Rigor
174
175
  # - no lockfile the key or the fingerprint digests was written, created or removed, and no opted-in gem
175
176
  # resolves to another version than at the mark (issue #1814);
176
177
  # - no file `rows` or `pinned` names changed. A content row's file changed if its change time moved. An
177
- # existence row asks only whether its path is there, so a path the mark recorded changed if it came or
178
- # went, and an editor's lock file created and removed beside it, which moves its directory's change
179
- # time, does not count. That needs no clock, so it holds on any filesystem; it cannot see a path that
180
- # comes and goes again within the run. Any other existence row falls back to change times: its own when
181
- # present, its nearest existing ancestor's when absent;
178
+ # existence row asks only whether its path is there, and a `:kind` row what it is (#1977), so a path the
179
+ # mark recorded changed if it came or went, or for a kind row turned from a directory into a file or back;
180
+ # an editor's lock file created and removed beside it, which moves its directory's change time, does not
181
+ # count. That needs no clock, so it holds on any filesystem; it cannot see a path that comes and goes
182
+ # again within the run. Any other presence row falls back to change times: its own when present, its
183
+ # nearest existing ancestor's when absent;
182
184
  # - no directory a glob row lists gained or lost an entry, nor, for a stat-mode glob, did any file it
183
185
  # matches change.
184
186
  #
@@ -231,15 +233,20 @@ module Rigor
231
233
  ns.nil? || device != @device || ns < @started_ns
232
234
  end
233
235
 
234
- # An existence row and a content row for one path are different questions (the first asks only whether it
235
- # is there, the second whether it changed), so the row that comes first must not hide the other.
236
+ # A presence row and a content row for one path are different questions (the first asks only whether it
237
+ # is there, or what it is, the second whether it changed), so the row that comes first must not hide the
238
+ # other; nor may an existence row hide a kind row.
239
+ PRESENCE_COMPARATORS = %i[exists kind].freeze
240
+ private_constant :PRESENCE_COMPARATORS
241
+
236
242
  def files_unchanged?(entries, strict:)
237
243
  seen = Set.new
238
244
  entries.none? do |entry|
239
- next false unless seen.add?([entry.path, entry.comparator == :exists])
245
+ presence = PRESENCE_COMPARATORS.include?(entry.comparator) ? entry.comparator : nil
246
+ next false unless seen.add?([entry.path, presence])
240
247
 
241
- if entry.comparator == :exists
242
- existence_changed?(entry.path, strict: strict)
248
+ if presence
249
+ presence_changed?(entry.path, presence, strict: strict)
243
250
  else
244
251
  changed?(entry.path, strict: strict)
245
252
  end
@@ -248,8 +255,8 @@ module Rigor
248
255
 
249
256
  def globs_unchanged?(entries, strict:)
250
257
  entries.none? do |entry|
251
- listed = Dir.glob(File.join(entry.root, File.dirname(entry.pattern), ""))
252
- matched = entry.mode == :stat ? Dir.glob(File.join(entry.root, entry.pattern)) : []
258
+ listed = RootedGlob.glob(entry.root, File.join(File.dirname(entry.pattern), ""))
259
+ matched = entry.mode == :stat ? RootedGlob.glob(entry.root, entry.pattern) : []
253
260
  (listed + matched).any? { |path| changed?(path, strict: strict) }
254
261
  end
255
262
  end
@@ -257,14 +264,25 @@ module Rigor
257
264
  # A lockfile is read for its bytes: one absent at the mark must still be absent, one present must still be
258
265
  # there with its change time before the mark.
259
266
  def lockfile_changed?(path)
260
- return File.exist?(path) unless @presence[path]
267
+ return File.exist?(path) unless present_at_mark?(path)
261
268
 
262
269
  changed?(path, strict: true, missing: true)
263
270
  end
264
271
 
265
- def existence_changed?(path, strict:)
272
+ def present_at_mark?(path)
273
+ kind = @presence[path]
274
+ !kind.nil? && kind != Cache::Descriptor::FileEntry::KIND_ABSENT
275
+ end
276
+
277
+ # `comparator` is `:exists` or `:kind`: whether the path came or went, or whether it is no longer the kind
278
+ # of thing the mark recorded.
279
+ def presence_changed?(path, comparator, strict:)
266
280
  absolute = File.absolute_path(path)
267
- return File.exist?(absolute) != @presence[absolute] if @presence.key?(absolute)
281
+ if @presence.key?(absolute)
282
+ return File.exist?(absolute) != present_at_mark?(absolute) if comparator == :exists
283
+
284
+ return Cache::Descriptor::FileEntry.kind_of(absolute) != @presence[absolute]
285
+ end
268
286
 
269
287
  nearest = absolute
270
288
  nearest = File.dirname(nearest) until File.exist?(nearest) || File.dirname(nearest) == nearest