サイクロマティック複雑度とは?計算方法・目安10の根拠とツールで値が違う理由
サイクロマティック複雑度(Cyclomatic Complexity)は、プログラムの制御フローに含まれる「線形独立な経路の本数」を数えた指標です。Thomas J. McCabe が1976年の論文「A Complexity Measure」(IEEE Transactions on Software Engineering, vol. SE-2, no. 4, pp. 308-320)で提唱しました。日本語では循環的複雑度とも訳され、サイクロマチック複雑度という表記ゆれもありますが、指すものは同一です。数え方そのものは単純なのに、現場では「10を超えたら分割」という目安の根拠が曖昧なまま運用され、しかも同じコードをESLintとCheckstyleで測ると別の値が出ます。
まとめ
- 定義は制御フローグラフ上の v(G) = E – N + 2P(辺の数 – ノードの数 + 2×連結成分数)。実務では「判定点の数 + 1」で数えれば同じ値になる。
- 目安の10は McCabe が1976年に示した推奨値で、NIST Special Publication 500-235(Watson・McCabe、1996年8月)は10に相当の裏付けが蓄積されたと述べている。
- ただしNISTは15への緩和を無条件では認めていない。経験あるスタッフ・形式的な設計・構造化プログラミング・コードウォークスルー・包括的なテスト計画といった運用上の優位を備えたプロジェクトに限る、と条件を付けている。
- 既定のしきい値はツールごとに違う。ESLintのcomplexityルールは20、Checkstyleは10、.NETのCA1502は25。「10超えは危険」は普遍の基準ではない。
- switchの数え方と論理演算子の扱いがツール間で異なるため、同じコードでも測定値が変わる。ツールをまたいで数値を比較しない。
- v(G) は基底パステストに必要なテストケース数と一致する。分岐網羅(C1)は多くの場合それより少ない本数で満たせるため、v(G)をC1の必要本数と取り違えない(v(G)はC1の上限)。
- 値が高い=常に分割すべき、ではない。分岐がフラットに並ぶディスパッチ処理は、分割するとかえって追いにくくなる。
定義と2通りの計算方法
サイクロマティック複雑度は、ソースコードを制御フローグラフ(処理の分岐を節点と辺で表した有向グラフ)に変換して測ります。
制御フローグラフから求める公式
McCabe の定義は次の式です。
v(G) = E - N + 2P
E: 辺(エッジ)の数
N: ノードの数
P: 連結成分の数(単一の関数を測るなら P = 1)
単一の関数であれば P = 1 なので v(G) = E – N + 2 になります。なおMicrosoftのCA1502のドキュメントは「エッジの数 – ノードの数 + 1」と記載しており、資料によって定数項が1つずれます。これは誤記ではなく、出口ノードから入口ノードへ仮想的な辺を1本足して強連結にしたグラフを前提にしているためで、辺が1本増える分だけ定数項が1つ小さくなります。どちらの式でも同じ値になります。
判定点を数える実務的な求め方
グラフを描かなくても、1 に判定点の数を足すだけで同じ値が得られます。判定点は if、while、for、case、catch、三項演算子、そして多くのツールでは && と || です。
function classify(n) {
if (n < 0) return "negative";
if (n === 0) return "zero";
return "positive";
}
この関数は判定点が if 2本なので、複雑度は 1 + 2 = 3 です。経路が3本(負・ゼロ・正)あることと一致します。
同じコードでもツールで値が違う理由
定義式は一つでも、何を判定点と見なすかの実装がツールごとに違います。同じ関数の測定値がツールを変えると変わるため、数値だけを見て「あのチームは複雑度8、うちは12」と比較しても意味がありません。
switch文の数え方の分岐(ESLint variant と Checkstyle プロパティ)
ESLintのcomplexityルールには variant オプションがあり、既定の "classic" は case句ごとに +1、"modified" は switch文全体で +1 として数えます。case句が3本ある次の関数なら、classicで4、modifiedで2という差が出ます。なお variant オプションは ESLint v9.12.0(2024年10月)で追加されたため、それ以前のバージョンでは指定できません。
function label(day) {
switch (day) {
case 0: return "sun";
case 1: return "mon";
case 2: return "tue";
default: return "other";
}
}
Checkstyleの CyclomaticComplexity チェックも同じ論点を switchBlockAsSingleDecisionPoint プロパティで持っており、既定値は false、つまりcaseを個別に数えます。true にするとswitchブロック全体を1つの判定点として扱います。Microsoft の CA1502 も、分岐(if・while・do 等)の数とswitch文の中のcase文の数に1を足す方式で、7つのcaseを持つメソッドの複雑度を8としてドキュメントに例示しています。
ただし、switchをまとめて1と数える設定をテスト計画の根拠に使うのは危険です。NIST SP 500-235 は、多方向分岐の複雑度を小さく見せる方向にメトリクスを改変すると各分岐すら実行できない本数のテストしか導けず、テストの目的には不適切になると一般論として批判しています。NISTが実例に挙げるのは複雑度を多方向分岐の枝数で割るという別の改変方式で、複雑度90のモジュールに何もしない10分岐のswitchを足すだけで、その改変版の値を10まで落とせてしまったという指摘です。レビューの目安として値を丸めるのは構いませんが、テストケース数の見積もりには既定の数え方を使ってください。
論理演算子とショートサーキット
もう一つの差は && と || です。ESLintはこれらに加えてオプショナルチェーン(?.)も複雑度に加算します。Checkstyleも既定トークンに LAND(&&)と LOR(||)を含み、CA1502 のドキュメントも if (condition1 || condition2) の複雑度を3と例示しています。一方、後述する認知的複雑度は同じ種類の論理演算子が連なる範囲をひとまとまりで1と数えるため、if (a && b && c) では論理演算子部分の加点が1にとどまります(a || b && c || d のように演算子が混ざる場合は、連なりの数だけ加算されます)。条件式の多いコードほど両指標の乖離が大きくなります。
運用の結論はこうです。プロジェクト内では測定ツールを1つに固定し、しきい値はそのツールの数え方に合わせて決める。ツールを乗り換えるときは、しきい値も測り直して再設定します。
目安「10」の出所と自プロジェクトのしきい値の決め方
「サイクロマティック複雑度の目安は10」という数字がどこから来たかを押さえておくと、機械的に従うべき場面とそうでない場面を切り分けられます。
McCabeの推奨とNISTの追認
10という値は、McCabe が1976年の原論文で、開発中のモジュールの複雑度が10を超えたら小さなモジュールへ分割すべきだと推奨したことに由来します。原論文はこの10を、合理的ではあるが魔法の数字ではない上限(a reasonable, but not magical, upper limit)と表現しています。その後、Arthur H. Watson と Thomas J. McCabe による NIST Special Publication 500-235「Structured Testing: A Testing Methodology Using the Cyclomatic Complexity Metric」(1996年8月)が、10という数字には相当の裏付けが蓄積されていると述べたうえで、15までの制限も問題なく運用されてきたとしています。
ただしNISTは緩和に条件を付けています。10を超える上限は、経験のあるスタッフ・形式的な設計・現代的なプログラミング言語・構造化プログラミング・コードウォークスルー・包括的なテスト計画といった運用上の優位を複数持つプロジェクトに限るべきであり、複雑なモジュールが要求する追加のテスト工数を負担する覚悟がある場合にのみ10より大きい上限を選べる、という書き方です。15という数字だけを引用して緩めるのは、原典の読み方として不正確です。
ツール別の既定しきい値
実際、主要ツールの既定値は10で揃っていません。
| ツール・ルール | 対象言語 | 既定のしきい値 | 備考 |
|---|---|---|---|
| Checkstyle CyclomaticComplexity | Java | 10 | maxプロパティ。標準の設定セットには非同梱 |
| ESLint complexity | JavaScript / TypeScript | 20 | maxオプション。ルール自体は既定で無効 |
| .NET CA1502 | C# / Visual Basic | 25 | これを超えると違反を報告。.NET 10でも既定で無効 |
いずれも有効化して初めて動く点に注意してください。Pythonのradonは合否ではなくランクで示し、CC 1〜5がA、6〜10がB、11〜20がC、21〜30がD、31〜40がE、41以上がFです。AとBが低リスク、Cから注意領域に入ります。CIで機械的に落としたい場合は、同じ計測エンジンを使う xenon を併用するとランクをしきい値として扱えます。
自プロジェクトの決め方は、既存コードの分布を先に測ることです。全関数の複雑度を計測して分布を取り、現状のコードのうちごく一部だけが引っかかる値をパーセンタイルで初期しきい値に置きます。いきなり10で運用を始めると既存コードが警告で埋まり、警告そのものが無視されるようになります。CIで新規追加分だけを対象にする設定にすれば、既存の負債を抱えたまま新しいコードの品質だけを守れます。
認知的複雑度との違いと使い分け
認知的複雑度(Cognitive Complexity)は、サイクロマティック複雑度の弱点を補うために SonarSource が公開したホワイトペーパー(G. Ann Campbell、初版2016年・現行 Version 1.7 / 2023年8月)で定義された別の指標です。SonarQube では検出ルール S3776 として実装され、関数あたりのしきい値の既定値は15です。
両者の性格差は、ネストの扱いに現れます。サイクロマティック複雑度は判定点の個数しか見ないため、if が3本フラットに並ぶコードと、3重にネストした if が同じ4という値になります。人間にとっての読みにくさは明らかに後者が上ですが、経路の本数は同じだからです。認知的複雑度はネストが深くなるほど加点を重くし、逆にswitchのように読み手の負担が小さい構造は控えめに数えます。
使い分けの基準は目的です。テスト設計で必要なケース数を見積もるならサイクロマティック複雑度、レビューやリファクタリングの優先順位付けなら認知的複雑度。どちらか一方に統一する必要はなく、SonarQubeは両方を同時に出力します。なお、制御構造の絡み方そのものを測る本質的複雑度(essential complexity)という指標もあり、こちらは構造化された制御構造を取り除いた後に残る複雑度=構造化されていない度合いを表します。
テストケース数の見積もり根拠としての使い道
サイクロマティック複雑度がテスト計画で使われるのは、値がそのまま基底パステスト(NISTの言う構造化テスト)に必要なテストケースの本数と一致するからです。NIST SP 500-235 は、この基準を満たすのに必要な最小テスト数はちょうど複雑度に等しいと述べています。複雑度8の関数なら8本、という見積もりが工数の根拠として提示できます。
ここで取り違えやすいのが分岐網羅(C1)との関係です。C1は多くの場合、複雑度より少ない本数で満たせます。例えば if (a) x++; if (b) y++; if (c) z++; のように独立した判定が並ぶ関数は複雑度4ですが、全条件trueと全条件falseの2本で全分岐を通せます。一方、先の classify のように各判定が return で抜ける関数はC1にも3本を要し、複雑度と一致します。NISTも、構造化テストは分岐網羅と命令網羅を包含する一方、他のホワイトボックス基準は任意に複雑なコードでも少ないテスト数で満たせてしまう欠点があると指摘しています。つまり複雑度はC1の必要本数の上限であり、C1の必要本数がこれを超えることはありません。C1を達成したからといって、判定の独立した組み合わせを検証したことにもなりません。
また「全経路を通す」ことは目標になりません。ループを含むプログラムの実行経路は事実上無限に存在するためで、複雑度が示すのはあくまで線形独立な経路の基底集合の大きさです。網羅基準ごとの違いはテストカバレッジとは?C0/C1/C2の網羅率と計測ツール・目標設定を実装者向けに解説で整理しています。
複雑度を下げる手順と、下げるべきでない場合
値が高い関数への対処は、闇雲な関数分割ではなく順序があります。
判定点を減らす手順(ガード節・テーブル駆動・ポリモーフィズム)
まず押さえるべきは、ガード節による早期returnはサイクロマティック複雑度を下げないという事実です。入力チェック3件をネストした関数(複雑度4)を早期returnに書き換えても、if は3本のままなので値は4から動きません。下がるのはネストに重み付けする認知的複雑度のほうです。読みやすさは確実に改善するので着手する価値はありますが、CIのしきい値対策としては効きません。
数値を実際に下げたいなら、判定点そのものを消す必要があります。値による分岐が目安として5本程度から並ぶなら、マップやディクショナリの参照に置き換えると case が丸ごと消えます。型による分岐であればポリモーフィズムへの置き換えが同じ効果を持ちます。設計段階で分岐を増やしすぎない考え方はKISS原則(KISSの法則)とは?意味・由来とシンプルな設計の実践が参考になります。着手のタイミング判断はリファクタリングを行うべきタイミングとその判断基準で扱っています。
分割するとかえって悪化するケース
分岐がフラットに並ぶディスパッチ処理は、複雑度が高くても分割しないほうがよい場面があります。例えば列挙型の全メンバを処理する巨大なswitchは、caseごとの中身が数行なら読み手は上から順に追うだけで全体を把握できます。これを複雑度を下げる目的だけで小関数に切り出すと、呼び出し先を何度も往復させられ、追加漏れの検出も難しくなります。
この判断は一次資料が二重に支えています。NIST SP 500-235 は、McCabe が当初から単一の多方向分岐(switch文・case文)だけで構成されるモジュールを複雑度の制限から除外することを推奨していたと記しています。ここで除外されるのは上限の適用であって、数え方そのものではありません。測り方を変えて値を小さく見せるのと、測ったうえで上限の適用外と判断するのは別の話です。Microsoft も CA1502 について、複雑さを簡単に下げられず、かつメソッドが理解・テスト・保守しやすい場合は警告を抑制して問題ないとし、特に大規模なswitchステートメントを含むメソッドを除外の候補として挙げています。判断基準は数値ではなく、分割後に読み手が追う往復の回数が減るかどうかです。減らないなら抑制コメントを添えて残し、なぜ残すかをコード上に書いておきます。
測定ツールと開発フローへの組み込み
言語ごとの代表的な測定手段は、Javaが Checkstyle と SonarQube、JavaScript / TypeScript が ESLint の complexity ルール、Python が radon と lizard、C# が Visual Studio のコードメトリクスと CA1502 です。lizard は複数言語を一つのコマンドで測れるため、言語が混在するリポジトリの一括計測に向きます。Visual Studio のコードメトリクスが出力する保守容易性指数(Maintainability Index)にも、複雑度は係数0.23の減点項として組み込まれており、単独の指標としてだけでなく他指標の構成要素としても使われます(現行のVisual Studioは、この式の結果を0から100の範囲に再基準化した値を表示します)。
組み込みは2段階に分けます。まずローカルとプルリクエストのCIで警告として出し、数値の分布とチームの感覚が合っているかを一定期間観察する。合意が取れてからCIの失敗条件に昇格させます。いきなり失敗させると、しきい値を回避するためだけの不自然な関数分割を誘発します。測定ツール全般の位置づけは静的解析とは何か?ソースコードを実行せずに品質を確保する手法、CIへの自動化の組み込みはコード品質管理における自動化ツールの活用方法で解説しています。
よくある質問
「サイクロマチック複雑度」「循環的複雑度」「サイクロマチック数」は別の指標ですか?
すべて同じ Cyclomatic Complexity を指す訳語・表記ゆれで、指標としては同一です。英語表記は Cyclomatic Complexity、提唱者の名を取った McCabe Complexity という呼び方もあります。日本語の技術文書や学術資料では循環的複雑度、ツールのドキュメントではサイクロマティック複雑度が使われる傾向があります。ツールの設定キーやCIログでは cyclomatic-complexity や CC と略記されます。
ESLintとSonarQubeで値が違うとき、どちらが正しいのですか?
どちらも正しく、比較すべきものではありません。SonarQubeが目立つ位置に出すのは認知的複雑度(既定15)で、ESLintのcomplexityが測るサイクロマティック複雑度(既定20)とは指標そのものが別です。同じサイクロマティック複雑度どうしでもswitchや論理演算子の数え方が違うため、値は一致しません。しきい値は使用するツールの数え方に合わせて決め、ツール間で数値を突き合わせないでください。
クラス単位やファイル単位の複雑度は何に使えますか?
リファクタリング対象の絞り込みに使います。関数単位の値はテストケース数の見積もりに直結しますが、クラス単位の合計値は「責務を持ちすぎているクラス」の検出に向きます。CA1502 も Type や Namespace 単位でしきい値を設定できます。ただし合計値は関数の本数が多いだけでも大きくなるため、合計と最大値の両方を見て、特定の関数が突出しているのか全体的に散らばっているのかを切り分けてください。
複雑度を下げたのにバグが減らないのはなぜですか?
複雑度が測るのは制御フローの分岐の数だけで、データの取り扱いの誤りや仕様の取り違えは対象外だからです。分岐を減らす目的だけで処理を小関数へ機械的に切り出した場合、判定点が呼び出し先へ移動しただけで全体の経路数は変わっておらず、テストの網羅性も上がっていません。値の改善そのものを目標にせず、テストケースの追加や責務の整理とセットで進めてください。
複雑度が数百から千を超えるような値は異常ですか?
単一の関数でその値なら、判定点が数百個あることを意味するので分割対象です。radonの基準ではCC 41以上が最低ランクのFなので、桁が1つから2つ違います。ただし測定単位を先に確認してください。クラス単位やファイル単位で合計値を出す設定になっていると、個々の関数の値は正常でも合計が数百に達します。関数単位の値かどうかをまず確かめます。