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
@@ -608,6 +608,45 @@ module Rigor
608
608
  ParamOverride.new(param_name: match[:param].to_sym, type: type)
609
609
  end
610
610
 
611
+ # A class- / module-level directive declaring that the
612
+ # annotated class satisfies a named structural interface as
613
+ # part of its public contract (spec:
614
+ # `docs/type-specification/rbs-extended.md` § "Explicit
615
+ # conformance directive"). Unlike the per-method directives
616
+ # above, this attaches to a `class` / `module` declaration and
617
+ # names a single RBS interface (`_RewindableStream`); the
618
+ # right-hand side is therefore an interface name (its last
619
+ # segment begins with `_`), never a refinement payload.
620
+ #
621
+ # This parser only extracts the interface name; the
622
+ # conformance check itself lives in
623
+ # {Rigor::RbsExtended::ConformanceChecker}, which the
624
+ # {Rigor::Analysis::Runner} runs once per project run.
625
+ CONFORMS_TO_DIRECTIVE_PATTERN = /
626
+ \A
627
+ rigor:v1:conforms-to
628
+ \s+
629
+ (?<interface>(?:::)?(?:[A-Z]\w*::)*_[A-Za-z]\w*)
630
+ \s*
631
+ \z
632
+ /x
633
+ private_constant :CONFORMS_TO_DIRECTIVE_PATTERN
634
+
635
+ # Returns the interface name (leading `::` stripped) for a
636
+ # `rigor:v1:conforms-to <Interface>` annotation, or `nil` when
637
+ # the string is not a conforms-to directive (so callers can
638
+ # walk an annotation list without pre-filtering). The name is
639
+ # returned verbatim otherwise — namespace resolution happens at
640
+ # the loader boundary when the interface is built.
641
+ def parse_conforms_to_annotation(string)
642
+ return nil if string.nil?
643
+
644
+ match = CONFORMS_TO_DIRECTIVE_PATTERN.match(string)
645
+ return nil if match.nil?
646
+
647
+ match[:interface].to_s.sub(/\A::/, "")
648
+ end
649
+
611
650
  # The shared {Rigor::FlowContribution::Provenance} for every
612
651
  # bundle this module produces. `source_family: :rbs_extended`
613
652
  # so consumers (today the documentation surface; v0.1.0 the
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Scope
5
+ # ADR-53 Track A — the seed-time discovery context every Scope snapshot
6
+ # carries by a single reference. Holds the tables the index-time
7
+ # pre-passes (`Inference::ScopeIndexer` per file, plus the cross-file
8
+ # project pre-pass) populate and that no control-flow transition ever
9
+ # varies: `Scope#==` ignores them and `Scope#join` copies them from the
10
+ # receiver unexamined, which is the membership litmus the ADR fixes.
11
+ #
12
+ # Immutable (`Data` instances are frozen); deriving a seeded index goes
13
+ # through `#with(table_name: table)`. `Scope` exposes each table through
14
+ # its existing reader surface, so engine call sites and plugins are
15
+ # unaffected by the extraction.
16
+ DiscoveryIndex = Data.define(
17
+ :declared_types,
18
+ :class_ivars,
19
+ :class_cvars,
20
+ :program_globals,
21
+ :discovered_classes,
22
+ :in_source_constants,
23
+ :discovered_methods,
24
+ :discovered_def_nodes,
25
+ :discovered_def_sources,
26
+ :discovered_method_visibilities,
27
+ :discovered_superclasses,
28
+ :discovered_includes,
29
+ :discovered_class_sources,
30
+ :data_member_layouts
31
+ )
32
+
33
+ class DiscoveryIndex
34
+ EMPTY_NODE_TABLE = {}.compare_by_identity.freeze
35
+ EMPTY_TABLE = {}.freeze
36
+ private_constant :EMPTY_NODE_TABLE, :EMPTY_TABLE
37
+
38
+ # The shared all-empty index `Scope.empty` (and every scope that never
39
+ # sees a seeding pass) points at — one allocation per process.
40
+ EMPTY = new(
41
+ declared_types: EMPTY_NODE_TABLE,
42
+ class_ivars: EMPTY_TABLE,
43
+ class_cvars: EMPTY_TABLE,
44
+ program_globals: EMPTY_TABLE,
45
+ discovered_classes: EMPTY_TABLE,
46
+ in_source_constants: EMPTY_TABLE,
47
+ discovered_methods: EMPTY_TABLE,
48
+ discovered_def_nodes: EMPTY_TABLE,
49
+ discovered_def_sources: EMPTY_TABLE,
50
+ discovered_method_visibilities: EMPTY_TABLE,
51
+ discovered_superclasses: EMPTY_TABLE,
52
+ discovered_includes: EMPTY_TABLE,
53
+ discovered_class_sources: EMPTY_TABLE,
54
+ data_member_layouts: EMPTY_TABLE
55
+ )
56
+ end
57
+ end
58
+ end
data/lib/rigor/scope.rb CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  require_relative "type"
4
4
  require_relative "environment"
5
+ require_relative "scope/discovery_index"
5
6
  require_relative "analysis/fact_store"
7
+ require_relative "analysis/dependency_recorder"
6
8
  require_relative "inference/expression_typer"
9
+ require_relative "inference/flow_tracer"
7
10
  require_relative "inference/statement_evaluator"
8
11
 
9
12
  module Rigor
@@ -16,14 +19,36 @@ module Rigor
16
19
  # See docs/internal-spec/inference-engine.md for the binding contract.
17
20
  # rubocop:disable Metrics/ClassLength,Metrics/ParameterLists
18
21
  class Scope
19
- attr_reader :environment, :locals, :fact_store, :self_type, :declared_types,
22
+ attr_reader :environment, :locals, :fact_store, :self_type,
20
23
  :ivars, :cvars, :globals,
21
- :class_ivars, :class_cvars, :program_globals,
22
- :discovered_classes, :in_source_constants, :discovered_methods,
23
- :discovered_def_nodes, :discovered_def_sources, :discovered_method_visibilities,
24
- :discovered_superclasses, :discovered_includes,
25
24
  :indexed_narrowings, :method_chain_narrowings,
26
- :source_path
25
+ :source_path, :discovery
26
+
27
+ # ADR-53 Track A — the seed-time discovery tables live on the
28
+ # {DiscoveryIndex} the scope carries by a single reference; the
29
+ # per-table readers stay on Scope so engine call sites and plugins
30
+ # are unaffected by the extraction. The whole index is swapped in
31
+ # one transition through {#with_discovery}.
32
+ #
33
+ # `declared_types` carries the identity-comparing
34
+ # `Prism::Node => Rigor::Type` declaration overrides
35
+ # `ExpressionTyper#type_of(node)` MUST consult before any other
36
+ # dispatch (a `module Foo` / `class Bar` header types as
37
+ # `Singleton[<qualified path>]` rather than `Dynamic[Top]`).
38
+ def declared_types = @discovery.declared_types
39
+ def class_ivars = @discovery.class_ivars
40
+ def class_cvars = @discovery.class_cvars
41
+ def program_globals = @discovery.program_globals
42
+ def discovered_classes = @discovery.discovered_classes
43
+ def in_source_constants = @discovery.in_source_constants
44
+ def discovered_methods = @discovery.discovered_methods
45
+ def discovered_def_nodes = @discovery.discovered_def_nodes
46
+ def discovered_def_sources = @discovery.discovered_def_sources
47
+ def discovered_method_visibilities = @discovery.discovered_method_visibilities
48
+ def discovered_superclasses = @discovery.discovered_superclasses
49
+ def discovered_includes = @discovery.discovered_includes
50
+ def discovered_class_sources = @discovery.discovered_class_sources
51
+ def data_member_layouts = @discovery.data_member_layouts
27
52
 
28
53
  # Narrowing key for an indexed read `receiver[key]` where both
29
54
  # the receiver and the key are stable enough to address. The
@@ -58,13 +83,10 @@ module Rigor
58
83
  # multi-hop loses the LoD guarantee).
59
84
  ChainKey = Data.define(:receiver_kind, :receiver_name, :method_name)
60
85
 
61
- EMPTY_DECLARED_TYPES = {}.compare_by_identity.freeze
62
86
  EMPTY_VAR_BINDINGS = {}.freeze
63
- EMPTY_CLASS_BINDINGS = {}.freeze
64
87
  EMPTY_INDEXED_NARROWINGS = {}.freeze
65
88
  EMPTY_CHAIN_NARROWINGS = {}.freeze
66
- private_constant :EMPTY_DECLARED_TYPES, :EMPTY_VAR_BINDINGS,
67
- :EMPTY_CLASS_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
89
+ private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
68
90
  :EMPTY_CHAIN_NARROWINGS
69
91
 
70
92
  class << self
@@ -78,21 +100,10 @@ module Rigor
78
100
  environment:, locals:,
79
101
  fact_store: Analysis::FactStore.empty,
80
102
  self_type: nil,
81
- declared_types: EMPTY_DECLARED_TYPES,
82
103
  ivars: EMPTY_VAR_BINDINGS,
83
104
  cvars: EMPTY_VAR_BINDINGS,
84
105
  globals: EMPTY_VAR_BINDINGS,
85
- class_ivars: EMPTY_CLASS_BINDINGS,
86
- class_cvars: EMPTY_CLASS_BINDINGS,
87
- program_globals: EMPTY_VAR_BINDINGS,
88
- discovered_classes: EMPTY_VAR_BINDINGS,
89
- in_source_constants: EMPTY_VAR_BINDINGS,
90
- discovered_methods: EMPTY_CLASS_BINDINGS,
91
- discovered_def_nodes: EMPTY_CLASS_BINDINGS,
92
- discovered_def_sources: EMPTY_CLASS_BINDINGS,
93
- discovered_method_visibilities: EMPTY_CLASS_BINDINGS,
94
- discovered_superclasses: EMPTY_CLASS_BINDINGS,
95
- discovered_includes: EMPTY_CLASS_BINDINGS,
106
+ discovery: DiscoveryIndex::EMPTY,
96
107
  indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
97
108
  method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
98
109
  source_path: nil
@@ -101,21 +112,10 @@ module Rigor
101
112
  @locals = locals
102
113
  @fact_store = fact_store
103
114
  @self_type = self_type
104
- @declared_types = declared_types
105
115
  @ivars = ivars
106
116
  @cvars = cvars
107
117
  @globals = globals
108
- @class_ivars = class_ivars
109
- @class_cvars = class_cvars
110
- @program_globals = program_globals
111
- @discovered_classes = discovered_classes
112
- @in_source_constants = in_source_constants
113
- @discovered_methods = discovered_methods
114
- @discovered_def_nodes = discovered_def_nodes
115
- @discovered_def_sources = discovered_def_sources
116
- @discovered_method_visibilities = discovered_method_visibilities
117
- @discovered_superclasses = discovered_superclasses
118
- @discovered_includes = discovered_includes
118
+ @discovery = discovery
119
119
  @indexed_narrowings = indexed_narrowings
120
120
  @method_chain_narrowings = method_chain_narrowings
121
121
  @source_path = source_path
@@ -127,6 +127,8 @@ module Rigor
127
127
  end
128
128
 
129
129
  def with_local(name, type)
130
+ # `rigor trace` — the moment a local enters the scope.
131
+ Inference::FlowTracer.bind(name, type) if Inference::FlowTracer.active?
130
132
  new_locals = @locals.merge(name.to_sym => type).freeze
131
133
  new_fact_store = fact_store.invalidate_target(Analysis::FactStore::Target.local(name))
132
134
  # Rebinding `name` invalidates every "after `receiver[key]
@@ -160,28 +162,19 @@ module Rigor
160
162
  # ADR-11 per-call-site assertion gating prerequisite. The
161
163
  # analyzer's per-file boundary stamps the current source
162
164
  # file's path onto the seed scope; nested rebuilds carry
163
- # the value through so plugin hooks like
164
- # `flow_contribution_for` can resolve "which file does
165
- # this call site belong to?" without thread-locals.
165
+ # the value through so plugin rules (`dynamic_return`'s
166
+ # `file_methods:` gate, sigil checks) can resolve "which
167
+ # file does this call site belong to?" without
168
+ # thread-locals.
166
169
  def with_source_path(path)
167
170
  rebuild(source_path: path)
168
171
  end
169
172
 
170
- # Slice A-declarations. Returns a scope that carries an
171
- # identity-comparing Hash of `Prism::Node => Rigor::Type`
172
- # overrides. `ExpressionTyper#type_of(node)` MUST consult
173
- # `declared_types[node]` before any other dispatch and
174
- # return the recorded type as-is when present. The table is
175
- # populated by `ScopeIndexer` for declaration-position
176
- # nodes (the `constant_path` of `Prism::ModuleNode` and
177
- # `Prism::ClassNode`) so a `module Foo` / `class Bar`
178
- # header types as `Singleton[<qualified path>]` instead of
179
- # falling through to `Dynamic[Top]`. The table is shared
180
- # by structural reference across every derived scope so
181
- # `with_local` / `with_fact` / `with_self_type` carry it
182
- # transparently.
183
- def with_declared_types(table)
184
- rebuild(declared_types: table)
173
+ # ADR-53 Track A swaps the whole discovery index in one transition.
174
+ # The sole seeding path; the per-table writers it replaced are derived
175
+ # off-`Scope` through `scope.discovery.with(table_name: table)`.
176
+ def with_discovery(index)
177
+ rebuild(discovery: index)
185
178
  end
186
179
 
187
180
  # Slice 7 phase 1 — instance/class/global variable bindings.
@@ -237,11 +230,7 @@ module Rigor
237
230
  def class_ivars_for(class_name)
238
231
  return EMPTY_VAR_BINDINGS if class_name.nil?
239
232
 
240
- @class_ivars[class_name.to_s] || EMPTY_VAR_BINDINGS
241
- end
242
-
243
- def with_class_ivars(table)
244
- rebuild(class_ivars: table)
233
+ @discovery.class_ivars[class_name.to_s] || EMPTY_VAR_BINDINGS
245
234
  end
246
235
 
247
236
  # Slice 7 phase 6 — class-level cvar accumulator (same shape
@@ -251,48 +240,7 @@ module Rigor
251
240
  def class_cvars_for(class_name)
252
241
  return EMPTY_VAR_BINDINGS if class_name.nil?
253
242
 
254
- @class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS
255
- end
256
-
257
- def with_class_cvars(table)
258
- rebuild(class_cvars: table)
259
- end
260
-
261
- # Slice 7 phase 6 — program-level globals accumulator.
262
- # Globals are process-wide in Ruby, so the analyzer carries a
263
- # single map (`Hash[Symbol, Type]`) keyed by the variable name
264
- # and seeded into every method body (instance and singleton)
265
- # plus the top-level program scope. `ScopeIndexer` populates
266
- # it from a single program-wide pre-pass.
267
- def with_program_globals(table)
268
- rebuild(program_globals: table)
269
- end
270
-
271
- # Slice 7 phase 7 — in-source class discovery. Maps a
272
- # qualified class name (e.g. `"Account"`) to its
273
- # `Type::Singleton` so references to user-defined classes
274
- # in the analyzed files resolve through
275
- # `ExpressionTyper#resolve_constant_name` even when no RBS
276
- # decl exists. Populated once at index time by
277
- # `ScopeIndexer` from every `Prism::ClassNode` and
278
- # `Prism::ModuleNode` it walks.
279
- def with_discovered_classes(table)
280
- rebuild(discovered_classes: table)
281
- end
282
-
283
- # Slice 7 phase 9 — in-source constant-value tracking.
284
- # Maps a qualified constant name (e.g. `"BUCKETS"` or
285
- # `"Rigor::Analysis::FactStore::BUCKETS"`) to the type of
286
- # the rvalue assigned at its `Prism::ConstantWriteNode` /
287
- # `Prism::ConstantPathWriteNode`. Populated by
288
- # `ScopeIndexer` once at index time. `ExpressionTyper#resolve_constant_name`
289
- # consults this map after class lookups so an in-source
290
- # constant assignment overrides any RBS-declared constant
291
- # of the same qualified name (matching Ruby's runtime
292
- # precedence: a constant defined in user code is the
293
- # authoritative value).
294
- def with_in_source_constants(table)
295
- rebuild(in_source_constants: table)
243
+ @discovery.class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS
296
244
  end
297
245
 
298
246
  # Slice 7 phase 12 — in-source method discovery. Maps a
@@ -305,7 +253,7 @@ module Rigor
305
253
  # user has defined dynamically, even when no RBS sig
306
254
  # describes them.
307
255
  def discovered_method?(class_name, method_name, kind)
308
- table = @discovered_methods[class_name.to_s]
256
+ table = @discovery.discovered_methods[class_name.to_s]
309
257
  return false unless table
310
258
 
311
259
  table[method_name.to_sym] == kind
@@ -325,10 +273,6 @@ module Rigor
325
273
  @self_type.nil?
326
274
  end
327
275
 
328
- def with_discovered_methods(table)
329
- rebuild(discovered_methods: table)
330
- end
331
-
332
276
  # v0.0.2 #5 — per-class table mapping
333
277
  # `method_name (Symbol) → Prism::DefNode`. Populated by
334
278
  # `ScopeIndexer` alongside `discovered_methods` for
@@ -339,11 +283,30 @@ module Rigor
339
283
  # inference when the receiver class is user-defined and
340
284
  # has no RBS sig.
341
285
  def user_def_for(class_name, method_name)
342
- table = @discovered_def_nodes[class_name.to_s]
343
- return nil unless table
344
-
345
- table[method_name.to_sym]
286
+ table = @discovery.discovered_def_nodes[class_name.to_s]
287
+ node = table && table[method_name.to_sym]
288
+ record_cross_file_method(class_name, method_name, node) if Analysis::DependencyRecorder.active?
289
+ node
290
+ end
291
+
292
+ # ADR-46 slice 1 — note the cross-file dependency this resolution
293
+ # creates: the file defining `class_name#method_name` (the consumer's
294
+ # analysis reads its body via `infer_user_method_return`), or, when
295
+ # unresolved, a negative edge so a later definition re-checks the
296
+ # consumer. Gated on the recorder being active — no-op on a normal run.
297
+ def record_cross_file_method(class_name, method_name, node)
298
+ if node
299
+ # ADR-46 slice 4 — pass the symbol so the recorder tracks this as a
300
+ # method-call (symbol-granularity) edge rather than a file-level edge.
301
+ Analysis::DependencyRecorder.read_site(
302
+ @discovery.discovered_def_sources.dig(class_name.to_s, method_name.to_sym),
303
+ "#{class_name}##{method_name}"
304
+ )
305
+ else
306
+ Analysis::DependencyRecorder.read_missing(:method, "#{class_name}##{method_name}")
307
+ end
346
308
  end
309
+ private :record_cross_file_method
347
310
 
348
311
  # v0.0.3 A — top-level def lookup for implicit-self
349
312
  # calls. Returns the `Prism::DefNode` for a top-level
@@ -353,15 +316,32 @@ module Rigor
353
316
  # consumers should treat its presence as an opaque
354
317
  # implementation detail and go through this accessor.
355
318
  def top_level_def_for(method_name)
356
- table = @discovered_def_nodes[Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY]
357
- return nil unless table
358
-
359
- table[method_name.to_sym]
360
- end
361
-
362
- def with_discovered_def_nodes(table)
363
- rebuild(discovered_def_nodes: table)
319
+ table = @discovery.discovered_def_nodes[Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY]
320
+ node = table && table[method_name.to_sym]
321
+ record_cross_file_toplevel(method_name, node) if Analysis::DependencyRecorder.active?
322
+ node
323
+ end
324
+
325
+ # ADR-46 slice 3 — a top-level (`def helper` outside any class) call has
326
+ # NO class ancestry to walk, so unlike {#user_def_for} a miss here records
327
+ # no positive ancestry edge that would re-check the consumer when the
328
+ # method later appears. Record the cross-file edge explicitly: the file
329
+ # defining the top-level method (symbol-granularity, so a body / removal
330
+ # edit re-checks the caller), or, on a miss, a negative `toplevel:` edge
331
+ # so a later top-level definition re-checks this consumer (the
332
+ # `call.unresolved-toplevel` stale-diagnostic gap).
333
+ def record_cross_file_toplevel(method_name, node)
334
+ key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
335
+ if node
336
+ Analysis::DependencyRecorder.read_site(
337
+ @discovery.discovered_def_sources.dig(key, method_name.to_sym),
338
+ "#{key}##{method_name}"
339
+ )
340
+ else
341
+ Analysis::DependencyRecorder.read_missing(:toplevel, method_name)
342
+ end
364
343
  end
344
+ private :record_cross_file_toplevel
365
345
 
366
346
  # Companion to {#user_def_for}: returns the `"path:line"` where
367
347
  # the project defines `class_name#method_name` (instance-side),
@@ -374,16 +354,12 @@ module Rigor
374
354
  # can point at `pre_eval:` (ADR-17) instead of reading as a bare
375
355
  # unresolved call.
376
356
  def user_def_site_for(class_name, method_name)
377
- table = @discovered_def_sources[class_name.to_s]
357
+ table = @discovery.discovered_def_sources[class_name.to_s]
378
358
  return nil unless table
379
359
 
380
360
  table[method_name.to_sym]
381
361
  end
382
362
 
383
- def with_discovered_def_sources(table)
384
- rebuild(discovered_def_sources: table)
385
- end
386
-
387
363
  # ADR-24 slice 2 — per-class table mapping a fully
388
364
  # qualified user-class name to its superclass name AS
389
365
  # WRITTEN at the `class Foo < Bar` declaration (`"Bar"`,
@@ -395,11 +371,25 @@ module Rigor
395
371
  # The as-written name is resolved to a qualified class at
396
372
  # walk time against the call's lexical nesting.
397
373
  def superclass_of(class_name)
398
- @discovered_superclasses[class_name.to_s]
399
- end
400
-
401
- def with_discovered_superclasses(table)
402
- rebuild(discovered_superclasses: table)
374
+ record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
375
+ @discovery.discovered_superclasses[class_name.to_s]
376
+ end
377
+
378
+ # ADR-48 — per-class table mapping a fully qualified class name to its
379
+ # ordered `Data.define` / `Struct.new` member-name list. Populated by
380
+ # `ScopeIndexer` for both the constant-assigned form
381
+ # (`Point = Data.define(:x, :y)`) and the named-subclass form
382
+ # (`class Point < Data.define(:x, :y)`). Consumed by
383
+ # {Inference::MethodDispatcher::DataFolding} so `Point.new(...)` on a
384
+ # `Singleton[Point]` receiver materialises a precise member instance.
385
+ # Returns nil when the class has no recorded layout.
386
+ def data_member_layout(class_name)
387
+ layout = @discovery.data_member_layouts[class_name.to_s]
388
+ # Record the ancestry dependency only on a hit — DataFolding consults
389
+ # this for every `Singleton[*].new`, and a miss (the common case: an
390
+ # ordinary class) must not manufacture a spurious cross-file edge.
391
+ record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active?
392
+ layout
403
393
  end
404
394
 
405
395
  # ADR-24 slice 2 — per-class/module table mapping a fully
@@ -412,12 +402,25 @@ module Rigor
412
402
  # `def`s, not just the superclass chain. As-written names
413
403
  # are resolved to qualified classes at walk time.
414
404
  def includes_of(class_name)
415
- @discovered_includes[class_name.to_s] || []
405
+ record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
406
+ @discovery.discovered_includes[class_name.to_s] || []
416
407
  end
417
408
 
418
- def with_discovered_includes(table)
419
- rebuild(discovered_includes: table)
409
+ # Records, for a resolved cross-class ancestry read, every file that
410
+ # declares `class_name` (its declaration / reopening / superclass /
411
+ # include sites). The `discovered_class_sources` table it reads is
412
+ # populated only by the cross-file project pre-pass
413
+ # ({Inference::ScopeIndexer.discovered_def_index_for_paths}) and only
414
+ # when dependency recording is active. No-op when the class is not a
415
+ # project class (core / stdlib / gem names never appear in the source
416
+ # map). Gated by the caller on the recorder being active.
417
+ def record_class_dependency(class_name)
418
+ sites = @discovery.discovered_class_sources[class_name.to_s]
419
+ return if sites.nil?
420
+
421
+ sites.each { |site| Analysis::DependencyRecorder.read_site(site) }
420
422
  end
423
+ private :record_class_dependency
421
424
 
422
425
  # v0.1.2 — per-class table mapping `method_name (Symbol) →
423
426
  # :public | :private | :protected`. Populated by
@@ -429,16 +432,12 @@ module Rigor
429
432
  # so explicit-non-self calls to a private method surface
430
433
  # a diagnostic.
431
434
  def discovered_method_visibility(class_name, method_name)
432
- table = @discovered_method_visibilities[class_name.to_s]
435
+ table = @discovery.discovered_method_visibilities[class_name.to_s]
433
436
  return nil unless table
434
437
 
435
438
  table[method_name.to_sym]
436
439
  end
437
440
 
438
- def with_discovered_method_visibilities(table)
439
- rebuild(discovered_method_visibilities: table)
440
- end
441
-
442
441
  # Closes the "`params[:f] ||= []; params[:f] << x`" precision
443
442
  # gap (ROADMAP § Type-language / engine — indexed-collection
444
443
  # narrowing through `Hash[k] ||= default`). After
@@ -577,14 +576,8 @@ module Rigor
577
576
 
578
577
  def rebuild(
579
578
  locals: @locals, fact_store: @fact_store, self_type: @self_type,
580
- declared_types: @declared_types, ivars: @ivars, cvars: @cvars, globals: @globals,
581
- class_ivars: @class_ivars, class_cvars: @class_cvars, program_globals: @program_globals,
582
- discovered_classes: @discovered_classes, in_source_constants: @in_source_constants,
583
- discovered_methods: @discovered_methods, discovered_def_nodes: @discovered_def_nodes,
584
- discovered_def_sources: @discovered_def_sources,
585
- discovered_method_visibilities: @discovered_method_visibilities,
586
- discovered_superclasses: @discovered_superclasses,
587
- discovered_includes: @discovered_includes,
579
+ ivars: @ivars, cvars: @cvars, globals: @globals,
580
+ discovery: @discovery,
588
581
  indexed_narrowings: @indexed_narrowings,
589
582
  method_chain_narrowings: @method_chain_narrowings,
590
583
  source_path: @source_path
@@ -592,18 +585,8 @@ module Rigor
592
585
  self.class.new(
593
586
  environment: environment, locals: locals,
594
587
  fact_store: fact_store, self_type: self_type,
595
- declared_types: declared_types,
596
588
  ivars: ivars, cvars: cvars, globals: globals,
597
- class_ivars: class_ivars, class_cvars: class_cvars,
598
- program_globals: program_globals,
599
- discovered_classes: discovered_classes,
600
- in_source_constants: in_source_constants,
601
- discovered_methods: discovered_methods,
602
- discovered_def_nodes: discovered_def_nodes,
603
- discovered_def_sources: discovered_def_sources,
604
- discovered_method_visibilities: discovered_method_visibilities,
605
- discovered_superclasses: discovered_superclasses,
606
- discovered_includes: discovered_includes,
589
+ discovery: discovery,
607
590
  indexed_narrowings: indexed_narrowings,
608
591
  method_chain_narrowings: method_chain_narrowings,
609
592
  source_path: source_path
@@ -611,8 +594,19 @@ module Rigor
611
594
  end
612
595
 
613
596
  def join_bindings(left, right)
614
- shared = left.keys & right.keys
615
- shared.to_h { |name| [name, Type::Combinator.union(left[name], right[name])] }.freeze
597
+ # Keys present in both, unioned. Iterating `left` and probing
598
+ # `right.key?` yields the same keys in the same order as the prior
599
+ # `(left.keys & right.keys)` while avoiding the two key arrays and
600
+ # the intersection array — this is the control-flow join, run at
601
+ # every branch merge, and was a top allocation site (~75% of
602
+ # `Hash#keys`).
603
+ result = {}
604
+ left.each do |name, ltype|
605
+ next unless right.key?(name)
606
+
607
+ result[name] = Type::Combinator.union(ltype, right[name])
608
+ end
609
+ result.freeze
616
610
  end
617
611
 
618
612
  def build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
@@ -621,21 +615,10 @@ module Rigor
621
615
  locals: joined_locals.freeze,
622
616
  fact_store: fact_store.join(other.fact_store),
623
617
  self_type: self_type == other.self_type ? self_type : nil,
624
- declared_types: declared_types,
625
618
  ivars: joined_ivars,
626
619
  cvars: joined_cvars,
627
620
  globals: joined_globals,
628
- class_ivars: class_ivars,
629
- class_cvars: class_cvars,
630
- program_globals: program_globals,
631
- discovered_classes: discovered_classes,
632
- in_source_constants: in_source_constants,
633
- discovered_methods: discovered_methods,
634
- discovered_def_nodes: discovered_def_nodes,
635
- discovered_def_sources: discovered_def_sources,
636
- discovered_method_visibilities: discovered_method_visibilities,
637
- discovered_superclasses: discovered_superclasses,
638
- discovered_includes: discovered_includes,
621
+ discovery: @discovery,
639
622
  indexed_narrowings: join_bindings(@indexed_narrowings, other.indexed_narrowings),
640
623
  method_chain_narrowings: join_bindings(@method_chain_narrowings, other.method_chain_narrowings),
641
624
  source_path: source_path
@@ -96,7 +96,8 @@ module Rigor
96
96
  parse_result = Prism.parse(source, filepath: path, version: @configuration.target_ruby)
97
97
  return if parse_result.errors.any?
98
98
 
99
- base_scope = Scope.empty(environment: environment).with_discovered_classes(discovered_classes)
99
+ base_scope = Scope.empty(environment: environment)
100
+ base_scope = base_scope.with_discovery(base_scope.discovery.with(discovered_classes: discovered_classes))
100
101
  scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
101
102
  bindings = collect_rspec_bindings(parse_result.value, scope_index)
102
103
 
@@ -104,11 +105,10 @@ module Rigor
104
105
  end
105
106
 
106
107
  # Pre-walks `@source_paths` to collect every qualified
107
- # class / module declaration. The result feeds
108
- # `Scope#with_discovered_classes` for each observe-tree
109
- # scope so `Foo.new` and `Foo` resolve to the right
110
- # singleton carrier even when no RBS sig describes
111
- # `Foo` yet.
108
+ # class / module declaration. The result seeds the
109
+ # `discovered_classes` table on each observe-tree scope so
110
+ # `Foo.new` and `Foo` resolve to the right singleton carrier
111
+ # even when no RBS sig describes `Foo` yet.
112
112
  def preindex_source_classes
113
113
  accumulator = {}
114
114
  resolve_paths(@source_paths).each { |path| harvest_classes_from(path, accumulator) }
@@ -100,6 +100,20 @@ module Rigor
100
100
  args.filter_map { |arg| symbol_or_string(arg) }
101
101
  end
102
102
 
103
+ # Whether a node is a literal `Prism::SymbolNode` that names `name`.
104
+ # The key-comparison counterpart to {.symbol_name} — for callers
105
+ # that need a predicate rather than an extraction (hash-key matching
106
+ # in keyword or assoc argument positions, e.g.
107
+ # `el.is_a?(AssocNode) && symbol_named?(el.key, "required")`).
108
+ # Uses `#unescaped` (not `#value`) for round-trip consistency.
109
+ #
110
+ # @param node [Prism::Node, nil]
111
+ # @param name [String]
112
+ # @return [Boolean]
113
+ def symbol_named?(node, name)
114
+ node.is_a?(Prism::SymbolNode) && node.unescaped == name
115
+ end
116
+
103
117
  # The literal Symbol/String at positional `index`, or `nil` when the
104
118
  # call has no argument list, the index is out of range, or the
105
119
  # argument there is not a literal Symbol/String.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Type
5
+ # Routes `accepts` through the engine's acceptance dispatcher.
6
+ #
7
+ # Every carrier's acceptance check is the same fixed forwarding on
8
+ # `self` — `Inference::Acceptance.accepts(self, other, mode: mode)` —
9
+ # so it lived as an identical copy in fourteen carriers. The one
10
+ # exception is `Type::App`, which forwards on its reduced `bound`
11
+ # type rather than `self`; it keeps its own `accepts` and does not
12
+ # include this.
13
+ module AcceptanceRouter
14
+ def accepts(other, mode: :gradual)
15
+ Inference::Acceptance.accepts(self, other, mode: mode)
16
+ end
17
+ end
18
+ end
19
+ end