写在前面
以下是我阅读eslint源码的过程 , 在这过程中 , 我首先会自己写一个eslint插件的demo , 然后自己定义一个规则 , 然后再进行检测 , 根据调用栈迅速的一步一步看下去 , 大致知道是怎么样的流程后 ; 接着再重新拆分每一步是怎么做的 , 分析规则和插件的运用 , 从而更加巩固自己对于eslint插件的开发 ; 基于这个想法 , 我们就开始吧
在大致流程中会交代eslint的修复过程 , 但是也是大致的说明一下 ; 详细拆分的过程是没有分析修复过程的
先上github上面把eslint源码clone下来eslint , git clone https://github.com/eslint/eslint.git
第一节 . 大致流程
1. 找到eslint命令入口文件
打开源码 , 我们通过package.json查看eslint的命令入口 , 在bin下的eslint.js
{
"bin": {
"eslint": "./bin/eslint.js"
}
}
2. 进入./bin/eslint.js
"use strict";
require("v8-compile-cache");
// 读取命令中 --debug参数, 并输出代码检测的debug信息和每个插件的耗时
if (process.argv.includes("--debug")) {
require("debug").enable("eslint:*,-eslint:code-path,eslintrc:*");
}
// 这里省略了readStdin getErrorMessage onFatalError 三个方法
// 主要看下面IIFE , 而且这个是用了一个promise包裹 , 并且有捕捉函数的一个IIFE
(async function main() {
process.on("uncaughtException", onFatalError);
process.on("unhandledRejection", onFatalError);
// Call the config initializer if `--init` is present.
if (process.argv.includes("--init")) {
await require("../lib/init/config-initializer").initializeConfig();
return;
}
// 最终这里读取了 lib/cli, lib/cli才是执行eslint开始的地方
process.exitCode = await require("../lib/cli").execute(
process.argv,
process.argv.includes("--stdin") ? await readStdin() : null
);
}()).catch(onFatalError);
2.1 lib/cli执行脚本文件
// 其他代码
const cli = {
// args 就是那些 --cache --debug等参数
async execute(args, text) {
/** @type {ParsedCLIOptions} */
// 开始对参数格式化
let options;
try {
options = CLIOptions.parse(args);
} catch (error) {
log.error(error.message);
return 2;
}
// 获取eslint编译器实例
const engine = new ESLint(translateOptions(options));
// results作为接收收集问题列表的变量
let results;
if (useStdin) {
results = await engine.lintText(text, {
filePath: options.stdinFilename,
warnIgnored: true
});
} else {
// 进入主流程
results = await engine.lintFiles(files);
}
// printResults进行命令行输出
let resultsToPrint = results;
if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) {
const { errorCount, fatalErrorCount, warningCount } = countErrors(results);
// 判断是否有出错的退出码
// ...
}
return 2;
}
};
module.exports = cli;
3. 如何fix
以字符串为例
比如我们写了一个自定义的eslint插件如下
replaceXXX.js 看代码块replaceXXX
// 代码块replaceXXX
module.exports = {
meta: {
type: 'problem', // "problem": 指的是该规则识别的代码要么会导致错误,要么可能会导致令人困惑的行为。开发人员应该优先考虑解决这个问题。
docs: {
description: 'XXX 不能出现在代码中!',
category: 'Possible Errors', // eslint规则首页的分类: Possible Errors、Best Practices、Strict Mode、Varibles、Stylistic Issues、ECMAScript 6、Deprecated、Removed
recommended: false, // "extends": "eslint:recommended"属性是否启用该规则
url: '', // 指定可以访问完整文档的URL
},
fixable: 'code', // 该规则是否可以修复
schema: [
{
type: 'string',
},
],
messages: {
unexpected: '错误的字符串XXX, 需要用{{argv}}替换',
},
},
create: function (context) {
const str = context.options[0];
function checkLiteral(node) {
if (node.raw && typeof node.raw === 'string') {
if (node.raw.indexOf('XXX') !== -1) {
context.report({
node,
messageId: 'unexpected',
data: {
// 占位数据
argv: str,
},
fix: fixer => {
// 这里获取到字符串中的XXX就会直接替换掉
return fixer.replaceText(node, str);
},
});
}
}
}
return {
Literal: checkLiteral,
};
},
};
4. 插件使用说明
因为在本地中使用 , 所以插件使用的是用的是npm link模式
my-project下的.eslintrc.json
{
//..其他配置
"plugins": [
// ...
"eslint-demo"
],
"rules": {
"eslint-demo/eslint-demo": ["error", "LRX"], // 将项目中所有的XXX字符串转换成MMM
}
}
my-project/app.js
// app.js
function foo() {
const bar = 'XXX';
console.log(name);
}
在my-project中使用 , 即可修复完成
npx eslint --fix ./*.js
4.1 那源码中是如何fix的呢?
eslint的fix就是执行了插件文件里面create方法如下
create: function (context) {
// 获取目标项目中.eslintrc.json文件下的rules的第二个参数
const str = context.options[0];
context.report({
// ...
fix: fixer => {
return fixer.replaceText(node, `'${str}'`);
},
})
}
在eslint源码中fix过程的代码在lib/linter/source-code-fixer.js和lib/linter/linter.js , 而lib/linter/linter.js文件是验证我们的修复代码的文件是否合法以及接收修复后的文件 ;
4.2 lib/linter/linter.js , fix方面的源码
verifyAndFix(text, config, options) {
let messages = [],
fixedResult,
fixed = false,
passNumber = 0, // 记录修复次数, 这里会和最大修复次数10次比较, 大于10次或者有修复完成的标志即可停止修复
currentText = text; // 修复前的源码字符串
const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`;
const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true;
// 每个问题循环修复10次以上或者已经修复完毕fixedResult.fixed, 即可判定为修复完成
do {
passNumber++;
debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
messages = this.verify(currentText, config, options);
debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
// 执行修复代码
fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);
/*
* stop if there are any syntax errors.
* 'fixedResult.output' is a empty string.
*/
if (messages.length === 1 && messages[0].fatal) {
break;
}
// keep track if any fixes were ever applied - important for return value
fixed = fixed || fixedResult.fixed;
// update to use the fixed output instead of the original text
currentText = fixedResult.output;
} while (
fixedResult.fixed &&
passNumber < MAX_AUTOFIX_PASSES
);
/*
* If the last result had fixes, we need to lint again to be sure we have
* the most up-to-date information.
*/
if (fixedResult.fixed) {
fixedResult.messages = this.verify(currentText, config, options);
}
// ensure the last result properly reflects if fixes were done
fixedResult.fixed = fixed;
fixedResult.output = currentText;
return fixedResult;
}
4.2.1 lib/linter/source-code-fixer.js , 修复代码的主文件
/*
这里会进行一些简单的修复, 如果是一些空格换行, 替换等问题, 这里会直接通过字符串拼接并且输出一个完整的字符串
*/
SourceCodeFixer.applyFixes = function(sourceText, messages, shouldFix) {
debug("Applying fixes");
if (shouldFix === false) {
debug("shouldFix parameter was false, not attempting fixes");
return {
fixed: false,
messages,
output: sourceText
};
}
// clone the array
const remainingMessages = [],
fixes = [],
bom = sourceText.startsWith(BOM) ? BOM : "",
text = bom ? sourceText.slice(1) : sourceText;
let lastPos = Number.NEGATIVE_INFINITY,
output = bom;
// 命中并修复问题
/*
problem的结构为
{
ruleId: 'eslint-demo/eslint-demo', // 插件名称
severity: 2,
message: '错误的字符串XXX, 需要用MMM替换', // 提示语
line: 17, // 行数
column: 18, // 列数
nodeType: 'Literal', // 当前节点在AST中是什么类型
messageId: 'unexpected', 对应meta.messages.XXX,message可以直接用message替换
endLine: 17, // 结尾的行数
endColumn: 23, // 结尾的列数
fix: { range: [ 377, 382 ], text: "'MMM'" } // 该字符串在整个文件字符串中的位置
}
*/
function attemptFix(problem) {
const fix = problem.fix;
const start = fix.range[0]; // 记录修复的起始位置
const end = fix.range[1]; // 记录修复的结束位置
// 如果重叠或为负范围,则将其视为问题
if (lastPos >= start || start > end) {
remainingMessages.push(problem);
return false;
}
// 移除非法结束符.
if ((start < 0 && end >= 0) || (start === 0 && fix.text.startsWith(BOM))) {
output = "";
}
// 拼接修复后的结果, output是一个全局变量
output += text.slice(Math.max(0, lastPos), Math.max(0, start));
output += fix.text;
lastPos = end;
return true;
}
/*
传进来的messages每一项
{
ruleId: 'eslint-demo/eslint-demo',
severity: 2,
message: '错误的字符串XXX, 需要用MMM替换',
line: 17,
column: 18,
nodeType: 'Literal',
messageId: 'unexpected',
endLine: 17,
endColumn: 23,
fix: { range: [Array], text: "'MMM'" }
},
*/
messages.forEach(problem => {
if (Object.prototype.hasOwnProperty.call(problem, "fix")) {
fixes.push(problem);
} else {
remainingMessages.push(problem);
}
});
// 当fixes有需要修复的方法则进行修复
if (fixes.length) {
debug("Found fixes to apply");
let fixesWereApplied = false;
for (const problem of fixes.sort(compareMessagesByFixRange)) {
if (typeof shouldFix !== "function" || shouldFix(problem)) {
attemptFix(problem);
// attemptFix方法唯一失败的一次是与之前修复的发生冲突, 这里默认将已经修复好的标志设置为true
fixesWereApplied = true;
} else {
remainingMessages.push(problem);
}
}
output += text.slice(Math.max(0, lastPos));
return {
fixed: fixesWereApplied,
messages: remainingMessages.sort(compareMessagesByLocation),
output
};
}
debug("No fixes to apply");
return {
fixed: false,
messages,
output: bom + text
};
};
二 . 详细流程
1. 项目代码准备
首先我们准备我们需要检测的工程结构如下
├── src
│ ├── App.tsx
│ ├── index.tsx
│ └── typings.d.ts
├── .eslintignore
├── .eslintrc.json
├── ....其他文件
└── package.json
1.1 App.tsx
import React from 'react';
function say() {
const name = 'XXX';
console.log(name);
}
const App = () => {
say();
return <div>app</div>;
};
export default App;
1.2 .eslintrc.json
{
"root": true,
"extends": [
"airbnb",
"airbnb/hooks",
"airbnb-typescript",
"plugin:react/recommended",
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
},
"plugins": [
"eslint-demo" /*这里是我们自定义的eslint插件*/
],
"rules": {
"react/function-component-definition": ["error", {
"namedComponents": "arrow-function"
}],
"strict": ["error", "global"],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"eslint-demo/eslint-demo": ["error", "LRX"], /*这里是我们自定义的eslint插件如何替换规则*/
}
}
1.3 eslint自定义插件
使用上面大致流程的eslint的自定义插件
2. 代码流程
当我们输入 npx eslint ./src/*.tsx
的时候做了什么呢
2.1 第一层bin/eslint.js
入口文件 在 ./bin/eslint.js
, 在这个文件中通过一个匿名自执行promise函数 , 引入了 lib/cli文件并且通过一下代码块001
// 代码块001
process.exitCode = await require("../lib/cli").execute(
process.argv,
process.argv.includes("--stdin") ? await readStdin() : null
);
2.2 进入第二层lib/cli
lib/cli文件的execute方法(查看代码块003) , 主要是返回一个退出码 , 用作判断eslint是否执行完毕 , 传入的参数是我们npx eslint --fix ./src
的fix这个参数 , 以及跟在--stdin后面的参数 ; 然后我们进入lib/cli , 因为上面直接调用了execute方法 , 那么我们就看看cli里面execute方法 , 首先定义了一个options
参数 , 然后调用了CLIOptions.parse(args)
方法 (查看代码块003), 这个方法是其他包optionator里面的方法 , 我们进入进去就可以看到parse的方法了 , 这个方法就是switch case将不同的参数处理装箱打包进行返回 , 这里面还用了一个.ls
包进行map管理 , 且在保证用户输入的时候用了type-check
这个包进行输入和测试进行管理 , 在非ts环境下 , 进行类似Haskell的类型语法检查 ; 好这时候我们拿到了经过装箱打包的options
了 , 这个key名不是我起的 , 它源码就这样(好随便啊) ; 得到了如下结构, (看代码块002)
// 代码块002
{
eslintrc: true,
ignore: true,
stdin: false,
quiet: false,
maxWarnings: -1,
format: 'stylish',
inlineConfig: true,
cache: false,
cacheFile: '.eslintcache',
cacheStrategy: 'metadata',
init: false,
envInfo: false,
errorOnUnmatchedPattern: true,
exitOnFatalError: false,
_: [ './src/App.tsx', './src/index.tsx' ]
}
得到这个结构后 , 就通过转换translateOptions
函数进行配置的转换 , 这里我猜是因为一些人接手别人的代码 , 需要写的一个转换文件 ; 接着开始创建我们的一个eslint的编译器 const engine = new ESLint(translateOptions(options));
2.3 进入第三层lib/eslint/eslint.js
在lib/eslint/eslint.js里面的Eslint类 , Eslint这个类的构造函数首先会将所有的配置进行检验 , 在ESLint类里面会创建一个cli的编译器 ,
2.4 进入第四层lib/cli-engine/cli-engine.js
这个编译器在lib/cli-engine/cli-engine.js里面 , 这里主要是处理一下缓存以及eslint内部的默认规则 ; 然后回来lib/eslint/eslint.js里面的Eslint类 , 接下来就是获取从cli-engine.js的内部插槽 , 设置私有的内存快照 , 判断是否更新 ,如果更新就删除缓存 ;
2.5 进入第五层lib/linter/linter.js
这一层就比较简单了 , 就是用了map结构记录了cwd , lastConfigArray , lastSourceCode , parserMap , ruleMap , 分别是当前文件路径 , 最新的配置数据 , 最新的源码使用编译器espree解析出来的ast源码字符串 , 编译器(记录我们用的是什么编译器默认是espree) , 以及规则map
2.6 返回到第二层
接着返回到第二层继续走下去 , 因为不是使用--stdin , 所以直接看else , 执行了engine.lintFiles
// 代码块003
const cli = {
// 其他方法
async execute() {
let options;
try {
options = CLIOptions.parse(args);
} catch (error) {
log.error(error.message);
return 2;
}
// 其他验证参数的代码
const engine = new ESLint(translateOptions(options));
let results;
if (useStdin) {
results = await engine.lintText(text, {
filePath: options.stdinFilename,
warnIgnored: true
});
} else {
results = await engine.lintFiles(files);
}
let resultsToPrint = results;
if (options.quiet) {
debug("Quiet mode enabled - filtering out warnings");
resultsToPrint = ESLint.getErrorResults(resultsToPrint);
}
// 最后会来到这里printResults方法, 我们看代码块012
if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) {
// Errors and warnings from the original unfiltered results should determine the exit code
const { errorCount, fatalErrorCount, warningCount } = countErrors(results);
const tooManyWarnings =
options.maxWarnings >= 0 && warningCount > options.maxWarnings;
const shouldExitForFatalErrors =
options.exitOnFatalError && fatalErrorCount > 0;
if (!errorCount && tooManyWarnings) {
log.error(
"ESLint found too many warnings (maximum: %s).",
options.maxWarnings
);
}
if (shouldExitForFatalErrors) {
return 2;
}
return (errorCount || tooManyWarnings) ? 1 : 0;
}
return 2;
}
}
从第二层中可以看到 , engine是通过ESLint类创建出来的所以我们去到第三层的lib/eslint/eslint.js的lintFiles方法 , (看代码块004)
// 代码块004
async lintFiles(patterns) {
if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) {
throw new Error("'patterns' must be a non-empty string or an array of non-empty strings");
}
// privateMembersMap在new ESLint构造函数的时候已经将cliEngine, set进去了, 所以这里直接获取即可
const { cliEngine } = privateMembersMap.get(this);
// processCLIEngineLintReport是返回linting指定的文件模式, 传入的参数是cliEngine, 并且第二个参数执行了executeOnFiles, 我们看看cliEngine这个类的executeOnFiles做了什么
return processCLIEngineLintReport(
cliEngine,
cliEngine.executeOnFiles(patterns)
);
}
2.7 再次进入第四层
再次进入第四层的CLIEngine类下的executeOnFiles方法, 从他接受的参数和方法名可以知道, 这个executeOnFiles主要是处理文件和文件组的问题 , 看代码块005
// 代码块005
executeOnFiles(patterns) {
const results = [];
// 这里是一个很迷的操作, 官方在这里手动把所有的最新配置都清除了, 这个是从外部传进来的, 但是它先手动清除然后下面再在迭代器里面每个都引用一遍,
lastConfigArrays.length = 0;
//... 其他函数
// 清除上次使用的配置数组。
// 清除缓存文件, 使用fs.unlinkSync进行缓存文件的请求, 当不存在此类文件或文件系统为只读(且缓存文件不存在)时忽略错误
// 迭代源文件并且放到results中
// fileEnumerator.iterateFiles(patterns), 这里的patterns还是一个需要eslint文件的绝对地址, 这时候还没有进行ast分析, fileEnumerator.iterateFiles这个方法是一个迭代器, 为了防止读写文件的时候有延迟, 这里需要使用迭代器
for (const { config, filePath, ignored } of fileEnumerator.iterateFiles(patterns)) {
if (ignored) {
results.push(createIgnoreResult(filePath, cwd));
continue;
}
// 收集已使用的废弃的方法, 这里就是很迷, 上面明明清除了lastConfigArrays, 所以这里肯定都是true
if (!lastConfigArrays.includes(config)) {
lastConfigArrays.push(config);
}
// 下面是清除缓存的过程
if (lintResultCache) {
// 得到缓存结果
const cachedResult = lintResultCache.getCachedLintResults(filePath, config);
if (cachedResult) {
const hadMessages =
cachedResult.messages &&
cachedResult.messages.length > 0;
if (hadMessages && fix) {
debug(`Reprocessing cached file to allow autofix: ${filePath}`);
} else {
debug(`Skipping file since it hasn't changed: ${filePath}`);
results.push(cachedResult);
continue;
}
}
}
// 这里开始进行lint操作, 这里去到verifyText里面, 打个记号0x010
const result = verifyText({
text: fs.readFileSync(filePath, "utf8"),
filePath,
config,
cwd,
fix,
allowInlineConfig,
reportUnusedDisableDirectives,
fileEnumerator,
linter
});
results.push(result);
// 存储缓存到lintResultCache对象中
if (lintResultCache) {
lintResultCache.setCachedLintResults(filePath, config, result);
}
}
// 这个通过file-entry-cache这个包将缓存持久化到磁盘。
if (lintResultCache) {
lintResultCache.reconcile();
}
debug(`Linting complete in: ${Date.now() - startTime}ms`);
let usedDeprecatedRules;
// 这里也是直接返回到代码块004
return {
results,
...calculateStatsPerRun(results),
//
get usedDeprecatedRules() {
if (!usedDeprecatedRules) {
usedDeprecatedRules = Array.from(
iterateRuleDeprecationWarnings(lastConfigArrays)
);
}
return usedDeprecatedRules;
}
};
}
2.8 开始进入linter类的检测和修复主流程
我们进入verifyText方法中, 就在第四层lib/cli-engine/cli-engine.js文件中 , 看代码块006
// 代码块006
function verifyText({
text,
cwd,
filePath: providedFilePath,
config,
fix,
allowInlineConfig,
reportUnusedDisableDirectives,
fileEnumerator,
linter
}){
// ...其他配置
// 这里再次进入第五层lib/linter/linter.js, 打个记号0x009
const { fixed, messages, output } = linter.verifyAndFix(
text,
config,
{
allowInlineConfig,
filename: filePathToVerify,
fix,
reportUnusedDisableDirectives,
/**
* Check if the linter should adopt a given code block or not.
* @param {string} blockFilename The virtual filename of a code block.
* @returns {boolean} `true` if the linter should adopt the code block.
*/
filterCodeBlock(blockFilename) {
return fileEnumerator.isTargetPath(blockFilename);
}
}
);
// 这里返回代码块5, 记号0x010
const result = {
filePath,
messages,
// 这里计算并收集错误和警告数, 这里检测就不看了
...calculateStatsPerFile(messages)
};
}
2.9 如何判断检测或者修复完成
const { fixed, messages, output } = linter.verifyAndFix()再次进入第五层lib/linter/linter.js, 这里的检测和修复都是先直接执行一变修复和检测流程do...while处理 , 具体处理如下 , 我们只是检测所以fixedResult.fixed
和shouldFix
都是false , 这时候依然在代码检测中还没有使用espree进行ast转换 ; 看代码块007
// 代码块007
do {
passNumber++;
debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
// 开始检测并且抛出错误, 我们看看下面是如何检测的, 打上记号0x007
messages = this.verify(currentText, config, options);
debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
// 这里是修复+处理信息的代码, 这里打个记号0x008,并且去到代码块011
fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);
// 如果有任何语法错误都会停止。
if (messages.length === 1 && messages[0].fatal) {
break;
}
fixed = fixed || fixedResult.fixed;
currentText = fixedResult.output;
} while (
fixedResult.fixed &&
passNumber < MAX_AUTOFIX_PASSES
);
return fixedResult; // 来到这里我们就返回到代码块006, 记号0x009
verify方法如下 , 看代码块008
// 代码块008
// 根据第二个参数指定的规则验证文本。
// textOrSourceCode要解析的文本或源代码对象。
// [config]配置一个ESLintConfig实例来配置一切。CLIEngine传递一个'ConfigArray'对象。
// [filenameOrptions]正在检查的文件的可选文件名。
verify(textOrSourceCode, config, filenameOrOptions) {
debug("Verify");
const options = typeof filenameOrOptions === "string"
? { filename: filenameOrOptions }
: filenameOrOptions || {};
// 这里把配置提取出来
if (config && typeof config.extractConfig === "function") {
return this._verifyWithConfigArray(textOrSourceCode, config, options);
}
// 这里是将options的数据在进程中进行预处理, 但是最后的ast转换还是在_verifyWithoutProcessors方法里面, 我们进入_verifyWithoutProcessors
if (options.preprocess || options.postprocess) {
return this._verifyWithProcessor(textOrSourceCode, config, options);
}
// 这里直接返回到代码块007, 记号0x007
return this._verifyWithoutProcessors(textOrSourceCode, config, options);
}
3.0 代码转换成AST啦
这时候我们还是在第五层的lib/linter/linter.js文件中 , 继续看_verifyWithoutProcessors这个方法, 这个方法后就已经将fs读取出来的文件转换成ast了 , 看代码块009
// 代码块009
_verifyWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) {
// 获取到自定义的配置和eslint的默认配置的插槽
const slots = internalSlotsMap.get(this);
const config = providedConfig || {};
const options = normalizeVerifyOptions(providedOptions, config);
let text;
//slots.lastSourceCode是记录ast结构的. 如果一开始textOrSourceCode是通过fs读取处理的文件字符串则不进行处理
if (typeof textOrSourceCode === "string") {
slots.lastSourceCode = null;
text = textOrSourceCode;
} else {
slots.lastSourceCode = textOrSourceCode;
text = textOrSourceCode.text;
}
let parserName = DEFAULT_PARSER_NAME; // 这里默认解析器名字 espree
let parser = espree; // 保存ast的espree编译器
// 这里是判断是否我们的自定义配置是否有传入解析器, 就是.eslintrc.*里面的parser选项, 如果有就进行替换
if (typeof config.parser === "object" && config.parser !== null) {
parserName = config.parser.filePath;
parser = config.parser.definition;
} else if (typeof config.parser === "string") {
if (!slots.parserMap.has(config.parser)) {
return [{
ruleId: null,
fatal: true,
severity: 2,
message: `Configured parser '${config.parser}' was not found.`,
line: 0,
column: 0
}];
}
parserName = config.parser;
parser = slots.parserMap.get(config.parser);
}
// 读取文件中的eslint-env
const envInFile = options.allowInlineConfig && !options.warnInlineConfig
? findEslintEnv(text)
: {};
const resolvedEnvConfig = Object.assign({ builtin: true }, config.env, envInFile);
const enabledEnvs = Object.keys(resolvedEnvConfig)
.filter(envName => resolvedEnvConfig[envName])
.map(envName => getEnv(slots, envName))
.filter(env => env);
const parserOptions = resolveParserOptions(parser, config.parserOptions || {}, enabledEnvs);
const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs);
const settings = config.settings || {};
// slots.lastSourceCode记录ast结构, 如果没有就继续解析
if (!slots.lastSourceCode) {
const parseResult = parse(
text,
parser,
parserOptions,
options.filename
);
if (!parseResult.success) {
return [parseResult.error];
}
slots.lastSourceCode = parseResult.sourceCode;
} else {
// 向后兼容处理
if (!slots.lastSourceCode.scopeManager) {
slots.lastSourceCode = new SourceCode({
text: slots.lastSourceCode.text,
ast: slots.lastSourceCode.ast,
parserServices: slots.lastSourceCode.parserServices,
visitorKeys: slots.lastSourceCode.visitorKeys,
scopeManager: analyzeScope(slots.lastSourceCode.ast, parserOptions)
});
}
}
const sourceCode = slots.lastSourceCode;
const commentDirectives = options.allowInlineConfig
? getDirectiveComments(options.filename, sourceCode.ast, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
: { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };
// augment global scope with declared global variables
addDeclaredGlobals(
sourceCode.scopeManager.scopes[0],
configuredGlobals,
{ exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals }
);
// 获取所有的eslint规则
const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);
// 记录检测问题
let lintingProblems;
// 开始执行规则检测
try {
// 这个方法就是遍历我们.eslintrc.*的rules规则, 这里打一个记号0x006
lintingProblems = runRules(
sourceCode,
configuredRules,
ruleId => getRule(slots, ruleId),
parserOptions,
parserName,
settings,
options.filename,
options.disableFixes,
slots.cwd,
providedOptions.physicalFilename
);
} catch (err) {
err.message += `\nOccurred while linting ${options.filename}`;
debug("An error occurred while traversing");
debug("Filename:", options.filename);
if (err.currentNode) {
const { line } = err.currentNode.loc.start;
debug("Line:", line);
err.message += `:${line}`;
}
debug("Parser Options:", parserOptions);
debug("Parser Path:", parserName);
debug("Settings:", settings);
throw err;
}
// 最后返回检测出来的所有问题
return applyDisableDirectives({
directives: commentDirectives.disableDirectives, // 这里是处理是否disable-line/disable-next-line了, 里面的逻辑也不具体看了, 就是处理problem数组的问题并且返回, 到这里我们继续返回到代码块008
problems: lintingProblems
.concat(commentDirectives.problems)
.sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
reportUnusedDisableDirectives: options.reportUnusedDisableDirectives
});
}
3.1 eslint读取插件规则
我们这次进入eslint是如何遍历rules的 , 我们进入runRules方法 , 这会我们依然在第五层的lib/linter/linter.js , 看代码块010
// 代码块010
// 这个方法就是执行ast对象和给定的规则是否匹配, 返回值是一个问题数组
function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd, physicalFilename) {
// 这里创建一个没有this的事件监听器
const emitter = createEmitter();
// 用来记录"program"节点下的所有ast节点
const nodeQueue = [];
let currentNode = sourceCode.ast;
// 开始迭代ast节点, 并且经过处理的节点, 那这里是怎么进行处理, 我们跳出去看看这里的代码, 所以这里再次手动打个记号0x002,这里查看代码块010_1
Traverser.traverse(sourceCode.ast, {
// 进入traverse递归ast的起始需要做的事情, isEntering是判断当前顶层节点是否为Program, 一个是否结束的标志
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
// 递归ast的完成需要做的事情
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
// ast上需要遍历的key名
visitorKeys: sourceCode.visitorKeys
});
// 公共的属性和方法进行冻结, 避免合并的时候有性能的不必要的消耗
const sharedTraversalContext = Object.freeze(
Object.assign(
Object.create(BASE_TRAVERSAL_CONTEXT),
{
getAncestors: () => getAncestors(currentNode),
getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
getCwd: () => cwd,
getFilename: () => filename,
getPhysicalFilename: () => physicalFilename || filename,
getScope: () => getScope(sourceCode.scopeManager, currentNode),
getSourceCode: () => sourceCode,
markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name),
parserOptions,
parserPath: parserName,
parserServices: sourceCode.parserServices,
settings
}
)
);
// 经过Traverser装箱的ast节点后, 开始进行验证
// lintingProblems用来记录问题列表
const lintingProblems = [];
// configuredRules自定义和eslint的默认规则
Object.keys(configuredRules).forEach(ruleId => {
// 获取到每个规则后, 开始判断是否起效就是"off", "warn", "error"三个参数的设置, 这里手动打记号0x003, 我们看代码块010_2
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
// 如果当前规则是0(off关闭的话就不进行检测)
if (severity === 0) {
return;
}
// 这里的ruleMap是在 ./lib/rule下的以及文件下的所有js文件和第三方插件和自定义插件的文件, 就是我们一开始自定义的replaceXXX在代码块replaceXXX
/*
rule 结构就是上面的文件
{
meta: {
// ...
},
create: [Function: create]
}
*/
const rule = ruleMapper(ruleId);
// 没有就直接创建一个空的
if (rule === null) {
lintingProblems.push(createLintingProblem({ ruleId }));
return;
}
const messageIds = rule.meta && rule.meta.messages;
let reportTranslator = null;
// 创建context上下文钩子, 这里怎么理解呢, 就是自定义eslint文件的下create方法的参数, 即上面代码块replaceXXX的create的context
const ruleContext = Object.freeze(
Object.assign(
Object.create(sharedTraversalContext), // 这里获取一下公共钩子, 如果我们在自定义插件里面没有使用就不会设置进去, 以保证性能
{
id: ruleId,
options: getRuleOptions(configuredRules[ruleId]), // 这里获取的是除了数组第一个元素到结尾即 ["error", "$1", "$2"]里面的 $1和$2
report(...args) {
// 在node 8.4以上才起效
// 创建一个报告器
if (reportTranslator === null) {
// 进入createReportTranslator文件这里打个记号0x004, 并且跳到下面代码块010_3
reportTranslator = createReportTranslator({
ruleId,
severity,
sourceCode,
messageIds,
disableFixes
});
}
// 根据上面记号0x004可以得到这个problem就是createReportTranslator的返回值, 其结构为
/*
{
ruleId: options.ruleId,
severity: options.severity,
message: options.message,
line: options.loc.start.line,
column: options.loc.start.column + 1,
nodeType: options.node && options.node.type || null,
messageId?: options.messageId,
problem.endLine?: options.loc.end.line;
problem.endColumn?: options.loc.end.column + 1;
problem.fix?: options.fix;
problem.suggestions?: options.suggestions;
}
*/
const problem = reportTranslator(...args);
if (problem.fix && rule.meta && !rule.meta.fixable) {
throw new Error("Fixable rules should export a `meta.fixable` property.");
}
// 将处理好的问题存储到lintingProblems中
lintingProblems.push(problem);
}
}
)
);
// 这里打个记号0x005,并一起来查看代码块010_4, 这里ruleListeners拿到的每个插件create返回值
const ruleListeners = createRuleListeners(rule, ruleContext);
// 这里将我们的所有规则通过事件发布系统发布出去
Object.keys(ruleListeners).forEach(selector => {
emitter.on(
selector,
timing.enabled
? timing.time(ruleId, ruleListeners[selector])
: ruleListeners[selector]
);
});
});
// 我认为这段代码是分析用的, 具体还有什么功能这里就不深究了, 不在eslint检测的过程中
const eventGenerator = nodeQueue[0].node.type === "Program"
? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
: new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
nodeQueue.forEach(traversalInfo => {
currentNode = traversalInfo.node;
try {
if (traversalInfo.isEntering) {
eventGenerator.enterNode(currentNode);
} else {
eventGenerator.leaveNode(currentNode);
}
} catch (err) {
err.currentNode = currentNode;
throw err;
}
});
// 好了看到这里的都是勇士了, 我写到这里的时候已经是第三天了, 满脑子都是eslint了, 我们开始返回到代码块009, 记号0x006
return lintingProblems;
}
3.2 eslint对AST进行遍历并且转换成特定的结构
我们看看Traverser的做了什么, 该文件在lib/shared/traverser.js , 看代码块010_1
// 代码块010_1
// Traverser是一个遍历AST树的遍历器类。使用递归的方式进行遍历ast树的
// 这里主要看它是如何递归的
class Traverser {
_traverse(node, parent) {
if (!isNode(node)) {
return;
}
this._current = node;
// 重置是否跳过就是那些需要disable的文件
this._skipped = false;
// 这里会是传入的cb, 一般都是处理_skipped和节点信息
this._enter(node, parent);
if (!this._skipped && !this._broken) {
// 这里的keys是确认eslint的ast需要递归什么key值, 这里是eslint的第三方包eslint-visitor-keys, eslint会通过这里面的key名进行遍历打包成eslint本身需要的数据结构
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;
}
}
看完eslint是如何递归处理espree解析出来的ast后 , 我们再滑看会上面的记号0x002, 在代码块010中
查看代码块010_2
// 代码块010_2
// 我们来看看这一句代码做了什么const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
// 首先这段代码是用来匹配0, 1, 2, "off", "warn", "error"这留个变量的
// configuredRules 自定义+eslint的rules规则配置, ruleId是对应的key名
// ConfigOps是重点, 这里进入ConfigOps的文件里面在@eslint/eslintrc包里面的lib/shared/config-ops.js, 并不是在eslint包里面哦
const RULE_SEVERITY_STRINGS = ["off", "warn", "error"],
RULE_SEVERITY = RULE_SEVERITY_STRINGS.reduce((map, value, index) => {
map[value] = index;
return map;
}, {}),
VALID_SEVERITIES = [0, 1, 2, "off", "warn", "error"];
// RULE_SEVERITY定义为{ off: 0, warn: 1, error: 2 }
// 主要是以下方法处理我们rules的0, 1, 2, "off", "warn", "error", 这里如果传入非法值, 就默认返回0, 而"off", "warn", "error"是不区分大小写的
getRuleSeverity(ruleConfig) {
const severityValue = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
if (severityValue === 0 || severityValue === 1 || severityValue === 2) {
return severityValue;
}
if (typeof severityValue === "string") {
return RULE_SEVERITY[severityValue.toLowerCase()] || 0;
}
return 0;
}
至此severity会根据"off", "warn", "error"得到(0|1|2) , 然后我们再返回代码块010中的记号0x003中
3. 以下的代码块作为附录说明 , 会跳来跳去 , 请根据下面提示读取下面的代码块 , 不然会很晕
查看代码块010_3
// 代码块010_3
// reportTranslator = createReportTranslator({}) 方法在/lib/linter/report-translator.js文件里面
function normalizeMultiArgReportCall(...args) {
/*
接收的参数因为是经过解构的所以就会变成
[
{
abc: true,
node: {
type: 'Literal',
value: 'XXX',
raw: "'XXX'",
range: [Array],
loc: [Object],
parent: [Object]
},
messageId: 'unexpected',
data: { argv: 'Candice1' },
fix: [Function: fix]
}
]
意味着context.report是可以接受一个数组或者对象的
*/
if (args.length === 1) {
return Object.assign({}, args[0]);
}
if (typeof args[1] === "string") {
return {
node: args[0],
message: args[1],
data: args[2],
fix: args[3]
};
}
// Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
return {
node: args[0],
loc: args[1],
message: args[2],
data: args[3],
fix: args[4]
};
}
module.exports = function createReportTranslator(metadata) {
/*
createReportTranslator`在每个文件中为每个启用的规则调用一次。它需要非常有表现力。
*报表转换器本身(即`createReportTranslator`返回的函数)获取
*每次规则报告问题时调用,该问题发生的频率要低得多(通常是
*大多数规则不会报告给定文件的任何问题)。
*/
return (...args) => {
const descriptor = normalizeMultiArgReportCall(...args);
const messages = metadata.messageIds;
// 断言descriptor.node是否是一个合法的report节点, 合法的report节点看上面normalizeMultiArgReportCall方法
assertValidNodeInfo(descriptor);
let computedMessage;
if (descriptor.messageId) {
if (!messages) {
throw new TypeError("context.report() called with a messageId, but no messages were present in the rule metadata.");
}
const id = descriptor.messageId;
if (descriptor.message) {
throw new TypeError("context.report() called with a message and a messageId. Please only pass one.");
}
// 这里要注意creat下context.report({messageId: 'unexpected', // 对应meta.messages.XXX,message可以直接用message替换})和meta.messages = {unexpected: '错误的字符串XXX, 需要用{{argv}}替换'}, 里面的key名要对应
if (!messages || !Object.prototype.hasOwnProperty.call(messages, id)) {
throw new TypeError(`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
}
computedMessage = messages[id];
} else if (descriptor.message) {
computedMessage = descriptor.message;
} else {
throw new TypeError("Missing `message` property in report() call; add a message that describes the linting problem.");
}
// 断言desc和fix参数
validateSuggestions(descriptor.suggest, messages);
// 接下来就是处理好所有的规则后, 开始创建最后的问题了, 看下面的createProblem
return createProblem({
ruleId: metadata.ruleId,
severity: metadata.severity,
node: descriptor.node,
message: interpolate(computedMessage, descriptor.data),
messageId: descriptor.messageId,
loc: normalizeReportLoc(descriptor),
fix: metadata.disableFixes ? null : normalizeFixes(descriptor, metadata.sourceCode), // 跟修复相关代码
suggestions: metadata.disableFixes ? [] : mapSuggestions(descriptor, metadata.sourceCode, messages)
});
};
};
// 创建有关报告的信息
function createProblem(options) {
const problem = {
ruleId: options.ruleId,
severity: options.severity,
message: options.message,
line: options.loc.start.line,
column: options.loc.start.column + 1,
nodeType: options.node && options.node.type || null
};
// 如果这不在条件中,则某些测试将失败因为问题对象中存在“messageId”
if (options.messageId) {
problem.messageId = options.messageId;
}
if (options.loc.end) {
problem.endLine = options.loc.end.line;
problem.endColumn = options.loc.end.column + 1;
}
// 跟修复相关
if (options.fix) {
problem.fix = options.fix;
}
if (options.suggestions && options.suggestions.length > 0) {
problem.suggestions = options.suggestions;
}
return problem;
}
接下来我们返回记号0x004
代码块010_4, 记录了eslint如何运行rules中插件的create方法的
function createRuleListeners(rule, ruleContext) {
try {
// 每次最后还是直接返回我们自定义返回的对象, 比如我们代码块replaceXXX的return, 具体看上面代码块replaceXXX
return rule.create(ruleContext);
} catch (ex) {
ex.message = `Error while loading rule '${ruleContext.id}': ${ex.message}`;
throw ex;
}
}
接下来我们返回记号0x005
代码块011, 因为本次是检测不涉及fix过程
SourceCodeFixer.applyFixes = function(sourceText, messages, shouldFix) {
debug("Applying fixes");
// 所以这里就知道返回了
if (shouldFix === false) {
debug("shouldFix parameter was false, not attempting fixes");
return {
fixed: false,
messages,
output: sourceText
};
}
//...其他代码
};
我们继续返回代码块007
代码块012
async function printResults(engine, results, format, outputFile) {
let formatter;
try {
formatter = await engine.loadFormatter(format);
} catch (e) {
log.error(e.message);
return false;
}
// 格式化输出
const output = formatter.format(results);
if (output) {
if (outputFile) {
const filePath = path.resolve(process.cwd(), outputFile);
if (await isDirectory(filePath)) {
log.error("Cannot write to output file path, it is a directory: %s", outputFile);
return false;
}
try {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, output);
} catch (ex) {
log.error("There was a problem writing the output file:\n%s", ex);
return false;
}
} else {
// 这里里面就是用最朴素的打印方式 console.log()进行打印输出
log.info(output);
}
}
return true;
}
3. 总结
文件流程如下
bin/eslint.js -> lib/cli.js -> lib/eslint/eslint.js>lintText() -> lib/cli-engine/cli-engine.js>executeOnFiles() -> lib/cli-engine.js/cli-engine.js>verifyText() -> lib/linter/linter.js>verify()>_verifyWithoutProcessors()>runRules()
文件路径 | 文件说明 |
---|---|
bin/eslint.js | 入口文件, 主要是在这里进入具体的主要执行文件 , 并且读取命令行的参数 |
lib/cli.js | 判断的传入的参数, 并且格式化所需要的参数, 创建eslint的编译器实例 , 在获取完所有的问题列表后会进行console打印到命令行 , 这里最后执行完后是返回对应rocess.exitCode的参数 |
lib/eslint/eslint.js | eslint编译器实例文件 , 这里会简单的判断插件和写入的eslintrc文件是否合法 , 还会对文件的检测和文本的检测的结果进行报告 , 进入真正的脚本执行文件 |
lib/cli-engine/cli-engine.js | 这个文件中会传进一个包含附加工具, 忽略文件, 缓存文件, 配置文件, 配置文件规则以及检测器的一个map插槽 |
lib/linter/linter.js | 检测器文件 , 这里进行ast转换和rule检测 , 已经插件的读取 , 最后把检测后的问题返回 |
eslint中最主要的三个类 , 分别是ESLint和CLIEngine和Linter; 由这三个类分工合作对传入的代码已经插件进行匹配检测 , 当然在eslint还有一些分析和缓存方面 , 在这里也会带过一点 , 还有一个就是eslint写了一个无this的事件发布系统 , 因为eslint里面拆分出来太多类了 , 每个类的新建都有可能改变当前调用this , 所以这个eslint的事件发布系统是无this且freeze安全的 ; 在修复方面 , eslint会根据ast读取到的位置进行替换 ;
在使用插件方面 , 用eslint的生成器生成出来的 , 统一使用eslint-plugin-**
这个格