Why?
As one of the Rails projects I've been working grew in size in terms of the number of lines of code as well as the number of people contributing code, it became challenging to maintain consistency in code quality and style.
Many times these issues were brought up in code reviews where they should be addressed well before that so the discussion in code reviews can focus on substance and not the style.
So having said that, I wanted to setup an automated way of fixing stylistic issues when the code is checked in. Here's how I went about setting that up.
In my previous blog post, I talked about Incorporating Modern Javascript Build Tools With Rails, if you have something similar setup already, you can skip the next two sections about setting up a NodeJS environment. If not, read on.
NodeJS
Before we can get started, let’s make sure we have NodeJS installed. I’ve found the easiest way to do so is via nvm. Rails developers will find this very similar to rvm. To install, run the following commands which will install nvm and the latest version of NodeJS:
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash
$ source ~/.bash_profile
$ nvm install node
$ nvm use node
Yarn
Next we'll need a package manager. Traditionally we'd use npm but I've found Facebook's yarn to be a lot more stable and reliable to work with. This is very similar to bundler. To install on Debian Linux, run the following commands or follow their installation guide for your OS:
$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
$ sudo apt-get update && sudo apt-get install yarn
Git Hook Management
Now in order to format the code automatically upon check-in, we will have to first figure out how to run those linting scripts. There are a couple of options:
1) Bash Script - You can manually save a bash script as .git/hooks/pre-commit
and give it execute permission. Downside of this approach is that you'd have to have every member of your team do this manually. Also if something changes in the script, everyone would have to repeat the process all over again. It would quickly become unmanageable.
2) pre-commit - This is a very robust framework built in Python to manage git hooks. I really like everything about it except the fact that for RoR projects, it adds another language dependency on the local environment in addition to Ruby and NodeJS. Also again this is something the entire team would have to install manually (albeit only once per environment) to get it up and running. I would definitely recommend it for a Python project.
3) overcommit (Recommended) - This is another excellent git hook manager very similar to pre-commit but written in Ruby. It has a ton of built-in hooks for formatting Ruby, JS, CSS and more. It's virtually plugin and play and perfect for a project if it doesn't have a NodeJS build pipeline setup. It will help you avoid introducing another language dependency. Although for the purpose of this blog post, we'll use the next option. I'll recommend checking out this blog post if you want to use this option.
4) husky & lint-staged (Recommended) - These two NodeJS packages act as a one-two punch. Husky lets you specify any script that you want to run against git hooks right in the package.json while lint-staged makes it possible to run arbitrary npm and shell tasks with a list of staged files as an argument, filtered by a specified glob pattern on pre-commit. The best part, once this is setup, your team doesn't have to do a thing other than run yarn install
.
To get started install both the packages:
yarn add lint-staged husky --dev
Next add a hook for precommit in your package.json:
{
"scripts": {
"precommit": "lint-staged"
},
}
Lastly create an empty .lintstagedrc
file at the root, this is where we'll integrate with the various linters we'll talk about next.
JavaScript
So now we are ready to actually hookup some linters. For JavaScript, there are several good linting frameworks out there ranging from very opinionated to very flexible:
1) StandardJS - This is the most opinionated framework out there and also very popular. It has excellent IDE integration and used by a lot of big names. Although having said that, we didn't agree with some of it's rules and there was no way of changing them. It is really designed to be a an install-it-and-forget-it kind of a linter which wasn't quite what I was looking for.
2) Prettier - So that lead me to investigate another very popular framework. Prettier is a lot like StandardJS, good IDE support, well adopted. It tries to provide little more flexibility over a few basic rules compared to StandardJS. Although it's main advantage over StandardJS is the fact that it is also able to lint CSS and GraphQL in additional to JavaScript and it's preprocessors.
3) ESLint (Recommended) - After trying both of the above mentioned linters, I ended up settling with ESLint primarily for the fact that it let us tweak all the options exactly per our needs. The flexibility and extensibility of this framework is impressive.
So let's go ahead and install it:
yarn install eslint --dev
Next you'll want to run through setup and answer some questions about your preferences
./node_modules/.bin/eslint --init
Based on your responses, it will create an .eslintrc
file in your project which you can always manually edit later. Here's one that I'm using:
env:
browser: true
commonjs: true
es6: true
extends: 'eslint:recommended'
parserOptions:
sourceType: module
rules:
indent:
- warn
- 2
linebreak-style:
- warn
- unix
quotes:
- warn
- single
semi:
- warn
- always
no-undef:
- off
no-unused-vars:
- warn
no-console:
- off
no-empty:
- warn
no-cond-assign:
- warn
no-redeclare:
- warn
no-useless-escape:
- warn
no-irregular-whitespace:
- warn
I went with setting most of the rules as non-blocking warnings since we were dealing with some legacy code and wanted to reduce developer friction as much as possible.
Finally add this line to your .lintstagedrc
{
"*.js": ["eslint --fix", "git add"]
}
Ruby
When it came to Ruby linting, there is really just one game in town i.e RuboCop. All you need to do is add it to the Gemfile
and run bundle install
:
gem 'rubocop', require: false
Next add a hook for it in your .lintstagedrc
:
{
"*.js": ["eslint --fix", "git add"],
"*.rb": ["rubocop -a -c .rubocop-linter.yml --fail-level E", "git add"],
}
Next you will need to create .rubocop-linter.yml
with your coniguration. Here's one that we used:
AllCops:
Exclude:
- 'vendor/**/*'
- 'spec/factories/**/*'
- 'tmp/**/*'
TargetRubyVersion: 2.2
Style/Encoding:
EnforcedStyle: when_needed
Enabled: true
Style/FrozenStringLiteralComment:
EnforcedStyle: always
Metrics/LineLength:
Max: 200
Metrics/ClassLength:
Enabled: false
IndentationConsistency:
EnforcedStyle: rails
Documentation:
Enabled: false
Style/ConditionalAssignment:
Enabled: false
Style/LambdaCall:
Enabled: false
Metrics:
Enabled: false
Also here's a list of all the auto corrections RuboCop is able to do when the -a
/ --auto-correct
flag is turned on if you need to add/change any more rules in that file.
CSS/SCSS
So now that we have Ruby and JS linting squared away. Let's look into how to do the same with CSS.
1) sass-lint - Since we were using SASS in the project, I first looked at this package. Although quickly realized there was no option for auto fixing available at the moment. There is a PR which is currently in the works that is supposed to add this feature at some point. But for now we'll have to look somewhere else.
2) stylelint (Recommended) - Ended up going with this option because of its large ruleset (150 at the time of writing) and the fact that it is powered by PostCSS which understands any syntax that PostCSS can parse, including SCSS, SugarSS, and Less. Only downside being the fact that auto fixing feature is experimental but it's worth a shot anyway.
So let's go ahead and install it:
yarn add stylelint --dev
Next add a hook for it in your .lintstagedrc
:
{
"*.js": ["eslint --fix", "git add"],
"*.rb": ["rubocop -a -c .rubocop-linter.yml --fail-level E", "git add"],
"*.scss": ["stylelint --fix", "git add"]
}
Again this is a very configurable package with a lot of options which you can manage in a .stylelintrc
file.
To being with, I'd probably just recommend extending either stylelint-config-standard or stylelint-config-recommended presets.
Here's an example of a .stylelintrc
:
{
"extends": "stylelint-config-standard",
"rules": {
/* exceptions to the rule go here */
}
}
HAML
As far as templating engine goes, our project uses HAML but unfortunately I couldn't find any auto formatting solution for it. haml-lint has an open ticket for adding this feature but it seems like it's not very easy to implement.
So until then you have the option to just hook up the linter so it can provide feedback about your markup which you would have to manually correct.
To get started, add the gem to your Gemfile
:
gem 'haml_lint', require: false
Next add a hook for it in your .lintstagedrc
:
{
"*.js": ["eslint --fix", "git add"],
"*.rb": ["rubocop -a -c .rubocop-linter.yml --fail-level E", "git add"],
"*.scss": ["stylelint --fix", "git add"]
"*.haml": ["haml-lint -c .haml-lint.yml", "git add"],
}
Next you will need to create .haml-lint.yml
with your configuration. Here's one that you can use:
# Whether to ignore frontmatter at the beginning of HAML documents for
# frameworks such as Jekyll/Middleman
skip_frontmatter: false
linters:
AltText:
enabled: false
ClassAttributeWithStaticValue:
enabled: true
ClassesBeforeIds:
enabled: true
ConsecutiveComments:
enabled: true
ConsecutiveSilentScripts:
enabled: true
max_consecutive: 2
EmptyScript:
enabled: true
HtmlAttributes:
enabled: true
ImplicitDiv:
enabled: true
LeadingCommentSpace:
enabled: true
LineLength:
enabled: false
MultilinePipe:
enabled: true
MultilineScript:
enabled: true
ObjectReferenceAttributes:
enabled: true
RuboCop:
enabled: false
RubyComments:
enabled: true
SpaceBeforeScript:
enabled: true
SpaceInsideHashAttributes:
enabled: true
style: space
TagName:
enabled: true
TrailingWhitespace:
enabled: true
UnnecessaryInterpolation:
enabled: true
UnnecessaryStringOutput:
enabled: true
Optionally, you can also exclude all the existing HAML files with linting issues by running the following command and including the exclusions file (inherits_from: .haml-lint_todo.yml
) in the configuration file above to ease the on-boarding process:
haml-lint --auto-gen-config
Conclusion
That's all folks! In a few weeks since hooking up the auto formatters our codebase has started to look much more uniform upon every commit. Code reviews can now focus on more important feedback.
This post was originally published on my blog. If you liked this post, please share it on social media and follow me on Twitter!
Top comments (2)
good article. In terms of ruby, I actually find Reek to be more useful. It critiques OO style. It overlaps rubocop a bit, but I find both have their uses.
Right on, didn't think reek had the ability to auto-fix issues which is primarily what I was looking for.