How to create your own Eslint rule with tests, boosting the DX, and code-review
Tags: eslint, javascript, dx, testing
Published: 2024-03-27 18:40:00



# Table of contents:
- <a href="#focus-on-what-matters">Focus on what matters <a/>
- <a href="#how-to-create-eslint-rule"> How to create your own Eslint rule <a/>
- <a href="#fixable-rule"> Fixable rule <a/>
- <a href="#eslint-tests"> How to create tests  <a/>
- <a href="#conclusion"> Conclusion  <a/>
- <a href="#references"> References  <a/>

Code review is an essential step in every good software development process. At this step, your team can learn, discuss, and detect problems related to code or business logic. A good code review requires attention to detail from the reviewer, and this is where I started to think more about [DX (Developer Experience)](https://microsoft.github.io/code-with-engineering-playbook/developer-experience/) and how to improve the code review to focus on what matters.

<h2 id="focus-on-what-matters"> Focus on what matters <h2/>
I want to remove as many noises as possible during the development process and code review, allowing the developer to focus on what matters. One of the ways to do it is to create a good set of lint rules for your project. It can force a specific code pattern, organize the code, and remove the responsibility from the developer to keep things clear.

Having a team with a good sense of ownership that works on refactoring and improving the DX is crucial.

For each repetitive mistake during the code review should have a pull-request similar to this one:

![image with 4125 insertions and 6151 code deletion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/chxxll0u736oejvof7vf.png)

<h2 id="how-to-create-eslint-rule"> How to create your own Eslint rule <h2/>

### Setup the project

You can start by creating a new folder containing your eslint package and another project to test your eslint package.

```bash
~ mkdir my-eslint-rule
~ cd my-eslint-rule
~ mkdir package
~ cd package
```

Inside the `package` folder, start a npm project
```bash
npm init
```

The project's name should contain the prefix `eslint-plugin`, like `eslint-plugin-my-custom-rule`.

## Creating the rule

The rule should detect the code we want to transform, show an error or alert about the problem, and be auto-fixable.

The rule I'll create for this tutorial will look for `console.log` calls and refactor it to `console.info().`

```javascript
const moveConsoleLogToInfo = (context) => {
  return {
    CallExpression: (node) => {
      if (
        node.callee.type === "MemberExpression" &&
        node.callee.object.name === "console" &&
        node.callee.property.name === "log"
      ) {
        context.report({
          node,
          message: "Use console.info instead of console.log",
        });
      }
    },
  };
};

module.exports = { moveConsoleLogToInfo };
```

To understand this syntax, I recommend exploring [AST Explorer](https://astexplorer.net/). You will have a better view of how the AST of JavaScript works and how to correlate it with the Eslint syntax:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/any63qwevf6eqmgxjkx0.png)

And then, we can import it into our `index.js` file.

```javascript
const { moveConsoleLogToInfo } = require("./move-console-log-to-info");

module.exports = {
  rules: {
    "move-console-log-to-info": {
      create: moveConsoleLogToInfo,
    },
  },
};

```

At this point, we're ready to install our new eslint rule on our test project.

Outside the `package` folder, initiate another npm project.
```bash
npm init
```

To install our package, we can tell NPM the package directory. In our case, it will be: 
```bash
npm install --save-dev ./package
``` 

And now you can see your package in `package.json`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uuwevxgkcel3v9fzt9pu.png)

And then, install lint:
```bash
npm install -D eslint
```

Create `.eslintrc.js`:
```javascript
module.exports = {
  env: {
    node: true,
  },
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: "module",
  },
  extends: ["eslint:recommended"],
  plugins: ["my-custom-rule"],
  rules: {
    "my-custom-rule/move-console-log-to-info": "error"
  },
};
```

Back to  your test file, and type `console.log` to see the magic:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8h09541z62b81256sfrj.png)

But our rule cannot be fixed by itself. Let's make it possible.

<h2 id="fixable-rule"> Fixable rule <h2/>

First, back to `package/index.js`:
```javascript
const { moveConsoleLogToInfo } = require("./move-console-log-to-info");

module.exports = {
  rules: {
    "move-console-log-to-info": {
      defaultOptions: [],
      meta: {
        type: "problem",
        messages: {
          defaultMessage: "Move console.log to logger.info",
        },
        fixable: "code",
        schema: [],
      },
      create: moveConsoleLogToInfo,
    },
  },
};

```

and `move-console-log-to-info.js`:
```javascript
const moveConsoleLogToInfo = (context) => {
  return {
    CallExpression: (node) => {
      if (
        node.callee.type === "MemberExpression" &&
        node.callee.object.name === "console" &&
        node.callee.property.name === "log"
      ) {
        context.report({
          node,
          message: "Use console.info instead of console.log",
          fix: (fixer) => fixer.replaceText(node.callee.property, "info"),
        });
      }
    },
  };
};

module.exports = { moveConsoleLogToInfo };

```

Now, install the package again and restart the eslint server:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9qmectq31f572hmv53qo.png)

And it will keep the content inside the `log()`

<h2 id="eslint-tests"> How to create tests <h2/> 

Having tests for your rule will help ensure that your package does not introduce any regression. We'll use Eslint and Jest for this.

Eslint provides RuleTester and Jest because it runs without additional configuration and works well with valid and invalid RuleTester syntax.

### Test folder

I won't discuss the folder architecture you'll use to organize your tests. You can check out how I organized a real-world Eslint rule [here](https://github.com/vit0rr/eslint-meteor-error).

You must follow the name file pattern `name.spec.js` because Jest will look for .spec files.

```javascript
// move-console-log-to-info.spec.js


const { RuleTester } = require("eslint");
const { rules } = require("../index");

const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });

ruleTester.run("move-console-log-to-info", rules["move-console-log-to-info"], {
  valid: [
    {
      code: 'console.info("My own Eslint rule is working!")',
    },
  ],
  invalid: [
    {
      code: 'console.log("My own Eslint rule is working!")',
      errors: [
        {
          message: "Use console.info instead of console.log",
          type: "CallExpression",
        },
      ],
      output: 'console.info("My own Eslint rule is working!")',
    },
  ],
});

```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4wxk5fmzi9f381j8qpxw.png)

<h2 id="conclusion"> Conclusion <h2/>

Now you understand why it's important to care about DX, how to use Eslint to make developers focus on what matters with a custom Eslint rule, and how to create automated tests to ensure your rule keeps working well.

---

If you liked what you saw today, please react to this post and do not forget to follow me on my socials:

[X(Formerly Twitter)](https://twitter.com/vitorsalmeida_)
[Linkedin](https://www.linkedin.com/in/vitorsalmeida/)


<h2 id="references"> References <h2/>

- https://eslint.org/docs/latest/integrate/nodejs-api
- https://astexplorer.net/
- https://jestjs.io/docs/getting-started