源码分析-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的整体执行过程。
首先在讲解流程之前先来了解两个类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; } 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员代码面试指南:IT名企算法与数据结构题目最优解
左程云 / 电子工业出版社 / 2015-9 / 79.00元
这是一本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现。针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近200道真实出现过的经典代码面试题,帮助广大程序员的面试准备做到万无一失。“刷”完本书后,你就是“题王”!__eol__本书采用题目+解答的方式组织内容,并把面试题类型相近或者解法相近的题目尽量放在一起,读者在学习本书时很容易看出面试题解法之间的联......一起来看看 《程序员代码面试指南:IT名企算法与数据结构题目最优解》 这本书的介绍吧!