rigortype 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) 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 +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -36,6 +36,8 @@ module Rigor
36
36
  # that stood down against a colliding generic arity (#610), as
37
37
  # `[path, class_name, existing_arity, incoming_arity, existing_file]` tuples. Defaults to none so
38
38
  # a caller that snapshots nothing of the kind need not say so.
39
+ # @param vendored_standdowns_snapshot — issue #1713 — reader returning what each required-feature-gated
40
+ # vendored directory left out (`Environment::RbsLoader#vendored_standdowns`). Defaults to none.
39
41
  # @param env_build_failure_snapshot — reader returning the total RBS env-build failure tuple
40
42
  # (`[error_class, first_error_line, conflicting_buffer_names]`) or nil when the env built.
41
43
  # @param definition_build_failures_snapshot — issue #696 — reader returning the per-class
@@ -55,7 +57,7 @@ module Rigor
55
57
  quarantined_signatures_snapshot:, env_build_failure_snapshot:,
56
58
  definition_build_failures_snapshot:, hkt_scan_failure_snapshot:,
57
59
  conformance_results_snapshot:, signature_standdowns_snapshot: -> { [] },
58
- pooled_run_disclosures: -> { [] })
60
+ vendored_standdowns_snapshot: -> { [] }, pooled_run_disclosures: -> { [] })
59
61
  @configuration = configuration
60
62
  @rbs_extended_reporter = rbs_extended_reporter
61
63
  @boundary_cross_reporter = boundary_cross_reporter
@@ -68,6 +70,7 @@ module Rigor
68
70
  @synthesized_namespaces_snapshot_reader = synthesized_namespaces_snapshot
69
71
  @quarantined_signatures_snapshot_reader = quarantined_signatures_snapshot
70
72
  @signature_standdowns_snapshot_reader = signature_standdowns_snapshot
73
+ @vendored_standdowns_snapshot_reader = vendored_standdowns_snapshot
71
74
  @env_build_failure_snapshot_reader = env_build_failure_snapshot
72
75
  @definition_build_failures_snapshot_reader = definition_build_failures_snapshot
73
76
  @hkt_scan_failure_snapshot_reader = hkt_scan_failure_snapshot
@@ -437,11 +440,23 @@ module Rigor
437
440
  standdowns.map { |entry| build_rbs_plugin_signature_stood_down_diagnostic(entry) }
438
441
  end
439
442
 
440
- # The two `:info` notices that close the `rbs.coverage.*` ladder, in this order: the namespace
441
- # synthesis first, then the stand-down (#610) — the outcome that AVOIDED a definition-build failure
442
- # and so the quietest row on it. One method so the runner's assembly reads them as one slot.
443
+ # Issue #1713 — the other outcome that avoided a definition-build failure: a required-feature-gated
444
+ # vendored directory (`data/vendored_gem_sigs/prime/`) left out what another signature source already
445
+ # declares — a member, a whole type, or, when nothing narrower built, the directory. One `:info` per
446
+ # directory, naming the declarations that stood down and the files that displaced them.
447
+ def rbs_vendored_signature_stood_down_diagnostics
448
+ standdowns = vendored_standdowns_snapshot
449
+ return [] if standdowns.nil? || standdowns.empty?
450
+
451
+ standdowns.map { |entry| build_rbs_vendored_signature_stood_down_diagnostic(entry) }
452
+ end
453
+
454
+ # The `:info` notices that close the `rbs.coverage.*` ladder, in this order: the namespace synthesis
455
+ # first, then the stand-downs (#610, #1713) — the outcomes that AVOIDED a definition-build failure and so
456
+ # the quietest rows on it. One method so the runner's assembly reads them as one slot.
443
457
  def rbs_coverage_notice_diagnostics
444
- rbs_synthesized_namespace_diagnostics + rbs_plugin_signature_stood_down_diagnostics
458
+ rbs_synthesized_namespace_diagnostics + rbs_plugin_signature_stood_down_diagnostics +
459
+ rbs_vendored_signature_stood_down_diagnostics
445
460
  end
446
461
 
447
462
  # Maps the per-run `rigor:v1:conforms-to` scan results into diagnostics (spec: `rbs-extended.md` §
@@ -769,6 +784,69 @@ module Rigor
769
784
  )
770
785
  end
771
786
 
787
+ def build_rbs_vendored_signature_stood_down_diagnostic(entry)
788
+ dir, declarations, whole = entry
789
+ Diagnostic.new(
790
+ path: ".rigor.yml",
791
+ line: 1,
792
+ column: 1,
793
+ message: vendored_standdown_message(dir, declarations, whole),
794
+ severity: :info,
795
+ rule: "rbs.coverage.vendored-signature-stood-down",
796
+ source_family: :builtin
797
+ )
798
+ end
799
+
800
+ def vendored_standdown_message(dir, declarations, whole)
801
+ subject = "Rigor's vendored `#{dir}` signatures (loaded because the project requires `#{dir}` or lists " \
802
+ "it under `libraries:`)"
803
+ return vendored_whole_standdown_message(subject, dir, declarations) if whole
804
+
805
+ vendored_partial_standdown_message(subject, declarations)
806
+ end
807
+
808
+ # One sentence per kind of stand-down present ({Environment::GatedSignaturePlan.standdowns}' reasons).
809
+ VENDORED_STANDDOWN_SENTENCES = {
810
+ "member" => "left out %<count>d method declaration(s) another signature source also makes, since two " \
811
+ "declarations of one method fail the class's definition build, and uses the other " \
812
+ "source's: %<list>s.",
813
+ "type" => "stood %<count>d type(s) down whole, keeping only the types nested in them, because another " \
814
+ "source declares each with a different kind (class or module) or generic arity, or the type " \
815
+ "failed to build beside that source's declaration: %<list>s.",
816
+ "open" => "kept %<count>d type(s) without their superclass, because each failed to build beside the " \
817
+ "other signatures although no other source declares it; calls they no longer answer read " \
818
+ "untyped rather than undefined: %<list>s."
819
+ }.freeze
820
+ private_constant :VENDORED_STANDDOWN_SENTENCES
821
+
822
+ def vendored_partial_standdown_message(subject, declarations)
823
+ sentences = VENDORED_STANDDOWN_SENTENCES.filter_map do |reason, template|
824
+ group = declarations.select { |entry| (entry[2] || "member") == reason }
825
+ next if group.empty?
826
+
827
+ format(template, count: group.size, list: sampled(vendored_standdown_labels(group), 5))
828
+ end
829
+ "#{subject} loaded without some of their declarations. Rigor #{sentences.join(' Rigor ')} The rest of the " \
830
+ "vendored signatures still load. Nothing to fix unless the other declaration is an outdated copy; " \
831
+ "removing it brings the vendored one back."
832
+ end
833
+
834
+ def vendored_whole_standdown_message(subject, dir, declarations)
835
+ labels = vendored_standdown_labels(declarations)
836
+ clashes = labels.empty? ? "" : " Declarations it clashed with: #{sampled(labels, 5)}."
837
+ "#{subject} stood down " \
838
+ "entirely: a type they declare failed to build beside the other signature sources even with the " \
839
+ "clashing declarations left out, so calls into `#{dir}`'s API resolve only against what those " \
840
+ "sources declare.#{clashes}"
841
+ end
842
+
843
+ def vendored_standdown_labels(declarations)
844
+ declarations.map do |declaration, cause_file|
845
+ cause = cause_file.nil? ? "" : " (#{relative_signature_path(cause_file.to_s)})"
846
+ "`#{declaration}`#{cause}"
847
+ end
848
+ end
849
+
772
850
  def displacing_source_phrase(existing_file)
773
851
  return "another loaded signature source" if existing_file.nil?
774
852
 
@@ -1196,6 +1274,10 @@ module Rigor
1196
1274
  @signature_standdowns_snapshot_reader.call
1197
1275
  end
1198
1276
 
1277
+ def vendored_standdowns_snapshot
1278
+ @vendored_standdowns_snapshot_reader.call
1279
+ end
1280
+
1199
1281
  def env_build_failure_snapshot
1200
1282
  @env_build_failure_snapshot_reader.call
1201
1283
  end
@@ -45,7 +45,8 @@ module Rigor
45
45
  snapshots:, plugin_registry:, dependency_source_index:,
46
46
  synthetic_method_index:, project_patched_methods:,
47
47
  analyze_file:, project_scope_seed: -> { {} }, record_dependencies: false,
48
- restored_run_level_rows: nil, template_units: -> { TemplateUnits.empty })
48
+ restored_run_level_rows: nil, template_units: -> { TemplateUnits.empty },
49
+ feature_sources: -> { {} })
49
50
  @configuration = configuration
50
51
  @cache_store = cache_store
51
52
  @explain = explain
@@ -67,6 +68,7 @@ module Rigor
67
68
  @collected_run_disclosures = {}
68
69
  @collect_stats = collect_stats
69
70
  @buffer = buffer
71
+ @feature_sources = feature_sources
70
72
  @environment_override = environment_override
71
73
  @rbs_extended_reporter = rbs_extended_reporter
72
74
  @boundary_cross_reporter = boundary_cross_reporter
@@ -208,7 +210,7 @@ module Rigor
208
210
 
209
211
  env = resolve_sequential_environment(source_files: project_files)
210
212
  end
211
- snapshot_project_signature_state(signature_state_environment(resolve))
213
+ snapshot_project_signature_state(signature_state_environment(resolve, project_files))
212
214
  snapshot_effect_annotation_carrier(
213
215
  annotation_carrier_environment(resolve, in_hand: !env.nil?)&.rbs_loader
214
216
  )
@@ -220,11 +222,21 @@ module Rigor
220
222
  record_hkt_scan_failure(hkt_scan_outcome(resolve.call))
221
223
  end
222
224
 
223
- # Issue #794 — the signature-state snapshot's own gates, asked BEFORE an environment exists. Both
224
- # answers are loader-free (the configuration and the plugin registry), and with neither gate open
225
- # {#snapshot_project_signature_state} writes the same empty state a nil environment gives it.
226
- def signature_state_environment(resolve)
227
- plugin_signature_paths? || project_signature_paths? ? resolve.call : nil
225
+ # Issue #794 — the signature-state snapshot's own gates, asked BEFORE an environment exists. Every
226
+ # answer is loader-free (the configuration, the plugin registry and, issue #1713, the required-feature
227
+ # scan), and with no gate open {#snapshot_project_signature_state} writes the same empty state a nil
228
+ # environment gives it.
229
+ def signature_state_environment(resolve, project_files)
230
+ gated = plugin_signature_paths? || project_signature_paths? || vendored_features_requested?(project_files)
231
+ gated ? resolve.call : nil
232
+ end
233
+
234
+ # Issue #1713 — whether this run activates a required-feature-gated vendored directory, whose stand-down
235
+ # {#snapshot_project_signature_state} reports: the scan every build of this run gates on, or the
236
+ # feature named under `libraries:`.
237
+ def vendored_features_requested?(project_files)
238
+ features = project_files.nil? || project_files.empty? ? [] : required_features_for(project_files)
239
+ !Environment::RequiredFeatures.tokens(features, @configuration.libraries).empty?
228
240
  end
229
241
 
230
242
  # Issue #794 — the same, for the #441 carrier. The carrier is a filter over the loader's
@@ -344,6 +356,10 @@ module Rigor
344
356
  # no such plugin reads nothing off its loader here, as before.
345
357
  @snapshots.signature_standdowns =
346
358
  plugin_signature_paths? ? signature_standdowns_for(environment&.rbs_loader) : []
359
+ # Issue #1713 — NOT behind the `signature_paths:` gate either: what displaces a vendored declaration may
360
+ # be a gem's or a collection's copy. The loader answers without building its env when no gated
361
+ # directory is active.
362
+ @snapshots.vendored_standdowns = vendored_standdowns_for(environment&.rbs_loader)
347
363
  unless project_signature_paths?
348
364
  @snapshots.synthesized_namespaces = []
349
365
  @snapshots.quarantined_signatures = []
@@ -459,7 +475,22 @@ module Rigor
459
475
  **ProjectEnvironment.dependency_discovery_options(@configuration),
460
476
  synthetic_method_index: synthetic_method_index,
461
477
  project_patched_methods: project_patched_methods,
462
- source_files: source_files
478
+ source_files: source_files,
479
+ required_features: required_features_for(source_files)
480
+ )
481
+ end
482
+
483
+ # Issue #1700 — the {Environment::RequiredFeatures} this run's environments gate on: the configured paths
484
+ # plus `source_files`, whichever files the run checks, with an editor buffer or an in-memory source read
485
+ # in place of its file. Taken once per file list and handed to every build (the sequential one, the
486
+ # pool's prewarm and each worker), so they agree and a pool does not re-read the project per worker.
487
+ def required_features_for(source_files)
488
+ key = source_files.map(&:to_s)
489
+ return @required_features if @required_features_key == key
490
+
491
+ @required_features_key = key
492
+ @required_features = Ractor.make_shareable(
493
+ Environment::RequiredFeatures.for_configuration(@configuration, key, sources: @feature_sources.call)
463
494
  )
464
495
  end
465
496
 
@@ -575,9 +606,10 @@ module Rigor
575
606
  # a frozen Array<String>; cheaply shareable. Issue #793 — it IS the full project now
576
607
  # (`source_files`), not the analyzed subset this comment always described.
577
608
  shareable_source_files = source_files.map { |path| path.to_s.dup.freeze }.freeze
609
+ features = required_features_for(source_files)
578
610
 
579
611
  pool = Array.new(@workers) do
580
- Ractor.new(configuration, cache_root, blueprints, explain, shareable_source_files, locked_gems) do |configuration, cache_root, blueprints, explain, shareable_source_files, locked_gems| # rubocop:disable Layout/LineLength
612
+ Ractor.new(configuration, cache_root, blueprints, explain, shareable_source_files, locked_gems, features) do |configuration, cache_root, blueprints, explain, shareable_source_files, locked_gems, features| # rubocop:disable Layout/LineLength
581
613
  cache_store = cache_root ? Rigor::Cache::Store.new(root: cache_root) : nil
582
614
  session = Rigor::Analysis::WorkerSession.new(
583
615
  configuration: configuration,
@@ -585,7 +617,8 @@ module Rigor
585
617
  plugin_blueprints: blueprints,
586
618
  explain: explain,
587
619
  source_files: shareable_source_files,
588
- locked_gems: locked_gems
620
+ locked_gems: locked_gems,
621
+ required_features: features
589
622
  )
590
623
  main = Ractor.main
591
624
  main.send([:prepare, session.prepare_diagnostics])
@@ -713,7 +746,8 @@ module Rigor
713
746
  project_scope_seed: project_scope_seed,
714
747
  source_files: source_files,
715
748
  record_dependencies: @record_dependencies,
716
- template_units: template_units
749
+ template_units: template_units,
750
+ required_features: required_features_for(source_files)
717
751
  )
718
752
  # Force the full RBS load on the parent so children copy-on-write inherit a warm Environment
719
753
  # rather than each rebuilding it after the fork.
@@ -890,6 +924,7 @@ module Rigor
890
924
  plugin_registry: plugin_registry,
891
925
  source_files: source_files,
892
926
  locked_gems: locked_gems,
927
+ required_features: required_features_for(source_files),
893
928
  **ProjectEnvironment.dependency_discovery_options(@configuration)
894
929
  )
895
930
  warm_env.rbs_loader&.prewarm
@@ -1094,6 +1129,10 @@ module Rigor
1094
1129
  loader.signature_standdowns
1095
1130
  end
1096
1131
 
1132
+ def vendored_standdowns_for(loader)
1133
+ loader.respond_to?(:vendored_standdowns) ? loader.vendored_standdowns : []
1134
+ end
1135
+
1097
1136
  def plugin_registry
1098
1137
  @plugin_registry_reader.call
1099
1138
  end
@@ -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 = []
@@ -575,6 +575,12 @@ module Rigor
575
575
  @in_memory_sources = nil
576
576
  end
577
577
 
578
+ # Issue #1700 — the sources the required-feature scan reads in place of a file on disk: an in-memory
579
+ # source ({#run_source}) and an editor buffer (`--tmp-file` read for its `--instead-of` path).
580
+ def required_feature_sources
581
+ (@in_memory_sources || {}).merge(ProjectEnvironment.buffer_sources(@buffer))
582
+ end
583
+
578
584
  # ADR-46 — the project file set that a run over `paths` would analyze, computed by globbing only (no
579
585
  # RBS environment build), so the incremental fingerprint can be derived cheaply on the warm path
580
586
  # before deciding whether to build the env at all.
@@ -636,11 +642,16 @@ module Rigor
636
642
  # detect a class that *appeared* in an edit so a subclass whose ancestor was previously undefined
637
643
  # (and so recorded a negative class edge) is re-checked. Inverts the project class-source attribution
638
644
  # (class → declaring files).
645
+ #
646
+ # Issue #1820 — each file's set also holds a `[class, superclass as written]` pair for every class it declares
647
+ # with a superclass, so a subclass appearing or vanishing re-checks the consumers that enumerated the parent's
648
+ # subclasses (`subclass_of:<last segment>`).
639
649
  def class_declarations
640
650
  result = Hash.new { |hash, key| hash[key] = Set.new }
641
651
  @project_discovered_class_sources.each do |class_name, files|
642
652
  files.each { |file| result[file] << class_name }
643
653
  end
654
+ Incremental.add_superclass_edges(result, @project_discovered_class_sources, @project_discovered_superclasses)
644
655
  result.transform_values(&:freeze).freeze
645
656
  end
646
657
 
@@ -2005,7 +2016,8 @@ module Rigor
2005
2016
  project_scope_seed: -> { project_scope_seed_tables },
2006
2017
  template_units: -> { template_units },
2007
2018
  analyze_file: ->(path, environment) { analyze_file(path, environment) },
2008
- restored_run_level_rows: @restored_run_level_rows
2019
+ restored_run_level_rows: @restored_run_level_rows,
2020
+ feature_sources: -> { required_feature_sources }
2009
2021
  )
2010
2022
  @diagnostic_aggregator = DiagnosticAggregator.new(
2011
2023
  configuration: @configuration,
@@ -2020,6 +2032,7 @@ module Rigor
2020
2032
  synthesized_namespaces_snapshot: -> { @snapshots.synthesized_namespaces },
2021
2033
  quarantined_signatures_snapshot: -> { @snapshots.quarantined_signatures },
2022
2034
  signature_standdowns_snapshot: -> { @snapshots.signature_standdowns },
2035
+ vendored_standdowns_snapshot: -> { @snapshots.vendored_standdowns },
2023
2036
  env_build_failure_snapshot: -> { @snapshots.env_build_failure },
2024
2037
  definition_build_failures_snapshot: -> { @snapshots.definition_build_failures },
2025
2038
  hkt_scan_failure_snapshot: -> { @snapshots.hkt_scan_failure },
@@ -2295,13 +2308,30 @@ module Rigor
2295
2308
  # global-write census (the `global.*` write rules); the issue #992 envelopes ride {#seed_paired_tables}. Split
2296
2309
  # out of {#project_scope_seed_tables} to keep it under the complexity budget.
2297
2310
  def seed_call_surface_tables(tables)
2298
- tables[:discovered_refinements] = @project_discovered_refinements unless @project_discovered_refinements.empty?
2311
+ refinements = seed_refinements(tables)
2312
+ tables[:discovered_refinements] = refinements unless refinements.nil? || refinements.empty?
2299
2313
  # Issue #1367 — read by the `global.*` write rules.
2300
2314
  return if @project_discovered_global_write_census.empty?
2301
2315
 
2302
2316
  tables[:discovered_global_write_census] = @project_discovered_global_write_census
2303
2317
  end
2304
2318
 
2319
+ # Issue #1672 — the project's refinements (this run's discovery, else an opt-in `discovery_seed:` base's)
2320
+ # unioned with the opt-in gems' ({DependencySourceInference::Index#refinements}). The gem half comes from
2321
+ # the pre-passes every run takes, warm or cold, so a file re-analysed warm is seeded what a cold run seeds.
2322
+ # The seed is rebuilt per file, so the union is kept while its two inputs are the same objects.
2323
+ def seed_refinements(tables)
2324
+ project = @project_discovered_refinements
2325
+ project = tables[:discovered_refinements] if project.empty?
2326
+ gems = @dependency_source_index.refinements
2327
+ memo = @seed_refinements
2328
+ return memo[2] if memo && memo[0].equal?(project) && memo[1].equal?(gems)
2329
+
2330
+ merged = Inference::ScopeIndexer.merge_refinement_tables(project, gems)
2331
+ @seed_refinements = [project, gems, merged]
2332
+ merged
2333
+ end
2334
+
2305
2335
  # The three mixin tables: the ADR-24 instance-side `include` map, its issue #1123 `prepend` twin
2306
2336
  # (which carries the instance-ancestor order the include table's set-shaped value cannot), and the
2307
2337
  # #898 singleton-side `extend` table. Grouped here because they are read as one picture of a class's
@@ -2603,7 +2633,8 @@ module Rigor
2603
2633
  self_call_misses: self_call_misses,
2604
2634
  comments: parse_result.comments,
2605
2635
  disabled_rules: @configuration.disabled_rules,
2606
- node_collectors: node_collectors
2636
+ node_collectors: node_collectors,
2637
+ stated_runtime_ruby: @configuration.stated_runtime_ruby
2607
2638
  )
2608
2639
  diagnostics + plugin_emitted_diagnostics(path, root, scope, node_results)
2609
2640
  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
@@ -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)
@@ -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
@@ -270,8 +271,18 @@ module Rigor
270
271
  "sig:#{digest_signature_paths(project_sig_roots(configuration))}",
271
272
  "pre_eval:#{digest_pre_eval(configuration.pre_eval)}"
272
273
  ]
274
+ parts.concat(dependency_source_parts(configuration))
273
275
  identity = EngineSource.process_identity
274
276
  parts << "engine-source:#{identity}" if identity
277
+ # Issue #1700 — the gated vendored signatures the run's source activates. A file that gains or drops a
278
+ # `require "prime"` changes the environment every other file was analysed against, which no per-file
279
+ # dependency edge records, so the whole snapshot goes. Scanned over the configured paths plus the roots'
280
+ # files, the set the run's environment scans; no part when nothing is required, so every other
281
+ # project's fingerprint is as it was.
282
+ features = Environment::RequiredFeatures.for_configuration(
283
+ configuration, Analysis::PathExpansion.ruby_files(roots, configuration.exclude_patterns)
284
+ )
285
+ parts << "required-features:#{features.join(',')}" unless features.empty?
275
286
  # ADR-116 WD5 — the snapshot's per-file rows and seed bundles are only as checked as the run that
276
287
  # wrote them ({Inference::DeclarationWalk::Shadow}); nil with the harness off adds no part.
277
288
  shadow = Analysis::ShadowHarness.cache_identity
@@ -286,6 +297,17 @@ module Rigor
286
297
  nil
287
298
  end
288
299
 
300
+ # ADR-10 slice 3, issue #1814 — the walked gems' versions: their method catalogue, their refinement table and the
301
+ # constants they bind reach every file's checks, and the lockfile digest above does not cover a project with none.
302
+ # No part for a project that lists no gem, so its fingerprint is what it was.
303
+ def self.dependency_source_parts(configuration)
304
+ rows = Analysis::RunCacheKey.dependency_source_entries(configuration).map do |row|
305
+ [row.gem_name, row.gem_version, row.mode]
306
+ end
307
+ rows.empty? ? [] : ["dependency-source:#{rows.sort.inspect}"]
308
+ end
309
+ private_class_method :dependency_source_parts
310
+
289
311
  def self.digest_file_if_present(path)
290
312
  path = path&.to_s
291
313
  path && File.file?(path) ? Digest::SHA256.file(path).hexdigest : "absent"
@@ -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 --------------------------------------------------
@@ -685,9 +685,10 @@ module Rigor
685
685
  # ADR-103 WD15 — captured before `DEFAULTS.merge` below folds "no `effects:` key" and "`effects:
686
686
  # false`" into the same value; see `Configuration.load`'s identical capture.
687
687
  effects_key_present = data.key?("effects")
688
+ target_ruby_key_present = data.key?("target_ruby")
688
689
  data = data.dup
689
690
  data["plugins"] = inject_treat_all_as_inline_rbs(Array(data["plugins"]))
690
- Configuration.new(Configuration::DEFAULTS.merge(data), effects_key_present)
691
+ Configuration.new(Configuration::DEFAULTS.merge(data), effects_key_present, target_ruby_key_present)
691
692
  end
692
693
 
693
694
  # ADR-50 § WD2 — applies the `--bleeding-edge[=ids]` / `--no-bleeding-edge` CLI selection over the configured
@@ -69,7 +69,12 @@ module Rigor
69
69
  def execute(file:, options:)
70
70
  configuration = Configuration.load(options.fetch(:config))
71
71
  source = File.read(file, encoding: Encoding::UTF_8)
72
+ # Prism parses the ORIGINAL bytes, so a `# -*- coding: -*-` file is read in its own encoding. Only the text the
73
+ # renderer sees is scrubbed: the colouriser and line splitter raise on a byte invalid in UTF-8 (a comment,
74
+ # `__END__` data, a non-UTF-8 file).
75
+ # Scrubbing is byte-for-byte (`?` per bad byte), so Prism's positions still address it.
72
76
  parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
77
+ source = source.scrub { |bytes| "?" * bytes.bytesize } unless source.valid_encoding?
73
78
  return 1 if parse_errors?(parse_result, file)
74
79
 
75
80
  events = record_events(parse_result.value, file, configuration)
@@ -87,7 +87,7 @@ module Rigor
87
87
  # file is parsed and scope-indexed once, however many positions land in it.
88
88
  def execute(targets:, options:, buffer: nil)
89
89
  configuration = Configuration.load(options.fetch(:config))
90
- environment = project_environment(targets.map(&:file).uniq, configuration)
90
+ environment = project_environment(targets.map(&:file).uniq, configuration, buffer)
91
91
  base_scope = Scope.empty(environment: environment)
92
92
 
93
93
  results_by_target = Array.new(targets.length)
@@ -218,8 +218,10 @@ module Rigor
218
218
  # closes). The probed file is threaded as the synthesizer's `source_files:`. Project-RBS auto-detection
219
219
  # roots at CWD today; future work will walk parent directories to find the enclosing `Gemfile`/`*.gemspec`
220
220
  # so probes against files outside the current process's CWD still see the right `sig/` tree.
221
- def project_environment(files, configuration)
222
- ProjectEnvironment.build(configuration: configuration, source_files: files)
221
+ # Issue #1700 — an editor buffer is scanned for required features in place of the file it stands for.
222
+ def project_environment(files, configuration, buffer = nil)
223
+ ProjectEnvironment.build(configuration: configuration, source_files: files,
224
+ feature_sources: ProjectEnvironment.buffer_sources(buffer))
223
225
  end
224
226
 
225
227
  def file_exists?(file)