2026-03-24

C++ の未使用 include を削除する VS Code Extension『C++ Unused Includes Remover』 を作った

作った経緯

AtCoder の環境を dev container に寄せた話は前に書きました。

https://michimani.net/post/programming-set-up-devcontainer-for-atcoder-with-cpp

VS Code で AtCoder の問題を解くとき、テンプレートとして下記のような内容を設定しています。

#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
#include <cmath>

using namespace std;
using ui = unsigned int;
using ull = unsigned long long;
using ll = long long;

int main()
{
  

  return 0;
}

しかし、必ずしもこれらの include が必要ではなく、使用しないこともしばしばあります。そんなときに毎回目検で不要な include を削除していたのですが、さすがに面倒でした。 C++ 用の既存の extension である llvm-vs-code-extensions.vscode-clangd は未使用の include に対してエディタ上では波線を、 Problems タブでは警告を出してくれますが、削除まではやってくれませんでした。

ということで作ってみることにしました。


拡張機能の概要

C++ Unused Includes Remover は、C/C++ ファイルを保存したときに未使用の #include ディレクティブを自動で削除する VS Code Extension です。

https://marketplace.visualstudio.com/items?itemName=michimani.cpp-unused-includes-remover


実装

基本的な仕組み

VS Code の Extension API には vscode.languages.getDiagnostics() という関数があり、現在エディタに表示されている Problems(問題)の一覧を取得できます。clangd は未使用の include を検出すると source: "clangd", code: "unused-includes" という diagnostic を報告します。この情報を利用して、該当行を WorkspaceEdit で削除するというシンプルな仕組みです。

ファイル保存
  → getDiagnostics() で未使用 include を特定
  → WorkspaceEdit で該当行を削除
  → document.save() で保存

ハマりポイント①:onWillSaveTextDocument は使えない

最初、ファイル保存のフックとして onWillSaveTextDocument を使っていました。しかしこれだと diagnostics が常に空になる問題が発生しました。

原因は、clangd が保存処理の中で diagnostics をいったんクリアするためです。保存前フックが走るタイミングでは、まだ再解析が完了しておらず getDiagnostics() が空を返してしまいます。

解決策onDidSaveTextDocument(保存完了後フック)に切り替え、さらに clangd の解析完了を待つための待機時間(waitForDiagnosticsMs、デフォルト 100ms)を設けました。

vscode.workspace.onDidSaveTextDocument(async (document) => {
  await delay(waitMs); // clangd の解析を待つ
  const diagnostics = vscode.languages.getDiagnostics(document.uri);
  // ...
});

ハマりポイント②:無限ループへの対処

onDidSaveTextDocument に切り替えると、次の問題が起きました。

  1. ファイルを保存する
  2. フックが発火し、未使用 include を削除して document.save() を呼ぶ
  3. その保存で再びフックが発火する
  4. 無限ループ…

解決策:処理中のファイルを Set<string> で管理する再入防止ガードを追加しました。

const processingFiles = new Set<string>();

onDidSaveTextDocument(async (document) => {
  const key = document.uri.toString();
  if (processingFiles.has(key)) return; // 処理中なのでスキップ
  processingFiles.add(key);
  try {
    await applyOnSave(document);
  } finally {
    processingFiles.delete(key);
  }
});

ハマりポイント③:diagnostic の code がオブジェクト型

ログに code="[object Object]" と出力されたため調査したところ、VS Code の Diagnostic.codestring | number | { value: string | number; target: Uri } という union 型であることがわかりました。clangd は { value: "unused-includes", target: ... } というオブジェクト形式で code を返すため、文字列と直接比較すると常に false になります。

解決策:code の型を判定してから比較するよう修正しました。

const codeValue = typeof diag.code === "object" ? diag.code.value : diag.code;
if (codeValue === "unused-includes") { ... }

Microsoft C/C++ Extension サポートは断念

当初は clangd だけでなく Microsoft C/C++ Extension(cpptools)の diagnostics にも対応しました。しかし動作が不安定なことと設定が複雑になることから、最終的に clangd 専用に割り切りました。diagnosticSource の設定値は "auto""clangd" 固定に変更しています。


clangd の環境構築

VS Code Extension として動作させるには、clangd の VS Code Extension(llvm-vs-code-extensions.vscode-clangd)に加えて、clangd のバイナリ本体がシステムにインストールされている必要があります。

apt の clangd は古い

Debian/Ubuntu で apt-get install clangd すると古いバージョン(今回の環境では 14.0.6)が入ります。-std=c++23 が正式にサポートされたのは Clang 17 以降のため、LLVM の公式 apt リポジトリから新しいバージョンを入れる必要がありました。

wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \
  | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null
echo "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-19 main" \
  | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get update && sudo apt-get install -y clangd-19

.clangd 設定ファイル

未使用 include の検出を有効化するには、プロジェクトルートに .clangd ファイルを置く必要があります。

CompileFlags:
  Add: [-std=c++23]
Diagnostics:
  UnusedIncludes: Strict

compile_commands.json がなくても動作しますが、あった方が検出精度が上がります。


リリース環境の構築

tagpr による自動リリース

tagpr を使い、main へのマージのたびにリリース PR を自動作成する仕組みにしました。リリース PR をマージするとタグが作成され、GitHub Actions の release ワークフローが起動して VSIX のビルドと Marketplace への publish が自動で行われます。

ハマりポイント:GITHUB_TOKEN ではワークフローがトリガーされない

tagpr が GITHUB_TOKEN を使ってタグを作成しても、release.ymlon: push: tags)が起動しませんでした。

これは GitHub Actions の仕様です。GITHUB_TOKEN によって発生したイベントは、他のワークフローをトリガーしません(無限ループ防止のため)。

解決策:tagpr で使うトークンを GITHUB_TOKEN から Personal Access Token(GH_PAT)に変更しました。なお、fine-grained PAT では動作せず、classic PATrepo + workflow スコープ)が必要でした。

- uses: actions/checkout@v6
  with:
    token: ${{ secrets.GH_PAT }}
- uses: Songmu/tagpr@v1
  env:
    GITHUB_TOKEN: ${{ secrets.GH_PAT }}

ハマりポイント:VSCE_PAT の設定

VS Code Marketplace への publish には Azure DevOps の PAT が必要です。PAT 作成時に Organization を特定の組織に設定していたため、/michimani publisher へのアクセスが拒否されていました。

解決策:Organization を 「All accessible organizations」 に変更して PAT を再発行しました。


まとめ

実装自体はシンプルですが、VS Code Extension API の挙動(diagnostics のタイミング、code のオブジェクト型)や GitHub Actions の仕様(GITHUB_TOKEN のトリガー制限)など、いくつかの落とし穴がありました。同様の Extension を作る際の参考になれば幸いです。