摘要:自定義規(guī)則校驗(yàn)代碼業(yè)務(wù)邏輯是社區(qū)中主流的工具,提供的大量規(guī)則有效的保障了許多項(xiàng)目的代碼質(zhì)量。本文將介紹如何通過(guò)自定義檢查規(guī)則,校驗(yàn)項(xiàng)目中特有的一些業(yè)務(wù)邏輯,如特殊作用域特殊使用規(guī)范性等。
自定義 eslint 規(guī)則校驗(yàn)代碼業(yè)務(wù)邏輯
eslint 是 JavaScript 社區(qū)中主流的 lint 工具,提供的大量規(guī)則有效的保障了許多項(xiàng)目的代碼質(zhì)量。本文將介紹如何通過(guò)自定義 eslint 檢查規(guī)則,校驗(yàn)項(xiàng)目中特有的一些業(yè)務(wù)邏輯,如 i18n、特殊作用域、特殊 API 使用規(guī)范性等。代碼靜態(tài)分析與 eslint
代碼靜態(tài)分意指是不需要實(shí)際執(zhí)行代碼就能獲取到程序中的部分信息并加以使用,lint 就是其中一種常見的實(shí)踐,通常為檢查代碼中錯(cuò)誤的寫法或是不符合標(biāo)準(zhǔn)的代碼風(fēng)格。許多編程語(yǔ)言都自帶 lint 工具,甚至直接將其植入到編譯器中。
但這一重要的功能對(duì)于 JavaScript 來(lái)說(shuō)卻是一大痛點(diǎn),作為動(dòng)態(tài)且弱類型的語(yǔ)言 JavaScript 沒(méi)有編譯階段也就無(wú)從進(jìn)行靜態(tài)分析,這導(dǎo)致程序錯(cuò)誤只能在運(yùn)行時(shí)被發(fā)現(xiàn),部分錯(cuò)誤非常低級(jí)例如variable is undefined。而當(dāng)程序變得更為復(fù)雜時(shí),這類錯(cuò)誤甚至難以在開發(fā)、測(cè)試階段暴露,只會(huì)在用戶實(shí)際使用的過(guò)程中遇到,造成嚴(yán)重的后果。
為了彌補(bǔ)語(yǔ)言天生的弱點(diǎn),社區(qū)開發(fā)出了一些 lint 工具,在所謂預(yù)編譯階段完成代碼的靜態(tài)分析檢查,而 eslint 就是其中的佼佼者。現(xiàn)在社區(qū)已經(jīng)普遍接受使用 eslint 作為代碼規(guī)范工具,也延伸出了許多常用的規(guī)則與規(guī)則集。但實(shí)際上 eslint 拓展性極佳,我們還可以基于 eslint 提功的靜態(tài)分析能力對(duì)代碼進(jìn)行業(yè)務(wù)邏輯的檢查,本文將講解一些筆者所在項(xiàng)目中的靜態(tài)分析實(shí)踐,以說(shuō)明這一方案的適用場(chǎng)景和優(yōu)缺點(diǎn)。
eslint 基本原理首先快速說(shuō)明 eslint 工作的基本流程,幫助理解它將給我們提供哪些方面的能力以及如何編寫我們的自定義規(guī)則。
配置規(guī)則與插件eslint 主要依靠配置決定執(zhí)行哪些規(guī)則的校驗(yàn),例如我們可以通過(guò)配置no-extra-semi決定是否需要寫分號(hào),這類規(guī)則中不包含具體的業(yè)務(wù)邏輯,而是對(duì)所有項(xiàng)目通用,因此會(huì)被集成在 eslint 的內(nèi)置規(guī)則中。
而還有一些規(guī)則也不包含業(yè)務(wù)邏輯,但只在部分項(xiàng)目場(chǎng)景中使用,如 React 相關(guān)的大量規(guī)則,那么顯然不應(yīng)該集成在內(nèi)置規(guī)則中,但也應(yīng)該自成一個(gè)集合。這種情況下 eslint 提供了另一種規(guī)則單位——插件,可以作為多個(gè)同類規(guī)則的集合被引入到配置中。
如果我們準(zhǔn)備自定義一些規(guī)則用于校驗(yàn)項(xiàng)目中的業(yè)務(wù)邏輯,那么也應(yīng)該創(chuàng)建一套自用的插件,并將自用的規(guī)則都存放其中。推薦使用 eslint 的 yeoman generator 腳手架新建插件或規(guī)則,該腳手架能夠生成插件項(xiàng)目的目錄結(jié)構(gòu)、規(guī)則文件、文檔以及單元測(cè)試等模版,下文中我們將通過(guò)示例理解這些文件的的作用。
JavaScript 解析如上文所說(shuō),要實(shí)現(xiàn)靜態(tài)分析則需要自建一個(gè)預(yù)編譯階段對(duì)代碼進(jìn)行解析,eslint 也不例外。
首先我們看看大部分編譯器工作時(shí)的三個(gè)階段:
解析,將未經(jīng)處理的代碼解析成更為抽象的表達(dá)式,通常為抽象語(yǔ)法樹,即 AST。
轉(zhuǎn)換,通過(guò)修改解析后的代碼表達(dá)式,將其轉(zhuǎn)換為符合預(yù)期的新格式。
代碼生成,將轉(zhuǎn)換后的表達(dá)式生成為新的目標(biāo)代碼。
如果想快速的加深對(duì)編譯器工作原理的理解,推薦閱讀 the-super-tiny-compiler。
對(duì)于 eslint 而言,主要是將 JavaScript 代碼解析為 AST 之后,再在遍歷 AST 的過(guò)程中對(duì)代碼進(jìn)行各個(gè)規(guī)則的校驗(yàn)。因此 eslint 也有一個(gè)解析器用于將原始代碼解析為特定的 AST,目前所使用的解析器是 eslint 基于 Acorn 開發(fā)的一個(gè)名為 Espree 的項(xiàng)目。而對(duì)于我們編寫自定義規(guī)則來(lái)說(shuō)更關(guān)心的是解析器生成的 AST 節(jié)點(diǎn)的結(jié)構(gòu),在閱讀 eslint 文檔之后會(huì)了解到包括 Espree 在內(nèi)的許多編譯器項(xiàng)目都需要一套 JavaScript 的 AST 規(guī)范,而為了保證規(guī)范的一致性以及實(shí)效性,社區(qū)共同維護(hù)了一套規(guī)范:estree。
在接下來(lái)講解規(guī)則編寫與執(zhí)行的過(guò)程中,我們將直接引用 estree 的各種 AST 結(jié)構(gòu)。
規(guī)則的執(zhí)行eslint 中一般一個(gè)規(guī)則存放在一個(gè)文件中,以 module 的形式導(dǎo)出并掛載,其結(jié)構(gòu)如下:
module.exports = { meta: { docs: { description: "disallow unnecessary semicolons", category: "Possible Errors", recommended: true, url: "https://eslint.org/docs/rules/no-extra-semi", }, fixable: "code", schema: [], // no options }, create: function(context) { return { // callback functions }; }, };
其中meta部分主要包括規(guī)則的描述、類別、文檔地址、修復(fù)方式以及配置下 schema 等信息,對(duì)于項(xiàng)目中自用的規(guī)則來(lái)說(shuō)可以只填寫基本的描述和類別,其余選項(xiàng)在有需要時(shí)再根據(jù)文檔補(bǔ)充,并不會(huì)影響規(guī)則的檢驗(yàn)邏輯。
而create則需要定義一個(gè)函數(shù)用于返回一個(gè)包含了遍歷規(guī)則的對(duì)象,并且該函數(shù)會(huì)接收context對(duì)象作為參數(shù),context對(duì)象中除了包含report等報(bào)告錯(cuò)誤的方法之外,還提供了許多幫助方法,可以簡(jiǎn)化規(guī)則的編寫。下文中我們會(huì)通過(guò)幾個(gè)示例理解create函數(shù)的使用方式,但首先可以通過(guò)一段代碼建立初步的印象:
module.exports = { create: function(context) { // declare the state of the rule return { ReturnStatement: function(node) {}, "FunctionExpression:exit": function(node) {}, "ArrowFunctionExpression:exit": function(node) {}, }; }, };
在這段代碼中我們可以看到create返回的所謂“包含了遍歷規(guī)則的對(duì)象”的基本結(jié)構(gòu)。對(duì)象的 value 均為一個(gè)接收當(dāng)前 AST 節(jié)點(diǎn)的函數(shù),而 key 則是 eslint 的節(jié)點(diǎn) selector。selector 分為兩部分,第一部分為必須聲明的 AST 節(jié)點(diǎn)類型,如ReturnStatement和FunctionExpression。第二部分則是可選的:exit標(biāo)示,因?yàn)樵诒闅v AST 的過(guò)程中會(huì)以“從上至下”再“從下至上”的順序經(jīng)過(guò)節(jié)點(diǎn)兩次,selector 默認(rèn)會(huì)在下行的過(guò)程中執(zhí)行對(duì)應(yīng)的訪問(wèn)函數(shù),如果需要再上行的過(guò)程中執(zhí)行,則需要添加:exit。
那么 eslint 解析出的 AST 有哪些節(jié)點(diǎn)類型,每種節(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu)又是什么,則需要通過(guò)查看上文提到的 estree 定義文檔進(jìn)行了解。
適用場(chǎng)景與示例接下來(lái)我們會(huì)看到 eslint 自定義規(guī)則校驗(yàn)的一些具體示例,但首先我們先要明確它的適用場(chǎng)景以及與一些常見代碼 QA 手段的異同。
適用場(chǎng)景我們可以通過(guò)以下方法判斷一個(gè)工具的質(zhì)量:
工具質(zhì)量 = 工具節(jié)省的時(shí)間 / 開發(fā)工具消耗的時(shí)間
對(duì)于靜態(tài)分析來(lái)說(shuō),要想提高“工具節(jié)省的時(shí)間”,應(yīng)該要讓檢查的規(guī)則盡量覆蓋全局性的且經(jīng)常發(fā)生的問(wèn)題,如使用最為廣泛的檢查:是否使用了未定義的變量。同時(shí)還需要考慮當(dāng)問(wèn)題發(fā)生后 debug 所消耗的時(shí)間,例如有的項(xiàng)目有 i18n 需求,而在代碼的個(gè)別地方又直接使用了中文的字符串,雖然問(wèn)題很小,但是人工測(cè)試覆蓋卻很麻煩,如果能夠通過(guò)工具進(jìn)行覆蓋,那么原來(lái)用于 debug 的時(shí)間也應(yīng)該歸入“工具節(jié)省的時(shí)間”當(dāng)中。
另一方面則是對(duì)比“開發(fā)工具消耗的時(shí)間”,首先要強(qiáng)調(diào)通過(guò)靜態(tài)分析去對(duì)邏輯進(jìn)行判斷,不論是學(xué)習(xí)成本還是實(shí)際編寫成本都較高,如果一類問(wèn)題可以通過(guò)編寫簡(jiǎn)單的單元測(cè)試進(jìn)行覆蓋,那么應(yīng)該優(yōu)先考慮使用單元測(cè)試。但有的時(shí)候代碼邏輯對(duì)外部依賴較多,單元測(cè)試的開銷很大,例如我們有一段 e2e 測(cè)試的代碼,需要在目標(biāo)瀏覽器環(huán)境中執(zhí)行一段代碼,但是常規(guī)的 eslint 并不能判斷某個(gè)函數(shù)中的代碼實(shí)際執(zhí)行在另一個(gè)作用域下,部分檢查就會(huì)失效,例如瀏覽器運(yùn)行時(shí)引用的變量實(shí)際定義在本地運(yùn)行時(shí)中,eslint 無(wú)法察覺(jué)。而如果通過(guò)單元測(cè)試覆蓋,則需要實(shí)際運(yùn)行對(duì)應(yīng)的 e2e 代碼,或者 mock 其執(zhí)行環(huán)境的各種依賴,都是非常重的工作,取舍之下通過(guò)靜態(tài)分析覆蓋會(huì)事半功倍。
最后還需要考慮到使用體驗(yàn),許多編輯器都有 eslint 的集成插件,可以在編程的過(guò)程中實(shí)時(shí)檢測(cè)各個(gè)規(guī)則,在實(shí)時(shí)性方面遠(yuǎn)強(qiáng)于單元測(cè)試等 QA 手段的使用體驗(yàn)。
示例 1:i18n許多項(xiàng)目都有國(guó)際化的需求,因此項(xiàng)目中的文案需要避免直接使用中文,常見的方案包括用變量代替字符串或者使用全局的翻譯函數(shù)處理字符串,例如:
// 錯(cuò)誤:直接只用中文字符串 console.log("中文"); // 使用變量 const currentLocale = "cn"; const T = { str_1: { cn: "中文", }, }; console.log(T.str_1[currentLocale]); // 使用翻譯函數(shù)處理 console.log(t("中文"));
如果出現(xiàn)了直接使用中文字符串的錯(cuò)誤,其實(shí)在代碼運(yùn)行過(guò)程中也不會(huì)有任何錯(cuò)誤提示,只能靠 code review 和人工觀察測(cè)試來(lái)發(fā)現(xiàn)。我們嘗試自定義一條 eslint 規(guī)則解決它,此處假設(shè)項(xiàng)目中使用的是將所有中文內(nèi)容存放在一個(gè)變量中,其余地方直接引用變量的方法。
const SYMBOL_REGEX = /[u3002uff1buff0cuff1au201cu201duff08uff09u3001uff1fu300au300b]/; const WORD_REGEX = /[u3400-u9FBF]/; function hasChinese(value) { return WORD_REGEX.test(value) || SYMBOL_REGEX.test(value); } module.exports = { create: function(context) { return { Literal: function(node) { const { value } = node; if (hasChinese(value)) { context.report({ node, message: "{{ str }} contains Chinese, move it to T constant.", data: { str: node.value, }, }); } }, }; }, };
在這段代碼中,我們?cè)?b>create里遍歷所有Literal類型節(jié)點(diǎn),因?yàn)槲覀冃枰獧z查的對(duì)象是所有字符串。根據(jù) estree 的定義,我們會(huì)知道Literal類型階段結(jié)構(gòu)如下:
interface Literal <: Expression { type: "Literal"; value: string | boolean | null | number | RegExp; }
那么需要做的就是判斷該節(jié)點(diǎn)的 value 是否包含中文,在這里我們用的是正則表達(dá)式進(jìn)行判斷,當(dāng)含有中文字符或標(biāo)點(diǎn)時(shí),就調(diào)用context.report方法報(bào)告一個(gè)錯(cuò)誤。在應(yīng)用這條規(guī)則之后,全局所有直接使用中文字符串的代碼都會(huì)報(bào)錯(cuò),只需要對(duì)統(tǒng)一存放中文的變量T所在的代碼部分禁用這條規(guī)則,就可以避免誤判。
在筆者所在項(xiàng)目中我們使用的是“通過(guò)翻譯函數(shù)處理”的方式,所以規(guī)則會(huì)更為復(fù)雜一些,需要判斷當(dāng)前字符串的父節(jié)點(diǎn)是否為我們的翻譯函數(shù),Espree 會(huì)在每個(gè)節(jié)點(diǎn)上都記錄對(duì)應(yīng)的父節(jié)點(diǎn)信息,因此我們可以通過(guò)類似node.parent.callee.name === "t"這樣的方式進(jìn)行判斷。不過(guò)實(shí)際情況中還需要做更安全、全面的判斷,例如正確識(shí)別這樣的使用方式t("你好" + "世界"),后一個(gè)字符串的父節(jié)點(diǎn)是加法運(yùn)算符。
在這個(gè)示例中我們主要理解了遍歷函數(shù)的工作方式以及如何使用合理的節(jié)點(diǎn)類型實(shí)現(xiàn)需求,因此不再過(guò)度展開實(shí)際場(chǎng)景中的細(xì)節(jié)實(shí)現(xiàn)。不過(guò)相信讀者已經(jīng)可以感受到寫一條自定義規(guī)則需要非常全面的考慮代碼中的各類場(chǎng)景,這也是為什么 eslint 要求自定義規(guī)則要遵循 TDD 的開發(fā)方式,用足夠多的單元測(cè)試保證規(guī)則使用時(shí)符合預(yù)期,在最后我們會(huì)介紹 eslint 提供的單測(cè)框架。
示例 2:特殊作用域首先構(gòu)建一個(gè)場(chǎng)景用于展示這類規(guī)則:
不論是以及非常成熟的 Node.JS + selenium 體系還是較新的 headless chrome 生態(tài),這類端到端工具一般都會(huì)提供在目標(biāo)瀏覽器上執(zhí)行一段 JavaScript 的能力,例如這樣:
client.execute( function(foo, bar) { document.title = foo + bar; }, ["foo", "bar"] );
client.execute方法接收兩個(gè)參數(shù),第一個(gè)為在瀏覽器端執(zhí)行的函數(shù),第二個(gè)則是從當(dāng)前代碼傳遞給執(zhí)行函數(shù)的參數(shù),而瀏覽器端也只能使用傳遞的參數(shù)而不能直接使用當(dāng)前代碼中的變量。在這種場(chǎng)景下,很容易出現(xiàn)類似這樣的問(wèn)題:
const foo = "foo"; const bar = "bar"; client.execute(function() { document.title = foo + bar; });
對(duì)于 eslint 來(lái)說(shuō)并不知道document.title = foo + bar;將在瀏覽器端的作用域中執(zhí)行,而又發(fā)現(xiàn)有同名變量foo和bar被定義在當(dāng)前代碼中,則不會(huì)認(rèn)為這段代碼有錯(cuò)誤,這種情況下我們就可以嘗試自定義規(guī)則來(lái)對(duì)這個(gè)特殊場(chǎng)景做檢查:
module.exports = { create: function(context) { return { "Program:exit": function() { const globalScope = context.getScope(); const stack = globalScope.childScopes.slice(); while (stack.length) { const scope = stack.pop(); stack.push.apply(stack, scope.childScopes); if (scope.block.parent.callee.property.name === "execute") { const undefs = scope.through.forEach((ref) => context.report({ node: ref.identifier, message: ""{{name}}" is not defined.", data: ref.identifier, }) ); } } }, }; }, };
以上代碼中繼續(xù)省略一些過(guò)于細(xì)節(jié)的實(shí)現(xiàn),例如判斷子作用域是否為client.execute的第一個(gè)參數(shù)以及將瀏覽器中的全局變量加入未定義變量的白名單等等,重點(diǎn)關(guān)注 eslint 為我們提供的一些幫助方法。
這次我們的節(jié)點(diǎn)選擇器為Program:exit,也就是下行完畢、開始上行完整的 AST 時(shí)執(zhí)行我們的自定義檢查,Program類型的節(jié)點(diǎn)對(duì)應(yīng)的是完整的源碼樹,在 eslint 中即是當(dāng)前文件。
在檢查時(shí),首先我們使用context.getScope獲取了當(dāng)前正在遍歷的作用域,又由于我們處在Program節(jié)點(diǎn)中,這個(gè)作用域即為這個(gè)代碼文件中的最高作用域。之后我們構(gòu)建一個(gè)棧,通過(guò)不斷地把 childScopes 壓入棧中在讀取出來(lái)的方式,實(shí)現(xiàn)遞歸的訪問(wèn)到所有的子作用域。
之后在處理每個(gè)子作用域時(shí),都做了一個(gè)簡(jiǎn)單的判斷(同樣是簡(jiǎn)化過(guò)后的版本),來(lái)確定該作用域是否為我們需要獨(dú)立判斷的client.execute方法中第一個(gè)函數(shù)內(nèi)的作用域。
當(dāng)找到該函數(shù)內(nèi)的作用域之后,我們就可以使用scope對(duì)象上的各種方法進(jìn)行判斷了。事實(shí)上作用域是靜態(tài)分析中較為復(fù)雜的部分,如果完全獨(dú)立的去判斷作用域中的引用等問(wèn)題相對(duì)復(fù)雜,好在 eslint 對(duì)外暴露了 scope manager interface,讓我們可以最大程度的復(fù)用封裝好的各類作用域接口。
在 scope manager interface 中可以看到scope.through方法的描述:
The array of references which could not be resolved in this scope.
正是我們需要的!所以最后只需要簡(jiǎn)單的遍歷scope.through返回的未定義引用數(shù)組,就可以找到該作用域下所有的未定義變量。
通過(guò)這個(gè)示例,可以看出 eslint 本身已經(jīng)對(duì)許多常用需求做了高階的封裝,直接復(fù)用可以大大縮減“開發(fā)工具消耗的時(shí)間”。
示例 3:保證 API 使用規(guī)范繼續(xù)構(gòu)建一個(gè)場(chǎng)景:假如我們?cè)跇I(yè)務(wù)中我們有一個(gè)內(nèi)部 API "Checker",用于校驗(yàn)?zāi)承┎僮鳎╝ction)是否可執(zhí)行,而校驗(yàn)的方式是判斷 action 對(duì)應(yīng)的規(guī)則(rule)是否全部通過(guò),代碼如下:
const checker = new Checker({ rules: { ruleA(value) {}, ruleB(value) {}, }, actions: { action1: ["ruleA", "ruleB"], action2: ["ruleB"], }, });
在 Checker 這個(gè) API 使用的過(guò)程中,我們需要:
所有 action 依賴的 rule 都在rules屬性中被定義。
所有定義的 rule 都被 action 使用。
由于 action 和 rule 的關(guān)聯(lián)性只靠 action value 數(shù)組中的字符串名稱與 rule key 值保持一致來(lái)維護(hù),所以第一條要求如果出了問(wèn)題只能在運(yùn)行時(shí)發(fā)現(xiàn)錯(cuò)誤,而第二條要求甚至不會(huì)造成任何錯(cuò)誤,但在長(zhǎng)期的迭代下可能會(huì)遺留大量無(wú)用代碼。
當(dāng)然這個(gè)場(chǎng)景我們很容易通過(guò)單元測(cè)試進(jìn)行覆蓋,但如果 Checker 是一個(gè)在項(xiàng)目各種都會(huì)分散使用的 API,那么單元測(cè)試即使有一個(gè)通用的用例,也需要開發(fā)者手動(dòng)導(dǎo)出 checker 再引入到測(cè)試代碼中去,這本身就存在一定遺漏的風(fēng)險(xiǎn)。
從開發(fā)體驗(yàn)出發(fā),我們也嘗試用 eslint 的自定義規(guī)則完成這個(gè)需求,實(shí)現(xiàn)一個(gè)實(shí)時(shí)的 Checker API 使用方式校驗(yàn)。
首先我們需要在靜態(tài)分析階段分辨代碼中的一個(gè) Class 是否為 Checker Class,從而進(jìn)一步做校驗(yàn),單純從變量名稱判斷過(guò)于粗暴,容易發(fā)生誤判;而從 Class 來(lái)源分析很可能出現(xiàn)跨文件引用的情況,又過(guò)于復(fù)雜。所以我們借鑒一些編程語(yǔ)言中處理類似場(chǎng)景的做法,在需要編譯器特殊處理的地方加一些特殊的標(biāo)記幫助編譯器定位,例如這樣:
// [action-checker] const checker = new Checker({});
在構(gòu)造 checker 實(shí)例的前一行寫一個(gè)注釋// [action-checker],表明下一行開始的代碼是使用了 Checker API,在這基礎(chǔ)上,我們就可以開始編寫 eslint 規(guī)則:
const COMMENT_MARKER = "[action-checker]"; function getStartLine(node) { return node.loc.start.line; } module.exports = { create: function(context) { const sourceCode = context.getSourceCode(); const markerLines = {}; return { Program: function() { const comments = sourceCode.getAllComments(); comments.forEach((comment) => { if (comment.value.trim() === COMMENT_MARKER) { markerLines[getStartLine(comment)] = comment; } }); }, ObjectExpression: function(expressionNode) { const startLine = getStartLine(expressionNode); if (markLines[startLine - 1]) { // check actions and rules } }, }; }, };
在這個(gè)示例中,我們使用了context.getSourceCode獲取 sourceCode 對(duì)象,和上個(gè)例子中的 scope 類似,也是 eslint 封裝過(guò)后的接口,例如可以繼續(xù)通過(guò)sourceCode.getAllComments獲取代碼中的所有注釋。
為了實(shí)現(xiàn)通過(guò)注釋定位 checker 實(shí)例的目的,我們?cè)?b>markLines對(duì)象中存儲(chǔ)了帶有特殊標(biāo)記的注釋的行數(shù),獲取行數(shù)的方式則是node.loc.start.line。這里的loc也是 eslint 給各個(gè) AST 節(jié)點(diǎn)增加的一個(gè)重要屬性,包含了節(jié)點(diǎn)對(duì)應(yīng)代碼在源代碼中的坐標(biāo)信息。
之后遍歷所有ObjectExpression類型節(jié)點(diǎn),通過(guò)markLines中存儲(chǔ)的位置信息,確定某個(gè)ObjectExpression節(jié)點(diǎn)是否為我們需要校驗(yàn)的 checker 對(duì)象,再根據(jù) estree 中定義的ObjectExpression結(jié)構(gòu),找到我們需要的 actions values 和 rules keys 進(jìn)行比較,此處不對(duì)細(xì)節(jié)處理做進(jìn)一步展開。
這個(gè)示例說(shuō)明注釋作為靜態(tài)分析中非常重要的元素有很好的利用價(jià)值,許多項(xiàng)目也提供從一定格式(例如 JSDoc)的注釋中直接生成文檔的功能,也是代碼靜態(tài)分析常見的應(yīng)用,除了示例中用到的sourceCode.getAllComments可以獲取所有注釋,還提供sourceCode.getJSDocComment這樣只獲取 JSDoc 類型注釋的方法。
總而言之,基于 eslint 提供的強(qiáng)大框架,我們可以拓展出很多極大提高開發(fā)體驗(yàn)和代碼質(zhì)量的用法。
雜項(xiàng) 借鑒社區(qū)eslint 本身提供的功能很強(qiáng)但也很多,光從文檔中不一定能找到最適用的方法,而 eslint 本身已經(jīng)有大量的 通用規(guī)則,很多時(shí)候直接從相近的規(guī)則中學(xué)習(xí)會(huì)更加有效。例如示例 2 中對(duì)作用域的判斷就是從社區(qū)的通用規(guī)則no-undef中借鑒了很多大部分思路。
TDD上文提到,靜態(tài)分析需要非常全面的考慮編譯器會(huì)遇到的各類代碼,但如果每次編寫規(guī)則都需要在一個(gè)很大的 code base 中進(jìn)行測(cè)試效率也很低。因此 eslint 提倡用測(cè)試驅(qū)動(dòng)開發(fā)的方式,先寫出對(duì)規(guī)則的預(yù)期結(jié)果,再實(shí)現(xiàn)規(guī)則。
如果通過(guò)上文提到的 eslint yeoman 腳手架新建一個(gè)規(guī)則模版,會(huì)自動(dòng)生成一個(gè)對(duì)應(yīng)的測(cè)試文件。以示例 1 為例,內(nèi)容如下:
const rule = require("../../../lib/rules/use-t-function"); const RuleTester = require("eslint").RuleTester; const parserOptions = { ecmaVersion: 8, sourceType: "module", ecmaFeatures: { experimentalObjectRestSpread: true, jsx: true, }, }; const ruleTester = new RuleTester({ parserOptions }); ruleTester.run("use-t-function", rule, { valid: [ { code: "fn()" }, { code: ""This is not a chinese string."" }, { code: "t("名稱:")" }, { code: "t("一" + "二" + "三")" }, ], invalid: [ { code: "名稱:", errors: [ { message: "名稱: contains Chinese, use t function to wrap it.", type: "Literal", }, ], }, ], });
核心的部分是require("eslint").RuleTester提供的單測(cè)框架 Class,傳入一些參數(shù)例如解析器配置之后就可以實(shí)例化一個(gè) ruleTester。實(shí)際執(zhí)行時(shí)需要提供足夠的 valid 和 invalid 代碼場(chǎng)景,并且對(duì) invalid 類型代碼報(bào)告的錯(cuò)誤信息做斷言,當(dāng)所有測(cè)試用例通過(guò)后,就可以認(rèn)為規(guī)則的編寫符合預(yù)期了。
完整示例代碼自定義 eslint 規(guī)則在我們的實(shí)際項(xiàng)目中已經(jīng)有所應(yīng)用,示例中的實(shí)際完整規(guī)則代碼都存放在公網(wǎng) Github 倉(cāng)庫(kù)中,如果對(duì)文中跳過(guò)的細(xì)節(jié)實(shí)現(xiàn)感興趣可以自行翻看。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/107728.html
摘要:接入分為兩部分,其一是可視化編輯器,在官網(wǎng)上我們可以獲取該編輯器的安裝包,并通過(guò)的插件管理進(jìn)行安裝。借助可視化編輯器,在整個(gè)過(guò)程中我們可以替換大部分手工編寫代碼的工作,進(jìn)行一站式操作。,有趣實(shí)用的分布式架構(gòu)頻道。本文根據(jù) SOFAChannel#5 直播分享整理,主題:給研發(fā)工程師的代碼質(zhì)量利器 —— 自動(dòng)化測(cè)試框架 SOFAActs?;仡櫼曨l以及 PPT 查看地址見文末。歡迎加入直播互動(dòng)釘...
摘要:看到很多團(tuán)隊(duì)和開源項(xiàng)目都在用代碼檢查工具,自己一直沒(méi)用過(guò),最近加入了新團(tuán)隊(duì)有項(xiàng)目在用,就想著研究一下。代碼校驗(yàn)工具能夠讓你在寫代碼時(shí)避免一些低級(jí)的錯(cuò)誤。同時(shí),也有友好的文檔針對(duì)每一條規(guī)則。在上文提高的所有工具當(dāng)中它對(duì)有著最好的支持。 看到很多團(tuán)隊(duì)和開源項(xiàng)目都在用代碼檢查工具,自己一直沒(méi)用過(guò),最近加入了新團(tuán)隊(duì)有項(xiàng)目在用,就想著研究一下??吹絪itepoint上的一篇2015年的文章覺(jué)得不...
摘要:前言本文主要是有關(guān)前端方面知識(shí)按照目前的認(rèn)知進(jìn)行的收集歸類概括和整理,涵蓋前端理論與前端實(shí)踐兩方面。 前言:本文主要是有關(guān)前端方面知識(shí)按照 XX 目前的認(rèn)知進(jìn)行的收集、歸類、概括和整理,涵蓋『前端理論』與『前端實(shí)踐』兩方面。本文會(huì)告訴你前端需要了解的知識(shí)大致有什么,看上去有很多,但具體你要學(xué)什么,還是要 follow your heart & follow your BOSS。 初衷...
摘要:前言本文主要是有關(guān)前端方面知識(shí)按照目前的認(rèn)知進(jìn)行的收集歸類概括和整理,涵蓋前端理論與前端實(shí)踐兩方面。 前言:本文主要是有關(guān)前端方面知識(shí)按照 XX 目前的認(rèn)知進(jìn)行的收集、歸類、概括和整理,涵蓋『前端理論』與『前端實(shí)踐』兩方面。本文會(huì)告訴你前端需要了解的知識(shí)大致有什么,看上去有很多,但具體你要學(xué)什么,還是要 follow your heart & follow your BOSS。 初衷...
摘要:前言本文主要是有關(guān)前端方面知識(shí)按照目前的認(rèn)知進(jìn)行的收集歸類概括和整理,涵蓋前端理論與前端實(shí)踐兩方面。 前言:本文主要是有關(guān)前端方面知識(shí)按照 XX 目前的認(rèn)知進(jìn)行的收集、歸類、概括和整理,涵蓋『前端理論』與『前端實(shí)踐』兩方面。本文會(huì)告訴你前端需要了解的知識(shí)大致有什么,看上去有很多,但具體你要學(xué)什么,還是要 follow your heart & follow your BOSS。 初衷...
閱讀 1918·2021-09-23 11:21
閱讀 1704·2019-08-29 17:27
閱讀 1062·2019-08-29 17:03
閱讀 729·2019-08-29 15:07
閱讀 1926·2019-08-29 11:13
閱讀 2384·2019-08-26 12:14
閱讀 930·2019-08-26 11:52
閱讀 1736·2019-08-23 17:09