lint 工具用来检查编程错误,最初是从 C 语言中发展起来的。在 C 语言最初时期,编译器无法捕获一些常见的编程错误,因此开发出了一个叫做 lint
的辅助程序,通过扫描源文件来查找问题。
当我们在 linting 的时候我们到底在干什么?实际上,最终目标是希望代码更加健壮,并且不论团队有多少成员,代码就像同一个人写出来的一样,可读性更强。
可以将众多 linters 的检查目标大致分为三类:
- programmer errors :主要是对语法的检查,这类错误会影响程序执行的正确性。
- best practices :其目的主要是为了避免出现让人困惑的代码,即使检查出问题也不一定意味着程序会执行出错,也有可能是正确的,但依然会令人困惑。这一步是避免潜在的错误,以及让代码更加清晰明确。
- style issues :主要是代码风格方面的检查,例如空格、标点符号、代码外观等等。
JavaScript
下图展示了 JavaScript linters 的进化史:
JSLint
2002 年由 Douglas Crockford 创建,用来进行 JavaScript 语法检查和校验。JSLint 定义了一个比 ECMAScript 编程语言标准更为严格的子集,是一种更高的标准。JSLint 完全是用 JavaScript 编写的。
JSLint 接收 JavaScript 源代码并对其进行扫描。如果发现问题,它将返回一条消息来描述问题以及源代码中的大概位置。这些问题多数时候是语法错误,但不全是语法错误,也可能是代码风格和结构的问题。它不能证明程序是正确的,只是提供了一个方式来帮助发现问题。JSLint 更加关心代码质量,因此即使浏览器可以正常运行的代码,JSLint 也可能不会通过。使用 JSLint 就意味着要欣然接受它所有的建议。
JSLint 可以对 JavaScript 源代码或 JSON 文本进行操作。
JSLint 将会认可 ES6 的一部分优秀的特性,例如 let
、 const
等等。
评价
优点
- 使用简单,开箱即用,无需再次配置。
缺点
- 有限的配置选项,很多规则不能禁用。
- 规范严格,凡是不认可的风格都会报一个 warning。
- 扩展性差。
- 无法根据错误定位到对应的规则。
JSHint
2010 年基于 JSLint 诞生了 JSHint ,它主要解决了 JSLint 过于专断的问题,提供了一些配置以及添加一些 rules 。相较之下更友好,也更容易配置,所以很快就发展了起来,也得到了众多 IDE 和编辑器的支持。
JSHint 扫描用 JavaScript 编写的程序,并报告常见的错误和潜在的错误。 潜在的问题可能是语法错误、由于隐式类型转换导致的错误、变量泄漏等。可以通过指定任意数量的 linting 选项或在源代码中声明指令来控制 JSHint 的行为。
JSHint 附带了一组默认的警告,但这些也是可配置的。可以在配置文件中指定要打开或关闭的 JSHint 选项。 例如,以下文件将启用有关未定义和未使用的变量的警告,并告知 JSHint 一个名为 MY_GLOBAL 的全局变量。
{
"undef": true,
"unused": true,
"globals": {
"MY_GLOBAL": true
}
}
但是,由于它是基于 JSLint 开发的,自然原有的一些问题它也继承下来了,比如不易扩展,不容易直接根据报错定位到具体的规则配置等。
评价
优点
- 可以灵活配置规则,支持配置文件
- 支持了一些常用类库
- 支持了基本的
ES6
语法
缺点
- 不支持自定义规则
- 无法根据错误定位到对应的规则
ESLint
2013年,Nicholas C. Zakas 创建了 ESLint ,提供了更好的 ES6
支持,以及更多的 rules ,尤其是一些代码风格方面的,以及一个灵活的插件系统,可以让开发者创建自己的 rules ,同时可以方便的根据报错定位到具体的规则配置。
规则的错误等级分为三级,可以更加细粒度地控制如何应用规则:
"off"
或0
- 关闭此条规则检查"warn"
或1
- 警告,不会影响 exit code"error"
或2
- 错误,exit code 为 1
默认情况下所有规则都是关闭的,"extends": "eslint:recommended"
会打开所有有“√”标记的规则,这些规则只跟着主版本更新,也可以在 npm 中查找以 eslint-config
开头的共享配置,通过 extends
配置项来添加。
ESLint 默认使用 Espree 作为 JavaScript 解析器,可以在 parser
配置项中更改解析器。解析器会将源代码解析成抽象语法树 AST(Abstract Syntax Tree),然后插件会根据这个 AST 来创建一些称为 lint rules 的断言,来描述代码应该是怎样的。
评价
优点
- 默认规则里面包含了
JSLint
和JSHint
的规则,易于迁移 - 有三种错误等级,可以更细粒度地控制 lint 的行为
- 灵活的插件扩展机制
- 可以自定义规则
- 可以根据错误定位到对应的规则
- 支持
ES6
- 支持
JSX
缺点
- 更大的灵活性意味着更复杂的配置
- 比前面两个慢
TypeScript
TSLint / typescript-eslint
用来检查 TypeScript 的,但是 2019 年已经废弃了,现在使用的是 ESLint,配合 typescript-eslint 。TypeScript 团队也宣布将 TypeScript 代码库从 TSLint 迁移到 typescript-eslint
。
ESLint 和 TypeScript
ESLint 使用一个 parser 将 source code 转成抽象语法树 Abstract Syntax Tree (AST) 的数据格式,然后插件根据这个 AST 来进行 lint rules 的检查。
TypeScript 是 JavaScript 的静态代码分析器,在基础的 JavaScript 上添加了一些额外的语法。TypeScript 使用一个 parser 将 source code 转成 AST ,然后 TypeScript Compiler 的其他部分使用这个 AST 来执行其他操作,例如给出类型检查后的问题反馈等等。
然而,ESLint 和 TypeScript 使用的是不同格式的 AST ,这就是 typescript-eslint
这个项目存在的主要原因。typescript-eslint
就是为了能够一起使用 ESLint 和 TypeScript 。
TSLint 使用的就是 TypeScript AST 格式,其优点是不需要一个调和 AST 格式之间差异的工具,但是主要缺点是 TSLint 无法重用 JavaScript 生态中围绕 linting 已经做好的工作,而是从头开始重新实现所有的功能,从规则到自动修复功能等等。因此,TypeScript AST 不兼容 ESLint 用户写成并使用的 1000 多条规则。
ESLint 解析 TypeScript:@typescript-eslint/parser
由于 TypeScript 是 JavaScript 的超集,它包含了所有 JavaScript 语法以外,还额外添加了一些语法,例如:
var x: number = 1;
当 TypeScript Compiler 解析这段代码生成 TypeScript AST 时,: number
语法也会出现在语法树中,ESLint 不借助其他工具是无法理解的。但 ESLint 在设计时就考虑到了这些用例。ESLint 不仅仅是一个库,而是由许多重要的移动部件组成。其中一个就是 parser 。ESLint 有一个内置的 parser 叫做 espree
,如果想支持非标准的 JavaScript 语法,只需要提供另外一个 parser 给 ESLint ,它需要将 TypeScript source code 解析为 ESLint 可以兼容的 AST 。 @typescript-eslint/parser
就是这样一个自定义的 ESLint parser 的实现。流程如下:
- ESLint 调用 ESLint config 中定义好的 parser (
@typescript-eslint/parser
)。 @typescript-eslint/parser
处理所有特定于 ESLint 的配置,然后调用@typescript-eslint/typescript-estree
。这个包只用来将 TypeScript source code 转为一个适当的 AST 。@typescript-eslint/typescript-estree
通过调用 TypeScript Compiler 将源代码生成一个 TypeScript AST ,然后将这个 AST 转换为 ESLint 需要的格式。这种 AST 格式不仅仅用于 ESLint,还有更广泛的用途。它有自己的规范,也就是 ESTree 。@typescript-eslint/typescript-estree
还被其他工具重用,例如 Prettier 的 TypeScript 使用。
ESLint rules 兼容 TypeScript:@typescript-eslint/eslint-plugin
TypeScript 和 ESLint 有类似的目标,因此可能出现 TypeScript 解决的一些问题原本是依赖 ESLint 解决的,两者可能会不兼容,最佳的解决方式是禁用相关的 ESLint 规则,转而交由 TypeScript Compiler 。
由于 TypeScript 是 JavaScript 的超集,即使 AST 进行了转换,最终的 AST 可能还会包含一部分让 ESLint 无法理解的部分,所以有些 ESLint rules 可能无法正常工作。有几种解决方案:要么解决 ESLint rules 的兼容性,要么使用另外的规则,即 @typescript-eslint/eslint-plugin
。
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
};
关于 Babel 和 babel-eslint
Babel 现在支持解析 TypeScript source code 但是不进行类型检查。这是使用 TypeScript Compiler 的一个替代方法。通过插件,它同样也可以支持许多其他 TypeScript Compiler 不支持的语法。
typescript-eslint
是由 TypeScript Compiler 提供支持的。babel-eslint
支持了一些 TypeScript 本身不支持的额外的语法,但是 typescript-eslint
利用类型信息可以支持创建 rules ,而这是 babel 做不到的,因为 babel 没有类型检查。因为它们是由不同的底层工具驱动的独立项目,所以目前不打算将它们一起使用。
其他
stylelint
用来检查样式,帮助避免错误和强制代码风格。可以理解最新的 CSS 语法,从 HTML、 markdown 及 CSS-in-JS 对象和模板中提取内联的样式,可以解析类 CSS 语法,如 SCSS、 Sass、 Less 和 SugarSS。支持插件,支持自定义规则。可以自动修复大多数违反代码风格的问题。并且是完全可配置的,通过在根目录添加配置文件 .stylelintrc.json
来按需配置。
commitlint
commitlint 用来检查 commit message ,帮助团队遵守 commit 约定,统一代码提交风格。支持通过 npm 安装已有的配置,或通过配置文件定义配置。使用 husky 来添加 git hooks :
// package.json
{
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
}
commit-msg
钩子会在一个新的 commit 创建时执行,通过 -E|--env
传递 husky 的 HUSKY_GIT_PARAMS
到 commitlint
,并将其定向到相关的编辑文件。
目前主流的代码风格主要有 Airbnb JavaScript Style Guide 、 Google JavaScript Style Guide以及 JavaScript Standard。
- AirbnbJavaScript Style Guide(github star 数:100k):对应的 ESLint 配置:eslint-config-airbnb
- Google JavaScript Style Guide(25.6k):谷歌内部的 JavaScript 编码标准。eslint-config-google
- JavaScript Standard(24.4k):零配置,只需要运行
standard --fix
即可自动格式化代码,还可以进行代码检查。对应的 ESLint 配置:eslint-config-standard - idiomatic.js(17.1k):社区版 JavaScript 编码风格指南。对应的 ESLint 配置:eslint-config-idiomatic
Prettier
目前最主流的是 Prettier ,它是一个专断的代码格式化工具,专注于 style issues。通过解析代码生成 AST,然后再按照自己的规则重新输出格式化后的代码。Prettier 执行的时机可以是在编辑器保存时、在 pre-commit hook 中或使用 CLI 工具在命令行中执行,以确保代码风格的一致。
由于历史原因,Prettier 仍然有一小部分选项,但官方更加推崇更少的配置项,因为其初衷就是终止团队关于代码风格的争论。这些选项一部分是初期添加的,一部分是需求增大导致的,还有一部分则与兼容性有关。选项越多,越有可能在团队内部引发争论。
Prettier 仅仅对 style issues 起作用。Prettier fork 了 recast 项目,并在内部使用了 Philip Wadler 提出的算法,该算法考虑了最大行宽(line width),最大行宽影响了代码的布局和换行,所以决定了最终输出的代码格式,而这是其他现有的代码格式化工具所欠缺的。例如,即便 eslint 会给出超出设定的最大行宽的警告,也无法自动修复。例如如下代码:
foo(reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne());
最终想要的效果可能是这样:
foo(
reallyLongArg(),
omgSoManyParameters(),
IShouldRefactorThis(),
isThereSeriouslyAnotherOne()
);
Wadler 算法是一个简单的基于约束的代码布局系统。它“测量”代码,如果代码跨越了最大的行宽,就会中断它。Prettier 禁止所有自定义样式,方法是将源代码解析成 AST 后,使用自己的规则同时考虑最大行宽,并在必要时换行,重新输出格式化的代码。
Prettier 和 linters 有何区别?
linters 有两类 rules :
- Formatting rules:例如:max-len, no-mixed-spaces-and-tabs, keyword-spacing, comma-style…
- Code-quality rules:例如:no-unused-vars, no-extra-bind, no-implicit-globals, prefer-promise-reject-errors…
Prettier 承担的是 Formatting rules 的工作,是一个“全自动”的风格指南。
因此,可以使用 Prettier 来进行代码风格的格式化,使用 linters 来检查 bugs!
Prettier 和 linters 配合使用
由于 linters 通常会包含样式相关的规则,使用 Prettier 时,大多数样式规则都是不必要的,而且更糟糕的是,它们可能与 Prettier 冲突!因此可以将 Prettier 用于代码格式问题,将 linter 用于代码质量问题。
对于 ESLint ,可以安装 eslint-config-prettier ,来关闭所有不需要的或者可能会跟 Prettier 冲突的 ESLint rules。
stylelint 同理可以使用 stylelint-config-prettier 。
linter + code style + code formatter 的组合:ESLint + Airbnb + Prettier 。
这里以一个 TypeScript + React 项目举例:
具体做法:
- 安装 ESLint (此时的版本是 8.6.0)
yarn add eslint --dev
- 初始化 ESLint 配置
yarn run eslint --init
这个过程中 ESLint 会询问你一些项目信息,逐个回答后 ESLint 会帮你生成适当的配置文件:
配置文件:
// .eslintrc.js
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:react/recommended",
"airbnb"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 13,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
}
};
eslint-config-airbnb 包含了 React,不包含 TypeScript 的相关 rules,需要手动修改配置文件:
{
"extends": [
- "plugin:react/recommended", // 使用 airbnb 中的 React rules
"airbnb", // 如果不需要 React 则使用 airbnb-base
"airbnb/hooks", // airbnb 默认未开启 React hooks,有需要可手动添加
+ "plugin:@typescript-eslint/recommended"
],
"plugins": [
- "react", // airbnb 已经添加了 React
"@typescript-eslint"
],
}
- 安装 Prettier
yarn add --dev prettier
# 如果需要配置文件(如果需要忽略文件,则创建一个 .prettierignore 文件)
echo {} > .prettierrc.json
- 设置编辑器
最好能在保存时就自动格式化代码,让开发者无需关注代码格式,强迫症必备。
以 WebStorm 为例:对于 2020.1 及以上的版本:Preferences | Languages & Frameworks | JavaScript | Prettier 然后启用选项 Run on save for files
。
现在保存时就可以自动格式化代码了。
- 配置 ESLint + Prettier
需要安装 eslint-config-prettier ,这个包会关闭所有不需要的或可能和 Prettier 冲突的ESLint rules。
然后将其添加到 ESLint 配置文件中的 extends
配置项的最后,好让它能够覆盖其他配置。这里的 prettier
除了关闭一些核心的 ESLint rules,还会关闭以下插件的部分 rules:
- @typescript-eslint/eslint-plugin
- @babel/eslint-plugin
- eslint-plugin-babel
- eslint-plugin-flowtype
- eslint-plugin-react
- eslint-plugin-standard
- eslint-plugin-unicorn
- eslint-plugin-vue
由于这个配置只会关闭 rules,所以跟其他配置一起使用才有意义,注意要放到最后:
{
"extends": [
"airbnb",
"airbnb/hooks",
"plugin:@typescript-eslint/recommended",
"prettier"
}
注意,还可以使用 eslint-plugin-prettier 插件把 Prettier 当做一个 linter rule 来运行,但这不是官方推荐的做法,主要有以下几个劣势:
- 编辑器中会出现很多烦人的红色波浪线,Prettier 的哲学就是让人忘记格式化,而不是给出一个提示。
- 比直接运行 Prettier 要慢。
- 它们只是一个间接层(one layer of indirection),有些东西可能会出问题。
- 添加 Git hooks
保险起见,最好在代码提交前也格式化一下。也就是可以在 pre-commit hook 里运行 Prettier 。
执行以下命令,会依赖 package.json 的 **devDependencies
中的代码检查工具来自动安装并配置 husky 和 lint-staged ,因此需要确保事先已经安装了 Prettier 和 ESlint 。
npx mrm lint-staged
package.json
会自动添加相关配置(默认生成的配置针对 .js
文件的,可以按需改为 .ts
或 .tsx
等),这样在每次提交前,都会通过 lint-staged 和 husky 运行 ESLint 和 Prettier 。
{
"devDependencies": {
"husky": ">=4",
"lint-staged": ">=10",
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,ts,tsx,jsx}": "eslint --cache --fix",
"*.{js,ts,tsx,jsx,css,md}": "prettier --write"
}
}
关于 lint-staged 的作用:在代码提交之前 linting 才有意义,这样可以确保不会将糟糕的代码上传到仓库中,以及强制统一风格。但是对整个项目运行一个 lint 过程很慢,而 linting 结果可能是无关紧要的。所以,只需要 lint 即将要提交的文件。 lint-staged 包含一个脚本,该脚本会运行任何 shell 任务,将staged files 作为参数,通过一个特定的 glob pattern 进行过滤。
husky(哈士奇)主要作用是添加 git hook(git 在特定的重要动作发生时触发自定义脚本),用来阻止不好的 git commit
、git push
等等。