rigortype 0.1.10 → 0.1.12

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 (144) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rigor/analysis/baseline.rb +51 -15
  3. data/lib/rigor/analysis/erb_template_detector.rb +38 -0
  4. data/lib/rigor/analysis/runner.rb +6 -1
  5. data/lib/rigor/analysis/worker_session.rb +6 -1
  6. data/lib/rigor/cli/baseline_command.rb +4 -3
  7. data/lib/rigor/cli/plugins_command.rb +308 -0
  8. data/lib/rigor/cli/plugins_renderer.rb +173 -0
  9. data/lib/rigor/cli.rb +44 -3
  10. data/lib/rigor/inference/block_parameter_binder.rb +35 -0
  11. data/lib/rigor/inference/expression_typer.rb +69 -30
  12. data/lib/rigor/inference/indexed_narrowing.rb +187 -0
  13. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
  14. data/lib/rigor/inference/method_dispatcher.rb +23 -0
  15. data/lib/rigor/inference/mutation_widening.rb +285 -0
  16. data/lib/rigor/inference/narrowing.rb +72 -4
  17. data/lib/rigor/inference/scope_indexer.rb +409 -12
  18. data/lib/rigor/inference/statement_evaluator.rb +256 -4
  19. data/lib/rigor/scope.rb +181 -4
  20. data/lib/rigor/version.rb +1 -1
  21. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +190 -0
  22. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +189 -0
  23. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +81 -0
  24. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +142 -0
  25. data/plugins/rigor-actioncable/lib/rigor-actioncable.rb +3 -0
  26. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +199 -0
  27. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +398 -0
  28. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +86 -0
  29. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +183 -0
  30. data/plugins/rigor-actionmailer/lib/rigor-actionmailer.rb +3 -0
  31. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +713 -0
  32. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +201 -0
  33. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +226 -0
  34. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +261 -0
  35. data/plugins/rigor-actionpack/lib/rigor-actionpack.rb +3 -0
  36. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +114 -0
  37. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +177 -0
  38. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +65 -0
  39. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +117 -0
  40. data/plugins/rigor-activejob/lib/rigor-activejob.rb +3 -0
  41. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +283 -0
  42. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +114 -0
  43. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +561 -0
  44. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +194 -0
  45. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +250 -0
  46. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +98 -0
  47. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +590 -0
  48. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +8 -0
  49. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +182 -0
  50. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +78 -0
  51. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +162 -0
  52. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +43 -0
  53. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +170 -0
  54. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +8 -0
  55. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +37 -0
  56. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +20 -0
  57. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +478 -0
  58. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +108 -0
  59. data/plugins/rigor-devise/lib/rigor-devise.rb +8 -0
  60. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +285 -0
  61. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +124 -0
  62. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +8 -0
  63. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +116 -0
  64. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +8 -0
  65. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +341 -0
  66. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +120 -0
  67. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +8 -0
  68. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +120 -0
  69. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +85 -0
  70. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +7 -0
  71. data/plugins/rigor-dry-validation/sig/dry_validation.rbs +25 -0
  72. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +177 -0
  73. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +242 -0
  74. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +56 -0
  75. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +174 -0
  76. data/plugins/rigor-factorybot/lib/rigor-factorybot.rb +3 -0
  77. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +409 -0
  78. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +114 -0
  79. data/plugins/rigor-graphql/lib/rigor-graphql.rb +8 -0
  80. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +124 -0
  81. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +111 -0
  82. data/plugins/rigor-hanami/lib/rigor-hanami.rb +3 -0
  83. data/plugins/rigor-hanami/sig/hanami_action.rbs +78 -0
  84. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +302 -0
  85. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +72 -0
  86. data/plugins/rigor-minitest/lib/rigor-minitest.rb +3 -0
  87. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +194 -0
  88. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +140 -0
  89. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +65 -0
  90. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +130 -0
  91. data/plugins/rigor-pundit/lib/rigor-pundit.rb +3 -0
  92. data/plugins/rigor-rails/lib/rigor-rails.rb +31 -0
  93. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +353 -0
  94. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +108 -0
  95. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +138 -0
  96. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +175 -0
  97. data/plugins/rigor-rails-i18n/lib/rigor-rails-i18n.rb +3 -0
  98. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +350 -0
  99. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
  100. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
  101. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
  102. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +164 -0
  103. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1538 -0
  104. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +235 -0
  105. data/plugins/rigor-rails-routes/lib/rigor-rails-routes.rb +3 -0
  106. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +163 -0
  107. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +24 -0
  108. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +110 -0
  109. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +200 -0
  110. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +170 -0
  111. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +233 -0
  112. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +190 -0
  113. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +188 -0
  114. data/plugins/rigor-rspec/lib/rigor-rspec.rb +3 -0
  115. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +128 -0
  116. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +60 -0
  117. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +75 -0
  118. data/plugins/rigor-rspec-rails/lib/rigor-rspec-rails.rb +3 -0
  119. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +266 -0
  120. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +113 -0
  121. data/plugins/rigor-shoulda-matchers/lib/rigor-shoulda-matchers.rb +3 -0
  122. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +152 -0
  123. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +190 -0
  124. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +61 -0
  125. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +124 -0
  126. data/plugins/rigor-sidekiq/lib/rigor-sidekiq.rb +3 -0
  127. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +85 -0
  128. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +8 -0
  129. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +108 -0
  130. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +250 -0
  131. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +95 -0
  132. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +226 -0
  133. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +28 -0
  134. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +154 -0
  135. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +100 -0
  136. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +323 -0
  137. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +660 -0
  138. data/plugins/rigor-sorbet/lib/rigor-sorbet.rb +3 -0
  139. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +209 -0
  140. data/plugins/rigor-statesman/lib/rigor-statesman.rb +8 -0
  141. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +163 -0
  142. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +9 -0
  143. data/sig/rigor/scope.rbs +22 -0
  144. metadata +157 -1
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rigor/plugin"
4
+
5
+ require_relative "actionmailer/mailer_index"
6
+ require_relative "actionmailer/mailer_discoverer"
7
+ require_relative "actionmailer/analyzer"
8
+
9
+ module Rigor
10
+ module Plugin
11
+ # rigor-actionmailer — validates `Mailer.action(args)`
12
+ # call sites and detects missing view templates.
13
+ #
14
+ # Tier 1C of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
15
+ # Statically discovers mailer classes by walking
16
+ # `mailer_search_paths` and parsing each file with
17
+ # Prism — no `action_mailer` runtime dependency.
18
+ #
19
+ # ## Configuration
20
+ #
21
+ # plugins:
22
+ # - gem: rigor-actionmailer
23
+ # config:
24
+ # mailer_search_paths: ["app/mailers"] # default; optional
25
+ # mailer_base_classes: ["ApplicationMailer", "ActionMailer::Base"] # default; optional
26
+ # views_root: "app/views" # default; optional
27
+ #
28
+ # ## What it checks
29
+ #
30
+ # 1. **Method existence** — `UserMailer.welcome(user)`
31
+ # is flagged when `welcome` is not defined on
32
+ # `UserMailer`.
33
+ # 2. **Argument arity** — calls with too few / too many
34
+ # positional arguments emit `wrong-arity`.
35
+ # 3. **View template existence** — for every action
36
+ # method, at least one of
37
+ # `app/views/<mailer_underscore>/<action>.{html,text}.{erb,haml,slim}`
38
+ # must exist. Missing actions get a `missing-view`
39
+ # diagnostic anchored on the action's `def`.
40
+ #
41
+ # ## Limitations (v0.1.0)
42
+ #
43
+ # - Direct-superclass match only.
44
+ # - Action methods are read from the syntactic instance-
45
+ # side `def` list. `define_method` actions are out of
46
+ # scope.
47
+ # - Adding a brand-new view file does not invalidate the
48
+ # cache until something the mailer file touches
49
+ # changes.
50
+ class Actionmailer < Rigor::Plugin::Base
51
+ manifest(
52
+ id: "actionmailer",
53
+ # Bumped 2026-05-28 — extended RESERVED_CLASS_METHODS to
54
+ # include `respond_to?` / `public_send` / `send` /
55
+ # `__send__` / `method` and friends so dynamic-dispatch
56
+ # idioms (`Mailer.respond_to?(action)` /
57
+ # `Mailer.public_send(action)`) stop firing
58
+ # `unknown-action` against the Ruby reflection method.
59
+ version: "0.4.0",
60
+ description: "Validates ActionMailer call shape and view template existence.",
61
+ config_schema: {
62
+ "mailer_search_paths" => :array,
63
+ "mailer_base_classes" => :array,
64
+ "views_root" => :string
65
+ }
66
+ )
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
+ producer :mailer_index do |_params|
73
+ MailerDiscoverer.new(
74
+ io_boundary: io_boundary,
75
+ search_paths: @mailer_search_paths,
76
+ base_classes: @mailer_base_classes,
77
+ views_root: @views_root
78
+ ).discover
79
+ end
80
+
81
+ 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
85
+ @mailer_index = nil
86
+ @load_error = nil
87
+ end
88
+
89
+ def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
90
+ index = mailer_index_or_nil
91
+ return [load_error_diagnostic(path)] if index.nil? && @load_error
92
+ return [] if index.nil? || index.empty?
93
+
94
+ diagnostics = []
95
+ diagnostics.concat(call_site_diagnostics(path, root, index))
96
+ diagnostics.concat(missing_view_diagnostics(path, index))
97
+ diagnostics
98
+ end
99
+
100
+ private
101
+
102
+ def mailer_index_or_nil
103
+ return @mailer_index if @mailer_index
104
+
105
+ # Two-glob descriptor: every mailer class under
106
+ # `mailer_search_paths` AND every view template under
107
+ # `views_root`. Without explicit enumeration the cache
108
+ # invalidates only on files the `IoBoundary` has already
109
+ # read in the current process — empty on the first call
110
+ # of a fresh process, so warm hits would serve stale
111
+ # `MailerIndex` data after mailers are added / removed or
112
+ # view templates are added (`view_exists?` failures aren't
113
+ # recorded, so the auto-built descriptor cannot detect a
114
+ # newly-added view).
115
+ mailer_d = glob_descriptor(@mailer_search_paths, "**/*.rb")
116
+ view_d = glob_descriptor([@views_root], "**/*")
117
+ descriptor = Rigor::Cache::Descriptor.compose(mailer_d, view_d)
118
+ @mailer_index = cache_for(:mailer_index, params: {}, descriptor: descriptor).call
119
+ rescue StandardError => e
120
+ @load_error = "rigor-actionmailer: failed to discover mailers: #{e.class}: #{e.message}"
121
+ nil
122
+ end
123
+
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
+ # Anchors `missing-view` diagnostics on the mailer file
129
+ # itself: when the file currently being analysed is the
130
+ # mailer's source file, emit one diagnostic per missing
131
+ # action template at the action's `def` location.
132
+ def missing_view_diagnostics(path, index)
133
+ canonical = canonical_path(path)
134
+ class_entry = index.find_by_file(canonical)
135
+ return [] if class_entry.nil? || class_entry.missing_views.empty?
136
+
137
+ class_entry.missing_views.map do |action_name|
138
+ action_entry = class_entry.find_action(action_name)
139
+ Rigor::Analysis::Diagnostic.new(
140
+ path: path,
141
+ line: action_entry&.def_line || 1,
142
+ column: action_entry&.def_column || 1,
143
+ severity: :warning,
144
+ rule: "missing-view",
145
+ message: "`#{class_entry.class_name}##{action_name}` has no view template " \
146
+ "under `#{@views_root}/#{underscore(class_entry.class_name.delete_prefix('::'))}/`"
147
+ )
148
+ end
149
+ end
150
+
151
+ def canonical_path(path)
152
+ File.realpath(path)
153
+ rescue StandardError
154
+ File.expand_path(path)
155
+ end
156
+
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
+ def load_error_diagnostic(path)
165
+ Rigor::Analysis::Diagnostic.new(
166
+ path: path, line: 1, column: 1,
167
+ message: @load_error,
168
+ severity: :warning,
169
+ rule: "load-error"
170
+ )
171
+ 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
+ end
180
+
181
+ Rigor::Plugin.register(Actionmailer)
182
+ end
183
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rigor/plugin/actionmailer"