前端框架系列之(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 === "" ? 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 || "";
        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做铺垫,只有知根知底方能百战百胜,下节见!!

你可能感兴趣的:(前端框架系列之(eslint源码解析))