Migrate Codelyzer with angular-eslint

Yosuke Kurami
8 min readDec 7, 2020
Photo by Antonio Batinic from Pexels.com

これは Angular Advent Calendar 2020 8 日目の記事です。

Angular と Linter

Angular: ESLint サポートの現状 2020 Autumn にあるように、Angular CLI v12 から TSLint が deprecated 扱いになる。

TSLint とともに使われてきた Codelyzer で提供されてきた Rule については、今後は angular-eslint が提供していくことになる。実際、多くの Codelyzer ルールが angular-eslint に移植済みだ。

Codelyzerルールの移管状況(一部抜粋)

一方でこの移行済みチェックリストを眺めていると、template- prefix から始まる、すなわち HTML テンプレートを相手にしたルールは空白が多いことに気づく。

「移行の上で何かしらのハードルが存在するのだろうか?」と思い、適当に一個 Codelyzer のルールを ESLint 化してみることにする。

対象に選んだのは template-use-track-by-function というルール。「*ngFor を使うときに、trackBy 関数の指定を強制する」というやつ。

ESLint の rule 開発

ESLint のルールを作る上で必須となるのが、「対象となるコードがどのような AST で展開されるか」を理解することだ。

angular-eslint の場合、2 種類の parser に頼っている。

  • @typescript-eslint/parser
  • @angular-eslint/template-parser

前者は TypeScripter にとっては最早お馴染みとなっていて、TypeScript 本体の parser が返す ts.SourceFile の AST を ESTree 形式に変換して ESLint から利用できるようするためのもの(正確に言うと、ESTree への変換は、より下位の @typescript-eslint/estree が担当している)。

Angular の HTML テンプレートを ESTree に変換するのが、後者の @angular-eslint/template-parser だ。 @angualr/compilerparseTemplate が HTML テンプレート文字列の AST を返してくれるので、これを ESTree 形式に変換している。

まずは、この @angular-eslint/template-parser が返す ESTree を可視化する手立てを作らねばならない。ということで、AST Explorer に parser を追加する PR を用意した。

実際に実行すると以下のようになる(https://quramy.github.io/astexplorer/ でも試せるようにしてある)

AST Explorerでangular-eslint/template-parserのASTを見る

ちなみに、 @angular/compiler の結果を直接参照できる機能は既に AST Explorer 上に実装されている。現状、 @angular-eslint/template-parser による AST 変換処理はとても薄いものなので、実はどちらの parser を使っても構造的な差異はあまりないのだが、ESLint のルール開発という文脈では @angular-eslint/template-parser の結果を信用するべきだろう。

HTML AST と esquery

さて、今回 Codelyzer から移植しようと思った template-use-track-by-function というルールは、ngFor を対象にしているわけだけど、これは ng-template に対して、ngFor のディレクティブを当てるか、*ngFor としてテンプレート構文を利用するかで二通りの書き方がある。

<ng-template ngFor let-item [ngForOf]="items" [ngForTrackBy]="trackByFn">
<li>{{item.name}}</li>
</ng-template>
<li *ngFor="let item of items; trackBy: trackByFn">{{item.name}}</li>

触ってみてわかったけど、 *ngFor で書いておいても、<ng-template> で書いた場合と同じく、「ngForOf は biding attribute で、その expression はitems である」という意味の AST Node が得られた。テンプレート構文(*のことね)の解釈まで @angular/compiler が済ませていることになる。AST のレベルで解釈が済まされている、ってことは * の部分、Angular にとってはマクロのように考えているのかもね。

さておき、これで ESLint のルールから参照すべき AST の構造が大体見えてきた。

ngForOf というバインディングで node を引っ掛けるには、 BoundAttribute[name="ngForOf"] という esquery を書けばよい。

https://github.com/estools/esquery は ESLint でも利用しているライブラリの 1 つで、AST に対して CSS セレクタのようにクエリが書ける、という代物。「ある程度までクエリで対象の AST node を絞り込んでから、細かい処理を node に対する条件処理として書いていく」というのが ESLint のルールを書く基本。

今回僕が実装したルールでは、ng-template の場合と *ngFor の場合でエラーの報告の仕方が若干異なる仕様だったため、

  • BoundAttribute.templateAtts[name="ngForOf"]
  • BoundAttribute.inputs[name="ngForOf"]

で処理を分けるように書くことにした。

仕上げ

ban 対象とする AST の構造がわかってしまえば、あとはこちらのものだ。

Codelyzer 本家の GitHub に移植元のルールに対するテスコードが用意されているので、それを angular-eslint 側で再現していく。もともと Codelyzer からの移植を強く意識していたからか、同じようにテストコードが書けるように細かいヘルパーが用意されており、作業しやすい。

(左: angular-eslint でのテストコード | 右: Codelyzer でのテストコード)

最終的に完成した PR はこちら。

おわりに

今回のエントリは、「移行の上で何かしらのハードルが存在するのだろうか?」に端を発して、自分で Codelyzer の template 系ルールを angular-eslint に移植するのをやってみたわけだが、大枠の所感としては下記。

  • そもそも template HTML の AST がわからない -> AST Explorer で見れるようにすれば問題ない
  • ルールの実装しやすさ -> esquery が利用できるなど、むしろ angular-eslint 上で実装する方が、ツールが揃っていてやりやすい

まだ幾つか未実装の Codelyzer ルールがあるので、PR チャンスと思ってトライしてみたらどうだろう?

以前、ESLint の plugin 開発チュートリアルというものを書いたことがあるので、こちらも参考にしてほしい。AST Explorer や esquery といった基本的な登場人物について、概念や使い方を説明してあり、AST 解析の入門に是非。

--

--

Yosuke Kurami

Front-end web developer. TypeScript, Angular and Vim, weapon of choice.