寫在前面
以下是我閱讀eslint源碼的過程 , 在這過程中 , 我首先會自己寫一個eslint插件的demo , 然后自己定義一個規(guī)則 , 然后再進行檢測 , 根據(jù)調(diào)用棧迅速的一步一步看下去 , 大致知道是怎么樣的流程后 ; 接著再重新拆分每一步是怎么做的 , 分析規(guī)則和插件的運用 , 從而更加鞏固自己對于eslint插件的開發(fā) ; 基于這個想法 , 我們就開始吧
在大致流程中會交代eslint的修復過程 , 但是也是大致的說明一下 ; 詳細拆分的過程是沒有分析修復過程的
先上github上面把eslint源碼clone下來eslint , git clone https://github.com/eslint/eslint.git
第一節(jié) . 大致流程
1. 找到eslint命令入口文件
打開源碼 , 我們通過package.json查看eslint的命令入口 , 在bin下的eslint.js
{
"bin": {
"eslint": "./bin/eslint.js"
}
}
2. 進入./bin/eslint.js
"use strict";
require("v8-compile-cache");
// 讀取命令中 --debug參數(shù), 并輸出代碼檢測的debug信息和每個插件的耗時
if (process.argv.includes("--debug")) {
require("debug").enable("eslint:*,-eslint:code-path,eslintrc:*");
}
// 這里省略了readStdin getErrorMessage onFatalError 三個方法
// 主要看下面IIFE , 而且這個是用了一個promise包裹 , 并且有捕捉函數(shù)的一個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才是執(zhí)行eslint開始的地方
process.exitCode = await require("../lib/cli").execute(
process.argv,
process.argv.includes("--stdin") ? await readStdin() : null
);
}()).catch(onFatalError);
2.1 lib/cli執(zhí)行腳本文件
// 其他代碼
const cli = {
// args 就是那些 --cache --debug等參數(shù)
async execute(args, text) {
/** @type {ParsedCLIOptions} */
// 開始對參數(shù)格式化
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": 指的是該規(guī)則識別的代碼要么會導致錯誤斤彼,要么可能會導致令人困惑的行為。開發(fā)人員應該優(yōu)先考慮解決這個問題踩麦。
docs: {
description: 'XXX 不能出現(xiàn)在代碼中!',
category: 'Possible Errors', // eslint規(guī)則首頁的分類: Possible Errors卿堂、Best Practices盅安、Strict Mode嚣潜、Varibles冬骚、Stylistic Issues、ECMAScript 6、Deprecated只冻、Removed
recommended: false, // "extends": "eslint:recommended"屬性是否啟用該規(guī)則
url: '', // 指定可以訪問完整文檔的URL
},
fixable: 'code', // 該規(guī)則是否可以修復
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: {
// 占位數(shù)據(jù)
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字符串轉(zhuǎn)換成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就是執(zhí)行了插件文件里面create方法如下
create: function (context) {
// 獲取目標項目中.eslintrc.json文件下的rules的第二個參數(shù)
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, // 記錄修復次數(shù), 這里會和最大修復次數(shù)10次比較, 大于10次或者有修復完成的標志即可停止修復
currentText = text; // 修復前的源碼字符串
const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`;
const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true;
// 每個問題循環(huán)修復10次以上或者已經(jīng)修復完畢f(xié)ixedResult.fixed, 即可判定為修復完成
do {
passNumber++;
debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
messages = this.verify(currentText, config, options);
debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
// 執(zhí)行修復代碼
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的結(jié)構(gòu)為
{
ruleId: 'eslint-demo/eslint-demo', // 插件名稱
severity: 2,
message: '錯誤的字符串XXX, 需要用MMM替換', // 提示語
line: 17, // 行數(shù)
column: 18, // 列數(shù)
nodeType: 'Literal', // 當前節(jié)點在AST中是什么類型
messageId: 'unexpected', 對應meta.messages.XXX,message可以直接用message替換
endLine: 17, // 結(jié)尾的行數(shù)
endColumn: 23, // 結(jié)尾的列數(shù)
fix: { range: [ 377, 382 ], text: "'MMM'" } // 該字符串在整個文件字符串中的位置
}
*/
function attemptFix(problem) {
const fix = problem.fix;
const start = fix.range[0]; // 記錄修復的起始位置
const end = fix.range[1]; // 記錄修復的結(jié)束位置
// 如果重疊或為負范圍庇麦,則將其視為問題
if (lastPos >= start || start > end) {
remainingMessages.push(problem);
return false;
}
// 移除非法結(jié)束符.
if ((start < 0 && end >= 0) || (start === 0 && fix.text.startsWith(BOM))) {
output = "";
}
// 拼接修復后的結(jié)果, 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方法唯一失敗的一次是與之前修復的發(fā)生沖突, 這里默認將已經(jīng)修復好的標志設置為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. 項目代碼準備
首先我們準備我們需要檢測的工程結(jié)構(gòu)如下
├── 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插件如何替換規(guī)則*/
}
}
1.3 eslint自定義插件
使用上面大致流程的eslint的自定義插件
2. 代碼流程
當我們輸入 npx eslint ./src/*.tsx
的時候做了什么呢
2.1 第一層bin/eslint.js
入口文件 在 ./bin/eslint.js
, 在這個文件中通過一個匿名自執(zhí)行promise函數(shù) , 引入了 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是否執(zhí)行完畢 , 傳入的參數(shù)是我們npx eslint --fix ./src
的fix這個參數(shù) , 以及跟在--stdin后面的參數(shù) ; 然后我們進入lib/cli , 因為上面直接調(diào)用了execute方法 , 那么我們就看看cli里面execute方法 , 首先定義了一個options
參數(shù) , 然后調(diào)用了CLIOptions.parse(args)
方法 (查看代碼塊003), 這個方法是其他包optionator里面的方法 , 我們進入進去就可以看到parse的方法了 , 這個方法就是switch case將不同的參數(shù)處理裝箱打包進行返回 , 這里面還用了一個.ls
包進行map管理 , 且在保證用戶輸入的時候用了type-check
這個包進行輸入和測試進行管理 , 在非ts環(huán)境下 , 進行類似Haskell的類型語法檢查 ; 好這時候我們拿到了經(jīng)過裝箱打包的options
了 , 這個key名不是我起的 , 它源碼就這樣(好隨便啊) ; 得到了如下結(jié)構(gòu), (看代碼塊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' ]
}
得到這個結(jié)構(gòu)后 , 就通過轉(zhuǎn)換translateOptions
函數(shù)進行配置的轉(zhuǎn)換 , 這里我猜是因為一些人接手別人的代碼 , 需要寫的一個轉(zhuǎn)換文件 ; 接著開始創(chuàng)建我們的一個eslint的編譯器 const engine = new ESLint(translateOptions(options));
2.3 進入第三層lib/eslint/eslint.js
在lib/eslint/eslint.js里面的Eslint類 , Eslint這個類的構(gòu)造函數(shù)首先會將所有的配置進行檢驗 , 在ESLint類里面會創(chuàng)建一個cli的編譯器 ,
2.4 進入第四層lib/cli-engine/cli-engine.js
這個編譯器在lib/cli-engine/cli-engine.js里面 , 這里主要是處理一下緩存以及eslint內(nèi)部的默認規(guī)則 ; 然后回來lib/eslint/eslint.js里面的Eslint類 , 接下來就是獲取從cli-engine.js的內(nèi)部插槽 , 設置私有的內(nèi)存快照 , 判斷是否更新 ,如果更新就刪除緩存 ;
2.5 進入第五層lib/linter/linter.js
這一層就比較簡單了 , 就是用了map結(jié)構(gòu)記錄了cwd , lastConfigArray , lastSourceCode , parserMap , ruleMap , 分別是當前文件路徑 , 最新的配置數(shù)據(jù) , 最新的源碼使用編譯器espree解析出來的ast源碼字符串 , 編譯器(記錄我們用的是什么編譯器默認是espree) , 以及規(guī)則map
2.6 返回到第二層
接著返回到第二層繼續(xù)走下去 , 因為不是使用--stdin , 所以直接看else , 執(zhí)行了engine.lintFiles
// 代碼塊003
const cli = {
// 其他方法
async execute() {
let options;
try {
options = CLIOptions.parse(args);
} catch (error) {
log.error(error.message);
return 2;
}
// 其他驗證參數(shù)的代碼
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類創(chuàng)建出來的所以我們?nèi)サ降谌龑拥膌ib/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構(gòu)造函數(shù)的時候已經(jīng)將cliEngine, set進去了, 所以這里直接獲取即可
const { cliEngine } = privateMembersMap.get(this);
// processCLIEngineLintReport是返回linting指定的文件模式, 傳入的參數(shù)是cliEngine, 并且第二個參數(shù)執(zhí)行了executeOnFiles, 我們看看cliEngine這個類的executeOnFiles做了什么
return processCLIEngineLintReport(
cliEngine,
cliEngine.executeOnFiles(patterns)
);
}
2.7 再次進入第四層
再次進入第四層的CLIEngine類下的executeOnFiles方法, 從他接受的參數(shù)和方法名可以知道, 這個executeOnFiles主要是處理文件和文件組的問題 , 看代碼塊005
// 代碼塊005
executeOnFiles(patterns) {
const results = [];
// 這里是一個很迷的操作, 官方在這里手動把所有的最新配置都清除了, 這個是從外部傳進來的, 但是它先手動清除然后下面再在迭代器里面每個都引用一遍,
lastConfigArrays.length = 0;
//... 其他函數(shù)
// 清除上次使用的配置數(shù)組。
// 清除緩存文件, 使用fs.unlinkSync進行緩存文件的請求, 當不存在此類文件或文件系統(tǒng)為只讀(且緩存文件不存在)時忽略錯誤
// 迭代源文件并且放到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) {
// 得到緩存結(jié)果
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;
}
}
}
// 這里開始進行l(wèi)int操作, 這里去到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,
// 這里計算并收集錯誤和警告數(shù), 這里檢測就不看了
...calculateStatsPerFile(messages)
};
}
2.9 如何判斷檢測或者修復完成
const { fixed, messages, output } = linter.verifyAndFix()再次進入第五層lib/linter/linter.js, 這里的檢測和修復都是先直接執(zhí)行一變修復和檢測流程do...while處理 , 具體處理如下 , 我們只是檢測所以fixedResult.fixed
和shouldFix
都是false , 這時候依然在代碼檢測中還沒有使用espree進行ast轉(zhuǎn)換 ; 看代碼塊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
// 根據(jù)第二個參數(shù)指定的規(guī)則驗證文本。
// 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的數(shù)據(jù)在進程中進行預處理, 但是最后的ast轉(zhuǎn)換還是在_verifyWithoutProcessors方法里面, 我們進入_verifyWithoutProcessors
if (options.preprocess || options.postprocess) {
return this._verifyWithProcessor(textOrSourceCode, config, options);
}
// 這里直接返回到代碼塊007, 記號0x007
return this._verifyWithoutProcessors(textOrSourceCode, config, options);
}
3.0 代碼轉(zhuǎn)換成AST啦
這時候我們還是在第五層的lib/linter/linter.js文件中 , 繼續(xù)看_verifyWithoutProcessors這個方法, 這個方法后就已經(jīng)將fs讀取出來的文件轉(zhuǎn)換成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結(jié)構(gòu)的. 如果一開始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結(jié)構(gòu), 如果沒有就繼續(xù)解析
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規(guī)則
const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);
// 記錄檢測問題
let lintingProblems;
// 開始執(zhí)行規(guī)則檢測
try {
// 這個方法就是遍歷我們.eslintrc.*的rules規(guī)則, 這里打一個記號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數(shù)組的問題并且返回, 到這里我們繼續(xù)返回到代碼塊008
problems: lintingProblems
.concat(commentDirectives.problems)
.sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
reportUnusedDisableDirectives: options.reportUnusedDisableDirectives
});
}
3.1 eslint讀取插件規(guī)則
我們這次進入eslint是如何遍歷rules的 , 我們進入runRules方法 , 這會我們依然在第五層的lib/linter/linter.js , 看代碼塊010
// 代碼塊010
// 這個方法就是執(zhí)行ast對象和給定的規(guī)則是否匹配, 返回值是一個問題數(shù)組
function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd, physicalFilename) {
// 這里創(chuàng)建一個沒有this的事件監(jiān)聽器
const emitter = createEmitter();
// 用來記錄"program"節(jié)點下的所有ast節(jié)點
const nodeQueue = [];
let currentNode = sourceCode.ast;
// 開始迭代ast節(jié)點, 并且經(jīng)過處理的節(jié)點, 那這里是怎么進行處理, 我們跳出去看看這里的代碼, 所以這里再次手動打個記號0x002,這里查看代碼塊010_1
Traverser.traverse(sourceCode.ast, {
// 進入traverse遞歸ast的起始需要做的事情, isEntering是判斷當前頂層節(jié)點是否為Program, 一個是否結(jié)束的標志
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
// 遞歸ast的完成需要做的事情
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
// ast上需要遍歷的key名
visitorKeys: sourceCode.visitorKeys
});
// 公共的屬性和方法進行凍結(jié), 避免合并的時候有性能的不必要的消耗
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
}
)
);
// 經(jīng)過Traverser裝箱的ast節(jié)點后, 開始進行驗證
// lintingProblems用來記錄問題列表
const lintingProblems = [];
// configuredRules自定義和eslint的默認規(guī)則
Object.keys(configuredRules).forEach(ruleId => {
// 獲取到每個規(guī)則后, 開始判斷是否起效就是"off", "warn", "error"三個參數(shù)的設置, 這里手動打記號0x003, 我們看代碼塊010_2
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
// 如果當前規(guī)則是0(off關閉的話就不進行檢測)
if (severity === 0) {
return;
}
// 這里的ruleMap是在 ./lib/rule下的以及文件下的所有js文件和第三方插件和自定義插件的文件, 就是我們一開始自定義的replaceXXX在代碼塊replaceXXX
/*
rule 結(jié)構(gòu)就是上面的文件
{
meta: {
// ...
},
create: [Function: create]
}
*/
const rule = ruleMapper(ruleId);
// 沒有就直接創(chuàng)建一個空的
if (rule === null) {
lintingProblems.push(createLintingProblem({ ruleId }));
return;
}
const messageIds = rule.meta && rule.meta.messages;
let reportTranslator = null;
// 創(chuàng)建context上下文鉤子, 這里怎么理解呢, 就是自定義eslint文件的下create方法的參數(shù), 即上面代碼塊replaceXXX的create的context
const ruleContext = Object.freeze(
Object.assign(
Object.create(sharedTraversalContext), // 這里獲取一下公共鉤子, 如果我們在自定義插件里面沒有使用就不會設置進去, 以保證性能
{
id: ruleId,
options: getRuleOptions(configuredRules[ruleId]), // 這里獲取的是除了數(shù)組第一個元素到結(jié)尾即 ["error", "$1", "$2"]里面的 $1和$2
report(...args) {
// 在node 8.4以上才起效
// 創(chuàng)建一個報告器
if (reportTranslator === null) {
// 進入createReportTranslator文件這里打個記號0x004, 并且跳到下面代碼塊010_3
reportTranslator = createReportTranslator({
ruleId,
severity,
sourceCode,
messageIds,
disableFixes
});
}
// 根據(jù)上面記號0x004可以得到這個problem就是createReportTranslator的返回值, 其結(jié)構(gòu)為
/*
{
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);
// 這里將我們的所有規(guī)則通過事件發(fā)布系統(tǒng)發(fā)布出去
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;
}
});
// 好了看到這里的都是勇士了, 我寫到這里的時候已經(jīng)是第三天了, 滿腦子都是eslint了, 我們開始返回到代碼塊009, 記號0x006
return lintingProblems;
}
3.2 eslint對AST進行遍歷并且轉(zhuǎn)換成特定的結(jié)構(gòu)
我們看看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和節(jié)點信息
this._enter(node, parent);
if (!this._skipped && !this._broken) {
// 這里的keys是確認eslint的ast需要遞歸什么key值, 這里是eslint的第三方包eslint-visitor-keys, eslint會通過這里面的key名進行遍歷打包成eslint本身需要的數(shù)據(jù)結(jié)構(gòu)
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規(guī)則配置, 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"是不區(qū)分大小寫的
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會根據(jù)"off", "warn", "error"得到(0|1|2) , 然后我們再返回代碼塊010中的記號0x003中
3. 以下的代碼塊作為附錄說明 , 會跳來跳去 , 請根據(jù)下面提示讀取下面的代碼塊 , 不然會很暈
查看代碼塊010_3
// 代碼塊010_3
// reportTranslator = createReportTranslator({}) 方法在/lib/linter/report-translator.js文件里面
function normalizeMultiArgReportCall(...args) {
/*
接收的參數(shù)因為是經(jīng)過解構(gòu)的所以就會變成
[
{
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是可以接受一個數(shù)組或者對象的
*/
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`在每個文件中為每個啟用的規(guī)則調(diào)用一次入桂。它需要非常有表現(xiàn)力奄薇。
*報表轉(zhuǎn)換器本身(即`createReportTranslator`返回的函數(shù))獲取
*每次規(guī)則報告問題時調(diào)用,該問題發(fā)生的頻率要低得多(通常是
*大多數(shù)規(guī)則不會報告給定文件的任何問題)抗愁。
*/
return (...args) => {
const descriptor = normalizeMultiArgReportCall(...args);
const messages = metadata.messageIds;
// 斷言descriptor.node是否是一個合法的report節(jié)點, 合法的report節(jié)點看上面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參數(shù)
validateSuggestions(descriptor.suggest, messages);
// 接下來就是處理好所有的規(guī)則后, 開始創(chuàng)建最后的問題了, 看下面的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)
});
};
};
// 創(chuàng)建有關報告的信息
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
};
}
//...其他代碼
};
我們繼續(xù)返回代碼塊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. 總結(jié)
文件流程如下
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 | 入口文件, 主要是在這里進入具體的主要執(zhí)行文件 , 并且讀取命令行的參數(shù) |
lib/cli.js | 判斷的傳入的參數(shù), 并且格式化所需要的參數(shù), 創(chuàng)建eslint的編譯器實例 , 在獲取完所有的問題列表后會進行console打印到命令行 , 這里最后執(zhí)行完后是返回對應rocess.exitCode的參數(shù) |
lib/eslint/eslint.js | eslint編譯器實例文件 , 這里會簡單的判斷插件和寫入的eslintrc文件是否合法 , 還會對文件的檢測和文本的檢測的結(jié)果進行報告 , 進入真正的腳本執(zhí)行文件 |
lib/cli-engine/cli-engine.js | 這個文件中會傳進一個包含附加工具, 忽略文件, 緩存文件, 配置文件, 配置文件規(guī)則以及檢測器的一個map插槽 |
lib/linter/linter.js | 檢測器文件 , 這里進行ast轉(zhuǎn)換和rule檢測 , 已經(jīng)插件的讀取 , 最后把檢測后的問題返回 |
eslint中最主要的三個類 , 分別是ESLint和CLIEngine和Linter; 由這三個類分工合作對傳入的代碼已經(jīng)插件進行匹配檢測 , 當然在eslint還有一些分析和緩存方面 , 在這里也會帶過一點 , 還有一個就是eslint寫了一個無this的事件發(fā)布系統(tǒng) , 因為eslint里面拆分出來太多類了 , 每個類的新建都有可能改變當前調(diào)用this , 所以這個eslint的事件發(fā)布系統(tǒng)是無this且freeze安全的 ; 在修復方面 , eslint會根據(jù)ast讀取到的位置進行替換 ;
在使用插件方面 , 用eslint的生成器生成出來的 , 統(tǒng)一使用eslint-plugin-**
這個格