Creating a simple ESLint plugin to glance at AST

In JavaScript world, ESLint is the most popular linter.
In this article, I’ll explain how ESLint analyzes our code by creating an ESLint plugin.

How ESLint works?

You might already know how it works.
But first, let me show you how ESLint works.

Let’s say we have a project like this. And ESLint has been installed via npm.

├──.eslintrc.json // ESLint configuration
├── node_modules
├── package-lock.json
├── package.json
└── src
    └── index.js

You can use ESLint to src/index.js like this

npx eslint src

As the result, an error was shown.

/Users/xxx/foo/src/index.js
  1:7  error  'foo' is assigned a value but never used  no-unused-vars

This is the code inside the src/index.js.

const foo = () => "baz"

foo is defined but it’s not called from anywhere. That’s why the error happened.

This is the basic usage of ESLint.
From the next section, i'll create a plugin to see how ESLint detects such errors.

What’s a plugin?

The purpose of using plugins is to add additional rules.
ESLint has lots of rules. no-unused-vars that we saw above is one of the rules.
But if you want to apply an additional rule that is not available in the official rule, you can add it by creating a plugin. (There are other several ways)

But how ESLint analyze source code to check if the source code aligns with rules or not?

AST(Abstract Syntax Tree)

ESLint uses AST (Abstract Syntax Tree) to analyze JavaScript code.What’s AST?

AST represents source code structure.For example, AST of const hoge = 0; is like this. (You can see AST easily with AST Explorer)

{
  "type": "File",
  "start": 0,
  "end": 16,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 2,
      "column": 0
    }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 16,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 2,
        "column": 0
      }
    },
    "sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 0,
        "end": 15,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 1,
            "column": 15
          }
        },
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 6,
            "end": 14,
            "loc": {
              "start": {
                "line": 1,
                "column": 6
              },
              "end": {
                "line": 1,
                "column": 14
              }
            },
            "id": {
              "type": "Identifier",
              "start": 6,
              "end": 10,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 6
                },
                "end": {
                  "line": 1,
                  "column": 10
                },
                "identifierName": "hoge"
              },
              "name": "hoge"
            },
            "init": {
              "type": "NumericLiteral",
              "start": 13,
              "end": 14,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 13
                },
                "end": {
                  "line": 1,
                  "column": 14
                }
              },
              "extra": {
                "rawValue": 0,
                "raw": "0"
              },
              "value": 0
            }
          }
        ],
        "kind": "const"
      }
    ],
    "directives": []
  },
  "comments": []
}

By using AST, we can get detailed information about the source code.It’s convenient to analyze source code rather than analyze plain text.

Creating a plugin

And the structure is simple like this.

├── lib
│   └── index.js
├── package.json

This plugin finds the string TODO in comment blocks and complains about it.

Let’s say you have the below code in your project

// Todo: will change variable name later
let hoge = 0;

ESLint shows an error like this

/Users/xxx/eslint-test/src/index.js
   1:1  error  Later becomes never  later-becomes-never/no-todo

You can see the rule no-todo which comes from later-becomes-never plugin.

To define rules for the plugin, using AST is necessary.
The definition of the rule is in lib/index.js.

module.exports = {
  rules: {
    "no-todo": {
      create: function (context) {
        return {
          Program: (node) => {
            node.comments.forEach((comment) => {
              if (comment.value.toLowerCase().indexOf("todo") !== -1) {
                context.report({
                  loc: comment.loc,
                  message: "Later becomes never",
                });
              }
            });
          },
        };
      },
    },
  },
};

In the file, a rule named “no-todo“ is defined.We can manipulate AST by using node.Comments are extracted from the source code by using AST. (Line7)

After that, I checked if each comment has a string todo or not. (Line8)
If it has, an error message is shown (Line9 - 12)

Using the plugin

Let’s use ESLint with the plugin to the below JavaScript.Line 1 is supposed to have an error. Line 3 is not.

// Todo: will change variable name later
let hoge = 0;
// doit

This is the result. It works :)

/Users/xxx/eslint-test/index.js
  1:1  error  Later becomes never  later-becomes-never/no-todo
1 problem (1 error, 0 warnings)

15