Upgrade to Pro — share decks privately, control downloads, hide ads and more …

デッドコード撲滅のためにエンドポイントの棚卸し機能を作った話 〜ESLintカスタムルールとtypescript-estree利用のすすめ〜

デッドコード撲滅のためにエンドポイントの棚卸し機能を作った話 〜ESLintカスタムルールとtypescript-estree利用のすすめ〜

Taiga KATARAO

June 04, 2024
Tweet

Other Decks in Technology

Transcript

  1. © 2024 Cloudbase Inc. tarao (Taiga Katarao) Cloudbase株式会社 ソフトウェアエンジニy x

    バックエンド〜Webフロントエンドまで TypeScriptで開6 x より興味があるのはWebフロントエンド @tarao1006 @tarao1006
  2. © 2024 Cloudbase Inc. 話すこ と F ESLintのカスタムルールの一R F 実装方法を逐一解説することはしなB

    F 全貌はZennに投稿済y F エッセンス (& 時間が余れば記事投稿後の取り組みの紹介) https://zenn.dev/cloudbase/articles/list-endpoints
  3. Cloudbaseの技術スタック © 2024 Cloudbase Inc. RDB API Server GraphDB Data

    Loader Storage スキャナー お客様のクラウド環境 Web Frontend お客様
  4. © 2024 Cloudbase Inc. 素朴にExpressとSWRを使用し ていた T Expressで素朴にルーティング T SWRで素朴にデータフェッチ

    T useFetchはuseSWRの薄いwrappe™ T 今日はClient Componentの話 € T エッセンスはServer Componentにも展開可能なはず const = => ... ( , ) { }; app. ( , handler); handler get req res " " /v1/foo/:fooId const = => const = ... () { { } < >( fooId ); }; Component useFetch ResponseType data `/v1/foo/${ }`
  5. © 2024 Cloudbase Inc. 呼び出されていないエンドポイン トあり ませんか ? w 「v2を追加した時にv1を消し忘れた」とか「そのエンドポイン

    トを使用しているページがなくなったr w 新任者の無駄なキャッチアップコストになるなど、未使用エンド ポイントはないに越したことはな• w Expressのルーティングは変数ではないため、未使用変数として 検出するといったことはできな• w 「消し忘れないように注意する」は解決策にならない
  6. © 2024 Cloudbase Inc. 定義済みエンドポイン トの列挙 8 割愛 t 8

    Expressのルーターをゴニョゴニョすることで実現可d 8 Zenn: 状態5: バックエンドでエンドポイントの棚卸しを自動化
  7. © 2024 Cloudbase Inc. 使用エンドポイン トの列挙 v 難儀 q v

    APIサーバーと違ってルーターのようなものは存在しなG v ESLint & @typescript-eslint/typescript-estreeが大活躍した https://github.com/typescript-eslint/typescript-eslint
  8. Step1: urlcat導入 フロントエンドでもExpressと同じ文字列を使ってAPIリクエストする ようにするためにurlcatを導入 © 2024 Cloudbase Inc. const =

    => const = ...
 () {
 { } ( ( , { fooId }));
 }; Component useFetch urlcat data " " /v1/foo/:fooId const = ( v1 foo :fooId , { fooId: foo , barId: bar }); console. (path) path urlcat log "/ / / " " " " " // /v1/foo/foo?barId=bar https://github.com/balazsbotond/urlcat
  9. Step2: 型で縛る(定義側) // /foo/:fooId/bar/:barId のような文字列から // { fooId: string }

    & { barId: string } のような型を生成する型 type extends = class extends constructor private : private : private : return ... ... const = < > ...; < > { ( , < >, < , >) {} () { ( .pathname, { .params, .searchParams }); } } < >(path: Path< >) => { return (path. ()); } PathParams T Path T T PathParams T Record toString urlcat useFetch useSWR toString string string string any this this this T pathname params searchParams any © 2024 Cloudbase Inc. useFetchの引数をstringからPathに変更し、 Pathクラスを通してurlcatの使用を強制
  10. Step2: 型で縛る(使用側) © 2024 Cloudbase Inc. const = => const

    = new () {
 { } < >( ( , { fooId }) );
 } Component useFetch ResponseType Path data "/v1/foo/:fooId" Pathを渡すことを強制
  11. Step2: 型で縛る(使用側) © 2024 Cloudbase Inc. const = => const

    = new () {
 { } < >( ( , { fooId }) );
 } Component useFetch ResponseType Path data "/v1/foo/:fooId" const = => const = new () {
 { } ( (` ) );
 } Component useFetch ResponseType Path data < > /v1/foo/${fooId}` Pathを渡すことを強制 Oh... テンプレートリテラルを渡すという抜け道
  12. Step3: ESLintで縛る j 型で縛りきれないのならESLintで縛ろう … (詰んだと思っていたが耐えたr j 調べてみたらESLintの世界では文字列リテラルとテンプレートリテ ラルを区別できるぽい!P j

    というわけで、PathクラスのNewExpressionの第一引数が TemplateLiteralだった場合にエラーになるルールを作れば良い(実 際には文字列リテラル以外を禁止) © 2024 Cloudbase Inc.
  13. Step3: ESLintで縛る export const = => return for const of

    if === && === && !== ESLintUtils.RuleCreator. < , >({ : ( , [ ]) { { ( ) { ( options) { ( node.callee.type node.callee.name option.className node.arguments[option.argumentIndex].type ) { context. ({ node, messageId: , data: { className: option.className, argumentIndex: (option.argumentIndex), }, }); } } }, }; }, }); rule option withoutDocs Options MessageId create NewExpression report ordinal context options node "Identifier" "Literal" "restrict-literal-argument" © 2024 Cloudbase Inc. 実装の雰囲気
  14. Step3: ESLintで縛る useFetch Path useFetch useFetch useFetch useFetch Path useFetch

    Path useFetch Path ( ( , { fooId })); ( fooId ); (文字列変数); (文字列を返す関数呼び出し); ( ( fooId )); ( (文字列変数)); ( (文字列を返す関数呼び出し)); new new new new "/v1/foo/:fooId" `/v1/foo/${ }` `/v1/foo/${ }` © 2024 Cloudbase Inc. ESLintでエラー 型でエラー
  15. Step4: typescript-estreeを使って使用エンドポイントを列挙 “ Pathクラスの第一引数は文字列リテラルしかあり得なくなったので、あとは Pathクラスの第一引数を列挙すれば、使用エンドポイントを列挙できそ う!!c “ でも、どうやって列挙する、、、? m “

    気合いで正規表現P “ というか、ESLintのカスタムルール作った時にエラー吐いてるところをstringの arrayにpushする実装に変えられたらめっちゃ簡単に列挙できるのでは? © 2024 Cloudbase Inc.
  16. Step4: typescript-estreeを使って使用エンドポイントを列挙 g @typescript-eslint/typescript-estree !!!!p g “A parser that produces

    an ESTree-compatible AST for TypeScript code.” であり@typescript- eslint/parserの内部で使用されているr g ESLintのカスタムルールとほぼ同じように書けて学習コストが低いのでおすすめ © 2024 Cloudbase Inc. const = new if === && === && === && typeof === < >() (node) { ( node.callee.type node.callee.name node.arguments[ ].type node.arguments[ ].type ) endpoints. (node.arguments[ ].value); } } endopoints string 0 0 0 Set NewExpression add "Identifier" "Path" "Literal" "string" NewExpression report ordinal (node) { ( options) { ( node.callee.type node.callee.name option.className node.arguments[option.argumentIndex].type ) { context. ({ node, messageId: , data: { className: option.className, argumentIndex: (option.argumentIndex), }, }); } } } for const of if === && === && !== option "Identifier" "Literal" "restrict-literal-argument"
  17. Step5: 型パラメータに制約をつける v 実は、Expressから定義済みエンドポイントを列挙する際にユニオン型にしてし まえば、Pathの型引数に制約をつけられu v ただし、この制約をつけたとしても前述したカスタムルールやtypescript- estreeによるASTの走査は依然必P v なぜか

    © 2024 Cloudbase Inc. type = | | class extends constructor private : private : private : ; < > { ( , < >, < , >) {} } GatPathname Path T GetPathname T PathParams T Record "/v1/foo/:fooId" "/v2/foo/:fooId" "/v1/bar/:barId" pathname params searchParams string any Expressからユニオン型を生成して型引数に制約をつける
  18. Schema firstに移行 © 2024 Cloudbase Inc. app. ( , );


    app. ( , );
 app. ( , ); get get post "/v1/foo/:fooId" "/v2/foo/:fooId" "/v1/foo" ... ... ... app. ( , );
 app. ( , );
 app. ( , ); get get post "/v1/foo/:fooId" "/v2/foo/:fooId" "/v1/foo" ... ... ... type = type = ; ; GetPathname PostPathname "..." "..." const = { : z. ({ request: z. ({ pathParams: z. ({}), queryParams: z. ({}), body: z. ({}), }), response: z. ({}), }), : z. ({ ... }), : z. ({ ... }), }; schema "GET /v1/foo/:fooId" "GET /v2/foo/:fooId" "POST /v1/foo" object object object object object object object object メソッドとパス リクエスト/レスポンスの情報
  19. Schema firstに移行 ) スキーマにリクエスト/レスポンスの型の情報があるのでgenericsが 不要になった © 2024 Cloudbase Inc. const

    = { } < >( fooId ); data useFetch ResponseType `/v1/foo/${ }` const = new { } ( ( , { fooId })); data useFetch Path "/v1/foo/:fooId"
  20. © 2024 Cloudbase Inc. まとめ d コーディングルールはESLintのルールで表現できると良b d ASTにおいてはstringリテラルとテンプレートリテラルを区別で きf

    d typescript-estreeはESLintの知識を活かしてASTをいじれるよう になるのでおすすe d (REST APIであってもエンドポイントの定義はSchema firstにし ておくと便利)