rigortype 0.1.19 → 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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
  3. data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
  4. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  5. data/lib/rigor/analysis/check_rules.rb +492 -71
  6. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  8. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  9. data/lib/rigor/analysis/fact_store.rb +5 -4
  10. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
  12. data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
  13. data/lib/rigor/analysis/runner.rb +17 -6
  14. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  15. data/lib/rigor/analysis/worker_session.rb +10 -14
  16. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  17. data/lib/rigor/cache/store.rb +5 -3
  18. data/lib/rigor/cli/annotate_command.rb +28 -7
  19. data/lib/rigor/cli/baseline_command.rb +4 -3
  20. data/lib/rigor/cli/check_command.rb +115 -16
  21. data/lib/rigor/cli/coverage_command.rb +148 -16
  22. data/lib/rigor/cli/coverage_scan.rb +57 -0
  23. data/lib/rigor/cli/explain_command.rb +2 -0
  24. data/lib/rigor/cli/lsp_command.rb +3 -7
  25. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  26. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  27. data/lib/rigor/cli/options.rb +9 -0
  28. data/lib/rigor/cli/plugins_command.rb +2 -1
  29. data/lib/rigor/cli/protection_renderer.rb +63 -0
  30. data/lib/rigor/cli/protection_report.rb +68 -0
  31. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  32. data/lib/rigor/cli/trace_command.rb +2 -1
  33. data/lib/rigor/cli/triage_command.rb +2 -1
  34. data/lib/rigor/cli/type_of_command.rb +1 -1
  35. data/lib/rigor/cli/type_scan_command.rb +2 -1
  36. data/lib/rigor/cli.rb +3 -2
  37. data/lib/rigor/configuration/dependencies.rb +2 -4
  38. data/lib/rigor/configuration.rb +45 -7
  39. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  40. data/lib/rigor/environment/class_registry.rb +4 -3
  41. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  42. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  43. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  44. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  45. data/lib/rigor/environment/rbs_loader.rb +49 -5
  46. data/lib/rigor/environment.rb +17 -7
  47. data/lib/rigor/flow_contribution/fact.rb +1 -1
  48. data/lib/rigor/flow_contribution.rb +3 -5
  49. data/lib/rigor/inference/acceptance.rb +17 -9
  50. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  51. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  52. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  53. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  54. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  55. data/lib/rigor/inference/expression_typer.rb +20 -28
  56. data/lib/rigor/inference/hkt_body.rb +8 -11
  57. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  58. data/lib/rigor/inference/hkt_registry.rb +10 -11
  59. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  60. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
  61. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  62. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  63. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  64. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  65. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  66. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  67. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  68. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
  69. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  70. data/lib/rigor/inference/method_dispatcher.rb +40 -48
  71. data/lib/rigor/inference/mutation_widening.rb +5 -11
  72. data/lib/rigor/inference/narrowing.rb +14 -16
  73. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  74. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  75. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  76. data/lib/rigor/inference/protection_scanner.rb +86 -0
  77. data/lib/rigor/inference/scope_indexer.rb +129 -55
  78. data/lib/rigor/inference/statement_evaluator.rb +244 -114
  79. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  80. data/lib/rigor/inference/synthetic_method.rb +7 -7
  81. data/lib/rigor/language_server/completion_provider.rb +6 -12
  82. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  83. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  84. data/lib/rigor/language_server/hover_provider.rb +2 -3
  85. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  86. data/lib/rigor/language_server/server.rb +9 -17
  87. data/lib/rigor/language_server.rb +4 -5
  88. data/lib/rigor/plugin/base.rb +10 -8
  89. data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
  90. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  91. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  92. data/lib/rigor/plugin/macro.rb +4 -5
  93. data/lib/rigor/plugin/manifest.rb +45 -66
  94. data/lib/rigor/plugin/registry.rb +6 -7
  95. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  96. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  97. data/lib/rigor/protection/mutator.rb +246 -0
  98. data/lib/rigor/rbs_extended.rb +24 -36
  99. data/lib/rigor/reflection.rb +4 -7
  100. data/lib/rigor/scope/discovery_index.rb +14 -2
  101. data/lib/rigor/scope.rb +54 -11
  102. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  103. data/lib/rigor/sig_gen/writer.rb +40 -2
  104. data/lib/rigor/source/constant_path.rb +62 -0
  105. data/lib/rigor/source.rb +1 -0
  106. data/lib/rigor/type/bound_method.rb +2 -11
  107. data/lib/rigor/type/combinator.rb +16 -3
  108. data/lib/rigor/type/constant.rb +2 -11
  109. data/lib/rigor/type/data_class.rb +2 -11
  110. data/lib/rigor/type/data_instance.rb +2 -11
  111. data/lib/rigor/type/hash_shape.rb +2 -11
  112. data/lib/rigor/type/integer_range.rb +2 -11
  113. data/lib/rigor/type/intersection.rb +2 -11
  114. data/lib/rigor/type/nominal.rb +2 -11
  115. data/lib/rigor/type/plain_lattice.rb +37 -0
  116. data/lib/rigor/type/refined.rb +72 -13
  117. data/lib/rigor/type/singleton.rb +2 -11
  118. data/lib/rigor/type/struct_class.rb +75 -0
  119. data/lib/rigor/type/struct_instance.rb +93 -0
  120. data/lib/rigor/type/tuple.rb +5 -15
  121. data/lib/rigor/type.rb +2 -0
  122. data/lib/rigor/version.rb +1 -1
  123. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  124. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  125. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
  126. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  127. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  128. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
  129. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  130. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  131. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
  132. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  133. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
  134. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  135. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  136. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  137. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  138. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  139. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  140. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  141. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
  142. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  143. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  144. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
  145. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  146. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  147. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  148. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
  149. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  150. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  151. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
  152. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  153. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  154. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  155. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
  156. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  157. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  158. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  159. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  160. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  161. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  162. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  163. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
  164. data/sig/rigor/scope.rbs +9 -1
  165. data/sig/rigor/type.rbs +36 -1
  166. metadata +19 -1
@@ -7,43 +7,30 @@ require_relative "rbs_extended/reporter"
7
7
  require_relative "rbs_extended/hkt_directives"
8
8
 
9
9
  module Rigor
10
- # Slice 7 phase 15 first-preview reader for the
11
- # `RBS::Extended` annotation surface described in
10
+ # Reader for the `RBS::Extended` annotation surface described in
12
11
  # `docs/type-specification/rbs-extended.md`.
13
12
  #
14
- # This module reads `%a{rigor:v1:<directive> <payload>}`
15
- # annotations off RBS method definitions and returns
16
- # well-typed effect objects the inference engine can
17
- # consume. v0.0.2 recognises:
13
+ # Reads `%a{rigor:v1:<directive> <payload>}` annotations off RBS
14
+ # method definitions and returns well-typed effect objects the
15
+ # inference engine can consume. Implemented directives:
18
16
  #
19
- # - `rigor:v1:predicate-if-true <target> is <ClassName>`
20
- # - `rigor:v1:predicate-if-false <target> is <ClassName>`
21
- # - `rigor:v1:assert <target> is <ClassName>`
22
- # - `rigor:v1:assert-if-true <target> is <ClassName>`
23
- # - `rigor:v1:assert-if-false <target> is <ClassName>`
17
+ # - `rigor:v1:predicate-if-true <target> is <ClassName|refinement>`
18
+ # - `rigor:v1:predicate-if-false <target> is <ClassName|refinement>`
19
+ # - `rigor:v1:assert <target> is <ClassName|refinement>`
20
+ # - `rigor:v1:assert-if-true <target> is <ClassName|refinement>`
21
+ # - `rigor:v1:assert-if-false <target> is <ClassName|refinement>`
22
+ # - `rigor:v1:param <name> <type-expr>` — per-call param narrowing
23
+ # - `rigor:v1:return <type-expr>` — per-call return override
24
+ # - `rigor:v1:conforms-to <InterfaceName>` — structural conformance
24
25
  #
25
- # `predicate-if-*` fires when the call is used as an
26
- # `if` / `unless` condition; `assert` fires unconditionally
27
- # at the call's post-scope; `assert-if-true` /
28
- # `assert-if-false` fire at the post-scope only when the
29
- # call's return value can be observed as truthy / falsey
30
- # (currently: when the call is the predicate of a
31
- # subsequent `if` / `unless`). Other directives in the spec
32
- # (`param`, `return`, `conforms-to`, negation `~T`,
33
- # `target: self` narrowing, ...) remain on the v0.0.x
34
- # roadmap. Annotations whose key is in the `rigor:v1:`
35
- # namespace but whose directive is unrecognised are
36
- # silently ignored at first-preview quality (a future slice
37
- # MAY surface them as diagnostics-on-Rigor-itself per the
38
- # spec's "unsupported metadata" guidance).
39
- #
40
- # The parser is minimal: it accepts a strict shape
41
- # `<target> is <ClassName>` where `<target>` is a Ruby
42
- # identifier (parameter name) or `self`, and `<ClassName>`
43
- # is a single non-namespaced class identifier or a
44
- # `::Foo::Bar` style constant path. Negative refinements
45
- # (`~T`), intersections, and unions are deferred to the
46
- # next iteration.
26
+ # `predicate-if-*` fires when the call is used as an `if` / `unless`
27
+ # condition; `assert` fires unconditionally at the call's post-scope;
28
+ # `assert-if-true` / `assert-if-false` fire at the post-scope only
29
+ # when the call's return value can be observed as truthy / falsey.
30
+ # Negation (`~T`) is supported for both class-name and refinement
31
+ # right-hand sides. Parameterised refinements (`non-empty-array[T]`)
32
+ # are also recognised. Annotations whose directive is unrecognised
33
+ # are silently ignored per the spec's "unsupported metadata" guidance.
47
34
  module RbsExtended # rubocop:disable Metrics/ModuleLength
48
35
  DIRECTIVE_PREFIX = "rigor:v1:"
49
36
 
@@ -58,9 +45,10 @@ module Rigor
58
45
  # a kebab-case refinement name (`non-empty-string`,
59
46
  # `lowercase-string`, …) instead of a Capitalised class
60
47
  # name. The narrowing tier substitutes the carrier for the
61
- # current local type; `class_name` is then nil and
62
- # `negative` is false (refinement-form directives do not
63
- # support `~T` negation in v0.0.4).
48
+ # current local type; `class_name` is then nil. `negative`
49
+ # may be true for refinement-form directives `~T` negation
50
+ # is supported; the narrowing tier computes the complement
51
+ # decomposition (see `AssertEffect` docs below).
64
52
  class PredicateEffect < Data.define(:edge, :target_kind, :target_name, :class_name, :negative, :refinement_type)
65
53
  def truthy_only? = edge == :truthy_only
66
54
  def falsey_only? = edge == :falsey_only
@@ -17,11 +17,8 @@ module Rigor
17
17
  # classes / modules, in-source constants, discovered method
18
18
  # nodes, class ivar / cvar declarations).
19
19
  #
20
- # This module is the **stable read shape** that v0.1.0's plugin
21
- # API will be designed against. ADR-2 (`docs/adr/2-extension-api.md`)
22
- # calls out a unified reflection layer as a prerequisite for the
23
- # extension protocols, and `docs/design/20260505-v0.1.0-readiness.md`
24
- # nominates this module as the highest-leverage cold-start slice.
20
+ # This module is the **stable read shape** the plugin API is
21
+ # designed against (ADR-2, `docs/adr/2-extension-api.md`).
25
22
  #
26
23
  # The facade is **read-only and additive**. Existing call sites
27
24
  # that read directly from `Rigor::Scope` or
@@ -52,8 +49,8 @@ module Rigor
52
49
  # defined in the analyzed sources?
53
50
  #
54
51
  # The provenance side of the API (which source family contributed
55
- # each fact) is explicitly out of scope for the v0.0.7 first
56
- # pass. v0.1.0's plugin API adds it as a separate concern.
52
+ # each fact) is explicitly out of scope for the v0.0.7 first pass;
53
+ # v0.1.0's plugin API added it as a separate concern.
57
54
  module Reflection
58
55
  module_function
59
56
 
@@ -28,7 +28,9 @@ module Rigor
28
28
  :discovered_superclasses,
29
29
  :discovered_includes,
30
30
  :discovered_class_sources,
31
- :data_member_layouts
31
+ :data_member_layouts,
32
+ :struct_member_layouts,
33
+ :param_inferred_types
32
34
  )
33
35
 
34
36
  class DiscoveryIndex
@@ -53,7 +55,17 @@ module Rigor
53
55
  discovered_superclasses: EMPTY_TABLE,
54
56
  discovered_includes: EMPTY_TABLE,
55
57
  discovered_class_sources: EMPTY_TABLE,
56
- data_member_layouts: EMPTY_TABLE
58
+ data_member_layouts: EMPTY_TABLE,
59
+ struct_member_layouts: EMPTY_TABLE,
60
+ # ADR-67 WD3 — the call-site parameter-inference table, keyed by
61
+ # `[class_name, method_name, kind]` (the same `(class, method, kind)`
62
+ # triple {Inference::ParameterInferenceCollector} records and that
63
+ # `build_method_entry_scope` reconstructs from the lexical class path).
64
+ # The value is a `{param_name(Symbol) => Rigor::Type}` map of the union
65
+ # of resolved call-site argument types. Empty on every normal run; only
66
+ # the `coverage --protection` collection pass populates it today, so a
67
+ # `check` run leaves it empty and seeds nothing (byte-identical).
68
+ param_inferred_types: EMPTY_TABLE
57
69
  )
58
70
  end
59
71
  end
data/lib/rigor/scope.rb CHANGED
@@ -23,7 +23,7 @@ module Rigor
23
23
  :ivars, :cvars, :globals,
24
24
  :indexed_narrowings, :method_chain_narrowings,
25
25
  :declaration_sourced,
26
- :source_path, :discovery
26
+ :source_path, :discovery, :struct_fold_safe_locals
27
27
 
28
28
  # ADR-53 Track A — the seed-time discovery tables live on the
29
29
  # {DiscoveryIndex} the scope carries by a single reference; the
@@ -51,6 +51,13 @@ module Rigor
51
51
  def discovered_includes = @discovery.discovered_includes
52
52
  def discovered_class_sources = @discovery.discovered_class_sources
53
53
  def data_member_layouts = @discovery.data_member_layouts
54
+ def struct_member_layouts = @discovery.struct_member_layouts
55
+ # ADR-67 WD3 — call-site-inferred parameter types, keyed by
56
+ # `[class_name, method_name, kind]`. `build_method_entry_scope` consults
57
+ # this to seed an undeclared `def` parameter with the union of its
58
+ # resolved call-site argument types (precision-additive; an RBS-declared
59
+ # parameter always wins). Empty unless a collection pass seeded it.
60
+ def param_inferred_types = @discovery.param_inferred_types
54
61
 
55
62
  # Narrowing key for an indexed read `receiver[key]` where both
56
63
  # the receiver and the key are stable enough to address. The
@@ -99,8 +106,14 @@ module Rigor
99
106
  # principle. Any flow-live touch (write / narrowing) drops the mark, so
100
107
  # the diagnostic keeps firing exactly as before on flow-observed nil.
101
108
  EMPTY_DECLARATION_SOURCED = Set.new.freeze
109
+ # ADR-48 Struct slice 3 — the per-body set of local names whose struct
110
+ # member reads are fold-safe (provably never mutated / aliased / escaped).
111
+ # A static per-scope context like {#source_path}: inherited unchanged
112
+ # through flow transitions and ignored by `==` / `hash`.
113
+ EMPTY_FOLD_SAFE = Set.new.freeze
102
114
  private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
103
- :EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED
115
+ :EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
116
+ :EMPTY_FOLD_SAFE
104
117
 
105
118
  class << self
106
119
  def empty(environment: Environment.default, source_path: nil)
@@ -120,7 +133,8 @@ module Rigor
120
133
  indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
121
134
  method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
122
135
  declaration_sourced: EMPTY_DECLARATION_SOURCED,
123
- source_path: nil
136
+ source_path: nil,
137
+ struct_fold_safe_locals: EMPTY_FOLD_SAFE
124
138
  )
125
139
  @environment = environment
126
140
  @locals = locals
@@ -134,6 +148,7 @@ module Rigor
134
148
  @method_chain_narrowings = method_chain_narrowings
135
149
  @declaration_sourced = declaration_sourced
136
150
  @source_path = source_path
151
+ @struct_fold_safe_locals = struct_fold_safe_locals
137
152
  freeze
138
153
  end
139
154
 
@@ -180,17 +195,29 @@ module Rigor
180
195
  rebuild(self_type: type)
181
196
  end
182
197
 
183
- # ADR-11 per-call-site assertion gating prerequisite. The
184
- # analyzer's per-file boundary stamps the current source
185
- # file's path onto the seed scope; nested rebuilds carry
186
- # the value through so plugin rules (`dynamic_return`'s
187
- # `file_methods:` gate, sigil checks) can resolve "which
188
- # file does this call site belong to?" without
198
+ # ADR-28 / ADR-52 slice 5a — per-file source path carried on
199
+ # the scope. The analyzer stamps the current file's path onto
200
+ # the seed scope; nested rebuilds propagate it so plugin rules
201
+ # (`dynamic_return`'s `file_methods:` gate, sigil checks) can
202
+ # resolve "which file does this call site belong to?" without
189
203
  # thread-locals.
190
204
  def with_source_path(path)
191
205
  rebuild(source_path: path)
192
206
  end
193
207
 
208
+ # ADR-48 Struct slice 3 — installs the per-body fold-safe-local set
209
+ # ({Inference::StructFoldSafety}). Set once at body entry; inherited
210
+ # unchanged through subsequent flow transitions.
211
+ def with_struct_fold_safe(locals)
212
+ rebuild(struct_fold_safe_locals: locals)
213
+ end
214
+
215
+ # True when `name`'s `Struct` member reads are fold-safe in this body
216
+ # (the local is provably never mutated / aliased / escaped).
217
+ def struct_fold_safe?(name)
218
+ @struct_fold_safe_locals.include?(name.to_sym)
219
+ end
220
+
194
221
  # ADR-53 Track A — swaps the whole discovery index in one transition.
195
222
  # The sole seeding path; the per-table writers it replaced are derived
196
223
  # off-`Scope` through `scope.discovery.with(table_name: table)`.
@@ -484,6 +511,20 @@ module Rigor
484
511
  layout
485
512
  end
486
513
 
514
+ # ADR-48 Struct follow-up — the `{ members:, keyword_init: }` layout
515
+ # recorded for a `Struct.new(...)`-defined class, in the constant form
516
+ # (`Point = Struct.new(:x, :y)`) and the named-subclass form
517
+ # (`class Point < Struct.new(:x, :y)`). Consumed by
518
+ # {Inference::MethodDispatcher::StructFolding} so `Point.new(...)` on a
519
+ # `Singleton[Point]` receiver materialises a member instance. Returns nil
520
+ # when the class has no recorded struct layout. Mirrors
521
+ # {#data_member_layout}'s dependency-recording contract.
522
+ def struct_member_layout(class_name)
523
+ layout = @discovery.struct_member_layouts[class_name.to_s]
524
+ record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active?
525
+ layout
526
+ end
527
+
487
528
  # ADR-24 slice 2 — per-class/module table mapping a fully
488
529
  # qualified user class or module to the list of module
489
530
  # names it `include`s / `prepend`s, AS WRITTEN at the
@@ -674,7 +715,8 @@ module Rigor
674
715
  indexed_narrowings: @indexed_narrowings,
675
716
  method_chain_narrowings: @method_chain_narrowings,
676
717
  declaration_sourced: @declaration_sourced,
677
- source_path: @source_path
718
+ source_path: @source_path,
719
+ struct_fold_safe_locals: @struct_fold_safe_locals
678
720
  )
679
721
  self.class.new(
680
722
  environment: environment, locals: locals,
@@ -684,7 +726,8 @@ module Rigor
684
726
  indexed_narrowings: indexed_narrowings,
685
727
  method_chain_narrowings: method_chain_narrowings,
686
728
  declaration_sourced: declaration_sourced,
687
- source_path: source_path
729
+ source_path: source_path,
730
+ struct_fold_safe_locals: struct_fold_safe_locals
688
731
  )
689
732
  end
690
733
 
@@ -5,9 +5,9 @@ module Rigor
5
5
  # Per-call-site argument observation produced by
6
6
  # {ObservationCollector}. ADR-14 follow-up: the earlier
7
7
  # MVP shape (`Array[Type]` of positional types only)
8
- # could not represent keyword arguments — every call like
9
- # `MethodCatalog.new(path: ..., mutating_selectors: ...)`
10
- # discarded the whole observation via `non_positional?`.
8
+ # could not represent keyword arguments — keyword calls
9
+ # like `MethodCatalog.new(path: ..., mutating_selectors: ...)`
10
+ # were silently skipped in that shape.
11
11
  # The new shape carries positional and keyword arg types
12
12
  # in parallel so the per-position / per-keyword unions
13
13
  # can each be reconstructed independently.
@@ -46,6 +46,11 @@ module Rigor
46
46
  def initialize(path_mapper:, overwrite: false)
47
47
  @path_mapper = path_mapper
48
48
  @overwrite = overwrite
49
+ # Run-level (cross-file) namespace-kind view, populated
50
+ # per `#write_all` from every candidate's per-file map.
51
+ # Empty until then so the single-target `#write` path
52
+ # falls back to per-candidate kinds only.
53
+ @global_namespace_kinds = {}
49
54
  end
50
55
 
51
56
  # Process the full candidate list by resolving each
@@ -65,6 +70,7 @@ module Rigor
65
70
  emittable = candidates.select { |c| EMITTABLE.include?(c.classification) }
66
71
  return [] if emittable.empty?
67
72
 
73
+ @global_namespace_kinds = build_namespace_kinds(candidates)
68
74
  emittable.group_by { |c| @path_mapper.target_for(c.path, class_name: c.class_name) }
69
75
  .map { |target, group| write_target(target, group) }
70
76
  end
@@ -161,13 +167,45 @@ module Rigor
161
167
  end
162
168
 
163
169
  def merged_namespace_kinds(candidates)
164
- merged = {}
170
+ merged = @global_namespace_kinds.dup
165
171
  candidates.each do |c|
166
- (c.namespace_kinds || {}).each { |k, v| merged[k] = v }
172
+ (c.namespace_kinds || {}).each { |k, v| apply_namespace_kind(merged, k, v) }
167
173
  end
168
174
  merged
169
175
  end
170
176
 
177
+ # Folds every candidate's per-file namespace-kind map
178
+ # into one run-level view so a `class Foo` recorded
179
+ # while scanning `foo.rb` governs the wrapper keyword
180
+ # emitted for `Foo` in a *sibling* file's target — e.g.
181
+ # `foo/bar.rb` declaring `class Foo::Bar`, whose compact
182
+ # constant path never names `Foo`, so the walker records
183
+ # no kind for it. Without this view that sibling target
184
+ # wraps the nested class in `module Foo` while `foo.rbs`
185
+ # declares `class Foo`; loading both raises
186
+ # `RBS::DuplicatedDeclarationError`, aborting the whole
187
+ # RBS env build.
188
+ def build_namespace_kinds(candidates)
189
+ candidates.each_with_object({}) do |candidate, acc|
190
+ (candidate.namespace_kinds || {}).each { |name, kind| apply_namespace_kind(acc, name, kind) }
191
+ end
192
+ end
193
+
194
+ # A `class` declaration is authoritative and MUST win
195
+ # over the `:module` wrapper default: a compact
196
+ # `class Foo::Bar` never names `Foo`, so the only signal
197
+ # for `Foo`'s kind is an actual `class Foo` (or
198
+ # `Const = Data.define(...)` shell) seen elsewhere. This
199
+ # guarantees the generated tree never mixes `class` /
200
+ # `module` for the same constant.
201
+ def apply_namespace_kind(map, key, kind)
202
+ if kind == :class
203
+ map[key] = :class
204
+ else
205
+ map[key] ||= :module
206
+ end
207
+ end
208
+
171
209
  # Tree node: { name:, children: Hash{String => node},
172
210
  # methods: Array<MethodCandidate>, shell: Boolean }.
173
211
  # `shell` flags nodes that came in via `class_shells`
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Source
5
+ # Flattens a Prism constant-reference node (`ConstantReadNode` /
6
+ # `ConstantPathNode`) to its source-qualified `"A::B::C"` string.
7
+ #
8
+ # Two nil policies for the one edge case that distinguishes the call
9
+ # sites — a constant path rooted in a *dynamic* base (`expr::Bar`, where
10
+ # the left side is a runtime expression rather than a constant):
11
+ #
12
+ # * {.qualified_name} / {.render} are LENIENT — they drop the dynamic
13
+ # segment and render the trailing constant names (`expr::Bar` => "Bar").
14
+ # The scope indexer and statement evaluator feed only genuine
15
+ # class/module path nodes and want a best-effort name.
16
+ # * {.qualified_name_or_nil} is STRICT — a dynamic base anywhere in the
17
+ # chain yields `nil`, so a caller that statically names constants can
18
+ # treat the path as opaque rather than guessing.
19
+ #
20
+ # A leading `::` (absolute root, `::Foo`) renders as `"Foo"` under both
21
+ # policies. A node that is neither a `ConstantReadNode` nor a
22
+ # `ConstantPathNode` yields `nil` under both.
23
+ module ConstantPath
24
+ module_function
25
+
26
+ # Lenient dispatch over a constant-reference node.
27
+ def qualified_name(node)
28
+ case node
29
+ when Prism::ConstantReadNode then node.name.to_s
30
+ when Prism::ConstantPathNode then render(node)
31
+ end
32
+ end
33
+
34
+ # Lenient render of a `ConstantPathNode`; never nil for a path node.
35
+ def render(node)
36
+ prefix =
37
+ case node.parent
38
+ when Prism::ConstantReadNode then "#{node.parent.name}::"
39
+ when Prism::ConstantPathNode then "#{render(node.parent)}::"
40
+ else ""
41
+ end
42
+ "#{prefix}#{node.name}"
43
+ end
44
+
45
+ # Strict dispatch: a dynamic base anywhere in the path yields nil.
46
+ def qualified_name_or_nil(node)
47
+ case node
48
+ when Prism::ConstantReadNode
49
+ node.name.to_s
50
+ when Prism::ConstantPathNode
51
+ parent = node.parent
52
+ return node.name.to_s if parent.nil?
53
+
54
+ parent_name = qualified_name_or_nil(parent)
55
+ return nil if parent_name.nil?
56
+
57
+ "#{parent_name}::#{node.name}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/rigor/source.rb CHANGED
@@ -14,3 +14,4 @@ end
14
14
  require_relative "source/node_locator"
15
15
  require_relative "source/node_walker"
16
16
  require_relative "source/literals"
17
+ require_relative "source/constant_path"
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -46,17 +47,7 @@ module Rigor
46
47
  "Method"
47
48
  end
48
49
 
49
- def top
50
- Trinary.no
51
- end
52
-
53
- def bot
54
- Trinary.no
55
- end
56
-
57
- def dynamic
58
- Trinary.no
59
- end
50
+ include Rigor::Type::PlainLattice
60
51
 
61
52
  include Rigor::Type::AcceptanceRouter
62
53
 
@@ -405,6 +405,21 @@ module Rigor
405
405
  DataInstance.new(members, class_name)
406
406
  end
407
407
 
408
+ # ADR-48 Struct follow-up — the class object produced by
409
+ # `Struct.new(:x, :y)`. `members` is the ordered Symbol member-name
410
+ # list; `keyword_init` records the `keyword_init:` flag; `class_name`
411
+ # tags the class when known (the named-subclass form).
412
+ def struct_class_of(members:, class_name: nil, keyword_init: false)
413
+ StructClass.new(members, class_name, keyword_init: keyword_init)
414
+ end
415
+
416
+ # ADR-48 Struct follow-up — a `Struct.new` value instance. `members`
417
+ # is the ordered member-name -> value-type map; `class_name` tags the
418
+ # instance's class when known.
419
+ def struct_instance_of(members:, class_name: nil)
420
+ StructInstance.new(members, class_name)
421
+ end
422
+
408
423
  # Normalized union. Flattens nested Unions, deduplicates structurally
409
424
  # equal members, drops Bot, and collapses 0/1-member results.
410
425
  def union(*types)
@@ -929,9 +944,7 @@ module Rigor
929
944
  end
930
945
  end
931
946
 
932
- # ADR-15 Phase 4b.x eager-allocate the singleton
933
- # `Dynamic[Top]` carrier on the main Ractor at load time.
934
- # The `untyped` reader above just returns this ivar.
947
+ # Eager-allocated at load time; see `untyped` method comment above.
935
948
  @untyped = Dynamic.new(Top.instance)
936
949
  end
937
950
  end
@@ -3,6 +3,7 @@
3
3
  require "date"
4
4
  require_relative "../trinary"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -100,17 +101,7 @@ module Rigor
100
101
  end
101
102
  end
102
103
 
103
- def top
104
- Trinary.no
105
- end
106
-
107
- def bot
108
- Trinary.no
109
- end
110
-
111
- def dynamic
112
- Trinary.no
113
- end
104
+ include Rigor::Type::PlainLattice
114
105
 
115
106
  include Rigor::Type::AcceptanceRouter
116
107
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -54,17 +55,7 @@ module Rigor
54
55
  "singleton(#{class_name || 'Data'})"
55
56
  end
56
57
 
57
- def top
58
- Trinary.no
59
- end
60
-
61
- def bot
62
- Trinary.no
63
- end
64
-
65
- def dynamic
66
- Trinary.no
67
- end
58
+ include Rigor::Type::PlainLattice
68
59
 
69
60
  include Rigor::Type::AcceptanceRouter
70
61
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -74,17 +75,7 @@ module Rigor
74
75
  name
75
76
  end
76
77
 
77
- def top
78
- Trinary.no
79
- end
80
-
81
- def bot
82
- Trinary.no
83
- end
84
-
85
- def dynamic
86
- Trinary.no
87
- end
78
+ include Rigor::Type::PlainLattice
88
79
 
89
80
  include Rigor::Type::AcceptanceRouter
90
81
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -96,17 +97,7 @@ module Rigor
96
97
  read_only_keys.include?(key)
97
98
  end
98
99
 
99
- def top
100
- Trinary.no
101
- end
102
-
103
- def bot
104
- Trinary.no
105
- end
106
-
107
- def dynamic
108
- Trinary.no
109
- end
100
+ include Rigor::Type::PlainLattice
110
101
 
111
102
  include Rigor::Type::AcceptanceRouter
112
103
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -103,17 +104,7 @@ module Rigor
103
104
  "Integer"
104
105
  end
105
106
 
106
- def top
107
- Trinary.no
108
- end
109
-
110
- def bot
111
- Trinary.no
112
- end
113
-
114
- def dynamic
115
- Trinary.no
116
- end
107
+ include Rigor::Type::PlainLattice
117
108
 
118
109
  include Rigor::Type::AcceptanceRouter
119
110
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -62,17 +63,7 @@ module Rigor
62
63
  members.first.erase_to_rbs
63
64
  end
64
65
 
65
- def top
66
- Trinary.no
67
- end
68
-
69
- def bot
70
- Trinary.no
71
- end
72
-
73
- def dynamic
74
- Trinary.no
75
- end
66
+ include Rigor::Type::PlainLattice
76
67
 
77
68
  include Rigor::Type::AcceptanceRouter
78
69
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -52,17 +53,7 @@ module Rigor
52
53
  "#{class_name}[#{rendered}]"
53
54
  end
54
55
 
55
- def top
56
- Trinary.no
57
- end
58
-
59
- def bot
60
- Trinary.no
61
- end
62
-
63
- def dynamic
64
- Trinary.no
65
- end
56
+ include Rigor::Type::PlainLattice
66
57
 
67
58
  include Rigor::Type::AcceptanceRouter
68
59