写点什么

ESLint- 源码分析

作者:Tone荣
  • 2022 年 2 月 25 日
  • 本文字数:6562 字

    阅读完需:约 22 分钟

ESLint-源码分析

前端的日常开发离不开各种 lint 的支持,但是有的人就是不喜欢,代码写法随意任性,这个我们就不去强求了。实际上规范的定义主要取决于开源项目作者的习惯,或者公司团队编码的习惯,即使两个前端专家,写出的代码规范也会有差别。ESLint 的初衷是为了让程序员可以创建自己的检测规则。ESLint 的所有规则都被设计成可插入的。ESLint 的默认规则与其他的插件并没有什么区别,规则本身和测试可以依赖于同样的模式。为了便于人们使用,ESLint 内置了一些规则,当然,你可以在使用过程中自定义规则。

我们今天主要从源码中探索 ESLint 的工作原理。

为什么要使用 ESLint

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



  • 如果你没有用 ESLint ,你的代码需要人工来检查,格式可能就千姿百态,自然 bug 也就避免不了层出不穷,看见你代码的其他开发者心情也是极糟的。

  • 如果你使用了 ESLint ,对你的代码从各个方面进行检查,运行起来的问题自然会少很多,别的同学阅读起来也就很满意了。


总体概括 ESLint 是一种静态代码分析工具,用于识别在 JavaScript 代码中发现的有问题的模式。不仅能够让我们减少 bug,而且还能帮我们统一编码风格,更容易多人长久去维护我们的代码和项目。


你是不是以为我会讲如何安装、配置、使用 ESlint?NO... 话不多说 走起~

Eslint 执行流程

要是需要更好的使用 Eslint,那么必须知道 Eslint 的工作原理和代码结构才能更好的了解 Eslint,接下来会讲解一下 Eslint 的执行流程,下图是 Eslint 的整体执行过程。



首先先来了解两个类 linterCliEngine


  • 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.jsmodule.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 的源码跟着流程图简单的走了一遍,可能讲的内容有限,要是深入的去研究的话一定会有更大的收获,我们用任何框架或者工具之前多读读源码还是挺有意义的,讲的不对的地方,还望各位大佬多多指点。

参考

用户头像

Tone荣

关注

还未添加个人签名 2022.02.24 加入

简简单单,有意思的活着

评论

发布
暂无评论
ESLint-源码分析