聊聊Eslint 的 Disble、Enable 的注釋配置是怎么實(shí)現(xiàn)的
不知道大家有沒(méi)有用過(guò) eslint 的注釋的配置方式:
- /* eslint-disable no-alert, no-console */
- alert('foo');
- console.log('bar');
- /* eslint-enable no-alert, no-console */
- // eslint-disable-next-line
- alert('foo');
eslint 支持 eslint-disable、eslint-enable、eslint-disable-next-line 等指定某個(gè) rule 是否生效的行內(nèi)配置,叫做 inline config。
webpack 中也有這種配置方式,可以在動(dòng)態(tài)引入一個(gè)模塊的時(shí)候配置代碼分割的方式,叫做 magic comment。
- import(
- /* webpackChunkName: "my-chunk-name" */
- /* webpackMode: "lazy" */
- /* webpackExports: ["default", "named"] */
- 'module'
- );
類似的,terser 也有這種機(jī)制,叫做 annotation,可以指定某個(gè) api 是否是純的,純函數(shù)的話如果沒(méi)用到可以直接刪除。
- var a = /*#__PURE__*/React.createElement("div", null);
可以看到,很多庫(kù)都用到了這種通過(guò)注釋來(lái)配置的方式,不管是叫 annotation 也好、magic comment 也好,或者 inline config 也好,都指的同一個(gè)東西。
既然是這么常見(jiàn)的配置方式,那么他們是怎么實(shí)現(xiàn)的呢?
注釋中配置的實(shí)現(xiàn)原理
我們拿 eslint 的 inline config 的實(shí)現(xiàn)來(lái)看一下。
eslint 會(huì)把源碼 parse 成 AST,然后對(duì)把 AST 傳入一系列 rule 來(lái)做檢查,檢查結(jié)果會(huì)用 formatter 格式化后輸出。
注釋的配置是在哪一步生效的呢?
我簡(jiǎn)化了一下源碼,是這樣的:
- verify(text) {
- // parse 源碼
- const ast = parse(text);
- // 調(diào)用 rule,拿到 lint 的問(wèn)題
- const lintingProblems = runRules(ast);
- // 通過(guò) AST 拿到注釋中的配置
- const commentDirectives = getDirectiveComments(ast);
- // 根據(jù)注釋中的配置過(guò)濾問(wèn)題
- return applyDisableDirectives(lintingProblems, commentDirectives);
- }
可以看到,整體流程是:
- 把源碼 parse 成 AST
- 調(diào)用 rule 對(duì) AST 做檢查,拿到 lint 的 problems
- 通過(guò) AST 拿到注釋中的 diectives
- 通過(guò) directives 過(guò)濾 problems,就是最終需要報(bào)出的問(wèn)題
也就是說(shuō) eslint 的 inline config 是在 lint 完 AST,拿到各種 problems 之后生效的,對(duì) problems 做一次過(guò)濾。
那怎么從 AST 中取出 directives 的呢?又是怎么過(guò)濾 problems 的呢?
我們分別看一下。
從 AST 取出 directives 的源碼簡(jiǎn)化以后是這樣的:
- function getDirectiveComments(ast){
- const directives = [];
- ast.comments.forEach(comment => {
- const match = /^[#@](eslint(?:-env|-enable|-disable(?:(?:-next)?-line)?)?|exported|globals?)(?:\s|$)/u.exec(comment.trim());
- if (match) {
- const directiveText = match[1];
- ...
- directives.push({ type: xxx, line: loc.start.line, column: loc.start.column + 1, ruleId });
- }
- }
- return directives;
- }
其實(shí)就是對(duì) AST 中所有的 comments 的內(nèi)容做一下正則的匹配,如果是支持的 directive,就把它收集起來(lái),并且記錄下對(duì)應(yīng)的行列號(hào)。
之后就是對(duì) problems 的過(guò)濾了。
簡(jiǎn)化后的源碼是這樣的:
- function applyDisableDirectives(problems, disableDirectives) {
- const filteredProblems = [];
- const disabledRuleMap = new Map();
- let nextIndex = 0;
- for (const problem of problems) {
- // 對(duì)每一個(gè) probelm,都要找到當(dāng)前被禁用的 rule
- while (
- nextIndex < disableDirectives.length &&
- compareLocations(disableDirectives[nextIndex], problem) <= 0
- ) {
- const directive = disableDirectives[nextIndex++];
- switch (directive.type) {
- case "disable":
- disabledRuleMap.set(directive.ruleId, directive);
- break;
- case "enable":
- disabledRuleMap.delete(directive.ruleId);
- break;
- }
- }
- //如果 problem 對(duì)應(yīng)的 rule 沒(méi)有被禁用,則返回
- if (!disabledRuleMap.has(problem.ruleId)) {
- filteredProblems.push(problem);
- }
- }
- return filteredProblems;
- }
- function compareLocations(itemA, itemB) {
- return itemA.line - itemB.line || itemA.column - itemB.column;
- }
我們理下思路:
我們要過(guò)濾掉 problems 中被 disabled 的 rule 報(bào)出的 problem,返回過(guò)濾后的 problems。
可以維護(hù)一個(gè) disabledRuleMap,表示禁用的 rule。
對(duì)每一個(gè) problem,都根據(jù)行列號(hào)來(lái)從 disableDirectives 中取出 directive 的信息,把對(duì)應(yīng)的 rule 放入 disabledRuleMap。
然后看下該 problem 的 rule 是否是被禁用了,也就是是否在 disabledRuleMap 中,如果是,就過(guò)濾掉。
這樣處理完一遍,返回的 problem 就是可以報(bào)出的了。
這就是 eslint 的 eslint-disable、eslint-enable、eslint-disable-next-line 等注釋可以配置 rule 是否生效的原理。
eslint 是根據(jù)行列號(hào)找到對(duì)應(yīng)的 comment 的,其實(shí)很多 AST 中會(huì)記錄每個(gè)節(jié)點(diǎn)關(guān)聯(lián)的 comment。
比如 babel 的 AST:
這樣可以根據(jù) AST 來(lái)取出注釋,之后通過(guò)正則來(lái)判斷是否是 directive。
通過(guò)行列號(hào)來(lái)查找 comment,通過(guò) AST 找到關(guān)聯(lián)的 comment,這是兩種查找注釋的方式。
總結(jié)
注釋中的配置在 eslint、webpack、terser 等工具中都有應(yīng)用,分別叫 inline config、magic comment、annotation,但都指的同一個(gè)東西。
它們都是找到 AST 中的 comments,通過(guò)正則匹配下是否是支持的 directive(指令),然后取出對(duì)應(yīng)的信息。
找到 directive 之后,還要找到 directive 生效的地方,可以用兩種方式來(lái)查找:一種是根據(jù)行列號(hào)的比較,一種是根據(jù)關(guān)聯(lián)的 AST 來(lái)查找。
找到 directive 和對(duì)應(yīng)生效的地方之后,就可以根據(jù) directive 中的信息做各種處理了。
注釋中的配置是一種比較常見(jiàn)的配置方式,適合一些局部的配置。理解了它們的實(shí)現(xiàn)原理,能夠讓我們更好的掌握這種機(jī)制。