rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -344,19 +344,96 @@ module Rigor
344
344
  when Rigor::Type::Singleton
345
345
  chain_lookup(self_type.class_name, method_name, anchor_kind: :singleton, mixin_kind: :extend)
346
346
  when Rigor::Type::Nominal
347
- chain_lookup(self_type.class_name, method_name, anchor_kind: :instance, mixin_kind: :include)
347
+ chain_lookup(self_type.class_name, method_name, anchor_kind: :instance, mixin_kind: :include) ||
348
+ struct_prop_lookup(self_type.class_name, method_name, scope)
348
349
  end
349
350
  rescue StandardError
350
351
  nil
351
352
  end
352
353
 
354
+ # Issue #2541 — after the sig catalog (the class and its mixins) declines an instance-side call, a
355
+ # `prop` / `const` accessor the class or a superclass declares under a `T::Struct`-family root.
356
+ # The accessor's type is translated here rather than at harvest: a relative constant in it resolves the
357
+ # way Ruby's lexical lookup would from the declaring class body — under each enclosing namespace,
358
+ # innermost first, then at the top level — against the classes the walk declared and the RBS
359
+ # environment. A constant that resolves nowhere reads `Dynamic[top]`, never a guessed class.
360
+ def struct_prop_lookup(class_name, method_name, scope)
361
+ signature = @catalog.lookup_struct_prop(class_name: class_name, method_name: method_name)
362
+ return signature if signature.nil?
363
+
364
+ source, nesting = @catalog.struct_prop_type(signature)
365
+ return signature if source.nil?
366
+
367
+ signature.with(return_type: struct_prop_type(source, nesting, signature.class_name, scope))
368
+ end
369
+
370
+ def struct_prop_type(source, nesting, class_name, scope)
371
+ @struct_prop_types ||= {}
372
+ @struct_prop_types[[source, nesting, class_name, scope.nil?]] ||= begin
373
+ node = Prism.parse(source).value.statements&.body&.first
374
+ resolve = ->(name) { resolve_struct_constant(name, nesting, class_name, scope) }
375
+ TypeTranslator.translate(node, resolve)
376
+ end
377
+ end
378
+
379
+ # Ruby's lookup for a relative constant in the class body: the lexical nesting, innermost first; then
380
+ # the class's ancestors; then the top level. The nesting step answers a class it finds, and declines
381
+ # (nil, so the constant reads `Dynamic[top]`) when it meets a constant that is not a declared class.
382
+ # Ancestry is not modelled precisely: a class or constant of that name under any ancestor declines.
383
+ # The top level answers only when the struct's ancestry is closed — no class on its walked superclass
384
+ # chain `include`s or `prepend`s anything, whose constants could shadow it — and no other walked
385
+ # `*::<name>` exists.
386
+ def resolve_struct_constant(name, nesting, class_name, scope)
387
+ environment = scope&.environment
388
+ return name.delete_prefix("::") if name.start_with?("::")
389
+
390
+ nesting.size.downto(1) do |depth|
391
+ candidate = "#{nesting.first(depth).join('::')}::#{name}"
392
+ return candidate if struct_constant_class?(candidate, environment)
393
+ return nil if struct_constant_bound?(candidate, scope)
394
+ end
395
+ @catalog.constant_ancestry(class_name).each do |ancestor|
396
+ candidate = "#{ancestor}::#{name}"
397
+ return nil if struct_constant_class?(candidate, environment) || struct_constant_bound?(candidate, scope)
398
+ end
399
+ return nil if struct_constant_elsewhere?(name)
400
+ return nil if @catalog.mixes_in_ancestry?(class_name)
401
+
402
+ struct_constant_class?(name, environment) ? name : nil
403
+ end
404
+
405
+ def struct_constant_class?(name, environment)
406
+ @catalog.namespace?(name) || !environment&.nominal_for_name(name).nil?
407
+ end
408
+
409
+ # A walked constant write, or any constant the engine types at the rooted path `::<name>` — an RBS
410
+ # constant declaration included. The probe goes through the plugin-facing `Scope#type_of` on a parsed
411
+ # constant-path node; a name nothing declares types as `Dynamic[top]` and is not bound.
412
+ def struct_constant_bound?(name, scope)
413
+ return true if @catalog.constant?(name)
414
+ return false if scope.nil?
415
+
416
+ node = Prism.parse("::#{name}").value.statements&.body&.first
417
+ type = scope.type_of(node)
418
+ !(type.nil? || type.is_a?(Rigor::Type::Dynamic))
419
+ rescue StandardError
420
+ true
421
+ end
422
+
423
+ def struct_constant_elsewhere?(name)
424
+ @struct_constant_names ||= @catalog.declared_constant_names
425
+ suffix = "::#{name}"
426
+ @struct_constant_names.any? { |declared| declared.end_with?(suffix) }
427
+ end
428
+
353
429
  def instance_chain_lookup(receiver_node, method_name, scope)
354
430
  return nil if scope.nil?
355
431
 
356
432
  receiver_type = scope.type_of(receiver_node)
357
433
  return nil unless receiver_type.is_a?(Rigor::Type::Nominal)
358
434
 
359
- chain_lookup(receiver_type.class_name, method_name, anchor_kind: :instance, mixin_kind: :include)
435
+ chain_lookup(receiver_type.class_name, method_name, anchor_kind: :instance, mixin_kind: :include) ||
436
+ struct_prop_lookup(receiver_type.class_name, method_name, scope)
360
437
  rescue StandardError
361
438
  # `scope.type_of` can raise on unrecognised synthetic nodes; degrade to "no contribution" rather
362
439
  # than bubbling the failure into the dispatcher.
@@ -548,7 +625,7 @@ module Rigor
548
625
  catalog
549
626
  end
550
627
 
551
- errors = CatalogWalker.walk(root: result.value, catalog: sig_catalog, path: path)
628
+ errors = CatalogWalker.walk(root: result.value, catalog: sig_catalog, path: path, struct_catalog: catalog)
552
629
  # ADR-88 WD2 — store Marshal-clean `{kind:, line:, column:}` tuples, not the `ParseError` (it holds a
553
630
  # live Prism node the producer bundle could not serialise). `diagnostics_for_file` rebuilds the
554
631
  # diagnostic from the tuple, at the same 1-based line / `start_column + 1` position `from_node` gave.
@@ -2,15 +2,15 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- require "rigor/inference/declaration_walk"
5
+ require "rigor/reflection"
6
6
 
7
7
  require_relative "serializer_index"
8
8
 
9
9
  module Rigor
10
10
  module Plugin
11
11
  class Typelizer < Rigor::Plugin::Base
12
- # A {Rigor::Inference::DeclarationWalk} collector over one file: the classes it declares, and whether each
13
- # one's own body says `include Typelizer::DSL` / `extend Typelizer::DSL`.
12
+ # A collector over one file, built on {Rigor::Reflection.class_declarations_in}: the classes it declares, and
13
+ # whether each one's own body says `include Typelizer::DSL` / `extend Typelizer::DSL`.
14
14
  #
15
15
  # A `module` that includes the DSL is deliberately not recorded: typelizer registers the module's own
16
16
  # name and `target_serializers` then calls `.descendants` on it, which a plain Module lacks. A class that
@@ -22,8 +22,6 @@ module Rigor
22
22
  # `included do include Typelizer::DSL end` and a `def` that includes it credit nothing to the lexical
23
23
  # class. A `class << self` body is a different node and is not entered either.
24
24
  class SerializerCollector
25
- include Rigor::Inference::DeclarationWalk::Collector
26
-
27
25
  DSL_MODULE = "Typelizer::DSL"
28
26
  DSL_CALLS = %i[include extend].freeze
29
27
 
@@ -33,17 +31,10 @@ module Rigor
33
31
  @dsl = {}
34
32
  end
35
33
 
36
- def on_declaration(node, context, body_context)
37
- return Rigor::Inference::DeclarationWalk::DESCEND unless node.is_a?(Prism::ClassNode)
38
-
39
- name = body_context.prefix.join("::")
40
- unless name.empty?
41
- superclass = superclass_name(node.superclass)
42
- @entries[name] = superclass if superclass || !@entries.key?(name)
43
- @nestings[name] = context.nesting || []
44
- @dsl[name] = true if dsl_in?(node.body) && !body_context.singleton_cref
45
- end
46
- Rigor::Inference::DeclarationWalk::DESCEND
34
+ # Walks `root` (a parsed file's `Prism::Node`) and records what it finds; answers the collector itself.
35
+ def collect(root)
36
+ Rigor::Reflection.class_declarations_in(root).each { |declaration| record(declaration) }
37
+ self
47
38
  end
48
39
 
49
40
  # The classes this file declares, as {SerializerIndex::ClassEntry} values.
@@ -58,6 +49,15 @@ module Rigor
58
49
 
59
50
  private
60
51
 
52
+ def record(declaration)
53
+ name = declaration.name
54
+ node = declaration.node
55
+ superclass = superclass_name(node.superclass)
56
+ @entries[name] = superclass if superclass || !@entries.key?(name)
57
+ @nestings[name] = declaration.nesting
58
+ @dsl[name] = true if dsl_in?(node.body)
59
+ end
60
+
61
61
  # Whether a statement reachable without entering a def, block, lambda or nested declaration is a
62
62
  # receiverless `include` / `extend` of the DSL module.
63
63
  def dsl_in?(node)
@@ -2,8 +2,6 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- require "rigor/inference/declaration_walk"
6
-
7
5
  require_relative "serializer_collector"
8
6
  require_relative "serializer_index"
9
7
 
@@ -30,10 +28,7 @@ module Rigor
30
28
  parsed = Prism.parse(contents)
31
29
  next unless parsed.success?
32
30
 
33
- collector = SerializerCollector.new
34
- Rigor::Inference::DeclarationWalk.run(
35
- parsed.value, [collector], Rigor::Inference::DeclarationWalk::Context.root(nesting: [])
36
- )
31
+ collector = SerializerCollector.new.collect(parsed.value)
37
32
  in_dirs = dir_files.include?(path)
38
33
  entries.concat(collector.class_entries.map { |entry| entry.with(in_dirs: in_dirs) })
39
34
  end
@@ -0,0 +1,6 @@
1
+ module Rigor::Analysis::CheckRules::RuleWalk
2
+ end
3
+
4
+ class Rigor::Analysis::CheckRules::RuleWalk::CollectorDriver
5
+ def initialize: (untyped) -> void
6
+ end
@@ -2,9 +2,8 @@ module Rigor
2
2
  module Analysis
3
3
  module CheckRules
4
4
  def self?.diagnose: (path: String, root: untyped, scope_index: Hash[untyped, Scope]) -> Array[Diagnostic]
5
- def self?.build_node_collectors: (String path, untyped scope_index, ?untyped root) -> Hash[Symbol, untyped]
6
- # authored: returns Rigor::Analysis::CheckRules::RuleWalk::CollectorDriver, which has no RBS declaration yet (#2404); stays `untyped` until it is declared
7
- def self?.node_collector_driver: (Hash[Symbol, untyped] collectors) -> untyped
5
+ def self?.build_node_collectors: (String path, untyped scope_index, ?untyped root, ?plugin_api_surface: bool) -> Hash[Symbol, untyped]
6
+ def self?.node_collector_driver: (Hash[Symbol, untyped] collectors) -> Rigor::Analysis::CheckRules::RuleWalk::CollectorDriver
8
7
  def self?.shadow_verify_converged_collectors: (String path, untyped root, untyped scope_index, Hash[Symbol, untyped]? collectors) -> void
9
8
  def self?.filter_suppressed: (Array[Diagnostic] diagnostics, comments: untyped, disabled_rules: Array[String]) -> Array[Diagnostic]
10
9
  end
@@ -0,0 +1,14 @@
1
+ class Rigor::Cache::Descriptor::PluginEntry
2
+ def initialize: (id: untyped, version: untyped, ?config_hash: untyped) -> void
3
+ def to_h: () -> Hash[String, untyped]
4
+ end
5
+
6
+ class Rigor::Cache::Descriptor
7
+ def initialize: (?files: untyped, ?gems: untyped, ?plugins: untyped, ?configs: untyped, ?dependencies: untyped, ?globs: untyped) -> void
8
+ def self.compose: (*untyped) -> Rigor::Cache::Descriptor
9
+ def cache_key_for: (producer_id: untyped, ?params: untyped) -> String
10
+ def to_canonical_bytes: () -> String
11
+ def to_canonical_hash: () -> Hash[String, untyped]
12
+ def ==: (untyped) -> (false | true)
13
+ def hash: () -> Integer
14
+ end
@@ -19,7 +19,9 @@ module Rigor
19
19
  def hkt_scan_failure: () -> [String, String, String?, Symbol]?
20
20
 
21
21
  def self.default: () -> Environment
22
- def self.for_project: (?root: String, ?libraries: Array[String], ?signature_paths: Array[String | _ToPath]?, ?cache_store: untyped?, ?plugin_registry: untyped?, ?dependency_source_index: untyped?, ?rbs_extended_reporter: untyped?, ?boundary_cross_reporter: untyped?, ?source_rbs_synthesis_reporter: untyped?, ?bundler_bundle_path: String?, ?bundler_auto_detect: bool, ?synthetic_method_index: untyped?, ?project_patched_methods: untyped?) -> Environment
22
+ def self.for_project: (?root: String, ?libraries: Array[String], ?signature_paths: Array[String | _ToPath]?, ?cache_store: untyped?, ?plugin_registry: untyped?, ?dependency_source_index: untyped?, ?rbs_extended_reporter: untyped?, ?boundary_cross_reporter: untyped?, ?source_rbs_synthesis_reporter: untyped?, ?bundler_bundle_path: String?, ?bundler_auto_detect: bool, ?synthetic_method_index: untyped?, ?project_patched_methods: untyped?, ?eager: bool) -> Environment
23
+ # authored: the return is the `signature_paths:` argument's type (authored intent) or the default ["sig"]; sig-gen types the `||` fallback untyped.
24
+ def self.project_signature_root_candidates: (Array[String | _ToPath]? signature_paths) -> Array[String | _ToPath]
23
25
  # authored: the return is the `signature_paths:` argument's type (authored intent) or the default [Pathname]; sig-gen types the `||` fallback untyped.
24
26
  def self.project_signature_roots: (Array[String | _ToPath]? signature_paths, ?root: String) -> Array[String | _ToPath]
25
27
 
@@ -36,6 +38,7 @@ module Rigor
36
38
  def rbs_module?: (String | Symbol name) -> bool
37
39
  def singleton_extended_modules: (String | Symbol name) -> Array[String]
38
40
  def class_ordering: (String | Symbol lhs, String | Symbol rhs) -> ordering
41
+ def prepare_for_typing!: () -> Environment
39
42
  def reflection: () -> untyped?
40
43
  def missing_rbs_gem_owner: (String | Symbol root_constant_name) -> String?
41
44
 
@@ -4,9 +4,20 @@
4
4
  # declared surface and give plugin subclasses (once they enter a check
5
5
  # target) method resolution for the DSL + hook surface.
6
6
  #
7
- # Collaborators that have no RBS yet (Diagnostic, Cache::Descriptor,
8
- # FlowContribution, NodeContext, Prism nodes) are typed `untyped`; the
9
- # precision ceiling is the sparse `sig/` tree, not this file.
7
+ # ADR-128 WD2 — every hook position takes a plugin-facing interface
8
+ # (`sig/rigor/plugin/interfaces.rbs`), a concrete Prism node class, a
9
+ # `Rigor::Type::t` or a core type, instead of `untyped`. A hook's `scope`
10
+ # is non-nil where every engine call site passes one: the per-file hooks
11
+ # receive the seeded `Scope.empty(...)`, `MethodDispatcher#try_plugin_contribution`
12
+ # returns before consulting a plugin when its scope is nil, and
13
+ # `StatementEvaluator#apply_plugin_assertions` has already read its own. The
14
+ # existing `scope.nil?` guards in plugin source stay valid either way.
15
+ #
16
+ # Collaborators that have no RBS yet (FlowContribution) stay `untyped`;
17
+ # the precision ceiling is the sparse `sig/` tree. A parameter row stays
18
+ # lenient even where its type IS declared — `cache_for`'s `?descriptor:` and
19
+ # `IoBoundary#replay` both take a `Cache::Descriptor` and both read `untyped`
20
+ # — by ADR-107 clause 2, not for want of a declaration.
10
21
  class Rigor::Plugin::Base
11
22
  # --- class-level declaration DSLs ---------------------------------
12
23
 
@@ -17,16 +28,32 @@ class Rigor::Plugin::Base
17
28
  def self.producer: (untyped id, ?watch: untyped, ?serialize: untyped, ?deserialize: untyped, ?generation_cap: untyped) { (untyped params) -> untyped } -> Symbol
18
29
  def self.producers: () -> Hash[Symbol, untyped]
19
30
 
20
- def self.node_rule: (untyped node_type) { (*untyped) -> untyped } -> Class
31
+ # One overload per node class the bundled plugins name, so the block's `node` is that class; `Prism::Node` is the
32
+ # fallback for any other (RBS cannot write `singleton(N)` over a type variable). The block runs `instance_exec`'d
33
+ # on the plugin: `file_context` is whatever the `node_file_context` block returned (plugin-defined), and a rule
34
+ # may answer one diagnostic, an array of them, or nil.
35
+ def self.node_rule: (singleton(::Prism::CallNode) node_type) { (::Prism::CallNode node, Rigor::Plugin::_Scope scope, String path, untyped file_context, Rigor::Plugin::NodeContext context) -> (Rigor::Analysis::Diagnostic | Array[Rigor::Analysis::Diagnostic] | nil) } -> singleton(::Prism::CallNode)
36
+ | (singleton(::Prism::ClassNode) node_type) { (::Prism::ClassNode node, Rigor::Plugin::_Scope scope, String path, untyped file_context, Rigor::Plugin::NodeContext context) -> (Rigor::Analysis::Diagnostic | Array[Rigor::Analysis::Diagnostic] | nil) } -> singleton(::Prism::ClassNode)
37
+ | (singleton(::Prism::ProgramNode) node_type) { (::Prism::ProgramNode node, Rigor::Plugin::_Scope scope, String path, untyped file_context, Rigor::Plugin::NodeContext context) -> (Rigor::Analysis::Diagnostic | Array[Rigor::Analysis::Diagnostic] | nil) } -> singleton(::Prism::ProgramNode)
38
+ | (singleton(::Prism::Node) node_type) { (::Prism::Node node, Rigor::Plugin::_Scope scope, String path, untyped file_context, Rigor::Plugin::NodeContext context) -> (Rigor::Analysis::Diagnostic | Array[Rigor::Analysis::Diagnostic] | nil) } -> singleton(::Prism::Node)
21
39
  def self.node_rules: () -> Array[untyped]
22
40
 
23
- def self.node_file_context: () { (untyped root, untyped scope) -> untyped } -> void
24
- def self.node_file_context_block: () -> (^(untyped root, untyped scope) -> untyped)?
41
+ def self.node_file_context: () { (::Prism::ProgramNode root, Rigor::Plugin::_Scope scope) -> untyped } -> void
42
+ def self.node_file_context_block: () -> (^(::Prism::ProgramNode root, Rigor::Plugin::_Scope scope) -> untyped)?
25
43
 
26
- def self.dynamic_return: (?receivers: (Array[String] | ^() -> Array[String])?, ?methods: (Array[untyped] | ^() -> Array[untyped])?, ?file_methods: (^(untyped path) -> Array[untyped])?) { (untyped call_node, untyped scope) -> untyped } -> nil
44
+ # A rule with a static `methods:` Array only ever runs its block on a `CallNode`: the name gate
45
+ # (`Registry#build_name_gates`, `Base#dynamic_return_rule_applies?`) matches on the call's name, which
46
+ # none of the other nodes in the union below has, so the gate declines them (#2681, #2764). Every other
47
+ # declaration shape is offered the whole union. A callable `methods:` also only ever sees a `CallNode`
48
+ # at runtime, since the same gate declines the nameless nodes, but RBS cannot tell a callable from an
49
+ # Array argument by overload alone, so it keeps the union, which is sound.
50
+ def self.dynamic_return: (?receivers: (Array[String] | ^() -> Array[String])?, methods: Array[untyped], ?file_methods: (^(String path) -> Array[untyped])?) { (::Prism::CallNode call_node, Rigor::Plugin::_Scope scope) -> Rigor::Type::t? } -> nil
51
+ | (?receivers: (Array[String] | ^() -> Array[String])?, ?methods: (Array[untyped] | ^() -> Array[untyped])?, ?file_methods: (^(String path) -> Array[untyped])?) { (::Prism::CallNode | ::Prism::CallOperatorWriteNode | ::Prism::CallOrWriteNode | ::Prism::CallAndWriteNode | ::Prism::IndexOperatorWriteNode | ::Prism::IndexOrWriteNode | ::Prism::IndexAndWriteNode | ::Prism::BlockArgumentNode call_node, Rigor::Plugin::_Scope scope) -> Rigor::Type::t? } -> nil
27
52
  def self.dynamic_returns: () -> Array[untyped]
28
53
 
29
- def self.narrowing_facts: (methods: Array[untyped]) { (untyped call_node, untyped scope) -> untyped } -> nil
54
+ # The block answers the facts that hold after the call returns normally, or nil. The element is a
55
+ # `Rigor::FlowContribution::Fact`, which has no RBS declaration yet, so it stays `untyped`.
56
+ def self.narrowing_facts: (methods: Array[untyped]) { (::Prism::CallNode call_node, Rigor::Plugin::_Scope scope) -> Array[untyped]? } -> nil
30
57
  # The engine-facing reader for the declared `narrowing_facts` rules. Named
31
58
  # `type_specifiers` before ADR-80; that spelling was removed in 0.3.0.
32
59
  def self.narrowing_facts_rules: () -> Array[untyped]
@@ -35,15 +62,16 @@ class Rigor::Plugin::Base
35
62
 
36
63
  # --- instance surface --------------------------------------------
37
64
 
38
- attr_reader services: untyped
39
- attr_reader config: untyped
65
+ attr_reader services: Rigor::Plugin::_Services
66
+ # Schema-driven per plugin, so the values stay `untyped` (ADR-128 WD2).
67
+ attr_reader config: Hash[String, untyped]
40
68
 
41
- def initialize: (services: untyped, ?config: untyped) -> void
69
+ def initialize: (services: Rigor::Plugin::_Services, ?config: Hash[String, untyped]) -> void
42
70
 
43
71
  # Overridable lifecycle / extension hooks (default no-ops).
44
- def init: (untyped services) -> nil
45
- def prepare: (untyped services) -> nil
46
- def diagnostics_for_file: (path: untyped, scope: untyped, root: untyped) -> Array[untyped]
72
+ def init: (Rigor::Plugin::_Services services) -> nil
73
+ def prepare: (Rigor::Plugin::_Services services) -> nil
74
+ def diagnostics_for_file: (path: String, scope: Rigor::Plugin::_Scope, root: ::Prism::ProgramNode) -> Array[Rigor::Analysis::Diagnostic]
47
75
  # authored: hook-contract widening — the element type comes from a plugin gem sig-gen never
48
76
  # sees, so no engine work would let it answer; the declared `Array[Rigor::Plugin::TemplateUnit]`
49
77
  # over a bare-`[]` default body is the author's contract.
@@ -54,12 +82,19 @@ class Rigor::Plugin::Base
54
82
  def declared_members: (untyped class_name) -> Array[untyped]
55
83
 
56
84
  # Engine-executed dispatch over the declared DSLs.
57
- def node_rule_diagnostics: (path: untyped, scope: untyped, root: untyped) -> Array[untyped]
85
+ def node_rule_diagnostics: (path: String, scope: Rigor::Plugin::_Scope, root: ::Prism::ProgramNode) -> Array[untyped]
58
86
  # sig-gen gap: #1007 — the true `return result if result` branch (base.rb:594) flows through an
59
87
  # `instance_exec`'d, plugin-supplied Proc that sig-gen's inference does not fold into the return
60
88
  # join, so it proposes the literal fallback `nil` instead of the honest contract.
61
- def dynamic_return_type: (call_node: untyped, scope: untyped, receiver_type: untyped) -> Rigor::Type::t?
62
- def narrowing_facts_for: (call_node: untyped, scope: untyped) -> Array[untyped]
89
+ # `call_node` is a `CallNode` except where the engine dispatches a compound write or a block argument. The
90
+ # compound writes (`x.attr op= v`, `x.attr ||= v`, `x.attr &&= v`, and the `x[i]` forms) reach a rule through
91
+ # `ExpressionTyper#attribute_compound_read_type`, `#call_operator_write_type_for` and `#index_read_dispatch`;
92
+ # the `&:symbol` block arguments through `ExpressionTyper#symbol_block_return_type` and `#apply_hash_block`.
93
+ # A rule the name gate does not screen out (a `receivers:`-only rule, for one) is offered those nodes, so the
94
+ # `dynamic_return` block union above is the full set. A rule that reads a `CallNode`-only member (`receiver`,
95
+ # `name`, `arguments`) must guard with `call_node.is_a?(::Prism::CallNode)` first (#2717).
96
+ def dynamic_return_type: (call_node: ::Prism::CallNode | ::Prism::CallOperatorWriteNode | ::Prism::CallOrWriteNode | ::Prism::CallAndWriteNode | ::Prism::IndexOperatorWriteNode | ::Prism::IndexOrWriteNode | ::Prism::IndexAndWriteNode | ::Prism::BlockArgumentNode, scope: Rigor::Plugin::_Scope, receiver_type: Rigor::Type::t) -> Rigor::Type::t?
97
+ def narrowing_facts_for: (call_node: ::Prism::CallNode, scope: Rigor::Plugin::_Scope) -> Array[untyped]
63
98
 
64
99
  # Authoring helpers.
65
100
  def diagnostic: (untyped node, path: untyped, message: untyped, ?severity: untyped, ?rule: untyped, ?location: untyped) -> Rigor::Analysis::Diagnostic
@@ -84,6 +119,5 @@ class Rigor::Plugin::Base
84
119
  def effect_ancestry: () -> Array[Rigor::Plugin::EffectAncestry]
85
120
  def io_boundary: () -> Rigor::Plugin::IoBoundary
86
121
  def cache_for: (untyped producer_id, ?params: untyped, ?descriptor: untyped) -> Proc
87
- # authored: returns Rigor::Cache::Descriptor::PluginEntry, which has no RBS declaration yet (#2404); stays `untyped` until it is declared
88
- def plugin_entry: () -> untyped
122
+ def plugin_entry: () -> Rigor::Cache::Descriptor::PluginEntry
89
123
  end
@@ -0,0 +1,51 @@
1
+ # ADR-128 — the plugin-facing structural interfaces: what a hook may call on the engine objects it receives.
2
+ #
3
+ # The criterion (ADR-128): a method is public plugin API only if it is declared on `Plugin::Base`, on one of the
4
+ # interfaces below, or in a namespace `docs/internal-spec/public-api.md` lists as public. Being reachable at
5
+ # runtime does not make a method public. `spec/rigor/plugin_boundary_spec.rb` gates the bundled plugins against
6
+ # these member lists, so adding a member here is an API promise, and `public_api_drift_spec.rb` pins the sets.
7
+ #
8
+ # Each interface is backed by a concrete engine class (the `BACKING` table in `spec/support/plugin_boundary.rb`).
9
+ # Engine signatures keep those concrete classes (ADR-128 WD5); an interface appears only at a plugin-facing
10
+ # position, in `base.rbs`.
11
+ module Rigor
12
+ module Plugin
13
+ # The scope a hook receives. Backed by `Rigor::Scope`, which has far more members (about 200); only these are
14
+ # promised. `environment` is non-nil, matching `Scope#environment`.
15
+ interface _Scope
16
+ def type_of: (::Prism::Node node) -> Rigor::Type::t
17
+ def self_type: () -> Rigor::Type::t?
18
+ def local: (String | Symbol name) -> Rigor::Type::t?
19
+ def source_path: () -> String?
20
+ def with_local: (String | Symbol name, Rigor::Type::t type) -> Rigor::Plugin::_Scope
21
+ def environment: () -> Rigor::Plugin::_Environment
22
+ end
23
+
24
+ # The project's RBS and class-registry handle, backed by `Rigor::Environment`.
25
+ interface _Environment
26
+ def nominal_for_name: (String | Symbol name) -> Rigor::Type::Nominal?
27
+ def rbs_module?: (String | Symbol name) -> bool
28
+ end
29
+
30
+ # The services container `Plugin::Base#init` / `#prepare` receive, backed by `Rigor::Plugin::Services`.
31
+ # `reflection` and `type` are public namespaces (`Rigor::Reflection`, `Rigor::Type::Combinator`) that a plugin
32
+ # also reaches by constant, so they are typed as those modules rather than narrowed to an interface.
33
+ interface _Services
34
+ def fact_store: () -> Rigor::Plugin::_FactStore
35
+ def configuration: () -> Rigor::Plugin::_Configuration
36
+ def reflection: () -> singleton(Rigor::Reflection)
37
+ def type: () -> singleton(Rigor::Type::Combinator)
38
+ end
39
+
40
+ # The per-run cross-plugin fact store, backed by `Rigor::Plugin::FactStore`.
41
+ interface _FactStore
42
+ def publish: (plugin_id: String | Symbol, name: Symbol, value: untyped) -> nil
43
+ def read: (plugin_id: String | Symbol, name: Symbol) -> untyped
44
+ end
45
+
46
+ # The project configuration, backed by `Rigor::Configuration`.
47
+ interface _Configuration
48
+ def paths: () -> Array[String]
49
+ end
50
+ end
51
+ end
@@ -14,8 +14,7 @@ class Rigor::Plugin::IoBoundary
14
14
  # whose parameter is undeclared.
15
15
  def refusal_summary: () -> { count: Integer, first_path: String?, nearest_root: String? }?
16
16
  def open_url: (untyped url) -> String
17
- # Returns a frozen `Rigor::Cache::Descriptor` (no RBS yet -> untyped).
18
- # authored: returns Rigor::Cache::Descriptor, which has no RBS declaration yet (#2404); stays `untyped` until it is declared
19
- def cache_descriptor: () -> untyped
17
+ # The returned descriptor is frozen.
18
+ def cache_descriptor: () -> Rigor::Cache::Descriptor
20
19
  def replay: (untyped descriptor) -> nil
21
20
  end
@@ -0,0 +1,21 @@
1
+ module Rigor
2
+ module Plugin
3
+ # ADR-37 slice 1d — the lexical context of a node, the fifth argument of a `node_rule` block.
4
+ class NodeContext
5
+ EMPTY: nil
6
+
7
+ attr_reader ancestors: Array[::Prism::Node]
8
+
9
+ def initialize: (Array[::Prism::Node] ancestors) -> void
10
+ # The three finders return the innermost ancestor of a given kind: a `DefNode`, a `ClassNode` or
11
+ # `ModuleNode`, and a `CallNode` respectively. `enclosing_def` is sig-gen's proposal (#2719, #2914); the other
12
+ # two blocks (`||`, `&&`) are not a single class test, so their bodies still infer `Prism::Node?`. A narrower
13
+ # declaration would be an unmarked sig-gen divergence (ADR-107).
14
+ # Known Steep 2.0.0 limitation (surfaced by `library "prism"`, #2718): it does not narrow `rfind` through an
15
+ # `is_a?` block, so it warns that the body is `Prism::Node?`; the declaration keeps the precise shape.
16
+ def enclosing_def: () -> (Prism::DefNode | nil)
17
+ def enclosing_module: () -> ::Prism::Node?
18
+ def enclosing_block: (Symbol method_name) -> ::Prism::Node?
19
+ end
20
+ end
21
+ end
@@ -19,5 +19,42 @@ module Rigor
19
19
  def self?.class_type_param_names: (String | Symbol class_name, ?scope: Scope) -> Array[Symbol]
20
20
  def self?.discovered_class?: (String | Symbol class_name, ?scope: Scope) -> bool
21
21
  def self?.discovered_method?: (String | Symbol class_name, String | Symbol method_name, ?kind: Symbol, ?scope: Scope) -> bool
22
+ def self?.project_method_defined: (String | Symbol? class_name, String | Symbol method_name, kind: :instance | :singleton, source: :def | :discovered | :either, scope: Scope) -> Trinary
23
+ def self?.rbs_method_defined: (String | Symbol? class_name, String | Symbol method_name, kind: :instance | :singleton, scope: Scope) -> Trinary
24
+ def self?.ancestry_reaches: (String | Symbol? class_name, Array[String | Symbol] ancestor_names, kind: :instance | :singleton, scope: Scope) -> Trinary
25
+ # sig-gen gap: #2302 — the collector's `@classes = []` is grown by `<<`, so sig-gen reads the array as `Array[untyped]`.
26
+ def self?.class_declarations_in: (Prism::Node root) ?{ (DeclaredCall call) -> void } -> Array[ClassDeclaration]
27
+
28
+ # One `class` header of a parsed file, as `class_declarations_in` reports it.
29
+ class ClassDeclaration < Data
30
+ # sig-gen gap: #1150 — `Data.define` members carry no type, so sig-gen proposes `untyped`; the shape is hand-written.
31
+ attr_reader name: String
32
+ # sig-gen gap: #1150 — `Data.define` members carry no type, so sig-gen proposes `untyped`; the shape is hand-written.
33
+ attr_reader nesting: Array[String]
34
+ # sig-gen gap: #1150 — `Data.define` members carry no type, so sig-gen proposes `untyped`; the shape is hand-written.
35
+ attr_reader node: Prism::ClassNode
36
+
37
+ # sig-gen gap: #1150 — `Data.define` members carry no type, so the constructor's keywords are `untyped` in sig-gen.
38
+ def self.new: (name: String, nesting: Array[String], node: Prism::ClassNode) -> instance
39
+ | (String name, Array[String] nesting, Prism::ClassNode node) -> instance
40
+ # sig-gen gap: #1150 — `Data.define` members carry no type, so the constructor's keywords are `untyped` in sig-gen.
41
+ def self.[]: (name: String, nesting: Array[String], node: Prism::ClassNode) -> instance
42
+ | (String name, Array[String] nesting, Prism::ClassNode node) -> instance
43
+ end
44
+
45
+ # One call `class_declarations_in` yields: the `Prism::CallNode` and the class or module it lexically sits in.
46
+ class DeclaredCall < Data
47
+ # sig-gen gap: #1150 — `Data.define` members carry no type, so sig-gen proposes `untyped`; the shape is hand-written.
48
+ attr_reader node: Prism::CallNode
49
+ # sig-gen gap: #1150 — `Data.define` members carry no type, so sig-gen proposes `untyped`; the shape is hand-written.
50
+ attr_reader owner: String?
51
+
52
+ # sig-gen gap: #1150 — `Data.define` members carry no type, so the constructor's keywords are `untyped` in sig-gen.
53
+ def self.new: (node: Prism::CallNode, owner: String?) -> instance
54
+ | (Prism::CallNode node, String? owner) -> instance
55
+ # sig-gen gap: #1150 — `Data.define` members carry no type, so the constructor's keywords are `untyped` in sig-gen.
56
+ def self.[]: (node: Prism::CallNode, owner: String?) -> instance
57
+ | (Prism::CallNode node, String? owner) -> instance
58
+ end
22
59
  end
23
60
  end
data/sig/rigor/scope.rbs CHANGED
@@ -30,7 +30,7 @@ module Rigor
30
30
  attr_reader struct_fold_safe_locals: Set[Symbol]
31
31
  attr_reader opaque_block_self: bool
32
32
  # Issue #1759 — `:refinement` (REFINEMENT_BLOCK_SELF) directly in a `refine X do … end` block body.
33
- attr_reader block_self_unknown: bool | :refinement
33
+ attr_reader block_self_unknown: bool | :refinement | :preserved
34
34
  attr_reader singleton_class_body: bool
35
35
  attr_reader lexical_nesting: Array[String]?
36
36
  # Issue #1429 — a guard's narrowing of a constant reference, keyed by its spelling.
@@ -100,6 +100,12 @@ module Rigor
100
100
  def run_generation: () -> Object?
101
101
  def record_dynamic_origin: (untyped node, Symbol cause) -> Scope
102
102
  def record_optimistic_origin: (untyped node, Symbol cause) -> Scope
103
+ # sig-gen gap: #2840 — the reader returns through a sentinel `equal?` guard over `Hash#fetch`; sig-gen emits nothing.
104
+ def dynamic_origin: (untyped node) -> Symbol?
105
+ # sig-gen gap: #2840 — the reader returns through a sentinel `equal?` guard over `Hash#fetch`; sig-gen emits nothing.
106
+ def optimistic_origin: (untyped node) -> Symbol?
107
+ # sig-gen gap: #2840 — the reader returns through a sentinel `equal?` guard over `Hash#fetch`; sig-gen emits nothing.
108
+ def void_origin: (untyped node) -> Inference::VoidOrigin?
103
109
 
104
110
  def record_void_origin: (untyped node, Inference::VoidOrigin origin) -> Scope
105
111
 
@@ -219,12 +225,16 @@ module Rigor
219
225
 
220
226
  def self.empty: (?environment: Environment, ?source_path: String?) -> Scope
221
227
 
222
- def initialize: (environment: Environment, locals: Hash[Symbol, Type::t], ?fact_store: Analysis::FactStore, ?self_type: Type::t?, ?ivars: Hash[Symbol, Type::t], ?cvars: Hash[Symbol, Type::t], ?globals: Hash[Symbol, Type::t], ?discovery: DiscoveryIndex, ?source_path: String?, ?opaque_block_self: bool, ?block_self_unknown: bool | :refinement, ?singleton_class_body: bool, ?dynamic_origins: Hash[untyped, Symbol], ?indexed_narrowings: Hash[IndexedKey, Type::t], ?method_chain_narrowings: Hash[ChainKey, Type::t], ?declaration_sourced: Set[[Symbol, Symbol] | [Symbol, Symbol, Symbol]], ?published_constant_sourced: Set[[Symbol, Symbol]], ?struct_fold_safe_locals: Set[Symbol], ?lexical_nesting: Array[String]?, ?local_origins: Hash[Symbol, Symbol], ?ivar_origins: Hash[Symbol, Symbol], ?void_origins: Hash[untyped, Inference::VoidOrigin], ?plugin_typed_calls: Hash[untyped, bool], ?optimistic_origins: Hash[untyped, Symbol], ?optimistic_locals: Hash[Symbol, Symbol | Inference::OptimisticOrigin::BoundMark], ?optimistic_ivars: Hash[Symbol, Symbol | Inference::OptimisticOrigin::BoundMark], ?construction_facts: Hash[Symbol, Set[Symbol]], ?repeated_or_writes: Hash[untyped, bool], ?match_frame: untyped, ?constant_narrowings: Hash[String, Type::t], ?guard_records: Hash[[Symbol, Symbol | String], Type::t], ?bot_guard_classes: Hash[[Symbol, Symbol | String], Array[String]], ?declared_refinements: Array[String | Symbol]) -> void
228
+ def initialize: (environment: Environment, locals: Hash[Symbol, Type::t], ?fact_store: Analysis::FactStore, ?self_type: Type::t?, ?ivars: Hash[Symbol, Type::t], ?cvars: Hash[Symbol, Type::t], ?globals: Hash[Symbol, Type::t], ?discovery: DiscoveryIndex, ?source_path: String?, ?opaque_block_self: bool, ?block_self_unknown: bool | :refinement | :preserved, ?singleton_class_body: bool, ?dynamic_origins: Hash[untyped, Symbol], ?indexed_narrowings: Hash[IndexedKey, Type::t], ?method_chain_narrowings: Hash[ChainKey, Type::t], ?declaration_sourced: Set[[Symbol, Symbol] | [Symbol, Symbol, Symbol]], ?published_constant_sourced: Set[[Symbol, Symbol]], ?struct_fold_safe_locals: Set[Symbol], ?lexical_nesting: Array[String]?, ?local_origins: Hash[Symbol, Symbol], ?ivar_origins: Hash[Symbol, Symbol], ?void_origins: Hash[untyped, Inference::VoidOrigin], ?plugin_typed_calls: Hash[untyped, bool], ?optimistic_origins: Hash[untyped, Symbol], ?optimistic_locals: Hash[Symbol, Symbol | Inference::OptimisticOrigin::BoundMark], ?optimistic_ivars: Hash[Symbol, Symbol | Inference::OptimisticOrigin::BoundMark], ?construction_facts: Hash[Symbol, Set[Symbol]], ?repeated_or_writes: Hash[untyped, bool], ?match_frame: untyped, ?constant_narrowings: Hash[String, Type::t], ?guard_records: Hash[[Symbol, Symbol | String], Type::t], ?bot_guard_classes: Hash[[Symbol, Symbol | String], Array[String]], ?declared_refinements: Array[String | Symbol]) -> void
223
229
  def with_source_path: (String? path) -> Scope
224
230
  def with_struct_fold_safe: (Set[Symbol] locals) -> Scope
225
231
  def with_lexical_nesting: (Array[String]? chain) -> Scope
226
232
  def struct_fold_safe?: (String | Symbol name) -> bool
233
+ def entering_block_without_gradual_marks: (Prism::Node block_node) -> Scope
227
234
  def entering_opaque_block: () -> Scope
235
+ PRESERVED_BLOCK_SELF: :preserved
236
+ def entering_self_preserving_block: () -> Scope
237
+ def walked_block_self_unknown?: () -> bool
228
238
  def block_self_unknown?: () -> bool
229
239
  REFINEMENT_BLOCK_SELF: :refinement
230
240
  def refinement_block_self?: () -> bool
@@ -297,6 +307,9 @@ module Rigor
297
307
  def with_inferred_param_mark: (String | Symbol name) -> Scope
298
308
  def without_inferred_param_mark: (String | Symbol name) -> Scope
299
309
  def inferred_param?: (String | Symbol name) -> bool
310
+ def with_gradual_narrowing_mark: (String | Symbol name) -> Scope
311
+ def gradual_narrowing?: (String | Symbol name) -> bool
312
+ def gradual_narrowing_names: () -> Array[Symbol]
300
313
  def forget_match_globals: () -> Scope
301
314
  def untyped_match_globals: () -> Scope
302
315
  def match_globals_bound?: () -> bool
@@ -321,8 +334,13 @@ module Rigor
321
334
  def def_node_slot: (untyped class_name, untyped method_name, ?Symbol kind) -> untyped
322
335
  def user_def_for: (String | Symbol class_name, String | Symbol method_name) -> ::Prism::DefNode?
323
336
  def user_def_through_ancestors: (String | Symbol class_name, String | Symbol method_name, ?name_memo: Hash[untyped, untyped]) -> ([::Prism::DefNode, String] | [nil, nil])
337
+ # Returns `[opaque_flag, recorder_keys]`. Steep 2.0.0 cannot verify a tuple through the
338
+ # `Array#freeze` on the returned literal and reports a MethodBodyTypeMismatch warning; the
339
+ # declaration keeps the precise shape because `rigor sig-gen` proves it.
324
340
  def subclassing_opacity: () -> [false | true, Array[untyped]]
325
- def user_instance_def_owners: (untyped) -> Array[untyped]
341
+ # authored: each owner is a frozen `[class_name, open_to_override]` pair. sig-gen types the lazily built
342
+ # `(owners ||= []) <<` array as `Array[untyped]`; the body keeps it to avoid `Hash#filter_map`'s per-entry pair.
343
+ def user_instance_def_owners: (untyped) -> Array[Array[untyped]]
326
344
  def discovered_method_through_ancestors?: (String | Symbol? class_name, String | Symbol method_name, Symbol kind, ?name_memo: Hash[untyped, untyped]) -> bool
327
345
  def external_ancestor_name_candidates: (String | Symbol class_name, ?name_memo: Hash[untyped, untyped]) -> Array[Array[String]]
328
346
  def enqueue_ancestors: (String current, Array[String] queue, Hash[untyped, untyped] name_memo, ?mixins: bool) -> void
@@ -357,6 +375,7 @@ module Rigor
357
375
  def data_member_layout: (String | Symbol class_name) -> Array[Symbol]?
358
376
  def struct_member_layout: (String | Symbol class_name) -> { members: Array[Symbol], keyword_init: bool }?
359
377
  def includes_of: (String | Symbol class_name) -> Array[String]
378
+ def prepends_of: (String | Symbol class_name) -> Array[String]
360
379
  # sig-gen gap: #2302 — `names = []` grown by `concat` reads `Array[untyped]`, so the element type is
361
380
  # hand-written, matching {#includes_of}.
362
381
  def singleton_extends_of: (String | Symbol class_name) -> Array[String]
@@ -22,6 +22,7 @@ module Rigor
22
22
  def and: (Trinary other) -> Trinary
23
23
  def or: (Trinary other) -> Trinary
24
24
  def ==: (untyped other) -> bool
25
+ alias eql? ==
25
26
  def hash: () -> Integer
26
27
  def to_s: () -> String
27
28
  def inspect: () -> String
data/sig/rigor/type.rbs CHANGED
@@ -56,8 +56,8 @@ module Rigor
56
56
  end
57
57
 
58
58
  class Dynamic
59
- attr_reader static_facet: Type::t
60
- def initialize: (Type::t static_facet) -> void
59
+ attr_reader static_facet: Type::t | Interface
60
+ def initialize: (Type::t | Interface static_facet) -> void
61
61
  def describe: (?Symbol verbosity) -> String
62
62
  def erase_to_rbs: () -> "untyped"
63
63
  def top: () -> Trinary
@@ -169,6 +169,11 @@ module Rigor
169
169
  def inspect: () -> String
170
170
  end
171
171
 
172
+ # ADR-130 — a `Rigor::Plugin` interface, carried only as a `Dynamic`'s facet (`Dynamic[Rigor::Plugin::_Scope]`), so
173
+ # it is not a member of `Type::t`. Declared for that name only; its members are read from the source.
174
+ class Interface
175
+ end
176
+
172
177
  class Union
173
178
  attr_reader members: Array[Type::t]
174
179
  def initialize: (Array[Type::t] members) -> void
@@ -446,7 +451,8 @@ module Rigor
446
451
  def self?.top: () -> Top
447
452
  def self?.bot: () -> Bot
448
453
  def self?.untyped: () -> Dynamic
449
- def self?.dynamic: (Type::t static_facet) -> Dynamic
454
+ def self?.untyped?: (Type::t? type) -> bool
455
+ def self?.dynamic: (Type::t | Interface static_facet) -> Dynamic
450
456
  def self?.nominal_of: (Module | String class_name_or_object, ?type_args: Array[Type::t]) -> Nominal
451
457
  def self?.singleton_of: (Module | String class_name_or_object) -> Singleton
452
458
  def self?.constant_of: (untyped value) -> Constant
data/sig/rigor.rbs CHANGED
@@ -165,8 +165,9 @@ module Rigor
165
165
  # sig-gen gap: #1008 — `result` is populated by `collect_return_summaries` mutating it by
166
166
  # reference from a sibling method; sig-gen folds only the `{}` seed and the empty-memo early
167
167
  # return, so it proposes the seed shape instead of the honest, usually-non-empty contract.
168
- # Keys are `[path, "Class#method"]` pairs; values are `{ keys:, returns:, effects: }` bundles.
169
- def return_summaries: () -> Hash[[String, String], Hash[Symbol, untyped]]
168
+ # Keys are `[path, "Class#method"]` pairs; values are `{ keys:, returns:, effects: }` bundles, or
169
+ # nil for a def whose keys the run cannot list. nil for a pool run, which harvests none (#3160).
170
+ def return_summaries: () -> Hash[[String, String], Hash[Symbol, untyped]?]?
170
171
  # ADR-67 — `[class_name, method_name, kind] => { param_name => Type }`.
171
172
  def param_inferred_types: () -> Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
172
173
  def collect_param_inference_table: (untyped files) -> Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]