Understanding ESLint: Principles, Configuration, AST, and Custom Rule Development
This article explains the fundamentals of ESLint, including its purpose, configuration files, AST-based analysis, the SourceCode abstraction, rule template creation, key functions, and the overall linting and fixing workflow, enabling developers to build custom linting plugins for consistent code style.
Good code should be readable by developers of varying skill levels, and inconsistent formatting (different indent sizes, naming conventions, etc.) can hinder readability; therefore, teams need automated tools to enforce a unified code style.
ESLint, along with tools like TSLint and StyleLint, provides such automation for front‑end projects. The ESLint npm package contains the linting rules and formatter, while IDE plugins (e.g., the VSCode ESLint extension) read those rules from node_modules/eslint and highlight issues directly in the editor.
Configuration is typically handled by an .eslintrc file. ESLint supports several formats, with the following priority order: .eslintrc.js , .eslintrc.yaml , .eslintrc.yml , .eslintrc.json , .eslintrc , and package.json . Projects can also override parent configurations in monorepos by placing specific rc files in sub‑directories.
ESLint operates on an Abstract Syntax Tree (AST). By default it uses the Espree parser, but for TypeScript projects the @typescript-eslint/parser is often preferred. Parsing source code into an AST enables ESLint to inspect and modify code without performing full code transformation.
After parsing, ESLint creates a SourceCode instance that stores the original text and the AST, providing utilities such as getText , isSpaceBetweenTokens , and token/comment collections, which simplify rule implementation.
Custom rules can be scaffolded with Yeoman via npm install -g yo generator-eslint . A typical rule module looks like:
"use strict";
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: { description: "xxxx", recommended: false, url: null },
messages: { temp: '不要用字面量作为函数的参数传入', novar: '不要用 var 声明', noExport: '退出时执行这个' },
fixable: 'code',
schema: []
},
create(context) {
const sourceCode = context.getSourceCode();
return {
ArrowFunctionExpression(node) { /* rule logic */ },
"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'); } }); } }
};
}
};The rule’s create function receives a context object, which offers report to flag violations and a fix callback to provide automatic fixes using token ranges.
The core linting process runs runRules in linter.js . It traverses the AST with Traverser.traverse , invoking visitor callbacks on entry and exit of each node. This visitor pattern, combined with an event‑emitter style subscription, allows each rule’s listener to react to relevant nodes.
During traversal, ESLint collects problems into a lintingProblem array, each containing location, message, and fix information. After traversal, the collected fixes are applied by simple string slicing and concatenation.
In summary, the article outlines how ESLint parses code into an AST, wraps it in a SourceCode object, provides a flexible configuration system, and enables developers to write custom rules that can automatically fix style violations, thereby improving team productivity and code consistency.
ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
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.