How ESLint Parses ASTs to Enforce Code Quality

ESLint maintains code quality and consistency in JavaScript projects by converting plain source text into an Abstract Syntax Tree (AST), traversing its nodes, and evaluating rules against specific code patterns. This article explains the end-to-end process of how ESLint parses source code, utilizes the Visitor pattern to inspect syntax tree nodes, reports rule violations, and applies automated fixes.

1. Code Tokenization and Parsing

The linting process begins by feeding raw JavaScript source code into a parser. By default, ESLint uses Espree, a parser built on Acorn that complies with the ESTree specification.

Parsing occurs in two primary phases: * Lexical Analysis (Tokenization): The source code string is scanned and broken down into a sequence of tokens (such as keywords, identifiers, operators, and literals), discarding meaningless whitespace and comments. * Syntactic Analysis (Tree Construction): The parser analyzes the token sequence against JavaScript grammatical rules to construct an Abstract Syntax Tree (AST).

An AST is a hierarchical, deeply nested JSON-like object representing the syntactic structure of the program. For example, a simple declaration like const total = 10; is transformed into a VariableDeclaration node containing a VariableDeclarator node, which in turn holds an Identifier (name: "total") and a Literal (value: 10). Each node also stores metadata, including its start and end line/column coordinates in the original file.

2. AST Traversal and the Visitor Pattern

Once the AST is built, ESLint traverses the tree using the Visitor pattern. It performs a depth-first search (DFS) traversal, visiting every node from the root down to the leaf nodes.

During traversal, ESLint emits events for each node type: * Enter: Triggered when the traverser first reaches a node. * Exit: Triggered after the traverser has visited all child nodes of that specific node.

Rules declare interest in specific node types by subscribing to these events using selector patterns similar to CSS selectors (e.g., FunctionDeclaration, BinaryExpression[operator="=="], or CallExpression:exit).

3. Rule Execution and Context Matching

ESLint rules are modular JavaScript functions that return an object mapping AST selectors to listener functions.

When ESLint’s traverser encounters a node that matches a rule’s selector, it executes the associated function, passing the current AST node and a context object. The rule then inspects the properties of the node:

module.exports = {
  meta: {
    type: "problem",
    docs: { description: "Disallow use of eval()" }
  },
  create(context) {
    return {
      CallExpression(node) {
        if (node.callee.name === "eval") {
          context.report({
            node,
            message: "Do not use eval()."
          });
        }
      }
    };
  }
};

If the node’s properties violate the criteria defined by the rule, the rule calls context.report(). This registers a linting problem associated with the precise line and column coordinates stored within the AST node.

4. Scope Analysis and Code Fixing

Beyond basic syntax checks, ESLint performs advanced analysis directly on top of the AST: * Scope Analysis: ESLint tracks variable creation, references, and closures using eslint-scope. This allows rules to detect unused variables, shadow variables, or out-of-scope references. * Automated Fixing: When calling context.report(), rules can provide a fix function. The fixer uses the node’s exact location offsets (range array) to insert, remove, or replace text directly within the original source code without breaking the rest of the file structure.

By decoupling syntax parsing from rule execution, ESLint allows custom parsers (like @typescript-eslint/parser or @babel/eslint-parser) and custom plugins to enforce complex style and safety guidelines across diverse codebases.