rigortype 0.1.18 → 0.2.0

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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -224
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
  8. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  9. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
  10. data/lib/rigor/analysis/check_rules.rb +756 -132
  11. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  12. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  13. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  14. data/lib/rigor/analysis/diagnostic.rb +8 -0
  15. data/lib/rigor/analysis/fact_store.rb +5 -4
  16. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  17. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
  18. data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
  19. data/lib/rigor/analysis/runner.rb +75 -27
  20. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  21. data/lib/rigor/analysis/worker_session.rb +31 -25
  22. data/lib/rigor/bleeding_edge.rb +123 -0
  23. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  24. data/lib/rigor/cache/descriptor.rb +86 -8
  25. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  26. data/lib/rigor/cache/store.rb +5 -3
  27. data/lib/rigor/cli/annotate_command.rb +122 -16
  28. data/lib/rigor/cli/baseline_command.rb +4 -3
  29. data/lib/rigor/cli/check_command.rb +118 -16
  30. data/lib/rigor/cli/coverage_command.rb +148 -16
  31. data/lib/rigor/cli/coverage_scan.rb +57 -0
  32. data/lib/rigor/cli/explain_command.rb +2 -0
  33. data/lib/rigor/cli/lsp_command.rb +3 -7
  34. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  35. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  36. data/lib/rigor/cli/options.rb +9 -0
  37. data/lib/rigor/cli/plugins_command.rb +4 -5
  38. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  39. data/lib/rigor/cli/protection_renderer.rb +63 -0
  40. data/lib/rigor/cli/protection_report.rb +68 -0
  41. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  42. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  43. data/lib/rigor/cli/trace_command.rb +2 -1
  44. data/lib/rigor/cli/triage_command.rb +8 -4
  45. data/lib/rigor/cli/triage_renderer.rb +15 -1
  46. data/lib/rigor/cli/type_of_command.rb +1 -1
  47. data/lib/rigor/cli/type_scan_command.rb +2 -1
  48. data/lib/rigor/cli.rb +12 -3
  49. data/lib/rigor/configuration/dependencies.rb +2 -4
  50. data/lib/rigor/configuration/severity_profile.rb +13 -1
  51. data/lib/rigor/configuration.rb +100 -6
  52. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  53. data/lib/rigor/environment/class_registry.rb +4 -3
  54. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  55. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  56. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  57. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  58. data/lib/rigor/environment/rbs_loader.rb +74 -5
  59. data/lib/rigor/environment.rb +17 -7
  60. data/lib/rigor/flow_contribution/fact.rb +1 -1
  61. data/lib/rigor/flow_contribution.rb +3 -5
  62. data/lib/rigor/inference/acceptance.rb +17 -9
  63. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  64. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  65. data/lib/rigor/inference/budget_trace.rb +29 -2
  66. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  67. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  68. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  69. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  70. data/lib/rigor/inference/expression_typer.rb +1072 -71
  71. data/lib/rigor/inference/hkt_body.rb +8 -11
  72. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  73. data/lib/rigor/inference/hkt_registry.rb +10 -11
  74. data/lib/rigor/inference/macro_block_self_type.rb +2 -2
  75. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  76. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  77. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
  78. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  79. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  80. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  81. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  82. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  83. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  84. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  85. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  86. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  87. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
  88. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  89. data/lib/rigor/inference/method_dispatcher.rb +112 -49
  90. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  91. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  92. data/lib/rigor/inference/mutation_widening.rb +147 -11
  93. data/lib/rigor/inference/narrowing.rb +284 -53
  94. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  95. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  97. data/lib/rigor/inference/protection_scanner.rb +86 -0
  98. data/lib/rigor/inference/scope_indexer.rb +821 -76
  99. data/lib/rigor/inference/statement_evaluator.rb +1179 -102
  100. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  101. data/lib/rigor/inference/synthetic_method.rb +7 -7
  102. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  103. data/lib/rigor/language_server/completion_provider.rb +6 -12
  104. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  105. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  106. data/lib/rigor/language_server/hover_provider.rb +2 -3
  107. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  108. data/lib/rigor/language_server/server.rb +9 -17
  109. data/lib/rigor/language_server.rb +4 -5
  110. data/lib/rigor/plugin/base.rb +245 -87
  111. data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
  112. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  113. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  114. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  115. data/lib/rigor/plugin/macro.rb +6 -8
  116. data/lib/rigor/plugin/manifest.rb +49 -90
  117. data/lib/rigor/plugin/node_rule_walk.rb +59 -14
  118. data/lib/rigor/plugin/registry.rb +18 -18
  119. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  120. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  121. data/lib/rigor/protection/mutator.rb +246 -0
  122. data/lib/rigor/rbs_extended.rb +24 -36
  123. data/lib/rigor/reflection.rb +4 -7
  124. data/lib/rigor/scope/discovery_index.rb +16 -2
  125. data/lib/rigor/scope.rb +185 -16
  126. data/lib/rigor/sig_gen/generator.rb +8 -0
  127. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  128. data/lib/rigor/sig_gen/writer.rb +40 -2
  129. data/lib/rigor/source/constant_path.rb +62 -0
  130. data/lib/rigor/source.rb +1 -0
  131. data/lib/rigor/triage/catalogue.rb +4 -19
  132. data/lib/rigor/triage.rb +69 -1
  133. data/lib/rigor/type/bound_method.rb +2 -11
  134. data/lib/rigor/type/combinator.rb +45 -3
  135. data/lib/rigor/type/constant.rb +2 -11
  136. data/lib/rigor/type/data_class.rb +2 -11
  137. data/lib/rigor/type/data_instance.rb +2 -11
  138. data/lib/rigor/type/hash_shape.rb +2 -11
  139. data/lib/rigor/type/integer_range.rb +2 -11
  140. data/lib/rigor/type/intersection.rb +2 -11
  141. data/lib/rigor/type/nominal.rb +2 -11
  142. data/lib/rigor/type/plain_lattice.rb +37 -0
  143. data/lib/rigor/type/refined.rb +72 -13
  144. data/lib/rigor/type/singleton.rb +2 -11
  145. data/lib/rigor/type/struct_class.rb +75 -0
  146. data/lib/rigor/type/struct_instance.rb +93 -0
  147. data/lib/rigor/type/tuple.rb +5 -15
  148. data/lib/rigor/type.rb +2 -0
  149. data/lib/rigor/version.rb +1 -1
  150. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  151. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  152. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  154. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
  157. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  158. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  159. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  160. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
  163. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  164. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  165. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  166. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  167. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  168. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
  171. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  172. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  173. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
  174. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  175. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  176. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
  179. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
  182. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  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 +21 -27
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  189. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  190. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  191. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  195. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  196. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  197. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
  198. data/sig/rigor/analysis/fact_store.rbs +3 -0
  199. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  200. data/sig/rigor/plugin/base.rbs +5 -2
  201. data/sig/rigor/plugin/manifest.rbs +1 -2
  202. data/sig/rigor/scope.rbs +18 -1
  203. data/sig/rigor/type.rbs +37 -1
  204. data/sig/rigor.rbs +1 -1
  205. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  206. data/skills/rigor-plugin-author/SKILL.md +6 -4
  207. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  208. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  209. metadata +25 -2
  210. data/lib/rigor/plugin/macro/external_file.rb +0 -143
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
+ require_relative "bleeding_edge"
5
6
  require_relative "configuration/dependencies"
6
7
  require_relative "configuration/severity_profile"
7
8
 
@@ -87,6 +88,15 @@ module Rigor
87
88
  },
88
89
  "severity_profile" => "balanced",
89
90
  "severity_overrides" => {},
91
+ # ADR-50 § WD2 — bleeding-edge overlay opt-in. Selects which of
92
+ # the *next major's* queued changes ({Rigor::BleedingEdge}) this
93
+ # project adopts early. Orthogonal to `severity_profile:`. Accepts
94
+ # `false` (default — adopt none), `true` (adopt the whole
95
+ # overlay), a list of feature ids (adopt only those), or
96
+ # `{ all: true, except: [ids] }` (adopt all but the named). The
97
+ # overlay is empty today, so every form is currently a no-op; it
98
+ # becomes live when the first discipline is queued for a major.
99
+ "bleeding_edge" => false,
90
100
  "dependencies" => {
91
101
  "source_inference" => [],
92
102
  "budget_per_gem" => Configuration::Dependencies::DEFAULT_BUDGET_PER_GEM
@@ -181,6 +191,7 @@ module Rigor
181
191
  :plugins_io_network, :plugins_io_allowed_paths,
182
192
  :plugins_io_allowed_url_hosts,
183
193
  :severity_profile, :severity_overrides,
194
+ :bleeding_edge, :bleeding_edge_severity_overrides,
184
195
  :dependencies, :parallel_workers,
185
196
  :bundler_bundle_path, :bundler_auto_detect, :bundler_lockfile,
186
197
  :rbs_collection_lockfile, :rbs_collection_auto_detect,
@@ -222,6 +233,9 @@ module Rigor
222
233
  # included files first (in declaration order), then the
223
234
  # current file's own keys override. Relative paths inside
224
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}).
225
239
  def self.load_with_includes(path, visited: Set.new)
226
240
  absolute = File.expand_path(path)
227
241
  raise ArgumentError, "circular include: #{absolute}" if visited.include?(absolute)
@@ -317,7 +331,7 @@ module Rigor
317
331
  out["source_inference"] = left_si + right_si unless both_empty # rigor:disable flow.always-truthy-condition
318
332
  out
319
333
  end
320
- private_class_method :load_with_includes, :merge_includes, :resolve_paths_in, :deep_merge,
334
+ private_class_method :merge_includes, :resolve_paths_in, :deep_merge,
321
335
  :merge_value, :merge_dependencies_hash
322
336
 
323
337
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
@@ -355,6 +369,10 @@ module Rigor
355
369
  @severity_overrides = coerce_severity_overrides(
356
370
  data.fetch("severity_overrides", DEFAULTS.fetch("severity_overrides"))
357
371
  )
372
+ @bleeding_edge = coerce_bleeding_edge(
373
+ data.fetch("bleeding_edge", DEFAULTS.fetch("bleeding_edge"))
374
+ )
375
+ @bleeding_edge_severity_overrides = BleedingEdge.severity_overrides_for(@bleeding_edge)
358
376
  @dependencies = Dependencies.from_h(
359
377
  data.fetch("dependencies", DEFAULTS.fetch("dependencies"))
360
378
  )
@@ -383,7 +401,7 @@ module Rigor
383
401
  end
384
402
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
385
403
 
386
- def to_h # rubocop:disable Metrics/MethodLength
404
+ def to_h # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
387
405
  {
388
406
  "target_ruby" => target_ruby,
389
407
  "paths" => paths,
@@ -405,6 +423,7 @@ module Rigor
405
423
  },
406
424
  "severity_profile" => severity_profile.to_s,
407
425
  "severity_overrides" => severity_overrides.to_h { |k, v| [k, v.to_s] },
426
+ "bleeding_edge" => bleeding_edge_to_h,
408
427
  "dependencies" => dependencies.to_h,
409
428
  "parallel" => {
410
429
  "workers" => parallel_workers
@@ -421,6 +440,32 @@ module Rigor
421
440
  }
422
441
  end
423
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
+
424
469
  private
425
470
 
426
471
  # ADR-17 slice 4 — `pre_eval:` glob expansion. Each entry is
@@ -483,10 +528,9 @@ module Rigor
483
528
  s.dup.freeze
484
529
  end
485
530
 
486
- # Slice 2 only accepts `:disabled` for the network policy. The
487
- # YAML scalar may arrive as a String (`"disabled"`) or already
488
- # as the Symbol; coerce to the canonical Symbol shape so the
489
- # 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.
490
534
  #
491
535
  # The accepted set is duplicated from
492
536
  # {Rigor::Plugin::TrustPolicy::VALID_NETWORK_POLICIES} so
@@ -566,5 +610,55 @@ module Rigor
566
610
  [k.to_s, sym]
567
611
  end.freeze
568
612
  end
613
+
614
+ # ADR-50 § WD2 — normalizes the `bleeding_edge:` selector to a
615
+ # canonical `{ "mode" => … }` hash (interpreted by
616
+ # {Rigor::BleedingEdge}). Validates *shape* only; membership against
617
+ # the overlay is intentionally NOT checked here (an unknown id stays
618
+ # inert, like an unknown `severity_overrides:` rule). Deep-frozen so
619
+ # the Configuration stays `Ractor.shareable?`.
620
+ def coerce_bleeding_edge(value)
621
+ case value
622
+ when nil, false then { "mode" => "none" }
623
+ when true then { "mode" => "all" }
624
+ when Array then { "mode" => "list", "ids" => freeze_ids(value) }
625
+ when Hash then coerce_bleeding_edge_hash(value)
626
+ else
627
+ raise ArgumentError,
628
+ "bleeding_edge must be true, false, a list of feature ids, " \
629
+ "or { all: true, except: [...] }, got #{value.inspect}"
630
+ end.freeze
631
+ end
632
+
633
+ def coerce_bleeding_edge_hash(value)
634
+ hash = value.to_h { |k, v| [k.to_s, v] }
635
+ if hash.fetch("all", false) == true
636
+ { "mode" => "all", "except" => freeze_ids(Array(hash["except"])) }
637
+ else
638
+ { "mode" => "none" }
639
+ end
640
+ end
641
+
642
+ # Feature ids reach `coerce_bleeding_edge` from YAML, a `to_h` round-trip,
643
+ # or the CLI's `--bleeding-edge=a,b` (all runtime-created, non-frozen
644
+ # Strings). Each id String is frozen so the normalized selector — and thus
645
+ # the whole Configuration — stays deeply frozen and `Ractor.shareable?`
646
+ # for the worker path (the same invariant `#initialize`'s final `freeze`
647
+ # upholds for every other field).
648
+ def freeze_ids(ids)
649
+ ids.map { |id| id.to_s.dup.freeze }.freeze
650
+ end
651
+
652
+ # Renders the normalized selector back into the user-facing
653
+ # `bleeding_edge:` form for `#to_h` round-trips.
654
+ def bleeding_edge_to_h
655
+ case bleeding_edge["mode"]
656
+ when "all"
657
+ except = bleeding_edge["except"] || []
658
+ except.empty? || { "all" => true, "except" => except }
659
+ when "list" then bleeding_edge["ids"] || []
660
+ else false
661
+ end
662
+ end
569
663
  end
570
664
  end
@@ -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}]
@@ -88,6 +88,15 @@ module Rigor
88
88
  vendored_gem_sig_paths.each do |path|
89
89
  rbs_loader.add(path: path) if path.directory?
90
90
  end
91
+ # Rigor-owned core overlay — loaded LAST so an upstream
92
+ # declaration always wins on conflict; these reopenings only
93
+ # fill genuine holes (e.g. `Numeric#to_f`/`to_i`/`to_r`, which
94
+ # upstream RBS declares on the concrete subclasses but not on
95
+ # the abstract `Numeric` that Rigor's arithmetic-chain widening
96
+ # produces).
97
+ core_overlay_sig_paths.each do |path|
98
+ rbs_loader.add(path: path) if path.directory?
99
+ end
91
100
  env = RBS::Environment.from_loader(rbs_loader)
92
101
  add_virtual_rbs(env, virtual_rbs)
93
102
  synthesize_missing_namespaces(env)
@@ -298,6 +307,22 @@ module Rigor
298
307
  ).freeze
299
308
  private_constant :VENDORED_GEM_SIGS_ROOT
300
309
 
310
+ # Rigor-owned core-overlay RBS (`data/core_overlay/`). Reopens
311
+ # Ruby core classes to add methods upstream `ruby/rbs` omits but
312
+ # which every concrete value answers at runtime — loaded last so
313
+ # upstream always wins on conflict. Public so the cache descriptor
314
+ # can digest these files into the env-blob key.
315
+ CORE_OVERLAY_SIGS_ROOT = File.expand_path(
316
+ "../../../data/core_overlay",
317
+ __dir__
318
+ ).freeze
319
+
320
+ def core_overlay_sig_paths
321
+ return [] unless File.directory?(CORE_OVERLAY_SIGS_ROOT)
322
+
323
+ [Pathname(CORE_OVERLAY_SIGS_ROOT)]
324
+ end
325
+
301
326
  def vendored_gem_sig_paths
302
327
  return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
303
328
 
@@ -338,11 +363,11 @@ module Rigor
338
363
  # out at build time so the loader stays robust to fixtures and
339
364
  # bare repositories.
340
365
  # @param cache_store [Rigor::Cache::Store, nil] the persistent
341
- # cache the loader consults for translated constant lookups
342
- # (and, in later v0.0.9 slices, other Marshal-clean
343
- # reflection artefacts). Pass `nil` (the default) to skip
344
- # the cache entirely; the runner threads its own Store
345
- # through here when caching is enabled.
366
+ # cache the loader threads through to `RbsEnvironment`,
367
+ # `RbsKnownClassNames`, `RbsConstantTable`,
368
+ # `RbsClassAncestorTable`, and `RbsClassTypeParamNames`
369
+ # producers. Pass `nil` (the default) to skip caching; the
370
+ # runner threads its own Store through here when enabled.
346
371
  # @param virtual_rbs [Array<[String, String]>] ADR-32 WD4 —
347
372
  # `[virtual_filename, rbs_source]` pairs synthesised from
348
373
  # project source by a plugin's
@@ -618,6 +643,29 @@ module Rigor
618
643
  interface_definition(interface_name)&.methods&.keys
619
644
  end
620
645
 
646
+ # @param rbs_alias [RBS::Types::Alias] a type-alias reference (`string`,
647
+ # `int`, `range[int?]`, …) appearing in a method signature.
648
+ # @return [RBS::Types::t, nil] the alias's aliased type one level out,
649
+ # with type arguments substituted for a generic alias (`string` →
650
+ # `::String | ::_ToStr`; `range[int?]` → `::Range[int?] |
651
+ # ::_Range[int?]`), or nil for an unresolved name. Lets a caller see
652
+ # through the alias that {Inference::RbsTypeTranslator} otherwise
653
+ # degrades to `untyped`, which is why an interface/alias parameter
654
+ # does not reject `nil`. `expand_alias2` handles the (rarer) generic
655
+ # case — a `range[T]` param previously fell back to "admits", which
656
+ # suppressed e.g. `MatchData#[](nil)`.
657
+ def expand_type_alias(rbs_alias)
658
+ return nil if env.nil?
659
+
660
+ name = rbs_alias.name
661
+ name = name.absolute! unless name.absolute?
662
+ return nil unless env.type_alias_decls.key?(name)
663
+
664
+ builder.expand_alias2(name, rbs_alias.args)
665
+ rescue ::RBS::BaseError, StandardError
666
+ nil
667
+ end
668
+
621
669
  # @return [RBS::Definition, nil] the resolved singleton (class
622
670
  # object) definition for `class_name`. The methods on this
623
671
  # definition are the *class methods* of `class_name`, including
@@ -1009,6 +1057,8 @@ module Rigor
1009
1057
  rbs_name = parse_type_name(class_name)
1010
1058
  return nil unless rbs_name
1011
1059
  return nil if env.nil?
1060
+
1061
+ rbs_name = canonical_module_name(rbs_name)
1012
1062
  return nil unless env.class_decls.key?(rbs_name)
1013
1063
 
1014
1064
  builder.build_instance(rbs_name)
@@ -1020,6 +1070,8 @@ module Rigor
1020
1070
  rbs_name = parse_type_name(class_name)
1021
1071
  return nil unless rbs_name
1022
1072
  return nil if env.nil?
1073
+
1074
+ rbs_name = canonical_module_name(rbs_name)
1023
1075
  return nil unless env.class_decls.key?(rbs_name)
1024
1076
 
1025
1077
  builder.build_singleton(rbs_name)
@@ -1027,6 +1079,23 @@ module Rigor
1027
1079
  nil
1028
1080
  end
1029
1081
 
1082
+ # Resolve an RBS class/module ALIAS to its canonical declared name.
1083
+ # `class Mutex = Thread::Mutex` lives only in `class_alias_decls`, so
1084
+ # `class_known?` reports it (it checks that table) but the definition
1085
+ # builder — which only knows `class_decls` — could not enumerate its
1086
+ # methods, leaving alias classes (`Mutex`, and any `X = Y`) with no
1087
+ # resolvable method surface. Normalising via the env (RBS's own alias
1088
+ # resolution) before the `class_decls` guard fixes dispatch AND the
1089
+ # `call.undefined-method` existence check on them. A non-alias name, or
1090
+ # one that does not normalise, is returned unchanged.
1091
+ def canonical_module_name(rbs_name)
1092
+ return rbs_name unless env.class_alias_decls.key?(rbs_name)
1093
+
1094
+ env.normalize_module_name?(rbs_name) || rbs_name
1095
+ rescue ::RBS::BaseError
1096
+ rbs_name
1097
+ end
1098
+
1030
1099
  # Memoised on `@state` (the per-loader store also holding `:env` /
1031
1100
  # `:builder`): `RBS::TypeName.parse` is a pure, deterministic
1032
1101
  # 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.
@@ -123,6 +125,7 @@ module Rigor
123
125
  # build at all.
124
126
  @hkt_registry_base = hkt_registry || Inference::HktRegistry::EMPTY
125
127
  @hkt_registry_holder = HktRegistryHolder.new
128
+ @constant_type_cache = ConstantTypeCacheHolder.new
126
129
  @name_scope = build_name_scope
127
130
  freeze
128
131
  end
@@ -453,11 +456,11 @@ module Rigor
453
456
  def invoke_synthesizer_safely(callable, path)
454
457
  callable.call(path.to_s)
455
458
  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".
459
+ # WD6 fail-soft — a synthesizer that raises does NOT crash
460
+ # analysis. Unlike the `[:error, msg]` return path (which
461
+ # the runner surfaces as `source-rbs-synthesis-failed`),
462
+ # an unhandled raise is swallowed silently; the
463
+ # unexamined-raise channel is deliberately silent per WD6.
461
464
  nil
462
465
  end
463
466
  end
@@ -504,7 +507,14 @@ module Rigor
504
507
  def constant_for_name(name)
505
508
  return nil if rbs_loader.nil?
506
509
 
507
- rbs_loader.constant_type(name.to_s)
510
+ # Pure function of `name` for this Environment's lifetime — the
511
+ # refinement table and the RBS constant table are both fixed — so
512
+ # memoise across the lexical-candidate ladder's heavy name reuse.
513
+ key = name.to_s
514
+ @constant_type_cache.fetch(key) do
515
+ Builtins::PredefinedConstantRefinements.lookup(key) ||
516
+ rbs_loader.constant_type(key)
517
+ end
508
518
  end
509
519
 
510
520
  # 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`