Why and How to Lint like a PRO

Luka Samkharadze
TBC Engineering

--

ESlint + Stylelint + Prettier + Husky + Lint-Staged === 💅🏻

One day while fixing some bugs, I discovered that we had a different indentation on scss and ts files. So I got up and tried to long-term fix it. Now I am going to share what approach we use at TBC and explain step by step how we got here.

First I tried looking through the web on how to the fix issues like this and found many outdated solutions, so I had to spend some time figuring out the best one. We will be using up-to-date tools and explain in-depth how to integrate each one of them into our project.

Photo by The Creative Exchange on Unsplash

Let’s imagine we have Jimmy’s pull request that’s adding the following code into the codebase:

class Person {
constructor(private name,private height) {
}
getInfo(){
return `${this.name} ${this.calculate(this.height)}`
}
calculate(height)
{
var tmp = 5;
return this.height*100
}
private test() {}
}

What if this pull request was 500 lines of code. What do we do? Do we comment on each formatting issue? Do we suggest removing all unused functions? Or do we leave it for a later refactoring task?

In the formatting part, the popular opinionated code formatter Prettier comes to the rescue. So let's start with it.

Prettier’s simple definition is that it makes your team not worry about if you need a space between the brackets of an empty object {} 🤔

Space, or not space, that is the question…

Simply install it via npm i -D prettier . We need -D to mark that we’re going to use this package only for development purposes.

Now if we want to, we can create a config file .prettierrc.js at the root of our application to specify custom configurations how we want the code to look like.

Let's specify printWidth (Prettier will try it’s best to keep the length of lines close to this number) to be 100 so It doesn’t break everything in half with new lines:

module.exports = {
printWidth: 100,
singleQuote: true,

};

For more configuration options see this.

(Note that specifying your options is not as good as it seems. In fact, it’s not recommended. The main purpose of opinionated code formatter is to give you guidelines and developers across the world to have 1 and only standard)

Now running prettier . --write will format your entire repository 🥳

For simpler usage, we can integrate Prettier into our IDEs. For VS Code simply install esbenp.prettier-vscode. In settings set default formatter to Prettier and use a shortcut alt shift f or enable format on save.

(We will talk about Webstorm plugins at the end)

Let’s see what Jimmy’s pull requests looks like:

class Person {
constructor(private name,private height) {
}
getInfo(){
return `${this.name} ${this.calculate(this.height)}`
}
calculate(height)
{
var tmp = 5;
return this.height*100
}
private test() {}
}

What? 🤯

Looks like he forgot to manually format code and also he did not enable format on save or whole another reason might be that his favorite IDE doesn’t support Prettier.

There has to be another way!

Command prettier . --write works everywhere, so we have to somehow call it before commits.

Husky’s main goal is to simplify a complex topic, git hooks, and enable us to run specific commands on certain events. 🕗

Run npm i -D husky and followed with npx husky install for setting up git hooks. After someone checks out to the branch with Husky installed, everyone needs to run npx husky install. To automate this, we can use npm set-script prepare husky install . The latter command ensures that git hooks are automatically set up after any npm i.

(Note that npm set-script only works with npm@^7, you can do this manually by setting “prepare” the script in package.json to “husky install”)

Now it’s actually time to enforce formatted code on each commit. Run this to add our pre-commit hook:

npx husky add .husky/pre-commit “npx prettier . --write”

That’s it, but… 😕

Houston, we have a problem…

First of all, if we try to commit unformatted code next things happen:

  1. Every file is formatted (Jimmy has a quite weak computer so he doesn't like Prettier crawling over the entire project again and again)
  2. Staged unformatted code gets actually committed and new formatted code by Prettier appears as new changes, which we have to commit again…

So how we gonna fix those issues...

Lint-Staged exactly does what we wanted, it calls commands on staged files only and adds formatted code to the commit. 👀

You might have guessed → npm i -D lint-staged. Then we add lint-staged field in package.json, which specifies what to call on which staged files, in our case: “Call prettier --write on every staged file”

"lint-staged": {
"*": "prettier --write"
}

Now we edit our old script in .husky/pre-commit and set it to:

npx lint-staged

That’s fine and dandy, let’s try committing unformatted code….

It really works…

class Person {
constructor(private name, private height) {}
getInfo() {
return `${this.name} ${this.calculate(this.height)}`;
}
calculate(height) {
var tmp = 5;
return height * 100;
}
private test() {}
}

Newer Jimmy’s pull request seems pretty good, but we have a long way to go. Who’s gonna add a new line between calculate and getInfo or who’s gonna fix some logic-based issues in our code, like that unused function and v̷a̸r̴?

ESLint is a static code analysis tool, that helps us write much better and readable code. 👾

Yes yes, you have heard that right. We’re gonna actually use ESlint for typescript. For this quite a few packages are needed…

npm i -D eslint

And now for tssupport

npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

We also need to create and configure .eslintrc.js It will look something like this. We need to use override to only lint ts files

Now after installing dbaeumer.vscode-eslint VSCode extension you will get little red squiggles on ESlint errors.

You can also try Requiring Type Checking for catching more advanced issues that require type information. Without installing any packages just add:

...
extends: {
...
'plugin:@typescript-eslint/recommended-requiring-type-checking'
}

(Note that the rule above might affect your IDE performance)

For stricter rules let's take a look at Airbnb Javascript Rules. Install rules via npm i -D eslint-config-airbnb-typescript eslint-plugin-importand add it in extends array.

...
extends: [
'airbnb-typescript/base',
...
]

(Note that this config is not final, there can be some rules which may not apply to us, for example, noplusplus or prefer default import in angular project. You may also look into lines between class members and see if it fits your needs)

...
rules: {
'import/prefer-default-export': 'off',
'no-plusplus': 'off',
}

Don’t be scared if you get peerDependency warnings after installing this package. This is a known problem.

Unfortunately, there can be formatting conflicts between Prettier and ESLint.

So, to fix this we can extend plugin:prettier/recommended and install eslint-config-prettier and eslint-plugin-prettier. The latter one enables prettier and the first one disables the conflicting rules.

(Note that you need to have plugin:prettier/recommended as the last element of the extends array, so It overrides any rule it needs to override)

extends: [
...
'plugin:prettier/recommended'
],

Our final and more advanced ESlint may look like this:

(Note that we have used plugin:prettier/recommended at 2 places, the first one is for js files. Also, note that the default severity of Prettier is error, I have changed it to warn so we don't get distracted with red squiggles while writing code)

Now that we integrated Prettier into ESlint, we can change the lint-staged script to:

"lint-staged": {
"*.{js,ts}": "eslint --fix"
}

(Note that we are now linting only js and ts files, we will get back to this later)

One might wonder what Jimmy’s pull request looks like right now…

class Person {
constructor(private name: string, private height: number) {}
getInfo(): string {
return `${this.name} ${Person.calculate(this.height)}`;
}
static calculate(height: number): number {
return height * 100;
}
}

Noice

(Note he had to make calculate static, because the method was not using the class’s state. Rules like this make us write semantically correct code. If you don’t like any of the rules you can turn them off simply by using rules object in the config file)

Angular

You can also install framework-specific ESLint plugins like Angular-ESLint. For Angular ready ESLint config file see this gist

Stylelint is the same as ESLint, but for style files. Do you want to see less confusing selectors and no more nested classes with 2 or more depth? Do you also want to have ordered properties and want to standardize between #FFF, #fff andwhite? 😎

.first[disabled]:first-child { // <-- Confusing selector
color: red;
...
:hover:nth-child(2 - 3n)#shiish { // <-- More Confusing selector
...
}
color: white; // <-- Duplicate styles
}

You are in the right hands 🤠👍

Start with npm i -D stylelint stylelint-config-standard

For SCSS stylelint-config-sass-guidelines

and for Prettier npm i -D stylelint-prettier stylelint-config-prettier

Your .stylelintrc.js should look this likes

module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-sass-guidelines',
'stylelint-prettier/recommended',
],
};

(Note that we have stylelint-config-sass-guidelines second in place, so It can override standard-config)

After installing stylelint.vscode-stylelint VSCode extension you should get this kind of errors:

If you think this is a little bit strict for style files, you can change Stylelint’s severity to warning

module.exports = {
defaultSeverity: 'warning',
...

If you are using a framework that heavily depends on pseudo-classes, like :host in Angular, you might also like looking into increasing max-nesting-depth, ignoring pseudo-classes, or bumping this Github issue ignorePseudoClasses: []

...
rules: {
'max-nesting-depth': [
1,
{
ignore: ['pseudo-classes']
},
],
},

All we have to do is to add Stylelint into lint-staged:

"lint-staged": {
"*.{js,ts}": "eslint --fix",
"*.{css,scss}": "stylelint --fix"
}

Since we no longer use Prettier in lint-staged, we don't get formatted html and json files, so we have to add this line:

"*.{html,json}": "prettier --write"

If you are trying to integrate these tools into an already existing code base, these changes might be handy:

Use comments to ignore specific files or code sections for ESLint and Stylelint.

You can also configure ESlint to lint specific directories only, but use Prettier for everything (You can slowly add more directories to files array) or change the severity of common problems to warn like we did on the entire Stylelint.

Webstrom already has these IDE plugins built-in and you just need to enable them.

Prettier:

Languages & Frameworks → Javascript → Prettier

Stylelint:

Languages & Frameworks → Style Sheets → Stylelint

ESLint:

Languages & Frameworks → Javascript → Code Quality Tools → ESLint

That’s it…

"husky": "^6.0.0",
"prettier": "^2.3.0",
"lint-staged": "^11.0.0",
"eslint": "^7.27.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-prettier": "^3.4.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-airbnb-typescript": "^12.3.1",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"stylelint": "^13.13.1",
"stylelint-prettier": "^1.2.0"
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-config-sass-guidelines": "^8.0.0"

All these packages just for linting? 🤯

Yep, having a good codebase comes with its price.

If you have any questions or spot any mistakes don’t hesitate to comment below.

--

--