Lint介绍
在计算机科学中,lint是一种工具程序的名称,它用来标记源代码中,某些可疑的、不具结构性(可能造成bug)的段落。它是一种静态程序分析工具,最早适用于C语言,在UNIX平台上开发出来。后来它成为通用术语,可用于描述在任何一种计算机程序语言中,用来标记源代码中有疑义段落的工具。
Lint会对代码做静态分析,检查出其中的一些结构错误或者格式错误。在前端领域中,我们常用的lint就是ESLint,它用于检查JavaScript代码是否符合规则。
为什么要使用Lint
JS 做为一种动态语言,写起来可以随心所欲,但是也容易出bug。通过合适的规则来约束,能让代码更健壮。
如果一个项目没有任何Lint工具,工程师编写出来的代码风格各异,会降低代码可读性、提高维护成本等。保持代码风格的一致性能增加可读性,更便于团队合作。保持一致就意味着要对工程师的代码增加一定的约束,ESLint正能做到。
ESLint
ESLint is a configurable JavaScript linter. It helps you find and fix problems in your JavaScript code. Problems can be anything from potential runtime bugs, to not following best practices, to styling issues.
- 发现问题
- ESLint静态分析代码,以快速发现问题。
- 自动修复问题
- 可配置
- 编写自定义规则插件,与内置ESLint规则共同工作
基本概念
Rules
Rules是ESLint中的核心,它定义了代码是否符合预期以及不符合预期的处理。ESLint内置了许多规则,我们可以通过配置这些规则来自定义代码规范。
举个例子,semi规则就指定了JavaScript语句是否需要以”;”分号结尾。
Configuration Files
ESLint配置文件(.eslintrc.*等)用于配置ESLint,包括内置规则、想要强制执行的内容、具有自定义规则的插件、可共享的配置,你希望规则适用于哪些文件等等。
module.exports = {
"extends": "eslint:recommended",
"rules": {
// enable additional rules
"indent": ["error", 4],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
// override configuration set by extending "eslint:recommended"
"no-empty": "warn",
"no-cond-assign": ["error", "always"],
// disable rules from base configurations
"for-direction": "off",
}
}
Shareable Configurations
可共享配置包是通过 npm 共享的 ESLint 配置,通常会用来配置项目基础规范。例如 eslint-config-airbnb-base 实现了流行的 Airbnb JavaScript 样式指南。
Plugins
ESLint 插件是一个 npm 模块,可以包含一组 ESLint 规则、配置、处理器和环境。插件可用于执行样式指南并支持 JavaScript 扩展(如 TypeScript)、库(如 React)和框架(Angular)。
插件的一个常见使用是执行框架规范的最佳实践。例如,eslint-plugin-vue包含使用 Vue 的规范最佳实践。
Parsers
ESLint 解析器将代码转换为 ESLint 可以处理的抽象语法树(AST)。默认情况下,ESLint 使用内置的 Espree 解析器,它与标准的 JavaScript 运行时和版本兼容。
自定义解析器让 ESLint 解析非标准的 JavaScript 语法。自定义解析器通常包含在可共享配置或插件中,因此一般不必直接使用它们。
例如,@typescript-eslint/parser 是一个包含在 typescript-eslint 项目中的自定义解析器,它可以让 ESLint 解析 TypeScript 代码。
Custom Processors
ESLint 处理器让ESLint可以从其他类型的文件中提取 JavaScript 代码,然后再让ESLint 对 JavaScript 代码进行检查。当然,除了以上,也可以使用处理器在使用 ESLint 解析 JavaScript 代码之前对其进行一些其他的操作。
eslint-plugin-markdown 包含一个自定义处理器,可以在 Markdown 代码块内对 JavaScript 代码进行检查。
Env
JavaScript 生态中有多个运行时、版本、扩展和框架。每个所支持的语法和全局变量都不尽相同。指定env,ESLint便可以识别。
基本使用
创建基本配置
npm init @eslint/config
通过工具生成eslint基本配置模版
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": "eslint:recommended",
"overrides": [
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
}
}
vue-cli集成
使用vue create初始化项目
生成了为vue3定制化的配置
/* eslint-env node */
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
parserOptions: {
ecmaVersion: 'latest'
}
}
基本原理
ESLint基本架构图如下:
lib/linter/
– 这个模块是核心的 Linter
类,根据配置选项进行代码验证、检查并修复问题。这个文件不做任何文件 I/O,并且完全不与console
互动。
Linter 是 eslint 最核心的类了,它提供了这几个 api:
- 检查:verify
- 检查并修复:verifyAndFix
- 获取 AST:getSourceCode
- 定义 Parser:defineParser
- 定义 Rule:defineRule
- 获取所有的 Rule:getRules
其中SourceCode指的是AST(抽象语法树),源代码字符串通过Parser解析成AST,之后ESLint就可以通过AST提供的信息与Rules对比,从而给出代码规范分析的结果,指出错误,并且还可以自动修复。
Linter 主要的功能是在 verify 和 verifyAndFix 里实现的,当命令行指定 --fix
或者配置文件指定 fix: true
就会调用 verifyAndFix 对代码进行检查并修复,否则会调用 verify 来进行检查。
AST
ESLint拿到源代码后会进行parse操作,生成AST用于静态分析。ESLint使用的是Espree parser。
Estree是一套AST标准,Esprima基于estree标准实现了AST。Acorn,它在Exprima之后出现,也是 estree 标准的实现,但是它速度比 esprima 快,而且支持插件,可以通过插件扩展语法支持。
Espree最初Fork自Esprima,因为Acorn的各种优点现在它建立在Acorn之上。
下面简单介绍下Espree解析器下AST的几个常见的节点,也可以在estree中查看更多详情。
Literal
Literal 是字面量的意思,它的值可以是布尔、数字、字符串等。
Identifer
Identifer 是标识符的意思,变量名、属性名、参数名等各种声明和引用的名字,都是Identifer。
statement
statement 是语句,它是可以独立执行的单位,比如 break、continue、debugger、return 或者 if 语句、while 语句、for 语句,还有声明语句,表达式语句等。我们写的每一条可以独立执行的代码都是语句。语句末尾一般会加一个分号分隔,或者用换行分隔。
下面这些我们经常写的代码,每一行都是一个 Statement:
break;
continue;
return;
debugger;
throw Error();
{}
try {} catch(e) {} finally{}
for (let key in obj) {}
for (let i = 0;i < 10;i ++) {}
while (true) {}
do {} while (true)
switch (v){case 1: break;default:;}
with (a){}
Verify & Fix
PreProcess阶段
1.确定是否需要process
上面介绍过,ESLint 处理器可以从其他类型的文件中提取 JavaScript 代码,然后让 ESLint 对 JavaScript 代码进行检查,这就是processor的作用之一。例如,对vue类型文件做ESLint检查,processor就派上用场了。更详细的介绍可以看我另外一篇文章 Processor
Parse阶段
1.确定parser:
默认是 ESLint 自带的Espree,也可以通过配置来切换成别的 parser,比如 @eslint/babel-parser、@typescript/eslint-parser 等。
2.执行parse生成Source Code(AST):
检查阶段
调用rule对SourceCode检查,生成linting problems
那么是如何检查?
1.遍历AST并存储AST Node
2.遍历规则列表
为每条规则添加对应AST Node的Listener
为constructor-super规则绑定对应Listener(ReturnStatement、”Program:exit”等),当AST遍历执行到ReturnStatement类型的节点的时候便会执行constructor-super规则ReturnStatement方法里的逻辑。
3.Emit对应AST Node的Listener
这样AST Node遍历完成后也就执行所有的rules了。
在执行rules的过程中对比AST发现和rule规则不匹配,就可以添加问题到linting problems
最后生成的linting problems就是lint检查结果了。从哪一行(line)哪一列(column)到哪一行(endLine)哪一列(endColumn),有什么错误(message)。
问:在检查阶段为什么需要先存储AST Node然后再从AST Node Queue遍历来Emit Listener呢?这样不是遍历两次了吗?
因为rules一直有个小问题,node的parent属性只会在节点被遍历后才能被访问到。为了解决这个问题ESLint延迟执行了Emit,这样node parent属性就可以被访问到了。相关issue:github.com/eslint/esli…
PostProcess阶段
这个阶段主要用来对生成的linting problems做一些处理,例如过滤、修改之类的。
Fix阶段
对于可以fix的规则在lint检查完后会,linting problems里会有生成的fix信息,用于自动修复问题。
其中range表示范围,text表示替换的内容。结合到一起就是,range内的字符串替换成text即完成修复。
举个例子:
1.源代码
2.配置rule:no-extra-semi,不允许多余的分号。
3.运行ESLint检查,生成如下结构
{
fix: {
range: [9, 11],
text: ';'
}
}
表示替换源码字符串(中index从9到11的内容为’;’,即替换”;;”为”;”,替换后结果如下:
源码fix流程分析:
1.根据linting problems替换
问:这里为什么要加循环呢?
答:因为多个linting problem之间的range也就是替换的范围可能是有重叠的,如果有重叠就放到下一次来修复,下一次修复则会根据当前修复过一次的代码再继续verify,生成linting problems,以此循环直至没有problem可以修复。不过这样的循环最多修复 10 次,如果还有linting problems没修复就不修了。
2.替换算法:其实就是简单的字符串拼接与替换。
至此,ESLint工作的主要流程完成。大致流程如下:
暂时无法在{app_display_name}文档外展示此内容
官方ESLint规则分析
no-extra-semi(无效分号)
实现no-extra-semi主要是对EmptyStatement类型的节点处理。看一下EmptyStatement类型节点的定义:
EmptyStatement
interface EmptyStatement <: Statement {
type: "EmptyStatement";
}
// An empty statement, i.e., a solitary semicolon.
简而言之,就是单独的分号,正是需要报告的问题。
所以,遇到这样的分号报告问题即可。但是,所有情况都需要报告吗?
从源码里可以看到,该规则只会报告当前节点的父亲节点类型不为allowedParentTypes的情况。
举个例子:以下语句虽然分号是EmptyStatement,但是这是必要的,否则会有语法错误。
不过,EmptyStatement类型并不能涵盖到Class,例如:
针对Class,并没有EmptyStatement类型的节点可以直接检测到这么方便。Class内则要针对不同类型的节点做tokens(分词)检查,找到不合规的分号分词然后报告问题。
简单介绍下token的概念:
parse 阶段会把源码字符串转换成 AST,这个过程分为词法分析、语法分析。
比如 let a = '1';
这样一段源码,我们要先把它分成一个个不能细分的分词(token),也就是 let
, a
, =
, '1'
,这个过程是词法分析,按照单词的构成规则来拆分字符串成单词。
Token types: github.com/acornjs/aco…
class a {
constructor() {
};;
}
有了分词列表,Class内部分别对以下几种类型节点开始分词遍历检查:
– ClassBody
检查ClassBody里与第一个方法定义之间的分词是否存在分号分词。
class Test{
;
}
– MethodDefinition
– PropertyDefinition
– StaticBlock
检查以上三个节点的定义与下一个以上三个类型节点的定义之间的单词是否存在分号分词。
class Test {
a() {};
b;;
static {
// code
};
}
遍历算法:遍历token列表,检查到分号则报告问题,遇到非符号类型或遇到结束符号’}’则遍历结束。
至此,no-extra-semi规则分析完成。
应用
ESLint插件
ESLint插件可以自定义规则、配置、处理器和环境。这里只编写规则插件,展示插件自定义规则的能力。
1.使用generator-eslint生成ESLint插件基本结构
yo eslint:plugin
2.生成rule模版
yo eslint:rule
.
├── README.md
├── docs // 使用文档
│ └── rules
│ └── no-local-storage.md
├── lib // eslint 规则开发
│ ├── index.js
│ └── rules // 此目录下可以构建多个规则
│ └── no-local-storage.js
├── package.json
└── tests // 单元测试
└── lib
└── rules
└── no-local-storage.js
3.no-local-storage规则编写
要检测到有没有直接使用localStorage API十分简单,从AST上可以非常直观的看到节点类型为Identifier且name为localStorage的节点即为目标节点。
核心代码如下:
4.编写测试
npm run test
5.配置自定义插件
// .eslintrc.js
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": "eslint:recommended",
"overrides": [
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ['kevin'],
"rules": {
'no-extra-semi': 2,
'kevin/no-local-storage': 2
}
}
源代码:
let a = 2;;
localStorage
至此一个简单的ESLint插件完成。
个性化配置与共享
很多时候ESLint自带的共享配置例如:eslint:recommended,并不能满足项目个性化基础规范。这时可以定义个性化的共享配置,专门服务于一系列项目。例如:**eslint-config-elemefe**是饿了么团队定义的个性化共享配置。
定义共享配置十分简单,ESLint本身就自带许多规则,例如no-with、no-var等,只是根据规范配置相应的规则即可。
const rules = require('./rules');
module.exports = {
'root': true,
'env': {
'browser': true,
'node': true,
'amd': false,
'mocha': false,
'jasmine': false
},
'parserOptions': {
ecmaVersion: 6,
sourceType: 'module',
'ecmaFeatures': {
'experimentalObjectRestSpread': true,
'jsx': true
}
},
rules: rules
};
// rules.js
module.exports = {
'accessor-pairs': 2,
'array-bracket-spacing': 0,
'block-scoped-var': 0,
'brace-style': [2, '1tbs', { 'allowSingleLine': true }],
'camelcase': 0,
'comma-dangle': [2, 'never'],
...
}
使用也非常简单,在配置文件.eslintrc.*extends添加个性化配置即可。
{
"extends": "elemefe"
}
ESLint插件与可共享配置的区别:
可共享配置简单来说就是一组文件规范,应用某个共享配置,则会应用该共享配置下的所有配置。而插件可以包括规则、配置、处理器和环境,仅有一个插件不能执行任何规则,它需要手动选择插件对应规则才可以生效。可以说插件包含可共享配置,所以插件的功能对比可共享配置而言是更强大的。
社区实践
eslint-plugin-vue
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update its content execute "npm run update"
*/
'use strict'
module.exports = {
rules: {
'array-bracket-newline': require('./rules/array-bracket-newline'),
'array-bracket-spacing': require('./rules/array-bracket-spacing'),
'array-element-newline': require('./rules/array-element-newline'),
...
},
configs: {
base: require('./configs/base'),
essential: require('./configs/essential'),
'no-layout-rules': require('./configs/no-layout-rules'),
recommended: require('./configs/recommended'),
'strongly-recommended': require('./configs/strongly-recommended'),
'vue3-essential': require('./configs/vue3-essential'),
'vue3-recommended': require('./configs/vue3-recommended'),
'vue3-strongly-recommended': require('./configs/vue3-strongly-recommended')
},
processors: {
'.vue': require('./processor')
},
environments: {
// TODO Remove in the next major version
/** @deprecated */
'setup-compiler-macros': {
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly'
}
}
}
}
Vue为了实现定制化的ESLint检查,创建了eslint-plugin-vue的插件。这个插件功能比较强大,包含了规则、配置、处理器和环境。还记得最开始用vue create生成的默认eslint配置吗?默认使用的就是configs/essential中的配置。
总结
ESLint主要有Rules、Configurations Files、Shareable Configurations、Plugins、Parsers、Custom Processors七个核心概念,理解这几个概念后,使用ESLint非常简单,只需要根据需求定义配置文件内容即可。
如果社区资源无法满足需求,需要编写自定义规则、处理器等,这就需要对ESLint基本原理有所理解。这里简单介绍了AST以及简单分析了ESLint的基本原理。ESLint执行主要分为PrePorcess、Parse、检查、PostProcess、Fix五个阶段,每个阶段都可以自定义。
最后,对官方ESLint规则做了简单分析,了解了ESLint插件与个性化配置与共享如何编写以及简单了解了下社区实践。