十分钟了解eslint配置 && 编写自定义eslint规则

eslint介绍

ESLint 是一个开源的 JavaScript 代码检查工具,由 Nicholas C. Zakas 于2013年6月创建。代码检查是一种静态的分析,常用于寻找有问题的模式或者代码,并且不依赖于具体的编码风格。对大多数编程语言来说都会有代码检查,一般来说编译程序会内置检查工具。

JavaScript 是一个动态的弱类型语言,在开发中比较容易出错。因为没有编译程序,为了寻找 JavaScript 代码错误通常需要在执行过程中不断调试。像 ESLint 这样的可以让程序员在编码的过程中发现问题而不是在执行的过程中。

ESLint 的初衷是为了让程序员可以创建自己的检测规则。ESLint 的所有规则都被设计成可插拔的。为了便于人们使用,ESLint 内置了一些规则,当然,你可以在使用过程中自定义规则。所有的规则默认都是禁用的。

ESLint 使用 Node.js 编写。

eslint配置

配置方式
  1. 一般都采用.eslintrc.* 的配置文件进行配置, 如果放在项目的根目录中,则会作用于整个项目。如果在项目的子目录中也包含着.eslintrc文件,则对于子目录中文件的检查会忽略掉根目录中的配置,而直接采用子目录中的配置,这就能够在不同的目录范围内应用不同的检查规则,显得比较灵活。ESLint采用逐级向上查找的方式查找.eslintrc.*文件,当找到带有 "root": true 配置项的.eslintrc.* 文件时,将会停止向上查找。
  2. 在 package.json文件里的 eslintConfig 字段进行配置。
具体配置规则

以使用项目为例,简单介绍一下eslint的具体配置及作用:

module.exports = {
    parser: 'babel-eslint', // parser指定解析器,默认的为espree。babel-eslint是一个Babel parser的包装器,这个包装器使得 Babel parser 可以和 ESLint 协调工作
    parserOptions: {
        sourceType: 'module', // 设置为 "script" (默认) 或 "module"(ES6)。
        ecmaFeatures: { // 这是个对象,表示你想使用的额外的语言特性:
            jsx: true // 启用 JSX
        }
    },
    extends: ['eslint:recommended'], // 使用eslint推荐的规则作为基础配置,可以在rules中覆盖
    plugins: ['html', 'vue', 'prettier', 'import'], // vue是eslint-plugin-vue的简写,此插件的作用是可以让eslint识别.vue中的script代码
    rules: { // 0或者off表示规则关闭,出错也被忽略;1或者warn表示如果出错会给出警告(不会导致程序退出);2或者error表示如果出错会报出错误(会导致程序退出,退出码是1)
        'no-console': 'off',
        'prefer-const': 'error',
        'prettier/prettier': 'warn',
        'prefer-arrow-callback': 'warn',
        'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
    },
    globals: { // 允许在代码中使用全局变量
        location: true,
        setTimeout: true
    }
};

具体的配置文档:http://eslint.cn/docs/user-guide/configuring
具体的eslint:recommended支持的规则:https://cn.eslint.org/docs/rules/

“extends”除了可以引入推荐规则,还可以以文件形式引入其它的自定义规则,然后在这些自定义规则的基础上用rules去定义个别规则,从而覆盖掉”extends”中引入的规则。

{
    "extends": [
        "./node_modules/coding-standard/eslintDefaults.js",
        // Override eslintDefaults.js
        "./node_modules/coding-standard/.eslintrc-es6",
        // Override .eslintrc-es6
        "./node_modules/coding-standard/.eslintrc-jsx",
    ],
    "rules": {
        // Override any settings from the "parent" configuration
        "eqeqeq": "warn"
    }
}

除了在配置文件中指定规则外,还可以在代码中指定规则,代码文件内以注释配置的规则会覆盖配置文件里的规则,即优先级要更高。平时我们常用的就是 `eslint-disable-next-line
`

忽略检查

可以通过在项目目录下建立.eslintignore文件,并在其中配置忽略掉对哪些文件的检查。需要注意的是,不管你有没有在.eslintignore中进行配置,eslint都会默认忽略掉对/node_modules/** 的检查。也可以在package.json文件的 eslintIgnore 字段进行配置。

eslint检查原理

要实现静态分析则需要自建一个预编译阶段对代码进行解析。

首先我们看看大部分编译器工作时的三个阶段:

解析:将未经处理的代码解析成更为抽象的表达式,通常为抽象语法树,即 AST。
转换:通过修改解析后的代码表达式,将其转换为符合预期的新格式。
代码生成:将转换后的表达式生成为新的目标代码。

对于eslint来说,规则校验发生在将JavaScript 代码解析为 AST 之后,遍历 AST 的过程中。eslint采用 Espree 来生成AST。具体的生成方法在这里
我们可以使用AST explorer来查看代码被解析后生成的AST。

rules工作原理

首先来看看eslint源码中关于rules的编写。eslint中的rules源码存在于lib/rules下。每一个rules都是一个node模块,用module.exports导出一个meta对象及一个create函数。

module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "disallow unnecessary semicolons",
            category: "Possible Errors",
            recommended: true,
            url: "https://eslint.org/docs/rules/no-extra-semi"
        },
        fixable: "code",
        schema: [] // no options
    },
    create: function(context) {
        return {
            // callback functions
        };
    }
};

meta 代表了这条规则的元数据,如这条规则的类别,文档,可接收的参数 schema 等等。

create 返回一个对象,其中定义了一些在 AST 遍历访问到对应节点需要执行的方法等等。函数接受一个context对象作为参数,里面包含了例如可以报告错误或者警告的context.report()、可以获取源代码的context.getSourceCode()等方法,可以简化规则的编写。

function checkLastSegment (node) {
    // report problem for function if last code path segment is reachable
}

module.exports = {
    meta: { ... },
    create: function(context) {
        // declare the state of the rule
        return {
            ReturnStatement: function(node) {
                // 在AST从上向下遍历到ReturnStatement node 时执行
            },
            // 在AST 从下向上遍历到 function expression node 时执行:
            "FunctionExpression:exit": checkLastSegment,
            "ArrowFunctionExpression:exit": checkLastSegment,
            onCodePathStart: function (codePath, node) {
                // 在分析代码路径开始时执行
            },
            onCodePathEnd: function(codePath, node) {
                // 在分析代码路径结束时执行
            }
        };
    }
};

遍历 AST 的过程中会以“从上至下”再“从下至上”的顺序经过节点两次,selector 默认会在下行的过程中执行对应的访问函数,如果需要再上行的过程中执行,则需要添加:exit。

详细的原理在官方文档中有说明,点这里
详细的代码路径分析在这里

如何编写一个rules

知道了rules的原理,接下来可以自定义一个rules。每一个rules需要有三个以该规则名命名的文件,分别是:

  • 在 lib/rules 目录下: 一个源文件(例如,no-extra-semi.js)
  • 在 tests/lib/rules 目录下: 一个测试文件 (例如, no-extra-semi.js)
  • 在 docs/rules 目录: 一个 markdown 文档文件 (例如, no-extra-semi)

接下来我们来编写一个简单的rules,例如禁止块级注释,当代码中使用了块级注释,eslint将报错。

rules文件:

// lib/rules/no-block-comments.js
module.exports = {
  meta: {
    docs: {
      description: '禁止块级注释',
      category: 'Stylistic Issues',
      recommended: true
    }
  },

  create (context) {
    // 获取源代码
    const sourceCode = context.getSourceCode()

    return {
      Program () {
        // 获取源代码中所有的注释
        const comments = sourceCode.getAllComments()

        const blockComments = comments.filter(({ type }) => type === 'Block')

        blockComments.length && context.report({
          node: node,
          message: 'No block comments'
        })
      }
    }
  }
}

rules的测试文件:

// tests/lib/rules/no-block-comments.js
const RuleTester = require("eslint").RuleTester;
const rule = require("../../../lib/rules/no-block-comments");

const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } }); // You do have to tell eslint what js you're using

ruleTester.run("no-block-comments", rule, {
    valid: ["var a = 1; console.log(a)"],
    invalid: [
        {
            code: "var a = 1; /* block comments */ console.log(a)",
            errors: [
                {
                    messageId: "blockComments",
                    line: 1,
                    nodeType: "Block"
                }
            ]
        }
    ]
});

官网的working with rules文档中有关于如何编写一个rules的详细介绍。

如何使用自定义的rules

编写好的rules需要发布到npm上,作为一个eslint-plugin,在项目中下载下来才能够使用。例子中代码的npm在这里

在项目中的配置:

// .eslintrc.js
module.exports = {
    ...
    "plugins": [
        "eslint-plugin-no-block-comments"
        // 你 publish 的 npm 包名称,可以省略 eslint-plugin
      ],
    "rules": { // 启用的规则及其各自的错误级别
        'no-console': 'off',
        "no-block-comments/no-block-comments": 2 // 引用no-block-comments插件中的no-block-comments规则
    }
};

之后就可以对代码进行检查了。比如我要检查的代码如下:

// src/index.js
const a = 1;
/*
    这里是块级注释
*/
console.log(a);

在命令行中执行eslint src,就可以看到报错结果。
十分钟了解eslint配置 && 编写自定义eslint规则

参考文章:

ESlint官网

ESLint 工作原理探讨

开发 eslint 规则