Mastering ESLint: How AST Powers Code Linting and Custom Rules
This article explores the fundamentals of ESLint, explaining how it leverages abstract syntax trees (AST) to enforce code style, detect errors, and enable custom rule creation, while providing practical configuration examples, code snippets, and insights into the underlying processing pipeline.
Preface
Code is written for humans, so good code should be understandable by readers of varying skill levels. Inconsistent formatting—different indentation, naming conventions, etc.—reduces readability, so teams need a way to enforce a unified style.
Automation is a key driver of productivity; linting automation tools like ESLint, TSLint, and StyleLint help standardize code style and catch potential errors.
What is ESLint/Lint?
ESLint consists of two parts: the npm package that provides linting rules and the IDE plugin (e.g., VSCode) that reads those rules and highlights issues in real time.
The npm package contains the actual rule definitions and formatting logic, while the VSCode plugin points to the project's /node_modules/eslint (or a global installation) to apply those rules during editing, such as on file save.
In short, ESLint rules constrain code style and flag potential errors by being imported into a project via the npm package and enforced by the IDE plugin.
How to Use it?
Most scaffolds initialize ESLint for you. The main configuration file ( .eslintrc) can be in various formats; the priority order is:
.eslintrc.js .eslintrc.yaml .eslintrc.yml .eslintrc.json .eslintrc package.jsonESLint also supports per‑directory overrides, useful for monorepos where each sub‑project may have slightly different rules.
{
"extends": "",
"root": "true",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"globalReturn": true,
"impliedStrict": true,
"jsx": true
}
},
"parse": "espree" | "esprima" | "Babel-ESLint" | "@typescript-eslint/parser",
"plugins": ["a-plugin"],
"processor": "a-plugin/a-processor",
"rules": {"eqeqeq": "error"},
"globals": {"var1": "writable", "var2": "readonly"},
"ignorePatterns": ["src/**/*.test.ts", "src/frontend/generated/*"]
}Directory layout example for overriding rules:
packages
├── package.json
├── .eslintrc.js
├── lib
│ └── test.js
└─┬ app
├── .eslintrc.js
└── test.jsAST
ESLint works on an abstract syntax tree (AST) generated by the parser Espree (or @typescript-eslint/parser for TypeScript). The AST represents the source code structure, enabling inspection and modification without transforming the code.
Parsing
Using @typescript-eslint/parser we can parse TypeScript files into an AST. Example:
const foo = "anthony"
const bar = "dst" import fs from 'fs';
import path from 'path';
import * as tsParser from '@typescript-eslint/parser';
const filePath = path.resolve('./src/test.ts');
const text = fs.readFileSync(filePath, "utf8");
const ast = tsParser.parse(text, {
comment: true,
ecmaVersion: 6,
loc: true,
range: true,
tokens: true
});The AST contains node locations, tokens, comments, etc., allowing precise text replacements (e.g., changing const to let based on node positions).
SourceCode
ESLint wraps the original text and its AST in a SourceCode object, which provides utilities such as getText, isSpaceBetweenTokens, and token/line information.
Rule Template
ESLint offers a Yeoman generator to scaffold rule templates:
npm install -g yo generator-eslintGenerated rule skeleton (simplified):
"use strict";
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: { description: "...", recommended: false, url: null },
messages: { temp: 'Do not use literals as function arguments', novar: 'Do not use var', noExport: 'Export on exit' },
fixable: 'code',
schema: []
},
create(context) {
const sourceCode = context.getSourceCode();
return {
ArrowFunctionExpression(node) { /* ... */ },
"Program:exit"(node) { context.report({ node, messageId: "noExport" }); },
VariableDeclaration(node) { if (node.kind === 'var') { context.report({ node, messageId: 'novar', fix(fixer) { const varToken = sourceCode.getFirstToken(node); return fixer.replaceText(varToken, 'let'); } }); } }
};
}
};Key Functions
The context object supplies report and fix methods, enabling rule authors to flag problems and provide automatic fixes.
Overall Process
When linting, ESLint traverses the AST using Traverser.traverse, invoking visitor methods on entry and exit. This follows the Visitor pattern, allowing rules to react to specific node types.
Rules are compiled into listeners, which subscribe to traversal events. During the node queue walk, listeners execute their logic based on whether the traversal is entering or leaving a node.
Conclusion
Although this article was written quickly and may contain omissions, it provides a foundation for understanding ESLint’s internals and building custom plugins.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
