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.

ELab Team
ELab Team
ELab Team
Mastering ESLint: How AST Powers Code Linting and Custom Rules

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.json

ESLint 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.js

AST

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-eslint

Generated 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.

frontendJavaScriptASTcode styleESLintlintingCustom Rules
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.