Recently, we had a big refactoring of one of our major Rails apps. In the process, we redid a lot of views, removed part of functionality, and also switched from a hybrid Sprockets+Webpacker to a Vite/Vue3 frontend package. In that kind of process, that will come sooner or later to many long running apps, there is always a lot of stuff, that will be left over. Especially when you are not sure, what views, what assets, what files are still used, it is difficult to decided, what is safe to delete.
Luckily, there are a lot of great tools and recipes to help out. In this post, I want to present a couple of tools that we used.
First: Make sure to have a good test coverage
Without a good test suite, it is hard to make sure, the app will still work. Also, it is the basis for the Ruby based techniques. 70%+ is a good mark IMO.
Ruby classes - Use eager loading + SimpleCov
Use SimpleCov, make a full test-run and have a look at the ./coverage
folder. I usually just symlink it into the public dir:
cd public && ln -s ../coverage .
And then open the coverage in the browser: localhost:3000/coverage/
. Sort by “Avg Lines/file”. The one with a “zero” are good candidates for deletions.
You should set config.config.eager_load = true
to make sure that all files are loaded in test. Otherwise, you might miss files, that won’t show up in SimpleCov at all.
Exception from SimpleCov: your code is ONLY used be tests
After a couple of pivots, maybe some code is still “used”, but only be some tests. Or in an Admin Page that is not really linked anymore, but still tested in a controller test.
One good tool (that I’ve only used a few times), to help here is coverband
. It’s a ruby coverage tool, that you are supposed to run in production. After a while you can export the info and get the same view as SimpleCov, but this time for the production code. It’s more hassle to setup, but especially when you have no trustworthy test suite, it is a good alternative.
Views - Covered
to find unused Views
Recently, there is a new alternative to SimpleCov, called Covered. Compared to SimpleCov, this can also track the Code coverage of your Views. At the moment, it only a has command line options that are harder to use and evaluate compared to the HTML view by SimpleCov. But luckily, we only need the info, if a view is touched at all. Recently, I’ve build a small tool, that uses the database that covered generated, compare the list of views with your current files and list untouched.
You can not run simplecov and covered at the same time, so you just might toggle to either in the spec_helper
:
#spec_helper/test_helper
if ENV['COVERAGE']
require 'covered/rspec'
else
require 'simplecov'
end
Then, you can run it:
COVERAGE=BriefSummary rspec
This will generate a .covered.db
file, which is a marshalled version and not readable without using Covered classes. So we create a bin/unused-views
file:
#!/usr/bin/env ruby
# put in bin/unused-views
# add chmod +x
# run COVERAGE=Summary before
require 'covered'
infos = Covered::Persist.allocate.make_unpacker(File.open(".covered.db")).each.map { |r| r }
by_name = infos.group_by { |i| i[:path] }.transform_values { |i| i.last[:coverage] }
executed_views = by_name.filter { |k, v| k['app/views'] }.keys.sort
all = Dir['app/views/**/*'].select { |f| File.file?(f) }
unused_views = all - executed_views
if unused_views.any?
# wrap in red:
puts "\e[31m Unused views: \e[0m"
puts unused_views.sort
exit 1
else
puts "All views are used!"
end
# From: https://github.com/ioquatix/covered/discussions/20
This will print out a list of all unused view files.
Routes
If you are using Rails 7.1+ (which as of this post is not released), you can just use:
rails routes --unused
Otherwise, have a look at Tracetroute Gem.
Additionally, Traceroute can do the reverse, and find public Controller methods, that don’t correspond to a Route. But in the beginning, you will have a ton of false positives, as maybe your before_action
s, controller helper methods are found. Make sure, to mark them as private to exclude them.
Translations
If you have a multi-language app, make sure to have a look at i18n-tasks
. It might be a little work to configure at correctly, but then it can also find unused translations, auto-remove them, sort/normalize your translations (so every language file have the same tree of keys), and auto translate via Google-Translate or Deepl. A godsend!
Depending on your app, you also have to configure a couple of keys under ignore_unused
, that are not detected correctly, because they are used by Gems, like devise, pagination, simple_form etc.
Also, if you “misuse” Rails’ I18n like us, to also contain javascriptIi18n keys (because i18n-tasks is sooo damn useful and convenient), you can also just paste a new “scanner” into the config file:
<% I18n::Tasks.add_scanner 'I18n::Tasks::Scanners::PatternMapper', only: %w(*.vue), patterns: [[/v-t=["']+(?<key>[^'"]+)/, '%{key}']] %>
Or, if you use loaf
to manage your Breadcrumb hierarchy, you can make a new scanner:
# lib/i18n_scanners.rb
# and then import it in config/i18n-tasks.yml: #
# <% require './lib/i18n_scanners' %>
class BreadcrumbScanner < I18n::Tasks::Scanners::FileScanner
include I18n::Tasks::Scanners::OccurrenceFromPosition
KEY_IN_QUOTES = /(?<in>["'])(?<key>[\w\.]+)(?<in>["'])/.freeze
WRAPPED_KEY_IN_QUOTES = /(?<out>["'])#{KEY_IN_QUOTES}(?<out>["'])/.freeze
# @return [Array<[absolute key, Results::Occurrence]>]
def scan_file(path)
return [] unless path.include?('_controller.rb')
out = []
text = read_file(path)
text.to_enum(:scan, /breadcrumb :([a-z]+)/).each do |_|
key = Regexp.last_match[1]
occurrence = occurrence_from_position(
path, text, Regexp.last_match.offset(1).first
)
out << ["loaf.breadcrumbs.#{key}", occurrence]
end
text.to_enum(:scan, /breadcrumb #{KEY_IN_QUOTES}/).each do |_|
key = Regexp.last_match[:key]
next if key[/[A-Z ]/]
occurrence = occurrence_from_position(
path, text, Regexp.last_match.offset(1).first
)
out << ["loaf.breadcrumbs.#{key}", occurrence]
end
out
end
end
I18n::Tasks.add_scanner 'BreadcrumbScanner', only: %w(*.rb)
Et cetera.
Feel free to go all in on I18n-tasks. We run i18n-tasks health
as part of our CI-pipeline to detect any deviations, and run i18n-tasks normalize -p
regularly to get homogeneous yml-files.
Javascript files (Vite)
Lastly, if you also refactored your frontend, and you are using a Javascript-bundler, there are tools for most.
We like to use Vite, so we’d settle for @gatsbylabs/vite-plugin-unused
. There are similar plugins for ESbuild and probably Webpack, too.
// vite.config.ts
import { pluginUnused } from "@gatsbylabs/vite-plugin-unused";
export default {
...
plugins: [
...
pluginUnused({ root: '.', ext: ["*.vue", "*.js", "*.ts", "*.jsx", "*.tsx"] }),
]
}
When you now run a .bin/vite build
, it will print a list of unused frontend files that are strong candidates for deletion.
Happy deleting!
662 files changed, 16577 insertions(+), 19125 deletions(-)