JS ESLint 插件开发:自定义代码规范检查与 AST 规则

嘿,各位代码界的弄潮儿!今天咱们来聊点刺激的——JS ESLint 插件开发,玩转自定义代码规范,让你的代码像艺术品一样优雅!

一、啥是 ESLint?为啥要搞插件?

简单来说,ESLint 就是个代码质量检测工具,能帮你找出代码里的潜在 Bug、不规范写法,让你少加班,多摸鱼(划掉)… 提升效率!

为啥要开发插件呢?因为 ESLint 内置的规则再强大,也满足不了所有人的需求。比如,你公司有自己的一套命名规范,或者项目里有一些特殊的约定,就需要自定义规则来约束。

举个例子,假设你的团队喜欢用 _ 开头的变量表示私有变量,但 ESLint 默认是不允许的。这时候,你就可以写个插件,告诉 ESLint:“嘿,哥们儿,见到 _ 开头的变量别大惊小怪,这是我们内部的规矩!”

二、AST:代码的“X 光片”

要搞 ESLint 插件,就得先了解 AST(Abstract Syntax Tree,抽象语法树)。你可以把它想象成代码的“X 光片”,它把代码结构拆解成一棵树,每个节点都代表一个语法单元(比如变量声明、函数调用、表达式等等)。

ESLint 插件的核心就是分析 AST,找到不符合规范的节点,然后发出警告或错误。

来个简单的例子:

const a = 1 + 2;

这行代码的 AST 大概长这样(简化版):

{
  "type": "VariableDeclaration",
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",
        "name": "a"
      },
      "init": {
        "type": "BinaryExpression",
        "operator": "+",
        "left": {
          "type": "Literal",
          "value": 1
        },
        "right": {
          "type": "Literal",
          "value": 2
        }
      }
    }
  ],
  "kind": "const"
}

可以看到,const a = 1 + 2; 被拆解成了 VariableDeclaration(变量声明)、VariableDeclarator(变量声明符)、Identifier(标识符,也就是变量名)、BinaryExpression(二元表达式)、Literal(字面量,也就是数字 1 和 2)等等节点。

三、插件开发:手把手教你撸一个

咱们现在就来撸一个简单的 ESLint 插件,实现一个规则:禁止使用 console.log

1. 初始化项目

首先,新建一个文件夹,比如 eslint-plugin-no-console-log,然后在里面初始化一个 npm 项目:

mkdir eslint-plugin-no-console-log
cd eslint-plugin-no-console-log
npm init -y

2. 安装 ESLint

npm install eslint --save-dev

3. 创建规则文件

在项目根目录下创建一个 rules 文件夹,然后在里面创建一个 no-console-log.js 文件。这个文件就是我们自定义规则的实现。

rules/no-console-log.js 的内容如下:

module.exports = {
  meta: {
    type: 'problem', // 规则类型:problem, suggestion, layout
    docs: {
      description: '禁止使用 console.log', // 规则描述
      category: 'Possible Errors', // 规则分类
      recommended: 'error', // 推荐级别:off, warn, error
      url: null, // 规则文档地址
    },
    fixable: null, // 是否可自动修复:null, 'code'
    schema: [], // 规则配置项
    messages: {
      noConsoleLog: '禁止使用 console.log', // 错误提示信息
    },
  },
  create: function (context) {
    return {
      CallExpression: function (node) {
        if (node.callee.type === 'MemberExpression' &&
            node.callee.object.type === 'Identifier' &&
            node.callee.object.name === 'console' &&
            node.callee.property.type === 'Identifier' &&
            node.callee.property.name === 'log') {
          context.report({
            node: node,
            messageId: 'noConsoleLog',
          });
        }
      },
    };
  },
};

代码解释:

  • meta:定义规则的元数据,包括类型、描述、推荐级别、配置项等等。
  • create:核心函数,接收一个 context 对象,用于报告错误和访问 AST。
  • CallExpression:ESLint 会遍历 AST,当遇到 CallExpression 类型的节点时,就会调用这个函数。
  • node:当前遍历到的 AST 节点。
  • context.report:用于报告错误,接收一个对象,包含节点信息和错误提示信息。

4. 创建插件入口文件

在项目根目录下创建一个 index.js 文件,作为插件的入口。

index.js 的内容如下:

module.exports = {
  rules: {
    'no-console-log': require('./rules/no-console-log'),
  },
  configs: {
    recommended: {
      rules: {
        'no-console-log/no-console-log': 'error',
      },
    },
  },
};

代码解释:

  • rules:定义插件包含的规则,键是规则名称,值是规则的实现。
  • configs:定义插件的预设配置,比如 recommended,可以方便用户直接使用推荐的规则集。

5. 配置 ESLint

在项目根目录下创建一个 .eslintrc.js 文件,配置 ESLint 使用我们的插件。

.eslintrc.js 的内容如下:

module.exports = {
  plugins: [
    'no-console-log', // 插件名称,通常是 npm 包名
  ],
  extends: [
    'eslint:recommended', // 继承 ESLint 推荐的规则
    'plugin:no-console-log/recommended', // 使用插件的 recommended 配置
  ],
  rules: {
    // 在这里可以覆盖或添加其他规则
  },
};

6. 测试规则

创建一个 test.js 文件,写入包含 console.log 的代码:

console.log('Hello, world!');

然后在命令行运行 ESLint:

npx eslint test.js

如果一切正常,你应该会看到类似这样的错误提示:

test.js
1:1  error  禁止使用 console.log  no-console-log/no-console-log

✖ 1 problem (1 error, 0 warnings)

四、规则进阶:更复杂的 AST 分析

上面的例子只是个简单的入门,实际开发中,你可能需要分析更复杂的 AST 结构,才能实现更精细的规则。

比如,你想禁止使用 alert 函数,但只在 if 语句中使用时才禁止。这时候,你需要分析 alert 函数的父节点是否是 IfStatement

module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description: '禁止在 if 语句中使用 alert',
      category: 'Possible Errors',
      recommended: 'error',
    },
    fixable: null,
    schema: [],
    messages: {
      noAlertInIf: '禁止在 if 语句中使用 alert',
    },
  },
  create: function (context) {
    return {
      CallExpression: function (node) {
        if (node.callee.type === 'Identifier' && node.callee.name === 'alert') {
          // 向上查找父节点
          let parent = node.parent;
          while (parent) {
            if (parent.type === 'IfStatement') {
              context.report({
                node: node,
                messageId: 'noAlertInIf',
              });
              break;
            }
            parent = parent.parent;
          }
        }
      },
    };
  },
};

代码解释:

  • node.parent:访问当前节点的父节点。
  • while (parent):循环向上查找父节点,直到找到 IfStatement 类型的节点。

五、可配置规则:让用户自定义行为

有时候,你可能需要让用户自定义规则的行为,比如允许用户配置哪些变量名可以以 _ 开头。这时候,你需要使用 schema 来定义规则的配置项。

module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description: '允许使用 _ 开头的变量名',
      category: 'Stylistic Issues',
      recommended: 'off',
    },
    fixable: null,
    schema: [
      {
        type: 'array',
        items: {
          type: 'string',
        },
        minItems: 0,
        uniqueItems: true,
        description: '允许使用 _ 开头的变量名列表',
      },
    ],
    messages: {
      invalidName: '变量名 {{name}} 不符合规范',
    },
  },
  create: function (context) {
    // 获取配置项
    const allowedNames = context.options[0] || [];

    return {
      Identifier: function (node) {
        if (node.name.startsWith('_') && !allowedNames.includes(node.name)) {
          context.report({
            node: node,
            messageId: 'invalidName',
            data: {
              name: node.name,
            },
          });
        }
      },
    };
  },
};

代码解释:

  • schema:定义规则的配置项,这里定义了一个数组类型的配置项,允许用户传入一个字符串列表。
  • context.options:访问用户配置的选项。
  • data:可以在错误提示信息中使用模板字符串,将变量名传递给用户。

.eslintrc.js 中配置规则:

module.exports = {
  plugins: [
    'my-custom-rules',
  ],
  extends: [
    'eslint:recommended',
  ],
  rules: {
    'my-custom-rules/allow-underscore-names': [
      'error',
      ['_privateVariable', '_internalFunction'], // 允许使用 _privateVariable 和 _internalFunction
    ],
  },
};

六、自动修复:让代码自动变美

ESLint 还可以自动修复一些简单的代码风格问题,比如自动添加分号、自动调整缩进等等。要实现自动修复,需要在 meta 中设置 fixable: 'code',并在 context.report 中提供 fix 函数。

module.exports = {
  meta: {
    type: 'layout',
    docs: {
      description: '强制在语句末尾添加分号',
      category: 'Stylistic Issues',
      recommended: 'warn',
    },
    fixable: 'code',
    schema: [],
    messages: {
      missingSemicolon: '语句末尾缺少分号',
    },
  },
  create: function (context) {
    return {
      ExpressionStatement: function (node) {
        if (node.expression.type !== 'Literal' && node.range[1] !== ';') {
          context.report({
            node: node,
            messageId: 'missingSemicolon',
            fix: function (fixer) {
              return fixer.insertTextAfter(node, ';');
            },
          });
        }
      },
    };
  },
};

代码解释:

  • fixable: 'code':表示该规则支持自动修复。
  • fix:一个函数,接收一个 fixer 对象,用于生成修复操作。
  • fixer.insertTextAfter:在指定节点之后插入文本。

七、发布插件:让更多人受益

当你完成一个有用的 ESLint 插件后,可以把它发布到 npm 上,让更多人使用。

  1. 修改 package.json

    • name:插件名称,必须以 eslint-plugin- 开头。
    • main:插件入口文件,通常是 index.js
    • keywords:关键词,方便用户搜索。
    • files:指定要发布的文件,通常包括 index.jsrules 文件夹等等。
    • peerDependencies:声明对 ESLint 的依赖。
    {
      "name": "eslint-plugin-no-console-log",
      "version": "1.0.0",
      "description": "禁止使用 console.log 的 ESLint 插件",
      "main": "index.js",
      "keywords": [
        "eslint",
        "eslintplugin",
        "console",
        "log"
      ],
      "files": [
        "index.js",
        "rules"
      ],
      "peerDependencies": {
        "eslint": "^7.0.0 || ^8.0.0"
      },
      "author": "Your Name",
      "license": "MIT"
    }
  2. 登录 npm

    npm login
  3. 发布插件

    npm publish

八、总结:代码规范,从我做起

ESLint 插件开发是一个充满乐趣和挑战的过程,它可以让你深入了解代码的结构和规范,提升你的编程水平。希望今天的讲座能帮助你入门 ESLint 插件开发,写出更优雅、更规范的代码!记住,代码规范不是束缚,而是提升效率、减少 Bug 的利器!

表格总结:

步骤 描述 命令/代码示例
1. 初始化项目 创建项目文件夹并初始化 npm 项目。 mkdir eslint-plugin-my-rule
cd eslint-plugin-my-rule
npm init -y
2. 安装 ESLint 安装 ESLint 作为开发依赖。 npm install eslint --save-dev
3. 创建规则文件 创建规则的实现文件 (例如: rules/my-rule.js),定义规则的 metacreate 方法。 module.exports = { meta: { ... }, create: function(context) { ... } }
4. 创建插件入口文件 创建插件的入口文件 (index.js),导出规则。 module.exports = { rules: { 'my-rule': require('./rules/my-rule') } }
5. 配置 ESLint .eslintrc.js 中配置 ESLint 使用插件和规则。 plugins: ['my-plugin'], rules: { 'my-plugin/my-rule': 'error' }
6. 测试规则 创建测试文件并运行 ESLint。 npx eslint test.js
7. 规则进阶 学习更复杂的 AST 分析方法,例如访问父节点。 node.parent
8. 可配置规则 使用 schema 定义规则的配置项,让用户自定义行为。 meta: { schema: [ { type: 'string' } ] }, create: function(context) { const option = context.options[0]; ... }
9. 自动修复 实现自动修复功能,使用 fixer 对象生成修复操作。 meta: { fixable: 'code' }, create: function(context) { fix: function(fixer) { return fixer.insertTextAfter(node, ';'); } }
10. 发布插件 修改 package.json 并发布插件到 npm。 npm login
npm publish

好了,今天的分享就到这里。希望大家都能成为代码规范的守护者!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注