Skip to content

v2.2.0

Latest

Choose a tag to compare

@github-actions github-actions released this 29 Aug 20:46
· 123 commits to main since this release
09fb230

One entry below is marked (breaking): the Python bindings'
analyze_batch / analyze_paths now return one element per input
under skip_generated=False, as both docstrings always promised. The
previous shape silently misattributed results to the wrong path, so
the fix ships in a minor release as a correctness exception to the
stability contract rather than waiting for 3.0.

Added

  • bca check now reports what it declined to look at (#1055). When the
    gate skipped files — a generated-code marker (@generated,
    DO NOT EDIT, GENERATED CODE) or a VCS ignore file committed in
    the tree under test — a one-line stderr summary names the counts:
    bca: 2 files not checked (1 generated, 1 ignored) — pass --report-skipped to list them. Either input could previously remove
    a file from a pull-request gate with nothing on stderr. The counts
    are measured at the walk's prune points and cover only files a
    parser owns (a generated Cargo.lock or an ignored log file was
    never a gate bypass), never a file named explicitly on the command
    line; a whole ignored directory is reported — under
    --report-skipped only, since ignored build trees exist in
    essentially every checkout — as its own
    N ignored directories not walked clause and is never entered, so
    the summary stays cheap and small on trees with large ignored build
    output. Clean runs stay silent, exit codes are unchanged, and
    --report-skipped also lists each ignore-dropped entry
    (note: skipped (ignored): …,
    note: skipped (ignored directory): …).

  • bca check --strict, plus a matching [check] strict manifest key
    (#1055): the untrusted-input gate profile. Flips both skip defaults
    in one flag (equivalent to --no-skip-generated --no-ignore), so a
    PR gate opts out of content sniffing and in-tree ignore files without
    remembering two flags. The manifest key is presence-only (it can turn
    the profile on, never off) so a project opts in once rather than per
    workflow.

  • A cargo-fuzz crate (fuzz/, workspace-excluded) with eleven libFuzzer
    targets over the parse-and-walk layer, committed seed corpora, and an
    out-of-band fuzz workflow (make fuzz-check / fuzz-smoke /
    fuzz-run; see
    docs/development/fuzzing.md). The
    target set is scoped to what the static lints adopted in #1152 cannot
    reach — chiefly the nine per-function indexing_slicing carve-outs in
    the C-family macro lexer, which only the preproc_macro target
    exercises — rather than to blanket byte fuzzing, which a 112-file
    adversarial corpus already showed the encoding/IO layer survives.
    Targets call Source::from_bytes so the bytes reach the parser
    unnormalised; the file-reading path appends a trailing newline, and a
    harness that inherited it would pass vacuously against the very class
    of bug (#1051) that motivated this. The tree-sitter grammars are
    compiled with -fsanitize=address too, so the C scanners are covered
    and not just the Rust. Its first bounded run found a bounded leak in
    tree-sitter-perl 1.1.2's heredoc scanner, whose
    external_scanner_destroy has its queue frees commented out — upstream
    in the grammar, recorded in fuzz/lsan-suppressions.txt with the
    measurement showing it is per-thread rather than per-parse. No library
    behaviour changes (#1154). A preproc_includes target covers the
    include-resolution half of the preprocessor — fix_includes and the
    graph building, SCC collapse and candidate scoring behind it, none of
    which preproc_macro reaches (#1288).

  • A check-ruff-lockstep gate (make check-ruff-lockstep, wired into
    make lint / pre-commit / ci and the pre-commit hooks) holds the one
    adopted ruff version together across the four files that declare it.
    big-code-analysis-py/uv.lock is the anchor, because the hash-pinned
    requirements/dev.txt CI installs from is generated from it — gating on
    the export would bless a stale export rather than catch one. The
    ruff-pre-commit rev: in .pre-commit-config.yaml must be v + the
    locked version, the export must pin it, and pyproject.toml's bound must
    match the one uv recorded resolving against. The rev: was previously held
    by a comment alone and had already drifted silently once (v0.15.14
    against a lockfile resolving 0.15.22), which stays invisible until the two
    versions disagree and then presents as "works locally, red in CI" (#1230).

  • A check-safety-doc-pin gate (make check-safety-doc-pin, wired into
    make lint / pre-commit / ci, the pre-commit hooks, and its own CI
    step) holds the tree-sitter version cited by the unsafe soundness
    argument in big-code-analysis-py/src/node.rs equal to the version
    [workspace.dependencies] pins. That module doc is the canonical
    justification for the workspace's only sanctioned unsafe block and
    reasons about a named release — the Node<'tree> layout, Tree::edit
    taking &mut self, Send + Sync — so a pin that moves while the literal
    does not leaves an argument reading as verified against a crate nobody
    compiles. The gate also fails when the literal is dropped altogether,
    since a version-free phrasing hides the staleness rather than fixing it.
    The scanner reads every //! line in the file rather than stopping at
    the first non-doc line, so a multi-line #![allow(…)] between doc
    paragraphs cannot silently truncate the scan in either direction
    (#1345). No library behaviour changes (#1057).

  • SkipReason (#[non_exhaustive], with a Display that renders a
    noun phrase) and read_file_with_eol_classified, the reason-naming
    sibling of read_file_with_eolio::Result<Result<Vec<u8>, SkipReason>>, of which the old function is now the .map(Result::ok)
    projection (#1287). The four variants map 1:1 onto the reader's skip
    gates: Empty, TooSmall (1–3 bytes, or a file that shrank below the
    probe window), Utf16Bom, and NotUtf8. Prefer the classified
    variant wherever a skip is reported to a user; the shape is recorded
    in STABILITY.md, whose per-variant Display wording is not
    SemVer-protected.

  • web: error_kind token vcs_invalid_author_hash_key. The
    STABILITY.md vocabulary list also gains not_acceptable and
    serialize_failed, added by #657 and never documented (#1245).

Fixed

  • Bash command names are no longer counted twice in Halstead N2
    (#1351). BashCode::get_op_type classified both the command_name
    wrapper and the word / string / expansion it wraps as operands,
    so ls bar reported N2 3 for two operands, and the spellings whose
    wrapper text differs from the child's ("$cmd", ${cmd}) also planted
    a spurious second n2 entry. The wrapper arm is gone; the child
    carries the count for all fourteen spellings the pinned grammar
    admits. The dead Bash::Concat arm — the hidden _concat external
    token, never emitted — was removed with it and pinned by a drift
    marker. Metric drift: Bash N2 falls by one per command name,
    moving length, volume, difficulty, level, effort, time
    and bugs.

  • Ruby %w[…] / %i[…] arrays and adjacent-string concatenation no
    longer count an extra Halstead operand for the wrapper (#1353).
    RubyCode::get_op_type suppressed a string-like literal only when it
    carried an Interpolation child, but chained_string, string_array
    and symbol_array hold classified operand children instead, so
    %w[x y] scored n2 4 / N2 4 for three operands — a vocabulary
    that depended on how the literals were grouped. The three kinds are
    gated rather than dropped, because %w[] parses to a wrapper holding
    nothing but its delimiters. bare_symbol joins bare_string under
    the same guard, fixing an asymmetry where %I[a#{n}b] billed both the
    element and the interpolated n. Metric drift: Ruby halstead.*
    and the derived values fall.

  • Perl no longer counts a qualified name once per part and once as a
    whole (#1355). package_name, package_variable and typeglob were
    operands alongside the operand kinds they contain, and they nest: use strict; scored N2 2 for one name, our $Foo::count = 3; scored
    n2 5 / N2 6 for two, and the vocabulary carried a bare :: entry.
    A parent-keyed guard now subsumes the contained operands.
    Metric drift: Perl halstead.* and the derived values fall.

  • Tcl and iRules no longer bill a braced word twice (#1354, #1317). A
    braced literal (set x {literal here}) was a Halstead operand and so
    was every word inside it, so set a {a b} scored three operands
    against its set a "a b" synonym's one. Tcl evaluates nothing between
    braces, so the literal is now the single operand its parts belong to.
    A braced script — a proc, if or when body — was additionally
    billed as one operand spanning its entire text beside the commands the
    walk already counts, so n2 grew with the size and uniqueness of the
    code rather than with its vocabulary; it is now billed through its
    contents alone, except when it holds no command (lappend l {} is an
    empty list whose brace pair is its only carrier, and a comment-only
    body scores like an empty one). Metric drift:
    Tcl and iRules halstead.*, the derived values, and hence mi fall
    for any file containing a braced word or block. A value-position
    braced literal still reports a {} operator (#1318).

  • C, C++, Mozcpp and Objective-C character literals are Halstead
    operands (#1316). char_literal was in neither the operator nor the
    operand arm of those four getters, so a character literal contributed
    nothing at all and char b = 'x'; billed b alone — while Rust,
    Java, Kotlin, C#, Go and Elixir each counted theirs. All prefixed
    spellings (L'x', u'x', U'x', u8'x') count as distinct
    operands, and a multi-character constant such as 'ab' counts once.
    Checker::is_string is deliberately unchanged: a character is not a
    string, the same split Rust and Go apply. Metric drift: n2 /
    N2 rise by one per character literal, moving the derived values and
    the three maintainability-index variants.

  • A plain C function written inside an Objective-C @implementation,
    @interface or @protocol no longer contributes to that container's
    wmc (#1356). Such a function is a file-static helper, not a method —
    no receiver, absent from the method table, unsendable — so npm never
    counted it, but the space tree nests its Function space inside the
    container's and wmc weighted every such space. The three metrics
    disagreed about one container: npm.class_methods 1 against
    wmc.class_wmc_sum 3. This is the C++ friend divergence #1301
    removed, surviving in a sibling language. Metric drift: ObjC
    wmc.class_wmc / class_wmc_sum / interface_wmc_sum / total fall
    by the cyclomatic complexity of each such helper. The helper keeps its
    own Function space and metrics, nom still counts it, and the
    file-level cyclomatic sum is unchanged.

  • The JS-family ABC assignment count no longer depends on the previous
    statement's terminator (#1277). The counter cleared its declaration
    sentinel only on a SEMI token, so a const written without a
    semicolon (ASI) left a stale sentinel that suppressed every subsequent
    = until the next ;const a = 1 then x = 2 scored zero
    assignments. const initializers are now identified structurally from
    the = token's ancestor chain, matching the Kotlin fix in #455; the
    same change closes TypeScript's x as const, which promoted a live
    let slot and leaked even in fully semicolon-terminated code. A
    destructuring default under a const declarator (const {a = 1} = o)
    stays part of the declaration, as it was under the sentinel. Affects
    Javascript, Mozjs, Typescript and Tsx. Metric drift: an assignment
    nested inside a const initializer's value (const x = (o.p = v),
    const x = a || (b = 1)) was blanket-suppressed by the sentinel and now
    counts, so abc.assignments rises in semicolon-terminated code that
    uses that shape — the change behind the pdf.js corpus refresh. Java,
    Groovy and C# now use the same structural rule for final / const
    initializers instead of a sentinel stack of their own; that stack
    stayed live from the keyword to the next ;, so in Java and Groovy
    every = inside a final initializer — a lambda or closure body's
    x = 1, an array initializer's — was suppressed with the declarator's
    own (final Runnable r = () -> { x = 1; }; scored 0). Metric
    drift:
    Java and Groovy abc.assignments rise by one per assignment
    nested in a final initializer; C# is unchanged, since a const
    initializer cannot contain one.

  • A ? used as type syntax is no longer counted as an ABC ternary
    condition in C#, TypeScript and TSX (#1275). The ternary ? and the
    type-syntax ? are the same anonymous token, so C# scored int? x
    and where T : class?, and TS/TSX scored every optional parameter,
    property, method, class field and tuple element, plus conditional
    types (T extends U ? X : Y), as decisions. C# safe navigation
    (a?.b, a?[0]) shares the same token and deliberately still counts,
    keeping ABC in step with C# cyclomatic — so C# gates by denylist and
    TS/TSX by allowlist, a polarity difference the arm comments record.
    Completes the type-syntax work #1274 began for Java and Groovy.

  • abc.conditions now counts the for header's condition slot
    (#1276). A bare, negated or parenthesised loop condition
    (for (; a; ), for (; !a; ), for (; (a); )) scored zero in C,
    C++, Mozcpp, Objective-C, JavaScript, Mozjs, TypeScript, TSX, PHP and
    Perl's C-style for (my $i = 0; $ok; $i++) while the same predicate in
    an if scored one; comparison-shaped conditions (i < n) were
    unaffected. Go's three-clause for init; cond; post had the same gap,
    and its header slot was additionally mis-read for a bare for {} and
    shifted by a header comment. Metric drift: abc.conditions — and
    therefore abc.magnitude / abc.value — rises for any function with
    such a loop in those eleven languages. An empty condition (for (;;),
    for (init; ; update)) now counts zero in every language: Java
    and Groovy previously scored one vacuous condition and drop by one per
    such loop. Java's and Groovy's for condition also no longer goes
    unread when a comment appears in the loop header.

  • Objective-C message sends are boolean terminals in ABC condition
    slots. cpp_bool_terminal_kinds! listed call_expression but not
    ObjC's message_expression, so if ([a ok]), while (![a ok]),
    for (; [a ok]; ), [a ok] ? x : y and each operand of a && /
    || chain scored zero conditions where the C-call twin if (ok())
    scored one. Metric drift: ObjC abc.conditions rises for any
    function with a message send in a decision slot.

  • Groovy's elvis operator a ?: c is an ABC condition. Groovy
    cyclomatic already counted the token and Kotlin's ABC arm counts its
    identical one, so a method whose only branching was elvis chains
    reported cyclomatic above one with zero conditions; one condition per
    ?:, so abc.conditions stays equal to cyclomatic() - 1 on the
    chain. Metric drift: Groovy abc.conditions rises by one per
    elvis.

  • Perl qw() lists are Halstead operands. Neither the elements, the
    wrapper nor the qw keyword had a classification, so my @a = qw(a b c) billed @a alone where ("a", "b", "c") billed four operands.
    Each element is one operand and the empty qw() is one, the rule
    #1353 set for Ruby's %w[]; the delimiter choice cannot move the
    score. Metric drift: Perl n2 / N2 rise by one per qw
    element.

  • iRules array references are no longer billed three times. The
    array_index wrapper of $arr(k) was an operand beside the reference
    and the index it wraps, so one reference contributed three operands
    where the Tcl twin contributed two. Metric drift: iRules N2 falls
    by one per array reference, and n2 by one per distinct index
    spelling.

  • The @ of an Objective-C string literal is no longer a Halstead
    operator. @"…" is one string_literal holding its @ as a child,
    so the marker was billed as an operator while the operand key already
    carried it; NSString *s = @"str"; scored an @ in n1 that @42
    and @[…] legitimately keep. Metric drift: ObjC n1 / N1 fall
    by one per string literal.

  • bca no longer announces every file the reader declines as
    skipping empty file (#1287). Emptiness is only one of four gates: a
    1–3-byte valid source, a multi-kilobyte binary, and a UTF-16-BOM file
    were all reported as empty, which sent anyone triaging a skipped file
    in the wrong direction. The -w diagnostic now names the gate that
    fired — skipping empty file, skipping file too small to analyze (3 bytes or fewer), skipping file with non-UTF-8 contents,
    skipping UTF-16 file (unsupported encoding) — rendered from the new
    SkipReason (see Added). Exit codes and which files are skipped are
    unchanged.

  • bca check --paths-from - no longer silently defeats [check.exclude]
    / --check-exclude globs (#1306). The gate re-read --paths-from to
    re-anchor exclude globs against the seed list, but - resolves to
    stdin and the walk had already drained it, so the second read returned
    an empty list and every violation anchored against the bare --paths
    set — a git diff --name-only | bca check --paths-from - gate failed
    on files the project had exempted, with nothing on stderr. The list is
    now materialized once, before the walk, and carried to the exclude
    stage; the remediation footer still echoes the caller's original
    --paths-from - spelling, and the no-exclude fast path still does no
    glob-set build.

  • bca metrics -O json / bca ops -O json to stdout now emit their
    per-file documents in the walk's file order rather than
    worker-completion order (#1303), so redirecting stdout to a file
    yields byte-identical output on every run at --jobs > 1 — the
    sibling of #1244's --output <FILE> fix, on the destination pipelines
    reach for most. A reorder buffer keyed on each file's index in the
    resolved list holds out-of-order completions; skipped, unreadable, and
    unparseable files release their slot with an empty marker so the drain
    never stalls, and a post-join flush turns a missed release into a late
    document rather than a lost one. Throughput is unchanged within noise
    (measured on a 13,583-file tree); peak memory rises by the documents
    buffered while an earlier file is still being analyzed.

  • A C++ friend defined inline no longer contributes to the enclosing
    class's wmc (#1301). A friend is a free function the class grants
    access to, not a member of it, so npm never counted it as a method
    — but the space tree nests its Function space inside the class
    space, and wmc weighted every such space it found. The three
    metrics disagreed about the same class: for class R { friend void amigo() { if (1) { } } void mine() { } };, npm.class_methods was
    1 while wmc.class_wmc_sum was 3. This is the divergence #1258
    removed for templated member bodies, surviving for friend. Both
    friend_declaration > function_definition and its templated
    template_declaration > friend_declaration > function_definition
    form are covered, in LANG::Cpp and LANG::Mozcpp alike; a friend
    declared without a body opens no space and was never affected.
    Metric values move: wmc.class_wmc / class_wmc_sum / total
    fall by the cyclomatic complexity of each inline friend, for classes
    that have one. Nothing else changes — the friend keeps its own
    Function space and its own metrics, nom still counts it (it
    counts free functions wherever they appear), and the file-level
    cyclomatic sum is unchanged.

  • The file-level unit's loc.sloc and loc.blank now count blank lines
    above the first token (#1247). The unit anchors its reported span at
    line 1 because the unit is the file (#1195), but its row span was
    still measured from the root node, which tree-sitter starts at the
    first token — so "\n\n\nfn a() {}\n" reported sloc 1, blank 0
    against a reported span of 1..4, while the same file shifted down by
    a leading comment reported sloc 4, blank 2, comments being in the
    tree where blank rows are not. The unit's sloc is now derived from
    the span it reports, so the two can no longer disagree. The anchor is
    applied once at space finalization rather than in the twenty-odd
    per-language Loc implementations, none of which knows the space kind.
    Metric values move, for files whose first token is not on line 1:
    loc.sloc and loc.blank rise by the number of leading blank rows,
    as do the sloc_average / sloc_max / blank_average / blank_max
    aggregates, and mi falls slightly through its ln(sloc) term. Files
    opening with code or a comment are unaffected, as are all nested
    spaces, which keep their measured spans.

  • Whitespace-only files now report their real row count regardless of a
    trailing newline (#1087, #1247). Most grammars collapse the root node
    to a zero-width point at end-of-input, so " \n \n" measured no rows
    at all and reported sloc 0 while its unterminated twin reported
    sloc 1 — accepted in #1087 as an upstream-owned carve-out, with the
    five grammars that behave otherwise (Elixir, Tcl, iRules, preproc,
    ccomment) pinned as the exception. Anchoring the unit's row span
    removed the premise: where the root node starts is no longer
    observable in LoC, and all twenty-five grammars now answer alike.
    Metric values move: a newline-terminated whitespace-only file goes
    from sloc 0, blank 0 to its real sloc n, blank n.

  • JavaScript, MozJS, TypeScript and TSX now count a var / let /
    const declaration as a logical line (#1283). Neither
    variable_declaration nor lexical_declaration had an LLOC arm, so a
    file of nothing but declarations reported lloc 0 and every real
    JS/TS file under-reported loc.lloc by one per declaration statement
    — while Java's LocalVariableDeclaration, Rust's let and Python's
    assignments all counted the equivalent construct. The two JavaScript
    modules also count using_declaration (using r = open();), the
    third member of the grammar's declaration supertype that runs an
    initializer; the TypeScript and TSX grammars pinned here emit no such
    node. Two enclosing constructs that already count the row carve the
    declaration out, mirroring Java's for-header rule: a classic
    for (let i = 0; …) header — recognised as the for_statement's
    initializer field, so a brace-less body (for (…) var s = i;)
    still counts as its own line — and an export const a = 1;
    (including TypeScript's export declare const y: string;, where an
    ambient_declaration sits between the export and the declaration).
    In TypeScript and TSX a declaration under an ambient_declaration
    (declare const x: number;, the body of a declare namespace or
    declare module) counts nothing: it has no initializer to run.
    for (const x of …) and for (var k in …) need no carve-out — the
    grammar inlines the keyword and emits no declaration node.
    Metric values move: loc.lloc rises for JS/TS/TSX/JSM input, and
    with it the lloc_average / lloc_min / lloc_max aggregates. No
    other metric is affected — mi does not consume lloc.

  • A member access or qualified name in JavaScript, MozJS, TypeScript,
    TSX, C# and Groovy no longer counts as a Halstead operand on top of
    the identifier leaves it contains (#1263). var r = a.b; reported
    the operands a, b and a.b, because the member_expression
    wrapper was classified while the walker independently counted both
    children — so a chain like a.b.c billed two composites on top of
    its three leaves. The same shape covered TS/TSX nested_identifier
    (namespace N.M), C# qualified_name (using System.Text;),
    generic_name (List<int>) and alias_qualified_name
    (global::Foo), and Groovy qualified_name (package com.example).
    The convention is now the one C, C++, Java, Rust, Python, Go, Kotlin,
    Ruby, Lua and PHP already followed and is recorded in the getter
    macro's doc comment: a member access is its leaves plus the . /
    :: operator, never the composite text as well. In the same change
    JavaScript's private_property_identifier (#x) becomes an operand,
    closing the gap the composite had been masking — a private field's
    declaration counted nothing at all, and this.#x counted only the
    composite. meta_property (import.meta, new.target) is the one
    composite kept, as a single operand like this: its meta /
    target leaves are anonymous tokens no arm classifies, so dropping
    it would have left the meta-object with no operand at all.
    Metric values shift: affected files report lower
    halstead n2 / N2 (−29% and −18% across the pdf.js and C#
    integration corpora) and therefore lower volume and a higher mi.
    Note difficulty and effort move up, because n2 falls faster
    than N2 and difficulty is (n1/2)·(N2/n2). Refresh affected
    baselines. Follows #1293, which fixed the same wrapper/leaf shape in
    PHP.

  • PHP's Halstead operand count no longer bills a type or qualified name
    once per wrapper node (#1293). primitive_type, optional_type,
    named_type, union_type, intersection_type,
    disjunctive_normal_form_type, qualified_name, relative_name and
    namespace_name all nest around the leaves they contain, and every
    level was listed as an operand — so int scored 2, ?int scored 3,
    Foo\Bar\Baz scored 5, and ?A\B scored 6 for two identifiers. Two
    design calls settle which node keeps the operand: a type is counted
    at its innermost concrete form (?int is the operand int, since
    ? is already an operator), and a qualified name is counted by its
    components (Foo\Bar\Baz is Foo, Bar, Baz around two \
    operators — the same reading PHP's own :: and -> already get).
    The primitive_type wrapper keeps the operand and its keyword child
    is suppressed under it, because the grammar emits no child token for
    callable, iterable, mixed, void, false or true and
    dropping the wrapper would score those six types zero; the
    suppression is parent-scoped, so the array heading an array(…)
    literal still counts. Metric values shift: PHP files report lower
    halstead N2 / n2 — 23 → 15 and 14 → 9 on the issue's two
    reproducers — and therefore lower volume, difficulty, effort and
    bugs, and a higher mi (maintainability index). Refresh affected
    PHP baselines. Follows #1259, which fixed the $variable half of the
    same wrapper/leaf shape.

  • nexits now counts the abrupt-exit builtins of Ruby, Perl, Tcl and
    iRules (#1270). Ruby raise / exit / exit!, Perl die / exit,
    Tcl error / throw / exit and iRules error leave a function
    exactly the way Python's raise, Go's panic and Lua's error do,
    but none of them has a dedicated grammar node, and only the latter
    three had a callee-text arm — so a Ruby guard clause that raised
    scored 1 where the byte-equivalent Python scored 3. Each is
    matched at the same seam its siblings already used: a call whose
    method identifier spells the builtin and whose receiver is absent or
    the explicit Kernel constant (Ruby), a call_expression_with_bareword
    naming the builtin bare or through CORE:: (Perl), the leading word
    of a generic command (Tcl/iRules). Calls with any other receiver
    (obj.raise, $obj->die), other package-qualified callees
    (Carp::croak), and the same words in argument position (puts error)
    are not counted. Metric values shift: any Ruby, Perl, Tcl or iRules
    function using these builtins reports a higher nexits sum, average
    and max than in 2.1.0, which can newly breach an nexits threshold —
    refresh affected baselines. Tcl 8.6's throw is deliberately absent
    from the iRules set (TMOS runs a Tcl 8.4-derived interpreter with no
    such builtin), and a bare argument-less Ruby raise stays uncounted
    because it parses as a plain identifier, indistinguishable from a
    variable read.

  • (breaking) The Python bindings' analyze_batch / analyze_paths dropped a
    result slot for any file the read gate declines to parse — three
    bytes or fewer, a UTF-16 BOM, or a leading window that is not valid
    UTF-8 — even under skip_generated=False, which both entry points
    documented as guaranteeing one element per input. That gate is
    unconditional, so a batch containing one such file returned a shorter
    list and the zip(inputs, results) pattern the docstrings endorse
    silently attributed every later result to the wrong path: a
    data-corruption class defect with no error and no AnalysisFailure to
    observe. With skip_generated=False such a file now holds its
    position as a None element — the same value single-file analyze
    returns for it, and one to_sarif already skips — so the documented
    zip is finally safe (#1238). The skip_generated=True default is
    unchanged: a skipped file, generated or unreadable, still yields no
    element. The typed surface widens to match: analyze_batch and
    analyze_paths are now annotated
    list[FuncSpaceDict | AnalysisFailure | None] in _native.pyi, so a
    mypy --strict consumer indexing a slot without a None check is
    told to add one rather than discovering it at runtime. An untyped
    skip_generated=False consumer sees the change at runtime instead: a
    loop that indexed every slot used to run to completion on such a batch
    (silently mis-paired) and now raises TypeError: 'NoneType' object is not subscriptable at the placeholder, and len(results) grows.
    That is the intended direction — loud and local beats silent and
    downstream — but it is a behaviour break, and analyze_paths shares
    it, so a directory walk under skip_generated=False gains one element
    per discovered file the gate declined. Widening a return union is not
    an additive change under STABILITY.md; the exception
    and its reasoning are recorded there under Python bindings →
    Typing
    .

  • Ruby regex literals and Perl bare match literals fabricated division
    operators: the delimiter tokens under the literal wrapper were
    classified through the generic / operator arm, so x = /abc/
    reported a division with none in the source. Both are now
    parent-guarded to Unknown under regex / pattern_matcher, the
    same compound-leaf shape #1256 applied to Elixir sigils. Ruby's
    %r{…} spellings alias onto the same token and are covered; a
    standalone a / b division still counts. Ruby and Perl Halstead
    operator counts drop accordingly (#1312). The remaining eight
    languages with this defect are tracked in #1314.

  • Literal delimiters fabricated operators in eight more languages, the
    rest of the class #1256 and #1312 fixed. A JavaScript, TypeScript,
    TSX or MozJS regex spelled both delimiters /; a Groovy slashy
    string spelled its closer /; a C++ or MozC++ raw string spelled its
    R"( opener (; and a Tcl or iRules braced word spelled its {
    the way a script block does. Each reported an operation absent from
    the source, and each moved with the author's choice of delimiter. All
    are now parent-guarded to Unknown under the literal wrapper, so a
    real division, call or block still counts (#1314).

  • JavaScript-family regex literals contributed no operand either —
    Regex was in neither the operator nor the operand arm, so /abc/g
    reached the Halstead vocabulary from neither side. It now counts as
    one operand, matching Ruby's regex and Elixir's sigils. n2/N2
    rise for JS-family code containing regex literals (#1314).

  • Perl's five pattern wrappers all scored zero, and are now split by
    what they are: /abc/, m/abc/ and qr/abc/ are pattern values
    and count as one operand each, while s/// and tr/// (with its
    y/// synonym) are operations applied to a target and count as
    operators, rendered in bca ops as s/// and tr///. Perl
    n1/N1/n2/N2 all move for code using patterns (#1314).

  • PHP string-interpolation openers fabricated a block operator. { is
    both the compound-statement brace and the complex-interpolation
    opener, so "{$x}" inflated the same {} vocabulary entry a real
    block uses — the worst case of the five interpolating languages, and
    the only one where the two share a token. Guarded in all four
    positions the grammar puts it (encapsed_string, heredoc_body,
    shell_command_expression, and the deprecated "${x}" form's
    dynamic_variable_name); a real block still counts (#1314).

  • Every bca subcommand man page omitted the CLI's global options
    (-w/--warnings, --report-skipped), and bca-vcs-commit.1 /
    bca-vcs-trend.1 additionally omitted the whole vcs history-tuning
    family. xtask rendered pages from a never-built clap::Command, and
    clap propagates global args into subcommands only during
    Command::build(). The drift gate could not catch it: the committed
    pages faithfully matched the wrong generator output. The regenerated
    pages gain the missing options and keep their existing
    bca-metrics-style synopsis spelling (#1248). The same pass removes
    two dangling cross-references: man/bca.1 pointed at bca-help(1)
    and man/bca-vcs.1 at bca-vcs-help(1), pages the renderer listed
    in SUBCOMMANDS but deliberately never wrote. clap's auto-inserted
    help subcommand is now suppressed in the rendering tree, so it is
    neither listed nor referenced; bca help <cmd> is unaffected at
    runtime.

  • C++ function-pointer data members (int (*fp)(int);) were counted as
    methods and skipped as attributes — both backwards. The
    function_declarator arm is now gated on the declarator child not
    being an indirection inside parentheses, and the attribute counter
    recurses through function_declarator / parenthesized_declarator
    so the field is reachable. Parenthesised method names
    (void (f)();) and Foo* operator->(); still count as methods
    (#1300).

  • C++ conversion operators declared without a body (operator float();
    and template<typename T> operator T();) were counted as neither a
    method nor an attribute — their declarator is an operator_cast, not
    a function_declarator, so both npm and npa skipped them. They
    now count as methods, in both the Cpp and Mozcpp grammars (#1298).

  • In TypeScript and TSX, a : string type annotation counted as both a
    Halstead operator (the predefined_type wrapper) and an operand (its
    inner anonymous "string" keyword token, added by #313 for parity
    with Checker::is_string) — one source token, two tallies, while
    : number / : boolean counted once. The keyword now counts exactly
    once, as the text-keyed string operator, symmetric with every other
    predefined type; Halstead length and vocabulary drop accordingly for
    annotation-dense TS/TSX code. Checker::is_string was narrowed in
    the same direction, so bca find --type string / bca count --type string report string literals and templates only, no longer the
    : string annotation keyword; string literals whose contents spell
    "string" are unaffected (#1261).

  • A Ruby stabby lambda (->(z) { … } / ->(z) do … end) opened two
    nested anonymous Function spaces — one for the Lambda node and a
    phantom zero-metric one for the Block / DoBlock that is the
    lambda's own body. #465 had fixed the closure count half; the
    space-promotion walk still promoted the body block a second time.
    The Block | DoBlock promotion is now gated on the same
    parent-is-not-Lambda predicate is_closure uses, so each stabby
    lambda opens exactly one space; keyword forms (lambda { },
    proc { }) and iterator blocks are unchanged. Serialized space
    trees for Ruby files with stabby lambdas lose the phantom entries,
    which also removes their zero contributions from function_spaces
    averages and per-file minimums (#1257).

  • Ruby cognitive complexity charged a stabby lambda's body twice for
    lambda nesting: both the Lambda wrapper and its own body Block /
    DoBlock incremented the surcharge, so f = ->(a) { if a then 1 end } scored 3 where the equivalent keyword form f = lambda { |a| if a then 1 end } scored 2. The body block is now excluded via the
    same shared stabby-lambda-body predicate as the closure count and
    the space tree (the sweep #1257's rationale mandated), and both
    forms score 2. Cognitive values drop by 1 per nesting-sensitive
    construct inside each stabby lambda; keyword lambdas, proc, and
    iterator blocks are unchanged.

  • Elixir sigil delimiter tokens (/, (, {, [, <, >, |)
    counted as ordinary Halstead operators, so the delimiter choice
    changed n1/N1 and ~r/abc/ fabricated two division operators.
    The delimiters are now suppressed when their parent is the sigil
    node (which is already the operand); ~ remains the single
    per-sigil operator, and standalone /, <, |, … in ordinary
    expressions — including inside a sigil's interpolation — still
    count. The ABC condition count had the same delimiter sensitivity
    through < / > (x = ~s<hi> scored 2 conditions, x = ~s(hi)
    zero) and now applies the same parent-is-sigil guard (#1256).

  • Elixir's bare _ -> catch-all arm (case / receive / rescue)
    and any unguarded true -> directly under a cond (shape-based,
    whatever the arm's position — matching Rust's position-blind
    bare-_ rule) counted toward standard cyclomatic complexity, while
    every sibling language excludes its default arm (Rust _ =>,
    Python case _:, Kotlin else ->, C-family default:, …). Both
    are now excluded; guarded wildcards (_ when …), named discards
    (_x ->), true -> under a case, and a multi-clause fn's
    trailing _ -> (its free base path is the #776 head-clause skip,
    so its 2nd+ clauses all count, in parity with the identical case)
    still count (#1272).

  • A Tcl try { … } on error { … } construct contributed zero to
    cyclomatic and cognitive complexity. Each on / trap handler now
    counts +1 in both metrics (finally stays free), matching catch
    and every exception-bearing sibling language, and the iRules Try
    sibling counts the same way — its on_handler / trap_handler
    nodes previously opened spurious anonymous function spaces,
    inflating nom, instead of counting as decisions (#1266).

  • A Tcl for loop contributed zero to cyclomatic and cognitive
    complexity because the grammar has no for rule — the loop parses
    as a generic command node. It is now recognised by command name
    at the same out-of-band slot as switch (#467) and counts like
    while / foreach, nesting increments included (#1264).

  • Every variable a Tcl script assigned was absent from Halstead
    operands: the parser emits the anonymous id kind in both of its
    positions, and the getter excluded it wholesale on the false
    premise that it only appears inside $var substitutions (whose
    wrapper is already the operand). The exclusion is now scoped to
    the substitution-leaf position, so set targets count in
    n2/N2, matching the iRules parent guard (#1294).

  • The enums code generator minted collision-breaking names
    (FooFoo2) without registering them, so a minted suffix could
    silently duplicate a node kind whose own sanitized name is literally
    base + digits. tree-sitter-php is one aliased rule away from
    exactly that — it already emits cast_type_token1 through
    cast_type_token12 — and the generator would have exited 0, leaving
    the duplicate to surface as a rustc duplicate-variant error in the
    parent crate during a grammar bump, where it reads as upstream
    breakage. Minted names are now probed against, and registered in, the
    taken-name set. No shipped artifact changes: no current grammar
    reaches the collision, so every generated file is byte-identical
    (#1237).

  • bca check --baseline rendered a malformed, double-signed
    [regr +-29%] tag for every mi.* regression. Baseline::classify
    has been direction-aware since #827 — for the lower-is-worse mi.*
    family a drop below the recorded value is the regression — but the
    tag formatter still computed (value - recorded) / recorded and
    prefixed a literal +, so the percentage carried its own minus sign.
    Anything grepping the documented [regr +N%] shape mis-parsed it. The
    magnitude is now measured in the metric's own direction, keeping one
    tag shape across every metric; the higher-is-worse rendering is
    unchanged (#1242).

  • The VCS bot-author filter was case-sensitive despite
    DEFAULT_BOT_PATTERN's doc promising the opposite since the option
    shipped. BotFilter::new compiled with a plain Regex, and
    push_if_human matches the raw signature bytes before any
    lowercasing, so Dependabot[bot] was counted as a human author while
    its lowercase twin was excluded — diluting ownership and raising file
    risk on the same repository depending only on how a bot capitalised
    itself. A user pattern behaves as written now too:
    --bot-pattern renovate matches Renovate Bot. Prefix (?-i) to
    restore exact matching. CACHE_SCHEMA_VERSION moves 1 → 2, since the
    cache fingerprint hashes only the pattern string and an event log
    walked under the old matcher would otherwise replay with its excluded
    set intact; the bump costs one cold walk (#1265).

  • The VCS window-boundary computation was non-saturating in two of its
    six sites — build_cached, the default path for bca vcs, and
    BlameWalk's constructor. Both operands are attacker-adjacent: the
    reference time is an --as-of value or a committer timestamp read out
    of an object header, and the window length is user-supplied up to
    roughly i64::MAX through --long-window. A plain - therefore
    panicked in a debug build and, in release, wrapped to a boundary in
    the far future, silently reversing the pure-hit check, the candidate
    filter, prune_and_dedup, and the walk_long_boundary persisted for
    future runs. All six sites now route through one
    vcs::options::window_boundary helper that documents and pins the
    saturation invariant once. days_between carried the same hazard
    under a .max(0) clamp that a wrapped (positive) delta defeats, and
    saturates too (#1271).

  • A Bash arithmetic ternary — the only ternary form Bash has —
    contributed nothing to cyclomatic or cognitive complexity, so
    local m=$(( a > b ? a : b )) reported cyclomatic 1 (base only) and
    cognitive 0, where the same construct scores 2 / 1 in every sibling
    that has a ternary. It is now a decision point in both cyclomatic
    tiers and a nesting construct for cognitive, matching the C-family
    ConditionalExpression. Both arithmetic contexts are covered — the
    $(( … )) expansion and the bare (( … )) statement. Bash
    cyclomatic, cognitive and the mi family derived from them shift
    for files using arithmetic ternaries (#1268).

  • Go ABC scored zero assignments for a var declaration with an
    initializer. var x = 5 and x := 5 are the same binding spelled two
    ways, but only the latter is a short_var_declaration; the former is a
    var_spec, which no arm matched. An initialized var_spec — typed or
    not, standalone or inside a grouped var ( … ) block — is now one
    assignment, matching what Rust, Java, C, C++, C#, JS, Lua and PHP
    already count for the same construct (each measured). const stays
    excluded, as does an uninitialized var z int. Go abc values rise
    by one per initialized var spec (#1278).

  • Constructor delegation scored zero ABC branches in Java, C# and
    Kotlin. super(…) / this(…) parses as explicit_constructor_invocation
    in Java, : base(…) / : this(…) as constructor_initializer in C#,
    and : super(…) as constructor_delegation_call in Kotlin — none of
    which is the call kind each language's branch counter matched, so a
    delegation contributed nothing where Groovy scored one for identical
    source. Each is now one branch, per Fitzpatrick's "one per function
    call". Calls in a delegation's argument list are separate nodes and
    still count on their own, so super(f()) is two branches, not three.
    Java, C# and Kotlin abc values rise by one per delegating
    constructor (#1279).

  • Ruby ABC counted the < of a superclass clause as a comparison
    condition, so every subclass declaration scored a phantom condition —
    class Foo < Bar with a single assignment inside reported
    conditions = 1 for a file with no conditional in it. LT / GT are
    now counted only under a binary parent, the positive polarity Rust,
    Go, C, C++, Java, Groovy, C#, Kotlin and the JS family already use.
    The same gate stops counting the < that names an operator method
    (def <(other)). Real comparisons, <=>, the << shovel and heredoc
    openers are unaffected. Ruby abc values drop by one per subclass
    declaration and per operator-method definition (#1280). The gate now
    covers the sibling comparison and equality tokens too — ==, !=,
    ===, <=, >=, <=>, =~, !~ are each equally definable as an
    operator method, so def ==(other) scored the same phantom condition
    def <(other) did.

  • Bash ABC counted every I/O redirection as a condition. > and <
    spell a redirect as well as a comparison, and the grammar parents the
    redirect under file_redirect, so echo hi > out.txt reported
    conditions = 1 for a line with no test in it — the Bash instance of
    the #1280 polarity, missed by that issue's cross-language sweep. Both
    tokens now require a binary_expression parent; comparisons inside
    [[ … ]] and (( … )) are unaffected. Bash abc values drop by one
    per redirection (#1280).

  • Bash ABC scored no condition for the arithmetic ternary, so
    local m=$(( a > b ? a : b )) reported 1 where the equivalent C
    int m = a > b ? a : b; reports 2. #1268 brought Bash's only ternary
    form into cyclomatic and cognitive; ABC now counts it too, matching
    the C-family ConditionalExpression. Bash abc values rise by one
    per arithmetic ternary (#1268).

  • fix_includes returned its diagnostics in a different order on every
    run for identical input, because both producers push while iterating a
    HashMapfiles in build_include_graph, nodes in
    record_indirect_includes. bca preproc prints that Vec straight to
    stderr, so the same source tree emitted the same warnings in a
    different order each time. Measured at 40 distinct orders across 40
    runs of one 8-file input, against a single distinct set — the
    content was already deterministic, only the sequence was not. The
    sequence is now sorted, which needed Ord on the public
    PreprocDiagnostic (additive; see STABILITY.md). The inner half of
    this had already been fixed: IncludeCycle sorts its own member list
    "so the emitted diagnostic is deterministic across runs", and only the
    outer sequence was left. Found while establishing whether
    fix_includes was safe to fuzz (#1288).

  • The LANG::C arm of the preprocessor macro-replacement pass had no
    test that could observe it. parse_then_metrics_c_with_preproc_… was
    added by #721 to give that arm "direct coverage", but its only
    assertion is a cyclomatic_sum, and DBG ? x : 0 and $$$ ? x : 0
    have the same complexity — so it passed unchanged with the whole
    LANG::C | LANG::Cpp | LANG::Mozcpp arm of get_fake_code disabled,
    which is how this was measured. It now asserts the rewritten bytes.
    The Cpp sibling has the same blind spot but is separately covered by
    cpp_ast_source_reflects_preproc_expansion; the C arm had nothing.
    Found while verifying that the new preproc_macro fuzz target is not
    vacuous (#1154).

  • Halstead, PHP: variable references no longer count twice. $x parses
    as a variable_name wrapping a name leaf and both were classified as
    operands, so N2 roughly doubled on variable-dense PHP and n2 carried a
    sigil-less twin per variable; variable-variable syntax compounded it ($$a
    scored 3, $$$b scored 4). Name is now suppressed under a
    variable_name or dynamic_variable_name parent, and those wrappers under
    a dynamic_variable_name parent, so every reference counts once at any
    nesting depth. Volume, difficulty, effort and MI change for every PHP file
    (#1259).

  • Halstead, Elixir and C#: true / false / nil / null literals no
    longer count twice in N2. The grammar wraps the keyword leaf in a named
    literal node and get_op_type classified both as operands; because
    operands are keyed by source text, n2 hid the duplication while N2
    and the length, volume, difficulty and effort derived from it — inflated by
    one per literal occurrence. Elixir drops the leaves outright; C# parent-
    guards them, because operator true emits a bare leaf with no
    boolean_literal wrapper (#1253).

  • ABC, Java and Groovy: generic type syntax is no longer counted as
    conditions. A generic declaration (class Gen<T>, <T> void m(), Groovy's
    def <U> U m(U x)) scored two conditions per bracket pair, and a wildcard
    bound (List<? extends T>) scored one as a phantom ternary. Both tokens
    are now gated on their parent node kind, matching the polarity C, C++,
    Objective-C, mozcpp, Rust and Go already use (#1274).

  • npm, C++ and Mozcpp: a templated member function with an inline body
    was not counted. The template_declaration guard resolves through a shared
    helper that hunted a function_declarator, but a templated member with a
    body
    parses as template_declaration > function_definition and has no
    such node at that level — so the method scored zero while nom opened a
    function space for it and wmc weighted its cyclomatic, leaving three
    metrics disagreeing about one class. The helper now accepts a
    function_definition child outright rather than recursing into it, which
    also fixes a templated conversion operator (whose declarator is an
    operator_cast, so no function_declarator exists at any depth). C++
    npm values rise on classes with templated inline-bodied members (#1258).

  • bca count -t call and bca find call reported 0 on all C, C++ and Mozcpp
    source. Checker::is_call matched only the unsuffixed CallExpression
    kind_id, which is the grammar's always-aliased preproc_call_expression
    and never reaches kind_id(); every real call carries the aliased variant.
    No metric values are affected (#1254).

  • web: a misused author_hash_key on POST /v1/vcs and /v1/vcs/trend
    reported error_kind: "vcs_internal_error", the token reserved for backend
    faults, so a client branching on the token saw its own mistake as a server
    failure. It now carries vcs_invalid_author_hash_key. The status was
    already correct (400) (#1245).

  • bca metrics --output <FILE> and bca ops --output <FILE> wrote the
    aggregate document's per-file elements in worker-completion order, so two
    runs over an unchanged tree produced differently-ordered files at
    --jobs > 1. The elements are now sorted by emitted path in every format
    (JSON, YAML, TOML, CBOR, CSV), making the artifact diffable and usable as a
    cache key. Multi-seed runs change too: the walk sorts per seed and
    concatenates, so bca metrics -p b -p a --output x.json previously emitted
    b's files first and now emits one globally sorted document (#1244).

  • bca check's remediation block printed a baseline-refresh command in the
    pre-#597 flag order (bca --paths … check …), which exits 1 with a clap
    usage error. The command now names the check subcommand first and mirrors
    every flag that decides what --write-baseline records — including
    --threshold, --no-config, --check-exclude and --check-exclude-from,
    without which the suggested refresh wrote a different baseline than the
    gate measured, or failed outright (#1243).

  • The man-page drift gate now fails on a newly generated page.
    git diff --exit-code -- man/ reports tracked content only, so the page
    cargo xtask writes for a brand-new subcommand passed both the CI
    manpage job and make manpages-check green and silently never shipped.
    The check moved out of the two hand-mirrored shell blocks into
    utils/check-manpage-drift.py, which both sites now call, and covers
    modified, deleted, and added pages (#1249).

  • cargo xtask deleted the man page it had just written when a command was
    renamed case-only, on macOS/APFS and Windows/NTFS, while still exiting 0.
    render_man_page's collision guard folded ASCII case; sweep_orphans
    compared byte-for-byte. Because case-insensitive filesystems are also
    case-preserving, the write for BCA.1 landed in the existing bca.1
    directory entry and the case-sensitive sweep then unlinked it as an orphan.
    The relation now has a single definition that both sites call, and the
    sweep classifies three ways rather than two: byte-equal keeps, no match
    removes, and a case-only match is refused with an error naming every
    conflicting spelling (#1250).

  • The pre-tag cargo publish --dry-run for big-code-analysis was skipped
    on every release, not only the first. It was gated on a crates.io sparse
    index probe for a leaf version that the Lockstep policy guarantees is the
    version being released — and therefore never published yet — so the branch
    that runs the dry-run was unreachable, while release.yml and
    RELEASING.md both told a maintainer it became a hard gate from the second
    tag onwards. big-code-analysis-cli and big-code-analysis-web had no
    such check at all: both pin big-code-analysis = "=<version>", so neither
    could have been dry-run either. Replaced with make check-publish-metadata
    (utils/check-publish-metadata.py), a registry-independent gate over every
    publishable crate's crates.io-required metadata, [package].include
    whitelist, and packaged size, wired into release-check, lint,
    pre-commit, ci, and the release workflow's preflight. It reads resolved
    fields from cargo metadata so [workspace.package] inheritance is
    honoured, and resolves include.workspace = true separately because
    cargo metadata does not emit include. Its cargo package --list runs
    --locked, carrying over the flag from the cargo publish --dry-run --locked it replaced: without it a stale Cargo.lock is silently
    re-resolved and rewritten and the gate still reports a pass, so a tag
    could be cut without the committed lockfile ever being verified (#1224).

  • make release-check now probes cargo-deny and cargo-about before
    running them, naming the missing tool and its exact install command instead
    of surfacing cargo's generic no such command partway through the gate.
    Both are listed by make check-tools and documented in RELEASING.md. The
    cargo-about hint spells --features cli: the binary sits behind a
    non-default feature, so a bare cargo install cargo-about compiles the
    library, installs no binary, reports the miss as a warning, and exits 0
    (#1226).

  • make release-check rejects uncommitted changes in the vendored grammar
    leaves before its slow stages, with the commit → gate → tag ordering as the
    remedy rather than cargo's misleading --allow-dirty hint — passing that
    flag would dry-run content differing from the tag, defeating the gate. The
    check is scoped to what cargo publish --dry-run actually rejects (tracked
    changes inside the five leaf directories), measured rather than assumed, so
    it cannot fail on trees cargo would accept. RELEASING.md's pre-release
    checklist now carries the gate and that ordering explicitly, and notes the
    push is separable (#1225).

  • The CLI and web crates no longer terminate on a library parse error.
    All fifteen .expect(FEATURES_PINNED) call sites — eight in bca's
    dispatch helpers, seven in bca-web's handlers, plus the constant
    itself in each crate — now propagate onto the error channel each
    caller already had: an io::Error of kind InvalidData that the
    concurrent runner reports per file and continues past, and the
    existing sanitized 500 that logs the cause server-side. The pinned
    all-languages feature does make MetricsError::LanguageDisabled
    unreachable, but MetricsError is #[non_exhaustive] and documents
    that variants may be added in a minor release, so the expect was a
    panic scheduled against a routine dependency bump rather than an
    invariant. No behaviour changes today: the only reachable outcome is
    still success (#1152).

Changed

  • Groovy's qualified-type operand classification is now pinned by a
    regression test (#1352). #1263 removed both QualifiedName and
    QualifiedType from GroovyCode::get_op_type's operand arm, but only
    the QualifiedName half was covered; re-adding the emitted alias
    QualifiedType2 failed no test, leaving the "complete the alias list"
    reading — which is the double count #1263 removed — unobstructed. No
    metric values change.

  • gix 0.86 → 0.87.1, with the rest of the gitoxide family advancing
    in step (36 lockfile entries). Forced rather than routine: every
    published version of bisync, a transitive dependency of
    gix-protocol 0.64, was yanked on 2026-08-24, so cargo deny's
    advisories check failed on any lockfile still holding it and no
    cargo update -p bisync could satisfy the ^0.3.0 requirement.
    gix-protocol 0.65 dropped the crate. One source change: gix-date
    now takes its "now" reference as a jiff::Zoned, so --as-of
    parsing passes gix::date::Zoned::now() instead of SystemTime.
    No behaviour change and no public-API change — no gix type appears
    in a public signature. The six excluded crates lock no gix and are
    untouched.

  • The pdf.js corpus test now covers all 384 files: the 118-entry
    exclude list frozen in the mozjs-default era (#84) was retired
    (#1282), since every entry parses without ERROR nodes under the
    post-#507 upstream tree-sitter-javascript grammar. The issue's own
    probe had reported 3 residual failures, but those were rg ERROR
    matching source text (MAX_ERROR, a regex literal) rather than
    parse-error nodes. The 118 orphaned mozjs-era snapshots were
    refreshed to current metric output, and the 5 DeepSpeech orphan
    snapshots (files still excluded under #86) were deleted, so on-disk
    snapshot counts match the asserted counts for every corpus. The
    corpus harness now asserts that inverse direction — a .snap with no
    corresponding corpus file fails the test naming the orphan — so a
    future exclude or corpus change cannot strand snapshots silently.

  • The tree-sitter runtime is =0.26.12, up one upstream patch
    release, pinned in lockstep across the root manifest, enums, and
    the five vendored bca-tree-sitter-* crates. tree_sitter is
    re-exported from the library root, so the resolved version is visible
    to consumers. Nothing in the release reaches this workspace's
    behaviour: include/tree_sitter/api.h and the Rust bindings are
    byte-identical to 0.26.11, so TREE_SITTER_LANGUAGE_VERSION stays
    at 15 and the vendored parser.c sources are unaffected, as is the
    Send + Sync argument the PyO3 bindings rest on. The three C fixes
    are an error-recovery restart when the parser is already in
    ERROR_STATE, a has_later_named_siblings correction in the tree
    cursor, and query-anchor semantics for skipped quantifiers — the
    last inert here, since this workspace uses no tree-sitter query API.
    No metric value moves.

  • Ruby's and Elixir's #{ interpolation opener no longer counts as a
    Halstead operator. Unlike PHP's {, which aliases the
    compound-statement brace and was fabricating a block (see Fixed),
    #{ is a token of its own and nothing was miscounted — this is a
    deliberate change of rule, so that the six interpolating languages
    agree. Kotlin, C# and Groovy already declined to count theirs; an
    interpolation opener is spelling rather than an operation, and the
    interpolated expression's own operators are counted either way. Ruby
    and Elixir spell the marker with the same token, so leaving either
    counted would have made the two disagree on one construct. Halstead
    operator counts drop for every literal that interpolates: Ruby
    strings, symbols, regexes, heredocs and subshells, and Elixir
    strings, charlists and sigils (#1314).

  • The enums generator's sanitize_string / get_token_names lost their
    escape: bool parameter. No production caller passed true: #862
    established that the JSON generator, the last one, was double-escaping
    by mistake. Removing the flag deletes the dead double-backslash branch,
    its test, and the JSON_TOKEN_ESCAPE constant that existed only to
    document why the answer must be false — making the #862 bug
    unrepresentable rather than one flipped boolean away. Internal build
    tool only; generated output is byte-identical (#1241).

  • The workspace-excluded enums crate declares the workspace lint posture
    (clippy::pedantic, missing_docs) in its own manifest.
    [workspace.lints] reaches members only, so make enums-check had been
    gating the crate at -D warnings against the compiler defaults while
    reading as a full lint gate. Clearing the table cost 38 findings, not the
    23 pedantic ones alone — missing_docs accounted for the other 15 — and
    none was silenced with a blanket allow.
    utils/check-excluded-manifests.py gained a third invariant so this cannot
    recur: every workspace-excluded crate must declare its own [lints] table
    or be named in the gate's exempt set. It is an exempt list rather than a
    required list, so the next excluded crate has to make the decision
    explicitly instead of inheriting the silence. The five vendored
    tree-sitter-* grammar crates are exempt — their Rust is generated binding
    boilerplate a regeneration replaces wholesale (#1228).

  • make py-fmt, py-fmt-check and py-lint resolve
    big-code-analysis-py/.venv/bin/ruff before PATH, the way py-typecheck
    already resolved mypy and pyright, so the local gate runs the
    uv.lock-resolved ruff rather than whichever unpinned copy mise.toml,
    the Dockerfile, or a bare pipx install ruff left on PATH. Those three
    provisioning paths stay unpinned deliberately: an exact version in any of
    them would be a fifth ungated copy to keep in lockstep, which is the
    failure this change exists to prevent (#1230).

  • clippy::arithmetic_side_effects is enforced on the loc metric
    module, and the span arithmetic there is now explicitly saturating.
    Loc is the one metric computing on tree-sitter row coordinates, and
    #1051 was a usize underflow of exactly that shape — a Rust doc
    comment at EOF drove end - 1 below zero from an input as small as
    /// x, panicking in debug and wrapping to usize::MAX in release.
    Validated by replaying the lint against the pre-#1051 tree, where it
    flags both reported panic sites. Metric values are unchanged: a
    saturating operation is identical to the plain one unless it would
    have overflowed, and none does (#1152).

  • clippy::indexing_slicing is enforced on src/c_macro.rs, the C/C++
    macro-masking byte lexer, with nine per-function carve-outs each
    naming the bound that makes its indexing safe. Validated by replaying
    it against the pre-#126 tree, where it flags the &DOLLARS[..] slices
    that panicked on macro identifiers longer than 2048 bytes. The one
    slice whose bound is established in a different function —
    step_raw_string's delimiter comparison, carried through
    LexState::RawString — is hardened with get rather than allowed
    (#1152).

  • clippy::unwrap_used is enforced on production code across every
    crate, as #![cfg_attr(not(test), warn(...))] at each of the eight
    lib/bin roots rather than a [workspace.lints] entry: a Cargo lint
    applies to every target of its package, and the ban is a production
    rule — this workspace has 0 production unwrap() calls against
    1,023 legitimate ones in test targets. cfg(test) is set for
    integration-test crates as well as the unit-test target, so the gate
    needs no per-file carve-out and carries zero #[allow]s. Adopting it
    costs nothing today and fails CI on the first production unwrap()
    added. clippy::expect_used is deliberately not enabled — all 37
    production expect sites already name their invariant in the message,
    the form AGENTS.md sanctions. The workspace-excluded enums codegen
    crate carries the same gate: it is CI-linted by make enums-check but
    invisible to cargo clippy --workspace, so its 7 production
    unwrap() calls were outside the original count. They now propagate
    onto the io::Result each generator already returned, except the Go
    generator's max() width, which becomes unwrap_or(0). None was a
    reachable crash: every one rests on an invariant as solid as the 37
    expect sites left alone. The difference is that an unwrap() states
    no invariant, which is the whole basis for gating it (#1227).

  • The Python bindings' ruff config states its rule set absolutely
    (select) instead of relative to ruff's defaults (extend-select),
    and the dev-extra bound moves to ruff>=0.13,<0.17 with uv.lock
    and the requirements/ exports resolving 0.16.2. ruff 0.16.0 grew its
    default rule set from 59 rules to 413, which under extend-select
    took this config from 265 enabled rules to 501 and pulled in 28
    families it never selected — PLC / PLE / PLR / PLW among
    them, silently overriding the deliberate omission of Pylint's design
    rules that the config comment states. The same release dropped 18
    opinionated E / F rules from the defaults, which a
    defaults-relative config would have lost just as quietly. Under
    select the count is 265 on 0.15.22 (unchanged, so nothing was lost
    by dropping the implicit defaults) and 268 on 0.16.2, the three
    additions being new RUF rules in an already-selected family.
    ruff check passes on 0.15.22 and 0.16.2 with no noqa, no
    suppressions, and no source changes; the three diagnostics 0.16
    reported (DTZ001, BLE001, PYI044) were all in families this
    config never asked for. The ruff-pre-commit rev: in
    .pre-commit-config.yaml moves to v0.16.2 to match what uv.lock
    resolves (it had drifted to v0.15.14 against a locked 0.15.22).
    The floor moves off 0.6 in the same breath because it was already
    fiction: UP038 left ruff's stable set in 0.13, and every release
    from 0.6.0 through 0.12.12 flags the same five
    isinstance(x, (A, B)) sites here — under the old extend-select
    config identically, so this states what was always true rather than
    changing anything (#1222).

Security

  • Cleared the OpenSSF Scorecard Vulnerabilities alert for
    RUSTSEC-2026-0258 (h2 queues empty HTTP/2 DATA frames without
    limit). The flagged crate was h2 0.3.27, reached only through
    actix-web's default http2 feature, and no patched 0.3 release
    exists: actix-http 3 is pinned to h2 0.3 and the fix ships only
    in h2 0.4.16. big-code-analysis-web now builds actix-web
    without http2 — the daemon binds plaintext, and actix negotiates
    HTTP/2 only over TLS ALPN, so the feature was never reachable — which
    drops h2 0.3 from Cargo.lock altogether. A web-crate test pins the
    lockfile against any h2 older than the fix, because cargo-deny's
    graph builder drops that crate before its advisory and ban checks run
    and so cannot guard it.