ESLint与Prettier底层原理详解
1. ESLint工作原理
ESLint的工作原理可以分为三个主要步骤:解析、验证和修复。每个步骤都有其特定的作用和实现方式。
1.1 解析过程详解
ESLint使用espree
(默认)或其他解析器将源代码转换为抽象语法树(AST)。这个过程分为两个阶段:
- 词法分析:将代码字符串分解成token流
- 语法分析:将token流转换为AST
让我们看一个具体例子:
javascript
// 原始代码
const greeting = "Hello" + name;
// 1. 词法分析后的Token流
[
{ type: "Keyword", value: "const" },
{ type: "Identifier", value: "greeting" },
{ type: "Punctuator", value: "=" },
{ type: "String", value: "Hello" },
{ type: "Punctuator", value: "+" },
{ type: "Identifier", value: "name" },
{ type: "Punctuator", value: ";" }
]
// 2. 语法分析后的AST(简化版)
{
type: "Program",
body: [{
type: "VariableDeclaration",
declarations: [{
type: "VariableDeclarator",
id: {
type: "Identifier",
name: "greeting"
},
init: {
type: "BinaryExpression",
operator: "+",
left: {
type: "Literal",
value: "Hello"
},
right: {
type: "Identifier",
name: "name"
}
}
}],
kind: "const"
}]
}
1.2 规则验证过程
ESLint的规则系统基于访问者模式(Visitor Pattern),每个规则都可以访问AST的不同节点类型。这使得规则可以针对特定的代码结构进行检查。
例如,一个检查变量命名的规则实现:
javascript
// camelCase命名规则的实现示例
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: '强制使用驼峰命名法'
},
fixable: 'code'
},
create(context) {
return {
Identifier(node) {
// 排除特定情况(如:导入的变量)
if (node.parent.type === 'ImportSpecifier') {
return;
}
const name = node.name;
// 检查是否符合驼峰命名
if (/^[a-z][a-zA-Z0-9]*$/.test(name)) {
return;
}
context.report({
node,
message: '变量名必须使用驼峰命名法',
fix(fixer) {
// 转换为驼峰命名
const newName = name.replace(/_([a-z])/g,
(_, letter) => letter.toUpperCase()
);
return fixer.replaceText(node, newName);
}
});
}
};
}
};
1.3 自动修复机制
ESLint的自动修复功能是通过收集所有可修复的问题,然后按照特定顺序应用修复来实现的。这个过程需要特别注意修复冲突的处理。
例如,处理多个重叠的修复建议:
javascript
// 源代码
var my_variable = "hello";
// 问题1:var -> const(范围:整行)
// 问题2:命名规范(范围:my_variable)
// 问题3:双引号 -> 单引号(范围:字符串)
// 修复顺序很重要
// 1. 先处理不重叠的修复(如:字符串引号)
// 2. 再处理范围较小的修复(如:变量命名)
// 3. 最后处理范围较大的修复(如:var声明)
// 最终结果
const myVariable = 'hello';
1.4 ESLint规则实现原理
规则执行流程 👇
flowchart TD A[源代码] --> B[解析器生成AST] B --> C[规则遍历AST节点] C --> D{是否符合规则条件?} D -->|是| E[收集错误信息] D -->|否| F[继续遍历] E --> G[生成错误报告] F --> G G --> H[应用自动修复]
1.4.1 规则实现示例
以"禁止使用var"为例:
javascript
// no-var规则实现
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: '禁止使用var声明变量',
category: 'Variables',
recommended: true
},
fixable: 'code',
schema: [] // 规则配置项
},
create(context) {
return {
// 访问者模式:处理var声明节点
VariableDeclaration(node) {
if (node.kind === 'var') {
context.report({
node,
message: '禁止使用var,请使用let或const',
fix(fixer) {
// 自动修复:将var替换为let
return fixer.replaceText(
node.kind === 'var' ? node : node.parent,
'let'
);
}
});
}
}
};
}
};
1.4.2 AST节点类型
TIP
ESLint规则主要处理以下AST节点类型:
声明类型:
- VariableDeclaration
- FunctionDeclaration
- ClassDeclaration
表达式类型:
- BinaryExpression
- CallExpression
- MemberExpression
语句类型:
- IfStatement
- ForStatement
- WhileStatement
1.4.3 规则测试示例
javascript
// no-var规则测试
const rule = require('../rules/no-var');
const RuleTester = require('eslint').RuleTester;
const ruleTester = new RuleTester({
parserOptions: { ecmaVersion: 2015 }
});
ruleTester.run('no-var', rule, {
valid: [
'let x = 1;',
'const y = 2;'
],
invalid: [
{
code: 'var z = 3;',
errors: [{ message: '禁止使用var,请使用let或const' }],
output: 'let z = 3;'
}
]
});
1.5 自定义规则最佳实践
规则设计原则:
- 单一职责
- 可配置性
- 错误提示明确
- 提供自动修复
性能考虑:
- 避免重复遍历
- 合理使用缓存
- 优化选择器匹配
测试覆盖:
- 正向测试
- 反向测试
- 边界情况
2. Prettier工作原理
Prettier的工作原理更加直接:它会完全重新格式化代码,而不是仅修复特定问题。
2.1 格式化流程详解
让我们看一个完整的例子:
javascript
// 原始代码(格式混乱)
function foo( a,b ){
if(a>b)return a+b;else{
return a-b
}}
// 1. 解析为AST
{
type: "FunctionDeclaration",
id: { type: "Identifier", name: "foo" },
params: [
{ type: "Identifier", name: "a" },
{ type: "Identifier", name: "b" }
],
body: {
type: "BlockStatement",
body: [
{
type: "IfStatement",
test: {
type: "BinaryExpression",
operator: ">",
left: { type: "Identifier", name: "a" },
right: { type: "Identifier", name: "b" }
},
consequent: {
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "+",
left: { type: "Identifier", name: "a" },
right: { type: "Identifier", name: "b" }
}
},
alternate: {
type: "BlockStatement",
body: [
{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "-",
left: { type: "Identifier", name: "a" },
right: { type: "Identifier", name: "b" }
}
}
]
}
}
]
}
}
// 2. 生成Doc
{
type: "group",
contents: [
{ type: "text", value: "function foo(a, b) {" },
{ type: "indent", contents: [
{ type: "hardline" },
{ type: "text", value: "if (a > b) " },
{ type: "text", value: "return a + b;" },
{ type: "text", value: " else {" },
{ type: "indent", contents: [
{ type: "hardline" },
{ type: "text", value: "return a - b;" }
]},
{ type: "hardline" },
{ type: "text", value: "}" }
]},
{ type: "hardline" },
{ type: "text", value: "}" }
]
}
// 3. 最终格式化结果
function foo(a, b) {
if (a > b) return a + b;
else {
return a - b;
}
}
2.2 格式化算法核心
Prettier的格式化算法主要考虑以下几点:
- 行长度限制:
javascript
// 原始代码
const veryLongVariableName = someFunction(argument1, argument2, argument3, argument4);
// 当超过printWidth时,Prettier会尝试不同的换行策略
const veryLongVariableName = someFunction(
argument1,
argument2,
argument3,
argument4
);
- 缩进处理:
javascript
// 处理嵌套结构的缩进
if (condition) {
while (otherCondition) {
// Prettier会保持一致的缩进级别
doSomething();
}
}
- 空行保留:
javascript
// Prettier会智能处理空行
function foo() {
doSomething();
// 这个空行会被保留,因为它提高了代码可读性
doSomethingElse();
}
2.3 实际应用示例
在实际项目中,我们经常会遇到这样的场景:
javascript
// 团队成员A的代码风格
function calculateTotal(items){
return items.reduce((sum,item)=>sum+item.price,0)
}
// 团队成员B的代码风格
function calculateTotal ( items )
{
return items.reduce( ( sum, item ) =>
sum + item.price
, 0 );
}
// Prettier格式化后(统一风格)
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
3. 深入理解工具链集成
3.1 编辑器集成原理
VSCode等编辑器是如何与ESLint和Prettier集成的?让我们深入了解:
javascript
// VSCode扩展实现示例
class ESLintLanguageServer {
constructor() {
this.documents = new Map();
this.diagnostics = new Map();
}
// 1. 文档变更监听
onDocumentChange(document) {
// 获取文档内容
const text = document.getText();
// 创建ESLint实例
const eslint = new ESLint({
baseConfig: this.configuration
});
// 执行lint
const results = await eslint.lintText(text);
// 转换为编辑器诊断信息
const diagnostics = results[0].messages.map(message => ({
range: this.toLSPRange(message),
message: message.message,
severity: this.toSeverity(message.severity),
source: 'eslint'
}));
// 发送诊断信息到编辑器
this.connection.sendDiagnostics({
uri: document.uri,
diagnostics
});
}
// 2. 自动修复实现
async autoFix(document) {
const text = document.getText();
const eslint = new ESLint({
fix: true,
baseConfig: this.configuration
});
const results = await eslint.lintText(text);
const [{ output }] = results;
if (output && output !== text) {
return [{
range: this.getFullRange(document),
newText: output
}];
}
return [];
}
}