背景
建立的程式碼規範沒人遵守,專案中遍地風格迥異的程式碼,你會不會抓狂?
通過測試用例的程式還會出現Bug,而原因僅僅是自己犯下的低階錯誤,你會不會抓狂?
某種程式碼寫法存在問題導致崩潰時,只能全工程檢查程式碼,這需要人工花費大量時間Review程式碼,你會不會抓狂?
以上這些問題,可以通過靜態檢查有效地緩解!
靜態檢查(Static Program Analysis)主要是以不執行程式的方式對於程式原始碼進行檢查分析的技術,而與之相反的就是動態檢查(Dynamic Program Analysis),通過實際執行程式輸入測試資料產生預期結果的技術。通過程式碼靜態檢查,我們可以快速定位程式碼的錯誤與缺陷,可以減少逐行閱讀程式碼浪費的時間,可以(根據需要)快速掃描程式碼中可能存在的漏洞等。程式碼靜態檢查可以在程式碼的規範性、安全性、可靠性、可維護性等方面起到重要作用。
在客戶端中,Android可以使用CheckStyle、Lint、Findbugs、PMD等工具,iOS可以使用Clang Static Analyzer、OCLint等工具。而在React Native的開發過程中,針對於JavaScript的ESLint,與TypeScript的TSLint,則成為了主要程式碼靜態檢查的工具。本文將按照使用TSLint的原因、使用TSLint的方法、自定義TSLint的步驟進行探究分析。
一、使用TSLint的原因
在客戶端團隊進入React Native專案的開發過程中,面臨著如下問題:
- 由於大家從客戶端轉入到React Native開發過程中,容易出現低階語法錯誤;
- 開發者之前從事Android、iOS、前端等工作,因此程式碼風格不同,導致專案程式碼風格不統一;
- 客戶端效果不一致,有可能Android端顯示正常、iOS端顯示異常,或者相反的情況出現。
雖然以上問題可以通過多次不斷將雷點標記出,並不斷地分享經驗與強化程式碼Review過程等方式來進行緩解,但是仍面臨著React Native開發者掌握的技術水平千差萬別,知識分享傳播的速度緩慢等問題,既導致了開發成本的不斷增加和開發效率持續低下的問題,還難以避免一個坑被踩了多次的情況出現。這時急需一款可以滿足以下目標的工具:
- 可檢測程式碼低階語法錯誤;
- 規範專案程式碼風格;
- 根據需要可自定義檢查程式碼的邏輯;
- 工具使用者可以“傻瓜式”的接入部署到開發IDE環境;
- 可以快速高效地將檢查工具最新檢查邏輯同步到開發IDE環境中;
- 對於檢查出的問題可以快速定位。
根據上述要求的描述,靜態檢查工具TSLint可以較為有效地達成目標。
二、TSLint介紹
TSLint是矽谷企業Palantir的一個專案,它是一款可以檢查TypeScript程式碼可讀性、可維護性以及功能性錯誤的靜態檢查工具,當前許多編輯器(Editors)和構建系統(Build Systems)支援這一工具,同時支援自定義編寫Lint規則、配置、格式化等。
當前TSLint已經包含了上百條規則,這些規則構築了當前TSLint檢查的基礎。在程式碼開發階段中,通過這些配置好的規則可以給工程一個完整的檢查,並隨時可以提示出可能存在的問題。本文內容參考了TSLint官方文件palantir.github.io/tslint/。
2.1 TSLint常見規則
以下規則主要來源於TSLint規則,是某些規則的簡單介紹。
2.2 常用TSLint規則包
上述2.1所列出的規則來源於Palantir官方TSLint規則。實際還有多種,可能會用到的有以下:
我們在專案的規則配置過程中,一般採用上述規則包其中一種或者若干種同時配置,那如何配置呢?請看下文。
三、如何進行TSLint規則配置與檢查
首先,在工程package.json檔案中配置TSLint包:
在根目錄中的tslint.json檔案中可以根據需要配置已有規則,例如:
其中extends陣列內放置繼承的TSLint規則包,上圖包括了airbnb配置的規則包、tslint-react的規則包,而rules用於配置規則的開關。
TSLint規則目前只有true和false的選項,這導致了結果要麼正常,要麼報錯ERROR,而不會出現WARNING等警告。
有些時候,雖然配置某些規則開啟,但是某個檔案內可能會關閉某些甚至全部規則檢查,這時候可以通過規則註釋來配置,如:
/* tslint:disable */
複製程式碼
上述註釋表示本檔案自此註釋所在行開始,以下的所有區域關閉TSLint規則檢查。
/* tslint:enable */
複製程式碼
上述註釋表示本檔案自此註釋所在行開始,以下的所有區域開啟TSLint規則檢查。
/* tslint:disable:rule1 rule2 rule3... */
複製程式碼
上述註釋表示本檔案自此註釋所在行開始,以下的所有區域關閉規則rule1 rule2 rule3...的檢查。
/* tslint:enable:rule1 rule2 rule3... */
複製程式碼
上述註釋表示本檔案自此註釋所在行開始,以下的所有區域開啟規則rule1 rule2 rule3...的檢查。
// tslint:disable-next-line
複製程式碼
上述註釋表示此註釋所在行的下一行關閉TSLint規則檢查。
someCode(); // tslint:disable-line
複製程式碼
上述註釋表示此註釋所在行關閉TSLint規則檢查。
// tslint:disable-next-line:rule1 rule2 rule3...
複製程式碼
上述註釋表示此註釋所在行的下一行關閉規則rule1 rule2 rule3...的檢查檢查。
以上配置資訊,這裡具體參考了palantir.github.io/tslint/usag…。
3.1 本地檢查
在完成工程配置後,需要下載所需要依賴包,要在工程所在根目錄使用npm install
命令完成下載依賴包。
IDE環境提示
在完成下載依賴包後,IDE環境可以根據對應配置檔案進行提示,可以實時地提示出存在問題程式碼的錯誤資訊,以VSCode為例:
本地命令檢查
VSCode目前還有繼續完善的空間,如果部分檔案未在視窗開啟的情況下,可能存在其中錯誤未提示出的情況,這時候,我們可以通過本地命令進行全工程的檢查,在React Native工程的根目錄下,通過以下命令列執行:
tslint --project tsconfig.json --config tslint.json
複製程式碼
(此命令如果不正確執行,可在之前加入./node_modules/.bin/)即為:
./node_modules/.bin/tslint --project tsconfig.json --config tslint.json
複製程式碼
從而會提示出類似以下錯誤的資訊:
src/Components/test.ts[1, 7]: Class name must be in pascal case
複製程式碼
3.2 線上CI檢查
本地進行程式碼檢查的過程也會存在被人遺忘的可能性,通過技術的保障,可以避免人為遺忘,作為程式碼提交的標準流程,通過CI檢查後再合併程式碼,可以有效避免程式碼錯誤的問題。CI系統可以為理解為一個雲端的環境,環境配置與本地一致,在這種情況下,可以生成與本地一致的報告,在美團內部可以使用基於Jenkins的Castle CI系統, 生成結果與本地結果一致:
3.3 其他方式
程式碼檢查不止侷限上述階段,在程式碼commit、pull request、打包等階段均可觸發。
- 程式碼commit階段,通過Hook方式可以觸發程式碼檢查,可以有效地將線上CI檢查階段強制提前,基本保證了線上CI檢查的完全正確性。
- 程式碼pull request階段,通過線上CI檢查可以觸發程式碼檢查,可以有效保證合入分支尤其是主分支的正確性。
- 程式碼打包階段,通過線上CI檢查可以觸發程式碼檢查,可以有效保證打包程式碼的正確性。
四、自定義編寫TSLint規則
4.1 為什麼要自定義TSLint規則
當前的TSLint規則雖然涵蓋了比較普遍問題的一些程式碼檢查,但是實踐中還是存在一些問題的:
- 團隊中的個性化需求難以滿足。例如,saga中的非同步函式需要在最外層加try-catch,且catch塊中需要加異常上報,這個明顯在官方的TSLint規則無法實現,為此需要自定義的開發。
- 官方規則的開啟與配置不符合當前團隊情況。
基於以上原因其他團隊也有自定義TSLint的先例,例如上文提到的tslint-microsoft-contrib、tslint-eslint-rules等。
4.2 自定義規則步驟
那自定義TSLint大概需要什麼步驟呢,首先規則檔案根據規範進行按部就班的編寫規則資訊,然後根據程式碼檢查邏輯對語法樹進行分析並編寫邏輯程式碼,這也是自定義規則的核心部分了,最後就是自定義規則的使用了。
自定義規則的示例直接參考官方的規則是最直接的,我們能這裡參考一個比較簡單的規則"class-name"。
"class-name"規則上文已經提到,它的意思是對類命名進行規範,當團隊中類相關的命名不規範,會導致專案程式碼風格不統一甚至其他出現的問題,而"class-name"規則可以有效解決這個問題。我們可以看下具體的原始碼檔案:github.com/palantir/ts…。
然後將分步對此自定義規則進行講解。
第一步,檔案命名
規則命名必須是符合以下2個規則:
- 駝峰命名。
- 以'Rule'為字尾。
第二步,類命名
規則的類名是Rule
,並且要繼承Lint.Rules.AbstractRule
這個型別,當然也可能有繼承TypedRule
這個類的時候,但是我們通過閱讀原始碼發現,其實它也是繼承自Lint.Rules.AbstractRule
這個類。
第三步,填寫metadata資訊
metadata包含了配置引數,定義了規則的資訊以及配置規則的定義。
- ruleName 是規則名,使用烤串命名法,一般是將類名轉為烤串命名格式。
- description 一個簡短的規則說明。
- descriptionDetails 詳細的規則說明。
- rationale 理論基礎。
- options 配置引數形式,如果沒有可以配置為null。
- optionExamples 引數範例 ,如沒有引數無需配置。
- typescriptOnly true/false 是否只適用於TypeScript。
- hasFix true/false 是否帶有修復方式。
- requiresTypeInfo 是否需要型別資訊。
- optionsDescrition options的介紹。
- type 規則的型別。
規則型別有四種,分別為:"functionality"、"maintainability"、"style"、"typescript"。
- functionality : 針對於語句問題以及功能問題。
- maintainability:主要以程式碼簡潔、可讀、可維護為目標的規則。
- style:以維護程式碼風格基本統一的規則。
- typescript:針對於TypeScript進行提示。
第四步,定義錯誤提示資訊
這個主要是在檢查出問題的時候進行提示的文字,並不侷限於使用一個靜態變數的形式,但是大部分官方規則都是這麼編寫,這裡對此進行介紹,防止引起歧義。
第五步,實現apply方法
apply
主要是進行靜態檢查的核心方法,通過返回applyWithFunction
方法或者返回applyWithWalker
來進行程式碼檢查,其實applyWithFunction
方法與applyWithWalker
方法的主要區別在於applyWithWalker
可以通過IWalker
實現一個自定義的IWaker
類,區別如下:
其中實現IWaker
的抽象類AbstractWalker
裡面也繼承了WalkContext
,
而這個WalkContext
就是上面提到的applyWithFunction
的內部實現類。
第六步,語法樹解析
無論是applyWithFunction
方法還是applyWithWalker
方法中的IWaker
實現都傳入了sourceFile
這個引數,這個相當於檔案的根節點,然後通過ts.forEachChild
方法遍歷整個語法樹節點。
這裡有兩個檢視AST語法樹的工具:
- AST Explorer:
astexplorer.net/
對應原始碼: github.com/fkling/aste… - TypeScript AST Viewer:
ts-ast-viewer.com/
對應原始碼: github.com/dsherret/ts…
AST Explorer
優點:
在AST Explorer可以高亮顯示所選中程式碼對應的AST語法樹資訊。
缺點:
- 不能選擇對應版本的解析器,導致顯示的語法樹程式碼版本固定。
- 語法樹顯示的資訊相對較少。
TypeScript AST Viewer
優點:
- 解析器對應版本可以動態選擇:
- 語法樹顯示的資訊不僅顯示對應的數字程式碼,還可為對應的實際資訊:
每個版本對應對kind資訊數值可能會變動,但是對應的列舉名字是固定的,如下圖:
從而這個工具可以避免頻繁根據其數值查詢對應資訊。
缺點: 不能高亮顯示程式碼對應的AST語法樹區域,定位效率較低。
綜上,通過同時使用上述兩個工具定位分析,可以有效地提高分析效率。
第七步,檢查規則程式碼編寫
通過ts.forEachChild
方法對於語法樹所有的節點進行遍歷,在遍歷的方法裡可以實現自己的邏輯,其中節點的類為ts.Node
:
其中kind為當前節點的型別,當然Node
是所有節點的基類,它的實現還包括Statement
、Expression
、Declaration
等,回到開頭這個"class-name"規則,我們的所有宣告類主要是class與interface關鍵字,分別對應ClassExpression
、ClassDeclaration
、InterfaceDeclaration
,
我們可以通過上步提到的AST語法樹工具,在語法樹中看到其為一一對應的。
在規則程式碼中主要通過isClassLikeDeclaration
、isInterfaceDeclaration
這兩個方法進行判斷的。
其中isClassLikeDeclaration
、isInterfaceDeclaration
對應的方法我們可以在node.js檔案中找到:
判斷是對應的型別時,呼叫addFailureAtNode
方法把錯誤資訊和節點傳入,當然還可以呼叫addFailureAt
、addFailure
方法。
最終這個規則編寫結束了,有一點再次強調下,因為每個版本所對應的型別程式碼可能不相同,當判斷kind的時候,一定不要直接使用各個型別對應的數字。
第八步,規則配置使用
完成規則程式碼後,是ts字尾的檔案,而ts規則檔案實際還是要用js檔案,這時候我們需要用命令將ts轉化為js檔案:
tsc ./src/*.ts --outDir dist
複製程式碼
將ts規則生成到dist資料夾(這個資料夾命名使用者自定),然後在tslint.json檔案中配置生成的規則檔案即可。
之後在專案的根目錄裡面,使用以下命令既可進行檢查:
tslint --project tsconfig.json --config tslint.json
複製程式碼
同時為了未來新增規則以及規則配置的更好的操作性,建議可以封裝到自己的規則包,以便與規則的管理與傳播。
總結
TSLint的優點:
- 速度快。相對於動態程式碼檢查,檢查速度較快,現有專案無論是在本地檢查,還是在CI檢查,對於由十餘個頁面組成的React Native工程,可以在1到2分鐘內完成;
- 靈活。通過配置規則,可以有效地避免常見程式碼錯誤與潛在的Bug;
- 易擴充套件。通過編寫配置自定義規則,可以及時準確快速查詢出程式碼中特定風險點。
TSLint缺點:
- 規則的結果只有對與錯兩種等級結果,沒有警告等級的的提示結果;
- 無法直接報告規則報錯數量,只能依賴其他手段統計;
- TSLint規則針對於當前單一檔案可以有效地通過語法樹進行分析判定,但對於引用到的其他檔案中的變數、類、方法等,則難以通過AST語法樹進行判定。
使用結果及分析
在美團,有十餘個頁面的單個工程首次接入TSLint後,檢查出的問題有近百條。但是由於開啟的規則不同,配置規則包的差異,檢查後的數量可能為幾十條到幾千條甚至更多。現在已開發十餘條自定義規則,在單個工程內,處理優化了數百處可能存在問題的程式碼。最終TSLint接入了相關React Native開發團隊,成為了程式碼提交階段的必要步驟。
通過團隊內部的驗證,文章開頭遇到的問題得到了有效地緩解,目標基本達到預期。TSLint在React Native開發過程中既保證了程式碼風格的統一,又保證了React Native開發人員的開發質量,避免了許多低階錯誤,有效地節省了問題排查和人員溝通的成本。
同時利用自定義規則,能夠將一些相容性問題在內的個性化問題進行總結與預防,提高了開發效率,不用花費大量時間查詢問題程式碼,又避免了在一個問題上跌倒多次的情況出現。對於不同經驗的開發者而言,不僅可以進行友好的提示,也可以幫助快速地定位問題,將一個人遇到的經驗教訓,用極低的成本擴散到其他團隊之中,將開發狀態從“亡羊補牢”進化到“防患未然”。
作者簡介
家正,美團點評Android高階工程師。2017 年加入美團點評,負責美團大交通的業務開發。