Automated Code Refactoring
An in-depth guide to choosing the best code refactoring tools
As developers, we have all been there. Staring at an unsavory piece of code, we decide to refactor and clean things up a bit. This leads to hours of followup changes to make the code compile and run tests successfully. Sometimes, this can also happen not by choice, due to various factors, such as:
Upgrading dependencies to fix security vulnerabilities, which introduce breaking APIs.
Dead code behind obsolete feature flags, making code hard to comprehend and test.
Migrating to the latest version of a platform or framework nearing the end of its life.
Refactoring Tools
Many tools exist to streamline the process of refactoring code. They fall into a few broad categories based on their capabilities. Each tool excels depending on the situation.
Lexical
Ideal for straightforward tasks that involve textual pattern matching. Grep and sed have been around for decades as part of the core command line tooling in Unix. These tools are fast (e.g., can take advantage of hardware-accelerated SIMD instructions), have detailed documentation and strong community support. Using advanced features like capture groups and alternations developers can automate many tasks that operate at the lexical analysis level.
However, many real world problems such as matching on complex expressions require operating on syntactic structures at the grammar level, which cannot be done with regular expressions. Furthermore, handling formatting differences becomes complicated using regular expressions. Tools like Comby introduce limited syntax-awareness to regular expressions via Dyck-extended grammars but cannot operate on the full syntax of a language.
Syntactic
To overcome the limitations of regular expressions, more advanced tools operate on the full syntactic structure of a program. One such tool is ast-grep, which allows syntactic find and replace. These tools allow powerful operations such as reordering arguments or matching on the else-clause of an if-statement. They are slower than regular expression based approaches because they generate a parse tree, and harder to use as the language for expressing the operations are at the grammar level.
Syntax matching has its limitations as parsing alone doesn’t include type information, which is required in refactoring involving type migration and updating method signatures. IDEs such as IntelliJ and language servers such as rust-analyzer augment their structural find-replace capabilities with type information.
Type based
A typed AST based refactoring tool operates by analyzing and manipulating the AST, with typing information included. This enables correct transformations by preserving the semantics of the code. They can automate complex refactoring tasks such as type migrations, nullness safety guarantees, & security fixes. Type-based tools are typically built using the native language compilers, e.g., tsmorph for TypeScript, ErrorProne for Java, and OpenRewrite for Java.
Building and maintaining type-based frameworks requires a deeper understanding of the language’s type system requiring a steep learning curve. Since they are language-specific, they are limited in their applicability across different programming languages. These tools fall short when transformations require understanding the dependencies, build order, changes in configuration files, codegen tools, etc. For example, upgrading a library version in Java may require updating pom.xml in Maven, build.gradle in Gradle and package.json in npm projects.
Build system based
Many IDEs utilize type information to ensure type-safe refactorings such as renaming symbols, extracting methods, and changing method signatures. They leverage build system information to manage cross-module dependencies, update project files, and adjust configurations. Examples of these tools include JetBrains’ ReSharper, IntelliJ IDEA, and Eclipse JDT.
Although these refactoring tools offer powerful capabilities that combine deep code analysis with project structures, they are complex and resource-intensive, and are often tightly integrated with specific IDEs. They are also harder to automate in environments like CI which run headless.
Role of AI
Integrating large language models (LLMs) into refactoring tools is an area of active research. LLMs can interpret comments and documentation alongside code to better understand the intent of the code. They offer refactoring capabilities that go beyond syntax and type correctness as they understand the broader context and functionality of the code. GitHub Copilot, Codota/Tabnine, and SourceGraph Cody are some examples of emerging LLM-based refactoring tools for suggesting improvements to existing code.
The capabilities of these tools are not yet fully understood and they tend to hallucinate which can produce inconsistent results. They also demand careful reviews by developers. Although LLMs understand a wide range of programming languages and tools, their suggestions might not account for the nuances of every language, compiler, and build system.
Summary
Based on our experience of using these tools for large scale automated code refactoring at top tech companies, we evaluate them against power, performance and user experience.
The path to optimal code refactoring is not one-size-fits-all. The choice of tool depends on the specific requirements of the task at hand, balancing power, performance, and user experience. At Gitar, we are dedicated to navigating this landscape, integrating the best tools for various scenarios to realize your code refactoring needs.
We invite you to join our Slack community, where we continue to explore and discuss these topics further.