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 です。
- ファイル保存時に自動削除
- 現在のファイル・ワークスペース全体への手動適用コマンド
- clangd の diagnostics を利用(
llvm-vs-code-extensions.vscode-clangdが必要)
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 に切り替えると、次の問題が起きました。
- ファイルを保存する
- フックが発火し、未使用 include を削除して
document.save()を呼ぶ - その保存で再びフックが発火する
- 無限ループ…
解決策:処理中のファイルを 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.code は string | 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.yml(on: push: tags)が起動しませんでした。
これは GitHub Actions の仕様です。GITHUB_TOKEN によって発生したイベントは、他のワークフローをトリガーしません(無限ループ防止のため)。
解決策:tagpr で使うトークンを GITHUB_TOKEN から Personal Access Token(GH_PAT)に変更しました。なお、fine-grained PAT では動作せず、classic PAT(repo + 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 を作る際の参考になれば幸いです。