DEV Community

Cover image for How to create your own Eslint rule with tests, boosting the DX, and code-review
Vitor for Meteor

Posted on

How to create your own Eslint rule with tests, boosting the DX, and code-review

Table of contents:

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) and how to improve the code review to focus on what matters.

Focus on what matters


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

How to create your own Eslint rule

Setup the project

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

~ mkdir my-eslint-rule
~ cd my-eslint-rule
~ mkdir package
~ cd package
Enter fullscreen mode Exit fullscreen mode

Inside the package folder, start a npm project

npm init
Enter fullscreen mode Exit fullscreen mode

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().

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 };
Enter fullscreen mode Exit fullscreen mode

To understand this syntax, I recommend exploring AST Explorer. You will have a better view of how the AST of JavaScript works and how to correlate it with the Eslint syntax:

Image description

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

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

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

Enter fullscreen mode Exit fullscreen mode

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

Outside the package folder, initiate another npm project.

npm init
Enter fullscreen mode Exit fullscreen mode

To install our package, we can tell NPM the package directory. In our case, it will be:

npm install --save-dev ./package
Enter fullscreen mode Exit fullscreen mode

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

Image description

And then, install lint:

npm install -D eslint
Enter fullscreen mode Exit fullscreen mode

Create .eslintrc.js:

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"
  },
};
Enter fullscreen mode Exit fullscreen mode

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

Image description

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

Fixable rule

First, back to package/index.js:

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,
    },
  },
};

Enter fullscreen mode Exit fullscreen mode

and move-console-log-to-info.js:

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 };

Enter fullscreen mode Exit fullscreen mode

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

Image description

And it will keep the content inside the log()

How to create tests

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.

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

// 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!")',
    },
  ],
});

Enter fullscreen mode Exit fullscreen mode

Image description

Conclusion

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)
Linkedin

References

Top comments (0)