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.