Add routes --unused option to detect extraneous routes.

Routes take a long time to draw. Over time, a Rails app can become slow
to boot simply because of how many routes it has. This script can be
used to detect routes that are drawn, but aren't actually valid.
Removing routes this script detects can help speed up your app and
remove dead code.

Example:

```
> bin/rails routes --unused

Found 2 unused routes:

Prefix Verb URI Pattern    Controller#Action
   one GET  /one(.:format) action#one
   two GET  /two(.:format) action#two
```
This commit is contained in:
Gannon McGibbon 2022-07-29 18:14:53 -05:00
parent bfa3a5baab
commit 5613b1240a
7 changed files with 291 additions and 5 deletions

@ -9,8 +9,8 @@ class Routes # :nodoc:
attr_reader :routes, :custom_routes, :anchored_routes
def initialize
@routes = []
def initialize(routes = [])
@routes = routes
@ast = nil
@anchored_routes = []
@custom_routes = []

@ -237,6 +237,27 @@ def route_header(index:)
"--[ Route #{index} ]".ljust(@width, "-")
end
end
class Unused < Sheet
def header(routes)
@buffer << <<~MSG
Found #{routes.count} unused #{"route".pluralize(routes.count)}:
MSG
super
end
def no_routes(routes, filter)
@buffer <<
if filter.none?
"No unused routes found."
elsif filter.key?(:controller)
"No unused routes found for this controller."
elsif filter.key?(:grep)
"No unused routes found for this grep pattern."
end
end
end
end
class HtmlTableFormatter

@ -1,3 +1,19 @@
* Add `routes --unused` option to detect extraneous routes.
Example:
```
> bin/rails rails --unused
Found 2 unused routes:
Prefix Verb URI Pattern Controller#Action
one GET /one(.:format) action#one
two GET /two(.:format) action#two
```
*Gannon McGibbon*
* Add `--parent` option to controller generator to specify parent class of job.
Example:

@ -8,6 +8,15 @@ class RoutesCommand < Base # :nodoc:
class_option :controller, aliases: "-c", desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController."
class_option :grep, aliases: "-g", desc: "Grep routes by a specific pattern."
class_option :expanded, type: :boolean, aliases: "-E", desc: "Print routes expanded vertically with parts explained."
class_option :unused, type: :boolean, aliases: "-u", desc: "Print unused routes."
def invoke_command(*)
if options.key?("unused")
Rails::Command.invoke "unused_routes", ARGV
else
super
end
end
def perform(*)
require_application_and_environment!

@ -0,0 +1,75 @@
# frozen_string_literal: true
require "rails/commands/routes/routes_command"
module Rails
module Command
class UnusedRoutesCommand < Rails::Command::Base # :nodoc:
hide_command!
class_option :controller, aliases: "-c", desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController."
class_option :grep, aliases: "-g", desc: "Grep routes by a specific pattern."
class RouteInfo
def initialize(route)
requirements = route.requirements
@controller_name = requirements[:controller]
@action_name = requirements[:action]
@controller_class = (@controller_name.to_s.camelize + "Controller").safe_constantize
end
def unused?
controller_class_missing? || (action_missing? && template_missing?)
end
private
def view_path(root)
File.join(root.path, @controller_name, @action_name)
end
def controller_class_missing?
@controller_name && @controller_class.nil?
end
def template_missing?
@controller_class && @controller_class.try(:view_paths).to_a.flat_map { |path| Dir["#{view_path(path)}.*"] }.none?
end
def action_missing?
@controller_class && @controller_class.instance_methods.exclude?(@action_name.to_sym)
end
end
def perform(*)
require_application_and_environment!
require "action_dispatch/routing/inspector"
say(inspector.format(formatter, routes_filter))
exit(1) if routes.any?
end
private
def inspector
ActionDispatch::Routing::RoutesInspector.new(routes)
end
def routes
@routes ||= begin
routes = Rails.application.routes.routes.select do |route|
RouteInfo.new(route).unused?
end
ActionDispatch::Journey::Routes.new(routes)
end
end
def formatter
ActionDispatch::Routing::ConsoleFormatter::Unused.new
end
def routes_filter
options.symbolize_keys.slice(:controller, :grep)
end
end
end
end

@ -251,7 +251,7 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
URI | /rails/conductor/action_mailbox/inbound_emails(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#index
--[ Route 9 ]--------------
Prefix |
Prefix |#{" "}
Verb | POST
URI | /rails/conductor/action_mailbox/inbound_emails(.:format)
Controller#Action | rails/conductor/action_mailbox/inbound_emails#create
@ -296,7 +296,7 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
URI | /rails/active_storage/blobs/proxy/:signed_id/*filename(.:format)
Controller#Action | active_storage/blobs/proxy#show
--[ Route 18 ]-------------
Prefix |
Prefix |#{" "}
Verb | GET
URI | /rails/active_storage/blobs/:signed_id/*filename(.:format)
Controller#Action | active_storage/blobs/redirect#show
@ -311,7 +311,7 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
URI | /rails/active_storage/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format)
Controller#Action | active_storage/representations/proxy#show
--[ Route 21 ]-------------
Prefix |
Prefix |#{" "}
Verb | GET
URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format)
Controller#Action | active_storage/representations/redirect#show
@ -334,6 +334,17 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase
# rubocop:enable Layout/TrailingWhitespace
end
test "rails routes with unused option" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
end
RUBY
output = run_routes_command([ "--unused" ])
assert_equal(output, "No unused routes found.\n")
end
private
def run_routes_command(args = [])
rails "routes", args

@ -0,0 +1,154 @@
# frozen_string_literal: true
require "isolation/abstract_unit"
require "rails/command"
require "rails/commands/routes/routes_command"
require "io/console/size"
class Rails::Command::UnusedRoutesTest < ActiveSupport::TestCase
setup :build_app
teardown :teardown_app
test "no results" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
end
RUBY
assert_equal <<~OUTPUT, run_unused_routes_command
No unused routes found.
OUTPUT
end
test "no controller" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get "/", to: "my#index", as: :my_route
end
RUBY
assert_equal <<~OUTPUT, run_unused_routes_command(allow_failure: true)
Found 1 unused route:
Prefix Verb URI Pattern Controller#Action
my_route GET / my#index
OUTPUT
end
test "no action" do
app_file "app/controllers/my_controller.rb", <<-RUBY
class MyController < ActionController::Base
end
RUBY
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get "/", to: "my#index", as: :my_route
end
RUBY
assert_equal <<~OUTPUT, run_unused_routes_command(allow_failure: true)
Found 1 unused route:
Prefix Verb URI Pattern Controller#Action
my_route GET / my#index
OUTPUT
end
test "implicit render" do
app_file "app/controllers/my_controller.rb", <<-RUBY
class MyController < ActionController::Base
end
RUBY
app_file "app/views/my/index.html.erb", <<-HTML
<h1>Hello world</h1>
HTML
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get "/", to: "my#index", as: :my_route
end
RUBY
assert_equal <<~OUTPUT, run_unused_routes_command
No unused routes found.
OUTPUT
end
test "multiple unused routes" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get "/one", to: "action#one"
get "/two", to: "action#two"
end
RUBY
assert_equal <<~OUTPUT, run_unused_routes_command(allow_failure: true)
Found 2 unused routes:
Prefix Verb URI Pattern Controller#Action
one GET /one(.:format) action#one
two GET /two(.:format) action#two
OUTPUT
end
test "filter by grep" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get "/one", to: "posts#one"
get "/two", to: "users#two"
end
RUBY
assert_equal <<~OUTPUT, run_unused_routes_command(["-g", "one"], allow_failure: true)
Found 1 unused route:
Prefix Verb URI Pattern Controller#Action
one GET /one(.:format) posts#one
OUTPUT
end
test "filter by grep no results" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
end
RUBY
assert_equal <<~OUTPUT, run_unused_routes_command(["-g", "one"])
No unused routes found for this grep pattern.
OUTPUT
end
test "filter by controller" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get "/one", to: "posts#one"
get "/two", to: "users#two"
end
RUBY
assert_equal <<~OUTPUT, run_unused_routes_command(["-c", "posts"], allow_failure: true)
Found 1 unused route:
Prefix Verb URI Pattern Controller#Action
one GET /one(.:format) posts#one
OUTPUT
end
test "filter by controller no results" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
end
RUBY
assert_equal <<~OUTPUT, run_unused_routes_command(["-c", "posts"])
No unused routes found for this controller.
OUTPUT
end
private
def run_unused_routes_command(args = [], allow_failure: false)
rails "unused_routes", args, allow_failure: allow_failure
end
end