乐趣区

关于前端:ESLint-源码分析

前端的日常开发离不开各种 lint 的反对,然而有的人就是不喜爱,代码写法随便任性,这个咱们就不去强求了。实际上标准的定义次要取决于开源我的项目作者的习惯,或者公司团队编码的习惯,即便两个前端专家,写出的代码标准也会有差异。ESLint 的初衷是为了让程序员能够创立本人的检测规定。ESLint 的所有规定都被设计成可插入的。ESLint 的默认规定与其余的插件并没有什么区别,规定自身和测试能够依赖于同样的模式。为了便于人们应用,ESLint 内置了一些规定,当然,你能够在应用过程中自定义规定。
咱们明天次要从源码中摸索 ESLint 的工作原理。

为什么要应用 ESLint

下一张图可能很多人都见过,他能很好的形容 ESLint 的作用:

  • 如果你没有用 ESLint,你的代码须要人工来查看,格局可能就千姿百态,天然 bug 也就防止不了层出不穷,看见你代码的其余开发者情绪也是极糟的。
  • 如果你应用了 ESLint,对你的代码从各个方面进行查看,运行起来的问题天然会少很多,别的同学浏览起来也就很称心了。

总体概括 ESLint 是一种动态代码剖析工具,用于辨认在 JavaScript 代码中发现的有问题的模式。不仅可能让咱们缩小 bug,而且还能帮咱们对立编码格调,更容易多人短暂去保护咱们的代码和我的项目。

你是不是认为我会讲如何装置、配置、应用 ESlint?NO… 话不多说 走起~

Eslint 执行流程

要是须要更好的应用 Eslint,那么必须晓得 Eslint 的工作原理和代码构造能力更好的理解 Eslint,接下来会解说一下 Eslint 的执行流程,下图是 Eslint 的整体执行过程。

首先先来理解两个类 linter 和 CliEngine

  • CLIEngine 该类是 Eslint 的大脑,管制 Eslint 的执行流程,调用 api 时个别只须要操作 CLIEngine 即可
  • Linter 该类是 Eslint 的执行总裁,配置文件加载、校验、修复都是该类来管制实现的

开始

咱们首先找到了 Eslint 命令的入口文件 Eslint.js

(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;
    }

    // Otherwise, call the CLI.
    process.exitCode = await require("../lib/cli").execute(
        process.argv,
        process.argv.includes("--stdin") ? await readStdin() : null);
}()).catch(onFatalError);

咱们从代码外面能够看出援用了 cli 文件并且执行了外面 execute 办法。

实例化

Eslint 实例化次要在 cli-engine.js 外面的 CLIEngine 做的,让咱们具体看看这个外面做哪些工作:

  • 合并配置参数和默认参数
  • 实例化 Linter 对象,在 Linter 类的构造函数中会实例化一个 Rules 对象,实例化 Rules 时会在构造函数中读取 lib/rules 的所有文件(所有的查看规定),并且以文件名称作为 key,绝对路径作为 value 存储在 map 中
const linter = new Linter({cwd: options.cwd});
constructor({cwd} = {}) {
    internalSlotsMap.set(this, {cwd: normalizeCwd(cwd),
        lastConfigArray: null,
        lastSourceCode: null,
        parserMap: new Map([["espree", espree]]),
        ruleMap: new Rules()});
    this.version = pkg.version;
}
  • 若配置了 rules,则校验 rules 的每一项是否非法
  • 实例化 Config,Config 是寄存所有的查看规定和插件

CLIEngine 实例化实现后会返回一个 CLIEngine 对象,能够调用该对象的 executeOnFiles(查看多个文件)或者 executeOnText(查看文本)来进行代码查看。

verify && verifyAndFix

其实 Eslint 提供了 executeOnFiles 和 executeOnText 两个代码查看的接口

executeOnFiles(patterns) {
    ...
    // Do lint.
    const result = verifyText({text: fs.readFileSync(filePath, "utf8"),
        filePath,
        config,
        cwd,
        fix,
        allowInlineConfig,
        reportUnusedDisableDirectives,
        fileEnumerator,
        linter
    });

    results.push(result);

    if (lintResultCache) {lintResultCache.setCachedLintResults(filePath, config, result);
    }

    if (lintResultCache) {lintResultCache.reconcile();
    }

    debug(`Linting complete in: ${Date.now() - startTime}ms`);
    let usedDeprecatedRules;

    return {
        results,
        ...calculateStatsPerRun(results),

        get usedDeprecatedRules() {if (!usedDeprecatedRules) {
                usedDeprecatedRules = Array.from(iterateRuleDeprecationWarnings(lastConfigArrays)
                );
            }
            return usedDeprecatedRules;
        }
    };
}

从代码中咱们能够看见 executeOnFiles 执行了 verifyText 办法,在 verifyText 办法中,咱们看到调用了 linter 的 verifyAndFix 办法,而后封装 verifyAndFix 办法的后果间接返回 result, 所以咱们找到 linter 的 verifyAndFix 办法

verifyAndFix(text, config, options) {let messages = [],
        fixedResult,
        fixed = false,
        passNumber = 0,
        currentText = text;
    const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`;
    const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true;
    do {
        passNumber++;

        debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
        messages = this.verify(currentText, config, options);

        debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
        // 如果须要修复就执行修复
        fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);

        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 (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;
}

如果须要修复的话就间接调用 SourceCodeFixer.applyFixes 办法进行源码修复, 最初返回一个修复过后的后果。

AST 生成和创立

上面咱们能够看看 runRules 办法,在 runRules 中我看到 Traverser.traverse 办法就是创立了一个 ast 解析器,去解析 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 办法中咱们能够看到,其实就是在递归遍历咱们的 ast 的节点。

形象语法树是这样的:

那么 traverser 怎么晓得遍历哪些字段呢?看上图右侧 type 的属性,type 值为“program”,在_traserve 办法中我看到这么一段代码:

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();}
}

那么下面的 keys 从哪来呢?找到这么一个文件

如果以后节点的 type 为“Program“的话,就会遍历 body 值,而后反复递归直到完结。

代码查看

把文本解析成 AST 并创立作用域后会调用 Linter 的 runRules 办法来调用每一条规定查看;首先会把 AST 树放入队列中,不便后续的操作,而后循环所有的规定,若该规定是关上的,则在缓存中取出规定,若该规定不存在缓存中则加载该规定(eslint 默认的规定会在此处加载到内存中),获取到查看规定后会注册该规定,当所有的规定都注册完后遍历方才放入队列中的 AST 节点,在遍历每一个节点时会依据该节点的类型触发对应的查看项做查看,若存在谬误保留在上下文中,当所有的节点都遍历完后此次查看就完结了。

代码修复

Eslint 的代码修复在文件 source-code-fixer.js 中实现的,在 SourceCodeFixer 中首先过滤掉 message 中没有 fix 的数据失去须要修复的信息,每一条修复信息中有一个 fix 对象,该对象是在对应的规定中查看时生成的,fix 对象中有 range 数组和 text 两字段,range 是一个长度为 2 的数字,第一个值示意从上一个修复条件修复的地位到该条修复条件的地位,第二个值示意下一条修复条件的地位,text 示意替换内容。
晓得了 message 和修复规定后,那么接下来讲述修复过程,Eslint 会创立一个空的 output 用来寄存修复实现的代码,循环执行修复条件,第一个修复条件执行修复时截取源码从 0 开始到 range 第一个值的内容,追加到 output 上,把修复内容的 text 追加到 output 上,而后把指针从 0 移到 range 的第二个值 end,下一个修复条件从上一个的 end 开始截取源码,顺次类推,最初把残余的的源码追加到 output 上失去了一个修复后的源码;为了更牢靠的实现修复性能,Eslint 把修复好的源码再次转换成 AST 剖析查看,若无修复的内容或者曾经修复 10 次则示意无奈再进一步修复了,那么修复就完结了。
看看 attemptFix 办法:

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;
}

以上就是整个 Eslint 的源码的实现原理,上面咱们还是要简略讲讲是如何配置的。

配置

能够通过以下形式配置 ESLint:

  1. 个别都采纳 .eslintrc 的配置文件进行配置, 如果放在我的项目的根目录中,则会作用于整个我的项目。如果在我的项目的子目录中也蕴含着 .eslintrc 文件,则对于子目录中文件的查看会疏忽掉根目录中的配置,而间接采纳子目录中的配置,这就可能在不同的目录范畴内利用不同的查看规定,显得比拟灵便。ESLint 采纳逐级向上查找的形式查找 .eslintrc 文件,当找到带有 “root”: true 配置项的 .eslintrc 文件时,将会进行向上查找。
  2. 在 package.json 中增加 eslintConfig 配置块;

以下是 .eslintrc 文件的示例和解释:

// .eslintrc.js
module.exports = {
    // 解析 ES6
    'parser': 'babel-eslint',
    'parserOptions': {
        // 启用 ES8 语法反对
        'ecmaVersion': 2017,    
        // module 示意 ECMAScript 模块
        'sourceType': 'module',
        // 应用额定的语言个性
        'ecmaFeatures': {
            'experimentalObjectRestSpread': true,
            'jsx': true,
            'modules': true,
        }
    },
    // 这些环境并不是互斥的,所以你能够同时定义多个
    'env': {
        'browser': true,
        'jquery': true,
        'node': true,
        'commonjs': true,
        'es6': true,
    },
    'root': true,
    // 当拜访以后源文件内未定义的变量时,no-undef 规定将收回正告
    // 所以须要定义这些额定的全局变量
    'globals': {
        'OnlySVG': true,
        'monitor': true,
        'CanvasRender': true,
        'Vue': true,
        'VueRouter': true
    },
    'rules': {
        // 变量必须在定义的时候赋值
        // @off 先定义后赋值很常见
        'init-declarations': 0,
        // jsx 语法中,属性的值必须应用双引号
        'jsx-quotes': [2, 'prefer-double'],
        // 对象字面量冒号前后的空格应用规定
        // @off 不关怀
        'key-spacing': 0,
        // 关键字前后必须有空格
        'keyword-spacing': 2,
        // 换行符应用规定
        // @off 不关怀
        'linebreak-style': 0,
        // 单行正文必须写在前一行还是行尾
        // @off 不限度
        'line-comment-position': 0,
        // 正文前后是否要空一行
        // @off 不限度
        'lines-around-comment': 0,
        // 最大块嵌套深度为 5 层
        'max-depth': [2, 5],
        // catch 中不得应用已定义的变量名
        'no-catch-shadow': 2,
        // class 定义的类名不得与其它变量重名
        'no-class-assign': 2,
        // 禁止正则表达式中呈现 Ctrl 键的 ASCII 示意,即 /\x1f/
        'no-control-regex': 2,
        // 禁止应用 eval
        'no-eval': 2,
        // 禁止呈现无用的表达式
        'no-unused-expressions': [2,
            {'allowShortCircuit': true, // 容许应用 a() || b 或 a && b()
                'allowTernary': true, // 容许在表达式中应用三元运算符
                'allowTaggedTemplates': true, // 容许标记模板字符串
            }
        ],
        // 禁止定义不应用的 label
        'no-unused-labels': 2,
        // 禁止定义不应用的变量
        'no-unused-vars': [2,
            {
                'vars': 'all', // 变量定义必须被应用
                'args': 'none', // 对于函数形参不检测
                'ignoreRestSiblings': true, // 疏忽残余子项 fn(...args),{a, b, ...coords}
                'caughtErrors': 'none', // 疏忽 catch 语句的参数应用
            }
        ],
        // 禁止在变量被定义之前应用它
        'no-use-before-define': [2,
            {
                'functions': false, // 容许函数在定义之前被调用
                'classes': false, // 容许类在定义之前被援用
            }
        ],
        // 禁止 Yoda 格局的判断条件,如 if (true === a),应应用 if (a === true)
        'yoda': 2,
    }
};

具体的配置文档:- configuring
具体的规定文档:- rules

除了在配置文件中指定规定外,还能够在代码中指定规定,代码文件内以正文配置的规定会笼罩配置文件里的规定,即优先级要更高。平时咱们罕用的就是 eslint-disable-next-line

/* eslint-disable-next-line no-alert */
alert('foo');

总结

整个 Eslint 的源码跟着流程图简略的走了一遍, 可能讲的内容无限,要是深刻的去钻研的话肯定会有更大的播种,
咱们用任何框架或者工具之前多读读源码还是挺有意义的, 讲的不对的中央,还望各位大佬多多指导。

参考

  • Eslint 官网
  • Eslint 源码
  • ESLint 工作原理探讨
退出移动版