big-code-analysis
本書には英語版もあります。
big-code-analysis は、多くの異なるプログラミング言語で書かれたソースコードを分析し、情報を抽出するための Rust ライブラリです。パーサージェネレーターであり、インクリメンタルなパースを行うライブラリでもある Tree Sitter をベースにしています。
このソフトウェアのソースコードは GitHub で公開されており、不具合の報告や機能要望は対応する GitHub Issue Tracker に投稿できます。
📖 Rust API リファレンス: クレートの型とメソッドの完全なリファレンスは docs.rs にあります。本書はタスク指向であり、docs.rs はソースから直接生成される、常に最新の正式なリファレンスです。
対応プラットフォーム
big-code-analysis は、Linux、macOS、Windows という主要なプラットフォームで動作します。
コンパイル済みでパッケージ化された Linux および Windows のバイナリは、GitHub Release Page から入手できます。
API ドキュメント
big-code-analysis をクレートとして利用したい場合は、Rustdoc で生成された完全な API docs(フィーチャゲートされた vcs モジュールを含む、すべての公開型・トレイト・関数)が docs.rs にあります。
クレートの組み込みに関するタスク指向のガイド(クイックスタート、メモリ上での分析、FuncSpace 結果のたどり方、エラー処理)については、ライブラリとしての利用 セクションを参照してください。
PyO3 バインディング(pip install big-code-analysis、バッチ処理、フラットレコードの反復処理、SARIF 出力、非同期パターン)については、Python バインディング セクションを参照してください。
ライセンス
-
Mozilla が定義した文法は、MIT ライセンスの下でリリースされています。
-
big-code-analysis、big-code-analysis-cli、big-code-analysis-web は Mozilla Public License v2.0 の下でリリースされています。
対応言語
これは big-code-analysis がパースするプログラミング言語の一覧です。以下の各エントリは(src/langs.rs の mk_langs! 呼び出しで定義される)実際の LANG バリアントであり、言語ごとの Cargo フィーチャ に記載された、対応する言語別 Cargo フィーチャの背後にゲートされています。
- Bash
- C
- C/C++
- C#
- Elixir
- Go
- Groovy
- Irules
- Java
- JavaScript
- Kotlin
- Lua
- Mozcpp
- Mozjs
- Objective-C
- Perl
- Php
- Python
- Ruby
- Rust
- Tcl
- Tsx
- Typescript
一部のエントリは、共有の文法パイプラインのバリアントです。JavaScript(アップストリームの tree-sitter-javascript 文法)は .js、.mjs、.cjs、.jsx ファイルのデフォルトです。Mozjs は Mozilla / SpiderMonkey フォークで、現在はオプトインです — .jsm(Firefox モジュール)拡張子のみを所有し、正規のスラッグ mozjs を報告します。両者は通常の JavaScript に対してメトリクス的に等価です。Tsx は JSX 構文を有効にした Typescript であり、別個のスラッグ tsx を報告します。#721 以降、C には独自のバリアント C(スラッグ c、アップストリームの tree-sitter-c)があり、.c と emacs モード c を所有します。C/C++ バリアント(スラッグ cpp、#720 以降はアップストリームの tree-sitter-cpp)は .cpp / .cc / .h とその他を引き続き受け持ちます。.h は意図的に Cpp のままです。C++ ヘッダーを C 文法に通すと class / template で ERROR が連鎖するのに対し、C ヘッダーを C++ 文法に通しても C++ キーワードと同名の識別子でつまずくだけだからです。Mozilla/Gecko の C++ 方言はオプトインの Mozcpp バリアント(スラッグ mozcpp)で、ファイル拡張子を所有せず、名前でのみ選択されます — ちょうど Mozjs と JavaScript の関係と同じです(C# は csharp を報告します)。#724 以降、Objective-C(スラッグ objc、アップストリームの tree-sitter-objc)は .m と emacs モード objc / objective-c を所有します。Objective-C++(.mm)は Cpp のままです。.mm ファイルには Objective-C と C++ が混在しており、tree-sitter-objc 文法は C++ 側(テンプレート、名前空間、::)をパースできないため、Objective-C の接合部分でしかつまずかない C++ 文法のほうが、この場合はより穏やかに劣化します — .h と同じトレードオフです。したがって .mm ファイルの Objective-C 部分のメトリクスは近似値です。各バリアントのスラッグはその LANG::name であり、小文字かつ句読点を含まないため、FromStr を通じてラウンドトリップできます。
内部ヘルパーバリアント
以下の LANG バリアントはユーザー向けの言語ではありません — C ファミリー分析パイプラインの内部ヘルパーであり(すべての C ファミリー Cargo フィーチャ cpp、c、mozcpp に同梱されます)、ソースファイルの分析時に直接選択されることはありません:
Ccomment— C/C++ のコメントに特化しています。Preproc— C/C++ のプリプロセッサマクロに特化しています。
注: #720 以降、Mozilla/Gecko の C++ 方言は
MozcppLANGバリアントとして公開されています(ベンダリングされたbca-tree-sitter-mozcppクレートに支えられ、オプトインのmozcppフィーチャで取り込まれます)。ファイル拡張子を所有しない、名前で選択可能な完全に公開の言語であり、上記のCcomment/Preprocヘルパーとは異なり、内部用では 「ありません」 。
対応コードメトリクス
この章は、big-code-analysis が計算するすべてのメトリクスのガイドツアーです。各セクションは元の研究論文から出発してアルゴリズムを順に解説し、そのメトリクスが 本来 意図されていた使われ方と、数年後に業界が実際に行き着いた使われ方の両方を説明します。ソフトウェアメトリクスに不慣れな場合は、セクションを順番に読んでください — 後半のメトリクス(特に保守容易性指数)は、前半のメトリクス(Halstead、循環的複雑度、LOC)の上に明示的に構築されています。
始める前に、いくつかの前提となる注意点を挙げます:
- メトリクスは測定値であって、判定ではありません。 このページのすべての数値は、ソースコードの構造的な性質を要約したものです。正しさ、生産性、開発者のスキルを測るものは一つもありません。どのメトリクスにとっても最も重要な問いは常に「何と比較して?」です — 1 か月前の同じモジュール、兄弟モジュールと比べたこのモジュール、業界のベースラインと比べたこのコードベース、というように。絶対的なしきい値は、せいぜい大まかなヒューリスティックにすぎません。
- ここにあるほとんどのメトリクスは 3 つのスコープで計算されます: 関数 / メソッド ごと、 クラスやユニット的なスペース ごと、そして 「ファイル」 ごとです。基盤となる tree-sitter パーサーは「スペース」(関数、クロージャ、クラス、名前空間など)のツリーを生成し、すべてのメトリクスはそのツリーを通じてロールアップされます。
- オブジェクト指向メトリクスは、オブジェクト指向の構文に対してのみ反応します。 WMC、NPA、NPM は、
implブロックのない Rust ファイルやクラスのない Python モジュールでは0を報告します。これは正しい答えであり、バグではありません。
一覧
| メトリクス | 測定対象 | 初出 |
|---|---|---|
| ABC | <Assignments, Branches, Conditions> として表したサイズ | Fitzpatrick, 1997 |
| 認知的複雑度 | 関数を 読む のがどれだけ難しいか | Campbell / SonarSource, 2017 |
| 循環的複雑度(CC) | 関数内の独立した経路の数 | McCabe, 1976 |
| Halstead | 語彙に基づくサイズ、難易度、労力、バグ数 | Halstead, 1977 |
| コード行数(SLOC、PLOC、LLOC、CLOC、BLANK) | 生・物理・論理・コメント・空白の各行数 | Conte, Dunsmore & Shen, 1986 |
| 保守容易性指数(MI) | 複合的な保守性スコア | Oman & Hagemeister, 1992; Coleman et al., 1994 |
| NArgs | 関数ごとの引数の数 | 慣習的なメトリクス |
| NExits | 関数ごとの出口点の数 | 構造化プログラミングの文献 |
| NOM | メソッドとクロージャの数 | Lorenz & Kidd, 1994 |
| NPA | 公開属性の数 | Lorenz & Kidd, 1994 |
| NPM | 公開メソッドの数 | Lorenz & Kidd, 1994 |
| Tokens | Tree-sitter のリーフトークン数(サイズの代理指標) | Lizard ツール、Terry Yin |
| WMC | クラスの全メソッドにわたる循環的複雑度の合計 | Chidamber & Kemerer, 1994 |
ABC
ABC メトリクスは、コード片のサイズを 3 次元ベクトルとして測定します。各成分は 1 種類の操作をカウントします:
- Assignments(代入)— 値を変数に格納するあらゆる操作で、複合代入(
+=、++)や明示的な初期化を含みます。 - Branches(分岐)— 関数およびメソッドの 呼び出し です。名前に反して、これは条件ジャンプの数ではありません。制御が他のコードへと分岐していく地点の数です。
- Conditions(条件)— ブール判定です。比較演算子(
==、!=、<=、>=、<、>)、三項演算子(?)、および固定のキーワード集合(else、case、try、catch)が対象です。default/ ワイルドカードアームはどの言語でもカウントされません(後述の言語別の逸脱を参照)。短絡論理演算子&&と||は単独ではカウントされず、代わりに&&/||チェーンの比較でない各オペランドが、Fitzpatrick の「単項条件式(unary conditional expression)」規則によって 1 条件として寄与します。次の節では、規則、言語別の逸脱、および実例を順に説明します。
このメトリクスは、Jerry Fitzpatrick が 1997 年の C++ Report 誌の記事 Applying the ABC metric to C, C++ and Java で導入しました。現代の言語で何を A、B、C としてカウントするかの規則を含む現行の正準仕様は、Fitzpatrick の Software Renovation サイトで維持されています。
カウント規則
Fitzpatrick の論文は規則を 3 つの図で列挙しています — Figure 2(C)、Figure 3(C++、Figure 2 を拡張)、Figure 4(Java)です。Big-code-analysis はこれらの規則集合を言語ごとにそのまま実装しています。以下の表は、各コンポーネントで何がカウントされるかを、各行をそれを導入した図に帰属させてまとめたものです。
代入
| 規則 | A としてのカウント | 初出 |
|---|---|---|
単純代入(=) | 出現ごとに 1 | Figure 2(C) |
複合代入(+=、-=、*=、/=、%=、<<=、>>=、&=、\|=、^=) | 出現ごとに 1 | Figure 2(C)/ Figure 4(Java) |
Java の符号なし右シフト代入(>>>=) | 出現ごとに 1 | Figure 4(Java) |
前置または後置のインクリメント / デクリメント(++、--) | 出現ごとに 1 | Figure 2(C) |
| 初期化を伴うコンストラクタ呼び出し | 出現ごとに 1 | Figure 3(C++) |
分岐
| 規則 | B としてのカウント | 初出 |
|---|---|---|
| 関数またはメソッドの呼び出し | 呼び出し箇所ごとに 1 | Figure 2(C)/ Figure 4(Java) |
new 演算子 | 出現ごとに 1 | Figure 3(C++)/ Figure 4(Java) |
delete 演算子 | 出現ごとに 1 | Figure 3(C++) |
goto label、break label、continue label | 出現ごとに 1 | Figure 2(C)/ Figure 3(C++)/ Figure 4(Java、ラベル付き break / continue のみ — Java に goto はありません) |
条件
| 規則 | C としてのカウント | 初出 |
|---|---|---|
比較演算子(==、!=、<=、>=、<、>) | 出現ごとに 1 | Figure 2、Rule 5 |
三項演算子 ? : | 出現ごとに 1 | Figure 2、Rule 5 |
else、case | 出現ごとに 1 | Figure 2、Rule 5 |
プリプロセッサの #else、#elif | 出現ごとに 1 | Figure 2、Rule 5 |
try、catch | 出現ごとに 1 | Figure 3(C++)/ Figure 4(Java) |
| 単項条件式 | && / \|\| の比較でないオペランドごとに 1(さらに if / while / 引数 / return スロット内の、! で包まれた条件または裸の真偽値条件ごとに 1) | Figure 3、Rule 7 / Figure 4、Rule 9 |
短絡論理演算子(&&、||、および各言語の同等物 — Ruby の and / or、Python の and / or、Perl の and / or / xor、Lua の and / or、Tcl の && / ||、iRules の && / || / and / or)は、単独では条件に寄与しません。代わりに、単項条件規則によって比較でない各オペランドが 1 条件として寄与します。論文はこれを 2 か所で明示しています。
- Listing 2 は
(am >= 0 && am <= 0xF) ? '/' : 'C'にacccと注記しています — 代入 1 つと条件 3 つで、3 つの条件は 2 つの比較(>=、<=)と三項演算子(?)です。&&自体の寄与はゼロです。 - 一方 Rule 7 / Rule 9 は各オペランドをカウントします。
if (x || y) printf("test failure\n");について、論文は「xとyの両方が条件式として評価されるため、単項条件は 2 つある」と記しています。ここでも||の寄与はゼロで、xとyがそれぞれ 1 ずつ寄与します。
言語別の逸脱
言語ごとの impl Abc ブロックは、その言語に相当する構文がない場合や、論文の規則を字義どおり適用すると過剰カウントになる場合に、規則集合を絞り込んでいます。
| 言語 | 逸脱 | 理由 |
|---|---|---|
| C、Go、Rust | try / catch を省略 | 文法に try/catch キーワードがありません。エラー処理には errno / Result / Result 系の直和型を使います。 |
| Ruby | catch の代わりに Rescue | Ruby の例外処理キーワードは rescue であり、AST ノード Rescue が Java の catch の役割を果たします。 |
| 全言語 | default / _ ワイルドカードアームを条件集合から除外 | Fitzpatrick の Figure 2 は default を挙げていますが、これは無条件にフォールスルーします — カウントすると、本体に関係なくすべての switch / match で C が膨らみます。big-code-analysis はすべての言語でこれを省略します(Rust の _ => アームや Java の default: アームも含みます)。 |
| Tcl | チェーンオペランドの単項条件は実装済み。裸の真偽値 / 引数 / return スロットは未実装 | expr {…} 内の && / \|\| チェーンの各オペランドが 1 条件としてカウントされるため、if {$a && $b} は 2 を報告します。より広い Phase 2B のスロット振り分けは未実装のため、裸の真偽値の if {$a} は依然として 0 を報告します。 |
| iRules | チェーンオペランドの単項条件は実装済み(同系の Tcl と異なります)。裸の真偽値 / 引数 / return スロットは未実装 | && / \|\| / and / or チェーンの各オペランドが 1 条件としてカウントされる(Rule 9)ため、if {!$a && !$b} は 2 を報告します。iRules は Tcl にはない語形式の文字列一致比較子(contains、starts_with、ends_with、equals、matches など)も認識します(Tcl の eq / ne / in / ni は共通です)。より広い Phase 2B のスロット振り分けは未実装のため、裸の真偽値の if {$a} は依然として 0 を報告します。 |
| All Phase 2 languages (Java, Groovy, C#, Rust, Go, JavaScript, TypeScript, TSX, Mozjs, PHP, C, C++, Objective-C, Mozcpp, Python, Perl, Lua) | if (true) {}、m(!a, !b)、return !x はそのオペランドをカウント | Phase 2B routes if / while / do-while / argument-list / return slots through the same walker, so the rule applies uniformly across decision-bearing positions. A bare return x continues to report zero — Fitzpatrick treats an identifier in a return slot as a value, not a unary conditional. |
| Ternary slots: Java, Groovy, C#, C, C++, Objective-C, Mozcpp, JavaScript, TypeScript, TSX, Mozjs, PHP, Perl | a ? !b : !c counts its condition and both branch operands | The same walker also runs over a ternary's three operand slots, so a ? !b : !c reports 4 (the ? plus three unary conditions) rather than 1. Languages with no ternary (Rust, Go, Kotlin, Lua, Elixir) are unaffected. Ruby, Python, Tcl and iRules do have one but do not yet route it (issue #1161). |
| Ruby | 裸の述語の if / unless / while / until(ブロック形式と修飾子形式)は 1 条件としてカウント | 慣用的な Ruby は裸の述語(if flag、x if flag)を好みます。条件スロットをカウントすることで、ABC の条件数が Ruby の循環的複雑度の判断数以上に保たれます(他の言語でも同じ整合が強制されています)。述語内の比較(if a == b)や && / \|\| チェーンは、それ自身の演算子 / ウォーカーのアームでカウントされ、二重カウントはされません。 |
| Bash | if / elif / while と、ワイルドカードでない各 case アームは 1 条件としてカウント | Bash の述語は コマンド であるため、埋め込まれたブール式ではなく、分岐キーワード自体が条件のシグナルです。それぞれが Bash の循環的複雑度の判断に対応します。裸の *) case アーム(default: に相当)は除外され、循環的複雑度の標準カウントに合わせています。 |
| Kotlin | catch に加えて try も 1 条件としてカウント | Fitzpatrick は両方のキーワードをカウントし、Java / C# / C++ / Groovy はすでに両方をカウントしています。Kotlin は以前は catch ブロックのみをカウントしていました。 |
実例
次の C 関数を考えます。
char digit_or_C(int am) {
char c;
if (am >= 0 && am <= 0xF) {
c = '/';
} else {
c = 'C';
}
return c;
}
関数本体を走査すると、次のようになります。
| トークン / 構文 | コンポーネント | 理由 |
|---|---|---|
am >= 0 | C += 1 | 比較(Rule 5、>=) |
am <= 0xF | C += 1 | 比較(Rule 5、<=) |
&& | — | 論理演算子 — 単独では寄与しません。 |
if/else | C += 1 | else キーワード(Rule 5) |
c = '/' | A += 1 | 代入(Rule 1) |
c = 'C' | A += 1 | 代入(Rule 1) |
合計: <A,B,C> = <2, 0, 3>、マグニチュードは √13 ≈ 3.61 です。
同じ本体を単項条件を使って書き換えると —
if (am_in_range || force_letter) {
c = 'C';
}
ウォーカーは am_in_range と force_letter をそれぞれ 1 回ずつカウントします(Rule 7 / 9 の単項条件)。|| 演算子自体の寄与はゼロです。これは論文にある Fitzpatrick の Rule 7 / 9 の実例 if (x || y) printf("test failure\n"); — 「x と y の両方が条件式として評価されるため、単項条件は 2 つある」— と一致します。
他の ABC ツールとの比較
本プロジェクトは && / || について Fitzpatrick の原論文に従います。演算子自体はカウントせず、比較でない各オペランドを単項条件として 1 回ずつカウントします。これは(and / or を直接カウントする)RuboCop の Metrics/AbcSize とは異なり、StepicOrg/abcmeter および eoinnoble/python-abc と一致します。ツール間で ABC の数値を比較するとき、同じソースに対する食い違いの最大の原因は、この演算子カウントの選択です。
アルゴリズム
実装は構文木のすべての葉ノードをちょうど 1 回ずつ走査します。各ノードについて、その言語の言語別 Abc トレイト実装に 3 つの yes/no の質問 — これは代入か?分岐か?条件か? — を尋ね、一致したカウンタをインクリメントします。4 つの主要値は次のとおりです。
- 3 つのコンポーネントそのもの(
assignments、branches、conditions)。 - マグニチュード
|<A,B,C>| = √(A² + B² + C²)。Fitzpatrick がベクトルを単一の数値へ要約する方法として推奨しているものです。
完全なシリアライズ出力(src/metrics/abc.rs)は、これら 4 つに加えて、value(CLI がしきい値判定に使うスペースごとのマグニチュードで、葉スペースでは magnitude と等しい値)、コンポーネントごとの平均(assignments_average、branches_average、conditions_average)、およびファイルスコープでのコンポーネントごとの *_min / *_max を出力し、合計 14 フィールドになります。このメトリクスは src/languages/language_*.rs で言語ごとに特殊化されています。
読み方
ABC は複雑度メトリクスではなく サイズ メトリクスです — 判断を持たない長く単調な関数でも、代入を多く行えば高いスコアになります。Fitzpatrick の当初の推奨は、マグニチュードを相対的な物差しとして使うことでした。すなわち、ファイル内の関数を ABC マグニチュードで順位付けし、上位 1 割に注目します。
実際には、ABC は Ruby コミュニティで最も広く採用されるようになりました。rubocop リンタ と flog ツール は、どちらもデフォルトでしきい値ベースの警告を出します。ABC マグニチュードがおよそ 17 を超える Ruby メソッドは慣例的にリファクタリング候補とされ、30 を超えると保守が難しいと見なされます。これらのしきい値は言語固有です — 明示的なゲッター / セッター代入をより多用する C++ や Java では、より高い値になると考えてください。
認知的複雑度
認知的複雑度は、SonarSource の G. Ann Campbell が 2017 年のホワイトペーパー Cognitive Complexity — A new way of measuring understandability と、その後の IEEE TechDebt 2018 論文 Cognitive Complexity — An Overview and Evaluation で導入しました。ホワイトペーパー自体は SonarSource のサイトで CognitiveComplexity.pdf として入手できます。
このメトリクスは、コード品質ツールにおける循環的複雑度の意図的な置き換えとして設計されました。Campbell の主張は、循環的複雑度が測るのはコードの テスト のしにくさであって、理解 のしにくさではない、というものです。1024 アームの switch 文は、同一のロジックを実行する深くネストした if の連鎖と同じスコアになりますが、人間の読み手にとってはネストしたコードの方がはるかに追いにくいのです。
アルゴリズム
認知的複雑度はゼロから始まり、木を走査しながら次の 3 つの規則を適用します。
- 「省略形」の制御フローは不問にします。 単一のブロックへ誘導するだけの構文 — ネストのないトップレベルの
if、自前の条件を持たないelse、forのヘッダ、?:三項演算子 — は、それぞれ基本の+1を加えますが、そのパターン自体へのペナルティはありません。 - 線形フローの中断にはペナルティを課します。 すべての
if、else if、else、switch、try/catch、ループ、ジャンプ(goto、break label、continue label)、再帰呼び出しは、少なくとも+1を加えます。 - ネストを罰します。 すでにネストされたブロックの 内側 に制御フローが現れるたびに、メトリクスは ネストの深さごとに 追加の
+1を加えます。メソッド内の外側のifの中のforの中のifは1 + 2 + 3 = 6になりますが、同じ 3 つの構文をフラットに並べた場合は1 + 1 + 1 = 3です。
同一のブール演算子の連なり(a && b && c)は、&& の連鎖は単一の && より読みにくいわけではないという理由で、連なり全体で +1 になります。演算子が切り替わる(a && b || c)ところで認知負荷が跳ね上がるため、2 つ目の演算子は独自の +1 を得ます。
big-code-analysis は、関数ごとの構造スコアに加えて、ファイル全体の sum、min、max と関数ごとの average を出力します。実装は src/metrics/cognitive.rs にあります。
読み方
認知的複雑度が 0 の関数は純粋に線形で、分岐もループもありません。SonarSource のツールはデフォルトで 15 を超える関数を「複雑すぎる」とフラグし、Campbell がホワイトペーパーで推奨しているのは、関数がおよそ 25 を超えることはめったにないようにすることです。循環的複雑度と異なり、このメトリクスは滑らかにスケールします。判断の数が同じでも、深くネストしたコードはフラットなコードより大幅に高いスコアになります。
後から定着したユースケースはコードレビュー時のリファクタリング指針です。このメトリクスはネストを特にペナルティ対象とするため、早期リターンや「メソッドの抽出」リファクタリングの恩恵を受けるような関数をまさに検出する傾向があります。SonarLint の IDE プラグイン(IntelliJ、VS Code、Visual Studio、Eclipse)はいずれも、ホバー時の代表的な複雑度としてこの値を表示し、このメトリクスはその後、Sonar エコシステム外のいくつかの言語サーバーやコードレビュープラットフォームにも採用されています。
言語別の逸脱
-
Elixir は再帰やジャンプ文をスコアしません。Elixir の制御フロー(
if/unless/cond/case/with)は、専用の文法プロダクションではなくマクロ形のCallノードから構築されており、言語にbreak/continue/gotoはありません。そのため実装は、識別できるネストを伴う構文のみをスコアし、これらの形を構文として公開する言語向けに SonarSource 仕様が追加する再帰(B3)と非構造化ジャンプ(B2)の加算を省略します。 -
For every language with a syntactic function-definition node, a nested function — a local function, or a method on a local / inner class — resets the nesting counter to zero at its boundary and adds a function-depth surcharge, so control flow inside it is scored against the nested function's own depth rather than the enclosing function's nesting. Byte-equivalent constructs therefore score identically across languages.
-
A lambda or closure (
x -> …,|x| …,lambda x: …, a Ruby block, an Objective-C block) is not a function boundary in that sense. It adds a surcharge on top of the enclosing nesting instead of replacing it, so a decision inside a lambda written inside anifis charged for both. -
Python charges a boolean operator an extra
+1for each enclosinglambda, on top of the+1the boolean sequence itself earns. No other language does this. Only the outermost operator inside a given lambda body pays the surcharge, and the walk that counts those lambdas stops at the nearest enclosingexpression_list. (It also stops atif,forandwhile, but a lambda body is a single expression, so no lambda ever sits above one of those statements and those three arms never change a count.) The sequence increments themselves still follow the operator-switch rule above, so a mixed chain inside one lambda pays two of them plus a single surcharge. Measured:Python source cognitive.sumg = a and b1 g = lambda y: y and a2 g = lambda y: y and a and b2 g = lambda y: y and a or b3 g = lambda x: (lambda y: y and a)3 k = lambda q: (yield a and b, c)1 def f(a): g = lambda x: x0 The last two rows are the boundaries. The parenthesised
yieldputs anexpression_listbetween the operator and thelambda, ending the walk before it reaches one, so only the fundamental+1is left; and a lambda containing no boolean operator costs nothing by itself.Campbell gives a boolean sequence a fundamental increment and no nesting increment, so this is an addition to the specification rather than an implementation of it. Issue #1150 reviewed the rule and kept it deliberately. Python's boolean-operator cost is not comparable with another language's score for the same code.
循環的複雑度(CC)
最初のソフトウェア複雑度メトリクスで、Thomas J. McCabe が 1976 年に A Complexity Measure(IEEE Transactions on Software Engineering, SE-2(4), 308–320 ページ)で導入しました。
McCabe のアイデアは、関数の 制御フローグラフ にグラフ理論を適用することでした。各基本ブロックをノード、ブロック間の各ジャンプをエッジとして描くと、そのグラフのサイクロマティック数は次のようになります。
M = E − N + 2P
ここで E はエッジ数、N はノード数、P は連結成分の数です。重要なのは、M が関数を通る線形独立パスの数、言い換えれば、すべての分岐を少なくとも 1 回カバーするのに必要な最小のテストケース数と正確に一致することです。
アルゴリズム
big-code-analysis は制御フローグラフを文字どおりに構築するわけではありません。代わりに、McCabe が 1976 年の論文で構造化プログラムについて証明した、等価ではるかに安価な定式化を使います。
循環的複雑度 = 1 + (判断点の数)
「判断点」とは、制御が分岐しうるあらゆるノードのことです。
if、else if、三項演算子?:switch/match/select内のcase/whenアームwhile、do … while、あらゆる種類のfor- 例外ハンドラの
catch節 - 短絡ブール演算子
&&と||
src/metrics/cyclomatic.rs にある言語別の Cyclomatic トレイトが、各 tree-sitter ノードに「これは判断か」を問い合わせてカウンタをインクリメントします。このメトリクスは関数ごととファイルごとにロールアップされます。メソッド本体をまたぐクラス単位の集約は、後述の WMC が別途提供します。
修正循環的複雑度
big-code-analysis は、単一の switch 文の中のすべての case / match / when アームを、アーム数に関係なく 1 つの判断点に折りたたむ修正(modified) 版も報告します。これは大きなディスパッチテーブルを少なめにカウントする傾向があり、厳密な McCabe の定義よりも開発者の直感に合うことがよくあります — 30 アームの enum ディスパッチは、30 ではなく 1 つの判断として読まれるからです。(この慣例自体は本プロジェクト独自のものではありません。Terry Yin の lizard ツールに古くからある -m モードを踏襲したもので、多くの読者はそこで初めて目にしているはずです。)両方の数値は並べて出力されます。どちらか一方を選び、一貫して使ってください。
Rust の ? 演算子のカウント
デフォルトでは、Rust の ? 演算子(文法ノード try_expression)は、標準と修正の両方の循環的複雑度に +1 を加えます。これはアップストリームの rust-code-analysis と同じ挙動で、? は早期リターンの分岐だからです。循環的複雑度を保守性の 「ゲート」 として使う場合、この挙動は、ハッピーパスにいくつかの ? を通しているだけの、線形だが失敗しうるコードに過剰なペナルティを与えることがあります。次の方法でオプトアウトし、? を線形なエラー伝播として扱えます。
- ライブラリ:
MetricsOptions::default().with_count_cyclomatic_try(false)。 - CLI:
--cyclomatic-count-try=false(または非推奨の--no-cyclomatic-tryエイリアス)、あるいはbca.tomlのcyclomatic_count_try = false(CLI の値はどちらの方向でもこのキーを上書きします)。 - リポジトリのゲート: 自動検出される
bca.tomlにcyclomatic_count_try = falseを設定します(本プロジェクト自身のmake self-scanはまさにこれを行っており、フラグも環境変数も使いません)。このポリシーの切り替えは循環的複雑度の値を変化させるため、同じ変更の中で.bca-baseline.tomlを再生成してください。
デフォルトは変わりません — ? は引き続きカウントされるため、公開済みのメトリクス値は保たれます。このトグルは Rust 専用です。他の言語は try_expression を出力しません。
読み方
McCabe の当初の推奨は、1976 年の論文で繰り返し述べられ、NIST の Structured Testing レポート(Special Publication 500-235、1996 年)にも引き継がれているもので、単一の関数の上限を 10 とすることです。これを超えると、分岐カバレッジに必要なテストケース数が不快なほど増大します。
循環的複雑度の定着した用途は次のとおりです。
- 欠陥予測。 複雑度は — 完全ではないものの — 関数がバグを含む 確率 とよく相関し、ほとんどの静的解析ツールは CC の高い関数をリスクありとフラグします。
- テストカバレッジ計画。 CC はすべての分岐を網羅するために必要なテストケース数の下限であるため、テストチームは工数の見積もりに直接利用します。
- リファクタリングのトリアージ。 循環的複雑度はほぼすべてのコード品質ダッシュボードで筆頭に挙げられる「複雑度」の数値であり、長さが同程度に見える 2 つの関数の優先順位を決めるタイブレーカーとしてもよく使われます。
このメトリクスのよく知られた盲点に注意してください。すべての判断を同じ重みとして扱う点です。enum に対する 30 分岐の switch と、それぞれがさらにネストした if を内包する 2 つのネストした if を持つ関数は、読む体験としてはまったく異なるにもかかわらず、どちらもおよそ 30 というスコアになります。認知的複雑度(前述)は、まさにこの点を解決するために設計されました。
Halstead
Halstead スイートは、このページで最も歴史の古いサイズと工数のメトリクスファミリーです。Maurice H. Halstead が 1977 年の著書 Elements of Software Science(Elsevier、ISBN 0-444-00205-7)で導入したもので、Wikipedia の Halstead complexity measures のページに公式がまとめられています。Halstead の構想は驚くほど野心的でした。物理学が物質を対象とする経験科学であるのと同じ意味で、定量的・経験的な ソフトウェアの科学 を打ち立てようとしたのです。
4 つの基本カウント
Halstead はプログラムをトークンに還元し、それらを 2 つのカテゴリに分割します。
- 演算子(operator) — 何かを 行う もの。キーワード(
if、return、while)、算術・論理演算子、代入、関数呼び出しの構文、制御フローを担う句読記号です。 - 被演算子(operand) — 何かで ある もの。識別子とリテラルです。
これらから 4 つの基本カウントを導出します。
| JSON キー | 記号 | 意味 |
|---|---|---|
unique_operators | n1 | 相異なる演算子の数 |
unique_operands | n2 | 相異なる被演算子の数 |
total_operators | N1 | 演算子の出現の合計数 |
total_operands | N2 | 被演算子の出現の合計数 |
シリアライズされた出力では説明的な JSON キー列の名前を使用します。後述の導出メトリクスの公式では、同じ 4 つのカウントに対して Halstead の古典的な n1/N1/n2/N2 表記を使用します。
big-code-analysis はこれら 4 つの数値を src/metrics/halstead.rs で関数ごと・ファイルごとに記録します。言語ごとのトレイトが、トークン単位で演算子か被演算子かを分類します。このルールは括弧や文の区切りのような純粋なレイアウト用句読記号を意図的に除外しており、そのため Halstead の合計は Tokens のカウントと 同じにはなりません。
導出メトリクス
Halstead はここから数多くの公式を導出します。big-code-analysis は標準的なものすべてに加えて、元のスイートに含まれるあまり一般的でない 3 つの導出値(estimated_program_length、purity_ratio、level)も報告します。
vocabulary n = n1 + n2
length N = N1 + N2
estimated_program_length N̂ = n1·log2(n1) + n2·log2(n2)
purity_ratio = N̂ / N
volume V = N · log2(n) (bits)
difficulty D = (n1 / 2) · (N2 / n2)
level L = 1 / D
effort E = D · V (elementary mental discriminations)
time T = E / 18 (seconds)
bugs B = E^(2/3) / 3000 (estimated delivered defects)
これらの数値定数は、FORTRAN、PL/I、Algol 系言語を含む CDC 時代の異種プログラム群に対する Halstead の経験的フィッティングに由来します。T = E / 18 の「Stroud 数」は別系統で、心理学に由来します。Halstead は、人間の頭脳が 1 秒あたり約 18 回の基本的な弁別を行うという John Stroud の推定を借用しました。
読み方
Halstead の 本来の 意図は、プログラムが書かれる前に 3 つのことを予測することでした。すなわち、ビット単位でどれだけの大きさになるか、実装にどれだけの時間がかかるか、デプロイ後にどれだけのバグが見込まれるか、です。ボリュームと長さの予測については経験的な裏付けがそれなりにありますが、時間とバグの予測には議論の余地があり、特に Purdue のテクニカルレポート Software Science Revisited で詳細に批判されています。
現代の実践では、Halstead の数値は 3 つの用途に使われます。
- 複合メトリクスへの入力として。最も重要なのは、Halstead の ボリューム に依存する保守容易性指数(次節)です。
- 言語に依存しないサイズの代理指標として。ビット単位のボリュームは、LOC では実現できない形で言語をまたいで滑らかにスケールします。
- 相対的な工数見積もりのため。2 つのリファクタリング候補の循環的複雑度が同程度の場合、Halstead の difficulty が高いほうがリグレッションを持ち込む可能性が高くなります。
コード行数
この節では 5 つの LOC バリアント — SLOC、PLOC、LLOC、CLOC、BLANK — を扱います。「行を数える」ことは、何を数えるのかを正確に定義しなければならなくなるまでは簡単に聞こえます。以下の 5 つのバリアントは事実上の標準的な分類で、その起源は Samuel Conte、Hubert Dunsmore、Vincent Shen による 1986 年の教科書 Software Engineering Metrics and Models(Benjamin/Cummings、ISBN 0-8053-2162-4)に遡ります。この本が物理行と論理行の区別を体系化しました。Wikipedia の source lines of code の項目は、この物理行対論理行の区別を読みやすくまとめています。
| バリアント | 数える対象 |
|---|---|
| SLOC | Source Lines Of Code — コメント・空行・コードを問わずファイル内のすべての行 |
| PLOC | Physical Lines Of Code — 空行でもコメントのみの行でもない行 |
| LLOC | Logical Lines Of Code — 文を含む行(定義、代入、宣言) |
| CLOC | Comment Lines Of Code — コメントを含む行(同じ行にコードがあってもなくても数える) |
| BLANK | 空行 — 空白文字のみの行 |
アルゴリズム
big-code-analysis は、tree-sitter の構文木を 1 回走査するだけで 5 つのカウントすべてを導出します(src/metrics/loc.rs を参照)。コメントと文字列は字句走査ではなく AST(抽象構文木)のノード型で識別されるため、複数行文字列、raw 文字列、doc コメント、文字列補間はすべて正しく処理されます。言語ごとの Loc トレイトが、LLOC で「文」として数えるノード種別を規定します。何が文として数えられるかは言語ごとに定義されるため、ここが微妙な部分です。
5 つのカウントは、いくつかの有用な恒等式を満たします。
SLOC = PLOC + BLANK + (lines that are comment-only)
CLOC ≥ (lines that are comment-only) # CLOC also counts mixed code+comment lines
読み方
- SLOC は、多くの人が口語的に「コード行数」と言うときに指すものです。標準的なサイズの代理指標ですが、フォーマットの影響を受けやすく、言語の慣習をまたいだ移植性はありません。
- PLOC は視覚的なノイズを取り除いたものです。後述の保守容易性指数の公式の内部で使われるサイズ指標です。
- LLOC は最も信頼できる 文 の数です。文ごとのテストケースを見積もる場合や、Python ファイルと Java ファイルの密度を比較する場合に適した指標です。
- CLOC は PLOC と組み合わせることで コメント密度 が得られます。
CLOC / PLOCは、ファイルのどれだけがドキュメントでどれだけが実装かを示す、大まかながら有用な代理指標です。 - BLANK は主に診断用です。BLANK の割合が非常に低いファイルは、読みにくいことが多いためです。
LOC バリアントの発展的な用途は、単なるサイズの測定を大きく超えています。コスト見積もりモデルへの最も一般的な入力であり(COCOMO も COCOMO II も KSLOC — ソース行の千行単位 — を基本単位としています)、製品ポートフォリオのダッシュボードにおける工数予測にも使われ、さらに他のほぼすべてのメトリクスを正規化する分母として使われます。KSLOC あたりの欠陥数、KSLOC あたりのチャーン、KSLOC あたりのテストケース数 などです。弱点 — LOC は容易に操作でき、コーディングスタイルの 10 倍の違いが LOC の 2 倍の違いを生み得ること — こそが、この章にこれほど多くの他のメトリクスがある理由です。
保守容易性指数(MI)
保守容易性指数は、前述のメトリクスのいくつかを 1 つの 0〜100 程度の数値にまとめ上げ、「このコードはどれだけ保守しやすいか」として読むことを意図した複合メトリクスです。Paul Oman と Jack Hagemeister が 1992 年の ICSM 論文 Metrics for assessing a software system's maintainability で提案し、Don Coleman、Dan Ash、Bruce Lowther、Paul Oman が 1994 年の IEEE Computer 論文 Using metrics to evaluate software system maintainability(IEEE Computer 27(8)、44-49 ページ)で改良しました。彼らの方法論は経験的なものでした。Hewlett-Packard の少数の本番システムについて専門家による保守性評価を収集し、それぞれに 40 個の候補メトリクスを計算し、回帰分析に最良の線形結合を選ばせたのです。生き残った組み合わせは、Halstead ボリューム、循環的複雑度、コード行数、コメント密度を使うものでした。
big-code-analysis は、実践で定着した 3 つの公式を報告します。
3 つの値は mi オブジェクトの下に original、sei、visual_studio というキーでネストされます(ドット区切りのしきい値名は mi.original、mi.sei、mi.visual_studio)。
mi.original = 171 − 5.2·ln(HV) − 0.23·CC − 16.2·ln(SLOC)
mi.sei = 171 − 5.2·log2(HV) − 0.23·CC − 16.2·log2(SLOC) + 50·sin(√(2.4·comment_percentage))
mi.visual_studio = max(0, mi.original · 100 / 171)
mi.originalは Coleman–Oman の公式です。病的なファイルでは負になることがあります。mi.seiは Software Engineering Institute による改良版で、コメント密度の項を追加しています。sin(√(...))という形は、コメントが ある程度 は役立つものの、一定量を超えて増やしても効果がないように選ばれました。comment_percentageは[0, 100]のパーセントで表したコメント行の割合であり([0, 1]の比率ではありません)、コードはこのパーセント値をそのまま SEI 項に与えます(src/metrics/mi.rsと issue #241 を参照)。mi.visual_studioは Microsoft が Visual Studio 用に選んだ線形リスケーリングで、スコアは[0, 100]にクランプされ、信号機方式で開発者に表示されます。緑は 20 以上、黄は 10 以上、赤はそれ未満です。
このメトリクスの歴史的背景と鋭い批判は、Arie van Deursen のブログ記事 Think Twice Before Using the Maintainability Index にまとめられています。
アルゴリズム
実装は純粋な算術です。src/metrics/mi.rs は計算済みの Halstead、Cyclomatic、LOC メトリクスを受け取り、3 つの公式を適用します。公式が Halstead ボリュームと SLOC の自然対数を使うため、MI は空のファイルに対しては未定義です。big-code-analysis は、SLOC がゼロまたは Halstead ボリュームがゼロのファイルに対して 0.0 を返します。
読み方
MI は 本来 ポートフォリオレベルのスコアとして設計されました。「このコードベースから今後 1 年でどれだけの保守の苦労が見込まれるか」というものです。健全なシステムではリリースをまたいでかなり安定しており、システムが「レガシー」の象限に入る前に目に見えて低下する傾向があります。
発展的なユースケースは Visual Studio の信号機表示です。IDE でメソッドにカーソルを合わせたことのある C# 開発者は皆、緑・黄・赤のアイコンを見たことがあり、その背後にある数値が mi.visual_studio です。これにより MI は .NET 開発者の一世代にとって群を抜いて最もユーザーの目に触れるソフトウェアメトリクスとなり、それこそが最も多くの批判を集めたメトリクスである理由でもあります。サーモスタットではなく煙探知機として扱ってください。急激な低下は有用なシグナルですが、絶対値はノイズが多いのです。
NArgs
NArgs は、関数・メソッド・クロージャが宣言する引数の数を数えます。このメトリクスに有名な起源論文はありません。少なくとも Kernighan と Plauger の The Elements of Programming Style(1974 年)に遡る昔ながらの知恵であり、Robert C. Martin の Clean Code(2008 年)で目立つ形で再度述べられました。同書は 3 引数を緩やかな上限として提案しています。
big-code-analysis は、呼び出し可能要素の種類ごとにカウントを分割します。すべての集計値が 「関数」 と クロージャ で別々に報告されるため、|…| … クロージャの多い Rust ファイルと、メソッドしかない Java ファイルが比較可能な数値を生成します。シリアライズされた出力(src/metrics/nargs.rs)は function_args、closure_args、function_args_average、closure_args_average、total、average、function_args_min、function_args_max、closure_args_min、closure_args_max です。実装は、デフォルト引数、可変長引数、キーワード専用引数、分割代入されたパラメータを言語ごとに一貫して処理します。
Languages where it reads 0
A metric that silently reports 0 reads as "no offenders" rather than "not measured", so it is worth knowing where the count is inert:
- Bash — correct and permanent. The shell has no formal parameter list; arguments arrive as
$1,$2, and so on. It is the only language where every function reads 0. - Perl subs without a signature — correct. Signatures (
sub add($x, $y)) are counted; a sub that reads its arguments from@_declares no formal parameters to count. - Perl anonymous subs — a gap, and an upstream one: the grammar parses an anonymous sub's signature inside an error node, so
my $f = sub ($x) {…}reads 0 even though it has a signature.
A nargs limit is therefore inert on a Bash codebase and sparse on Perl written before use v5.36. Gate it per language rather than repository-wide; see Choosing thresholds.
読み方
引数の多い関数は正しく呼び出すのが難しく、網羅的にテストするのはさらに困難です。テストマトリクスはおおよそ指数関数的に増大します。古典的なリファクタリングの助言は パラメータオブジェクトの導入 パターンです。関数が 4 つを超える関連した引数を取る場合、それらをレコード / 構造体 / データクラスにまとめます。
発展的な用途は、レビューをブロックする lint ルールとしてです。現代的なリンターの多く(pylint の R0913、ESLint の max-params、Checkstyle の ParameterNumber)は、設定可能なしきい値を超える引数を持つ関数にフラグを立てます。NArgs は API 設計ダッシュボードの構成要素としても有用です。平均 NArgs が時間とともにじわじわ増えている公開 API は、「あともう 1 つだけパラメータを」という機能フラグを積み重ねてきた API であることが多いのです。
NExits
NExits は、関数からの相異なる出口点の数を数えます。すべての明示的な return、すべての throw / raise、そして(Rust では)すべての ? による早期リターンです。関数末尾の暗黙的なフォールスルーのリターンは数えません。明示的な出口のみが対象です(issue #243 を参照)。
このメトリクスは 1970 年代の構造化プログラミングの文献に遡ります。Edsger Dijkstra らは、関数は単一の入口と単一の出口を持つべきだと論じました(「SESE」ルール)。現代の考え方ははるかにニュアンスに富んでいます。Steve McConnell の Code Complete 第 2 版(Microsoft Press、2004 年)を参照してください。同書は、ネストを減らせる場合の 早期リターン を、明確さを高めるパターンとして明示的に推奨しています。
big-code-analysis は各関数の構文木を走査し、言語固有の出口ノードを特定し(src/metrics/nexits.rs の言語ごとの Exit トレイトを参照)、関数ごとのカウントに加えてファイルレベルの sum、average、min、max を報告します。シリアライズされたフィールド名は nexits で、本文で使っている略語表記と一致します。
読み方
厳格な SESE コーディング標準(アビオニクス向けの DO-178C、組み込み自動車向けの MISRA C — MISRA の公式サイト を参照)は、今でも関数あたりの NExits を 1 とすることを要求しています。複数の出口点は、認証対象の制御フロー解析を複雑にするためです。これらの領域の外では、NExits が 2-4 であることはたいてい 良い 兆候です。ほとんどの場合、その関数がガード節で事前条件を処理し、その後フラットな本体で処理を進めていることを意味するからです。
非常に 高い NExits — たとえば 8 超 — は警告のサインです。たいていの場合、その関数は複数の小さな関数に分割されるべきであり、それぞれの「成功する分岐」が独自のヘルパーになるべきだったことを意味します。
NOM
NOM は Number Of Methods(メソッド数)の略で、あるスコープ(ファイル、クラス、または名前空間)内で定義されたすべての関数・メソッド・クロージャを数えます。オブジェクト指向コードベースにとっては、Mark Lorenz と Jeff Kidd が 1994 年の著書 Object-Oriented Software Metrics(Prentice Hall、ISBN 0-13-179292-X)で最初期に導入したメトリクスの 1 つであり、同書では主要なクラスサイズ指標として扱われています。
big-code-analysis は src/metrics/nom.rs で、呼び出し可能要素の種類ごとに分割してカウントを報告します。シリアライズされるフィールドは functions、closures、functions_average、closures_average、total、average(内包するスペース全体での平均)、および種類ごとの functions_min、functions_max、closures_min、closures_max です。
この分割により、同じコードに対して異なる問いを立てられます。クロージャが多く関数が少ない Rust クレートはイテレータ中心のコードに典型的であり、関数が多くクロージャが少ない Python モジュールはスクリプト風のコードに典型的です。
読み方
NOM は他のいくつかのメトリクスへの入力です。WMC は NOM が数えるのと同じメソッド集合に対して 循環的 複雑度を合計し、NPM は同じ集合を公開メソッドだけに絞り込みます。単独のメトリクスとしては、Lorenz–Kidd の推奨はクラスあたり ≤ 20 メソッドです。発展的な用途は God クラス検出器 としてです。NOM が数十に達するクラスはほぼ確実に多くを抱え込みすぎており、Martin Fowler の Refactoring カタログの Large Class の項目 に記述されている「コラボレーターの抽出」リファクタリングの有力候補です。
NPA
NPA は、クラスまたはインターフェイスが宣言する公開属性の数(フィールド、プロパティ、インスタンス変数とも呼ばれます)を数えます。Lorenz と Kidd が Object-Oriented Software Metrics(1994 年)で導入したメトリクスファミリーの一部であり、後に Brito e Abreu と Carapuça(1994 年) が提案した MOOD(「Metrics for Object-Oriented Design」)スイートに取り込まれました。
big-code-analysis は、定義箇所の種類ごとにカウントを分割します。クラス(状態を持つ具象型)と インターフェイス(抽象的な契約)です。シリアライズされた出力(src/metrics/npa.rs)は class_npa_sum(全クラスにわたる NPA の合計)、interface_npa_sum(インターフェイスにわたる合計)、class_attributes(公開か否かを問わない すべての 属性のクラスにわたる合計)、interface_attributes、class_cda(クラスの公開属性密度 — 平均ではなくアクセシビリティの 比率)、interface_cda、total、total_attributes、cda です。言語ごとの Npa トレイトが、何を「公開」と見なすか(Java の public、C# の public、Rust の pub、Python の「先頭にアンダースコアを付けない」慣習など)、そして何を「メソッド」ではなく「属性」と見なすかを決定します。
読み方
NPA はカプセル化の 直接的な 尺度です。すべての公開属性は、呼び出し側がメソッドを介さずに読み書きできる内部状態であり、つまりクラスが呼び出し側を壊さずには検証も進化もさせられない内部状態だということです。標準的な指針 — Bertrand Meyer の Object-Oriented Software Construction(Prentice Hall、1988 年)で最初に明示的に述べられ、* 統一アクセスの原則_ として知られています — は、NPA をゼロまたはその近くに保ち、代わりに公開メソッドを通じて状態を公開することです。
発展的な用途は API 安定性の監査です。時間とともに NPA が増えていく公開ライブラリのクラスは、公開メソッドの表面よりも速いペースで破壊的変更の負債を蓄積します。
NPM
NPM は、クラスまたはインターフェイスが宣言する公開メソッドの数を数えます。NPA のメソッド側の対になるもので、こちらも Lorenz と Kidd(1994 年)によって体系化されました。
NPA と同様に、big-code-analysis は NPM を定義箇所の種類(クラス対インターフェイス)で分割します。シリアライズされた出力(src/metrics/npm.rs)は class_npm_sum(クラスにわたる NPM の合計)、interface_npm_sum、class_methods(公開か否かを問わない すべての メソッドのクラスにわたる合計)、interface_methods、class_coa、interface_coa(平均ではなく操作アクセシビリティの 比率)、total、total_methods、coa です。言語固有の Npm トレイトが、何を公開と見なすかを決定し — たとえば Rust の pub、Python の先頭アンダースコアの慣習、C++ の public: セクション — 通常のメソッド、コンストラクタ、演算子オーバーロードを言語に応じて適切にまとめて扱います。
NPM は、Mark Hitz と Behzad Montazeri の Class Interface Size メトリクス や、Chidamber と Kemerer の Response For a Class(RFC)への入力の 1 つでもあります。
読み方
NPM は公開インターフェイスのサイズです。NPM が数十に達するクラスは、API の契約が大きすぎるクラスです。すべての公開メソッドは呼び出し側が依存し得るものであり、それに対するあらゆる変更は破壊的変更になります。Lorenz–Kidd の指針はクラスあたり公開メソッド ≤ 20 で、40 を超えるものは強力なリファクタリング候補と見なされます。同じルールは Java や C# の インターフェイス に特に強く当てはまります。そこでは契約こそが、クライアントが依存の拠り所とする形そのものだからです。
発展的な用途は、ライブラリ向けの公開 API 変更トラッカーとしてです。パッケージレベルで NPM を監視すると、NPA が内部フィールドの偶発的な露出を捉えるのと同じように、ライブラリの表面積の偶発的な拡大を捉えられます。
Tokens
Tokens は、tree-sitter のリーフトークン — 識別子、リテラル、キーワード、句読記号 — を関数ごと・ファイルごとに数えるカウントで、AST 上の祖先にコメントノードを持つトークンは除外されます。フォーマットの影響を受けにくい LOC の代替として意図された、現代的なレクサー駆動のサイズ代理指標です。(同じアイデアは Terry Yin の lizard コマンドラインツールでよく知られており、多くの読者がトークンカウントメトリクスを最初に目にするのはそこでしょう。)
実装は src/metrics/tokens.rs にあります。Tokens は Halstead が意図的に飛ばす句読記号を含む すべての リーフを数えるため、値は Halstead の N1 + N2 とは一致しません。また、行ではなくトークンを数えるため、どの LOC バリアントとも等価では*「ありません」*。空白のみの再フォーマットでは Tokens は変化しません。変数のリネームでもカウントは変化しません。コメントの削除でも Tokens は変化しません。トークンそのもの を変える編集 — if の追加、単一文ブロックへの省略可能な波括弧の追加、セミコロンが省略可能な言語でのセミコロンの挿入・削除 — はカウントを変化させます。
読み方
Tokens はこのスイートで最もフォーマットの影響を受けにくいサイズ代理指標です。言語をまたいで、あるいはスタイル規約の異なるチームをまたいで別のメトリクスを正規化する際に使うべきサイズ指標です。bugs per KSLOC はフォーマットの影響を受けますが、bugs per 1000 tokens ははるかに受けにくいのです。
発展的な用途は、言語横断的な研究における欠陥密度の分母の定番としてです。1000 行の Java ファイルと 1000 行の Lisp ファイルに含まれるコード量は大きく異なりますが、それぞれの 1000 トークン のスライスにはおおよそ同量の情報が含まれます。このため Tokens は、多くの言語にまたがって学習する機械学習ベースのコード品質モデルに特に有用です。
WMC
WMC — Weighted Methods per Class(クラスあたりの重み付きメソッド数)— は、Chidamber と Kemerer のスイート の最初のメトリクスで、1994 年の IEEE Transactions on Software Engineering 論文 A Metrics Suite for Object Oriented Design(20 巻 6 号、476-493 ページ)で導入されました。CK スイート — WMC、DIT、NOC、CBO、RFC、LCOM — は、学術文献で最も引用されている OO メトリクス集です。big-code-analysis は現在 WMC と、より単純なサイズメトリクス(NOM、NPA、NPM)を実装しており、継承ベース・結合ベースのものは今後の課題として追跡されています。
WMC はクラス内で定義されたすべてのメソッドの循環的複雑度の合計です。原論文は「重み付け」を意図的に抽象的なままにしました。Chidamber と Kemerer は「すべてのメソッドの複雑度を 1 と見なすなら、WMC = n、すなわちメソッド数になる」と書いています。しかし、その後の経験的な追試文献はほぼ例外なく循環的複雑度を重みとして採用しており、big-code-analysis もそれを使用します。
アルゴリズム
言語ごとのパーサーが見つけた各クラスまたはインターフェイスについて、big-code-analysis はその内部のすべてのメソッド本体の標準的な循環的複雑度を合計します(src/metrics/wmc.rs)。ファイルレベルのシリアライズされた出力は 3 つのフィールドです。class_wmc_sum(ファイル内の全クラスにわたる WMC の合計)、interface_wmc_sum(インターフェイスにわたる合計)、total(両者の合計)です。ファイルスコープでは min/max/average の集計は出力されません。個々のクラスを WMC でランク付けするには、Type hotspots (top N by WMC) セクションを表示する report サブコマンドを使ってください(Commands → Report を参照)。
読み方
Chidamber と Kemerer は WMC について 3 つの仮説を提示し、そのすべてがその後繰り返し検証されてきました。
- WMC が高いほど保守工数の増加を予測します。 個々のメソッドが複雑なクラスは、理解しにくくなります。
- WMC が高いほど再利用が減ります。 多くの複雑なことを行うクラスは、新しいコンテキストに投入しにくいのです。
- WMC が高いほどアプリケーション固有の振る舞いが広いことを示唆します。 そのようなクラスは、再利用可能な部品ではなく「メインループ」型のコーディネーターになりがちです。
発展的な用途は God クラス検出です。NOM と組み合わせると、WMC はクラスを分割すべきだという最も明確なシグナルの 1 つになります。NOM が高く WMC が低いクラスは受動的なデータホルダーです(おそらく問題ありません)。NOM が低く WMC が高いクラスは、少数の巨大なメソッドを持っています(クラスではなくメソッドを分割してください)。NOM と WMC の 両方 が高いクラスが、古典的な God クラスです。
次のステップ
- 対応言語 の章では、対応しているすべての言語と文法を一覧しています。一部のメトリクス定義(
NPA、NPM、WMC)はクラスを持つ言語でしか意味をなさないため、メトリクスの対応範囲は言語によって異なります。 - 対応する変更履歴(VCS)メトリクス の章では、ソースの AST ではなくバージョン管理履歴から導出される補完的なファミリー — コミット頻度、チャーン、所有権、複合的なリスクスコアとホットスポットスコア — を扱います。
- Commands → Metrics のページでは、これらの数値の JSON / YAML / TOML / CBOR 出力を生成するための
bca metricsの呼び出し方を説明しています。 - レシピ の章では、これらのメトリクスから品質レポートを生成するエンドツーエンドの例を示します。ダッシュボードへのパイプライン連携も含みます。
対応する変更履歴(VCS)メトリクス
この章は、変更履歴メトリクス — big-code-analysis がソースの AST ではなくバージョン管理履歴から導出するシグナル — のガイドツアーです。対応するコードメトリクス の章が現在あるがままのコードの 形 を測定するのに対し、これらのメトリクスはそのコードが どうやってそこに至ったか を測定します。どれくらいの頻度で、どれだけの量が、何人によって、どのような種類のコミットで変更されるのか、です。各節は、そのシグナルを最初に欠陥と結び付けた経験的な論文から始まり、big-code-analysis がそれをどのように計算するかをたどり、実践でその数値をどう読むべきかを説明します。
このファミリー全体は bca vcs コマンドで計算され、bca metrics --vcs の出力に付加されます。Commands → Change-history (VCS) metrics のページが、フラグ、ウィンドウ、出力形式、キャッシュを説明しています。この章が 「なぜ」 を、あちらのページが どうやって を担います。
始める前に、いくつかの前提となる注意点を挙げます:
- これらのメトリクスは欠陥が集まりやすい場所を予測するものであり、コードが正しいかどうかを予測するものではありません。 欠陥・脆弱性予測の文献は一貫して、プロセス シグナル — ファイルが時間とともにどう編集されてきたか — が、サイズや複雑度のような プロダクト シグナルを予測精度で上回ることを見いだしています。Graves らは 2000 年に率直にこう述べました。モジュールの変更回数は、その長さよりも優れた欠陥数の予測因子である、と。これらの数値はどれも正しさを測定しません。バグが潜んでいる確率でファイルをランク付けするのです。
- すべては 2 つのウィンドウで測定されます。 呼び出しごとの 1 回の履歴走査で、長期ウィンドウ(デフォルト
12mo≈ 365 日)と直近ウィンドウ(デフォルト90d)の両方についてすべてのシグナルが生成されます。直近性はジャストインタイム欠陥予測の系譜における最も強い単独シグナルであるため、全体を通じて直近の活動は古い活動より重く重み付けされます。 - 複合スコアは順序尺度であり、基数尺度ではありません。
risk_score、hotspot_score、コミットレベルの JIT スコアは温度計ではなく物差しです。意味を持つのは相対的な順位だけです。リポジトリのファイル(またはコミット)を互いに対して、あるいはそのリポジトリ自身の時間的な分布に対してランク付けしてください。絶対値を確率として読んだり、無関係なプロジェクト間で生のスコアを比較したりしないでください。 - レコードが存在しないことはゼロではありません。 追跡されていないファイルにはレコードがまったく存在せず、これはウィンドウ内の活動がゼロの追跡対象ファイルとは区別されます。計算結果としての
0.0(たとえば、常に単独でしか変更されなかったファイルの共変更エントロピーはゼロです)は実際の測定値です。
一覧
| メトリクス | 測定対象 | 最初に欠陥と結び付けた研究 |
|---|---|---|
| コミット頻度 | ファイルがどれくらいの頻度で変更されるか | Graves et al.、2000 年 |
| コードチャーン | 何行が変更されるか | Nagappan & Ball、2005 年 |
| バースト | 直近ウィンドウへの変更の集中度 | 直近性の原則(Graves、JIT の系譜) |
| 作者と所有権 | 何人の手がファイルに触れ、それがどれだけ集中しているか | Bird et al.、2011 年;Meneely & Williams、2009 年 |
| 修正・セキュリティ・リバートコミット | 是正的な変更の履歴 | Śliwerski, Zimmermann & Zeller、2005 年 |
| 経過時間と最終更新 | ファイルがいつ生まれ、いつ最後に触れられたか | ジャストインタイム欠陥予測の系譜 |
| 変更エントロピー | ファイルに触れるコミットがどれだけ 「分散」 しているか | Hassan, 2009 |
| 共変更エントロピー | ファイルの変更が波及する影響範囲がどれだけ広いか | arXiv 2504.18511, 2025 |
| 複合リスクスコア | 上記すべてのシグナルの重み付き集約 | 本プロジェクト(式 v2) |
| ホットスポットスコア | 複雑度 × 直近のチャーン | Tornhill, 2015 |
| バスファクター | ファイル集合全体での知識の集中度 | Avelino et al., 2016 |
| ジャストインタイムコミットスコア | 単一コミットの欠陥誘発リスク | Kamei et al., 2013 |
コミット頻度
最も単純な変更履歴シグナルは、ファイルにいくつの個別コミットが触れたかです。big-code-analysis はこれをウィンドウごとに commits_long と commits_recent として記録します。
このメトリクスの欠陥予測指標としての評価は、Todd Graves、Alan Karr、J. S. Marron、Harvey Siy による 2000 年の IEEE TSE 論文 Predicting Fault Incidence Using Software Change History に由来します。大規模な電話交換システムを研究した彼らは、変更履歴から導かれるプロセス指標が、コードそのもののどのプロダクトメトリクスよりも欠陥率をよく予測すること、なかでも過去の変更回数が最も強力な指標のひとつであることを見出しました。直観は単純です。誰も触れないファイルは、誰も壊していないファイルだということです。
アルゴリズム
1 回の履歴ウォークで、解析対象の ref から到達可能な各コミット(デフォルトでは first-parent のみ、--full-history で完全な DAG)を訪問し、そのコミットが変更する各ファイルに帰属させます。マージコミットは --include-merges を渡さない限り除外され、リネームはデフォルトで追跡されるため、ファイルの履歴は移動後も維持されます。ウォークが数えるのはファイルごと・ウィンドウごとの 個別コミット数 であり、diff やハンクの数ではありません。そのため、ファイルに 1 か所だけ触れるコミットも 3 つのハンクで触れるコミットも、どちらも 1 とカウントされます。
読み方
コミット頻度はレートであるため、ベースラインとの比較においてのみ意味を持ちます。すなわち、このファイルと兄弟ファイルの比較、あるいはこのファイルの今四半期と前四半期の比較です。一貫して高いカウントは、そのファイルが活発な機能開発の対象であるか、構造的に不安定であるかのどちらかを示します。コードチャーンと組み合わせれば両者を見分けられます。少数のコミットでの大量のチャーンと、多数のコミットでの少量のチャーンとでは事情が異なるからです。
コードチャーン
コードチャーンは変更の量です。ファイルに触れた追加行数と削除行数の合計で、ウィンドウごとに churn_long と churn_recent として記録されます。
チャーンの欠陥予測としての系譜は、Nachiappan Nagappan と Thomas Ball による 2005 年の ICSE 論文 Use of Relative Code Churn Measures to Predict System Defect Density にあります。Windows Server 2003 で検証された彼らの主要な発見は、絶対 チャーン単独では予測力が乏しい一方、ファイルサイズや変更の時間的な広がりに対する 相対 チャーンは欠陥密度を高い精度で予測する、というものです。big-code-analysis はウィンドウごとの生のチャーンをシグナルとして保持し、単一の絶対値を判定として報告するのではなく、複合スコアがそれをサイズや新しさと組み合わせられるようにしています。
アルゴリズム
ウィンドウ内でファイルに触れる各コミットについて、ウォークはその diff の追加行数と削除行数の合計を、当該ウィンドウにおけるそのファイルのチャーン合計に加算します。追加行と削除行を合算するため、1 行の編集は 2(削除 1、追加 1)とカウントされます。チャーンが測るのは 活動量 であって、正味の増加量ではありません。
読み方
チャーンは密度比の自然な分子です。コミットあたりのチャーンは、着実な編集と少数の大規模な書き換えを区別します。ファイルサイズに対するチャーンは Nagappan と Ball の相対指標を再現します。直近のチャーンが長期ウィンドウのチャーンに比べて高いファイルは、履歴から予測されるよりも速く変化しています。これがまさに後述のバーストシグナルです。
バースト
バーストは、ファイルの活動のうち直近ウィンドウに集中している割合で、commits_recent / commits_long を [0, 1] の比率として報告します。1 に近い値はファイルのコミットのほぼすべてが直近のものであることを、0 に近い値はファイルが昔は活発だったがその後静かになったことを意味します。
このシグナルは、変更履歴の研究文献を貫く新近性(recency)の原則から直接導かれます。Graves et al. は、古い変更よりも最近の変更の方が欠陥発生に強く影響することを見出しており、ジャストインタイム欠陥予測の系譜(JIT スコアを参照)も同じ観察の上に築かれています。変更履歴が 現在に前倒しで集中している ファイルは活発な変動のさなかにあり、活発な変動こそ欠陥が入り込む局面です。
読み方
バーストは単独の警報ではなく、判定の決め手(タイブレーカー)です。総履歴の少ないファイルでの高いバーストは、若く動きの速いファイルを意味します。深い履歴を持つファイルでの高いバーストは、突然目を覚ました古いファイルであり、多くの場合こちらの方が興味深いケースです。経過日数と合わせて読むことで両者を見分けられます。
作者数と所有権
関連する 2 つのシグナルが、ファイルを 誰が 変更しているかを表します。作者数(authors_long、authors_recent)は、各ウィンドウでそのファイルに触れた個別の人数を数えます。所有権(ownership_top_share、[0, 1] の比率)は、最も活発な単一の作者に帰属する編集の割合です。低いシェアは所有権が分散していることを、高いシェアは一人が支配的であることを意味します。
どちらも所有権とセキュリティに関する実証研究に遡ります。Christian Bird らによる 2011 年の FSE 論文 Don't Touch My Code! Examining the Effects of Ownership on Software Quality は、Windows Vista と 7 を対象に、分散した所有権(専門性の低い多数の貢献者と低いトップオーナーシェア)がリリース前の欠陥とリリース後の障害の両方を予測することを見出しました。セキュリティ面では、Andrew Meneely と Laurie Williams による 2009 年の CCS 論文 Secure Open Source Collaboration: An Empirical Study of Linus' Law が、Red Hat Enterprise Linux 4 において 9 人以上の開発者が触れたファイルは脆弱性を抱える可能性がおよそ 16 倍高いと報告しています。
アルゴリズム
作者の同一性はリポジトリの .mailmap を通じて正規化され、小文字化したメールアドレスでカウントされるため、2 つのアドレスでコミットする貢献者は 1 人として数えられます。Co-authored-by: トレーラーは参加者を追加します。ボットの ID(dependabot[bot]、renovate[bot]、github-actions[bot] など)はデフォルトで除外され、自動化によるチャーンがカウントを膨らませないようになっています。ownership_top_share は、ウィンドウ内でのそのファイルの総編集数に対するトップ作者の編集数の割合です。
読み方
長寿命のファイルで作者数が増えていくのは知識拡散のシグナルです。より多くの人がそのファイルを理解する義務を負うようになったことを意味します。複合リスクスコアはこれを 2 つの形で織り込みます。作者ファクターを所有権の 希薄化 項 (1 - ownership_top_share) でスケーリングするため、所有権が薄く広がっているときほど同じ人数がより大きく効きます。さらに、Meneely と Williams の RHEL4 しきい値を符号化した、開発者 6 人・9 人の節目でのカテゴリカルな加点を行います。1 つのファイル内ではなくファイルの 集合 をまたぐ知識の集中については、バスファクターを参照してください。
修正・セキュリティ・リバートコミット
big-code-analysis は、ファイルに触れる長期ウィンドウ内の各コミットをメッセージの意図で分類し、3 つのカウントを保持します。bug_fix_commits(バグ修正キーワードに一致するメッセージ)、security_fix_commits(CVE-####、security、vuln、exploit、sanitize などに一致するメッセージ)、revert_commits(リバートまたはロールバックである件名)です。
コミットの目的をメッセージから読み取る手法は、Jacek Śliwerski、Thomas Zimmermann、Andreas Zeller による 2005 年の MSR 論文 When Do Changes Induce Fixes? で導入されたもので、現在では SZZ という略称で広く知られています。前提となるのは、ファイルの 是正的 変更の履歴それ自体が予測力を持つということです。多くの修正を必要としてきたファイルは、さらに修正を必要とする可能性が高いファイルです。セキュリティ修正は通常のバグ修正より鋭いシグナルであり、リバートは取り消さざるを得なかった変更、つまりそこで何かがうまくいかなかったという局所的な自認を刻印します。
アルゴリズム
分類はコミットの件名と本文に対するキーワードベースです(src/vcs/classify.rs を参照)。意図的に単純で、言語にもトラッカーにも依存しません。課題トラッカーとの連携も、修正誘発コミットへの blame の遡及もないため、これは SZZ のうち軽量なメッセージ分類の半分であって、完全なアルゴリズムではありません。カウントは長期ウィンドウについてのみ保持されます。
読み方
bug_fix_commits の高いカウントは、修理を必要とし続けているファイルを意味します。複合スコアは 3 つのカウントすべてを、セキュリティ修正を 2 倍に重み付けした単一の対数スケール項に通します。そのため、セキュリティ修正の履歴を持つファイルは、それ以外が同一で通常のバグ修正しかないファイルより上位にランクされます。分類器はメッセージを読むため、その精度はプロジェクトのコミット規律に依存します。簡素なメッセージやテンプレート化されたメッセージのリポジトリでは過小カウントになります。
経過日数と最終更新
2 つのタイミングシグナルがファイルの履歴を区切ります。age_days はファイルの 最初の ウィンドウ内コミットからの経過日数(長期ウィンドウを上限とする)、last_modified_days は 最新の ウィンドウ内コミットからの経過日数です。
これらは、このファミリーの他のシグナルが依拠する新近性の推論を支えます。小さい age_days は 新しい ファイルを示し、新しく追加されたコードは高いリスクを伴います。これはジャストインタイム系の研究と、新たに追加された機能が不釣り合いに欠陥を出しやすかったという Chromium 自身の欠陥分析から得られた観察です。小さい last_modified_days は最近編集されたファイルを示しますが、この性質はウィンドウがすでに重み付けしています。
読み方
age_days の最大の用途は、リスクスコアに供給される新規ファイルトリガーです。直近ウィンドウ内で初めて観測されたファイルは小さな加点を得ます。これは、できたばかりのコードが実行やレビューを経る機会を最も持っていないことを反映しています。last_modified_days は逆方向の陳腐化の読み取りです。何か月も触れられていない高リスクファイルと、今日編集されているファイルとでは、保守上の位置付けが異なります。
変更エントロピー
変更エントロピーは、ファイルに触れるコミットがどれだけ 「分散」 しているかを測ります。ウィンドウごとに change_entropy_long と change_entropy_recent として、ビット単位で報告されます。
このメトリクスは、Ahmed Hassan の 2009 年 ICSE 論文 Predicting Faults Using the Complexity of Code Changes による History Complexity Metric です。アイデアは Shannon エントロピーをコミットに適用したものです。各コミットについて、チャーンが触れたファイル間にどう分布するかを取り、その分布のエントロピーをビット単位で計算します。1 ファイルにしか触れないコミットは 0 となり、チャーンを n ファイルに均等に分散させるコミットは log₂(n) に近づきます。分散した横断的コミットは、焦点の定まったコミットより理解が困難です。Hassan は、この変更プロセスの複雑さが従来のコードベースのモデルや変更回数モデルよりも欠陥をよく予測すると報告しました。後続の研究では、8 つの Apache プロジェクトにおいて、ファイルレベルの変更エントロピーが欠陥数と最大 0.54 の Pearson 相関を示すことが測定されています(共変更エントロピーを参照)。
アルゴリズム
big-code-analysis は各コミットについて、触れたファイル間のチャーン分布の Shannon エントロピー H を計算します(src/vcs/entropy.rs を参照)。次に、参加している各ファイルにそのコミットのチャーンシェア pᵢ·H(Hassan の History Complexity Metric)を付与し、これらのシェアをファイルごと・ウィンドウごとに合計します。拡散した複数ファイルの変更に繰り返し巻き込まれるファイルは高い変更エントロピーを蓄積し、常に焦点の定まったコミットでのみ変更されるファイルは低いままです。
読み方
変更エントロピーは、生のチャーンが同一であっても、大きく散漫な編集の一部として 変更されるファイルと、自己完結したコミットで変更されるファイルを区別します。高い変更エントロピーは 横断的関心事 の兆候です。このファイルへの編集が、いつも多くの他ファイルを巻き込んでいるということです。これは直近ウィンドウのリスク項に加算的に入り、チャーンやコミット数の言い換えではなく補完として働きます。
共変更エントロピー
共変更エントロピーは、ファイルの変更の 影響範囲 がどれだけ広いか、つまりどれだけ多くの異なるファイルと一緒に変更される傾向があるかを測ります。ウィンドウごとに cochange_entropy_long と cochange_entropy_recent として、ビット単位で報告されます。
このシグナルは 2025 年の研究 Co-Change Graph Entropy: A New Process Metric for Defect Prediction(arXiv 2504.18511)に由来します。2 つのファイルが同じコミットで変更されるたびに両者をエッジで結び、共有コミット数を重みとする重み付きグラフを構築します。ファイルの共変更エントロピーは、そのエッジ重み分布の Shannon エントロピーです。常に同じ 1 つの相手とだけ共変更されるなら低く、変更が多くの異なるファイルへ波及するなら高くなります。この研究では、8 つの Apache プロジェクトにおいて、変更エントロピーに共変更エントロピーを加えることで、従来のシグナル集合に対して 82.5% のケースで AUROC が改善したと報告されています。
アルゴリズム
ウォークはコミットごとに、どのスコープ内ファイルが一緒に変更されたかを記録して共変更グラフのエッジ重みを蓄積し、その後ウィンドウごとに各ファイルのエッジ重みエントロピーを計算します。1000 ファイル超に触れる一括インポートコミットは、エッジ数がコミット幅の 2 乗で増えるため共変更グラフから除外されますが、(線形コストの)変更エントロピーには引き続き寄与します。計算結果の 0.0 は、そのウィンドウにファイルの共変更相手が存在しないことを意味し、これは欠測ではなく実際の測定値です。
読み方
変更エントロピーが「このファイルに触れるコミットはどれだけ分散しているか」を問うのに対し、共変更エントロピーは「このファイルに触れると 「他の」 ファイルをいくつ巻き込むか」を問います。高い値は、編集の帰結が予測不能で広範囲に及ぶファイル、すなわち結合のホットスポットを示します。2 つのエントロピーシグナルは互いを補完するよう設計されており、リスクスコア v2 は両方の直近ウィンドウ項を加算します。
複合リスクスコア
リスクスコアは、上記のすべてのシグナルをファイルごとの単一の数値 risk_score に集約します。これは bca vcs の主要な出力であり、ランキングテーブルのソートキーとなるフィールドです。2 つの式が用意されており、どちらも単一の risk_score_version(現在は 2)で一緒にバージョン付けされるため、下流の消費者は変更を検出できます。
重み付き式(デフォルト)
デフォルトの式は、カテゴリカルな乗算的加点を伴う対数スケールの重み付き和です。カウントは ln(1 + x) に通されるため、10 コミットと 20 コミットの差は 110 と 120 の差よりも大きく効きます。これは変更活動が実際に飽和していく様子に合致します。
base = 0.30 · ln(1 + churn_recent)
+ 0.25 · ln(1 + commits_recent)
+ 0.15 · ln(1 + commits_long)
+ 0.15 · ln(1 + authors_long) · (1 + dilution)
+ 0.10 · ln(1 + bug_fix_commits + 2 · security_fix_commits)
+ 0.05 · ln(1 + churn_long)
+ 0.10 · change_entropy_recent + 0.05 · cochange_entropy_recent
+ ln(1 + sloc)² / 100
risk_score = base · (1 + dev_bonus + new_file_bonus)
ここで dilution = 1 - ownership_top_share であり、dev_bonus は長期ウィンドウの作者が 9 人以上で 0.35、6 人以上で 0.15、new_file_bonus は直近ウィンドウ内で初めて観測されたファイルに対して 0.15 です。重みは、本章で引用した文献に項ごとに根拠づけられています。直近のチャーンとコミット頻度が最大の重みを持ち(Nagappan & Ball、JIT 系)、作者ファクターは所有権の希薄化でスケーリングされ(Bird et al.)、RHEL4 の開発者数しきい値で加点され(Meneely & Williams)、セキュリティ修正は 2 倍に重み付けされ、2 つの直近ウィンドウのエントロピー項は加算的に入ります(Hassan、arXiv 2504.18511)。完全な導出は src/vcs/score.rs にあります。
サイズ項 ln(1 + sloc)² / 100 は本物の寄与項であり、和の末尾という位置から想像されるような小さなタイブレーカーではありません。10k SLOC で約 0.85 に達し、およそ 50k SLOC を超えると 1.0 を上回り、チャーン項に匹敵する大きさになります。大きなファイルと欠陥の相関は弱いに過ぎませんが、2 乗対数スケーリングにより、サイズは支配的になることなく第一級の加算シグナルであり続けます。
パーセンタイル式
--risk-formula percentile が代替の式です。各シグナルは解析対象の集合内でのパーセンタイルに再ランク付けされ、ファイルごとのパーセンタイルの平均がスコアになります。これは、文献でチューニングされた重みをプロジェクト横断の頑健性と引き換えにするものです。予測研究の文献は一般に、絶対的なハードしきい値よりも 相対的 トリガーを推奨しています。代償として、スコアは単一の実行のファイル集合内でしか意味を持ちません。
読み方
このスコアは順序尺度です。リポジトリのファイルをこれでソートしてリストの上位を見る、というランキングを生み出すことがこのスコアの存在目的です。生の risk_score を 2 つのリポジトリ間で比較したり、その大きさを欠陥確率として読んだりしないでください。単一ファイルの推移を時間軸で追うには bca vcs trend を使ってください。これは各履歴時点でウォークを再アンカーするため、系列は当時のファイルの実際の姿を反映します。
ホットスポットスコア
ホットスポットスコアは、ファイルの複雑度と直近チャーンの積です(hotspot_score = cyclomatic_sum × churn_recent)。これは Option であり、両方の半分を必要とするため、履歴と併せて AST 複雑度の数値が計算されている場合(たとえば bca metrics --metrics cyclomatic --vcs)にのみ存在します。
このメトリクスは、Adam Tornhill の 2015 年の著書 Your Code as a Crime Scene と、その上に築かれた CodeScene ツール群の中心的なアイデアです。論旨はこうです。複雑さ それ自体 は無視しても安上がりで(誰も触れない複雑なファイルには何のコストもかかりません)、チャーン単独もまた安上がりです。危険なのは両者の交差、すなわち複雑で かつ 頻繁に変更されているコードであり、そこに欠陥が集中し、開発者の労力が繰り返し費やされます。Tornhill は、コードベースのごく一部が変更活動の大半を占めるのが典型だと観察しており、ホットスポットスコアはその一部を見つけるために作られています。
読み方
リスクスコアと同様、ホットスポットの積は順序尺度です。ファイルをこれでランク付けし、大きさそのものは読まないでください。CLI は慣例として複雑度の軸にファイルレベルの cyclomatic 合計を使いますが、どの AST 複雑度の数値でも役割を果たします。このスコアの価値は 優先順位付け にあります。複雑なファイルすべての中から、現に編集されているものを浮かび上がらせます。それらは最も壊れやすく、かつ誰かの画面ですでに開かれている今こそ最も安上がりにリファクタリングできるファイルです。
バスファクター
ownership_top_share が単一ファイル 「内」 の知識集中を測るのに対し、バスファクター(トラックファクター とも呼ばれます)はそれを ファイル集合をまたいで 測ります。その離脱によってディレクトリのファイルの半分超が知識ある保守者を失うことになる、最小の開発者数です。この概念全般には Wikipedia の概説があります。big-code-analysis はこれを、リポジトリ全体、各トップレベルディレクトリ、およびその直下のサブディレクトリを対象とする vcs_aggregate オブジェクトとして出力します。
推定手法は、Guilherme Avelino、Leonardo Passos、Andre Hora、Marco Tulio Valente による 2016 年の ICPC 論文 A Novel Approach for Estimating Truck Factors のものです。各開発者の各ファイルに対する作者性は、彼らの Degree-of-Authorship ヒューリスティックでスコア付けされます。
DoA(d, f) = 3.293 + 1.098 · FA + 0.164 · DL − 0.321 · ln(1 + AC)
ここで FA は最初の作者性(開発者 d がファイル f を作成していれば 1)、DL は d の f への提供(変更)回数、AC は 「他の」 開発者による変更数です。開発者は、ファイルの最大値で正規化した DoA が 0.75(論文のしきい値)を超えるとき、そのファイルの作者とみなされます。
アルゴリズム
トラックファクターは貪欲な除去法で計算されます(src/vcs/bus_factor.rs を参照)。まだカバーされているファイルを最も多く作者として持つ開発者を繰り返し取り除き、--bus-factor-threshold(デフォルト 0.5、Avelino に準拠)を超える割合のファイルが孤児化した時点で停止し、取り除いた開発者数を報告します。集約は 1 回のウォークでスコープ内の全ファイルをカバーするため、by_directory の各エントリは各ディレクトリ配下の全ファイルを再帰的に対象として計算されます。
読み方
バスファクター 1 は、1 人を失うとその集合が孤児化することを意味します。これは、ほぼ単一作者のファイルからなるリポジトリではよくあることで、正しい結果です。この数値は保証ではなく計画のためのシグナルとして扱ってください。長期ウィンドウ内で 観測された 作者性に対するヒューリスティックであり、「最初の作者性」はそのウィンドウで観測された最初のコミットを意味し、必ずしもファイルの真の作成を意味しません。知識が危険なほど集中しているディレクトリを見つけ、それに応じてレビューを分散させるために使ってください。
ジャストインタイムコミットスコア
ここまでのすべては、ある時点での 「ファイル」 をランク付けするものでした。ジャストインタイム(JIT)スコアは代わりに、単一の 「コミット」 をその欠陥誘発リスクについてスコア付けします。これは継続的インテグレーションのゲートがチェックイン時に実際にレビューする単位です。bca vcs commit <commit> が生成し、グループごとの contributions 内訳を伴う順序尺度の risk_score として報告されます。
特徴量グループとその符号は、Yasutaka Kamei らによる 2013 年の IEEE TSE 論文 A Large-Scale Empirical Study of Just-in-Time Quality Assurance に始まり、公開追試の Commit Guru(FSE 2015)および Shane McIntosh と Yasutaka Kamei の Are Fix-Inducing Changes a Moving Target?(IEEE TSE 2018)で確認された、ジャストインタイム欠陥予測の文献から採られています。big-code-analysis は学習済みモデルではなく静的なルールベースのスコアラーを実装しているため、プロジェクトが古びても何もドリフトしません。
アルゴリズム
5 つの特徴量グループがスコアを動かします。各グループはコミットの第一親に対して評価されます(src/vcs/jit.rs を参照)。
| グループ | 特徴量 | 方向 |
|---|---|---|
| サイズ | 追加 / 削除行数、変更ファイル数、diff ハンク数 | 大きいほど ⇒ 高リスク |
| 拡散度 | 個別のサブシステム数とディレクトリ数、コミット内変更エントロピー | 分散しているほど ⇒ 高リスク |
| 履歴 | 触れたファイルの事前値(過去の変更回数、個別作者数、修正回数、およびそれらの複合リスク)をコミット 前 に測定したもの | 波乱の履歴 ⇒ 高リスク |
| 経験 | 作者のそれまでのコミット数(長期・直近) | 経験が多いほど ⇒ リスクは低下(このグループは減算) |
| 目的 | メッセージの修正 / セキュリティ修正 / リバート分類 | 修正は加算、リバートは減衰 |
contributions ブロックは各グループの符号付き寄与を報告するため、消費者はコミットが 「なぜ」 その位置にランクされたかを確認できます。マージコミットはフラグ付けされ、第一親に対してスコア付けされます。ルートコミットと新規ファイルは構造上、事前値がゼロになるため、スコアはサイズと作者の経験に依拠します。これはファイル履歴のない変更に対して文献が定める通りの挙動です。素の git diff もスコア付けできます(bca vcs commit --diff)が、diff から計算できるのはサイズと拡散度のグループだけなので、その経路は意図的に部分的な partial_risk_score を出力します。
読み方
ファイルレベルのスコアと同様、JIT スコアは順序尺度です。コミットをこれでランク付けするか、リポジトリ自身のコミットスコア分布と比較してください。ただし、その大きさを確率として読んではいけません。想定される用途はチェックインゲートです。bca vcs commit HEAD --fail-above <N> は、コミットのスコアがしきい値以上のとき非ゼロで終了します。しきい値は絶対値として扱うのではなく、自分たちの履歴に照らして調整してください。式に対するいかなる変更も、ファイルレベルの risk_score_version とは独立した jit_score_version を上げます。
次のステップ
- コマンド → 変更履歴(VCS)メトリクスのページでは、
bca vcsの呼び出し方、ウィンドウの設定、出力形式の選択、永続的な履歴キャッシュの使い方を説明しています。 - 対応コードメトリクスの章では、これらの変更履歴シグナルが補完する AST 由来のメトリクスを扱います。特にホットスポットスコアは 2 つのファミリーを組み合わせたものです。
- Python バインディング → 変更履歴(VCS)メトリクスのページでは、同じファミリーを
big_code_analysis.vcsモジュールから利用する方法を示します。
移行: フラグ CLI からサブコマンド CLI へ
CLI は、フラットなフラグスタイルのインターフェイス(1 つのプロセスに相互排他的な多数の --action フラグ)からサブコマンドスタイルのインターフェイス(bca <verb>)へ再構成されました。このページは、旧来のあらゆる呼び出しをその置き換え先に対応付けます。
変更の理由
フラグ CLI は --output-format に 2 つの無関係な意味を重ねていました。ファイルごとのシリアライズ(-O json/yaml/toml/cbor)と、ウォーク後の集約レポート(-O markdown)です。不正な組み合わせを取り締まるために 2 つの clap ArgGroup に加えてランタイムチェックが必要で、--top / --strip-prefix は 1 つの形式にしか適用されないのにグローバルフラグとして存在していました。将来の集約形式(たとえば HTML)はこの脆さをさらに悪化させたでしょう。
サブコマンド CLI はこの構造を修正します。bca metrics と bca ops はファイルごとの出力を、bca report <FORMAT> は集約レポートを出力し、各動詞は自分専用にスコープされたフラグ集合を持ちます。
移行対応表
| 旧 | 新 |
|---|---|
--metrics -O markdown (+ --top, --strip-prefix) | report markdown |
--metrics -O json/yaml/toml/cbor | metrics -O json/yaml/toml/cbor |
--metrics -O checkstyle/sarif/code-climate/clang-warning/msvc-warning | check --threshold ... --report-format <fmt> [--output FILE] |
--ops -O ... | ops -O ... |
--dump | dump |
--find <NODE> | find -t <NODE> [-t <NODE>...] |
--count <LIST> | count -t <NODE> [-t <NODE>...] |
--function | functions |
--comments [--in-place] | strip-comments [--in-place] |
--preproc <FILE> <FILE>...(生成側) | preproc -o <OUT> |
--preproc <FILE>(消費側) | --preproc-data <FILE>(サブコマンドごと、動詞の後に指定) |
--list-metrics [MODE] | list-metrics [MODE] |
--pr(pretty 表示) | --pretty(metrics と ops で使用) |
--ls, --le(グローバル) | dump/find の --line-start、--line-end(--ls/--le は非推奨のエイリアスとして残存) |
-p, -I, -X, -j, -l | サブコマンドにスコープされます。動詞の 後に 渡してください(-w は引き続き全体共通) |
新旧対照の例
集約 Markdown レポート
# 旧
big-code-analysis-cli \
--metrics \
--paths "$PWD" \
--output-format markdown \
--jobs $(nproc) \
--top 20 \
--strip-prefix "$PWD/"
# 新
bca \
report \
--paths "$PWD" \
--format markdown \
--top 20 \
--strip-prefix "$PWD/"
ファイル単位のメトリクス抽出
# 旧
big-code-analysis-cli --metrics --paths ./src --output-format json --output ./out/
# 新(ファイル単位ツリー: --output は 2.0 で --output-dir になりました)
bca metrics --paths ./src -O json --output-dir ./out/
ファイル単位の ops 抽出
# 旧: big-code-analysis-cli --ops --paths ./src -O json -o ./out/
# 新: bca ops --paths ./src -O json --output-dir ./out/
AST ダンプ
# 旧: big-code-analysis-cli --dump --paths ./file.rs
# 新: bca dump --paths ./file.rs
ノードの検索 / カウント
# 旧: big-code-analysis-cli --find call_expression --paths ./src
# 新: bca find --paths ./src -t call_expression
# 旧: big-code-analysis-cli --count if_statement,for_statement --paths ./src
# 新: bca count --paths ./src -t if_statement -t for_statement
注意:
countは、カンマ区切りの 1 つの文字列ではなく、引数 1 つにつき 1 つのノード型(スペース区切り)を受け取るようになりました。
関数のスパン
# 旧: big-code-analysis-cli --function --paths ./src
# 新: bca functions --paths ./src
コメントの除去
# 旧: big-code-analysis-cli --comments --in-place --paths ./src
# 新: bca strip-comments --paths ./src --in-place
プリプロセッサデータ — 生成側
# 旧
big-code-analysis-cli --metrics --preproc a.h --preproc b.h \
--paths ./src -o /tmp/p.json
# 新
bca preproc --paths ./src -o /tmp/p.json
プリプロセッサデータ — 消費側
# 旧
big-code-analysis-cli --metrics --preproc /tmp/p.json \
--paths ./src -O json -o ./out/
# 新
bca metrics --paths ./src --preproc-data /tmp/p.json \
-O json --output-dir ./out/
メトリクス一覧
# 旧: big-code-analysis-cli --list-metrics descriptions
# 新: bca list-metrics descriptions
実行時の移行ヒント
旧形式の呼び出しを実行すると、CLI は clap 自身のエラーの前に、認識した旧フラグとその新しい対応フラグを示すヒントを表示します。例:
$ bca --metrics -O markdown
note: the CLI was restructured into subcommands. See migration.md for the full mapping.
--metrics -> bca metrics
-O markdown -> bca report markdown|html [--top N] [--strip-prefix P]
Run `bca --help` for the new command list.
error: unexpected argument '--metrics' found
コマンド
bcaは、ソースコードを解析して情報を抽出するためのさまざまなコマンドを提供します。各コマンドは、実行するタスクに固有のパラメーターを持つ場合があります。以下では、bca で利用できる主要なコマンドの種類を説明します。
インストール
bca コマンドラインツールは、pip でインストールできる wheel として提供されています。配布名は big-code-analysis-cliで、インストールされるコマンドは bca です — この 2 つは意図的に異なります(PyPI 上の bca という名前は無関係なプロジェクトのものであり、big-code-analysis は本プロジェクトのインポート可能な ライブラリ バインディングです):
pip install big-code-analysis-cli # PATH に `bca` コマンドをインストールします
bca --version
これにより、pip install ruff が ruff コマンドを提供するのと同じように、コンパイル済みの bca バイナリが PATH に配置されます — Rust ツールチェインは不要です。wheel には完全な all-languages 文法セットが含まれているため、すべての 対応言語 が追加設定なしで動作します。単一の py3-none-<platform> wheel が、そのプラットフォーム上のすべての CPython 3.x(および PyPy)をカバーします。ビルド済み wheel は Linux(manylinux_2_28 x86_64 / aarch64)、macOS(x86_64 / arm64)、Windows(x86_64)向けに提供されます。それ以外のプラットフォームでは pip はソースビルドにフォールバックし、その場合は Rust ツールチェインが必要です。
これはバイナリの CLI であり、インポート可能な Python バインディング(pip install big-code-analysis)とは別物です。その他のインストール方法 — Homebrew、.deb / .rpm / .apk パッケージ、ビルド済みリリースアーカイブ、cargo install big-code-analysis-cli — はリポジトリの README に記載されています。
wheel のビルドと公開のマトリクスは .github/workflows/python-cli-wheels.yml で定義されています。
終了コード
bca はすべてのサブコマンドで単一の終了コード規約に従うため、CI スクリプトは出力を検査せずにプロセスのステータスで分岐できます:
| コード | 意味 |
|---|---|
0 | 成功。 |
1 | ツールエラー — 不正なフラグ / しきい値 / glob 指定、読み取れない入力、またはパース失敗。使用方法エラー(未知のフラグ、不正なサブコマンド、clap に拒否された不正な --threshold 値)も含まれます。決して メトリクスのシグナルではありません。 |
2 | メトリクスゲート: check のしきい値を超過した、vcs commit --fail-above に違反した、または --exit-code 指定時の diff / diff-baseline がフィルター後の空でない差分を検出した場合。 |
3–5 | check --exit-codes=tiered のみ: 段階化された違反の重大度(回帰のみ / 混在 / ハード違反。tiered モードではコード 2 は新規のみを意味します)。 |
コード 2–5 はゲートシグナルであり、check、vcs commit --fail-above、およびオプトインの --exit-code フラグ指定時の diff / diff-baseline だけが発行します。これらはメトリクスの結果を報告するものであって、ツール自体の失敗ではありません。それ以外のすべてのサブコマンド — metrics、ops、report、diff、diff-baseline、exemptions、init など — は、成功時に 0、エラー時に 1 で終了します。1 はツールエラー専用であり — 使用方法エラーも含むため、フラグの打ち間違いがゲート帯域に入ることはありません — CI は常に「ゲートが回帰を検出した」(2–5)と「ツール自体がクラッシュした」(1)を区別できます。
読み取れない入力
ソースファイルを走査するすべてのサブコマンド — metrics、ops、report、functions、find、count、dump、exemptions、preproc、strip-comments、check、init(ベースラインを check 経由で生成します)、および diff --since — は、入力ファイルのいずれかが読み取れなかった場合に 1 で終了します。権限がない場合や、走査から読み取りまでの間にパスが消えた場合です。個々の失敗は error processing <path>: … として stderr に列挙され、続いて 1 行のサマリーが出力されます。
この規則が「出力がまったくない場合」ではなく「読み取り失敗が 1 つでもあった場合」なのは、欠落したファイルが結果からは見えないからです。部分的な metrics --output ドキュメントや report、count は完全なものに見えてしまいます。ファイルを失った diff --since の片側は、それを本来の I/O エラーではなく_追加_または_削除_として報告します。この失敗は終了コードでしか観測できないため、終了コードがそれを担う必要があります。
走査_中にストリーミングされた_出力は、チェックがその後に走るため、そのまま出力されます。したがって混在した実行でも、読み取れたファイルは表示されます — metrics / ops / dump / find / functions の stdout ツリーと、--output-dir のファイル単位のドキュメントです。
走査_後に組み立てられる_出力は完全に抑制されます。部分的なものと完全なものを区別できないからです。これは metrics --output / ops --output の集約ドキュメント、report ドキュメント、count の集計、preproc の JSON、および exemptions レポートに適用されます — いずれも出力も書き込みもされません。
開けないファイルがツリーに正当に含まれている場合は、--exclude で除外してください(あるいは --include でより狭い集合を指定します)。これらのフィルタで除去されたファイルは開かれないため、読み取り失敗にはなりません。
Unlistable directories
A directory the walk cannot list is the same failure one level up, and carries the same exit 1. Its whole subtree drops out of the analysed set before any file is selected, so every count downstream — the metrics document, the count tally, a diff --since side, vcs rank's ranking — is short by an amount nothing in the output reveals. bca check is the worst case: a gate that reports clean on a tree it could not read is indistinguishable from a gate that passed.
Each unlistable entry warns on stderr as bca: warning: skipping walk entry in …, the walk continues so one bad directory does not take down the rest of the tree, and the run ends with a summary line and exit 1.
To exempt a directory you knowingly cannot list, name it in an ignore file (.gitignore, .ignore) or narrow --paths so the walk never reaches it. --exclude does not work here, though it is the right answer for an unreadable file: --exclude filters the paths the walk yielded, and a directory that could not be listed yielded none — the failure happened before the filter could apply.
Two neighbouring cases stay non-fatal by design:
- A malformed ignore file, or a pattern in one that will not compile. The walker reports these through the same channel and they warn identically, but they describe how the walk was configured rather than files it lost. Only errors carrying an underlying I/O error are counted, so a stray
.gitignoretypo cannot fail a build. - A broken symlink discovered by walking. It is dropped for not being a regular file and never surfaces as an error at all — the walk does not follow links, so it deliberately does not resolve symlinks and has nothing to report. Treating one as fatal would make a stale symlink in a vendored tree a hard CI failure.
An explicitly named path is the exception to that last point, and a pre-existing one: --paths resolves a symlink seed once, and a seed that does not exist — dangling link or typo — is its own error, also exit 1.
Unwritable output
The mirror image is the same rule, and it holds for every emission path: a run whose output could not be written exits 1. That covers a per-file document under an unwritable --output-dir, and a full disk on stdout — dump's banners and trees, find's matches, strip-comments' rewritten source, count's tally, preproc's JSON, and the single-document reports from vcs, vcs commit, and vcs trend alike, in every format. A per-file failure is named on stderr and counted in a summary line; output assembled after the walk reports the operating system's error directly.
The one exemption is a closed downstream pipe: bca dump | head is routine rather than a failure, so BrokenPipe is swallowed and the run still exits 0.
フラグの位置と入力パス
ほとんどのサブコマンドは、解析対象の入力を末尾の位置引数パスとして読み取るため、一般的なケースは他のコードツール(tokei、cloc、scc、rg)と同じ書き方になります。例外は次のとおりです: report と vcs は --paths で入力を選択し、diff は 2 つの結果セットを比較し、init は --dir でディレクトリを対象にします。
bca metrics src/ # src/ ツリーを解析する
bca check src/ tests/ # 2 つのサブツリーをゲートする
bca find -t function_item . # 現在のツリー内のすべての関数を検索する
フラグは それを消費するサブコマンドにスコープされ、サブコマンドのトークンの後に 書く必要があります:
bca metrics --exclude '*.generated.rs' src/ # 正しい
bca --exclude '*.generated.rs' metrics src/ # エラー(終了コード 1)
どの位置でも受け付けられる汎用フラグは -w / --warnings と --report-skipped だけです。すべての入力選択フラグ(-p / --paths、-I / --include、-X / --exclude、-l / --language、--paths-from、--exclude-from、--no-ignore、--no-skip-generated、--no-config)、ウォーカー調整フラグ(-j / --jobs、--exclude-tests、--cyclomatic-count-try)、プリプロセッサフラグ(--preproc-data)、および出力フラグ(--color)は、それを読み取るサブコマンドのヘルプ内でグループ化されたセクション(Input selection / Walker tuning / Preprocessor / Output)に属します。あるサブコマンドが消費しないフラグをそのサブコマンドに渡すと、黙って無視されるのではなくハードな使用方法エラー(終了コード 1)になります — そのため bca vcs commit --exclude-tests と bca list-metrics --paths はどちらもエラーになり、bca list-metrics --help はウォーカーフラグを表示しません。
-p / --paths フラグは引き続き機能し、位置引数のパスと 和集合 になります。つまり bca metrics a.rs --paths b.rs は両方を走査します。find と count サブコマンドは、繰り返し指定できる -t / --type フラグでノード種別を受け取ります(そのため位置引数のスロットはパス用に空いています): bca find -t function_item -t struct_item src/。
メトリクス
メトリクスはソースコードに関する定量的な指標を提供し、次のことに役立ちます:
- 異なるプログラミング言語を比較する
- コードの品質に関する情報を提供する
- コードのどこが扱いにくいかを開発者に伝える
- 開発プロセスの早い段階で潜在的な問題を発見する
big-code-analysis は、プログラムのソースコードを起点にメトリクスを計算します。この種のメトリクスは 静的メトリクス と呼ばれます。
ノード
プログラムコードの構造を表現するために、bca は 抽象構文木(AST) を構築します。ノード はこの木の要素であり、言語に存在する任意の構文構造を表します。
ノードは次の用途に使えます:
- ソースファイルの構文構造を作成する
- 解析対象のコードに、ある言語構造が存在するかどうかを調べる
- 特定の種類の構造の数を数える
- ソースコード内のエラーを検出する
REST API
bca-web は REST API を提供するサーバーを実行します。これにより、ユーザーは HTTP 経由でソースコードを送信し、対応するメトリクスを JSON 形式で受け取れます。
生成コードのスキップ
生成されたバインディング(protobuf スタブ、OpenAPI クライアント、lex/yacc の出力、ビルドシステムの補助コード)は、誰もリファクタリングしないコードでメトリクスを水増しします。デフォルトでは、bca は各ファイルの先頭約 50 行 / 5 KiB を走査して生成コードマーカーを探し、一致したファイルをパース 「前に」 スキップします。そのため、スキップされたファイルに tree-sitter のパースコストはかかりません。
認識されるマーカー(大文字小文字を区別しません):
@generated— Facebook / Meta の慣習。buck2、rustfmt、prettier をはじめ、多くのコードジェネレーターも出力します。DO NOT EDIT— Go の// Code generated by … DO NOT EDIT.が標準形です。この語句単体でも広くコピーされています(Bazel、protoc、OpenAPI クライアント)。GENERATED CODE— Lizard のマーカーで、互換性のために認識されます。
マーカー語句がファイル本体の深い位置(走査ウィンドウの外)にのみ現れる場合、スキップは 発動しません — 検出器は意図的にファイルヘッダーだけを見ます。
このスキップは bca metrics、bca report、およびしきい値エンジンに一律に適用されます。
フラグ
--no-skip-generated— 自動スキップを無効にし、以前の動作(すべてのファイルをパースする)に戻します。--report-skipped— 検出器が除外した各ファイルについてskipped (generated): <path>を stderr にログ出力します。これにより除外を監査でき、ファイルが誤って生成コード扱いされていた場合は明示的な include を追加できます。
.gitignore の尊重
--paths にディレクトリが渡されると、bca はデフォルトで .gitignore を考慮して走査します。次のいずれかに一致するファイルは、パース前にスキップされます:
- 走査対象ツリー内の
.gitignoreファイル。 .ignoreファイル(ripgrep /fdの慣習)。.git/info/exclude。- グローバル gitignore(
~/.config/git/ignore、またはcore.excludesFileが指す先)。 - 起点の祖先ディレクトリにある
.gitignoreファイル(そのため、プロジェクトルートからbca metrics src/を実行すると、プロジェクト最上位の.gitignoreが反映されます)。
ウォーカーは、チェックインされた git リポジトリの外でも .gitignore を尊重します。そのため、.gitignore ファイルを含む展開済みのソース tarball も、git clone した直後と同じ扱いになります。
隠しファイル(ベース名が . で始まるもの)は走査中に除外され、以前の動作と一致します。
明示的なパスはフィルターを迂回する
Files passed by name — via --paths, --paths-from, or a trailing positional path — are always analyzed, even when the project's ignore rules cover them. This makes it safe to do bca metrics --paths-from - from git diff --name-only-style pipelines without losing files that happen to match a wildcard ignore rule.
The override is deliberate, and it is the same rule rg and fd apply: a path you named is a direct request. It spans every deny-set the walk consults — .gitignore and friends, -X / --exclude, --exclude-from, a .bcaignore, and a manifest exclude list alike. rg --glob '!x.txt' pattern x.txt searches x.txt for exactly this reason.
Two boundaries on it:
-I/--includestill applies. The allow-list narrows which named files are analyzed, sobca metrics -I '*.rs' notes.mdanalyzes nothing. Only the deny-sets are overridden.[check] excludestill applies. The gate-exemption set (--check-exclude/--check-exclude-from, orexcludeunder[check]inbca.toml) is not a walk filter — it drops violations, not files — so it survives an explicit path and reportsbca: skipped N violations via [check.exclude]when it fires. One caveat while #1164 is open: for an explicitly named path those globs resolve against the working directory rather than the manifest root, so runbcafrom the directory holdingbca.toml— which is what CI and the agent hooks already do.
That second point is the one to reach for. A walker exclude shapes what gets analyzed; a check exclude shapes what gets gated. Anything you want kept out of the threshold gate permanently — dev tooling, generated code you still want measured, a subtree under active rewrite — belongs in [check] exclude, because any caller that names paths one at a time bypasses the walker excludes by design. Per-file callers are not hypothetical: that is the shape the agent feedback hooks use on every edit.
When an explicitly named path does override a walker exclude, bca says so on stderr and names the glob:
bca: warning: utils/gate.py matches an exclude pattern (./utils/**) but was named explicitly; analyzing anyway
パス探索フラグ
--no-ignore— ディレクトリの起点を展開する際の.gitignore/.ignore/ グローバル gitignore の考慮を無効にします。--paths-from <FILE>— read newline-separated input paths from<FILE>, or from stdin when<FILE>is-. Combined as a union with any--pathsvalues.-Iglobs still apply;-Xglobs do not reach an entry that names a file directly, per Explicit paths bypass the filter. Blank lines are skipped;#is treated as a path character (not a comment). To pass a file literally named-, write./-.--exclude-from <FILE>— 改行区切りの--excludeglob パターンを<FILE>から読み取ります。<FILE>が-の場合は stdin から読み取ります。パターンはインラインの--exclude/-Xの値と和集合として 1 つの拒否セットにまとめられ、順序は関係ありません。.gitignore形式です: 空行と、最初の非空白文字が#の行はスキップされ、先頭の UTF-8 BOM は取り除かれます。慣習としては、.gitignore/.dockerignoreに倣ってリポジトリルートに.bcaignoreを置きます。文字どおり-という名前のファイルを渡すには./-と書きます。
メトリクス
bca metrics はファイル単位のメトリクスを計算し、stdout、単一の集約ファイル(--output)、またはファイル単位の構造化ファイルを収めるディレクトリ(--output-dir)のいずれかに出力します。
移行中ですか? このコマンドは、再構成前の
--metricsフラグを置き換えるものです。以前-O markdownで選択していた集約レポートはbca reportに移り、CI/IDE 向けの違反レポートフォーマット(Checkstyle、SARIF、code-climate、clang-warning、msvc-warning)はbca check --report-format <fmt>に移動しました。移行ガイド を参照してください。
メトリクスの表示
指定したファイルまたはディレクトリのメトリクスを計算して表示するには、次を実行します:
bca metrics --paths /path/to/your/file/or/directory
--paths(または-p): 解析するファイルまたはディレクトリ。ディレクトリを指定すると、その中のすべての対応ファイルについてメトリクスが計算されます。パスは位置引数でも指定できます(bca metrics file.rs dir/)。
明示的に名前を指定したファイルはパース可能でなければなりません。 言語をツールが認識できないファイルを直接(位置引数または
--paths/--paths-from経由で)指定すると、bcaは stderr に警告を出力し、実行が出力をまったく生成しなかった場合は 1 で終了します。これは、存在しない明示パスが失敗するのと同じ挙動です。少なくとも 1 ファイルを解析した混在実行は、警告を出しつつ 0 で終了します。ファイルの拡張子が内容と食い違う場合は、--language <lang>を渡してパーサーを強制してください。「ディレクトリ」 の走査によってのみ到達したファイルは黙ってスキップされます(README や設定ファイルだらけのツリーで騒がしくなってはいけません)。そうしたスキップも表示したい場合は-wを渡してください。認識は_される_ものの読み取れないファイルはより厳格な扱いになり、読み取れない入力の規則に従います。他のファイルの解析が成功していても実行は 1 で終了します。黙って短くなったメトリクスドキュメントは、完全なものとして読まれてしまうからです。
メトリクスのエクスポート
bca metrics は 5 つのファイル単位出力フォーマットに対応しています:
- CBOR
- CSV
- JSON
- TOML
- YAML
JSON と TOML は、いずれも整形(pretty-print)してエクスポートできます。
トップレベルの 3 種類の出力は 3 つの別々のコマンドに対応しており、それぞれが自身のデータモデルとの一貫性を保ちます:
| コマンド | 出力 | 対象 |
|---|---|---|
bca metrics | ファイル単位のメトリクスツリー | 下流のツール |
bca report | 集約された品質ダッシュボード | 人間 / プルリクエスト |
bca check | しきい値違反レポート | CI / IDE |
CI/IDE 向けの違反レポートフォーマット(Checkstyle、SARIF、code-climate、clang-warning、msvc-warning)は、かつて bca metrics -O <fmt> にありました。これらの入力はしきい値違反のリストであって、上記の他フォーマットが扱うファイル単位のメトリクスツリーではないため、bca check --report-format <fmt> に移動しました。新しい呼び出し方は bca check の章 を参照してください。
エクスポートコマンド
メトリクスを JSON ファイルとしてエクスポートするには:
bca metrics --paths /path/to/your/file/or/directory \
-O json --output metrics.json
-O, --format: 出力フォーマット。デフォルトはtextで、stdout に表示される人間可読のカラー付きメトリクスツリーです。このデフォルトを明示的に要求するには--format textを渡します(たとえば、構造化フォーマットを設定したbca.tomlを上書きする場合)。構造化されたファイル単位のシリアライザーはcbor、csv、json、toml、yamlです。--output-formatは非推奨のエイリアスとして受け付けられますが、次のメジャーリリースで削除予定です。-o, --output: 実行全体の集約ドキュメント 1 つを保持する単一ファイル — ファイル単位の結果を並べたトップレベル配列です(TOML は配列をfilesキーの下にラップし、CSV は各ファイルの行を連結します)。省略した場合、結果は stdout に出力されます。--output-dir: 入力ファイルごとに 1 つのドキュメントを保持するディレクトリで、各ドキュメントは入力パスにフォーマットの拡張子を付けた名前になります。--outputとは相互排他で、両方を渡すとエラーです。- CBOR はバイナリのため、出力先(
--outputまたは--output-dir)が必須です。構造化された--formatなしでいずれかの出力先を渡すとエラーになります(デフォルトのtextフォーマットは stdout にストリームし、ファイルを書きません)。これにより、出力先の指定が黙って無視されることはありません(#661)。 --metrics <name,…>: 計算をメトリクスのサブセットに制限します(カンマ区切りまたは繰り返し指定、例:--metrics cyclomatic,cognitive --metrics loc)。名前はbca list-metricsが表示する正規 ID で、bca check --thresholdやbca diff --metricと同じ語彙です。ドット付き(cyclomatic.modified)や、locサブメトリクスの単独表記(sloc)も受け付けられます。派生メトリクスは依存するメトリクスを自動的に取り込みます。未知の名前は「did you mean」ヒント付きのエラーになります。省略すると、すべてのメトリクスを計算します(#691)。
CSV(スプレッドシートと Pandas)
bca metrics --paths /path/to/your/code \
-O csv --output-dir csv-output
CSV ライターは FuncSpace(関数、クラス、構造体、ユニットなど)ごとに 1 行を出力し、メトリクス行列全体を列として並べます。ヘッダーの順序は固定です — 正規のリストは src/output/csv.rs の CSV_HEADER を参照してください。識別列(path、space_name、space_kind、start_line、end_line)が先頭に来て、その後にすべての葉メトリクスが JSON と同じドット付き名(loc.lloc、halstead.volume、cyclomatic.modified.average など)で続くため、1 つの列名で CSV と JSON の両方のメトリクスを指せます。
空セル(0 ではなく値なし)は「このスペースには該当しない」ことを示します — たとえば、OOP 専用メトリクス(wmc.*、npm.*、npa.*)は手続き型コードでは空になります。RFC 4180 の引用符処理は [csv] クレートに委譲されているため、カンマ、引用符、改行を含むパスや名前も正しく往復します。
- を使って、結果を単一の出力にストリームします:
bca metrics --paths /path/to/your/code -O csv \
> metrics.csv
CSV はファイル単位のフォーマットです。--output-dir <dir> を指定すると、各入力ファイルは出力ディレクトリ配下に <input>.csv のミラーを生成します。--output <file> を指定すると、すべてのファイルの行が 1 つの集約 CSV に連結されます。
走査全体をカバーする集約 HTML レポート は
bca report htmlで利用できます。以前のファイル単位のbca metrics -O htmlライターは、実際のリポジトリでは開けないほど巨大な単一ファイルのテーブルに劣化してしまうため削除されました — フラットなFuncSpace単位の行には CSV が適切な形です。
整形出力
bca metrics --paths /path/to/your/file/or/directory \
--pretty -O json
インラインテストコードの除外
bca metrics --paths /path/to/your/code --exclude-tests
デフォルトでは、インラインのテスト項目を含む AST 内のすべてのノードがカウントされます。そのため、慣用的な #[cfg(test)] mod tests { ... } レイアウトに従う Rust ファイルでは、主要メトリクスに本番コードとテストコードが混在します。
メトリクスが計算される前にテスト専用のサブツリーを取り除くには、--exclude-tests を渡します。このフラグは AST を走査するすべてのサブコマンド(metrics、report、check)で認識され、現在は次の Rust 属性の形を理解します:
#[test]および#[rstest]/#[test_case]/#[wasm_bindgen_test]#[cfg(test)]、#[cfg(all(test, ...))]、#[cfg(any(test, ...))]#[tokio::test]、#[async_std::test]、#[test_log::test]など(::testで終わる任意のパス)modアイテムに付く#
Checker::should_skip_subtree のオーバーライドを持たない言語では、このフラグは単に無視されます — 現在プルーニングを適用するのは Rust のみです。デフォルトはオフのままなので、オプトインしないユーザーの既存のメトリクス値はバイト単位で同一に保たれます。
フラグを繰り返さずにプロジェクト全体をオプトインするには、リポジトリの bca.toml マニフェストで exclude_tests = true を設定します。--exclude-tests は存在のみのフラグ(=false 形式なし)のため、マニフェストキーはプルーニングをオンにすることしかできません。CLI の --exclude-tests は引き続き優先されますが、マニフェスト側からオフに戻すことはできません。プルーニングはノード数ベースのメトリクス(cyclomatic、cognitive、Halstead、nom、nargs など)を下げますが、ユニットレベルの loc.sloc はファイル全体の範囲のまま残ります。ユニット SLOC はトラバーサルの累積ではなくファイルルートのスパンだからです。
集約レポート
包括的で人間が読みやすい品質レポートには、bca report markdown を使用してください。このコマンドは解析したすべてのファイルにわたってメトリクスを集約し、言語ごとのホットスポットテーブルを生成します。
利用可能なメトリクスの一覧
CLI を駆動するツールは、メトリクスのカタログをハードコードする代わりに実行時に検出できます。
bca list-metrics
はメトリクス名を 1 行に 1 つずつ出力します。各メトリクスの 1 行要約を得るには descriptions を渡します。
bca list-metrics descriptions
変更履歴(VCS)メトリクス
bca vcs は、ソースの AST(抽象構文木)ではなくバージョン管理履歴から導かれるシグナルである変更履歴リスクでファイルをランク付けします。これはプロジェクト初の、言語非依存かつ非 AST のメトリクスファミリーです。目的は、欠陥予測・脆弱性予測の実証研究文献が最も一貫して裏付けているシグナルを用いて、バグや脆弱性を抱えている可能性が最も高いファイルを浮かび上がらせることです。
履歴の走査は呼び出しごとに 1 回だけ実行され(ファイルごとには決して実行されません)、設定可能な 2 つのウィンドウ — 長期ウィンドウ(デフォルト 12mo ≈ 365 日)と直近ウィンドウ(デフォルト 90d)— にわたるファイルごとのシグナルを生成します。
クイックスタート
$ bca vcs --paths src --top 20
Change-history risk (long window 365d, recent 90d, formula v2)
RANK RISK COMMITS rec/long CHURN rec/long AUTHORS long FILE
1 7.2 68/68 11634/11634 1 src/metrics/cyclomatic.rs
2 6.9 68/68 7299/7299 1 src/metrics/npa.rs
...
--format を指定しない場合、人間が読めるランク付きテーブルが出力されます。レンダリングされたレポートページには --format markdown|html を、構造化出力には --format json|yaml|toml|cbor|csv を渡します。bca metrics / bca ops(これらの --output-dir はファイルごとの出力を格納する「ディレクトリ」です)と異なり、変更履歴レポートはリポジトリ全体で 1 つの文書なので、bca vcs --output <file> は1 つのファイルを書き出します(バイナリ形式である CBOR には --output が必須です)。グローバルな --paths / --include / --exclude / --no-ignore フィルタは、どの追跡ファイルをレポートするかの選択に再利用されます。
bca vcs を git 作業ツリーの外で実行すると、明確なエラーになります。
ファイルタイプのスコープ
デフォルトでは、bca vcs は bca がメトリクスを計算するファイルのみ — bca metrics が解析するのと同じ集合 — をランク付けします。チャーンの多い非ソースファイル(CHANGELOG.md、Cargo.lock、生成された設定ファイル)は保守性の観点では意味を持たない一方でチャーン / コミット / 作者のシグナルを最大化するため、ソースコードと並べてランク付けするとノイズになります。メトリクス対象ファイルへのスコープ設定には、スタンドアロンのランキングを bca report --vcs の AST ホットスポットテーブルと揃えておく効果もあります。
--file-types <SCOPE> でスコープを選択します。
| 値 | 意味 |
|---|---|
metrics (デフォルト) | bca が言語 / メトリクスを持つファイルのみ(拡張子で判定) |
all | 追跡中の、バイナリでもシンボリックリンクでもないすべてのテキストファイル |
rs,py,toml,… | カンマ区切りの拡張子許可リスト(先頭のドットは省略可、大文字小文字は区別しない) |
bca vcs # ソースファイルのみをランク付け(デフォルト)
bca vcs --file-types all # 追跡中のすべてのテキストファイルをランク付け
bca vcs --file-types rs,py # Rust と Python のファイルのみをランク付け
この判定は拡張子のみで行われ(ファイル内容は読み取りません)、--paths / --include / --exclude / --no-ignore フィルタと AND 条件になります — ランク付けされるには、ファイルが両方を通過する必要があります。拡張子のないファイル(Makefile、Dockerfile、LICENSE)や未知の拡張子は metrics スコープの対象外です。カスタムリストはリテラルな拡張子フィルタなので、toml のようなメトリクス対象外のタイプも含められます。空または空白のみのカスタムリストは、何もランク付けせず沈黙するスコープになるのではなく、明確なエラーになります。
レンダリングされたレポートページ
bca vcs --top 50 --format html --output vcs.html
bca vcs --top 50 --format markdown --output vcs.md
--format html は、bca report html とまったく同じスタイルの、自己完結型でソート可能なページを生成します(任意の列ヘッダーをクリックすると並べ替えられます)。--format markdown は同じランク付きテーブルを GitHub-Flavored Markdown として生成します。どちらもすべてのシグナル列をレンダリングします(構造化フォーマットが持つのと同じデータの、完全でソート可能なビューです)。列セットは一度だけ定義され両方のレンダラーで共有されるため、両者が乖離することはありません。
ランキングをスタンドアロンページではなく集約品質レポートに組み込むには、bca report --vcs を渡します。これは report markdown / report html に「Change-history risk」セクションを追加します。
シグナル
| フィールド | 型 | 説明 |
|---|---|---|
commits_long / commits_recent | u32 | 各ウィンドウでファイルに触れた個別コミット数 |
churn_long / churn_recent | u64 | 各ウィンドウにおける Σ(added + deleted) 行数 |
authors_long / authors_recent | u32 | 各ウィンドウにおける正規化済み作者 ID の個別数 |
ownership_top_share | f64 ∈ [0,1] | トップ作者に帰属する編集の割合(低いほど所有権の希薄化が進んでいる) |
burst | f64 ∈ [0,1] | commits_recent / commits_long |
bug_fix_commits | u32 | メッセージがバグ修正キーワードに一致する長期ウィンドウのコミット数 |
security_fix_commits | u32 | セキュリティキーワード(CVE-####、security、vuln、exploit、sanitize など)に一致する長期ウィンドウのコミット数 |
revert_commits | u32 | 件名がリバート / ロールバックである長期ウィンドウのコミット数 |
age_days | u32 | ファイルのウィンドウ内で最初のコミットからの経過日数(長期ウィンドウが上限) |
last_modified_days | u32 | ファイルのウィンドウ内で最も新しいコミットからの経過日数 |
change_entropy_long / change_entropy_recent | f64 | ウィンドウごとの変更エントロピー(ビット単位、下記参照) |
cochange_entropy_long / cochange_entropy_recent | f64 | ウィンドウごとの共変更グラフエントロピー(ビット単位、下記参照) |
risk_score | f64 | 複合スコア。式はバージョン管理されます(下記参照)— 順序尺度であり、基数尺度ではない |
hotspot_score | f64? | complexity × churn_recent。AST メトリクスが同時に計算される場合のみ存在します |
risk_score_version / vcs_schema_version | u32 | 前方互換性のためのバージョンスタンプ。long_window_days / recent_window_days と並んでレポートのエンベロープに 1 回だけ付与され、ファイルごとの vcs ブロック内では繰り返されません(issue #635) |
作者 ID はリポジトリの .mailmap を通じて正規化され、小文字化したメールアドレスで数えられます。Co-authored-by: トレーラーは参加者を追加します。ボット ID(dependabot[bot]、renovate[bot]、github-actions[bot] など)はデフォルトで除外されます。バイナリファイルとシンボリックリンクはスキップされます。未追跡ファイルにはレコードがまったく存在しません(ウィンドウ内の活動がゼロの追跡ファイルとは区別されます)。
変更エントロピーと共変更エントロピー
2 つのプロセスエントロピーシグナル(risk_score_version 2 で追加)は、ファイルの変更量だけでなく、「どのように」変更されるかを捉えます。
- 変更エントロピー(Hassan, 2009 — Predicting Faults Using the Complexity of Code Changes)。各コミットについて、そのコミットが触れたファイル群にわたるチャーン分布の Shannon エントロピー(ビット単位)が、その変更がどれだけ「分散」していたかを測ります。1 ファイルのみのコミットは 0 で、n 個のファイルへ均等にチャーンを広げるコミットは log₂(n) に近づきます。次に、各ファイルには参加した各コミットのチャーンシェア
pᵢ·Hがクレジットされます(Hassan の History Complexity Metric)。値が高いほど、そのファイルは拡散的で横断的な変更に繰り返し巻き込まれていることを意味します。後続研究(下記の arXiv 2504.18511)では、8 つの Apache プロジェクトにおいてファイルレベルの変更エントロピーと欠陥数との Pearson 相関が最大 0.54 と測定されました。 - 共変更グラフエントロピー(arXiv 2504.18511, 2025)。同じコミットで変更されたファイル同士は、重み付きエッジ(重み = 共有コミット数)で結ばれます。ファイルの共変更エントロピーは、そのエッジ重み分布の Shannon エントロピーです。常に同じ相手と共変更される場合は低く、変更が多くの異なるファイルへ波及する場合は高くなります。変更エントロピーと組み合わせることで、8 つの Apache プロジェクトにおいて v1 シグナルセットに対し 82.5% のケースで AUROC を改善しました。
どちらもウィンドウごとに報告されます。0.0 は欠損値ではなく計算された値です。そのファイルが常に単独で変更されていた(共変更の隣接ファイルがない、または変更エントロピーがゼロの単一ファイルコミットのみ)ことを意味します。1000 ファイル超に触れる一括インポートコミットは、エッジ数が O(width²) で増えるため共変更グラフからは除外されますが、O(width) の変更エントロピーには引き続き寄与します。
複合リスクスコア
デフォルトの加重(weighted) 式は、カテゴリ別の乗法的ボーナスを伴う、対数スケールの加重和です。
recency_churn = ln(1 + churn_recent)
recency_count = ln(1 + commits_recent)
long_count = ln(1 + commits_long)
long_churn = ln(1 + churn_long)
author_factor = ln(1 + authors_long)
dilution = (1 - ownership_top_share).clamp(0, 1)
fix_factor = ln(1 + bug_fix_commits + 2 * security_fix_commits)
size_factor = ln(1 + sloc)^2 / 100 // タイブレーカーではなく本来の係数
entropy_factor = 0.10 * change_entropy_recent + 0.05 * cochange_entropy_recent
new_file_bonus = 0.15 if age_days < recent_window_days else 0
dev_bonus = 0.35 if authors_long >= 9 else 0.15 if authors_long >= 6 else 0
base = 0.30 * recency_churn
+ 0.25 * recency_count
+ 0.15 * long_count
+ 0.15 * author_factor * (1 + dilution)
+ 0.10 * fix_factor
+ 0.05 * long_churn
+ entropy_factor
+ size_factor
risk_score = base * (1 + dev_bonus + new_file_bonus)
各項の重みは文献に基づいています。直近のチャーンとコミット頻度が最大の重みを持ちます(Nagappan & Ball の相対チャーン、just-in-time 欠陥予測、Firefox の NumChanges PD 86)。作者ファクターは所有権の希薄化でスケールされます(Avelino の DoA / トラックファクター、Bird ら)。カテゴリ別の開発者数ボーナスは、9 人以上の開発者が触れたファイルは脆弱性を抱える可能性が約 16 倍だったという RHEL4 の知見をエンコードしています。セキュリティ修正には 2 倍の重みが付きます(Sentence-Level VFC 研究、PySecDB)。そして直近ウィンドウの変更エントロピーおよび共変更エントロピーの項は加算的に入ります(Hassan 2009、arXiv 2504.18511)。完全な導出は src/vcs/score.rs にあります。
このスコアは順序尺度であり、相対的な順位のみが意味を持ちます。単一の risk_score_version(現在は 2)が両方の式をバージョン管理します。加重和「または」 --risk-formula percentile ブレンドのどちらを変更してもバージョンが上がります。直近のエントロピー 2 項は両方の式に加わっています。
--risk-formula percentile は代替の式です。各シグナルを解析対象集合内のパーセンタイルに再ランク付けし、その平均を取ります — 文献は、プロジェクト横断の頑健性のために、固定しきい値よりも相対的なトリガーを推奨しています。
フラグ
| フラグ | デフォルト | 意味 |
|---|---|---|
--long-window <DUR> | 12mo | 長期ウィンドウ(12mo、2y、8w、365d、ISO 8601 の P1Y) |
--recent-window <DUR> | 90d | 直近ウィンドウ |
--top <N> | 50 | 上位 N 件のみ表示(0 = すべて) |
--file-types <SCOPE> | metrics | ランク付け対象のファイル: metrics、all、または拡張子リスト(rs,py) |
--ref <REF> | HEAD | 解析対象のリビジョン |
--full-history | オフ | DAG 全体を走査(デフォルト: first-parent のみ) |
--include-merges | オフ | マージコミットを含める |
--no-follow-renames | オフ | リネームの追跡を止める(デフォルト: 追跡する) |
--no-exclude-bots / --bot-pattern <RE> | 除外 | ボット作者のフィルタリング |
--as-of <WHEN> | 現在時刻 | 再現可能なスナップショットのための基準「now」(RFC 3339 / @unix / git 日付形式) |
--risk-formula {weighted\|percentile} | weighted | 複合スコアの式 |
--emit-author-details | オフ | SHA-256 でハッシュ化した正規作者 ID を出力 |
--author-hash-key <KEY> | 未設定 | 出力される作者ダイジェストを鍵付き HMAC に強化する(作者詳細のプライバシーを参照)。--emit-author-details が必要 |
--include-deleted | オフ | 対象 ref で削除済みのファイルもランク付けする |
--no-cache | オフ | 永続履歴キャッシュをスキップする(常に新規に走査) |
--clear-cache | オフ | 実行前にこのリポジトリのキャッシュ済み履歴を消去する |
--cache-dir <DIR> | プラットフォーム標準のキャッシュ | キャッシュディレクトリを上書きする |
キャッシュ
ランキングが再走査するのは長期ウィンドウ内の履歴部分だけですが、大規模で活発なリポジトリではそれでも支配的なコストです — そして CI では、実行間で意味のある差分は前回以降にプッシュされたコミットだけです。そのため bca vcs は、解決済みの HEAD SHA とリポジトリの同一性をキーとして、各走査の永続キャッシュを保持します。
- 変更のないツリーでは前回の結果が再生され、履歴走査は行われません。
HEADが前進した場合、走査は新しいコミットのみを訪れ、それをキャッシュ済み履歴に継ぎ足します。- フォースプッシュ(キャッシュされた head が新しい head の祖先でなくなった状態)では、完全走査にフォールバックします。
このキャッシュは純粋な最適化です。キャッシュヒットは新規の走査とビット単位で同一であり、時間ウィンドウは実行のたびに「現在」時刻に対して再計算されるため、キャッシュされた結果が古くなることはありません。スキーマ、スコア式のバージョン、または「走査に影響する」オプションが異なる場合、エントリは無視され履歴が再計算されます。特に、ウィンドウを変更すると新規の走査が強制されます。(--risk-formula、--emit-author-details、--author-hash-key、--include-deleted のような最終化のみのノブは再生時に適用されるため、同じキャッシュ済み走査を再利用します — キャッシュ済み走査は、「異なる」作者ハッシュ鍵の下でも再走査なしで再最終化されます。)
デフォルトでは、キャッシュは $XDG_CACHE_HOME/big-code-analysis/vcs の下に置かれます(Windows では %LOCALAPPDATA%、それ以外では ~/.cache)。作者 ID は SHA-256 ダイジェストとしてのみ保存され、平文では決して保存されないため、キャッシュに生の作者メールアドレスは含まれません。これは匿名化ではなく仮名化である点に注意してください。ダイジェストは候補となるメールアドレス集合に対して復元可能です(--emit-author-details を参照)。同じキャッシュは bca metrics --vcs と bca report --vcs も透過的に高速化します。
# 最初の実行がキャッシュを準備し、2 回目はそれを再生します。
bca vcs --paths .
bca vcs --paths . # 前回の作業を再利用
bca vcs --no-cache --paths . # この実行ではキャッシュを無視
bca vcs --clear-cache --paths . # ゼロから再構築
bca vcs --cache-dir /tmp/bca-cache --paths .
REST(POST /v1/vcs)と Python(vcs.rank)の各インターフェースは、省略可能な no_cache / cache_dir パラメータを通じて同じ挙動を提供します。
このキャッシュはファイルランキング専用です。trend と commit の各サブコマンド — および /v1/vcs/trend と /v1/vcs/jit エンドポイント — はキャッシュを使用しないため、キャッシュ関連のフラグはそこでは適用されません。サブコマンドと併せて --no-cache / --cache-dir を渡すと使用方法エラーになり、trend エンドポイントは no_cache / cache_dir フィールドを黙って無視するのではなく拒否します(issue #961)。
bca metrics での利用
bca metrics --vcs を渡すと、各ファイルの metrics に vcs ブロック(およびファイルの cyclomatic 合計から計算される hotspot_score)が付加されます:
$ bca metrics --vcs --paths src/parser.rs --format json
{ "name": "src/parser.rs",
"metrics": { "cyclomatic": { ... },
"vcs": { "commits_long": 15, "churn_recent": 211,
"risk_score": 3.7, "hotspot_score": 7596.0, ... } } }
bca metrics --vcs はデフォルトのウィンドウと重み付き計算式を使用します。ウィンドウや計算式を調整したい場合は bca vcs を使用してください。
関数単位の帰属
bca metrics --vcs-per-function(--vcs を暗黙に含みます)は、さらに入れ子になったすべての関数・メソッド・クラスのスペースに vcs ブロックを付加します。各ファイルを git blame で一度だけ blame し、現存する行を AST の関数スパンに振り分けるため、リスクの高いファイルの中のリスクの高い「関数」をランク付けできます:
$ bca metrics --vcs-per-function --paths src/parser.rs --format json
{ "name": "src/parser.rs",
"metrics": { "vcs": { "risk_score": 3.7, ... } }, // ファイルレベルのブロック
"spaces": [
{ "name": "parse", "kind": "function",
"metrics": { "vcs": { "commits_long": 4, "churn_recent": 12,
"risk_score": 2.1, "hotspot_score": 144.0 } } } ] }
関数単位のブロックは現時点の blame のスナップショットであり、ファイルレベルのブロックと直接比較「できません」。その churn は、最終変更がウィンドウ内に収まる現存行を数えるものであり(過去の追加+削除のチャーンではありません)、所有権は変更したコミットごとに加算されます。ウィンドウ内で誰も変更していない関数のカウントはゼロと報告されます。最終変更が長期ウィンドウより前の行は、関数のサイズには寄与しますが、ウィンドウ付きのどのカウントにも寄与しません。
制限事項。 blame はファイルのリネームを追跡します(そのため旧パスでの編集も帰属します)が、「関数間を移動した」行は現在の位置にのみ帰属します。2 つに分割された関数には分割前のアイデンティティの記録はなく、削除後に再作成された関数は再作成したコミットに帰属します。ファイルが blame できない場合 — 未追跡の場合や、病的に反復的な内容に対するまれな gix-blame の失敗の場合 — その関数単位のブロックは単に省略され、ファイルレベルのブロック(と AST メトリクス)は引き続き出力されます。
ジャストインタイム(コミットレベル)スコアリング
ここまでの機能がリファレンス時点の「ファイル」をランク付けするのに対し、bca vcs commit <commit> は単一の「コミット」の欠陥誘発リスクをスコアリングします — これは CI ゲートがチェックイン時にレビューする単位です。(このサブコマンドは 2.0 で bca vcs jit から改名されました。旧称の jit は 1 リリースサイクルの間、隠しエイリアスとして引き続き動作します。「ジャストインタイム(JIT)」は以下でも文献上の用語として残ります。)これは静的なルールベースのスコアラーで(学習済みモデルを使わないため、プロジェクトが古くなっても何もドリフトしません)、特徴量グループと符号はジャストインタイム欠陥予測の文献から採られています:Kamei et al., A Large-Scale Empirical Study of Just-in-Time Quality Assurance, IEEE TSE 2013、およびそのオープンな追試である Commit Guru(FSE 2015)と McIntosh & Kamei, Are Fix-Inducing Changes a Moving Target?(IEEE TSE 2018)です。
$ bca vcs commit HEAD --pretty
{
"jit_schema_version": 3,
"jit_score_version": 1,
"source": "commit",
"risk_score": 4.40,
"commit": { "id": "5176d3e…", "parent_count": 1, "is_merge": false,
"purpose": { "is_fix": true, "is_security_fix": false,
"is_revert": false } },
"features": {
"size": { "lines_added": 942, "lines_deleted": 60,
"files_touched": 19, "hunks": 78 },
"diffusion": { "subsystems": 5, "directories": 8, "entropy": 3.48 },
"history": { "prior_changes": 275, "prior_distinct_authors": 1,
"prior_bug_fix_commits": 237,
"prior_security_fix_commits": 21,
"file_risk_max": 10.97, "file_risk_mean": 3.87,
"new_files": 2 },
"experience": { "author_prior_commits": 962,
"author_recent_commits": 962 }
},
"contributions": { "size": 2.74, "diffusion": 0.97, "history": 1.57,
"purpose": 0.15, "experience": -1.03 }
}
5 つの特徴量グループと、それぞれがスコアをどう動かすか:
| グループ | 特徴量 | 方向 |
|---|---|---|
| サイズ | 追加 / 削除行数、変更ファイル数、diff ハンク数 | 大きいほど ⇒ 高リスク |
| 拡散度 | 個別のサブシステム数とディレクトリ数、コミット内の変更エントロピー | 分散しているほど ⇒ 高リスク |
| 履歴 | 変更対象ファイルの事前値 — 過去の変更回数、個別の作者数、バグ修正・セキュリティ修正の回数、および複合 risk_score — をコミット「以前」の履歴から測定 | 荒れたファイル履歴 ⇒ よりリスキー |
| 経験 | 作者の過去のコミット数(長期・直近) | 経験が多いほど ⇒ リスクは低下(このグループは減算) |
| 目的 | メッセージの修正 / セキュリティ修正 / リバート分類 | 修正は加算、リバートは減衰 |
contributions ブロックは、順序尺度である risk_score への各グループの符号付き寄与を報告するため、コミットが「なぜ」その順位になったのかを利用側が確認できます。ファイルレベルの risk_score と同様、このスコアは順序尺度です。スコアでコミットをランク付けしたり、リポジトリ自身の分布とコミットを比較したりできますが、その大きさを確率として読み取らないでください。計算式が変更されると jit_score_version が上がります(ファイルレベルの risk_score_version とは別です)。
コミットは第一親に対してスコアリングされます。*「マージ」*コミットにはフラグが立てられ(is_merge、parent_count ≥ 2)、その第一親に対してスコアリングされます。ルートコミットと新規ファイルは、構造上、事前値がゼロになります — その場合スコアはサイズと作者の経験に依存します。これはファイル履歴のない変更に対して文献が定めるとおりの挙動です。
ウィンドウ / --ref / ボット / マージ / リネームの各フラグは親コマンドの bca vcs と共有されます。commit 専用のフラグは、位置引数の <commit>(デフォルト HEAD)、--format json|yaml|toml|cbor(デフォルト json)、--output、--pretty、および次のとおりです:
# CI ゲート:コミットのスコアがしきい値以上のとき exit 2 で終了する。
bca vcs commit HEAD --fail-above 6.0
--fail-above は終了コード 2 を使用します(bca check と同じ「メトリクスゲート」の慣例です。終了コード 1 はツールエラー用に予約されたままです)。スコアは順序尺度なので、しきい値は絶対値として扱うのではなく、リポジトリ自身のコミットスコア分布に対して調整してください。
任意の diff のスコアリング(--diff)
bca vcs commit --diff <file> はコミットの代わりに git diff をスコアリングします(標準入力から diff を読むには --diff - を使用します)。変更がまだコミットされておらず diff としてのみ存在する pre-commit フックやコードレビューボットで便利です。
git diff --cached | bca vcs commit --diff - --pretty
入力は、git diff や git format-patch が生成する、diff --git ファイルヘッダーを持つ git 形式の unified diff でなければなりません。素の diff -u / diff -ru の出力(---/+++ ヘッダー行はあるが diff --git ヘッダーがないもの)は 0 ファイルとしてパースされ、combined / マージ diff(@@@ ハンクヘッダーを持つ git diff --cc)は不正な diff として拒否されます — 代わりに通常の 2-way git diff をパイプしてください。
素の diff には作者・親・ファイル履歴の情報が一切ないため、計算できるのは sizeとdiffusion のグループだけです。したがって出力は意図的に「部分的な」レポートであり、コミットレポートとは異なる形をしています:
$ git diff | bca vcs commit --diff - --pretty
{
"jit_schema_version": 3,
"jit_score_version": 1,
"source": "diff",
"partial_risk_score": 1.83,
"size": { "lines_added": 42, "lines_deleted": 8,
"files_touched": 3, "hunks": 6 },
"diffusion": { "subsystems": 2, "directories": 3, "entropy": 1.46 },
"contributions": { "size": 1.18, "diffusion": 0.65 }
}
source フィールドは恒久的な "diff" マーカーであり、history / experience / purpose の各グループはレポートから完全に欠落します — ゼロとして現れるのでは「ありません」。ゼロは実際の値です(本当に事前履歴のないコミットは、これらのグループをゼロとしてスコアリングします)。欠落したグループは「利用不可」を意味するため、利用側がスコアリングされていないグループを「低リスク」と取り違えることはありません。同じ理由で、スコアのフィールド名は risk_score ではなく partial_risk_score です。
diff のみのスコアはコミットスコアと比較できません。 部分スコアは size + diffusion のみを合計するため、同じ変更に対する完全なコミットスコア(history、experience、purpose も織り込まれます)より常に低くなります。diff は「他の diff」 に対してランク付けし、決してコミットスコアと比較しないでください。
--diffと位置引数の<commit>は同時に指定できません。--fail-aboveは両方のモードで機能します(diff モードのしきい値は自身の diff スコア分布に対して調整してください)。
パーサーは git のデフォルトである C 形式のパス引用(core.quotePath=true)を理解するため、非 ASCII 文字や空白を含む名前のファイル(git は "a/na\303\257ve.txt" のように出力します)に触れる diff は、diffusion の特徴量では引用付きの生文字列ではなく、デコードされたパスの下にグループ化されます。
REST と Python のパリティ
JIT スコアは CLI 以外からも利用できます:
- REST:
POST /v1/vcs/jitに{ "id", "repo_path", "commit" }を渡すとコミットのJitReportJSON が返り、{ "id", "diff" }を渡すと部分的な diff レポートが返ります。REST API の利用を参照してください。 - Python:
vcs.commit(repo_path, commit=...)はコミットレポートをdictとして返し、vcs.score_diff(diff)は部分的な diff レポートを返します。変更履歴(VCS)メトリクスを参照してください。
ML ベースの JIT モデルとサーバーサイドフック統合は、引き続きスコープ外です。
履歴トレンド(時系列)
単発の bca vcs 実行は 「いま何がリスキーか」 に答えます。bca vcs trend は、複数の時点でメトリクスをサンプリングしてファイルごとの時系列を出力することで、「良くなっているのか悪くなっているのか」 — 技術的負債プログラムにとって行動につながる問い — に答えます。
$ bca vcs --top 20 trend --points 12 --span 24mo --pretty
{
"trend_schema_version": 1,
"vcs_schema_version": 2,
"risk_score_version": 2,
"long_window_days": 365,
"recent_window_days": 90,
"truncated_shallow_clone": false,
"as_of_points": [ 1700000000, 1705259520, ... ],
"files": {
"src/parser.rs": [
null, // 最古の時点では存在しなかった
{ "as_of": 1705259520, "vcs": { "risk_score": 4.1, ... } },
{ "as_of": 1710519040, "vcs": { "risk_score": 6.8, ... } }
]
},
"deltas": {
"improved": [ { "path": "src/old.rs", "delta": -3.2, ... } ],
"regressed": [ { "path": "src/parser.rs", "delta": 2.7, ... } ]
}
}
--points N 個の等間隔サンプル(両端点を含む)が --span DURATION をカバーし、--as-of(指定がなければ現在時刻)で終わります。as_of_points はサンプルのタイムスタンプを古い順に列挙し、各ファイルの配列はそれと 1:1 で対応します。null 要素は、その時点でファイルがまだ存在しなかったことを示します。deltas は、各ファイルの最初と最後に存在した時点の間で risk_score が最も下がったファイル(improved)と最も上がったファイル(regressed)をランク付けします。--top-deltas で各リストを切り詰められます。
重要なのは、各時点がその瞬間かそれ以前に存在したメインラインの先端に再アンカーすることです — 今日の HEAD ツリーのウィンドウを引き直すだけではありません。これにより、後から生まれたファイルは古い時点で null として表示されます(現在のメトリクスが過去に漏れ出すことはありません)。系列に残されるファイルは、最新サンプルの時点でリスクが最も高い --top 件です。
親コマンド bca vcs から再利用されるフラグ:ウィンドウ(--long-window / --recent-window)、--ref、--file-types、ボット / マージ / リネームのトグル、--as-of(最新のアンカー)、および --top。-O は json(デフォルト)、yaml、cbor を受け付けます。存在しない時点は null としてシリアライズされ、TOML はこれを表現できないため、TOML は除外されています。時点数は、深い履歴でも時点ごとの履歴走査が扱いきれる範囲に収まるよう、2–120 に制限されています。
リネームに関する注意。 リネームは各サンプルの走査「内」では追跡されますが、2 つのサンプルの「間」でリネームされたファイルは、1 本の連続した線ではなく、2 つの別々のパス系列(旧名、次に新名)として現れます。サンプル間のリネームの縫合は後回しのフォローアップです。
バスファクター(ディレクトリ・リポジトリレベル)
ファイルごとの ownership_top_share がファイル「内」の集中度を測るのに対し、バスファクター(別名トラックファクター)は「ファイル集合をまたいで」集中度を測ります。すなわち、その離脱によってディレクトリ内のファイルの半数超が精通した保守者を失うことになる、最小の開発者数です。bca vcs はこれを、ランク付けされた files と並ぶトップレベルの vcs_aggregate オブジェクトとして出力します:
{
"vcs_aggregate": {
"bus_factor": {
"bus_factor_schema_version": 2,
"coverage_threshold": 0.5,
"doa_threshold": 0.75,
"repo": { "bus_factor": 3, "files": 412, "authors": 11 },
"by_directory": [
{ "directory": "src", "bus_factor": 2, "files": 180, "authors": 7 },
{ "directory": "src/vcs", "bus_factor": 1, "files": 24, "authors": 3 }
]
}
}
}
各開発者の各ファイルに対する作者性は、Avelino の Degree-of-Authorship ヒューリスティック(Avelino, Passos, Hora & Valente, A Novel Approach for Estimating Truck Factors, ICPC 2016)でスコアリングされます:
DoA(d, f) = 3.293 + 1.098·FA + 0.164·DL − 0.321·ln(1 + AC)
ここで FA は最初の作者性(d が f を作成したなら 1)、DL は d の f への提供(変更)回数、AC は「他の」開発者による変更回数です。ファイルの最大値で正規化した DoA が 0.75(論文のしきい値)を超えるとき、その開発者は f の作者とみなされます。トラックファクターは貪欲な除去で求めます。すなわち、まだカバーされているファイルを最も多く作者として持つ開発者を除去し、ファイルの --bus-factor-threshold(デフォルト 0.5、Avelino に準拠)超が孤児になるまで繰り返し、除去した人数を報告します。by_directory は各トップレベルディレクトリとその直下のサブディレクトリをカバーし、それぞれ再帰的に配下のすべてのファイルに対して計算されます。
構造上の注意点:
- 大部分が単一作者のファイルからなるリポジトリ(またはディレクトリ)のバスファクターは
1と報告されます — その 1 人の作者を失うと各ファイルが孤児になるからです。これはヒューリスティックが意図どおりに働いているのであってバグではありません。この数値は保証ではなく計画のためのシグナルとして扱ってください。 - ボットのアイデンティティは(ファイルごとのシグナルと同様に)フィルタリングされ、ウィンドウ内に活動のないファイルは作者性を持たず、分母から除外されます。
- 「最初の作者性」は「長期ウィンドウ内で観測された」最古のコミットを意味し、必ずしもファイルの真の作成を意味しません。
この集約はファイル種別スコープ内のリポジトリ全体を反映します(1 回の履歴走査がスコープ内のすべてのファイルをカバーします — デフォルトはメトリクスを持つファイルの集合で、ファイル種別スコープを参照 — したがって --file-types all はバスファクターを追跡対象のすべてのファイルに広げます)。--paths / --include / --exclude はランク付けされたファイルごとのリストのみをスコープし、バスファクターには「適用されません」。サブシステムに注目したい場合は、走査をフィルタリングするのではなく by_directory の該当エントリを読んでください。
--emit-author-details は各グループに key_author_ids リストを追加します — 除去されたキー開発者の SHA-256 ハッシュ化されたアイデンティティを、除去順に並べたものです(平文のアイデンティティがプロセスの外に出ることはありません)。この集約は専用の bca vcs / bca report --vcs レポートと REST / Python エンドポイントに対してのみ計算されます。ファイルごとの bca metrics --vcs 注入パスはこのコストを払いません。
作者詳細のプライバシー
key_author_ids のダイジェストは安定した仮名であり、匿名化ではありません。ハッシュ化により平文のメールアドレスはレポートとキャッシュに含まれず、安易な漏洩は抑止されますが、このハッシュは暗号学的に不可逆では「ありません」。原像はメールアドレス — 低エントロピーで列挙可能 — であり、コミット履歴は公開されているため、候補となるメールアドレスの集合を持つ者なら誰でも、各候補をハッシュ化するか、事前計算されたメール→ハッシュのテーブルを使って、どのダイジェストが誰のものかを割り出せます。これは Gravatar のメールハッシュ化を破ったのと同じ弱点です。
公開された key_author_ids(およびファイルごとの author_ids)は、平文のメールアドレスの出力を避ける仮名化として扱い、執念深い攻撃者によって作者が再識別されないことの保証としては扱わないでください。その保証が必要なら、ダイジェストを公開しないでください。
強化モード:--author-hash-key
より強い耐性が必要な場合は、--author-hash-key <KEY> で秘密鍵を渡します(--emit-author-details が必要です)。出力されるダイジェストは素のハッシュではなく HMAC-SHA256(key, SHA-256(email)) になります。鍵を持たない攻撃者は、候補のメールアドレスをハッシュ化してダイジェストを見分けることも、事前計算されたメール→ハッシュのテーブルを使うこともできなくなります — どちらの攻撃にも秘密鍵が必要です。高エントロピーの鍵を選び、秘密に保ってください。鍵を知った者は誰でも列挙をやり直せます。
鍵は安定しています。同じ鍵なら、すべてのレポートにわたって、また永続キャッシュのリプレイをまたいでも同じダイジェストが得られるため、レポート間の相関付けとキャッシュは引き続き機能します。異なる鍵は無関係なダイジェストを生成するため、履歴を共有する 2 つのチームは、鍵を共有しない限り作者を相互にひも付けることはできません。
フラグよりも環境変数 BCA_AUTHOR_HASH_KEY を優先してください — コマンドラインに置かれた鍵はプロセスリスト(ps)経由で他のローカルユーザーから見え、シェル履歴にも保存されます。両方が設定されている場合はフラグが優先されます:
export BCA_AUTHOR_HASH_KEY="$(cat ~/.config/bca/author-key)"
bca vcs --emit-author-details
鍵がカバーしないもの:ディスク上の履歴キャッシュ(issue #334)は意図的に「鍵なしの」内側の SHA-256 ダイジェストを保存します。鍵は確定処理の段階で適用されるため、キャッシュ済みの走査は再走査なしに任意の鍵で再確定できるからです。キャッシュはローカル限定で公開されることはありませんが、脅威モデルにローカルのキャッシュディレクトリを読む攻撃者が含まれる場合は、キャッシュを無効化(--no-cache)するかクリア(--clear-cache)してください。同じ鍵オプションは REST エンドポイント(author_hash_key)と Python(vcs.Options(author_hash_key=…))でも利用できます。
このリポジトリでのドッグフーディング
このプロジェクトは自身のソースに対して bca vcs を実行しています。make vcs はランク付けされたテーブルを表示します(パス選択と .bcaignore の除外セットは、make self-scan と make report が使うのと同じ、リポジトリルートの bca.toml マニフェストから取得されます。BCA_VCS_TOP で行数の上限を上書きできます)。マニフェストの [vcs] file_types キーがデフォルトのスコープを設定します(--file-types CLI フラグが指定された場合はそちらで置き換えられます)。main へのプッシュのたびに、Pages CI ジョブがレンダリング済みランキングをフラッグシップレポート — bca report html --vcs / report markdown --vcs — に織り込むため、公開される reports/index.html には変更履歴リスクのセクションが AST ホットスポットと並んで表示され、さらにツール向けに完全なトップ 100 ランキングが reports/vcs-report.json として公開されます。
REST と Python
- REST: JSON ボディ
{ "id": "...", "repo_path": "/path/to/repo", ... }を伴うPOST /v1/vcsはランク付けされたレポートを返し、POST /v1/vcs/trend(同じフィールドに加えてpoints/span/top_deltas)は履歴の時系列を返します。REST API の利用を参照してください。 - Python:
big_code_analysis.vcs.rank(repo_path, …)はランク付けされたレポートを dict として返し、vcs.trend(repo_path, points=…, span=…, …)は時系列を返します。またanalyze(path, vcs=True)は単一ファイルのメトリクスにvcsブロックを付加します。
POST /v1/vcs と vcs.rank()(vcs.Options 経由)のどちらも、ランク付け対象のファイルをスコープする省略可能な file_types("metrics" / "all" / "rs,py")を受け付けます。これは CLI の --file-types に対応します。
どちらも結果に vcs_aggregate のバスファクターを含み、カバレッジの割合を調整する bus_factor_threshold(範囲は (0, 1))を受け付けます。
レポート
bca report [--format <FORMAT>] は、走査したすべてのファイルにわたる品質メトリクスの集約レポートを生成します。プルリクエスト、wiki、課題トラッカーへの貼り付けを想定して設計されています。
フォーマットは --format / -O で選択します(bca report --format html)。省略時のデフォルトは markdown です。素の位置引数形式(bca report markdown)は非推奨のエイリアスとして引き続き動作しますが、次のメジャーバージョンで削除される予定です。--format を使用してください。
CI 統合。 Markdown レポートを PR/MR コメントとして投稿する、実行可能な GitHub Actions と GitLab CI のレシピについては、CI 統合レシピを参照してください。
利用できるフォーマットは 2 つです。markdown(プレーンテキストで、PR コメントに最適)と html(ソート可能なテーブルを備えた自己完結型ダッシュボードで、ビルド成果物として共有するのに最適)です。
移行の際は。 このコマンドは、再構成前の
--metrics -O markdown呼び出しを置き換えるものです。移行ガイドを参照してください。
クイックスタート
標準出力に表示:
bca report --paths /path/to/project markdown
ファイルに書き出し:
bca report --paths /path/to/project markdown --output report.md
注:
--outputはディレクトリではなく「ファイル」パスでなければなりません。
フラグ
| フラグ | デフォルト | 説明 |
|---|---|---|
--top N | 20 | ホットスポットテーブルごとの最大エントリ数(0 = すべて)。 |
--strip-prefix PATH | (空) | ファイルパスから取り除くプレフィックス。 |
--no-suppress | (オフ) | ソース内の抑制マーカーで沈黙させられた関数を含めます(生の監査ビュー)。 |
--vcs | (オフ) | bca metrics --vcs に対応する、VCS リスク(デフォルトウィンドウ)でファイルをランク付けする「変更履歴リスク」セクションを追加します。このセクションは AST ホットスポットテーブルと同じメトリクスを持つファイル集合(metrics ファイル種別スコープ、#576)をランク付けするため、両者は同一のファイル母集団を記述します。git 作業ツリーの外では警告とともに無視されます。bca vcs を参照してください。 |
-o, --output FILE | (標準出力) | 出力ファイル。親ディレクトリが存在している必要があります。 |
抑制マーカー
デフォルトでは、bca report markdown|html はソース内の抑制マーカーを尊重します — bca check と SARIF エミッターが尊重するのと同じ // bca: suppress、// bca: suppress-file、#lizard forgives コメントです(抑制を参照)。あるメトリクスが抑制されている関数はそのメトリクスのホットスポットテーブルから除外されるため、公開されるレポートは、沈黙済みの違反をすべて再表示するのではなく、しきい値ゲートと一致します。
抑制はメトリクス単位です。// bca: suppress(cyclomatic) マーカーは関数を Cyclomatic テーブルからのみ除外します — Cognitive、Halstead などのテーブルには引き続き表示されます。引数なしの // bca: suppress(または // bca: suppress-file)はすべてのメトリクスをカバーします。
マーカーに関係なくすべての違反を列挙する生の監査ビューには --no-suppress を渡します。この設定は bca.toml マニフェストに固定することもできます:
[report]
no_suppress = true
CLI フラグが優先されます。素の --no-suppress は監査ビューを強制的に有効化できますが、マニフェストがそれを強制的に無効化することはありません。
使用例
セクションごとに最悪のホットスポット 5 件のみを表示:
bca report -p src/ markdown --top 5
表示されるパスからワークスペースルートを取り除く:
bca report -p /home/user/project markdown \
--strip-prefix /home/user/project/
日常使いの呼び出し例:
bca report \
--paths "$PWD" \
markdown \
--top 20 \
--strip-prefix "$PWD/"
レポートの構成
生成されたレポートには以下のセクションが含まれます(データが存在しないセクションは省略されます)。すべてのホットスポットテーブルには SLOC と並んで Tokens 列(Lizard 方式の葉トークン数、コメントは除外)が含まれ、行ごとに補完的な 2 つのサイズ指標を確認できます。
- プロジェクトサマリー — 分析したファイル数、言語、SLOC / PLOC / コメントの総数、関数数とクラス数、コメント比率。
- 言語別概要テーブル — 言語ごとに 1 行で、ファイル数、SLOC、関数数、SLOC 加重平均の保守容易性指数(MI)、平均循環的複雑度(CC)、平均認知的複雑度を示します。MI の平均はサイズ加重で、「クランプされていない」 Visual Studio の値を使用するため、ファイルの大半が保守不能な言語は、ファイルごとの
MI列の下限 0 で飽和するのではなく負の値として表示されます。 - 言語別ホットスポットセクション(言語ごとに繰り返されます)。すべてのホットスポットのタイトルは
<Concept> hotspots (top N by <column>)という 1 つのテンプレートに従い、切り詰め句は実際の--topの状態を示します(top 20 by CC、--top 0の場合はall, by CC):- Summary — ファイル数、SLOC、PLOC、コメント比率、および GOOD / MODERATE / LOW の評価付き
Average MI (SLOC-weighted)。見出しの値は「クランプされていない」 Visual Studio MI の SLOC 加重平均です。大きなファイルが支配的になり、表示上の MI が 0 にクランプされるファイルも、誤解を招く 0 ではなく真の(多くの場合負の)保守性で寄与します。 - Actionable Summary — 一般的なしきい値を超える関数の数(デフォルトでは CC > 10、cognitive > 15、SLOC > 100、args > 3、Halstead bugs > 1。マニフェストの
[thresholds]テーブルで各カットオフを上書きできます)。Summary の直後、どのホットスポットテーブルよりも前に最初に出力されるため、テーブルを 1〜2 個読んで止めた読者でも最も俯瞰的なカウントを目にできます。これらは抑制を無視した生のカウントです。セクションのキャプションにその旨が記され、抑制された関数がいくつ含まれているかが明示されます。このサマリーがカウントしているメトリクスのホットスポットテーブルが抑制によって空になった場合、そのテーブルは「table omitted: all N matching functions suppressed」という 1 行の注記に置き換えられ、サマリーの箇条書きが存在しないテーブルを指すことはありません。 - Maintainability Index hotspots (lowest N by MI) — MI の昇順でソートされたファイル。
- Cyclomatic complexity hotspots (top N by CC) — CC の降順でソートされた関数。要約統計(平均、最大、10 超・20 超の件数)付き。
- Cognitive complexity hotspots (top N by Cognitive) — 認知的複雑度の降順でソートされた関数。
- Halstead effort hotspots (top N by Effort) — Halstead effort の降順でソートされた関数。volume と推定バグ数を含みます。Effort と Volume は千区切り付きの丸め整数(
8,845)として表示され、完全な精度は JSON/CSV に保持されます。 - Function size hotspots (top N by SLOC) — ソースコード行数の降順でソートされた関数。
- Many parameters hotspots (top N by Args) — パラメータが 3 個を超える関数。降順でソートされます。
- Type hotspots (top N by WMC) — Weighted Methods per Class の降順でソートされた型。NOM、NPA、NPM 付き。「Type」はレポートが数える 6 種類すべて(class、struct、trait、impl、interface、namespace)をカバーします(凡例の WMC エントリに列挙されています)。
- Exit points hotspots (top N by Exits) — 出口点が 2 個を超える関数。降順でソートされます。単一の
returnは基準であってホットスポットではないため、テーブルにはnexits > 2のみが載ります。下限を超えるものがなければセクションは省略されます。 - ABC magnitude hotspots (top N by ABC) — ABC メトリクスの大きさの降順でソートされた関数。
- Summary — ファイル数、SLOC、PLOC、コメント比率、および GOOD / MODERATE / LOW の評価付き
フォーマット間の一貫性
Markdown レポートと HTML レポートは、1 つの基盤データモデルの 2 つのレンダリングであり、常に同じデータを提示します。共有されるすべての数値(プロジェクトおよび言語別のサマリー、ホットスポットテーブルのメンバーシップ、循環的複雑度の Average / Max / CC > 10 注記のような各ホットスポットのキャプション)は一度だけ計算されて両方でレンダリングされるため、1 回の実行で --format markdown と --format html のどちらを出力しても同一の数値になります。
どちらのフォーマットにも、すべてのメトリクス列の略語(CC、MI、ABC、WMC、…)とグローバルヘッダーの統計(PLOC、Comments、Comment ratio)を定義する凡例が含まれます — Markdown では ## Legend セクション(最後の言語の下に入れ子になるのではなく、独立したアウトラインエントリ)、HTML では展開済みの(<details open>)ブロックです。これにより、ホバーツールチップに加えて、印刷・モバイル・スクリーンリーダーでも凡例が失われません。各エントリは対応メトリクスリファレンスの該当章にリンクしているため、1 行の定義から完全な説明へ読者を案内できます。定義はツールチップが使うのと同じ共有列仕様に由来するため、2 つのフォーマットが乖離することはありません。
どちらのフォーマットも、bca のバージョン、生成日、走査したシードパス、テーブルごとの --top 値、抑制マーカーを尊重したかどうかを記した来歴フッターで締めくくられます — これにより、切り離された成果物(PR コメント、Pages デプロイ、チケットに添付されたファイル)に、それが何から生成されたのかが記録されます。生成日は再現可能ビルドのために SOURCE_DATE_EPOCH を尊重します。HTML レポートはさらに <meta name="viewport"> タグを持ち、すべてのテーブルを水平スクロールコンテナで包むため、幅の広いテーブルもモバイルや狭いウィンドウで閲覧可能なままです。また目次では、各言語のホットスポットのサブセクションが折りたたみ可能なエントリの下に入れ子になります。
抑制はレポートだけでなくすべての出力に一律に適用されます。あるメトリクスについて — ソース内マーカーまたはベースラインにより — 沈黙させられた関数は、bca check の違反出力フォーマット(code-climate、sarif、checkstyle、clang-warning、msvc-warning)からも、対応するレポートのホットスポットテーブルからも同様に除外されます。CodeClimate、SARIF、Checkstyle の各ドキュメント自体が 1 つの違反集合の 3 つのレンダリングなので、構造上一致します。レポートも同じメトリクス単位の抑制判断を尊重します。
唯一の意図的な例外は Actionable Summary です。これはコードベース全体の健全性指標であり、抑制に関係なく生の測定値を意図的にカウントします — あるメトリクスのホットスポットテーブルで関数を沈黙させても、この集約された懸念カウントからは消えません。各ホットスポットテーブルのキャプションを含む他のすべての数値は、抑制でフィルタリングされた集合を反映します。読者が 2 つの母集団を二重カウントと取り違えないよう、それぞれにキャプションが付きます。循環的複雑度の注記には「(excluding suppressed functions)」が追加され、Actionable Summary はそのカウントが抑制を無視した生の値に基づくことを明示します。
HTML フォーマット
bca report html は、Markdown レポートと同じセクションをカバーする単一の自己完結型 HTML ページを出力します。静的成果物として配信することを想定した設計です。インライン CSS、すべてのホットスポットテーブルをクリックでソートするためのインラインのプレーンな JavaScript を備え、外部依存はゼロです(CDN なし、フォントなし、テンプレートエンジンなし)。ページはオフラインでも同一にレンダリングされます。
ファイルに書き出して任意のブラウザで開きます:
bca report --paths /path/to/project \
html --top 10 --output report.html
任意の列ヘッダーをクリックするとそのテーブルが昇順でソートされ、もう一度クリックすると降順に切り替わります。各テーブルは独立してソートされます。空のセル(メトリクスが測定されなかった箇所)は正の無限大であるかのようにソートされるため、「データなし」の行がホットスポットの見える上位に紛れ込むことはありません。
任意のメトリクス列ヘッダー — SLOC、MI、CC、ABC、WMC、NPA、NPM、Exits など — にホバー(ブラウザが対応していればキーボードフォーカスでも可)すると、そのメトリクスの平易な英語による 1 文の説明が表示されます。ツールチップはネイティブの HTML title 属性で提供されるため、JavaScript なしでもオフラインで機能します。
title ツールチップはホバー時のみ有効で — 印刷・モバイル・スクリーンリーダーでは見えないため — ページの末尾には、すべてのメトリクス列の 1 行定義を列挙する、目に見える折りたたみ可能な凡例(<details>)も置かれます。ツールチップと凡例は同じ列仕様から生成されるため、ホバー時と凡例とで定義が食い違うことはありません。
補間されるすべての文字列 — 関数名、ファイルパス、言語ラベル — は出力時に HTML エスケープされるため、細工されたソースパスやシンボル名がマークアップを注入したり、属性値の外に抜け出したりすることはできません。
言語ごとの各 <section> には安定した lang-<name> クラス(例:lang-rust、lang-python)が付与され、低アルファの背景色とそれに対応する左ボーダーでスタイル付けされるため、多言語レポートのセクション境界が一目で分かります。明示的なパレットエントリを持たない言語は中立的な lang-other の色にフォールバックし、prefers-color-scheme: dark アダプターがアルファを引き上げるため、どちらのテーマでもコントラストが保たれます。
メトリクス値のゼロ
レポート内のメトリクス値 0 は、その項目についてメトリクスが測定されなかったことを意味します(例:空の関数に対する Halstead メトリクス)。エントリがすべてゼロのセクションは完全に省略されます。
チェック
bca check は関数ごとのメトリクスをしきい値に照らして評価し、いずれかの関数が制限を超えると非ゼロで終了します。これは CI の統合ポイントです。ビルドステップに組み込めば、コード複雑度の悪化が変更の取り込み前にパイプラインを失敗させます。
完全な CI レシピをお探しですか? CI 統合レシピには、
--report-formatのマトリクス、実行可能な GitHub Actions と.gitlab-ci.ymlの例、ベースライン / ラチェットのパターン、GitLab Code Quality のパスがまとめられています。本ページはコマンド自体を、レシピはパイプラインへの組み込み方をそれぞれ説明します。
終了コード
| コード | 意味 |
|---|---|
0 | すべての関数がしきい値内(または --no-fail 指定時)。 |
2 | 少なくとも 1 つのしきい値を超過。 |
1 | ツールエラー(不正な引数、読み取れない設定または入力、未知のメトリクス)。 |
1 は予約されており、CI がリグレッション(2)とツールの設定ミス(1)を区別できるようになっています。
A gate that could not read all of its input has no verdict to report, so three input problems exit 1 rather than 0: nothing matched --paths / --include / --exclude, any input file that failed to read, and any directory the walk could not list. The last two are the workspace-wide unreadable-input rule — check is not special here, it is just where the rule matters most, since a gate reporting clean on a tree it could not read is indistinguishable from a gate that passed. All three run before the gate is evaluated and are not suppressed by --no-fail, which suppresses threshold failures, not broken input, so none of them lets --write-baseline record a partial run.
段階的終了コード(--exit-codes=tiered)
--exit-codes=tiered (or [check] exit_codes = "tiered" in bca.toml) splits the single violation code 2 by severity so CI can branch on it without parsing the [new] / [regr +N%] row tags:
| コード | 意味(tiered モード) |
|---|---|
0 | すべての関数がしきい値内(または --no-fail 指定時)。 |
1 | ツールエラー。 |
2 | 新規違反のみ(一致する --baseline エントリなし)。 |
3 | ベースライン退行のみ(ベースライン登録済みの違反が悪化)。 |
4 | 新規違反と退行の両方。 |
5 | ハード制限も超えている --tier=soft 違反。 |
tiered コードはオプトインであり、上記のデフォルト契約は 0/1/2 のままです。すべての失敗状態は非ゼロのままなので、exit != 0 → fail 方式のラッパーは引き続き動作します — $? -eq 2 を明示的に判定するツールだけが 2-5 に広げる必要があります。--no-fail は引き続き終了コード 0 を強制します。コード 5 はソフト層でのみ出力されます。ハード層では定義上すべての違反がハード超過であるため、代わりに 2/3/4 の分割が適用されます。--exit-codes <default|tiered> は値を取るフラグで、CLI の値はどちらの方向でもマニフェストの [check] exit_codes キーを上書きします。無効な exit_codes 値はツールエラー(1)です。--print-effective-config は解決後の exit_codes スタイルを報告します。非推奨の --strict-exit-codes フラグは --exit-codes tiered の 1 サイクル限りのエイリアスです(警告を出し、次のメジャーで削除されます)。
しきい値の宣言
メトリクスごとに --threshold <metric>=<limit> を 1 回ずつ渡します(繰り返し指定可能)。メトリクス名は bca list-metrics と一致し、サブメトリクスはドット区切り形式を使います。0 は有効な制限値で、「いかなる値も許容しない」ことを意味します。
bca check --paths src/ \
--threshold cyclomatic=15 \
--threshold cognitive=20 \
--threshold loc.lloc=200
あるいは、しきい値を bca.toml マニフェストに置くこともできます(CI のしきい値をコードと一緒にバージョン管理できる一元的な場所です)。リポジトリルートに置けば自動検出され、--config フラグなしの素の bca check がそれを読み込みます。
# bca.toml
paths = ["src"]
[thresholds]
cyclomatic = 15
cognitive = 20
"loc.lloc" = 200
"halstead.volume" = 1000
bca check
1 回の実行でマニフェストの上に別のしきい値ファイルをマージするには、--config で明示的に渡します。CLI フラグと --config の値は、同じメトリクス名についてマニフェストを上書きするため、プロジェクト全体のデフォルトを保ちながら、特定の実行でひとつのメトリクスだけを厳しくできます。
bca check --paths src/ --config bca.toml
bca init scaffolds a starting table for you. For where those numbers come from, which ones to override for the language you are gating, and how to pick a different set for agent feedback, legacy triage, or safety-critical work, see Choosing thresholds.
使用できるメトリクス名
トップレベルのスカラーメトリクスは list-metrics の名前をそのまま使います:cognitive、cyclomatic、nargs、nexits、nom、tokens、abc、wmc、npm、npa。複数のサブフィールドを持つメトリクススイートはドット区切り形式を使います。
| メトリクス | 使用できるしきい値名 |
|---|---|
| 循環的複雑度 | cyclomatic、cyclomatic.modified |
| Halstead | halstead.volume、halstead.difficulty、halstead.effort、halstead.time、halstead.bugs |
| コード行数 | loc.sloc、loc.ploc、loc.lloc、loc.cloc、loc.blank |
| 保守容易性指数 | mi.original、mi.sei、mi.visual_studio |
不明なしきい値名はツールエラー(終了コード 1)となり、黙って無視されることはありません。
しきい値のスコープ
しきい値は、そのメトリクスが実際に測定するスペース種別に対してのみチェックされるため、メトリクスのファイル全体や impl 全体の集計値が関数単位の制限と取り違えられることはありません。各メトリクスのスコープは固定で、設定項目はありません。
| スコープ | ゲート対象のスペース | メトリクス |
|---|---|---|
| ファイル | ファイル全体のルートのみ | loc.sloc、loc.ploc、loc.lloc、loc.cloc、loc.blank |
| 関数 | 個々の関数、メソッド、クロージャ | cognitive、cyclomatic、cyclomatic.modified、halstead.*、mi.*、abc、nargs、nexits、tokens |
| コンテナ | クラス、構造体、トレイト、impl、名前空間、インターフェイス | nom、wmc、npm、npa |
関数スコープのメトリクスにはサブツリー合計(nargs、nexits、tokens、halstead.*)が含まれます。これらは引き続き関数自身の入れ子クロージャをその値に繰り入れますが、ファイル全体や impl 全体にわたって合算されることはなくなりました。コンテナスコープのメトリクスは型のメソッド集合(クラスあたりのメソッド数、重み付きメソッド、公開メンバー)を表すため、末端の関数ごとではなくコンテナをゲートします。これにより、個々の関数は問題ないクリーンなファイルが、ファイル全体の合計だけで加算型の制限に引っかかることはなくなりました — かつて bca: suppress-file マーカーが覆い隠していた偽陽性です。
loc サブメトリクスの素の bca diff --metric 表記は、ドット区切り形式のエイリアスとして受け付けられます(sloc は loc.sloc と等価で、ploc/lloc/cloc/blank も同様)。そのため diff 実行からコピーした名前は正しくゲートされます。単一のしきい値スカラーを持たない素のファミリー名(halstead、mi)はあいまいなため拒否され、具体的なサブメトリクスを列挙する「もしかして」ヒントが表示されます — いずれか 1 つ(例:halstead.volume)を選んでください。
The two spellings name one metric, so they override each other wherever limits merge: a [thresholds.lang.c] ploc = 100 replaces the global "loc.ploc", and --threshold ploc=100 replaces either. Within a single table, writing both is an error rather than a silent winner — set the metric once, under whichever spelling you prefer.
Per-language limits ([thresholds.lang.<slug>])
Metric distributions vary by language more than by project — the measured 97.5th-percentile per-function cognitive value runs from 4 in C# to 50 in C. A [thresholds.lang.<slug>] table gives one language its own limits, layered over the project-wide table:
[thresholds]
cognitive = 15
cyclomatic = 15
"loc.ploc" = 600
[thresholds.lang.c]
cognitive = 30
"loc.ploc" = 1200
[thresholds.lang.elixir]
nom = 150
wmc = 300
C is now gated at cognitive = 30 and loc.ploc = 1200 while keeping the project's cyclomatic = 15; every other language keeps all three. The override is per metric, not a replacement table — a language inherits every limit it does not restate. Which numbers to change, and the two cases where an override is a correction rather than tuning, are in Choosing thresholds.
The key is the canonical language slug, the same vocabulary --language accepts: rust, python, cpp, csharp, objc, tsx, mozcpp, mozjs, and so on — bca check --language nonsense prints the full list. Two rules point in opposite directions here:
- An unknown slug in the manifest is a tool error (exit
1) with a did-you-mean hint. A typo'd[thresholds.lang.rust-lang]must not silently leave a gate at the project limit while the author believes it was loosened. - An unrecognised file language falls through to the global table, as does any language with no override of its own. Nothing is silently ungated. (A file whose extension maps to no grammar at all is skipped by the walk before the gate ever sees it — with a warning if you named it explicitly.)
--threshold on the command line stays global and still applies last and absolutely: it overrides the project table and every per-language one, so a limit you type is the limit that runs.
--print-effective-config prints one fully resolved table per overridden language — inherited limits included, not a diff — so the number that will actually fire is the number you read:
[thresholds]
cognitive = 15.0
cyclomatic = 15.0
[thresholds.lang.c]
cognitive = 30.0
cyclomatic = 15.0
二層しきい値(--tier)
--tier <hard|soft|soft=RATIO> selects which threshold tier the gate compares against. hard (the default) uses the [thresholds] table verbatim; soft is an early-warning tier that fires before the hard gate, tightening every limit by RATIO. A bare --tier means soft; soft alone uses the default ratio 0.95; soft=0.90 pins the ratio to 0.90; soft=1.0 disables the blanket scale.
RATIO scales the band, not the number. For most metrics a limit is a ceiling, so tightening it means multiplying: cognitive = 15 with soft=0.9 warns at 13.5. For the lower-is-worse mi.* family a limit is a floor — a value below it is the violation — so the same 0.9 divides: "mi.original" = 20 warns at 20 / 0.9 = 22.2223, rounded up so the band never resolves below the exact quotient. Multiplying a floor would move the warning to 18, under the hard gate, where nothing could reach it first.
[thresholds.soft] テーブルはメトリクスごとのソフト制限を設定します。各値は絶対値の数値か、そのメトリクスのハード制限をスケールする "<ratio>x" 文字列のいずれかです。
[thresholds]
cognitive = 25
cyclomatic = 15
nargs = 7
[thresholds.soft]
cognitive = 22 # 絶対値のソフト制限
cyclomatic = "0.9x" # ハード制限の 90% → 13.5
# nargs は未指定 → ソフト層はハード制限を継承(ソフト帯なし)
bca check --paths src/ --tier=soft
ソフト層は次の固定順序で解決されます。
[thresholds](bca.tomlマニフェスト、--configとマージ済み)から始めます。[thresholds.soft]テーブルが存在する場合は、その上書きを上にマージします。そこに無いメトリクスはハード制限を継承します。一括のRATIOは適用されません(明示的なメトリクスごとの制限が優先されます)。- Otherwise tighten every limit by the soft
RATIO(default0.95for a baresoft;soft=1.0disables scaling). - 繰り返し指定された
--threshold name=valueフラグは最後に、絶対値として適用されます。
Steps 1 to 3 run once per language, against that language's own resolved hard limits. There is no [thresholds.lang.<slug>].soft table and none is needed: with [thresholds] cognitive = 15, [thresholds.lang.c] cognitive = 30, and --tier=soft=0.9, C's soft band is 27 — nine tenths of its limit, and the ceiling a C offender is measured against for the exit-5 escalation is 30, not 15. Derive either from the project's 15 and every C function between 15 and 30 reports "also breaches the hard limit" while sitting inside the limit the project configured for it.
The one combination that can invert the two tiers is an absolute [thresholds.soft] value looser than the hard limit it shadows — [thresholds.soft] cognitive = 12 alongside [thresholds.lang.csharp] cognitive = 4, or an mi.* soft floor below its hard floor. That is a tool error (exit 1) naming the offending table, for the same reason a "<ratio>x" factor above 1 is rejected at parse time: a soft tier that fires after the hard gate is never the intent.
ソフト RATIO(および "<ratio>x" 文字列のスケール係数)は (0, 1] の範囲でなければなりません。[check] headroom マニフェストキーは、素の --tier=soft に比率を供給します。非推奨の --headroom <R> フラグは --tier=soft=<R> の 1 サイクル限りのエイリアスで(警告を出し、次のメジャーで削除されます)、現在はハード実行をソフト層に昇格させます。両方の層は同じ --baseline を通じてラチェットし、--print-effective-config はマージ後の制限と併せて解決後の tier を報告します。移行のヒントと背景についてはローカルしきい値ゲートレシピを参照してください。
Previewing a candidate limit (--explain-threshold)
--explain-threshold <metric>=<limit> reports what a candidate limit would cost — at both tiers — instead of gating. It is repeatable, takes one candidate per metric, and writes nothing.
$ bca check --explain-threshold nargs=6
nargs: candidate limit 6
hard tier (limit 6): 60 offenders, 60 already baselined, 0 new
soft tier (limit 5.7, 0.95x): 135 offenders, 61 already baselined, 74 new
cluster: 75 of 75 soft-band offenders sit at exactly 6 — the candidate
limit itself. The soft tier measures distance to the limit, so a limit
of 6 places them inside the 5.7 band by construction and none of them
can clear it without real work.
The new column is the figure to weigh: it is how many baseline entries adopting the limit would add. Here the hard tier reads as free and the soft tier costs 74 entries, none of which any amount of tidying can retire — which is the whole reason the flag exists.
--threshold nargs=6 cannot tell you this. Its limits are applied last and absolutely, never scaled, so a candidate trialled that way has no soft tier at all. Passing both flags for the same metric is rejected rather than silently resolved.
The soft limit is derived from the candidate exactly as a real run would derive it: a [thresholds.soft] entry for the metric wins, then the --tier=soft=RATIO ratio if one was given, then 0.95. A [thresholds.lang.<slug>] table that overrides the metric keeps its own limit — a candidate global limit does not reach it — and the report says so on its own line.
Everything else matches the run being predicted: exclude_tests, [check] exclude, in-source suppression markers, --changed-only, and the baseline all apply as usual. The one difference is that baseline-covered offenders are counted rather than dropped, which is what makes the already baselined / new split possible.
The preview replaces the gate, so it never fails: the exit code is 0 unless a tool error (exit 1) stops the run, and a one-line reminder of that goes to stderr. It conflicts with --write-baseline, --print-effective-config, --report-format, and --output, each of which would produce a second, different artifact.
See Choosing thresholds for the rule this flag exists to make visible.
違反の出力
Every offending (function, metric) pair prints one line to stdout in this stable format:
<path>:<start_line>-<end_line>: <function_name>: <metric> = <value> (limit <limit>)
例:
src/parser.rs:42-117: parse_expression: cyclomatic = 22 (limit 15)
src/parser.rs:42-117: parse_expression: cognitive = 31 (limit 20)
行はパス、開始行、メトリクス名の順でソートされるため、同じツリーに対する実行間で出力は決定的です。
Which stream
The offender rows are the command's product, so they go to stdout: bca check | wc -l, | head, | rg -c and 2>/dev/null all reach them.
Everything the run says about itself goes to stderr:
- the per-file
--- summary ---footer, - the
--- next steps ---remediation block, - the GitHub Actions
::errorannotations, - the
bca: skipped N violations via [check.exclude]andbca: filtered N violations via baselinecounts, - every
warning:anderror:diagnostic.
One combination inverts this. --report-format <dialect> without --output puts the aggregated SARIF / Checkstyle / Code Climate document on stdout, so the human rows fall back to stderr rather than corrupting it:
bca check --report-format sarif | jq '.runs[0].results | length' # document on stdout
bca check --report-format sarif --output report.sarif | wc -l # rows back on stdout
The --summary-file digest is a file, not a stream, and appears on neither.
Earlier releases sent the rows to stderr along with everything else, which made | wc -l and 2>/dev/null report an empty offender list — indistinguishable from a clean tree. A pipeline that reads the rows through 2>&1 needs no change; one that captured them with 2>file should now use >file.
抑制マーカーによる違反の抑制
ソース内コメントによって、問題のコードを編集したりウォークから除外したりせずに、個々の関数またはファイル全体のしきい値違反を抑制できます。ネイティブの記法は bca: suppress / bca: suppress-file で、Lizard の #lizard forgives は互換シムとして認識されます。完全なリファレンスと --no-suppress CI 監査フラグについては抑制マーカーを参照してください。
ファイルカテゴリ全体の除外([check.exclude])
一部のファイルは分析・報告はするがゲートは決してしない扱いにすべきです。意図的に cognitive/cyclomatic を超過させるテストフィクスチャ、生成されたバインディング、複雑さが構造的で決して「修正」されないマクロディスパッチモジュールなどです。これらを .bcaignore に入れるのは大雑把すぎます — ウォークから完全に除外されるため、bca report からも消えてしまいます。ベースラインに登録するのも誤りです — これらは返済中の負債ではなく、ベースラインの diff を永遠にかき乱すことになります。
[check.exclude] はグロブレベルの中間手段です。マッチしたファイルはウォークされ、パースされ、メトリクスが計算され、bca report にも表示されますが、bca check は違反を出力する前、かつ --write-baseline が何かを記録する前にそれらの違反を破棄するため、構造的な除外対象は .bca-baseline.toml の外に保たれます。
bca.toml の場合:
[check]
exclude = [
"tests/**",
"src/languages/language_*.rs",
"xtask/**",
]
またはコマンドラインで指定します(--check-exclude は繰り返し指定可能で、--check-exclude-from と結合されます)。
bca check --check-exclude "tests/**" --check-exclude "xtask/**"
bca check --check-exclude-from .bcacheckignore
--check-exclude-from は .gitignore 形式のファイルを読み込みます(空行と # コメントはスキップ)。慣例的な名前は、ウォーカー用の .bcaignore に対応する .bcacheckignore です。グロブは、ウォーカーが --exclude でマッチさせたのと全く同じパスにマッチします。「負のフィルター」キーであるため、明示的な --check-exclude リストはマニフェストの [check] exclude リストと結合されます(置き換えではありません)。CLI の除外はプロジェクトの除外に追加されるのであって置き換えではないため、マニフェストが意図的に除外したパスを誤って再ゲートすることはできません。重複はまとめられ、CLI のパターンが先にソートされます。マニフェストの除外を完全に外すには --no-config を渡します。(paths / include のような正のスコープキーは CLI では引き続き「置き換え」です — マージされるのは exclude フィルターだけです。)
他の抑制メカニズムとの優先順位
bca check は、最も特異的なものから順に、次の順序で除外を解決します。
- ソース内マーカー(
bca: suppress/bca: suppress-file)— 常に優先されます。ウォーク中に適用されるため、その関数はそもそも違反になりません。 [check.exclude]グロブ — ファイルの「カテゴリ」(テスト、生成コード)を除外します。.bca-baseline.toml— 返済中の既知の違反。
--print-effective-config は、他のゲート入力と併せて解決後の check_exclude グロブを報告します。
ベースライン
既存のコードベースにしきい値を導入するとき、通常は「何も発火しなくなるまで制限を引き上げる」か「ゲートを有効にする前にすべての違反を修正する」かの二者択一に直面します。ベースラインファイルはラチェットダウン式の代替手段です。今日の違反を記録し、退行と新規違反のみを失敗させ、チームが負債を返済するにつれてファイルを縮小していきます。
ベースラインは、抑制マーカーの抑制マーカーを補完するものであり、代替ではありません。抑制は「この関数は意図的に永久に除外する」を表現し、ソースに置かれます。ベースラインは「これは返済中の技術的負債である」を表現し、コミットされた TOML ファイルに置かれます。bca check はまず抑制を尊重し、残ったものにベースラインフィルターを適用します。
ベースラインの書き出し
bca check --paths src/ \
--write-baseline .bca-baseline.toml
これはツリーをウォークし、そのままではチェックを失敗させるすべてのしきい値違反を捕捉して、ソート済み TOML としてファイルに書き出します。この実行は違反数に関係なく 0 で終了します — 目的は違反を捕捉することだからです。
# bca baseline file. Generated by `bca check --write-baseline`.
# Listed offenders are filtered from threshold checks; a function that
# gets worse than its recorded value still fails. Refresh with
# `--write-baseline` when entries become stale.
version = 6
[provenance]
tier = "hard"
[[entry]]
path = "src/parser.rs"
qualified = "Parser::parse_expression"
metric = "cyclomatic"
value = 22.0
The qualified field is the function's qualified symbol (the ::-joined chain of enclosing named containers plus the function name). An entry carries a start_line only when its (path, qualified, metric) identity is shared with another entry, the one case matching consults a line number; recording it elsewhere would only rewrite the file every time an edit above the function shifted it. With --baseline-fuzzy-match, each entry also carries a body_hash for rename-tolerant matching.
ソース内の抑制マーカーで既にカバーされている関数は除外されます。すべての違反を記録するには、--write-baseline と一緒に --no-suppress を渡します(CI 監査フロー)。
--write-baseline は --baseline、--report-format、--output、--since、--changed-only と組み合わせられません — ベースラインファイルが出力「そのもの」だからです。
ベースラインの読み込み
bca check --paths src/ \
--baseline .bca-baseline.toml
違反は、次の両方の条件が成り立つときに抑制されます。
- エントリが
(path, qualified_symbol, metric)で一致する — 行番号には依存しません — か、それで一致しない場合は--baseline-fuzzy-match指定時にボディハッシュで一致する。(完全な解決順序はベースラインレシピを参照してください。) - 現在の
valueが記録された値以下である。
ベースライン値より悪化した関数は引き続き失敗します。ベースラインに載っていない新規違反も引き続き失敗します。改善は黙って通過します(エントリは次の --write-baseline による更新まで、古い高めの値のまま残ります)。
存在しない、空である、version が欠落もしくは未対応である、またはパースに失敗するベースラインファイルはツールエラー(終了コード 1)であり、黙ってゼロマッチ扱いにはなりません。
パスキーはベースラインファイル自身のディレクトリ(アンカー)を基準に正規化されるため、--paths .、--paths src/、--paths "$PWD" はバイト単位で同一のベースラインを生成し、どの --paths 形式でファイルを生成したかに関係なく --baseline 実行はマッチします — --write-baseline を再実行せずに自由に切り替えられます。
制限事項
- あいまいなシンボル / 無名関数。 エントリは修飾シンボルをキーとするため、「名前付き」関数の上にコードを挿入してもキーは変わらなくなりました。例外は、同じ修飾シンボルを共有する関数が
--baseline-line-toleranceを超えて離れた場合と、無名のクロージャ / ラムダ(合成シンボルに行番号が埋め込まれます)です。どちらも移動すると「新規」として再キーされます。--write-baselineで更新してください。 - OS 間の可搬性。 パスはスラッシュ区切りで保存されるため、ある OS で書かれたベースラインは別の OS 上の同じツリーにマッチします。有効な UTF-8 でないパスは損失のある表示形式(U+FFFD 置換)にフォールバックし、正確にラウンドトリップしない場合があります。
エンドツーエンドの導入フローと CI 統合パターンについては、ベースラインレシピを参照してください。
失敗させずに報告する
--no-fail reports offenders as usual but exits 0. Useful while adopting baselines without flipping CI red. Other CI tools call this behavior --report-only or --soft-fail; here the flag is spelled --no-fail.
bca check --paths src/ --no-fail
対処しやすい失敗出力
bca check が失敗したとき、5 つのフラグが失敗ストリームを整形し、CI ログを流し読みする開発者が、何が引っかかったのか、PR のどこで引っかかったのか、次に何をすべきかを把握できるようにします。各フラグは独立しており、存在する場合はいずれも GitHub Actions の環境変数から自動検出されるため、一般的な CI のケースでは明示的な設定は不要です。
| フラグ | 効果 | 自動検出する環境変数 |
|---|---|---|
--since <ref> | ファイル別フッターを "Files in this range" と "Other offenders" に分割 | BCA_DIFF_BASE、GITHUB_BASE_REF、GITHUB_EVENT_BEFORE |
--changed-only | diff スコープ外の違反を完全に除外 | 解決可能なベースが必要(--since または上記のいずれか) |
--github-annotations <auto\|always\|never> | インラインのファイル注釈のために ::error file=…::msg ワークフローコマンドを出力(フラグ単独指定 = always) | auto は GITHUB_ACTIONS == "true" を検出 |
--summary-file <path\|auto\|never> | Markdown ダイジェスト(ファイル別集計 + 内訳 + 違反トップ 10)を追記。never で抑止 | auto は GITHUB_STEP_SUMMARY を検出 |
--no-remediation | 末尾の --- next steps --- ブロックを抑止 | このフラグを渡さない限り、失敗時にブロックを出力 |
The per-violation rows and the per-file rollup footer remain unchanged in content when none of the above are active, so CI tooling that grep-anchors on the legacy text keeps working — but see Which stream for where each now lands.
実例については CI 統合レシピを参照してください — 5 つすべてを 1 つのステップにまとめる「全部まとめて」の GHA スニペットも含まれます。また、修復ブロックがリンクする --write-baseline 更新フローについてはベースラインレシピを参照してください。
diff ベース自動検出の優先順位
--since が省略された場合、bca は次の順序で環境変数を参照します。
BCA_DIFF_BASE— ローカルシェルや GHA 以外の CI ランナー向けの明示的なオーバーライド手段。GITHUB_BASE_REF—pull_requestイベントで GHA が設定します。origin/<value>に展開されます。対応するgit fetchはランナーの責任です(actions/checkoutのfetch-depth: 0)。GITHUB_EVENT_BEFORE—pushイベントで GHA が、プッシュ前の HEAD の SHA に設定します。すべてゼロのセンチネル(強制プッシュ、新規ブランチ)はシグナルなしとして扱われます。
ベースの解決に失敗しても致命的ではありません。ただし --changed-only が渡されている場合は別で、その場合ゲートは失敗します — 誤設定されたベースの下ですべての違反を黙って抑制することは、この機能が防ごうとしている最悪の失敗モードだからです。--write-baseline も --since / --changed-only と競合します(部分的なベースラインは、次のフルツリー実行で diff スコープ外のすべての違反を黙って覆い隠してしまいます)。
CI の例(GitHub Actions)
- name: Check code complexity thresholds
run: |
bca check
# しきい値とパスは、リポジトリルートで自動検出される `bca.toml`
# マニフェストから取得されます。デフォルトの挙動 — 非ゼロ終了で
# ステップが失敗する — はまさにここで求めるものです。追加の配線は不要です。
違反数を減らしている間、ジョブをグリーンに保ちつつ違反をビルド注釈として表示したい場合は、--no-fail に置き換えます。
- name: Surface complexity hot spots (non-blocking)
run: |
bca check --paths src/ --no-fail
違反レコードのエクスポート
bca check は、ウォーク内のすべての違反を網羅する単一の CI/IDE 向けドキュメントも出力します。--report-format <fmt> で形式を選び、--output <file> でディスクに書き出します(省略時は stdout)。--format、-O、--output-format の綴りは非推奨のエイリアスとして受け付けられますが、将来のリリースで削除されます。終了コードの契約はこれらのフラグの影響を受けません。クリーンなら 0、違反があれば 2(--no-fail 時を除く)、ツールエラーなら 1 です。
--report-format なしで --output が指定された場合、フォーマットは出力ファイルの拡張子から推定されます。.sarif は sarif を、.xml は checkstyle を選択します。一意なフォーマットに対応しない拡張子(特に sarif と code-climate の両方が生成する .json)や拡張子なしの場合は、--report-format を挙げる使用方法エラー(終了コード 1)になります — 明示的な --output が黙って無視されることは決してありません。明示的な --report-format は常に拡張子より優先されます。
| フォーマット | 対象 |
|---|---|
checkstyle | Jenkins、SonarQube、GitLab、"warnings plugin" 系 CI |
sarif | GitHub Code Scanning、モダンな IDE / セキュリティツール |
code-climate | GitLab MR の Code Quality ウィジェット |
clang-warning | エディタの quickfix パーサー、GitHub Actions の problem matcher |
msvc-warning | Visual Studio、VS Code、Windows CI ランナー |
違反が存在しない場合、ライターは整形式だが空のドキュメントを出力します — SARIF では空の runs[].results 配列、Code Climate では空の JSON 配列([])、Checkstyle では <checkstyle> ルート直下に <file> 子要素なし、2 つの警告行フォーマットでは 0 バイト — これにより CI コンシューマーは違反のない実行結果もそのまま取り込めます。
Checkstyle(CI 統合)
bca check --paths src/ \
--threshold cyclomatic=15 \
--report-format checkstyle \
--output report.checkstyle.xml
Checkstyle ライターは、ソースパスごとに 1 つの <file> 要素を含む単一の <checkstyle version="4.3"> ドキュメントを出力し、各 <file> はメトリクスしきい値違反ごとに 1 つの <error> を保持します。スキーマは Checkstyle 4.3 の XSD で、Jenkins および SonarQube の「Warnings Next Generation」/「Generic Issue」インポーターが直接取り込めます。
SARIF(GitHub Code Scanning)
bca check --paths src/ \
--threshold cyclomatic=15 \
--report-format sarif \
--output report.sarif.json
SARIF ライターは、1 つの runs[] 要素を持つ単一の SARIF 2.1.0 JSON ドキュメントを出力します。各メトリクスしきい値違反は runs[0].results[] 配下の result になり、実行結果に現れるメトリクス名は重複排除されて、短い説明とともに runs[0].tool.driver.rules[] に格納されます。
ワークフローから GitHub Code Scanning に SARIF ファイルをアップロードするには:
name: bca-sarif
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- name: Run big-code-analysis
run: |
bca check --paths . \
--report-format sarif \
--output report.sarif.json \
--no-fail
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: report.sarif.json
--no-fail はジョブを成功(グリーン)のまま保つため、違反が存在しても SARIF アップロードステップは実行されます。メトリクスの悪化でワークフローを失敗させたくなったら外してください。
GitLab Code Quality(Code Climate JSON)
bca check --paths src/ \
--threshold cyclomatic=15 \
--report-format code-climate \
--output gl-code-quality-report.json
Code Climate ライターは、上流の Code Climate エンジン仕様に対する GitLab の厳密なサブセットに適合する issue オブジェクトの単一 JSON 配列を出力します — メトリクスしきい値違反ごとに 1 エントリ、バイトオーダーマークなし、末尾の改行 1 つ(入力が空の場合は []\n になります)。各 issue には、名前空間付きの check_name(big-code-analysis/<metric>)、path \0 function \0 metric に対する安定した SHA-256 の fingerprint(行番号にも値にも依存しないため、体裁だけの編集でも MR ウィジェットでの重複排除が維持されます)、そして値としきい値の比率を GitLab の 5 段階 enum に対応付けた severity が含まれます: ≤ 1.5× → minor、≤ 2× → major、≤ 4× → critical、> 4× → blocker(値が低いほど悪い mi.* ファミリーでは反転)。enum の全体は info/minor/major/critical/blocker ですが、bca が info を出力することはありません — しきい値違反は常に minor 以上になります。
アーティファクトを GitLab の MR Code Quality ウィジェットに接続するには:
code_quality:
stage: quality
script:
- bca check --paths "$CI_PROJECT_DIR"
--report-format code-climate
--output gl-code-quality-report.json
--no-fail
artifacts:
when: always
reports:
codequality: gl-code-quality-report.json
paths:
- gl-code-quality-report.json
パイプライン全体(Code Climate + Checkstyle + Markdown レポートの組み合わせ)とローカルでの jq によるスモークチェックについては、GitLab Code Quality ウィジェットのレシピを参照してください。
--no-fail はジョブを成功(グリーン)のまま保つため、違反が存在しても Code Quality レポートはアップロードされます。メトリクスの悪化でパイプラインを失敗させたくなったら外してください。
Clang/GCC 警告行(エディタの quickfix と CI アノテーター)
bca check --paths src/ \
--threshold cyclomatic=15 \
--report-format clang-warning \
--output report.txt
Clang フォーマットは、慣例的なコンパイラ警告の形式で違反を 1 行に 1 件ずつ出力します:
path/to/file.rs:42:5: warning: cyclomatic 17 exceeds limit 15 [big-code-analysis-cyclomatic]
これは clang -fdiagnostics-format= が生成するフォーマットであり、あらゆるエディタの quickfix パーサー(VS Code、IntelliJ、Vim)と大半の CI アノテーターが設定なしで解釈できる形式です。
GitHub Actions は、組み込みの GCC problem matcher(または任意のコミュニティ製 compiler-problem-matchers アクション)を介して、これらの行を PR の差分上のインラインアノテーションとして表示します:
name: bca-clang-warnings
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable GCC problem matcher
run: echo "::add-matcher::$RUNNER_TOOL_CACHE/problem-matchers/gcc.json"
- name: Run big-code-analysis
run: |
bca check --paths . \
--report-format clang-warning \
--no-fail
ランナーに GCC matcher が同梱されていない場合は、これらの行をストリーム処理して ::warning file=...,line=...:: ワークフローコマンドとして再出力する方法にフォールバックしてください。
MSVC 警告行(Visual Studio と Windows CI)
bca check --paths src/ \
--threshold cyclomatic=15 \
--report-format msvc-warning \
--output report.txt
MSVC フォーマットは、Visual Studio の cl.exe 診断形式で違反を 1 行に 1 件ずつ出力します:
path\to\file.rs(42,5): warning : cyclomatic 17 exceeds limit 15
warning/error の後のコロンの前にスペースがある点に注意してください — これが MSVC の慣例です。Windows ではパスは \ 区切りに正規化されます(cl.exe の出力に一致)。それ以外のプラットフォームではパスはそのまま出力されます。Visual Studio、C/C++ 拡張機能を入れた VS Code、および Windows CI ランナー(Azure Pipelines、windows-latest 上の GitHub Actions)は、追加設定なしでこれらをインラインで解釈します。
抑制マーカー
ソース内抑制マーカーは、違反している関数を編集したり、ファイルを走査から除外したりすることなく、しきい値違反を沈黙させます。ソースファイル内の任意のコメントにマーカーを置くと、bca check は対象のメトリクスがそのスコープでは制限内にあるものとして扱います。メトリクスの計算自体には影響しません — 生の bca metrics 出力は引き続きすべての数値を報告します。抑制は測定結果の表示に関する仕組みです: bca check は対象の違反をゲートから除外し、bca report markdown|html はデフォルトで該当するホットスポットテーブルから対象の関数を省略します(生の監査ビューが必要な場合は bca report --no-suppress を渡してください — report を参照)。
マーカーは、コードの編集が選択肢にならないケースのために存在します: 書き直し待ちの生成コード的なレガシーモジュール、コメントに文書化された承認済みの例外、そして Lizard の #lizard forgives 慣例からの移行です。
ネイティブマーカー(bca:)
ネイティブ方言は bca: 名前空間と suppress 動詞を使用し、プロジェクト内部の「suppression(抑制)」語彙(SuppressionPolicy、FuncSpace::suppressed、--no-suppress)と一致しています。形式は 4 つあります:
| マーカー | スコープ | 効果 |
|---|---|---|
bca: suppress | 囲んでいる関数 | すべてのメトリクスを抑制 |
bca: suppress(metric, ...) | 囲んでいる関数 | 列挙したメトリクスのみを抑制 |
bca: suppress-file | ファイル | すべてのメトリクスを抑制 |
bca: suppress-file(metric, ...) | ファイル | 列挙したメトリクスのみを抑制 |
The two metric-list forms may carry a rationale — free text on the same line, after the marker itself:
#![allow(unused)] fn main() { // bca: suppress(nargs) — threaded context, not a god-function }
No separator is required and none is privileged: — why, - why, : why, // why, and bare prose all read the same. The two bare verbs take no trailing text at all — bca: suppress see #123 is not a marker, whatever punctuation opens the trailing words. See Rationale text for why.
関数スコープのマーカーは、そのコメントをソース範囲に含む最も内側の FuncSpace(FuncSpace の rustdoc を参照)に付与されます。どの関数本体にも含まれない関数スコープのマーカーは黙って無視されます。ファイル全体を沈黙させるには、明示的な suppress-file 動詞を使用してください。ファイルスコープのマーカーはソース内のどこに書いても構いません — 「先頭 N 行以内に書かなければならない」といった規則はありません。
bca: suppress — 関数スコープ、全メトリクス(Rust)
#![allow(unused)] fn main() { // bca: suppress fn legacy_dispatch(opcode: u8) -> Action { // サポートされる全オペコードに対する高密度な match match opcode { /* ... */ } } }
bca: suppress(metric, ...) — 関数スコープ、列挙したメトリクス(Python)
def parse_token_stream(tokens):
# bca: suppress(cognitive)
# 認知的複雑度はこのステートマシンに本質的なもの。
# 循環的複雑度は依然として制限内に収まっている。
...
その他のしきい値(cyclomatic、halstead、loc など)は引き続き適用されます。
bca: suppress-file — ファイルスコープ、全メトリクス(JavaScript)
// bca: suppress-file
// 手動チューニングされたホットパス。しきい値を満たすための書き換えはしないこと。
function transform(input) { /* ... */ }
function validate(input) { /* ... */ }
bca: suppress-file(metric, ...) — ファイルスコープ、列挙したメトリクス(C++)
/* bca: suppress-file(halstead) */
// Halstead の volume は下の生成テーブルによって膨らんでいる。
// それ以外のメトリクスはファイル全体で引き続き適用される。
まずはより狭いツールを検討してください。 しきい値スコープ(#969)以降、メトリクスのファイル全体または
impl全体の 集計値 が関数ごとの制限として発火することはなくなったため、suppress-fileに手が伸びる最も一般的な理由 — ファイルレベルのhalstead/nargs/nexits/nomの合計を黙らせること — はなくなりました。suppress-fileは、ファイル内の すべての 関数についてそのメトリクスを本当に沈黙させたい場合にのみ使ってください。どうしても複雑にならざるを得ない関数を 1 つだけ除外するには、その内部に関数スコープのbca: suppress(...)を書きます。将来の悪化に対してゲートを盲目にせずに既存の違反を容認するには、ベースラインエントリを推奨します。ベースラインは、関数が記録された値より 悪化 した時点で再び発火します。
Lizard 互換マーカー
既存の Lizard 対応コードベースを書き換えずに済むよう、2 つの Lizard 形式マーカーがそのまま認識されます:
| Lizard マーカー | スコープ | 対応するネイティブマーカー |
|---|---|---|
#lizard forgives | 囲んでいる関数 | bca: suppress |
#lizard forgive global | ファイル | bca: suppress-file |
互換レイヤーは意図的に狭くしてあります: 受け付けるのはこの 2 つの形だけです。その他の Lizard ディレクティブは通常のコメントとして解釈されます。Lizard にはメトリクス単位のスコープ指定がないため、ネイティブ形式の bca: suppress(metric, ...) リストに相当するものは Lizard にはありません — Lizard 形式のマーカーはすべてのメトリクスを沈黙させます。
Lizard の GENERATED CODE マーカーはここでは扱いません。これは生成コードの自動スキップ機構の一部です(生成コードのスキップと --no-skip-generated フラグを参照)。
ネイティブと Lizard の対照表
| 効果 | ネイティブ形式 | Lizard 形式 |
|---|---|---|
| 1 つの関数のすべてのメトリクスを沈黙させる | // bca: suppress | // #lizard forgives |
| 1 つの関数の 1 つのメトリクスを沈黙させる | // bca: suppress(cyclomatic) | (該当なし) |
| ファイル全体のすべてのメトリクスを沈黙させる | // bca: suppress-file | // #lizard forgive global |
| ファイル全体の 1 つのメトリクスを沈黙させる | // bca: suppress-file(halstead) | (該当なし) |
メトリクス識別子
bca: suppress(...) と bca: suppress-file(...) の内側で受け付けられる識別子は次のとおりです:
abc, cognitive, cyclomatic, halstead, loc, mi, nargs, nexits, nom, npa, npm, wmc.
これらは、しきい値名および CodeMetrics に出力される JSON フィールド名と一致しますが、意図的な除外が 1 つあります:
nexitsis the canonical spelling —bca: suppress(nexits)silences anexitsthreshold violation. The legacyexitalias was retired in #555 and is no longer accepted; spelling itexitis an unknown identifier, which warns and is skipped — the recognized names beside it still suppress (see below).tokensはしきい値チェック可能なメトリクス(かつCodeMetricsの JSON フィールド)ですが、意図的に抑制リストから外されています: マーカーで無効化することはできません。tokensは保守性のヒューリスティックではなく、ハードなリソース上限として扱ってください。
ファミリー(例えば halstead)を沈黙させると、その配下のすべてのサブメトリクスしきい値(halstead.volume、halstead.effort など)が対象になります。抑制の語彙にドット付きの形式はありません。
bca: suppress(...) リスト内の未知の識別子は、次の形式の stderr 警告を出力します
warning: path/to/file.rs:42: unknown metric 'no_such_metric' in bca suppression marker; known metrics: abc, cognitive, ...
Only the offending identifier is dropped: bca: suppress(cognitive, exit) still silences cognitive. Skipping can only ever shrink what a marker covers, so a typo never widens scope to a metric the author did not name — while voiding the whole marker, which is what earlier releases did, produced the opposite hazard: a suppression its author believed was active and the gate did not.
Unknown verbs (anything other than suppress / suppress-file) and bodies that parse to no directive at all (an unbalanced parenthesis, a bare verb followed by any trailing text) produce the same shape of warning, and those do void the marker — there is nothing left of it to honour. None of these are fatal: a typo in one file does not derail a workspace walk, and a doc comment that merely mentions the syntax does not fail your gate.
Rationale text
Text after a metric list is yours; bca parses past it and does not interpret it. The asymmetry between the two forms is deliberate:
-
After a metric list —
bca: suppress(nargs) threaded context— anything goes. The parentheses are a positive signal that this comment is a marker, and the list has already stated the intent unambiguously, so whatever follows is prose. -
After a bare verb —
bca: suppress — irreducible dispatch— there is no trailing text. Nothing in this shape separates a rationale from a sentence about the feature, and no separator can:-,:,//,#and the dashes are exactly the punctuation someone writing// bca: suppress - we removed this marker, see #123reaches for. Accepting them silences every metric in the enclosing function on the strength of a comment, so the whole shape warns instead.If you want to record a reason, name the metrics —
bca: suppress(cognitive, cyclomatic) — reason— which is more precise than theAllhammer anyway. IfAllreally is what you mean, put the reason on the line above the marker.
マーカーを書ける場所
マーカーは、コメントスタイルを問わず、任意のソースコメント内で認識されます。スキャナーはマッチングの前に、先頭の区切り文字 /、*、!、#、;、- と ASCII 空白を取り除きます。これは bca が現在パースするすべてのコメント形式をカバーします:
- C 系の行コメント:
// bca: suppress - C 系のブロックコメント:
/* bca: suppress */ - Rust の内部ドキュメントコメント:
//! bca: suppressと/*! bca: suppress */ - Python / シェル / Ruby / Perl の
#コメント:# bca: suppress - Lisp / Lua / SQL の行コメント:
;; bca: suppress、-- bca: suppress
関数スコープのマーカーは、コメントの行を (start_line..=end_line) 範囲に含む最も内側の Function 種別の FuncSpace に付与されます。クラスや構造体の本体内にあってもどのメソッドにも含まれないマーカーは黙って無視されます — クラス全体を沈黙させるには bca: suppress-file を使うか、各メソッドでマーカーを繰り返してください。
ファイルスコープのマーカーはトップレベルの Unit スペースにマージされ、ネストに関係なくファイル内のすべての関数に適用されます。
マーカーはコメントの先頭近くに配置してください。スキャナーは両端の区切り文字を取り除いた後、先頭に bca:(または #lizard)が来ることを期待します。複数行ブロックコメントの奥深くに埋もれたマーカーは認識されません。
--no-suppress(CI での監査)
bca check --no-suppress は、ネイティブと Lizard の両方を含むすべての抑制マーカーを無視し、走査内のすべてのしきい値違反を報告します。生の、沈黙させていない違反リストが必要な監査パイプラインで使用してください:
bca check --paths src/ --no-suppress
このフラグはメトリクスの値自体には影響しません: 生の bca metrics 出力は常にすべての数値を報告します。bca report markdown|html はデフォルトでホットスポットテーブルにおいてマーカーを尊重し、同じ生の監査ビューのために独自の --no-suppress フラグを受け付けます。
抑制された負債の可視化(--report-suppressed)
抑制は違反をゲートから外しますが、同時に --format ドキュメントからも外れます — そのため抑制されたモジュールはコードスキャンレポートから完全に消えます。bca check --report-suppressed は、それをアクティブではなく 抑制済み として書き戻します:
bca check --report-format sarif --no-fail --report-suppressed \
--tier=soft=0.95 --output bca.sarif
Offenders silenced by an in-source marker or covered by the baseline are emitted into the SARIF document with a SARIF suppressions entry — kind: "inSource" for markers, kind: "external" for the baseline. The suppression never fails the gate (exit code and the human offender rows are unaffected); the suppressions entry lets downstream tooling tell suppressed debt apart from active offenders.
GitHub Code Scanning に関する注意。 GitHub は SARIF の
suppressionsプロパティをネイティブには尊重しません — 抑制された結果はクローズ済みではなく オープン なアラートとして取り込まれます。Security タブでそれらを却下するには、advanced-security/dismiss-alertsアクションのような後続ステップが必要です。このアクションはsuppressions[]を読み取り、一致するアラートを却下します。アクティブな違反だけを表示したい場合は、アップロードから--report-suppressedを外してください(本リポジトリ自身の Pages ワークフローがまさにそうしています)。
注記:
- 抑制を表現できるのは SARIF フォーマットだけです。他の
--format値はこのフラグを無視し、アクティブな違反のみを出力します。 - ベースラインでカバーされた違反のうちハード制限を下回っているものも表示されるように、(ベースラインの由来に合わせて)
--tier=soft=0.95と併用してください。 - (マーカーの沈黙を解除して生の違反リストを表示する)
--no-suppressおよび--write-baselineとは相互排他です。
適用除外の監査(bca exemptions)
--no-suppress はマーカーが 沈黙させている 違反を表示しますが、マーカーそのものは表示しません。従来は、すべての抑制箇所を見つけるには --no-suppress 付きの実行結果と通常の実行結果を diff する必要がありました。bca exemptions はこの回避策に代わり、bca check ゲートがスキップするすべての項目を、3 つの適用除外ティアすべてにわたって 1 つのレポートで直接一覧表示します:
| ティア | 粒度 | ソース |
|---|---|---|
| ソース内マーカー | 関数単位 / ファイル単位 | bca: suppress、#lizard forgives、… |
[check.exclude] グロブ | グロブ単位(ファイルのカテゴリ) | bca.toml [check] exclude / --check-exclude |
| ベースラインエントリ | (path, symbol, metric) 単位 | .bca-baseline.toml |
# ツリー内のすべての適用除外を一覧表示します(ソース内マーカーは
# 他のすべての走査コマンドと同様に [walker.exclude] を尊重します)。
bca exemptions --paths src/
# In-source markers (2)
src/parser.rs:120 bca: suppress metrics=all parse_long
src/lib.rs:1 bca: suppress-file metrics=halstead (whole file)
# [check.exclude] globs (1)
tests/**
# Baseline (.bca-baseline.toml, 1 entry)
src/markdown_report.rs write_language_section cognitive 29
(関数スコープのマーカーの場合)囲んでいる関数がスコープの文脈を示します。ファイルスコープのマーカーは (whole file) と表示されます。また、関数の外に書かれた関数スコープのマーカー(何も抑制しません)は (no enclosing fn) と表示されるため、無効なマーカーも可視化されます。
フォーマットとセクションフィルタ
--format markdown は PR コメント向けのテーブルを出力します。--format json は、ダッシュボードや jq によるフィルタリングのために、3 つのティアすべてを単一の suppressions エンベロープの下にネストします:
bca exemptions --paths src/ --format json | jq '.suppressions.markers[] | select(.dialect == "lizard")'
JSON 形式では、省略されたセクション(--*-only フラグで要求されなかったもの)は null になり、要求されたが空だったセクションは [] になるため、フィルタは両者を区別できます。
相互排他的な --markers-only / --excludes-only / --baseline-only フラグは、PR ボットの特化(例:新規追加されたソース内マーカーにのみコメントするボット)のために、レポートを単一のティアに絞り込みます。ベースライン(bca.toml トップレベルの baseline)と [check.exclude]([check] exclude)の入力は、bca check が読み取るのと同じソースをデフォルトとするため、監査はゲートがスキップする内容を正確に反映します。ベースラインは --baseline <path> で上書きできます。
以前の --only-markers / --only-excludes / --only-baseline の綴りは、既存の PR ボットの呼び出しを動かし続けるため、1 リリースサイクルの間は非表示のエイリアスとして残ります。diff-baseline のセクションフィルタと一致する --<section>-only 形式を優先してください。
bca check と異なり、bca exemptions は情報提供用であり、成功時は常に終了コード 0 で終了します。これはレビューのための表示面であって、ゲートではありません。
PR レビュー中に bca exemptions を bca diff-baseline と併用する方法については、ベースラインのレシピ も参照してください。
JSON 出力
FuncSpace は、マージされた抑制スコープを JSON 出力のオプションフィールド suppressed として公開します。スペースに適用されるマーカーがない場合、このフィールドは省略されるため、既存のスナップショット利用者には変化がありません。マーカーが発動した場合、このフィールドは次の 2 つの形のいずれかになります:
{ "suppressed": { "kind": "all" } }
{ "suppressed": { "kind": "some", "metrics": ["cognitive", "loc"] } }
kind: all は素のマーカー(bca: suppress、bca: suppress-file、または任意の Lizard 形式のマーカー)に対応します。kind: some は bca: suppress(...) / bca: suppress-file(...) で指定された明示的なメトリクスのリストを保持します。どちらの形も、ダッシュボードや監査ログに適した安定したシリアライズ出力です。
Lizard からの移行
互換レイヤーがあるため、移行は段階的に行えます:
- 既存の
#lizard forgivesと#lizard forgive globalのマーカーは変更なしでそのまま機能します。bca checkは追加設定なしにそれらを尊重します。 - 機会を見てネイティブ形式に書き換えてください。
bca: suppress(...)はメトリクス単位のスコープ指定が可能で(Lizard 形式はすべてを抑制します)、将来の監査証跡機能が拡張していく形式です。
本プロジェクトは Lizard 互換レイヤーを無期限に維持します。削除予定日はありません。
予約済み構文
次の形は将来の使用のために予約されており、現時点ではパースされません:
bca: suppress-next— silence the immediately following declaration rather than the enclosing function. Rejected today as an unknown verb; it will be promoted to first-class behavior in a future release without breaking existing markers.
The reason = "..." argument this section used to reserve is no longer planned. Write the rationale after the metric list instead — see Rationale text — which is the spelling authors already reach for. Inside the parentheses, reason = "..." is still an unknown metric identifier and is skipped with a stderr warning.
ノード
bca は、ソースファイルの抽象構文木(AST) のノードに関する情報を分析・抽出するコマンドを提供します。
移行しますか? 以下の動詞は、再構成前のフラグによるアクション(
-d、-f、--countなど)を置き換えるものです。移行ガイド を参照してください。
エラー検出
コード内の構文エラーを検出するには、次を実行します:
bca find -t ERROR -I "*.ext" /path/to/your/file/or/directory
[PATHS].../-p, --paths:分析対象のファイルまたはディレクトリ(ディレクトリを指定するとすべてのファイルを分析します)。パスは位置引数または--pathsで指定でき、両者は和集合になります。フラグはサブコマンドの後に置きます。-t, --type:マッチさせるノード種別。複数の種別を指定するにはフラグを繰り返します(-t function_item -t struct_item)。少なくとも 1 つが必須です。文字列 値はノード種別名に正確にマッチします(例えばfunction_item)。純粋な 数値 は代わりに生の tree-sitterkind_idとして解釈され、内部シンボル ID がその数値に等しいノードにマッチします(したがって-t 0は末尾 /ERRORセンチネルにマッチします)。数値形式は文法検査のための緊急避難口であり、不安定です。kind_idは文法のシンボルテーブルへのインデックスであるため、文法バージョンのバンプ後には同じ数値が別のノードを指します。安定した名前を持たない種別がどうしても必要な場合を除き、文字列形式を優先してください。-I, --include:拡張子でファイルを選択するグロブフィルタ(例:*.js、*.rs)。各-Iはちょうど 1 つの値を取るため、後続の位置引数のパスが飲み込まれることはありません。
ノードのカウント
count コマンドで、1 つ以上のノード種別の出現回数をカウントします:
bca count -t <NODE_TYPE> [-t <NODE_TYPE>...] -I "*.ext" \
/path/to/your/file/or/directory
AST の表示
ソースファイルの AST を可視化するには、dump コマンドを使用します(明示的なパスが必須です。ツリー全体の AST ダンプが有用になることはないためです):
bca dump /path/to/your/file/or/directory
コードの一部の分析
コードの特定の部分だけを分析するには、dump サブコマンドの --line-start と --line-end オプションを使用します。例えば、5 行目から 10 行目までの単一の関数の AST を表示するには:
bca dump --line-start 5 --line-end 10 /path/to/your/file/or/directory
これらのフラグは dump と find に固有のものであるため、サブコマンドの後に置く必要があります。短い綴りの --ls / --le は非推奨のエイリアスとして引き続き機能しますが、次のメジャーバージョンで削除される予定です。
関数の一覧表示
すべての関数またはメソッドとその行範囲の一覧を得るには、次を使用します:
bca functions /path/to/your/file/or/directory
REST API
bca-web は、REST API を通じてソースコードを分析できる Web サーバーです。このサービスは、HTTP 経由でコード分析を行いたいすべての人に役立ちます。
サーバーは任意のホストとポートで実行でき、次の主要な機能をサポートします:
- ソースコードからのコメント除去。
- 指定したコードの関数スパンの取得。
- 指定したコードの AST(抽象構文木)の取得。
- 提供されたソースコードのメトリクスの計算。
サーバーの実行
サーバーを実行するには、次のコマンドを使用します:
bca-web --host 127.0.0.1 --port 9090
--hostはサーバーを実行する IP アドレスを指定します(デフォルトは 127.0.0.1)。--portは使用するポートを指定します(デフォルトは 8080)。-jは並列ジョブ数を指定します(省略可能)。--corsはブラウザベースのツール向けに CORS を有効にします(デフォルトは無効)。
フラグの全一覧、環境変数、リソース制限、およびデーモンを公開する前に守るべき信頼境界については、bca-web の運用 を参照してください。
CORS
デフォルトでは bca-web は CORS ヘッダーを一切送出しません。別のオリジンから配信されたブラウザスクリプトは、この API のレスポンスを読み取れません。これにより、ローカルの bca-web(デフォルトの 127.0.0.1 バインド)が、運用者がたまたま閲覧している任意の Web サイトにリポジトリのパスやメトリクスを晒すことを防ぎます。
オプトインするには --cors を渡します。引数は オリジン の明示的なカンマ区切り許可リストです。許可リストに含まれるオリジンだけが Access-Control-Allow-Origin ヘッダーを受け取り、マッチしたオリジンがそのままエコーバックされます(それ以外のオリジンからのリクエストはヘッダーを受け取らず、ブラウザによってブロックされます):
bca-web --cors https://app.example,https://tools.example
すべてのオリジンに Access-Control-Allow-Origin: * で応答するには、リテラルの * を渡します:
bca-web --cors '*'
全開放の * は、サーバーのメトリクスとリポジトリのパスをあらゆるオリジンに公開するため、信頼できるネットワークでのみ使用してください。
CORS が有効な場合、プリフライトの OPTIONS リクエストには 204 No Content で応答し、Access-Control-Allow-Origin、Access-Control-Allow-Methods(そのリソース自身が受け付けるメソッド。Allow ヘッダーが広告するのと同じ集合)、および Access-Control-Allow-Headers(リクエストの Access-Control-Request-Headers をエコーし、素のプローブに対しては Content-Type, Accept)を付与します。この API には認証も Cookie もないため、Access-Control-Allow-Credentials は決して送信されません。
API バージョニング
すべてのエンドポイントは /v1 プレフィックスの下にマウントされます(例えば /v1/metrics)。ルートの全集合は /v1/ping、/v1/version、/v1/languages、/v1/ast、/v1/comment、/v1/function、/v1/metrics、/v1/vcs、/v1/vcs/trend、/v1/vcs/jit、およびルートインデックス /v1 です。プログラムから発見するには GET /v1 を使用します(ルートインデックス を参照)。
以前の 1.x リリースが非推奨エイリアスとして提供していたプレフィックスなしのパス(/metrics、/comment、/ast、/、…)は 2.0 で削除されました。現在それらをリクエストすると 404 が返ります。どこでも /v1 形式を使用してください。
エラーレスポンス
エラーは 200 のボディ内ではなく、HTTP ステータスコードで報告されます。すべてのエラー(JSON エンドポイント、raw/octet-stream エンドポイント、415/405/404 フォールバックのいずれでも)は統一された機械可読の JSON ボディを 1 つ返すため、クライアントは成功時のコンテンツタイプに関わらず単一のエラー形式をパースすれば済みます:
{
"error": "human-readable message",
"error_kind": "stable_machine_token",
"id": "echoed-request-id"
}
error は具体的な人間可読の原因で、error_kind は安定した snake_case の機械向けトークン(例:unknown_field、unsupported_language、bad_request、parse_timeout)です。これによりクライアントは説明文の文字列マッチングをせずに原因で分岐できます(issue #631)。トークンの語彙は閉じており、STABILITY.md によって管理されています。
id キーは常に存在します。リクエストにクライアント指定の相関 ID があった場合(JSON エンドポイント)はそれを保持し、それ以外の場合は空文字列になります(octet-stream / クエリのエンドポイントは ID を持たず、コンテンツタイプ / メソッド / not-found のフォールバック、および ID が読み取られる前にボディのパースに失敗したリクエストには、エコーすべきパース済み ID が存在しません)。
ステータスコード:
400 Bad Request— 不正なボディまたはクエリパラメータ。無効な JSON、必須フィールドの欠落、認識されないキー(厳格なdeny_unknown_fieldsパース。削除されたunitフラグを含みます — 後述の Compute Metrics を参照)、またはfull/file以外のscope値。422 Unprocessable Entity—file_nameの拡張子(およびコンテンツスニッフィング)が対応言語のいずれにもマップされない場合。ルートはマッチしボディはパースされており、送信されたエンティティだけが処理できません。レスポンスは安定した機械向けトークン"error": "unsupported_language"を含みます。対応言語の集合はGET /v1/languagesで照会してください。(2.0 より前はこれが404であり、未知の URL と区別できませんでした — issue #634 を参照。)404 Not Found— URL がどのエンドポイントにもマッチしない場合。415 Unsupported Media Type— 既知のPOSTエンドポイントが、application/jsonでもapplication/octet-streamでもないContent-Typeを受け取った場合(charsetパラメータは許可されます)。405 Method Not Allowed— 既知のエンドポイントが誤った HTTP メソッドで呼ばれた場合(分析エンドポイントはPOST専用、/ping、/version、/languagesはGET専用です)。406 Not Acceptable— リクエストのAcceptヘッダーが、サーバーが生成できないメディアタイプのみを指定していた場合。構造化分析エンドポイントはapplication/json、application/yaml、application/cborを提供します。それ以外の具体的なタイプ(例:application/xml)はnot_acceptableトークンを伴う406になります。コンテンツネゴシエーション を参照してください。413 Payload Too Large— リクエストボディがサーバーの制限を超えた場合。500 Internal Server Error— それ以外は有効なリクエストに対してメトリクス計算または AST 構築が失敗した場合、あるいは/vcsの履歴ウォークがサーバー側で失敗した場合。ネストが深すぎてシリアライズできないレスポンス(serialize_failed)もこれに含まれます — 下記のネストの上限を参照してください。503 Service Unavailable— パースプールが孤立した(タイムアウトした)タスクで飽和している場合。後で再試行してください。504 Gateway Timeout— パース(または履歴ウォーク)がサーバーに設定された期限を超過した場合。
ネストの上限
レスポンスはツリーであり、どのシリアライザもレベルごとに 1 つのネイティブスタックフレームを使わずにツリーを出力することはできません。深くネストしたレスポンスにスタックを溢れさせ、プロセス全体を — 実行中のすべてのリクエストもろとも — 落としてしまうのを避けるため、サーバーは固定の深さを超えるシリアライズを拒否し、serialize_failed トークンとともに 500 を返します。
| レスポンス | 上限 | 1 レベルとみなされるもの |
|---|---|---|
/metrics | 128 | ネストした関数スペース 1 つ(関数の中の関数の中の…) |
/ast | 512 | AST ノード 1 つ |
どちらの上限も、実在のソースが到達する水準をはるかに上回っています。本プロジェクトがテスト対象とする 14,450 ファイルのコーパス(TensorFlow、DeepSpeech、serde など)全体で、最も深い AST は 188 ノード、最も深い関数スペースのネストは 10 です。/metrics の上限は読み取り側よりも余裕があります — ネストしたスペースの JSON ドキュメントは約 61 レベルを超えると読み戻せません。serde_json 自身の 128 レベルの再帰上限が、スペース 1 つあたり 2 レベルを消費するためです。
コンテンツネゴシエーション
構造化分析エンドポイント(/v1/ast、/v1/comment(JSON バリアント)、/v1/function、/v1/metrics、/v1/vcs、/v1/vcs/trend、/v1/vcs/jit)は、CLI の -O json|yaml|cbor 出力を反映して、リクエストの Accept ヘッダーからレスポンスのシリアライズ形式を選択します。同じ値は、CLI 由来でもサーバー由来でも、バイト単位で同一にシリアライズされます。
Accept の値 | レスポンスの Content-Type |
|---|---|
なし、*/*、application/*、application/json | application/json |
application/yaml(または text/yaml、application/x-yaml) | application/yaml |
application/cbor | application/cbor |
| その他の任意の具体的なタイプ | 406 Not Acceptable |
ルール:
- JSON がデフォルトです。
Acceptヘッダーのないリクエスト、または*/*/application/*/application/jsonを含むリクエストには JSON が返ります。これは以前のリリースが常に返していたのと同じボディとContent-Typeであり、既存のクライアントに変更は不要です。 q重みは尊重されます。 サポートされるタイプの中でq重みが最も高いエントリが選ばれます(Accept: application/json;q=0.5, application/yaml;q=0.9は YAML を返します)。q=0はそのタイプを拒否します。同点の場合は先に列挙されたエントリが維持されます。- サポートされないタイプは
406になり、 暗黙の JSON フォールバックは行われません。ボディはerror_kind: "not_acceptable"を持つ統一の{error, error_kind, id}エンベロープで、メッセージにはサポートされるメディアタイプが列挙されます。 - 構造化シリアライズのみが提供されます。 TOML と CSV は除外されています。TOML は深くネストしたスペースツリーには不向きで、CSV はフラットな表形式だからです。エラーエンベロープのボディと
/v1/commentの octet-stream バリアント(生バイト入力 / 生バイト出力)は、それぞれ常に JSON / 生バイトであり、ネゴシエーションを行いません。イントロスペクションルート(/v1、/v1/version、/v1/languages)は JSON メタデータのみを返します。
# YAML メトリクス
curl --silent \
--header 'Content-Type: application/json' \
--header 'Accept: application/yaml' \
--data '{"file_name": "foo.py", "code": "def f():\n pass\n"}' \
http://127.0.0.1:8080/v1/metrics
# CBOR メトリクス(バイナリ。デコーダーにパイプしてください)
curl --silent \
--header 'Content-Type: application/json' \
--header 'Accept: application/cbor' \
--data '{"file_name": "foo.py", "code": "def f():\n pass\n"}' \
http://127.0.0.1:8080/v1/metrics --output metrics.cbor
エンドポイント
1. サーバーの Ping
このエンドポイントは、サーバーが稼働しているかどうかの確認に使用します。
リクエスト:
GET http://127.0.0.1:8080/v1/ping
レスポンス:
- ステータスコード:
200 OK - ボディ:空。
curl -sf http://127.0.0.1:8080/v1/ping && echo ok を使うと死活チェックをスクリプト化できます。-f により、curl は HTTP エラー時に非ゼロの終了コードで終了します。
2. コメントの除去
このエンドポイントは、提供されたソースコードからコメントを除去します。2 つの Content-Type バリアントを受け付けます。生バイト入力 / 生バイト出力には application/octet-stream を、JSON エンベロープには application/json を使用します。
リクエスト:
POST http://127.0.0.1:8080/v1/comment
ペイロード:
{
"id": "unique-id",
"file_name": "filename.ext",
"code": "source code with comments"
}
id: リクエストの一意な識別子です。省略可能です(issue #645)。省略すると空文字列にデフォルトされ、「相関 ID なし」として扱われます。file_name: 解析対象のファイル名です。code: コメントを含むソースコードです。
レスポンス(JSON バリアント):
{
"id": "unique-id",
"language": "cpp",
"code": "print"
}
レスポンスのエンベロープは id、検出された language(正規の小文字スラッグ — 後述の メトリクスの計算 を参照)、および結果キー code を報告します。code フィールドはコメント除去後のソースを保持する文字列です。リクエストの code は JSON 文字列として届くため、除去後の出力は有効な UTF-8 であることが保証され、リクエストや他のすべての JSON エンドポイントと同様に文字列として返されます。application/octet-stream バリアントは、除去後のソースをそのまま(エンベロープなしの)生のレスポンスボディとして返します。これはバイナリを忠実に往復させる用途に適した形であり、シェルパイプラインでも扱いやすくなります。その エラー には引き続き上記の統一 JSON エラーボディが使われます。
ソースに除去可能なコメントが含まれない場合、どちらのバリアントも 200 ステータスと空のペイロードで空の結果を通知します。JSON バリアントは "code": ""(空文字列)を返し、octet-stream バリアントは空のボディを返します。したがって、要求された Content-Type にかかわらずステータスコードとエンベロープの形は同一です。octet-stream バリアントは 204 No Content ではなく空の 200 ボディを返します。
3. 関数スパンの取得
このエンドポイントは、指定されたソースコード内の関数のスパンを取得します。
リクエスト:
POST http://127.0.0.1:8080/v1/function
ペイロード:
{
"id": "unique-id",
"file_name": "filename.ext",
"code": "source code with functions"
}
id: リクエストの一意な識別子です。省略可能です(issue #645)。省略すると空文字列にデフォルトされ、「相関 ID なし」として扱われます。file_name: 解析対象のファイル名です。code: 関数を含むソースコードです。
レスポンス:
{
"id": "unique-id",
"language": "cpp",
"spans": [
{
"name": "function_name",
"start_line": 1,
"end_line": 10
}
]
}
エンベロープは id、検出された language スラッグ、および結果キー spans を報告します。パーサーが AST から関数名を解決できなかった場合(無名の定義や不正な形式の定義など)、name は null になります。null の name は不正な形式のスパンを示すシグナルです。
4. AST の取得
このエンドポイントは、指定されたソースコードの完全な tree-sitter 抽象構文木(AST)を、再帰的な JSON ノードツリーとして返します。
リクエスト:
POST http://127.0.0.1:8080/v1/ast
ペイロード:
{
"id": "unique-id",
"file_name": "filename.ext",
"code": "source code to parse",
"comment": false,
"span": true
}
id: リクエストの一意な識別子です。省略可能です。省略すると空文字列にデフォルトされ、「相関 ID なし」として扱われます。file_name: 解析対象のファイル名です。code: パースするソースコードです。comment:trueの場合、コメントノードがツリーから省かれます。省略可能で、デフォルトはfalseです。span:trueの場合、各ノードはソース上のspanを持ちます。falseの場合、spanはnullになります。省略可能で、デフォルトはfalseです。
id、comment、span は省略可能で、デフォルトは上記のとおりです(issue #645)。file_name と code は必須です。未知のキーは 400 で拒否されます(issue #633)。
レスポンス:
{
"id": "unique-id",
"language": "rust",
"root": {
"type": "source_file",
"value": "",
"span": { "start_line": 1, "start_col": 1, "end_line": 2, "end_col": 1 },
"field_name": null,
"children": [
{
"type": "function_item",
"value": "",
"span": { "start_line": 1, "start_col": 1, "end_line": 1, "end_col": 13 },
"field_name": null,
"children": [
{
"type": "identifier",
"value": "main",
"span": { "start_line": 1, "start_col": 4, "end_line": 1, "end_col": 8 },
"field_name": "name",
"children": []
}
]
}
]
}
}
エンベロープは id、検出された language スラッグ、およびルート AST ノードである root を報告します。各ノードは次の情報を持ちます。
type: tree-sitter 文法のノード種別です(文法固有です。どの文法が生成したかはlanguageスラッグで分かります)。value: リーフ/名前付きトークンのソーステキストです(内部ノードでは空です)。span:{ start_line, start_col, end_line, end_col }オブジェクト(すべて 1 始まり)です。spanがfalseの場合はnullになります。これらのスパンキーは/functionおよび/metricsと共通の*_line語彙を使用します(issue #638 で旧来の*_rowキーから改名されました)。field_name: 親がこのノードに到達する際に経由する tree-sitter 文法フィールドです(例:name、left、body)。ルート、無名トークン、フィールドを持たない子ではnullになります。children: ノードの子ノードです(再帰的に続きます)。
メトリクスや関数のエンドポイントと異なり、AST エンドポイントはクライアントが送信した正確なバイト列に対するノード座標を報告します。ソースの改行コードは正規化されないため、スパンはクライアント側の手元のコピーと一致します(issue #640)。
5. メトリクスの計算
このエンドポイントは、指定されたソースコードに対して各種メトリクスを計算します。
リクエスト:
POST http://127.0.0.1:8080/v1/metrics
ペイロード:
{
"id": "unique-id",
"file_name": "filename.ext",
"code": "source code for metrics",
"scope": "full"
}
-
id: リクエストの一意な識別子です。省略可能です(issue #645)。省略すると空文字列にデフォルトされ、「相関 ID なし」として扱われます。 -
file_name: ソースコードファイルのファイル名です。 -
code: 解析するソースコードです。 -
scope: スペースツリーをどこまで返すかを指定します。full(デフォルト)は完全にネストされたスペースツリー、すなわちファイルレベルのルートに加えて、すべての関数・クラス・その他のユニットの再帰的なspacesリストを返します。fileはファイルレベルのルートのみを返し、そのspacesの子は空になります。このフィールドは 2.0 より前のブール値unitフラグを置き換えるものです(issue#638)。旧来の
unitキーを送信すると、現在は400で失敗します。
ペイロードは厳密に検証されます。認識されないキー(タイプミスや、削除済みの unit)は 400 と統一 JSON エラーボディで拒否され、問題のフィールド名が示されます(issue #633)。
application/octet-stream バリアントでは、ソースは生のリクエストボディで渡し、scope はクエリパラメータとして指定します(?file_name=…&scope=full)。パラメータを省略するとデフォルトの full になり、認識されない値は 400 で拒否されます。
レスポンス:
{
"id": "unique-id",
"language": "rust",
"root": {
"name": "sample.rs",
"start_line": 1,
"end_line": 7,
"kind": "unit",
"spaces": [
{
"name": "double",
"start_line": 1,
"end_line": 7,
"kind": "function",
"spaces": [],
"metrics": {
"cyclomatic": { "sum": 2, "average": 2.0, "min": 2, "max": 2 },
"loc": { "sloc": 7, "ploc": 7, "lloc": 1, "cloc": 0, "blank": 0 },
"nom": { "functions": 1, "closures": 0, "total": 1 }
}
}
],
"metrics": { "...": "the same metric block, aggregated over the file" }
}
}
レスポンスのエンベロープは id、検出された language スラッグ、および root — 単一のファイルレベルのスペースオブジェクト — を報告します(issue #638 で、誤解を招く複数形の spaces からこのキーに改名されました)。root は name(リクエストの file_name)、start_line / end_line のスパン、kind 判別子(unit、function、class など)、その metrics ブロック、および子ユニットの再帰的な spaces リストを持ちます。上の例は省略されています。実際の各 metrics ブロックには、すべてのメトリクスファミリー — cyclomatic、cognitive、halstead、loc、nom、nargs、nexits、tokens、mi、abc、wmc — が、bca CLI が出力するのと同じネストされたフィールドとともに含まれます。scope が file の場合、root.spaces は空のリストになります。
language の値は正規の小文字スラッグ(例: rust、cpp、csharp、tsx)です。これは言語語彙が受け付けるトークンと同じもので、人間向けの表示名ではありません。すべての解析エンドポイント(/ast、/comment、/function、/metrics)がこの language フィールドを報告するため、クライアントはどの文法が選択されたかを確認できます。
6. サーバーとライブラリのバージョン
実行中のサーバーのバージョンと、そのビルドに使用された big-code-analysis ライブラリのバージョンを報告します。
リクエスト:
GET http://127.0.0.1:8080/v1/version
レスポンス:
{
"server": "2.0.0",
"library": "2.0.0"
}
7. 対応言語
対応言語と、登録されているファイル拡張子の一覧を返します。名前は正規の小文字スラッグです。一覧と拡張子はライブラリの言語テーブルから取得され、ハードコードされることはありません。
リクエスト:
GET http://127.0.0.1:8080/v1/languages
レスポンス:
{
"languages": [
{ "name": "cpp", "extensions": ["cpp", "cc", "hpp", "..."] },
{ "name": "rust", "extensions": ["rs"] }
]
}
他のすべてのエンドポイントと同様に、/version と /languages は /v1 プレフィックスの下でのみ提供されます。プレフィックスなしの 1.x エイリアスは 2.0 で削除されました(API バージョニングを参照)。
8. ルートインデックス
登録されているすべてのルートの機械可読なインデックス — パス、受け付ける HTTP メソッド、1 行の説明 — を返します。これによりクライアントは、本章をスクレイピングすることなく API サーフェスを発見できます。インデックスはサーバーが登録するのと同じルートテーブルから生成されるため、実際のルーティングから乖離することはありません。
リクエスト:
GET http://127.0.0.1:8080/v1
レスポンス:
{
"service": "bca-web",
"version": "2.0.0",
"routes": [
{ "path": "/v1", "methods": ["GET", "HEAD"], "description": "This route index." },
{ "path": "/v1/metrics", "methods": ["POST"], "description": "Compute maintainability metrics for the source." }
]
}
service は常に bca-web です。version は GET /v1/version の server フィールドと一致します。以前のリリースでこのエンドポイントのエイリアスとして提供されていたプレフィックスなしのルート / は、2.0 で削除されました。
変更履歴(VCS)メトリクス
3 つのエンドポイントが変更履歴(バージョン管理)メトリクス — CLI の bca vcs が計算するのと同じ数値 — を公開します。他のすべてのエンドポイントと異なり、これらはリクエストボディで運ばれるソースコードではなく、サーバーのファイルシステム上に既に存在する git リポジトリを解析します。VCS メトリクスはコミット履歴から導出されるものであり、リクエスト内に表現を持たないためです。
運用者向け警告 —
repo_pathは信頼境界です。repo_pathフィールドはサーバー側のファイルシステムパスです。これらのエンドポイントは、サーバーが読み取れる あらゆる git リポジトリをサーバーに走査させ、そのリポジトリの相対ファイルパス、チャーン、作者シグナルを返します。これは、クライアントが送信したコードしか見ないソース同梱型のエンドポイントとは本質的に異なります。省略可能なcache_dirフィールドは、呼び出し側が指定するもう 1 つのサーバー側パスで、書き込み 能力を付与します。キャッシュが有効な場合(デフォルト)、サーバーはその配下にディレクトリを作成して JSON キャッシュファイルを書き込む(<cache_dir>/<repo>/<head_sha>.json)ため、cache_dirを制御する呼び出し側は、サーバープロセスが書き込める任意のパスにキャッシュファイルを書かせることができます。(cache_dirを受け付けるのは/vcsのみで、/vcs/trendと/vcs/jitはキャッシュしません。)したがって、このエンドポイントのファイルシステム到達範囲は、読み取り可能な任意の git リポジトリの任意読み取り および 書き込み可能な任意のパス配下へのキャッシュファイルの任意書き込みです。bca-webの前段に認可レイヤーを置かないまま、/vcs、/vcs/trend、/vcs/jitを信頼できないクライアントに公開しないでください。 デフォルトの127.0.0.1バインドはこれらをローカルに留めます。各走査は、解析エンドポイントと同じパースタイムアウトとブロッキングプールのガードの下で実行されます。
3 つのエンドポイントはいずれも POST 専用で、application/json を受け付け、リクエストの id をエコーバックし、統一の {error, error_kind, id} ボディでエラーを報告します(その error_kind トークンは vcs_* ファミリーです — 例: vcs_not_a_repository、vcs_invalid_window)。クライアント側の誤り — repo_path が存在しない、または git 作業ツリーでない(どちらも vcs_not_a_repository を伴います)、解決できない ref/commit、不正な形式または diff でない diff、不正な形式のウィンドウ / タイムスタンプ / 式 / ファイルタイプ / しきい値 / トレンドのパラメータ — は 400 です。履歴走査自体の失敗は 500 です。存在しない repo_path はタイプミス — ここで最も多いクライアントエラー — であるため、存在するもののリポジトリではないパスと同様に、500 ではなく 400 を返します(issue 653)。
9. リスク順のファイルランキング — /vcs
リポジトリの履歴を一度だけ走査し、複合リスクスコアでランク付けしたファイル一覧を返します(issue #328)。
リクエスト:
POST http://127.0.0.1:8080/v1/vcs
ペイロード:
{
"id": "unique-id",
"repo_path": "/srv/repos/my-project"
}
repo_path は必須です。他のすべてのフィールドは省略可能で、デフォルトは bca vcs のデフォルト値です。id も省略可能で(issue #645)、省略すると空文字列となり、そのままエコーバックされます。省略可能なフィールドは次のとおりです。
long_window/recent_window: ウィンドウ指定です(例:12mo、90d)。デフォルトは12mo/90dです。top: リスク上位 N 件のファイルのみを保持します。未指定の場合のデフォルトは50(bca vcs --topのデフォルト)で、明示的に0を指定するとすべてのファイルを返します。ref: 解析対象のリビジョンです(デフォルトはHEAD)。risk_formula:weighted(デフォルト)またはpercentileです。file_types:metrics(デフォルト — bca がメトリクスを持つファイルのみ)、all(追跡されているすべてのテキストファイル)、またはカンマ区切りの拡張子許可リスト(rs,py)です。full_history: 第一親のみではなく、完全な DAG を走査します。include_merges: マージコミットを含めます。follow_renames: リネームを追跡します(デフォルトtrue)。exclude_bots: ボットの識別情報を除外します(デフォルトtrue)。bot_pattern: ボット作者を除外する正規表現を上書きします。as_of: スナップショットの基準となる「現在」(RFC 3339 /@unix/ git の日付形式)を指定します。emit_author_details: SHA-256 でハッシュ化した作者の識別情報を出力します。author_hash_key:emit_author_detailsを鍵付き HMAC-SHA256 に強化する秘密鍵です(emit_author_detailsが必要です。空の鍵、またはフラグなしでの指定は400になります)。include_deleted: 対象の ref で削除済みのファイルを含めます。bus_factor_threshold:(0, 1)の範囲で指定するバスファクターのカバレッジしきい値です(デフォルト0.5)。no_cache: このリクエストで永続的な変更履歴キャッシュを無効にします(デフォルトfalse)。cache_dir: サーバー側のキャッシュディレクトリを上書きします。
レスポンス:
{
"id": "unique-id",
"vcs_schema_version": 2,
"risk_score_version": 2,
"long_window_days": 365,
"recent_window_days": 90,
"truncated_shallow_clone": false,
"vcs_aggregate": { "...": "directory- / repo-level bus factor" },
"files": [
{
"path": "src/main.rs",
"vcs": {
"commits_long": 12,
"commits_recent": 3,
"churn_long": 540,
"churn_recent": 80,
"authors_long": 4,
"authors_recent": 2,
"risk_score": 1.42
}
}
]
}
files は vcs.risk_score の降順に並びます。各エントリはリポジトリ相対の path と、入れ子の vcs メトリクスブロック(bca vcs が出力するものと同じ形式、issue #684)を持ちます。含まれるのは、長期・直近ウィンドウでのコミット数とチャーン数、作者数、所有権シェア、バースト、バグ修正 / セキュリティ修正 / リバートの各カウント、経過期間、変更エントロピーと共変更エントロピー、そして合成値の risk_score です。hotspot_score とハッシュ化された author_ids は、計算可能な場合 / 要求された場合にのみそのブロック内に現れます。4 つの定数スタンプ vcs_schema_version、risk_score_version、long_window_days、recent_window_days はトップレベルに一度だけ置かれ、行ごとには現れません(issue #635)。vcs_aggregate はディレクトリレベルおよびリポジトリレベルのバスファクターを持ちます(issue #332)。
10. 履歴トレンド — /vcs/trend
変更履歴メトリクスを等間隔の複数時点でサンプリングし、ファイルごとの時系列を返します(issue #333)。応答はランク付けされたスナップショットではなく系列であるため、/vcs とは別のルートになっています。
リクエスト:
POST http://127.0.0.1:8080/v1/vcs/trend
ペイロード: 上記の /vcs フィールドすべて(ただしキャッシュ制御 no_cache / cache_dir を除きます — trend は永続キャッシュを使わないため、どちらかを送ると黙って無視されるのではなく 400 になります。issue #961)に加えて、次を指定できます:
points: 等間隔のサンプル点の数(>= 2)。省略時は12(bca vcs trend --pointsのデフォルト)になります。span: サンプル点全体がカバーする遡及期間(デフォルト12mo)。top_deltas: 改善 / 悪化それぞれのリストに載せる上位 N ファイル数。未指定の場合は10、明示的な0は全件を返します。
レスポンス:
{
"id": "unique-id",
"trend_schema_version": 1,
"vcs_schema_version": 2,
"risk_score_version": 2,
"long_window_days": 365,
"recent_window_days": 90,
"truncated_shallow_clone": false,
"as_of_points": [1704067200, 1711929600],
"files": {
"src/main.rs": [ { "as_of": 1704067200, "vcs": { "risk_score": 1.1 } }, null ]
},
"deltas": { "improved": [], "regressed": [] }
}
as_of_points はサンプル時刻を古い順に列挙します。files 内の各ファイルの配列はこれと 1:1 で対応し、その時点でファイルがまだ存在しなかった場合は null 要素になります。存在する各要素は { "as_of": ..., "vcs": { ... } } の形で、その時点のそのファイルの VCS ブロックが vcs の下に入れ子になります(issue #684)。4 つの定数スタンプはトップレベルに一度だけ置かれ、時点ごとには現れません(issue #635)。deltas は、系列全体でのリスクスコアの変動に基づいて、最も改善したファイルと最も悪化したファイルをランク付けします。
11. ジャストインタイムリスク — /vcs/jit
単一の変更のジャストインタイムリスクをスコアリングします — 対象はサーバー側リポジトリ上の 1 コミット、またはリクエストボディで渡される任意の unified diff のいずれかです(issue #331 / #580)。この 2 つのモードは相互排他です。
コミットモードは repo_path 上のコミットをスコアリングします:
{
"id": "unique-id",
"repo_path": "/srv/repos/my-project",
"commit": "HEAD"
}
コミットモードは、経験ウィンドウを調整する long_window、recent_window、full_history、include_merges、follow_renames、as_of も受け付けます。応答は source が "commit" の完全なレポートで、その risk_score は 5 つの特徴グループ(サイズ、拡散、履歴、経験、目的)すべてを織り込みます:
{
"id": "unique-id",
"jit_schema_version": 3,
"jit_score_version": 1,
"source": "commit",
"long_window_days": 365,
"recent_window_days": 90,
"risk_score": 0.87,
"commit": { "id": "…", "parent_count": 1, "is_merge": false, "purpose": {} },
"features": { "size": {}, "diffusion": {}, "history": {}, "experience": {} },
"contributions": { "size": 0.4, "diffusion": 0.2, "history": 0.1, "purpose": 0.0, "experience": -0.1 }
}
diff モードはリポジトリなしで任意の unified diff をスコアリングします:
{
"id": "unique-id",
"diff": "--- a/x\n+++ b/x\n@@ -1 +1 @@\n-old\n+new\n"
}
素の diff には作者・親・履歴の情報がないため、計算できるのは サイズ と 拡散 のグループだけです。diff レポートの source は "diff" で、risk_score ではなく partial_risk_score を報告します。欠けているグループは ボディから完全に省かれ、ゼロとして現れることは決してないためです:
{
"id": "unique-id",
"jit_schema_version": 3,
"jit_score_version": 1,
"source": "diff",
"partial_risk_score": 0.6,
"size": {},
"diffusion": {},
"contributions": { "size": 0.4, "diffusion": 0.2 }
}
source 判別子("commit" か "diff" か)で分岐して、正しいスコアフィールドを読み取ってください。partial_risk_score は、同じ変更に対するコミットの risk_score より常に低く、異なるスケール上にあります。diff は他の diff とだけ比較し、コミットのスコアとは決して比較しないでください。
モードの競合。 diff をいずれかのコミットモードのフィールド(repo_path、commit、ウィンドウ、履歴、リネーム、as_of の各設定)と同時に指定すると、diff を黙って優先して残りを捨てるのではなく 400 で拒否されます。2 つのモードは互いに比較できない別々の問いに答えるものであり、この組み合わせはクライアントの誤りとして扱われます(issue 632)。
diff でない diff。 git の unified diff ではない diff 値 — フィールドの取り違え、途中で壊れた文字列、任意のテキストなど — は、確信を持った partial_risk_score の 0.0 としてスコアリングされるのではなく、400(vcs_invalid_diff)で拒否されます。リスクを「ゲート」するエンドポイントでは、誤った「リスクゼロ」が最も危険な失敗モードであるため、diff でない入力はハードエラーになります(issue 652)。唯一の例外は空または空白のみの diff です。これは正当に「変更なし」を意味するため、有効な 0.0 を返します — 空の diff を計算した CI ステップは、期待どおりのリスクゼロという答えを得られます。
bca-web の運用
bca-web は big-code-analysis ライブラリをラップする HTTP デーモンで、コメント除去、関数スパン、AST ダンプ、保守性メトリクス、変更履歴(VCS)メトリクスを REST API として公開します。このページはデーモンを運用するオペレーター向けに、ビルドと起動の方法、チューニングに使うフラグと環境変数、そして公開前に守るべき信頼境界を説明します。
本ページは 2 つのリファレンスページを補う運用ガイドです。REST API リファレンスは、全エンドポイントとそのリクエスト / レスポンス形式、エラー契約を文書化しています。REST API を操作するレシピは、エンドツーエンドの curl 呼び出しを示します。このページはプロセスそのものを扱い、内容を繰り返す代わりにこの 2 つへリンクします。
ビルドと実行
bca-web は big-code-analysis-web クレートのバイナリです。チェックアウトからは Cargo 経由で実行します:
cargo run -p big-code-analysis-web -- --host 127.0.0.1 --port 8080
バイナリを PATH 上に置くには、リリースアーティファクトをビルドしてコピーするか、クレートからインストールします:
cargo install big-code-analysis-web # `bca-web` コマンドをインストールします
bca-web --host 127.0.0.1 --port 8080
bca-web は指定されたアドレスにバインドし、中断されるまでサービスを提供します。ポートをバインドできない場合や I/O エラーに遭遇した場合は非ゼロで終了するため、スーパーバイザー(systemd、コンテナオーケストレーター、CI のスモークチェック)が失敗を検知して再起動や警告を行えます。
言語のサブセットでのビルドは機能しません
出荷される bca-web バイナリは、対応するすべての tree-sitter 文法をコンパイルして組み込みます。big-code-analysis-web クレートはライブラリの all-languages フィーチャセットを明示的に固定しているため、cargo build -p big-code-analysis-web に --no-default-features や独自の --features リストを渡しても、生成されるバイナリから文法は除外されません。ユーザー向けデーモンから文法が黙って抜け落ちると、ビルドエラーではなくリクエスト時に「言語 X が動かなくなった」という形で表面化するため、このクレートはそれを禁止しています(issue #252)。
文法セットを削減する必要がある場合は、自分の Rust コードに big-code-analysis ライブラリを組み込み、自分の Cargo.toml でフィーチャを選択してください。言語別 Cargo フィーチャの章に、すべてのフィーチャと実際の例が載っています。
コマンドラインフラグ
デフォルト値付きの全フラグ一覧:
| フラグ | デフォルト | 目的 |
|---|---|---|
-j, --num-jobs <N\|auto> | auto | ワーカースレッド数。auto は OS が報告する実効 CPU 数に解決されます。 |
--host <HOST> | 127.0.0.1 | バインドするアドレス。 |
-p, --port <PORT> | 8080 | TCP ポート。 |
--parse-timeout-secs <SECS> | 30 | パース 1 回あたりの期限。0 で無効になります。 |
--cors <ORIGINS> | オフ | カンマ区切りのオリジン許可リストに対して CORS を有効にします。 |
-h, --help | ヘルプを表示して終了します。 | |
-V, --version | バージョンを表示して終了します。 |
--num-jobs auto は Linux で cgroup のクォータと cpuset を認識します。CPU クォータ付きのコンテナ内では、ホストの物理コア数ではなくクォータに解決され、bca CLI の --num-jobs と同じ挙動になります。この数はワーカープールとパース許可セマフォのサイズを決めるため、同時に実行できるパース数の上限になります。最小値は 1 で、0 はパース時に拒否されます。
--parse-timeout-secs は、リクエストが 504 Gateway Timeout を返すまでに 1 回のパースが実行できる時間を制限します。デフォルトの 30 は、病的な入力がワーカーを無期限に塞ぐことを防ぎます。0 に設定すると期限がなくなり、それとともに後述の負荷制御も無効になります。無制限のパースが許容できる場合にのみ 0 を使ってください。タイムアウト時に返されるレスポンスボディは REST API リファレンスを参照してください。
CORS はデフォルトで無効です。リファレンスの CORS セクションが、プリフライト処理、ワイルドカード形式、クレデンシャル非対応を含めて完全に文書化しています。要約すると、ブラウザーのツールにレスポンスを読ませたい場合は明示的なオリジン許可リストを付けて --cors を渡し、省略すれば Access-Control-* ヘッダーは一切出力されません。
環境変数
| 変数 | デフォルト | 目的 |
|---|---|---|
BCA_MAX_ORPHANED_TASKS | max(num_jobs * 2, 4) | 孤児化した(タイムアウト後もまだ実行中の)パースタスクの上限。これに達すると新しいリクエストは 503 で遮断されます。 |
RUST_LOG | info | tracing サブスクライバーのログフィルター。 |
RUST_LOG は EnvFilter の構文を使います(例: RUST_LOG=big_code_analysis_web=debug)。デーモンは完了したリクエストごとに、メソッド、ルート、ステータス、レイテンシを含むアクセスログを 1 行出力します。
リソース制限とバックプレッシャー
単一のクライアントによるリソース枯渇からデーモンを守る制限が 2 つあります。
リクエストボディサイズ。 すべてのエンドポイントは、4 MiB を超えるリクエストボディを 413 Payload Too Large で拒否します。この制限は JSON と raw オクテットストリームのコンテンツタイプに一律に適用されるため、どちらも同じしきい値で過大なボディを拒否します。
孤児タスクの流入制御。 パースが --parse-timeout-secs を超えるとリクエストは 504 を返しますが、tree-sitter はパース途中で中断できないため、ブロッキングスレッドはパースが自然に終わるまでプール上で動き続けます。持続的な病的入力によってバックグラウンド作業が際限なく積み上がるのを止めるため、孤児タスク数がソフト上限に達すると、新しいリクエストは 503 Service Unavailable で拒否されます。上限のデフォルトは max(num_jobs * 2, 4) で、BCA_MAX_ORPHANED_TASKS で上書きできます(符号なし整数としてパースされ、不正な値やゼロはデフォルトにフォールバックします)。--parse-timeout-secs 0 を設定すると、この仕組み全体が無効になります。期限がなければタスクが孤児化することはないためです。
セキュリティと信頼境界
bca-web 自体には認証、認可、レート制限がありません。デフォルト値はローカルの単一オペレーター運用向けに選ばれています。広げる場合は意図的に行ってください。
デフォルトのバインド先はループバックです。 サーバーは --host で指定しない限り 127.0.0.1 にバインドします。ネットワークに公開する前は、そのままにしておくか、前段に認証プロキシを置いてください。0.0.0.0 にバインドすると、ポートに到達できる誰もが以下のすべての機能に到達できるようになります。
CORS はデフォルトで無効です。 --cors フラグがなければ、別オリジンのブラウザースクリプトは API レスポンスを読めないため、オペレーターがたまたま訪れたページがループバックの bca-web をこっそり操作することはできません。ワイルドカード形式(--cors '*')はすべてのオリジンに応答し、サーバーのメトリクスやリポジトリパスを任意のサイトに公開するため、信頼できるネットワークでのみ使ってください。完全なセマンティクスは CORS にあります。
VCS エンドポイントはサーバー側のリポジトリを読み取ります。 ソースコードをボディで受け取るエンドポイントと異なり、/v1/vcs、/v1/vcs/trend、/v1/vcs/jit は、リクエストの repo_path で指定される、サーバーのファイルシステム上に既にある git リポジトリを解析します。これらのエンドポイントに到達できる呼び出し元は、サーバーが読み取れる任意の git リポジトリをサーバーに走査させ、そのリポジトリのファイルパス、チャーン、作者シグナルを知ることができます。リファレンスの VCS 信頼境界の警告がこれを完全に扱っています。認可レイヤーなしで、信頼できないクライアントにこれらのエンドポイントを公開しないでください。
VCS キャッシュディレクトリはクライアントが制御する書き込みパスです。 VCS エンドポイントはオプションの cache_dir フィールドを受け付け、永続的な変更履歴キャッシュの書き込み先を上書きできます。デフォルトはプラットフォームのキャッシュ位置($XDG_CACHE_HOME/big-code-analysis/vcs)です。cache_dir を設定できる呼び出し元は、サーバープロセスが書き込むディレクトリを選べるため、信頼できないクライアントは攻撃者が選んだパスにキャッシュ書き込みを向けられます。これも、VCS エンドポイントを認可レイヤーの背後に置き、信頼できない入力に決して開放してはならない理由の 1 つです。
レシピ
bca と bca-web で作業を進めるためのタスク指向の例です。各レシピは、バイナリがビルド済み(cargo build --release)で、bca が PATH 上にあることを前提とします。
レシピは目的別にまとめられています:
- 品質レポート — プルリクエスト、ダッシュボード、Wiki に適した Markdown レポートを生成します。C/C++ のプリプロセッサ対応ワークフローも含みます。
- CI 統合 —
bca checkとbca reportを GitHub Actions と GitLab CI に組み込みます。ベースライン / ラチェットパターンと Code Quality ウィジェットへの経路も含みます。 - ベースライン — 既存の違反を
.bca-baseline.tomlに記録して、ゲートが新規または悪化した違反にのみ反応するようにし、レビュー時にそのベースラインを監査したり差分を確認したりします。 - ローカルしきい値ゲート — 2 層構成(ハード + ヘッドルーム)の Makefile /
just/pre-commitパターンで、CI のしきい値ゲートを開発者マシン上に複製し、リグレッションをプルリクエストに到達させません。 - Choosing thresholds — where the shipped default limits come from, which ones to override for the language you are gating, and how to pick a different set for CI, agent feedback, legacy triage, or safety-critical work.
- エージェントへのメトリクス供給 —
bca checkをエージェント型コーディングツールの編集後フィードバックループ(Claude Code のPostToolUseフック、opencode プラグイン)に組み込みます。ループを健全に保つアンチゲーミングの指針も含みます。 - AST クエリ — 構文要素の検索、ノード種別のカウント、ツリーのダンプ、パースエラーの検出を行います。
- メトリクスデータのエクスポート — 構造化出力(JSON / YAML / TOML / CBOR)を出力し、シェルパイプラインから利用します。
- REST API を操作する — HTTP サーバーを起動し、すべてのエンドポイントを
curlで呼び出します。
レシピで使われているフラグを詳しく知りたい場合は、コマンド配下のコマンド別ページを参照してください。これらのレシピに登場するメトリクスの一覧は、対応メトリクスを参照してください。
上流リファレンス。
big-code-analysisは Mozilla のrust-code-analysisのフォークです。上流のrust-code-analysis-cliバイナリで動くレシピは、たいていそのまま使えます — バイナリ名を置き換え、移行ガイドに記載されているサブコマンド再編に合わせて調整してください。
品質レポート
集約された人間可読の Markdown レポートを生成するためのレシピです。
レポートを CI に組み込みたい場合。 Markdown レポートを PR/MR コメントとして投稿し、しきい値違反をプラットフォーム標準のコード品質ウィジェットで表示する、実行可能な GitHub Actions と GitLab CI の例は CI 統合レシピを参照してください。
実際のレポート例
big-code-analysis は、main へのプッシュごとに、自身のソースツリーに対する bca report -O markdown --vcs と bca report -O html --vcs の出力を公開しています。どちらかを開けば、このページのレシピが多言語の Rust + Python コードベースで生成する結果をそのまま確認できます:
- HTML ホットスポットレポート(ソート可能なテーブル、言語別セクション、さらに
--vcsによる「変更履歴リスク」セクション付き): https://dekobon.github.io/big-code-analysis/reports/index.html - Markdown の PR/MR コメント(そのまま issue に貼り付け可能): https://dekobon.github.io/big-code-analysis/reports/report.md
- 変更履歴リスク。上位 100 件の完全なランキングを機械可読な JSON で提供します(
bca vcsを参照): https://dekobon.github.io/big-code-analysis/reports/vcs-report.json
これらを生成する仕組みは .github/workflows/pages.yml にあります。同じワークフローがしきい値ゲートも実行します。パイプライン全体の構成は CI 統合 を参照してください。
プロジェクト全体の品質レポートを生成する
プロジェクトルートから実行し、レポートをファイルに書き出します:
bca report \
--paths "$PWD" \
-O markdown \
--top 20 \
--strip-prefix "$PWD/" \
--output report.md
--strip-prefixはファイルパスを短く保ち、マシン間で安定させます。指定しない場合、各行には現在のチェックアウトの絶対パスが含まれます。--topは各ホットスポットテーブルに表示される行数を制御します。PR コメントには 20 が適切なデフォルトです。ダッシュボードのタイルなら 5 に減らし、すべての行を表示するには0を渡します。--jobsのデフォルトは実効 CPU 数です(Linux では cgroup / cpuset を考慮します)。デバッグのために直列モードを強制したい場合にのみ--jobs 1を渡してください。
レポートを特定の言語に限定する
bca は拡張子から言語を推定するため、include / exclude のグロブでフィルタリングを行います:
bca report \
--include "*.rs" --include "*.py" \
--paths "$PWD" \
-O markdown --output report.md
ベンダリングされたツリーや生成されたツリーを除外するには、さらに --exclude を重ねます:
bca report \
--include "*.rs" \
--exclude "**/target/**" --exclude "**/vendor/**" \
--paths "$PWD" \
-O markdown
フラグの引数個数。
--includeと--excludeは 1 回の指定につきちょうど 1 つのグロブを受け取ります。パターンを追加するにはフラグを繰り返してください。=形式も同様に動作します:--include="*.rs" --exclude="**/target/**"。先頭の
./は省略可能です。 素の相対パターンと./付きの表記は等価です。--exclude "vendor/**"は--exclude "./vendor/**"とまったく同じものにマッチします。これは--include、--exclude、--exclude-from、.bcaignore、そして[check.exclude]ゲート免除セットという、すべてのグロブ入力面に当てはまります。
リポジトリ全体で安定した除外セットを維持するには、パターンをリポジトリルートのファイル(慣例として .bcaignore)に置き、--exclude-from で読み込みます。パターンはインラインの --exclude の値と和集合になります。空行と # で始まるコメントはスキップされます:
bca report \
--paths . \
--exclude-from .bcaignore \
-O markdown --output report.md
最も深刻な違反だけを表示する
セクションごとに上位 3 件の問題をハイライトする簡易トリアージビューには次を使います:
bca report -p src/ -O markdown --top 3
レポートには引き続きすべてのセクションが含まれますが、各テーブルは一目で確認できる程度に短くなります。
2 つのリビジョンを比較する
集約レポート自体にはリビジョン間の差分を取る機能はありません。両側でそれぞれレポートを実行し、Markdown を diff します:
git worktree add /tmp/before main
bca report -p /tmp/before -O markdown \
--strip-prefix /tmp/before/ --output /tmp/before.md
bca report -p "$PWD" -O markdown \
--strip-prefix "$PWD/" --output /tmp/after.md
diff -u /tmp/before.md /tmp/after.md | less
両方のレポートが同じ --strip-prefix の形式を使うため、パスの列が揃い、diff はパスのノイズではなくメトリクスの変化が中心になります。
C/C++ プリプロセッサ対応レポート
マクロを多用する C/C++ コードベースでは、プリプロセッサデータをアナライザーに与えることで、条件付きコンパイルをコンパイラが見るのと同じように解釈できます。ワークフローは 2 段階です:
# 1. ヘッダーとソースからプリプロセッサデータの JSON を構築します。
bca preproc \
--paths src/ include/ \
--output /tmp/preproc.json
# 2. そのデータを添えてレポート(または他の任意のコマンド)を実行します。
bca report \
--paths src/ \
--preproc-data /tmp/preproc.json \
-O markdown --output report.md
--preproc-data は、メトリクスを計算してツリーをウォークするすべてのサブコマンド(metrics、ops、functions、report、check など)で受け付けられます。正確な C/C++ 解析が重要な場面ならどこでも使えます。これを消費しないサブコマンド(vcs、preproc、list-metrics、diff-baseline)では、使用方法エラーとして拒否されます。
PR で変更されたファイルだけを解析する
変更されたファイルの一覧を --paths-from - にパイプすると、ツリー全体ではなく差分だけを採点できます:
git diff --name-only --diff-filter=AM origin/main...HEAD \
| bca metrics --paths-from - -O json --output-dir ./out
--diff-filter=AMは追加(Added)と変更(Modified)されたファイルを残し、削除(Deletion)を除外します。もはや存在しないファイルは解析できないためです。--paths-from -は標準入力から改行区切りのパスを読み取ります。ファイル引数でも同様に動作します:--paths-from changed.txt。- この方法で渡されたパスは明示的なものとして扱われるため、ディレクトリウォークではそれらを隠していたはずの
.gitignoreルールをバイパスします。言語で絞り込むには-I '*.py' -I '*.rs'を組み合わせてください(グロブごとにフラグを 1 回ずつ繰り返します)。
PR 範囲の Markdown サマリーが必要な場合は、metrics をレポートパイプラインに置き換えます:
git diff --name-only --diff-filter=AM origin/main...HEAD \
| bca report --paths-from - -O markdown \
--top 10 --output pr-report.md
ディレクトリをウォークする際は .gitignore が自動的に尊重されるため、このページの前半のレシピでは、対象のパスがプロジェクトの .gitignore で既にカバーされていれば、明示的な -X "**/target/**" -X "**/node_modules/**" はもう不要です。gitignore されたツリーを解析する必要がある場合は --no-ignore を追加してください。
CI 連携
bca をビルドパイプラインに組み込むためのレシピ集です。bca check コマンドは、現代の CI に必要なあらゆる出力形式(Checkstyle、SARIF、GitLab Code Climate JSON、clang/GCC 警告行、MSVC 警告行)を既に備えており、加えて人間向けの bca report markdown があります。このページは、ユーザーの 目的 から、適切なサブコマンド・フラグ・プラットフォーム連携の組み合わせへの統合マップです。
出力の選び方
以下のマトリクスは、よくある目的ごとに、対応する CI サーフェスへ出力を供給する bca の呼び出し方を対応付けたものです。実行可能な例はリンク先の各セクションにあります。
| 目的 | コマンド + フラグ |
|---|---|
| しきい値リグレッションに対するハードゲート | bca check (自動検出された bca.toml のしきい値を使用) |
| 既存コードベースでしきい値をラチェットする | bca check --baseline .bca-baseline.toml (‡) |
| インライン PR アノテーション(GitHub) | bca check … --report-format clang-warning --no-fail + GCC problem matcher |
| Code Scanning アラート(GitHub) | bca check … --report-format sarif --no-fail + github/codeql-action/upload-sarif |
| マージリクエストウィジェット(GitLab Code Quality) | bca check … --report-format code-climate --no-fail |
| Jenkins / SonarQube への取り込み | bca check … --report-format checkstyle |
| 人間可読な PR/MR コメントまたはダウンロード可能な成果物 | bca report -O markdown --top 20 --strip-prefix "$PWD/" |
| ダッシュボード向けの機械可読アーティファクト | bca metrics --format json --output-dir ./out |
(‡)既存の違反があるコードベースにしきい値を導入する際に推奨される導入パスです。ブートストラップ・リフレッシュ・リタイアのワークフローについてはベースラインのレシピを参照してください。
bca check の出力形式、終了コード(0 はクリーン、2 は違反、1 はツールエラー)、しきい値設定の完全なリファレンスは Check コマンドのページ にあります。Markdown レポートの形式については、Report コマンドのページ と品質レポートのレシピを参照してください。
GitHub Actions
実際の動作例
big-code-analysis は、すべてのプッシュと PR で、以下のレシピを自身のソースに対して実行しています。ワークフローのソースである .github/workflows/pages.yml は、しきい値ゲート、ベースラインのラチェット、両方のレポート形式、そして GitHub Code Scanning への SARIF アップロードを、ワークスペース自身に対してエンドツーエンドで実行します。(SARIF アップロードは同一リポジトリのプッシュと PR でのみ実行されます。アップロードには書き込みスコープのトークンが必要なため、フォークからの PR ではスキップされます。これは clippy の SARIF ジョブとまったく同じ扱いです。)出力は本書と並んで GitHub Pages 上に置かれています:
- HTML ホットスポットレポート: https://dekobon.github.io/big-code-analysis/reports/index.html
- Markdown 版 PR/MR コメント: https://dekobon.github.io/big-code-analysis/reports/report.md
以下のスニペットはそのまま自身のワークフローにコピーできます。記載されている bca のバージョンは、執筆時点で最新の公開リリースです。
リポジトリ内のワークフローは、ピン留めされたリリースをダウンロードするのではなく、現在のチェックアウトから
bcaをビルドしてインストールします。これは、.bca-baseline.tomlが常にそれをゲートするのと同じbcaによって書かれるリポジトリにおいて、後述の GitHub Release からのbcaのインストールで説明する CLI アーティファクトのスキーマ不整合という失敗モードを避けるためです。安定リリース系列を追跡する下流の採用者は、ピン留めされた tarball のパターンを使い続けるべきです。「チェックアウトからビルド」に切り替えるのは、あなたもバイナリと同時に CLI アーティファクトのスキーマを変更している場合だけにしてください。
しきい値ゲート、SARIF、clang-warning マッチャー
既存の 3 つのレシピ、すなわちハードなしきい値ゲート、Code Scanning への SARIF アップロード、インライン PR アノテーション用の clang-warning + GCC problem matcher は、Check コマンドのページにあります。ここで再実装せず、リンク先を利用してください。
GitHub Release からの bca のインストール(推奨)
最も高速で再現性の高いインストール方法は、このリポジトリの GitHub Releases にあるビルド済み tarball です。curl | sha256sum | tar の 1 手順だけで済み、Rust ツールチェーンを必要とせず、実行のたびにバイト単位で同一のバイナリが得られます。バージョンをキーにした actions/cache と組み合わせると、正常パスの再実行ではダウンロード自体をスキップできます:
CLI アーティファクトのスキーマ互換性。 ここでピン留めする
BCA_VERSIONは、リポジトリがコミットするすべての CLI アーティファクト — 最も重要なのは(独自のversionフィールドを持つ).bca-baseline.tomlとbca.tomlマニフェスト — のスキーマバージョンをサポートしている必要があります。より新しいbca(より新しいスキーマバージョンを持つもの)が書いたベースラインファイルは、古いbcaでは読み込めず、ゲートはbaseline version N is not supported by this bcaというエラーで失敗します。mainを追跡している場合や、より新しいbcaでローカルにベースラインを再生成した場合は、新しいスキーマに対応したリリースへピン留めし直すか、ベースラインを書いたのと同じコミットを指すbcaのcargo install --gitビルドに切り替えてください(後述のcargo installの代替手段を参照)。互換性の契約は STABILITY.md に記録されています。
env:
BCA_VERSION: "2.0.0"
BCA_TARGET: "x86_64-unknown-linux-gnu"
# リリースの SHA256SUMS ファイルにある big-code-analysis-${BCA_VERSION}-${BCA_TARGET}.tar.gz の sha256。
# BCA_VERSION と合わせて更新してください。
BCA_SHA256: "a205fff13108d0f8c679a062e352ba8468109c4adfdd8c9e3567cf5fcc99c3d5"
steps:
# キャッシュキーには必ず BCA_SHA256(と BCA_TARGET)を含めてください。
# キーに sha256 が含まれていないと、バージョンを上げずに公開チェックサムを
# ローテーションした場合、キャッシュヒット時に古いバイナリが返され、
# インストールステップの `sha256sum --check`(キャッシュミス時のみ実行)を
# 黙ってバイパスしてしまいます。同じワークフローを複数の `runs-on` で
# 実行する場合は、BCA_TARGET を含めることが重要です。
- name: Cache bca binary
id: bca-cache
uses: actions/cache@v5
with:
path: ~/.local/bin/bca
key: bca-${{ runner.os }}-${{ env.BCA_TARGET }}-${{ env.BCA_VERSION }}-${{ env.BCA_SHA256 }}
- name: Install bca from GitHub Releases
if: steps.bca-cache.outputs.cache-hit != 'true'
run: |
set -euo pipefail
stage="big-code-analysis-${BCA_VERSION}-${BCA_TARGET}"
tarball="${stage}.tar.gz"
url="https://github.com/dekobon/big-code-analysis/releases/download/v${BCA_VERSION}/${tarball}"
mkdir -p "$HOME/.local/bin"
curl -fsSL --proto '=https' --tlsv1.2 -o "/tmp/${tarball}" "$url"
echo "${BCA_SHA256} /tmp/${tarball}" | sha256sum --check --strict -
tar -xzf "/tmp/${tarball}" -C /tmp
install -m 0755 "/tmp/${stage}/bca" "$HOME/.local/bin/bca"
rm -rf "/tmp/${tarball}" "/tmp/${stage}"
- name: Prepend ~/.local/bin to PATH
run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
利用可能な BCA_TARGET の値(runs-on に一致するものを選択してください): x86_64-unknown-linux-gnu、x86_64-unknown-linux-musl、aarch64-unknown-linux-gnu、aarch64-unknown-linux-musl、aarch64-apple-darwin、x86_64-pc-windows-msvc、aarch64-pc-windows-msvc。Windows のアセットは .tar.gz ではなく .zip を使用します。bca-web バイナリは同じアーカイブ内で bca と一緒に配布されます。
代替手段: ビルド済みバイナリ対応アクション経由の cargo install
ランナーから github.com に到達できないが crates.io には到達できる場合(エアギャップ環境、カスタムミラー)、次の 2 つのアクションは、ビルド済みバイナリが公開されていないときに透過的に cargo install へフォールバックします — その代償はコールドパスでのコンパイル時間です。どちらも GitHub Releases のアセットと同じ crates.io リリースにピン留めするため、CLI アーティファクトのスキーマ互換性の警告はここでもそのまま当てはまります。
最新の crates.io リリースより先の bca が特に必要な場合(例えば、コミットされた .bca-baseline.toml のスキーマが、公開されているどの bca も理解できないほど新しい場合)は、tool: big-code-analysis-cli@<version> や --version の形式を、ベースラインが生成された正確なコミットを対象とする cargo install --git https://github.com/dekobon/big-code-analysis --rev <SHA> --locked big-code-analysis-cli に置き換えてください。これはリポジトリ内の pages.yml ワークフローが(--path によりローカルチェックアウトに対して)行っていることですが、bca 自身のリポジトリのための意図的な回避策であり、下流の採用者に推奨されるデフォルトではありません。
# 選択肢 1: taiki-e/install-action
- name: Install bca
uses: taiki-e/install-action@v2
with:
tool: big-code-analysis-cli@2.0.0
# 選択肢 2: cargo-binstall
- name: Install cargo-binstall
uses: cargo-bins/cargo-binstall@main
- name: Install bca
run: cargo binstall --no-confirm big-code-analysis-cli --version 2.0.0
いずれかのアクションがコンパイルにフォールバックした場合は、2 回目の実行が速くなるように cargo レジストリとインストール済みバイナリをキャッシュしてください:
- name: Cache cargo registry and bca binary
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cargo/bin/bca
# crates.io のリリースは不変なものとして公開されるため、ここでは
# `<version>` のキーで十分です — ローテーションすべき sha256 はありません。
# (上記の GitHub Releases インストールパスは事情が異なります。再公開された
# リリースアセットはバージョンを共有するため、そのキャッシュキーには sha256 が必要です。)
key: bca-${{ runner.os }}-2.0.0
レポートが実行間で再現可能であり続けるよう、特定のバージョン(crates.io に公開されている big-code-analysis-cli のリリースに一致するもの)にピン留めしてください。バージョンを固定しないインストールでは、メトリクス集計の変更が月曜日に「謎の CI フレーク」として現れます。
Markdown レポートを PR コメントとして投稿する
bca report markdown は PR/MR コメント専用に設計されています。安定したヘッダー構造、ホットスポットごとに 1 行のテーブル、そして --strip-prefix を渡せば短いパスが得られます。marocchino/sticky-pull-request-comment と組み合わせると、プッシュのたびに新しいコメントが積み重なるのではなく、単一のコメントが更新されます:
name: bca-pr-report
on:
pull_request:
branches: [main]
jobs:
report:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Install bca
uses: taiki-e/install-action@v2
with:
tool: big-code-analysis-cli@2.0.0
- name: Generate report
run: |
bca \
report -O markdown \
--paths "$PWD" \
--top 20 \
--strip-prefix "$PWD/" \
--output report.md
- name: Post or update PR comment
uses: marocchino/sticky-pull-request-comment@v2
with:
path: report.md
header: bca-quality-report
同じ Markdown ファイルは、PR コメントに加えてワークフロー実行ページからもダウンロードできるようにしたい場合、ビルドアーティファクトとしてのアップロード(actions/upload-artifact@v7)にも適しています。
ベースライン / ラチェットパターン
bca check --baseline はネイティブのラチェットです。今日時点の違反をコミット済みの TOML ファイルに記録し、リグレッションと新規違反に対してのみ失敗させ、ファイルを時間をかけて縮小していきます。一度ブートストラップしてコミットし、CI をそのファイルに向けます:
# 開発者マシンで一度だけ実行します。両方のファイルをコミットしてください。
bca check --paths src/ \
--write-baseline .bca-baseline.toml
git add bca.toml .bca-baseline.toml
このスニペットは src/ のみからブートストラップします — 単一クレートのライブラリに適した形です。複数クレートのワークスペースについては実際の動作例を参照してください。その .github/workflows/pages.yml は、ベンダリングされた文法、生成されたツリー、テストをカバーするチェックイン済みの除外セットである --exclude-from .bcaignore を使って、リポジトリ全体をスキャンします。
除外リストはワークフロー・レシピ・ブートストラップで共有してください。 除外セットをリポジトリルートの単一ファイル(
.gitignore/.dockerignoreに倣い、慣例として.bcaignore)に置き、すべてのbca呼び出しを--exclude-from .bcaignoreでそのファイルに向けます。--exclude-fromのパターンはインラインの--exclude <GLOB>フラグと和集合され、1 つの除外セットになります —--excludeは一回限りのアドホックな除外用に残してください。ファイル内の空行と#で始まるコメント行はスキップされます。パターンは--exclude引数と同じ./プレフィックスの規約(ウォーカーが出力する形式)に従います。.bcaignoreの編集は--write-baselineによる更新とセットで行ってください — ベースラインのキーは、ウォーカーがどのファイルを訪れるかに敏感です。
- name: Threshold check with baseline
run: |
bca check --paths src/ \
--baseline .bca-baseline.toml
リグレッションした関数(current value > baseline value)は引き続き失敗します。ベースラインにない新規の違反も引き続き失敗します。改善された関数は黙って通過し、次の --write-baseline による更新までベースラインに残ります。
Each surviving violation row is prefixed with a tag so a developer can tell at a glance whether they are looking at a brand-new offender or a known one that has worsened:
[new]— この関数 / メトリクスに対するベースラインエントリがありません。[regr +N%]— 現在値が記録されたベースラインをNパーセント超過しています。特殊形式として、ベースライン値がゼロだった場合は[regr from 0]、リグレッションがベースラインの 100 倍を超える場合は[regr +>9999%]、現在値が NaN の場合は[regr NaN]となります。
The violation rows go to stdout; everything below goes to stderr. See Which stream for the full split.
After the per-violation rows, stderr carries a per-file rollup footer with the format <path>: <count> violations (worst: <metric> = <value> vs limit <limit> at L<start>), sorted by violation count descending. This is intended to be the first thing a reader looks at: which file has the most problems, and which metric is the loudest in that file. Pass --no-summary to suppress the footer for tooling that reads the merged streams.
対処しやすい失敗出力
以下の 4 つのサブセクションは、bca check の失敗出力を「違反行の羅列」から CI を意識した表現のスタックへと変えます。この PR のどのファイルがしきい値に触れたか(--since / --changed-only)、インラインのファイル diff アノテーション(--github-annotations)、レンダリングされたステップサマリーのダイジェスト($GITHUB_STEP_SUMMARY)、そしてコピー&ペーストしても安全な修復ブロックです。それぞれ独立しているので、CI サーフェスごとに自由に組み合わせられます。組み合わせた実例はこのグループの最後にあります。
差分対応モード(--since / --changed-only)
PR やプッシュで開発者が最初に抱く疑問はたいてい、ツリー全体の違反リストではなく、この変更に含まれる自分のファイルのうちどれが しきい値に触れたのか、です。2 つのフラグがそれに答えます:
--since <ref>はファイル別フッターを、「Files in this range:」セクション(<ref>とHEADの間で変更されたファイルにある違反)と、それに続く「Other offenders:」(それ以外すべて)に分割します。違反ごとの行は変更されないため、grep を前提とした既存のツールはそのまま動作し続けます。--changed-onlyは、変更されたファイル群の外にあるファイルの違反を完全に除外します。簡潔さが求められる PR ゲートに使用してください。
- uses: actions/checkout@v4
with:
# `--since origin/<base>` はマージベースを解決します。デフォルトの
# `fetch-depth: 1` チェックアウトではその ref に到達できません。`0` を
# 指定すると全履歴を取得するため、差分ベースが解決できます。
fetch-depth: 0
- name: 差分対応フッター付きのしきい値チェック
run: |
bca check --paths . --exclude-from .bcaignore \
--baseline .bca-baseline.toml \
--since "origin/${{ github.base_ref }}"
--since を省略すると、bca は次の優先順位で環境から差分ベースを自動検出します。
BCA_DIFF_BASE— 明示的にオーバーライドするための手段です。ローカルシェルや GHA 以外の CI ランナーから自動検出を模倣するために使用します。GITHUB_BASE_REF— GitHub Actions がpull_requestイベントで設定します。origin/<value>に展開されます。対応するgit fetchはランナー側の責任です。GITHUB_EVENT_BEFORE— GitHub Actions がpushイベントで、プッシュ前の HEAD の SHA に設定します。すべてゼロの SHA(強制プッシュや新規ブランチ)はシグナルなしとして扱われます。
自動検出の失敗(git がない、ref が解決できない、git チェックアウトではない)は、--changed-only なしでは致命的ではありません。bca は警告を表示し、従来どおりのツリー全体のフッターにフォールバックします。--changed-only を指定した場合、同じ失敗は致命的になります。設定を誤った CI がすべての違反を抑制して黙って合格扱いになることを防ぐためです。
"Files in this range:" バナーには、解決されたベースとそれを導いたシグナルが表示されるため、CI ログの読者はゲートが期待どおりの ref を捕捉したことを確認できます。
Files in this range (diff base: origin/main via GITHUB_BASE_REF):
./src/a.rs: 1 violation (worst: cyclomatic = 11 vs limit 2 at L1)
Other offenders:
./src/b.rs: 1 violation (worst: cyclomatic = 11 vs limit 2 at L1)
これは bca diff-baseline とは異なります。bca diff-baseline はディスク上の 2 つのパス間でベースラインファイルを比較し、追加・削除・悪化・改善されたエントリを報告します。--since は 2 つの git ref 間でソースファイルを比較します。
GitHub Actions のインラインアノテーション(--github-annotations)
GHA の UI は ::error file=…,line=…,title=…::msg ワークフローコマンドをファイル差分ビュー上のインラインアノテーションとして表示します。生のジョブログをスクロールするよりはるかに見つけやすい方法です。bca check は、3 状態の --github-annotations <auto|always|never> に従って違反ごとに 1 件ずつ出力します。auto(デフォルト)は $GITHUB_ACTIONS == "true"(すべての GHA ワークフローステップで設定されます)のときに有効化し、always は常に有効にし、never はステップ内でも抑制します(ワークフローが bca check を 2 回実行し、片方の実行だけからアノテーションを得たい場合に便利です)。引数なしの --github-annotations は always を意味します。
アノテーションは既存の違反ごとの人間向けストリームに重ねて出力され、両方が出力されます。GitHub のステップあたり 10 エラーという UI クォータを使い切らないよう、アノテーションはメトリクスごとに 10 件までに制限され、超過分は 1 行の ::error::N more <metric> violations not shown にまとめられるため、件数は常に確認できます。
- name: インラインアノテーション付きのしきい値チェック
run: |
bca check --paths . --exclude-from .bcaignore \
--baseline .bca-baseline.toml
# `--github-annotations` フラグは不要です — GHA では自動的に有効になります。
上記の --since と組み合わせると、アノテーションが違反者リスト全体ではなく PR 内のファイルを指すようになります。
ステップサマリーの Markdown ダイジェスト($GITHUB_STEP_SUMMARY)
GitHub Actions は $GITHUB_STEP_SUMMARY を公開しています。これは Markdown ファイルへのパスで、内容を書き込むとジョブ UI にそのステップのサマリービューとして表示されます。bca check は、この環境変数が設定されているとき、または --summary-file <path> が明示的に渡されたときに、ファイル別ロールアップ表、メトリクス別の件数内訳、value / limit 比による上位 10 件の違反者を含むダイジェストを追記します。--summary-file never は、$GITHUB_STEP_SUMMARY が設定されていてもダイジェストを抑制します。
ダイジェストは HTML コメントマーカー(<!-- bca-step-summary-begin --> / <!-- bca-step-summary-end -->)で囲まれているため、リトライされたステップは前のブロックを(積み重ねるのではなく)置き換えます。3 回リトライしても、最新のダイジェストがちょうど 1 つに収束します。マーカーの外側の内容(同じステップ内で先に他のツールが書き込んだサマリーなど)は保持されます。
- name: ステップサマリーダイジェスト付きのしきい値チェック
run: |
bca check --paths . --exclude-from .bcaignore \
--baseline .bca-baseline.toml
# フラグは不要です — GHA では `$GITHUB_STEP_SUMMARY` が自動的に設定されます。
ローカルユーザーは --summary-file <path> でダイジェストを任意の Markdown ファイルに書き出せます。入力が空(クリーンな実行)でも "✓ No threshold violations." ブロックが書き込まれるため、ステップサマリーからゲートが実行されたことを明確に確認できます。
修復フッター(常時有効)
ゲートが違反を検出すると、bca check は stderr に(さらに上記のステップサマリーダイジェスト内にも)末尾の --- next steps --- ブロックを出力します。
--- next steps ---
* Detailed reports: bca-reports artifact at https://github.com/<owner>/<repo>/actions/runs/<run-id>
* To refresh baseline: bca check --paths . --exclude-from .bcaignore --write-baseline .bca-baseline.toml
* Adoption guide: https://dekobon.github.io/big-code-analysis/recipes/baselines.html
このベースライン更新用の呼び出しは、ゲートで解決された --paths、--exclude、--exclude-from、--config、--baseline をそのまま反映するため、失敗した CI ログを初めて読む人でもそのままコピー&ペーストできます。アーティファクト URL は $GITHUB_REPOSITORY と $GITHUB_RUN_ID の両方が存在するとき(GHA では常に真)に導出されます。ローカル実行では指し示すべきアップロードが存在しないため、代わりに bca report を実行して詳細ビューをローカルで確認することを提案します。
Suppress the block with --no-remediation for tooling that reads the merged streams; a plain bca check | ... pipeline never sees it.
集中的なリファクタリングの後に更新します。
bca check --paths src/ \
--write-baseline .bca-baseline.toml
git diff .bca-baseline.toml # ファイルは縮小していくはずです
変更のないツリーに対して --write-baseline を 2 回実行するとバイト単位で同一の出力が生成されるため、偽の差分は違反者が実際に変化したときにのみ現れます。採用フローの全体、PR レビューのヒューリスティクス、抑制の合成規則についてはベースラインのレシピを参照してください。
すべてを組み合わせる
上記の 4 つのフラグは組み合わせて使えます。PR ゲートのワークフローで推奨される呼び出しは次のとおりです。
- uses: actions/checkout@v4
with:
# `--since origin/<base>` はマージベースを解決します。デフォルトの
# `fetch-depth: 1` ではその ref に到達できません。`0` を指定すると
# 全履歴を取得するため、差分が解決できます。
fetch-depth: 0
- name: しきい値ゲート(差分対応 + GHA UX)
run: |
bca check --paths . --exclude-from .bcaignore \
--baseline .bca-baseline.toml \
--since "origin/${{ github.base_ref }}"
# `--github-annotations` や `--summary-file` フラグは不要です。
# どちらも `$GITHUB_ACTIONS == "true"` と `$GITHUB_STEP_SUMMARY` から
# 自動的に有効になります。末尾の修復ブロックも自動的に
# 出力されます。
失敗した PR で得られるもの:
- Per-violation rows on stdout — same shape as the legacy gate, so existing grep tooling keeps working.
- ファイル別ロールアップフッター —
Files in this range:(PR で変更されたファイル)がOther offenders:の前に列挙されるため、開発者は自分の変更分を最初に確認できます。 - インライン GHA アノテーション — ファイル差分ビュー上に表示され、メトリクスごとに 10 件で打ち切られ、超過分はまとめて表示されます。
- ステップサマリーパネル — レンダリングされた Markdown ダイジェスト(ファイル別ロールアップ、メトリクス別内訳、比率による上位 10 件の違反者)を表示します。
- 末尾の修復ブロック — アーティファクト名を示し、そのまま使える
--write-baseline更新コマンドを表示し、ベースラインのレシピへリンクします。
調整ノブ:
| フラグ | 効果 | デフォルト |
|---|---|---|
--since <ref> | フッターを分割。省略時は環境から自動検出 | オフ。BCA_DIFF_BASE / GITHUB_BASE_REF / GITHUB_EVENT_BEFORE により自動検出 |
--changed-only | 差分外の違反を完全に除外 | オフ |
--github-annotations <auto\|always\|never> | ::error file=…::msg ワークフローコマンドを出力(引数なしのフラグ = always) | auto は $GITHUB_ACTIONS == "true" のとき有効化。never で無効化 |
--summary-file <path\|auto\|never> | Markdown ダイジェストを追記。never で無効化 | auto は $GITHUB_STEP_SUMMARY を検出 |
--no-remediation | 末尾の --- next steps --- ブロックを抑止 | 失敗時にブロックを出力 |
GHA の外で bca check を実行するローカルユーザーにとって、デフォルトの挙動に変化はありません。環境からのシグナルなしに自動有効化される機能は 4 つのうち 1 つもありません。GHA での体験をローカルでプレビューするには次のようにします。
GITHUB_ACTIONS=true GITHUB_STEP_SUMMARY=/tmp/bca-summary.md \
BCA_DIFF_BASE=main \
bca check --paths . --exclude-from .bcaignore \
--baseline .bca-baseline.toml
cat /tmp/bca-summary.md
GHA 以外の CI(GitLab、Buildkite、Jenkins)では、ランナーが公開する環境変数を設定する(またはフラグを明示的に渡す)ことで、同じ出力経路が動作します。
マージベースに対する違反者数の差分(暫定策)
(ポリシー上の理由などで)ベースラインファイルをコミットできないチーム向けのより粗い近似として、2 つの Checkstyle ドキュメント(マージベース上のものと PR ヘッド上のもの)の <error> 要素を数え、件数が増えたら失敗させる方法があります。
- name: マージベースに対する違反者の差分を計算
run: |
set -euo pipefail
BASE="$(git merge-base origin/main HEAD)"
git worktree add /tmp/base "$BASE"
bca check --paths /tmp/base \
--report-format checkstyle \
--output /tmp/base.xml \
--no-fail
BASE_COUNT=$(grep -c "<error" /tmp/base.xml || true)
bca check --paths "$PWD" \
--report-format checkstyle \
--output /tmp/head.xml \
--no-fail
HEAD_COUNT=$(grep -c "<error" /tmp/head.xml || true)
echo "Offenders: base=$BASE_COUNT head=$HEAD_COUNT"
if [ "$HEAD_COUNT" -gt "$BASE_COUNT" ]; then
echo "::error::Offender count grew from $BASE_COUNT to $HEAD_COUNT"
exit 1
fi
これは違反の件数を数えるだけで、その同一性は追跡しません。違反している関数の名前を変えてもリグレッションとして検出されず、ある違反者を改善しつつ別の違反者を悪化させると差し引きゼロになります。上記のネイティブなベースラインフローの方が厳密に高精度であり、推奨されるアプローチです。
セルフスキャンしきい値ゲート(CI ゲートのローカルミラー)
CI のしきい値ゲートはプッシュ後にしか作動しないため、リファクタリングが気付かないうちにメトリクスを限界値の先へ押し出していた場合には手遅れです。big-code-analysis リポジトリの Makefile は、CI ゲート(.github/workflows/pages.yml の Threshold gate ステップ)をローカルでミラーする 4 つのターゲットを公開し、さらに各限界値の 95% に第 2 の段階を追加して、ハードゲートが作動する 1〜2 コミット前に接近を検出できるようにしています。
make self-scan # ハードゲート。bca.toml のしきい値の 100%
make self-scan-headroom # ソフトゲート。デフォルト 95%(BCA_HEADROOM)
make self-scan-write-baseline # ハードしきい値でベースラインを更新
make self-scan-write-baseline-headroom # ソフトしきい値でベースラインを更新
パス選択、.bcaignore の除外セット、関数ごとのしきい値、循環的複雑度の ? ポリシー、ベースラインファイルは、すべてリポジトリルートの bca.toml マニフェストにあり、bca はこれを自動的に発見します。ハード段階は CI が実行するものとまったく同じです。展開すると、引数なしのチェック(パス / しきい値 / ベースラインのフラグなし — マニフェストが供給します)になります。
cargo run --quiet --release -p big-code-analysis-cli -- check
両段階は同じ bca.toml のしきい値と同じ .bca-baseline.toml を使用します。ソフト段階は、各しきい値に BCA_HEADROOM を掛けた上でハード段階のレシピを実行するだけです。どちらもクリーンなら 0、しきい値違反があれば 2、ツールエラーなら 1 で終了します。ソフト段階は助言的なものではなく本物のゲートなので、make self-scan-headroom を || true で包まないでください。2 つのゲートターゲット(self-scan、self-scan-headroom)は make pre-commit、make ci、.pre-commit-config.yaml に組み込まれています。これらのチェーンはハード段階をソフト段階より先に実行するため、真のリグレッションは常に限界値接近の警告より先に報告されます。2 つの write-baseline ターゲットは副作用を伴うため、意図的に組み込まれていません。
BCA_HEADROOM=0.90 make self-scan-headroom は帯域を広げ、BCA_HEADROOM=0.99 は最後の 1% まで狭めます。ソフト段階が作動したときは、make self-scan-write-baseline-headroom(すべての違反者をスケール後のしきい値で記録します — ハード段階の違反者の厳密な上位集合です)でベースラインに違反者を取り込みます。
このパターン(CI をミラーするハード段階 + 早期警告帯としてのソフト段階、どちらも同じベースラインでラチェット)はプロジェクトに依存しません。ローカルしきい値ゲートのレシピには、基礎となる原則、そのまま使える Makefile / just / package.json のスケルトン、しきい値をスケールするヘルパースクリプトが記載されており、同じワークフローを自分のリポジトリでも採用できます。汎用レシピは上記の Makefile と同じ BCA_* 環境変数名を使うため、BCA_HEADROOM=0.90 のようなオーバーライドは両方でまったく同じように機能します。
GitLab CI
完全な .gitlab-ci.yml の例
以下のジョブは bca をインストールし、しきい値チェックを実行して Code Climate JSON(MR の Code Quality ウィジェット用)、Checkstyle XML、Markdown レポートを生成し、それらをアーティファクトとしてアップロードします。
GitHub Actions セクションの CLI アーティファクトのスキーマ互換性に関する注意がここでも当てはまります。
BCA_VERSIONのピンは、リポジトリにコミットするすべての CLI アーティファクトのスキーマバージョンをカバーしなければなりません。
stages:
- quality
variables:
BCA_VERSION: "2.0.0" # 公開済みの big-code-analysis-cli リリースにピン留め
BCA_TARGET: "x86_64-unknown-linux-gnu"
# リリースの SHA256SUMS ファイルに記載された
# big-code-analysis-${BCA_VERSION}-${BCA_TARGET}.tar.gz の sha256。BCA_VERSION と一緒に更新します。
BCA_SHA256: "a205fff13108d0f8c679a062e352ba8468109c4adfdd8c9e3567cf5fcc99c3d5"
bca-quality:
stage: quality
image: debian:stable-slim
cache:
# GitHub Actions のスニペットと同じキー形式です。BCA_VERSION を
# 上げるとキャッシュは自動的に無効化されます。
key: "bca-$BCA_VERSION"
paths:
- .cache/bca/
before_script:
- apt-get update -qq && apt-get install -y --no-install-recommends ca-certificates curl tar
- |
set -euo pipefail
install -d "$CI_PROJECT_DIR/.cache/bca" "$HOME/.local/bin"
if [ ! -x "$CI_PROJECT_DIR/.cache/bca/bca" ]; then
stage="big-code-analysis-${BCA_VERSION}-${BCA_TARGET}"
tarball="${stage}.tar.gz"
url="https://github.com/dekobon/big-code-analysis/releases/download/v${BCA_VERSION}/${tarball}"
curl -fsSL --proto '=https' --tlsv1.2 -o "/tmp/${tarball}" "$url"
echo "${BCA_SHA256} /tmp/${tarball}" | sha256sum --check --strict -
tar -xzf "/tmp/${tarball}" -C /tmp
install -m 0755 "/tmp/${stage}/bca" "$CI_PROJECT_DIR/.cache/bca/bca"
rm -rf "/tmp/${tarball}" "/tmp/${stage}"
fi
install -m 0755 "$CI_PROJECT_DIR/.cache/bca/bca" "$HOME/.local/bin/bca"
export PATH="$HOME/.local/bin:$PATH"
script:
- bca
check
--paths "$PWD"
--report-format code-climate
--output gl-code-quality-report.json
--no-fail
- bca
check
--paths "$PWD"
--report-format checkstyle
--output bca-checkstyle.xml
--no-fail
- bca
report -O markdown
--paths "$PWD"
--top 20
--strip-prefix "$PWD/"
--output bca-report.md
# しきい値ゲートは別に実行するため、上記のアーティファクトは失敗時にも
# 公開されます。終了コード 2 = 少なくとも 1 件のしきい値超過。
- bca check --paths "$PWD"
artifacts:
when: always
reports:
codequality: gl-code-quality-report.json
paths:
- gl-code-quality-report.json
- bca-checkstyle.xml
- bca-report.md
この例に関する補足です。
- 最初の 2 回の
bca check … --no-fail呼び出しはアーティファクト用に違反者を収集します。最後のbca check(--no-failなし)が合否を決めるゲートです。3 回の実行はすべて同じしきい値設定を使うため、アーティファクトは常にゲートの判定と一致します。 artifacts:when: alwaysにより、パイプラインが赤のときでも — まさに最も必要になる場面ですが — すべてのアーティファクトをダウンロードできます。artifacts:reports:codequalityは Code Climate JSON を GitLab の MR Code Quality ウィジェットに直接接続します。フィールドごとの意味は下記の Code Quality ウィジェットのセクションを参照してください。
GitLab Code Quality ウィジェット
GitLab の第一級の Code Quality 体験(MR 差分上のインライン指摘、MR 概要ページのサマリー)は Code Climate JSON を読み取ります。bca check は --report-format code-climate によりこれをネイティブに出力するため、統合は 1 行で済みます。
code_quality:
stage: quality
script:
- bca check --paths "$CI_PROJECT_DIR"
--report-format code-climate
--output gl-code-quality-report.json
--no-fail
artifacts:
when: always
reports:
codequality: gl-code-quality-report.json
paths:
- gl-code-quality-report.json
重大度の帯は、各メトリクスが設定されたしきい値をどれだけ超えているか(value / limit 比。値が低いほど悪い保守容易性指数系では反転)から導出されます: ≤ 1.5× → minor、≤ 2× → major、≤ 4× → critical、> 4× → blocker。ウィジェットは fingerprint で検出結果を重複排除します。bca は path \0 function \0 metric を(行番号も値も含めずに)ハッシュ化するため、変更による行ずれを生き延びた違反は、パイプライン実行をまたいで同じウィジェットエントリに集約されます。
生成されたレポートをローカルで検証するには次のようにします。
jq 'all(.[]; has("description") and has("check_name")
and has("fingerprint") and has("severity")
and has("location"))' gl-code-quality-report.json
# → true
jq '[.[] | .severity] | unique' gl-code-quality-report.json
# → ["info","minor","major","critical","blocker"] の部分集合
Markdown レポートを MR 限定コメントとして投稿
Markdown レポートを MR ノートとして添付する(GitHub の PR コメントレシピの GitLab 版)には、プロジェクトアクセストークンと Notes API を使用します。
bca-mr-comment:
stage: quality
image: alpine:3
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
needs: ["bca-quality"]
before_script:
- apk add --no-cache curl jq
script:
- |
BODY=$(jq -Rs '.' < bca-report.md)
curl --fail --silent --show-error \
--request POST \
--header "PRIVATE-TOKEN: $CI_BCA_BOT_TOKEN" \
--header "Content-Type: application/json" \
--data "{\"body\": $BODY}" \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"
CI_BCA_BOT_TOKEN は api スコープを持つプロジェクトアクセストークンです。このジョブは bca-quality に依存するため、実行前に Markdown アーティファクトが揃っています。
Jenkins / SonarQube
Jenkins(Warnings Next Generation プラグイン経由)と SonarQube(Generic Issue インポーター経由)は、どちらも Checkstyle 4.3 XML を直接読み取ります。同じ呼び出しが両方に使えます。
bca check --paths src/ \
--report-format checkstyle \
--output report.checkstyle.xml
report.checkstyle.xml を、既存の Jenkins の Record Issues / SonarQube の External Issues ステップに接続してください。Checkstyle ライターは違反者がいないときも空の(整形式の)ドキュメントを出力するため、どちらのツールもクリーンな実行を特別扱いする必要はありません。ライターのスキーマの詳細は check コマンドのページを参照してください。
汎用的な CI ガイダンス
以下はプロバイダーを問わず当てはまります。
bcaは特定のバージョンにピン留めしてください。cargo install --versionとcargo binstall --versionはどちらもbig-code-analysis-cliの公開クレートバージョンを受け付けます。バージョンを固定しないインストールでは、メトリクス計上の変更が月曜日に「不可解な CI の揺らぎ」として現れます。CLI アーティファクトのスキーマ(ベースライン、しきい値)がリポジトリにコミットしているファイルと一致するバージョンにピン留めしてください — インストールセクションのスキーマ互換性に関する注意を参照してください。--jobsのデフォルトは有効 CPU 数です。 このフラグはavailable_parallelism()に従います — Linux では cgroup / cpuset / クォータを考慮し、macOS / Windows では OS の CPU 数を使います — ので、CI ランナーであらゆるレシピに--jobs "$(nproc)"を通す必要はもうありません。--jobs 1はデフォルトではなく、デバッグ用のノブとして残っています。bca report markdownには常に--strip-prefix "$PWD/"を渡してください。 そうすればパス列は、ワークスペースパスが異なるランナー間でも同一になります。これがないと、2 つのレポートの差分は/home/runner/work/...と/builds/group/project/...のノイズに支配されます。bca.tomlはリポジトリルートに置いてください —Cargo.toml/pyproject.toml/package.jsonと同じ場所です。bcaはこれを自動的に発見するため、引数なしのbca checkがコミット済みのしきい値、パス、ベースラインを読み込みます。これをソースコードとして扱い、しきい値の緩和はコードレビューで審査してください。- 終了コードの契約。
bca checkは、クリーンなら0、しきい値違反があれば2、ツールエラー(不正な設定、未知のメトリクス、読み取れないパス)なら1で終了します。1をツールエラー専用に予約することで、CI は「関数が複雑になりすぎた」と「アナライザーがクラッシュした」を区別できます。--exit-codes=tieredを渡す(またはbca.tomlに[check] exit_codes = "tiered"を設定する)と、違反の場合を重大度で分割できます:2は新規違反者のみ、3はリグレッションのみ、4は両方、5はハード限界も同時に超えた--tier=soft違反です。tiered の終了コードはオプトインで、デフォルトは0/1/2のままです。すべての失敗状態は非ゼロのままなので、exit != 0 → fail型のラッパーはそのまま機能します — 明示的に$? -eq 2を検査するツールだけが2-5に広げる必要があります。 - ソース内の抑制マーカーを尊重し、監査には
--no-suppressを使ってください。 デフォルトのbca checkはbca: suppress/bca: suppress-fileマーカーを尊重します。--no-suppressを渡すとそれらを無視するため、監査者は生の違反者リストを確認できます。
ベースライン: 既存コードでしきい値をラチェットする
既存のコードベースにメトリクスのしきい値を導入すると、たいてい同じ壁にぶつかります。妥当なしきい値はどれも既存の関数を何百件もフラグし、CI はプッシュのたびに赤になります。現実的な採用経路は「現状からラチェットし、新規の違反者だけを失敗させる」ことです。ベースラインファイルは、bca check がそのワークフローを支えるための仕組みです。
ベースラインはソース内の抑制マーカーを補完するものであり、その代替ではありません。関数が意図的に恒久的に複雑な場合(パーサー、状態機械、生成コード)は抑制マーカー(抑制マーカー)を使ってください。チームが負債を返済していくつもりの場合はベースラインを使います。両方を同じリポジトリで併用でき、抑制の方が先に評価されます。
エンドツーエンドの採用フロー
ワンショットのショートカット:
bca initは、統合されたbca.tomlマニフェスト(paths、exclude_from、baseline、[thresholds]テーブルを含む)、それが参照する.bcaignore、そして現在のツリーから導出した初期.bca-baseline.tomlを 1 つのコマンドでスキャフォールドします。マニフェストが配置されていれば、引数なしのbca checkがそれを自動発見し、設定なしでゲートします。既存ファイルを上書きするには--forceを、ツリーの走査をスキップするには--no-baselineを渡します。以下の長めのレシピは、ベースラインをブートストラップする前にしきい値を調整したい場合に有用です。
1. 初期しきい値を決める
感覚的な数値(cyclomatic=15、cognitive=20)を使うか、リポジトリ全体に対する bca check --no-fail の実行結果から現在の分布を確認して決めます。
# bca.toml — リポジトリのルートに置くと `bca check` が自動発見します。
paths = ["src"]
[check]
baseline = ".bca-baseline.toml"
[thresholds]
cyclomatic = 15
cognitive = 20
"loc.lloc" = 200
2. ベースラインをブートストラップする
bca check --write-baseline
パスなしの --write-baseline は、先ほど作成した bca.toml の baseline キーに書き込むため、ファイル名は 1 か所だけで管理されます。明示的なパス(--write-baseline <file>)を渡すのは、デフォルトとして使えるマニフェストの baseline がない場合だけにしてください。マニフェストがない場合、パスなしの形式はファイル名を推測せずエラーになります。
両方のファイルを同じ変更でコミットします:
git add bca.toml .bca-baseline.toml
git commit -m "ci: introduce metric thresholds with baseline"
ベースライン内のパスキーは、ベースラインファイル自身のディレクトリ(アンカー)からの相対パスで保存されます。--paths .、--paths src/、--paths "$PWD" はバイト単位で同一のベースラインを生成し、CI がどの --paths 形式を使っても --baseline の実行結果は一致します。--write-baseline を再実行することなく、自由に切り替えられます。
3. CI ゲートを組み込む
GitHub Actions:
- name: Check code complexity thresholds
run: |
bca check
# `paths`、しきい値、`baseline` はすべてリポジトリルートで
# 自動発見された `bca.toml` マニフェストから取得されます。
GitLab CI(該当ジョブ向けのスニペット):
threshold-check:
image: rust:1
before_script:
- cargo install --locked big-code-analysis-cli@<VERSION>
script:
- bca check
終了コード: 0 はクリーン、2 はリグレッションまたは新規違反、1 はツールエラーです。CI 環境ごとの幅広い対応表については CI 統合 を参照してください。
4. チームの負債返済に合わせてベースラインを更新する
数週間ごと、または集中的なリファクタリングの後に:
cp .bca-baseline.toml .bca-baseline.old.toml
bca check --write-baseline
bca diff-baseline .bca-baseline.old.toml .bca-baseline.toml
diff が縮小していくことが目標です。変更のないツリーに対して --write-baseline を 2 回実行するとバイト単位で同一の出力が生成されるため、余計な diff が現れるのは実際の違反箇所が変わったときだけです。
Before tightening a limit, price it at both tiers
Paying debt down invites tightening the limit that produced it, and the obvious measurement — bca check --threshold <metric>=<candidate> — answers only half the question. A --threshold value is applied last and absolutely, never scaled, so it has no soft tier: a candidate that costs nothing at the hard gate can still put a whole population permanently inside the --tier=soft band, and you will not see it until you have edited bca.toml and run the other gate.
bca check --explain-threshold cognitive=15
Weigh the new column, not the offender count: it is how many baseline entries the change would add, and it is what a reviewer is actually being asked to approve. A cluster: line means the candidate landed on top of an existing population, and those entries can never be retired — see Tightening a limit onto a cluster.
5. PR レビューのヒューリスティクス
生の git diff .bca-baseline.toml を頭の中で解析する代わりに、bca diff-baseline <old> <new> を実行してサマリーを読みます。エントリは (path, qualified, metric) のアイデンティティで対応付けられるため、ファイル内で位置が上下しただけの関数は削除 + 追加としては報告「されず」、実際の変更はすべて次のバケットに分類されます:
1 added, 1 removed, 2 worsened, 0 improved
## Added
src/new.rs::shiny cognitive = 30
## Removed
src/gone.rs::old_fn nargs = 9
## Worsened
src/bar.rs::act_on_file cognitive 60 → 63
src/foo.rs::do_thing cognitive 25 → 27
各バケットを従来のヒューリスティクスに対応付けると:
removed(ベースラインが縮小)。 負債が返済されました。追加の対応は不要です。added(ベースラインが拡大)。 誰かが意図的に新しい違反をファイルに追加しました。値をレビューしてください — これは意図的な暫定措置だったのか、それとも作者がゲートを回避したのか。意識的な判断であればどちらでも構いません。このファイルをコミットする意義は、その選択をレビュー可能にすることにあります。worsened(エントリのvalueが上昇)。 関数が悪化した後に作者が--write-baselineを再実行しました。addedと同様に扱い、レビューでその変更を明示してください。improved。 記録済みの違反がベースラインから外れることなく改善しました。無害であり、リファクタリングが機能している良い兆候です。
PR ボット向けには、bca diff-baseline <old> <new> --format markdown がスティッキーコメントにそのまま貼り付けられるフェンス付きブロックを出力し、--worsened-only / --added-only フィルタでレビュアーが必ず見るべきリグレッションだけに絞り込めます。--format json は同じ diff を他のツールに渡せます。このコマンドはデフォルトで終了コード 0 を返します — レビューに情報を提供するものであり、ゲートではありません(ゲートは bca check 自体です)。ただしオプトインの --exit-code フラグを使うと、フィルタ後の diff が空でない場合に終了コード 2 を返します。
ゲート出力の読み方
bca check --baseline の実行が失敗すると、残った各違反にタグのプレフィックスが付き、リストの後にファイルごとのロールアップが続きます:
bca: filtered 422 violations via baseline
[regr +60%] src/foo.rs:1-865: <file>: halstead.effort = 1557107.72 (limit 50000)
[new] src/bar.rs:506-747: act_on_file: cognitive = 63 (limit 25)
...
--- summary ---
src/foo.rs: 5 violations (worst: halstead.effort = 1557107.72 vs limit 50000 at L1)
src/bar.rs: 4 violations (worst: cognitive = 63 vs limit 25 at L506)
タグのプレフィックス:
[new]— 修飾シンボル(行トレランスの範囲内)でも、--baseline-fuzzy-match指定時のボディハッシュでも、この違反に一致するベースラインエントリがありません。ベースライン作成後に新たに発生した違反です。解決順序についてはマッチングを参照してください。[regr +N%]— ベースラインに記録された値があり、現在の値がそれよりN%高い状態です。特殊なケース:[regr from 0]— 記録された値が0.0で、非ゼロのパーセンテージがゼロ除算になる場合。[regr +>9999%]— リグレッションがベースライン値の 100 倍を超えた時点でこの上限表記になります。[regr NaN]— 現在のメトリクス値が NaN の場合(自明な関数における退化した Halstead 入力)。
Tags only appear when --baseline is passed; without it the line format is byte-identical to the no-baseline default. CI tooling that reads the merged streams can suppress the trailing summary with --no-summary.
サマリーフッターは違反をファイルごとにグループ化し、ファイルごとに最悪のメトリクス 1 件(value / limit 比の最大値)を提示し、行を違反数の降順、次にパスの昇順でソートします。長い違反リストを読み、どのファイルから着手すべきかを見つける最速の方法です。
Merging two branches
A baseline is generated wholesale, so a textual merge of two branches that both touched it is never right: each side's values were measured against its own tree, and the merged tree matches neither. Tell git not to try, with one line in .gitattributes:
.bca-baseline.toml -merge
Git then leaves the file conflicted as a whole instead of splicing the two sides together, and the resolution is always the same — regenerate:
bca check --write-baseline
git add .bca-baseline.toml
Unlike a merge=ours driver, -merge needs no per-clone git config, so it works for everyone who clones the repository rather than only for those who already knew to set it up.
6. ベースラインを退役させる
When .bca-baseline.toml contains only version = 6 and no entries, drop the --baseline flag from CI and delete the file. The thresholds now stand on their own.
ティア/ヘッドルームの来歴
--write-baseline(v5 以降)で書き込まれたベースラインは、「どのゲートに対して書き込まれたか」を [provenance] テーブルに記録します:
version = 6
[provenance]
tier = "soft"
headroom = 0.95
tier = "hard"— ハードゲート(bca check --write-baseline …)によって書き込まれたもの。headroomキーはありません。tier = "soft",headroom = <ratio>— ソフト比率でスケールされたソフトゲート(bca check --tier=soft=0.95 --write-baseline …)によって書き込まれたもの。tier = "soft"でheadroomなし —[thresholds.soft]テーブル(メトリクスごとの制限で、単一の比率なし)に駆動されるソフトゲートによって書き込まれたもの。
この来歴はコメントではなく本物の TOML テーブルなので、bca diff-baseline や外部ツールが読み取れます。古い bca(v2–v4)で書き込まれたベースラインは来歴を持ちませんが、エラーなく読み込まれます。
ベースラインより厳しい場合の警告
bca check は、ベースラインの来歴と現在の実行の実効制限を単一の「厳格度」スカラーに縮約し(ハード → 1.0、h でスケールされたソフト → h、小さいほど厳しい)、現在の実行がベースライン書き込み時よりも*「厳しい」*場合に警告します:
warning: this check's effective limits (strictness 0.9) are stricter
than the baseline was written against (strictness 0.95); the baseline
may under-cover and the gate can fire on untouched files. Refresh it at
the matching tier, …
これはベースライン更新の規律が防ごうとしているサイレントな不整合です。現在のゲートより「緩く」書き込まれたベースラインは、より厳しいゲートが検出するすべての違反を網羅していない可能性があり、誰も触れていないファイルで突然ゲートが発火することがあります。
この警告は方向性を持ちます。現在の実行のほうが厳しい場合にのみ発火します。安全な方向では沈黙します — ハードチェック(厳格度 1.0)がソフト 0.95 のベースラインを読む場合、そこには自身の違反の「上位集合」が含まれており、これはまさに make self-scan(ハード)と make self-scan-headroom(ソフト)が同じ .bca-baseline.toml を通じてラチェットする、意図された単一ベースライン構成です。来歴が等しい場合、v5 より前のベースライン(来歴不明)の場合、どちらか一方が [thresholds.soft] テーブル方式のベースライン(比較すべき単一の比率がない)である場合も沈黙します。本当の警告を解消するには、対応する --write-baseline レシピを使って現在のティアでベースラインを更新します。
マッチングの仕組み
各エントリは (path, qualified_symbol, metric) をキーとします。修飾シンボルとは、囲んでいる名前付きコンテナを :: で連結した連鎖に関数名を加えたものです(MyStruct::do_thing、my_namespace::MyClass::method)。ファイルのトップレベルのスペースは <file> に畳み込まれます。違反は次の順序でベースラインに対して解決されます:
-
修飾シンボル。 違反の
(path, qualified_symbol, metric)を共有するエントリがちょうど 1 つであれば、行番号に関係なく一致します。そのため、関数より上のコードを編集しても[new]として再キー化されることはなくなりました。 -
開始行トレランス。 複数のエントリがそのキーを共有する場合(アナライザが区別できなかった異なる
implブロック上の同名メソッドis_valid、オーバーロードなど)、記録されたstart_lineが違反に最も近く、かつ--baseline-line-tolerance行以内(デフォルト 50)にあるエントリが選ばれます。トレランスを超えた違反は[new]になります。This is the only rule that reads a line number, so from schema v6 a
start_lineis written only for entries in such a group. Everywhere else the field is absent, and the file therefore does not change when an edit above a baselined function moves it — which is what keeps a real baseline change (avaluemoving) visible in a diff instead of buried in line-number noise. -
ボディハッシュ(オプトイン)。
--baseline-fuzzy-matchを指定すると、修飾シンボルがもはや一致しない違反は、同じ(path, metric)内で正規化されたボディハッシュが同一のエントリと照合されます。これにより、関数の形を保ったままのリネームを吸収できます(ダイジェストは関数自身の名前を除外し、インデント・空行・CRLF の影響を受けません)。ハッシュは--baseline-fuzzy-matchが設定されている場合にのみベースラインに書き込まれるため、ファジー読み取りを有効にするには一度ファジーな--write-baselineでシードしてください。両方のキーはbca.tomlの[check]の下にbaseline_line_toleranceおよびbaseline_fuzzy_matchとして設定します(トップレベルに直接書く綴りは非推奨で警告が出ます。#599 を参照)。
匿名関数(クロージャ、ラムダ)には安定した名前がないため、その修飾シンボルには行番号が焼き込まれます(outer::<anon@L42>)。したがって移動すると [new] として再キー化されます。シンボルによる対応付けが行のずれに耐えるのは、ベースラインのチャーンの大半を生む「名前付き」のトップレベル関数とメソッドに束縛された関数だけです。
修復フッター
When the gate finds violations, bca check emits a trailing --- next steps --- block on stderr — the offender rows themselves go to stdout — and inside the $GITHUB_STEP_SUMMARY digest, that names the artifact, prints a copy-paste-safe --write-baseline refresh invocation, and links back to this recipe. The refresh invocation mirrors the gate's resolved --paths / --exclude / --exclude-from / --config / --baseline arguments, so a first-time reader of a failing CI log can refresh the baseline without leaving the page.
Suppress the block with --no-remediation if downstream tooling reads the merged streams and the trailing block confuses it.
抑制マーカーとの組み合わせ
--write-baseline は、bca: suppress または #lizard forgives マーカーで抑止された関数をあらかじめ除外するため、同じ関数が 2 か所に登録されることはありません。関数を恒久的に除外するつもりであれば、ソース内マーカーを優先してください(コードの隣に置かれ、リファクタリングに耐え、コミットすべき追加ファイルもありません)。ベースラインは、チームが本当に修正するつもりのある違反にのみ使ってください。
フィルタされていない違反の全体 — 抑制やベースラインに関係のないすべての違反 — を監査するには、--no-suppress を渡し、--baseline を省略します:
bca check --paths src/ \
--no-suppress \
--no-fail
--write-baseline と組み合わせると、--no-suppress は抑制マーカーが通常隠す違反も含め、すべての違反を記録します。
すべての除外を一度に監査する
ベースラインは、コードがゲートを逃れる 3 つの方法のうちの 1 つです。残りの 2 つは、ソース内の bca: suppress マーカーと [check.exclude] グロブです。bca exemptions はこの 3 層すべてを 1 つのレポートに列挙するため、レビュアーは 3 つのコマンドを実行することなく、bca check がスキップしているものを一覧できます:
bca exemptions --paths src/
# In-source markers (2)
src/parser.rs:120 bca: suppress metrics=all parse_long
...
# [check.exclude] globs (1)
tests/**
# Baseline (.bca-baseline.toml, 417 entries)
src/markdown_report.rs write_language_section cognitive 29
...
ベースラインのセクションは、bca check と同じ --baseline / bca.toml の [check] baseline ソース(またはデフォルトの .bca-baseline.toml)を読み取ります。ベースライン化された違反だけを列挙するには --baseline-only を、PR コメント用には --format markdown を、ダッシュボード用には --format json を使います。PR レビュー時には、前述の bca diff-baseline <old> <new> と組み合わせてください。diff はベースラインで何が「変わった」かを示し、bca exemptions は現在の除外面の全体を示します。完全なフラグリファレンスは抑制マーカーのページを参照してください。
制限事項
- あいまいなシンボル。 2 つの関数が修飾シンボルを共有し(アナライザが異なるコンテナを解決できなかった、または言語がオーバーロードを許している)、かつ両方が記録された行から
--baseline-line-toleranceを超えてずれている場合、どちらも判別できず、違反は[new]として現れます。--write-baselineで更新するか、トレランスを引き上げてください。 - 匿名関数。 クロージャとラムダは、その合成シンボルに行番号が埋め込まれているため、移動すると再キー化されます(マッチングの仕組みを参照)。
- OS 間の可搬性。 パスは書き込み時にスラッシュ区切りへ正規化され、読み取り時にも再正規化されるため、Linux で生成したベースラインは Windows 上の同じツリーと一致します。UTF-8 でないパスは損失のある表示形式にフォールバックし、正確にラウンドトリップしない場合があります。
- しきい値の引き締め。 制限を下げると、これまでクリーンだった関数が新たに露出することがあります。それらはベースラインに含まれないため、CI は失敗します。これは正しい挙動です — 引き締めは新しい違反を露出させるべきです。チームが新しいエントリを吸収すると決めた場合は、ベースラインを更新してください。
ローカルしきい値ゲート
CI は最後の防衛線であり、最初の防衛線ではありません。bca check(リポジトリルートの bca.toml マニフェストとその .bca-baseline.toml を読み取る)がプルリクエストで赤く点灯する頃には、問題のある変更はすでにプッシュされ、作者はコンテキストを切り替えており、誰かが diff を見直してメトリクスを制限内に押し戻さなければなりません。ローカルのしきい値ゲートは、そのフィードバックを git commit の瞬間 — cargo fmt --check と cargo clippy -- -D warnings がすでに発火するのと同じ瞬間 — に移すため、リグレッションが開発者のキーボードを越えることはありません。
このレシピは、big-code-analysis が自身のソースに対して使っているパターン(Makefile の self-scan* ターゲット。統合された bca.toml マニフェストに支えられています)を捉え、あなたのリポジトリの Makefile、justfile、package.json スクリプト、pre-commit 設定にそのまま入れられる形に蒸留したものです。根底にある考え方はプロバイダ中立です。どのしきい値チェッカー(bca、ESLint、clippy、SonarLint、Qodana)でも同じ方法で組み込めます。
原則
設計を駆動するのは 3 つの原則です。これらは bca に固有のものではありません。Sonar がデフォルトの品質ゲートを新しいコードに焦点を当てる方向へ転換したときに到達したのと同じ結論であり、より広いラチェットパターンが定式化しているものです。
- ローカルでゲートし、CI を正確にミラーする。 ローカルゲートは、CI と同じバイナリを、同じ引数、同じしきい値 / ベースライン / 除外ファイルで実行しなければなりません。ローカルゲートが「CI の実行内容とほぼ同じ」では、両者が乖離した瞬間にリグレッションを捕捉できなくなります。プッシュ前にゲートを一度実行するコストは安く、PR ボットの赤い通知のコストは安くありません。
- ラチェットせよ、リセットするな。 既存のコードベースにしきい値を導入すると、「どんな」妥当な制限でも数十の既存関数で発火します。現実的な導入経路は「今日の違反をベースラインファイルに吸収し、新規または悪化したものだけを失敗させ、ベースラインを時間をかけて縮小する」です。これは、長年運用されてきたコードベースが数か月がかりの全面改修なしに strict TypeScript や厳格な clippy リントを導入できるのと同じ戦略です。ブートストラップ → CI → 更新 → 退役の流れはベースラインのレシピを参照してください。
- 失敗させる前に警告する。 100% のハードゲートは制限「ちょうど」で失敗し、関数がしきい値の 80% から 95%、99% へと忍び寄る間は何の信号も出しません。たとえば各制限の 95% で発火する、より緩い第 2 のティアがあれば、1〜2 コミット分の早期警告になります。作者はまだファイルを開いており、テストケースが頭に入っており、違反が「まあ、もう main に入ってるし」として固定化する前にリファクタリングする自由があります。Sonar の「新しいコード」品質ゲート、GCC の
-Wall/-Werrorの分離、clippy のwarnとdenyのリントレベルは、いずれも同じ洞察を体現しています。「クリーン」と「壊れている」の間のティアこそ、チームが実際にドリフトを捕捉する場所です。
2 つのティア
このパターンは、同じチェッカーをラップする 2 つのレシピと、各ティアでベースラインを更新するための 2 つのレシピから成ります。
| ターゲット | ティア | しきい値 | ベースラインでフィルタ | ユースケース |
|---|---|---|---|---|
self-scan | ハード | 設定の 100% | はい | CI のミラー。すべてのコミットでグリーンを維持しなければなりません。 |
self-scan-headroom | ソフト | tightened by HEADROOM | はい | 早期警告バンド。ハードティアより先に発火します。 |
self-scan-write-baseline | ハード | 設定の 100% | (書き込み) | 今日のハードティア違反を吸収します。 |
self-scan-write-baseline-headroom | ソフト | tightened by HEADROOM | (書き込み) | バンドの導入時や拡大時にソフトティアの違反を吸収します。 |
The hard tier and the soft tier consume the same [thresholds] table and the same .bca-baseline.toml. The only difference between them is the HEADROOM ratio applied to every threshold value before bca check sees it — tightening each limit, which for the lower-is-worse mi.* family means raising it rather than lowering it.
共有ベースラインはソフトティア(self-scan-write-baseline-headroom)で書き込みます。v5 のベースラインは、書き込み時のティアとヘッドルームを [provenance] テーブルに記録し、bca check は現在の実行がベースライン書き込み時よりも「厳しい」場合に警告します。ソフト 0.95 のベースラインはハードゲートの違反の上位集合なので、ハードの self-scan はそれを黙って読みます。逆にハードティアでベースラインを書くと、ソフトの self-scan-headroom が自分のほうが厳しいゲートだと警告するようになります。ティア/ヘッドルームの来歴を参照してください。
これは重要です。ソフトティアをより厳しくしたい(侵食をより早い段階で捕捉したい)貢献者は、環境変数を 1 つ変えるだけで済み、誰かが両方のファイルの更新を忘れた瞬間にハード設定から乖離していく並行のソフトしきい値ファイルを維持する必要がない、ということです。
2 ティアのしきい値
bca check --tier <hard|soft|soft=RATIO> は、どのティアに対してゲートするかを選択します。hard(デフォルト)は [thresholds] をそのまま比較します。soft は早期警告ティアで、次の順序で解決されます:
[thresholds](マニフェスト。--configとマージ済み)から始めます。[thresholds.soft]テーブルが存在する場合、そのオーバーライドを上に重ねます。ソフトテーブルにないメトリクスはハード制限をそのまま継承します(ソフトバンドなし)。ソフトテーブルがある場合、一括のRATIOは適用されません — 明示的なメトリクスごとの制限がスカラーに勝ちます。- Otherwise tighten every limit by the soft
RATIO(default0.95for a baresoft, so--tier=softis never a silent no-op;soft=1.0disables scaling). - 繰り返し指定された
--threshold name=valueフラグは最後に、絶対値として適用されます。
引数なしの --tier=soft(比率 0.95)が設定なしの入り口です。[thresholds.soft] テーブルは、成熟したプロジェクトが育っていく先の設定面です。メトリクスごとに異なるソフトバンドを表現でき、そのバンドを実行時の乗数に埋もれさせず、ハード制限のすぐ隣に記録できるからです:
[thresholds]
cognitive = 25
cyclomatic = 15
nargs = 7
[thresholds.soft]
cognitive = 22 # 絶対値のソフト制限
cyclomatic = "0.9x" # ハード制限の 90% → 13.5
# nargs は未指定 → ソフト層はハード制限を継承(ソフト帯なし)
整数型のソフト制限は、浮動小数でスケールした値よりも絶対値として書くほうが明快に読めます。スカラーが生む 0.95 × 7 = 6.65 よりも、(ハードの nargs = 7 に対して)nargs = 6 を優先してください。正確な整数のソフト制限を選びにくい大きな値のメトリクス(halstead.*、loc.*)には "<ratio>x" 形式を使います。スケール係数は (0, 1] の範囲でなければなりません — ソフトティアはハードゲートの「前に」発火する早期警告バンドであり、ハードゲートより緩くなることは決してありません。
That "tighter, never looser" rule is what fixes the direction of the scaling, and it is not always a multiplication. A mi.* limit is a floor rather than a ceiling — a value below it is the violation — so RATIO divides instead: "mi.original" = 20 under --tier=soft=0.9 resolves to 22.2223, not 18. The intuition that a soft band is "90% of the limit" is right for every other metric and exactly backwards here.
両ティアは同じ .bca-baseline.toml を通じてラチェットします(別個のソフトベースラインファイルはありません)。bca check --print-effective-config --tier=soft は解決済みの制限を出力します。その [thresholds] 出力を [thresholds.soft] に貼り付ければ、一括比率のバンドから明示的なメトリクスごとの制限へ移行できます。
ゼロコンフィグ: bca.toml マニフェスト
すべてのレシピに --paths、--exclude-from、--jobs、--config、--baseline、--tier=soft=<ratio> を渡して回る代わりに、リポジトリルートに bca.toml を置き、bca check に発見させます:
# bca.toml — 作業ディレクトリ(またはその上位)で自動的に発見されます。
paths = ["."]
exclude_from = ".bcaignore"
jobs = "auto" # または整数(旧 `num_jobs`)
[check]
baseline = ".bca-baseline.toml"
[thresholds]
cognitive = 25
cyclomatic = 15
"halstead.effort" = 50000
nom = 30
nargs = 7
nexits = 5
abc = 50
wmc = 60
headroomキーはソフトティアのスケール比率です。--tier=softの下でのみ効果を持つため、引数なしのbca check(ハードティア)はheadroomキーの有無にかかわらず正確な CI ミラーのままです。メトリクスごとのソフト制限には、スカラーよりも[thresholds.soft]テーブル(後述)を優先してください。選んだバンドを実行時の乗数に委ねるのではなく、ハード制限の隣に記録できます。
このファイルを置けば、4 つのレシピはそれぞれフラグ 1 つに集約されます:
.PHONY: self-scan self-scan-headroom \
self-scan-write-baseline self-scan-write-baseline-headroom
self-scan: # hard tier (CI mirror)
bca check
self-scan-headroom: # soft tier (early warning)
bca check --tier=soft=0.95
self-scan-write-baseline: # absorb hard-tier offenders
bca check --write-baseline
self-scan-write-baseline-headroom: # absorb soft-tier offenders
bca check --tier=soft=0.95 --write-baseline
発見と優先順位
bcaは作業ディレクトリからリポジトリルート(.gitを含むディレクトリ)まで遡ってbca.tomlを探し、最初に見つかったものが使われます。マニフェスト内の相対パスはマニフェスト自身のディレクトリを基準に解決されるため、現在のディレクトリより上にあるbca.tomlでも正しいファイルを指します。- スカラーと肯定的スコープキー: CLI が勝ちます。 明示的な
--baseline、--tier、--jobsなどは対応するマニフェストキーを上書きし、「肯定的スコープ」のリストキー(paths、include)は明示的な CLI 値によって*「置き換え」*られます(マニフェストにpaths = ["src"]があってもbca check one.rsはone.rsだけをチェックします)。--config <file>はマニフェストの[thresholds]テーブルの上に「マージ」され(衝突時は config のキーが勝ちます)、繰り返し指定した--threshold name=valueフラグは絶対制限として最後に適用されます。完全な解決順序 —[thresholds]→--config→ ティア解決([thresholds.soft]またはソフトRATIOスケーリング。--tier=softの下でのみ) →--thresholdオーバーライド — は--config/--tier/ マニフェストのすべてに共通です。 - 否定的フィルタキー: CLI はマニフェストと合併します。 「除外」のリストキー(トップレベルの
exclude、[check] exclude)は置き換えではなく*「マージ」*されます。CLI の--exclude/--check-excludeはマニフェストの拒否セットに「追加」されます。こうすることで、プロジェクト設定が意図的にスキップしたディレクトリ(例:vendor/)を、コマンドラインのフィルタが黙って除外解除することは決してありません。2 つのソースにまたがる重複は畳み込まれ、CLI のパターンが先にソートされます。これは ruff/ESLint のexclude(置換)とextend-exclude(追加)の一般化です。ターゲットは置き換え、フィルタは追加。 従来どおり、--xと--x-fromは常に互いに合併します。マニフェストの除外を完全に消したい場合は--no-configを使ってください。 --no-configは発見を完全にスキップします。リポジトリレベルの設定を拾ってはならない、再現可能で完全に明示的な呼び出しのためのものです。bca initも既存のマニフェストを無視します — 設定を消費するのではなく、スキャフォールドするためです。- トップレベルの
include/excludeキーは、どのファイルを「そもそも解析するか」を決めるグローバルなファイルフィルタのグロブ(--include/--excludeフラグ)です。これらは[check] excludeテーブル(解析され報告されるがゲートされないパス。ファイルカテゴリ全体の除外を参照)とは別物です。 [check]テーブルはゲート専用のオプションを設定します。excludeはグロブのリストで、一致したファイルは解析・報告されますが、しきい値ゲート(および--write-baseline)からは除外されます。exclude_fromは同じグロブを並べた.gitignore形式のファイルを指します(どちらも--check-exclude/--check-exclude-fromフラグに対応します)。exit_codes = "tiered"はより細かい終了コードにオプトインします(--exit-codes=tieredに対応。終了コードを参照)。"default"(暗黙の値)は安定した0/1/2の契約を維持します。ベースラインとヘッドルームのキーもゲート専用なのでここに置かれます:baseline(bca checkが読み、引数なしの--write-baselineが書き込むファイル)、baseline_line_tolerance、baseline_fuzzy_match、そしてheadroom(ソフトティアのスケール比率。--tier=soft=<R>に対応)。いずれのキーも、どちらの方向でも CLI の値がテーブルの値を上書きします。- これら 4 つのキーは以前トップレベルにありました。その綴りは非推奨(#599)で、一度だけ警告が出ます。1 リリースサイクルの間は尊重され、その後の次のメジャーバージョンで削除されます。
baseline、baseline_line_tolerance、baseline_fuzzy_match、headroomを[check]の下へ移してください。キーが両方に設定されている場合は[check]の値が勝ちます。 [vcs]テーブルはbca vcsの変更履歴ランキングのオプションを設定します。そのfile_typesキー(デフォルトの"metrics"/"all"/"rs,py"形式の拡張子リスト)は、どのファイルをランク付けするかを絞り込みます。肯定的スコープキーなので、明示的な--file-typesCLI フラグはそれを*「置き換え」*ます(bca vcsのファイルタイプスコープを参照)。cyclomatic_count_tryとexclude_testsは、--cyclomatic-count-try/--exclude-testsフラグに対応するウォーカー調整用のブール値です。exclude_tests = trueは、メトリクス計算の前に Rust のインラインテストのサブツリー(#[test]、#[cfg(test)]など)を刈り取ります。どちらも Rust 専用で、他の文法では効果がありません。--exclude-testsは存在のみのフラグ(=false形式なし)なので、マニフェストキーは刈り取りをオンにすることしかできません。CLI の--exclude-testsは勝ちますが、CLI が設定していないキーをマニフェストがオフにすることはできません。[thresholds.soft]テーブルは、メトリクスごとのソフトティア制限を設定します(--tier=softによって消費されます。2 ティアのしきい値を参照)。認識されないキーは 1 行の警告とともに無視されるため、古いbcaビルドを壊すことなく、今後のスキーマ追加を先行採用できます。bca check --print-effective-configは、manifestの来歴行を含む解決済みのビューを出力するので、マージが何を生成したかを正確に確認できます。
以下の明示フラグのスケルトンは引き続き完全にサポートされます — マニフェストは同じフラグの糖衣であって、置き換えではありません。リポジトリルートにファイルを置けない場合や、ある CI ジョブがコミット済みマニフェストと異なるレイアウトを必要とする場合(フラグを
--no-configと組み合わせます)に使ってください。
スケルトン: GNU Make(明示フラグ)
以下の 4 つのレシピは、すべてのフラグを明示的に渡す自己完結のドロップインで、上記のマニフェストレシピのロングフォームです。BCA 変数は、チェッカーを提供する呼び出し(ピン留めしたリリースバイナリ、cargo run --release、npm / pip のラッパー)を指すように調整してください。PATHS と EXCLUDE_FROM はあなたのレイアウトに合わせて調整します。
# --- bca ローカルしきい値ゲート ------------------------------------------
# ハード(HARD)ティアは CI を正確にミラーします。両ティアは同じ
# thresholds.toml + .bca-baseline.toml を消費し、ソフトティアはすべての
# しきい値を $(BCA_HEADROOM)(デフォルト 0.95)でスケールします。
#
# Knobs are namespaced with `BCA_` so they don't collide with anything
# else in your environment. The big-code-analysis repo itself uses the
# manifest form above (a single `bca.toml`) rather than these explicit
# flags; reach for this skeleton when you can't drop a manifest at the
# repo root and must point `--config` at a standalone threshold file.
BCA := bca
BCA_PATHS := .
BCA_EXCLUDE_FROM := .bcaignore
BCA_THRESHOLDS := thresholds.toml
BCA_BASELINE := .bca-baseline.toml
BCA_HEADROOM ?= 0.95
# 共通引数。4 つのレシピが揃った状態を保てるよう括り出しています。
# `--jobs` のデフォルトは OS が報告する実効 CPU 数
# (Linux では cgroup / cpuset を考慮)なので、`$(nproc)` の引き回しは
# needed. Override with `--jobs N` (or `--jobs 1` to force
# serial mode for debugging).
BCA_BASE_ARGS := --paths $(BCA_PATHS) --exclude-from $(BCA_EXCLUDE_FROM)
.PHONY: self-scan self-scan-headroom \
self-scan-write-baseline self-scan-write-baseline-headroom
self-scan:
@echo "bca self-scan (hard gate)..."
@$(BCA) check $(BCA_BASE_ARGS) \
--config $(BCA_THRESHOLDS) \
--baseline $(BCA_BASELINE)
# `self-scan-headroom: self-scan` is intentional: under `make -j` Make
# would otherwise run both gates in parallel and the soft tier's scaled
# error message could land before the true regression on the hard tier.
# `--tier=soft=$(BCA_HEADROOM)` scales every config limit before the
# offender comparison — no helper script, no second TOML file.
self-scan-headroom: self-scan
@echo "bca self-scan (soft gate, BCA_HEADROOM=$(BCA_HEADROOM))..."
@$(BCA) check $(BCA_BASE_ARGS) \
--config $(BCA_THRESHOLDS) \
--tier=soft=$(BCA_HEADROOM) \
--baseline $(BCA_BASELINE)
self-scan-write-baseline:
@echo "Refreshing $(BCA_BASELINE) at hard thresholds..."
@$(BCA) check $(BCA_BASE_ARGS) \
--config $(BCA_THRESHOLDS) \
--write-baseline $(BCA_BASELINE)
# Soft-tier baseline write. NOTE: this and `self-scan-write-baseline`
# どちらも `$(BCA_BASELINE)` に書き込みます。これらを並列に
# prerequisites of one umbrella target or invoke them with `make -j2`,
# or the two `bca` processes will race on the same file and the
# losing tier's offenders will silently vanish from the baseline.
# Run them sequentially (hard first, then soft) and commit the diff.
self-scan-write-baseline-headroom:
@echo "Refreshing $(BCA_BASELINE) at soft thresholds (BCA_HEADROOM=$(BCA_HEADROOM))..."
@$(BCA) check $(BCA_BASE_ARGS) \
--config $(BCA_THRESHOLDS) \
--tier=soft=$(BCA_HEADROOM) \
--write-baseline $(BCA_BASELINE)
bca check --tier=soft=<ratio> は、違反判定の前に --config のすべての限度値を指定した比率(引数なしの --tier=soft ではデフォルトの 0.95)でスケーリングし、その後、ハードティアが書き込むものと同じ .bca-baseline.toml に対してフィルタリングします。明示的な --threshold name=value による上書きは絶対値で、再スケーリングされません。維持が必要な補助スクリプトや 2 つ目の TOML ファイルはありません — ソフトティアは、ハードティアの呼び出しにフラグを 1 つ追加しただけのものです。
終了コード
ゲートの終了コードは bca check からそのまま伝播します。0 はクリーン、2 はしきい値違反(ハード・ソフトを問わず)、1 はツールエラーです。ソフトティアは本物のゲートです — 助言的なものだと考えて make self-scan-headroom を || true でラップしてはいけません。非ゼロの終了コードこそが、この接近警告バンドの眼目です。
Pass --exit-codes=tiered (or set [check] exit_codes = "tiered") to split the single violation code 2 by severity: 2 new offenders only, 3 regressions only, 4 both, 5 a --tier=soft violation that also breaches the hard limit. The tiered codes are opt-in; the default stays 0/1/2, and every fail-state remains non-zero. Use them when CI needs to route "a new offender appeared" differently from "a baselined offender got worse" without parsing the [new] / [regr +N%] row tags.
pre-commit と CI への組み込み
開発者がプッシュ前にすでに実行している統括ターゲットに、ソフトゲートを追加してください。ハードゲートはその前提条件として実行される(上記の self-scan-headroom: self-scan のエッジを参照)ため、ソフトターゲットだけを列挙すれば十分です — そして重要なことに、これは make -j にも耐えます。前提条件のエッジがなければ、両方のリーフが並列にスケジュールされ、出力が交互に混ざってしまうところです:
.PHONY: pre-commit
pre-commit: fmt-check clippy test self-scan-headroom
順序が重要です。ハードティアは、スケーリング後ではなく 100% の限度に対する真のリグレッションを指摘します。前提条件のエッジは、並列 Make の下でもその順序を強制します。
CI では、ハードティアのみを実行します:
- name: Threshold gate
run: make self-scan
ソフトティアは開発者向けのフィードバック用ノブであり、リリースゲートではありません。CI で実行すると、(何も接近していなければ)ハードティアと重複するか、100% を超えないままじわじわ悪化しているベースライン吸収済みの違反に対して騒がしく発火するかのどちらかで、いずれも CI がすでにカバーしている以上のものは得られません。
ヘッドルームノブ
BCA_HEADROOM は (0, 1] の範囲の単一のスカラー値です。意味のあるバンドは狭い範囲に限られます:
BCA_HEADROOM | 関数が次に達すると発火… | ユースケース |
|---|---|---|
0.99 | いずれかの限度の 99% | 可能な限り厳しい警告。ハードゲートが発火する直前の最後のコミットで発火します。 |
0.95 | いずれかの限度の 95%(デフォルト) | 1〜2 コミット分のリードタイム。良いデフォルトです。 |
0.90 | いずれかの限度の 90% | より広いバンド。限度を引き上げた直後、新しい上限が落ち着くまでの間に有用です。 |
1.00 | 100%(ハードゲートと同等) | 2 つのティアが一致していることを確認するサニティチェックです。 |
およそ 0.80 を下回る値では、ソフトティアは恣意的な数値による第 2 のハードティアと化し、有用でなくなります。現実のコードベースでは、どのしきい値にもその 80% 付近にある関数が 何かしら 存在するため、ソフトティアは早期警告シグナルではなく、恒常的なベースライン管理の雑務になってしまいます。
ソフトティアが発火したとき
ソフトゲートの失敗はバグ報告ではなく、判断のタイミングです。正当な解決策は、ちょうど次の 3 つです:
- リファクタリングする。 他の複雑度リグレッションと同じワークフローです — ヘルパーを抽出する、ディスパッチアームを畳み込む、関数を分割する。これが最も一般的なケースであり、ソフトティアは同じブランチ上でそれを行う時間を確保するために存在します。
- 限度を引き上げる。
[thresholds]テーブル(このリポジトリではbca.toml、それ以外では各自のしきい値ファイル)を編集し、何が変わったのか(新しい言語モジュール、真にアルゴリズム上の下限、再分類されたマクロなど)を説明する why コメントを残してください。make self-scan-headroomを再実行し、新しい値が余裕をもって違反箇所をカバーしていることを確認します。 - ベースラインに吸収する。 その値が今後もずっと正当である場合 — カバーする文法に見合った幅を持つパーサーのディスパッチアーム、安定した状態機械、生成コードなど — は、
make self-scan-write-baseline(ハードティア)またはmake self-scan-write-baseline-headroom(ソフトティア)を実行してください。.bca-baseline.tomlの差分は、それを生んだコードと同じプルリクエストでコミットします。
ゲートを黙らせるためだけに「限度を引き上げる」を無言で選んではいけません。コミットされた why コメントは、次の読者にとって唯一の監査証跡です。それがなければ、引き上げられた限度は怠慢と見分けがつきません。
スケルトン:justfile
just を好むプロジェクトの場合:
# bca のローカルしきい値ゲート。ハードティアは CI を反映します。ソフト
# ティア(ヘッドルーム)はローカル限定の早期警告です。
bca := "bca"
paths := "."
exclude := ".bcaignore"
thresholds := "thresholds.toml"
baseline := ".bca-baseline.toml"
headroom := env_var_or_default("BCA_HEADROOM", "0.95")
# `--jobs` のデフォルトは実効 CPU 数なので、このスケルトンでは
# `$(nproc)` を `just` に通しません。必要ならインラインで上書き
# します: `just self-scan --jobs 1`。
base_args := "--paths " + paths + " --exclude-from " + exclude
self-scan:
{{bca}} check {{base_args}} \
--config {{thresholds}} --baseline {{baseline}}
self-scan-headroom: self-scan
{{bca}} check {{base_args}} \
--config {{thresholds}} --tier=soft={{headroom}} --baseline {{baseline}}
self-scan-write-baseline:
{{bca}} check {{base_args}} \
--config {{thresholds}} --write-baseline {{baseline}}
# Make のスケルトンと同様、これを `self-scan-write-baseline` と並列に
# 組み合わせないでください — 同じ {{baseline}} ファイルを奪い合います。
self-scan-write-baseline-headroom:
{{bca}} check {{base_args}} \
--config {{thresholds}} --tier=soft={{headroom}} --write-baseline {{baseline}}
スケルトン:package.json スクリプト
npx またはピン留めしたバイナリで bca を取り込む JavaScript プロジェクト向けです。--jobs のデフォルトは実効 CPU 数(Linux では cgroup / cpuset を考慮)なので、npm 側でも Make / just とバイト単位で同一の bca check 呼び出しを生成するために BCA_NUM_JOBS 環境変数はもう必要ありません。--jobs 1 を明示的に渡すのはデバッグ時だけにしてください:
{
"scripts": {
"self-scan": "bca check --paths . --exclude-from .bcaignore --config thresholds.toml --baseline .bca-baseline.toml",
"self-scan-headroom": "bca check --paths . --exclude-from .bcaignore --config thresholds.toml --tier=soft=0.95 --baseline .bca-baseline.toml",
"self-scan-write-baseline": "bca check --paths . --exclude-from .bcaignore --config thresholds.toml --write-baseline .bca-baseline.toml",
"self-scan-write-baseline-headroom": "bca check --paths . --exclude-from .bcaignore --config thresholds.toml --tier=soft=0.95 --write-baseline .bca-baseline.toml"
}
}
ソフトティアはいまや素の bca check 呼び出しなので、npm スクリプトはどのシェルでもバイト単位で同一です — 補助スクリプトも、取り繕うべき python3-vs-py エイリアスも、環境変数とシェル展開の移植性の罠もありません。バンドを広げるには、スクリプト内のリテラル 0.95 を編集する(またはお好みのタスクランナー経由で配線する)だけです。このフラグはどのプラットフォームでも同じようにパースされます。
husky や pre-commit と組み合わせて、同じスクリプトが git commit 時に実行されるようにしてください。
スケルトン:pre-commit フック
pre-commit フレームワーク(バージョン 3.2.0 以降 — 下記のバージョン注記を参照)を使っている場合、両ティアとも make を呼び出すローカルフックになります:
- repo: local
hooks:
- id: bca-self-scan
name: bca self-scan (hard gate)
entry: make self-scan
language: system
pass_filenames: false
stages: [pre-commit]
- id: bca-self-scan-headroom
name: bca self-scan-headroom (soft gate)
entry: make self-scan-headroom
language: system
pass_filenames: false
stages: [pre-commit]
pass_filenames: false は意図的です — bca は --paths とベースラインから自分で入力を発見します。pre-commit に変更ファイルを渡させると、スキャンがそれらのファイルだけに縮小され、ベースライン更新のファイル横断的な影響を見逃してしまいます。
pre-commitの最低バージョンは 3.2.0 です。stages:の語彙は pre-commit 3.2.0(2024 年 3 月)で改名されました —commit→pre-commit、push→pre-pushなど。古いインストール(特に RHEL 8 EPEL、Ubuntu 20.04 のデフォルトパッケージ、レガシー語彙にピン留めされた.pre-commit-config.yaml)はstages: [pre-commit]を未知のステージ名として拒否し、フックが登録されません。古いインストールをサポートする必要がある場合はstages: [commit]に置き換えてください。混在環境では、この矛盾が黙って表面化しないよう、開発ツールのドキュメントでフレームワークをpre-commit --version≥ 3.2.0 にピン留めしてください。
より広いベースラインワークフローとの組み合わせ
上記の 4 つの self-scan* ターゲットは、ドキュメント化されたベースラインのレシピの代替ではありません — それらはそのレシピ 「そのもの」 を、開発者マシンのコマンドとして機械化したものです。同じ順序がそのまま適用されます:
- 最初に一度ブートストラップする。 初期のしきい値を書き、初期のベースラインを書き、両方をコミットします。
- コミットごとにゲートする。 ハードティアはリグレッションで失敗し、ソフトティアは限度への接近で失敗します。
- 集中的なリファクタリング中に更新する。 関数の値が正当に動いた(誰かが 実際に 負債を返済した)場合は、ベースラインを再生成して差分をレビューします。
- Retire when empty. When
.bca-baseline.tomlshrinks to justversion = 6(the bare schema stamp with no offender entries), drop the--baselineflag and delete the file. The thresholds now stand on their own.
ローカルのティアは、ステップ 2 と 3 のフィードバックループを「プルリクエストで CI が赤くなる」から「git commit が返る前に Make レシピが赤くなる」へと短縮します。それがこの仕組みの売りのすべてです。
関連する業界パターン
ハード / ソフトのティア分割は、より広いパターンの一例です。以下のいずれかを使ったことがあれば、そのメンタルモデルがそのまま通用します:
- Sonar の新規コードに焦点を当てた品質ゲート。 既存コードは現状のまま維持され、変更 が事態を悪化させてはならない、という考え方です。ベースラインファイルは、「新規コード」/「リーク期間」という発想の
bcaネイティブな形です。 - clippy の
warn-vs-denyリントレベル。warnリントはローカルビルドで表面化し、同じリントを-D warningsで deny すると CI が失敗します。2 段構えの設定は、実験的により厳しいルールを導入する場所を与えてくれます。 - 一般的な移行ツーリングにおける ラチェットパターン。今日のカウントを記録し、増加で失敗させ、カウントが減るにつれ上限を下げていきます。
bca checkはパターン単位ではなく関数単位でラチェットしますが、単調性の保証は同じです。 - C/C++ における
-Wall+-Werror。 まず-Wallでノイズを洗い出し、ベースラインがゼロに達してから-Werrorに昇格させるのは、空になった.bca-baseline.tomlを削除するのと同じ退役ステップです。
Choosing thresholds
bca check compares each metric against a limit you configure. This page explains where the shipped defaults come from, how to adjust them for the language you are gating, and how to pick a different set for a different job. It is for anyone editing a [thresholds] table, whether by hand or through a coding agent.
If you have not set up a gate yet, start with Local threshold gates for the mechanics and Baselines for absorbing the offenders you already have. This page is about the numbers.
The shipped defaults
bca init scaffolds this table. It is defined once in big-code-analysis-cli/src/default_thresholds.rs.
| メトリクス | 上限 | スコープ | Anchor |
|---|---|---|---|
cognitive | 15 | function | SonarSource default |
cyclomatic | 15 | function | lizard default; MISRA and NASA safety-critical ceiling |
abc | 40 | function | between RuboCop AbcSize 17 and Flog's "60 and above is dangerous" |
nargs | 5 | function | RuboCop ParameterLists 5; Code Climate is stricter at 4 |
nexits | 5 | function | Code Climate return-statements 4 |
halstead.effort | 50000 | function | none published; percentile-derived |
loc.ploc | 600 | file | none published; percentile-derived |
loc.sloc | 1200 | file | bloat backstop, not the working limit |
nom | 30 | container | Code Climate method-count 20; PMD TooManyMethods 10 |
wmc | 60 | container | none published; percentile-derived |
Scope matters when you read these. A cognitive limit applies to each function; nom and wmc apply to each class, struct, trait, impl, namespace, or interface; loc.* applies to the whole-file root. bca check will not compare a container's method count against a per-function limit. See Check for the full scope rules.
Two of these limits deserve their reasoning spelled out.
loc.ploc and loc.sloc are a pair. PLOC counts physical lines of code with blanks and comments excluded, so it is the working file-size limit: growing a file by documenting a decision costs nothing against it. SLOC counts everything, so it sits far looser and does one job only, which is stopping a file from growing without bound on comment volume while still clearing loc.ploc. Gating file size on SLOC alone charges the same price for a paragraph of rationale as for a new branch, and the observable result is that people delete the rationale.
cyclomatic at 15 rather than McCabe's 10 is a deliberate loosening. NIST 500-235, the document that made 10 the canonical number, allows raising it for teams with the process to justify it, and observes that limits as high as 15 have been used successfully. Measured against real code, 10 flags roughly twice as many functions as 15 without a corresponding change in what a reviewer would call a problem.
How the defaults were derived
Published thresholds disagree with each other by wide margins. For cyclomatic complexity alone the defaults in common tools span 7 (RuboCop) to 30 (gocyclo), with Checkstyle and PMD at 10, lizard and MISRA at 15, and ESLint at 20. Picking one by authority means picking an authority.
So each limit here is checked against measurement as well. The reference corpus is 43 real-world repositories across 20 languages, cloned at their default branch on 2026-07-31, with test trees, vendored code, and generated files excluded. Measured with bca metrics, that is roughly a quarter of a million function spaces, forty thousand container spaces, and twenty-seven thousand files. Values are binned by the same File, Function, and Container scope bca check applies, so a container's nom never lands in a per-function distribution.
The design rule is that a default should flag roughly the worst 1% to 3% of spaces in the median language. Below that a limit is inert and gives false comfort. Above it the gate stops being a gate and becomes a style rule that people route around. This is a stricter reading of the benchmark approach in Alves, Ypma, and Visser, who derive risk bands at the 70th, 80th, and 90th percentiles. Their 90th percentile is a useful "worth looking at" line; it is far too noisy to fail a build on.
Two of the previous defaults failed that rule and changed as a result. nargs at 7 fired on under 1% of functions in the median language and on nothing at all in this project's own source, which makes it a limit that cannot catch anything. cognitive at 25 was inherited from clippy, which is deliberately conservative because it lints rather than gates; 15 is what the metric's designers chose.
Per-language overrides
Metric distributions vary by language more than by project. The 90th-percentile per-function cognitive value in the reference corpus runs from 0 in C# to 21 in Tcl. Median file loc.ploc runs from 16 in TypeScript to 283 in C, an eighteen-fold spread. A single table cannot fit both ends.
The table below gives the measured 97.5th percentile per language, which is the value a limit would need to flag the worst 2.5% of that language's code. Read it as calibration data, not as a prescription: a low number means the language's code is mostly simple, which is a reason you can tighten, not evidence that you should.
| 言語 | cognitive | cyclomatic | abc | halstead.effort | loc.ploc |
|---|---|---|---|---|---|
| Bash | 35 | 25 | 60 | 95000 | (thin sample) |
| C | 50 | 30 | 60 | 200000 | 3500 |
| C++ | 14 | 10 | 25 | 50000 | (thin sample) |
| C# | 4 | 4 | 13 | 9500 | 700 |
| Elixir | 5 | 7 | 20 | 15000 | 1500 |
| Go | 25 | 17 | 45 | 120000 | 1500 |
| Groovy | 16 | 13 | 25 | 30000 | 450 |
| Java | 7 | 5 | 16 | 15000 | 800 |
| JavaScript | 20 | 15 | 35 | 55000 | (thin sample) |
| Kotlin | 10 | 8 | 20 | 20000 | 300 |
| Lua | 35 | 20 | 40 | 85000 | 800 |
| Objective-C | 18 | 12 | 35 | 60000 | (thin sample) |
| Perl | 35 | 25 | 45 | 140000 | 1500 |
| PHP | 10 | 9 | 25 | 50000 | 900 |
| Python | 20 | 13 | 25 | 25000 | 700 |
| Ruby | 7 | 6 | 19 | 10000 | 400 |
| Rust | 8 | 8 | 20 | 35000 | 900 |
| Tcl | 45 | 25 | 85 | 90000 | (thin sample) |
| TypeScript | 20 | 13 | 25 | 55000 | 450 |
| TSX | 14 | 10 | 65 | 95000 | 400 |
"Thin sample" marks a language whose file count in the corpus is too small to derive a file-size figure from. The per-function figures for Bash, Tcl, JavaScript, and Objective-C rest on the smallest samples in the table and are the ones most worth re-deriving against your own code.
What to change, and why
Most languages need no override. The cases that do fall into three groups.
Procedural languages with large dispatch functions. C, Tcl, Bash, Lua, Perl, and Go all run two to three times the defaults. These languages lack exceptions or discourage them, so error handling is inline branching, and they favour long switch-style dispatch over polymorphism. The default cognitive limit flags 5% to 15% of their functions, against 3% in the median language. Raise cognitive and halstead.effort first; those two carry most of the excess.
In a single-language repository this is the whole [thresholds] table; in a mixed one it belongs under the language it describes, or it drags every other language up with it.
# Gating C. The same shape suits Tcl, Bash, Lua, Perl, and Go.
[thresholds.lang.c]
cognitive = 30
cyclomatic = 20
abc = 50
"halstead.effort" = 120000
"loc.ploc" = 1200
"loc.sloc" = 2000
Languages whose module construct is not a class. nom and wmc are Container-scoped, and a container is whatever the grammar calls a class, struct, trait, impl, namespace, or interface. An Elixir defmodule holds dozens of functions by design, so roughly a third of Elixir modules breach nom = 30. That is a scope mismatch, not a code smell. Rust sits at the other end: an impl block is usually a handful of methods, so the default never fires.
# Elixir modules and Rust impls sit at opposite ends of the same metric.
[thresholds.lang.elixir]
nom = 150
wmc = 300
[thresholds.lang.rust]
nom = 20
wmc = 40
Compact, heavily-abstracted languages. Java, Ruby, Kotlin, C#, and Elixir sit far under the defaults on every per-function metric. Their 97.5th percentile cyclomatic is 4 to 8. Gating them at 15 means the gate will effectively never fire, so tighten if you want it to do work.
Read the C# row with care. Its distribution is dominated by property accessors, which bca counts as functions and which are almost always trivial, so its percentiles sit lower than the code a reviewer would actually be looking at. The same effect applies more weakly to Java, Kotlin, and Ruby. Prefer re-deriving from your own repository over adopting any of those four rows directly; see Re-deriving for your own codebase.
Metrics that do not apply to every language
nargs reports 0 for every Bash function, and that is correct and permanent: the shell has no formal parameter list, arguments arrive as $1, $2, and so on, and the limit is simply inert. A nargs limit therefore passes unconditionally on a Bash codebase, which reads as "no offenders" rather than "not measured". Bash is the only language left in that position.
Perl is counted, but only where the source declares a signature (sub add($x, $y), stable since 5.20 and on by default under use v5.36). A sub without a signature takes its arguments from @_ and has no formal parameter list to measure, so it reads 0 — correctly. Perl's nargs distribution is therefore sparse on codebases predating use v5.36, and one anonymous-sub form still reads 0 because the grammar misparses its signature.
wmc, npm, and npa are only produced for languages with a class-like container. They are absent from Bash, C, Go, Lua, Perl, and Tcl output.
A per-language override cannot fix any of these. Bash nargs is 0 for every function, so no limit — not even 0, which fires only on a value above it — can make the gate say anything. The override mechanism tunes limits; it does not add a measurement the grammar cannot supply.
Applying overrides
Write them into bca.toml as [thresholds.lang.<slug>] tables. Each one layers over the project's [thresholds] per metric, so a language inherits every limit it does not restate:
[thresholds]
cognitive = 15
cyclomatic = 15
nargs = 5
nom = 30
wmc = 60
# Procedural, dispatch-heavy: raise the two metrics carrying the excess.
[thresholds.lang.c]
cognitive = 30
cyclomatic = 20
"halstead.effort" = 120000
"loc.ploc" = 1200
# A defmodule is not a class; nom and wmc need module-sized limits.
[thresholds.lang.elixir]
nom = 150
wmc = 300
# Compact and heavily abstracted: tighten, or the gate never fires.
[thresholds.lang.csharp]
cognitive = 8
cyclomatic = 8
The key is the canonical language slug, the same vocabulary --language accepts (cpp, csharp, objc, tsx, mozcpp, mozjs); an unknown one is a tool error rather than a silent no-op. A language with no table of its own is gated by [thresholds]. Under --tier=soft each language's soft band is derived from its own resolved limit, so a language that raises a limit gets a soft band scaled from the raised number rather than the project-wide one. The full reference — including how --print-effective-config renders the resolved tables — is in bca check.
Two things to watch when splitting a table this way. .h is analyzed with the C++ grammar, not the C one, so a C project's headers are gated by [thresholds.lang.cpp]; see Supported Languages for the extension map. And --threshold on the command line stays global — it overrides every per-language table too, which is what you want for a one-off run and a surprise if you expected it to compose.
The alternative is still available and still worse: keep one table sized for the loosest language and let per-file baselines carry the rest. It is simpler to maintain and strictly weaker, because the stricter languages stop being gated at all.
Per-use-case profiles
The same codebase wants different limits depending on what the gate is for.
Blocking CI gate
This is what the shipped defaults are for. A limit that fails a pull request has to be one the team agrees is a real problem, because the cost of a false positive is a blocked merge and an argument. Pair it with a baseline so the gate fails only on new offenders and regressions, and tighten from there.
Use the shipped defaults unchanged, plus:
[check]
baseline = ".bca-baseline.toml"
Agent feedback loop
When the consumer is a coding agent rather than a human reviewer, a false positive costs one wasted refactor rather than a blocked merge, and the signal arrives while the code is still being written. Tighten toward the published per-function values, and drop the file-size and container limits, which an agent editing one function cannot act on.
[thresholds]
cognitive = 10
cyclomatic = 10
abc = 25
nargs = 4
nexits = 4
"halstead.effort" = 25000
See Feeding metrics to an agent for wiring this into an edit loop, and Suppression markers for telling the agent when complexity is essential rather than accidental.
Legacy audit and triage
When the goal is to rank an unfamiliar codebase rather than to gate it, you want the handful of functions that are genuinely worst, not a list of thousands. Set limits at roughly twice the defaults so only extreme outliers surface, run without a baseline, and use bca report markdown rather than bca check.
[thresholds]
cognitive = 40
cyclomatic = 30
abc = 80
"halstead.effort" = 250000
"loc.ploc" = 1500
nom = 60
wmc = 120
Quality reports covers the report output. Adding --vcs ranks by change history as well, which is usually a better triage order than complexity alone: a complex function nobody has touched in three years is not where the bugs are.
Safety-critical and regulated
Standards in this space specify limits directly, and the standard wins over any measurement. MISRA and NASA both cap cyclomatic complexity at 15; the JSF C++ standard allows 20 with a documented exception for large switch statements. McCabe's original 10 applies where the testing budget supports it, since the number is a bound on the basis-path test count, not a style opinion.
[thresholds]
cognitive = 15
cyclomatic = 10
abc = 30
nargs = 5
nexits = 1
"halstead.effort" = 25000
nexits = 1 encodes the single-exit rule from MISRA C:2023 Rule 15.5. It is contentious outside regulated work, and in most codebases early returns make code simpler rather than harder to follow, so treat it as a compliance setting rather than a general recommendation.
Metrics not gated by default
These are computed and visible in bca report markdown|html. They are left out of the default table on purpose.
halstead.volume has the most widely-cited threshold of any Halstead measure, the guideline that a function's volume should stay under 1000. Measured against the corpus it flags about 7% of functions in the median language and 20% in the worst, which makes it useful for ranking and unusable as a gate.
mi.original, mi.sei, and mi.visual_studio are the Maintainability Index family, and they are lower-is-worse: the violation is a value below the limit. Visual Studio's bands (below 10 poor, 10 to 19 moderate, 20 and above good) apply to its own rescaled 0 to 100 output, which bca reports as mi.visual_studio. mi.original is unbounded above and reaches 167 on the corpus, so the original SEI bands of 65 and 85 do not transfer. The index is also a function of the metrics you are already gating, so gating it too double-counts. See Supported Code Metrics for the formulas.
npm, npa, tokens, and cyclomatic.modified are omitted because each duplicates something already in the table, or because their distributions are dominated by a language idiom rather than by design quality.
Tightening a limit onto a cluster
Converging a limit onto a cluster of existing values is never free while a proportional soft tier is active. Measure a candidate at both tiers before calling it free — the hard-tier reading is the one you naturally take, and it is the one that misleads.
The soft tier measures distance to the limit. A limit chosen to sit exactly on a population's value therefore maximises soft-tier breach by construction: every function at that value is inside the band the moment the limit lands, and no amount of tidying moves it out, because it is the limit.
This project walked into it. nargs was at 7, and tightening it to 6 looked free — every offender at the tighter limit was already in the baseline, so bca check --threshold nargs=6 reported nothing:
nargs limit | hard offenders | soft limit (0.95) | soft offenders |
|---|---|---|---|
| 7 (kept) | 27 | 6.65 | 60 |
| 6 (proposed) | 60 | 5.7 | 134 |
Seventy-four functions sit at exactly 6. A limit of 6 puts all of them 0.3 below the soft band at once — 74 baseline entries bought for no hard-tier gain, none of which can ever be retired short of rewriting the signatures. The limit stayed at 7; the honest alternative was the real 6 → 5 work. (#1143, #1169.)
bca check --explain-threshold <metric>=<limit> measures both tiers in one walk and names the cluster when it finds one, without touching bca.toml:
$ bca check --explain-threshold nargs=6
nargs: candidate limit 6
hard tier (limit 6): 60 offenders, 60 already baselined, 0 new
soft tier (limit 5.7, 0.95x): 135 offenders, 61 already baselined, 74 new
cluster: 75 of 75 soft-band offenders sit at exactly 6 — the candidate limit itself. …
The trap is not confined to the global table. A per-language override is another way to converge a limit onto a population, and it has the same cliff — --explain-threshold reports a language keeping its own limit on a separate line rather than folding it into the count.
The rule generalises past the soft tier: a limit that lands on a cluster leaves the population with no room, so the next tightening is the one that has to move real code. Prefer a limit in the gap between clusters. See Preview a candidate limit for the flag's full contract.
Re-deriving for your own codebase
Corpus percentiles are a starting point. Your own distribution is better evidence, and producing it takes one command plus a short script.
bca metrics --no-config -O json -I '*.rs' -X '**/tests/**' . > metrics.jsonl
Each line is one file's FuncSpace tree. Walk it, keep each space's own value for the metric you care about, filter by kind to match the metric's scope ("function", "unit" for loc.*, or a container kind for nom and wmc), and take the 97.5th percentile. Setting a limit there flags your worst 2.5%.
Two checks are worth running afterwards. Count the offenders the limit produces: if it is more than a few percent of your spaces, the gate will be ignored rather than obeyed. Then look at where the values pile up. A natural distribution tails off smoothly, so a cluster of files sitting just under the limit means the limit is shaping the code rather than measuring it, and people are trimming to fit. That happened to this project's own loc.sloc gate, which is documented in issue #1138.
bca check --write-baseline records today's offenders so you can adopt a stricter limit without failing on day one. Baselines covers the bootstrap, refresh, and retire flow.
エージェント型コーディングツールへのメトリクスのフィード
Claude Code や opencode のようなエージェント型コーディングツールは、保守性フィードバックの急成長中の消費者であり、エディタの中の人間とは異なる形でそのフィードバックを求めます。人間はキー入力をするので、エディタのループは言語サーバーに頼ります。didChange のたびにパースし、複雑度をマージンに描画し、1 文字ごとに更新する、という具合です。エージェントはキー入力をしません。ツール呼び出しで編集全体を書き込み、ターンを明け渡します。その消費者にとって正しいフィードバックは、バッチで、編集後に、構造化されて届くもの — まさに bca check がすでに出力しているものです。
そのため、このレシピは新しいバイナリの表面を一切追加しません。bca check は、エージェントが必要とするすべてをすでに提供しています:
- A machine-parseable offender list (per-violation rows on stdout, or
--report-format sarif | code-climate | clang-warning | msvc-warning | checkstylefor a structured document). - 段階化された終了コード — 違反があれば
2、クリーンなら0、ツールエラーなら1— により、フックは何もパースせずに「この編集はコードを複雑にしすぎたか?」で分岐できます。 - Baseline filtering, in-source suppression markers, and
[check.exclude]globs, so the signal an agent sees is the same ratcheted signal a human sees — provided the project's exclusions live under[check]rather than in a walker deny-set.
欠けていたのは配線です。このページがその配線です。ツールごとにコピー&ペーストできるフィードバックループと、ループが裏目に出ないようにするためのエージェント向けガイダンスを提供します。
キーストローク時ではなく編集後に
このレシピは、提案中の bca lsp サーバー (#384) と意図的に対をなすものです。両者は異なる消費者に仕え、互いに依存しません:
bca lsp (#384) | 本レシピ | |
|---|---|---|
| 消費者 | エディタ内の人間 | ツールループ内のエージェント |
| トリガー | キーストローク(didChange) | ツール呼び出しの完了(編集の反映) |
| 再パース | インクリメンタル | 編集ごとにファイル全体を 1 回 |
| 出力先 | マージンの診断表示 | モデルへフィードバックされるテキスト |
| ステータス | 提案段階 | bca check で今日から動作 |
人が入力しているときは LSP を、エージェントが編集しているときはこちらを選んでください。エージェントを LSP のインクリメンタルな didChange 経路につなぐのは、エージェントが決して使わない機構の代価を払うことになります。
以下のツール別セクションがすべて呼び出すコマンドは、手で実行するのと同じものです:
# 終了コード 2 ⇒ このファイルに違反が少なくとも 1 件あります。しきい値は
# リポジトリルートの bca.toml(自動的に発見されます)から来ます。その場限りの
# 上書きは 1 つ以上の --threshold フラグで行います。
bca check path/to/edited_file.rs --threshold cognitive=10
An agent loop wants tighter limits than a blocking CI gate: a false positive costs one wasted refactor rather than a blocked merge, and the signal arrives while the code is still being written. See the agent feedback profile for the rest of the table, and Choosing thresholds for where the shipped defaults come from.
リポジトリルートに bca.toml があれば(ローカルしきい値ゲート を参照)、--threshold フラグは不要です。素の bca check <file> がコミット済みの限度値・ベースライン・除外設定を読み込むため、エージェントのループは CI とまったく同じ条件でゲートします。
Put the hook's exclusions under [check]
One thing does not carry over from the CI invocation, and it will produce false positives if you skip it. A hook names one file per run, and an explicitly named path overrides every walker exclude — the rg convention that a path you named is a direct request. So a file your project keeps out of scope with -X, --exclude-from, a .bcaignore, or a manifest exclude list is nonetheless analyzed the moment the hook passes it by name, and reported as an offender under an exit 2 that frames it as a problem to address.
Walker excludes shape what gets analyzed. Check excludes shape what gets gated. A per-file hook invocation only respects the second:
# bca.toml — survives an explicit path, so the hook sees what CI sees.
[check]
exclude = ["./utils/**", "./benches/**"]
bca warns on stderr whenever an explicitly named path overrides a walker exclude, naming the glob, so the miswiring is visible rather than silent:
bca: warning: utils/gate.py matches an exclude pattern (./utils/**) but was named explicitly; analyzing anyway
Treat that line as a to-do: the entry it names wants moving to [check] exclude. This repository moved its own dev-tooling globs there for exactly this reason.
One constraint on where the hook runs: while #1164 is open, a [check] exclude glob resolves against the working directory rather than the manifest root when the path is named explicitly. Both hooks below inherit the agent's working directory, which is the project root, so they are unaffected — but a hook that cds into a subdirectory first would see its exemptions stop matching.
Claude Code
仕組み: .claude/settings.json 内の PostToolUse フック。matcher はファイル編集ツールにスコープします。
フィードバックチャネル: これはあらゆるエージェント型ツールの中で最も強力な適合です。PostToolUse フックは編集が反映された瞬間に発火し、テキストを直接モデルへ注入できます — メッセージを stderr に出して終了コード 2 で終了する(Claude は stderr を何が起きたかのコンテキストとして読みます)か、hookSpecificOutput.additionalContext を含む JSON を出力するかのいずれかです。フィードバックは編集境界そのものに届き、言うべきことが生じるまでトークンコストはゼロです。
.claude/settings.json にフックを配線します:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/bca-check.sh"
}
]
}
]
}
}
ラッパースクリプトは、フックの stdin に届く JSON から編集されたファイルのパスを読み取り、そのファイルだけに bca check を実行し、ゲートに引っかかったときに限り、違反リストとそれに続くエージェント向けガイダンスブロックを stderr に出力して 2 で終了します:
#!/usr/bin/env bash
# .claude/hooks/bca-check.sh — 編集後に、編集された 1 ファイルをゲートします。
set -euo pipefail
# PostToolUse はツール呼び出しを JSON として stdin に渡します。編集された
# ファイルのパスは、Edit/Write/MultiEdit では .tool_input.file_path です。
file_path="$(jq -r '.tool_input.file_path // empty')"
[ -n "$file_path" ] || exit 0 # チェック対象なし。何も出力しません。
[ -f "$file_path" ] || exit 0 # ファイルが消えています(削除など)。
# Thresholds, baseline, and excludes come from the repo-root bca.toml.
# --no-summary / --no-remediation keep the feedback to the offender
# rows themselves; the guidance below tells the agent what to do. The
# rows are on stdout and any diagnostic on stderr, so `2>&1` captures
# whichever the run produced.
status=0
report="$(bca check "$file_path" --no-summary --no-remediation 2>&1)" || status=$?
# bca check の終了コードは、クリーンなら 0、違反ありなら 2、ツールエラーなら 1。
# 設定や IO のエラーが「複雑度」と誤ラベルされないよう、2 に限定して分岐します。
case "$status" in
0) exit 0 ;; # クリーンな場合 ⇒ 何も言わない。
2) ;; # 違反がある場合 ⇒ 以下で報告する。
*) printf 'bca check could not run (exit %s):\n%s\n' "$status" "$report" >&2
exit 0 ;;
esac
# 終了コード 2 により、Claude は stderr を編集に関する文脈として読みます。
# `set -u` の下では未設定の変数がフックを中断させ、ファイルが無い場合は
# 案内文の代わりに素の `cat` エラーが出るため、CLAUDE_PROJECT_DIR に既定値を
# 与え、案内ファイルの存在を確認します。
guidance="${CLAUDE_PROJECT_DIR:-$PWD}/.claude/hooks/bca-guidance.txt"
cat >&2 <<EOF
bca flagged complexity in the file you just edited:
$report
$([ -f "$guidance" ] && cat "$guidance")
EOF
exit 2
エージェント向けガイダンスブロック を .claude/hooks/bca-guidance.txt に保存し、フックのフィードバックと CLAUDE.md が同一の文言を参照するようにします。(依存関係は jq のみで、ほとんどのエージェントイメージに同梱されており、なくても 1 行でインストールできます。)
exit 2 シグナルを使わずに助言的なコンテキストを注入したい場合は、stderr に書き込む代わりに stdout へ JSON を出力します:
jq -n --arg ctx "$offenders" '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: $ctx
}
}'
exit 0
違反リストをメモとして Claude の視界に入れたい場合は additionalContext を、先へ進む前に対処すべき問題として提示したい場合は exit 2 を使います。どちらも編集はそのまま残ります — PostToolUse はツールの実行 後 に動作するため、いずれも編集を取り消すことはできません。
opencode
仕組み: プラグイン(hooks オブジェクトを返す非同期関数をエクスポートする JavaScript または TypeScript モジュール)で、ツール実行後(ファイル変更ツールの write と edit を含む)に発火する tool.execute.after フックを使用します。
フィードバックチャネル: after フックはthrow することで問題をエージェントに提示します。opencode の公開プラグインページには throw によるシグナルパターンは記載されていますが、after フック用の助言的な戻り値は記載されていないため、本レシピでは throw を既定とします — throw された Error のメッセージは、ツールの失敗としてエージェントに返されます。
引数の形に注意 — before フックとは異なります。 公開されている @opencode-ai/plugin の型定義では、tool.execute.after のシグネチャは (input, output) で、ツール名は input.tool、ツールの引数は input.args にあります(ファイルパスは input.args.filePath)。ここが罠です。ドキュメント唯一の実例は tool.execute.before のもので、そこでは引数が output.args(実行前で可変)にあります。それを after フックにコピーすると output.args は undefined になり、下記のガードが常に作動して、プラグインは一切実行されないまま沈黙します — インストール済みに見える no-op です。after フックでは input.args.filePath を読み取ってください。
このファイルを .opencode/plugins/ に配置します(プロジェクトレベルで自動読み込みされるため、opencode.json へのエントリは不要です。そのキーは npm 公開プラグイン用です)。以下のプラグインはプレーンな JavaScript です。TypeScript プラグインにする場合は import type { Plugin } from "@opencode-ai/plugin" してエクスポートに型注釈を付け、追加の依存関係は .opencode/package.json に宣言します(opencode が Bun でインストールします):
// .opencode/plugins/bca-check.js
const GUIDANCE = `
Responding to bca metric feedback: make the code genuinely simpler,
not the number smaller. Do not extract a meaningless helper or split a
cohesive function to dodge the count — a spurious helper often raises
file-level nom/nargs and helps nothing. If the complexity is essential
and the function is clearest left whole, add a suppression marker with
a one-line reason instead of contorting the code. Keep the fix in the
function that was flagged rather than widening it into a module
rewrite.
`.trim()
export const BcaCheck = async ({ $ }) => {
return {
// 注意: after フックの引数は `input.args` にあり、`output.args` ではない
// (output はツールの結果 title/output/metadata を保持する)。
"tool.execute.after": async (input, _output) => {
// ファイル書き込みツールにのみ反応する。(パッチ形式の編集ツールは
// 単一の filePath を持たないため、意図的に対象外とする。)
if (input.tool !== "write" && input.tool !== "edit") return
const filePath = input.args?.filePath
if (!filePath) return
// `bca check` はクリーンなら 0、違反があれば 2、ツールエラーなら 1 で終了する。
// Bun の $ はデフォルトで非ゼロ終了時に throw するため、正確なコードで
// 分岐できるようにキャプチャする。
const res = await $`bca check ${filePath} --no-summary --no-remediation`
.quiet()
.nothrow()
// 0 はクリーン、1 はツールエラーで、複雑度の問題ではない。`=== 2` ではなく
// `< 2` を使うことで、段階的終了コード(`--exit-codes=tiered` /
// `exit_codes = "tiered"` による 3-5)も報告される。
if (res.exitCode < 2) return
// Surface the offenders to the agent by throwing. The rows are on
// stdout; stderr is the fallback for a run whose only output is a
// diagnostic.
const offenders = res.stdout.toString().trim() || res.stderr.toString().trim()
throw new Error(`bca flagged complexity in ${filePath}:\n\n${offenders}\n\n${GUIDANCE}`)
},
}
}
GUIDANCE 文字列は、下記のそのまま使用するブロックと同期を保ってください(または共有ファイルから読み込みます)。チャネルが throw されたエラーであるため、opencode はこれを編集後ステップの失敗として報告します — これは意図どおりの「続行する前に対処せよ」という位置付けです。編集自体は反映されます。tool.execute.after は write / edit ツールがファイルを書き込み終えた後に実行されるため、throw は変更を取り消すことなく次のステップを方向付けます。
プラグイン追加後は opencode を再起動してください。 プラグインは起動時に一度だけ読み込まれ、ホットリロードされません。配置したばかりの .opencode/plugins/bca-check.js は実行中のセッションでは何もしません。opencode を終了して再起動し、しきい値を超えているとわかっているファイルを編集して、edit/write ツールが bca の失敗を報告することを確認してください。インストール済みなのに動かないプラグインは最もよくある症状で、古いセッションが最もよくある原因です。
堅牢化されたリファレンスとして、このリポジトリは独自のコピーを .opencode/plugins/bca-check.js に同梱しています。最小例では省かれている 3 つのガードが追加されており、いずれも実プロジェクトに移植する価値があります:
- リポジトリスコープガード。 パスを解決してプロジェクトルート外のものはスキップし、エージェントがディスク上の別の場所で編集したファイルに対してフックが
bcaを実行しないようにします。 - ローカルビルドの解決。
$BCA、次にチェックアウト内のtarget/release/bca、最後にPATH上のbcaの順で優先します。bcaを自前でビルドするプロジェクトは、グローバルにインストールされている何かではなく、自身のアナライザーでゲートできます。 - 共有ガイダンス。 このプラグインと Claude Code フックの両方が参照する 1 つのファイルからガイダンステキストを読み込み、両者が乖離しないようにします。
エージェント向けガイダンス(このまま使用する)
フィードバックチャネルはレシピの半分にすぎません。素の「cognitive 26 > 25」は、確実にメトリクスゲーミングの動きを誘発します — エージェントは関数ごとの数値を削るために意味的に空のヘルパーを抽出し、関数単位の複雑度を 下げ ながらファイル単位の nom/nargs を 上げ、コードを悪化させます。緩和策は、違反が何を意味し、どう対処すべきかをエージェントに伝えることです。このブロックをエージェントルールファイル(CLAUDE.md、opencode の AGENTS.md)とフックのフィードバックテキストの両方に貼り付け、恒常的なポリシーとしても違反の瞬間にも指示が存在するようにしてください:
**Responding to `bca` metric feedback.** A threshold violation
(cognitive, cyclomatic, ABC, …) means *this function is hard for a
human to follow*. The number is a proxy for that, not the goal. Your
job is to make the code genuinely simpler — not to make the number go
down.
- **Do not game the metric.** Do not extract a helper that exists only
to move complexity off one function, split a cohesive function at an
arbitrary line, collapse readable branches into a dense expression,
or inline/obfuscate logic to dodge the count. These lower the
per-function score while making the code worse — and a spurious
helper often *raises* file-level `nom`/`nargs`, so you have not even
helped the file.
- **Refactor only when it truly clarifies.** A good split has a name
that means something and a boundary a reader would have drawn anyway.
If you cannot name the extracted piece without inventing a
`foo_part2`, the split is gaming — stop.
- **When the complexity is essential, suppress with a reason.** Some
functions are irreducibly complex *and clearest left whole* — a
dispatch `match`, a hand-rolled parser table, an exhaustive state
machine. For these, do not contort the code: add a suppression marker
with the rationale on the same line —
`// bca: suppress(cognitive) — exhaustive opcode dispatch` — and move
on. A clear function with an honest marker is better than a
"compliant" tangle.
- **Keep the fix where the violation is.** The flag is scoped to the
function you just edited. Fix it there, mention anything larger you
noticed, and do not widen the change into a module rewrite to bring
the number down.
誠実な抑制(正確な構文)
上記のガイダンスは、抑制が正当な手段であることをエージェントに伝えます。それが機能するには、エージェントがマーカーを正しく綴る必要があります — マーカー構文は静かな no-op の頻出原因です。正確に教えてください(完全なリファレンス: 抑制マーカー):
- Per function — place the marker in a comment inside the function body, naming the metric(s):
// bca: suppress(cyclomatic, abc) — hand-rolled parser table. A bare// bca: suppress(no list) silences every metric for that function. - ファイル全体 —
// bca: suppress-file(halstead, nargs, nexits)をファイル内の任意の場所に置きます。リストのない// bca: suppress-file形式は、ファイル全体ですべてのメトリクスを抑制します。 - Put the rationale on the marker line, after a metric list. Anything after the list is free text and does not need a separator, so the reason lives where the next reader of the flagged function will see it. (A bare verb takes no trailing text at all — nothing there distinguishes a rationale from prose about the marker, so naming the metrics is what buys you a reason.) Write it every time: a reviewer — human or agent — needs to tell an honest exemption from a dodge, and
bca exemptionslists every marker in the tree for exactly that audit. - Use canonical metric names. The accepted identifiers are
abc,cognitive,cyclomatic,halstead,loc,mi,nargs,nexits,nom,npa,npm,wmc. It isnexits, notexit(the legacyexitalias was retired). An unknown identifier warns and is skipped; the recognized names beside it still suppress, sosuppress(cognitive, exit)silencescognitiveand complains aboutexit.tokensis deliberately not suppressible; treat it as a hard resource cap.
編集ごとではなくタスク境界でゲートする
上記の編集ごとのフックは早期警告のための利便機能であり、ゲートではありません。ゲートは、人間がタスク完了を宣言する前に実行するのと同じチェック、すなわち 2 段構えの make self-scan / pre-commit パターン(ハード層は CI をミラーし、ソフト層は上限の 95% のヘッドルーム帯)です。エージェントには「完了と言う前」のステップとしてこれを指し示してください。
タスク境界より細かい粒度は、エージェントにとって価値が低いか、むしろ逆効果です。複雑度のしきい値は正しさのゲートではなくプロキシなので(下記の注意点を参照)、リファクタリング途中の あらゆる 微小編集の後に再実行しても、数編集後には自然に解消される一時的な違反が生まれるだけです — エージェントはそのノイズの「修正」に 1 ターンを浪費します。エージェントに一貫した変更を仕上げさせ、それから一度だけゲートしてください。
ルールファイルに書かないほうがよい指示が 1 つあります。フックの上に重ねて「修正を検証し、メトリクスを再チェックする」という常設の手順を置くことです。現在のモデルは、指示されなくても自分の編集を読み直し、再チェックします。明示的な指示はその挙動と重なり、入力が変わっていないゲートの再実行にループがターンを費やすことになります。編集が着地すれば、フックは自ら発火します。それを合図としてください。
注意点
本レシピが依存する 3 つの注意点です。無視すると、このループは益より害をもたらします。
- グッドハートの法則 / メトリクスゲーミング。 「この数値を小さくせよ」は「このコードを簡潔にせよ」と同じ指示ではなく、前者を告げられた LLM は最も安上がりな方法でそれを満たします — 通常は複雑さを取り除くのではなく、関数境界の向こうへ動かすだけです。エージェント向けガイダンスブロック と 誠実な抑制のセクション がその緩和策であり、これらは任意の飾りではなく構造上不可欠です。フックと一緒に出荷しなければ、ゲーミングの動きを覚悟してください。
- しきい値はプロキシであり、正しさのゲートではありません。 コンパイル失敗や赤いテストには曖昧さがなく、それらに対するタイトなエージェントループは収束します。複雑度のしきい値はもっと柔らかいものです。しきい値超過は「これがまだ読みやすいか人間が確認すべき」という意味であり、判断の問題であって欠陥ではありません。それに応じて期待値を設定してください — 複雑度フィードバックは、何としてもゼロに追い込むべき合否判定ではなく、エージェントが重み付けして考慮する助言として配線します。
- 複雑度フィードバックはスコープクリープを招きます。 「この関数は追いにくい」という指摘は、エージェントには再構成への誘いとして読まれます。そしてその再構成は、フックが名指しした関数で確実に止まるとは限りません — 2 行の修正が、誰もレビューを頼んでいないモジュール書き換えになります。ルールファイルで範囲を区切ってください。違反は、それを引き起こした編集に限定されます。指摘された場所で直し、それより大きな気づきがあれば言及するにとどめ、数字を追って変更を広げないでください。
AST クエリ
パース済みの構文木を直接扱うレシピです。ノード型の検索、そのカウント、ツリーのダンプを行います。
ライブラリ側の同等物。 以下の各レシピには、AST を直接歩く にプロセス内で動く Rust 版の対応物があります — ファイルごとのシェル起動が遅すぎる場合や、1 回のパースでメトリクスとカスタム AST 分析を組み合わせたい場合に有用です。
コミット前にパースエラーを検出する
tree-sitter はパースできなかったあらゆる箇所に合成の ERROR ノードを出します。find でそれらを表面化させます:
bca find \
--include "*.rs" \
--paths "$PWD" \
-t ERROR
1 回の指定につきグロブ 1 つ。
--includeと--excludeは出現ごとにちょうど 1 つの値を取ります。複数のグロブにはフラグを繰り返してください(--include "*.rs" --include "*.py")。後続の位置引数パスが飲み込まれることはありません。=形式(--include="*.rs")も使えます。
クリーンな実行は何も出力しません。これを pre-commit フックに組み込めば、構文的に壊れたファイルがステージされたときに素早く失敗させられます。
特定の構文要素を数える
count は繰り返し可能な -t/--type フラグで 1 つ以上のノード型を受け取り、合計を報告します。たとえば Rust プロジェクト全体で if、for、while の構文を数えるには:
bca count \
--include "*.rs" \
--paths src/ \
-t if_expression -t for_expression -t while_expression
正確なノード型名は下層の tree-sitter 文法に由来します。見つけるには、小さなサンプルファイルの AST をダンプし(下記参照)、ツリーからノード名を読み取ってください。
Rust クレート内のすべての unsafe ブロックを見つける
bca find \
--include "*.rs" \
--paths src/ \
-t unsafe_block
各マッチはファイルパスとノードの行範囲を出力します。
ファイルの AST をダンプする
メトリクスがなぜその値になったのかを理解したり、find / count に必要な tree-sitter ノード名を発見したりするのに便利です:
bca dump --paths src/lib.rs
ダンプを特定の関数やブロックに絞るには、--line-start と --line-end フラグで行範囲を追加します(dump サブコマンドの後に置く必要があります):
bca dump \
--paths src/lib.rs \
--line-start 42 --line-end 88
--line-start / --line-end は dump と find に適用されるため、同じ範囲指定で検索を単一の関数にスコープできます:
bca find \
--paths src/lib.rs \
--line-start 42 --line-end 88 -t return_expression
短縮形の --ls / --le は非推奨エイリアスとして残っていますが、次のメジャーリリースで削除される予定です。
すべての関数・メソッドを一覧する
人間が読むための簡単なインベントリには:
bca functions \
--include "*.rs" \
--paths src/
出力はファイルごとのツリーです。In file … ヘッダーに続き、関数ごとに名前と行スパンを持つインデントされた行が並びます。読むためのものであり、パースするためのものではありません。
構造化されたインベントリが必要なツール(カバレッジマッピング、ドキュメント生成、コードオーナーレポート)には、代わりに JSON の metrics 出力を使い、.spaces[] を再帰的に歩いて kind が function のエントリを取り出します:
bca metrics \
--include "*.rs" \
--paths src/ \
-O json \
| jq -c '
. as $root
| def funcs: if .kind == "function" then [.] else [] end
+ (.spaces // [] | map(funcs) | add // []);
funcs[] | {file: $root.name, name, start_line, end_line}
'
これは関数ごとに 1 つの JSON オブジェクトを出力し、下流のツールへ安全にパイプできます。
メトリクスデータのエクスポート
metrics、ops、preproc の各サブコマンドはいずれも、機械処理向けの構造化出力フォーマットをサポートします。アドホックな分析には jq のような JSON プロセッサと組み合わせるか、データベースやダッシュボードに投入してください。
ファイルごとのメトリクスを JSON でエクスポートする
bca metrics \
--paths src/ \
-O json \
--output-dir /tmp/metrics
これは解析対象のソースファイルごとに 1 つの JSON ファイルを /tmp/metrics/ 以下に書き出します。出力ファイル名は入力パスにフォーマットの拡張子を付加したものです — src/lib.rs は src/lib.json ではなく src/lib.rs.json になります。手で読むつもりなら --pretty を使ってください:
bca metrics -p src/ --pretty -O json --output-dir /tmp/metrics
実行全体をツリーではなく 1 つのファイルにまとめるには --output <file> を使います。単一の集約ドキュメント(ファイルごとの結果のトップレベル JSON 配列)を書き出します:
bca metrics -p src/ -O json --output /tmp/metrics.json
CBOR(-O cbor)は最もコンパクトなフォーマットですが、バイナリのため出力先(--output または --output-dir)が必要です。JSON、TOML、YAML は出力先が指定されない場合いずれも stdout にストリームでき、パイプラインに便利です。
bca diff で 2 つのメトリクス実行を比較する
bca diff は 2 つの JSON メトリクス実行を比較し、メトリクスごとに、どのファイルが変化したか(旧 → 新)に加えて、2 つのセット間で追加・削除されたファイルを報告します。各サイドは単一のファイル別 JSON ドキュメント、またはそれらのディレクトリツリー全体(metrics -O json --output-dir <dir> 形式が書き出すもの)なので、一般的なワークフローは 2 回の bca metrics 実行を別々のディレクトリへ出力することです:
# 「変更前」の状態を取得する。
bca metrics -p src/ -O json --output-dir /tmp/before
# ...変更を加える(例: tree-sitter 文法のバンプ)...
# 「変更後」の状態を取得して diff を取る。
bca metrics -p src/ -O json --output-dir /tmp/after
bca diff /tmp/before /tmp/after
出力はファイルごとの差分をメトリクス名でまとめます — bca list-metrics が出力するのと同じ名前です(cyclomatic、cognitive、sloc、…):
2 metric(s) changed, 1 added file(s), 0 removed file(s)
## Added files
src/new_module.rs.json
## cyclomatic (2 change(s))
src/lib.rs.json.sum 12 → 14
src/lib.rs.json.max 4 → 6
## halstead (1 change(s))
src/lib.rs.json.effort 5820.3 → 7104.9
便利なフラグ:
--format markdownはスティッキーな PR コメント向け、--format jsonは安定した機械可読スキーマ向け(CI コンシューマー)です。--min-change <N>は絶対変化量がN以上の差分のみを報告します(デフォルトの0はあらゆる変化を報告します)。--metric <name>(繰り返し可能)は diff を特定のメトリクスに限定します。
--since で git ref と比較する
「コミットしていない変更はメトリクスに何をしたか?」という対話的なケースでは、--since <ref> が 2 回キャプチャする手間を省きます。変更前サイドとして git ref 時点のツリーを解析し、現在の作業ツリー(または明示的に指定した変更後サイドのツリー)と比較します:
# 変更前 = HEAD~1 時点のツリー。変更後 = 現在の作業ツリー。
bca diff --since HEAD~1 -p src/
# 変更後 = 作業ツリーの代わりに明示的なツリー(例: 2 つ目のチェックアウト)。
# 唯一の位置引数が変更後サイドになる。
bca diff --since main /path/to/other-checkout -p src/
--since は ref のツリーを一時ディレクトリに実体化し(git archive 経由)、それに対して同じメトリクスウォークを実行してから diff を取ります — 一時ツリーはエラー時も含めて自動的に削除されます。同じ -p/--paths、-I/--include、-X/--exclude の選択が両サイドに適用されるため、同一のファイルセットを解析します。選択パスはリポジトリルート相対で、相対パスでなければなりません。作業ツリー側は(ref の全ツリー git archive に合わせて)リポジトリルートに固定されるため、bca diff --since はどのサブディレクトリから実行しても同じ結果になります。絶対パスの --paths は拒否されます(終了コード 1)— 展開された ref ツリーを指せないためです。
ベストエフォートの bca check --since とは異なり、bca diff --since は明示的な要求です。解決できない ref、git の欠如、git 管理外の作業ディレクトリはハードエラー(終了コード 1)になります。--since は位置引数を最大 1 つ(変更後サイド)しか取りません。2 つ渡すとエラーです。
bca diff は成功時に 0 で終了します — ゲートではなく情報提供です — ただしオプトインの --exit-code フラグを渡すと、フィルタ後の diff が空でない場合に 2 で終了します。これは、文法バンプがメトリクスを退行させていないことの検証に使われていた、かつての json-minimal-tests + split-minimal-tests.py チェーンを置き換えるものです。utils/check-grammar-crate.py ヘルパーは現在、内部で bca diff を呼び出します。
ツリー全体から単一のメトリクスを取り出す
ストリームされた JSON 出力を jq と組み合わせて、ファイルごとに 1 つの値を抽出します:
bca metrics -p src/ -O json \
| jq -c '{file: .name, mi: .metrics.mi.visual_studio}'
同じ考え方はどのメトリクスにも使えます — cyclomatic.sum、cognitive.sum、loc.sloc など。カタログを見るには bca list-metrics descriptions を実行してください。
実行時にメトリクスカタログを発見する
CLI を駆動するツールはメトリクス名をハードコードすべきではありません。バイナリに尋ねてください:
bca list-metrics # 1 行につき 1 つの名前
bca list-metrics descriptions # 名前 + 概要
これはコードジェネレーター、スキーマ定義、タブ補完に適した入力です。
オペランドとオペレーターを抽出する(Halstead)
ops はファイルごとに生のオペランド・オペレーターのリストを出力します。これは、組み込みレポートが示す範囲を超えた Halstead 系メトリクス計算への入力になります:
bca ops \
--include "*.rs" \
--paths src/ \
-O json --pretty \
--output-dir /tmp/ops
1 回の指定につきグロブ 1 つ。
--includeと--excludeは出現ごとにちょうど 1 つの値を取ります。複数のグロブにはフラグを繰り返してください(--include "*.rs" --include "*.py")。後続の位置引数パスが飲み込まれることはありません。=形式(--include="*.rs")も使えます。
各出力ファイルは入力パスを /tmp/ops/ 以下にミラーします。
どちらのリストもソート済みなので、変更のないソースに対して ops を再実行するとバイト単位で同一の出力が得られます — 実行間で差分を取ったり、リポジトリにコミットしたり、キャッシュキーとして使ったりしても安全です。
ツリーからコメントを除去する
strip-comments は、コメント構文を理解しない下流のツールでもコードを利用できるようにソースを書き換えます。出力ルーティングには 3 つのモードがあります:
- stdout(デフォルト)。 どちらのフラグもない場合、除去済みソースは stdout にストリームされます — パイプライン内の単一ファイルに最適です。
--output/-o <FILE>(単一ファイル)。 除去済みソースを<FILE>に書き出し、入力はそのまま残します。入力ファイルが 1 つの場合にのみ意味を持ち、--in-placeとは相互排他です。--in-place(複数ファイル)。 マッチした各入力ファイルをディスク上で書き換えます。ツリー全体にはこれを使ってください。--outputとは相互排他です。
# コメントを除去した単一ファイルをストリーム出力する。
bca strip-comments --paths src/lib.rs
# 除去済みの単一ファイルを新しいパスに書き出す(入力は変更しない)。
bca strip-comments --paths src/lib.rs --output src/lib.stripped.rs
# src/ 内のすべての Python ファイルをインプレースで書き換える。
bca strip-comments --include "*.py" --paths src/ \
--in-place
--in-place は破壊的です — 事前にツリーがコミット済みまたはバックアップ済みであることを確認してください。--in-place と --output の両方を渡すのは使用方法エラーです。
REST API の操作
bca-web は同じ分析プリミティブを HTTP 経由で公開します。コンシューマーが長時間稼働するサービス(エディタープラグイン、CI ワーカー、Web アプリ)で、ファイルごとに CLI を起動するコストを払うべきでない場合に使ってください。
エンドポイントの完全なリファレンスは Rest API を参照してください。以下のレシピは curl を使った実践的なエンドツーエンドの呼び出しを示します。すべてのエンドポイントは /v1 プレフィックスの下にマウントされます。旧来のプレフィックスなしパスは 2.0 リリース(#637)で削除され、現在は 404 を返します。
サーバーを起動する
bca-web --host 127.0.0.1 --port 8080 -j "$(nproc)"
起動していることを確認します:
curl -sf http://127.0.0.1:8080/v1/ping && echo "ok"
# => ok
/ping は空のボディで 200 OK を返します — curl -sf は成功時に 0、HTTP エラー時に非ゼロで終了し、これはまさにスクリプトが望む挙動です。
ビルド、フラグや環境変数のチューニング、リソース制限、デーモン自体のセキュリティ態勢については、bca-web の運用 を参照してください。
インラインスニペットのメトリクスを計算する
curl -s http://127.0.0.1:8080/v1/metrics \
-H 'Content-Type: application/json' \
-d '{
"id": "snippet-1",
"file_name": "demo.rs",
"code": "fn add(a: i32, b: i32) -> i32 { a + b }",
"scope": "full"
}' \
| jq '.root.metrics'
scope: "file" はトップレベルのメトリクスのみを返します。"full"(デフォルト)はスニペット内のすべての関数・クラススペースを歩きます。サーバーは file_name から言語を推測するため、拡張子が重要です。
ディスク上のファイルのメトリクスを計算する
curl --data-binary と jq を組み合わせれば、実ファイルをサーバーが期待する JSON エンベロープへ簡単に詰め込めます:
jq -nc \
--arg id "$(uuidgen)" \
--arg file_name "src/lib.rs" \
--rawfile code src/lib.rs \
'{id: $id, file_name: $file_name, code: $code, scope: "full"}' \
| curl -s http://127.0.0.1:8080/v1/metrics \
-H 'Content-Type: application/json' \
--data-binary @- \
| jq '.root.metrics.cyclomatic, .root.metrics.cognitive'
このパターン — リクエストを組み立てる jq -n --rawfile と、それをストリームする curl --data-binary @- — は、複数行のソースコードでの引用符の問題を避ける最も簡単な方法です。
API 経由でコメントを除去する
エンドポイントは /comment(単数形)です。Content-Type で選択される 2 つのバリアントがあります:
application/json— リクエストとレスポンスを JSON でラップします。レスポンスのcodeフィールドは文字列ではなくバイト配列です。下層の API がバイト指向のためです。application/octet-stream— ソースを生のリクエストボディとして受け取り、除去済みソースを生のレスポンスボディとして返します。シェルから使うには断然これが最も簡単なバリアントです。
octet-stream 形式(単発のシェル利用に推奨):
curl -s "http://127.0.0.1:8080/v1/comment?file_name=demo.py" \
-H 'Content-Type: application/octet-stream' \
--data-binary $'# leading comment\nprint("hi") # trailing'
# => print("hi")
JSON 形式(クライアントがネイティブに JSON を扱う場合に使用します)。ASCII / UTF-8 のソースであれば、バイト配列は jq … | implode でデコードできます:
curl -s http://127.0.0.1:8080/v1/comment \
-H 'Content-Type: application/json' \
-d '{
"id": "strip-1",
"file_name": "demo.py",
"code": "# leading comment\nprint(\"hi\") # trailing"
}' \
| jq -r '.code | implode'
JSON レスポンスには送信した id がそのまま含まれるため、多数のリクエストを多重化するクライアントでも対応付けができます。
エディタプラグイン向けに関数スパンを抽出する
エンドポイントは /function(単数形)です:
curl -s http://127.0.0.1:8080/v1/function \
-H 'Content-Type: application/json' \
-d '{
"id": "spans-1",
"file_name": "demo.rs",
"code": "fn a() {}\nfn b() {}\n"
}' \
| jq '.spans'
各エントリには name、start_line、end_line、および error ブール値(パーサーが関数スパンを不正と判定した場合に設定されます)が含まれます。エディタがローカルでファイルを再パースせずに関数ナビゲーターを描画するには十分な情報です。
変更履歴リスクでリポジトリをランク付けする
/vcs エンドポイントは、サーバー側のファイルシステム上に既に存在する git 作業ツリーを分析し(変更履歴はリクエスト内で表現できません)、複合リスクスコアでランク付けしたファイル一覧を返します。シグナルと計算式のリファレンスは変更履歴(VCS)メトリクスを参照してください。
セキュリティ: 他のエンドポイントと異なり、
/vcsはサーバー側のrepo_pathを受け取り、サーバープロセスが読み取れる任意の git リポジトリを走査して、そのリポジトリのファイルパスと変更シグナルを返します。認可レイヤーなしで/vcsを信頼できないクライアントに公開しないでください。デフォルトの127.0.0.1バインドであればローカルに閉じたままになります。
curl -s http://127.0.0.1:8080/v1/vcs \
-H 'Content-Type: application/json' \
-d '{
"id": "risk-1",
"repo_path": "/srv/checkouts/my-project",
"top": 20
}' \
| jq '.files[] | {path, risk_score, churn_recent}'
本文には bca vcs と同じ調整項目(long_window、recent_window、top、ref、risk_formula、file_types、full_history、include_merges、follow_renames、exclude_bots、bot_pattern、as_of、emit_author_details、include_deleted、bus_factor_threshold、no_cache、cache_dir)を省略可能なフィールドとして指定できます。file_types はランキングの対象範囲を絞り込み(デフォルトの metrics / all / rs,py 形式の拡張子リスト)、CLI の --file-types に対応します。git 作業ツリーでない repo_path や、不正なウィンドウ / タイムスタンプ / 計算式 / 対象範囲を指定した場合は、統一 JSON エラー本文とともに 400 が返ります。
時系列トレンド
POST /vcs/trend は同じメトリクスを複数の時点でサンプリングし、ファイルごとの時系列を返します(履歴トレンドを参照)。本文には /vcs のフィールドに加えて points(2 以上)、span(デフォルト 12mo)、top_deltas を指定します。
curl -s http://127.0.0.1:8080/v1/vcs/trend \
-H 'Content-Type: application/json' \
-d '{
"id": "trend-1",
"repo_path": "/srv/checkouts/my-project",
"points": 12,
"span": "24mo",
"top": 20
}' \
| jq '.deltas.regressed[] | {path, delta}'
ポイント数が 2 未満(またはサポートされる上限を超える場合)は、他の不正リクエストの場合と同様に、統一 JSON エラー本文とともに 400 が返ります。
ジャストインタイムのコミット / diff スコアリング
POST /vcs/jit は単一のコミットをスコアリングします(ジャストインタイムスコアリングを参照)。本文には repo_path、commit(デフォルト HEAD)と、long_window / recent_window / full_history / include_merges / follow_renames / as_of の各調整項目を指定します。エコーバックされた id を含むコミットの JitReport JSON が返ります。
curl -s http://127.0.0.1:8080/v1/vcs/jit \
-H 'Content-Type: application/json' \
-d '{ "id": "jit-1", "repo_path": "/srv/checkouts/my-project",
"commit": "HEAD" }' \
| jq '{risk_score, purpose: .commit.purpose}'
代わりに任意の diff をスコアリングするには、unified diff を格納した diff フィールドを送信します(repo_path は不要です)。この場合のレスポンスは 部分 レポートになります。すなわち source: "diff" と partial_risk_score を含み、履歴 / 経験 / 目的の各グループは含まれません(ゼロではなく欠落します)。部分スコアはコミットスコアと比較できません。
git diff | jq -Rs '{id: "jit-diff", diff: .}' \
| curl -s http://127.0.0.1:8080/v1/vcs/jit \
-H 'Content-Type: application/json' -d @- \
| jq '{source, partial_risk_score}'
不正な diff(あるいは解決できない commit、git 作業ツリーでない repo_path)を指定すると、統一 JSON エラー本文とともに 400 が返ります。
CI から API を呼び出す
サーバーはミリ秒単位で起動するため、短時間で終わる CI ジョブでは、ジョブ内でバックグラウンドプロセスとして起動し、最後に停止するのが最も簡単なことが多いです:
bca-web --port 8080 &
SERVER_PID=$!
trap 'kill "$SERVER_PID"' EXIT
# 起動するまで待ちます。
until curl -sf http://127.0.0.1:8080/v1/ping >/dev/null; do sleep 0.1; done
# … ここで分析の呼び出しを実行します …
長時間稼働するワーカーでは、サーバーを systemd ユニット(またはコンテナ)として実行し、ジョブからそのホスト / ポートを参照してください。
ライブラリとしての利用
big-code-analysis は Rust ライブラリとして crates.io に公開されています。CLI(bca)と REST サーバー(bca-web)はどちらも同じ公開 API の薄いラッパーなので、それらにできることはすべて自分のクレートから直接行えます。
このセクションはタスク指向です。完全な型シグネチャとフィールドのドキュメントは docs.rs の rustdoc を参照してください。
ライブラリを組み込むべきとき
次のうち一つ以上が必要な場合は、(bca を外部プロセスとして呼び出す代わりに)ライブラリを使用してください:
- インプロセス分析。 カスタムツール、IDE プラグイン、静的解析パイプラインで数千のファイルをスコアリングする際に、ファイルごとにサブプロセスを起動するコストを回避します。
- インメモリソース。 生成された、前処理済みの、あるいはストリーミングされたソースを、先にディスクへ書き出すことなくスコアリングします。インメモリソースの分析を参照してください。
- 選択的な走査。
FuncSpaceツリーを独自に走査し、関数ごとのメトリクスを任意のタイミングで抽出します。FuncSpace 結果の走査を参照してください。 - カスタム出力。
src/output/配下の JSON / YAML / TOML / CBOR シリアライザーを使わず、独自のレポート形式(CSV、SARIF、データベース行など)を出力します。
Markdown の品質レポートや CI のしきい値ゲートが欲しいだけであれば、bca CLI の方が手早く構築できます。
現在提供している内容
- クイックスタート — 文字列をパースし、
FuncSpaceを取得して、認知的複雑度を出力します。 - インメモリソースの分析 — ファイルではなくバッファからソースを与えます。
- 既存の tree-sitter Tree の再利用 — 呼び出し側で構築した
tree_sitter::Treeをメトリクスウォーカーに渡します。 - 一度のパースでメトリクスを何度も実行する — パース済みの
Astを保持し、同じツリーに対して複数のメトリクスサブセットやカスタム走査を実行します。 - AST を直接走査する — 構文要素のカウント、種類によるノードの検索、パースエラーの検出、メトリクス走査と並行したシンボルテーブルの構築を行います。
- メトリクスの選択 —
MetricsOptions::with_onlyで必要なメトリクスだけを計算します。依存するメトリクスが入力をどう引き込むかも説明します。 FuncSpace結果の走査 — 入れ子になった関数 / クラス / impl のスペースへ再帰します。- エラー処理 — 現在の
Result<FuncSpace, MetricsError>の意味と、それを有用な診断へ変換する方法です。 - 安定性とバージョニング —
2.x系を通じて信頼できるもの・できないものです。
API の安定性について
このライブラリは 2.x 系にあり、明文化された安定性契約のもとで提供されています。公開 API の形状はパッチおよびマイナーバンプの間で安定に保たれ、破壊的変更は次のメジャーバンプまで保留されます。このセクションのすべての例は現在公開中のクレートに対してコンパイルでき、2.x の間は修正なしでコンパイルできることが期待されます。
メトリクスの 値 は、文法のピンが移動したりメトリクス定義が修正されたりすると、マイナーバンプの間でも変動することがあります。この例外については STABILITY.md § What is stable in value を参照してください。各変動は、それを導入する changelog エントリで明示されます。
クイックスタート
このページでは、ソースコードの文字列からメトリクスを計算するのに必要な最小限のコードを説明します。
1. クレートを追加する
# Cargo.toml
[dependencies]
big-code-analysis = "2.0.0"
このクレートは Rust エディション 2024 を使用し、rust-version = "1.94" をピン留めしています。これより古いツールチェーンではビルドできません。ポリシーは STABILITY.md の MSRV セクション を参照してください。
2. 文字列からメトリクスを計算する
推奨エントリポイントは analyze です。言語、ソースバイト列、省略可能な表示名を持つ Source と、走査ごとのフラグを指定する MetricsOptions を渡します。ファイルシステムのパスは不要です。
use big_code_analysis::{analyze, MetricsOptions, Source, LANG}; fn main() { let source = "fn add(a: i32, b: i32) -> i32 { a + b }"; let space = analyze( Source::new(LANG::Rust, source.as_bytes()) .with_name(Some("snippet.rs".to_owned())), MetricsOptions::default(), ) .expect("Rust source should parse"); println!( "cognitive complexity (file-level): {}", space.metrics.cognitive.cognitive_sum(), ); }
Source::name はトップレベルの FuncSpace::name になります。None を渡すとトップレベルの名前は未設定のままです。戻り値の型は Result<FuncSpace, MetricsError> です。実際には、Err バリアントは要求した言語の Cargo フィーチャがこのビルドで無効であることを意味します。パースの失敗は Err を生みません(tree-sitter は ERROR ノードで復旧します)。バリアントの一覧とマッチングのパターンはエラー処理を参照してください。MetricsError は #[non_exhaustive] なので、マッチする際は必ず _ アームを含めてください。
ヒント: use big_code_analysis::prelude::*; で、推奨エントリポイント(analyze、Ast、Source、MetricsOptions、MetricsError、LANG、FuncSpace、CodeMetrics、SpaceKind、Metric)を 1 行でスコープに取り込めます。プレリュード外のものも名前で参照できます。たとえば use big_code_analysis::guess_language; のようにします。
一度のパースからメトリクス以外のもの(演算子 / オペランド、AST ダンプ、関数スパンの一覧)が必要ですか?その場合は
Ast::parseで一度だけパースし、ハンドルに対してパスごとのメソッドを呼び出してください。一度のパースでメトリクスを何度も実行するを参照してください。すでに独自のtree_sitter::Parserを使用している場合は、生成されたツリーをAst::from_tree_sitterで取り込みます(既存の tree-sitter Tree の再利用を参照)。
3. 返ってきたもの
FuncSpace はスペースのツリーです。トップレベルのノードはファイル全体を表し、その spaces フィールドが入れ子の関数 / クラス / impl スペースを保持します。すべてのノードが同じ CodeMetrics 構造体を持つため、どの粒度のレベルでも任意のメトリクスを読み取れます。
use big_code_analysis::{analyze, MetricsOptions, Source, SpaceKind, LANG}; fn main() { let source = "\ fn outer() { fn inner() {} } "; let space = analyze( Source::new(LANG::Rust, source.as_bytes()) .with_name(Some("snippet.rs".to_owned())), MetricsOptions::default(), ) .expect("Rust source should parse"); assert_eq!(space.kind, SpaceKind::Unit); assert_eq!(space.spaces.len(), 1); // `outer` assert_eq!(space.spaces[0].spaces.len(), 1); // `inner` }
FuncSpace をより深く走査する方法は FuncSpace 結果の走査を参照してください。
言語の選択
言語が事前にわからない場合は guess_language を使用してください。パスの拡張子、バッファ内の Emacs モード行、シバンの順に調べます:
use std::path::PathBuf; use big_code_analysis::{analyze, guess_language, MetricsOptions, Source}; fn main() { let source = b"print('hi')\n"; let path = PathBuf::from("hello.py"); let (Some(lang), _name) = guess_language(source, &path) else { eprintln!("unrecognised language"); return; }; let _space = analyze( Source::new(lang, source).with_name(Some("hello.py".to_owned())), MetricsOptions::default(), ); }
guess_language は未知の拡張子に対して (None, _) を返します。これはパースエラーではなく「このファイルをスキップする」ものとして扱ってください。
いつ何が変わるか
推奨エントリポイントは analyze(Source, MetricsOptions) で、Result<FuncSpace, MetricsError> を返します。
メモリ上のソースの分析
big-code-analysis はソースがディスク上にあることを要求しません。推奨エントリポイント analyze は、言語、ソースバイト列、* 省略可能な_ 呼び出し側指定の表示名を持つ Source を受け取ります。C/C++ プリプロセッサの参照でパスが必要になる場合(Source::preproc_path)を除き、ファイルシステムのパスは関与しません。
これは次の用途に有用です:
- 生成コードを書き出す前にスコアリングする。
- 前処理済み「または」*バンドル済み*のソース(テンプレート展開後など)をスコアリングする。
- すでにバッファをメモリ上に保持している言語サーバーやエディタプラグインから分析ツールを駆動する。
- 標準入力パイプラインや、ファイルシステムに触れるべきでない単体テスト。
バッファからの読み取り
#![allow(unused)] fn main() { use big_code_analysis::{analyze, MetricsOptions, Source, LANG}; fn analyze_buffer(source: &[u8]) -> Option<u64> { // `Source::name` はトップレベルの `FuncSpace` に埋め込まれる // 表示識別子です。下流の消費者(ログ、JSON 出力)にとって // 意味のあるものを選んでください。付ける価値のあるものが // なければ `None` を渡します。 let space = analyze( Source::new(LANG::Python, source).with_name(Some("<stdin>".to_owned())), MetricsOptions::default(), ) .ok()?; Some(space.metrics.cognitive.cognitive_sum()) } }
Source::new はソースバイト列を借用し、所有権は呼び出し側に残ります。下流のパイプラインが同じバイト列上で検出結果をハイライトする必要がある場合、analyze が返った後も元のバッファをそのまま使い続けられます。
標準入力からの読み取り
use std::io::{self, Read}; use big_code_analysis::{analyze, MetricsOptions, Source, LANG}; fn main() -> io::Result<()> { let mut source = Vec::new(); io::stdin().read_to_end(&mut source)?; let space = match analyze( Source::new(LANG::Javascript, &source) .with_name(Some("<stdin>".to_owned())), MetricsOptions::default(), ) { Ok(space) => space, Err(err) => { eprintln!("parse failed: {err}"); std::process::exit(1); } }; println!("{}", space.metrics.cyclomatic.cyclomatic_sum()); Ok(()) }
内容から言語を選択する
言語が事前にわからない場合は、guess_language と analyze を組み合わせてください。guess_language はパスの拡張子、Emacs モード行、シバンの順に調べます:
#![allow(unused)] fn main() { use std::path::PathBuf; use big_code_analysis::{analyze, guess_language, MetricsOptions, Source}; fn analyze_unknown(path: PathBuf, source: Vec<u8>) -> Option<()> { let (lang, _name) = guess_language(&source, &path); let lang = lang?; // `.ok()?` は `MetricsError` を `None` に畳み込み、このヘルパーの // `Option` の戻り値の形を保ちます。バリアントを保持するより // 詳細なマッピングは `error-handling.md` を参照してください。 let _space = analyze( Source::new(lang, &source) .with_name(path.to_str().map(str::to_owned)), MetricsOptions::default(), ) .ok()?; Some(()) } }
guess_language は認識できない拡張子に対して (None, _) を返します。これはハードエラーではなく「スキップ」として扱ってください。
注意点
- 名前の同一性は重要です。 トップレベルの
FuncSpace::nameはSource::nameに指定した文字列そのものです。同じ名前を共有する 2 つの分析は、それをキーにする下流の消費者からは区別できません。異なるバッファには異なるラベルを使用してください。 Source::nameはOption<String>です。Noneを渡すとトップレベルのFuncSpace::nameはNoneのままになります。意味のある識別性を持たないアドホックなスニペットには便利です。安定した識別子を 必要とする 下流の消費者は、Noneを明示的にチェックすべきです。- ファイルシステムへのフォールバックはありません。 CLI と異なり、ライブラリは隣接ファイルを読んだり、
#includeを辿ったり、.gitignoreを解釈したりしません。分析したいバイト列を正確に与えてください。
一度のパースを複数のパスで再利用する
analyze はワンショットのエントリポイントです。バイト列を入れると FuncSpace が返ります。一度のパースからメトリクス以外のもの(演算子とオペランド、AST ダンプ、関数スパンの一覧)が必要な場合は、Ast::parse で一度だけパースし、返されたハンドルに対してパスごとのメソッド(metrics、ops、dump、functions など)を呼び出してください。一度のパースでメトリクスを何度も実行するを参照してください。
既存の tree-sitter Tree の再利用
よくある課題として、構文ハイライト、コード折りたたみ、クエリのためにすでに tree-sitter を利用している呼び出し側は、すべてのファイルを 2 回パースすることになります。自分のツリーのために 1 回、メトリクスウォーカーの内部でもう 1 回です。パースシームを使うと、パース済みの tree_sitter::Tree を big-code-analysis に渡し、再パースなしで同じ FuncSpace を得られます。
Ast::from_tree_sitterを使用してください。 呼び出し側で構築したtree_sitter::Treeを取り込み、同じパース結果に対してメトリクスウォーカーを複数回実行できます(MetricsOptions::with_onlyの選択の切り替え、メトリクスと交互に行うカスタム tree-sitter 走査、演算子 / オペランド抽出のためのAst::opsなど)。一度のパースでメトリクスを何度も実行するを参照してください。トップレベルのFuncSpace::nameを損失のある UTF-8 変換でパスから導出するのではなく、明示的なname: Option<String>を受け取ります。
これを使うべきとき
次に当てはまる場合はパースシームを使用してください:
- 開いているバッファごとに
tree_sitter::Treeをすでに保持しており(エディタ、LSP、言語サーバー、カスタム静的解析パイプライン)、バイトベースのコストを再度払わずにそのパースをメトリクスに再利用したい。 - 一つのパース結果に対して複数のパス(メトリクス + AST ダンプ + カスタム分析)を実行したい。
- このライブラリとは別の依存関係を取らずに、自分の側で
tree-sitterをピン留めしたい。再エクスポートされたbig_code_analysis::tree_sitterモジュールはリンク対象と同一のクレートなので、型は定義上一致します。
ツリーをまだ持っていない場合は、バイトベースのエントリポイント analyze(Source を渡す)を使用してください。内部でパーサーを構築し、パースを最初から最後まで管理します。
動作する例
use big_code_analysis::{analyze, tree_sitter, Ast, LANG, MetricsOptions, Source}; let source_code = "fn main() { if true { 1 } else { 2 }; }"; let source = source_code.as_bytes().to_vec(); // ステップ 1: *再エクスポートされた* tree-sitter クレートでツリーを構築します。 // (自分の側で直接 `tree-sitter` に依存するのではなく) // `big_code_analysis::tree_sitter` を使うことで、メトリクスウォーカーの // コンパイル時と同じバージョンであることが保証されます。 let mut parser = tree_sitter::Parser::new(); parser .set_language( &LANG::Rust.tree_sitter_language().expect("rust feature enabled"), ) .expect("rust grammar pinned to a compatible version"); let tree = parser .parse(&source, None) .expect("parser has a language set"); // ステップ 2: 明示的な表示名を付けてツリーを取り込みます。 let from_tree = Ast::from_tree_sitter( LANG::Rust, tree, source.clone(), Some("foo.rs".to_owned()), ) .expect("rust feature enabled") .metrics(MetricsOptions::default()) .expect("non-empty input"); // ステップ 3(任意): 値がバイトベースの経路と一致することを確認します。 let from_bytes = analyze( Source::new(LANG::Rust, &source).with_name(Some("foo.rs".to_owned())), MetricsOptions::default(), ) .expect("non-empty input"); assert_eq!( from_tree.metrics.cyclomatic.cyclomatic_sum(), from_bytes.metrics.cyclomatic.cyclomatic_sum(), );
同じ形はどの LANG バリアントでも機能します。対応する文法を(LANG::tree_sitter_language 経由で)tree_sitter::Parser::set_language に渡せば、メトリクスウォーカーはバイト列から生成した場合と同じ FuncSpace を生成します。
唯一のツリー取り込みシーム
Ast::from_tree_sitter がツリー再利用の *唯一の_ エントリポイントです。実行時に LANG でディスパッチし、パーサーの配管を完全に隠蔽します。かつての低レベル経路(ジェネリックな Parser<T> / ParserTrait と言語ごとの *Parser / *Code タグ型)は現在クレート内限定(pub(crate))であり、公開サーフェスには含まれません。STABILITY.md を参照してください。ライブラリ利用者は Ast::from_tree_sitter を通じてツリーを取り込むべきです。この経路は言語ごとのタグ型やトレイト境界を一切露出しません。
対象外
- インクリメンタルな再計算。
tree_sitter::InputEditを適用して変更されたスパンだけを再クエリすることはまだサポートされていません。メトリクスウォーカーは呼び出しのたびにツリー全体を走査します。パースシームは第一歩であり、ウォーカー自体のインクリメンタル化は今後の課題です。 Nodeのpub(crate)走査メソッドすべての公開。Nodeはアドホックな走査のために内部のtree_sitter::Nodeを公開フィールド.0として露出したままですが、ラッパーヘルパーはクレート内限定のままです。
一度のパースで複数回メトリクスを実行
big-code-analysis のワンショットエントリポイント analyze は、呼び出しのたびに Source を再パースします。異なるメトリクスサブセット、交互に行うカスタム tree-sitter 走査、設定変更後のメトリクス再実行など、ファイルを複数回スコアリングするパイプラインでは、その再パースは無駄な作業です。
1.1.0 で追加された Ast 型がこのシームを公開します。ソースを一度パースし、保持したパース結果に対して Ast::metrics を必要な回数だけ呼び出せます。
これを使うべきとき
次のいずれかに当てはまる場合は Ast を使用してください:
- 選択的なメトリクス実行。 同じファイルに対して、レポート用にあるメトリクスの集合を計算し、次に CI のしきい値ゲート用に別の集合を計算する。
- カスタム tree-sitter 走査。 クエリ / ハイライト / シンボル抽出のためにすでに
tree_sitter::Treeを駆動しており、同じパースにメトリクスウォーカーを組み込みたい。 - キャッシュされた解析。 パース済みファイルをメモリに保持する LSP のようなサービスは、設定が変わったときに、バイト列まで戻ることなくオンデマンドでメトリクスを再計算できるべきです。
ファイルごとにすべてのメトリクスを 1 回だけ計算するのであれば、analyze を使い続けてください。現在は内部で Ast に委譲しているため両者の形状は一致しますが、ワンショット API のほうがシンプルなままです。
呼び出しをまたいだ選択的メトリクス
#![allow(unused)] fn main() { use big_code_analysis::{Ast, LANG, Metric, MetricsOptions, Source}; let source = b"fn f(x: i32) -> i32 { if x > 0 { 1 } else { -1 } }"; // 1 回のパースで、2 つのメトリクスサブセットを計算します。 let ast = Ast::parse(Source::new(LANG::Rust, source)) .expect("rust feature enabled"); let loc = ast .metrics(MetricsOptions::default().with_only(&[Metric::Loc])) .expect("walker succeeds"); let cyclomatic = ast .metrics(MetricsOptions::default().with_only(&[Metric::Cyclomatic])) .expect("walker succeeds"); println!("ploc = {}", loc.metrics.loc.ploc()); println!("ccn = {}", cyclomatic.metrics.cyclomatic.cyclomatic_sum()); }
各 metrics 呼び出しはツリーを 1 回走査します。analyze を 2 回呼ぶ場合と比べた節約分はパースの省略によるものです。パースは、ごく大きなソースファイルを除くあらゆるケースで実行時間の大半を占めます。
同じパース上でのカスタム tree-sitter 走査とメトリクス
Ast::as_tree_sitter は基盤となる tree_sitter::Tree を借用します。返される参照は Ast のライフタイムの間有効で、そこから得られるノードは Ast::source に対して解決されます(マクロ展開時に source が何を返すかについては、下記の C++ プリプロセッサに関する注記 を参照してください)。
現実的な AST 作業(ノード種別のカウント、名前による構文要素の検索、パースエラーの検出、シンボルテーブルの構築など)については、AST を直接走査する を参照してください。以下の例は最小限のスモークテストです。専用の章では完全なパターン(再利用可能な深さ優先ウォーカー、フィールド名による検索、エラー検出)を示します。
#![allow(unused)] fn main() { use big_code_analysis::{Ast, LANG, MetricsOptions, Source}; let ast = Ast::parse(Source::new(LANG::Rust, b"fn f() {}")) .expect("rust feature enabled"); // 独自の目的でツリーを走査します… let root = ast.as_tree_sitter().root_node(); assert_eq!(root.kind(), "source_file"); // …そして同じパースに対してメトリクスウォーカーを実行します。 let space = ast .metrics(MetricsOptions::default()) .expect("walker succeeds"); println!("name = {:?}", space.name); }
同じパース上のオペレーターとオペランド
Ast::ops は、スペースごとのオペレーター/オペランドの Ops ツリー(Halstead メトリクスの背後にあるデータ)を返します。トップレベルの Ops::name は、損失のあるパス文字列ではなく、呼び出し側が渡した Source::name が(None の場合も含めて)明示的にそのまま引き継がれたものです。そのため、この経路で Ops::name_was_lossy が設定されることはありません。
#![allow(unused)] fn main() { use big_code_analysis::{Ast, LANG, Source}; let ops = Ast::parse( Source::new(LANG::Rust, b"fn f() { let x = 1 + 2; }") .with_name(Some("snippet.rs".to_owned())), ) .expect("rust feature enabled") .ops() .expect("walker succeeds"); assert_eq!(ops.name.as_deref(), Some("snippet.rs")); assert!(ops.operators.iter().any(|op| op == "+")); }
呼び出し側で構築したツリーの取り込み
すでに自分で tree_sitter::Tree を構築している場合(たとえばエディタや LSP が独自のパーサープールを持っている場合)は、Ast::from_tree_sitter がツリー取り込みの継ぎ目になります。損失のある UTF-8 変換でパスから名前を導出するのではなく、明示的な name: Option<String> をエンドツーエンドで引き継ぎます。
#![allow(unused)] fn main() { use big_code_analysis::{Ast, LANG, MetricsOptions, tree_sitter}; let source = b"fn f() {}".to_vec(); let mut parser = tree_sitter::Parser::new(); parser .set_language( &LANG::Rust .tree_sitter_language() .expect("rust feature enabled"), ) .expect("rust grammar compatible"); let tree = parser .parse(&source, None) .expect("parser has a language set"); let ast = Ast::from_tree_sitter(LANG::Rust, tree, source, None) .expect("rust feature enabled"); let _ = ast.metrics(MetricsOptions::default()).expect("walker succeeds"); }
ツリーは、lang に対して LANG::tree_sitter_language が返す文法を使って code から生成されたものでなければなりません。不一致は unsafe ではありませんが、メトリクスウォーカーはその言語の enum に由来する tree-sitter の kind_id 値でマッチするため、別の文法から来た値は意味のない結果をもたらします。
C++ プリプロセッサ
プリプロセッサ入力(Source::with_preproc_path + Source::with_preproc)を持つ Source に対して Ast::parse が呼ばれ、言語が LANG::Cpp の場合、tree-sitter より前にマクロの事前パスが実行されます。そして Ast::source は、元の入力ではなく、パーサーが実際に見た 展開後 のバイト列を返します。
Ast::from_tree_sitter は影響を受けません。呼び出し側が構築したツリーをそのまま取り込みます。ツリーを構築する前に呼び出し側が適用した展開(あるいは展開していないこと)が、そのまま Ast::source に反映されます。
並行性
Ast は Send + Sync です。同じ &Ast に対して複数スレッドから Ast::metrics を実行しても安全です。ウォーカーは保持している tree_sitter::Tree から読み取るだけだからです。(並列メトリクス実行のベンチマークは別途のフォローアップです。)
対象外
tree_sitter::InputEditによる増分再パース。 解析パイプライン全体で安定したAstをキャッシュすることはスコープ内ですが、保持しているツリーを編集することはスコープ外です。- デフォルトで並列な API。
Ast::metricsはメトリクス集合をまたいで内部的に並列化しません。サブセットごとに 1 スレッドを使いたい呼び出し側は、自由にそうできます。
AST を直接たどる
Ast::parse は、パース済みの tree_sitter::Tree を、そのパース元となったソースのバイト列とともに提供します。Ast::as_tree_sitter は、そのツリーを借用参照として渡します。本章では、これを使って独自の構文木解析(ノード種別のカウント、名前による構文要素の検索、パースエラーの検出、シンボルテーブルの抽出など)を、2 回目のパースのコストを払わずに行う方法を示します。
これを使うべきとき
次のような場合には、直接の AST 走査を選んでください。
- プロセス内で構文要素をカウントまたは検索したい場合。CLI での同等機能(
bca count -t <kind>、bca find -t <kind>、レシピ)はファイルごとに外部プロセスを起動しますが、ライブラリ経由なら 1 回のパースと 1 つの Rust ループで済みます。 - パースエラーをプログラムから検出したい場合。tree-sitter は、文法がマッチできなかった箇所に合成の
ERRORノードを出力します。Node::has_errorは O(1) であり(tree-sitter はすべてのノードにエラービットをキャッシュします)、数 MB のソースファイルでもチェックのコストは実質ゼロです。 - 1 回のパースでメトリクスとカスタム解析を組み合わせたい場合。たとえば、カバレッジマッピング、IDE のアウトライン、コードオーナーレポートのために、メトリクス値 と 関数名のリストを同時に取得するケースです。
標準のメトリクスだけが必要なら、analyze または Ast::metrics を使い続けてください。これらはツリーの走査を代行してくれます。直接走査は、メトリクスウォーカーがまだ計算していないものを求める場合のための手段です。
再エクスポートされた tree_sitter を使う
別途 tree-sitter 依存を追加するのではなく、big_code_analysis::tree_sitter から tree_sitter をインポートしてください。この再エクスポートはメトリクスウォーカーのビルドに使われた正確なバージョンに固定されているため、Tree 型は定義上一致します。この再エクスポートが持つ「値は安定ではない」という姿勢については、既存の tree-sitter Tree を再利用する と 安定性とバージョニング を参照してください。
再利用可能な DFS ウォーカー
以下の例の多くは、すべての子孫を深さ優先で走査する必要があります。tree-sitter には、これを 1 ステップあたり O(1) で行う TreeCursor が付属しています(カーソル自体以外のアロケーションはありません)。標準的な走査はインラインで書けるほど短いものです。
#![allow(unused)] fn main() { use big_code_analysis::tree_sitter; /// `tree` のすべてのノードをルートから先行順(pre-order)で訪問し、 /// 各ノードを `visit` に渡します。カーソル自体を除きアロケーションはありません。 fn walk_preorder<F: FnMut(tree_sitter::Node<'_>)>( tree: &tree_sitter::Tree, mut visit: F, ) { let mut cursor = tree.walk(); 'walk: loop { visit(cursor.node()); if cursor.goto_first_child() { continue; } loop { if cursor.goto_next_sibling() { continue 'walk; } if !cursor.goto_parent() { return; } } } } }
The pattern is: visit, descend, climb back up while there is no next sibling, repeat. Every example in this chapter is a thin wrapper around this walker — the code fences below are marked ignore because they assume walk_preorder is already in scope; the matching set of tests in tests/api/book_ast_traversal_examples.rs keeps them honest, so a refactor that broke an example would fail cargo test.
種別ごとにノードをカウントする
AST クエリのレシピ にある bca count -t if_expression -t for_expression -t while_expression のライブラリ版です。
use big_code_analysis::{Ast, LANG, Source};
use std::collections::HashMap;
let ast = Ast::parse(Source::new(
LANG::Rust,
b"fn a() { if true { 1 } else { 2 } } fn b() { for _ in 0..10 {} }",
))
.expect("rust feature enabled");
let mut counts: HashMap<&str, usize> = HashMap::new();
walk_preorder(ast.as_tree_sitter(), |node| {
*counts.entry(node.kind()).or_default() += 1;
});
assert_eq!(counts.get("if_expression").copied().unwrap_or(0), 1);
assert_eq!(counts.get("for_expression").copied().unwrap_or(0), 1);
文字列キー("if_expression"、"for_expression" など)は、tree-sitter 文法のノード型名です。新しい言語でこれらを調べる最も速い方法は、AST 全体を出力する bca dump --paths sample.rs です。
匿名トークン。 ウォーカーは、
"{"や";"、キーワードリテラルのような匿名トークンも含め、tree-sitter が出力するすべてのノードを訪問します。上記のような対象を絞ったcounts.get("if_expression")の検索は影響を受けませんが(匿名トークンは異なる種別名を持ちます)、counts.values().sum()は 名前付き の文法プロダクションの数よりはるかに大きくなります。名前付きノードだけが必要な場合は、ビジター内でtree_sitter::Node::is_named()を使ってフィルタしてください。
種別ごとにノードを検索する
bca find -t unsafe_block のライブラリ版です。
use big_code_analysis::{Ast, LANG, Source};
let ast = Ast::parse(Source::new(
LANG::Rust,
b"fn safe() {} fn risky() { unsafe { } }",
))
.expect("rust feature enabled");
let source = ast.source();
// キャプチャしたスライスは `source` から借用します。ヒットごとの `String` アロケーションはありません。
let mut hits: Vec<((usize, usize), &str)> = Vec::new();
walk_preorder(ast.as_tree_sitter(), |node| {
if node.kind() == "unsafe_block" {
let span = (node.start_position().row, node.end_position().row);
let text = node
.utf8_text(source)
.expect("source is valid utf-8");
hits.push((span, text));
}
});
assert_eq!(hits.len(), 1);
Node::utf8_text(&source[..]) は、ノードのバイト範囲でソースのバイト列をスライスします。Ast::source と組み合わせて使ってください。プリプロセッサ入力を Ast::parse に渡した C++ の場合、source は元の入力ではなく、パーサーが実際に見た 展開後 のバッファです(C++ プリプロセッサに関する注記 を参照)。
パースエラーを検出する
tree-sitter はロスレスです。不正な入力に対してもツリーを返しますが、マッチできなかったノードにはエラーのタグが付きます。最も安価なチェックはルートに対するものです。
#![allow(unused)] fn main() { use big_code_analysis::{Ast, LANG, Source}; let ast = Ast::parse(Source::new(LANG::Rust, b"fn broken(")) .expect("rust feature enabled"); // 何かが失敗したことを確認できるところまでは走査しますが、すべての // エラー箇所を列挙するわけではありません。 assert!(ast.as_tree_sitter().root_node().has_error()); }
問題のノードを列挙するには、ツリーを走査して各ノードをチェックします。
use big_code_analysis::{Ast, LANG, Source};
let ast = Ast::parse(Source::new(LANG::Rust, b"fn broken("))
.expect("rust feature enabled");
let mut error_lines = Vec::new();
walk_preorder(ast.as_tree_sitter(), |node| {
if node.is_error() || node.is_missing() {
error_lines.push(node.start_position().row);
}
});
assert!(!error_lines.is_empty());
Node::is_error() は、tree-sitter が文法にマッチできなかった箇所に挿入する合成の ERROR ノードを示します。Node::is_missing() は、欠落したトークンから回復するためにパーサーが作り出したファントムノードを示します。CLI の bca find -t ERROR レシピも同じノードを使っています。
メトリクスとカスタム走査を組み合わせる
Ast の眼目は「1 回パースして何度も計算する」ことにあります。現実的なパイプラインは、同じパースからメトリクスを計算し、さらに シンボルテーブルも抽出します。
use big_code_analysis::{Ast, LANG, MetricsOptions, Source};
let ast = Ast::parse(Source::new(
LANG::Rust,
b"fn outer() { fn inner() {} } fn alone() {}",
))
.expect("rust feature enabled");
// 1 回のパース。メトリクスウォーカーがそれを使い…
let space = ast
.metrics(MetricsOptions::default())
.expect("walker succeeds");
// …カスタム走査もまったく同じツリーに対して行われます。キャプチャ
// した名前は、関数ごとに新しい `String` をアロケートするのではなく
// `source` から借用します。上記の `find_unsafe_blocks` と同じ
// パターンです。
let source = ast.source();
let mut functions: Vec<&str> = Vec::new();
walk_preorder(ast.as_tree_sitter(), |node| {
if node.kind() == "function_item"
&& let Some(name_node) = node.child_by_field_name("name")
{
let name = name_node
.utf8_text(source)
.expect("source is valid utf-8");
functions.push(name);
}
});
assert_eq!(space.metrics.nom.functions_sum(), 3);
assert_eq!(functions, ["outer", "inner", "alone"]);
Node::child_by_field_name は名前付きの文法フィールドをたどります。これは、シリアライズされた AST(REST の /ast、Ast::dump)の field_name キーに現れるのと同じフィールドです。フィールドベースの検索は、匿名トークン(カンマ、丸括弧など)に対して文法がどの子ノードを出力するかに依存しないため、位置ベースのインデックス参照より堅牢です。
シリアライズ可能な JSON ツリーが欲しい場合
構造化された AST をデータとして扱いたいパイプライン(差分、ワイヤ越しのクエリ、言語非依存のスキーマ作業など)のために、Ast::dump はツリーを、AstNode からなる Serialize 可能な AstResponse として実体化します。これは REST の /ast エンドポイントが生成するのと同じ形状です。パースハンドルに対して呼び出します。
#![allow(unused)] fn main() { use big_code_analysis::{Ast, AstCfg, AstPayload, LANG, Source}; let payload = AstPayload { id: "snippet".to_owned(), file_name: "snippet.rs".to_owned(), code: "fn f() {}".to_owned(), comment: false, span: true, }; let cfg = AstCfg { id: payload.id.clone(), language: "rust".to_owned(), comment: payload.comment, span: payload.span, }; let response = Ast::parse( Source::new(LANG::Rust, payload.code.as_bytes()) .with_name(Some(payload.file_name.clone())), ) .expect("rust feature enabled") .dump(cfg); let json = serde_json::to_string(&response).expect("AstResponse serializes"); println!("{json}"); }
一度きりのプロセス内作業には、上記の as_tree_sitter() ウォーカーのほうが安価です(ノードごとのアロケーションがありません)。シリアライズ可能な所有ツリーが必要なときに Ast::dump を使ってください。
対象外
- 増分再パース — tree-sitter は増分更新のための
tree_sitter::InputEditをサポートしていますが、Astはスナップショットです。ソースの編集を反映するには、新しくAst::parseを実行するか、再エクスポートされたtree_sitterを介してtree_sitter::Parser::parse(&new_source, Some(&old_tree))を直接実行し、その結果をAst::from_tree_sitterに渡してください。 - クレート内部の
big_code_analysis::Nodeラッパー。 これはメトリクスウォーカーの走査ニーズのために公開されていますが、その走査メソッドの大半(kind、child_count、children、cursorなど)はpub(crate)のままです。ライブラリ利用者はas_tree_sitter().root_node()を通じて tree-sitter のNodeにアクセスしてください。それが文書化された継ぎ目です。
メトリクスの選択
デフォルトでは、analyze のすべての呼び出しが完全なメトリクススイート(ABC、cognitive、cyclomatic、Halstead、LoC、MI、NArgs、NExits、NOM、NPA、NPM、tokens、WMC)を計算します。ユーザーが単に「メトリクス」を求めた CLI にとっては正しいデフォルトですが、ファイルごとに 1 つの数値だけが欲しい呼び出し側には重量級です。
MetricsOptions::with_only(&[Metric]) を使うと、ウォーカーをメトリクスのサブセットに制限できます。選択されなかったメトリクスはノード単位でスキップされ(T::Halstead::compute も T::Cognitive::compute なども実行されません)、CodeMetrics のシリアライズ出力からも省略されます。
具体例
LoC だけを計算し、その結果を読み取ります。
use big_code_analysis::{analyze, LANG, Metric, MetricsOptions, Source}; fn main() { let source = b"fn f(x: i32) -> i32 { if x > 0 { 1 } else { 0 } }"; let opts = MetricsOptions::default().with_only(&[Metric::Loc]); let space = analyze( Source::new(LANG::Rust, source).with_name(Some("snippet.rs".to_owned())), opts, ) .expect("parses"); // LoC は選択されているため、実際の数値を持ちます。 println!("ploc = {}", space.metrics.loc.ploc()); // Halstead、cognitive、cyclomatic などはスキップされました。 // それらの `Stats` フィールドは `Default` のままで、JSON 出力からは省略されます。 let json = serde_json::to_string_pretty(&space.metrics).unwrap(); println!("{json}"); }
この呼び出しの JSON 出力に含まれるのは loc オブジェクトだけで、その他のメトリクスは存在しません。
メトリクス間の依存関係
2 つのメトリクスは 派生 メトリクスです。ファイナライズの段階で、他のメトリクスの出力を利用します。
| メトリクス | 依存関係 |
|---|---|
Metric::Mi | Loc, Cyclomatic, Halstead |
Metric::Wmc | Cyclomatic, Nom |
with_only はこれらの依存関係の閉包を暗黙のうちに解決します。Mi だけを指定しても Loc + Cyclomatic + Halstead が計算されるため、MI の値はゼロデフォルト入力の関数ではなく、意味のある値になります。
#![allow(unused)] fn main() { use big_code_analysis::{Metric, MetricSet, MetricsOptions}; let opts = MetricsOptions::default().with_only(&[Metric::Mi]); // opts.metrics には Mi + Loc + Cyclomatic + Halstead が含まれるようになります。 }
最終的な選択集合は、得られた FuncSpace から space.metrics.selected() を介して確認できます。
#![allow(unused)] fn main() { use big_code_analysis::{analyze, LANG, Metric, MetricsOptions, Source}; let space = analyze( Source::new(LANG::Rust, b"fn f() {}"), MetricsOptions::default().with_only(&[Metric::Mi]), ).unwrap(); let sel = space.metrics.selected(); assert!(sel.contains(Metric::Mi)); assert!(sel.contains(Metric::Loc)); // 自動的に追加された依存メトリクス }
デフォルトの動作は変わりません
MetricsOptions::default() はすべてのメトリクスを選択します。with_only なしで analyze(または Ast::metrics)を呼び出すと、従来とバイト単位で同一の JSON が生成されます。
「X 以外のすべて」はどうするか
補集合を指定する組み込み API はありません。with_only が受け取るのは除外リストではなく、選択対象の明示的なリストです。この意図的な非対称性が、依存関係の閉包を曖昧さのないものに保ちます。正のリストは常に Metric::dependencies を通じて拡張されますが、除外リストの場合、残したメトリクスの依存先を呼び出し側が除外したときにどうするかを決めなければならなくなります。
本当に「Halstead 以外のすべて」が欲しい場合は、リストを明示的に構築してください。Metric は #[non_exhaustive] なので、下流のクレートはバリアントを構築できますが、網羅的に match することはできません。そのため慣習的なパターンは、必要なバリアントを列挙し、将来 Metric バリアントが追加されても自動的には含まれないことを受け入れる、というものです。
#![allow(unused)] fn main() { use big_code_analysis::{Metric, MetricsOptions}; let opts = MetricsOptions::default().with_only(&[ Metric::Cognitive, Metric::Cyclomatic, Metric::Loc, Metric::Nom, Metric::Tokens, Metric::Nargs, Metric::Nexits, Metric::Abc, Metric::Npm, Metric::Npa, Metric::Wmc, // Metric::Mi は意図的に省略しています。依存関係の閉包を通じて // Halstead を再び引き込んでしまうためです。 ]); }
落とし穴に注意してください。Metric::Mi を残すと、Metric::dependencies を通じて Metric::Halstead が再び追加されます。本当に Halstead を外すには、Mi も外す必要があります。
with_only を使うべき場面
- ファイルごとに 1〜2 個のメトリクスしか必要としないホットパス。特に Halstead はスペースごとの
HalsteadMapsアロケーションを独自に持っており、LoC のみの実行における最大の節約対象です。 - 1 つの数値だけを表示し(たとえば認知的複雑度のゲート)、
CodeMetricsの残りをキャッシュされる JSON ペイロードから取り除きたい CI 統合。 big-code-analysisを自前のレポートに組み込むライブラリ呼び出し側。この機能がなければ、自分の UI にすべてのメトリクスのフィールドが表示されてしまいます。
メトリクス単位の Cargo フィーチャ(コンパイル時の除去)は、このノブの対象外です。
言語ごとの Cargo フィーチャ
このライブラリが同梱する tree-sitter 文法は、どれも専用の Cargo フィーチャでゲートされています。デフォルトのフィーチャセットは all-languages なので、次のデフォルト指定
[dependencies]
big-code-analysis = "2.0.0"
は、すべての文法を取り込みます。これはライブラリの従来の動作、および bca / bca-web バイナリ自体が同梱するものと一致します。そのコストは具体的です。ライブラリのコンパイル時にはすべての文法クレートがコンパイルされ、最終バイナリにはすべての文法のパーステーブルが残り続けます。
一部の言語だけが必要なライブラリ利用者は、デフォルトをオプトアウトして、必要な文法だけを再度有効にできます。
具体例
Rust と TypeScript だけを解析する下流のサービスの例です。
[dependencies]
big-code-analysis = { version = "2.0.0", default-features = false, features = ["rust", "typescript"] }
この場合でもライブラリはコンパイルでき、LANG enum には引き続きすべてのバリアントがあり、analyze / Ast をはじめとするディスパッチ面は、有効化された言語に対してそのまま機能します。
サポートされるフィーチャ
利用できる言語別フィーチャは次のとおりです。各フィーチャは、対応する文法クレート(および言語別パイプラインが依存するヘルパー文法)を取り込みます。
| フィーチャ | 取り込まれる文法クレート |
|---|---|
bash | tree-sitter-bash |
c | tree-sitter-c(+ c-family-helpers)。.c を担当する専用の C バリアント(#721 で追加) |
c-family-helpers | bca-tree-sitter-ccomment、bca-tree-sitter-preproc — 内部用。c / cpp / mozcpp によって自動的に有効化され、Ccomment / Preproc ヘルパーバリアントをゲートします。直接選択することは想定されていません |
cpp | tree-sitter-cpp(+ c-family-helpers)。Cpp バリアント(#720 以降はアップストリーム文法)。Ccomment / Preproc ヘルパーバリアントも有効化します |
csharp | tree-sitter-c-sharp |
elixir | tree-sitter-elixir |
go | tree-sitter-go |
groovy | dekobon-tree-sitter-groovy |
irules | tree-sitter-irules(F5 iRules、Tcl 方言) |
java | tree-sitter-java |
javascript | tree-sitter-javascript |
kotlin | tree-sitter-kotlin-ng |
lua | tree-sitter-lua |
mozcpp | bca-tree-sitter-mozcpp(+ c-family-helpers)。オプトインの Mozilla/Gecko C++ 方言で Mozcpp バリアント — 拡張子を持たず、名前で選択します |
mozjs | bca-tree-sitter-mozjs |
objc | tree-sitter-objc。Objc バリアント(Objective-C)です。.m を担当し、.mm(Objective-C++)は Cpp のままです |
perl | tree-sitter-perl |
php | tree-sitter-php |
python | tree-sitter-python |
ruby | tree-sitter-ruby |
rust | tree-sitter-rust |
tcl | bca-tree-sitter-tcl |
typescript | tree-sitter-typescript(Typescript と Tsx の両バリアントで使用) |
包括フィーチャ all-languages は、この表のすべてのエントリを有効にします。bca-tree-sitter-* クレートは、アップストリームの Mozilla / コミュニティ文法のツリー内フォークですが、Rust のインポートパスはいずれも tree_sitter_<lang> のままです。リネームの理由と、利用側の呼び出し箇所を変更せずに済ませるワークスペースの package = ... エイリアスの仕組みについては、RELEASING.md を参照してください。
フィーチャが無効なときに起こること
LANG 列挙型は、有効なフィーチャセットに関係なくすべてのバリアントを定義したまま保ちます。フィーチャを無効にしても、列挙型の表面や、ファイル拡張子 / emacs モード検出ヘルパーは一切変わりません。フィーチャが無効な LANG を選択した場合に影響を受けるのは、ディスパッチ経路だけです。
Result を返すすべてのディスパッチエントリポイントは、無効状態を Err(MetricsError::LanguageDisabled(LANG)) として表面化させます。
analyzeAst::parse/Ast::from_tree_sitter(および返されたAstのmetrics/opsメソッド)LANG::tree_sitter_language— アップストリームプロジェクトが返していた素のLanguageではなく、Result<tree_sitter::Language, MetricsError>を返します
呼び出し側は、ディスパッチャを経由せずに、コンパイル時に組み込まれた言語の集合を照会できます。
#![allow(unused)] fn main() { use big_code_analysis::LANG; for lang in LANG::into_enum_iter() { if lang.is_enabled() { println!("{:?} is compiled in", lang); } } }
これは get_language_for_file / guess_language ヘルパーと組み合わせると便利です。これらのヘルパーは、認識された拡張子に対して引き続き任意の LANG バリアントを返すため、ディレクトリを走査する呼び出し側は、現在のビルドで有効になっていない言語のファイルをスキップするとよいでしょう。
安定性
言語ごとのフィーチャ自体も契約の一部です。新しい言語フィーチャの追加はマイナーバンプでの追加的変更ですが、削除はメジャーバンプ(3.0)の破壊的変更です。デフォルトの all-languages は 2.x の間は恒久的であり、デフォルトビルドがカバーするバリアントが 3.0 より前に減ることはありません。この種の変更は、チェンジログで (breaking) として明示されます。
FuncSpace 結果の走査
FuncSpace は、ライブラリが analyze から返すツリーです。トップレベルのノードはファイル全体を表し、その spaces フィールドにはネストされた関数 / クラス / impl / トレイト / 名前空間のスペースが入ります。各ノードは同じ CodeMetrics ペイロードを持つため、どのメトリクスも任意の粒度で参照できます。
FuncSpace の構造
最もよく使うフィールドは次のとおりです。
| フィールド | 型 | 説明 |
|---|---|---|
name | Option<String> | 呼び出し側が指定した識別子(トップレベル)またはシンボル名(ネスト時) |
kind | SpaceKind | Unit, Function, Class, Impl, … |
start_line | usize | 開始行(1 始まり) |
end_line | usize | 終了行(1 始まり) |
spaces | Vec<FuncSpace> | ネストされたスペース |
metrics | CodeMetrics | スペースごとのすべてのメトリクス値 |
suppressed | SuppressionScope | ソース内の抑制マーカー |
SpaceKind は列挙型です。これに対してマッチすることで、関心のあるもの(Function のみ、あるいは「メソッドを持つものすべて」など)に絞り込めます。
再帰的な走査
再帰はツリーの形をそのまま反映します。ここでは、認知的複雑度がしきい値を超えるすべての関数スペースを収集します。
use big_code_analysis::{ analyze, FuncSpace, MetricsOptions, SpaceKind, Source, LANG, }; fn hotspots(space: &FuncSpace, threshold: u64, out: &mut Vec<String>) { if space.kind == SpaceKind::Function && space.metrics.cognitive.cognitive_sum() > threshold && let Some(name) = &space.name { out.push(format!( "{name} (lines {}–{})", space.start_line, space.end_line, )); } for child in &space.spaces { hotspots(child, threshold, out); } } fn main() { let source = b"\ fn easy() { let _ = 1; } fn hard(x: i32) -> i32 { if x > 0 { if x > 10 { 1 } else { 2 } } else { 3 } } "; let space = analyze( Source::new(LANG::Rust, source).with_name(Some("snippet.rs".to_owned())), MetricsOptions::default(), ) .expect("parses"); let mut hits = Vec::new(); hotspots(&space, 2, &mut hits); for hit in hits { println!("{hit}"); } }
反復的な走査
深いツリーには明示的なスタックを使ってください。Rust は末尾呼び出し最適化を行わず、病的な生成コードは際限なくネストし得ます。
#![allow(unused)] fn main() { use big_code_analysis::FuncSpace; fn total_functions(root: &FuncSpace) -> usize { let mut stack = vec![root]; let mut count = 0; while let Some(space) = stack.pop() { if space.kind == big_code_analysis::SpaceKind::Function { count += 1; } stack.extend(space.spaces.iter()); } count } }
メトリクスごとの値の読み取り
CodeMetrics は各メトリクスをそれぞれ独立した Stats 構造体として公開します。各構造体は、整数値のサマリーアクセサに加えて、スペースごとの派生値のアクセサを提供します。いくつかのパターンを示します。
#![allow(unused)] fn main() { use big_code_analysis::FuncSpace; fn summary(space: &FuncSpace) { let m = &space.metrics; println!("cognitive (this space): {}", m.cognitive.cognitive_sum()); println!("cyclomatic (this space): {}", m.cyclomatic.cyclomatic_sum()); println!("# functions in this space: {}", m.nom.functions_sum()); println!("source lines (sloc): {}", m.loc.sloc()); println!("physical lines (ploc): {}", m.loc.ploc()); println!("ABC branches: {}", m.abc.branches()); } }
*_sum アクセサは子スペースをまたいで集計します。m.loc.sloc() のような素のアクセサは、この ノード自体に帰属する値です。フィールドとメソッドの全一覧はメトリクスごとの rustdoc にあります。
走査順序に依存しない
ライブラリは AST(抽象構文木)をソース順に走査しますが、契約上保証されるのは、各スペースがツリーに一度だけ現れることだけです。バージョン間で安定した順序が必要な場合は、走査後に start_line でソートしてください。
#![allow(unused)] fn main() { use big_code_analysis::FuncSpace; fn flatten(space: &FuncSpace, out: &mut Vec<(usize, String)>) { if let Some(name) = &space.name { out.push((space.start_line, name.clone())); } for child in &space.spaces { flatten(child, out); } } fn sorted(space: &FuncSpace) -> Vec<(usize, String)> { let mut v = Vec::new(); flatten(space, &mut v); v.sort_by_key(|&(line, _)| line); v } }
エラー処理
エントリポイント analyze は Result<FuncSpace, MetricsError> を返します。このページでは、各バリアントの意味と、それぞれへの対処方法を説明します。
注意。
MetricsErrorは#[non_exhaustive]です。将来のバリアントに対する前方互換性を保つため、網羅的にマッチする際は必ず_アームを含めてください。
エラーバリアントのパターンマッチ
use big_code_analysis::{analyze, LANG, MetricsError, MetricsOptions, Source}; fn main() { let result = analyze( Source::new(LANG::Rust, b"this is not rust") .with_name(Some("snippet.rs".to_owned())), MetricsOptions::default(), ); match result { Ok(space) => println!("ok: {} lines", space.metrics.loc.sloc()), Err(MetricsError::EmptyRoot) => { eprintln!("walker produced no top-level FuncSpace"); } Err(MetricsError::LanguageDisabled(lang)) => { eprintln!("language {:?} is not enabled in this build", lang); } // `MetricsError` は `#[non_exhaustive]` です。新しいバリアントが追加される可能性があります。 Err(_) => eprintln!("unexpected MetricsError variant"), } }
各バリアントの意味
EmptyRoot— 予約済みで、現在は生成されません。metrics_with_optionsは AST の走査前に必ず合成のトップレベルUnitFuncSpaceをプッシュするため、空の入力・空白のみの入力・コメントのみの入力を含むすべてのパースがOk(FuncSpace { kind: Unit, .. })を返します。このバリアントは、状態スタックが正当に空になり得るような将来のウォーカー変更に備えて残されています。LanguageDisabled(LANG)— 要求されたLANGがこのビルドで有効になっていません。呼び出し側が、言語ごとの Cargo フィーチャが無効なLANGを選択すると、すべてのディスパッチエントリポイントがこのバリアントを生成します。デフォルトのフィーチャセット(all-languages)はすべての文法をコンパイルに含めるため、このバリアントを見るのは、より狭いセット(--no-default-features --features rust,…)にオプトインした後だけです。
MetricsError には ParseHasErrors や NonUtf8Path といったバリアントはありません。#[non_exhaustive] のままであるため、将来の厳格パースモードや厳格識別子モードは、破壊的変更なしにそれらを導入できます。非 UTF-8 のパスはすでに前段で処理されています。推奨エントリポイントの analyze は、呼び出し側が指定する Source::name(Option<String>)を受け取るため、損失のあるパスがそもそも往復されることはありません。
tree-sitter は常に「ノー」と言うわけではない
ほとんどのパースエラーは Err(_) としては 現れません。tree-sitter はエラー回復型のパーサーであり、構文的に壊れた入力に対してもツリーを生成し、問題のある領域を ERROR ノードでマークします。メトリクスの走査は、回復されたツリーの上で何事もなかったかのように数値を計算します。つまり次のことが起こります。
- ゴミを入れれば、数値が出てきます。 C++ のソースを
LANG::Pythonに与えても、たいていはメトリクスが無意味なOk(FuncSpace)が返ります。結果を信頼する前に、正しい言語を選択していること(例えばguess_languageを使って)を確認してください。 - 不完全なファイルにもスコアが付きます。 閉じられていない波かっこを含む、途中で切れたファイルでも
Ok(FuncSpace)が返ります。メトリクスは、意図されたソースではなく回復されたツリーを反映します。
入力が正常にパースされたかどうかを知る必要がある場合は、tree-sitter の AST を自分で走査して ERROR ノードを数えるか(STABILITY.md の Node エスケープハッチを参照)、CLI 側で bca find -t ERROR を使ってください(Nodes ページを参照)。
? による MetricsError の伝播
MetricsError は [std::error::Error] を実装しているため、ボイラープレートなしに任意の Result<_, Box<dyn Error>> チェーンを通して伝播させられます。
#![allow(unused)] fn main() { use std::error::Error; use big_code_analysis::{analyze, FuncSpace, LANG, MetricsOptions, Source}; pub fn run( lang: LANG, source: &[u8], name: Option<String>, ) -> Result<FuncSpace, Box<dyn Error>> { Ok(analyze( Source::new(lang, source).with_name(name), MetricsOptions::default(), )?) } }
プロジェクト固有のエラー型が欲しい場合は、明示的な From 実装を書けば、呼び出し箇所を簡潔に保ちながら、追加のコンテキスト(ファイルパス、言語の推定結果など)を付与できます。
警告はエラーではない
ライブラリは、致命的でない問題(主に不正な形式の bca: 抑制マーカー)について stderr に警告を書き出します。警告は走査を中断させず、Ok を Err に変えることもありません。サーバーやライブラリの内部に組み込んで実行していて警告を捕捉する必要がある場合は、プロセスレベルで stderr をリダイレクトしてください。ライブラリは現在、プログラムから利用できる警告シンクを公開していません。
安定性とバージョニング
big-code-analysis は 2.x 系列(現在は 2.0.0)です。完全な安定性契約は、リポジトリのルートにある STABILITY.md に記載されています。このファイルが唯一の正であり、各リリースでチェンジログとともに更新されます。
ライブラリ利用者向けの要点は次のとおりです。
- パッチ・マイナーバンプをまたぐ形状の安定性。 STABILITY.md § "What is stable in shape" に列挙されたすべての公開型と関数シグネチャは、
2.x系列を通じて維持されます。追加的変更(新しい項目、新しいLANGバリアント、新しいMetricsErrorバリアント、新しい言語フィーチャ)はマイナーバンプで許可されます。破壊的な形状変更は次のメジャーバンプまで保留され、チェンジログ の3.0.0セクションに (breaking) として記載されます。 2.x内での値の安定性は保証されません。 文法ピンのバンプやメトリクス定義のバグ修正は、パッチバンプであっても、任意のファイルの任意のメトリクス値を任意の方向に変動させ得ます。そのようなドリフトはいずれもチェンジログに明記されます。実行間でビット単位の再現性が必要な場合は、正確なバージョンにピン留めしてください(big-code-analysis = "= 2.0.0")。- MSRV は
1.94です。MSRV の引き上げはマイナーバンプ相当のイベントとして扱われ、チェンジログに(breaking) として明記されます — STABILITY.md § MSRV policy を参照してください。 - エスケープハッチ。
Nodeラッパーは.0を通じてtree_sitter::Nodeを公開し、tree_sitterクレートはbig_code_analysis::tree_sitterとして再エクスポートされています。これらの継ぎ目を通じて到達するものは、本プロジェクト自身の SemVer ではなく、ピン留めされたtree-sitterバージョンに従います。依存する前に STABILITY.md § Escape hatches を参照してください。
3.0 に向けて
かつて 2.0 向けに準備されていた破壊的変更は 2.0.0 で出荷済みです。開かれた公開列挙型への #[non_exhaustive] マーカー、シリアライズキーの正規化、整数メトリクスの u64 への移行、言語ディスパッチと文法のデフォルト変更、Python と REST の表面変更、そして 1.0 以降に蓄積したドリフトを織り込んだメトリクス値の一括再ベースライン化です。パス位置引数によるコールバックディスパッチ(action / Callback トレイト)、フリー関数の metrics / metrics_with_options / get_function_spaces / metrics_from_tree / get_ops、およびジェネリックな Parser<T> / ParserTrait の配管も同時に削除されました。現在は analyze と Ast が唯一の解析の継ぎ目であり、Parser と言語ごとのパーサー / タグ型は pub(crate) に降格されています。
積み残しが一つ、次のメジャーに先送りされています。メトリクスごとの Stats 構造体はまだ #[non_exhaustive] ではないため、フィールドの追加は厳密な SemVer の意味では形状の破壊になります。実際には、フィールド追加はマイナーバンプでの追加的変更として扱われ、チェンジログに明記されます。構造体への #[non_exhaustive] の付与は 3.0 ロードマップ に載っており、この例外扱いを廃止できるようにする予定です。
3.0 の予定はありません。2.0 で追加された #[non_exhaustive] マーカーにより、将来の追加(新しい列挙型バリアント、新しいフィールド)のほとんどは非破壊的になるため、依存すべき表面は 2.x です。
Python バインディング
big-code-analysis はファーストパーティの Python バインディング(PyO3 + maturin)を提供しており、Rust ライブラリや bca CLI と同じメトリクスパイプラインを公開します。JSON の形状も、数値のフォーマットも、対応言語も同じです。
import big_code_analysis as bca
result = bca.analyze("src/main.rs")
if result is not None:
print(result["metrics"]["cyclomatic"]["sum"])
バインディングは Rust API と対等です。本書が Rust の関数(big_code_analysis::analyze、FuncSpace、各メトリクスモジュール)を指し示す箇所ではどこでも、Python に一対一の対応物があります。パイプラインに合う方の言語を選んでください — メトリクスは同一です。
Python を選ぶべき場面
- すでにデータパイプラインのスタック(pandas、Jupyter、Airflow、dbt、Polars)の中にいて、CLI をシェル実行することなくメトリクスレコードを
dict/DataFrameの行として取得したい場合。 - SARIF を消費する Python ネイティブのセキュリティツールと統合する場合 — SARIF 出力を参照してください。
- バックエンドが Python 製 Web フレームワーク(FastAPI、Django)であるコード品質ダッシュボードを構築する場合。
コマンドラインからの一回限りの品質レポートだけが必要なら、bca CLI の方がシンプルです — コマンド → メトリクスを参照してください。
長時間稼働する Rust プログラムに解析を組み込むのであれば、Rust ライブラリの方がオーバーヘッドの小さい選択肢です。
本章の内容
- インストール —
pip install、ホイールのマトリクス、ソースからのビルド。 - クイックスタート — 1 つのファイルを解析し、1 つのメトリクスを表示します。
- バッチ処理 —
analyze_batch、AnalysisFailure、ThreadPoolExecutorによる並列化。 - フラットレコード反復 —
flatten_spacesから sqlite / pandas への供給。 - メトリクス選択 —
metrics=キーワード引数、bca.METRIC_NAMES、依存メトリクスの自動取り込み(dependency-pull)の意味論。 - AST 走査 — 保持されたパース結果に対する
Ast、Node、walk()、find()。 - SARIF 出力 —
to_sarifと GitHub Code Scanning へのアップロード。 - 変更履歴(VCS)メトリクス — git 作業ツリーに対する
vcs.rank、vcs.trend、vcs.commit、vcs.score_diff。 - エラー処理 — 例外の全分類と、決して例外を送出しないバッチ契約。
- 非同期パターン —
asyncio.to_threadが正規のレシピです。
各ページの冒頭の例は、big-code-analysis-py/examples/ 配下のインポート可能なファイルからそのまま埋め込まれており、big-code-analysis-py/tests/test_book_examples.py によってエンドツーエンドで実行されます。そのため、主要経路上のキーワード引数のリネームや関数の削除は、ドキュメントを腐らせる前に CI で失敗します。埋め込まれた例の周囲にある短い説明用スニペット(ロギングのレシピ、errno サフィックスの正規表現パース、asyncio のアンチパターン、pandas のワンライナーなど)はインラインであり、意図的にテストで固定されていません — 両者が食い違う場合は、埋め込まれたブロックを正とみなしてください。
インストール
バインディングはホイールのみ(pure-wheel)の Python パッケージとして配布されます。推奨のインストール方法は pip(またはお好みのロックファイルマネージャー — uv、poetry、pdm)です。
pip install big-code-analysis
Python >=3.12 が必要です。コンパイル済み拡張は CPython の安定 abi3 サーフェス(abi3-py312)を使用するため、1 つのホイールで 3.12、3.13、および将来のすべてのマイナーリリースを、バージョンごとのホイールビルドなしにカバーできます。
Wheel マトリクス
CI は現在、以下のターゲット向けの wheel を公開しています。お使いのプラットフォームが記載されていない場合は、ソースからビルドしてください。
| プラットフォーム | アーキテクチャ |
|---|---|
Linux(manylinux_2_28) | x86_64, aarch64 |
wheel マトリクスは .github/workflows/python-wheels.yml で定義されています。manylinux_2_28 には glibc >= 2.28(RHEL 8 / Debian 10 / Ubuntu 18.10 以降)が必要です。それより古いディストリビューション(RHEL 7 / CentOS 7、glibc 2.17)ではソースからビルドする必要があります。macOS と Windows の wheel はまだ提供されていません — これらのプラットフォームでの pip install は現在ソースビルドにフォールバックします。
インストールの確認
python -c "import big_code_analysis as bca; print(bca.__version__)"
表示されるバージョンは、Rust ワークスペースの Cargo.toml にある [workspace.package].version と一致します — バインディングと Rust ライブラリのバージョンは常に同期しています。
ソースからのビルド
お使いのプラットフォームに合う wheel がない場合や、未リリースの Rust コミットに対してバインドしたい場合は、maturin でビルドします。
git clone https://github.com/dekobon/big-code-analysis.git
cd big-code-analysis/big-code-analysis-py
python -m venv .venv && source .venv/bin/activate
pip install --upgrade pip
pip install "maturin>=1.7,<2.0"
maturin develop --release # big_code_analysis を編集可能な形でインストール
python -c "import big_code_analysis as bca; print(bca.__version__)"
maturin develop は Rust 拡張をその場でビルドしてアクティブな venv にインストールするため、import big_code_analysis はローカルで解決されます — 別途 pip install -e . を実行する必要はありません。--release フラグは最適化を有効にします。開発中はこれを省略するとリビルドが高速になります。
このほかに以下が必要です。
- 安定版の Rust ツールチェーン(MSRV:
1.94)。rustup からインストールしてください。 - C コンパイラ(tree-sitter 文法クレートのビルドに使用されます)。
- CPython の開発用ヘッダー(Debian / Ubuntu では
python3-dev)。
次のステップ
クイックスタートに進んで最初のメトリクスを計算してください。多数のファイルを処理するパイプラインに組み込む場合は、バッチ処理まで読み飛ばしても構いません。
クイックスタート
このページでは、単一のソースファイルからメトリクスを計算するために必要な最小限のコードを順に説明します。
1. パッケージをインストールする
pip install big-code-analysis
wheel マトリクスとソースからのビルド手順についてはインストールを参照してください。
2. ファイルを分析する
bca.analyze(path) は、同じファイルに対して bca metrics --format json が出力する JSON と一致する dict を返します — フィールドの順序も、数値の書式も、構造も同じです。
"""Quick-start: analyse one file and print the headline cyclomatic count.
Mirrors the worked example shown on the book's
``python/quick-start.md`` page. The book embeds this file verbatim,
so the snippet is the test fixture — if the API drifts, the
``test_book_examples.py`` test fails and the docs are forced back
into sync.
"""
from __future__ import annotations
from pathlib import Path
import big_code_analysis as bca
from big_code_analysis import FuncSpaceDict
def run(path: Path) -> FuncSpaceDict:
"""Analyse ``path`` and return its metric dict."""
result = bca.analyze(path)
if result is None:
msg = f"{path} was skipped (empty, binary, or generated)"
raise SystemExit(msg)
cyclomatic = result["metrics"]["cyclomatic"]
print(f"{result['name']}: cyclomatic sum = {cyclomatic['sum']:.0f}")
return result
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
sys.exit("usage: python quick_start.py <path>")
run(Path(sys.argv[1]))
注目すべき点をいくつか挙げます。
analyzeは、CLI のウォーカーがスキップするファイルに対してNoneを返します。具体的には、3 バイト以下のファイル(空として扱われます)、先頭部分が有効な UTF-8 でないファイル(バイナリとして扱われます)、そしてデフォルトのskip_generated=Trueの下でウォーカーのis_generated述語に一致するファイル(先頭に@generated、DO NOT EDIT、GENERATED CODEのいずれかのマーカーがあるもの)です。result["metrics"]の中身にアクセスする前に、必ずこのオプショナルな戻り値を処理してください。- 返されるオブジェクトは、実行時にはただの dict です —
json.dumpsでシリアライズしたり、下流のサービスに送ったり、表形式のコンシューマー向けにflatten_spacesに渡したりしても安全です。型チェッカーからは(Rust のワイヤ形状から生成された)FuncSpaceDictという TypedDict として見えるため、ネストしたメトリクスへのアクセスはキャストなしで mypy/pyright の静的チェックを通ります。 - 言語検出は CLI と完全に同じ挙動です。まずパスの拡張子を見て、次にシバン / emacs モード行にフォールバックします。ソースをメモリ上に持っている場合は
bca.analyze_source(code, language)を使ってください。
3. メモリ上のスニペットを分析する
import big_code_analysis as bca
metrics = bca.analyze_source("fn main() {}\n", "rust")
print(metrics["metrics"]["loc"]["sloc"])
analyze_source は str、bytes、bytearray を受け付けます。返される dict は analyze の出力と同じ構造で、name は None になります(メモリ上のバッファにはパスが関連付けられないためです)。
次のステップ
- バッチ処理 — ファイルごとの try/except の煩雑さなしに多数のファイルを処理する
analyze_batch。 - メトリクス選択 — 必要なメトリクスだけを計算します。
- エラー処理 — 例外の完全な分類。
- CLI の Metrics コマンドは、これに相当するシェルレベルのワークフローです。
バッチ処理
bca.analyze_batch(paths) は、イテラブル内のすべてのパスに対して bca.analyze と同じ分析を実行し、ファイル単位のエラーで例外を送出することはありません。各結果要素は、分析結果の dict か、失敗内容を記述する bca.AnalysisFailure のいずれかです。結果は入力順を保持するため、どのパスもスキップされない限り zip(inputs, results) はインデックスどおりに対応します。analyze_batch は analyze と同じキーワード専用オプション — exclude_tests、allow_lossy_path、skip_generated(デフォルト True)、metrics — を共有しており、2 つのエントリポイントは同じ挙動を保ちます。
def run(paths: Iterable[Path]) -> dict[str, int]:
"""Analyse ``paths`` as a batch and bucket successes vs failures.
Returns a small summary dict (`ok`, `errors`, `total`) so the
accompanying test can assert on it without re-parsing.
"""
materialised = list(paths)
# `skip_generated=False` は入力ごとに 1 つの結果要素を保証します
# (生成ファイルは除外されず分析されます)。そのため `materialised`
# に対する `strict=True` の zip が `ValueError` を送出することは
# ありません。2.0 のデフォルト(`skip_generated=True`)では生成
# ファイルの入力はスロットを生まず、長さがずれて strict な zip が
# 失敗します — `pipeline_db.py` で修正されたバグ #660 と同じです。
results = bca.analyze_batch(materialised, skip_generated=False)
ok = 0
errors = 0
for path, result in zip(materialised, results, strict=True):
if isinstance(result, bca.AnalysisFailure):
errors += 1
print(f" skip {path}: ({result.error_kind}) {result.error}")
else:
ok += 1
sloc = result["metrics"]["loc"]["sloc"]
print(f" ok {path}: sloc = {sloc:.0f}")
return {"ok": ok, "errors": errors, "total": len(materialised)}
重要な契約をいくつか挙げます。
AnalysisFailureは例外として送出されるのではなく、「返されます」。Exceptionのサブクラスではないため、isinstance(slot, bca.AnalysisFailure)が判別手段になります。pathsは遅延評価で消費されるため、ジェネレーターも使えます — ただしzipのために入力を保持したい場合は、先にリストへ実体化してください。- デフォルトの
skip_generated=Trueでは、生成ファイルは スキップ され、要素を 一切 生成しません。そのため結果リストは入力より短くなることがあります — これは、生成ファイルに対してNoneを返す単一ファイル版analyzeと正確に一致する挙動です。入力ごとに 1 要素を保証したい場合はskip_generated=Falseを渡してください(2.0 より前のデフォルト)。このデフォルトが 2.0 で反転したのは、analyzeとanalyze_batchを切り替えても生成ファイルの扱いが暗黙に変わらないようにするためです。
ディレクトリの走査:analyze_paths
analyze_batch は、明示的なリスト で渡されたパスをそのまま分析します。まずソースファイルを 見つける ところから始めたい場合 —「リポジトリを丸ごと分析したい」場合 — は、CLI の gitignore 対応ウォーカーを再利用する analyze_paths(#658)を使ってください。
import big_code_analysis as bca
results = bca.analyze_paths("path/to/repo", include="*.py")
各位置引数のシードにはファイルまたはディレクトリを指定できます。ディレクトリは .gitignore、include / exclude グロブ(単一のグロブ文字列またはシーケンス。先頭の ./ は省略可能で、dir/** ≡ ./dir/**)、および生成ファイルフィルターを尊重して走査されます。ファイルを直接指すシードは exclude に関係なく常に分析されます — 明示的な指定は ignore 系のルールに優先します — が、include はベース名による絞り込みとして引き続き適用されます。respect_gitignore=False を指定すると、ignore されたファイルも走査対象になります。結果は analyze_batch と同じ list[FuncSpaceDict | AnalysisFailure] の形と、例外を送出しない契約に従い、同じ exclude_tests / allow_lossy_path / skip_generated / metrics / vcs / vcs_per_function キーワード引数を転送します。
変更履歴メトリクスの付与
analyze_batch と analyze_paths は、単一ファイル版 analyze と同じ vcs=True / vcs_per_function=True キーワード引数を受け付けます(#670)。バッチは、対象ファイルを含むリポジトリごとに履歴インデックス / blame エンジンを 1 つだけ 構築し、そのリポジトリのファイル間で再利用します — analyze(p, vcs=True) を内包表記で回した場合にファイルごとに繰り返される走査を償却する形です。あるファイルで VCS が失敗しても、その AST メトリクスは無傷のまま残ります(AnalysisFailure になることはありません)。どのリポジトリにも属さないファイルには、単に vcs ブロックが付かないだけです。(ファイル単位の付与ではなく)リポジトリ全体をランキングしたい場合は、専用の big_code_analysis.vcs サーフェスを使ってください。
並列実行
analyze_batch に組み込みの並行処理はありません — 逐次的なスイープです。並列化するには、ファイルごとの analyze 呼び出しをスレッドプールへファンアウトしてください。
def run_parallel(paths: Iterable[Path], *, workers: int = 4) -> list[FuncSpaceDict | None]:
"""Fan ``analyze`` out across a thread pool.
PyO3 releases the GIL across each file's read + parse, so a
thread pool actually parallelises the heavy work. Use this when
you need per-file exceptions instead of ``AnalysisFailure`` slots.
"""
def _analyze(p: Path) -> FuncSpaceDict | None:
return bca.analyze(p)
with ThreadPoolExecutor(max_workers=workers) as pool:
return list(pool.map(_analyze, paths))
PyO3 の Python::detach は、各ファイルの読み込みと tree-sitter によるパースの間 GIL を解放するため、スレッドがインタープリターのロックで直列化されることはありません — ロックを取り合う協調動作ではなく、本当の並列処理です。
AnalysisFailure の分類
error_kind は閉じた Literal です。
error_kind | 発生条件 |
|---|---|
"UnsupportedLanguage" | 未知の拡張子で、シバン / emacs モードにも一致しない |
"ParseError" | tree-sitter がソースを拒否したか、まれな内部シリアライズ失敗(internal: serialization error: …) |
"IoError" | std::fs::read が失敗した、「または」 パスが有効な UTF-8 でなかった |
AnalysisFailure は凍結(frozen)されており、3 つのフィールドすべてに対する __eq__ / __hash__ / __repr__ を実装しているため、呼び出し側はエラーを set に入れて実行間で失敗を重複排除できます。リトライの分類のために、errno は Rust のデフォルト書式によって error 文字列内に保持されます。
import re
match = re.search(r"\(os error (\d+)\)$", slot.error)
errno = int(match.group(1)) if match else None
型付きのディスパッチ(FileNotFoundError、PermissionError など)が必要な場合は、analyze_batch の代わりにファイルごとに bca.analyze(path) を呼んでください — 単一ファイル版 analyze は正規の OSError サブクラスを送出します。エラー処理を参照してください。
フラットレコードの反復処理
bca.flatten_spaces(result) は、ネストした FuncSpace ツリーを行きがけ順(pre-order)に走査し、ノードごとにスカラー値のみのフラットな dict を 1 つずつ生成します — sqlite3.executemany や pandas.DataFrame.from_records をはじめ、あらゆる表形式のコンシューマーにそのまま渡せます。
メトリクスのキーは、CLI の CSV ライターと同じドット区切りの規約(cyclomatic.modified.sum、halstead.volume、loc.lloc_average など)を使います。識別キー(path、name、kind、start_line、end_line、parent_name、depth)はすべてのレコードに付加されます。
executemany による SQLite への書き込み
以下の例は、1 つのファイルを分析し、フラット化された全キーの和集合を列とする sqlite テーブルへ、FuncSpace ごとに 1 行を挿入します。
"""Flatten a FuncSpace tree into scalar rows for sqlite / pandas.
Demonstrates ``bca.flatten_spaces`` + ``sqlite3.executemany``. The
pandas equivalent is shown in the book as a non-executed snippet so
this example stays dependency-free (sqlite ships with the stdlib).
Tied to the book's ``python/flat-records.md`` page.
"""
from __future__ import annotations
import sqlite3
from contextlib import closing
from pathlib import Path
import big_code_analysis as bca
def run(path: Path, db_path: Path) -> int:
"""Analyse ``path`` and insert one row per FuncSpace into ``db_path``.
Returns the number of rows inserted so the test can assert on it.
"""
result = bca.analyze(path)
if result is None:
msg = f"{path} was skipped (looks generated)"
raise SystemExit(msg)
# フラット化されたキーはドット区切りの小文字名
# (`halstead.unique_operators`、`halstead.total_operators` など)で、
# SQLite の大文字小文字を区別しない列比較の下でも一意です(かつての
# `N1`/`n1` の Halstead 衝突は #511 で解消済み)。そのため各キーは
# リネームなしでそれぞれ独自の列に収まります。
records = [dict(r) for r in bca.flatten_spaces(result)]
if not records:
return 0
columns = sorted({k for r in records for k in r})
cols_sql = ", ".join(f'"{c}"' for c in columns)
placeholders = ", ".join("?" for _ in columns)
rows = [tuple(r.get(c) for c in columns) for r in records]
# `closing(sqlite3.connect(...))` がドキュメント化されたイディオムです —
# 素の ``with sqlite3.connect(...)`` コンテキストマネージャーは
# トランザクションのコミット / ロールバックを行うだけで、接続を
# クローズしません。そのため長時間動くコンシューマーはファイル
# ディスクリプターをリークします(さらに Windows では db ファイルの
# 排他的書き込みロックを保持し続けます)。
with closing(sqlite3.connect(db_path)) as db, db:
db.execute(f"CREATE TABLE IF NOT EXISTS metrics ({cols_sql})")
db.executemany(
f"INSERT INTO metrics ({cols_sql}) VALUES ({placeholders})",
rows,
)
return len(rows)
if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
sys.exit("usage: python flat_records.py <source-file> <out.db>")
inserted = run(Path(sys.argv[1]), Path(sys.argv[2]))
print(f"inserted {inserted} rows into {sys.argv[2]}")
このイテレーターは 遅延評価かつ単回使用 です。リスト全体を実体化せずに、入力を 1 回だけ走査します。同じイテレーターを 2 回目に反復しても何も返しません — 再反復が必要な場合は一度 list() を呼んでください。
Pandas
flatten_spaces は pandas.DataFrame.from_records への自然な入力です。Pandas はバインディングの依存関係ではないため、DataFrame ビューが必要な場合は別途インストールしてください。
import big_code_analysis as bca
import pandas as pd
result = bca.analyze("src/lib.rs")
if result is not None:
df = pd.DataFrame.from_records(bca.flatten_spaces(result))
print(df.head())
# スペースの kind でグループ化し、関数・クラス・ファイルそれぞれの
# 循環的複雑度の平均を確認します。
by_kind = df.groupby("kind")["cyclomatic.sum"].mean()
識別列と CLI CSV の比較
フラットレコードのスキーマは CLI の CSV ライターとほぼ揃っていますが、意図的な差分がいくつかあります。
- 識別列は、こちらでは
name/kindを使いますが、CSV ライターはspace_name/space_kindを使います。フラットレコードにはparent_name/depthも追加されますが、CSV ライターにはありません。 tokens.*は JSON の形(tokens.tokens、tokens.average、tokens.min、tokens.max)にフラット化されます。CSV と異なるのは合計のリーフだけで、CSV ではtokens.sumと綴られます。average/min/maxのリーフは現在一致しています(#590)。CSV と完全に揃える必要がある場合は、コンシューマー側で合計のリーフをリネームしてください。
匿名スペース(Rust のクロージャ、JavaScript の関数式やアロー関数)は name == "<anonymous>" マーカーをそのまま保持します — flatten_spaces は正規化を行いません。
注意点
parent_nameだけでは、異なる親の下にネストした同名の兄弟を区別できません(例えば、異なる外側クラスの下にある 2 つのInnerクラスは、どちらも自身の子に対してparent_name == "Inner"として現れます)。完全修飾パスが必要な場合は、depthとソース順の位置を組み合わせるか、コンシューマー側で修飾名を再構築してください。- 反復中に入力の
resultを変更しないでください。ウォーカーは入力への参照を保持しているため、まだ生成されていないサブツリーへの変更は後続のレコードに反映されてしまいます。 - 存在しないメトリクスのサブツリーはキーを生成しません(
Noneではなく欠落)。これはメトリクス選択の「Halstead 無効時」のエッジケースと一致します。 flatten_spacesは、入力がマッピングでない場合にTypeErrorを送出します。呼び出し側は、渡す前にbca.analyzeのNone戻り値(例えばskip_generated=Trueの下での生成ファイル)をフィルタリングする必要があります。
メトリクス選択
メトリクススイートの一部だけを計算するには metrics=[…] を渡します。metrics=None(デフォルト)は「すべて計算する」挙動を維持します。要求されなかったメトリクスは結果の dict から 欠落 します(None のプレースホルダーとして存在するのではありません)。
def run(path: Path) -> FuncSpaceDict:
"""Compute only LoC + cyclomatic for ``path`` and return the result.
``bca.METRIC_NAMES`` is a ``tuple[MetricName, ...]`` of canonical
names accepted by ``metrics=``; its ``StrEnum`` members are
``str``-comparable, so ``"halstead" in bca.METRIC_NAMES`` works — an
ABI smoke check the catalog is populated, not a test of the selection.
"""
if "halstead" not in bca.METRIC_NAMES:
msg = "halstead is missing from METRIC_NAMES — bindings ABI drift"
raise RuntimeError(msg)
selected = bca.analyze(path, metrics=["loc", "cyclomatic"])
if selected is None:
msg = f"{path} was skipped (looks generated)"
raise SystemExit(msg)
metric_keys = sorted(selected["metrics"])
print(f"computed only: {metric_keys}")
return selected
def run_derived(path: Path) -> FuncSpaceDict:
"""Selecting ``mi`` auto-pulls in its three dependencies."""
selected = bca.analyze(path, metrics=["mi"])
if selected is None:
msg = f"{path} was skipped (looks generated)"
raise SystemExit(msg)
pulled = sorted(selected["metrics"])
print(f"mi pulled in: {pulled}")
return selected
同じキーワード引数は bca.analyze_source と bca.analyze_batch でも有効です — 後者はバッチ内のすべてのファイルに選択を一様に適用します。バリデーションはファイル I/O の 「前に」 実行されます。空のリストや未知の名前は即座に ValueError を送出し、実際には呼び出し側のバグであるものに対して AnalysisFailure スロットを返すことはありません。
正規名
全メトリクスの集合は MetricName メンバーのタプルとして利用できます。各メンバーは StrEnum なので、それ自体が str です — "halstead" in bca.METRIC_NAMES は動作し、bca.MetricName.HALSTEAD == "halstead" は True になります。metrics= には素の文字列とメンバーのどちらも渡せます。
import big_code_analysis as bca
from big_code_analysis import MetricName
assert "halstead" in bca.METRIC_NAMES
assert bca.MetricName.HALSTEAD == "halstead"
# どちらの表記でも `metrics=` に渡せます:
selection = [MetricName.CYCLOMATIC, "cognitive"]
これらのメンバーは、CLI と JSON 出力が使うのと同じ Metric テーブルから生成されるため、その値が bca metrics --format json で目にするスラッグから乖離することはありません。
名前は小文字で、大文字小文字を区別します。未知の名前を渡すと、正規の一覧をメッセージに含む ValueError が送出されます。終了点メトリクスの正規表記は、あらゆる場所で "nexits" です(enum の Display、METRIC_NAMES、JSON 出力キー)。レガシーの "exit" エイリアスは 2.0 で廃止され、現在は他の未知の名前と同様に ValueError を送出します。重複は暗黙に畳み込まれます。
| メトリクス | JSON キー | 引き込まれる依存メトリクス |
|---|---|---|
| LoC | loc | — |
| 循環的複雑度 | cyclomatic | — |
| 認知的複雑度 | cognitive | — |
| Halstead | halstead | — |
| ABC | abc | — |
nargs | nargs | — |
nom | nom | — |
npa | npa | — |
npm | npm | — |
nexits | nexits | — |
tokens | tokens | — |
| 保守容易性指数 | mi | loc, cyclomatic, halstead |
| クラスごとの重み付きメソッド数(Weighted Methods per Class) | wmc | cyclomatic, nom |
パフォーマンスのトレードオフ
全メトリクスの計算がデフォルトなのは、それが CLI の動作と同じだからです。単一のメトリクスだけを選択すると、対応する compute パスがスキップされるため確実に速くなりますが、ほとんどの入力では tree-sitter のパースと AST の走査がコストの大半を占めるため、単一ファイルでの節約はわずかです。効果はバッチサイズに応じて大きくなります。analyze_batch を大規模なリポジトリ全体に対して実行する場合、不要かつ最も高価なメトリクス(深い呼び出しツリーでは多くの場合 Halstead)を外すことは測定可能な効果をもたらします。
要求しなかったメトリクスは結果に含まれません。無条件に result["metrics"]["mi"] へアクセスするコードは、mi を外した場合に KeyError になります。if "mi" in result["metrics"] でガードするか、.get("mi") を使ってください。
関連項目
- バッチ処理 —
metrics=はバッチ内のすべてのファイルに一律に適用されます。検証は入力の反復処理が始まる前に一度だけ実行されます。 - SARIF 出力 — しきい値の名前は
metrics=の選択とは独立しています。metrics=["loc"]を要求しつつcyclomaticのしきい値でゲートすることもできますが、外したメトリクスについては SARIF に検出結果が含まれません。 - フラットレコードの反復処理 —
flatten_spacesは、元の dict に存在しないメトリクスのキーを黙って出力しないため、metrics=の選択はフラット化された列を自然に絞り込みます。
AST トラバーサル
bca.analyze(...) は メトリクス を返します。すべての関数定義を見つける、docstring を取り出す、py-tree-sitter のマッチャーを移植するなど、構文木 そのものが必要な場合は、一度だけ Ast にパースし、遅延評価される Node ハンドルで走査してください。
Ast ハンドル
bca.Ast.parse(code, language)(または bca.Ast.from_path(path))はソースを一度だけパースし、メトリクスと構文木の両方を取り出せるハンドルを返します。py-tree-sitter で一度、analyze() でもう一度と、二度パースする必要はありません。
import big_code_analysis as bca
ast = bca.Ast.parse("fn main() { let x = 1 + 2; }", "rust")
ast.metrics() # analyze_source(...) と同じ dict
ast.root_node # 構文木。遅延走査される(後述)
このハンドルはイミュータブルかつスレッドセーフなので、analyze とまったく同様に ThreadPoolExecutor によるファンアウトと組み合わせられます。
Node ハンドル
ast.root_node は木のルートを遅延評価の Node として返します。ノードごとに dict を実体化する ast.dump() と異なり、Node は保持された木へのカーソルです。読み取るまでコストはかからず、選択的な抽出処理は訪問したノードの分だけコストを払います。
root = ast.root_node
root.kind # "source_file"
root.type # "source_file"(kind の py-tree-sitter 互換エイリアス)
root.children # list[Node]、直接の子ノード
root.child_by_field_name("…") # フィールドの子ノード、なければ None
node.text # ノードのソースバイト列
走査 API は py-tree-sitter を踏襲しています。children / named_children、parent、next_sibling / prev_sibling(および *_named_* 系)、child(i) / named_child(i)、child_by_field_name(name) / children_by_field_name(name)、そして親がそのノードに到達する際に経由する field_name が使えます。
サブツリー全体の走査
walk() はノードとその子孫を先行順(pre-order)でたどる遅延イテレータです。descendants_by_kind(kinds) は一回の走査でマッチを収集し、ast.find(filters) は bca count と同じ語彙(function、call、comment、string、正確な kind 名など)を受け付けて木全体を検索します。
# ファイル内のすべての関数名を遅延評価で取得する。
for fn in ast.find(["function_item"]):
name = fn.child_by_field_name("name")
print(name.text.decode())
# あるいは生の文法 kind でサブツリーを絞り込む。
idents = root.descendants_by_kind(["identifier"])
これらには Rust 側の対応物 — Node::preorder と Node::descendants_by_kind — があるため、ライブラリの呼び出し側でも同じヘルパーを利用できます。
座標
ノードは自身の位置をあらゆる語彙で報告するため、手作業での変換は不要です。
| アクセサ | 意味 |
|---|---|
start_byte / end_byte | ast.source へのバイトオフセット |
start_point / end_point | 0 始まりの (row, col)(py-tree-sitter 互換) |
start_line / end_line | 1 始まりの行番号 |
span | dump() が出力する 1 始まりの {start_line, start_col, …, start_byte, end_byte} dict |
したがって node.start_line == node.start_point[0] + 1 であり、ast.source[node.start_byte:node.end_byte] == node.text が成り立ちます。
node.type は node.kind の py-tree-sitter 互換エイリアスです。py-tree-sitter の node.type を前提に書かれたマッチャーはそのまま移植できます。bca における正準の綴りは kind のままです。
遅延ノードと dump() の比較
ast.dump() は木をネストした dict として返し、ast.root_node は遅延ハンドルを返します。両者には重要な違いが 2 つあります。
-
メモリ。
dump()はノードごとに(span、value、childrenを持つ)dict を 1 つ構築します。小さなファイルでは問題ありませんが、大きなファイルではコストがかさみます。Nodeによる走査では、触れたハンドルの分しかアロケーションが発生しません。 -
分類。
Nodeのkindは生の文法 kind です。dump()の kind は bca のAlteratorを通過して整形されます。たとえば文字列リテラルのノードは"string"にリネームされ、フラット化 されます(文法上の子ノードが除去されます)。そのため、変換対象のノードでは 2 つの表面が意図的に食い違います。ast = bca.Ast.parse('fn f() { let s = "hi"; }', "rust") # 生の木では、文字列は引用符や内容の子ノードを保持している。 raw = next(n for n in ast.root_node.walk() if "string" in n.kind) assert raw.children文法が生成したものをそのまま扱いたい場合(py-tree-sitter のマッチャーを移植するならこちらが正解です)は遅延ノードを、bca が整形した JSON シリアライズ可能なビューが欲しい場合は
dump()を使ってください。
ライフタイムとスレッド
Node はその Ast を生かし続けます。パース結果への他の参照をすべて手放しても有効なままなので、Ast をローカルに構築する関数からノード(またはノードのリスト)を返しても安全です。ノードはスレッド間で共有しても安全です。
C/C++ プリプロセッサ。 プリプロセッサ入力付きでパースした
Cppでは、ast.source— したがってすべてのノードのバイトオフセット — は、ディスク上のファイルではなく、パーサーが見た 展開後 のソースを指します。
次のステップ
- メトリクスの選択 — 同じパース結果から必要なメトリクスだけを計算します。
- CLI の
dumpとcountコマンドは、dump()とfind()のシェルレベルの対応物です。
SARIF 出力
bca.to_sarif(result, *, thresholds=None) は、分析結果(またはそのイテラブル)を SARIF 2.1.0 JSON ドキュメントとしてレンダリングし、GitHub Code Scanning をはじめとする任意の SARIF コンシューマへそのままアップロードできる形にします。出力は bca check --report-format sarif を支えるのと同じ Rust ライターが生成するため、スキーマ URL、ツールドライバの名前とバージョン、ルール記述は CLI とバイト単位で一致します。
def run(
paths: Iterable[Path],
sarif_path: Path,
thresholds: Mapping[str, float],
) -> str:
"""``paths`` を分析し、SARIF ドキュメントを ``sarif_path`` に書き込む。
レンダリング済みの SARIF JSON を返すため、呼び出し側(やテスト)は
ファイルを読み直さずに内容を検査できる。
"""
batch = bca.analyze_batch(paths)
sarif = bca.to_sarif(batch, thresholds=dict(thresholds))
sarif_path.parent.mkdir(parents=True, exist_ok=True)
sarif_path.write_text(sarif, encoding="utf-8")
print(f"wrote {sarif_path} ({len(sarif.encode('utf-8'))} bytes)")
return sarif
to_sarif は次を受け付けます。
bca.analyzeまたはbca.analyze_sourceが返す単一のdict。- そのような dict や
bca.AnalysisFailureインスタンスを生成する任意のイテラブル(bca.analyze_batchの戻り値そのままの形)。AnalysisFailureエントリは黙ってスキップされます。これは分析できなかったファイルを表すものであり、検出結果ではないためです。
しきい値
受け付けられるしきい値名は、big-code-analysis-cli/src/thresholds.rs にある CLI の EXTRACTORS テーブルと一致します。
cognitive,cyclomatic,cyclomatic.modifiedhalstead.volume、halstead.difficulty、halstead.effort、halstead.time、halstead.bugsloc.sloc、loc.ploc、loc.lloc、loc.cloc、loc.blanknom,tokens,nexits,nargsmi.original、mi.sei、mi.visual_studioabc,wmc,npm,npa
未知の名前に対しては、受け付け可能な名前の一覧を含む ValueError が送出されるため、タイポは黙って空の SARIF run を生成するのではなく、即座に失敗します。
thresholds=None(デフォルト)と thresholds={} はどちらも、空の results 配列と rules 配列を持つ整形式の SARIF ドキュメントを生成します。これは CLI の方針と一致します。すなわち組み込みのデフォルトしきい値は存在せず、各 check 実行が自前の上限を指定します。
GitHub Code Scanning へのアップロード
# .github/workflows/code-scanning.yml(抜粋)
- name: Compute metric SARIF
run: |
python - <<'PY'
import big_code_analysis as bca
with open("paths.txt", encoding="utf-8") as paths_fh:
results = bca.analyze_batch(paths_fh.read().splitlines())
with open("metrics.sarif", "w", encoding="utf-8") as fh:
fh.write(bca.to_sarif(results, thresholds={"cyclomatic": 15}))
PY
- name: Upload to Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: metrics.sarif
アップロード用アクションのドキュメントは github/codeql-action/upload-sarif にあります。バインディングは呼び出しごとに 1 つの SARIF run を生成し、リポジトリの Code Scanning アラートへのアップロードはこのアクションが担います。
「Unit」の検出結果の意味
to_sarif は、自身の値が上限を超えているすべてのスペース — ファイル単位(Unit)、各コンテナ、各リーフ関数・クロージャ — で検出結果を出力し、bca check --report-format sarif と正確に一致します。ほとんどのメトリクスでは、スペースの JSON 見出し値がそのままそのスペース自身の値です。サブツリー集約型の 4 つのメトリクス — cyclomatic、cyclomatic.modified、cognitive、abc — はこれに加えて、子スペースを合算した sum / magnitude を公開しますが、バインディングは代わりにスペースごとの value フィールドを読むため、より大きな集約値に惑わされることなく、内部スペースでの超過(たとえば、ネストしたクロージャは超過していないのに関数自身の複雑度が超過している場合)を報告できます。value フィールドが存在する以前は、バインディングは集約値しか読めなかったため、この 4 メトリクスをリーフスペースでしか出力できず、CLI が報告する真の内部超過を見逃していました(#958)。
Unit の検出結果には logicalLocations: [{"fullyQualifiedName": "<file>"}] が付きます。名前のない非 Unit スペース(まれなパース失敗のケース)には "<unnamed>" が付きます。いずれも CLI の function_token プレースホルダーと一致します。
関連項目
- バッチ処理 —
to_sarifへの入力イテラブルの自然な供給源です。AnalysisFailureエントリは黙ってスキップされます。 - メトリクスの選択 — しきい値名は
metrics=とは独立した閉じた集合です。メトリクスの組を狭く要求しつつ、外したメトリクスのしきい値でゲートすると、空の SARIF run になります。 - エラー処理 — 不正な呼び出し側入力に対して
to_sarifが送出する型付き例外(TypeError/ValueError)。
変更履歴(VCS)メトリクス
big_code_analysis.vcs サブモジュールは、変更履歴リスク — ソースの AST ではなくバージョン管理の履歴から導かれるシグナル — によってファイルをランク付けし、コミットをスコアリングします。これは bca vcs CLI コマンドの Python 版であり、両者は同じ Rust エンジンに支えられているため、返される dict は CLI の構造化出力とフィールド単位で一致します。
from big_code_analysis import vcs
report = vcs.rank("path/to/repo", top=20)
trend = vcs.trend("path/to/repo", points=6)
commit = vcs.commit("path/to/repo", commit="HEAD")
diff = vcs.score_diff(unified_diff_text)
4 つのエントリポイントは bca vcs のサブコマンドに対応します。vcs.rank はファイルをランク付けし(bca vcs)、vcs.trend はそのランキングを時間軸に沿ってサンプリングし(bca vcs trend)、vcs.commit は 1 つのコミットをスコアリングし(bca vcs commit)、vcs.score_diff は素の unified diff をスコアリングします(bca vcs commit --diff)。シグナル、複合リスクスコア、その背後にある欠陥予測の文献については CLI の章を参照してください。このページでは Python 側のインターフェースを扱います。
これは、単一ファイルのメトリクスに vcs ブロックを付加する analyze(..., vcs=True) とは別物です。vcs サブモジュールはリポジトリ全体の履歴を一度だけ走査するため、ランク付けにはこちらを優先してください。analyze のキーワード引数は、ファイルの AST メトリクスと並べて変更履歴の数値が欲しい場合にのみ使います。リポジトリ内のファイル全体で履歴走査を償却する analyze_batch(..., vcs=True) の経路についてはバッチ処理を参照してください。
ファイルのランク付け
vcs.rank(repo_path, *, options=None, top=None, no_cache=False, cache_dir=None) は、対象範囲内のすべてのファイルをリスク降順でランク付けし、VcsReportDict を返します。呼び出しごとに変わるキーワード専用の設定は rank にあり、trend や commit と共有される履歴走査の設定は共通の Options オブジェクト(後述)にあります。
from big_code_analysis import vcs
report = vcs.rank("path/to/repo", top=20)
print(f"long window: {report['long_window_days']} days")
print(f"recent window: {report['recent_window_days']} days")
for ranked in report["files"]:
block = ranked["vcs"]
print(f"{block['risk_score']:6.2f} {ranked['path']}")
top はランキングに残すファイル数の上限を指定します。0 または None はすべてを残します。レポートは、解決済みのウィンドウ長と risk_score_version / vcs_schema_version スタンプを(各ファイルの vcs ブロックではなく)トップレベルに一度だけ持ちます。files リストは vcs.risk_score の降順で並び、計算された場合はリポジトリのバスファクターサマリーを vcs_aggregate キーが保持します。
aggregate = report.get("vcs_aggregate")
if aggregate is not None:
bus = aggregate["bus_factor"]
print(f"repo bus factor: {bus['repo']['bus_factor']}")
コミットのスコアリング
vcs.commit(repo_path, *, commit="HEAD", options=None) は、単一のコミットを第一親に対する just-in-time(コミットレベル)リスクの観点でスコアリングし、JitCommitReportDict を返します。commit 引数には任意の git リビジョン表記("HEAD"、"HEAD~3"、ブランチ、タグ、SHA)を指定できます。
from big_code_analysis import vcs
report = vcs.commit("path/to/repo", commit="HEAD")
print(f"risk score: {report['risk_score']}")
print(f"is merge: {report['commit']['is_merge']}")
size = report["features"]["size"]
print(f"+{size['lines_added']} -{size['lines_deleted']} "
f"across {size['files_touched']} files")
# 各特徴グループが順序スコアに与える符号付きの寄与。
for group, value in report["contributions"].items():
print(f" {group:<11} {value}")
このスコアは順序尺度です。コミット同士のランク付けや、リポジトリ自身の分布との比較には使えますが、その大きさを確率として読んではいけません。contributions ブロックは各特徴グループの符号付き寄与を報告するため、コミットが 「なぜ」 その順位になったのかを利用側が確認できます。
任意の diff のスコアリング
vcs.score_diff(diff) は、まだコミットされていない git 形式の unified diff — pre-commit フックやコードレビューボットが扱う形 — をスコアリングし、JitDiffReportDict を返します。
import subprocess
from big_code_analysis import vcs
staged = subprocess.run(
["git", "diff", "--cached"],
capture_output=True, text=True, check=True,
).stdout
report = vcs.score_diff(staged)
print(f"partial risk: {report['partial_risk_score']}")
素の diff には作者・親コミット・ファイル履歴の情報が含まれないため、source はリテラルの "diff" になり、計算可能なのは size と diffusion のグループだけで、partial_risk_score はコミットの risk_score と比較できません。history、experience、purpose の各グループはゼロではなく、欠落しています。
トレンドのサンプリング
vcs.trend(repo_path, *, options=None, points=12, span=None, top=None, top_deltas=None) は、ファイルのランキングを複数の時点でサンプリングし、VcsTrendDict を返します。各時点はその瞬間のメインラインの先頭に改めてアンカーされるため、この系列は現在のランキングを過去に投影し直したものではなく、真の歴史的スナップショットの並びになります。
from big_code_analysis import vcs
trend = vcs.trend("path/to/repo", points=6, span="6mo", top_deltas=10)
# サンプリング時刻。古い順(Unix 秒)。
print("sampled at:", trend["as_of_points"])
# 期間中に最も悪化したファイル。
for delta in trend["deltas"]["regressed"]:
print(f" +{delta['delta']:.2f} {delta['path']}")
points 個(2 以上)のサンプルが span(デフォルト 12mo)の期間に分布し、options.as_of で終わります。files マップは各ファイルの系列を as_of_points と 1:1 で揃え、そのファイルがまだ存在しなかった箇所は None 要素になります。deltas サマリーはファイルを improved と regressed のリストに分け、top_deltas が各リストを切り詰め、top が残すファイル数の上限を決めます。
共通オプション
リポジトリを走査する 3 つのエントリポイント — rank、trend、commit — は同じ vcs.Options オブジェクトを受け取るため、共通の設定項目を繰り返すことなく、1 つの構成でランク付けとトレンドの両方のパスを実行できます。すべてのフィールドはキーワード専用かつ省略可能で、デフォルト値は bca vcs CLI のデフォルトを再現するため、Options() はデフォルトのランキングと一致します。
from datetime import datetime, timezone
from big_code_analysis import vcs
options = vcs.Options(
long_window="2y",
recent_window="60d",
risk_formula="percentile",
file_types=["rs", "py"],
as_of=datetime(2026, 1, 1, tzinfo=timezone.utc),
)
report = vcs.rank("path/to/repo", options=options, top=20)
拡張されたオプションのキーワード引数(issue #619)は、それぞれ単なる文字列以外の値も受け付けます:
file_typesはランク付けの対象ファイルを選択します。"metrics"(デフォルト — bca がメトリクスを計算するファイルのみ)、"all"(追跡対象のすべてのテキストファイル)、カンマ区切りの拡張子許可リスト("rs,py")、または拡張子のSequence[str](["rs", "py"])を指定できます。as_ofは再現可能なスナップショットのために基準となる「現在」を固定します。datetime、または文字列(RFC 3339、@unix、git の日付形式)で指定します。as_ofを固定すると実行が再現可能になります。ランキングは実時間に対してではなく、その時点での状態として計算されます。cache_dir(Optionsではなくrankのパラメーター)はstrまたは任意のos.PathLikeを受け付けます —pathlib.Pathはそのまま渡せます。
履歴走査のトグルは CLI フラグを反映しています。full_history、include_merges、follow_renames(デフォルト True)、exclude_bots(デフォルト True)、そしてボット作者の正規表現を上書きする bot_pattern です。bus_factor_threshold はバスファクターのフラグに用いるカバレッジ割合を設定し(デフォルト 0.5)、emit_author_details は SHA-256 でハッシュ化された正規化済み作者 ID を含めます。author_hash_key(emit_author_details が必要)はこれらのダイジェストを鍵付き HMAC-SHA256 に強化します。これは 作者詳細のプライバシー で説明されているものと同じオプトインです。
キャッシュ
vcs.rank は各履歴走査の永続キャッシュをデフォルトで有効にして保持します。キャッシュヒットは新規の走査とビット単位で同一であり、時間ウィンドウは実行のたびに現在時刻に対して再計算されるため、キャッシュ済みの結果が古くなることはありません。
from big_code_analysis import vcs
# 最初の呼び出しがキャッシュを準備し、2 回目はそれを再生します。
vcs.rank("path/to/repo")
vcs.rank("path/to/repo") # 前回の走査を再利用
vcs.rank("path/to/repo", no_cache=True) # キャッシュを無視
vcs.rank("path/to/repo", cache_dir="/tmp/bca") # ディレクトリを上書き
デフォルトでは、キャッシュはプラットフォームのキャッシュディレクトリ配下に置かれます。作者 ID は SHA-256 ダイジェストとしてのみ保存され、平文で保存されることはありません。ハッシュ化は匿名化ではなく仮名化である点に注意してください。ダイジェストは候補となるメールアドレス集合に対して復元可能です — 作者詳細のプライバシーを参照してください。
GIL の解放
リポジトリを走査する呼び出し(vcs.rank、vcs.trend、および vcs.commit のコミットスコア)は、履歴走査の間 GIL を解放します(issue #620)。そのため ThreadPoolExecutor を使えば、インタープリターのロックで直列化されることなく、複数のリポジトリを並列にランク付けできます:
from concurrent.futures import ThreadPoolExecutor
from big_code_analysis import vcs
repos = ["service-a", "service-b", "service-c"]
with ThreadPoolExecutor() as pool:
reports = list(pool.map(lambda r: vcs.rank(r, top=20), repos))
これは 非同期パターン のページがファイル単位の analyze 呼び出しに適用しているのと同じパターンです。
エラー
vcs の各関数は、bca.VcsError(それ自体が ValueError)を頂点とする型付き例外階層を送出します。完全な分類と、どの呼び出しがどの型を送出するかについてはエラー処理を参照してください。
関連項目
- 変更履歴(VCS)メトリクス — CLI の章。シグナルの定義、複合リスクスコアの計算式、およびその背景にある欠陥予測の文献を扱います。
- バッチ処理 —
analyze_batch(..., vcs=True)は、リポジトリごとに 1 つの履歴インデックスを共有しながら、ファイルごとのvcsブロックを付加します。 - エラー処理 — VCS の例外分類。
エラー処理
バインディングはエラーを 2 つの領域に分けています:
- 呼び出し側のエラーは例外として送出されます — 不正な引数には
ValueError、誤った型にはTypeError、ファイルシステムの失敗にはOSErrorとそのサブクラスです。 - バッチ内のファイルごとの解析エラーは、結果リストの中の
bca.AnalysisFailure値として「返されます」。これらは例外ではなく、送出されることはありません。
単一ファイルの bca.analyze は前者の経路をたどり、バッチの bca.analyze_batch は後者の経路をたどります。
def run(
fixtures: Path,
*,
missing_path: Path,
) -> dict[str, Any]:
"""Trigger each error path and return a small report.
``fixtures`` is a directory containing at least ``hello.rs``;
``missing_path`` must NOT exist on disk.
"""
report: dict[str, Any] = {
"file_not_found": False,
"unsupported": False,
"batch_errors": 0,
}
# 1. 存在しないパスに対する analyze() は型付きの OSError サブクラスを送出します。
try:
bca.analyze(missing_path)
except FileNotFoundError as err:
report["file_not_found"] = True
print(f"file_not_found: errno={err.errno} filename={err.filename}")
# 2. 未知の拡張子に対する analyze() は
# UnsupportedLanguageError(それ自体が ValueError のサブクラス)を送出します。
# 書き込みを try/finally の内側に置いているため、将来 analyse 呼び出しの
# 前に 2 つ目の変更が加わってもクリーンアップされます。
unknown = fixtures / "hello.unknown_extension"
try:
unknown.write_text("noop", encoding="utf-8")
bca.analyze(unknown)
except bca.UnsupportedLanguageError as err:
report["unsupported"] = True
print(f"unsupported_language: {err}")
finally:
unknown.unlink(missing_ok=True)
# 3. analyze_batch() は AnalysisFailure を返し、ファイル単位で例外を送出することはありません。
paths = [fixtures / "hello.rs", missing_path]
for slot in bca.analyze_batch(paths):
if isinstance(slot, bca.AnalysisFailure):
report["batch_errors"] += 1
print(f"batch_error: ({slot.error_kind}) {slot.error}")
return report
単一ファイルの例外
bca.analyze と bca.analyze_source は次を送出します:
| 例外 | 継承元 | 発生条件 |
|---|---|---|
bca.UnsupportedLanguageError | ValueError | 未知の拡張子で、シバン / emacs モードにも一致しない |
bca.ParseError | ValueError | tree-sitter がソースを拒否した |
ValueError(直接) | — | allow_lossy_path=False(デフォルト)での非 UTF-8 パス |
OSError とそのサブクラス | — | std::fs::read が失敗した |
analyze が送出する OSError は、errno に基づいて正規のサブクラスへ振り分けられます:
import big_code_analysis as bca
path = "src/example.rs"
try:
bca.analyze(path)
except FileNotFoundError as err:
print("missing:", err.errno, err.filename)
except PermissionError as err:
print("denied:", err.errno, err.filename)
except IsADirectoryError as err:
print("directory:", err.errno, err.filename)
各分岐は元となる errno に基づいて振り分けられます:
| 例外 | 典型的な err.errno(Linux) | 発生条件 |
|---|---|---|
FileNotFoundError | 2 (ENOENT) | パスが存在しない。 |
PermissionError | 13 (EACCES) | 呼び出しユーザーに読み取り権限が与えられていない。 |
IsADirectoryError | 21 (EISDIR) | パスがディレクトリを指している。 |
ファミリー全体を捕捉して err.errno / err.filename を自分で調べたい場合は、except OSError を使ってください。
UnsupportedLanguageError と ParseError はどちらも ValueError のサブクラスなので、単一の except ValueError で両方を捕捉できます。区別したい場合は型付きの捕捉を優先してください。
バッチのエラー
bca.analyze_batch は例外を送出する代わりに bca.AnalysisFailure 値を返すため、1 つの不正なファイルがバッチ全体を壊すことはありません。
for slot in bca.analyze_batch(paths):
if isinstance(slot, bca.AnalysisFailure):
log.warning("%s (%s): %s", slot.path, slot.error_kind, slot.error)
else:
process(slot)
error_kind は閉じた Literal です。
"UnsupportedLanguage"— 拡張子と shebang / emacs モードのどちらの解決も空振りに終わった場合です。"ParseError"— tree-sitter が入力を拒否したか、または(まれに)Rust 側での結果の JSON シリアライズが失敗した場合です。シリアライズの失敗ではerror文字列にinternal: serialization error:という接頭辞が付きます。区別が必要な場合はこの接頭辞を確認してください(シリアライズの失敗はファイルの再読み込みでは回復できません)。"IoError"— 最も一般的な種類で、std::fs::readが失敗した場合です。この閉じた分類には非 UTF-8 パスの失敗も畳み込まれているため、パスのエンコーディングエラーは独立した 4 つ目の値としてではなく"IoError"として現れます。
"IoError" のインスタンスでは、元となる OS の errno が Rust のデフォルト書式(Unix では "<msg> (os error <N>)")で error 文字列に保持されます。リトライの分類に必要な場合は正規表現でパースしてください:
import re
match = re.search(r"\(os error (\d+)\)$", slot.error)
errno = int(match.group(1)) if match else None
型付きの OSError サブクラスが必要な場合は、analyze_batch ではなくファイルごとに bca.analyze を呼び出してください — 単一ファイルの analyze は FileNotFoundError / PermissionError / IsADirectoryError を直接送出します。
バッチにおけるプログラマーエラー
analyze_batch も、呼び出し側のバグに対しては例外を送出します:
pathsがイテラブルでない場合、または要素がstr/os.PathLike[str]でない場合はTypeErrorです。これは呼び出し全体を中断し、不正な要素より前に計算された結果はすべて破棄されます。metrics=が明示的に空のシーケンスであるか、未知の名前を含む場合はValueErrorです。検証は入力イテラブルの__iter__より「前に」実行されるため、この送出経路ではジェネレーターの副作用(および部分的な yield)は温存されます。
変更履歴(VCS)の例外
big_code_analysis.vcs の各関数は、bca.VcsError(それ自体が ValueError)を頂点とする型付き階層を送出するため、既存の except ValueError(または except bca.VcsError)ですべての VCS の失敗を捕捉できます(#624)。analyze(..., vcs=True) キーワード引数も同じオプション解析エラーを共有します。
| 例外 | 継承元 | 発生条件 |
|---|---|---|
bca.NotARepositoryError | bca.VcsError | repo_path が git 作業ツリーの中にない |
bca.InvalidRevisionError | bca.VcsError | reference / commit を解決できなかった |
bca.InvalidDiffError | bca.VcsError | vcs.score_diff に渡された diff が不正である |
bca.VcsEnvironmentError | bca.VcsError | 履歴走査、差分計算、.mailmap、blame、またはキャッシュ I/O が失敗した |
bca.VcsError(直接) | ValueError | 不正なオプション値(ウィンドウ / タイムスタンプ / 計算式 / ファイルタイプの範囲 / バスファクターのしきい値 / ボットパターン / トレンドのポイント数)。メッセージが問題の値を示します |
NotARepositoryError は「リポジトリではないのでこのディレクトリをスキップする」ために分岐すべきバリアントです。ベースの VcsError は不正なオプションに対して直接送出され、メッセージが問題の値を示します。一方、名前付きのサブクラスは入力の失敗(存在しないリビジョン、不正な diff)を扱います。VcsEnvironmentError は環境 / バックエンドの区分で、同じ失敗に対して web クレートが返す 500(400 ではなく)レスポンスに対応します。
import big_code_analysis as bca
from big_code_analysis import vcs
try:
report = vcs.rank("path/to/repo", top=20)
except bca.NotARepositoryError:
print("not a git repository, skipping")
except bca.VcsError as err:
# 不正なウィンドウ、計算式、ファイルタイプの範囲など。
print("bad VCS option:", err)
analyze(..., vcs=True) は NotARepositoryError のルールの例外です。リポジトリ外のファイルは例外を送出せず、単に vcs ブロックを生成しないだけなので、この経路から呼び出し側に届くのはオプション解析の VcsError だけです。
ログ出力のレシピ
バッチ出力用の小さなログ補助関数を使えば、独自の整形を書かずに成功 / 失敗を揃えて記録できます:
import logging
import big_code_analysis as bca
log = logging.getLogger(__name__)
def report(paths: list[str]) -> None:
# skip_generated=False は結果リストのインデックスを `paths` と
# 揃えたままにします。デフォルトの True では、生成ファイルはスロットを
# 持たず、zip が黙ってずれてしまいます。
for path, slot in zip(paths, bca.analyze_batch(paths, skip_generated=False)):
if isinstance(slot, bca.AnalysisFailure):
log.warning(
"skip %s (%s): %s", path, slot.error_kind, slot.error
)
else:
log.info(
"ok %s sloc=%s", path,
slot["metrics"]["loc"]["sloc"],
)
関連項目
- バッチ処理 — ファイルごとの失敗を
AnalysisFailureのスロットへ振り向ける、例外を送出しない契約。 - 非同期パターン —
asyncio.gather(..., return_exceptions=True)はバッチの契約の非同期側の等価物です。タスクごとの例外は gather 全体をキャンセルする代わりに結果リストに入ります。 - クイックスタート — 型付き
OSErrorサブクラスを送出する単一ファイルのanalyze経路。
非同期パターン
bca.analyze は CPU バウンドです。処理の中身は tree-sitter によるパースとメトリクスの各パスで、どちらも PyO3 の Python::detach を介して Rust 側で GIL を解放します。したがって正規の非同期パターンは asyncio.to_thread です:
async def analyze_async(path: Path) -> FuncSpaceDict | None:
"""Run ``bca.analyze(path)`` on the default thread executor."""
return await asyncio.to_thread(bca.analyze, path)
async def analyze_all(
paths: Iterable[Path],
) -> list[FuncSpaceDict | BaseException | None]:
"""Fan ``analyze_async`` out across ``paths`` with ``asyncio.gather``.
``return_exceptions=True`` matters here: ``bca.analyze`` runs
inside ``asyncio.to_thread`` and Python threads cannot be
cancelled. If one call raises and gather re-raises with
``return_exceptions=False``, the surviving threads keep running
in the default executor, producing results that are silently
discarded. With ``return_exceptions=True`` every thread's
result (success OR exception) lands in the returned list so
the caller can dispatch per-file.
"""
return await asyncio.gather(
*(analyze_async(p) for p in paths),
return_exceptions=True,
)
ネイティブ async ではなく to_thread を使う理由
bca.analyze は同期的な Rust コードに支えられた同期的な Python 関数で、内部に await 境界はありません。asyncio.to_thread でラップすると:
- 呼び出しをデフォルトのスレッドプールにスケジュールします。
- パースとメトリクスのパスが走る間、他のコルーチンを進行させます。
- 完了時に結果を呼び出し元のコルーチンへ返します。
Rust 側が重い処理の間 GIL を解放するため、複数の to_thread(bca.analyze, ...) 呼び出しは本当に並列に実行されます — これは協調的な I/O 多重化ではなく、スレッドプールのサイズを上限とする実際のマルチコア活用です。
カスタムエグゼキューター
ワーカー数をより厳密に制限するには、専用に用意したエグゼキューターを to_thread に渡します:
import asyncio
from concurrent.futures import ThreadPoolExecutor
import big_code_analysis as bca
async def analyze_many(paths: list[str]) -> list[object]:
loop = asyncio.get_running_loop()
with ThreadPoolExecutor(max_workers=8) as pool:
return await asyncio.gather(
*(loop.run_in_executor(pool, bca.analyze, p) for p in paths)
)
純粋に CPU バウンドな処理では、8 コアのマシンで 8 ワーカーが無理のない上限です。これ以上増やすとマシンを過剰に割り当てることになり、スループットをコンテキストスイッチのオーバーヘッドと引き換えにしてしまいます。
結果のストリーミング
asyncio.as_completed を使うと、最初の解析が完了した時点から結果の消費を始められます。ファイルごとの処理コストが大きくばらつく場合(5 KB のファイルと 500 KB の生成バンドルなど)に有用です:
import asyncio
import big_code_analysis as bca
async def first_failure(paths: list[str]) -> str | None:
"""Return the path of the first file with cyclomatic > 50."""
tasks = [asyncio.create_task(asyncio.to_thread(bca.analyze, p)) for p in paths]
try:
for coro in asyncio.as_completed(tasks):
result = await coro
if result is None:
continue
if result["metrics"]["cyclomatic"]["sum"] > 50:
return result["name"]
finally:
for t in tasks:
t.cancel()
return None
finally ブロックでのキャンセルが重要です。as_completed は呼び出し元が早期リターンしても保留中のタスクを自動でキャンセルしないため、リークしたタスクが非同期関数の復帰後もスレッドプール上で動き続けることがあります。
アンチパターン:コルーチン内での bca.analyze の直接呼び出し
# これはやってはいけません。
async def bad(path: str) -> dict | None:
return bca.analyze(path) # 呼び出しのたびにイベントループをブロックします
async def は本体を非同期にするわけではありません。to_thread や明示的なエグゼキューターなしでは、bca.analyze を呼び出すすべてのコルーチンがパースの全時間にわたってイベントループを停止させます — I/O、タイマー、キューを待つ他のタスクはすべて、パースが返るまで凍結します。to_thread ラッパーは 1 行で書けて、応答性のあるサーバーとシングルスレッドのサーバーの違いを生みます。
analyze_batch が適している場面
静的で有限のパスのリストを処理していて結果のストリーミングが不要なら、bca.analyze_batch の方が gather(*to_thread(...)) よりシンプルです。呼び出し元スレッド上で逐次実行されますが、ファイルごとのエラーで例外を送出することはありません。イベントループの応答性を保つには、analyze_batch の呼び出し全体を asyncio.to_thread でラップしてください:
import asyncio
import big_code_analysis as bca
async def batch(paths: list[str]) -> list[object]:
return await asyncio.to_thread(bca.analyze_batch, paths)
これは gather のファイル単位の並列性を、analyze_batch のよりシンプルなエラーモデルと引き換えにします。並列性と型付き OSError の振り分けの両方が欲しい場合は gather を、非同期呼び出しを 1 つにまとめて例外を送出しない契約が欲しい場合は to_thread(analyze_batch, paths) を選んでください。
開発者ガイド
big-code-analysis の開発に貢献したい方のために、ビルド作業の助けとなる一連のガイドラインをここにまとめています。
前提条件として、Rust の最新バージョンをインストールする必要があります。その方法は こちら で確認できます。
リポジトリのクローン
まず、リポジトリをクローンする必要があります。次の方法で実行できます。
HTTPS を使う方法
git clone -j8 https://github.com/dekobon/big-code-analysis.git
または SSH を使う方法
git clone -j8 git@github.com:dekobon/big-code-analysis.git
Make が正式なエントリポイント
このリポジトリには、ビルド・テスト・リント・フォーマット・ドキュメント生成といった一般的なタスクをすべてラップする Makefile が同梱されています。make help を実行するとターゲットの全一覧を確認でき、make check-tools を実行すると、オプションのツール(taplo、rumdl、shellcheck、shfmt、checkmake、mdbook、cargo-insta、cargo-udeps、cargo-nextest)のどれがマシンに存在するかを確認できます。
最もよく使うことになる 2 つの複合ターゲットは次のとおりです。
make pre-commit— コミット前に推奨されるローカルゲートです。cargo fmt --check、2 種類の clippy 実行(デフォルトフィーチャと--all-features)、make test+make test-docによるテストスイート全体(lib + bin + 統合 + doc)、cargo +nightly udeps、および Markdown / TOML / シェル / Makefile のリント群を 1 回の並列パスで実行します。make ci— CI が実行するのと同じチェックを同じ順序で、自動修正なしで実行します。CI の失敗をローカルで再現するために使用してください。
GNU Make 4 やオプションのツールが利用できない場合は、以下に示す生の cargo コマンドにフォールバックしてください。これらは対応する Make ターゲットと同等です。
ビルド
big-code-analysis ライブラリ、CLI、Web サーバーを一括でビルドするには、次を実行します。
make build # cargo build --workspace --all-targets
make build-release # cargo build --workspace --release
個別のクレートに対しては、cargo を直接呼び出します。
cargo build # library only
cargo build -p big-code-analysis-cli # CLI only
cargo build -p big-code-analysis-web # web server only
make check は cargo check --workspace --all-targets を実行し、反復作業中の高速な型チェックを行います。
テスト
すべてのテストが通ることを確認するには、次を実行します。
make test # cargo nextest run --workspace --all-features
make test-doc # cargo test --workspace --all-features --doc
make test は cargo-nextest を優先します。CI が実行しているのもこちらです。素の cargo test は各テストバイナリを終えてから次を開始するため、並列性はバイナリ内にしか存在しません。nextest はすべてのバイナリのテストを 1 つのグローバルなプールにスケジュールします。cargo install --locked cargo-nextest でインストールできます。
nextest がない場合、make test は cargo test --workspace --all-features --lib --bins --tests にフォールバックし、同じテスト集合を実行します。ただし 2 つのランナーは完全に交換可能ではありません。nextest は各テストを独自のプロセスで実行するため、バイナリ内でテスト間に状態を共有するもの — OnceLock キャッシュ、環境変数、カレントディレクトリ — は nextest では分離され、cargo test では共有されます。ローカルで nextest を優先するということは、その軸においてローカル実行が CI と一致するということです。
NEXTEST= を設定すると、何もアンインストールせずにフォールバックを強制できます(make test NEXTEST=)。特定のバイナリを指すこともできます(make test NEXTEST=/path/to/cargo-nextest)。
doctest は nextest が実行できないため、別のターゲットになっています。make pre-commit と make ci はその両方を実行します。
cargo コマンドだけを自分で実行したい場合は、次のとおりです。
cargo test --workspace --all-features --verbose
insta テストの更新
本プロジェクトでは insta を使用しています。スナップショットを管理するには cargo insta をインストールしてください。必要になる 2 つの操作は Makefile がラップしています。
make insta-review # cargo insta test --review (interactive)
make insta-accept # cargo insta test --accept (use with care)
make insta-review はテストを実行して新しいスナップショット参照を生成し、各差分をレビューできるようにします。make insta-accept は、差分のパターンが一様であることを確認済みの、メトリクス値のみが変わる一括更新(文法のバージョンアップ、Halstead 演算子の再分類など)に限って使用してください。
コードフォーマット
これまでのステップがすべて問題なく完了し、あなたの貴重な貢献をコードベースに取り込むためのプルリクエストを作成したいなら、最後に残っているステップはコードフォーマットです。make fmt ターゲットはプロジェクト内のすべてのフォーマッタ(Rust、Markdown、TOML、Bash)を一括で実行します。make fmt-check はファイルを変更せずにフォーマットを検証します。
make fmt # cargo fmt + rumdl check --fix + shfmt -w + taplo fmt
make fmt-check # the equivalent --check variants
Rustfmt
このツールは、Rust のスタイルガイドラインに従ってコードをフォーマットします。
インストールするには、次を実行します。
rustup component add rustfmt
コードをフォーマットするには、次を実行します(make fmt が自動的に処理します)。
cargo fmt
Clippy
このツールは、よくある間違いの多くを自動的に検出し、開発者がより良いコードを書く手助けをします。コード内の一連のエラーや警告を検出し、これらはプルリクエストを作成する前に必ず修正しなければなりません。
make clippy は、プロジェクトが強制する 2 種類の clippy 実行(デフォルトフィーチャと --all-features)を実行します。make lint はさらに Markdown、シェル、TOML、Makefile のリンターも実行します。
インストールするには、次を実行します。
rustup component add clippy
エラーと警告を検出するには、次を実行します。
make clippy
# or, manually:
cargo clippy --workspace --all-targets -- -D warnings
cargo clippy --workspace --all-targets --all-features -- -D warnings
未使用の依存関係
make udeps は cargo +nightly udeps --workspace --all-targets を実行し、Cargo.toml に宣言されているものの一度も参照されていない依存関係を検出します。nightly ツールチェーン(rustup toolchain install nightly)と cargo-udeps が必要です。
コードドキュメント
make doc # cargo doc --no-deps --workspace --all-features (warning-tolerant)
make doc-open # same, then open in a browser
make doc-check # strict gate: appends -D warnings to RUSTDOCFLAGS, fails on any rustdoc warning
make doc と make doc-open は対話的なビューアーで、ビルドできるものをビルドするため、リファクタリングの途中でもレンダリング結果を確認できます。make doc-check は make pre-commit と CI の一部として実行される厳格なゲートで(RUSTDOCFLAGS に -D warnings を追加した cargo doc --no-deps --workspace --all-features)、壊れたドキュメント内リンク、非公開アイテムへのリンク、その他の rustdoc の回帰を検出します。
big-code-analysis が使用する各依存関係のドキュメントもビルドしたい場合は、基になる cargo 呼び出しから --no-deps オプションを削除してください。
本書のビルド
いま読んでいる本書は big-code-analysis-book/ 以下にあります。
make book # mdbook build
make book-serve # mdbook serve with live reload
コードの実行
bca は次のように実行できます。
cargo run -p big-code-analysis-cli -- [bca-parameters]
bca のパラメーター一覧を確認するには、次を実行します。
cargo run -p big-code-analysis-cli -- --help
bca-web は次のように実行できます。
cargo run -p big-code-analysis-web -- [bca-web-parameters]
bca-web のパラメーター一覧を確認するには、次を実行します。
cargo run -p big-code-analysis-web -- --help
make install、make install-cli、make install-web は、それぞれ対応するバイナリクレートに対して cargo install --path を呼び出します。
実践的なアドバイス
- 新機能を追加するときは、すべてが正しく動作することを確認するために、少なくとも 1 つのユニットテストまたは統合テストを追加してください
- 公開 API をドキュメント化してください
- デッドコードを追加しないでください
- 複雑なコードには、何を実現したのかを他の人が理解できるようにコメントを付けてください
- プッシュ前に
make pre-commitを実行してください — CI が実行するのと同じゲートです
新しい言語のサポート
このセクションは、開発者が big-code-analysis に新しい言語のサポートを実装するのを支援するためのものです。
新しい言語を実装するには、2 つのステップが必要です。
- 文法を生成する
- 文法を
big-code-analysisに追加する
多数のメトリクスがサポートされており、それらを実装するための手引きはドキュメントの別の場所で扱われています。
文法の生成
新しい文法を追加するための前提条件として、目的の言語について、本プロジェクトで使用しているバージョンと一致する tree-sitter のバージョンが存在している必要があります。
文法は、このリポジトリ内の enums というプロジェクトによって生成されます。以下のステップでは、言語クレートから言語サポートを追加し、メトリクスを評価するために本プロジェクトで文法として使用される enum ファイルを生成します。
- 言語固有の
tree-sitterクレートをenumsクレートに追加します。このとき、依存関係がルートのbig-code-analysisのCargo.tomlで使用されているのと同じバージョンに=X.Y.Zでピン留めされていることを確認してください。たとえば Rust サポートの場合、/enums/Cargo.toml には次の行があります:tree-sitter-rust = "=0.24.2"。 - /enums/src/languages.rs で
enumクレートに言語を追記します。引き続き Rust を例にすると、その行は(Rust, tree_sitter_rust)になります。第 1 パラメーターは生成される Rust の enum の名前で、第 2 パラメーターはその言語の文法を取得するために呼び出すtree-sitterの関数です。 - /enums/src/macros.rs の
mk_get_languageマクロルール内にある match の末尾にケースを追加します。現在の慣例では、最近の文法クレートが公開するLANGUAGE定数を使用します。Rust の場合、その行はLang::Rust => tree_sitter_rust::LANGUAGE.into()です。 - 最後に、/recreate-grammars.sh スクリプトを実行します。これは
enumsクレートを実行して、新しい言語の文法を生成します。
この時点で、/src/languages/ に新しい言語の文法ファイルができているはずです。生成された enum の例としては /src/languages/language_rust.rs を参照してください。
新しい文法を big-code-analysis に追加する
- 言語固有の
tree-sitterクレートを、enumsクレートと同じ=X.Y.Zのピン留めでbig-code-analysisワークスペースに追加します。たとえば Rust サポートの場合、ルートの Cargo.toml の行はtree-sitter-rust = "=0.24.2"です。 - 次に、新しい
tree-sitter言語の名前空間を /src/languages/mod.rs に追加します。例:
#![allow(unused)] fn main() { pub mod language_rust; pub use language_rust::*; }
- 最後に、/src/langs.rs の
mk_langs!マクロの引数に言語の定義を追加します。
#![allow(unused)] fn main() { // 1) このバリアントの文法を有効にする Cargo フィーチャ名 // 2) enum の名前 // 3) 言語の説明 // 4) 表示名 // 5) 実装対象となる空の構造体名 // 6) パーサー名 // 7) Language を取得するために呼び出す tree-sitter 関数 // 8) ファイル拡張子 // 9) emacs モード // 10) ピン留めされた文法クレートのバージョン(ワークスペースの Cargo.toml にある // `=X.Y.Z` ピンを反映。両者の一致はドリフトテストで検証されます) ( "rust", Rust, "The `Rust` language", "rust", RustCode, RustParser, tree_sitter_rust, [rs], ["rust"], "0.24.2" ) }
トレイトとテストの実装
文法の配線は最初のステップにすぎません。新しい <Lang>Code 型は、AST の基盤処理と、ワークスペースが定義するすべてのメトリクストレイトも実装しなければなりません。
- /src/checker.rs の
Checker— 文法のkind_idに対する、コメント、関数、クロージャ、呼び出し、文字列リテラル、else-ifの各述語。 - /src/getter.rs の
Getter—get_space_kindに加え、Halstead の演算子/オペランド分類テーブル。 - /src/alterator.rs の
Alterator— 通常は文字列リテラルの保全のみで、ほとんどの言語ではデフォルト実装で十分です。 - 13 個すべてのメトリクストレイト:
Abc、Cognitive、Cyclomatic、Exit、Halstead、Loc、Mi、NArgs、Nom、Npa、Npm、Tokens、Wmc。まず /src/metrics/ のimplement_metric_trait!マクロ呼び出しで各トレイトをデフォルト(no-op)の本体として登録し、その後、その言語にとって意味のあるセマンティクスを持つメトリクスについては実際の実装に置き換えます。
エイリアス化された文法バリアントの監査
tree-sitter の文法は、同じ node.kind() 文字列に対応する複数の異なる kind_id を発行することがよくあります(Go の Identifier / Identifier2 / Identifier3、C# の InvocationExpression / InvocationExpression2、Elixir の QuotedContent ⋯ QuotedContent20)。エイリアス化されうるルールに触れるすべての match node.kind_id() アームは、番号付きバリアントをすべて列挙するか、代わりに文字列 node.kind() で比較しなければなりません。エイリアスを見落とすと、ノードがメトリクスから黙って脱落します。機械的な監査手順については add-lang スキルを、失敗モードについては docs/development/lessons_learned.md のレッスン 2、4、13 を参照してください。
テスト
各 src/metrics/*.rs のテストモジュールに言語ごとのテストを追加します。各メトリクスファイルにわたって、既存の Rust のカバレッジと同等になることを目指してください。すべての insta::assert_json_snapshot! 呼び出しは必ずアンカーされていなければなりません。すなわち、インラインの期待値ブロック、その直上に置いた主要な整数アクセサに対する正の assert_eq!、または説明のための // expected: コメントのいずれかを伴う必要があります。make snapshot-anchors(make pre-commit の一部として実行)が .snapshot-anchor-baseline.txt に照らしてこれを強制します。
エンドツーエンドのワークフロー
エイリアス監査、テストレイアウト、スナップショットのアンカー付け、コード品質の後処理パスを含む、明確な方針を持つエンドツーエンドのレシピについては、プロジェクトの add-lang Claude Code スキルを参照してください。これは最近の言語追加(Elixir、PHP、C#、Bash、Go)で使用された正式なワークフローです。
コード行数(LoC)
このドキュメントでは、このクレートで利用できる LoC メトリクスの実装方法についての手引きを示します。コード行数は、ソースコードの行を数えることでソースコードの規模の目安を与えるソフトウェアメトリクスです。LoC には多くの種類があるため、まず例を使ってそれらを説明します。
LoC の種類
#![allow(unused)] fn main() { /* 課題: 階乗関数を実装せよ 追加加点として、可変状態や `for`・`while` のような命令型ループは使用しないこと。 */ /// 階乗: n! = n*(n-1)*(n-2)*(n-3)...3*2*1 fn factorial(num: u64) -> u64 { // `Iterator` の `product` を使う (1..=num).product() } }
上の例を使って、以下で説明する各 LoC メトリクスを説明します。
SLOC
コード、コメント、空行を含む、ファイル内の全行を単純に数えたものです。
メトリクス値: 11
PLOC
ソースコードに含まれる命令行の数です。新しい行に置かれた括弧などの類似構文も含まれます。コメントと空行はここには数えられないことに注意してください。メトリクス値: 3
LLOC
「論理」行は、コード内の文の数を数えたものです。何を文とみなすかは言語によって異なることに注意してください。上の例では文は 1 つだけで、それは Iterator を引数とする product の関数呼び出しです。メトリクス値: 1
CLOC
コード内のコメントの数です。単一行、ブロック、doc といったコメントの種類は問いません。
メトリクス値: 6
BLANK
最後になりましたが、このメトリクスはコード中に存在する空行を数えます。メトリクス値: 2
空白文字のみのファイル
トークンをまったく含まないソース — スペース、タブ、改行だけのファイル — は、末尾の改行が LoC の値を変える唯一の入力クラスです。それ以外の場所では、末尾の改行はどのメトリクスも依存しない書式上の詳細にすぎません。
そのような入力に対して、ほとんどの文法は tree-sitter のルートノードをファイル全体にまたがらせるのではなく、入力末尾の幅ゼロのノードに畳み込みます。SLOC はそのスパンから導出されるため、これらのファイルは改行で終わる場合に sloc 0、終わらない場合に sloc 1 を報告します。5 つの文法 — Elixir、Tcl、iRules、および preproc / ccomment ヘルパー — はルートのスパンを保持し、どちらの場合も行数を報告します。
ある言語がどちらに該当するかは上流の文法の挙動であって、このクレートが下す判断ではありません。したがって文法のバージョン更新によって言語が別側へ移ることがあります。言語ごとの値は /src/metrics/loc.rs の whitespace_only_input_is_the_documented_carve_out で固定されています。
実装
上で説明した LoC 関連のメトリクスを実装するには、サポートしたい言語に対して Loc トレイトを実装する必要があります。
これには compute 関数の実装が必要です。実装場所と他言語の例については /src/metrics/loc.rs を参照してください。
PLOC に行を挿入するキャッチオールの _ アームには注意してください。Tcl ファミリーのように、文法が行終端子を extra ではなく_トークン_として表出する場合、そのトークンの開始行は終端する行そのものなので、キャッチオールはコメントのみの行や空行を PLOC に計上してしまいます。そうしたトークンには明示的な no-op アームを与えてください。
文法の更新
各プログラミング言語は、その構文と意味 — いわゆる言語の文法 — を抽出するためにパースされる必要があります。big-code-analysis では、対応プログラミング言語ごとに個別の文法一式を提供している tree-sitter をパースライブラリとして使用しています。しかし文法は静的な一枚岩ではなく、時間とともに変化し、バグの影響を受けることもあるため、折に触れて更新する必要があります。
現時点では、操作の自動化に bash スクリプトを使用しているため、文法をネイティブに更新できるのは Linux と MacOS システムのみですが、これらのスクリプトは WSL を使えば Windows でも実行できます。
big-code-analysis では、サードパーティの文法と内部の文法の両方を使用しています。前者は crates.io で公開され外部の開発者によって保守されているもので、後者は Firefox で使用される一部言語の変種を扱うためにプロジェクト内で考案・定義されたものです。以下のセクションで両方の更新方法を説明します。
サードパーティの文法
Cargo.toml と enums/Cargo.toml の文法バージョンを更新します。以下は tree-sitter-java 文法の例です
tree-sitter-java = "x.xx.x"
ここで x は数字を表します。
./recreate-grammars.sh を実行して、すべての文法の構造とデータを再作成・更新します
./recreate-grammars.sh
上記スクリプトの実行が完了したら、文法の変更によって発生した失敗テストや問題があれば、それらをすべて修正する必要があります。
変更をコミットして、新しいプルリクエストを作成します
内部の文法
内部文法の package.json ファイルにある tree-sitter-cli のバージョンを更新し、同じディレクトリで npm install --package-lock-only --ignore-scripts を実行してコミット済みの package-lock.json を更新し、両方のファイルを一緒にコミットします。再生成スクリプトは npm ci でインストールを行うため、ロックファイルが存在しない場合や package.json と同期していない場合には明示的に失敗します。これにより、すべての再生成がハッシュ検証済みかつバイト単位で再現可能に保たれます(OpenSSF Scorecard の Pinned-Dependencies)。
5 つのベンダリングされた文法は bca-tree-sitter-* 名前空間で公開されます(改名の理由は RELEASING.md を参照)。ただし、利用側の呼び出し箇所は Cargo の package = ... エイリアスを介して引き続き tree-sitter-<lang> として参照します。文法の更新だけではリーフのバージョンは上がりません — このリポジトリのすべてのクレートはワークスペース全体で 1 つのバージョンを共有しており、リーフを親と食い違わせてバージョンを上げることは許可されていません(RELEASING.md の「Lockstep version policy」を参照)。パーサーテーブルを再生成し、生じたテストスナップショットのドリフトを受け入れ、現在のバージョンのまま変更を出荷してください。次のワークスペースリリースが、次のタグが宣言する共有バージョンで新しい文法を取り込みます。
再生成に加えて tree-sitter の「ランタイム」依存関係の更新も必要な場合は、リーフの Cargo.toml 内の dev-dependency 行を更新します。
[dev-dependencies]
tree-sitter = "=x.x.x"
[package] name = "bca-tree-sitter-<lang>"、[package] version、[lib] name = "tree_sitter_<lang>" はそのままにしてください — [lib] での改名の仕掛けが Rust のインポートパスを安定に保っており、バージョン行はリリース時のロックステップバンプで管理されます。
適切なスクリプトを実行し、すべてのファイルとスクリプトを再作成・更新して文法を更新します。
tree-sitter-ccomment と tree-sitter-preproc の場合は、./generate-grammars/generate-grammar.sh に続けて文法名を指定して実行します。以下は引き続き tree-sitter-ccomment 文法を使った例です
./generate-grammars/generate-grammar.sh tree-sitter-ccomment
一方、tree-sitter-mozcpp と tree-sitter-mozjs には、それぞれ専用のスクリプトを使用します。
tree-sitter-mozcpp の場合は、次を実行します
./generate-grammars/generate-mozcpp.sh
tree-sitter-mozjs の場合は、次を実行します
./generate-grammars/generate-mozjs.sh
5 番目のベンダリング文法である tree-sitter-tcl には再生成スクリプトがありません。事前生成されたパーサーソースのみをベンダリングしている(grammar.js がない)ため、更新する際はローカルで tree-sitter generate を実行するのではなく、アップストリームプロジェクトから生成済みの src/ を再ベンダリングします。
上記スクリプトの実行が完了したら、文法の変更によって発生した失敗テストや問題があれば、それらをすべて修正する必要があります。
変更をコミットして、新しいプルリクエストを作成します