Rails: Cleanup after a big rewrite - find unused views, Javascript assets, routes, translations, Ruby files

18th March 2023 – 1520 words

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_actions, 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(-)