Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

big-code-analysis

本書には英語版もあります。

big-code-analysis is a Rust library and command line tool (bca) that computes maintainability metrics from source code in many programming languages. It parses with tree-sitter, an incremental parsing library with a grammar per language, so it needs no compiler or language runtime.

This book is task-oriented: metric definitions, the command reference, CI recipes, and guides for the Rust crate and the Python bindings. The source code is on GitHub, and issues and feature requests go to the issue tracker.

対応プラットフォーム

big-code-analysis runs on Linux, macOS, and Windows. The GitHub releases page carries prebuilt binaries for all three.

API ドキュメント

The complete Rustdoc reference for the crate (every public type, trait, and function, including the feature-gated vcs module) lives on docs.rs, generated straight from the source.

For task-oriented guides on embedding the crate (quick start, in-memory analysis, walking FuncSpace results, and error handling), see Using as a Library.

For the PyO3 bindings (pip install big-code-analysis, batch processing, flat-record iteration, SARIF output, and async patterns), see Python Bindings.

ライセンス

  • The vendored grammar crates (tree-sitter-ccomment, tree-sitter-mozcpp, tree-sitter-mozjs, tree-sitter-preproc, tree-sitter-tcl) are released under the MIT license.

  • big-code-analysis, big-code-analysis-ast, big-code-analysis-cli, big-code-analysis-web, and big-code-analysis-py are released under the Mozilla Public License v2.0.

対応言語

This is the list of programming languages big-code-analysis parses. Each entry is a LANG variant (defined by the mk_langs! invocation in big-code-analysis-ast/src/langs.rs) and is gated behind the matching per-language Cargo feature documented in Per-language Cargo features.

  • 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

Every variant’s slug is its LANG::name, lowercase and punctuation-free so it round-trips through FromStr (C# reports csharp). Some entries are variants of a shared grammar pipeline:

  • JavaScript (the upstream tree-sitter-javascript grammar) is the default for .js, .mjs, .cjs, and .jsx files. Mozjs is the Mozilla / SpiderMonkey fork, opt-in: it owns only the .jsm (Firefox module) extension and reports the slug mozjs. The two are metric-equivalent on ordinary JavaScript.
  • Tsx is Typescript (upstream tree-sitter-typescript) with JSX syntax enabled and reports the distinct slug tsx.
  • Since #721 C has its own variant C (slug c, upstream tree-sitter-c), owning .c and the c emacs mode; the C/C++ variant (slug cpp, upstream tree-sitter-cpp since #720) keeps .cpp / .cc / .h and the rest. .h deliberately stays on Cpp: a C++ header through the C grammar ERROR-cascades on class / template, whereas a C header through the C++ grammar only trips on C++-keyword identifiers.
  • The Mozilla/Gecko C++ dialect is the opt-in Mozcpp variant (slug mozcpp), which owns no file extensions and is selected only by name, exactly as Mozjs relates to JavaScript.
  • Since #724 Objective-C (slug objc, upstream tree-sitter-objc) owns .m and the objc / objective-c emacs modes. Objective-C++ (.mm) stays on Cpp. These files mix Objective-C and C++, but tree-sitter-objc cannot parse C++ templates, namespaces, or ::. The C++ grammar struggles only with the Objective-C portions, so it gives the better result, using the same trade-off as .h. Metrics for those Objective-C portions remain approximate.

K&R definitions with a wrapped return type

A pre-ANSI (K&R) function definition opens no function space under C or Objective-C when its return type wraps the declarator; int *, char **, struct S *, and a static pointer return all reproduce it:

int *krptr(a, b) int a; int b; { if (a) { return 0; } return 1; }

tree-sitter-c 0.24.2 builds its old-style function definition from a declarator nested inside the old-style declarator, so an outer pointer_declarator is unreachable and the parser prefers a plain declaration that swallows the first parameter declaration, orphaning the body as a bare compound_statement. No ERROR node is produced: the parse silently succeeds wrongly, and there is no dispatch arm missing on our side.

The consequence is that the body’s decisions are charged to the file’s unit space: the line above reports cyclomatic.sum 2 and nom.functions 0, a file-level count no function-level row accounts for. A K&R definition whose return type does not wrap the declarator (int krplain(a, b) int a; int b; { … }) is unaffected, as is any ANSI definition. C/C++ and Mozcpp open no space for either K&R form, because tree-sitter-cpp has no rule for the syntax at all. This is an upstream grammar limitation, tracked here as #1209.

内部ヘルパーバリアント

The following LANG variants are internal helpers, not user-facing languages. They belong to the C-family analysis pipeline (they ride every C-family Cargo feature: cpp, c, and mozcpp) and are not selected directly when analysing source files:

  • Ccomment: C/C++ comments.
  • Preproc: C/C++ preprocessor macros.

Note: Since #720 the Mozilla/Gecko C++ dialect is exposed as the Mozcpp LANG variant (backed by the vendored bca-tree-sitter-mozcpp crate, pulled in by the opt-in mozcpp feature). It is a public, name-selectable language that owns no file extensions. Unlike the Ccomment / Preproc helpers above, it is not internal.

対応コードメトリクス

This chapter describes every metric that big-code-analysis computes. Each section starts from the original research paper, walks through the algorithm, and explains both how the metric was originally meant to be used and how the industry uses it today. If you are new to software metrics, read the sections in order: the later metrics (the Maintainability Index in particular) are built on the earlier ones (Halstead, Cyclomatic, LOC).

Three framing notes:

  • A metric is a measurement, not a verdict. Every number on this page summarises a structural property of source code. None of them measures correctness, productivity, or developer skill. The first question for any metric is “compared with what?”: the same module a month ago, this module versus its siblings, this codebase versus an industry baseline. Absolute thresholds are rough heuristics.
  • Most metrics here are computed at three scopes: per function / method, per class or unit-like space, and per file. The underlying tree-sitter parser produces a tree of “spaces” (functions, closures, classes, namespaces, and so on) and every metric is rolled up through that tree.
  • オブジェクト指向メトリクスは、オブジェクト指向の構文に対してのみ反応します。 WMC、NPA、NPM は、impl ブロックのない Rust ファイルやクラスのない Python モジュールでは 0 を報告します。これは正しい答えであり、バグではありません。

一覧

メトリクス測定対象初出
ABC<Assignments, Branches, Conditions> として表したサイズFitzpatrick, 1997
認知的複雑度関数を 読む のがどれだけ難しいかCampbell / SonarSource, 2017
循環的複雑度(CC)関数内の独立した経路の数McCabe, 1976
Halstead語彙に基づくサイズ、難易度、労力、バグ数Halstead, 1977
Lines of Code (SLOC, PLOC, LLOC, CLOC, BLANK, DLOC)Raw, physical, logical, comment, blank, and documentation line countsConte, 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 tool, Terry Yin
WMCクラスの全メソッドにわたる循環的複雑度の合計Chidamber & Kemerer, 1994

ABC

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

  • Assignments: anything that stores a value into a variable, including compound assignments (+=, ++) and explicit initialisation.
  • Branches: function and method calls. Despite the name, this is not the count of conditional jumps; it is the number of points where control branches out to other code.
  • Conditions: boolean tests. These are comparison operators (==, !=, <=, >=, <, >), ternary operators (?), and the fixed keyword set (else, case, try, catch). The default / wildcard arm is not counted in any language (see the per-language deviations below). The short-circuit logical operators && and || are not counted on their own; instead, each non-comparison operand of a && / || chain contributes one condition via Fitzpatrick’s “unary conditional expression” rule. The next subsection walks through the rules, the per-language deviations, and worked examples.

Jerry Fitzpatrick introduced the metric in the 1997 C++ Report article Applying the ABC metric to C, C++ and Java. The current canonical specification, including the rules for what counts as an A, B, or C in modern languages, is maintained on Fitzpatrick’s Software Renovation site.

カウント規則

Fitzpatrick’s paper enumerates the rules in three figures: Figure 2 (C), Figure 3 (C++, which extends Figure 2), and Figure 4 (Java). The tables below summarise the paper’s rule set, with each row attributed to the figure that introduces it. Big-code-analysis implements them per language, with the exceptions listed under Per-language deviations and these paper rows that no language implements today: the delete operator, the goto / labeled break / labeled continue jumps, the preprocessor #else / #elif, and the C++ initializing constructor invocation (S s(1);). Each scores zero.

代入

規則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 label、break label、continue label出現ごとに 1Figure 2 (C) / Figure 3 (C++) / Figure 4 (Java, labeled break / continue only, since Java has no goto)

条件

規則C としてのカウント初出
比較演算子(==、!=、<=、>=、<、>)出現ごとに 1Figure 2、Rule 5
三項演算子 ? :出現ごとに 1Figure 2、Rule 5
else、case出現ごとに 1Figure 2、Rule 5
プリプロセッサの #else、#elif出現ごとに 1Figure 2、Rule 5
try、catch出現ごとに 1Figure 3(C++)/ Figure 4(Java)
単項条件式one per && / || operand and per if / while / ternary / guard predicate that no other rule already counts (and per !-wrapped argument or return value)Figure 3、Rule 7 / Figure 4、Rule 9

The short-circuit logical operators (&&, ||, and the per-language equivalents: Ruby and / or, Python and / or, Perl and / or, Lua and / or, Tcl && / ||, iRules && / || / and / or) do not contribute a condition on their own. Each non-comparison operand contributes one instead, via the unary-conditional rule. Perl and PHP xor evaluate both operands, so like ^ they are a value: the slot holding one pays a single condition. The paper makes this explicit twice:

  1. Listing 2 annotates (am >= 0 && am <= 0xF) ? '/' : 'C' as accc: one assignment plus three conditions, where the three conditions are the two comparisons (>=, <=) and the ternary (?). The && itself contributes zero.
  2. 一方 Rule 7 / Rule 9 は各オペランドをカウントします。if (x || y) printf("test failure\n"); について、論文は「x と y の両方が条件式として評価されるため、単項条件は 2 つある」と記しています。ここでも || の寄与はゼロで、x と y がそれぞれ 1 ずつ寄与します。

言語別の逸脱

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

言語逸脱理由
C、Go、Rusttry / catch を省略文法に try/catch キーワードがありません。エラー処理には errno / Result / Result 系の直和型を使います。
Rubycatch の代わりに RescueRuby の例外処理キーワードは rescue であり、AST ノード Rescue が Java の catch の役割を果たします。
全言語default / _ ワイルドカードアームを条件集合から除外Fitzpatrick’s Figure 2 lists default, but it falls through unconditionally, so counting it would inflate C on every switch / match regardless of body. big-code-analysis omits it for every language (the Rust _ => and Java default: arms included).
Tclチェーンオペランド・裸の真偽値・三項演算子のスロットは実装済み。引数と return のスロットは未実装Each operand of a && / || chain inside expr {…} counts as one condition, so if {$a && $b} reports two. if / elseif / while route their expr {…} predicate, so a bare-truthy if {$a} reports one and if {!$a} likewise, matching C’s if (a) (#1180). A predicate written as a command substitution is a truthy test of that command’s result, so if {[somecmd]} also reports one, and the redundant if {[expr {$a || $b}]} idiom reports three (the chain’s two operands plus the substitution). The argument and return slots remain unrouted: a negation reached only through a standalone expr {…} command still reports zero.
iRulesチェーンオペランド・裸の真偽値・三項演算子のスロットは実装済み。引数と return のスロットは未実装Each operand of a && / || / and / or chain counts as one condition (Rule 9), so if {!$a && !$b} reports two. iRules also recognises the word-form string-match comparators (contains, starts_with, ends_with, equals, matches, and so on) that Tcl lacks (Tcl’s eq / ne / in / ni are shared). Bare-truthy and ternary slot routing matches its Tcl sibling exactly (#1180); see that row for the shared detail, including the argument and return slots that remain unrouted.
Java, Groovy, C#, Rust, Go, JavaScript, TypeScript, TSX, Mozjs, PHP, C, C++, Objective-C, Mozcpp, Python, Perl, Luaif (true) {}、m(!a, !b)、return !x はそのオペランドをカウントThese implementations pass if, while, do-while, argument-list and return expressions through the same condition 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# wraps each argument in an argument node, so the walker reads the value after any named-argument label or ref / out / in keyword: M(!a, !b) and M(name: !a, !b) report two, as do the base calls : base(!a, !b) and class S(bool a, bool b) : Base(!a, !b). Before #1537 it read the wrapper instead, and every C# argument reported zero. An indexer argument (o[!a]) is not a call argument and reports zero in C#, as Java’s array index does.
三項演算子のスロット: 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 条件としてカウントIdiomatic Ruby favours bare predicates (if flag, x if flag); counting the condition slot keeps ABC conditions at or above Ruby’s cyclomatic decision count (the alignment enforced across the other languages). A comparison (if a == b) or && / || chain in the predicate is counted by its own operator / walker arm and is not double-counted. Each && / || / and / or operand is scored by the same rule (once, unless its own arm already counts it), so a && self, a && Foo::Bar and a && -b score 2 like a && b, wherever the chain is written (#1529).
Python, JavaScript, TypeScript, TSX, Mozjs, C, C++, Objective-C, Mozcpp, Java, C#, Rust, PHP, Perl, Lua, Groovy, Kotlin, Tcl, iRulesA boolean slot pays one condition unless something inside its predicate already counts itEvery if / while / do / for condition, ternary condition, guard and && / || operand scores the way Ruby’s has since #1520: one condition, unless the predicate (seen through parentheses, a negation, a cast or a plain assignment) is a comparison, chain, ternary or type test that its own rule counts. Before #1526 these slots paid only for a predicate from a fixed list of operand kinds, so if (-x), if (x + 1), if (this), if (*p), if (flags & k), if (y = x) and if (sizeof x) each scored 0 against a cyclomatic decision of 1, as did Java’s, C#’s and Rust’s if (a & b) and a boolean switch / match expression. The same rule stops a wrapped comparison paying twice: if ((int)(x > 1)), Python’s if (y := x > 1): and Python’s if not (x > 0): score 1, not 2, as Java’s if (!(x > 0)) always has. Python’s elif, PHP’s elseif, Lua’s elseif and Perl’s elsif now score like Ruby’s elsif and Java’s else if: one for the else plus the predicate slot, so elif b no longer scores one below elif x > 0. Kotlin joined in #1533, so the slot pays for a when it holds, as it does for Java’s switch, C#’s switch expression and Rust’s match: if (when (a) { else -> true }) scores 1 (it scored 0) and if (when (x) { 1 -> true; else -> false }) 2 (it scored 1), its entry paying 1 and the slot 1. Go still pays a slot through an operand list (#1526).
Bashif / elif / while と、ワイルドカードでない各 case アームは 1 条件としてカウントA Bash predicate is a command, so the branch keyword itself, not an embedded boolean expression, is the condition signal. Each matches a Bash cyclomatic decision; the bare *) case arm (the analogue of default:) is excluded, mirroring the cyclomatic standard count. The arithmetic ternary $(( a ? b : c )) counts one, as the C-family ternary does (#1268). Each comparison inside a test pays one more: ==, !=, the -eq / -z / -f family, and =, which compares in [ … ] / [[ … ]] and assigns in (( … )). The [ … ] connectives -a / -o join two tests as && / || do, and pay nothing, so [ X -a Y ], [ X ] && [ Y ] and [[ X && Y ]] score alike in every decision metric; a bare test X -a Y is all plain words to the grammar and scores none of them (#1536).
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).
RubyA subject-less case scores each when pattern as an || operand; a subject-ful case scores per clauseA subject-less clause (case; when x > 5) lists ordinary boolean expressions, evaluated like an if predicate, and several patterns (when a, b) are an implicit ||. Each pattern therefore scores once unless its own operator already counted it, so when x > 5 scores 1 like if x > 5, and when a, b scores 2 like if a || b. Before this, a blanket per-clause increment counted a comparison twice. A subject-ful clause (case x; when 1) matches each pattern with an implicit pattern === x that nothing in the source spells, so the clause pays once (#1453).
GoA tagless switch scores each case expression as an || operand; a tagged switch scores per caseA tagless switch (switch { case x > 5: }, with or without an init statement) is switch true: each case expression is an ordinary boolean scored by the same slot an if predicate uses, and a list (case a, b:) is an implicit ||. So case x > 5: scores 1 like if x > 5, and case a, b: scores 2 like if a || b. Before this, a blanket per-case increment counted a comparison twice. A tagged case (switch x { case 1: }) compares with an implicit x == 1 that nothing in the source spells, so the case pays once; type-switch and select cases are unchanged (#1523).
ElixirAn if / unless predicate and each cond clause are condition slots; every other clause construct scores per clause; an if / unless else scores oneAn if predicate scores one condition unless an operator inside it already counts it, as Ruby’s does: if x > 5 and if b each score 1, and if a && b scores 2, its two operands. cond is an if / else if chain written as clauses, so each clause’s condition is scored the same way, and cond do x > 5 -> …; b -> …; true -> … end scores 2, like Go’s tagless switch twin. The unguarded true -> catch-all is cond’s default arm and scores nothing, as in cyclomatic. Before this, the keyword call added one condition on top of its predicate, so if x > 5 scored 2 and cond scored once however many clauses it had (#1527). The same change scores every && / || operand and guard that no operator counts, so a && &f/1 now scores 2, like a && b. The clauses of a case, of a with’s else, of a receive, of a try’s rescue / catch / else, and of a multi-clause fn after its head each score one, through the same gate cyclomatic uses: a bare _ -> catch-all outside a fn scores nothing, like Go’s default: and Rust’s _ =>. A typespec’s function type (@spec f((any -> any)) :: list) parses as a clause too and scores nothing in either metric; cyclomatic counted it before. The keyword forms (case(x, do: (1 -> :a; …)), cond(do: (…)), with’s else:) hold their clauses in the same parenthesised block a typespec does, so they are recognised by the do: / else: / rescue: / catch: key that holds the block, and score exactly as the do … end forms. A clause whose pattern an operator already counts does not pay again, so rescue e in RuntimeError -> scores 1, its in. So a two-clause case scores 2, like Ruby’s subject-ful case and Rust’s match; it scored 1 once per construct. Each pattern <- expr clause of a with scores one as well, because it either matches or falls through to the else, so with {:ok, a} <- g(x) do a end scores 1, like Rust’s if let Ok(a) = g(x). A when guard on that pattern is a guard slot like any other. A for generator’s <- is not a clause and scores nothing. A for … reduce: block’s accumulator clauses score like a multi-clause fn’s: the first is free, so a single acc -> … clause scores nothing (#1535). The else of an if / unless, in the block or the else: keyword form, is Fitzpatrick’s Rule 5 else and scores one, as Ruby’s, Java’s, Python’s and Go’s do (#1531). A bracketed keyword list in last position (if(c, [do: 1, else: 2])) is the same argument as the bare one and scores the same, as does a quoted key ("else": 2); a list anywhere else, or one with a non-pair item, holds no sections. A Kernel.&& / Kernel.|| remote call scores its operands as a && b / a || b does, the piped value of a |> Kernel.||(b) included (#1574), and so do Kernel.and / Kernel.or (#1582). A receive has one free arm, as a case does: its bare _ -> when the do section holds one, otherwise its after clause (the timeout, the path taken when no message clause matched in time), which then scores nothing; beside a bare _ -> the timeout is a path of its own and scores one (#1581).
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 TSX, JavaScript and Mozjs (TypeScript carries the same gate through the shared macro, but the .ts dialect of tree-sitter-typescript has no JSX production, so it has nothing to exclude there); 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. C++ and Mozcpp do the same for their seven, operator<=> included, and also exclude the operator a comparison fold spells twice: (0 == ... == a) counts once, like (a == ...) (#1448). An && / || fold applies its operator once as well, and scores what one application of it scores: (true && ... && a) what true && a does, and (... && a), whose unwritten side is one more operand, what a && a does (#1533). Their ISO alternative tokens not_eq, not, and and or count exactly as !=, !, && and || do (#1470). 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). The exclusion is ABC’s alone: Halstead counts vocabulary rather than decisions, so bca ops still bills every one of these brackets as an operator (a type-enclosing pair as one <> since #1559 and #1567), and the two metrics are meant to disagree here (#1395); see the Halstead delimiter rule.
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, Elixir, PerlA 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 >, and, since #1531, alongside ==, ===, !=, !==, <= and >=, so &==/2 and Kernel.==(a, b) score nothing. Perl’s bare pattern match (my $r = /^#/;, or m{^#}) matches the implicit $_ with no operator token at all, and scores by use on the same rule, level with $x =~ /^#/ (#1467). Two patterns are excluded: the one bound by =~ / !~, whose operator already counts, and the one split takes first (split /,/, $s), which is a delimiter rather than a match. A substitution or transliteration (s///, tr///, y///) edits its string and yields a count, so it is no test by use in either spelling: bare on $_ or bound as $x =~ s/a/b/, it scores nothing outside a slot, and inside one the slot pays for it once (#1540). 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).
PerlA comparison’s right operand is not a condition, although the grammar parses it as onetree-sitter-perl 1.1.2 lets a chaining comparison (<, >, <=, >=, lt, gt, le, ge, ==, !=, eq, ne) take everything of lower precedence to its right as its operand, so $a > $b ? 1 : 2 parses as $a > ($b ? 1 : 2) and $a > $b && $c as $a > ($b && $c). The ternary’s condition slot and the chain’s first operand then held $b, which scored as a condition of its own: one above the JavaScript twin for each misparsed comparison. The walker recognises the shape and leaves that operand to the comparison, so both spellings score what the parenthesised ($a > $b) ? 1 : 2 and ($a > $b) && $c do. An explicit $a > ($b ? 1 : 2) is unaffected. <=>, cmp, =~ and !~ do not chain and parse correctly. The workaround goes when the grammar is fixed upstream (#1273).
PerlAn auto-quoted hash key is a string, not a callPerl quotes a bareword before => and alone in a hash subscript, a leading - included, so (name => 1), new(name => $x), (-name => 1) and $h{-name} hold the strings "name" and "-name". tree-sitter-perl 1.1.2 parses each word as a call, which scored a branch, and each -name as a negation, which billed a Halstead - operator and the operand name. Each now scores as its quoted twin ('name' => 1) does, and Halstead bills the key as one operand spelled as written: name, -name. A package-qualified word (Foo::Bar => 1), which => does not quote, a word with arguments (name() => 1) and a bareword value ((a => foo)) keep their call. bca find --type call reports the same calls ABC counts (#1580). Nor does nexits count a die or exit key as an exit (#1585). The workaround goes when the grammar is fixed upstream (#1571, #1572).

Where a constructor call lands

Fitzpatrick’s branch rule is “function invocation or object creation”, so object construction and constructor delegation count as branches even though they are not call sites. They do not appear in bca find --type call, which reports invocations only; see Semantic filters.

Which space a constructor call is charged to depends on how it is spelled, and C# 12 and Kotlin both let you spell it two ways:

class Sub(int x) : Base(x) { }            // charged to the class space
class Classic : Base {
    public Classic(int x) : base(x) { }   // charged to the constructor
}

A primary constructor has no body. Its superclass call sits in the class header (C#’s base_list, Kotlin’s delegation_specifiers), outside every member, so no function space encloses it and the branch belongs to the class. The classic spelling puts the same call inside a constructor declaration, which does open a function space.

File-level abc.branches is identical either way, so nothing shifts at the aggregate. Per-function figures do: branches_max, branches_average, and a per-space bca check --threshold abc=N all see the primary-constructor form on a class row and never on a function row. A codebase migrating to primary constructors will therefore see its per-function ABC distribution move without any behaviour change. That is the intended reading: there is no function to attribute the call to.

実例

次の 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、<=)
&&(none)Logical operator; does not contribute on its own.
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 the same body is rewritten with a unary conditional:

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 ツールとの比較

The project follows Fitzpatrick’s original paper for && / ||: the operator does not count; each non-comparison operand counts once as a unary conditional. This deviates from RuboCop’s Metrics/AbcSize (which counts and / or directly) and matches StepicOrg/abcmeter and eoinnoble/python-abc. Operator-counting choices cause ABC results to differ across tools on the same source.

アルゴリズム

The metric walk passes syntax-tree nodes to the language’s Abc implementation. It counts assignments, calls and conditions, inspecting child expressions where a construct requires it. The four headline values are:

  • 3 つのコンポーネントそのもの(assignments、branches、conditions)。
  • マグニチュード |<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 src/metrics/abc/<lang>.rs, matching on the node kinds generated into big-code-analysis-ast/src/languages/language_*.rs.

読み方

ABC is a size metric, not a complexity metric: a long, dull function with no decisions still scores high if it does many assignments. Fitzpatrick’s original recommendation was to use the magnitude as a relative ruler: rank a file’s functions by ABC magnitude and look at the top decile.

In practice the Ruby community adopted ABC most widely; the rubocop linter and the flog tool both default to threshold-based warnings. A Ruby method with an ABC magnitude over about 17 is conventionally a refactoring candidate; over 30 is considered hard to maintain. Those thresholds are language-specific. Expect higher values in C++ and Java, which use explicit getter/setter assignments more often.

認知的複雑度

G. Ann Campbell at SonarSource introduced Cognitive Complexity in the 2017 white paper Cognitive Complexity: A new way of measuring understandability and the follow-up IEEE TechDebt 2018 paper Cognitive Complexity: An Overview and Evaluation.

The metric was designed as a replacement for Cyclomatic Complexity in code-quality tooling. Campbell’s argument is that cyclomatic complexity measures how hard code is to test, not how hard it is to understand: a 1024-arm switch statement scores the same as a deeply nested chain of ifs that perform identical logic, yet a human reader finds the nested code much harder to follow.

アルゴリズム

認知的複雑度はゼロから始まり、木を走査しながら次の 3 つの規則を適用します。

  1. Ignore “shorthand” control flow. Constructs that route to a single block (a top-level if with no nesting, an else without conditions of its own, the head of a for, a ?: ternary) add a baseline +1 each, but they do not punish you for the pattern.
  2. Penalise breaks in linear flow. Conditionals, switches, loops, catch clauses and labeled jumps add increments under the rules below. A try block alone adds nothing. big-code-analysis does not score the recursive-call increment in any language: it performs no call resolution, so a recursive call costs nothing (see the per-language deviations below).
  3. ネストを罰します。 すでにネストされたブロックの 内側 に制御フローが現れるたびに、メトリクスは ネストの深さごとに 追加の +1 を加えます。メソッド内の外側の if の中の for の中の if は 1 + 2 + 3 = 6 になりますが、同じ 3 つの構文をフラットに並べた場合は 1 + 1 + 1 = 3 です。

Sequences of identical boolean operators (a && b && c) score +1 for the whole run, on the grounds that a chain of &&s is no harder to read than a single &&. Switching operators (a && b || c) is where the cognitive load jumps, so the second operator earns its own +1. “Identical” means the same logical operation, not the same token: where a language spells conjunction and disjunction both ways (C++, Ruby, PHP, Perl, Elixir, iRules), a && b and c is one sequence. Precedence still decides what nests where, so Ruby’s a || b and c is (a || b) and c and scores two. ?? and Perl’s // are operations of their own; xor, like ^, is no sequence.

big-code-analysis exports the per-space structural score as value along with the subtree sum, min, max, and a per-function average. The implementation is in src/metrics/cognitive.rs.

読み方

A Cognitive Complexity of 0 means the function is purely linear: no branches, no loops. SonarSource’s tooling defaults to flagging functions above 15 as “too complex”, and Campbell’s recommendation in the white paper is that a function should rarely exceed about 25. Unlike Cyclomatic Complexity, the metric scales smoothly: deeply nested code scores well above flat code with the same number of decisions.

The common use is refactoring guidance during code review: because the metric penalises nesting specifically, it flags the kind of function that benefits from an early-return or “extract method” refactor. SonarLint’s IDE plugins (IntelliJ, VS Code, Visual Studio, Eclipse) all surface it as the headline complexity number on hover, and several language servers and code-review platforms outside the Sonar ecosystem have since adopted it.

言語別の逸脱

  • No language scores the recursion increment (B1): the metric walk resolves no calls, so a function calling itself costs nothing beyond its explicit control flow.

  • Elixir does not score jump statements either. Elixir control flow (if / unless / cond / case / with / receive) is built from macro-shaped Call nodes rather than dedicated grammar productions, and the language has no break / continue / goto. The implementation therefore scores only the nesting-bearing constructs it can identify and omits the unstructured-jump (B1) increments that the SonarSource specification adds for languages that expose those shapes syntactically. A with expression’s else clauses act like case arms. The with expression already contributes the nesting increment, so its else adds nothing, just as Ruby’s case/else does. An if / unless / try else still adds +1. The keyword forms (else:, rescue:, catch:, bare or bracketed) score as their do … end blocks do, and Kernel.||(a, b) is a boolean sequence like a || b (#1574), as Kernel.or(a, b) is like a or b (#1582). A receive is a case over the mailbox and pays +nesting as one; its after timeout adds nothing, and a receive holding only an after is a timed wait that scores nothing (#1581).

  • For every language with a syntactic function-definition node, a nested function (a local function, or a method on a local or 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.

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

  • That surcharge stops at the next function boundary. A function declared inside a closure body opens a fresh lexical scope, so it does not inherit the closure’s surcharge: fn g inside a || { … } scores what it would score outside one. Until #1187 only the JavaScript family applied this, so the same body scored differently in Rust, Java, C++, PHP and C# depending on whether something two levels up happened to be a closure. Python cannot express the shape at all: a def is a statement and a lambda body is a single expression.

  • Rust’s let-else (let Some(a) = y else { continue };) postdates the specification. It is scored as the if let it replaces: +1 plus the current nesting, with everything under it, the else block included, one level deeper. Its else token does not pay again. A for holding it scores 3, the same as one holding if let Some(a) = y { … }. Before #1548 the let-else paid a flat +1 through its else token whatever its depth, so that for scored 2.

  • Python は、ブール演算子に対して、ブール演算子の連なり自体が得る +1 に加えて、それを囲む lambda 1 つごとに追加の +1 を課します。これを行う言語は他にありません。上乗せ分を支払うのは、あるラムダ本体の中で最も外側にある演算子だけであり、これらのラムダを数える走査は最も近い外側の expression_list で停止します。(if、for、while でも停止しますが、ラムダ本体は単一の式であるため、ラムダがこれらの文の上に位置することはなく、この 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)

Cyclomatic Complexity is the original software complexity metric. Thomas J. McCabe introduced it in 1976 in A Complexity Measure (IEEE Transactions on Software Engineering, SE-2(4), pages 308–320).

McCabe のアイデアは、関数の 制御フローグラフ にグラフ理論を適用することでした。各基本ブロックをノード、ブロック間の各ジャンプをエッジとして描くと、そのグラフのサイクロマティック数は次のようになります。

M = E − N + 2P

where E is the number of edges, N the number of nodes, and P the number of connected components. M counts linearly independent control-flow paths. Basis-path testing uses this count; branch coverage alone can require fewer tests.

アルゴリズム

big-code-analysis does not build a control-flow graph. Instead it uses the equivalent, much cheaper formulation McCabe proved in the 1976 paper for structured programs:

循環的複雑度 = 1 + (判断点の数)

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

  • if、else if、三項演算子 ?:
  • switch / match / select 内の case / when アーム
  • while、do … while、あらゆる種類の for
  • 例外ハンドラの catch 節
  • short-circuit boolean operators && and ||, including their word spellings (and / or in C++, Python, Ruby, Lua, Perl, PHP, Elixir and iRules)

A && or || counts only where it applies the operator. C++ also spells the token in rvalue references (int&& x), the ref-qualifier (void f() &&), overload names (operator&&) and requires-clause constraints, and none of those is a decision (#1525). Nothing inside a requires clause or a requires expression is a decision, written with parentheses or without (#1533); cognitive complexity and ABC conditions skip it too, though a call in it is still an ABC branch. Any other compile-time expression still counts, as if constexpr does: static_assert(A && B), and a concept body concept C = A && B.

src/metrics/cyclomatic.rs にある言語別の Cyclomatic トレイトが、各 tree-sitter ノードに「これは判断か」を問い合わせてカウンタをインクリメントします。このメトリクスは関数ごととファイルごとにロールアップされます。メソッド本体をまたぐクラス単位の集約は、後述の WMC が別途提供します。

修正循環的複雑度

big-code-analysis also reports a modified variant that collapses all case / match / when arms inside a single switch statement into one decision point, regardless of how many arms it has. This undercounts big dispatch tables in a way that often matches developer intuition better than the strict McCabe definition: a 30-arm enum dispatch reads as one decision, not thirty. The convention is not original to this project; it echoes the -m mode of Terry Yin’s lizard tool, where many readers will first have seen it. Both numbers are exported side by side; pick one and be consistent.

Only alternatives collapse. A sequence of tests is still one decision per test in both variants. Elixir’s with is the case where the two meet: each pattern <- expr clause is a separate test, scored once in both variants, as Rust’s if let and ? are. The clauses of its else are alternatives, so modified scores them once, and only when one of them is a decision: else _ -> 0 selects nothing, like a plain else, and is free in both variants (#1535). Rust’s let-else (let Ok(a) = g(x) else { return 0 };) is one such test too. It binds or diverges, so it scores one in both variants, as its if let twin does. Before #1542 it scored nothing.

Elixir’s receive is a case over the mailbox, and modified scores its clauses once, as it does a case’s. Like a case it has one free arm: its bare _ -> when the do section holds one, otherwise its after clause (the timeout, the path taken when no clause matched in time). So receive do {:a, y} -> y after 10 -> :t end scores one decision in standard, as the two-clause case does, and receive do {:a, y} -> y; _ -> 0 after 10 -> :t end two: beside the catch-all the timeout is a path of its own. A receive holding only an after is a timed wait and scores nothing at all (#1581).

Rust の ? 演算子のカウント

By default Rust’s ? operator (the try_expression grammar node) adds +1 to both standard and modified cyclomatic, matching upstream rust-code-analysis: ? is an early-return branch. When cyclomatic is used as a maintainability gate, this can over-penalise linear but fallible code that threads a handful of ? through a happy path. You can opt out so ? is treated as linear error propagation:

  • ライブラリ: MetricsOptions::default().with_count_cyclomatic_try(false)。
  • CLI: --cyclomatic-count-try=false(または非推奨の --no-cyclomatic-try エイリアス)、あるいは bca.toml の cyclomatic_count_try = false(CLI の値はどちらの方向でもこのキーを上書きします)。
  • A repo gate: set cyclomatic_count_try = false in the auto-discovered bca.toml. This project’s own make self-scan does exactly this, with no flag or env var. Toggling the policy shifts cyclomatic values, so regenerate .bca-baseline.toml in the same change.

The default is unchanged (? keeps counting), so published metric values are preserved. The toggle is Rust-only; no other language emits try_expression.

読み方

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

Cyclomatic complexity is used for three things:

  1. Defect prediction. Complexity correlates, imperfectly, with the probability of a function containing a bug, and most static-analysis tools flag high-CC functions as risky.
  2. Test planning. CC measures the size of a basis-path test set, not the minimum test count for branch coverage.
  3. リファクタリングのトリアージ。 循環的複雑度はほぼすべてのコード品質ダッシュボードで筆頭に挙げられる「複雑度」の数値であり、長さが同程度に見える 2 つの関数の優先順位を決めるタイブレーカーとしてもよく使われます。

The metric’s known blind spot is that it treats every decision as equal weight. A 30-arm switch over an enum and a function with two nested ifs each containing nested ifs both score around 30, even though they read very differently. Cognitive Complexity (above) was designed to fix exactly that.

Halstead

The Halstead suite is the oldest size-and-effort metric family on this page. Maurice H. Halstead introduced it in his 1977 book Elements of Software Science (Elsevier, ISBN 0-444-00205-7); the Wikipedia page on Halstead complexity measures summarises the formulas. Halstead’s aim was a quantitative, empirical science of software, in the same way that physics is the empirical science of matter.

4 つの基本カウント

Halstead はプログラムをトークンに還元し、それらを 2 つのカテゴリに分割します。

  • Operators, anything that does something: keywords (if, return, while), arithmetic and logical operators, assignment, function-call syntax, punctuation that controls flow.
  • Operands, anything that is something: identifiers and literals.

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

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

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

big-code-analysis records these four numbers in src/metrics/halstead.rs per function and per file. The per-language trait classifies tokens as operator vs. operand on a token-by-token basis. Punctuation counts as an operator: a bracket pair is one operator, billed once at its opener and listed by bca ops as (), [] or {}, and ,, ;, . and : count once per occurrence. Closing brackets, comments and a literal’s own delimiters count as nothing, which is why the Halstead totals are not the same as the Tokens count.

The following rules define which syntax contributes to Halstead’s vocabulary. Context matters because grammars reuse punctuation for operators, literal delimiters and other syntax:

  • 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. The same holds for punctuation the grammar spells inside a literal: Perl’s bare /[a]#/ bills no [] or #, matching m/[a]#/, whose text the grammar does not split into tokens (#1549). 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. {a b} is a block or a quoted value depending on the command it is passed to, so the classifier reads that command’s name. eval {…}, uplevel, after, time, an on or trap handler clause written outside a try, 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). Every other command (lappend, puts, list, and any user-defined proc) is taken to receive a value, so its braces score no operator.

    Recognising the command is only half of it, because most of those signatures mix the two roles. A braced word in a slot the command’s documented syntax reserves for a value scores no operator either, however script-like the command around it: after’s delay (after {100} {puts hi} scores the one {} its script earns, the same as after 100 {puts hi}), time’s iteration count, a braced proc name (proc {my proc} {} {}), a defaulted proc parameter (proc p {a {b {x y}}}), namespace export’s pattern list, namespace ensemble create -map’s dictionary, and a trap or on handler’s error code and variable list. The slots come from the Tcl 8.6 manual pages. The two proc cases are positions the grammar names, so the classifier reads them off the tree; the rest are recorded as per-command signatures in big-code-analysis-ast/src/lang_helpers/tcl_family.rs. Where a command’s argument list does not match the signature (a top-level try … trap … finally parses its whole tail as one trap command, five arguments rather than three), no position in it is read, and every brace stays a block.

    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 consequence for the operand column is an asymmetry, and it is the contract rather than an open defect: a braced value scores one operand where the grammar names the command (set x {a b} → the operand {a b}) and one per word where only the command name would (lappend x {a b} → the operands a and b). Both ways of removing it were weighed and neither is available. Deciding from the contents (no substitution, no nested block, therefore a list) was built and measured: on a file holding an oo::class body, a tcltest -body and an apply lambda it took n2 from 25 to 15 and N2 from 32 to 15, each of those three collapsing into a single operand. And listing the value-taking commands instead needs an open set where the script-taking one is closed, since every user proc belongs to it, so the list would be incomplete by construction and would make three behaviours where there are two.

  • A string-interpolation opener is not an operator. "{$x}" in PHP, "#{x}" in Ruby and Elixir, "${x}" in Kotlin and Groovy, $"{x}" in C#, f"{x}" in Python and Java 21’s STR."\{x}" all count the interpolated expression’s own operators and nothing for the opener itself, and the wrapping literal is not an operand either. Before #1703, Java counted the wrapping string template as an operand as well as the interpolated x. The : that opens a Python format spec (f"{x:>10}") counts nothing either, because the spec after it is string content (#1486). Nor does the self-documenting = of f"{x=}", which prints the expression’s text and assigns nothing (#1551). C#’s format : and alignment , ($"{x,8:F2}") count nothing for the same reason (#1573), but the alignment is an expression and keeps its own vocabulary: 8 is an operand, and {x,-w} counts - and w. A Python literal width (f"{x:>10}") is format-spec text and counts nothing.

  • A delimiter around syntax is an operator, whatever it delimits. The rule above is about a literal’s own punctuation, and it stops there. Halstead counts the vocabulary a program is written in and has no notion of a decision. Halstead uses the same < vocabulary entry for comparisons, JSX tag delimiters, Lua <const> attributes, C# operator < declarations and Perl <FH> readlines, and bca ops reports every one of them. This is the deliberate counterpart to the ABC deviation that excludes the same tokens from conditions: ABC asks whether a branch is taken, and these are not branches; Halstead asks what alphabet the source uses, and these are part of it. Expect the two metrics to disagree here. The same reading bills Python’s (, [, {, ; and block, slice, dict and annotation :, and Rust’s annotation, field, bound and label :, which is a separate operator from ::. Both languages left these out until #1486, so their Halstead scores rose with it.

  • A declaration keyword is an operator. Python’s class is billed like def since #1550, as Ruby, Kotlin, C# and PHP bill their class. Java, Groovy, the JavaScript family, C, C++ and Objective-C followed in #1552 with their type-declaration keywords: class, struct, union, enum, interface, record, trait, @interface, @implementation and @protocol. The class tail of Java’s C.class is an operand, not this keyword (see the class literal below). Groovy’s var bills as its def does since #1704, once per local declaration however many declarators follow it. A var used as a name stays an operand (def var = 3, [var: 1]), or bills nothing where the grammar cannot parse the statement (var = 4), and the var of a field (var field = 1) or of final var, which the grammar reads as a type name, still bills as a type-name operand (#1706). Kotlin’s interface followed in #1556. #1557 added C++’s namespace, template, typename, typedef and using (C and Objective-C bill typedef too), so template <class T> and template <typename T> each bill their keyword and score alike. It also added TypeScript’s type, namespace, module, declare and global. #1561 made TypeScript’s type-level operator keywords operators, as their value-level siblings as, typeof and extends already were: satisfies, keyof, infer, and the is and asserts of a type predicate. #1564 made Kotlin’s typealias, init, where, by, get and set operators, as C# bills its get, set, init and where. The @get: and @set: annotation targets bill nothing, like @field:. The backing field in an accessor is an operand, and the words used as names (val by = 1, x.get(i)) stay operands.

  • A declaration modifier is an operator, keyed by its keyword and never its wrapper node. C#, Kotlin, Java, Groovy, TypeScript and TSX, C, C++, Mozcpp and Objective-C bill every modifier once, as the keyword and never as the node that wraps it: Kotlin’s and Java’s modifiers, TypeScript’s accessibility_modifier and override_modifier, and the C family’s storage_class_specifier, type_qualifier and virtual_specifier. So the const of void f() const and of int * const p each bill once. C# already did; the others followed in #1558 (Kotlin), #1563 (TypeScript), #1565 (Java and Groovy), #1578 (C, C++ and Mozcpp) and #1579 (Objective-C). The exceptions:

    • The same word used as a name stays an operand (val open = 1, let readonly = 1, obj.static, var sealed = 1, int override = 1;). Where Kotlin’s grammar cannot parse the name (open = !open at the start of a statement, val suspend = 1, a parameter named vararg), the word bills nothing.

    • The static of import static and requires static bills nothing, as import does not: Java and Groovy bill no directive keyword (package, import, module, requires, exports), and the word a directive qualifies follows it. C# is the outlier here: it bills using, and so using static in full. Each reading is consistent with the language’s own directive rule, and #1600 kept both rather than move one.

    • C++’s auto is a placeholder type and stays unbilled. Attributes, the MS calling conventions, the Gecko MOZ_* macros and the Apple SDK macros (NS_INLINE, FOUNDATION_EXPORT, IBOutlet) bill nothing. The words each language bills:

    • Kotlin: the enum of enum class, data, sealed, the visibility keywords, open, override, suspend, reified, out and the rest.

    • TypeScript and TSX: the member modifiers public, private, protected, override, readonly, abstract, accessor and static. JavaScript and Mozjs bill static too. Their grammars lex static get followed by a newline as one static get token, which bills as one operator, so that spelling scores one entry where static get y() on one line bills static and get.

    • Java and Groovy: the visibility keywords, static, native, strictfp, volatile, sealed and non-sealed, where before #1565 they billed only abstract, final, transient, synchronized and default; plus the permits clause keyword beside extends and implements.

    • C, C++ and Mozcpp: every storage class, function specifier and type qualifier: static, extern, register, inline, thread_local, const, volatile, restrict, _Atomic, constexpr, alignas and their vendor spellings (__inline, __forceinline, __restrict__, __thread, __volatile__, __extension__, …), plus C++’s virtual, mutable, constinit, consteval, explicit, friend, the access labels, override, final and noexcept. C bills its storage class auto. extern "C" bills extern and keeps "C" an operand.

    • Objective-C: C’s list, plus the @public, @private, @protected and @package ivar labels, the @optional and @required protocol sections, the ownership qualifiers (__weak, __strong, __block, …), the nullability qualifiers (nullable, _Nullable, __nullable, …), __kindof, the __covariant and __contravariant variance, and the in, out, inout, bycopy, byref and oneway method-type qualifiers. A property attribute (nonatomic, copy, getter) bills as its word; a getter= or setter= value stays an operand.

  • A type name is an operand. #1557 made every TypeScript type name an operand (x: Foo, interface I, Map<K, V>), as C#, Rust and the C family already did. Java followed in #1560: Foo f, implements I, List<Foo> and each segment of java.util.List bill their names as operands. The inferred type var does not, as C# bills nothing for its own. Java’s primitive types are operators everywhere. Groovy, whose grammar parses a primitive type as a name, bills it the same way since #1568: int, boolean, void and the other primitives are operators keyed by their spelling, as a declared type and as a class literal’s receiver. Groovy reserves these words, and they bill as operators; in the positions Groovy nonetheless allows them as names (x.int, [int: 1]), they stay operands. Java’s boolean is listed as boolean rather than its node kind boolean_type, so both languages report it alike.

  • A class literal bills its parts. Since #1566 a Java class literal bills its parts rather than one operand spelled by its whole text, since JLS §15.8.2 defines it as an expression rather than a literal: in java.util.List.class and Foo[].class each name and the class tail are operands and each . and [] an operator, as Groovy’s Foo.class and Kotlin’s and PHP’s Foo::class bill theirs. The int of int.class and the void of void.class stay operators, as Java bills them everywhere.

  • Angle brackets that enclose types are one bracket pair. The < and > around type arguments and type parameters count as one <> operator, billed at the < as () is at the (, and the > counts nothing (#1559). That covers Vec<u8>, fn f<T>, a turbofish h::<u8>(), for<'a>, use<T> and a qualified path’s <T as Trait>:: or <T>:: in Rust; C++’s template <class T> and std::vector<int>; and the type-argument and type-parameter lists of Java, Groovy, C#, Kotlin, TypeScript and TSX, plus C#’s delegate*<int, void> pointer type and Kotlin’s super<A> (#1567). Objective-C has four generic lists: NSArray<NSString *>, id<NSCopying>, the protocol list in @interface A : NSObject <NSCopying>, and a type passed as a call argument, f(NSArray<NSString *>). A comparison’s < and > and a shift’s << and >> stay separate operators, and a C++ >> that closes two template lists parses as two >, so it bills nothing. The rule follows the parse: where tree-sitter-cpp reads comparisons as a template argument list, as in x = a < b || c > (d), their brackets bill as <> too, since only name lookup tells that from a template call spelled the same. The comparisons Objective-C and C# misread as a call argument’s generic list, g(a<b, c>d) and F(a < b, c > d), keep < and >. The exception is C#’s >= spelling, F(a < b, c >= d), which the grammar parses as an assignment and which still bills <> and =. Go’s generics use square brackets and already billed []. Angle brackets around anything other than types still bill < and >: the JSX, Lua and Perl constructs above. So does a Kotlin generic call with arguments, id<Int>(a), which tree-sitter-kotlin-ng parses as two comparisons (#1394), and a generic inside a Rust macro call, vec![Vec::<u8>::new()], whose token tree tree-sitter-rust leaves unparsed; a >> closing two lists there bills as the shift. Before #1559 each bracket billed under the comparison’s own entry, so Vec<u8> and a < b shared one. Every generic now costs one operator occurrence less, so N1 falls in any code that uses generics. n1 rises by one where a comparison < or > remains beside the new <>, so halstead.effort falls in most files and rises in the rest; on the C++ and Rust corpora it fell in 87% of the files that moved, and MI rose in most of them.

  • An alternative spelling is an operator of its own. C++ spells && equally as and, != as not_eq and so on through all eleven alternative tokens. Each counts once in total_operators, exactly like its symbol, but a && b and c has two entries in unique_operators where a && b && c has one. Operators are distinct by token, and bca ops lists and and && separately, so the metric matches what it reports (#1522).

  • A self-reference is an operand. this, self, $this, super and base name the receiver of a member access, so this.x counts the way p.x does: one . operator and two operands. Java, Groovy, C#, Kotlin (labelled this@Outer included), Python, Rust and PHP all agree. The keyword is an operator only where it declares something, as the operator keyword of an overload does: C#’s indexer public int this[int i], and the wildcard bound ? super T in Java and Groovy (#1380).

  • A keyword’s role follows its text, not its node kind. C#’s grammar parses the bare this, ref, out, in, scoped and readonly of a parameter, and the static / async of a lambda, as a modifier node, a different kind from the one the same keyword has anywhere else. Each keeps the role its text has elsewhere: an extension method’s this Foo f is an operand, the ref in M(ref int a) is the same operator as the ref in the call M(ref x), and static (int x) => x bills the same static as static void M(). A repeated operator keyword adds to total_operators but not to unique_operators (#1418, #1482).

  • Perl’s __END__ and __DATA__ end the program. Neither marker nor the data section after it is an operator or an operand, so two files that differ only in their data section score alike. The grammar holds the marker and every byte after it in the same special_literal node it uses for __FILE__, which until #1644 billed the whole section as one operand spelling its contents, and billed each of __FILE__, __LINE__, __PACKAGE__ and __SUB__ twice, once as that wrapper and once as the keyword inside it. Each constant is now one operand, billed once. The marker and the opaque data leaf are still leaves, so tokens still counts them.

  • JSX element text is an operand. In JavaScript, Mozjs and TSX, the hi of <p>hi</p> counts the way a string’s contents do. It is keyed on its text with leading and trailing whitespace trimmed, so a line of text at two indentation depths is one operand, and the spaces between two tags on one line count as nothing. Whitespace inside a multi-line text is kept. A character reference (&amp;) splits the text into separate operands (a &amp; b is three). Inside an attribute string, the reference is part of that string’s one operand (#1483).

  • An overloaded operator’s name is an operator. The + of operator + was always one, and the names a grammar spells as whole tokens now agree: C#’s operator true / operator false, and C++’s operator[], operator(), operator""_x and operator co_await. The operator keyword is an operator in both languages, as is C++’s co_await in an expression. C++’s [] and () names are tokens of their own, so bca ops lists operator[] beside a subscript’s [] as a second operator, as Ruby does for def [](i) (#1296). C++’s coroutine keywords co_return and co_yield are operators as return is (#1547).

  • Only written tokens count. When tree-sitter cannot parse a construct, its error recovery may insert zero-width tokens the source does not contain: a ;, a ), an identifier. None of them is billed in either half. Before #1546 an inserted identifier became an operand whose text is the empty string. One case is a C++ destructor called through a template name (p->~T<A>()), which tree-sitter-cpp cannot parse. Another is a statement-position typename cast, which made every inserted ; a second ; operator.

  • A named operator is one operand. Elixir names an operator in a capture (&==/2, &and/2) and as the function of a remote call (Kernel.||(a, b)), and its grammar wraps the token in an operator_identifier. That wrapper is the operand, as foo is in &foo/2, so the token inside it adds no operator; &==/2 scores what &foo/2 does. A captured && / || / and / or is no decision either (#1534), but the remote call applies the operator, so Kernel.||(a, b) scores what a || b does in cyclomatic, ABC and cognitive alike, and a user module’s Foo.||(a, b) in none (#1574). Kernel.and(a, b) and Kernel.or(a, b) decide the same way, though their names are plain identifiers with no operator token (#1582). This differs from a C# operator <, whose < has no wrapper and so stays the operator the rule above makes it.

導出メトリクス

Halstead then derives a set of formulas. big-code-analysis reports all of the standard ones, plus three less common derivations (estimated_program_length, purity_ratio, level) that are part of the original suite:

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)

The numeric constants come from Halstead’s empirical fits against a heterogeneous corpus of CDC-era programs including FORTRAN, PL/I, and Algol-family languages. The T = E / 18 “Stroud number” comes from psychology instead: Halstead borrowed John Stroud’s estimate that the human mind makes about 18 elementary discriminations per second.

読み方

Halstead’s original intent was to predict three things about a program before it was written: how big it would be in bits, how long it would take to implement, and how many bugs to expect in deployment. The empirical evidence for the volume and length predictions is reasonable; the time and bugs predictions are more controversial and have been criticised at length, notably in the Purdue technical report Software Science Revisited.

現代の実践では、Halstead の数値は 3 つの用途に使われます。

  1. As inputs into composite metrics, most importantly the Maintainability Index (next section), which depends on Halstead volume.
  2. 言語に依存しないサイズの代理指標として。ビット単位のボリュームは、LOC では実現できない形で言語をまたいで滑らかにスケールします。
  3. 相対的な工数見積もりのため。2 つのリファクタリング候補の循環的複雑度が同程度の場合、Halstead の difficulty が高いほうがリグレッションを持ち込む可能性が高くなります。

コード行数

This section covers the LOC variants: SLOC, PLOC, LLOC, CLOC, BLANK, and DLOC. “Counting lines” sounds trivial until you have to define exactly what counts. SLOC through BLANK are the de-facto standard breakdown, going back to Samuel Conte, Hubert Dunsmore and Vincent Shen’s 1986 textbook Software Engineering Metrics and Models (Benjamin/Cummings, ISBN 0-8053-2162-4), which codified the distinction between physical and logical lines. The Wikipedia entry on source lines of code summarises that physical-versus-logical distinction.

バリアント数える対象
SLOCSource Lines Of Code: every line in the file, comments, blanks, and code alike
PLOCPhysical Lines Of Code: non-blank, non-comment-only lines
LLOCLogical Lines Of Code: statement-bearing lines (definitions, assignments, declarations)
CLOCComment Lines Of Code: lines that contain a comment (with or without code on the same line)
BLANKBlank lines: whitespace-only lines
DLOCDocumentation Lines Of Code: rows the language treats as documentation (a doc comment, a Python docstring, a POD block); always a subset of CLOC

アルゴリズム

big-code-analysis derives every count from a single pass over the tree-sitter syntax tree (see src/metrics/loc.rs). Comments and strings are identified by their AST node type rather than by lexical scanning, so multi-line strings, raw strings, doc comments, and string interpolations are all handled correctly. The per-language Loc trait specifies which node kinds count as a “statement” for LLOC; this is the subtle one, because what counts as a statement is language-defined.

Documentation rows are comment rows that are also reported on their own as DLOC (loc.dloc), so CLOC can be read with or without them. Each language’s own documentation tool decides which comments are documentation; there is no single rule for all:

  • Where the grammar marks it, the mark decides: a Rust ///, //!, /** */ or /*! */ comment (tree-sitter’s doc_comment child; //// and /**/ carry none), a Groovy groovydoc_comment (but not the empty /**/ that grammar also marks), a Perl POD block (but not one that is nothing but =begin comment and =for comment regions, Perl’s idiom for commenting code out; POD after such a region’s end documents, and makes the block documentation).

  • Where the grammar emits one comment kind for every form, the opening marker decides, as the language’s documentation tool reads it: Doxygen’s /**, /*!, /// and //! (and their after-member < forms) for C, C++ and Objective-C; /** for Javadoc, KDoc, JSDoc and PHPDoc, plus /// for Java’s Markdown doc comments; /// and /** for C# XML docs. The tools disagree at the edges and so does DLOC: /*** is documentation to javac, Kotlin, Groovydoc and tsc (so TypeScript and TSX read it, while plain JavaScript follows JSDoc) but not to Doxygen, JSDoc, Roslyn or PHP; /**/ is documentation nowhere; PHP needs whitespace after /**; and //// is never documentation, which is stricter than Doxygen’s lexer.

  • A doc comment counts only where its tool reads it. A Rust, Java, Kotlin, Groovy, C# or PHP doc comment inside a function or closure body, inside a Java or Groovy initializer, or inside a Kotlin accessor or init block, is an ordinary comment: rustc warns that one on a statement is unused, Roslyn raises CS1587, javadoc and Dokka document declarations only, and an in-body PHPDoc /** @var */ is a type hint. A working note spelled /// … inside such a function therefore stays charged by MI. C, C++ and Objective-C keep in-body doc blocks: Doxygen appends them to the function’s documentation as its in-body description, and HIDE_IN_BODY_DOCS defaults to NO. Two placements inside a function still document: a Rust inner doc comment (//!, /*! */) opening a function’s body is that function’s own documentation, and a doc comment in a signature counts where its tool reads it there (a Doxygen /**< parameter block or a block after [[attributes]], a PHPDoc after #[…] or on a promoted constructor property, a KDoc after an annotation), but not in Java or C#, which bind a doc comment only before a declaration’s first token. JavaScript, TypeScript and TSX keep in-body JSDoc, which documents inner symbols, and Perl keeps POD wherever it sits, as perldoc does, after __END__ or __DATA__ too, where every other non-blank row of the data section counts as PLOC.

  • In Rust, Java, Kotlin and Groovy, a doc comment also has to be attached: it counts only when the declaration after it is one its tool documents. Attributes, annotations, modifiers and blank rows may come between the comment and the declaration. A Rust 2024 safe fn in an extern block counts too, although tree-sitter-rust parses its safe as an error. A doc comment above anything else stays a plain comment row: an import (Rust’s use and extern crate take one), a statement, a macro call, a Rust extern block, a closing brace, or the end of the file. A KDoc nested in an argument or type-parameter list documents nothing. The tools also differ on a run of several doc comments. rustc attaches every outer doc comment above an item, so all of them count. javac and Kotlin keep only the last one; javac’s -Xlint:dangling-doc-comments names the earlier ones, and they do not count. Consecutive /// lines are one Java Markdown doc comment. Groovydoc reads only the comment nearest the declaration, so a plain // comment in between detaches it. A Java doc comment above package counts only in a file with no type after it, which is the shape of package-info.java. Rust’s inner //! is unaffected: it documents the item it sits in.

  • C#, PHP, JavaScript and TypeScript bind doc comments the same way. Each language still follows its own tool:

    • C#: Roslyn keeps the nearest run of doc comments that only blank rows separate, reading past plain comments and directives such as #region to find it. Every other doc comment is CS1587, and so is a doc comment above a namespace, a using directive, or an accessor such as get;.
    • PHP: phpDocumentor gives a class, function, method, property, constant or enum case the last DocBlock before it. A DocBlock above a statement, a use or a namespace counts only as the file DocBlock. phpDocumentor takes the file’s first DocBlock as the file DocBlock when no class, function, constant, interface or trait follows the DocBlocks at the top of the file. It also takes it when exactly two DocBlocks come before the first such element.
    • JavaScript: JSDoc gives a declaration or an assignment the nearest block comment before it, reading past // lines, so a plain /* */ in between detaches a doc comment. A call or another statement takes nothing. A block tagged @file, @fileoverview, @overview or @module, or with a tag that names a symbol (@typedef {Object} T), documents that symbol wherever it sits. @license does not make a block stand alone.
    • TypeScript: tsc keeps the last /** before a declaration, reading past every plain comment, and documents no assignment statement. A doc comment after a decorator, a modifier such as static, or export or declare is inside the declaration and documents nothing, and so does one after code on its own row, except before a parameter. A @module or @packageDocumentation block documents the file, as TypeDoc reads it. In a JavaScript or TypeScript function body every doc comment still counts, since JSDoc documents inner symbols.
  • C, C++ and Objective-C follow Doxygen, which keeps every doc block it meets and gives them all to the next entry. A doc block counts when what follows it is something Doxygen documents: a declaration, a definition, a macro, a typedef or using, a namespace, a template, an enumerator, a macro call such as FOO(bar);, or an Objective-C @interface, @property or method. Doxygen reads past blank rows, plain comments, #include, #import, #pragma and an empty ;, and into an include guard or another #if that holds a declaration. A doc block above a forward declaration (struct s;, @class C;), a closing } or @end, or the end of the file stays a plain comment row. A block carrying a structural command documents an entry of its own, so it counts wherever it sits: \file, \mainpage, \page, \defgroup, \addtogroup and \name always, and a command that names its subject, such as \class X or \def M, when it has a name. The doc blocks above a \file block count with it. \brief and \ingroup are not structural. A member-after comment (/**<, ///<) documents the member before it and always counts, as do the blocks in a function body, which Doxygen publishes as the function’s in-body description.

  • In Python, every bare string statement (a string whose value is discarded) is a comment row, but only one where Python reads documentation is DLOC, following PEP 257: the first statement of a module, class or function (the string stored as __doc__, which must be a str literal, never an f-string or bytes), successive strings after any docstring, and an attribute docstring directly after an assignment to a name or a type alias at module or class level, or after an assignment to self.<name> at the top of a class’s __init__, alone or in a tuple of targets, which Sphinx autodoc reads. Sphinx also reads such an assignment inside an if, with, loop or try suite; PEP 257 names the top level only, and DLOC follows the PEP there. PEP 258 also includes successive strings after attribute docstrings. Comments between them do not count as statements. Any other bare string, such as a triple-quoted string wrapping commented-out code in the middle of a function, stays a comment row and is charged by MI like a # comment. This is narrower than radon’s multi, which takes every bare multi-line string. A docstring row that also holds code (def f(): """doc""") is code too. An implicitly concatenated literal ("""doc""" "more") is one string and a bare tuple of strings is none, the same test Halstead applies when it drops a bare string’s operands. A parenthesized string (("""doc""")) is the same string, because CPython drops the parentheses, and its ( and ) rows are its rows. A comment is documentation in Python only in Sphinx’s #: form: consecutive #: lines directly above one of those attribute assignments or aliases, or a #: after the code on its line (MAX = 10 #: in bytes). A plain # line breaks a #: block.

Go, Ruby and Lua have no marker to read, so a comment is documentation by where it sits, decided once per run: the chain of comment siblings on consecutive rows that hold nothing but comments (a trailing x := 1 // t never joins one, and a blank row ends one):

  • Go: a run directly above a declaration is go/doc’s lead comment, where a declaration is the package clause, a func or method, a top-level type / var / const declaration or one of its grouped specs, a struct field, or an interface method; never an import, and never a declaration inside a function body, which go/doc does not read. A directive row such as //go:generate belongs to the group but is not documentation, and is not counted.
  • Ruby: a run directly above a def, def self., class, module, class << Const, a constant or class-variable assignment (MAX = 10, Foo::BAR = 1, @@count = 0), a method alias (alias new old, alias_method :new, :old), an attr or attr_* call or a visibility-wrapped def (private def m), alone or under an if / unless modifier, as YARD attaches it, including the first statement of a class body or of an if, elsif or unless body. In a method body YARD reads only a nested def, and nothing in a singleton class or conditional opened there; no YARD handler claims a case, a begin or a loop, so nothing in their bodies counts anywhere. A shebang, an encoding line or a frozen_string_literal magic comment is dropped from the run, as YARD drops it. Unlike YARD, one blank row between the run and the def is a gap, not a tolerance; a trailing comment on the statement’s own row (MAX = 3 # The maximum.) documents nothing; and a statement in a call’s block (included do, an RSpec describe block, in either do … end or { } form), which YARD never reads, still counts. A block that opens with RDoc’s ## (exactly two #, so a #### banner or a ##### Helpers ##### divider opens nothing) or a YARD @! directive (# @!method, # @!attribute, # @!parse) documents a method with no def to sit above (define_method, a DSL call), so it counts whatever follows it, in a namespace body but never in a method’s.
  • Lua: a run that opens with an LDoc --- row (a row of dashes alone opens too; --- x --- is LDoc’s end marker and does not) and continues over the -- rows below it, or a --[[- long comment. A long comment ends a run, as LDoc’s lexer ends a block there. A block with no text (a row of dashes alone) is a comment, as LDoc discards it. A --- row directly under a line that ends in a trailing -- note is no block at all: LDoc’s lexer keeps a line comment’s newline and gathers consecutive comments into one group, whose first token (the trailing note) decides it. Otherwise a block counts when it is the first block of a file that opens with a comment or a shebang, which LDoc skips (the module’s documentation), when one of its lines opens with an @ tag (-- @param x, LuaLS’s ---@class), or when an item follows it on the next row (the first statement of an if, do or loop body included): a function, an assignment to one name or dotted name (local x = 1, M.a.b = 1; not a bare local x, local a, b = …, local x <const> = … or M[1] = …), a named table field (new = function … end), or a return of a function or a table. Any other block is a comment, as LDoc drops it with a “definition cannot be parsed” warning. Three deviations from LDoc: a tagged block counts even with no item or name to attach to; a blank row between a block and its item is a gap; and a file that opens with code still has its blocks read, where LDoc reads none of them without a module() call. A block inside a function body is a comment: LDoc documents what a module declares, and an in-body --- annotates the code there instead (LuaLS’s ---@type on a local), as PHP’s in-body @var does.

Nothing else counts: an Elixir @doc heredoc is an assignment the compiler stores and is code, and Bash, Tcl and iRules have no documentation syntax.

The counts satisfy a few useful identities:

SLOC = PLOC + BLANK + (lines that are comment-only)
CLOC ≥ (lines that are comment-only)        # CLOC also counts mixed code+comment lines
DLOC ≤ CLOC                                 # every documentation row is a comment row

読み方

  • SLOC は、多くの人が口語的に「コード行数」と言うときに指すものです。標準的なサイズの代理指標ですが、フォーマットの影響を受けやすく、言語の慣習をまたいだ移植性はありません。
  • PLOC strips away the visual noise. It is the measure to gate file length on (loc.ploc), and the size term of the Maintainability Index below.
  • LLOC は最も信頼できる 文 の数です。文ごとのテストケースを見積もる場合や、Python ファイルと Java ファイルの密度を比較する場合に適した指標です。
  • CLOC, combined with PLOC, gives you a comment density: CLOC / PLOC is a rough proxy for how much of the file is documentation versus implementation.
  • BLANK is mostly diagnostic: a file with a very low BLANK proportion is often hard to read.
  • DLOC is the slice of CLOC that is documentation: doc comments, docstrings, POD. CLOC − DLOC is the count of working comments, the numerator of the Maintainability Index’s SEI comment share.

LOC variants are used well beyond raw size. They are the most common input into cost-estimation models (COCOMO and COCOMO II both use KSLOC, thousands of source lines, as their base unit), they feed effort prediction in product-portfolio dashboards, and they are the normalising denominator for almost every other metric: defects per KSLOC, churn per KSLOC, test cases per KSLOC. The weakness is that LOC is easy to game: a 10× difference in coding style can produce a 2× difference in LOC. That is why this chapter has so many other metrics in it.

保守容易性指数(MI)

The Maintainability Index is a composite metric that rolls several of the metrics above into a single number, roughly 0 to 100, meant to be read as “how maintainable is this code?”. Paul Oman and Jack Hagemeister proposed it in their 1992 ICSM paper Metrics for assessing a software system’s maintainability, and Don Coleman, Dan Ash, Bruce Lowther, and Paul Oman refined it in the 1994 IEEE Computer paper Using metrics to evaluate software system maintainability (IEEE Computer 27(8), pages 44-49). Their method was empirical: they collected expert maintainability ratings on a handful of production Hewlett-Packard systems, computed forty candidate metrics on each, and let regression analysis pick the best linear combination. The combination that survived used Halstead volume, cyclomatic complexity, lines of code, and comment density.

big-code-analysis reports the three formulas that have stuck in practice. The three values nest under the mi object as the keys original, sei, and visual_studio (the dotted threshold names mi.original, mi.sei, mi.visual_studio):

mi.original      = 171 − 5.2·ln(HV) − 0.23·CC − 16.2·ln(PLOC)
mi.sei           = mi.original + 50·sin(√(2.4·CM))
mi.visual_studio = max(0, mi.original · 100 / 171)

CM               = (CLOC − DLOC) / (SLOC − DLOC rows that carry no code), in [0, 1]
  • mi.original は Coleman–Oman の公式です。病的なファイルでは負になることがあります。
  • mi.sei is the Software Engineering Institute’s refinement, which adds a comment-density term. The sin(√(...)) shape was chosen so that some comments help, but adding more after a point does not, and never hurts. CM is the share of working (non-documentation) comment rows among the space’s rows, a ratio in [0, 1]. On that scale the term rises monotonically from 0 with no comment to about 50 (50·sin(√2.4) ≈ 49.99) when every row is a comment, so mi.sei is never below mi.original and equals it in a comment-free space.
  • mi.visual_studio is the linear rescaling Microsoft chose for Visual Studio, where the score is clamped to [0, 100] and shown to developers traffic-light style: green ≥ 20, yellow ≥ 10, red below.

The size term is PLOC, the rows that carry code (#1607). Comment, documentation and blank rows never enter it, so a // TODO, a commented-out block, a Rust /// block, a docstring or a run of blank lines leaves mi.original and mi.visual_studio exactly where they were, with Halstead and cyclomatic unchanged. A row a doc comment or docstring shares with code, as in def f(): """doc""" or int x; /** doc */, is a code row and stays counted.

Documentation also leaves both sides of CM in every language (#1597), so a function’s mi.sei is the same with and without its /// block, its Javadoc comment, its POD or its docstring, which keeps MI consistent with Halstead, where a docstring’s operands are already ignored. Blank rows stay in CM’s denominator: adding one to a space with a working comment dilutes the comment share and lowers mi.sei slightly, while a comment-free space is unmoved.

Two details differ from some published statements of the SEI formula. Its ln matches mi.original’s; releases before #1607 took log2 of the volume and size in mi.sei alone. And CM is a ratio rather than a percentage: on the 0–100 scale the sin term swings between −50 and +50 several times across ordinary comment densities.

Charging documentation

With mi_charges_docs on, mi.sei counts documentation as comment: it takes its comment share as CLOC / SLOC, the inputs that releases without loc.dloc fed that share. Nothing else moves. The size term is PLOC in both modes, so mi.original and mi.visual_studio read the same either way, and since the comment term only rises with CM, charging documentation can only raise mi.sei. It does not reproduce an earlier release’s numbers: those sized every variant on SLOC and took mi.sei on a different scale. It changes no other metric: loc.dloc is reported either way.

  • Library: MetricsOptions::default().with_mi_charges_docs(true).
  • CLI: --mi-charges-docs (or --mi-charges-docs=false to say the default out loud), or mi_charges_docs = true in bca.toml; the CLI value overrides the key in either direction.
  • Python: mi_charges_docs=True on analyze, analyze_source, analyze_batch, analyze_paths and Ast.metrics.
  • The REST server has no request field for it; an embedder sets WebMetricsCfg::with_mi_charges_docs.

The LOC inputs it reads are the current ones, so a Python file whose docstring is an implicit concatenation ("""doc""" "more") or whose bare "a", "b" tuple statement was once a comment row reads a different comment share from a release that classified those rows the other way.

A baseline records which mode wrote it, and bca check --baseline warns when it reads mi.* entries under the other mode. Only mi.sei entries can disagree, and in one direction: a value recorded with documentation charged sits above the default’s, so reading it under the default can fire on untouched code, and a value recorded under the default can cover a real regression under the charged mode. Switching the mode therefore means reviewing any mi.sei threshold and regenerating the baseline in the same change, as for cyclomatic_count_try. bca check --print-effective-config names the mode in force.

A baseline also records the MI formula revision that computed its values (mi_formula in [provenance]; revision 2 since #1607, and an unstamped file reads as revision 1). When a baseline holding any mi.* entry was written under another revision, bca check --baseline warns that its values are not comparable, in place of the mode warning; refresh it with --write-baseline.

Arie van Deursen’s blog post Think Twice Before Using the Maintainability Index collects the historical context and a sharp critique of the metric.

アルゴリズム

The implementation is purely arithmetic: src/metrics/mi.rs consumes the already-computed Halstead, Cyclomatic, and LOC metrics and applies the three formulas. Because the formulas use the natural log of Halstead volume and of PLOC, MI is undefined for empty files; big-code-analysis returns 0.0 for any space with zero code lines or zero Halstead volume (a comment-only file or a docstring-only module has no volume). That 0.0 is a sentinel, not a score, and it cannot be told apart from a measured 0 or from mi.visual_studio’s clamp: mi::Stats::is_empty() identifies such a space in the library, and bca report’s averages skip it. The JSON value stays 0.0 until 3.0, which is planned to emit null instead (#1621).

読み方

MI was originally designed as a portfolio-level score: “how much maintenance pain should we expect from this codebase over the next year?”. It is stable across releases of a healthy system and tends to drop measurably before a system enters the “legacy” quadrant.

The common use is the Visual Studio traffic-light rendering: Visual Studio exposes mi.visual_studio through green, yellow and red indicators on a method, which is where many .NET developers meet it, and also why it has attracted much criticism. Treat it as a smoke detector, not a thermostat: a sudden drop is a useful signal, but the absolute number is noisy.

NArgs

NArgs counts the number of arguments declared by a function, method, or closure. The metric has no well-known origin paper; guidance on limiting argument counts appears in Kernighan and Plauger’s The Elements of Programming Style (1974) and Robert C. Martin’s Clean Code (2008), which suggests three arguments as a soft ceiling.

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, such as 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 gates each callable on its own parameter list, which is what every comparable tool measures: RuboCop Metrics/ParameterLists, ESLint max-params, Clippy too_many_arguments, lizard, SonarQube S107 and Pylint R0913 all count one callable at a time.

This is not the serialized total. The function_args and closure_args keys above are subtree sums: a function that declares two parameters and contains a three-parameter nested function reports function_args: 5. Before #1196 the gate read that sum, so a three-parameter function with a two-parameter sort comparator was flagged at 5, and the remediation its number implied, fewer parameters, was not the one that would clear it.

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.

Nothing escapes the narrower rule. In the twelve grammars whose closures open their own space — Rust, JavaScript, TypeScript, TSX, MozJS, C#, Go, PHP, Perl, Ruby, Lua and Elixir — a closure is gated on its own offender row, which is also where its fix belongs. In Python, Java, Kotlin, C++ (and its Mozilla dialect), Groovy and Objective-C a lambda, closure or block opens no space, so its arguments can only be attributed to the enclosing function; there the offender row shows the split:

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

The split identifies how many arguments come from the enclosing signature and how many come from lambdas.

Both lists are read from the per-language role tables rather than maintained by hand: a language is in the first when every kind its Closure row owns is also an opener of its FuncOpener row, and in the second when none is (closures_that_open_a_space_are_what_the_book_says in big-code-analysis-ast/src/roles/space_tree_tests.rs pins the two against this paragraph; Bash, C, Tcl and iRules have no closure at all).

0 と表示される言語

A metric that silently reports 0 reads as “no offenders” rather than “not measured”, so know 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.

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

読み方

A function with many arguments is hard to call correctly and harder to test exhaustively, since the test matrix grows roughly exponentially. The classic refactoring advice is the introduce parameter object pattern: when a function takes more than four related arguments, group them into a record / struct / dataclass.

The common use is as a review-blocking lint rule: most linters (Pylint’s R0913, ESLint’s max-params, Checkstyle’s ParameterNumber) flag functions with more than a configurable threshold. NArgs is also a useful component of API-design dashboards: public APIs whose average NArgs has crept upward over time tend to be ones that have accreted “just one more parameter” feature flags.

NExits

NExits counts the number of distinct exit points from a function: every explicit return, every throw / raise, and (in Rust) every ? early-return. A generator’s yield counts in Python, the JavaScript family, C# and PHP, because it hands control back to the caller, and so does C++’s co_yield; C++’s co_return is a return (#1547). The implicit fall-through return at the end of a function is not counted; only explicit exits are (see issue #243).

The metric goes back to the structured-programming literature of the 1970s, where Edsger Dijkstra and others argued that functions should have a single entry and a single exit point (the “SESE” rule). Modern thinking is more nuanced; see Steve McConnell’s Code Complete, 2nd edition (Microsoft Press, 2004), which recommends early returns as a clarity-improving pattern when they reduce nesting.

big-code-analysis は各関数の構文木を走査し、言語固有の出口ノードを特定し(src/metrics/nexits.rs の言語ごとの Exit トレイトを参照)、関数ごとのカウントに加えてファイルレベルの sum、average、min、max を報告します。シリアライズされたフィールド名は nexits で、本文で使っている略語表記と一致します。

読み方

Strict SESE coding standards (DO-178C for avionics, MISRA C for embedded automotive) still require an NExits of 1 per function, because multiple exit points complicate certified control-flow analysis. Outside those domains, an NExits of 2-4 is usually a good sign: it almost always means the function uses guard clauses to handle preconditions and then proceeds in a flat body.

A very high NExits, say above 8, is the warning sign. It usually means the function should have been split into several smaller functions, with each “successful branch” becoming its own helper.

NOM

NOM stands for Number Of Methods and counts every function, method, and closure defined inside a given scope (file, class, or namespace). It is one of the first metrics Mark Lorenz and Jeff Kidd introduced in their 1994 book Object-Oriented Software Metrics (Prentice Hall, ISBN 0-13-179292-X), where it is treated as the primary class-size indicator.

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 モジュールはスクリプト風のコードに典型的です。

Some constructs that carry executable code are deliberately counted by neither field: Kotlin property accessors (get() / set()) and init blocks, Java and Groovy static { … } initialisers, and JavaScript class static blocks. Each opens a function space, so it has its own complexity scores, bca check can flag it, and it contributes to WMC. But none is a callable you name at a call site, and counting an accessor as a method would make NPM bill the same property once as an attribute and again as a method, skewing the NPA/NPM ratio the OOP metrics exist to report. A Kotlin file of nothing but property accessors therefore reports nom.functions == 0 while still reporting their complexity (#1184).

読み方

NOM is the input to several other metrics: WMC sums cyclomatic complexity across the same set of methods that NOM counts, and NPM filters that same set down to public methods. As a standalone metric, the Lorenz–Kidd recommendation is ≤ 20 methods per class. The common use is as a God-class detector: a class with NOM in the dozens is almost always doing too much, and is a strong candidate for “extract collaborator” refactoring as documented in Martin Fowler’s Refactoring catalogue entry on Large Class.

NPA

NPA counts the number of public attributes (also called fields, properties, or instance variables) declared by a class or interface. It is part of the metric family introduced by Lorenz and Kidd in Object-Oriented Software Metrics (1994) and was later folded into the MOOD (“Metrics for Object-Oriented Design”) suite proposed by Brito e Abreu and Carapuça (1994).

big-code-analysis splits the count by definition-site kind: classes (concrete types with state) and interfaces (abstract contracts). The serialised output (src/metrics/npa.rs) is class_npa_sum (sum of NPA across all classes), interface_npa_sum (sum across interfaces), class_attributes (sum of all attributes, public or not, across classes), interface_attributes, class_cda (class density of public attributes, an accessibility ratio rather than an average), interface_cda, total, total_attributes, and cda. The per-language Npa trait decides what counts as “public” (Java public, C# public, Rust pub, Python’s “no leading underscore” convention, and so on) and what counts as “attribute” rather than “method”.

Which spaces carry NPA, NPM and WMC

NPA and NPM are emitted on container spaces (class, 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 direct consequence: 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 is a direct measure of encapsulation. Every public attribute is a piece of internal state that callers can read or write without going through a method, which means it is a piece of internal state the class cannot validate or evolve without breaking callers. The canonical guidance, first stated in Bertrand Meyer’s Object-Oriented Software Construction (Prentice Hall, 1988) and known as the Uniform Access Principle, is to keep NPA at or near zero and to expose state through public methods instead.

The common use is API-stability auditing: a public library class whose NPA grows over time accumulates breaking-change liability faster than its public-method surface.

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 is the public interface size. A class with NPM in the dozens is a class with too large an API contract: every public method is something callers can come to depend on, and every change to it is a breaking change. The Lorenz–Kidd guidance is ≤ 20 public methods per class, with anything over 40 considered a strong refactoring candidate. The same rule applies with particular force to interfaces in Java and C#, where the contract is the shape clients pin against.

The common use is as a public-API change tracker for libraries: monitoring NPM at the package level catches accidental expansion of a library’s surface area in the same way that NPA catches accidental exposure of internal fields.

Tokens

Tokens is a per-function and per-file count of the tree-sitter leaf tokens (identifiers, literals, keywords, punctuation), excluding any token whose AST ancestor is a comment node. It is a lexer-driven size proxy intended as a more formatting-resilient alternative to LOC. The same idea is known from Terry Yin’s lizard command-line tool, where many readers will first have seen a token-count metric.

The implementation lives in src/metrics/tokens.rs. Because Tokens counts every leaf, including punctuation that Halstead deliberately skips, the value will not equal Halstead N1 + N2, and because it counts tokens rather than lines it is not equivalent to any LOC variant. Whitespace-only reformatting, renaming a variable, and removing a comment all leave Tokens unchanged. Edits that change the tokens themselves (adding an if, adding optional braces around a single-statement block, or inserting or removing semicolons in a language where they are optional) do change the count.

読み方

Tokens is less sensitive to formatting than LOC. It is the right size measure when you are normalising another metric across languages or across teams with different style conventions: bugs per KSLOC is sensitive to formatting, while bugs per 1000 tokens is much less so.

The common use is as the defect-density denominator of choice in cross-language research: a 1000-line Java file and a 1000-line Lisp file contain very different amounts of code, but a 1000-token slice of each contains roughly the same amount of information. This makes Tokens useful for machine-learning code-quality models that train across many languages.

WMC

WMC (Weighted Methods per Class) is the first metric in the Chidamber and Kemerer suite, introduced in their 1994 IEEE Transactions on Software Engineering paper A Metrics Suite for Object Oriented Design (volume 20, issue 6, pages 476-493). The CK suite comprises WMC, DIT, NOC, CBO, RFC and LCOM. big-code-analysis currently implements WMC and the simpler size metrics (NOM, NPA, NPM), with the inheritance- and coupling-based ones tracked for future work.

WMC is the sum of the cyclomatic complexity of every method defined in a class. The original paper deliberately left the “weighting” abstract: Chidamber and Kemerer wrote that “if all method complexities are considered to be unity, then WMC = n, the number of methods”. The empirical follow-up literature has settled on cyclomatic complexity as the weight, and that is what big-code-analysis uses.

アルゴリズム

For each class or interface found by the per-language parser, big-code-analysis sums the standard cyclomatic complexity of every method body inside it (src/metrics/wmc.rs). The file-level serialised output is three fields: class_wmc_sum (sum of WMC across all classes in the file), interface_wmc_sum (sum across interfaces), and total (the two combined). No min/max/average aggregation is emitted at the file scope. To rank individual classes by WMC, use the report subcommand, which surfaces a Type hotspots (top N by WMC) section (see Commands → Report).

読み方

Chidamber と Kemerer は WMC について 3 つの仮説を提示し、そのすべてがその後繰り返し検証されてきました。

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

The common use is God-class detection: combined with NOM, WMC is one of the clearest signals that a class needs to be split. For a class with high NOM but low WMC, check whether the methods form a cohesive data API before splitting the class. A class with low NOM and high WMC has a few gargantuan methods (split the methods, not the class). A class with both high NOM and high WMC is the classic God class.


次のステップ

  • 対応言語 の章では、対応しているすべての言語と文法を一覧しています。一部のメトリクス定義(NPA、NPM、WMC)はクラスを持つ言語でしか意味をなさないため、メトリクスの対応範囲は言語によって異なります。
  • The Supported Change-history (VCS) Metrics chapter covers the complementary family derived from version-control history rather than from the source AST: commit frequency, churn, ownership, and the composite risk and hotspot scores.
  • Commands → Metrics のページでは、これらの数値の JSON / YAML / TOML / CBOR 出力を生成するための bca metrics の呼び出し方を説明しています。
  • レシピ の章では、これらのメトリクスから品質レポートを生成するエンドツーエンドの例を示します。ダッシュボードへのパイプライン連携も含みます。

対応する変更履歴(VCS)メトリクス

This chapter describes the change-history metrics: the signals big-code-analysis derives from version-control history rather than from the source AST. Where the Supported Code Metrics chapter measures the shape of the code as it exists today, these metrics measure how that code got there: how often it changes, how much, by how many people, and in what kind of commits. Each section starts from the empirical paper that first connected the signal to defects, walks through how big-code-analysis computes it, and explains how to read the number in practice.

このファミリー全体は bca vcs コマンドで計算され、bca metrics --vcs の出力に付加されます。Commands → Change-history (VCS) metrics のページが、フラグ、ウィンドウ、出力形式、キャッシュを説明しています。この章が 「なぜ」 を、あちらのページが どうやって を担います。

Four framing notes:

  • These metrics predict where defects cluster, not whether code is correct. The defect- and vulnerability-prediction literature consistently finds that process signals (how a file has been edited over time) out-predict product signals like size or complexity. Graves and colleagues put it bluntly in 2000: the number of times a module has been changed is a better predictor of its fault count than its length. None of these numbers measures correctness; they rank files by the probability that a bug is hiding in one.
  • すべては 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 に由来します。大規模な電話交換システムを研究した彼らは、変更履歴から導かれるプロセス指標が、コードそのもののどのプロダクトメトリクスよりも欠陥率をよく予測すること、なかでも過去の変更回数が最も強力な指標のひとつであることを見出しました。直観は単純です。誰も触れないファイルは、誰も壊していないファイルだということです。

アルゴリズム

A single history walk visits each commit reachable from the analysed ref (first-parent only by default; --full-history follows all parent links in the commit graph) and attributes it to every file it modifies. Merge commits are excluded unless --include-merges is passed, and renames are followed by default so a file’s history survives a move. The walk counts distinct commits per file per window, not diffs or hunks, so a commit that touches a file once and a commit that touches it in three hunks both count as one.

読み方

Commit frequency is a rate, so it is only meaningful relative to a baseline: this file versus its siblings, or this file this quarter versus last. A consistently high count flags a file that is under active feature development or structurally unstable; pairing it with code churn tells the two apart, since heavy churn on few commits is a different story from light churn on many.

コードチャーン

コードチャーンは変更の量です。ファイルに触れた追加行数と削除行数の合計で、ウィンドウごとに 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 はウィンドウごとの生のチャーンをシグナルとして保持し、単一の絶対値を判定として報告するのではなく、複合スコアがそれをサイズや新しさと組み合わせられるようにしています。

アルゴリズム

For every in-window commit touching a file, the walk adds that diff’s added-plus-deleted line count to the file’s churn total for the window. Because added and deleted lines are summed, a one-line edit counts as two (one deletion, one addition): churn measures activity, not net growth.

読み方

Churn is the natural numerator for a density ratio. Churn per commit distinguishes steady editing from a few large rewrites; churn against file size recovers Nagappan and Ball’s relative measure. A file whose recent churn is high relative to its long-window churn is changing faster now than its history would predict, which is the burst signal below.

バースト

バーストは、ファイルの活動のうち直近ウィンドウに集中している割合で、commits_recent / commits_long を [0, 1] の比率として報告します。1 に近い値はファイルのコミットのほぼすべてが直近のものであることを、0 に近い値はファイルが昔は活発だったがその後静かになったことを意味します。

The signal follows from the recency principle that runs through the change-history literature. Graves and colleagues found that recent changes weigh more heavily on fault incidence than old ones, and the just-in-time defect-prediction line (see the JIT score) is built on the same observation. A file whose change history is front-loaded into the present is in active flux, and active flux is when defects enter.

読み方

バーストは単独の警報ではなく、判定の決め手(タイブレーカー)です。総履歴の少ないファイルでの高いバーストは、若く動きの速いファイルを意味します。深い履歴を持つファイルでの高いバーストは、突然目を覚ました古いファイルであり、多くの場合こちらの方が興味深いケースです。経過日数と合わせて読むことで両者を見分けられます。

作者数と所有権

関連する 2 つのシグナルが、ファイルを 誰が 変更しているかを表します。作者数(authors_long、authors_recent)は、各ウィンドウでそのファイルに触れた個別の人数を数えます。所有権(ownership_top_share、[0, 1] の比率)は、最も活発な単一の作者に帰属する編集の割合です。低いシェアは所有権が分散していることを、高いシェアは一人が支配的であることを意味します。

Both trace to empirical work on ownership and security. Christian Bird and colleagues’ 2011 FSE paper Don’t Touch My Code! Examining the Effects of Ownership on Software Quality found, on Windows Vista and 7, that diffuse ownership (many low-expertise contributors, a low top-owner share) predicts both pre-release faults and post-release failures. On the security side, Andrew Meneely and Laurie Williams’ 2009 CCS paper Secure Open Source Collaboration: An Empirical Study of Linus’ Law reported that Red Hat Enterprise Linux 4 files touched by nine or more developers were roughly sixteen times more likely to harbour a vulnerability.

アルゴリズム

Author identities are canonicalised through the repository .mailmap and counted by lowercased email, so a contributor who commits under two addresses counts once. Co-authored-by: trailers add participants. Bot identities (dependabot[bot], renovate[bot], github-actions[bot], and similar) are excluded by default so automated churn does not inflate the count. ownership_top_share is the top author’s edit count over the file’s total edits in the window.

読み方

A rising author count on a long-lived file is a knowledge-diffusion signal: more people are now obliged to understand it. The composite risk score folds this in two ways. It scales the author factor by an ownership-dilution term (1 - ownership_top_share), so the same head-count counts for more when ownership is spread thin, and it adds categorical bumps at the six- and nine-developer marks that encode Meneely and Williams’ RHEL4 thresholds. For knowledge concentration across a set of files rather than within one, see the bus factor.

修正・セキュリティ・リバートコミット

big-code-analysis は、ファイルに触れる長期ウィンドウ内の各コミットをメッセージの意図で分類し、3 つのカウントを保持します。bug_fix_commits(バグ修正キーワードに一致するメッセージ)、security_fix_commits(CVE-####、security、vuln、exploit、sanitize などに一致するメッセージ)、revert_commits(リバートまたはロールバックである件名)です。

Reading a commit’s purpose from its message is the technique introduced by Jacek Śliwerski, Thomas Zimmermann, and Andreas Zeller’s 2005 MSR paper When Do Changes Induce Fixes?, the work now abbreviated SZZ. The premise is that a file’s history of corrective change is itself predictive: a file that has needed many fixes is a file likely to need more. A security fix is a sharper signal than an ordinary bug fix, and a revert marks a change that had to be undone, a localized admission that something went wrong there.

アルゴリズム

Classification is keyword-based on the commit subject and body (see src/vcs/classify.rs). It is deliberately simple and language- and tracker-agnostic: there is no issue-tracker linkage and no blame back-tracing to the fix-inducing commit, so this is the message-classification half of SZZ, not the full algorithm. The counts are kept over the long window only.

読み方

A high bug_fix_commits count is a file that keeps needing repair. The weighted composite score combines bug-fix and security-fix counts in a log-scaled term, with security fixes double-weighted, so a file with a history of security fixes ranks above an otherwise-identical file with only ordinary bug fixes. The file-level formula does not use revert_commits; only the just-in-time commit score reads the revert classification. Because the classifier reads messages, its accuracy tracks your project’s commit hygiene: a repository with terse or templated messages will under-count.

経過日数と最終更新

2 つのタイミングシグナルがファイルの履歴を区切ります。age_days はファイルの 最初の ウィンドウ内コミットからの経過日数(長期ウィンドウを上限とする)、last_modified_days は 最新の ウィンドウ内コミットからの経過日数です。

These anchor the recency reasoning the rest of the family depends on. A small age_days marks a new file, and newly added code carries elevated risk, an observation from the just-in-time line and from Chromium’s own defect analysis, where freshly added features were disproportionately fault-prone. A small last_modified_days marks a file edited recently, which the windows already weight.

読み方

age_days の最大の用途は、リスクスコアに供給される新規ファイルトリガーです。直近ウィンドウ内で初めて観測されたファイルは小さな加点を得ます。これは、できたばかりのコードが実行やレビューを経る機会を最も持っていないことを反映しています。last_modified_days は逆方向の陳腐化の読み取りです。何か月も触れられていない高リスクファイルと、今日編集されているファイルとでは、保守上の位置付けが異なります。

変更エントロピー

変更エントロピーは、ファイルに触れるコミットがどれだけ 「分散」 しているかを測ります。ウィンドウごとに change_entropy_long と change_entropy_recent として、ビット単位で報告されます。

The metric is Ahmed Hassan’s History Complexity Metric from the 2009 ICSE paper Predicting Faults Using the Complexity of Code Changes. The idea adapts Shannon entropy to commits: for each commit, take the distribution of its churn across the files it touched and compute that distribution’s entropy in bits. A commit that touches one file scores 0; a commit that spreads its churn evenly across n files approaches log₂(n). A scattered, cross-cutting commit is harder to reason about than a focused one. Hassan reported that this change-process complexity predicts faults better than prior code-based or change-count models; later work measured file-level change entropy at a Pearson correlation up to 0.54 with defect counts on eight Apache projects (see co-change entropy).

アルゴリズム

For each commit, big-code-analysis computes the Shannon entropy H of its churn distribution across the touched files (see src/vcs/entropy.rs). Each participating file is then credited its churn share pᵢ·H of that commit (Hassan’s History Complexity Metric), and these shares are summed per file per window. A file repeatedly caught up in diffuse, multi-file changes accumulates high change entropy; a file that only ever changes in tightly focused commits stays low.

読み方

変更エントロピーは、生のチャーンが同一であっても、大きく散漫な編集の一部として 変更されるファイルと、自己完結したコミットで変更されるファイルを区別します。高い変更エントロピーは 横断的関心事 の兆候です。このファイルへの編集が、いつも多くの他ファイルを巻き込んでいるということです。これは直近ウィンドウのリスク項に加算的に入り、チャーンやコミット数の言い換えではなく補完として働きます。

共変更エントロピー

Co-change entropy measures how wide a file’s change blast radius is: how many different files it tends to change alongside. It is reported per window as cochange_entropy_long and cochange_entropy_recent, in bits.

The signal comes from the 2025 study Co-Change Graph Entropy: A New Process Metric for Defect Prediction (arXiv 2504.18511). Build a weighted graph in which two files share an edge whenever they change in the same commit, the weight being the number of shared commits. A file’s co-change entropy is the Shannon entropy of its edge-weight distribution: low when it always co-changes with the same one partner, high when its changes ripple out across many different files. The study found that adding co-change entropy to change entropy improved the area under the receiver operating characteristic curve (AUROC, a measure of ranking discrimination) in 82.5% of cases over the prior signal set across eight Apache projects.

アルゴリズム

The walk records, per commit, which in-scope files changed together and accumulates the co-change graph’s edge weights, then computes each file’s edge-weight entropy per window. Bulk-import commits touching more than 1000 files are excluded from the co-change graph, whose edge count grows with the square of commit width, though they still contribute their (linear-cost) change entropy. A computed 0.0 means the file has no co-change neighbours or only one neighbour in the window. It is a real measurement, not a missing record.

読み方

Where change entropy asks “how scattered are the commits that touch this file?”, co-change entropy asks “how many other files does touching this one drag along?”. A high value flags a file whose edits have unpredictable, far-reaching consequences: a coupling hotspot. The two entropy signals complement each other, and the risk score v2 adds both recent-window terms together.

複合リスクスコア

リスクスコアは、上記のすべてのシグナルをファイルごとの単一の数値 risk_score に集約します。これは bca vcs の主要な出力であり、ランキングテーブルのソートキーとなるフィールドです。2 つの式が用意されており、どちらも単一の risk_score_version(現在は 2)で一緒にバージョン付けされるため、下流の消費者は変更を検出できます。

重み付き式(デフォルト)

The default formula is a log-scaled weighted sum with categorical multiplicative bumps. Counts are passed through ln(1 + x) so that the difference between 10 and 20 commits matters more than the difference between 110 and 120, matching how change activity saturates:

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 にあります。

The size term ln(1 + sloc)² / 100 is a real contributor, not the tie-breaker its position at the end of the sum might suggest: it reaches about 0.85 at 10k SLOC and exceeds 1.0 past roughly 50k SLOC, comparable in magnitude to the churn terms. Large files are only weakly correlated with defects, but the squared-log scaling keeps size a first-class additive signal rather than letting it dominate.

パーセンタイル式

--risk-formula percentile is the alternative: each signal is re-ranked to its percentile within the analysed set, and the per-file mean of those percentiles becomes the score. This formula replaces fixed weights with relative ranks within the analysed file set. Prediction research recommends relative triggers over fixed absolute thresholds, but this score is meaningful only within one run’s file set.

読み方

The score is ordinal. Sort a repository’s files by it and look at the top of the list; that is the ranking the score exists to produce. Do not compare a raw risk_score between two repositories, and do not read its magnitude as a defect probability. To watch a single file move over time, use bca vcs trend, which re-anchors the walk at each historical point so the series reflects what the file looked like then.

ホットスポットスコア

The hotspot score is the product of a file’s complexity and its recent churn: hotspot_score = cyclomatic_sum × churn_recent. It is an Option, present only when an AST complexity figure is computed alongside the history (for example bca metrics --metrics cyclomatic --vcs), because it needs both halves. A per-function block uses the space’s own cyclomatic instead of the subtree sum, so a nested function’s complexity is not counted again in every enclosing space.

The metric is the central idea of Adam Tornhill’s 2015 book Your Code as a Crime Scene and the CodeScene tooling built on it. The argument is that complexity on its own is cheap to ignore (a complex file nobody touches costs nothing) and churn on its own is cheap too. The danger is their intersection: code that is both complicated and changing often is where defects concentrate and where developer effort is repeatedly spent. Tornhill observes that a small fraction of a codebase typically accounts for a large majority of its change activity, and the hotspot score is built to find that fraction.

読み方

リスクスコアと同様、ホットスポットの積は順序尺度です。ファイルをこれでランク付けし、大きさそのものは読まないでください。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 の各エントリは各ディレクトリ配下の全ファイルを再帰的に対象として計算されます。

読み方

A bus factor of 1 means losing one person orphans the set, which is common, and correct, for a repository of mostly single-author files. Treat the number as a planning signal, not a guarantee: it is a heuristic over observed authorship within the long window, so “first authorship” means the earliest commit seen in that window, not necessarily a file’s true creation. Use it to find the directories where knowledge is dangerously concentrated and spread review accordingly.

ジャストインタイムコミットスコア

Everything above ranks files at a point in time. The just-in-time (JIT) score estimates the defect-induction risk of a single commit. A continuous-integration gate reviews that commit at check-in. bca vcs commit <commit> reports an ordinal risk_score and a per-group contributions breakdown.

特徴量グループとその符号は、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 ハンク数大きいほど ⇒ 高リスク
拡散度個別のサブシステム数とディレクトリ数、コミット内変更エントロピー分散しているほど ⇒ 高リスク
履歴the touched files’ priors (prior changes, distinct authors, fix counts, and their composite risk), measured before the commit波乱の履歴 ⇒ 高リスク
経験作者のそれまでのコミット数(長期・直近)経験が多いほど ⇒ リスクは低下(このグループは減算)
目的メッセージの修正 / セキュリティ修正 / リバート分類修正は加算、リバートは減衰

contributions ブロックは各グループの符号付き寄与を報告するため、消費者はコミットが 「なぜ」 その位置にランクされたかを確認できます。マージコミットはフラグ付けされ、第一親に対してスコア付けされます。ルートコミットと新規ファイルは構造上、事前値がゼロになるため、スコアはサイズと作者の経験に依拠します。これはファイル履歴のない変更に対して文献が定める通りの挙動です。素の git diff もスコア付けできます(bca vcs commit --diff)が、diff から計算できるのはサイズと拡散度のグループだけなので、その経路は意図的に部分的な partial_risk_score を出力します。

読み方

Like the file-level score, the JIT score is ordinal: rank commits by it, or compare a commit against the repository’s own commit-score distribution, but do not read the magnitude as a probability. Its intended use is a check-in gate: bca vcs commit HEAD --fail-above <N> exits non-zero when a commit scores at or above a threshold, with the threshold calibrated against your own history rather than treated as an absolute. Any change to the formula bumps a jit_score_version that is independent of the file-level risk_score_version.


次のステップ

移行: フラグ CLI からサブコマンド CLI へ

This page is for users of the old flag-style CLI (one process, many mutually exclusive --action flags) who are moving to the subcommand CLI (bca <verb>). It maps every old invocation to its replacement. The mapping table is the quick reference; the side-by-side examples show each in context.

変更の理由

The flag CLI overloaded --output-format with two unrelated meanings: per-file serialization (-O json/yaml/toml/cbor) and a post-walk aggregated report (-O markdown). It needed two clap ArgGroups plus runtime checks to police invalid combinations, and --top / --strip-prefix lived as global flags that only applied to one format. Each further aggregated format (HTML, for example) would have compounded the fragility.

サブコマンド CLI はこの構造を修正します。bca metrics と bca ops はファイルごとの出力を、bca report <FORMAT> は集約レポートを出力し、各動詞は自分専用にスコープされたフラグ集合を持ちます。

移行対応表

旧新
--metrics -O markdown (+ --top, --strip-prefix)report markdown
--metrics -O json/yaml/toml/cbormetrics -O json/yaml/toml/cbor
--metrics -O checkstyle/sarif/code-climate/clang-warning/msvc-warningcheck --threshold ... --report-format <fmt> [--output FILE]
--ops -O ...ops -O ...
--dumpdump
--find <NODE>find -t <NODE> [-t <NODE>...]
--count <LIST>count -t <NODE> [-t <NODE>...]
--functionfunctions
--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

Note: find and count now take one node type per -t / --type flag (repeat it for several) rather than a positional or a comma-separated string; the positional slot holds input paths.

関数のスパン

# 旧: 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

Preproc data: producer

# 旧
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

Preproc data: consumer

# 旧
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

実行時の移行ヒント

If you run a legacy invocation, the CLI prints a hint naming the recognized old flags and their new equivalents before clap’s own error:

$ bca --metrics -O markdown
note: the CLI was restructured into subcommands. See the migration guide for the full mapping:
  https://dekobon.github.io/big-code-analysis/migration.html
  --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 is the command-line tool. Each subcommand performs one analysis and takes its own flags. This section covers what every subcommand shares: installation, how flags and input paths are placed, which files the walk visits, and the exit-code contract. The subcommands themselves each have a page: Metrics, Change-history (VCS) metrics, Report, Check, Suppression markers, Nodes, the REST API, and Operating bca-web.

インストール

The bca command-line tool ships as a pip-installable wheel. The distribution name is big-code-analysis-cli and the installed command is bca. The two differ on purpose: the bca name on PyPI belongs to an unrelated project, and big-code-analysis is this project’s importable library bindings.

pip install big-code-analysis-cli   # PATH に `bca` コマンドをインストールします
bca --version

This puts the compiled bca binary on your PATH the way pip install ruff gives you the ruff command, with no Rust toolchain required. The wheel carries the full all-languages grammar set, so every supported language works out of the box. A single py3-none-<platform> wheel covers every CPython 3.x (and PyPy) on that platform. Prebuilt wheels ship for Linux (manylinux_2_28 x86_64 / aarch64), macOS (x86_64 / arm64), and Windows (x86_64). On any other platform pip falls back to a source build, which needs a Rust toolchain.

This is the binary CLI, distinct from the importable Python bindings (pip install big-code-analysis). The repository README describes the other install paths: Homebrew, .deb / .rpm / .apk packages, prebuilt release archives, and cargo install big-code-analysis-cli.

wheel のビルドと公開のマトリクスは .github/workflows/python-cli-wheels.yml で定義されています。

メトリクス

Metrics are quantitative measures of source code. They help to:

  • compare programming languages;
  • describe the quality of a codebase;
  • show developers where their code is hardest to handle;
  • surface potential problems early in development.

bca computes its metrics from the source text alone, without running the program; such metrics are called static metrics. See Metrics for the command and Supported Metrics for each metric’s definition.

ノード

To represent the structure of program code, bca builds an Abstract Syntax Tree (AST). A node is an element of this tree and denotes one syntactic construct of the language. The Nodes commands use them to:

  • print the syntactic structure of a source file;
  • find whether a construct is present in the analyzed code;
  • count the constructs of a given kind;
  • detect syntax errors in the source.

REST API

bca-web runs a server offering a REST API. Clients send source code over HTTP and receive the corresponding metrics as JSON.

フラグの位置と入力パス

Most subcommands read the input they analyze as a trailing positional path, so the common case reads like every other code tool (tokei, cloc, scc, rg). The exceptions: report and vcs select input with --paths, diff compares two result sets, and init targets a directory via --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)

Only -w / --warnings and --report-skipped are universal and accepted in any position. Every other shared flag lives in a help-grouped section on the subcommands that read it:

  • Input selection: -p / --paths, -I / --include, -X / --exclude, -l / --language, --paths-from, --exclude-from, --no-ignore, --no-skip-generated, --no-config.
  • Walker tuning: -j / --jobs, --exclude-tests, --cyclomatic-count-try, --mi-charges-docs.
  • Preprocessor: --preproc-data.
  • Output: --color.

A flag passed to a subcommand that never consumes it is a hard usage error (exit 1) rather than a silent no-op, so bca vcs commit --exclude-tests and bca list-metrics --paths both error, and bca list-metrics --help does not advertise walker flags.

-p / --paths フラグは引き続き機能し、位置引数のパスと 和集合 になります。つまり bca metrics a.rs --paths b.rs は両方を走査します。find と count サブコマンドは、繰り返し指定できる -t / --type フラグでノード種別を受け取ります(そのため位置引数のスロットはパス用に空いています): bca find -t function_item -t struct_item src/。

生成コードのスキップ

Generated files inflate metrics for code no human will refactor. By default, bca scans the first ~50 lines / 5 KiB of each file for a generated-code marker and skips matches before parsing, so the skipped file pays no tree-sitter parse cost.

認識されるマーカー(大文字小文字を区別しません):

  • @generated: a marker emitted by code generators.
  • DO NOT EDIT: a marker used by Go’s generated-code convention and other generators.
  • GENERATED CODE: Lizard’s marker, recognized for compatibility.

A marker phrase that appears only deep in the file body (past the scan window) does not trigger the skip; the detector deliberately looks only at the file header.

このスキップは bca metrics、bca report、およびしきい値エンジンに一律に適用されます。

フラグ

  • --no-skip-generated: disable the auto-skip, so every file is parsed.
  • --report-skipped: log skipped (generated): <path> to stderr for each file the detector excludes, so you can audit the exclusions. If a file is wrongly tagged, pass --no-skip-generated; an include filter does not override the detector.

さらに bca check は、ゲート実行で検出器がファイルを除外した場合に 1 行のサマリーを stderr に出力します(bca: 1 file not checked (1 generated) — pass --report-skipped to list them)。また、--strict プロファイルは検出器そのものを無効にします。マーカーはテスト対象のブランチが管理するコンテンツなので、プルリクエストのゲートでは、それによって検査対象のファイル集合が狭められるべきではありません。

.gitignore の尊重

--paths にディレクトリが渡されると、bca はデフォルトで .gitignore を考慮して走査します。次のいずれかに一致するファイルは、パース前にスキップされます:

  • 走査対象ツリー内の .gitignore ファイル。
  • .ignore files (the ripgrep / fd convention).
  • .git/info/exclude。
  • グローバル gitignore(~/.config/git/ignore、または core.excludesFile が指す先)。
  • 起点の祖先ディレクトリにある .gitignore ファイル(そのため、プロジェクトルートから bca metrics src/ を実行すると、プロジェクト最上位の .gitignore が反映されます)。

ウォーカーは、チェックインされた git リポジトリの外でも .gitignore を尊重します。そのため、.gitignore ファイルを含む展開済みのソース tarball も、git clone した直後と同じ扱いになります。

Hidden files (those whose basename starts with .) are filtered during the walk.

明示的なパスはフィルターを迂回する

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 run bca metrics --paths-from - from a git diff --name-only pipeline 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.

これには 2 つの境界があります。

  • -I / --include は引き続き適用されます。 許可リストは、名指しされたファイルの「どれ」を解析するかを絞り込むため、bca metrics -I '*.rs' notes.md は何も解析しません。上書きされるのは拒否セットだけです。
  • [check] exclude still applies. The gate-exemption set (--check-exclude / --check-exclude-from, or exclude under [check] in bca.toml) is not a walk filter: it drops violations, not files. It therefore survives an explicit path and reports bca: skipped N violations via [check.exclude] when it fires. A glob written in bca.toml resolves against the manifest’s directory whichever directory you run from (#1164); one passed as --check-exclude resolves against the working directory. See What a relative glob is relative to.

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.

明示的に名指しされたパスが実際にウォーカーの除外を上書きした場合、bca は stderr でそのことを伝え、該当するグロブを名指しします。

warning: utils/gate.py matches an exclude pattern (./utils/**) but was named explicitly; analyzing anyway

パス探索フラグ

  • --no-ignore: disable .gitignore / .ignore / global-gitignore awareness when expanding directory seeds. bca check counts the analyzable files ignore rules dropped from a gate run in its not-checked summary, lists every dropped entry under --report-skipped (note: skipped (ignored): <path>, note: skipped (ignored directory): <path>), and disables the awareness under its --strict profile, since an ignore file committed in the branch under test otherwise shrinks the checked set.
  • --paths-from <FILE>: read newline-separated input paths from <FILE>, or from stdin when <FILE> is -. Combined as a union with any --paths values. -I globs still apply; -X globs 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>: read newline-separated --exclude glob patterns from <FILE>, or from stdin when <FILE> is -. Patterns are unioned with any inline --exclude / -X values into a single deny-set; order does not matter. .gitignore-style: blank lines and lines whose first non-whitespace character is # are skipped, and a leading UTF-8 BOM is stripped. Convention is a .bcaignore at the repo root, mirroring .gitignore / .dockerignore. To pass a file literally named -, write ./-.

終了コード

bca はすべてのサブコマンドで単一の終了コード規約に従うため、CI スクリプトは出力を検査せずにプロセスのステータスで分岐できます:

コード意味
0成功。
1Tool error: a bad flag / threshold / glob spec, unreadable input, or a parse failure. This includes usage errors (unknown flag, bad subcommand, a malformed --threshold value rejected by clap). Never a metric signal.
2メトリクスゲート: check のしきい値を超過した、vcs commit --fail-above に違反した、または --exit-code 指定時の diff / diff-baseline がフィルター後の空でない差分を検出した場合。
3–5check --exit-codes=tiered のみ: 段階化された違反の重大度(回帰のみ / 混在 / ハード違反。tiered モードではコード 2 は新規のみを意味します)。

Codes 2–5 are gate signals, emitted only by check, vcs commit --fail-above, and diff / diff-baseline under the opt-in --exit-code flag; they report a metric result, not a failure of the tool. Every other subcommand (metrics, ops, report, diff, diff-baseline, exemptions, init, and the rest) exits 0 on success and 1 on error. Code 1 is reserved for tool errors, usage errors included, so a mistyped flag never lands in the gate band and CI can always tell “the gate found a regression” (2–5) from “the tool itself failed” (1).

読み取れない入力

Every source-walking subcommand (metrics, ops, report, functions, find, count, dump, exemptions, preproc, strip-comments, check) exits 1 if any selected input file cannot be read: permission denied, or a path that vanished between the walk and the read. This includes init, which uses check to scaffold its baseline, and diff --since. Each failure names the path on stderr as error processing <path>: …, followed by a summary.

The rule is “any read failure”, not “no output at all”, because a missing file is invisible in the result. A partial metrics --output document, a report, or a count looks complete; a diff --since side that lost a file reports it as added or removed rather than as the I/O error it was. The exit code is the only place the failure can be observed, so it has to carry it.

Output streamed during the walk is still emitted, because the check runs after it. A mixed run therefore still shows the files that were read: the stdout trees of metrics / ops / dump / find / functions, and the per-file documents of --output-dir.

Output assembled after the walk is suppressed entirely, because a partial one is indistinguishable from a complete one. That covers the metrics --output / ops --output aggregate document, the report document, count’s tally, preproc’s JSON, and the exemptions report; none of them is printed or written.

開けないファイルがツリーに正当に含まれている場合は、--exclude で除外してください(あるいは --include でより狭い集合を指定します)。これらのフィルタで除去されたファイルは開かれないため、読み取り失敗にはなりません。

列挙できないディレクトリ

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 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, because the failure happened before the filter could apply.

隣接する 2 つのケースは、設計上、致命的にはなりません。

  • 不正な形式の ignore ファイル、またはその中のコンパイルできないパターン。 ウォーカーはこれらを同じチャネルで報告し、警告の形も同一ですが、これらが記述するのは走査が失ったファイルではなく、走査がどう「構成」されたかです。カウントされるのは基盤に I/O エラーを伴うエラーだけなので、.gitignore に紛れ込んだタイポがビルドを失敗させることはありません。
  • 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 (a dangling link or a typo) is its own error, also exit 1.

書き込めない出力

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.

唯一の免除は、閉じられた下流のパイプです。bca dump | head は失敗ではなく日常的な操作であるため、BrokenPipe は握りつぶされ、実行はそれでも 0 で終了します。

メトリクス

bca metrics computes per-file metrics and emits them to stdout, to a single aggregate file (--output), or to a directory of per-file structured files (--output-dir). It is the command for feeding downstream tooling; for a human-readable summary see bca report, and for a pass/fail gate see bca check. The shared walker flags (paths, includes, ignore handling) are described in Commands.

Migrating? This command replaces the pre-restructure --metrics flag. The aggregated report previously selected with -O markdown now lives under bca report, and the CI/IDE offender formats (Checkstyle, SARIF, code-climate, clang-warning, msvc-warning) moved to bca check --report-format <fmt>. See the migration guide.

メトリクスの表示

指定したファイルまたはディレクトリのメトリクスを計算して表示するには、次を実行します:

bca metrics --paths /path/to/your/file/or/directory
  • --paths(または -p): 解析するファイルまたはディレクトリ。ディレクトリを指定すると、その中のすべての対応ファイルについてメトリクスが計算されます。パスは位置引数でも指定できます(bca metrics file.rs dir/)。

Explicitly named files must be parseable. When you name a file directly (positionally or via --paths / --paths-from) whose language the tool cannot recognize, bca prints a warning on stderr. If the run produced no output at all it exits 1, the same way a nonexistent explicit path fails; a mixed run that analyzed at least one file still exits 0 with the warning. Pass --language <lang> to force a parser when a file’s extension lies about its contents. Files reached only by walking a directory are skipped silently, since a tree full of READMEs and configs must not be noisy; pass -w to surface those skips too.

A file that is recognized but cannot be read is a stricter case and follows the unreadable-input rule: the run exits 1 even when other files analyzed fine, because a silently shorter metrics document reads as a complete one.

メトリクスのエクスポート

bca metrics supports five per-file output formats: CBOR, CSV, JSON, TOML, and YAML. JSON and TOML can also be pretty-printed.

トップレベルの 3 種類の出力は 3 つの別々のコマンドに対応しており、それぞれが自身のデータモデルとの一貫性を保ちます:

コマンド出力対象
bca metricsファイル単位のメトリクスツリー下流のツール
bca report集約された品質ダッシュボード人間 / プルリクエスト
bca checkしきい値違反レポートCI / IDE

The CI/IDE offender formats (Checkstyle, SARIF, code-climate, clang-warning, msvc-warning) used to live on bca metrics -O <fmt>. They moved to bca check --report-format <fmt> because their input is a list of threshold violations, not the per-file metric tree the other formats carry. See Exporting offender records for the new invocation.

エクスポートコマンド

メトリクスを JSON ファイルとしてエクスポートするには:

bca metrics --paths /path/to/your/file/or/directory \
    -O json --output metrics.json
  • -O, --format: output format. Defaults to text, a human-readable colored metric tree printed to stdout; pass --format text to request that default explicitly (for example to override a bca.toml that set a structured format). The structured per-file serializers are cbor, csv, json, toml, and yaml. --output-format is accepted as a deprecated alias and is slated for removal in the next major.
  • -o, --output: a single file holding one aggregate document for the whole run, a top-level array of the per-file results (TOML wraps the array under a files key; CSV concatenates each file’s rows). If omitted, results are printed to stdout.
  • --output-dir: 入力ファイルごとに 1 つのドキュメントを保持するディレクトリで、各ドキュメントは入力パスにフォーマットの拡張子を付けた名前になります。--output とは相互排他で、両方を渡すとエラーです。
  • CBOR はバイナリのため、出力先(--output または --output-dir)が必須です。構造化された --format なしでいずれかの出力先を渡すとエラーになります(デフォルトの text フォーマットは stdout にストリームし、ファイルを書きません)。これにより、出力先の指定が黙って無視されることはありません(#661)。
  • --metrics <name,…>: restrict computation to a subset of metrics, comma-separated or repeated (--metrics cyclomatic,cognitive --metrics loc). Names are the canonical ids bca list-metrics prints, the same vocabulary bca check --threshold and bca diff --metric use. Dotted (cyclomatic.modified) and bare loc sub-metric (sloc) spellings are accepted, as is a family name (loc). Each selects its whole family: --metrics sloc prints the full loc block. The Python bindings’ metrics= takes the same names. Derived metrics pull in their dependencies automatically. An unknown name errors with a “did you mean” hint. Omit it to compute every metric (#691).

CSV (spreadsheets and pandas)

bca metrics --paths /path/to/your/code \
    -O csv --output-dir csv-output

The CSV writer emits one row per FuncSpace (function, class, struct, unit, and so on) with the entire metric matrix as columns, a shape a spreadsheet or pandas loads directly. Header order is fixed; CSV_HEADER in src/output/csv.rs is the canonical list. Identity columns come first (path, space_name, space_kind, start_line, end_line), followed by every leaf metric under the same dotted JSON-style names (loc.lloc, halstead.volume, cyclomatic.modified.average), so a single column name addresses the metric in both CSV and JSON.

Empty cells (no value, not 0) signal “not applicable for this space”; for example, the OOP-only metrics (wmc.*, npm.*, npa.*) appear empty for procedural code. RFC 4180 quoting is delegated to the csv crate, so paths and names containing commas, quotes, or newlines round-trip cleanly.

Redirect stdout to collect the CSV output in one file:

bca metrics --paths /path/to/your/code -O csv \
    > metrics.csv

CSV はファイル単位のフォーマットです。--output-dir <dir> を指定すると、各入力ファイルは出力ディレクトリ配下に <input>.csv のミラーを生成します。--output <file> を指定すると、すべてのファイルの行が 1 つの集約 CSV に連結されます。

An aggregated HTML report covering the whole walk is available via bca report html. The previous per-file bca metrics -O html writer was removed because it degraded to an unopenable single-file table on real-world repos; CSV is the right shape for flat per-FuncSpace rows.

整形出力

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 ファイルでは、主要メトリクスに本番コードとテストコードが混在します。

Pass --exclude-tests to elide test-only subtrees before any metric is computed. The flag is recognised by every subcommand that walks a source tree (metrics, ops, report, check, dump, find, count, functions, and the rest of the walker group), and currently understands the following Rust attribute shapes:

  • #[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 アイテムに付く #![cfg(test)](内部属性形式)

Languages without a Checker::should_skip_subtree override ignore the flag; only Rust applies the pruning today. The default remains off so existing metric numbers stay byte-identical for users who do not opt in.

To opt a whole project in without repeating the flag, set exclude_tests = true in the repo’s bca.toml manifest. Because --exclude-tests is presence-only (no =false form), the manifest key can only turn pruning on; a CLI --exclude-tests still wins, but the manifest cannot turn it back off.

Pruning lowers the node-counted metrics (cyclomatic, cognitive, Halstead, nom, nargs, and the rest), and loc.sloc drops with them. Unlike node-counted metrics, loc.sloc comes from source spans. The walker subtracts each pruned subtree’s rows from every enclosing span, up to the file. Rows a pruned item shares with retained code or comments are kept, so sloc never falls below the ploc and cloc that survive the prune.

集約レポート

For a human-readable quality report, use bca report markdown. That command aggregates metrics across all analyzed files and produces per-language hotspot tables.

利用可能なメトリクスの一覧

CLI を駆動するツールは、メトリクスのカタログをハードコードする代わりに実行時に検出できます。

bca list-metrics

はメトリクス名を 1 行に 1 つずつ出力します。各メトリクスの 1 行要約を得るには descriptions を渡します。

bca list-metrics descriptions

変更履歴(VCS)メトリクス

bca vcs ranks files by change-history risk: signals derived from git history rather than the source AST. It is the project’s one language-agnostic, non-AST metric family. The goal is to surface the files most likely to harbour bugs or vulnerabilities, using the signals the empirical defect- and vulnerability-prediction literature most consistently backs. This page is the command reference; Change-history metrics defines each signal and cites its source.

A single history walk runs once per invocation (never per file) and produces per-file signals over two configurable windows: a long window (default 12mo, about 365 days) and a recent window (default 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
    ...

With no --format, a human-readable ranked table is printed. Pass --format markdown|html for a rendered report page, or --format json|yaml|toml|cbor|csv for structured output. Unlike bca metrics / bca ops (whose --output-dir is a directory of per-file emissions), a change-history report is a single whole-repo document, so bca vcs --output <file> writes one file (CBOR, being binary, requires --output). The shared --paths / --include / --exclude / --no-ignore filters pick which tracked files to report.

bca vcs exits with an error when run outside a git working tree.

ファイルタイプのスコープ

By default bca vcs ranks only the files bca computes metrics for, the same set bca metrics would analyse. High-churn non-source files (CHANGELOG.md, Cargo.lock, generated config) carry no maintainability meaning yet maximise the churn / commit / author signals, so ranking them beside source code is noise. Scoping to files-with-metrics also keeps the standalone ranking aligned with the AST hotspot tables in bca report --vcs.

--file-types <SCOPE> でスコープを選択します。

値意味
metrics (デフォルト)bca が言語 / メトリクスを持つファイルのみ(拡張子で判定)
all追跡中の、バイナリでもシンボリックリンクでもないすべてのテキストファイル
rs,py,toml,…カンマ区切りの拡張子許可リスト(先頭のドットは省略可、大文字小文字は区別しない)
bca vcs                          # ソースファイルのみをランク付け(デフォルト)
bca vcs --file-types all         # 追跡中のすべてのテキストファイルをランク付け
bca vcs --file-types rs,py       # Rust と Python のファイルのみをランク付け

The check is extension-only (no file content is read) and ANDs with the --paths / --include / --exclude / --no-ignore filters: a file must pass both to be ranked. Extension-less files (Makefile, Dockerfile, LICENSE) and unknown extensions are out of the metrics scope; a custom list is a literal extension filter, so it can include a non-metrics type like toml. An empty or all-blank custom list is a clear error rather than a scope that silently ranks nothing.

レンダリングされたレポートページ

bca vcs --top 50 --format html --output vcs.html
bca vcs --top 50 --format markdown --output vcs.md

--format html produces a self-contained, sortable page styled like bca report html (click any column header to re-sort); --format markdown produces the same ranked table as GitHub-Flavored Markdown. Both render every signal column, the same data the structured formats carry. The column set is defined once and shared by both renderers, so they cannot drift.

ランキングをスタンドアロンページではなく集約品質レポートに組み込むには、bca report --vcs を渡します。これは report markdown / report html に「Change-history risk」セクションを追加します。

シグナル

フィールド型説明
commits_long / commits_recentu32各ウィンドウでファイルに触れた個別コミット数
churn_long / churn_recentu64各ウィンドウにおける Σ(added + deleted) 行数
authors_long / authors_recentu32各ウィンドウにおける正規化済み作者 ID の個別数
ownership_top_sharef64 ∈ [0,1]トップ作者に帰属する編集の割合(低いほど所有権の希薄化が進んでいる)
burstf64 ∈ [0,1]commits_recent / commits_long
bug_fix_commitsu32メッセージがバグ修正キーワードに一致する長期ウィンドウのコミット数
security_fix_commitsu32セキュリティキーワード(CVE-####、security、vuln、exploit、sanitize など)に一致する長期ウィンドウのコミット数
revert_commitsu32件名がリバート / ロールバックである長期ウィンドウのコミット数
age_daysu32ファイルのウィンドウ内で最初のコミットからの経過日数(長期ウィンドウが上限)
last_modified_daysu32ファイルのウィンドウ内で最も新しいコミットからの経過日数
change_entropy_long / change_entropy_recentf64ウィンドウごとの変更エントロピー(ビット単位、下記参照)
cochange_entropy_long / cochange_entropy_recentf64ウィンドウごとの共変更グラフエントロピー(ビット単位、下記参照)
risk_scoref64Composite, formula-versioned (see below); ordinal, not cardinal
hotspot_scoref64?complexity × churn_recent。AST メトリクスが同時に計算される場合のみ存在します
risk_score_version / vcs_schema_versionu32Forward-compatibility version stamps. Carried once on the report envelope, alongside long_window_days / recent_window_days, not repeated inside each per-file vcs block (issue #635)

Author identities are canonicalised through the repository .mailmap and counted by lowercased email; Co-authored-by: trailers add participants. Bot identities (dependabot[bot], renovate[bot], github-actions[bot], and the like) are excluded by default. Both the built-in bot pattern and a custom --bot-pattern match case-insensitively against substrings of the author name and email. For example, --bot-pattern renovate matches Renovate Bot. Prefix the pattern with (?-i) for case-sensitive matching. Binary files and symlinks are skipped; an untracked file has no record at all (distinct from a tracked file with zero in-window activity).

変更エントロピーと共変更エントロピー

2 つのプロセスエントロピーシグナル(risk_score_version 2 で追加)は、ファイルの変更量だけでなく、「どのように」変更されるかを捉えます。

  • Change entropy (Hassan, 2009, Predicting Faults Using the Complexity of Code Changes). For each commit, the Shannon entropy (in bits) of its churn distribution across the files it touched measures how scattered that change was: a one-file commit is 0; a commit spreading churn evenly across n files approaches log₂(n). Each file is then credited its churn share pᵢ·H of every commit it took part in (Hassan’s History Complexity Metric). Higher means the file is repeatedly caught up in diffuse, cross-cutting changes. Later work (arXiv 2504.18511, below) measured file-level change entropy at a Pearson correlation up to 0.54 with defect counts on eight Apache projects.
  • Co-change graph entropy (arXiv 2504.18511, 2025). Files that change in the same commit are joined by a weighted edge (weight = number of shared commits). A file’s co-change entropy is the Shannon entropy of its edge-weight distribution: low when it always co-changes with the same partner, high when its changes ripple across many different files. Combined with change entropy it improved the area under the receiver operating characteristic curve (AUROC) in 82.5% of cases over the v1 signal set on eight Apache projects.

Both are reported per window. A 0.0 is computed, not missing: the file only ever changed alone (no co-change neighbours, or single-file commits with zero change entropy). Bulk-import commits touching more than 1000 files are excluded from the co-change graph, whose edge count grows O(width²), but still contribute their O(width) change entropy.

複合リスクスコア

デフォルトの加重(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 にあります。

The score is ordinal: only relative ranks have meaning. A single risk_score_version (now 2) versions both formulas: any change to the weighted sum or the --risk-formula percentile blend bumps it, and the recent entropy pair joins both.

--risk-formula percentile is an alternative: each signal is re-ranked to its percentile within the analyzed set, then averaged. The literature recommends relative triggers over hard thresholds for cross-project robustness.

フラグ

フラグデフォルト意味
--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>現在時刻Reference “now” (RFC 3339 / @unix / git date) for reproducible snapshots
--risk-formula {weighted|percentile}weighted複合スコアの式
--emit-author-detailsオフEmit SHA-256-hashed canonical author IDs
--author-hash-key <KEY>未設定Harden the emitted author digests into a keyed HMAC (see Author-detail privacy); requires --emit-author-details
--include-deletedオフ対象 ref で削除済みのファイルもランク付けする
--no-cacheオフ永続履歴キャッシュをスキップする(常に新規に走査)
--clear-cacheオフ実行前にこのリポジトリのキャッシュ済み履歴を消去する
--cache-dir <DIR>プラットフォーム標準のキャッシュキャッシュディレクトリを上書きする

キャッシュ

On large, active repositories, walking the long-window history dominates runtime. Most consecutive CI runs add only a few commits. bca vcs therefore keeps a persistent cache of history walks, keyed by the resolved HEAD SHA and repository identity:

  • 変更のないツリーでは前回の結果が再生され、履歴走査は行われません。
  • HEAD が前進した場合、走査は新しいコミットのみを訪れ、それをキャッシュ済み履歴に継ぎ足します。
  • フォースプッシュ(キャッシュされた head が新しい head の祖先でなくなった状態)では、完全走査にフォールバックします。

The cache is a pure optimization: a hit is bit-identical to a fresh walk, and the time windows are recomputed against the current moment on every run rather than frozen when the entry was written. An entry is ignored, and the history recomputed, whenever the schema, the score-formula version, the walk-affecting options, the repository’s effective .mailmap, or the git diff configuration that decides how gix diffs (diff.algorithm, core.bigFileThreshold, and a diff.<driver> section’s algorithm, binary, and whether it sets a textconv; a driver’s command never runs here and does not count), or the shallow boundary of a shallow clone (a git fetch --deepen or --unshallow leaves HEAD where it was but reaches commits the entry never saw) differ; in particular changing a window forces a fresh walk. One walk input is not covered: the diff attribute bindings (.gitattributes, .git/info/attributes, the global attributes file) that route a path to a diff driver or mark it -diff, so after changing those run once with --clear-cache. A bca release that adds a term retires every existing entry, so the first bca vcs after such an upgrade re-walks the whole long window once. Nothing deletes an entry that no longer matches, since a run with other options may still use it; --clear-cache removes the repository’s entries, retired ones included. Finalization-only knobs such as --risk-formula, --emit-author-details, --author-hash-key, and --include-deleted are applied on replay, so they reuse the same cached walk; a cached walk even re-finalizes under a different author-hash key without re-walking.

By default the cache lives under $XDG_CACHE_HOME/big-code-analysis/vcs (%LOCALAPPDATA% on Windows, ~/.cache otherwise). Author identities are stored only as their SHA-256 digests, never plaintext, so the cache holds no raw author emails. This is pseudonymization, not anonymization: the digests are recoverable against a candidate email set (see --emit-author-details). The same cache transparently accelerates bca metrics --vcs and 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 パラメータを通じて同じ挙動を提供します。

The cache is specific to the file ranking. The trend and commit subcommands, and the /v1/vcs/trend and /v1/vcs/jit endpoints, do not use it, so the cache flags do not apply there: passing --no-cache / --cache-dir alongside a subcommand is a usage error, and the trend endpoint rejects a no_cache / cache_dir field rather than silently ignoring it (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 (which implies --vcs) additionally attaches a vcs block to every nested function, method, and class space. It blames each file once with git blame and buckets the surviving lines into the AST function spans, so you can rank the risky function inside a risky file:

$ 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 は、最終変更がウィンドウ内に収まる現存行を数えるものであり(過去の追加+削除のチャーンではありません)、所有権は変更したコミットごとに加算されます。ウィンドウ内で誰も変更していない関数のカウントはゼロと報告されます。最終変更が長期ウィンドウより前の行は、関数のサイズには寄与しますが、ウィンドウ付きのどのカウントにも寄与しません。

Each nested space’s hotspot_score multiplies its own cyclomatic (cyclomatic.value) by its churn_recent, not its subtree cyclomatic.sum, so a nested function’s complexity is not counted again at every enclosing level; the file-level block keeps the subtree sum. When cyclomatic is computed, the Python bindings’ vcs_per_function=True reports the same scores.

Limitations. Blame follows file renames (so edits under a former path still attribute), but attributes a line moved between functions to its current position only. A function split into two has no record of its pre-split identity, and a deleted-then-recreated function attributes to the recreating commits. If a file cannot be blamed (untracked, or the rare gix-blame failure on pathologically repetitive content), its per-function blocks are omitted while the file-level block and the AST metrics still emit.

ジャストインタイム(コミットレベル)スコアリング

Where everything above ranks files at a ref, bca vcs commit <commit> scores a single commit for defect-induction risk, the unit a CI gate reviews at check-in. (The subcommand was renamed from bca vcs jit in 2.0; the old jit spelling keeps working as a hidden alias for one release cycle. “Just-in-time (JIT)” stays the literature term, below.) It is a static, rule-based scorer (no trained model, so nothing drifts as the project ages), with the feature groups and signs taken from the just-in-time defect-prediction literature: Kamei et al., A Large-Scale Empirical Study of Just-in-Time Quality Assurance, IEEE TSE 2013, with the open replications Commit Guru (FSE 2015) and 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 ハンク数大きいほど ⇒ 高リスク
拡散度個別のサブシステム数とディレクトリ数、コミット内の変更エントロピー分散しているほど ⇒ 高リスク
履歴the touched files’ priors (prior changes, distinct authors, bug- and security-fix counts, and the composite risk_score), measured from history before the commit荒れたファイル履歴 ⇒ よりリスキー
経験作者の過去のコミット数(長期・直近)経験が多いほど ⇒ リスクは低下(このグループは減算)
目的メッセージの修正 / セキュリティ修正 / リバート分類修正は加算、リバートは減衰

contributions ブロックは、順序尺度である risk_score への各グループの符号付き寄与を報告するため、コミットが「なぜ」その順位になったのかを利用側が確認できます。ファイルレベルの risk_score と同様、このスコアは順序尺度です。スコアでコミットをランク付けしたり、リポジトリ自身の分布とコミットを比較したりできますが、その大きさを確率として読み取らないでください。計算式が変更されると jit_score_version が上がります(ファイルレベルの risk_score_version とは別です)。

The commit is scored against its first parent. A merge commit is flagged (is_merge, parent_count ≥ 2) and scored against that first parent. A root commit and any new files carry zero priors by construction; the score then leans on size and author experience, as the literature prescribes for changes with no file history.

ウィンドウ / --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> scores a git diff instead of a commit (use --diff - to read the diff from stdin). This suits a pre-commit hook or a code-review bot, where the change exists only as a diff and has not been committed yet.

git diff --cached | bca vcs commit --diff - --pretty

The input must be a git-style unified diff carrying diff --git file headers, as produced by git diff or git format-patch. Plain diff -u / diff -ru output (which has ---/+++ header lines but no diff --git header) parses to zero files, and combined / merge diffs (git diff --cc, with @@@ hunk headers) are rejected as a malformed diff; pipe a regular two-way git diff instead.

A bare diff carries no author, parent, or file history, so only the size and diffusion groups are computable. The output is therefore a deliberately partial report, a distinct shape from a commit report:

$ 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 }
}

The source field is a permanent "diff" marker, and the history / experience / purpose groups are absent from the report entirely, not present as zero. Zero is a real value (a commit genuinely with no prior history scores those groups at zero); an absent group means “unavailable”, so a consumer can never mistake an unscored group for “low risk”. For the same reason the score field is named partial_risk_score, not risk_score.

A diff-only score is not comparable to a commit score. The partial score sums only size + diffusion; the full commit score for the same change also folds in history, experience, and purpose, and because experience is negatively signed the two are not even ordered (an experienced author can push the full score below the partial one). Rank diffs against other diffs, never against commit scores. --diff and the positional <commit> are mutually exclusive; --fail-above works in both modes (calibrate the diff-mode threshold against your own diff-score distribution).

パーサーは 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" } を渡すとコミットの JitReport JSON が返り、{ "id", "diff" } を渡すと部分的な diff レポートが返ります。REST API の利用を参照してください。
  • Python: vcs.commit(repo_path, commit=...) はコミットレポートを dict として返し、vcs.score_diff(diff) は部分的な diff レポートを返します。変更履歴(VCS)メトリクスを参照してください。

ML ベースの JIT モデルとサーバーサイドフック統合は、引き続きスコープ外です。

履歴トレンド(時系列)

A single bca vcs run answers “what is risky now”. bca vcs trend answers “is it getting better or worse”, the question a technical-debt programme acts on, by sampling the metrics at several points in time and emitting a per-file time series.

$ 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 で各リストを切り詰められます。

Each point re-anchors at the mainline tip that existed at or before that moment; it does not just re-window today’s HEAD tree. That is what makes a file born later show as null at older points rather than leaking its present-day metrics backwards. Files kept in the series are the --top highest-risk by their most-recent sample.

親コマンド bca vcs から再利用されるフラグ:ウィンドウ(--long-window / --recent-window)、--ref、--file-types、ボット / マージ / リネームのトグル、--as-of(最新のアンカー)、および --top。-O は json(デフォルト)、yaml、cbor を受け付けます。存在しない時点は null としてシリアライズされ、TOML はこれを表現できないため、TOML は除外されています。時点数は、深い履歴でも時点ごとの履歴走査が扱いきれる範囲に収まるよう、2–120 に制限されています。

リネームに関する注意。 リネームは各サンプルの走査「内」では追跡されますが、2 つのサンプルの「間」でリネームされたファイルは、1 本の連続した線ではなく、2 つの別々のパス系列(旧名、次に新名)として現れます。サンプル間のリネームの縫合は後回しのフォローアップです。

バスファクター(ディレクトリ・リポジトリレベル)

Where the per-file ownership_top_share measures concentration within a file, the bus factor (also called the truck factor) measures it across a set of files: the minimum number of developers whose departure would leave more than half of a directory’s files without a knowledgeable maintainer. bca vcs emits it as a top-level vcs_aggregate object alongside the ranked files:

{
  "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 }
      ]
    }
  }
}

Each developer’s authorship of each file is scored with the Avelino Degree-of-Authorship heuristic (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 は各トップレベルディレクトリとその直下のサブディレクトリをカバーし、それぞれ再帰的に配下のすべてのファイルに対して計算されます。

構造上の注意点:

  • With the default threshold, a repository whose files all share one sole author has a bus factor of 1. Single-author files owned by different developers can produce a higher bus factor. This is the heuristic working as intended, not a bug; treat the number as a planning signal, not a guarantee.
  • ボットのアイデンティティは(ファイルごとのシグナルと同様に)フィルタリングされ、ウィンドウ内に活動のないファイルは作者性を持たず、分母から除外されます。
  • 「最初の作者性」は「長期ウィンドウ内で観測された」最古のコミットを意味し、必ずしもファイルの真の作成を意味しません。

The aggregate reflects the whole repository within the file-type scope: one history walk covers every in-scope file, by default the files-with-metrics set (see File-type scope), so --file-types all widens the bus factor to every tracked file. --paths / --include / --exclude scope only the ranked per-file list, not the bus factor. To focus on a subsystem, read its entry in by_directory rather than filtering the walk.

--emit-author-details adds a key_author_ids list to each group: the SHA-256-hashed identities of the removed key developers, in removal order (plaintext identities never leave the process). The aggregate is computed only for the dedicated bca vcs / bca report --vcs reports and the REST / Python endpoints; the per-file bca metrics --vcs injection path does not pay for it.

作者詳細のプライバシー

The key_author_ids digests are a stable pseudonym, not anonymization. Hashing keeps plaintext emails out of the report and the cache and deters casual disclosure, but the hash is not cryptographically irreversible. The pre-image is an email, which is low-entropy and enumerable, and commit histories are public, so anyone with a candidate set of emails can recover which digest belongs to whom by hashing each candidate or with a precomputed email→hash table. This is the same weakness that broke Gravatar’s email hashing.

公開された key_author_ids(およびファイルごとの author_ids)は、平文のメールアドレスの出力を避ける仮名化として扱い、執念深い攻撃者によって作者が再識別されないことの保証としては扱わないでください。その保証が必要なら、ダイジェストを公開しないでください。

強化モード:--author-hash-key

For stronger resistance, pass a secret key with --author-hash-key <KEY> (requires --emit-author-details). The emitted digests then become an HMAC, HMAC-SHA256(key, SHA-256(email)), instead of a bare hash: an attacker without the key can no longer hash a candidate email to recognise its digest, nor use a precomputed email→hash table, because both attacks need the secret key. Pick a high-entropy key and keep it secret; anyone who learns it can re-run the enumeration.

鍵は安定しています。同じ鍵なら、すべてのレポートにわたって、また永続キャッシュのリプレイをまたいでも同じダイジェストが得られるため、レポート間の相関付けとキャッシュは引き続き機能します。異なる鍵は無関係なダイジェストを生成するため、履歴を共有する 2 つのチームは、鍵を共有しない限り作者を相互にひも付けることはできません。

Prefer the BCA_AUTHOR_HASH_KEY environment variable over the flag: a key on the command line is visible to other local users via the process list (ps) and is saved in shell history. The flag takes precedence when both are set:

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=…))でも利用できます。

このリポジトリでのドッグフーディング

This project runs bca vcs on its own source. make vcs prints the ranked table; path selection and the .bcaignore deny-set come from the repo-root bca.toml manifest, the same config make self-scan and make report use, and BCA_VCS_TOP overrides the row cap. The manifest’s [vcs] file_types key sets the default scope (the --file-types CLI flag replaces it when given). On every push to main, the Pages CI job includes the ranking in the published quality report (bca report html --vcs / report markdown --vcs), so reports/index.html shows the change-history risk section beside the AST hotspots. The same job publishes the full top-100 ranking as reports/vcs-report.json for tooling.

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>] produces an aggregated quality-metrics report across every file walked, for pasting into pull requests, wikis, or issue trackers. For per-file data meant for tooling, use bca metrics; for a pass/fail gate, use bca check.

Two formats are available: markdown (plain text, suited to PR comments) and html (a self-contained dashboard with sortable tables, suited to a build artifact). Pick one with --format / -O (bca report --format html); the default is markdown. The bare positional form (bca report markdown) still works as a deprecated alias and is slated for removal in the next major; prefer --format.

CI integration. For runnable GitHub Actions and GitLab CI recipes that post the Markdown report as a PR/MR comment, see the CI integration recipe.

移行の際は。 このコマンドは、再構成前の --metrics -O markdown 呼び出しを置き換えるものです。移行ガイドを参照してください。

クイックスタート

標準出力に表示:

bca report --paths /path/to/project markdown

ファイルに書き出し:

bca report --paths /path/to/project markdown --output report.md

注: --output はディレクトリではなく「ファイル」パスでなければなりません。

フラグ

フラグデフォルト説明
--top N20ホットスポットテーブルごとの最大エントリ数(0 = すべて)。
--strip-prefix PATH(空)ファイルパスから取り除くプレフィックス。
--no-suppress(オフ)ソース内の抑制マーカーで沈黙させられた関数を含めます(生の監査ビュー)。
--vcs(オフ)bca metrics --vcs に対応する、VCS リスク(デフォルトウィンドウ)でファイルをランク付けする「変更履歴リスク」セクションを追加します。このセクションは AST ホットスポットテーブルと同じメトリクスを持つファイル集合(metrics ファイル種別スコープ、#576)をランク付けするため、両者は同一のファイル母集団を記述します。git 作業ツリーの外では警告とともに無視されます。bca vcs を参照してください。
-o, --output FILE(標準出力)出力ファイル。親ディレクトリが存在している必要があります。

抑制マーカー

By default, bca report markdown|html honours in-source suppression markers: the same // bca: suppress, // bca: suppress-file, and Lizard #lizard forgives comments that bca check and the SARIF emitter respect (see Suppression). A function is omitted from a metric’s hotspot table when that metric is suppressed for it, so the published report agrees with the threshold gate instead of re-surfacing every silenced offender.

Suppression is per-metric: a // bca: suppress(cyclomatic) marker drops the function from the Cyclomatic table only; it still appears in the Cognitive, Halstead, and other tables. A bare // bca: suppress (or // bca: suppress-file) covers every metric.

マーカーに関係なくすべての違反を列挙する生の監査ビューには --no-suppress を渡します。この設定は bca.toml マニフェストに固定することもできます:

[report]
no_suppress = true

The CLI flag wins in either direction: a bare --no-suppress forces the audit view on, and --no-suppress=false restores the marker-honoring default even when the manifest enabled the audit view. The manifest only fills the value when the CLI left it unset.

使用例

セクションごとに最悪のホットスポット 5 件のみを表示:

bca report -p src/ markdown --top 5

表示されるパスからワークスペースルートを取り除く:

bca report -p /home/user/project markdown \
    --strip-prefix /home/user/project/

A typical day-to-day invocation:

bca report \
    --paths "$PWD" \
    markdown \
    --top 20 \
    --strip-prefix "$PWD/"

レポートの構成

生成されたレポートには以下のセクションが含まれます(データが存在しないセクションは省略されます)。すべてのホットスポットテーブルには SLOC と並んで Tokens 列(Lizard 方式の葉トークン数、コメントは除外)が含まれ、行ごとに補完的な 2 つのサイズ指標を確認できます。

  1. Project summary: files analyzed, languages, total SLOC / PLOC / comment counts, function and class counts, comment ratio.
  2. Per-language overview table: one row per language with file count, SLOC, function count, the SLOC-weighted average Maintainability Index (MI), average Cyclomatic Complexity (CC), and average Cognitive Complexity. The MI average is size-weighted and uses the unclamped Visual Studio value, so a language whose files are mostly unmaintainable reads negative instead of saturating at the 0 floor of the per-file MI column.
  3. Per-language hotspot sections (repeated for each language). Every hotspot title follows one template, <Concept> hotspots (top N by <column>), where the truncation clause states the actual --top state (top 20 by CC, or all, by CC for --top 0):
    • Summary: file count, SLOC, PLOC, comment ratio, and Average MI (SLOC-weighted) with a GOOD / MODERATE / LOW rating. The headline is the SLOC-weighted mean of the unclamped Visual Studio MI: large files dominate, and a file whose displayed MI clamps to 0 still contributes its true (often negative) maintainability rather than a misleading 0. A file with no code to measure (an #include-only header, an empty module) is left out of every MI average and of the MI hotspot table, because its MI of 0.0 is a placeholder, not a score; it still counts toward Files and SLOC. A language with no file to measure reads n/a.
    • Actionable Summary: counts of functions exceeding common thresholds (by default CC > 10, cognitive > 15, SLOC > 100, args > 3, Halstead bugs > 1; a manifest [thresholds] table overrides each cutoff). The Actionable Summary appears immediately after the Summary and before the hotspot tables. It counts raw measurements without applying suppression; the section is captioned to say so, naming how many suppressed functions are folded in. When suppression empties a hotspot table whose metric this summary still counts, the table is replaced by a one-line “table omitted: all N matching functions suppressed” note so a summary bullet never points at a missing table.
    • Maintainability Index hotspots (lowest N by MI): files sorted ascending by MI.
    • Cyclomatic complexity hotspots (top N by CC): functions sorted descending by CC, with summary statistics (average, max, counts above 10 and 20).
    • Cognitive complexity hotspots (top N by Cognitive): functions sorted descending by cognitive complexity.
    • Halstead effort hotspots (top N by Effort): functions sorted descending by Halstead effort, including volume and estimated bugs. Effort and Volume render as rounded integers with thousands separators (8,845); full precision lives in JSON/CSV.
    • Function size hotspots (top N by SLOC): functions sorted descending by source lines of code.
    • Many parameters hotspots (top N by Args): functions with more than three parameters, sorted descending.
    • Type hotspots (top N by WMC): types sorted descending by Weighted Methods per Class, with NOM, NPA, and NPM. “Type” covers all six kinds the report counts: class, struct, trait, impl, interface, namespace (the legend’s WMC entry lists them).
    • Exit points hotspots (top N by Exits): functions with more than two exit points, sorted descending. A single return is the baseline, not a hotspot, so the table admits only nexits > 2; when nothing clears the floor the section is omitted.
    • ABC magnitude hotspots (top N by ABC): functions sorted descending by ABC metric magnitude.

フォーマット間の一貫性

The Markdown and HTML renderers share one data model. They use the same summaries, hotspot membership, and captions, so both formats report identical numbers.

Both formats carry a Legend that defines every metric column abbreviation (CC, MI, ABC, WMC, and so on) plus the global-header stats (PLOC, Comments, Comment ratio). It is a ## Legend section in Markdown (its own outline entry, not nested under the last language) and an expanded (<details open>) block in HTML, so it survives print, mobile, and screen readers as well as a hover tooltip. Each entry links to its chapter in the Supported Metrics reference, so a one-line definition can hand the reader the full explanation. The definitions come from the same shared column specs the tooltips use, so the two formats cannot drift.

Both formats close with a provenance footer stating the bca version — followed, for a bca built from a source checkout, by the first twelve digits of its build-inputs digest in parentheses, the same digest bca --version prints in full — the generation date, the seed paths scanned, the per-table --top value, and whether suppression markers were honored, so a detached artifact (a PR comment, a Pages deployment, a file on a ticket) records what it was generated from. The date honors SOURCE_DATE_EPOCH for reproducible builds. The HTML report additionally carries a <meta name="viewport"> tag and wraps every table in a horizontal-scroll container so wide tables stay reachable on mobile and narrow windows, and its table of contents nests each language’s hotspot subsections under a collapsible entry.

Source suppression markers apply per metric to both report hotspot tables and bca check’s offender formats (code-climate, sarif, checkstyle, clang-warning, msvc-warning). Check baselines additionally filter check violations; they do not filter report hotspot tables. The Code Climate, SARIF, and Checkstyle documents are themselves three renderings of one offender set, so they agree by construction; the reports honour the same per-metric suppression decisions.

The single deliberate exception is the Actionable Summary, a whole-codebase health indicator that counts raw measurements regardless of suppression: silencing a function in one metric’s hotspot table does not erase it from that aggregate concern count. Every other figure, including each hotspot table’s caption, reflects the suppression-filtered set. To stop a reader mistaking the two populations for a double-count, each is captioned: the cyclomatic note adds “(excluding suppressed functions)”, and the Actionable Summary names the raw, suppression-ignoring basis of its counts.

HTML フォーマット

bca report html emits a single self-contained HTML page covering the same sections as the Markdown report. It is built to be served as a static artifact: inline CSS, inline vanilla JavaScript for click-to-sort on every hotspot table, and no external dependencies (no CDN, no fonts, no template engine). The page renders identically offline.

Write it to a file and open it in any browser:

bca report --paths /path/to/project \
    html --top 10 --output report.html

任意の列ヘッダーをクリックするとそのテーブルが昇順でソートされ、もう一度クリックすると降順に切り替わります。各テーブルは独立してソートされます。空のセル(メトリクスが測定されなかった箇所)は正の無限大であるかのようにソートされるため、「データなし」の行がホットスポットの見える上位に紛れ込むことはありません。

Hover (or keyboard-focus, where the browser supports it) any metric column header (SLOC, MI, CC, ABC, WMC, NPA, NPM, Exits, and the rest) for a one-sentence plain-English explanation of the metric. The tooltip is delivered through the native HTML title attribute, so it works offline with no JavaScript.

Because title tooltips are hover-only (invisible in print, on mobile, and to screen readers), the page also ends with a visible, collapsible Legend (<details>) listing every metric column’s one-line definition. Both the tooltips and the legend draw from the same column specs, so a definition cannot say one thing on hover and another in the legend.

Every interpolated string (function name, file path, language label) is HTML-escaped on the way out, so a crafted source path or symbol name cannot inject markup or break out of an attribute value.

Each per-language <section> carries a stable lang-<name> class (lang-rust, lang-python) styled with a low-alpha background tint and matching left border so a multi-language report’s section boundaries are obvious at a glance. Languages without an explicit palette entry fall back to a neutral lang-other tint, and a prefers-color-scheme: dark adapter raises the alpha so contrast holds in both themes.

メトリクス値のゼロ

Zero can be a valid measurement or a placeholder for unavailable data, depending on the metric. Each hotspot table applies its own eligibility rule; a measured MI of zero remains eligible. A table with no eligible entries is omitted.

チェック

bca check compares metrics against thresholds at each metric’s own scope (function, file, or container) and exits non-zero when any value exceeds its limit. It is the CI integration point: wire it into a build step and a regression in code complexity fails the pipeline before the change lands.

This page documents the command. The first half covers what most runs need: declaring thresholds, exit codes, offender output, baselines, and CI wiring. The second half covers the exemption mechanisms, per-language limits, the soft tier, and the edge cases in how they combine. For runnable pipeline examples see the CI integration recipe, which covers the --report-format matrix, GitHub Actions and .gitlab-ci.yml examples, the baseline ratchet, and the GitLab Code Quality path.

しきい値の宣言

Pass --threshold <metric>=<limit> once per metric (repeatable). Metric names match bca list-metrics; sub-metrics use a dotted form. A value violates its limit only when it exceeds it, so 0 is a valid limit that permits exactly zero.

bca check --paths src/ \
    --threshold cyclomatic=15 \
    --threshold cognitive=20 \
    --threshold loc.lloc=200

Or keep thresholds in the bca.toml manifest, so CI thresholds are versioned alongside the code. A manifest at the repo root is discovered automatically: a bare bca check reads it with no --config flag.

# 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 は出発点となるテーブルをスキャフォールドします。それらの数値の出どころ、ゲート対象の言語に合わせて上書きすべき項目、エージェントフィードバック・レガシートリアージ・セーフティクリティカルな作業向けに別のセットを選ぶ方法については、しきい値の選択を参照してください。

使用できるメトリクス名

トップレベルのスカラーメトリクスは list-metrics の名前をそのまま使います:cognitive、cyclomatic、nargs、nexits、nom、tokens、abc、wmc、npm、npa。複数のサブフィールドを持つメトリクススイートはドット区切り形式を使います。

メトリクス使用できるしきい値名
循環的複雑度cyclomatic、cyclomatic.modified
Halsteadhalstead.volume、halstead.difficulty、halstead.effort、halstead.time、halstead.bugs
コード行数loc.sloc, loc.ploc, loc.lloc, loc.cloc, loc.blank, loc.dloc
保守容易性指数mi.original、mi.sei、mi.visual_studio

不明なしきい値名はツールエラー(終了コード 1)となり、黙って無視されることはありません。

しきい値のスコープ

A threshold is checked only against the space kind its metric measures, so a metric’s whole-file or whole-impl aggregate is never mistaken for a per-function limit. Each metric has a fixed scope; there is nothing to configure.

スコープゲート対象のスペースメトリクス
ファイルファイル全体のルートのみloc.sloc, loc.ploc, loc.lloc, loc.cloc, loc.blank, loc.dloc
関数個々の関数、メソッド、クロージャcognitive、cyclomatic、cyclomatic.modified、halstead.*、mi.*、abc、nargs、nexits、tokens
コンテナクラス、構造体、トレイト、impl、名前空間、インターフェイスnom、wmc、npm、npa

The Function-scoped metrics include the subtree sums (nargs, nexits, tokens, halstead.*). These still roll a function’s own nested closures into its figure, but they are not summed across an entire file or impl. The Container-scoped metrics describe a type’s method set (methods per class, weighted methods, public members), so they gate the container rather than every leaf function. A file whose functions are individually fine therefore never trips an additive limit on the file-wide total, which is the false positive bca: suppress-file markers used to mask.

The bare bca diff --metric spelling of a loc sub-metric is accepted as an alias for its dotted form (sloc for loc.sloc, and so on for ploc/lloc/cloc/blank/dloc), so a name copied from a diff run gates correctly. A bare family head with no single threshold scalar (halstead, mi) is ambiguous and rejected with a “did you mean” hint listing the concrete sub-metrics; pick one, such as halstead.volume.

The two spellings name one metric, so they override each other wherever limits merge: a per-language [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.

終了コード

コード意味
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 where the rule matters most, because a gate reporting clean on a tree it could not read looks the same as a gate that passed. All three checks run before the gate is evaluated. --no-fail does not suppress them, since it suppresses threshold failures rather than 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 …] row tags a baseline run prints:

コード意味(tiered モード)
0すべての関数がしきい値内(または --no-fail 指定時)。
1ツールエラー。
2新規違反のみ(一致する --baseline エントリなし)。
3ベースライン退行のみ(ベースライン登録済みの違反が悪化)。
4新規違反と退行の両方。
5ハード制限も超えている --tier=soft 違反。

The tiered codes are opt-in; the default contract above stays 0/1/2. Every fail-state remains non-zero, so exit != 0 → fail wrappers keep working. Only tooling that tests $? -eq 2 explicitly needs to widen to 2-5. --no-fail still forces exit 0. Code 5 is emitted only at the soft tier; at the hard tier every violation is a hard breach by definition, so the 2/3/4 split applies instead.

--exit-codes <default|tiered> takes a value, and the CLI value overrides the [check] exit_codes manifest key in either direction. An invalid exit_codes value is a tool error (1). --print-effective-config reports the resolved exit_codes style. The deprecated --strict-exit-codes flag is a one-cycle alias for --exit-codes tiered (warns; removed at the next major).

違反の出力

違反した (function, metric) の各ペアは、次の安定したフォーマットで 1 行ずつ stdout に出力されます。

<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)

行はパス、開始行、メトリクス名の順でソートされるため、同じツリーに対する実行間で出力は決定的です。

どちらのストリームか

違反行はこのコマンドの成果物なので stdout に出力されます。bca check | wc -l、| head、| rg -c、2>/dev/null のいずれでも違反行に届きます。

実行が「自分自身について」語る内容はすべて stderr に出力されます:

  • ファイルごとの --- summary --- フッター、
  • --- next steps --- の修復ブロック、
  • GitHub Actions の ::error アノテーション、
  • bca: skipped N violations via [check.exclude] と bca: filtered N violations via baseline の件数表示、
  • すべての warning: および error: 診断メッセージ。

One combination inverts this. --report-format <dialect> without --output puts the aggregated SARIF / Checkstyle / Code Climate document on stdout, so the human rows move to stderr rather than corrupting it (the first example counts results with jq):

bca check --report-format sarif | jq '.runs[0].results | length'   # ドキュメントは stdout に
bca check --report-format sarif --output report.sarif | wc -l      # 違反行は stdout に戻る

--summary-file のダイジェストはストリームではなくファイルであり、どちらのストリームにも現れません。

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.

ベースライン

Adopting thresholds on an existing codebase usually presents a choice between “raise the limit until nothing fires” and “fix every offender before turning the gate on”. A baseline file is the ratchet-down alternative: record today’s offenders, fail only on regressions and new offenders, and shrink the file over time as the team pays down debt.

Baselines complement the suppression markers; they are not a substitute. A suppression says “this function is intentionally exempt forever” and lives in source; a baseline says “this is tech debt we are paying down” and lives in a committed TOML file. bca check applies in-source suppressions first, then [check.exclude] globs, then the baseline filter.

ベースラインの書き出し

bca check --paths src/ \
    --write-baseline .bca-baseline.toml

This walks the tree, captures every threshold violation that would otherwise fail the check, and writes them to the file as sorted TOML. The run exits 0 regardless of offender count, since the point is to capture them.

# bca ベースラインファイル。`bca check --write-baseline` により生成。
# 記載された違反はしきい値チェックから除外されます。記録値より
# 悪化した関数は引き続き失敗します。エントリが古くなったら
# `--write-baseline` で更新してください。
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 another entry shares its (path, qualified, metric) identity, the one case where matching consults a line number. Recording it elsewhere would 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.

Functions already covered by an in-source suppression marker are excluded. Pass --no-suppress together with --write-baseline to include violations hidden by source markers (the CI-auditor flow); [check.exclude] exemptions still apply.

--write-baseline cannot be combined with --baseline, --report-format, --output, --since, or --changed-only, because the baseline file is the output.

ベースラインの読み込み

bca check --paths src/ \
    --baseline .bca-baseline.toml

違反は、次の両方の条件が成り立つときに抑制されます。

  • An entry matches by (path, qualified_symbol, metric), independent of line number, or, failing that and with --baseline-fuzzy-match, by body hash. See the Baselines recipe for the full resolution order.
  • 現在の value が記録された値以下である。

A function that gets worse than its baseline value still fails. New offenders not listed in the baseline still fail. Improvements pass silently; the entry keeps its older, higher value until the next --write-baseline refresh.

存在しない、空である、version が欠落もしくは未対応である、またはパースに失敗するベースラインファイルはツールエラー(終了コード 1)であり、黙ってゼロマッチ扱いにはなりません。

Path keys are canonicalised relative to the baseline file’s own directory (the anchor), so --paths ., --paths src/, and --paths "$PWD" produce byte-identical baselines, and a --baseline run matches whichever --paths form generated the file. Switch between them freely without re-running --write-baseline.

制限事項

  • Ambiguous symbols and anonymous functions. Entries key on the qualified symbol, so inserting code above a named function does not re-key it. Two exceptions re-key as “new” on movement: functions sharing a qualified symbol that drift more than --baseline-line-tolerance apart, and anonymous closures or lambdas, whose synthetic symbol embeds the line. Refresh with --write-baseline.
  • OS 間の可搬性。 パスはスラッシュ区切りで保存されるため、ある OS で書かれたベースラインは別の OS 上の同じツリーにマッチします。有効な UTF-8 でないパスは損失のある表示形式(U+FFFD 置換)にフォールバックし、正確にラウンドトリップしない場合があります。

エンドツーエンドの導入フローと CI 統合パターンについては、ベースラインレシピを参照してください。

失敗させずに報告する

--no-fail reports offenders as usual but exits 0. Use it while adopting baselines without turning 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

CI の例(GitHub Actions)

- name: Check code complexity thresholds
  run: |
    bca check
  # Thresholds and paths come from the auto-discovered `bca.toml`
  # manifest at the repo root. The default non-zero exit fails the
  # step, which is the behaviour wanted here. No extra wiring.

To keep the job green and surface offenders as a build annotation while you reduce the count, swap in --no-fail:

- name: Surface complexity hot spots (non-blocking)
  run: |
    bca check --paths src/ --no-fail

対処しやすい失敗出力

When bca check fails, five flags shape the failure stream so a developer skimming a CI log can see what tripped, where in their PR it tripped, and what to do next. Each flag is independent, and all of them auto-detect from GitHub Actions environment variables when present, so the common CI case needs no explicit configuration.

フラグ効果自動検出する環境変数
--since <ref>ファイル別フッターを “Files in this range” と “Other offenders” に分割BCA_DIFF_BASE、GITHUB_BASE_REF、GITHUB_EVENT_BEFORE
--changed-onlydiff スコープ外の違反を完全に除外解決可能なベースが必要(--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 keep the same content when none of the above are active, so CI tooling that grep-anchors on the legacy text keeps working. See Which stream for where each now lands.

See the CI integration recipe for worked examples, including a GitHub Actions snippet that composes all five flags into one step, and the Baselines recipe for the --write-baseline refresh flow the remediation block links to.

diff ベース自動検出の優先順位

When --since is omitted, bca consults environment variables in this order:

  1. BCA_DIFF_BASE: an explicit override for local shells or CI runners other than GitHub Actions.
  2. GITHUB_BASE_REF: set by GitHub Actions on pull_request events. Expanded to origin/<value>; the runner is responsible for the corresponding git fetch (fetch-depth: 0 on actions/checkout).
  3. GITHUB_EVENT_BEFORE: set by GitHub Actions on push events to the SHA at HEAD before the push. The all-zeroes sentinel (force push, brand-new branch) is treated as no signal.

Failing to resolve a base is non-fatal unless --changed-only is passed, in which case the gate dies. Silently suppressing every violation under a misconfigured base is the failure mode this feature exists to prevent. --write-baseline also conflicts with --since / --changed-only: a partial baseline would silently mask every offender outside the diff scope on the next full-tree run.

違反レコードのエクスポート

bca check also emits a single CI/IDE document covering every offender in the walk. Pass --report-format <fmt> to pick the shape and --output <file> to write it to disk (stdout if omitted). The --format, -O, and --output-format spellings are accepted as deprecated aliases and will be removed in a future release. These flags do not change the exit-code contract: 0 clean, 2 on any violation (unless --no-fail), 1 on tool error.

When --output is given without --report-format, the format is inferred from the output extension: .sarif selects sarif and .xml selects checkstyle. An extension with no unique format (notably .json, which both sarif and code-climate produce) or no extension at all is a usage error (exit 1) naming --report-format, so an explicit --output is never silently ignored. An explicit --report-format always wins over the extension.

フォーマット対象
checkstyleJenkins、SonarQube、GitLab、“warnings plugin” 系 CI
sarifGitHub Code Scanning、モダンな IDE / セキュリティツール
code-climateGitLab MR の Code Quality ウィジェット
clang-warningエディタの quickfix パーサー、GitHub Actions の problem matcher
msvc-warningVisual Studio、VS Code、Windows CI ランナー

When no offenders exist the writer emits a well-formed but empty document, so CI consumers can ingest clean runs unchanged: an empty runs[].results array for SARIF, an empty JSON array ([]) for Code Climate, no <file> children under the <checkstyle> root for Checkstyle, and zero bytes for the two warning-line formats.

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

The Code Climate writer emits a single JSON array of issue objects matching GitLab’s strict subset of the upstream Code Climate engine spec: one entry per metric-threshold violation, no byte-order-mark, one trailing newline (empty input renders as []\n).

Each issue carries three fields of note. The check_name is namespaced as big-code-analysis/<metric>. The fingerprint is a stable SHA-256 over path \0 function \0 metric; it ignores line and value, so cosmetic edits still dedup in the MR widget. The severity maps GitLab’s five-level enum from the same severity key bca check ranks its own offenders by: ≤ 1.5 → minor, ≤ 2 → major, ≤ 4 → critical, > 4 → blocker.

For a count the key is value / limit, and a breach of a zero limit is always blocker. For the mi.* family, where lower is worse and the index has no true zero, it is 1 + (limit − value) / unit: one step per maintainability band (17.1 points of mi.original / mi.sei, 10 of mi.visual_studio) below the floor. An mi.original of 0 against a floor of 20 is critical; one of −40 is blocker. The full enum is info/minor/major/critical/blocker; bca never emits info, because a threshold violation always lands at minor or higher.

アーティファクトを 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]

This is the format clang -fdiagnostics-format= produces and the shape editor quickfix parsers (VS Code, IntelliJ, Vim) and most CI annotators understand without configuration.

GitHub Actions surfaces the lines as inline annotations on the PR diff through the built-in GCC problem matcher (or any community compiler-problem-matchers action):

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

The space before the colon after warning/error is the MSVC convention. On Windows the path is normalized to \ separators, matching cl.exe output; on other platforms the path is emitted as-is. Visual Studio, VS Code with the C/C++ extension, and Windows CI runners (Azure Pipelines, GitHub Actions on windows-latest) parse these inline without extra configuration.

抑制マーカーによる違反の抑制

ソース内コメントによって、問題のコードを編集したりウォークから除外したりせずに、個々の関数またはファイル全体のしきい値違反を抑制できます。ネイティブの記法は bca: suppress / bca: suppress-file で、Lizard の #lizard forgives は互換シムとして認識されます。完全なリファレンスと --no-suppress CI 監査フラグについては抑制マーカーを参照してください。

ファイルカテゴリ全体の除外([check.exclude])

Some files should be analysed and reported but never gated: test fixtures that intentionally trip cognitive or cyclomatic, generated bindings, macro-dispatch modules whose complexity is structural and will never be “fixed”. Putting these in .bcaignore is too blunt: it removes them from the walk entirely, so bca report loses them too. Baselining them is also wrong, because they are not debt being paid down, and they churn the baseline diff forever.

[check.exclude] is the glob-level middle ground. Matching files are walked, parsed, measured, and shown by bca report, but bca check drops their violations before emitting offenders and before --write-baseline records anything, so the structural exemptions stay out of .bca-baseline.toml.

bca.toml の場合:

[check]
exclude = [
    "tests/**",
    "big-code-analysis-ast/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 reads a .gitignore-style file (blank lines and # comments skipped); the conventional name is .bcacheckignore, mirroring .bcaignore for the walker. Globs match the path exactly as the walker matched it for --exclude. As a negative filter key, an explicit --check-exclude list unions with the manifest [check] exclude list rather than replacing it. A CLI exemption is added to the project’s, so you cannot accidentally re-gate a path the manifest deliberately exempted. Duplicates collapse; CLI patterns sort first. Pass --no-config to drop the manifest’s exemptions entirely. Positive scope keys like paths / include still replace on the CLI; only the exclude filters merge.

相対グロブは何に対して相対か

The source of a glob determines its root:

ソース相対グロブが解決される基準
bca.toml [check] exclude / exclude_frombca.toml を置いているディレクトリ
--check-exclude / --check-exclude-frombca を実行したディレクトリ

So exclude = ["./vendor/**"] in a manifest at the repo root means <repo>/vendor/, whichever directory you invoke bca check from, including from inside vendor/ itself. The same glob passed as --check-exclude './vendor/**' means vendor/ under your shell’s current directory, because that is where you typed it.

This matters most for per-file callers (editor integrations, pre-commit hooks, the agent hooks in Agent feedback), which run bca check <one file> from whatever directory they happen to be in. Their exemptions belong in the manifest, where the anchor is the project’s and not the caller’s.

The walker’s exclude / --exclude pair is split by origin the same way: a --exclude glob is matched against each file’s path relative to the seed you named, and a manifest exclude glob is resolved against the manifest’s own directory, whatever seed you name and wherever you run from. So bca metrics and bca metrics -p sub honour the same manifest exclusions (#1189), and the warning that a named file overrode an exclude resolves each glob at its own root (#1164).

他の抑制メカニズムとの優先順位

bca check は、最も特異的なものから順に、次の順序で除外を解決します。

  1. In-source markers (bca: suppress / bca: suppress-file) always win; they apply during the walk, so the function never becomes a violation.
  2. [check.exclude] globs exempt categories of files (tests, generated code).
  3. .bca-baseline.toml lists known offenders being paid down.

--print-effective-config は、他のゲート入力と併せて解決後の check_exclude グロブを報告します。

check_exclude lists the combined CLI and manifest globs. manifest_check_exclude identifies the manifest subset; resolve those globs relative to the reported manifest directory. The walker uses exclude and manifest_exclude in the same way. The manifest-specific keys are absent when the manifest contributes no globs.

manifest_exclude_from and manifest_check_exclude_from follow the same idea for the exclude_from files, with one difference: a command-line --exclude-from replaces the manifest’s file rather than adding to it, so the manifest key is present only when the manifest’s file is the one in effect. The inline glob lists union; the files do not.

言語別の制限([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:

  • マニフェスト内の未知のスラッグは、did-you-mean ヒント付きのツールエラー(終了コード 1)です。タイポした [thresholds.lang.rust-lang] が、作者は緩めたつもりでいるのにゲートを黙ってプロジェクトの制限のままにしておく、という事態があってはなりません。
  • 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 sees it, with a warning if you named it explicitly.

コマンドラインの --threshold はグローバルのままで、これまでどおり最後に絶対的に適用されます。プロジェクトのテーブル「と」すべての言語別テーブルの両方を上書きするため、あなたが入力した制限が実際に動く制限になります。

--print-effective-config prints one fully resolved table per overridden language, inherited limits included rather than a diff, so the number that will fire is the number you read:

[thresholds]
cognitive = 15.0
cyclomatic = 15.0

[thresholds.lang.c]
cognitive = 30.0
cyclomatic = 15.0

Per-path limits ([[thresholds.path]])

A [[thresholds.path]] entry re-limits the files its glob matches for the metrics it names, and inherits everything else. It is for a population of files whose shape the project limit misreads — a directory of dispatch tables, a generated layer — and it keeps those files gated, at a limit chosen for them, where a suppression marker would remove them:

[[thresholds.path]]
glob = "src/metrics/{abc,cognitive,cyclomatic,loc}/*.rs"
cyclomatic = 40
"halstead.effort" = 150000

The glob is matched relative to the directory of the config that declared it — the bca.toml, or the --config file — with the engine and anchor [check] exclude uses. Where several entries match one file the last wins, per metric. An unknown metric, a missing or empty glob, and tokens are each a tool error naming the entry. The soft tier derives from the overridden limit, and --threshold still outranks it; the full order is stated once in How the layers compose, and when to reach for an entry rather than a marker is in Per-path overrides.

--print-effective-config prints each entry after the per-language tables, with the metrics it declares at their hard-tier limit — --threshold applied last, the tier not applied — so the printed file replays the gate at the hard tier through --config. That is the whole guarantee. The resolved view carries no [thresholds.soft] table and does not record which limits came from --threshold, so a soft-tier run is not replayed by the printout alone: loading it with the same --tier re-derives a band from the printed hard limits, which matches the original only when the original’s soft configuration is still in scope (the manifest is still discovered, not --no-config) and it used no --threshold (a CLI limit is absolute and unscaled on the original run; reloaded, it is a hard limit the band scales). The reason the printed path limits are hard, not banded, is in Per-path overrides:

[thresholds]
cyclomatic = 15.0

[[thresholds.path]]
glob = "src/metrics/{abc,cognitive,cyclomatic,loc}/*.rs"
cyclomatic = 40.0
"halstead.effort" = 150000.0

二層しきい値(--tier)

--tier <hard|soft|soft=RATIO> は、ゲートが比較に使うしきい値の層を選択します。hard(デフォルト)は [thresholds] テーブルをそのまま使います。soft はハードゲートの「前に」発火する早期警告の層で、すべての制限を RATIO 倍に引き締めます。--tier 単独指定は soft を意味し、soft だけならデフォルト比率 0.95 を使い、soft=0.90 は比率を 0.90 に固定し、soft=1.0 は一括スケールを無効にします。

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.

The lower-is-worse mi.* family is different. Its limit is a floor, a value below it is the violation, and the index has no true zero (mi.original and mi.sei go negative on large functions). So the floor is raised by 1 − RATIO of one maintainability band rather than scaled. One band is 10 points of mi.visual_studio, whose 20/10 cut-offs Visual Studio and radon use, and the same 17.1 points on the 171-point mi.original / mi.sei scale. "mi.original" = 20 with soft=0.9 warns at 20 + 0.1 × 17.1 = 21.71, and a floor of -50 at -48.29. Multiplying a floor would move the warning to 18, under the hard gate, and dividing a negative one would do the same (-50 / 0.9 = -55.6). Either way nothing could reach the soft band first.

Limits must be finite. A count limit must also be non-negative; an mi.original or mi.sei floor may be negative. A limit that cannot fire, or fires on every value, still parses but prints a warning: mi.visual_studio is clamped to [0, 100], so = 0 reads warning: limit mi.visual_studio=0 cannot fire: 0 is the metric's floor; gate mi.original instead, and = 101 warns that it fires on every value.

[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

The soft tier is one step of the layering order stated in How the layers compose. Within that step: if a [thresholds.soft] table exists, its overrides merge onto the resolved hard limits and metrics absent from it inherit their hard limit — the blanket RATIO does not apply, since explicit per-metric limits win; otherwise every limit is tightened by the soft RATIO (default 0.95 for a bare soft; soft=1.0 disables scaling). Repeated --threshold name=value flags still apply after it, absolutely.

The step runs once per table — the project’s, each language’s, and each [[thresholds.path]] combination’s — against that table’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.

The soft RATIO (and the scale factor in a "<ratio>x" string) must be in (0, 1]. The [check] headroom manifest key supplies the ratio for a bare --tier=soft. The deprecated --headroom <R> flag is a one-cycle alias for --tier=soft=<R> (warns; removed at the next major); it now promotes a hard run to the soft tier. Both tiers ratchet through the same --baseline, and --print-effective-config reports the resolved tier alongside the post-merge limits. See the Local threshold gates recipe for the migration tip and rationale.

候補制限のプレビュー(--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. Making that visible is the reason the flag exists.

--threshold nargs=6 はこれを教えてくれません。その制限は最後に、絶対的に、決してスケールされずに適用されるため、この方法で試した候補にはソフト層がまったくありません。同じメトリクスに両方のフラグを渡すと、黙って解決されるのではなく拒否されます。

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, since a candidate global limit does not reach it, and the report says so on its own line. A ratio-derived soft limit names its arithmetic: 5.7, 0.95x for a count, and for an mi.* floor the additive step, as in mi.original=20 → 20.855, +0.855 (0.05 × 17.1).

それ以外はすべて、予測対象の実行と一致します。exclude_tests、[check] exclude、ソース内の抑制マーカー、--changed-only、ベースラインはいずれも通常どおり適用されます。唯一の違いは、ベースラインでカバーされた違反者が、落とされるのではなくカウントされることです。これによって already baselined / new の分割が可能になっています。

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, --output, and an explicit --summary-file <path> (the auto / never keywords are still accepted), each of which would produce a second, different artifact.

このフラグが可視化するために存在する規則については、しきい値の選択を参照してください。

信頼できない入力に対するモード(--strict)

ゲートが検査するファイル集合を狭める既定の動作が 2 つあり、どちらもテスト対象のツリーから入力を読み取ります。生成コード検出器はヘッダにマーカー語句を含むファイルをスキップし、ウォーカーは走査対象ツリー内の .gitignore ファイルを解釈します。どちらも信頼できるチェックアウトの分析には適切な既定値です。しかしプルリクエストのゲートでは、レビュー対象の変更が引けるレバーになります。コメント 1 行の追加、または .gitignore への 1 エントリで、ファイルがゲートから外れます。CI レシピに両方の再現手順があります。

--strict は両者を 1 つのフラグで無効にします。--no-skip-generated --no-ignore を渡すのと等価です。

bca check --strict --paths src/

プロジェクトはワークフローごとではなく、bca.toml で一度だけオプトインします。

[check]
strict = true

このキーはトップレベルの exclude_tests と同様に presence-only です。プロファイルを有効にすることはできますが、無効に戻すことはできません(--no-strict はありません)。そのため CI ワークフローがコミット済みのポリシーを黙って弱めることはできません。

プロファイルが無効でも、bca check は検査しなかったものを報告します。いずれかの仕組みがファイルを除外した場合は、1 行のサマリーを stderr に出力して件数を示します。

bca: 2 files not checked (1 generated, 1 ignored) — pass --report-skipped to list them

クリーンな実行では何も出力されず、サマリーが終了コードを変えることもありません。--report-skipped は除外された各エントリを note: skipped (generated): <path>、note: skipped (ignored): <path>、または note: skipped (ignored directory): <path> の行で一覧表示します。--strict の下では何もスキップされないため、要約するものもありません。

The counts come from the walk’s prune points: for every directory the walker entered, the gate checks which immediate children the ignore rules dropped. Only files a parser owns are counted, on both sides. An ignored log file, or a generated Cargo.lock (whose header matches @generated), would have been read and dropped rather than checked, and counting either would make the summary fire on every run. A file named explicitly on the command line bypasses ignore rules and is analyzed, so it is never counted.

Ignored directories are reported only under --report-skipped, as N ignored directories not walked entries, and are never entered. Nearly every checkout has an ignored build tree on disk, and enumerating a target/-sized tree would cost a full second traversal for a million-file count with no signal in it. The ignored tally reflects every source the walker honors (in-tree .gitignore and .ignore files, the global gitignore, and ancestor ignore files), so local and CI counts can differ.

With content sniffing off, generated trees still need an exemption. Give them one the reviewers own, either a committed walker deny-set (--exclude-from .bcaignore) or a [check.exclude] glob, rather than a marker the submitter controls.

抑制マーカー

In-source suppression markers silence threshold violations without editing the offending function or excluding the file from the walk. Drop a marker in any comment in the source file and bca check treats the covered metrics as if they were within limits for that scope. Metric computation is unaffected: raw bca metrics output still reports every number. Suppression is a display concern. bca check drops the covered violations from the gate, and bca report markdown|html omits the covered functions from the matching hotspot tables by default (pass bca report --no-suppress for the raw audit view; see report).

Markers exist for the cases where editing the code is not an option: legacy modules awaiting rewrite, accepted exceptions documented in the comment, and migration from Lizard’s #lizard forgives convention. This page is the marker reference: the syntax, the metric names it accepts, where a marker may sit, and the flags that audit or bypass markers.

ネイティブマーカー(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) — 引き回されるコンテキストであって、神関数ではない
}

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.

A function-scope marker attaches to the innermost FuncSpace (see the FuncSpace rustdoc) whose source range contains the comment. A function-scope marker outside every function body is silently ignored; for file-wide silencing use the explicit suppress-file verb. A file-scope marker may appear anywhere in the source; there is no “must be in first N lines” rule.

bca: suppress: function-scoped, all metrics (Rust)

#![allow(unused)]
fn main() {
// bca: suppress
fn legacy_dispatch(opcode: u8) -> Action {
    // サポートされる全オペコードに対する高密度な match
    match opcode { /* ... */ }
}
}

bca: suppress(metric, ...): function-scoped, listed metrics (Python)

def parse_token_stream(tokens):
    # bca: suppress(cognitive)
    # 認知的複雑度はこのステートマシンに本質的なもの。
    # 循環的複雑度は依然として制限内に収まっている。
    ...

その他のしきい値(cyclomatic、halstead、loc など)は引き続き適用されます。

bca: suppress-file: file-scoped, all metrics (JavaScript)

// bca: suppress-file
// 手動チューニングされたホットパス。しきい値を満たすための書き換えはしないこと。
function transform(input) { /* ... */ }
function validate(input) { /* ... */ }

bca: suppress-file(metric, ...): file-scoped, listed metrics (C++)

/* bca: suppress-file(halstead) */
// Halstead の volume は下の生成テーブルによって膨らんでいる。
// それ以外のメトリクスはファイル全体で引き続き適用される。

Prefer a narrower tool first. Thresholds apply at each metric’s defined scope. A file-wide or impl-wide aggregate does not trigger a per-function limit, so muting a file-level halstead / nargs / nexits / nom total is no longer a reason to reach for suppress-file (#969). Use suppress-file when you intend to silence the metric throughout the file. To excuse one irreducibly complex function, use a function-scoped bca: suppress(...) inside it. To grandfather existing offenders without blinding the gate to future regressions, prefer a baseline entry, which keeps firing once a function gets worse than its recorded value.

Lizard 互換マーカー

既存の Lizard 対応コードベースを書き換えずに済むよう、2 つの Lizard 形式マーカーがそのまま認識されます:

Lizard マーカースコープ対応するネイティブマーカー
#lizard forgives囲んでいる関数bca: suppress
#lizard forgive globalファイルbca: suppress-file

The compatibility layer is intentionally narrow: only these two shapes are accepted. Other Lizard directives parse as ordinary comments. Lizard offers no per-metric scoping, so the native form’s bca: suppress(metric, ...) list has no Lizard analogue; every Lizard-style marker silences every metric.

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 つあります:

  • nexits is the canonical spelling: bca: suppress(nexits) silences a nexits threshold violation. The legacy exit alias was retired in #555 and is no longer accepted. Spelling it exit is 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 shrink what a marker covers, so a typo never widens scope to a metric the author did not name. Rejecting the whole marker, which is what earlier releases did, would also discard valid metric names, leaving intended suppressions inactive.

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, since 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.

根拠テキスト

メトリクスリストの後のテキストは書き手のものです。bca はそこを読み飛ばし、解釈しません。2 つの形式の間の非対称性は意図的なものです:

  • 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 #123 reaches for. Accepting them silences every metric in the enclosing function on the strength of a comment, so the whole shape warns instead.

    To record a reason, name the metrics (bca: suppress(cognitive, cyclomatic) — reason), which is more precise than the All hammer anyway. If All really 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

Function-scope markers attach to the innermost Function-kind FuncSpace whose (start_line..=end_line) range contains the comment’s line. Markers buried in a class or struct body but outside every method are silently ignored; for class-wide silencing use bca: suppress-file or repeat the marker on each method.

ファイルスコープのマーカーはトップレベルの Unit スペースにマージされ、ネストに関係なくファイル内のすべての関数に適用されます。

マーカーはコメントの先頭近くに配置してください。スキャナーは両端の区切り文字を取り除いた後、先頭に bca:(または #lizard)が来ることを期待します。複数行ブロックコメントの奥深くに埋もれたマーカーは認識されません。

--no-suppress(CI での監査)

bca check --no-suppress ignores every suppression marker, native and Lizard alike, and reports every threshold violation in the walk. Use it in audit pipelines that need the raw, un-silenced offender list:

bca check --paths src/ --no-suppress

このフラグはメトリクスの値自体には影響しません: 生の bca metrics 出力は常にすべての数値を報告します。bca report markdown|html はデフォルトでホットスポットテーブルにおいてマーカーを尊重し、同じ生の監査ビューのために独自の --no-suppress フラグを受け付けます。

抑制された負債の可視化(--report-suppressed)

Suppression keeps an offender out of the gate, which also keeps it out of the --report-format document, so a suppressed module disappears from the code-scan report entirely. bca check --report-suppressed puts it back, as suppressed rather than active:

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 renders a result that carries a suppressions entry as a suppressed (closed) alert, so the debt stays visible on the Security tab without failing the gate. If you only want active offenders to appear, omit --report-suppressed from the upload (this repo’s own Pages workflow does exactly that).

注記:

  • Use SARIF to export suppressed offenders; other report formats emit only active offenders.
  • Match the tier to your baseline’s provenance (--tier=soft=0.95 for a soft-0.95 baseline) so offenders below the hard limit still appear.
  • Do not combine --report-suppressed with --no-suppress or --write-baseline.

適用除外の監査(bca exemptions)

--no-suppress shows you the offenders a marker silences, but not the markers themselves; to find every silencer you previously had to diff a --no-suppress run against a normal one. bca exemptions replaces that workaround with a direct listing of everything the bca check gate skips, across all three exemption tiers, in one report:

ティア粒度ソース
ソース内マーカー関数単位 / ファイル単位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

The surrounding function (for function-scoped markers) gives scope context; file-scoped markers read (whole file), and a function-scoped marker written outside any function, which silences nothing, reads (no enclosing fn) so dead markers are visible.

フォーマットとセクションフィルタ

--format markdown は PR コメント向けのテーブルを出力します。--format json は、ダッシュボードや jq によるフィルタリングのために、3 つのティアすべてを単一の suppressions エンベロープの下にネストします:

bca exemptions --paths src/ --format json | jq '.suppressions.markers[] | select(.dialect == "lizard")'

JSON 形式では、省略されたセクション(--*-only フラグで要求されなかったもの)は null になり、要求されたが空だったセクションは [] になるため、フィルタは両者を区別できます。

The mutually exclusive --markers-only / --excludes-only / --baseline-only flags narrow the report to a single tier for a specialised PR bot, such as one that only comments on newly added in-source markers. The baseline (bca.toml top-level baseline) and [check.exclude] ([check] exclude) inputs default to the same sources bca check reads, so the audit reflects exactly what the gate would skip; override the baseline with --baseline <path>.

以前の --only-markers / --only-excludes / --only-baseline の綴りは、既存の PR ボットの呼び出しを動かし続けるため、1 リリースサイクルの間は非表示のエイリアスとして残ります。diff-baseline のセクションフィルタと一致する --<section>-only 形式を優先してください。

Unlike bca check, bca exemptions is informational and always exits 0 on success. It is a review surface, not a gate.

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 からの移行

互換レイヤーがあるため、移行は段階的に行えます:

  1. Keep existing #lizard forgives and #lizard forgive global markers during migration; bca check recognizes them.
  2. Replace markers with bca: suppress(...) when you need per-metric scoping; the Lizard form silences every metric.

本プロジェクトは 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.

ノード

The find, count, dump, and functions subcommands inspect the Abstract Syntax Tree (AST) that tree-sitter builds for a source file, rather than computing metrics from it. A node is one syntactic construct in that tree, and a node type is the grammar’s name for it (function_item, if_statement). Use these commands to locate constructs, count them, spot parse errors, or see how a grammar shapes a file.

移行しますか? 以下の動詞は、再構成前のフラグによるアクション(-d、-f、--count など)を置き換えるものです。移行ガイド を参照してください。

エラー検出

コード内の構文エラーを検出するには、次を実行します:

bca find -t ERROR -I "*.ext" /path/to/your/file/or/directory
  • [PATHS]... / -p, --paths:分析対象のファイルまたはディレクトリ(ディレクトリを指定するとすべてのファイルを分析します)。パスは位置引数または --paths で指定でき、両者は和集合になります。フラグはサブコマンドの後に置きます。
  • -t, --type: the node type to match. Repeat the flag for several types (-t function_item -t struct_item); at least one is required. A string value matches the node-type name exactly (for example function_item). A purely numeric value is instead interpreted as a raw tree-sitter kind_id and matches nodes whose internal symbol id equals that number; for example, -t 65535 matches the ERROR sentinel. Prefer -t ERROR for error detection. The numeric form is an escape hatch for grammar inspection and is unstable: a kind_id is an index into the grammar’s symbol table, so the same number names a different node after a grammar-version bump. Prefer the string form unless you specifically need a kind that has no stable name.
  • -I, --include: glob filter for selecting files by extension (*.js, *.rs). Each -I takes exactly one value, so a following positional path is never swallowed.

Semantic filters

Six -t/--type values are not node-type names but semantic filters, resolved per language so that one spelling works across every grammar. They take precedence over an identically named node type, so a grammar with a literal call node cannot be matched by name; use its numeric kind_id for that.

  • all: every node, named and anonymous. bca count -t all therefore always reports 100%.
  • function: a named function, method or other callable declaration. Anonymous ones are excluded where the language distinguishes them: in the JavaScript family an arrow function or function expression bound to a name (const add = (a, b) => …) is a function match, while the same expression passed inline as a callback (xs.map(x => x * 2)) is a closure and is not.
  • call: a call site, meaning a function, method or command invocation a reader would navigate to. Object construction (new T(…)) and constructor delegation (C#’s : base(x) and a C# 12 primary constructor’s : Base(x), Java’s super(…), Kotlin’s superclass call) are deliberately excluded. They are counted by the ABC branches axis, whose Fitzpatrick rule is “function invocation or object creation”, a wider question than “where is this called”. Expect abc.branches to exceed a file’s call count in C#, Java, Groovy, Kotlin, C++, JavaScript, TypeScript, PHP, Ruby and Rust; see Where a constructor call lands. Each call matches once. In Perl, where the grammar parses a call’s name as a call of its own, foo(1) matches as the whole call only, and an auto-quoted hash key ((name => 1), $h{-name}), which it parses as a call too, does not match at all.
  • comment: a comment of any of the language’s forms.
  • string: a string literal. Type-annotation keywords that share the literal’s node name (TypeScript’s and PHP’s : string) are excluded.
  • error: a node that is or contains a parse error, so a match covers every ancestor of an ERROR node up to the root as well as the node itself. -t ERROR is the narrower spelling used above: it matches the error nodes alone, by name.

ノードのカウント

count コマンドで、1 つ以上のノード種別の出現回数をカウントします:

bca count -t <NODE_TYPE> [-t <NODE_TYPE>...] -I "*.ext" \
    /path/to/your/file/or/directory

AST の表示

To visualize the AST of a source file, use the dump command. It requires an explicit path, since a whole-tree AST dump is never useful:

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 serves the big-code-analysis library over HTTP. This page is the reference for a client author: every route, its request and response shape, the error envelope, and the status codes. For running the daemon (flags, environment variables, resource limits, and the trust boundaries to respect before exposing it) see Operating bca-web; for end-to-end curl examples see Driving the REST API.

The source-in-body endpoints remove comments, list function spans, return the tree-sitter syntax tree, and compute metrics. Three further endpoints compute change-history (VCS) metrics over a repository on the server’s filesystem.

Running the server

bca-web --host 127.0.0.1 --port 9090
  • --host: the address to bind (default 127.0.0.1).
  • --port: the TCP port (default 8080).
  • -j: the number of worker threads (optional).
  • --cors: enable CORS for browser-based tooling (off by default).

CORS

By default bca-web emits no CORS headers: a browser script served from a different origin cannot read the API’s responses. This keeps a local bca-web (the default 127.0.0.1 bind) from exposing its repository paths and metrics to any website the operator happens to be visiting.

オプトインするには --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 形式を使用してください。

エラーレスポンス

Errors are reported with an HTTP status code, not inside a 200 body. Every error (on the JSON endpoints, the raw/octet-stream endpoints, and the 415/405/404 fallbacks alike) returns one uniform machine-readable JSON body, so clients parse a single error shape regardless of the success content-type:

{
  "error": "human-readable message",
  "error_kind": "stable_machine_token",
  "id": "echoed-request-id"
}

error is the specific human-readable cause; error_kind is a stable snake_case machine token (unknown_field, unsupported_language, bad_request, parse_timeout, and so on) so clients branch on the cause without string-matching the prose (issue #631). The token vocabulary is closed and governed by STABILITY.md.

The id key is always present. It carries the client-supplied correlation id when the request had one (the JSON endpoints), and an empty string otherwise: the octet-stream / query endpoints carry no id, and the content-type / method / not-found fallbacks, like any request whose body failed to parse before the id was read, have no parsed id to echo.

ステータスコード:

  • 400 Bad Request: a malformed body or query parameter: invalid JSON, a missing required field, an unrecognised key (the strict deny_unknown_fields parse, including the removed unit flag; see Compute metrics below), or a scope value that is not full / file.
  • 422 Unprocessable Entity: the file_name extension (and content sniffing) maps to no supported language. The route matched and the body parsed; only the submitted entity cannot be processed. The response carries the stable machine token "error": "unsupported_language"; query GET /v1/languages for the supported set. (Before 2.0 this was a 404, indistinguishable from an unknown URL; see issue #634.)
  • 404 Not Found: the URL matches no endpoint.
  • 415 Unsupported Media Type: a known POST endpoint received a Content-Type that is neither application/json nor application/octet-stream (a charset parameter is allowed).
  • 405 Method Not Allowed: a known endpoint was called with the wrong HTTP method. The analysis endpoints accept POST; /v1/ping, /v1/version, /v1/languages, and /v1 accept GET and HEAD.
  • 406 Not Acceptable: the request Accept header named only media types the server cannot produce. The structured analysis endpoints serve application/json, application/yaml, and application/cbor; any other concrete type (application/xml, say) is a 406 carrying the not_acceptable token. See Content negotiation.
  • 413 Payload Too Large: the request body exceeded the server limit.
  • 500 Internal Server Error: metric computation or AST construction failed for an otherwise-valid request, or a /vcs history walk failed on the server side. This also covers a response too deeply nested to serialize (serialize_failed); see Nesting limits below.
  • 503 Service Unavailable: the parse pool is saturated by orphaned (timed-out) tasks; retry later.
  • 504 Gateway Timeout: the parse (or history walk) exceeded the server’s configured deadline.

ネストの上限

Responses are trees, and the server’s serializers recurse through nested response trees, one native stack frame per level. A deeply nested response could overflow the stack, which aborts the whole process and takes every in-flight request with it. So the server refuses to serialize past a fixed depth and answers 500 with the serialize_failed token:

レスポンス上限1 レベルとみなされるもの
/metrics128ネストした関数スペース 1 つ(関数の中の関数の中の…)
/ast512AST ノード 1 つ

Both limits sit far above anything real source reaches. Across the integration corpus under tests/repositories (TensorFlow, DeepSpeech, serde, and others) the deepest AST measured 188 nodes and the deepest function-space nesting 10 when the limits were set (#1056). The /metrics limit is also more generous than the read side: a JSON document of nested spaces cannot be parsed back past ~61 levels, because serde_json’s own 128-level recursion limit charges two levels per space.

コンテンツネゴシエーション

The structured analysis endpoints (/v1/ast, /v1/comment in its JSON variant, /v1/function, /v1/metrics, /v1/vcs, /v1/vcs/trend, and /v1/vcs/jit) choose their response serialization from the request Accept header, mirroring the CLI’s -O json|yaml|cbor outputs. The same value serializes byte-for-byte identically whether it comes from the CLI or the server.

Accept の値レスポンスの Content-Type
なし、*/*、application/*、application/jsonapplication/json
application/yaml(または text/yaml、application/x-yaml)application/yaml
application/cborapplication/cbor
その他の任意の具体的なタイプ406 Not Acceptable

ルール:

  • JSON is the default. Without an Accept header, the server returns JSON: the same body and Content-Type earlier releases always returned, so existing clients need no change. JSON entries and wildcards (*/*, application/*) make JSON a candidate.
  • q weights are honored. The supported candidate with the highest q weight wins (Accept: application/json;q=0.5, application/yaml;q=0.9 returns YAML), with ties resolved by list order; q=0 refuses a type.
  • サポートされないタイプは 406 になり、 暗黙の JSON フォールバックは行われません。ボディは error_kind: "not_acceptable" を持つ統一の {error, error_kind, id} エンベロープで、メッセージにはサポートされるメディアタイプが列挙されます。
  • Only structured serializations are offered. TOML and CSV are excluded: TOML is awkward for the deeply nested space tree and CSV is flat and tabular. The error-envelope body is always JSON and the /v1/comment octet-stream variant (raw byte-in / byte-out) is always raw bytes; neither negotiates. The introspection routes (/v1, /v1/version, /v1/languages) return JSON metadata only.
# 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 the server

Checks that the server is running.

リクエスト:

GET http://127.0.0.1:8080/v1/ping

レスポンス:

  • Status code: 200 OK
  • ボディ:空。

Use curl -sf http://127.0.0.1:8080/v1/ping && echo ok to script a liveness check; -f makes curl exit non-zero on any HTTP error.

2. Remove comments

Removes comments from the submitted source. The endpoint accepts two Content-Type variants: application/octet-stream for raw byte-in / byte-out, and application/json for a JSON envelope.

リクエスト:

POST http://127.0.0.1:8080/v1/comment

ペイロード:

{
  "id": "unique-id",
  "file_name": "filename.ext",
  "code": "source code with comments"
}
  • id: a correlation identifier for the request. Optional (issue #645); omitting it defaults to an empty string (treated as “no correlation id”).
  • file_name: the name of the file being analyzed.
  • code: the source code with comments.

レスポンス(JSON バリアント):

{
  "id": "unique-id",
  "language": "cpp",
  "code": "print"
}

The response envelope reports id, the detected language (the canonical lowercase slug; see Compute metrics below), and the code result key. The code field is a string holding the stripped source: the request code arrived as a JSON string, so the stripped output is guaranteed valid UTF-8 and is handed back as a string, matching the request and every other JSON endpoint. The application/octet-stream variant returns the stripped source as the raw response body (no envelope), which suits binary-faithful round-trips and shell pipelines; its errors still use the uniform JSON error body above.

ソースに除去可能なコメントが含まれない場合、どちらのバリアントも 200 ステータスと空のペイロードで空の結果を通知します。JSON バリアントは "code": ""(空文字列)を返し、octet-stream バリアントは空のボディを返します。したがって、要求された Content-Type にかかわらずステータスコードとエンベロープの形は同一です。octet-stream バリアントは 204 No Content ではなく空の 200 ボディを返します。

3. Retrieve function spans

Returns the line span of every function in the submitted source.

リクエスト:

POST http://127.0.0.1:8080/v1/function

ペイロード:

{
  "id": "unique-id",
  "file_name": "filename.ext",
  "code": "source code with functions"
}
  • id: a correlation identifier for the request. Optional (issue #645); omitting it defaults to an empty string (treated as “no correlation id”).
  • file_name: the name of the file being analyzed.
  • code: the source code with functions.

レスポンス:

{
  "id": "unique-id",
  "language": "cpp",
  "spans": [
    {
      "name": "function_name",
      "start_line": 1,
      "end_line": 10
    }
  ]
}

The envelope reports id, the detected language slug, and the spans result key. name is null when the parser could not resolve the function’s name from the AST (an anonymous or malformed definition). A null name is the malformed-span signal.

4. AST の取得

Returns bca’s JSON representation of the parsed syntax tree, as a recursive node tree. The dump normalizes some node kinds and flattens some constructs, including string literals.

リクエスト:

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: a correlation identifier for the request. Optional; omitting it defaults to an empty string (treated as “no correlation id”).
  • file_name: the name of the file being analyzed.
  • code: the source code to parse.
  • comment: when true, comment nodes are omitted from the tree. Optional; defaults to false.
  • span: when true, each node carries its source span; when false, span is null. Optional; defaults to 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,
              "start_byte": 0, "end_byte": 13 },
    "field_name": null,
    "children": [
      {
        "type": "function_item",
        "value": "",
        "span": { "start_line": 1, "start_col": 1, "end_line": 1, "end_col": 13,
                  "start_byte": 0, "end_byte": 12 },
        "field_name": null,
        "children": [
          {
            "type": "identifier",
            "value": "main",
            "span": { "start_line": 1, "start_col": 4, "end_line": 1, "end_col": 8,
                      "start_byte": 3, "end_byte": 7 },
            "field_name": "name",
            "children": []
          }
        ]
      }
    ]
  }
}

The envelope reports id, the detected language slug, and root, the root AST node. Each node carries:

  • type: tree-sitter 文法のノード種別です(文法固有です。どの文法が生成したかは language スラッグで分かります)。
  • value: リーフ/名前付きトークンのソーステキストです(内部ノードでは空です)。
  • span: a { start_line, start_col, end_line, end_col, start_byte, end_byte } object (lines and columns 1-based; byte offsets 0-based and half-open, indexing into the submitted code), or null when span was false. These span keys use the *_line vocabulary shared with /function and /metrics (issue #638 renamed the former *_row keys).
  • field_name: the tree-sitter grammar field through which the parent reaches this node (name, left, body, and so on), or null for the root, anonymous tokens, and unfielded children.
  • children: ノードの子ノードです(再帰的に続きます)。

Unlike the metric and function endpoints, the AST endpoint reports node coordinates over the exact bytes the client submitted: the source is not EOL-normalised, so spans line up with the client’s own copy (issue #640).

5. Compute metrics

Computes the maintainability metrics for the submitted source.

リクエスト:

POST http://127.0.0.1:8080/v1/metrics

ペイロード:

{
  "id": "unique-id",
  "file_name": "filename.ext",
  "code": "source code for metrics",
  "scope": "full"
}
  • id: a correlation identifier for the request. Optional (issue #645); omitting it defaults to an empty string (treated as “no correlation id”).
  • file_name: the filename of the source code file.
  • code: the source code to analyze.
  • scope: how much of the space tree to return. full (the default) returns the complete nested space tree: the file-level root plus a recursive spaces list for every function, class, and other unit. file returns only the file-level root with its spaces children cleared. This field replaces the pre-2.0 boolean unit flag (issue #638); sending the old unit key now fails with 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, "dloc": 0 },
          "nom": { "functions": 1, "closures": 0, "total": 1 }
        }
      }
    ],
    "metrics": { "...": "the same metric block, aggregated over the file" }
  }
}

The response envelope reports id, the detected language slug, and root, the single file-level space object (issue #638 renamed this key from the misleading plural spaces). root carries name (the request file_name), the start_line / end_line span, a kind discriminator (unit, function, class, …), its metrics block, and a recursive spaces list of child units. The example above is trimmed: each real metrics block contains every metric family (cyclomatic, cognitive, halstead, loc, nom, nargs, nexits, tokens, mi, abc, and, on the file root and container spaces, wmc, npm, and npa) with the same nested fields the bca CLI emits. When scope is file, root.spaces is an empty list.

The language value is the canonical lowercase slug (rust, cpp, csharp, tsx), the same token the language vocabulary accepts, not a human-pretty display name. Every analysis endpoint (/ast, /comment, /function, /metrics) reports this language field so clients can confirm which grammar was selected.

6. Server and library version

実行中のサーバーのバージョンと、そのビルドに使用された big-code-analysis ライブラリのバージョンを報告します。

リクエスト:

GET http://127.0.0.1:8080/v1/version

レスポンス:

{
  "server": "2.4.0",
  "library": "2.4.0"
}

7. Supported languages

対応言語と、登録されているファイル拡張子の一覧を返します。名前は正規の小文字スラッグです。一覧と拡張子はライブラリの言語テーブルから取得され、ハードコードされることはありません。

リクエスト:

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. ルートインデックス

Returns a machine-readable index of every registered route (its path, the HTTP methods it accepts, and a one-line description) so clients can discover the API surface without scraping this chapter. The index is generated from the same route table the server registers, so it cannot drift from the live routing.

リクエスト:

GET http://127.0.0.1:8080/v1

レスポンス:

{
  "service": "bca-web",
  "version": "2.4.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)メトリクス

Three endpoints expose the change-history (version-control) metrics, the same numbers bca vcs computes from the CLI. Unlike every other endpoint, these analyse a git repository already present on the server’s filesystem rather than source code carried in the request body: VCS metrics derive from commit history, which has no in-request representation.

Operator warning: repo_path is a trust boundary. The repo_path field is a server-side filesystem path. These endpoints make the server walk any git repository it can read and return that repository’s relative file paths, churn, and author signals. This is materially different from the source-in-body endpoints, which only ever see code the client sends. The optional cache_dir field is a second caller-supplied server-side path that grants a write capability: with caching enabled (the default), the server creates directories and writes JSON cache files under it (<cache_dir>/<repo>/<head_sha>.json), so a caller controlling cache_dir can direct the server to write cache files at any path the server process can write to. (cache_dir is accepted only by /vcs; /vcs/trend and /vcs/jit do not cache.) The endpoint’s filesystem reach is therefore an arbitrary read of any readable git repository and an arbitrary write of cache files under any writable path. Do not expose /vcs, /vcs/trend, or /vcs/jit to untrusted clients without an authorization layer in front of bca-web. The default 127.0.0.1 bind keeps them local. Each walk runs under the same parse-timeout and blocking-pool guard as the analysis endpoints.

All three endpoints are POST-only, accept application/json, echo the request id, and report errors with the uniform {error, error_kind, id} body; its error_kind tokens are the vcs_* family (vcs_not_a_repository, vcs_invalid_window, and so on). A client mistake is a 400: a repo_path that does not exist or is not a git working tree (both carry vcs_not_a_repository), an unresolvable ref/commit, a malformed or non-diff diff, an unusable author_hash_key (vcs_invalid_author_hash_key), or a malformed window / timestamp / formula / file-type / threshold / trend parameter. A failure of the history walk itself is a 500. A nonexistent repo_path is a typo, the most common client error here, so it answers 400 like a path that exists but is not a repository, not a 500 (issue 653).

9. Rank files by risk: /vcs

リポジトリの履歴を一度だけ走査し、複合リスクスコアでランク付けしたファイル一覧を返します(issue #328)。

リクエスト:

POST http://127.0.0.1:8080/v1/vcs

ペイロード:

{
  "id": "unique-id",
  "repo_path": "/srv/repos/my-project"
}

repo_path is required; every other field is optional and defaults to the bca vcs default. id is optional too (issue #645); omitting it defaults to an empty string, echoed back unchanged. The optional fields are:

  • long_window / recent_window: window specs such as 12mo or 90d. Defaults 12mo / 90d.
  • top: リスク上位 N 件のファイルのみを保持します。未指定の場合のデフォルトは 50(bca vcs --top のデフォルト)で、明示的に 0 を指定するとすべてのファイルを返します。
  • ref: 解析対象のリビジョンです(デフォルトは HEAD)。
  • risk_formula: weighted(デフォルト)または percentile です。
  • file_types: metrics (default: only files bca has metrics for), all (every tracked text file), or a comma-separated extension allow-list (rs,py).
  • full_history: 第一親のみではなく、完全な DAG を走査します。
  • include_merges: マージコミットを含めます。
  • follow_renames: リネームを追跡します(デフォルト true)。
  • exclude_bots: ボットの識別情報を除外します(デフォルト true)。
  • bot_pattern: ボット作者を除外する正規表現を上書きします。
  • as_of: reference “now” (RFC 3339 / @unix / git date) for snapshots.
  • 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. Historical trend: /vcs/trend

変更履歴メトリクスを等間隔の複数時点でサンプリングし、ファイルごとの時系列を返します(issue #333)。応答はランク付けされたスナップショットではなく系列であるため、/vcs とは別のルートになっています。

リクエスト:

POST http://127.0.0.1:8080/v1/vcs/trend

Payload: every /vcs field above except the cache controls (no_cache / cache_dir), plus the three below. Trend does not use the persistent cache, so sending either cache field is a 400 (issue #961) rather than a silent no-op.

  • 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. Just-in-time risk: /vcs/jit

Scores the just-in-time risk of a single change: either one commit on a server-side repository, or an arbitrary unified diff carried in the request body (issues #331 / #580). The two modes are mutually exclusive.

コミットモードは 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"
}

A bare diff carries no author, parent, or history, so only the size and diffusion groups are computable. The diff report’s source is "diff" and it reports partial_risk_score, not risk_score, because the missing groups are absent from the body entirely, never present as zero:

{
  "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 }
}

Branch on the source discriminator ("commit" vs "diff") to read the right score field. partial_risk_score sums only the size and diffusion contributions, while a commit’s risk_score also folds in history, purpose, and the negatively signed experience group, so the two are neither ordered nor on the same scale: rank diffs against other diffs, never against commit scores.

Mode conflict. Supplying diff together with any commit-mode field (repo_path, commit, a window, history, rename, or as_of knob) is rejected with a 400 rather than silently honouring the diff and dropping the rest. The two modes answer different, non-comparable questions, so the combination is treated as a client mistake (issue number 632).

Non-diff diff. A diff value that is not a git unified diff (the wrong field, an accidentally-mangled string, arbitrary text) is rejected with a 400 (vcs_invalid_diff) rather than scored as a confident partial_risk_score of 0.0. On a risk-gating endpoint a spurious “zero risk” is the most dangerous failure mode, so non-diff input is a hard error (issue 652). An empty or whitespace-only diff is the one exception: it legitimately means “no changes”, so it still returns a valid 0.0, and a CI step that computed an empty diff gets the zero-risk answer it expects.

bca-web の運用

bca-web は big-code-analysis ライブラリをラップする HTTP デーモンで、コメント除去、関数スパン、AST ダンプ、保守性メトリクス、変更履歴(VCS)メトリクスを REST API として公開します。このページはデーモンを運用するオペレーター向けに、ビルドと起動の方法、チューニングに使うフラグと環境変数、そして公開前に守るべき信頼境界を説明します。

Use the REST API reference for request and response fields, and Driving the REST API for complete curl examples. This page covers the process itself and links to those two rather than repeating them.

ビルドと実行

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 のスモークチェック)が失敗を検知して再起動や警告を行えます。

言語のサブセットでのビルドは機能しません

The shipped bca-web binary compiles every supported tree-sitter grammar in. The big-code-analysis-web crate pins the library’s all-languages feature set explicitly, so passing --no-default-features or a custom --features list to cargo build -p big-code-analysis-web does not drop grammars from the resulting binary. Dropping a grammar silently from a user-facing daemon would surface as “language X stopped working” at request time rather than as a build error, so the crate forbids it (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>8080TCP ポート。
--parse-timeout-secs <SECS>30パース 1 回あたりの期限。0 で無効になります。
--cors <ORIGINS>オフカンマ区切りのオリジン許可リストに対して CORS を有効にします。
-h, --helpヘルプを表示して終了します。
-V, --versionバージョンを表示して終了します。

--num-jobs auto is cgroup-quota- and cpuset-aware on Linux: in a container with a CPU quota it resolves to the quota rather than the host’s physical core count, matching the bca CLI’s --num-jobs. This count sizes the worker pool and the parse-admission semaphore, so it caps how many parses run concurrently. The minimum is 1; 0 is rejected when the flag is parsed.

--parse-timeout-secs は、リクエストが 504 Gateway Timeout を返すまでに 1 回のパースが実行できる時間を制限します。デフォルトの 30 は、病的な入力がワーカーを無期限に塞ぐことを防ぎます。0 に設定すると期限がなくなり、それとともに後述の負荷制御も無効になります。無制限のパースが許容できる場合にのみ 0 を使ってください。タイムアウト時に返されるレスポンスボディは REST API リファレンスを参照してください。

CORS is off by default. The CORS section of the reference documents it in full, covering preflight handling, the wildcard form, and the absence of credentials. The short version: pass --cors with an explicit origin allow-list to let browser tooling read responses; omit it to emit no Access-Control-* headers at all.

環境変数

変数デフォルト目的
BCA_MAX_ORPHANED_TASKSmax(num_jobs * 2, 4)孤児化した(タイムアウト後もまだ実行中の)パースタスクの上限。これに達すると新しいリクエストは 503 で遮断されます。
RUST_LOGinfotracing サブスクライバーのログフィルター。

RUST_LOG は EnvFilter の構文を使います(例: RUST_LOG=big_code_analysis_web=debug)。デーモンは完了したリクエストごとに、メソッド、ルート、ステータス、レイテンシを含むアクセスログを 1 行出力します。

リソース制限とバックプレッシャー

単一のクライアントによるリソース枯渇からデーモンを守る制限が 2 つあります。

リクエストボディサイズ。 すべてのエンドポイントは、4 MiB を超えるリクエストボディを 413 Payload Too Large で拒否します。この制限は JSON と raw オクテットストリームのコンテンツタイプに一律に適用されるため、どちらも同じしきい値で過大なボディを拒否します。

Orphaned-task admission control. When a parse exceeds --parse-timeout-secs, the request returns 504, but the blocking thread keeps running on the pool until the parse finishes on its own, because tree-sitter cannot be interrupted mid-parse. To stop sustained pathological input from piling up unbounded background work, new requests are rejected with 503 Service Unavailable once the count of orphaned tasks reaches a soft cap. The cap defaults to max(num_jobs * 2, 4); BCA_MAX_ORPHANED_TASKS overrides it (parsed as an unsigned integer; an invalid or zero value falls back to the default). Setting --parse-timeout-secs 0 disables this mechanism entirely, since with no deadline no task is ever orphaned.

セキュリティと信頼境界

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 上にあることを前提とします。

レシピは目的別にまとめられています:

  • Quality reports: generate Markdown reports for pull requests, dashboards, or wikis, including the C/C++ preprocessor-aware workflow.
  • CI integration: wire bca check and bca report into GitHub Actions and GitLab CI, including the baseline / ratchet pattern and the Code Quality widget path.
  • Baselines: record existing offenders in a .bca-baseline.toml so the gate only fires on new or worsened violations, and audit or diff that baseline during review.
  • Local threshold gates: mirror the CI threshold gate on a developer machine with a two-tier (hard + headroom) Makefile / just / pre-commit pattern, so regressions never reach the pull request.
  • Choosing thresholds: learn where the shipped defaults come from, choose language-specific overrides, and select limits for CI, agent feedback, legacy triage, or safety-critical work.
  • Feeding metrics to an agent: wire bca check into an agentic coding tool’s after-edit feedback loop (Claude Code PostToolUse hook, OpenCode plugin), with the anti-gaming guidance that keeps the loop honest.
  • AST queries: search for syntactic constructs, count node types, dump trees, and detect parse errors.
  • Exporting metric data: emit structured output (JSON / YAML / TOML / CBOR) and consume it from shell pipelines.
  • Driving the REST API: run the HTTP server and call every endpoint with curl.

For a deeper look at any flag the recipes use, see the per-command pages under Commands. For the full list of metrics that show up in these recipes, see Supported Metrics.

Upstream reference. big-code-analysis is a fork of Mozilla’s rust-code-analysis. Recipes that work for the upstream rust-code-analysis-cli binary usually translate directly: replace the binary name and adjust for the subcommand restructure documented in the migration guide.

品質レポート

集約された人間可読の Markdown レポートを生成するためのレシピです。

Wiring reports into CI? See the CI integration recipe for runnable GitHub Actions and GitLab CI examples that post the Markdown report as a PR/MR comment and surface threshold violations through the platform’s native code quality widgets.

実際のレポート例

big-code-analysis publishes the output of bca report -O markdown --vcs and bca report -O html --vcs against its own source tree on every push to main. Open either to see what the recipes on this page produce on a multi-language Rust + Python codebase:

これらを生成する仕組みは .github/workflows/pages.yml にあります。同じワークフローがしきい値ゲートも実行します。パイプライン全体の構成は CI 統合 を参照してください。

プロジェクト全体の品質レポートを生成する

プロジェクトルートから実行し、レポートをファイルに書き出します:

bca report \
    --paths "$PWD" \
    -O markdown \
    --top 20 \
    --strip-prefix "$PWD/" \
    --output report.md
  • --strip-prefix keeps the file paths short and stable across machines. Without it every row carries the absolute path of the current checkout.
  • --top controls how many rows appear in each hotspot table. 20 is a reasonable default for a PR comment; drop to 5 for a dashboard tile, or pass 0 to list every row.
  • --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/**"。

Leading ./ is optional. A bare-relative pattern and its ./-prefixed spelling are equivalent: --exclude "vendor/**" matches exactly what --exclude "./vendor/**" does. This holds for every glob surface: --include, --exclude, --exclude-from, .bcaignore, and the [check.exclude] gate-exemption set.

リポジトリ全体で安定した除外セットを維持するには、パターンをリポジトリルートのファイル(慣例として .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 します:

work=$(mktemp -d "${TMPDIR:-/tmp}/bca-report-diff.XXXXXX")
git worktree add "$work/before" main
bca report -p "$work/before" -O markdown \
    --strip-prefix "$work/before/" --output "$work/before.md"

bca report -p "$PWD" -O markdown \
    --strip-prefix "$PWD/" --output "$work/after.md"

diff -u "$work/before.md" "$work/after.md" | less

Because both reports use the same --strip-prefix shape, the path columns line up and the diff shows metric changes rather than path noise.

C/C++ プリプロセッサ対応レポート

For macro-heavy C/C++ codebases, feed preprocessor data into the analyzer so conditional compilation is interpreted the way the compiler sees it. The workflow is two steps:

# 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

Every metric-computing walking subcommand (metrics, ops, functions, report, check, …) accepts --preproc-data. Subcommands that do not consume it (vcs, preproc, list-metrics, diff-baseline) reject it as a usage error.

Step 1’s document is deterministic. The files map is written in sorted path order, and each entry’s direct_includes, indirect_includes, and macros arrays in sorted name order, so two bca preproc runs over an unchanged tree produce byte-identical output, on stdout as well as through --output. Check /tmp/preproc.json into a build cache or diff it between branches without spurious churn, as you can a metrics --output or ops --output document.

You need the sorting rules only when reproducing the output order. The files keys sort by path component, so a/x.h precedes a-b/x.h. Include and macro arrays sort by bytes, so a-b/x.h precedes a/x.h. You do not need to reproduce either sort when reading the document; jq preserves the stored order.

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 keeps Added and Modified files and drops Deletions, since a file that no longer exists cannot be analyzed.
  • --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

A directory walk honors .gitignore automatically, so the recipes earlier on this page need no explicit -X "**/target/**" -X "**/node_modules/**" when your project’s .gitignore already covers those paths. Add --no-ignore to analyze gitignored trees.

CI 連携

Recipes for wiring bca into a build pipeline. The bca check command ships every output shape a CI system consumes (Checkstyle, SARIF, GitLab Code Climate JSON, clang/GCC warning lines, MSVC warning lines), plus bca report markdown for humans. This page maps each goal to the subcommand, flags, and platform glue that serve it. Copy-paste snippets come first; the design notes behind them (live worked example, self-scan gate, trust boundary) come after.

出力の選び方

The matrix below maps each common goal to the bca invocation that feeds the corresponding CI surface. The linked sections have the runnable example.

目的コマンド + フラグ
しきい値リグレッションに対するハードゲート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

For a reproducible install without a Rust toolchain, use the prebuilt tarball from this repository’s GitHub Releases. It is a single curl | sha256sum | tar and produces byte-identical binaries across runs. Pair it with actions/cache keyed by version so a green-path rerun skips the download entirely:

CLI-artifact schema compatibility. The BCA_VERSION you pin here must support the schema version of every CLI artifact your repo commits, above all .bca-baseline.toml (which carries its own version field) and the bca.toml manifest. A baseline file written by a newer bca (carrying a newer schema version) is not loadable by an older bca, and the gate fails with baseline version N is not supported by this bca. When tracking main or regenerating baselines locally with a newer bca, either re-pin to a release that covers the new schema or switch to a cargo install --git build of bca pointed at the same commit your baseline was written from (see the cargo install alternative below). The compatibility contract is recorded in STABILITY.md.

env:
  BCA_VERSION: "2.4.0"
  BCA_TARGET:  "x86_64-unknown-linux-gnu"
  # リリースの SHA256SUMS ファイルにある big-code-analysis-${BCA_VERSION}-${BCA_TARGET}.tar.gz の sha256。
  # BCA_VERSION と合わせて更新してください。
  BCA_SHA256:  "d132c0ef559b4917b7679e17fe3d8f69f5bc14a67692f5d3596d0ec21954d20e"

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

When a runner cannot reach github.com (air-gapped, custom mirror) but can reach crates.io, the two actions below fall back to cargo install when no prebuilt is published, at the cost of compile time on the cold path. Both pin to the same crates.io release as the GitHub Releases assets, so the CLI-artifact schema compatibility warning applies here unchanged.

If you need a bca ahead of the latest crates.io release (for example, your .bca-baseline.toml is committed at a newer schema than any published bca understands), swap the tool: big-code-analysis-cli@<version> or --version form for cargo install --git https://github.com/dekobon/big-code-analysis --rev <SHA> --locked big-code-analysis-cli against the exact commit the baseline was generated from. This is what the in-tree pages.yml workflow does (against the local checkout via --path). It is a deliberate workaround for bca’s own repo, not a recommended default for downstream adopters.

Use taiki-e/install-action or cargo-binstall.

# 選択肢 1: taiki-e/install-action
- name: Install bca
  uses: taiki-e/install-action@v2
  with:
    tool: big-code-analysis-cli@2.4.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.4.0

If either action falls back to compilation, cache the cargo registry and the installed binary so the second run is fast:

- name: Cache cargo registry and bca binary
  uses: actions/cache@v5
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      ~/.cargo/bin/bca
    # crates.io publishes immutable releases, so a `<version>` key is
    # sufficient here; there is no sha256 to rotate. (The GitHub
    # Releases install path above is different: republished release
    # assets share a version, so its cache key must include the sha256.)
    key: bca-${{ runner.os }}-2.4.0

Pin to a specific version (matching a published big-code-analysis-cli release on crates.io) so reports stay reproducible across runs. A floating install surfaces metric-counting changes as unexplained CI flakes.

しきい値ゲート、SARIF、clang-warning マッチャー

Three recipes live on the Check command page rather than here: the hard threshold gate, the SARIF upload to Code Scanning, and the clang-warning output paired with the GCC problem matcher for inline PR annotations.

Markdown レポートを PR コメントとして投稿する

bca report markdown is built for PR/MR comments: a stable header structure, one row per hot spot, and short paths once you pass --strip-prefix. Pair it with marocchino/sticky-pull-request-comment so each push updates a single comment instead of stacking new ones:

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.4.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

The same Markdown file is suitable for upload as a build artifact (actions/upload-artifact@v7) if you want it downloadable from the workflow run page in addition to the PR comment.

ベースライン / ラチェットパターン

bca check --baseline はネイティブのラチェットです。今日時点の違反をコミット済みの TOML ファイルに記録し、リグレッションと新規違反に対してのみ失敗させ、ファイルを時間をかけて縮小していきます。一度ブートストラップしてコミットし、CI をそのファイルに向けます:

# 開発者マシンで一度だけ実行します。両方のファイルをコミットしてください。
bca check --paths src/ \
    --write-baseline .bca-baseline.toml
git add bca.toml .bca-baseline.toml

This snippet bootstraps from src/ only, which suits a single-crate library. For a multi-crate workspace, see the live worked example: its .github/workflows/pages.yml scans the entire repo with --exclude-from .bcaignore, a checked-in deny-set covering vendored grammars, generated trees, and tests.

Share the exclude list across workflow, recipe, and bootstrap. Put the deny-set in a single file at the repo root (a .bcaignore by convention, mirroring .gitignore / .dockerignore) and point every bca invocation at it with --exclude-from .bcaignore. Patterns from --exclude-from are unioned with any inline --exclude <GLOB> flags into one deny-set; keep --exclude for one-off excludes. Blank lines and #-prefixed comment lines in the file are skipped. Patterns follow the same ./-prefix convention as --exclude arguments (the walker’s emitted form). Pair edits to .bcaignore with a --write-baseline refresh, because the baseline keys depend on which files the walker visits.

- name: Threshold check with baseline
  run: |
    bca check --paths src/ \
        --baseline .bca-baseline.toml

A regressed function (worse than its baseline value in the metric’s own direction: higher, or lower for the mi.* family) still fails. A new offender not in the baseline still fails. An improved function passes silently and stays in the baseline until the next --write-baseline refresh.

Each surviving violation row starts with a tag that distinguishes a new offender from a known one that has worsened:

  • [new]: no baseline entry for this function / metric.
  • [regr …]: the current value is worse than the recorded baseline, measured in the metric’s own direction (for the lower-is-worse mi.* family that is a drop, still rendered +). The amount comes in two shapes: [regr +N%], a percentage of the record, for counts, and [regr +N.N pts], index points, for mi.*, whose arbitrary zero makes a percentage meaningless. Both start [regr , so rg '\[regr ' matches every regressed row. Special forms: [regr from 0] when a count’s baseline value was zero, [regr +>9999%] when a count’s regression exceeds 100× the baseline, [regr NaN] when the current value is NaN. See Reading the gate output for the details.

違反行は stdout に出力され、それより下のものはすべて stderr に出力されます。完全な振り分けはどちらのストリームかを参照してください。

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. It is meant 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.

対処しやすい失敗出力

The four sub-sections below turn bca check’s failure output from a wall of offender lines into CI-aware presentations: which files in this PR tripped a threshold (--since / --changed-only), inline file-diff annotations (--github-annotations), a rendered step-summary digest ($GITHUB_STEP_SUMMARY), and a copy-paste-safe remediation block. Each is independent; mix and match per CI surface. A combined worked example is at the end of this group.

差分対応モード(--since / --changed-only)

On a PR or push, the developer’s first question is usually which of my files in this change tripped a threshold, not the whole-tree offender list. Two flags answer that:

  • --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 は次の優先順位で環境から差分ベースを自動検出します。

  1. BCA_DIFF_BASE: the explicit override. Use this from a local shell or non-GHA CI runner to mimic the auto-detection.
  2. GITHUB_BASE_REF: set by GitHub Actions on pull_request events. Expanded to origin/<value>; the runner is responsible for the corresponding git fetch.
  3. GITHUB_EVENT_BEFORE: set by GitHub Actions on push events to the SHA at HEAD before the push. The all-zeroes SHA (force push, brand-new branch) is treated as no signal.

A failed auto-detection (git missing, ref unresolvable, not a git checkout) is non-fatal without --changed-only: bca prints a warning and falls back to the whole-tree footer. With --changed-only, the same failure is fatal, so a misconfigured CI cannot pass by suppressing every violation.

“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)

The GHA UI renders ::error file=…,line=…,title=…::msg workflow commands as inline annotations on the file-diff view, which are easier to find than lines in the raw job log. bca check emits one per violation according to the tri-state --github-annotations <auto|always|never>: auto (the default) enables them when $GITHUB_ACTIONS == "true" (set by every GHA workflow step), always forces them on, and never suppresses them even inside a step (useful when a workflow runs bca check twice and wants annotations from only one run). A bare --github-annotations means always.

The annotations are emitted alongside the existing per-violation human stream, not instead of it. To avoid exhausting GitHub’s 10-error-per-step UI quota, annotations are capped at 10 per metric; overflow rolls up to a single ::error::N more <metric> violations not shown line so the count stays visible.

- name: インラインアノテーション付きのしきい値チェック
  run: |
    bca check --paths . --exclude-from .bcaignore \
        --baseline .bca-baseline.toml
  # No `--github-annotations` flag needed: auto-enabled in GHA.

上記の --since と組み合わせると、アノテーションが違反者リスト全体ではなく PR 内のファイルを指すようになります。

ステップサマリーの Markdown ダイジェスト($GITHUB_STEP_SUMMARY)

GitHub Actions exposes $GITHUB_STEP_SUMMARY, a path to a markdown file that, when populated, renders as the step’s summary view in the job UI. Whenever that env var is set, or when --summary-file <path> is passed explicitly, bca check appends a digest containing the per-file rollup table, a per-metric count breakdown, and the top-10 offenders by severity (ranked the same way as the footer’s worst metric). --summary-file never suppresses the digest even when $GITHUB_STEP_SUMMARY is set.

The digest is bracketed by HTML-comment markers (<!-- bca-step-summary-begin --> / <!-- bca-step-summary-end -->) so a retried step replaces the previous block rather than stacking a new one; three retries converge to exactly one up-to-date digest. Content outside the markers (for example, summaries written by other tools earlier in the same step) is preserved.

- name: ステップサマリーダイジェスト付きのしきい値チェック
  run: |
    bca check --paths . --exclude-from .bcaignore \
        --baseline .bca-baseline.toml
  # No flag needed: `$GITHUB_STEP_SUMMARY` is set automatically in GHA.

ローカルユーザーは --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

The refresh invocation mirrors every flag the gate ran with that decides what a baseline records, so a first-time reader of a failing CI log can copy-paste it verbatim. That covers the walk scope (--paths, --include, --exclude, --exclude-from, --paths-from), the walker tuning (--language, --exclude-tests, --cyclomatic-count-try, --no-ignore, --no-skip-generated, --no-config), and the gate’s own inputs (--config, --threshold, --tier, --no-suppress, --check-exclude, --check-exclude-from, --baseline-fuzzy-match, --baseline). Flags that change only reporting or the exit code (--jobs, --report-format, --output, --no-fail, --exit-codes) are left off, since none can move an entry into or out of the file. The artifact URL is derived from $GITHUB_REPOSITORY and $GITHUB_RUN_ID when both are present (always true in GHA). Local runs have no upload to point at, so they suggest running bca report for the detailed view instead.

統合されたストリームを読むツール向けには --no-remediation でブロックを抑制します。素の bca check | ... パイプラインがこのブロックを見ることはありません。

集中的なリファクタリングの後に更新します。

bca check --paths src/ \
    --write-baseline .bca-baseline.toml
git diff .bca-baseline.toml   # ファイルは縮小していくはずです

Two --write-baseline runs over an unchanged tree produce byte-identical output, so a diff appears only when offenders changed. See the Baselines recipe for the full adoption flow, PR-review heuristics, and the suppression composition rules.

すべてを組み合わせる

上記の 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 で得られるもの:

  1. Per-violation rows on stdout, in the same shape as the legacy gate, so existing grep tooling keeps working.
  2. Per-file rollup footer with Files in this range: (touched in the PR) listed before Other offenders:, so the developer sees their own contributions first.
  3. インライン GHA アノテーション — ファイル差分ビュー上に表示され、メトリクスごとに 10 件で打ち切られ、超過分はまとめて表示されます。
  4. Step-summary panel with a rendered markdown digest (per-file rollup, per-metric breakdown, top-10 offenders by severity).
  5. 末尾の修復ブロック — アーティファクト名を示し、そのまま使える --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 での体験をローカルでプレビューするには次のようにします。

summary=$(mktemp "${TMPDIR:-/tmp}/bca-summary.XXXXXX.md")
GITHUB_ACTIONS=true GITHUB_STEP_SUMMARY="$summary" \
  BCA_DIFF_BASE=main \
  bca check --paths . --exclude-from .bcaignore \
      --baseline .bca-baseline.toml
cat "$summary"

GHA 以外の CI(GitLab、Buildkite、Jenkins)では、ランナーが公開する環境変数を設定する(またはフラグを明示的に渡す)ことで、同じ出力経路が動作します。

マージベースに対する違反者数の差分(暫定策)

For teams who cannot commit a baseline file (for policy reasons, say), a coarser approximation counts <error> elements in two Checkstyle documents, one on the merge base and one on the PR head, and fails when the count grows:

- 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

これは違反の件数を数えるだけで、その同一性は追跡しません。違反している関数の名前を変えてもリグレッションとして検出されず、ある違反者を改善しつつ別の違反者を悪化させると差し引きゼロになります。上記のネイティブなベースラインフローの方が厳密に高精度であり、推奨されるアプローチです。

実際の動作例

big-code-analysis runs the recipes above against its own source on every push and PR. The workflow source, .github/workflows/pages.yml, exercises the threshold gate, the baseline ratchet, both report formats, and a SARIF upload to GitHub Code Scanning end-to-end against the workspace itself. (The SARIF upload runs on same-repo pushes and PRs only; fork PRs skip it because the upload needs a write-scoped token, exactly as the clippy SARIF job does.) The output sits on GitHub Pages alongside this book:

The bca version quoted in the snippets on this page is the latest published release at the time of writing.

The in-tree workflow installs bca by building it from the current checkout rather than downloading a pinned release. This avoids the CLI-artifact schema-skew failure mode described under Installing bca from a GitHub Release for repos whose .bca-baseline.toml is always written by the same bca that gates it. Downstream adopters tracking a stable release line should stick with the pinned-tarball pattern; only switch to “build from checkout” if you, too, are mutating CLI artifact schemas in lockstep with the binary.

セルフスキャンしきい値ゲート(CI ゲートのローカルミラー)

CI’s threshold gate fires only after push, which is too late if a refactor silently nudged a metric past its limit. The big-code-analysis repo’s Makefile exposes four targets that mirror the CI gate (the Threshold gate step in .github/workflows/pages.yml) locally and add a second tier at 95% of every limit, so encroachment is caught a commit or two before the hard gate trips:

make self-scan                            # ハードゲート。bca.toml のしきい値の 100%
make self-scan-headroom                   # ソフトゲート。デフォルト 95%(BCA_HEADROOM)
make self-scan-write-baseline             # ハードしきい値でベースラインを更新
make self-scan-write-baseline-headroom    # ソフトしきい値でベースラインを更新

Path selection, the .bcaignore deny-set, the per-function thresholds, the cyclomatic ? policy, and the baseline file all live in the repo-root bca.toml manifest, which bca discovers automatically. The hard tier is exactly what CI runs; expanded, it is a bare check with no path, threshold, or baseline flags, because the manifest supplies them:

cargo run --quiet -p big-code-analysis-cli -- check

That is the dev profile: the release relink costs ~100 s after any edit under src/, and both profiles write a byte-identical baseline, which make self-scan-profile-parity checks per PR. The recipes go through a small wrapper that can also run an already-installed bca when it proves it was built from the checkout; that mechanism and its knobs are repository tooling, documented in the Makefile, not part of the gate an adopter copies.

Both tiers consume the same bca.toml thresholds and the same .bca-baseline.toml; the soft tier runs the hard recipe with every threshold value multiplied by BCA_HEADROOM. Both exit 0 clean, 2 on any threshold violation, and 1 on tool error. The soft tier is a real gate, not advisory, so do not wrap make self-scan-headroom in || true. The two gate targets (self-scan, self-scan-headroom) are wired into make pre-commit, make ci, and .pre-commit-config.yaml; those chains run the hard tier before the soft tier, so a true regression always reports before near-limit headroom. The two write-baseline targets are side-effecting and deliberately not wired in.

BCA_HEADROOM=0.90 make self-scan-headroom widens the band; BCA_HEADROOM=0.99 tightens it to the last 1%. When the soft tier fires, absorb the offender into the baseline with make self-scan-write-baseline-headroom, which records every offender at the scaled thresholds (a strict superset of the hard-tier offenders).

The pattern (hard tier mirroring CI, soft tier as an early-warning band, both ratcheted by the same baseline) is project-agnostic. The Local threshold gates recipe documents the underlying principles, drop-in Makefile / just / package.json skeletons, and the helper script that scales thresholds, so you can adopt the same workflow in your own repo. The generic recipe uses the same BCA_* env-var names as the Makefile above, so overrides like BCA_HEADROOM=0.90 work identically across both.

GitLab CI

完全な .gitlab-ci.yml の例

以下のジョブは bca をインストールし、しきい値チェックを実行して Code Climate JSON(MR の Code Quality ウィジェット用)、Checkstyle XML、Markdown レポートを生成し、それらをアーティファクトとしてアップロードします。

The same CLI-artifact schema-compatibility note from the GitHub Actions section applies here: the BCA_VERSION pin must cover the schema version of every CLI artifact you commit.

stages:
  - quality

variables:
  BCA_VERSION: "2.4.0"  # pin a published big-code-analysis-cli release
  BCA_TARGET:  "x86_64-unknown-linux-gnu"
  # リリースの SHA256SUMS ファイルに記載された
  # big-code-analysis-${BCA_VERSION}-${BCA_TARGET}.tar.gz の sha256。BCA_VERSION と一緒に更新します。
  BCA_SHA256:  "d132c0ef559b4917b7679e17fe3d8f69f5bc14a67692f5d3596d0ec21954d20e"

bca-quality:
  stage: quality
  image: debian:stable-slim
  cache:
    # Same key shape as the GitHub Actions snippet: bumping
    # BCA_VERSION invalidates the cache automatically.
    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 keeps every artifact downloadable even on a red pipeline, which is when you need them most.
  • artifacts:reports:codequality wires the Code Climate JSON directly into GitLab’s MR Code Quality widget; see the Code Quality widget section below for the field-by-field semantics.

GitLab Code Quality ウィジェット

GitLab’s Code Quality view (inline complaints on the MR diff, summary on the MR overview page) consumes Code Climate JSON. bca check emits this via --report-format code-climate, so the integration is a one-liner:

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

Severity bands are derived from how far each metric breaches its configured threshold: value / limit for a count, and for the maintainability-index family, where lower is worse and the index goes negative, one step per maintainability band below the floor. ≤ 1.5 → minor, ≤ 2 → major, ≤ 4 → critical, > 4 → blocker; bca check’s summary ranks offenders by the same key. The widget deduplicates findings by fingerprint; bca hashes path \0 function \0 metric (no line, no value) so a violation that survives an upstream line-drift edit still collapses into the same widget entry across pipeline runs.

生成されたレポートをローカルで検証するには次のようにします。

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

Both Jenkins (via the Warnings Next Generation plugin) and SonarQube (via its Generic Issue importer) consume Checkstyle 4.3 XML directly. The same invocation feeds both:

bca check --paths src/ \
    --report-format checkstyle \
    --output report.checkstyle.xml

report.checkstyle.xml を、既存の Jenkins の Record Issues / SonarQube の External Issues ステップに接続してください。Checkstyle ライターは違反者がいないときも空の(整形式の)ドキュメントを出力するため、どちらのツールもクリーンな実行を特別扱いする必要はありません。ライターのスキーマの詳細は check コマンドのページを参照してください。

汎用的な CI ガイダンス

以下はプロバイダーを問わず当てはまります。

  • Pin bca to a specific version. Both cargo install --version and cargo binstall --version accept the published crate version of big-code-analysis-cli. A floating install surfaces metric-counting changes as unexplained CI flakes. Pin to a version whose CLI-artifact schemas (baseline, thresholds) match the files your repo commits; see the schema-compatibility note in the install section.
  • Use the default CPU-based parallelism unless memory requires fewer workers. --jobs defaults to the effective CPU count and honors available_parallelism() (cgroup-, cpuset- and quota-aware on Linux, OS CPU count on macOS/Windows), so CI runners need not thread --jobs "$(nproc)" through every recipe. --jobs 1 remains a debugging knob, not a default. It is not memory-aware. Each worker holds one whole file at a time and a parse-only run measures 25–70× the source size, so the in-flight term is roughly jobs × 70 × the largest source file in the tree, and --jobs bounds that term only. The destination decides the rest: --output-dir writes each document as it is produced and bca check reduces each file to its violations, so both stay at the in-flight bound. metrics --output <FILE> collects every file’s result and serializes it after the walk, and structured stdout holds finished documents while an earlier file is still being analyzed, which is worst when one large file sorts early. A memory-limited container with many vCPUs should pass an explicit --jobs, and prefer --output-dir to --output <FILE> on a large tree.
  • bca report markdown には常に --strip-prefix "$PWD/" を渡してください。 そうすればパス列は、ワークスペースパスが異なるランナー間でも同一になります。これがないと、2 つのレポートの差分は /home/runner/work/... と /builds/group/project/... のノイズに支配されます。
  • bca.toml はリポジトリルートに置いてください — Cargo.toml / pyproject.toml / package.json と同じ場所です。bca はこれを自動的に発見するため、引数なしの bca check がコミット済みのしきい値、パス、ベースラインを読み込みます。これをソースコードとして扱い、しきい値の緩和はコードレビューで審査してください。
  • Handle the exit-code contract. bca check exits 0 clean, 2 on any threshold violation, 1 on tool error (bad config, unknown metric, unreadable path). Reserving 1 for tool errors lets CI distinguish “a function got too complex” from “the analyzer crashed”. Pass --exit-codes=tiered (or set [check] exit_codes = "tiered" in bca.toml) to split the violation case 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. Every fail-state remains non-zero, so exit != 0 → fail wrappers keep working; only tooling that tests $? -eq 2 explicitly needs to widen to 2-5.
  • ソース内の抑制マーカーを尊重し、監査には --no-suppress を使ってください。 デフォルトの bca check は bca: suppress / bca: suppress-file マーカーを尊重します。--no-suppress を渡すとそれらを無視するため、監査者は生の違反者リストを確認できます。

ゲートの信頼境界

プルリクエストのゲートは、その作成者が管理するコードを分析します。そして「ゲートがどのファイルを見るか」を決める入力のうち 2 つが、その同じブランチの中にあります。生成コード検出器が判定するファイル内容と、ウォーカーが解釈する .gitignore ファイルです。どちらの既定値も、信頼できるチェックアウトの分析には適切です。しかし PR ゲートでは、レビュー対象の変更が引けるレバーになります。bca check は既定でどちらも報告し(後述のサマリー)、--strict の下ではどちらも無効にします。

生成コードマーカーはファイルをゲートから外す

f() が cyclomatic=2 を超える C ファイルを、bca check --no-config --paths . --threshold cyclomatic=2 で検査した場合:

状態終了コード
ベースライン2(違反が報告される)
1 行目に // @generated を追加0
同じファイルで --no-skip-generated を指定2
マーカーを再び削除2

コメント 1 行でファイルがゲートから外れます。認識されるマーカー語句はどれでも有効で(@generated、DO NOT EDIT、GENERATED CODE、大文字小文字を区別しない)、検出器は 1 行目だけでなく先頭の約 50 行 / 5 KiB を走査します。差分レビューの際に知っておくべき形が 2 つあります。

  • // DO NOT EDIT, see docs/schema.md before changing は日常的な整備コメントに見え、指摘されずにレビューを通過します。
  • 5 行のライセンスヘッダの 4 行目に埋め込まれた @generated もマッチします。不審な 1 行目を探すレビュアーはこれを見つけられません。

コミットされた .gitignore はファイルをウォークから外す

同じゲートで、違反ファイルに無害な兄弟ファイルを 1 つ添えた場合:

状態終了コード
違反ファイル + 無害な兄弟ファイル2
+ violator.c を含む .gitignore0
同じ構成で --no-ignore を指定2

ウォーカーは走査対象ツリー内で見つかった ignore ファイルを解釈するため、テスト対象のブランチにコミットされた .gitignore は検査対象のファイル集合を狭めます。1 つ注意があります。無視されたファイルが唯一の分析可能な入力だった場合、実行は 0 ではなく 1(“no input files matched”)で終了します。つまりクリーンなバイパスには、少なくとも 1 つ他のファイルが残っている必要があります。

ゲートはスキップしたものを報告する

bca check は、いずれかの仕組みがファイルを除外した場合に 1 行のサマリーを stderr に出力するため、どちらのバイパスも無音ではありません。

bca: 2 files not checked (1 generated, 1 ignored) — pass --report-skipped to list them

クリーンな実行では何も出力されません。--report-skipped はファイルごとに note: skipped (generated): <path> / note: skipped (ignored): <path> の行を追加します。サマリーは終了コードを変えません。バイパスされた実行は依然として 0 で終了し、その事実を表示するだけです。

The ignored count covers analyzable files at the directories the walk entered. A whole directory dropped by an ignore rule (a build tree, or an attacker ignoring a source directory outright) is never entered and appears only under --report-skipped, as its own clause (3 ignored directories not walked), because ignored build trees are present in nearly every checkout and would otherwise make the default summary permanent noise. A PR gate that must see whole-directory drops should pass --report-skipped or, better, --strict. See the check page for the exact rules.

--strict は両方の仕組みを無効にする

信頼できない入力に対するゲートでは、stderr を監査するのではなく、両方の仕組みを無効にしてください。

- name: Threshold gate (PR)
  run: bca check --strict --paths src/

--strict は --no-skip-generated --no-ignore と等価です。2 つの構成フラグのうち片方だけを覚えているレシピにはもう片方のバイパスが残るため、単一のフラグを選んでください。プロジェクトは各ワークフローを編集する代わりに、bca.toml の [check] に strict = true を書いて一度だけオプトインします。本当に生成されたツリーは、提出者が管理するコンテンツ判定ではなく、レビュアーが管理する明示的な deny-set(コミット済みの .bcaignore パターン)に置くべきです。

ベースライン: 既存コードでしきい値をラチェットする

Introducing thresholds on an existing codebase can flag violations unrelated to the current change. A baseline records accepted violations so the gate fails on new offenders and regressions, and shrinks as the team pays the recorded debt down. This recipe walks through adopting one, reading the gate output it produces, and keeping it fresh; the flag reference is on the check page.

Baselines complement in-source suppression markers; they are not a substitute. Use suppression markers when a function is intentionally complex forever (a parser, a state machine, generated code). Use a baseline when the team intends to pay the debt down. Both can live in the same repo; suppression is checked first.

エンドツーエンドの採用フロー

ワンショットのショートカット: 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 at the repo root, discovered automatically by `bca check`.
paths = ["src"]

[check]
baseline = ".bca-baseline.toml"

[thresholds]
cyclomatic = 15
cognitive = 20
"loc.lloc" = 200

2. ベースラインをブートストラップする

bca check --write-baseline

A bare --write-baseline (no path) writes to the baseline key from the bca.toml you just created, so the filename lives in exactly one place. Pass an explicit path (--write-baseline <file>) only when you have no manifest baseline to default to; without one, the bare form errors rather than guessing a filename.

両方のファイルを同じ変更でコミットします:

git add bca.toml .bca-baseline.toml
git commit -m "ci: introduce metric thresholds with baseline"

Path keys in the baseline are stored relative to the baseline file’s own directory (the anchor). --paths ., --paths src/, and --paths "$PWD" produce byte-identical baselines, and --baseline runs match whichever --paths form CI uses. Switch between them freely without re-running --write-baseline.

3. CI ゲートを組み込む

GitHub Actions:

- name: Check code complexity thresholds
  run: |
    bca check
  # `paths`、しきい値、`baseline` はすべてリポジトリルートで
  # 自動発見された `bca.toml` マニフェストから取得されます。

GitLab CI/CD (job excerpt):

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 が現れるのは実際の違反箇所が変わったときだけです。

You do not have to guess when a refresh is due. A gated run whose offenders have measured better than their recorded values says so on stderr (see the stale-entry warning), because the difference between recorded and live is suppression nobody chose.

制限を引き締める前に、両方の層でコストを見積もる

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 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 レビューのヒューリスティクス

Run bca diff-baseline <old> <new> and read the summary instead of parsing a raw git diff .bca-baseline.toml in your head. It pairs entries on their (path, qualified, metric) identity, so a function that merely drifted up or down the file is not reported as a remove + add, and buckets every real change:

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 (baseline grew). Someone added a new offender to the file intentionally. Review the values: was this a deliberate stopgap, or did the author bypass the gate? Either is fine if conscious; the point of committing the file is to make the choice reviewable.
  • worsened (an entry got a higher value). The author re-ran --write-baseline after the function got worse. Treat it the same as added and surface the change in review.
  • improved. A recorded offender got better without dropping out of the baseline. That is a good sign the refactor is working, and the reason to land the refreshed file rather than leave it. Until it is refreshed, the old entry keeps suppressing the offender up to a value the tree no longer produces.

For a PR bot, bca diff-baseline <old> <new> --format markdown emits a fenced block ready to drop into a sticky comment, and the --worsened-only / --added-only filters narrow it to just the regressions reviewers must look at. --format json feeds the same diff to other tooling. The command exits 0 by default, because it informs review rather than gating (the gate is bca check itself); the opt-in --exit-code flag exits 2 on a non-empty filtered diff.

ゲート出力の読み方

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]: no baseline entry matched this violation by qualified symbol (within the line tolerance) or, when --baseline-fuzzy-match is set, by body hash. The violation is new since the baseline was written. See Matching for the resolution order.

  • [regr …]: the baseline contains a recorded value and the current value is worse than it. “Worse” is measured in the metric’s own direction, so for the lower-is-worse mi.* family a drop below the recorded value reads + like any other regression. The amount takes one of two shapes, both starting [regr , so rg '\[regr ' matches every regressed row:

    • [regr +N%] for a count (every metric outside mi.*): N is the rise as a percentage of the recorded value, rounded to an integer.

    • [regr +N.N pts] for mi.original, mi.sei, and mi.visual_studio: N.N is the drop in index points, to one decimal. The Maintainability Index has an arbitrary zero and mi.original and mi.sei go negative on large functions, so a percentage of the record means nothing: a drop from −10 to −20 reads [regr +10.0 pts], and a 0.01-point drop from a record of 0.001 reads [regr +0.0 pts] rather than +1069%. Special forms:

    • [regr from 0] when a count’s recorded value is 0.0 and a percentage would divide by zero. An mi.* record of zero is an ordinary point and reads in points.

    • [regr +>9999%] caps once a count’s regression exceeds 100× the baseline value. Points are not capped.

    • [regr NaN] — 現在のメトリクス値が NaN の場合(自明な関数における退化した Halstead 入力)。

タグは --baseline を渡したときにのみ表示されます。渡さない場合、行のフォーマットはベースラインなしのデフォルトとバイト単位で同一です。統合されたストリームを読む CI ツールは、--no-summary で末尾のサマリーを抑制できます。

The summary footer groups violations by file, cites the single worst metric per file (by severity: value / limit for a count, one step per maintainability band below the floor for mi.*), and sorts rows by violation count descending then path ascending. Use the footer to identify files with the most violations before reading individual offender rows.

2 つのブランチのマージ

ベースラインは丸ごと生成されるものなので、双方が触れた 2 つのブランチの「テキスト上の」マージが正しくなることはありません。各側の値はそれぞれ自分のツリーに対して測定されたものであり、マージ後のツリーはどちらにも一致しません。.gitattributes の 1 行で、git に試みるなと伝えてください:

.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

merge=ours ドライバーと異なり、-merge はクローンごとの git config を必要としないため、設定方法をすでに知っていた人だけでなく、リポジトリをクローンする全員に対して機能します。

6. ベースラインを退役させる

.bca-baseline.toml が version = 6 のみでエントリを含まなくなったら、CI から --baseline フラグを外してファイルを削除します。以降はしきい値だけで成立します。

ティア/ヘッドルームの来歴

--write-baseline(v5 以降)で書き込まれたベースラインは、「どのゲートに対して書き込まれたか」を [provenance] テーブルに記録します:

version = 6

[provenance]
tier = "soft"
headroom = 0.95
mi_charges_docs = false
mi_formula = 2
  • tier = "hard": written by the hard gate (bca check --write-baseline …); no headroom key.
  • tier = "soft", headroom = <ratio>: written by the soft gate scaled by the soft ratio (bca check --tier=soft=0.95 --write-baseline …).
  • tier = "soft" with no headroom: written by the soft gate driven by a [thresholds.soft] table (per-metric limits, no single ratio).
  • mi_charges_docs: whether the maintainability index charged documentation rows when the file was written (the --mi-charges-docs flag or manifest key; see Charging documentation). Absent from a file written before the key existed, which reads as true: every earlier bca charged documentation.
  • mi_formula: the revision of the maintainability-index formula the file’s mi.* values were computed under (2 since the size term moved to PLOC and mi.sei to a 0–1 comment ratio). Absent from a file written before the key existed, which reads as revision 1.

この来歴はコメントではなく本物の 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, …

これはベースライン更新の規律が防ごうとしているサイレントな不整合です。現在のゲートより「緩く」書き込まれたベースラインは、より厳しいゲートが検出するすべての違反を網羅していない可能性があり、誰も触れていないファイルで突然ゲートが発火することがあります。

The warning is directional. It fires only when the current run is stricter. It stays silent in the safe direction: a hard check (strictness 1.0) reading a soft-0.95 baseline sees a superset of its offenders, which is the intended single-baseline setup where make self-scan (hard) and make self-scan-headroom (soft) ratchet through the same .bca-baseline.toml. It also stays silent for equal provenance, for pre-v5 baselines (provenance unknown), and when either side is a [thresholds.soft]-table baseline (no single ratio to compare). To clear a real warning, refresh the baseline at the current tier with the matching --write-baseline recipe.

The MI formula-revision warning

A baseline’s mi.* values are only comparable with a run that computes MI the same way. When the formula itself changes, every recorded value moves for unchanged code, so bca check warns whenever a baseline holding any mi.* entry records a different mi_formula revision from the one the run computes, in either direction:

warning: this baseline records mi.* values under MI formula revision 1;
this run computes revision 2. The recorded values are not comparable
and can misclassify a regression. Review any mi.* threshold and refresh
with `--write-baseline <file>`.

It does not wait for an mi.* offender: it names the stale file on every read, so the record is refreshed before an mi.* gate comes to depend on it. A file written before the key existed reads as revision 1, so the first check after upgrading warns. While it fires, the documentation-mode warning below is not shown: a baseline from another formula is not worth comparing mode by mode. Refresh it with --write-baseline. bca diff-baseline warns the same way when its two files record different revisions and either holds an mi.* entry, since their mi.* deltas then reflect the formula, not the code.

The MI documentation-mode warning

The mi.sei values a baseline records depend on whether documentation rows were charged; mi.original and mi.visual_studio do not. Including documentation rows raises mi.sei. A baseline recorded with documentation included can therefore reject unchanged code when the current run excludes documentation. Reversing the modes can hide a regression. bca check warns in either direction:

warning: this check computes mi.* leaving documentation out
(mi_charges_docs = false) but the baseline was written charging
documentation (mi_charges_docs = true); its mi.sei entries describe a
different comment share (mi.original and mi.visual_studio read the
same in both modes), …

A baseline that records no mode was written by a bca that charged documentation, so it reads as mi_charges_docs = true. Every such file a release wrote also predates the formula stamp, so after upgrading the formula-revision warning above fires instead. The documentation-mode warning stays silent when the modes agree and when no mi.sei value is read against the baseline: a baseline holding no mi.sei entry, or a run with no mi.sei offender, is unaffected by the mode. Clear it by refreshing under the current mode, or by passing the --mi-charges-docs=<bool> the message names to match the baseline.

The stale-entry warning

The ratchet suppresses a violation for as long as it has not worsened past its recorded value, so everything on the improving side classifies alike: a function measuring 5 against a recorded 7 is treated exactly like one still measuring 7. The two points between them are gate headroom nobody chose; the function can grow back to 7 and the gate will not notice.

A gated run reports that drift in one stderr line:

warning: 3 baseline entries improved past the recorded value (worst:
src/spaces/compute.rs::metrics_inner halstead.effort 119147.75 →
116715.61); that gap is gate headroom nobody chose, so refresh with
`--write-baseline`. …

It names one example rather than every entry, because the response to any number of them is the same wholesale refresh. Direction follows the metric: for the lower-is-worse mi.* family the stale direction is a rise above the record. An entry sitting exactly on its recorded value is silent, so a freshly written baseline never warns about itself.

It finds only half of the staleness. A function that stopped breaching its limit altogether produces no violation at all, so it never reaches the baseline matcher and cannot be counted here. Its entry stays in the file, inert and invisible, until someone regenerates.

This repository closes the other half with a scheduled job rather than a gate, and the shape is worth copying. baseline-freshness.yml runs quarterly. It regenerates the baseline with the same --write-baseline recipe that wrote the committed one (the job pins that recipe, so a tier change has to land in both places), diffs the two with bca diff-baseline --exit-code, and files a labelled issue naming what moved. Two details of the job carry its design:

  • It uses diff-baseline rather than git diff, because git diff reports a moved start_line as staleness.
  • It looks up an existing issue by label and title before filing, because a stale baseline persists until someone commits a refresh, so an unconditional file would open one issue per quarter for the same entry. The label alone is shared with other findings; the fixed title is what keeps the comment on the right issue.

Nothing is gated on the result: the tree is fine, the file describing it has aged.

マッチングの仕組み

Each entry is keyed on (path, qualified_symbol, metric). The qualified symbol is the ::-joined chain of enclosing named containers plus the function name (MyStruct::do_thing, my_namespace::MyClass::method). The top-level file space collapses to <file>. A violation is resolved against the baseline in this order:

  1. Qualified symbol. If exactly one entry shares the violation’s (path, qualified_symbol, metric), it matches regardless of line number, so editing code above a function does not re-key it as [new].

  2. Start-line tolerance. If several entries share that key (two methods named is_valid on different impl blocks the analyzer could not tell apart, overloads, …), the entry whose recorded start_line is closest to the violation, and within --baseline-line-tolerance lines (default 50), wins. Beyond the tolerance the violation is [new].

    This is the only rule that reads a line number, so from schema v6 a start_line is 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. That is what keeps a real baseline change (a value moving) visible in a diff instead of buried in line-number noise.

  3. ボディハッシュ(オプトイン)。 --baseline-fuzzy-match を指定すると、修飾シンボルがもはや一致しない違反は、同じ (path, metric) 内で正規化されたボディハッシュが同一のエントリと照合されます。これにより、関数の形を保ったままのリネームを吸収できます(ダイジェストは関数自身の名前を除外し、インデント・空行・CRLF の影響を受けません)。ハッシュは --baseline-fuzzy-match が設定されている場合にのみベースラインに書き込まれるため、ファジー読み取りを有効にするには一度ファジーな --write-baseline でシードしてください。両方のキーは bca.toml の [check] の下に baseline_line_tolerance および baseline_fuzzy_match として設定します(トップレベルに直接書く綴りは非推奨で警告が出ます。#599 を参照)。

Anonymous functions (closures, lambdas) have no stable name, so their qualified symbol bakes in the line (outer::<anon@L42>). They therefore re-key as [new] when they move. Symbol matching survives line drift only for named top-level and method-bound functions, which produce the bulk of baseline churn.

修復フッター

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. The block 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.

下流のツールが統合されたストリームを読んでいて末尾のブロックが混乱を招く場合は、--no-remediation でブロックを抑制します。

抑制マーカーとの組み合わせ

--write-baseline already excludes any function silenced by a bca: suppress or #lizard forgives marker, so the same function does not end up in two places. If a function is intentionally exempt forever, prefer the in-source marker: it lives next to the code, survives refactors, and needs no extra file to commit. Use the baseline only for violations the team intends to fix.

To audit the un-filtered offender set (every violation regardless of suppression or baseline), pass --no-suppress and omit --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 portability. Paths are normalized to forward slashes on write and re-normalized on read, so a baseline generated on Linux matches the same tree on Windows. On Unix and Windows the path key is percent-encoded, so a non-UTF-8 path keeps its identity rather than collapsing onto a lossy display form; the backslash-to-slash normalization is the one deliberate exception.
  • Tightening a threshold. Lowering a limit may newly expose functions that were previously clean. They will not be in the baseline, so CI will fail. This is correct: tightening should expose new offenders. Refresh the baseline if the team chooses to absorb the new entries.

ローカルしきい値ゲート

Run the threshold gate before committing to catch complexity regressions before CI. This recipe runs the same checker and configuration (the repo-root bca.toml manifest and its .bca-baseline.toml) locally and in CI, with an additional local tier for values approaching their limits. The local gate fires at git commit, alongside cargo fmt --check and cargo clippy -- -D warnings, while the author still has the change in hand.

This recipe captures the pattern big-code-analysis uses on its own source (Makefile’s self-scan* targets, backed by a consolidated bca.toml manifest) and distils it into something you can drop into your own repo’s Makefile, justfile, package.json script, or pre-commit config. The idea is provider-neutral: any threshold checker (bca, ESLint, clippy, SonarLint, Qodana) can be wired the same way.

原則

設計を駆動するのは 3 つの原則です。これらは bca に固有のものではありません。Sonar がデフォルトの品質ゲートを新しいコードに焦点を当てる方向へ転換したときに到達したのと同じ結論であり、より広いラチェットパターンが定式化しているものです。

  1. Gate locally, mirror CI exactly. The local gate must run the same binary with the same arguments and the same threshold, baseline, and exclude files as CI. If the local gate is “almost what CI runs”, it stops catching regressions the moment one diverges from the other. Running the gate once before pushing is cheap; a red PR-bot ping is not.
  2. Ratchet, do not reset. When you introduce thresholds on an existing codebase, every reasonable limit fires on dozens of pre-existing functions. The realistic adoption path is “absorb today’s offenders into a baseline file, fail only on new or worsening ones, shrink the baseline over time”. This is the same strategy that lets a multi-year codebase introduce strict TypeScript or strict clippy lints without a months-long boil-the-ocean pass. See the Baselines recipe for the bootstrap, CI, refresh, and retire flow.
  3. Warn before you fail. A hard 100% gate fails only once a value exceeds the limit and gives no signal as a function creeps from 80% to 95% to 99% of its threshold. A stricter tier reports values approaching the hard limit. The author still has the file open, the test cases in their head, and the freedom to refactor before the offender hardens into “well, it’s in main now”. Sonar’s “new code” Quality Gate, the GCC -Wall / -Werror split, and clippy’s warn vs. deny lint levels all encode the same insight: a tier between clean and broken is where teams catch drift.

2 つのティア

このパターンは、同じチェッカーをラップする 2 つのレシピと、各ティアでベースラインを更新するための 2 つのレシピから成ります。

ターゲットティアしきい値ベースラインでフィルタユースケース
self-scanハード設定の 100%はいCI のミラー。すべてのコミットでグリーンを維持しなければなりません。
self-scan-headroomソフトHEADROOM で引き締めはい早期警告バンド。ハードティアより先に発火します。
self-scan-write-baselineハード設定の 100%(書き込み)今日のハードティア違反を吸収します。
self-scan-write-baseline-headroomソフトHEADROOM で引き締め(書き込み)バンドの導入時や拡大時にソフトティアの違反を吸収します。

This repository’s own Makefile adds two checks on the baseline file rather than on the code: self-scan-baseline-fresh regenerates to a temporary file and runs bca diff-baseline against the committed one (exit 2 when the file no longer describes the tree — the quarterly freshness job’s question, answerable locally), and self-scan-profile-parity regenerates under both cargo profiles and cmps the results, which is what lets the repo build its gate binary under the fast dev profile. Neither belongs in a generic adopter skeleton; an installed bca has no profile to vary.

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. The ratio tightens 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 が自分のほうが厳しいゲートだと警告するようになります。ティア/ヘッドルームの来歴を参照してください。

A contributor who wants the soft tier to be stricter (to catch encroachment further out) therefore bumps a single environment variable rather than maintaining a parallel soft-threshold file, which drifts out of sync with the hard config the first time anyone forgets to update both.

2 ティアのしきい値

bca check --tier <hard|soft|soft=RATIO> selects which tier to gate against. hard (the default) compares against the configured limits verbatim. soft is the early-warning tier, derived from the limits the file’s tables resolved to — the project table, its language table and any matching path entry, in the order How the layers compose states once. If a [thresholds.soft] table exists its overrides are merged on top, and metrics absent from it inherit their hard limit (no soft band); when a soft table is present the blanket RATIO does not apply, because explicit per-metric limits win over the scalar. Otherwise every limit is tightened by the soft RATIO (default 0.95 for a bare soft, so --tier=soft is never a silent no-op; soft=1.0 disables scaling). Repeated --threshold name=value flags apply last, absolutely.

A bare --tier=soft (ratio 0.95) is the zero-config entry point. Use [thresholds.soft] when metrics need different warning bands. The table records each soft limit beside the hard limits:

[thresholds]
cognitive  = 25
cyclomatic = 15
nargs      = 7

[thresholds.soft]
cognitive  = 22       # 絶対値のソフト制限
cyclomatic = "0.9x"   # ハード制限の 90% → 13.5
# nargs は未指定 → ソフト層はハード制限を継承(ソフト帯なし)

Soft limits with integer types read more cleanly as absolute values than as float-scaled ones: prefer nargs = 6 (for a hard nargs = 7) over the 0.95 × 7 = 6.65 a scalar would produce. Use the "<ratio>x" form for the large-valued metrics (halstead.*, loc.*) where an exact integer soft limit is fussy to pick. The scale factor must be in (0, 1]: the soft tier is an early-warning band that fires before the hard gate, never looser than it.

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), and the index has no true zero, so RATIO raises it by 1 − RATIO of one maintainability band (17.1 points of mi.original) instead: "mi.original" = 20 under --tier=soft=0.9 resolves to 21.71, not 18. The intuition that a soft band is “90% of the limit” is right for every other metric and exactly backwards here.

Both tiers ratchet through the same .bca-baseline.toml (no separate soft baseline file). bca check --print-effective-config --tier=soft prints the resolved limits; paste its [thresholds] output into [thresholds.soft] to migrate from a blanket-ratio band to explicit per-metric limits.

ゼロコンフィグ: bca.toml マニフェスト

すべてのレシピに --paths、--exclude-from、--jobs、--config、--baseline、--tier=soft=<ratio> を渡して回る代わりに、リポジトリルートに bca.toml を置き、bca check に発見させます:

# bca.toml, discovered automatically at (or above) the working dir.
paths        = ["."]
exclude_from = ".bcaignore"
jobs         = "auto"          # or an integer (was `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

The headroom key is the soft-tier scale ratio: it only takes effect under --tier=soft, so a bare bca check (hard tier) stays the exact CI mirror regardless of any headroom key. For per-metric soft limits, prefer a [thresholds.soft] table over the scalar; it records the band you chose next to the hard limit instead of leaving it to a runtime multiplier.

このファイルを置けば、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 でも正しいファイルを指します。
  • Scalars and positive scope keys: CLI wins. Any explicit --baseline, --tier, --jobs, and so on overrides the corresponding manifest key, and the positive scope list keys (paths, include) are replaced by any explicit CLI value (bca check one.rs with manifest paths = ["src"] checks just one.rs). --config <file> merges on top of the manifest [thresholds] table (config keys win on collision), and repeated --threshold name=value flags apply last as absolute limits. The full order, including the per-language and per-path layers, is stated once in How the layers compose; it is shared across --config, --tier, and the manifest.
  • Negative filter keys: CLI unions with the manifest. The exclude list keys (top-level exclude, [check] exclude) are merged, not replaced: a CLI --exclude / --check-exclude is added to the manifest’s deny-set. A command-line filter can therefore never silently un-exclude a directory the project config deliberately skipped (such as vendor/). Duplicates across the two sources collapse; CLI patterns sort first. This mirrors ruff and ESLint’s exclude (replace) vs extend-exclude (add), generalized: targets replace, filters add. --x and --x-from union with each other regardless. Reach for --no-config when you need the manifest excludes gone entirely.
  • --no-config skips discovery entirely, for reproducible fully-explicit invocations that must not pick up repo-level config. bca init also ignores any existing manifest; it scaffolds config rather than consuming it.
  • トップレベルの 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] の値が勝ちます。
  • A [vcs] table sets change-history ranking options for bca vcs. Its file_types key ("metrics", the default, or "all", or a "rs,py"-style extension list) scopes which files are ranked; as a positive scope key, an explicit --file-types CLI flag replaces it (see bca vcs file-type scope).
  • cyclomatic_count_try and exclude_tests are walker-tuning bools that mirror the --cyclomatic-count-try / --exclude-tests flags. exclude_tests = true prunes Rust inline-test subtrees (#[test], #[cfg(test)], …) before metric computation. Both are Rust-only and inert for other grammars. --exclude-tests is presence-only (no =false form), so its manifest key can only turn pruning on: a CLI --exclude-tests wins, but the manifest cannot turn off a key the CLI did not set.
  • mi_charges_docs mirrors --mi-charges-docs: true has mi.sei’s comment share count documentation rows as comments, which can only raise mi.sei; mi.original and mi.visual_studio read the same either way. Off by default, merged like cyclomatic_count_try (the CLI value wins in either direction). A baseline records the mode it was written under and bca check warns when it is read under the other; see Charging documentation.
  • [thresholds.soft] テーブルは、メトリクスごとのソフトティア制限を設定します(--tier=soft によって消費されます。2 ティアのしきい値を参照)。認識されないキーは 1 行の警告とともに無視されるため、古い bca ビルドを壊すことなく、今後のスキーマ追加を先行採用できます。
  • bca check --print-effective-config は、manifest の来歴行を含む解決済みのビューを出力するので、マージが何を生成したかを正確に確認できます。

The explicit-flag skeletons below remain fully supported; the manifest is sugar over the same flags, not a replacement. Reach for them when you cannot drop a file at the repo root, or when one CI job needs a different layout than the committed manifest (pair the flags with --no-config).

スケルトン: GNU Make(明示フラグ)

The four recipes below are a self-contained drop-in that thread every flag explicitly, the long form of the manifest recipe above. Adjust the BCA variable to point at whatever invocation gives you the checker (a pinned release binary, cargo run --release, an npm or pip wrapper). Adjust PATHS and EXCLUDE_FROM to match your layout.

# --- 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)

The soft tier tightens limits from --config, then applies the shared baseline. It multiplies count ceilings by the ratio (default 0.95 for a bare --tier=soft) and raises mi.* floors by 1 − ratio of one maintainability band. Explicit --threshold values apply last and are not scaled. There is no separate helper script or second TOML file to maintain; the soft tier is the hard-tier invocation plus one flag.

終了コード

The gate exit codes propagate verbatim from bca check: 0 clean, 2 on any threshold violation (hard or soft), 1 on tool error. The soft tier is a real gate. Never wrap make self-scan-headroom in || true thinking it is advisory; the non-zero exit is the whole point of the encroachment band.

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 …] row tags.

pre-commit と CI への組み込み

Add the soft gate to whatever umbrella target your developers already run before pushing. The hard gate runs as its prerequisite (see the self-scan-headroom: self-scan edge above), so listing only the soft target is enough. That edge also survives make -j, which would otherwise schedule both leaves in parallel and interleave their output:

.PHONY: pre-commit
pre-commit: fmt-check clippy test self-scan-headroom

順序が重要です。ハードティアは、スケーリング後ではなく 100% の限度に対する真のリグレッションを指摘します。前提条件のエッジは、並列 Make の下でもその順序を強制します。

CI では、ハードティアのみを実行します:

- name: Threshold gate
  run: make self-scan

The soft tier is a developer feedback knob, not a release gate. Running it in CI either duplicates the hard tier (when nothing has encroached) or fires noisily on a baseline-absorbed offender that crept upward without crossing 100%. Neither buys you anything CI does not already cover.

ヘッドルームノブ

BCA_HEADROOM は (0, 1] の範囲の単一のスカラー値です。意味のあるバンドは狭い範囲に限られます:

BCA_HEADROOM関数が次に達すると発火…ユースケース
0.9999% of a count ceilingNarrow warning band.
0.9595% of a count ceiling (default)Default warning band.
0.9090% of a count ceilingWider band, useful immediately after raising a limit, while the new ceiling settles.
1.00100%(ハードゲートと同等)2 つのティアが一致していることを確認するサニティチェックです。

An mi.* floor is raised by 1 − BCA_HEADROOM of one band instead of scaled, as Two-tier thresholds explains.

およそ 0.80 を下回る値では、ソフトティアは恣意的な数値による第 2 のハードティアと化し、有用でなくなります。現実のコードベースでは、どのしきい値にもその 80% 付近にある関数が 何かしら 存在するため、ソフトティアは早期警告シグナルではなく、恒常的なベースライン管理の雑務になってしまいます。

ソフトティアが発火したとき

A failed soft gate is a decision point, not a bug report. There are three legitimate resolutions:

  1. Refactor. Same workflow as any other complexity regression: extract a helper, collapse a dispatch arm, split the function. This is the common case, and the soft tier exists to give you the time to do it on the same branch.
  2. Raise the limit. Edit the [thresholds] table (in bca.toml for this repo, or your own threshold file) and leave a why-comment explaining what changed (a new language module, a real algorithmic floor, a re-classified macro). Re-run make self-scan-headroom to confirm the new value covers the offender with room to spare.
  3. Absorb into the baseline. Run make self-scan-write-baseline (hard tier) or make self-scan-write-baseline-headroom (soft tier) when the value is legitimate forever: a parser dispatch arm whose width matches the grammar it covers, a stable state machine, generated code. Commit the diff in .bca-baseline.toml in the same PR as the code that produced it.

Do not pick “raise the limit” silently to make the gate go away. The committed why-comment is the only audit trail the next reader has; without it the bumped limit looks indistinguishable from neglect.

スケルトン:justfile

just を好むプロジェクトの場合:

# bca local threshold gates. Hard tier mirrors CI; soft tier (headroom)
# is local-only early warning.
bca         := "bca"
paths       := "."
exclude     := ".bcaignore"
thresholds  := "thresholds.toml"
baseline    := ".bca-baseline.toml"
headroom    := env_var_or_default("BCA_HEADROOM", "0.95")

# `--jobs` defaults to the effective CPU count, so the skeleton
# no longer threads `$(nproc)` through `just`. Override
# inline if needed: `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}}

# Like the Make skeleton, never compose this with `self-scan-write-baseline`
# in parallel; they race on the same {{baseline}} file.
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"
  }
}

Because the soft tier is a plain bca check invocation, the npm scripts are byte-identical across shells: no helper script, no python3-vs-py alias to paper over, no env-var-vs-shell-expansion portability traps. To widen the band, edit the literal 0.95 in the script (or wire it through your task runner of choice); the flag parses the same on every platform.

husky や pre-commit と組み合わせて、同じスクリプトが git commit 時に実行されるようにしてください。

スケルトン:pre-commit フック

If you use the pre-commit framework (version 3.2.0 or newer; see the version note below), both tiers are local hooks that shell out to 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 is deliberate: bca discovers its own inputs from --paths plus the baseline. Letting pre-commit pass the changed files in would shrink the scan to just those files and miss the cross-file effect of a baseline refresh.

Minimum pre-commit version 3.2.0. The stages: vocabulary was renamed in pre-commit 3.2.0 (2023-03-17): commit became pre-commit, push became pre-push, and so on. Older installs (notably RHEL 8 EPEL, Ubuntu 20.04 default packages, and any .pre-commit-config.yaml pinned to the legacy vocabulary) reject stages: [pre-commit] as an unknown stage name and the hook never registers. If you must support older installations, substitute stages: [commit]; in mixed fleets, pin the framework with pre-commit --version ≥ 3.2.0 in the dev-tooling docs so this contradiction does not surface silently.

より広いベースラインワークフローとの組み合わせ

The four self-scan* targets above are not a replacement for the documented Baselines recipe; they are that recipe, mechanised into developer-machine commands. The same ordering still applies:

  1. 最初に一度ブートストラップする。 初期のしきい値を書き、初期のベースラインを書き、両方をコミットします。
  2. コミットごとにゲートする。 ハードティアはリグレッションで失敗し、ソフトティアは限度への接近で失敗します。
  3. 集中的なリファクタリング中に更新する。 関数の値が正当に動いた(誰かが 実際に 負債を返済した)場合は、ベースラインを再生成して差分をレビューします。
  4. 空になったら退役させる。 .bca-baseline.toml が version = 6 だけ(違反エントリのない素のスキーマスタンプ)に縮んだら、--baseline フラグを外してファイルを削除します。以後はしきい値が単独で機能します。

関連する業界パターン

ハード / ソフトのティア分割は、より広いパターンの一例です。以下のいずれかを使ったことがあれば、そのメンタルモデルがそのまま通用します:

  • Sonar の新規コードに焦点を当てた品質ゲート。 既存コードは現状のまま維持され、変更 が事態を悪化させてはならない、という考え方です。ベースラインファイルは、「新規コード」/「リーク期間」という発想の bca ネイティブな形です。
  • clippy の warn-vs-deny リントレベル。 warn リントはローカルビルドで表面化し、同じリントを -D warnings で deny すると CI が失敗します。2 段構えの設定は、実験的により厳しいルールを導入する場所を与えてくれます。
  • 一般的な移行ツーリングにおける ラチェットパターン。今日のカウントを記録し、増加で失敗させ、カウントが減るにつれ上限を下げていきます。bca check はパターン単位ではなく関数単位でラチェットしますが、単調性の保証は同じです。
  • -Wall + -Werror in C/C++. A first pass with -Wall reveals the noise; promoting to -Werror after the baseline reaches zero is the same retirement step as deleting .bca-baseline.toml once it is empty.

しきい値の選択

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, manually or through a coding agent.

まだゲートを設定していない場合は、仕組みについてはローカルしきい値ゲートから、すでに抱えている違反者の吸収についてはベースラインから始めてください。このページは数値そのものを扱います。

出荷時のデフォルト

bca init scaffolds this table. It is defined once in big-code-analysis-cli/src/default_thresholds.rs. The anchor column cites MISRA and Flog alongside the tools linked inline.

メトリクス上限スコープアンカー
cognitive15関数SonarSource のデフォルト
cyclomatic15関数lizard のデフォルト。MISRA と NASA のセーフティクリティカル上限
abc40関数RuboCop AbcSize の 17 と、Flog の「60 以上は危険」の中間
nargs5関数RuboCop ParameterLists の 5。Code Climate はより厳しい 4
nexits5関数Code Climate return-statements の 4
halstead.effort50000関数公表値なし。パーセンタイルから導出
loc.ploc600ファイル公表値なし。パーセンタイルから導出
loc.sloc1200ファイル肥大化の歯止めであり、実働の制限ではない
nom30コンテナCode Climate method-count の 20。PMD TooManyMethods の 10
wmc60コンテナ公表値なし。パーセンタイルから導出

これらを読むときはスコープが重要です。cognitive の制限は各関数に適用されます。nom と wmc は各クラス・struct・trait・impl・名前空間・インターフェイスに適用されます。loc.* はファイル全体のルートに適用されます。bca check がコンテナのメソッド数を関数単位の制限と比較することはありません。スコープ規則の全体は Check を参照してください。

Two of these limits need their reasoning spelled out.

loc.ploc と loc.sloc は対になっています。PLOC は空行とコメントを除いた物理コード行数を数えるので、実働のファイルサイズ制限です。判断の経緯を文書化してファイルが伸びても、この制限に対するコストはゼロです。SLOC はすべてを数えるため、はるかに緩い位置に置かれ、仕事は 1 つだけです。それは、loc.ploc をクリアしたまま、コメント量によってファイルが際限なく成長するのを止めることです。ファイルサイズを SLOC だけでゲートすると、根拠の説明 1 段落にも新しい分岐 1 つと同じ代償を課すことになり、観測される結果は、人々がその根拠説明を削除することです。

cyclomatic を McCabe の 10 ではなく 15 とするのは意図的な緩和です。10 を標準的な数値として定着させた文書である NIST 500-235 は、それを正当化できるプロセスを持つチームには引き上げを認めており、15 という高さの制限も成功裏に使われてきたと述べています。実際のコードで測定すると、10 は 15 のおよそ 2 倍の関数をフラグしますが、レビュアーが問題と呼ぶものが相応に増えるわけではありません。

デフォルト値の導出方法

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, as measured for #1140, was 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.

設計ルールは、デフォルト値が中央値の言語でおよそ最悪 1% 〜 3% のスペースをフラグすべき、というものです。それを下回ると制限は機能せず、誤った安心感を与えます。それを上回るとゲートはゲートでなくなり、人々が迂回するスタイルルールになります。これは、70・80・90 パーセンタイルでリスク帯を導出する Alves, Ypma, and Visser のベンチマークアプローチをより厳格に読み替えたものです。彼らの 90 パーセンタイルは「見る価値がある」ラインとしては有用ですが、ビルドを失敗させるにはノイズが多すぎます。

Two of the previous defaults failed that rule and changed as a result. The original corpus measurement found that nargs = 7 flagged fewer than 1% of functions in the median language, and nothing at all in this project’s own source: a limit that cannot catch anything. That measurement includes nested-closure parameters, which #1196 later removed from nargs; the current own-parameter metric cannot produce a higher count on the same inputs, and the drop is largest for closure-heavy languages, so the conclusion holds. The shipped limit is therefore stricter. 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.

言語ごとのオーバーライド

メトリクスの分布は、プロジェクトよりも言語によって大きく変わります。参照コーパスにおける関数ごとの cognitive の 90 パーセンタイル値は、C# の 0 から Tcl の 21 まで広がっています。ファイルの loc.ploc の中央値は TypeScript の 16 から C の 283 までで、18 倍の開きがあります。1 つの表で両端をカバーすることはできません。

以下の表は言語ごとに測定した 97.5 パーセンタイルを示します。これは、その言語のコードの最悪 2.5% をフラグするために制限が取るべき値です。処方箋ではなく較正データとして読んでください。数値が低いのはその言語のコードが概ね単純だということであり、引き締めることが「できる」理由ではあっても、そうすべきだという証拠ではありません。

言語cognitivecyclomaticabchalstead.effortloc.ploc
Bash35256044000(サンプル不足)
C5030601900003500
C++14102554000(サンプル不足)
C#441344000700
Elixir5720150001500
Go251745890001500
Groovy16132525000450
Java751620000800
JavaScript201535170000(サンプル不足)
Kotlin1082018000300
Lua35204082000800
Objective-C18123577000(サンプル不足)
Perl352545410001500
PHP1092526000900
Python20132542000700
Ruby76195000400
Rust882034000900
Tcl45258546000(サンプル不足)
TypeScript20132560000450
TSX141065130000400

「サンプル不足」は、コーパス内のファイル数が少なすぎてファイルサイズの数値を導出できない言語を示します。Bash、Tcl、JavaScript、Objective-C の関数ごとの数値は表の中で最も小さいサンプルに基づいており、自分のコードに対して再導出する価値が最も高いものです。

The halstead.effort column comes from a different measurement than the rest of the table. It was re-measured on 2026-10-10, after the Halstead vocabulary changes in #1486 through #1573, on a rebuilt corpus of 41 repositories that is comparable to the original but not identical to it. The C, C++, and Objective-C rows were re-measured on 2026-10-15 to include the declaration modifiers of #1578 and #1579. The repository list and commits are recorded in docs/development/threshold_corpus.md. Several rows moved by a factor of two to five, and almost all of that movement came from the change of repositories, not from the metric: the same 41 repositories measured with the pre-change release differ by at most half (Python) and mostly not at all. Treat any single row as a property of the repositories that represent the language, which is one more reason to re-derive against your own. In this column the thinnest samples are Objective-C, Perl, Bash, and C++, each under 2,500 functions.

何を変えるか、そしてなぜ

ほとんどの言語にオーバーライドは不要です。必要になるケースは 3 つのグループに分かれます。

Procedural languages with large dispatch functions. C, Tcl, Bash, Lua, Perl, and Go all run two to three times the cognitive default and above the cyclomatic one. 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 first. halstead.effort carries the rest of the excess for C, Go, and Lua; Tcl, Bash, and Perl measure close to the default on it.

単一言語のリポジトリでは、これは [thresholds] テーブル全体になります。混在リポジトリでは、対象とする言語の下に置くべきです。さもないと、他のすべての言語まで一緒に引き上げてしまいます。

# Gating C. Tcl, Bash, Lua, Perl, and Go share its cognitive and
# cyclomatic shape, but not its effort: Go and Lua measure near 90000,
# and Tcl, Bash, and Perl near the default.
[thresholds.lang.c]
cognitive = 30
cyclomatic = 20
abc = 50
"halstead.effort" = 120000
"loc.ploc" = 1200
"loc.sloc" = 2000

モジュール構成要素がクラスではない言語。 nom と wmc は Container スコープであり、コンテナとは文法がクラス、struct、trait、impl、namespace、interface と呼ぶものすべてを指します。Elixir の defmodule は設計上何十もの関数を保持するため、Elixir モジュールのおよそ 3 分の 1 が nom = 30 に違反します。これはスコープの不一致であって、コードスメルではありません。Rust は反対側の端に位置します。impl ブロックは通常わずかなメソッドしか持たないため、デフォルト値が発火することはありません。

# 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 except C#’s halstead.effort. Their 97.5th percentile cyclomatic is 4 to 8. Gating them at 15 means the gate will almost 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 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.

すべての言語に適用されるわけではないメトリクス

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 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, Lua, Perl, and Tcl output. Go is a half case: it has no container space at all, so wmc is absent and npm / npa are reported only on the file unit root. bca check gates both on container spaces and never on the root, so no npm or npa limit can fire on Go source; see Which spaces carry NPA, NPM and WMC.

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.

オーバーライドの適用

bca.toml に [thresholds.lang.<slug>] テーブルとして記述します。各テーブルはメトリクスごとにプロジェクトの [thresholds] の上に重ねられるため、言語は再指定しなかったすべての制限を継承します。

[thresholds]
cognitive = 15
cyclomatic = 15
nargs = 5
nom = 30
wmc = 60

# 手続き型でディスパッチが多い言語: 超過分を占める 2 つのメトリクスを引き上げます。
[thresholds.lang.c]
cognitive = 30
cyclomatic = 20
"halstead.effort" = 120000
"loc.ploc" = 1200

# defmodule はクラスではないため、nom と wmc にはモジュール規模の制限が必要です。
[thresholds.lang.elixir]
nom = 150
wmc = 300

# コンパクトで高度に抽象化された言語: 引き締めないとゲートは一度も発火しません。
[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 weaker, because the stricter languages stop being gated at all.

Per-path overrides

A language table tunes a limit for everything written in that language. Some populations of files are a different shape from the rest of their language, and the shape is a property of the directory, not the grammar: a tree of hand-rolled dispatch tables, a generated binding layer, a parser whose state machine is one function per state. The cyclomatic count of a dispatch table is the length of its construct list, and its Halstead effort is the size of its vocabulary; neither number tells a reader anything a split would improve, and the project limit misreads every function in the directory the same way.

The per-function answer is a suppression marker on each one, and it does not scale. This repository accumulated 38 bca: suppress markers naming cyclomatic and Halstead across its per-language dispatchers, in several wordings and two operand orders, with nothing stating that the directory was exempt as a policy. A [[thresholds.path]] entry states the policy once:

[thresholds]
cyclomatic = 15
"halstead.effort" = 50000

# The per-language dispatchers are exhaustive kind tables: their
# cyclomatic is the construct list and their effort is the vocabulary.
[[thresholds.path]]
glob = "src/metrics/{abc,cognitive,cyclomatic,loc}/*.rs"
cyclomatic = 40
"halstead.effort" = 150000

An entry is an array element, so a manifest can carry several with different globs. Each names a glob and the metrics it re-limits, and inherits every limit it does not restate. The glob is matched against the file’s path relative to the directory of the config that declared it (the bca.toml, or the --config file), with the same engine and the same anchor that [check] exclude uses, so one spelling describes the same files on both surfaces. A file outside that directory is matched by its seed-relative form, exactly as a [check] exclude glob matches it: the path under the --paths seed that produced it, so with bca check -p src an entry declared outside the tree sees src/gen/a.c as gen/a.c, and its src/** does not reach it. Where several entries match one file, the last entry wins, per metric, in declaration order — the manifest’s entries first, then the --config file’s — the rule .gitignore readers already know.

An override is not a suppression. The function stays in bca check’s output and in the baseline with its real value, and the soft tier scales the overridden limit the way it scales any other: at --tier=soft=0.9 the entry above warns at cyclomatic = 36, and the ceiling an offender is measured against for exit 5 is 40, not the project’s 15. The marker takes a function out of the gate; the entry keeps it in, at a limit chosen for its population. That is the whole difference, and it is why --explain-threshold can still price a candidate limit over those files: it lists each entry that declares the metric, with the number it declares, so the count is read against the right denominator. It reports the declaration rather than a per-entry limit because entries overlap — where two match one file, the later one’s number is the limit — and the offender counts it prints are already taken against each file’s own resolved limit.

Three things the parser refuses, each naming the entry by its position in its own file and its glob: a metric name [thresholds] does not know, an entry with no glob or an empty one ("" and ./ would match nothing), and tokens, which is a cap on what the walk will parse rather than a maintainability limit and is not overridable by any layer. --print-effective-config prints every entry under [[thresholds.path]] with the metrics it declares at their hard-tier limit — --threshold applied last, the tier not applied — so the printed file replays the gate at the hard tier through --config. Under --tier soft the printed path limits are the hard limits the soft band is derived from, not the band: the band belongs to a file’s whole match set (a second entry naming only another metric changes the first’s), so no per-entry row can carry it, and a printed band would be scaled a second time on the way back in. A soft-tier run is therefore not replayed by the printout alone — the view echoes no [thresholds.soft] table and cannot mark a --threshold limit as absolute — so the hard tier is the replay contract; see Per-path limits for what a same---tier reload does and does not reproduce.

How the layers compose

This is the one statement of the order; bca check --help and the bca check reference cite it rather than restating it.

  1. [thresholds] — the project table, from bca.toml with --config merged on top.
  2. [thresholds.lang.<slug>] — the file’s language, per metric.
  3. [[thresholds.path]] — every entry whose glob matches the file, in declaration order, per metric; the last match wins.
  4. The tier — [thresholds.soft] or the soft ratio — derived from the limits the three layers above resolved to, so each table’s soft band and hard ceiling come from its own numbers. An absolute [thresholds.soft] limit applies as written to every table, path entries included, so it must sit under each one’s hard limit — an entry that lowers a metric below the soft limit is rejected before the walk, naming the entry.
  5. --threshold name=value — applied last and absolutely, to every table, never scaled.

A limit a --threshold flag names is therefore the limit that runs, for every file, whatever the tables say; and a limit only a path entry names is still computed for every file that entry matches, so the gate it adds can fire.

ユースケース別プロファイル

同じコードベースでも、ゲートの目的によって求められる制限は異なります。

ブロッキング CI ゲート

同梱のデフォルトはこのためのものです。プルリクエストを失敗させる制限は、チームが本当に問題だと合意できるものでなければなりません。偽陽性のコストがマージのブロックと論争になるからです。ベースラインと組み合わせて、ゲートが新規の違反とリグレッションでのみ失敗するようにし、そこから引き締めていきます。

同梱のデフォルトを変更せずに使い、さらに次を追加します。

[check]
baseline = ".bca-baseline.toml"

エージェントのフィードバックループ

利用者が人間のレビュアーではなくコーディングエージェントである場合、偽陽性のコストはマージのブロックではなく無駄なリファクタリング 1 回で済み、シグナルはコードを書いている最中に届きます。公表されている関数ごとの値に向けて引き締め、ファイルサイズとコンテナの制限は外してください。1 つの関数を編集しているエージェントには対処のしようがないからです。

[thresholds]
cognitive = 10
cyclomatic = 10
abc = 25
nargs = 4
nexits = 4
"halstead.effort" = 25000

これを編集ループに組み込む方法はエージェントにメトリクスを渡すを、複雑さが偶発的ではなく本質的である場合にそれをエージェントへ伝える方法は抑制マーカーを参照してください。

レガシー監査とトリアージ

When the goal is to rank an unfamiliar codebase rather than to gate it, you want the handful of functions that are 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

レポート出力については品質レポートで説明しています。--vcs を追加すると変更履歴によるランク付けも行われ、通常は複雑さ単独よりも優れたトリアージ順序になります。3 年間誰も触れていない複雑な関数は、バグの在り処ではありません。

セーフティクリティカルおよび規制対象

この分野の標準は制限を直接規定しており、どのような測定よりも標準が優先されます。MISRA と NASA はいずれも循環的複雑度の上限を 15 とし、JSF C++ 標準は大きな switch 文に対する文書化された例外付きで 20 を許容します。McCabe のオリジナルの 10 は、テスト予算がそれを支えられる場合に適用されます。この数値はスタイルに関する意見ではなく、基底パステスト数の境界だからです。

[thresholds]
cognitive = 15
cyclomatic = 10
abc = 30
nargs = 5
nexits = 1
"halstead.effort" = 25000

nexits = 1 は MISRA C:2023 Rule 15.5 の単一出口ルールを符号化したものです。規制対象の業務以外では異論が多く、ほとんどのコードベースでは早期リターンはコードを追いにくくするどころかむしろ単純にするため、一般的な推奨ではなくコンプライアンス設定として扱ってください。

デフォルトではゲートされないメトリクス

These are computed and present in every bca metrics output format; bca report markdown|html shows only a subset of them. They are left out of the default table on purpose.

halstead.volume には、Halstead の測定値の中で最も広く引用されるしきい値があります。関数の volume は 1000 未満に保つべきだというガイドラインです。コーパスに対して測定すると、中央値の言語で関数の約 7%、最悪の言語で 20% にフラグが立ちます。つまりランク付けには有用でも、ゲートとしては使いものにならないということです。

mi.original、mi.sei、mi.visual_studio は Maintainability Index(保守性指標)のファミリーで、低いほど悪い指標です。違反となるのは制限を下回る値です。Visual Studio のバンド(10 未満は不良、10〜19 は中程度、20 以上は良好)は、同製品が 0〜100 に再スケールした独自の出力に適用されるもので、bca はこれを mi.visual_studio として報告します。mi.original は上に有界ではなくコーパス上で 167 に達するため、オリジナルの SEI の 65 と 85 のバンドはそのまま移せません。この指標はまた、すでにゲートしているメトリクスから計算される関数でもあるため、これもゲートすると二重カウントになります。数式は対応コードメトリクスを参照してください。

npm、npa、tokens、cyclomatic.modified が省かれているのは、それぞれがテーブルに既にある何かと重複しているか、あるいはその分布が設計品質ではなく言語のイディオムに支配されているためです。

クラスターに向けた制限の引き締め

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.

ソフト層は制限までの距離を測ります。したがって、母集団の値のちょうど上に置かれる制限は、構造上ソフト層の超過を最大化します。その値にある関数はすべて、制限が定まった瞬間にソフト帯の内側に入り、どれだけ整理しても外には出られません。その値こそが制限「そのもの」だからです。

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 の制限ハード違反ソフト制限(0.95)ソフトの違反
7(維持)276.6560
6(提案)605.7134

Seventy-four functions sit at exactly 6. A limit of 6 puts all of them 0.3 inside 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.)

上記の数値は #1196 より前、つまり nargs ゲートが関数自身のパラメータにネストしたすべてのクロージャの分を合算していた時期に測定されたものです。その母集団の大半はそもそもパラメータ過多ではありませんでした。制限 5 が新たにゲートしたはずの 76 関数のうち、自身のパラメータが 6 個以上あったのは 17 だけで、うち 1 つはパラメータ 1 個に本体内のクロージャが寄与した 5 個を加えたものでした。自身のパラメータのみをゲートする方式で同じツリーを再測定すると次のようになります。

nargs の制限ハード違反新規(ベースライン未登録)ソフトの違反新
7(維持)60160
61603817
5381712999

The cluster trap itself is unchanged and is the reason this section exists: the re-measurement for #1143 found 91 functions sitting at exactly 5, so a limit of 5 still put every one of them permanently inside the soft band. What changed is the size of the real work behind it: signatures rather than mixed cases.

The convergence landed on that evidence. One thing measuring it clarified, which this section previously got wrong: the cluster effect is structural for every integer metric, not a property of landing on a population’s mode. A function sitting exactly at limit L is always above 0.95 x L, so it is always in the soft band. In that measurement the ten functions at exactly 7 were in it before the change, just as the ninety-one at exactly 5 were after. What the limit choice controls is only how many sit there. Read #1143’s objection that way: it was sound because the hard-tier gain was zero, not because 6 was near a mode.

これはまた、0.95 x L と L の差が 1 未満になる小さな整数のメトリクスでは、ソフト層が持つ情報が乏しいことも意味します。halstead.effort(50000 対 47500)では実質的な仕事をしていますが、nargs(5 対 4.75。上限ぎりぎりにある適合済みの関数すべてにフラグを立てます)ではほとんど何もしていません。

bca check --explain-threshold <metric>=<limit> は、bca.toml に触れることなく、1 回の走査で両方の層を測定し、クラスターを見つけた場合はそれを名指しします。

$ 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.

この規則はソフト層を越えて一般化できます。クラスターの上に着地した制限は母集団に余地を残さないため、実際のコードを動かさなければならなくなるのは「次の」引き締めです。クラスターとクラスターの間隙にある制限を選んでください。このフラグの完全な契約については候補の制限をプレビューするを参照してください。

自分のコードベースに合わせた再導出

コーパスのパーセンタイルは出発点にすぎません。自分自身の分布のほうが優れた証拠であり、それを得るにはコマンド 1 つと短いスクリプトで足ります。

bca metrics --no-config -O json -I '*.rs' -X '**/tests/**' . > metrics.jsonl

各行が 1 ファイル分の FuncSpace ツリーです。これを走査し、関心のあるメトリクスについて各スペース自身の値を保持し、メトリクスのスコープに合わせて kind でフィルタし("function"、loc.* には "unit"、nom と wmc にはコンテナの kind)、97.5 パーセンタイルを取ります。そこに制限を設定すると、最悪の 2.5% にフラグが立ちます。

Run two checks 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 は今日時点の違反を記録するため、初日から失敗することなく、より厳しい制限を採用できます。ブートストラップ、更新、退役のフローはベースラインで説明しています。

エージェント型コーディングツールへのメトリクスのフィード

このレシピでは、ファイル編集ツールの実行後に、保守性に関する指摘を Claude Code や OpenCode に渡す方法を説明します。プロジェクトで bca check を実行でき、コーディングエージェントの設定を変更できることを前提としています。指摘はエージェントへの助言です。変更が完了したかどうかは、引き続きプロジェクトの最終検証ゲートで判断します。

The hooks use the existing bca check command, which provides:

  • 機械的にパース可能な違反リスト(stdout への違反ごとの行、または構造化ドキュメントとしての --report-format sarif | code-climate | clang-warning | msvc-warning | checkstyle)。
  • Exit codes that distinguish current findings (2, or 2–5 in tiered mode), a clean check (0), and tool errors (1). The findings cover the whole file, not only the latest edit.
  • Filtering by the project’s policy: baselines, in-source suppression markers, and [check] exclude globs. Put exclusions that must apply to explicitly named files under [check].

キーストローク時ではなく編集後に

エディタとの連携では、Language Server Protocol を使って入力中に診断を更新できます。エージェントのフックは、ファイルの編集が完了してからチェックします。提案中の bca lsp サーバー (#384) と本レシピのフックは、それぞれ次の用途を対象としています。

bca lsp (#384)本レシピ
利用者エディタを使う人ツールループ内のエージェント
トリガーキーストローク(didChange)ツール呼び出しの完了(編集の反映)
再パースインクリメンタル編集ごとにファイル全体を 1 回
出力先マージンの診断表示モデルへフィードバックされるテキスト
ステータス提案段階bca check で今日から動作

入力中のフィードバックにはエディタ連携を、エージェントの編集ループ内でのフィードバックにはツール実行後のフックを使ってください。

以下のツール別セクションがすべて呼び出すコマンドは、手で実行するのと同じものです:

# 終了コード 2 ⇒ このファイルに違反が少なくとも 1 件あります。しきい値は
# リポジトリルートの bca.toml(自動的に発見されます)から来ます。その場限りの
# 上書きは 1 つ以上の --threshold フラグで行います。
bca check path/to/edited_file.rs --threshold cognitive=10

エージェントフィードバック用プロファイル は、合否を決める CI ゲートより厳しいしきい値を提案しています。採用する前に、増える指摘を評価してください。不要なリファクタリングは、作業範囲の拡大やバグの混入につながることがあります。デフォルト値とほかのプロファイルは、しきい値の選択で説明しています。

bca.toml にしきい値を設定すると(ローカルしきい値ゲートを参照)、bca check <file> はコミット済みのしきい値、ベースライン、除外設定を読み込みます。CI と同じ基準で判定する場合は、設定と判定の層(tier)を CI とそろえてください。

フックの除外は [check] の下に置く

One thing does not carry over from the CI invocation, and skipping it produces false positives. A hook names one file per run, and an explicitly named path overrides every walker exclude, following 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 still 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.

ウォーカーの除外は何が解析されるかを形作ります。check の除外は何がゲートされるかを形作ります。ファイル単位のフック呼び出しが尊重するのは後者だけです。

# 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:

warning: utils/gate.py matches an exclude pattern (./utils/**) but was named explicitly; analyzing anyway

Treat that line as a to-do: move the entry it names to [check] exclude. This repository moved its own dev-tooling globs there for this reason.

The hook uses the nearest bca.toml above the session’s working directory, stopping at the checkout boundary. The reference hooks also recognize targets inside another nested checkout and apply that checkout’s policy.

Claude Code

Claude Code の PostToolUse フックを使うと、ファイル編集の成功後に、現在のしきい値違反をモデルへ渡せます。リファレンス実装は stdout に hookSpecificOutput.additionalContext を返し、終了コード 0 で終了します。ツールの元の実行結果は保持されます。

Prerequisites:

  • Claude Code 2.1.139 or later; that release added the hook args field used below (see the official changelog).
  • Node.js LTS and bca.
  • A nonempty [thresholds] table in bca.toml, from bca init or written by hand. Run bca check path/to/a/source-file directly to confirm the configuration loads.

Then copy these files into the same paths in your project:

  • .claude/hooks/bca-check.mjs: フックのイベントを読み取り、助言を含む JSON を返します。
  • .claude/hooks/bca-common.mjs: アナライザーを実行し、出力量を制限します。OpenCode プラグインもこのヘルパーを使います。
  • .claude/hooks/bca-guidance.txt: 実行時に渡す短い注意事項です。末尾の make コマンドは、自分のプロジェクトの検証コマンドに置き換えてください。

既存の設定を保持したまま、次の設定を .claude/settings.json に追加してください。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/bca-check.mjs"],
            "timeout": 10
          }
        ]
      }
    ]
  }
}

The exec form passes the script path as one argument, including spaces and shell metacharacters. The hook requires Node on PATH; its JSON handling uses no npm packages or jq. For an existing bca-check.sh registration, replace the old script with the current wrapper and copy the .mjs files alongside it, or replace that registration with the Node configuration above. Keep only one registration to avoid duplicate checks.

How the hook finds its configuration and analyzer:

  • The event’s cwd selects the nearest manifest within the current checkout. For a target in a different nested checkout, the hook discovers configuration from that target’s directory.
  • CLAUDE_PROJECT_DIR locates the script, but stays at the original project when Claude enters a worktree. Using it to select the analyzer configuration would check the wrong tree.
  • Select $BCA first, then the current checkout’s executable target/release/bca (bca.exe on Windows), then bca on PATH. An explicit but unavailable $BCA stays unavailable rather than falling back to another binary.
  • Within one checkout, the session’s manifest also applies when it edits a sibling package. Change the session directory to select that package’s own manifest.
  • Nested checkouts, including Git submodules, are separate configuration boundaries. If a nested checkout has no thresholds, the check is unavailable; it does not inherit the parent’s policy.

アナライザーの結果は次のように扱います。フック自体の終了コードは 0 です。

  • 終了コード 0: 指摘がないファイルや除外されたファイルについては、モデルにコンテキストを追加しません。
  • 終了コード 2–5: stdout の指摘を助言としてモデルのコンテキストに追加します。4,000 文字に制限し、省略した場合はその旨を示します。
  • アナライザーの失敗: バイナリがない場合、終了コード 1、異常終了、タイムアウト、出力上限の超過では、モデルにコンテキストを追加しません。各子プロセスの実行時間は 3 秒、出力はストリームごとに 64 KiB が上限です。タイムアウト、キャンセル、出力上限の超過が起きると、POSIX では起動したプロセスグループを、Windows ではプロセスツリーを終了させます。
  • 解析中のファイル変更: 解析中にファイルの識別情報、サイズ、タイムスタンプが変わった場合は、指摘を破棄します。

このアダプターは重複排除の状態を永続化しないため、変化のない違反も次の編集で再び報告されることがあります。チェック対象はファイル全体です。[new] と [regr] は直前の編集ではなく、設定したベースラインとの比較を示します。基本方針に従い、対応をタスクの範囲内に収めてください。この matcher の対象は Edit、Write、MultiEdit です。シェルコマンドや外部エディタには別の連携が必要です。

Verify model delivery after installation. Using Claude’s Write tool, create a disposable supported source file that breaches a configured limit, and ask Claude to quote the finding it receives. Then simplify the file through Edit and confirm the clean result adds no advisory. In debug output, [bca-check] JSON records distinguish findings, clean/excluded files, out-of-scope paths, superseded results, and unavailable checks. These records go to stderr with exit 0, so Claude does not receive them as additional context. They record the checkout, analyzer, outcome, and up to 1,000 characters of analyzer stderr for troubleshooting. Offender rows remain in the advisory; CI annotations and summary-file side effects are disabled. The reference suite runs with make plugin-test, including the shared OpenCode helper regressions.

OpenCode V1(旧バージョン)

この節の例は V1 のプラグイン API を使います。本リポジトリの現在の V2 プラグインは V2 フック APIを使い、.claude/hooks/bca-common.mjs も必要です。V1 プラグインを移植する際は、移行ガイドを参照してください。

Mechanism: implement a plugin: a JavaScript or TypeScript module that exports an async function returning a hooks object. Register tool.execute.after, which runs after a tool completes, including the write and edit tools.

Feedback channel: the after-hook surfaces a problem to the agent by throwing. opencode’s public plugins page documents the throw-to-signal pattern but not an advisory return value for the after-hook, so this recipe defaults to throwing. A thrown Error carries its message back to the agent as the tool’s failure.

Mind the argument shape; it differs from the before hook. The published @opencode-ai/plugin types give tool.execute.after the signature (input, output) where the tool name is input.tool and the tool arguments are on input.args (the file path is input.args.filePath). This is the trap: the docs’ only worked example is for tool.execute.before, where args live on output.args (mutable, pre-execution). Copy that into an after hook and output.args is undefined, the guard below always trips, and the plugin silently never runs: a no-op that looks installed. Read input.args.filePath in the after hook.

Drop this file in .opencode/plugins/. It is project-level and auto-loaded; no opencode.json entry is needed, since that key is for npm-published plugins. The plugin below is plain JavaScript. For a TypeScript plugin, import type { Plugin } from "@opencode-ai/plugin" and annotate the export, and declare any extra deps in .opencode/package.json (opencode installs it with 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

      // throw して違反をエージェントに提示する。行は stdout にあり、
      // stderr は唯一の出力が診断メッセージである実行のための
      // フォールバック。
      const offenders = res.stdout.toString().trim() || res.stderr.toString().trim()
      throw new Error(`bca flagged complexity in ${filePath}:\n\n${offenders}\n\n${GUIDANCE}`)
    },
  }
}

Keep the GUIDANCE string in sync with the verbatim block below (or read it from a shared file). Because the channel is a thrown error, opencode reports it as a failed post-edit step, which is the intended “address this before continuing” framing. The edit itself still lands: tool.execute.after runs once the write or edit tool has already written the file, so the throw frames the next step without undoing the change.

Restart opencode after adding the plugin. Plugins are loaded once at startup and are not hot-reloaded. A freshly dropped .opencode/plugins/bca-check.js does nothing in the running session. Quit and relaunch opencode, then confirm by editing a file you know is over threshold and checking that the edit/write tool reports the bca failure. An installed-but-inert plugin is the most likely symptom, and a stale session is the most likely cause.

The repository ships a V2 reference at .opencode/plugins/bca-check.js. It uses a different plugin API and adds guards worth porting to a V1 setup:

  • リポジトリスコープガード。 パスを解決してプロジェクトルート外のものはスキップし、エージェントがディスク上の別の場所で編集したファイルに対してフックが bca を実行しないようにします。
  • ローカルビルドの解決。 $BCA、次にチェックアウト内の target/release/bca、最後に PATH 上の bca の順で優先します。bca を自前でビルドするプロジェクトは、グローバルにインストールされている何かではなく、自身のアナライザーでゲートできます。
  • 共有ガイダンス。 このプラグインと Claude Code フックの両方が参照する 1 つのファイルからガイダンステキストを読み込み、両者が乖離しないようにします。

エージェント向けガイダンス(このまま使用する)

数値だけのフィードバックは、役割のないヘルパーの抽出など、読みやすさを改善せずにスコアだけを下げる変更を促すことがあります。指摘への対応方針をエージェントに伝えてください。次のブロックを基本方針としてエージェントのルールファイル(CLAUDE.md または AGENTS.md)に貼り付けます。実行時のフィードバックには短い bca-guidance.txt を使ってください。リファレンス実装では、このテキストの長さを指摘とは別に制限しています。

以下の方針文は、翻訳版でも英語のまま掲載しています。そのままコピーして使ってください。

**Responding to `bca` metric feedback.** A threshold violation is a
reason to review readability, not proof of a defect. Improve the code
where that helps the current task; lowering a score is not itself a goal.

- **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 without necessarily improving readability.
  New helpers also add names and interfaces that readers must understand.
- **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`, reconsider the split.
- **When the complexity is essential, suppress with a reason.** Some
  functions are clearest left whole, such as 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`.
  A clear function with an honest marker is better than a
  "compliant" tangle.
- **Keep the fix within the task.** Review findings relevant to the
  current change. Findings may concern other functions or the whole
  file; mention unrelated findings without expanding the edit.

誠実な抑制(正確な構文)

抑制には、抑制マーカーで説明する正確な構文が必要です。

  • 関数単位: 関数本体内のコメントに、対象メトリクスを指定したマーカーを置きます。例: // bca: suppress(cyclomatic, abc) -- hand-rolled parser table。一覧を省略した // bca: suppress は、その関数の抑制可能なすべてのメトリクスを抑制します。tokens の制限は引き続き適用されます。
  • ファイル全体: ファイル内の任意の場所に // bca: suppress-file(halstead, nargs, nexits) を置きます。一覧を省略した // bca: suppress-file は、ファイル全体で抑制可能なすべてのメトリクスを抑制します。tokens は抑制しません。
  • 理由は、マーカーと同じ行のメトリクス一覧の後に書いてください。 一覧の後には自由に理由を書けます。区切り文字は不要です。メトリクス一覧を省略した形式では、マーカーに関する説明文と区別できないため、末尾に理由を書けません。適用除外の妥当性をレビュアーが判断できるよう、理由を添えてください。bca exemptions で、その確認に使うマーカー一覧を表示できます。
  • 正規のメトリクス名を使ってください。 受け付けられる識別子は abc、cognitive、cyclomatic、halstead、loc、mi、nargs、nexits、nom、npa、npm、wmc です。exit ではなく nexits です(レガシーの exit エイリアスは廃止されました)。未知の識別子は警告が出た上でスキップされ、併記された認識済みの名前は引き続き抑制されます。そのため suppress(cognitive, exit) は cognitive を抑制しつつ exit について警告します。tokens は意図的に抑制「不可」です。ハードなリソース上限として扱ってください。

編集ごとではなくタスク境界でゲートする

編集ごとのフックは、早い段階で助言を提供します。ひとまとまりの変更を終えてから、テストやリンターを含むプロジェクトの検証ゲートを実行してください。make self-scan / pre-commit パターンでは、ハードゲートに CI と同じしきい値を使います。値が大きいほど悪いメトリクスでは、デフォルトのソフトしきい値をハード上限の 95% に設定し、上限に近づいた段階で警告します。

編集の途中では、リファクタリングの完了時に解消される一時的な指摘が出ることがあります。それらに対応する前に、エージェントにひとまとまりの編集を終えさせてください。メトリクスは読みやすさを判断する目安であり、コードの正しさを証明するものではありません。

フックがすでにチェックしている場合は、編集のたびに追加のメトリクスチェックを要求しないでください。最終検証を実行する義務は明示し、入力が変わった場合や前回の結果を利用できない場合にチェックを再実行します。

注意点

  • 数値だけを改善する変更: 代理指標の最適化が、本来の目標を損なうことがあります。グッドハートの法則が説明するリスクです。数値を下げるよう求めると、読みやすさを改善せずに複雑さを別の関数へ移す変更を促すことがあります。エージェント向けの指針を含め、根拠のある抑制を説明してください。
  • しきい値は目安であり、正しさを判定するものではありません: しきい値を超えた場合は、読みやすさを確認してください。それだけでコードに欠陥があるとは言えません。指摘はエージェントが判断材料として使う助言として渡し、変更に適したテストやほかのチェックで動作を検証してください。
  • 作業範囲の拡大: 指摘への対応が、タスクに必要な範囲を超えるリファクタリングにつながることがあります。修正は現在のタスクの範囲に収めてください。指摘には、無関係な関数やファイル全体に関するものも含まれます。無関係な指摘は報告にとどめ、修正範囲を広げないでください。

AST クエリ

パース済みの構文木を直接扱うレシピです。ノード型の検索、そのカウント、ツリーのダンプを行います。

Library-side equivalents. Every recipe below has an in-process Rust counterpart in Walking the AST directly. Use that when shelling out per file is too slow or when you want to compose metrics with custom AST analysis in one parse.

コミット前にパースエラーを検出する

Tree-sitter exposes a synthetic ERROR node anywhere it could not parse. Use find to surface them:

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 apply to dump and find, so the same range scopes a search to a single function:

bca find \
    --paths src/lib.rs \
    --line-start 42 --line-end 88 -t return_expression

短縮形の --ls / --le は非推奨エイリアスとして残っていますが、次のメジャーリリースで削除される予定です。

すべての関数・メソッドを一覧する

人間が読むための簡単なインベントリには:

bca functions \
    --include "*.rs" \
    --paths src/

The output is a tree per file: an In file … header followed by an indented row per function with name and line span. It is meant for reading, not parsing.

For tooling that needs a structured inventory (coverage mapping, documentation generation, code-owner reports), use the JSON metrics output instead and walk .spaces[] recursively with jq, taking entries whose kind is 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}
    '

This emits one JSON object per function, ready to pipe into downstream tooling.

メトリクスデータのエクスポート

The metrics, ops, and preproc subcommands all support structured output formats meant for machine consumption. This page covers writing those documents, diffing two runs, discovering the metric catalog at runtime, and stripping comments for downstream tools. Pair the output with a JSON processor like jq for ad-hoc analysis, or feed it into a database or dashboard.

ファイルごとのメトリクスを JSON でエクスポートする

bca metrics \
    --paths src/ \
    -O json \
    --output-dir /tmp/metrics

This writes one JSON file per analyzed source file under /tmp/metrics/. The output filename is the input path with the format extension appended: src/lib.rs becomes src/lib.rs.json, not src/lib.json. Use --pretty if you intend to read the files by hand:

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

Its elements are ordered by file path regardless of the order the parallel workers finished in, so the document is byte-identical between runs over unchanged sources. bca ops --output <file> orders its aggregate the same way.

CBOR (-O cbor) is the most compact format; it is binary and so requires a destination (--output or --output-dir). JSON, TOML, and YAML stream to stdout when no destination is given, which suits pipelines.

bca diff で 2 つのメトリクス実行を比較する

bca diff compares two JSON metric runs and reports, per metric, which files changed (old → new), plus any files added or removed between the two sets. Each side is a single per-file JSON document, the aggregate file metrics -O json --output <file> writes, or a whole directory tree of either (what metrics -O json --output-dir <dir> writes), so the common workflow is two bca metrics runs into separate directories:

snap=$(mktemp -d "${TMPDIR:-/tmp}/bca-diff.XXXXXX")

# 「変更前」の状態を取得する。
bca metrics -p src/ -O json --output-dir "$snap/before"

# ...make a change (for example, bump a tree-sitter grammar)...

# 「変更後」の状態を取得して diff を取る。
bca metrics -p src/ -O json --output-dir "$snap/after"
bca diff "$snap/before" "$snap/after"

The output buckets every per-file delta by metric name, using the same names bca list-metrics prints (cyclomatic, cognitive, sloc, …):

2 metric(s) changed, 1 added file(s), 0 removed file(s), 0 added field(s), 0 removed field(s)

## Added files
  src/new_module.rs

## cyclomatic (2 change(s))
  src/lib.rs.max  4 → 6
  src/lib.rs.sum  12 → 14

## halstead (1 change(s))
  src/lib.rs.effort  5820.3 → 7104.9

A leaf that carries a number on one side only (a family the other capture never computed because the two runs used different --metrics sets, or a field a newer bca started emitting) has nothing to diff against, so it is never compared with zero. It is listed under ## Added fields / ## Removed fields (and as added_fields / removed_fields rows of {file, metric, field} in the JSON form), is counted in the summary line, and counts as a difference for --exit-code. --metric filters these rows as it filters buckets; --min-change does not apply to them, since there is no delta to measure. “Present” means numeric: a leaf written as null on one side (how a non-finite derived value, such as a vcs ratio with nothing to divide by, is serialized) reads as absent there, so two captures by the same bca version can list a field that became applicable between them.

A document pairs with the other side’s document of the same name, the source path bca recorded, compared in lexical form: . components are dropped, so captures taken with -p ./src and -p src pair, and a .. is kept. A directory side reads every *.json file under it and passes over any other file, such as a README. Input bca diff cannot pair is an error (exit 1) naming it, never a file silently missing from one side: a directory with no *.json file (a capture written with -O yaml, say), JSON with no metrics object, two documents with one name, and a document with no name unless it is a whole file inside a directory side, where its relative path stands in. bca metrics writes no name for a source path that is not valid UTF-8, and no layout can diff such a file. A symlinked *.json file is read like the file it names, and one that dangles is an error. A symlinked directory inside a side is not entered, as the source walk enters none.

便利なフラグ:

  • --format markdown produces Markdown for a sticky PR comment; --format json produces a stable machine-readable schema for CI consumers.
  • --min-change <N> は絶対変化量が N 以上の差分のみを報告します(デフォルトの 0 はあらゆる変化を報告します)。
  • --metric <name>(繰り返し可能)は diff を特定のメトリクスに限定します。

A saved document carries no record of the walker settings that produced it, so bca diff cannot tell two captures made under different settings apart from a real change. Compare captures taken by the same version with the same exclude_tests, cyclomatic_count_try and mi_charges_docs; a different MI documentation mode moves the mi.sei value of every documented space. --since runs both sides itself with one set of flags, so it cannot mix modes.

--since で git ref と比較する

To answer “what did my uncommitted change do to the metrics?”, --since <ref> skips the two captures: it analyzes the tree at a git ref for the before side and diffs it against the current working tree, which is always the after side:

# 変更前 = HEAD~1 時点のツリー。変更後 = 現在の作業ツリー。
bca diff --since HEAD~1 -p src/

# The same diff, scoped by the optional positional instead of -p. It
# selects the src/ subtree on both sides; it never names another tree.
bca diff --since HEAD~1 src

--since materializes the ref’s tree into a temporary directory (via git ls-tree + git cat-file), runs the same metric walk against it, then diffs. The temp tree is removed when the command finishes, whether it succeeds or exits with an error. Only a run killed by a signal (Ctrl-C included) leaves it behind in the temporary directory. The same -p/--paths, -I/--include, and -X/--exclude selection applies to both sides so they analyze the same file set. Each side reads an --exclude-from file for itself, a relative one from its own root (the ref’s copy for the before side), so --exclude-from - (stdin, readable only once) is rejected, as is --paths-from. Selection paths are repo-root-relative and must be relative: the working-tree side is anchored at the repository root (matching the whole-tree materialization of the ref), so bca diff --since produces the same result from any subdirectory. An absolute --paths or positional is rejected (exit 1) because it cannot address the extracted ref tree.

Unlike bca check --since (best-effort), bca diff --since is an explicit request: an unresolvable ref, a missing git, or a non-git working directory is a hard error (exit 1). --since takes at most one positional, the relative path scope above; passing two is an error.

bca diff is informational, not a gate, and exits 0 on success. The opt-in --exit-code flag changes that: it exits 2 when the filtered diff is non-empty.

ツリー全体から単一のメトリクスを取り出す

ストリームされた JSON 出力を jq と組み合わせて、ファイルごとに 1 つの値を抽出します:

bca metrics -p src/ -O json \
  | jq -c '{file: .name, mi: .metrics.mi.visual_studio}'

The same idea works for any metric: cyclomatic.sum, cognitive.sum, loc.sloc, and so on. Run bca list-metrics descriptions to see the catalog.

実行時にメトリクスカタログを発見する

Tooling that drives the CLI should not hard-code metric names. Ask the binary:

bca list-metrics                # 1 行につき 1 つの名前
bca list-metrics descriptions   # 名前 + 概要

これはコードジェネレーター、スキーマ定義、タブ補完に適した入力です。

オペランドとオペレーターを抽出する(Halstead)

ops emits the raw operand and operator lists per file, which is the input to Halstead-style metric calculations beyond what the built-in report shows:

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/ 以下にミラーします。

Both lists are sorted, so re-running ops over unchanged sources produces byte-identical output. It is safe to diff between runs, check into a repository, or use as a cache key.

ツリーからコメントを除去する

strip-comments rewrites source so that downstream tools that do not understand comment syntax can still consume the code. Output routing has three modes:

  • stdout (default). With neither flag, the stripped source streams to stdout, which suits a single file in a pipeline.
  • --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 is destructive: commit or back up the tree first. Passing both --in-place and --output is a usage error.

REST API の操作

bca-web exposes the same analysis primitives as the bca CLI over HTTP. Use it when the consumer is a long-running service (an editor plugin, CI worker, or web app) that should not pay the cost of spawning the CLI per file. This page is a set of end-to-end curl calls, one per endpoint; the REST API reference documents every field.

Every endpoint is mounted under the /v1 prefix; the old unprefixed paths were removed at the 2.0 release (#637) and now return 404.

サーバーを起動する

bca-web --host 127.0.0.1 --port 8080 -j "$(nproc)"

Verify it is up:

curl -sf http://127.0.0.1:8080/v1/ping && echo "ok"
# => ok

/ping returns 200 OK with an empty body. curl -sf exits 0 on success and non-zero on any HTTP error, which is what scripts want.

ビルド、フラグや環境変数のチューニング、リソース制限、デーモン自体のセキュリティ態勢については、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 plus jq packages a real file into the JSON envelope the server expects:

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'

This pattern (jq -n --rawfile to build the request, curl --data-binary @- to stream it) avoids quoting problems with multi-line source code.

API 経由でコメントを除去する

エンドポイントは /comment(単数形)です。Content-Type で選択される 2 つのバリアントがあります:

  • application/json wraps the request and response in JSON. The response code field is a string holding the stripped source (the request code arrived as a JSON string, so the result is valid UTF-8; #629 replaced the earlier byte-array shape).
  • application/octet-stream accepts the source as the raw request body and returns the stripped source as the raw response body. This is the simpler variant to use from the shell.

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 form (use when your client speaks JSON natively):

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'

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'

Each entry has name, start_line, and end_line; name is null when the parser could not resolve the function’s name (an anonymous or malformed definition). That is enough for an editor to draw a function navigator without re-parsing the file locally.

変更履歴リスクでリポジトリをランク付けする

/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: .vcs.risk_score, churn_recent: .vcs.churn_recent}'

The body accepts the same knobs as 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, author_hash_key, include_deleted, bus_factor_threshold, no_cache, cache_dir) as optional fields; top defaults to 50 like the CLI, and 0 returns every file. file_types scopes the ranking (metrics, the default, or all, or a rs,py-style extension list), mirroring the CLI --file-types. A repo_path that does not exist or is not a git working tree, or a malformed window / timestamp / formula / scope, returns 400 with the uniform JSON error body. Unknown fields are rejected with 400 too.

時系列トレンド

POST /vcs/trend samples the same metrics at several points in time and returns a per-file time series (see Historical trend). The body takes the /vcs fields except the cache controls (no_cache / cache_dir are rejected with 400, since trend does not cache) plus points (>= 2, default 12), span (default 12mo), and top_deltas (default 10; 0 returns all).

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}'

To score an arbitrary diff instead, send a diff field carrying the unified diff (no repo_path needed). The response is then the partial report: source: "diff", a partial_risk_score, and no history / experience / purpose groups, which are absent rather than zero. The partial score is not comparable to a commit 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 を呼び出す

For a short-lived CI job, start the server as a background process and stop it when the job finishes:

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

# … ここで分析の呼び出しを実行します …

For longer-lived workers, run the server as a systemd unit (or a container) and point your jobs at its host and port.

ライブラリとしての利用

big-code-analysis は Rust ライブラリとして crates.io に公開されています。CLI(bca)と REST サーバー(bca-web)はどちらも同じ公開 API の薄いラッパーなので、それらにできることはすべて自分のクレートから直接行えます。

このセクションはタスク指向です。完全な型シグネチャとフィールドのドキュメントは docs.rs の rustdoc を参照してください。

ライブラリを組み込むべきとき

Use the library instead of shelling out to bca when you want any of the following:

  • In-process analysis. Avoid spawning a subprocess per file when scoring thousands of files in a custom tool, IDE plugin, or static-analysis pipeline.
  • インメモリソース。 生成された、前処理済みの、あるいはストリーミングされたソースを、先にディスクへ書き出すことなくスコアリングします。インメモリソースの分析を参照してください。
  • 選択的な走査。 FuncSpace ツリーを独自に走査し、関数ごとのメトリクスを任意のタイミングで抽出します。FuncSpace 結果の走査を参照してください。
  • Custom output. Skip the bca serializers (JSON / YAML / TOML / CBOR in the CLI, CSV / SARIF / Checkstyle in the library’s output module) and emit your own format, such as a database row.

If you only want a Markdown quality report or a CI threshold gate, the bca CLI is faster to wire up.

Pages in this section

API stability

このライブラリは 2.x 系にあり、明文化された安定性契約のもとで提供されています。公開 API の形状はパッチおよびマイナーバンプの間で安定に保たれ、破壊的変更は次のメジャーバンプまで保留されます。このセクションのすべての例は現在公開中のクレートに対してコンパイルでき、2.x の間は修正なしでコンパイルできることが期待されます。

Metric values may still drift across minor bumps when a grammar pin moves or a metric definition is fixed; see STABILITY.md § What is stable in value for the carve-out. Each drift is called out in the changelog entry that introduces it.

クイックスタート

This page shows the minimum code needed to compute metrics from a string of source code. It assumes a working Rust toolchain and a crate to add the dependency to.

1. クレートを追加する

# Cargo.toml
[dependencies]
big-code-analysis = "2.4.0"

The crate uses Rust edition 2024 and pins rust-version = "1.94". Older toolchains will not build it; see the MSRV section of STABILITY.md for the policy.

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 becomes the top-level FuncSpace::name; passing None leaves the top-level name unset. The return type is Result<FuncSpace, MetricsError>. In practice the Err variant means the requested language’s Cargo feature is disabled in this build. A parse failure does not produce Err: tree-sitter recovers with ERROR nodes. See Error handling for the variant set and matching patterns. MetricsError is #[non_exhaustive], so always include a _ arm when matching.

Tip: use big_code_analysis::prelude::*; brings the recommended entry points (analyze, Ast, Source, MetricsOptions, MetricsError, FromPathError, SkipReason, LANG, FuncSpace, CodeMetrics, SpaceKind, Metric) into scope in one line. Anything outside the prelude is still reachable by name, for example use big_code_analysis::guess_language;.

Need more than metrics from one parse (operators and operands, an AST dump, a function-span list)? Parse once with Ast::parse and call the per-pass methods on the handle. See Parse once, run metrics many times. If you already drive your own tree_sitter::Parser, adopt the resulting tree with Ast::from_tree_sitter (see Reusing an existing tree-sitter Tree).

3. Inspect the result

FuncSpace is a tree of spaces. The top-level node represents the whole file; its spaces field holds nested function, class and impl spaces. Every node carries the same CodeMetrics struct, so you can read any metric at any level.

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 結果の走査を参照してください。

言語の選択

If you do not know the language up front, use guess_language. It consults the path extension, an Emacs mode declaration in the buffer, and the shebang, in that order:

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 returns (None, _) only when the extension, the mode declaration, and the shebang all fail to name a language; treat that as “skip this file” rather than as a parse error.

メモリ上のソースの分析

big-code-analysis never requires source to live on disk. The recommended entry point analyze takes a Source carrying the language, source bytes, and an optional caller-supplied display name. No filesystem path is involved unless the C/C++ preprocessor lookup needs one (Source::preproc_path).

これは次の用途に有用です:

  • 生成コードを書き出す前にスコアリングする。
  • Scoring pre-processed or bundled source, such as the output of a template expansion.
  • すでにバッファをメモリ上に保持している言語サーバーやエディタプラグインから分析ツールを駆動する。
  • Running stdin pipelines and unit tests that should not touch the filesystem.

バッファからの読み取り

#![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 borrows the source bytes; the caller keeps ownership. If your downstream pipeline needs to highlight findings on the same bytes, you can keep using the original buffer after analyze returns.

標準入力からの読み取り

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(())
}

内容から言語を選択する

If you do not know the language up front, combine guess_language with analyze. guess_language checks the path extension, an Emacs mode declaration, and the shebang, in that order:

#![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 returns (None, _) when the extension, the mode line, and the shebang all fail to name a language. Treat that as “skip” rather than as a hard error.

注意点

  • Name identity matters. Top-level FuncSpace::name is whatever string you put in Source::name. Two analyses sharing the same name look identical to a downstream consumer that keys on it. Use distinct labels for distinct buffers.
  • Source::name is Option<String>. Passing None leaves the top-level FuncSpace::name as None, which suits ad-hoc snippets with no meaningful identity. Downstream consumers that require a stable identifier should check for None explicitly.
  • ファイルシステムへのフォールバックはありません。 CLI と異なり、ライブラリは隣接ファイルを読んだり、#include を辿ったり、.gitignore を解釈したりしません。分析したいバイト列を正確に与えてください。

一度のパースを複数のパスで再利用する

analyze is the one-shot entry point: bytes in, FuncSpace out. When you need more than metrics from a single parse (operators and operands, an AST dump, a function-span list), parse once with Ast::parse and call the per-pass methods on the returned handle (metrics, ops, dump, functions, and so on). See Parse once, run metrics many times.

既存の tree-sitter Tree の再利用

Callers who already drive tree-sitter for syntax highlighting, code folding, or queries otherwise parse every file twice: once for their own tree, once inside the metric walker. The parse seam lets you hand big-code-analysis an already-parsed tree_sitter::Tree and get the same FuncSpace back without re-parsing.

Use Ast::from_tree_sitter. It adopts a caller-built tree_sitter::Tree and lets you run the metric walker more than once against the same parse: different MetricsOptions::with_only selections, custom tree-sitter walks interleaved with metrics, or Ast::ops for operator and operand extraction. See Parse once, run metrics many times. It carries an explicit name: Option<String> rather than deriving the top-level FuncSpace::name from a path via lossy UTF-8 conversion.

これを使うべきとき

次に当てはまる場合はパースシームを使用してください:

  • Already keep a tree_sitter::Tree per open buffer (editor, language server, custom static-analysis pipeline) and want to reuse that parse for metrics rather than paying the byte-based cost again.
  • Want to run several passes (metrics, AST dump, custom analysis) against one parse result.
  • このライブラリとは別の依存関係を取らずに、自分の側で tree-sitter をピン留めしたい。再エクスポートされた big_code_analysis::tree_sitter モジュールはリンク対象と同一のクレートなので、型は定義上一致します。

Use the byte-based entry point analyze (with a Source) if you do not already have a tree. It constructs the parser internally and owns the parse end to end.

動作する例

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(),
);

The same shape works for any LANG variant: pass the matching grammar to tree_sitter::Parser::set_language (via LANG::tree_sitter_language) and the metric walker produces the same FuncSpace it would have produced from bytes.

唯一のツリー取り込みシーム

Ast::from_tree_sitter is the entry point for tree reuse. It dispatches on a LANG at runtime and hides the parser plumbing entirely. The former lower-level path (the generic Parser<T> / ParserTrait and the per-language *Parser / *Code tag types) is now crate-private (pub(crate)) and no longer part of the public surface; see STABILITY.md. Ast::from_tree_sitter exposes no per-language tag types or trait bounds.

対象外

  • Incremental re-computation. Applying a tree_sitter::InputEdit and re-querying only the changed spans is not supported yet; the metric walker still walks the entire tree on every call. The parse seam is the first step; making the walker itself incremental is a follow-up.
  • Promising every Node method. Node exposes its inner tree_sitter::Node through Node::as_tree_sitter() for ad-hoc traversal, and only the accessor list in STABILITY.md § Escape hatches is shape-stable; the helpers that take a Search or Ancestors are not covered.

一度のパースで複数回メトリクスを実行

The one-shot entry point analyze re-parses its Source on every call. For pipelines that score a file several times (different metric subsets, an interleaved custom tree-sitter walk, or a metric re-run after a configuration change) that re-parse is wasted work.

The Ast type, added in 1.1.0, exposes the seam: parse the source once, then call Ast::metrics as many times as you need against the held parse. Ast::from_path is the file-backed counterpart: it reads, language-detects and parses one file, returning a FromPathError naming the reason when it cannot.

これを使うべきとき

次のいずれかに当てはまる場合は Ast を使用してください:

  • 選択的なメトリクス実行。 同じファイルに対して、レポート用にあるメトリクスの集合を計算し、次に CI のしきい値ゲート用に別の集合を計算する。
  • Custom tree-sitter walks. You already drive a tree_sitter::Tree for queries, highlighting or symbol extraction and want to fold the metric walker into the same parse.
  • Cached analysis. A language-server-like service that holds parsed files in memory re-runs metrics on demand when configuration changes, without going back to bytes.

If you only ever compute every metric once per file, stick with analyze. It delegates to Ast internally, so the shapes line up, but the one-shot API stays simpler.

呼び出しをまたいだ選択的メトリクス

#![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++ プリプロセッサに関する注記 を参照してください)。

For realistic AST work (counting node kinds, finding constructs by name, detecting parse errors, building a symbol table) see Walking the AST directly. The example below is a minimal smoke test; the dedicated chapter shows the full pattern (reusable depth-first walker, field-name lookup, error detection).

#![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 returns the per-space operator and operand Ops tree, the data behind the Halstead metrics. The top-level Ops::name is the Source::name you supplied, carried explicitly (including the None case) rather than a lossy-path string, so Ops::name_was_lossy is never set on this path.

#![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 == "+"));
}

呼び出し側で構築したツリーの取り込み

If you already build the tree_sitter::Tree yourself (for example, because your editor or language server has its own parser pool), Ast::from_tree_sitter is the tree-adoption seam. It carries an explicit name: Option<String> end to end instead of deriving one from a path via lossy UTF-8 conversion.

#![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");
}

The tree must have been produced from code with the grammar returned by LANG::tree_sitter_language for lang. A mismatch is not unsafe, but it is a precondition violation. The walk slices code by node byte ranges, so a tree built from longer source than code indexes out of bounds and panics. A tree from a different grammar that stays in bounds yields nonsensical results instead, because the metric walker matches on tree-sitter kind_id values from the language’s enum.

C++ プリプロセッサ

When Ast::parse is called on a Source carrying preprocessor inputs (Source::with_preproc_path plus Source::with_preproc) and the language is a C-family one (LANG::C, LANG::Cpp or LANG::Mozcpp), the macro pre-pass runs before tree-sitter does. Ast::source then returns the expanded bytes the parser saw, not the original input.

Ast::from_tree_sitter is unaffected: it adopts whatever tree the caller built. Ast::source reflects whatever expansion, if any, the caller applied before building the tree.

並行性

Ast is Send + Sync. Running Ast::metrics from several threads against the same &Ast is safe, because the walker only reads from the held tree_sitter::Tree. Benchmarking parallel metric runs is a separate follow-up.

対象外

  • tree_sitter::InputEdit による増分再パース。 解析パイプライン全体で安定した Ast をキャッシュすることはスコープ内ですが、保持しているツリーを編集することはスコープ外です。
  • デフォルトで並列な API。 Ast::metrics はメトリクス集合をまたいで内部的に並列化しません。サブセットごとに 1 スレッドを使いたい呼び出し側は、自由にそうできます。

AST を直接たどる

Ast::parse gives you a parsed tree_sitter::Tree together with the source bytes it was parsed from; Ast::as_tree_sitter hands that tree out as a borrowed reference. This chapter shows how to use it to drive your own syntax-tree analysis (counting node kinds, finding constructs by name, detecting parse errors, or pulling out a symbol table) without paying for a second parse. It assumes you know the tree-sitter node model: a tree of nodes, each with a kind name, a byte range and optional named fields.

これを使うべきとき

次のような場合には、直接の AST 走査を選んでください。

  • You want to count or find syntactic constructs in-process. The CLI equivalents (bca count -t <kind>, bca find -t <kind>, recipe) mean launching a separate process and parsing its output; the library path is one parse and one Rust loop.
  • You want to detect parse errors programmatically. Tree-sitter emits a synthetic ERROR node anywhere the grammar could not match. Node::has_error is O(1) because tree-sitter caches the error bit on every node, so the check is free even on a multi-MB source file.
  • You want to mix metrics with custom analysis in one parse: for example, capture metric values and a list of function names for a coverage mapping, an IDE outline, or a code-owner report.

If you only need standard metrics, stay with analyze or Ast::metrics, which walk the tree for you. The direct path is for things the metric walker does not already compute.

再エクスポートされた 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 tests in tests/api/book_ast_traversal_examples.rs keep 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);

The string keys ("if_expression", "for_expression", …) are the tree-sitter grammar’s node-type names. To discover a language’s node-type names, run bca dump --paths sample.rs, which prints the full AST.

Anonymous tokens. The walker visits every node tree-sitter emits, including anonymous tokens like "{", ";", and keyword literals. The targeted counts.get("if_expression") lookups above are unaffected, since anonymous tokens have different kind names, but counts.values().sum() would be much larger than the count of named grammar productions. Filter with tree_sitter::Node::is_named() inside the visitor if you only want named nodes.

種別ごとにノードを検索する

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();
// Captured slices borrow from `source`: no per-hit `String` allocation.
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[..]) slices the source bytes by the node’s byte range. Pair it with Ast::source: for C++ with preprocessor inputs supplied to Ast::parse, source is the expanded buffer the parser saw, not the original input (see the C++ preprocessor note).

パースエラーを検出する

Tree-sitter is lossless: even on malformed input it returns a tree, with the nodes it could not match tagged as errors. Check the root for a cached error flag:

#![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");

// …and so does the custom walk, against the very same tree. The
// captured names borrow from `source` rather than allocating a fresh
// `String` per function, the same pattern as `find_unsafe_blocks`
// above.
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 walks the named grammar fields, the same fields that appear in the field_name key of the serialized AST (REST /ast, Ast::dump). Field-based lookup survives grammar changes that positional indexing does not, because it does not depend on which anonymous tokens (commas, parentheses, and so on) the grammar emits as children.

シリアライズ可能な JSON ツリーが欲しい場合

For pipelines that want a structured AST as data (diffing, queries on the wire, language-agnostic schema work), Ast::dump materializes the tree as a Serialize-able AstResponse of AstNodes. This is the same shape the REST /ast endpoint produces. Call it on the parse handle:

#![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 を使ってください。

Ast::dump walks the tree with one cursor, the same way the walker above does. Two siblings name the strategy explicitly: Ast::dump_cursor is that walk, and Ast::dump_indexed reaches each child through Node::child and its field through Node::field_name_for_child instead. They produce the same tree; the difference is cost. Both lookups scan toward the requested child. Lookup costs O(log n) for lists stored as balanced repeat() structures and O(index) for children stored in a flat array. Error recovery can create such flat arrays from skipped tokens. Enumerating a wide flat node by index is therefore quadratic in its width. Prefer the cursor walk for a whole-tree export, field lookup (child_by_field_name) for a grammatical role, and indexed access for picking one slot. A loop that needs repeated random access to a wide list should collect the children once and index the Vec.

対象外

  • Incremental reparse. Tree-sitter supports tree_sitter::InputEdit for incremental updates, but Ast is a snapshot. To reflect a source edit, build a fresh Ast::parse or drive tree_sitter::Parser::parse(&new_source, Some(&old_tree)) directly via the re-exported tree_sitter and feed the result through Ast::from_tree_sitter.
  • Everything on big_code_analysis::Node that is not listed in STABILITY.md. The wrapper is re-exported, and its traversal methods (kind, child_count, children, cursor, preorder, child, child_by_field_name, and so on) are pub, but only the STABILITY.md accessor list is shape-stable; the helpers that take a Search or Ancestors argument are not. For anything else, reach the tree-sitter Node through as_tree_sitter().root_node(), the documented seam.

メトリクスの選択

By default, every call to analyze computes the full metric suite: ABC, cognitive, cyclomatic, Halstead, LoC, MI, NArgs, NExits, NOM, NPA, NPM, tokens, and WMC (see Metrics for what each measures). That is the right default for the CLI, where the user has asked for the metrics, but it is heavyweight for callers that only want one number per file.

MetricsOptions::with_only(&[Metric]) restricts the walker to a subset of metrics. Unselected metrics are skipped at the per-node level (no T::Halstead::compute, no T::Cognitive::compute, and so on) and elided from the CodeMetrics serialization output.

具体例

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 was selected, so it carries real numbers.
    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 オブジェクトだけで、その他のメトリクスは存在しません。

メトリクス間の依存関係

Some metrics are derived: they consume the outputs of other metrics during the finalize step, or divide by a count another metric owns. Metric::dependencies is the source of truth:

メトリクス依存関係
Metric::MiLoc, Cyclomatic, Halstead
Metric::WmcCyclomatic, Nom
Metric::Cognitive, Metric::Nexits, Metric::NargsNom (the per-function average’s divisor)

with_only automatically adds the selected metrics’ dependencies. Selecting Mi therefore also computes Loc, Cyclomatic and Halstead, rather than calculating MI from default-zero inputs:

#![allow(unused)]
fn main() {
use big_code_analysis::{Metric, MetricsOptions};
let opts = MetricsOptions::default().with_only(&[Metric::Mi]);
// The stored set now contains Mi + Loc + Cyclomatic + Halstead;
// `MetricsOptions::with_metric_set` closes a `MetricSet` the same way.
}

最終的な選択集合は、得られた 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 以外のすべて」はどうするか

There is no built-in complement API: with_only takes a positive selection, not an exclusion list. The asymmetry is deliberate and keeps the dependency closure unambiguous. A positive list always grows through Metric::dependencies, whereas an exclusion list would need to decide what to do when the caller excludes a dependency of a metric they kept.

If you want “all except Halstead”, build the list explicitly. Because Metric is #[non_exhaustive], downstream crates can construct the variants but cannot exhaustively match on them. The conventional pattern is to enumerate the variants you want and accept that a future Metric variant will not silently opt you in:

#![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 を再び引き込んでしまうためです。
]);
}

Note the trap: keeping Metric::Mi re-adds Metric::Halstead through Metric::dependencies. To drop Halstead you must also drop Mi.

with_only を使うべき場面

  • Hot paths that need only one or two metrics per file. Halstead in particular owns its own per-space HalsteadMaps allocation and is the headline saving for an LoC-only run.
  • CI integrations that display one number (a cognitive-complexity gate, say) and want the rest of CodeMetrics to drop out of the cached JSON payload.
  • big-code-analysis を自前のレポートに組み込むライブラリ呼び出し側。この機能がなければ、自分の UI にすべてのメトリクスのフィールドが表示されてしまいます。

メトリクス単位の Cargo フィーチャ(コンパイル時の除去)は、このノブの対象外です。

言語ごとの Cargo フィーチャ

Every tree-sitter grammar this library bundles is gated behind its own Cargo feature. The default feature set is all-languages, so the default

[dependencies]
big-code-analysis = "2.4.0"

pulls every grammar in, matching the library’s historical behaviour and what the bca / bca-web binaries ship with. The cost is concrete: every grammar crate compiles when the library compiles, and every grammar’s parsing tables stay live in the final binary.

Library consumers that need only a subset of languages can opt out of the defaults and re-enable just the grammars they use.

具体例

Rust と TypeScript だけを解析する下流のサービスの例です。

[dependencies]
big-code-analysis = { version = "2.4.0", default-features = false, features = ["rust", "typescript"] }

この場合でもライブラリはコンパイルでき、LANG enum には引き続きすべてのバリアントがあり、analyze / Ast をはじめとするディスパッチ面は、有効化された言語に対してそのまま機能します。

サポートされるフィーチャ

利用できる言語別フィーチャは次のとおりです。各フィーチャは、対応する文法クレート(および言語別パイプラインが依存するヘルパー文法)を取り込みます。

フィーチャ取り込まれる文法クレート
bashtree-sitter-bash
ctree-sitter-c(+ c-family-helpers)。.c を担当する専用の C バリアント(#721 で追加)
c-family-helpersbca-tree-sitter-ccomment, bca-tree-sitter-preproc; internal, enabled automatically by c / cpp / mozcpp; gates the Ccomment / Preproc helper variants. Not meant to be selected directly
cpptree-sitter-cpp(+ c-family-helpers)。Cpp バリアント(#720 以降はアップストリーム文法)。Ccomment / Preproc ヘルパーバリアントも有効化します
csharptree-sitter-c-sharp
elixirtree-sitter-elixir
gotree-sitter-go
groovydekobon-tree-sitter-groovy
irulestree-sitter-irules(F5 iRules、Tcl 方言)
javatree-sitter-java
javascripttree-sitter-javascript
kotlintree-sitter-kotlin-ng
luatree-sitter-lua
mozcppbca-tree-sitter-mozcpp (+ c-family-helpers); opt-in Mozilla/Gecko C++ dialect, Mozcpp variant; owns no extensions, selected by name
mozjsbca-tree-sitter-mozjs
objctree-sitter-objc。Objc バリアント(Objective-C)です。.m を担当し、.mm(Objective-C++)は Cpp のままです
perltree-sitter-perl
phptree-sitter-php
pythontree-sitter-python
rubytree-sitter-ruby
rusttree-sitter-rust
tclbca-tree-sitter-tcl
typescripttree-sitter-typescript(Typescript と Tsx の両バリアントで使用)

Since #1376 the grammar crates are dependencies of big-code-analysis-ast, the parse and classification layer this crate is built on, so every feature above forwards to the same-named feature there and the grammars appear one level down in cargo tree. Nothing changes for a consumer: the feature names, the LANG enum and the whole analysis surface are unaffected, and enabling rust still links exactly one grammar.

包括フィーチャ all-languages は、この表のすべてのエントリを有効にします。bca-tree-sitter-* クレートは、アップストリームの Mozilla / コミュニティ文法のツリー内フォークですが、Rust のインポートパスはいずれも tree_sitter_<lang> のままです。リネームの理由と、利用側の呼び出し箇所を変更せずに済ませるワークスペースの package = ... エイリアスの仕組みについては、RELEASING.md を参照してください。

フィーチャが無効なときに起こること

The LANG enum keeps every variant defined regardless of the active feature set. Disabling a feature does not change the enum surface or any of the file-extension and Emacs mode declaration detection helpers. Selecting a LANG whose feature is off only affects the dispatch path.

Result を返すすべてのディスパッチエントリポイントは、無効状態を Err(MetricsError::LanguageDisabled(LANG)) として表面化させます。

  • analyze
  • Ast::parse / Ast::from_tree_sitter(および返された Ast の metrics / ops メソッド)
  • LANG::tree_sitter_language, which returns Result<tree_sitter::Language, MetricsError> rather than the bare Language the upstream project returned

呼び出し側は、ディスパッチャを経由せずに、コンパイル時に組み込まれた言語の集合を照会できます。

#![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);
    }
}
}

This pairs well with the get_language_for_file / guess_language helpers, which still hand back any LANG variant for a recognised extension. Callers walking a directory may want to skip files whose language is not enabled in the current build.

安定性

言語ごとのフィーチャ自体も契約の一部です。新しい言語フィーチャの追加はマイナーバンプでの追加的変更ですが、削除はメジャーバンプ(3.0)の破壊的変更です。デフォルトの all-languages は 2.x の間は恒久的であり、デフォルトビルドがカバーするバリアントが 3.0 より前に減ることはありません。この種の変更は、チェンジログで (breaking) として明示されます。

FuncSpace 結果の走査

FuncSpace is the tree the library hands back from analyze. The top-level node represents the whole file; its spaces field holds nested function, class, impl, trait and namespace spaces. Each node carries the same CodeMetrics payload, so any metric is available at any level.

FuncSpace の構造

最もよく使うフィールドは次のとおりです。

フィールド型説明
nameOption<String>呼び出し側が指定した識別子(トップレベル)またはシンボル名(ネスト時)
kindSpaceKindUnit, Function, Class, Impl, …
start_lineusize開始行(1 始まり)
end_lineusize終了行(1 始まり)
spacesVec<FuncSpace>ネストされたスペース
metricsCodeMetricsスペースごとのすべてのメトリクス値
suppressedSuppressionScopeソース内の抑制マーカー

SpaceKind is an enum; match on it to filter what you care about (Function only, or “anything that owns methods”).

start_line..=end_line は両端を含み、ファイルの終端を越えることはありません。また、ネストしたスペースの範囲は常に親の範囲の内側にあるため、これでソースをスライスしても安全です。

再帰的な走査

再帰はツリーの形をそのまま反映します。ここでは、認知的複雑度がしきい値を超えるすべての関数スペースを収集します。

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}");
    }
}

反復的な走査

For deep trees, prefer an explicit stack. Rust does not guarantee tail-call optimisation, and pathological generated code can be arbitrarily nested:

#![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 exposes each metric as its own Stats struct. Each struct offers integer-valued summary accessors plus per-space derived ones. A few patterns:

#![allow(unused)]
fn main() {
use big_code_analysis::FuncSpace;

fn summary(space: &FuncSpace) {
    let m = &space.metrics;

    println!("cognitive (this space):     {}", m.cognitive.cognitive());
    println!("cyclomatic (this space):    {}", m.cyclomatic.cyclomatic());
    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 にあります。

Do not rely on traversal order

ライブラリは 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 is #[non_exhaustive], so always include a _ arm when matching to stay forward-compatible with future variants.

エラーバリアントのパターンマッチ

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: reserved; not produced today. The metric walker always pushes a synthetic top-level Unit FuncSpace before walking the AST, so every parse (including empty, whitespace-only, and comment-only input) returns Ok(FuncSpace { kind: Unit, .. }). The variant is kept for a future walker change that could let the state stack legitimately drain to empty.
  • LanguageDisabled(LANG): the requested LANG is not enabled in this build. Every dispatch entry point produces it when the caller selects a LANG whose per-language Cargo feature is off. The default feature set (all-languages) compiles every grammar in, so you only see this variant after opting into a narrower set (--no-default-features --features rust,…).

MetricsError has no ParseHasErrors or NonUtf8Path variant. Since it stays #[non_exhaustive], a future strict-parsing or strict-identifier mode can introduce them without a breaking change. Non-UTF-8 paths are already handled up front: the recommended analyze entry point takes a caller-supplied Source::name (Option<String>), so a lossy path is never round-tripped in the first place. The file-backed Ast::from_path is the one entry point that reads a path itself, and it returns its own #[non_exhaustive] FromPathError (Io, NonUtf8Path, Skipped(SkipReason), UnknownLanguage, Parse(MetricsError)) rather than widening MetricsError. Skipped carries the same SkipReason the bca walker prints when it skips an empty, tiny, UTF-16 or non-UTF-8 file, so a caller can say which gate declined the file; it supersedes the deprecated Unreadable.

tree-sitter は常に「ノー」と言うわけではない

Most parse errors do not surface as Err(_). Tree-sitter is an error-recovering parser: it produces a tree even for syntactically broken input, marking the bad regions with ERROR nodes. The metric walk computes numbers over the recovered tree. That means:

  • Garbage in, numbers out. Feeding C++ source to LANG::Python generally produces an Ok(FuncSpace) whose metrics are nonsense. Select the right language (via guess_language, for example) before trusting the result.
  • Partial files score. A truncated file with an unterminated brace still returns Ok(FuncSpace). The metrics reflect the recovered tree, not the intended source.

入力が正常にパースされたかどうかを知る必要がある場合は、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(),
    )?)
}
}

If you want a project-specific error type, an explicit From impl keeps call sites clean while letting you attach extra context (file path, language guess, and so on).

警告はエラーではない

The library writes warnings to stderr for non-fatal issues (malformed bca: suppression markers, and a ConcurrentRunner input path that is not a regular file). They do not abort the walk and they do not flip Ok to Err. If you run embedded inside a server or library and need to capture those warnings, redirect stderr at the process level; the library does not expose a programmatic warning sink.

安定性とバージョニング

big-code-analysis is on the 2.x line (currently 2.4.0). The full stability contract lives in STABILITY.md at the root of the repository. That file is the source of truth and is updated alongside the changelog at every release; this page is the summary for library consumers.

The headlines:

  • パッチ・マイナーバンプをまたぐ形状の安定性。 STABILITY.md § “What is stable in shape” に列挙されたすべての公開型と関数シグネチャは、2.x 系列を通じて維持されます。追加的変更(新しい項目、新しい LANG バリアント、新しい MetricsError バリアント、新しい言語フィーチャ)はマイナーバンプで許可されます。破壊的な形状変更は次のメジャーバンプまで保留され、チェンジログ の 3.0.0 セクションに (breaking) として記載されます。
  • No value stability guarantee within 2.x. A grammar pin bump or a bug fix in a metric definition can shift any metric value on any file in any direction, even across a patch bump. Each such drift is flagged in the changelog. Pin to an exact version (big-code-analysis = "= 2.4.0") if you need bit-for-bit reproducibility across runs.
  • The minimum supported Rust version (MSRV) is 1.94. Bumping the MSRV is treated as a minor-bump event and is flagged in the changelog under (breaking); see STABILITY.md § MSRV policy.
  • Escape hatches. The Node wrapper exposes its tree_sitter::Node through Node::as_tree_sitter(), and the tree_sitter crate is re-exported as big_code_analysis::tree_sitter. Node also carries the accessors the metric walk itself uses (kind, kind_id, child, children, child_by_field_name, utf8_text, the position accessors). Their signatures are shape-stable, but the values they return (node kinds and kind_id numbers) belong to the pinned grammar and move when it does. Anything reached through those seams follows the pinned tree-sitter version rather than our own SemVer. See STABILITY.md § Escape hatches before depending on them.
  • big-code-analysis-ast is not a stability surface. Since #1376 the parse and classification layer lives in that separate published crate, which this one pins exactly and releases in lockstep. It appears in your cargo tree. Nothing in it carries a promise of its own: depend on big-code-analysis and use what it re-exports.

3.0 に向けて

The breaking changes once staged for 2.0 shipped in 2.0.0: the #[non_exhaustive] markers on the open public enums and the per-metric Stats structs, the serialized-key normalization, the integer-metric u64 shift, the language-dispatch and grammar defaults, the Python and REST surface changes, and a consolidated metric-value re-baseline folding in the drift accumulated since 1.0. The path-positional callback dispatch (action / the Callback trait), the free metrics / metrics_with_options / get_function_spaces / metrics_from_tree / get_ops functions, and the generic Parser<T> / ParserTrait plumbing were removed at the same time; analyze and Ast are now the single analysis seam. Since #1376 Parser, ParserTrait and the per-language parser and tag types live in big-code-analysis-ast, which this crate does not re-export.

No 3.0 is scheduled. The #[non_exhaustive] markers added at 2.0 keep most future additions (new enum variants, new fields) non-breaking, so 2.x is the surface you should depend on. See STABILITY.md § On the 3.0 horizon.

Python バインディング

big-code-analysis ships first-party Python bindings, built with PyO3 and maturin, that expose the same metric pipeline as the Rust library and the bca CLI: same JSON shape, same numeric formatting, same language coverage. This chapter is for a Python developer deciding whether to use the bindings and then finding the page for each workflow.

import big_code_analysis as bca

result = bca.analyze("src/main.rs")
if result is not None:
    print(result["metrics"]["cyclomatic"]["sum"])

The bindings cover the analysis workflows: file and in-memory analysis (big_code_analysis::analyze), the FuncSpace result as a typed dict, metric selection, AST traversal, SARIF output, and the VCS metrics. The metrics are computed by the same Rust code, so the numbers are identical. Python also exposes the language-detection helpers. Some lower-level Rust facilities, including the C preprocessor pass and the output writers, have no Python counterpart. Pick whichever language fits your pipeline.

Python を選ぶべき場面

  • You work in a data-pipeline stack (pandas, Jupyter, Airflow, dbt, Polars) and want metric records as dict or DataFrame rows without shelling out to the CLI.
  • You integrate with a Python-native security tool that consumes SARIF; see SARIF output.
  • You build a code-quality dashboard on a Python web framework (FastAPI, Django).

If you only need a one-shot quality report from the command line, the bca CLI is the simpler tool; see Commands → Metrics.

If you embed the analysis in a long-running Rust program, the Rust library has lower overhead.

本章の内容

The headline example on each page is embedded verbatim from an importable file under big-code-analysis-py/examples/ and run end-to-end by big-code-analysis-py/tests/test_book_examples.py, so a renamed kwarg or a removed function on the primary path fails CI before it can rot the docs. The shorter snippets around each embedded example (logging recipes, regex parsing of the errno suffix, the asyncio anti-pattern, the pandas one-liner) are inline and deliberately not test-pinned. Treat the embedded blocks as the canonical reference when the two disagree.

インストール

The bindings are distributed as a prebuilt binary wheel. Install with pip, or with your lockfile manager of choice (uv, poetry, pdm):

pip install big-code-analysis

Python >=3.12 is required. The compiled extension uses CPython’s stable abi3 surface (abi3-py312), so one wheel covers 3.12, 3.13, 3.14, and every future minor release without a per-version wheel build.

Wheel マトリクス

CI は現在、以下のターゲット向けの wheel を公開しています。お使いのプラットフォームが記載されていない場合は、ソースからビルドしてください。

プラットフォームアーキテクチャ
Linux(manylinux_2_28)x86_64, aarch64

The wheel matrix is defined in .github/workflows/python-wheels.yml. manylinux_2_28 requires glibc >= 2.28 (RHEL 8 / Debian 10 / Ubuntu 18.10 and newer); older distributions (RHEL 7 / CentOS 7, glibc 2.17) need to build from source. macOS and Windows wheels are not yet shipped, so pip install on those platforms falls back to a source build today.

インストールの確認

python -c "import big_code_analysis as bca; print(bca.__version__)"

The printed version equals [workspace.package].version in the Rust workspace’s Cargo.toml. The bindings and Rust library share a version.

ソースからのビルド

お使いのプラットフォームに合う 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 builds the Rust extension in place and installs it into the active venv so import big_code_analysis resolves locally; no separate pip install -e . step is required. The --release flag turns on the optimiser; omit it during development for faster rebuilds.

You also need:

  • 安定版の Rust ツールチェーン(MSRV: 1.94)。rustup からインストールしてください。
  • A C compiler (used by the tree-sitter grammar crates).
  • CPython の開発用ヘッダー(Debian / Ubuntu では python3-dev)。

次のステップ

Walk through the quick-start to compute your first metric, or skip ahead to batch processing if you are wiring this into a pipeline over many files.

クイックスタート

This page shows the least code needed to compute metrics for one source file: install the package, call analyze, read a value. It assumes Python 3.12 or newer and pip.

1. パッケージをインストールする

pip install big-code-analysis

wheel マトリクスとソースからのビルド手順についてはインストールを参照してください。

2. ファイルを分析する

bca.analyze(path) returns a dict matching the JSON bca metrics --format json emits for the same file: same field order, same numeric formatting, same shape.

"""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]))

Three details matter here:

  • analyze returns None for any file the CLI walker would skip: one that is three bytes or fewer (treated as empty), one whose leading window is not valid UTF-8 (treated as binary), or, with the default skip_generated=True, one matching the walker’s is_generated predicate (a leading @generated, DO NOT EDIT, or GENERATED CODE marker). Handle the None before reaching into result["metrics"].
  • The returned object is a plain dict at runtime, so you can serialise it with json.dumps, ship it to a downstream service, or feed it to flatten_spaces for tabular consumers. Type checkers see it as the FuncSpaceDict TypedDict (generated from the Rust wire shapes), so nested metric access checks statically under mypy and pyright without casts.
  • Language detection mirrors the CLI: path extension first, then an Emacs or Vim mode line, then the shebang. Pass bca.analyze_source(code, language) when the source is already in memory.

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 になります(メモリ上のバッファにはパスが関連付けられないためです)。

次のステップ

バッチ処理

bca.analyze_batch(paths) runs the same analysis as bca.analyze over every path in an iterable and never raises on per-file errors: each result element is an analysis dict, a bca.AnalysisFailure describing the failure, or None. Results preserve input order, so zip(inputs, results) lines up by index when no path is skipped. This page is for a caller analysing many files who wants one list back instead of a try/except per file.

analyze_batch shares analyze’s keyword-only options (exclude_tests, allow_lossy_path, skip_generated with default True, metrics, vcs, vcs_per_function, and mi_charges_docs), so both entry points treat a file the same way. The list shape matches the [bca.analyze(p) for p in paths] comprehension only under skip_generated=False; with the default, the comprehension keeps a None per skipped file while the batch drops the slot.

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`, `skipped`, `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
    skipped = 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}")
        elif result is None:
            # The read gate declined this file; the slot is held open
            # so the strict zip above stays aligned (#1238).
            skipped += 1
            print(f"  skip {path}: nothing to parse (empty or binary)")
        else:
            ok += 1
            sloc = result["metrics"]["loc"]["sloc"]
            print(f"  ok   {path}: sloc = {sloc:.0f}")

    return {
        "ok": ok,
        "errors": errors,
        "skipped": skipped,
        "total": len(materialised),
    }

The contracts to rely on:

  • AnalysisFailure is returned, not raised. It is not an Exception subclass; isinstance(slot, bca.AnalysisFailure) is the discriminator.
  • paths is consumed lazily, so generators work. To keep the input around for zip, materialise it into a list first.
  • With the default skip_generated=True, a generated file is skipped and produces no element, so the result list can be shorter than the input. This matches single-file analyze, which returns None for a generated file. Pass skip_generated=False to guarantee one element per input (the pre-2.0 default). The default flipped at 2.0 so that switching between analyze and analyze_batch no longer silently changes generated-file handling.
  • A file that cannot be parsed at all still holds its slot under skip_generated=False, as None. The read gate shared with the CLI walker declines a file of three bytes or fewer, one carrying a UTF-16 BOM, and one whose leading window is not valid UTF-8. That gate is unconditional, so before #1238 those files shrank the list even with the flag off and the zip above mis-paired every later entry. None is the same value single-file analyze returns for them, and bca.to_sarif skips it, so a batch list can be passed straight through.

ディレクトリの走査:analyze_paths

analyze_batch analyses an explicit list of paths verbatim. To find the source files first (“analyze my repo”), use analyze_paths (#658), which reuses the CLI’s gitignore-aware walker:

import big_code_analysis as bca

results = bca.analyze_paths("path/to/repo", include="*.py")

Each positional seed may be a file or a directory. Directories are walked honouring .gitignore, the include / exclude globs (a single glob string or a sequence; a leading ./ is optional, so dir/** ≡ ./dir/**), and the generated-file filter. A seed naming a file directly is always analysed regardless of exclude, because an explicit request overrides ignore-style rules; include still narrows it by basename. respect_gitignore=False opts into walking ignored files.

The result is the same list[FuncSpaceDict | AnalysisFailure | None] shape and never-raise contract as analyze_batch, and it forwards the same exclude_tests / allow_lossy_path / skip_generated / metrics / vcs / vcs_per_function / mi_charges_docs kwargs. A seed that does not exist leads the result list as an AnalysisFailure (error_kind="IoError") rather than being dropped.

The None slots reach this entry point too, under skip_generated=False, one per discovered file the read gate declined. Results follow the walk, so there is no caller-supplied ordering to zip against; the slots exist so that a file the walk found but could not analyse stays visible in the output. On a tree with many binary assets that is a lot of Nones; filter them with [r for r in results if r is not None] if you only want records.

変更履歴メトリクスの付与

analyze_batch and analyze_paths accept the same vcs=True / vcs_per_function=True kwargs as single-file analyze (#670). The batch builds one history index and blame engine per containing repository and reuses it across that repository’s files, amortising the walk that a comprehension over analyze(p, vcs=True) would repeat per file. A VCS failure on one file leaves its AST metrics intact (it never becomes an AnalysisFailure); a file outside any repository gets no vcs block. To rank a whole repository rather than attach per-file numbers, use the dedicated big_code_analysis.vcs surface instead.

並列実行

analyze_batch has no built-in concurrency; it is a sequential sweep. For parallelism, fan the per-file analyze call out across a thread pool:

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’s Python::detach releases the GIL across each file’s read and tree-sitter parse, so the threads do not serialise on the interpreter lock.

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

If you need typed dispatch (FileNotFoundError, PermissionError, and so on), call bca.analyze(path) per file instead of analyze_batch; single-file analyze raises the canonical OSError subclass. See Error handling.

フラットレコードの反復処理

bca.flatten_spaces(result) walks the nested FuncSpace tree in pre-order and yields one flat, scalar-only dict per node, ready for sqlite3.executemany, pandas.DataFrame.from_records, or any other tabular consumer. This page is for a caller loading metrics into a database or a DataFrame.

Metric keys use the same dotted convention as the CLI’s CSV writer (cyclomatic.modified.sum, halstead.volume, loc.lloc_average, and so on). Identity keys (path, name, kind, start_line, end_line, parent_name, depth) are added on every record.

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]}")

The iterator is lazy and single-use: it walks the input once without materialising the whole list. A second iteration of the same iterator yields nothing; call list() once if you need to re-iterate.

Pandas

flatten_spaces is the natural input to pandas.DataFrame.from_records. pandas is not a dependency of the bindings; install it separately if you want the DataFrame view.

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 の比較

The flat-record schema is mostly aligned with the CLI’s CSV writer, with a few intentional deltas:

  • 識別列は、こちらでは name / kind を使いますが、CSV ライターは space_name / space_kind を使います。フラットレコードには parent_name / depth も追加されますが、CSV ライターにはありません。
  • The per-space cognitive.value, cyclomatic.value, cyclomatic.modified.value, abc.value and nargs.value leaves (the numbers bca check thresholds against) are present here, as in the JSON shape, but absent from the CSV header.
  • tokens.* は JSON の形(tokens.tokens、tokens.average、tokens.min、tokens.max)にフラット化されます。CSV と異なるのは合計のリーフだけで、CSV では tokens.sum と綴られます。average / min / max のリーフは現在一致しています(#590)。CSV と完全に揃える必要がある場合は、コンシューマー側で合計のリーフをリネームしてください。

Anonymous spaces (Rust closures, JavaScript function expressions and arrows) keep their name == "<anonymous>" marker verbatim; flatten_spaces does not normalise.

注意点

  • parent_name alone cannot disambiguate same-named siblings nested under different parents (two Inner classes under two different outer classes both surface as parent_name == "Inner" for their own children). Pair it with depth and source-order position, or rebuild the qualified name in your consumer, if you need a fully-qualified path.
  • Do not mutate the input result while iterating: the walker keeps references into it, so mutations to not-yet-yielded subtrees show up in later records.
  • 存在しないメトリクスのサブツリーはキーを生成しません(None ではなく欠落)。これはメトリクス選択の「Halstead 無効時」のエッジケースと一致します。
  • flatten_spaces raises TypeError if the input is not a mapping. Filter out the None returns from bca.analyze (generated files under skip_generated=True) before passing, and likewise the None slots analyze_batch emits under skip_generated=False, which mark a file the read gate declined to parse.

メトリクス選択

Pass metrics=[…] to compute only a subset of the metric suite. metrics=None (the default) computes everything. Unrequested metrics are absent from the result dict, not present with None placeholders. This page is for a caller who wants fewer metrics, either to save time on a large batch or to narrow the output.

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 and bca.analyze_batch honour the same kwarg; the batch applies the selection uniformly to every file. Validation runs before any file I/O: an empty list or unknown name raises ValueError immediately and never returns an AnalysisFailure slot for what is really a caller bug.

正規名

The full set is available as a tuple of MetricName members. Each member is a StrEnum, so it is a str: "halstead" in bca.METRIC_NAMES works, and bca.MetricName.HALSTEAD == "halstead" is True. Pass either a plain string or a member to 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 で目にするスラッグから乖離することはありません。

Accepted names

metrics= takes every name bca metrics --metrics takes, so a name copied from bca list-metrics or from a bca check --threshold limit works unchanged:

SpellingExampleSelects
Family name, from METRIC_NAMES"loc", "cyclomatic"that family
bca list-metrics row name"sloc", "dloc"the family that owns the row, loc
Dotted threshold id"cyclomatic.modified", "mi.original"that id’s family

Selection works per family, because the walk computes whole families: metrics=["dloc"] returns the full loc block, sloc and all, exactly as metrics=["loc"] does. METRIC_NAMES lists only the family names, which are also the JSON keys, so name in bca.METRIC_NAMES does not tell you whether metrics= accepts name.

Names are case-sensitive lowercase. An unknown name raises ValueError naming the closest accepted names, then listing every family and row name, as the CLI does:

ValueError: unknown metric: dlocc; did you mean `dloc`?; known metrics: abc, blank, cloc, …

The canonical spelling for the exit-point metric is "nexits" everywhere (the enum Display, METRIC_NAMES, and the JSON output key). The legacy "exit" alias was retired at 2.0 and now raises ValueError like any other unknown name. Duplicates are silently collapsed.

依存関係

Selecting a metric that reads another metric’s result computes that one too. cognitive, nargs and nexits each report a per-function average whose divisor is the function count nom supplies.

メトリクスJSON キー引き込まれる依存メトリクス
LoClocnone
循環的複雑度cyclomaticnone
認知的複雑度cognitivenom
Halsteadhalsteadnone
ABCabcnone
nargsnargsnom
nomnomnone
npanpanone
npmnpmnone
nexitsnexitsnom
tokenstokensnone
保守容易性指数miloc, cyclomatic, halstead
クラスごとの重み付きメソッド数(Weighted Methods per Class)wmccyclomatic, nom

パフォーマンスのトレードオフ

Computing the full suite is the default because it is what the CLI does. Selecting fewer metric families skips their computations but still runs the tree-sitter parse and walks the AST. Benchmark a representative batch to measure the saving for your workload.

Unrequested metrics are absent from the result. Code that unconditionally indexes into result["metrics"]["mi"] raises KeyError if you opted out of mi; guard with if "mi" in result["metrics"] or use .get("mi").

関連項目

  • Batch processing: metrics= applies uniformly to every file in a batch; validation runs once, before the input is iterated.
  • SARIF output: threshold names are independent of the metrics= selection. You can request metrics=["loc"] and still gate on cyclomatic thresholds, but the SARIF has no findings for the dropped metrics.
  • Flat-record iteration: flatten_spaces emits no keys for metrics absent from the source dict, so a metrics= selection narrows the flattened columns.

AST トラバーサル

bca.analyze(...) gives you metrics. When you need the syntax tree itself (to find every function definition, pull a docstring, or port a py-tree-sitter matcher), parse once into an Ast and walk it with lazy Node handles. This page assumes you know what a tree-sitter parse tree is; the Node API mirrors py-tree-sitter’s.

Ast ハンドル

bca.Ast.parse(code, language) (or bca.Ast.from_path(path)) parses the source once and hands back a handle you can draw both metrics and the tree from, instead of parsing twice (once in py-tree-sitter, once in 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() is a lazy pre-order iterator over a node and its descendants; descendants_by_kind(kinds) collects the matches in one pass; and ast.find(filters) searches the whole tree, accepting the same vocabulary as bca count (all, function, call, comment, string, error, an exact kind, or a numeric kind_id):

# ファイル内のすべての関数名を遅延評価で取得する。
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"])

These have Rust counterparts, Node::preorder and Node::descendants_by_kind, so library callers get the same helpers.

座標

ノードは自身の位置をあらゆる語彙で報告するため、手作業での変換は不要です。

アクセサ意味
start_byte / end_byteast.source へのバイトオフセット
start_point / end_point0 始まりの (row, col)(py-tree-sitter 互換)
start_line / end_line1 始まりの行番号
spanthe {start_line, start_col, …, start_byte, end_byte} dict dump() emits: lines and columns are 1-based; byte offsets are 0-based and half-open

したがって 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 つあります。

  1. Memory. dump() builds one dict (with span, value, children) per node: fine for small files, costly for large ones. A Node walk allocates only the handles you touch.

  2. Taxonomy. A Node’s kind is the raw grammar kind. dump() kinds pass through bca’s Alterator and are curated; for example, string-literal nodes are renamed to "string" and flattened (their grammar children removed). So the two surfaces intentionally disagree on altered nodes:

    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++ preprocessor. The Python Ast takes no preprocessor inputs: Ast.parse accepts only code, language and name, and Ast.from_path does not run the C/C++ preprocessor pass. So ast.source, and every node’s byte offsets, always index into the bytes the parser was given: the code you passed to parse, or the file as from_path read it (through the same EOL-normalising reader analyze uses). The expanded-source behaviour described in the Rust chapter applies only to a Rust Source built with preprocessor results.

次のステップ

  • Metric selection: compute only the metrics you need from the same parse.
  • The CLI’s dump and count commands: the shell-level equivalents of dump() and find().

SARIF 出力

bca.to_sarif(result, *, thresholds=None) renders an analysis result (or an iterable of them) into a SARIF 2.1.0 JSON document, ready for upload to GitHub Code Scanning or any other SARIF consumer. The output is produced by the same Rust writer that backs bca check --report-format sarif, so the schema URL, tool driver name / version, and rule descriptions match the CLI byte-for-byte.

Findings match in order as well as in content. Both surfaces sort their findings by path, then start line, then metric name (the order bca check applies after its walk), and findings tying on all three keep the depth-first source order of the space tree. For the same files and thresholds the two results arrays therefore line up entry for entry against bca check --no-suppress. to_sarif compares raw metric values, so it applies none of the in-source suppression markers bca check honours by default (each marked space keeps its suppressed key, for a caller that wants to filter), no baseline, and no [check] exclude globs.

“The same files” means a unique file set. The CLI folds repeated path seeds together, so bca check -p a.py -p a.py analyses a.py once and emits one finding per breach. analyze_batch instead returns one result per input, and to_sarif renders every result it is handed, so to_sarif(analyze_batch([a, a]), ...) emits each finding twice. Deduplicating in the binding would be wrong, because two distinct results may legitimately share a name (analyze_source takes the caller’s). Hand to_sarif a unique file set when comparing the two documents positionally.

Examples on this page import the package as bca (import big_code_analysis as bca). A bare bca in a shell command is the CLI binary.

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。
  • Any iterable yielding such dicts, bca.AnalysisFailure instances, or None (the natural shape of bca.analyze_batch’s return value). AnalysisFailure and None entries are skipped silently; they represent files for which no record was emitted, not findings.
  • A scalar None, the documented return of bca.analyze for a skipped file; it yields an empty SARIF run.

しきい値

Accepted threshold names are the library’s metric_catalog::METRICS table in src/metric_catalog.rs, the same table the CLI’s --threshold reads:

  • cognitive, cyclomatic, cyclomatic.modified
  • halstead.volume、halstead.difficulty、halstead.effort、halstead.time、halstead.bugs
  • loc.sloc, loc.ploc, loc.lloc, loc.cloc, loc.blank, loc.dloc
  • nom, tokens, nexits, nargs
  • mi.original、mi.sei、mi.visual_studio
  • abc, wmc, npm, npa

Names resolve by the rule bca check --threshold applies, through the library’s metric_catalog::resolve_threshold_metric, so a name copied from a CLI invocation works unchanged. A bare loc sub-metric name (sloc, ploc, lloc, cloc, blank, dloc, the spelling bca diff --metric buckets by) stands for its dotted id, so {"sloc": 1200} and {"loc.sloc": 1200} render the same document. A family head with no scalar of its own (mi, halstead, loc) raises ValueError naming the ids to choose from:

ambiguous metric "mi": this family has no single threshold scalar; did you mean one of: mi.original, mi.sei, mi.visual_studio?

One id under both spellings ({"sloc": 1200, "loc.sloc": 1500}) is also a ValueError, as the same pair is in a bca.toml table: it is one metric, so set it once. Any other name raises ValueError listing the accepted set, with a “did you mean” hint when an id is close, so a typo fails fast instead of silently producing an empty SARIF run.

Limits are validated by the same rules bca check applies, so a dict that the binding accepts is one the CLI accepts too. Most metrics are counts: the limit is a ceiling, a value strictly above it is a finding, and the limit must be a finite, non-negative number. The mi.* Maintainability Index family is the other way round. Its limit is a floor, and a value strictly below it is the finding. mi.original and mi.sei have no true zero and go negative on large functions, so {"mi.original": -50} is a legal floor. A limit that parses but can never do what you meant raises a UserWarning instead of a ValueError. mi.visual_studio is clamped to [0, 100], so {"mi.visual_studio": 0} warns limit mi.visual_studio=0 cannot fire: 0 is the metric's floor; gate mi.original instead. A NaN or infinite limit raises ValueError, and a bool limit raises TypeError rather than reading False as 0. The CLI side of these rules is described under bca check.

thresholds=None (the default) and thresholds={} both produce a well-formed SARIF document with empty results and rules arrays. This matches bca check, which applies no implicit limits: every run supplies its own, from --threshold or a bca.toml (which bca init scaffolds with a starting table).

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 アラートへのアップロードはこのアクションが担います。

Which spaces produce findings

to_sarif emits a finding when a space’s own value breaches a limit and the metric applies to that space, exactly matching bca check --report-format sarif. LoC limits apply to file units; nom, wmc, npm, and npa apply to containers; the remaining limits apply to functions. For most metrics the JSON headline at a space already is that space’s own value. The five subtree-aggregate metrics (cyclomatic, cyclomatic.modified, cognitive, abc and nargs) also expose a sum / magnitude / total rolled up across child spaces; the binding reads their per-space value field instead, so it reports an interior breach (for example a function whose own complexity breaches even though a nested closure’s does not) without being fooled by the larger aggregate. For nargs that means a function is gated on its own parameter list, exactly as bca check has been since #1196; a closure with its own space produces its own finding rather than inflating the enclosing function’s.

Unit findings carry logicalLocations: [{"fullyQualifiedName": "<file>"}]. Every other space carries its qualified symbol. Within that symbol, a closure/lambda (the <anonymous> name every grammar emits) and the None-name parse-failure case both collapse to <anon@L{start_line}>, matching the CLI’s space_segment.

関連項目

  • Batch processing: the natural source of input iterables for to_sarif; AnalysisFailure entries are skipped silently.
  • Metric selection: threshold names are a closed set independent of metrics=. Metrics omitted by metrics= produce no findings; other selected metrics can still produce findings for their supplied thresholds.
  • Error handling: the typed exceptions to_sarif raises for bad caller input (TypeError / ValueError).

変更履歴(VCS)メトリクス

The big_code_analysis.vcs submodule ranks files and scores commits by change-history risk: signals derived from git history rather than the source AST. It is the Python analogue of the bca vcs CLI command, and the same Rust engine backs both, so the returned dicts match the CLI’s structured output field-for-field. This page is for a caller who already knows what the signals mean and wants the Python call shapes.

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) scores a git-style unified diff that has not been committed yet, the shape a pre-commit hook or code-review bot works with. It returns a 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']}")

The trend contains points samples, with a minimum of 2. They cover the interval specified by span (default 12mo) and end at options.as_of. The files map aligns each file’s series 1:1 with as_of_points, with a None element where the file did not yet exist. The deltas summary splits files into improved and regressed lists; top_deltas trims each list and top caps how many files are kept.

共通オプション

The three repository-walking entry points (rank, trend, and commit) accept the same vcs.Options object, so one configuration can drive a rank plus a trend pass without restating the common knobs. Every field is keyword-only and optional, and the defaults reproduce the bca vcs CLI defaults, so Options() matches the default ranking.

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 selects which files to rank: "metrics" (the default: only files bca computes metrics for), "all" (every tracked text file), a comma-separated extension allow-list ("rs,py"), or a Sequence[str] of extensions (["rs", "py"]).
  • as_of pins the reference “now” for reproducible snapshots, as either a datetime or a string (RFC 3339, @unix, or a git date). With as_of pinned, the ranking is computed as it stood at that moment, not against the wall clock.
  • cache_dir (on rank, not Options) accepts a str or any os.PathLike; a pathlib.Path passes straight through.

The history-walk toggles mirror the CLI flags: full_history, include_merges, follow_renames (default True), exclude_bots (default True), and bot_pattern to override the bot-author regular expression. bus_factor_threshold sets the coverage fraction for the bus-factor flag (default 0.5), and emit_author_details includes SHA-256-hashed canonical author identities. author_hash_key (requires emit_author_details) hardens those digests into a keyed HMAC-SHA256, the same opt-in described under Author-detail privacy.

キャッシュ

vcs.rank keeps a persistent cache of each history walk, on by default. A cache hit is bit-identical to a fresh walk, and the time windows are recomputed against the current moment on every run. The caching section lists what invalidates an entry, and the one walk input that does not.

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")     # ディレクトリを上書き

By default the cache lives under the platform cache directory. Author identities are stored only as their SHA-256 digests, never plaintext. Hashing is pseudonymization, not anonymization: the digests are recoverable against a candidate email set. See Author-detail privacy.

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)を頂点とする型付き例外階層を送出します。完全な分類と、どの呼び出しがどの型を送出するかについてはエラー処理を参照してください。

関連項目

  • Change-history (VCS) metrics: the CLI chapter, with the signal definitions, the composite risk-score formula, and the defect-prediction literature behind them.
  • Batch processing: analyze_batch(..., vcs=True) attaches a per-file vcs block while sharing one history index per repository.
  • Error handling: the VCS exception taxonomy.

エラー処理

Single-file analysis raises exceptions for file and analysis failures. Batch analysis returns those failures as AnalysisFailure values and continues. Invalid caller arguments (ValueError for a bad value, TypeError for the wrong type) still raise and abort the batch. This page lists every exception each entry point can raise and the one value type a batch returns instead.

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.UnsupportedLanguageErrorValueError未知の拡張子で、シバン / emacs モードにも一致しない
bca.ParseErrorValueErrortree-sitter rejected the source
ValueError(直接)(builtin)allow_lossy_path=False(デフォルト)での非 UTF-8 パス
OSError とそのサブクラス(builtin)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)発生条件
FileNotFoundError2 (ENOENT)パスが存在しない。
PermissionError13 (EACCES)呼び出しユーザーに読み取り権限が与えられていない。
IsADirectoryError21 (EISDIR)パスがディレクトリを指している。

On Windows the Win32 code is on err.winerror, and err.errno holds the POSIX code CPython maps it to, so the same branches apply: a missing file or a missing parent directory is a FileNotFoundError. Windows reports a directory read as access denied, so a directory raises PermissionError there rather than IsADirectoryError.

ファミリー全体を捕捉して 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": extension and shebang / emacs-mode resolution both came up empty.
  • "ParseError": tree-sitter rejected the input, or (rarely) a Rust-side JSON serialisation of the result failed. The serialisation case is prefixed with internal: serialization error: in the error string; check for the prefix when the distinction matters, since re-reading the file does not recover from a serialisation failure.
  • "IoError": a filesystem read or path-encoding failure. The closed taxonomy folds non-UTF-8 paths in here rather than adding a distinct fourth value.

For "IoError" instances the underlying OS errno is preserved in the error string via Rust’s default formatting ("<msg> (os error <N>)" on Unix). Parse it with a regex if you need it for retry classification:

import re

match = re.search(r"\(os error (\d+)\)$", slot.error)
errno = int(match.group(1)) if match else None

If you need typed OSError subclasses, call bca.analyze per file instead of analyze_batch; single-file analyze raises FileNotFoundError / PermissionError / IsADirectoryError directly.

バッチにおけるプログラマーエラー

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.NotARepositoryErrorbca.VcsErrorrepo_path が git 作業ツリーの中にない
bca.InvalidRevisionErrorbca.VcsErrorreference / commit を解決できなかった
bca.InvalidDiffErrorbca.VcsErrorvcs.score_diff に渡された diff が不正である
bca.VcsEnvironmentErrorbca.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) is the exception to the NotARepositoryError rule: a file outside any repository yields no vcs block rather than raising, so only the option-parsing VcsError reaches the caller from that path.

ログ出力のレシピ

A small logging helper for batch output keeps successes and failures aligned without bespoke formatting:

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
            )
        elif slot is None:
            # Nothing to parse: three bytes or fewer, a UTF-16 BOM, or a
            # binary leading window. The slot is held open so the zip
            # above stays aligned (#1238); it is not a failure.
            log.info("skip %s: empty or binary", path)
        else:
            log.info(
                "ok %s sloc=%s", path,
                slot["metrics"]["loc"]["sloc"],
            )

関連項目

  • Batch processing: the never-raise contract that routes per-file failures into AnalysisFailure slots.
  • Async patterns: asyncio.gather(..., return_exceptions=True) is the async-side equivalent of the batch contract; per-task exceptions land in the result list instead of cancelling the whole gather.
  • Quick start: the single-file analyze path that raises typed OSError subclasses.

非同期パターン

bca.analyze is CPU-bound: the work is a tree-sitter parse plus the metric passes, both of which release the GIL on the Rust side via PyO3’s Python::detach. The canonical pattern for calling it from an asyncio application is therefore 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 is a synchronous Python function backed by synchronous Rust code; there is no await boundary inside it. Wrapping it in asyncio.to_thread:

  1. 呼び出しをデフォルトのスレッドプールにスケジュールします。
  2. Lets other coroutines progress while the parse and metric pass run.
  3. Returns the result to the calling coroutine when done.

Because the Rust side releases the GIL across the heavy work, several to_thread(bca.analyze, ...) calls run in parallel. This is not co-operative I/O multiplexing; it is real multi-core use, capped by the thread pool’s size.

カスタムエグゼキューター

to_thread always uses the event loop’s default executor. For a tighter cap on the worker count, use loop.run_in_executor with a purpose-built pool instead:

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)
        )

Start with a worker count near the CPUs available to the process. Benchmark your workload before increasing it; additional workers compete for CPU time.

結果のストリーミング

asyncio.as_completed lets you start consuming results as soon as the first analysis finishes, which helps when the per-file work varies widely in cost (a 5 KB file against a 500 KB generated bundle):

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

The finally block cancels pending asyncio tasks; as_completed does not cancel them itself when the caller returns early. Analyses already running in worker threads continue until they finish; cancellation does not interrupt the Rust work.

アンチパターン:コルーチン内での bca.analyze の直接呼び出し

# これはやってはいけません。
async def bad(path: str) -> dict | None:
    return bca.analyze(path)  # 呼び出しのたびにイベントループをブロックします

async def does not make the body asynchronous. Without to_thread or an explicit executor, every coroutine that calls bca.analyze stalls the event loop for the full duration of the parse: other tasks waiting on I/O, timers, or queues all freeze until the parse returns. The to_thread wrapper is one line and is the difference between a responsive server and a single-threaded one.

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) を選んでください。

開発者ガイド

How to build, test, lint and run big-code-analysis from a checkout. It is for contributors; the repository-level rules (stability contract, validation gates, review conventions) live in AGENTS.md and CONTRIBUTING.md at the repository root.

You need a current stable Rust toolchain; the minimum supported version is declared in the root Cargo.toml.

リポジトリのクローン

Clone through 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 が正式なエントリポイント

The repository ships a Makefile that wraps every common build, test, lint, format, and docs task. Run make help to see the full list of targets. Run make check-tools to list the optional tools and check which are installed.

最もよく使うことになる 2 つの複合ターゲットは次のとおりです。

  • make pre-commit: the recommended local gate before committing. It runs formatting, both clippy configurations, tests, documentation checks, dependency checks, and the lint families. It also runs repository consistency checks, both self-scan tiers, and the Python validation stages, in one parallel pass. The membership comparison (make check-test-lang-gates-compare) requires COMPARE_REF and runs separately in CI. See _pc-all in the Makefile for the complete stage list. The gate ends with a single BCA_GATE: pass or BCA_GATE: fail (…) line; read the verdict from that line and nothing else.
  • make ci: the same checks in the order CI runs them, with no auto-fixing. Use this to reproduce a failing CI run locally.

If GNU Make 4 or any of the optional tools are unavailable, fall back to the raw cargo commands shown below; they are equivalent to the corresponding Make targets.

ビルド

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-ast     # parse + classification layer 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 でインストールできます。

Without it, make test falls back to cargo test --workspace --all-features --lib --bins --tests, which runs the same set of tests. The two runners are not quite interchangeable, though: nextest runs each test in its own process, so anything sharing state across tests within a binary (a OnceLock cache, an environment variable, the working directory) is isolated under nextest and shared under cargo test. Preferring nextest locally means local runs match CI on that axis.

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 演算子の再分類など)に限って使用してください。

コードフォーマット

Format before opening a pull request. The make fmt target runs every formatter in the project (Rust, Markdown, TOML, Bash) in one shot; make fmt-check verifies formatting without modifying files.

make fmt         # cargo fmt + rumdl check --fix + shfmt -w + taplo fmt
make fmt-check   # the equivalent --check variants

Rustfmt

rustfmt formats Rust code to the project style.

インストールするには、次を実行します。

rustup component add rustfmt

コードをフォーマットするには、次を実行します(make fmt が自動的に処理します)。

cargo fmt

Clippy

Clippy catches common mistakes. The warnings it reports must be fixed before making a pull request.

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 runs cargo +nightly udeps --workspace --all-targets to catch dependencies declared in Cargo.toml but never referenced. Requires the nightly toolchain (rustup toolchain install nightly) and 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 and make doc-open are the interactive viewers: they build whatever they can so you can still inspect rendered output mid-refactor. make doc-check is the strict gate that runs as part of make pre-commit and CI (cargo doc --no-deps --workspace --all-features with RUSTDOCFLAGS extended by -D warnings); it catches broken intra-doc links, links into private items, and other rustdoc regressions.

big-code-analysis が使用する各依存関係のドキュメントもビルドしたい場合は、基になる cargo 呼び出しから --no-deps オプションを削除してください。

本書のビルド

The book you are reading is an mdBook under big-code-analysis-book/:

make book        # mdbook build
make book-serve  # mdbook serve with live reload

コードの実行

Run bca:

cargo run -p big-code-analysis-cli -- [bca-parameters]

List its parameters:

cargo run -p big-code-analysis-cli -- --help

Run bca-web:

cargo run -p big-code-analysis-web -- [bca-web-parameters]

List its parameters:

cargo run -p big-code-analysis-web -- --help

make install、make install-cli、make install-web は、それぞれ対応するバイナリクレートに対して cargo install --path を呼び出します。

実践的なアドバイス

  • Add at least one unit or integration test with every new feature.
  • Document the public API.
  • Do not add dead code.
  • Comment intricate code so others can follow it.
  • Run make pre-commit before pushing; it is the same gate CI runs.

新しい言語のサポート

How to add a language to big-code-analysis, for contributors who have a tree-sitter grammar crate for it. The work has three parts:

  1. Generate the kind enum from the grammar.
  2. Wire the grammar into the workspace.
  3. Implement the traits and tests for every supported metric.

文法の生成

As a prerequisite, a tree-sitter grammar crate for the language must exist that matches the tree-sitter runtime version used in this project.

The kind enums are generated by a crate in this repository called enums. The following steps register the grammar crate there and generate the enum file this project matches on when it evaluates metrics.

  1. 言語固有の tree-sitter クレートを enums クレートに追加します。このとき、依存関係がルートの big-code-analysis の Cargo.toml で使用されているのと同じバージョンに =X.Y.Z でピン留めされていることを確認してください。たとえば Rust サポートの場合、/enums/Cargo.toml には次の行があります: tree-sitter-rust = "=0.24.2"。
  2. Append the language to the mk_langs! list in /enums/src/languages.rs. Keeping with Rust as the example, the line is Rust,: one variant name per line, which becomes the name of the Rust enum that will be generated. Add a // -> <crate> comment when the backing crate is not the obvious tree_sitter_<name>.
  3. Add a case to the match in the mk_get_language macro rule in /enums/src/macros.rs, which binds each variant to its grammar crate. The current convention uses the LANGUAGE constant exposed by modern grammar crates: for Rust that line is Lang::Rust => tree_sitter_rust::LANGUAGE.into().
  4. Run the /recreate-grammars.sh script, which runs the enums crate to generate the enum for the new language.

This produces a new enum file for the language in /big-code-analysis-ast/src/languages/. See /big-code-analysis-ast/src/languages/language_rust.rs as an example of the generated enum.

新しい文法を big-code-analysis に追加する

  1. Add the language specific tree-sitter crate to the big-code-analysis workspace, with the same =X.Y.Z pin as the enums crate uses. Since #1376 this is three edits across two manifests. In the root Cargo.toml [workspace.dependencies], add the exact grammar pin (for Rust, tree-sitter-rust = "=0.24.2"). In /big-code-analysis-ast/Cargo.toml, add the optional workspace dependency (tree-sitter-rust = { workspace = true, optional = true }) and its feature (rust = ["dep:tree-sitter-rust"]), then include that feature in all-languages. In the root [features], add the forwarding feature (rust = ["big-code-analysis-ast/rust"]) and include it in all-languages.
  2. Add the new language module to /big-code-analysis-ast/src/languages/mod.rs:
#![allow(unused)]
fn main() {
pub mod language_rust;
pub use language_rust::*;
}
  1. Add a definition of the language to the arguments of the mk_langs! macro in /big-code-analysis-ast/src/langs.rs.
#![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 の基盤処理と、ワークスペースが定義するすべてのメトリクストレイトも実装しなければなりません。

  • Checker in a new per-language file under /big-code-analysis-ast/src/checker/ (checker/<lang>.rs, registered with a mod <lang>; line in checker.rs): comment, function, closure, call, string-literal, and else-if predicates over the grammar’s kind_ids, most of which forward to the language’s role table (below).
  • Getter in a new per-language file under /big-code-analysis-ast/src/getter/ (getter/<lang>.rs, registered in getter.rs): get_space_kind plus the Halstead operator/operand classification table.
  • Alterator in /big-code-analysis-ast/src/alterator.rs: an alterate body that flattens the kinds the language’s StringLit row lists to one verbatim leaf, so the AST dump agrees with find string. Every grammar carries one; only the Preproc and Ccomment helper variants, whose dump is not a user surface, keep the bare default.
  • A role table in /big-code-analysis-ast/src/roles/ (roles/<lang>.rs, one role_table! per language), declaring for every role either its rows, absent "why", or pending Rnn. make role-census, make role-coverage and make role-bypass (all in make pre-commit) fail on a language with no table, a kind id that is neither claimed by a row, excluded with a reason, nor baselined, or a claimed kind restated as classification in a dispatch file without a // role-bypass: allow(<Role>) exemption.
  • All thirteen metric traits: Abc, Cognitive, Cyclomatic, Exit (the nexits metric), Halstead, Loc, Mi, NArgs, Nom, Npa, Npm, Tokens, Wmc. Register twelve of them via the implement_metric_trait! macro invocation in each module under /src/metrics/ to start with default (no-op) bodies, then replace with real impls for the metrics that have meaningful semantics for the language. Halstead is the exception: every grammar is listed in impl_halstead_forwarding! in src/metrics/halstead.rs, which routes the metric through the language’s Getter, so there is no per-language Halstead body to write.

エイリアス化された文法バリアントの監査

Tree-sitter grammars frequently emit several distinct kind_ids that map to the same node.kind() string (Identifier / Identifier2 / Identifier3 in Go, InvocationExpression / InvocationExpression2 in C#, QuotedContent through QuotedContent20 in Elixir). Every match node.kind_id() arm that touches an aliasable rule must either list every numbered variant or compare on the string node.kind() instead. Missing an alias silently drops nodes from the metric. See the add-lang skill for the mechanical audit procedure, and .claude/rules/grammar-dispatch.md plus lessons 2, 19 and 34 in docs/development/lessons_learned.md for the failure modes.

テスト

Add per-language tests under each src/metrics/*.rs test module; aim for parity with the existing Rust coverage across the metric files. Every insta assert_json_snapshot! call MUST be anchored: either with an inline expected block, a positive assert_eq! on the headline integer accessor above it, or an explanatory // expected: comment. make snapshot-anchors (run as part of make pre-commit) enforces this against .snapshot-anchor-baseline.txt. Every test that names the new parser also needs #[cfg(feature = "<lang>")], since the *Parser alias compiles in every build and panics in one without the grammar; make check-test-lang-gates (also in make pre-commit) derives the required gate, and ./utils/check-test-lang-gates.py --fix writes it (the Makefile target forwards no arguments).

エンドツーエンドのワークフロー

For an opinionated, end-to-end recipe (including the alias audit, test layout, snapshot anchoring, and code-quality post-passes) see the project’s add-lang Claude Code skill. It is the canonical workflow used by recent language additions (Elixir, PHP, C#, Bash, Go).

コード行数(LoC)

How to implement the LoC metrics for a language, for contributors adding or fixing a Loc impl. Lines of code is a software metric that indicates the size of source code by counting its lines. There are several kinds of LoC; this page first explains each by way of an example, then covers the implementation.

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()
}
}

The example above illustrates each of the LoC metrics described below.

SLOC

A straight count of all lines in the file including code, comments, and blank lines.

METRIC VALUE: 11

PLOC

A count of the instruction lines of code contained in the source code. This includes any brackets or similar syntax on a new line. Comments and blank lines are not counted.

METRIC VALUE: 3

LLOC

The “logical” lines are a count of the statements in the code. What a statement is depends on the language. In the example above there is a single statement, the call of product with the Iterator as its argument.

METRIC VALUE: 1

CLOC

A count of the comment lines in the code. The type of comment does not matter: single line, block, or doc. A Python docstring counts here as well, and is also reported on its own as DLOC below.

METRIC VALUE: 6

BLANK

A count of the blank lines in the code.

METRIC VALUE: 2

DLOC

Documentation Lines Of Code: the rows the language treats as documentation, such as a doc comment (the /// Factorial line above). Documentation includes Python docstrings and Perl POD blocks. Those rows are always counted in CLOC too, so DLOC never exceeds it. Each language follows its documentation tool’s rules (Doxygen, Javadoc, KDoc, JSDoc, PHPDoc, C# XML docs, LDoc, go/doc, YARD); see the DLOC reference. The SEI variant of the Maintainability Index excludes DLOC from the comment lines used in its formula. An Elixir @doc heredoc is an assignment that counts as code.

METRIC VALUE: 1

空白文字のみのファイル

Source that contains no token at all (a file of nothing but spaces, tabs, and newlines) reports the rows it has: a four-row file of spaces is sloc 4, ploc 0, blank 4, with or without a trailing newline.

This used to be the one input class where a trailing newline changed a LoC value. Most grammars collapse tree-sitter’s root node to a zero-width node at end-of-input for such input rather than spanning the file, and the file-level SLOC span was measured from that node, so those files reported sloc 0 when they ended in a newline and sloc 1 when they did not, while Elixir, Tcl, iRules and the preproc / ccomment helpers kept the root span and reported the rows either way.

The file-level span is now anchored at line 1 rather than measured from the root node’s first token, so where the root node starts is no longer observable in LoC and every grammar answers alike. The sweep that used to pin the split, whitespace_only_input_is_uniform_across_grammars in /src/metrics/loc.rs, now pins the absence of one.

The same anchoring is what makes leading blank lines count. A file opening with three blank rows before its first token reports those rows in sloc and blank, exactly as interior blank rows are reported.

実装

Implement the Loc trait, and its compute function, for the language. Each language’s impl Loc lives in its own file under /src/metrics/loc/ (rust.rs, python.rs, …), with the row-billing helpers every arm routes through in shared.rs; the tests stay in /src/metrics/loc.rs. Read a sibling language’s file for the shape.

Take care with the catch-all _ arm that inserts a row into PLOC. If the grammar surfaces its row terminator as a token rather than as extra, as the Tcl family does, that token’s start row is the row it terminates, so the catch-all credits comment-only and blank rows to PLOC. Give such tokens an explicit no-op arm.

文法の更新

How to bump a tree-sitter grammar, for contributors maintaining the grammar pins. big-code-analysis parses each language with a tree-sitter grammar. Grammars change over time and carry bugs of their own, so they need updating now and then.

The update scripts are bash, so grammars can be regenerated natively only on Linux and macOS. On Windows, run the scripts under Windows Subsystem for Linux (WSL).

Two kinds of grammar are in use: third-party grammars published on crates.io and maintained by external developers, and internal grammars defined inside the project for the variants of some languages used in Firefox. Each has its own procedure below.

サードパーティの文法

Update the grammar version in Cargo.toml and enums/Cargo.toml. For example, for the tree-sitter-java grammar:

tree-sitter-java = "=x.xx.x"

where x represents a digit. The pin must be exact (=X.Y.Z); make check-excluded-manifests rejects a caret range. The same version also appears as the last field of the language’s mk_langs! tuple in big-code-analysis-ast/src/langs.rs, and the grammar_version_matches_cargo_toml_pin test there fails until the two agree.

Run ./recreate-grammars.sh to regenerate every grammar’s enum:

./recreate-grammars.sh

Once the script finishes, fix any failed tests and problems the grammar changes introduced. make enums-codegen-drift (part of make pre-commit) re-runs the codegen into a temporary directory and fails if the checked-in language_*.rs or c_langs_macros/*.rs files differ from it.

Commit your changes and open a pull request.

内部の文法

Update the version of tree-sitter-cli in the package.json file of the internal grammar, then refresh its committed package-lock.json with npm install --package-lock-only --ignore-scripts in the same directory and commit both files together. The regen scripts install with npm ci, which fails loudly when the lockfile is missing or out of sync with package.json. This keeps every regen hash-verified and byte-reproducible (OpenSSF Scorecard Pinned-Dependencies).

The five vendored grammars publish under the bca-tree-sitter-* namespace (see RELEASING.md for the rename rationale), but consumer call sites still reference them as tree-sitter-<lang> via Cargo’s package = ... alias. A grammar refresh does not bump the leaf’s version on its own: every crate in this repository shares one workspace-wide version, and bumping the leaves out of step with the parent is not allowed (see the “Lockstep version policy” in RELEASING.md). Regenerate the parser tables, accept the resulting test-snapshot drift, and ship the change under the current version. The next workspace release picks up the new grammars at whatever shared version the next tag declares.

再生成に加えて tree-sitter の「ランタイム」依存関係の更新も必要な場合は、リーフの Cargo.toml 内の dev-dependency 行を更新します。

[dev-dependencies]
tree-sitter = "=x.x.x"

Leave [package] name = "bca-tree-sitter-<lang>", [package] version, and [lib] name = "tree_sitter_<lang>" untouched; the rename trick in [lib] is what keeps Rust import paths stable, and the version line is managed by the lockstep bump at release time.

Then run the script for the grammar, which regenerates every file.

For tree-sitter-ccomment and tree-sitter-preproc run ./generate-grammars/generate-grammar.sh followed by the name of the grammar, for example:

./generate-grammars/generate-grammar.sh tree-sitter-ccomment

tree-sitter-mozcpp and tree-sitter-mozjs have their own scripts.

tree-sitter-mozcpp の場合は、次を実行します

./generate-grammars/generate-mozcpp.sh

tree-sitter-mozjs の場合は、次を実行します

./generate-grammars/generate-mozjs.sh

Both of those crates carry a notification-only marker dependency in their Cargo.toml (tree-sitter-cpp for mozcpp, tree-sitter-javascript for mozjs) that Cargo never compiles. make grammar-marker-sync compares each marker against .grammar-marker-baseline.toml, so a Dependabot bump of the marker without a regen fails the gate. After a verified regen, refresh the baseline in the same commit with ./utils/check-grammar-marker-sync.py --update.

5 番目のベンダリング文法である tree-sitter-tcl には再生成スクリプトがありません。事前生成されたパーサーソースのみをベンダリングしている(grammar.js がない)ため、更新する際はローカルで tree-sitter generate を実行するのではなく、アップストリームプロジェクトから生成済みの src/ を再ベンダリングします。

Once the script finishes, fix any failed tests and problems the grammar changes introduced.

Commit your changes and open a pull request.