DEV Community

Douglas Lise
Douglas Lise

Posted on • Edited on

How to start using ruby syntatic analysis in a legacy application?

As known in ruby community, Rubocop is the most used tool for static code analysis.

It comes with a lot of default rules, what is very useful for new projects, when you could follow the most of defaults and just customize some of them.

But how to proceed in legacy or bigger projects, that have a lot of files already written and potentially having many issues?

Rubocop gem comes with an auto-formatter that could fix a lot of issues automatically in our code.

Additionally, it could create a TODO config file that ignores all the issues already present, file by file. So we can start using rubocop just for new files, or new issues in existing files, and incrementally fix issue by issue.

The recommendation is to fix all possible issues automatically and keep others as ignored, to be fixed in a future step.

You can start by adding the gem to Gemfile (or simply installing it, but it is recommended to keep it in Gemfile).

$> bundle add rubocop

With this you could just run rubocop and all issues will be shown. Like this example:

$> rubocop
...
test/test_helper.rb:5:7: C: Style/ClassAndModuleChildren: Use nested module/class definitions instead of compact style.
class ActiveSupport::TestCase
      ^^^^^^^^^^^^^^^^^^^^^^^
test/test_helper.rb:6:81: C: Metrics/LineLength: Line is too long. [82/80]
  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
                                                                                ^^

135 files inspected, 1659 offenses detected

Now there are two strategies to go ahead.

1. Fix all issues in a single batch

The first strategy is to fix all the issues in a single batch. To do this just run rubocop --auto-correct and all fixable issues are fixed and shown with the "[Corrected]" status.

$> rubocop --auto-correct
...
ENV['RAILS_ENV'] ||= 'test'
^
test/application_system_test_case.rb:1:1: C: [Corrected] Style/FrozenStringLiteralComment: Missing magic comment # frozen_string_literal: true.
require "test_helper"
^

135 files inspected, 1848 offenses detected, 1485 offenses corrected

In the next run just the remaining issues are shown.

$> rubocop
...
135 files inspected, 363 offenses detected

We can now ignore them all running with --auto-gen-config option. This will create two files:

  • .rubocop_todo.yml: This file contains ignore rules for all current issues.
  • .rubocop.yml: The main configuration file. Initially just inheriting/including the TODO file.
$> rubocop --auto-gen-config
...
135 files inspected, 133 offenses detected
Created .rubocop_todo.yml.

The next run should return no issues, because they are all ignored in .rubocop_todo.yml:

$> rubocop
...
135 files inspected, no offenses detected

Now you need to just commit all the changes and, optionally, add a step in your CI pipeline to run rubocop to check your source files.

2. Fix issues cop by cop

The other strategy is to do this incrementally, using a script, to separate each cop fix in a different git commit. This helps in the future identifications of changes and why they happened.
To do this, just run this script and it will apply and commit each issue in a separated commit.

It is important to create this script in a path ignored by git, like tmp/script.rb. After create the script file, just call it running ruby tmp/script.rb.

# tmp/script.rb
require "yaml"
require "rubocop"

puts("Generating config...")
system("rubocop --auto-gen-config")
system("rm .rubocop.yml") # OR git checkout .rubocop.yml if you want to start with some config

puts("Reading TODO config to get all possible issues...")
todo = YAML.load_file(".rubocop_todo.yml")
system("rm .rubocop_todo.yml")

todo.each_with_index do |(cop, _cop_settings), index|
  puts("Fixing #{cop} (#{index + 1}/#{todo.count})...")
  if eval("RuboCop::Cop::#{cop.gsub("/", "::")}").new.support_autocorrect?
    files = `rubocop --auto-correct --force-default-config --only #{cop} --format files`.split("\n")
    system("git add #{files.join(" ")}")
    system("git commit -m \"chore: Correct source files with rubocop #{cop} cop\" \
            --author \"Rubocop Auto Correct <rubocop@rubocop>\"")
  else
    puts("  Does not support auto-correct")
  end
end

puts("Generating final TODO file with _uncorrectable_ issues...")
system("rubocop --auto-gen-config")

puts("Done")

It may take several minutes to run, depending on the number and size of your source files.

Rubocop is designed to be safe, but some fixes could cause undesired behaviors, so it is important to have a great test coverage. You could run your unit tests in each iteration of this script or just run them in the end of the process. In this case, if an error occurs, you could use git bisect to find which issue introduced the error.

In the end the script generates commits like the ones bellow:

$> git log --oneline
fad06ac (HEAD -> rubocop) chore: Correct source files with rubocop Style/WordArray cop
22aca61 chore: Correct source files with rubocop Style/ClassCheck cop
06b12a2 chore: Correct source files with rubocop Style/BracesAroundHashParameters cop
6dcace8 chore: Correct source files with rubocop Style/BlockDelimiters cop
a504890 chore: Correct source files with rubocop Style/BlockComments cop
1fae73b chore: Correct source files with rubocop Naming/RescuedExceptionsVariableName cop
10802ca chore: Correct source files with rubocop Lint/UnusedMethodArgument cop
bd11ffc chore: Correct source files with rubocop Lint/UnusedBlockArgument cop
b8e098d chore: Correct source files with rubocop Layout/TrailingBlankLines cop
4cabbd7 chore: Correct source files with rubocop Layout/Tab cop
f29e766 chore: Correct source files with rubocop Layout/SpaceInsidePercentLiteralDelimiters cop
b451890 chore: Correct source files with rubocop Layout/SpaceInsideHashLiteralBraces cop
ee9eae6 chore: Correct source files with rubocop Layout/SpaceInsideBlockBraces cop
f6611fd chore: Correct source files with rubocop Layout/SpaceInsideArrayLiteralBrackets cop
f858545 chore: Correct source files with rubocop Layout/SpaceInLambdaLiteral cop
3aeed2e chore: Correct source files with rubocop Layout/SpaceBeforeComment cop
8b40371 chore: Correct source files with rubocop Layout/SpaceBeforeBlockBraces cop
444f7ae chore: Correct source files with rubocop Layout/SpaceAroundEqualsInParameterDefault cop
42d4e69 chore: Correct source files with rubocop Layout/SpaceAfterComma cop
1acc003 chore: Correct source files with rubocop Layout/SpaceAfterColon cop
28ed714 chore: Correct source files with rubocop Layout/MultilineOperationIndentation cop
3e08ad7 chore: Correct source files with rubocop Layout/MultilineMethodCallIndentation cop
f4611e5 chore: Correct source files with rubocop Layout/MultilineMethodCallBraceLayout cop
6cc68b6 chore: Correct source files with rubocop Layout/MultilineHashBraceLayout cop
9f66fe9 chore: Correct source files with rubocop Layout/LeadingCommentSpace cop
...

Regardless of the strategy used, it is recommended to resolve all the issues that remained in the TODO file and, incrementally, move all ignores that really needs to be ignored to the main .rubocop.yml file. This will ensure that you keeps improving your code or, or at least, maintaining its quality.

Finally, rubocop is a great tool that helps us keeping and improving the quality of our code. It is extremelly recomended to use it in all of our ruby projects.

Top comments (0)