Skip to content

Commit 9414a8b

Browse files
authored
Merge pull request rails#45701 from gmcgibbon/unused_routes_script
Add `routes --unused` option to detect extraneous routes.
2 parents 914ac17 + 5613b12 commit 9414a8b

File tree

7 files changed

+291
-5
lines changed

7 files changed

+291
-5
lines changed

actionpack/lib/action_dispatch/journey/routes.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ class Routes # :nodoc:
99

1010
attr_reader :routes, :custom_routes, :anchored_routes
1111

12-
def initialize
13-
@routes = []
12+
def initialize(routes = [])
13+
@routes = routes
1414
@ast = nil
1515
@anchored_routes = []
1616
@custom_routes = []

actionpack/lib/action_dispatch/routing/inspector.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,27 @@ def route_header(index:)
237237
"--[ Route #{index} ]".ljust(@width, "-")
238238
end
239239
end
240+
241+
class Unused < Sheet
242+
def header(routes)
243+
@buffer << <<~MSG
244+
Found #{routes.count} unused #{"route".pluralize(routes.count)}:
245+
MSG
246+
247+
super
248+
end
249+
250+
def no_routes(routes, filter)
251+
@buffer <<
252+
if filter.none?
253+
"No unused routes found."
254+
elsif filter.key?(:controller)
255+
"No unused routes found for this controller."
256+
elsif filter.key?(:grep)
257+
"No unused routes found for this grep pattern."
258+
end
259+
end
260+
end
240261
end
241262

242263
class HtmlTableFormatter

railties/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
* Add `routes --unused` option to detect extraneous routes.
2+
3+
Example:
4+
5+
```
6+
> bin/rails rails --unused
7+
8+
Found 2 unused routes:
9+
10+
Prefix Verb URI Pattern Controller#Action
11+
one GET /one(.:format) action#one
12+
two GET /two(.:format) action#two
13+
```
14+
15+
*Gannon McGibbon*
16+
117
* Add `--parent` option to controller generator to specify parent class of job.
218
319
Example:

railties/lib/rails/commands/routes/routes_command.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ class RoutesCommand < Base # :nodoc:
88
class_option :controller, aliases: "-c", desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController."
99
class_option :grep, aliases: "-g", desc: "Grep routes by a specific pattern."
1010
class_option :expanded, type: :boolean, aliases: "-E", desc: "Print routes expanded vertically with parts explained."
11+
class_option :unused, type: :boolean, aliases: "-u", desc: "Print unused routes."
12+
13+
def invoke_command(*)
14+
if options.key?("unused")
15+
Rails::Command.invoke "unused_routes", ARGV
16+
else
17+
super
18+
end
19+
end
1120

1221
def perform(*)
1322
require_application_and_environment!
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
require "rails/commands/routes/routes_command"
4+
5+
module Rails
6+
module Command
7+
class UnusedRoutesCommand < Rails::Command::Base # :nodoc:
8+
hide_command!
9+
class_option :controller, aliases: "-c", desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController."
10+
class_option :grep, aliases: "-g", desc: "Grep routes by a specific pattern."
11+
12+
class RouteInfo
13+
def initialize(route)
14+
requirements = route.requirements
15+
@controller_name = requirements[:controller]
16+
@action_name = requirements[:action]
17+
@controller_class = (@controller_name.to_s.camelize + "Controller").safe_constantize
18+
end
19+
20+
def unused?
21+
controller_class_missing? || (action_missing? && template_missing?)
22+
end
23+
24+
private
25+
def view_path(root)
26+
File.join(root.path, @controller_name, @action_name)
27+
end
28+
29+
def controller_class_missing?
30+
@controller_name && @controller_class.nil?
31+
end
32+
33+
def template_missing?
34+
@controller_class && @controller_class.try(:view_paths).to_a.flat_map { |path| Dir["#{view_path(path)}.*"] }.none?
35+
end
36+
37+
def action_missing?
38+
@controller_class && @controller_class.instance_methods.exclude?(@action_name.to_sym)
39+
end
40+
end
41+
42+
def perform(*)
43+
require_application_and_environment!
44+
require "action_dispatch/routing/inspector"
45+
46+
say(inspector.format(formatter, routes_filter))
47+
48+
exit(1) if routes.any?
49+
end
50+
51+
private
52+
def inspector
53+
ActionDispatch::Routing::RoutesInspector.new(routes)
54+
end
55+
56+
def routes
57+
@routes ||= begin
58+
routes = Rails.application.routes.routes.select do |route|
59+
RouteInfo.new(route).unused?
60+
end
61+
62+
ActionDispatch::Journey::Routes.new(routes)
63+
end
64+
end
65+
66+
def formatter
67+
ActionDispatch::Routing::ConsoleFormatter::Unused.new
68+
end
69+
70+
def routes_filter
71+
options.symbolize_keys.slice(:controller, :grep)
72+
end
73+
end
74+
end
75+
end

railties/test/commands/routes_test.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
251251
URI | /rails/conductor/action_mailbox/inbound_emails(.:format)
252252
Controller#Action | rails/conductor/action_mailbox/inbound_emails#index
253253
--[ Route 9 ]--------------
254-
Prefix |
254+
Prefix |#{" "}
255255
Verb | POST
256256
URI | /rails/conductor/action_mailbox/inbound_emails(.:format)
257257
Controller#Action | rails/conductor/action_mailbox/inbound_emails#create
@@ -296,7 +296,7 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
296296
URI | /rails/active_storage/blobs/proxy/:signed_id/*filename(.:format)
297297
Controller#Action | active_storage/blobs/proxy#show
298298
--[ Route 18 ]-------------
299-
Prefix |
299+
Prefix |#{" "}
300300
Verb | GET
301301
URI | /rails/active_storage/blobs/:signed_id/*filename(.:format)
302302
Controller#Action | active_storage/blobs/redirect#show
@@ -311,7 +311,7 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
311311
URI | /rails/active_storage/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format)
312312
Controller#Action | active_storage/representations/proxy#show
313313
--[ Route 21 ]-------------
314-
Prefix |
314+
Prefix |#{" "}
315315
Verb | GET
316316
URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format)
317317
Controller#Action | active_storage/representations/redirect#show
@@ -334,6 +334,17 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
334334
# rubocop:enable Layout/TrailingWhitespace
335335
end
336336

337+
test "rails routes with unused option" do
338+
app_file "config/routes.rb", <<-RUBY
339+
Rails.application.routes.draw do
340+
end
341+
RUBY
342+
343+
output = run_routes_command([ "--unused" ])
344+
345+
assert_equal(output, "No unused routes found.\n")
346+
end
347+
337348
private
338349
def run_routes_command(args = [])
339350
rails "routes", args
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# frozen_string_literal: true
2+
3+
require "isolation/abstract_unit"
4+
require "rails/command"
5+
require "rails/commands/routes/routes_command"
6+
require "io/console/size"
7+
8+
class Rails::Command::UnusedRoutesTest < ActiveSupport::TestCase
9+
setup :build_app
10+
teardown :teardown_app
11+
12+
test "no results" do
13+
app_file "config/routes.rb", <<-RUBY
14+
Rails.application.routes.draw do
15+
end
16+
RUBY
17+
18+
assert_equal <<~OUTPUT, run_unused_routes_command
19+
No unused routes found.
20+
OUTPUT
21+
end
22+
23+
test "no controller" do
24+
app_file "config/routes.rb", <<-RUBY
25+
Rails.application.routes.draw do
26+
get "/", to: "my#index", as: :my_route
27+
end
28+
RUBY
29+
30+
assert_equal <<~OUTPUT, run_unused_routes_command(allow_failure: true)
31+
Found 1 unused route:
32+
33+
Prefix Verb URI Pattern Controller#Action
34+
my_route GET / my#index
35+
OUTPUT
36+
end
37+
38+
test "no action" do
39+
app_file "app/controllers/my_controller.rb", <<-RUBY
40+
class MyController < ActionController::Base
41+
end
42+
RUBY
43+
44+
app_file "config/routes.rb", <<-RUBY
45+
Rails.application.routes.draw do
46+
get "/", to: "my#index", as: :my_route
47+
end
48+
RUBY
49+
50+
assert_equal <<~OUTPUT, run_unused_routes_command(allow_failure: true)
51+
Found 1 unused route:
52+
53+
Prefix Verb URI Pattern Controller#Action
54+
my_route GET / my#index
55+
OUTPUT
56+
end
57+
58+
test "implicit render" do
59+
app_file "app/controllers/my_controller.rb", <<-RUBY
60+
class MyController < ActionController::Base
61+
end
62+
RUBY
63+
64+
app_file "app/views/my/index.html.erb", <<-HTML
65+
<h1>Hello world</h1>
66+
HTML
67+
68+
app_file "config/routes.rb", <<-RUBY
69+
Rails.application.routes.draw do
70+
get "/", to: "my#index", as: :my_route
71+
end
72+
RUBY
73+
74+
assert_equal <<~OUTPUT, run_unused_routes_command
75+
No unused routes found.
76+
OUTPUT
77+
end
78+
79+
test "multiple unused routes" do
80+
app_file "config/routes.rb", <<-RUBY
81+
Rails.application.routes.draw do
82+
get "/one", to: "action#one"
83+
get "/two", to: "action#two"
84+
end
85+
RUBY
86+
87+
assert_equal <<~OUTPUT, run_unused_routes_command(allow_failure: true)
88+
Found 2 unused routes:
89+
90+
Prefix Verb URI Pattern Controller#Action
91+
one GET /one(.:format) action#one
92+
two GET /two(.:format) action#two
93+
OUTPUT
94+
end
95+
96+
test "filter by grep" do
97+
app_file "config/routes.rb", <<-RUBY
98+
Rails.application.routes.draw do
99+
get "/one", to: "posts#one"
100+
get "/two", to: "users#two"
101+
end
102+
RUBY
103+
104+
assert_equal <<~OUTPUT, run_unused_routes_command(["-g", "one"], allow_failure: true)
105+
Found 1 unused route:
106+
107+
Prefix Verb URI Pattern Controller#Action
108+
one GET /one(.:format) posts#one
109+
OUTPUT
110+
end
111+
112+
test "filter by grep no results" do
113+
app_file "config/routes.rb", <<-RUBY
114+
Rails.application.routes.draw do
115+
end
116+
RUBY
117+
118+
assert_equal <<~OUTPUT, run_unused_routes_command(["-g", "one"])
119+
No unused routes found for this grep pattern.
120+
OUTPUT
121+
end
122+
123+
test "filter by controller" do
124+
app_file "config/routes.rb", <<-RUBY
125+
Rails.application.routes.draw do
126+
get "/one", to: "posts#one"
127+
get "/two", to: "users#two"
128+
end
129+
RUBY
130+
131+
assert_equal <<~OUTPUT, run_unused_routes_command(["-c", "posts"], allow_failure: true)
132+
Found 1 unused route:
133+
134+
Prefix Verb URI Pattern Controller#Action
135+
one GET /one(.:format) posts#one
136+
OUTPUT
137+
end
138+
139+
test "filter by controller no results" do
140+
app_file "config/routes.rb", <<-RUBY
141+
Rails.application.routes.draw do
142+
end
143+
RUBY
144+
145+
assert_equal <<~OUTPUT, run_unused_routes_command(["-c", "posts"])
146+
No unused routes found for this controller.
147+
OUTPUT
148+
end
149+
150+
private
151+
def run_unused_routes_command(args = [], allow_failure: false)
152+
rails "unused_routes", args, allow_failure: allow_failure
153+
end
154+
end

0 commit comments

Comments
 (0)