DEV Community

Cover image for ESLint Understand By Doing Part 1: Abstract Syntax Trees
about14sheep
about14sheep

Posted on • Updated on

ESLint Understand By Doing Part 1: Abstract Syntax Trees

Like everything in software development, its best to learn by doing. In this blog series we will go over what is happening, under the hood, when you lint your JavaScript files by writing our very own custom eslint rule!

Setting the stage

Like many I am always searching for answers to life's greatest questions. Also like many, I can narrow these questions down to the three most elusive:

  1. What happens when we die?
  2. Do aliens exist?
  3. How does ESLint work?

Due to recent advances in modern sciences we can finally begin to understand the most important of these questions: How does ESLint work?

TL:DR

I have no idea what I am talking about.

Abstract Syntax Trees

When you run ESLint, the first thing it does is create an Abstract Syntax Tree (AST) of your code. Basically an AST is a description of your codes syntax. A tool can use this description to evaluate your codes structure.

ESLint is not the only tool that uses ASTs. The TypeScript parser creates an AST in its own format to check for incomplete code, type checking, and if you have used the any type implicitly (hehe).

For this blog we will focus on how ESLint uses an AST to understand your code.

Learn by doing

The best way to understand what an AST is.. is to simply play with them over on AST Explorer. I like to switch the right side panel to json as I find it easier to follow, but you do you.

You can follow along with this example:

// Simply setting a variable `foo` to a string 'bar'
const foo = 'bar';
Enter fullscreen mode Exit fullscreen mode

ESLint's AST format, ESTree, would represent this line of code as:

{
  "type": "Program",
  "start": 0,
  "end": 16,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 16,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 15,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 9,
            "name": "foo"
          },
          "init": {
            "type": "Identifier",
            "start": 12,
            "end": 15,
            "name": "bar"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}
Enter fullscreen mode Exit fullscreen mode

You can gather a lot of information from this. To help we can break it down, then build it back up while we describe whats going on.

The Root

{
  "type": "Program",
  "start": 0,
  "end": 16,
  "body": [
    // Not important right now
  ],
  "sourceType": "module"
}
Enter fullscreen mode Exit fullscreen mode

The root of this object has a type of 'Program'. This is standard and encapsulates the the rest of the program code. It shows what byte the program code starts and ends, in the file, as well as describes the type of structure your file has, like whether it is a commonJS file or a ES module file.

The Body

{
  "type": "Program",
  "start": 0,
  "end": 16,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 16,
      "declarations": [
        // Not important right now
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}
Enter fullscreen mode Exit fullscreen mode

In the body of the program you have an array of objects. This is where everything you write lives. Since we are only declaring a variable in our program, there is only one object whos type is of "VariableDeclaration". If we were to add a function to the file, a new object would pop up in the body array (specifically it would be a "FunctionDeclaration" object).

Looking back at the "VariableDeclaration" object you will notice it has the familiar start and end, as well the kind of variable it is (its a constant).

The Meat

{
  "type": "Program",
  "start": 0,
  "end": 16,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 16,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 15,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 9,
            "name": "foo"
          },
          "init": {
            "type": "Identifier",
            "start": 12,
            "end": 15,
            "name": "bar"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}
Enter fullscreen mode Exit fullscreen mode

Now to bring it all back by adding the meat, the "declarations" property. Inside of this property is where the meat is. The id object is where the name of our variable is housed. Likewise, the init property is where the value our variable was initialized too is house. Each of these nested objects also has a type property and their own start and end properties. Can you see a pattern?

That's all an AST is. It simply describes what and where the individual pieces of your code are. This is the map ESLint uses to run its rules off of.

In part 2 of this series we will show how ESLint does this by creating rules of our own.

Top comments (0)