Automated refactorings for Ruby

✒️ DescriptionA RubyMine IDE plugin that implements various automated code refactorings for Ruby, developed based on real-world usage patterns.
⚡ TechnologiesScala, JetBrains Platform SDK, Ruby
🔌 DownloadJetBrains Marketplace
🗂️ Source CodeGitHub 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]:

A notebook page showing my handwritten notes with automated refactorings ideas.

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]: Animation showing the "introduce interpolation" refactoring; the main example goes from "hello, world" to "hello, #{"world"}".

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]:

Animation showing an example of "replace conditional with guard clause". One of the examples goes from 'if is_retired; retired_amount; else normal_pay_amount; end' to 'return retired_amount if is_retired; normal_pay_amount'

Some cases require special handling, such as when working inside a block: Animation showing a special case of "replace conditional with guard clause": when the conditional is inside a block it uses 'next' instead of 'return' to preserve the behavior of the code

Extract method object

This refactoring —also known in the literature as “replace function with command”— moves a method’s logic to a new object:

Animation showing the "extract method object" refactoring. It is applied to a method and generates a new class, such that the original method parameters are received in the constructor, and the logic is delegated to a new instance of that class. It also allows the user to rename the new class and message

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.

Animation showing an example of "introduce chained map". The example converts a single '.map do', such that the result is obtained by first applying f and then g, to a chain of two '.map do's: one applying f and the other applying g; allowing the user to choose the split point. The title of the code intention item includes an aside: "may change semantics".

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:

scala
@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.


  1. 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.

    ↩︎
  2. 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. ↩︎

  3. Even a simple refactoring like this one has many edge cases. I call it “simple” because it only requires local information to be performed. ↩︎

  4. 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. ↩︎