ESLint插件开发指南

3,494 阅读7分钟

前言

随着项目的日渐庞大,必定导致负责该项目的人员不断增加或者更换,那么就意味着代码风格不尽相同的人员同时参与同一个项目开发,会导致该项目的代码风格是“百花齐放”。

问题来了,如何保持统一的代码风格呢?

这还不简单?

使用ESLint插件,加载各种规则去实时校验,在搭配pre-commit进行git提交校验不就行了吗?

那么问题又来了,当社区所提供的规则,不满足一些定制化程度比较高的规范时,我们就需要利用ESLint的插件机制,来实现自定义规则的校验。

在之前一段时间,针对组内一些比较特殊的规范,实现了一款插件:eslint-plugin-imports-sorter,提供了如下的校验。

  • 校验引入顺序:第三方库/官方库 > 绝对路径资源 > 相对路径资源

    // error
    import React from 'react'
    import { BarEnum } from '../../const'
    import style from '../index.less'
    import utils from 'src/utils'
    import { pick } from 'loadsh'
    
    // right
    import React from 'react'
    import { pick } from 'loadsh'
    import utils from 'src/utils'
    import { BarEnum } from '../../const'
    import style from '../index.less'
    
  • 不同引入顺序换行分割(可配置)

    import React from 'react'
    import { pick } from 'loadsh'
    
    import utils from 'src/utils'
    
    import { BarEnum } from '../../const'
    import style from '../index.less'
    
  • 相同引入顺序,层级排序(可配置)

    import React from 'react'
    import { pick } from 'loadsh'
    
    import utils from 'src/utils'
    
    import style from '../index.less'
    import { BarEnum } from '../../const'
    
  • 绝对路径资源配置别名

由于提供了自动修复的逻辑,因此搭配VsCode的配置(可以参考VsCode配置ESLint保存修复),可以实现如下效果。 fix.gif


一、ESLint校验流程

这里预先了解下ESLint配置中,影响最终规则校验的一些常规属性。

image.png

  • extends 中的每一项内容最终都指向了一个和 ESLint 本身配置规则相同的对象, 然后ESLint会递归的合并所有继承来的配置
  • ESLint 的核心就是规则(rule),每条规则都是独立,互不影响。
  • 最终来进行校验的是结合extendsrule被ESLint注释命令包含的节点而生成的最终生效配置,调用runRules生成遍及AST下规则指定的节点,进行校验。

1、拿到最终生效的规则配置

举个例子🌰

对于规则no-var

继承来的规则:no-var:2

自定义修改的规则:no-var:1

自定义指令修改规则:no-var:0

最终这条规则生效的配置为no-var:0

compare.gif

eslint/lib/linter/linter.js

image.png

自定义指令配置结构如下

/* 根据AST+注解生成的自定义指令配置 */
const commentDirectivesConfg = {
    /* 局部修改某些规则的配置信息 */
    configuredRules: {},
    /* 通过 「exported variableName」 指令,声明的暴露出的变量  */
    enabledGlobals: {},
    exportedVariables: {},
    problems: [],
    disableDirectives: [
        {
            type: 'disable-next-line',
            line: xxx,
            column: xxx,
            ruleId: 'ruleId'
        }
    ]
}

2、runRules所有规则遍历校验

最终每条规则会设置一个监听器,对当前节点进行校验。

image.png


总结
  • 插件集成了规则+规则配置,规则的核心逻辑的实现是create方法中。
  • ESLint校验过程的本质:根据最终得到的所有生效的规则配置,对规则选中的类型节点进行遍历校验。

二、拆解成AST(略)

AST Explorer

三、插件开发流程

2.1 工欲善其事,必先利其器

这里我们使用Yeoman脚手架以及generator-eslint生成器来生成ESLint插件的默认开发模版。

  • 安装依赖
// 安装 Yeomansudonpm i -g yo

// 安装 eslint 插件模板生成器sudonpm i -g generator-eslint
  • 创建模版
// 找到你要创建插件的文件夹  
// 初始化模版
yo eslint:plugin
  • 创建规则模版
yo eslint:rule

添加一些规则的相关信息

注意事项:插件的名称一般是以eslint-plugin-xxx来命名,但是也支持@xx/eslint-plugin-xx来命名,在插件配置的时候,只需要把开头或者中间的eslint-plugin-省略就好

  • 目录结构

    生成之后,我们的默认模板应该是这个图中这个样子。

image.png

简单说明下,这些目录的作用

-   docs/rules: lib/rules 下校验规则的说明文档
-   tests/libs/rules: lib/rules下的单元测试
-   lib/rules:所有规则
-   lib/index: 插件配置

2.2 本地实时调试

由于纯字符串代码中存在某些限制,不完全契合真实环境的ESLint校验流程,因此需要链接🔗到本地进行测试。

// 在插件工程下
sudo npm link 

链接成功后,我们会得到如下结果

// 切换至项目工程
 npm link xxxx

接下来,我们在插件中的改动都会同步至项目工程中

2.3 编写规则逻辑

1、配置插件参数(点我查看):规定了此条规则可接收哪些配置项,是否可自动修复,文档链接地址、错误提示文案等。

image.png 比较重要的几个参数

  • meta
    • docs
      • recommended:规定了配置文件中的 "extends": "eslint:recommended"属性是否启用该规则。
    • fixable: 该错误行为是否提供修复功能。如果没有 fixable 属性,即使规则实现了 fix 功能,ESLint 也不会进行修复。如果规则不是可修复的,就省略 fixable 属性。
    • schema:规定规则配置参数的类型
    interface Config {
      /* 不同引入方式换行分割:默认值true */
      isCheckEmptyLine: boolean
      /* 校验层级:默认值false */
      isCheckDeepth: boolean
      /* 绝对路径别名:默认值['src','@'] */
      alias: Array<string>
    }

那在使用的时候配置项就是如下格式

schema.gif

  • create (function) 返回一个对象,其中包含了 ESLint 在遍历 JavaScript 代码的抽象语法树 AST (ESTree 定义的 AST) 时,用来访问节点的方法。

  • 先明确需要校验的场景在哪类节点上,这是我相对比较重要的一点。 因为某些规则的实现方式可能有多重,但是实现成本一定程度上取决于你设定的AST节点类型

// 这里是ESlint支持的全部节点类型,可以通过[https://astexplorer.net/]来查看节点类型
const KNOWN_NODES = new Set([
    "AssignmentExpression",
    "AssignmentPattern",
    "ArrayExpression",
    "ArrayPattern",
    "ArrowFunctionExpression",
    "AwaitExpression",
    "BlockStatement",
    "BinaryExpression",
    "BreakStatement",
    "CallExpression",
    "CatchClause",
    "ChainExpression",
    "ClassBody",
    "ClassDeclaration",
    "ClassExpression",
    "ConditionalExpression",
    "ContinueStatement",
    "DoWhileStatement",
    "DebuggerStatement",
    "EmptyStatement",
    "ExperimentalRestProperty",
    "ExperimentalSpreadProperty",
    "ExpressionStatement",
    "ForStatement",
    "ForInStatement",
    "ForOfStatement",
    "FunctionDeclaration",
    "FunctionExpression",
    "Identifier",
    "IfStatement",
    "Literal",
    "LabeledStatement",
    "LogicalExpression",
    "MemberExpression",
    "MetaProperty",
    "MethodDefinition",
    "NewExpression",
    "ObjectExpression",
    "ObjectPattern",
    "Program",
    "Property",
    "RestElement",
    "ReturnStatement",
    "SequenceExpression",
    "SpreadElement",
    "Super",
    "SwitchCase",
    "SwitchStatement",
    "TaggedTemplateExpression",
    "TemplateElement",
    "TemplateLiteral",
    "ThisExpression",
    "ThrowStatement",
    "TryStatement",
    "UnaryExpression",
    "UpdateExpression",
    "VariableDeclaration",
    "VariableDeclarator",
    "WhileStatement",
    "WithStatement",
    "YieldExpression",
    "JSXFragment",
    "JSXOpeningFragment",
    "JSXClosingFragment",
    "JSXIdentifier",
    "JSXNamespacedName",
    "JSXMemberExpression",
    "JSXEmptyExpression",
    "JSXExpressionContainer",
    "JSXElement",
    "JSXClosingElement",
    "JSXOpeningElement",
    "JSXAttribute",
    "JSXSpreadAttribute",
    "JSXText",
    "ExportDefaultDeclaration",
    "ExportNamedDeclaration",
    "ExportAllDeclaration",
    "ExportSpecifier",
    "ImportDeclaration",
    "ImportSpecifier",
    "ImportDefaultSpecifier",
    "ImportNamespaceSpecifier",
    "ImportExpression"
]);

是否可修复,取决于该规则校验失败的原因是否偏向于主观化。

举个例子🌰

场景1: 在某些调试过程中,临时修改一些变量,参数,方便调试。但是偶尔会忘记修改回来。

插件功能:打上/* eslint:todo */注释的节点抛出警告。

上述这样的插件,他的错误就是趋于主观化,你通过上下文不能确定这个错误能否修复。

使用 context.report() 时指定 fix 函数。fix 函数接收一个参数,即一个 fixer 对象,你可以用它来进行修复。例如:

context.report({
    node: node,
    message: "Missing semicolon",
    fix: function(fixer) {
        return fixer.insertTextAfter(node, ";");
    }
});

重要: 除非规则输出 meta.fixable 属性,ESLint 不会进行修复,即使该规则实现了 fix 函数。

注意事项⚠:

  1. 避免任何可能改变代码运行时行为和导致其停止工作的修复。
  2. 做尽可能小的修复。那些不必要的修复可能会与其他修复发生冲突,应该避免。
  3. 使每条消息只有一个修复。这是强制的,因为你必须从 fix() 返回修复操作的结果。

参考文献