rigortype 0.1.15 → 0.1.17

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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/exe/rigor +19 -0
  4. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
  5. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +209 -0
  6. data/lib/rigor/analysis/check_rules.rb +174 -71
  7. data/lib/rigor/analysis/dependency_recorder.rb +122 -0
  8. data/lib/rigor/analysis/diagnostic.rb +58 -0
  9. data/lib/rigor/analysis/incremental.rb +162 -0
  10. data/lib/rigor/analysis/incremental_session.rb +337 -0
  11. data/lib/rigor/analysis/rule_catalog.rb +48 -0
  12. data/lib/rigor/analysis/runner.rb +485 -29
  13. data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
  14. data/lib/rigor/analysis/worker_session.rb +3 -2
  15. data/lib/rigor/builtins/static_return_refinements.rb +7 -1
  16. data/lib/rigor/cache/descriptor.rb +56 -51
  17. data/lib/rigor/cache/incremental_snapshot.rb +147 -0
  18. data/lib/rigor/cache/rbs_cache_producer.rb +30 -0
  19. data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
  20. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
  21. data/lib/rigor/cache/rbs_constant_table.rb +2 -8
  22. data/lib/rigor/cache/rbs_environment.rb +2 -8
  23. data/lib/rigor/cache/rbs_instance_definitions.rb +3 -16
  24. data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
  25. data/lib/rigor/cache/store.rb +99 -1
  26. data/lib/rigor/cli/annotate_command.rb +2 -7
  27. data/lib/rigor/cli/baseline_command.rb +2 -7
  28. data/lib/rigor/cli/command.rb +47 -0
  29. data/lib/rigor/cli/coverage_command.rb +3 -23
  30. data/lib/rigor/cli/coverage_renderer.rb +3 -8
  31. data/lib/rigor/cli/diff_command.rb +3 -7
  32. data/lib/rigor/cli/explain_command.rb +2 -7
  33. data/lib/rigor/cli/lsp_command.rb +3 -7
  34. data/lib/rigor/cli/mcp_command.rb +3 -7
  35. data/lib/rigor/cli/options.rb +57 -0
  36. data/lib/rigor/cli/plugin_command.rb +3 -7
  37. data/lib/rigor/cli/plugins_command.rb +52 -10
  38. data/lib/rigor/cli/plugins_renderer.rb +86 -1
  39. data/lib/rigor/cli/renderable.rb +26 -0
  40. data/lib/rigor/cli/sig_gen_command.rb +2 -7
  41. data/lib/rigor/cli/skill_command.rb +3 -7
  42. data/lib/rigor/cli/triage_command.rb +2 -7
  43. data/lib/rigor/cli/type_of_command.rb +5 -38
  44. data/lib/rigor/cli/type_of_renderer.rb +4 -9
  45. data/lib/rigor/cli/type_scan_command.rb +3 -23
  46. data/lib/rigor/cli/type_scan_renderer.rb +4 -9
  47. data/lib/rigor/cli.rb +260 -48
  48. data/lib/rigor/configuration/dependencies.rb +18 -1
  49. data/lib/rigor/configuration/severity_profile.rb +22 -3
  50. data/lib/rigor/configuration.rb +13 -3
  51. data/lib/rigor/environment/rbs_loader.rb +335 -4
  52. data/lib/rigor/environment.rb +8 -2
  53. data/lib/rigor/inference/block_parameter_binder.rb +1 -2
  54. data/lib/rigor/inference/budget_trace.rb +137 -0
  55. data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
  56. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
  57. data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
  58. data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
  59. data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
  60. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
  61. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
  62. data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
  63. data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
  64. data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
  65. data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
  66. data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
  67. data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
  68. data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
  69. data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
  70. data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
  71. data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
  72. data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
  73. data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
  74. data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
  75. data/lib/rigor/inference/expression_typer.rb +149 -22
  76. data/lib/rigor/inference/hkt_reducer.rb +2 -0
  77. data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
  78. data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
  79. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
  80. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
  81. data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
  82. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
  83. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  84. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
  85. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
  86. data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
  87. data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
  88. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
  89. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +100 -23
  90. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
  91. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
  93. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
  94. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
  95. data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
  96. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
  97. data/lib/rigor/inference/method_dispatcher.rb +147 -60
  98. data/lib/rigor/inference/narrowing.rb +202 -5
  99. data/lib/rigor/inference/precision_scanner.rb +60 -1
  100. data/lib/rigor/inference/scope_indexer.rb +257 -11
  101. data/lib/rigor/inference/statement_evaluator.rb +110 -26
  102. data/lib/rigor/inference/synthetic_method_index.rb +23 -4
  103. data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
  104. data/lib/rigor/language_server/buffer_resolution.rb +33 -0
  105. data/lib/rigor/language_server/completion_provider.rb +4 -4
  106. data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
  107. data/lib/rigor/language_server/folding_range_provider.rb +4 -4
  108. data/lib/rigor/language_server/hover_provider.rb +4 -4
  109. data/lib/rigor/language_server/selection_range_provider.rb +4 -4
  110. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  111. data/lib/rigor/plugin/additional_initializer.rb +108 -0
  112. data/lib/rigor/plugin/base.rb +337 -2
  113. data/lib/rigor/plugin/box.rb +64 -0
  114. data/lib/rigor/plugin/inflector.rb +121 -0
  115. data/lib/rigor/plugin/isolation.rb +191 -0
  116. data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
  117. data/lib/rigor/plugin/macro.rb +1 -0
  118. data/lib/rigor/plugin/manifest.rb +120 -23
  119. data/lib/rigor/plugin/node_context.rb +62 -0
  120. data/lib/rigor/plugin/registry.rb +49 -1
  121. data/lib/rigor/plugin.rb +3 -0
  122. data/lib/rigor/rbs_extended/conformance_checker.rb +208 -0
  123. data/lib/rigor/rbs_extended.rb +39 -0
  124. data/lib/rigor/scope.rb +123 -9
  125. data/lib/rigor/sig_gen/generator.rb +2 -3
  126. data/lib/rigor/sig_gen/observation_collector.rb +2 -2
  127. data/lib/rigor/source/literals.rb +118 -0
  128. data/lib/rigor/source/node_walker.rb +26 -0
  129. data/lib/rigor/source.rb +1 -0
  130. data/lib/rigor/type/acceptance_router.rb +19 -0
  131. data/lib/rigor/type/accepts_result.rb +3 -10
  132. data/lib/rigor/type/app.rb +3 -7
  133. data/lib/rigor/type/bot.rb +2 -3
  134. data/lib/rigor/type/bound_method.rb +5 -12
  135. data/lib/rigor/type/combinator.rb +23 -1
  136. data/lib/rigor/type/constant.rb +2 -3
  137. data/lib/rigor/type/data_class.rb +80 -0
  138. data/lib/rigor/type/data_instance.rb +100 -0
  139. data/lib/rigor/type/difference.rb +5 -10
  140. data/lib/rigor/type/dynamic.rb +5 -10
  141. data/lib/rigor/type/hash_shape.rb +5 -15
  142. data/lib/rigor/type/integer_range.rb +5 -10
  143. data/lib/rigor/type/intersection.rb +5 -10
  144. data/lib/rigor/type/nominal.rb +5 -10
  145. data/lib/rigor/type/refined.rb +5 -10
  146. data/lib/rigor/type/singleton.rb +5 -10
  147. data/lib/rigor/type/top.rb +2 -3
  148. data/lib/rigor/type/tuple.rb +5 -10
  149. data/lib/rigor/type/union.rb +69 -10
  150. data/lib/rigor/type.rb +2 -0
  151. data/lib/rigor/value_semantics.rb +77 -0
  152. data/lib/rigor/version.rb +1 -1
  153. data/lib/rigor.rb +2 -0
  154. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
  155. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
  156. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
  157. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
  158. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
  159. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
  160. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
  161. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
  162. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
  163. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
  164. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  165. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
  166. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
  167. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
  168. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
  169. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
  170. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
  171. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  172. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
  173. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
  174. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
  175. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
  176. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
  177. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
  178. data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
  179. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
  180. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
  181. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
  182. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
  183. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
  184. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
  185. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
  186. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
  187. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
  188. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +48 -33
  189. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  190. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +6 -3
  191. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
  192. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
  193. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
  194. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
  195. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
  196. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
  197. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
  198. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
  199. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
  200. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
  201. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
  202. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
  203. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
  204. data/sig/rigor/cache.rbs +19 -0
  205. data/sig/rigor/inference.rbs +22 -0
  206. data/sig/rigor/plugin/access_denied_error.rbs +3 -1
  207. data/sig/rigor/plugin/base.rbs +58 -3
  208. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  209. data/sig/rigor/plugin/manifest.rbs +31 -1
  210. data/sig/rigor/rbs_extended.rbs +2 -0
  211. data/sig/rigor/scope.rbs +5 -0
  212. data/sig/rigor/source.rbs +12 -0
  213. data/sig/rigor/type.rbs +58 -1
  214. data/sig/rigor.rbs +11 -1
  215. data/skills/rigor-plugin-author/SKILL.md +13 -9
  216. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
  217. data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
  218. data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
  219. metadata +73 -2
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
@@ -3,7 +3,37 @@ class Rigor::Plugin::Manifest::Consumption
3
3
  end
4
4
 
5
5
  class Rigor::Plugin::Manifest
6
- def initialize: (id: untyped, version: untyped, ?description: untyped, ?protocols: untyped, ?config_schema: untyped, ?produces: untyped, ?consumes: untyped, ?owns_receivers: untyped, ?type_node_resolvers: untyped, ?block_as_methods: untyped, ?heredoc_templates: untyped, ?trait_registries: untyped, ?external_files: untyped) -> void
6
+ def initialize: (id: untyped, version: untyped, ?description: untyped, ?config_schema: untyped, ?produces: untyped, ?consumes: untyped, ?owns_receivers: untyped, ?open_receivers: untyped, ?type_node_resolvers: untyped, ?block_as_methods: untyped, ?heredoc_templates: untyped, ?nested_class_templates: untyped, ?trait_registries: untyped, ?external_files: untyped, ?hkt_registrations: untyped, ?hkt_definitions: untyped, ?signature_paths: untyped, ?protocol_contracts: untyped, ?source_rbs_synthesizer: untyped, ?additional_initializers: untyped) -> void
7
+
8
+ # Public attribute readers (the full `attr_reader` surface). Plugins
9
+ # read `manifest.id` / `manifest.protocol_contracts` etc.; declaring
10
+ # them keeps the contract surface resolvable so ADR-43 ancestor
11
+ # resolution does not surface a false `call.undefined-method` on a
12
+ # real reader. Identity fields are typed; the structured slots stay
13
+ # `untyped` per the sparse `sig/` tree.
14
+ def id: () -> String
15
+ def version: () -> String
16
+ def description: () -> String?
17
+ def config_schema: () -> untyped
18
+ def config_defaults: () -> Hash[String, untyped]
19
+ def produces: () -> untyped
20
+ def consumes: () -> untyped
21
+ def owns_receivers: () -> untyped
22
+ def open_receivers: () -> untyped
23
+ def type_node_resolvers: () -> untyped
24
+ def block_as_methods: () -> untyped
25
+ def heredoc_templates: () -> untyped
26
+ def nested_class_templates: () -> untyped
27
+ def trait_registries: () -> untyped
28
+ def external_files: () -> untyped
29
+ def hkt_registrations: () -> untyped
30
+ def hkt_definitions: () -> untyped
31
+ def signature_paths: () -> untyped
32
+ def protocol_contracts: () -> untyped
33
+ def source_rbs_synthesizer: () -> untyped
34
+ def additional_initializers: () -> untyped
35
+
7
36
  def validate_config: (untyped) -> Array[String]
8
37
  def to_h: () -> Hash[String, untyped]
38
+ def hash: () -> Integer
9
39
  end
@@ -45,6 +45,8 @@ module Rigor
45
45
 
46
46
  def self?.read_flow_contribution: (untyped method_def) -> untyped?
47
47
 
48
+ def self?.parse_conforms_to_annotation: (String? string) -> String?
49
+
48
50
  class ParamOverride
49
51
  attr_reader param_name: Symbol
50
52
  attr_reader type: Type::t
data/sig/rigor/scope.rbs CHANGED
@@ -19,6 +19,8 @@ module Rigor
19
19
  attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
20
20
  attr_reader discovered_superclasses: Hash[String, String]
21
21
  attr_reader discovered_includes: Hash[String, Array[String]]
22
+ attr_reader discovered_class_sources: Hash[String, Set[String]]
23
+ attr_reader data_member_layouts: Hash[String, Array[Symbol]]
22
24
  attr_reader indexed_narrowings: Hash[IndexedKey, Type::t]
23
25
  attr_reader method_chain_narrowings: Hash[ChainKey, Type::t]
24
26
  attr_reader source_path: String?
@@ -66,8 +68,11 @@ module Rigor
66
68
  def discovered_method_visibility: (String | Symbol class_name, String | Symbol method_name) -> Symbol?
67
69
  def superclass_of: (String | Symbol class_name) -> String?
68
70
  def with_discovered_superclasses: (Hash[String, String] table) -> Scope
71
+ def data_member_layout: (String | Symbol class_name) -> Array[Symbol]?
72
+ def with_data_member_layouts: (Hash[String, Array[Symbol]] table) -> Scope
69
73
  def includes_of: (String | Symbol class_name) -> Array[String]
70
74
  def with_discovered_includes: (Hash[String, Array[String]] table) -> Scope
75
+ def with_discovered_class_sources: (Hash[String, Set[String]] table) -> Scope
71
76
  def indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key) -> Type::t?
72
77
  def with_indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key, Type::t type) -> Scope
73
78
  def without_indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key) -> Scope
data/sig/rigor/source.rbs CHANGED
@@ -11,10 +11,22 @@ module Rigor
11
11
  def at_offset: (Integer offset) -> untyped?
12
12
  end
13
13
 
14
+ module Literals
15
+ def self?.symbol_or_string: (untyped? node) -> Symbol?
16
+ def self?.symbol_or_string_name: (untyped? node) -> String?
17
+ def self?.symbol: (untyped? node) -> Symbol?
18
+ def self?.symbol_name: (untyped? node) -> String?
19
+ def self?.symbol_arguments: (untyped? call_node) -> Array[Symbol]
20
+ def self?.symbol_arg: (untyped? call_node, Integer index) -> Symbol?
21
+ end
22
+
14
23
  module NodeWalker
15
24
  def self?.each: (untyped root) { (untyped) -> void } -> nil
16
25
  | (untyped root) -> Enumerator[untyped, nil]
17
26
  def self?.walk: (untyped node) { (untyped) -> void } -> void
27
+ def self?.each_with_ancestors: (untyped root) { (untyped, Array[untyped]) -> void } -> nil
28
+ | (untyped root) -> Enumerator[untyped, nil]
29
+ def self?.walk_with_ancestors: (untyped node, Array[untyped] ancestors) { (untyped, Array[untyped]) -> void } -> void
18
30
  end
19
31
  end
20
32
  end
data/sig/rigor/type.rbs CHANGED
@@ -1,9 +1,32 @@
1
1
  module Rigor
2
2
  module Type
3
- type t = Top | Bot | Dynamic | Constant | IntegerRange | Nominal | Singleton | Union | Difference | Tuple | HashShape
3
+ type t = Top | Bot | Dynamic | Constant | IntegerRange | Nominal | Singleton | Union | Difference | Tuple | HashShape | DataClass | DataInstance
4
4
 
5
5
  type accepts_mode = :strict | :gradual | :loose
6
6
 
7
+ # The structural interface every `Type::*` carrier satisfies — the
8
+ # internal type-object API (see
9
+ # docs/internal-spec/internal-type-api.md). A structural *interface*
10
+ # (duck-typed: satisfied by having the methods, no `implements`
11
+ # clause), not a "protocol" — that word is reserved for ADR-28
12
+ # path-scoped protocol contracts. The closed `type t` union above is
13
+ # what the engine threads day-to-day; this records the shared
14
+ # per-carrier method contract in one place. Carriers supply the
15
+ # identity trio via `Rigor::ValueSemantics` and `accepts` via
16
+ # `Rigor::Type::AcceptanceRouter`.
17
+ interface _Type
18
+ def describe: (?Symbol verbosity) -> String
19
+ def erase_to_rbs: () -> String
20
+ def top: () -> Trinary
21
+ def bot: () -> Trinary
22
+ def dynamic: () -> Trinary
23
+ def accepts: (Type::t other, ?mode: accepts_mode) -> AcceptsResult
24
+ def ==: (untyped other) -> bool
25
+ def eql?: (untyped other) -> bool
26
+ def hash: () -> Integer
27
+ def inspect: () -> String
28
+ end
29
+
7
30
  class Top
8
31
  def self.instance: () -> Top
9
32
  def describe: (?Symbol verbosity) -> String
@@ -222,6 +245,38 @@ module Rigor
222
245
  def inspect: () -> String
223
246
  end
224
247
 
248
+ class DataClass
249
+ attr_reader members: Array[Symbol]
250
+ attr_reader class_name: String?
251
+ def initialize: (Array[Symbol] members, ?String? class_name) -> void
252
+ def describe: (?Symbol verbosity) -> String
253
+ def erase_to_rbs: () -> String
254
+ def top: () -> Trinary
255
+ def bot: () -> Trinary
256
+ def dynamic: () -> Trinary
257
+ def accepts: (Type::t other, ?mode: accepts_mode) -> AcceptsResult
258
+ def ==: (untyped other) -> bool
259
+ def hash: () -> Integer
260
+ def inspect: () -> String
261
+ end
262
+
263
+ class DataInstance
264
+ attr_reader members: Hash[Symbol, Type::t]
265
+ attr_reader class_name: String?
266
+ def initialize: (Hash[Symbol, Type::t] members, ?String? class_name) -> void
267
+ def member_names: () -> Array[Symbol]
268
+ def member_type: (Symbol name) -> Type::t?
269
+ def describe: (?Symbol verbosity) -> String
270
+ def erase_to_rbs: () -> String
271
+ def top: () -> Trinary
272
+ def bot: () -> Trinary
273
+ def dynamic: () -> Trinary
274
+ def accepts: (Type::t other, ?mode: accepts_mode) -> AcceptsResult
275
+ def ==: (untyped other) -> bool
276
+ def hash: () -> Integer
277
+ def inspect: () -> String
278
+ end
279
+
225
280
  class AcceptsResult
226
281
  attr_reader trinary: Trinary
227
282
  attr_reader mode: accepts_mode
@@ -282,6 +337,8 @@ module Rigor
282
337
  def self?.universal_int: () -> IntegerRange
283
338
  def self?.tuple_of: (*Type::t elements) -> Tuple
284
339
  def self?.hash_shape_of: (?Hash[untyped, Type::t]? pairs, **untyped options) -> HashShape
340
+ def self?.data_class_of: (members: Array[Symbol], ?class_name: String?) -> DataClass
341
+ def self?.data_instance_of: (members: Hash[Symbol, Type::t], ?class_name: String?) -> DataInstance
285
342
  def self?.union: (*Type::t types) -> Type::t
286
343
  def self?.key_of: (Type::t type) -> Type::t
287
344
  def self?.value_of: (Type::t type) -> Type::t
data/sig/rigor.rbs CHANGED
@@ -53,6 +53,11 @@ module Rigor
53
53
  def error?: () -> bool
54
54
  def to_h: () -> Hash[String, untyped]
55
55
  def to_s: () -> String
56
+
57
+ # Singleton factories plugins build diagnostics through (the
58
+ # `node` / `location` are Prism values, received as `untyped`).
59
+ def self.from_node: (untyped node, path: String, message: String, ?severity: Symbol, ?rule: String?, ?source_family: String, ?receiver_type: untyped, ?method_name: untyped, ?project_definition_site: untyped) -> Rigor::Analysis::Diagnostic
60
+ def self.from_location: (untyped location, path: String, message: String, ?severity: Symbol, ?rule: String?, ?source_family: String, ?receiver_type: untyped, ?method_name: untyped, ?project_definition_site: untyped) -> Rigor::Analysis::Diagnostic
56
61
  end
57
62
 
58
63
  class Result
@@ -108,8 +113,13 @@ module Rigor
108
113
  attr_reader cache_store: untyped
109
114
  attr_reader plugin_registry: untyped
110
115
  attr_reader buffer: untyped
111
- def initialize: (configuration: Configuration, ?explain: bool, ?cache_store: untyped, ?plugin_requirer: untyped, ?workers: Integer, ?collect_stats: bool, ?buffer: untyped, ?prebuilt: untyped, ?environment: untyped) -> void
116
+ attr_reader file_dependencies: Hash[String, untyped]
117
+ attr_reader analyzed_files: Array[String]
118
+ def initialize: (configuration: Configuration, ?explain: bool, ?cache_store: untyped, ?plugin_requirer: untyped, ?workers: Integer, ?collect_stats: bool, ?buffer: untyped, ?prebuilt: untyped, ?environment: untyped, ?record_dependencies: bool, ?analyze_only: untyped) -> void
112
119
  def run: (?Array[String] paths) -> Result
120
+ def run_source: (source: String, ?path: String) -> Result
121
+ def analysis_file_set: (?Array[String] paths) -> Array[String]
122
+ def file_dependents: () -> Hash[String, untyped]
113
123
  def prepare_project_scan: (?paths: Array[String]) -> untyped
114
124
  end
115
125
  end
@@ -90,20 +90,24 @@ Do NOT use this skill for:
90
90
  A Rigor plugin is a Ruby class that subclasses `Rigor::Plugin::Base`,
91
91
  declares a `manifest(id:, version:, …)`, and calls
92
92
  `Rigor::Plugin.register(self)` at load time. When `.rigor.yml` lists
93
- the plugin under `plugins:`, Rigor `require`s it and, for every
94
- analysed file, calls the plugin's `#diagnostics_for_file(path:,
95
- scope:, root:)` handing it the file's Prism AST (`root`) and a
96
- `scope` it can query for inferred types. The plugin walks the AST and
97
- returns an array of `Rigor::Analysis::Diagnostic`. Optionally it also
98
- implements `#flow_contribution_for(call_node:, scope:)` to *supply* a
99
- return type for call sites the core analyzer types as `Dynamic`.
93
+ the plugin under `plugins:`, Rigor `require`s it and runs its
94
+ **node rules**: the plugin declares `node_rule(Prism::CallNode) { |node,
95
+ scope, path, _fc, context| }`, and the engine which owns the single
96
+ AST walk per file hands every matching node to the block along with a
97
+ `scope` it can query for inferred types. The block returns an array of
98
+ `Rigor::Analysis::Diagnostic` (built via the `diagnostic` helper).
99
+ Optionally the plugin also declares `dynamic_return(receivers:)` /
100
+ `type_specifier(methods:)` to *supply* a return type or narrowing facts
101
+ for call sites the core analyzer types as `Dynamic`. (The older
102
+ `#diagnostics_for_file` / `#flow_contribution_for` fat hooks remain as
103
+ deprecated escape valves — see Phase 2.)
100
104
 
101
105
  ## Phase outline
102
106
 
103
107
  | Phase | What | Reference |
104
108
  | --- | --- | --- |
105
109
  | 1 | Package and scaffold — gem vs project-private layout, gemspec / Gemfile, the plugin class skeleton, `.rigor.yml` activation. | [`references/01-plan-and-scaffold.md`](references/01-plan-and-scaffold.md) |
106
- | 2 | The walker — `diagnostics_for_file`, building `Diagnostic`s, querying `scope.type_of`, optional `flow_contribution_for`, RBS for the DSL. | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) |
110
+ | 2 | Node rules — `node_rule` (engine-owned walk), building `Diagnostic`s via `Base#diagnostic`, querying `scope.type_of`, calling the target library directly instead of reimplementing it (ADR-39: `Plugin::Inflector`, `Base.suggest`), optional `dynamic_return` / `type_specifier`, RBS for the DSL. | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) |
107
111
  | 3 | Test and ship — fixture-based tests (RSpec / Minitest, no rigor internals), version pinning, README, publish or keep private. | [`references/03-test-and-ship.md`](references/03-test-and-ship.md) |
108
112
 
109
113
  ## Reading order — modules
@@ -111,5 +115,5 @@ return type for call sites the core analyzer types as `Dynamic`.
111
115
  | Module | Read | Covers |
112
116
  | --- | --- | --- |
113
117
  | 1 | [`references/01-plan-and-scaffold.md`](references/01-plan-and-scaffold.md) | **Phase 1.** The gem vs project-private packaging split, directory trees for both, gemspec template, project-private path-gem / `RUBYLIB` activation, the `Rigor::Plugin::Base` skeleton, `.rigor.yml` `plugins:` wiring. |
114
- | 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `diagnostics_for_file` AST walk over Prism nodes, the `Diagnostic` constructor shape, asking the analyzer for inferred types via `scope.type_of`, the optional `flow_contribution_for` return-type hook, and shipping `sig/*.rbs` so the DSL's types are visible. |
118
+ | 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `node_rule` engine-owned AST walk over Prism nodes, the `Base#diagnostic` helper, asking the analyzer for inferred types via `scope.type_of`, two-pass / lexical context (`node_file_context` / `NodeContext`), the optional `dynamic_return` / `type_specifier` return-type hooks (and the deprecated `flow_contribution_for` escape valve), calling the target library's pure methods directly rather than reimplementing them (ADR-39: `Plugin::Inflector` over the real `ActiveSupport::Inflector`; `Base.suggest` for did-you-mean), and shipping `sig/*.rbs` so the DSL's types are visible. |
115
119
  | 3 | [`references/03-test-and-ship.md`](references/03-test-and-ship.md) | **Phase 3.** Testing a plugin from outside the monorepo — fixture projects driven through `rigor check --format json`, plus pure unit tests of dispatch tables — with RSpec or Minitest. Version pinning against the pre-1.0 contract. README. Publishing to RubyGems or keeping the plugin private. |
@@ -184,10 +184,11 @@ module Rigor
184
184
  @module_name = config.fetch("module_name", "Default")
185
185
  end
186
186
 
187
- # Called per analysed file. `root` is the file's Prism AST,
188
- # `scope` answers inferred-type queries. Return an Array of
189
- # Rigor::Analysis::Diagnostic. See 02-walker-and-types.md.
190
- def diagnostics_for_file(path:, scope:, root:)
187
+ # The engine owns the AST walk and hands every matching node to
188
+ # this rule. `scope` answers inferred-type queries; return an
189
+ # Array of Rigor::Analysis::Diagnostic (built via `diagnostic`).
190
+ # See 02-walker-and-types.md.
191
+ node_rule Prism::CallNode do |node, scope, path, _file_context, _context|
191
192
  []
192
193
  end
193
194
  end
@@ -227,7 +228,7 @@ inert.
227
228
 
228
229
  ## Output of this module
229
230
 
230
- A scaffolded plugin that loads (even if `diagnostics_for_file` still
231
+ A scaffolded plugin that loads (even if its `node_rule` still
231
232
  returns `[]`) and is activated in `.rigor.yml`. Proceed to Phase 2
232
233
  ([`02-walker-and-types.md`](02-walker-and-types.md)) to make it
233
234
  actually analyse.
@@ -1,57 +1,94 @@
1
- # 02 — The walker and types
1
+ # 02 — Node rules and types
2
2
 
3
- Covers **Phase 2** — making `diagnostics_for_file` analyse the AST,
4
- emit diagnostics, and (optionally) contribute return types.
3
+ Covers **Phase 2** — recognising the DSL's call shapes, emitting
4
+ diagnostics, and (optionally) contributing return types. The engine
5
+ owns the AST walk; you write the per-node check.
5
6
 
6
- ## `diagnostics_for_file` — the core hook
7
+ ## `node_rule` — the core hook (ADR-37)
8
+
9
+ A diagnostic-emitting plugin declares a **node rule**. The engine walks
10
+ each analysed file's AST **once** and hands every node of the declared
11
+ type to your block — so you write the check and never the traversal:
7
12
 
8
13
  ```ruby
9
- def diagnostics_for_file(path:, scope:, root:)
10
- # root — Prism::Node, the parsed file (a ProgramNode).
11
- # scope — answers inferred-type queries: scope.type_of(node).
12
- # path — the file path, for Diagnostic#path.
13
- # return Array<Rigor::Analysis::Diagnostic>
14
+ class MyPlugin < Rigor::Plugin::Base
15
+ manifest(id: "my-plugin", version: "0.1.0")
16
+
17
+ node_rule Prism::CallNode do |node, scope, path, file_context, context|
18
+ # node — the Prism::CallNode the engine matched.
19
+ # scope — answers inferred-type queries: scope.type_of(node).
20
+ # path — the file path, for the diagnostic location.
21
+ # file_context — the value node_file_context built (see below); nil if none.
22
+ # context — Rigor::Plugin::NodeContext: lexical ancestors.
23
+ # → return Array<Rigor::Analysis::Diagnostic> ([] to fire nothing)
24
+ end
14
25
  end
15
26
  ```
16
27
 
17
- Walk `root` with a Prism visitor or a recursive descent, recognise
18
- the DSL's call shapes, and collect diagnostics. Keep the walk in a
19
- separate `Analyzer` class once it grows past a few methods — pass it
20
- `path` and let it return the diagnostic array.
28
+ `node_rule(node_type)` matches by `node.is_a?(node_type)`, so declare
29
+ the concrete class you care about (`Prism::CallNode` for almost every
30
+ DSL plugin) or `Prism::Node` to see everything. The block runs through
31
+ `instance_exec`, so `config`, `services`, `services.fact_store`, and the
32
+ `diagnostic` helper (below) are all in scope. Declare multiple rules
33
+ (they run in declaration order); a plugin that declares none pays zero
34
+ cost. Keep the matching logic in a separate `Analyzer` class once it
35
+ grows — have it take the call node and return location-free `Violation`
36
+ rows the rule positions with `diagnostic` (the bundled plugins follow
37
+ this `Analyzer.violations_for` split, which also makes the logic
38
+ unit-testable without running the whole engine).
39
+
40
+ > The legacy `diagnostics_for_file(path:, scope:, root:)` hook — where
41
+ > you hand-rolled a `def walk` / `compact_child_nodes.each` recursion
42
+ > over `root` yourself — is the **deprecated whole-file escape valve**.
43
+ > Use it only for a genuinely file-scoped result (a single load-error
44
+ > row, or a check that needs the whole parsed file at once). New
45
+ > node-scoped checks use `node_rule`.
21
46
 
22
47
  ### Recognising call sites
23
48
 
24
- Most DSL plugins key off `Prism::CallNode`:
25
-
26
- ```ruby
27
- def each_call(node, &block)
28
- block.call(node) if node.is_a?(Prism::CallNode)
29
- node&.compact_child_nodes&.each { |child| each_call(child, &block) }
30
- end
31
- ```
32
-
33
- Then match on `node.name` (the method name, a Symbol),
34
- `node.receiver`, and `node.arguments&.arguments`.
49
+ Inside the rule, match on `node.name` (the method name, a Symbol),
50
+ `node.receiver` (nil for an implicit-self call), and
51
+ `node.arguments&.arguments`. For literal `:sym` / `"str"` arguments
52
+ reach for `Rigor::Source::Literals` (`symbol_or_string(node)` /
53
+ `symbol_arguments(call)` / `symbol_arg(call, i)`) rather than
54
+ re-deriving the `unescaped.to_sym` shape.
55
+
56
+ ### Two-pass and lexical context
57
+
58
+ - **Same-file collect-then-validate** (gather declared names, then
59
+ validate references): declare `node_file_context { |root, scope| … }`.
60
+ It runs once per file before the walk and its return value is threaded
61
+ to every rule as `file_context`. (A *cross-file* collect belongs in
62
+ `#prepare` + `services.fact_store` instead — see
63
+ [`01-plan-and-scaffold.md`](01-plan-and-scaffold.md).)
64
+ - **Where the node sits**: `context` (a `Rigor::Plugin::NodeContext`)
65
+ carries the lexical ancestor chain — `context.enclosing_def`,
66
+ `context.enclosing_module`, `context.enclosing_block(:describe)`, and
67
+ the raw `context.ancestors`. Read it when the check depends on the
68
+ enclosing class / method / block DSL (the enclosing controller a
69
+ `before_action` sits in, the `describe <Model>` a matcher is under).
35
70
 
36
71
  ## Building a `Diagnostic`
37
72
 
38
- Every diagnostic the plugin emits is a `Rigor::Analysis::Diagnostic`.
39
- A small constructor helper keeps the call sites clean:
73
+ Use the `Base#diagnostic` helper it internalises the load-bearing
74
+ 1-based `line` / `start_column + 1` convention so you never unpack
75
+ `node.location` by hand:
40
76
 
41
77
  ```ruby
42
- def diagnostic(path, node, severity:, rule:, message:)
43
- loc = node.location
44
- Rigor::Analysis::Diagnostic.new(
45
- path: path,
46
- line: loc.start_line,
47
- column: loc.start_column + 1, # 1-based column
48
- message: message,
49
- severity: severity, # :error | :warning | :info
50
- rule: rule # short kebab-case id
51
- )
78
+ node_rule Prism::CallNode do |node, scope, path, _fc, _ctx|
79
+ next [] unless offending?(node)
80
+
81
+ [diagnostic(node, path: path, message: "...", severity: :error, rule: "my-rule")]
52
82
  end
53
83
  ```
54
84
 
85
+ Pass `location:` (a Prism location) to point at a sub-location rather
86
+ than the whole node — typically `location: node.message_loc`, so a
87
+ method-name diagnostic points at the name, not the receiver-spanning
88
+ whole call. Do **not** set `source_family` — the runner stamps
89
+ `plugin.<manifest.id>` on every returned diagnostic, so anything you set
90
+ is overwritten.
91
+
55
92
  `rule` is a short identifier (`dimension-mismatch`,
56
93
  `unknown-state`). Rigor namespaces it under your plugin —
57
94
  diagnostics surface as `plugin.<manifest.id>.<rule>`, and that
@@ -87,46 +124,80 @@ Match with `case`/`when` on the carrier class. Treat any carrier you
87
124
  do not recognise as "decline to act" — never crash on an unexpected
88
125
  type.
89
126
 
90
- ## Optional contribute a return type with `flow_contribution_for`
127
+ ## Use the target library, don't reimplement it (ADR-39)
128
+
129
+ A plugin may **call the pure methods of the library it targets
130
+ directly** instead of reimplementing them — the Ruby analogue of a
131
+ PHPStan extension calling into the real framework. Reimplementing a
132
+ library's rules (an inflector, a status-code table, …) risks diverging
133
+ from the real behaviour, and a wrong *derived* fact (a wrong class /
134
+ helper / column name) is a false positive on working code. So declare
135
+ the target gem as your plugin's dependency and call its safe methods.
91
136
 
92
- > **Critical this hook does NOT make a method "defined", so it does
137
+ The bundled `Rigor::Plugin::Inflector` is the worked example it calls
138
+ the real `ActiveSupport::Inflector` rather than a hand-rolled
139
+ approximation. If your plugin needs inflection, use it:
140
+
141
+ ```ruby
142
+ Rigor::Plugin::Inflector.classify("users") # => "User"
143
+ Rigor::Plugin::Inflector.tableize("BlogPost") # => "blog_posts"
144
+ ```
145
+
146
+ For your own target library, follow the same harness: a **fixed
147
+ allow-list** of pure methods, inputs derived from source, and **decline
148
+ (return nil / emit nothing) when the library is unavailable — never
149
+ approximate**. How the call is isolated from Rigor (in-process, a forked
150
+ worker, or a `Ruby::Box`) is a configurable strategy the user picks
151
+ (`plugins_isolation:`); you just call the method.
152
+
153
+ For the common "did you mean …?" suggestion, use the shared helper
154
+ rather than hand-rolling Levenshtein:
155
+
156
+ ```ruby
157
+ Rigor::Plugin::Base.suggest(typo, known_names) # nearest match, or nil
158
+ ```
159
+
160
+ ## Optional — contribute a return type with `dynamic_return` / `type_specifier`
161
+
162
+ > **Critical — these hooks do NOT make a method "defined", so they do
93
163
  > NOT suppress `call.undefined-method`.** Method *existence* and call
94
- > *type* are two independent checks. `flow_contribution_for` sharpens
164
+ > *type* are two independent checks. A return-type contribution sharpens
95
165
  > the type of a call the analyzer has **already resolved to a real
96
166
  > method** (turning a `Dynamic` return into something precise). It is
97
167
  > never consulted for a receiver/method the analyzer cannot find — that
98
- > fires `call.undefined-method` first, and a flow contribution does
99
- > nothing to silence it. **If your goal is to kill a
100
- > `call.undefined-method` cluster on a DSL-generated method (the common
101
- > reason `rigor-project-init` hands off to this skill), the fix is to
102
- > make the method *exist* in Rigor's view — ship RBS declaring it (see
103
- > "Shipping RBS for the DSL" below), not a flow contribution.** Reach
104
- > for `flow_contribution_for` only when the call already resolves and
105
- > you want a *better return type*.
168
+ > fires `call.undefined-method` first, and a contribution does nothing
169
+ > to silence it. **If your goal is to kill a `call.undefined-method`
170
+ > cluster on a DSL-generated method (the common reason
171
+ > `rigor-project-init` hands off to this skill), the fix is to make the
172
+ > method *exist* in Rigor's view — ship RBS declaring it (see "Shipping
173
+ > RBS for the DSL" below), not a return-type contribution.** Reach for
174
+ > these only when the call already resolves and you want a *better
175
+ > return type*.
106
176
 
107
177
  A plugin can do more than emit diagnostics: it can *supply* the
108
- inferred return type for a call site the core analyzer would
109
- otherwise type as `Dynamic`. Implement `flow_contribution_for`:
178
+ inferred return type (or narrowing facts) for a call site the core
179
+ analyzer would otherwise type as `Dynamic`. ADR-37 gives two narrow,
180
+ declaratively-gated DSLs — prefer them; the engine indexes plugins by
181
+ their gate so it only calls the block for matching calls:
110
182
 
111
183
  ```ruby
112
- def flow_contribution_for(call_node:, scope:)
113
- return nil unless call_node.is_a?(Prism::CallNode)
114
- # ... decide the call site's real return type ...
115
- return nil if undecidable # nil = "I have nothing to add"
116
-
117
- Rigor::FlowContribution.new(
118
- return_type: a_rigor_type,
119
- provenance: Rigor::FlowContribution::Provenance.new(
120
- source_family: "plugin.#{manifest.id}",
121
- plugin_id: manifest.id,
122
- node: call_node,
123
- descriptor: nil
124
- )
125
- )
184
+ # Per-call-site RETURN TYPE, gated on the receiver's class.
185
+ # The block fires only when the receiver type's class is (or inherits
186
+ # from) a declared `receivers:` entry. Return a Rigor::Type or nil.
187
+ dynamic_return receivers: ["Money"] do |call_node, scope|
188
+ next nil unless call_node.name == :+
189
+ Rigor::Type::Combinator.nominal_of("Money")
190
+ end
191
+
192
+ # Post-return NARROWING FACTS, gated on the call's method name.
193
+ # Return an Array of facts (or nil). Used for assertion / predicate
194
+ # narrowing (`assert_kind_of(Foo, x)` ⇒ x is Foo afterwards).
195
+ type_specifier methods: [:assert_kind_of] do |call_node, scope|
196
+ # ... build and return the post-return facts ...
126
197
  end
127
198
  ```
128
199
 
129
- Build the `return_type` with `Rigor::Type::Combinator`:
200
+ Build return types with `Rigor::Type::Combinator`:
130
201
 
131
202
  ```ruby
132
203
  Rigor::Type::Combinator.nominal_of("Money") # a class type
@@ -134,14 +205,27 @@ Rigor::Type::Combinator.constant_of(true) # a literal
134
205
  Rigor::Type::Combinator.union(a, b) # a union
135
206
  ```
136
207
 
137
- Returning `nil` is always safe — it means "no contribution", and the
138
- core analyzer keeps its own answer. Contribute a type only when the
208
+ Returning `nil` (or `[]`) is always safe — it means "no contribution",
209
+ and the core analyzer keeps its own answer. Contribute only when the
139
210
  plugin is confident; a wrong contribution propagates downstream.
140
211
 
141
- This hook is the most contract-sensitive surface — it is the part
142
- most likely to shift before v0.2.0. Implement it only if the plugin
143
- genuinely needs to sharpen call-site types; a diagnostics-only
144
- plugin can skip it entirely.
212
+ `receivers:` / `methods:` are the greppable gates the
213
+ `rigor plugins --capabilities` catalogue enumerates run it to see
214
+ exactly what each loaded plugin contributes.
215
+
216
+ > **The deprecated escape valve.** The original fat hook,
217
+ > `flow_contribution_for(call_node:, scope:)`, returns a single
218
+ > `Rigor::FlowContribution` and is still consulted alongside the narrow
219
+ > DSLs. It is retained only for the two shapes the narrow DSLs do not
220
+ > express: a **method-gated return type** (an RSpec `let(:x) { … }`
221
+ > binding, a Sorbet `sig`-driven return — keyed on the method, not a
222
+ > fixed receiver class) and a **dynamic per-project receiver set**
223
+ > (ActiveStorage's `Attached::One` on discovered model classes). If your
224
+ > plugin needs one of those, use `flow_contribution_for`; otherwise
225
+ > prefer `dynamic_return` / `type_specifier`. These return-type surfaces
226
+ > are the most contract-sensitive part of the API — implement one only
227
+ > if the plugin genuinely needs to sharpen call-site types; a
228
+ > diagnostics-only plugin skips them entirely.
145
229
 
146
230
  ## Shipping RBS for the DSL — the way to suppress `call.undefined-method`
147
231
 
@@ -151,7 +235,7 @@ If the DSL introduces methods or classes that Rigor cannot see (a
151
235
  Rigor RBS declaring them so *core* inference — not just your plugin —
152
236
  treats them as **defined**. This is what removes the
153
237
  `call.undefined-method` diagnostics on those methods; nothing else
154
- (not `diagnostics_for_file`, not `flow_contribution_for`) makes a
238
+ (not a `node_rule`, not `dynamic_return` / `type_specifier`) makes a
155
239
  method exist in Rigor's view.
156
240
 
157
241
  Two ways to wire the RBS, depending on how the plugin is packaged:
@@ -197,8 +281,8 @@ They compose — many plugins ship both.
197
281
 
198
282
  ## Output of this module
199
283
 
200
- A plugin whose `diagnostics_for_file` recognises the DSL and emits
201
- diagnostics with correct severities and rule ids — optionally a
202
- `flow_contribution_for` and a `sig/` bundle. Verify by eye with
203
- `rigor check`; lock it down with tests in Phase 3
284
+ A plugin whose `node_rule`(s) recognise the DSL and emit diagnostics
285
+ with correct severities and rule ids — optionally a `dynamic_return` /
286
+ `type_specifier` and a `sig/` bundle. Verify by eye with `rigor check`;
287
+ lock it down with tests in Phase 3
204
288
  ([`03-test-and-ship.md`](03-test-and-ship.md)).
@@ -114,9 +114,9 @@ SKILL.md). Concretely:
114
114
  - Your plugin's own version is normal semver, independent of
115
115
  `rigortype`'s.
116
116
  - When you bump the `rigortype` pin to a new minor, **re-run the
117
- full test suite** — the walker hook signature or the `Diagnostic` /
118
- type-carrier shapes may have changed. The fixture CLI tests are
119
- what catch a contract drift.
117
+ full test suite** — the `node_rule` / contribution hook signatures
118
+ or the `Diagnostic` / type-carrier shapes may have changed. The
119
+ fixture CLI tests are what catch a contract drift.
120
120
  - State the supported `rigortype` range in the README so users do
121
121
  not pair the plugin with an incompatible Rigor.
122
122