diff --git a/CHANGELOG.md b/CHANGELOG.md index 853f1d03..20e6ee2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,9 +53,48 @@ for historical reference. workflow runs those scripts against a cheap dev build on any PR that touches the release/wheel plumbing, so a future metric rename or serialization change reds a PR check instead of a release. +- `wire::MAX_SPACE_SERIALIZE_DEPTH` (128) and `MAX_AST_SERIALIZE_DEPTH` + (512): the nesting depths past which `FuncSpace` / `Ops` and `AstNode` + refuse to serialize (#1056). Both are set far clear of real source: + across the 14 450-file corpus under `tests/repositories` (TensorFlow, + DeepSpeech, serde, …) the deepest AST is 188 levels and the deepest + space nesting is 10. The space limit is also more permissive than the + read side, where a document caps out near 61 levels — `serde_json`'s + own 128-level `Deserializer` limit charges two levels per space. + +### Changed + +- **(breaking)** `FuncSpace`, `Ops`, `AstNode`, `wire::FuncSpace`, and + `wire::Ops` now implement `Drop` (#1056), so fields can no longer be + moved out of one by value: `let m = space.metrics;` becomes + `let m = space.metrics.clone();` or `let m = &space.metrics;` + (`E0509`). The compiler-generated `Drop` glue recursed once per + nesting level and aborted the process on a deep tree — reachable + through `bca-web`'s 4 MiB body cap — and an explicit `Drop` that + hoists descendants into a flat work list is the only way to break + that chain. [`STABILITY.md`](./STABILITY.md) reserves source-level + shape breaks for a major bump; this one is landed under a minor as a + deliberate, documented exception, because the alternative was leaving + a reachable remote process abort open until `3.0`. The mechanical fix + at each call site is a `.clone()` or a borrow; 13 sites inside this + repository needed it. ### Fixed +- `wire::FuncSpace::from` and `wire::Ops::from` no longer recurse + (#1056). Both projected a nested tree with + `spaces.iter().map(Self::from).collect()`, one stack frame per nesting + level at roughly 2.3 KB each, which aborted the process at ~900 levels + on a default 2 MiB thread. They now walk an explicit work stack and + convert a 1 000 000-level chain on a 512 KiB thread. +- Serializing a `FuncSpace`, `Ops`, or `AstNode` deeper than its limit + now fails with an ordinary serializer error naming the type and the + limit, instead of overflowing the stack (#1056). `serde` cannot emit a + tree without one native frame per level — `serialize_field` must run + the child's `Serialize` to completion before returning — so the depth + is bounded rather than de-recursed, mirroring the 128-level recursion + limit `serde_json`'s `Deserializer` already applies to the same + documents. - The `cognitive` metric's nesting lookup is no longer quadratic in nesting depth (#1062). It recovered each node's inherited nesting via `node.parent()`, which is `O(depth)` — tree-sitter stores no parent @@ -150,6 +189,21 @@ for historical reference. ### Security +- Closed a remotely-triggerable process abort in the recursive + `Serialize` and `Drop` paths (#1056). `bca metrics -O json` on ~1 000 + nested functions (11 KB of source) overflowed the thread stack, and a + stack overflow is a `SIGABRT`, not a catchable panic: `bca-web`'s + `spawn_blocking` wrapper turns a panic into one failed request, but an + abort takes the whole process down with every request in flight. Three + recursions were involved, all now bounded — see the **Fixed** and + **Changed** entries below. Reachable payloads were small: ~11 KB of + nested `fn`s for the serialization overflow, ~80 KB of nested + parentheses for the `/ast` one, both far inside the 4 MiB body cap. + + Issues #700 / #709 had converted every AST *traversal* to an explicit + work stack; this was the same hazard in the recursive *types*, which + those tests did not reach. + - Narrowed a remotely-triggerable CPU-exhaustion vector: a few kilobytes of deeply nested source could pin a core for minutes against the unauthenticated `bca-web` endpoints, whose parse deadline frees the diff --git a/Cargo.lock b/Cargo.lock index 829e71a2..071750c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,7 +364,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bca-tree-sitter-ccomment" -version = "2.0.1" +version = "2.1.0" dependencies = [ "cc", "tree-sitter-language", @@ -372,7 +372,7 @@ dependencies = [ [[package]] name = "bca-tree-sitter-mozcpp" -version = "2.0.1" +version = "2.1.0" dependencies = [ "cc", "tree-sitter-cpp", @@ -381,7 +381,7 @@ dependencies = [ [[package]] name = "bca-tree-sitter-mozjs" -version = "2.0.1" +version = "2.1.0" dependencies = [ "cc", "tree-sitter-javascript", @@ -390,7 +390,7 @@ dependencies = [ [[package]] name = "bca-tree-sitter-preproc" -version = "2.0.1" +version = "2.1.0" dependencies = [ "cc", "tree-sitter-language", @@ -398,7 +398,7 @@ dependencies = [ [[package]] name = "bca-tree-sitter-tcl" -version = "2.0.1" +version = "2.1.0" dependencies = [ "cc", "tree-sitter-language", @@ -406,7 +406,7 @@ dependencies = [ [[package]] name = "big-code-analysis" -version = "2.0.1" +version = "2.1.0" dependencies = [ "aho-corasick", "bca-tree-sitter-ccomment", @@ -462,7 +462,7 @@ dependencies = [ [[package]] name = "big-code-analysis-cli" -version = "2.0.1" +version = "2.1.0" dependencies = [ "assert_cmd", "big-code-analysis", @@ -484,7 +484,7 @@ dependencies = [ [[package]] name = "big-code-analysis-py" -version = "2.0.1" +version = "2.1.0" dependencies = [ "big-code-analysis", "globset", @@ -498,7 +498,7 @@ dependencies = [ [[package]] name = "big-code-analysis-web" -version = "2.0.1" +version = "2.1.0" dependencies = [ "actix-rt", "actix-web", @@ -5102,7 +5102,7 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xtask" -version = "2.0.1" +version = "2.1.0" dependencies = [ "big-code-analysis-cli", "big-code-analysis-web", diff --git a/Cargo.toml b/Cargo.toml index 8c717b89..8d7ead9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ exclude = [ ] [workspace.package] -version = "2.0.1" +version = "2.1.0" edition = "2024" rust-version = "1.94" license = "MPL-2.0" @@ -76,11 +76,11 @@ tree-sitter-c = "=0.24.2" # Rationale and the rejected alternatives live on the umbrella # issue (dekobon/big-code-analysis#149); the operational details # (publish order, first-bootstrap workflow) live in `RELEASING.md`. -tree-sitter-ccomment = { package = "bca-tree-sitter-ccomment", path = "./tree-sitter-ccomment", version = "=2.0.1" } -tree-sitter-mozcpp = { package = "bca-tree-sitter-mozcpp", path = "./tree-sitter-mozcpp", version = "=2.0.1" } -tree-sitter-mozjs = { package = "bca-tree-sitter-mozjs", path = "./tree-sitter-mozjs", version = "=2.0.1" } -tree-sitter-preproc = { package = "bca-tree-sitter-preproc", path = "./tree-sitter-preproc", version = "=2.0.1" } -tree-sitter-tcl = { package = "bca-tree-sitter-tcl", path = "./tree-sitter-tcl", version = "=2.0.1" } +tree-sitter-ccomment = { package = "bca-tree-sitter-ccomment", path = "./tree-sitter-ccomment", version = "=2.1.0" } +tree-sitter-mozcpp = { package = "bca-tree-sitter-mozcpp", path = "./tree-sitter-mozcpp", version = "=2.1.0" } +tree-sitter-mozjs = { package = "bca-tree-sitter-mozjs", path = "./tree-sitter-mozjs", version = "=2.1.0" } +tree-sitter-preproc = { package = "bca-tree-sitter-preproc", path = "./tree-sitter-preproc", version = "=2.1.0" } +tree-sitter-tcl = { package = "bca-tree-sitter-tcl", path = "./tree-sitter-tcl", version = "=2.1.0" } # PyO3 powers the `big-code-analysis-py` cdylib. Pinned to a major # series; bumping is a deliberate, separate change because the Bound diff --git a/STABILITY.md b/STABILITY.md index 91081711..d04fb51c 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -145,6 +145,19 @@ section. note below). `SpaceKind` additionally derives `Hash` (#552) so it can key a `HashMap`/`HashSet`. These derive additions only widen the trait set and are additive. + + `FuncSpace` implements `Drop` (as do `Ops`, `AstNode`, and their + `wire` counterparts), so its fields cannot be moved out of a value + by name — `let m = space.metrics;` is `E0509`; clone or borrow + instead. This is the one source-level shape break landed under a + minor bump rather than held for `3.0` (#1056): the + compiler-generated `Drop` glue recursed once per nesting level and + aborted the process on a tree deep enough to reach through + `bca-web`'s body cap, and an explicit `Drop` that flattens + descendants into a work list is the only way to break that + recursion. Treat the `Drop` impls themselves as an implementation + detail — the contract is that these trees tear down in constant + stack, not the presence of any particular `Drop` body. - `FunctionSpan` in `src/function.rs`. - **Offender / catalog enums** - `Severity` in `src/output/offenders.rs` (re-exported from the @@ -469,6 +482,21 @@ back with *this library's* serde stack: absent from the document stays absent, and `wire::CodeMetrics::selected()` rebuilds the `MetricSet` from the present keys. +**Nesting is bounded on the way out, too.** `serde` cannot emit a tree +without one native stack frame per level, and overflowing that stack +aborts the process instead of raising a catchable panic. Serialization +therefore stops at `wire::MAX_SPACE_SERIALIZE_DEPTH` (128) nested +`FuncSpace` / `Ops` levels and `MAX_AST_SERIALIZE_DEPTH` (512) nested +`AstNode` levels, failing with an ordinary serializer error naming the +type and the limit (#1056). The space limit mirrors the 128-level +recursion limit `serde_json`'s `Deserializer` already applies on the +way in — which, at two JSON levels per space, caps *reading* a document +back near 61 levels, so the emit limit is the more generous of the two. +For scale, the deepest space nesting across the 14 450-file corpus +under `tests/repositories` is 10 levels, and the deepest AST is 188. +Both limits may be raised in a minor bump; lowering one is a `3.0` +break. + The bit-exactness of float magnitudes is a property of *this library's* parser configuration, not of JSON text in general: a downstream consumer parsing the same document with a stock serde_json (no `float_roundtrip`) @@ -1167,8 +1195,11 @@ Every release MUST update [`CHANGELOG.md`][changelog]: may also carry an **(breaking)** entry *only* for an MSRV bump (see [MSRV policy](#msrv-policy)), which is a toolchain break, not a source-level shape break. Treat that as the single permitted - exception; any other `(breaking)` entry in a minor section is a - bug. + routine exception; any other `(breaking)` entry in a minor section + needs the justification spelled out in the entry itself. Exactly one + such entry exists: the `Drop` impls on the result trees in `2.1.0` + (#1056), landed early because holding them for `3.0` would have left + a remotely-reachable process abort open. - **Patch bumps** list every *known* value change and any internal-only refactors that have user-visible effects (e.g. a default that flips). A patch bump never carries **(breaking)**. diff --git a/big-code-analysis-book/src/commands/rest.md b/big-code-analysis-book/src/commands/rest.md index 2e005d69..95c2615e 100644 --- a/big-code-analysis-book/src/commands/rest.md +++ b/big-code-analysis-book/src/commands/rest.md @@ -136,12 +136,36 @@ Status codes: - `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. + on the server side. This also covers a response too deeply nested to + serialize (`serialize_failed`) — see + [Nesting limits](#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. +## Nesting limits {#nesting-limits} + +Responses are trees, and no serializer can emit a tree without one +native stack frame per level. Rather than let a deeply nested response +overflow the stack — which aborts the whole process, taking every +in-flight request with it — the server refuses to serialize past a +fixed depth and answers `500` with the `serialize_failed` token: + +| Response | Limit | What counts as a level | +|---|---|---| +| `/metrics` | 128 | one nested function space (a function inside a function inside a …) | +| `/ast` | 512 | one AST node | + +Both limits sit far above anything real source reaches. Across the +14 450-file corpus this project tests against (TensorFlow, DeepSpeech, +serde, …) the deepest AST is 188 nodes and the deepest function-space +nesting is 10. 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. + ## Content negotiation {#content-negotiation} The structured analysis endpoints — `/v1/ast`, `/v1/comment` (JSON diff --git a/big-code-analysis-cli/Cargo.toml b/big-code-analysis-cli/Cargo.toml index adbf5ba2..61a63fb5 100644 --- a/big-code-analysis-cli/Cargo.toml +++ b/big-code-analysis-cli/Cargo.toml @@ -20,7 +20,7 @@ ignore = "^0.4" # The CLI exposes every language `bca` advertises, so it pins the # library's full feature set explicitly. Disabling default features # here would silently drop grammars from the CLI build. See #252. -big-code-analysis = { path = "..", version = "=2.0.1", default-features = false, features = [ +big-code-analysis = { path = "..", version = "=2.1.0", default-features = false, features = [ "all-languages", # End-user binary ships every backend that exists; `vcs` is the # umbrella turning on `vcs-git` (issue #328). diff --git a/big-code-analysis-cli/src/markdown_report.rs b/big-code-analysis-cli/src/markdown_report.rs index cc6d4aa5..d8af53d3 100644 --- a/big-code-analysis-cli/src/markdown_report.rs +++ b/big-code-analysis-cli/src/markdown_report.rs @@ -1483,14 +1483,10 @@ mod tests { assert_eq!(out[0].name, "root.rs"); assert_eq!(out[DEPTH].name, format!("f_{}::f_inner", DEPTH - 1)); - // FuncSpace contains `spaces: Vec`, so - // letting the chained tree drop at scope exit walks - // one frame per nested space and would overflow this - // tight stack — masking the production-side - // assertions above with a Drop-side overflow on test - // exit. The OS reclaims the memory at process exit; - // this is fine for a test. - std::mem::forget(current); + // `current` drops at scope exit rather than being + // leaked: `FuncSpace`'s `Drop` is iterative as of #1056, + // so tearing the chain down costs no stack depth and + // cannot mask the production-side assertions above. }) .expect("spawn worker thread with bounded stack"); handle diff --git a/big-code-analysis-web/Cargo.toml b/big-code-analysis-web/Cargo.toml index 7ab0d472..b345797d 100644 --- a/big-code-analysis-web/Cargo.toml +++ b/big-code-analysis-web/Cargo.toml @@ -18,7 +18,7 @@ futures = "^0.3" # pins the library's full feature set explicitly. Disabling default # features here would silently drop grammars from the daemon build. # See #252. -big-code-analysis = { path = "..", version = "=2.0.1", default-features = false, features = [ +big-code-analysis = { path = "..", version = "=2.1.0", default-features = false, features = [ "all-languages", # `POST /vcs` change-history endpoint (issue #328). "vcs", diff --git a/docs/development/lessons_learned.md b/docs/development/lessons_learned.md index 76b401d4..8e18c4c1 100644 --- a/docs/development/lessons_learned.md +++ b/docs/development/lessons_learned.md @@ -2559,6 +2559,17 @@ iteratively unwind it before returning — otherwise a Drop-side overflow on test exit shadows the production-side correctness check and the test fails for the wrong reason. +**Update (#1056).** That last sentence no longer applies to +this repository's own trees: `FuncSpace`, `Ops`, `AstNode`, +and the two `wire` mirrors now carry hand-written iterative +`Drop` impls, so a deep chain tears down in constant stack. +The three `mem::forget` / flatten-before-drop workarounds +this lesson prescribed were removed with that fix, and +letting the tree drop normally is now the *stronger* test — +it exercises the iterative teardown instead of stepping +around it. The advice still stands for any recursive type +that lacks such a `Drop`. + --- ## 48. Hand-written enum lists need a match-based companion to enforce exhaustiveness @@ -4182,3 +4193,70 @@ nothing; here the masking mechanism is type coercion, not grammar error-recovery). --- + +## 81. De-recursing a traversal does not de-recurse the type it walks + +Converting every walk over a tree to an explicit work stack leaves three +recursions untouched, because they are generated rather than written: the +`From`/`map` projection that rebuilds the tree in another shape, the +derived `Serialize`, and the compiler-generated `Drop` glue. None of them +turns up in a traversal audit — there is no `fn walk(...)` to grep for — +and each still costs one native stack frame per nesting level. The +failure mode is worse than an ordinary bug: a stack overflow raises +`SIGABRT`, not a catchable panic, so a `catch_unwind` / `spawn_blocking` +harness that contains every other failure cannot contain this one. The +process dies, taking every in-flight request with it. + +**#700 / #709 de-recursed every walk and left the types recursive** +(#1056, `3fd01c70`). The three small-stack regression tests those issues +left behind all passed, because all three exercise the *dump* walk and +construct their fixtures by hand. Meanwhile `bca metrics -O json` on +1 000 nested functions — 11 KB of source — aborted the process, and the +`/ast` endpoint aborted on 80 KB of nested parentheses. Measured abort +depths on a default 2 MiB thread, release / debug: the recursive +`wire::FuncSpace::from` at ~900 / ~380, the derived `Serialize` at +3 072 / 384 for the most expensive format (TOML), and the derived `Drop` +between 32 768 and 65 536 / between 8 192 and 16 384. Nesting depth is +caller-controlled in every supported language, and ~380 000 nested `fn`s +fit inside `bca-web`'s 4 MiB body cap. + +**Per-level frame cost varies by an order of magnitude, so the stage +that overflows first is rarely the one the symptom implicates** (#1056). +The issue was filed against `Serialize`, and its own bisection supported +that: `bca check`, which builds and drops the identical tree without +serializing it, survived where `bca metrics -O json` aborted. But the +delegating `Serialize` first materialises the entire wire projection, and +*that* conversion was the overflow — ~2.3 KB per frame in release against +the JSON serializer's ~170 bytes. The same issue wrote `Drop` off after a +10 000-level chain survived; a cheap frame is not an iterative one, and it +aborts at 16 000 in a debug build. Measure each stage in isolation before +scoping the fix. + +**`Serialize` is the one that cannot be fixed, only bounded** (#1056). +`serde` offers no iterative escape: `serialize_field` must run the child's +`Serialize` to completion before it returns, so there is nowhere to put a +work stack. `serde_json`'s `Deserializer` already solves this on the way +*in* with a 128-level recursion limit; `Serialize` has no equivalent, so +the crate now supplies one (`wire::MAX_SPACE_SERIALIZE_DEPTH`, +`MAX_AST_SERIALIZE_DEPTH`) and fails with an ordinary serializer error +instead of an abort. `Drop` *can* be fixed outright, by hoisting +descendants into one flat work list so each node is dropped only after its +children have been moved out of it — at the cost of an `impl Drop`, which +forbids moving fields out of the type by value (`E0509`) and is therefore +a source-level SemVer break. Landing that break under a minor bump needed +an explicit, documented exception in `STABILITY.md`. + +**Lesson:** After de-recursing the walks over a recursive type, audit the +*type*: the projection that rebuilds it, every derived `Serialize`, and +the `Drop` glue — and record that `Clone`, `PartialEq`, and `Debug` +recurse for the same reason, whether or not any current path reaches +them. Exercise each stage separately on a bounded stack (lesson #47, +which covers how to bound it), because a fixture that clears one stage by +10x may clear the next by 0.5x — and prefer letting the tree drop +normally over `mem::forget`, so teardown is covered rather than stepped +around. Where the recursion is inside a generated `Serialize`, bound the +depth and return an error rather than assuming the walk-side fix reached +it: the overflow is an abort, and no panic-catching layer above it will +help. + +--- diff --git a/enums/Cargo.lock b/enums/Cargo.lock index 1a5dae1b..ceeb9dd5 100644 --- a/enums/Cargo.lock +++ b/enums/Cargo.lock @@ -125,7 +125,7 @@ dependencies = [ [[package]] name = "bca-tree-sitter-ccomment" -version = "2.0.1" +version = "2.1.0" dependencies = [ "cc", "tree-sitter-language", @@ -133,7 +133,7 @@ dependencies = [ [[package]] name = "bca-tree-sitter-mozcpp" -version = "2.0.1" +version = "2.1.0" dependencies = [ "cc", "tree-sitter-cpp", @@ -142,7 +142,7 @@ dependencies = [ [[package]] name = "bca-tree-sitter-mozjs" -version = "2.0.1" +version = "2.1.0" dependencies = [ "cc", "tree-sitter-javascript", @@ -151,7 +151,7 @@ dependencies = [ [[package]] name = "bca-tree-sitter-preproc" -version = "2.0.1" +version = "2.1.0" dependencies = [ "cc", "tree-sitter-language", @@ -159,7 +159,7 @@ dependencies = [ [[package]] name = "bca-tree-sitter-tcl" -version = "2.0.1" +version = "2.1.0" dependencies = [ "cc", "tree-sitter-language", @@ -233,7 +233,7 @@ dependencies = [ [[package]] name = "enums" -version = "2.0.1" +version = "2.1.0" dependencies = [ "askama", "bca-tree-sitter-ccomment", diff --git a/enums/Cargo.toml b/enums/Cargo.toml index 52ee9102..5662949c 100644 --- a/enums/Cargo.toml +++ b/enums/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "enums" -version = "2.0.1" +version = "2.1.0" publish = false authors = ["Calixte Denizet "] edition = "2024" @@ -37,11 +37,11 @@ tree-sitter-c = "=0.24.2" # Vendored grammar forks — see ../Cargo.toml for rationale on the # `package = ...` alias trick that preserves the `tree_sitter_` # import path while the published crate name is `bca-tree-sitter-`. -tree-sitter-tcl = { package = "bca-tree-sitter-tcl", path = "../tree-sitter-tcl", version = "=2.0.1" } -tree-sitter-preproc = { package = "bca-tree-sitter-preproc", path = "../tree-sitter-preproc", version = "=2.0.1" } -tree-sitter-ccomment = { package = "bca-tree-sitter-ccomment", path = "../tree-sitter-ccomment", version = "=2.0.1" } -tree-sitter-mozcpp = { package = "bca-tree-sitter-mozcpp", path = "../tree-sitter-mozcpp", version = "=2.0.1" } -tree-sitter-mozjs = { package = "bca-tree-sitter-mozjs", path = "../tree-sitter-mozjs", version = "=2.0.1" } +tree-sitter-tcl = { package = "bca-tree-sitter-tcl", path = "../tree-sitter-tcl", version = "=2.1.0" } +tree-sitter-preproc = { package = "bca-tree-sitter-preproc", path = "../tree-sitter-preproc", version = "=2.1.0" } +tree-sitter-ccomment = { package = "bca-tree-sitter-ccomment", path = "../tree-sitter-ccomment", version = "=2.1.0" } +tree-sitter-mozcpp = { package = "bca-tree-sitter-mozcpp", path = "../tree-sitter-mozcpp", version = "=2.1.0" } +tree-sitter-mozjs = { package = "bca-tree-sitter-mozjs", path = "../tree-sitter-mozjs", version = "=2.1.0" } [profile.release] debug = "line-tables-only" diff --git a/man/bca-check.1 b/man/bca-check.1 index 73671ed7..83988ceb 100644 --- a/man/bca-check.1 +++ b/man/bca-check.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-CHECK 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-CHECK 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-check \- Check per\-function metrics against thresholds. Exits 2 when any threshold is exceeded; reserve exit 1 for tool errors so CI can distinguish "metric regression" from "tool crashed". `\-\-strict\-exit\-codes` opts into tiered codes (2\-5) that split the violation case by severity .SH SYNOPSIS diff --git a/man/bca-count.1 b/man/bca-count.1 index 928ee249..005a7d2d 100644 --- a/man/bca-count.1 +++ b/man/bca-count.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-COUNT 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-COUNT 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-count \- Count nodes of one or more types .SH SYNOPSIS diff --git a/man/bca-diff-baseline.1 b/man/bca-diff-baseline.1 index 8f253364..4a00a47c 100644 --- a/man/bca-diff-baseline.1 +++ b/man/bca-diff-baseline.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-DIFF-BASELINE 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-DIFF-BASELINE 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-diff\-baseline \- Diff two `.bca\-baseline.toml` files and report what was added, removed, worsened, or improved. Replaces the in\-the\-head TOML diff parsing the book\*(Aqs PR\-review recipe used to walk through. Exits 0 on success by default — the diff is informational, not a gate. With `\-\-exit\-code`, exits 2 when the filtered diff is non\-empty .SH SYNOPSIS diff --git a/man/bca-diff.1 b/man/bca-diff.1 index d40f0789..171a9c15 100644 --- a/man/bca-diff.1 +++ b/man/bca-diff.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-DIFF 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-DIFF 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-diff \- Compare two metric\-output runs and report, per metric, which files changed (old to new), plus files added/removed between the two sets. Each side is a per\-file JSON file or a directory tree of them (the form `bca metrics \-O json \-\-output\-dir DIR` writes). Replaces the grammar\-bump glue chain — the external `json\-minimal\-tests` binary plus `split\-minimal\-tests.py` — with one native command. Exits 0 on success by default; the diff is informational, not a gate. With `\-\-exit\-code`, exits 2 when the filtered diff is non\-empty .SH SYNOPSIS diff --git a/man/bca-dump.1 b/man/bca-dump.1 index abb346de..d8d1055e 100644 --- a/man/bca-dump.1 +++ b/man/bca-dump.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-DUMP 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-DUMP 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-dump \- Dump the AST to stdout. Each file\*(Aqs tree is prefixed with a `== ==` banner so a multi\-file dump is attributable. Requires an explicit path — unlike the other walking subcommands, bare `bca dump` errors instead of dumping the whole current directory (a whole\-tree AST dump has no plausible use) .SH SYNOPSIS diff --git a/man/bca-exemptions.1 b/man/bca-exemptions.1 index 407d1772..3da83c53 100644 --- a/man/bca-exemptions.1 +++ b/man/bca-exemptions.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-EXEMPTIONS 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-EXEMPTIONS 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-exemptions \- Audit everything the `bca check` gate skips in one view: in\-source suppression markers (`bca: suppress`, `#lizard forgives`, …), `[check.exclude]` globs, and `.bca\-baseline.toml` entries. Read\-only; always exits 0 on success .SH SYNOPSIS diff --git a/man/bca-find.1 b/man/bca-find.1 index c51294b3..3ae9f50e 100644 --- a/man/bca-find.1 +++ b/man/bca-find.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-FIND 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-FIND 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-find \- Find nodes of one or more types .SH SYNOPSIS diff --git a/man/bca-functions.1 b/man/bca-functions.1 index bee39c15..70616534 100644 --- a/man/bca-functions.1 +++ b/man/bca-functions.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-FUNCTIONS 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-FUNCTIONS 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-functions \- List functions/methods and their spans .SH SYNOPSIS diff --git a/man/bca-init.1 b/man/bca-init.1 index 8e123aa6..c552ffe6 100644 --- a/man/bca-init.1 +++ b/man/bca-init.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-INIT 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-INIT 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-init \- Scaffold the canonical adoption files (`bca.toml` manifest, `.bcaignore`, `.bca\-baseline.toml`) in the current directory. Replaces the six\-step copy\-paste flow from the book\*(Aqs adoption recipe. Refuses to overwrite existing files without `\-\-force` .SH SYNOPSIS diff --git a/man/bca-list-metrics.1 b/man/bca-list-metrics.1 index 00a6a360..d3dd7e12 100644 --- a/man/bca-list-metrics.1 +++ b/man/bca-list-metrics.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-LIST-METRICS 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-LIST-METRICS 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-list\-metrics \- List the metrics this tool can compute and exit .SH SYNOPSIS diff --git a/man/bca-metrics.1 b/man/bca-metrics.1 index 797e6ee1..5186f081 100644 --- a/man/bca-metrics.1 +++ b/man/bca-metrics.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-METRICS 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-METRICS 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-metrics \- Compute per\-file metrics and emit them in a structured format .SH SYNOPSIS diff --git a/man/bca-ops.1 b/man/bca-ops.1 index 875d97fd..a3db68da 100644 --- a/man/bca-ops.1 +++ b/man/bca-ops.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-OPS 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-OPS 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-ops \- Extract per\-file operands and operators .SH SYNOPSIS diff --git a/man/bca-preproc.1 b/man/bca-preproc.1 index fa860464..9cea1d83 100644 --- a/man/bca-preproc.1 +++ b/man/bca-preproc.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-PREPROC 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-PREPROC 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-preproc \- Generate preprocessor\-data JSON for C/C++ analysis .SH SYNOPSIS diff --git a/man/bca-report.1 b/man/bca-report.1 index 038c42b7..70e1e209 100644 --- a/man/bca-report.1 +++ b/man/bca-report.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-REPORT 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-REPORT 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-report \- Generate an aggregated report across the analyzed source .SH SYNOPSIS diff --git a/man/bca-strip-comments.1 b/man/bca-strip-comments.1 index 94474aef..0a648b08 100644 --- a/man/bca-strip-comments.1 +++ b/man/bca-strip-comments.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-STRIP-COMMENTS 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-STRIP-COMMENTS 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-strip\-comments \- Remove comments from source files .SH SYNOPSIS diff --git a/man/bca-vcs-commit.1 b/man/bca-vcs-commit.1 index b76869a0..fa761528 100644 --- a/man/bca-vcs-commit.1 +++ b/man/bca-vcs-commit.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-VCS-COMMIT 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-VCS-COMMIT 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-vcs\-commit \- Score a single commit for defect\-induction risk — the just\-in\-time (JIT) defect\-prediction unit a CI gate reviews at check\-in. Emits a JSON breakdown of size / diffusion / history / experience / purpose features, their contributions, and an ordinal composite score. Window / `\-\-ref` / bot / merge / rename behaviour comes from the parent `vcs` flags. The old `jit` spelling stays a hidden alias for one release cycle .SH SYNOPSIS diff --git a/man/bca-vcs-trend.1 b/man/bca-vcs-trend.1 index 17701c2b..dee004fd 100644 --- a/man/bca-vcs-trend.1 +++ b/man/bca-vcs-trend.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-VCS-TREND 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-VCS-TREND 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-vcs\-trend \- Sample the change\-history metrics at several points in time and emit a per\-file time series, surfacing whether code is improving or degrading over the project\*(Aqs life. Each point re\-anchors at the mainline tip of that moment, so it is a faithful historical snapshot. Window / `\-\-ref` / bot / merge / rename / as\-of (the most\-recent anchor) and `\-\-top` (files kept) come from the parent `vcs` flags .SH SYNOPSIS diff --git a/man/bca-vcs.1 b/man/bca-vcs.1 index 14807e49..fbe38bbc 100644 --- a/man/bca-vcs.1 +++ b/man/bca-vcs.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-VCS 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-VCS 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-vcs \- Rank files by change\-history (VCS) risk: churn, commit and author counts, ownership dilution, and bug\- / security\-fix history over a git working tree. Errors clearly outside a repo .SH SYNOPSIS diff --git a/man/bca-web.1 b/man/bca-web.1 index 68288cb3..15a3ab18 100644 --- a/man/bca-web.1 +++ b/man/bca-web.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA-WEB 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA-WEB 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca\-web \- Run a web server. .SH SYNOPSIS @@ -34,6 +34,6 @@ Print help (see a summary with \*(Aq\-h\*(Aq) \fB\-V\fR, \fB\-\-version\fR Print version .SH VERSION -v2.0.1 +v2.1.0 .SH AUTHORS Calixte Denizet , Elijah Zupancic diff --git a/man/bca.1 b/man/bca.1 index a23823f2..9b7e9e9e 100644 --- a/man/bca.1 +++ b/man/bca.1 @@ -1,6 +1,6 @@ .ie \n(.g .ds Aq \(aq .el .ds Aq ' -.TH BCA 1 "big-code-analysis 2.0.1" "big-code-analysis Manual" +.TH BCA 1 "big-code-analysis 2.1.0" "big-code-analysis Manual" .SH NAME bca \- Analyze source code. .SH SYNOPSIS @@ -88,6 +88,6 @@ Every other subcommand exits 0 on success and 1 on error. Migrating from the flag\-style CLI? See the migration guide: https://dekobon.github.io/big\-code\-analysis/migration.html .SH VERSION -v2.0.1 +v2.1.0 .SH AUTHORS Calixte Denizet , Elijah Zupancic diff --git a/src/ast.rs b/src/ast.rs index cf27c539..0ce1b5e4 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -142,6 +142,32 @@ pub struct AstResponse { pub root: Option, } +/// Greatest AST depth [`AstNode`] will serialize. +/// +/// An `AstNode` tree is as deep as the parsed expression nesting, which a +/// caller controls directly — 100 000 levels fit in a 200 KB payload, well +/// inside `bca-web`'s body cap. `serde` cannot emit a tree without one +/// native stack frame per level, and overflowing that stack aborts the +/// process rather than raising a catchable panic (#1056), so the depth is +/// bounded: a deeper tree fails serialization with an ordinary serializer +/// error naming the limit. +/// +/// The bound is set well clear of real source: the deepest AST across the +/// ~8 000-file corpus under `tests/repositories` (TensorFlow, serde, +/// DeepSpeech, …) is 188 levels. It is also set well clear of the stack: +/// the earliest measured overflow of any emitted format was 2 000 levels +/// on a debug build's default 2 MiB thread. +pub const MAX_AST_SERIALIZE_DEPTH: usize = 512; + +/// Serializes an [`AstNode`]'s children one level deeper, refusing to +/// descend past [`MAX_AST_SERIALIZE_DEPTH`]. +fn serialize_children( + children: &[AstNode], + serializer: S, +) -> Result { + crate::recursion::serialize_bounded(children, MAX_AST_SERIALIZE_DEPTH, "AstNode", serializer) +} + /// Information on an `AST` node. /// /// Serialized as a flat object with `snake_case` keys: `type`, `value`, @@ -164,9 +190,16 @@ pub struct AstNode { /// equivalent children without grammar-specific positional knowledge. pub field_name: Option<&'static str>, /// The children of a node + #[serde(serialize_with = "serialize_children")] pub children: Vec, } +// AST depth is caller-controlled — 100 000 levels of nested parentheses +// fit in a 200 KB payload — so the compiler-generated `Drop` glue would +// recurse once per level and abort the process (#1056). See +// [`crate::recursion`]. +crate::recursion::impl_iterative_drop!(AstNode, children); + impl AstNode { /// Builds an `AstNode` with the supplied type, value, span, and /// children. The `field_name` is set to `None`; use @@ -515,4 +548,67 @@ mod tests { let span: Span = serde_json::from_str(legacy).expect("deserialize legacy span"); assert_eq!(span, Span::new(2, 3, 4, 5, 0, 0)); } + + // ----------------------------------------------------------------- + // Stack-depth regression tests (#1056) + // ----------------------------------------------------------------- + + /// A chain of `depth` nested [`AstNode`]s below the root, built + /// directly so the depth is exact rather than a function of how many + /// AST levels a grammar spends per source construct. + fn ast_chain(depth: usize) -> AstNode { + let mut node = AstNode::new("leaf", String::new(), None, Vec::new()); + for _ in 0..depth { + node = AstNode::new("branch", String::new(), None, vec![node]); + } + node + } + + #[test] + fn ast_depth_at_the_serialize_limit_is_accepted_and_one_deeper_is_not() { + let at_limit = ast_chain(MAX_AST_SERIALIZE_DEPTH); + serde_json::to_string(&at_limit).expect("the limit itself must serialize"); + + let past_limit = ast_chain(MAX_AST_SERIALIZE_DEPTH + 1); + let err = serde_json::to_string(&past_limit).expect_err("one deeper must be refused"); + assert!( + err.to_string().contains(&format!( + "AstNode nesting is deeper than the serialization limit of \ + {MAX_AST_SERIALIZE_DEPTH} levels" + )), + "the error must name the type and the limit, got: {err}" + ); + } + + #[test] + fn a_pathologically_deep_ast_errors_and_tears_down_without_overflowing() { + // The chain is built directly rather than parsed, but this depth + // is reachable through `bca-web`'s `/ast`: 50 000 levels of + // nested parentheses fit in 100 KB, an order of magnitude inside + // the 4 MiB body cap. Both the recursive `Serialize` and the + // compiler-generated `Drop` glue used to abort the process here — + // `SIGABRT`, which `spawn_blocking` cannot contain (#1056). + const DEPTH: usize = 50_000; + // The stack a `bca` consumer thread and a `tokio` blocking thread + // get; the serialization limit is dimensioned against it. + const PRODUCTION_STACK: usize = 2 * 1024 * 1024; + let message = std::thread::Builder::new() + .stack_size(PRODUCTION_STACK) + .spawn(|| { + let root = ast_chain(DEPTH); + let message = serde_json::to_string(&root) + .expect_err("depth past the limit must fail, not serialize") + .to_string(); + // `root` drops here: the iterative `Drop` must unwind + // 50 000 levels without touching the stack. + message + }) + .expect("spawn bounded-stack thread") + .join() + .expect("bounded-stack thread must not overflow"); + assert!( + message.contains("AstNode nesting is deeper than the serialization limit"), + "the error must explain the refusal, got: {message}" + ); + } } diff --git a/src/langs.rs b/src/langs.rs index 17c4d382..91906856 100644 --- a/src/langs.rs +++ b/src/langs.rs @@ -76,7 +76,7 @@ mk_langs!( tree_sitter_mozjs, [jsm], [], - "2.0.1" + "2.1.0" ), ( "java", @@ -148,7 +148,7 @@ mk_langs!( tree_sitter_tcl, [tcl, tk, tm], ["tcl"], - "2.0.1" + "2.1.0" ), ( "irules", @@ -210,7 +210,7 @@ mk_langs!( tree_sitter_mozcpp, [], [], - "2.0.1" + "2.1.0" ), ( "objc", @@ -311,7 +311,7 @@ mk_langs!( tree_sitter_ccomment, [], [], - "2.0.1" + "2.1.0" ), ( "c-family-helpers", @@ -323,7 +323,7 @@ mk_langs!( tree_sitter_preproc, [], [], - "2.0.1" + "2.1.0" ), ( "perl", diff --git a/src/lib.rs b/src/lib.rs index 4ae34f10..3fa9986b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -304,7 +304,10 @@ pub use crate::function::{FunctionSpan, dump_function_spans, dump_function_spans // --- AST dump --- mod ast; -pub use crate::ast::{AstCfg, AstNode, AstPayload, AstResponse, Span}; +pub use crate::ast::{AstCfg, AstNode, AstPayload, AstResponse, MAX_AST_SERIALIZE_DEPTH, Span}; + +// --- Stack-depth bounds shared by the crate's recursive types --- +mod recursion; // --- Halstead operator/operand result type --- mod ops; diff --git a/src/metrics/cyclomatic.rs b/src/metrics/cyclomatic.rs index 3d58769b..07b73cb3 100644 --- a/src/metrics/cyclomatic.rs +++ b/src/metrics/cyclomatic.rs @@ -1176,7 +1176,7 @@ mod tests { crate::MetricsOptions::default().with_count_cyclomatic_try(count_try), ) .expect("analyze must succeed on a well-formed Rust fixture"); - func_space.metrics.cyclomatic + func_space.metrics.cyclomatic.clone() } #[test] @@ -1212,7 +1212,7 @@ mod tests { crate::MetricsOptions::default(), ) .expect("analyze must succeed on a well-formed Rust fixture"); - func_space.metrics.cyclomatic + func_space.metrics.cyclomatic.clone() }; let explicit_on = rust_cyclomatic_with_try(true); assert_eq!(default_path.cyclomatic_sum(), explicit_on.cyclomatic_sum()); diff --git a/src/ops.rs b/src/ops.rs index df504de5..9b14939d 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -57,6 +57,11 @@ pub struct Ops { pub operators: Vec, } +// Space nesting is caller-controlled, so the compiler-generated `Drop` +// glue would recurse once per level and abort the process on a deep tree +// (#1056). See [`crate::recursion`]. +crate::recursion::impl_iterative_drop!(Ops, spaces); + impl Ops { /// Project this tree into its [`crate::wire::Ops`] form — the /// plain, `Deserialize`-capable record that defines the serialized diff --git a/src/output/dump_metrics.rs b/src/output/dump_metrics.rs index 5a93f313..4c4de594 100644 --- a/src/output/dump_metrics.rs +++ b/src/output/dump_metrics.rs @@ -366,15 +366,9 @@ mod tests { } let mut sink = termcolor::NoColor::new(Vec::new()); let ok = dump_space(&root, "", true, &mut sink).is_ok(); - // Flatten the chain before it drops: `FuncSpace`'s derived - // `Drop` recurses through `spaces`, so a deep tree would - // overflow the small stack on teardown and mask the dump - // result. Hoisting each level's children out turns the - // drop into an iterative one. - let mut node = root; - while let Some(child) = node.spaces.pop() { - node = child; - } + // `root` drops here without flattening: `FuncSpace`'s + // `Drop` is iterative as of #1056, so teardown costs no + // stack depth and cannot mask the dump result. ok }) .expect("spawn dump thread"); diff --git a/src/output/dump_ops.rs b/src/output/dump_ops.rs index 087412d0..65fa0b73 100644 --- a/src/output/dump_ops.rs +++ b/src/output/dump_ops.rs @@ -242,15 +242,9 @@ mod tests { } let mut sink = NoColor::new(Vec::new()); let ok = dump_space(&root, "", true, &mut sink).is_ok(); - // Flatten the chain before it drops: `Ops`'s derived - // `Drop` recurses through `spaces`, so a deep tree would - // overflow the small stack on teardown and mask the dump - // result. Hoisting each level's children out turns the - // drop into an iterative one. - let mut node = root; - while let Some(child) = node.spaces.pop() { - node = child; - } + // `root` drops here without flattening: `Ops`'s `Drop` is + // iterative as of #1056, so teardown costs no stack depth + // and cannot mask the dump result. ok }) .expect("spawn dump thread"); diff --git a/src/output/snapshots/big_code_analysis__output__sarif__tests__sarif_empty.snap b/src/output/snapshots/big_code_analysis__output__sarif__tests__sarif_empty.snap index c33040b5..a4d84841 100644 --- a/src/output/snapshots/big_code_analysis__output__sarif__tests__sarif_empty.snap +++ b/src/output/snapshots/big_code_analysis__output__sarif__tests__sarif_empty.snap @@ -10,7 +10,7 @@ expression: "render(&[])" "tool": { "driver": { "name": "big-code-analysis", - "version": "2.0.1", + "version": "2.1.0", "rules": [] } }, diff --git a/src/output/snapshots/big_code_analysis__output__sarif__tests__sarif_multi.snap b/src/output/snapshots/big_code_analysis__output__sarif__tests__sarif_multi.snap index 06344347..0982ebd4 100644 --- a/src/output/snapshots/big_code_analysis__output__sarif__tests__sarif_multi.snap +++ b/src/output/snapshots/big_code_analysis__output__sarif__tests__sarif_multi.snap @@ -10,7 +10,7 @@ expression: render(&offenders) "tool": { "driver": { "name": "big-code-analysis", - "version": "2.0.1", + "version": "2.1.0", "rules": [ { "id": "cognitive", diff --git a/src/recursion.rs b/src/recursion.rs new file mode 100644 index 00000000..230d9049 --- /dev/null +++ b/src/recursion.rs @@ -0,0 +1,136 @@ +//! Stack-depth bounds for the crate's recursive types. +//! +//! [`FuncSpace`](crate::FuncSpace), [`Ops`](crate::Ops), and +//! [`AstNode`](crate::AstNode) are trees whose nesting depth is +//! caller-controlled: nested functions, nested closures, and nested +//! expressions are ordinary legal constructs in every supported language. +//! Issues #700 / #709 converted every AST *traversal* to an explicit work +//! stack, but two implicit recursions survived on the types themselves — +//! `Serialize` and the compiler-generated `Drop` glue. Overflowing the +//! stack in either is not a catchable panic: the runtime aborts the +//! process with `SIGABRT`, taking every in-flight `bca-web` request with +//! it (#1056). +//! +//! `Drop` is made iterative outright by [`impl_iterative_drop`]. +//! `Serialize` cannot be: `serde` offers no way to emit a tree without one +//! native frame per level, because `serialize_field` must run the child's +//! `Serialize` to completion before it returns. So it is bounded instead — +//! [`serialize_bounded`] refuses to descend past a per-type limit and +//! returns an ordinary serializer error, mirroring the 128-level recursion +//! limit `serde_json`'s `Deserializer` already applies to the same shapes. +//! +//! The third recursion #1056 covered is not here: projecting a compute +//! tree onto its wire form. That one *is* de-recursed outright, by +//! `wire::map_tree`, which lives next to the `From` impls it serves. + +use std::cell::Cell; + +use serde::{Serialize, Serializer, ser::Error as _}; + +thread_local! { + /// Levels of bounded nesting currently being serialized on this thread. + /// + /// A thread-local rather than a parameter because `serde`'s + /// `serialize_with` hook receives only the field and the serializer. + /// It is shared by every bounded type; the crate's recursive types + /// never nest inside one another, and if they ever did, sharing the + /// counter is the conservative direction. + static DEPTH: Cell = const { Cell::new(0) }; +} + +/// Restores [`DEPTH`] when a bounded level finishes, including when the +/// serializer returns an error or a `Serialize` impl further down panics. +struct DepthGuard; + +impl Drop for DepthGuard { + fn drop(&mut self) { + DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1))); + } +} + +/// Serializes a recursive type's child collection one level deeper. +/// +/// `limit` is the greatest number of nested child levels below the root +/// that will be emitted; `type_name` names the offending type in the +/// error. A deeper tree fails with a serializer error — reported by the +/// caller like any other output failure — rather than recursing far enough +/// to overflow the thread stack. +pub(crate) fn serialize_bounded( + children: &[T], + limit: usize, + type_name: &str, + serializer: S, +) -> Result +where + S: Serializer, + T: Serialize, +{ + // A leaf's empty child list recurses no further, so it costs no depth. + // Skipping it keeps `limit` an exact count of *nested* levels and + // spares the counter on the majority of nodes in any real tree. + if children.is_empty() { + return children.serialize(serializer); + } + let depth = DEPTH.with(|d| { + let entered = d.get() + 1; + d.set(entered); + entered + }); + let _guard = DepthGuard; + if depth > limit { + return Err(S::Error::custom(format!( + "{type_name} nesting is deeper than the serialization limit of {limit} levels" + ))); + } + children.serialize(serializer) +} + +/// Implements `Drop` for a tree type so teardown costs no stack depth. +/// +/// `$children` names the field holding the node's children. Every +/// descendant is hoisted into one flat work list, so a node is dropped +/// only after its own children have been moved out of it and its +/// compiler-generated glue finds an empty list — the recursion is one +/// level deep regardless of the tree's shape. +macro_rules! impl_iterative_drop { + ($ty:ty, $children:ident) => { + impl Drop for $ty { + fn drop(&mut self) { + let mut pending = ::std::mem::take(&mut self.$children); + while let Some(mut node) = pending.pop() { + pending.append(&mut node.$children); + } + } + } + }; +} + +pub(crate) use impl_iterative_drop; + +#[cfg(test)] +mod tests { + use super::*; + + /// The guard must restore the counter on the error path, or one + /// rejected tree would poison every later serialization on the thread. + #[test] + fn depth_counter_is_restored_after_a_rejection() { + let deep = vec![vec![vec![0_u8]]]; + // Limit 0 rejects immediately at the first bounded level. + let mut out = Vec::new(); + let mut ser = serde_json::Serializer::new(&mut out); + let err = serialize_bounded(&deep, 0, "Probe", &mut ser).expect_err("limit 0 must reject"); + assert!( + err.to_string().contains("serialization limit of 0 levels"), + "error must name the limit, got: {err}" + ); + assert_eq!(DEPTH.with(Cell::get), 0, "counter must unwind to zero"); + + // The same thread must still serialize a shallow value afterwards. + let mut out = Vec::new(); + let mut ser = serde_json::Serializer::new(&mut out); + serialize_bounded(&deep, 1, "Probe", &mut ser).expect("limit 1 accepts one level"); + assert_eq!(DEPTH.with(Cell::get), 0, "counter must unwind to zero"); + assert_eq!(String::from_utf8(out).expect("utf-8"), "[[[0]]]"); + } +} diff --git a/src/spaces.rs b/src/spaces.rs index 614657ef..1bc5cde9 100644 --- a/src/spaces.rs +++ b/src/spaces.rs @@ -327,6 +327,11 @@ pub struct FuncSpace { pub suppressed: SuppressionScope, } +// Space nesting is caller-controlled, so the compiler-generated `Drop` +// glue would recurse once per level and abort the process on a deep tree +// (#1056). See [`crate::recursion`]. +crate::recursion::impl_iterative_drop!(FuncSpace, spaces); + impl FuncSpace { /// Project this space into its [`crate::wire::FuncSpace`] form — the /// plain, `Deserialize`-capable record that defines the serialized diff --git a/src/tools.rs b/src/tools.rs index a8387cf0..50773d2e 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -772,7 +772,9 @@ pub(crate) fn check_metrics( filename: &str, check: fn(crate::CodeMetrics) -> (), ) { - check_func_space::(source, filename, |func_space| check(func_space.metrics)); + check_func_space::(source, filename, |func_space| { + check(func_space.metrics.clone()); + }); } /// Analyses `source` **byte-for-byte** and returns its metrics. @@ -794,6 +796,7 @@ pub(crate) fn metrics_verbatim( crate::analyze(crate::Source::new(lang, source), options) .expect("verbatim source must analyse") .metrics + .clone() } /// Asserts that `func_space` has a direct child space named `name` and that diff --git a/src/wire.rs b/src/wire.rs index bf7ee9f2..ee625b89 100644 --- a/src/wire.rs +++ b/src/wire.rs @@ -387,6 +387,98 @@ impl CodeMetrics { } } +/// Greatest space-nesting depth [`FuncSpace`] and [`Ops`] will serialize. +/// +/// Both are trees, and `serde` cannot emit a tree without one native stack +/// frame per level, so the depth has to be bounded somewhere: past it, the +/// runtime aborts the process instead of raising a catchable panic +/// (#1056). A space tree deeper than this fails serialization with an +/// ordinary serializer error naming the limit. +/// +/// The value matches the recursion limit `serde_json`'s `Deserializer` +/// already imposes on the same documents, and is generous against both +/// ends of that comparison. On the read side, a `FuncSpace` costs *two* +/// JSON nesting levels (its object plus its `spaces` array), so parsing +/// one of these documents back caps out near 61 levels — the emit limit +/// is the more permissive of the two. On the source side, the deepest +/// space nesting across the 14 450-file corpus under `tests/repositories` +/// is 10 levels. +pub const MAX_SPACE_SERIALIZE_DEPTH: usize = 128; + +/// Maps a recursive compute-side tree onto its wire form using an explicit +/// work stack. +/// +/// The natural `children.iter().map(Self::from).collect()` recursion costs +/// one stack frame per nesting level and overflows a default 2 MiB thread +/// stack at roughly 900 levels of nested functions — a `SIGABRT`, not a +/// catchable panic (#1056). Space nesting is attacker-controlled, so the +/// conversion is iterative: `build` is called on each node exactly once, +/// bottom-up, with that node's already-converted children. +fn map_tree<'a, Src, Dst>( + root: &'a Src, + children_of: fn(&'a Src) -> &'a [Src], + build: fn(&'a Src, Vec) -> Dst, +) -> Dst { + // The root frame is held outside the stack so that popping a completed + // frame always has somewhere to deposit it, and so the loop needs no + // fallible "the stack cannot be empty here" step. + let mut root_frame = MapFrame::new(root, children_of); + let mut descendants = Vec::new(); + loop { + let frame = match descendants.last_mut() { + Some(frame) => frame, + None => &mut root_frame, + }; + let source = frame.source; + if let Some(child) = children_of(source).get(frame.next_child) { + frame.next_child += 1; + descendants.push(MapFrame::new(child, children_of)); + continue; + } + // The current frame has no children left: fold it into its parent, + // or stop once that frame is the root. + let Some(done) = descendants.pop() else { break }; + let converted = build(done.source, done.children); + match descendants.last_mut() { + Some(parent) => parent.children.push(converted), + None => root_frame.children.push(converted), + } + } + build(root_frame.source, root_frame.children) +} + +/// One in-progress node of a [`map_tree`] walk. +struct MapFrame<'a, Src, Dst> { + /// The compute-side node being converted. + source: &'a Src, + /// How many of `source`'s children have been pushed onto the walk. + next_child: usize, + /// Wire forms of the children completed so far, in source order. + children: Vec, +} + +impl<'a, Src, Dst> MapFrame<'a, Src, Dst> { + fn new(source: &'a Src, children_of: fn(&'a Src) -> &'a [Src]) -> Self { + Self { + source, + next_child: 0, + children: Vec::with_capacity(children_of(source).len()), + } + } +} + +/// Serializes a [`FuncSpace`]'s children one nesting level deeper, +/// refusing to descend past [`MAX_SPACE_SERIALIZE_DEPTH`]. +fn serialize_spaces(spaces: &[FuncSpace], serializer: S) -> Result { + crate::recursion::serialize_bounded(spaces, MAX_SPACE_SERIALIZE_DEPTH, "FuncSpace", serializer) +} + +/// Serializes an [`Ops`] node's children one nesting level deeper, +/// refusing to descend past [`MAX_SPACE_SERIALIZE_DEPTH`]. +fn serialize_ops_spaces(spaces: &[Ops], serializer: S) -> Result { + crate::recursion::serialize_bounded(spaces, MAX_SPACE_SERIALIZE_DEPTH, "Ops", serializer) +} + /// Wire form of [`crate::spaces::FuncSpace`] — a recursive metric tree. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FuncSpace { @@ -399,6 +491,7 @@ pub struct FuncSpace { /// The space kind. pub kind: SpaceKind, /// All nested subspaces. + #[serde(serialize_with = "serialize_spaces")] pub spaces: Vec, /// The metrics of the space. pub metrics: CodeMetrics, @@ -408,17 +501,25 @@ pub struct FuncSpace { pub suppressed: SuppressionScope, } +// The wire tree mirrors the compute tree's nesting, so its `Drop` needs +// the same de-recursion (#1056). See [`crate::recursion`]. +crate::recursion::impl_iterative_drop!(FuncSpace, spaces); + impl From<&crate::spaces::FuncSpace> for FuncSpace { fn from(f: &crate::spaces::FuncSpace) -> Self { - Self { - name: f.name.clone(), - start_line: f.start_line, - end_line: f.end_line, - kind: f.kind, - spaces: f.spaces.iter().map(FuncSpace::from).collect(), - metrics: CodeMetrics::from(&f.metrics), - suppressed: f.suppressed.clone(), - } + map_tree( + f, + |source| &source.spaces, + |source, spaces| Self { + name: source.name.clone(), + start_line: source.start_line, + end_line: source.end_line, + kind: source.kind, + spaces, + metrics: CodeMetrics::from(&source.metrics), + suppressed: source.suppressed.clone(), + }, + ) } } @@ -437,6 +538,7 @@ pub struct Ops { /// The space kind. pub kind: SpaceKind, /// All nested subspaces. + #[serde(serialize_with = "serialize_ops_spaces")] pub spaces: Vec, /// The operands in the space. pub operands: Vec, @@ -444,18 +546,26 @@ pub struct Ops { pub operators: Vec, } +// The wire tree mirrors the compute tree's nesting, so its `Drop` needs +// the same de-recursion (#1056). See [`crate::recursion`]. +crate::recursion::impl_iterative_drop!(Ops, spaces); + impl From<&ops::Ops> for Ops { fn from(o: &ops::Ops) -> Self { - Self { - name: o.name.clone(), - name_was_lossy: o.name_was_lossy, - start_line: o.start_line, - end_line: o.end_line, - kind: o.kind, - spaces: o.spaces.iter().map(Ops::from).collect(), - operands: o.operands.clone(), - operators: o.operators.clone(), - } + map_tree( + o, + |source| &source.spaces, + |source, spaces| Self { + name: source.name.clone(), + name_was_lossy: source.name_was_lossy, + start_line: source.start_line, + end_line: source.end_line, + kind: source.kind, + spaces, + operands: source.operands.clone(), + operators: source.operators.clone(), + }, + ) } } @@ -815,4 +925,223 @@ fn run() { assert!(pruned.cyclomatic.is_none()); }); } + + // ----------------------------------------------------------------- + // Stack-depth regression tests (#1056) + // + // The #700 / #709 small-stack tests cover the *dump* walk and build + // `FuncSpace` values by hand, so nothing exercised the wire + // conversion or `Serialize`. These drive `analyze` and then convert / + // serialize, because the hazard scales with `FuncSpace` nesting, not + // AST depth — nested parentheses reach depth 200 000 while opening a + // single space, so testing the wrong shape looks like a pass. + // ----------------------------------------------------------------- + + /// The size of a `bca` consumer thread and of a `tokio` blocking + /// thread — the stack the guarded limits are dimensioned against. + const PRODUCTION_STACK: usize = 2 * 1024 * 1024; + + /// Deliberately far below `PRODUCTION_STACK`: a re-recursed `From` or + /// `Drop` fails loudly here instead of riding on the test runner's + /// generous stack. + const TIGHT_STACK: usize = 512 * 1024; + + /// Rust source nesting `depth` functions, one `FuncSpace` per level + /// below the file-level `Unit`. + fn nested_functions(depth: usize) -> String { + use std::fmt::Write as _; + let mut source = String::with_capacity(depth * 14); + for level in 0..depth { + let _ = writeln!(source, "fn f{level}() {{"); + } + for _ in 0..depth { + source.push_str("}\n"); + } + source + } + + /// Analyses [`nested_functions`], computing only `loc` so the cost of + /// unrelated metrics does not dominate a deep fixture. + fn analyze_nested(depth: usize) -> crate::FuncSpace { + crate::analyze( + crate::Source::new(crate::LANG::Rust, nested_functions(depth).as_bytes()) + .with_name(Some("nested.rs".to_owned())), + crate::MetricsOptions::default().with_only(&[Metric::Loc]), + ) + .expect("nested-function fixture must analyse") + } + + /// Longest chain of nested spaces in `space`, measured without + /// recursing so the measurement cannot overflow before the code + /// under test does. + fn wire_nesting_depth(space: &FuncSpace) -> usize { + let mut deepest = 0; + let mut stack = vec![(space, 1_usize)]; + while let Some((node, depth)) = stack.pop() { + deepest = deepest.max(depth); + for child in &node.spaces { + stack.push((child, depth + 1)); + } + } + deepest + } + + /// Runs `body` on a thread with an explicit stack so the result does + /// not depend on the test harness's own stack size. + fn on_stack(bytes: usize, body: impl FnOnce() -> T + Send + 'static) -> T { + std::thread::Builder::new() + .stack_size(bytes) + .spawn(body) + .expect("spawn bounded-stack thread") + .join() + .expect("bounded-stack thread must not overflow") + } + + /// A chain of `depth` nested spaces below the root, built directly. + /// + /// `analyze` is the more faithful fixture and the deep tests below + /// use it, but only to a depth the remaining quadratic ancestor walks + /// (#1062) make affordable. This builds the same shape for free, so + /// the stack properties can be pinned an order of magnitude deeper + /// than an analysed fixture could reach in a debug build. + fn space_chain(depth: usize) -> crate::FuncSpace { + let leaf = || crate::FuncSpace { + name: Some("f".to_owned()), + start_line: 1, + end_line: 1, + kind: SpaceKind::Function, + spaces: Vec::new(), + metrics: crate::CodeMetrics::default(), + suppressed: SuppressionScope::default(), + }; + let mut root = leaf(); + let mut cursor = &mut root; + for _ in 0..depth { + cursor.spaces.push(leaf()); + cursor = cursor.spaces.last_mut().expect("just pushed"); + } + root + } + + #[test] + fn deeply_nested_spaces_convert_to_wire_without_stack_overflow() { + // `From<&spaces::FuncSpace>` walks an explicit work stack: the + // former `spaces.iter().map(FuncSpace::from).collect()` recursed + // once per level and aborted the process at roughly 900 levels on + // a 2 MiB thread — under 100 on this one. Analysed fixture, so + // the whole `analyze` → `to_wire` pipeline is covered. + const DEPTH: usize = 2_000; + let depth = on_stack(TIGHT_STACK, || { + let space = analyze_nested(DEPTH); + wire_nesting_depth(&space.to_wire()) + }); + // The file-level `Unit` plus one `Function` space per nested `fn`. + assert_eq!(depth, DEPTH + 1, "the whole chain must survive conversion"); + } + + #[test] + fn a_pathologically_deep_space_chain_converts_and_tears_down() { + // Both `Drop` impls at a depth no recursive teardown survives: + // the compiler-generated glue overflowed this thread's stack at + // roughly 4 000 levels, and the compute tree, the wire tree, and + // the wire tree's own nested `Vec`s all unwind inside it. + const DEPTH: usize = 100_000; + let depth = on_stack(TIGHT_STACK, || { + let space = space_chain(DEPTH); + wire_nesting_depth(&space.to_wire()) + }); + assert_eq!(depth, DEPTH + 1, "the whole chain must survive conversion"); + } + + #[test] + fn spaces_deeper_than_the_limit_fail_serialization_rather_than_abort() { + // The reported symptom: `bca metrics -O json` on ~1 000 nested + // functions overflowed the stack, and a stack overflow is a + // `SIGABRT`, not a catchable panic — `bca-web`'s `spawn_blocking` + // wrapper cannot contain it. It must now be an ordinary error. + const DEPTH: usize = 2_000; + let message = on_stack(PRODUCTION_STACK, || { + let space = analyze_nested(DEPTH); + serde_json::to_string(&space) + .expect_err("nesting past the limit must fail, not serialize") + .to_string() + }); + assert!( + message.contains("FuncSpace nesting is deeper than the serialization limit of 128"), + "the error must name the type and the limit, got: {message}" + ); + } + + #[test] + fn space_nesting_at_the_serialize_limit_is_accepted_and_one_deeper_is_not() { + // `depth` counts non-empty child lists, so `n` nested functions + // reach depth `n`: the file-level `Unit` down to the last `fn` + // that still contains another one. + let (accepted, rejected) = on_stack(PRODUCTION_STACK, || { + let at_limit = analyze_nested(MAX_SPACE_SERIALIZE_DEPTH); + let past_limit = analyze_nested(MAX_SPACE_SERIALIZE_DEPTH + 1); + ( + [ + serde_json::to_string(&at_limit).is_ok(), + serde_yaml::to_string(&at_limit).is_ok(), + toml::to_string(&at_limit).is_ok(), + { + let mut bytes = Vec::new(); + ciborium::into_writer(&at_limit, &mut bytes).is_ok() + }, + ], + [ + serde_json::to_string(&past_limit).is_ok(), + serde_yaml::to_string(&past_limit).is_ok(), + toml::to_string(&past_limit).is_ok(), + { + let mut bytes = Vec::new(); + ciborium::into_writer(&past_limit, &mut bytes).is_ok() + }, + ], + ) + }); + assert_eq!( + accepted, [true; 4], + "exactly {MAX_SPACE_SERIALIZE_DEPTH} levels must serialize in every format" + ); + assert_eq!( + rejected, [false; 4], + "one level past the limit must be refused in every format" + ); + } + + #[test] + fn deeply_nested_ops_convert_and_serialize_without_stack_overflow() { + // `Ops` mirrors `FuncSpace`'s nesting and had the same recursive + // `From` and `Serialize`, so it needs the same coverage. + const DEPTH: usize = 2_000; + let (converted_depth, message) = on_stack(PRODUCTION_STACK, || { + let ops = crate::Ast::parse(crate::Source::new( + crate::LANG::Rust, + nested_functions(DEPTH).as_bytes(), + )) + .expect("nested-function fixture must parse") + .ops() + .expect("nested-function fixture must yield ops"); + let wire = Ops::from(&ops); + let mut deepest = 0; + let mut stack = vec![(&wire, 1_usize)]; + while let Some((node, depth)) = stack.pop() { + deepest = deepest.max(depth); + for child in &node.spaces { + stack.push((child, depth + 1)); + } + } + let message = serde_json::to_string(&ops) + .expect_err("nesting past the limit must fail, not serialize") + .to_string(); + (deepest, message) + }); + assert_eq!(converted_depth, DEPTH + 1, "the whole chain must convert"); + assert!( + message.contains("Ops nesting is deeper than the serialization limit of 128"), + "the error must name the type and the limit, got: {message}" + ); + } } diff --git a/tests/derive_eq_hash_ord.rs b/tests/derive_eq_hash_ord.rs index 31a60d4d..cb725843 100644 --- a/tests/derive_eq_hash_ord.rs +++ b/tests/derive_eq_hash_ord.rs @@ -38,9 +38,9 @@ const SRC_B: &str = r"fn noop() {} #[test] fn cognitive_stats_partial_eq_same_and_different_source() { - let a1 = analyze_rust(SRC_A).metrics.cognitive; - let a2 = analyze_rust(SRC_A).metrics.cognitive; - let b = analyze_rust(SRC_B).metrics.cognitive; + let a1 = analyze_rust(SRC_A).metrics.cognitive.clone(); + let a2 = analyze_rust(SRC_A).metrics.cognitive.clone(); + let b = analyze_rust(SRC_B).metrics.cognitive.clone(); assert_eq!(a1, a2, "same source must yield equal cognitive Stats"); assert_ne!(a1, b, "different source must yield unequal cognitive Stats"); @@ -48,9 +48,9 @@ fn cognitive_stats_partial_eq_same_and_different_source() { #[test] fn halstead_stats_partial_eq_same_and_different_source() { - let a1 = analyze_rust(SRC_A).metrics.halstead; - let a2 = analyze_rust(SRC_A).metrics.halstead; - let b = analyze_rust(SRC_B).metrics.halstead; + let a1 = analyze_rust(SRC_A).metrics.halstead.clone(); + let a2 = analyze_rust(SRC_A).metrics.halstead.clone(); + let b = analyze_rust(SRC_B).metrics.halstead.clone(); assert_eq!(a1, a2, "same source must yield equal halstead Stats"); assert_ne!(a1, b, "different source must yield unequal halstead Stats"); @@ -58,9 +58,9 @@ fn halstead_stats_partial_eq_same_and_different_source() { #[test] fn loc_stats_partial_eq_same_and_different_source() { - let a1 = analyze_rust(SRC_A).metrics.loc; - let a2 = analyze_rust(SRC_A).metrics.loc; - let b = analyze_rust(SRC_B).metrics.loc; + let a1 = analyze_rust(SRC_A).metrics.loc.clone(); + let a2 = analyze_rust(SRC_A).metrics.loc.clone(); + let b = analyze_rust(SRC_B).metrics.loc.clone(); assert_eq!(a1, a2, "same source must yield equal loc Stats"); assert_ne!(a1, b, "different source must yield unequal loc Stats"); diff --git a/tests/snapshots/sarif_test__sarif_multi_offender.snap b/tests/snapshots/sarif_test__sarif_multi_offender.snap index a01319b5..8ca46865 100644 --- a/tests/snapshots/sarif_test__sarif_multi_offender.snap +++ b/tests/snapshots/sarif_test__sarif_multi_offender.snap @@ -10,7 +10,7 @@ expression: out "tool": { "driver": { "name": "big-code-analysis", - "version": "2.0.1", + "version": "2.1.0", "rules": [ { "id": "cyclomatic", diff --git a/tests/snapshots/sarif_test__sarif_zero_offenders.snap b/tests/snapshots/sarif_test__sarif_zero_offenders.snap index 35ef5b22..b34f02e1 100644 --- a/tests/snapshots/sarif_test__sarif_zero_offenders.snap +++ b/tests/snapshots/sarif_test__sarif_zero_offenders.snap @@ -10,7 +10,7 @@ expression: out "tool": { "driver": { "name": "big-code-analysis", - "version": "2.0.1", + "version": "2.1.0", "rules": [] } }, diff --git a/tree-sitter-ccomment/Cargo.toml b/tree-sitter-ccomment/Cargo.toml index 1f4019fc..c7b1ba01 100644 --- a/tree-sitter-ccomment/Cargo.toml +++ b/tree-sitter-ccomment/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bca-tree-sitter-ccomment" description = "Ccomment grammar for the tree-sitter parsing library (big-code-analysis fork)" -version = "2.0.1" +version = "2.1.0" authors = [ "Calixte Denizet ", "Elijah Zupancic ", diff --git a/tree-sitter-mozcpp/Cargo.toml b/tree-sitter-mozcpp/Cargo.toml index b418e8c4..e586da6a 100644 --- a/tree-sitter-mozcpp/Cargo.toml +++ b/tree-sitter-mozcpp/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bca-tree-sitter-mozcpp" description = "Mozcpp grammar for the tree-sitter parsing library (big-code-analysis fork)" -version = "2.0.1" +version = "2.1.0" authors = [ "Calixte Denizet ", "Elijah Zupancic ", diff --git a/tree-sitter-mozjs/Cargo.toml b/tree-sitter-mozjs/Cargo.toml index 4898449f..35ed98ec 100644 --- a/tree-sitter-mozjs/Cargo.toml +++ b/tree-sitter-mozjs/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bca-tree-sitter-mozjs" description = "Mozjs grammar for the tree-sitter parsing library (big-code-analysis fork)" -version = "2.0.1" +version = "2.1.0" authors = [ "Calixte Denizet ", "Elijah Zupancic ", diff --git a/tree-sitter-preproc/Cargo.toml b/tree-sitter-preproc/Cargo.toml index 286cf9e2..33321ce5 100644 --- a/tree-sitter-preproc/Cargo.toml +++ b/tree-sitter-preproc/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bca-tree-sitter-preproc" description = "Preproc grammar for the tree-sitter parsing library (big-code-analysis fork)" -version = "2.0.1" +version = "2.1.0" authors = [ "Calixte Denizet ", "Elijah Zupancic ", diff --git a/tree-sitter-tcl/Cargo.toml b/tree-sitter-tcl/Cargo.toml index 42908d3b..1f8e0dde 100644 --- a/tree-sitter-tcl/Cargo.toml +++ b/tree-sitter-tcl/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bca-tree-sitter-tcl" description = "Tcl grammar for the tree-sitter parsing library (big-code-analysis fork)" -version = "2.0.1" +version = "2.1.0" authors = [ "Lewis Russell", "Elijah Zupancic ",