LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

TypeScriptの不要なexportを自動で削除するOSS「ts-remove-unused」を開発しました

こんにちは。メディアカンパニーに所属する鴻巣和司 (@kazushikonosu) です。普段はLINEスキマニのフロントエンドの開発をしています。業務をきっかけに、TypeScriptの不要なデッドコードを自動で削除するツール ts-remove-unused を開発し、GitHub上でOSSとして公開しました。本稿では、ts-remove-unusedが解決しようとしている問題や直近で行った大規模な変更について紹介します。

exportしていることで気づきにくい不要なコード

TypeScriptを使っている場合、tsconfig.jsoncompilerOptions.noUnusedLocals を有効にすることで、ファイル内で参照がない不要な変数などの定義をTypeScriptコンパイラの実行時に検知することが可能です。また、ESLintなどの静的解析ツールを使い適切にルールを設定することによって、このような不要なコードを検知することも可能です。

不要な定義にはエラーが表示される

一方で、変数をexportしている場合はどうでしょうか。この変数がコードベース内のすべてのファイルから参照されていない場合でも、TypeScriptはエラーを出力しません。またESLintなどのリンターは、ファイル単位で処理を行っており、内部でモジュール間の依存関係のグラフを保持していないことが一般的なため、ルールを設定してこのようなコードを検知することが難しくなっています。

exportされていると不要な定義にもエラーが表示されない

LINEスキマニの開発では、大規模な改修に取り組んだ際にこの問題が顕在化しました。モジュールごとに漸進的に置き換える作業を行ったのですが、適切に不要になったコードを削除してコードベースの状態を維持することが作業の負担になっていました。例えばa→b→cという依存関係がある場合、aが使われてないことを発見できないとb, cはいつまでもコードベースに残り続けます。使われていないコードが残り続けると、必要のないコードのメンテナンスを気づかずに行ってしまうという問題も生じていました。

一般的に、デッドコードはコードベースのメンテナンス性や可読性に悪影響を及ぼすので、適切に削除することが望ましいです。しかし、手動で削除するには単調で負担の大きい作業が必要になります。

ts-remove-unusedの開発

そこで、ts-remove-unusedを開発しました。TypeScriptコンパイラが持つ依存関係のグラフをもとに export キーワードを削除する機能と、 export を削除したことによってファイル内での参照箇所がなくなった定義や import 文の削除を行う機能を提供しています。手動で行うには負担が大きい作業を自動化して、開発者はツールが生成した変更をプルリクエスト内でレビューする作業フローを想定しています。

当初は社内のみでの利用を想定して、プロダクトのリポジトリで頻出するコードのパターンにのみ対応したツールとして開発をスタートしました。この段階ではすべてのパターンを網羅するよりも、頻出するパターンを中心に対応することで、80%の作業を自動化できるツールをすばやく開発するメリットが大きいと判断したためです。

初期実装ではts-morphというTypeScript APIによるAST操作を簡単にするライブラリを使ったほか、上書き内容を決定するためにTypeScriptのTransformerをファイル全体に対して使用する処理を書きました。これによってスピーディーな実装を実現できました。しかし、TypeScriptのTransformerはESLintなどの静的解析ツールと異なりバンドルの生成を念頭におかれたものなので、結果を出力する際にもとのファイルのコードフォーマットを維持する機能を提供していません。そのため、ツールを実行すると可読性のために記述された空行がファイルから失われる荒削りなものになっていました。

その後、OSSとして公開され、ツールとしてのクオリティが求められることになったため、直近でリリースした ts-remove-unused の v0.3.0 ではこれらの問題を解決するために一から書き直しました。ツールが対応しているコードのパターンの網羅性を高めたほか、ts-morphに依存した実装からTypeScript内部のLanguageServiceクラスをベースとした独自実装に切り替えました。Transformerによりファイルの変更を行うアプローチから、ファイル内で削除が必要なASTノードの位置をもとにコードを編集する処理をツール内部で実装して使用することで、空行などのコードフォーマットが失われないようにしました。

次の節では、サンプルコードに対してts-remove-unusedを実際に使いながら機能や特徴を取り上げます。

ts-remove-unused の機能と特徴

ts-remove-unusedは「Zero Config」なCLIツールです。ただし「プロジェクト内で」exportされた定義が参照されているかを判断するため、場合によってはプロジェクトのファイルが適切に網羅されるように tsconfig.jsoninclude などの設定を正しい状態に見直す必要があります。

src/main.ts, src/a.ts, src/b.ts という3つのファイルが存在するプロジェクトについて考えます。ファイルの内容は次の通りです。

// src/main.ts
import { a } from './a.js';

console.log(a);
// src/a.ts

export const a = 'a';

export const a2 = 'a2';
// src/b.ts

export const b = 'b';

src/main.ts がプロジェクトのentry pointです。 src/a.ts の変数 asrc/main.ts から参照されていますが、変数 a2 に関してはプロジェクト内で参照されておらず、デッドコードになっています。 src/b.tsb という変数をexportしていますが、ファイル内のすべてのexportがプロジェクト内で参照されておらず、ファイル自体が不要になっています。

このプロジェクトに対して、以下のコマンドを実行します。 --skip オプションはts-remove-unusedの対象から除外したいファイルの正規表現を指定するパターンです。ここではプロジェクトのentry pointとなる src/main.ts がマッチするように指定しました。--check オプションを指定することによって、ts-remove-unusedをリンターとして使用できます。

npx ts-remove-unused --skip src/main.ts --check

--checkを指定した場合の出力

出力から、使われていないexport (src/a.tsa2) と、モジュール全体として不要なファイル (src/b.ts) が確認できます。--check オプションを指定することで、ファイルを上書きせずにプロジェクト内の不要なexportを確認することができます。

次に以下のコマンドを実行しました。

npx ts-remove-unused --skip src/main.ts

--checkを指定しない場合の出力

--check オプションなしで実行した場合には、ファイルを上書きして修正を加えます。src/a.ts は以下のように修正され、src/b.ts は削除されます。

// src/a.ts

export const a = 'a'; 

ts-remove-unusedを使用することで、単調になる削除の編集はツールにまかせて、開発者はツールが出力した結果をレビューすることに専念できます。ここでは export const を含むコードを例に実行しましたが、このほかにも function, class, type, interface などの定義のexportや、export default, export {} といった構文にも対応しています。また、例を複雑にしないためにサンプルコードには含めませんでしたが、ファイル内で使われている変数については export キーワードだけが取り除かれ、定義自体は削除されません。

おわりに

ts-remove-unusedの開発と導入によって、不要になったコードの削除を自動化することができたので、本質的なロジックの変更に専念してより積極的に大規模なリファクタリングに取り組めるようになりました。またts-remove-unusedが行った変更をリファクタリングとは別のプルリクエストに分割することで、レビュワーの視点からも重点的に確認が必要な差分が明確になり負担が軽減されました。

本稿がts-remove-unusedに興味をもつきっかけになり、既存のコードベースの品質改善や大規模なリファクタリングの過程での負担の軽減につながると幸いです。

LINEヤフー株式会社ではプロダクト開発の現場でも、技術的な取り組みを積極的に行っています。フロントエンド領域の技術的な取り組みについては、Podcast 「UIT INSIDE」で定期的に発信しています。こちらもあわせてチェックしていただけると嬉しいです。