Dead code is code that is never executed. It can be a commented out block of code, a method that's no longer called, or an unreachable return statement. It often reflects functionality that no longer exists. Dead code has absolutely no upside and it costs us money, time, and maintenance headaches.

It's possible to identify the unused block of code in smaller projects. But in larger projects, it is not as straightforward. It is a delicate process and requires absolute surety of the deadness status to avoid any unexpected breakdowns. A few tools are present that can aid us in analyzing dead code to some extent. We can divide these tools broadly into two categories.

  • Static analysis tools - Static analysis tools examine the source code without executing it. And hence there is a precision issue of whether the detected code is actually in use during the execution. Example - Debride, Unused
  • Dynamic analysis tools - Dynamic analysis tools examine the source code at runtime and provides richer and more accurate results. These tools are slower than the static analysis tools because of runtime analysis. Example - Coverband, Scythe

These analysis tools generate a list of unused methods. While this list appears to be like a dead code list, it only is a list of potential dead code. None of these tools give you 100 percent confidence in the identification of a dead code. A deeper investigation is required to validate the usage of the code in question.

To begin with a manual investigation,  we should start by searching the method's name in the codebase with the help of search functionality provided by IDE or using ripgrep. One thing we should be aware of at this point is the use of Ruby metaprogramming in method calls.

Once we are certain that the method is not used in the current codebase, we need to dig into the archaeological history of the method and address questions like -  "When was the method introduced? How it was used? When was it last used?"

We can use the following git pickaxe command to get the complete history of the method.

git log -S  'foo'  --full-history

The above command enlists every commit that either introduced or removed the string foo. The -S switch takes a string match. If we want to match a regex instead, we can add the --pickaxe-regex switch.

After this detailed study, when we are certain that the code in question is indeed dead, we can create a "proposed-as-dead" commit for the code block and create a pull request where we can put all of our findings for easy traceback.

However, while working with legacy applications there is always this paranoia of production breakdowns. We know that code-cleaning/refactoring is a good habit but it should not come at the cost of breaking a feature or bugs or production downtime.

To tackle the paranoia we introduced an extra step in the process. In which the solution is to log the potential dead methods with a special tag proposed-dead-code, and then check production logs in short and long intervals (eg. once a quarter or semi-annually), to analyze if there are any such entries. The time period here definitely varies as per specific use cases. In general, if any of the proposed-dead-method is alive, it will show up in the production logs and we can revert the proposed-as-dead commit. And if there are no such entries in the log for at least a sufficient time span then we can safely remove all the proposed-as-dead methods from the codebase in a separate Pull Request or series of Pull Requests to make it easier to review and remove.

We can also set up automated monitoring on production to keep an eye out for the proposed-dead-code label. If any occurrence of the label is found in the logs, an email can be triggered with dead methods stack-trace to the dev team.

To implement the above approach, we created a log method in dead_code_logger.rb module:

  module DeadCodeLogger
    class << self
      def log(pr_link, &exec_block)
        exec_block.call
      ensure
        Rails.logger.tagged('proposed-dead-code') do |logger|
          logger.info "Proposed as dead code in #{pr_link} and executed at #{exec_block.source_location}"
        end
      end
    end
  end
lib/dead_code_logger.rb

It accepts two parameters:

  • pr_link: GitHub Pull request in which the findings of potential dead code is documented
  • exec_block: The method body to be tested for being a potential dead code

dead-code-logger method is called from the proposed dead method with hardcoded GitHub pull request link and complete contents of the current method. This ensures if the method gets called, it runs as it is and ensures to log details.

  # :dead-code:
  def foo
    DeadCodeLogger.log 'https://github.com/username/sample-app/pull/1' do
      return 'bar';
    end
  end
  # :dead-code:
app/models/foo.rb

If you're using any code coverage analysis tool like simplecov and you don't want potential dead methods to be included in test coverage reports. You can wrap the method under label :dead-code. With this label mechanism in place, we do not have to write/maintain test cases for proposed-dead-code. The name of the label could be modified in spec_helper.rb with a simple configuration. Check the code below for details.

SimpleCov.start do
  nocov_token 'dead-code'
end
spec/spec_helper.rb

Do you have similar dead code-related challenges in your legacy application? What is your go-to tool for dead code analysis? Did you like the idea in this article of how we approached the dead code?

References