源码分析-eslint

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

内容简介: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;
    }
复制代码

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

查看所有标签

猜你喜欢:

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

算法分析-有效的学习方法(影印版)

算法分析-有效的学习方法(影印版)

Jeffrey J.McConnell / 高等教育出版社 / 2003-03-01 / 28.0

本书主要目标是提高读者关于算法对程序效率的影响等问题的认知水平,并培养读者分析程序中的算法所必需的技巧。各章材料以激发读者有效的、协同的学习方法的形式讲述。通过全面的论述和完整的数学推导,本书帮助读者最大限度地理解基本概念。 本书内容包括促使学生参与其中的大量程序设计课题。书中所有算法以伪码形式给出,使得具备条件表达式、循环与递归方面知识的读者均易于理解。本书以简洁的写作风格向读者介绍了兼具......一起来看看 《算法分析-有效的学习方法(影印版)》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

MD5 加密
MD5 加密

MD5 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具