解析js文件
eslint本质上是遍历ast,在遇到各种节点时,会走各类定义好的规则校验,这些规则代码就是访问并校验ast节点的。因此首先需要得到ast,借助的就是各种解析器。
eslint默认通过Espree作为解析器解析js。
但Espree不支持最新的ecmascript语法,因此一般会使用@babel/eslint-parser代替。.eslintrc.js中配置如下:
module.exports = {
parser: "@babel/eslint-parser",
parserOptions: {
sourceType: "module",
allowImportExportEverywhere: false,
ecmaFeatures: {
globalReturn: false,
},
babelOptions: {
configFile: path.resolve(__dirname, '.babelrc') // 引用项目中的.babelrc配置
}
}
}
如果项目中使用了typescript,则替换为@typescript-eslint/parser
一个文件经过解析器,会返回一个ast语法树
遍历语法树
traverse(node, options) {
this._current = null;
this._parents = [];
this._skipped = false;
this._broken = false;
this._visitorKeys = options.visitorKeys || vk.KEYS;
this._enter = options.enter || noop;
this._leave = options.leave || noop;
this._traverse(node, null);
}
_traverse(node, parent) {
if (!isNode(node)) {
return;
}
this._current = node;
this._skipped = false;
this._enter(node, parent);
if (!this._skipped && !this._broken) {
const keys = getVisitorKeys(this._visitorKeys, node);
if (keys.length >= 1) {
this._parents.push(node);
for (let i = 0; i < keys.length && !this._broken; ++i) {
const child = node[keys[i]];
if (Array.isArray(child)) {
for (let j = 0; j < child.length && !this._broken; ++j) {
this._traverse(child[j], node);
}
} else {
this._traverse(child, node);
}
}
this._parents.pop();
}
}
if (!this._broken) {
this._leave(node, parent);
}
this._current = parent;
}
挨个节点访问规则,并在校验不通过时抛错
zhuanlan.zhihu.com/p/53680918 这篇文章中 ESLint 是怎么看待我们传入的 rule 的 这一节中说的比较清楚,这里不再复述
分析代码是否能被访问到
eslint-plugin-vue
类似于这种有特殊模版的插件,都是先通过定制的解析器,分离js和模版,js生成ast,模版单独解析成ast并挂载在js的ast上,比如vue-eslint-parser是将模版语法树使用属性templateBody挂在跟语法树上
plugins
.eslintrc.js中可以配置插件,比如
module.exports = {
extends: [
'plugin:vue/essential'
],
plugins: [
//这里vue是eslint-plugin-vue的缩写
//只要eslint-plugin-xx这种格式的插件这里就可以简写为xx
'vue'
],
rules: {
'no-unused-vars': 2,
'no-debugger': 2
}
}
自动修复
基本原理是eslint提供了增、删、替换ast节点的能力,和一个修复函数fix,在修复函数中对ast进行操纵实现修复
extends
module.exports = {
extends: [
'plugin:vue/essential',
'standard'
],
plugins: [
'vue'
]
}
这里有几种情况:
- 如果使用
eslint-config-xx这种格式的配置,就可以简写为xx eslint默认有两种可继承的配置:eslint:recommended、eslint:all,定义在eslint-recommended.js和eslint-all.js- 如果是自定义插件,也可以生成一些可用于继承的配置集,在插件的入口文件中声明
configs,比如eslint-plugin-vue的:
module.exports = {
rules: {
// 自定义规则,在这里声明
...
},
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: {
// 声明处理器,用于预处理代码和处理lint结果
...
},
environments: {
...
}
}
使用
仅使用的话直接eslint --init,然后根据提示一步步选择就行
有不懂的地方,官网其实写的很清楚了
英文官网
vscode校验原理
vscode中使用vetur提供eslint能力
源码如下:
import { ESLint, Linter } from 'eslint';
import { configs } from 'eslint-plugin-vue';
import { Diagnostic, Range, DiagnosticSeverity } from 'vscode-languageserver-types';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import { resolve } from 'path';
import { VueVersion } from '../../../utils/vueVersion';
function toDiagnostic(error: Linter.LintMessage): Diagnostic {
const line = error.line - 1;
const column = error.column - 1;
const endLine = error.endLine ? error.endLine - 1 : line;
const endColumn = error.endColumn ? error.endColumn - 1 : column;
return {
range: Range.create(line, column, endLine, endColumn),
message: `[${error.ruleId}]\n${error.message}`,
source: 'eslint-plugin-vue',
severity: error.severity === 1 ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error
};
}
export async function doESLintValidation(document: TextDocument, engine: ESLint): Promise<Diagnostic[]> {
const rawText = document.getText();
// skip checking on empty template
if (rawText.replace(/\s/g, '') === '') {
return [];
}
const text = rawText.replace(/ {10}/, '<template>') + '</template>';
const report = await engine.lintText(text, { filePath: document.uri });
return report?.[0]?.messages?.map(toDiagnostic) ?? [];
}
export function createLintEngine(vueVersion: VueVersion) {
const SERVER_ROOT = __dirname;
const versionSpecificConfig: Linter.Config =
vueVersion === VueVersion.V30 ? configs['vue3-essential'] : configs.essential;
if (vueVersion === VueVersion.V30) {
versionSpecificConfig.parserOptions = {
...versionSpecificConfig.parserOptions,
vueFeatures: {
...versionSpecificConfig.parserOptions?.vueFeatures,
interpolationAsNonHTML: true
}
};
}
const baseConfig: Linter.Config = configs.base;
baseConfig.ignorePatterns = ['!.*'];
return new ESLint({
useEslintrc: false,
cwd: SERVER_ROOT,
baseConfig,
overrideConfig: versionSpecificConfig
});
}
首先,引用eslint-plugin-vue,并使用其配置初始化eslint插件
然后doESLintValidation这个函数里调用上一步生成的eslint对象的lintText方法校验文本
eslint配置流程如果按官网的来,或者修改了全局eslint配置,但不生效,可以尝试重启vscode
eslint/babel插件的对比
相同点
- 都是基于访问语法树节点的方式,语法树都是相同的,可以去astexplorer上查看语法树
不同点
-
访问节点方式
// babel ImportDeclaration: { enter() {} exit() {} } // eslint ImportDeclaration(){} 'ImportDeclaration:exit'(){}此外eslint插件支持对路径节点的访问code-path
-
babel有提供很多辅助能力
- babel-types用于判断和构造节点类型,eslint只能根据节点结构中的属性去判断,也不支持快捷构造节点
- babel-template快速构建节点树
- babel在node和path上提供了便捷的查找、新增、修改、替换等方法
-
babel可以直接修改ast
在babel插件中可以直接操作ast,但eslint中不能直接修改ast,只能在fix方法中提供修改ast的入口
当然这跟两者的功能区分有关系,babel插件就是用来修改代码的,而eslint本质上只是提供校验,fix方法是用于修复问题代码的,所以才能修改ast