rigortype 0.1.19 → 0.2.1

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 (197) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -6
  3. data/data/core_overlay/numeric.rbs +33 -0
  4. data/data/core_overlay/pathname.rbs +25 -0
  5. data/data/core_overlay/string_scanner.rbs +28 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
  7. data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
  8. data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
  9. data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
  10. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
  11. data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
  12. data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
  13. data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
  14. data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
  15. data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
  16. data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
  17. data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
  18. data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
  19. data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
  20. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
  21. data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
  22. data/data/vendored_gem_sigs/redis/future.rbs +5 -0
  23. data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
  24. data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
  25. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
  26. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
  27. data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
  28. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  29. data/lib/rigor/analysis/check_rules.rb +492 -71
  30. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  31. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  32. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  33. data/lib/rigor/analysis/fact_store.rb +5 -4
  34. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
  36. data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
  37. data/lib/rigor/analysis/runner.rb +17 -6
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  39. data/lib/rigor/analysis/worker_session.rb +10 -14
  40. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  41. data/lib/rigor/cache/store.rb +5 -3
  42. data/lib/rigor/cli/annotate_command.rb +28 -7
  43. data/lib/rigor/cli/baseline_command.rb +4 -3
  44. data/lib/rigor/cli/check_command.rb +138 -16
  45. data/lib/rigor/cli/coverage_command.rb +138 -31
  46. data/lib/rigor/cli/coverage_mutation.rb +149 -0
  47. data/lib/rigor/cli/coverage_scan.rb +57 -0
  48. data/lib/rigor/cli/explain_command.rb +2 -0
  49. data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
  50. data/lib/rigor/cli/fused_protection_report.rb +76 -0
  51. data/lib/rigor/cli/lsp_command.rb +3 -7
  52. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  53. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  54. data/lib/rigor/cli/options.rb +9 -0
  55. data/lib/rigor/cli/plugins_command.rb +2 -1
  56. data/lib/rigor/cli/protection_renderer.rb +63 -0
  57. data/lib/rigor/cli/protection_report.rb +68 -0
  58. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  59. data/lib/rigor/cli/trace_command.rb +2 -1
  60. data/lib/rigor/cli/triage_command.rb +2 -1
  61. data/lib/rigor/cli/type_of_command.rb +1 -1
  62. data/lib/rigor/cli/type_scan_command.rb +2 -1
  63. data/lib/rigor/cli.rb +3 -2
  64. data/lib/rigor/config_audit.rb +152 -0
  65. data/lib/rigor/configuration/dependencies.rb +2 -4
  66. data/lib/rigor/configuration.rb +57 -7
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  68. data/lib/rigor/environment/class_registry.rb +4 -3
  69. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  70. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  71. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  72. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  73. data/lib/rigor/environment/rbs_loader.rb +76 -5
  74. data/lib/rigor/environment.rb +66 -8
  75. data/lib/rigor/flow_contribution/fact.rb +1 -1
  76. data/lib/rigor/flow_contribution.rb +3 -5
  77. data/lib/rigor/inference/acceptance.rb +17 -9
  78. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  79. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  80. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  81. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  82. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  83. data/lib/rigor/inference/expression_typer.rb +20 -28
  84. data/lib/rigor/inference/hkt_body.rb +8 -11
  85. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  86. data/lib/rigor/inference/hkt_registry.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  88. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +169 -24
  89. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  90. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  91. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  92. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  93. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  94. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  95. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  96. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
  97. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  98. data/lib/rigor/inference/method_dispatcher.rb +40 -48
  99. data/lib/rigor/inference/mutation_widening.rb +5 -11
  100. data/lib/rigor/inference/narrowing.rb +14 -16
  101. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  102. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  103. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  104. data/lib/rigor/inference/protection_scanner.rb +86 -0
  105. data/lib/rigor/inference/scope_indexer.rb +129 -55
  106. data/lib/rigor/inference/statement_evaluator.rb +271 -114
  107. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  108. data/lib/rigor/inference/synthetic_method.rb +7 -7
  109. data/lib/rigor/language_server/completion_provider.rb +6 -12
  110. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  111. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  112. data/lib/rigor/language_server/hover_provider.rb +2 -3
  113. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  114. data/lib/rigor/language_server/server.rb +9 -17
  115. data/lib/rigor/language_server.rb +4 -5
  116. data/lib/rigor/plugin/base.rb +10 -8
  117. data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
  118. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  119. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  120. data/lib/rigor/plugin/macro.rb +4 -5
  121. data/lib/rigor/plugin/manifest.rb +45 -66
  122. data/lib/rigor/plugin/registry.rb +6 -7
  123. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  124. data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
  125. data/lib/rigor/protection/mutation_scanner.rb +180 -0
  126. data/lib/rigor/protection/mutator.rb +267 -0
  127. data/lib/rigor/protection/test_suite_oracle.rb +68 -0
  128. data/lib/rigor/rbs_extended.rb +24 -36
  129. data/lib/rigor/reflection.rb +4 -7
  130. data/lib/rigor/scope/discovery_index.rb +14 -2
  131. data/lib/rigor/scope.rb +54 -11
  132. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  133. data/lib/rigor/sig_gen/writer.rb +40 -2
  134. data/lib/rigor/signature_path_audit.rb +92 -0
  135. data/lib/rigor/source/constant_path.rb +62 -0
  136. data/lib/rigor/source.rb +1 -0
  137. data/lib/rigor/type/bound_method.rb +2 -11
  138. data/lib/rigor/type/combinator.rb +16 -3
  139. data/lib/rigor/type/constant.rb +2 -11
  140. data/lib/rigor/type/data_class.rb +2 -11
  141. data/lib/rigor/type/data_instance.rb +2 -11
  142. data/lib/rigor/type/hash_shape.rb +2 -11
  143. data/lib/rigor/type/integer_range.rb +2 -11
  144. data/lib/rigor/type/intersection.rb +2 -11
  145. data/lib/rigor/type/nominal.rb +2 -11
  146. data/lib/rigor/type/plain_lattice.rb +37 -0
  147. data/lib/rigor/type/refined.rb +72 -13
  148. data/lib/rigor/type/singleton.rb +2 -11
  149. data/lib/rigor/type/struct_class.rb +75 -0
  150. data/lib/rigor/type/struct_instance.rb +93 -0
  151. data/lib/rigor/type/tuple.rb +5 -15
  152. data/lib/rigor/type.rb +2 -0
  153. data/lib/rigor/version.rb +1 -1
  154. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  155. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  156. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
  157. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  158. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  159. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
  160. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  161. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  162. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
  163. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  164. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
  165. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  166. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  167. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  168. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  171. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  172. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
  173. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  174. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  175. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
  176. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  179. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  182. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  184. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  185. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  186. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  189. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  190. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  191. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
  195. data/sig/rigor/scope.rbs +9 -1
  196. data/sig/rigor/type.rbs +36 -1
  197. metadata +49 -1
@@ -233,6 +233,9 @@ module Rigor
233
233
  # included files first (in declaration order), then the
234
234
  # current file's own keys override. Relative paths inside
235
235
  # each file are resolved against that file's directory.
236
+ # Public so the CLI can run the include-aware load before
237
+ # applying `--treat-all-as-inline-rbs`'s plugin injection
238
+ # (see {CLI::CheckCommand#load_check_configuration}).
236
239
  def self.load_with_includes(path, visited: Set.new)
237
240
  absolute = File.expand_path(path)
238
241
  raise ArgumentError, "circular include: #{absolute}" if visited.include?(absolute)
@@ -328,7 +331,7 @@ module Rigor
328
331
  out["source_inference"] = left_si + right_si unless both_empty # rigor:disable flow.always-truthy-condition
329
332
  out
330
333
  end
331
- private_class_method :load_with_includes, :merge_includes, :resolve_paths_in, :deep_merge,
334
+ private_class_method :merge_includes, :resolve_paths_in, :deep_merge,
332
335
  :merge_value, :merge_dependencies_hash
333
336
 
334
337
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
@@ -437,6 +440,32 @@ module Rigor
437
440
  }
438
441
  end
439
442
 
443
+ # ADR-50 § WD2 — returns a sibling Configuration whose bleeding-edge
444
+ # selection (and the derived `bleeding_edge_severity_overrides` the two
445
+ # {SeverityProfile.resolve} sites consult) is replaced by `value`,
446
+ # leaving every other field shared with the receiver. `value` takes the
447
+ # same forms as the `bleeding_edge:` config key — `false` / `true` / a
448
+ # feature-id Array / `{ "all" => true, "except" => [...] }` — and is
449
+ # normalised through the same {#coerce_bleeding_edge} path, so an unknown
450
+ # id stays inert.
451
+ #
452
+ # The CLI's `--bleeding-edge[=ids]` / `--no-bleeding-edge` flag uses this
453
+ # to override the configured selection for a single run (the same
454
+ # CLI-over-config precedence `--workers` and `--no-cache` follow). It is a
455
+ # frozen `dup` with the two bleeding-edge ivars re-set: `dup` returns an
456
+ # unfrozen shallow copy (every other ivar is the receiver's deeply-frozen
457
+ # value, safe to share read-only), the two replacements are themselves
458
+ # deeply frozen, and the re-`freeze` keeps the result `Ractor.shareable?`
459
+ # for the worker path.
460
+ def with_bleeding_edge(value)
461
+ selector = coerce_bleeding_edge(value)
462
+ copy = dup
463
+ copy.instance_variable_set(:@bleeding_edge, selector)
464
+ copy.instance_variable_set(:@bleeding_edge_severity_overrides,
465
+ BleedingEdge.severity_overrides_for(selector))
466
+ copy.freeze
467
+ end
468
+
440
469
  private
441
470
 
442
471
  # ADR-17 slice 4 — `pre_eval:` glob expansion. Each entry is
@@ -499,10 +528,9 @@ module Rigor
499
528
  s.dup.freeze
500
529
  end
501
530
 
502
- # Slice 2 only accepts `:disabled` for the network policy. The
503
- # YAML scalar may arrive as a String (`"disabled"`) or already
504
- # as the Symbol; coerce to the canonical Symbol shape so the
505
- # downstream `TrustPolicy` constructor stays strict.
531
+ # YAML scalar may arrive as a String or already as a Symbol;
532
+ # coerce to the canonical Symbol shape so the downstream
533
+ # `TrustPolicy` constructor stays strict.
506
534
  #
507
535
  # The accepted set is duplicated from
508
536
  # {Rigor::Plugin::TrustPolicy::VALID_NETWORK_POLICIES} so
@@ -572,6 +600,18 @@ module Rigor
572
600
  raise ArgumentError, "severity_overrides must be a Hash, got #{value.inspect}" unless value.is_a?(Hash)
573
601
 
574
602
  value.to_h do |k, v|
603
+ # YAML 1.1 parses bare `off`/`on`/`no`/`yes`/`true`/`false`
604
+ # as booleans, so a user who wrote `off` (a valid severity)
605
+ # without quotes hands us `false`. Catch the non-Symbol /
606
+ # non-String case before `to_sym` blows up with a backtrace.
607
+ unless v.is_a?(String) || v.is_a?(Symbol)
608
+ hint = v == false ? %( — did you mean the string "off"?) : ""
609
+ raise ArgumentError,
610
+ "severity_overrides[#{k.inspect}] is #{v.inspect}, a YAML boolean#{hint} " \
611
+ "Bare off/on/no/yes/true/false are parsed as booleans; quote the severity " \
612
+ "(e.g. \"off\")."
613
+ end
614
+
575
615
  sym = v.to_sym
576
616
  unless SeverityProfile::VALID_SEVERITIES.include?(sym)
577
617
  raise ArgumentError,
@@ -593,7 +633,7 @@ module Rigor
593
633
  case value
594
634
  when nil, false then { "mode" => "none" }
595
635
  when true then { "mode" => "all" }
596
- when Array then { "mode" => "list", "ids" => value.map(&:to_s).freeze }
636
+ when Array then { "mode" => "list", "ids" => freeze_ids(value) }
597
637
  when Hash then coerce_bleeding_edge_hash(value)
598
638
  else
599
639
  raise ArgumentError,
@@ -605,12 +645,22 @@ module Rigor
605
645
  def coerce_bleeding_edge_hash(value)
606
646
  hash = value.to_h { |k, v| [k.to_s, v] }
607
647
  if hash.fetch("all", false) == true
608
- { "mode" => "all", "except" => Array(hash["except"]).map(&:to_s).freeze }
648
+ { "mode" => "all", "except" => freeze_ids(Array(hash["except"])) }
609
649
  else
610
650
  { "mode" => "none" }
611
651
  end
612
652
  end
613
653
 
654
+ # Feature ids reach `coerce_bleeding_edge` from YAML, a `to_h` round-trip,
655
+ # or the CLI's `--bleeding-edge=a,b` (all runtime-created, non-frozen
656
+ # Strings). Each id String is frozen so the normalized selector — and thus
657
+ # the whole Configuration — stays deeply frozen and `Ractor.shareable?`
658
+ # for the worker path (the same invariant `#initialize`'s final `freeze`
659
+ # upholds for every other field).
660
+ def freeze_ids(ids)
661
+ ids.map { |id| id.to_s.dup.freeze }.freeze
662
+ end
663
+
614
664
  # Renders the normalized selector back into the user-facing
615
665
  # `bleeding_edge:` form for `#to_h` round-trips.
616
666
  def bleeding_edge_to_h
@@ -4,7 +4,7 @@ require "yaml"
4
4
 
5
5
  module Rigor
6
6
  class Environment
7
- # Open item O4 — target-project Bundler awareness.
7
+ # Target-project Bundler awareness (O4, implemented).
8
8
  #
9
9
  # Walks a Bundler-installed gem tree (e.g., the project's
10
10
  # `vendor/bundle` or a Docker-mounted bundle root) and
@@ -84,11 +84,12 @@ module Rigor
84
84
  # supplied) minus any whose `(name, version, platform)`
85
85
  # does not match a lockfile entry.
86
86
  def self.discover(bundle_path:, project_root: Dir.pwd, auto_detect: true,
87
- skip_gems: SKIPPED_GEMS_BY_DEFAULT, locked_gems: nil)
87
+ skip_gems: SKIPPED_GEMS_BY_DEFAULT, locked_gems: nil, home: nil)
88
88
  resolved = resolve_bundle_path(
89
89
  bundle_path: bundle_path,
90
90
  project_root: project_root,
91
- auto_detect: auto_detect
91
+ auto_detect: auto_detect,
92
+ home: home
92
93
  )
93
94
  return [] if resolved.nil?
94
95
 
@@ -143,7 +144,7 @@ module Rigor
143
144
  # Returns `Pathname` resolved bundle path, or `nil` when
144
145
  # neither explicit nor auto-detected. Public for the stats
145
146
  # banner so end users can see what rigor picked up.
146
- def self.resolve_bundle_path(bundle_path:, project_root: Dir.pwd, auto_detect: true)
147
+ def self.resolve_bundle_path(bundle_path:, project_root: Dir.pwd, auto_detect: true, home: nil)
147
148
  if bundle_path
148
149
  path = Pathname.new(File.expand_path(bundle_path.to_s, project_root))
149
150
  return path if path.directory?
@@ -153,31 +154,77 @@ module Rigor
153
154
 
154
155
  return nil unless auto_detect
155
156
 
156
- detected = auto_detect(project_root: project_root)
157
+ detected = auto_detect(project_root: project_root, home: home)
157
158
  Pathname.new(detected) if detected
158
159
  end
159
160
 
160
- # Auto-detection order:
161
+ # Auto-detection order — project-local strategies win over the
162
+ # user-global one, mirroring Bundler's own config precedence:
161
163
  # 1. `<project_root>/.bundle/config` carries `BUNDLE_PATH:`
162
164
  # set by `bundle config set --local path <dir>`.
163
165
  # 2. `<project_root>/vendor/bundle/` — the conventional
164
166
  # in-tree install location when a developer ran
165
167
  # `bundle install --path vendor/bundle`.
166
- # 3. `nil` let the caller proceed without bundle sig
168
+ # 3. The user-global bundler config `<home>/.bundle/config`
169
+ # `BUNDLE_PATH:` (`bundle config set --global path <dir>`),
170
+ # resolved relative to the project root and used only when
171
+ # it points at an existing directory — the last resort for
172
+ # a project with no in-tree bundle. Purely additive: it is
173
+ # consulted only when steps 1–2 found nothing, so it never
174
+ # changes an already-working detection.
175
+ # 4. `nil` — let the caller proceed without bundle sig
167
176
  # discovery (rigor's vendored RBS still loads).
168
- def self.auto_detect(project_root:)
169
- from_config = read_bundle_config_path(project_root)
177
+ #
178
+ # Note (ADR-27): rigor reads the *project* as data, so
179
+ # detection is limited to paths recorded in project-local or
180
+ # user-global Bundler config files. The pure-default install
181
+ # location — gems in the active Ruby's GEM_HOME with no `path`
182
+ # configured — is the *project's* Ruby's gem home, which the
183
+ # isolated analyzer cannot know without running the project's
184
+ # toolchain. Point rigor at it with `bundler.bundle_path:`, or
185
+ # supply signatures via `rbs collection install` /
186
+ # `dependencies.source_inference:`. `BUNDLE_PATH` from rigor's
187
+ # own environment is deliberately NOT consulted — it describes
188
+ # rigor's bundle, not the analyzed project's.
189
+ #
190
+ # `home:` defaults to the invoking user's home directory; it is
191
+ # a parameter so tests stay hermetic (no read of the real
192
+ # `~/.bundle/config`).
193
+ def self.auto_detect(project_root:, home: nil)
194
+ from_config = read_bundle_config_path(File.join(project_root, ".bundle", "config"))
170
195
  return File.expand_path(from_config, project_root) if from_config
171
196
 
172
197
  vendor = File.join(project_root, "vendor", "bundle")
173
198
  return vendor if File.directory?(vendor)
174
199
 
200
+ global_bundle_path(project_root: project_root, home: home)
201
+ end
202
+
203
+ # Resolves the user-global `BUNDLE_PATH` against the project
204
+ # root, returning it only when it is an existing directory.
205
+ # Returns nil when there is no global config, no home, or the
206
+ # configured path does not exist.
207
+ def self.global_bundle_path(project_root:, home:)
208
+ home ||= default_home
209
+ return nil if home.nil?
210
+
211
+ configured = read_bundle_config_path(File.join(home, ".bundle", "config"))
212
+ return nil unless configured
213
+
214
+ resolved = File.expand_path(configured, project_root)
215
+ File.directory?(resolved) ? resolved : nil
216
+ end
217
+ private_class_method :global_bundle_path
218
+
219
+ def self.default_home
220
+ Dir.home
221
+ rescue StandardError
175
222
  nil
176
223
  end
224
+ private_class_method :default_home
177
225
 
178
- def self.read_bundle_config_path(project_root)
179
- config_path = File.join(project_root, ".bundle", "config")
180
- return nil unless File.exist?(config_path)
226
+ def self.read_bundle_config_path(config_path)
227
+ return nil unless config_path && File.exist?(config_path)
181
228
 
182
229
  # `.bundle/config` is YAML with all-caps env-style keys.
183
230
  # `BUNDLE_PATH:` is the canonical key (Bundler 2.x); the
@@ -185,7 +232,8 @@ module Rigor
185
232
  data = YAML.safe_load_file(config_path)
186
233
  return nil unless data.is_a?(Hash)
187
234
 
188
- data["BUNDLE_PATH"]
235
+ path = data["BUNDLE_PATH"]
236
+ path && !path.to_s.empty? ? path.to_s : nil
189
237
  rescue StandardError
190
238
  # Malformed `.bundle/config` should not break analysis;
191
239
  # silently skip auto-detection.
@@ -7,8 +7,8 @@ module Rigor
7
7
  # Resolves Ruby Class/Module objects to Rigor::Type::Nominal instances.
8
8
  # The hardcoded list spans the core classes the literal typer (Slice 1)
9
9
  # and the constant-resolution path (Slice 2 strengthening) need.
10
- # Slice 4 will extend the registry by reading RBS Definitions through
11
- # Rigor::Environment::RbsLoader.
10
+ # This is the static fast path for built-ins; `Environment` falls through
11
+ # to `RbsLoader` for all other names — the two tiers are complementary.
12
12
  #
13
13
  # See docs/internal-spec/inference-engine.md for the binding contract
14
14
  # (every entry below MUST always be recognised).
@@ -28,7 +28,8 @@ module Rigor
28
28
  # Common Ruby core classes that user code routinely names by constant
29
29
  # reference. Adding them to the registry lets `nominal_for_name`
30
30
  # resolve `Array`, `Hash`, etc. without each call site re-listing
31
- # them; Slice 4's RBS loader will subsume these once it lands.
31
+ # them. The static registry is the cheap fast path; `RbsLoader`
32
+ # extends coverage to all other RBS-declared names (Slice 4, landed).
32
33
  SLICE_2_BUILT_INS = [
33
34
  Array,
34
35
  Hash,
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Environment
5
+ # Per-key memoization container for {Environment#constant_for_name}.
6
+ # Held by {Environment} so the otherwise-frozen instance can cache
7
+ # constant-resolution results on first access — the sibling of
8
+ # {HktRegistryHolder}, generalised from one slot to a name-keyed map.
9
+ #
10
+ # Why this exists: `constant_for_name` is a pure function of its
11
+ # name for a given Environment (both the predefined-constant
12
+ # refinement table and the RBS loader's constant table are fixed for
13
+ # the Environment's lifetime), yet it is the hottest non-dispatch
14
+ # path on a large Rails app. {ExpressionTyper#resolve_constant_name}
15
+ # peels a lexical-candidate ladder per constant reference, so the
16
+ # same qualified names recur thousands of times across files — and
17
+ # each miss runs a `const_get` walk that raises + rescues `NameError`
18
+ # for every project-defined constant (the common case). Caching the
19
+ # result collapses the repeats to a hash lookup.
20
+ #
21
+ # Caches `nil` results too (a name that resolves to no refined /
22
+ # RBS constant type stays unresolved for the run), so the
23
+ # const_get-and-rescue is paid at most once per distinct name.
24
+ #
25
+ # Concurrency: single-threaded use only, the same discipline as
26
+ # {HktRegistryHolder} — the Ractor pool builds a per-worker
27
+ # Environment and the LSP single-publish invariant serialises the
28
+ # shared-Environment reader path. If a future caller introduces a
29
+ # multi-threaded reader against one Environment, the synchronisation
30
+ # belongs at that caller's seam, not here.
31
+ class ConstantTypeCacheHolder
32
+ def initialize
33
+ @cache = {}
34
+ end
35
+
36
+ def fetch(key)
37
+ return @cache[key] if @cache.key?(key)
38
+
39
+ @cache[key] = yield
40
+ end
41
+ end
42
+ end
43
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rigor
4
4
  class Environment
5
- # Open item O4 Layer 3 — Gemfile.lock parse.
5
+ # Gemfile.lock parser for target-project bundler awareness (O4 Layer 3, implemented).
6
6
  #
7
7
  # Parses a target project's `Gemfile.lock` via Bundler's
8
8
  # `LockfileParser` and exposes the locked gem set as a frozen
@@ -4,8 +4,7 @@ require "yaml"
4
4
 
5
5
  module Rigor
6
6
  class Environment
7
- # Open item O4 Layer 3 slice 2 — `rbs collection install`
8
- # awareness.
7
+ # `rbs collection install` awareness (O4 Layer 3 slice 2, implemented).
9
8
  #
10
9
  # When the target project has been set up with `rbs
11
10
  # collection install` (the standard RBS-ecosystem flow for
@@ -42,7 +42,8 @@ module Rigor
42
42
  # enough that hard-coding is acceptable; a directory walk
43
43
  # at every call would add stat-cost to no benefit.)
44
44
  VENDORED_GEM_NAMES = Set[
45
- "bcrypt", "bundler", "cgi", "did_you_mean", "idn-ruby", "mysql2", "nokogiri", "pg", "prism", "redis", "rubygems"
45
+ "ast", "bcrypt", "bundler", "cgi", "did_you_mean",
46
+ "idn-ruby", "mysql2", "nokogiri", "pg", "prism", "redis", "rubygems"
46
47
  ].freeze
47
48
 
48
49
  # @param locked_gems [Hash{String => LockfileResolver::LockedGem}]
@@ -323,6 +323,33 @@ module Rigor
323
323
  [Pathname(CORE_OVERLAY_SIGS_ROOT)]
324
324
  end
325
325
 
326
+ # Rigor-owned per-gem RBS overlays (`data/gem_overlay/<gem>/`),
327
+ # ADR-72. Unlike the unconditional `core_overlay`, each gem's
328
+ # overlay is loaded ONLY when that gem is locked in the
329
+ # project's Gemfile.lock but ships no RBS of its own —
330
+ # {Environment.for_project} decides eligibility and passes the
331
+ # already-filtered gem-name set here. One directory per gem name
332
+ # keeps the membership check a cheap `File.directory?`.
333
+ GEM_OVERLAY_SIGS_ROOT = File.expand_path(
334
+ "../../../data/gem_overlay",
335
+ __dir__
336
+ ).freeze
337
+
338
+ # @param gem_names [Enumerable<String>] overlay-eligible
339
+ # Gemfile.lock gem names (the caller filters to the
340
+ # `:missing`-coverage, no-conflicting-plugin set).
341
+ # @return [Array<Pathname>] the bundled overlay directory for
342
+ # each gem that ships one; empty when none match or the
343
+ # overlay root is absent.
344
+ def gem_overlay_sig_paths(gem_names)
345
+ return [] unless File.directory?(GEM_OVERLAY_SIGS_ROOT)
346
+
347
+ gem_names.filter_map do |name|
348
+ dir = File.join(GEM_OVERLAY_SIGS_ROOT, name.to_s)
349
+ Pathname(dir) if File.directory?(dir)
350
+ end
351
+ end
352
+
326
353
  def vendored_gem_sig_paths
327
354
  return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
328
355
 
@@ -363,11 +390,11 @@ module Rigor
363
390
  # out at build time so the loader stays robust to fixtures and
364
391
  # bare repositories.
365
392
  # @param cache_store [Rigor::Cache::Store, nil] the persistent
366
- # cache the loader consults for translated constant lookups
367
- # (and, in later v0.0.9 slices, other Marshal-clean
368
- # reflection artefacts). Pass `nil` (the default) to skip
369
- # the cache entirely; the runner threads its own Store
370
- # through here when caching is enabled.
393
+ # cache the loader threads through to `RbsEnvironment`,
394
+ # `RbsKnownClassNames`, `RbsConstantTable`,
395
+ # `RbsClassAncestorTable`, and `RbsClassTypeParamNames`
396
+ # producers. Pass `nil` (the default) to skip caching; the
397
+ # runner threads its own Store through here when enabled.
371
398
  # @param virtual_rbs [Array<[String, String]>] ADR-32 WD4 —
372
399
  # `[virtual_filename, rbs_source]` pairs synthesised from
373
400
  # project source by a plugin's
@@ -643,6 +670,29 @@ module Rigor
643
670
  interface_definition(interface_name)&.methods&.keys
644
671
  end
645
672
 
673
+ # @param rbs_alias [RBS::Types::Alias] a type-alias reference (`string`,
674
+ # `int`, `range[int?]`, …) appearing in a method signature.
675
+ # @return [RBS::Types::t, nil] the alias's aliased type one level out,
676
+ # with type arguments substituted for a generic alias (`string` →
677
+ # `::String | ::_ToStr`; `range[int?]` → `::Range[int?] |
678
+ # ::_Range[int?]`), or nil for an unresolved name. Lets a caller see
679
+ # through the alias that {Inference::RbsTypeTranslator} otherwise
680
+ # degrades to `untyped`, which is why an interface/alias parameter
681
+ # does not reject `nil`. `expand_alias2` handles the (rarer) generic
682
+ # case — a `range[T]` param previously fell back to "admits", which
683
+ # suppressed e.g. `MatchData#[](nil)`.
684
+ def expand_type_alias(rbs_alias)
685
+ return nil if env.nil?
686
+
687
+ name = rbs_alias.name
688
+ name = name.absolute! unless name.absolute?
689
+ return nil unless env.type_alias_decls.key?(name)
690
+
691
+ builder.expand_alias2(name, rbs_alias.args)
692
+ rescue ::RBS::BaseError, StandardError
693
+ nil
694
+ end
695
+
646
696
  # @return [RBS::Definition, nil] the resolved singleton (class
647
697
  # object) definition for `class_name`. The methods on this
648
698
  # definition are the *class methods* of `class_name`, including
@@ -1034,6 +1084,8 @@ module Rigor
1034
1084
  rbs_name = parse_type_name(class_name)
1035
1085
  return nil unless rbs_name
1036
1086
  return nil if env.nil?
1087
+
1088
+ rbs_name = canonical_module_name(rbs_name)
1037
1089
  return nil unless env.class_decls.key?(rbs_name)
1038
1090
 
1039
1091
  builder.build_instance(rbs_name)
@@ -1045,6 +1097,8 @@ module Rigor
1045
1097
  rbs_name = parse_type_name(class_name)
1046
1098
  return nil unless rbs_name
1047
1099
  return nil if env.nil?
1100
+
1101
+ rbs_name = canonical_module_name(rbs_name)
1048
1102
  return nil unless env.class_decls.key?(rbs_name)
1049
1103
 
1050
1104
  builder.build_singleton(rbs_name)
@@ -1052,6 +1106,23 @@ module Rigor
1052
1106
  nil
1053
1107
  end
1054
1108
 
1109
+ # Resolve an RBS class/module ALIAS to its canonical declared name.
1110
+ # `class Mutex = Thread::Mutex` lives only in `class_alias_decls`, so
1111
+ # `class_known?` reports it (it checks that table) but the definition
1112
+ # builder — which only knows `class_decls` — could not enumerate its
1113
+ # methods, leaving alias classes (`Mutex`, and any `X = Y`) with no
1114
+ # resolvable method surface. Normalising via the env (RBS's own alias
1115
+ # resolution) before the `class_decls` guard fixes dispatch AND the
1116
+ # `call.undefined-method` existence check on them. A non-alias name, or
1117
+ # one that does not normalise, is returned unchanged.
1118
+ def canonical_module_name(rbs_name)
1119
+ return rbs_name unless env.class_alias_decls.key?(rbs_name)
1120
+
1121
+ env.normalize_module_name?(rbs_name) || rbs_name
1122
+ rescue ::RBS::BaseError
1123
+ rbs_name
1124
+ end
1125
+
1055
1126
  # Memoised on `@state` (the per-loader store also holding `:env` /
1056
1127
  # `:builder`): `RBS::TypeName.parse` is a pure, deterministic
1057
1128
  # function of the normalised string, and the `RBS::TypeName` it
@@ -7,6 +7,7 @@ require_relative "environment/rbs_loader"
7
7
  require_relative "environment/reflection"
8
8
  require_relative "environment/reporters"
9
9
  require_relative "environment/hkt_registry_holder"
10
+ require_relative "environment/constant_type_cache_holder"
10
11
  require_relative "environment/bundle_sig_discovery"
11
12
  require_relative "environment/lockfile_resolver"
12
13
  require_relative "environment/rbs_collection_discovery"
@@ -15,12 +16,13 @@ require_relative "inference/synthetic_method_index"
15
16
  require_relative "inference/project_patched_methods"
16
17
  require_relative "inference/hkt_registry"
17
18
  require_relative "builtins/hkt_builtins"
19
+ require_relative "builtins/predefined_constant_refinements"
18
20
  require_relative "type_node/name_scope"
19
21
  require_relative "type_node/resolver_chain"
20
22
 
21
23
  module Rigor
22
24
  # The engine's view of the type universe outside the current scope.
23
- # Slice 1 only exposed the class registry; Slice 4 adds the RBS loader,
25
+ # Slice 1 exposed only the class registry; Slice 4 added the RBS loader,
24
26
  # which threads through ExpressionTyper and MethodDispatcher to type
25
27
  # constant references and method calls that the literal-typer and
26
28
  # constant-folding tiers cannot answer.
@@ -63,6 +65,13 @@ module Rigor
63
65
  prism rbs
64
66
  ].freeze
65
67
 
68
+ # ADR-72 — a Gemfile.lock gem name mapped to the opt-in plugin id
69
+ # that ships the SAME core-ext RBS. When that plugin is loaded the
70
+ # auto-overlay for the gem stands down, so the two never both
71
+ # declare the methods (which would raise a duplicate-declaration
72
+ # error). Keyed on the gem name `RbsCoverageReport` reports.
73
+ GEM_OVERLAY_PLUGIN_IDS = { "activesupport" => "activesupport-core-ext" }.freeze
74
+
66
75
  attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
67
76
  :reporters, :name_scope,
68
77
  :synthetic_method_index, :project_patched_methods
@@ -123,6 +132,7 @@ module Rigor
123
132
  # build at all.
124
133
  @hkt_registry_base = hkt_registry || Inference::HktRegistry::EMPTY
125
134
  @hkt_registry_holder = HktRegistryHolder.new
135
+ @constant_type_cache = ConstantTypeCacheHolder.new
126
136
  @name_scope = build_name_scope
127
137
  freeze
128
138
  end
@@ -288,7 +298,24 @@ module Rigor
288
298
  # collection discovery. A duplicate-declaration conflict
289
299
  # degrades through the same O7 failure-memo path.
290
300
  plugin_sig_paths = plugin_registry ? plugin_registry.signature_paths.map(&:to_s) : []
291
- loader_signature_paths = resolved_paths + plugin_sig_paths + gem_sig_paths + collection_paths
301
+ # ADR-72 Gemfile.lock-gated bundled RBS overlays. For each
302
+ # locked gem that ships no RBS through any resolution path
303
+ # (`:missing`) and has a Rigor-bundled overlay, load that
304
+ # overlay so its core-class extensions resolve (e.g.
305
+ # `Integer#minutes` on a Rails project) — turning a systematic
306
+ # false `call.undefined-method` into a no-op while a project
307
+ # WITHOUT the gem still sees the genuine diagnostic. Appended
308
+ # last so any RBS the project already supplies wins, and skipped
309
+ # for a gem whose opt-in plugin twin is loaded (no duplicate
310
+ # declaration). The paths ride in `loader_signature_paths`, so
311
+ # the env cache descriptor digests them for free.
312
+ overlay_paths = gem_overlay_paths(
313
+ locked: locked, default_libraries: merged_libraries,
314
+ bundle_sig_paths: gem_sig_paths, rbs_collection_paths: collection_paths,
315
+ plugin_registry: plugin_registry
316
+ )
317
+ loader_signature_paths = resolved_paths + plugin_sig_paths + gem_sig_paths +
318
+ collection_paths + overlay_paths
292
319
  # ADR-32 WD4 + WD5 — invoke each loaded plugin's
293
320
  # `source_rbs_synthesizer` once per project source file
294
321
  # and collect non-nil `[filename, rbs_source]` pairs.
@@ -343,6 +370,30 @@ module Rigor
343
370
  sig.directory? ? [sig] : []
344
371
  end
345
372
 
373
+ # ADR-72 — resolve the bundled RBS overlay directories to load for
374
+ # this project. A gem is eligible when it is locked in the
375
+ # Gemfile.lock, classified `:missing` by {RbsCoverageReport}
376
+ # (no RBS through default-library / vendored / bundle-`sig/` /
377
+ # rbs-collection), and its conflicting opt-in plugin (if any) is
378
+ # not loaded. Returns `[Pathname]`, deterministically ordered, or
379
+ # `[]` when no lockfile / no eligible gem.
380
+ def gem_overlay_paths(locked:, default_libraries:, bundle_sig_paths:,
381
+ rbs_collection_paths:, plugin_registry:)
382
+ return [] if locked.empty?
383
+
384
+ missing = RbsCoverageReport.classify(
385
+ locked_gems: locked, default_libraries: default_libraries,
386
+ bundle_sig_paths: bundle_sig_paths, rbs_collection_paths: rbs_collection_paths
387
+ ).select { |row| row.source == :missing }.map(&:gem_name)
388
+
389
+ loaded_ids = plugin_registry ? plugin_registry.ids.to_set : Set.new
390
+ eligible = missing.reject do |gem_name|
391
+ plugin_id = GEM_OVERLAY_PLUGIN_IDS[gem_name]
392
+ plugin_id && loaded_ids.include?(plugin_id)
393
+ end.sort
394
+ RbsLoader.gem_overlay_sig_paths(eligible)
395
+ end
396
+
346
397
  # ADR-32 WD4 + WD5 — for each project source file, invoke
347
398
  # every plugin-registered synthesizer once and collect
348
399
  # non-nil returns. The returned array is `[[virtual_filename,
@@ -453,11 +504,11 @@ module Rigor
453
504
  def invoke_synthesizer_safely(callable, path)
454
505
  callable.call(path.to_s)
455
506
  rescue StandardError
456
- # WD6 fail-soft — a synthesizer that raises does NOT
457
- # crash analysis. Slice 2b will turn this into a
458
- # `source-rbs-synthesis-failed` info diagnostic; for now
459
- # the contract is "no analysis crash on a misbehaving
460
- # synthesizer".
507
+ # WD6 fail-soft — a synthesizer that raises does NOT crash
508
+ # analysis. Unlike the `[:error, msg]` return path (which
509
+ # the runner surfaces as `source-rbs-synthesis-failed`),
510
+ # an unhandled raise is swallowed silently; the
511
+ # unexamined-raise channel is deliberately silent per WD6.
461
512
  nil
462
513
  end
463
514
  end
@@ -504,7 +555,14 @@ module Rigor
504
555
  def constant_for_name(name)
505
556
  return nil if rbs_loader.nil?
506
557
 
507
- rbs_loader.constant_type(name.to_s)
558
+ # Pure function of `name` for this Environment's lifetime — the
559
+ # refinement table and the RBS constant table are both fixed — so
560
+ # memoise across the lexical-candidate ladder's heavy name reuse.
561
+ key = name.to_s
562
+ @constant_type_cache.fetch(key) do
563
+ Builtins::PredefinedConstantRefinements.lookup(key) ||
564
+ rbs_loader.constant_type(key)
565
+ end
508
566
  end
509
567
 
510
568
  # Returns true when the constant name is known to either the static
@@ -18,7 +18,7 @@ module Rigor
18
18
  # (`Rigor::RbsExtended::PredicateEffect`).
19
19
  # 3. RBS::Extended `assert*` directives
20
20
  # (`Rigor::RbsExtended::AssertEffect`).
21
- # 4. Future plugin contributions (slice 5 emission protocol).
21
+ # 4. Plugin contributions via `type_specifier` DSL (ADR-52).
22
22
  #
23
23
  # Each of those four carriers translates to / from Fact at
24
24
  # its boundary; downstream of {Rigor::FlowContribution#to_element_list}
@@ -20,11 +20,9 @@ module Rigor
20
20
  # contract — see ADR-2 § "Flow Contribution Bundle" for the binding
21
21
  # definition.
22
22
  #
23
- # The element-list flattening (`to_element_list`) ADR-2 mentions is
24
- # intentionally not implemented yet: it is the analyzer-internal
25
- # bookkeeping behind the merge policy and will land alongside the
26
- # plugin contribution merger in v0.1.0. Plugin authors should not
27
- # rely on it.
23
+ # `to_element_list` and `Merger` are implemented; plugin authors
24
+ # should not depend on the `Element` shape — the bundle is the
25
+ # public contract.
28
26
  class FlowContribution
29
27
  # Provenance carries the metadata every contribution needs for
30
28
  # diagnostic attribution and cache invalidation. `source_family`