Custom ESLint rules #3/5: creating a new rule

Photo by Glen Carrie on Unsplash

Custom ESLint rules #3/5: creating a new rule

Where things starts getting interesting.

To save our teammates from eating pineapple pizza, we decided to create a custom ESLint plugin with a dedicated rule. In the previous bite, we set up a sample monorepo and now it's time to create our rule!

How to write a custom ESLint rule

At this point, we need to define our custom ESLint ruleset. We will focus on eslint-plugin-pizza folder.

We have to decide our rule name. According to naming conventions, we should use something like no-pineapple-pizza. Let's go for it and create all the relevant files.

yummy-pizza $ touch packages/eslint-plugin-pizza/rules/no-pineapple-pizza.js packages/eslint-plugin-pizza/index.js

This is what we have:

yummy-pizza/
├─ packages/
│  ├─ eslint-plugin-pizza/
│  │  ├─ rules/
│  │  │  ├─ no-pineapple-pizza.js
│  │  ├─ package.json
│  │  ├─ index.js
│  ├─ app/
│  │  ├─ src/
│  │  ├─ package.json
├─ package.json
  • rules/no-pineapple-pizza.js is our rule

  • index.js is where our rules are exported

It is time to write our rule! To create it, my suggestion is to take advantage of the Abstract Syntax Tree Explorer tool and understand what is the best way to match our pineapple code. Also, we need to provide some metadata to our rule. For the detailed list of metadata to use, refer to the official docs.

In our example, we want to match the eatPineapplePizza function. If we feed AST Explorer with a sample code, it will tell us how to build our rule. Place the cursor upon the incriminated text and see what is highlighted on the right-hand side. For a way more detailed guide on how to use AST, you can check this blog post that I found extremely helpful.

So after playing a bit with it, I ended up with the following code:

// yummy-pizza/packages/eslint-plugin-pizza/rule/no-pineapple-pizza.js

const rule = {
  meta: {
    type: "problem", // our rule is detecting an error
    fixable: "code", // our rule can be automatically fixed
    docs: {
      description: "Don't eat pineapple pizza.", // short rule description
      category: "Pizza ruleset",
    },
  },
  create: function (context) {
    return {
      // this code is generated using AST
      CallExpression(node) {
        if (
          node.callee.type === "Identifier" &&
          node.callee.name === "eatPineapplePizza"
        ) {
          context.report({
            node,
            // message to display to pineapple pizza eaters
            message:
              "A notification was sent to the Italian police. They're coming. Pineapple pizza is illegal, please eat pizza Margherita instead",
            fix(fixer) {
            // we provide an automatic fix: replace 'usePineapplePizza' with 'usePizzaMargherita'
              return fixer.replaceText(node.callee, "eatPizzaMargherita");
            },
          });
        }
      },
    };
  },
};

export default rule;

Finally, edit index.js file to export the new rule.

// yummy-pizza/packages/eslint-plugin-pizza/index.js

import noPineapplePizzaRule from "./rules/no-pineapple-pizza"

const plugin = {
  rules: {
    "no-pineapple-pizza": noPineapplePizzaRule,
  },
};

export default plugin;

Our rule is ready!

In the next bite, we'll learn how to test it to ensure it works properly.

References