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
@@ -4,9 +4,11 @@ require "prism"
4
4
 
5
5
  require_relative "../scope"
6
6
  require_relative "../type"
7
+ require_relative "../source/constant_path"
7
8
  require_relative "mutation_widening"
8
9
  require_relative "narrowing"
9
10
  require_relative "statement_evaluator"
11
+ require_relative "struct_fold_safety"
10
12
 
11
13
  module Rigor
12
14
  module Inference
@@ -114,12 +116,8 @@ module Rigor
114
116
  # recognised `define_method` calls and records the
115
117
  # introduced method names. `rigor check` consults the
116
118
  # table to suppress false positives for methods the
117
- # user has defined but no RBS sig describes.
118
- # Merged UNDER any cross-file pre-pass seed (like the def-node
119
- # / include tables below) so a method `def`/`attr_reader`-
120
- # declared in one file suppresses a false `undefined-method`
121
- # for a call in another — `rigor check` seeds the project-wide
122
- # table via `Runner#seed_project_scope`.
119
+ # user has defined but no RBS sig describes. Merged
120
+ # UNDER the cross-file pre-pass seed; details: merge_project_method_indexes.
123
121
  discovered_methods = deep_merge_class_methods(
124
122
  default_scope.discovered_methods, build_discovered_methods(root)
125
123
  )
@@ -150,6 +148,10 @@ module Rigor
150
148
  # `table[node]` to type predicates; the second pass's
151
149
  # entry is the one that reflects all flow-derived
152
150
  # rebinds, so it MUST overwrite the first.
151
+ # ADR-48 Struct slice 3 — install the top-level fold-safe-local set so
152
+ # a member read off a mutation-free top-level struct binding folds.
153
+ seeded_scope = seed_struct_fold_safe(seeded_scope, root)
154
+
153
155
  on_enter = ->(node, scope) { table[node] = scope }
154
156
  StatementEvaluator.new(scope: seeded_scope, on_enter: on_enter,
155
157
  converged_loop_recording: converged_loop_recording).evaluate(root)
@@ -158,6 +160,17 @@ module Rigor
158
160
  table
159
161
  end
160
162
 
163
+ # ADR-48 Struct slice 3 — installs the top-level fold-safe-local set
164
+ # ({Inference::StructFoldSafety}). Struct member layouts of constant
165
+ # receivers are resolved through the side-table the seeded scope carries.
166
+ def seed_struct_fold_safe(seeded_scope, root)
167
+ seeded_scope.with_struct_fold_safe(
168
+ StructFoldSafety.fold_safe_locals(
169
+ root, ->(name) { seeded_scope.struct_member_layout(name)&.[](:members) }
170
+ )
171
+ )
172
+ end
173
+
161
174
  # v0.0.2 #5 + ADR-24 slice 2 — seeds the three
162
175
  # project-method indexes onto `seeded_scope`: the
163
176
  # per-instance-method def-node table, the class ->
@@ -185,11 +198,9 @@ module Rigor
185
198
  method_visibilities = default_scope.discovered_method_visibilities.merge(
186
199
  build_discovered_method_visibilities(root)
187
200
  ) { |_class, cross_file, per_file| cross_file.merge(per_file) }
188
- # ADR-48 — per-file Data member layouts merged OVER the cross-file
189
- # seed (same-file declaration is authoritative for its own classes).
190
- data_member_layouts = default_scope.data_member_layouts.merge(
191
- build_data_member_layouts(root)
192
- )
201
+ # ADR-48 — per-file Data + Struct member layouts merged OVER the
202
+ # cross-file seed (same-file declaration is authoritative).
203
+ data_member_layouts, struct_member_layouts = merge_member_layouts(default_scope, root)
193
204
 
194
205
  seeded_scope.with_discovery(
195
206
  seeded_scope.discovery.with(
@@ -198,11 +209,23 @@ module Rigor
198
209
  discovered_superclasses: superclasses,
199
210
  discovered_includes: includes,
200
211
  discovered_method_visibilities: method_visibilities,
201
- data_member_layouts: data_member_layouts
212
+ data_member_layouts: data_member_layouts,
213
+ struct_member_layouts: struct_member_layouts
202
214
  )
203
215
  )
204
216
  end
205
217
 
218
+ # ADR-48 — the per-file Data + Struct member-layout tables, each merged
219
+ # OVER the cross-file seed so a same-file declaration wins for its own
220
+ # classes. Returned as a pair to keep {#merge_project_method_indexes}
221
+ # under the method-size budget.
222
+ def merge_member_layouts(default_scope, root)
223
+ [
224
+ default_scope.data_member_layouts.merge(build_data_member_layouts(root)),
225
+ default_scope.struct_member_layouts.merge(build_struct_member_layouts(root))
226
+ ]
227
+ end
228
+
206
229
  # Slice 7 phase 2. Builds the class-level ivar accumulator
207
230
  # by walking every `Prism::ClassNode` / `Prism::ModuleNode`
208
231
  # body, descending into each nested `Prism::DefNode`, and
@@ -357,7 +380,7 @@ module Rigor
357
380
 
358
381
  case node
359
382
  when Prism::ClassNode, Prism::ModuleNode
360
- name = qualified_name_for(node.constant_path)
383
+ name = Source::ConstantPath.qualified_name(node.constant_path)
361
384
  if name
362
385
  child_prefix = qualified_prefix + [name]
363
386
  if node.body
@@ -841,7 +864,7 @@ module Rigor
841
864
 
842
865
  case root
843
866
  when Prism::ClassNode, Prism::ModuleNode
844
- name = qualified_name_for(root.constant_path)
867
+ name = Source::ConstantPath.qualified_name(root.constant_path)
845
868
  if name && root.body
846
869
  child = prefix + [name]
847
870
  collect_class_method_defs(root.body, child, acc)
@@ -1251,7 +1274,7 @@ module Rigor
1251
1274
 
1252
1275
  case node
1253
1276
  when Prism::ClassNode, Prism::ModuleNode
1254
- name = qualified_name_for(node.constant_path)
1277
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1255
1278
  if name
1256
1279
  child_prefix = qualified_prefix + [name]
1257
1280
  walk_class_cvars(node.body, child_prefix, default_scope, accumulator) if node.body
@@ -1337,7 +1360,7 @@ module Rigor
1337
1360
 
1338
1361
  case node
1339
1362
  when Prism::ClassNode, Prism::ModuleNode
1340
- name = qualified_name_for(node.constant_path)
1363
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1341
1364
  if name
1342
1365
  child_prefix = qualified_prefix + [name]
1343
1366
  walk_constant_writes(node.body, child_prefix, default_scope, accumulator) if node.body
@@ -1347,7 +1370,7 @@ module Rigor
1347
1370
  record_constant_write(node, qualified_prefix, default_scope, accumulator, node.name.to_s)
1348
1371
  return
1349
1372
  when Prism::ConstantPathWriteNode
1350
- full = qualified_name_for(node.target)
1373
+ full = Source::ConstantPath.qualified_name(node.target)
1351
1374
  record_constant_write(node, [], default_scope, accumulator, full) if full
1352
1375
  return
1353
1376
  end
@@ -1413,7 +1436,7 @@ module Rigor
1413
1436
 
1414
1437
  case node
1415
1438
  when Prism::ClassNode, Prism::ModuleNode
1416
- name = qualified_name_for(node.constant_path)
1439
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1417
1440
  if name
1418
1441
  child_prefix = qualified_prefix + [name]
1419
1442
  record_meta_superclass_members(node, child_prefix, accumulator) if node.is_a?(Prism::ClassNode)
@@ -1466,7 +1489,7 @@ module Rigor
1466
1489
  when Prism::SelfNode
1467
1490
  qualified_prefix
1468
1491
  when Prism::ConstantReadNode, Prism::ConstantPathNode
1469
- rendered = qualified_name_for(node.expression)
1492
+ rendered = Source::ConstantPath.qualified_name(node.expression)
1470
1493
  return nil unless rendered
1471
1494
 
1472
1495
  if !qualified_prefix.empty? && qualified_prefix.last == rendered
@@ -1573,7 +1596,7 @@ module Rigor
1573
1596
  when Prism::ConstantReadNode
1574
1597
  receiver.name.to_s == qualified_prefix.last
1575
1598
  when Prism::ConstantPathNode
1576
- rendered = render_constant_path(receiver)
1599
+ rendered = Source::ConstantPath.render(receiver)
1577
1600
  return false unless rendered
1578
1601
 
1579
1602
  path = rendered.split("::")
@@ -1605,7 +1628,7 @@ module Rigor
1605
1628
 
1606
1629
  case node
1607
1630
  when Prism::ClassNode, Prism::ModuleNode
1608
- name = qualified_name_for(node.constant_path)
1631
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1609
1632
  if name
1610
1633
  child_prefix = qualified_prefix + [name]
1611
1634
  walk_def_nodes(node.body, child_prefix, false, accumulator) if node.body
@@ -1681,7 +1704,7 @@ module Rigor
1681
1704
 
1682
1705
  case node
1683
1706
  when Prism::ClassNode, Prism::ModuleNode
1684
- name = qualified_name_for(node.constant_path)
1707
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1685
1708
  if name
1686
1709
  walk_singleton_body(node.body, qualified_prefix + [name], false, accumulator) if node.body
1687
1710
  return
@@ -1810,16 +1833,16 @@ module Rigor
1810
1833
 
1811
1834
  case node
1812
1835
  when Prism::ClassNode
1813
- name = qualified_name_for(node.constant_path)
1836
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1814
1837
  if name
1815
1838
  full = (qualified_prefix + [name]).join("::")
1816
- superclass = node.superclass && qualified_name_for(node.superclass)
1839
+ superclass = node.superclass && Source::ConstantPath.qualified_name(node.superclass)
1817
1840
  accumulator[full] = superclass if superclass
1818
1841
  walk_class_superclasses(node.body, qualified_prefix + [name], accumulator) if node.body
1819
1842
  return
1820
1843
  end
1821
1844
  when Prism::ModuleNode
1822
- name = qualified_name_for(node.constant_path)
1845
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1823
1846
  if name
1824
1847
  walk_class_superclasses(node.body, qualified_prefix + [name], accumulator) if node.body
1825
1848
  return
@@ -1851,14 +1874,14 @@ module Rigor
1851
1874
 
1852
1875
  case node
1853
1876
  when Prism::ClassNode
1854
- name = qualified_name_for(node.constant_path)
1877
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1855
1878
  if name
1856
1879
  record_data_member_layout(accumulator, qualified_prefix + [name], node.superclass)
1857
1880
  walk_data_member_layouts(node.body, qualified_prefix + [name], accumulator) if node.body
1858
1881
  return
1859
1882
  end
1860
1883
  when Prism::ModuleNode
1861
- name = qualified_name_for(node.constant_path)
1884
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1862
1885
  if name
1863
1886
  walk_data_member_layouts(node.body, qualified_prefix + [name], accumulator) if node.body
1864
1887
  return
@@ -1883,6 +1906,74 @@ module Rigor
1883
1906
  accumulator[qualified_parts.join("::")] = members.freeze
1884
1907
  end
1885
1908
 
1909
+ # ADR-48 Struct follow-up — the `Struct.new(...)` sibling of
1910
+ # {#build_data_member_layouts}. A separate, additive table so the
1911
+ # existing `Data.define` value-shape contract (a bare `[Symbol]`) is
1912
+ # untouched: a Struct entry carries `{ members:, keyword_init: }`
1913
+ # because the dispatcher needs the flag to fold the matching `.new`
1914
+ # call form (positional vs keyword) without manufacturing a wrong map.
1915
+ def build_struct_member_layouts(root)
1916
+ accumulator = {}
1917
+ walk_struct_member_layouts(root, [], accumulator)
1918
+ accumulator.freeze
1919
+ end
1920
+
1921
+ def walk_struct_member_layouts(node, qualified_prefix, accumulator)
1922
+ return unless node.is_a?(Prism::Node)
1923
+
1924
+ case node
1925
+ when Prism::ClassNode
1926
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1927
+ if name
1928
+ record_struct_member_layout(accumulator, qualified_prefix + [name], node.superclass)
1929
+ walk_struct_member_layouts(node.body, qualified_prefix + [name], accumulator) if node.body
1930
+ return
1931
+ end
1932
+ when Prism::ModuleNode
1933
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1934
+ if name
1935
+ walk_struct_member_layouts(node.body, qualified_prefix + [name], accumulator) if node.body
1936
+ return
1937
+ end
1938
+ when Prism::ConstantWriteNode
1939
+ record_struct_member_layout(accumulator, qualified_prefix + [node.name.to_s], node.value)
1940
+ end
1941
+
1942
+ node.compact_child_nodes.each do |child|
1943
+ walk_struct_member_layouts(child, qualified_prefix, accumulator)
1944
+ end
1945
+ end
1946
+
1947
+ # Records `qualified -> { members:, keyword_init: }` when `expr` is a
1948
+ # `Struct.new(*Symbol [, keyword_init: <bool>])` call with at least one
1949
+ # literal-Symbol member.
1950
+ def record_struct_member_layout(accumulator, qualified_parts, expr)
1951
+ return unless struct_new_call?(expr)
1952
+
1953
+ members = meta_member_names(expr)
1954
+ return if members.empty?
1955
+
1956
+ accumulator[qualified_parts.join("::")] = {
1957
+ members: members.freeze,
1958
+ keyword_init: struct_new_keyword_init?(expr)
1959
+ }.freeze
1960
+ end
1961
+
1962
+ # True when a `Struct.new` call carries `keyword_init: true` as a
1963
+ # literal in its trailing keyword hash. A non-literal value (or its
1964
+ # absence) reads as `false` — the conservative positional default.
1965
+ def struct_new_keyword_init?(call_node)
1966
+ args = call_node.arguments&.arguments || []
1967
+ last = args.last
1968
+ return false unless last.is_a?(Prism::KeywordHashNode)
1969
+
1970
+ last.elements.any? do |assoc|
1971
+ assoc.is_a?(Prism::AssocNode) &&
1972
+ assoc.key.is_a?(Prism::SymbolNode) && assoc.key.unescaped == "keyword_init" &&
1973
+ assoc.value.is_a?(Prism::TrueNode)
1974
+ end
1975
+ end
1976
+
1886
1977
  MIXIN_CALL_NAMES = %i[include prepend].freeze
1887
1978
 
1888
1979
  # ADR-24 slice 2 — per-class/module table mapping a fully
@@ -1906,7 +1997,7 @@ module Rigor
1906
1997
 
1907
1998
  case node
1908
1999
  when Prism::ClassNode, Prism::ModuleNode
1909
- name = qualified_name_for(node.constant_path)
2000
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1910
2001
  if name
1911
2002
  full = (qualified_prefix + [name]).join("::")
1912
2003
  walk_class_includes(node.body, qualified_prefix + [name], full, accumulator) if node.body
@@ -1926,7 +2017,7 @@ module Rigor
1926
2017
  return unless MIXIN_CALL_NAMES.include?(node.name)
1927
2018
 
1928
2019
  node.arguments&.arguments&.each do |arg|
1929
- mod = qualified_name_for(arg)
2020
+ mod = Source::ConstantPath.qualified_name(arg)
1930
2021
  (accumulator[current_class] ||= []) << mod if mod
1931
2022
  end
1932
2023
  end
@@ -1966,7 +2057,7 @@ module Rigor
1966
2057
 
1967
2058
  case node
1968
2059
  when Prism::ClassNode, Prism::ModuleNode
1969
- name = qualified_name_for(node.constant_path)
2060
+ name = Source::ConstantPath.qualified_name(node.constant_path)
1970
2061
  if name
1971
2062
  child_prefix = qualified_prefix + [name]
1972
2063
  walk_method_visibilities(node.body, child_prefix, false, :public, accumulator) if node.body
@@ -2104,7 +2195,7 @@ module Rigor
2104
2195
 
2105
2196
  case node
2106
2197
  when Prism::ClassNode, Prism::ModuleNode
2107
- name = qualified_name_for(node.constant_path)
2198
+ name = Source::ConstantPath.qualified_name(node.constant_path)
2108
2199
  if name
2109
2200
  collect_class_alias_map(node.body, qualified_prefix + [name], accumulator) if node.body
2110
2201
  return accumulator
@@ -2252,7 +2343,8 @@ module Rigor
2252
2343
  # `{ def_nodes:, def_sources:, superclasses:, includes:, class_sources: }`
2253
2344
  def discovered_def_index_for_paths(paths, buffer: nil)
2254
2345
  acc = { def_nodes: {}, singleton_def_nodes: {}, def_sources: {}, superclasses: {}, includes: {},
2255
- method_visibilities: {}, methods: {}, class_sources: {}, data_member_layouts: {} }
2346
+ method_visibilities: {}, methods: {}, class_sources: {}, data_member_layouts: {},
2347
+ struct_member_layouts: {} }
2256
2348
  paths.each do |path|
2257
2349
  physical = buffer ? buffer.resolve(path) : path
2258
2350
  root = Prism.parse(File.read(physical), filepath: path).value
@@ -2306,6 +2398,7 @@ module Rigor
2306
2398
  record_class_sources(acc[:class_sources], path, root, superclasses, includes)
2307
2399
  merge_class_keyed_index_tables(acc, root)
2308
2400
  acc[:data_member_layouts].merge!(build_data_member_layouts(root))
2401
+ acc[:struct_member_layouts].merge!(build_struct_member_layouts(root))
2309
2402
  end
2310
2403
 
2311
2404
  # Folds the per-class method-visibility and method-existence tables of
@@ -2364,14 +2457,14 @@ module Rigor
2364
2457
 
2365
2458
  case node
2366
2459
  when Prism::ClassNode
2367
- name = qualified_name_for(node.constant_path)
2460
+ name = Source::ConstantPath.qualified_name(node.constant_path)
2368
2461
  if name
2369
2462
  full = (qualified_prefix + [name]).join("::")
2370
2463
  accumulator[full] = Type::Combinator.singleton_of(full)
2371
2464
  return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if node.body
2372
2465
  end
2373
2466
  when Prism::ModuleNode
2374
- name = qualified_name_for(node.constant_path)
2467
+ name = Source::ConstantPath.qualified_name(node.constant_path)
2375
2468
  return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if name && node.body
2376
2469
  when Prism::ConstantWriteNode
2377
2470
  record_class_new_constant_decl(node, qualified_prefix, accumulator)
@@ -2417,7 +2510,7 @@ module Rigor
2417
2510
  arg = call_node.arguments&.arguments&.first
2418
2511
  return nil if arg.nil?
2419
2512
 
2420
- raw = qualified_name_for(arg)
2513
+ raw = Source::ConstantPath.qualified_name(arg)
2421
2514
  return nil if raw.nil?
2422
2515
 
2423
2516
  prefix = qualified_prefix.dup
@@ -2462,7 +2555,7 @@ module Rigor
2462
2555
  end
2463
2556
 
2464
2557
  def record_class_or_module?(node, qualified_prefix, identity_table, discovered)
2465
- name = qualified_name_for(node.constant_path)
2558
+ name = Source::ConstantPath.qualified_name(node.constant_path)
2466
2559
  return false unless name
2467
2560
 
2468
2561
  full = (qualified_prefix + [name]).join("::")
@@ -2574,25 +2667,6 @@ module Rigor
2574
2667
  end
2575
2668
  end
2576
2669
 
2577
- def qualified_name_for(constant_path_node)
2578
- case constant_path_node
2579
- when Prism::ConstantReadNode
2580
- constant_path_node.name.to_s
2581
- when Prism::ConstantPathNode
2582
- render_constant_path(constant_path_node)
2583
- end
2584
- end
2585
-
2586
- def render_constant_path(node)
2587
- prefix =
2588
- case node.parent
2589
- when Prism::ConstantReadNode then "#{node.parent.name}::"
2590
- when Prism::ConstantPathNode then "#{render_constant_path(node.parent)}::"
2591
- else ""
2592
- end
2593
- "#{prefix}#{node.name}"
2594
- end
2595
-
2596
2670
  # Walks `node`'s subtree DFS and fills in scope entries for every
2597
2671
  # Prism node the StatementEvaluator did not visit (i.e. expression-
2598
2672
  # interior nodes like the receiver/args of a CallNode). Those