対応コードメトリクス

この章は、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
TokensTree-sitter のリーフトークン数(サイズの代理指標)Lizard ツール、Terry Yin
WMCクラスの全メソッドにわたる循環的複雑度の合計Chidamber & Kemerer, 1994

ABC

ABC メトリクスは、コード片のサイズを 3 次元ベクトルとして測定します。各成分は 1 種類の操作をカウントします:

  • Assignments(代入)— 値を変数に格納するあらゆる操作で、複合代入(+=++)や明示的な初期化を含みます。
  • Branches(分岐)— 関数およびメソッドの 呼び出し です。名前に反して、これは条件ジャンプの数ではありません。制御が他のコードへと分岐していく地点の数です。
  • Conditions(条件)— ブール判定です。比較演算子(==!=<=>=<>)、三項演算子(?)、および固定のキーワード集合(elsecasetrycatch)が対象です。default / ワイルドカードアームはどの言語でもカウントされません(後述の言語別の逸脱を参照)。短絡論理演算子 &&|| は単独ではカウントされず、代わりに && / || チェーンの比較でない各オペランドが、Fitzpatrick の「単項条件式(unary conditional expression)」規則によって 1 条件として寄与します。次の節では、規則、言語別の逸脱、および実例を順に説明します。

このメトリクスは、Jerry Fitzpatrick が 1997 年の C++ Report 誌の記事 Applying the ABC metric to C, C++ and Java で導入しました。現代の言語で何を ABC としてカウントするかの規則を含む現行の正準仕様は、Fitzpatrick の Software Renovation サイトで維持されています。

カウント規則

Fitzpatrick の論文は規則を 3 つの図で列挙しています — Figure 2(C)、Figure 3(C++、Figure 2 を拡張)、Figure 4(Java)です。Big-code-analysis はこれらの規則集合を言語ごとにそのまま実装しています。以下の表は、各コンポーネントで何がカウントされるかを、各行をそれを導入した図に帰属させてまとめたものです。

代入

規則A としてのカウント初出
単純代入(=出現ごとに 1Figure 2(C)
複合代入(+=-=*=/=%=<<=>>=&=\|=^=出現ごとに 1Figure 2(C)/ Figure 4(Java)
Java の符号なし右シフト代入(>>>=出現ごとに 1Figure 4(Java)
前置または後置のインクリメント / デクリメント(++--出現ごとに 1Figure 2(C)
初期化を伴うコンストラクタ呼び出し出現ごとに 1Figure 3(C++)

分岐

規則B としてのカウント初出
関数またはメソッドの呼び出し呼び出し箇所ごとに 1Figure 2(C)/ Figure 4(Java)
new 演算子出現ごとに 1Figure 3(C++)/ Figure 4(Java)
delete 演算子出現ごとに 1Figure 3(C++)
goto labelbreak labelcontinue label出現ごとに 1Figure 2(C)/ Figure 3(C++)/ Figure 4(Java、ラベル付き break / continue のみ — Java に goto はありません)

条件

規則C としてのカウント初出
比較演算子(==!=<=>=<>出現ごとに 1Figure 2、Rule 5
三項演算子 ? :出現ごとに 1Figure 2、Rule 5
elsecase出現ごとに 1Figure 2、Rule 5
プリプロセッサの #else#elif出現ごとに 1Figure 2、Rule 5
trycatch出現ごとに 1Figure 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 か所で明示しています。

  1. Listing 2(am >= 0 && am <= 0xF) ? '/' : 'C'accc と注記しています — 代入 1 つと条件 3 つで、3 つの条件は 2 つの比較(>=<=)と三項演算子(?)です。&& 自体の寄与はゼロです。
  2. 一方 Rule 7 / Rule 9 は各オペランドをカウントします。if (x || y) printf("test failure\n"); について、論文は「xy の両方が条件式として評価されるため、単項条件は 2 つある」と記しています。ここでも || の寄与はゼロで、xy がそれぞれ 1 ずつ寄与します。

言語別の逸脱

言語ごとの impl Abc ブロックは、その言語に相当する構文がない場合や、論文の規則を字義どおり適用すると過剰カウントになる場合に、規則集合を絞り込んでいます。

言語逸脱理由
C、Go、Rusttry / catch を省略文法に try/catch キーワードがありません。エラー処理には errno / Result / Result 系の直和型を使います。
Rubycatch の代わりに RescueRuby の例外処理キーワードは rescue であり、AST ノード Rescue が Java の catch の役割を果たします。
全言語default / _ ワイルドカードアームを条件集合から除外Fitzpatrick の Figure 2 は default を挙げていますが、これは無条件にフォールスルーします — カウントすると、本体に関係なくすべての switch / matchC が膨らみます。big-code-analysis はすべての言語でこれを省略します(Rust の _ => アームや Java の default: アームも含みます)。
Tclチェーンオペランド・裸の真偽値・三項演算子のスロットは実装済み。引数と return のスロットは未実装expr {…} 内の && / \|\| 連鎖の各被演算子は 1 条件として数えられるため、if {$a && $b} は 2 を報告します。if / elseif / while はその expr {…} 述語をルーティングするため、素の真偽値判定である if {$a} は 1 を報告し、if {!$a} も同様です — これは C の if (a) と一致します(#1180)。コマンド置換として書かれた述語はそのコマンドの結果に対する真偽値判定であるため、if {[somecmd]} も 1 を報告し、冗長な if {[expr {$a \|\| $b}]} イディオムは 3(連鎖の 2 つの被演算子と置換)を報告します。引数と return のスロットはルーティングされないままです。独立した expr {…} コマンド経由でのみ到達される否定は依然として 0 を報告します。
iRulesチェーンオペランド・裸の真偽値・三項演算子のスロットは実装済み。引数と return のスロットは未実装&& / \|\| / and / or チェーンの各オペランドが 1 条件としてカウントされる(Rule 9)ため、if {!$a && !$b} は 2 を報告します。iRules は Tcl にはない語形式の文字列一致比較子(containsstarts_withends_withequalsmatches など)も認識します(Tcl の eq / ne / in / ni は共通です)。裸の真偽値と三項演算子のスロット振り分けは Tcl の兄弟実装と完全に一致します(#1180)— 引き続き未配線の引数と return のスロットを含め、共通の詳細はその行を参照してください。
Phase 2 の全言語(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. C# is the one exception to the argument-list half of the rule: its grammar wraps each argument in an argument node that the walker does not descend, so M(!a, !b) reports zero where every other language in this row reports two. if (true) and return !x behave as stated.
三項演算子のスロット: Java、Groovy、C#、C、C++、Objective-C、Mozcpp、JavaScript、TypeScript、TSX、Mozjs、PHP、Perl、Ruby、Python、Tcl、iRulesa ? !b : !c はその条件と両方の分岐被演算子をカウント同じウォーカーは三項演算子の 3 つの被演算子スロットにも適用されるため、a ? !b : !c は 1 ではなく 4(? に加えて 3 つの単項条件)を報告します。Tcl と iRules は文法フィールドなしで同じ 4 に到達します。これらの ternary_expr はフィールドを一切公開せず、_expr( … ) をインライン化するため、スロットはインデックスではなく ?: のトークンからの相対位置で特定されます(#1180)。Python は別の経路で同じ合計に到達します。not の被演算子は出現箇所を問わず NotOperator 規則によってカウントされるため、ウォーカーを通してルーティングされるのは条件スロットだけであり、(not b) if a else (not c) も同様に 4 を報告します。三項演算子を持たない言語(Rust、Go、Kotlin、Lua、Elixir)は影響を受けません。
for header condition slot: Java, Groovy, C#, C, C++, Objective-C, Mozcpp, JavaScript, TypeScript, TSX, Mozjs, PHP, Go, Perlfor (; a; ) counts one condition; for (;;) counts noneThe loop header's condition is a decision-bearing slot like an if predicate, so for (; a; ), for (; !a; ) and for (; (a); ) each report one. A comparison-shaped condition (i < n) is already counted by its own operator arm and is not double-counted. Every one of these grammars except Go exposes a condition field on for_statement (Perl's sits on its C-style for_statement_1), so the slot is read by field name rather than by child index; Go's field sits on the nested for_clause, and its outer header slot is located structurally (first named child that is neither the body nor a comment). An empty condition counts zero in every language: Fitzpatrick's rules count conditional operators and unary conditions that are present, and an omitted test is not a decision. Java and Groovy previously scored for (;;) as one vacuously-true condition and no longer do. Tcl, iRules and Bash are command-dispatched grammars whose loop headers carry no boolean-expression slot to route (grammar-dispatch §9) (#1276).
Ruby裸の述語の if / unless / while / until(ブロック形式と修飾子形式)は 1 条件としてカウント慣用的な Ruby は裸の述語(if flagx if flag)を好みます。条件スロットをカウントすることで、ABC の条件数が Ruby の循環的複雑度の判断数以上に保たれます(他の言語でも同じ整合が強制されています)。述語内の比較(if a == b)や && / \|\| チェーンは、それ自身の演算子 / ウォーカーのアームでカウントされ、二重カウントはされません。
Bashif / elif / while と、ワイルドカードでない各 case アームは 1 条件としてカウントBash の述語は「コマンド」であるため、埋め込まれたブール式ではなく、分岐キーワード自体が条件のシグナルです。それぞれが Bash の循環的複雑度の判断に対応します。裸の *) case アーム(default: に相当)は除外され、循環的複雑度の標準カウントに合わせています。したがって算術三項演算子 $(( a ? b : c )) は何も加算しません。分岐キーワードを持たないため、規則の隙間から漏れるのではなく、規則群の対象外に位置づけられているのです。
Kotlincatch に加えて try も 1 条件としてカウントFitzpatrick は両方のキーワードをカウントし、Java / C# / C++ / Groovy はすでに両方をカウントしています。Kotlin は以前は catch ブロックのみをカウントしていました。
C#, Java, Rust, Python, Ruby, ElixirA pattern-match guard is a condition slot, scoring one however it is spelledA guard is a branch: the pattern can match while the guard fails. The guard's expression is scored exactly as an if condition is — one for a call, type test, attribute or bare identifier, one via the operator arm for a comparison — so every spelling agrees, and a compound guard (when a > 1 && b < 2) keeps its sub-structure rather than collapsing to one. Before this, ABC scored whatever operator happened to sit inside, so when x > 2 counted one and the equivalent when IsEven(x) counted none. The spellings, per language: C# when_clause on a switch arm or case section and catch_filter_clause on a catch (#1422); Java 21's guard on a pattern-switch label, in both the arrow and colon forms; Rust's match_pattern guard; Python's case … if g:; Ruby's if_guard / unless_guard on a case … in arm; Elixir's when operator on a stab_clause head or a def / defguard head (#1454). The same change made the guard a cyclomatic decision wherever it was not already one — Java, Ruby and Elixir — and excluded Elixir's typespec when (@spec f(a) :: a when a: integer), which spells the same token as a guard but is type syntax. Groovy and Kotlin are absent because neither pinned grammar has a guard production at all: Kotlin 2.1 guard syntax does not parse, so there is nothing to classify (#1454).
KotlinA subject-less when arm scores through the predicate slot; a subject-ful arm scores per entryA subject-less arm's condition (when { x > 5 -> … }) is an ordinary boolean expression, compiled as an if predicate, so the comparison inside it is already counted by the operator arms and a blanket per-entry increment double-counted it. A subject-ful arm (when (x) { in 1..2 -> … }) lists a pattern rather than an independent boolean expression, so the implicit subject == pattern is a decision nothing in the source spells and the entry itself pays for it (#1421).
C++, Objective-C, Mozcpp, Rust, Go, Java, Groovy, C#, Kotlin, TypeScript, TSX, JavaScript, Mozjs, Lua, Perl, Ruby, Bash, ElixirA < or > that is not a comparison is not a conditionEvery one of these grammars spells at least one non-comparison construct with the same bare < / > token a comparison uses, so the comparison rule is gated on the token's parent. What that excludes, per family: template and generic brackets in C++, Objective-C, Mozcpp, Rust, Go, Java, Groovy, C#, Kotlin, TypeScript and TSX (#1274); JSX tag delimiters in TypeScript, TSX, JavaScript and Mozjs; Lua 5.4 variable attributes (local x <const> = 1); a C# comparison-operator overload's declared name (operator <); Kotlin's qualified super call (super<A>.g()) (#1297); Perl's filehandle and lexical-handle readlines (<FH>, <$fh> — but not <STDIN>, which the grammar lexes as one token); Ruby's superclass clause (class Foo < Bar) and comparison-operator method names (def <(other)), and Bash I/O redirection (cmd > out) (#1280); Elixir's sigil delimiters (~s<hi>) (#1256). C# additionally excludes the operator of a relational pattern (x is > 0, and the > 5 => arm of a switch expression): the arm or if condition slot that owns the pattern already scores the decision, so counting the operator too charged a relational arm twice what the constant arm 5 => 1 scores (#1383). Its >= / <= spelling is excluded by the same rule through a separate token. The declared name of an operator overload is excluded on every spelling too: C# overloads six comparison operators and gives each a distinct token, so operator <=, operator >=, operator == and operator != join operator < and operator >, which alone were excluded before #1420. The exclusion is on the operator alone, not on the test that encloses it: since #1461 the is test itself is a condition wherever it is written, so bool b = x is > 0; and return x is > 0; each score 1 — level with the x > 0 they are sugar for, and the same 1 a when n is > 5 guard has scored since #1422 made the guard a condition slot. Counting the pattern's operator as well would make a relational arm worth twice the constant arm 5 => 1. PHP, Python, Tcl and iRules emit a bare < / > from no non-comparison production; C carries the same gate as its C-family siblings although, having no templates, it has nothing to exclude. The gate is a claim about the grammar's productions, not about every parse: where a grammar resolves a generic call into nested binary_expression nodes, as tree-sitter-kotlin-ng does for id<Int>(a), no polarity can exclude it (#1394).
Java, Groovy, C#, TypeScript, TSXA ? used as type syntax is not a ternaryIn each of these grammars the ternary ? and the type-syntax ? are the same anonymous token, so the ternary rule above is gated on the token's parent. Java and Groovy exclude the wildcard bound List<? extends T> (#1274); C# excludes the nullable type int? x and the constraint where T : class?; TypeScript and TSX exclude optional parameters, properties, methods, class fields and tuple elements, and conditional types (T extends U ? X : Y, which the type checker resolves and erases before runtime, so it is no more a branch than the < / > already excluded) (#1275). Safe navigation is untouched: C#'s a?.b shares the same token and still counts, while the other languages spell theirs as a distinct one.
C#, Java, Groovy, Kotlin, Ruby, ElixirA relational operator scores by use; a value-bearing operand scores in a boolean slotFive of these grammars spell at least one relational construct as its own production rather than as a binary expression with an operator token: C#'s x is int and x is null, Java's and Groovy's x instanceof T, Groovy's a in l, Kotlin's a is T and a in 1..2, and Ruby's one-line a in Integer. Having no token to count, each was reached only through the language's terminal-operand set, which the walker consults inside an if / while / ternary / &&-operand slot and nowhere else — so var b = x == 1; scored 1 while var b = x is int; scored 0. Fitzpatrick's Rule 5 counts a relational operator wherever it appears, so each now counts wherever it appears and has left the operand set; being in both would score it twice. Groovy's spaceship <=> counts on the same rule, as it already did in Ruby, PHP, C++ and Mozcpp, although its result is an integer rather than a boolean — what Rule 5 measures is the comparison, not its type. Elixir reached the same 0 by the other route: its membership and type tests (a in [1, 2], rescue e in RuntimeError) do carry an operator token, and simply had no arm matching it. They count by use on the same rule, gated on the token's parent so that an operator merely named (&in/2) stays excluded alongside < and >. The converse still holds for a construct whose value fills the slot: a cast, a Go type assertion, a Rust matches! / cfg! macro and a Python walrus are all operands and count only where a slot reads them as a predicate (#1461).

実例

次の C 関数を考えます。

char digit_or_C(int am) {
    char c;
    if (am >= 0 && am <= 0xF) {
        c = '/';
    } else {
        c = 'C';
    }
    return c;
}

関数本体を走査すると、次のようになります。

トークン / 構文コンポーネント理由
am >= 0C += 1比較(Rule 5、>=
am <= 0xFC += 1比較(Rule 5、<=
&&論理演算子 — 単独では寄与しません。
if/elseC += 1else キーワード(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_rangeforce_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 つのコンポーネントそのもの(assignmentsbranchesconditions)。
  • マグニチュード |<A,B,C>| = √(A² + B² + C²)。Fitzpatrick がベクトルを単一の数値へ要約する方法として推奨しているものです。

The full serialised output (src/metrics/abc.rs) emits these four together with value (the per-space magnitude the CLI thresholds against, which equals magnitude at a leaf space), the per-component averages (assignments_average, branches_average, conditions_average), and per-component *_min / *_max at the file scope, for fourteen fields total. The metric is specialised per language in big-code-analysis-ast/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 つの規則を適用します。

  1. 「省略形」の制御フローは不問にします。 単一のブロックへ誘導するだけの構文 — ネストのないトップレベルの if、自前の条件を持たない elsefor のヘッダ、?: 三項演算子 — は、それぞれ基本の +1 を加えますが、そのパターン自体へのペナルティはありません。
  2. 線形フローの中断にはペナルティを課します。 すべての ifelse ifelseswitchtry/catch、ループ、ジャンプ(gotobreak labelcontinue label)、再帰呼び出しは、少なくとも +1 を加えます。
  3. ネストを罰します。 すでにネストされたブロックの 内側 に制御フローが現れるたびに、メトリクスは ネストの深さごとに 追加の +1 を加えます。メソッド内の外側の if の中の for の中の if1 + 2 + 3 = 6 になりますが、同じ 3 つの構文をフラットに並べた場合は 1 + 1 + 1 = 3 です。

同一のブール演算子の連なり(a && b && c)は、&& の連鎖は単一の && より読みにくいわけではないという理由で、連なり全体で +1 になります。演算子が切り替わる(a && b || c)ところで認知負荷が跳ね上がるため、2 つ目の演算子は独自の +1 を得ます。

big-code-analysis は、関数ごとの構造スコアに加えて、ファイル全体の summinmax と関数ごとの 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)の加算を省略します。

  • 構文上の関数定義ノードを持つすべての言語で、ネストした関数 — ローカル関数、またはローカル / 内部クラスのメソッド — は、その境界でネストカウンタをゼロにリセットし、関数深度の加算を追加します。そのため、その内側の制御フローは、外側の関数のネストではなく、ネストした関数自身の深さに対してスコアされます。したがって、バイト単位で等価な構文は言語間で同一のスコアになります。

  • ラムダやクロージャ(x -> …|x| …lambda x: …、Ruby のブロック、Objective-C のブロック)は、その意味での関数境界ではありません。外側のネストを置き換えるのではなく、その「上に」上乗せ分を追加するため、if の内側に書かれたラムダの中の決定には両方の分が課されます。

  • この上乗せ分は、次の関数境界で止まります。クロージャ本体の「内側で宣言された」関数は新しいレキシカルスコープを開くため、クロージャの上乗せ分を継承しません — || { … } の内側の fn g は、その外側にある場合と同じスコアになります。#1187 までこれを適用していたのは JavaScript ファミリーだけだったため、同じ本体でも、2 レベル上にたまたまクロージャがあるかどうかによって Rust、Java、C++、PHP、C# ではスコアが異なっていました。Python はこの形をそもそも表現できません。def は文であり、ラムダ本体は単一の式だからです。

  • Python は、ブール演算子に対して、ブール演算子の連なり自体が得る +1 に加えて、それを囲む lambda 1 つごとに追加の +1 を課します。これを行う言語は他にありません。上乗せ分を支払うのは、あるラムダ本体の中で最も外側にある演算子だけであり、これらのラムダを数える走査は最も近い外側の expression_list で停止します。(ifforwhile でも停止しますが、ラムダ本体は単一の式であるため、ラムダがこれらの文の上に位置することはなく、この 3 つのアームがカウントを変えることはありません。)連なりの増分自体は上記の演算子切り替え規則に従うため、1 つのラムダ内の混合連鎖は増分 2 つと上乗せ分 1 つを支払います。実測値は次のとおりです。

    Python ソースcognitive.sum
    g = 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

    最後の 2 行が境界です。括弧で囲まれた yield は演算子と lambda の間に expression_list を挟み、走査がラムダに到達する前に終了させるため、基本の +1 だけが残ります。また、ブール演算子を含まないラムダはそれ自体では何のコストも生じません。

    Campbell はブール演算子の連なりに基本増分を与え、ネスト増分は与えていないため、これは仕様の実装というより仕様への追加です。Issue #1150 はこの規則をレビューし、意図的に維持しました。Python のブール演算子のコストは、同じコードに対する他言語のスコアとは比較できません。

循環的複雑度(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 + (判断点の数)

「判断点」とは、制御が分岐しうるあらゆるノードのことです。

  • ifelse if、三項演算子 ?:
  • switch / match / select 内の case / when アーム
  • whiledo … 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.tomlcyclomatic_count_try = false(CLI の値はどちらの方向でもこのキーを上書きします)。
  • リポジトリのゲート: 自動検出される bca.tomlcyclomatic_count_try = false を設定します(本プロジェクト自身の make self-scan はまさにこれを行っており、フラグも環境変数も使いません)。このポリシーの切り替えは循環的複雑度の値を変化させるため、同じ変更の中で .bca-baseline.toml を再生成してください。

デフォルトは変わりません — ? は引き続きカウントされるため、公開済みのメトリクス値は保たれます。このトグルは Rust 専用です。他の言語は try_expression を出力しません。

読み方

McCabe の当初の推奨は、1976 年の論文で繰り返し述べられ、NIST の Structured Testing レポート(Special Publication 500-235、1996 年)にも引き継がれているもので、単一の関数の上限を 10 とすることです。これを超えると、分岐カバレッジに必要なテストケース数が不快なほど増大します。

循環的複雑度の定着した用途は次のとおりです。

  1. 欠陥予測。 複雑度は — 完全ではないものの — 関数がバグを含む 確率 とよく相関し、ほとんどの静的解析ツールは CC の高い関数をリスクありとフラグします。
  2. テストカバレッジ計画。 CC はすべての分岐を網羅するために必要なテストケース数の下限であるため、テストチームは工数の見積もりに直接利用します。
  3. リファクタリングのトリアージ。 循環的複雑度はほぼすべてのコード品質ダッシュボードで筆頭に挙げられる「複雑度」の数値であり、長さが同程度に見える 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) — 何かを 行う もの。キーワード(ifreturnwhile)、算術・論理演算子、代入、関数呼び出しの構文、制御フローを担う句読記号です。
  • 被演算子(operand) — 何かで ある もの。識別子とリテラルです。

これらから 4 つの基本カウントを導出します。

JSON キー記号意味
unique_operatorsn1相異なる演算子の数
unique_operandsn2相異なる被演算子の数
total_operatorsN1演算子の出現の合計
total_operandsN2被演算子の出現の合計

シリアライズされた出力では説明的な JSON キー列の名前を使用します。後述の導出メトリクスの公式では、同じ 4 つのカウントに対して Halstead の古典的な n1/N1/n2/N2 表記を使用します。

big-code-analysis はこれら 4 つの数値を src/metrics/halstead.rs で関数ごと・ファイルごとに記録します。言語ごとのトレイトが、トークン単位で演算子か被演算子かを分類します。このルールは括弧や文の区切りのような純粋なレイアウト用句読記号を意図的に除外しており、そのため Halstead の合計は Tokens のカウントと 同じにはなりません

Two classification rules are worth knowing because they are choices rather than consequences, and because several grammars spell the tokens involved the same way they spell real operators:

  • A literal's own delimiters are not operators. A JavaScript regex, a Groovy slashy string, a C++ raw string, a Tcl braced word and a Perl or Ruby pattern each contribute one operand — the literal — and no operator for the punctuation around it. Otherwise the score would move with the author's choice of delimiter, which says nothing about the code. Tcl and iRules need one extra step to honour this, because their grammars spell a braced literal and a braced script body the same way: whether {a b} is a block or a quoted value depends on the command it is passed to, so the classifier reads that command's name. eval {…}, uplevel, after, time, and Tcl's for and switch take scripts and keep their {} operator, as does every construct the grammar models with a node of its own (proc, if, while, foreach, catch, try, namespace, an iRules when handler) — except a defaulted proc parameter (proc p {a {b {x y}}}), which holds data the interpreter assigns rather than a script it runs. Every other command — lappend, puts, list, and any user-defined proc — is taken to receive a value, so its braces score no operator.

    This decides the operator only. The words inside a braced argument are counted either way, because an unrecognised command is as likely to have been handed real code (an oo::class create C {…} body, a tcltest -body {…}) as a list. So a script passed to a command outside the list — a Tk -command {…} callback, a dict for body — gives up one {} occurrence per block and nothing else, with one visible consequence: a space whose only blocks are of that kind has no operator left, and because Halstead's difficulty multiplies by the operator count, its effort reads 0.0 rather than slightly low. A proc keeps its own proc keyword and body brace, so this reaches top-level script fragments, not functions. The remaining asymmetry is that a braced value scores one operand where the grammar names the command (set x {a b}) and one per word where only the command name would (lappend x {a b}); closing that needs a signal neither grammar gives.

  • A string-interpolation opener is not an operator. "{$x}" in PHP, "#{x}" in Ruby and Elixir, "${x}" in Kotlin and Groovy and $"{x}" in C# all count the interpolated expression's own operators and nothing for the opener itself.

導出メトリクス

Halstead はここから数多くの公式を導出します。big-code-analysis は標準的なものすべてに加えて、元のスイートに含まれるあまり一般的でない 3 つの導出値(estimated_program_lengthpurity_ratiolevel)も報告します。

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 つの用途に使われます。

  1. 複合メトリクスへの入力として。最も重要なのは、Halstead の ボリューム に依存する保守容易性指数(次節)です。
  2. 言語に依存しないサイズの代理指標として。ビット単位のボリュームは、LOC では実現できない形で言語をまたいで滑らかにスケールします。
  3. 相対的な工数見積もりのため。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 の項目は、この物理行対論理行の区別を読みやすくまとめています。

バリアント数える対象
SLOCSource Lines Of Code — コメント・空行・コードを問わずファイル内のすべての行
PLOCPhysical Lines Of Code — 空行でもコメントのみの行でもない行
LLOCLogical Lines Of Code — 文を含む行(定義、代入、宣言)
CLOCComment 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 オブジェクトの下に originalseivisual_studio というキーでネストされます(ドット区切りのしきい値名は mi.originalmi.seimi.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 は計算済みの HalsteadCyclomaticLOC メトリクスを受け取り、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 splits the count by callable kind: every aggregate is reported separately for functions and closures so a Rust file heavy on |…| … closures and a Java file with only methods produce comparable numbers. The serialised output (src/metrics/nargs.rs) is function_args, closure_args, function_args_average, closure_args_average, total, value, average, function_args_min, function_args_max, closure_args_min, closure_args_max. The implementation handles default arguments, variadic arguments, keyword-only arguments, and destructured parameters consistently per language. Comments written inside the parameter list are not parameters, including the C++ idiom that puts one where an unused parameter's name would go — void f(int /*unused*/) is one argument.

Macro-obscured C-family declarators

C, C++, Mozcpp and Objective-C are parsed without running the preprocessor, so a function-like macro standing where the declared name belongs is still there in the tree. The idiom is the JNI shim:

#define RUN_STATS_METHOD(name) JNICALL Java_org_tensorflow_RunStats_##name

JNIEXPORT jlong RUN_STATS_METHOD(allocate)(JNIEnv *env, jclass clazz) { … }

Read as written, RUN_STATS_METHOD(allocate) is the declarator and (JNIEnv *env, jclass clazz) belongs to the return type — one argument. big-code-analysis reads it the other way and reports two, because neither language lets a function return a function type (C11 6.7.6.3p1, C++ [dcl.fct]): a declarator nested directly inside another one cannot be a declarator chain, so the outer list is the function's own. Every legitimate function-returning-a-function-pointer form — int (*fp(int a, int b))(int c) — puts parentheses in between and is unaffected, as is C++ operator().

The space is named for the macro, not the function. There is no other candidate: the real symbol is assembled by ## token pasting and never appears in the source. So several shims in one file share a name, which bca check shows as several rows with the same label at different lines, and which .bca-baseline.toml disambiguates by body hash the way it does an overload set.

しきい値ゲートが測定するもの

bca check --threshold nargs=N は、各呼び出し可能要素をそれ自身のパラメータリストでゲートします。これは同種のツールすべてが測っているものです — RuboCop の Metrics/ParameterLists、ESLint の max-params、Clippy の too_many_arguments、lizard、SonarQube の S107、Pylint の R0913 は、いずれも呼び出し可能要素を 1 つずつ数えます。

これはシリアライズされた total では「ない」ことに注意してください。上記の function_args キーと closure_args キーはサブツリーの合計です。パラメータを 2 つ宣言し、3 パラメータのネストした関数を含む関数は function_args: 5 を報告します。#1196 より前は、ゲートがこの合計を読んでいたため、2 パラメータのソート比較関数を持つ 3 パラメータの関数は 5 でフラグされました — そして、その数字が示唆する対処であるパラメータ削減は、違反を解消できる対処ではありませんでした。

The number the gate does read is serialized as value, added in #1236 so a JSON-consuming front-end can reproduce the gate. Until then the Python to_sarif binding had nothing but total to compare and reported breaches bca check did not.

この狭い方の規則から漏れるものはありません。クロージャが独自のスペースを開く12 の文法 — Rust、JavaScript、TypeScript、TSX、MozJS、C#、Go、PHP、Perl、Ruby、Lua、Elixir — では、クロージャは独自の違反者行でゲートされ、修正もそこに属します。Python、Java、Kotlin、C++ ではラムダはスペースを開かないため、その引数は外側の関数にしか帰属できません。その場合、違反者行は内訳を表示します。

small: nargs = 8 (1 own + 7 lambda) (limit 5)

そのため、手を入れるべきはシグネチャなのかラムダなのかがひと目で分かります。

0 と表示される言語

黙って 0 を報告するメトリクスは「測定されていない」ではなく「違反者なし」と読めてしまうため、カウントが不活性な箇所を知っておく価値があります。

  • Bash — 正しい挙動であり、恒久的です。シェルには形式的なパラメータリストがなく、引数は $1$2 などとして届きます。すべての関数が 0 と表示される唯一の言語です。
  • シグネチャのない Perl のサブルーチン — 正しい挙動です。シグネチャ(sub add($x, $y))はカウントされます。@_ から引数を読み取るサブルーチンは、数えるべき形式パラメータを宣言していません。
  • Perl の無名サブルーチン — これはギャップであり、しかも上流のギャップです。文法が無名サブルーチンのシグネチャをエラーノードの内側にパースするため、my $f = sub ($x) {…} はシグネチャを持っているにもかかわらず 0 と表示されます。

したがって nargs の制限は、Bash のコードベースでは不活性であり、use v5.36 以前に書かれた Perl ではまばらにしか効きません。リポジトリ全体ではなく言語ごとにゲートしてください。しきい値の選択を参照してください。

読み方

引数の多い関数は正しく呼び出すのが難しく、網羅的にテストするのはさらに困難です。テストマトリクスはおおよそ指数関数的に増大します。古典的なリファクタリングの助言は パラメータオブジェクトの導入 パターンです。関数が 4 つを超える関連した引数を取る場合、それらをレコード / 構造体 / データクラスにまとめます。

発展的な用途は、レビューをブロックする lint ルールとしてです。現代的なリンターの多く(pylintR0913、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 トレイトを参照)、関数ごとのカウントに加えてファイルレベルの sumaverageminmax を報告します。シリアライズされたフィールド名は nexits で、本文で使っている略語表記と一致します。

読み方

厳格な SESE コーディング標準(アビオニクス向けの DO-178C、組み込み自動車向けの MISRA C — MISRA の公式サイト を参照)は、今でも関数あたりの NExits を 1 とすることを要求しています。複数の出口点は、認証対象の制御フロー解析を複雑にするためです。これらの領域の外では、NExits が 2-4 であることはたいてい 良い 兆候です。ほとんどの場合、その関数がガード節で事前条件を処理し、その後フラットな本体で処理を進めていることを意味するからです。

非常に 高い NExits — たとえば 8 超 — は警告のサインです。たいていの場合、その関数は複数の小さな関数に分割されるべきであり、それぞれの「成功する分岐」が独自のヘルパーになるべきだったことを意味します。

NOM

NOMNumber 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 で、呼び出し可能要素の種類ごとに分割してカウントを報告します。シリアライズされるフィールドは functionsclosuresfunctions_averageclosures_averagetotalaverage(内包するスペース全体での平均)、および種類ごとの functions_minfunctions_maxclosures_minclosures_max です。

この分割により、同じコードに対して異なる問いを立てられます。クロージャが多く関数が少ない Rust クレートはイテレータ中心のコードに典型的であり、関数が多くクロージャが少ない Python モジュールはスクリプト風のコードに典型的です。

実行可能なコードを含みながら、どちらのフィールドにも意図的にカウントされない構文があります。Kotlin のプロパティアクセサ(get() / set())と init ブロック、Java と Groovy の static { … } 初期化子、JavaScript のクラス静的ブロックです。それぞれは関数「スペース」を開きます — そのため独自の複雑度スコアを持ち、bca check はそれをフラグでき、WMC にも寄与します — が、いずれも呼び出し箇所で名前を挙げて呼べる呼び出し可能要素ではなく、アクセサをメソッドとして数えると、NPM が同じプロパティを属性として 1 回、メソッドとしてもう 1 回計上することになり、OOP メトリクスが報告するために存在する NPA/NPM 比を歪めてしまいます。したがって、プロパティアクセサだけからなる Kotlin ファイルは、その複雑度を報告しつつ nom.functions == 0 を報告します(#1184)。

読み方

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_attributesclass_cda(クラスの公開属性密度 — 平均ではなくアクセシビリティの 比率)、interface_cdatotaltotal_attributescda です。言語ごとの Npa トレイトが、何を「公開」と見なすか(Java の public、C# の public、Rust の pub、Python の「先頭にアンダースコアを付けない」慣習など)、そして何を「メソッド」ではなく「属性」と見なすかを決定します。

Which spaces carry NPA, NPM and WMC

NPA and NPM are emitted on container spacesclass, struct, trait, impl, namespace, interface — and on the whole-file unit root, which carries the roll-up across every container in the file. WMC follows the same rule wherever it is computed at all, which is narrower in two ways set out below. A function space does not carry any of them: a method owns no methods or attributes of its own, so the block would be all zeros. Before big-code-analysis 2.1.0, NPA and NPM did emit that all-zero block on function spaces in C#, JavaScript, MozJS, TypeScript, TSX, PHP and Ruby, and on the Kotlin, Java, Groovy and JS-family accessor / init / static spaces. In the same release Go, Rust, Python, C++, Objective-C and Elixir went the other way: they decided from their own grammar node kinds, so a struct declared inside a function put the block on that function space, while a namespace or a file root with no container at file scope carried none. Both deviations are gone — the space's kind is now the only input, for every language.

That rule governs the block, not the numbers behind it: the counts roll up through every enclosing space regardless. So a type declared inside a function body is reported by the nearest enclosing container, or by the file root when there is none.

Four things read differently. A Go file's NPA and NPM live on the unit root and nowhere else, because Go is the one language that emits them without having a container kind in its space tree — type … struct and type … interface open no space of their own. (Bash, C, Lua, Perl, Tcl and iRules have no container kind either, but they emit neither block anywhere, so the question does not arise.)

Neither WMC narrowing moves NPA or NPM, which is why the rule above still holds as stated for those two. The first is language-level: Go emits no wmc block at all, on any space including the unit root, because its flat space model cannot attribute a method to a receiver class — so a Go file carries npa and npm at the root and wmc nowhere. The second is space-kind-level: a namespace space carries npa and npm but no wmc, because a namespace's member functions are free functions rather than methods of a class, so there is no per-class complexity to weight. That covers every construct mapping to SpaceKind::Namespace — a C++ or Mozcpp namespace, and a Ruby module — not just the C++ spelling. Objective-C has no namespace construct of its own, so the case does not arise there. The class inside the namespace carries all three, and so does the file root.

Finally, the CSV projection is a fixed-column format: it writes the npa.* / npm.* columns on every row regardless of space kind, carrying the real accessor values rather than eliding them.

Thresholds are narrower still — bca check gates npa / npm on container spaces only, never the file root (see Threshold scope). Taken with the paragraph above, that has a consequence worth stating outright: since a Go file's NPA and NPM are only ever reported at the root, no npa or npm threshold can fire on Go source.

読み方

NPA はカプセル化の 直接的な 尺度です。すべての公開属性は、呼び出し側がメソッドを介さずに読み書きできる内部状態であり、つまりクラスが呼び出し側を壊さずには検証も進化もさせられない内部状態だということです。標準的な指針 — Bertrand Meyer の Object-Oriented Software Construction(Prentice Hall、1988 年)で最初に明示的に述べられ、* 統一アクセスの原則_ として知られています — は、NPA をゼロまたはその近くに保ち、代わりに公開メソッドを通じて状態を公開することです。

発展的な用途は API 安定性の監査です。時間とともに NPA が増えていく公開ライブラリのクラスは、公開メソッドの表面よりも速いペースで破壊的変更の負債を蓄積します。

NPM

NPM は、クラスまたはインターフェイスが宣言する公開メソッドの数を数えます。NPA のメソッド側の対になるもので、こちらも Lorenz と Kidd(1994 年)によって体系化されました。

As with NPA, big-code-analysis splits NPM by definition-site kind (classes vs. interfaces). The serialised output (src/metrics/npm.rs) is class_npm_sum (sum of NPM across classes), interface_npm_sum, class_methods (sum of all methods — public or not — across classes), interface_methods, class_coa, interface_coa (operation-accessibility ratios, not averages), total, total_methods, and coa. It follows the same emission rule as NPA (above). The language-specific Npm trait decides what counts as public — for example, Rust's pub, Python's leading-underscore convention, C++'s public: section — and folds together regular methods, constructors, and operator overloads as appropriate.

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

WMCWeighted 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 つの仮説を提示し、そのすべてがその後繰り返し検証されてきました。

  1. WMC が高いほど保守工数の増加を予測します。 個々のメソッドが複雑なクラスは、理解しにくくなります。
  2. WMC が高いほど再利用が減ります。 多くの複雑なことを行うクラスは、新しいコンテキストに投入しにくいのです。
  3. WMC が高いほどアプリケーション固有の振る舞いが広いことを示唆します。 そのようなクラスは、再利用可能な部品ではなく「メインループ」型のコーディネーターになりがちです。

発展的な用途は God クラス検出です。NOM と組み合わせると、WMC はクラスを分割すべきだという最も明確なシグナルの 1 つになります。NOM が高く WMC が低いクラスは受動的なデータホルダーです(おそらく問題ありません)。NOM が低く WMC が高いクラスは、少数の巨大なメソッドを持っています(クラスではなくメソッドを分割してください)。NOM と WMC の 両方 が高いクラスが、古典的な God クラスです。


次のステップ

  • 対応言語 の章では、対応しているすべての言語と文法を一覧しています。一部のメトリクス定義(NPANPMWMC)はクラスを持つ言語でしか意味をなさないため、メトリクスの対応範囲は言語によって異なります。
  • 対応する変更履歴(VCS)メトリクス の章では、ソースの AST ではなくバージョン管理履歴から導出される補完的なファミリー — コミット頻度、チャーン、所有権、複合的なリスクスコアとホットスポットスコア — を扱います。
  • Commands → Metrics のページでは、これらの数値の JSON / YAML / TOML / CBOR 出力を生成するための bca metrics の呼び出し方を説明しています。
  • レシピ の章では、これらのメトリクスから品質レポートを生成するエンドツーエンドの例を示します。ダッシュボードへのパイプライン連携も含みます。