rigortype 0.4.1 → 0.4.3

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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -1
@@ -16,7 +16,8 @@ module Rigor
16
16
  # back-reference cycle.
17
17
  class RunSnapshots
18
18
  attr_accessor :class_decl_paths, :signature_paths,
19
- :synthesized_namespaces, :quarantined_signatures, :signature_standdowns, :conformance_results,
19
+ :synthesized_namespaces, :quarantined_signatures, :signature_standdowns, :vendored_standdowns,
20
+ :conformance_results,
20
21
  :env_build_failure, :definition_build_failures, :hkt_scan_failure,
21
22
  :effect_annotation_carrier
22
23
 
@@ -32,12 +33,15 @@ module Rigor
32
33
  # `signature_standdowns` (#610) is the quarantine slot's twin for the plugin-contributed files that
33
34
  # stood down against a colliding generic arity: a list, assigned once from the loader as
34
35
  # `quarantined_signatures` is, and derived from the final env so a cache HIT carries it too.
36
+ # `vendored_standdowns` (#1713) is the same shape of slot for what a required-feature-gated vendored
37
+ # directory left out because another signature source declares it.
35
38
  def initialize
36
39
  @class_decl_paths = {}.freeze
37
40
  @signature_paths = [].freeze
38
41
  @synthesized_namespaces = [].freeze
39
42
  @quarantined_signatures = [].freeze
40
43
  @signature_standdowns = [].freeze
44
+ @vendored_standdowns = [].freeze
41
45
  @conformance_results = [].freeze
42
46
  @env_build_failure = nil
43
47
  @definition_build_failures = [].freeze
@@ -53,6 +57,7 @@ module Rigor
53
57
  @synthesized_namespaces = []
54
58
  @quarantined_signatures = []
55
59
  @signature_standdowns = []
60
+ @vendored_standdowns = []
56
61
  @conformance_results = []
57
62
  @env_build_failure = nil
58
63
  @definition_build_failures = []
@@ -297,8 +297,13 @@ module Rigor
297
297
  record_dependencies: false, record_self_calls: false, analyze_only: nil,
298
298
  seed_bundles: nil, collect_seed_bundles: false, param_inferred_types: nil,
299
299
  discovery_seed: nil, no_tolerated_effects: false,
300
- restored_run_level_rows: nil, served_per_file_diagnostics: nil)
300
+ restored_run_level_rows: nil, served_per_file_diagnostics: nil, on_file_indexed: nil)
301
301
  @configuration = configuration
302
+ # ADR-113 WD2 — `rigor lens` reads a target's seeded index here: called with `(path, parse_result, scope,
303
+ # index)` for every file this process indexes, after its rules ran and inside the run window, so the reader
304
+ # sees what `check` saw. A pooled file is indexed in a worker and never reaches it; the incremental session
305
+ # analyses forced targets with `workers: 0` for that reason.
306
+ @on_file_indexed = on_file_indexed
302
307
  # Issues #796 / #794 — the previous full run's run-level rows, supplied only by
303
308
  # {IncrementalSession} on a narrowed run; see {PoolCoordinator#replay_restored_run_level_rows}.
304
309
  @restored_run_level_rows = restored_run_level_rows
@@ -431,6 +436,7 @@ module Rigor
431
436
  # Issue #1367 — the project's `Inference::GlobalWriteCensus`, which the `global.*` write rules read.
432
437
  @project_discovered_global_write_census = Set.new.freeze
433
438
  @project_discovered_class_sources = {}.freeze
439
+ @project_singleton_class_sources = {}.freeze
434
440
  # Issue #644 — the cross-file VALUE-constant publication table (`{qualified name => Type::Constant}`,
435
441
  # literal writes only) and its per-name write attribution. The first seeds `in_source_constants` on
436
442
  # every per-file scope so `FOO = :sym` in one file reads as `:sym` in another; the second seeds
@@ -575,6 +581,12 @@ module Rigor
575
581
  @in_memory_sources = nil
576
582
  end
577
583
 
584
+ # Issue #1700 — the sources the required-feature scan reads in place of a file on disk: an in-memory
585
+ # source ({#run_source}) and an editor buffer (`--tmp-file` read for its `--instead-of` path).
586
+ def required_feature_sources
587
+ (@in_memory_sources || {}).merge(ProjectEnvironment.buffer_sources(@buffer))
588
+ end
589
+
578
590
  # ADR-46 — the project file set that a run over `paths` would analyze, computed by globbing only (no
579
591
  # RBS environment build), so the incremental fingerprint can be derived cheaply on the warm path
580
592
  # before deciding whether to build the env at all.
@@ -636,11 +648,16 @@ module Rigor
636
648
  # detect a class that *appeared* in an edit so a subclass whose ancestor was previously undefined
637
649
  # (and so recorded a negative class edge) is re-checked. Inverts the project class-source attribution
638
650
  # (class → declaring files).
651
+ #
652
+ # Issue #1820 — each file's set also holds a `[class, superclass as written]` pair for every class it declares
653
+ # with a superclass, so a subclass appearing or vanishing re-checks the consumers that enumerated the parent's
654
+ # subclasses (`subclass_of:<last segment>`).
639
655
  def class_declarations
640
656
  result = Hash.new { |hash, key| hash[key] = Set.new }
641
- @project_discovered_class_sources.each do |class_name, files|
642
- files.each { |file| result[file] << class_name }
657
+ [@project_discovered_class_sources, @project_singleton_class_sources].each do |sources|
658
+ sources.each { |class_name, files| files.each { |file| result[file] << class_name } }
643
659
  end
660
+ Incremental.add_superclass_edges(result, @project_discovered_class_sources, @project_discovered_superclasses)
644
661
  result.transform_values(&:freeze).freeze
645
662
  end
646
663
 
@@ -1737,6 +1754,8 @@ module Rigor
1737
1754
  @project_discovered_prepends = discovery.discovered_prepends
1738
1755
  @project_discovered_extends = discovery.discovered_extends
1739
1756
  @project_unpositioned_mixins = discovery.unpositioned_mixins
1757
+ # Issue #1752 — the files that open `class << Const`, read only by {#class_declarations}.
1758
+ @project_singleton_class_sources = discovery.singleton_class_sources
1740
1759
  end
1741
1760
 
1742
1761
  # Internal: builds the deferred cross-file discovery tables at most once per run and adopts them.
@@ -1793,10 +1812,8 @@ module Rigor
1793
1812
  # the pre-pass then saw two writers for every constant a file declares and withheld its publication (#644),
1794
1813
  # and every file was a discovered-not-analysed dependency of the run's cache entry.
1795
1814
  #
1796
- # The widening decision itself still compares the paths as spelled. `exclude:` patterns match a path as it
1797
- # is spelled, so until #1576 is fixed the configured absolute `lib` can discover files the argument `lib`
1798
- # excludes (a relative `lib/gen/**` pattern never matches `/abs/lib/gen/x.rb`), and treating the two as the
1799
- # same root would drop those files from discovery.
1815
+ # The widening decision itself compares the paths as spelled. `exclude:` patterns apply to both spellings
1816
+ # alike (#1576), so the configured absolute `lib` and the argument `lib` exclude the same files.
1800
1817
  def project_discovery_expansion(paths, expansion)
1801
1818
  return nil unless widen_discovery_to_project?(paths)
1802
1819
 
@@ -2005,7 +2022,9 @@ module Rigor
2005
2022
  project_scope_seed: -> { project_scope_seed_tables },
2006
2023
  template_units: -> { template_units },
2007
2024
  analyze_file: ->(path, environment) { analyze_file(path, environment) },
2008
- restored_run_level_rows: @restored_run_level_rows
2025
+ restored_run_level_rows: @restored_run_level_rows,
2026
+ feature_sources: -> { required_feature_sources },
2027
+ plugin_prepare: -> { @pre_passes.prepare_throwaway_registry }
2009
2028
  )
2010
2029
  @diagnostic_aggregator = DiagnosticAggregator.new(
2011
2030
  configuration: @configuration,
@@ -2020,6 +2039,7 @@ module Rigor
2020
2039
  synthesized_namespaces_snapshot: -> { @snapshots.synthesized_namespaces },
2021
2040
  quarantined_signatures_snapshot: -> { @snapshots.quarantined_signatures },
2022
2041
  signature_standdowns_snapshot: -> { @snapshots.signature_standdowns },
2042
+ vendored_standdowns_snapshot: -> { @snapshots.vendored_standdowns },
2023
2043
  env_build_failure_snapshot: -> { @snapshots.env_build_failure },
2024
2044
  definition_build_failures_snapshot: -> { @snapshots.definition_build_failures },
2025
2045
  hkt_scan_failure_snapshot: -> { @snapshots.hkt_scan_failure },
@@ -2050,7 +2070,8 @@ module Rigor
2050
2070
  )
2051
2071
  RunStats.new(
2052
2072
  wall_seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC) - wall_started_at,
2053
- peak_rss_bytes: RunStats.peak_rss_bytes,
2073
+ # Issue #1529 — read lazily on the summary's first use, so a run that prints none never forks for it.
2074
+ peak_rss_probe: RunStats::CURRENT_PROCESS_PEAK_RSS,
2054
2075
  # #392 — the template units count too: they are files this run parsed and typed, and a stats line
2055
2076
  # that omitted them would under-report exactly the work a views-heavy project added.
2056
2077
  target_files: expansion.fetch(:files).size + template_units.paths.size,
@@ -2163,7 +2184,8 @@ module Rigor
2163
2184
  bad = []
2164
2185
  Array(paths).each do |path|
2165
2186
  if File.directory?(path)
2166
- files.concat(PathExpansion.directory_files(path, @configuration.exclude_patterns))
2187
+ files.concat(PathExpansion.directory_files(path, @configuration.exclude_patterns,
2188
+ base: @configuration.exclude_base))
2167
2189
  # Editor-mode bypass: the buffer's logical path is treated as a real `.rb` file regardless of
2168
2190
  # on-disk presence — `parse_source` reads bytes from the buffer's physical path. Common case: LSP
2169
2191
  # client editing a brand-new file.
@@ -2295,13 +2317,30 @@ module Rigor
2295
2317
  # global-write census (the `global.*` write rules); the issue #992 envelopes ride {#seed_paired_tables}. Split
2296
2318
  # out of {#project_scope_seed_tables} to keep it under the complexity budget.
2297
2319
  def seed_call_surface_tables(tables)
2298
- tables[:discovered_refinements] = @project_discovered_refinements unless @project_discovered_refinements.empty?
2320
+ refinements = seed_refinements(tables)
2321
+ tables[:discovered_refinements] = refinements unless refinements.nil? || refinements.empty?
2299
2322
  # Issue #1367 — read by the `global.*` write rules.
2300
2323
  return if @project_discovered_global_write_census.empty?
2301
2324
 
2302
2325
  tables[:discovered_global_write_census] = @project_discovered_global_write_census
2303
2326
  end
2304
2327
 
2328
+ # Issue #1672 — the project's refinements (this run's discovery, else an opt-in `discovery_seed:` base's)
2329
+ # unioned with the opt-in gems' ({DependencySourceInference::Index#refinements}). The gem half comes from
2330
+ # the pre-passes every run takes, warm or cold, so a file re-analysed warm is seeded what a cold run seeds.
2331
+ # The seed is rebuilt per file, so the union is kept while its two inputs are the same objects.
2332
+ def seed_refinements(tables)
2333
+ project = @project_discovered_refinements
2334
+ project = tables[:discovered_refinements] if project.empty?
2335
+ gems = @dependency_source_index.refinements
2336
+ memo = @seed_refinements
2337
+ return memo[2] if memo && memo[0].equal?(project) && memo[1].equal?(gems)
2338
+
2339
+ merged = Inference::ScopeIndexer.merge_refinement_tables(project, gems)
2340
+ @seed_refinements = [project, gems, merged]
2341
+ merged
2342
+ end
2343
+
2305
2344
  # The three mixin tables: the ADR-24 instance-side `include` map, its issue #1123 `prepend` twin
2306
2345
  # (which carries the instance-ancestor order the include table's set-shaped value cannot), and the
2307
2346
  # #898 singleton-side `extend` table. Grouped here because they are read as one picture of a class's
@@ -2561,10 +2600,11 @@ module Rigor
2561
2600
  # the rule is active or `record_self_calls:` opted in.
2562
2601
  index = nil
2563
2602
  self_call_record = with_self_call_recording(path) do
2564
- index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
2603
+ index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope, widen_clauses: true)
2565
2604
  end
2566
2605
  self_call_misses = self_call_record ? self_call_record.calls : []
2567
2606
  diagnostics = rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
2607
+ notify_file_indexed(path, parse_result, scope, index)
2568
2608
  diagnostics + explain_diagnostics(path, parse_result.value, scope)
2569
2609
  rescue Errno::ENOENT => e
2570
2610
  [
@@ -2588,6 +2628,14 @@ module Rigor
2588
2628
  ]
2589
2629
  end
2590
2630
 
2631
+ # ADR-113 WD2 — the `on_file_indexed` observer. It reads; an error inside it never changes this file's
2632
+ # diagnostics, so it is contained here rather than reaching {#analyze_file_body}'s crash row.
2633
+ def notify_file_indexed(path, parse_result, scope, index)
2634
+ @on_file_indexed&.call(path, parse_result, scope, index)
2635
+ rescue StandardError
2636
+ nil
2637
+ end
2638
+
2591
2639
  # ADR-53 B4 — the built-in node collectors and the plugin node rules share ONE traversal of the file.
2592
2640
  # The collectors are built here (they need the completed `index`) and populated by the converged
2593
2641
  # plugin walk; `node_results` carries the per-plugin node-rule output. Both the built-in `diagnose`
@@ -2603,7 +2651,8 @@ module Rigor
2603
2651
  self_call_misses: self_call_misses,
2604
2652
  comments: parse_result.comments,
2605
2653
  disabled_rules: @configuration.disabled_rules,
2606
- node_collectors: node_collectors
2654
+ node_collectors: node_collectors,
2655
+ stated_runtime_ruby: @configuration.stated_runtime_ruby
2607
2656
  )
2608
2657
  diagnostics + plugin_emitted_diagnostics(path, root, scope, node_results)
2609
2658
  end
@@ -86,7 +86,7 @@ module Rigor
86
86
  plugin_blueprints: [], explain: false, buffer: nil,
87
87
  synthetic_method_index: nil, project_patched_methods: nil,
88
88
  project_scope_seed: {}, source_files: [], record_dependencies: false,
89
- template_units: nil, locked_gems: nil)
89
+ template_units: nil, locked_gems: nil, required_features: nil)
90
90
  @configuration = configuration
91
91
  @cache_store = cache_store
92
92
  @explain = explain
@@ -119,7 +119,9 @@ module Rigor
119
119
  # RubyGems module state a non-main Ractor may not touch, so a Ractor worker that resolved it here got
120
120
  # `Ractor::IsolationError` and an empty map — no gem `sig/` directories, no ADR-72 overlays. nil (the
121
121
  # fork backend and every other caller) resolves inside `Environment.for_project` exactly as before.
122
- @locked_gems = locked_gems
122
+ # Issue #1700 — `required_features` is the coordinator's scan ({Runner::PoolCoordinator#required_features_for});
123
+ # nil scans `source_files` inside `Environment.for_project`.
124
+ @environment_inputs = { locked_gems: locked_gems, required_features: required_features }.freeze
123
125
 
124
126
  # NOTE: `Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths` is process-global
125
127
  # state. Writing it from a non-main Ractor would raise `Ractor::IsolationError`, so the session does
@@ -153,7 +155,7 @@ module Rigor
153
155
  synthetic_method_index: @synthetic_method_index,
154
156
  project_patched_methods: @project_patched_methods,
155
157
  source_files: @source_files,
156
- locked_gems: @locked_gems
158
+ **@environment_inputs
157
159
  )
158
160
  @prepare_diagnostics = run_plugin_prepare.freeze
159
161
  end
@@ -226,7 +228,7 @@ module Rigor
226
228
  scope = @template_units.seed(
227
229
  seed_project_scope(Scope.empty(environment: @environment, source_path: path)), path
228
230
  )
229
- index = Inference::ScopeIndexer.index(root, default_scope: scope)
231
+ index = Inference::ScopeIndexer.index(root, default_scope: scope, widen_clauses: true)
230
232
  # ADR-53 B4 — built-in collectors + plugin node rules share one walk.
231
233
  node_collectors = CheckRules.build_node_collectors(path, index, root)
232
234
  node_results = node_rule_results_by_plugin(path, root, scope, node_collectors, index)
@@ -236,7 +238,8 @@ module Rigor
236
238
  scope_index: index,
237
239
  comments: parse_result.comments,
238
240
  disabled_rules: @configuration.disabled_rules,
239
- node_collectors: node_collectors
241
+ node_collectors: node_collectors,
242
+ stated_runtime_ruby: @configuration.stated_runtime_ruby
240
243
  )
241
244
  diagnostics += plugin_emitted_diagnostics(path, root, scope, node_results)
242
245
  diagnostics + explain_diagnostics(path, parse_result.value, scope)
@@ -59,6 +59,19 @@ module Rigor
59
59
  # `apps/` and `tool/` are not loaded by an analysis and stay out.
60
60
  SOURCE_DIRECTORIES = %w[lib plugins].freeze
61
61
 
62
+ # Engine DATA the analysis reads at runtime, relative to the gem root (issue #1492). A data edit moves
63
+ # diagnostics as surely as a `lib/` edit, yet the caches that key only on `signature_paths` plus this
64
+ # digest (the `--incremental` snapshot fingerprint, `Protection::MutationCache`) see nothing else.
65
+ #
66
+ # - YAML: `data/builtins` (`MethodCatalog::DATA_ROOT`) and `data/effects` (`Effects::Catalog`,
67
+ # `Effects::Registry`).
68
+ # - RBS: every `.rbs` under `data/` (`core_overlay`, `vendored_gem_sigs`, `capability_roles`,
69
+ # `gem_overlay`) plus each bundled plugin's `plugins/*/sig`. `RbsDescriptor` digests the same files
70
+ # into the run-result and env caches, but those are the only two consumers that do.
71
+ #
72
+ # Tooling-only trees (`data/oss-sweep`) are not read by an analysis and stay out.
73
+ DATA_GLOBS = %w[data/builtins/**/*.yml data/effects/**/*.yml data/**/*.rbs plugins/*/sig/**/*.rbs].freeze
74
+
62
75
  # The one directory whose absence means "this is not an engine tree". `plugins/` may legitimately be
63
76
  # missing (a slimmed install contributes nothing); a missing `lib/` would silently shrink the digest
64
77
  # to whatever else happened to be there, which is exactly the weakening this module forbids.
@@ -153,8 +166,9 @@ module Rigor
153
166
  !File.exist?(File.join(root, ".git"))
154
167
  end
155
168
 
156
- # A SHA-256 over every engine `.rb` file: its ROOT-RELATIVE path (so the digest survives moving or
157
- # re-cloning the checkout) followed by its bytes, in sorted path order.
169
+ # A SHA-256 over every engine `.rb` file and {DATA_GLOBS} data file: its ROOT-RELATIVE
170
+ # path (so the digest survives moving or re-cloning the checkout) followed by its bytes, in sorted path
171
+ # order.
158
172
  #
159
173
  # The walk itself is not memoised — {.process_identity} is where a production caller gets the
160
174
  # once-per-process value, and this stays the computation so a spec can point it at another tree.
@@ -178,12 +192,12 @@ module Rigor
178
192
  raise Unavailable, "engine source under #{root} could not be read: #{e.message}"
179
193
  end
180
194
 
181
- # Sorted absolute paths of every engine `.rb` file, across the directories that exist.
195
+ # Sorted, de-duplicated absolute paths of every engine `.rb` file and {DATA_GLOBS} data file, across
196
+ # the directories that exist. `plugins/*/sig/**/*.rbs` overlaps nothing else but is de-duplicated anyway.
182
197
  def source_files(root)
183
- SOURCE_DIRECTORIES.flat_map do |relative|
184
- directory = File.join(root, relative)
185
- File.directory?(directory) ? Dir.glob(File.join(directory, "**", "*.rb")) : []
186
- end.sort
198
+ sources = SOURCE_DIRECTORIES.flat_map { |relative| Dir.glob(File.join(root, relative, "**", "*.rb")) }
199
+ data = DATA_GLOBS.flat_map { |pattern| Dir.glob(File.join(root, pattern)) }
200
+ (sources + data).uniq.sort
187
201
  end
188
202
  end
189
203
  end
@@ -9,6 +9,7 @@ require_relative "store"
9
9
  require_relative "../analysis/shadow_harness"
10
10
  require_relative "../analysis/run_cache_key"
11
11
  require_relative "../environment"
12
+ require_relative "../analysis/path_expansion"
12
13
 
13
14
  module Rigor
14
15
  module Cache
@@ -259,7 +260,11 @@ module Rigor
259
260
  # {EngineSource.process_identity} closes it, and answers nil for a version-pinned tree — which adds no
260
261
  # part, so a released gem's fingerprint is byte-identical to the pre-#289 one and its warm snapshots
261
262
  # survive the upgrade untouched.
262
- def self.fingerprint(configuration:, roots:)
263
+ #
264
+ # Issue #1533 — `explain:` is the run's `--explain` flag. It adds `:info` rows to the per-file diagnostics
265
+ # the snapshot caches, so a snapshot written without it holds none and one written with it holds rows a
266
+ # plain run must not print. No part for the default, so every plain run's fingerprint is as it was.
267
+ def self.fingerprint(configuration:, roots:, explain: false)
263
268
  bundler_lock, collection_lock = Analysis::RunCacheKey.resolved_lockfile_paths(configuration)
264
269
  parts = [
265
270
  "engine:#{Rigor::VERSION}:#{SCHEMA}",
@@ -270,12 +275,19 @@ module Rigor
270
275
  "sig:#{digest_signature_paths(project_sig_roots(configuration))}",
271
276
  "pre_eval:#{digest_pre_eval(configuration.pre_eval)}"
272
277
  ]
278
+ parts.concat(dependency_source_parts(configuration))
273
279
  identity = EngineSource.process_identity
274
280
  parts << "engine-source:#{identity}" if identity
281
+ # Issue #1700 — the gated vendored signatures the run's source activates. A file that gains or drops a
282
+ # `require "prime"` changes the environment every other file was analysed against, which no per-file
283
+ # dependency edge records, so the whole snapshot goes. Scanned over the configured paths plus the roots'
284
+ # files, the set the run's environment scans; no part when nothing is required, so every other
285
+ # project's fingerprint is as it was.
286
+ features = required_features(configuration, roots)
287
+ parts << "required-features:#{features.join(',')}" unless features.empty?
275
288
  # ADR-116 WD5 — the snapshot's per-file rows and seed bundles are only as checked as the run that
276
289
  # wrote them ({Inference::DeclarationWalk::Shadow}); nil with the harness off adds no part.
277
- shadow = Analysis::ShadowHarness.cache_identity
278
- parts << "shadow-harness:#{shadow}" if shadow
290
+ parts.concat(run_mode_parts(explain))
279
291
  Digest::SHA256.hexdigest(parts.join("\x00"))
280
292
  rescue StandardError
281
293
  # {EngineSource::Unavailable} lands here too, and nil is the answer it requires rather than one it
@@ -286,12 +298,38 @@ module Rigor
286
298
  nil
287
299
  end
288
300
 
301
+ def self.required_features(configuration, roots)
302
+ files = Analysis::PathExpansion.ruby_files(roots, configuration.exclude_patterns,
303
+ base: configuration.exclude_base)
304
+ Environment::RequiredFeatures.for_configuration(configuration, files)
305
+ end
306
+
307
+ # ADR-10 slice 3, issue #1814 — the walked gems' versions: their method catalogue, their refinement table and the
308
+ # constants they bind reach every file's checks, and the lockfile digest above does not cover a project with none.
309
+ # No part for a project that lists no gem, so its fingerprint is what it was.
310
+ def self.dependency_source_parts(configuration)
311
+ rows = Analysis::RunCacheKey.dependency_source_entries(configuration).map do |row|
312
+ [row.gem_name, row.gem_version, row.mode]
313
+ end
314
+ rows.empty? ? [] : ["dependency-source:#{rows.sort.inspect}"]
315
+ end
316
+ private_class_method :dependency_source_parts
317
+
289
318
  def self.digest_file_if_present(path)
290
319
  path = path&.to_s
291
320
  path && File.file?(path) ? Digest::SHA256.file(path).hexdigest : "absent"
292
321
  end
293
322
  private_class_method :digest_file_if_present
294
323
 
324
+ # The parts naming how the run was asked to behave rather than what it reads: the ADR-116 WD5 shadow harness
325
+ # (the snapshot's rows are only as checked as the run that wrote them; nil with the harness off) and
326
+ # `--explain` (#1533).
327
+ def self.run_mode_parts(explain)
328
+ shadow = Analysis::ShadowHarness.cache_identity
329
+ [("shadow-harness:#{shadow}" if shadow), ("explain:true" if explain)].compact
330
+ end
331
+ private_class_method :run_mode_parts
332
+
295
333
  # Content-digest every `pre_eval:` file in configuration order, so an edit to one invalidates the snapshot
296
334
  # even when it lies outside the analysed paths. A missing file digests as "absent", so creating it counts too.
297
335
  def self.digest_pre_eval(paths)
@@ -324,8 +362,20 @@ module Rigor
324
362
  end
325
363
  private_class_method :digest_signature_paths
326
364
 
327
- def initialize(root:)
328
- @path = File.join(root.to_s, "incremental", "snapshot.bin")
365
+ # Issue #1867 — one snapshot FILE per fingerprint mode. A run whose fingerprint carries run-mode parts
366
+ # (`--explain`, the ADR-116 shadow harness) keeps its own file so it neither reads nor overwrites the
367
+ # plain run's warm snapshot, and the reverse. The plain file name is unchanged. The fingerprint still
368
+ # carries the mode parts, so the files never cross-serve.
369
+ def initialize(root:, explain: false)
370
+ @path = File.join(root.to_s, "incremental", self.class.file_name(explain: explain))
371
+ end
372
+
373
+ # `snapshot.bin` for a plain run; `snapshot-<mode digest>.bin` when {run_mode_parts} is non-empty.
374
+ def self.file_name(explain: false)
375
+ parts = run_mode_parts(explain)
376
+ return "snapshot.bin" if parts.empty?
377
+
378
+ "snapshot-#{Digest::SHA256.hexdigest(parts.join("\x00"))[0, 12]}.bin"
329
379
  end
330
380
 
331
381
  attr_reader :path
@@ -100,7 +100,12 @@ module Rigor
100
100
  # in `#annotate` raise `invalid byte sequence in US-ASCII`. Ruby source is UTF-8 by convention (the parser's own
101
101
  # assumption absent a magic comment).
102
102
  source = File.read(file, mode: "r:bom|utf-8")
103
+ # Prism parses the ORIGINAL bytes, so a `# -*- coding: -*-` file is read in its own encoding. Only the text the
104
+ # line-wise regexps below see is scrubbed: a byte invalid in UTF-8 (a comment, `__END__` data, a non-UTF-8
105
+ # file) would make them raise.
106
+ # Scrubbing is byte-for-byte (`?` per bad byte), so Prism's positions still address it.
103
107
  parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
108
+ source = source.scrub { |bytes| "?" * bytes.bytesize } unless source.valid_encoding?
104
109
  return 1 if parse_errors?(parse_result, file)
105
110
 
106
111
  # `converged_loop_recording` re-records fixpoint-tracked loop bodies from their converged (post-writeback)
@@ -126,8 +126,10 @@ module Rigor
126
126
 
127
127
  def collect_diagnostics(configuration, _options)
128
128
  cache_store = Cache::Store.new(root: configuration.cache_path)
129
- # IMPORTANT: do NOT activate the existing baseline when generating a fresh one — otherwise the new file records
130
- # the post-filter (silenced) diagnostic set, which is empty after a successful first run.
129
+ # The generating run is the loaded configuration with `baseline:` off (#1777): every other key shapes the
130
+ # diagnostic set `rigor check` reports, so it must shape the recorded one too. The baseline filter itself is
131
+ # applied CLI-side by `check` (`CheckCommand#apply_baseline_filter`), never by the runner; clearing it here is
132
+ # defensive.
131
133
  configuration_for_generation = override_configuration_baseline_off(configuration)
132
134
  runner = Analysis::Runner.new(
133
135
  configuration: configuration_for_generation,
@@ -137,23 +139,10 @@ module Rigor
137
139
  runner.run(configuration_for_generation.paths).diagnostics
138
140
  end
139
141
 
142
+ # The loaded configuration with only `baseline` disabled (#1777): any other key can change the
143
+ # diagnostic set, and the baseline must record the set `rigor check` will then report.
140
144
  def override_configuration_baseline_off(configuration)
141
- # Synthesise a new Configuration with `baseline` explicitly disabled. The original Configuration is frozen-ish
142
- # so we round-trip through the constructor with an override hash.
143
- defaults = Configuration::DEFAULTS.merge(
144
- "paths" => configuration.paths,
145
- "exclude" => configuration.exclude_patterns,
146
- "plugins" => configuration.plugins,
147
- "disable" => configuration.disabled_rules,
148
- "libraries" => configuration.libraries,
149
- "signature_paths" => configuration.signature_paths,
150
- "pre_eval" => configuration.pre_eval,
151
- "severity_profile" => configuration.severity_profile.to_s,
152
- "severity_overrides" => configuration.severity_overrides,
153
- "baseline" => false,
154
- "cache" => { "path" => configuration.cache_path }
155
- )
156
- Configuration.new(defaults)
145
+ configuration.without_baseline
157
146
  end
158
147
 
159
148
  # ---- dump --------------------------------------------------