rigortype 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
data/lib/rigor/cli.rb CHANGED
@@ -33,8 +33,11 @@ module Rigor
33
33
  # actionable": `SystemStackError` from an ancestry walk that never terminates, `NoMemoryError` from a
34
34
  # runaway allocation. Neither is a `StandardError`, so every rescue between here and the raise site
35
35
  # declines them by design — the whole point of those rescues is to convert a per-file or per-plugin
36
- # failure into a diagnostic, and a process running out of stack is not per-file. Left uncaught they
37
- # abort with Ruby's own ~40-frame dump, which is non-zero (so a CI gate does catch it) but says
36
+ # failure into a diagnostic. Since #2455 the per-file phases (`Runner#analyze_file_body`, the worker session,
37
+ # the discovery loops) do contain a `SystemStackError` raised from Ruby code, as that file's own crash row;
38
+ # what still reaches here is one raised from C (`Prism.parse` of a source nested near Prism's cap, which has
39
+ # no backtrace and leaves no stack to recover on), one outside a per-file phase, and `NoMemoryError`. Left
40
+ # uncaught they abort with Ruby's own ~40-frame dump, which is non-zero (so a CI gate does catch it) but says
38
41
  # neither that Rigor is at fault nor that the report on disk is empty. `Interrupt`, `SignalException`
39
42
  # and `SystemExit` stay out: those are the user's answer, not a failure.
40
43
  FATAL_ERRORS = [SystemStackError, NoMemoryError].freeze
@@ -3,7 +3,10 @@
3
3
  require "did_you_mean"
4
4
 
5
5
  require_relative "config_path"
6
+ require_relative "environment/bundle_sig_discovery"
7
+ require_relative "environment/rbs_collection_discovery"
6
8
  require_relative "flow_contribution/conflict"
9
+ require_relative "library_probe"
7
10
  require_relative "signature_path_audit"
8
11
  # ADR-87 WD4 — only the pure rule-id table (`ALL_RULES` / `RULE_FAMILIES`) is needed, so require the light
9
12
  # `rule_ids.rb` rather than the engine-pulling `check_rules.rb`; keeps `config_audit` (loaded by every check)
@@ -37,8 +40,8 @@ module Rigor
37
40
  module ConfigAudit
38
41
  # One config-level finding. `kind` discriminates the source key (`:unknown_key`, `:signature_path`,
39
42
  # `:bundled_plugin_signature_path`, `:library`, `:disabled_rule`, `:severity_override`,
40
- # `:bundler_bundle_path`, `:bundler_lockfile`, `:rbs_collection_lockfile`); `fields` carries the kind-specific
41
- # structured data merged into {#to_h} for JSON consumers.
43
+ # `:bundler_bundle_path`, `:bundler_lockfile`, `:rbs_collection_lockfile`, `:rbs_collection_library`); `fields`
44
+ # carries the kind-specific structured data merged into {#to_h} for JSON consumers.
42
45
  Warning = Data.define(:kind, :message, :fields) do
43
46
  def to_h
44
47
  { "kind" => kind.to_s, "message" => message }.merge(fields)
@@ -51,7 +54,7 @@ module Rigor
51
54
  unknown_key_warnings(configuration) +
52
55
  signature_path_warnings(configuration) +
53
56
  bundled_plugin_signature_path_warnings(configuration) +
54
- library_warnings(configuration) +
57
+ library_warnings(configuration, project_root) +
55
58
  rule_token_warnings(configuration) +
56
59
  explicit_path_warnings(configuration, project_root)
57
60
  end
@@ -129,21 +132,32 @@ module Rigor
129
132
  end
130
133
  end
131
134
 
132
- # `libraries:` entries RBS does not recognise. Uses the same
133
- # `RBS::EnvironmentLoader#has_library?` guard the loader filters through
134
- # ({Environment::RbsLoader} `build_env_for`), so a flagged name is exactly one the loader
135
- # skipped. Fails soft: if RBS itself raises, no warning rather than a crash.
136
- def self.library_warnings(configuration)
137
- configured = Array(configuration.libraries)
138
- return [] if configured.empty?
139
-
140
- loader = ::RBS::EnvironmentLoader.new
141
- configured.reject { |lib| loader.has_library?(library: lib.to_s, version: nil) }.map do |lib|
142
- Warning.new(
143
- kind: :library,
144
- message: "libraries: #{lib.to_s.inspect} is not an available RBS library (no signatures loaded from it)",
145
- fields: { "name" => lib.to_s }
146
- )
135
+ # `libraries:` entries RBS does not recognise, and — issue #2946 — `source: { type: stdlib }` entries of
136
+ # the run's `rbs_collection.lock.yaml`, which load as libraries too ({Environment::BaseLibraries.resolve}). Uses the
137
+ # same `RBS::EnvironmentLoader#has_library?` answer the loader filters through
138
+ # ({Environment::RbsLoader} `library_loader`), so a flagged name is one the loader skipped. A
139
+ # {Environment::RequiredFeatures::VENDORED_DIRS} name (`prime`) under `libraries:` is not audited: the
140
+ # configuration turns it into a vendored feature token the loader never asks `has_library?` about. A
141
+ # lockfile entry of that name is not a token, so it is still audited. The `rbs` CLI warns on the same
142
+ # lockfile entry (`Collection::Sources::Stdlib#manifest_of`).
143
+ # Fails soft: if RBS itself raises, no warning rather than a crash.
144
+ def self.library_warnings(configuration, project_root = Dir.pwd)
145
+ lock = { lockfile_path: configuration.rbs_collection_lockfile, project_root: project_root,
146
+ auto_detect: configuration.rbs_collection_auto_detect }
147
+ lockfile = Environment::RbsCollectionDiscovery.stdlib_library_names(**lock)
148
+ # Issue #2971 — a `libraries:` name the collection lockfile provides loads from the collection, not as an
149
+ # RBS library, so it is not an unknown library.
150
+ provided = Environment::RbsCollectionDiscovery.collection_gem_names(**lock)
151
+ named = (LibraryProbe.auditable(configuration.libraries) - provided).map { |lib| [:library, "libraries:", lib] } +
152
+ lockfile.map { |name| [:rbs_collection_library, "rbs_collection.lock.yaml: stdlib entry", name] }
153
+ return [] if named.empty?
154
+
155
+ unavailable = LibraryProbe.predicate.then { |has| named.reject { |_, _, lib| has.call(lib) } }
156
+ # Issue #2987 — nor is a name whose own `sig/` the bundle loaded; read only once a name is unavailable.
157
+ bundled = unavailable.empty? ? [] : Environment::BundleSigDiscovery.loaded_names(configuration, project_root)
158
+ unavailable.reject { |kind, _, lib| kind == :library && bundled.include?(lib) }.map do |kind, source, lib|
159
+ Warning.new(kind: kind, fields: { "name" => lib },
160
+ message: "#{source} #{lib.inspect} is not an available RBS library (no signatures loaded from it)")
147
161
  end
148
162
  rescue StandardError
149
163
  []
@@ -86,6 +86,7 @@ module Rigor
86
86
  # a config edit the author may deliberately not want.
87
87
  "effect.annotations-unchecked" => :info,
88
88
  "static.value-use.void" => :off,
89
+ "plugin.api-surface" => :off,
89
90
  # Opt-in author assertion: you only see it if you wrote a
90
91
  # `conforms-to` directive, so it stays a :warning even in
91
92
  # lenient — it is never unsolicited noise.
@@ -131,6 +132,7 @@ module Rigor
131
132
  "suppression.empty" => :warning,
132
133
  "suppression.unknown-marker" => :warning,
133
134
  "static.value-use.void" => :off,
135
+ "plugin.api-surface" => :off,
134
136
  "effect.envelope-exceeded" => :warning,
135
137
  "effect.liskov-widened" => :warning,
136
138
  "effect.unknown-label" => :info,
@@ -173,6 +175,7 @@ module Rigor
173
175
  "suppression.unknown-marker" => :warning,
174
176
  # `:off` even under strict: the gate is `bleeding_edge:`, not the profile (ADR-50 WD1 / ADR-100).
175
177
  "static.value-use.void" => :off,
178
+ "plugin.api-surface" => :off,
176
179
  "effect.envelope-exceeded" => :error,
177
180
  "effect.liskov-widened" => :error,
178
181
  "effect.unknown-label" => :warning,
@@ -234,11 +237,17 @@ module Rigor
234
237
 
235
238
  private_class_method :row_override
236
239
 
240
+ # Rule ids a dot-free family key does not reach. `plugin.api-surface` is the one built-in id under `plugin.`,
241
+ # the prefix of plugin rows, so a `plugin` key must not turn the bleeding-edge rule on (#2822); `disable:
242
+ # [plugin]` does not reach it either. A key naming the id itself still does, in either direction.
243
+ FAMILY_FALLBACK_EXEMPT = ["plugin.api-surface"].freeze
244
+
237
245
  # A family name has no `.` in it, so only a dot-free key can match one. `resolve` runs for every
238
246
  # diagnostic in a run's stream, and most configurations carry no such key (usually no overrides at all),
239
247
  # so the split below is skipped for them rather than allocating a name that nothing can match.
240
248
  def family_override(rule, overrides)
241
249
  return nil unless overrides.any? { |key, _| !key.to_s.include?(".") }
250
+ return nil if FAMILY_FALLBACK_EXEMPT.include?(rule)
242
251
 
243
252
  family = rule.split(".").first
244
253
  return nil if family.nil?
@@ -212,8 +212,8 @@ module Rigor
212
212
  # set up with `rbs collection install`, the resulting `rbs_collection.lock.yaml` carries the resolved
213
213
  # (gem, version, source) triples and `.gem_rbs_collection/` holds the downloaded `.rbs` files. Rigor
214
214
  # parses the lockfile and auto-feeds each gem's `<collection_root>/<name>/<version>/` directory into
215
- # `RbsLoader`'s `signature_paths:`. Sources of type `stdlib` are skipped because rigor's bundled
216
- # `DEFAULT_LIBRARIES` already covers that surface.
215
+ # `RbsLoader`'s `signature_paths:`. Sources of type `stdlib` install no directory; issue #2946 — they
216
+ # load as RBS libraries, appended to `DEFAULT_LIBRARIES + libraries:`, as the `rbs` CLI loads them.
217
217
  #
218
218
  # `lockfile:` (String, optional): explicit path to `rbs_collection.lock.yaml`. Resolved relative to
219
219
  # the project root when relative.
@@ -361,7 +361,8 @@ module Rigor
361
361
  # [PHPStan](https://phpstan.org/config-reference#paths).
362
362
  def self.load(path = nil)
363
363
  resolved = path || discover
364
- if resolved.nil? || !File.exist?(resolved)
364
+ loaded = resolved && File.exist?(resolved) ? resolved : nil
365
+ if loaded.nil?
365
366
  data = DEFAULTS
366
367
  effects_key_present = false
367
368
  target_ruby_key_present = false
@@ -370,13 +371,14 @@ module Rigor
370
371
  # `DEFAULTS` itself carries an `"effects" => false` entry: once merged, "the file never wrote
371
372
  # `effects:`" and "the file wrote `effects: false`" collapse to the same value and become
372
373
  # indistinguishable to {#initialize}. This is the only place that distinction still exists.
373
- raw = load_with_includes(resolved)
374
+ raw = load_with_includes(loaded)
374
375
  effects_key_present = raw.key?("effects")
375
376
  target_ruby_key_present = raw.key?("target_ruby")
376
377
  data = DEFAULTS.merge(raw)
377
378
  end
378
379
  new(autowire_default_plugins(data), effects_key_present, target_ruby_key_present || false)
379
- .with_exclude_base(resolved && File.exist?(resolved) ? File.dirname(resolved) : nil)
380
+ .with_exclude_base(loaded && File.dirname(loaded))
381
+ .with_config_path(loaded)
380
382
  end
381
383
 
382
384
  # #1576 — the directory relative `exclude:` patterns are matched against: the directory of the config file in
@@ -385,6 +387,17 @@ module Rigor
385
387
  @base_dir || Dir.pwd
386
388
  end
387
389
 
390
+ # Issue #2742 — the path config-anchored diagnostics are positioned at: the loaded config file's path
391
+ # relative to the working directory (the project-root convention {Analysis::Baseline} uses), or
392
+ # `.rigor.yml` when no file was loaded. A `--config=config/rigor.yml` run names `config/rigor.yml`.
393
+ def config_display_path
394
+ return DEFAULT_PATH if @config_path.nil?
395
+
396
+ Pathname.new(@config_path).relative_path_from(Pathname.new(Dir.pwd)).to_s
397
+ rescue ArgumentError
398
+ @config_path
399
+ end
400
+
388
401
  # ADR-93 WD2 — the one bundled plugin default-wired without a `plugins:` entry. `rigor-rbs-inline` is the
389
402
  # gem; `rbs-inline` is its manifest id (the loader raises on a duplicate id, so both forms count as
390
403
  # "already listed").
@@ -675,6 +688,10 @@ module Rigor
675
688
  def to_h # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
676
689
  {
677
690
  "target_ruby" => target_ruby,
691
+ # Issue #2742 — the loaded file's display path is part of the resolved configuration: two files
692
+ # with identical bodies still anchor their config diagnostics differently, so the run-cache and
693
+ # incremental fingerprints must tell them apart.
694
+ "config_path" => config_display_path,
678
695
  # A stated `target_ruby` turns on the deprecation rules a defaulted one leaves off (#1692), so the run cache
679
696
  # key and the incremental fingerprint must tell the two apart even where the value matches.
680
697
  "target_ruby_explicit" => target_ruby_explicit?,
@@ -784,6 +801,16 @@ module Rigor
784
801
  copy.freeze
785
802
  end
786
803
 
804
+ # Issue #2742 — a sibling Configuration recording which file {Configuration.load} read, so the
805
+ # diagnostics anchored at "the configuration" name it ({#config_display_path}). `path` is the resolved
806
+ # config path; nil — no file loaded, or a bare {Configuration.new} — keeps the `.rigor.yml` default.
807
+ # Same frozen `dup` shape as {#with_exclude_base}.
808
+ def with_config_path(path)
809
+ copy = dup
810
+ copy.instance_variable_set(:@config_path, path && File.absolute_path(path).freeze)
811
+ copy.freeze
812
+ end
813
+
787
814
  # #1777 — a sibling Configuration with only `baseline:` unset, every other field (including the
788
815
  # explicitness of `target_ruby`) shared with the receiver. `rigor baseline generate` analyses with
789
816
  # this so the generating run sees exactly the configuration `rigor check` will, minus the filter it
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../type"
4
4
  require_relative "../inference/origin_lookup"
5
+ require_relative "../typing_state"
5
6
  require_relative "attribution"
6
7
  require_relative "envelope_index"
7
8
  require_relative "file_collection"
@@ -24,8 +25,26 @@ module Rigor
24
25
  # `dynamic_origins` cause the typer already recorded. With no `effects:` block nothing calls
25
26
  # {collect_for}, `@active_count` stays zero, and `rigor check` is byte-identical.
26
27
  module Collector
27
- KEY = :__rigor_effects_collector__
28
- private_constant :KEY
28
+ # ADR-129 WD1 — the installed accumulator is a Frame whose contents are a Resource; only {record_root} touches it
29
+ # now, and the call-site decisions are the bound Table {CALLS}. The activation count below is a process switch,
30
+ # outside the typing state (WD5).
31
+ ACCUMULATOR = TypingState.frame(
32
+ :effects_collector,
33
+ contents: :resource, reason: "the accumulator a file's collection installs; each call site records into it",
34
+ # G1′ witness: `Collector.record_call` reads it during typing, so it is not a never-reads switch.
35
+ witness: "spec/fixtures/typing_state_witnesses/effects_collector.rb"
36
+ )
37
+ private_constant :ACCUMULATOR
38
+
39
+ # ADR-129 WD1 — the per-file call-site decisions are a bound Table: each installed accumulator's `calls` is one of
40
+ # its stores, journaled while a capture is open so a later `type_of` memo can validate the reads and replay the
41
+ # writes. The root ({record_root}) stays a touch of the accumulator: it is set once per file, before typing.
42
+ CALLS = TypingState.table(
43
+ :effects_calls,
44
+ bound: true,
45
+ reason: "node => CallRecord per file accumulator; first write wins, and a re-query reads the pinned decision"
46
+ )
47
+ private_constant :CALLS
29
48
 
30
49
  # What one call site decided, as the typer saw it.
31
50
  #
@@ -97,17 +116,18 @@ module Rigor
97
116
  # site imports must not depend on which process typed the file.
98
117
  def collect_for(path, attribution: Attribution.empty, envelopes: EnvelopeIndex.empty,
99
118
  plugin_facts: PluginFacts.empty, unit_key: nil, unit_owner: nil)
100
- previous = Thread.current[KEY]
101
119
  accumulator = Accumulator.new(path.to_s, attribution: attribution, envelopes: envelopes,
102
120
  plugin_facts: plugin_facts, unit_key: unit_key,
103
121
  unit_owner: unit_owner)
104
- Thread.current[KEY] = accumulator
105
- @mutex.synchronize { @active_count += 1 }
106
- yield
107
- build(accumulator)
108
- ensure
109
- Thread.current[KEY] = previous
110
- @mutex.synchronize { @active_count -= 1 }
122
+ TypingState.current.with(ACCUMULATOR, accumulator) do
123
+ @mutex.synchronize { @active_count += 1 }
124
+ begin
125
+ yield
126
+ build(accumulator)
127
+ ensure
128
+ @mutex.synchronize { @active_count -= 1 }
129
+ end
130
+ end
111
131
  end
112
132
 
113
133
  def active?
@@ -117,8 +137,12 @@ module Rigor
117
137
  # The parsed root of the file being analyzed. Recorded by the analysis body once the parse is known
118
138
  # to have succeeded, so the scan runs over exactly what the typer saw.
119
139
  def record_root(root)
120
- accumulator = Thread.current[KEY]
121
- accumulator.root = root if accumulator && accumulator.root.nil?
140
+ state = TypingState.current
141
+ accumulator = state[ACCUMULATOR]
142
+ return unless accumulator
143
+
144
+ state.touch(ACCUMULATOR)
145
+ accumulator.root = root if accumulator.root.nil?
122
146
  end
123
147
 
124
148
  # One call site's dispatch decision. `receiver` is the type the typer computed for the receiver and
@@ -127,30 +151,38 @@ module Rigor
127
151
  # The `scope.source_path` guard matters: inter-procedural inference re-types a *callee's* body while
128
152
  # the caller's file is being analyzed, and those nodes belong to the callee's own file collection.
129
153
  def record_call(node, receiver, scope)
130
- accumulator = Thread.current[KEY]
154
+ state = TypingState.current
155
+ accumulator = state[ACCUMULATOR]
131
156
  return if accumulator.nil? || accumulator.path != scope.source_path
157
+
132
158
  # First write wins, so a re-query has nothing to add — and asking here rather than inside
133
159
  # {Accumulator#record} is what stops it paying for the `Data` and the origin lookup below.
160
+ calls = accumulator.calls
161
+ state.log_bound_read(CALLS, calls, node, calls.fetch(node, TypingState::ABSENT)) if state.capturing?
134
162
  return if accumulator.recorded?(node)
135
163
 
136
164
  dynamic = dynamic_receiver?(receiver)
137
165
  class_name, kind = descriptor_for(receiver)
138
- accumulator.record(
139
- node,
140
- CallRecord.new(
141
- receiver_class: class_name, kind: kind, dynamic: dynamic,
142
- cause: dynamic ? Inference::OriginLookup.origin_for(scope, node.receiver)&.to_s : nil,
143
- resolved: true
144
- )
166
+ call_record = CallRecord.new(
167
+ receiver_class: class_name, kind: kind, dynamic: dynamic,
168
+ cause: dynamic ? Inference::OriginLookup.origin_for(scope, node.receiver)&.to_s : nil,
169
+ resolved: true
145
170
  )
171
+ accumulator.record(node, call_record)
172
+ state.log_bound_write(CALLS, calls, node, call_record, TypingState::ABSENT)
146
173
  end
147
174
 
148
175
  # The typer's fallback verdict: no tier resolved this call.
149
176
  def record_unresolved(node, source_path)
150
- accumulator = Thread.current[KEY]
177
+ state = TypingState.current
178
+ accumulator = state[ACCUMULATOR]
151
179
  return if accumulator.nil? || accumulator.path != source_path
152
180
 
181
+ calls = accumulator.calls
182
+ existing = calls.fetch(node, TypingState::ABSENT)
183
+ state.log_bound_read(CALLS, calls, node, existing)
153
184
  accumulator.mark_unresolved(node)
185
+ state.log_bound_write(CALLS, calls, node, calls[node], existing) unless TypingState::ABSENT.equal?(existing)
154
186
  end
155
187
 
156
188
  # The class a receiver type projects to, as `[class_name, kind]`. A deliberately small mirror of the
@@ -4,6 +4,7 @@ require "yaml"
4
4
 
5
5
  require_relative "../config_path"
6
6
  require_relative "../rooted_glob"
7
+ require_relative "lockfile_resolver"
7
8
 
8
9
  module Rigor
9
10
  class Environment
@@ -49,6 +50,10 @@ module Rigor
49
50
  "bundler", "rubygems"
50
51
  ].freeze
51
52
 
53
+ # The two bundle layouts {.discover} walks, relative to the resolved bundle root. Also the listing rows the
54
+ # run cache records (issue #2987), so a gem gaining or losing `sig/` invalidates it.
55
+ SIG_GLOBS = %w[ruby/*/gems/*/sig ruby/*/bundler/gems/*/sig].freeze
56
+
52
57
  # @param bundle_path — explicit path to the bundler install root. When `nil`,
53
58
  # falls back to `auto_detect` if `auto_detect:` is true.
54
59
  # @param project_root — resolution base for relative `bundle_path:` and the auto-detect search.
@@ -92,45 +97,75 @@ module Rigor
92
97
  # all (`Bundler::Source::Path#path` is the user's own `path:` directory, used in place), so there is
93
98
  # no bundle-relative location to walk. A `path:`-sourced gem's `sig/` is only reachable by adding it
94
99
  # directly to the project's `signature_paths:` — which already works today and needs no discovery.
95
- all = (RootedGlob.glob(resolved, "ruby/*/gems/*/sig") +
96
- RootedGlob.glob(resolved, "ruby/*/bundler/gems/*/sig")).map { |d| Pathname.new(d) }
97
- filtered = all.reject { |sig_dir| skip_gems.include?(gem_name_from_sig_path(sig_dir)) }
98
- return filtered if locked_gems.nil? || locked_gems.empty?
100
+ all = SIG_GLOBS.flat_map { |pattern| RootedGlob.glob(resolved, pattern) }.map { |d| Pathname.new(d) }
101
+ if locked_gems.nil? || locked_gems.empty?
102
+ return all.reject { |sig_dir| skip_gems.include?(gem_name_from_sig_path(sig_dir)) }
103
+ end
99
104
 
100
- expected_dirs = expected_gem_dirs(locked_gems)
101
- filtered.select do |sig_dir|
102
- if git_sourced_layout?(sig_dir)
103
- # A git install's directory carries no version, so the lockfile filter can only check the gem NAME.
104
- # Name alone is not enough: a gem that moved from `git:` to a released version keeps its stale
105
- # `bundler/gems/` checkout until `bundle clean`, under a name the lockfile still lists. `locked_gems`
106
- # must say this name is CURRENTLY git-sourced, not merely present.
107
- locked = locked_gems[gem_name_from_sig_path(sig_dir)]
108
- !!locked&.git_source
109
- else
110
- expected_dirs.include?(sig_dir.parent.basename.to_s)
111
- end
105
+ # Issue #2978 — with a lockfile, a directory's gem name is the locked entry it matches, not a parse of the
106
+ # directory name, which reads `foo-2fa-1.0` as `foo`.
107
+ names = locked_dir_names(locked_gems)
108
+ all.select do |sig_dir|
109
+ name = locked_name_of(sig_dir, locked_gems, names)
110
+ name && !skip_gems.include?(name)
111
+ end
112
+ end
113
+
114
+ # The locked gem name `sig_dir` belongs to, or nil when it matches no locked entry. A git install's directory
115
+ # carries no version, so only the gem NAME can be checked. Name alone is not enough: a gem that moved from
116
+ # `git:` to a released version keeps its stale `bundler/gems/` checkout until `bundle clean`, under a name the
117
+ # lockfile still lists, so `locked_gems` must say this name is CURRENTLY git-sourced, not merely present.
118
+ #
119
+ # @param names — {.locked_dir_names} of `locked_gems`.
120
+ def self.locked_name_of(sig_dir, locked_gems, names)
121
+ if git_sourced_layout?(sig_dir)
122
+ name = gem_name_from_sig_path(sig_dir)
123
+ locked_gems[name]&.git_source ? name : nil
124
+ else
125
+ names[sig_dir.parent.basename.to_s]
112
126
  end
113
127
  end
128
+ private_class_method :locked_name_of
129
+
130
+ # Issue #2971 — the locked gem names `sig_dirs` (a {.discover} result) belong to, keyed the way {.discover}
131
+ # filters: a rubygems-layout directory by `<name>-<version>[-<platform>]` against the locked variants, a git
132
+ # layout by a git-sourced locked entry of that name. Unlike {.gem_name_from_sig_path} it never truncates a
133
+ # name that holds a `-<digit>` (`foo-2fa-1.0` is `foo-2fa`, not `foo`).
134
+ def self.locked_names_of(sig_dirs, locked_gems)
135
+ return [] if locked_gems.nil? || locked_gems.empty?
136
+
137
+ names = locked_dir_names(locked_gems)
138
+ sig_dirs.filter_map { |dir| locked_name_of(dir, locked_gems, names) }.uniq
139
+ end
140
+
141
+ # Issue #2987 — the locked gem names whose own `sig/` {.discover} loads for a configuration's bundle and
142
+ # lockfile: a pure file read ({LockfileResolver}), so the `libraries:` audit can ask without loading `rbs`.
143
+ def self.loaded_names(configuration, project_root)
144
+ auto_detect = configuration.bundler_auto_detect
145
+ locked = LockfileResolver.locked_gems(lockfile_path: configuration.bundler_lockfile,
146
+ project_root: project_root, auto_detect: auto_detect)
147
+ dirs = discover(bundle_path: configuration.bundler_bundle_path, project_root: project_root,
148
+ auto_detect: auto_detect, locked_gems: locked.empty? ? nil : locked)
149
+ locked_names_of(dirs, locked)
150
+ end
114
151
 
115
- # `{name => LockedGem}` → set of canonical bundler gem directory basenames. Pure-Ruby gems install as
152
+ # `{name => LockedGem}` → `{canonical bundler gem directory basename => name}`. Pure-Ruby gems install as
116
153
  # `<name>-<version>`; platform-specific gems install as `<name>-<version>-<platform>` (e.g.
117
154
  # `ffi-1.17.4-aarch64-linux-gnu`). Lockfile platform `"ruby"` is the pure-Ruby case; any other value is
118
155
  # treated as a platform tag. Every locked variant contributes a directory (#1844): the set only selects
119
156
  # among directories that are actually installed, so admitting another platform's name costs nothing,
120
- # while dropping the host's would discard its `sig/`.
121
- def self.expected_gem_dirs(locked_gems)
122
- locked_gems.each_value.with_object(Set.new) do |locked, set|
157
+ # while dropping the host's would discard its `sig/`. Issue #2978 — the name comes from the lockfile, so a name
158
+ # holding `-<digit>` is read whole.
159
+ def self.locked_dir_names(locked_gems)
160
+ locked_gems.each_value.with_object({}) do |locked, names|
123
161
  locked.variants.each do |version, platform|
124
162
  base = "#{locked.name}-#{version}"
125
- set << if platform == "ruby" || platform.empty?
126
- base
127
- else
128
- "#{base}-#{platform}"
129
- end
163
+ dir = platform == "ruby" || platform.empty? ? base : "#{base}-#{platform}"
164
+ names[dir] = locked.name
130
165
  end
131
166
  end
132
167
  end
133
- private_class_method :expected_gem_dirs
168
+ private_class_method :locked_dir_names
134
169
 
135
170
  # `<bundle>/ruby/X.Y.Z/gems/<name>-<ver>/sig` → `<name>`, or
136
171
  # `<bundle>/ruby/X.Y.Z/bundler/gems/<repo>-<12-hex-revision>/sig` → `<repo>`. The two bundler-install
@@ -145,8 +180,8 @@ module Rigor
145
180
  # different repo name, a monorepo). Strip the fixed-width hex suffix instead; best-effort only, since
146
181
  # the true gem name isn't recoverable from the filesystem without running Bundler.
147
182
  #
148
- # Public so the O4 Layer 3 slice-3 coverage report (`RbsCoverageReport`) can classify discovered bundle
149
- # sigs against locked gem names without re-running discovery.
183
+ # Only for a directory no lockfile names: with one, {.locked_names_of} reads the name off the locked entry
184
+ # instead (issue #2978), since a name holding `-<digit>` (`foo-2fa-1.0`) truncates here to `foo`.
150
185
  def self.gem_name_from_sig_path(sig_dir)
151
186
  gem_dir = sig_dir.parent.basename.to_s
152
187
  if git_sourced_layout?(sig_dir)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../typing_state"
4
+
3
5
  module Rigor
4
6
  class Environment
5
7
  # Per-key memoization container for {Environment#constant_for_name}. Held by {Environment} so the
@@ -22,6 +24,9 @@ module Rigor
22
24
  # reader path. If a future caller introduces a multi-threaded reader against one Environment, the
23
25
  # synchronisation belongs at that caller's seam, not here.
24
26
  class ConstantTypeCacheHolder
27
+ TypingState.memo_field(self, :@cache, reason: "keyed on the constant name; a pure function of the name for " \
28
+ "the environment's lifetime (Environment#constant_for_name)")
29
+
25
30
  def initialize
26
31
  @cache = {}
27
32
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "rbs_collection_discovery"
4
+
3
5
  module Rigor
4
6
  class Environment
5
7
  # Stdlib libraries that `Environment.for_project` loads on top of RBS core
@@ -32,5 +34,20 @@ module Rigor
32
34
  strscan
33
35
  prism rbs
34
36
  ].freeze
37
+
38
+ # The library list a run loads before its `RequiredFeatures` tokens: {DEFAULT_LIBRARIES}, then the
39
+ # configured `libraries:`, then — issue #2946 — the `source: { type: stdlib }` entries of the run's
40
+ # `rbs_collection.lock.yaml` ({RbsCollectionDiscovery.stdlib_library_names}), de-duplicated in that order.
41
+ # `Environment.for_project` and the boot-slimming probe's `RunCacheKey.libraries_config_entries` both build
42
+ # it here, so the cache key's `rbs.libraries` slot names the lockfile's libraries on both paths. A module of
43
+ # its own, not an `Environment` class method, so the plugin-facing `Environment` surface does not grow.
44
+ module BaseLibraries
45
+ def self.resolve(libraries, rbs_collection_lockfile:, rbs_collection_auto_detect:, root: Dir.pwd)
46
+ collection_stdlib = RbsCollectionDiscovery.stdlib_library_names(
47
+ lockfile_path: rbs_collection_lockfile, project_root: root, auto_detect: rbs_collection_auto_detect
48
+ )
49
+ (DEFAULT_LIBRARIES + libraries.map(&:to_s) + collection_stdlib).uniq
50
+ end
51
+ end
35
52
  end
36
53
  end