Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Created March 24, 2021 18:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/d8c788af5c3e926abbb53058477dbd06 to your computer and use it in GitHub Desktop.
Save JoshCheek/d8c788af5c3e926abbb53058477dbd06 to your computer and use it in GitHub Desktop.
Show unused / untested stuff in Rails
require 'set'
# For answering questions:
# * What views exist that we didn't render?
# * What controller actions exist that we didn't hit?
# * What routes exist that we didn't request? / generate a link to?
# * What files exist that we never required
class ShowUnused
def self.call(outstream)
data = {
views_rendered: Set.new,
controller_actions_hit: Set.new,
routes_requested: Set.new,
}
at_exit { write data, outstream }
ActiveSupport::Notifications.subscribe('start_processing.action_controller') do |name, start, finish, id, payload|
data[:controller_actions_hit] << [
payload[:controller],
payload[:action],
]
data[:routes_requested] << [
payload[:method],
payload[:path],
]
end
ActiveSupport::Notifications.subscribe('render_template.action_view') do |name, start, finish, id, payload|
data[:views_rendered] << payload[:identifier]
end
ActiveSupport::Notifications.subscribe('render_partial.action_view') do |name, start, finish, id, payload|
data[:views_rendered] << payload[:identifier]
end
end
def self.write(data, outstream)
new(data: data, outstream: outstream).call
end
def initialize(data:, outstream:)
self.data, self.outstream = data, outstream
end
def call
header 'Unused Views'
unused_views.each { |path| puts relative_path path }
puts
header 'Unused Controller Actions'
unused_actions.each { |controller, action| puts "#{controller}##{action}" }
puts
header 'Unused Routes'
print_table unused_routes
puts
header 'Unrequired Files'
unrequired_files.each { |path| puts relative_path path }
end
private
attr_accessor :data, :outstream
def header(str)
puts "\e[94m===== #{str} ====\e[0m"
end
def puts(*strs)
outstream.puts *strs
end
def print_table(hashes)
keys = hashes.flat_map(&:keys).uniq
rows = hashes.map { |hash| hash.values_at(*keys).map(&:to_s) }
sizes = [keys, *rows].transpose.map { |col| col.map(&:size).max }
fmt = sizes.map { |size| "%-#{size}s" }.join(" | ")
puts fmt % keys
puts (fmt % sizes.map { "" }).tr(" |", "-+")
rows.each { |row| puts fmt % row }
end
def relative_path(absolute_path)
relative_path = Pathname(absolute_path).relative_path_from(Rails.root).to_s
relative_path.sub(%r[^(app/[^/]+/)(.*?)(\.[^.]*)?$]) { "\e[0m#$1\e[92m#$2\e[0m#$3" }
end
def unused_views
glob = Rails.root/'app/views/**/*'
existing_views = Dir[glob].reject { |f| File.directory? f }
observed_views = data[:views_rendered].to_a
existing_views - observed_views
end
def unrequired_files
glob = Rails.root/'app/**/*.rb'
possible_files = Dir[glob]
required_files = $LOADED_FEATURES
(possible_files - required_files).sort
end
def unused_actions
existing_actions = ApplicationController
.descendants
.reject(&:abstract?)
.flat_map { |c|
c.public_instance_methods(false).map { |m| [c.to_s, m.to_s] }
}
observed_actions = data[:controller_actions_hit].to_a
(existing_actions - observed_actions).map do |controller, action|
controller = controller.sub(/Controller$/, '').tableize
[controller, action]
end
end
def unused_routes
existing_routes = Rails.application.routes.routes
.reject { |r| r.path.spec.to_s.start_with? "/rails/" }
.reject(&:internal)
.to_a
observed_routes = data[:routes_requested].map { |verb, path| route_for verb, path }
(existing_routes - observed_routes).map do |route|
wrapped = ActionDispatch::Routing::RouteWrapper.new route
{ verb: route.verb,
path: wrapped.path.chomp('(.:format)'),
name: wrapped.name,
controller: wrapped.controller,
action: wrapped.action,
}
end.sort_by { |r| r[:path] }
end
def route_for(verb, path)
Rails.application.routes.routes.simulator.memos(path) { [] }.find do |route|
route.verb == verb
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment