Stanby Tech Blog

求人検索エンジン「スタンバイ」を運営するスタンバイの開発組織やエンジニアリングについて発信するブログです。

TypeScript の AST と JSDoc を使ってコードを安全に削除する

スタンバイアドベントカレンダー 2024 の 3 日目です! (スタンバイでは、毎年アドベントカレンダーを実施しており、そちらにもこの記事をリンクさせています。スタンバイアドベントカレンダーに興味を持っていただいた方は、そちらもご覧いただけると嬉しいです。)

株式会社スタンバイでフロントエンドエンジニアをしている川野です。 フロントエンドエンジニアという役割を担っていますが、最近では開発者体験や開発生産性というところに興味があり、そのあたりの改善にもよく取り組んでいます。

はじめに

フロントエンドの機能を追加するとき、該当機能の影響を分析した上で 100% 公開に進むため、私たちはよく A/B テストを実施します。 いくつかのパターンを用意し、それぞれのパターンに対して異なる実装をします。 そして A/B テストの結果、その企画が棄却されることになると、そのコードは削除されます。

不要となったパターンのコードを削除する際に、TypeScript の AST と JSDoc を使うことで、コードの中から削除する箇所を機械的に検出し、安全に削除するプログラムを作ることができたので紹介します。

ただし、今回紹介する方法では、条件分岐を含むような複雑なケースに対応できていないため、すべてのケースには対応できません。 可能であれば、改善していきたい今後の課題です。

困っていたこと

A/B テストのコードは頻繁に追加され、削除されます。 そのため、削除対象の箇所がすぐにわかるように、コメントアウトを使って目印を残していました。

たとえば、このような感じです。

const messages = {
  // ↓↓↓ AB テスト (KEY-042)
  no001: (value: string) => {
    return `No 001: ${value}`;
  },

  no003: (value: string) => {
    return `No 003: ${value}`;
  },
  // ↑↑↑ AB テスト (KEY-042)

  no002: (value: string) => {
    return `No 002: ${value}`;
  },
};

この方法にはいくつかの問題がありました。

  • ある A/B テストの範囲に、誤って別の A/B テストのコードが含まれてしまうことがある。
    • 必要なコードまで消してしまうリスクがある。
  • 順序通りにコードを書きたくても、範囲の記述により、順序通りに書けないことがある。
    • たとえば、オブジェクトのキーが上記のサンプルのように連番ときに、それを順序通りに記述的ないケースがある。
  • 対応するコメントアウトを記述し忘れることがある。
    • たとえば、開いているが閉じていないコメントアウトがある。
  • 人の目で確認して削除するため、削除し忘れることがある。

最初は、これらの問題を解決するための目印の残し方を考えていました。 そこで思いついたのが、JSDoc を使うことでした。 そして、JSDoc を使うのであれば、構文解析することで削除対象のコードを検出し、そのまま削除できるのではないかと考えました。

TypeScript の AST と JSDoc を使って課題を解決する

AST (Abstract Syntax Tree: 抽象構文木) は、ソースコードをツリー構造で表現したものです。 AST を使うことで、削除対象のコードブロックを検出できます。

先ほど例に挙げたコードを、次のように修正します。 ここでは、JSDoc のタグを abtest にしています。

const messages = {
  /** @abtest KEY-042 */
  no001: (value: string) => {
    return `No 001: ${value}`;
  },

  no002: (value: string) => {
    return `No 002: ${value}`;
  },

  /** @abtest KEY-042 */
  no003: (value: string) => {
    return `No 003: ${value}`;
  },
};

この中から削除対象となるコードブロックを検出し、削除するプログラムは次のようになります。

import path from "path";

import { Node, Project } from "ts-morph";
import ts from "typescript";

const project = new Project({
  tsConfigFilePath: path.join(import.meta.dirname, "/path/to/tsconfig.json"),
});

const sourceFiles = project.getSourceFiles();

sourceFiles.forEach((sourceFile) => {
  sourceFile.forEachDescendant((node) => {
    // `remove` メソッドを持っていない `node` もあるため、型を絞り込む必要があります。
    // 必要に応じて、絞り込む条件を追加します。
    if (!Node.isPropertyAssignment(node)) return;

    // ts-morph で任意の `node` に対して JSDoc を取得する方法を見つけられなかったため、TypeScript の機能も併用しています。
    // ts-morph で取得した `node` は `node.compilerNode` とすることで、 ts の関数に渡すことができます。
    const jsDocs = ts.getJSDocTags(node.compilerNode);

    jsDocs.forEach((jsDoc) => {
      if (jsDoc.tagName.text === "abtest" && jsDoc.comment === "KEY-042") {
        node.remove();
      }
    });
  });

  sourceFile.saveSync();
});

ts.getJSDocTags を使うことで、node が持つ JSDoc のタグの一覧を取得できます。 このタグの中に、削除対象の条件と合致するものがあれば、その node を削除します。

これで不要になったコードを安全に削除できます。

まとめ

TypeScript の AST と JSDoc を使うことで、コードの中から削除する箇所を検出し、安全に削除するプログラムを作ることができました。

これまで人間が目視で注意深く読みながらコードを削除していましたが、プログラムによって安全に行えるようになりました。 JSDoc にタグを書いておくだけで、コードを読まなくても不要になった部分を削除できるため、作業効率も良くなりました。

最初はどのように目印を残そうかと考えていただけだったのが、生産性の向上にまでつなげることができたのは嬉しい誤算でした。 取り組み自体はそこまで派手なものではないですが、このような地味に嬉しい改善をこれからも続けていきたいと考えています。

スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com