关于eslint
ESLint 是一个高度可配置的 Javascript 代码静态分析工具,其中静态分析是指将源码转为 AST 后,基于AST进行遍历、应用规则、上报问题;而可配置性则使得 ESLint 能够满足各种项目需求,极大地增强了其功能。例如:
校验规则(Rules) :ESLint 允许根据项目需求灵活配置规则,这些规则集可以作为基础配置,帮助开发者快速上手并维护一致的代码风格。社区中常见的 ESLint 规则集包括: eslint-config-airbnb
:遵循 Airbnb 的 JavaScript 风格指南。eslint-config-google
:遵循 Google 的 JavaScript 风格指南。扩展插件(Plugins) :ESLint 支持通过插件扩展新的规则,插件可以增加 ESLint 的功能,使其能够处理更多的代码规范和风格要求,例如: @typescript-eslint/eslint-plugin
:用于扩展 TypeScript 的规则。解析器(Parser) :虽然 ESLint 是 JavaScript 的静态分析工具,但其功能并不限于此。通过配置解析器,ESLint 可以解析和处理其他语言,例如: @typescript-eslint/parser
:将 TypeScript 文件解析为 ESTree 兼容的 AST 格式,使 ESLint 能够识别和处理 TypeScript 文件。yaml-eslint-parser
:使 ESLint 能够识别和处理 YAML 文件,并应用相应的规则。处理器(Processors) :ESLint 还可以通过配置处理器来处理其他格式文档中的代码。例如: eslint-plugin-markdown
:可以 lint Markdown 文件中的 JavaScript 和 TypeScript 代码片段。eslint-plugin-html
:可以处理 HTML 文件中的<script>
标签内的代码片段。
不过,初学者总容易混淆上面提到的若干概念,这里先集中介绍一下:
parser和processor的区别
不知道大家有没有跟我一样的困惑,为什么yaml文件可以通过yaml-eslint-parser处理,但是makdown或者html文件是通过plugin的processors处理?yaml文件的lint配置:
{
"files": ["*.yaml", "*.yml"],
"parser": "yaml-eslint-parser",
"plugins": ["yaml"],
"extends": ["plugin:yaml/recommended"]
}
markdown文件的lint配置:
{
"files": ["*.md"],
"processor": "markdown/markdown"
}
解析器(parser) :负责将代码转换为 AST,直接影响 ESLint 如何解析文件。
换句话说,代码能够转出eslint能够识别的AST格式(参考eslint的AST规范,本质上就是ESTree的兼容格式)
处理器(processor) :负责预处理和后处理文件内容,主要用于处理非 JavaScript 文件中特定的文件片段。比如说,markdown这种语言并不是一门编程语言,也就没办法转换Estree格式的AST,但是我们却想lint其中的js代码,就可以通过plugin的preocessor提取其中的代码片段,然后通过处理。
config和plugin的区别:
config简单理解就是一份完整的eslint配置文件,我们可以在配置中通过extends的来继承和使用这些配置:
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"no-console": "warn"
},
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
}
}
可能你注意到,plugin除了可以配置在plugins中,也可以配置在extends,其实plugin除了声明规则、processer(预处理器),也可以声明配置,例如声明一份推荐的配置:
// plugin config
module.exports = {
configs: {
recommended: {
plugins: ["myPlugin"],
env: ["browser"],
rules: {
semi: "error",
"myPlugin/my-rule": "error",
"eslint-plugin-myPlugin/another-rule": "error"
}
}
rules: {
'some-rule': {/* rule definition */},
},
processers: { //... }
},
extends里面配置的其实都是一份eslint配置,意味着这份配置文件也可以通过extends字段配置另一个eslint配置,然后另外一个配置文件也可以extends配置... ...
事情好像变得复杂一些了,下一节将会专门讲解eslint配置的复杂度。
eslint配置的复杂度
参考:
https://eslint.org/blog/2022/08/new-config-system-part-1/ https://eslint.org/blog/2022/08/new-config-system-part-2/
你是否有以下困惑:
eslint的配置文件怎么那么多? .eslintrc.js .eslintrc.cjs .eslintrc.yaml .eslintrc.yml .eslintrc.json package.json
.eslint.js
的plugin配置和config配置为什么要写字符串?为什么要省略eslint-config?而不是完整的npm包名?怎么莫名其妙有一些新的规则? 我的规则配置怎么没有生效?plugin好像没有生效? ......
其实eslint最早期只支持 .eslintrc
配置(本质上是json格式),作为特性后面支持让用户使用更多格式的配置文件,也就看到上述的这么多配置文件。
yml和json是可以相互转换的,倒也还好,但是js的对象并不能完全转换为json。例如plugin中的配置是可以配置使用正则表达式,但是json文件中其实不方便配置正则(虽然可以通过一些方法去解析),越来越多plugin支持配置js语法,这也让后续js文件的配置格式逐渐成为主流。
但是为了兼容json格式的配置,config和plugin保留了使用字符串的特性,其实就是为了配置的简洁性,其实是约定的可以省略一些配置前缀。
这意味着如何去加载和处理这些配置的完全是eslint自己去处理的(eslint如果解析不到声明的这些包,配置将会无效),但是我们共享和发布的config或者plugin的包对于他们依赖的config或者plugin或者parser,官方推荐而只需声明 peerDependency
(因为即使声明 dependency,eslint的还是按照自己的安装路径进行解析)
这个特性在npm v3之前都是没有问题的,因为npm3会自动安装 peerDependency
,但是之后npm取消了这个特性......
很常见的,我们要安装使用一个config,我们还要安装好多其他包,以确保eslint能够正确解析。比如我们要使用:eslint-config-airbnb-typescript
,安装后进行eslint配置:
extends: [
'airbnb-typescript'
]
但是这个config其实还依赖 @typescript-eslint/eslint-plugin
和 @typescript-eslint/parser
,所以我们还需要手动安装这两个依赖,才能正常工作。这也是一些规则或者插件没有生效的原因之一。
除了使用 extends
复用eslint的配置,eslint还有一个早期就支持的特性叫配置级联(configuration cascade
)。
eslint会根据当前lint文件的位置一直往他的上层文件夹找eslintrc配置文件并且合并这个配置,直到找到配置了"root":true的配置文件或者到用户配置目录 ~/.eslintrc
。
此外eslint还支持配置overrides 配置,让允许我们用glob表达式匹配文件,然后使用不同的规则。例如overrides可以针对ts文件和js文件配置不同的规则,当然overrides里面也是可以使用其他config的。
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-unused-vars": ["error"],
"@typescript-eslint/explicit-module-boundary-types": "off"
}
},
现在假设我们要用eslint检测一个文件,我们该如何获取检测这个文件需要的完整配置呢?
首先eslint会通过配置级联特性,逐个文件夹的去查找配置文件... 当然如果有多个格式的配置文件,他们有自己的优先级... 找完了所有的配置文件... 需要找到他们又 extends
使用了哪些配置文件...overrides
又配置了哪些规则...当然这些配置文件也可能 extends
其他配置... 当然overrides
也可以extends
配置....... 最后配置文件解析后,还要根据文件的指令以及cli的参数,合成最后的应用规则。
对文件应用的过程要合并各种配置,判断各种优先级,总而言之十分复杂和繁琐,连eslint作者本人也摇头。
上面所有的问题都是eslint8.x(我们项目中使用的版本)存在的,但是eslint9.x已经完全重新设计的eslintrc配置系统(8.x也可以通过环境ESLINT_USE_FLAT_CONFIG开启该特性),使用的flat config,解决了上述的很多问题。感兴趣可以参考本节开头引用的那两篇文章。
Can do and can't do
eslint的原理其实就决定了这个工具能做什么不能做什么?静态语法分析!所有基于静态语法分析能做的事情,eslint几乎都可胜任。
能做:
语法检测。如缺少分号、未定义的变量等。 代码风格检测。如缩进、空格、引号类型等。 最佳实践。React、React hooks等框架的最佳实践。 代码质量。如未使用的变量、未处理的 Promise 等。 自动修复。自动修复一些简单的代码问题。 ...
不能做的:
运行时错误检测。代码到底跑是怎么样,还得靠自己保证。 复杂的逻辑检测。比如一些无法泛化和抽象的场景,因为JS语言很灵活,很多问题不太可能静态就检测出来。
不善于做的:
系统IO:虽然通过插件已经可以检测命名规范等功能,但是eslint的规则还是不太建议去调用系统IO去做一些事情,比如动态的去生成或者读取一些其他信息。 跨文件的代码分析:当然,一些 plugin也已经实现跨文件的分析比如eslint-plugin-import,它能检测依赖循环等功能。跨文件分析的关键在于通过语法解析到另外一个模块,这个plugin通过配置resolver,内置支持node的模块解析逻辑,也支持配置其他的解析逻辑如webpack、typescript的解析。所以如果有跨文件解析的需求可考虑直接使用eslint-modules-uitls
一些调试技巧
1. 如何检测eslint规则的性能?
我们可以在eslint命令前加上环境变量TIMING,并且设置为all或者*,就可以得到规则的耗时统计。
设置为*或者1,获取前10的耗时规则
设置为all,获取所有耗时的规则
2. 如何知道某个文件应用了哪些规则?
上面关于eslint配置的复杂度介绍到,如果想知道一个文件到底生效和应用了哪些规则,单从配置文件上来看十分难以推断。有两种方法可以帮助我们直接拿到eslint最终应用的规则:
方法一 :通过 --print-config
命令行参数:npx eslint --print-config src/index.ts
方法二:通过 DEBUG
环境变量eslintrc:*
,这个方法不仅能看到最终的config,还能看到整个查找和解析的过程:DEBUG=eslintrc:* npx eslint src/index.t
s方法三:通过vscode eslint插件的outpout日志。 首先打开vscode配置,把 "eslint.debug"设置为true,然后 reload
重新启动插件,然后打开vscode的output面板,选择eslint。这个方法不仅能看到eslintrc的日志,还能看到整个eslint的日志,相当于 DEBUG=*
。也适用于我们想快速验证和调试(而不是通过命令行启动eslint)一个新的规则在某个文件中的运行情况。
3. 如何快速debug分析eslint执行逻辑?
eslint生态基本上都可以通过 DEBUG
变量来调试eslint运行日志。如果不确定DEBUG的名称,可以先直接使用 DEBUG=*
运行,然后选择关注自己想要关注的部分。比如:我想关注 eslint-import
的 resolver
的性能,我可以运行:
DEBUG=eslint-import-resolver-typescript,eslint-plugin-import:resolver:* npx eslint ./src/index.ts
如何详细debug分析eslint执行逻辑?如果通过日志也无法分析具体问题,除了看源码,我比较喜欢方式是直接debug运行代码,参考node debug。可以运行:
node --inspect-brk ./node_modules/eslint/bin/eslint.js src/index.ts
然后我们可以在chrome上一行一行的对源码进行调试,也可以使用perfomance面板直接记录火焰图,分析性能问题。
实战: import/no-cycle性能问题排查
这个是排查pre-commit的过程中发现的一个问题:import/no-cycle
的性能十分的差,几乎占到所有的时间。
通过node debug和chrome的perfomance看板分析火焰图:
从火焰图可以看出:
lintFiles
其实就是某个规则检测这个文件所用的时间,发现红框区域的耗时是最久的。分析其调用栈,不难看出其一直在递归调用 processImportedModules
,即使用例只引用了一个文件,但是调用栈已经很恐怖了。
归因到 import/no-cycle
的逻辑,他其实会递归的分析到的文件的的所有模块都解析一遍。
通过分析其源码发现,其实这个规则的逻辑会通过ignoreExternal
这个配置进行剪枝,从而优化性能。
所以,优化方式也很简单,对 import/no-cycle
规则配置开启 ignoreExternal
,该配置会忽略非本项目之外(外部模块)的循环依赖检测(比如node_modules里的依赖、workspace的其他pkg)。