開始
我們直接clone一份eslint的源碼
git clone https://github.com/eslint/eslint.git
爲了更好的理解源碼,我直接貼一張自己整理的eslint的流程圖,我們對照流程圖再一步步解析源碼
bin
我們首先找到了eslint命令的入口文件
bin/eslint.js:
...
(async function main() {
...
process.exitCode = await require("../lib/cli").execute(
process.argv,
process.argv.includes("--stdin") ? await readStdin() : null
);
}()).catch(onFatalError);
在命令的入口文件中我們可以看到,直接引用了…/lib/cli文件,然後執行了cli的execute方法,所以我們找到cli文件
lib/cli.js:
...
const cli = {
async execute(args, text) {
...
const engine = new ESLint(translateOptions(options));
const fileConfig =
await engine.calculateConfigForFile(options.printConfig);
log.info(JSON.stringify(fileConfig, null, " "));
return 0;
}
...
//支持輸出入輸入內容做校驗
if (useStdin) {
results = await engine.lintText(text, {
filePath: options.stdinFilename,
warnIgnored: true
});
} else {
//執行需要校驗的文件
results = await engine.lintFiles(files);
}
...
};
module.exports = cli;
可以看到,如果是傳入的文件信息的話,就直接執行需要校驗的文件engine.lintFiles(files),
所以我們繼續往下看engine.lintFiles方法。
lib/eslint/eslint.js:
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");
}
const { cliEngine } = privateMembersMap.get(this);
return processCLIEngineLintReport(
cliEngine,
cliEngine.executeOnFiles(patterns)
);
}
lintFiles方法直接執行了一個叫executeOnFiles的方法,
lib/cli-engine/cli-engine.js:
executeOnFiles(patterns) {
...
// Do lint.
const result = verifyText({
text: fs.readFileSync(filePath, "utf8"),
filePath,
config,
cwd,
fix,
allowInlineConfig,
reportUnusedDisableDirectives,
fileEnumerator,
linter
});
results.push(result);
...
return {
results,
...calculateStatsPerRun(results),
// Initialize it lazily because CLI and `ESLint` API don't use it.
get usedDeprecatedRules() {
if (!usedDeprecatedRules) {
usedDeprecatedRules = Array.from(
iterateRuleDeprecationWarnings(lastConfigArrays)
);
}
return usedDeprecatedRules;
}
};
}
可以看到,executeOnFiles方法直接執行了一個叫verifyText的方法,所以我們繼續往下。
verify
我們已經跑到了流程圖中的verify了,我們看一下verifyText方法,
lib/cli-engine/cli-engine.js:
function verifyText({
text,
cwd,
filePath: providedFilePath,
config,
fix,
allowInlineConfig,
reportUnusedDisableDirectives,
fileEnumerator,
linter
}) {
...
const filePathToVerify = filePath === "<text>" ? path.join(cwd, filePath) : filePath;
const { fixed, messages, output } = linter.verifyAndFix(
text,
config,
{
allowInlineConfig,
filename: filePathToVerify,
fix,
reportUnusedDisableDirectives,
...
filterCodeBlock(blockFilename) {
return fileEnumerator.isTargetPath(blockFilename);
}
}
);
// Tweak and return.
const result = {
filePath,
messages,
...calculateStatsPerFile(messages)
};
...
return result;
}
在verifyText方法中,我們看到又直接掉用了linter的verifyAndFix方法,然後封裝verifyAndFix方法的結果直接返回result,所以我們找到linter的verifyAndFix方法。
lib/linter/linter.js:
verifyAndFix(text, config, options) {
let messages = [],
fixedResult,
fixed = false,
passNumber = 0,
currentText = text;
...
do {
passNumber++;
//執行驗證流程
messages = this.verify(currentText, config, options);
//如果需要修復就執行修復
fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);
...
} while (
fixedResult.fixed &&
passNumber < MAX_AUTOFIX_PASSES
);
...
//返回修復過後的結果
return fixedResult;
}
}
可以看到,執行了verify方法(也就是我們定義的rules),拿到驗證結果,如果需要修復的話(也就是加了–fix選項)就直接調用SourceCodeFixer.applyFixes方法進行源碼修復,最後返回結果。
我們先看一下verify方法:
verify(textOrSourceCode, config, filenameOrOptions) {
...
//如果配置中有preprocess或者postprocess函數
if (options.preprocess || options.postprocess) {
return this._verifyWithProcessor(textOrSourceCode, config, options);
}
return this._verifyWithoutProcessors(textOrSourceCode, config, options);
}
preprocess?
如果有preprocess或者postprocess的時候,就執行了一個叫_verifyWithProcessor的方法,
lib/linter/linter.js:
_verifyWithProcessor(textOrSourceCode, config, options, configForRecursive) {
const filename = options.filename || "<input>";
const filenameToExpose = normalizeFilename(filename);
const text = ensureText(textOrSourceCode);
const preprocess = options.preprocess || (rawText => [rawText]);
const postprocess = options.postprocess || lodash.flatten;
const filterCodeBlock =
options.filterCodeBlock ||
(blockFilename => blockFilename.endsWith(".js"));
const originalExtname = path.extname(filename);
const messageLists = preprocess(text, filenameToExpose).map((block, i) => {
debug("A code block was found: %o", block.filename || "(unnamed)");
// Keep the legacy behavior.
if (typeof block === "string") {
return this._verifyWithoutProcessors(block, config, options);
}
const blockText = block.text;
const blockName = path.join(filename, `${i}_${block.filename}`);
// Skip this block if filtered.
if (!filterCodeBlock(blockName, blockText)) {
debug("This code block was skipped.");
return [];
}
// Resolve configuration again if the file extension was changed.
if (configForRecursive && path.extname(blockName) !== originalExtname) {
debug("Resolving configuration again because the file extension was changed.");
return this._verifyWithConfigArray(
blockText,
configForRecursive,
{ ...options, filename: blockName }
);
}
// Does lint.
return this._verifyWithoutProcessors(
blockText,
config,
{ ...options, filename: blockName }
);
});
return postprocess(messageLists, filenameToExpose);
}
可以看到_verifyWithProcessor方法中調用了配置的preprocess方法,把源碼text傳給了它,然後獲取preprocess方法返回代碼塊,繼續調用了_verifyWithoutProcessors方法,
parse
lib/linter/linter.js:
_verifyWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) {
...
//獲取解析器並且解析文件爲ast對象
const parseResult = parse(
text,
parser,
parserOptions,
options.filename
);
...
try {
//執行rules
lintingProblems = runRules(
sourceCode,
configuredRules,
ruleId => getRule(slots, ruleId),
parserOptions,
parserName,
settings,
options.filename,
options.disableFixes,
slots.cwd
);
} catch (err) {
...
}
}
可以看到,_verifyWithoutProcessors方法中去獲取瞭解析器parser(espree),然後通過espree解析源碼爲ast對象,最後把ast對象當成sourceCode傳遞給了runRules方法。
runRules
lib/linter/linter.js:
function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd) {
...
Traverser.traverse(sourceCode.ast, {
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
visitorKeys: sourceCode.visitorKeys
});
/*
* Create a frozen object with the ruleContext properties and methods that are shared by all rules.
* All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
* properties once for each rule.
*/
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,
getScope: () => getScope(sourceCode.scopeManager, currentNode),
getSourceCode: () => sourceCode,
markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name),
parserOptions,
parserPath: parserName,
parserServices: sourceCode.parserServices,
settings
}
)
);
const lintingProblems = [];
Object.keys(configuredRules).forEach(ruleId => {
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
// not load disabled rules
if (severity === 0) {
return;
}
const rule = ruleMapper(ruleId);
if (rule === null) {
lintingProblems.push(createLintingProblem({ ruleId }));
return;
}
const messageIds = rule.meta && rule.meta.messages;
let reportTranslator = null;
const ruleContext = Object.freeze(
Object.assign(
Object.create(sharedTraversalContext),
{
id: ruleId,
options: getRuleOptions(configuredRules[ruleId]),
report(...args) {
if (reportTranslator === null) {
reportTranslator = createReportTranslator({
ruleId,
severity,
sourceCode,
messageIds,
disableFixes
});
}
const problem = reportTranslator(...args);
if (problem.fix && rule.meta && !rule.meta.fixable) {
throw new Error("Fixable rules should export a `meta.fixable` property.");
}
lintingProblems.push(problem);
}
}
)
);
...
return lintingProblems;
}
通過espress編譯器編譯過後的源碼變成了一個ast對象,ast就是抽象語法樹的意思,我記得我在babel解析的時候也提到過ast,其實babel的原理跟eslint差不多,感興趣的小夥伴可以看一下我的另外一篇文章babel源碼解析一
比如我們上一節的demo項目中的demo1.js:
document.write("hello eslint");
變成抽象語法樹是這樣的:
感興趣的小夥伴可以自己去ast在線轉換裏面測試:https://astexplorer.net/
那麼我們怎麼遍歷這個ast對象呢? 接下來就是我們的traverse上場了,traverse就是專門用來遍歷ast對象的。
traverse
我們回到之前的runRules方法,在runRules中我看到了這麼一段代碼,
lib/linter/linter.js:
//創建ast語法樹遍歷器
Traverser.traverse(sourceCode.ast, {
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
visitorKeys: sourceCode.visitorKeys
});
Traverser.traverse方法就是創建了一個ast解析器,去解析ast對象。
lib/shared/traverser.js:
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 the given AST tree recursively.
* @param {ASTNode} node The current node.
* @param {ASTNode|null} parent The parent node.
* @returns {void}
* @private
*/
_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;
}
在_traverse方法中我們可以看到,其實就是在遞歸遍歷我們的ast的節點,那麼每個節點到底是什麼呢?
以我們的demo1.js爲例子:
document.write("hello eslint");
變成抽象語法樹是這樣的:
那麼traverser怎麼知道遍歷哪些字段呢? 通過上面的demo1.js的ast對象我們可以看到一個叫type的屬性,type值爲“program”,在_traserve方法中我看到這麼一段代碼:
//獲取可以遍歷的屬性keys
const keys = getVisitorKeys(this._visitorKeys, node);
if (keys.length >= 1) {
this._parents.push(node);
//遍歷每一個key
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) {
//遞歸遍歷key
this._traverse(child[j], node);
}
} else {
//遞歸遍歷key
this._traverse(child, node);
}
}
this._parents.pop();
}
那麼上面的keys從哪來呢?我們找到這麼一個文件,
xxx/node_modules/eslint-visitor-keys/lib/visitor-keys.json:
{
...
"Program": [
"body"
]
...
}
內容有點多了,我們直接看我們demo.js需要的type值“Program”,也就是如果當前節點的type爲“Program“的話,就會遍歷body值,然後重複遞歸直到結束。
每次遍歷一個節點的時候會調用_enter方法,然後離開當前節點的時候會調用_leave方法。
現在再來看這段代碼是不是就清晰多了呢?
lib/linter/linter.js:
//創建ast語法樹遍歷器
Traverser.traverse(sourceCode.ast, {
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
visitorKeys: sourceCode.visitorKeys
});
那麼traverse怎麼執行每個rule方法的呢?
執行rule
我們回到runRules方法
lib/linter/linter.js:
function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd) {
const emitter = createEmitter();
const nodeQueue = [];
let currentNode = sourceCode.ast;
Traverser.traverse(sourceCode.ast, {
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
visitorKeys: sourceCode.visitorKeys
});
/*
* Create a frozen object with the ruleContext properties and methods that are shared by all rules.
* All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
* properties once for each rule.
*/
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,
getScope: () => getScope(sourceCode.scopeManager, currentNode),
getSourceCode: () => sourceCode,
markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name),
parserOptions,
parserPath: parserName,
parserServices: sourceCode.parserServices,
settings
}
)
);
const lintingProblems = [];
Object.keys(configuredRules).forEach(ruleId => {
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
// not load disabled rules
if (severity === 0) {
return;
}
const rule = ruleMapper(ruleId);
if (rule === null) {
lintingProblems.push(createLintingProblem({ ruleId }));
return;
}
const messageIds = rule.meta && rule.meta.messages;
let reportTranslator = null;
const ruleContext = Object.freeze(
Object.assign(
Object.create(sharedTraversalContext),
{
id: ruleId,
options: getRuleOptions(configuredRules[ruleId]),
report(...args) {
/*
* Create a report translator lazily.
* In a vast majority of cases, any given rule reports zero errors on a given
* piece of code. Creating a translator lazily avoids the performance cost of
* creating a new translator function for each rule that usually doesn't get
* called.
*
* Using lazy report translators improves end-to-end performance by about 3%
* with Node 8.4.0.
*/
if (reportTranslator === null) {
reportTranslator = createReportTranslator({
ruleId,
severity,
sourceCode,
messageIds,
disableFixes
});
}
const problem = reportTranslator(...args);
if (problem.fix && rule.meta && !rule.meta.fixable) {
throw new Error("Fixable rules should export a `meta.fixable` property.");
}
lintingProblems.push(problem);
}
}
)
);
const ruleListeners = createRuleListeners(rule, ruleContext);
// add all the selectors from the rule as listeners
Object.keys(ruleListeners).forEach(selector => {
emitter.on(
selector,
timing.enabled
? timing.time(ruleId, ruleListeners[selector])
: ruleListeners[selector]
);
});
});
// only run code path analyzer if the top level node is "Program", skip otherwise
const eventGenerator = nodeQueue[0].node.type === "Program" ? new CodePathAnalyzer(new NodeEventGenerator(emitter)) : new NodeEventGenerator(emitter);
nodeQueue.forEach(traversalInfo => {
currentNode = traversalInfo.node;
try {
if (traversalInfo.isEntering) {
eventGenerator.enterNode(currentNode);
} else {
eventGenerator.leaveNode(currentNode);
}
} catch (err) {
err.currentNode = currentNode;
throw err;
}
});
return lintingProblems;
}
代碼我直接全部貼過來了,我們首先看到:
Traverser.traverse(sourceCode.ast, {
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
visitorKeys: sourceCode.visitorKeys
});
這個我們上面解析過了,就是創建一個ast遍歷器,可以看到把每個ast的節點對象裝進了一個叫nodeQueue數組中,
接下來就是創建每一個rule對象了,然後執行rule對象的create方法。
...
//遍歷每條rule規則
Object.keys(configuredRules).forEach(ruleId => {
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
// not load disabled rules
if (severity === 0) {
return;
}
const rule = ruleMapper(ruleId);
if (rule === null) {
lintingProblems.push(createLintingProblem({ ruleId }));
return;
}
const messageIds = rule.meta && rule.meta.messages;
let reportTranslator = null;
const ruleContext = Object.freeze(
Object.assign(
Object.create(sharedTraversalContext),
{
id: ruleId,
options: getRuleOptions(configuredRules[ruleId]),
report(...args) {
...
if (reportTranslator === null) {
reportTranslator = createReportTranslator({
ruleId,
severity,
sourceCode,
messageIds,
disableFixes
});
}
const problem = reportTranslator(...args);
if (problem.fix && rule.meta && !rule.meta.fixable) {
throw new Error("Fixable rules should export a `meta.fixable` property.");
}
lintingProblems.push(problem);
}
}
)
);
//創建一個rule對象
const ruleListeners = createRuleListeners(rule, ruleContext);
createRuleListeners方法
lib/linter/linter.js:
function createRuleListeners(rule, ruleContext) {
try {
return rule.create(ruleContext);
} catch (ex) {
ex.message = `Error while loading rule '${ruleContext.id}': ${ex.message}`;
throw ex;
}
}
很簡單,也就是調用了每個規則的create方法,那麼create方法返回的又是什麼呢?其實就是我們的ast的每個節點類型,travese已經拿到了ast的每個節點,然後遍歷節點的時候就去rule裏面找,一個個找rule問,你是否需要處理呢?
我們隨便找一個eslint內置rule看看,
lib/rules/semi.js:
module.exports = {
...
create(ruleContext){
return {
VariableDeclaration: checkForSemicolonForVariableDeclaration,
ExpressionStatement: checkForSemicolon,
ReturnStatement: checkForSemicolon,
ThrowStatement: checkForSemicolon,
DoWhileStatement: checkForSemicolon,
DebuggerStatement: checkForSemicolon,
BreakStatement: checkForSemicolon,
ContinueStatement: checkForSemicolon,
ImportDeclaration: checkForSemicolon,
ExportAllDeclaration: checkForSemicolon,
ExportNamedDeclaration(node) {
if (!node.declaration) {
checkForSemicolon(node);
}
},
ExportDefaultDeclaration(node) {
if (!/(?:Class|Function)Declaration/u.test(node.declaration.type)) {
checkForSemicolon(node);
}
}
};
}
...
}
在看一下我們demo1.js的ast圖:
可以看到,當traverse遍歷到ExpressionStatement節點的時候,就會觸發semi規則的ExpressionStatement節點回調函數
checkForSemicolon方法,然後semi根據自己的rule處理代碼,最後通過ruleContext的report方法報告給eslint,eslint收集每個rule的返回值。
postprocess?
我們繼續回到我們的_verifyWithProcessor方法,
lib/linter/linter.js:
_verifyWithProcessor(textOrSourceCode, config, options, configForRecursive) {
...
const postprocess = options.postprocess || lodash.flatten;
...
const messageLists = preprocess(text, filenameToExpose).map((block, i) => {
...
// Does lint.
return this._verifyWithoutProcessors(
blockText,
config,
{ ...options, filename: blockName }
);
});
//執行配置文件的postprocess方法
return postprocess(messageLists, filenameToExpose);
}
可以看到,當rule處理完畢代碼後,就直接把處理完畢的結果給了postprocess方法,所以我們可以在postprocess方法中對rule處理的結果進行處理。
返回lintingProblems
rule處理完畢後返回lintingProblems結果給linter
lib/linter/linter.js
verifyAndFix(text, config, options) {
...
messages = this.verify(currentText, config, options);
...
}
fix?
如果需要修復代碼的話,那麼就調用SourceCodeFixer.applyFixes
lib/linter/linter.js
verifyAndFix(text, config, options) {
...
messages = this.verify(currentText, config, options);
...
fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);
}
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;
/**
* Try to use the 'fix' from a problem.
* @param {Message} problem The message object to apply fixes from
* @returns {boolean} Whether fix was successfully applied
*/
function attemptFix(problem) {
const fix = problem.fix;
const start = fix.range[0];
const end = fix.range[1];
// Remain it as a problem if it's overlapped or it's a negative range
if (lastPos >= start || start > end) {
remainingMessages.push(problem);
return false;
}
// Remove BOM.
if ((start < 0 && end >= 0) || (start === 0 && fix.text.startsWith(BOM))) {
output = "";
}
// Make output to this fix.
output += text.slice(Math.max(0, lastPos), Math.max(0, start));
output += fix.text;
lastPos = end;
return true;
}
messages.forEach(problem => {
if (Object.prototype.hasOwnProperty.call(problem, "fix")) {
fixes.push(problem);
} else {
remainingMessages.push(problem);
}
});
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);
/*
* The only time attemptFix will fail is if a previous fix was
* applied which conflicts with it. So we can mark this as 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
};
};
每個rule除了提供了校驗ast的回調函數外,還提供了fix方法供eslint修復代碼,比如semi.js:
fix = function(fixer) {
return fixer.insertTextAfter(lastToken, ";");
};
fix = function(fixer) {
/*
* Expand the replacement range to include the surrounding
* tokens to avoid conflicting with no-extra-semi.
* https://github.com/eslint/eslint/issues/7928
*/
return new FixTracker(fixer, sourceCode)
.retainSurroundingTokens(lastToken)
.remove(lastToken);
};
semi規則會告訴eslint你需要添加或者移除“;”符號。
修復文件
好啦,rule處理完畢了,fix的結果我們也拿到了,現在需要把fixedResult重新寫入文件。
lib/cli.js:
if (options.fix) {
debug("Fix mode enabled - applying fixes");
await ESLint.outputFixes(results);
}
static async outputFixes(results) {
if (!Array.isArray(results)) {
throw new Error("'results' must be an array");
}
await Promise.all(
results
.filter(result => {
if (typeof result !== "object" || result === null) {
throw new Error("'results' must include only objects");
}
return (
typeof result.output === "string" &&
path.isAbsolute(result.filePath)
);
})
.map(r => writeFile(r.filePath, r.output))
);
}
很簡單,也就是文件的讀寫修改操作。
好啦~ 整個eslint的源碼我們跟着流程圖簡單的走了一遍,當然,深入研究eslint的話絕對不止我們這一點點內容,一個parser就夠我們玩了,感興趣的小夥伴可以自己去看看哦,寫這篇文章的目的也是爲後面我們自定義plugin做鋪墊,只有知根知底方能百戰百勝,下節見!!