Automated refactorings for Ruby
✒️ Description | A RubyMine IDE plugin that implements various automated code refactorings for Ruby, developed based on real-world usage patterns. |
⚡ Technologies | Scala, JetBrains Platform SDK, Ruby |
🔌 Download | JetBrains Marketplace |
🗂️ Source Code | GitHub Repository |
From development notes to a working implementation
Refactoring —making small, systematic changes to improve program structure without altering its behavior[1]— is a fundamental part of the software development process, especially when practicing Test-Driven Development. IDEs often offer automated refactoring tools, but their availability and sophistication vary significantly across different programming languages.
While working on a Ruby project, I frequently found myself wishing for more complete and robust refactoring support. Our team was using the RubyMine IDE by JetBrains, and the limitations of the automated refactoring tools were particularly noticeable, especially compared to those offered in IntelliJ IDEA —another JetBrains IDE, primarily used for Java development.
I didn’t want to just accept these limitations, so as an exercise, I started keeping a daily log of refactorings I would have used during my work, if only they were available[2]:
Eventually, this led me to develop RubyRefactorings, a RubyMine plugin that would provide me with the automated refactorings I wanted to use while working on that project. My development notes helped me ensure I was focusing on real-world use cases drawn from my own experience.
The plugin is available on GitHub. It implements the refactoring transformations as code intentions, a term used on JetBrains IDEs to refer to contextual suggestions that appear when focusing on a particular expression in the code.
Some of the implemented refactorings
The plugin provides many refactorings (though several from my original list remain unimplemented). I’ll highlight some of them below.
Introduce interpolation
This is a simple refactoring that automates a transformation that’s very cumbersome to perform manually: interpolating parts of an existing string literal. This is especially useful as a preliminary step before extracting parts of the string as variables, or when adding new dynamic contents to a string.
The animation below demonstrates some examples and edge cases[3]:
Replace conditional with guard clause
This refactoring is useful when you want to separate error or special cases from the main flow of the program, while also reducing conditional nesting[4]:
Some cases require special handling, such as when working inside a block:
Extract method object
This refactoring —also known in the literature as “replace function with command”— moves a method’s logic to a new object:
Introduce chained map
This refactoring splits a map
, collect
, or each
by introducing a chained map
. You can use it to separate different steps used to process a collection, and it’s usually followed by an application of the “extract method” refactoring.
It is important to note that this preserves the behavior of the program only when f
and g
have no side effects, or when the order of their side effects is not relevant. That is the reason why the intention has the additional “may change semantics” warning, following the conventions of the IDE.
Other refactorings
Some additional automated refactorings implemented by the tool are:
- Convert single-quoted string to double-quoted (remarkably not built into RubyMine).
- Remove useless conditional statement.
- Move statement into conditional above.
- Use self-assignment.
Ruby-specific refactorings include:
- Replace singleton method by opening singleton class.
- Remove unnecessary braces from hash argument.
- Convert string/symbol word list to use array syntax.
Some technical details
The plugin is implemented in Scala using JetBrains’ Platform SDK. It was developed following strict Test-Driven Development practices. The test suite verifies both the detection of refactoring opportunities and the correctness of the transformations.
Much thought was given to make the tests readable by distilling their essential elements. As an example, here’s a test case for the “split map” refactoring:
@Test
def splitsTheMapWhenThePartitionsShareMoreThanOneVariable(): Unit = {
loadRubyFileWith(
"""
|[1, 2, 3].<caret>map do |n|
| x = n + 1
| y = n + 2
| z = x + y
|end
""")
applySplitRefactor(splitPoint = "y = n + 2")
expectResultingCodeToBe(
"""
|[1, 2, 3].map do |n|
| x = n + 1
| y = n + 2
| [x, y]
|end.map do |x, y|
| z = x + y
|end
""")
}
Conclusions
Through this project, I’ve learned that developing even small improvements for the tools we use can significantly impact our experience: reflecting on our work improves our capacity to envision new tools, and creating new tools deepens our knowledge of the craft. As a bonus, by making these refactorings available through intuitive interfaces, we help developers maintain cleaner, more maintainable code with less effort.
The term “refactoring” was popularized by Martin Fowler. From refactoring.com:
↩︎Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior.
At the time, I was reading The Reflective Practitioner by Donald A. Schön. This was one of the sources of inspiration for that reflective practice made “in action”, which includes recognizing something as it happens and making tacit knowledge (e.g. identifying and isolating a transformation as a refactoring) explicit. ↩︎
Even a simple refactoring like this one has many edge cases. I call it “simple” because it only requires local information to be performed. ↩︎
One could argue that it reduces the explicit nesting of conditionals, since in some way the conditions are still “nested”: the conditions below depend on the conditions above not having matched. ↩︎