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 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 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:
# 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!
# 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.
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.
- 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.
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)
{
- 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.
# 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,
)