Skip to content

ESLint与Prettier底层原理详解

1. ESLint工作原理

ESLint的工作原理可以分为三个主要步骤:解析、验证和修复。每个步骤都有其特定的作用和实现方式。

1.1 解析过程详解

ESLint使用espree(默认)或其他解析器将源代码转换为抽象语法树(AST)。这个过程分为两个阶段:

  1. 词法分析:将代码字符串分解成token流
  2. 语法分析:将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 自定义规则最佳实践

  1. 规则设计原则

    • 单一职责
    • 可配置性
    • 错误提示明确
    • 提供自动修复
  2. 性能考虑

    • 避免重复遍历
    • 合理使用缓存
    • 优化选择器匹配
  3. 测试覆盖

    • 正向测试
    • 反向测试
    • 边界情况

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的格式化算法主要考虑以下几点:

  1. 行长度限制
javascript
// 原始代码
const veryLongVariableName = someFunction(argument1, argument2, argument3, argument4);

// 当超过printWidth时,Prettier会尝试不同的换行策略
const veryLongVariableName = someFunction(
  argument1,
  argument2,
  argument3,
  argument4
);
  1. 缩进处理
javascript
// 处理嵌套结构的缩进
if (condition) {
  while (otherCondition) {
    // Prettier会保持一致的缩进级别
    doSomething();
  }
}
  1. 空行保留
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 [];
  }
}