rigortype 0.1.16 → 0.1.18

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
  4. data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
  5. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
  6. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
  7. data/lib/rigor/analysis/check_rules.rb +180 -73
  8. data/lib/rigor/analysis/dependency_recorder.rb +122 -0
  9. data/lib/rigor/analysis/diagnostic.rb +18 -0
  10. data/lib/rigor/analysis/incremental.rb +162 -0
  11. data/lib/rigor/analysis/incremental_session.rb +337 -0
  12. data/lib/rigor/analysis/rule_catalog.rb +48 -0
  13. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
  14. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  15. data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
  16. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  17. data/lib/rigor/analysis/runner.rb +477 -1110
  18. data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
  19. data/lib/rigor/analysis/worker_session.rb +47 -8
  20. data/lib/rigor/builtins/static_return_refinements.rb +7 -1
  21. data/lib/rigor/cache/descriptor.rb +50 -49
  22. data/lib/rigor/cache/incremental_snapshot.rb +153 -0
  23. data/lib/rigor/cache/rbs_cache_producer.rb +34 -0
  24. data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
  25. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
  26. data/lib/rigor/cache/rbs_constant_table.rb +2 -8
  27. data/lib/rigor/cache/rbs_environment.rb +2 -8
  28. data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
  29. data/lib/rigor/cache/store.rb +145 -14
  30. data/lib/rigor/cli/annotate_command.rb +2 -7
  31. data/lib/rigor/cli/baseline_command.rb +2 -7
  32. data/lib/rigor/cli/check_command.rb +705 -0
  33. data/lib/rigor/cli/ci_detector.rb +94 -0
  34. data/lib/rigor/cli/command.rb +47 -0
  35. data/lib/rigor/cli/coverage_command.rb +3 -23
  36. data/lib/rigor/cli/coverage_renderer.rb +3 -8
  37. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  38. data/lib/rigor/cli/diff_command.rb +3 -7
  39. data/lib/rigor/cli/explain_command.rb +2 -7
  40. data/lib/rigor/cli/lsp_command.rb +3 -7
  41. data/lib/rigor/cli/mcp_command.rb +3 -7
  42. data/lib/rigor/cli/options.rb +57 -0
  43. data/lib/rigor/cli/plugin_command.rb +3 -7
  44. data/lib/rigor/cli/plugins_command.rb +2 -7
  45. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  46. data/lib/rigor/cli/renderable.rb +26 -0
  47. data/lib/rigor/cli/sig_gen_command.rb +2 -7
  48. data/lib/rigor/cli/skill_command.rb +3 -7
  49. data/lib/rigor/cli/trace_command.rb +143 -0
  50. data/lib/rigor/cli/trace_renderer.rb +310 -0
  51. data/lib/rigor/cli/triage_command.rb +2 -7
  52. data/lib/rigor/cli/type_of_command.rb +5 -38
  53. data/lib/rigor/cli/type_of_renderer.rb +4 -9
  54. data/lib/rigor/cli/type_scan_command.rb +3 -23
  55. data/lib/rigor/cli/type_scan_renderer.rb +4 -9
  56. data/lib/rigor/cli.rb +15 -532
  57. data/lib/rigor/configuration/dependencies.rb +18 -1
  58. data/lib/rigor/configuration/severity_profile.rb +22 -3
  59. data/lib/rigor/configuration.rb +16 -3
  60. data/lib/rigor/environment/rbs_loader.rb +129 -71
  61. data/lib/rigor/environment.rb +1 -1
  62. data/lib/rigor/inference/acceptance.rb +10 -0
  63. data/lib/rigor/inference/block_parameter_binder.rb +1 -2
  64. data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
  65. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
  66. data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
  67. data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
  68. data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
  69. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
  70. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
  71. data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
  72. data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
  73. data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
  74. data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
  75. data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
  76. data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
  77. data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
  78. data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
  79. data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
  80. data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
  81. data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
  82. data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
  83. data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
  84. data/lib/rigor/inference/expression_typer.rb +149 -63
  85. data/lib/rigor/inference/flow_tracer.rb +180 -0
  86. data/lib/rigor/inference/macro_block_self_type.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
  88. data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
  89. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
  90. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
  91. data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
  92. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
  93. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  94. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
  95. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
  96. data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
  97. data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
  98. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  99. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
  100. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
  101. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
  102. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
  103. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
  104. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
  105. data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
  106. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
  107. data/lib/rigor/inference/method_dispatcher.rb +185 -84
  108. data/lib/rigor/inference/narrowing.rb +262 -5
  109. data/lib/rigor/inference/scope_indexer.rb +208 -21
  110. data/lib/rigor/inference/statement_evaluator.rb +110 -48
  111. data/lib/rigor/language_server/buffer_resolution.rb +33 -0
  112. data/lib/rigor/language_server/completion_provider.rb +4 -4
  113. data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
  114. data/lib/rigor/language_server/folding_range_provider.rb +4 -4
  115. data/lib/rigor/language_server/hover_provider.rb +4 -4
  116. data/lib/rigor/language_server/selection_range_provider.rb +4 -4
  117. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  118. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  119. data/lib/rigor/plugin/base.rb +302 -45
  120. data/lib/rigor/plugin/node_rule_walk.rb +147 -0
  121. data/lib/rigor/plugin/registry.rb +281 -15
  122. data/lib/rigor/plugin.rb +1 -0
  123. data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
  124. data/lib/rigor/rbs_extended.rb +39 -0
  125. data/lib/rigor/scope/discovery_index.rb +58 -0
  126. data/lib/rigor/scope.rb +150 -167
  127. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  128. data/lib/rigor/source/literals.rb +14 -0
  129. data/lib/rigor/type/acceptance_router.rb +19 -0
  130. data/lib/rigor/type/accepts_result.rb +3 -10
  131. data/lib/rigor/type/app.rb +3 -7
  132. data/lib/rigor/type/bot.rb +2 -3
  133. data/lib/rigor/type/bound_method.rb +5 -12
  134. data/lib/rigor/type/combinator.rb +22 -0
  135. data/lib/rigor/type/constant.rb +2 -3
  136. data/lib/rigor/type/data_class.rb +80 -0
  137. data/lib/rigor/type/data_instance.rb +100 -0
  138. data/lib/rigor/type/difference.rb +5 -10
  139. data/lib/rigor/type/dynamic.rb +5 -10
  140. data/lib/rigor/type/hash_shape.rb +5 -15
  141. data/lib/rigor/type/integer_range.rb +5 -10
  142. data/lib/rigor/type/intersection.rb +5 -10
  143. data/lib/rigor/type/nominal.rb +5 -10
  144. data/lib/rigor/type/refined.rb +5 -10
  145. data/lib/rigor/type/singleton.rb +5 -10
  146. data/lib/rigor/type/top.rb +2 -3
  147. data/lib/rigor/type/tuple.rb +5 -10
  148. data/lib/rigor/type/union.rb +5 -10
  149. data/lib/rigor/type.rb +2 -0
  150. data/lib/rigor/value_semantics.rb +77 -0
  151. data/lib/rigor/version.rb +1 -1
  152. data/lib/rigor.rb +1 -1
  153. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
  156. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  157. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
  158. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  159. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  160. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  161. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  163. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  164. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  166. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  167. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  168. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
  169. data/sig/rigor/cache.rbs +19 -0
  170. data/sig/rigor/environment.rbs +0 -2
  171. data/sig/rigor/inference.rbs +27 -0
  172. data/sig/rigor/plugin/base.rbs +1 -2
  173. data/sig/rigor/rbs_extended.rbs +2 -0
  174. data/sig/rigor/scope.rbs +42 -25
  175. data/sig/rigor/source.rbs +1 -0
  176. data/sig/rigor/type.rbs +58 -1
  177. data/sig/rigor.rbs +6 -1
  178. data/skills/rigor-ci-setup/SKILL.md +319 -0
  179. metadata +36 -2
  180. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
@@ -476,31 +476,19 @@ module Rigor
476
476
  # carriers like `Nominal[Integer]` (Integer is always truthy
477
477
  # in Ruby — including 0) also collapse the dead else.
478
478
  def live_branch_for_if(node, pred_type, post_pred)
479
- case predicate_certainty(pred_type)
480
- when :always_truthy then eval_branch_or_nil(node.statements, post_pred)
481
- when :always_falsey then eval_branch_or_nil(node.subsequent, post_pred)
479
+ case Narrowing.predicate_certainty(pred_type)
480
+ when :truthy then eval_branch_or_nil(node.statements, post_pred)
481
+ when :falsey then eval_branch_or_nil(node.subsequent, post_pred)
482
482
  end
483
483
  end
484
484
 
485
485
  def live_branch_for_unless(node, pred_type, post_pred)
486
- case predicate_certainty(pred_type)
487
- when :always_truthy then eval_branch_or_nil(node.else_clause, post_pred)
488
- when :always_falsey then eval_branch_or_nil(node.statements, post_pred)
486
+ case Narrowing.predicate_certainty(pred_type)
487
+ when :truthy then eval_branch_or_nil(node.else_clause, post_pred)
488
+ when :falsey then eval_branch_or_nil(node.statements, post_pred)
489
489
  end
490
490
  end
491
491
 
492
- def predicate_certainty(pred_type)
493
- return nil if pred_type.nil? || pred_type.is_a?(Type::Bot)
494
-
495
- truthy_bot = Narrowing.narrow_truthy(pred_type).is_a?(Type::Bot)
496
- falsey_bot = Narrowing.narrow_falsey(pred_type).is_a?(Type::Bot)
497
-
498
- return :always_falsey if truthy_bot && !falsey_bot
499
- return :always_truthy if !truthy_bot && falsey_bot
500
-
501
- nil
502
- end
503
-
504
492
  def eval_else(node)
505
493
  return [Type::Combinator.constant_of(nil), scope] if node.statements.nil?
506
494
 
@@ -528,26 +516,76 @@ module Rigor
528
516
  results = []
529
517
  falsey_scope = entry_scope
530
518
  conditions.each do |branch|
519
+ # ADR-47 WD2 — record the scope ENTERING this clause (the
520
+ # subject narrowed by every earlier clause's negation) on the
521
+ # clause's first condition node, so `flow.unreachable-clause`
522
+ # can tell a prior-exhausted subject (entry already `bot`)
523
+ # from a per-clause-disjoint one (entry concrete, this clause
524
+ # disjoint). `on_enter`-only (no recursion) so no condition
525
+ # sub-expression is newly typed; `propagate` preserves the
526
+ # entry because it already keys the node.
527
+ record_clause_entry_scope(branch, falsey_scope)
531
528
  body_scope, falsey_scope = branch_body_and_falsey_scopes(subject, branch, falsey_scope)
532
529
  results << sub_eval(branch, body_scope)
533
530
  end
534
531
  [results, falsey_scope]
535
532
  end
536
533
 
534
+ # ADR-47 WD2/WD3 — record the scope ENTERING a `when`/`in` clause on
535
+ # the node `flow.unreachable-clause` reads to classify a dead clause
536
+ # (`when`: first condition; `in`: the pattern). `on_enter`-only so no
537
+ # sub-expression is newly typed; `propagate` preserves it.
538
+ def record_clause_entry_scope(branch, entry_scope)
539
+ node =
540
+ case branch
541
+ when Prism::WhenNode then branch.conditions.first
542
+ when Prism::InNode then branch.pattern
543
+ end
544
+ @on_enter&.call(node, entry_scope) if node
545
+ end
546
+
537
547
  # Returns `[body_scope, updated_falsey_scope]` for a single branch.
538
- # `InNode` branches apply pattern bindings; `WhenNode` branches
539
- # narrow through `Narrowing.case_when_scopes`. The falsey scope is
540
- # unchanged for `in` branches (conservative: no exhaustiveness
541
- # tracking yet).
548
+ # `WhenNode` branches narrow through `Narrowing.case_when_scopes`.
549
+ # `InNode` branches narrow soundly only for a bare class pattern
550
+ # (`in C` / `in C => x`, pure `is_a?`); every other pattern keeps
551
+ # the conservative "body = entry + bindings, falsey unchanged" shape.
542
552
  def branch_body_and_falsey_scopes(subject, branch, falsey_scope)
543
553
  if branch.is_a?(Prism::InNode)
544
- [apply_in_pattern_bindings(subject, branch.pattern, falsey_scope), falsey_scope]
554
+ in_branch_body_and_falsey_scopes(subject, branch, falsey_scope)
545
555
  else
546
556
  when_conditions = branch.respond_to?(:conditions) ? branch.conditions : []
547
557
  Narrowing.case_when_scopes(subject, when_conditions, falsey_scope)
548
558
  end
549
559
  end
550
560
 
561
+ # ADR-47 WD3a — a bare class pattern matches on `C === subject`, i.e.
562
+ # exactly `subject.is_a?(C)` with no deconstruction, so it narrows
563
+ # like `when C`: the body sees the subject narrowed to `C` and the
564
+ # next clause's falsey scope has `C` removed. Other patterns can fail
565
+ # to match even when a class test would pass (deconstruction arity,
566
+ # hash keys, ...), so removing anything from the falsey scope would
567
+ # be unsound — they keep the conservative shape.
568
+ def in_branch_body_and_falsey_scopes(subject, branch, falsey_scope)
569
+ class_node = bare_class_pattern_node(branch.pattern)
570
+ return [apply_in_pattern_bindings(subject, branch.pattern, falsey_scope), falsey_scope] unless class_node
571
+
572
+ truthy_scope, narrowed_falsey = Narrowing.case_when_scopes(subject, [class_node], falsey_scope)
573
+ [apply_in_pattern_bindings(subject, branch.pattern, truthy_scope), narrowed_falsey]
574
+ end
575
+
576
+ # The class-constant node of a `in C` / `in C => x` pattern (the only
577
+ # `in` shapes whose match is pure `is_a?`), or nil for any pattern
578
+ # that deconstructs, binds, or matches a value.
579
+ def bare_class_pattern_node(pattern)
580
+ case pattern
581
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
582
+ pattern
583
+ when Prism::CapturePatternNode
584
+ value = pattern.value
585
+ value if value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
586
+ end
587
+ end
588
+
551
589
  def eval_case_else(else_clause, falsey_scope)
552
590
  return sub_eval(else_clause, falsey_scope) if else_clause
553
591
 
@@ -1371,22 +1409,47 @@ module Rigor
1371
1409
  end
1372
1410
  end
1373
1411
 
1374
- # ADR-37 slice 2 — gathers each plugin's post-return narrowing from
1375
- # BOTH the narrow `type_specifier` DSL (method-gated, wrapped as a
1376
- # facts-only `FlowContribution`) and the legacy
1377
- # `flow_contribution_for` escape valve, swallowing per-plugin
1412
+ # ADR-37 slice 2 / ADR-52 WD3 — gathers each plugin's post-return
1413
+ # narrowing from the method-gated `type_specifier` DSL, wrapped as
1414
+ # a facts-only `FlowContribution`, swallowing per-plugin
1378
1415
  # exceptions so a buggy plugin can't abort the assertion path.
1416
+ EMPTY_CONTRIBUTIONS = [].freeze
1417
+ private_constant :EMPTY_CONTRIBUTIONS
1418
+
1419
+ # Per-dispatch collection of plugin narrowing contributions. Mirrors
1420
+ # `MethodDispatcher#collect_plugin_contributions`: visit only the
1421
+ # registry-ordered subset of plugins that implement a per-call path
1422
+ # (`for_statement` = declares a `type_specifier`), gate each path
1423
+ # by membership AND by the ADR-52 WD1 method-name gates (every
1424
+ # `type_specifier` rule is `methods:`-gated, so the common
1425
+ # no-candidate case is a single Set probe; a pruned
1426
+ # consultation could only have returned `[]`), and accumulate
1427
+ # lazily (shared frozen empty array otherwise). Same contributions in
1428
+ # the same order as visiting every plugin; the caller is read-only.
1379
1429
  def collect_plugin_contributions(registry, call_node, current_scope)
1380
- registry.plugins.flat_map do |plugin|
1381
- contributions = []
1382
- legacy = plugin.flow_contribution_for(call_node: call_node, scope: current_scope)
1383
- contributions << legacy if legacy.is_a?(Rigor::FlowContribution)
1430
+ index = registry.contribution_index
1431
+ relevant = index.for_statement
1432
+ return EMPTY_CONTRIBUTIONS if relevant.empty?
1433
+
1434
+ name = call_node.respond_to?(:name) ? call_node.name : nil
1435
+ return EMPTY_CONTRIBUTIONS unless index.statement_candidate?(name)
1436
+
1437
+ collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
1438
+ end
1439
+
1440
+ # The post-gate walk, in registry order — the same order the
1441
+ # ungated walk used.
1442
+ def collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
1443
+ result = nil
1444
+ relevant.each do |plugin|
1445
+ next unless index.type_specifier_candidate_for?(plugin, name)
1446
+
1384
1447
  facts = plugin.type_specifier_facts(call_node: call_node, scope: current_scope)
1385
- contributions << Rigor::FlowContribution.new(post_return_facts: facts) if facts && !facts.empty?
1386
- contributions
1448
+ (result ||= []) << Rigor::FlowContribution.new(post_return_facts: facts) if facts && !facts.empty?
1387
1449
  rescue StandardError
1388
- []
1450
+ next
1389
1451
  end
1452
+ result || EMPTY_CONTRIBUTIONS
1390
1453
  end
1391
1454
 
1392
1455
  def resolve_call_method(call_node, current_scope)
@@ -1766,20 +1829,19 @@ module Rigor
1766
1829
  # ScopeIndexer-populated declaration overrides
1767
1830
  # (`Prism::ConstantReadNode` for `module Foo` headers, etc.)
1768
1831
  # remain reachable from inside nested bodies.
1769
- def build_fresh_body_scope # rubocop:disable Metrics/AbcSize
1770
- Scope.empty(environment: scope.environment)
1771
- .with_source_path(scope.source_path)
1772
- .with_declared_types(scope.declared_types)
1773
- .with_discovered_classes(scope.discovered_classes)
1774
- .with_in_source_constants(scope.in_source_constants)
1775
- .with_class_ivars(scope.class_ivars)
1776
- .with_class_cvars(scope.class_cvars)
1777
- .with_program_globals(scope.program_globals)
1778
- .with_discovered_methods(scope.discovered_methods)
1779
- .with_discovered_def_nodes(scope.discovered_def_nodes)
1780
- .with_discovered_superclasses(scope.discovered_superclasses)
1781
- .with_discovered_includes(scope.discovered_includes)
1782
- .with_discovered_method_visibilities(scope.discovered_method_visibilities)
1832
+ def build_fresh_body_scope
1833
+ # Single allocation instead of a deep `with_*` chain — this runs
1834
+ # per class/method body on the main walk, so the chain's throwaway
1835
+ # intermediate Scopes were a top `Scope#rebuild` source (ADR-44).
1836
+ # Local-empty by design; the discovery index is inherited whole by
1837
+ # reference (ADR-53 Track A), so a table added to the index can no
1838
+ # longer be dropped here by a missed per-field copy.
1839
+ Scope.new(
1840
+ environment: scope.environment,
1841
+ locals: {}.freeze,
1842
+ source_path: scope.source_path,
1843
+ discovery: scope.discovery
1844
+ )
1783
1845
  end
1784
1846
 
1785
1847
  def singleton_def?(def_node)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "uri"
4
+
5
+ module Rigor
6
+ module LanguageServer
7
+ # Shared buffer lookup for the LSP providers.
8
+ #
9
+ # Every provider's `provide` opens with the same two steps: translate
10
+ # the document URI to an on-disk path, then fetch the open buffer
11
+ # entry from the buffer table — returning nil to the editor when
12
+ # either is missing. The parse step that follows differs per provider
13
+ # (strict vs error-tolerant, whole-buffer vs cursor-recovery), so
14
+ # only this resolution is shared here.
15
+ module BufferResolution
16
+ private
17
+
18
+ # Resolves `[path, entry]` for the document `uri`, or nil when the
19
+ # uri has no file path or no open buffer. A caller that destructures
20
+ # `path, entry = buffer_for(uri)` can guard on `entry.nil?` to cover
21
+ # both misses (a nil return leaves both locals nil).
22
+ def buffer_for(uri)
23
+ path = Uri.to_path(uri)
24
+ return nil if path.nil?
25
+
26
+ entry = @buffer_table[uri]
27
+ return nil if entry.nil?
28
+
29
+ [path, entry]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "uri"
6
+ require_relative "buffer_resolution"
6
7
  require_relative "../environment"
7
8
  require_relative "../reflection"
8
9
  require_relative "../scope"
@@ -35,6 +36,8 @@ module Rigor
35
36
  #
36
37
  # Slice 6 will add 7 (Class), 9 (Module), 21 (Constant).
37
38
  class CompletionProvider # rubocop:disable Metrics/ClassLength
39
+ include BufferResolution
40
+
38
41
  KIND_METHOD = 2
39
42
  KIND_FIELD = 5
40
43
  KIND_CLASS = 7
@@ -54,10 +57,7 @@ module Rigor
54
57
  # means "we tried and got nothing".
55
58
  def provide(uri:, line:, character:, trigger_character: nil)
56
59
  _ = trigger_character # Trigger info logged-not-routed in v1.
57
- path = Uri.to_path(uri)
58
- return nil if path.nil?
59
-
60
- entry = @buffer_table[uri]
60
+ path, entry = buffer_for(uri)
61
61
  return nil if entry.nil?
62
62
 
63
63
  # Slice B4 — parse recovery. The common mid-edit buffer
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "uri"
6
+ require_relative "buffer_resolution"
6
7
 
7
8
  module Rigor
8
9
  module LanguageServer
@@ -18,6 +19,8 @@ module Rigor
18
19
  # - Method (6) — `def m` inside a class / module
19
20
  # - Function (12) — `def m` at top-level (no enclosing class)
20
21
  class DocumentSymbolProvider
22
+ include BufferResolution
23
+
21
24
  KIND_MODULE = 2
22
25
  KIND_CLASS = 5
23
26
  KIND_METHOD = 6
@@ -33,10 +36,7 @@ module Rigor
33
36
  # doesn't parse cleanly enough to surface symbols — LSP
34
37
  # clients fall back to no-outline in that case.
35
38
  def provide(uri)
36
- path = Uri.to_path(uri)
37
- return nil if path.nil?
38
-
39
- entry = @buffer_table[uri]
39
+ path, entry = buffer_for(uri)
40
40
  return nil if entry.nil?
41
41
 
42
42
  parse_result = Prism.parse(entry.bytes, filepath: path,
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "uri"
6
+ require_relative "buffer_resolution"
6
7
 
7
8
  module Rigor
8
9
  module LanguageServer
@@ -18,6 +19,8 @@ module Rigor
18
19
  # collapsed view shows the opener intact and hides the body
19
20
  # only.
20
21
  class FoldingRangeProvider
22
+ include BufferResolution
23
+
21
24
  def initialize(buffer_table:, project_context:)
22
25
  @buffer_table = buffer_table
23
26
  @project_context = project_context
@@ -26,10 +29,7 @@ module Rigor
26
29
  # @return [Array<Hash>, nil] LSP `FoldingRange[]` for the
27
30
  # buffer, or nil when the URI isn't open / parseable.
28
31
  def provide(uri)
29
- path = Uri.to_path(uri)
30
- return nil if path.nil?
31
-
32
- entry = @buffer_table[uri]
32
+ path, entry = buffer_for(uri)
33
33
  return nil if entry.nil?
34
34
 
35
35
  parse_result = Prism.parse(entry.bytes, filepath: path,
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "uri"
6
+ require_relative "buffer_resolution"
6
7
  require_relative "hover_renderer"
7
8
  require_relative "../environment"
8
9
  require_relative "../scope"
@@ -22,6 +23,8 @@ module Rigor
22
23
  # for ASCII source (UTF-16 conversion is queued, see design
23
24
  # doc § "Open questions").
24
25
  class HoverProvider
26
+ include BufferResolution
27
+
25
28
  def initialize(buffer_table:, project_context:, renderer: HoverRenderer.new)
26
29
  @buffer_table = buffer_table
27
30
  @project_context = project_context
@@ -33,10 +36,7 @@ module Rigor
33
36
  # maps to `result: null` per the LSP spec — clients
34
37
  # suppress the hover popup in that case.
35
38
  def provide(uri:, line:, character:)
36
- path = Uri.to_path(uri)
37
- return nil if path.nil?
38
-
39
- entry = @buffer_table[uri]
39
+ path, entry = buffer_for(uri)
40
40
  return nil if entry.nil?
41
41
 
42
42
  parse_result = Prism.parse(entry.bytes, filepath: path, version: @project_context.configuration.target_ruby)
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "uri"
6
+ require_relative "buffer_resolution"
6
7
 
7
8
  module Rigor
8
9
  module LanguageServer
@@ -13,6 +14,8 @@ module Rigor
13
14
  # one keystroke moves up the chain, another moves further out,
14
15
  # all the way to the root.
15
16
  class SelectionRangeProvider
17
+ include BufferResolution
18
+
16
19
  def initialize(buffer_table:, project_context:)
17
20
  @buffer_table = buffer_table
18
21
  @project_context = project_context
@@ -23,10 +26,7 @@ module Rigor
23
26
  # @return [Array<Hash>, nil] one `SelectionRange` per
24
27
  # position, or nil when the URI / buffer isn't resolvable.
25
28
  def provide(uri, positions)
26
- path = Uri.to_path(uri)
27
- return nil if path.nil?
28
-
29
- entry = @buffer_table[uri]
29
+ path, entry = buffer_for(uri)
30
30
  return nil if entry.nil?
31
31
 
32
32
  parse_result = Prism.parse(entry.bytes, filepath: path,
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "uri"
6
+ require_relative "buffer_resolution"
6
7
  require_relative "../environment"
7
8
  require_relative "../reflection"
8
9
  require_relative "../scope"
@@ -35,6 +36,8 @@ module Rigor
35
36
  # active-parameter override per overload land in follow-up
36
37
  # slices (queued in the design doc § "Out of scope for v2").
37
38
  class SignatureHelpProvider
39
+ include BufferResolution
40
+
38
41
  ARG_SENTINEL = "__rigor_lsp_arg_sentinel__"
39
42
  private_constant :ARG_SENTINEL
40
43
 
@@ -47,10 +50,7 @@ module Rigor
47
50
  # when the cursor isn't inside a resolvable method call.
48
51
  def provide(uri:, line:, character:, context: nil)
49
52
  _ = context # Trigger info accepted but not routed in v1.
50
- path = Uri.to_path(uri)
51
- return nil if path.nil?
52
-
53
- entry = @buffer_table[uri]
53
+ path, entry = buffer_for(uri)
54
54
  return nil if entry.nil?
55
55
 
56
56
  bytes, locate_at = parse_attempt_bytes(entry.bytes, line, character)
@@ -3,32 +3,40 @@
3
3
  module Rigor
4
4
  module Plugin
5
5
  # ADR-38 declaration: "on `receiver_constraint` (and its
6
- # subclasses), every method named in `methods` also establishes
7
- # instance-variable state treat it like `initialize` for the
8
- # read-before-write nil soundness gate."
6
+ # subclasses), every method named in `methods` (def-form) or
7
+ # `block_methods` (block-form) also establishes instance-variable
8
+ # state — treat it like `initialize` for the read-before-write nil
9
+ # soundness gate."
10
+ #
11
+ # **Def-form** (`methods:`) — applies when the ivar write lives in a
12
+ # named `def` body. Example: Minitest `def setup; @conn = …; end`.
13
+ #
14
+ # **Block-form** (`block_methods:`) — applies when the ivar write
15
+ # lives in a block passed to a method call. Example: RSpec
16
+ # `before { @user = create(:user) }` / `let(:x) { @y = … }`.
17
+ # `ScopeIndexer` descends the block body of any `CallNode` whose
18
+ # method name is in `block_methods`, collecting ivar writes exactly
19
+ # as it would for a def-form initializer.
20
+ #
21
+ # At least one of `methods:` or `block_methods:` must be non-empty.
9
22
  #
10
23
  # Authored on a plugin manifest:
11
24
  #
12
- # manifest(
13
- # id: "minitest",
14
- # version: "0.1.0",
15
- # additional_initializers: [
16
- # Rigor::Plugin::AdditionalInitializer.new(
17
- # receiver_constraint: "Minitest::Test",
18
- # methods: [:setup]
19
- # )
20
- # ]
25
+ # # def-form (Minitest):
26
+ # AdditionalInitializer.new(
27
+ # receiver_constraint: "Minitest::Test",
28
+ # methods: [:setup]
29
+ # )
30
+ #
31
+ # # block-form (RSpec):
32
+ # AdditionalInitializer.new(
33
+ # receiver_constraint: "RSpec::ExampleGroup",
34
+ # block_methods: [:before, :let, :subject]
21
35
  # )
22
36
  #
23
37
  # The Ruby analogue of PHPStan's `AdditionalConstructorsExtension`.
24
38
  # `Rigor::Inference::ScopeIndexer` consults the aggregated set at
25
- # its single read-before-write gate: for a `def` whose name is in
26
- # `methods` on a class that equals or inherits from
27
- # `receiver_constraint` (matched via `Environment#class_ordering`,
28
- # the same mechanism ADR-16 Tier A uses), the method's ivar writes
29
- # are folded into the class's `init_writes` set, so a sibling
30
- # method reading those ivars no longer gets a `Constant[nil]`
31
- # widening.
39
+ # its read-before-write gate.
32
40
  #
33
41
  # The contribution can only ever *suppress* a nil widening — it
34
42
  # never makes the analyzer stricter — so a missed or over-broad
@@ -39,38 +47,47 @@ module Rigor
39
47
  #
40
48
  # - `receiver_constraint` — fully-qualified class name (String).
41
49
  # The entry applies to that class and its subclasses.
42
- # - `methods` — Array of Symbol method names treated as
43
- # initializers on a matching class.
50
+ # - `methods` — Array of Symbol `def`-form method names (may be
51
+ # empty when only block_methods is used).
52
+ # - `block_methods` — Array of Symbol call-with-block method names
53
+ # (may be empty when only methods is used).
44
54
  #
45
55
  # ## Ractor-shareability
46
56
  #
47
- # Both fields are frozen at construction (ADR-15 Phase 1);
48
- # `Ractor.shareable?` returns true after `#initialize`, so the
49
- # value object survives `Plugin::Registry.materialize` into a
50
- # worker Ractor.
57
+ # All fields are frozen at construction (ADR-15 Phase 1);
58
+ # `Ractor.shareable?` returns true after `#initialize`.
51
59
  class AdditionalInitializer
52
- attr_reader :receiver_constraint, :methods
60
+ attr_reader :receiver_constraint, :methods, :block_methods
53
61
 
54
- def initialize(receiver_constraint:, methods:)
62
+ def initialize(receiver_constraint:, methods: [], block_methods: [])
55
63
  validate_receiver_constraint!(receiver_constraint)
56
- validate_methods!(methods)
64
+ validate_method_list!(methods, :methods)
65
+ validate_method_list!(block_methods, :block_methods)
66
+ validate_at_least_one!(methods, block_methods)
57
67
 
58
68
  @receiver_constraint = receiver_constraint.dup.freeze
59
69
  @methods = methods.map(&:to_sym).freeze
70
+ @block_methods = block_methods.map(&:to_sym).freeze
60
71
  freeze
61
72
  end
62
73
 
63
- # True when `method_name` (a Symbol) is declared an initializer
64
- # by this entry. The class-constraint match is the caller's
65
- # responsibility (it needs the environment's class graph).
74
+ # True when `method_name` (a Symbol) is declared a def-form
75
+ # initializer by this entry.
66
76
  def covers_method?(method_name)
67
77
  methods.include?(method_name)
68
78
  end
69
79
 
80
+ # True when `method_name` (a Symbol) is declared a block-form
81
+ # initializer by this entry.
82
+ def covers_block_method?(method_name)
83
+ block_methods.include?(method_name)
84
+ end
85
+
70
86
  def to_h
71
87
  {
72
88
  "receiver_constraint" => receiver_constraint,
73
- "methods" => methods.map(&:to_s)
89
+ "methods" => methods.map(&:to_s),
90
+ "block_methods" => block_methods.map(&:to_s)
74
91
  }
75
92
  end
76
93
 
@@ -93,16 +110,22 @@ module Rigor
93
110
  "got #{value.inspect}"
94
111
  end
95
112
 
96
- def validate_methods!(value)
97
- if value.is_a?(Array) && !value.empty? &&
98
- value.all? { |m| m.is_a?(Symbol) || (m.is_a?(String) && !m.empty?) }
99
- return
100
- end
113
+ def validate_method_list!(value, field)
114
+ return if value.is_a?(Array) &&
115
+ value.all? { |m| m.is_a?(Symbol) || (m.is_a?(String) && !m.empty?) }
101
116
 
102
117
  raise ArgumentError,
103
- "Plugin::AdditionalInitializer#methods must be a non-empty Array of " \
118
+ "Plugin::AdditionalInitializer##{field} must be an Array of " \
104
119
  "Symbol/non-empty String, got #{value.inspect}"
105
120
  end
121
+
122
+ def validate_at_least_one!(methods, block_methods)
123
+ return unless methods.empty? && block_methods.empty?
124
+
125
+ raise ArgumentError,
126
+ "Plugin::AdditionalInitializer requires at least one of methods: or block_methods: " \
127
+ "to be non-empty"
128
+ end
106
129
  end
107
130
  end
108
131
  end