rigortype 0.1.11 → 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.
- checksums.yaml +4 -4
- data/lib/rigor/analysis/erb_template_detector.rb +38 -0
- data/lib/rigor/analysis/runner.rb +6 -1
- data/lib/rigor/analysis/worker_session.rb +6 -1
- data/lib/rigor/cli/plugins_command.rb +308 -0
- data/lib/rigor/cli/plugins_renderer.rb +173 -0
- data/lib/rigor/cli.rb +28 -0
- data/lib/rigor/inference/block_parameter_binder.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +69 -30
- data/lib/rigor/inference/indexed_narrowing.rb +187 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
- data/lib/rigor/inference/method_dispatcher.rb +23 -0
- data/lib/rigor/inference/mutation_widening.rb +285 -0
- data/lib/rigor/inference/narrowing.rb +72 -4
- data/lib/rigor/inference/scope_indexer.rb +409 -12
- data/lib/rigor/inference/statement_evaluator.rb +256 -4
- data/lib/rigor/scope.rb +181 -4
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +22 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +94 -6
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +11 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +7 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +135 -11
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +94 -43
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +138 -35
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +17 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +10 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +13 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +6 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +83 -7
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +4 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +16 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +81 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +11 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +194 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +64 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1107 -59
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +81 -4
- data/sig/rigor/scope.rbs +22 -0
- metadata +9 -1
|
@@ -15,34 +15,126 @@ module Rigor
|
|
|
15
15
|
|
|
16
16
|
# Built-in Rails helpers we don't want to flag as
|
|
17
17
|
# unknown. The plugin's HelperTable describes
|
|
18
|
-
# user-declared routes; Rails
|
|
19
|
-
#
|
|
20
|
-
#
|
|
18
|
+
# user-declared routes; Rails (and a small set of
|
|
19
|
+
# widely-used asset gems) ship built-in helpers
|
|
20
|
+
# (`url_for`, `polymorphic_path`, vite_ruby's
|
|
21
|
+
# `vite_asset_path`, …) the plugin deliberately ignores.
|
|
21
22
|
BUILTIN_PASSTHROUGH = %w[
|
|
22
23
|
url_for_path url_for_url
|
|
23
24
|
polymorphic_path polymorphic_url
|
|
25
|
+
vite_asset_path vite_asset_url
|
|
26
|
+
expose_path expose_url
|
|
27
|
+
asset_path asset_url
|
|
28
|
+
image_path image_url
|
|
29
|
+
javascript_path javascript_url
|
|
30
|
+
stylesheet_path stylesheet_url
|
|
31
|
+
font_path font_url
|
|
32
|
+
video_path video_url
|
|
33
|
+
audio_path audio_url
|
|
24
34
|
].freeze
|
|
25
35
|
|
|
26
36
|
Diagnostic = Struct.new(:path, :line, :column, :severity, :rule, :message, keyword_init: true)
|
|
27
37
|
|
|
28
38
|
module_function
|
|
29
39
|
|
|
40
|
+
# RSpec / Minitest DSL methods that DECLARE a memoized
|
|
41
|
+
# local — `let(:foo) { ... }`, `subject(:foo) { ... }`,
|
|
42
|
+
# plus the bang and let_it_be variants. The first
|
|
43
|
+
# positional Symbol argument is the local name. A bare
|
|
44
|
+
# `subject { ... }` (no arg) defines `:subject` itself.
|
|
45
|
+
SHADOWING_DSL = %i[
|
|
46
|
+
let let! let_it_be let_it_be!
|
|
47
|
+
subject subject!
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
# Paths under which `unknown-helper` is suppressed. Route
|
|
51
|
+
# helpers are not customarily resolved through these
|
|
52
|
+
# directories, so a bare `shared_inbox_url` call inside
|
|
53
|
+
# `app/models/account.rb` (the call-site is the
|
|
54
|
+
# `accounts.shared_inbox_url` AR column accessor — same
|
|
55
|
+
# name as a hypothetical route helper) or a `default_url`
|
|
56
|
+
# inside `lib/paperclip/url_generator_extensions.rb` (the
|
|
57
|
+
# call-site is `Paperclip::UrlGenerator#default_url`, a
|
|
58
|
+
# gem-side instance method) would be a false positive.
|
|
59
|
+
# The core engine's `call.undefined-method` still catches
|
|
60
|
+
# a genuinely unreachable receiver. Cheap path-prefix
|
|
61
|
+
# check; no AR-column / gem-include analysis required.
|
|
62
|
+
SKIP_UNKNOWN_HELPER_PATHS = [
|
|
63
|
+
%r{(?:\A|/)app/models/},
|
|
64
|
+
%r{(?:\A|/)app/services/},
|
|
65
|
+
%r{(?:\A|/)app/workers/},
|
|
66
|
+
%r{(?:\A|/)app/finders/},
|
|
67
|
+
%r{(?:\A|/)app/policies/},
|
|
68
|
+
%r{(?:\A|/)app/validators/},
|
|
69
|
+
%r{(?:\A|/)app/uploaders/},
|
|
70
|
+
%r{(?:\A|/)lib/},
|
|
71
|
+
%r{(?:\A|/)db/},
|
|
72
|
+
%r{(?:\A|/)config/}
|
|
73
|
+
].freeze
|
|
74
|
+
|
|
30
75
|
# @param path [String] file being analysed
|
|
31
76
|
# @param root [Prism::Node]
|
|
32
77
|
# @param helper_table [HelperTable]
|
|
33
78
|
# @return [Array<Diagnostic>]
|
|
34
79
|
def diagnose(path:, root:, helper_table:)
|
|
80
|
+
suppress_unknown = SKIP_UNKNOWN_HELPER_PATHS.any? { |re| path.match?(re) }
|
|
81
|
+
# Same SKIP set silences `wrong-arity` too. A call
|
|
82
|
+
# like `group_path` inside `app/services/groups/nested_
|
|
83
|
+
# create_service.rb` is almost certainly the service's
|
|
84
|
+
# own instance method (`attr_accessor :group_path`) —
|
|
85
|
+
# both the unknown-helper check and the arity check
|
|
86
|
+
# against a registered route helper would FP.
|
|
35
87
|
diagnostics = []
|
|
88
|
+
# Pre-walk the file to collect every name that
|
|
89
|
+
# shadows a route helper at call time: `let(:foo)`,
|
|
90
|
+
# `subject(:foo)`, `def foo`, and explicit local
|
|
91
|
+
# assignments (`foo_url = "..."`). At the call site
|
|
92
|
+
# `foo` then resolves to the shadowing local, not to
|
|
93
|
+
# the registered route helper — firing `unknown-helper`
|
|
94
|
+
# / `wrong-arity` against the helper would be a false
|
|
95
|
+
# positive against canonical RSpec idioms (Mastodon
|
|
96
|
+
# has 200+ such patterns in `spec/`).
|
|
97
|
+
shadowing = collect_shadowing_names(root)
|
|
98
|
+
|
|
36
99
|
walk(root) do |call_node|
|
|
37
100
|
name = call_node.name.to_s
|
|
38
101
|
next unless name.end_with?("_path") || name.end_with?("_url")
|
|
39
102
|
next if BUILTIN_PASSTHROUGH.include?(name)
|
|
103
|
+
next if shadowing.include?(name)
|
|
40
104
|
|
|
41
105
|
entry = helper_table.find(name)
|
|
42
106
|
if entry
|
|
107
|
+
# When the file is in a `SKIP_UNKNOWN_HELPER_PATHS`
|
|
108
|
+
# directory (models / services / workers / lib /
|
|
109
|
+
# …), don't emit the info or arity diagnostic
|
|
110
|
+
# against a registered helper either — the call
|
|
111
|
+
# is more likely the file's own instance method
|
|
112
|
+
# (e.g. `group_path` as an `attr_accessor`) that
|
|
113
|
+
# happens to share a name with a registered
|
|
114
|
+
# route helper. The unknown-helper suppression
|
|
115
|
+
# already silences the inverse case.
|
|
116
|
+
next if suppress_unknown
|
|
117
|
+
|
|
43
118
|
diagnostics << info_diagnostic(path, call_node, entry)
|
|
44
119
|
arity_diagnostic = arity_check(path, call_node, entry, helper_table)
|
|
45
120
|
diagnostics << arity_diagnostic if arity_diagnostic
|
|
121
|
+
elsif helper_table.recognised?(name)
|
|
122
|
+
# Custom helper (discovered via
|
|
123
|
+
# `app/helpers/**/*.rb`) or a dynamic-provider
|
|
124
|
+
# Devise OmniAuth helper. We do NOT have an
|
|
125
|
+
# arity / path to validate — the helper is just
|
|
126
|
+
# known-to-exist. Skip the arity / info
|
|
127
|
+
# diagnostic; the absence of an `unknown-helper`
|
|
128
|
+
# error is the user-visible outcome.
|
|
129
|
+
next
|
|
130
|
+
elsif suppress_unknown
|
|
131
|
+
# File is in a directory where route helpers are
|
|
132
|
+
# not customarily resolved (models / lib / db /
|
|
133
|
+
# config). Skip `unknown-helper` to avoid false
|
|
134
|
+
# positives on AR column accessors / gem-side
|
|
135
|
+
# instance methods that happen to end in `_url` /
|
|
136
|
+
# `_path`.
|
|
137
|
+
next
|
|
46
138
|
else
|
|
47
139
|
diagnostics << unknown_helper_diagnostic(path, call_node, name, helper_table)
|
|
48
140
|
end
|
|
@@ -50,6 +142,52 @@ module Rigor
|
|
|
50
142
|
diagnostics
|
|
51
143
|
end
|
|
52
144
|
|
|
145
|
+
# Walks the AST once and returns the Set of names that
|
|
146
|
+
# shadow a route helper for this file. Includes:
|
|
147
|
+
#
|
|
148
|
+
# - `def name` declarations (any level — the
|
|
149
|
+
# per-method-scope visibility model Ruby uses means a
|
|
150
|
+
# local `def` shadows the helper at every call site
|
|
151
|
+
# reachable from where it is defined; we approximate
|
|
152
|
+
# "reachable" with "anywhere in the same file").
|
|
153
|
+
# - RSpec `let` / `let!` / `let_it_be` / `let_it_be!` /
|
|
154
|
+
# `subject` / `subject!` declarations.
|
|
155
|
+
# - Local assignments at any scope
|
|
156
|
+
# (`foo_url = "..."`).
|
|
157
|
+
def collect_shadowing_names(root)
|
|
158
|
+
names = Set.new
|
|
159
|
+
walk_for_shadowing(root, names)
|
|
160
|
+
names
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def walk_for_shadowing(node, names)
|
|
164
|
+
return unless node.is_a?(Prism::Node)
|
|
165
|
+
|
|
166
|
+
case node
|
|
167
|
+
when Prism::DefNode
|
|
168
|
+
names << node.name.to_s if node.receiver.nil?
|
|
169
|
+
when Prism::LocalVariableWriteNode
|
|
170
|
+
names << node.name.to_s
|
|
171
|
+
when Prism::CallNode
|
|
172
|
+
record_let_like_name(node, names)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
node.compact_child_nodes.each { |child| walk_for_shadowing(child, names) }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def record_let_like_name(call_node, names)
|
|
179
|
+
return unless call_node.receiver.nil?
|
|
180
|
+
return unless SHADOWING_DSL.include?(call_node.name)
|
|
181
|
+
|
|
182
|
+
arg = call_node.arguments&.arguments&.first
|
|
183
|
+
if arg.is_a?(Prism::SymbolNode)
|
|
184
|
+
names << arg.unescaped
|
|
185
|
+
elsif call_node.name == :subject && call_node.arguments.nil?
|
|
186
|
+
# Bare `subject { ... }` defines `:subject` itself.
|
|
187
|
+
names << "subject"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
53
191
|
def walk(node, &)
|
|
54
192
|
return unless node.is_a?(Prism::Node)
|
|
55
193
|
|
|
@@ -66,6 +204,25 @@ module Rigor
|
|
|
66
204
|
node.receiver.nil? && (node.name.to_s.end_with?("_path") || node.name.to_s.end_with?("_url"))
|
|
67
205
|
end
|
|
68
206
|
|
|
207
|
+
# Paths where the implicit-params-fill pattern is
|
|
208
|
+
# idiomatic — controllers / mailers / views routinely
|
|
209
|
+
# call `*_path` / `*_url` helpers with fewer args than
|
|
210
|
+
# the route's static placeholder count because Rails
|
|
211
|
+
# fills `:foo_id` segments from `request.params` at
|
|
212
|
+
# runtime (controllers / views) or from the call's
|
|
213
|
+
# polymorphic-friendly receiver (mailers).
|
|
214
|
+
IMPLICIT_FILL_PATHS = [
|
|
215
|
+
%r{(?:\A|/)app/controllers/},
|
|
216
|
+
%r{(?:\A|/)app/mailers/},
|
|
217
|
+
%r{(?:\A|/)app/views/},
|
|
218
|
+
%r{(?:\A|/)app/components/},
|
|
219
|
+
%r{(?:\A|/)app/helpers/}
|
|
220
|
+
].freeze
|
|
221
|
+
|
|
222
|
+
def implicit_fill_path?(path)
|
|
223
|
+
IMPLICIT_FILL_PATHS.any? { |re| path.match?(re) }
|
|
224
|
+
end
|
|
225
|
+
|
|
69
226
|
def info_diagnostic(path, call_node, entry)
|
|
70
227
|
location = call_node.location
|
|
71
228
|
method_label = entry.http_method ? entry.http_method.to_s.upcase : "*"
|
|
@@ -80,7 +237,8 @@ module Rigor
|
|
|
80
237
|
end
|
|
81
238
|
|
|
82
239
|
def arity_check(path, call_node, entry, helper_table)
|
|
83
|
-
|
|
240
|
+
args = call_node.arguments&.arguments || []
|
|
241
|
+
actual = args.size
|
|
84
242
|
# Uncountable nouns (`news` / `series` / `media`) cause
|
|
85
243
|
# Rails to register two entries under the same helper
|
|
86
244
|
# name — `news_path` accepts both arity 0 (index) and
|
|
@@ -88,7 +246,38 @@ module Rigor
|
|
|
88
246
|
# accepts_arity? checks the full set.
|
|
89
247
|
return nil if helper_table.accepts_arity?(entry.name, actual)
|
|
90
248
|
|
|
91
|
-
|
|
249
|
+
# Rails accepts a kwargs-only call shape that supplies
|
|
250
|
+
# route segments by name:
|
|
251
|
+
# short_account_status_url(account_username: u, id: i)
|
|
252
|
+
# for the route `/@:account_username/:id` (arity 2).
|
|
253
|
+
# Our positional-arg count is 1 (the KeywordHashNode),
|
|
254
|
+
# so the strict check rejects — but Rails would
|
|
255
|
+
# resolve every segment from the hash. When the call
|
|
256
|
+
# has a trailing KeywordHashNode AND the positional
|
|
257
|
+
# count (excluding it) is `<= expected_arity`, accept
|
|
258
|
+
# — the kwargs may carry the missing segments.
|
|
259
|
+
if args.last.is_a?(Prism::KeywordHashNode)
|
|
260
|
+
positional = actual - 1
|
|
261
|
+
return nil if helper_table.acceptable_arities(entry.name).any? { |exp| positional <= exp }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Underflow tolerance for controllers / mailers /
|
|
265
|
+
# views. A `redirect_to namespace_project_milestones_path`
|
|
266
|
+
# inside `Projects::MilestonesController` legitimately
|
|
267
|
+
# passes 0 args — Rails fills the missing `:namespace_id`
|
|
268
|
+
# / `:project_id` segments from `request.params` at
|
|
269
|
+
# runtime. Mailers reach helpers polymorphically too
|
|
270
|
+
# (`project_commit_url(commit)` resolves project from
|
|
271
|
+
# commit.project). Silence when `actual < min_arity`
|
|
272
|
+
# AND the call site is in a controller / mailer / view
|
|
273
|
+
# directory (where the implicit-params-fill pattern is
|
|
274
|
+
# idiomatic). Overflow (`actual > max_arity`) still
|
|
275
|
+
# fires — that's almost always a typo.
|
|
276
|
+
arities_set = helper_table.acceptable_arities(entry.name)
|
|
277
|
+
min_arity = arities_set.min
|
|
278
|
+
return nil if actual < min_arity && implicit_fill_path?(path)
|
|
279
|
+
|
|
280
|
+
arities = arities_set.sort
|
|
92
281
|
expected = arities.length == 1 ? arities.first.to_s : "#{arities.first}..#{arities.last}"
|
|
93
282
|
location = call_node.location
|
|
94
283
|
Diagnostic.new(
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
class RailsRoutes < Rigor::Plugin::Base
|
|
6
|
+
# Generates the catalogue of route helpers a `devise_for
|
|
7
|
+
# :resources` call adds to a Rails app's routing table.
|
|
8
|
+
# The set is fixed and well-documented (the
|
|
9
|
+
# [Devise wiki](https://github.com/heartcombo/devise/wiki/How-To:-Add-routes-helpers)
|
|
10
|
+
# enumerates every generated helper); the generator
|
|
11
|
+
# mirrors it.
|
|
12
|
+
#
|
|
13
|
+
# Used by `RoutesParser` when it sees a top-level
|
|
14
|
+
# `devise_for :name` (or `devise_for :name, skip:
|
|
15
|
+
# [...]`) call. Each generated helper is materialised as
|
|
16
|
+
# a `HelperTable::Entry` so the analyzer's
|
|
17
|
+
# `unknown-helper` rule recognises them and the arity
|
|
18
|
+
# check accepts the canonical signatures (Devise helpers
|
|
19
|
+
# take zero positional args plus an optional trailing
|
|
20
|
+
# options hash, which `HelperTable#accepts_arity?`
|
|
21
|
+
# already handles via the `arity + 1` rule).
|
|
22
|
+
#
|
|
23
|
+
# Scope:
|
|
24
|
+
#
|
|
25
|
+
# - Recognises the standard six controllers
|
|
26
|
+
# (`:sessions`, `:passwords`, `:confirmations`,
|
|
27
|
+
# `:unlocks`, `:registrations`, `:omniauth_callbacks`).
|
|
28
|
+
# - Honours `skip: [...]` to suppress controllers the
|
|
29
|
+
# project disables.
|
|
30
|
+
# - Honours `path: "..."` to remap the URL path (does
|
|
31
|
+
# NOT remap the helper-name prefix — Devise uses the
|
|
32
|
+
# resource name for the helper, not the path).
|
|
33
|
+
# - Does NOT yet honour `as: "..."` overrides, `class_name:`,
|
|
34
|
+
# `controllers: { ... }` (these are rare; expand on
|
|
35
|
+
# demand).
|
|
36
|
+
# - `omniauth_callbacks` helpers are dynamic — the
|
|
37
|
+
# provider name is supplied at call time
|
|
38
|
+
# (`user_facebook_omniauth_authorize_path`) and Devise
|
|
39
|
+
# declares them from the configured providers, which
|
|
40
|
+
# live in an initializer this static parser does not
|
|
41
|
+
# read. We register the SHAPE-FAMILY suffix matchers
|
|
42
|
+
# `_omniauth_authorize_path` / `_omniauth_callback_path`
|
|
43
|
+
# via a separate hook (see `OMNIAUTH_HELPER_PATTERNS`);
|
|
44
|
+
# these are NOT in the table but consulted by the
|
|
45
|
+
# `Analyzer.allowed_dynamic_pattern?` check.
|
|
46
|
+
module DeviseRoutes
|
|
47
|
+
# The standard Devise controllers and the helper
|
|
48
|
+
# actions each generates. Keys are the controller
|
|
49
|
+
# name; values are arrays of `[helper_prefix,
|
|
50
|
+
# http_method, action]` triples — `helper_prefix` is
|
|
51
|
+
# the part BEFORE the resource-name segment (Rails
|
|
52
|
+
# produces `<prefix>_<resource>_<suffix>_path`; for
|
|
53
|
+
# the bare `<resource>_session_path` shape the prefix
|
|
54
|
+
# is empty).
|
|
55
|
+
CONTROLLER_HELPERS = {
|
|
56
|
+
sessions: [
|
|
57
|
+
["new", "session", :get, :new],
|
|
58
|
+
[nil, "session", :post, :create],
|
|
59
|
+
["destroy", "session", :delete, :destroy]
|
|
60
|
+
],
|
|
61
|
+
passwords: [
|
|
62
|
+
["new", "password", :get, :new],
|
|
63
|
+
["edit", "password", :get, :edit],
|
|
64
|
+
[nil, "password", :get, :show]
|
|
65
|
+
],
|
|
66
|
+
confirmations: [
|
|
67
|
+
["new", "confirmation", :get, :new],
|
|
68
|
+
[nil, "confirmation", :get, :show]
|
|
69
|
+
],
|
|
70
|
+
unlocks: [
|
|
71
|
+
["new", "unlock", :get, :new],
|
|
72
|
+
[nil, "unlock", :get, :show]
|
|
73
|
+
],
|
|
74
|
+
registrations: [
|
|
75
|
+
["cancel", "registration", :get, :show],
|
|
76
|
+
["new", "registration", :get, :new],
|
|
77
|
+
["edit", "registration", :get, :edit],
|
|
78
|
+
[nil, "registration", :get, :show]
|
|
79
|
+
]
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
82
|
+
# Dynamic-provider helper patterns. `_omniauth_authorize_path`
|
|
83
|
+
# and `_omniauth_callback_path` are generated per
|
|
84
|
+
# configured provider (Facebook, GitHub, …) by an
|
|
85
|
+
# initializer this parser does not read. We treat any
|
|
86
|
+
# `<resource>_<provider>_omniauth_(authorize|callback)_(path|url)`
|
|
87
|
+
# call as recognised when the resource is one the
|
|
88
|
+
# project declared via `devise_for`.
|
|
89
|
+
OMNIAUTH_SUFFIXES = %w[
|
|
90
|
+
_omniauth_authorize_path _omniauth_authorize_url
|
|
91
|
+
_omniauth_callback_path _omniauth_callback_url
|
|
92
|
+
].freeze
|
|
93
|
+
|
|
94
|
+
module_function
|
|
95
|
+
|
|
96
|
+
# @param resource [String, Symbol] the `devise_for`
|
|
97
|
+
# argument — typically `:users`. Used as both the
|
|
98
|
+
# helper-name segment (`new_user_session_path`) and
|
|
99
|
+
# to derive the resource path.
|
|
100
|
+
# @param skip [Array<Symbol>] controllers the project
|
|
101
|
+
# disables via `devise_for :users, skip: [:registrations]`.
|
|
102
|
+
# @return [Array<HelperTable::Entry>] one entry per
|
|
103
|
+
# generated `_path` helper. The caller is expected to
|
|
104
|
+
# pair `_url` variants the same way `RoutesParser` does
|
|
105
|
+
# for other entries.
|
|
106
|
+
def generate(resource:, skip: [])
|
|
107
|
+
resource_segment = singularize(resource.to_s)
|
|
108
|
+
skip_set = skip.to_set(&:to_sym)
|
|
109
|
+
entries = []
|
|
110
|
+
|
|
111
|
+
CONTROLLER_HELPERS.each do |controller, actions|
|
|
112
|
+
next if skip_set.include?(controller)
|
|
113
|
+
|
|
114
|
+
actions.each do |(prefix, suffix, http_method, action)|
|
|
115
|
+
helper = if prefix
|
|
116
|
+
"#{prefix}_#{resource_segment}_#{suffix}_path"
|
|
117
|
+
else
|
|
118
|
+
"#{resource_segment}_#{suffix}_path"
|
|
119
|
+
end
|
|
120
|
+
path = devise_path_for(resource_segment, controller, action)
|
|
121
|
+
entries << HelperTable::Entry.new(
|
|
122
|
+
name: helper,
|
|
123
|
+
arity: 0,
|
|
124
|
+
path: path,
|
|
125
|
+
http_method: http_method,
|
|
126
|
+
action: action
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Devise also ships *scoped* helpers
|
|
132
|
+
# (`Devise::Controllers::UrlHelpers`) that DROP the
|
|
133
|
+
# resource segment and take the scope as a positional
|
|
134
|
+
# argument: `new_password_path(resource_name)` is the
|
|
135
|
+
# bare form of `new_user_password_path`. Used inside
|
|
136
|
+
# `Auth::PasswordsController < Devise::PasswordsController`
|
|
137
|
+
# (Mastodon's idiom). Arity 1 (the scope), and a
|
|
138
|
+
# trailing options-hash bumps it via the existing
|
|
139
|
+
# `arity + 1` rule.
|
|
140
|
+
entries.concat(scoped_helpers(skip_set))
|
|
141
|
+
|
|
142
|
+
# `omniauth_authorize_path(scope, provider)` and
|
|
143
|
+
# `omniauth_callback_path(scope, provider)` are
|
|
144
|
+
# `Devise::Controllers::UrlHelpers` scoped helpers
|
|
145
|
+
# that delegate to the OmniAuth gem. Arity 2.
|
|
146
|
+
unless skip_set.include?(:omniauth_callbacks)
|
|
147
|
+
%w[omniauth_authorize_path omniauth_callback_path].each do |name|
|
|
148
|
+
entries << HelperTable::Entry.new(
|
|
149
|
+
name: name,
|
|
150
|
+
arity: 2,
|
|
151
|
+
path: "/users/auth/:provider",
|
|
152
|
+
http_method: :get,
|
|
153
|
+
action: :show
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
entries
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Scoped helpers Devise exposes inside its controllers.
|
|
162
|
+
# The arity-1 family — caller supplies the resource
|
|
163
|
+
# scope (`:user` etc.) as the first positional arg.
|
|
164
|
+
# Names mirror the qualified `<scope>_<name>_path`
|
|
165
|
+
# entries above, minus the scope segment.
|
|
166
|
+
SCOPED_HELPERS = {
|
|
167
|
+
sessions: %w[new_session_path session_path destroy_session_path],
|
|
168
|
+
passwords: %w[new_password_path edit_password_path password_path],
|
|
169
|
+
confirmations: %w[new_confirmation_path confirmation_path],
|
|
170
|
+
unlocks: %w[new_unlock_path unlock_path],
|
|
171
|
+
registrations: %w[cancel_registration_path new_registration_path edit_registration_path registration_path]
|
|
172
|
+
}.freeze
|
|
173
|
+
private_constant :SCOPED_HELPERS
|
|
174
|
+
|
|
175
|
+
def scoped_helpers(skip_set)
|
|
176
|
+
SCOPED_HELPERS.flat_map do |controller, names|
|
|
177
|
+
next [] if skip_set.include?(controller)
|
|
178
|
+
|
|
179
|
+
names.map do |name|
|
|
180
|
+
HelperTable::Entry.new(
|
|
181
|
+
name: name,
|
|
182
|
+
arity: 1,
|
|
183
|
+
path: "/<scope>",
|
|
184
|
+
http_method: :get,
|
|
185
|
+
action: :show
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Returns the set of OmniAuth pattern suffixes the
|
|
192
|
+
# analyzer accepts for a given resource. The analyzer
|
|
193
|
+
# consults this set (via `HelperTable#allows_omniauth?`)
|
|
194
|
+
# when a `*_path` / `*_url` call's name does not match
|
|
195
|
+
# any registered entry and its prefix matches a Devise
|
|
196
|
+
# resource.
|
|
197
|
+
def omniauth_suffixes
|
|
198
|
+
OMNIAUTH_SUFFIXES
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The same tiny singularizer used by `RoutesParser`.
|
|
202
|
+
# We duplicate the catalogue here so this module does
|
|
203
|
+
# not need to reach into `Context`'s private state.
|
|
204
|
+
UNCOUNTABLE = %w[
|
|
205
|
+
equipment information rice money species series fish
|
|
206
|
+
sheep jeans police news media settings
|
|
207
|
+
].to_set.freeze
|
|
208
|
+
private_constant :UNCOUNTABLE
|
|
209
|
+
|
|
210
|
+
def singularize(word)
|
|
211
|
+
return word if UNCOUNTABLE.include?(word)
|
|
212
|
+
return "#{word.chomp('ies')}y" if word.end_with?("ies") && word.length > 3
|
|
213
|
+
return word.chomp("es") if word.end_with?("ses") || word.end_with?("xes")
|
|
214
|
+
# `ss` preserved — Rails default inflector behaviour.
|
|
215
|
+
return word if word.end_with?("ss")
|
|
216
|
+
return word.chomp("s") if word.end_with?("s")
|
|
217
|
+
|
|
218
|
+
word
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Approximate path generation. We don't honour the
|
|
222
|
+
# `path:` Devise option here because the helper NAME
|
|
223
|
+
# is what the analyzer cares about; the `path` field is
|
|
224
|
+
# informational and surfaces only in the `:helper` info
|
|
225
|
+
# diagnostic.
|
|
226
|
+
def devise_path_for(resource_segment, controller, action)
|
|
227
|
+
plural = "#{resource_segment}s"
|
|
228
|
+
base = "/#{plural}"
|
|
229
|
+
case controller
|
|
230
|
+
when :sessions then session_path_for(action)
|
|
231
|
+
when :registrations then registration_path_for(action, base)
|
|
232
|
+
else "#{base}/#{controller}#{action_suffix(action)}"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def session_path_for(action)
|
|
237
|
+
case action
|
|
238
|
+
when :new then "/users/sign_in"
|
|
239
|
+
when :create then "/users/sign_in"
|
|
240
|
+
when :destroy then "/users/sign_out"
|
|
241
|
+
else "/users/sign_in"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def registration_path_for(action, base)
|
|
246
|
+
case action
|
|
247
|
+
when :new then "#{base}/sign_up"
|
|
248
|
+
when :edit then "#{base}/edit"
|
|
249
|
+
when :show then base
|
|
250
|
+
else base
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def action_suffix(action)
|
|
255
|
+
case action
|
|
256
|
+
when :new then "/new"
|
|
257
|
+
when :edit then "/edit"
|
|
258
|
+
else ""
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
class RailsRoutes < Rigor::Plugin::Base
|
|
6
|
+
# Generates the catalogue of OAuth route helpers a
|
|
7
|
+
# `use_doorkeeper do ... end` block adds to a Rails app's
|
|
8
|
+
# routing table. The set is the Doorkeeper gem's
|
|
9
|
+
# documented default — see Doorkeeper's
|
|
10
|
+
# [Routing](https://github.com/doorkeeper-gem/doorkeeper/wiki/Customizing-routes)
|
|
11
|
+
# documentation; this module mirrors the canonical
|
|
12
|
+
# helper names.
|
|
13
|
+
#
|
|
14
|
+
# Used by `RoutesParser` when it sees a `use_doorkeeper`
|
|
15
|
+
# call. Each generated helper is materialised as a
|
|
16
|
+
# `HelperTable::Entry` so the analyzer's
|
|
17
|
+
# `unknown-helper` rule recognises them. Arity for the
|
|
18
|
+
# `oauth_application_path(:id)` / `oauth_authorized_application_path(:id)`
|
|
19
|
+
# forms is 1 (the `:id`); all other Doorkeeper helpers
|
|
20
|
+
# are arity 0.
|
|
21
|
+
#
|
|
22
|
+
# Scope:
|
|
23
|
+
#
|
|
24
|
+
# - Recognises the standard helper set: authorization,
|
|
25
|
+
# token, revoke, introspect, token_info, userinfo,
|
|
26
|
+
# applications (resources), authorized_applications.
|
|
27
|
+
# - Honours `skip_controllers <names>` inside the block
|
|
28
|
+
# to omit the named controller's helpers.
|
|
29
|
+
# - Does NOT yet honour `controllers ...:` remappings —
|
|
30
|
+
# those change WHICH class serves the route but not
|
|
31
|
+
# the helper NAMES, so omitting them is sound.
|
|
32
|
+
# - Does NOT yet honour custom `as:` / `at:` on
|
|
33
|
+
# `use_doorkeeper` itself.
|
|
34
|
+
module DoorkeeperRoutes
|
|
35
|
+
# Per-controller helper catalogue. Keys are the
|
|
36
|
+
# `skip_controllers :<key>` names; values are the
|
|
37
|
+
# entries (name + arity + path + http_method).
|
|
38
|
+
# Both `_path` and `_url` variants are paired by
|
|
39
|
+
# `RoutesParser` after registration.
|
|
40
|
+
CONTROLLER_HELPERS = {
|
|
41
|
+
authorizations: [
|
|
42
|
+
["oauth_authorization_path", 0, "/oauth/authorize", :get, :show],
|
|
43
|
+
["oauth_authorization_url", 0, "/oauth/authorize", :get, :show]
|
|
44
|
+
],
|
|
45
|
+
tokens: [
|
|
46
|
+
["oauth_token_path", 0, "/oauth/token", :post, :create],
|
|
47
|
+
["oauth_token_url", 0, "/oauth/token", :post, :create],
|
|
48
|
+
["oauth_revoke_path", 0, "/oauth/revoke", :post, :create],
|
|
49
|
+
["oauth_revoke_url", 0, "/oauth/revoke", :post, :create],
|
|
50
|
+
["oauth_introspect_path", 0, "/oauth/introspect", :post, :create],
|
|
51
|
+
["oauth_introspect_url", 0, "/oauth/introspect", :post, :create]
|
|
52
|
+
],
|
|
53
|
+
token_info: [
|
|
54
|
+
["oauth_token_info_path", 0, "/oauth/token/info", :get, :show],
|
|
55
|
+
["oauth_token_info_url", 0, "/oauth/token/info", :get, :show]
|
|
56
|
+
],
|
|
57
|
+
userinfo: [
|
|
58
|
+
["oauth_userinfo_path", 0, "/oauth/userinfo", :get, :show],
|
|
59
|
+
["oauth_userinfo_url", 0, "/oauth/userinfo", :get, :show]
|
|
60
|
+
],
|
|
61
|
+
applications: [
|
|
62
|
+
["oauth_applications_path", 0, "/oauth/applications", :get, :index],
|
|
63
|
+
["oauth_applications_url", 0, "/oauth/applications", :get, :index],
|
|
64
|
+
["new_oauth_application_path", 0, "/oauth/applications/new", :get, :new],
|
|
65
|
+
["new_oauth_application_url", 0, "/oauth/applications/new", :get, :new],
|
|
66
|
+
["edit_oauth_application_path", 1, "/oauth/applications/:id/edit", :get, :edit],
|
|
67
|
+
["edit_oauth_application_url", 1, "/oauth/applications/:id/edit", :get, :edit],
|
|
68
|
+
["oauth_application_path", 1, "/oauth/applications/:id", :get, :show],
|
|
69
|
+
["oauth_application_url", 1, "/oauth/applications/:id", :get, :show]
|
|
70
|
+
],
|
|
71
|
+
authorized_applications: [
|
|
72
|
+
["oauth_authorized_applications_path", 0, "/oauth/authorized_applications", :get, :index],
|
|
73
|
+
["oauth_authorized_applications_url", 0, "/oauth/authorized_applications", :get, :index],
|
|
74
|
+
["oauth_authorized_application_path", 1, "/oauth/authorized_applications/:id", :delete, :destroy],
|
|
75
|
+
["oauth_authorized_application_url", 1, "/oauth/authorized_applications/:id", :delete, :destroy]
|
|
76
|
+
]
|
|
77
|
+
}.freeze
|
|
78
|
+
|
|
79
|
+
module_function
|
|
80
|
+
|
|
81
|
+
# @param skip [Array<Symbol>] controller names the
|
|
82
|
+
# project omits via `skip_controllers :name, ...`.
|
|
83
|
+
# @return [Array<HelperTable::Entry>] flattened entries.
|
|
84
|
+
def generate(skip: [])
|
|
85
|
+
skip_set = skip.to_set(&:to_sym)
|
|
86
|
+
CONTROLLER_HELPERS.flat_map do |controller, rows|
|
|
87
|
+
next [] if skip_set.include?(controller)
|
|
88
|
+
|
|
89
|
+
rows.map do |name, arity, path, http_method, action|
|
|
90
|
+
HelperTable::Entry.new(
|
|
91
|
+
name: name, arity: arity, path: path,
|
|
92
|
+
http_method: http_method, action: action
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|