创建工程
我们创建一个叫eslint-demo的工程,然后执行npm初始化
https://github.com/913453448/eslint-demo
npm init
安装使用
安装eslint
$ npm install eslint --save-dev
创建配置文件
执行eslint的初始化
npx eslint --init
执行完毕后可以看到一个配置文件,我选的是json格式的配置文件,还有package.json中直接引用、yaml、js格式的配置文件,后面我们会讲到。
.eslintrc.json
{
}
可以看到,我们这里是一个空的配置文件。
运行命令
我们创建一个叫src的目录,然后创建一个demo1.js的文件测试
demo1.js:
我们随便写点代码,比如直接document页面输出一个字符串
document.write("hello eslint");
运行eslint测试:
npx eslint ./src/demo1.js
运行完毕后你会发现,没有报错跟提示,这是因为我们还没有进行任何eslint的配置,下面我们就结合demo对eslint配置逐个进行解析。
为了更好的理解eslint,我们直接clone一份源码,https://github.com/eslint/eslint.git
配置
我们这里以一个前端vue+webpack+ts+es2020的工程为demo为例子进行eslint的配置。
env&parserOptions
ESLint 允许你指定你想要支持的 JavaScript 语言选项。默认情况下,ESLint 支持 ECMAScript 5 语法。你可以覆盖该设置,以启用对 ECMAScript 其它版本和 JSX 的支持,为什么要把env跟parserOptions放在一起讲呢? 因为env中包含了对parserOptions的配置,最终两个参数传入供给parse解析器使用。
env
我们的demo需要运行在es2020的浏览器环境中,所以我们的env配置为:
{
"env": {
"browser": true,
"es2020": true
}
}
那么,env到底可以为设置为哪些呢?我们直接找到eslint的源码。
conf/environments.js:
...
const newGlobals2015 = getDiff(globals.es2015, globals.es5); // 19 variables such as Promise,
const newGlobals2017 = {
Atomics: false,
SharedArrayBuffer: false
};
const newGlobals2020 = {
BigInt: false,
BigInt64Array: false,
BigUint64Array: false,
globalThis: false
};
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/** @type {Map<string, import("../lib/shared/types").Environment>} */
module.exports = new Map(Object.entries({
// Language
builtin: {
globals: globals.es5
},
es6: {
globals: newGlobals2015,
parserOptions: {
ecmaVersion: 6
}
},
es2015: {
globals: newGlobals2015,
parserOptions: {
ecmaVersion: 6
}
},
es2017: {
globals: {
...newGlobals2015, ...newGlobals2017 },
parserOptions: {
ecmaVersion: 8
}
},
es2020: {
globals: {
...newGlobals2015, ...newGlobals2017, ...newGlobals2020 },
parserOptions: {
ecmaVersion: 11
}
},
// Platforms
browser: {
globals: globals.browser
},
node: {
globals: globals.node,
parserOptions: {
ecmaFeatures: {
globalReturn: true
}
}
},
"shared-node-browser": {
globals: globals["shared-node-browser"]
},
worker: {
globals: globals.worker
},
serviceworker: {
globals: globals.serviceworker
},
// Frameworks
commonjs: {
globals: globals.commonjs,
parserOptions: {
ecmaFeatures: {
globalReturn: true
}
}
},
amd: {
globals: globals.amd
},
mocha: {
globals: globals.mocha
},
jasmine: {
globals: globals.jasmine
},
jest: {
globals: globals.jest
},
phantomjs: {
globals: globals.phantomjs
},
jquery: {
globals: globals.jquery
},
qunit: {
globals: globals.qunit
},
prototypejs: {
globals: globals.prototypejs
},
shelljs: {
globals: globals.shelljs
},
meteor: {
globals: globals.meteor
},
mongo: {
globals: globals.mongo
},
protractor: {
globals: globals.protractor
},
applescript: {
globals: globals.applescript
},
nashorn: {
globals: globals.nashorn
},
atomtest: {
globals: globals.atomtest
},
embertest: {
globals: globals.embertest
},
webextensions: {
globals: globals.webextensions
},
greasemonkey: {
globals: globals.greasemonkey
}
}));
可以看到,默认是“builtin” 也就是es5,我们可以看到“es6”还可以叫“es2015”,然后还有一个“parserOptions”的配置:
es2015: {
globals: newGlobals2015,
parserOptions: {
ecmaVersion: 6
}
},
那么parserOptions到底是什么呢? 其实是给解析器用的参数,告诉解析器你需要利用ecmaVersion:6的语法去解析我们的源文件,那么globals属性里面又是什么东西呢?我们直接找到newGlobals2015然后点开源码,我们找到一个叫globals的第三方库,然后找到了一个叫globals.json的文件:
{
...
"es2015": {
"Array": false,
"ArrayBuffer": false,
"Boolean": false,
"constructor": false,
"DataView": false,
"Date": false,
"decodeURI": false,
"decodeURIComponent": false,
"encodeURI": false,
"encodeURIComponent": false,
"Error": false,
"escape": false,
"eval": false,
"EvalError": false,
"Float32Array": false,
"Float64Array": false,
"Function": false,
"hasOwnProperty": false,
"Infinity": false,
"Int16Array": false,
"Int32Array": false,
"Int8Array": false,
"isFinite": false,
"isNaN": false,
"isPrototypeOf": false,
"JSON": false,
"Map": false,
"Math": false,
"NaN": false,
"Number": false,
"Object": false,
"parseFloat": false,
"parseInt": false,
"Promise": false,
"propertyIsEnumerable": false,
"Proxy": false,
"RangeError": false,
"ReferenceError": false,
"Reflect": false,
"RegExp": false,
"Set": false,
"String": false,
"Symbol": false,
"SyntaxError": false,
"toLocaleString": false,
"toString": false,
"TypeError": false,
"Uint16Array": false,
"Uint32Array": false,
"Uint8Array": false,
"Uint8ClampedArray": false,
"undefined": false,
"unescape": false,
"URIError": false,
"valueOf": false,
"WeakMap": false,
"WeakSet": false
},
...
}
可以看到,其实就是我们es6中内置的对象、属性、方法,所以env是提供了一个es环境,parserOptions则是负责解析es语法。我们可以看到每一个变量都是一个boolean值,false
代表这个变量不允许修改,true
代表可以修改。
我们继续运行一下我们的demo:
$npx eslint ./src/demo1.js
我们可以发现,我们控制台还是没啥反应,这是为什么呢?因为我们还没配置我们的rules,我们继续往下走。
parserOptions
sourceType
- 设置为"script"
(默认) 或"module"
(如果你的代码是 ECMAScript 模块)。
在看env的时候我们看到了parserOptions的一个参数“ecmaVersion”,那么ecmaVersion还有哪些配置呢?
ecmaVersion:
globalReturn
- 允许在全局作用域下使用return
语句impliedStrict
- 启用全局 strict mode (如果ecmaVersion
是 5 或更高)jsx
- 启用 JSXexperimentalObjectRestSpread
- 启用实验性的 object rest/spread properties 支持。(**重要:**这是一个实验性的功能,在未来可能会有明显改变。 建议你写的规则 不要 依赖该功能,除非当它发生改变时你愿意承担维护成本。)
我们可能会用到jsx,所以我们把jsx
配置成为true
.eslintrc.json:
{
"env": {
"browser": true,
"es2020": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
},
}
因为我们demo需要用到webpack模块打包,所以我们需要把sourceType
设置成为module
{
"env": {
"browser": true,
"es2020": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
}
}
解析器(Parser)
抽象语法树(Abstract Syntax Tree,AST),parse会把我们的源代码转换成抽象语法树,然后再对每个节点做eslint的校验,可以说是eslint中最重要的模块了,eslint默认使用的Esprima作为ast工具,ast的工具还有acorn等,之前写过一篇关于babel的文章,感兴趣的小伙伴可以去看看babel源码解析一,里面用到的就是acorn。
{
"parser": "esprima",
"rules": {
"semi": "error"
}
}
以下解析器与 ESLint 兼容:
- Esprima
- Babel-ESLint - 一个对Babel解析器的包装,使其能够与 ESLint 兼容。
- @typescript-eslint/parser - 将 TypeScript 转换成与 estree 兼容的形式,以便在ESLint中使用。
注意,在使用自定义解析器时,为了让 ESLint 在处理非 ECMAScript 5 特性时正常工作,配置属性 parserOptions
仍然是必须的。解析器会被传入 parserOptions
,但是不一定会使用它们来决定功能特性的开关。
在线转换工具:https://astexplorer.net/
打开esprima的文档我们简单看一下:
README.md
```javascript
const espree = require("espree");
const ast = espree.parse(code, options);
```js
const options = {
// attach range information to each node
range: false,
// attach line/column location information to each node
loc: false,
// create a top-level comments array containing all comments
comment: false,
// create a top-level tokens array containing all tokens
tokens: false,
// Set to 3, 5 (default), 6, 7, 8, 9, or 10 to specify the version of ECMAScript syntax you want to use.
// You can also set to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), or 2020 (same as 11) to use the year-based naming.
ecmaVersion: 5,
// specify which type of script you're parsing ("script" or "module")
sourceType: "script",
// specify additional language features
ecmaFeatures: {
// enable JSX parsing
jsx: false,
// enable return in global scope
globalReturn: false,
// enable implied strict mode (if ecmaVersion >= 5)
impliedStrict: false
}
}
```
可以看到,esprima接受的其实就是我们传递的parserOptions参数,那么在eslint的配置中我们怎么使用parse呢?
因为我们demo是需要加载.vue文件的,所以用默认的esprima解析肯定是不行的,所以我们安装一个vue-eslint-parser,
因为vue-eslint-parser直接是包含在eslint-plugin-vue中的,所以我们直接安装一个eslint-plugin-vue:
npm install -D eslint-plugin-vue
然后直接把vue-eslint-parser配置到eslint中
.eslintrc.json:
{
"env": {
"browser": true,
"es2020": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"parser": "vue-eslint-parser"
}
我们继续运行eslint:
npx eslint ./src/*
运行后还是没任何反应,这是为什么呢? 别慌,还没到我们的rules,我们继续~
处理器(Processor)
processor可以理解为在parse解析器要解析源文件之前跟eslint的rules处理过后的构造函数,也就是说可以在parse解析之前跟eslint的rules处理过后做一些事情,processor提供了两个钩子函数,我们先看一眼eslint-plugin-vue中提供的processor:
xxx/node_modules/eslint-plugin-vue/lib/processor.js
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/
'use strict'
module.exports = {
preprocess (code) {
console.log("code",code)
return [code]
},
postprocess (messages) {
const state = {
block: {
disableAll: false,
disableRules: new Set()
},
line: {
disableAll: false,
disableRules: new Set()
}
}
// Filter messages which are in disabled area.
return messages[0].filter(message => {
if (message.ruleId === 'vue/comment-directive') {
const rules = message.message.split(' ')
const type = rules.shift()
const group = rules.shift()
switch (type) {
case '--':
state[group].disableAll = true
break
case '++':
state[group].disableAll = false
break
case '-':
for (const rule of rules) {
state[group].disableRules.add(rule)
}
break
case '+':
for (const rule of rules) {
state[group].disableRules.delete(rule)
}
break
case 'clear':
state.block.disableAll = false
state.block.disableRules.clear()
state.line.disableAll = false
state.line.disableRules.clear()
break
}
return false
} else {
return !(
state.block.disableAll ||
state.line.disableAll ||
state.block.disableRules.has(message.ruleId) ||
state.line.disableRules.has(message.ruleId)
)
}
})
},
supportsAutofix: true
}
里面的具体代码我们就一一解析了,我们后面在做自定义plugin的时候会详细说明一下processor,我们可以简单的看到两个回调函数,preprocess跟postprocess,preprocess是在parse解析源文件之前调用的方法,postprocess则是eslint的rules处理完毕后的回调函数。
那我们用一下eslint-plugin-vue的processor.
如果插件提供了processor的话,eslint会自动根据文件后缀调用processor,比如在eslint-plugin-vue的清单文件中我们可以看到:
xxx/node_modules/eslint-plugin-vue/lib/index.js
/*
* IMPORTANT!
* This file has been automatically generated,
* in order to update it's content execute "npm run update"
*/
'use strict'
module.exports = {
rules: {
....
},
configs: {
'base': require('./configs/base'),
'essential': require('./configs/essential'),
'no-layout-rules': require('./configs/no-layout-rules'),
'recommended': require(