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.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules.rb +25 -1
- data/lib/rigor/analysis/diagnostic.rb +40 -0
- data/lib/rigor/analysis/runner.rb +61 -2
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/cache/descriptor.rb +6 -2
- data/lib/rigor/cli/plugins_command.rb +51 -4
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli.rb +135 -5
- 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 +127 -8
- 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/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/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/source.rbs +12 -0
- data/sig/rigor.rbs +5 -0
- data/skills/rigor-plugin-author/SKILL.md +13 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- metadata +52 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
|
@@ -68,22 +68,22 @@ module Rigor
|
|
|
68
68
|
class Actionpack < Rigor::Plugin::Base
|
|
69
69
|
manifest(
|
|
70
70
|
id: "actionpack",
|
|
71
|
-
# Bumped 2026-
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# `
|
|
79
|
-
#
|
|
80
|
-
# `domain_blocks/new`) and filter-chain validation
|
|
81
|
-
#
|
|
82
|
-
version: "0.
|
|
71
|
+
# Bumped 2026-06-02 — ADR-37 node_rule migration. The four
|
|
72
|
+
# phases (helper / filter / render / strong-params) now run
|
|
73
|
+
# per-call over the engine-owned walk instead of the
|
|
74
|
+
# hand-rolled `diagnostics_for_file` traversal; the enclosing
|
|
75
|
+
# controller is read from the node-rule `NodeContext` ancestors.
|
|
76
|
+
# Nested-module qualification is preserved — a
|
|
77
|
+
# `module Admin; class DomainBlocksController; end` file still
|
|
78
|
+
# resolves as `Admin::DomainBlocksController` (matching the
|
|
79
|
+
# `ControllerDiscoverer`), so render paths
|
|
80
|
+
# (`admin/domain_blocks/new`) and filter-chain validation on
|
|
81
|
+
# nested controllers are unchanged.
|
|
82
|
+
version: "0.8.0",
|
|
83
83
|
description: "Validates Action Pack route-helper calls and filter chains inside controllers.",
|
|
84
84
|
config_schema: {
|
|
85
|
-
"controller_search_paths" => :array,
|
|
86
|
-
"view_search_paths" => :array
|
|
85
|
+
"controller_search_paths" => { kind: :array, default: ["app/controllers"] },
|
|
86
|
+
"view_search_paths" => { kind: :array, default: ["app/views"] }
|
|
87
87
|
},
|
|
88
88
|
consumes: [
|
|
89
89
|
{ plugin_id: "rails-routes", name: :helper_table, optional: true },
|
|
@@ -91,9 +91,6 @@ module Rigor
|
|
|
91
91
|
]
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
DEFAULT_CONTROLLER_SEARCH_PATHS = ["app/controllers"].freeze
|
|
95
|
-
DEFAULT_VIEW_SEARCH_PATHS = ["app/views"].freeze
|
|
96
|
-
|
|
97
94
|
# Phase 2 cached producer — the controller index built
|
|
98
95
|
# from `controller_search_paths`. The IoBoundary records
|
|
99
96
|
# a `FileEntry` digest for every file the discoverer
|
|
@@ -108,12 +105,8 @@ module Rigor
|
|
|
108
105
|
|
|
109
106
|
def init(services)
|
|
110
107
|
@services = services
|
|
111
|
-
@controller_search_paths = Array(
|
|
112
|
-
|
|
113
|
-
).map(&:to_s)
|
|
114
|
-
@view_search_paths = Array(
|
|
115
|
-
config.fetch("view_search_paths", DEFAULT_VIEW_SEARCH_PATHS)
|
|
116
|
-
).map(&:to_s)
|
|
108
|
+
@controller_search_paths = Array(config.fetch("controller_search_paths")).map(&:to_s)
|
|
109
|
+
@view_search_paths = Array(config.fetch("view_search_paths")).map(&:to_s)
|
|
117
110
|
@helper_table = nil
|
|
118
111
|
@helper_table_resolved = false
|
|
119
112
|
@controller_index = nil
|
|
@@ -121,63 +114,75 @@ module Rigor
|
|
|
121
114
|
@model_index_resolved = false
|
|
122
115
|
end
|
|
123
116
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
117
|
+
# ADR-37 — the four Action Pack phases run per-call over the
|
|
118
|
+
# engine-owned walk. Each rule gates on `controller_file?(path)` (the
|
|
119
|
+
# plugin only validates files under `controller_search_paths`,
|
|
120
|
+
# exactly as the former `diagnostics_for_file` top-level guard did),
|
|
121
|
+
# then delegates to a per-node `Analyzer.*_violations_for` and
|
|
122
|
+
# positions each location-free `Violation` with `Base#diagnostic`.
|
|
123
|
+
# The filter / render phases read the enclosing controller from the
|
|
124
|
+
# node-rule `NodeContext` ancestors (its fifth block argument).
|
|
132
125
|
|
|
133
|
-
|
|
126
|
+
# Phase 4 — route-helper consumption.
|
|
127
|
+
node_rule Prism::CallNode do |node, _scope, path|
|
|
128
|
+
next [] unless controller_file?(path)
|
|
134
129
|
|
|
135
|
-
def helper_diagnostics(path, root)
|
|
136
130
|
table = helper_table
|
|
137
|
-
|
|
131
|
+
next [] if table.nil? || table.empty?
|
|
138
132
|
|
|
139
|
-
Analyzer.
|
|
140
|
-
|
|
133
|
+
Analyzer.helper_violations_for(call_node: node, helper_table: table).map do |v|
|
|
134
|
+
diagnostic(node, path: path, location: v.location, message: v.message, severity: v.severity, rule: v.rule)
|
|
135
|
+
end
|
|
141
136
|
end
|
|
142
137
|
|
|
143
|
-
# Phase 2 —
|
|
144
|
-
# controller
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
138
|
+
# Phase 2 — filter-chain validation. Skips silently when the
|
|
139
|
+
# controller index is absent or doesn't recognise the enclosing
|
|
140
|
+
# class.
|
|
141
|
+
node_rule Prism::CallNode do |node, _scope, path, _fc, context|
|
|
142
|
+
next [] unless controller_file?(path)
|
|
143
|
+
|
|
148
144
|
index = controller_index_or_nil
|
|
149
|
-
|
|
145
|
+
next [] if index.nil? || index.empty?
|
|
150
146
|
|
|
151
|
-
Analyzer.
|
|
152
|
-
|
|
147
|
+
Analyzer.filter_violations_for(call_node: node, ancestors: context.ancestors, controller_index: index).map do |v|
|
|
148
|
+
diagnostic(node, path: path, location: v.location, message: v.message, severity: v.severity, rule: v.rule)
|
|
149
|
+
end
|
|
153
150
|
end
|
|
154
151
|
|
|
155
|
-
# Phase 3 —
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
152
|
+
# Phase 3 — render-target validation against the configured
|
|
153
|
+
# `view_search_paths`. Recognised purely from the call site + the
|
|
154
|
+
# enclosing controller name, so no per-controller pre-discovery is
|
|
155
|
+
# needed; the controller index is consulted only to suppress
|
|
156
|
+
# gem-shipped-view false positives.
|
|
157
|
+
node_rule Prism::CallNode do |node, _scope, path, _fc, context|
|
|
158
|
+
next [] unless controller_file?(path)
|
|
159
|
+
|
|
160
|
+
Analyzer.render_violations_for(
|
|
161
|
+
call_node: node, ancestors: context.ancestors, path: path,
|
|
162
|
+
view_search_roots: @view_search_paths, controller_index: controller_index_or_nil
|
|
163
|
+
).map do |v|
|
|
164
|
+
diagnostic(node, path: path, location: v.location, message: v.message, severity: v.severity, rule: v.rule)
|
|
165
|
+
end
|
|
166
166
|
end
|
|
167
167
|
|
|
168
|
-
# Phase 1 — strong-parameter validation. Reads the
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
# `params.require(:user).permit(:name, :email)` chain
|
|
172
|
-
#
|
|
173
|
-
|
|
168
|
+
# Phase 1 — strong-parameter validation. Reads the `:model_index`
|
|
169
|
+
# fact from the cross-plugin fact store (published by
|
|
170
|
+
# rigor-activerecord) and validates every
|
|
171
|
+
# `params.require(:user).permit(:name, :email)` chain against the
|
|
172
|
+
# User model's column list.
|
|
173
|
+
node_rule Prism::CallNode do |node, _scope, path|
|
|
174
|
+
next [] unless controller_file?(path)
|
|
175
|
+
|
|
174
176
|
index = model_index
|
|
175
|
-
|
|
177
|
+
next [] if index.nil? || index.empty?
|
|
176
178
|
|
|
177
|
-
Analyzer.
|
|
178
|
-
|
|
179
|
+
Analyzer.permit_violations_for(call_node: node, model_index: index).map do |v|
|
|
180
|
+
diagnostic(node, path: path, location: v.location, message: v.message, severity: v.severity, rule: v.rule)
|
|
181
|
+
end
|
|
179
182
|
end
|
|
180
183
|
|
|
184
|
+
private
|
|
185
|
+
|
|
181
186
|
def controller_index_or_nil
|
|
182
187
|
return @controller_index if @controller_index
|
|
183
188
|
|
|
@@ -247,13 +252,6 @@ module Rigor
|
|
|
247
252
|
path.include?("/#{root}/") || path.start_with?("#{root}/") || path == root
|
|
248
253
|
end
|
|
249
254
|
end
|
|
250
|
-
|
|
251
|
-
def build_diagnostic(diag)
|
|
252
|
-
Rigor::Analysis::Diagnostic.new(
|
|
253
|
-
path: diag.path, line: diag.line, column: diag.column,
|
|
254
|
-
message: diag.message, severity: diag.severity, rule: diag.rule
|
|
255
|
-
)
|
|
256
|
-
end
|
|
257
255
|
end
|
|
258
256
|
|
|
259
257
|
Rigor::Plugin.register(Actionpack)
|
|
@@ -23,35 +23,33 @@ module Rigor
|
|
|
23
23
|
# the bare execution path.
|
|
24
24
|
ENTRY_METHODS = %i[perform_later perform_now perform].freeze
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# One job-call observation. Carries no path/location — the
|
|
27
|
+
# caller (the `node_rule` block) positions it via
|
|
28
|
+
# `Plugin::Base#diagnostic`.
|
|
29
|
+
Violation = Struct.new(:rule, :severity, :message, keyword_init: true)
|
|
27
30
|
|
|
28
31
|
module_function
|
|
29
32
|
|
|
30
|
-
#
|
|
31
|
-
#
|
|
33
|
+
# The job-call violations for a single call node (0..2), or `[]`
|
|
34
|
+
# when the node is not a `<Job>.perform_*` entry call on a known
|
|
35
|
+
# job. ADR-37: the engine owns the walk.
|
|
36
|
+
#
|
|
37
|
+
# @param call_node [Prism::Node]
|
|
32
38
|
# @param job_index [JobIndex]
|
|
33
|
-
# @return [Array<
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
walk(root) do |call_node|
|
|
37
|
-
class_name = constant_receiver_name(call_node.receiver)
|
|
38
|
-
next if class_name.nil?
|
|
39
|
+
# @return [Array<Violation>]
|
|
40
|
+
def violations_for(call_node:, job_index:)
|
|
41
|
+
return [] unless call_node.is_a?(Prism::CallNode) && entry_call?(call_node)
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
class_name = constant_receiver_name(call_node.receiver)
|
|
44
|
+
return [] if class_name.nil?
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
diagnostics << arity if arity
|
|
46
|
-
end
|
|
47
|
-
diagnostics
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def walk(node, &)
|
|
51
|
-
return unless node.is_a?(Prism::Node)
|
|
46
|
+
entry = job_index.find(class_name) || job_index.find("::#{class_name}")
|
|
47
|
+
return [] if entry.nil?
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
violations = [info_violation(call_node, entry)]
|
|
50
|
+
arity = arity_violation(call_node, entry)
|
|
51
|
+
violations << arity if arity
|
|
52
|
+
violations
|
|
55
53
|
end
|
|
56
54
|
|
|
57
55
|
def entry_call?(node)
|
|
@@ -59,27 +57,19 @@ module Rigor
|
|
|
59
57
|
(node.receiver.is_a?(Prism::ConstantReadNode) || node.receiver.is_a?(Prism::ConstantPathNode))
|
|
60
58
|
end
|
|
61
59
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
Diagnostic.new(
|
|
65
|
-
path: path,
|
|
66
|
-
line: location.start_line,
|
|
67
|
-
column: location.start_column + 1,
|
|
60
|
+
def info_violation(call_node, entry)
|
|
61
|
+
Violation.new(
|
|
68
62
|
severity: :info,
|
|
69
63
|
rule: "job-call",
|
|
70
64
|
message: "`#{entry.class_name}.#{call_node.name}` matches `#perform` (arity #{entry.arity_label})"
|
|
71
65
|
)
|
|
72
66
|
end
|
|
73
67
|
|
|
74
|
-
def
|
|
68
|
+
def arity_violation(call_node, entry)
|
|
75
69
|
actual = (call_node.arguments&.arguments || []).size
|
|
76
70
|
return nil if entry.accepts?(actual)
|
|
77
71
|
|
|
78
|
-
|
|
79
|
-
Diagnostic.new(
|
|
80
|
-
path: path,
|
|
81
|
-
line: location.start_line,
|
|
82
|
-
column: location.start_column + 1,
|
|
72
|
+
Violation.new(
|
|
83
73
|
severity: :error,
|
|
84
74
|
rule: "wrong-arity",
|
|
85
75
|
message: "`#{entry.class_name}.#{call_node.name}` expects #{entry.arity_label} argument(s), got #{actual}"
|
|
@@ -43,14 +43,11 @@ module Rigor
|
|
|
43
43
|
version: "0.1.0",
|
|
44
44
|
description: "Validates ActiveJob `Job.perform_later` argument arity.",
|
|
45
45
|
config_schema: {
|
|
46
|
-
"job_search_paths" => :array,
|
|
47
|
-
"job_base_classes" => :array
|
|
46
|
+
"job_search_paths" => { kind: :array, default: ["app/jobs"] },
|
|
47
|
+
"job_base_classes" => { kind: :array, default: %w[ApplicationJob ActiveJob::Base] }
|
|
48
48
|
}
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
-
DEFAULT_JOB_SEARCH_PATHS = ["app/jobs"].freeze
|
|
52
|
-
DEFAULT_JOB_BASE_CLASSES = %w[ApplicationJob ActiveJob::Base].freeze
|
|
53
|
-
|
|
54
51
|
# Cached: discovered job index. The producer reads every
|
|
55
52
|
# file under `job_search_paths` via the trusted
|
|
56
53
|
# `IoBoundary`; the descriptor's auto-collected
|
|
@@ -65,18 +62,30 @@ module Rigor
|
|
|
65
62
|
end
|
|
66
63
|
|
|
67
64
|
def init(_services)
|
|
68
|
-
@job_search_paths = Array(config.fetch("job_search_paths"
|
|
69
|
-
@job_base_classes = Array(config.fetch("job_base_classes"
|
|
65
|
+
@job_search_paths = Array(config.fetch("job_search_paths")).map(&:to_s)
|
|
66
|
+
@job_base_classes = Array(config.fetch("job_base_classes")).map(&:to_s)
|
|
70
67
|
@job_index = nil
|
|
71
68
|
@load_error = nil
|
|
72
69
|
end
|
|
73
70
|
|
|
71
|
+
# File-level only: the load-error emission. Per-call arity
|
|
72
|
+
# validation runs over the engine-owned walk via the node_rule
|
|
73
|
+
# below (ADR-37). The job index is lazily loaded + memoised by
|
|
74
|
+
# job_index_or_nil, shared by both surfaces.
|
|
74
75
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
75
76
|
index = job_index_or_nil
|
|
76
77
|
return [load_error_diagnostic(path)] if index.nil? && @load_error
|
|
77
|
-
return [] if index.nil? || index.empty?
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
[]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
node_rule Prism::CallNode do |node, _scope, path|
|
|
83
|
+
index = job_index_or_nil
|
|
84
|
+
next [] if index.nil? || index.empty?
|
|
85
|
+
|
|
86
|
+
Analyzer.violations_for(call_node: node, job_index: index).map do |violation|
|
|
87
|
+
diagnostic(node, path: path, message: violation.message, severity: violation.severity, rule: violation.rule)
|
|
88
|
+
end
|
|
80
89
|
end
|
|
81
90
|
|
|
82
91
|
private
|
|
@@ -103,13 +112,6 @@ module Rigor
|
|
|
103
112
|
rule: "load-error"
|
|
104
113
|
)
|
|
105
114
|
end
|
|
106
|
-
|
|
107
|
-
def build_diagnostic(diag)
|
|
108
|
-
Rigor::Analysis::Diagnostic.new(
|
|
109
|
-
path: diag.path, line: diag.line, column: diag.column,
|
|
110
|
-
message: diag.message, severity: diag.severity, rule: diag.rule
|
|
111
|
-
)
|
|
112
|
-
end
|
|
113
115
|
end
|
|
114
116
|
|
|
115
117
|
Rigor::Plugin.register(Activejob)
|
|
@@ -36,8 +36,6 @@ module Rigor
|
|
|
36
36
|
create_or_find_by create_or_find_by!
|
|
37
37
|
].freeze
|
|
38
38
|
|
|
39
|
-
DID_YOU_MEAN_DISTANCE = 3
|
|
40
|
-
|
|
41
39
|
attr_reader :diagnostics
|
|
42
40
|
|
|
43
41
|
def initialize(path:, model_index:)
|
|
@@ -107,7 +105,7 @@ module Rigor
|
|
|
107
105
|
else
|
|
108
106
|
unknown.each do |pair|
|
|
109
107
|
key = pair[:key]
|
|
110
|
-
suggestion =
|
|
108
|
+
suggestion = Rigor::Plugin::Base.suggest(key, entry.column_names)
|
|
111
109
|
hint = suggestion ? " (did you mean `:#{suggestion}`?)" : ""
|
|
112
110
|
push_error(node, "unknown-column",
|
|
113
111
|
"`#{entry.class_name}.#{node.name}(#{key}: ...)` references " \
|
|
@@ -157,7 +155,7 @@ module Rigor
|
|
|
157
155
|
values = entry.enum_values(pair[:key])
|
|
158
156
|
return if values.include?(value)
|
|
159
157
|
|
|
160
|
-
suggestion =
|
|
158
|
+
suggestion = Rigor::Plugin::Base.suggest(value, values)
|
|
161
159
|
hint = suggestion ? " (did you mean `:#{suggestion}`?)" : ""
|
|
162
160
|
push_error(node, "unknown-enum-value",
|
|
163
161
|
"`#{entry.class_name}.#{node.name}(#{pair[:key]}: :#{value})` references " \
|
|
@@ -224,40 +222,6 @@ module Rigor
|
|
|
224
222
|
pairs
|
|
225
223
|
end
|
|
226
224
|
|
|
227
|
-
def closest_column(name, candidates)
|
|
228
|
-
best = nil
|
|
229
|
-
best_distance = DID_YOU_MEAN_DISTANCE + 1
|
|
230
|
-
candidates.each do |candidate|
|
|
231
|
-
distance = levenshtein(name, candidate)
|
|
232
|
-
if distance < best_distance
|
|
233
|
-
best = candidate
|
|
234
|
-
best_distance = distance
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
best
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def levenshtein(a, b) # rubocop:disable Naming/MethodParameterName
|
|
241
|
-
return b.length if a.empty?
|
|
242
|
-
return a.length if b.empty?
|
|
243
|
-
|
|
244
|
-
rows = Array.new(a.length + 1) { |_i| Array.new(b.length + 1, 0) }
|
|
245
|
-
(0..a.length).each { |i| rows[i][0] = i }
|
|
246
|
-
(0..b.length).each { |j| rows[0][j] = j }
|
|
247
|
-
|
|
248
|
-
(1..a.length).each do |i|
|
|
249
|
-
(1..b.length).each do |j|
|
|
250
|
-
cost = a[i - 1] == b[j - 1] ? 0 : 1
|
|
251
|
-
rows[i][j] = [
|
|
252
|
-
rows[i - 1][j] + 1,
|
|
253
|
-
rows[i][j - 1] + 1,
|
|
254
|
-
rows[i - 1][j - 1] + cost
|
|
255
|
-
].min
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
|
-
rows[a.length][b.length]
|
|
259
|
-
end
|
|
260
|
-
|
|
261
225
|
def push_info(node, rule, message)
|
|
262
226
|
push_diagnostic(node, severity: :info, rule: rule, message: message)
|
|
263
227
|
end
|
|
@@ -267,14 +231,8 @@ module Rigor
|
|
|
267
231
|
end
|
|
268
232
|
|
|
269
233
|
def push_diagnostic(node, severity:, rule:, message:)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
path: @path,
|
|
273
|
-
line: location.start_line,
|
|
274
|
-
column: location.start_column + 1,
|
|
275
|
-
message: message,
|
|
276
|
-
severity: severity,
|
|
277
|
-
rule: rule
|
|
234
|
+
@diagnostics << Rigor::Analysis::Diagnostic.from_node(
|
|
235
|
+
node, path: @path, message: message, severity: severity, rule: rule
|
|
278
236
|
)
|
|
279
237
|
end
|
|
280
238
|
end
|
|
@@ -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
|
module Rigor
|
|
6
7
|
module Plugin
|
|
@@ -312,10 +313,9 @@ module Rigor
|
|
|
312
313
|
args = node.arguments&.arguments
|
|
313
314
|
return nil if args.nil? || args.empty?
|
|
314
315
|
|
|
315
|
-
|
|
316
|
-
return nil
|
|
316
|
+
name = Rigor::Source::Literals.symbol_name(args.first)
|
|
317
|
+
return nil if name.nil?
|
|
317
318
|
|
|
318
|
-
name = name_node.unescaped
|
|
319
319
|
polymorphic = POLYMORPHIC_BY_DEFAULT.include?(node.name) ||
|
|
320
320
|
association_option(args, "polymorphic") == true
|
|
321
321
|
|
|
@@ -326,7 +326,7 @@ module Rigor
|
|
|
326
326
|
if polymorphic
|
|
327
327
|
target = nil
|
|
328
328
|
else
|
|
329
|
-
target = explicit_class_name(args) || Inflector.classify(name)
|
|
329
|
+
target = explicit_class_name(args) || Rigor::Plugin::Inflector.classify(name)
|
|
330
330
|
return nil if target.nil? || target.empty?
|
|
331
331
|
end
|
|
332
332
|
|
|
@@ -153,7 +153,7 @@ module Rigor
|
|
|
153
153
|
override = row[:table_name_override]
|
|
154
154
|
return override if override
|
|
155
155
|
end
|
|
156
|
-
Inflector.tableize(strip_leading_namespace(chain.first.fetch(:class_name)))
|
|
156
|
+
Rigor::Plugin::Inflector.tableize(strip_leading_namespace(chain.first.fetch(:class_name)))
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
# Dedups association-style rows by `:name`, keeping the
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "rigor/plugin"
|
|
4
4
|
|
|
5
|
-
require_relative "activerecord/inflector"
|
|
6
5
|
require_relative "activerecord/schema_table"
|
|
7
6
|
require_relative "activerecord/schema_parser"
|
|
8
7
|
require_relative "activerecord/model_index"
|
|
@@ -69,9 +68,9 @@ module Rigor
|
|
|
69
68
|
version: "0.5.0",
|
|
70
69
|
description: "Types ActiveRecord finders against the project's db/schema.rb and AR models.",
|
|
71
70
|
config_schema: {
|
|
72
|
-
"schema_file" => :string,
|
|
73
|
-
"model_search_paths" => :array,
|
|
74
|
-
"model_base_classes" => :array
|
|
71
|
+
"schema_file" => { kind: :string, default: "db/schema.rb" },
|
|
72
|
+
"model_search_paths" => { kind: :array, default: ["app/models"] },
|
|
73
|
+
"model_base_classes" => { kind: :array, default: %w[ApplicationRecord ActiveRecord::Base] }
|
|
75
74
|
},
|
|
76
75
|
produces: [:model_index],
|
|
77
76
|
# ADR-25 — the bundled `ActiveRecord::Relation` RBS, the
|
|
@@ -87,10 +86,6 @@ module Rigor
|
|
|
87
86
|
open_receivers: ["ActiveRecord::Relation"]
|
|
88
87
|
)
|
|
89
88
|
|
|
90
|
-
DEFAULT_SCHEMA_FILE = "db/schema.rb"
|
|
91
|
-
DEFAULT_MODEL_SEARCH_PATHS = ["app/models"].freeze
|
|
92
|
-
DEFAULT_MODEL_BASE_CLASSES = %w[ApplicationRecord ActiveRecord::Base].freeze
|
|
93
|
-
|
|
94
89
|
# The class the bundled `sig/active_record/relation.rbs`
|
|
95
90
|
# describes; `flow_contribution_for` contributes
|
|
96
91
|
# `ActiveRecord::Relation[Model]` for relation-returning
|
|
@@ -117,10 +112,11 @@ module Rigor
|
|
|
117
112
|
end
|
|
118
113
|
|
|
119
114
|
def init(_services)
|
|
120
|
-
@schema_file = config.fetch("schema_file"
|
|
121
|
-
@model_search_paths = Array(config.fetch("model_search_paths"
|
|
122
|
-
@model_base_classes = Array(config.fetch("model_base_classes"
|
|
115
|
+
@schema_file = config.fetch("schema_file")
|
|
116
|
+
@model_search_paths = Array(config.fetch("model_search_paths")).map(&:to_s)
|
|
117
|
+
@model_base_classes = Array(config.fetch("model_base_classes")).map(&:to_s)
|
|
123
118
|
@schema_table = nil
|
|
119
|
+
@schema_load_attempted = false
|
|
124
120
|
@model_index = nil
|
|
125
121
|
@load_errors = []
|
|
126
122
|
end
|
|
@@ -555,7 +551,16 @@ module Rigor
|
|
|
555
551
|
|
|
556
552
|
def schema_table_or_nil
|
|
557
553
|
return @schema_table if @schema_table
|
|
558
|
-
|
|
554
|
+
# Memoize the *failure*, not just the success: `model_index`
|
|
555
|
+
# (and thus this method) is invoked per AR call site, so a
|
|
556
|
+
# missing / unreadable schema file would otherwise re-attempt
|
|
557
|
+
# the read and append a fresh interpolated error string to
|
|
558
|
+
# `@load_errors` on every call. On a large Rails app that grew
|
|
559
|
+
# `@load_errors` to millions of retained strings (measured: 4.2 M
|
|
560
|
+
# strings / ~1.5 GB on Redmine). One attempt is enough.
|
|
561
|
+
return nil if @schema_load_attempted
|
|
562
|
+
|
|
563
|
+
@schema_load_attempted = true
|
|
559
564
|
# Same pattern: read schema file via boundary, then call
|
|
560
565
|
# cache_for so the descriptor includes the file digest.
|
|
561
566
|
io_boundary.read_file(@schema_file)
|
|
@@ -62,14 +62,8 @@ module Rigor
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def push_info(node, rule, message)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
path: @path,
|
|
68
|
-
line: location.start_line,
|
|
69
|
-
column: location.start_column + 1,
|
|
70
|
-
message: message,
|
|
71
|
-
severity: :info,
|
|
72
|
-
rule: rule
|
|
65
|
+
@diagnostics << Rigor::Analysis::Diagnostic.from_node(
|
|
66
|
+
node, path: @path, message: message, severity: :info, rule: rule
|
|
73
67
|
)
|
|
74
68
|
end
|
|
75
69
|
end
|
|
@@ -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
|
module Rigor
|
|
6
7
|
module Plugin
|
|
@@ -126,13 +127,7 @@ module Rigor
|
|
|
126
127
|
end
|
|
127
128
|
|
|
128
129
|
def symbol_literal_arg(node)
|
|
129
|
-
|
|
130
|
-
return nil if args.nil? || args.empty?
|
|
131
|
-
|
|
132
|
-
first = args.first
|
|
133
|
-
return nil unless first.is_a?(Prism::SymbolNode)
|
|
134
|
-
|
|
135
|
-
first.unescaped
|
|
130
|
+
Rigor::Source::Literals.symbol_name(node.arguments&.arguments&.first)
|
|
136
131
|
end
|
|
137
132
|
|
|
138
133
|
def constant_path_name(node)
|
|
@@ -48,13 +48,11 @@ module Rigor
|
|
|
48
48
|
version: "0.1.0",
|
|
49
49
|
description: "Types ActiveStorage attachment macros (has_one_attached / has_many_attached) on AR models.",
|
|
50
50
|
config_schema: {
|
|
51
|
-
"model_search_paths" => :array
|
|
51
|
+
"model_search_paths" => { kind: :array, default: ["app/models"] }
|
|
52
52
|
},
|
|
53
53
|
consumes: [{ plugin_id: "activerecord", name: :model_index, optional: true }]
|
|
54
54
|
)
|
|
55
55
|
|
|
56
|
-
DEFAULT_MODEL_SEARCH_PATHS = ["app/models"].freeze
|
|
57
|
-
|
|
58
56
|
# Cached: attachment index. Walks every `.rb` file under
|
|
59
57
|
# `model_search_paths` for `has_*_attached` macros.
|
|
60
58
|
producer :attachment_index do |_params|
|
|
@@ -66,9 +64,7 @@ module Rigor
|
|
|
66
64
|
end
|
|
67
65
|
|
|
68
66
|
def init(_services)
|
|
69
|
-
@model_search_paths = Array(
|
|
70
|
-
config.fetch("model_search_paths", DEFAULT_MODEL_SEARCH_PATHS)
|
|
71
|
-
).map(&:to_s)
|
|
67
|
+
@model_search_paths = Array(config.fetch("model_search_paths")).map(&:to_s)
|
|
72
68
|
@attachment_index = nil
|
|
73
69
|
@load_errors = []
|
|
74
70
|
end
|
|
@@ -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
|
module Rigor
|
|
6
7
|
module Plugin
|
|
@@ -187,10 +188,10 @@ module Rigor
|
|
|
187
188
|
current = node
|
|
188
189
|
while current.is_a?(Prism::CallNode)
|
|
189
190
|
if %i[required optional].include?(current.name)
|
|
190
|
-
|
|
191
|
-
return [nil, nil]
|
|
191
|
+
key = Rigor::Source::Literals.symbol(current.arguments&.arguments&.first)
|
|
192
|
+
return [nil, nil] if key.nil?
|
|
192
193
|
|
|
193
|
-
return [
|
|
194
|
+
return [key, current.name]
|
|
194
195
|
end
|
|
195
196
|
current = current.receiver
|
|
196
197
|
end
|
|
@@ -47,7 +47,11 @@ module Rigor
|
|
|
47
47
|
version: "0.1.0",
|
|
48
48
|
description: "Recognises `class T < Dry::Validation::Contract` subclasses and " \
|
|
49
49
|
"publishes the contract FQN set.",
|
|
50
|
-
produces: [:dry_validation_contracts]
|
|
50
|
+
produces: [:dry_validation_contracts],
|
|
51
|
+
# Auto-contribute the bundled RBS overlay (Contract#call -> Result,
|
|
52
|
+
# Result#success?/#to_h/...) per ADR-25, so no project-side
|
|
53
|
+
# signature_paths wiring is needed.
|
|
54
|
+
signature_paths: ["sig"]
|
|
51
55
|
)
|
|
52
56
|
|
|
53
57
|
def prepare(services)
|