rigortype 0.3.8 → 0.3.9

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 (447) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/gem_overlay/activesupport/core_ext.rbs +109 -13
  5. data/docs/handbook/02-everyday-types.md +17 -14
  6. data/docs/handbook/03-narrowing.md +37 -4
  7. data/docs/handbook/05-methods-and-blocks.md +1 -1
  8. data/docs/handbook/06-classes.md +2 -2
  9. data/docs/handbook/07-rbs-and-extended.md +14 -1
  10. data/docs/handbook/11-sig-gen.md +72 -1
  11. data/docs/handbook/README.md +1 -1
  12. data/docs/handbook/appendix-elixir.md +2 -2
  13. data/docs/handbook/appendix-go.md +2 -2
  14. data/docs/handbook/appendix-java-csharp.md +2 -2
  15. data/docs/handbook/appendix-mypy.md +2 -2
  16. data/docs/handbook/appendix-phpstan.md +1 -1
  17. data/docs/handbook/appendix-rust.md +1 -1
  18. data/docs/handbook/appendix-type-theory.md +4 -4
  19. data/docs/handbook/appendix-typescript.md +1 -1
  20. data/docs/llms.txt +2 -0
  21. data/docs/manual/02-cli-reference.md +32 -4
  22. data/docs/manual/03-configuration.md +11 -0
  23. data/docs/manual/04-diagnostics.md +11 -0
  24. data/docs/manual/07-plugins.md +15 -5
  25. data/docs/manual/08-skills.md +23 -2
  26. data/docs/manual/11-ci.md +9 -0
  27. data/docs/manual/15-type-protection-coverage.md +8 -0
  28. data/docs/manual/16-rbs-extended-annotations.md +6 -1
  29. data/docs/manual/18-removing-dead-code.md +10 -8
  30. data/docs/manual/plugins/rigor-actionpack.md +18 -0
  31. data/docs/manual/plugins/rigor-activerecord.md +28 -0
  32. data/docs/manual/plugins/rigor-activesupport-core-ext.md +32 -2
  33. data/docs/manual/plugins/rigor-rbs-inline.md +42 -1
  34. data/lib/rigor/analysis/baseline.rb +2 -2
  35. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  36. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  37. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  38. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +1 -1
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  40. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/rule_walk.rb +39 -4
  42. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  43. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  44. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  45. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  46. data/lib/rigor/analysis/check_rules.rb +200 -32
  47. data/lib/rigor/analysis/crash_signature.rb +8 -24
  48. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  52. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  53. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  54. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  55. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  56. data/lib/rigor/analysis/fact_store.rb +6 -1
  57. data/lib/rigor/analysis/incremental.rb +16 -0
  58. data/lib/rigor/analysis/incremental_session.rb +81 -22
  59. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  60. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  61. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -5
  62. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  63. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -2
  65. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  66. data/lib/rigor/analysis/result.rb +1 -3
  67. data/lib/rigor/analysis/rule_catalog.rb +4 -1
  68. data/lib/rigor/analysis/run_cache_key.rb +1 -1
  69. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  70. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  71. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  72. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +177 -26
  73. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  74. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -10
  75. data/lib/rigor/analysis/runner/pool_coordinator.rb +256 -88
  76. data/lib/rigor/analysis/runner/project_pre_passes.rb +12 -6
  77. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  78. data/lib/rigor/analysis/runner.rb +133 -35
  79. data/lib/rigor/analysis/worker_session.rb +9 -10
  80. data/lib/rigor/bleeding_edge.rb +12 -24
  81. data/lib/rigor/builtins/hkt_builtins.rb +2 -2
  82. data/lib/rigor/builtins/imported_refinements.rb +155 -21
  83. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  84. data/lib/rigor/builtins/regex_refinement.rb +4 -4
  85. data/lib/rigor/builtins/static_return_refinements.rb +5 -6
  86. data/lib/rigor/cache/annotation_location.rb +2 -4
  87. data/lib/rigor/cache/descriptor.rb +58 -20
  88. data/lib/rigor/cache/engine_source.rb +4 -4
  89. data/lib/rigor/cache/file_digest.rb +9 -1
  90. data/lib/rigor/cache/incremental_snapshot.rb +40 -6
  91. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  92. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  93. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  94. data/lib/rigor/cache/rbs_descriptor.rb +79 -11
  95. data/lib/rigor/cache/rbs_environment.rb +8 -4
  96. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  97. data/lib/rigor/cache/store.rb +77 -32
  98. data/lib/rigor/cli/annotate_command.rb +5 -6
  99. data/lib/rigor/cli/check_command.rb +57 -10
  100. data/lib/rigor/cli/check_invocation.rb +6 -11
  101. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  102. data/lib/rigor/cli/coverage_command.rb +1 -1
  103. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  104. data/lib/rigor/cli/coverage_scan.rb +1 -5
  105. data/lib/rigor/cli/diff_command.rb +1 -1
  106. data/lib/rigor/cli/doc_links.rb +3 -3
  107. data/lib/rigor/cli/docs_command.rb +2 -2
  108. data/lib/rigor/cli/doctor_command.rb +85 -38
  109. data/lib/rigor/cli/effects_command.rb +2 -2
  110. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  111. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  112. data/lib/rigor/cli/explain_command.rb +34 -1
  113. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  114. data/lib/rigor/cli/lsp_command.rb +1 -1
  115. data/lib/rigor/cli/mcp_command.rb +1 -1
  116. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  117. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  118. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  119. data/lib/rigor/cli/plugin_command.rb +1 -1
  120. data/lib/rigor/cli/plugins_command.rb +2 -1
  121. data/lib/rigor/cli/prism_colorizer.rb +2 -2
  122. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  123. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  124. data/lib/rigor/cli/sig_gen_command.rb +2 -2
  125. data/lib/rigor/cli/skill_command.rb +1 -1
  126. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  127. data/lib/rigor/cli/skill_describe.rb +36 -35
  128. data/lib/rigor/cli/trace_command.rb +4 -4
  129. data/lib/rigor/cli/trace_renderer.rb +5 -6
  130. data/lib/rigor/cli/triage_command.rb +1 -1
  131. data/lib/rigor/cli/type_of_command.rb +10 -6
  132. data/lib/rigor/cli/type_scan_command.rb +4 -4
  133. data/lib/rigor/cli/unused_command.rb +11 -3
  134. data/lib/rigor/cli/upgrade_command.rb +1 -1
  135. data/lib/rigor/cli.rb +68 -6
  136. data/lib/rigor/config_audit.rb +30 -5
  137. data/lib/rigor/configuration/severity_profile.rb +6 -6
  138. data/lib/rigor/configuration.rb +43 -4
  139. data/lib/rigor/effects/attribution.rb +1 -3
  140. data/lib/rigor/effects/config_envelopes.rb +6 -8
  141. data/lib/rigor/effects/definition_lines.rb +25 -6
  142. data/lib/rigor/effects/effect_table.rb +0 -1
  143. data/lib/rigor/effects/entry_points.rb +0 -2
  144. data/lib/rigor/effects/envelope_check.rb +8 -8
  145. data/lib/rigor/effects/envelope_index.rb +5 -8
  146. data/lib/rigor/effects/framework_units.rb +4 -6
  147. data/lib/rigor/effects/identity.rb +4 -6
  148. data/lib/rigor/effects/inline_anchor.rb +7 -7
  149. data/lib/rigor/effects/label_intent.rb +3 -4
  150. data/lib/rigor/effects/liskov_check.rb +8 -8
  151. data/lib/rigor/effects/method_key.rb +1 -1
  152. data/lib/rigor/effects/plugin_facts.rb +4 -6
  153. data/lib/rigor/effects/propagator.rb +2 -3
  154. data/lib/rigor/effects/scanner.rb +1 -1
  155. data/lib/rigor/effects/signature_sources.rb +3 -5
  156. data/lib/rigor/effects/snapshot.rb +10 -10
  157. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  158. data/lib/rigor/effects/unit_scan.rb +10 -10
  159. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  160. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  161. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  162. data/lib/rigor/environment/class_registry.rb +3 -1
  163. data/lib/rigor/environment/failure_slot.rb +2 -2
  164. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  165. data/lib/rigor/environment/lockfile_resolver.rb +51 -4
  166. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  167. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  168. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  169. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  170. data/lib/rigor/environment/rbs_loader.rb +388 -59
  171. data/lib/rigor/environment.rb +105 -20
  172. data/lib/rigor/flow_contribution/merger.rb +0 -2
  173. data/lib/rigor/flow_contribution.rb +11 -13
  174. data/lib/rigor/inference/acceptance.rb +153 -8
  175. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  176. data/lib/rigor/inference/body_fixpoint.rb +5 -5
  177. data/lib/rigor/inference/budget_trace.rb +1 -5
  178. data/lib/rigor/inference/captured_locals.rb +2 -3
  179. data/lib/rigor/inference/closure_escape_analyzer.rb +2 -4
  180. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  181. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  182. data/lib/rigor/inference/element_read_widening.rb +168 -0
  183. data/lib/rigor/inference/expression_typer.rb +763 -143
  184. data/lib/rigor/inference/fork_map.rb +5 -7
  185. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  186. data/lib/rigor/inference/hkt_registry.rb +4 -7
  187. data/lib/rigor/inference/index_write_widening.rb +1 -5
  188. data/lib/rigor/inference/macro_block_self_type.rb +1 -4
  189. data/lib/rigor/inference/method_dispatcher/block_folding.rb +2 -9
  190. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  191. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  192. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  193. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  194. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +2 -2
  195. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  197. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  198. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  199. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +61 -47
  200. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  201. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +165 -31
  202. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  203. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  204. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  206. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  208. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  209. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -5
  210. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  211. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  212. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  213. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  214. data/lib/rigor/inference/method_dispatcher.rb +12 -13
  215. data/lib/rigor/inference/method_parameter_binder.rb +4 -6
  216. data/lib/rigor/inference/multi_target_binder.rb +1 -3
  217. data/lib/rigor/inference/mutation_rejoin.rb +168 -0
  218. data/lib/rigor/inference/mutation_widening.rb +100 -51
  219. data/lib/rigor/inference/narrowing.rb +299 -46
  220. data/lib/rigor/inference/optimistic_origin.rb +1 -7
  221. data/lib/rigor/inference/origin_lookup.rb +2 -3
  222. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  223. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  224. data/lib/rigor/inference/precision_scanner.rb +3 -4
  225. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  226. data/lib/rigor/inference/project_patched_scanner.rb +3 -3
  227. data/lib/rigor/inference/protection_scanner.rb +1 -2
  228. data/lib/rigor/inference/range_constant.rb +57 -0
  229. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  230. data/lib/rigor/inference/receiver_alias.rb +3 -3
  231. data/lib/rigor/inference/refinement_mutation.rb +72 -0
  232. data/lib/rigor/inference/scope_indexer.rb +688 -151
  233. data/lib/rigor/inference/statement_evaluator.rb +221 -50
  234. data/lib/rigor/inference/string_mutation.rb +60 -0
  235. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  236. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  237. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  238. data/lib/rigor/inference/version_guard.rb +28 -21
  239. data/lib/rigor/inference/void_origin.rb +3 -3
  240. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  241. data/lib/rigor/language_server/buffer_table.rb +4 -4
  242. data/lib/rigor/language_server/completion_provider.rb +1 -1
  243. data/lib/rigor/language_server/debouncer.rb +1 -1
  244. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  245. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  246. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  247. data/lib/rigor/language_server/hover_provider.rb +1 -1
  248. data/lib/rigor/language_server/hover_renderer.rb +2 -2
  249. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  250. data/lib/rigor/language_server/project_context.rb +2 -5
  251. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  252. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  253. data/lib/rigor/language_server/server.rb +9 -9
  254. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  255. data/lib/rigor/language_server/uri.rb +1 -1
  256. data/lib/rigor/plugin/base.rb +90 -21
  257. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  258. data/lib/rigor/plugin/effect_attribution.rb +7 -7
  259. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  260. data/lib/rigor/plugin/fact_store.rb +6 -6
  261. data/lib/rigor/plugin/io_boundary.rb +93 -8
  262. data/lib/rigor/plugin/isolation.rb +29 -6
  263. data/lib/rigor/plugin/loader.rb +31 -12
  264. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  265. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  266. data/lib/rigor/plugin/manifest.rb +23 -1
  267. data/lib/rigor/plugin/registry.rb +42 -12
  268. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +12 -1
  269. data/lib/rigor/plugin/trust_policy.rb +2 -4
  270. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  271. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  272. data/lib/rigor/project_environment.rb +138 -0
  273. data/lib/rigor/protection/analysis_guard.rb +93 -14
  274. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  275. data/lib/rigor/protection/dependency_closure.rb +5 -8
  276. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  277. data/lib/rigor/protection/discovery_seed.rb +11 -11
  278. data/lib/rigor/protection/kill_signature.rb +2 -4
  279. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  280. data/lib/rigor/protection/mutation_cache.rb +6 -9
  281. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  282. data/lib/rigor/protection/mutator.rb +2 -1
  283. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  284. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  287. data/lib/rigor/rbs_extended.rb +61 -21
  288. data/lib/rigor/reflection/constant_path.rb +126 -0
  289. data/lib/rigor/reflection.rb +26 -10
  290. data/lib/rigor/runtime/jit.rb +6 -8
  291. data/lib/rigor/scope/discovery_index.rb +30 -0
  292. data/lib/rigor/scope.rb +203 -4
  293. data/lib/rigor/sig_gen/generator.rb +115 -22
  294. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  295. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  296. data/lib/rigor/sig_gen/observation_collector.rb +17 -15
  297. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  298. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  299. data/lib/rigor/sig_gen/renderer.rb +4 -5
  300. data/lib/rigor/sig_gen/skip_reason_catalog.rb +111 -0
  301. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  302. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  303. data/lib/rigor/sig_gen/writer.rb +23 -6
  304. data/lib/rigor/signature_path_audit.rb +153 -6
  305. data/lib/rigor/source/literals.rb +0 -23
  306. data/lib/rigor/source/node_children.rb +0 -2
  307. data/lib/rigor/source/node_locator.rb +5 -11
  308. data/lib/rigor/source/node_walker.rb +2 -5
  309. data/lib/rigor/triage/catalogue.rb +1 -3
  310. data/lib/rigor/triage.rb +3 -5
  311. data/lib/rigor/type/accepts_result.rb +24 -6
  312. data/lib/rigor/type/combinator.rb +68 -5
  313. data/lib/rigor/type/data_class.rb +2 -2
  314. data/lib/rigor/type/data_instance.rb +4 -4
  315. data/lib/rigor/type/float_range.rb +128 -0
  316. data/lib/rigor/type/hash_shape.rb +5 -5
  317. data/lib/rigor/type/integer_range.rb +13 -8
  318. data/lib/rigor/type/nominal.rb +8 -2
  319. data/lib/rigor/type/refined.rb +3 -3
  320. data/lib/rigor/type/struct_class.rb +3 -3
  321. data/lib/rigor/type/struct_instance.rb +4 -4
  322. data/lib/rigor/type.rb +1 -0
  323. data/lib/rigor/type_node/generic.rb +1 -1
  324. data/lib/rigor/type_node/range_literal.rb +25 -0
  325. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  326. data/lib/rigor/type_node.rb +1 -0
  327. data/lib/rigor/version.rb +1 -1
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  329. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  330. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  331. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -0
  332. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  333. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  334. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  335. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +1 -0
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +33 -30
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  338. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  339. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +28 -2
  340. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  341. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  342. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  343. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  344. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  345. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  346. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +1 -0
  347. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +3 -4
  348. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  349. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  350. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +42 -7
  351. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  352. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +119 -14
  355. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  356. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +1 -0
  357. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  358. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  359. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  360. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  361. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  362. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  363. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  364. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  365. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  366. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  367. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  368. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  369. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  370. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  371. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  372. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +6 -3
  373. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  374. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  375. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +0 -3
  376. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  377. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  378. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  379. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  380. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  381. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +1 -0
  382. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  383. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  384. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  385. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -0
  386. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  387. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -10
  388. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +3 -3
  389. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +2 -2
  390. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -0
  395. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  396. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  397. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +128 -3
  398. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  399. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  400. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  403. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  404. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  405. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  406. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  407. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  408. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  409. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  410. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -1
  411. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +1 -0
  414. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  415. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +4 -5
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +5 -5
  423. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  424. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  425. data/sig/rigor/cache.rbs +2 -3
  426. data/sig/rigor/environment.rbs +5 -0
  427. data/sig/rigor/inference.rbs +3 -10
  428. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  429. data/sig/rigor/plugin/manifest.rbs +2 -1
  430. data/sig/rigor/reflection.rbs +3 -1
  431. data/sig/rigor/scope.rbs +24 -3
  432. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -0
  433. data/sig/rigor/type.rbs +27 -1
  434. data/sig/rigor.rbs +1 -0
  435. data/skills/rigor-ask/SKILL.md +8 -6
  436. data/skills/rigor-plugin-author/references/02-walker-and-types.md +13 -2
  437. data/skills/rigor-project-init/SKILL.md +6 -3
  438. data/skills/rigor-project-init/references/02-configure.md +10 -6
  439. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  440. data/skills/rigor-type-oracle/SKILL.md +228 -0
  441. data/skills/rigor-type-oracle/references/01-oracle-commands.md +261 -0
  442. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  443. data/skills/rigor-type-oracle/references/03-gap-protocol.md +128 -0
  444. data/skills/rigor-unused-adjudicate/SKILL.md +8 -5
  445. metadata +26 -3
  446. data/lib/rigor/cli/probe_environment.rb +0 -85
  447. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
data/lib/rigor/scope.rb CHANGED
@@ -21,7 +21,7 @@ module Rigor
21
21
  attr_reader :environment, :locals, :fact_store, :self_type,
22
22
  :ivars, :cvars, :globals,
23
23
  :indexed_narrowings, :method_chain_narrowings,
24
- :declaration_sourced,
24
+ :declaration_sourced, :published_constant_sourced,
25
25
  :source_path, :discovery, :struct_fold_safe_locals,
26
26
  :opaque_block_self, :lexical_nesting,
27
27
  :dynamic_origins, :local_origins, :ivar_origins,
@@ -49,12 +49,21 @@ module Rigor
49
49
  def discovered_method_visibilities = @discovery.discovered_method_visibilities
50
50
  def discovered_superclasses = @discovery.discovered_superclasses
51
51
  def discovered_includes = @discovery.discovered_includes
52
+ def discovered_extends = @discovery.discovered_extends
52
53
  def discovered_class_sources = @discovery.discovered_class_sources
53
54
  # Issue #644 — `{qualified constant name => Set[declaring file]}`; seeded only on an ADR-46 recording run.
54
55
  def constant_sources = @discovery.constant_sources
55
56
  def published_constant_names = @discovery.published_constant_names
56
57
  def local_constant_names = @discovery.local_constant_names
57
58
 
59
+ # Issue #667 — the instance-variable names of `class_name` whose class-ivar seed comes from a foreign
60
+ # published constant (`@mode = AppConfig::MODE` in `initialize`, read in a sibling method).
61
+ # `StatementEvaluator#seed_instance_ivars` stamps the flow mark for these at method-body entry, the way
62
+ # ADR-58's seed stamps its own; a method-local write or narrowing then drops it through `with_ivar`.
63
+ def published_constant_ivars_for(class_name)
64
+ @discovery.published_constant_ivars[class_name] || EMPTY_PUBLISHED_CONSTANT_IVARS
65
+ end
66
+
58
67
  # Issue #644 — true when the constant reference `name` (as written: `MODE` or `AppConfig::MODE`) is
59
68
  # published by the cross-file value-constant table and is NOT assigned by the file being analysed: a
60
69
  # value this file's author cannot see the declaration of. {Analysis::CheckRules::PublishedConstantGuard}
@@ -76,14 +85,27 @@ module Rigor
76
85
  # over already-qualified names rather than a second lexical ladder: the engine's ladder
77
86
  # (`Reflection.resolve_constant_type`) answers with a TYPE and not with the candidate that won, and
78
87
  # reimplementing the walk here is the very thing that made the receiver resolution wrong once already.
88
+ #
89
+ # Issue #667 — a third answer sits beside those two. `MODE2 = AppConfig::MODE` is assigned by this file,
90
+ # so the exemption above releases it, yet its value is still one the author never saw: the alias only
91
+ # renames the foreign declaration. `published_constant_alias_names` holds the last segments of exactly
92
+ # those aliases, and matching on the last segment over-answers in the WITHHOLDING direction, which is the
93
+ # safe one.
79
94
  def published_constant?(name)
80
95
  names = @discovery.published_constant_names
96
+ return true if published_constant_alias?(name)
81
97
  return false if names.empty?
82
98
  return false unless names.include?(name.split("::").last)
83
99
 
84
100
  !locally_declared_constant?(name)
85
101
  end
86
102
 
103
+ def published_constant_alias?(name)
104
+ aliases = @discovery.published_constant_alias_names
105
+ !aliases.empty? && aliases.include?(name.split("::").last)
106
+ end
107
+ private :published_constant_alias?
108
+
87
109
  def locally_declared_constant?(reference_name)
88
110
  local = @discovery.local_constant_names
89
111
  return false if local.empty?
@@ -153,9 +175,21 @@ module Rigor
153
175
  # threaded by reference through transitions; reset per method body (a fresh entry scope drops it), so the
154
176
  # name keys never collide across bodies.
155
177
  EMPTY_ORIGINS = {}.freeze
178
+ # Issue #667 — the set of variable references currently bound to a value COPIED out of a constant the
179
+ # project published and this file does not declare. Members are frozen `[kind, name]` pairs, the same
180
+ # spelling {EMPTY_DECLARATION_SOURCED} uses, but a DELIBERATELY SEPARATE carrier: this mark joins by
181
+ # UNION where ADR-58's kinds intersect, and it answers a different question (what a value's *constancy*
182
+ # rests on, not what a binding's *optionality* rests on) for a different consumer. Folding it into the
183
+ # ADR-58 set would be the third establishing transition its *Non-transitivity* passage reserves as a
184
+ # decision, on a Set that already carries two opposite join policies.
185
+ EMPTY_PUBLISHED_CONSTANT_SOURCED = Set.new.freeze
186
+ # Issue #667 — the empty answer of {#published_constant_ivars_for}, so a class with no such ivar (every
187
+ # class in a project that publishes nothing) allocates none.
188
+ EMPTY_PUBLISHED_CONSTANT_IVARS = Set.new.freeze
156
189
  private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
157
190
  :EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
158
- :EMPTY_FOLD_SAFE, :EMPTY_ORIGINS
191
+ :EMPTY_FOLD_SAFE, :EMPTY_ORIGINS, :EMPTY_PUBLISHED_CONSTANT_SOURCED,
192
+ :EMPTY_PUBLISHED_CONSTANT_IVARS
159
193
 
160
194
  class << self
161
195
  def empty(environment: Environment.default, source_path: nil)
@@ -207,6 +241,7 @@ module Rigor
207
241
  indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
208
242
  method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
209
243
  declaration_sourced: EMPTY_DECLARATION_SOURCED,
244
+ published_constant_sourced: EMPTY_PUBLISHED_CONSTANT_SOURCED,
210
245
  source_path: nil,
211
246
  struct_fold_safe_locals: EMPTY_FOLD_SAFE,
212
247
  opaque_block_self: false,
@@ -231,6 +266,7 @@ module Rigor
231
266
  @indexed_narrowings = indexed_narrowings
232
267
  @method_chain_narrowings = method_chain_narrowings
233
268
  @declaration_sourced = declaration_sourced
269
+ @published_constant_sourced = published_constant_sourced
234
270
  @source_path = source_path
235
271
  @struct_fold_safe_locals = struct_fold_safe_locals
236
272
  @opaque_block_self = opaque_block_self
@@ -314,6 +350,10 @@ module Rigor
314
350
  indexed_narrowings: new_indexed_narrowings,
315
351
  method_chain_narrowings: new_chain_narrowings,
316
352
  declaration_sourced: drop_declaration_sourced_for(:local, name),
353
+ # Issue #667 — rebinding is flow-live for the published-constant mark too: the new value need
354
+ # not be a copy of anything. `with_published_constant_mark` re-stamps afterward when the write's
355
+ # rvalue is one.
356
+ published_constant_sourced: drop_published_constant_sourced_for(:local, name),
317
357
  local_origins: drop_origin(@local_origins, name),
318
358
  optimistic_locals: drop_origin(@optimistic_locals, name))
319
359
  end
@@ -419,6 +459,7 @@ module Rigor
419
459
  indexed_narrowings: new_indexed_narrowings,
420
460
  method_chain_narrowings: new_chain_narrowings,
421
461
  declaration_sourced: drop_declaration_sourced_for(:ivar, name),
462
+ published_constant_sourced: drop_published_constant_sourced_for(:ivar, name),
422
463
  ivar_origins: drop_origin(@ivar_origins, name),
423
464
  optimistic_ivars: drop_origin(@optimistic_ivars, name))
424
465
  end
@@ -445,6 +486,22 @@ module Rigor
445
486
  rebuild(declaration_sourced: add_declaration_sourced(:local, name))
446
487
  end
447
488
 
489
+ # Issue #667 — record that `name` is currently bound to a value copied out of a foreign published
490
+ # constant. Always applied AFTER the `with_local` / `with_ivar` that binds the value (both drop the mark
491
+ # unconditionally), exactly as {#with_local_declaration_mark} is.
492
+ def with_published_constant_mark(kind, name)
493
+ rebuild(published_constant_sourced: add_published_constant_sourced(kind, name))
494
+ end
495
+
496
+ # Issue #667 — true when `(kind, name)`'s current binding is a copy of a constant the project published
497
+ # and this file does not declare. Asked through
498
+ # {Analysis::CheckRules::PublishedConstantGuard}, never directly from a rule.
499
+ def published_constant_sourced?(kind, name)
500
+ return false if @published_constant_sourced.empty?
501
+
502
+ @published_constant_sourced.include?([kind.to_sym, name.to_sym])
503
+ end
504
+
448
505
  # ADR-58 WD1 — true when `(kind, name)`'s binding optionality is purely declaration-sourced (no flow-live
449
506
  # write/narrowing has touched it).
450
507
  def declaration_sourced?(kind, name)
@@ -744,6 +801,34 @@ module Rigor
744
801
  @discovery.discovered_includes[class_name.to_s] || []
745
802
  end
746
803
 
804
+ # Issue #898 — the module names `extend`ed onto `class_name`'s SINGLETON, as written, gathered up the
805
+ # as-written superclass chain because a singleton class inherits its superclass's singleton class
806
+ # (`class Base; extend Comparable; end; class Widget < Base; end` leaves `Widget.is_a?(Comparable)`
807
+ # true). Empty for a class the project never extends, and for every scope that saw no seeding pass.
808
+ #
809
+ # This is EVIDENCE FOR a singleton ancestor, never against one: the walk sees only what a constant
810
+ # argument to a receiverless `extend` (or, since #915, a receiverless `include` / `prepend` inside a
811
+ # `class << self` body) written inside a declaration body spells, so a runtime `Widget.extend(m)` and an
812
+ # `extend` in a file outside the analysed set are both absent from it, as is an `extend` declared only
813
+ # in RBS — that one reaches the same consumer through `Environment#singleton_extended_modules` instead.
814
+ # Its one consumer ({Inference::Narrowing.narrow_class}) therefore reads it
815
+ # only to WITHHOLD a `Bot`, and never to assert that a guard matches.
816
+ def singleton_extends_of(class_name)
817
+ table = @discovery.discovered_extends
818
+ return [] if table.empty?
819
+
820
+ names = []
821
+ current = class_name.to_s
822
+ seen = {}
823
+ while current && !seen[current] && seen.size <= ANCESTOR_WALK_LIMIT
824
+ seen[current] = true
825
+ record_class_dependency(current) if Analysis::DependencyRecorder.active?
826
+ names.concat(table[current] || [])
827
+ current = @discovery.discovered_superclasses[current]
828
+ end
829
+ names.uniq
830
+ end
831
+
747
832
  # ADR-24 slice 2 — the user-side ancestor walk: resolves `method_name` against `class_name`'s own `def`s,
748
833
  # then breadth-first through its included / prepended modules (mixins first, as Ruby orders them) and its
749
834
  # superclass chain. Returns `[def_node, owner]`, or `[nil, nil]` when nothing in the project defines it.
@@ -810,9 +895,56 @@ module Rigor
810
895
  [nil, nil]
811
896
  end
812
897
 
898
+ # Issue #633 — the ancestors a project class reaches that the project itself does NOT declare: the
899
+ # `< StandardError` / `< Array` superclasses and the `include Comparable` mixins the two walks above
900
+ # deliberately drop, gathered breadth-first over the project ancestry so an inherited edge counts too.
901
+ #
902
+ # Each entry is the CANDIDATE LIST for one as-written name ({#ancestor_name_candidates}: the nesting
903
+ # spellings first, the bare name last), not a resolved class — resolving it means asking the RBS
904
+ # environment, which this frozen-index walk does not read. The caller takes the first candidate its own
905
+ # oracle knows and ignores the rest, the same "most-qualified first" order the project-side resolution
906
+ # uses.
907
+ def external_ancestor_name_candidates(class_name, name_memo: {})
908
+ groups = []
909
+ queue = [class_name.to_s]
910
+ seen = {}
911
+ visited = 0
912
+ until queue.empty?
913
+ current = queue.shift
914
+ next if current.nil? || seen[current]
915
+
916
+ seen[current] = true
917
+ visited += 1
918
+ break if visited > ANCESTOR_WALK_LIMIT
919
+
920
+ collect_external_ancestors(current, queue, groups, name_memo)
921
+ end
922
+ groups
923
+ end
924
+
925
+ # One node of {#external_ancestor_name_candidates}: the project-declared ancestors continue the walk,
926
+ # everything else is reported as a candidate list.
927
+ def collect_external_ancestors(current, queue, groups, name_memo)
928
+ raw_names = includes_of(current).dup
929
+ raw_super = superclass_of(current)
930
+ raw_names << raw_super if raw_super
931
+ raw_names.each do |raw|
932
+ resolved = resolve_ancestor_class_name(current, raw, name_memo)
933
+ if resolved
934
+ queue.push(resolved)
935
+ else
936
+ groups << ancestor_name_candidates(current, raw)
937
+ end
938
+ end
939
+ end
940
+ private :collect_external_ancestors
941
+
813
942
  # The BFS node cap; a hierarchy past it gives up rather than walking unboundedly (ADR-41 WD4).
814
943
  ANCESTOR_WALK_LIMIT = 100
815
944
 
945
+ EMPTY_HEADER_NESTING = [].freeze
946
+ private_constant :EMPTY_HEADER_NESTING
947
+
816
948
  def ancestor_walk_gave_up
817
949
  Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
818
950
  [nil, nil]
@@ -929,10 +1061,25 @@ module Rigor
929
1061
  return [raw.delete_prefix("::")] if raw.start_with?("::")
930
1062
 
931
1063
  recorded = @discovery.discovered_header_nestings[subclass_qualified.to_s]
932
- entries = recorded || peeled_header_nesting(subclass_qualified)
1064
+ entries = recorded ? recorded_header_nesting(recorded, raw) : peeled_header_nesting(subclass_qualified)
933
1065
  entries.map { |entry| "#{entry}::#{raw_ancestor}" } << raw_ancestor.to_s
934
1066
  end
935
1067
 
1068
+ # Issue #728 — the chain of the declaration site that WROTE `raw`, which is the cref Ruby resolves that
1069
+ # one name in. A class's sites need not agree: `class Foo < Base` at the top level and a rooted
1070
+ # `class ::Foo; include Helper; end` inside `module Outer` are two sites of `Foo`, and the union of
1071
+ # their chains put `Outer::Base` — a class Ruby never looks at — ahead of `::Base` for the superclass
1072
+ # the top-level site wrote. That is a WRONG CLASS, not a wider candidate list.
1073
+ #
1074
+ # The unkeyed entry is the union, and answers a name no site recorded under its own key: an `include`
1075
+ # attributed to this class from OUTSIDE its declaration (`Recv.class_eval { include M }`, which
1076
+ # `Inference::ScopeIndexer#walk_class_includes` names but the header walk cannot see), or a dynamic
1077
+ # mixin argument. It is the pre-#728 answer, so those are unchanged rather than degraded.
1078
+ def recorded_header_nesting(bucket, raw)
1079
+ bucket[raw] || bucket[DiscoveryIndex::UNKEYED_HEADER_NESTING] || EMPTY_HEADER_NESTING
1080
+ end
1081
+ private :recorded_header_nesting
1082
+
936
1083
  # The pre-#682 reading of a qualified class name: every proper prefix of it, innermost first, as if the
937
1084
  # class had been declared one `module` keyword per segment.
938
1085
  def peeled_header_nesting(subclass_qualified)
@@ -974,6 +1121,21 @@ module Rigor
974
1121
  end
975
1122
  private :record_class_dependency
976
1123
 
1124
+ # Issue #639 — the EXISTENCE edge for a bare class reference, `class:<last segment>`, the hit-side twin of
1125
+ # the miss-side key `Inference::ExpressionTyper` already records. ADR-46 slice 1c left the existence hit
1126
+ # edgeless on the reasoning that a file merely referencing `Post` depends on `Post`'s methods, not on its
1127
+ # bare existence; that stops holding the moment the DECLARING FILE IS DELETED, because existence is then
1128
+ # exactly what changed and a reference-only consumer had recorded nothing to be re-checked by.
1129
+ #
1130
+ # NAME-keyed rather than a positive edge to the declaring file, which is the distinction the ADR's cost
1131
+ # argument turns on: the declared-class set of a file does not move when a method body in it is edited,
1132
+ # so a bare referent is re-checked when the class appears or disappears and at no other time. A file edge
1133
+ # would have re-checked every bare referent of every class the file declares on any edit to it.
1134
+ def record_class_existence(class_name)
1135
+ segment = class_name.to_s.delete_prefix("::").split("::").last
1136
+ Analysis::DependencyRecorder.read_name(:class, segment) if segment
1137
+ end
1138
+
977
1139
  # Issue #644 — the positive ADR-46 edge for a cross-file VALUE constant. `Reflection.constant_type_at`
978
1140
  # calls this the moment a candidate resolves through `in_source_constants`, so the reader depends on the
979
1141
  # file that wrote the constant: editing the literal, or deleting the file, re-checks the reader. Recorded
@@ -1125,7 +1287,8 @@ module Rigor
1125
1287
  @globals == other.globals &&
1126
1288
  @indexed_narrowings == other.indexed_narrowings &&
1127
1289
  @method_chain_narrowings == other.method_chain_narrowings &&
1128
- @declaration_sourced == other.declaration_sourced
1290
+ @declaration_sourced == other.declaration_sourced &&
1291
+ @published_constant_sourced == other.published_constant_sourced
1129
1292
  end
1130
1293
  alias eql? ==
1131
1294
 
@@ -1142,6 +1305,7 @@ module Rigor
1142
1305
  indexed_narrowings: @indexed_narrowings,
1143
1306
  method_chain_narrowings: @method_chain_narrowings,
1144
1307
  declaration_sourced: @declaration_sourced,
1308
+ published_constant_sourced: @published_constant_sourced,
1145
1309
  source_path: @source_path,
1146
1310
  struct_fold_safe_locals: @struct_fold_safe_locals,
1147
1311
  opaque_block_self: @opaque_block_self,
@@ -1163,6 +1327,7 @@ module Rigor
1163
1327
  indexed_narrowings: indexed_narrowings,
1164
1328
  method_chain_narrowings: method_chain_narrowings,
1165
1329
  declaration_sourced: declaration_sourced,
1330
+ published_constant_sourced: published_constant_sourced,
1166
1331
  source_path: source_path,
1167
1332
  struct_fold_safe_locals: struct_fold_safe_locals,
1168
1333
  opaque_block_self: opaque_block_self,
@@ -1208,6 +1373,12 @@ module Rigor
1208
1373
  # path made the binding flow-live (a method-local nil write / failed-guard narrowing), the merge is
1209
1374
  # flow-live and `possible-nil-receiver` fires as before.
1210
1375
  declaration_sourced: join_declaration_sourced(other),
1376
+ # Issue #667 — UNION, the opposite of the line above, which is the first reason this mark does not
1377
+ # ride the ADR-58 Set. The mark only ever WITHHOLDS a firing, so keeping it when either arm bound the
1378
+ # name from a published constant is the false-positive-safe merge: `if c then m = MODE else m = MODE2
1379
+ # end; m == :x` must not warn because one arm's copy is invisible to the reader's author. ADR-67
1380
+ # WD6b's `:inferred_param` taint takes the same direction for the same reason.
1381
+ published_constant_sourced: join_published_constant_sourced(other),
1211
1382
  source_path: source_path,
1212
1383
  # Issue #589 — the fold-safe set MUST survive a merge. It was simply absent from this constructor
1213
1384
  # call, so it fell back to the empty default and every `if` / `while` in a method silently revoked
@@ -1339,6 +1510,34 @@ module Rigor
1339
1510
  end
1340
1511
 
1341
1512
  # ADR-58 WD1 — set/clear the declaration-sourced provenance mark.
1513
+ # Issue #667 — the #667 carrier's add/drop pair. Deliberately not reusing
1514
+ # {#add_declaration_sourced} / {#drop_declaration_sourced_for}: the two sets are joined by opposite
1515
+ # policies, so sharing the storage would make every future edit to either one a decision about both.
1516
+ def add_published_constant_sourced(kind, name)
1517
+ ref = [kind.to_sym, name.to_sym].freeze
1518
+ return @published_constant_sourced if @published_constant_sourced.include?(ref)
1519
+
1520
+ (@published_constant_sourced.dup << ref).freeze
1521
+ end
1522
+
1523
+ def drop_published_constant_sourced_for(kind, name)
1524
+ return @published_constant_sourced if @published_constant_sourced.empty?
1525
+
1526
+ ref = [kind.to_sym, name.to_sym]
1527
+ return @published_constant_sourced unless @published_constant_sourced.include?(ref)
1528
+
1529
+ (@published_constant_sourced - [ref]).freeze
1530
+ end
1531
+
1532
+ def join_published_constant_sourced(other)
1533
+ mine = @published_constant_sourced
1534
+ theirs = other.published_constant_sourced
1535
+ return mine if mine.equal?(theirs) || theirs.empty?
1536
+ return theirs if mine.empty?
1537
+
1538
+ (mine | theirs).freeze
1539
+ end
1540
+
1342
1541
  def add_declaration_sourced(kind, name)
1343
1542
  ref = [kind.to_sym, name.to_sym]
1344
1543
  return @declaration_sourced if @declaration_sourced.include?(ref)
@@ -4,9 +4,11 @@ require "prism"
4
4
 
5
5
  require_relative "../configuration"
6
6
  require_relative "../environment"
7
+ require_relative "../project_environment"
7
8
  require_relative "../scope"
8
9
  require_relative "../reflection"
9
10
  require_relative "../type"
11
+ require_relative "../source/constant_path"
10
12
  require_relative "../source/literals"
11
13
  require_relative "../source/node_children"
12
14
  require_relative "../inference/def_return_typer"
@@ -39,17 +41,23 @@ module Rigor
39
41
  # - Tighter-return detection compares the RBS-erased spellings only when the existing declared return
40
42
  # strictly accepts the inferred one (acceptance check under the engine's current `:gradual` mode; ADR-14
41
43
  # reserves the eventual `:strict` mode).
44
+ # - A declared `void` return is never compared as a value type. It is the author's statement that the
45
+ # return is not part of the contract, which no inference can synthesize, so the method classifies
46
+ # `equivalent` carrying `void` itself as the declared spelling (#836; see {#declares_void?}).
47
+ # - A proposal that erases to an RBS literal never tightens an existing declaration: the declared type is
48
+ # the author's abstraction over the body and the literal is what it hides (#837; see
49
+ # {#pins_literal_over_declaration?}). A method with no declaration is unaffected — clause 1 still emits
50
+ # the strictest carrier the body proves.
42
51
  class Generator # rubocop:disable Metrics/ClassLength
43
52
  # Methods the generator rendered into RBS that `rbs` itself rejects. Populated by {#build_candidate}; each
44
53
  # one is a Rigor rendering DEFECT, not a property of the user's code, so the CLI reports them as such.
45
54
  UnrenderableMethod = Data.define(:path, :class_name, :method_name, :rbs, :error)
46
55
 
47
- # @return [Array<UnrenderableMethod>] empty on a healthy run; read after {#run}.
56
+ # @return empty on a healthy run; read after {#run}.
48
57
  attr_reader :unrenderable, :unresolvable_superclasses
49
58
 
50
- # @param configuration [Rigor::Configuration]
51
- # @param paths [Array<String>] files / directories to scan.
52
- # @param observations [Hash{[String, Symbol] => Array<Array<Rigor::Type>>}]
59
+ # @param paths — files / directories to scan.
60
+ # @param observations — => Array<Array<Rigor::Type>>}]
53
61
  # ADR-14 slice 3 — per-target-method arg-tuple observations
54
62
  # produced by {ObservationCollector}. An empty Hash (the default)
55
63
  # means "no observations available; emit `untyped` for every
@@ -84,10 +92,9 @@ module Rigor
84
92
  map.transform_values { |entries| entries.map { |entry| ObservedCall.from(entry) } }
85
93
  end
86
94
 
87
- # @return [Array<MethodCandidate>]
88
95
  def run
89
- @environment = build_environment
90
96
  resolved = resolve_paths(@paths)
97
+ @environment = build_environment(resolved)
91
98
  candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
92
99
  demote_overridden_base_methods(
93
100
  demote_unresolvable_superclasses(resolve_superclass_spellings(candidates))
@@ -311,11 +318,13 @@ module Rigor
311
318
  )
312
319
  end
313
320
 
314
- def build_environment
315
- Environment.for_project(
316
- libraries: @configuration.libraries,
317
- signature_paths: @configuration.signature_paths
318
- )
321
+ # Issue #821 — the SAME type universe `rigor check` analyses against: the project's `sig/`, the plugins,
322
+ # the bundle's per-gem signatures and the `rbs collection`. Built with two keywords only, the generator
323
+ # could not see a gem-declared superclass, and the skip guard above (`record_unresolvable_superclass`)
324
+ # then declined to emit every class descending from one — every Rails model, on a project whose
325
+ # collection was installed and whose `check` read it.
326
+ def build_environment(source_files = [])
327
+ ProjectEnvironment.build(configuration: @configuration, source_files: source_files)
319
328
  end
320
329
 
321
330
  def resolve_paths(args)
@@ -413,14 +422,22 @@ module Rigor
413
422
  end
414
423
  end
415
424
 
425
+ # [#722](https://github.com/rigortype/rigor/issues/722) residue 3 — the qualified name is built by
426
+ # `Source::ConstantPath.declaration_prefix`, the same reader the engine's declaration walks use, so a
427
+ # ROOTED header re-anchors here too. Appending the rendered name to the enclosing prefix wrote
428
+ # `class ::Foo` inside `module MyApp` out as `MyApp::Foo` — sig-gen's own copy of #708, and the one
429
+ # copy that persists its answer into a file the user keeps.
430
+ #
431
+ # `prefix` stays the nesting the header is WRITTEN in for {#record_superclass}: `::` re-anchors the
432
+ # declaration, not the superclass expression beside it, which Ruby still evaluates in the enclosing cref.
416
433
  def descend_into_namespace?(node, prefix, out)
417
- name = qualified_constant_path(node.constant_path)
418
- return false unless name
434
+ child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
435
+ return false unless child_prefix
419
436
 
420
- full = (prefix + [name]).join("::")
437
+ full = child_prefix.join("::")
421
438
  @namespace_kinds[full] = node.is_a?(Prism::ClassNode) ? :class : :module
422
439
  record_superclass(node, full, prefix)
423
- walk_namespace_body(node, prefix + [name], out)
440
+ walk_namespace_body(node, child_prefix, out)
424
441
  true
425
442
  end
426
443
 
@@ -836,7 +853,14 @@ module Rigor
836
853
  )
837
854
  end
838
855
 
856
+ # The RBS spelling carried for a `void`-declared method, so `--diff` shows the author's own word rather
857
+ # than the `top` the translator would erase it to.
858
+ VOID_RETURN_RBS = "void"
859
+ private_constant :VOID_RETURN_RBS
860
+
839
861
  def compare_against_declared(path, def_node, class_name, kind, inferred, method_def)
862
+ return equivalent(path, def_node, class_name, kind, inferred, VOID_RETURN_RBS) if declares_void?(method_def)
863
+
840
864
  declared = build_declared_return(method_def)
841
865
  declared_rbs = declared&.erase_to_rbs
842
866
  inferred_rbs = inferred.erase_to_rbs
@@ -845,7 +869,7 @@ module Rigor
845
869
  return equivalent(path, def_node, class_name, kind, inferred, declared_rbs)
846
870
  end
847
871
 
848
- unless tighter?(declared, inferred) && !computed_literal_tightening?(inferred, def_node)
872
+ unless tighter?(declared, inferred) && !literal_decline?(inferred, def_node)
849
873
  return equivalent(path, def_node, class_name, kind, inferred, declared_rbs)
850
874
  end
851
875
 
@@ -861,6 +885,28 @@ module Rigor
861
885
  )
862
886
  end
863
887
 
888
+ # Issue #836 — a declared `void` is return INTENT, never a wide value type waiting to be narrowed, so it
889
+ # is not compared against the body at all.
890
+ #
891
+ # `void` says the return value is not part of the method's contract. No synthesis can produce it: a type
892
+ # built from a body is always the type of the body's last expression, so `void` exists only on the
893
+ # CHECKING side, against a declaration the author wrote. That puts it in the same position ADR-107 gives
894
+ # a parameter type — authored intent inference cannot derive — and proposing the body's value for it is
895
+ # a category error rather than a false positive to tune.
896
+ #
897
+ # Mechanically the misreading came from the translator: `docs/type-specification/special-types.md`
898
+ # § `void` records that the engine maps RBS `void` to `top`, `Type::Top` accepts everything, so
899
+ # {#tighter?} answered "yes" for every `void`-declared method whose body happens to return a typed
900
+ # value. Seven of the fifteen tighter-returns in Rigor's own `sig/` were this shape, and the ADR-107 G3
901
+ # provenance gate inherited the misclassification with them.
902
+ #
903
+ # ANY overload declaring `void` is enough. One proposal is rendered for the whole method, so an overload
904
+ # set that mixes `void` with a value return still has an author saying "not part of the contract" about
905
+ # the return sig-gen would rewrite.
906
+ def declares_void?(method_def)
907
+ method_def.method_types.any? { |mt| mt.type.return_type.is_a?(RBS::Types::Bases::Void) }
908
+ end
909
+
864
910
  def build_declared_return(method_def)
865
911
  translated = method_def.method_types.filter_map { |mt| translate_method_type_return(mt) }
866
912
  return nil if translated.empty?
@@ -943,6 +989,48 @@ module Rigor
943
989
  inferred.is_a?(Type::Tuple) || inferred.is_a?(Type::HashShape)
944
990
  end
945
991
 
992
+ # The two reasons a `Type::Constant` the body proves is not the method's contract. Both say the same
993
+ # thing from opposite ends: the precision is real about this body and says nothing about the promise
994
+ # the method makes to its callers.
995
+ def literal_decline?(inferred, def_node)
996
+ computed_literal_tightening?(inferred, def_node) || pins_literal_over_declaration?(inferred)
997
+ end
998
+
999
+ # Issue #837 — a proposal that erases to an RBS LITERAL type never tightens an existing declaration.
1000
+ #
1001
+ # `Type::Top#describe` really does return `"top"`, but `describe` is the surface every `Rigor::Type::*`
1002
+ # class implements and every one of them declares `String`. The declared nominal is the author's
1003
+ # abstraction over the body, and the literal is the implementation detail that abstraction deliberately
1004
+ # hides — the same reading #836 gave a declared `void`. ADR-107 § Decision puts both on the intent
1005
+ # side: no synthesis produces an abstraction, because a type built from a body is always the type of
1006
+ # the body's last expression, so the choice to be wider than the body only ever exists in a declaration
1007
+ # someone wrote.
1008
+ #
1009
+ # The generator already answered this way whenever the declaration happened to be a UNION.
1010
+ # `Configuration.discover` declares `String?` against a body proving `".rigor.dist.yml" |
1011
+ # ".rigor.yml" | nil` and {#loses_declared_union_member?} refuses it, because no literal covers the
1012
+ # declared `String`. Whether the author wrote `String` or `String?` is not a decision about literals,
1013
+ # so the two shapes now answer alike.
1014
+ #
1015
+ # Scoped by the ERASURE, not by the carrier. `Type::Constant` also carries values RBS has no literal
1016
+ # spelling for (`Float`, `Regexp`, `Set`, `Pathname`, `Date`, …) and those erase to the class name, so
1017
+ # `def pi: () -> Numeric` against a body of `3.14` still proposes the ordinary nominal `Float`. A union
1018
+ # is refused when ANY member is a literal: a mixed `("a" | Float)` pins `"a"` just as a bare one does.
1019
+ # A literal nested inside a shape carrier (`[1, 2]`, a `HashShape` value) is a different question,
1020
+ # already answered for the collection declarations that matter by {#narrows_collection_to_shape?}.
1021
+ def pins_literal_over_declaration?(inferred)
1022
+ members = inferred.is_a?(Type::Union) ? inferred.members : [inferred]
1023
+ members.any? { |member| rbs_literal?(member) }
1024
+ end
1025
+
1026
+ # `Type::Constant#erase_to_rbs` spells the values RBS writes as literal types (`true` / `false` /
1027
+ # `nil`, an Integer, a Symbol, a String) as that literal and every other carrier as its value's class
1028
+ # name. Asking which of the two branches it took keeps the list of literal-spellable classes from
1029
+ # forking a second copy into this file.
1030
+ def rbs_literal?(type)
1031
+ type.is_a?(Type::Constant) && type.erase_to_rbs != type.value.class.name
1032
+ end
1033
+
946
1034
  # Heuristic added after the third-round self-dogfood: `FallbackTracer#size` body is `@events.size`, where
947
1035
  # `@events` is initialised to `[]` and never assigned again at the class-ivar pre-pass level. The
948
1036
  # `Type::Tuple[]` (size 0) folds `.size` to `Constant<0>` — the carrier knows the empty-tuple cardinality
@@ -950,6 +1038,11 @@ module Rigor
950
1038
  # signal is "the body's last expression is NOT a directly-authored literal but the inferred type IS a
951
1039
  # Constant"; in that case the precision came from inference over an internal computation, not the
952
1040
  # author's contract, so refuse to tighten.
1041
+ #
1042
+ # Since #837 the literal-ERASING half of this is refused whichever expression produced it. What stays
1043
+ # this guard's own is the `Constant` that erases to a class name: `def average = @total / 2.0` folds to
1044
+ # `Constant<2.5>` and would propose the perfectly ordinary `Float`, which is still the fold's answer
1045
+ # about one body rather than the method's contract.
953
1046
  def computed_literal_tightening?(inferred, def_node)
954
1047
  return false unless inferred.is_a?(Type::Constant)
955
1048
 
@@ -1082,9 +1175,9 @@ module Rigor
1082
1175
 
1083
1176
  case node
1084
1177
  when Prism::ClassNode, Prism::ModuleNode
1085
- name = qualified_constant_path(node.constant_path)
1086
- if name
1087
- walk_attr_calls(node.body, prefix + [name], false, ctx) if node.body
1178
+ child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
1179
+ if child_prefix
1180
+ walk_attr_calls(node.body, child_prefix, false, ctx) if node.body
1088
1181
  return
1089
1182
  end
1090
1183
  when Prism::SingletonClassNode
@@ -1166,9 +1259,9 @@ module Rigor
1166
1259
 
1167
1260
  case node
1168
1261
  when Prism::ClassNode, Prism::ModuleNode
1169
- name = qualified_constant_path(node.constant_path)
1170
- if name
1171
- collect_init_ivar_obs(node.body, prefix + [name], result) if node.body
1262
+ child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
1263
+ if child_prefix
1264
+ collect_init_ivar_obs(node.body, child_prefix, result) if node.body
1172
1265
  return
1173
1266
  end
1174
1267
  when Prism::DefNode
@@ -21,20 +21,19 @@ module Rigor
21
21
  # in multiple files for additive member contributions, but the writer only needs one canonical target per
22
22
  # class.
23
23
  class LayoutIndex
24
- # @param signature_paths [Array<String, Pathname>, nil]
24
+ # @param signature_paths
25
25
  # the `.rigor.yml`-configured signature directories.
26
26
  # When `nil` or empty, falls back to `<project_root>/sig`
27
27
  # if it exists (matching `Environment.for_project`'s
28
28
  # auto-detection convention).
29
- # @param project_root [String, Pathname]
30
29
  def initialize(signature_paths:, project_root: Dir.pwd)
31
30
  @signature_paths = resolve_paths(signature_paths, project_root)
32
31
  @index = nil
33
32
  end
34
33
 
35
- # @param class_name [String] fully-qualified Ruby class
34
+ # @param class_name fully-qualified Ruby class
36
35
  # name (e.g. `"Rigor::Type::Top"`).
37
- # @return [Pathname, nil] absolute path of the sig file
36
+ # @return absolute path of the sig file
38
37
  # that already declares this class, or `nil` when no
39
38
  # existing declaration is found.
40
39
  def file_for(class_name)
@@ -29,11 +29,9 @@ module Rigor
29
29
 
30
30
  module_function
31
31
 
32
- # @param kind [:data, :struct]
33
- # @param members [Array<Symbol>] ordered member names from the ADR-48 layout.
34
- # @param member_types [Hash{Symbol => String}] erased RBS spellings; a member with no entry renders `untyped`.
35
- # @param keyword_init [Boolean] the `Struct.new(..., keyword_init: true)` flag; ignored for `:data`.
36
- # @return [Shape]
32
+ # @param members ordered member names from the ADR-48 layout.
33
+ # @param member_types erased RBS spellings; a member with no entry renders `untyped`.
34
+ # @param keyword_init the `Struct.new(..., keyword_init: true)` flag; ignored for `:data`.
37
35
  def of(kind:, members:, member_types: {}, keyword_init: false)
38
36
  types = members.to_h { |member| [member, member_types[member] || "untyped"] }
39
37
  accessors = kind == :struct ? struct_accessors(members, types) : readers(members, types)