Knipで余計なコードを増やさないようにしよう

## はじめに

みなさん、不要なコードや依存関係の解消に悩まれてませんか。  
悩むほどではないにしても、使ってないって分かっているなら消したいですよね。  
もちろん私もボーイスカウトを嗜んでいるので不要なコードは見かけ次第消してるものの、発生した時点で知りたいんです、消したいんです。  
ref. https://github.com/97-things/97-things-every-programmer-should-know/tree/master/en/thing_08

そんなわたしに嬉しいKnipって言うツールがあったんです。  
ref. https://knip.dev/  

今回はKnipについてざっくりどんなことが出来るツールなのか、開発プロセスに組み込んだ話を紹介します。

## おことわり

KnipはJavaScript/TypeScriptのためのツールです。  
他言語のツールをお探しの方のご期待には沿えません :bow:  

本記事を書いた時のKnipのバージョンは [5.17.3](https://github.com/webpro-nl/knip/tree/5.17.3) です。

公式ドキュメントの内容が最新かつ正確です。 

## Knipとはなにものであるか

とんでもなくざっくりいうと、不要な依存関係やコードを収集してくれるツールです。  


### 何を検知してくれるのか

- 未使用なファイル
- 未使用な型
- 未使用な exports 
- 未使用な依存関係
- 未定義（暗黙的）な依存関係

ここらへんを検知/報告してくれます。
例えばこんな感じで見ることが出来ます

```sh
$ yarn run knip --reporter markdown
# Knip report

## Unused files (1)

* src/unused.ts

## Unused dependencies (1)

| Name  | Location     | Severity |
| :---- | :----------- | :------- |
| axios | package.json | error    |

## Unused devDependencies (1)

| Name         | Location     | Severity |
| :----------- | :----------- | :------- |
| @types/axios | package.json | error    |

## Unlisted dependencies (1)

| Name | Location     | Severity |
| :-- | :----------- | :------- |
| zod | src/index.ts | error    |

## Unused exports (1)

| Name           | Location          | Severity |
| :------------- | :---------------- | :------- |
| unusedFunction | src/types.ts:9:14 | error    |

## Unused exported types (1)

| Name       | Location          | Severity |
| :--------- | :---------------- | :------- |
| UnusedType | src/types.ts:5:13 | error    |

```


<details>
    <summary>参考コード</summary>
恣意的に起こしているので違和感のあるコードになるかなと思いますが、参考程度に置いておきます。

```json:package.json
{
  "main": "src/index.ts",
  "dependencies": {
    "axios": "^1.7.2"
  },
  "devDependencies": {
    "@types/axios": "^0.14.0",
    "@types/node": "^20.14.0",
    "knip": "^5.17.3",
    "typescript": "^5.4.5"
  }
}
```
```ts:src/index.ts
import type { UsedType } from "./types";
import zod from "zod";

const main = (): UsedType => ({ id: "a" });
const zodFn = () =>
  new zod.ZodBigInt({
    checks: [],
    coerce: false,
    typeName: zod.ZodFirstPartyTypeKind.ZodBigInt,
  });

() => {
  console.log("start");
  console.dir(main());
  console.dir(zodFn());
};
```
```ts:src/types.ts
export type UsedType = {
	id: string;
}

export type UnusedType = {
	name: string;
}

export const unusedFunction = () => console.log('unused')
```
```ts:src/unused.ts
console.log('unused file');
```

</details>


### 何が嬉しいのか

もちろん、種々様々な未使用・未定義なコード・ファイル等を検知してくれること自体がとても素晴らしくありがたく使わせてもらう動機になりますよね。  
それだけでなく、**利用しているライブラリを検知してZero Configで良しなに動いてくれます**。

例えば、バンドルツールにViteを使っているプロジェクトがあったとして、Viteのプラグインはプロダクトのコードにもちろん出てきません。
ただ、Viteの設定ファイルではまず間違いなく宣言されているはずです。  
良くあるツールですと、「うちはVite使ってるから`vite.config.ts`をチェック対象に入れなきゃ」とか例外の条件を書いたりする必要に追われますよね。  
coverage reportの対象外とするファイルを丁寧に拾い上げるように…。  
そういうのをまるっと不要にしてくれます。もちろん、明示することもできます。

また、最近流行の風向きを感じるyarn workspaceのようなモノリポにも最初から対応してくれています。  
何よりも公式ドキュメントがしっかりしているので、やりたいことは大体探せそうです。  

## 不要なコードを増やさないようにしよう

どのタイミングで未使用なコードやライブラリが増えたことを知りたいか、というとやはりPull Requestが投げられたタイミングかと思われます。  
不要なコードは、必要に応じてリファクタリングを行いコードのメンテナンス性を高める営みで発生するか、PoCを高速で繰り返すプロダクト開発の現場で見られますよね。  
いずれも手元でチェックするには限定的なシチュエーションで、マージされた後では遅いと感じます。  

PRが投げられたタイミングでCI環境上に任せてしまうのが最適で、楽そうです。  

ということで、こんな感じのワークフローを用意するのが良いでしょう。  

```yaml:.github/workflows/check-unused.yaml
name: Check Unused
on:
  pull_request:
    branches:
      - main
    types: [opened, reopened, ready_for_review]

permissions:
  pull-requests: write
  contents: read

jobs:
  check-unused:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: .tool-versions
      - uses: actions/cache@v4
        id: cache
        env:
          cache-name: yarn-cache
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-
      - name: install
        if: steps.cache.outputs.cache-hit != 'true'
        run: yarn install
      - name: knip
        id: knip
        run: yarn run knip --reporter markdown
        continue-on-error: true
      - name: Comment PR
        if: failure()
        uses: thollander/actions-comment-pull-request@v2
        with:
          message: |
            ## Knip Result
            ```
            ${{ steps.knip.outputs.stdout }}
            ```
          comment_tag: execution
          mode: upsert
          pr_number: context.issue.number
```

mainブランチにPRが投げられると、もし不要なファイル等があればPRにコメントをするように動くはずです。
動作は手元で確認しているわけではないので必要に応じて修正をお願いします。

## 終わりに

もちろん、銀の弾丸は存在しないと言いますし、Knipがすべてを良きように解決するわけではありません。  
わたしが出会った困りごとは例えば次のようなものがあります。

- webpackで使用しているライブラリが"不要"として検知されてしまう
  - パッとソースコードを読んだ感じだと、wepbackの `use`/`rule` あたりに宣言されているライブラリしか検知してくれなさそう
    - ref. https://github.com/webpro-nl/knip/blob/5.17.3/packages/knip/src/plugins/webpack/index.ts#L33-L49
  - `tsconfig-paths-webpack-plugin` を `resolve` 以下で宣言している弊プロダクトでは検知してもらえず

- Serverless Frameworkに対応していない
  - ライブラリのほとんどを文字列で指定しているServerless Frameworkはいまだ対応されていない
    - Issueはあるので気長に待つか、実装しましょう
      - ref. https://github.com/webpro-nl/knip/issues/483#issuecomment-1935618539
    - 今回弊プロダクトでは `ignoreDependencies: ['.*serverless.*'] `と指定することで回避しました

もしかすると他にも出会う不都合があるかもしれませんが、本件について回避策・解決策を見つけた暁にはどこかに残していただけると嬉しいです :pray: