前端框架系列之(eslint源碼解析)

開始

我們直接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做鋪墊,只有知根知底方能百戰百勝,下節見!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章