rigortype 0.1.15 → 0.1.16

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 (106) 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.rb +25 -1
  5. data/lib/rigor/analysis/diagnostic.rb +40 -0
  6. data/lib/rigor/analysis/runner.rb +61 -2
  7. data/lib/rigor/analysis/worker_session.rb +3 -2
  8. data/lib/rigor/cache/descriptor.rb +6 -2
  9. data/lib/rigor/cli/plugins_command.rb +51 -4
  10. data/lib/rigor/cli/plugins_renderer.rb +86 -1
  11. data/lib/rigor/cli.rb +135 -5
  12. data/lib/rigor/environment/rbs_loader.rb +259 -1
  13. data/lib/rigor/environment.rb +8 -2
  14. data/lib/rigor/inference/budget_trace.rb +137 -0
  15. data/lib/rigor/inference/expression_typer.rb +9 -2
  16. data/lib/rigor/inference/hkt_reducer.rb +2 -0
  17. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
  18. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +81 -14
  19. data/lib/rigor/inference/method_dispatcher.rb +57 -10
  20. data/lib/rigor/inference/precision_scanner.rb +60 -1
  21. data/lib/rigor/inference/scope_indexer.rb +127 -8
  22. data/lib/rigor/inference/statement_evaluator.rb +13 -8
  23. data/lib/rigor/inference/synthetic_method_index.rb +23 -4
  24. data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
  25. data/lib/rigor/plugin/additional_initializer.rb +108 -0
  26. data/lib/rigor/plugin/base.rb +321 -2
  27. data/lib/rigor/plugin/box.rb +64 -0
  28. data/lib/rigor/plugin/inflector.rb +121 -0
  29. data/lib/rigor/plugin/isolation.rb +191 -0
  30. data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
  31. data/lib/rigor/plugin/macro.rb +1 -0
  32. data/lib/rigor/plugin/manifest.rb +120 -23
  33. data/lib/rigor/plugin/node_context.rb +62 -0
  34. data/lib/rigor/plugin/registry.rb +10 -0
  35. data/lib/rigor/plugin.rb +3 -0
  36. data/lib/rigor/sig_gen/generator.rb +2 -3
  37. data/lib/rigor/sig_gen/observation_collector.rb +2 -2
  38. data/lib/rigor/source/literals.rb +118 -0
  39. data/lib/rigor/source/node_walker.rb +26 -0
  40. data/lib/rigor/source.rb +1 -0
  41. data/lib/rigor/type/combinator.rb +6 -1
  42. data/lib/rigor/type/union.rb +65 -1
  43. data/lib/rigor/version.rb +1 -1
  44. data/lib/rigor.rb +1 -0
  45. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
  46. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
  47. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
  48. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
  49. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
  50. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
  51. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
  52. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
  53. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
  54. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
  55. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  56. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
  57. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
  58. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
  59. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
  60. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
  61. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
  62. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  63. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
  64. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
  65. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
  66. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
  67. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
  68. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
  69. data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
  70. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
  71. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
  72. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
  73. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
  74. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
  75. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
  76. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
  77. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
  78. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
  79. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +36 -31
  80. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  81. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +6 -3
  82. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
  83. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
  84. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
  85. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
  86. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
  87. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
  88. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
  89. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
  90. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
  91. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
  92. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
  93. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
  94. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
  95. data/sig/rigor/plugin/access_denied_error.rbs +3 -1
  96. data/sig/rigor/plugin/base.rbs +58 -3
  97. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  98. data/sig/rigor/plugin/manifest.rbs +31 -1
  99. data/sig/rigor/source.rbs +12 -0
  100. data/sig/rigor.rbs +5 -0
  101. data/skills/rigor-plugin-author/SKILL.md +13 -9
  102. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
  103. data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
  104. data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
  105. metadata +52 -2
  106. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
+ require "rigor/source/literals"
4
5
 
5
6
  require_relative "mailer_index"
6
7
 
@@ -264,7 +265,7 @@ module Rigor
264
265
  next unless node.is_a?(Prism::CallNode) && node.receiver.nil?
265
266
 
266
267
  args = (node.arguments&.arguments || []).filter_map do |arg|
267
- arg.is_a?(Prism::SymbolNode) ? arg.unescaped.to_sym : nil
268
+ Rigor::Source::Literals.symbol(arg)
268
269
  end
269
270
  next if args.empty?
270
271
 
@@ -371,7 +372,11 @@ module Rigor
371
372
  # denied) are swallowed.
372
373
  def view_exists?(class_name, action_name)
373
374
  views_root_absolute = File.expand_path(@views_root)
374
- underscore_path = underscore(class_name.delete_prefix("::"))
375
+ # ADR-39: the real ActiveSupport::Inflector resolves the mailer's
376
+ # view directory (`Foo::BarMailer` → `foo/bar_mailer`), so a
377
+ # divergence from Rails' real underscore can't point the
378
+ # missing-view check at the wrong directory.
379
+ underscore_path = Rigor::Plugin::Inflector.underscore(class_name.delete_prefix("::"))
375
380
  mailer_dir = File.join(views_root_absolute, underscore_path)
376
381
 
377
382
  VIEW_FORMATS.any? do |format|
@@ -381,17 +386,6 @@ module Rigor
381
386
  end
382
387
  end
383
388
  end
384
-
385
- # Convert `Foo::BarMailer` → `foo/bar_mailer`. Mirrors
386
- # ActiveSupport's String#underscore for ASCII-only
387
- # constant names; we don't try to be inflector-perfect
388
- # here.
389
- def underscore(name)
390
- name.gsub("::", "/")
391
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
392
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
393
- .downcase
394
- end
395
389
  end
396
390
  end
397
391
  end
@@ -59,16 +59,12 @@ module Rigor
59
59
  version: "0.4.0",
60
60
  description: "Validates ActionMailer call shape and view template existence.",
61
61
  config_schema: {
62
- "mailer_search_paths" => :array,
63
- "mailer_base_classes" => :array,
64
- "views_root" => :string
62
+ "mailer_search_paths" => { kind: :array, default: ["app/mailers"] },
63
+ "mailer_base_classes" => { kind: :array, default: %w[ApplicationMailer ActionMailer::Base] },
64
+ "views_root" => { kind: :string, default: "app/views" }
65
65
  }
66
66
  )
67
67
 
68
- DEFAULT_MAILER_SEARCH_PATHS = ["app/mailers"].freeze
69
- DEFAULT_MAILER_BASE_CLASSES = %w[ApplicationMailer ActionMailer::Base].freeze
70
- DEFAULT_VIEWS_ROOT = "app/views"
71
-
72
68
  producer :mailer_index do |_params|
73
69
  MailerDiscoverer.new(
74
70
  io_boundary: io_boundary,
@@ -79,22 +75,33 @@ module Rigor
79
75
  end
80
76
 
81
77
  def init(_services)
82
- @mailer_search_paths = Array(config.fetch("mailer_search_paths", DEFAULT_MAILER_SEARCH_PATHS)).map(&:to_s)
83
- @mailer_base_classes = Array(config.fetch("mailer_base_classes", DEFAULT_MAILER_BASE_CLASSES)).map(&:to_s)
84
- @views_root = config.fetch("views_root", DEFAULT_VIEWS_ROOT).to_s
78
+ @mailer_search_paths = Array(config.fetch("mailer_search_paths")).map(&:to_s)
79
+ @mailer_base_classes = Array(config.fetch("mailer_base_classes")).map(&:to_s)
80
+ @views_root = config.fetch("views_root").to_s
85
81
  @mailer_index = nil
86
82
  @load_error = nil
87
83
  end
88
84
 
85
+ # File-level: load-error + the missing-view check (anchored on the
86
+ # mailer's own source file, so it is genuinely per-file, not
87
+ # per-call). The per-call mailer validation (action existence /
88
+ # arity) runs over the engine-owned walk via the node_rule below
89
+ # (ADR-37). The mailer index is lazily loaded + memoised, shared.
89
90
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
90
91
  index = mailer_index_or_nil
91
92
  return [load_error_diagnostic(path)] if index.nil? && @load_error
92
93
  return [] if index.nil? || index.empty?
93
94
 
94
- diagnostics = []
95
- diagnostics.concat(call_site_diagnostics(path, root, index))
96
- diagnostics.concat(missing_view_diagnostics(path, index))
97
- diagnostics
95
+ missing_view_diagnostics(path, index)
96
+ end
97
+
98
+ node_rule Prism::CallNode do |node, _scope, path|
99
+ index = mailer_index_or_nil
100
+ next [] if index.nil? || index.empty?
101
+
102
+ Analyzer.violations_for(call_node: node, mailer_index: index).map do |violation|
103
+ diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
104
+ end
98
105
  end
99
106
 
100
107
  private
@@ -121,10 +128,6 @@ module Rigor
121
128
  nil
122
129
  end
123
130
 
124
- def call_site_diagnostics(path, root, index)
125
- Analyzer.diagnose(path: path, root: root, mailer_index: index).map { |diag| build_diagnostic(diag) }
126
- end
127
-
128
131
  # Anchors `missing-view` diagnostics on the mailer file
129
132
  # itself: when the file currently being analysed is the
130
133
  # mailer's source file, emit one diagnostic per missing
@@ -143,7 +146,7 @@ module Rigor
143
146
  severity: :warning,
144
147
  rule: "missing-view",
145
148
  message: "`#{class_entry.class_name}##{action_name}` has no view template " \
146
- "under `#{@views_root}/#{underscore(class_entry.class_name.delete_prefix('::'))}/`"
149
+ "under `#{@views_root}/#{Rigor::Plugin::Inflector.underscore(class_entry.class_name.delete_prefix('::'))}/`"
147
150
  )
148
151
  end
149
152
  end
@@ -154,13 +157,6 @@ module Rigor
154
157
  File.expand_path(path)
155
158
  end
156
159
 
157
- def underscore(name)
158
- name.gsub("::", "/")
159
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
160
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
161
- .downcase
162
- end
163
-
164
160
  def load_error_diagnostic(path)
165
161
  Rigor::Analysis::Diagnostic.new(
166
162
  path: path, line: 1, column: 1,
@@ -169,13 +165,6 @@ module Rigor
169
165
  rule: "load-error"
170
166
  )
171
167
  end
172
-
173
- def build_diagnostic(diag)
174
- Rigor::Analysis::Diagnostic.new(
175
- path: diag.path, line: diag.line, column: diag.column,
176
- message: diag.message, severity: diag.severity, rule: diag.rule
177
- )
178
- end
179
168
  end
180
169
 
181
170
  Rigor::Plugin.register(Actionmailer)