ESLint Isn't Magic: How ASTs Turn Code Style Rules Into Machine-Readable Checks
ESLint Code Standards Complete Guide — From AST Principles to Flat Config Line-by-Line Analysis
Abstract: The readme labels
eslintas a "code style standard," but these five words hide a whole mechanism. This article covers everything from "why ESLint is needed" to its underlying AST principles, and then breaks down a realeslint.config.mjsandpackage.jsonline by line, so you can not only understand the configuration but also write your own rules.
1. Why ESLint is Needed: Code Style Standards
When a team collaborates, if everyone has different habits for indentation, quotes, and semicolons, the code becomes increasingly messy and harder to maintain. Worse, some coding patterns are pitfalls themselves (like var's hoisting, ASI traps from missing semicolons, or console.log leaking debug info into production).
What ESLint does is: lock down "style" and "quality" with rules. Code that doesn't satisfy the rules directly reports errors or warnings. So the readme's definition is accurate—code style standard.
ESLint has a special trait: it only checks, it doesn't change your code (unless you add --fix). It acts like a "code reviewer," pointing out issues and leaving the decision to you.
2. What ESLint Is: A Static Code Analysis Tool
ESLint is a Linter that performs static analysis—without running your code, it finds problems and style deviations just by "reading the source code."
An analogy:
- Dynamic analysis: Run the code and see if it crashes (like testing).
- Static analysis: Without running a single line, just by looking at the text and structure, it can point out "
varshouldn't be used here" or "missing semicolon here" (like a Code Review).
Because it doesn't run the code, it's fast, can be integrated into editors for real-time hints (you mistype a semicolon, the editor immediately marks it red), and can act as a checkpoint before commits.
3. Underlying Principle: AST (Abstract Syntax Tree)
This is the key to understanding ESLint's foundation. After ESLint receives your source code, the first step it takes is parsing—turning a string of code into an Abstract Syntax Tree (AST).
For example, this line of code:
const a = 1
is parsed into a structured tree (simplified representation):
VariableDeclaration
└─ VariableDeclarator
├─ Identifier: a
└─ Literal: 1
With this tree, ESLint can traverse every node and apply rules to each node. For instance, the no-var rule works like this: during traversal, if it finds a node of type VariableDeclaration using var, it reports an error.
So the essence of every ESLint rule is: "Find a node of a certain shape in the AST → determine if it violates the rule → report". Once you understand AST, you understand why ESLint can "read" code—it sees structure, not text.
Source code string ──parse──► AST (Abstract Syntax Tree) ──traverse + apply rules──► Error/Warning list
4. Rule Severity Levels: 2 / 1 / 0
This is the key point emphasized in the eslint.config.mjs comments:
Level 2=error 1=warn warning 0 off
Each rule can be assigned a "level" that determines what happens on violation:
| Level | Numeric | String | Meaning |
|---|---|---|---|
| Error | 2 |
"error" |
Violation reports an error, usually blocks commit |
| Warn | 1 |
"warn" |
Hint, but does not block |
| Off | 0 |
"off" |
Ignore this rule |
Numbers and strings are completely equivalent; numbers save characters. So "no-var": 2 and "no-var": "error" mean the same thing.
5. Evolution of Configuration Format: Flat Config
Looking at the configuration file extension reveals a generational shift:
- Old format:
.eslintrc.json/.eslintrc.js—layered nested configuration, poor readability, cumbersome to extend. - New format:
eslint.config.mjs—flat config, a flat array where configuration items are laid out flat, clear and direct.
The .mjs suffix indicates this file uses ES modules (import/export), not CommonJS.
6. Line-by-Line Analysis of eslint.config.mjs
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.browser },
rules: {
// Level 2=error 1=warn warning 0 off
"no-var": 2, // cannot use var
"no-console": 1, // use during development, not in production
"quotes": ["error", "double"],
"semi": ["error", "always"],
"indent": ["error", 2] // indent 2 spaces
}
},
{ files: ["**/*.js"], languageOptions: { sourceType: "script" } },
tseslint.configs.recommended,
]);
Understanding block by block:
import js from "@eslint/js": The official rule set.@eslint/jsprovides a set of universal recommended rules, the basis forextends.import globals from "globals": A definition package for global variables. ESLint doesn't recognize browser globals likewindowordocumentby default; this package declares them.import tseslint from "typescript-eslint": Allows ESLint to understand TypeScript syntax (ESLint natively only understands JS).import { defineConfig } from "eslint/config": A helper function provided by ESLint, its purpose is to add type hints to the configuration—if a field is wrong, the editor immediately marks it red.
Now look at the array inside defineConfig([...]), where flat config's "flatness" manifests: each item is an independent configuration object, applied sequentially from top to bottom.
First item (core configuration):
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"]: A glob pattern declaring "these rules apply to which files"—covering JS, TS, and various variants.**matches any directory level.plugins: { js }+extends: ["js/recommended"]: The new flat config way—register the plugin first, then inherit itsrecommendedpreset.languageOptions: { globals: globals.browser }: Declares browser global variables. Without this declaration, ESLint would falsely reportwindowaswindow is not defined.
Second item:
{ files: ["**/*.js"], languageOptions: { sourceType: "script" } }: Specifically targets.jsfiles, declaring they should be parsed as traditional scripts (CommonJS) (therequiresystem), echoing"type": "commonjs"inpackage.json.sourceTypedetermines whether a file is treated as a "module" or a "script".
Third item:
tseslint.configs.recommended: TypeScript's recommended rule set, directly flattened into the array, allowing ESLint to also check TS code.
7. Line-by-Line Analysis of Rules (Key Focus)
rules is the "soul" of the configuration, each corresponding to a requirement in the comments:
"no-var": 2 — Cannot use var
var has historical baggage like hoisting and redeclaration; modern code uses let/const. Level 2 (error), violation directly reports an error. This is a "code quality" type rule—guarding against pitfalls.
"no-console": 1 — Use during development, not in production
console.log is very useful for debugging but shouldn't remain in production. Level 1 (warn), only warns without blocking—because a machine cannot judge whether this console is "intentional"; a human must decide.
This reveals the wisdom in rule design: what the machine can determine (like
varshould definitely not be used) is set to error; what the machine cannot judge intent for (likeconsolemight be left intentionally) is set to warn.
"quotes": ["error", "double"] — Unify double quotes
Array format: the first item is the level, the second is the rule-specific configuration option. "double" means enforce double quotes ("hello"), reject single quotes ('hello').
"semi": ["error", "always"] — Must add semicolons
"always" means always add semicolons at the end of statements. This is a "style" type rule—just unify it across the team.
"indent": ["error", 2] — Indent 2 spaces
2 means the indentation width is 2 spaces (not Tab, not 4 spaces). A style rule.
8. package.json: How to Run ESLint
{
"name": "eslint-demo",
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"type": "commonjs",
"devDependencies": {
"@eslint/js": "^10.0.1",
"eslint": "^10.8.1",
"globals": "^17.11.0",
"typescript-eslint": "^8.66.0"
}
}
"lint": "eslint .": Checks all files in the current directory, listing places that don't conform to rules (only checks, doesn't change)."lint:fix": "eslint . --fix": Checks and auto-fixes. For issues like semicolons, indentation, and quotes where "the machine can determine the correct answer," ESLint directly fixes them for you; but for rules likeno-consolethat "require human judgment," it only warns without acting.
In the future, run npm run lint to check, npm run lint:fix to check + auto-fix.
devDependencies: The ESLint family is installed indevDependencies(development dependencies), because they are development tools, not packaged into the final product. The four packages each have their own role:eslint: Core engine@eslint/js: Official rule setglobals: Global variable definitionstypescript-eslint: TypeScript support
Summary
A thorough explanation of ESLint from "what it is" to "how to configure it":
- Why: Unify code style + discover pitfalls early, defined in the readme as "code style standard".
- What: A static analysis tool, doesn't run code, finds problems by "reading source code".
- Underlying: Parses source code into an AST (Abstract Syntax Tree), traverses nodes and applies rules—the essence of a rule is "finding a certain AST shape and judging violation".
- Levels:
2/1/0i.e. error/warn/off; what the machine can determine is set to error, what requires human judgment is set to warn. - Configuration: Flat config (
eslint.config.mjs) organized as a flat array,filesdefines scope,rulesdefines rules,tseslintextends TS. - Implementation:
npm run lintchecks,npm run lint:fixauto-fixes.
Once you understand AST + rule levels + flat config, ESLint is no longer an "incomprehensible config file" but a code quality tool you can control yourself.