The raise of minimum-churn style in Ruby
Tags: ruby, rubocop, style, churn
Published: 2021-10-08 01:32:50

---
title: The raise of minimum-churn style in Ruby
published: true
description: An exploration of style conventions that reduce noise in diffs
tags: Ruby, Rubocop, Style, Churn
cover_image: https://i.postimg.cc/fWmnf4vs/diff.png
---

## TL;DR
* Add trailing commas everywhere
* Use consistent indentation
* Disallow whitespace for formatting/aligning
* Consider leading dot syntax, YMMW

## The story
For me it all started with the [announcement](https://rubygems.org/gems/standard/versions/1.0.0) of version 1.0 release of the `standard` gem sometime back in March, 2021. Here was a library by the accomplished @searls that pre-configured Rubocop for the benefit of all mankind, and I was eager to imbibe. Perusing the readme, I immediately noticed the "__Leading dots on multi-line method chains__" point and opened the [thread](https://github.com/testdouble/standard/issues/75) that lead to that decision.  
The first argument for adopting the particular approach was that it would reduce the lines highlighted by git as changed if a new call was added to the end of a chain:

```rb
# trailing dot, noisy
  upcase.
  chars.
- drop(1)
+ drop(1).
+ reverse

# leading dot, quiet
  .upcase
  .chars
  .drop(1)
+ .reverse
```

I had never really appreciated how last lines in chains like this, and, similarly, last lines in hashes and arrays are made different by the style used.
I had been burned by the occasional missing comma in a hash or array, but always figured aesthetics demand no comma at the end; now there was a good argument, even two, for breaking with aesthetics and instead adopting an approach that favours consistency and results in fewer mistakes and less noise!

```rb
# no final comma, inconsistent, very noisy
 {
   name: "Bob",
   faves: [
     :apples,
-    :beer
+    :beer,
+    :candy    
-   ]
+   ],
+  dislikes: [:radishes]  
 }

# final comma, very quiet
 {
   name: "Bob",
   faves: [
     :apples,
     :beer,    
+    :candy, 
   ],
+  dislikes: [:radishes],  
 }
```

The same goes for multiline argument lists. Basically in any multiline list where individual entries would be comma-separated, keep adding trailing comma.

```rb
 data.dig(
   :properties,
   0,
   :rooms,
   -1,
-  :size
+  :size,
+  :square_meters
 )

 data.dig(
   :properties,
   0,
   :rooms,
   -1,
   :size,
+  :square_meters,
 )
```

Now I was sold. Looking to minimise diff noise was a valid and powerful way to drive style decisions!
I looked through some of my recent commits to see what else produces noise and discovered a surprising fact - whitespace was the number one culprit for noise. It results from whitespace being used for alignment, thus tying several lines together, and sometimes change in one line demands changes in all. Extreme noise ensues.

An example with often-seen style where arbitrary leading whitespace is allowed (this sometimes even produces odd-numbered space indent, yuck!). Only the class name is changed, but it's impossible to easily tell. Good style setup should constrain any line to being __at most__ one level deeper than the previous line.
```rb
- SomeClass.where(vip: true)
-          .where(local: false)
-          .where(name: "Jack", 
-                 title: "mr",)   
+ SomeOtherClass.where(vip: true)
+               .where(local: false)
+               .where(name: "Jack", 
+                      title: "mr",) 

# contrast with consistent indentation approach
- SomeClass
+ SomeOtherClass
   .where(vip: true)
   .where(local: false)
   .where(
     name: "Jack", 
     title: "mr",
    )   
```

An example with often-seen arbitrary options hash alignment. Having consistent indentation and requiring that options hash starts on its own line also improves readability as to where positional args end and kwargs/options begin.
```rb
 FactoryBot.build_stubbed(
-  :some_model, :full_build, vip: true
-                            best: true
+  :some_model, :no_build, vip: true
+                          best: true
 )

# contrast with consistent indentation
 FactoryBot.build_stubbed(
-  :some_model, :full_build,
+  :some_model, :no_build,
   vip: true
   best: true
 )
```

And an example with aligned hashes, what got changed? (hint, a new longest key appeared)
```rb
 {
-   the_longest_key: "value",
-   middle_key:      "value",
-   mini_key:        "value",
+   the_longeest_key: "value",
+   the_longest_key:  "value",
+   middle_key:       "value",
+   mini_key:         "value",
 }

# contrast with disallowed whitespace for alignment
 {
+  the_longeest_key: "value",
   the_longest_key: "value",
   middle_key: "value",
   mini_key: "value",  
 }
```

## The cops
This is not an exhaustive list, created with Rubocop v1.22 in mind. The settings provided should reduce whitespace abuses and add commas everywhere.

```
Layout/ArgumentAlignment:
  EnforcedStyle: with_fixed_indentation

Layout/ArrayAlignment:
  EnforcedStyle: with_fixed_indentation

Layout/ExtraSpacing:  
  Enabled: true  
  AllowForAlignment: false  
  AllowBeforeTrailingComments: true 
  ForceEqualSignAlignment: false

Layout/ParameterAlignment:
  EnforcedStyle: with_fixed_indentation

Layout/SpaceAroundOperators:
  AllowForAlignment: false
  EnforcedStyleForExponentOperator: no_space  

Style/TrailingCommaInArguments:  
  EnforcedStyleForMultiline: consistent_comma

Style/TrailingCommaInArrayLiteral: 
  EnforcedStyleForMultiline: consistent_comma

Style/TrailingCommaInHashLiteral:  
  EnforcedStyleForMultiline: consistent_comma

Layout/DotPosition:
  EnforcedStyle: leading  
```

## Addendum
You may also look at `Layout/First*` cops. Using them with `EnforcedStyle: consistent` complements alignment cops.

```rb
# with only alignment
some_method(:arg1,
  :arg2,
  :arg3,
)

other_method(arg, key1: :value1,
  key2: :value2,
)

# with First* cops
some_method(
  :arg1,
  :arg2,
  :arg3,
)

other_method(
  arg,
  key1: :value1,
  key2: :value2,
)
```