Deep Dive into ESLint: How Rules, CLI, and AST Processing Work

This article explains why ESLint is essential for frontend development, details how its rule configuration works, walks through the CLI execution flow, and reveals the internal call stack that parses code into an AST, applies custom rules, and performs auto‑fixes.

ELab Team
ELab Team
ELab Team
Deep Dive into ESLint: How Rules, CLI, and AST Processing Work

ESLint Overview

In frontend development, ESLint is essential for enforcing code style and preventing bugs.

1. ESLint Rules

Rules are configured with numeric levels 0 (off), 1 (warn), 2 (error). Example configuration:

{
  "rules": {
    "arrow-body-style": 0,
    "quotes": ["error", "single"]
  }
}

Each rule key corresponds to a specific check.

ESLint Core Rules

Understanding a rule’s structure (meta and create) is key to writing custom rules.

Example of a simple custom rule no-with:

module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "disallow `with` statements",
      recommended: true,
      url: "https://eslint.org/docs/rules/no-with"
    },
    messages: {
      unexpectedWith: "Unexpected use of 'with' statement."
    }
  },
  create(context) {
    return {
      WithStatement(node) {
        context.report({ node, messageId: "unexpectedWith" });
      }
    };
  }
};

A rule consists of meta (metadata) and create (function that returns visitor methods).

2. ESLint CLI Execution

Define a bin entry in package.json and create the executable file.

"bin": {
  "eslint": "bin/eslint.js"
}
#!/usr/bin/env node
console.log("console.log output")

The main CLI function parses arguments, creates a CLIEngine instance, and runs engine.executeOnFiles or engine.executeOnText.

execute(args, text) {
  // parse options
  const engine = new CLIEngine(translateOptions(currentOptions));
  const report = useStdin
    ? engine.executeOnText(text, currentOptions.stdinFilename, true)
    : engine.executeOnFiles(files);
  return 0;
}

3. Execution Call Stack

The execute flow leads to engine.executeOnFiles, which iterates files, reads content, and calls verifyText.

executeOnFiles(patterns) {
  for (const { config, filePath, ignored } of fileEnumerator.iterateFiles(patterns)) {
    const result = verifyText({ /* ... */ });
    results.push(result);
  }
}
verifyText

invokes verifyAndFix, which repeatedly applies fixes until no more changes or a maximum of ten passes.

do {
  messages = this.verify(currentText, config, options);
  fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);
  if (messages.length === 1 && messages[0].fatal) break;
  fixed = fixed || fixedResult.fixed;
  currentText = fixedResult.output;
} while (fixedResult.fixed && passNumber < MAX_AUTOFIX_PASSES);

During verification, the code is parsed into an AST (default parser espree), which can be replaced (e.g., @typescript-eslint/parser).

const espree = require("espree");
let parserName = DEFAULT_PARSER_NAME; // 'espree'
let parser = espree;

4. Rule Execution Engine

The engine creates an event emitter, traverses the AST, registers rule listeners, and emits events for matching selectors.

const emitter = createEmitter();
Traverser.traverse(sourceCode.ast, {
  enter(node, parent) { node.parent = parent; nodeQueue.push({ isEntering: true, node }); },
  leave(node) { nodeQueue.push({ isEntering: false, node }); },
  visitorKeys: sourceCode.visitorKeys
});
Object.keys(configuredRules).forEach(ruleId => {
  const rule = ruleMapper(ruleId);
  const ruleContext = { /* ... */ };
  const ruleListeners = createRuleListeners(rule, ruleContext);
  Object.keys(ruleListeners).forEach(selector => {
    emitter.on(selector, addRuleErrorHandler(ruleListeners[selector]));
  });
});

The emitter triggers visitor callbacks during a second pass over nodeQueue, allowing rules to report problems.

5. Overall Mechanism

ESLint walks the AST, fires events at specific nodes or traversal moments, and rules can report errors or auto‑fix code.

Tips

Replace the default parser when ESLint cannot understand TypeScript syntax:

{
  "parser": "@typescript-eslint/parser"
}

The article provides a comprehensive view of ESLint’s internal workflow, useful for developers who want to extend or customize linting behavior.

CLIASTcode qualityESLintlintingCustom 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.