How to implement custom ESLint rules in your codebase
Maintaining a consistent codebase is every engineer’s dream. To achieve this, one brute-force method is to look through every pull request being merged and find issues yourself. However, this is too much manual work and could frustrate the reviewer and the pull request author. What if it could be automated?
Hello everyone, I am Suyash Patil, working as Member of Technical Staff here at Fyle. In this blog, I will discuss how you can set up ESLint rules in your Angular application locally. So, let’s get started on this.
Initialize Angular Application
The first thing to do is install Angular CLI on your system.
You can use the command npm install -g @angular/cli
.
You can check the version by running ng version
. Now create an Angular application by running the command ng new <app_name>
.
This will install all the required dependencies of eslint
and @typescript-eslint
.
The dependency we are looking for is typescript-eslint/parser. This package will make our TypeScript code ESLint compatible.
A Little Bit About Abstract Syntax Tree
To create custom rules, you need to understand how to work with AST.
An Abstract Syntax Tree (AST) is a tree representation of the structure of your code. ESLint uses it to traverse through the codebase and apply rules on each node of the tree, which helps in identifying problematic pieces of code. You can use tools like AST Explorer to understand how nodes are traversed. We will leverage this tool to write our own ESLint rule based on our requirements.
Long story short, we have unit tests written in Jasmine in our mobile application.
There was a slight inconsistency in our test code: in some places, we used toBe(true)
, in few places we had toEqual(true)
, and in the rest, we had toBeTrue()
.
PR reviewers would have a hard time going through each line and leaving comments to use toBeTrue()
to maintain consistency in the codebase. Similar was the case for toBe(null)
and toBe(false)
.
We will see how we can write a custom ESLint rule to not only give a linting error but also fix the code automatically.
Create a custom rule
The first step in configuring local plugins is to create a new directory in our project root. Let's name it eslint-custom-rules
. In this directory, we will create an index.js
file which will serve as the entry point for all our ESLint rules. In your package.json file, add this specific line in devDependencies
.
"eslint-plugin-custom-rules": "file:eslint-custom-rules"
This indicates that the dependency eslint-plugin-custom-rules is being referenced from a local file path rather than being fetched from a remote registry like npm. Now we will create a file named eslint-plugin-prefer-to-be-true.js which contains all our custom rule code.
// eslint-plugin-prefer-to-be-true.js
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Enforce using toBeTrue(), toBeFalse() and toBeNull() instead of toBe() or toEqual()",
category: "Best Practices",
recommended: true,
},
fixable: "code",
// Indicates that the rule can automatically fix the code
schema: [],
},
create: function (context) {
return {
CallExpression(node) {
const isToBe = node.callee.property &&
node.callee.property.name === "toBe";
const isToEqual = node.callee.property &&
node.callee.property.name === "toEqual";
/*
* Check if the function called is
* toBe() or toEqual() and it has exactly one argument
*/
if ((isToBe || isToEqual) && node.arguments.length === 1) {
const argValue = node.arguments[0].value;
// Check if the argument value is true
if (argValue === true) {
context.report({
node,
message: "Prefer using toBeTrue() instead of toBe(true) or toEqual(true)",
fix: function (fixer) {
const replacementText = 'toBeTrue()';
const sourceCode = context.getSourceCode();
const nodeText = sourceCode.getText(node);
// Find the starting of the node
const start = node.callee.property.range[0];
const end = node.callee.property.range[1] + 6;
/*
* We are adding 6 here as we have to shift
* the end position of fixer after `(true)`
* which is six character
*/
return fixer.replaceTextRange(
[start, end],
replacementText
);
}
});
}
}
}
};
}
};
The meta
object consists of the description of the rule. Once you specify the fixable
property, you can auto fix lint errors by using the --fix
flag.
In this rule, we are listening to all the call expressions and checking if any of the calls are toBe()
or toEqual()
and have an argument value of true
, false
, or null
.
If yes, we will replace the text with toBeTrue()
, toBeFalse()
, or toBeNull()
accordingly.
Add the Custom Rule to Your Project
Save the ESLint rule with the file name prefixed with eslint-plugin-prefer-to-be-true
in a directory inside eslint-custom-rules
. You can give this any name, just make sure to give it the prefix eslint-plugin
. Now, in the index.js file, import this rule:
module.exports = {
rules: {
'prefer-to-be-true': require('./rules/eslint-plugin-prefer-to-be-true')
},
};
In your .eslintrc.json file, add custom-rules as plugins so that ESLint knows about the local rules.
Add the rule prefer-to-be-true
in the rules section specifying the file type as *.spec.ts. This will ensure the rule runs only for files ending with the extension .spec.ts
{
"files": ["*.spec.ts"],
"parserOptions": {
"project": ["tsconfig.json", "e2e/tsconfig.json"],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/ng-cli-compat"
/*
* the ng-cli-compat package is used for transition from
* TSLint (which is deprecated) to ESLint in Angular projects
* that use the Angular CLI.
*/
],
"rules": {
"custom-rules/prefer-to-be-true": "error"
}
}
Automating the Fix
By setting up an ESLint workflow in GitHub Actions, you can catch linting issues whenever a PR is raised. This way, reviewers don’t need to manually find errors and leave comments.
Conclusion
You can leverage the fixable
property specified and fix the ESLint issue automatically without manual intervention. Just run npx eslint src/app/app.component.spec.ts --fix
and it will automatically fix the linting issues. You can create more rules like this and achieve automation in code quality.