摘要: 完全理解ESLint。javascript
Fundebug經受權轉載,版權歸原做者全部。前端
ESLint 可謂是現代前端開發過程當中必備的工具了。其用法簡單,做用卻很大,使用過程當中不知曾幫我減小過多少次可能的 bug。其實仔細想一想前端開發過程當中的必備工具彷佛也沒有那麼多,ESLint 作爲必備之一,值得深挖,理解其工做原理。java
在正式討論原理以前,咱們仍是先來聊聊爲何要使用 ESLint。node
ESLint 其實早在 2013年 7月就發佈了,不過我首次使用,是不到三年前的一個下午(清楚的記得那時候使用的編輯器主要仍是 sublime text3 )。我在一個項目中嘗試了 ESLint ,輸入 eslint init
後按照提示最終選擇了很是出名的 airbnb 的代碼風格,結果整個項目幾乎全部文件都被標紅,嘗試使用 --fix
卻沒法所有修復,心裏十分沮喪。react
如今想一想,那時候的我對 ESLint 的認知是不完整的,在那時候的我看來 ESLint 就是輔助咱們保持代碼風格一致的工具,airbnb 的 js 風格備受你們推崇。git
那時候的我知道保持代碼風格的一致性能增長可讀性,更便於團隊合做。不過一致沒有去深想,爲何你們會推崇某特定的風格,這背後確定是有着特殊的意義。es6
保持一致就意味着要對咱們編寫的代碼增長必定的約束,ESLint 就是這麼一個經過各類規則(rule)對咱們的代碼添加約束的工具。JS 作爲一種動態語言,寫起來能夠爲所欲爲,bug 遍野,可是經過合適的規則來約束,能讓咱們的代碼更健壯,工程更可靠。github
在官方文檔 ESLint - rules 一節中,咱們能夠看到官方提供的了大量的規則,有推薦使用的("eslint:recommended"
),也有默認不啓用的,還有一些廢棄的。typescript
這和現實生活是一致的,現實生活中,咱們也在不自覺中遵照和構建着各類不一樣的規則。新的規則被構建是由於咱們在某方面有了更多的經驗總結,將其轉變爲規則多是但願之後少踩坑,也能共享一套最佳實踐,提升咱們的工做效率。 就像咱們提交代碼時,把但願你們共同遵照的約定轉變爲 MR 模板,但願全部人都能遵照。npm
在我看來 ESLint 的核心可能就是其中包含的各類規則,這些規則大多爲衆多開發者經驗的結晶:
以前看過一張圖能很好的描述 ESLint 的做用:
總得來講,ESLint 容許咱們經過自由拓展,組合的一套代碼應當遵循的規則,可讓咱們的代碼更爲健壯,其功能不只在於幫咱們的代碼風格保持統一,還能幫咱們用上社區的最佳實踐,減小錯誤。
ESLint 居然這麼重要,下面咱們來看看 ESLint 的用法及這些用法是怎麼生效的。
可能你們都已經很熟悉,ESLint 的用法包括兩部分: 經過配置文件配置 lint 規則; 經過命令行執行 lint,找出不符合規範的地方(固然有些不符合的規則也能夠嘗試修復);
配合編輯器插件,ESLint 也能很好的起做用,實際上,不少人可能更習慣這種用法。
經過 eslint --init
隨後作各類選擇是生成 eslint 配置文件的一種常見方式,以下:
$ eslint --init zhangwang@zhangwangdeMacBook-Pro-2 ? How would you like to configure ESLint? Use a popular style guide ? Which style guide do you want to follow? Airbnb (https://github.com/airbnb/javascript) ? Do you use React? No ? What format do you want your config file to be in? JavaScript
經過上述選擇,ESLint 自動爲咱們生成來配置文件 .eslintrc.js
,其內容以下:
/** .eslintrc.js */ module.exports = { "extends": "airbnb" };
不要小看上述簡單的配置, extends
中包含了 ESLint 的共享機制,讓咱們已很是低的成本就能用上最好的社區實踐。可能每一個前端開發者都聽過說 airbnb javascript style(Github Star 近 80000)。那麼問題來了,這裏的一句 "extends": "airbnb"
怎麼就讓咱們能夠用上這種規則還挺多的代碼風格的呢?
實際上這裏的airbnb
是 eslint-config-airbnb
的簡寫。咱們查一下其源碼,能夠發現以下內容:
module.exports = { extends: [ 'eslint-config-airbnb-base', 'eslint-config-airbnb-base/rules/strict', './rules/react', './rules/react-a11y', ].map(require.resolve), rules: {} };
咱們本身配置中的"extends": "airbnb"
至關於告訴 ESLint ,把 eslint-config-airbnb
的規則作爲拓展引用到咱們本身的項目中來。
若是你想知道 ESLint 源碼中是怎麼解析配置文件中的extends
關鍵字的,能夠參照下述連接指向的源碼:
config-file.js - applyExtends
extends
能夠是一個字符串,也能夠是一個數組。其中能夠包含如下內容: 已 eslint: 開頭的字符串,如 eslint:recommended,這樣寫意味着使用 ESLint 的推薦配置,在這裏能夠查看其具體有哪些規則; 已 plugin:
開頭的字符串,如 "plugin:react/recommended"
,這些寫意味着應用第三方插件,eslint-plugin-react 的全部推薦規則,關於 plugin 後文中咱們還會討論;已 eslint-config-開頭的包,這實際上是第三方規則的集合,因爲 eslint 中添加了額外的處理,咱們也能夠省略 eslint-config-,如上面的 eslint-config-airbnb-base也能夠寫做airbnb-base; 一個本地路徑,指向本地的 ESLint 配置,如 ./rules/react
;
extents 中的每一項內容最終都指向了一個和 ESLint 自己配置規則相同的對象。
若是咱們在 npm 中搜索 eslint-config-
能夠發現大量的 ESLint 拓展配置模塊,咱們能夠直接經過這些模塊在 ESLint 中使用上流行的風格,也能夠把本身的配置結果封裝爲一個模塊,供以後複用。
如今咱們明白了什麼是 extends
,不過咱們好像仍是不知道 ESLint 是怎麼工做的?怎麼辦呢?咱們來拆一下上面eslint-config-airbnb
的 extends
中用到的 eslint-config-airbnb-base,其主文件內容以下:
module.exports = { extends: [ './rules/best-practices', './rules/errors', './rules/node', './rules/style', './rules/variables', './rules/es6', './rules/imports', ].map(require.resolve), parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, rules: { strict: 'error', }, };
除了 extends
,配置文件中出現了 parserOptions
和 rules
。 經過 parserOptions
咱們能夠告知 ESLint 咱們想要支持什麼版本的 JS 語法(ecmaVersion
),源碼類型sourceType
,以及是否啓用其它一些語法相關的特性(如 jsx
), parserOptions
的配置比較簡單,能夠參考官方文檔中的相關。
rules
咱們後文中將重點講解,這裏先賣個關子。
再來看一下咱們熟悉的 extends
,若是你對官方文檔中 rules 那一節有印象,可能會發現 extends
中的項,除了 ./rules/imports
其它項與官方文檔 rule 的類別一一對應,這就有趣了,咱們先看看 ./rules/best-practices
中的內容,這有利於咱們理解 rule。
./rules/best-practices
中的內容以下:
module.exports = { rules: { // enforces getter/setter pairs in objects 'accessor-pairs': 'off', // enforces return statements in callbacks of array's methods // https://eslint.org/docs/rules/array-callback-return 'array-callback-return': ['error', { allowImplicit: true }], // treat var statements as if they were block scoped 'block-scoped-var': 'error', // disallow the use of alert, confirm, and prompt 'no-alert': 'warn', ... } }
./rules/best-practices
其實也是一個 ESLint 配置文件,但它比較純粹,其中只有 rules
一項。
前文咱們提到過,ESLint 的核心是由各類 rule 組成的集合,這個配置文件的展開,讓咱們靠近 ESLint 核心近了不少。我第一次看到這個配置文件時,心生多個疑問: 1. 這個文件中,咱們只針對單條 rule 添加了簡單的配置,error
,warn
,off
,最多也就再加了一個 option ,ESLint 是怎麼依據這些配置起做用的呢? 2. 在以往的工做過程當中,咱們曾經過註釋,如// eslint-disable-next-line no-console
或 /*eslint no-console: ["error", { allow: ["warn"] }] */
來屏蔽或者啓用某條規則,這又是怎麼生效的? 3. 多條 rule 又是如何配合工做的?
Lint 是基於靜態代碼進行的分析,對於 ESLint 來講,咱們的輸入的核心就說 rule 及其配置以及須要進行 Lint 分析的源碼。rule 是咱們本身規定的,固然能夠知足必定的規則,可是須要進行 Lint 的源碼則各不相同了,作爲開發者,咱們都明白抽象的重要性,若是咱們能抽象出 JS 源碼的共性,再對源碼進行分析也許就容易多了,而對代碼的抽象被稱做 AST(Abstract Syntax Tree(抽象語法樹))。
在我學習前端的這幾年裏,我在不少地方遇到過 AST。
AST 自己並非一個新鮮的話題,可能在任何涉及到編譯原理的地方都會涉及到它。
ESLint 使用 espree 來解析咱們的 JS 語句,來生成抽象語法樹,具體源碼是在這裏。
AST explorer 是一個很是酷的 AST 工具網站,能夠幫咱們方便的查看,一段代碼被解析成 AST 後的樣子。
從上面的截圖中,也許你也發現了若是我鼠標在右側選中某一個值時,左側也有對應的區域被高亮展現。實際上,咱們確實能夠經過 AST 方便的找到代碼中的特定內容。右側被選中的項,叫作 AST selectors ,熟悉 CSS selectors 的咱們,應該能很容易理解。
就像 CSS 選擇器同樣,AST 選擇器也有多種規則讓咱們能夠更方便的選中特定的代碼片斷,具體規則能夠參考
經過 AST selectors 咱們能夠方便的找到靜態代碼中的內容,這樣理解 rule 是怎麼生效的就有了必定的基礎了,咱們繼續上面提出的問題。
關於如何寫一條 rule,官方文檔中 Working with Rules 一節中已經有了詳細的闡述,這裏只作簡單的描述。
上文咱們提到過 ESLint 的核心就是規則(rule),每條規則都是獨立的,且均可以被設置爲禁止off
🈲️,警告warn
⚠️,或者報錯error
❌。
咱們選擇"no-debugger": "error"
來看看 rule 是如何工做的。源碼以下:
module.exports = { meta: { type: "problem", docs: { description: "disallow the use of `debugger`", category: "Possible Errors", recommended: true, url: "https://eslint.org/docs/rules/no-debugger" }, fixable: null, schema: [], messages: { unexpected: "Unexpected 'debugger' statement." } }, create(context) { return { DebuggerStatement(node) { context.report({ node, messageId: "unexpected" }); } }; } };
一條 rule 就是一個 node 模塊,其主要由 meta
和 create
兩部分組成,其中
meta
表明了這條規則的元數據,如其類別,文檔,可接收的參數的 schema
等等,官方文檔對其有詳細的描述,這裏不作贅述。create
:若是說 meta 表達了咱們想作什麼,那麼 create
則用表達了這條 rule 具體會怎麼分析代碼;Create 返回的是一個對象,其中最多見的鍵的名字能夠是上面咱們提到的選擇器,在該選擇器中,咱們能夠獲取對應選中的內容,隨後咱們能夠針對選中的內容做必定的判斷,看是否知足咱們的規則,若是不知足,可用 context.report()
拋出問題,ESLint 會利用咱們的配置對拋出的內容作不一樣的展現。
上面的代碼實際上代表在匹配到 debugger
語句時,會拋出 "Unexpected 'debugger' statement." 。
到這裏,好像咱們對 rule 的理解已經深刻不少了。經過上面這類靜態的匹配分析確實能夠幫咱們避免不少問題,不過 ESLint 好像也能幫咱們找到永遠不會執行的語句。僅僅經過上面的匹配彷佛還不足以作到這一點,這就引入了 rule 匹配的另外一個要點,code path analysis
。
咱們的程序中免不了有各類條件語句,循環語句,這讓咱們程序中的代碼不必定是順序執行,也不必定只執行一次。code path 指的是程序的執行路徑。程序能夠由若干 code path 表達,一個 code path 可能包括兩種類型的對象 CodePath
和 CodePathSegment
。
上面是一段好枯燥的描述,那麼究竟什麼是 code path。咱們舉例來講:
if (a && b) { foo(); } bar();
咱們分析一下上述代碼的可能執行路徑。
foo()
— 執行 bar()
bar()
bar()
轉換爲 AST 的表達方式,可能會更清晰,以下圖所示:
在這裏上述這個總體能夠看做一個 CodePath,而所謂 CodePathSegment 則是上述分支中的一部分,一個 code path 由多個 CodePathSegment 組成,ESLint 將 code path 抽象爲 5 個事件。
onCodePathStart
:onCodePathEnd
onCodePathSegmentStart
onCodePathSegmentEnd
onCodePathSegmentLoop
若是你感興趣 ESLint 是如何抽象出這五個事件的,可參考源碼 code-path-analyzer.js,官方文檔中 Code Path Analysis Details 中對 JS 中的 code path 也有很詳細的描述,能夠參考。
有了這些事件,咱們在靜態代碼分析過程就能有效的控制檢測的範圍了。如規則 no-fallthrough中,用以輔助避免執行多條 case
語句
const lodash = require("lodash"); const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/i; function hasFallthroughComment(node, context, fallthroughCommentPattern) { const sourceCode = context.getSourceCode(); const comment = lodash.last(sourceCode.getCommentsBefore(node)); return Boolean(comment && fallthroughCommentPattern.test(comment.value)); } function isReachable(segment) { return segment.reachable; } function hasBlankLinesBetween(node, token) { return token.loc.start.line > node.loc.end.line + 1; } module.exports = { meta: {...}, create(context) { const options = context.options[0] || {}; let currentCodePath = null; const sourceCode = context.getSourceCode(); let fallthroughCase = null; let fallthroughCommentPattern = null; if (options.commentPattern) { fallthroughCommentPattern = new RegExp(options.commentPattern); } else { fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT; } return { onCodePathStart(codePath) { currentCodePath = codePath; }, onCodePathEnd() { currentCodePath = currentCodePath.upper; }, SwitchCase(node) { if (fallthroughCase && !hasFallthroughComment(node, context, fallthroughCommentPattern)){ context.report({ message: "Expected a 'break' statement before '{{type}}'.", data: { type: node.test ? "case" : "default" }, node }); } fallthroughCase = null; }, "SwitchCase:exit"(node) { const nextToken = sourceCode.getTokenAfter(node); if (currentCodePath.currentSegments.some(isReachable) && (node.consequent.length > 0 || hasBlankLinesBetween(node, nextToken)) && lodash.last(node.parent.cases) !== node) { fallthroughCase = node; } } }; } };
上面這條規則中就用到了 onCodePathStart
和 onCodePathEnd
來控制 currentCodePath
的值。整體想作的事情也比較簡單,在SwitchCase
的時候判斷是否是 fallthrough
了,若是 fallthrough
而且沒有相關評論聲明這裏是有意 fallthrough,則拋出錯誤。
不過這裏又有一個問題,SwitchCase:exit
是啥?要理解 :exit
咱們就須要知道 ESLint 是如何對待一段代碼的了。咱們回到源碼中找答案。
咱們節錄了 eslint/lib/util/traverser.js
中的部分代碼
traverse(node, options) { this._current = null; this._parents = []; this._skipped = false; this._broken = false; this._visitorKeys = options.visitorKeys || vk.KEYS; this._enter = options.enter || noop; this._leave = options.leave || noop; this._traverse(node, null); } _traverse(node, parent) { if (!isNode(node)) { return; } this._current = node; this._skipped = false; this._enter(node, parent); 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(); } } if (!this._broken) { this._leave(node, parent); } this._current = parent; }
觀看上述代碼,咱們會發現,對 AST 的遍歷,用到了遞歸,實際上是存在一個由外向內再向外的過程的。:exit
其實至關於添加了一重額外的回調,讓咱們對靜態代碼有了更多的控制。
好啦,咱們總結一下,selector
,selector:exit
,code path event
其實能夠看做對代碼 AST 遍歷的不一樣階段,讓咱們對一段靜態代碼的分析有了充分的控制。
至此,咱們對如何寫一條 rule 已經有了充分的瞭解。繼續上面提出的其它問題,
如今咱們已經知道,咱們能夠經過多種途徑傳入 rule。咱們先看這些 rule 是怎麼組合的
最後生效的 rule 主要有兩個來源: 1. 配置文件中涉及到的 rule ,對於這部分 rule 的處理,主要位於源碼中的 lib/rule.js 中; 2. 評論中的 rule ,對這部分 rule ,被稱做 directive rule 的處理,ESLint 對其的處理位於 getDirectiveComments 函數中;
這裏有點像之前在學校使用遊標卡尺,配置文件中的 rule 用做粗調,文件內部的 rule 用做精調。
最終的 rule 是這兩部分的組合,被稱做 configuredRules 的對象,其中每一項的內容相似於 'accessor-pairs': 'off'
。
獲取到全部的須要對某個文件應用的規則後,接下來的應用則是一個典型的多重遍歷過程,源碼位於 runRules 中 ,節選部份內容放在下面:
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 }); 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) { if (reportTranslator === null) {...} 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(); }); }); 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; }
lintingProblems
中;這裏有些細節,咱們沒有討論到,不過大體就是這樣一個過程了,都說 node 的事件驅動機制是其特點之一,這裏在 ESLint 中應用rule 是一個事件驅動的好範例。
至此咱們已經理解了 ESLint 的核心,不過還有一些內容沒有涉及到。咱們繼續拆解 eslint-config-airbnb
中出現的 './rules/react'
module.exports = { plugins: [ 'react', ], parserOptions: { ecmaFeatures: { jsx: true, }, }, rules: { 'jsx-quotes': ['error', 'prefer-double'], ... }, settings: { 'import/resolver': { node: { extensions: ['.js', '.jsx', '.json'] } }, ... } }
這裏又出現了兩個咱們至今還未討論過的配置,這裏又出現了兩個咱們至今還未見過的配置plugin
和 settings
。settings
用來傳輸一些共享的配置,比較簡單,使用可參考官方文檔,這裏再也不贅述。不過 plugin
也是 ESLint 的重點之一,指的咱們再討論一下。
plugin 有兩重概念;
plugins: ['react',],
;eslint-plugin-
就能發現不少,比較出名的有 eslint-plugin-react ,eslint-plugin-import plugin 其實能夠看做是第三方規則的集合,ESLint 自己規則只會去支持標準的 ECMAScript 語法,可是若是咱們想在 React 中也使用 ESLint 則須要本身去定義一些規則,這就有了 eslint-plugin-react 。
plugin 的配置和 ESLint 配置文件的配置很類似,在此再也不贅述。若是你感興趣,能夠參考官方文檔 - working-with-plugins。
不過咱們這裏能夠提一下 plugin 的兩種用法:
extends
中使用, plugin 具備本身的命名空間,可經過」extends": ["plugin:myPlugin/myConfig"]
引用 plugin 中的某類規則(多是所有,也多是推薦);plugin
中使用,如添加配置 plugins: ['react',],
可聲明本身想要引用的eslint-plugin-react
提供的規則,可是具體用哪些,怎麼用,仍是須要在 rules 中配置;對 plugin 的支持讓 ESLint 更開放,增長了其自己的使用範圍。
不過還有另一個問題,JS 發展這麼快,若是咱們想對一些非標準 JS 語法添加 Lint 怎麼辦呢?有辦法,ESLint 還支持咱們自定義 parser。
只須要知足 ESLint 的規定,ESLint 支持自定義 parser,實際上社區在這方面也作了不少工做。好比
自定義的 parser 使用方法以下:
{ "parser": "./path/to/awesome-custom-parser.js" }
經過自定義 parser ,ESLint 的使用場景又被大大拓展。
咱們再去 Configuring ESLint 看看 ESLint 支持的全部配置,看看還有哪些是咱們比較陌生的。
global
,env
: 不可避免的咱們會使用到一些全局變量,而使用全局變量和一些規則的使用是有衝突的,經過global
配置咱們能夠聲明想用的全局變量,而 env 則是對全局變量的一重封裝,讓咱們能夠直接使用,讓咱們能夠直接使用一些常見的庫暴露出來的全局變量,能夠查看 eslint/environments.js 查看其實現細節。還有一些 ESLint 相關的內容在本文沒有提到,好比說 formatters,ESLint 畢竟最終是要給人看的,好的輸出很重要,關於 formatters 不少庫都會有涉及到,也是一個頗有意思的話題,能夠很大的話題,還有就是 ESLint 的層疊機制,忽略機制等等。
另外就是每條 rule 的使用都有其背後的緣由,不少 rule 我都沒有仔細去看其添加的初衷,不過在下次用以前,也許我會提醒一下本身爲何要用這條 rule,感受會頗有意思,你也能夠看看。
ESLint 就說到這裏了,用了好久的 ESLint ,可是此次嘗試把 ESLint 說清楚真的是花了好久,因此還有些別的想聊一下。
當咱們在學習一個工具時,咱們在學什麼?
我想,當咱們學習一個工具時 首先咱們應當瞭解其想要解決的問題,知道了具體想解決的問題就能夠對症下藥,就不容易致使分不清 prettier
和 ESLint
的區別了; 其次,咱們應當去了解其基本用法; 再次,若是這個工具足夠經常使用,可能咱們應當深刻去了解其實現用到的一些基礎,好比說此次咱們瞭解了 AST,也許下次去理解 Babel 是怎麼工做的,就會簡單不少,而不會把它又看成一個全新的東西。
第二個想聊的問題是,若是遇到本身沒法理解的問題怎麼辦? 我想,慢慢的,咱們已經逐漸養成了閱讀官方文檔的習慣,隨後,但願也能逐步養成閱讀源碼的習慣,讀源碼能解決不少本身看文檔沒法理解的問題。
若是能看到這裏,不妨點個贊吧。哈哈哈
關於 ESLint 的使用:
關於 AST: