源码分析-eslint

栏目: JavaScript · 发布时间: 6年前

内容简介:eslint是前端项目工程化的质量保障,它提供静态检查代码规范、语法错误、自定义规范以及自动修复等功能;使用它能规避代码规格的不统一和语法错误,目前很多前端项目都集成了eslint,但是很少有人能熟练的使用它更别说了解它的执行机制,导致eslint被人们当成一个喜欢它但是又不愿意去接触它的工具;本文主要介绍eslint的执行流程和源码解析,让大家知道eslint的运行原理,以方便后续大家能更好的使用它以及可以扩展一些自己的插件。eslint的应用场景很多,比如在提交代码之前检查来阻止提交、集成在某些框架中

eslint是前端项目工程化的质量保障,它提供静态检查代码规范、语法错误、自定义规范以及自动修复等功能;使用它能规避代码规格的不统一和语法错误,目前很多前端项目都集成了eslint,但是很少有人能熟练的使用它更别说了解它的执行机制,导致eslint被人们当成一个喜欢它但是又不愿意去接触它的工具;本文主要介绍eslint的执行流程和源码解析,让大家知道eslint的运行原理,以方便后续大家能更好的使用它以及可以扩展一些自己的插件。

eslint集成第三方项目

eslint的应用场景很多,比如在提交代码之前检查来阻止提交、集成在某些框架中、编辑器插件实时检查代码规范等使用。

eslint集成git

使用eslint来做提交之前检查时,首先实现git的钩子函数pre-commit,在钩子函数插件中使用child_process来调用git的diff获取到变化的文件,然后继续使用child_process来调用eslint来做代码检查或者修复。

const exec = require('child_process').exec;
const os = require('os');

function lint(cb, conf) {
  const platform = os.platform();
  const unsetCommand = platform == 'win32'
   ? 'set GIT_DIR ='
   : 'unset GIT_DIR';
  const diffCommand = 'git diff HEAD --name-only --diff-filter=ACMR -- ';
  const command = unsetCommand + ' && ' + diffCommand;

  // console.log(command);
  exec(command + name + '', (error, stdout, stderr) => {
      // 再此调用exec调用eslint命令
  })
复制代码

eslint集成其他编辑器插件

有的项目需要更复杂、更高效的操作eslint,不可能像集成于git一样很粗暴的解决;eslint专门提供了一套api,可以单独的处理某一类需求。

eslint执行流程

上面简单介绍了eslint的应用场景后,若需要更复杂的使用eslint,那么必须知道eslint的工作原理和代码结构才能更好的调用eslint的API,接下来一步一步讲解一下eslint的执行流程,下图是eslint的整体执行过程。

源码分析-eslint

首先在讲解流程之前先来了解两个类linter和CliEngine

  • CLIEngine 该类是eslint的大脑,控制eslint的执行流程,调用api时一般只需要操作CLIEngine即可
  • Linter 该类是selint的执行总裁,配置文件加载、校验、修复都是该类来控制完成的

启动

当执行eslint命令时,eslint会调用bin下的eslint.js来启动eslint的代码检查,该文件会调用cli.js来实例化CLIEngine和调用CLIEngine的方法来处理文件的检查。

初始化

eslint实例化CLIEngine时会做了如下几件事:

  • 合并配置参数和默认参数

  • 实例化Linter对象,在Linter类的构造函数中会实例化一个Rules对象和Environments对象

    • 实例化Rules时会在构造函数中读取lib/rules的所有文件(所有的检查规则),并且以文件名称作为key,绝对路径作为value存储在map中
    • 实例化Environments读取conf/environments(该文件的配置是所有执行环境的配置)的内容,并且放在this._environments上
  • 若配置了rulePaths,则读取自定义的检查规则

  • 若配置了rules,则校验rules的每一项是否合法

  • 实例化Config,Config是存放所有的检查规则和插件

CLIEngine实例化完成后会返回一个CLIEngine对象,可以调用该对象的executeOnFiles(检查多个文件)或者executeOnText(检查文本)来进行代码检查。

代码检查

虽然eslint提供了executeOnFiles和executeOnText两个代码检查的接口,但是下面使用executeOnFiles来讲述检查过程,其实在CLIEngine内部这两个都是调用Linter的api来进行检查的;executeOnFiles接口接受一个数组,该数组的每一项是即将要检查文件的路径,在该方法中会循环所有的文件,若该文件不是忽略文件则调用processFile方法,processFile方法中读取文件内容后调用processText方法来处理,在该方法中加载规则,并调用Linter来真正的开始检查。

规则和插件的加载

根据被检查文件调用configHelper.getConfig方法获取所有的检查项,然后把所有的插件加载到内存中,并且把插件中的检查规则放在rules对象中。

// 读取配置,并且价值插件
 const config = configHelper.getConfig(filePath);

    if (config.plugins) {
        configHelper.plugins.loadAll(config.plugins);
    }

    const loadedPlugins = configHelper.plugins.getAll();

    for (const plugin in loadedPlugins) {
        if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) {
            processor = loadedPlugins[plugin].processors[fileExtension];
            break;
        }
    }
复制代码

AST生成和作用域的创建

所有的配置读取完成后会调用Linter的verifyAndFix方法来真正的检查和修复流程;首先会判断该文件是否需要修复,然后做一个最多10次的循环,在该循环中实现检查和修复文件;循环规则是如果不修复只检查则只循环一次,若需要修复,则把检查需要修复的消息给SourceCodeFixer.applyFixes进行修复,返回一个被修复后的文本,再次循环,直到无修复或者修复次数大于10次终止该修复流程。之所以有循环是因为多条修复规则有交叉情况,在一次修复中不能同时修复;另一个原因是防止修复错误。

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;

        /**
         * This loop continues until one of the following is true:
         *
         * 1. No more fixes have been applied.
         * 2. Ten passes have been made.
         *
         * That means anytime a fix is successfully applied, there will be another pass.
         * Essentially, guaranteeing a minimum of two passes.
         */
        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);

            /*
             * stop if there are any syntax errors.
             * 'fixedResult.output' is a empty string.
             */
            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 the last result had fixes, we need to lint again to be sure we have
         * the most up-to-date information.
         */
        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;
    }
复制代码

在调用this.verify之前eslint会先把文本转换成AST语法树,并且根据语法树生产检查时需要使用的作用域;首先会选择AST解析器来解析文本,若用户配置了解析器则使用用户配置的解析器,否则使用默认的,具体eslint支持哪些AST解析器可以参考官网;生产AST语法树后eslint会使用eslint-scope插件遍历所有的AST节点生成作用域,存放在ScopeManager类中。

代码检查

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

function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename) {
    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),
                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]);

        if (severity === 0) {
            return;
        }

        const rule = ruleMapper(ruleId);
        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 });
                        }
                        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]
            );
        });
    });

    const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));

    nodeQueue.forEach(traversalInfo => {
        currentNode = traversalInfo.node;

        if (traversalInfo.isEntering) {
            eventGenerator.enterNode(currentNode);
        } else {
            eventGenerator.leaveNode(currentNode);
        }
    });

    return lintingProblems;
}
复制代码

代码修复

eslint的代码修复在文件source-code-fixer.js中实现的,eslint的代码修复思想很新奇,首先eslint实现了一个10次的循环,但是该循环最多执行10此,若不修复该循环只执行一次,若修复则修复完毕即结束;每次循环都会把校验完饭后的信息给SourceCodeFixer进行代码修复。

在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次则表示无法再进一步修复了,那么结束修复流程。

// 第一次修复条件调用时lastPos是-∞
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;
    }
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员代码面试指南:IT名企算法与数据结构题目最优解

程序员代码面试指南:IT名企算法与数据结构题目最优解

左程云 / 电子工业出版社 / 2015-9 / 79.00元

这是一本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现。针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近200道真实出现过的经典代码面试题,帮助广大程序员的面试准备做到万无一失。“刷”完本书后,你就是“题王”!__eol__本书采用题目+解答的方式组织内容,并把面试题类型相近或者解法相近的题目尽量放在一起,读者在学习本书时很容易看出面试题解法之间的联......一起来看看 《程序员代码面试指南:IT名企算法与数据结构题目最优解》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

MD5 加密
MD5 加密

MD5 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具