What is a JavaScript linter: inside out view

01 Dec 2021

A "linter", or a static code analysis tool, is used to detect potential (or actual) errors by analysing the code itself (and not executing it).

The term was first used as a name of a utility to examine code in C language. You can find a short but rather entertaining history of it on Wikipedia.

This article is not going to dive into all details on how existing tools work, rather focusing on a big picture here. However, we'll use ESLint as an example of a linter architecture. Let's take a look at how ESLint works.

Architecture of ESLint

If you take a look at the ESLint architecture, you'll find several prominent parts of it (CLI-related things aside for now):

  • api.js — exposes a very limited set of public classes.
  • linter — the core that does the verification based on configuration. It parses the source code into an AST and executes rules. It does not perform any I/O operations itself.
  • rules — responsible for inspecting the AST for certain patterns and reporing errors and warnings.

AST (absract syntax tree) is a tree representation of a source code.

If we look at major dependencies, ESLint uses:

  • Espree, that was a fork of Esprima v1.2.2, and then was re-written using Acorn. It has some extra features on top of Acorn.
  • Estraverse to walk the tree.

An interesting (and somewhat expected) fact: The ESTree Spec is used in these tools. Acorn clearly states that in their documentation. The ESTree Spec Steering Committee consists of people from ESLint, Acorn, and Babel.

From this, we can already see several important things:

  • modularity;
  • single responsibility principle;
  • use of compatibility layers;
  • separate business logic from the presentation layer(s).

Let's see if we can apply the same principles when working on our task.

Our task

The task for today is to implement two main components—a linter and a rule (similar to ESLint rules)—in a simple way. That is, not to create a full-blown linter of our own, but to understand the basic principles behind this class of tools. We will use the Acorn parser and walker to work with the AST. No reach features, no performance optimisations, no state.

On the practical side, let's say that we want to create a linting rule that would prohibit us from using cy.wait in our Cypress tests.

You can use the examples in this article. Some of them are going to be interactive. But if you want to follow along, you'll need to bootstrap a TypeScript project yourself. You'll also need to yarn add acorn acorn-walk.

Parsing

This is an example of using the Acorn parser:

Now, let's apply the parser to an example source code. Below, you can see the code itself and the corresponding AST. The code in this example is editable. For example, if you add const a = 1 in the beginning, you'll see a VariableDeclaration popping up on the right.

Walking the AST

Next thing. We need to learn to traverse the tree and find the nodes corresponding to the patterns we are looking for. We'll use a utility called acorn-walk for that and catch all CallExpressions (remember, looking for cy.waits). We will also apply some very basic filtering to the list of CallExpression nodes.

Namely, we will only select the nodes which have a callee of type MemberExpression with both object and property defined. This describes the pattern we are looking for: <object>.<property>(...) (e.g. cy.wait(...)). Therefore, describe and it calls from the example source code are skipped.

Let's see how that works using the same example source code. Use the arrows to navigate through the matched call expressions.

(Note that some properties on the nodes of the AST are filtered to ease the reading here.)

The linter

The walkAst code from the previous section now feels a bit smelly. The traversing function should not care about the specific nodes a given rule is looking for. Let's enforce this separation by introducing a contract: the "Linter" is going to handle the traversal, and a "Rule" is going to be responsible for choosing nodes and checking them. We'll also introduce a concept of "Context" which can be used by the rules to report errors.

The rule

The rule factory definition then becomes pretty simple too. A rule factory should:

  • define the node type in operates upon;
  • return the rule itself;
  • the rule will do the check and report an error (if found).

Applying the new linter

With the refactoring we've made, configuring and applying the linter is very straightforward:

In the example below, you can edit the source code and see what happens. Hover over a line number to see an error message from the linter. (If there's a syntax error, it will show a generic message.)

That's it for today! If you didn't know how linters and similar tools worked, I hope these examples helped you to get the gist of it.

About

This blog contains things around software engineering that I consider worth sharing or at least noting down. I will be happy if you find any of these notes helpful. Always feel free to give me feedback at: kk [at] kirill-k.pro.

KK © 2025