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
@@ -58,14 +58,11 @@ module Rigor
58
58
  version: "0.1.0",
59
59
  description: "Validates Sidekiq `Worker.perform_async` argument arity.",
60
60
  config_schema: {
61
- "worker_search_paths" => :array,
62
- "worker_marker_modules" => :array
61
+ "worker_search_paths" => { kind: :array, default: ["app/workers", "app/sidekiq"] },
62
+ "worker_marker_modules" => { kind: :array, default: %w[Sidekiq::Job Sidekiq::Worker] }
63
63
  }
64
64
  )
65
65
 
66
- DEFAULT_WORKER_SEARCH_PATHS = ["app/workers", "app/sidekiq"].freeze
67
- DEFAULT_WORKER_MARKER_MODULES = %w[Sidekiq::Job Sidekiq::Worker].freeze
68
-
69
66
  producer :worker_index do |_params|
70
67
  WorkerDiscoverer.new(
71
68
  io_boundary: io_boundary,
@@ -75,20 +72,30 @@ module Rigor
75
72
  end
76
73
 
77
74
  def init(_services)
78
- @worker_search_paths = Array(config.fetch("worker_search_paths", DEFAULT_WORKER_SEARCH_PATHS)).map(&:to_s)
79
- @worker_marker_modules = Array(
80
- config.fetch("worker_marker_modules", DEFAULT_WORKER_MARKER_MODULES)
81
- ).map(&:to_s)
75
+ @worker_search_paths = Array(config.fetch("worker_search_paths")).map(&:to_s)
76
+ @worker_marker_modules = Array(config.fetch("worker_marker_modules")).map(&:to_s)
82
77
  @worker_index = nil
83
78
  @load_error = nil
84
79
  end
85
80
 
81
+ # File-level only: the load-error emission. The per-call arity
82
+ # validation runs over the engine-owned walk via the node_rule
83
+ # below (ADR-37). The worker index is lazily loaded + memoised by
84
+ # worker_index_or_nil, shared by both surfaces.
86
85
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
87
86
  index = worker_index_or_nil
88
87
  return [load_error_diagnostic(path)] if index.nil? && @load_error
89
- return [] if index.nil? || index.empty?
90
88
 
91
- Analyzer.diagnose(path: path, root: root, worker_index: index).map { |diag| build_diagnostic(diag) }
89
+ []
90
+ end
91
+
92
+ node_rule Prism::CallNode do |node, _scope, path|
93
+ index = worker_index_or_nil
94
+ next [] if index.nil? || index.empty?
95
+
96
+ Analyzer.violations_for(call_node: node, worker_index: index).map do |violation|
97
+ diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
98
+ end
92
99
  end
93
100
 
94
101
  private
@@ -96,7 +103,8 @@ module Rigor
96
103
  def worker_index_or_nil
97
104
  return @worker_index if @worker_index
98
105
 
99
- @worker_index = cache_for(:worker_index, params: {}).call
106
+ descriptor = glob_descriptor(@worker_search_paths, "**/*.rb")
107
+ @worker_index = cache_for(:worker_index, params: {}, descriptor: descriptor).call
100
108
  rescue StandardError => e
101
109
  @load_error = "rigor-sidekiq: failed to discover workers: #{e.class}: #{e.message}"
102
110
  nil
@@ -110,13 +118,6 @@ module Rigor
110
118
  rule: "load-error"
111
119
  )
112
120
  end
113
-
114
- def build_diagnostic(diag)
115
- Rigor::Analysis::Diagnostic.new(
116
- path: diag.path, line: diag.line, column: diag.column,
117
- message: diag.message, severity: diag.severity, rule: diag.rule
118
- )
119
- end
120
121
  end
121
122
 
122
123
  Rigor::Plugin.register(Sidekiq)
@@ -106,6 +106,21 @@ module Rigor
106
106
  if pending_sig && def_node?(child)
107
107
  record_def_with_sig(child, pending_sig, state, lexical_path, in_singleton_class)
108
108
  pending_sig = nil
109
+ elsif pending_sig && attr_macro_call?(child)
110
+ # `sig { returns(T) }` above `attr_reader :name` /
111
+ # `attr_accessor` / `attr_writer` types the generated
112
+ # accessor(s). This is a first-class Sorbet idiom — not
113
+ # a dangling sig — so record the accessor signature(s)
114
+ # instead of warning.
115
+ record_attr_with_sig(child, pending_sig, state, lexical_path, in_singleton_class)
116
+ pending_sig = nil
117
+ elsif pending_sig && (inner_def = visibility_wrapped_def(child))
118
+ # `sig { ... }` above `private def foo` /
119
+ # `private_class_method def self.foo` / `module_function
120
+ # def bar` — a visibility macro wrapping the very def the
121
+ # sig types. Record the inner def, not a dangling sig.
122
+ record_def_with_sig(inner_def, pending_sig, state, lexical_path, in_singleton_class)
123
+ pending_sig = nil
109
124
  elsif sig_call?(child)
110
125
  state.record_error(:duplicate_sig, pending_sig) if pending_sig
111
126
  pending_sig = child
@@ -171,6 +186,64 @@ module Rigor
171
186
  node.is_a?(Prism::DefNode)
172
187
  end
173
188
 
189
+ VISIBILITY_MACROS = %i[
190
+ private public protected
191
+ private_class_method public_class_method module_function
192
+ ].freeze
193
+
194
+ # `private def foo` / `private_class_method def self.bar` /
195
+ # `module_function def baz` — a receiverless visibility macro
196
+ # whose single argument is the `def` it wraps. Returns the
197
+ # inner `Prism::DefNode` (so a preceding sig types it), or nil.
198
+ # The def's own receiver still drives instance-vs-singleton, so
199
+ # `private_class_method def self.bar` records as singleton.
200
+ def visibility_wrapped_def(node)
201
+ return nil unless node.is_a?(Prism::CallNode) && node.receiver.nil?
202
+ return nil unless VISIBILITY_MACROS.include?(node.name)
203
+
204
+ args = node.arguments&.arguments
205
+ return nil unless args&.size == 1 && args.first.is_a?(Prism::DefNode)
206
+
207
+ args.first
208
+ end
209
+
210
+ ATTR_MACROS = %i[attr_reader attr_writer attr_accessor].freeze
211
+
212
+ # `attr_reader :a, :b` / `attr_writer` / `attr_accessor` —
213
+ # a receiverless attribute macro whose arguments are all
214
+ # symbol literals (the only shape a preceding `sig` types).
215
+ def attr_macro_call?(node)
216
+ return false unless node.is_a?(Prism::CallNode) && node.receiver.nil?
217
+ return false unless ATTR_MACROS.include?(node.name)
218
+
219
+ args = node.arguments&.arguments
220
+ !args.nil? && !args.empty? && args.all?(Prism::SymbolNode)
221
+ end
222
+
223
+ # Records the method signature(s) a `sig` contributes to the
224
+ # accessor(s) it precedes: the reader `name` and/or the
225
+ # writer `name=`, each carrying the sig's `returns(...)`
226
+ # type. `attr_reader` emits the reader, `attr_writer` the
227
+ # writer, `attr_accessor` both — for every symbol argument.
228
+ def record_attr_with_sig(attr_node, sig_call, state, lexical_path, in_singleton_class)
229
+ parsed = SigParser.parse(sig_call)
230
+ if parsed.is_a?(SigParser::ParseError)
231
+ state.record_error(parsed.reason, sig_call)
232
+ return
233
+ end
234
+
235
+ class_name = lexical_path.empty? ? "Object" : lexical_path.join("::")
236
+ kind = in_singleton_class ? :singleton : :instance
237
+ reader = attr_node.name != :attr_writer
238
+ writer = attr_node.name != :attr_reader
239
+
240
+ attr_node.arguments.arguments.each do |arg|
241
+ name = arg.unescaped
242
+ catalog_record(state.catalog, class_name, name.to_sym, kind, parsed) if reader
243
+ catalog_record(state.catalog, class_name, :"#{name}=", kind, parsed) if writer
244
+ end
245
+ end
246
+
174
247
  def record_def_with_sig(def_node, sig_call, state, lexical_path, in_singleton_class)
175
248
  parsed = SigParser.parse(sig_call)
176
249
  if parsed.is_a?(SigParser::ParseError)
@@ -38,6 +38,7 @@ module Rigor
38
38
  # | `T.class_of(C)` | `Singleton[C]` |
39
39
  # | `[A, B]` (tuple in sig) | `Tuple[A, B]` |
40
40
  # | `{a: A, b: B}` (shape) | `HashShape{a: A, b: B}` (closed) |
41
+ # | `Foo::Bar[A, B]` (user) | `Nominal["Foo::Bar", [A, B]]` |
41
42
  #
42
43
  # Anything else (`T.proc`, `T.attached_class`,
43
44
  # `T.self_type`, `T.type_parameter`, `T::Struct` / `T::Enum`
@@ -101,16 +102,26 @@ module Rigor
101
102
  Rigor::Type::Combinator.nominal_of(name)
102
103
  end
103
104
 
104
- # `Prism::CallNode` covers two distinct surfaces:
105
+ # `Prism::CallNode` covers three distinct surfaces:
105
106
  #
106
107
  # 1. `T.something(...)` — `untyped` / `anything` /
107
108
  # `noreturn` / `nilable` / `any` / `all` / `class_of`.
108
109
  # 2. `T::SomeClass[...]` — the `[]` method on a generic
109
110
  # `T::*` constant (slice 3 widening). Maps to
110
111
  # `Nominal[name, type_args]`.
112
+ # 3. `Some::User::Generic[...]` — the `[]` method on any
113
+ # OTHER constant (a user-defined generic application,
114
+ # e.g. `Mangrove::Result::Ok[String, StandardError]`).
115
+ # Maps to `Nominal[name, type_args]` so generic carriers
116
+ # authored outside Sorbet's `T::*` set round-trip with
117
+ # their instantiation intact (the unwrap-site receiver a
118
+ # plugin like rigor-mangrove reads `type_args` from).
119
+ # Without this branch such forms degraded to `untyped`,
120
+ # silently dropping the receiver's generic arguments.
111
121
  def translate_call(node)
112
122
  return translate_t_method(node) if sorbet_t_namespaced?(node.receiver)
113
123
  return translate_t_subscript(node) if sorbet_subscript?(node)
124
+ return translate_user_subscript(node) if user_generic_subscript?(node)
114
125
 
115
126
  degraded
116
127
  end
@@ -188,6 +199,37 @@ module Rigor
188
199
  end
189
200
  end
190
201
 
202
+ # A user-defined generic application — a `[]` call on a
203
+ # constant that is NOT `T`-rooted, e.g.
204
+ # `Mangrove::Result::Ok[String, StandardError]` or a
205
+ # top-level `Box[Integer]`. Translates to
206
+ # `Nominal[name, type_args]`, recursively translating each
207
+ # argument (so nested `T::Array[...]` inside a user generic
208
+ # still resolves). Ordered AFTER `translate_t_subscript` in
209
+ # `translate_call`, so the `T::*` forms never reach here.
210
+ def user_generic_subscript?(node)
211
+ return false unless node.name == :[]
212
+
213
+ receiver = node.receiver
214
+ return false unless receiver.is_a?(Prism::ConstantReadNode) ||
215
+ receiver.is_a?(Prism::ConstantPathNode)
216
+
217
+ # `T::Foo[...]` is handled by translate_t_subscript; only
218
+ # non-`T`-rooted constants are user generics.
219
+ !sorbet_t_qualified?(receiver)
220
+ end
221
+
222
+ def translate_user_subscript(node)
223
+ # Parity with translate_constant_path: the constant's
224
+ # rendered name (carrying a leading `::` for absolute
225
+ # paths) becomes the nominal class name.
226
+ name = constant_path_name(node.receiver)
227
+ return degraded if name.nil?
228
+
229
+ args = call_arguments(node).map { |arg| translate(arg) }
230
+ Rigor::Type::Combinator.nominal_of(name, type_args: args)
231
+ end
232
+
191
233
  # `T::Class[T]` — Sorbet's "any class object whose
192
234
  # instances are at least `T`". Rigor has no exact
193
235
  # analogue (Singleton names a specific class); the
@@ -76,29 +76,29 @@ module Rigor
76
76
  version: "0.1.0",
77
77
  description: "Ingests Sorbet `sig` blocks as method-signature contributions.",
78
78
  config_schema: {
79
+ # `paths` keeps a `fetch`-with-default in `init` because its
80
+ # default is dynamic (the project's configured `paths`), not a
81
+ # static literal the manifest can declare.
79
82
  "paths" => :array,
80
- "rbi_paths" => :array,
81
- "enforce_sigil" => :boolean
83
+ # Default RBI directory tree. Matches the layout `tapioca init`
84
+ # generates — see Sorbet's `rbi.md`. Slice 4 walks every `.rbi`
85
+ # file under these roots recursively; the four standard Tapioca
86
+ # subdirectories (`gems` / `annotations` / `dsl` / `shims`) are
87
+ # picked up as a side effect of recursing into the parent root.
88
+ "rbi_paths" => { kind: :array, default: ["sorbet/rbi"] },
89
+ "enforce_sigil" => { kind: :boolean, default: true }
82
90
  }
83
91
  )
84
92
 
85
- # Default RBI directory tree. Matches the layout
86
- # `tapioca init` generates — see Sorbet's `rbi.md`. Slice 4
87
- # walks every `.rbi` file under these roots recursively;
88
- # the four standard Tapioca subdirectories
89
- # (`gems` / `annotations` / `dsl` / `shims`) are picked
90
- # up as a side effect of recursing into the parent root.
91
- DEFAULT_RBI_PATHS = ["sorbet/rbi"].freeze
92
-
93
93
  def init(services)
94
94
  @services = services
95
95
  @configured_paths = Array(config.fetch("paths", services.configuration.paths)).map(&:to_s)
96
- @rbi_paths = Array(config.fetch("rbi_paths", DEFAULT_RBI_PATHS)).map(&:to_s)
97
- # Default `true` — only files marked `# typed: true` /
96
+ @rbi_paths = Array(config.fetch("rbi_paths")).map(&:to_s)
97
+ # Schema default `true` — only files marked `# typed: true` /
98
98
  # `:strict` / `:strong` contribute their sigs. Set to
99
99
  # `false` to record every file's sigs regardless of
100
100
  # sigil (current behaviour pre-this-config).
101
- @enforce_sigil = config.fetch("enforce_sigil", true)
101
+ @enforce_sigil = config.fetch("enforce_sigil")
102
102
  # ADR-11 deferred follow-up — per-call-site assertion
103
103
  # gating. Catalog harvest's `@sigil_by_path` cache is
104
104
  # consulted at every `flow_contribution_for` call so
@@ -503,11 +503,9 @@ module Rigor
503
503
  end
504
504
 
505
505
  def absurd_diagnostic(path, call_node)
506
- location = call_node.location
507
- Rigor::Analysis::Diagnostic.new(
506
+ Rigor::Analysis::Diagnostic.from_node(
507
+ call_node,
508
508
  path: path,
509
- line: location.start_line,
510
- column: location.start_column + 1,
511
509
  message: "`T.absurd` is reachable: the discriminant did not narrow to `T.noreturn`. " \
512
510
  "Either add the missing case branch above the `else`, or remove the " \
513
511
  "`T.absurd(...)` call.",
@@ -558,11 +556,9 @@ module Rigor
558
556
  end
559
557
 
560
558
  def reveal_type_diagnostic(path, call_node, display)
561
- location = call_node.location
562
- Rigor::Analysis::Diagnostic.new(
559
+ Rigor::Analysis::Diagnostic.from_node(
560
+ call_node,
563
561
  path: path,
564
- line: location.start_line,
565
- column: location.start_column + 1,
566
562
  message: "`T.reveal_type` inferred type: #{display}",
567
563
  severity: :info,
568
564
  rule: "reveal-type"
@@ -616,11 +612,9 @@ module Rigor
616
612
  end
617
613
 
618
614
  def assert_type_mismatch_diagnostic(path, call_node, inferred_display, asserted_display)
619
- location = call_node.location
620
- Rigor::Analysis::Diagnostic.new(
615
+ Rigor::Analysis::Diagnostic.from_node(
616
+ call_node,
621
617
  path: path,
622
- line: location.start_line,
623
- column: location.start_column + 1,
624
618
  message: "`T.assert_type!` failed: inferred type #{inferred_display} is not " \
625
619
  "compatible with asserted type #{asserted_display}.",
626
620
  severity: :error,
@@ -629,11 +623,9 @@ module Rigor
629
623
  end
630
624
 
631
625
  def parse_error_diagnostic(path, error)
632
- location = error.node.location
633
- Rigor::Analysis::Diagnostic.new(
626
+ Rigor::Analysis::Diagnostic.from_node(
627
+ error.node,
634
628
  path: path,
635
- line: location.start_line,
636
- column: location.start_column + 1,
637
629
  message: parse_error_message(error.kind),
638
630
  severity: :warning,
639
631
  rule: "parse-error"
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "prism"
4
4
  require "rigor/plugin"
5
+ require "rigor/source/literals"
5
6
 
6
7
  module Rigor
7
8
  module Plugin
@@ -50,41 +51,51 @@ module Rigor
50
51
  version: "0.1.0",
51
52
  description: "Validates state-machine transition references against declared states.",
52
53
  config_schema: {
53
- "dsl_method" => :string,
54
- "state_method" => :string,
55
- "transition_method" => :string
54
+ "dsl_method" => { kind: :string, default: "state_machine" },
55
+ "state_method" => { kind: :string, default: "state" },
56
+ "transition_method" => { kind: :string, default: "transition_to" }
56
57
  }
57
58
  )
58
59
 
59
- DEFAULT_DSL_METHOD = "state_machine"
60
- DEFAULT_STATE_METHOD = "state"
61
- DEFAULT_TRANSITION_METHOD = "transition_to"
62
- DID_YOU_MEAN_DISTANCE = 3
63
-
64
60
  def init(_services)
65
- @dsl_method = config.fetch("dsl_method", DEFAULT_DSL_METHOD).to_sym
66
- @state_method = config.fetch("state_method", DEFAULT_STATE_METHOD).to_sym
67
- @transition_method = config.fetch("transition_method", DEFAULT_TRANSITION_METHOD).to_sym
61
+ @dsl_method = config.fetch("dsl_method").to_sym
62
+ @state_method = config.fetch("state_method").to_sym
63
+ @transition_method = config.fetch("transition_method").to_sym
64
+ end
65
+
66
+ # ADR-37 — the two-pass shape made explicit. The collect pass
67
+ # (pass 1) runs once per file as the node-rule file context: it
68
+ # MUST complete before validation because a `transition_to` may
69
+ # precede the `state` that declares its target, so it cannot be a
70
+ # per-node rule in the engine's single forward walk. The validate
71
+ # pass (pass 2) is then a per-`CallNode` rule over the
72
+ # engine-owned walk — no hand-rolled traversal.
73
+ node_file_context do |root, _scope|
74
+ collect_states(root)
68
75
  end
69
76
 
70
- def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
71
- states = collect_states(root)
72
- return [] if states.empty?
77
+ node_rule Prism::CallNode do |node, _scope, path, states|
78
+ next [] if states.nil? || states.empty?
79
+ next [] unless transition_call?(node)
73
80
 
74
- validate_transitions(path, root, states)
81
+ sym = literal_symbol_arg(node, 0)
82
+ next [] if sym.nil? # not a literal — defer to runtime
83
+
84
+ [build_diagnostic(path, node, sym, states)]
75
85
  end
76
86
 
77
87
  private
78
88
 
79
89
  # Pass 1 — every `state :foo` declaration inside a
80
90
  # `<dsl_method> do ... end` block on the file. Returns a
81
- # frozen Set of state name Symbols.
91
+ # frozen Set of state name Symbols. Walks via the engine's
92
+ # shared `Source::NodeWalker` rather than a hand-rolled traversal.
82
93
  def collect_states(root)
83
94
  states = Set.new
84
- walk(root) do |node|
95
+ Source::NodeWalker.each(root) do |node|
85
96
  next unless dsl_call?(node)
86
97
 
87
- walk(node.block) do |inner|
98
+ Source::NodeWalker.each(node.block) do |inner|
88
99
  next unless state_declaration?(inner)
89
100
 
90
101
  sym = literal_symbol_arg(inner, 0)
@@ -94,33 +105,19 @@ module Rigor
94
105
  states.freeze
95
106
  end
96
107
 
97
- # Pass 2 — every `<transition_method>(:sym)` call.
98
- def validate_transitions(path, root, states)
99
- diagnostics = []
100
- walk(root) do |node|
101
- next unless transition_call?(node)
102
-
103
- sym = literal_symbol_arg(node, 0)
104
- next if sym.nil? # not a literal — defer to runtime
105
-
106
- diagnostics << build_diagnostic(path, node, sym, states)
107
- end
108
- diagnostics
109
- end
110
-
111
108
  def build_diagnostic(path, node, sym, states)
112
109
  if states.include?(sym)
113
110
  diagnostic(
114
- path, node,
115
- severity: :info,
116
- rule: "known-state",
117
- message: "#{@transition_method}(:#{sym}) — declared state"
111
+ node, path: path,
112
+ severity: :info,
113
+ rule: "known-state",
114
+ message: "#{@transition_method}(:#{sym}) — declared state"
118
115
  )
119
116
  else
120
- hint = did_you_mean(sym, states)
117
+ hint = Rigor::Plugin::Base.suggest(sym, states)
121
118
  message = "unknown state :#{sym}"
122
119
  message += " (did you mean :#{hint}?)" if hint
123
- diagnostic(path, node, severity: :error, rule: "unknown-state", message: message)
120
+ diagnostic(node, path: path, severity: :error, rule: "unknown-state", message: message)
124
121
  end
125
122
  end
126
123
 
@@ -143,64 +140,7 @@ module Rigor
143
140
  end
144
141
 
145
142
  def literal_symbol_arg(call, index)
146
- node = call.arguments.arguments[index]
147
- return nil unless node.is_a?(Prism::SymbolNode)
148
-
149
- node.unescaped.to_sym
150
- end
151
-
152
- def walk(node, &)
153
- return if node.nil?
154
-
155
- yield node
156
- node.compact_child_nodes.each { |child| walk(child, &) }
157
- end
158
-
159
- def did_you_mean(name, states)
160
- target = name.to_s
161
- best = nil
162
- best_distance = DID_YOU_MEAN_DISTANCE + 1
163
- states.each do |state|
164
- distance = levenshtein(target, state.to_s)
165
- if distance < best_distance
166
- best = state
167
- best_distance = distance
168
- end
169
- end
170
- best
171
- end
172
-
173
- def levenshtein(a, b) # rubocop:disable Naming/MethodParameterName
174
- return b.length if a.empty?
175
- return a.length if b.empty?
176
-
177
- rows = Array.new(a.length + 1) { |_i| Array.new(b.length + 1, 0) }
178
- (0..a.length).each { |i| rows[i][0] = i }
179
- (0..b.length).each { |j| rows[0][j] = j }
180
-
181
- (1..a.length).each do |i|
182
- (1..b.length).each do |j|
183
- cost = a[i - 1] == b[j - 1] ? 0 : 1
184
- rows[i][j] = [
185
- rows[i - 1][j] + 1,
186
- rows[i][j - 1] + 1,
187
- rows[i - 1][j - 1] + cost
188
- ].min
189
- end
190
- end
191
- rows[a.length][b.length]
192
- end
193
-
194
- def diagnostic(path, node, severity:, rule:, message:)
195
- location = node.location
196
- Rigor::Analysis::Diagnostic.new(
197
- path: path,
198
- line: location.start_line,
199
- column: location.start_column + 1,
200
- message: message,
201
- severity: severity,
202
- rule: rule
203
- )
143
+ Rigor::Source::Literals.symbol(call.arguments.arguments[index])
204
144
  end
205
145
  end
206
146
 
@@ -0,0 +1,19 @@
1
+ module Rigor
2
+ module Cache
3
+ # Structural interface for the RBS-derived cache producers: a class
4
+ # object that responds to `.fetch(loader:, store:)` and yields the
5
+ # cached (or freshly computed) value. A structural *interface*
6
+ # (duck-typed), not a "protocol" (reserved for ADR-28). Satisfied by
7
+ # `RbsCacheProducer` and every subclass; matches the _Type and
8
+ # _DispatchTier interfaces declared elsewhere in sig/.
9
+ interface _CacheProducer
10
+ def fetch: (loader: untyped, store: untyped) -> untyped
11
+ end
12
+
13
+ # Base owning the shared producer `fetch` wiring; subclasses add a
14
+ # PRODUCER_ID constant and a private `self.compute(loader)`.
15
+ class RbsCacheProducer
16
+ def self.fetch: (loader: untyped, store: untyped) -> untyped
17
+ end
18
+ end
19
+ end
@@ -57,6 +57,28 @@ module Rigor
57
57
  module MethodDispatcher
58
58
  def self?.dispatch: (receiver_type: Type::t?, method_name: Symbol, arg_types: Array[Type::t], ?block_type: Type::t?, ?environment: Environment?) -> Type::t?
59
59
  def self?.expected_block_param_types: (receiver_type: Type::t?, method_name: Symbol, arg_types: Array[Type::t], ?environment: Environment?) -> Array[Type::t]
60
+
61
+ # Immutable value object threaded through every dispatch tier.
62
+ class CallContext
63
+ attr_reader receiver: Type::t?
64
+ attr_reader method_name: Symbol
65
+ attr_reader args: Array[Type::t]
66
+ attr_reader block_type: Type::t?
67
+ attr_reader environment: Environment?
68
+ attr_reader call_node: untyped
69
+ attr_reader scope: untyped
70
+ attr_reader self_type_override: Type::t?
71
+ attr_reader public_only: bool
72
+
73
+ def self.build: (receiver: Type::t?, method_name: Symbol, args: Array[Type::t], ?block_type: Type::t?, ?environment: Environment?, ?call_node: untyped, ?scope: untyped, ?self_type_override: Type::t?, ?public_only: bool) -> CallContext
74
+ def with: (**untyped) -> CallContext
75
+ end
76
+
77
+ # The interface every dispatch tier observes: fold one call site,
78
+ # returning a `Type::t` or nil to defer to the next tier.
79
+ interface _DispatchTier
80
+ def try_dispatch: (CallContext) -> Type::t?
81
+ end
60
82
  end
61
83
 
62
84
  class MethodParameterBinder
@@ -1,3 +1,5 @@
1
- class Rigor::Plugin::AccessDeniedError
1
+ # `< StandardError` so inherited `Exception` methods (`message`,
2
+ # `backtrace`, …) resolve on a rescued instance.
3
+ class Rigor::Plugin::AccessDeniedError < StandardError
2
4
  def initialize: (untyped, reason: untyped, ?resource: untyped) -> void
3
5
  end
@@ -1,6 +1,61 @@
1
+ # The plugin contract every Rigor plugin subclasses. The signatures
2
+ # below are the RBS face of `lib/rigor/plugin/base.rb`; they let the
3
+ # `lib` Steep target check Base's own implementation against the
4
+ # declared surface and give plugin subclasses (once they enter a check
5
+ # target) method resolution for the DSL + hook surface.
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.
1
10
  class Rigor::Plugin::Base
2
- def self.manifest: () -> Rigor::Plugin::Manifest
11
+ # --- class-level declaration DSLs ---------------------------------
12
+
13
+ # Dual-purpose: `manifest(id:, version:, ...)` declares + caches,
14
+ # the no-arg `manifest` reads the cached value back.
15
+ def self.manifest: (**untyped fields) -> Rigor::Plugin::Manifest
16
+
17
+ def self.producer: (untyped id, ?serialize: untyped, ?deserialize: untyped) { (untyped params) -> untyped } -> Symbol
18
+ def self.producers: () -> Hash[Symbol, untyped]
19
+
20
+ def self.node_rule: (untyped node_type) { (*untyped) -> untyped } -> untyped
21
+ def self.node_rules: () -> Array[untyped]
22
+
23
+ def self.node_file_context: () { (untyped root, untyped scope) -> untyped } -> untyped
24
+ def self.node_file_context_block: () -> untyped
25
+
26
+ def self.dynamic_return: (receivers: Array[String]) { (untyped call_node, untyped scope) -> untyped } -> nil
27
+ def self.dynamic_returns: () -> Array[untyped]
28
+
29
+ def self.type_specifier: (methods: Array[untyped]) { (untyped call_node, untyped scope) -> untyped } -> nil
30
+ def self.type_specifiers: () -> Array[untyped]
31
+
32
+ def self.suggest: (untyped name, untyped candidates) -> String?
33
+
34
+ # --- instance surface --------------------------------------------
35
+
36
+ attr_reader services: untyped
37
+ attr_reader config: untyped
38
+
3
39
  def initialize: (services: untyped, ?config: untyped) -> void
4
- def init: (untyped) -> nil
5
- def prepare: (untyped) -> nil
40
+
41
+ # Overridable lifecycle / extension hooks (default no-ops).
42
+ def init: (untyped services) -> nil
43
+ def prepare: (untyped services) -> nil
44
+ def flow_contribution_for: (call_node: untyped, scope: untyped) -> untyped
45
+ def diagnostics_for_file: (path: untyped, scope: untyped, root: untyped) -> Array[untyped]
46
+
47
+ # Engine-executed dispatch over the declared DSLs.
48
+ def node_rule_diagnostics: (path: untyped, scope: untyped, root: untyped) -> Array[untyped]
49
+ def dynamic_return_type: (call_node: untyped, scope: untyped, receiver_type: untyped) -> untyped
50
+ def type_specifier_facts: (call_node: untyped, scope: untyped) -> Array[untyped]
51
+
52
+ # Authoring helpers.
53
+ def diagnostic: (untyped node, path: untyped, message: untyped, ?severity: untyped, ?rule: untyped, ?location: untyped) -> untyped
54
+ def manifest: () -> Rigor::Plugin::Manifest
55
+ def signature_paths: () -> Array[String]
56
+ def protocol_contracts: () -> untyped
57
+ def io_boundary: () -> Rigor::Plugin::IoBoundary
58
+ def cache_for: (untyped producer_id, ?params: untyped, ?descriptor: untyped) -> untyped
59
+ def glob_descriptor: (untyped roots, *untyped patterns) -> untyped
60
+ def plugin_entry: () -> untyped
6
61
  end
@@ -1,4 +1,7 @@
1
1
  class Rigor::Plugin::IoBoundary
2
2
  def initialize: (policy: untyped, plugin_id: untyped, ?http_client: untyped) -> void
3
3
  def read_file: (untyped) -> String
4
+ def open_url: (untyped url) -> String
5
+ # Returns a frozen `Rigor::Cache::Descriptor` (no RBS yet -> untyped).
6
+ def cache_descriptor: () -> untyped
4
7
  end