rigortype 0.1.14 → 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.
- checksums.yaml +4 -4
- data/README.md +10 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules.rb +428 -6
- data/lib/rigor/analysis/diagnostic.rb +55 -3
- data/lib/rigor/analysis/rule_catalog.rb +80 -0
- data/lib/rigor/analysis/runner.rb +71 -2
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/cache/descriptor.rb +6 -2
- data/lib/rigor/cli/plugin_command.rb +245 -0
- data/lib/rigor/cli/plugins_command.rb +51 -4
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli.rb +143 -5
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/environment/rbs_loader.rb +259 -1
- data/lib/rigor/environment.rb +8 -2
- data/lib/rigor/inference/budget_trace.rb +137 -0
- data/lib/rigor/inference/expression_typer.rb +9 -2
- data/lib/rigor/inference/hkt_reducer.rb +2 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +81 -14
- data/lib/rigor/inference/method_dispatcher.rb +57 -10
- data/lib/rigor/inference/precision_scanner.rb +60 -1
- data/lib/rigor/inference/scope_indexer.rb +184 -27
- data/lib/rigor/inference/statement_evaluator.rb +13 -8
- data/lib/rigor/inference/synthetic_method_index.rb +23 -4
- data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
- data/lib/rigor/plugin/additional_initializer.rb +108 -0
- data/lib/rigor/plugin/base.rb +321 -2
- data/lib/rigor/plugin/box.rb +64 -0
- data/lib/rigor/plugin/inflector.rb +121 -0
- data/lib/rigor/plugin/isolation.rb +191 -0
- data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
- data/lib/rigor/plugin/macro.rb +1 -0
- data/lib/rigor/plugin/manifest.rb +120 -23
- data/lib/rigor/plugin/node_context.rb +62 -0
- data/lib/rigor/plugin/registry.rb +10 -0
- data/lib/rigor/plugin.rb +3 -0
- data/lib/rigor/scope.rb +27 -1
- data/lib/rigor/sig_gen/generator.rb +2 -3
- data/lib/rigor/sig_gen/observation_collector.rb +2 -2
- data/lib/rigor/source/literals.rb +118 -0
- data/lib/rigor/source/node_walker.rb +26 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +71 -5
- data/lib/rigor/type/combinator.rb +6 -1
- data/lib/rigor/type/union.rb +65 -1
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
- data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +36 -31
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +6 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
- data/sig/rigor/plugin/access_denied_error.rbs +3 -1
- data/sig/rigor/plugin/base.rbs +58 -3
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/plugin/manifest.rbs +31 -1
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor/source.rbs +12 -0
- data/sig/rigor.rbs +5 -0
- data/skills/rigor-plugin-author/SKILL.md +33 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +65 -26
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +213 -80
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- data/skills/rigor-project-init/SKILL.md +72 -7
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +233 -19
- metadata +53 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
|
@@ -62,17 +62,14 @@ module Rigor
|
|
|
62
62
|
version: "0.1.0",
|
|
63
63
|
description: "Validates ActionCable broadcast call shape against discovered channels.",
|
|
64
64
|
config_schema: {
|
|
65
|
-
"channel_search_paths" => :array,
|
|
66
|
-
"channel_base_classes" =>
|
|
65
|
+
"channel_search_paths" => { kind: :array, default: ["app/channels"] },
|
|
66
|
+
"channel_base_classes" => {
|
|
67
|
+
kind: :array,
|
|
68
|
+
default: ["ApplicationCable::Channel", "ActionCable::Channel::Base"]
|
|
69
|
+
}
|
|
67
70
|
}
|
|
68
71
|
)
|
|
69
72
|
|
|
70
|
-
DEFAULT_CHANNEL_SEARCH_PATHS = ["app/channels"].freeze
|
|
71
|
-
DEFAULT_CHANNEL_BASE_CLASSES = [
|
|
72
|
-
"ApplicationCable::Channel",
|
|
73
|
-
"ActionCable::Channel::Base"
|
|
74
|
-
].freeze
|
|
75
|
-
|
|
76
73
|
producer :channel_index do |_params|
|
|
77
74
|
ChannelDiscoverer.new(
|
|
78
75
|
io_boundary: io_boundary,
|
|
@@ -82,22 +79,30 @@ module Rigor
|
|
|
82
79
|
end
|
|
83
80
|
|
|
84
81
|
def init(_services)
|
|
85
|
-
@channel_search_paths = Array(
|
|
86
|
-
|
|
87
|
-
).map(&:to_s)
|
|
88
|
-
@channel_base_classes = Array(
|
|
89
|
-
config.fetch("channel_base_classes", DEFAULT_CHANNEL_BASE_CLASSES)
|
|
90
|
-
).map(&:to_s)
|
|
82
|
+
@channel_search_paths = Array(config.fetch("channel_search_paths")).map(&:to_s)
|
|
83
|
+
@channel_base_classes = Array(config.fetch("channel_base_classes")).map(&:to_s)
|
|
91
84
|
@channel_index = nil
|
|
92
85
|
@load_error = nil
|
|
93
86
|
end
|
|
94
87
|
|
|
88
|
+
# File-level only: the load-error emission. Per-call broadcast
|
|
89
|
+
# validation runs over the engine-owned walk via the node_rule
|
|
90
|
+
# below (ADR-37). The channel index is lazily loaded + memoised by
|
|
91
|
+
# channel_index_or_nil, shared by both surfaces.
|
|
95
92
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
96
93
|
index = channel_index_or_nil
|
|
97
94
|
return [load_error_diagnostic(path)] if index.nil? && @load_error
|
|
98
|
-
return [] if index.nil? || index.empty?
|
|
99
95
|
|
|
100
|
-
|
|
96
|
+
[]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
node_rule Prism::CallNode do |node, _scope, path|
|
|
100
|
+
index = channel_index_or_nil
|
|
101
|
+
next [] if index.nil? || index.empty?
|
|
102
|
+
|
|
103
|
+
Analyzer.violations_for(call_node: node, channel_index: index).map do |violation|
|
|
104
|
+
diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
|
|
105
|
+
end
|
|
101
106
|
end
|
|
102
107
|
|
|
103
108
|
private
|
|
@@ -128,13 +133,6 @@ module Rigor
|
|
|
128
133
|
rule: "load-error"
|
|
129
134
|
)
|
|
130
135
|
end
|
|
131
|
-
|
|
132
|
-
def build_diagnostic(diag)
|
|
133
|
-
Rigor::Analysis::Diagnostic.new(
|
|
134
|
-
path: diag.path, line: diag.line, column: diag.column,
|
|
135
|
-
message: diag.message, severity: diag.severity, rule: diag.rule
|
|
136
|
-
)
|
|
137
|
-
end
|
|
138
136
|
end
|
|
139
137
|
|
|
140
138
|
Rigor::Plugin.register(Actioncable)
|
|
@@ -45,53 +45,44 @@ module Rigor
|
|
|
45
45
|
method instance_method methods
|
|
46
46
|
].freeze
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
# One mailer-call observation. Carries no path/location — the
|
|
49
|
+
# caller (the `node_rule` block) positions it via
|
|
50
|
+
# `Plugin::Base#diagnostic`.
|
|
51
|
+
Violation = Struct.new(:rule, :severity, :message, keyword_init: true)
|
|
49
52
|
|
|
50
53
|
module_function
|
|
51
54
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
55
|
+
# The mailer-call violations for a single call node (0..2), or
|
|
56
|
+
# `[]` when it is not a `<Mailer>.action(...)` call on a known
|
|
57
|
+
# mailer. ADR-37: the engine owns the walk.
|
|
58
|
+
#
|
|
59
|
+
# @param call_node [Prism::Node]
|
|
54
60
|
# @param mailer_index [MailerIndex]
|
|
55
|
-
# @return [Array<
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class_entry = mailer_index.find(class_name) || mailer_index.find("::#{class_name}")
|
|
64
|
-
next if class_entry.nil?
|
|
65
|
-
|
|
66
|
-
action_entry = class_entry.find_action(call_node.name)
|
|
67
|
-
if action_entry.nil?
|
|
68
|
-
# Skip `unknown-action` when the mailer's include
|
|
69
|
-
# set has any unresolved module — the unresolved
|
|
70
|
-
# module may legitimately define the action
|
|
71
|
-
# (gem-shipped concern, dynamically loaded
|
|
72
|
-
# mailer extension). Mirrors the same predicate
|
|
73
|
-
# `rigor-actionpack` uses for unknown-filter-method.
|
|
74
|
-
next if class_entry.unresolved_includes?
|
|
75
|
-
|
|
76
|
-
diagnostics << unknown_action_diagnostic(path, call_node, class_entry)
|
|
77
|
-
next
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
diagnostics << action_call_info(path, call_node, class_entry, action_entry)
|
|
81
|
-
arity_diag = arity_check(path, call_node, class_entry, action_entry)
|
|
82
|
-
diagnostics << arity_diag if arity_diag
|
|
83
|
-
end
|
|
84
|
-
diagnostics
|
|
85
|
-
end
|
|
61
|
+
# @return [Array<Violation>]
|
|
62
|
+
def violations_for(call_node:, mailer_index:)
|
|
63
|
+
return [] unless call_node.is_a?(Prism::CallNode) && action_call_candidate?(call_node)
|
|
64
|
+
|
|
65
|
+
class_name = mailer_class_for_call(call_node)
|
|
66
|
+
return [] if class_name.nil?
|
|
67
|
+
return [] if RESERVED_CLASS_METHODS.include?(call_node.name)
|
|
86
68
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
69
|
+
class_entry = mailer_index.find(class_name) || mailer_index.find("::#{class_name}")
|
|
70
|
+
return [] if class_entry.nil?
|
|
71
|
+
|
|
72
|
+
action_entry = class_entry.find_action(call_node.name)
|
|
73
|
+
if action_entry.nil?
|
|
74
|
+
# Skip `unknown-action` when the mailer's include set has any
|
|
75
|
+
# unresolved module — it may legitimately define the action
|
|
76
|
+
# (gem-shipped concern, dynamically loaded extension).
|
|
77
|
+
return [] if class_entry.unresolved_includes?
|
|
78
|
+
|
|
79
|
+
return [unknown_action_violation(call_node, class_entry)]
|
|
80
|
+
end
|
|
92
81
|
|
|
93
|
-
|
|
94
|
-
|
|
82
|
+
violations = [action_call_info(call_node, class_entry, action_entry)]
|
|
83
|
+
arity = arity_violation(call_node, class_entry, action_entry)
|
|
84
|
+
violations << arity if arity
|
|
85
|
+
violations
|
|
95
86
|
end
|
|
96
87
|
|
|
97
88
|
def action_call_candidate?(node)
|
|
@@ -120,12 +111,8 @@ module Rigor
|
|
|
120
111
|
end
|
|
121
112
|
end
|
|
122
113
|
|
|
123
|
-
def action_call_info(
|
|
124
|
-
|
|
125
|
-
Diagnostic.new(
|
|
126
|
-
path: path,
|
|
127
|
-
line: location.start_line,
|
|
128
|
-
column: location.start_column + 1,
|
|
114
|
+
def action_call_info(_call_node, class_entry, action_entry)
|
|
115
|
+
Violation.new(
|
|
129
116
|
severity: :info,
|
|
130
117
|
rule: "mailer-call",
|
|
131
118
|
message: "`#{class_entry.class_name}.#{action_entry.method_name}` " \
|
|
@@ -133,7 +120,7 @@ module Rigor
|
|
|
133
120
|
)
|
|
134
121
|
end
|
|
135
122
|
|
|
136
|
-
def
|
|
123
|
+
def arity_violation(call_node, class_entry, action_entry)
|
|
137
124
|
args = call_node.arguments&.arguments || []
|
|
138
125
|
actual = args.size
|
|
139
126
|
return nil if action_entry.accepts?(actual)
|
|
@@ -147,11 +134,7 @@ module Rigor
|
|
|
147
134
|
# carrying calls don't surface as wrong-arity.
|
|
148
135
|
return nil if args.last.is_a?(Prism::KeywordHashNode) && action_entry.accepts?(actual - 1)
|
|
149
136
|
|
|
150
|
-
|
|
151
|
-
Diagnostic.new(
|
|
152
|
-
path: path,
|
|
153
|
-
line: location.start_line,
|
|
154
|
-
column: location.start_column + 1,
|
|
137
|
+
Violation.new(
|
|
155
138
|
severity: :error,
|
|
156
139
|
rule: "wrong-arity",
|
|
157
140
|
message: "`#{class_entry.class_name}.#{action_entry.method_name}` " \
|
|
@@ -159,14 +142,10 @@ module Rigor
|
|
|
159
142
|
)
|
|
160
143
|
end
|
|
161
144
|
|
|
162
|
-
def
|
|
163
|
-
location = call_node.location
|
|
145
|
+
def unknown_action_violation(call_node, class_entry)
|
|
164
146
|
known = class_entry.actions.keys.sort.join(", ")
|
|
165
147
|
known_part = known.empty? ? "no actions defined" : "known actions: #{known}"
|
|
166
|
-
|
|
167
|
-
path: path,
|
|
168
|
-
line: location.start_line,
|
|
169
|
-
column: location.start_column + 1,
|
|
148
|
+
Violation.new(
|
|
170
149
|
severity: :error,
|
|
171
150
|
rule: "unknown-action",
|
|
172
151
|
message: "`#{class_entry.class_name}.#{call_node.name}` is not a defined " \
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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"
|
|
83
|
-
@mailer_base_classes = Array(config.fetch("mailer_base_classes"
|
|
84
|
-
@views_root = config.fetch("views_root"
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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)
|