自定義TSLint Rule之旅

項(xiàng)目中原來有好多_.get(a, `b.c.d`, [])樣式的代碼房待,看著很不爽践盼,因?yàn)轫?xiàng)目用的Typescript思瘟,這種語(yǔ)法直接把Typescript的類型約束破壞掉了。但又不能把代碼換成:

  (a && a.b && a.b.c && a.b.d) || []

所以一直無技可施囚似,當(dāng)然了赏酥,也不是全無辦法,但強(qiáng)推谆构,總會(huì)遇到動(dòng)力,所以想著有沒有更優(yōu)雅的方式解決這個(gè)問題框都。
同事前幾天說Typescript3.7開始支持Optional ChainingNullish Coalescing搬素,原來的代碼就可以改成:

a?.b?.c?.d ?? []

具體文檔請(qǐng)查詢Typescript官網(wǎng)。是不是優(yōu)雅了很多魏保,而且完美的保留了類型約束熬尺。但升級(jí)有兩個(gè)問題:

  1. 但項(xiàng)目上原來的_.get怎么辦?只能手工把所有代碼替換掉谓罗,全局搜索后發(fā)現(xiàn)總計(jì)300多處使用粱哼,但為了推一波,沒辦法檩咱。替換的過程中確實(shí)發(fā)現(xiàn)好多地方直接把類型約束去掉了揭措,類型中無這個(gè)字段胯舷,_.get可以跳過類型約束直接獲取值。
  2. 怎么防止再增加_.get? 團(tuán)隊(duì)每天都進(jìn)行Code Review绊含,但靠人為約束桑嘶,總歸不是個(gè)辦法。所以思考能不能在tslint里加一個(gè)規(guī)則躬充,禁掉一部分lodash的上帝函數(shù)逃顶?再結(jié)合husky,在git commit階段禁止提交。

注意:盡量不自己造輪子充甚,偶爾折騰一下也是可以的以政,本人不是自己造輪子的激進(jìn)愛好者

查資料

TSLint是如何工作的?

TSLint 用的是 TypeScript AST伴找,語(yǔ)法樹的每個(gè)節(jié)點(diǎn)對(duì)應(yīng)原文件的一小段文字盈蛮,并包含了一些額外信息。學(xué)習(xí)AST(Abstruct Tree)可以查詢相關(guān)書籍疆瑰,或者到A handbook for making programming languages學(xué)習(xí)如何利用AST實(shí)現(xiàn)一門編程語(yǔ)言眉反。可以在AST Explorer上查看相關(guān)語(yǔ)法生成的Tree穆役。
大概意思是寸五,TSLint會(huì)基于整個(gè)Tree去檢查每個(gè)節(jié)點(diǎn)上的語(yǔ)法是否符合規(guī)范。

官方文檔

以下內(nèi)容摘自官網(wǎng)耿币,略作刪減

重要事項(xiàng)

  • Rule 標(biāo)識(shí)遵循kebab-cased梳杏。例如:no-lodash-functions
  • Rule 文件名遵循camel-cased。 例如:camelCasedRule.ts
  • Rule 文件名必須以Rule名為后綴淹接。例如:noLodashFunctions.ts
  • Rule 文件必須導(dǎo)出一個(gè)名為Rule的類十性,并且該類繼承Lint.Rules.AbstractRule

示例代碼

import * as Lint from "tslint";
import * as ts from "typescript";

export class Rule extends Lint.Rules.AbstractRule {
    public static FAILURE_STRING = "import statement forbidden";
    
    public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
        return this.applyWithWalker(new NoImportsWalker(sourceFile, this.getOptions()));
    }
}

// The walker takes care of all the work.
class NoImportsWalker extends Lint.RuleWalker {
    public visitImportDeclaration(node: ts.ImportDeclaration) {
        // create a failure at the current position
        this.addFailure(this.createFailure(node.getStart(), node.getWidth(), Rule.FAILURE_STRING));

        // call the base version of this visitor to actually parse this node
        super.visitImportDeclaration(node);
    }
}

上面是一個(gè)禁止Import語(yǔ)法的代碼示例塑悼,Typescript中的解析器訪問每個(gè)AST節(jié)點(diǎn)的方式是Visitor模式劲适,所以自定義的Walker只需要重載對(duì)應(yīng)的visitor(例如visitImportDeclaration)方法,在里面實(shí)現(xiàn)自己的檢查語(yǔ)法厢蒜。

  • Lint.RuleWalker提供的基礎(chǔ)方法可以在syntaxWalker中查詢霞势。
  • 可以在AST Explorer調(diào)試代碼。

如何生效

其實(shí)有兩種方式斑鸦,但官方只給出了直接編譯導(dǎo)入的方式愕贡,后面我們會(huì)介紹另一種方式。

  1. 由于TSLint不支持直接ts類型的文件巷屿,所以我們需要先將代碼編譯為es2015版本:tsc noImportsRule.ts
  2. 在tslint的配置文件中導(dǎo)入生成的文件, 并配置相應(yīng)規(guī)則:
{
    "extends":[],
    "rules": {
        "no-import-rule": true
    },
    //省略其它配置固以,只要知道在什么層級(jí)配置就好
    //./tslint-rules/lib是tsc編譯后輸出的目錄
    "rulesDirectory": ["./tslint-rules/lib"]
} 

如何添加相應(yīng)的Fix代碼

// create a fixer for this failure
const fix = new Lint.Replacement(node.getStart(), node.getWidth(), "");

// create a failure at the current position
this.addFailure(this.createFailure(node.getStart(), node.getWidth(), Rule.FAILURE_STRING, fix));

在addFailure時(shí),添加對(duì)應(yīng)的fix規(guī)則

最后提示

  • 核心規(guī)則不能被自定義規(guī)則覆蓋(overwritten)
  • 自定義規(guī)則可以通過this.getOptions()獲取相關(guān)配置
  • 在TSLint 5.7.0版本之后可以不編譯.ts文件了嘱巾。但需要指明如何加載.ts文件憨琳。例如使用ts-node
ts-node node_modules/.bin/tslint <your options>
# 或者
node -r ts-node/register node_modules/.bin/tslint <your options>
# 或者
NODE_OPTIONS="-r ts-node/register" tslint <your options>

實(shí)戰(zhàn)

第一版 仿官方版

Rule文件 noLodashFuctions.ts

將Rule文件放置到tslint-rules/src目錄下诫钓。因?yàn)橹皇墙靡徊糠趾瘮?shù),所以規(guī)則很好寫栽渴,大概思路是在調(diào)用函數(shù)時(shí)尖坤,判斷是不是調(diào)用的_.get函數(shù)(文件中做了增強(qiáng),把functionNames做為了參數(shù))闲擦。具體的AST Node怎么找到的慢味,可以直接在AST Explorer上調(diào)試。

export class Rule extends Lint.Rules.AbstractRule {
  public static metadata: Lint.IRuleMetadata = {
    description: 'Disallows the functions in the lodash library.',
    options: {
      oneOf: [{ type: 'array', items: { type: 'string' } }, { type: 'string' }]
    },
    optionsDescription: 'the function name(s).',
    requiresTypeInfo: true,
    ruleName: 'no-lodash-functions',
    type: 'maintainability',
    typescriptOnly: false
  };

  public static GET_FAILURE_MESSAGE = (functionName: string) =>
    `The "${functionName}" in the lodash library is not allowed to be used in this project.`;

  public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
    return this.applyWithWalker(new NoLodashFunctionsRuleVisitor(sourceFile, this.getOptions()));
  }
}

// tslint:disable-next-line:max-classes-per-file
class NoLodashFunctionsRuleVisitor extends Lint.RuleWalker {
  private functionNames: Set<string>;
  constructor(sourceFile: ts.SourceFile, options: IOptions) {
    super(sourceFile, options);
    this.functionNames = new Set<string>(options.ruleArguments);
  }
  protected visitCallExpression(node: ts.CallExpression): void {
    if (node.expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
      const propertyAccessExpression = (node as ts.CallExpression).expression as ts.PropertyAccessExpression;
      if (
        this.functionNames.has(propertyAccessExpression.name.text) &&
        (propertyAccessExpression.expression as ts.Identifier).text === '_'
      ) {
        this.addFailureAtNode(propertyAccessExpression.name, Rule.GET_FAILURE_MESSAGE(propertyAccessExpression.name.text));
      }
    }
  }
}

編譯

在tslint-rules目錄下添加tsconfig.json, 主要是為了將代碼編譯為es2015的版本:

{
  // 省略n多配置
  "compilerOptions": {
    "target": "es2015",
    "outDir": "./lib"
  },
  "include": [
    "./src/"
  ]
}

在項(xiàng)目的package.json中添加編譯命令墅冷,主要是為了復(fù)用:

"lint-rules-compile": "tsc -p ./tslint-rules"

運(yùn)行編譯命令

yarn run lint-rules-compile

成功編譯后纯路,可以看到tslint-rules/lib中已經(jīng)存在編譯后的文件

導(dǎo)入并配置

tslint.json中添加如下配置:

{
  "rules": {
    "no-lodash-functions": [true, "get"]
  },
  "rulesDirectory": ["./tslint-rules/lib"]
}

效果

在webStorm中可以看到:


image

問題

  • 經(jīng)過多次嘗試,發(fā)現(xiàn)增加規(guī)則后寞忿,打開ts(x)等文件速度明顯變慢驰唬,webstorm反應(yīng)變慢。
  • 查詢?cè)创a發(fā)現(xiàn)Lint中的RuleWalker已經(jīng)標(biāo)記棄用腔彰。文檔:
/**
 * @deprecated
 * RuleWalker-based rules are slow,
 * so it's generally preferable to use applyWithFunction instead of applyWithWalker.
 * @see https://github.com/palantir/tslint/issues/2522
 */

第二版

解決第一版問題

官網(wǎng)中的Performance Tip中有重要提示Implement your own walking algorithm, 文檔如下:

Convenience comes with a price. When using SyntaxWalker or any subclass thereof like RuleWalker you pay the price for the big switch statement in visitNode which then calls the appropriate visitXXX method for every node in the AST, even if you don’t use them.

Use AbstractWalker instead and implement the walk method to fit the needs of your rule. It’s as simple as this:
大意是說叫编,visitXXXX類的方法會(huì)訪問每一個(gè)AST節(jié)點(diǎn),可以用AbstractWalker去定義適合自己的算法霹抛,防止遍歷所有節(jié)點(diǎn)搓逾。

認(rèn)真思考了下該問題,做為一個(gè)TSLint新手杯拐,可不可以借鑒TSLint核心規(guī)則中的一些寫法霞篡,來優(yōu)化當(dāng)前代碼呢?例如端逼,no-console-log和現(xiàn)在的算法就很相近朗兵,能不能抄一抄?畢竟抄代碼是程序員生存的第一技能顶滩。

注意:作者不是無腦抄選手余掖,也不建議大家無腦抄。我們要抄的有理有據(jù)礁鲁,并稱其為“借鑒”盐欺。

翻閱TSLint 核心Rule的源碼, 找到noConsoleRule.ts文件,動(dòng)手實(shí)現(xiàn)第二版代碼救氯。noConsoleRule的代碼請(qǐng)自行查閱。

實(shí)現(xiàn)(仿noConsoleRule)

import * as ts from 'typescript';
import * as Lint from 'tslint';
import { isPropertyAccessExpression, isIdentifier } from 'tsutils';

export class Rule extends Lint.Rules.AbstractRule {
  // 省略metadata和GET_FAILURE_MESSAGE

  apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
    return this.applyWithFunction<Set<string>>(sourceFile, walk, new Set<string>(this.getOptions().ruleArguments));
  }
}

function walk(ctx: Lint.WalkContext<Set<string>>) {
  return ts.forEachChild(ctx.sourceFile, function cb(node): void {
    if (
      isPropertyAccessExpression(node) &&
      isIdentifier((node as ts.PropertyAccessExpression).expression) &&
      ((node as ts.PropertyAccessExpression).expression as ts.Identifier).text === '_' &&
      ctx.options.has((node as ts.PropertyAccessExpression).name.text)
    ) {
      ctx.addFailureAtNode(
        (node as ts.PropertyAccessExpression).name,
        Rule.GET_FAILURE_MESSAGE((node as ts.PropertyAccessExpression).name.text)
      );
    }
    return ts.forEachChild(node, cb);
  });
}

延伸閱讀

另一種導(dǎo)入模式:發(fā)布到repository

其實(shí)可以把rule文件單獨(dú)放到一個(gè)工程里歌憨,然后編譯發(fā)布到私有或者公有的npm repository中, 在tslint.json中可以像如下方式導(dǎo)入:

{
  "extends": ["tslint:latest", "tslint-eslint-rules", "tslint-react", "tslint-config-prettier"]
}

開源項(xiàng)目 You-Dont-Need-Lodash-Underscore

在我實(shí)現(xiàn)自定義的Lint規(guī)則后着憨,我的同事(就前面那個(gè)同事,沒錯(cuò)务嫡,還是他甲抖,項(xiàng)目上的好兄弟-強(qiáng)哥)給我推了這個(gè)項(xiàng)目You-Dont-Need-Lodash-Underscore, 項(xiàng)目介紹如下:

Lodash and Underscore are great modern JavaScript utility libraries, and they are widely used by Front-end developers. However, when you are targeting modern browsers, you may find out that there are many methods which are already supported natively thanks to ECMAScript5 ES5 and ECMAScript2015 ES6. If you want your project to require fewer dependencies, and you know your target browser clearly, then you may not need Lodash/Underscore.

大意是漆改,Lodash和Underscore是很好的JavaScript庫(kù),但是其中的好多方法在ES5/ES6/Typescript中已經(jīng)實(shí)現(xiàn)准谚,本著更少依賴和可讀性的原則挫剑,我們可能不再需要Lodash/Underscore中的部分函數(shù)。
Readme中還列舉了一些大牛開發(fā)者的聲音柱衔,感興趣的可以上去看一下樊破。

思考:Typescript和Lodash

首先說明Typescript和Lodash并不沖突,而且Lodash也有type定義唆铐,lodash中確實(shí)提供了一些不錯(cuò)的方法哲戚,例如chain可以優(yōu)化一些流式計(jì)算速度,例如a.filter().map().filter()艾岂。
但為什么我們有時(shí)候會(huì)不提倡lodash顺少?
我們引入Typescript的一個(gè)很大原因就是類型約束,但loadsh中一些過于方便的上帝方法會(huì)將這層約束破壞掉王浴。在這個(gè)層面上我們認(rèn)為類型約束要比便利性更重要脆炎。抉擇本來就是一個(gè)Balance,如果在Team上對(duì)于一些技術(shù)的認(rèn)知能達(dá)到一定的共識(shí)氓辣,那么我們就要遵守秒裕。而且Typescript也在向著便利性邁進(jìn),比如支持Optional ChainingNullish Coalescing筛婉。
lodash中的很多函數(shù)簇爆,ts中本來就支持,我們沒必要引入過多的依賴爽撒,主要是代碼也不太優(yōu)雅入蛆,一屏幕的"_"。有些過于便利的函數(shù)硕勿,可能本身就是個(gè)黑盒哨毁,由于我們對(duì)其的不了解,在一些特殊情況下源武,會(huì)產(chǎn)生一些莫名其妙的Bug扼褪,例如_.isEmpty(2)
在某些情況下引入lodash可能是為了更語(yǔ)義化粱栖,但有時(shí)候真的更語(yǔ)義化嗎话浇?還是說引入了一個(gè)黑盒?我們不得不承認(rèn)在引入“語(yǔ)義化”的同時(shí)闹究,增加了“認(rèn)知成本”幔崖。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子赏寇,更是在濱河造成了極大的恐慌吉嫩,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗅定,死亡現(xiàn)場(chǎng)離奇詭異自娩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)渠退,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門忙迁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人智什,你說我怎么就攤上這事动漾。” “怎么了荠锭?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵旱眯,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我证九,道長(zhǎng)删豺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任愧怜,我火速辦了婚禮呀页,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拥坛。我一直安慰自己蓬蝶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布猜惋。 她就那樣靜靜地躺著丸氛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪著摔。 梳的紋絲不亂的頭發(fā)上缓窜,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音谍咆,去河邊找鬼禾锤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛摹察,可吹牛的內(nèi)容都是我干的恩掷。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼供嚎,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼黄娘!你這毒婦竟也來了旦签?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤寸宏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后偿曙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氮凝,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年望忆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了罩阵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡启摄,死狀恐怖稿壁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情歉备,我是刑警寧澤傅是,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站蕾羊,受9級(jí)特大地震影響喧笔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜龟再,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一书闸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧利凑,春花似錦浆劲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至日丹,卻和暖如春走哺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哲虾。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工丙躏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人束凑。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓晒旅,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親汪诉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子废恋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容