🤖

VSCodeで.clang-format-ignoreが無視される問題

2024/10/22に公開

タイトルの問題にぶち当たって原因を調査しPRを出すまでの記録です。

C/C++ Extensionで問題が出たんですが、同様の方法でclang-formatを起動する他の拡張機能も同様の問題を抱えていると思われます。

TL;DR

  1. 19.1.0時点のclang-formatは、書式化対象を標準入力から与えファイル名を-assume-filenameオプションで指定する場合、.clang-format-ignoreを無視します。
  2. VSCodeのC/C++ Extensionはそのような方法でclang-formatを起動しているので、フォーマットの際に.clang-format-ignoreが無視されます。
  3. この問題を修正するPRを出しておきました
  4. (2024/10/25追記) 無事マージされたのでLLVM 20の頃には直ってるんじゃないかと思います。

.clang-format-ignore

clang-format 18.1.0から.clang-format-ignoreファイルのサポートが追加されました。これは.gitignoreに似たフォーマットで1つ以上のパターンを記載しておくと、そのパターンのいずれかにマッチしたファイルについては書式化を行わないというモードです。

例えば以下のような.clang-format-ignoreを用意します。

foo/*.c
a*.c

このとき、それと同じディレクトリにあるabc.cfooサブディレクトリにあるxyz.cなどといったファイルに対してclang-formatを実行しても、何も起こりません。

$ clang-format abc.c  # 何も出力されない(コマンド自体は正常終了する)
$ clang-format -i abc.c  # 何も起こらない
$ clang-format -output-replacement-xml abc.c  # 何も出力されない(!?)

最後の-output-replacement-xmlオプションを付けた場合であっても何も出てこないのは本当にそれでOKなのかよくわかりませんが…。

なお、すべての階層のサブディレクトリを指定する**という表記は19.1.0現在まだ使えません。PRは出ているようなのでこれがマージされるのを待ちましょう。

問題: VSCode上で.clang-format-ignoreが効かない

C/C++ Extensionを入れると、VSCodeでclang-formatを使ったCファイルやC++ファイルの自動書式化が可能なのですが、なぜか.clang-format-ignoreが無視されてしまうという問題があります。

C/C++ Extensionにはclang-formatコマンドが同梱されており、システムに入っていない場合にはそれが代わりに使われます。その辺りが悪さをしているのかと疑いましたが、同梱コマンドも18.1.0以降のバージョンになっていたので、バージョンに起因する問題ではなさそうです。

C/C++ Extensionが発行しているコマンドを確認する

拡張機能の設定にLogging Levelというものがあり、これをデフォルトのErrorからDebugに変更することで、書式化の際どのようなコマンドが実行されているのかを確認することができます。

出力パネルからC/C++を選択すると、以下のような出力が確認できます。

ドキュメントの書式設定: file:///foo/bar/baz.c
Formatting Engine: clangFormat
...
/home/hoge/.vscode/extensions/ms-vscode.cpptools-1.22.10-linux-x64/bin/../LLVM/bin/clang-format -style=file -fallback-style=LLVM --Wno-error=unknown -assume-filename=/foo/bar/baz.c

どうやらC/C++ Extensionでは、(たとえファイル全体の書式化であっても)clang-formatコマンドにファイルを直接指定することはせず、標準入力からファイルの内容を与え、標準出力から書式化の結果を受け取っているようです。

-assume-filenameオプションは、標準入力から書式化したい内容を与える際に、書式化対象のファイル名を指定する機能です。これにより、-style=fileオプションを与えた場合に適切な.clang-formatファイルを探したり、拡張子から言語を推定したりすることができるようになります。

(以下、https://clang.llvm.org/docs/ClangFormat.html より引用)

  --assume-filename=<string>     - Set filename used to determine the language and to find
                                   .clang-format file.
                                   Only used when reading from stdin.
                                   If this is not passed, the .clang-format file is searched
                                   relative to the current working directory when reading stdin.
...
  --style=<string>               - Set coding style. <string> can be:
...
                                   2. 'file' to load style configuration from a
                                      .clang-format file in one of the parent directories
                                      of the source file (for stdin, see --assume-filename).
                                      If no .clang-format file is found, falls back to
                                      --fallback-style.
                                      --style=file is the default.

clang-formatの挙動を確認

C/C++ Extensionがどのようなコマンドを実行しているかが分かったので、コマンドラインから同じオプションを指定して手動でclang-formatを実行し、その挙動を確認していきます。

以下のような.clang-format-ignoreファイルを用意しました。

foo.c

同じディレクトリに適当な内容のfoo.cファイルを用意して試します。

$ clang-format foo.c  # 何も出ない
$ clang-format -assume-filename=foo.c < foo.c
int main(void) { return 0; }

なんか出た!

どうやらclang-formatは-assume-filenameオプションで指定したファイル名に対して.clang-format-ignoreを適用しないようです。.clang-formatの適用はしてくれるのに…。

ソースコードを確認する

clang-formatコマンドのmain()を見てみます。

https://github.com/llvm/llvm-project/blob/llvmorg-19.1.2/clang/tools/clang-format/ClangFormat.cpp#L709-L736

(本記事の著者によるコメントを適宜挿入しています)

  // コマンドラインに処理対象のファイル名が指定されなかった場合は、
  // 標準入力を対象として書式化を実行し終了する
  if (FileNames.empty())
    return clang::format::format("-", FailOnIncompleteFormat);

  ...

  // コマンドラインで指定された各ファイルを順次処理していく
  for (const auto &FileName : FileNames) {
    // .clang-format-ignoreのチェック
    const bool Ignored = isIgnored(FileName);
    ...
    // チェックに引っかかった場合は何もせず次のファイルに移る
    if (Ignored)
      continue;
    // --verboseオプションが指定された場合は処理対象ファイルのファイル名を出力する
    if (Verbose) {
      errs() << "Formatting [" << FileNo++ << "/" << FileNames.size() << "] "
             << FileName << "\n";
    }
    // 書式化を実行
    Error |= clang::format::format(FileName, FailOnIncompleteFormat);
  }

ということで、先ほど確認した通り、(コマンドラインから処理対象ファイルを指定せず)書式化対象のコードを標準入力から与えた場合、.clang-format-ignoreのチェックは行われません。

修正案

PR#113100を出しました。

コマンドラインに処理対象のファイル名が指定されず、かつ-assume-filenameが指定されている場合、無視チェックを行い、チェックに引っかかった場合には何も出力せずコマンドを終了します。

-output-replacements-xml指定時にこの挙動でよいのか正直自信がありません…)

マージされるまで待ってられない人は自前でclang-formatをビルドしてそれを使ってください(参考)。

$ git clone -b assume-filename-with-clang-format-ignore --filter=blob:none https://github.com/kakkoko/llvm-project.git
$ cd llvm-project
$ cmake -S llvm -B build -G Ninja -DLLVM_ENABLE_PROJECTS=clang -DCMAKE_BUILD_TYPE=Release
$ cmake --build build
$ cp build/bin/clang-format (適当な場所)

Discussion