Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 2 additions & 14 deletions .bca-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ value = 120805.48245794396
path = "big-code-analysis-ast/src/macros/kind_sets.rs"
qualified = "<file>"
metric = "loc.sloc"
value = 1357.0
value = 1343.0

[[entry]]
path = "big-code-analysis-ast/src/node.rs"
Expand Down Expand Up @@ -897,26 +897,14 @@ value = 16.0
path = "src/metrics/abc/go.rs"
qualified = "GoCode::compute"
metric = "halstead.effort"
value = 49999.43426005393

[[entry]]
path = "src/metrics/abc/java.rs"
qualified = "java_inspect_container"
metric = "cognitive"
value = 15.0
value = 49998.590597403396

[[entry]]
path = "src/metrics/abc/kotlin.rs"
qualified = "KotlinCode::compute"
metric = "cyclomatic"
value = 20.0

[[entry]]
path = "src/metrics/abc/objc.rs"
qualified = "ObjcCode::compute"
metric = "halstead.effort"
value = 50110.01872952996

[[entry]]
path = "src/metrics/abc/rust.rs"
qualified = "RustCode::compute"
Expand Down
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,57 @@ for historical reference.

## [Unreleased]

Every entry under **Fixed** changes ABC values for valid input, mostly
`conditions` (one Elixir case changes `assignments`). Only `abc.*`
values move; cyclomatic, cognitive and Halstead are unchanged.

### Fixed

- **A C++ or Mozcpp comparison-operator overload no longer scores a
condition** (#1448). `operator<=`, `operator>=`, `operator==`,
`operator!=` and `operator<=>` each declared one, while `operator<`
and `operator>` were already excluded; all seven comparison tokens
now count only where they are applied. A fold over a comparison
(`(a < ...)`, `(0 == ... == a)`) now counts once, through the fold's
`operator` field, where `<` / `>` folds scored 0 and a binary fold
scored 2. `#if A <= B` still counts. In the openfst and kenlm corpora each
comparison-overload definition and declaration (`operator==`,
`operator!=`, `operator>=`) drops by one.
- **C++ and Mozcpp score the ISO alternative tokens like their
symbols** (#1470). `not b`, `b and c`, `b or c` and `b not_eq c` scored
0 where `!b`, `b && c`, `b || c` and `b != c` score 1, 2, 2 and 1.
Cyclomatic, cognitive and Halstead do not yet recognise them (#1522).
- **A Ruby subject-less `case` no longer counts a comparison twice**
(#1453). `case; when x > 5 … else …` scored 3 against its `if`
analogue's 2. A subject-less `when` is evaluated like an `if`
predicate, so each of its patterns is one operand of an implicit `||`
and scores once unless its own operator already counted it:
`when a, b` and `when a, x > 1` now score 2, level with `if a || b`.
A subject-ful `case x; when 1` still scores one per clause. A
parenthesised statement sequence in a condition (`if (x; x > 1)`) is
now read by its last statement, the value Ruby evaluates it to, where
the first was read before.
- **A Perl bare pattern match scores by use** (#1467). `/^#/` and
`m{^#}` match the implicit `$_`, and scored a condition only inside a
boolean slot, so `my $r = /^#/;` read 0 against `$x =~ /^#/`'s 1. They
now count wherever they are written. The pattern bound by `=~` / `!~`
is not counted again, and `split`'s delimiter (`split /,/, $s`) is not
a match and scores 0. In-slot spellings such as `if (/x/)` are
unchanged.
- **A comment inside or before a condition no longer zeroes it**
(#1455). Condition and value slots were read by child index, and the
`(…)` / `!` peels stepped to child 1 or to the first named child, so a
comment (`if (/*c*/ b)`, `if /*c*/ (b)`, `return /*c*/ !b`,
`((/*c*/ b))`, `! /*c*/ b`) took the operand's place and the condition
scored 0. Every slot is now read by grammar field or as the first
operand that is not a comment, in C, C++, C#, Elixir, Go, Groovy,
Java, JavaScript, Kotlin, Lua, Mozcpp, Mozjs, Objective-C, Perl, PHP,
Python, Ruby, Rust, TSX and TypeScript. The same change fixes a C++
`if` with an init-statement (`if (int y = f(); b)`), a labelled Rust
`while`, and an Elixir `=` behind a comment, which scored no
assignment. In the corpora, two pdf.js commented conditions and one
serde `while try!(…)` loop gain one condition each.

## [2.3.0] - 2026-10-07

This release adds a ninth published crate, `big-code-analysis-ast`,
Expand Down
24 changes: 5 additions & 19 deletions big-code-analysis-ast/src/macros/kind_sets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,23 +732,11 @@ macro_rules! python_bool_terminal_kinds {
// carry no truthy numeric operand, and no corpus carries a `.groovy`
// file at all.
//
// `pattern_matcher` (`/^#/`) and `pattern_matcher_m` (`m{^#}`) are the
// two spellings of a match against the implicit `$_`. They are
// **sibling rules, not aliases** — ids 337 and 336, the distinction
// this file's own numeric-literal note warns is invisible to an alias
// sweep — so both have to be listed or the `m{}` half stays at zero.
// A bound match (`$x =~ /^#/`) is a `binary_expression` whose `=~`
// token the dispatcher already counts; the bare form carries no
// operator token at all, which is why `if (/^#/)` scored zero
// conditions against `if ($x)`'s one.
//
// Listing them double-counts nothing (§5). In `$x =~ /^#/` the
// pattern is a child of the `binary_expression`, and every walker that
// consumes this set either breaks on that `binary_expression`
// (`perl_inspect_container`, `perl_count_condition`) or requires the
// list node itself to be the `&&`-chain parent
// (`perl_count_unary_conditions`), so the pattern node is never
// reached alongside its own `=~`.
// `pattern_matcher` (`/^#/`) and `pattern_matcher_m` (`m{^#}`), the
// two spellings of a match against the implicit `$_`, are absent: a
// match is a relational operator, so it scores by use through a gated
// arm in `PerlCode::compute` (#1467), not only where a slot reads it.
// Listing them here as well would score `if (/^#/)` twice (§5).
//
// `substitution_pattern_s` (`s///`) and `transliteration_tr_or_y`
// (`tr///`) are deliberately absent: both edit `$_` and evaluate to a
Expand Down Expand Up @@ -805,8 +793,6 @@ macro_rules! perl_bool_terminal_kinds {
| $crate::Perl::CallExpressionRecursive
| $crate::Perl::CallExpressionWithBareword
| $crate::Perl::MethodInvocation
| $crate::Perl::PatternMatcher
| $crate::Perl::PatternMatcherM
| $crate::Perl::StringSingleQuoted
| $crate::Perl::StringQQuoted
| $crate::Perl::StringDoubleQuoted
Expand Down
5 changes: 3 additions & 2 deletions big-code-analysis-book/src/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,10 @@ application would over-count.
| Kotlin | `try` counts a condition alongside `catch` | Fitzpatrick counts both keywords, and Java / C# / C++ / Groovy already count both; Kotlin previously counted only the catch block. |
| C#, Java, Rust, Python, Ruby, Elixir | A pattern-match guard is a condition slot, scoring one however it is spelled | A 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). |
| Kotlin | A subject-less `when` arm scores through the predicate slot; a subject-ful arm scores per entry | A subject-less arm's condition (`when { x > 5 -> … }`) is an ordinary boolean expression, compiled as an `if` predicate, so the comparison inside it is already counted by the operator arms and a blanket per-entry increment double-counted it. A subject-ful arm (`when (x) { in 1..2 -> … }`) lists a pattern rather than an independent boolean expression, so the implicit `subject == pattern` is a decision nothing in the source spells and the entry itself pays for it (#1421). |
| C++, Objective-C, Mozcpp, Rust, Go, Java, Groovy, C#, Kotlin, TypeScript, TSX, JavaScript, Mozjs, Lua, Perl, Ruby, Bash, Elixir | A `<` or `>` that is not a comparison is not a condition | Every 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. 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 `<` / `>` as an operator, and the two metrics are meant to disagree here (#1395) — see [the Halstead delimiter rule](#halstead). |
| Ruby | A subject-less `case` scores each `when` pattern as an `\|\|` operand; a subject-ful `case` scores per clause | A 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). |
| C++, Objective-C, Mozcpp, Rust, Go, Java, Groovy, C#, Kotlin, TypeScript, TSX, JavaScript, Mozjs, Lua, Perl, Ruby, Bash, Elixir | A `<` or `>` that is not a comparison is not a condition | Every 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). 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 `<` / `>` as an operator, and the two metrics are meant to disagree here (#1395) — see [the Halstead delimiter rule](#halstead). |
| Java, Groovy, C#, TypeScript, TSX | A `?` used as type syntax is not a ternary | In 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 | A relational operator scores by use; a value-bearing operand scores in a boolean slot | Five of these grammars spell at least one relational construct as its own production rather than as a binary expression with an operator token: C#'s `x is int` and `x is null`, Java's and Groovy's `x instanceof T`, Groovy's `a in l`, Kotlin's `a is T` and `a in 1..2`, and Ruby's one-line `a in Integer`. Having no token to count, each was reached only through the language's terminal-operand set, which the walker consults inside an `if` / `while` / ternary / `&&`-operand slot and nowhere else — so `var b = x == 1;` scored 1 while `var b = x is int;` scored 0. Fitzpatrick's Rule 5 counts a relational operator wherever it appears, so each now counts wherever it appears and has left the operand set; being in both would score it twice. Groovy's spaceship `<=>` counts on the same rule, as it already did in Ruby, PHP, C++ and Mozcpp, although its result is an integer rather than a boolean — what Rule 5 measures is the comparison, not its type. Elixir reached the same 0 by the other route: its membership and type tests (`a in [1, 2]`, `rescue e in RuntimeError`) *do* carry an operator token, and simply had no arm matching it. They count by use on the same rule, gated on the token's parent so that an operator merely *named* (`&in/2`) stays excluded alongside `<` and `>`. The converse still holds for a construct whose *value* fills the slot: a cast, a Go type assertion, a Rust `matches!` / `cfg!` macro and a Python walrus are all operands and count only where a slot reads them as a predicate (#1461). |
| C#, Java, Groovy, Kotlin, Ruby, Elixir, Perl | A relational operator scores by use; a value-bearing operand scores in a boolean slot | Five 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 `>`. 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. 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). |

#### Where a constructor call lands {#abc-constructor-attribution}

Expand Down
Loading
Loading