diff --git a/.claude/rules/shell.md b/.claude/rules/shell.md index 7f30b208a..9a3617de6 100644 --- a/.claude/rules/shell.md +++ b/.claude/rules/shell.md @@ -94,3 +94,129 @@ a plausible number rather than an error: - **`bca check` writes offenders to stderr.** `2>/dev/null` on a check invocation discards the entire result and leaves an empty stdout that reads as "no offenders". + +## A `pgrep -f` wait loop matches itself and never exits + +`pgrep -f` matches against the **full command line**. When a wait loop +is passed as text — `zsh -c "until ! pgrep -f 'make pre-commit' …"`, +which is exactly how the `Bash` tool runs every command — that text +*is* the shell's argv. The loop therefore finds itself, the condition +never goes false, and it runs until the machine reboots: + +```zsh +# Never exits: this shell's own argv contains "make pre-commit". +zsh -c "until ! pgrep -f 'make pre-commit' >/dev/null; do sleep 30; done" +``` + +**The `-c` part is the precondition, not incidental.** The identical +loop inside a script file has argv `zsh /path/to/waiter.sh`, does not +match itself, and exits normally — verified by probe. So the bug is +specific to the way commands are issued here, and a reader who fails to +reproduce it from a `.sh` file has not disproved it. + +The failure is silent in the way the rest of this file describes: no +error, no output, nothing in the log. It reads as "the job is still +running", which is indistinguishable from the truth right up until you +notice the job finished half a day ago. + +Ten of these accumulated in one session here — seven waiting on a +`make pre-commit` that had long since written its `BCA_GATE: pass`, +three on a finished `collect.sh`. Each spun a `sleep` every 15-30s, the +oldest for twelve hours. They also **match each other**, so killing one +at a time does not help: six siblings keep the seventh's condition +true. `pgrep -af ` is the diagnosis — if every PID it prints +is a waiter, that is the whole bug. + +### How to apply + +- **Prefer a condition that is not a process at all.** The artifact the + job produces cannot match the watcher — and bound the wait, because + an unbounded loop on a job that dies is the same hang by another + route: + + ```zsh + log=$(mktemp /tmp/bca-pre-commit.XXXXXX.log) + make pre-commit >"$log" 2>&1 & + + for _ in {1..90}; do # ceiling: 90 x 20s = 30 min + grep -qs '^BCA_GATE:' "$log" && break + sleep 20 + done + + grep -s '^BCA_GATE:' "$log" || + { echo "no BCA_GATE line after 30 min — crashed, killed, or still running" >&2; exit 1; } + ``` + + Three details earn their place. The `for` ceiling is what makes a + dead job a bounded failure instead of a hang. `-s` on both greps + suppresses `No such file or directory` before the log exists — + without it the loop emits one stderr line per tick, forever. And the + trailing `grep ||` makes the no-verdict case exit non-zero, so + "silence" cannot read as success; that is the third state + [`AGENTS.md`](../../AGENTS.md) names under "Reading the verdict". + + Exit 0 here means *a verdict appeared*, not that it said `pass` — + read the line, as that section requires. + + Verified against four cases: verdict already present (breaks early), + verdict arriving mid-wait (picked up), log present with no verdict, + and log never created (both exit 1, no stderr noise). + +- **When it really must be a process, break the self-match** with a + bracket class, the standard `ps | grep` trick — `[m]ake` matches the + string `make` but the pattern itself does not contain it: + + ```zsh + until ! pgrep -f '[m]ake pre-commit' >/dev/null; do sleep 30; done + ``` + + Verified both halves by probe: the bracket watcher does not appear in + its own `pgrep` output (so the loop exits), and the pattern still + matches a real process whose argv contains `make pre-commit`. The + naive form in the same probe matched itself and hung. + + It protects the watcher from *itself* only. Any other process quoting + the plain string still matches — a sibling watcher written the naive + way, a `ps | grep` someone left running, an editor holding the + command in a buffer. That is why the file-artifact form above is the + first recommendation and this one the fallback. + +- **Do not reach for `$$` to exclude yourself. It does not work.** + The obvious repair — + + ```zsh + # BROKEN. Hangs exactly like the naive form. + until ! (pgrep -f 'make pre-commit' | grep -qv "^$$\$"); do sleep 30; done + ``` + + — fails because the watcher is not the only process carrying that + argv. The command substitution and the `(…)` subshell both fork from + it and inherit it, so `pgrep` returns several PIDs where `$$` is only + one: + + ```text + watcher $$ = 1347315 ; pgrep sees: 1347301 1347315 1347316 + ``` + + Filtering one PID can never empty that list, the pipeline stays true, + and the loop never ends — measured, in both quoting styles. (Under + `zsh -c "…"` there is a second, independent defect: a double-quoted + `$$` is expanded by the *parent* before the child ever sees it, so + the watcher excludes someone else's PID.) The bracket class above + avoids the whole class, because it forks no subshell and `pgrep`'s + own argv carries `[m]ake`, not `make`. + +- **Do not poll for harness-tracked work at all.** `Bash` with + `run_in_background` re-invokes on exit, and `Monitor` streams events. + A hand-rolled waiter is only for something neither can see. + +- **Check for leaks before ending a long session**: `pgrep -af 'do + sleep'`. A waiter costs almost nothing, but it outlives the session + and the next one inherits a process list nobody can account for. + +Every snippet in this section was run before it was written down. The +first draft was not: the bracket form was probed and the other two were +reasoned about, and both of the reasoned ones were wrong — one hung, +one claimed a timeout it did not have. In a file about shell that +returns a plausible answer instead of an error, an unrun example is the +defect it documents. diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index a0d1c9a6e..8b3623b13 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -461,6 +461,189 @@ it, and then the counts varied (0 / 2 / 3). A feature-gate check run without `-p` proves nothing, and proves it while looking green; confirm real isolation by watching the *filtered-out* count differ across sets. +## Every test that names a language carries that language's gate + +The rule above is about a *fixture table*. The same requirement applies +one level down, to every individual test, and for a blunter reason: +`mk_langs!` generates each `*Parser` alias, `*Code` tag and `LANG` +variant unconditionally, so `check_metrics::(…)` compiles +with `python` off and then panics inside `Tree::new`. Until #1472, +about 2,950 tests named a grammar without gating on it — 2,804 in +`src/`, 104 in `big-code-analysis-ast/src/`, 33 under `tests/`, plus 229 +helpers and fixture tables. Approximate because the figure moves with +the derivation rules themselves: each refinement that stops reading +something as a use lowers it, and this section has already quoted a +higher number taken before the comparison rules landed. A +partial-feature build failed in the thousands. + +`make check-test-lang-gates` derives and enforces it across all three of +those roots: for every item in a test scope it collects the languages +the body reaches — directly, and through same-module helpers that +hardcode a parser — and fails when the item's `cfg` would still admit it +into a build lacking one. A test needs +`all(…)` of what it names; a helper, a `const` fixture table or an +import needs `any(…)` of its users. `--fix` writes the markers and +`--show` prints the derivation. + +Four things the derivation cannot see, so write those by hand and say +why in a comment: + +- **A language chosen from a string.** A helper that maps a path + extension or a filename glob onto a `LANG` hides it — the corpus tests + and `suppression_test.rs`'s `analyze_lang` both did. Prefer passing + the `LANG` explicitly where you can; that is a better call site + anyway. +- **A trait used through its methods.** Nothing names `ParserTrait` in a + body that calls `SomeParser::new`, so its import falls back to the + module's whole union, so it carries no derived gate at all. +- **Anything `pub`.** Its users are in other files, which this + single-file scanner does not see; `src/test_support.rs` is the whole + story. +- **Which grammar a re-export serves.** Same reason, one step further + out — a scoped `#[allow(unused_imports)]` beats a hand-copied union of + seven files' gates. + +Say so with a marker the gate reads, not prose alone: + +```rust +// test-lang-gates: hand-written(cpp) — the corpus walk picks a +// language per file from its extension, so the glob list decides +// it and nothing in the body names it +#[cfg(feature = "cpp")] +``` + +Because the gate checks the *other* direction too (#1478). A gate wider +than the item needs keeps it out of builds it could have run in, and +that is the failure nothing else can see: too wide panics on the leg +that lacks the grammar, too narrow just drops the test and the leg still +looks green. Sixteen gates in this tree are wider than their bodies +justify, every one for a reason above; the marker is how you say which, +and a gate that grows a feature nobody can account for fails the gate. + +A marker that stops being load-bearing fails too. Narrow the gate and +the feature it named is no longer over-declared, so the marker now +claims a reason that does not apply — and sixteen accepted gates only +read as a census while every one of them is still doing something. The +gate names the stale entry; drop the feature, or the whole marker when +it names nothing else. + +Two things follow for anyone editing a marker by hand: + +- **A comparison is not a use.** `lang == LANG::Go` asks which variant a + value is; the enum is generated unconditionally, so it parses nothing + and needs no grammar. Counting one as a requirement is what conjoined + `feature = "go"` onto `container_scope_tests.rs` and dropped the + positive half of the #1197 contract from every build without Go. This + covers *every* alternative of a `matches!`, not only the leading one: + the call has no arms and drives no table, so `matches!(lang, + LANG::Ccomment | LANG::Preproc)` written to **skip** two languages is + not a requirement for either. Reading the later arms as uses is what + gated two `every_*_in_every_language` parity sweeps down to the one + `c-family-helpers` that leaked out of that exclusion — they ran in + four builds instead of twenty-three, and `--compare` was the only + check that could see it. +- **A sweep still needs the parsers it hardcodes.** `is_enabled` + filtering earns a row set only `any(…)`, because the loop skips what + is missing. A parser named through a *type parameter* + (`check_metrics::`) cannot be skipped by any runtime + filter, so it is required even inside a sweep — otherwise the + exemption is a way round the whole gate. It propagates through + helpers exactly as `needs` does: a sweep whose only fixed-parser call + sits one hop away pins that parser just the same. + + **The pin covers a parser named as a *type*, and nothing else.** A + `LANG::Rust` literal handed to `analyze` inside a sweep is not pinned, + because the gate cannot tell one that sits in the iterated row table — + which the `is_enabled` filter does skip — from one outside the loop, + which it does not. Every sweep in the tree today filters per language, + so nothing is currently wrong; but a sweep that mentions `is_enabled` + anywhere and *also* parses a hard-coded `LANG` outside the loop would + pass this gate and panic (#1480). Write the fixed-parser call as + `check_metrics::` and it is pinned correctly. +- **A sweep over the whole enum needs `any()`**, when it + carries both halves of the rule above — an `is_enabled()` row filter + *and* a non-vacuity assertion. Those two together mean it *fails* + rather than skips with no language enabled, so it has to be absent + then. Do not try to read a narrower set off the body: the fixtures + come from a `LANG`-parameterised helper whose arms are deliberately + not attributed to callers, so the body names almost nothing and + whatever leaks through becomes the whole gate. A full-enum sweep + *without* that guard is a different animal — `Display`, `FromStr` and + slug round-trips walk the same enum over variants that exist without + their grammars, and gating those stops them running on the + `--no-default-features` leg that is exactly where they belong. + +### Why the import lint is off on a partial build + +Both library roots and the five integration-test crate roots carry: + +```rust +#![cfg_attr(not(feature = "all-languages"), allow(unused_imports))] +``` + +Per-language gating makes "is this import live" a function of the +enabled feature set. A `use` that every test in a module needs under +`--all-features` is used by none of them once their grammars are gated +out, and `unused_imports` cannot express that. Nor can a `cfg` on the +import itself: a trait is reached through its methods and so is named +nowhere, a glob binds an unknowable set, and a macro is invoked from +item position. A derived union is wrong in both directions — too wide +and the import is unused, too narrow and it vanishes from under a test +that still compiles. + +The scoping is what keeps it honest. `all-languages` is on by default +and under `--all-features`, so the build CI gates on and the one a +contributor runs both report an unused import exactly as before; only +the single-language legs, where the answer cannot mean anything, go +quiet. + +Dead *items* are relaxed only in the two library roots, which already +carried an `allow(dead_code)` on the same condition before #1472. The +test crates do not, so every helper, `const`, macro and test in +`tests/` still needs its own gate. + +### The direction that needs history + +Both checks above compare a marker against the derivation. When the two +agree and are *both* wrong there is nothing left to compare against — +the marker faithfully mirrors a derivation that is itself too wide, and +no amount of re-reading either one says so. + +`--compare ` is the answer, and it needs no cargo: it scans the +tree at `ref` as well (`git archive` into a scratch directory), computes +which single-language builds compile each test in each, and fails on a +test that still exists but stopped being built somewhere. The probe set +comes from the language table, so a new language extends it for free. + +CI runs it per pull request against the base branch head — the PR's +`base.sha`, not `git merge-base`, because the job checks out shallow and +cannot compute one. The difference shows on a PR that is behind: if +`main` widens a gate meanwhile, this reports it against a PR that never +touched it, and the label is then the right answer. It is deliberately +not in `make pre-commit`, which has no base revision to be meaningful +against; by hand it is +`make check-test-lang-gates-compare COMPARE_REF=origin/main`. + +A test that was *deleted* or renamed reads as one name gone and another +arrived, and is not reported — a deliberate removal is not a gate +narrowing under it. + +A *deliberate* narrowing looks identical, though, and does happen: the +gate really was too wide and the test does not need that grammar. Say so +with the `gate-narrowing-intended` label on the pull request, not with +an in-source marker. A marker would be permanently stale the moment the +branch lands — the comparison is relative to a moving base — and would +then sit in the tree as a hole nothing can detect, which is the failure +mode this whole gate exists to prevent. + +The step reads that label **live**, not from the event payload, so +applying it after the failing run — which is when you find out you need +it — takes effect on the next run, and re-running the failed job works +too. Reading it from `github.event.pull_request.labels` did not: that +payload is a snapshot from when the run was queued, and a re-run replays +the same stale copy, so the documented remedy could never take effect at +all (measured on #1479). + ## Assert a whole-run invariant in the run, not in a fixture list When a change establishes an invariant that holds at the end of *every* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02b75fbf4..28a9098e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -402,7 +402,9 @@ jobs: feature-matrix: name: features (${{ matrix.name }}) runs-on: ubuntu-latest - timeout-minutes: 30 + # Raised from 30 with #1472: each leg now links and runs a test + # binary as well as a clippy check build. + timeout-minutes: 45 strategy: fail-fast: false matrix: @@ -498,15 +500,9 @@ jobs: # being narrowed later. All ten pre-existing legs were verified # against the stricter command before it landed. # - # These legs verify that the workspace *compiles* across feature - # combinations. They deliberately do not run the suite — that runs - # under `default` and `--all-features` in the `test` job — because - # running it here would fail in the thousands rather than report - # anything. Per-language tests are not feature-gated (30 of 3,482 - # `#[test]` fns in `src/`), and `Node` panics by contract on a - # grammar that is not compiled in, so `--no-default-features - # --features rust` fails 2,642 of 3,238 tests. Gating them is - # tracked in #1472. + # These legs verify that the workspace compiles across feature + # combinations. Since #1472 they also *run* the suite, in the step + # below. - run: cargo clippy --all-targets ${{ matrix.flags }} --locked -- -D warnings - uses: taiki-e/install-action@fa23953489c080190314742a9b907f8e97c6767c # v2.87.10 with: @@ -525,6 +521,28 @@ jobs: # list`, which would otherwise be the one cargo invocation in this # job allowed to rewrite `Cargo.lock` instead of failing on it. - run: ./utils/check-feature-gates.py ${{ matrix.flags }} --locked + # Until #1472 these legs compiled the suite and never ran it, + # because running it failed in the thousands: a test naming + # `PythonParser` builds without the `python` grammar and then + # panics in `ParserTrait::new`. Every such test now carries its + # language's feature, so the leg that drops the grammar drops the + # test with it and what remains actually passes. This is the half + # `cargo clippy` cannot see — a gate too *narrow* compiles + # perfectly and silently stops running. + # + # The corpus-dependent binaries are excluded rather than gated + # away: their fixtures live in the `tests/repositories/` + # submodules, which this job checks out with `submodules: false`. + # The `test` job runs them with `submodules: recursive`. + # + # Three binaries, not one. The lib's `corpus` target is the + # obvious one; the CLI's `cli_ux` and `output` targets are the + # non-obvious ones, because nineteen of their tests resolve + # `DeepSpeech/stats.py` through `common::corpus_fixture_path()`, + # which *panics* with the #1171 "integration corpus not checked + # out" diagnostic rather than skipping. Exact matchers (`=name`) + # so `output` cannot also swallow the lib's `output_formats`. + - run: cargo nextest run ${{ matrix.flags }} --locked -E 'not binary(=corpus) and not binary(=cli_ux) and not binary(=output)' deny: name: cargo-deny @@ -582,6 +600,11 @@ jobs: name: lint (make lint) runs-on: ubuntu-latest timeout-minutes: 20 + # `pull-requests: read` is for the `--compare` step below, which + # reads this PR's labels live rather than from the event payload. + permissions: + contents: read + pull-requests: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -756,6 +779,50 @@ jobs: # gate without surfacing the regression in CI. - name: check-excluded-manifests self-tests (explicit) run: python3 -m unittest -q utils/check-excluded-manifests-test.py + # The one direction `make lint`'s gates cannot reach (#1478). Both + # of them compare a `cfg` marker against the derivation; when the + # two agree and are both wrong, only the previous revision says + # so. This scans the base branch head too and fails on a test that + # still exists but stopped being compiled by some single-language + # build — which nothing else notices, because a gate too *narrow* + # just drops the test and every leg stays green. + # + # Pull requests only: it needs a base revision, and a push to + # `main` has no meaningful one. `fetch-depth: 1` above leaves the + # base commit unfetched, so fetch exactly that one object — + # `git archive` needs the tree, not the history. + # + # Narrowing a gate on purpose — the gate *was* too wide and the + # test really does not need that grammar — drops builds too, and + # looks identical from here. The escape is the + # `gate-narrowing-intended` label rather than an in-source marker: + # this comparison is relative to the base branch head, so a marker + # would be permanently stale the moment the PR lands and would sit in + # the tree as a hole nothing can detect. A label leaves the + # decision on the PR, where a reviewer sees it. + # + # The label is read **live**, not from `github.event.pull_request`. + # That payload is a snapshot taken when the run was queued, so a + # label applied in response to this very failure is invisible to + # it — and re-running the job replays the same stale payload, so + # the documented remedy could never take effect. Measured on + # PR #1479, where exactly that happened. + - name: check-test-lang-gates --compare (pull requests) + if: github.event_name == 'pull_request' + run: | + set -euo pipefail + if gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/labels" \ + --jq '.[].name' | grep -qx 'gate-narrowing-intended'; then + echo "gate-narrowing-intended is set: skipping the comparison." \ + "Narrowing on this PR is declared deliberate." + exit 0 + fi + git fetch --depth=1 origin "${BASE_SHA}" + python3 utils/check-test-lang-gates.py --compare "${BASE_SHA}" + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ github.token }} # Defensive twin for the ruff-lockstep gate (#1230): the # ruff-pre-commit `rev:`, `uv.lock`, and the hash-pinned # `requirements/dev.txt` export CI installs from must name one diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df43e211e..989acff33 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -429,6 +429,33 @@ repos: entry: python3 -m unittest -q utils/check-feature-gates-test.py pass_filenames: false + # Per-language test gating (#1472). The companion to the gate + # above and the opposite question: that one asks whether a + # declared union still excludes what it claims to, this one + # derives the marker an item needs from the languages it names and + # reports the ones missing it. Unlike its sibling this is a pure + # source scan with no cargo call, so it runs here in full rather + # than only in CI. + - id: check-test-lang-gates + name: check-test-lang-gates + language: system + # `Cargo.toml` is in the list because `IMPLIES_C_FAMILY_HELPERS` + # is a hand-copy of the sub-crate's feature table — a new + # `c-family-helpers` enabler has to retrigger this. + files: '^(src/.*\.rs|big-code-analysis-ast/src/.*\.rs|tests/(?!repositories/).*\.rs|(big-code-analysis-ast/)?Cargo\.toml|utils/check-test-lang-gates\.py)$' + entry: python3 utils/check-test-lang-gates.py + pass_filenames: false + + # Self-tests for the test-language gate. Same hazard as every + # other scanner here: one that stops matching reports a clean + # tree. + - id: check-test-lang-gates-test + name: check-test-lang-gates-test + language: system + files: '^utils/check-test-lang-gates(-test)?\.py$' + entry: python3 -m unittest -q utils/check-test-lang-gates-test.py + pass_filenames: false + # Safety-doc pin gate — the module doc of # big-code-analysis-py/src/node.rs is the canonical soundness # argument for this workspace's only sanctioned `unsafe` block, diff --git a/AGENTS.md b/AGENTS.md index 686f2c1c3..d015c2a92 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,7 +82,7 @@ and `cargo run -p big-code-analysis-web --`. `check-grammar-marker-sync.py`, `check-enums-codegen-drift.sh`, `check-grammar-crate.py`, `check-grammars-crates.sh`, `check-excluded-manifests.py`, `check-ruff-lockstep.py`, - `check-publish-metadata.py`, + `check-publish-metadata.py`, `check-test-lang-gates.py`, `verify-name-only-churn.py`, and each gate's `*-test.py` self-tests. Each resolves the repository root from its own location @@ -295,7 +295,13 @@ modified, deleted, **and** newly added pages, the last of which `git diff` alone cannot see, #1249), the diagnostic-prefix gate (`make check-diagnostic-prefix`, which blocks a capitalised `Warning:` / `Error:` / `Note:` string literal — see "Rust -conventions"), the safety-doc pin gate +conventions"), the per-language test gate +(`make check-test-lang-gates`, which fails on a test that would be +compiled into a build lacking a grammar it names — #1472 — and, in the +other direction, on a gate *wider* than the item it guards, which keeps +a test out of builds it could have run in and which nothing else can +see — #1478), +the safety-doc pin gate (`make check-safety-doc-pin`, which fails when the `tree-sitter` version cited by the `unsafe` soundness argument in `big-code-analysis-py/src/node.rs` is not the version @@ -472,6 +478,19 @@ If `pre-commit` is installed, also run `pre-commit run --all-files`. The project's `.pre-commit-config.yaml` runs clippy, `cargo +nightly udeps`, and the test suite. +**The per-language membership comparison** (`make +check-test-lang-gates-compare COMPARE_REF=origin/main`) is the one gate above +that is not in `make pre-commit`, because it needs a base revision to +mean anything. The gates that are in it compare a `cfg` marker against the +derivation; when the two agree and are both wrong, only the previous +revision says so. This scans the tree at `REF` as well and fails on a +test that still exists but stopped being compiled by some +single-language build — the silent direction, since a gate too narrow +just drops the test and every leg stays green. It runs no cargo, so it +costs a couple of seconds; the `lint` CI job runs it per pull request +against the base branch head — `base.sha` rather than `git merge-base`, +since that job checks out shallow (#1478). + **The ancestor-chain audit** (`make chain-audit`) re-runs the library tests with `--cfg chain_audit`, which restores the exact `chain.last() == node.parent()` assertion in `Ancestors::checked`. The diff --git a/CHANGELOG.md b/CHANGELOG.md index cc20a4b2f..6dd0e4611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,55 @@ for historical reference. ### Changed +- Every test that names a per-language grammar now carries that + language's Cargo feature as a `cfg` marker, so a partial-feature build + drops it rather than compiling it and panicking (#1472, #1413). The + `*Parser` aliases, `*Code` tags and `LANG` variants `mk_langs!` + generates are unconditional — only the grammar lookup behind them is + gated — so `check_metrics::(…)` built cleanly without + `python` and then failed inside `ParserTrait::new`, whose own doc says + to check `LANG::is_enabled` first. Almost none did: measured across + `src/`, `big-code-analysis-ast/src/` and `tests/`, 3,025 tests named a + grammar without gating on it, 2,840 of them in `src/` alone. A + partial-feature build was therefore useless for verifying anything — + the failures buried any real signal. The markers are + derived rather than hand-written, by the new + `utils/check-test-lang-gates.py`, which also keeps them accurate: it + reads the language table out of the `mk_langs!` invocation, collects + the languages each test item reaches, and fails when an item's `cfg` + would still admit it into a build lacking one. It is wired into + `make pre-commit` and `make ci`, and `--fix` writes the markers. The + `feature-matrix` CI legs now run the suite instead of only compiling + it — every target but the corpus-dependent ones, whose fixtures live in + the `tests/repositories/` submodules that job does not check out. No + test changed its name, its assertions, or whether it runs under + `--all-features`. + + The gate checks both directions (#1478). A marker *narrower* than the + languages an item names lets it compile into a build that panics; one + *wider* keeps it out of builds it could have run in, which nothing else + can see — too wide panics on the leg that lacks the grammar, too narrow + just drops the test and the leg still looks green. Sixteen gates in + the tree are deliberately wider than their bodies justify (a corpus + walk picking a language per file, a non-vacuity anchor, a `mod` + declaration); each says so with a + `// test-lang-gates: hand-written(…) — why` marker, and a gate that + grows a feature nobody can account for now fails — as does a marker + that stops being load-bearing. Two derivation rules came with it: a + `LANG` being *compared* is an identity test rather than a parse, and a + sweep still requires the parsers it names through a type parameter, + which no runtime `is_enabled` filter can skip, including through a + helper. + + Both of those compare a marker against the derivation, so neither can + see a derivation that is itself wrong. `--compare ` closes that: + it scans the tree at `ref` too, computes which single-language builds + compile each test in each, and fails on a test that still exists but + stopped being built somewhere. No cargo and no second build — the same + source scan, run twice. CI runs it per pull request against the merge + base; by hand it is + `make check-test-lang-gates-compare COMPARE_REF=origin/main`. + - `metrics::halstead::HalsteadType` is renamed **`TokenRole`** and moves to `big_code_analysis_ast::token_role`. It answers whether a node acts as an operator or an operand, which the grammar decides and any diff --git a/Makefile b/Makefile index 5c4557f18..5308d75c9 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ find-by-ext = $(if $(FD),$(FD) --extension $(1) $(FD_EXCLUDE) $(2),find . -name NEXTEST := $(shell command -v cargo-nextest 2>/dev/null) TEST_CMD = $(if $(NEXTEST),$(NEXTEST) nextest run --workspace --all-features,cargo test --workspace --all-features --lib --bins --tests) -.PHONY: help check-tools worktree-setup worktree-setup-test build build-release check test test-doc chain-audit fmt fmt-check markdown-fmt markdown-lint shellcheck sh-fmt sh-fmt-check toml-fmt toml-fmt-check toml-lint makefile-check actionlint snapshot-anchors snapshot-anchors-test rustfmt-bail rustfmt-bail-test grammar-marker-sync grammar-marker-sync-test check-versions check-excluded-manifests check-excluded-manifests-test check-ruff-lockstep check-ruff-lockstep-test check-publish-metadata check-publish-metadata-test check-manpage-assets check-manpage-drift-test check-diagnostic-prefix check-diagnostic-prefix-test check-feature-gates check-feature-gates-test check-safety-doc-pin check-safety-doc-pin-test gate-status-test check-tools-test enums-check enums-codegen-drift enums-codegen-drift-test self-scan self-scan-headroom self-scan-write-baseline self-scan-write-baseline-headroom vcs lint clippy udeps insta-review insta-accept clean distclean install install-cli install-web doc doc-open doc-check doc-check-docsrs book book-serve book-pot book-po-update book-ja book-deploy all pre-commit ci release-check verify-changelog pkg-deb-local pkg-rpm-local dev-env-build dev-env-run dev-env-shell dev-env-rm py-bootstrap py-sync py-relock py-clean py-fmt py-fmt-check py-lint py-typecheck py-test py-stubtest smoke smoke-cli smoke-lib bench bench-scaling bench-walk fuzz-check fuzz-smoke fuzz-replay fuzz-run fuzz-tmin _check-find _pc-all _pc-fmt _pc-clippy _pc-test _pc-doc-check _pc-udeps _pc-shellcheck _pc-markdown-lint _pc-toml-lint _pc-makefile-check _pc-actionlint _pc-snapshot-anchors _pc-snapshot-anchors-test _pc-rustfmt-bail _pc-rustfmt-bail-test _pc-grammar-marker-sync _pc-grammar-marker-sync-test _pc-check-versions _pc-check-versions-test _pc-check-grammar-crate-test _pc-check-excluded-manifests _pc-check-excluded-manifests-test _pc-check-ruff-lockstep _pc-check-ruff-lockstep-test _pc-check-publish-metadata _pc-check-publish-metadata-test _pc-check-manpage-assets _pc-check-manpage-drift-test _pc-check-diagnostic-prefix _pc-check-diagnostic-prefix-test _pc-check-feature-gates-test _pc-check-safety-doc-pin _pc-check-safety-doc-pin-test _pc-worktree-setup-test _pc-gate-status-test _pc-check-tools-test _pc-enums-check _pc-enums-codegen-drift _pc-enums-codegen-drift-test _pc-self-scan _pc-self-scan-headroom _pc-py-fmt _pc-py-typecheck _pc-py-test _pc-py-stubtest _ci-all _ci-fmt-check _ci-clippy _ci-test _ci-doc-check _ci-build _ci-udeps _ci-shellcheck _ci-markdown-lint _ci-toml-lint _ci-makefile-check _ci-actionlint _ci-snapshot-anchors _ci-snapshot-anchors-test _ci-rustfmt-bail _ci-rustfmt-bail-test _ci-grammar-marker-sync _ci-grammar-marker-sync-test _ci-check-versions _ci-check-versions-test _ci-check-grammar-crate-test _ci-check-excluded-manifests _ci-check-excluded-manifests-test _ci-check-ruff-lockstep _ci-check-ruff-lockstep-test _ci-check-publish-metadata _ci-check-publish-metadata-test _ci-check-manpage-assets _ci-check-manpage-drift-test _ci-check-diagnostic-prefix _ci-check-diagnostic-prefix-test _ci-check-feature-gates-test _ci-check-safety-doc-pin _ci-check-safety-doc-pin-test _ci-worktree-setup-test _ci-gate-status-test _ci-check-tools-test _ci-enums-check _ci-enums-codegen-drift _ci-enums-codegen-drift-test _ci-enums-codegen-drift-test _ci-self-scan _ci-self-scan-headroom _ci-cargo-pipeline _ci-py-fmt-check _ci-py-lint _ci-py-typecheck _ci-py-test _ci-py-stubtest +.PHONY: help check-tools worktree-setup worktree-setup-test build build-release check test test-doc chain-audit fmt fmt-check markdown-fmt markdown-lint shellcheck sh-fmt sh-fmt-check toml-fmt toml-fmt-check toml-lint makefile-check actionlint snapshot-anchors snapshot-anchors-test rustfmt-bail rustfmt-bail-test grammar-marker-sync grammar-marker-sync-test check-versions check-excluded-manifests check-excluded-manifests-test check-ruff-lockstep check-ruff-lockstep-test check-publish-metadata check-publish-metadata-test check-manpage-assets check-manpage-drift-test check-diagnostic-prefix check-diagnostic-prefix-test check-feature-gates check-feature-gates-test check-test-lang-gates check-test-lang-gates-compare check-test-lang-gates-test check-safety-doc-pin check-safety-doc-pin-test gate-status-test check-tools-test enums-check enums-codegen-drift enums-codegen-drift-test self-scan self-scan-headroom self-scan-write-baseline self-scan-write-baseline-headroom vcs lint clippy udeps insta-review insta-accept clean distclean install install-cli install-web doc doc-open doc-check doc-check-docsrs book book-serve book-pot book-po-update book-ja book-deploy all pre-commit ci release-check verify-changelog pkg-deb-local pkg-rpm-local dev-env-build dev-env-run dev-env-shell dev-env-rm py-bootstrap py-sync py-relock py-clean py-fmt py-fmt-check py-lint py-typecheck py-test py-stubtest smoke smoke-cli smoke-lib bench bench-scaling bench-walk fuzz-check fuzz-smoke fuzz-replay fuzz-run fuzz-tmin _check-find _pc-all _pc-fmt _pc-clippy _pc-test _pc-doc-check _pc-udeps _pc-shellcheck _pc-markdown-lint _pc-toml-lint _pc-makefile-check _pc-actionlint _pc-snapshot-anchors _pc-snapshot-anchors-test _pc-rustfmt-bail _pc-rustfmt-bail-test _pc-grammar-marker-sync _pc-grammar-marker-sync-test _pc-check-versions _pc-check-versions-test _pc-check-grammar-crate-test _pc-check-excluded-manifests _pc-check-excluded-manifests-test _pc-check-ruff-lockstep _pc-check-ruff-lockstep-test _pc-check-publish-metadata _pc-check-publish-metadata-test _pc-check-manpage-assets _pc-check-manpage-drift-test _pc-check-diagnostic-prefix _pc-check-diagnostic-prefix-test _pc-check-feature-gates-test _pc-check-test-lang-gates _pc-check-test-lang-gates-test _pc-check-safety-doc-pin _pc-check-safety-doc-pin-test _pc-worktree-setup-test _pc-gate-status-test _pc-check-tools-test _pc-enums-check _pc-enums-codegen-drift _pc-enums-codegen-drift-test _pc-self-scan _pc-self-scan-headroom _pc-py-fmt _pc-py-typecheck _pc-py-test _pc-py-stubtest _ci-all _ci-fmt-check _ci-clippy _ci-test _ci-doc-check _ci-build _ci-udeps _ci-shellcheck _ci-markdown-lint _ci-toml-lint _ci-makefile-check _ci-actionlint _ci-snapshot-anchors _ci-snapshot-anchors-test _ci-rustfmt-bail _ci-rustfmt-bail-test _ci-grammar-marker-sync _ci-grammar-marker-sync-test _ci-check-versions _ci-check-versions-test _ci-check-grammar-crate-test _ci-check-excluded-manifests _ci-check-excluded-manifests-test _ci-check-ruff-lockstep _ci-check-ruff-lockstep-test _ci-check-publish-metadata _ci-check-publish-metadata-test _ci-check-manpage-assets _ci-check-manpage-drift-test _ci-check-diagnostic-prefix _ci-check-diagnostic-prefix-test _ci-check-feature-gates-test _ci-check-test-lang-gates _ci-check-test-lang-gates-test _ci-check-safety-doc-pin _ci-check-safety-doc-pin-test _ci-worktree-setup-test _ci-gate-status-test _ci-check-tools-test _ci-enums-check _ci-enums-codegen-drift _ci-enums-codegen-drift-test _ci-enums-codegen-drift-test _ci-self-scan _ci-self-scan-headroom _ci-cargo-pipeline _ci-py-fmt-check _ci-py-lint _ci-py-typecheck _ci-py-test _ci-py-stubtest # Default target help: @@ -148,6 +148,9 @@ help: @echo " check-diagnostic-prefix-test Self-tests for the diagnostic-prefix gate" @echo " check-feature-gates Assert union-gated tests are absent under a disjoint feature set" @echo " check-feature-gates-test Self-tests for the feature-gates gate" + @echo " check-test-lang-gates Assert every test naming a language carries its cfg" + @echo " check-test-lang-gates-compare Fail if a test stopped being built by a language leg since COMPARE_REF" + @echo " check-test-lang-gates-test Self-tests for the test-language gate" @echo " check-safety-doc-pin Assert node.rs's unsafe soundness doc cites the live tree-sitter pin" @echo " check-safety-doc-pin-test Self-tests for the safety-doc-pin gate" @echo " worktree-setup-test Self-tests for the worktree-setup submodule classifier" @@ -657,6 +660,47 @@ check-feature-gates-test: @echo "Running check-feature-gates self-tests..." @(cd $(BASE_DIR) && python3 -m unittest -q utils/check-feature-gates-test.py) +# Per-language test gating (#1472). The companion to the gate above, +# and the opposite question: that one asks whether a *declared* union +# still excludes what it claims to, this one derives what the marker +# should be from the languages an item names and reports the ones +# missing it. A scanner of declared markers cannot see a missing one — +# deleting the marker deletes the subject it scans for. +# +# Unlike `check-feature-gates`, this IS in `pre-commit` / `ci`: it is a +# pure source scan with no cargo invocation, so it costs a few seconds. +# `--fix` writes the missing markers in place. +check-test-lang-gates: + @echo "Checking per-language test gates..." + @(cd $(BASE_DIR) && python3 utils/check-test-lang-gates.py) + +# The third direction (#1478), and the only one that needs history. +# Both checks above compare a marker against the derivation; when the +# two agree and are both wrong, nothing static can tell. This scans the +# tree at COMPARE_REF too, computes which single-language builds compile +# each test in each, and fails on a test that still exists but stopped +# being built somewhere. No cargo: `git archive` plus the same scanner. +# +# Deliberately NOT in `pre-commit`. It needs a base revision to be +# meaningful, and a working tree mid-edit has none — the PR's merge +# base is the answer, so CI supplies it. Locally: +# +# make check-test-lang-gates-compare COMPARE_REF=origin/main +# +# Spelled out rather than a bare `REF`, which `?=` would take from +# an exported environment variable of that name. +COMPARE_REF ?= origin/main +check-test-lang-gates-compare: + @echo "Comparing per-language test membership against $(COMPARE_REF)..." + @(cd $(BASE_DIR) && python3 utils/check-test-lang-gates.py --compare $(COMPARE_REF)) + +# Self-tests for the test-language gate. Its `RepositoryTest` also pins +# that the derivation still reproduces every marker written by hand, +# which is what licenses the ~3,000 it generated. +check-test-lang-gates-test: + @echo "Running check-test-lang-gates self-tests..." + @(cd $(BASE_DIR) && python3 -m unittest -q utils/check-test-lang-gates-test.py) + # Safety-doc pin gate (#1057). The module doc of # big-code-analysis-py/src/node.rs is the canonical soundness argument # for this workspace's only sanctioned `unsafe` block, and it reasons @@ -1392,7 +1436,7 @@ lint: $(MAKE) -j --output-sync=target \ _ci-clippy \ _ci-shellcheck _ci-markdown-lint _ci-toml-lint _ci-makefile-check \ - _ci-actionlint _ci-snapshot-anchors _ci-snapshot-anchors-test _ci-rustfmt-bail _ci-rustfmt-bail-test _ci-grammar-marker-sync _ci-grammar-marker-sync-test _ci-check-versions _ci-check-versions-test _ci-check-grammar-crate-test _ci-check-excluded-manifests _ci-check-excluded-manifests-test _ci-check-ruff-lockstep _ci-check-ruff-lockstep-test _ci-check-publish-metadata _ci-check-publish-metadata-test _ci-check-manpage-assets _ci-check-manpage-drift-test _ci-check-diagnostic-prefix _ci-check-diagnostic-prefix-test _ci-check-feature-gates-test _ci-check-safety-doc-pin _ci-check-safety-doc-pin-test _ci-worktree-setup-test _ci-gate-status-test _ci-check-tools-test _ci-enums-check _ci-enums-codegen-drift _ci-enums-codegen-drift-test + _ci-actionlint _ci-snapshot-anchors _ci-snapshot-anchors-test _ci-rustfmt-bail _ci-rustfmt-bail-test _ci-grammar-marker-sync _ci-grammar-marker-sync-test _ci-check-versions _ci-check-versions-test _ci-check-grammar-crate-test _ci-check-excluded-manifests _ci-check-excluded-manifests-test _ci-check-ruff-lockstep _ci-check-ruff-lockstep-test _ci-check-publish-metadata _ci-check-publish-metadata-test _ci-check-manpage-assets _ci-check-manpage-drift-test _ci-check-diagnostic-prefix _ci-check-diagnostic-prefix-test _ci-check-feature-gates-test _ci-check-test-lang-gates _ci-check-test-lang-gates-test _ci-check-safety-doc-pin _ci-check-safety-doc-pin-test _ci-worktree-setup-test _ci-gate-status-test _ci-check-tools-test _ci-enums-check _ci-enums-codegen-drift _ci-enums-codegen-drift-test # --------------------------------------------------------------------------- # Maintenance @@ -1564,7 +1608,7 @@ _pc-all: $(MAKE) -j --output-sync=target \ _pc-test \ _pc-shellcheck _pc-markdown-lint _pc-toml-lint _pc-makefile-check \ - _pc-actionlint _pc-snapshot-anchors _pc-snapshot-anchors-test _pc-rustfmt-bail _pc-rustfmt-bail-test _pc-grammar-marker-sync _pc-grammar-marker-sync-test _pc-check-versions _pc-check-versions-test _pc-check-grammar-crate-test _pc-check-excluded-manifests _pc-check-excluded-manifests-test _pc-check-ruff-lockstep _pc-check-ruff-lockstep-test _pc-check-publish-metadata _pc-check-publish-metadata-test _pc-check-manpage-assets _pc-check-manpage-drift-test _pc-check-diagnostic-prefix _pc-check-diagnostic-prefix-test _pc-check-feature-gates-test _pc-check-safety-doc-pin _pc-check-safety-doc-pin-test _pc-worktree-setup-test _pc-gate-status-test _pc-check-tools-test _pc-enums-check _pc-enums-codegen-drift _pc-enums-codegen-drift-test \ + _pc-actionlint _pc-snapshot-anchors _pc-snapshot-anchors-test _pc-rustfmt-bail _pc-rustfmt-bail-test _pc-grammar-marker-sync _pc-grammar-marker-sync-test _pc-check-versions _pc-check-versions-test _pc-check-grammar-crate-test _pc-check-excluded-manifests _pc-check-excluded-manifests-test _pc-check-ruff-lockstep _pc-check-ruff-lockstep-test _pc-check-publish-metadata _pc-check-publish-metadata-test _pc-check-manpage-assets _pc-check-manpage-drift-test _pc-check-diagnostic-prefix _pc-check-diagnostic-prefix-test _pc-check-feature-gates-test _pc-check-test-lang-gates _pc-check-test-lang-gates-test _pc-check-safety-doc-pin _pc-check-safety-doc-pin-test _pc-worktree-setup-test _pc-gate-status-test _pc-check-tools-test _pc-enums-check _pc-enums-codegen-drift _pc-enums-codegen-drift-test \ _pc-manpages \ _pc-self-scan _pc-self-scan-headroom \ _pc-py-fmt _pc-py-typecheck _pc-py-test _pc-py-stubtest @@ -1574,7 +1618,7 @@ _ci-all: $(MAKE) -j --output-sync=target \ _ci-cargo-pipeline \ _ci-shellcheck _ci-markdown-lint _ci-toml-lint _ci-makefile-check \ - _ci-actionlint _ci-snapshot-anchors _ci-snapshot-anchors-test _ci-rustfmt-bail _ci-rustfmt-bail-test _ci-grammar-marker-sync _ci-grammar-marker-sync-test _ci-check-versions _ci-check-versions-test _ci-check-grammar-crate-test _ci-check-excluded-manifests _ci-check-excluded-manifests-test _ci-check-ruff-lockstep _ci-check-ruff-lockstep-test _ci-check-publish-metadata _ci-check-publish-metadata-test _ci-check-manpage-assets _ci-check-manpage-drift-test _ci-check-diagnostic-prefix _ci-check-diagnostic-prefix-test _ci-check-feature-gates-test _ci-check-safety-doc-pin _ci-check-safety-doc-pin-test _ci-worktree-setup-test _ci-gate-status-test _ci-check-tools-test _ci-enums-check _ci-enums-codegen-drift _ci-enums-codegen-drift-test \ + _ci-actionlint _ci-snapshot-anchors _ci-snapshot-anchors-test _ci-rustfmt-bail _ci-rustfmt-bail-test _ci-grammar-marker-sync _ci-grammar-marker-sync-test _ci-check-versions _ci-check-versions-test _ci-check-grammar-crate-test _ci-check-excluded-manifests _ci-check-excluded-manifests-test _ci-check-ruff-lockstep _ci-check-ruff-lockstep-test _ci-check-publish-metadata _ci-check-publish-metadata-test _ci-check-manpage-assets _ci-check-manpage-drift-test _ci-check-diagnostic-prefix _ci-check-diagnostic-prefix-test _ci-check-feature-gates-test _ci-check-test-lang-gates _ci-check-test-lang-gates-test _ci-check-safety-doc-pin _ci-check-safety-doc-pin-test _ci-worktree-setup-test _ci-gate-status-test _ci-check-tools-test _ci-enums-check _ci-enums-codegen-drift _ci-enums-codegen-drift-test \ _ci-py-fmt-check _ci-py-lint _ci-py-typecheck _ci-py-test _ci-py-stubtest # --------------------------------------------------------------------------- @@ -1746,6 +1790,12 @@ _pc-check-safety-doc-pin: _pc-fmt _pc-check-feature-gates-test: _pc-fmt $(MAKE) check-feature-gates-test +_pc-check-test-lang-gates: _pc-fmt + $(MAKE) check-test-lang-gates + +_pc-check-test-lang-gates-test: _pc-fmt + $(MAKE) check-test-lang-gates-test + _pc-check-safety-doc-pin-test: _pc-fmt $(MAKE) check-safety-doc-pin-test @@ -1952,6 +2002,12 @@ _ci-check-safety-doc-pin: _ci-check-feature-gates-test: $(MAKE) check-feature-gates-test +_ci-check-test-lang-gates: + $(MAKE) check-test-lang-gates + +_ci-check-test-lang-gates-test: + $(MAKE) check-test-lang-gates-test + _ci-check-safety-doc-pin-test: $(MAKE) check-safety-doc-pin-test diff --git a/big-code-analysis-ast/src/alterator.rs b/big-code-analysis-ast/src/alterator.rs index 1d4d0f50b..d5de2f35d 100644 --- a/big-code-analysis-ast/src/alterator.rs +++ b/big-code-analysis-ast/src/alterator.rs @@ -802,6 +802,7 @@ mod tests { use super::*; + #[cfg(feature = "cpp")] #[test] fn get_text_span_non_utf8_uses_replacement_char() { // Regression: `String::from_utf8(...).unwrap()` panicked on non-UTF-8 @@ -820,6 +821,21 @@ mod tests { /// Collects all AstNode entries whose type matches `target_kind`, /// recursively walking the tree. + #[cfg(any( + feature = "bash", + feature = "cpp", + feature = "go", + feature = "groovy", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "mozjs", + feature = "perl", + feature = "php", + feature = "python", + feature = "rust", + feature = "typescript", + ))] fn collect_nodes_by_kind<'a>(node: &'a AstNode, target_kind: &str, out: &mut Vec<&'a AstNode>) { if node.r#type == target_kind { out.push(node); @@ -830,6 +846,21 @@ mod tests { } /// Builds an AST from source code using the given parser type. + #[cfg(any( + feature = "bash", + feature = "cpp", + feature = "go", + feature = "groovy", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "mozjs", + feature = "perl", + feature = "php", + feature = "python", + feature = "rust", + feature = "typescript", + ))] fn build_ast(code: &[u8], filename: &str) -> AstNode { let path = PathBuf::from(filename); let parser = P::new(code.to_vec(), &path, None); @@ -845,6 +876,7 @@ mod tests { /// Asserts that every `"string"` node in the AST is flattened: /// non-empty text value and no children. + #[cfg(any(feature = "javascript", feature = "php", feature = "typescript"))] fn assert_strings_flattened(root: &AstNode) { let mut strings = Vec::new(); collect_nodes_by_kind(root, "string", &mut strings); @@ -869,6 +901,7 @@ mod tests { // Regression tests for #119: String2 (and String3) variants must be // flattened the same way as String. These exercises string literals in // multiple grammatical positions to cover aliased kind_ids. + #[cfg(feature = "javascript")] #[test] fn javascript_string_nodes_all_flattened() { // Strings in expression, property key, and import positions @@ -883,6 +916,7 @@ mod tests { assert_strings_flattened(&root); } + #[cfg(feature = "typescript")] #[test] fn typescript_string_nodes_all_flattened() { let code = br#" @@ -895,6 +929,7 @@ mod tests { assert_strings_flattened(&root); } + #[cfg(feature = "typescript")] #[test] fn tsx_string_nodes_all_flattened() { // TSX has String, String2, and String3 — exercise JSX attribute @@ -908,6 +943,7 @@ mod tests { assert_strings_flattened(&root); } + #[cfg(feature = "php")] #[test] fn php_string_like_nodes_all_flattened() { // Regression: issue #288. PHP `string`, `encapsed_string`, @@ -958,6 +994,7 @@ mod tests { } } + #[cfg(feature = "groovy")] #[test] fn groovy_string_literal_preserved_verbatim() { // Regression for the `impl Alterator for GroovyCode` arms: @@ -1001,6 +1038,7 @@ mod tests { } } + #[cfg(feature = "groovy")] #[test] fn groovy_multiline_string_fragment_preserves_newlines() { // The `StringLiteral`/`MultilineStringFragment` arms route @@ -1026,6 +1064,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_raw_string_literal_flattened() { // Regression for issue #391: `Rust::RawStringLiteral` was missing @@ -1069,6 +1108,7 @@ mod tests { assert_eq!(strings[0].value, "\"world\""); } + #[cfg(feature = "cpp")] #[test] fn cpp_raw_string_literal_flattened() { // Regression for issue #398: `Cpp::RawStringLiteral` was missing @@ -1115,6 +1155,18 @@ mod tests { /// Asserts that every node of kind `target_kind` in the AST is /// flattened (no children) and carries non-empty verbatim text. /// Shared by the #699 per-language string-flattening regressions. + #[cfg(any( + feature = "bash", + feature = "cpp", + feature = "go", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "mozjs", + feature = "perl", + feature = "python", + feature = "typescript", + ))] fn assert_kind_flattened(root: &AstNode, target_kind: &str) { let mut nodes = Vec::new(); collect_nodes_by_kind(root, target_kind, &mut nodes); @@ -1142,6 +1194,7 @@ mod tests { // node as a string — a 3-way (alterator / is_string / get_op_type) // dump asymmetry. Each test pins that the dump now collapses the kind. + #[cfg(feature = "javascript")] #[test] fn javascript_template_string_flattened() { // #699: `is_string` matches `TemplateString`; the alterator now @@ -1153,6 +1206,7 @@ mod tests { assert_kind_flattened(&root, "template_string"); } + #[cfg(feature = "typescript")] #[test] fn typescript_template_string_flattened() { let code = br#"const a = 1; const b = `bare`; const c = `pre ${a} post`;"#; @@ -1160,6 +1214,7 @@ mod tests { assert_kind_flattened(&root, "template_string"); } + #[cfg(feature = "typescript")] #[test] fn tsx_template_string_flattened() { let code = br#"const a = 1; const b = `bare`; const c = `pre ${a} post`;"#; @@ -1167,6 +1222,7 @@ mod tests { assert_kind_flattened(&root, "template_string"); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_template_string_flattened() { // The MozJS arm's comment previously claimed `TemplateString` @@ -1176,6 +1232,7 @@ mod tests { assert_kind_flattened(&root, "template_string"); } + #[cfg(feature = "cpp")] #[test] fn cpp_concatenated_string_flattened() { // #699: `is_string` matches `concatenated_string` (`"a" "b"`); @@ -1186,6 +1243,7 @@ mod tests { assert_kind_flattened(&root, "concatenated_string"); } + #[cfg(feature = "python")] #[test] fn python_string_and_concatenated_string_flattened() { // #699: Python had no alterator override, so `string` (incl. @@ -1198,6 +1256,7 @@ mod tests { assert_kind_flattened(&root, "concatenated_string"); } + #[cfg(feature = "java")] #[test] fn java_string_literals_flattened() { // #699: Java had no alterator override, so `string_literal` kept @@ -1222,6 +1281,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_string_literals_flattened() { // #699: Kotlin had no alterator override. `string_literal` (incl. @@ -1233,6 +1293,7 @@ mod tests { assert_kind_flattened(&root, "multiline_string_literal"); } + #[cfg(feature = "go")] #[test] fn go_rune_literal_flattened_but_not_a_string_kind() { // #699 verdict: Go `rune_literal` is operand + flattened but @@ -1245,6 +1306,7 @@ mod tests { assert_kind_flattened(&root, "rune_literal"); } + #[cfg(feature = "bash")] #[test] fn bash_heredoc_body_flattened() { // #761: `Checker::is_string` matches Bash `heredoc_body` @@ -1267,6 +1329,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_heredoc_body_statement_flattened() { // #761: `Checker::is_string` matches Perl `heredoc_body_statement` diff --git a/big-code-analysis-ast/src/ast.rs b/big-code-analysis-ast/src/ast.rs index 629aec7e7..9fe816730 100644 --- a/big-code-analysis-ast/src/ast.rs +++ b/big-code-analysis-ast/src/ast.rs @@ -376,6 +376,7 @@ mod tests { use super::*; + #[cfg(any(feature = "cpp", feature = "rust", feature = "tcl"))] fn build_ast(code: &[u8], filename: &str) -> AstNode { let path = PathBuf::from(filename); let parser = P::new(code.to_vec(), &path, None); @@ -390,6 +391,7 @@ mod tests { .expect("parser should produce a root AST node") } + #[cfg(feature = "rust")] fn build_ast_with_span(code: &[u8], filename: &str) -> AstNode { let path = PathBuf::from(filename); let parser = P::new(code.to_vec(), &path, None); @@ -404,6 +406,7 @@ mod tests { .expect("parser should produce a root AST node") } + #[cfg(any(feature = "cpp", feature = "rust"))] fn find_first<'a>(node: &'a AstNode, kind: &str) -> Option<&'a AstNode> { if node.r#type == kind { return Some(node); @@ -411,16 +414,19 @@ mod tests { node.children.iter().find_map(|c| find_first(c, kind)) } + #[cfg(any(feature = "cpp", feature = "rust"))] fn find_child<'a>(parent: &'a AstNode, field: &str) -> Option<&'a AstNode> { parent.children.iter().find(|c| c.field_name == Some(field)) } + #[cfg(feature = "rust")] #[test] fn root_has_no_field_name() { let root = build_ast::(b"fn main() {}", "test.rs"); assert_eq!(root.field_name, None); } + #[cfg(feature = "rust")] #[test] fn rust_assignment_carries_left_and_right_field_names() { // `assignment_expression` in the Rust grammar names its operands @@ -450,6 +456,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_function_carries_name_and_body_field_names() { // `function_item` names children `name`, `parameters`, `body`. @@ -471,6 +478,7 @@ mod tests { assert_eq!(body_child.r#type, "block"); } + #[cfg(feature = "cpp")] #[test] fn cpp_assignment_carries_left_and_right_field_names() { // Cross-language confirmation: the C/C++ grammar uses the same @@ -489,6 +497,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn serialized_json_includes_field_name_key() { // Regression for the Serialize derive: every node must serialize @@ -513,6 +522,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn serialized_json_uses_snake_case_keys() { // The serialized AST shape uses snake_case keys (#535). This @@ -537,6 +547,7 @@ mod tests { } } + #[cfg(feature = "rust")] #[test] fn span_serializes_as_named_object() { // The span is a flat named object preserving the 1-based @@ -619,6 +630,7 @@ mod tests { /// The deepest `children` chain in a tree, measured iteratively so a /// pathological input cannot overflow the measurement itself. + #[cfg(feature = "tcl")] fn ast_depth(root: &AstNode) -> usize { let mut deepest = 0; let mut stack = vec![(root, 1usize)]; diff --git a/big-code-analysis-ast/src/checker.rs b/big-code-analysis-ast/src/checker.rs index 3f289c702..9eccf6a3d 100644 --- a/big-code-analysis-ast/src/checker.rs +++ b/big-code-analysis-ast/src/checker.rs @@ -1128,20 +1128,24 @@ mod tests { use std::fmt::Write as _; use std::path::PathBuf; + #[cfg(feature = "bash")] fn parse(source: &str) -> BashParser { BashParser::new(source.as_bytes().to_vec(), &PathBuf::from("test.sh"), None) } + #[cfg(feature = "bash")] fn count_strings(source: &str) -> usize { count(&parse(source), &["string".to_string()]).0 } // `count`'s filter parser accepts a numeric string as a `kind_id` match // (parser.rs `filters`), so `has_kind` reuses the same primitive. + #[cfg(feature = "bash")] fn has_kind(source: &str, kind_id: u16) -> bool { count(&parse(source), &[kind_id.to_string()]).0 > 0 } + #[cfg(feature = "bash")] #[test] fn bash_is_string_excludes_word_tokens() { // `echo hello world` produces three Word nodes — none of them are @@ -1154,6 +1158,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_is_string_matches_quoted_literals() { // Regular double-quoted string -> `string` (Bash::String). @@ -1164,6 +1169,7 @@ mod tests { assert_eq!(count_strings("echo $'ansi-c'\n"), 1); } + #[cfg(feature = "bash")] #[test] fn bash_is_string_matches_translated_string() { // tree-sitter-bash only emits a visible `translated_string` node @@ -1180,6 +1186,7 @@ mod tests { assert_eq!(count_strings(src), 2); } + #[cfg(feature = "bash")] #[test] fn bash_is_string_matches_heredoc_bodies() { // Plain heredoc body. @@ -1200,14 +1207,17 @@ mod tests { // ===== PHP `is_string` regression tests (issue #288) ===== + #[cfg(feature = "php")] fn parse_php(source: &str) -> PhpParser { PhpParser::new(source.as_bytes().to_vec(), &PathBuf::from("test.php"), None) } + #[cfg(feature = "php")] fn count_php_strings(source: &str) -> usize { count(&parse_php(source), &["string".to_string()]).0 } + #[cfg(feature = "php")] #[test] fn php_is_string_matches_single_quoted_literal() { // `Php::String` is the named single-quoted literal. Inert @@ -1216,6 +1226,7 @@ mod tests { assert_eq!(count_php_strings(" bool>( parser: &P, target: u16, @@ -1285,6 +1322,7 @@ mod tests { hits } + #[cfg(feature = "javascript")] #[test] fn javascript_is_string_matches_string2_alias() { // `Javascript::String2` (kind_id 221) aliases to `"string"` @@ -1311,6 +1349,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_is_string_matches_string2_alias() { // Parallel coverage for the MozJS dialect; same `String2` @@ -1327,6 +1366,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_is_string_excludes_type_keyword_alias_1261() { // TypeScript's only anonymous `"string"` alias, `String2` @@ -1362,6 +1402,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_is_string_matches_literal_alias_not_type_keyword_1261() { // TSX uniquely carries two anonymous `"string"` aliases: @@ -1402,6 +1443,14 @@ mod tests { // `target`. Used by the `is_else_if` tests below to fish a // specific node out of the parse tree without depending on the // `count` helper above. + #[cfg(any( + feature = "c", + feature = "cpp", + feature = "groovy", + feature = "mozcpp", + feature = "python", + feature = "rust", + ))] fn find_first_kind(parser: &P, target: u16) -> Option> { let mut stack = vec![parser.root()]; while let Some(node) = stack.pop() { @@ -1423,6 +1472,7 @@ mod tests { /// `rust_outer_attr_marks_test` — not the inner scan. Pins the key /// invariant of the helper split: a `mod_item`'s outer attributes are /// never skipped in favour of only its inner attributes. + #[cfg(feature = "rust")] #[test] fn rust_outer_attr_on_mod_is_test_only() { let src = "#[cfg(test)]\nmod tests {\n fn t() {}\n}\n"; @@ -1445,6 +1495,7 @@ mod tests { /// attribute nested in the module body. The outer sibling scan finds /// nothing; `rust_inner_attr_marks_test` descends via the `body` /// field and catches it. + #[cfg(feature = "rust")] #[test] fn rust_inner_attr_in_mod_is_test_only() { let src = "mod tests {\n #![cfg(test)]\n fn t() {}\n}\n"; @@ -1462,6 +1513,7 @@ mod tests { } /// A plain, unattributed item is not test-only — neither scan matches. + #[cfg(feature = "rust")] #[test] fn rust_plain_item_is_not_test_only() { let src = "fn foo() {}\n"; @@ -1495,6 +1547,7 @@ mod tests { /// The counted `marked` / `unmarked` totals are what stop it from /// passing vacuously — two readings that both answered `false` /// everywhere would agree perfectly. + #[cfg(feature = "rust")] #[test] fn rust_outer_attr_scans_agree() { // One attribute run longer than the file's actual code, so the @@ -1658,6 +1711,7 @@ mod tests { /// one byte past the item, say, or a run start taken as the run end /// — is a *silently wrong prune* in production, because the walker /// never asks again (#1446). + #[cfg(feature = "rust")] #[test] fn rust_should_skip_subtree_matches_the_backward_reading() { let source = "#[cfg(test)]\nmod tests {\nfn a() {}\n}\n\ @@ -1813,6 +1867,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_is_else_if_recognises_else_followed_by_if() { // Direct assertion that `GroovyCode::is_else_if` returns true @@ -1846,6 +1901,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_is_else_if_false_for_standalone_if() { // A bare `if` (no `else` preceding it) must NOT register as @@ -1857,6 +1913,7 @@ mod tests { assert!(!GroovyCode::is_else_if(&node, Ancestors::unknown())); } + #[cfg(feature = "groovy")] #[test] fn groovy_is_call_excludes_constructors() { // Regression for #430. `GroovyCode::is_call` previously matched @@ -1915,6 +1972,7 @@ mod tests { // exactly what justifies keeping the unsuffixed arm defensively // (the grammar-dispatch §2 technique, applied to an alias-mapped // rule rather than a hidden one). + #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp"))] #[track_caller] fn assert_call_kinds_are_aliased( parser: &P, @@ -1939,6 +1997,7 @@ mod tests { // `function_declarator`, the `sizeof(int)` operand, and the `(int)x` // cast. Six differs from the C++ fixture's five, so a test that // reaches for the wrong parser cannot pass on the right number. + #[cfg(feature = "c")] const C_CALL_SHAPES: &str = "int f(int);\n\ int main(void) {\n\ f(1);\n\ @@ -1957,6 +2016,7 @@ mod tests { // from the fixture — the grammar emits it as a plain `call_expression`, // so it *is* counted, and including it here would blur what the five // is pinning. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const CPP_CALL_SHAPES: &str = "struct T { T(int); int m(); };\n\ namespace ns { int f(); }\n\ int main() {\n\ @@ -1979,6 +2039,7 @@ mod tests { /// `alias(…, $.call_expression)` — it is absent from `node-types.json` /// and never reaches `kind_id()`. Matching it alone left `is_call` /// dead: `bca count -t call` reported 0 on ordinary C (#1254). + #[cfg(feature = "c")] #[test] fn c_is_call_matches_the_aliased_call_expression() { let parser = CParser::new( @@ -2016,6 +2077,7 @@ mod tests { /// C++ counterpart of [`c_is_call_matches_the_aliased_call_expression`]. /// Member, qualified and function-pointer calls all arrive as the same /// aliased `CallExpression2`; object construction does not (#1254). + #[cfg(feature = "cpp")] #[test] fn cpp_is_call_matches_the_aliased_call_expression() { let parser = CppParser::new( @@ -2054,6 +2116,7 @@ mod tests { /// extension-owning sibling instead: the Mozilla fork inherits /// upstream C++'s aliasing, so the same source must yield the same /// calls (grammar-dispatch, "sweep the rest"). + #[cfg(all(feature = "cpp", feature = "mozcpp"))] #[test] fn mozcpp_is_call_agrees_with_cpp() { let source = CPP_CALL_SHAPES.as_bytes().to_vec(); @@ -2087,6 +2150,7 @@ mod tests { ); } + #[cfg(feature = "python")] fn parse_python(src: &str) -> PythonParser { PythonParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.py"), None) } @@ -2094,6 +2158,7 @@ mod tests { // Walk the AST and return every node whose `kind_id` equals `target`, // in DFS pre-order. Used by the Python `is_else_if` tests below to // distinguish the outer if from the inner one in an `else: if` chain. + #[cfg(feature = "python")] fn find_all_kinds(parser: &P, target: u16) -> Vec> { let mut out = Vec::new(); let mut stack = vec![parser.root()]; @@ -2110,6 +2175,7 @@ mod tests { out } + #[cfg(feature = "python")] #[test] fn python_is_else_if_recognises_if_inside_else_clause() { // `else: if b:` chains parse as `else_clause → block → if_statement` @@ -2132,6 +2198,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_is_else_if_false_for_standalone_if() { // A bare `if` whose parent is the module / function body must @@ -2142,6 +2209,7 @@ mod tests { assert!(!PythonCode::is_else_if(&node, Ancestors::unknown())); } + #[cfg(feature = "python")] #[test] fn python_is_else_if_false_for_outer_if_with_elif_alternative() { // `elif` parses as an `ElifClause`, not an `IfStatement`, so the @@ -2159,6 +2227,7 @@ mod tests { // `else_clause`'s `block` wrapper, or `None` if no such node exists. // Used by tests below instead of relying on `find_all_kinds`'s DFS // pre-order to land at `ifs[1]`. + #[cfg(feature = "python")] fn find_python_if_inside_else_block(parser: &PythonParser) -> Option> { find_all_kinds(parser, Python::IfStatement as u16) .into_iter() @@ -2171,6 +2240,7 @@ mod tests { }) } + #[cfg(feature = "python")] #[test] fn python_is_else_if_false_when_else_body_has_siblings() { // `else: if b:` followed by another statement at the same indent @@ -2201,6 +2271,7 @@ mod tests { // (function, class, if/for bodies, lambda); if a bump ever emits one, // the guard flips red and forces a positive assertion to be added — // mirroring the `Php::String3` hidden-supertype guard above. + #[cfg(feature = "python")] #[test] fn python_hidden_block_and_lambda_aliases_stay_unseen() { let src = "def f(a, b):\n if a:\n return b\n for x in b:\n print(x)\n\nclass C:\n def m(self):\n pass\n\ng = lambda x: x + 1\n"; @@ -2239,6 +2310,7 @@ mod tests { // emitted `Lambda` and that the predicate is not vacuously true (it // rejects the enclosing `FunctionDefinition`). The unseen `Lambda2` // half of the set is covered by the drift guard above. + #[cfg(feature = "python")] #[test] fn python_is_lambda_matches_live_lambda_and_agrees_with_is_closure() { use crate::lang_helpers::python::python_is_lambda; @@ -2278,6 +2350,7 @@ mod tests { /// changed both signatures to take `Ancestors`, which is exactly the /// kind of edit that can quietly invert a one-line predicate, so pin /// the contract directly. + #[cfg(all(feature = "c-family-helpers", feature = "elixir"))] #[test] fn languages_without_else_if_chains_answer_false() { // The `Checker` default, via the two grammars that do not @@ -2334,6 +2407,25 @@ mod tests { // their dedicated `impl_js_family_is_string!` macro and have // their own alias-aware tests above; they are intentionally // not duplicated here. + #[cfg(any( + feature = "bash", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "kotlin", + feature = "lua", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + ))] fn count_with_parser(parser: &P) -> usize { count(parser, &["string".to_string()]).0 } @@ -2343,6 +2435,25 @@ mod tests { // failures unambiguous: a presence failure means the fixture no // longer produces the variant (likely grammar drift); a match // failure means a macro invocation dropped the variant. + #[cfg(any( + feature = "bash", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "kotlin", + feature = "lua", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + ))] fn assert_variant_is_string bool>( parser: &P, target: u16, @@ -2367,6 +2478,25 @@ mod tests { // exercise. The macro feeds `stringify!` for both the language // and variant labels so test failures keep the same "Lang::Variant" // wording the helper already emits. + #[cfg(all( + feature = "bash", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "kotlin", + feature = "lua", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + ))] macro_rules! assert_variants_is_string { ($parser:expr, $lang:ident, $code:ident, [$($variant:ident),+ $(,)?]) => { $( @@ -2385,6 +2515,25 @@ mod tests { // filter yields zero matches. Used by the negative test, which // walks every language consolidated under `impl_simple_is_string!` // with identical per-language shape (parse → count → assert_eq! 0). + #[cfg(all( + feature = "bash", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "kotlin", + feature = "lua", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + ))] macro_rules! assert_no_string_matches { ($parser_ty:ident, $path:expr, $src:expr, $lang:literal $(,)?) => {{ let parser = $parser_ty::new($src.to_vec(), $path, None); @@ -2392,6 +2541,25 @@ mod tests { }}; } + #[cfg(all( + feature = "bash", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "kotlin", + feature = "lua", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + ))] #[test] fn simple_is_string_macro_recognises_each_language() { use crate::langs::{ @@ -2656,6 +2824,25 @@ mod tests { assert_variants_is_string!(&parser, Groovy, GroovyCode, [StringLiteral]); } + #[cfg(all( + feature = "bash", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "kotlin", + feature = "lua", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + ))] #[test] fn simple_is_string_macro_rejects_non_string_nodes() { // Pure-identifier source must produce zero string matches. @@ -2714,6 +2901,7 @@ mod tests { assert_no_string_matches!(GroovyParser, &path, b"def m() { def x = y }\n", "Groovy"); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_parses_using_declaration() { // Drift marker for the JS-base-grammar bump 0.23.1 -> 0.25.0 @@ -2731,6 +2919,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_rune_literal_is_not_a_string() { // #699 verdict: Go `RuneLiteral` is an operand in `get_op_type` @@ -2758,6 +2947,7 @@ mod tests { ); } + #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] #[test] fn c_family_char_literal_is_not_a_string() { // The C-family half of the #699 verdict `go_rune_literal_is_not_a_string` @@ -2777,6 +2967,7 @@ mod tests { // `char_literal` at all. use crate::langs::{CParser, CppParser, MozcppParser, ObjcParser}; + #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] fn check bool + Copy>( source: &[u8], file: &str, @@ -2843,6 +3034,12 @@ mod tests { /// into the closure column. The counters keep a fixture honest: both /// answers have to occur, for both predicates, or the parity holds /// only over nodes the predicates never classify. + #[cfg(any( + feature = "javascript", + feature = "mozjs", + feature = "ruby", + feature = "typescript" + ))] fn assert_func_parity( label: &str, code: &[u8], @@ -2886,6 +3083,7 @@ mod tests { /// These are the predicates with the longest ancestor lookup: an /// upward walk, its `is_else_if` filter, and a `has_sibling` /// adjacency check. + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_func_and_closure_agree_between_known_and_climbing() { // One of each shape the walk distinguishes: an arrow bound to a @@ -2928,6 +3126,7 @@ mod tests { /// the only node here whose answer depends on the parent lookup — /// every other block's parent is a `Call`, which the arm accepts /// whether the lookup is right or wrong. + #[cfg(feature = "ruby")] #[test] fn ruby_block_closure_agrees_between_known_and_climbing() { let code = concat!( diff --git a/big-code-analysis-ast/src/comment_rm.rs b/big-code-analysis-ast/src/comment_rm.rs index 6d14a1f3f..4ea442f7f 100644 --- a/big-code-analysis-ast/src/comment_rm.rs +++ b/big-code-analysis-ast/src/comment_rm.rs @@ -191,12 +191,20 @@ mod tests { /// Panics when nothing was removed, which every caller below relies /// on: each fixture carries at least one strippable comment, so a /// `None` means the walk stopped finding comments at all. + #[cfg(any( + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "mozcpp", + feature = "objc", + ))] fn strip(src: &str, path: &str) -> String { let parser = T::new(src.as_bytes().to_vec(), &PathBuf::from(path), None); let stripped = rm_comments(&parser).expect("every fixture has a removable comment"); String::from_utf8(stripped).expect("stripping preserves UTF-8") } + #[cfg(feature = "c-family-helpers")] const SOURCE_CODE: &str = "/* Remove this code block */\n\ int a = 42; // Remove this comment\n\ // Remove this comment\n\ @@ -206,6 +214,7 @@ mod tests { * comment\n\ */"; + #[cfg(feature = "c-family-helpers")] const SOURCE_CODE_NO_COMMENTS: &str = "\n\ int a = 42; \n\ \n\ @@ -215,6 +224,7 @@ mod tests { \n\ \n"; + #[cfg(feature = "c-family-helpers")] #[test] fn ccomment_remove_comments() { let path = PathBuf::from("foo.c"); @@ -237,6 +247,7 @@ mod tests { /// newline as `\r\n` — including the lines the removed comment spanned /// (issue #767). Before the fix, `remove_from_code` substituted bare `\n` /// for those lines, producing a mixed-ending buffer. + #[cfg(feature = "c-family-helpers")] #[test] fn ccomment_remove_comments_preserves_crlf() { let path = PathBuf::from("foo.c"); @@ -287,6 +298,7 @@ mod tests { /// than calling `Node::parent` (#1096), so this pins that the chain /// really reaches the comment: a chain that went stale would report /// the wrong parent and strip the token. + #[cfg(feature = "rust")] #[test] fn rust_keeps_a_macro_token_comment_and_strips_an_ordinary_one() { let path = PathBuf::from("foo.rs"); @@ -319,11 +331,25 @@ mod tests { /// Each case pairs the marker comment with an ordinary one so a /// `is_useful_comment` that answered `true` unconditionally — the /// other way to make the first assertion pass — fails the second. + #[cfg(all( + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "mozcpp", + feature = "objc", + ))] #[test] fn the_c_family_keeps_a_rustbindgen_comment_and_strips_an_ordinary_one() { // `/**
( label: &str, code: &[u8], @@ -1135,6 +1144,12 @@ mod ancestor_tests { } } + #[cfg(all( + feature = "elixir", + feature = "javascript", + feature = "mozjs", + feature = "typescript", + ))] #[test] fn func_space_name_agrees_between_known_and_climbing() { // `outer` and `keyed` are only reachable through the parent: diff --git a/big-code-analysis-ast/src/langs.rs b/big-code-analysis-ast/src/langs.rs index 2b2846594..83c835e3b 100644 --- a/big-code-analysis-ast/src/langs.rs +++ b/big-code-analysis-ast/src/langs.rs @@ -389,6 +389,31 @@ mod tests { /// runtime accessor and the actual dependency graph cannot silently /// disagree (#727). Feature-independent: the version literals and the /// manifest pins both exist regardless of the enabled language set. + #[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", + ))] #[test] fn grammar_version_matches_cargo_toml_pin() { // CARGO_MANIFEST_DIR is this crate's dir, one level below the @@ -655,6 +680,7 @@ mod tests { // only by explicit `--language mozcpp` / manifest / API selection. // Pin this so a future `mk_langs!` reorder cannot silently hand a // C-family extension to the fork (the failure mode #720 guards). + #[cfg(any(feature = "cpp", feature = "mozcpp"))] #[test] fn cpp_extension_dispatch_defaults_to_upstream() { assert_eq!(get_from_ext("cpp"), Some(LANG::Cpp)); @@ -717,6 +743,7 @@ mod tests { // `Hash` (+ `Eq`) lets `LANG` key a `HashMap` / populate a // `HashSet` — the headline use case from issue #508. + #[cfg(any(feature = "cpp", feature = "python", feature = "rust"))] #[test] fn lang_is_usable_as_hash_key() { use std::collections::{HashMap, HashSet}; @@ -737,6 +764,7 @@ mod tests { // can distinguish "X is disabled" from "Y is disabled" in a // mixed batch. Verifies the `Display` impl mentions the // language name as documented in `src/error.rs`. + #[cfg(feature = "rust")] #[test] fn language_disabled_display_includes_language_name() { let err = MetricsError::LanguageDisabled(LANG::Rust); diff --git a/big-code-analysis-ast/src/lib.rs b/big-code-analysis-ast/src/lib.rs index 8b0c47300..966a6e025 100644 --- a/big-code-analysis-ast/src/lib.rs +++ b/big-code-analysis-ast/src/lib.rs @@ -72,12 +72,21 @@ // root `Cargo.toml` for why this is a per-root attribute and not a // Cargo lint (#1227). #![cfg_attr(not(test), warn(clippy::unwrap_used))] - // The `pub(crate)` entries below are named by nothing outside this // crate, so they stay narrow per AGENTS.md ("widen visibility only when // an item is re-exported from `lib.rs`"). `comment_rm` and `find` look // like exceptions and are not: they are reached through `$crate::` in // `mk_action!`, which expands here. + +// The import half of the `allow(dead_code)` carve-out above, and for +// the same reason: per-language test gating (#1472) makes "is this +// import live" a function of the enabled feature set, which no `cfg` on +// the import itself can express. Partial builds only — `all-languages` +// is on by default and under `--all-features`, so the build CI gates on +// and the one a contributor runs still police every unused import. See +// `.claude/rules/testing.md`, "Why the import lint is off on a partial +// build". +#![cfg_attr(not(feature = "all-languages"), allow(unused_imports))] pub mod alterator; pub mod ast; pub mod c_declarator; diff --git a/big-code-analysis-ast/src/node.rs b/big-code-analysis-ast/src/node.rs index 63c8aeddb..23e37828f 100644 --- a/big-code-analysis-ast/src/node.rs +++ b/big-code-analysis-ast/src/node.rs @@ -1099,6 +1099,7 @@ mod tests { /// children was supposed to change, and this is what says so — /// node-by-node over a real tree, same order and same short-circuit, /// without hardcoding grammar `kind_id`s. + #[cfg(feature = "mozjs")] fn sibling_chain_has_sibling(node: OtherNode, id: u16) -> bool { node.parent().is_some_and(|parent| { let mut cur = parent.child(0); @@ -1138,6 +1139,7 @@ mod tests { assert_eq!(root.utf8_text(&code[..4]), None); } + #[cfg(feature = "mozjs")] #[test] fn has_sibling_matches_the_retired_sibling_chain() { // Arrow functions exercise the `check_if_arrow_func!` call site @@ -1211,6 +1213,7 @@ mod tests { /// count). This pins the no-duplicate-padding property: a desync /// between `child_count` and the cursor walk would surface here as /// extra trailing duplicates or a length mismatch. + #[cfg(feature = "mozjs")] #[test] fn children_matches_tree_sitter_child_walk() { // Mix of leaf nodes (no children), single-child wrappers, and @@ -1254,6 +1257,7 @@ mod tests { /// `parent`'s borrow and this would fail to compile. Binding the /// child to a variable that outlives the `&parent` reborrow inside /// the helper exercises the widened lifetime. + #[cfg(feature = "cpp")] #[test] fn child_by_field_name_outlives_self_borrow() { // `find_named_child` takes the parent by value, reborrows it @@ -1298,6 +1302,7 @@ mod tests { /// the wrapper through the public `CppParser` + `ParserTrait::root` /// path (rather than the in-module `Tree::new`) proves the accessor /// is the public seam that replaced the former `pub` `.0` field. + #[cfg(feature = "cpp")] #[test] fn as_tree_sitter_round_trips_wrapper_kind() { use crate::{CppParser, ParserTrait}; @@ -1325,6 +1330,7 @@ mod tests { /// document order (`child(0..child_count)`). [`Node::preorder`] must /// emit exactly this sequence of node ids — node first, then each /// child subtree left to right. + #[cfg(feature = "cpp")] fn ground_truth_preorder(node: OtherNode) -> Vec { let mut out = vec![node.id()]; for i in 0..node.child_count() as u32 { @@ -1335,6 +1341,7 @@ mod tests { out } + #[cfg(feature = "cpp")] #[test] fn preorder_matches_recursive_document_order() { // A nested construct (function holding a declaration and a call) @@ -1355,6 +1362,7 @@ mod tests { assert_eq!(actual[0], root.id(), "root must be yielded first"); } + #[cfg(feature = "cpp")] #[test] fn descendants_by_kind_collects_matching_subtree_nodes() { // `x` is declared once and used twice, so three `identifier` @@ -1400,6 +1408,7 @@ mod tests { /// child iterators are checked against it, so the contract is stated /// once — `children_with` exists to save an allocation, and a /// separate copy of this is how the two would come to disagree. + #[cfg(feature = "mozjs")] fn drain_checking_exact_size<'a>( mut iter: impl ExactSizeIterator>, child_count: usize, @@ -1431,6 +1440,16 @@ mod tests { } /// Ancestor ids yielded by `ancestors`, nearest first. + #[cfg(any( + feature = "c", + feature = "elixir", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "python", + feature = "ruby", + feature = "rust", + ))] fn ancestor_ids(ancestors: Ancestors<'_, '_>, node: &Node<'_>) -> Vec { ancestors.iter(node).map(|(a, _)| a.id()).collect() } @@ -1445,6 +1464,16 @@ mod tests { /// parent clause, `loc`'s declaration gate), JVM-family /// (`is_else_if` via the preceding `else` token), Python (the /// grandparent shape), and Elixir (`quote` templates). + #[cfg(all( + feature = "c", + feature = "elixir", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "python", + feature = "ruby", + feature = "rust", + ))] #[test] fn a_known_chain_answers_exactly_what_climbing_answers() { /// `must_nest` names kinds that have to appear *inside another @@ -1453,6 +1482,16 @@ mod tests { /// the nesting a row was added for would leave a large, /// clean-parsing tree that no longer exercises the shape, and /// the parity assertions would keep passing over it. + #[cfg(any( + feature = "c", + feature = "elixir", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "python", + feature = "ruby", + feature = "rust", + ))] fn assert_parity(label: &str, code: &[u8], must_nest: &[&str]) { let mut nested_seen = vec![false; must_nest.len()]; let visited = for_each_node_with_chain::(code, |node, chain| { @@ -1574,7 +1613,10 @@ mod tests { /// Seeding a real scan first is what makes it falsifiable: compared /// against zero these assertions would also pass with `record()` /// never wired up at all. - #[cfg(all(feature = "c", feature = "mozjs", feature = "python", feature = "rust"))] + // `python` and `rust` were stale: the body builds only `CCode` and + // `MozjsCode`, so naming them kept the test out of builds that could + // run it. + #[cfg(all(feature = "c", feature = "mozjs"))] #[test] fn the_converted_traversals_scan_a_tree_on_one_cursor() { let seed_tree = Tree::new::(b"int main() { int a; }"); @@ -1639,6 +1681,7 @@ mod tests { /// Checked through both `Ancestors` constructors: the chain and the /// climb reach the end by different code paths (`split_last` on an /// empty slice, versus `Node::parent` returning `None`). + #[cfg(feature = "c")] #[test] fn parent_grandparent_match_is_false_when_either_link_is_absent() { let tree = Tree::new::(b"int main() { int a; }"); @@ -1676,6 +1719,7 @@ mod tests { /// step were wrong. It also covers the reuse itself — one cursor /// drives every node's scan here, so a `reset` that failed to rewind /// would show as the second node inheriting the first's position. + #[cfg(feature = "mozjs")] #[test] fn children_with_yields_exactly_what_children_does() { let code = b"const o = { m: (a) => a + 1, n: function () {} }; foo(); ;"; @@ -1727,6 +1771,7 @@ mod tests { /// single-child wrapper (`expression_statement` over its expression, /// say) spans exactly what its child spans, so tightening either /// bound to `<` would reject a correct chain on most real input. + #[cfg(all(feature = "c", feature = "javascript", feature = "python"))] #[test] fn checked_accepts_the_chains_the_walkers_build() { let mut equal_span_pairs = 0; @@ -1765,6 +1810,7 @@ mod tests { /// /// Debug-gated because `debug_assert!` compiles out under /// `--release`, where `checked` degrades to `known` by design. + #[cfg(feature = "c")] #[test] #[cfg(debug_assertions)] #[should_panic(expected = "ancestor chain desynchronised")] @@ -1781,6 +1827,7 @@ mod tests { /// A dropped `truncate` leaves the previous subtree's path in place, /// so the next node up gets a `chain.last()` from a sibling subtree — /// disjoint from it in bytes. That is the containment half. + #[cfg(feature = "c")] #[test] #[cfg(debug_assertions)] #[should_panic(expected = "ancestor chain desynchronised")] @@ -1809,6 +1856,7 @@ mod tests { /// reclassify an arrow function rather than fail. Checked for every /// node against every kind the fixture contains, plus one that never /// occurs so the absent-sibling answer is covered too. + #[cfg(feature = "javascript")] #[test] fn has_sibling_agrees_between_known_and_climbing() { // Object-literal methods and an arrow bound to a property are @@ -1861,6 +1909,7 @@ mod tests { /// the fixture below is the guard, and it fails against an empty /// seed both here and through `Ancestors::checked`'s debug /// assertion. + #[cfg(feature = "mozjs")] #[test] fn act_on_node_hands_a_subtree_its_real_ancestry() { let code = b"var outer = function () { return 1; };\n"; @@ -1895,6 +1944,7 @@ mod tests { /// entry for `node`; a miss means the caller paired the two wrongly. /// Reporting `None` there would be a wrong answer dressed as a /// legitimate one, so the fallback re-asks the tree. + #[cfg(feature = "c")] #[test] fn previous_sibling_falls_back_on_a_chain_that_is_not_this_nodes() { let code = b"int main() { int a; int b; }"; @@ -1936,6 +1986,7 @@ mod tests { /// (`while`/`for`/`if` header, stopping at the enclosing block), so /// the fixture exercises both the counted case (the `for`-header /// declaration) and the stopped case (the block-scoped ones). + #[cfg(feature = "c")] #[test] fn count_specific_ancestors_agrees_between_known_and_climbing() { let code = diff --git a/big-code-analysis-ast/src/parser.rs b/big-code-analysis-ast/src/parser.rs index cd2638e4b..d5e30b990 100644 --- a/big-code-analysis-ast/src/parser.rs +++ b/big-code-analysis-ast/src/parser.rs @@ -260,10 +260,12 @@ mod tests { use crate::traits::ParserTrait; use std::path::PathBuf; + #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))] fn parse_python(source: &str) -> PythonParser { PythonParser::new(source.as_bytes().to_vec(), &PathBuf::from("t.py"), None) } + #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))] fn count_kind(source: &str, filter: &str) -> usize { count(&parse_python(source), &[filter.to_string()]).0 } @@ -273,6 +275,7 @@ mod tests { // not a numeric `kind_id` must match `node.kind()` exactly, not via // substring containment. + #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))] #[test] fn get_filters_exact_match_hits_named_kind() { // Python's `if`/`elif`/`else` clauses each appear as their own @@ -282,6 +285,7 @@ mod tests { assert_eq!(count_kind(src, "if_statement"), 1); } + #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))] #[test] fn get_filters_no_substring_match() { // Filter `expression` must not match `expression_statement`, @@ -299,6 +303,7 @@ mod tests { assert_eq!(count_kind(src, "assignment"), 2); } + #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))] #[test] fn get_filters_unknown_kind_returns_empty() { // A filter that names no real node kind matches nothing — the @@ -309,6 +314,7 @@ mod tests { assert_eq!(count_kind(src, "definitely_not_a_python_kind"), 0); } + #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "python"))] #[test] fn get_filters_empty_request_matches_every_node() { // Requesting nothing means "match everything": `filters` falls diff --git a/big-code-analysis-ast/src/preproc_tests.rs b/big-code-analysis-ast/src/preproc_tests.rs index 52dd0aa4e..203512da0 100644 --- a/big-code-analysis-ast/src/preproc_tests.rs +++ b/big-code-analysis-ast/src/preproc_tests.rs @@ -20,6 +20,7 @@ use super::*; +#[cfg(any(feature = "c-family-helpers", feature = "cpp"))] fn parse(source: &str) -> PreprocParser { PreprocParser::new(source.as_bytes().to_vec(), &PathBuf::from("test.h"), None) } @@ -28,6 +29,7 @@ fn parse(source: &str) -> PreprocParser { /// implementations called `unwrap()` on `position`/`rposition` of the /// trimmed slice, which returns `None` for an all-whitespace or empty /// payload. +#[cfg(feature = "c-family-helpers")] #[test] fn preprocess_empty_include_does_not_panic() { let parser = parse("#include \"\"\n"); @@ -42,6 +44,7 @@ fn preprocess_empty_include_does_not_panic() { /// Whitespace-only include strings (`#include " "`) must not panic — /// `position` returns `None` because no non-whitespace byte exists. +#[cfg(feature = "c-family-helpers")] #[test] fn preprocess_whitespace_only_include_does_not_panic() { let parser = parse("#include \" \"\n"); @@ -56,6 +59,7 @@ fn preprocess_whitespace_only_include_does_not_panic() { /// A well-formed include is still recorded with surrounding whitespace /// stripped. +#[cfg(feature = "c-family-helpers")] #[test] fn preprocess_valid_include_is_recorded() { let parser = parse("#include \" foo.h \"\n"); @@ -69,6 +73,7 @@ fn preprocess_valid_include_is_recorded() { } /// `#define` of a normal identifier records the macro name. +#[cfg(feature = "c-family-helpers")] #[test] fn preprocess_define_records_macro() { let parser = parse("#define FOO 1\n"); @@ -81,6 +86,7 @@ fn preprocess_define_records_macro() { assert!(pf.macros.contains("FOO")); } +#[cfg(feature = "c-family-helpers")] fn macros_of(source: &str) -> HashSet { let parser = parse(source); let mut results = PreprocResults::default(); @@ -97,6 +103,7 @@ fn macros_of(source: &str) -> HashSet { /// FOO from the macro set — the pre-fix code shared a `Define | Undef` /// arm that inserted the identifier for both, leaving `#undef FOO` /// recording FOO as *defined*. +#[cfg(feature = "c-family-helpers")] #[test] fn preprocess_undef_removes_defined_macro() { let macros = macros_of("#define FOO 1\n#undef FOO\n"); @@ -108,6 +115,7 @@ fn preprocess_undef_removes_defined_macro() { /// `#undef` of a macro that was never defined is a no-op (and must not /// leave the name recorded as defined). +#[cfg(feature = "c-family-helpers")] #[test] fn preprocess_undef_of_never_defined_is_noop() { let macros = macros_of("#undef NEVER_DEFINED\n"); @@ -123,6 +131,7 @@ fn preprocess_undef_of_never_defined_is_noop() { /// last) so a missing or reversed sort flips the result — a `define` /// … `undef` … `define` sequence ends on a `define` either way and would /// not exercise the ordering at all. +#[cfg(feature = "c-family-helpers")] #[test] fn preprocess_define_after_undef_reintroduces_in_source_order() { let macros = macros_of("#undef FOO\n#define FOO 1\n"); @@ -133,6 +142,7 @@ fn preprocess_define_after_undef_reintroduces_in_source_order() { } /// `#undef` removes only the named macro; unrelated defines survive. +#[cfg(feature = "c-family-helpers")] #[test] fn preprocess_undef_leaves_other_macros() { let macros = macros_of("#define FOO 1\n#define BAR 2\n#undef FOO\n"); @@ -145,6 +155,7 @@ fn preprocess_undef_leaves_other_macros() { /// they never pollute the recorded macro set, while an ordinary macro on /// an adjacent line is still recorded. Pins the `is_specials` guard that /// the #736 refactor moved out of the inline walk and into the helper. +#[cfg(feature = "c-family-helpers")] #[test] fn preprocess_define_of_special_token_is_skipped() { let macros = macros_of("#define size_t unsigned\n#define APP_FLAG 1\n"); @@ -914,6 +925,7 @@ fn parsing_a_cpp_file_never_owns_the_macro_set() { /// End-to-end: a truncated `#include "` with no closing quote must not /// panic the preprocessor pass (issue #432). The file entry is still /// inserted with no recorded include. +#[cfg(feature = "c-family-helpers")] #[test] fn preprocess_truncated_include_does_not_panic() { let parser = parse("#include \"\n"); diff --git a/docs/development/lessons_learned.md b/docs/development/lessons_learned.md index 072cd741a..a0f1cd737 100644 --- a/docs/development/lessons_learned.md +++ b/docs/development/lessons_learned.md @@ -370,7 +370,12 @@ derive at least one assertion from an external source — the metric specification, a hand-computed value, or a reference implementation in another language module — never from the current code's output. Keep at least one hand-derived test per metric per language as an external -anchor; snapshots are scaffolding around it, not a substitute. +anchor; snapshots are scaffolding around it, not a substitute. The same +holds wherever the expected value is *produced* rather than asserted: a +gate checking generated artifacts against their generator, or a triage +computed from the model it is meant to audit, can only confirm that +model. Name the oracle outside it — prior human work, the previous +revision, a fresh-context reviewer — before trusting a green result. `AGENTS.md` carries the enforceable form of this ("Anchor every `insta::assert_json_snapshot!` call", with the three acceptable anchor @@ -396,6 +401,22 @@ the prose had drifted, and the mismatch was invisible until the bare snapshot gained an `assert_eq!(…, 5.0)` immediately above it. A comment can silently desync from reality; a literal value in source cannot. +**A generated gate checked against its own generator** (#1478, PR #1479). +`check-test-lang-gates.py` derived ~3,200 per-language `#[cfg]` markers, +and its `over_gated` check compared each against that same derivation — +so it could catch a hand edit and nothing else. Comparison rules that +stopped at the first `(` of an `assert_eq!`, or read a variant's +`.extensions()` receiver as a parse, gated 34 grammar-free tests onto +languages they never touch; the check stayed green, and a triage run to +confirm the branch ("3,001 lost builds, 0 suspicious") used the same +derivation and certified every one. The defects were found by oracles +outside the model: the 167 gates humans had written earlier, which the +generator had to reproduce before generating anything; a comparison +against the previous revision, which caught two parity sweeps gated on +four of the twenty-three language features; and a fresh-context review. +That comparison was also the one check a PR label switched off — on the +very PR where the derivation's bugs were live. + For grammar bumps, run `cargo insta test --accept` per file only after spot-checking that the diff is metric values shifting in a direction consistent with the grammar change, not structural changes hiding a diff --git a/src/c_family_space_names_tests.rs b/src/c_family_space_names_tests.rs index 709cf86b8..2f433ea8d 100644 --- a/src/c_family_space_names_tests.rs +++ b/src/c_family_space_names_tests.rs @@ -35,6 +35,7 @@ use crate::{FuncSpace, LANG, MetricsOptions, SpaceKind}; /// silently if the walk stopped one link too early. /// /// The last row expects no name at all; its comment says why. +#[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] const SHARED_SHAPES: &[(&str, Option<&str>)] = &[ ("int (*fp(int a, int b))(int c) { return 0; }", Some("fp")), ( @@ -89,6 +90,7 @@ const SHARED_SHAPES: &[(&str, Option<&str>)] = &[ /// conversion operator's declarator field is the type it converts /// *to*, so [`super::innermost_declarator`] deliberately cuts the /// chain there and returns `None`. +#[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] const CPP_ONLY_SHAPES: &[(&str, Option<&str>)] = &[ ("struct S { ~S() { } };", Some("~S")), ("void Foo::bar(int a) { }", Some("Foo::bar")), @@ -136,8 +138,10 @@ const CPP_ONLY_SHAPES: &[(&str, Option<&str>)] = &[ /// line, so the asserted span is `(2, 2)` — a value a /// default-constructed or off-by-one span does not also satisfy, /// unlike the `(1, 1)` a bare one-line fixture would produce. +#[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] const FIXTURE_LINE: usize = 2; +#[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] fn pad(source: &str) -> String { format!("// leading\n{source}\n// trailing\n") } @@ -150,6 +154,7 @@ fn pad(source: &str) -> String { /// *exactly one* function space, which is `get_space_kind` and /// `is_func_space` agreeing with the name — `.claude/rules/ /// grammar-dispatch.md` §6. +#[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] fn function_spaces(space: &FuncSpace, found: &mut Vec<(Option, usize, usize)>) { if space.kind == SpaceKind::Function { found.push((space.name.clone(), space.start_line, space.end_line)); @@ -159,6 +164,7 @@ fn function_spaces(space: &FuncSpace, found: &mut Vec<(Option, usize, us } } +#[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] fn check(lang: LANG, shapes: &[(&str, Option<&str>)], failures: &mut Vec) { for (source, expected) in shapes { let root = space_verbatim(lang, pad(source).as_bytes(), MetricsOptions::default()); @@ -179,6 +185,7 @@ fn check(lang: LANG, shapes: &[(&str, Option<&str>)], failures: &mut Vec /// Shared so the failure formatting exists once: it is by /// construction unreachable while the suite is green, so a second /// copy is coverage the tests can never earn. +#[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] #[track_caller] fn assert_all_matched(failures: &[String], checked: usize, what: &str) { assert!( @@ -198,6 +205,7 @@ fn assert_all_matched(failures: &[String], checked: usize, what: &str) { /// declarator wraps for the three macro rows (#1213). "Same walk" /// rather than "same node" is why this is not named for the /// innermost declarator alone. +#[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] #[test] fn the_declarator_walk_names_the_function_space() { let mut failures = Vec::new(); @@ -289,6 +297,7 @@ fn the_table_reports_a_name_that_does_not_match() { /// [`super::innermost_declarator`], which measured this exact shape /// as one of the two corpus spaces #1208 un-named. This pins what /// the walk does there, not a claim that it is the right answer. +#[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] #[test] fn a_macro_where_an_attribute_belongs_divides_the_grammars() { const SOURCE: &str = "int *f() TF_ATTRIBUTE_NOINLINE { return 0; }"; @@ -333,6 +342,7 @@ fn a_macro_where_an_attribute_belongs_divides_the_grammars() { /// its own, and every rule the walk follows is void inside an /// `ERROR`. Teach the walk to unwrap one and this is a row to /// update, not a row to delete. +#[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] #[test] fn a_parenthesised_macro_takes_the_name_of_the_function_it_annotates() { const SOURCE: &str = "int *f() TF_LOCKS_EXCLUDED(mu_) { return 0; }"; diff --git a/src/from_path_error.rs b/src/from_path_error.rs index 68cbe213c..2f9d604d4 100644 --- a/src/from_path_error.rs +++ b/src/from_path_error.rs @@ -85,6 +85,7 @@ mod tests { // `source` chaining contract, which is what `?`-propagating callers // and `anyhow`-style reporters rely on. + #[cfg(feature = "rust")] #[test] fn from_path_error_display_covers_every_variant() { let io = FromPathError::Io(IoError::new(ErrorKind::PermissionDenied, "denied")); @@ -134,6 +135,7 @@ mod tests { } } + #[cfg(feature = "cpp")] #[test] fn metrics_error_converts_into_parse_variant() { let converted: FromPathError = MetricsError::LanguageDisabled(LANG::Cpp).into(); diff --git a/src/function_tests.rs b/src/function_tests.rs index f728c8941..73cbf3eaa 100644 --- a/src/function_tests.rs +++ b/src/function_tests.rs @@ -194,6 +194,7 @@ fn dump_span_ansi_layout_error_branch() { /// trips its assertion here rather than only in the web crate's /// endpoint tests (fully under `make chain-audit`; in a plain debug /// build, for the slips the `O(1)` guard sees — see #1122). +#[cfg(feature = "javascript")] #[test] fn js_function_spans_name_expressions_from_their_binding() { use crate::langs::JavascriptParser; diff --git a/src/lib.rs b/src/lib.rs index 4d50d3013..0b5702415 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,7 +123,6 @@ // root `Cargo.toml` for why this is a per-root attribute and not a // Cargo lint (#1227). #![cfg_attr(not(test), warn(clippy::unwrap_used))] - // The parse and classification layer lives in `big-code-analysis-ast` // (#1376). Its public names — the generated token enums, the `*Code` / // `*Parser` tags, `Node`, `Ancestors`, `Checker`, `Getter`, the language @@ -131,6 +130,16 @@ // through `use crate::*`, so the whole crate root is glob-imported here // at `pub(crate)`. Only the explicit `pub use` lines further down widen // the published surface. + +// The import half of the `allow(dead_code)` carve-out above, and for +// the same reason: per-language test gating (#1472) makes "is this +// import live" a function of the enabled feature set, which no `cfg` on +// the import itself can express. Partial builds only — `all-languages` +// is on by default and under `--all-features`, so the build CI gates on +// and the one a contributor runs still police every unused import. See +// `.claude/rules/testing.md`, "Why the import lint is off on a partial +// build". +#![cfg_attr(not(feature = "all-languages"), allow(unused_imports))] #[doc(hidden)] pub(crate) use big_code_analysis_ast::*; diff --git a/src/metrics/abc.rs b/src/metrics/abc.rs index e9f0b6ac4..0a2d5e039 100644 --- a/src/metrics/abc.rs +++ b/src/metrics/abc.rs @@ -454,10 +454,28 @@ implement_metric_trait!(Abc, PreprocCode, CcommentCode); clippy::too_many_lines )] mod tests { + #[cfg(feature = "csharp")] + use crate::test_support::assert_csharp_fixture_spells; use crate::test_support::{ - assert_csharp_fixture_spells, assert_fixture_spells, ast_has_kind_id, - check_func_space_only_shim, check_metrics_only_shim, child_space, metrics_verbatim, + assert_fixture_spells, ast_has_kind_id, check_func_space_only_shim, + check_metrics_only_shim, child_space, metrics_verbatim, }; + // Hand-written, not derived. A trait is used through its methods, so + // nothing in a test body names `ParserTrait` and + // `check-test-lang-gates.py` derives no gate for an import at all, + // so an ungated one stays compiled into builds that never call + // `Parser::new`. These seven are the languages that do; a new + // `XParser::new` test in this module needs its feature added here, + // and the leg that lacks it says so as an unused import. + #[cfg(any( + feature = "csharp", + feature = "groovy", + feature = "kotlin", + feature = "php", + feature = "ruby", + feature = "rust", + feature = "typescript", + ))] use crate::traits::ParserTrait; use super::*; @@ -475,6 +493,17 @@ mod tests { /// a reference value; this can. Restricted rather than /// `MetricsOptions::default()` per `metrics_verbatim`'s own doc /// (#1127). + #[cfg(any( + feature = "c", + feature = "cpp", + feature = "go", + feature = "javascript", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "typescript", + ))] fn abc_conditions(lang: LANG, src: &str) -> u64 { metrics_verbatim( lang, @@ -489,6 +518,15 @@ mod tests { // named above there. `conditions()` is that one space's own count, // so it pairs with `cyclomatic()` and never `cyclomatic_sum()`, // which folds in a base of 1 per nested space. + #[cfg(any( + feature = "cpp", + feature = "csharp", + feature = "groovy", + feature = "java", + feature = "perl", + feature = "php", + feature = "typescript", + ))] fn assert_deepest_conditions_match_cyclomatic(space: &crate::FuncSpace, expected: u64) { let mut deepest = space; while let Some(child) = deepest.spaces.last() { @@ -505,6 +543,7 @@ mod tests { // and so inspects exactly one member, which is the shape #1383's // over-count hid behind — a merged root total cannot tell a // relational method scoring 2 from the constant control scoring 2. + #[cfg(any(feature = "csharp", feature = "elixir"))] fn assert_every_member_scores( container: &crate::FuncSpace, members: usize, @@ -542,6 +581,16 @@ mod tests { // decision count — an `else` arm, or a comparison nested inside // another comparison, is an ABC condition with no cyclomatic // decision behind it (#1421). + #[cfg(any( + feature = "csharp", + feature = "elixir", + feature = "groovy", + feature = "java", + feature = "kotlin", + feature = "python", + feature = "ruby", + feature = "rust", + ))] #[track_caller] fn assert_members_score(container: &crate::FuncSpace, expected: &[(&str, u64, u64)]) { assert_eq!( @@ -580,6 +629,7 @@ mod tests { // The `EQ` arm of `java_count_token_assignment`: a plain `=` counts // unless `java_eq_initializes_final_binding` finds it initialising a // `final` binding, whether the declaration is a local or a field. + #[cfg(feature = "java")] #[test] fn java_eq_arm_counts_outside_final_declarations() { check_metrics::( @@ -593,6 +643,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_eq_arm_skips_final_initializers() { check_metrics::( @@ -612,6 +663,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_final_initializer_does_not_suppress_the_assignments_inside_it() { // The sentinel stack the structural predicate replaced stayed @@ -652,6 +704,7 @@ mod tests { } // Constant declarations are not counted as assignments + #[cfg(feature = "java")] #[test] fn java_constant_declarations() { check_metrics::( @@ -705,6 +758,7 @@ mod tests { // According to this definition, boolean expressions that are evaluated to make a decision are considered as conditions // Variables, method invocations and true or false values used inside // variable declarations and assignment expressions are not counted as conditions + #[cfg(feature = "java")] #[test] fn java_declarations_with_conditions() { check_metrics::( @@ -754,6 +808,7 @@ mod tests { } // Conditions can be found in assignment expressions + #[cfg(feature = "java")] #[test] fn java_assignments_with_conditions() { check_metrics::( @@ -803,6 +858,7 @@ mod tests { } // Conditions can be found in method arguments + #[cfg(feature = "java")] #[test] fn java_methods_arguments_with_conditions() { check_metrics::( @@ -848,6 +904,7 @@ mod tests { // "A unary conditional expression is an implicit condition that uses no relational operators." // Source: Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report. // https://www.softwarerenovation.com/Articles.aspx (page 5) + #[cfg(feature = "java")] #[test] fn java_if_single_conditions() { check_metrics::( @@ -904,6 +961,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_if_multiple_conditions() { check_metrics::( @@ -950,6 +1008,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_while_and_do_while_conditions() { check_metrics::( @@ -996,6 +1055,7 @@ mod tests { // According to this definition, unary conditional expressions are counted also in function return values. // Source: https://dx42.github.io/gmetrics/metrics/AbcMetric.html // Examples: https://github.com/dx42/gmetrics/blob/master/src/test/groovy/org/gmetrics/metric/abc/AbcMetric_MethodTest.groovy + #[cfg(feature = "java")] #[test] fn java_return_with_conditions() { check_metrics::( @@ -1048,6 +1108,7 @@ mod tests { // Variables, method invocations, and true or false values // inside return statements are not counted as conditions + #[cfg(feature = "java")] #[test] fn java_return_without_conditions() { check_metrics::( @@ -1099,6 +1160,7 @@ mod tests { // Variables, method invocations, and true or false values // in lambda expression return values are not counted as conditions + #[cfg(feature = "java")] #[test] fn java_lambda_expressions_return_with_conditions() { check_metrics::( @@ -1144,6 +1206,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_for_with_variable_declaration() { check_metrics::( @@ -1183,6 +1246,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_for_without_variable_declaration() { check_metrics::( @@ -1238,6 +1302,7 @@ mod tests { // unary conditions that are present. Java and Groovy were the only // two impls disagreeing, and the field-addressed walker now reports // zero the way the C family, the JS family, PHP, C# and Go all do. + #[cfg(feature = "java")] #[test] fn java_empty_for_condition_counts_nothing() { check_metrics::( @@ -1267,6 +1332,7 @@ mod tests { // the header shifted every index and the condition went unread — // the same failure #1181 removed from `java_walk_ternary`. Reading // the `condition` field cannot shift. + #[cfg(feature = "java")] #[test] fn java_for_condition_survives_a_header_comment() { check_metrics::( @@ -1286,6 +1352,7 @@ mod tests { // Variables, method invocations, and true or false values // in ternary expression return values are not counted as conditions + #[cfg(feature = "java")] #[test] fn java_ternary_conditions() { check_metrics::( @@ -1328,6 +1395,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_assignments_only() { check_metrics::( @@ -1363,6 +1431,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_commands_only() { check_metrics::( @@ -1397,6 +1466,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_control_flow_counts_conditions() { // Regression for #696: Bash control-flow branches are ABC @@ -1430,6 +1500,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_conditions_mix() { // Exercises every condition path: `==` and `!=` inside `[[ ]]`, @@ -1484,6 +1555,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_redirection_is_not_a_condition() { // `>` and `<` spell an I/O redirection as well as a comparison, and @@ -1505,6 +1577,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_comparison_inside_an_arithmetic_or_test_context_is_a_condition() { // The positive control for the gate above: the same tokens under a @@ -1521,6 +1594,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_arithmetic_ternary_is_a_condition() { // The ABC half of #1268. Cyclomatic and cognitive both count Bash's @@ -1537,6 +1611,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_magnitude() { // Combined assignments + branches + conditions. The single `if` @@ -1578,6 +1653,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_malformed_parenthesized_no_panic() { check_metrics::("class A { void m() { if (( }) }", "foo.java", |metric| { @@ -1591,6 +1667,7 @@ mod tests { }); } + #[cfg(feature = "java")] #[test] fn java_bool_returning_terminal_kinds_count() { // Companion to `csharp_bool_returning_terminal_kinds_count` @@ -1646,6 +1723,7 @@ mod tests { // tokens under separate `type_arguments`. Pre-fix this file scored // 4 conditions — two per `type_parameters` bracket pair; expected 0, // the file contains no conditional construct at all. + #[cfg(feature = "java")] #[test] fn java_generic_declarations_are_not_conditions() { check_metrics::( @@ -1681,6 +1759,7 @@ mod tests { // ternary is deliberate — with one of each, aiming the gate at the // wrong one of the two productions still totals 2 and the test // cannot see the difference. + #[cfg(feature = "java")] #[test] fn java_generic_wildcard_is_not_a_condition() { check_metrics::( @@ -1702,6 +1781,7 @@ mod tests { // method's own space the ABC condition count must equal the // cyclomatic decision count (`cyclomatic()` minus the per-space // base of 1). Both are 2, one per `if`. + #[cfg(feature = "java")] #[test] fn java_comparison_operators_still_count_alongside_generics() { check_func_space::( @@ -1717,6 +1797,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_constructor_delegation_is_a_branch() { // Regression for #1279: `super(…)` / `this(…)` parse as @@ -1737,6 +1818,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_constructor_delegation_does_not_double_count_arguments() { // The delegation node does not wrap a `method_invocation` for the @@ -1754,6 +1836,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_constructor_initializer_is_a_branch() { // C# spells the same delegation as a `constructor_initializer` @@ -1771,6 +1854,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_primary_constructor_base_call_is_a_branch() { // The C# 12 primary-constructor superclass call invokes the base @@ -1841,6 +1925,7 @@ mod tests { }); } + #[cfg(feature = "csharp")] #[test] fn csharp_base_list_gate_excludes_other_argument_lists() { // The `base_list` parent gate on the `ArgumentList` arm is @@ -1907,6 +1992,7 @@ mod tests { }); } + #[cfg(feature = "csharp")] #[test] fn csharp_base_call_and_constructor_initializer_do_not_double_count() { // A class can spell *both* delegations at once: the primary @@ -1945,6 +2031,7 @@ mod tests { }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_constructor_delegation_is_a_branch() { // Kotlin's secondary-constructor delegation is a @@ -1962,6 +2049,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_primary_constructor_superclass_call_is_a_branch() { // Kotlin's *primary*-constructor superclass call is a @@ -1993,6 +2081,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_annotation_arguments_are_not_a_branch() { // The parent gate on that arm is load-bearing, not decoration: @@ -2046,6 +2135,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_constructor_delegation_is_a_branch() { // Groovy already counted this shape before #1279; the assertion @@ -2084,6 +2174,7 @@ mod tests { // reason `assert_kotlin_class_members` couples its pair: a branch // total added without the census reads as coverage and is not. + #[cfg(any(feature = "groovy", feature = "java", feature = "kotlin"))] fn assert_enum_branches( src: &str, path: &str, @@ -2096,6 +2187,7 @@ mod tests { }); } + #[cfg(feature = "java")] #[test] fn java_enum_constant_with_arguments_is_a_branch() { // `@SuppressWarnings("x") B` is the discriminating case for the @@ -2136,6 +2228,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_enum_entry_with_arguments_is_a_branch() { // Kotlin needs a defaulted primary-constructor parameter to spell @@ -2162,6 +2255,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_enum_constant_with_arguments_is_a_branch() { // Groovy's enum has no annotated-constant case to cover: the @@ -2189,6 +2283,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_no_abc() { // Comment-only file has no executable code → all-zero ABC. @@ -2203,6 +2298,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_single_assignment() { // `int x = 1` is a local-variable declaration whose `=` counts @@ -2214,6 +2310,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_assignments() { check_metrics::( @@ -2237,6 +2334,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_branches() { check_metrics::( @@ -2253,6 +2351,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_conditions_in_if() { check_metrics::( @@ -2269,6 +2368,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_branches_with_juxt_call() { // Groovy's parens-less call form `println foo` must be counted @@ -2286,6 +2386,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_try_catch_conditions() { // Each `try` and `catch` keyword token contributes +1 to @@ -2306,6 +2407,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_ternary_conditions() { check_metrics::( @@ -2320,6 +2422,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_constant_excluded_from_assignments() { // `final` declarations are not counted as assignments @@ -2338,6 +2441,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_malformed_parenthesized_no_panic() { // Regression: malformed Groovy input must not panic the ABC @@ -2351,6 +2455,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_bool_returning_terminal_kinds_count() { // Companion to `csharp_bool_returning_terminal_kinds_count` @@ -2393,6 +2498,7 @@ mod tests { // The class-level `type_parameters` and its bound's nested // `type_arguments` mirror the Java fixture; pre-fix this file scored // 2 conditions, expected 0. + #[cfg(feature = "groovy")] #[test] fn groovy_generic_declarations_are_not_conditions() { check_metrics::( @@ -2412,6 +2518,7 @@ mod tests { // including its choice of a non-zero expected total and its // two-wildcards-one-ternary shape; the dekobon grammar emits the // same `wildcard` node. Pre-fix: 4. + #[cfg(feature = "groovy")] #[test] fn groovy_generic_wildcard_is_not_a_condition() { check_metrics::( @@ -2440,6 +2547,7 @@ mod tests { // The body carries a ternary over a comparison for the same // non-vacuity reason as the wildcard tests: expected 2, pre-fix 4 // (the `` bracket pair), 0 if the fixture stops parsing. + #[cfg(feature = "groovy")] #[test] fn groovy_method_type_parameters_are_not_conditions() { check_metrics::( @@ -2457,6 +2565,7 @@ mod tests { // `java_comparison_operators_still_count_alongside_generics`: the // narrowed arm keeps counting real comparisons, pinned against the // cyclomatic decision count on the method's own space (§8). + #[cfg(feature = "groovy")] #[test] fn groovy_elvis_counts_one_condition_per_token() { // `a ?: c` is a short-circuit decision Groovy cyclomatic already @@ -2478,6 +2587,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_comparison_operators_still_count_alongside_generics() { check_func_space::( @@ -2493,6 +2603,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_if_multiple_conditions() { // Mirrors `java_if_multiple_conditions`: `&&` / `||` chains @@ -2519,6 +2630,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_while_and_do_while_conditions() { // Covers the WhileStatement and DoStatement arms in @@ -2546,6 +2658,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_if_while_boolean_literal_condition() { // Regression for the Groovy half of #371-class bugs: the @@ -2582,6 +2695,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_return_unary_boolean_literal() { // Companion to `groovy_if_while_boolean_literal_condition`: @@ -2618,6 +2732,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_short_circuit_with_boolean_literal_operand() { // Companion to `groovy_if_while_boolean_literal_condition`: @@ -2655,6 +2770,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_methods_arguments_with_conditions() { // Mirror of `java_methods_arguments_with_conditions`: a @@ -2681,6 +2797,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_return_with_conditions() { // Mirror of `java_return_with_conditions`: a parenthesised @@ -2706,6 +2823,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_for_with_variable_declaration() { // Classical `for (int i = 0; cond; i++)` form. The init @@ -2736,6 +2854,7 @@ mod tests { /// contributes there, so it stayed uncovered. A bare-identifier /// condition has no comparison token, so the count can only come /// from the walker. + #[cfg(feature = "groovy")] #[test] fn groovy_for_with_bare_identifier_condition() { check_metrics::( @@ -2760,6 +2879,7 @@ mod tests { /// path — the condition moved from child(4) to child(3) — and the /// pair is kept as a shape guard now that the walker reads the /// `condition` field and cannot see the difference. + #[cfg(feature = "groovy")] #[test] fn groovy_for_with_empty_initializer_counts_the_condition() { check_metrics::( @@ -2784,6 +2904,7 @@ mod tests { /// child(4) as a vacuously-true condition, so `for (;;)` scored /// one. An omitted test is not a decision, and every other impl /// scores it zero. See `java_empty_for_condition_counts_nothing`. + #[cfg(feature = "groovy")] #[test] fn groovy_empty_for_condition_counts_nothing() { check_metrics::("void f() { for (;;) { break } }", "foo.groovy", |metric| { @@ -2803,6 +2924,7 @@ mod tests { /// The cascade's other defect, shared with Java: a comment in the /// header shifted every child index, so the condition went unread. /// Reading the `condition` field cannot shift. + #[cfg(feature = "groovy")] #[test] fn groovy_for_condition_survives_a_header_comment() { check_metrics::( @@ -2823,6 +2945,7 @@ mod tests { /// `!`-prefixed one through `csharp_inspect_container`. Every other /// C# `for` test uses a comparison (`i < n`), which the `LT` token /// arm counts without entering the walker. + #[cfg(feature = "csharp")] #[test] fn csharp_for_with_negated_condition() { check_metrics::( @@ -2843,6 +2966,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_eq_arm_counts_outside_final_declarations() { // Bare reassignment of an already-declared variable: the `=` @@ -2861,6 +2985,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_final_field_initializer_does_not_suppress_the_closure_body() { // The Groovy spelling of @@ -2883,6 +3008,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_final_local_is_an_error_at_the_pinned_grammar() { // `groovy_eq_initializes_final_binding` lists @@ -2913,6 +3039,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_const_initializer_shapes() { // `csharp_eq_initializes_const_binding` reads the `const` @@ -2937,6 +3064,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_constant_declarations() { check_metrics::( @@ -2958,6 +3086,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_declarations_with_conditions() { check_metrics::( @@ -2972,6 +3101,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_assignments_with_conditions() { check_metrics::( @@ -2992,6 +3122,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_methods_arguments_with_conditions() { check_metrics::( @@ -3006,6 +3137,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_if_single_conditions() { check_metrics::( @@ -3021,6 +3153,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_if_multiple_conditions() { check_metrics::( @@ -3035,6 +3168,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_while_and_do_while_conditions() { check_metrics::( @@ -3049,6 +3183,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_return_with_conditions() { check_metrics::( @@ -3072,6 +3207,7 @@ mod tests { // gated `SwitchExpressionArm` arm is what lifts this from 0 to 2. The // bare `_ =>` discard arm is excluded (the `default:` analogue), // mirroring the cyclomatic gate (lesson 11). + #[cfg(feature = "csharp")] #[test] fn csharp_switch_expression_arm_counts_condition() { check_metrics::( @@ -3096,6 +3232,7 @@ mod tests { // asserted in each callback rather than compared across closures; the // matching constant is what enforces parity. This guards against the // C# fix drifting away from the Java arrow-case treatment. + #[cfg(all(feature = "csharp", feature = "java"))] #[test] fn csharp_java_switch_arm_abc_parity() { // C# switch expression: two arms, no fallback → 2 conditions. @@ -3137,6 +3274,7 @@ mod tests { // policy). The cyclomatic side is pinned separately in // `java_csharp_cpp_switch_default_cyclomatic_parity` below, where // the per-space `cyclomatic()` decision count is isolated. + #[cfg(feature = "java")] #[test] fn java_switch_default_not_a_condition() { // Classic statement `default:`. @@ -3168,6 +3306,7 @@ mod tests { // cases → 2), so it would catch a fix that over-eagerly dropped a // real case (e.g. treating the trailing case as a fallthrough). // expected: case 1 (+1) + case 2 (+1) = 2. + #[cfg(feature = "java")] #[test] fn java_switch_without_default_counts_all_cases() { check_metrics::( @@ -3182,6 +3321,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_switch_default_not_a_condition() { check_metrics::( @@ -3223,6 +3363,7 @@ mod tests { /// assertion below satisfied and the construct under test gone; the /// anchor's count of 7 — six arms plus the one jump — fails by name /// instead. + #[cfg(feature = "csharp")] #[test] fn csharp_goto_case_is_not_a_condition() { let src = "class A { @@ -3253,6 +3394,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_switch_default_not_a_condition() { // C++ (and plain C, which shares this grammar) already excluded @@ -3266,6 +3408,7 @@ mod tests { ); } + #[cfg(feature = "objc")] #[test] fn objc_abc() { // ObjC ABC reuses the C/C++ walker with two additions: a message @@ -3302,6 +3445,7 @@ mod tests { ); } + #[cfg(feature = "objc")] #[test] fn objc_abc_conditions() { // Exercises the condition-slot arms shared with C/C++: a `while` @@ -3331,6 +3475,7 @@ mod tests { ); } + #[cfg(feature = "objc")] #[test] fn objc_abc_message_send_unary_condition() { // A negated boolean passed as a message-send argument is a unary @@ -3354,6 +3499,7 @@ mod tests { ); } + #[cfg(feature = "objc")] #[test] fn objc_message_send_is_a_bool_terminal_in_condition_slots() { // `[obj ok]` is Objective-C's call, so in a condition slot it is @@ -3398,6 +3544,7 @@ mod tests { assert!(cases.iter().any(|&(_, c, _)| c == 2)); } + #[cfg(feature = "objc")] #[test] fn objc_message_send_condition_agrees_with_c_call() { // The intra-ObjC parity C++ cannot express: a message send and a @@ -3420,6 +3567,7 @@ mod tests { } } + #[cfg(feature = "groovy")] #[test] fn groovy_switch_default_not_a_condition() { check_metrics::( @@ -3433,6 +3581,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn js_switch_default_not_a_condition() { check_metrics::( @@ -3444,6 +3593,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn ts_switch_default_not_a_condition() { check_metrics::( @@ -3462,6 +3612,7 @@ mod tests { // (the `default` excluded). `check_metrics` takes a non-capturing // `fn` pointer, so the shared expected value is asserted in each // callback; the matching constant is what enforces parity. + #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))] #[test] fn java_csharp_cpp_switch_default_abc_parity() { check_metrics::( @@ -3497,6 +3648,7 @@ mod tests { // `conditions()` for the same switch. Both must be 2 — the two case // arms — with the `default` excluded from each. Revert-verified: pre- // #469 ABC `conditions()` was 3 here while cyclomatic stayed at 2. + #[cfg(all(feature = "cpp", feature = "csharp", feature = "java"))] #[test] fn java_csharp_cpp_switch_default_cyclomatic_parity() { check_func_space::( @@ -3533,6 +3685,7 @@ mod tests { // `default` excluded. Revert-verified: re-adding `DefaultStatement` to // the PHP ABC condition arm makes `conditions()` 3 here while cyclomatic // stays at 2, failing the invariant. + #[cfg(feature = "php")] #[test] fn php_switch_default_not_a_condition() { check_func_space::( @@ -3555,6 +3708,7 @@ mod tests { // (`cyclomatic() - 1`) — both 2 for the two non-default match arms. // Revert-verified: re-adding `MatchDefaultExpression` to the PHP ABC // condition arm makes `conditions()` 3 here while cyclomatic stays at 2. + #[cfg(feature = "php")] #[test] fn php_match_default_not_a_condition() { check_func_space::( @@ -3571,6 +3725,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_if_bare_identifier_condition() { check_metrics::( @@ -3592,6 +3747,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_while_bare_identifier_condition() { check_metrics::( @@ -3610,6 +3766,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_do_while_bare_identifier_condition() { check_metrics::( @@ -3629,6 +3786,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_if_unary_not_condition() { // Two cases share one test: @@ -3669,6 +3827,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_if_double_parenthesized_condition() { // Audit-tests follow-up: with only the @@ -3702,6 +3861,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_bool_returning_terminal_kinds_count() { // Regression for issue #372 (lesson #19): before the fix, @@ -3747,6 +3907,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_if_method_call_condition() { check_metrics::( @@ -3767,6 +3928,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_if_while_boolean_literal_condition() { // Regression for #371: the tree-sitter-c-sharp grammar wraps a @@ -3807,6 +3969,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_short_circuit_with_boolean_literal_operand() { // Regression for #371 (companion to @@ -3849,6 +4012,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_return_without_conditions() { check_metrics::( @@ -3861,6 +4025,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_lambda_expressions_return_with_conditions() { check_metrics::( @@ -3875,6 +4040,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_for_with_variable_declaration() { check_metrics::( @@ -3890,6 +4056,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_for_without_variable_declaration() { check_metrics::( @@ -3906,6 +4073,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_for_identifier_condition() { check_metrics::( @@ -3945,6 +4113,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_for_invocation_condition() { check_metrics::( @@ -3991,6 +4160,7 @@ mod tests { // attribute one condition; without it, `for (; true ;)` would // contribute 0 (the bug fixed by this commit also affected this // shape). + #[cfg(feature = "csharp")] #[test] fn csharp_for_boolean_literal_condition() { check_metrics::( @@ -4013,6 +4183,7 @@ mod tests { // Regression coverage for #279: an empty for-loop condition such as // `for (; ;) {}` must contribute 0 to conditions — there is no // condition node to count. + #[cfg(feature = "csharp")] #[test] fn csharp_for_empty_condition() { check_metrics::( @@ -4050,6 +4221,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_ternary_conditions() { check_metrics::( @@ -4063,6 +4235,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_malformed_parenthesized_no_panic() { check_metrics::("class A { void M() { if (( }) }", "foo.cs", |metric| { @@ -4072,6 +4245,7 @@ mod tests { }); } + #[cfg(feature = "csharp")] #[test] fn csharp_function_pointer_type_no_double_count() { // EC1 extension — `<` and `>` are also parameter-list delimiters @@ -4095,6 +4269,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_generic_type_args_no_double_count() { // EC1 — `<` and `>` inside TypeArgumentList must not count as @@ -4127,6 +4302,7 @@ mod tests { // survive), 0 if the fixture stops parsing. Each is one higher than // before #1461, which added the `is` test as a count no gate on the // `<` / `>` token can reach. + #[cfg(feature = "csharp")] #[test] fn csharp_operator_declaration_is_not_a_condition() { check_func_space::( @@ -4187,6 +4363,7 @@ mod tests { // inside `binary_expression` parents, which must keep scoring one // condition each. Without it the gate could be satisfied by refusing // to count these tokens at all. + #[cfg(feature = "csharp")] #[test] fn csharp_comparison_operator_overloads_are_not_conditions() { let src = "class V { @@ -4261,6 +4438,7 @@ mod tests { // asks that such an entry be kept *and* have its unreachability // asserted, so that a pin promoting the symbol changes behaviour // loudly rather than invisibly. + #[cfg(feature = "csharp")] #[test] fn csharp_preproc_equality_counts_through_the_binary_expression_alias() { let src = "class P { @@ -4324,6 +4502,7 @@ mod tests { // // `c` is the constant-pattern control and reads 2 in both rows: it is // what the relational methods are supposed to agree with. + #[cfg(feature = "csharp")] #[test] fn csharp_relational_pattern_does_not_double_count_its_arm() { let src = "class A { @@ -4364,6 +4543,7 @@ mod tests { // however many relational operands it carries — which is exactly // what would regress if a later fix re-derived the gate from the // operand instead of the parent. + #[cfg(feature = "csharp")] #[test] fn csharp_is_pattern_and_combinators_score_one_decision() { let src = "class A { @@ -4412,6 +4592,7 @@ mod tests { // The four probes stay because they are four different enclosings — // a declarator, a bare `return`, an argument, a type test — and the // point was never that the pattern is special in one of them. + #[cfg(feature = "csharp")] #[test] fn csharp_relational_pattern_scores_one_wherever_it_is_written() { let src = "class A { @@ -4486,6 +4667,7 @@ mod tests { // 2 rather than leaving the assertion satisfied by something else; // `assert_csharp_fixture_spells` pins both guards by kind against // the same decay. + #[cfg(feature = "csharp")] #[test] fn csharp_switch_arm_guard_operator_still_counts() { let src = "class A { @@ -4563,6 +4745,7 @@ mod tests { // closing it. The remaining one is the slot's standing policy of // leaving a `binary_expression` to its operators, and `cmp` pays it // too — it just happens to break even there. + #[cfg(feature = "csharp")] #[test] fn csharp_switch_arm_guard_scores_one_condition_however_spelled() { let src = "class A { @@ -4639,6 +4822,7 @@ mod tests { // already-correct `is_pattern_expression` twins, so a regression // that reintroduced the asymmetry fails on the pair rather than on // an absolute number. + #[cfg(feature = "csharp")] #[test] fn csharp_bare_is_type_test_scores_one_condition() { let src = "class A { @@ -4703,6 +4887,7 @@ mod tests { // that only `conditions` moved — the suffix is not a decision in any // language, so a fix that moved both together would be a different // bug. + #[cfg(feature = "csharp")] #[test] fn csharp_null_forgiving_operand_scores_like_its_operand() { let src = "class A { @@ -4771,6 +4956,7 @@ mod tests { // whenever one is written there. That is #1455, which predates // #1463 and is a separate change; what this test adds is that the // new arm does not join it. + #[cfg(feature = "csharp")] #[test] fn csharp_null_forgiving_operand_survives_an_interposed_comment() { let src = "class A { @@ -4827,6 +5013,7 @@ mod tests { // here, the `SwitchExpressionArm` node there), so a guard rule // written against one shape could be dead for the other and every // expression-form fixture would still read correct. + #[cfg(feature = "csharp")] #[test] fn csharp_statement_switch_section_guard_counts() { let src = "class A { @@ -4887,6 +5074,7 @@ mod tests { // `CatchFilterClause` seed in `csharp_inspect_container`; only a // second pair of parentheses produces a `parenthesized_expression` // there. + #[cfg(feature = "csharp")] #[test] fn csharp_catch_filter_guard_scores_one_condition_however_spelled() { let src = "class A { @@ -4937,6 +5125,7 @@ mod tests { // the guard that makes it conditional. `bare` is the control that // keeps the exclusion itself pinned: drop the guard and the arm goes // back to costing nothing. + #[cfg(feature = "csharp")] #[test] fn csharp_guarded_discard_arm_scores_arm_and_guard() { let src = "class A { @@ -4975,6 +5164,7 @@ mod tests { // literals themselves are pinned by // `csharp_switch_arm_guard_scores_one_condition_however_spelled` and // its catch-filter sibling. + #[cfg(feature = "csharp")] #[test] fn csharp_guard_keeps_its_condition_across_a_comment() { let src = "class A { @@ -5013,6 +5203,7 @@ mod tests { // `csharp_bool_terminal_kinds!()` — so it reads one above `single`'s // lone comparison rather than collapsing to the same number, and // cyclomatic agrees because it counts the `&&`. + #[cfg(feature = "csharp")] #[test] fn csharp_compound_guard_keeps_its_sub_structure() { let src = "class A { @@ -5045,6 +5236,7 @@ mod tests { // genuine ternary, 0 if the fixture stops parsing. Asserting 0 on a // nullable-only body would have been vacuous — an unparsable file // scores 0 too. + #[cfg(feature = "csharp")] #[test] fn csharp_nullable_type_syntax_is_not_a_condition() { check_metrics::( @@ -5076,6 +5268,7 @@ mod tests { // `??`, which #1459 added to close the gap this comment used to // record as out of scope for #1275 — so a flipped `QMARK` polarity // now reads 1 rather than 0, and the discrimination is unchanged. + #[cfg(feature = "csharp")] #[test] fn csharp_conditional_access_still_counts_as_a_condition() { let src = "class A { @@ -5110,6 +5303,7 @@ mod tests { // `plain` is the control that keeps this from being an assertion // about `return` or about the method shape: same body without the // operator, 0 conditions and 1 decision. + #[cfg(feature = "csharp")] #[test] fn csharp_null_coalescing_is_a_condition() { let src = "class A { @@ -5139,6 +5333,7 @@ mod tests { // the Java `<` / `>` sibling is: C# cyclomatic scores this method 1 // decision, and the divergence is the pre-existing unary-condition // rule, not the `?` gate. + #[cfg(feature = "csharp")] #[test] fn csharp_ternary_still_counts_alongside_nullable_types() { check_metrics::( @@ -5154,6 +5349,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_aliased_invocation_expression_branches() { // Regression for issue #94 (lesson #2): the C# grammar emits three @@ -5178,6 +5374,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_zero_abc() { check_metrics::("( @@ -5201,6 +5399,7 @@ function f(): void { ); } + #[cfg(feature = "php")] #[test] fn php_augmented_assignment() { check_metrics::( @@ -5217,6 +5416,7 @@ function f(int $x): int { ); } + #[cfg(feature = "php")] #[test] fn php_const_excluded() { // Constant declarations and enum cases are NOT counted as @@ -5236,6 +5436,7 @@ enum Color { ); } + #[cfg(feature = "php")] #[test] fn php_function_call() { check_metrics::( @@ -5249,6 +5450,7 @@ function f(): void { ); } + #[cfg(feature = "php")] #[test] fn php_method_call() { check_metrics::( @@ -5262,6 +5464,7 @@ function f($obj): void { ); } + #[cfg(feature = "php")] #[test] fn php_static_call() { check_metrics::( @@ -5275,6 +5478,7 @@ function f(): void { ); } + #[cfg(feature = "php")] #[test] fn php_nullsafe_call() { check_metrics::( @@ -5288,6 +5492,7 @@ function f($obj): void { ); } + #[cfg(feature = "php")] #[test] fn php_object_creation() { check_metrics::( @@ -5301,6 +5506,7 @@ function f(): void { ); } + #[cfg(feature = "php")] #[test] fn php_comparison_eq() { check_metrics::( @@ -5313,6 +5519,7 @@ function f(int $a, int $b): bool { ); } + #[cfg(feature = "php")] #[test] fn php_comparison_strict() { check_metrics::( @@ -5325,6 +5532,7 @@ function f(int $a, int $b): bool { ); } + #[cfg(feature = "php")] #[test] fn php_spaceship() { check_metrics::( @@ -5337,6 +5545,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "php")] #[test] fn php_instanceof() { check_metrics::( @@ -5349,6 +5558,7 @@ function f($x): bool { ); } + #[cfg(feature = "php")] #[test] fn php_complex_function() { // One snippet exercising A, B, C buckets together. @@ -5367,6 +5577,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "php")] #[test] fn php_if_boolean_literal_condition() { check_metrics::( @@ -5385,6 +5596,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "php")] #[test] fn php_methods_arguments_with_conditions() { check_metrics::( @@ -5402,6 +5614,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "php")] #[test] fn php_return_with_conditions() { check_metrics::( @@ -5419,6 +5632,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "php")] #[test] fn php_name2_hidden_rule_drift_marker() { // Drift marker (findings.md round-2 #3): `Php::Name2` maps @@ -5437,6 +5651,7 @@ function f(int $a, int $b): int { assert!(!ast_has_kind_id(&parser, Php::Name2 as u16)); } + #[cfg(feature = "php")] #[test] fn php_scoped_property_access_condition_counts() { // Regression for findings.md round-2 #1 (PHP): @@ -5460,6 +5675,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "php")] #[test] fn php_named_argument_unary_conditional_counts() { // Regression for the code-review finding: PHP 8 named-argument @@ -5479,6 +5695,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "php")] #[test] fn php_low_precedence_keyword_logical_ops_trigger_walker() { // Regression: pre-fix, `$a or $b` reported 0 conditions @@ -5499,6 +5716,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "php")] #[test] fn php_if_multiple_conditions() { check_metrics::( @@ -5516,6 +5734,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "php")] #[test] fn php_while_and_do_while_conditions() { check_metrics::( @@ -5532,6 +5751,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "php")] #[test] fn php_short_circuit_with_boolean_literal_operand() { check_metrics::( @@ -5550,6 +5770,7 @@ function f(int $a, int $b): int { // does emit the token, but the `conditional_expression` node is // what carries the tally's +1 — so the arm keeps that increment and // adds the operand slots. + #[cfg(feature = "php")] #[test] fn php_ternary_operand_slots_count_as_unary_conditions() { // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4. @@ -5586,6 +5807,7 @@ function f(int $a, int $b): int { // grammar names `body` (not `consequence`) and marks optional. The // alternative lands at child(3), so addressing the slot by field // name rather than a fixed child(4) is what keeps `!$b` counted. + #[cfg(feature = "php")] #[test] fn php_elided_ternary_body_still_walks_the_alternative() { // ternary (1) + condition `$a` (1) + `!$b` (1) = 3. @@ -5603,6 +5825,7 @@ function f(int $a, int $b): int { // fixture below is a shape only the new arm can classify — a // comparison-shaped condition proves nothing here, because the `<` // token arm counts it either way (grammar-dispatch §11). + #[cfg(feature = "php")] #[test] fn php_for_condition_slot_counts_unary_conditions() { // Bare variable: the whole condition, no operator token. @@ -5647,6 +5870,7 @@ function f(int $a, int $b): int { // --- Kotlin ABC tests ------------------------------------------------- + #[cfg(feature = "kotlin")] #[test] fn kotlin_empty_class() { check_metrics::("class C {}", "foo.kt", |metric| { @@ -5657,6 +5881,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_val_declarations_are_not_assignments() { // `val` introduces an immutable binding — the `=` initialising it @@ -5676,6 +5901,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_var_declarations_count_assignment() { // `var` initialisers count as assignments (mutable binding). @@ -5692,6 +5918,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_val_then_assignments_count() { // Regression for #455: a `val` initialiser must not suppress the @@ -5714,6 +5941,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_var_then_assignments_count() { // Companion to the #455 regression: a `var` declaration leaves a @@ -5736,6 +5964,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_augmented_assignments_count() { // Augmented operators (+=, -=, etc.) and ++/-- always count. @@ -5758,6 +5987,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_branches_call_expression() { check_metrics::( @@ -5774,6 +6004,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_object_construction_branch() { // Kotlin's object construction is just `Foo()` — a `CallExpression`. @@ -5788,6 +6019,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_comparisons_count_conditions() { check_metrics::( @@ -5814,6 +6046,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_identity_equality_conditions() { // `===` / `!==` are referential equality in Kotlin; they count too. @@ -5829,6 +6062,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_else_branch_counts() { check_metrics::( @@ -5844,6 +6078,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_when_entries_count() { check_metrics::( @@ -5869,6 +6104,7 @@ function f(int $a, int $b): int { // is `else ->` must not count that arm. Revert-verified — gating the // `WhenEntry` arm on `!kotlin_when_entry_is_else` is what drops this // from 3 to 2 (issue #456, lesson 11). Mirrors the cyclomatic gate. + #[cfg(feature = "kotlin")] #[test] fn kotlin_when_else_not_a_condition() { check_metrics::( @@ -5891,6 +6127,7 @@ function f(int $a, int $b): int { // score can stand in for: a `when` that grew a subject scores its // entries the pre-#1421 way with every other row still satisfied. // Each fixture is one class, so the members are `spaces[0]`'s. + #[cfg(feature = "kotlin")] #[track_caller] fn assert_kotlin_class_members( src: &str, @@ -5932,6 +6169,7 @@ function f(int $a, int $b): int { // measured numbers would notice a fixture that grew one — a // subject-ful `when (x) { … }` scores its entries the old way and // `two` would read 4 again with every other row still satisfied. + #[cfg(feature = "kotlin")] #[test] fn kotlin_subjectless_when_arm_counts_its_condition_once() { let src = "class K { @@ -6017,6 +6255,7 @@ function f(int $a, int $b): int { // `Any`, and `(a as? Boolean)!!` back to a `Boolean`. The kind // anchors are what stop a later edit from trimming a spelling out // and turning its member into a silent copy of `bare`. + #[cfg(feature = "kotlin")] #[test] fn kotlin_condition_slot_peels_null_assertions_and_casts() { let src = "class K { @@ -6102,6 +6341,7 @@ function f(int $a, int $b): int { // `if` (`x == (y > 5)`) and ABC has always scored it 2 against the // same 1 decision. A comparison nested inside a comparison is two // comparisons; only one of them is a branch. + #[cfg(feature = "kotlin")] #[test] fn kotlin_subjectful_when_arms_keep_the_per_entry_count() { let src = "class K { @@ -6153,6 +6393,7 @@ function f(int $a, int $b): int { // The `block_comment` census is the fixture anchor: delete either // comment and the members still read 1, because a plain `when (x)` // and a plain `when {` both do. The count fails by name instead. + #[cfg(feature = "kotlin")] #[test] fn kotlin_when_subject_scan_clears_a_comment_before_the_brace() { let src = "class K { @@ -6185,6 +6426,7 @@ function f(int $a, int $b): int { // parenthesised operand is not in a slot the walker calls boolean and // `paren` drops to 0. `notted` cannot stand in for it — a `!` // operator proves boolean content on its own. + #[cfg(feature = "kotlin")] #[test] fn kotlin_subjectless_when_condition_wrappers_count_once() { let src = "class K { @@ -6246,6 +6488,7 @@ function f(int $a, int $b): int { // // `subjAlts` is the subject-ful control, unchanged and at parity: its // alternatives are constants carrying no token to count. + #[cfg(feature = "kotlin")] #[test] fn kotlin_subjectless_when_multi_alternative_entry() { let src = "class K { @@ -6295,6 +6538,7 @@ function f(int $a, int $b): int { // the arm does. `kotlin_is_and_in_score_outside_a_boolean_slot` is // the half this fixture cannot see, every member of it being inside // a slot. + #[cfg(feature = "kotlin")] #[test] fn kotlin_is_and_in_expressions_are_unary_conditions() { let src = "class K { @@ -6345,6 +6589,7 @@ function f(int $a, int $b): int { // statement of the bug. `andAmp` holds the pair level against the // `&&` spelling, which never regressed and would otherwise be the // only form under test. + #[cfg(feature = "kotlin")] #[test] fn kotlin_infix_boolean_functions_are_unary_conditions() { let src = "class K { @@ -6378,6 +6623,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_catch_block_counts() { check_metrics::( @@ -6399,6 +6645,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_elvis_and_safe_cast() { // `?:` (elvis) and `as?` (safe cast) are condition-like. @@ -6416,6 +6663,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_generic_brackets_not_conditions() { // `<` / `>` used as type-parameter brackets must not be counted. @@ -6439,6 +6687,7 @@ function f(int $a, int $b): int { // separates every mis-aim: 3 pre-fix, 1 once the arm allows // `BinaryExpression`, 0 if it allows the wrong parent or the // fixture stops parsing. + #[cfg(feature = "kotlin")] #[test] fn kotlin_super_type_argument_is_not_a_condition() { check_metrics::( @@ -6463,6 +6712,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_class_with_methods_and_branches() { check_metrics::( @@ -6485,6 +6735,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_object_singleton_abc() { check_metrics::( @@ -6511,6 +6762,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_interface_abc() { // Pure-abstract interface with no bodies — all-zero. @@ -6529,6 +6781,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_nested_class_abc() { check_metrics::( @@ -6550,6 +6803,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_data_class_abc() { // `data class` with primary-constructor `val`s — no assignments @@ -6566,6 +6820,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_primary_constructor_default_value_not_assignment() { // Regression: default values on primary-constructor `val` @@ -6580,6 +6835,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_unary_conditions_in_chain() { // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a @@ -6598,6 +6854,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_comparison_operands_add_nothing() { // Isolation check: comparison operands of a `&&` chain are nested @@ -6615,6 +6872,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_negated_operand_is_unary_condition() { // A `!`-negated operand is still a unary condition: `a && !b` @@ -6645,6 +6903,7 @@ function f(int $a, int $b): int { // which is what both lines scored before #1459. The bare `a && b` // control is `kotlin_unary_conditions_in_chain`'s shape at 2, so a // regression cannot be read as the chain itself changing. + #[cfg(feature = "kotlin")] #[test] fn kotlin_chain_operands_peel_null_assertions_and_casts() { // Anchored per row, because the assertion alone cannot tell the @@ -6687,6 +6946,7 @@ function f(int $a, int $b): int { } } + #[cfg(feature = "kotlin")] #[test] fn kotlin_bare_if_predicate_is_one_condition() { // Issue #773: a bare-boolean `if` predicate (`if (flag)`) is one @@ -6703,6 +6963,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_bare_while_predicate_is_one_condition() { // Issue #773: the bare predicate of a `while` loop counts one @@ -6716,6 +6977,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_bare_do_while_predicate_is_one_condition() { // Issue #773: the bare predicate of a `do`/`while` loop counts one @@ -6729,6 +6991,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_comparison_predicate_not_double_counted() { // Double-count guard (#773): a comparison predicate (`if (a == b)`) @@ -6744,6 +7007,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_short_circuit_predicate_not_double_counted() { // Double-count guard (#773): an `&&`/`||` predicate is counted by @@ -6759,6 +7023,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_parenthesised_bare_predicate_is_one_condition() { // A parenthesised bare predicate (`if ((flag))`) is unwrapped by @@ -6784,6 +7049,7 @@ function f(int $a, int $b): int { // and `<`/`>` (outside `type_arguments` / `type_parameters`) count // as conditions. + #[cfg(feature = "typescript")] #[test] fn typescript_assignments_basic() { check_metrics::( @@ -6803,6 +7069,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_const_excluded_from_assignments() { check_metrics::( @@ -6829,6 +7096,7 @@ function f(int $a, int $b): int { // languages. The replacement is structural — see // `impl_js_family_const_binding!` in `src/metrics/abc/js_family.rs`. + #[cfg(feature = "typescript")] #[test] fn typescript_asi_const_does_not_suppress_later_assignments() { check_metrics::( @@ -6847,6 +7115,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_semicolon_const_does_not_suppress_later_assignments() { // The semicolon-terminated spelling of the fixture above, which @@ -6865,6 +7134,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_as_const_does_not_suppress_later_assignments() { // The sentinel stack was also reachable from the other side: the @@ -6885,6 +7155,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_const_declarator_shapes_stay_suppressed() { // Shapes the sentinel stack handled implicitly, which the @@ -6920,6 +7191,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_const_initializer_value_assignments_still_count() { // TypeScript half of @@ -6933,6 +7205,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_branches_function_calls() { check_metrics::( @@ -6951,6 +7224,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_conditions_comparison_operators() { check_metrics::( @@ -6974,6 +7248,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_conditions_control_flow_arms() { check_metrics::( @@ -6998,6 +7273,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_conditions_switch_case() { check_metrics::( @@ -7021,6 +7297,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_ternary_and_nullish() { check_metrics::( @@ -7042,6 +7319,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_instanceof_counts_as_condition() { check_metrics::( @@ -7058,6 +7336,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_generic_lt_gt_not_a_condition() { // `` in `class C` and `Array` should not contribute @@ -7107,6 +7386,7 @@ function f(int $a, int $b): int { // genuinely reports a `binary_expression`. It is a wrong-dialect // input rather than a defect in this arm, and no allowlist can // distinguish it; see the same caveat on Kotlin's arm. + #[cfg(feature = "typescript")] #[test] fn typescript_comparison_operators_still_count_alongside_generics() { check_func_space::( @@ -7135,6 +7415,7 @@ function f(int $a, int $b): int { // fixture stops parsing. Any partial gate — one that named some of // the type-syntax parents in a denylist instead — lands between 3 // and 7 and is equally visible. + #[cfg(feature = "typescript")] #[test] fn typescript_optional_type_syntax_is_not_a_condition() { check_metrics::( @@ -7161,6 +7442,7 @@ function f(int $a, int $b): int { // The real ternary below keeps the expectation off zero: 3 pre-fix, // 2 with the conditional type excluded, 1 if the ternary is // swallowed too. + #[cfg(feature = "typescript")] #[test] fn typescript_conditional_type_is_not_a_condition() { check_metrics::( @@ -7189,6 +7471,7 @@ function f(int $a, int $b): int { // satisfy the positive assertion on its own, leaving it true no // matter what id the ternary's `?` came back as — decoration rather // than the non-vacuity guard it is here for. + #[cfg(feature = "typescript")] #[test] fn typescript_ternary_qmark_alias_stays_unreachable() { let parser = TypescriptParser::new( @@ -7202,6 +7485,7 @@ function f(int $a, int $b): int { assert!(!ast_has_kind_id(&parser, Typescript::QMARK2 as u16)); } + #[cfg(feature = "typescript")] #[test] fn typescript_abstract_class_abc() { // Abstract methods have no body — they contribute nothing. @@ -7222,6 +7506,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_interface_abc_zero() { check_metrics::( @@ -7240,6 +7525,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_arrow_field_contributes_abc() { // Arrow function class members are function spaces; their @@ -7262,6 +7548,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_parameter_property_init_not_assignment() { // Parameter properties don't introduce a `=` token themselves; @@ -7287,6 +7574,7 @@ function f(int $a, int $b): int { // TSX parity + #[cfg(feature = "typescript")] #[test] fn tsx_assignments_basic() { check_metrics::( @@ -7306,6 +7594,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_const_excluded_from_assignments() { check_metrics::( @@ -7323,6 +7612,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_branches_function_calls() { check_metrics::( @@ -7340,6 +7630,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_conditions_comparison_operators() { check_metrics::( @@ -7356,6 +7647,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_conditions_control_flow_arms() { check_metrics::( @@ -7377,6 +7669,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_conditions_switch_case() { check_metrics::( @@ -7397,6 +7690,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_ternary_and_nullish() { check_metrics::( @@ -7414,6 +7708,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_instanceof_counts_as_condition() { check_metrics::( @@ -7426,6 +7721,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_generic_lt_gt_not_a_condition() { check_metrics::( @@ -7451,6 +7747,7 @@ function f(int $a, int $b): int { // number: 8 pre-fix, 1 once it allows `BinaryExpression`, 0 if it // allows the wrong parent or the fixture stops parsing. Asserting 0 // on a JSX-only body would not have separated those last two. + #[cfg(feature = "typescript")] #[test] fn tsx_jsx_elements_are_not_conditions() { check_metrics::( @@ -7477,6 +7774,7 @@ function f(int $a, int $b): int { // own fixture — a passing TypeScript test says nothing about the // macro's other expansion. Four type-syntax `?` plus one `>` and one // ternary: 6 pre-fix, 2 after, 1 if the allowlist is misaimed. + #[cfg(feature = "typescript")] #[test] fn tsx_optional_type_syntax_is_not_a_condition() { check_metrics::( @@ -7497,6 +7795,7 @@ function f(int $a, int $b): int { // without this one, which reads as the TSX expansion being fine // rather than untested. `conditional_type` is in the tsx grammar's // `?` set exactly as it is in typescript's. + #[cfg(feature = "typescript")] #[test] fn tsx_conditional_type_is_not_a_condition() { check_metrics::( @@ -7513,6 +7812,7 @@ function f(int $a, int $b): int { // — the tsx grammar declares the same `_ternary_qmark` external and // maps it back onto `anon_sym_QMARK` at its own id. Same // single-`?` fixture rule; see that test for why. + #[cfg(feature = "typescript")] #[test] fn tsx_ternary_qmark_alias_stays_unreachable() { let parser = TsxParser::new( @@ -7526,6 +7826,7 @@ function f(int $a, int $b): int { assert!(!ast_has_kind_id(&parser, Tsx::QMARK2 as u16)); } + #[cfg(feature = "typescript")] #[test] fn tsx_abstract_class_abc() { check_metrics::( @@ -7545,6 +7846,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_interface_abc_zero() { check_metrics::( @@ -7559,6 +7861,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_arrow_field_contributes_abc() { check_metrics::( @@ -7577,6 +7880,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_asi_const_does_not_suppress_later_assignments() { // TSX half of the #1277 cluster; see @@ -7597,6 +7901,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_const_declarator_shapes_stay_suppressed() { // TSX half of `typescript_const_declarator_shapes_stay_suppressed`. @@ -7616,6 +7921,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_parameter_property_init_not_assignment() { // Parameter properties contribute no `=`; the body's `let z = 0` @@ -7642,6 +7948,7 @@ function f(int $a, int $b): int { // `else` / `elsif` / `when` / `then` / `?` / `rescue` clause is // one condition. + #[cfg(feature = "ruby")] #[test] fn ruby_zero_abc() { check_metrics::("\n", "foo.rb", |metric| { @@ -7652,6 +7959,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_simple_assignment() { check_metrics::("def f\n a = 1\n b = 2\nend\n", "foo.rb", |metric| { @@ -7662,6 +7970,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_augmented_assignment() { // `+=`, `-=`, `*=` are `operator_assignment` nodes — each is @@ -7677,6 +7986,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_logical_augmented_assignment() { // `||=` and `&&=` are also `operator_assignment` nodes. @@ -7686,6 +7996,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_method_call_branch() { // Each method invocation is one branch. @@ -7699,6 +8010,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_super_and_yield_branches() { // `super` and `yield` both count as branches (control-pass). @@ -7709,6 +8021,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_attr_macro_is_branch() { // `attr_accessor` is a `Call3` node and registers as a branch @@ -7719,6 +8032,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_comparison_conditions() { // Each comparison operator is one condition. @@ -7732,6 +8046,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_superclass_clause_is_not_a_condition() { // Regression for #1280: a superclass clause spells its `<` with the @@ -7751,6 +8066,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_operator_method_name_is_not_a_condition() { // The `<` naming an operator method parents under `operator`, which @@ -7764,6 +8080,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_every_comparison_operator_method_name_is_not_a_condition() { // The sibling half of #1280. `<` is not special: every comparison @@ -7800,6 +8117,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_case_match_in_arms_are_conditions() { // Regression for #977: each non-wildcard `case … in` arm is one @@ -7816,6 +8134,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_case_match_guarded_wildcard_is_a_condition() { // Regression for #977: a guarded wildcard arm `in _ if x` is not a @@ -7834,6 +8153,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_case_match_bare_wildcard_is_not_a_condition() { // Regression for #977: a `case … in` whose only arm is the bare @@ -7850,6 +8170,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_bare_predicate_control_flow_counts_one_condition() { // Regression for #696: idiomatic Ruby bare predicates @@ -7868,6 +8189,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_bare_predicate_does_not_double_count_comparison_or_chain() { // `if a == b` counts only the `==` comparison (the condition field @@ -7885,6 +8207,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_spaceship_and_case_equality() { // `<=>` and `===` are comparison operators (conditions). @@ -7898,6 +8221,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_ternary_condition() { // The `?` ternary marker is one condition; the inner `==` is @@ -7917,6 +8241,7 @@ function f(int $a, int $b): int { // Every expectation below is the value its C++ sibling // (`cpp_ternary_operand_slots_count_as_unary_conditions`) already // asserts for the same expression, so the two read as one table. + #[cfg(feature = "ruby")] #[test] fn ruby_ternary_operand_slots_count_as_unary_conditions() { // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4. @@ -7975,6 +8300,7 @@ function f(int $a, int $b): int { // A parenthesised *branch* is the input that separates them: the // unwrap reaches a bare terminal, so only the seed decides whether // it counts. `?` (1) + condition `a` (1) = 2 in both directions. + #[cfg(feature = "ruby")] #[test] fn ruby_ternary_branch_operands_are_not_double_counted() { check_metrics::("def f\n x = a ? (b) : c\nend\n", "foo.rb", |metric| { @@ -8005,6 +8331,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_case_when_arms() { // Each `when` named clause and the `else` clause count as one @@ -8021,6 +8348,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_elsif_and_else() { // `elsif` and `else` named clauses are conditions; their inner @@ -8036,6 +8364,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_rescue_clause_condition() { // The `rescue` named clause is one condition; the `rescue` @@ -8053,6 +8382,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_class_complex_function() { // Mixed: assignment(=), branch(call), conditions(`>` and `==`). @@ -8073,6 +8403,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_unary_conditions_in_chain() { // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a @@ -8088,6 +8419,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_keyword_and_or_chain_counts_operands() { // The keyword forms `and` / `or` get the same Rule 9 treatment as @@ -8101,6 +8433,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_negated_operand_is_unary_condition() { // A `!`-negated operand unwraps the `unary` node to the inner @@ -8114,6 +8447,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_comparison_operands_add_nothing() { // Isolation for Rule 9 (issue #557): when the `&&` operands are @@ -8144,6 +8478,7 @@ function f(int $a, int $b): int { // --- Python ABC --------------------------------------------------- + #[cfg(feature = "python")] #[test] fn python_empty_module_zero() { check_metrics::("", "empty.py", |metric| { @@ -8154,6 +8489,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "python")] #[test] fn python_plain_assignments_count() { // Three plain `=` assignments → A=3. No branches, no conditions. @@ -8165,6 +8501,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "python")] #[test] fn python_typed_assignment_counts_bare_annotation_does_not() { // `x: int = 1` carries an `=`, so it counts. @@ -8176,6 +8513,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "python")] #[test] fn python_augmented_assignments_count() { // Each augmented op counts once. @@ -8186,6 +8524,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "python")] #[test] fn python_walrus_counts_as_assignment() { // `x := 10` is a `NamedExpression` (PEP 572). It binds a value @@ -8199,6 +8538,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "python")] #[test] fn python_calls_are_branches() { // `foo()`, `bar()`, `Baz()` (constructor) all parse as `Call` @@ -8214,6 +8554,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_comparisons_count_conditions() { // `x > 0`, `x == y`, `x is None` are each a single @@ -8230,6 +8571,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_chained_comparison_counts_once() { // tree-sitter-python collapses `0 < x < 10` into a single @@ -8240,6 +8582,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "python")] #[test] fn python_number_truthy_condition_counts() { // Regression for #772: Python treats every non-zero number as @@ -8262,6 +8605,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_boolean_operators_not_counted_directly() { // Python's `and` / `or` are not counted as conditions on @@ -8287,6 +8631,7 @@ function f(int $a, int $b): int { /// counts as one condition, matching Java's `!x` rule. Closes /// the parity gap noted in #214: without this, `if not flag:` /// reported 0 conditions while the Java equivalent reports 1. + #[cfg(feature = "python")] #[test] fn python_unary_not_counts_as_condition() { check_metrics::( @@ -8304,6 +8649,7 @@ function f(int $a, int $b): int { /// `return not flag` — the unary `not` is the entire return /// expression. Without `NotOperator` counted, this reports zero /// conditions; with it, one. Java's `return !flag;` is one. + #[cfg(feature = "python")] #[test] fn python_return_unary_not_counts() { check_metrics::("def f(flag):\n return not flag\n", "foo.py", |metric| { @@ -8315,6 +8661,7 @@ function f(int $a, int $b): int { /// `foo(not ready, value)` — the unary `not` inside an argument /// list still contributes. Mirrors Java's /// `java_count_unary_conditions` walk over argument lists. + #[cfg(feature = "python")] #[test] fn python_unary_not_in_argument_list_counts() { check_metrics::( @@ -8335,6 +8682,7 @@ function f(int $a, int $b): int { /// ComparisonOperator))`; both the unary and the comparison /// contribute one condition (mirrors Java's `!(x > 0)` = 2 /// conditions). + #[cfg(feature = "python")] #[test] fn python_unary_not_with_comparison_counts_each_once() { check_metrics::( @@ -8355,6 +8703,7 @@ function f(int $a, int $b): int { /// counted by the Rule 9 walker (issue #403). Total: 2. /// `NotOperator` is intentionally not walked-into a second /// time — the walker skips it to avoid double-counting. + #[cfg(feature = "python")] #[test] fn python_unary_not_with_boolean_combinator_counts_each() { check_metrics::( @@ -8368,6 +8717,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_control_flow_arms_count_conditions() { // `elif`, `else`, `except`, `finally`, `case` each contribute @@ -8385,6 +8735,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_ternary_counts_as_condition() { // `a if c else b` is `ConditionalExpression` → 1 condition. @@ -8405,6 +8756,7 @@ function f(int $a, int $b): int { // `python_inspect_container`'s `ConditionalExpression` boolean- // context seed was unreachable, no call site having passed that // parent. + #[cfg(feature = "python")] #[test] fn python_ternary_condition_slot_counts_as_a_unary_condition() { // ternary (1) + condition `c()` (1) = 2. `c()` is a `Call`, a @@ -8470,6 +8822,7 @@ function f(int $a, int $b): int { // Both fixtures below are 2 today and 4 under such a copy, so a // later "make Python consistent with the others" change cannot land // silently. + #[cfg(feature = "python")] #[test] fn python_ternary_branch_operands_are_not_double_counted() { // ternary (1) + condition `a` (1) = 2. The two parenthesised @@ -8497,6 +8850,7 @@ function f(int $a, int $b): int { // index after them. `python_count_ternary_condition` therefore // anchors on the `if` keyword and skips comments after it; both // halves are needed and each fixture below fails without one. + #[cfg(feature = "python")] #[test] fn python_ternary_condition_survives_an_interposed_comment() { // Comment before the keyword: `child(2)` is the `if` token here, @@ -8520,6 +8874,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_try_except_finally_count_conditions() { // ExceptClause + FinallyClause → 2 conditions. @@ -8533,6 +8888,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_match_case_counts_conditions() { // Each non-wildcard `CaseClause` → 1 condition. The bare @@ -8550,6 +8906,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_match_case_guarded_wildcard_counts() { // `case _ if g:` is NOT a bare wildcard — the guard @@ -8569,6 +8926,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_complex_function_abc() { // Mixed-shape regression: assignments, calls, conditions all in @@ -8594,6 +8952,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_if_multiple_conditions() { // Fitzpatrick Rule 9 walker on `and` / `or` (issue #403). @@ -8619,6 +8978,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_while_conditions() { // Python has no `do { ... } while(cond);` construct, so this @@ -8639,6 +8999,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_short_circuit_with_boolean_literal_operand() { // `a and True` reports 2 conditions: one identifier, one @@ -8650,6 +9011,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "python")] #[test] fn python_await_expression_condition_counts() { // Regression for findings.md round-2 #2 (Python): @@ -8670,6 +9032,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_if_call_terminal_condition_counts_once() { // Pins the Phase-2B behaviour for Python's `Call` terminal-bool @@ -8686,6 +9049,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "python")] #[test] fn python_if_boolean_literal_condition() { // Phase 2B (issue #403): bare-boolean conditions count once. @@ -8708,6 +9072,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_methods_arguments_with_conditions() { // `m(not a, not b)` reports 2 conditions — both `NotOperator` @@ -8727,6 +9092,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "python")] #[test] fn python_return_with_conditions() { // Phase 2B (issue #403). Python uses the pre-existing top- @@ -8749,6 +9115,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_empty_unit_zero() { // No code at all → A=B=C=0. Establishes the trait is wired up @@ -8761,6 +9128,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "rust")] #[test] fn rust_assignments_let_init_plain_and_compound() { // `let mut x = 0` is a `let_declaration` carrying an `=` @@ -8781,6 +9149,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_let_without_initializer_does_not_count() { // `let a;` is a `let_declaration` with NO `=` and no `value` @@ -8801,6 +9170,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_let_initializers_immutable_and_mutable_count() { // Issue #393: `let a = 1;`, `let b = 2;`, `let c = a + b;`, @@ -8819,6 +9189,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_calls_are_branches() { // Free function call + method call (parses as call_expression @@ -8838,6 +9209,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_try_operator_is_branch() { // `?` parses as `try_expression` and counts as one branch @@ -8854,6 +9226,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_comparisons_count_conditions() { // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six @@ -8868,6 +9241,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_generic_brackets_not_conditions() { // `<` / `>` in `Vec` are TypeArguments delimiters, not @@ -8883,6 +9257,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_if_let_counts_as_condition() { // `if let Some(v) = opt { ... }` introduces a `let_condition` @@ -8898,6 +9273,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_while_let_counts_as_condition() { // `while let Some(y) = it.next() { ... }` is also a @@ -8914,6 +9290,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_match_arms_count_conditions_wildcard_excluded() { // Three arms: `0 => 1`, `n if n > 0 => n`, `_ => -1`. The @@ -8932,6 +9309,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_else_counts_as_condition() { // `if a > b { ... } else { ... }` → `a > b` is one condition, @@ -8946,6 +9324,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_let_chain2_hidden_rule_drift_marker() { // Drift marker (findings.md round-2 #3): `Rust::LetChain2` @@ -8969,6 +9348,7 @@ function f(int $a, int $b): int { assert!(!ast_has_kind_id(&parser, Rust::LetChain2 as u16)); } + #[cfg(feature = "rust")] #[test] fn rust_scoped_identifier_condition_counts() { // Regression for findings.md round-2 #1 (Rust): @@ -8985,6 +9365,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "rust")] #[test] fn rust_await_expression_condition_counts() { // Regression for findings.md round-2 #2 (Rust): @@ -9006,6 +9387,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_complex_function_abc() { // Mixed-shape regression: assignments, calls, conditions, `?`, @@ -9049,6 +9431,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_let_chain_bare_identifier_operand_counts() { // Regression: pre-fix, `if a && let Some(_z) = y { }` reported @@ -9071,6 +9454,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_if_multiple_conditions() { // Fitzpatrick Rule 7 / Listing 2 (issue #403): every operand of @@ -9094,6 +9478,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_while_conditions() { // Rust has no `do { ... } while(cond);` construct, so this @@ -9113,6 +9498,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_if_boolean_literal_condition() { // Phase 2B (issue #403): a condition whose entire body is a @@ -9133,6 +9519,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_methods_arguments_with_conditions() { // Phase 2B (issue #403): unary-conditional arguments to a @@ -9155,6 +9542,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_return_with_conditions() { // Phase 2B (issue #403). Mirrors `java_return_with_conditions` @@ -9191,6 +9579,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "rust")] #[test] fn rust_short_circuit_with_boolean_literal_operand() { // `if a && true` reports 2 conditions: one for the identifier @@ -9208,6 +9597,7 @@ function f(int $a, int $b): int { // ----- Go ----- + #[cfg(feature = "go")] #[test] fn go_empty_unit_zero() { // Package declaration only — no Fitzpatrick events. Confirms the @@ -9220,6 +9610,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "go")] #[test] fn go_assignments_count_plain_compound_short_var_and_incdec() { // `x := 0` (short var decl), `x = 5` and `x = 7` (plain `=`), @@ -9242,6 +9633,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_var_declarations_count_only_when_initialized() { // Regression for #1278: a `var` declaration with an initializer is @@ -9262,6 +9654,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_grouped_var_block_counts_each_initialized_spec() { // A grouped `var ( … )` block is one `var_declaration` holding one @@ -9280,6 +9673,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_calls_are_branches() { // Three calls: free function `g()`, method call `r.Inc()`, and @@ -9299,6 +9693,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_comparisons_count_conditions() { // `<`, `>`, `<=`, `>=`, `==`, `!=` each count once. Six @@ -9313,6 +9708,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_generic_brackets_not_conditions() { // Generic instantiation `Min[int](a, b)` puts `int` inside @@ -9329,6 +9725,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_switch_arms_count_conditions_default_excluded() { // Four arms: `case 1:`, `case 2:`, `case 3:`, `default:`. The @@ -9346,6 +9743,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_type_switch_arms_count_conditions() { // Type switch: `case int:`, `case string:`, `default:`. Two @@ -9360,6 +9758,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_select_arms_count_conditions() { // `select { case <-ch: ...; case ch <- 1: ...; default: ... }`. @@ -9374,6 +9773,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_else_counts_as_condition() { // `if a > b { ... } else { ... }` → `a > b` is one condition, @@ -9388,6 +9788,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_complex_function_abc() { // Mixed shape, verified by hand: @@ -9422,6 +9823,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_if_multiple_conditions() { // Fitzpatrick Rule 7 walker fan-out (issue #403). Mirrors @@ -9442,6 +9844,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_for_with_conditions() { // Go has no `while` or `do { … } while(…);` — the `for` loop @@ -9462,6 +9865,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_for_bare_condition_counts() { // Regression for findings.md #1: `for true {}` / `for !ready {}` @@ -9503,6 +9907,7 @@ function f(int $a, int $b): int { // field. Letting the `for_clause` fall through — which the arm's // own comment used to call harmless — scored a bare three-clause // condition zero while `for a {}` scored one. + #[cfg(feature = "go")] #[test] fn go_three_clause_for_condition_counts() { // Bare identifier in the three-clause header: no comparison @@ -9555,6 +9960,7 @@ function f(int $a, int $b): int { // children — the #1181 failure), and the body of a bare `for {}`, // which IS child(1) and would otherwise be offered to // `go_count_condition` as though it were a condition. + #[cfg(feature = "go")] #[test] fn go_for_header_slot_skips_comments_and_the_body() { // Each pair is (source, expected conditions). The commented @@ -9587,6 +9993,7 @@ function f(int $a, int $b): int { assert!(cases.iter().any(|&(_, n)| n == 0)); } + #[cfg(feature = "go")] #[test] fn go_if_init_statement_condition_counts() { // Regression for the code-review finding: Go's @@ -9610,6 +10017,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_if_boolean_literal_condition() { check_metrics::( @@ -9626,6 +10034,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_methods_arguments_with_conditions() { check_metrics::( @@ -9643,6 +10052,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_return_with_conditions() { check_metrics::( @@ -9664,6 +10074,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "go")] #[test] fn go_short_circuit_with_boolean_literal_operand() { // `a && true` reports 2 conditions: one identifier, one @@ -9685,6 +10096,7 @@ function f(int $a, int $b): int { // zero. Uses a bare expression rather than a `defmodule` wrapper // (which would itself be a Call → 1 branch). Confirms the // ElixirCode Abc trait is wired up and the metric emits. + #[cfg(feature = "elixir")] #[test] fn elixir_empty_unit_zero() { check_metrics::(":ok\n", "foo.ex", |metric| { @@ -9701,6 +10113,7 @@ function f(int $a, int $b): int { // directives (`alias`, `import`, `require`, `use`) are NOT // runtime dispatch and therefore do NOT inflate `branches`, // matching Cognitive's treatment. + #[cfg(feature = "elixir")] #[test] fn elixir_defmodule_is_zero_branches() { check_metrics::("defmodule Foo do\nend\n", "foo.ex", |metric| { @@ -9715,6 +10128,7 @@ function f(int $a, int $b): int { // `defmodule` and `def` are declarative-Call wrappers and are // filtered out of branches; the assertion focuses on assignments // so we only pin that vector. + #[cfg(feature = "elixir")] #[test] fn elixir_pattern_match_is_assignment() { check_metrics::( @@ -9734,6 +10148,7 @@ function f(int $a, int $b): int { // pipeline Call tree, contributing additional Call branches. // The headline assertion confirms (a) `|>` is detected and (b) // pipeline steps are not silently dropped. + #[cfg(feature = "elixir")] #[test] fn elixir_pipeline_each_step_is_branch() { check_metrics::( @@ -9754,6 +10169,7 @@ function f(int $a, int $b): int { // Comparison operators all count as conditions. Six comparisons // (`==`, `!=`, `<`, `>`, `<=`, `>=`) → C = 6. + #[cfg(feature = "elixir")] #[test] fn elixir_comparisons_are_conditions() { check_metrics::( @@ -9767,6 +10183,7 @@ function f(int $a, int $b): int { } // Strict-equality operators `===` / `!==` count as conditions too. + #[cfg(feature = "elixir")] #[test] fn elixir_strict_equality_is_condition() { check_metrics::( @@ -9795,6 +10212,7 @@ function f(int $a, int $b): int { // `in` rides the `<` / `>` `binary_operator` gate rather than // standing alone — it has the same three grammar positions — and // `elixir_operator_identifier_is_not_a_condition` pins that gate. + #[cfg(feature = "elixir")] #[test] fn elixir_membership_is_a_condition_by_use() { check_func_space::( @@ -9836,6 +10254,7 @@ function f(int $a, int $b): int { // Was 2, on a flat `+1` for the `when` token laid on top of the `>`: // the §5 double count a whole-branch review of the #1454 batch // found. + #[cfg(feature = "elixir")] #[test] fn elixir_guard_when_is_condition() { check_metrics::( @@ -9853,6 +10272,7 @@ function f(int $a, int $b): int { // Keyword-shaped Calls (`case`, `cond`, `if`, `with`) each count // as one condition AND one branch. `case` here adds 1 condition // (the keyword Call) + 1 branch (the Call itself). + #[cfg(feature = "elixir")] #[test] fn elixir_case_is_condition_and_branch() { check_metrics::( @@ -9867,6 +10287,7 @@ function f(int $a, int $b): int { } // `cond` is structurally identical to `case` for Abc. + #[cfg(feature = "elixir")] #[test] fn elixir_cond_is_condition() { check_metrics::( @@ -9883,6 +10304,7 @@ function f(int $a, int $b): int { // `for` is a comprehension/loop, NOT in the issue's condition // list. It is still a Call so it contributes one branch, but no // condition. + #[cfg(feature = "elixir")] #[test] fn elixir_for_is_branch_not_condition() { check_metrics::( @@ -9901,6 +10323,7 @@ function f(int $a, int $b): int { // - Branches: `defmodule` and `def` are declarative and excluded; // `if` Call + `side_effect()` Call → 2 Calls, plus 0 `|>` → B = 2. // - Conditions: `if` keyword → 1, `x > 0` → 1 → C = 2. + #[cfg(feature = "elixir")] #[test] fn elixir_mixed_abc() { check_metrics::( @@ -9915,6 +10338,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_unary_conditions_in_chain() { // Fitzpatrick Rule 9 (issue #557): each bare boolean operand of a @@ -9947,6 +10371,7 @@ function f(int $a, int $b): int { // a `&&` operand is an ABC condition with no cyclomatic decision // behind it, so both members legitimately sit one above their // decision count (`base 1 + if + &&` = 3). + #[cfg(feature = "elixir")] #[test] fn elixir_keyword_not_negates_like_bang() { check_func_space::( @@ -9958,6 +10383,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_comparison_operands_add_nothing() { // Isolation check: comparison operands of a `&&` chain are nested @@ -9973,6 +10399,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_keyword_and_or_chain_counts_operands() { // The keyword forms `and` / `or` get the same Rule 9 treatment as @@ -9996,6 +10423,7 @@ function f(int $a, int $b): int { // is not a `Call` node — verified by AST dump: `binary_operator` // wrapping `identifier`, `=`, `sigil`), C = 0 (no comparison, no // guard, no keyword Call). + #[cfg(feature = "elixir")] #[test] fn elixir_sigil_delimiter_choice_is_abc_invariant() { for src in ["x = ~s\n", "x = ~s(hi)\n"] { @@ -10011,6 +10439,7 @@ function f(int $a, int $b): int { // comparison and must keep counting even with a `<`-delimited // sigil in the same unit. expected: A = 2 (`x =`, `y =`), C = 1 // (only `a < b`; the sigil's `<` / `>` delimiters are guarded). + #[cfg(feature = "elixir")] #[test] fn elixir_lt_comparison_still_counts_beside_sigil() { check_metrics::("x = ~s\ny = a < b\n", "foo.ex", |metric| { @@ -10033,6 +10462,7 @@ function f(int $a, int $b): int { // control: a qualified call whose name is not an operator was // always 0, so a guard that merely stopped counting `<` everywhere // would pass this row and fail the two below it. + #[cfg(feature = "elixir")] #[test] fn elixir_operator_identifier_is_not_a_condition() { check_func_space::( @@ -10070,6 +10500,7 @@ function f(int $a, int $b): int { // ----- C++ ----- + #[cfg(feature = "cpp")] #[test] fn cpp_empty_unit_zero() { // No code → A=B=C=0. Wires up the trait and exercises the @@ -10082,6 +10513,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_plain_and_compound_assignments_count() { // `int x = 0` is an `init_declarator` carrying an `=` token @@ -10101,6 +10533,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_increment_and_decrement_count_as_assignment() { // `x++` / `--x` / prefix and postfix forms each parse as @@ -10117,6 +10550,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_init_declarators_count_as_assignments() { // Issue #393 regression: `int a=1;`, `int b=2;`, `int c=a+b;`, @@ -10133,6 +10567,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_declaration_without_initializer_does_not_count() { // `int a;` parses as a plain declarator inside `declaration`, @@ -10146,6 +10581,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_init_declarator_brace_paren_init_does_not_count() { // `init_declarator` has two grammar forms: `declarator = value` @@ -10166,6 +10602,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_calls_are_branches() { // Free call + member-fn call (parses as `call_expression` with @@ -10184,6 +10621,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_comparisons_count_conditions() { // `<`, `>`, `<=`, `>=`, `==`, `!=`, and the C++20 spaceship @@ -10209,6 +10647,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_short_circuit_ops_not_counted_directly() { // `&&` and `||` do NOT count on their own (see the @@ -10233,6 +10672,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_generic_brackets_not_conditions() { // `<` / `>` in `std::vector` are `template_argument_list` @@ -10248,6 +10688,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_else_and_ternary_count_conditions() { // `if (cond) ... else ...` + ternary `cond ? a : b`. The @@ -10271,6 +10712,7 @@ function f(int $a, int $b): int { // Fitzpatrick Rule 9 unary conditions, exactly as `java_walk_ternary` // has always counted them. Before the fix the C family scored // `a ? !b : !c` as 1 — the `?` token alone — against Java's 4. + #[cfg(feature = "cpp")] #[test] fn cpp_ternary_operand_slots_count_as_unary_conditions() { // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4. @@ -10311,6 +10753,7 @@ function f(int $a, int $b): int { // lands at child(3) rather than child(4). Addressing the operand // slots by grammar field name — never by index — is what keeps `!b` // counted here; a fixed `child(4)` reads `None` and scores 2. + #[cfg(feature = "cpp")] #[test] fn cpp_elided_ternary_consequence_still_walks_the_alternative() { // `?` (1) + condition `a` (1) + `!b` (1) = 3. @@ -10328,8 +10771,10 @@ function f(int $a, int $b): int { // The expected value is *derived from the C++ run*, not hardcoded, // so the four languages cannot silently drift apart if the C++ // expectation ever legitimately moves. + #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] #[test] fn c_family_ternary_operand_slots_agree_with_cpp() { + #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] const SRC: &str = "void f() { x = a ? !b : !c; }\n"; let conditions = abc_conditions; @@ -10361,6 +10806,7 @@ function f(int $a, int $b): int { // had. Every fixture is a shape only that walker can classify — // a comparison-shaped condition proves nothing, the `<` token arm // counts it either way (grammar-dispatch §11). + #[cfg(feature = "cpp")] #[test] fn cpp_for_condition_slot_counts_unary_conditions() { // Bare identifier: the whole condition, no operator token. @@ -10401,8 +10847,10 @@ function f(int $a, int $b): int { // integration-snapshot coverage at all, making this its only guard. // The expected value is derived from the C++ run rather than // hardcoded, so the four cannot silently drift apart. + #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] #[test] fn c_family_for_condition_slot_agrees_with_cpp() { + #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] const SRC: &str = "void f(int a) { for (; !a; ) {} }\n"; let conditions = abc_conditions; @@ -10427,6 +10875,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_switch_cases_count_default_excluded() { // `case 1`, `case 2` → 2 conditions. `default` is intentionally @@ -10450,6 +10899,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_try_catch_count_conditions() { // `try` and `catch` each add one condition (Fitzpatrick's rule; @@ -10465,6 +10915,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_complex_function_abc() { // Mixed-shape regression: assignments, calls, conditions, @@ -10512,6 +10963,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_if_multiple_conditions() { // Fitzpatrick Rule 9 walker (issue #403): each operand of a @@ -10530,6 +10982,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_while_and_do_while_conditions() { // Exercise both the WhileStatement and DoStatement arms via @@ -10547,6 +11000,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_if_constexpr_condition_counts() { // Regression for the code-review finding: C++ `if constexpr @@ -10569,6 +11023,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_cast_expression_in_logical_chain_counts() { // Regression for findings.md round-2 #1 (C++): @@ -10589,6 +11044,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_qualified_identifier_condition_counts() { // Regression for findings.md #3 (C++): tree-sitter-cpp emits @@ -10610,6 +11066,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_if_boolean_literal_condition() { check_metrics::( @@ -10627,6 +11084,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_methods_arguments_with_conditions() { check_metrics::( @@ -10643,6 +11101,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_return_with_conditions() { check_metrics::( @@ -10667,6 +11126,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_short_circuit_with_boolean_literal_operand() { // `a && true` reports 2 conditions: one for the identifier @@ -10681,6 +11141,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_empty_unit_zero() { // No code → A=B=C=0. Wires up the trait and exercises the @@ -10693,6 +11154,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_plain_and_compound_assignments_count() { // `let` / `var` declarations behave like TypeScript: only a @@ -10711,6 +11173,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_const_initializer_not_assignment() { // `const PI = 3.14` must NOT count as an assignment — its `=` @@ -10728,6 +11191,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_asi_const_does_not_suppress_later_assignments() { // The issue #1277 reproducer verbatim. JavaScript half of the @@ -10748,6 +11212,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_nested_arrow_const_does_not_leak() { // The ASI leak beside a nested space: the arrow body opens its @@ -10770,6 +11235,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_non_declarator_equals_still_count() { // An `=` that does not belong to a `const` declarator is always @@ -10789,6 +11255,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_const_initializer_value_assignments_still_count() { // An `=` inside a `const` initializer's *value* is an @@ -10808,6 +11275,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_const_declarator_shapes_stay_suppressed() { // JavaScript half of @@ -10828,6 +11296,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_increment_and_decrement_count_as_assignment() { // `x++` (post) and `--x` (pre) both update an lvalue and so @@ -10844,6 +11313,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_calls_are_branches() { // `g(1)` is a `call_expression` → B = 1. `new Foo(2)` is a @@ -10859,6 +11329,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_comparisons_count_conditions() { // `==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=` each count @@ -10884,6 +11355,7 @@ function f(int $a, int $b): int { // `tsx_jsx_elements_are_not_conditions` covers scored seven // conditions in a `.js` file too. Same fixture minus the type // annotations, same discriminating numbers: 8 pre-fix, 1 after. + #[cfg(feature = "javascript")] #[test] fn javascript_jsx_elements_are_not_conditions() { check_metrics::( @@ -10904,6 +11376,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_number_truthy_condition_counts() { // Regression for #772: JS treats every non-zero number as @@ -10925,6 +11398,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_number_truthy_condition_counts() { // Regression for #772: TS shares the JS truthy semantics. The @@ -10943,6 +11417,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_nullish_coalescing_counts_condition() { // `a ?? b` is one nullish-coalescing operator → C = 1. @@ -10956,6 +11431,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_else_ternary_case_default_try_catch() { // `else`, `?` (ternary), `case`, `try`, `catch` all count. @@ -10982,6 +11458,7 @@ function f(int $a, int $b): int { // Issue #1102, JS-family half. See // `cpp_ternary_operand_slots_count_as_unary_conditions` for the // rule; the two families were behind Java by the same three units. + #[cfg(feature = "javascript")] #[test] fn javascript_ternary_operand_slots_count_as_unary_conditions() { // `?` (1) + condition `a` (1) + `!b` (1) + `!c` (1) = 4. @@ -11018,6 +11495,7 @@ function f(int $a, int $b): int { // macro body than JavaScript's `js_abc_compute!`, so wiring one and // not the other is a live failure mode; TSX and Mozjs are clones of // these two. + #[cfg(feature = "typescript")] #[test] fn typescript_ternary_operand_slots_count_as_unary_conditions() { check_metrics::( @@ -11037,6 +11515,7 @@ function f(int $a, int $b): int { // The JS grammar marks the `condition` field on both the expression // and the `;` closing it, so `child_by_field_name` is the only // addressing that lands on the expression for every header shape. + #[cfg(feature = "javascript")] #[test] fn javascript_for_condition_slot_counts_unary_conditions() { // Bare identifier: no operator token anywhere in the header. @@ -11081,9 +11560,12 @@ function f(int $a, int $b): int { // wiring one and not the other is a live failure mode; TSX and // Mozjs are the clones of those two. The expected values are // derived from the JavaScript run rather than hardcoded. + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_family_for_condition_slot_agrees_with_javascript() { + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] const BARE: &str = "function f(a) { for (; a; ) {} }\n"; + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] const EMPTY: &str = "function f() { for (;;) { break; } }\n"; let conditions = abc_conditions; @@ -11103,6 +11585,7 @@ function f(int $a, int $b): int { } } + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_family_ternary_operand_slots_agree_with_javascript() { // `a ? !b : !c` is the one shape that tells the ternary walker @@ -11112,6 +11595,7 @@ function f(int $a, int $b): int { // compiles, passes every condition-slot test, and drops only the // two branch operands. TypeScript alone pinned those before; the // other three expansions now do too. + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] const SRC: &str = "function f(a, b, c) { x = a ? !b : !c; }\n"; let javascript = abc_conditions(LANG::Javascript, SRC); // expected: 4 — the `?`, the `a` condition slot and both negated @@ -11129,6 +11613,7 @@ function f(int $a, int $b): int { } } + #[cfg(feature = "javascript")] #[test] fn javascript_instanceof_counts_condition() { // `x instanceof Foo` is a binary expression whose operator is @@ -11143,6 +11628,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_complex_function_abc() { // Mixed-shape regression. Verified by hand: @@ -11185,6 +11671,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_asi_const_does_not_suppress_later_assignments() { // Mozjs half of the #1277 cluster; the fork carries its own @@ -11203,6 +11690,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_const_declarator_shapes_stay_suppressed() { // Mozjs half of @@ -11223,6 +11711,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_complex_function_abc() { // Mozjs shares JavaScript's expression / statement vocabulary; @@ -11258,6 +11747,7 @@ function f(int $a, int $b): int { // ----- JS / TS / Tsx / Mozjs Phase-2B condition slots ----- + #[cfg(feature = "javascript")] #[test] fn javascript_await_expression_condition_counts() { // Regression for findings.md round-2 #2 (JS): @@ -11278,6 +11768,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_member_expression_condition_counts() { // Regression for findings.md #3 (JS-family): tree-sitter- @@ -11302,6 +11793,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_if_boolean_literal_condition() { check_metrics::( @@ -11319,6 +11811,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_methods_arguments_with_conditions() { check_metrics::( @@ -11335,6 +11828,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_return_with_conditions() { check_metrics::( @@ -11351,6 +11845,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_if_boolean_literal_condition() { check_metrics::( @@ -11368,6 +11863,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_methods_arguments_with_conditions() { check_metrics::( @@ -11384,6 +11880,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_return_with_conditions() { check_metrics::( @@ -11398,6 +11895,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_if_boolean_literal_condition() { check_metrics::( @@ -11415,6 +11913,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_methods_arguments_with_conditions() { check_metrics::( @@ -11431,6 +11930,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_return_with_conditions() { check_metrics::( @@ -11445,6 +11945,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_jsx_elements_are_not_conditions() { // #1297 in the second expansion of `js_abc_compute!`. The @@ -11473,6 +11974,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_if_boolean_literal_condition() { check_metrics::( @@ -11490,6 +11992,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_methods_arguments_with_conditions() { check_metrics::( @@ -11506,6 +12009,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_return_with_conditions() { check_metrics::( @@ -11522,6 +12026,7 @@ function f(int $a, int $b): int { // ----- JS / TS / Tsx / Mozjs unary-conditional walker ----- + #[cfg(feature = "javascript")] #[test] fn javascript_if_multiple_conditions() { check_metrics::( @@ -11538,6 +12043,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_while_and_do_while_conditions() { check_metrics::( @@ -11553,6 +12059,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_short_circuit_with_boolean_literal_operand() { check_metrics::( @@ -11565,6 +12072,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_if_multiple_conditions() { check_metrics::( @@ -11581,6 +12089,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_while_and_do_while_conditions() { check_metrics::( @@ -11596,6 +12105,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_short_circuit_with_boolean_literal_operand() { check_metrics::( @@ -11608,6 +12118,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_if_multiple_conditions() { check_metrics::( @@ -11624,6 +12135,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_while_and_do_while_conditions() { check_metrics::( @@ -11639,6 +12151,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_short_circuit_with_boolean_literal_operand() { check_metrics::( @@ -11651,6 +12164,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_if_multiple_conditions() { check_metrics::( @@ -11667,6 +12181,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_while_and_do_while_conditions() { check_metrics::( @@ -11682,6 +12197,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_short_circuit_with_boolean_literal_operand() { check_metrics::( @@ -11696,6 +12212,7 @@ function f(int $a, int $b): int { // ---------- Perl ABC tests ---------- + #[cfg(feature = "perl")] #[test] fn perl_empty_unit_zero() { // Empty source produces zero ABC magnitude — pins the trait @@ -11708,6 +12225,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "perl")] #[test] fn perl_plain_and_compound_assignments_count() { // `my $x = 0` parses as a `binary_expression` with an `=` @@ -11729,6 +12247,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_calls_are_branches() { // `foo()` parses as `call_expression_with_args_with_brackets` @@ -11751,6 +12270,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_method_invocation_counts_as_branch() { // `$obj->method(...)` parses as `method_invocation`. Any @@ -11785,6 +12305,7 @@ function f(int $a, int $b): int { // fixture stops parsing. The assertion is also the grammar-dispatch // §8 pin — `cyclomatic()` is 3 on this space, so decisions is 2 and // the two counts agree exactly. + #[cfg(feature = "perl")] #[test] fn perl_readline_angle_brackets_are_not_conditions() { check_func_space::( @@ -11810,6 +12331,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_numeric_and_string_comparisons_count_conditions() { // Numeric ops `==`, `!=`, `<`, `>`, `<=`, `>=`, `<=>` and @@ -11848,6 +12370,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_short_circuit_not_counted_directly_ternary_counts() { // `&&`, `||`, `//`, low-precedence `and`, `or`, `xor` are @@ -11900,6 +12423,7 @@ function f(int $a, int $b): int { // what carries the tally's +1. tree-sitter-perl names the branch // fields `true` / `false` rather than the C-family `consequence` / // `alternative`, so a copied C-family gate would match nothing. + #[cfg(feature = "perl")] #[test] fn perl_ternary_operand_slots_count_as_unary_conditions() { // ternary (1) + condition `$a` (1) + `!$b` (1) + `!$c` (1) = 4. @@ -11929,6 +12453,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_elsif_and_else_count_conditions() { // `if (… == …) { … } elsif (… < …) { … } else { … }` → @@ -11958,6 +12483,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_regex_match_operators_count_conditions() { // `=~` and `!~` are pattern-match operators; we count both @@ -11976,6 +12502,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_complex_function_abc() { // Mixed program exercising every category. Computed @@ -12017,6 +12544,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_if_multiple_conditions() { // Fitzpatrick Rule 9 walker (issue #403): each operand of a @@ -12039,6 +12567,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_while_and_until_conditions() { // Perl has no `do { ... } while(cond);` shape in this grammar @@ -12059,6 +12588,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_for_header_condition_slot_counts_unary_conditions() { // The Perl half of #1276. The C-style `for` header's condition @@ -12101,6 +12631,7 @@ function f(int $a, int $b): int { } } + #[cfg(feature = "perl")] #[test] fn perl_short_circuit_counts_scalar_variable_operands() { // `$a && $b` reports 2 conditions — one walker count per @@ -12121,6 +12652,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_array_in_binary_operand_descends_to_scalar_context_value() { // Regression test for the code-review findings on the @@ -12156,6 +12688,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_if_scalar_variable_condition() { // Renamed from the cross-language @@ -12178,6 +12711,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_methods_arguments_with_conditions() { // `call(!$a, !$b)` — argument list walker counts each @@ -12201,6 +12735,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "perl")] #[test] fn perl_return_with_conditions() { // `return !$a` reports 1 condition via the walker (unary @@ -12226,6 +12761,7 @@ function f(int $a, int $b): int { // ---------- Lua ABC tests ---------- + #[cfg(feature = "lua")] #[test] fn lua_empty_unit_zero() { check_metrics::("", "empty.lua", |metric| { @@ -12236,6 +12772,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "lua")] #[test] fn lua_assignments_count_locals_and_plain() { // `local x = 0` wraps an `assignment_statement` under a @@ -12261,6 +12798,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "lua")] #[test] fn lua_calls_are_branches() { // `print(x)`, `obj.m(x)`, `obj:m(x)`, `f(g(1))` — every @@ -12283,6 +12821,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "lua")] #[test] fn lua_comparisons_count_logical_ops_do_not() { // Each comparison token contributes one condition; `and` / @@ -12330,6 +12869,7 @@ function f(int $a, int $b): int { // passing on the `a < b` alone. The coverage that the attributes // are reaching the arm is the revert test: removing the gate makes // this the only failing test in the suite. + #[cfg(feature = "lua")] #[test] fn lua_variable_attributes_are_not_conditions() { check_metrics::( @@ -12346,6 +12886,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "lua")] #[test] fn lua_elseif_and_else_count_conditions() { // Each elseif / else arm of the if contributes one @@ -12372,6 +12913,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "lua")] #[test] fn lua_complex_function_abc() { // Combines every category to pin the metric. @@ -12401,6 +12943,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "lua")] #[test] fn lua_if_multiple_conditions() { // Fitzpatrick Rule 9 walker (issue #403). Lua's `and` / `or` @@ -12420,6 +12963,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "lua")] #[test] fn lua_while_conditions() { // Lua has no `do { ... } while(cond);` — `while cond do … @@ -12437,6 +12981,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "lua")] #[test] fn lua_short_circuit_with_boolean_literal_operand() { // `a and true` reports 2 conditions: one Identifier, one @@ -12447,6 +12992,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "lua")] #[test] fn lua_number_truthy_condition_counts() { // Regression for findings.md #2: Lua treats every non-nil, @@ -12475,6 +13021,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "lua")] #[test] fn lua_if_boolean_literal_condition() { check_metrics::( @@ -12492,6 +13039,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "lua")] #[test] fn lua_methods_arguments_with_conditions() { // `m(not a, not b)` — argument list walker counts each @@ -12508,6 +13056,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "lua")] #[test] fn lua_return_with_conditions() { // `return not (z >= 0)` → walker on `not` unwraps the paren @@ -12537,6 +13086,7 @@ function f(int $a, int $b): int { // ---------- Tcl ABC tests ---------- + #[cfg(feature = "tcl")] #[test] fn tcl_empty_unit_zero() { check_metrics::("", "empty.tcl", |metric| { @@ -12547,6 +13097,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "tcl")] #[test] fn tcl_set_command_counts_assignment() { // `set` has its own grammar production; each invocation is @@ -12570,6 +13121,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_incr_append_lappend_count_assignment() { // Variable-mutation commands (`incr`, `append`, `lappend`) @@ -12594,6 +13146,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_qualified_mutator_commands_count_assignment() { // `::incr` is `incr` through the global namespace. Anchored on @@ -12616,6 +13169,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_namespaced_mutator_command_stays_a_branch() { // Control: only the *leading* qualifier names the core command, @@ -12633,6 +13187,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_computed_command_name_is_not_an_assignment() { // A command whose leading word is computed (`$cmd args`) names no @@ -12660,6 +13215,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_generic_commands_are_branches() { // Anything that isn't `set` or a known mutator command @@ -12682,6 +13238,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_comparisons_count_logical_ops_do_not() { // `expr` predicates expose comparison / logical tokens at @@ -12717,6 +13274,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_ternary_counts_condition() { // The `ternary_expr` node is one condition and its condition @@ -12746,6 +13304,7 @@ function f(int $a, int $b): int { /// so `($a) ? !$b : !$c` shifts every operand right by one and /// `child(0)` / `child(2)` / `child(4)` land on `(`, `)` and `?`. /// Without this case the whole fixed-index revert passes. + #[cfg(feature = "tcl")] #[test] fn tcl_parenthesised_ternary_condition_matches_the_bare_form() { let conditions = |source: &str| { @@ -12776,6 +13335,7 @@ function f(int $a, int $b): int { /// neither form counted. Found in review, not by the tests: the /// parenthesised fixtures added with #1180 covered the ternary /// *condition* slot only. + #[cfg(feature = "tcl")] #[test] fn tcl_parenthesised_negated_operands_match_the_bare_form() { let conditions = |source: &str| { @@ -12811,6 +13371,7 @@ function f(int $a, int $b): int { } } + #[cfg(feature = "irules")] #[test] fn irules_abc_parenthesised_negated_operands_match_the_bare_form() { let conditions = |source: &str| { @@ -12830,6 +13391,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_bare_truthy_and_negated_predicates_count_one_condition() { // The headline #1180 fix, on the Tcl side: both were 0 before. @@ -12848,6 +13410,7 @@ function f(int $a, int $b): int { assert_eq!(conditions("proc f {a} {\n while {!$a} { puts x }\n}"), 1); } + #[cfg(feature = "tcl")] #[test] fn tcl_bare_truthy_elseif_predicate_counts_one_condition() { // The `Tcl::Elseif` arm routes its predicate through @@ -12872,6 +13435,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_elseif_and_else_count_conditions() { // `if` / `elseif` / `else` clause productions each @@ -12900,6 +13464,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_if_multiple_conditions() { // Fitzpatrick Rule 9 walker (issue #403). Tcl's `expr` slot @@ -12926,6 +13491,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_while_conditions() { // Tcl has no `do { ... } while(cond);` — `while {…} {…}` is @@ -12947,6 +13513,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_short_circuit_with_boolean_literal_operand() { // `$a && 1` reports 2 conditions: a VariableSubstitution @@ -12968,6 +13535,7 @@ function f(int $a, int $b): int { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_complex_function_abc() { // Mixed program covering every category. Tcl's grammar @@ -13013,6 +13581,7 @@ function f(int $a, int $b): int { } /// The dedicated `set name value` production counts as one assignment. + #[cfg(feature = "irules")] #[test] fn irules_abc_set_assignment() { check_metrics::("when X {\n set x 1\n}\n", "foo.irule", |metric| { @@ -13025,6 +13594,7 @@ function f(int $a, int $b): int { /// Mutator commands (`incr` / `append` / `lappend`) count as /// assignments, not branches — iRules has no assignment operators, so /// mutation is always a command invocation. + #[cfg(feature = "irules")] #[test] fn irules_abc_mutator_commands() { check_metrics::( @@ -13041,6 +13611,7 @@ function f(int $a, int $b): int { /// the leading word in `irules_command_is_assignment` rather than /// through `tcl_command_name`, so this pins the strip on that second /// path (#1381 review). + #[cfg(feature = "irules")] #[test] fn irules_abc_qualified_mutator_commands() { check_metrics::( @@ -13054,6 +13625,7 @@ function f(int $a, int $b): int { } /// Control: `ns::incr` is a proc in `ns`, so it stays a branch. + #[cfg(feature = "irules")] #[test] fn irules_abc_namespaced_mutator_command_stays_a_branch() { check_metrics::("when X {\n ns::incr x\n}\n", "foo.irule", |metric| { @@ -13063,6 +13635,7 @@ function f(int $a, int $b): int { } /// Generic (non-mutator) commands count as branches. + #[cfg(feature = "irules")] #[test] fn irules_abc_branch_commands() { check_metrics::( @@ -13078,6 +13651,7 @@ function f(int $a, int $b): int { /// A numeric comparison (`==`) is one condition; the `log` inside the /// `if` body is one branch. + #[cfg(feature = "irules")] #[test] fn irules_abc_comparison_condition() { check_metrics::( @@ -13093,6 +13667,7 @@ function f(int $a, int $b): int { /// A word-form string comparator (`contains`) is a condition just like /// `==` — iRules-specific (Tcl has only `eq`/`ne`/`in`/`ni`). If /// `contains` were dropped from the condition set this would report 0. + #[cfg(feature = "irules")] #[test] fn irules_abc_string_op_condition() { check_metrics::( @@ -13107,6 +13682,7 @@ function f(int $a, int $b): int { /// Each `elseif` / `else` clause is one condition; the three `set`s are /// assignments. The leading `if` is not itself a condition. + #[cfg(feature = "irules")] #[test] fn irules_abc_elseif_else_conditions() { check_metrics::( @@ -13125,6 +13701,7 @@ function f(int $a, int $b): int { /// A ternary contributes its own condition plus the `>` comparison in /// its test: conditions 2; the `set` is one assignment. + #[cfg(feature = "irules")] #[test] fn irules_abc_ternary_condition() { check_metrics::( @@ -13141,6 +13718,7 @@ function f(int $a, int $b): int { /// but each negated bare operand (`!$a`, `!$b`) in the chain is. Guards /// the `irules_count_unary_conditions` / `irules_inspect_container` /// walker — conditions 2. + #[cfg(feature = "irules")] #[test] fn irules_abc_negated_operands_in_chain() { check_metrics::( @@ -13162,6 +13740,7 @@ function f(int $a, int $b): int { /// deviation table said so. Now the `if` node routes its `expr` /// predicate and the count matches C++'s `if (a)`, which is also 1. /// The `log` command remains the single branch. + #[cfg(feature = "irules")] #[test] fn irules_abc_bare_truthy_counts_one_condition() { check_metrics::( @@ -13179,6 +13758,7 @@ function f(int $a, int $b): int { /// context, so the terminal operand was never counted. Distinct from /// `irules_abc_negated_operands_in_chain`, whose `&&` supplied the /// seed the bare form lacked. + #[cfg(feature = "irules")] #[test] fn irules_abc_negated_bare_truthy_counts_one_condition() { check_metrics::( @@ -13198,6 +13778,7 @@ function f(int $a, int $b): int { /// truthy condition, and one per negated branch — the same value /// Java, C#, Groovy, the C family, the JS family, PHP, Perl, Ruby /// and Python report for the identical expression. + #[cfg(feature = "irules")] #[test] fn irules_abc_ternary_routes_its_operand_slots() { check_metrics::( @@ -13212,6 +13793,7 @@ function f(int $a, int $b): int { /// The #1161 control: a ternary whose condition is a *comparison* /// must not move. The `>` already supplied its condition and the /// branches are unnegated, so routing the slots adds nothing. + #[cfg(feature = "irules")] #[test] fn irules_abc_comparison_ternary_is_unchanged_by_slot_routing() { check_metrics::( @@ -13230,6 +13812,7 @@ function f(int $a, int $b): int { /// fixed-index reading of the slots would shift right by one and /// mis-assign every operand. This is the input that discriminates /// the token-relative location the fix uses. + #[cfg(feature = "irules")] #[test] fn irules_abc_parenthesised_ternary_condition_matches_the_bare_form() { // `check_metrics` takes a bare `fn`, so it cannot carry the @@ -13278,6 +13861,7 @@ function f(int $a, int $b): int { // the guarded members and their control sit at different values, // which is the comparison these tests exist to make. + #[cfg(feature = "rust")] #[test] fn rust_match_guard_scores_one_condition_however_spelled() { let src = "fn is_even(n: i32) -> bool { @@ -13333,6 +13917,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "java")] #[test] fn java_pattern_switch_guard_scores_one_condition_however_spelled() { let src = "class T { @@ -13379,6 +13964,7 @@ function f(int $a, int $b): int { }); } + #[cfg(feature = "python")] #[test] fn python_case_guard_scores_one_condition_however_spelled() { let src = "def is_even(n): @@ -13460,6 +14046,7 @@ def none(x): // change deliberately leaves where it found it. The test is here so // that a later `IfClause` arm added without the field read fails // loudly rather than moving comprehensions silently. + #[cfg(feature = "python")] #[test] fn python_comprehension_if_clause_is_not_a_case_guard() { let src = "def m(xs): @@ -13475,6 +14062,7 @@ def none(x): }); } + #[cfg(feature = "ruby")] #[test] fn ruby_in_clause_guard_scores_one_condition_however_spelled() { let src = "def is_even(x) @@ -13579,6 +14167,7 @@ end // §5 double count with the `>` token arm. A whole-branch review of // the batch found it; all three rows now read 2, level with the // sibling fixtures above and with their own decision counts. + #[cfg(feature = "elixir")] #[test] fn elixir_guard_is_a_decision_however_spelled() { let src = "defmodule T do @@ -13669,6 +14258,7 @@ end // stand in for it — deleting the `in` arm leaves `membership` and // `nonmembership` alone failing, deleting the `not` recognition // leaves `negated` alone failing. + #[cfg(feature = "elixir")] #[test] fn elixir_guard_scores_one_however_spelled() { let src = "defmodule T do @@ -13768,6 +14358,7 @@ end // `left` (`clause` / `clause_or`). `single` is the control the // repeated rows must sit *above*, which is the comparison that // failed before. + #[cfg(feature = "elixir")] #[test] fn elixir_repeated_guard_matches_the_or_chain() { let src = "defmodule T do @@ -13849,6 +14440,7 @@ end // Both members carry the same `@spec`; only `guarded` carries a real // head guard, so the difference between the two rows is the guard // and nothing else. + #[cfg(feature = "elixir")] #[test] fn elixir_typespec_when_is_not_a_guard() { let src = "defmodule T do @@ -13925,6 +14517,7 @@ end // module's rows to the two guards and nothing else, and the guard // bodies are calls (`is_integer/1`, `is_atom/1`) rather than // comparisons so neither can supply a condition of its own. + #[cfg(feature = "elixir")] #[test] fn elixir_defguard_head_is_a_guard() { let src = "defmodule T do @@ -14019,6 +14612,7 @@ end // must stay at 0. It is the reason those three count the node and // not the token. + #[cfg(feature = "csharp")] #[test] fn csharp_is_tests_score_outside_a_boolean_slot() { let src = "class A { @@ -14058,6 +14652,7 @@ end }); } + #[cfg(feature = "java")] #[test] fn java_instanceof_scores_outside_a_boolean_slot() { let src = "class A { @@ -14098,6 +14693,7 @@ end // belonged in the terminal set — that set holds operands — but it // is a relational operator, and `LTEQGT` is already a condition // token in Ruby, PHP, C++ and Mozcpp. Groovy was the outlier at 0. + #[cfg(feature = "groovy")] #[test] fn groovy_relational_productions_score_outside_a_boolean_slot() { let src = "class A { @@ -14149,6 +14745,7 @@ end }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_is_and_in_score_outside_a_boolean_slot() { let src = "class K { @@ -14182,6 +14779,7 @@ end ); } + #[cfg(feature = "ruby")] #[test] fn ruby_test_pattern_scores_outside_a_boolean_slot() { let src = "def out_in(a) @@ -14245,6 +14843,7 @@ end // genuinely both binds a name and decides a branch. Without this // row a later reader has no way to tell the intent from an // oversight. + #[cfg(feature = "python")] #[test] fn python_walrus_is_a_unary_condition() { let src = "def in_slot(g): @@ -14299,6 +14898,7 @@ def ctrl(g): // serde's one changed snapshot is `if cfg!(no_underscore_consts)`, // not a `matches!` — so asserting only `matches!` would leave the // measured case uncovered. + #[cfg(feature = "rust")] #[test] fn rust_macro_invocation_is_a_unary_condition() { let src = "fn in_slot(x: Option) -> u8 { if matches!(x, Some(_)) { 1 } else { 0 } } diff --git a/src/metrics/abc/cpp.rs b/src/metrics/abc/cpp.rs index b07149af3..f7983815e 100644 --- a/src/metrics/abc/cpp.rs +++ b/src/metrics/abc/cpp.rs @@ -339,6 +339,7 @@ mod tests { // the per-language `compute` paths — which also pins behaviour the // whole-source integration tests reach only transitively. + #[cfg(feature = "cpp")] fn parse(src: &str) -> CppParser { CppParser::new( src.as_bytes().to_vec(), @@ -352,12 +353,14 @@ mod tests { // on the metric walk (#1096). These tests reach their container by // search rather than by descent, so the authoritative lookup is what // supplies it here. + #[cfg(feature = "cpp")] fn parent_of<'a>(node: &Node<'a>) -> Node<'a> { node.parent() .expect("every fixture below places its container under a parent node") } // First node in pre-order (document order) whose kind name is `kind`. + #[cfg(feature = "cpp")] fn first_of_kind<'a>(node: Node<'a>, kind: &str) -> Option> { let mut stack = vec![node]; while let Some(n) = stack.pop() { @@ -377,6 +380,7 @@ mod tests { // and counts each boolean-terminal operand once. `a` and `b` are both // `identifier`s (members of `cpp_bool_terminal_kinds!`) and the `&&` // token is anonymous, so the count is exactly 2. + #[cfg(feature = "cpp")] #[test] fn count_unary_conditions_counts_each_boolean_operand() { let p = parse("int f(int a, int b) { return a && b; }"); @@ -390,6 +394,7 @@ mod tests { // `if (a)`: the `condition_clause` wraps `( a )`. `cpp_inspect_container` // seeds boolean context from the `if_statement` parent, unwraps the // parens to the `a` identifier terminal, and counts it once. + #[cfg(feature = "cpp")] #[test] fn inspect_container_counts_parenthesized_condition() { let p = parse("void f(int a) { if (a) {} }"); @@ -402,6 +407,7 @@ mod tests { // `if (((a)))`: the unwrap loop strips every parenthesis layer and // counts the single terminal `a` exactly once — not once per paren. + #[cfg(feature = "cpp")] #[test] fn inspect_container_unwraps_nested_parens_once() { let p = parse("void f(int a) { if (((a))) {} }"); @@ -415,6 +421,7 @@ mod tests { // `if (!a)`: the leading `!` drives the `is_not` branch, which marks the // unwrap chain as boolean content before reaching the `a` terminal, so // the negated operand is counted once. + #[cfg(feature = "cpp")] #[test] fn inspect_container_counts_negated_condition() { let p = parse("void f(int a) { if (!a) {} }"); @@ -429,6 +436,7 @@ mod tests { // initializer, not a condition, so the `has_boolean_content` guard // stays false and the unwrapped `a` terminal is NOT counted. This // guard branch is awkward to reach through the full `compute` path. + #[cfg(feature = "cpp")] #[test] fn inspect_container_ignores_non_boolean_context() { let p = parse("int g(int a) { int x = (a); return x; }"); diff --git a/src/metrics/abc/csharp.rs b/src/metrics/abc/csharp.rs index 64fe1f462..b77fd9d9c 100644 --- a/src/metrics/abc/csharp.rs +++ b/src/metrics/abc/csharp.rs @@ -778,6 +778,7 @@ mod tests { /// way to reach its `false` arm, and the walker does hand out short /// chains at the root. Pinning it keeps a future "simplify the /// climb" from turning a missing ancestor into a counted `const`. + #[cfg(feature = "csharp")] #[test] fn const_predicate_fails_closed_on_a_truncated_chain() { let source = b"class A { const int x = 1; }"; diff --git a/src/metrics/cognitive.rs b/src/metrics/cognitive.rs index f0791229a..94a311ca9 100644 --- a/src/metrics/cognitive.rs +++ b/src/metrics/cognitive.rs @@ -709,6 +709,7 @@ mod tests { /// `else_clause` through the parent, Java through the preceding /// `else` token, which the chain answers by scanning the parent's /// children. + #[cfg(all(feature = "c", feature = "java"))] #[test] fn else_if_is_recognised_at_every_nesting_depth() { use crate::test_support::metrics_verbatim; @@ -716,6 +717,7 @@ mod tests { // 1 for the `if`, plus 1 for each `else if` as a branch // extension. No nesting penalty: an `else if` continues the // chain rather than nesting inside it. + #[cfg(any(feature = "c", feature = "java"))] const CHAIN_COGNITIVE: u64 = 3; let chain = "if (a) { } else if (b) { } else if (c) { }"; @@ -750,6 +752,7 @@ mod tests { assert_eq!(stats.cognitive_min(), 0); } + #[cfg(feature = "python")] #[test] fn python_no_cognitive() { check_metrics::("a = 42", "foo.py", |metric| { @@ -768,6 +771,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn rust_no_cognitive() { check_metrics::("let a = 42;", "foo.rs", |metric| { @@ -786,6 +790,7 @@ mod tests { }); } + #[cfg(feature = "c")] #[test] fn c_no_cognitive() { check_metrics::("int a = 42;", "foo.c", |metric| { @@ -804,6 +809,7 @@ mod tests { }); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_no_cognitive() { check_metrics::("var a = 42;", "foo.js", |metric| { @@ -822,6 +828,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_no_cognitive() { check_metrics::("var a = 42;", "foo.js", |metric| { @@ -840,6 +847,7 @@ mod tests { }); } + #[cfg(feature = "python")] #[test] fn python_simple_function() { check_metrics::( @@ -871,6 +879,7 @@ mod tests { /// `switch_statement` do. A 2-arm match with one explicit arm /// plus a wildcard contributes one cognitive decision point. /// Regression test for #212. + #[cfg(feature = "python")] #[test] fn python_match_two_arm_wildcard() { check_metrics::( @@ -902,6 +911,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_expression_statement() { // Boolean expressions containing `And` and `Or` operators were not @@ -927,6 +937,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_tuple() { // Boolean expressions containing `And` and `Or` operators were not @@ -952,6 +963,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_elif_function() { // Boolean expressions containing `And` and `Or` operators were not @@ -980,6 +992,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_more_elifs_function() { // Boolean expressions containing `And` and `Or` operators were not @@ -1010,6 +1023,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_if_elif_elif_else_chain() { // Regression for #274: `if/elif/elif/else` must score as a flat @@ -1049,6 +1063,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_else_if_chain_matches_elif() { // Regression for #276: `else: if x:` (no `elif`) is semantically @@ -1085,6 +1100,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_try_except_finally_finally_is_free() { // Regression for #416: a `finally` clause is structured cleanup that @@ -1119,6 +1135,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_try_except_matches_try_except_finally() { // Companion to #416: try/except (no finally) scores the same as the @@ -1150,6 +1167,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_comprehension_matches_explicit_loop() { // Regression for #417: a list comprehension's `for`/`if` clauses must @@ -1186,6 +1204,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_comprehension_plain_no_filter() { // A comprehension with no `if` filter scores just the loop. @@ -1202,6 +1221,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_comprehension_nested_for() { // Two `for` clauses are nested loops: the second nests under the @@ -1219,6 +1239,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_comprehension_multiple_filters() { // Each `if` filter is an independent condition nested under the for. @@ -1237,6 +1258,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_comprehension_variants_consistent() { // dict / set / generator comprehensions reuse the same for_in_clause / @@ -1261,6 +1283,7 @@ mod tests { } } + #[cfg(feature = "python")] #[test] fn python_comprehension_nested_in_element() { // Regression for #421: a comprehension in another comprehension's @@ -1297,6 +1320,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_comprehension_three_levels_nested() { // Three comprehensions nested through each other's element positions @@ -1329,6 +1353,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_generator_in_comprehension_element() { // #421 edge case: a generator passed to a call (`sum(...)`) in a @@ -1358,6 +1383,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_try_finally_no_except_is_free() { // #416: try/finally with no except clause scores 0 — neither the try @@ -1389,6 +1415,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_constructs_inside_finally_still_count() { // #416 guard: making `finally` free must not make its body invisible. @@ -1422,6 +1449,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_simple_function() { check_metrics::( @@ -1451,6 +1479,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_simple_function() { check_metrics::( @@ -1480,6 +1509,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_simple_function() { check_metrics::( @@ -1509,6 +1539,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_simple_function() { check_metrics::( @@ -1538,6 +1569,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_sequence_same_booleans() { check_metrics::( @@ -1562,6 +1594,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_sequence_same_booleans() { check_metrics::( @@ -1616,6 +1649,7 @@ mod tests { // node rather than a `BinaryExpression`. Before #396 these // tokens were invisible to the cognitive boolean-sequence // counter (cyclomatic already counted them via AMPAMP). + #[cfg(feature = "rust")] #[test] fn rust_let_chain_sequence_booleans() { // expected: +1 for the `if`, +1 for the chain of two `&&` @@ -1647,6 +1681,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_let_chain_vs_nested_if_let() { // Companion to `rust_let_chain_sequence_booleans`. The nested @@ -1681,6 +1716,7 @@ mod tests { ); } + #[cfg(all(feature = "c", feature = "cpp"))] #[test] fn c_sequence_same_booleans() { check_metrics::( @@ -1730,6 +1766,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_sequence_same_booleans() { check_metrics::( @@ -1779,6 +1816,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_not_booleans() { check_metrics::( @@ -1857,6 +1895,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_not_does_not_affect_boolean_sequence_392() { // Regression test for issue #392: `!` does not affect cognitive @@ -1920,6 +1959,7 @@ mod tests { ); } + #[cfg(all(feature = "c", feature = "cpp"))] #[test] fn c_not_booleans() { // `!` does not break boolean sequences (issue #392): the inner @@ -1972,6 +2012,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_not_booleans() { // `!` does not break boolean sequences (issue #392): inner `&&` @@ -2025,6 +2066,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_sequence_different_booleans() { check_metrics::( @@ -2049,6 +2091,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_sequence_different_booleans() { check_metrics::( @@ -2075,6 +2118,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_sequence_different_booleans() { check_metrics::( @@ -2101,6 +2145,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_sequence_different_booleans() { check_metrics::( @@ -2127,6 +2172,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_formatted_sequence_different_booleans() { check_metrics::( @@ -2154,6 +2200,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_1_level_nesting() { check_metrics::( @@ -2179,6 +2226,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_1_level_nesting() { check_metrics::( @@ -2241,6 +2289,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_1_level_nesting() { check_metrics::( @@ -2277,6 +2326,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_1_level_nesting() { check_metrics::( @@ -2313,6 +2363,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_nesting() { check_metrics::( @@ -2343,6 +2394,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_2_level_nesting() { check_metrics::( @@ -2369,6 +2421,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_2_level_nesting() { check_metrics::( @@ -2400,6 +2453,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_try_construct() { check_metrics::( @@ -2428,6 +2482,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_flat_try_except() { // Regression for #242: flat try/except at function top level @@ -2462,6 +2517,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_except_inside_if() { // Regression for #242: try/except nested inside an `if` must @@ -2497,6 +2553,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_except_inside_for() { // Regression for #242: try/except nested inside a `for` must @@ -2528,6 +2585,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_multi_except_inside_if() { // Regression for #242: every clause in a multi-except chain @@ -2566,6 +2624,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_try_construct() { check_metrics::( @@ -2600,6 +2659,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_try_construct() { check_metrics::( @@ -2641,6 +2701,7 @@ mod tests { // bump that splits `for...of` into its own node kind would surface // here rather than silently scoring `for...of` loops as 0 cognitive. + #[cfg(feature = "javascript")] #[test] fn javascript_for_of_loop() { check_metrics::( @@ -2671,6 +2732,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_for_of_loop() { check_metrics::( @@ -2701,6 +2763,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_for_of_loop() { check_metrics::( @@ -2731,6 +2794,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_for_of_loop() { check_metrics::( @@ -2761,6 +2825,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_break_continue() { // Only labeled break and continue statements are considered @@ -2802,6 +2867,7 @@ mod tests { // (LoopExpression) distinct from WhileExpression. The cognitive nesting // arm previously matched only For/While/Match, so `loop {}` silently // contributed neither a structural +1 nor a nesting bump. + #[cfg(feature = "rust")] #[test] fn rust_loop_single() { check_metrics::( @@ -2834,6 +2900,7 @@ mod tests { // Regression for #389: nested `loop` blocks must accrue nesting just // like nested `while`/`for` would. + #[cfg(feature = "rust")] #[test] fn rust_loop_nested() { check_metrics::( @@ -2866,6 +2933,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_nested_function_resets_nesting_and_adds_depth() { // Regression for #696: a method defined on a local struct declared @@ -2901,6 +2969,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_goto() { check_metrics::( @@ -2931,6 +3000,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_switch() { check_metrics::( @@ -2968,6 +3038,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_ternary() { // Sonar's rule scores the ternary `?:` as +1 (and +nesting), matching @@ -3003,6 +3074,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_try_catch_single() { check_metrics::( @@ -3034,6 +3106,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_try_multiple_catches() { check_metrics::( @@ -3069,6 +3142,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_try_catch_in_loop() { check_metrics::( @@ -3102,6 +3176,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_range_based_for() { check_metrics::( @@ -3135,6 +3210,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_nested_range_based_for() { check_metrics::( @@ -3167,6 +3243,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_nested_for() { check_metrics::( @@ -3200,6 +3277,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_nested_while() { check_metrics::( @@ -3232,6 +3310,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_recursion() { // Sonar's rule scores each recursive call to the enclosing function @@ -3267,6 +3346,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_goto_sibling_jump() { check_metrics::( @@ -3303,6 +3383,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_lambda_inside_function() { // Per `increase_nesting`, entering a lambda bumps the effective nesting @@ -3349,6 +3430,7 @@ mod tests { /// `mozcpp`'s `LambdaExpression` arm had no cognitive test before /// this, so the whole arm measured zero-coverage even though the /// fork is expected to stay metric-equivalent to `cpp`. + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_lambda_inside_function() { check_metrics::( @@ -3369,6 +3451,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_switch_fall_through() { // A `case` without `break` (fall-through) does not add cognitive cost @@ -3409,6 +3492,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_switch_in_loop() { check_metrics::( @@ -3448,6 +3532,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_macro_expanded_control_flow() { // Per the file-level comment in `cognitive.rs`, macro expansion is not @@ -3483,6 +3568,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_switch() { check_metrics::( @@ -3520,6 +3606,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_switch() { check_metrics::( @@ -3554,6 +3641,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_ternary_operator() { check_metrics::( @@ -3591,6 +3679,7 @@ mod tests { /// asserting on the arms — the one arm that can differ, /// `ExpressionList`, is discriminated by /// `python_boolean_in_expression_list_under_lambda` (#1090). + #[cfg(feature = "python")] #[test] fn python_boolean_in_lambda_scores_under_each_enclosing_statement() { use crate::test_support::metrics_verbatim; @@ -3654,6 +3743,7 @@ mod tests { /// stays at 3 (#1090). Whether 1 or 2 is the *right* score is a /// separate question — this pins current behaviour, and the /// per-lambda surcharge itself is under review in #1150. + #[cfg(feature = "python")] #[test] fn python_boolean_in_expression_list_under_lambda() { use crate::test_support::metrics_verbatim; @@ -3680,6 +3770,7 @@ mod tests { } } + #[cfg(feature = "python")] #[test] fn python_nested_functions_lambdas() { check_metrics::( @@ -3733,8 +3824,10 @@ mod tests { /// two levels the correct answer stays 2 while an unreset /// implementation gives 4 (Python, which also bumps depth) or 3 /// (depth dropped as well). + #[cfg(all(feature = "java", feature = "python"))] #[test] fn python_nested_def_inside_conditional_scores_like_java() { + #[cfg(any(feature = "java", feature = "python"))] fn cognitive_of(space: &FuncSpace, name: &str) -> u64 { function_space(space, name).metrics.cognitive.cognitive() } @@ -3780,6 +3873,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_real_function() { check_metrics::( @@ -3815,6 +3909,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_if_let_else_if_else() { check_metrics::( @@ -3846,6 +3941,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_if_else_if_else() { check_metrics::( @@ -3879,6 +3975,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_no_cognitive() { check_metrics::("int a = 42;", "foo.java", |metric| { @@ -3897,6 +3994,7 @@ mod tests { }); } + #[cfg(feature = "java")] #[test] fn java_single_branch_function() { check_metrics::( @@ -3925,6 +4023,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_multiple_branch_function() { check_metrics::( @@ -3959,6 +4058,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_compound_conditions() { check_metrics::( @@ -3990,6 +4090,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_switch_statement() { check_metrics::( @@ -4025,6 +4126,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_switch_expression() { check_metrics::( @@ -4055,6 +4157,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_not_booleans() { // `!` does not break boolean sequences (issue #392): pre-order @@ -4087,6 +4190,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_enhanced_for_statement() { check_metrics::( @@ -4122,6 +4226,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_nested_enhanced_for_statement() { check_metrics::( @@ -4156,6 +4261,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_ternary() { // Java's ternary `?:` (grammar `ternary_expression`) is a @@ -4188,6 +4294,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_nested_ternary() { // Nested ternaries inside an `if` block compound by nesting, @@ -4223,6 +4330,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_nested_method_resets_nesting_and_adds_depth() { // Regression for #696: a local-class method declared two `if`s deep @@ -4270,6 +4378,7 @@ mod tests { /// while `class R` scores 0 of its own. Both halves are asserted: /// checking only the new space would still pass if the class kept a /// duplicate count of the same two branches. + #[cfg(feature = "java")] #[test] fn java_record_compact_constructor_opens_function_space() { check_func_space::( @@ -4318,6 +4427,7 @@ mod tests { /// count as `f`'s enclosing function. /// expected: `f`'s `if` is +1 base +1 depth = 2. Without the `stops` /// entry the surcharge is 0 and it scores 1. + #[cfg(feature = "java")] #[test] fn java_record_compact_constructor_is_a_function_boundary() { check_func_space::( @@ -4370,6 +4480,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_labeled_break_continue() { // Per SonarSource Cognitive Complexity §B2 (issue #225), labeled @@ -4411,6 +4522,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_unlabeled_break_continue_not_counted() { // Negative test for issue #225: plain `break;` / `continue;` are @@ -4446,6 +4558,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_no_cognitive() { check_metrics::("int a = 42;", "foo.cs", |metric| { @@ -4464,6 +4577,7 @@ mod tests { }); } + #[cfg(feature = "csharp")] #[test] fn csharp_single_branch_function() { check_metrics::( @@ -4484,6 +4598,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_multiple_branch_function() { check_metrics::( @@ -4509,6 +4624,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_compound_conditions() { check_metrics::( @@ -4532,6 +4648,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_switch_statement() { check_metrics::( @@ -4560,6 +4677,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_switch_expression() { check_metrics::( @@ -4581,6 +4699,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_not_booleans() { // `!` does not break boolean sequences (issue #392): pre-order @@ -4604,6 +4723,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_ternary() { // C#'s ternary `?:` (grammar `conditional_expression`) is a @@ -4635,6 +4755,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_nested_ternary() { // Nested ternaries inside an `if` compound by nesting (mirrors @@ -4670,6 +4791,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_local_function_in_if_does_not_inherit_nesting() { // Regression for #696 (the acute C# case): a `local_function_statement` @@ -4708,6 +4830,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_goto_statement() { // Per SonarSource Cognitive Complexity §B2 (issue #225), any `goto` @@ -4743,6 +4866,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_goto_case_and_default() { // `goto case` and `goto default` inside a `switch` are also @@ -4780,6 +4904,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_unlabeled_break_not_counted() { // Negative test for issue #225: C#'s grammar does not allow @@ -4816,6 +4941,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_no_cognitive() { check_metrics::("my $a = 42;", "foo.pl", |metric| { @@ -4831,6 +4957,7 @@ mod tests { }); } + #[cfg(feature = "perl")] #[test] fn perl_simple_function() { check_metrics::( @@ -4852,6 +4979,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_sequence_same_booleans() { check_metrics::( @@ -4875,6 +5003,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_sequence_different_booleans() { check_metrics::( @@ -4898,6 +5027,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_compound_short_circuit_assignment_249() { // Regression for issue #249: `&&=`, `||=`, `//=` are compound @@ -4934,6 +5064,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_not_booleans() { // `!` does not break boolean sequences (issue #392): pre-order @@ -4960,6 +5091,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_1_level_nesting() { check_metrics::( @@ -4985,6 +5117,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_2_level_nesting() { check_metrics::( @@ -5012,6 +5145,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_break_continue() { // Perl's `last`/`next` are loop-control statements; per Sonar's @@ -5039,6 +5173,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_if_elsif_else() { check_metrics::( @@ -5066,6 +5201,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_function_definition_without_sub_depth() { // Regression: FunctionDefinitionWithoutSub must be a stop in @@ -5094,6 +5230,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_goto_single_increment() { // Regression (#450): `goto LABEL;` parses as `goto_expression` @@ -5116,6 +5253,7 @@ mod tests { }); } + #[cfg(feature = "perl")] #[test] fn perl_labeled_loop_control() { // Regression (#450): the jump target of `last/next/redo LABEL` is @@ -5146,6 +5284,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_bare_loop_control_zero() { // Bare `last;` / `next;` / `redo;` have no `Identifier` jump-target @@ -5173,6 +5312,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_nested_if_for_with_booleans() { check_metrics::( @@ -5203,6 +5343,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_nested_if_with_boolean_sequence() { check_metrics::( @@ -5232,6 +5373,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_try_catch_with_nesting() { check_metrics::( @@ -5266,6 +5408,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_cognitive_control_flow() { check_metrics::( @@ -5307,6 +5450,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_no_cognitive() { check_metrics::("fun main() { val x = 42 }", "foo.kt", |metric| { @@ -5322,6 +5466,7 @@ mod tests { }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_simple_if_with_boolean() { check_metrics::( @@ -5341,6 +5486,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_nesting() { check_metrics::( @@ -5368,6 +5514,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_when_expression() { check_metrics::( @@ -5387,6 +5534,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_when_else_no_increment() { check_metrics::( @@ -5412,6 +5560,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_labeled_break_continue() { // Regression (#450): tree-sitter-kotlin-ng has no break/continue @@ -5444,6 +5593,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_labeled_nonjump_expression_not_counted() { // Regression (#450 follow-up): tree-sitter-kotlin-ng models ANY @@ -5460,6 +5610,7 @@ mod tests { }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_else_in_if_still_increments() { check_metrics::( @@ -5485,6 +5636,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_else_if_chain() { check_metrics::( @@ -5510,6 +5662,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_lambda_nesting() { check_metrics::( @@ -5529,6 +5682,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_secondary_constructor_depth() { // Regression: SecondaryConstructor must be a stop in increment_function_depth so @@ -5558,6 +5712,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_no_cognitive() { check_metrics::("package main\nvar x = 42", "foo.go", |metric| { @@ -5576,6 +5731,7 @@ mod tests { }); } + #[cfg(feature = "go")] #[test] fn go_simple_function() { check_metrics::( @@ -5606,6 +5762,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_nesting() { check_metrics::( @@ -5637,6 +5794,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_switch() { check_metrics::( @@ -5669,6 +5827,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_goto() { check_metrics::( @@ -5698,6 +5857,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_else_if_chain() { check_metrics::( @@ -5729,6 +5889,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_labeled_break_continue() { check_metrics::( @@ -5761,6 +5922,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_method_declaration() { // Coverage: MethodDeclaration is processed as a function boundary (nesting @@ -5792,6 +5954,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_no_cognitive() { check_metrics::("a=42", "foo.sh", |metric| { @@ -5810,6 +5973,7 @@ mod tests { }); } + #[cfg(feature = "bash")] #[test] fn bash_simple_if() { check_metrics::( @@ -5836,6 +6000,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_if_elif_else() { check_metrics::( @@ -5866,6 +6031,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_nested_loops() { check_metrics::( @@ -5894,6 +6060,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_until_loop() { // `until` parses to `Bash::WhileStatement`; this test pins that @@ -5923,6 +6090,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_case() { // `case` adds +1 nesting; case arms do not contribute extra cognitive @@ -5953,6 +6121,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_arithmetic_ternary_increases_nesting() { // Regression for #1268: Bash's only ternary form scored zero @@ -5985,6 +6154,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_nested_arithmetic_ternary_charges_the_inner_one_twice() { // A ternary inside a ternary is +1 for the outer and +2 for the @@ -6012,6 +6182,7 @@ mod tests { ); } + #[cfg(feature = "bash")] #[test] fn bash_boolean_sequence() { // First if: a chain of `&&` is one boolean increment regardless of @@ -6046,6 +6217,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_no_cognitive() { // No proc, no control flow → cognitive complexity is zero everywhere. @@ -6056,6 +6228,7 @@ mod tests { }); } + #[cfg(feature = "tcl")] #[test] fn tcl_simple_function() { // proc with one if and one &&: if(+1) + &&(+1) = 2. @@ -6074,6 +6247,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_sequence_same_booleans() { // Sequences of the same boolean operator count as a single increment. @@ -6097,6 +6271,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_sequence_different_booleans() { // Switching operator type increments again: `$a && $b || $c` → +2 (one &&, one ||). @@ -6116,6 +6291,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_not_booleans() { // `!` does not contribute cognitive cost on its own (issue @@ -6137,6 +6313,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_1_level_nesting() { // while(+1) then if at depth 1 (+2) = 3 for the proc. @@ -6158,6 +6335,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_2_level_nesting() { // while(+1) + foreach at depth 1 (+2) + if at depth 2 (+3) = 6. @@ -6181,6 +6359,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_catch_cognitive() { // `catch` is a conditional handler: +1 at nesting 0, then body at nesting 1. @@ -6203,6 +6382,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_if_elseif_else() { // if(+1) + elseif(+1) + else(+1) = 3; nesting does not increase for elseif/else. @@ -6226,6 +6406,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_not_booleans_nested() { // `$a && !($b && $c)`: `!` does not break boolean sequences @@ -6246,6 +6427,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_not_booleans_double_nested() { // `!($a || $b) && !($c || $d)`: the two `||` sub-expressions and @@ -6269,6 +6451,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_nested_procedure_cognitive() { // Inner proc is at depth=1; its `if` adds +1+1=2 instead of +1+0=1. @@ -6291,6 +6474,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_ternary_cognitive() { // Ternary `? :` inside expr is a conditional expression: adds +1+depth. @@ -6312,6 +6496,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_switch_cognitive() { // Tcl `switch` is a generic command, not a dedicated kind. As a @@ -6335,6 +6520,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_switch_cognitive_nested() { // A `switch` nested inside an outer `switch` arm pays the nesting @@ -6360,6 +6546,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_switch_split_form_adds_no_cognitive() { // The split arm form passes each arm body as its own sibling @@ -6382,6 +6569,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_for_cognitive() { // Tcl `for` is a generic command — the grammar has no `for` rule — @@ -6402,6 +6590,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_qualified_for_resolves_to_the_builtin() { // `::for` is `for` reached through the global namespace, so it @@ -6424,6 +6613,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_namespaced_for_is_not_the_builtin() { // Control for the test above: only the *leading* qualifier names @@ -6445,6 +6635,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_for_cognitive_nested() { // The `for` also nests its body: constructs inside it pay the @@ -6466,6 +6657,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_for_cognitive_name_gate() { // The detection reads the command's `name` field: a command whose @@ -6485,6 +6677,7 @@ mod tests { ); } + #[cfg(all(feature = "irules", feature = "tcl"))] #[test] fn tcl_irules_for_parity() { // iRules models `for` as a dedicated kind counted by the kind @@ -6519,6 +6712,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_try_on_error_cognitive() { // Tcl `try`'s `on error` handler is a conditional error path: @@ -6543,6 +6737,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_try_finally_only_cognitive() { // A `try` with only a `finally` has no conditional path and must @@ -6564,6 +6759,7 @@ mod tests { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_try_handler_nesting_cognitive() { // Only the handler body nests (issue #1266): the `try` body and @@ -6599,6 +6795,7 @@ mod tests { ); } + #[cfg(feature = "irules")] #[test] fn irules_try_trap_cognitive() { // iRules wraps each `try` handler in a dedicated `on_handler` / @@ -6627,6 +6824,7 @@ mod tests { ); } + #[cfg(all(feature = "irules", feature = "tcl"))] #[test] fn tcl_irules_try_parity() { // The same single-handler `try` must score identically in Tcl @@ -6666,6 +6864,7 @@ mod tests { /// re-run the mistake #1266 fixed, where the handlers were read as /// `when`-style event handlers and opened function spaces of their /// own. Nothing else in the suite would go red. + #[cfg(feature = "irules")] #[test] fn irules_try_handler_kinds_appear_only_under_try() { use std::path::PathBuf; @@ -6725,6 +6924,7 @@ mod tests { ); } + #[cfg(feature = "lua")] #[test] fn lua_cognitive_no_cognitive() { // Top-level local assignment, no control flow → cognitive complexity is zero. @@ -6744,6 +6944,7 @@ mod tests { }); } + #[cfg(feature = "lua")] #[test] fn lua_cognitive_simple_function() { // Two `if … and …` statements at function scope: each contributes @@ -6775,6 +6976,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_cognitive_sequence_same_booleans() { // Sequences of the same boolean operator count as a single increment. @@ -6807,6 +7009,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_cognitive_not_booleans() { // `not a and not b`: `not` does not contribute cognitive cost @@ -6836,6 +7039,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_cognitive_sequence_different_booleans() { // Switching operator type increments again: `a and b or c` @@ -6864,6 +7068,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_cognitive_1_level_nesting() { // for at depth 0 (+1) + if at depth 1 (+2) = 3. @@ -6893,6 +7098,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_cognitive_2_level_nesting() { // outer for (+1) + inner for at depth 1 (+2) + if at depth 2 (+3) = 6. @@ -6924,6 +7130,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_cognitive_break_continue() { // Lua's `break` is always unlabeled (the grammar has no labeled @@ -6956,6 +7163,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_cognitive_goto_counted() { // `goto label` is a genuinely unstructured jump and adds +1 per @@ -6985,6 +7193,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_cognitive_elseif_nesting() { // Lua-specific: `elseif_statement` is a dedicated grammar node that @@ -7020,6 +7229,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_switch_statement() { check_metrics::( @@ -7042,6 +7252,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_no_cognitive() { check_metrics::( @@ -7057,6 +7268,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_no_cognitive() { check_metrics::( @@ -7072,6 +7284,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_simple_if() { check_metrics::( @@ -7090,6 +7303,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_boolean_sequence() { check_metrics::( @@ -7105,6 +7319,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_2_level_nesting() { check_metrics::( @@ -7126,6 +7341,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_else_if_chain() { check_metrics::( @@ -7148,6 +7364,7 @@ end", ); } + #[cfg(feature = "javascript")] #[test] fn js_sibling_bool_sequences() { // (a&&b)||(c&&d) — the right-hand && is a *new* sequence (sibling, not nested), @@ -7167,6 +7384,7 @@ end", ); } + #[cfg(feature = "javascript")] #[test] fn js_nested_bool_same_op() { // a||(b&&c&&d) — the inner && operators are nested inside ||, so they form @@ -7184,6 +7402,7 @@ end", ); } + #[cfg(feature = "python")] #[test] fn python_sibling_bool_sequences() { // Python uses keyword boolean operators (`and`/`or`), routed through a @@ -7204,6 +7423,7 @@ end", ); } + #[cfg(feature = "python")] #[test] fn python_nested_bool_same_op() { // a or (b and c and d) — the inner `and` operators are nested inside `or`, @@ -7221,6 +7441,7 @@ end", ); } + #[cfg(feature = "perl")] #[test] fn perl_sibling_bool_sequences() { // Perl uses `compute_perl_booleans` (a separate function supporting five @@ -7242,6 +7463,7 @@ end", ); } + #[cfg(feature = "perl")] #[test] fn perl_nested_bool_same_op() { // $a || ($b && $c && $d) — the inner `&&` operators are nested inside `||`, @@ -7262,6 +7484,7 @@ end", ); } + #[cfg(feature = "rust")] #[test] fn rust_sibling_bool_sequences() { // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested. @@ -7279,6 +7502,7 @@ end", ); } + #[cfg(feature = "rust")] #[test] fn rust_nested_bool_same_op() { // a||(b&&c&&d) — the inner && operators are nested, forming one sequence. @@ -7296,6 +7520,7 @@ end", ); } + #[cfg(feature = "c")] #[test] fn c_sibling_bool_sequences() { // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested. @@ -7313,6 +7538,7 @@ end", ); } + #[cfg(feature = "c")] #[test] fn c_nested_bool_same_op() { // a||(b&&c&&d) — the inner && operators are nested, forming one sequence. @@ -7330,6 +7556,7 @@ end", ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_sibling_bool_sequences() { // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested. @@ -7347,6 +7574,7 @@ end", ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_nested_bool_same_op() { // a||(b&&c&&d) — the inner && operators are nested, forming one sequence. @@ -7364,6 +7592,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_sibling_bool_sequences() { // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested. @@ -7381,6 +7610,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_nested_bool_same_op() { // a||(b&&c&&d) — the inner && operators are nested, forming one sequence. @@ -7398,6 +7628,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_sibling_bool_sequences() { // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested. @@ -7415,6 +7646,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_nested_bool_same_op() { // a||(b&&c&&d) — the inner && operators are nested, forming one sequence. @@ -7432,6 +7664,7 @@ end", ); } + #[cfg(feature = "javascript")] #[test] fn javascript_nullish_coalescing_chain_230() { // Regression for issue #230: `??` is a short-circuit operator and @@ -7462,6 +7695,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_nullish_coalescing_with_if_230() { // Regression for issue #230: the example from the issue body. @@ -7496,6 +7730,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_nullish_coalescing_chain_230() { // Regression for issue #230: TSX parity with JS/TS for `??`. @@ -7523,6 +7758,7 @@ end", ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_nullish_coalescing_chain_230() { // Regression for issue #230: Mozjs parity with JS for `??`. @@ -7550,6 +7786,7 @@ end", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_null_coalescing_cognitive_230() { // Regression for issue #230: C# `??` must form a boolean sequence @@ -7586,6 +7823,7 @@ end", ); } + #[cfg(feature = "php")] #[test] fn php_null_coalescing_cognitive_230() { // Regression for issue #230: PHP `??` must form a boolean sequence @@ -7629,6 +7867,7 @@ end", // `||` chains do. Each word-form gets its own test so a regression // that drops a single variant (e.g. only `Or`) is still caught. + #[cfg(feature = "php")] #[test] fn php_word_form_and_forms_boolean_sequence_230() { check_metrics::( @@ -7647,6 +7886,7 @@ end", ); } + #[cfg(feature = "php")] #[test] fn php_word_form_or_forms_boolean_sequence_230() { check_metrics::( @@ -7665,6 +7905,7 @@ end", ); } + #[cfg(feature = "php")] #[test] fn php_word_form_xor_forms_boolean_sequence_230() { check_metrics::( @@ -7683,6 +7924,7 @@ end", ); } + #[cfg(feature = "java")] #[test] fn java_cognitive_else_if_chain() { // Regression for #115: else-if chains must not receive a nesting @@ -7716,6 +7958,7 @@ end", ); } + #[cfg(feature = "java")] #[test] fn java_cognitive_nested_else_if() { // Regression for #115: else-if inside a loop must still respect @@ -7751,6 +7994,7 @@ end", ); } + #[cfg(feature = "java")] #[test] fn java_cognitive_if_inside_else_block_is_not_else_if() { // Regression for #115: an `if` whose previous sibling is the block's @@ -7786,6 +8030,7 @@ end", ); } + #[cfg(feature = "java")] #[test] fn java_sibling_bool_sequences() { // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested. @@ -7805,6 +8050,7 @@ end", ); } + #[cfg(feature = "java")] #[test] fn java_nested_bool_same_op() { // a||(b&&c&&d) — the inner && operators are nested, forming one sequence. @@ -7824,6 +8070,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_no_cognitive() { check_metrics::("class A { int x = 42 }", "foo.groovy", |metric| { @@ -7831,6 +8078,7 @@ end", }); } + #[cfg(feature = "groovy")] #[test] fn groovy_single_branch_function() { check_metrics::( @@ -7847,6 +8095,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_nested_if() { check_metrics::( @@ -7865,6 +8114,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_else_if_chain() { // Regression for the #115 / #239 stub pattern: an `else if` @@ -7889,6 +8139,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_else_if_chain_lower_than_nested_ifs() { // The `else if` chain in `groovy_else_if_chain` MUST score @@ -7916,6 +8167,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_sequence_booleans_same_op() { // SonarSource B1: a chain of identical short-circuit ops counts as one. @@ -7931,6 +8183,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_sequence_booleans_mixed_ops() { // A `&&` followed by `||` is two distinct sequences = +2. @@ -7946,6 +8199,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_not_operator_negation() { // SonarSource: `!` negation flips a boolean sequence's polarity @@ -7962,6 +8216,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_for_while_do_loops() { check_metrics::( @@ -7980,6 +8235,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_enhanced_for() { check_metrics::( @@ -7995,6 +8251,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_try_catch_nesting() { check_metrics::( @@ -8013,6 +8270,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_ternary_expression() { check_metrics::( @@ -8027,6 +8285,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_elvis_chain_246() { // Regression for issue #246: Groovy's Elvis operator `?:` is @@ -8050,6 +8309,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_elvis_inside_if_246() { // Regression for issue #246: Elvis chain inside an `if` body. @@ -8070,6 +8330,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_labeled_break_continue() { // SonarSource B2: labeled break/continue each add +1. @@ -8093,6 +8354,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_multiple_branch_function() { // Sibling `if` statements at the same nesting level each @@ -8119,6 +8381,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_unlabeled_break_continue_not_counted() { // SonarSource B2: plain `break` / `continue` are NOT @@ -8141,6 +8404,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_cognitive_closure_body_counts_lambda_nesting() { // #519: control flow inside a Groovy closure must pay the same @@ -8168,6 +8432,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_nested_method_resets_nesting_and_adds_depth() { // Regression for #696: a local-class method declared two `if`s deep @@ -8202,6 +8467,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_cognitive_top_level_typed_method_parity() { // Regression for the upstream grammar defect @@ -8232,6 +8498,7 @@ end", }); } + #[cfg(feature = "groovy")] #[test] fn groovy_cognitive_nested_else_if() { // Regression for the #115 stub pattern at deeper nesting: @@ -8258,6 +8525,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_cognitive_if_inside_else_block_is_not_else_if() { // Regression for #115 — an inner `if` whose previous sibling @@ -8283,6 +8551,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_nested_ternary() { // Nested ternaries inside an `if` compound by nesting — same @@ -8306,6 +8575,7 @@ end", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_cognitive_else_if_chain() { // Regression for #115: else-if chains must not receive a nesting @@ -8339,6 +8609,7 @@ end", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_cognitive_nested_else_if() { // Regression for #115: else-if inside a loop must still respect @@ -8374,6 +8645,7 @@ end", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_cognitive_if_inside_else_block_is_not_else_if() { // Regression for #115: an `if` whose previous sibling is the block's @@ -8409,6 +8681,7 @@ end", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_sibling_bool_sequences() { // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested. @@ -8428,6 +8701,7 @@ end", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_nested_bool_same_op() { // a||(b&&c&&d) — the inner && operators are nested, forming one sequence. @@ -8447,6 +8721,7 @@ end", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_sibling_bool_sequences() { // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested. @@ -8463,6 +8738,7 @@ end", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_nested_bool_same_op() { // a||(b&&c&&d) — the inner && operators are nested, forming one sequence. @@ -8479,6 +8755,7 @@ end", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_elvis_chain_239() { // Regression for issue #239: Kotlin's Elvis operator `?:` is a @@ -8509,6 +8786,7 @@ end", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_elvis_inside_if_239() { // Regression for issue #239: Elvis chain inside an `if` body. @@ -8542,6 +8820,7 @@ end", ); } + #[cfg(feature = "go")] #[test] fn go_sibling_bool_sequences() { // (a&&b)||(c&&d) — the right-hand && is a sibling, not nested. @@ -8560,6 +8839,7 @@ end", ); } + #[cfg(feature = "go")] #[test] fn go_nested_bool_same_op() { // a||(b&&c&&d) — the inner && operators are nested, forming one sequence. @@ -8578,6 +8858,7 @@ end", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_sibling_bool_sequences() { // ($a && $b) || ($c && $d) — the right-hand && is a sibling, not nested. @@ -8597,6 +8878,7 @@ end", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_nested_bool_same_op() { // $a || ($b && $c && $d) — the inner && operators are nested, one sequence. @@ -8616,6 +8898,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_sibling_bool_sequences() { // (a and b) or (c and d) — the right-hand `and` is a sibling, not nested. @@ -8635,6 +8918,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_nested_bool_same_op() { // a or (b and c and d) — the inner `and` operators are nested, one sequence. @@ -8654,6 +8938,7 @@ end", ); } + #[cfg(feature = "bash")] #[test] fn bash_sibling_bool_sequences() { // [[ a ]] && [[ b ]] || [[ c ]] && [[ d ]] — bash is left-associative so this @@ -8674,6 +8959,7 @@ end", ); } + #[cfg(feature = "bash")] #[test] fn bash_nested_bool_same_op() { // [[ a ]] || [[ b ]] && [[ c ]] && [[ d ]] — bash left-associativity gives @@ -8695,6 +8981,7 @@ end", ); } + #[cfg(feature = "php")] #[test] fn php_no_cognitive() { check_metrics::("( @@ -9246,6 +9551,7 @@ end", // `if cond do … end`: single-branch construct → +1 nesting at depth // 0 inside `def` body → cognitive 1. + #[cfg(feature = "elixir")] #[test] fn elixir_simple_if() { check_metrics::( @@ -9260,6 +9566,7 @@ end", // `if cond do … else … end`: +1 nesting for `if`, +1 for `else` token // (matches Java/Kotlin) → cognitive 2. + #[cfg(feature = "elixir")] #[test] fn elixir_if_else() { check_metrics::( @@ -9276,6 +9583,7 @@ end", // `case x do … end` with three arms: only the container Call earns // a nesting bump (matches Java's `SwitchBlock` rule). Individual // `stab_clause` arms add no extra cost. Expected cognitive 1. + #[cfg(feature = "elixir")] #[test] fn elixir_case_arms_count_once() { check_metrics::( @@ -9291,6 +9599,7 @@ end", // `cond do … end` is structurally identical to `case` for our // purposes: container Call earns +1 nesting; arms add nothing. + #[cfg(feature = "elixir")] #[test] fn elixir_cond_counts_once() { check_metrics::( @@ -9306,6 +9615,7 @@ end", // Nested `if` inside another `if`: outer +1, inner +2 (nested // depth 1) → cognitive 3. + #[cfg(feature = "elixir")] #[test] fn elixir_nested_if_amplifies() { check_metrics::( @@ -9323,6 +9633,7 @@ end", // NOT bump nesting (matches Java / C#'s "try is a wrapper" rule); // each `rescue` / `catch` block bumps +1 nesting at depth 0. The // single `stab_clause` inside each block adds no extra cost. + #[cfg(feature = "elixir")] #[test] fn elixir_try_rescue_catch() { check_metrics::( @@ -9339,6 +9650,7 @@ end", // Short-circuit booleans: `x && y || z` is two operator types in // sequence — `&&` once, `||` once → +2. The `if` container that // surrounds them adds +1 → total cognitive 3. + #[cfg(feature = "elixir")] #[test] fn elixir_boolean_sequence() { check_metrics::( @@ -9357,6 +9669,7 @@ end", // cognitive complexity. The anonymous function body inside // contributes +1 lambda nesting, but its only operation is a // function call (no control flow) → cognitive 0. + #[cfg(feature = "elixir")] #[test] fn elixir_enum_reduce_is_zero() { check_metrics::( @@ -9376,6 +9689,7 @@ end", // scope reasons (documented). The body's lone Call earns nothing, // so cognitive stays at 0. This test pins the documented omission // so any future recursion work has to update it deliberately. + #[cfg(feature = "elixir")] #[test] fn elixir_recursion_is_zero_documented_limitation() { check_metrics::( @@ -9388,6 +9702,7 @@ end", ); } + #[cfg(feature = "php")] #[test] fn php_match_cognitive() { // `match` is treated like `switch`: a single nesting bump for the @@ -9411,6 +9726,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_no_cognitive() { check_metrics::("a = 42\n", "foo.rb", |metric| { @@ -9419,6 +9735,7 @@ end", }); } + #[cfg(feature = "ruby")] #[test] fn ruby_simple_function() { // A function body with no branching scores zero cognitive. @@ -9428,6 +9745,7 @@ end", }); } + #[cfg(feature = "ruby")] #[test] fn ruby_1_level_nesting() { // Single `if` inside a function: +1. @@ -9437,6 +9755,7 @@ end", }); } + #[cfg(feature = "ruby")] #[test] fn ruby_2_level_nesting() { // expected: outer `if` (+1) + inner `if` (+2, nested) = 3. @@ -9450,6 +9769,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_sequence_same_booleans() { // `a && b && c`: same operator collapses to a single boolean @@ -9464,6 +9784,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_sequence_different_booleans() { // `a && b || c`: alternating operators add per change. @@ -9477,6 +9798,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_not_booleans() { // `!a` (Unary) is the not-operator: it doesn't add cognitive @@ -9491,6 +9813,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_break_next() { // Ruby has no labeled loops, so `break`/`next` are always @@ -9507,6 +9830,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_redo_retry_counted() { // `redo` (restart the current loop iteration) and `retry` (re-run a @@ -9524,6 +9848,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_else_if_chain() { // `elsif` extends the parent branch (no extra nesting). An @@ -9554,6 +9879,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_case_else_no_extra_increment() { // #451: the `else` arm of a `case/when` is the default arm of a @@ -9582,6 +9908,7 @@ end", }); } + #[cfg(all(feature = "java", feature = "kotlin", feature = "ruby"))] #[test] fn ruby_case_else_matches_kotlin_when_and_java_switch() { // #451 cross-language parity (lesson #11): the catch-all arm of a @@ -9608,6 +9935,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_if_else_still_counts() { // #451 over-suppression guard: the `else` of an `if`/`elsif` chain @@ -9628,6 +9956,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_stabby_and_keyword_lambda_nesting_parity() { // A stabby lambda parses as a `Lambda` node CONTAINING its own @@ -9649,6 +9978,7 @@ end", }); } + #[cfg(feature = "ruby")] #[test] fn ruby_if_inside_stabby_lambda_inside_method() { // expected: the method boundary resets nesting for its contents @@ -9668,6 +9998,7 @@ end", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_do_block_spelling_matches_brace_spelling() { // The lambda-nesting arm is gated on `Block | DoBlock`, and the @@ -9703,6 +10034,7 @@ end", ); } + #[cfg(feature = "javascript")] #[test] fn javascript_labeled_break_continue() { // Per SonarSource Cognitive Complexity §B2 (issue #435), a labeled @@ -9741,6 +10073,7 @@ end", ); } + #[cfg(feature = "javascript")] #[test] fn javascript_unlabeled_break_continue_not_counted() { // Negative test for issue #435: plain `break;` / `continue;` are @@ -9774,6 +10107,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_labeled_break_continue() { // TS parity with JS for labeled jumps (issue #435): labeled @@ -9820,6 +10154,7 @@ end", /// a definition nested in *conditionals*, the `stops` entry on one /// nested in another *function*. Both are covered below, plus the two /// shapes the fix must leave alone. + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] fn check_js_function_boundary(filename: &str) { fn score(space: &FuncSpace, name: &str) -> u64 { function_space(space, name).metrics.cognitive.cognitive() @@ -9969,26 +10304,31 @@ end", // One `#[test]` per language instantiating `js_cognitive!`: the macro // body is shared but each grammar's `kind_id`s are its own, so a // per-language enum drift is invisible from a single language's run. + #[cfg(feature = "javascript")] #[test] fn javascript_function_boundary_covers_methods_and_function_expressions_1159() { check_js_function_boundary::("foo.js"); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_function_boundary_covers_methods_and_function_expressions_1159() { check_js_function_boundary::("foo.js"); } + #[cfg(feature = "typescript")] #[test] fn typescript_function_boundary_covers_methods_and_function_expressions_1159() { check_js_function_boundary::("foo.ts"); } + #[cfg(feature = "typescript")] #[test] fn tsx_function_boundary_covers_methods_and_function_expressions_1159() { check_js_function_boundary::("foo.tsx"); } + #[cfg(feature = "javascript")] #[test] fn javascript_compound_short_circuit_assignment_236() { // Regression for issue #236: `&&=`, `||=`, `??=` are compound @@ -10022,6 +10362,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_compound_short_circuit_assignment_236() { // Regression for issue #236: TS parity with JS for `&&=`, @@ -10052,6 +10393,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_compound_short_circuit_assignment_236() { // Regression for issue #236: TSX parity with JS/TS for `&&=`, @@ -10082,6 +10424,7 @@ end", ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_compound_short_circuit_assignment_236() { // Regression for issue #236: Mozjs (SpiderMonkey-flavoured JS) @@ -10113,6 +10456,7 @@ end", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_compound_short_circuit_assignment_236() { // Regression for issue #236: C#'s grammar only provides `??=` @@ -10148,6 +10492,7 @@ end", ); } + #[cfg(feature = "php")] #[test] fn php_compound_short_circuit_assignment_236() { // Regression for issue #236: PHP's only compound short-circuit @@ -10182,6 +10527,7 @@ end", } /// A handler with no control flow has zero cognitive complexity. + #[cfg(feature = "irules")] #[test] fn irules_no_cognitive() { check_metrics::("when X { set a 1 }\n", "foo.irule", |metric| { @@ -10190,6 +10536,7 @@ end", } /// A single `if` adds one. + #[cfg(feature = "irules")] #[test] fn irules_simple_function() { check_metrics::( @@ -10203,6 +10550,7 @@ end", /// A run of the *same* boolean operator (`$a && $b && $c`) is one /// sequence: `if` (1) + boolean sequence (1) = 2. + #[cfg(feature = "irules")] #[test] fn irules_sequence_same_booleans() { check_metrics::( @@ -10216,6 +10564,7 @@ end", /// Switching operator (`$a && $b || $c`) starts a new sequence: `if` (1) /// + `&&` sequence (1) + `||` sequence (1) = 3. + #[cfg(feature = "irules")] #[test] fn irules_sequence_different_booleans() { check_metrics::( @@ -10229,6 +10578,7 @@ end", /// Unary negation (`!`) does not itself add cognitive cost; only the /// boolean sequence does: `if` (1) + `&&` sequence (1) = 2. + #[cfg(feature = "irules")] #[test] fn irules_not_booleans() { check_metrics::( @@ -10241,6 +10591,7 @@ end", } /// One level of nesting: `while` (1) + `if` (1 + nesting 1 = 2) = 3. + #[cfg(feature = "irules")] #[test] fn irules_1_level_nesting() { check_metrics::( @@ -10253,6 +10604,7 @@ end", } /// Two levels: `while` (1) + `if` (2) + `foreach` (1 + nesting 2 = 3) = 6. + #[cfg(feature = "irules")] #[test] fn irules_2_level_nesting() { check_metrics::( @@ -10271,6 +10623,7 @@ end", /// predicate that treated `elseif` like a fresh nested `if` would push /// the chain's score up toward the nested value, so the strict `<` /// assertion catches the regression that #115 found in Java/C#. + #[cfg(feature = "irules")] #[test] fn irules_else_if_chain() { use std::cell::Cell; @@ -10302,6 +10655,7 @@ end", /// A `switch` nested in an `if`: `if` (1) + `switch` (1 + nesting 1 = 2) /// = 3. Confirms `switch` participates in nesting like other branches. + #[cfg(feature = "irules")] #[test] fn irules_switch_nesting() { check_metrics::( @@ -10320,6 +10674,7 @@ end", /// /// The `Catch` arm had no test before this: the whole arm measured /// zero-coverage while every other iRules branch kind was exercised. + #[cfg(feature = "irules")] #[test] fn irules_catch_nesting() { check_metrics::("when X { catch { foo } }\n", "foo.irule", |metric| { @@ -10336,6 +10691,7 @@ end", /// Objective-C straight-line method body has zero cognitive /// complexity. + #[cfg(feature = "objc")] #[test] fn objc_no_cognitive() { check_metrics::( @@ -10364,6 +10720,7 @@ end", /// Objective-C single `if` at method top level: +1, no nesting /// surcharge. + #[cfg(feature = "objc")] #[test] fn objc_simple_if() { check_metrics::( @@ -10395,6 +10752,7 @@ end", /// one for the first `&&` and zero for each additional same-operator /// link in the sequence, so the whole `if (a && b && c)` is +1 (if) /// + 1 (one boolean sequence) = 2. + #[cfg(feature = "objc")] #[test] fn objc_sequence_same_booleans() { check_metrics::( @@ -10424,6 +10782,7 @@ end", /// Objective-C nesting surcharge: an `if` nested inside a `for` /// scores `for` (+1) + `if` (+1 base +1 nesting) = 3. + #[cfg(feature = "objc")] #[test] fn objc_nested() { check_metrics::( @@ -10453,6 +10812,7 @@ end", ); } + #[cfg(feature = "objc")] #[test] fn objc_block_nesting() { // A decision inside an ObjC block `^{ … }` picks up the lambda @@ -10492,6 +10852,7 @@ end", /// This guards the `is_else_if` predicate (a regression that failed /// to recognise the else-if extension would inflate the chain to the /// nested score). + #[cfg(feature = "objc")] #[test] fn objc_else_if_chain() { use std::cell::Cell; @@ -10568,6 +10929,7 @@ end", /// parity and totals the same either way. A mutant writing /// `function_depth = 0` in place of `lambda = 0` leaves `lambda 2, /// function_depth 1` here and charges the `if` 4 rather than 2. + #[cfg(feature = "javascript")] #[test] fn javascript_function_depth_and_lambda_are_distinguishable() { // expected: `inner` takes the boundary, so `conditional` and @@ -10589,6 +10951,7 @@ end", /// The `ArrowFunction` arm's own `lambda += 1`, pinned separately: /// with no `function_declaration` between the arrow and the `if`, /// nothing resets lambda, so the arrow's level reaches the `if`. + #[cfg(feature = "javascript")] #[test] fn javascript_arrow_contributes_lambda_nesting() { // expected: 1 for the `if`, +1 for the enclosing arrow level. @@ -10635,6 +10998,7 @@ end", /// that fix did not touch. #1084 moved that predicate onto the /// walker's ancestor chain, and the harness now measures the `if` /// shape under the same linear bound as this one. + #[cfg(feature = "c")] #[test] fn cognitive_nesting_is_inherited_at_depth() { // Restricted to `Cognitive` — which pulls in `Nom` as a declared @@ -10680,6 +11044,7 @@ end", /// `cognitive/nested-fn` probe in the benchmark harness /// (`cargo bench -p big-code-analysis-bench --bench scaling`), /// which asserts the complexity class. + #[cfg(feature = "rust")] #[test] fn cognitive_function_depth_is_inherited_at_depth() { fn cognitive_of(source: &str) -> u64 { diff --git a/src/metrics/cognitive/python.rs b/src/metrics/cognitive/python.rs index 39ff9b6b7..3cece379b 100644 --- a/src/metrics/cognitive/python.rs +++ b/src/metrics/cognitive/python.rs @@ -256,6 +256,7 @@ mod tests { // drive the helper directly and assert each clause's map slot // field-by-field — which is what the `Nesting` parameter now makes // checkable at the call site too. + #[cfg(feature = "python")] #[test] fn python_comprehension_clauses_carry_inherited_depth_and_lambda() { // Both fixtures have three clauses and differ only in which kind diff --git a/src/metrics/cyclomatic.rs b/src/metrics/cyclomatic.rs index 95b8c9204..e7051b2cd 100644 --- a/src/metrics/cyclomatic.rs +++ b/src/metrics/cyclomatic.rs @@ -610,9 +610,10 @@ mod typescript; clippy::too_many_lines )] mod tests { + #[cfg(feature = "csharp")] + use crate::test_support::assert_csharp_fixture_spells; use crate::test_support::{ - assert_csharp_fixture_spells, ast_has_kind_id, check_func_space_only, - check_metrics_only_shim, child_space, + ast_has_kind_id, check_func_space_only, check_metrics_only_shim, child_space, }; use super::*; @@ -664,6 +665,7 @@ mod tests { /// see `cyclomatic_python_lambda_divisor_excludes_spaceless_closure`.) /// Before #512 the divisor was 4 (every space, base 1 each) and the /// averages were two-thirds of these values (`6 / 4 == 1.5`). + #[cfg(feature = "csharp")] #[test] fn cyclomatic_average_is_per_function_512() { check_cyclomatic_and_nom::( @@ -703,6 +705,7 @@ mod tests { /// `nom.total()` is `0.0` here (Nom was never computed) yet the /// average is still the correct per-function `6 / 2 == 3.0` — proof /// the divisor does not read `nom`. + #[cfg(feature = "csharp")] #[test] fn cyclomatic_average_per_function_without_nom_512() { let space = crate::analyze( @@ -736,6 +739,7 @@ mod tests { /// that matches the spaces contributing to `cyclomatic_sum`. The /// behaviour is intentional, not a bug — pinning it so a future change /// to lambda space-handling is a deliberate, visible decision. + #[cfg(feature = "python")] #[test] fn cyclomatic_python_lambda_divisor_excludes_spaceless_closure() { check_cyclomatic_and_nom::( @@ -763,6 +767,7 @@ mod tests { /// /// Expected: unit(1) + fn(1) + if(1) = 3. No contribution from /// `else`. + #[cfg(feature = "python")] #[test] fn python_if_else_does_not_overcount_229() { check_metrics::( @@ -804,6 +809,7 @@ mod tests { /// per `if` and per `elif`, never the bare `else`. /// /// Expected: unit(1) + fn(1) + if(1) + elif(1) + elif(1) = 5. + #[cfg(feature = "python")] #[test] fn python_if_elif_else_chain_229() { check_metrics::( @@ -831,6 +837,7 @@ mod tests { /// distinct decision point. /// /// Expected: unit(1) + fn(1) + for(1) + else(1) = 4. + #[cfg(feature = "python")] #[test] fn python_for_else_still_counts_229() { check_metrics::( @@ -856,6 +863,7 @@ mod tests { /// completion of the loop. /// /// Expected: unit(1) + fn(1) + while(1) + else(1) = 4. + #[cfg(feature = "python")] #[test] fn python_while_else_still_counts_229() { check_metrics::( @@ -880,6 +888,7 @@ mod tests { /// alongside the `except` arm. /// /// Expected: unit(1) + fn(1) + except(1) + try/else(1) = 4. + #[cfg(feature = "python")] #[test] fn python_try_except_else_counts_229() { check_metrics::( @@ -906,6 +915,7 @@ mod tests { /// `using` sibling and textbook McCabe. Regression test for #418. /// /// Expected: unit(1) + fn(1) = 2; the `with` adds nothing. + #[cfg(feature = "python")] #[test] fn python_with_is_not_a_decision_point_418() { check_metrics::( @@ -928,6 +938,7 @@ mod tests { /// #418. /// /// Expected: unit(1) + fn(1) = 2. + #[cfg(feature = "python")] #[test] fn python_with_multiple_managers_is_not_a_decision_point_418() { check_metrics::( @@ -948,6 +959,7 @@ mod tests { /// it too. Companion to #418. /// /// Expected: unit(1) + fn(1) = 2; neither `async` nor `with` counts. + #[cfg(feature = "python")] #[test] fn python_async_with_is_not_a_decision_point_418() { check_metrics::( @@ -968,6 +980,7 @@ mod tests { /// over-broad fix. Companion to #418. /// /// Expected: unit(1) + fn(1) + if(1) = 3; the `with` adds nothing. + #[cfg(feature = "python")] #[test] fn python_with_body_branch_still_counts_418() { check_metrics::( @@ -986,6 +999,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_simple_function() { check_metrics::( @@ -1025,6 +1039,7 @@ mod tests { /// `match_statement` contributes one modified decision. A bare /// `case _:` (no guard) is skipped, mirroring Rust's `MatchArm` /// bare-wildcard filter. Regression test for #212. + #[cfg(feature = "python")] #[test] fn python_match_two_arm_wildcard() { check_metrics::( @@ -1070,6 +1085,7 @@ mod tests { /// Python contributes a decision) — long-standing behaviour /// shared with regular `if` statements. Companion to the /// `python_match_case_guarded_wildcard_counts` test in `abc.rs`. + #[cfg(feature = "python")] #[test] fn python_match_guarded_wildcard_counts() { check_metrics::( @@ -1114,6 +1130,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_1_level_nesting() { check_metrics::( @@ -1147,6 +1164,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_1_level_nesting() { check_metrics::( @@ -1187,6 +1205,7 @@ mod tests { /// Modified CCN: a match with N arms counts as 1 decision, not N. /// Bare `_ =>` wildcard arm does not count toward standard CCN (same /// as C-family `default:`). + #[cfg(feature = "rust")] #[test] fn rust_match_modified() { check_metrics::( @@ -1227,14 +1246,17 @@ mod tests { // counting on (the default) each adds +1 to both standard and // modified; with counting off they add nothing. The two runs must // therefore differ by exactly N on both sub-metrics. + #[cfg(feature = "rust")] const RUST_TRY_FIXTURE: &str = "fn f(s: &str) -> Result { let a: i64 = s.parse()?; let b: i64 = s.parse()?; let c: i64 = s.parse()?; Ok(a + b + c) }"; + #[cfg(feature = "rust")] const RUST_TRY_COUNT: u64 = 3; + #[cfg(feature = "rust")] fn rust_cyclomatic_with_try(count_try: bool) -> super::Stats { let func_space = crate::analyze( crate::Source::new(crate::LANG::Rust, RUST_TRY_FIXTURE.as_bytes()) @@ -1245,6 +1267,7 @@ mod tests { func_space.metrics.cyclomatic.clone() } + #[cfg(feature = "rust")] #[test] fn rust_try_toggle_differs_by_exactly_n() { let with = rust_cyclomatic_with_try(true); @@ -1266,6 +1289,7 @@ mod tests { assert_ne!(with.cyclomatic_sum(), without.cyclomatic_sum()); } + #[cfg(feature = "rust")] #[test] fn rust_try_default_counts() { // The default (no options) must keep counting `?`, preserving @@ -1305,6 +1329,7 @@ mod tests { /// `compute` that delegated with `false` *and* a /// `compute_with_options` that ignored the flag, so the count is /// pinned against the opted-out run as well. + #[cfg(feature = "rust")] #[test] fn rust_compute_delegates_with_try_counting_on() { use crate::traits::ParserTrait; @@ -1343,6 +1368,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_switch() { check_metrics::( @@ -1389,6 +1415,7 @@ mod tests { } /// Modified CCN: 3 case arms in one switch collapse to 1 decision. + #[cfg(feature = "c")] #[test] fn c_switch_modified() { check_metrics::( @@ -1427,6 +1454,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_real_function() { check_metrics::( @@ -1468,6 +1496,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_unit_before() { check_metrics::( @@ -1522,6 +1551,7 @@ mod tests { /// Test to handle the case of min and max when merge happen before the final value of one module are set. /// In this case the min value should be 3 because the unit space has 2 branches and a complexity of 3 /// while the function sumOfPrimes has a complexity of 4. + #[cfg(feature = "c")] #[test] fn c_unit_after() { check_metrics::( @@ -1574,6 +1604,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_simple_class() { check_metrics::( @@ -1621,6 +1652,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_real_class() { check_metrics::( @@ -1684,6 +1716,7 @@ mod tests { } /// Modified CCN: Java switch with 2 cases counts as 1 (not 2). + #[cfg(feature = "java")] #[test] fn java_switch_modified() { check_metrics::( @@ -1728,6 +1761,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_simple_class() { check_metrics::( @@ -1773,6 +1807,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_real_class() { check_metrics::( @@ -1834,6 +1869,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_anonymous_method() { check_metrics::( @@ -1871,6 +1907,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_switch_expression_arms() { // Each non-default arm of a switch_expression contributes +1. @@ -1920,6 +1957,7 @@ mod tests { /// Regression #282: the bare discard arm `_ =>` in a C# switch /// expression must NOT contribute to standard CCN, mirroring the /// C-family `default:` rule. + #[cfg(feature = "csharp")] #[test] fn csharp_switch_expression_discard_arm_not_counted() { check_metrics::( @@ -1942,6 +1980,7 @@ mod tests { /// Regression #282: `var _` is also a discard pattern and must be /// excluded from standard CCN. + #[cfg(feature = "csharp")] #[test] fn csharp_switch_expression_var_underscore_not_counted() { check_metrics::( @@ -1975,6 +2014,7 @@ mod tests { /// of the `default:` exclusion, the `WhenClause` arm scores the /// guard — so this test would still fail if #282's exclusion were /// reintroduced, at sum 5 / max 3 against the asserted 6 / 4. + #[cfg(feature = "csharp")] #[test] fn csharp_switch_expression_guarded_discard_still_counts() { check_metrics::( @@ -2003,6 +2043,7 @@ mod tests { /// standard decision. Locks in the /// `DeclarationPattern → _ => return NotDiscard` catch-all in /// `csharp_switch_expression_arm_is_bare_discard`. + #[cfg(feature = "csharp")] #[test] fn csharp_switch_expression_typed_discard_still_counts() { check_metrics::( @@ -2035,6 +2076,7 @@ mod tests { /// the guard has scored a decision of its own since #1422, so the /// arm is worth 2. Reintroducing #303's exclusion would read sum 5 / /// max 3 against the asserted 6 / 4. + #[cfg(feature = "csharp")] #[test] fn csharp_switch_expression_guarded_var_underscore_still_counts() { check_metrics::( @@ -2081,6 +2123,7 @@ mod tests { /// leaves every assertion below satisfied and the construct under /// test gone; the anchor's count of 7 — six arms plus the one jump — /// fails by name instead. + #[cfg(feature = "csharp")] #[test] fn csharp_goto_case_is_not_a_switch_arm() { let src = "class A { @@ -2118,6 +2161,7 @@ mod tests { } /// Modified CCN: C# switch statement with 2 cases counts as 1. + #[cfg(feature = "csharp")] #[test] fn csharp_switch_modified() { check_metrics::( @@ -2160,6 +2204,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_null_coalescing_and_conditional_access() { // Each `??` and `?.` is +1 cyclomatic. @@ -2194,6 +2239,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_simple_function() { check_metrics::( @@ -2230,6 +2276,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_switch() { check_metrics::( @@ -2275,6 +2322,7 @@ mod tests { } /// Modified CCN: JS switch with 3 cases collapses to 1. + #[cfg(feature = "javascript")] #[test] fn javascript_switch_modified() { check_metrics::( @@ -2312,6 +2360,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_simple_function() { check_metrics::( @@ -2343,6 +2392,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_if_else() { check_metrics::( @@ -2379,6 +2429,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_else_if_chain() { check_metrics::( @@ -2418,6 +2469,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_for_loop() { check_metrics::( @@ -2451,6 +2503,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_for_range() { check_metrics::( @@ -2487,6 +2540,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_switch() { check_metrics::( @@ -2524,6 +2578,7 @@ mod tests { } /// Modified CCN: Go switch with 3 cases collapses to 1. + #[cfg(feature = "go")] #[test] fn go_switch_modified() { check_metrics::( @@ -2565,6 +2620,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_type_switch() { check_metrics::( @@ -2600,6 +2656,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_select() { check_metrics::( @@ -2636,6 +2693,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_logical_operators() { check_metrics::( @@ -2669,6 +2727,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_defer_and_go_do_not_count() { check_metrics::( @@ -2707,6 +2766,7 @@ mod tests { // https://github.com/sebastianbergmann/php-code-coverage/issues/607 // An anonymous class declaration is not considered when computing the Cyclomatic Complexity metric for Java // Only the complexity of the anonymous class content is considered for the computation + #[cfg(feature = "java")] #[test] fn java_anonymous_class() { check_metrics::( @@ -2772,6 +2832,7 @@ mod tests { /// the dedicated `JavaCode` impl already counts. Adding /// `Java::DoStatement` would double-count — see issue #284. This /// test pins the correct keyword-driven count. + #[cfg(feature = "java")] #[test] fn java_do_statement_counts_in_cyclomatic() { check_metrics::( @@ -2819,6 +2880,7 @@ mod tests { /// just like inside a classic `ForStatement`. Pinning this /// prevents reintroducing the double-count from issue #284's /// incorrect fix proposal. + #[cfg(feature = "java")] #[test] fn java_enhanced_for_statement_counts_in_cyclomatic() { check_metrics::( @@ -2859,6 +2921,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_simple_class() { check_metrics::( @@ -2889,6 +2952,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_nested_control_flow() { check_metrics::( @@ -2907,6 +2971,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_switch_with_cases() { check_metrics::( @@ -2933,6 +2998,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_try_catch() { check_metrics::( @@ -2951,6 +3017,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_closure_body_short_circuit() { // Top-level `def pred = { … }` collapses the closure into the @@ -2967,6 +3034,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_assert_adds_branch() { // Groovy `assert` is a runtime check that branches on its @@ -2989,6 +3057,7 @@ mod tests { /// by the dedicated `GroovyCode` impl. Adding `Groovy::DoStatement` /// would double-count (issue #284). This test pins the correct /// keyword-driven count. + #[cfg(feature = "groovy")] #[test] fn groovy_do_statement_counts_in_cyclomatic() { check_metrics::( @@ -3015,6 +3084,7 @@ mod tests { /// inside a classic `ForStatement`. Pinning this prevents /// reintroducing the double-count from issue #284's incorrect fix /// proposal. + #[cfg(feature = "groovy")] #[test] fn groovy_enhanced_for_statement_counts_in_cyclomatic() { check_metrics::( @@ -3034,6 +3104,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_safe_navigation_cyclomatic() { // Issue #452: Groovy's safe-navigation `?.` (QMARKDOT) is a @@ -3050,6 +3121,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_safe_subscript_cyclomatic() { // Issue #1471: `?[` short-circuits on a null receiver exactly as @@ -3069,6 +3141,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_safe_chain_dot_cyclomatic() { // Issue #452: Groovy's `??.` (QMARKQMARKDOT, the spread-safe @@ -3084,6 +3157,7 @@ mod tests { }); } + #[cfg(feature = "perl")] #[test] fn perl_nested_control_flow() { check_metrics::( @@ -3119,6 +3193,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_postfix_conditionals() { check_metrics::( @@ -3151,6 +3226,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_unless_and_until() { check_metrics::( @@ -3187,6 +3263,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_logical_operators_and_ternary() { check_metrics::( @@ -3221,6 +3298,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_word_logical_operators() { check_metrics::( @@ -3253,6 +3331,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_compound_short_circuit_assignment_249() { // Regression for issue #249: `&&=`, `||=`, `//=` are each one @@ -3296,6 +3375,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_foreach_loop() { check_metrics::( @@ -3326,6 +3406,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_else_does_not_count_but_elsif_does() { check_metrics::( @@ -3363,6 +3444,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_simple_function() { check_metrics::( @@ -3399,6 +3481,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_if_else_and_switch() { check_metrics::( @@ -3443,6 +3526,7 @@ mod tests { } /// Modified CCN: TypeScript switch with 3 cases collapses to 1. + #[cfg(feature = "typescript")] #[test] fn typescript_switch_modified() { check_metrics::( @@ -3481,6 +3565,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_if_else_and_switch() { check_metrics::( @@ -3525,6 +3610,7 @@ mod tests { } /// Modified CCN: MozJS switch with 2 cases collapses to 1. + #[cfg(feature = "mozjs")] #[test] fn mozjs_switch_modified() { check_metrics::( @@ -3561,6 +3647,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_cyclomatic_mixed() { check_metrics::( @@ -3616,6 +3703,7 @@ mod tests { } /// Modified CCN: Kotlin when with 3 entries collapses to 1. + #[cfg(feature = "kotlin")] #[test] fn kotlin_when_modified() { check_metrics::( @@ -3660,6 +3748,7 @@ mod tests { /// Regression #282: the `else -> …` arm in a Kotlin `when` /// expression must NOT contribute to standard CCN, mirroring the /// C-family `default:` rule. + #[cfg(feature = "kotlin")] #[test] fn kotlin_when_else_arm_not_counted() { check_metrics::( @@ -3684,6 +3773,7 @@ mod tests { /// pins the single-explicit case) to confirm the count scales /// linearly with explicit arms and is not accidentally hard-coded /// to one. + #[cfg(feature = "kotlin")] #[test] fn kotlin_when_multiple_explicit_arms_each_count() { check_metrics::( @@ -3704,6 +3794,7 @@ mod tests { ); } + #[cfg(feature = "lua")] #[test] fn lua_1_level_nesting() { // chunk: base=1; f: base=1 + for=1 + if=1 = 3; sum=4 @@ -3738,6 +3829,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_elseif_branches() { // chunk: base=1; classify: base=1 + if=1 + elseif=1 + elseif=1 = 4 @@ -3776,6 +3868,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_logical_operators() { // chunk: base=1; f: base=1 + if=1 + and=1 + or=1 = 4; sum=5 @@ -3808,6 +3901,7 @@ end", ); } + #[cfg(feature = "bash")] #[test] fn bash_nested_control_flow() { check_metrics::( @@ -3834,6 +3928,7 @@ f() { /// Regression test for #107: case…esac must not double-count the container. /// Standard CCN counts only arms (matching C-family `switch` semantics). /// Modified CCN counts only the container. + #[cfg(feature = "bash")] #[test] fn bash_case_modified() { check_metrics::( @@ -3872,6 +3967,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_1_level_nesting() { // chunk: base=1; f: base=1 + while=1 + if=1 = 3; sum=4 @@ -3893,6 +3989,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_elseif_branch() { // if=1, elseif=1; else does NOT add a branch; sum=3 (chunk base=1) @@ -3917,6 +4014,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_logical_operators() { check_metrics::( @@ -3935,6 +4033,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_catch_branch() { // `catch` command adds +1 (conditional handler); `try` does NOT add a branch. @@ -3955,6 +4054,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_try_no_branch() { // `try` is NOT a conditional construct; it does not add cyclomatic complexity. @@ -3992,6 +4092,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_switch_cyclomatic() { // Tcl `switch` is a generic command; each non-`default` arm is a @@ -4019,6 +4120,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_switch_cyclomatic_no_default_with_options() { // No `default` arm, and leading `switch` options (`-exact --`) precede @@ -4042,6 +4144,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_switch_split_form_stays_uncounted() { // The split arm form (`switch $x a {…} b {…}`) passes each arm @@ -4068,6 +4171,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_for_cyclomatic() { // Tcl `for` is a generic command — the grammar has no `for` rule — @@ -4090,6 +4194,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_for_cyclomatic_name_gate() { // The detection reads the command's `name` field: a command whose @@ -4111,6 +4216,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_qualified_for_resolves_to_the_builtin() { // `::for` is `for` through the global namespace, so it scores @@ -4131,6 +4237,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_namespaced_for_is_not_the_builtin() { // Control: `ns::for` lives in `ns` and is not the core loop, so @@ -4150,6 +4257,7 @@ f() { ); } + #[cfg(all(feature = "irules", feature = "tcl"))] #[test] fn tcl_irules_for_parity() { // iRules models `for` as a dedicated kind counted by the kind @@ -4187,6 +4295,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_try_on_error_cyclomatic() { // Tcl `try` is a dedicated kind whose single permitted `on error` @@ -4214,6 +4323,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_try_finally_only_cyclomatic() { // A `try` with no handler has no decision point: `finally` is @@ -4237,6 +4347,7 @@ f() { ); } + #[cfg(feature = "irules")] #[test] fn irules_try_handlers_cyclomatic() { // iRules wraps each `try` handler in a dedicated `on_handler` / @@ -4267,6 +4378,7 @@ f() { ); } + #[cfg(all(feature = "irules", feature = "tcl"))] #[test] fn tcl_irules_try_parity() { // The same single-handler `try` must score identically in Tcl @@ -4294,6 +4406,7 @@ f() { }); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_for_loop() { check_metrics::( @@ -4314,6 +4427,7 @@ f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_logical_operators() { check_metrics::( @@ -4333,6 +4447,7 @@ f() { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_nullish_coalescing_chain_226() { // `??` is short-circuit and must count as @@ -4370,6 +4485,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_nullish_coalescing_with_if_226() { // TypeScript must count `??` as a @@ -4407,6 +4523,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_nullish_coalescing_chain_226() { // TSX must count `??` the same as JS/TS. @@ -4442,6 +4559,7 @@ f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_nullish_coalescing_chain_226() { // Mozjs must count `??` the same as JS. @@ -4477,6 +4595,7 @@ f() { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_nullish_coalescing_assignment_231() { // `x ??= y` is `x = x ?? y` — one short-circuit decision edge, @@ -4515,6 +4634,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_nullish_coalescing_assignment_231() { // TypeScript must count `??=` the same as JS. @@ -4552,6 +4672,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_nullish_coalescing_assignment_231() { // TSX must count `??=` the same as JS/TS. @@ -4589,6 +4710,7 @@ f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_nullish_coalescing_assignment_231() { // Mozjs must count `??=` the same as JS. @@ -4626,6 +4748,7 @@ f() { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_short_circuit_assignments_248() { // `&&=`, `||=`, `??=` are each one short-circuit decision edge — @@ -4666,6 +4789,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_short_circuit_assignments_248() { // TypeScript parallel of #248: `&&=` / `||=` / `??=` each +1. @@ -4704,6 +4828,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_short_circuit_assignments_248() { // TSX parallel of #248: `&&=` / `||=` / `??=` each +1. @@ -4742,6 +4867,7 @@ f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_short_circuit_assignments_248() { // Mozjs parallel of #248: `&&=` / `||=` / `??=` each +1. @@ -4786,6 +4912,7 @@ f() { // cyclomatic ignored `?.` entirely. The four tests below mirror // the existing `nullish_coalescing_chain_226` pattern but for // `?.`: two `?.` in a chain add +2 on top of the function entry. + #[cfg(feature = "javascript")] #[test] fn javascript_optional_chain_counted_in_cyclomatic_281() { check_metrics::( @@ -4801,6 +4928,7 @@ f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_optional_chain_counted_in_cyclomatic_281() { check_metrics::( @@ -4815,6 +4943,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_optional_chain_counted_in_cyclomatic_281() { // TS exposes `?.` as both an `optional_chain` wrapper (over @@ -4833,6 +4962,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_optional_chain_counted_in_cyclomatic_281() { check_metrics::( @@ -4851,6 +4981,7 @@ f() { // ensures the TS/TSX dispatch on `QMARKDOT` (not the wrapper) // counts both forms exactly once. Both forms emit the bare `?.` // token; the wrapper only appears around member expressions. + #[cfg(feature = "typescript")] #[test] fn typescript_optional_chain_call_form_counted_281() { check_metrics::( @@ -4865,6 +4996,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_optional_chain_call_form_counted_281() { check_metrics::( @@ -4879,6 +5011,7 @@ f() { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_nullish_coalescing_assignment_231() { // C#'s `??=` is short-circuit (RHS evaluates only when LHS is null) @@ -4921,6 +5054,7 @@ f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_while_loop() { check_metrics::( @@ -4941,6 +5075,7 @@ f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_while_loop() { check_metrics::( @@ -4962,6 +5097,7 @@ f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_case_statement() { check_metrics::( @@ -4990,6 +5126,7 @@ f() { /// (1 base + 2 arms); with the fix it reports `2` (1 base + 1 /// explicit arm), matching every other switch-bearing language /// in `tests/parity/cyclomatic_cross_language_parity.rs`. + #[cfg(feature = "bash")] #[test] fn bash_case_bare_wildcard_excluded() { check_metrics::( @@ -5016,6 +5153,7 @@ f() { /// A multi-value pattern containing `*` (`a|*)`) is NOT a bare /// wildcard — both alternations make it a non-default case. The /// arm still contributes one standard decision. + #[cfg(feature = "bash")] #[test] fn bash_case_multi_value_with_star_counts() { check_metrics::( @@ -5036,6 +5174,7 @@ f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_arithmetic_ternary_is_a_decision() { // Regression for #1268: the arithmetic ternary is the only ternary @@ -5065,6 +5204,7 @@ g() { ); } + #[cfg(feature = "bash")] #[test] fn bash_ternary_expression_alias_is_unreachable() { // Drift marker for the defensive `TernaryExpression2` arm (lesson @@ -5093,6 +5233,7 @@ g() { assert!(!ast_has_kind_id(&parser, Bash::TernaryExpression2 as u16)); } + #[cfg(feature = "bash")] #[test] fn bash_nested_arithmetic_ternary_counts_each_occurrence() { // Each ternary is its own decision point (#1268). @@ -5110,6 +5251,7 @@ h() { ); } + #[cfg(feature = "bash")] #[test] fn bash_simple_function() { check_metrics::( @@ -5127,6 +5269,7 @@ f() { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_for_loop() { check_metrics::( @@ -5147,6 +5290,7 @@ f() { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_while_loop() { check_metrics::( @@ -5167,6 +5311,7 @@ f() { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_logical_operators() { check_metrics::( @@ -5183,6 +5328,7 @@ f() { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_elvis_operator_239() { // Regression for issue #239: Kotlin's Elvis operator `?:` is a @@ -5222,6 +5368,7 @@ f() { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_safe_navigation_436() { // Issue #436: Kotlin's safe-navigation `?.` is a short-circuit @@ -5243,6 +5390,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_for_loop() { check_metrics::( @@ -5263,6 +5411,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_while_loop() { check_metrics::( @@ -5283,6 +5432,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_logical_operators() { check_metrics::( @@ -5299,6 +5449,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_try_catch() { check_metrics::( @@ -5319,6 +5470,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_for_loop() { check_metrics::( @@ -5339,6 +5491,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_while_loop() { check_metrics::( @@ -5359,6 +5512,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_logical_operators() { check_metrics::( @@ -5375,6 +5529,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_try_catch() { check_metrics::( @@ -5395,6 +5550,7 @@ f() { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_switch() { check_metrics::( @@ -5420,6 +5576,7 @@ f() { } /// Modified CCN: TSX switch with 2 cases collapses to 1. + #[cfg(feature = "typescript")] #[test] fn tsx_switch_modified() { check_metrics::( @@ -5442,6 +5599,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_1_level_nesting() { // Mirrors java_simple_class' if-inside-method shape: @@ -5488,6 +5646,7 @@ f() { // Three func spaces (Unit + defmodule Class + def Function) each // seed one entry: standard = 3 entries + 2 counted stabs = 5; // modified = 3 entries + 1 case Call = 4. + #[cfg(feature = "elixir")] #[test] fn elixir_case_arms() { check_metrics::( @@ -5508,6 +5667,7 @@ f() { // 3 entries + case + the guard = 5. The guard is the #1454 arm: it // is a second way the arm can fail, and no container collapses it, // so it counts in both tiers where the arm counts only in standard. + #[cfg(feature = "elixir")] #[test] fn elixir_case_guarded_wildcard_counts() { check_metrics::( @@ -5524,6 +5684,7 @@ f() { // the bare `_` is the default arm, matching Rust's bare-`_`-only // MatchArm rule (issue #1272). standard = 3 entries + `1 ->` + // `_x ->` = 5; modified = 3 entries + case Call = 4. + #[cfg(feature = "elixir")] #[test] fn elixir_case_named_discard_counts() { check_metrics::( @@ -5541,6 +5702,7 @@ f() { // (issue #1272, grammar-dispatch §8: anchor the exclusion to the // owning construct). standard = 3 entries + `true ->` + // `false ->` = 5; modified = 3 entries + case Call = 4. + #[cfg(feature = "elixir")] #[test] fn elixir_case_true_pattern_counts() { check_metrics::( @@ -5557,6 +5719,7 @@ f() { // decision point — Elixir does not expose `if`/`unless` as a // distinct kind_id, so this is the only operator-driven path the // metric can see. + #[cfg(feature = "elixir")] #[test] fn elixir_logical_operators() { check_metrics::( @@ -5574,6 +5737,7 @@ f() { // Call contributes once to modified CCN, while each rescue/catch // arm's matched pattern (a `stab_clause`) contributes once to // standard CCN. This mirrors C-family `try`/`catch` semantics. + #[cfg(feature = "elixir")] #[test] fn elixir_try_rescue() { check_metrics::( @@ -5592,6 +5756,7 @@ f() { // metric inspects the source text of the call's target field to // identify it. Single-branch keyword Calls (`if`/`unless`/`for`/ // `while`) contribute to both standard and modified CCN. + #[cfg(feature = "elixir")] #[test] fn elixir_if_else_counts() { check_metrics::( @@ -5610,6 +5775,7 @@ f() { // form — the `else` keyword is a do-block keyword argument, not // an extra `stab_clause`, so its presence does not change the // cyclomatic count. + #[cfg(feature = "elixir")] #[test] fn elixir_if_without_else_counts() { check_metrics::( @@ -5625,6 +5791,7 @@ f() { // `unless x do ... end` is the negated `if`; it surfaces as // `Call(target=unless)` and is treated identically to `if`. + #[cfg(feature = "elixir")] #[test] fn elixir_unless_counts() { check_metrics::( @@ -5640,6 +5807,7 @@ f() { // `for x <- list, do: ...` is Elixir's comprehension generator — // a `Call(target=for)`. Counts once for both standard and // modified, mirroring `if`/`unless`. + #[cfg(feature = "elixir")] #[test] fn elixir_for_comprehension_counts() { check_metrics::( @@ -5665,6 +5833,7 @@ f() { // `Call`, so it adds no modified-CCN container decision. // Standard = 4 entries (Unit, defmodule, def, anon-fn) + 1 counted // branch (`_ ->`) = 5; modified = 4 entries = 4. + #[cfg(feature = "elixir")] #[test] fn elixir_anonymous_fn_arms_count() { check_metrics::( @@ -5688,6 +5857,7 @@ f() { // `_ ->` (2 counted arms there: the bare `_ ->` is free but the // container's arms 1 and 2 count). Standard = 4 entries (Unit, // defmodule, def, anon-fn) + 2 branches = 6; modified = 4. + #[cfg(feature = "elixir")] #[test] fn elixir_multi_clause_fn_catchall_composition() { check_metrics::( @@ -5705,6 +5875,7 @@ f() { // `cond`'s `do_block`, so the cond-default exclusion must not fire // (issue #1272). Standard = 4 entries (Unit, defmodule, def, // anon-fn) + 1 branch (the `true ->` clause) = 5; modified = 4. + #[cfg(feature = "elixir")] #[test] fn elixir_fn_true_clause_counts() { check_metrics::( @@ -5725,6 +5896,7 @@ f() { // `elixir_enum_reduce_is_zero`). Before the fix the head clause // added a spurious +1, reporting 2. Standard = 4 entries (Unit, // defmodule, def, anon-fn) + 0 branches = 4; modified = 4. + #[cfg(feature = "elixir")] #[test] fn elixir_single_clause_anonymous_fn_is_not_a_branch() { check_metrics::( @@ -5746,6 +5918,7 @@ f() { // anyway (#1272). Standard = 4 entries (Unit, defmodule, def, // anon-fn) + 1 branch = 5; modified = 4 entries, the `fn` itself // being no container Call. + #[cfg(feature = "elixir")] #[test] fn elixir_zero_arity_multi_clause_fn_counts_second_clause() { check_metrics::( @@ -5768,6 +5941,7 @@ f() { // default, the analogue of `if`/`elif`/`else`'s free `else` // (issue #1272). The `cond` Call is a multi-arm container // (modified CCN, once). + #[cfg(feature = "elixir")] #[test] fn elixir_cond_arms() { check_metrics::( @@ -5786,6 +5960,7 @@ f() { // exclusion targets only the designated-default clause, not the // container's last arm (issue #1272). standard = 3 entries + // 2 stabs = 5; modified = 3 entries + 1 cond Call = 4. + #[cfg(feature = "elixir")] #[test] fn elixir_cond_without_default_counts_all_arms() { check_metrics::( @@ -5806,6 +5981,7 @@ f() { // family sets the precedent (issue #1272). standard = 3 entries + // 1 counted stab (`x > 5 ->`; the shadowing `true ->` is free) // = 4; modified = 3 entries + 1 cond Call = 4. + #[cfg(feature = "elixir")] #[test] fn elixir_cond_shadowing_true_arm_also_excluded() { check_metrics::( @@ -5825,6 +6001,7 @@ f() { // and the inner bare `_ ->` is free (case default). // standard: 3 entries + outer `x > 1 ->` + inner `true ->` = 5; // modified: 3 entries + cond Call + case Call = 5. + #[cfg(feature = "elixir")] #[test] fn elixir_nested_case_inside_cond_keeps_exclusions_scoped() { check_metrics::( @@ -5843,6 +6020,7 @@ f() { // branch, when present, contains `stab_clause`s that count for // standard. The `with` Call itself is a multi-arm container Call // that contributes once to modified CCN. + #[cfg(feature = "elixir")] #[test] fn elixir_with_else_only_counts_else_arms() { check_metrics::( @@ -5857,6 +6035,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_match_expression() { // Each `match_conditional_expression` arm (+1) but the default arm @@ -5899,6 +6078,7 @@ f() { } /// Modified CCN: PHP switch with 3 cases collapses to 1. + #[cfg(feature = "php")] #[test] fn php_switch_modified() { check_metrics::( @@ -5927,6 +6107,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_null_coalescing() { // `??` and `??=` are each one short-circuit decision (#231). @@ -5967,6 +6148,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_nullsafe_operator_436() { // Issue #436: PHP's nullsafe operator `?->` is a short-circuit @@ -5994,6 +6176,7 @@ f() { /// Modified CCN: nested switches contribute one decision each, not one /// total — the outer container does not absorb the inner one. + #[cfg(feature = "cpp")] #[test] fn cpp_nested_switch_modified() { check_metrics::( @@ -6037,6 +6220,7 @@ f() { /// Modified CCN: nested Rust matches each contribute one container. /// Bare `_ =>` arms are skipped. + #[cfg(feature = "rust")] #[test] fn rust_nested_match_modified() { check_metrics::( @@ -6079,6 +6263,7 @@ f() { /// Pin the empty-switch edge case: standard counts no arms (0) while /// modified still counts the container (+1) per Lizard's `-m`. + #[cfg(feature = "cpp")] #[test] fn cpp_empty_switch_modified() { check_metrics::("void f() { switch (x) {} }", "foo.c", |metric| { @@ -6108,6 +6293,7 @@ f() { /// Two nested `for` loops contribute +1 each on top of the function and /// unit decisions. No condition expressions, so `&&` / `||` do not fire. + #[cfg(feature = "c")] #[test] fn c_nested_loops() { check_metrics::( @@ -6156,6 +6342,7 @@ f() { /// statement node would double-count — see the macro doc comment /// and issue #284. This test pins the correct keyword-driven /// count. + #[cfg(feature = "cpp")] #[test] fn cpp_do_statement_counts_in_cyclomatic() { check_metrics::( @@ -6202,6 +6389,7 @@ f() { /// inside a classic `ForStatement`. Pinning this prevents /// reintroducing the double-count from issue #284's incorrect fix /// proposal. + #[cfg(feature = "cpp")] #[test] fn cpp_for_range_loop_counts_in_cyclomatic() { check_metrics::( @@ -6245,6 +6433,7 @@ f() { /// add +1; `switch` adds only to the modified count. C has no /// `catch`, so the hand-written `Cyclomatic for CCode` impl omits /// the exception arm the C++ macro carries. + #[cfg(feature = "c")] #[test] fn c_grammar_decision_kinds_count_in_cyclomatic() { check_metrics::( @@ -6276,6 +6465,7 @@ f() { /// `?:` ternary is matched by `Cpp::ConditionalExpression` in the /// C-family macro and contributes +1 standard *and* +1 modified. /// Two nested ternaries in one expression therefore add 2 to each. + #[cfg(feature = "c")] #[test] fn c_ternary_chain() { check_metrics::( @@ -6314,6 +6504,7 @@ f() { /// Short-circuit `&&` / `||` chains each contribute +1 — every binary /// operator token in the chain is a separate decision (Lizard parity). + #[cfg(feature = "c")] #[test] fn c_short_circuit_chain() { check_metrics::( @@ -6356,6 +6547,7 @@ f() { /// Switch with intentional fall-through: every `case` adds +1 standard /// regardless of whether the arm `break`s. Modified collapses all three /// arms into one switch container. + #[cfg(feature = "c")] #[test] fn c_switch_fallthrough() { check_metrics::( @@ -6410,6 +6602,7 @@ f() { /// Lizard, which also does not count `goto`. This test pins that /// decision so a future change that adds `Cpp::GotoStatement` to the /// macro fires here first. + #[cfg(feature = "c")] #[test] fn c_goto_not_counted() { check_metrics::( @@ -6456,6 +6649,7 @@ f() { /// the values we expect from a known fixture, bypassing the JSON /// serializer. Modified must never exceed standard for non-degenerate /// inputs (a switch with at least one arm). + #[cfg(feature = "rust")] #[test] fn cyclomatic_modified_accessors() { check_metrics::( @@ -6483,6 +6677,7 @@ f() { } /// Bare `_ =>` wildcard is not counted (matches C-family `default:`). + #[cfg(feature = "rust")] #[test] fn rust_wildcard_only_match() { check_metrics::( @@ -6519,6 +6714,7 @@ f() { } /// Wildcard arm plus explicit arms: only explicit arms count. + #[cfg(feature = "rust")] #[test] fn rust_wildcard_plus_explicit_arms() { check_metrics::( @@ -6558,6 +6754,7 @@ f() { } /// `Some(_)` is NOT a bare wildcard — still counts. + #[cfg(feature = "rust")] #[test] fn rust_some_wildcard_still_counts() { check_metrics::( @@ -6595,6 +6792,7 @@ f() { } /// Tuple pattern `(_, x)` is NOT a bare wildcard — still counts. + #[cfg(feature = "rust")] #[test] fn rust_tuple_wildcard_still_counts() { check_metrics::( @@ -6633,6 +6831,7 @@ f() { /// `_ if guard` is NOT a bare wildcard — still counts. /// The `if` keyword inside the guard also contributes +1 standard/modified. + #[cfg(feature = "rust")] #[test] fn rust_guarded_wildcard_still_counts() { check_metrics::( @@ -6672,6 +6871,7 @@ f() { /// Regression #107: empty case…esac has no arms, so standard adds 0 and /// modified adds 1 (the container). + #[cfg(feature = "bash")] #[test] fn bash_case_empty() { check_metrics::( @@ -6709,6 +6909,7 @@ f() { /// Regression #107: nested case…esac — each container contributes to /// modified independently, and each arm contributes to standard. + #[cfg(feature = "bash")] #[test] fn bash_nested_case() { check_metrics::( @@ -6752,6 +6953,7 @@ f() { } /// Nested matches with wildcards: only bare `_` skipped at each level. + #[cfg(feature = "rust")] #[test] fn rust_nested_match_with_wildcards() { check_metrics::( @@ -6792,6 +6994,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_nested_branches() { // expected: unit(1) + method(1 + `if` + `while`) = 1 + 3 = 4 @@ -6806,6 +7009,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_case_when_arms() { // Each `when` arm adds standard CCN; the `case` container is @@ -6823,6 +7027,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_case_match_default_only_arm_not_counted() { // Regression for #977: a `case … in` whose only arm is the bare @@ -6842,6 +7047,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_case_match_in_arms_and_guard_counted() { // Regression for #977: a non-wildcard `in 1` arm and a guarded @@ -6871,6 +7077,7 @@ f() { /// function is just its base 1. Per-language snapshot suites pin each /// history but cannot catch the cross-language disagreement this /// guards (lesson 11; #106 was exactly a wildcard-counting drift). + #[cfg(all(feature = "python", feature = "ruby", feature = "rust"))] #[test] fn cyclomatic_bare_wildcard_default_arm_cross_language() { check_metrics::( @@ -6890,6 +7097,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_ternary_conditional() { // Ruby's `cond ? a : b` parses as `Conditional` and counts as a @@ -6905,6 +7113,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_and_or_keywords() { // Word-form `and` / `or` are distinct grammar kinds from @@ -6937,6 +7146,7 @@ f() { /// language asserts the literal 3.0 in its own closure so a future /// drift in any single language fails THIS test (and only this /// test), making cross-language disagreement visible at a glance. + #[cfg(all(feature = "java", feature = "ruby", feature = "rust"))] #[test] fn cyclomatic_if_elseif_else_chain_cross_language() { check_metrics::( @@ -6985,8 +7195,10 @@ f() { /// modified assertion a mutation that drops /// `stats.cyclomatic_modified += 1.` from any shared arm (or /// drops the `Switch` arm entirely) would pass. + #[cfg(all(feature = "groovy", feature = "java"))] #[test] fn cyclomatic_java_groovy_parity_300() { + #[cfg(any(feature = "groovy", feature = "java"))] const JAVA_SRC: &str = "class C {\n\ int decide(int x, int y, int[] xs) {\n\ int r = 0;\n\ @@ -6999,6 +7211,7 @@ f() { return r;\n\ }\n\ }\n"; + #[cfg(any(feature = "groovy", feature = "java"))] const GROOVY_SRC: &str = "class C {\n\ int decide(int x, int y, int[] xs) {\n\ int r = 0\n\ @@ -7036,6 +7249,7 @@ f() { /// statement is grammar-distinct and not in this macro's arm). /// Dropping `[Assert]` from the Groovy invocation would fail this /// test. + #[cfg(feature = "groovy")] #[test] fn cyclomatic_groovy_assert_arm_300() { check_metrics::("void check(int x) { assert x > 0 }", "foo.groovy", |m| { @@ -7062,6 +7276,7 @@ f() { /// `elvis_expression` node with a real `QMARKCOLON` token, so the /// `impl_cyclomatic_java_like!(GroovyCode, Groovy, [Assert, /// QMARKCOLON])` invocation picks it up directly. + #[cfg(feature = "groovy")] #[test] fn cyclomatic_groovy_elvis_chain_246() { check_metrics::( @@ -7080,6 +7295,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_rescue_modifier() { // Postfix `x rescue y` parses as a `RescueModifier` node that @@ -7097,6 +7313,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_safe_navigation_cyclomatic() { // Issue #452: Ruby's safe-navigation `&.` (AMPDOT) is a @@ -7116,6 +7333,7 @@ f() { /// Nested control flow inside a `when` handler (the iRules floor case, /// mirroring `rust_1_level_nesting`). unit(1) + handler(base 1 + while 1 /// + if 1 = 3) = sum 4, max 3. + #[cfg(feature = "irules")] #[test] fn irules_1_level_nesting() { check_metrics::( @@ -7140,6 +7358,7 @@ f() { /// standard decision; the whole `switch` is one modified decision. /// standard: unit(1) + handler(base 1 + 2 arms) = 4; modified: /// unit(1) + handler(base 1 + switch 1) = 3. The `default` arm is free. + #[cfg(feature = "irules")] #[test] fn irules_switch() { check_metrics::( @@ -7165,6 +7384,7 @@ f() { /// like `&&` / `||` (iRules-specific — Tcl's grammar has no keyword /// forms). unit(1) + handler(base 1 + if 1 + and 1 + or 1 = 4) = 5. /// Guards edge case #3 / the keyword-operator arms in the impl. + #[cfg(feature = "irules")] #[test] fn irules_and_or_keywords() { check_metrics::( @@ -7190,6 +7410,7 @@ f() { /// case #4: if each string operator were wrongly counted as a branch the /// sum would be 6, so the divergence (4 vs 6) is unambiguous — it cannot /// be confused with the `if`/`||` simply being miscounted. + #[cfg(feature = "irules")] #[test] fn irules_string_ops_not_branches() { check_metrics::( @@ -7210,6 +7431,7 @@ f() { /// A ternary `? :` in an `expr` is one decision; the bare `>` comparison /// is not. unit(1) + handler(base 1 + ternary 1 = 2) = 3. + #[cfg(feature = "irules")] #[test] fn irules_ternary() { check_metrics::( @@ -7229,6 +7451,7 @@ f() { /// `dict for` iterates and is a loop decision; the non-looping /// `dict update` / `dict with` are excluded by the impl. /// unit(1) + handler(base 1 + dict_for 1 = 2) = 3. + #[cfg(feature = "irules")] #[test] fn irules_dict_for_loop() { check_metrics::( @@ -7251,6 +7474,7 @@ f() { /// `method_definition` held by an `@implementation`. The /// `@implementation` opens a Class space (+1). Standard CCN = /// unit(1) + class(1) + method(1) + for(1) + if(1) = 5. + #[cfg(feature = "objc")] #[test] fn objc_nested_control() { check_metrics::( @@ -7290,6 +7514,7 @@ f() { /// Objective-C `@try { } @catch { }`: the `catch_clause` node adds /// one decision point. Standard CCN = unit(1) + class(1) + method(1) /// + catch(1) = 4. + #[cfg(feature = "objc")] #[test] fn objc_try_catch() { check_metrics::( @@ -7330,6 +7555,7 @@ f() { /// `for_statement` whose `for` keyword fires once, exactly like a /// classic `for`. Standard CCN = unit(1) + class(1) + method(1) + /// for(1) = 4. + #[cfg(feature = "objc")] #[test] fn objc_fast_enumeration() { check_metrics::( diff --git a/src/metrics/halstead.rs b/src/metrics/halstead.rs index b9f44393c..f0f8f743b 100644 --- a/src/metrics/halstead.rs +++ b/src/metrics/halstead.rs @@ -505,6 +505,20 @@ mod tests { /// Runs the `--ops` walk over `source` and returns the root space's /// merged vocabulary, with every nested space still reachable /// through [`crate::ops::Ops::spaces`]. + #[cfg(any( + feature = "c", + feature = "cpp", + feature = "csharp", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "kotlin", + feature = "mozcpp", + feature = "objc", + feature = "perl", + feature = "ruby", + feature = "tcl", + ))] fn ops_of(source: &str, file: &str) -> crate::ops::Ops { let path = PathBuf::from(file); let parser = T::new(source.as_bytes().to_vec(), &path, None); @@ -522,6 +536,18 @@ mod tests { // (`assert_char_literal_operands`, #1316) are tracked too, so the // reported location names the language row instead of a shared line // no assertion message distinguishes. + #[cfg(any( + feature = "c", + feature = "cpp", + feature = "groovy", + feature = "irules", + feature = "kotlin", + feature = "mozcpp", + feature = "objc", + feature = "perl", + feature = "ruby", + feature = "tcl", + ))] #[track_caller] fn assert_ops_operands( source: &str, @@ -553,6 +579,12 @@ mod tests { /// `n2`/`N2` — and an assertion that only looked for its arrival /// among the operands would pass on that. The operator side is what /// pins the removal. + #[cfg(any( + feature = "csharp", + feature = "groovy", + feature = "java", + feature = "kotlin" + ))] #[track_caller] fn assert_keywords_are_operands_only( source: &str, @@ -589,6 +621,7 @@ mod tests { /// no row, so the result is the set of places the keyword actually /// reaches — which is what distinguishes a parent gate from its own /// inverse, and what the merged root vocabulary cannot show. + #[cfg(feature = "csharp")] fn collect_keyword_roles(ops: &crate::ops::Ops, keyword: &str, out: &mut Vec) { let operator = ops.operators.iter().any(|o| o == keyword); let operand = ops.operands.iter().any(|o| o == keyword); @@ -632,6 +665,7 @@ mod tests { /// The fixtures deliberately exclude the two gated positions (C#'s /// indexer declarator, Java's wildcard bound); those are operators, /// and their own tests pin them. + #[cfg(any(feature = "csharp", feature = "java", feature = "kotlin"))] #[track_caller] fn assert_self_reference_leaves( source: &str, @@ -703,6 +737,26 @@ mod tests { /// plain `fn` that cannot capture a loop variable, so they reach /// for the closure-taking helper it wraps. Three copies of that /// dance is two too many. + #[cfg(any( + feature = "bash", + feature = "c", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "ruby", + feature = "tcl", + feature = "typescript", + ))] fn assert_halstead_counts( source: &str, file: &str, @@ -729,6 +783,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_operators_and_operands() { check_metrics::( @@ -774,6 +829,7 @@ mod tests { /// once in `n1`; multiple uses bump `N1`. The headline integer values /// (`u_operators`, `u_operands`) anchor the snapshot per the /// snapshot-anchor policy. + #[cfg(feature = "c")] #[test] fn c_pointer_arithmetic_operators() { check_metrics::( @@ -798,6 +854,7 @@ mod tests { /// `!`) operators are distinct kind_ids and count as separate unique /// operators in Halstead. `&` (bitwise-and) and `&&` (logical-and) /// must NOT collapse, even though both render as ampersands. + #[cfg(feature = "c")] #[test] fn c_bitwise_and_logical_operators() { check_metrics::( @@ -830,6 +887,7 @@ mod tests { /// each contribute distinct unique operators. C-style casts in the /// tree-sitter grammar surface as `cast_expression` with the type /// token classified as a primitive_type operator. + #[cfg(feature = "c")] #[test] fn c_increment_decrement_and_sizeof() { check_metrics::( @@ -855,6 +913,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_operators_and_operands() { // Define operators and operands for C/C++ grammar according to this specification: @@ -908,6 +967,7 @@ mod tests { /// contributed nothing. They must each count as a distinct operator, /// while `long long`'s two `long` tokens fold to one `n1` entry but /// two `N1` hits. Regression test for issue #466. + #[cfg(feature = "cpp")] #[test] fn cpp_sized_type_specifier_operators() { let source = "unsigned int u = 3; signed long b = 4; long long c = 5;"; @@ -953,6 +1013,7 @@ mod tests { /// dropped from `n1` / `N1`, under-reporting volume / effort on any /// C++20+ codebase that defines `operator<=>`. Regression test for /// issue #197. + #[cfg(feature = "cpp")] #[test] fn cpp_spaceship_operator_is_halstead_operator() { check_metrics::( @@ -1000,6 +1061,7 @@ mod tests { /// arm and was silently dropped from `n1` / `N1` — under-reporting /// volume / effort wherever C++ code subtracts in place. Regression /// test for issue #198. + #[cfg(feature = "cpp")] #[test] fn cpp_dash_eq_is_halstead_operator() { check_metrics::("void f(int a, int b) { a -= b; }", "foo.cpp", |metric| { @@ -1023,6 +1085,7 @@ mod tests { /// `DOTSTAR` leaf; in expression position (`a.*b`) some grammar /// versions split the token into `DOT` + `STAR` and the regression /// would be masked. + #[cfg(feature = "cpp")] #[test] fn cpp_dot_star_is_halstead_operator() { check_metrics::("struct S { void operator.*(int); };", "foo.cpp", |metric| { @@ -1046,6 +1109,7 @@ mod tests { /// `DASHGTSTAR` leaf; in expression position (`a->*b`) the grammar /// splits the token into `DASHGT` + `STAR` and the regression would /// be masked. + #[cfg(feature = "cpp")] #[test] fn cpp_dash_gt_star_is_halstead_operator() { check_metrics::( @@ -1063,6 +1127,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_raw_string_delimiter_is_not_an_operator() { // Regression: issue #1314, the C++ sibling of Elixir #1256 and @@ -1096,6 +1161,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_operators_and_operands() { check_metrics::( @@ -1133,6 +1199,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_aliased_primitive_type_classification() { // Regression for issue #95 (lesson #2): the Rust grammar emits 17 @@ -1202,6 +1269,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_field_identifier_is_operand() { // Regression for issue #390: prior to the fix, `FieldIdentifier` @@ -1251,6 +1319,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_type_identifier_is_operand() { // Regression for issue #390: `TypeIdentifier` (e.g. `Vec`, @@ -1304,6 +1373,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_path_separator_is_operator() { // Regression for issue #394: `::` (`COLONCOLON`) was missing @@ -1340,6 +1410,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_declaration_keywords_are_operators() { // Regression for issue #394: the Rust impl already accepted 17 @@ -1370,6 +1441,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_operators_and_operands() { check_metrics::( @@ -1412,6 +1484,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_operators_and_operands() { check_metrics::( @@ -1454,6 +1527,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_operators_and_operands() { check_metrics::( @@ -1496,6 +1570,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_operators_and_operands() { check_metrics::( @@ -1538,6 +1613,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_template_string_plain_is_operand() { // Regression: issue #192. A backtick-delimited `` `hello` `` @@ -1560,6 +1636,7 @@ mod tests { /// operands, so the same accessor keyword landed in opposite Halstead /// groups across languages. This pins them in the operator store and /// out of the operand store. + #[cfg(feature = "javascript")] #[test] fn js_get_set_accessors_are_operators() { let source = "class C { get x() { return 1; } set x(v) { this._x = v; } }"; @@ -1580,6 +1657,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_template_string_interpolation_no_double_count() { // Regression: issue #192. An interpolated template literal @@ -1607,6 +1685,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_template_string_plain_is_operand() { // Regression: issue #192. Mirrors @@ -1619,6 +1698,7 @@ mod tests { }); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_template_string_interpolation_no_double_count() { // Regression: issue #192. Mirrors @@ -1634,6 +1714,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_template_string_plain_is_operand() { // Regression: issue #192. Mirrors @@ -1656,6 +1737,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_template_string_interpolation_no_double_count() { // Regression: issue #192. Mirrors @@ -1677,6 +1759,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_template_string_plain_is_operand() { // Regression: issue #192. Mirrors @@ -1696,6 +1779,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_template_string_interpolation_no_double_count() { // Regression: issue #192. Mirrors @@ -1722,6 +1806,7 @@ mod tests { /// 224/250/264/225), so each expansion is a separate compiled arm /// and a drift in one grammar is invisible if only one is checked /// (grammar-dispatch section 11). + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] fn assert_js_family_counts(source: &str, expected: [u64; 4]) { assert_halstead_counts::(source, "foo.js", expected, "javascript"); assert_halstead_counts::(source, "foo.jsm", expected, "mozjs"); @@ -1729,6 +1814,7 @@ mod tests { assert_halstead_counts::(source, "foo.tsx", expected, "tsx"); } + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_family_regex_delimiters_are_not_operators() { // Regression: issue #1314, the JS-family sibling of Elixir @@ -1756,6 +1842,7 @@ mod tests { assert_js_family_counts("const a = /abc/g;\nlet b = a;\nb = a;\n", [4, 8, 3, 6]); } + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_family_division_survives_the_regex_guard() { // Control for #1314: the guard is scoped to a `Regex` parent, @@ -1770,6 +1857,7 @@ mod tests { assert_js_family_counts("const q = a / b / c;\nconst r = /x/;\n", [4, 8, 6, 6]); } + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_regex_delimiter_guard_is_parent_scoped_is_unobservable() { // Companion to the two above, and a statement of what they do @@ -1828,6 +1916,7 @@ mod tests { /// /// Backs `js_regex_delimiter_guard_is_parent_scoped_is_unobservable` /// — see there for why the property is worth pinning. + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] fn assert_regex_slashes_are_immediate_children( source: &[u8], slash: u16, @@ -1877,6 +1966,7 @@ mod tests { // unique — `LPAREN`/`LBRACE` count once, closing tokens are not // in the operator set). Before the fix, TS/TSX reported 9/7 // instead of 7/6. + #[cfg(feature = "javascript")] #[test] fn javascript_optional_chain_not_double_counted_in_halstead_281() { check_metrics::("function f(a) { return a?.b?.c; }", "foo.js", |m| { @@ -1885,6 +1975,7 @@ mod tests { }); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_optional_chain_not_double_counted_in_halstead_281() { check_metrics::("function f(a) { return a?.b?.c; }", "foo.js", |m| { @@ -1893,6 +1984,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn typescript_optional_chain_not_double_counted_in_halstead_281() { // The TS grammar wraps member-expression `?.` in an @@ -1905,6 +1997,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn tsx_optional_chain_not_double_counted_in_halstead_281() { check_metrics::("function f(a) { return a?.b?.c; }", "foo.tsx", |m| { @@ -1937,12 +2030,14 @@ mod tests { // separate fixture. The `PredefinedType` operator path (`: void` // double-count) is now covered by `ts_void_return_type_single_operator_453` // below. + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_family_get_op_type_parity_optional_chain_member_299() { // Non-capturing closure (coerced to the `fn` pointer that // `check_metrics` accepts) avoids the // `clippy::needless_pass_by_value` warning that a free `fn` // taking `CodeMetrics` by value would trigger. + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] const SRC: &str = "function f(a) { return a?.b?.c; }"; let check = |m: crate::CodeMetrics| { assert_eq!(m.halstead.unique_operators(), 6); @@ -1973,8 +2068,10 @@ mod tests { // `impl_js_family_get_op_type!` emits one shared operand arm: the // lockstep is the point of the macro, and a per-language extras // list is exactly where a future edit could break it. + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_family_member_access_counts_leaves_not_the_composite_1263() { + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] const SRC: &str = "var r = a.b;"; let check = |m: crate::CodeMetrics| { assert_eq!(m.halstead.unique_operators(), 4); @@ -2008,8 +2105,10 @@ mod tests { // parses as `type_identifier`, which those getters do not // classify, so both counts drop by one to 5/4 — a pre-existing // divergence this fixture records rather than fixes. + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_family_private_field_leaf_is_the_operand_1263() { + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] const SRC: &str = "class C { #x = 1; m() { return this.#x; } }"; let check_js = |m: crate::CodeMetrics| { assert_eq!(m.halstead.unique_operators(), 6); @@ -2043,8 +2142,10 @@ mod tests { // asserted: the `import` / `new` keyword tokens inside the // meta-property keep their pre-existing operator classification, // which this fixture neither pins nor contests. + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_family_meta_property_is_one_operand_1263() { + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] const SRC: &str = "var t = import.meta.url; function f() { return new.target; }"; let check = |m: crate::CodeMetrics| { assert_eq!(m.halstead.unique_operands(), 5); @@ -2066,6 +2167,7 @@ mod tests { // the JS-family operator arm.) // * Operands: `N`, `M` — 2 total, 2 unique. Before the fix the // `nested_identifier` added `N.M`, making both 3. + #[cfg(feature = "typescript")] #[test] fn ts_nested_identifier_counts_leaves_not_the_composite_1263() { const SRC: &str = "namespace N.M { }"; @@ -2098,6 +2200,7 @@ mod tests { // Verified by test-via-revert: restoring `String2` to TS's // `operand_extras` (or `String3` to TSX's) trips this test on // `u_operands` / `operands` for the affected language. + #[cfg(feature = "typescript")] #[test] fn ts_family_type_keyword_counts_once_1261() { const SRC: &str = "let x: string = \"y\";"; @@ -2126,6 +2229,7 @@ mod tests { // operands — while a string *literal* `"string"` stays an operand // (distinct from the keyword: TS kind `String`, TSX kind `String2`, // both quoted in the operand key). + #[cfg(feature = "typescript")] #[test] fn ts_family_string_annotation_symmetric_with_number_1261() { const SRC: &str = "let x: string = \"a\";\nlet y: number = 1;\nlet s = \"string\";"; @@ -2162,6 +2266,7 @@ mod tests { /// generic argument, and template-literal type. A string *literal* /// spelling `"string"` is a different kind and must not be confused /// for the keyword, so one is in the fixture too. + #[cfg(feature = "typescript")] #[test] fn ts_family_type_keyword_only_appears_under_predefined_type_1261() { // Exercises each position the keyword can take. Valid in both @@ -2251,6 +2356,7 @@ mod tests { // (one kind_id-keyed, one in `primitive_operators`). Both `metrics()` // and the `ops`-list dedup invariant (`ts_void_return_and_expression_*` // in `ops.rs`) are pinned per lesson 4. + #[cfg(feature = "typescript")] #[test] fn ts_void_return_type_single_operator_453() { const SRC: &str = "function f(): void { return; }"; @@ -2273,6 +2379,7 @@ mod tests { // // * Operators (n1 = 4, N1 = 4): `const`, `=`, `void`, `;`. // * Operands (n2 = 2, N2 = 2): `x`, `0`. + #[cfg(feature = "typescript")] #[test] fn ts_void_expression_still_single_operator_453() { const SRC: &str = "const x = void 0;"; @@ -2287,6 +2394,7 @@ mod tests { check_metrics::(SRC, "foo.tsx", check); } + #[cfg(feature = "python")] #[test] fn python_wrong_operators() { check_metrics::("()[]{}", "foo.py", |metric| { @@ -2314,6 +2422,7 @@ mod tests { }); } + #[cfg(feature = "python")] #[test] fn python_check_metrics() { check_metrics::( @@ -2346,6 +2455,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_operators_and_operands() { check_metrics::( @@ -2386,6 +2496,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_primitive_types_and_booleans() { check_metrics::( @@ -2437,6 +2548,7 @@ mod tests { // scored `this.x` as a binary operator with one operand while `p.x` // is one operator and two operands. Both are operands now, matching // the eleven other languages that classify a self-reference. + #[cfg(feature = "java")] #[test] fn java_self_and_super_references_are_operands() { let source = "class T {\n int f() { return this.x + super.y; }\n}"; @@ -2461,6 +2573,7 @@ mod tests { // the independent path through the operand arm (grammar-dispatch // section 11): only it can put `this` in the operand vocabulary // here, and only the wildcard can put `super` in the operator one. + #[cfg(feature = "java")] #[test] fn java_wildcard_super_bound_stays_an_operator() { let source = "import java.util.List;\n\ @@ -2493,6 +2606,7 @@ mod tests { /// call, an explicit superclass constructor call, a field access, a /// method-invocation receiver, a method reference, a call argument, /// and the two qualified forms an inner class allows. + #[cfg(feature = "java")] const JAVA_SELF_POSITIONS: &str = "class Pos extends P { int x; Pos() { this(1); } @@ -2508,6 +2622,7 @@ mod tests { } }"; + #[cfg(feature = "java")] #[test] fn java_self_and_super_leaves_are_unaliased_and_unconditional() { assert_self_reference_leaves::( @@ -2533,6 +2648,7 @@ mod tests { // deliberately untested rather than pinned against an invalid // fixture. The node census is the drift marker for it: a grammar bump // that routes a reference to `Groovy::Super` fails here by name. + #[cfg(feature = "groovy")] #[test] fn groovy_wildcard_super_bound_stays_an_operator() { let bounds = @@ -2589,6 +2705,7 @@ mod tests { assert_keywords_are_operands_only::(refs, "foo.groovy", &["super", "this"]); } + #[cfg(feature = "groovy")] #[test] fn groovy_operators_and_operands() { check_metrics::( @@ -2644,6 +2761,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_primitive_types_and_booleans() { check_metrics::( @@ -2714,6 +2832,7 @@ mod tests { // expected, for `package com.example`: operators `.` (1/1); // operands `com`, `example` (2/2). Pre-fix the `qualified_name` // added `com.example`, making the operand counts 3/3. + #[cfg(feature = "groovy")] #[test] fn groovy_qualified_name_counts_leaves_not_the_composite_1263() { check_metrics::("package com.example", "foo.groovy", |metric| { @@ -2742,6 +2861,7 @@ mod tests { // `=` → n1 = 2, N1 = 3; operands `java`, `util`, `List`, `x`, // `null` → n2 = 5, N2 = 5. Listing the wrapper would add the whole // span `java.util.List`, making the operand counts 6/6. + #[cfg(feature = "groovy")] #[test] fn groovy_qualified_type_counts_leaves_not_the_composite_1352() { const SOURCE: &str = "java.util.List x = null"; @@ -2771,6 +2891,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_closure_operators_and_operands() { check_metrics::("def double = { x -> x * 2 }", "foo.groovy", |metric| { @@ -2793,6 +2914,7 @@ mod tests { /// `?[` safe index — every distinct operator kind must appear in /// `u_operators` (the count grows by exactly the number of new /// distinct operator tokens introduced). + #[cfg(feature = "groovy")] #[test] fn groovy_dekobon_operator_coverage_247() { check_metrics::( @@ -2850,6 +2972,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_gstring_no_double_count() { // Issue #454: before the fix Groovy had no interpolation guard @@ -2876,6 +2999,7 @@ mod tests { assert_ops_operands::(src, "foo.groovy", 2, vec!["greet", "name"]); } + #[cfg(feature = "groovy")] #[test] fn groovy_gstring_dollar_form_no_double_count() { // Issue #454: the short `$name` GString form emits a distinct @@ -2896,6 +3020,7 @@ mod tests { assert_ops_operands::(src, "foo.groovy", 3, vec!["greet", "name", "$name"]); } + #[cfg(feature = "groovy")] #[test] fn groovy_plain_string_still_operand() { // Counterpart to `groovy_gstring_no_double_count`: a plain @@ -2912,6 +3037,7 @@ mod tests { assert_ops_operands::(src, "foo.groovy", 2, vec!["f", "\"plain\""]); } + #[cfg(feature = "groovy")] #[test] fn groovy_slashy_string_delimiter_is_not_an_operator() { // Regression: issue #1314, the Groovy sibling of Elixir #1256 @@ -2937,6 +3063,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_division_survives_the_slashy_guard() { // Control for #1314: the guard is scoped to a `StringLiteral` @@ -2955,6 +3082,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_slashy_guard_is_parent_scoped_not_ancestor_scoped() { // The input that separates the parent-scoped guard from the @@ -2988,6 +3116,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_every_string_spelling_scores_alike() { // Companion to the three above (#1314). Groovy has five ways to @@ -3017,6 +3146,7 @@ mod tests { } } + #[cfg(feature = "csharp")] #[test] fn csharp_operators_and_operands() { // After issue #286, `void`, `string`, and `int` count as three @@ -3058,6 +3188,7 @@ mod tests { // Three fixtures rather than one, so a regression names which // container came back. Each is hand-tallied; the removed composite // is called out per case. + #[cfg(feature = "csharp")] #[test] fn csharp_name_containers_count_leaves_not_the_composite_1263() { // expected: operators `using`, `.`, `;` (3/3); operands @@ -3105,6 +3236,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_primitive_types_and_booleans() { // After issue #286: each of `byte`, `short`, `int`, `long`, @@ -3147,6 +3279,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_boolean_literal_counts_once() { // Regression: issue #1253. `boolean_literal: choice('true', @@ -3173,6 +3306,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_boolean_keyword_outside_a_literal_still_counts() { // Companion to the test above (#1253): the suppression fires on @@ -3203,6 +3337,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_predefined_types_keyed_by_lexeme() { // Regression: issue #286. The C# grammar emits one `PredefinedType` @@ -3229,6 +3364,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_interpolated_string_no_double_count() { // Regression: issue #183. A C# `$"Hi {name}!"` used to be @@ -3258,6 +3394,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_static_interpolated_string_is_operand() { // Regression: issue #183. A `$"..."` with no `{...}` is @@ -3277,6 +3414,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_plain_string_still_operand() { // The fix for #183 only changes how `InterpolatedStringExpression` @@ -3297,6 +3435,7 @@ mod tests { // C# half of #1380 — see `java_self_and_super_references_are_operands` // for the structural argument. `base` moves with `this`: both are // receivers of a member access. + #[cfg(feature = "csharp")] #[test] fn csharp_self_and_base_references_are_operands() { let source = "class T {\n int F() { return this.x + base.y; }\n}"; @@ -3323,6 +3462,7 @@ mod tests { // Asserting only the operand side would pass with the gate deleted, // and only the operator side would pass with the whole #1380 change // reverted (grammar-dispatch section 11). + #[cfg(feature = "csharp")] #[test] fn csharp_indexer_declaration_keyword_is_not_a_self_reference() { let source = "class C {\n int[] _a;\n \ @@ -3370,6 +3510,7 @@ mod tests { /// initializer, a base-constructor initializer, a member access, an /// element access, and a call argument. No `indexer_declaration` — /// that position is the gated one and is an operator. + #[cfg(feature = "csharp")] const CSHARP_SELF_POSITIONS: &str = "class Pos : B { int[] _a; public Pos() : this(1) { } @@ -3382,6 +3523,7 @@ mod tests { void M(object o) { } }"; + #[cfg(feature = "csharp")] #[test] fn csharp_self_and_base_leaves_are_unaliased_and_unconditional() { assert_self_reference_leaves::( @@ -3394,6 +3536,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_operators_and_operands() { check_metrics::( @@ -3428,6 +3571,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_operators_and_operands() { check_metrics::( @@ -3462,6 +3606,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_interpolated_string_no_double_count() { // Regression: issue #199. A `string_double_quoted` (and @@ -3492,6 +3637,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_plain_string_still_operand() { // The fix for #199 only skips wrapping literals that carry an @@ -3509,6 +3655,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_single_quoted_string_never_interpolates() { // Single-quoted (`'…'`) and `q{…}` literals are not subject to @@ -3527,6 +3674,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_plain_heredoc_counts_as_one_operand() { // Regression: issue #287. A plain (non-interpolating) Perl @@ -3552,6 +3700,7 @@ mod tests { }); } + #[cfg(feature = "perl")] #[test] fn perl_interpolated_heredoc_no_double_count() { // Regression: issue #287. An interpolating Perl heredoc @@ -3585,6 +3734,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_bare_pattern_delimiters_are_not_operators() { // Regression: issue #1312, the Perl sibling of Elixir #1256. @@ -3608,6 +3758,7 @@ mod tests { }); } + #[cfg(feature = "perl")] #[test] fn perl_every_pattern_value_spelling_scores_alike() { // Companion to the test above (#1312, extended by #1314). @@ -3654,6 +3805,7 @@ mod tests { } } + #[cfg(feature = "perl")] #[test] fn perl_every_pattern_operation_spelling_scores_alike() { // The other half of the split (#1314). Substitution and @@ -3679,6 +3831,7 @@ mod tests { } } + #[cfg(feature = "perl")] #[test] fn perl_interpolated_pattern_operands_agree_but_operators_do_not() { // Two things at once (#1314), because they are the same @@ -3730,6 +3883,7 @@ mod tests { } } + #[cfg(feature = "perl")] #[test] fn perl_division_emits_no_slash_token() { // Drift marker, not an endorsement. Ruby's counterpart @@ -3793,6 +3947,7 @@ mod tests { /// components, the kinds it subsumes the distinct second ones. /// `perl_name_wrappers_bill_the_name_once_1355` witnesses every row /// and fails on an eleventh pairing. + #[cfg(feature = "perl")] const PERL_NAME_WRAPPER_PAIRINGS: [(Perl, Perl); 10] = [ (Perl::PackageName, Perl::Identifier), (Perl::PackageName, Perl::ScalarVariable), @@ -3815,6 +3970,7 @@ mod tests { /// token-shaped kinds too (`True`, `FILE`, `SUB`, …), and a bump /// that let one of those inside a wrapper would otherwise be /// silenced with nothing failing. + #[cfg(feature = "perl")] const PERL_NAME_WRAPPER_TOKENS: [Perl; 4] = [Perl::COLONCOLON, Perl::STAR, Perl::LBRACE, Perl::RBRACE]; @@ -3825,6 +3981,7 @@ mod tests { /// ancestor chain exactly as `spaces::compute` does, so "parent" /// here means what `Ancestors::parent` means inside the guard /// rather than what a differently-built chain would say. + #[cfg(feature = "perl")] fn perl_subsumed_operands(source: &str) -> (Vec, HashSet<(u16, u16)>) { let wrappers: HashSet = PERL_NAME_WRAPPER_PAIRINGS .map(|(wrapper, _)| wrapper as u16) @@ -3913,6 +4070,7 @@ mod tests { /// test. What *is* pinned is the arm's position: moving it above the /// operator arm swallows `::`, `*` and the typeglob's opening brace, /// and the operator columns below fail. + #[cfg(feature = "perl")] #[test] fn perl_name_wrappers_bill_the_name_once_1355() { let cases: [PerlNameWrapperCase; 12] = [ @@ -4068,6 +4226,7 @@ mod tests { /// the `identifier` leaf and would be collateral damage. It does /// not: `use 'Foo.pm'` parses to a leaf holding only its two quote /// tokens, so it wraps nothing and is untouched either way. + #[cfg(feature = "perl")] #[test] fn perl_qw_list_bills_one_operand_per_element() { // `qw(a b c)` was invisible to Halstead — neither the elements, @@ -4118,6 +4277,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_qualified_name_leaves_still_count_elsewhere_1355() { // expected: operators `my` × 4, `$` × 3 (one per `$`-sigilled @@ -4157,6 +4317,7 @@ mod tests { ); } + #[cfg(feature = "lua")] #[test] fn lua_operators_and_operands() { check_metrics::( @@ -4203,6 +4364,7 @@ end", /// inflating n1 and N1. With the fix only the folded `(` opener counts: /// `local x = (1)` yields operators `local`, `=`, `()` — n1 = N1 = 3, /// with no standalone `)`. + #[cfg(feature = "lua")] #[test] fn lua_balanced_paren_counts_opener_only() { let source = "local x = (1)\n"; @@ -4243,8 +4405,10 @@ end", /// the pair glyph. If a grammar bump makes an alias id observable, this /// goes red and signals that the alias arms must additionally fold to /// the base in `get_operator_id_as_str` (the fix #768 proposed). + #[cfg(all(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))] #[test] fn second_alias_opener_collapses_to_base_kind_id() { + #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))] fn assert_no_alias( source: &str, file: &str, @@ -4272,6 +4436,7 @@ end", // Balanced openers must count once and render folded (no bare // `(`/`[`, no n1 inflation) — the property #768 feared was broken. + #[cfg(any(feature = "c", feature = "cpp", feature = "elixir", feature = "ruby"))] fn assert_folded_openers(source: &str, file: &str) { let path = PathBuf::from(file); let parser = T::new(source.as_bytes().to_vec(), &path, None); @@ -4330,6 +4495,7 @@ end", assert_folded_openers::("f(1)\nb = [1]\nb[0]\n", "b.rb"); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_halstead_basic() { check_metrics::( @@ -4364,6 +4530,7 @@ end", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_string_template_no_double_count() { // Re-anchored for issue #454. The pre-#454 comment claimed @@ -4406,6 +4573,7 @@ end", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_short_interpolation_counts_inner_not_wrapper() { // Issue #454: the short `$name` template — distinct from the @@ -4434,6 +4602,7 @@ end", assert_ops_operands::(src, "foo.kt", 4, vec!["f", "x", "println", "1"]); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_short_interpolation_space_separated() { // Issue #454 follow-up: tree-sitter-kotlin-ng splits the literal @@ -4482,6 +4651,7 @@ end", assert_ops_operands::(prose_long, "foo.kt", 3, vec!["f", "s", "x"]); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_dollar_non_identifier_stays_literal() { // Issue #454 boundary: a `$` not followed by a clean identifier @@ -4499,6 +4669,7 @@ end", assert_ops_operands::(src, "foo.kt", 3, vec!["f", "a", "\"price: $5\""]); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_string_template_long_form_no_double_count() { // The `${expr}` long form of a Kotlin string template also @@ -4522,6 +4693,7 @@ end", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_plain_string_still_operand() { // The fix for #191 only skips wrapping templates that contain @@ -4544,6 +4716,7 @@ end", // Kotlin half of #1380 — see // `java_self_and_super_references_are_operands` for the structural // argument. + #[cfg(feature = "kotlin")] #[test] fn kotlin_self_and_super_references_are_operands() { let source = "class T {\n fun f() = this.x + super.y\n}"; @@ -4566,6 +4739,7 @@ end", // and `Inner` stay separate operands: the `this_expression` wrapper // whose span would have swallowed them is deliberately unclassified // (section 5). + #[cfg(feature = "kotlin")] #[test] fn kotlin_labelled_self_and_super_references_are_operands() { let source = "class Outer {\n inner class Inner : A() {\n \ @@ -4599,6 +4773,7 @@ end", // delegation zero. This is the only spelling that can tell the two // choices apart — every other `this` carries both nodes — so it is // the independent path grammar-dispatch section 11 asks for. + #[cfg(feature = "kotlin")] #[test] fn kotlin_constructor_delegation_self_reference_is_an_operand() { let source = "class C(val n: Int) {\n constructor() : this(0)\n}"; @@ -4620,6 +4795,7 @@ end", /// call argument, a navigation receiver, a type-argument-qualified /// `super

`, both label-qualified forms, and the /// `constructor_delegation_call` that carries no wrapper. + #[cfg(feature = "kotlin")] const KOTLIN_SELF_POSITIONS: &str = "class P { fun h() = 1 } @@ -4640,6 +4816,7 @@ end", } "; + #[cfg(feature = "kotlin")] #[test] fn kotlin_self_and_super_leaves_are_unaliased_and_unconditional() { assert_self_reference_leaves::( @@ -4654,6 +4831,7 @@ end", ); } + #[cfg(feature = "python")] #[test] fn python_fstring_no_double_count() { // Regression: issue #191. A Python f-string (`f"Hi {name}!"`) @@ -4680,6 +4858,7 @@ end", ); } + #[cfg(feature = "python")] #[test] fn python_plain_string_still_operand() { // The fix for #191 only skips wrapping `String` nodes that @@ -4697,6 +4876,7 @@ end", }); } + #[cfg(feature = "python")] #[test] fn python_concatenated_docstring_suppressed() { // Regression for #695. An implicit-concatenation docstring @@ -4721,6 +4901,7 @@ end", ); } + #[cfg(feature = "python")] #[test] fn python_concatenated_non_docstring_still_counts() { // The #695 fix must only suppress concatenated literals in the @@ -4743,6 +4924,7 @@ end", ); } + #[cfg(feature = "python")] #[test] fn python_empty_file_halstead() { check_metrics::("", "empty.py", |metric| { @@ -4764,6 +4946,7 @@ end", /// operator arm listed both the await-expression node (Await=237) and the /// nested `await` keyword token (Await2=95). Only the node should count, /// mirroring how `yield` counts only the Yield node. + #[cfg(feature = "python")] #[test] fn python_await_counted_once_per_use() { check_metrics::( @@ -4783,6 +4966,7 @@ end", /// Regression #413, sub-fix (3): `lambda` was dropped entirely. Only the /// `lambda` keyword token (Lambda3=73) is classified, not the wrapping /// Lambda/Lambda2 expression nodes, to avoid an await-style double count. + #[cfg(feature = "python")] #[test] fn python_lambda_counted_once() { check_metrics::("g = lambda x: x + 1\n", "foo.py", |metric| { @@ -4796,6 +4980,7 @@ end", /// Regression #413, sub-fix (2): `match` / `case` keyword tokens /// (Match=26, Case=27) were dropped. Each should now count as an operator, /// matching the cyclomatic metric which already counts every `case`. + #[cfg(feature = "python")] #[test] fn python_match_case_counted() { check_metrics::( @@ -4813,6 +4998,7 @@ end", /// Regression #413, sub-fix (2): `nonlocal` (Nonlocal=41) was dropped while /// `global` was already classified. Both should count, for parity. + #[cfg(feature = "python")] #[test] fn python_nonlocal_and_global_counted() { check_metrics::( @@ -4831,6 +5017,7 @@ end", /// (Isnot=194) are single compound operators. The parent-guard suppresses /// the inner Not/In/Is leaves only under those compounds, so standalone /// `not x`, `a in b`, `a is b`, and `for x in y` still count their leaves. + #[cfg(feature = "python")] #[test] fn python_not_in_is_not_counted_as_single_operator() { check_metrics::( @@ -4853,6 +5040,7 @@ end", ); } + #[cfg(feature = "bash")] #[test] fn bash_operators_and_operands() { check_metrics::( @@ -4886,6 +5074,7 @@ f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_interpolated_string_no_double_count() { // Regression: issue #180. A double-quoted Bash string containing @@ -4915,6 +5104,7 @@ f() { }); } + #[cfg(feature = "elixir")] #[test] fn elixir_interpolated_string_no_double_count() { // Regression: issue #180. Without the fix, an interpolated @@ -4950,6 +5140,7 @@ f() { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_plain_string_still_operand() { // The fix for #180 only skips wrapping literals that contain @@ -4962,6 +5153,7 @@ f() { }); } + #[cfg(feature = "elixir")] #[test] fn elixir_boolean_and_nil_literals_count_once() { // Regression: issue #1253. `boolean: choice("true", "false")` @@ -4998,6 +5190,7 @@ f() { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_reserved_word_after_a_dot_stays_an_operand() { // Companion to the test above (#1253). Elixir drops `True` / @@ -5021,6 +5214,7 @@ f() { }); } + #[cfg(feature = "elixir")] #[test] fn elixir_interpolated_sigil_no_double_count() { // Sigils mirror strings under #180. For `~r/foo#{name}/`, the @@ -5039,6 +5233,7 @@ f() { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_interpolated_charlist_no_double_count() { // Charlists mirror strings and sigils under #180. The @@ -5062,6 +5257,7 @@ f() { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_sigil_delimiters_are_not_operators() { // Regression: issue #1256. Sigil delimiter tokens share their @@ -5088,6 +5284,7 @@ f() { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_sigil_delimiter_choice_is_invariant() { // Companion to the test above (#1256): two sigils differing @@ -5119,6 +5316,7 @@ f() { } } + #[cfg(feature = "elixir")] #[test] fn elixir_standalone_operators_survive_the_sigil_guard() { // Control for #1256: the guard is parent-scoped, so the same @@ -5143,6 +5341,7 @@ f() { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_interpolated_sigil_keeps_inner_nodes_counting() { // Interpolation inside a sigil after #1256: the `{` delimiter @@ -5181,6 +5380,7 @@ f() { }); } + #[cfg(feature = "bash")] #[test] fn bash_all_expansion_kinds_skip_wrapper() { // Exercises every node kind tested by @@ -5236,6 +5436,7 @@ f() { /// when its parent is a `simple_expansion`, so `$x` contributes exactly /// one operand while the assignment LHS `variable_name` (`x` in `x=…`, /// parent is `variable_assignment`) still counts. + #[cfg(feature = "bash")] #[test] fn bash_bare_variable_no_double_count() { let source = "x=1\necho $x\necho $?\n"; @@ -5301,6 +5502,7 @@ f() { /// assertion here that fails when the wrapper arm comes back; measured /// by neutralising it under the pre-fix arm, which leaves both tables /// green. + #[cfg(feature = "bash")] #[track_caller] fn assert_bash_wrapper_sheds_one_operand( source: &str, @@ -5363,6 +5565,7 @@ f() { /// `translated_string` wrapper to #1358 as well. /// `assert_bash_wrapper_sheds_one_operand` re-derives both from the /// current parse rather than trusting them. + #[cfg(feature = "bash")] #[test] fn bash_command_name_wrapper_no_double_count() { // (source, [n1, N1, n2, N2], (n2_before, N2_before)) @@ -5427,6 +5630,7 @@ f() { /// two vocabulary entries rather than repeating one — which is why the /// `n2_before` column is carried too, and checked against the wrapper /// spellings the row actually parses to. + #[cfg(feature = "bash")] #[test] fn bash_translated_string_wrapper_no_double_count() { // (source, [n1, N1, n2, N2], (n2_before, N2_before)) @@ -5474,6 +5678,7 @@ f() { /// the wrapper makes the wrapper-bearing positions agree with argument /// position rather than newly disagree. If a grammar bump starts /// emitting the wrapper here, the deletion has to be re-derived. + #[cfg(feature = "bash")] #[test] fn bash_translated_string_scores_alike_in_both_positions() { // `${#}` carries the residue: it contributes no operand of its own, @@ -5526,6 +5731,7 @@ f() { /// The parity is the load-bearing half of that argument and nothing /// else asserts it, so if a future arm starts classifying these the /// two positions have to move together. + #[cfg(feature = "bash")] #[test] fn bash_operandless_expansion_scores_alike_in_both_positions() { // `${!}` carries a `!`, which the operator arm counts; `${#}`'s `#` @@ -5555,6 +5761,7 @@ f() { /// arm fails no test in the suite, because the token is unreachable. /// Unreachability is the only coverage such an arm can have, which is /// why this test asserts it directly instead of asserting a count. + #[cfg(feature = "bash")] #[test] fn bash_hidden_concat_token_is_unreachable() { let source = "a=foo$x\nb=pre\"$y\"post\ncmd bar$z\n"; @@ -5575,6 +5782,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_operators_and_operands() { check_metrics::( @@ -5606,6 +5814,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_bitwise_ternary_string_ops() { // Exercises operator families not covered by tcl_operators_and_operands: @@ -5646,6 +5855,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_array_reference_bills_the_reference_and_the_index() { // `$arr($i)` is the reference plus the index Tcl substitutes @@ -5663,6 +5873,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_bare_variable_operand() { // Bare `$varname` produces a VariableSubstitution node (already an operand). @@ -5690,6 +5901,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_inert_quoted_word_counts_as_operand() { // Regression for #277. A `"..."` literal with no `$var` / `[cmd]` @@ -5721,6 +5933,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_interpolated_quoted_word_no_double_count() { // Regression for #277. Before the fix, `"$x is $y"` produced an @@ -5747,6 +5960,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_command_substitution_quoted_word_no_double_count() { // Regression for #277. A `"...[cmd]..."` literal exposes the @@ -5782,6 +5996,7 @@ f() { /// fix from a regression in either direction: a re-blanketed /// exclusion drops `s`/`t` (total 2), while losing the guard /// double-counts the `$s` leaf as a second `s` (total 5). + #[cfg(feature = "tcl")] #[test] fn tcl_set_target_is_operand() { let source = "set s 1\nset t $s\n"; @@ -5825,6 +6040,7 @@ f() { /// var-sub leaf). The `Tcl::Id` arm in `get_op_type` is therefore /// defensive; if a grammar bump starts emitting 84 this fails and /// the arm's classification must be re-derived instead of trusted. + #[cfg(feature = "tcl")] #[test] fn tcl_named_id_variant_is_unreachable() { let source = "proc f {x} {\n set s $x\n foreach v {1 2} { puts \"$v\" }\n}\n"; @@ -5843,6 +6059,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_braced_word_delimiter_is_not_an_operator() { // Regression: issue #1314, the Tcl sibling of Elixir #1256 and @@ -5869,6 +6086,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_script_bodies_keep_their_braces() { // Control for #1314, and the reason a kind-scoped guard is safe @@ -5906,6 +6124,7 @@ f() { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_braced_word_guard_is_parent_scoped_not_ancestor_scoped() { // The input that separates the parent-scoped guard from the @@ -5945,6 +6164,7 @@ f() { }); } + #[cfg(feature = "irules")] #[test] fn irules_braced_word_guard_is_parent_scoped_not_ancestor_scoped() { // The iRules twin of the test above — the two getters are @@ -6006,6 +6226,7 @@ f() { interpolation: [u16; 2], } + #[cfg(feature = "tcl")] const TCL_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds { wrapper: Tcl::BracedWordSimple as u16, script_body: Tcl::BracedWord as u16, @@ -6031,6 +6252,7 @@ f() { ], }; + #[cfg(feature = "irules")] const IRULES_BRACED_WORD_KINDS: BracedWordKinds = BracedWordKinds { wrapper: Irules::BracedWordSimple as u16, script_body: Irules::BracedWord as u16, @@ -6067,6 +6289,7 @@ f() { /// the wrapper outside `children` ∪ `delimiters` fails on the spot, /// which is what makes keying the arm on the parent alone — rather /// than on an enumerated child list — safe to rely on. + #[cfg(any(feature = "irules", feature = "tcl"))] fn braced_word_shed( source: &str, kinds: &BracedWordKinds, @@ -6130,6 +6353,7 @@ f() { /// Every row is measured in *both* dialects, so a fix applied to /// one getter and not its clone fails here. `braced_word_shed`'s /// drift assertion likewise runs against both grammars. + #[cfg(any(feature = "irules", feature = "tcl"))] fn check_braced_word_cases( cases: &[BracedWordCase], file: &str, @@ -6175,6 +6399,7 @@ f() { /// `braced_word_simple` admits, the childless spelling, the /// repeated-value row that separates `n2` from `N2` (#1294), and /// the braced/quoted parity pair #1317 asks for. + #[cfg(any(feature = "irules", feature = "tcl"))] const BRACED_WORD_CASES: [BracedWordCase; 11] = [ // simple_word, the reported fixture. Two words inside one // value scored two operands beside the value itself. @@ -6299,6 +6524,7 @@ f() { /// block's `{}`. The operand columns are untouched by it — #1318 /// revises the brace and nothing else — so every `before` here /// still describes #1354 alone. + #[cfg(any(feature = "irules", feature = "tcl"))] const SCRIPT_BODY_CASES: [BracedWordCase; 9] = [ BracedWordCase { source: "proc p {} { set b 1 }\n", @@ -6386,6 +6612,7 @@ f() { /// admits inside a braced word, and no other — the other half of /// the drift marker in `braced_word_shed`, which can only police /// kinds a fixture actually produces. + #[cfg(any(feature = "irules", feature = "tcl"))] fn assert_braced_word_children_witnessed( witnessed: &HashSet, kinds: &BracedWordKinds, @@ -6425,6 +6652,7 @@ f() { /// arm: it fails if the grammar ever puts a seventh kind directly /// inside a braced word, and the union assertion below fails if a /// bump stops emitting one of the six. + #[cfg(feature = "tcl")] #[test] fn tcl_braced_word_bills_its_content_once_1354() { let mut witnessed = check_braced_word_cases::( @@ -6444,6 +6672,7 @@ f() { /// only `big-code-analysis-ast/src/getter/tcl.rs` fails every row /// here — the two getters are deliberate clones and #1354 names /// both. + #[cfg(feature = "irules")] #[test] fn irules_braced_word_bills_its_content_once_1354() { let mut witnessed = check_braced_word_cases::( @@ -6507,6 +6736,7 @@ f() { /// Runs one #1318 table against one dialect. Both dialects run /// every shared row, so a fix that reached one getter and not its /// clone fails here. + #[cfg(any(feature = "irules", feature = "tcl"))] fn check_braced_word_value_cases( cases: &[BracedWordValueCase], file: &str, @@ -6536,6 +6766,7 @@ f() { /// *contents* — the tidier-looking rule, which collapses an /// `oo::class create C {…}` body into a single operand — fails /// here rather than passing as an improvement. + #[cfg(any(feature = "irules", feature = "tcl"))] const BRACED_WORD_VALUE_CASES: [BracedWordValueCase; 16] = [ // The rule reaches the opener and *only* the opener. A `;` // separating two commands is a direct child of the @@ -6703,6 +6934,7 @@ f() { /// body, in both the one-arm-per-line and the one-line layouts. /// Their iRules counterparts are dedicated nodes and are covered by /// the sibling test. + #[cfg(feature = "tcl")] #[test] fn tcl_braced_word_role_follows_the_enclosing_command_1318() { check_braced_word_value_cases::(&BRACED_WORD_VALUE_CASES, "foo.tcl"); @@ -6779,6 +7011,7 @@ f() { /// unchanged — the two getters are deliberate clones and #1318 /// names both — and the dialect rows cover the handler bodies, /// which have no Tcl spelling. + #[cfg(feature = "irules")] #[test] fn irules_braced_word_role_follows_the_enclosing_command_1318() { check_braced_word_value_cases::(&BRACED_WORD_VALUE_CASES, "foo.irule"); @@ -6824,6 +7057,7 @@ f() { check_braced_word_value_cases::(&irules_only, "foo.irule"); } + #[cfg(feature = "php")] #[test] fn php_operators_and_operands() { check_metrics::( @@ -6855,6 +7089,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_simple_function() { check_metrics::( @@ -6880,6 +7115,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_variable_reference_counts_once() { // Regression: issue #1259. `$x` parses as a `variable_name` @@ -6907,6 +7143,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_dynamic_variable_name_counts_once_at_any_depth() { // Regression: issue #1259. Variable-variable syntax nests the @@ -6935,6 +7172,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_dynamic_variable_name_guard_is_parent_scoped() { // Companion to the two tests above (#1259): the guards fire on @@ -6963,6 +7201,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_type_wrappers_count_the_type_once() { // Regression: issue #1293. A parameter type nests wrapper nodes @@ -6993,6 +7232,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_qualified_name_counts_its_components_once() { // Regression: issue #1293. `Foo\Bar\Baz` parses as @@ -7022,6 +7262,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_nested_type_wrappers_count_once_at_any_depth() { // Companion to the two tests above (#1293): the type and @@ -7051,6 +7292,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_childless_primitive_types_still_count() { // Guards the direction of the #1293 fix for `primitive_type`, @@ -7080,6 +7322,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_primitive_type_keyword_guard_is_parent_scoped() { // Companion to the test above (#1293): the keyword suppression @@ -7104,6 +7347,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_encapsed_string_interpolation_no_double_count() { // Regression: issue #184. A PHP `"Hello $name!"` used to be @@ -7138,6 +7382,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_encapsed_string_no_interpolation_still_operand() { // The fix for #184 only drops `EncapsedString`/`Heredoc` from @@ -7153,6 +7398,7 @@ f() { }); } + #[cfg(feature = "php")] #[test] fn php_heredoc_interpolation_no_double_count() { // Regression: issue #184. A PHP heredoc whose body @@ -7181,6 +7427,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_nowdoc_unaffected() { // `Nowdoc` (single-quoted heredoc) never interpolates and is @@ -7204,6 +7451,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_encapsed_string_bare_member_access_no_double_count() { // Regression: issue #184 follow-up. The PHP grammar allows @@ -7240,6 +7488,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_encapsed_string_bare_subscript_no_double_count() { // Regression: issue #184 follow-up. Bare `$arr[0]` inside @@ -7266,6 +7515,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_shell_command_expression_inert_is_operand() { // Regression: issue #288. Backtick command literals (PHP's @@ -7289,6 +7539,7 @@ f() { }); } + #[cfg(feature = "php")] #[test] fn php_shell_command_expression_interpolation_no_double_count() { // Regression: issue #288. PHP backtick literals DO support @@ -7319,6 +7570,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_interpolation_opener_is_not_an_operator() { // Regression: issue #1314. `Php::LBRACE` is *both* the @@ -7342,6 +7594,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_interpolation_opener_guard_covers_every_wrapper() { // The opener is a direct child of four distinct parents, and @@ -7378,6 +7631,7 @@ f() { } } + #[cfg(feature = "php")] #[test] fn php_every_interpolation_spelling_scores_alike() { // The policy stated as a test (#1314). PHP writes one @@ -7408,6 +7662,7 @@ f() { } } + #[cfg(feature = "php")] #[test] fn php_compound_statement_brace_still_counts() { // Control for #1314: the guard is scoped to the four @@ -7430,6 +7685,7 @@ f() { ); } + #[cfg(feature = "php")] #[test] fn php_interpolation_guard_is_parent_scoped_not_ancestor_scoped() { // The input that separates the parent-scoped guard from the @@ -7456,6 +7712,7 @@ f() { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_operators_and_operands() { // Exercises every Halstead family classified in Elixir's @@ -7503,6 +7760,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_operators_and_operands() { // A small Ruby method exercising operators (def/if/end keyword @@ -7530,6 +7788,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_halstead_plain_string_operand() { // A bare string literal contributes exactly one operand. The @@ -7546,6 +7805,7 @@ f() { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_halstead_interpolated_string_no_double_count() { // Regression mirror for #180 (Bash) / #183 (C#): when a Ruby @@ -7570,6 +7830,7 @@ f() { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_halstead_symbol_literal_operand() { // `:foo` is a `SimpleSymbol` leaf — counts as a single @@ -7582,6 +7843,7 @@ f() { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_halstead_regex_operand() { // `/foo/` parses as a `Regex` node — one operand. Its two @@ -7600,6 +7862,7 @@ f() { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_regex_delimiters_are_not_operators() { // Regression: issue #1312, the Ruby sibling of Elixir #1256. @@ -7618,6 +7881,7 @@ f() { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_regex_delimiter_choice_is_invariant() { // Companion to the test above (#1312): `%r`-form regexes are @@ -7643,6 +7907,7 @@ f() { } } + #[cfg(feature = "ruby")] #[test] fn ruby_division_survives_the_regex_guard() { // Control for #1312: the guard is scoped to a `Regex` parent, @@ -7660,6 +7925,7 @@ f() { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_regex_guard_is_parent_scoped_not_ancestor_scoped() { // The one input that separates the correct parent-scoped guard @@ -7688,6 +7954,7 @@ f() { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_regex_start_alias_never_reaches_kind_id() { // Drift marker for the `R::SLASH2` half of #1312's guard. @@ -7717,6 +7984,7 @@ f() { } } + #[cfg(feature = "ruby")] #[test] fn ruby_subshell_delimiters_are_not_operators() { // Regression: issue #1360, the second delimiter family of the @@ -7737,6 +8005,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_subshell_delimiter_choice_is_invariant() { // Companion to the test above (#1360): `%x`-form subshells are @@ -7772,6 +8041,7 @@ f() { } } + #[cfg(feature = "ruby")] #[test] fn ruby_backtick_method_name_survives_the_subshell_guard() { // Control for #1360, and the reason the kind is gated rather @@ -7793,6 +8063,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_subshell_guard_is_parent_scoped_not_ancestor_scoped() { // The one input that separates the correct parent-scoped guard @@ -7824,6 +8095,7 @@ f() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_subshell_start_alias_never_reaches_kind_id() { // Drift marker for #1360's guard, the `BQUOTE2` sibling of @@ -7861,6 +8133,7 @@ f() { } } + #[cfg(feature = "ruby")] #[test] fn ruby_interpolation_opener_is_not_an_operator() { // Behaviour change, not a fabrication fix: #1314 drops @@ -7890,6 +8163,7 @@ f() { } } + #[cfg(feature = "ruby")] #[test] fn ruby_interpolation_opener_drop_covers_every_literal() { // `HASHLBRACE` is one arm, but it fires under every Ruby @@ -7910,6 +8184,7 @@ f() { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_element_containers_count_elements_not_the_composite_1353() { // #1353. `chained_string`, `string_array` and `symbol_array` @@ -7977,6 +8252,7 @@ f() { } } + #[cfg(feature = "ruby")] #[test] fn ruby_empty_word_and_symbol_arrays_still_bill_one_operand_1353() { // The childless spelling, and the whole reason #1353 gates the @@ -7999,6 +8275,7 @@ f() { } } + #[cfg(feature = "ruby")] #[test] fn ruby_interpolated_array_elements_are_not_double_counted_1353() { // `bare_string` and `bare_symbol` are two aliases of a single @@ -8031,6 +8308,7 @@ f() { } } + #[cfg(feature = "ruby")] #[test] fn ruby_suffixed_numeric_literals_bill_one_operand_1359() { // #1359, the wrapper/leaf double count one arm below #1353's. @@ -8165,6 +8443,7 @@ f() { } } + #[cfg(feature = "ruby")] #[test] fn ruby_numeric_suffixes_stay_distinct_operands_1359() { // The reason #1359 keeps the *wrapper* and gates the leaf @@ -8205,6 +8484,7 @@ f() { /// `n1`/`n2`. A classification change that moved one store without the /// other (e.g. a kind landing in both the operator and operand arms) /// would break this even though the snapshot stayed green. + #[cfg(feature = "irules")] #[test] fn irules_operators_and_operands() { let source = "proc f { a b } { @@ -8253,6 +8533,7 @@ f() { /// the operand count and #1354 dropped the proc-body `braced_word` /// from both. Mirrors `tcl_inert_quoted_word_counts_as_operand` /// (#277). + #[cfg(feature = "irules")] #[test] fn irules_inert_quoted_word_counts_as_operand() { let source = "proc f {} {\n set s \"hello world\"\n}\n"; @@ -8290,6 +8571,7 @@ f() { /// `braced_word`). If the guard regressed (wrapper classified /// `Operand`), the wrapper string would add a 7th operand. This is the /// branch that had no test before. + #[cfg(feature = "irules")] #[test] fn irules_interpolated_quoted_word_no_double_count() { let source = "proc f {x y} {\n set s \"$x is $y\"\n}\n"; @@ -8332,6 +8614,7 @@ f() { /// `contains`, `matches`, `eq`, `ne`), and the keyword logical operator /// (`and`). Pins every operator-family arm in `get_op_type` plus the /// lesson-4 dedupe invariant. + #[cfg(feature = "irules")] #[test] fn irules_bitwise_ternary_string_ops() { let source = "proc f { a b } { @@ -8386,6 +8669,7 @@ f() { /// (it text-collides with the proc arg `x`, so `u_operands` would stay /// 4 but `total_operands()` would rise to 5 — hence the total, not just /// the unique count, is asserted). + #[cfg(feature = "irules")] #[test] fn irules_array_reference_bills_the_reference_and_the_index() { // The iRules twin of @@ -8401,6 +8685,7 @@ f() { ); } + #[cfg(feature = "irules")] #[test] fn irules_bare_variable_operand() { let source = "proc f {x} {\n return $x\n}\n"; @@ -8424,6 +8709,7 @@ f() { ); } + #[cfg(feature = "irules")] #[test] fn irules_braced_word_delimiter_is_not_an_operator() { // Regression: issue #1314. The iRules twin of @@ -8462,6 +8748,7 @@ f() { /// the same token across `Display` and JSON. The space-separated forms /// (`estimated program length` / `purity ratio`) were the only outliers, /// mirroring the `dump` fix in #562. + #[cfg(feature = "cpp")] #[test] fn display_halstead_labels_use_underscore_keys() { check_metrics::("int a = 42;", "foo.cpp", |metric| { @@ -8491,6 +8778,7 @@ f() { /// n1 for a file whose only `@` was in NSString literals and billed /// the same byte in both streams. Boxing (`@42`) keeps its `@`: there /// the token is a child of the `at_expression`, not of the literal. + #[cfg(feature = "objc")] #[test] fn objc_nsstring_literal_is_one_operand() { // expected: [n1, N1, n2, N2]. Before the guard the first two rows @@ -8519,6 +8807,7 @@ f() { /// assignment. Pins every field and enforces the lesson-4 invariants /// `unique_operators == n1` / `unique_operands == n2` via the /// independent `--ops` store. + #[cfg(feature = "objc")] #[test] fn objc_operators_and_operands() { let source = "@implementation Foo @@ -8582,6 +8871,7 @@ f() { /// Both #1316 fixtures are plain C, which every C-family grammar /// parses to the same shape, so one source proves the same thing /// about each of the four clones. + #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] const C_FAMILY_CHAR_REPEATS: &str = "char a = 'x';\nchar b = 'x';\nchar c = 'y';\nchar d = '\\n';\nint e = 'ab';\n"; @@ -8589,6 +8879,7 @@ f() { /// opens on a distinct delimiter kind (`'`, `L'`, `u'`, `U'`, `u8'`), /// and operands key on source text, so the five are five vocabulary /// entries rather than one. + #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] const C_FAMILY_CHAR_PREFIXES: &str = "char a = 'x';\nchar b = L'x';\nchar c = u'x';\nchar d = U'x';\nchar e = u8'x';\n"; @@ -8604,6 +8895,7 @@ f() { /// rather than a second classification — `ops_inner` reads the keys /// of the same `HalsteadMaps` — which is worth knowing before /// reading it as independent corroboration of the count. + #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] #[track_caller] fn assert_char_literal_operands(file: &str, label: &str) { // `char` x4 and `int` are text-keyed primitive operators, so @@ -8651,6 +8943,7 @@ f() { /// /// Mozcpp owns no file extension, so no integration snapshot ever /// reaches its clone; its row is the whole coverage that arm has. + #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] #[test] fn c_family_char_literals_are_operands() { assert_char_literal_operands::("chars.c", "c"); @@ -8664,6 +8957,7 @@ f() { /// operator. The wrapper is in no operand arm, so the boxed form /// bills exactly the literal it wraps and stays distinct from a bare /// one (#1316). + #[cfg(feature = "objc")] #[test] fn objc_boxed_char_literal_is_one_operand() { let source = "char a = 'x';\nid b = @'y';\n"; @@ -8698,8 +8992,10 @@ f() { /// Both loops are non-vacuous by assertion, since a fixture that /// stopped containing a character literal would otherwise make this /// test pass having checked nothing. + #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] #[test] fn c_family_char_literal_internals_stay_unclassified() { + #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))] fn check(char_literal: u16, label: &str) { let mut literals = 0_usize; let mut children = 0_usize; @@ -8768,6 +9064,7 @@ f() { /// /// Plain C++ that the mozcpp fork parses identically, so one source /// proves the same thing about both clones. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const CPP_THIS_RECEIVER_PARITY: &str = "struct S { int x; int m1() { return this->x; } @@ -8795,6 +9092,7 @@ f() { /// `for_each_node_with_chain` rejects outright. That spelling is /// already an operand through `TypeIdentifier`, so the construct is /// unaffected by this arm either way. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const CPP_THIS_POSITIONS: &str = "struct S { int x; void g(S*); @@ -8816,6 +9114,7 @@ f() { /// under, which the counts alone cannot see: billing the enclosing /// `field_expression` instead of the `this` leaf would hold `n2` at /// 6 while the vocabulary silently became `this->x`. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] #[track_caller] fn assert_this_receiver_parity(file: &str, label: &str) { // Operators, keyed by kind_id except the text-keyed primitives: @@ -8853,6 +9152,7 @@ f() { /// extension, so no integration snapshot reaches its clone; this row /// and `cpp_and_mozcpp_agree_on_this` in `tests/parity/` are the /// whole coverage that arm has. + #[cfg(all(feature = "cpp", feature = "mozcpp"))] #[test] fn cpp_this_is_an_operand() { assert_this_receiver_parity::("this.cpp", "cpp"); @@ -8875,8 +9175,10 @@ f() { /// samples one position per container kind and is deliberately not /// exhaustive (see `CPP_THIS_POSITIONS`), so a name promising "every /// position" would claim more than it checks. + #[cfg(all(feature = "cpp", feature = "mozcpp"))] #[test] fn cpp_this_is_an_operand_regardless_of_position() { + #[cfg(any(feature = "cpp", feature = "mozcpp"))] fn check(label: &str) { let mut seen = 0_usize; for_each_node_with_chain::(CPP_THIS_POSITIONS.as_bytes(), |node, chain| { @@ -8933,8 +9235,10 @@ f() { /// time, and no `field_expression` / `pointer_expression` / /// `lambda_capture_specifier` / `argument_list` wrapper bills the /// same source text from above. + #[cfg(all(feature = "cpp", feature = "mozcpp"))] #[test] fn cpp_this_is_a_childless_unaliased_leaf() { + #[cfg(any(feature = "cpp", feature = "mozcpp"))] fn check(this: u16, label: &str) { let mut seen = 0_usize; for source in [CPP_THIS_RECEIVER_PARITY, CPP_THIS_POSITIONS] { diff --git a/src/metrics/loc.rs b/src/metrics/loc.rs index 89dec4410..ef35a832d 100644 --- a/src/metrics/loc.rs +++ b/src/metrics/loc.rs @@ -1100,6 +1100,7 @@ mod tests { /// values would happen to match what an error tree produces — a parse /// regression in tree-sitter-perl could otherwise leave such tests /// silently green. + #[cfg(feature = "perl")] #[cfg(test)] fn assert_perl_parses_cleanly(source: &str) { use crate::traits::ParserTrait; @@ -1116,6 +1117,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_sloc() { check_metrics::( @@ -1158,6 +1160,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_blank() { check_metrics::( @@ -1201,6 +1204,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_blank() { check_metrics::( @@ -1276,6 +1280,7 @@ mod tests { }); } + #[cfg(feature = "c")] #[test] fn c_blank() { check_metrics::( @@ -1320,6 +1325,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_no_zero_blank() { // Checks that the blank metric is not equal to 0 when there are some @@ -1369,6 +1375,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_no_blank() { // Checks that the blank metric is equal to 0 when there are no blank @@ -1417,6 +1424,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_no_zero_blank_more_comments() { // Checks that the blank metric is not equal to 0 when there are more @@ -1466,6 +1474,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_no_zero_blank() { // Checks that the blank metric is not equal to 0 when there are some @@ -1516,6 +1525,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_no_zero_blank() { // Checks that the blank metric is not equal to 0 when there are some @@ -1566,6 +1576,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_no_zero_blank() { // Checks that the blank metric is not equal to 0 when there are some @@ -1616,6 +1627,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_code_line_start_block_blank() { // Checks that the blank metric is equal to 1 when there are @@ -1667,6 +1679,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_block_comment_blank() { // Checks that the blank metric is equal to 1 when there are @@ -1719,6 +1732,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_code_line_block_one_line_blank() { // Checks that the blank metric is equal to 1 when there are @@ -1768,6 +1782,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_code_line_end_block_blank() { // Checks that the blank metric is equal to 1 when there are @@ -1819,6 +1834,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_cloc() { check_metrics::( @@ -1861,6 +1877,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_cloc() { check_metrics::( @@ -1909,6 +1926,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_cloc() { check_metrics::( @@ -1954,6 +1972,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_lloc() { check_metrics::( @@ -1994,6 +2013,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_lloc() { check_metrics::( @@ -2077,6 +2097,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_lloc() { check_metrics::( @@ -2116,6 +2137,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_lloc() { check_metrics::( @@ -2158,6 +2180,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_return_lloc() { check_metrics::( @@ -2198,6 +2221,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_for_lloc() { check_metrics::( @@ -2240,6 +2264,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_while_lloc() { check_metrics::( @@ -2282,6 +2307,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_string_on_new_line() { // More lines of the same instruction were counted as blank lines @@ -2322,6 +2348,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_multiline_string_assignment_ploc() { // Regression test for issue #415: interior rows of a multi-line, @@ -2371,6 +2398,7 @@ ORDER BY name ); } + #[cfg(feature = "python")] #[test] fn python_multiline_string_argument_ploc() { // Regression test for issue #415: a multi-line string passed as a call @@ -2391,6 +2419,7 @@ line two ); } + #[cfg(feature = "python")] #[test] fn python_single_line_string_assignment_ploc() { // Single-line, non-docstring string: behaviour must be unchanged by @@ -2404,6 +2433,7 @@ line two }); } + #[cfg(feature = "python")] #[test] fn python_multiline_docstring_still_cloc() { // The fix for issue #415 must leave docstring classification unchanged: @@ -2424,6 +2454,7 @@ line two ); } + #[cfg(feature = "rust")] #[test] fn rust_no_field_expression_lloc() { check_metrics::( @@ -2466,6 +2497,7 @@ line two ); } + #[cfg(feature = "rust")] #[test] fn rust_no_parenthesized_expression_lloc() { check_metrics::("let a = (42 + 0);", "foo.rs", |metric| { @@ -2500,6 +2532,7 @@ line two }); } + #[cfg(feature = "rust")] #[test] fn rust_no_array_expression_lloc() { check_metrics::("let a = [0; 42];", "foo.rs", |metric| { @@ -2534,6 +2567,7 @@ line two }); } + #[cfg(feature = "rust")] #[test] fn rust_no_tuple_expression_lloc() { check_metrics::("let a = (0, 42);", "foo.rs", |metric| { @@ -2568,6 +2602,7 @@ line two }); } + #[cfg(feature = "rust")] #[test] fn rust_no_unit_expression_lloc() { check_metrics::("let a = ();", "foo.rs", |metric| { @@ -2602,6 +2637,7 @@ line two }); } + #[cfg(feature = "rust")] #[test] fn rust_call_function_lloc() { check_metrics::( @@ -2642,6 +2678,7 @@ line two ); } + #[cfg(feature = "rust")] #[test] fn rust_macro_invocation_lloc() { check_metrics::( @@ -2682,6 +2719,7 @@ line two ); } + #[cfg(feature = "rust")] #[test] fn rust_function_in_loop_lloc() { check_metrics::( @@ -2722,6 +2760,7 @@ line two ); } + #[cfg(feature = "rust")] #[test] fn rust_function_in_if_lloc() { check_metrics::( @@ -2761,6 +2800,7 @@ line two ); } + #[cfg(feature = "rust")] #[test] fn rust_function_in_return_lloc() { check_metrics::( @@ -2800,6 +2840,7 @@ line two ); } + #[cfg(feature = "rust")] #[test] fn rust_closure_expression_lloc() { check_metrics::( @@ -2840,6 +2881,7 @@ line two ); } + #[cfg(feature = "python")] #[test] fn python_general_loc() { check_metrics::( @@ -2883,6 +2925,7 @@ line two ); } + #[cfg(feature = "python")] #[test] fn python_real_loc() { check_metrics::( @@ -2936,6 +2979,7 @@ line two ); } + #[cfg(feature = "javascript")] #[test] fn javascript_real_loc() { check_metrics::( @@ -2978,6 +3022,7 @@ line two ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_real_loc() { check_metrics::( @@ -3020,6 +3065,7 @@ line two ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_blank_and_comment_loc() { check_metrics::( @@ -3062,6 +3108,7 @@ line two ); } + #[cfg(feature = "cpp")] #[test] fn cpp_namespace_loc() { check_metrics::( @@ -3100,6 +3147,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_comments() { check_metrics::( @@ -3143,6 +3191,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_blank() { check_metrics::( @@ -3184,6 +3233,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_sloc() { check_metrics::( @@ -3224,6 +3274,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_module_sloc() { check_metrics::( @@ -3264,6 +3315,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_single_ploc() { check_metrics::("int x = 1;", "foo.java", |metric| { @@ -3298,6 +3350,7 @@ line two }); } + #[cfg(feature = "java")] #[test] fn java_simple_ploc() { check_metrics::( @@ -3338,6 +3391,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_multi_ploc() { check_metrics::( @@ -3379,6 +3433,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_single_statement_lloc() { check_metrics::("int max = 10;", "foo.java", |metric| { @@ -3413,6 +3468,7 @@ line two }); } + #[cfg(feature = "java")] #[test] fn java_for_lloc() { check_metrics::( @@ -3453,6 +3509,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_foreach_lloc() { check_metrics::( @@ -3495,6 +3552,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_while_lloc() { check_metrics::( @@ -3538,6 +3596,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_do_while_lloc() { check_metrics::( @@ -3581,6 +3640,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_switch_lloc() { check_metrics::( @@ -3634,6 +3694,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_continue_lloc() { check_metrics::( @@ -3677,6 +3738,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_try_lloc() { check_metrics::( @@ -3721,6 +3783,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_class_loc() { check_metrics::( @@ -3768,6 +3831,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_expressions_lloc() { check_metrics::( @@ -3817,6 +3881,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_statement_inline_loc() { check_metrics::( @@ -3855,6 +3920,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_general_loc() { check_metrics::( @@ -3903,6 +3969,7 @@ line two ); } + #[cfg(feature = "java")] #[test] fn java_main_class_loc() { check_metrics::( @@ -3952,6 +4019,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_general_loc() { check_metrics::( @@ -4001,6 +4069,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_for_clause_does_not_double_count_lloc() { // Bare `for` body has only a return; the `for_statement` itself is the @@ -4023,6 +4092,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_blank() { check_metrics::( @@ -4068,6 +4138,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_cloc_line_comments() { check_metrics::( @@ -4114,6 +4185,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_cloc_block_comments() { check_metrics::( @@ -4159,6 +4231,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_lloc_if_for_switch() { check_metrics::( @@ -4208,6 +4281,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_lloc_go_defer() { check_metrics::( @@ -4252,6 +4326,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_lloc_var_const_declarations() { check_metrics::( @@ -4300,6 +4375,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_lloc_select() { check_metrics::( @@ -4348,6 +4424,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_sloc_multiline_function() { check_metrics::( @@ -4394,6 +4471,7 @@ line two ); } + #[cfg(feature = "go")] #[test] fn go_code_comment_same_line() { check_metrics::( @@ -4440,6 +4518,7 @@ line two ); } + #[cfg(feature = "perl")] #[test] fn perl_grammar_smoke() { // Pin the contract that tree-sitter-perl 1.1.2 cleanly parses every @@ -4493,6 +4572,7 @@ END ); } + #[cfg(feature = "perl")] #[test] fn perl_blank() { check_metrics::( @@ -4533,6 +4613,7 @@ my $b = 43; ); } + #[cfg(feature = "perl")] #[test] fn perl_no_zero_blank() { // Blank line interleaved with code that carries trailing comments — @@ -4555,6 +4636,7 @@ my $e = 5;", ); } + #[cfg(feature = "perl")] #[test] fn perl_blank_zero_sanity() { // Sanity check: blank must report 0, never go negative, when the @@ -4578,6 +4660,7 @@ my $b = 2;", /// both tallies. This pinned `ploc 3` until #1137: the `#` token /// inside the `comments` node reached the PLOC catch-all, which also /// reclassified row 0 from comment-only to code-and-comment. + #[cfg(feature = "perl")] #[test] fn perl_cloc_line_comments() { check_metrics::( @@ -4614,6 +4697,7 @@ my $b = 2;", ); } + #[cfg(feature = "perl")] #[test] fn perl_cloc_pod_block() { check_metrics::( @@ -4653,6 +4737,7 @@ my $y = 2;", ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_simple_statements() { check_metrics::( @@ -4689,6 +4774,7 @@ my $c = 3;", ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_compound_statements() { check_metrics::( @@ -4728,6 +4814,7 @@ while ($n > 0) { ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_postfix_form_counts_once() { // `do_thing() if cond;` is one logical line — wrapped in @@ -4744,6 +4831,7 @@ while ($n > 0) { ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_use_statement() { check_metrics::( @@ -4780,6 +4868,7 @@ my $x = 1;", ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_for_loop() { check_metrics::( @@ -4794,6 +4883,7 @@ my $x = 1;", ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_loop_control_statement() { check_metrics::( @@ -4808,6 +4898,7 @@ my $x = 1;", ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_no_double_count_inside_single_line_statement() { // SEMI inside a single_line_statement (postfix form) is a child of @@ -4823,6 +4914,7 @@ my $x = 1;", ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_function_definition_not_counted() { // `sub f { ... }` itself is a function space, not an LLOC; only its @@ -4838,6 +4930,7 @@ my $x = 1;", ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_anonymous_function() { // `my $f = sub { return 1; };` — the assignment is one LLOC at the @@ -4848,6 +4941,7 @@ my $x = 1;", }); } + #[cfg(feature = "perl")] #[test] fn perl_multiline_string_assignment_ploc() { // Regression test for issue #778: interior rows of a multi-line string @@ -5112,6 +5206,7 @@ line3\";", } } + #[cfg(feature = "perl")] #[test] fn perl_lloc_unless_until() { check_metrics::( @@ -5130,6 +5225,7 @@ until ($n == 0) { ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_heredoc_body_not_counted() { // Heredoc body content is data, not code: the body lines should not @@ -5159,6 +5255,7 @@ my $x = 1;", ); } + #[cfg(feature = "perl")] #[test] fn perl_lloc_package_and_require() { check_metrics::( @@ -5195,6 +5292,7 @@ my $x = 1;", ); } + #[cfg(feature = "lua")] #[test] fn lua_blank() { check_metrics::( @@ -5213,6 +5311,7 @@ local y = 2", ); } + #[cfg(feature = "lua")] #[test] fn lua_no_zero_blank() { // Blank line interleaved with code that carries trailing comments — @@ -5235,6 +5334,7 @@ local e = 5", ); } + #[cfg(feature = "lua")] #[test] fn lua_blank_zero_sanity() { // Sanity check: blank must report 0, never go negative, when the @@ -5253,6 +5353,7 @@ local y = 2", ); } + #[cfg(feature = "lua")] #[test] fn lua_cloc() { check_metrics::( @@ -5274,6 +5375,7 @@ local x = 1 ); } + #[cfg(feature = "lua")] #[test] fn lua_lloc() { check_metrics::( @@ -5296,6 +5398,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_no_string_lloc() { // Long strings spanning multiple lines must not inflate lloc. @@ -5320,6 +5423,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_no_functiondefinition_lloc() { // Anonymous function definition is an expression, not a statement. @@ -5340,6 +5444,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_no_elseif_lloc() { // elseif_statement must not add lloc; only if_statement does. @@ -5365,6 +5470,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_no_else_lloc() { // else_statement must not add lloc. @@ -5388,6 +5494,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_functiondeclaration_lloc() { // Named function declaration counts as one lloc. @@ -5407,6 +5514,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_local_function_lloc() { // local function declaration is also a function_declaration node → one lloc. @@ -5426,6 +5534,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_for_numeric_lloc() { check_metrics::( @@ -5444,6 +5553,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_for_generic_lloc() { check_metrics::( @@ -5462,6 +5572,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_repeat_lloc() { check_metrics::( @@ -5481,6 +5592,7 @@ until i >= 10", ); } + #[cfg(feature = "lua")] #[test] fn lua_local_decl_lloc() { check_metrics::( @@ -5498,6 +5610,7 @@ local y, z = 2, 3", ); } + #[cfg(feature = "lua")] #[test] fn lua_function_call_lloc() { // Standalone function calls have no expression_statement wrapper in Lua. @@ -5517,6 +5630,7 @@ local x = 1", ); } + #[cfg(feature = "lua")] #[test] fn lua_toplevel_assignment_lloc() { // Bare `x = 1` at chunk level: parent is Chunk, not VariableDeclaration, @@ -5536,6 +5650,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_basic_loc() { check_metrics::( @@ -5582,6 +5697,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_basic_loc() { check_metrics::( @@ -5626,6 +5742,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_comments() { check_metrics::( @@ -5647,6 +5764,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_blank() { check_metrics::( @@ -5666,6 +5784,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_sloc() { check_metrics::( @@ -5684,6 +5803,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_module_sloc() { check_metrics::( @@ -5702,6 +5822,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_single_ploc() { check_metrics::("int x = 1;", "foo.cs", |metric| { @@ -5714,6 +5835,7 @@ y, z = 2, 3", }); } + #[cfg(feature = "csharp")] #[test] fn csharp_simple_ploc() { check_metrics::( @@ -5732,6 +5854,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_multi_ploc() { check_metrics::( @@ -5751,6 +5874,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_single_statement_lloc() { check_metrics::("int max = 10;", "foo.cs", |metric| { @@ -5763,6 +5887,7 @@ y, z = 2, 3", }); } + #[cfg(feature = "csharp")] #[test] fn csharp_for_lloc() { check_metrics::( @@ -5781,6 +5906,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_foreach_lloc() { check_metrics::( @@ -5799,6 +5925,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_while_lloc() { check_metrics::( @@ -5818,6 +5945,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_do_while_lloc() { check_metrics::( @@ -5837,6 +5965,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_switch_lloc() { check_metrics::( @@ -5858,6 +5987,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_continue_lloc() { check_metrics::( @@ -5877,6 +6007,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_try_lloc() { check_metrics::( @@ -5899,6 +6030,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_class_loc() { check_metrics::( @@ -5920,6 +6052,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_expressions_lloc() { check_metrics::( @@ -5939,6 +6072,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_statement_inline_loc() { check_metrics::( @@ -5955,6 +6089,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_general_loc() { check_metrics::( @@ -5981,6 +6116,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "csharp")] #[test] fn csharp_using_lloc() { // EC11 — `using_directive` does not bump LLOC; `using_statement` @@ -6011,6 +6147,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_basic() { check_metrics::( @@ -6055,6 +6192,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_bare_expression() { check_metrics::( @@ -6099,6 +6237,7 @@ y, z = 2, 3", ); } + #[cfg(feature = "bash")] #[test] fn bash_loc() { check_metrics::( @@ -6125,6 +6264,7 @@ f", // CRLF regression tests: metrics must be identical regardless of line ending style. // These also serve as canaries for tree-sitter row-counting behaviour with \r bytes. + #[cfg(feature = "python")] #[test] fn python_cloc_crlf_matches_lf() { check_metrics::("# comment\nx = 1", "foo.py", |m| { @@ -6150,6 +6290,7 @@ f", }); } + #[cfg(feature = "python")] #[test] fn python_blank_crlf_matches_lf() { check_metrics::("# comment\n\nx = 1", "foo.py", |m| { @@ -6164,6 +6305,7 @@ f", }); } + #[cfg(feature = "rust")] #[test] fn rust_cloc_crlf_matches_lf() { check_metrics::( @@ -6194,6 +6336,7 @@ f", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_blank() { check_metrics::("set x 1\n\nset y 2", "foo.tcl", |metric| { @@ -6206,6 +6349,7 @@ f", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_no_zero_blank() { // Blank line interleaved with code that carries trailing comments — @@ -6228,6 +6372,7 @@ f", /// #1135: the `LF` token terminating the comment row landed in the /// `_` catch-all and inserted that row into PLOC, which also drove /// `cloc + ploc` past `sloc`. + #[cfg(feature = "tcl")] #[test] fn tcl_cloc() { check_metrics::("# This is a comment\nset x 1", "foo.tcl", |metric| { @@ -6240,6 +6385,7 @@ f", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_lloc() { check_metrics::( @@ -6262,6 +6408,7 @@ f", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_no_command_substitution_lloc() { // `string toupper` inside [...] is a sub-expression; only `puts` is top-level. @@ -6275,6 +6422,7 @@ f", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_procedure_lloc() { check_metrics::("proc foo {} {\n puts hello\n}", "foo.tcl", |metric| { @@ -6287,6 +6435,7 @@ f", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_if_lloc() { check_metrics::("if {1} {\n puts hello\n}", "foo.tcl", |metric| { @@ -6299,6 +6448,7 @@ f", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_elseif_lloc() { // if=1 lloc, elseif=1 lloc, else adds 0 lloc @@ -6322,6 +6472,7 @@ f", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_while_lloc() { check_metrics::( @@ -6338,6 +6489,7 @@ f", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_foreach_lloc() { check_metrics::( @@ -6354,6 +6506,7 @@ f", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_set_lloc() { check_metrics::("set x 42", "foo.tcl", |metric| { @@ -6366,6 +6519,7 @@ f", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_global_lloc() { check_metrics::("global x", "foo.tcl", |metric| { @@ -6378,6 +6532,7 @@ f", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_try_catch_lloc() { // try=1 lloc; catch command=1 lloc; commands inside bodies count separately @@ -6402,6 +6557,7 @@ try { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_namespace_lloc() { check_metrics::( @@ -6418,6 +6574,7 @@ try { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_regexp_lloc() { check_metrics::("regexp {^[0-9]+$} $x", "foo.tcl", |metric| { @@ -6430,6 +6587,7 @@ try { }); } + #[cfg(feature = "tcl")] #[test] fn tcl_expr_cmd_lloc() { check_metrics::("expr {1 + 2}", "foo.tcl", |metric| { @@ -6442,6 +6600,7 @@ try { }); } + #[cfg(feature = "tcl")] #[test] fn tcl_no_expr_cmd_substitution_lloc() { // `expr` inside [...] is a sub-expression, not a statement; only `set` counts. @@ -6455,6 +6614,7 @@ try { }); } + #[cfg(feature = "tcl")] #[test] fn tcl_nested_commands_lloc() { // Commands inside proc body are recursively parsed; verify each counts. @@ -6475,6 +6635,7 @@ try { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_command_lloc() { check_metrics::("puts hello", "foo.tcl", |metric| { @@ -6487,6 +6648,7 @@ try { }); } + #[cfg(feature = "tcl")] #[test] fn tcl_no_else_lloc() { // `else` block does not add a logical line. @@ -6504,6 +6666,7 @@ try { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_no_finally_lloc() { // `finally` block, like `else`, does not add a logical line. @@ -6521,6 +6684,7 @@ try { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_multiline_block() { check_metrics::( @@ -6542,6 +6706,7 @@ try { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_no_string_lloc() { // Multi-line double-quoted strings must not inflate lloc — only the @@ -6570,6 +6735,7 @@ try { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_multiline_quoted_word_credits_every_row_to_ploc() { // Regression test for #1260. A Tcl `quoted_word` carries no child @@ -6603,6 +6769,7 @@ try { }); } + #[cfg(feature = "tcl")] #[test] fn tcl_braced_word_body_blank_rows_stay_blank() { // The #1260 arm deliberately stops at `quoted_word`. Tcl spells a @@ -6628,6 +6795,7 @@ try { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_blank() { check_metrics::( @@ -6650,6 +6818,7 @@ try { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_cloc() { check_metrics::( @@ -6671,6 +6840,7 @@ try { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_cloc_html_comment() { // The Annex-B `` `html_comment` must count as CLOC, not @@ -6692,6 +6862,7 @@ function f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_blank() { check_metrics::( @@ -6712,6 +6883,7 @@ function f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_cloc() { check_metrics::( @@ -6732,6 +6904,7 @@ function f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_no_zero_blank() { // Blank line interleaved with code that carries trailing comments — @@ -6754,6 +6927,7 @@ function f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_arrow_function_loc() { check_metrics::( @@ -6773,6 +6947,7 @@ function f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_multiple_functions_loc() { check_metrics::( @@ -6794,6 +6969,7 @@ function f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_nested_function_loc() { check_metrics::( @@ -6815,6 +6991,7 @@ function f() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_if_lloc() { check_metrics::( @@ -6849,8 +7026,22 @@ function f() { // invariant — every brace block now contributes 0 lloc, as it always // had elsewhere. Pre-#777 the JS variants reported lloc 6 (three brace // blocks over-counted) against C's and Rust's 3. + #[cfg(all( + feature = "cpp", + feature = "javascript", + feature = "mozjs", + feature = "rust", + feature = "typescript", + ))] #[test] fn js_family_if_lloc_matches_c_and_rust() { + #[cfg(any( + feature = "cpp", + feature = "javascript", + feature = "mozjs", + feature = "rust", + feature = "typescript", + ))] const JS_SRC: &str = "function f(x) { if (x > 0) { return 1; @@ -6858,6 +7049,13 @@ function f() { return -1; } }"; + #[cfg(any( + feature = "cpp", + feature = "javascript", + feature = "mozjs", + feature = "rust", + feature = "typescript", + ))] const C_SRC: &str = "int f(int x) { if (x > 0) { return 1; @@ -6865,6 +7063,13 @@ function f() { return -1; } }"; + #[cfg(any( + feature = "cpp", + feature = "javascript", + feature = "mozjs", + feature = "rust", + feature = "typescript", + ))] const RUST_SRC: &str = "fn f(x: i32) -> i32 { if x > 0 { return 1; @@ -6875,6 +7080,13 @@ function f() { // The logical-statement count is grammar-independent: one `if` // plus two `return`s, regardless of brace style or language. + #[cfg(any( + feature = "cpp", + feature = "javascript", + feature = "mozjs", + feature = "rust", + feature = "typescript", + ))] const EXPECTED_LLOC: usize = 3; check_metrics::(C_SRC, "f.c", |m| { @@ -6897,6 +7109,7 @@ function f() { }); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_for_lloc() { check_metrics::( @@ -6919,6 +7132,7 @@ function f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_blank() { check_metrics::( @@ -6941,6 +7155,7 @@ function f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_cloc() { check_metrics::( @@ -6961,6 +7176,7 @@ function f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_no_zero_blank() { // Blank line interleaved with code that carries trailing comments — @@ -6983,6 +7199,7 @@ function f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_comment_before_code_line_reclassified() { // Regression for #547: a standalone `#` comment sitting on a line @@ -7038,6 +7255,7 @@ function f() { }); } + #[cfg(feature = "bash")] #[test] fn bash_if_lloc() { check_metrics::( @@ -7060,6 +7278,7 @@ function f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_for_lloc() { check_metrics::( @@ -7080,6 +7299,7 @@ function f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_while_lloc() { check_metrics::( @@ -7120,6 +7340,7 @@ function f() { /// Both halves need a row: a table of standalone assignments alone /// passes with the parent gate deleted, and a table of wrapped ones /// alone passes with the alias never added. + #[cfg(feature = "bash")] #[test] fn bash_assignment_counts_one_logical_line_per_statement() { for (source, lloc) in [ @@ -7138,6 +7359,7 @@ function f() { } } + #[cfg(feature = "bash")] #[test] fn bash_case_lloc() { check_metrics::( @@ -7160,6 +7382,7 @@ function f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_multiple_functions_loc() { check_metrics::( @@ -7181,6 +7404,7 @@ function f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_nested_function_loc() { check_metrics::( @@ -7203,6 +7427,7 @@ function f() { ); } + #[cfg(feature = "bash")] #[test] fn bash_heredoc_loc() { // expected: six physical rows (`f() {`, `cat <( @@ -7611,6 +7840,7 @@ EOF ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_cloc() { check_metrics::( @@ -7632,6 +7862,7 @@ EOF ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_no_zero_blank() { // Checks that the blank metric is not equal to 0 when there are some @@ -7687,6 +7918,7 @@ EOF ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_blank_zero_sanity() { // Sanity: when the source has no blank lines, blank() must be 0. @@ -7710,6 +7942,7 @@ EOF ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_if_lloc() { check_metrics::( @@ -7733,6 +7966,7 @@ EOF ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_for_lloc() { check_metrics::( @@ -7755,6 +7989,7 @@ EOF ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_when_lloc() { check_metrics::( @@ -7777,6 +8012,7 @@ EOF ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_lambda_lloc() { check_metrics::( @@ -7796,6 +8032,7 @@ EOF ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_class_loc() { check_metrics::( @@ -7816,6 +8053,7 @@ EOF ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_multiple_functions_loc() { check_metrics::( @@ -7837,6 +8075,7 @@ EOF ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_loc_while_lloc() { check_metrics::( @@ -7859,6 +8098,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_blank() { check_metrics::( @@ -7879,6 +8119,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_cloc() { check_metrics::( @@ -7900,6 +8141,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_no_zero_blank() { // Blank line interleaved with code that carries trailing comments — @@ -7922,6 +8164,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_if_lloc() { check_metrics::( @@ -7944,6 +8187,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_for_lloc() { check_metrics::( @@ -7966,6 +8210,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_while_lloc() { check_metrics::( @@ -7988,6 +8233,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_switch_lloc() { check_metrics::( @@ -8010,6 +8256,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_class_loc() { check_metrics::( @@ -8030,6 +8277,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_arrow_function_loc() { check_metrics::( @@ -8049,6 +8297,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_interface_loc() { check_metrics::( @@ -8071,6 +8320,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_multiple_functions_loc() { check_metrics::( @@ -8095,6 +8345,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_try_catch_lloc() { check_metrics::( @@ -8117,6 +8368,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_nested_functions_loc() { check_metrics::( @@ -8138,6 +8390,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn typescript_generic_function_loc() { check_metrics::( @@ -8159,6 +8412,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_blank() { check_metrics::( @@ -8179,6 +8433,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_cloc() { check_metrics::( @@ -8200,6 +8455,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_no_zero_blank() { // Blank line interleaved with code that carries trailing comments — @@ -8222,6 +8478,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_if_lloc() { check_metrics::( @@ -8244,6 +8501,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_for_lloc() { check_metrics::( @@ -8266,6 +8524,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_while_lloc() { check_metrics::( @@ -8288,6 +8547,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_switch_lloc() { check_metrics::( @@ -8310,6 +8570,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_class_loc() { check_metrics::( @@ -8330,6 +8591,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_arrow_function_loc() { check_metrics::( @@ -8349,6 +8611,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_multiple_functions_loc() { check_metrics::( @@ -8373,6 +8636,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_try_catch_lloc() { check_metrics::( @@ -8395,6 +8659,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_nested_functions_loc() { check_metrics::( @@ -8416,6 +8681,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_interface_loc() { check_metrics::( @@ -8438,6 +8704,7 @@ EOF ); } + #[cfg(feature = "typescript")] #[test] fn tsx_generic_function_loc() { check_metrics::( @@ -8459,6 +8726,7 @@ EOF ); } + #[cfg(feature = "php")] #[test] fn php_blank() { check_metrics::( @@ -8481,6 +8749,7 @@ $b = 2; ); } + #[cfg(feature = "php")] #[test] fn php_no_zero_blank() { // Blank line interleaved with code that carries trailing comments — @@ -8503,6 +8772,7 @@ $c = 3; // trailing ); } + #[cfg(feature = "php")] #[test] fn php_cloc_double_slash() { check_metrics::( @@ -8522,6 +8792,7 @@ $a = 1; // trailing", ); } + #[cfg(feature = "php")] #[test] fn php_cloc_hash() { check_metrics::( @@ -8541,6 +8812,7 @@ $a = 1;", ); } + #[cfg(feature = "php")] #[test] fn php_cloc_block() { check_metrics::( @@ -8562,6 +8834,7 @@ $a = 1;", ); } + #[cfg(feature = "php")] #[test] fn php_lloc() { // Three statements: assignment, if (with body), echo. @@ -8583,6 +8856,7 @@ if ($a > 0) { ); } + #[cfg(feature = "php")] #[test] fn php_no_parenthesized_expression_lloc() { // Parenthesized expression should not add an extra LLOC over the @@ -8602,6 +8876,7 @@ $a = (1 + 2);", ); } + #[cfg(feature = "php")] #[test] fn php_no_compound_statement_lloc() { // Block wrappers (`{ … }`) are not LLOC themselves. @@ -8622,6 +8897,7 @@ function f(): void { ); } + #[cfg(feature = "php")] #[test] fn php_no_colon_block_lloc() { // Alternative syntax (`if: … endif;`) uses ColonBlock instead of @@ -8643,6 +8919,7 @@ endif;", ); } + #[cfg(feature = "php")] #[test] fn php_no_else_clause_lloc() { // ElseClause and ElseIfClause are sub-parts of IfStatement. @@ -8667,6 +8944,7 @@ if ($x) { ); } + #[cfg(feature = "php")] #[test] fn php_no_case_statement_lloc() { // CaseStatement / DefaultStatement are switch arms, not separate @@ -8695,6 +8973,7 @@ switch ($x) { ); } + #[cfg(feature = "php")] #[test] fn php_no_match_arm_lloc() { // MatchConditionalExpression / MatchDefaultExpression are arms; @@ -8718,6 +8997,7 @@ $a = match ($x) { ); } + #[cfg(feature = "php")] #[test] fn php_no_throw_in_expression_lloc() { // PHP 8 `throw` as expression: only the surrounding statement @@ -8737,6 +9017,7 @@ $x = $y ?? throw new \\Exception('nope');", ); } + #[cfg(feature = "php")] #[test] fn php_no_closure_in_assignment_lloc() { // Anonymous function as RHS does not add an LLOC; only the @@ -8759,6 +9040,7 @@ $f = function (): int { ); } + #[cfg(feature = "php")] #[test] fn php_for_lloc() { // The for_statement contributes 1 LLOC; init/cond/update are NOT @@ -8780,6 +9062,7 @@ for ($i = 0; $i < 10; $i++) { ); } + #[cfg(feature = "php")] #[test] fn php_foreach_lloc() { check_metrics::( @@ -8799,6 +9082,7 @@ foreach ($items as $k => $v) { ); } + #[cfg(feature = "php")] #[test] fn php_try_lloc() { check_metrics::( @@ -8822,6 +9106,7 @@ try { ); } + #[cfg(feature = "php")] #[test] fn php_class_loc() { check_metrics::( @@ -8845,6 +9130,7 @@ class A { ); } + #[cfg(feature = "php")] #[test] fn php_namespace_use_lloc() { check_metrics::( @@ -8865,6 +9151,7 @@ $a = 1;", ); } + #[cfg(feature = "php")] #[test] fn php_general_loc() { check_metrics::( @@ -8895,6 +9182,7 @@ class Bar { ); } + #[cfg(feature = "php")] #[test] fn php_match_in_expression_lloc() { // Match inside another expression (e.g. assignment RHS) — the @@ -8914,6 +9202,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "php")] #[test] fn php_html_island_ploc() { // Embedded HTML between PHP tags ("text interpolation"). HTML @@ -8938,6 +9227,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "php")] #[test] fn php_short_echo_tag_ploc() { // ` 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_blank() { // Two blank lines separate three top-level expressions. @@ -9351,6 +9642,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_no_zero_blank() { // Blank line interleaved with code that carries trailing comments — @@ -9367,6 +9659,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_blank_zero_sanity() { // Sanity check: blank must report 0, never go negative, when the @@ -9380,6 +9673,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_cloc() { // Mix of standalone comments and a comment on the same line as @@ -9393,6 +9687,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_lloc() { // Two statements at the top level of the module body — the @@ -9408,6 +9703,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_no_nested_call_lloc() { // Calls nested inside another call's arguments are NOT direct @@ -9422,6 +9718,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_no_binary_operator_inside_call_lloc() { // Binary operators inside call arguments are sub-expressions, @@ -9438,6 +9735,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_stab_clause_counts_lloc() { // Each `stab_clause` arm in a `case do ... end` is a direct @@ -9452,6 +9750,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_no_comment_lloc() { // Comments are direct children of a statement container but @@ -9467,6 +9766,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_no_do_token_lloc() { // The `do` and `end` keyword tokens are unnamed leaves inside a @@ -9480,6 +9780,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", }); } + #[cfg(feature = "elixir")] #[test] fn elixir_no_keyword_pair_lloc() { // `key: value` keyword pairs inside an argument list (`def f, @@ -9495,6 +9796,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_no_string_content_lloc() { // `quoted_content` chunks inside a heredoc / regular string are @@ -9511,6 +9813,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_multiline_string_forms_credit_every_row_to_ploc() { // Regression test for #1260. `quoted_content` — the literal text of @@ -9552,6 +9855,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", }); } + #[cfg(feature = "elixir")] #[test] fn elixir_module_attribute_docstring_rows_are_ploc_not_cloc() { // #1260 had to choose a bucket for `@doc` / `@moduledoc` heredoc @@ -9580,6 +9884,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_rescue_arm_counts_lloc() { // Each rescue arm's body has a single expression (e.g. `:bad`) @@ -9597,6 +9902,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_no_arg_punctuation_lloc() { // Function-call arguments (`a, b` inside `def add(a, b)`) are @@ -9612,6 +9918,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_no_list_element_lloc() { // List literal elements live under a `list` node, not a @@ -9626,6 +9933,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_no_map_field_lloc() { // Map `pair`s live under `map`, not a statement container. @@ -9638,6 +9946,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "elixir")] #[test] fn elixir_anonymous_fn_body_lloc() { // `lloc()` on the Unit space returns the aggregate (own + @@ -9656,6 +9965,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_blank() { // The parser's root span starts at the first non-blank line, so @@ -9666,6 +9976,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", }); } + #[cfg(feature = "ruby")] #[test] fn ruby_no_zero_blank() { // Mirrors `rust_no_zero_blank`: the blank counter must stay @@ -9682,6 +9993,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_cloc() { // 3 comment lines. @@ -9694,6 +10006,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_lloc() { // expected: 3 logical lines = `def` (Method) + `if` (If) + @@ -9708,6 +10021,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_no_call_lloc() { // expected: 1 logical line (the surrounding `def`). The bare @@ -9723,6 +10037,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_no_assignment_lloc() { // Same rationale as `ruby_no_call_lloc`. expected: 1 lloc @@ -9736,6 +10051,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_modifier_lloc() { // Postfix modifier forms each count as one logical line. A @@ -9752,6 +10068,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_class_lloc() { // expected: 1 class + 1 module + 2 methods = 4. @@ -9764,6 +10081,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_begin_rescue_lloc() { // expected: 1 def + 1 begin = 2. Rescue clauses are part of @@ -9778,6 +10096,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_nested_defs_lloc() { // Each `Method` declaration contributes one logical line. @@ -9791,6 +10110,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_no_block_body_lloc() { // A top-level `[1,2,3].each do |x| puts x end` produces zero @@ -9807,6 +10127,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_no_lambda_body_lloc() { // `add = ->(a, b) { a + b }` produces zero logical lines for @@ -9818,6 +10139,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", }); } + #[cfg(feature = "ruby")] #[test] fn ruby_heredoc_lloc_and_blank() { // A `<<~TXT` heredoc contributes: SLOC = every line in the file @@ -9838,6 +10160,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_semicolon_multistatement_lloc_undercount() { // Documented limitation: Ruby has no `expression_statement` @@ -9855,6 +10178,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_ploc_skips_comments_and_blanks() { // PLOC counts physical instruction lines: code-bearing lines @@ -9874,6 +10198,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", // (`typescript_nested_functions_loc`, `tsx_nested_functions_loc`). // ----------------------------------------------------------------- + #[cfg(feature = "python")] #[test] fn python_nested_def_lloc() { // Nested `def`: the inner function declaration plus the outer @@ -9893,6 +10218,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "python")] #[test] fn python_lambda_in_def_lloc() { // `lambda x: x + 1` is an expression, not a Python `function_definition`, @@ -9912,6 +10238,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "python")] #[test] fn python_match_statement_lloc() { // `match` (PEP 634) is a control-flow statement that must add one @@ -9930,6 +10257,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "python")] #[test] fn python_match_lloc_matches_if_else() { // Parity with the equivalent two-branch `if`/`else`: both have the @@ -9951,6 +10279,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "python")] #[test] fn python_type_alias_lloc() { // A `type` alias (PEP 695) is a leaf statement, counted like an @@ -9963,6 +10292,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", }); } + #[cfg(feature = "java")] #[test] fn java_local_class_in_method_lloc() { // A `class` declared inside a method body produces its own function @@ -9982,6 +10312,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "java")] #[test] fn java_lambda_in_method_lloc() { // Java lambdas are expressions; the two LLOC come from the @@ -10001,6 +10332,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_blank() { // Blank lines + simple statements. Newlines act as the @@ -10013,6 +10345,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", }); } + #[cfg(feature = "groovy")] #[test] fn groovy_no_zero_blank() { // A single line with no blanks: blank() == 0. @@ -10022,6 +10355,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", }); } + #[cfg(feature = "groovy")] #[test] fn groovy_cloc_line_comments() { check_metrics::( @@ -10037,6 +10371,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_cloc_block_comment() { check_metrics::( @@ -10052,6 +10387,7 @@ $y = 10 + match ($x) { 1 => 2, default => 0 };", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_cloc_groovydoc_comment() { // Groovy `/** … */` `groovydoc_comment` counts as CLOC. The @@ -10070,6 +10406,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_simple_lloc() { // One LLOC per simple expression statement. @@ -10084,6 +10421,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_no_local_variable_declaration_in_for_lloc() { // The variable declaration inside a classic `for` init slot @@ -10101,6 +10439,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_lambda_in_method_lloc() { // Closures contain a statement list — the dekobon grammar wraps @@ -10123,6 +10462,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_try_lloc() { // try-statement counts as one LLOC; the catch body's @@ -10143,6 +10483,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_class_loc() { // Source-file-level totals across multiple methods. @@ -10169,6 +10510,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_partial_parse_recovers_unit() { // Malformed input parses with ERROR but still emits a Unit @@ -10181,6 +10523,7 @@ class A { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_sloc() { // Mirrors `java_sloc`: basic per-line count across a mix of @@ -10199,6 +10542,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_single_ploc() { // Mirrors `java_single_ploc`: one non-blank, non-comment @@ -10209,6 +10553,7 @@ class A { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_multi_ploc() { // Multiple statements on separate lines all contribute to @@ -10226,6 +10571,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_single_statement_lloc() { // A single expression statement contributes one LLOC. @@ -10235,6 +10581,7 @@ class A { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_for_lloc() { // The classical `for` statement itself counts as one LLOC; @@ -10253,6 +10600,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_foreach_lloc() { // `for (item in list)` parses as `enhanced_for_statement` — @@ -10269,6 +10617,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_while_lloc() { // `while` itself is one LLOC; each body statement adds @@ -10287,6 +10636,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_do_while_lloc() { // `do…while` is one LLOC plus its body. Mirrors @@ -10304,6 +10654,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_continue_lloc() { // `continue` is an LLOC. Same gating as `java_continue_lloc`. @@ -10322,6 +10673,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_expressions_lloc() { // A bag of expression statements: each independent @@ -10340,6 +10692,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_throw_lloc() { // `throw` is one LLOC via the `ThrowStatement` arm. @@ -10352,6 +10705,7 @@ class A { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_general_loc() { // Comprehensive mix: class + method + control flow. @@ -10384,6 +10738,7 @@ class A { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_local_function_in_method_lloc() { // C# local functions (`int Inner(int x) { ... }` inside `Bar()`) @@ -10403,6 +10758,7 @@ class A { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_lambda_in_method_lloc() { // C# lambdas are expressions: the two LLOC come from the @@ -10421,6 +10777,7 @@ class A { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_lambda_in_function_lloc() { // C++11 lambdas are expressions. The outer function `bar()` produces @@ -10444,6 +10801,7 @@ class A { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_nested_function_lloc() { // Nested function_declaration: 4 LLOC = outer's `return inner();`, @@ -10463,6 +10821,7 @@ class A { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_arrow_function_lloc() { // The arrow function `(x) => x + 1` is an expression: the LLOC @@ -10481,6 +10840,7 @@ class A { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_lambda_literal_in_fun_lloc() { // A lambda literal (`{ x -> x + 1 }`) assigned to a `val` plus the @@ -10499,6 +10859,7 @@ class A { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_local_fun_in_fun_lloc() { // Kotlin's local `fun inner(...)` is also a function_declaration, @@ -10518,6 +10879,7 @@ class A { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_object_expression_in_fun_lloc() { // An `object : Runnable { ... }` expression with an overridden @@ -10537,6 +10899,7 @@ class A { ); } + #[cfg(feature = "go")] #[test] fn go_function_literal_initializer_lloc() { // `inner := func(x int) int { return x + 1 }` — the function @@ -10557,6 +10920,7 @@ class A { ); } + #[cfg(feature = "php")] #[test] fn php_anonymous_function_in_function_lloc() { // Anonymous function `function ($x) { return $x + 1; }`: outer @@ -10576,6 +10940,7 @@ class A { ); } + #[cfg(feature = "php")] #[test] fn php_arrow_function_in_function_lloc() { // The `fn ($x) => $x + 1` arrow function is an expression; the @@ -10594,6 +10959,7 @@ class A { ); } + #[cfg(feature = "lua")] #[test] fn lua_nested_local_function_lloc() { // Two nested `local function` declarations: outer + inner both @@ -10613,6 +10979,7 @@ class A { ); } + #[cfg(feature = "lua")] #[test] fn lua_function_expression_in_local_decl_lloc() { // `local f = function (x) return x + 1 end` — the function @@ -10633,6 +11000,7 @@ class A { ); } + #[cfg(feature = "tcl")] #[test] fn tcl_apply_closure_lloc() { // `apply $f 2` is a regular Tcl command, not a separate function @@ -10655,6 +11023,7 @@ class A { ); } + #[cfg(feature = "perl")] #[test] fn perl_anonymous_sub_in_sub_lloc() { // Anonymous sub `sub { ... }` opens its own function space; the @@ -10680,6 +11049,7 @@ class A { ); } + #[cfg(feature = "perl")] #[test] fn perl_named_sub_in_sub_lloc() { // Perl `sub` declarations are not LLOC (see @@ -10710,6 +11080,7 @@ class A { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_fn_inside_def_lloc() { // `fn x -> x + 1 end` inside a `def`: defmodule + def + @@ -10740,6 +11111,7 @@ class A { /// spans are larger; `sloc_min` must be the small leaf, not the class /// or unit span. Verified against the pre-fix code by reverting the /// merge/compute_minmax change (it reports the unit span instead). + #[cfg(feature = "rust")] #[test] fn rust_nested_min_max_propagates() { check_metrics::( @@ -10770,6 +11142,7 @@ class A { /// `java_class_loc` snapshot above showed the bug directly: a class /// with methods reported `sloc_min == sloc` (the unit span). Here we /// assert the smallest method propagates. + #[cfg(feature = "java")] #[test] fn java_nested_min_max_propagates() { check_metrics::( @@ -10789,6 +11162,7 @@ class A { /// Python sibling of `rust_nested_min_max_propagates` (#437). Python /// nesting is class -> method, mirroring the worked example in the /// issue (file -> class C -> method m). + #[cfg(feature = "python")] #[test] fn python_nested_min_max_propagates() { check_metrics::( @@ -10853,6 +11227,12 @@ class A { /// instead of unioning would report `ploc`/`cloc` of 2 against an /// `sloc` of 1 — an impossible reading that also drives `blank` /// negative. + #[cfg(all( + feature = "cpp", + feature = "javascript", + feature = "python", + feature = "rust" + ))] #[test] fn sibling_spaces_sharing_a_line_count_it_once() { check_metrics::( @@ -10907,6 +11287,7 @@ class A { /// `sloc == 1`, the `cloc > sloc` state that pushes MI's /// comments_percentage above 100% (the failure mode of issue #461, /// here across the space merge rather than within one space). + #[cfg(all(feature = "cpp", feature = "rust"))] #[test] fn sibling_spaces_sharing_a_comment_line_count_it_once() { check_metrics::( @@ -10943,6 +11324,7 @@ class A { /// agree whenever the sets happen to be disjoint, and the point of /// #1109 is the repeated fold. The body row belongs to every level's /// span, so a merge that accumulated would report `ploc == 15`. + #[cfg(feature = "rust")] #[test] fn a_row_folded_through_nested_spaces_counts_once() { const DEPTH: usize = 15; @@ -10967,6 +11349,7 @@ class A { /// the MI comments_percentage above 100% (issue #461). Reverting /// the per-line de-dup in `add_code_comment_line` makes the /// `cloc == 1` assertions fail with `2`. + #[cfg(feature = "cpp")] #[test] fn cloc_multiple_block_comments_one_line_cpp() { check_metrics::( @@ -10989,6 +11372,7 @@ class A { /// Sibling-language coverage: `add_cloc_lines` is shared across /// every block-comment language, so the Rust path must behave /// identically to C++ (issue #461). + #[cfg(feature = "rust")] #[test] fn cloc_multiple_block_comments_one_line_rust() { check_metrics::( @@ -11008,6 +11392,7 @@ class A { /// must still contribute one comment line per physical line it /// spans. The de-dup keys on the start row only, so the three /// independent continuation lines are unaffected (issue #461). + #[cfg(feature = "cpp")] #[test] fn cloc_multiline_block_comment_counts_each_line() { check_metrics::( @@ -11031,6 +11416,7 @@ class A { /// violating `cloc <= sloc`. Reverting the per-line set in /// `add_only_comment_lines` makes the `cloc == 1` assertion fail /// with `2` (verified by reverting to `only_comment_lines += …`). + #[cfg(feature = "cpp")] #[test] fn cloc_multiple_standalone_block_comments_one_line_cpp() { check_metrics::("/*a*/ /*b*/", "foo.cpp", |metric| { @@ -11050,6 +11436,7 @@ class A { /// Sibling-language coverage: the standalone de-dup lives in the /// shared `add_only_comment_lines` helper, so Rust must match C++ /// (issue #461 follow-up). + #[cfg(feature = "rust")] #[test] fn cloc_multiple_standalone_block_comments_one_line_rust() { check_metrics::("/*a*/ /*b*/", "foo.rs", |metric| { @@ -11072,6 +11459,7 @@ class A { /// summed `code_comment_lines` (and double-counted a boundary line) /// rather than reading the per-line set. Three standalone comments /// share line 1, so the whole file has exactly one comment line. + #[cfg(feature = "cpp")] #[test] fn cloc_standalone_comments_then_code_no_double_count() { check_metrics::("/*a*/ /*b*/ /*c*/\nint x = 1;\n", "foo.cpp", |metric| { @@ -11088,6 +11476,7 @@ class A { }); } + #[cfg(feature = "irules")] #[test] fn irules_multiline_quoted_word_credits_every_row_to_ploc() { // Regression test for #1260, the dialect half of the Tcl fix: a @@ -11128,6 +11517,7 @@ class A { /// Also the dialect's half of the #1260 carve-out: the handler body is /// a `braced_word`, which the grammar parses as a script, so its blank /// rows stay blank — only `quoted_word` is routed to PLOC. + #[cfg(feature = "irules")] #[test] fn irules_blank() { check_metrics::( @@ -11145,6 +11535,7 @@ class A { /// A handler body with no blank lines reports zero BLANK. lloc 3 = /// handler + `set` + `log` command. + #[cfg(feature = "irules")] #[test] fn irules_no_zero_blank() { check_metrics::( @@ -11167,6 +11558,7 @@ class A { /// them this test passed all the way through #1135, which credited /// each of the three comment rows to PLOC as well (`ploc == 4`, /// `cloc + ploc == 7` against `sloc == 4`). + #[cfg(feature = "irules")] #[test] fn irules_cloc() { check_metrics::( @@ -11184,6 +11576,7 @@ class A { /// LLOC counts each statement once: handler header, `if`, `set`, and the /// generic `log` command = 4. The `switch_arm` headers are not counted /// (their bodies' commands are), verified in `irules_switch_lloc`. + #[cfg(feature = "irules")] #[test] fn irules_lloc() { check_metrics::( @@ -11200,6 +11593,7 @@ class A { /// handler + `set`; the inner `expr` is NOT counted. Removing the /// `CommandSubstitution` guard would push lloc to 3 — this is the loc /// gating-decision regression test. + #[cfg(feature = "irules")] #[test] fn irules_no_command_substitution_lloc() { check_metrics::( @@ -11214,6 +11608,7 @@ class A { /// `switch` counts once; each arm's *body* command counts, but the /// `switch_arm` pattern/body pair itself is not a logical line. lloc 4 = /// handler + `switch` + two `set`s (one per arm body). + #[cfg(feature = "irules")] #[test] fn irules_switch_lloc() { check_metrics::( @@ -11227,6 +11622,7 @@ class A { /// A `proc` definition and its `return` command are each one logical /// line: lloc 2. + #[cfg(feature = "irules")] #[test] fn irules_proc_lloc() { check_metrics::( @@ -11240,6 +11636,7 @@ class A { /// Objective-C blank-line accounting: two code lines separated by /// blank lines. + #[cfg(feature = "objc")] #[test] fn objc_blank() { check_metrics::( @@ -11283,6 +11680,7 @@ class A { /// Objective-C comment accounting: a block comment and a line /// comment each contribute to `cloc`. + #[cfg(feature = "objc")] #[test] fn objc_cloc() { check_metrics::( @@ -11323,6 +11721,7 @@ class A { /// Objective-C logical-line accounting: a method whose body has three /// statements. The `method_definition` opens a function space but is /// not itself a logical line; each statement adds one. + #[cfg(feature = "objc")] #[test] fn objc_lloc() { check_metrics::( @@ -11372,6 +11771,7 @@ class A { /// line and must NOT add a second one (mirrors the C / C++ gate). /// Reverting the `count_specific_ancestors` gate would push lloc from /// 2 to 3. + #[cfg(feature = "objc")] #[test] fn objc_no_declaration_in_for_header_lloc() { check_metrics::( @@ -11417,6 +11817,7 @@ class A { ); } + #[cfg(feature = "objc")] #[test] fn objc_at_directives_lloc() { // The only ObjC-specific LLOC work the impl does beyond the C @@ -11475,6 +11876,7 @@ class A { /// /// Goes through `metrics_verbatim` rather than `check_metrics` /// because the #1051 cases end at EOF; see that helper for why. + #[cfg(feature = "rust")] fn rust_loc(source: &[u8]) -> Stats { metrics_verbatim(crate::LANG::Rust, source, crate::MetricsOptions::default()).loc } @@ -11485,6 +11887,7 @@ class A { /// (debug: the subtraction; release: a hash-table capacity overflow in /// `add_only_comment_lines`). On any later row release did not crash — /// it silently reported one `cloc` too few. + #[cfg(feature = "rust")] #[test] fn rust_doc_comment_at_eof_does_not_underflow() { // `end == start == 0` — underflowed at the subtraction itself. @@ -11521,6 +11924,7 @@ class A { /// EOF. The `DocComment` adjustment exists only to discount the newline /// the scanner consumes; at EOF there is none to discount, so the two /// shapes are indistinguishable for LOC purposes. + #[cfg(feature = "rust")] #[test] fn rust_doc_comment_at_eof_matches_plain_comment() { let plain = rust_loc(b"// x"); @@ -11544,6 +11948,7 @@ class A { /// whenever the scanner consumed a newline, and that row must still be /// excluded — otherwise the guard would silently become a no-op and /// inflate CLOC for every doc-commented Rust file. + #[cfg(feature = "rust")] #[test] fn rust_doc_comment_with_trailing_newline_still_discounts_the_row() { // expected: one rendered comment row, not two. @@ -11564,6 +11969,7 @@ class A { /// owed. A lone trailing `\r` at EOF is the opposite case. Without this, /// a future grammar bump that stops consuming the newline would leave /// every LF test passing while the discount silently became dead code. + #[cfg(feature = "rust")] #[test] fn rust_doc_comment_crlf_still_discounts_the_row() { // Newline consumed despite the `\r`: discount applies. @@ -11598,6 +12004,31 @@ class A { /// `mi != 0` assertion is unreachable with `ploc == 0`. They get /// their own check in /// `no_op_loc_grammars_still_count_their_unterminated_row`. + #[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", + ))] const UNTERMINATED_ONE_LINERS: &[(crate::LANG, &[u8])] = &[ (crate::LANG::Rust, b"fn main() {}"), (crate::LANG::C, b"int main(void) { return 0; }"), @@ -11634,6 +12065,30 @@ class A { /// re-appends a trailing newline, which makes this entire input class /// unreachable and the test vacuous (the same blind spot that hid /// #1051). + #[cfg(all( + feature = "bash", + feature = "c", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", + ))] #[test] fn unterminated_one_line_file_reports_one_source_line() { for (lang, source) in UNTERMINATED_ONE_LINERS { @@ -11667,6 +12122,12 @@ class A { /// Before #1067 an unterminated one-liner measured `0` rows here too. /// Kept apart from [`UNTERMINATED_ONE_LINERS`] only because the /// `mi != 0` half of the sweep below cannot hold with `ploc == 0`. + #[cfg(any( + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "mozcpp" + ))] #[test] fn no_op_loc_grammars_still_count_their_unterminated_row() { for (lang, source) in [ @@ -11834,6 +12295,30 @@ class A { /// is swept separately — it cannot ride this test, whose closing /// `assert_ne!` requires a non-zero MI — in /// [`whitespace_only_input_is_uniform_across_grammars`]. + #[cfg(all( + feature = "bash", + feature = "c", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", + ))] #[test] fn trailing_newline_does_not_change_loc_or_mi() { for (lang, source) in UNTERMINATED_ONE_LINERS { @@ -11896,6 +12381,7 @@ class A { /// The second #1067 symptom: `b"fn f(){}\n/// x"` reported `sloc == 1` /// with `ploc == 1` *and* `cloc == 1`, so `cloc + ploc > sloc`. The /// file has two rows; only the missing one made the sums disagree. + #[cfg(feature = "rust")] #[test] fn unterminated_trailing_comment_upholds_the_cloc_ploc_invariant() { // expected: row 0 is code, row 1 is comment-only, nothing blank. @@ -11909,6 +12395,7 @@ class A { /// Degenerate inputs, pinned so the end-column rule in /// `Node::end_line` cannot drift into fabricating rows for files /// that have none. + #[cfg(feature = "rust")] #[test] fn degenerate_inputs_report_their_real_row_count() { // No bytes, no rows. @@ -11939,6 +12426,31 @@ class A { /// only the language, and must not omit `Preproc`/`Ccomment` — their /// synthetic Unit root is anchored like any other, so they carry a /// real span and answer the #1087/#1247 question too. + #[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", + ))] fn all_loc_grammars() -> impl Iterator { UNTERMINATED_ONE_LINERS .iter() @@ -11964,6 +12476,36 @@ class A { /// /// The unterminated side is unchanged and was always uniform: every /// grammar reports the row, as one blank line. + #[cfg(all( + feature = "bash", + feature = "c", + any( + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "mozcpp" + ), + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", + ))] #[test] fn whitespace_only_input_is_uniform_across_grammars() { // Spaces and tabs both, so a grammar that lexes one as extra and @@ -12021,8 +12563,10 @@ class A { /// /// `space_verbatim`, not `check_metrics`: the shim trims leading and /// trailing newlines, which deletes this test's entire subject. + #[cfg(all(feature = "c", feature = "python", feature = "rust", feature = "tcl"))] #[test] fn leading_blank_rows_count_in_the_units_sloc_and_blank() { + #[cfg(any(feature = "c", feature = "python", feature = "rust", feature = "tcl"))] const LEADING_BLANKS: u64 = 3; for (lang, body, ploc) in [ (crate::LANG::Rust, &b"fn a() {}\n"[..], 1), @@ -12055,6 +12599,7 @@ class A { /// comment on line 1 flipped a byte-identical file from `sloc 1` to /// `sloc 4`, because comments are in the tree and blank rows are not. /// A fix that reached past the unit would move one of these. + #[cfg(feature = "rust")] #[test] fn interior_blanks_and_leading_comments_are_unmoved_by_the_anchor() { // expected: rows 1 and 3 are code, row 2 is blank. @@ -12085,6 +12630,7 @@ class A { /// entire separation between "the file starts at line 1" and "every /// space starts at line 1". Asserted on the nested space's `sloc` as /// well as its span, because only the `sloc` half is new. + #[cfg(feature = "rust")] #[test] fn the_unit_anchor_does_not_reach_nested_spaces() { let space = space_verbatim( @@ -12111,6 +12657,7 @@ class A { /// token, and no pruned subtree can overlap them. Pinned rather than /// argued, since the failure mode is a silent `saturating_sub` clamp /// to 0 rather than a panic (#722, #1247, #1417). + #[cfg(feature = "rust")] #[test] fn exclude_tests_pruning_composes_with_the_unit_anchor() { // Rows 1-3 blank, 4 `fn a`, 5 blank, 6 `#[test]`, 7-9 `fn t`. @@ -12305,6 +12852,7 @@ class A { /// a row it does not occupy. The old unconditional `+ 1` credited that /// row, inflating the last sub of every Perl file by one line — here, /// reporting a 3-row `sub` as 4. + #[cfg(feature = "perl")] #[test] fn perl_last_sub_does_not_absorb_the_trailing_newline() { // Two identical 3-row subs; only the second hits the quirk. @@ -12598,6 +13146,7 @@ class A { /// /// Uses [`metrics_verbatim`] so the fixtures reach the parser /// byte-for-byte; `check_metrics` rewrites the trailing newline. + #[cfg(all(feature = "irules", feature = "tcl"))] #[test] fn tcl_family_does_not_count_terminator_rows_as_code() { for lang in [crate::LANG::Tcl, crate::LANG::Irules] { @@ -12655,6 +13204,30 @@ class A { /// comment node reaching a PLOC catch-all, which any one spelling of /// a comment exposes. The block and doc entries are there because /// those nodes have child tokens the line form does not. + #[cfg(any( + feature = "bash", + feature = "c", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", + ))] fn comment_spellings(lang: crate::LANG) -> &'static [&'static str] { use crate::LANG::*; match lang { @@ -12683,6 +13256,30 @@ class A { /// fixtures carry no comment row at all. Since the failure mode is /// structural rather than language-specific, the sweep is per /// language rather than a sample. + #[cfg(all( + feature = "bash", + feature = "c", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", + ))] #[test] fn a_comment_row_is_never_counted_as_code() { for (lang, code) in UNTERMINATED_ONE_LINERS { @@ -12726,6 +13323,7 @@ class A { /// carrying two declarators, so it counts once, not twice. The fifth row /// is a `using_declaration` — the grammar's third executable declaration /// kind, which TypeScript and TSX do not have. + #[cfg(feature = "javascript")] #[test] fn javascript_declaration_lloc() { check_metrics::( @@ -12751,6 +13349,7 @@ class A { /// `StatementBlock` stops the ancestor walk (#1283). /// /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4 + #[cfg(feature = "javascript")] #[test] fn javascript_for_header_declaration_not_double_counted() { check_metrics::( @@ -12773,6 +13372,7 @@ class A { /// is reached (#1283). /// /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5 + #[cfg(feature = "javascript")] #[test] fn javascript_exported_declaration_counts_once() { check_metrics::( @@ -12795,6 +13395,7 @@ class A { /// carrying two declarators, so it counts once, not twice. The fifth row /// is a `using_declaration` — the grammar's third executable declaration /// kind, which TypeScript and TSX do not have. + #[cfg(feature = "mozjs")] #[test] fn mozjs_declaration_lloc() { check_metrics::( @@ -12820,6 +13421,7 @@ class A { /// `StatementBlock` stops the ancestor walk (#1283). /// /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4 + #[cfg(feature = "mozjs")] #[test] fn mozjs_for_header_declaration_not_double_counted() { check_metrics::( @@ -12842,6 +13444,7 @@ class A { /// is reached (#1283). /// /// expected: 4 export statements + the `const c = 4;` in `f`'s body = 5 + #[cfg(feature = "mozjs")] #[test] fn mozjs_exported_declaration_counts_once() { check_metrics::( @@ -12862,6 +13465,7 @@ class A { /// Rust's `let` (#1283 — before the fix a declarations-only file /// reported `lloc 0`). The fourth row is one `variable_declaration` /// carrying two declarators, so it counts once, not twice. + #[cfg(feature = "typescript")] #[test] fn typescript_declaration_lloc() { check_metrics::( @@ -12887,6 +13491,7 @@ class A { /// `StatementBlock` stops the ancestor walk (#1283). /// /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4 + #[cfg(feature = "typescript")] #[test] fn typescript_for_header_declaration_not_double_counted() { check_metrics::( @@ -12914,6 +13519,7 @@ class A { /// `ambient_declaration` sits between the export and the declaration: the /// carve-out walks the ancestor chain rather than checking the parent, so /// it still sees the enclosing `ExportStatement`. + #[cfg(feature = "typescript")] #[test] fn typescript_exported_declaration_counts_once() { check_metrics::( @@ -12934,6 +13540,7 @@ class A { /// Rust's `let` (#1283 — before the fix a declarations-only file /// reported `lloc 0`). The fourth row is one `variable_declaration` /// carrying two declarators, so it counts once, not twice. + #[cfg(feature = "typescript")] #[test] fn tsx_declaration_lloc() { check_metrics::( @@ -12959,6 +13566,7 @@ class A { /// `StatementBlock` stops the ancestor walk (#1283). /// /// expected: for-statement 1 + body declaration 1 + for-of 1 + for-in 1 = 4 + #[cfg(feature = "typescript")] #[test] fn tsx_for_header_declaration_not_double_counted() { check_metrics::( @@ -12986,6 +13594,7 @@ class A { /// `ambient_declaration` sits between the export and the declaration: the /// carve-out walks the ancestor chain rather than checking the parent, so /// it still sees the enclosing `ExportStatement`. + #[cfg(feature = "typescript")] #[test] fn tsx_exported_declaration_counts_once() { check_metrics::( @@ -13011,9 +13620,12 @@ class A { /// /// expected: for 1 + body declaration 1 = 2; for 1 + switch 1 + /// case declaration 1 = 3. + #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))] #[test] fn js_family_braceless_for_body_declaration_counts() { + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] const BODY: &str = "for (var i = 0; i < 3; i++) var s = i;\n"; + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] const NESTED: &str = "for (let i = 0; i < 2; i++) switch (i) { case 0: let y = 1; }\n"; check_metrics::(BODY, "foo.js", |m| assert_eq!(m.loc.lloc(), 2)); check_metrics::(BODY, "foo.js", |m| assert_eq!(m.loc.lloc(), 2)); @@ -13033,6 +13645,7 @@ class A { /// reported one LLOC per `declare const`). /// /// expected: 0 — every row is ambient. + #[cfg(feature = "typescript")] #[test] fn typescript_ambient_declarations_are_not_logical_lines() { const SRC: &str = "declare const VERSION: string;\ndeclare let mutable: number;\ndeclare namespace NS { const inner: number; }\ndeclare module \"m\" { let y: string; }\n"; diff --git a/src/metrics/mi.rs b/src/metrics/mi.rs index 8f4cb7e6a..67f26815b 100644 --- a/src/metrics/mi.rs +++ b/src/metrics/mi.rs @@ -200,6 +200,7 @@ mod tests { // defaults. check_metrics_only_shim!(check_metrics, Mi); + #[cfg(feature = "python")] #[test] fn mi_empty_file() { check_metrics::("", "empty.py", |metric| { @@ -210,6 +211,7 @@ mod tests { }); } + #[cfg(feature = "python")] #[test] fn check_mi_metrics() { // This test checks that MI metric is computed correctly, so it verifies @@ -285,6 +287,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_mi_smoke() { // Rust now derives MI from the populated Loc / Cyclomatic / @@ -306,6 +309,7 @@ mod tests { }); } + #[cfg(feature = "go")] #[test] fn go_mi_smoke() { // Go uses the default `Mi::compute`; once Loc / Cyclomatic / @@ -323,6 +327,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_mi_smoke() { // Elixir uses the default `Mi::compute`; with Loc / Cyclomatic @@ -340,6 +345,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_mi_smoke() { // C++ uses the default `Mi::compute`; Loc / Cyclomatic / @@ -358,6 +364,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_mi_smoke() { // JavaScript uses the default `Mi::compute`; Loc / Cyclomatic @@ -375,6 +382,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_mi_smoke() { // Mozjs shares JavaScript's MI cascade; this is a parity pin. @@ -397,6 +405,7 @@ mod tests { /// capped at exactly 100, not the ~209 the raw ratio would give /// (issue #461). Here `cloc = 2`, `sloc = 1` => raw 200%. Reverting /// the `.clamp(0.0, 100.0)` in `Mi::compute` makes this fail. + #[cfg(feature = "python")] #[test] fn mi_comments_percentage_clamped() { // cloc = 2 (degenerate), sloc = 1 (single non-unit row) => raw diff --git a/src/metrics/nargs.rs b/src/metrics/nargs.rs index 69fdb3b0c..c05153a0d 100644 --- a/src/metrics/nargs.rs +++ b/src/metrics/nargs.rs @@ -859,6 +859,7 @@ mod tests { /// The fixture is the #1236 reproducer, picked because no space in it /// has `own == total`: a fixture where the two agree passes whichever /// one the code reads. + #[cfg(feature = "rust")] #[test] fn own_args_excludes_nested_closure_spaces() { fn walk(space: &FuncSpace, rows: &mut Vec<(String, usize, u64, u64)>) { @@ -909,6 +910,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_no_functions_and_closures() { check_metrics::("a = 42", "foo.py", |metric| { @@ -934,6 +936,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn rust_no_functions_and_closures() { check_metrics::("let a = 42;", "foo.rs", |metric| { @@ -959,6 +962,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_no_functions_and_closures() { check_metrics::("int a = 42;", "foo.cpp", |metric| { @@ -984,6 +988,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_no_functions_and_closures() { check_metrics::("var a = 42;", "foo.js", |metric| { @@ -1009,6 +1014,7 @@ mod tests { }); } + #[cfg(feature = "python")] #[test] fn python_single_function() { check_metrics::( @@ -1040,6 +1046,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_single_function() { check_metrics::( @@ -1073,6 +1080,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_single_function() { check_metrics::( @@ -1106,6 +1114,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_single_function() { check_metrics::( @@ -1137,6 +1146,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_single_lambda() { check_metrics::("bar = lambda a: True", "foo.py", |metric| { @@ -1162,6 +1172,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn rust_single_closure() { check_metrics::("let bar = |i: i32| -> i32 { i + 1 };", "foo.rs", |metric| { @@ -1187,6 +1198,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_single_lambda() { check_metrics::( @@ -1216,6 +1228,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_single_closure() { check_metrics::("function (a, b) {return a + b};", "foo.js", |metric| { @@ -1241,6 +1254,7 @@ mod tests { }); } + #[cfg(feature = "python")] #[test] fn python_functions() { check_metrics::( @@ -1306,6 +1320,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_functions() { check_metrics::( @@ -1382,6 +1397,7 @@ mod tests { /// The `self` receiver (`self`, `&self`, `&mut self`) parses as a /// `self_parameter` node and, like Go's `receiver` field and C++'s /// implicit `this`, must not be counted as a formal parameter (#457). + #[cfg(feature = "rust")] #[test] fn rust_method_excludes_self_receiver() { check_metrics::( @@ -1427,6 +1443,7 @@ mod tests { ); } + #[cfg(all(feature = "c", feature = "cpp"))] #[test] fn c_functions() { check_metrics::( @@ -1500,6 +1517,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_functions() { check_metrics::( @@ -1565,6 +1583,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_nested_functions() { check_metrics::( @@ -1599,6 +1618,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_nested_functions() { check_metrics::( @@ -1635,6 +1655,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_nested_functions() { check_metrics::( @@ -1671,6 +1692,7 @@ mod tests { /// Default arguments still surface as separate `parameter_declaration` /// nodes — defaults are not removed from the count. A 3-param function /// whose third parameter has a default value reports `nargs = 3`. + #[cfg(feature = "cpp")] #[test] fn cpp_default_arguments() { check_metrics::( @@ -1710,6 +1732,7 @@ mod tests { /// as a sibling parameter node that `count_args` counts, because it is /// neither a comment nor one of the `(`, `)`, `,` tokens `CCode::is_non_arg` /// rejects. + #[cfg(feature = "c")] #[test] fn c_variadic_function() { check_metrics::( @@ -1750,6 +1773,7 @@ mod tests { /// not on `parameters`. The tree-sitter-cpp grammar represents /// `Args... args` as a single `variadic_parameter_declaration` under /// `parameters`. + #[cfg(feature = "cpp")] #[test] fn cpp_template_parameter_pack() { check_metrics::( @@ -1789,6 +1813,7 @@ mod tests { /// `compute_args` reads the `declarator` field, which only contains the /// `( … )` parameter list. Variables captured for the closure body do /// not inflate `nargs`. + #[cfg(feature = "cpp")] #[test] fn cpp_lambda_capture_not_counted() { check_metrics::( @@ -1831,6 +1856,7 @@ mod tests { /// parameter list — it is an implicit argument at the language level /// only. A non-static member function `void M(int a)` reports /// `nargs = 1`, not 2. + #[cfg(feature = "cpp")] #[test] fn cpp_member_function_this_not_counted() { check_metrics::( @@ -1869,6 +1895,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_zero_args() { check_metrics::( @@ -1898,6 +1925,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_multiple_args() { check_metrics::( @@ -1927,6 +1955,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_method_excludes_receiver() { check_metrics::( @@ -1960,6 +1989,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_variadic() { check_metrics::( @@ -1989,6 +2019,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_grouped_params() { check_metrics::( @@ -2020,6 +2051,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_func_literal_args() { check_metrics::( @@ -2050,6 +2082,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_nested_functions() { check_metrics::( @@ -2086,6 +2119,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_no_functions_and_closures() { check_metrics::( @@ -2121,6 +2155,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_single_function() { // This sub declares no signature, so it has no formal parameters to @@ -2159,6 +2194,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_single_closure() { // This closure declares no signature, so nargs stays 0; it takes its @@ -2197,6 +2233,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_multiple_functions() { // Neither sub declares a signature, so both count 0. Assert nom @@ -2234,6 +2271,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_nested_closure() { // Neither the outer sub nor the nested closure declares a signature, @@ -2273,6 +2311,7 @@ mod tests { /// Regression for #1147: a signature sub reported 0 because the /// signature is an unnamed `function_signature` child, not a /// `parameters` field. + #[cfg(feature = "perl")] #[test] fn perl_signature_function() { check_metrics::( @@ -2310,6 +2349,7 @@ mod tests { /// `scalar_variable`, so counting only the variable kinds would report /// 2 here instead of 3. Pins the negative filter in /// `compute_perl_args` (#1147). + #[cfg(feature = "perl")] #[test] fn perl_signature_defaults_and_slurpy() { check_metrics::( @@ -2346,6 +2386,7 @@ mod tests { /// A signature sub and an `@_` sub in one file: the min/max and the /// average have to keep the zero-argument sub in the divisor rather /// than folding it away. + #[cfg(feature = "perl")] #[test] fn perl_signature_and_at_underscore_mixed() { check_metrics::( @@ -2386,6 +2427,7 @@ mod tests { /// (`sub NAME ATTRS SIG BLOCK`), and a bare attribute swallows the /// signature into its own `function_attribute` node. Pins the /// one-level descent in `perl_signature`. + #[cfg(feature = "perl")] #[test] fn perl_signature_behind_attribute() { check_metrics::( @@ -2423,6 +2465,7 @@ mod tests { /// children sitting directly under `function_signature`, so the /// negative filter has to exclude them or a documented 3-parameter sub /// reads 6 and trips the default `nargs` limit of 5. + #[cfg(feature = "perl")] #[test] fn perl_signature_comments_are_not_parameters() { check_metrics::( @@ -2464,6 +2507,7 @@ mod tests { /// depends on: an empty signature has no children but the parens, and /// a prototype (`($$)`) is a `function_prototype`, a different kind /// that `perl_signature` deliberately does not match. + #[cfg(feature = "perl")] #[test] fn perl_empty_signature_and_prototype_are_zero() { check_metrics::( @@ -2483,6 +2527,7 @@ mod tests { /// Perl 5.38's `method` is a second `is_func` kind /// (`function_definition_without_sub`) reaching the same helper, so it /// gets its own fixture rather than riding on the `sub` tests. + #[cfg(feature = "perl")] #[test] fn perl_method_signature_function() { check_metrics::( @@ -2503,6 +2548,7 @@ mod tests { /// `perl_signature` lists it defensively. Pin that the grammar never /// emits it, so a bump that promotes the rule fails loudly instead of /// changing behaviour invisibly (lesson 34). + #[cfg(feature = "perl")] #[test] fn perl_hidden_function_signature_is_unreachable() { let mut hidden = false; @@ -2530,6 +2576,7 @@ mod tests { /// 1.1.2 parses an anonymous sub's signature inside an `ERROR` node, /// so a signature-carrying closure counts 0. A grammar bump that fixes /// the parse should fail this test rather than shift metrics silently. + #[cfg(feature = "perl")] #[test] fn perl_anonymous_sub_signature_is_zero() { check_metrics::( @@ -2564,6 +2611,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_no_functions() { check_metrics::( @@ -2595,6 +2643,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_single_method() { check_metrics::( @@ -2627,6 +2676,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_multiple_methods() { check_metrics::( @@ -2662,6 +2712,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_constructor_args() { check_metrics::( @@ -2707,6 +2758,7 @@ mod tests { /// walked to the outermost record instead would give `Single` 2 and /// total 4, and one that read the constructor node itself — which has /// no `parameters` field — would give 0. + #[cfg(feature = "java")] #[test] fn java_record_compact_constructor_counts_record_components() { check_metrics::( @@ -2737,6 +2789,7 @@ mod tests { /// `formal_parameter` — and binds `this`, not a value. Like Rust's /// `self_parameter` (#457), Go's `receiver` field, and C++'s implicit /// `this`, it must not be counted as a formal parameter (#470). + #[cfg(feature = "java")] #[test] fn java_method_excludes_explicit_receiver() { check_metrics::( @@ -2757,6 +2810,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_lambda_args() { check_metrics::( @@ -2789,6 +2843,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_no_functions_and_closures() { check_metrics::("int x = 1", "foo.groovy", |metric| { @@ -2796,6 +2851,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_single_method() { check_metrics::( @@ -2812,6 +2868,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_multiple_methods() { check_metrics::( @@ -2826,6 +2883,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_lambda_args() { // Two-parameter Groovy closure inside a method body. Groovy's @@ -2846,6 +2904,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_implicit_it_not_counted() { // The `it` implicit closure parameter is just an identifier in @@ -2864,6 +2923,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_no_functions() { check_metrics::( @@ -2895,6 +2955,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_single_method() { check_metrics::( @@ -2927,6 +2988,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_multiple_methods() { check_metrics::( @@ -2962,6 +3024,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_constructor_args() { check_metrics::( @@ -2994,6 +3057,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_lambda_args() { check_metrics::( @@ -3026,6 +3090,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_function_and_arrow() { check_metrics::( @@ -3057,6 +3122,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_typed_and_optional_params() { check_metrics::( @@ -3088,6 +3154,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_single_function() { check_metrics::( @@ -3118,6 +3185,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_closure_args() { check_metrics::("function (a, b) {return a + b};", "foo.js", |metric| { @@ -3151,6 +3219,7 @@ mod tests { // on its enclosing context — e.g. a `VariableDeclarator` ancestor makes // it a function). + #[cfg(feature = "javascript")] #[test] fn javascript_bare_arrow_function() { check_metrics::("const f = x => x;", "foo.js", |metric| { @@ -3158,6 +3227,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_async_bare_arrow_function() { check_metrics::("const f = async x => x;", "foo.js", |metric| { @@ -3165,6 +3235,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_parenthesized_arrow_function() { check_metrics::("const f = (x) => x;", "foo.js", |metric| { @@ -3172,6 +3243,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_multi_parenthesized_arrow_function() { check_metrics::("const f = (x, y) => x + y;", "foo.js", |metric| { @@ -3179,6 +3251,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn typescript_bare_arrow_function() { check_metrics::("const f = x => x;", "foo.ts", |metric| { @@ -3186,6 +3259,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn typescript_async_bare_arrow_function() { check_metrics::("const f = async x => x;", "foo.ts", |metric| { @@ -3193,6 +3267,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn typescript_parenthesized_arrow_function() { check_metrics::("const f = (x: number) => x;", "foo.ts", |metric| { @@ -3200,6 +3275,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn typescript_multi_parenthesized_arrow_function() { check_metrics::( @@ -3211,6 +3287,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_bare_arrow_function() { check_metrics::("const f = x => x;", "foo.tsx", |metric| { @@ -3218,6 +3295,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn tsx_async_bare_arrow_function() { check_metrics::("const f = async x => x;", "foo.tsx", |metric| { @@ -3225,6 +3303,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn tsx_parenthesized_arrow_function() { check_metrics::("const f = (x: number) => x;", "foo.tsx", |metric| { @@ -3232,6 +3311,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn tsx_multi_parenthesized_arrow_function() { check_metrics::( @@ -3243,6 +3323,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_bare_arrow_function() { check_metrics::("const f = x => x;", "foo.js", |metric| { @@ -3250,6 +3331,7 @@ mod tests { }); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_async_bare_arrow_function() { check_metrics::("const f = async x => x;", "foo.js", |metric| { @@ -3257,6 +3339,7 @@ mod tests { }); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_parenthesized_arrow_function() { check_metrics::("const f = (x) => x;", "foo.js", |metric| { @@ -3264,6 +3347,7 @@ mod tests { }); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_multi_parenthesized_arrow_function() { check_metrics::("const f = (x, y) => x + y;", "foo.js", |metric| { @@ -3271,6 +3355,7 @@ mod tests { }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_nargs_functions_and_closures() { check_metrics::( @@ -3302,6 +3387,7 @@ mod tests { ); } + #[cfg(feature = "lua")] #[test] fn lua_no_functions_and_closures() { check_metrics::("local x = 1", "foo.lua", |metric| { @@ -3312,6 +3398,7 @@ mod tests { }); } + #[cfg(feature = "lua")] #[test] fn lua_single_function() { check_metrics::("function f(a, b) return a + b end", "foo.lua", |metric| { @@ -3322,6 +3409,7 @@ mod tests { }); } + #[cfg(feature = "lua")] #[test] fn lua_single_closure() { check_metrics::( @@ -3336,6 +3424,7 @@ mod tests { ); } + #[cfg(feature = "lua")] #[test] fn lua_functions() { check_metrics::( @@ -3351,6 +3440,7 @@ function g(x, y, z) return x + y + z end", ); } + #[cfg(feature = "lua")] #[test] fn lua_vararg_function() { // `...` is a vararg_expression node and counts as one argument. @@ -3362,6 +3452,7 @@ function g(x, y, z) return x + y + z end", }); } + #[cfg(feature = "lua")] #[test] fn lua_colon_method_nargs() { // Colon syntax: `self` is implicit and NOT in the `parameters` node. @@ -3378,6 +3469,7 @@ function g(x, y, z) return x + y + z end", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_no_functions() { check_metrics::("set x 1", "foo.tcl", |metric| { @@ -3388,6 +3480,7 @@ function g(x, y, z) return x + y + z end", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_single_function() { check_metrics::("proc f {a b} { puts $a }", "foo.tcl", |metric| { @@ -3398,6 +3491,7 @@ function g(x, y, z) return x + y + z end", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_single_function_no_args() { check_metrics::("proc f {} { puts hello }", "foo.tcl", |metric| { @@ -3408,6 +3502,7 @@ function g(x, y, z) return x + y + z end", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_functions() { check_metrics::( @@ -3423,6 +3518,7 @@ proc g {x y z} { puts $x }", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_nested_functions() { check_metrics::( @@ -3440,6 +3536,7 @@ proc g {x y z} { puts $x }", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_args_vararg() { // `args` is the Tcl variadic catch-all; it counts as one argument. @@ -3451,6 +3548,7 @@ proc g {x y z} { puts $x }", }); } + #[cfg(feature = "tcl")] #[test] fn tcl_default_arg() { // `{name default}` is a single argument with a default value. @@ -3468,6 +3566,7 @@ proc g {x y z} { puts $x }", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_zero_args() { check_metrics::("fun f(): Int { return 42 }", "foo.kt", |metric| { @@ -3478,6 +3577,7 @@ proc g {x y z} { puts $x }", }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_single_arg() { check_metrics::( @@ -3492,6 +3592,7 @@ proc g {x y z} { puts $x }", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_multiple_args() { check_metrics::( @@ -3506,6 +3607,7 @@ proc g {x y z} { puts $x }", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_default_args() { check_metrics::( @@ -3522,6 +3624,7 @@ proc g {x y z} { puts $x }", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_empty_lambda() { // Two lambdas in the same function body: one with two explicit parameters @@ -3548,6 +3651,7 @@ proc g {x y z} { puts $x }", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_anonymous_function() { // `fun(x: Int, y: Int) = x + y` — anonymous function expression. @@ -3565,6 +3669,7 @@ proc g {x y z} { puts $x }", ); } + #[cfg(feature = "php")] #[test] fn php_no_functions_and_closures() { check_metrics::("( @@ -3742,6 +3852,7 @@ proc g {x y z} { puts $x }", /// A guard interposes a `when` `binary_operator` between the macro's /// `arguments` and the head `Call`. Without unwrapping it every /// guarded clause — a large fraction of real Elixir — counts 0. + #[cfg(feature = "elixir")] #[test] fn elixir_guarded_clause_args() { check_metrics::( @@ -3760,6 +3871,7 @@ proc g {x y z} { puts $x }", /// would be. It has no parameter list, and the walk must stop there /// rather than fall through to the enclosing `arguments` — which /// holds the `do:` keyword pair and would count 1. + #[cfg(feature = "elixir")] #[test] fn elixir_zero_arg_function_has_no_parameter_list() { check_metrics::( @@ -3775,6 +3887,7 @@ proc g {x y z} { puts $x }", /// Pattern and defaulted parameters are `map` and `binary_operator` /// nodes rather than plain identifiers, so the punctuation-negative /// filter is what keeps them counted. + #[cfg(feature = "elixir")] #[test] fn elixir_pattern_and_default_args() { check_metrics::( @@ -3791,6 +3904,7 @@ proc g {x y z} { puts $x }", /// Every clause of one `fn` has the same arity, so a two-clause /// two-argument closure is 2 — summing the clauses would report 4. + #[cfg(feature = "elixir")] #[test] fn elixir_multi_clause_closure_counts_one_clause() { check_metrics::( @@ -3810,6 +3924,7 @@ proc g {x y z} { puts $x }", /// unwrap. Without it the count is the guard expression's fixed three /// children — 3 for any arity, which is why the four-parameter form is /// the fixture here. + #[cfg(feature = "elixir")] #[test] fn elixir_guarded_closure_args() { check_metrics::( @@ -3827,6 +3942,7 @@ proc g {x y z} { puts $x }", /// `def a + b` and `def -a` define the operator functions `+/2` and /// `-/1`. Their head is the operator node itself, with no `arguments` /// container to walk, so the arity comes from the operator's shape. + #[cfg(feature = "elixir")] #[test] fn elixir_operator_definition_args() { check_metrics::( @@ -3844,6 +3960,7 @@ proc g {x y z} { puts $x }", /// A `def` inside `quote do … end` is a code template, not a /// declaration, and must not contribute arguments (#310). The quoted /// head carries three parameters, so dropping the rule reads 3. + #[cfg(feature = "elixir")] #[test] fn elixir_quoted_def_contributes_no_args() { check_metrics::( @@ -3865,6 +3982,7 @@ proc g {x y z} { puts $x }", /// `defmodule` and `defdelegate` are ordinary `Call`s of the same /// shape — `defdelegate log(msg), to: Logger` has a head `Call` with /// one parameter, so a gate that matched any macro would read 1. + #[cfg(feature = "elixir")] #[test] fn elixir_non_method_macros_count_zero() { check_metrics::( @@ -3877,6 +3995,7 @@ proc g {x y z} { puts $x }", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_no_functions_and_closures() { check_metrics::("a = 42\n", "foo.rb", |metric| { @@ -3885,6 +4004,7 @@ proc g {x y z} { puts $x }", }); } + #[cfg(feature = "ruby")] #[test] fn ruby_single_function() { // Single method with 3 parameters. @@ -3894,6 +4014,7 @@ proc g {x y z} { puts $x }", }); } + #[cfg(feature = "ruby")] #[test] fn ruby_single_closure() { // A bare block `[1,2,3].each { |x| ... }` is the only closure @@ -3904,6 +4025,7 @@ proc g {x y z} { puts $x }", }); } + #[cfg(feature = "ruby")] #[test] fn ruby_functions() { // Two methods, args=2 and args=1; one lambda with args=2. @@ -3917,6 +4039,7 @@ proc g {x y z} { puts $x }", ); } + #[cfg(feature = "ruby")] #[test] fn ruby_nested_functions() { // An outer method with 1 arg containing an inner method with 2. @@ -3934,6 +4057,7 @@ proc g {x y z} { puts $x }", /// punctuation, not parameters. The grammar emits them as /// `positional_separator` / `keyword_separator` siblings of the real /// parameter nodes; both must be excluded from nargs (issue #414). + #[cfg(feature = "python")] #[test] fn python_both_parameter_separators() { // 1 function, 3 real parameters: pos_only, normal, kw_only. @@ -3948,6 +4072,7 @@ proc g {x y z} { puts $x }", } /// Trailing positional-only `/` (no following parameter) is still excluded. + #[cfg(feature = "python")] #[test] fn python_positional_separator_only() { // 1 function, 2 real parameters: a, b (`/` excluded). @@ -3959,6 +4084,7 @@ proc g {x y z} { puts $x }", /// Leading keyword-only `*` (forcing all following parameters to be /// keyword-only) is excluded. + #[cfg(feature = "python")] #[test] fn python_keyword_separator_only() { // 1 function, 2 real parameters: a, b (`*` excluded). @@ -3970,6 +4096,7 @@ proc g {x y z} { puts $x }", /// Lambdas accept the same keyword-only `*` separator; it is excluded /// from the closure arg count. + #[cfg(feature = "python")] #[test] fn python_lambda_keyword_separator() { // 1 lambda, 2 real parameters: a, b (`*` excluded). @@ -3982,6 +4109,7 @@ proc g {x y z} { puts $x }", /// Regression guard: `*args` / `**kwargs` are real parameter nodes /// (`list_splat_pattern` / `dictionary_splat_pattern`), not separators, /// and must keep contributing to the count after the #414 fix. + #[cfg(feature = "python")] #[test] fn python_args_kwargs_still_counted() { // 1 function, 3 parameters: a, *args, **kwargs. @@ -3993,6 +4121,7 @@ proc g {x y z} { puts $x }", /// A file of bare top-level commands has no function spaces, so the /// argument count is zero. + #[cfg(feature = "irules")] #[test] fn irules_no_functions_and_closures() { check_metrics::("set x 1\nlog local0. $x\n", "foo.irule", |metric| { @@ -4004,6 +4133,7 @@ proc g {x y z} { puts $x }", /// A `when` handler is a function space but has no formal parameters /// (the event context is implicit), so its argument count is zero — /// `when_event` carries no `arguments` field. Guards edge case #10. + #[cfg(feature = "irules")] #[test] fn irules_handler_zero_args() { check_metrics::( @@ -4019,6 +4149,7 @@ proc g {x y z} { puts $x }", } /// A `proc` with two formal parameters contributes two arguments. + #[cfg(feature = "irules")] #[test] fn irules_single_proc() { check_metrics::("proc f { a b } { return $a }\n", "foo.irule", |metric| { @@ -4028,6 +4159,7 @@ proc g {x y z} { puts $x }", } /// A `proc` with an empty argument list contributes zero arguments. + #[cfg(feature = "irules")] #[test] fn irules_proc_no_args() { check_metrics::("proc f { } { return 1 }\n", "foo.irule", |metric| { @@ -4038,6 +4170,7 @@ proc g {x y z} { puts $x }", /// A default-valued parameter (`{b 5}`) is a single `argument`, so each /// formal parameter counts once regardless of its default: `{a {b 5} c}` /// is three arguments. + #[cfg(feature = "irules")] #[test] fn irules_proc_arg_defaults() { check_metrics::( @@ -4051,6 +4184,7 @@ proc g {x y z} { puts $x }", /// A `proc` and a `when` handler in one file: only the proc's two /// parameters count; the handler contributes zero. + #[cfg(feature = "irules")] #[test] fn irules_multiple_functions() { check_metrics::( @@ -4066,6 +4200,7 @@ when HTTP_REQUEST { log local0. \"hit\" } } /// Objective-C unary method `- (void)foo` declares zero arguments. + #[cfg(feature = "objc")] #[test] fn objc_no_args() { check_metrics::( @@ -4099,6 +4234,7 @@ when HTTP_REQUEST { log local0. \"hit\" } /// Objective-C keyword method `- (void)foo:(int)a bar:(int)b` has two /// `method_parameter` children, so `function_args` is 2. + #[cfg(feature = "objc")] #[test] fn objc_method_two_args() { check_metrics::( @@ -4132,6 +4268,7 @@ when HTTP_REQUEST { log local0. \"hit\" } /// Free C `function_definition` inside an ObjC translation unit counts /// its declarator parameters: `void f(int a, int b, int c)` has 3. + #[cfg(feature = "objc")] #[test] fn objc_function_args() { check_metrics::( @@ -4164,6 +4301,7 @@ when HTTP_REQUEST { log local0. \"hit\" } /// Objective-C block literal `^(int x, int y){ … }` is a closure /// whose `parameter_list` holds two `parameter_declaration`s, so /// `closure_args` is 2. + #[cfg(feature = "objc")] #[test] fn objc_block_args() { check_metrics::( @@ -4209,6 +4347,7 @@ when HTTP_REQUEST { log local0. \"hit\" } /// through `count_args`, while the function channel beside it was /// already correct: `host` below reports 0 either way, which is what /// makes this a test of the block channel specifically. + #[cfg(feature = "objc")] #[test] fn objc_block_void_marker_is_not_a_parameter() { check_metrics::( @@ -4252,6 +4391,7 @@ when HTTP_REQUEST { log local0. \"hit\" } /// arm switched to `count_args`, whose *negative* filtering is what /// now makes `Checker::is_comment` live on this path. Perturb it by /// dropping `is_comment` from `count_args`, not by reverting the arm. + #[cfg(feature = "objc")] #[test] fn objc_block_comment_is_not_a_parameter() { check_metrics::( @@ -4276,6 +4416,7 @@ when HTTP_REQUEST { log local0. \"hit\" } /// old match and in none of the new filters, so only a fixture says /// whether it survived. `ObjcCode::is_non_arg` covers the list's /// punctuation (`(`, `,`, `)`) and nothing else, so it does. + #[cfg(feature = "objc")] #[test] fn objc_block_variadic_parameter_still_counts() { check_metrics::( @@ -4299,6 +4440,7 @@ when HTTP_REQUEST { log local0. \"hit\" } /// `count_args` is reached. Pinned beside the `^(void)` case because /// the two spellings mean the same thing and only one of them ever /// went through the counting path. + #[cfg(feature = "objc")] #[test] fn objc_block_without_a_parameter_list_is_zero() { check_metrics::( @@ -4324,6 +4466,7 @@ when HTTP_REQUEST { log local0. \"hit\" } /// function `f` (2 args) and no merged closures (0), while the sum /// is 3 function args (f=2, foo=1) and 2 closure args. Before the /// fix Display printed `function_args: 2, closure_args: 0`. + #[cfg(feature = "python")] #[test] fn display_headline_matches_sum_for_nested_functions() { check_metrics::( diff --git a/src/metrics/nexits.rs b/src/metrics/nexits.rs index bedfda0dc..3c355a789 100644 --- a/src/metrics/nexits.rs +++ b/src/metrics/nexits.rs @@ -541,6 +541,7 @@ mod tests { assert_eq!(stats.nexits_min(), 0); } + #[cfg(feature = "python")] #[test] fn python_no_exit() { check_metrics::("a = 42", "foo.py", |metric| { @@ -559,6 +560,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn rust_no_exit() { check_metrics::("let a = 42;", "foo.rs", |metric| { @@ -577,6 +579,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn rust_question_mark() { check_metrics::("let _ = a? + b? + c?;", "foo.rs", |metric| { @@ -602,6 +605,7 @@ mod tests { // return type was getting one extra exit on top of its real // `return` / `?` exits. The fix drops the spurious clause; this // test pins exit == 1 for a function with one explicit return. + #[cfg(feature = "rust")] #[test] fn rust_explicit_return_with_return_type() { check_metrics::("fn foo() -> i32 { return 1; }", "foo.rs", |metric| { @@ -623,6 +627,7 @@ mod tests { // Regression for #243: an implicit final-expression return must // NOT count as an exit — matching every other language's // convention (Java, C++, Go, etc. don't count implicit returns). + #[cfg(feature = "rust")] #[test] fn rust_implicit_return_not_counted() { check_metrics::("fn foo() -> i32 { 0 }", "foo.rs", |metric| { @@ -644,6 +649,7 @@ mod tests { // Regression for #243: a function with both an explicit return on // one branch and an implicit final expression should count only // the explicit return. + #[cfg(feature = "rust")] #[test] fn rust_mixed_explicit_and_implicit_return() { check_metrics::( @@ -669,6 +675,7 @@ mod tests { // Regression for #243: `?` inside a function body is the only // implicit-exit form that does count, and the function having an // explicit `Result` return type must not double it. + #[cfg(feature = "rust")] #[test] fn rust_question_mark_in_function() { check_metrics::( @@ -693,6 +700,7 @@ mod tests { // Regression for #243: a unit-returning function with no // explicit `return` or `?` must report 0 exits. + #[cfg(feature = "rust")] #[test] fn rust_unit_return_no_exit() { check_metrics::("fn foo() { let _x = 1; }", "foo.rs", |metric| { @@ -711,6 +719,7 @@ mod tests { }); } + #[cfg(feature = "c")] #[test] fn c_no_exit() { check_metrics::("int a = 42;", "foo.c", |metric| { @@ -731,6 +740,7 @@ mod tests { /// Multiple `return` statements across `if` / `else` branches. Every /// `Cpp::ReturnStatement` adds +1 — there is no early-out collapse. + #[cfg(feature = "c")] #[test] fn c_multiple_returns_in_branches() { check_metrics::( @@ -771,6 +781,7 @@ mod tests { /// function node and two `return`s, so a metric-count assertion alone /// does not distinguish the two grammars — only the error-free parse /// does. C has no `throw`, so `return` is the sole exit kind. + #[cfg(feature = "c")] #[test] fn c_keyword_identifiers_parse_and_returns_count() { use std::path::PathBuf; @@ -797,6 +808,7 @@ mod tests { /// `return` statements inside `try` and `catch` blocks both count; /// the impl matches `Cpp::ReturnStatement` regardless of enclosing /// scope. C++-only: bare C has no `try`/`catch`. + #[cfg(feature = "cpp")] #[test] fn cpp_return_in_try_catch() { check_metrics::( @@ -833,6 +845,7 @@ mod tests { /// Early `return` inside a loop body is counted separately from the /// trailing return — every reachable `return` is an exit. + #[cfg(feature = "c")] #[test] fn c_early_return_in_loop() { check_metrics::( @@ -866,6 +879,7 @@ mod tests { /// `void` function with no explicit `return` — exit count is 0. /// The implicit fall-through return is intentionally not modelled. + #[cfg(feature = "c")] #[test] fn c_void_no_explicit_return() { check_metrics::( @@ -892,6 +906,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_no_exit() { check_metrics::("var a = 42;", "foo.js", |metric| { @@ -910,6 +925,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_simple_function() { check_metrics::( @@ -937,6 +953,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_nested_functions() { check_metrics::( @@ -964,6 +981,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_simple_function() { check_metrics::( @@ -988,6 +1006,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_more_functions() { check_metrics::( @@ -1015,6 +1034,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_nested_functions() { check_metrics::( @@ -1042,6 +1062,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_no_exit() { check_metrics::("int a = 42;", "foo.java", |metric| { @@ -1060,6 +1081,7 @@ mod tests { }); } + #[cfg(feature = "java")] #[test] fn java_simple_function() { check_metrics::( @@ -1086,6 +1108,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_no_return() { check_metrics::( @@ -1112,6 +1135,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_single_return() { check_metrics::( @@ -1136,6 +1160,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_multiple_returns() { check_metrics::( @@ -1167,6 +1192,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_naked_return() { check_metrics::( @@ -1193,6 +1219,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_multivalue_return() { check_metrics::( @@ -1218,6 +1245,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_panic_counts_as_exit() { check_metrics::( @@ -1244,6 +1272,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_panic_and_return_both_count() { check_metrics::( @@ -1272,6 +1301,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_package_qualified_panic_is_not_exit() { check_metrics::( @@ -1299,6 +1329,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_split_function() { check_metrics::( @@ -1328,6 +1359,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_no_exit() { check_metrics::("int a = 42;", "foo.cs", |metric| { @@ -1345,6 +1377,7 @@ mod tests { }); } + #[cfg(feature = "csharp")] #[test] fn csharp_simple_function() { check_metrics::( @@ -1370,6 +1403,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_split_function() { check_metrics::( @@ -1398,6 +1432,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_yield_and_throw() { check_metrics::( @@ -1429,6 +1464,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_no_exit() { check_metrics::( @@ -1452,6 +1488,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_no_function_no_exit() { check_metrics::("my $x = 1;\nprint $x;\n", "foo.pl", |metric| { @@ -1466,6 +1503,7 @@ mod tests { }); } + #[cfg(feature = "perl")] #[test] fn perl_multiple_returns() { check_metrics::( @@ -1498,6 +1536,7 @@ mod tests { /// spelling of each is the same builtin reached past an override /// and keeps its qualifier in the bareword text, so it is matched /// by name alongside the bare one. + #[cfg(feature = "perl")] #[test] fn perl_die_and_exit_are_exits() { check_metrics::( @@ -1524,6 +1563,7 @@ mod tests { /// Carp helpers are library functions rather than builtins, and /// `$obj->die` parses as a `method_invocation` whose callee is a /// plain `identifier` — none of them is an exit. + #[cfg(feature = "perl")] #[test] fn perl_lookalike_call_is_not_exit() { check_metrics::( @@ -1541,6 +1581,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_function_with_returns() { check_metrics::( @@ -1570,6 +1611,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_no_exit() { check_metrics::("const x: number = 42;", "foo.ts", |metric| { @@ -1587,6 +1629,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn typescript_function_with_returns() { check_metrics::( @@ -1613,6 +1656,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_no_exit() { check_metrics::("var a = 42;", "foo.js", |metric| { @@ -1630,6 +1674,7 @@ mod tests { }); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_function_with_returns() { check_metrics::( @@ -1656,6 +1701,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_exit_return_and_throw() { check_metrics::( @@ -1682,6 +1728,7 @@ mod tests { ); } + #[cfg(feature = "lua")] #[test] fn lua_no_exit() { check_metrics::( @@ -1705,6 +1752,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_return() { check_metrics::( @@ -1731,6 +1779,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_error_counts_as_exit() { check_metrics::( @@ -1756,6 +1805,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_os_exit_counts_as_exit() { check_metrics::( @@ -1781,6 +1831,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_error_and_return_both_count() { check_metrics::( @@ -1808,6 +1859,7 @@ end", ); } + #[cfg(feature = "lua")] #[test] fn lua_user_call_is_not_exit() { check_metrics::( @@ -1834,6 +1886,7 @@ end", ); } + #[cfg(feature = "bash")] #[test] fn bash_no_exit() { check_metrics::("echo \"no exits\"", "foo.sh", |metric| { @@ -1851,6 +1904,7 @@ end", }); } + #[cfg(feature = "bash")] #[test] fn bash_explicit_return() { check_metrics::( @@ -1877,6 +1931,7 @@ end", ); } + #[cfg(feature = "bash")] #[test] fn bash_explicit_exit() { check_metrics::( @@ -1900,6 +1955,7 @@ end", ); } + #[cfg(feature = "bash")] #[test] fn bash_multiple_exits() { check_metrics::( @@ -1926,6 +1982,7 @@ end", ); } + #[cfg(feature = "bash")] #[test] fn bash_returnish_names_are_not_exits() { // `returncode=1` is a `variable_assignment`, not a Command. The @@ -1955,6 +2012,7 @@ end", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_no_exit() { check_metrics::( @@ -1978,6 +2036,7 @@ end", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_return() { check_metrics::( @@ -1993,6 +2052,7 @@ end", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_multiple_returns() { check_metrics::( @@ -2015,6 +2075,7 @@ end", /// `error`, the 8.6 `throw`, and `exit` all parse as generic /// commands told apart by their leading word, the same seam /// `return` uses (#1270). + #[cfg(feature = "tcl")] #[test] fn tcl_error_and_throw_are_exits() { check_metrics::( @@ -2046,6 +2107,7 @@ end", /// `::return` *is* `return`, so all four must count; the leading word /// is the only seam these have, and an unstripped qualifier made each /// read as an ordinary call (#1381 review). + #[cfg(feature = "tcl")] #[test] fn tcl_qualified_exits_are_exits() { check_metrics::( @@ -2071,6 +2133,7 @@ end", /// Control for the test above: only the *leading* qualifier resolves /// to the core command, so a proc in `ns` is not an exit. + #[cfg(feature = "tcl")] #[test] fn tcl_namespaced_return_is_not_an_exit() { check_metrics::( @@ -2084,6 +2147,7 @@ end", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_error_in_argument_position_is_not_exit() { check_metrics::( @@ -2099,6 +2163,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_multiple_returns() { check_metrics::( @@ -2119,6 +2184,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_nested_functions() { check_metrics::( @@ -2138,6 +2204,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_no_exit() { check_metrics::( @@ -2153,6 +2220,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_multiple_returns() { check_metrics::( @@ -2173,6 +2241,7 @@ end", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_multiple_returns() { check_metrics::( @@ -2193,6 +2262,7 @@ end", ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_no_exit() { check_metrics::( @@ -2208,6 +2278,7 @@ end", ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_nested_functions() { check_metrics::( @@ -2227,6 +2298,7 @@ end", ); } + #[cfg(feature = "php")] #[test] fn php_no_exit() { check_metrics::("( @@ -2441,6 +2523,7 @@ end", /// `Kernel#exit!`, the immediate process exit. Both count. A bare /// `exit!` counts too, unlike a bare `exit`: the `!` cannot name a /// local, so the grammar emits a `call` rather than an `identifier`. + #[cfg(feature = "ruby")] #[test] fn ruby_kernel_qualified_and_bang_exits_count() { check_metrics::( @@ -2458,6 +2541,7 @@ end", /// the same bare-callee gate Go uses to keep `foo.panic()` out. A /// symbol or hash key spelling the builtin parses as /// `simple_symbol` / `hash_key_symbol` and is likewise not a call. + #[cfg(feature = "ruby")] #[test] fn ruby_receiver_call_is_not_exit() { check_metrics::( @@ -2475,6 +2559,7 @@ end", /// Pinning the exclusion here keeps a future "just match bare /// identifiers too" change from silently counting every variable /// read. + #[cfg(feature = "ruby")] #[test] fn ruby_bare_raise_identifier_is_not_exit() { check_metrics::( @@ -2488,6 +2573,7 @@ end", ); } + #[cfg(feature = "python")] #[test] fn python_return_and_raise() { // `raise` exits the function (stack unwinds) @@ -2516,6 +2602,7 @@ end", ); } + #[cfg(feature = "javascript")] #[test] fn javascript_return_and_throw() { // `throw` is a function exit. @@ -2542,6 +2629,7 @@ end", ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_return_and_throw() { // Same shape as plain JavaScript. @@ -2568,6 +2656,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_return_and_throw() { check_metrics::( @@ -2593,6 +2682,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_return_and_throw() { check_metrics::( @@ -2618,6 +2708,7 @@ end", ); } + #[cfg(feature = "java")] #[test] fn java_return_and_throw() { // `throw` exits the method. @@ -2656,6 +2747,7 @@ end", /// aggregate `nexits_sum` is 2 either way, so checking only the new /// space would pass against the unfixed code as long as the space /// existed at all. + #[cfg(feature = "java")] #[test] fn java_record_compact_constructor_owns_its_exits() { check_func_space::( @@ -2686,6 +2778,7 @@ end", ); } + #[cfg(feature = "java")] #[test] fn java_yield_in_switch_expression() { // Java-14+ switch-expression `yield` is an explicit exit. Each @@ -2706,6 +2799,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_no_exit() { // No functions at all — `nexits.sum` is 0. @@ -2714,6 +2808,7 @@ end", }); } + #[cfg(feature = "groovy")] #[test] fn groovy_simple_function() { // One explicit return in a top-level function. @@ -2728,6 +2823,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_return_and_throw() { check_metrics::( @@ -2744,6 +2840,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_yield_in_switch_expression() { // Groovy inherits Java-14+ switch-expression `yield`. Each @@ -2764,6 +2861,7 @@ end", ); } + #[cfg(feature = "groovy")] #[test] fn groovy_implicit_return_not_counted() { // Groovy allows implicit return of the last expression in a @@ -2775,6 +2873,7 @@ end", }); } + #[cfg(feature = "cpp")] #[test] fn cpp_return_and_throw() { // `throw` exits the function. @@ -2801,6 +2900,7 @@ end", ); } + #[cfg(feature = "python")] #[test] fn python_yield_counts_as_exit() { // Generator suspension via `yield` hands control back to the @@ -2830,6 +2930,7 @@ end", ); } + #[cfg(feature = "javascript")] #[test] fn javascript_yield_counts_as_exit() { // `function*` generator: each `yield` is an exit edge, same as @@ -2858,6 +2959,7 @@ end", ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_yield_counts_as_exit() { // Same shape as plain JavaScript. @@ -2885,6 +2987,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_yield_counts_as_exit() { check_metrics::( @@ -2911,6 +3014,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_yield_counts_as_exit() { check_metrics::( @@ -2937,6 +3041,7 @@ end", ); } + #[cfg(feature = "python")] #[test] fn python_yield_forms_count_as_exit() { // tree-sitter-python emits a single `Python::Yield` node kind for @@ -2966,6 +3071,7 @@ end", ); } + #[cfg(feature = "javascript")] #[test] fn javascript_yield_delegate_counts_as_exit() { // Delegating yield (`yield*`) parses as the same @@ -2996,6 +3102,7 @@ end", ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_yield_delegate_counts_as_exit() { check_metrics::( @@ -3022,6 +3129,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_yield_delegate_counts_as_exit() { check_metrics::( @@ -3048,6 +3156,7 @@ end", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_yield_delegate_counts_as_exit() { check_metrics::( @@ -3076,6 +3185,7 @@ end", /// A handler with no `return` has zero exits (iRules has no `return` /// keyword node; `return` is a generic command matched by name). + #[cfg(feature = "irules")] #[test] fn irules_no_exit() { check_metrics::( @@ -3092,6 +3202,7 @@ end", } /// A `return` command contributes one exit. + #[cfg(feature = "irules")] #[test] fn irules_return() { check_metrics::( @@ -3111,6 +3222,7 @@ end", /// A multi-value `return` (`return [list ...]`) is a single command and /// counts once, not once per returned value. + #[cfg(feature = "irules")] #[test] fn irules_multi_value_return_counts_once() { check_metrics::( @@ -3129,6 +3241,7 @@ end", /// parses to the same `command` + name-word shape (#1270), /// re-derived against the iRules grammar rather than assumed from /// Tcl's. + #[cfg(feature = "irules")] #[test] fn irules_error_is_an_exit() { check_metrics::( @@ -3155,6 +3268,7 @@ end", /// through `tcl_command_name`, so the `::` strip needs pinning on /// that second path too — the sibling sweep grammar-dispatch.md /// requires (#1381 review). + #[cfg(feature = "irules")] #[test] fn irules_qualified_exits_are_exits() { check_metrics::( @@ -3173,6 +3287,7 @@ end", } /// Control: `ns::return` is a proc in `ns`, not the core command. + #[cfg(feature = "irules")] #[test] fn irules_namespaced_return_is_not_an_exit() { check_metrics::( @@ -3187,6 +3302,7 @@ end", ); } + #[cfg(feature = "irules")] #[test] fn irules_throw_and_argument_position_error_are_not_exits() { check_metrics::( @@ -3204,6 +3320,7 @@ end", /// Objective-C method with no `return` and no `@throw` has zero exit /// points. + #[cfg(feature = "objc")] #[test] fn objc_no_exit() { check_metrics::( @@ -3230,6 +3347,7 @@ end", /// Objective-C exit set is `return_statement` + `@throw` /// (`throw_statement`): a method with one of each counts 2. + #[cfg(feature = "objc")] #[test] fn objc_return_and_throw() { check_metrics::( diff --git a/src/metrics/nom.rs b/src/metrics/nom.rs index 99faf8b79..940cd2736 100644 --- a/src/metrics/nom.rs +++ b/src/metrics/nom.rs @@ -310,6 +310,7 @@ mod tests { assert_eq!(stats.closures_min(), 0); } + #[cfg(feature = "python")] #[test] fn python_nom() { check_metrics::( @@ -349,6 +350,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_nom() { check_metrics::( @@ -379,6 +381,7 @@ mod tests { ); } + #[cfg(feature = "c")] #[test] fn c_nom() { check_metrics::( @@ -411,6 +414,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_nom() { check_metrics::( @@ -447,6 +451,7 @@ mod tests { /// `Cpp::FunctionDefinition` and count toward `functions`. Member /// functions are nested inside a struct/class space; the count is on /// the function-definition node itself, not on the enclosing scope. + #[cfg(feature = "cpp")] #[test] fn cpp_free_and_member_functions() { check_metrics::( @@ -469,6 +474,7 @@ mod tests { /// `static` member functions still surface as `Cpp::FunctionDefinition` /// — the `static` keyword is a storage-class specifier, not a separate /// node kind — so they are counted just like non-static members. + #[cfg(feature = "cpp")] #[test] fn cpp_static_member_function() { check_metrics::( @@ -490,6 +496,7 @@ mod tests { /// `Cpp::FunctionDefinition` nodes with a `function_declarator` whose /// identifier is the class name (ctor) or `~ClassName` (dtor). Both /// count as functions. + #[cfg(feature = "cpp")] #[test] fn cpp_constructor_and_destructor() { check_metrics::( @@ -512,6 +519,7 @@ mod tests { /// Operator overloads surface as `FunctionDefinition` whose declarator /// has an `OperatorName` identifier (`operator+`, `operator==`). Both /// inline overloads count toward `functions`. + #[cfg(feature = "cpp")] #[test] fn cpp_operator_overloads() { check_metrics::( @@ -533,6 +541,7 @@ mod tests { /// Function-template definition counts as a single function — the /// `template<>` prefix wraps a `FunctionDefinition` and does not /// produce additional function-definition nodes. + #[cfg(feature = "cpp")] #[test] fn cpp_function_template() { check_metrics::( @@ -551,6 +560,7 @@ mod tests { /// Class-template member functions defined in-line each count as one /// function. The `template<>` head wraps the class, and the methods /// inside it surface as ordinary `FunctionDefinition` nodes. + #[cfg(feature = "cpp")] #[test] fn cpp_class_template_members() { check_metrics::( @@ -574,6 +584,7 @@ mod tests { /// `functions` — Cpp::LambdaExpression is the closure kind. The /// enclosing function adds 1 to `functions`; each lambda adds 1 to /// `closures`. + #[cfg(feature = "cpp")] #[test] fn cpp_lambdas_inside_function() { check_metrics::( @@ -594,6 +605,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_nom() { check_metrics::( @@ -655,6 +667,7 @@ mod tests { /// Uses `metrics_verbatim` rather than the `check_metrics` shim, /// whose bare-`fn` callback cannot capture the case's label or its /// expectation. + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] fn check_js_binding_site_parity(lang: crate::LANG) { let split = |source: &str| { let m = crate::test_support::metrics_verbatim( @@ -785,26 +798,31 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_binding_site_parity() { check_js_binding_site_parity(crate::LANG::Javascript); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_binding_site_parity() { check_js_binding_site_parity(crate::LANG::Mozjs); } + #[cfg(feature = "typescript")] #[test] fn typescript_binding_site_parity() { check_js_binding_site_parity(crate::LANG::Typescript); } + #[cfg(feature = "typescript")] #[test] fn tsx_binding_site_parity() { check_js_binding_site_parity(crate::LANG::Tsx); } + #[cfg(feature = "javascript")] #[test] fn javascript_call_nom() { check_metrics::( @@ -836,6 +854,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_assignment_nom() { check_metrics::( @@ -864,6 +883,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_labeled_nom() { check_metrics::( @@ -894,6 +914,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_labeled_arrow_nom() { check_metrics::( @@ -924,6 +945,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_pair_nom() { check_metrics::( @@ -956,6 +978,7 @@ mod tests { ); } + #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))] fn check_returned_object_arrow_nom(file_name: &str) { check_metrics::( "function f() { return { foo: x => x }; }", @@ -984,26 +1007,31 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_returned_object_arrow_nom() { check_returned_object_arrow_nom::("foo.js"); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_returned_object_arrow_nom() { check_returned_object_arrow_nom::("foo.js"); } + #[cfg(feature = "typescript")] #[test] fn typescript_returned_object_arrow_nom() { check_returned_object_arrow_nom::("foo.ts"); } + #[cfg(feature = "typescript")] #[test] fn tsx_returned_object_arrow_nom() { check_returned_object_arrow_nom::("foo.tsx"); } + #[cfg(feature = "javascript")] #[test] fn javascript_unnamed_nom() { check_metrics::( @@ -1036,6 +1064,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_arrow_nom() { check_metrics::( @@ -1068,6 +1097,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_arrow_assignment_nom() { check_metrics::("sink.onPull = () => { };", "foo.js", |metric| { @@ -1092,6 +1122,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_arrow_new_nom() { check_metrics::( @@ -1120,6 +1151,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_arrow_call_nom() { check_metrics::( @@ -1150,6 +1182,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_nom() { check_metrics::( @@ -1185,6 +1218,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_nom() { check_metrics::( @@ -1228,6 +1262,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_closure_nom() { check_metrics::( @@ -1261,6 +1296,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_indexer_nom() { // A bodied indexer defines two callable accessors (`get`, `set`). @@ -1300,6 +1336,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_expression_bodied_indexer_nom() { // An expression-bodied indexer (`this[int i] => _d[i];`) has NO @@ -1339,6 +1376,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_property_nom() { // A bodied property (`int W { get => _w; set => _w = value; }`) @@ -1361,6 +1399,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_auto_property_nom() { // An auto-property (`int Y { get; set; }`) still has two @@ -1379,6 +1418,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_expression_bodied_property_nom() { // An expression-bodied property (`int W => _w;`) has NO @@ -1402,6 +1442,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_top_level_funcs() { check_metrics::( @@ -1433,6 +1474,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_method_declaration() { check_metrics::( @@ -1463,6 +1505,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_func_literal_is_closure() { check_metrics::( @@ -1492,6 +1535,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_nested_closures() { check_metrics::( @@ -1535,6 +1579,7 @@ mod tests { /// `constructor_declaration` and was always counted, so a fixture with /// only the compact form could not distinguish "counted once" from /// "counted as the other constructor". + #[cfg(feature = "java")] #[test] fn java_record_compact_constructor_counts_as_a_function() { check_metrics::( @@ -1553,6 +1598,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_closure_nom() { check_metrics::( @@ -1598,6 +1644,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_nom() { check_metrics::( @@ -1626,6 +1673,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_nom_function_definition() { // `def foo() {}` at top level uses `function_definition`, not @@ -1642,6 +1690,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_nom() { check_metrics::( @@ -1675,6 +1724,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_named_and_arrow_functions() { check_metrics::( @@ -1706,6 +1756,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_named_arrow_and_class_methods() { check_metrics::( @@ -1741,6 +1792,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_nom() { check_metrics::( @@ -1780,6 +1832,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_arrow_and_method() { check_metrics::( @@ -1812,6 +1865,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_nom_class_with_methods() { check_metrics::( @@ -1846,6 +1900,7 @@ mod tests { ); } + #[cfg(feature = "lua")] #[test] fn lua_nom() { check_metrics::( @@ -1884,6 +1939,7 @@ end", ); } + #[cfg(feature = "bash")] #[test] fn bash_nom() { check_metrics::( @@ -1919,6 +1975,7 @@ bar", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_nom() { check_metrics::( @@ -1935,6 +1992,7 @@ bar 2 3", ); } + #[cfg(feature = "tcl")] #[test] fn tcl_nested_nom() { check_metrics::( @@ -1951,6 +2009,7 @@ bar 2 3", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_class_methods() { check_metrics::( @@ -1967,6 +2026,7 @@ bar 2 3", ); } + #[cfg(feature = "typescript")] #[test] fn typescript_arrow_and_function() { check_metrics::( @@ -1982,6 +2042,7 @@ bar 2 3", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_class_methods() { check_metrics::( @@ -1998,6 +2059,7 @@ bar 2 3", ); } + #[cfg(feature = "typescript")] #[test] fn tsx_arrow_and_function() { check_metrics::( @@ -2013,6 +2075,7 @@ bar 2 3", ); } + #[cfg(feature = "bash")] #[test] fn bash_multiple_functions_nom() { check_metrics::( @@ -2032,6 +2095,7 @@ g() { ); } + #[cfg(feature = "bash")] #[test] fn bash_nested_functions_nom() { check_metrics::( @@ -2051,6 +2115,7 @@ outer() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_nested_function_nom() { check_metrics::( @@ -2069,6 +2134,7 @@ outer() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_class_methods_nom() { check_metrics::( @@ -2085,6 +2151,7 @@ outer() { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_iife_nom() { check_metrics::( @@ -2101,6 +2168,7 @@ outer() { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_class_methods_nom() { check_metrics::( @@ -2117,6 +2185,7 @@ outer() { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_lambda_nom() { check_metrics::( @@ -2133,6 +2202,7 @@ outer() { ); } + #[cfg(feature = "php")] #[test] fn php_nom() { // Top-level function + 2 methods inside a class + 1 anonymous + @@ -2171,6 +2241,7 @@ outer() { ); } + #[cfg(feature = "php")] #[test] fn php_nom_anonymous_class() { // Methods inside `new class { … }` count toward the closure-style @@ -2214,6 +2285,7 @@ outer() { // the two `fn x -> … end` literals count as CLOSURES — the same split // every other language already produced. `functions_sum` was pinned at // 0 before the fix (the bug this test now guards against regressing). + #[cfg(feature = "elixir")] #[test] fn elixir_nom_counts_def_as_functions_and_fn_as_closures() { check_metrics::( @@ -2264,6 +2336,7 @@ outer() { } } + #[cfg(feature = "ruby")] #[test] fn ruby_nom() { // expected: total = 4 (2 methods `add`/`mul` + 1 singleton @@ -2282,6 +2355,7 @@ outer() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_stabby_lambda_single_closure() { // A stabby lambda `->(z) { … }` parses as a `Lambda` node that @@ -2294,6 +2368,7 @@ outer() { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_stabby_lambda_multi_statement_single_closure() { // A multi-statement body does not change the structure: still one @@ -2307,6 +2382,7 @@ outer() { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_stabby_lambda_do_block_single_closure() { // The `do … end` body form of a stabby lambda parses as a `Lambda` @@ -2316,6 +2392,7 @@ outer() { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_keyword_lambda_single_closure() { // The keyword forms `lambda { }` / `proc { }` parse as a `Call` @@ -2336,6 +2413,7 @@ outer() { /// and one proc reports three functions, zero closures. Confirms the /// handlers-as-functions decision end to end. (`try`'s `on` / `trap` /// handlers are branch points, not functions — issue #1266.) + #[cfg(feature = "irules")] #[test] fn irules_nom_handlers_and_procs() { check_metrics::( @@ -2363,6 +2441,7 @@ proc helper { x } { /// one function (issue #1266). Before the fix each handler's dedicated /// `on_handler` / `trap_handler` node was classified as a function /// space, so this fixture reported three. + #[cfg(feature = "irules")] #[test] fn irules_try_handlers_are_not_functions() { check_metrics::( @@ -2389,6 +2468,7 @@ proc helper { x } { /// `block_literal`: the two methods are functions, the block is a /// closure (it does not open its own space), so functions = 2, /// closures = 1, total = 3. + #[cfg(feature = "objc")] #[test] fn objc_nom() { check_metrics::( diff --git a/src/metrics/npa.rs b/src/metrics/npa.rs index 8a98bc03a..cdc93b950 100644 --- a/src/metrics/npa.rs +++ b/src/metrics/npa.rs @@ -625,6 +625,7 @@ mod tests { check_metrics_only_shim!(check_metrics, Npa); check_func_space_only_shim!(check_func_space, Npa); + #[cfg(feature = "java")] #[test] fn java_single_attributes() { check_metrics::( @@ -668,6 +669,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_multiple_attributes() { check_metrics::( @@ -711,6 +713,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_initialized_attributes() { check_metrics::( @@ -754,6 +757,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_array_attributes() { check_metrics::( @@ -797,6 +801,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_object_attributes() { check_metrics::( @@ -836,6 +841,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_no_attributes() { check_metrics::("class A { void foo() {} }", "foo.groovy", |metric| { @@ -844,6 +850,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_public_attributes() { check_metrics::( @@ -861,6 +868,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_def_attributes_not_public() { // `def field` at class scope is a FieldDeclaration whose @@ -880,6 +888,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_interface_attributes() { // Structural `assert_child_space_kind` guards against an @@ -901,6 +910,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_no_attributes_in_unit_scope() { check_metrics::("int x = 1", "foo.groovy", |metric| { @@ -908,6 +918,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_multiple_classes() { check_metrics::( @@ -921,6 +932,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_initialized_attributes() { // Mirror of `java_initialized_attributes`: each @@ -942,6 +954,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_object_attributes() { // Object-typed attributes (boxed primitives, user types, @@ -960,6 +973,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_attribute_modifiers() { // Multiple modifier orderings (public/static/final/transient/ @@ -983,6 +997,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] #[ignore = "dekobon Groovy grammar v1 does not yet support inner classes inside class bodies (https://github.com/dekobon/tree-sitter-groovy SPECIFICATION.md §4 — 'Field declarations, static initialisers, and inner classes land later')"] fn groovy_nested_inner_classes() { @@ -1007,6 +1022,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_array_attributes() { // Array-typed attributes. Mirrors `java_array_attributes`. @@ -1024,6 +1040,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_anonymous_inner_class() { // Object-creation expression containing a `class_body` — @@ -1050,6 +1067,7 @@ mod tests { // annotation handling. Record support in the dekobon Groovy grammar // lags behind groovyc, but the grammar exposes `record_declaration` // and the `Npa` body walker treats it identically. + #[cfg(feature = "groovy")] #[test] fn groovy_enum_counts_explicit_public_fields() { check_metrics::( @@ -1066,6 +1084,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_annotation_type_counts_constants_as_implicit_public() { // The dekobon Groovy grammar parses `@interface` like Java @@ -1089,6 +1108,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_generic_attributes() { check_metrics::( @@ -1126,6 +1146,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_attribute_modifiers() { check_metrics::( @@ -1169,6 +1190,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_classes() { check_metrics::( @@ -1204,6 +1226,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_nested_inner_classes() { check_metrics::( @@ -1238,6 +1261,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_local_inner_classes() { check_metrics::( @@ -1277,6 +1301,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_anonymous_inner_classes() { check_metrics::( @@ -1319,6 +1344,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_interface() { check_metrics::( @@ -1352,6 +1378,7 @@ mod tests { // Regression for issue #280: Java `EnumDeclaration` must be // classified as a class space so `Npa` walks its body and counts // explicit public fields declared after the enum constants. + #[cfg(feature = "java")] #[test] fn java_enum_counts_explicit_public_fields() { check_metrics::( @@ -1376,6 +1403,7 @@ mod tests { // implicit public final fields at the bytecode level but are NOT // counted here, matching the C# precedent (only explicit body // members count). + #[cfg(feature = "java")] #[test] fn java_record_counts_explicit_body_fields() { check_metrics::( @@ -1393,6 +1421,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_annotation_type_counts_constants_as_implicit_public() { // Asserting only `interface_na_sum` / `interface_npa_sum` @@ -1417,6 +1446,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_no_class_attributes() { check_metrics::( @@ -1426,6 +1456,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_single_attributes() { check_metrics::( @@ -1457,6 +1488,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_multiple_attributes() { check_metrics::( @@ -1479,6 +1511,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_initialized_attributes() { check_metrics::( @@ -1499,6 +1532,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_array_attributes() { check_metrics::( @@ -1517,6 +1551,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_object_attributes() { check_metrics::( @@ -1536,6 +1571,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_generic_attributes() { check_metrics::( @@ -1554,6 +1590,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_attribute_modifiers() { check_metrics::( @@ -1578,6 +1615,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_classes() { check_metrics::( @@ -1600,6 +1638,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_nested_inner_classes() { check_metrics::( @@ -1621,6 +1660,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_struct_attributes() { // C#-only: structs declare fields like classes; visibility rule @@ -1641,6 +1681,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_record_attributes() { // C#-only: records can declare body fields just like classes. @@ -1660,6 +1701,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_interface() { // EC14 — interface members default to public; all fields count. @@ -1685,6 +1727,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_interface_explicit_modifiers() { // #780 — C# 8+ permits explicit `private`/`protected` on interface @@ -1710,6 +1753,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_interface_multi_declarator_modifier() { // The visibility modifier applies to every declarator of a field, so @@ -1731,6 +1775,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_one_public_attribute() { check_metrics::( @@ -1740,6 +1785,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_one_private_attribute() { check_metrics::( @@ -1749,6 +1795,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_one_protected_attribute() { check_metrics::( @@ -1758,6 +1805,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_mixed_visibility_attributes() { check_metrics::( @@ -1773,6 +1821,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_static_public_attribute() { check_metrics::( @@ -1782,6 +1831,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_readonly_public_attribute() { check_metrics::( @@ -1791,6 +1841,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_multiple_attributes_per_declaration() { // A single property_declaration can declare several @@ -1802,6 +1853,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_interface_constants() { // Interface constants are implicitly public. @@ -1816,6 +1868,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_enum_cases_not_counted() { // #781: enum cases are sum-type tags, not data fields, so they @@ -1839,6 +1892,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_enum_const_not_counted() { // #781: a PHP enum body may declare `const`s alongside its @@ -1861,6 +1915,7 @@ mod tests { ); } + #[cfg(all(feature = "java", feature = "php"))] #[test] fn php_enum_npa_matches_java_enum_npa() { // #781 cross-language parity: an enum whose only members are @@ -1893,6 +1948,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_trait_attributes() { check_metrics::( @@ -1906,6 +1962,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_no_explicit_visibility_excluded() { // PHP 8.x deprecates implicit-public for properties; we follow @@ -1922,6 +1979,7 @@ mod tests { }); } + #[cfg(feature = "php")] #[test] fn php_anonymous_class_attributes() { // Anonymous classes have their own DeclarationList space and @@ -1939,6 +1997,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_property_promotion_excluded() { // Constructor property promotion (PHP 8.0+) declares both a @@ -1967,6 +2026,7 @@ mod tests { // enclosing class. Top-level properties belong to the `Unit` space // and are excluded. + #[cfg(feature = "kotlin")] #[test] fn kotlin_empty_class_no_attributes() { check_metrics::("class C {}", "foo.kt", |metric| { @@ -1977,6 +2037,7 @@ mod tests { }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_public_val_var_default() { // Kotlin's default visibility is public — no modifier means public. @@ -1995,6 +2056,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_private_val_var() { // Private properties contribute to total `na` but not to `npa`. @@ -2014,6 +2076,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_protected_internal_excluded_from_public() { check_metrics::( @@ -2031,6 +2094,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_primary_constructor_parameter_property() { // `val`/`var` on primary constructor parameters declares both a @@ -2050,6 +2114,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_primary_constructor_private_param_property() { check_metrics::( @@ -2063,6 +2128,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_secondary_constructor_does_not_add_attrs() { // Secondary constructors are methods, not attribute declarations. @@ -2080,6 +2146,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_companion_object_attributes() { // Companion-object properties fold into the enclosing class as @@ -2103,6 +2170,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_data_class_attributes() { // `data class` parameters are the canonical positional attributes. @@ -2117,6 +2185,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_object_singleton_attributes() { check_metrics::( @@ -2135,6 +2204,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_interface_attributes() { // Interface members are implicitly public; all properties count @@ -2159,6 +2229,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_nested_class_attributes() { // Each class space has its own attribute count; nested class @@ -2181,6 +2252,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_inner_class_attributes() { check_metrics::( @@ -2199,6 +2271,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_top_level_properties_excluded() { // Top-level `val` belongs to `Unit`, not a class — must not @@ -2216,6 +2289,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_multiple_classes_attributes() { check_metrics::( @@ -2237,6 +2311,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_class_with_methods_no_attrs() { // Methods are not attributes. @@ -2265,6 +2340,7 @@ mod tests { // attributes. Interface property signatures count as implicitly // public attributes. + #[cfg(feature = "typescript")] #[test] fn typescript_empty_class_no_attributes() { check_metrics::("class C {}", "foo.ts", |metric| { @@ -2274,6 +2350,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn typescript_default_public_fields() { // No accessibility modifier means public. @@ -2292,6 +2369,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_visibility_modifiers() { // Public / private / protected. Default public. @@ -2312,6 +2390,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_static_fields() { // `static` is orthogonal to visibility — the field still counts. @@ -2331,6 +2410,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_parameter_properties() { // Constructor parameter properties are class attributes. @@ -2349,6 +2429,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_readonly_constructor_param_property() { // A bare `readonly` constructor parameter is a public parameter @@ -2373,6 +2454,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_readonly_field() { // `readonly` is a non-visibility modifier — the field still counts @@ -2391,6 +2473,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_abstract_class_attributes() { // `abstract_class_declaration` opens its own class space; fields @@ -2412,6 +2495,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_arrow_field_is_method_not_attribute() { // A field whose initializer is an arrow function is counted by @@ -2430,6 +2514,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_interface_property_signatures() { // Interface property signatures count as implicitly-public @@ -2455,6 +2540,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_generic_class_attributes() { // Type parameters on the class do not contribute attributes. @@ -2473,6 +2559,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_getters_setters_not_attributes() { // `get x()` / `set x(v)` are method_definitions, not attributes. @@ -2492,6 +2579,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_multiple_classes_and_interface() { check_func_space::( @@ -2515,6 +2603,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_nested_class_attributes_independent() { // Each class space tracks its own attributes; the outer class's @@ -2544,6 +2633,7 @@ mod tests { // TSX parity tests — mirror the TS rules to confirm the shared helper // expansion behaves identically on the TSX grammar. + #[cfg(feature = "typescript")] #[test] fn tsx_empty_class_no_attributes() { check_metrics::("class C {}", "foo.tsx", |metric| { @@ -2553,6 +2643,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn tsx_default_public_fields() { check_metrics::( @@ -2569,6 +2660,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_visibility_modifiers() { check_metrics::( @@ -2586,6 +2678,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_parameter_properties() { check_metrics::( @@ -2601,6 +2694,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_readonly_constructor_param_property() { // TSX sibling of `typescript_readonly_constructor_param_property` @@ -2620,6 +2714,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_abstract_class_attributes() { check_metrics::( @@ -2637,6 +2732,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_interface_property_signatures() { check_func_space::( @@ -2656,6 +2752,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_arrow_field_is_method_not_attribute() { check_metrics::( @@ -2672,6 +2769,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_static_fields() { check_metrics::( @@ -2688,6 +2786,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_readonly_field() { check_metrics::( @@ -2704,6 +2803,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_generic_class_attributes() { check_metrics::("class Box { value: T; }", "foo.tsx", |metric| { @@ -2713,6 +2813,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn tsx_getters_setters_not_attributes() { check_metrics::( @@ -2730,6 +2831,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_multiple_classes_and_interface() { check_func_space::( @@ -2760,6 +2862,7 @@ mod tests { // introduce attributes. Visibility flows from keyword markers as // in `Npm`. + #[cfg(feature = "ruby")] #[test] fn ruby_no_class_attributes() { check_metrics::( @@ -2773,6 +2876,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_instance_variable_attribute() { // Bare `@x = …` at class scope is one public attribute. @@ -2783,6 +2887,7 @@ mod tests { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_class_variable_attribute() { // `@@y = …` at class scope is one attribute. @@ -2793,6 +2898,7 @@ mod tests { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_attr_accessor_counts_symbols() { // `attr_accessor :x, :y, :z` declares three attributes. @@ -2807,6 +2913,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_attr_reader_and_writer() { check_metrics::( @@ -2820,6 +2927,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_mixed_attributes_and_assignments() { check_metrics::( @@ -2833,6 +2941,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_private_attributes() { // Bare `private` flips visibility for the subsequent attr. @@ -2847,6 +2956,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_visibility_public_resets_private() { // `private` then `public` returns to default-public. @@ -2861,6 +2971,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_method_scope_assignments_excluded() { // `@x = 1` inside a method does NOT count — it's a method-local @@ -2877,6 +2988,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_module_attributes_not_counted() { // `module M` is a `Namespace` space — its attr_* macros and @@ -2892,6 +3004,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_inheritance_attributes() { // Inheritance does not change the attribute count for this class. @@ -2906,6 +3019,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_constant_assignments_excluded() { // `CONST = …` at class scope binds a constant, not an @@ -2923,6 +3037,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_multiple_classes_attribute_rollup() { check_metrics::( @@ -2937,6 +3052,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_visibility_call_wrapping_attr_macro_counts_symbols() { // `private attr_accessor :b` nests the `attr_accessor` call @@ -2956,6 +3072,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_wrapped_attr_macro_reads_the_keyword_not_the_flag() { // Seeds the body-wide flag to `private` first, so the assertion @@ -2976,6 +3093,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_parenthesised_bare_keyword_flips_the_attribute_flag() { // `private()` is the explicit-parens spelling of the bare @@ -2996,6 +3114,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_attr_macro_counts_symbol_array_elements() { // `attr_writer %i[e f]` passes one argument naming two @@ -3014,6 +3133,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_attr_macro_on_another_object_declares_nothing() { // `Other.attr_accessor :b` adds an attribute to `Other`, not @@ -3037,6 +3157,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_hash_key_symbol_declares_no_attribute() { // Pins the defensive `HashKeySymbol` arm in @@ -3062,6 +3183,7 @@ mod tests { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_hidden_call_alias_is_not_emitted() { // The Ruby `Npm` / `Npa` walkers dispatch on @@ -3083,6 +3205,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_visibility_call_without_a_wrapped_macro_declares_nothing() { // The visibility-call arm must not invent attributes out of the @@ -3116,6 +3239,7 @@ mod tests { // --- Python NPA --------------------------------------------------- + #[cfg(feature = "python")] #[test] fn python_empty_class_no_attributes() { check_metrics::("class C:\n pass\n", "foo.py", |metric| { @@ -3126,6 +3250,7 @@ mod tests { }); } + #[cfg(feature = "python")] #[test] fn python_class_level_assignments_are_attributes() { // Two class-level `=` assignments → 2 attributes, all public @@ -3137,6 +3262,7 @@ mod tests { }); } + #[cfg(feature = "python")] #[test] fn python_bare_type_annotation_not_attribute() { // `x: int` is a bare annotation (declares a type, binds @@ -3151,6 +3277,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_self_attributes_in_init() { // `self.x` and `self.y` assigned in `__init__` → 2 instance @@ -3166,6 +3293,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_self_attributes_in_nested_control_flow() { // `self.z = 1` and `self.z = 2` in if/else now count once — @@ -3191,6 +3319,7 @@ mod tests { /// (rather than identifier-name dedup) would NOT collapse them. /// This pins the rule to the attribute *name*, not the /// assignment text. + #[cfg(feature = "python")] #[test] fn python_defensive_reinit_self_attribute_counts_once() { check_metrics::( @@ -3206,6 +3335,7 @@ mod tests { /// Distinct attribute names still accumulate normally — the /// dedup is per-name, not per-method. + #[cfg(feature = "python")] #[test] fn python_distinct_self_attributes_count_independently() { check_metrics::( @@ -3224,6 +3354,7 @@ mod tests { /// The dedup helper must see both forms and treat them as the /// same attribute. Regression guard for the review finding on /// #215: ensure annotated assignments aren't missed. + #[cfg(feature = "python")] #[test] fn python_self_attribute_annotated_assignment_dedupes() { check_metrics::( @@ -3236,6 +3367,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_class_level_and_self_attrs_combine() { // 1 class-level + 2 instance = 3 total attributes. @@ -3249,6 +3381,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_self_attrs_isolated_per_class() { // Nested class `Inner` opens its own class space; its @@ -3271,6 +3404,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_decorated_methods_do_not_inflate_attrs() { // `@property` / `@staticmethod` wrap a `FunctionDefinition` in @@ -3292,6 +3426,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_module_level_assignments_not_attributes() { // `x = 1` at module scope is not a class attribute. @@ -3307,6 +3442,7 @@ mod tests { /// the class. Only `self.name` — whose receiver is the `self` alias /// — counts. The prior structural-only check treated every /// `obj.x = …` as an instance attribute, reporting 3. + #[cfg(feature = "python")] #[test] fn python_foreign_object_writes_not_attributes() { check_metrics::( @@ -3329,6 +3465,7 @@ mod tests { /// `self.a, self.b = 1, 2` is a `pattern_list`, not a single /// `Attribute`; the prior code bailed on non-Attribute targets and /// missed both `a` and `b`, reporting 1 (only `self.c`). + #[cfg(feature = "python")] #[test] fn python_self_attribute_unpacking_counts_each() { check_metrics::( @@ -3351,6 +3488,7 @@ mod tests { /// shared `python_walk_target_elements` recursion descends into the /// nested pattern so `b` and `c` are counted, not just `a` (review /// follow-up to #412 (b); a flat iteration reports 1). + #[cfg(feature = "python")] #[test] fn python_self_attribute_nested_unpacking_counts_each() { check_metrics::( @@ -3370,6 +3508,7 @@ mod tests { /// `tuple_pattern` inside the target. Each bound name — including the /// nested `b` and `c` — contributes one attribute (review follow-up to /// #412 (c); a flat iteration reports 1). + #[cfg(feature = "python")] #[test] fn python_class_level_nested_unpacking_counts_each() { check_metrics::( @@ -3391,6 +3530,7 @@ mod tests { /// unparenthesized `p, q = …` form uses. Matching only the hidden /// supertype aliases (168 / 167) dropped these entirely; both bound /// names must be counted (#419 hidden-alias discipline). + #[cfg(feature = "python")] #[test] fn python_class_level_parenthesized_unpacking_counts_each() { check_metrics::( @@ -3409,6 +3549,7 @@ mod tests { /// #412 (b) edge: unpacking that mixes a self attribute with a /// foreign / local target (`self.a, x = …`) counts only the self /// attribute. + #[cfg(feature = "python")] #[test] fn python_self_attribute_unpacking_skips_non_self_targets() { check_metrics::( @@ -3428,6 +3569,7 @@ mod tests { /// attribute per name. `a = b = 3` (chained) binds two; `p, q = 1, /// 2` (unpacking) binds two; with `x = 1` that is five names. The /// prior code counted one per `=` statement, reporting 3. + #[cfg(feature = "python")] #[test] fn python_class_level_multi_target_counts_each_name() { check_metrics::( @@ -3445,6 +3587,7 @@ mod tests { /// #412 (b)/(c): a chained instance assignment `self.a = self.b = 1` /// binds both `a` and `b` on `self`. The nested `Assignment` in the /// value is visited by the subtree walk, so both are counted. + #[cfg(feature = "python")] #[test] fn python_chained_self_assignment_counts_each() { check_metrics::( @@ -3461,6 +3604,7 @@ mod tests { /// #412 (a): a classmethod binds class attributes through the `cls` /// alias; `cls.registry = …` counts, while a foreign `other.thing = /// …` write in the same body does not. + #[cfg(feature = "python")] #[test] fn python_classmethod_cls_attribute_counts() { check_metrics::( @@ -3483,6 +3627,7 @@ mod tests { /// on `self.f`; it does NOT introduce a new attribute of the class. /// The receiver of the outer Attribute is itself an Attribute /// (`self.f`), not the `self` Identifier, so it is rejected. + #[cfg(feature = "python")] #[test] fn python_nested_self_attribute_not_counted() { check_metrics::( @@ -3499,6 +3644,7 @@ mod tests { /// `self.x = 2` name the same attribute; the instance binding /// shadows the class default, so `x` counts once. The class-level /// and instance passes share one dedup set. + #[cfg(feature = "python")] #[test] fn python_class_default_and_self_attr_dedupe() { check_metrics::( @@ -3512,6 +3658,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_empty_unit_no_attributes() { check_metrics::("", "empty.rs", |metric| { @@ -3523,6 +3670,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn rust_struct_fields_are_attributes() { // 3 named fields → class_na = 3. `pub a` and `pub c` are public @@ -3538,6 +3686,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_pub_self_field_is_private() { // Regression for #460. A `pub(self)` / `pub(in self)` field @@ -3565,6 +3714,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_pub_self_assoc_const_is_private() { // Regression for #460 on the associated-const path. `pub(self)` @@ -3587,6 +3737,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_pub_self_tuple_field_is_private() { // Regression for #460 on the tuple-struct positional path. @@ -3606,6 +3757,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_tuple_struct_fields_are_attributes() { // Tuple-struct field counting is positional. `Bar(pub i32, @@ -3617,6 +3769,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn rust_unit_struct_has_no_attributes() { // `struct Empty;` is a unit struct (no fields). 0 attributes. @@ -3626,6 +3779,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn rust_empty_struct_body_has_no_attributes() { // `struct Empty {}` is named-field with zero fields. @@ -3635,6 +3789,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn rust_impl_associated_consts_are_attributes() { // `const X` and `pub const Y` and `static Z` and `pub static W` @@ -3658,6 +3813,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_trait_consts_and_associated_types_are_attributes() { // `const DEFAULT_COLOR` + `type Item` → 2 interface attributes, @@ -3678,6 +3834,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_multiple_impls_aggregate() { // Two `impl Foo` blocks each have one associated const. The @@ -3695,6 +3852,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_module_level_consts_not_attributes() { // `const PI: f64 = 3.14;` at file scope is a free-standing @@ -3713,6 +3871,7 @@ mod tests { // ----- Go ----- + #[cfg(feature = "go")] #[test] fn go_empty_unit_no_attributes() { // Package-only file declares no struct → npa stays disabled, @@ -3723,6 +3882,7 @@ mod tests { }); } + #[cfg(feature = "go")] #[test] fn go_empty_struct_has_no_attributes() { // `type Empty struct{}` has an empty FieldDeclarationList → @@ -3733,6 +3893,7 @@ mod tests { }); } + #[cfg(feature = "go")] #[test] fn go_struct_fields_are_attributes() { // Three named fields: `X int`, `y string`, `Z float64` → 3 @@ -3749,6 +3910,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_grouped_struct_fields_each_count() { // `X, Y int` declares two field names in one @@ -3766,6 +3928,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_embedded_type_counts_as_attribute() { // `io.Reader` and `*Foo` are embedded types — field @@ -3785,6 +3948,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_multiple_structs_aggregate_at_unit() { // Two structs declared at file scope each contribute their @@ -3803,6 +3967,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_top_level_var_const_not_attributes() { // Package-level `var` and `const` declarations are NOT @@ -3818,6 +3983,7 @@ mod tests { ); } + #[cfg(feature = "go")] #[test] fn go_npa_excludes_unexported() { // Issue #458: mixed exported / unexported fields exercising a @@ -3847,6 +4013,7 @@ mod tests { // Issue #275: `defstruct` is Elixir's closest analog to a class // field-set declaration. We count its field arguments as // (public) attributes. + #[cfg(feature = "elixir")] #[test] fn elixir_npa_defstruct_keyword_list() { check_metrics::( @@ -3860,6 +4027,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_npa_defstruct_atom_list() { check_metrics::( @@ -3872,6 +4040,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_npa_defstruct_bracketed_keyword_list() { check_metrics::( @@ -3884,6 +4053,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_npa_defstruct_single_field() { check_metrics::( @@ -3896,6 +4066,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_npa_no_defstruct_is_zero() { check_metrics::( @@ -3917,6 +4088,7 @@ mod tests { /// `is_func_space_with_code` gate that used to precede the /// `defmodule` keyword check could not change the outcome, because /// `elixir_is_class_macro` is exactly `defmodule`. + #[cfg(feature = "elixir")] #[test] fn elixir_npa_counts_a_quoted_defmodule_as_a_class() { check_metrics::( @@ -3933,6 +4105,7 @@ mod tests { // ----- Objective-C ----- + #[cfg(feature = "objc")] #[test] fn objc_npa() { // `@property` is always a public attribute. Instance variables @@ -3968,6 +4141,7 @@ mod tests { ); } + #[cfg(feature = "objc")] #[test] fn objc_npa_protocol() { // A `@protocol`'s `@property` after an `@optional` / `@required` @@ -3989,6 +4163,7 @@ mod tests { // ----- C++ ----- + #[cfg(feature = "cpp")] #[test] fn cpp_empty_unit_no_attributes() { // No code → no class spaces → npa = 0. Establishes the trait @@ -4000,6 +4175,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_empty_class_no_attributes() { // `class Foo {};` has no fields. Marked as class space (npa @@ -4011,6 +4187,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_class_public_attributes() { // `class` defaults to private. `public:` flips visibility → @@ -4028,6 +4205,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_class_private_default_visibility() { // No access specifier → `class` keeps its default private @@ -4040,6 +4218,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_struct_default_public_visibility() { // `struct` defaults to public — opposite of `class`. The same @@ -4051,6 +4230,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_mixed_visibility_sections() { // Public section: 1 field. Protected section (bucketed with @@ -4071,6 +4251,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_methods_not_counted_as_attributes() { // Inline-defined methods (`function_definition`) and @@ -4094,6 +4275,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_pointer_array_fields_count() { // `int* p;` wraps the `field_identifier` inside @@ -4116,6 +4298,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_multiple_classes_aggregate_at_unit() { // Two classes in one file. Each contributes to its own @@ -4134,6 +4317,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_empty_unit_no_attributes() { // Wires up the trait and ensures no spurious attribute counts @@ -4145,6 +4329,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_empty_class_no_attributes() { // A class with no body and no fields has zero attributes. @@ -4155,6 +4340,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_class_fields_count() { // ES2022 class fields: `class Foo { x = 1; y; static z = 2; }`. @@ -4172,6 +4358,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_arrow_field_is_method_not_attribute() { // `class Foo { x = () => {} }` declares a method, not an @@ -4189,6 +4376,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_methods_not_counted_as_attributes() { // `method_definition` direct children of `class_body` are @@ -4205,6 +4393,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_multiple_classes_aggregate_at_unit() { // Two classes contribute their attribute counts to the @@ -4221,6 +4410,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_class_fields_count() { // Mozjs shares JS's class vocabulary. Same expectation as the @@ -4243,6 +4433,12 @@ mod tests { // `!is_nan()` proves the guard fires; the `== 0.0` checks pin the // chosen convention. Exercised across the explicit-visibility OO // languages (Java, C#, Kotlin, PHP). + #[cfg(all( + feature = "csharp", + feature = "java", + feature = "kotlin", + feature = "php" + ))] #[test] fn empty_class_cda_is_zero_not_nan() { let assert_zero = |metric: crate::CodeMetrics| { @@ -4262,6 +4458,7 @@ mod tests { // existing all-public guard explicitly excludes the empty case // (`!= 0`), so without the divisor guard `interface_cda` returned // 0.0 / 0.0 = NaN. The defined value is 0.0. + #[cfg(all(feature = "csharp", feature = "java"))] #[test] fn empty_interface_cda_is_zero_not_nan() { let assert_zero = |metric: crate::CodeMetrics| { @@ -4276,6 +4473,7 @@ mod tests { // Rounds out `npa`'s public surface — the `Display` impl and the // per-space `class_npa` / `class_na` / `interface_*` accessors — // mirroring the `Display` tests the sibling metrics carry. + #[cfg(feature = "java")] #[test] fn stats_display_and_per_space_accessors() { check_func_space::( diff --git a/src/metrics/npm.rs b/src/metrics/npm.rs index 7a630968f..608505e0b 100644 --- a/src/metrics/npm.rs +++ b/src/metrics/npm.rs @@ -573,6 +573,7 @@ mod tests { // operators read as absent from both, so both must be pinned. check_metrics_only_shim!(check_metrics_with_npa, Npm, Npa); + #[cfg(feature = "java")] #[test] fn java_constructors() { check_metrics::( @@ -604,6 +605,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_no_methods() { check_metrics::("class A { int x = 1 }", "foo.groovy", |metric| { @@ -611,6 +613,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_public_methods() { check_metrics::( @@ -627,6 +630,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_interface_methods_implicitly_public() { // Asserting only the body-walker `interface_*_sum` totals @@ -653,6 +657,7 @@ mod tests { // Regression for issue #280: Groovy mirrors Java's enum / record / // annotation method counting. + #[cfg(feature = "groovy")] #[test] fn groovy_enum_counts_methods() { check_metrics::( @@ -669,6 +674,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] #[ignore = "dekobon Groovy grammar v1 does not support annotation type elements with `default` values; the trailing `default \"\"`/`default 0` make the body fail to parse"] fn groovy_annotation_type_counts_elements() { @@ -696,6 +702,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_constructors() { check_metrics::( @@ -714,6 +721,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_no_methods_in_unit_scope() { check_metrics::("int x = 1", "foo.groovy", |metric| { @@ -721,6 +729,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_multiple_classes_methods() { check_metrics::( @@ -734,6 +743,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_methods_returning_primitive_types() { // Mirror of `java_methods_returning_primitive_types`. Each @@ -757,6 +767,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_methods_with_generic_types() { // Methods with generic parameter/return types. @@ -774,6 +785,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_method_modifiers() { // Modifier ordering doesn't matter — what matters is @@ -799,6 +811,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] #[ignore = "dekobon Groovy grammar v1 does not yet support inner classes inside class bodies"] fn groovy_nested_inner_classes() { @@ -823,6 +836,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] #[ignore = "dekobon Groovy grammar v1 does not yet support anonymous inner classes (`new T() { … }`)"] fn groovy_anonymous_inner_class() { @@ -845,6 +859,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_interfaces_and_class() { // Mixed interfaces + class. Interface methods are @@ -884,6 +899,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_methods_returning_primitive_types() { check_metrics::( @@ -927,6 +943,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_methods_returning_arrays() { check_metrics::( @@ -970,6 +987,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_methods_returning_objects() { check_metrics::( @@ -1009,6 +1027,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_methods_with_generic_types() { check_metrics::( @@ -1046,6 +1065,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_method_modifiers() { check_metrics::( @@ -1085,6 +1105,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_classes() { check_metrics::( @@ -1120,6 +1141,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_nested_inner_classes() { check_metrics::( @@ -1154,6 +1176,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_local_inner_classes() { check_metrics::( @@ -1190,6 +1213,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_anonymous_inner_classes() { check_metrics::( @@ -1233,6 +1257,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_interface() { check_metrics::( @@ -1266,6 +1291,7 @@ mod tests { // Regression for issue #280: Java enum bodies hold methods after // the constants. The Npm body walker recognises // `EnumBodyDeclarations` and treats it like `ClassBody`. + #[cfg(feature = "java")] #[test] fn java_enum_counts_methods() { check_metrics::( @@ -1284,6 +1310,7 @@ mod tests { // Regression for issue #280: Java records can declare methods in // their explicit body; they share `ClassBody`'s walker. + #[cfg(feature = "java")] #[test] fn java_record_counts_methods() { check_metrics::( @@ -1313,6 +1340,7 @@ mod tests { /// `half` is the control that keeps the two sums apart — without a /// non-public member, `class_nm_sum == class_npm_sum` and a bug that /// counted every member as public would still pass. + #[cfg(feature = "java")] #[test] fn java_record_counts_a_compact_constructor_as_a_method() { check_metrics::( @@ -1333,6 +1361,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_annotation_type_counts_elements() { // Asserting only the body-walker counts (`interface_nm_sum`, @@ -1358,6 +1387,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_interfaces_and_class() { check_metrics::( @@ -1400,6 +1430,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_constructors() { check_metrics::( @@ -1413,6 +1444,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_methods_returning_primitive_types() { check_metrics::( @@ -1427,6 +1459,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_methods_returning_arrays() { check_metrics::( @@ -1440,6 +1473,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_methods_returning_objects() { check_metrics::( @@ -1454,6 +1488,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_methods_with_generic_types() { check_metrics::( @@ -1467,6 +1502,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_method_modifiers() { check_metrics::( @@ -1483,6 +1519,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_classes() { check_metrics::( @@ -1500,6 +1537,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_nested_inner_classes() { check_metrics::( @@ -1516,6 +1554,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_property_accessors() { // EC7 — each property accessor (get/set/init) counts as a method. @@ -1536,6 +1575,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_narrowed_accessor_visibility() { // #783 — a C# accessor inherits the member's visibility unless it @@ -1572,6 +1612,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_local_functions() { // Local functions inside a method body are nested function spaces; @@ -1597,6 +1638,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_interface() { // EC14 — interface methods default to public. @@ -1611,6 +1653,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_interfaces_and_class() { check_metrics::( @@ -1625,6 +1668,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_no_class_methods() { check_metrics::( @@ -1634,6 +1678,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_one_public_method() { check_metrics::( @@ -1643,6 +1688,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_one_private_method() { check_metrics::( @@ -1652,6 +1698,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_one_protected_method() { check_metrics::( @@ -1661,6 +1708,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_mixed_visibility_methods() { check_metrics::( @@ -1676,6 +1724,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_static_public_method() { check_metrics::( @@ -1685,6 +1734,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_abstract_method() { check_metrics::( @@ -1694,6 +1744,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_final_public_method() { check_metrics::( @@ -1703,6 +1754,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_interface_methods() { // Interface methods are implicitly public. @@ -1717,6 +1769,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_enum_methods() { // Enum can declare public methods (PHP 8.1+). @@ -1737,6 +1790,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_trait_methods() { check_metrics::( @@ -1750,6 +1804,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_no_explicit_visibility_method_excluded() { // Methods without explicit visibility (which PHP treats as public) @@ -1763,6 +1818,7 @@ mod tests { // --- Kotlin NPM tests ------------------------------------------------- + #[cfg(feature = "kotlin")] #[test] fn kotlin_empty_class_no_methods() { check_metrics::("class C {}", "foo.kt", |metric| { @@ -1773,6 +1829,7 @@ mod tests { }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_public_methods_default() { // Kotlin default visibility is public — no modifier means public. @@ -1791,6 +1848,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_private_method() { check_metrics::( @@ -1808,6 +1866,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_protected_internal_methods() { check_metrics::( @@ -1825,6 +1884,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_secondary_constructor_counts() { // Secondary constructors are explicit `secondary_constructor` @@ -1845,6 +1905,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_companion_object_methods() { // Companion object methods fold into the enclosing class (static @@ -1866,6 +1927,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_data_class_methods() { // `data class` compiler-generated members are NOT counted — @@ -1884,6 +1946,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_object_singleton_methods() { check_metrics::( @@ -1900,6 +1963,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_interface_methods() { check_func_space::( @@ -1919,6 +1983,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_interface_with_default_method() { check_func_space::( @@ -1939,6 +2004,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_override_fun_counts() { check_metrics::( @@ -1961,6 +2027,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_nested_class_methods() { check_metrics::( @@ -1980,6 +2047,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_inner_class_methods() { check_metrics::( @@ -1998,6 +2066,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_top_level_function_excluded() { // Top-level `fun` belongs to `Unit`, not any class. @@ -2015,6 +2084,7 @@ class C { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_extension_function_excluded() { // Extension functions parse as top-level `function_declaration` @@ -2033,6 +2103,7 @@ class C { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_class_in_interface() { // Interface with nested class — methods count to the right @@ -2063,6 +2134,7 @@ class C { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_interface_in_class() { // Class with nested interface — methods count to the right @@ -2093,6 +2165,7 @@ class C { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_init_block_not_a_method() { // `init` blocks are anonymous initializers — they are not @@ -2126,6 +2199,7 @@ class C { // `construct_signature`) count as implicitly-public interface // methods. + #[cfg(feature = "typescript")] #[test] fn typescript_empty_class_no_methods() { check_metrics::("class C {}", "foo.ts", |metric| { @@ -2135,6 +2209,7 @@ class C { }); } + #[cfg(feature = "typescript")] #[test] fn typescript_default_public_methods() { check_metrics::( @@ -2152,6 +2227,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_method_visibility() { check_metrics::( @@ -2171,6 +2247,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_static_methods() { check_metrics::( @@ -2189,6 +2266,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_constructor_counts_as_method() { // The constructor is a `method_definition` — one method. @@ -2206,6 +2284,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_getter_setter_each_count_once() { // `get x()` and `set x(v)` are distinct `method_definition` @@ -2225,6 +2304,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_arrow_field_counts_as_method() { // `foo = () => {}` is a class method. @@ -2244,6 +2324,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_method_overload_counts_once() { // Only the implementation `method_definition` counts; the two @@ -2263,6 +2344,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_abstract_class_methods() { // Abstract method signatures count; concrete methods count; both @@ -2286,6 +2368,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_interface_methods() { // Interface method signatures are implicitly public. @@ -2307,6 +2390,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_generic_class_methods() { check_metrics::( @@ -2324,6 +2408,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_multiple_classes_and_interface() { check_func_space::( @@ -2347,6 +2432,7 @@ class C { // TSX parity + #[cfg(feature = "typescript")] #[test] fn tsx_empty_class_no_methods() { check_metrics::("class C {}", "foo.tsx", |metric| { @@ -2356,6 +2442,7 @@ class C { }); } + #[cfg(feature = "typescript")] #[test] fn tsx_default_public_methods() { check_metrics::( @@ -2372,6 +2459,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_method_visibility() { check_metrics::( @@ -2389,6 +2477,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_static_methods() { check_metrics::( @@ -2405,6 +2494,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_constructor_counts_as_method() { check_metrics::( @@ -2421,6 +2511,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_getter_setter_each_count_once() { check_metrics::( @@ -2438,6 +2529,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_arrow_field_counts_as_method() { check_metrics::( @@ -2454,6 +2546,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_method_overload_counts_once() { check_metrics::( @@ -2471,6 +2564,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_abstract_class_methods() { check_metrics::( @@ -2489,6 +2583,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_interface_methods() { check_func_space::( @@ -2507,6 +2602,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_generic_class_methods() { check_metrics::( @@ -2520,6 +2616,7 @@ class C { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_multiple_classes_and_interface() { check_func_space::( @@ -2549,6 +2646,7 @@ class C { // The argument-form (`private :foo`, `private def x`) is a `call` // node and does NOT change the body-wide flag. + #[cfg(feature = "ruby")] #[test] fn ruby_no_class_methods() { check_metrics::("def foo\n 1\nend\n", "foo.rb", |metric| { @@ -2558,6 +2656,7 @@ class C { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_one_public_method() { // No visibility keyword → default public. @@ -2572,6 +2671,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_one_private_method() { // Bare `private` flips visibility for `f`. @@ -2586,6 +2686,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_one_protected_method() { check_metrics::( @@ -2599,6 +2700,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_mixed_visibility_methods() { // `a` is public (default). `b` is private. `c` is public again @@ -2615,6 +2717,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_singleton_method_is_counted() { // `def self.x` and plain `def x` both count; default is public. @@ -2629,6 +2732,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_singleton_class_methods() { // `class << self` opens a separate class space whose methods @@ -2644,6 +2748,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_argument_form_visibility_does_not_flip() { // `private :y` is a `call` node (argument form). It does NOT @@ -2666,6 +2771,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_visibility_call_wrapping_def_counts_the_method() { // `private def hidden; end` parses as a `private` call whose sole @@ -2688,6 +2794,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_visibility_call_wrapping_def_reads_the_keyword_not_the_flag() { // Seeds the body-wide flag to `private` first, so the assertion @@ -2708,6 +2815,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_bare_private_leaves_singleton_methods_public() { // Ruby's `private` sets the default for instance methods only; @@ -2726,6 +2834,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_symbol_argument_promotes_under_a_private_flag() { // The demotion pass must be able to move a method *back* to @@ -2747,6 +2856,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_class_method_keywords_scope_to_singletons() { // `private_class_method` is the only keyword that reaches a @@ -2770,6 +2880,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_symbol_argument_does_not_cross_the_singleton_boundary() { // An instance method and a singleton method may share a name. @@ -2788,6 +2899,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_class_method_symbol_does_not_cross_the_singleton_boundary() { // The mirror of the test above: `private_class_method :s` names @@ -2805,6 +2917,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_symbol_argument_reads_non_identifier_method_names() { // Ruby method names are not all `identifier`s: `val=` is a @@ -2827,6 +2940,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_parenthesised_bare_keyword_flips_the_flag() { // `private()` is the explicit-parens spelling of the bare @@ -2846,6 +2960,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_visibility_call_with_unresolvable_arguments_changes_nothing() { // A splat (`private *SYMS`), a bare identifier (`private foo`) @@ -2866,6 +2981,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_protected_wrapping_call_is_not_public() { // `protected` is a third state: `Npm` counts *public* methods, @@ -2888,6 +3004,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_class_method_keyword_governs_a_wrapped_def() { // The wrapping form of the class-method keyword, plus @@ -2909,6 +3026,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_private_in_a_singleton_class_body_demotes() { // The one place a bare `private` legitimately demotes a class @@ -2970,6 +3088,7 @@ class C { // three remain deletion-anchored only; adding the same call to them // is cheap and welcome if you are already in the file. + #[cfg(feature = "ruby")] #[test] fn ruby_initialize_is_not_a_public_method() { // The issue's own fixture. Ruby reports `[:value]` for @@ -2988,6 +3107,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_every_automatically_private_name_is_demoted() { // All five names in one body, so no member of @@ -3009,6 +3129,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_public_symbol_republishes_initialize() { // `public :initialize` is legal and does exactly what it says @@ -3031,6 +3152,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_public_keyword_wrapping_initialize_wins() { // `public def initialize` is public in Ruby, so a keyword that @@ -3060,6 +3182,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_a_class_method_keyword_does_not_republish_initialize() { // The one shape where a visibility keyword wraps an @@ -3095,6 +3218,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_public_marker_does_not_republish_initialize() { // The body-wide flag is *not* a keyword naming the declaration, @@ -3123,6 +3247,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_an_already_private_auto_private_name_is_not_flipped() { // Both spellings of "already private" in one body: `initialize` @@ -3153,6 +3278,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_singleton_initialize_stays_public() { // `def self.initialize` defines a method on the class object, @@ -3172,6 +3298,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_initialize_in_a_singleton_class_body_stays_public() { // The other spelling of the same exemption, and the one the @@ -3198,6 +3325,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_visibility_call_on_another_object_is_ignored() { // A receiver other than `self` puts the call on a different @@ -3221,6 +3349,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_visibility_call_on_self_is_honoured() { // The receiver gate above must still let `self.private :a` @@ -3238,6 +3367,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_symbol_array_argument_names_every_element() { // `private %i[g h]` is one argument naming two methods. Reading @@ -3255,6 +3385,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_interpolated_symbol_names_nothing() { // `:"get_#{suffix}"` is a `delimited_symbol` carrying a @@ -3279,6 +3410,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_visibility_does_not_leak_into_a_nested_class() { // Each class body opens its own `body_statement`, so the flag a @@ -3306,6 +3438,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_multiple_classes() { check_metrics::( @@ -3320,6 +3453,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_module_methods_not_counted() { // `Module` is `Namespace`, not `Class` — its methods do not @@ -3335,6 +3469,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_class_with_inheritance() { // Inheritance does not change method counts. @@ -3349,6 +3484,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_visibility_resets_between_classes() { // Each class body starts in default-public state regardless of @@ -3365,6 +3501,7 @@ class C { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_empty_class_no_methods() { check_metrics::("class Empty\nend\n", "foo.rb", |metric| { @@ -3386,6 +3523,7 @@ class C { // --- Python NPM --------------------------------------------------- + #[cfg(feature = "python")] #[test] fn python_empty_class_no_methods() { check_metrics::("class C:\n pass\n", "foo.py", |metric| { @@ -3395,6 +3533,7 @@ class C { }); } + #[cfg(feature = "python")] #[test] fn python_class_methods_count() { // 3 `def`s inside the class body → 3 methods, all public. @@ -3415,6 +3554,7 @@ class C { ); } + #[cfg(feature = "python")] #[test] fn python_decorated_methods_count() { // `@property`, `@staticmethod`, `@classmethod`, custom @@ -3439,6 +3579,7 @@ class C { ); } + #[cfg(feature = "python")] #[test] fn python_async_method_counts() { // `async def m` parses as a FunctionDefinition with an Async @@ -3453,6 +3594,7 @@ class C { ); } + #[cfg(feature = "python")] #[test] fn python_nested_class_methods_independent() { // Outer.method belongs to Outer; Inner.inner_method belongs @@ -3472,6 +3614,7 @@ class C { ); } + #[cfg(feature = "python")] #[test] fn python_module_level_function_is_not_method() { // `def f()` outside any class is a top-level function, not a @@ -3487,6 +3630,7 @@ class C { ); } + #[cfg(feature = "python")] #[test] fn python_dunder_methods_count() { // `__init__`, `__repr__`, `__eq__` are dunder methods — public @@ -3508,6 +3652,7 @@ class C { ); } + #[cfg(feature = "rust")] #[test] fn rust_empty_unit_no_methods() { check_metrics::("", "empty.rs", |metric| { @@ -3519,6 +3664,7 @@ class C { }); } + #[cfg(feature = "rust")] #[test] fn rust_impl_methods_count() { // 3 `fn`s in `impl Foo` body. `pub new` and `pub process` are @@ -3539,6 +3685,7 @@ class C { ); } + #[cfg(feature = "rust")] #[test] fn rust_pub_self_is_private() { // Regression for #460. `pub(self)` / `pub(in self)` restrict to @@ -3567,6 +3714,7 @@ class C { ); } + #[cfg(feature = "rust")] #[test] fn rust_trait_methods_count() { // `fn draw(&self);` (signature only) + `fn area(&self) -> f64 @@ -3592,6 +3740,7 @@ class C { ); } + #[cfg(feature = "rust")] #[test] fn rust_module_level_function_not_method() { // Top-level `fn` is NOT a method. The npa/npm metric on a @@ -3604,6 +3753,7 @@ class C { }); } + #[cfg(feature = "rust")] #[test] fn rust_multiple_impls_methods_aggregate() { // Two `impl Foo` blocks contribute 1 + 1 = 2 methods. @@ -3620,6 +3770,7 @@ class C { ); } + #[cfg(feature = "rust")] #[test] fn rust_trait_impl_block_counts_methods() { // `impl Drawable for Foo` is also an `impl_item` — its methods @@ -3647,6 +3798,7 @@ class C { // ----- Go ----- + #[cfg(feature = "go")] #[test] fn go_empty_unit_no_methods() { // No receiver methods → npm stays disabled, class_nm_sum = 0. @@ -3656,6 +3808,7 @@ class C { }); } + #[cfg(feature = "go")] #[test] fn go_method_declarations_count() { // Two `func (r Foo) ...` methods on the same receiver type → @@ -3675,6 +3828,7 @@ class C { ); } + #[cfg(feature = "go")] #[test] fn go_free_function_is_not_method() { // `func g() {}` has no receiver → NOT a method. class_nm_sum @@ -3690,6 +3844,7 @@ class C { ); } + #[cfg(feature = "go")] #[test] fn go_methods_on_different_receivers_aggregate_at_unit() { // Go's flat space model cannot group methods by receiver, so @@ -3710,6 +3865,7 @@ class C { ); } + #[cfg(feature = "go")] #[test] fn go_interface_methods_count_as_interface_nm() { // `interface { Read() error; Close() error }` declares two @@ -3738,6 +3894,7 @@ class C { ); } + #[cfg(feature = "go")] #[test] fn go_interface_methods_respect_export() { // Go's lexical export rule applies to interface method names @@ -3757,6 +3914,7 @@ class C { ); } + #[cfg(feature = "go")] #[test] fn go_pointer_receiver_methods_count() { // Pointer-receiver methods (`func (r *Foo) M() {}`) parse as @@ -3775,6 +3933,7 @@ class C { ); } + #[cfg(feature = "go")] #[test] fn go_npm_excludes_unexported() { // Mixed exported / unexported methods (issue #458). `Greet` @@ -3801,6 +3960,7 @@ class C { // Issue #275: Elixir `def` is public, `defp` is private. All // count toward `class_nm`; only the public ones bump `class_npm`. + #[cfg(feature = "elixir")] #[test] fn elixir_npm_def_is_public_defp_is_private() { check_metrics::( @@ -3814,6 +3974,7 @@ class C { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_npm_defmacro_counts_as_public() { check_metrics::( @@ -3827,6 +3988,7 @@ class C { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_npm_multiple_def_clauses_each_count() { // Pattern-match clauses each form their own method head. @@ -3840,6 +4002,7 @@ class C { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_npm_nested_defmodule_each_class() { check_metrics::( @@ -3853,6 +4016,7 @@ class C { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_npm_user_macro_not_classified_as_method() { // User-defined `custom_def` is a defmacro (counts) but its @@ -3872,6 +4036,7 @@ class C { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_npm_quoted_defs_do_not_inflate_method_count() { // Companion to `wmc::tests::elixir_wmc_quoted_defs_do_not_inflate_method_count` @@ -3909,6 +4074,7 @@ class C { /// reading of "is this a class space?" would show up: if the /// quote-template rule were ever extended to class macros, these /// counts would move. + #[cfg(feature = "elixir")] #[test] fn elixir_npm_counts_a_quoted_defmodule_as_a_class() { check_metrics::( @@ -3925,6 +4091,7 @@ class C { // ----- Objective-C ----- + #[cfg(feature = "objc")] #[test] fn objc_npm() { // ObjC has no method-privacy keyword: methods declared in @@ -3954,6 +4121,7 @@ class C { ); } + #[cfg(feature = "objc")] #[test] fn objc_npm_protocol() { // A `@protocol`'s methods after an `@optional` / `@required` @@ -3976,6 +4144,7 @@ class C { // ----- C++ ----- + #[cfg(feature = "cpp")] #[test] fn cpp_empty_unit_no_methods() { // No code → no class spaces → npm = 0. @@ -3986,6 +4155,7 @@ class C { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_class_methods_count() { // Two member functions (one defined inline, one declared only). @@ -4004,6 +4174,7 @@ class C { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_constructors_and_destructors_count() { // Constructors and destructors are parsed as `declaration` @@ -4025,6 +4196,7 @@ class C { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_template_methods_count() { // `template T foo(T x);` parses as @@ -4044,6 +4216,7 @@ class C { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_struct_methods_default_public() { // `struct` defaults to public visibility. All three methods @@ -4063,6 +4236,7 @@ class C { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_free_function_is_not_method() { // Top-level function — not inside any class — does not count @@ -4075,6 +4249,7 @@ class C { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_mixed_visibility_methods() { // `class` defaults to private. Public section gets 1 method, @@ -4095,6 +4270,7 @@ class C { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_multiple_classes_aggregate_at_unit() { // File-level rollup: Foo has 2 methods, Bar has 1. Unit @@ -4116,6 +4292,7 @@ class C { // fork gets no integration-snapshot coverage and its clone of the // `TemplateDeclaration` arm can only be pinned against its // extension-owning sibling (grammar-dispatch, "sweep the rest"). + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const TEMPLATE_METHOD_WITH_BODY: &str = "class C {\n\ public:\n\ template T get() { return T{}; }\n\ @@ -4138,6 +4315,7 @@ class C { // member, so `class_na`/`class_npa` are 1/1 rather than the // default 0 that a leak into `Npa` would be indistinguishable // from. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const CONVERSION_OPERATORS_WITHOUT_BODIES: &str = "class C {\n\ public:\n\ operator float();\n\ @@ -4171,6 +4349,7 @@ class C { // alone leaves the recursion one level short. Verified by // perturbation.) An empty `Nested` would leave that descent // untested in either direction. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const NON_METHOD_TEMPLATE_PAYLOADS: &str = "class C {\n\ public:\n\ template class Nested { void hidden() {} };\n\ @@ -4180,6 +4359,7 @@ class C { template T real() { return T{}; }\n\ };"; + #[cfg(feature = "cpp")] #[test] fn cpp_template_method_with_inline_body_counts() { // A templated member *with a body* parses as @@ -4199,6 +4379,7 @@ class C { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_template_method_with_inline_body_respects_visibility() { // Deliberately asymmetric — 2 public, 1 private. A template arm @@ -4222,6 +4403,7 @@ class C { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_template_conversion_operator_with_body_counts() { // A conversion operator's declarator is an `operator_cast`, so @@ -4246,6 +4428,7 @@ class C { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_conversion_operators_without_bodies_count_as_methods() { check_metrics_with_npa::( @@ -4263,6 +4446,7 @@ class C { ); } + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_conversion_operators_without_bodies_count_as_methods() { check_metrics_with_npa::( @@ -4316,6 +4500,7 @@ class C { // - the `private:` section makes public and total differ on both // metrics, so neither pair can be reached by an arm that ignores // `current_is_public`. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const FUNCTION_POINTER_MEMBERS: &str = "class F {\n\ public:\n\ int (*fp)(int);\n\ @@ -4348,6 +4533,7 @@ class C { // values are 2/1 rather than the 1/1/1/1 an all-public version // would give — which an arm ignoring `current_is_public` would // satisfy on both metrics at once. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const METHOD_RETURNING_FUNCTION_POINTER: &str = "class F {\n\ public:\n\ int (*getFp(int))(int);\n\ @@ -4357,6 +4543,7 @@ class C { int (*privFp)(int);\n\ };"; + #[cfg(feature = "cpp")] #[test] fn cpp_function_pointer_members_are_attributes_not_methods() { check_metrics_with_npa::(FUNCTION_POINTER_MEMBERS, "foo.cpp", |metric| { @@ -4378,6 +4565,7 @@ class C { }); } + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_function_pointer_members_are_attributes_not_methods() { check_metrics_with_npa::(FUNCTION_POINTER_MEMBERS, "foo.cpp", |metric| { @@ -4388,6 +4576,7 @@ class C { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_method_returning_a_function_pointer_stays_a_method() { check_metrics_with_npa::( @@ -4402,6 +4591,7 @@ class C { ); } + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_method_returning_a_function_pointer_stays_a_method() { check_metrics_with_npa::( @@ -4416,6 +4606,7 @@ class C { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_non_method_template_payloads_are_not_counted() { check_metrics_with_nom_wmc::( @@ -4443,6 +4634,7 @@ class C { ); } + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_template_method_with_inline_body_counts() { check_metrics_with_nom_wmc::( @@ -4457,6 +4649,7 @@ class C { ); } + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_non_method_template_payloads_are_not_counted() { check_metrics_with_nom_wmc::( @@ -4472,6 +4665,7 @@ class C { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_empty_unit_no_methods() { check_metrics::("", "empty.js", |metric| { @@ -4481,6 +4675,7 @@ class C { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_class_methods_count() { // `method_definition` direct children of `class_body` cover @@ -4502,6 +4697,7 @@ class C { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_arrow_field_is_method() { // `class Foo { x = () => {} }` is a method written as a field @@ -4519,6 +4715,7 @@ class C { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_free_function_is_not_method() { // Top-level functions and arrow functions outside a class @@ -4535,6 +4732,7 @@ class C { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_multiple_classes_aggregate_at_unit() { // File-level rollup: Foo has 2 methods, Bar has 1. Unit @@ -4550,6 +4748,7 @@ class C { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_class_methods_count() { // Mozjs shares JS's class vocabulary. @@ -4576,6 +4775,12 @@ class C { // `!is_nan()` proves the guard fires; the `== 0.0` checks pin the // chosen convention. Exercised across the explicit-visibility OO // languages (Java, C#, Kotlin, PHP). + #[cfg(all( + feature = "csharp", + feature = "java", + feature = "kotlin", + feature = "php" + ))] #[test] fn empty_class_coa_is_zero_not_nan() { let assert_zero = |metric: crate::CodeMetrics| { @@ -4595,6 +4800,7 @@ class C { // existing all-public guard explicitly excludes the empty case // (`!= 0`), so without the divisor guard `interface_coa` returned // 0.0 / 0.0 = NaN. The defined value is 0.0. + #[cfg(all(feature = "csharp", feature = "java"))] #[test] fn empty_interface_coa_is_zero_not_nan() { let assert_zero = |metric: crate::CodeMetrics| { @@ -4609,6 +4815,7 @@ class C { // Rounds out `npm`'s public surface — the `Display` impl and the // per-space `class_npm` / `class_nm` / `interface_*` accessors — // mirroring the `Display` tests the sibling metrics carry. + #[cfg(feature = "java")] #[test] fn stats_display_and_per_space_accessors() { check_func_space::( diff --git a/src/metrics/tokens.rs b/src/metrics/tokens.rs index 3d37533f8..e3c62a6e4 100644 --- a/src/metrics/tokens.rs +++ b/src/metrics/tokens.rs @@ -213,6 +213,7 @@ mod tests { /// `def foo(x): return x` → leaves: `def`, `foo`, `(`, `x`, `)`, /// `:`, `return`, `x` = 8 tokens, hand-counted. + #[cfg(feature = "python")] #[test] fn python_tokens_exact_count() { check_metrics::("def foo(x): return x", "foo.py", |metric| { @@ -222,6 +223,7 @@ mod tests { } /// Adding a Python comment must not change the token count. + #[cfg(feature = "python")] #[test] fn python_tokens_comments_excluded() { check_metrics::( @@ -234,6 +236,7 @@ mod tests { } /// Blank lines and indentation must not change the token count. + #[cfg(feature = "python")] #[test] fn python_tokens_whitespace_excluded() { check_metrics::( @@ -248,6 +251,7 @@ mod tests { /// Tokens must exceed Halstead `N1 + N2` for code containing /// punctuation Halstead skips. Guards against accidental Halstead /// reuse. + #[cfg(feature = "python")] #[test] fn python_tokens_distinct_from_halstead() { check_tokens_and_halstead::( @@ -274,6 +278,7 @@ mod tests { /// Asserting the exact `tokens_max` is what catches an attribution /// regression — a broken implementation that credited all 12 /// tokens to one scope would still pass `max <= sum`. + #[cfg(feature = "python")] #[test] fn python_tokens_nested_attribution() { check_metrics::( @@ -289,6 +294,7 @@ mod tests { /// C++ `/* … */` block comments must not contribute. /// Same fixture with and without comment yields the same count. + #[cfg(feature = "cpp")] #[test] fn cpp_tokens_block_comments_excluded() { check_metrics::( @@ -308,6 +314,7 @@ mod tests { /// C++ `// …` line comments must not contribute, matching the Python /// hand-counted style. Leaves outside the comment: /// `int`, `x`, `=`, `1`, `;` = 5. + #[cfg(feature = "cpp")] #[test] fn cpp_tokens_line_comments_excluded() { check_metrics::("int x = 1; // a one-line comment\n", "foo.cpp", |m| { @@ -320,6 +327,7 @@ mod tests { /// Whitespace and blank lines must not contribute to the token count /// (mirrors `python_tokens_whitespace_excluded`). + #[cfg(feature = "cpp")] #[test] fn cpp_tokens_whitespace_excluded() { check_metrics::("\n\nint foo(int x) {\n return x;\n}\n", "foo.cpp", |m| { @@ -332,6 +340,7 @@ mod tests { /// semicolons), so `tokens_sum` must exceed `N1 + N2` for a fixture /// with significant punctuation. Mirrors /// `python_tokens_distinct_from_halstead`. + #[cfg(feature = "cpp")] #[test] fn cpp_tokens_distinct_from_halstead() { check_tokens_and_halstead::( @@ -360,6 +369,7 @@ mod tests { /// regression: a broken implementation that credited every leaf to one /// scope would raise `tokens_max` to 27 while still passing /// `max <= sum`, mirroring the Python sibling's exact-max guard. + #[cfg(feature = "cpp")] #[test] fn cpp_tokens_nested_attribution() { check_metrics::( @@ -374,6 +384,7 @@ mod tests { } /// Java `// …` line comments must not contribute. + #[cfg(feature = "java")] #[test] fn java_tokens_line_comments_excluded() { check_metrics::( @@ -389,6 +400,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_tokens_line_comments_excluded() { // Groovy mirror — `// …` line comments must not contribute. @@ -407,6 +419,7 @@ mod tests { /// JS-family `` Annex-B `html_comment` leaves must not /// contribute tokens — they classify as comments now (#697). The /// count must match the comment-free source exactly. + #[cfg(feature = "javascript")] #[test] fn javascript_tokens_html_comment_excluded() { check_metrics::("\nlet x = 1;\n", "foo.js", |m| { @@ -421,6 +434,7 @@ mod tests { /// Groovy `/** … */` `groovydoc_comment` leaves must not contribute /// tokens (#697 — `is_comment` previously missed this kind even /// though `Loc` counted it). + #[cfg(feature = "groovy")] #[test] fn groovy_tokens_groovydoc_excluded() { check_metrics::( @@ -445,6 +459,7 @@ mod tests { /// would have misattributed a regression; they are linear now, but /// isolating the metric under test is still what makes the reading /// mean something. + #[cfg(feature = "rust")] fn tokens_of(source: &str) -> u64 { metrics_verbatim( crate::LANG::Rust, @@ -455,6 +470,7 @@ mod tests { .tokens_sum() } + #[cfg(feature = "rust")] fn nested_parens(depth: usize) -> String { format!( "fn f() -> i32 {{ {}1{} }}\n", @@ -486,6 +502,7 @@ mod tests { /// llvm-cov` and on shared Windows / macOS runners; the equivalent /// assertion in `cognitive` produced false failures in four /// separate environments before it was retired. + #[cfg(feature = "rust")] #[test] fn tokens_count_holds_at_depth() { let shallow = tokens_of(&nested_parens(1)); @@ -502,6 +519,7 @@ mod tests { /// `rust_tokens_doc_comments_excluded` already covers the latter at /// depth 1. What this adds is the anchored differential below: a /// count that would not survive an `in_comment` wired to a constant. + #[cfg(feature = "rust")] #[test] fn rust_tokens_comment_excluded_at_depth() { let deep_block = format!( @@ -531,6 +549,7 @@ mod tests { /// not themselves comment kinds (`//`, `outer_doc_comment_marker`, /// `doc_comment`), so excluding only the comment node is not enough /// — every leaf beneath it must be filtered too. + #[cfg(feature = "rust")] #[test] fn rust_tokens_doc_comments_excluded() { check_metrics::( @@ -554,6 +573,7 @@ mod tests { // metric is registered but never fires. `check_metrics` takes a // `fn` pointer so each test inlines its assertion directly. + #[cfg(feature = "python")] #[test] fn smoke_python() { check_metrics::("x = 1\n", "foo.py", |m| { @@ -561,6 +581,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn smoke_rust() { check_metrics::("fn f() { let x = 1; }", "foo.rs", |m| { @@ -568,6 +589,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn smoke_cpp() { check_metrics::("int x = 1;", "foo.cpp", |m| { @@ -575,6 +597,7 @@ mod tests { }); } + #[cfg(feature = "java")] #[test] fn smoke_java() { check_metrics::("class A { int x = 1; }", "A.java", |m| { @@ -582,6 +605,7 @@ mod tests { }); } + #[cfg(feature = "csharp")] #[test] fn smoke_csharp() { check_metrics::("class A { int X = 1; }", "A.cs", |m| { @@ -589,6 +613,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn smoke_javascript() { check_metrics::("let x = 1;", "foo.js", |m| { @@ -596,6 +621,7 @@ mod tests { }); } + #[cfg(feature = "mozjs")] #[test] fn smoke_mozjs() { check_metrics::("let x = 1;", "foo.js", |m| { @@ -603,6 +629,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn smoke_typescript() { check_metrics::("const x: number = 1;", "foo.ts", |m| { @@ -610,6 +637,7 @@ mod tests { }); } + #[cfg(feature = "typescript")] #[test] fn smoke_tsx() { check_metrics::("const x: number = 1;", "foo.tsx", |m| { @@ -617,6 +645,7 @@ mod tests { }); } + #[cfg(feature = "go")] #[test] fn smoke_go() { check_metrics::("package main\nfunc f() {}", "foo.go", |m| { @@ -624,6 +653,7 @@ mod tests { }); } + #[cfg(feature = "kotlin")] #[test] fn smoke_kotlin() { check_metrics::("fun f(): Int = 1", "foo.kt", |m| { @@ -631,6 +661,7 @@ mod tests { }); } + #[cfg(feature = "lua")] #[test] fn smoke_lua() { check_metrics::("local x = 1", "foo.lua", |m| { @@ -638,6 +669,7 @@ mod tests { }); } + #[cfg(feature = "bash")] #[test] fn smoke_bash() { check_metrics::("x=1", "foo.sh", |m| { @@ -645,6 +677,7 @@ mod tests { }); } + #[cfg(feature = "tcl")] #[test] fn smoke_tcl() { check_metrics::("set x 1", "foo.tcl", |m| { @@ -652,6 +685,7 @@ mod tests { }); } + #[cfg(feature = "perl")] #[test] fn smoke_perl() { check_metrics::("my $x = 1;", "foo.pl", |m| { @@ -659,6 +693,7 @@ mod tests { }); } + #[cfg(feature = "php")] #[test] fn smoke_php() { check_metrics::("("#define FOO 1\n", "foo.h", |m| { @@ -673,6 +714,12 @@ mod tests { }); } + #[cfg(any( + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "mozcpp" + ))] #[test] fn smoke_ccomment() { // Ccomment's grammar parses bare C source; non-comment text @@ -682,6 +729,7 @@ mod tests { }); } + #[cfg(feature = "c")] #[test] fn smoke_c() { check_metrics::("int x = 1;\n", "foo.c", |m| { @@ -689,6 +737,7 @@ mod tests { }); } + #[cfg(feature = "objc")] #[test] fn smoke_objc() { check_metrics::("int x = 1;\n", "foo.m", |m| { @@ -696,6 +745,7 @@ mod tests { }); } + #[cfg(feature = "elixir")] #[test] fn smoke_elixir() { check_metrics::("defmodule Foo do\n :ok\nend\n", "foo.ex", |m| { @@ -703,6 +753,7 @@ mod tests { }); } + #[cfg(feature = "ruby")] #[test] fn smoke_ruby() { check_metrics::("def foo\n a = 1\nend\n", "foo.rb", |m| { @@ -710,6 +761,7 @@ mod tests { }); } + #[cfg(feature = "irules")] #[test] fn smoke_irules() { check_metrics::("when X {\n set x 1\n}\n", "foo.irule", |m| { diff --git a/src/metrics/wmc.rs b/src/metrics/wmc.rs index 2166f15f7..c9f8a9bc2 100644 --- a/src/metrics/wmc.rs +++ b/src/metrics/wmc.rs @@ -486,6 +486,7 @@ mod tests { // member, so they need Npm too. check_metrics_only_shim!(check_wmc_and_npm, Wmc, Npm); + #[cfg(feature = "java")] #[test] fn java_single_class() { check_metrics::( @@ -546,6 +547,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_single_class() { // WMC = sum of method cyclomatic complexities for the class. @@ -578,6 +580,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_empty_class() { check_metrics::("class Empty {}", "foo.groovy", |metric| { @@ -585,6 +588,7 @@ mod tests { }); } + #[cfg(feature = "groovy")] #[test] fn groovy_class_with_single_method() { check_metrics::( @@ -601,6 +605,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_multiple_classes() { check_metrics::( @@ -618,6 +623,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_class_with_branching_methods() { check_metrics::( @@ -642,6 +648,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_interface_wmc_is_zero() { // Interfaces declare method signatures with no body — wmc = 0. @@ -657,6 +664,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_static_nested_class() { // Mirrors `java_static_nested_class`: nested classes get @@ -677,6 +685,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] #[ignore = "dekobon Groovy grammar v1 does not yet support inner classes inside class bodies"] fn groovy_nested_inner_classes_wmc() { @@ -700,6 +709,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_local_inner_class() { // A class declared inside a method body. WMC counts its method @@ -727,6 +737,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] #[ignore = "dekobon Groovy grammar v1 does not yet support anonymous inner classes (`new T() { … }`)"] fn groovy_anonymous_inner_class_wmc() { @@ -755,6 +766,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_lambda_expression_wmc() { // Lambdas inside a method body don't form their own class @@ -777,6 +789,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_single_interface_wmc() { // Default methods inside an interface contribute to WMC. @@ -800,6 +813,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] #[ignore = "dekobon Groovy grammar v1 does not yet support inner classes inside interface bodies"] fn groovy_class_in_interface() { @@ -825,6 +839,7 @@ mod tests { // Regression for issue #280: Groovy enum bodies fold method-level // cyclomatic into `class_wmc_sum` just like Java. + #[cfg(feature = "groovy")] #[test] fn groovy_enum_wmc_aggregates_method_complexity() { check_metrics::( @@ -849,6 +864,7 @@ mod tests { // declarations. The structural assertion is what distinguishes a // working fix from a vacuous one (see the Java sibling for the // rationale). + #[cfg(feature = "groovy")] #[test] #[ignore = "dekobon Groovy grammar v1 does not support annotation type elements with `default` values"] fn groovy_annotation_type_opens_interface_space_with_zero_wmc() { @@ -867,6 +883,7 @@ mod tests { // Constructors are considered as methods // Reference: https://pdepend.org/documentation/software-metrics/weighted-method-count.html + #[cfg(feature = "java")] #[test] fn java_multiple_classes() { check_metrics::( @@ -909,6 +926,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_static_nested_class() { check_metrics::( @@ -936,6 +954,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_nested_inner_classes() { check_metrics::( @@ -1005,6 +1024,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_local_inner_class() { check_metrics::( @@ -1053,6 +1073,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_anonymous_inner_class() { check_metrics::( @@ -1088,6 +1109,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_nested_anonymous_inner_classes() { check_metrics::( @@ -1137,6 +1159,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_lambda_expression() { check_metrics::( @@ -1173,6 +1196,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_single_interface() { check_metrics::( @@ -1202,6 +1226,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_multiple_interfaces() { check_metrics::( @@ -1233,6 +1258,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_nested_inner_interfaces() { check_metrics::( @@ -1268,6 +1294,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_class_in_interface() { check_metrics::( @@ -1303,6 +1330,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_interface_in_class() { check_metrics::( @@ -1341,6 +1369,7 @@ mod tests { // Regression for issue #280: Java `EnumDeclaration` opens a class // space, so method-level cyclomatic complexity inside the enum // body folds into `class_wmc_sum`. + #[cfg(feature = "java")] #[test] fn java_enum_wmc_aggregates_method_complexity() { check_metrics::( @@ -1364,6 +1393,7 @@ mod tests { // Regression for issue #280: Java `RecordDeclaration` is treated as // a class space; methods inside its explicit body contribute to // WMC. + #[cfg(feature = "java")] #[test] fn java_record_wmc_aggregates_method_complexity() { check_metrics::( @@ -1389,6 +1419,7 @@ mod tests { /// compactly and normally, but an alternative constructor delegating /// with `this(…)` is legal alongside a compact one — and each must /// open its own space even though both are named `R`. + #[cfg(feature = "java")] #[test] fn java_record_compact_and_alternative_constructors_open_two_spaces() { check_func_space::( @@ -1427,6 +1458,7 @@ mod tests { /// that change can make this fail. It is here to state the boundary /// of the change, not to cover a line — the `RecordDeclaration` arm /// itself is covered by #280's tests. + #[cfg(feature = "java")] #[test] fn java_record_without_constructor_opens_only_its_methods() { check_func_space::( @@ -1461,6 +1493,7 @@ mod tests { // omit the annotation type space, and `0 == 0` would still hold); // the structural check on `space.kind` is what catches that // regression. + #[cfg(feature = "java")] #[test] fn java_annotation_type_opens_interface_space_with_zero_wmc() { check_func_space::( @@ -1478,6 +1511,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_single_class() { check_metrics::( @@ -1508,6 +1542,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_multiple_classes() { check_metrics::( @@ -1531,6 +1566,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_static_nested_class() { check_metrics::( @@ -1550,6 +1586,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_nested_inner_classes() { check_metrics::( @@ -1572,6 +1609,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_local_inner_class() { // C# uses local functions instead of Java's local classes. @@ -1594,6 +1632,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_anonymous_inner_class() { check_metrics::( @@ -1613,6 +1652,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_nested_anonymous_inner_classes() { check_metrics::( @@ -1634,6 +1674,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_lambda_expression() { check_metrics::( @@ -1651,6 +1692,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_indexer_wmc() { // A bodied indexer folds its accessor complexities into the @@ -1675,6 +1717,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_expression_bodied_indexer_wmc() { // The accessor-less expression-bodied form (`this[int i] => _d[i];`) @@ -1697,6 +1740,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_property_wmc() { // A bodied property folds its accessor complexities into the @@ -1719,6 +1763,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_expression_bodied_property_wmc() { // The accessor-less expression-bodied form (`int W => _w;`) has no @@ -1740,6 +1785,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_single_interface() { check_metrics::( @@ -1756,6 +1802,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_multiple_interfaces() { check_metrics::( @@ -1770,6 +1817,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_nested_inner_interfaces() { check_metrics::( @@ -1788,6 +1836,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_class_in_interface() { check_metrics::( @@ -1806,6 +1855,7 @@ mod tests { ); } + #[cfg(feature = "csharp")] #[test] fn csharp_interface_in_class() { check_metrics::( @@ -1828,6 +1878,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_no_classes() { check_metrics::( @@ -1837,6 +1888,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_one_class_simple() { check_metrics::( @@ -1850,6 +1902,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_one_class_with_loops() { check_metrics::( @@ -1868,6 +1921,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_one_class_with_branches() { check_metrics::( @@ -1888,6 +1942,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_class_with_methods_only() { check_metrics::( @@ -1902,6 +1957,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_multiple_classes() { check_metrics::( @@ -1922,6 +1978,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_anonymous_class() { check_metrics::( @@ -1937,6 +1994,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_class_with_static_methods() { check_metrics::( @@ -1953,6 +2011,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_interface_wmc() { check_metrics::( @@ -1966,6 +2025,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_trait_wmc() { check_metrics::( @@ -1981,6 +2041,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_enum_with_methods() { check_metrics::( @@ -2000,6 +2061,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_class_inside_namespace() { check_metrics::( @@ -2016,6 +2078,7 @@ mod tests { ); } + #[cfg(feature = "php")] #[test] fn php_class_complex() { check_metrics::( @@ -2048,6 +2111,7 @@ mod tests { // function cyclomatic complexity accumulates into the enclosing // class/interface bucket, mirroring the Java impl. + #[cfg(feature = "kotlin")] #[test] fn kotlin_empty_class() { // Empty class — no methods, WMC = 0. @@ -2058,6 +2122,7 @@ mod tests { }); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_single_class() { // wmc = 1 (method base) + 1 (if) + 1 (explicit when arm; `else` @@ -2083,6 +2148,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_multiple_classes() { // A: constructor 1 + setA 1 + getA 1 = 3 @@ -2108,6 +2174,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_nested_class() { // Outer: 0 methods. Nested: m(): +1 @@ -2126,6 +2193,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_inner_class() { // `inner class` differs semantically (captures outer reference) but @@ -2146,6 +2214,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_data_class() { // `data class` synthesizes copy/equals/hashCode/toString at @@ -2164,6 +2233,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_object_singleton() { // `object` declarations are singletons; the getter routes them to @@ -2184,6 +2254,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_companion_object() { // A `companion object` opens its own Class space, exactly like a @@ -2209,6 +2280,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_companion_object_opens_class_space() { // Structural guard for #431: a named `companion object` must open @@ -2247,6 +2319,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_object_literal_opens_class_space() { // Structural guard for #463: an anonymous `object : T { ... }` @@ -2321,6 +2394,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_anonymous_class_opens_space() { // #463: a Java anonymous class (`new Runnable() { ... }`) opens its @@ -2376,6 +2450,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_lambda_opens_no_class_space() { // Guard against mis-detection (#463): a Java lambda is a @@ -2410,6 +2485,7 @@ mod tests { ); } + #[cfg(feature = "groovy")] #[test] fn groovy_anonymous_class_models_body_as_closure() { // #463 upstream-grammar note: the pinned dekobon Groovy grammar @@ -2464,6 +2540,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_interface_simple() { // Interface methods all contribute to the interface bucket. @@ -2481,6 +2558,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_interface_with_default_method() { // Default method with control flow counts its full cyclomatic. @@ -2500,6 +2578,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_override_function() { // `override fun` is structurally just a `function_declaration` with @@ -2520,6 +2599,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_secondary_constructor() { // Secondary constructors are explicit `secondary_constructor` @@ -2544,6 +2624,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_init_block() { // An `init` block opens a function space since #1184, so its @@ -2573,6 +2654,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_top_level_function_excluded() { // Top-level `fun` and `val` belong to the `Unit` space, not a class @@ -2591,6 +2673,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_extension_function_excluded() { // Extension functions look syntactically like methods but the @@ -2610,6 +2693,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_generic_class() { // Generic class with two methods. @@ -2627,6 +2711,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_class_in_interface() { // Nested class inside an interface: the inner class is a class @@ -2648,6 +2733,7 @@ mod tests { ); } + #[cfg(feature = "kotlin")] #[test] fn kotlin_interface_in_class() { // Inverse of the prior test. @@ -2674,6 +2760,7 @@ mod tests { // methods. Interface method signatures have no bodies and add zero // (matching Java's abstract-method rule). + #[cfg(feature = "typescript")] #[test] fn typescript_class_wmc_single_method() { check_metrics::( @@ -2688,6 +2775,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_class_wmc_two_methods() { check_metrics::( @@ -2706,6 +2794,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_class_wmc_with_branches() { check_metrics::( @@ -2727,6 +2816,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_class_wmc_arrow_field() { // Arrow-function class fields contribute their cyclomatic to @@ -2746,6 +2836,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_class_wmc_with_loops() { check_metrics::( @@ -2766,6 +2857,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_abstract_class_wmc() { // Abstract method signatures have no body — contribute 0. @@ -2782,6 +2874,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_interface_wmc_zero() { // Interface method signatures have no bodies → 0 WMC. @@ -2799,6 +2892,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_constructor_wmc() { // Constructor counts as a method; its cyclomatic adds to the @@ -2822,6 +2916,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_getter_setter_wmc() { // Getter and setter each contribute 1 (base). @@ -2839,6 +2934,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_multiple_classes_wmc_independent() { check_metrics::( @@ -2858,6 +2954,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_class_wmc_with_ternary_and_logical() { check_metrics::( @@ -2876,6 +2973,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_generic_class_wmc() { check_metrics::( @@ -2894,6 +2992,7 @@ mod tests { // TSX parity + #[cfg(feature = "typescript")] #[test] fn tsx_class_wmc_single_method() { check_metrics::( @@ -2906,6 +3005,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_class_wmc_two_methods() { check_metrics::( @@ -2924,6 +3024,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_class_wmc_with_branches() { check_metrics::( @@ -2942,6 +3043,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_class_wmc_arrow_field() { check_metrics::( @@ -2959,6 +3061,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_class_wmc_with_loops() { check_metrics::( @@ -2977,6 +3080,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_abstract_class_wmc() { check_metrics::( @@ -2992,6 +3096,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_interface_wmc_zero() { check_metrics::( @@ -3005,6 +3110,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_constructor_wmc() { check_metrics::( @@ -3023,6 +3129,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_getter_setter_wmc() { check_metrics::( @@ -3039,6 +3146,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_multiple_classes_wmc_independent() { check_metrics::( @@ -3057,6 +3165,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_class_wmc_with_ternary_and_logical() { check_metrics::( @@ -3073,6 +3182,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_generic_class_wmc() { check_metrics::( @@ -3096,6 +3206,7 @@ mod tests { // and does not contribute to WMC. Method cyclomatic complexities // accumulate into the enclosing class via `class_interface_compute`. + #[cfg(feature = "ruby")] #[test] fn ruby_no_classes() { // File with only a top-level method — no class space, WMC = 0. @@ -3106,6 +3217,7 @@ mod tests { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_empty_class() { // Class with no methods → wmc = 0. @@ -3115,6 +3227,7 @@ mod tests { }); } + #[cfg(feature = "ruby")] #[test] fn ruby_one_class_simple() { // Two methods, each with cyclomatic = 1 (the method base) → wmc = 2. @@ -3128,6 +3241,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_one_class_with_branch() { // One method with cyclomatic 1 (base) + 1 (if) = 2. @@ -3141,6 +3255,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_one_class_with_loop() { // One method with cyclomatic 1 (base) + 1 (while) = 2. @@ -3154,6 +3269,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_singleton_method_included() { // Mix of regular and singleton (`def self.x`) methods, both @@ -3168,6 +3284,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_singleton_class_methods_included() { // Methods inside `class << self` belong to the enclosing class @@ -3184,6 +3301,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_multiple_classes() { // Each class contributes its method-cyclomatic sum to the rollup. @@ -3198,6 +3316,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_module_only() { // Module is a `Namespace` space — does NOT contribute to WMC even @@ -3212,6 +3331,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_class_with_inheritance() { // `class A < B` inherits — irrelevant to WMC, which depends only on @@ -3226,6 +3346,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_class_with_visibility_keywords() { // Visibility keywords do NOT affect WMC — every method body @@ -3240,6 +3361,7 @@ mod tests { ); } + #[cfg(feature = "ruby")] #[test] fn ruby_class_complex() { // Class with two methods whose cyclomatic sums combine. @@ -3267,6 +3389,7 @@ mod tests { // --- Python WMC --------------------------------------------------- + #[cfg(feature = "python")] #[test] fn python_empty_class_zero_wmc() { check_metrics::("class C:\n pass\n", "foo.py", |metric| { @@ -3276,6 +3399,7 @@ mod tests { }); } + #[cfg(feature = "python")] #[test] fn python_single_method_wmc_one() { // Single straight-line method → cyclomatic 1 → WMC 1. @@ -3289,6 +3413,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_method_with_if_adds_to_wmc() { // Cyclomatic: 1 (base) + 1 (if) = 2. WMC = 2. @@ -3302,6 +3427,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_multiple_methods_wmc_sums() { // method1 cyclomatic 1, method2 cyclomatic 2 (if), method3 @@ -3327,6 +3453,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_top_level_function_does_not_contribute_to_class_wmc() { // Top-level function lives in the module/unit space, not in a @@ -3341,6 +3468,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_multiple_classes_wmc_independent() { // Each class accumulates its own methods' cyclomatic. The @@ -3363,6 +3491,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_empty_unit_zero_wmc() { check_metrics::("", "empty.rs", |metric| { @@ -3372,6 +3501,7 @@ mod tests { }); } + #[cfg(feature = "rust")] #[test] fn rust_single_impl_method_wmc_one() { // Single straight-line method → cyclomatic 1 → WMC 1. @@ -3385,6 +3515,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_method_with_if_adds_to_wmc() { // Cyclomatic: 1 (base) + 1 (if) = 2. WMC = 2. @@ -3403,6 +3534,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_multiple_methods_wmc_sums() { // m1 cyclomatic 1, m2 cyclomatic 2 (if), m3 cyclomatic 3 (if @@ -3425,6 +3557,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_multiple_impls_wmc_aggregate() { // Two `impl` blocks for Foo, each contributing 1 method with @@ -3441,6 +3574,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_trait_default_method_contributes_to_interface_wmc() { // A trait method with a default body — `area` is a function @@ -3458,6 +3592,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_top_level_function_does_not_contribute_to_class_wmc() { // Free `fn f()` opens a Function space but no class/trait @@ -3476,6 +3611,7 @@ mod tests { // ----- Go ----- + #[cfg(feature = "go")] #[test] fn go_wmc_is_zero_documented_limitation() { // Go's flat space model does not expose per-receiver class @@ -3509,6 +3645,7 @@ mod tests { // them to Function spaces inside the surrounding `defmodule` // Class. WMC then aggregates cyclomatic per method into the // class via the shared `class_interface_compute` aggregator. + #[cfg(feature = "elixir")] #[test] fn elixir_wmc_aggregates_def_methods() { check_metrics::( @@ -3532,6 +3669,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_wmc_def_plus_defp_counts_both() { check_metrics::( @@ -3545,6 +3683,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_wmc_defmacro_counts() { check_metrics::( @@ -3557,6 +3696,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_wmc_multiple_clauses_each_a_method() { // Each `def f(...)` head is a Call with its own Function @@ -3571,6 +3711,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_wmc_nested_defmodule_isolates() { check_metrics::( @@ -3583,6 +3724,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_wmc_user_macro_not_classified_as_method() { // A user-defined `defmacro custom_def`, then invoking @@ -3605,6 +3747,7 @@ mod tests { ); } + #[cfg(feature = "elixir")] #[test] fn elixir_wmc_quoted_defs_do_not_inflate_method_count() { // Regression test for #310: previously, every `def` lexically @@ -3630,6 +3773,7 @@ mod tests { // ----- Objective-C ----- + #[cfg(feature = "objc")] #[test] fn objc_wmc() { // `@implementation` is a Class space; each `method_definition` @@ -3673,6 +3817,7 @@ mod tests { // so the expectation cannot hold for the wrong reason: 3 is the // pre-fix value (`helper`'s cyclomatic 2 plus `m`'s 1), 1 is // correct, and 0 would mean the roll-up dropped `m` as well. + #[cfg(feature = "objc")] const OBJC_HELPER_IN_IMPLEMENTATION: &str = "@implementation Foo\n\ static int helper(int x) { if (x) { return 1; } return 0; }\n\ - (void)m { }\n\ @@ -3680,6 +3825,7 @@ mod tests { // `m` branches, so the expectation (2) differs from the pre-fix // value (4), from the method count (1), and from zero. + #[cfg(feature = "objc")] const OBJC_HELPER_AND_BRANCHING_METHOD: &str = "@implementation Foo\n\ static int helper(int x) { if (x) { return 1; } return 0; }\n\ - (int)m:(int)x { if (x) { return 1; } return 0; }\n\ @@ -3688,6 +3834,7 @@ mod tests { // A category `@implementation Foo (Cat)` parses as the same // `class_implementation` node with a `category` field, so its // members nest identically. + #[cfg(feature = "objc")] const OBJC_HELPER_IN_CATEGORY: &str = "@implementation Foo (Cat)\n\ static int helper(int x) { if (x) { return 1; } return 0; }\n\ - (int)m:(int)x { if (x) { return 1; } return 0; }\n\ @@ -3699,6 +3846,7 @@ mod tests { // `method_declaration`s, so a correct `interface_wmc_sum` is 0; the // `@implementation` in the same fixture keeps a non-zero // `class_wmc_sum` so the pair cannot pass by everything being zero. + #[cfg(feature = "objc")] const OBJC_HELPER_IN_INTERFACE: &str = "@interface Foo : NSObject\n\ static int helper(int x) { if (x) { return 1; } return 0; }\n\ - (int)m:(int)x;\n\ @@ -3707,6 +3855,7 @@ mod tests { - (int)m:(int)x { if (x) { return 1; } return 0; }\n\ @end\n"; + #[cfg(feature = "objc")] const OBJC_HELPER_IN_PROTOCOL: &str = "@protocol Proto\n\ static int helper(int x) { if (x) { return 1; } return 0; }\n\ - (int)m:(int)x;\n\ @@ -3720,6 +3869,7 @@ mod tests { // `implementation_definition`; inside `@interface` it is the // `preproc_if` — a kind shared with a file-scope `#if`. Keying on // the node's own kind covers both. + #[cfg(feature = "objc")] const OBJC_HELPER_BEHIND_PREPROC: &str = "@interface Foo : NSObject\n\ #if FOO\n\ static int declared(int x) { if (x) { return 1; } return 0; }\n\ @@ -3736,12 +3886,14 @@ mod tests { // The reverse direction: a C function at file scope, which no // container ever weighted, alongside a class that has one branching // method. + #[cfg(feature = "objc")] const OBJC_FILE_SCOPE_FUNCTION: &str = "\ static int loose(int x) { if (x) { return 1; } return 0; }\n\ @implementation Foo\n\ - (int)m:(int)x { if (x) { return 1; } return 0; }\n\ @end\n"; + #[cfg(feature = "objc")] #[test] fn objc_static_helper_in_implementation_is_not_weighted_into_the_class() { check_wmc_and_npm::(OBJC_HELPER_IN_IMPLEMENTATION, "foo.m", |metric| { @@ -3759,6 +3911,7 @@ mod tests { }); } + #[cfg(feature = "objc")] #[test] fn objc_static_helper_keeps_its_own_function_space() { // Where the excluded complexity lands. The space tree is built @@ -3778,6 +3931,7 @@ mod tests { }); } + #[cfg(feature = "objc")] #[test] fn objc_static_helper_leaves_a_branching_method_its_weight() { check_wmc_and_npm::(OBJC_HELPER_AND_BRANCHING_METHOD, "foo.m", |metric| { @@ -3787,6 +3941,7 @@ mod tests { }); } + #[cfg(feature = "objc")] #[test] fn objc_static_helper_in_a_category_is_not_weighted_into_the_class() { check_wmc_and_npm::(OBJC_HELPER_IN_CATEGORY, "foo.m", |metric| { @@ -3796,6 +3951,7 @@ mod tests { }); } + #[cfg(feature = "objc")] #[test] fn objc_static_helper_in_an_interface_is_not_weighted_into_the_interface() { check_wmc_and_npm::(OBJC_HELPER_IN_INTERFACE, "foo.m", |metric| { @@ -3808,6 +3964,7 @@ mod tests { }); } + #[cfg(feature = "objc")] #[test] fn objc_static_helper_in_a_protocol_is_not_weighted_into_the_protocol() { check_wmc_and_npm::(OBJC_HELPER_IN_PROTOCOL, "foo.m", |metric| { @@ -3818,6 +3975,7 @@ mod tests { }); } + #[cfg(feature = "objc")] #[test] fn objc_static_helper_behind_a_preprocessor_conditional_is_not_weighted() { check_wmc_and_npm::(OBJC_HELPER_BEHIND_PREPROC, "foo.m", |metric| { @@ -3831,6 +3989,7 @@ mod tests { }); } + #[cfg(feature = "objc")] #[test] fn objc_file_scope_function_is_unaffected() { check_wmc_and_npm::(OBJC_FILE_SCOPE_FUNCTION, "foo.m", |metric| { @@ -3854,6 +4013,7 @@ mod tests { // ----- C++ ----- + #[cfg(feature = "cpp")] #[test] fn cpp_empty_unit_zero_wmc() { // No code → no class spaces → wmc = 0. Wires up the trait. @@ -3864,6 +4024,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_single_method_wmc_one() { // One method with no control flow → cyclomatic = 1 → wmc = 1. @@ -3873,6 +4034,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_method_with_if_adds_to_wmc() { // One method with one `if` → cyclomatic = 2 → wmc = 2. @@ -3892,6 +4054,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_struct_wmc_maps_to_class() { // `struct` opens a `SpaceKind::Struct` space — the C++ Wmc @@ -3913,6 +4076,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_free_function_does_not_contribute_to_class_wmc() { // A top-level function is not inside any class — its @@ -3931,6 +4095,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_multiple_methods_wmc_sums() { // Two methods, one with `if` (cyclomatic 2), one without @@ -3949,6 +4114,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_multiple_classes_wmc_aggregate() { // File-level rollup: Foo has wmc 1, Bar has wmc 1. Unit @@ -3977,6 +4143,7 @@ mod tests { // so the expectation cannot hold for the wrong reason: 3 is the // pre-fix value (`amigo`'s cyclomatic 2 plus `mine`'s 1), 1 is // correct, and 0 would mean the roll-up dropped `mine` as well. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const INLINE_FRIEND: &str = "class R {\n\ public:\n\ friend void amigo() { if (1) { } }\n\ @@ -3988,6 +4155,7 @@ mod tests { // function_definition` shape as a named one. `dump` is deliberately // a *branching* method so the expectation (2) differs from both the // pre-fix value (4) and the method count (1). + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const INLINE_FRIEND_OPERATOR: &str = "class R {\n\ public:\n\ friend std::ostream& operator<<(std::ostream& o, const R& r) {\n\ @@ -4004,6 +4172,7 @@ mod tests { // way and one parent check covers both shapes. This is the friend // from #1258's `NON_METHOD_TEMPLATE_PAYLOADS`, whose `npm` side // that issue fixed and whose `wmc` side this one does. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const TEMPLATED_INLINE_FRIEND: &str = "class R {\n\ public:\n\ template friend void amigo(T t) { if (t) { } }\n\ @@ -4014,6 +4183,7 @@ mod tests { // declaration, and a befriended class. All parse as // `friend_declaration > declaration` (or bare tokens), open no // function space, and so never reach the predicate at all. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const FRIEND_WITHOUT_BODY: &str = "class R {\n\ public:\n\ friend void amigo();\n\ @@ -4026,6 +4196,7 @@ mod tests { // (2, from `hidden`) and `Outer` keeps its own (1, from // `outer_m`), so the file sum is 3 — never 5, which is what // folding `amigo` into `Inner` produced. + #[cfg(any(feature = "cpp", feature = "mozcpp"))] const NESTED_CLASS_FRIEND: &str = "class Outer {\n\ public:\n\ class Inner {\n\ @@ -4036,6 +4207,7 @@ mod tests { void outer_m() { }\n\ };"; + #[cfg(feature = "cpp")] #[test] fn cpp_inline_friend_is_not_weighted_into_the_class() { check_wmc_and_npm::(INLINE_FRIEND, "foo.cpp", |metric| { @@ -4055,6 +4227,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_inline_friend_keeps_its_own_function_space() { // Where the excluded complexity lands. The space tree is built @@ -4074,6 +4247,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_inline_friend_operator_is_not_weighted_into_the_class() { check_wmc_and_npm::(INLINE_FRIEND_OPERATOR, "foo.cpp", |metric| { @@ -4083,6 +4257,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_templated_inline_friend_is_not_weighted_into_the_class() { check_wmc_and_npm::(TEMPLATED_INLINE_FRIEND, "foo.cpp", |metric| { @@ -4092,6 +4267,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_friend_declared_without_a_body_leaves_wmc_alone() { check_wmc_and_npm::(FRIEND_WITHOUT_BODY, "foo.cpp", |metric| { @@ -4104,6 +4280,7 @@ mod tests { }); } + #[cfg(feature = "cpp")] #[test] fn cpp_friend_of_a_nested_class_is_weighted_into_neither() { check_wmc_and_npm::(NESTED_CLASS_FRIEND, "foo.cpp", |metric| { @@ -4119,6 +4296,7 @@ mod tests { // it and its `Checker` clone would drift silently. These mirror the // C++ cases above over the same fixtures. + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_inline_friend_is_not_weighted_into_the_class() { check_wmc_and_npm::(INLINE_FRIEND, "foo.cpp", |metric| { @@ -4130,6 +4308,7 @@ mod tests { }); } + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_inline_friend_operator_is_not_weighted_into_the_class() { check_wmc_and_npm::(INLINE_FRIEND_OPERATOR, "foo.cpp", |metric| { @@ -4139,6 +4318,7 @@ mod tests { }); } + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_templated_inline_friend_is_not_weighted_into_the_class() { check_wmc_and_npm::(TEMPLATED_INLINE_FRIEND, "foo.cpp", |metric| { @@ -4148,6 +4328,7 @@ mod tests { }); } + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_friend_declared_without_a_body_leaves_wmc_alone() { check_wmc_and_npm::(FRIEND_WITHOUT_BODY, "foo.cpp", |metric| { @@ -4157,6 +4338,7 @@ mod tests { }); } + #[cfg(feature = "mozcpp")] #[test] fn mozcpp_friend_of_a_nested_class_is_weighted_into_neither() { check_wmc_and_npm::(NESTED_CLASS_FRIEND, "foo.cpp", |metric| { @@ -4166,6 +4348,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_empty_unit_zero_wmc() { check_metrics::("", "empty.js", |metric| { @@ -4174,6 +4357,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_single_method_wmc_one() { // Class with a single straight-line method has wmc = 1 (the @@ -4184,6 +4368,7 @@ mod tests { }); } + #[cfg(feature = "javascript")] #[test] fn javascript_method_with_if_adds_to_wmc() { // Method body with an `if` has cyclomatic = 2 → class_wmc = 2. @@ -4197,6 +4382,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_free_function_does_not_contribute_to_class_wmc() { // Top-level functions are not class methods; their @@ -4212,6 +4398,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_multiple_classes_wmc_aggregate() { // File-level rollup: Foo has wmc 1, Bar has wmc 1. Unit @@ -4226,6 +4413,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_single_method_wmc_one() { check_metrics::("class Foo { a() { return 1; } }", "foo.js", |metric| { @@ -4240,6 +4428,7 @@ mod tests { // (base 1) sum to exactly 2 with no double-attribution and no // negative intermediate. Mirrors `java_local_inner_class`, kept // minimal to pin the `u64` accessor's non-negativity. + #[cfg(feature = "java")] #[test] fn java_method_with_nested_class_wmc_is_non_negative_integer() { check_metrics::( @@ -4261,6 +4450,7 @@ mod tests { // Rounds out `wmc`'s public surface — the `Display` impl and the // per-space `class_wmc` / `interface_wmc` accessors — mirroring the // `Display` tests the sibling metrics (nom, nargs, halstead) carry. + #[cfg(feature = "java")] #[test] fn stats_display_and_per_space_accessors() { check_func_space::( diff --git a/src/ops.rs b/src/ops.rs index 44899ceda..b35837726 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -413,6 +413,16 @@ mod tests { use super::Ops; use crate::{Ast, LANG, Source}; + #[cfg(any( + feature = "cpp", + feature = "java", + feature = "javascript", + feature = "mozjs", + feature = "perl", + feature = "python", + feature = "rust", + feature = "typescript", + ))] #[inline] fn check_ops( lang: LANG, @@ -443,6 +453,7 @@ mod tests { assert_eq!(&operands_str[..], correct_operands); } + #[cfg(feature = "python")] #[test] fn python_ops() { check_ops( @@ -455,6 +466,7 @@ mod tests { ); } + #[cfg(feature = "perl")] #[test] fn perl_pattern_operations_render_as_source_spellings() { // #1314 classifies `s///` and `tr///` as Halstead operators. @@ -483,6 +495,7 @@ mod tests { ); } + #[cfg(feature = "python")] #[test] fn python_function_ops() { check_ops( @@ -499,6 +512,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_ops() { check_ops( @@ -512,6 +526,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn cpp_function_ops() { check_ops( @@ -540,6 +555,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_ops() { check_ops( @@ -551,6 +567,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn rust_function_ops() { check_ops( @@ -566,6 +583,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_ops() { check_ops( @@ -583,6 +601,7 @@ mod tests { ); } + #[cfg(feature = "javascript")] #[test] fn javascript_function_ops() { check_ops( @@ -604,6 +623,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_ops() { check_ops( @@ -621,6 +641,7 @@ mod tests { ); } + #[cfg(feature = "mozjs")] #[test] fn mozjs_function_ops() { check_ops( @@ -642,6 +663,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_ops() { // Issue #1261: the `: string` annotation counts exactly once, @@ -682,6 +704,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn typescript_function_ops() { // Issue #1261: see `typescript_ops` — the `string` type keyword @@ -723,6 +746,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_ops() { // Issue #1261: TSX exposes the `: string` type-keyword child as @@ -761,6 +785,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_function_ops() { // Issue #1261: see `tsx_ops` — TSX::String3 (type-keyword @@ -809,6 +834,7 @@ mod tests { // keyed `operators[Void]` — trips the assertion. This pins the lesson-4 // `n1 == dedupe(ops.operators)` invariant for the two `void` forms in // one file. + #[cfg(feature = "typescript")] #[test] fn typescript_void_return_and_expression_single_operator_453() { check_ops( @@ -820,6 +846,7 @@ mod tests { ); } + #[cfg(feature = "typescript")] #[test] fn tsx_void_return_and_expression_single_operator_453() { check_ops( @@ -831,6 +858,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_ops() { check_ops( @@ -864,6 +892,7 @@ mod tests { ); } + #[cfg(feature = "java")] #[test] fn java_primitive_ops() { check_ops( @@ -1235,6 +1264,9 @@ mod tests { /// hoisting it back out a failure. The node-count assertion is what /// makes the counts distinguishable — a fixture whose nodes and /// spaces were equal in number could not tell the two apart. + // test-lang-gates: hand-written(rust) — the one grammar that + // makes the fixture list non-empty, so the non-vacuity + // assertion cannot fire on a minimal build #[test] // Gated on the language that guarantees a non-empty case list, so // the emptiness assertion below cannot fire on a minimal build. diff --git a/src/output/dump.rs b/src/output/dump.rs index 8098e3b5d..17b62e5ca 100644 --- a/src/output/dump.rs +++ b/src/output/dump.rs @@ -372,6 +372,7 @@ mod tests { use super::*; + #[cfg(feature = "cpp")] #[test] fn dump_node_non_utf8_source_emits_the_raw_snippet() { // Regression: `stdout.write_all(code).unwrap()` panicked when the @@ -423,6 +424,7 @@ mod tests { assert_eq!(Connector::Inner.glyphs(), ("│ ", "├─ ")); } + #[cfg(feature = "cpp")] #[test] fn start_connector_distinguishes_parentless_from_parented() { // `start_connector` is the walk's only `Node::parent` call @@ -442,6 +444,7 @@ mod tests { assert_eq!(start_connector(&child), Connector::Last); } + #[cfg(feature = "cpp")] #[test] fn dump_output_matches_expected_tree() { // Byte-exact guard that the split preserves the rendered tree. @@ -467,6 +470,7 @@ mod tests { /// Render `node` to an in-memory sink under the given line filter and /// return the raw bytes. Not necessarily UTF-8: a non-UTF-8 source /// snippet is written through verbatim by `write_node_snippet`. + #[cfg(feature = "cpp")] fn render_raw( code: &[u8], node: &Node, @@ -502,6 +506,7 @@ mod tests { /// a hoisted cursor records **zero** and a per-node one records once /// per interior node. The exact zero is the discriminator; a /// fraction-of-nodes bound would hold for either on a small fixture. + #[cfg(feature = "cpp")] #[test] fn dump_holds_one_cursor_for_the_whole_tree() { let parser = CppParser::new( @@ -529,6 +534,7 @@ mod tests { } /// [`render_raw`] as text, for the (usual) UTF-8 case. + #[cfg(feature = "cpp")] fn render_range( code: &[u8], node: &Node, @@ -541,10 +547,12 @@ mod tests { } /// [`render_range`] with the filter disabled — the `bca dump` default. + #[cfg(feature = "cpp")] fn render(code: &[u8], node: &Node, depth: i32) -> String { render_range(code, node, depth, None, None) } + #[cfg(feature = "cpp")] #[test] fn dump_output_restores_prefix_after_nested_subtree() { // The walk keeps one shared prefix buffer that is appended to on @@ -582,6 +590,7 @@ mod tests { assert_eq!(rendered, expected); } + #[cfg(feature = "cpp")] #[test] fn dump_output_from_a_parented_start_node_indents_as_a_last_child() { // `bca find` dumps the matched node, not the file root, so the @@ -614,6 +623,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn dump_output_line_range_filters_rows() { // A tight `[2, 2]` range hides every node whose start row is 1, @@ -634,6 +644,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn deeply_nested_ast_dumps_without_stack_overflow() { // The dump walk is iterative (#700): a pathologically deep AST — @@ -681,6 +692,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn dump_output_depth_limits_recursion() { // `bca find` dumps with depth=1 @@ -727,6 +739,7 @@ mod tests { /// The fixture is deliberately the smallest tree that still nests: /// the sweep re-runs the whole dump once per write position, so cost /// is quadratic in the node count. + #[cfg(feature = "cpp")] #[test] fn every_write_position_propagates_an_io_error() { let code = b"int a = 42;\n"; diff --git a/src/output/dump_metrics.rs b/src/output/dump_metrics.rs index bda28e0bf..9446c92e3 100644 --- a/src/output/dump_metrics.rs +++ b/src/output/dump_metrics.rs @@ -308,6 +308,7 @@ mod tests { String::from_utf8(buf.into_inner()).expect("utf-8 dump") } + #[cfg(feature = "cpp")] #[test] fn fields_after_a_nested_metric_object_resume_the_group_rail() { // `cyclomatic.modified` is the one metric group that nests @@ -423,6 +424,7 @@ mod tests { ); } + #[cfg(feature = "cpp")] #[test] fn selection_mask_omits_unselected_metric_groups() { // `with_only(&[Loc])` must restrict the dump to the loc group: @@ -445,6 +447,7 @@ mod tests { } } + #[cfg(feature = "cpp")] #[test] fn last_emitted_metric_group_uses_closing_connector() { // The genuinely-last emitted metric group must carry the closing @@ -536,6 +539,7 @@ mod tests { /// the dump — i.e. the root `Unit`'s, which is emitted before any child /// space. `{val}` Display renders whole f64s without a decimal point, so /// callers can compare against `"0"`. + #[cfg(feature = "rust")] fn root_block_field(out: &str, block: &str, field: &str) -> String { let body = &out[out .find(&format!("{block}\n")) @@ -549,6 +553,7 @@ mod tests { /// matching the JSON serializer and `Display`), not the space's IMMEDIATE /// counts — which are 0 at any parent whose functions all live in a nested /// module/impl, and would not sum to the aggregate `total`. + #[cfg(feature = "rust")] #[test] fn dump_nom_and_nargs_use_subtree_aggregates_at_parent_space() { // The one function (with args) is nested in `mod m`, so the root Unit's @@ -583,6 +588,7 @@ mod tests { /// underscore key that matches the JSON/CSV key name, so a user can grep /// the same token across `dump` and JSON. The space-separated forms /// (`estimated program length` / `purity ratio`) were the only outliers. + #[cfg(feature = "cpp")] #[test] fn dump_halstead_labels_use_underscore_keys() { let space = analyze( diff --git a/src/spaces_tests.rs b/src/spaces_tests.rs index 027edb9f8..00ee0b411 100644 --- a/src/spaces_tests.rs +++ b/src/spaces_tests.rs @@ -44,6 +44,7 @@ fn space_kind_non_exhaustive_serde_roundtrip_unchanged() { /// at the predicate call sites in /// `big-code-analysis-ast/src/checker.rs` and /// `big-code-analysis-ast/src/getter.rs` — see issue #285. +#[cfg(feature = "cpp")] #[test] fn cpp_function_definition_is_classified_as_function() { use crate::Cpp; @@ -90,6 +91,7 @@ fn cpp_function_definition_is_classified_as_function() { ); } +#[cfg(feature = "cpp")] #[test] fn cpp_scope_resolution_operator() { check_func_space::( @@ -111,6 +113,7 @@ fn cpp_scope_resolution_operator() { /// happens for parts of DeepSpeech's KenLM and OpenFst sources), the /// top-level `FuncSpace` must still be a `Unit` spanning the whole /// file, with `blank >= 0` and `sloc >= ploc`. +#[cfg(feature = "cpp")] #[test] fn cpp_error_root_yields_unit_top_level_space() { // This snippet (a chunk of kenlm/lm/model.hh shape) is rejected by @@ -190,6 +193,29 @@ fn cpp_error_root_yields_unit_top_level_space() { /// wrapper path. Issue #220 tracks finding additional per-grammar /// fixtures that surface ERROR roots so each language can have /// both a contract test and a wrapper-exercising test. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn assert_top_level_space_is_unit_contract(source: &str, filename: &str) { let path = std::path::PathBuf::from(filename); let parser = P::new(source.as_bytes().to_vec(), &path, None); @@ -227,6 +253,7 @@ fn assert_top_level_space_is_unit_contract(source: &str, filenam /// the contract-only path. Use this for languages where a fixture /// is known to make the grammar return ERROR (currently: Lua, C++ /// via mozcpp). +#[cfg(feature = "lua")] fn assert_partial_input_yields_synthetic_unit_wrapper( source: &str, filename: &str, @@ -240,6 +267,7 @@ fn assert_partial_input_yields_synthetic_unit_wrapper( assert_top_level_space_is_unit_contract::

(source, filename); } +#[cfg(feature = "python")] #[test] fn python_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -248,6 +276,7 @@ fn python_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "javascript")] #[test] fn javascript_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -256,6 +285,7 @@ fn javascript_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "mozjs")] #[test] fn mozjs_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -264,6 +294,7 @@ fn mozjs_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "typescript")] #[test] fn typescript_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -272,6 +303,7 @@ fn typescript_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "typescript")] #[test] fn tsx_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -280,6 +312,7 @@ fn tsx_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "java")] #[test] fn java_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -288,6 +321,7 @@ fn java_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "kotlin")] #[test] fn kotlin_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -296,6 +330,7 @@ fn kotlin_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "go")] #[test] fn go_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -304,6 +339,7 @@ fn go_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "rust")] #[test] fn rust_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -312,6 +348,7 @@ fn rust_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "csharp")] #[test] fn csharp_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -325,6 +362,7 @@ fn csharp_top_level_space_is_unit_contract() { /// `Class` (matching Java/PHP/Groovy) rather than letting it fall /// through to `SpaceKind::Unknown`. The enum is the only declared /// space, so it appears as a direct child of the top-level Unit. +#[cfg(feature = "csharp")] #[test] fn csharp_enum_space_kind_is_class() { let src = "enum Color { Red, Green, Blue }\n"; @@ -342,6 +380,7 @@ fn csharp_enum_space_kind_is_class() { assert_eq!(enum_space.name.as_deref(), Some("Color")); } +#[cfg(feature = "bash")] #[test] fn bash_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -357,6 +396,7 @@ fn bash_top_level_space_is_unit_contract() { /// The 16 sibling `*_top_level_space_is_unit_contract` tests /// only pin the public-API contract; only this and the C++ test /// actually trigger the wrapper code path. See #220. +#[cfg(feature = "lua")] #[test] fn lua_partial_input_yields_synthetic_unit_wrapper() { assert_partial_input_yields_synthetic_unit_wrapper::( @@ -365,6 +405,7 @@ fn lua_partial_input_yields_synthetic_unit_wrapper() { ); } +#[cfg(feature = "tcl")] #[test] fn tcl_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -377,6 +418,7 @@ fn tcl_top_level_space_is_unit_contract() { /// mid-body) must still yield a `Unit` top-level space. Like Tcl, the /// grammar keeps `source_file` as the root with an inner `ERROR`, so /// this pins the contract rather than the synthetic-Unit wrapper path. +#[cfg(feature = "irules")] #[test] fn irules_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -385,6 +427,7 @@ fn irules_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "perl")] #[test] fn perl_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -393,6 +436,7 @@ fn perl_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "php")] #[test] fn php_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -401,6 +445,7 @@ fn php_top_level_space_is_unit_contract() { ); } +#[cfg(feature = "elixir")] #[test] fn elixir_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -416,6 +461,7 @@ fn elixir_top_level_space_is_unit_contract() { // tree-sitter-elixir grammar wraps the head in an `Arguments` // node, so every promoted Class / Function space was labelled // `` despite the source carrying a name. +#[cfg(feature = "elixir")] #[test] fn elixir_func_space_names_resolve_through_arguments_wrapper() { let src = "defmodule Foo.Bar do\n def hello(x), do: x\n defp helper, do: :ok\n defmodule Inner do\n def i, do: 1\n end\nend\n"; @@ -463,6 +509,12 @@ fn elixir_func_space_names_resolve_through_arguments_wrapper() { /// `ParserTrait` API, so the lesson-9 contract must hold for them /// too — a grammar bump promoting an inner construct to root would /// otherwise produce a non-`Unit` file-level space. +#[cfg(any( + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "mozcpp" +))] #[test] fn preproc_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -471,6 +523,12 @@ fn preproc_top_level_space_is_unit_contract() { ); } +#[cfg(any( + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "mozcpp" +))] #[test] fn ccomment_top_level_space_is_unit_contract() { assert_top_level_space_is_unit_contract::( @@ -484,6 +542,7 @@ fn ccomment_top_level_space_is_unit_contract() { /// path is unreachable today. The test pins the contract so a /// future grammar bump that starts promoting an inner kind to /// root would fail here. +#[cfg(feature = "ruby")] #[test] fn ruby_top_level_space_is_unit_contract() { // Truncated method definition (missing `end`) plus an @@ -511,6 +570,7 @@ fn ruby_top_level_space_is_unit_contract() { /// string the caller passed, byte-for-byte. This is the /// post-#254 contract: callers analysing in-memory snippets no /// longer need a `Path` to identify the resulting `FuncSpace`. +#[cfg(feature = "cpp")] #[test] fn analyze_in_memory_snippet_carries_caller_supplied_name() { use crate::{Source, analyze}; @@ -529,6 +589,7 @@ fn analyze_in_memory_snippet_carries_caller_supplied_name() { /// `analyze` with `Source::name = None` leaves the top-level /// `FuncSpace::name` as `None`. The pre-#254 entry points always /// forced a `Some(...)`; the new API lets callers opt out. +#[cfg(feature = "cpp")] #[test] fn analyze_without_name_leaves_top_level_name_none() { use crate::{Source, analyze}; @@ -1322,6 +1383,7 @@ mod exclude_tests_non_rust { use crate::{CppParser, MetricsOptions, ParserTrait}; use std::path::PathBuf; + #[cfg(feature = "cpp")] #[test] fn cpp_ignores_exclude_tests_flag() { let source = "\ @@ -1361,12 +1423,14 @@ int helper() { return 2; } mod with_only { use crate::{LANG, Metric, MetricSet, MetricsOptions, Source, analyze}; + #[cfg(feature = "rust")] const SOURCE: &str = "\ fn prod(x: i32) -> i32 { if x > 0 { x + 1 } else { x - 1 } } "; + #[cfg(feature = "rust")] fn analyse(metrics: &[Metric]) -> crate::FuncSpace { let opts = MetricsOptions::default().with_only(metrics); analyze( @@ -1381,6 +1445,7 @@ fn prod(x: i32) -> i32 { // (cognitive / cyclomatic / halstead / ...) at their default // values. The dependent-metric anchors guard against the // walker silently running them anyway. + #[cfg(feature = "rust")] #[test] fn loc_only_skips_other_metrics() { let full = analyze( @@ -1412,6 +1477,7 @@ fn prod(x: i32) -> i32 { // (Loc + Cyclomatic + Halstead) — otherwise the MI formula // would compute against zero inputs and return a meaningless // score. + #[cfg(feature = "rust")] #[test] fn mi_auto_pulls_dependencies() { let pruned = analyse(&[Metric::Mi]); @@ -1450,6 +1516,7 @@ fn prod(x: i32) -> i32 { } // `with_only(&[Metric::Wmc])` auto-adds Cyclomatic + Nom. + #[cfg(feature = "rust")] #[test] fn wmc_auto_pulls_dependencies() { let pruned = analyse(&[Metric::Wmc]); @@ -1480,6 +1547,7 @@ fn prod(x: i32) -> i32 { // function with one `if` branch and one argument, so the // function count is exactly 1 and each average equals its // own sum. + #[cfg(feature = "rust")] #[test] fn cognitive_only_pulls_nom_and_average_is_finite() { let pruned = analyse(&[Metric::Cognitive]); @@ -1503,6 +1571,7 @@ fn prod(x: i32) -> i32 { assert_eq!(avg, 2.0); } + #[cfg(feature = "rust")] #[test] fn exit_only_pulls_nom_and_average_is_finite() { let pruned = analyse(&[Metric::Nexits]); @@ -1524,6 +1593,7 @@ fn prod(x: i32) -> i32 { assert_eq!(avg, 0.0); } + #[cfg(feature = "rust")] #[test] fn nargs_only_pulls_nom_and_average_is_finite() { let pruned = analyse(&[Metric::Nargs]); @@ -1545,6 +1615,7 @@ fn prod(x: i32) -> i32 { // `MetricsOptions::default()` selects every metric (#257's // default-preservation contract). + #[cfg(feature = "rust")] #[test] fn default_options_select_every_metric() { let full = analyze( @@ -1560,6 +1631,7 @@ fn prod(x: i32) -> i32 { // `metrics` object rather than the full payload so a future // additive change (new metric, new sub-field) doesn't shift // unrelated tests. + #[cfg(feature = "rust")] #[test] fn unselected_metrics_are_skipped_in_json() { let pruned = analyse(&[Metric::Loc]); @@ -1605,6 +1677,7 @@ fn prod(x: i32) -> i32 { // keyword scan, and `defmodule` / `def` promote to Class / // Function spaces whose kind would be lost if the lazy gate // skipped a node it shouldn't. + #[cfg(feature = "elixir")] #[test] fn elixir_loc_deselected_preserves_kinds_and_metrics() { use crate::SpaceKind; @@ -1694,6 +1767,7 @@ end // Empty slice = nothing selected. Every metric must be // elided from JSON output; the space tree is still // produced. + #[cfg(feature = "rust")] #[test] fn empty_slice_selects_nothing() { let pruned = analyse(&[]); @@ -1713,6 +1787,7 @@ end // `empty().with(Mi)` would otherwise compute the MI formula // against zero-valued Loc / Cyclomatic / Halstead inputs and // emit a meaningless score with no error. + #[cfg(feature = "rust")] #[test] fn with_metric_set_resolves_dependency_closure() { let unresolved = MetricSet::empty().with(Metric::Mi); @@ -1765,6 +1840,7 @@ end // An already-resolved set passes through `with_metric_set` // unchanged (idempotence at the builder level). + #[cfg(feature = "rust")] #[test] fn with_metric_set_passes_resolved_set_unchanged() { let resolved = MetricSet::from_slice_with_deps(&[Metric::Mi]); @@ -1810,6 +1886,8 @@ mod metric_selection_parity { // multi-clause `if` (cognitive, cyclomatic, abc, halstead, tokens), // an early `return` (nexits), parameters (nargs), and a comment // (loc). It is the fixture the non-vacuity assertion below leans on. + // test-lang-gates: hand-written(rust) — a source fixture for + // this grammar, and its text is a string #[cfg(feature = "rust")] const RUST: &str = "\ pub struct Counter { @@ -1838,6 +1916,8 @@ fn choose(a: u32, b: u32) -> u32 { } "; + // test-lang-gates: hand-written(java) — a source fixture for + // this grammar, and its text is a string #[cfg(feature = "java")] const JAVA: &str = "\ public class Shape { @@ -1853,6 +1933,8 @@ public class Shape { } "; + // test-lang-gates: hand-written(python) — a source fixture for + // this grammar, and its text is a string #[cfg(feature = "python")] const PYTHON: &str = "\ class Bag: @@ -1940,6 +2022,9 @@ class Bag: out } + // test-lang-gates: hand-written(rust) — the one grammar that + // makes the fixture list non-empty, so the non-vacuity + // assertion cannot fire on a minimal build #[test] // Gated on the language whose fixture makes the non-vacuity // assertion satisfiable for all thirteen metrics. @@ -2004,6 +2089,10 @@ class Bag: // here are feature-independent but live in the same cohesive module; the // canonical all-features test run (and the minimal-langs leg, which // enables `rust`) still exercises every one. +// test-lang-gates: hand-written(rust) — `from_path` picks the grammar +// from the file extension, so the language is a `"foo.rs"` string +// literal and nothing in these bodies names it. The one `LANG` +// mention is the `assert_eq!` checking the detection worked. #[cfg(feature = "rust")] mod from_path_tests { use crate::{Ast, FromPathError, LANG, MetricsOptions, SpaceKind}; @@ -2101,6 +2190,7 @@ mod from_path_tests { /// cognitive, cyclomatic, halstead, loc, nom, tokens, mi — each /// `writeln!`-separated, with `mi` last and no trailing newline. Pin /// that contract so a future reorder or stray newline is caught. +#[cfg(feature = "cpp")] #[test] fn code_metrics_display_concatenates_reported_submetrics_in_order() { use crate::{Source, analyze}; @@ -2177,6 +2267,7 @@ fn ast_debug_reports_language_and_name_non_exhaustively() { /// names *and* kinds for an `@interface`, an `@implementation` + its /// method, and a free function, so an ObjC naming regression cannot /// hide behind a vacuous metric assertion (#724; lessons 2 & 31). +#[cfg(feature = "objc")] #[test] fn objc_func_space_tree_carries_names_and_kinds() { use crate::ObjcParser; @@ -2358,11 +2449,29 @@ mod nameless_construct_spaces { use crate::test_support::space_verbatim; use crate::{FuncSpace, LANG, MetricsOptions, SpaceKind}; + #[cfg(any( + feature = "groovy", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "mozjs", + feature = "ruby", + feature = "typescript", + ))] fn analyse(lang: LANG, source: &str) -> FuncSpace { space_verbatim(lang, source.as_bytes(), MetricsOptions::default()) } /// The `(name, kind)` of every descendant space, in preorder. + #[cfg(any( + feature = "groovy", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "mozjs", + feature = "ruby", + feature = "typescript", + ))] fn shape(space: &FuncSpace) -> Vec<(Option<&str>, SpaceKind)> { let mut out = vec![(space.name.as_deref(), space.kind)]; for child in &space.spaces { @@ -2371,6 +2480,15 @@ mod nameless_construct_spaces { out } + #[cfg(any( + feature = "groovy", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "mozjs", + feature = "ruby", + feature = "typescript", + ))] fn child<'a>(space: &'a FuncSpace, name: &str) -> &'a FuncSpace { fn find<'a>(s: &'a FuncSpace, name: &str) -> Option<&'a FuncSpace> { if s.name.as_deref() == Some(name) { diff --git a/src/suppression.rs b/src/suppression.rs index 0f9ec9a6f..587ea5448 100644 --- a/src/suppression.rs +++ b/src/suppression.rs @@ -1474,11 +1474,13 @@ mod tests { use std::path::PathBuf; /// Collect markers from a Rust snippet via the public collector. + #[cfg(feature = "rust")] fn rust_markers(src: &str) -> Vec { let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.rs"), None); suppression_markers(&parser) } + #[cfg(feature = "rust")] #[test] fn collector_function_scoped_native_marker_attributes_enclosing_fn() { // The marker sits inside `do_thing`'s body, so the audit must @@ -1495,6 +1497,7 @@ mod tests { assert_eq!(m.function.as_deref(), Some("do_thing")); } + #[cfg(feature = "rust")] #[test] fn collector_metric_list_scope_is_preserved() { let src = "fn f() {\n // bca: suppress(cyclomatic, cognitive)\n}\n"; @@ -1508,6 +1511,7 @@ mod tests { assert_eq!(metrics.len(), 2); } + #[cfg(feature = "rust")] #[test] fn collector_file_scoped_marker_has_no_enclosing_fn() { // A `suppress-file` marker is whole-file by definition; the @@ -1520,6 +1524,7 @@ mod tests { assert_eq!(markers[0].function, None); } + #[cfg(feature = "rust")] #[test] fn collector_nested_fn_attributes_innermost() { // The marker is inside the inner function; attribution must pick @@ -1530,6 +1535,7 @@ mod tests { assert_eq!(markers[0].function.as_deref(), Some("inner")); } + #[cfg(feature = "rust")] #[test] fn collector_marker_outside_any_fn_has_no_enclosing_fn() { // A function-scoped marker with no enclosing function silences @@ -1542,6 +1548,7 @@ mod tests { assert_eq!(markers[0].function, None); } + #[cfg(feature = "rust")] #[test] fn collector_recognizes_lizard_dialect() { let src = "fn f() {\n // #lizard forgives\n}\n"; @@ -1551,6 +1558,7 @@ mod tests { assert_eq!(markers[0].function.as_deref(), Some("f")); } + #[cfg(feature = "rust")] #[test] fn collector_markers_sorted_by_line() { let src = "fn a() {\n // bca: suppress\n}\nfn b() {\n // bca: suppress\n}\n"; @@ -1561,6 +1569,7 @@ mod tests { assert_eq!(markers[1].function.as_deref(), Some("b")); } + #[cfg(feature = "python")] #[test] fn collector_python_hash_marker() { let src = "def helper():\n # bca: suppress\n pass\n"; @@ -1571,6 +1580,7 @@ mod tests { assert_eq!(markers[0].function.as_deref(), Some("helper")); } + #[cfg(feature = "cpp")] #[test] fn collector_cpp_attributes_enclosing_function() { // Cross-language coverage: C++ functions are detected and the @@ -1583,6 +1593,7 @@ mod tests { assert_eq!(markers[0].function.as_deref(), Some("compute")); } + #[cfg(feature = "elixir")] #[test] fn collector_elixir_requires_code_aware_func_predicate() { // Elixir is the language whose `Checker::is_func` returns `false` @@ -1600,6 +1611,7 @@ mod tests { assert_eq!(markers[0].function.as_deref(), Some("parse_long")); } + #[cfg(feature = "rust")] #[test] fn collector_empty_source_yields_no_markers() { assert!(rust_markers("").is_empty()); @@ -1625,6 +1637,7 @@ mod tests { /// /// Without this, every comment the collector's tests feed it parses /// successfully, and the reject arm is never taken. + #[cfg(feature = "rust")] #[test] fn collector_skips_comments_that_are_not_valid_markers() { let src = "// an ordinary comment\n\ @@ -1661,6 +1674,7 @@ mod tests { /// part of the marker adjacent to the missing newline, so a future /// parser that indexed past the `)` unconditionally would fail here /// and nowhere else. + #[cfg(feature = "rust")] #[test] fn rationale_marker_at_eof_without_trailing_newline() { let space = crate::test_support::space_verbatim( @@ -1681,6 +1695,7 @@ mod tests { /// list. Pinned because the pre-#1168 parser reached the same answer /// for the opposite reason: it trimmed the `\r` off a body that had /// nothing after the `)` at all. + #[cfg(feature = "rust")] #[test] fn rationale_marker_survives_crlf_line_endings() { let space = crate::test_support::space_verbatim( @@ -1703,6 +1718,7 @@ mod tests { ); } + #[cfg(feature = "rust")] #[test] fn collector_lists_a_marker_whose_list_was_partly_unusable() { // The audit reports the suppression that is *in force*. Since diff --git a/src/test_support.rs b/src/test_support.rs index a8a6ba4b9..7ed7827e1 100644 --- a/src/test_support.rs +++ b/src/test_support.rs @@ -317,6 +317,12 @@ pub(crate) fn assert_fixture_spells( /// The C# binding of [`assert_fixture_spells`]. Every caller passes a /// `foo.cs` fixture, so the parser and path are fixed here rather than /// repeated at each one. +// Gated to match the Perl binding below: every caller of this one is a +// `#[cfg(feature = "csharp")]` test now too, so the definition says the +// same thing they do. `lib.rs`'s `allow(dead_code)` would have kept a +// partial build quiet either way — the gate is for the reader and for +// the derivation, not for a lint (#1472). +#[cfg(feature = "csharp")] #[track_caller] pub(crate) fn assert_csharp_fixture_spells(src: &str, kinds: &[(u16, usize, &str)]) { assert_fixture_spells::(src, "foo.cs", kinds); @@ -332,4 +338,12 @@ pub(crate) fn assert_perl_fixture_spells(src: &str, kinds: &[(u16, usize, &str)] // The parse-only helpers live beside the parse layer and are shared with // its own tests through the `test-support` feature. +// +// Left ungated on purpose: these are re-exported to call sites across +// seven metric modules, so which build uses them depends on which +// per-language tests it compiled, and enumerating that union here would +// be a hand-maintained copy of seven files' gates. The crate-level +// `cfg_attr` in `lib.rs` silences the partial builds; an item-level +// `allow` on top of it would be live only in the *full* build, which is +// the one that should still police this (#1472). pub(crate) use big_code_analysis_ast::test_support::{ast_has_kind_id, for_each_node_with_chain}; diff --git a/src/wire.rs b/src/wire.rs index 8087df76f..5d69764c8 100644 --- a/src/wire.rs +++ b/src/wire.rs @@ -664,6 +664,7 @@ mod tests { /// A branchy multi-function Rust fixture so several metrics are /// non-trivial (cyclomatic > 1, multiple spaces, real Halstead/MI). + #[cfg(feature = "rust")] const FIXTURE: &str = "\ fn classify(x: i32) -> i32 { if x > 0 { @@ -687,6 +688,7 @@ fn run() { /// so a swap corrupts them identically — so the round-trip tests anchor /// against these known values to break the closed loop. (Grammar bumps /// may shift them; update alongside the metric snapshot tests.) + #[cfg(feature = "rust")] fn assert_fixture_oracle(tree: &FuncSpace) { // Two top-level functions: `classify` and `run`. assert_eq!(tree.kind, SpaceKind::Unit); @@ -745,6 +747,7 @@ fn run() { /// back into a `wire::FuncSpace` that re-serializes byte-for-byte, is /// structurally equal to the source projection, and carries the /// hand-verified metric values. + #[cfg(feature = "rust")] #[test] fn json_round_trips() { check_func_space::(FIXTURE, "fixture.rs", |fs| { @@ -766,6 +769,7 @@ fn run() { }); } + #[cfg(feature = "rust")] #[test] fn yaml_round_trips() { check_func_space::(FIXTURE, "fixture.rs", |fs| { @@ -776,6 +780,7 @@ fn run() { }); } + #[cfg(feature = "rust")] #[test] fn toml_round_trips() { check_func_space::(FIXTURE, "fixture.rs", |fs| { @@ -786,6 +791,7 @@ fn run() { }); } + #[cfg(feature = "rust")] #[test] fn cbor_round_trips() { check_func_space::(FIXTURE, "fixture.rs", |fs| { @@ -927,6 +933,7 @@ fn run() { /// `selected()` reconstructs the `MetricSet` from the metric keys /// present on the wire: a full tree marks every metric, a pruned tree /// (here keeping only `loc`) marks exactly that one. + #[cfg(feature = "rust")] #[test] fn selected_is_inferred_from_present_keys() { check_func_space::(FIXTURE, "fixture.rs", |fs| { @@ -962,6 +969,7 @@ fn run() { /// The size of a `bca` consumer thread and of a `tokio` blocking /// thread — the stack the guarded limits are dimensioned against. + #[cfg(feature = "rust")] const PRODUCTION_STACK: usize = 2 * 1024 * 1024; /// Deliberately far below `PRODUCTION_STACK`: a re-recursed `From` or @@ -985,6 +993,7 @@ fn run() { /// Analyses [`nested_functions`], computing only `loc` so the cost of /// unrelated metrics does not dominate a deep fixture. + #[cfg(feature = "rust")] fn analyze_nested(depth: usize) -> crate::FuncSpace { crate::analyze( crate::Source::new(crate::LANG::Rust, nested_functions(depth).as_bytes()) @@ -1046,6 +1055,7 @@ fn run() { root } + #[cfg(feature = "rust")] #[test] fn deeply_nested_spaces_convert_to_wire_without_stack_overflow() { // `From<&spaces::FuncSpace>` walks an explicit work stack: the @@ -1076,6 +1086,7 @@ fn run() { assert_eq!(depth, DEPTH + 1, "the whole chain must survive conversion"); } + #[cfg(feature = "rust")] #[test] fn spaces_deeper_than_the_limit_fail_serialization_rather_than_abort() { // The reported symptom: `bca metrics -O json` on ~1 000 nested @@ -1095,6 +1106,7 @@ fn run() { ); } + #[cfg(feature = "rust")] #[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 @@ -1134,6 +1146,7 @@ fn run() { ); } + #[cfg(feature = "rust")] #[test] fn deeply_nested_ops_convert_and_serialize_without_stack_overflow() { // `Ops` mirrors `FuncSpace`'s nesting and had the same recursive diff --git a/src/wire_ops_tests.rs b/src/wire_ops_tests.rs index 83b657202..af5a337dd 100644 --- a/src/wire_ops_tests.rs +++ b/src/wire_ops_tests.rs @@ -63,6 +63,9 @@ fn parse_ops(lang: crate::LANG, source: &str) -> ops::Ops { /// This is what keeps them from drifting: a field renamed, reordered, /// retyped, or given a different `skip_serializing_if` on one side /// fails here. +// test-lang-gates: hand-written(rust) — the one grammar that makes the +// fixture list non-empty, so the non-vacuity assertion cannot fire +// on a minimal build #[test] // Gated on the language that guarantees a non-empty fixture list, so // the emptiness assertion below cannot fire on a minimal build. diff --git a/tests/api/ast_seam_test.rs b/tests/api/ast_seam_test.rs index 80664c23f..02ef30cae 100644 --- a/tests/api/ast_seam_test.rs +++ b/tests/api/ast_seam_test.rs @@ -224,6 +224,7 @@ fn metrics_can_be_recomputed_with_different_selections() { // `Rc`, `RefCell`, a raw `*mut`, or any non-`Sync` smart pointer // would silently strip the auto-trait — and this assertion would then // fail to compile, alerting the author before the docs go out of sync. +#[cfg(feature = "rust")] const _: fn() = || { fn assert_send_sync() {} assert_send_sync::(); @@ -762,10 +763,14 @@ fn preprocess_harvest_feeds_the_macro_masking_pass() { /// failing (`.claude/rules/testing.md`, "Perturb the fixture as well as /// the production line"). Inside, the `proc` is load-bearing for the /// expected sequence. +// test-lang-gates: hand-written(tcl) — a source fixture for this +// grammar, and its text is a string #[cfg(feature = "tcl")] const TCL_SCRIPT_AND_LITERALS: &str = "proc p {x} { puts \"q\" }\nlappend l {a b}\n"; /// The iRules twin, which already had the quoted word inside the body. +// test-lang-gates: hand-written(irules) — a source fixture for this +// grammar, and its text is a string #[cfg(feature = "irules")] const IRULES_SCRIPT_AND_LITERALS: &str = "when HTTP_REQUEST { log local0. \"hi\" }\nlappend l {x y}\n"; diff --git a/tests/api/book_ast_traversal_examples.rs b/tests/api/book_ast_traversal_examples.rs index 9eb029de0..a86715cab 100644 --- a/tests/api/book_ast_traversal_examples.rs +++ b/tests/api/book_ast_traversal_examples.rs @@ -12,6 +12,7 @@ use std::collections::HashMap; use big_code_analysis::{Ast, AstNode, LANG, MetricsOptions, Source, tree_sitter}; /// Recursive `kind` finder used by the [`AstCallback`] test below. +#[cfg(feature = "rust")] fn ast_node_contains_kind(node: &AstNode, kind: &str) -> bool { node.r#type == kind || node @@ -21,6 +22,7 @@ fn ast_node_contains_kind(node: &AstNode, kind: &str) -> bool { } /// Visit every node in `tree` in pre-order, root first. +#[cfg(feature = "rust")] fn walk_preorder)>(tree: &tree_sitter::Tree, mut visit: F) { let mut cursor = tree.walk(); 'walk: loop { @@ -39,6 +41,7 @@ fn walk_preorder)>(tree: &tree_sitter::Tree, mut } } +#[cfg(feature = "rust")] #[test] fn count_nodes_by_kind() { let ast = Ast::parse(Source::new( @@ -56,6 +59,7 @@ fn count_nodes_by_kind() { assert_eq!(counts.get("for_expression").copied().unwrap_or(0), 1); } +#[cfg(feature = "rust")] #[test] fn find_unsafe_blocks() { let ast = Ast::parse(Source::new( @@ -89,12 +93,14 @@ fn find_unsafe_blocks() { assert_eq!((start_row, end_row), (0, 0)); } +#[cfg(feature = "rust")] #[test] fn detect_parse_error_on_root() { let ast = Ast::parse(Source::new(LANG::Rust, b"fn broken(")).expect("rust feature enabled"); assert!(ast.as_tree_sitter().root_node().has_error()); } +#[cfg(feature = "rust")] #[test] fn enumerate_parse_error_lines() { let ast = Ast::parse(Source::new(LANG::Rust, b"fn broken(")).expect("rust feature enabled"); @@ -116,6 +122,7 @@ fn enumerate_parse_error_lines() { ); } +#[cfg(feature = "rust")] #[test] fn metrics_plus_symbol_table_one_parse() { let ast = Ast::parse(Source::new( @@ -143,6 +150,7 @@ fn metrics_plus_symbol_table_one_parse() { assert_eq!(functions, ["outer", "inner", "alone"]); } +#[cfg(feature = "rust")] #[test] fn ast_dump_produces_serializable_tree() { use big_code_analysis::{Ast, AstCfg, AstPayload, Source}; diff --git a/tests/api/book_library_examples.rs b/tests/api/book_library_examples.rs index 42f757251..7603a094f 100644 --- a/tests/api/book_library_examples.rs +++ b/tests/api/book_library_examples.rs @@ -29,6 +29,7 @@ fn in_memory_analyze_buffer() { } /// `walking-funcspace.md` — "Recursive walk". +#[cfg(feature = "rust")] fn hotspots(space: &FuncSpace, threshold: u64, out: &mut Vec) { if space.kind == SpaceKind::Function && space.metrics.cognitive.cognitive_sum() > threshold @@ -44,6 +45,7 @@ fn hotspots(space: &FuncSpace, threshold: u64, out: &mut Vec) { } } +#[cfg(feature = "rust")] #[test] fn walking_funcspace_hotspots() { let source = b"\ @@ -64,6 +66,7 @@ fn hard(x: i32) -> i32 { } /// `reuse-tree.md` — "Working example". +#[cfg(feature = "rust")] #[test] fn reuse_tree_working_example() { use big_code_analysis::{Ast, tree_sitter}; diff --git a/tests/api/derive_eq_hash_ord.rs b/tests/api/derive_eq_hash_ord.rs index cb7258432..f114c16dd 100644 --- a/tests/api/derive_eq_hash_ord.rs +++ b/tests/api/derive_eq_hash_ord.rs @@ -14,6 +14,7 @@ use big_code_analysis::metric_catalog::Direction; use big_code_analysis::{FuncSpace, LANG, MetricsOptions, Severity, Source, SpaceKind, analyze}; /// Analyze a Rust snippet via the public `analyze` entry point. +#[cfg(feature = "rust")] fn analyze_rust(source: &str) -> FuncSpace { analyze( Source::new(LANG::Rust, source.as_bytes()).with_name(Some("eq.rs".to_string())), @@ -22,6 +23,7 @@ fn analyze_rust(source: &str) -> FuncSpace { .expect("parser produced no FuncSpace for fixture") } +#[cfg(feature = "rust")] const SRC_A: &str = r#"fn classify(x: u8) -> &'static str { if x > 10 && x < 100 { "mid" @@ -33,9 +35,11 @@ const SRC_A: &str = r#"fn classify(x: u8) -> &'static str { } "#; +#[cfg(feature = "rust")] const SRC_B: &str = r"fn noop() {} "; +#[cfg(feature = "rust")] #[test] fn cognitive_stats_partial_eq_same_and_different_source() { let a1 = analyze_rust(SRC_A).metrics.cognitive.clone(); @@ -46,6 +50,7 @@ fn cognitive_stats_partial_eq_same_and_different_source() { assert_ne!(a1, b, "different source must yield unequal cognitive Stats"); } +#[cfg(feature = "rust")] #[test] fn halstead_stats_partial_eq_same_and_different_source() { let a1 = analyze_rust(SRC_A).metrics.halstead.clone(); @@ -56,6 +61,7 @@ fn halstead_stats_partial_eq_same_and_different_source() { assert_ne!(a1, b, "different source must yield unequal halstead Stats"); } +#[cfg(feature = "rust")] #[test] fn loc_stats_partial_eq_same_and_different_source() { let a1 = analyze_rust(SRC_A).metrics.loc.clone(); @@ -66,6 +72,7 @@ fn loc_stats_partial_eq_same_and_different_source() { assert_ne!(a1, b, "different source must yield unequal loc Stats"); } +#[cfg(feature = "rust")] #[test] fn code_metrics_and_func_space_partial_eq() { let a1 = analyze_rust(SRC_A); diff --git a/tests/api/main.rs b/tests/api/main.rs index f1327d7b3..559878c4a 100644 --- a/tests/api/main.rs +++ b/tests/api/main.rs @@ -14,9 +14,22 @@ //! declaration here instead, so this file's `//!` doc stays ungated for //! the no-default-features and minimal-langs CI legs. +// Per-language test gating (#1472) makes "is this import live" a +// function of the enabled feature set, which no `cfg` on the import +// itself can express. Partial builds only — the build CI gates on and +// the one a contributor runs still police every unused import. Dead +// *items* are not relaxed: unlike the two library roots this test crate +// carries no `allow(dead_code)`, so every helper, `const`, macro and +// test here still needs its own gate. See `.claude/rules/testing.md`, +// "Why the import lint is off on a partial build". +#![cfg_attr(not(feature = "all-languages"), allow(unused_imports))] mod ast_seam_test; +// test-lang-gates: hand-written(rust) — a `mod` declaration has no body +// to read; the grammar is named inside the file it points at #[cfg(feature = "rust")] mod book_ast_traversal_examples; +// test-lang-gates: hand-written(rust) — a `mod` declaration has no body +// to read; the grammar is named inside the file it points at #[cfg(feature = "rust")] mod book_library_examples; mod derive_eq_hash_ord; @@ -24,6 +37,15 @@ mod derive_eq_hash_ord; // file used to carry: as a module it can be gated at the declaration, // and keeping the wrapper would have nested `parser_reuse` inside // itself. -#[cfg(all(feature = "rust", feature = "typescript"))] +// `rust` alone. Every test in the file needs the Rust grammar — three +// of them need TypeScript as well and say so individually — so +// conjoining TypeScript here dropped the other two from every Rust-only +// build. That is the over-gating #1478 is about, and it had been in the +// tree the whole time. `any(rust, typescript)` would be wrong the other +// way: under TypeScript alone the module has no tests at all and its +// shared fixtures go dead. +// test-lang-gates: hand-written(rust) — a `mod` declaration has no body +// to read; the grammar is named inside the file it points at +#[cfg(feature = "rust")] mod parser_reuse; mod suppression_test; diff --git a/tests/api/parser_reuse.rs b/tests/api/parser_reuse.rs index b77b36006..a7f605e15 100644 --- a/tests/api/parser_reuse.rs +++ b/tests/api/parser_reuse.rs @@ -33,6 +33,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use big_code_analysis::{Ast, LANG, Source, tree_sitter}; +#[cfg(any(feature = "rust", feature = "typescript"))] const RUST_SRC: &str = r#" fn classify(n: i32) -> &'static str { if n < 0 { @@ -53,6 +54,7 @@ impl Point { } "#; +#[cfg(any(feature = "rust", feature = "typescript"))] const TS_SRC: &str = r" function classify(n: number): string { if (n < 0) { @@ -74,12 +76,14 @@ class Point { /// Rust source the grammar cannot parse cleanly. Error recovery is /// what leaves the most state behind on a parser, so this is the /// worst thing to have parsed just before the file under test. +#[cfg(feature = "rust")] const BROKEN_SRC: &str = "fn oops( { let ] = ; if while }} impl for 42"; /// Parses `code` on a parser built for this one call, bypassing the /// thread-local slot entirely. This is the oracle every assertion /// below compares against — comparing two `Ast::parse` results to /// each other would pass even if both were wrong. +#[cfg(any(feature = "rust", feature = "typescript"))] fn reference_sexp(lang: LANG, code: &str) -> String { let language = lang .tree_sitter_language() @@ -96,6 +100,7 @@ fn reference_sexp(lang: LANG, code: &str) -> String { /// Parses `code` through the public seam, which routes to the /// thread-local parser. +#[cfg(any(feature = "rust", feature = "typescript"))] fn cached_sexp(lang: LANG, code: &str) -> String { let ast = Ast::parse(Source::new(lang, code.as_bytes())).expect("language feature enabled"); ast.as_tree_sitter().root_node().to_sexp() @@ -104,6 +109,7 @@ fn cached_sexp(lang: LANG, code: &str) -> String { /// The fixture a language is exercised with. Single source of truth: /// the reference tree and the tree under test must come from the same /// bytes, and selecting them at two separate sites is how they drift. +#[cfg(any(feature = "rust", feature = "typescript"))] fn fixture(lang: LANG) -> &'static str { if lang == LANG::Rust { RUST_SRC } else { TS_SRC } } @@ -112,6 +118,7 @@ fn fixture(lang: LANG) -> &'static str { /// grammar that failed to bind would yield a tiny ERROR tree, and /// every "identical to the reference" assertion would still hold if /// the reference were equally broken. +#[cfg(any(feature = "rust", feature = "typescript"))] fn assert_parsed_cleanly(sexp: &str, what: LANG) { assert!( !sexp.contains("ERROR") && !sexp.contains("MISSING"), @@ -128,6 +135,7 @@ fn assert_parsed_cleanly(sexp: &str, what: LANG) { ); } +#[cfg(all(feature = "rust", feature = "typescript"))] #[test] fn cached_parser_matches_a_fresh_parser_per_language() { for lang in [LANG::Rust, LANG::Typescript] { @@ -145,6 +153,7 @@ fn cached_parser_matches_a_fresh_parser_per_language() { /// `set_language` were skipped when the slot already held a parser, /// the second language in each pair would be parsed under the first /// language's grammar. +#[cfg(all(feature = "rust", feature = "typescript"))] #[test] fn alternating_languages_on_one_thread_stay_correct() { let rust_reference = reference_sexp(LANG::Rust, fixture(LANG::Rust)); @@ -175,6 +184,7 @@ fn alternating_languages_on_one_thread_stay_correct() { /// The test that would catch parse state surviving between files: /// a failed parse must not colour the next one. +#[cfg(feature = "rust")] #[test] fn parse_state_does_not_survive_between_files() { let reference = reference_sexp(LANG::Rust, fixture(LANG::Rust)); @@ -204,6 +214,7 @@ fn parse_state_does_not_survive_between_files() { /// takes the build-a-parser branch while later ones take the reuse /// branch. Both are exercised here, on threads that interleave /// languages so no thread can rely on another's binding. +#[cfg(all(feature = "rust", feature = "typescript"))] #[test] fn threads_are_isolated_and_trees_outlive_their_thread() { let rust_reference = reference_sexp(LANG::Rust, fixture(LANG::Rust)); @@ -262,15 +273,18 @@ fn threads_are_isolated_and_trees_outlive_their_thread() { thread_local! { /// Parses from its destructor, which runs during thread teardown /// — possibly after the parser slot has already been destroyed. + #[cfg(feature = "rust")] static TEARDOWN_PROBE: ParseOnDrop = const { ParseOnDrop }; } /// Set by `ParseOnDrop::drop`, checked after the thread is joined. +#[cfg(feature = "rust")] static TEARDOWN_PARSE_OK: AtomicBool = AtomicBool::new(false); struct ParseOnDrop; impl Drop for ParseOnDrop { + #[cfg(feature = "rust")] fn drop(&mut self) { // Must not panic. Whether this takes the "slot already // destroyed" fallback or still finds a live slot depends on @@ -292,6 +306,7 @@ impl Drop for ParseOnDrop { /// A parse issued while thread-locals are being destroyed must not /// panic, however the platform orders the destructors. +#[cfg(feature = "rust")] #[test] fn parsing_during_thread_local_teardown_does_not_panic() { std::thread::spawn(|| { diff --git a/tests/api/suppression_test.rs b/tests/api/suppression_test.rs index 79f1e6226..3e96aafaf 100644 --- a/tests/api/suppression_test.rs +++ b/tests/api/suppression_test.rs @@ -12,15 +12,19 @@ use big_code_analysis::{ FuncSpace, LANG, Metric, MetricsOptions, Source, SuppressionScope, analyze, }; -fn analyze_lang(source: &str, path: &str) -> FuncSpace { - let ext = path.rsplit('.').next().unwrap_or(""); - let lang = match ext { - "py" => LANG::Python, - "cpp" | "cc" | "hpp" | "h" => LANG::Cpp, - "rs" => LANG::Rust, - "js" => LANG::Javascript, - other => panic!("unsupported test extension {other:?}"), - }; +// `lang` is a parameter rather than something derived from `path`'s +// extension, as it was until #1472. A language picked out of a string is +// invisible to `check-test-lang-gates.py`, so every test here read as +// needing no grammar in particular and none of them was gated — which is +// 21 panics on any single-language build. Naming it at the call site is +// also what tells a reader which grammar a fixture exercises. +#[cfg(any( + feature = "cpp", + feature = "javascript", + feature = "python", + feature = "rust" +))] +fn analyze_lang(lang: LANG, source: &str, path: &str) -> FuncSpace { analyze( Source::new(lang, source.as_bytes()).with_name(Some(path.to_owned())), MetricsOptions::default(), @@ -31,6 +35,7 @@ fn analyze_lang(source: &str, path: &str) -> FuncSpace { /// Recursively locate the first non-Unit space whose name matches /// `name`. Tests use this to assert markers attached to the correct /// function rather than leaking up to the file-level space. +#[cfg(any(feature = "cpp", feature = "python", feature = "rust"))] fn find_function<'a>(space: &'a FuncSpace, name: &str) -> Option<&'a FuncSpace> { if space.name.as_deref() == Some(name) { return Some(space); @@ -38,6 +43,7 @@ fn find_function<'a>(space: &'a FuncSpace, name: &str) -> Option<&'a FuncSpace> space.spaces.iter().find_map(|s| find_function(s, name)) } +#[cfg(feature = "python")] #[test] fn python_native_function_scoped_marker_attaches_to_enclosing_function() { // Python is a non-C-family language: comments are `#`, distinct @@ -53,7 +59,7 @@ def noisy(x): return 1 return 0 "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); let noisy = find_function(&space, "noisy").expect("noisy function should be present"); assert!( noisy.suppressed.covers(Metric::Cyclomatic), @@ -71,6 +77,7 @@ def noisy(x): ); } +#[cfg(feature = "cpp")] #[test] fn cpp_native_function_scoped_marker_attaches_to_enclosing_function() { // C++ exercises the `//`-comment path; the marker is identical to @@ -84,7 +91,7 @@ int noisy(int x) { return 0; } "#; - let space = analyze_lang(src, "fixture.cpp"); + let space = analyze_lang(LANG::Cpp, src, "fixture.cpp"); let noisy = find_function(&space, "noisy").expect("noisy function should be present"); assert!(noisy.suppressed.covers(Metric::Cognitive)); assert!(noisy.suppressed.covers(Metric::Cyclomatic)); @@ -103,6 +110,7 @@ int noisy(int x) { ); } +#[cfg(feature = "rust")] #[test] fn rust_block_comment_marker_attaches() { // Rust block-comment form `/* bca: suppress */` exercises a different @@ -114,11 +122,12 @@ fn noisy(x: i32) -> i32 { if x > 0 { 1 } else { 0 } } "#; - let space = analyze_lang(src, "fixture.rs"); + let space = analyze_lang(LANG::Rust, src, "fixture.rs"); let noisy = find_function(&space, "noisy").expect("noisy function should be present"); assert!(noisy.suppressed.is_all(), "expected All scope on noisy"); } +#[cfg(feature = "python")] #[test] fn native_file_scoped_marker_lands_on_unit_space() { let src = r#" @@ -127,7 +136,7 @@ fn native_file_scoped_marker_lands_on_unit_space() { def fine(): return 1 "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); assert!(space.suppressed.covers(Metric::Loc)); assert!(space.suppressed.covers(Metric::Halstead)); assert!(!space.suppressed.covers(Metric::Cyclomatic)); @@ -138,6 +147,7 @@ def fine(): assert!(fine.suppressed.is_empty()); } +#[cfg(feature = "python")] #[test] fn lizard_function_marker_recognized_on_python() { // Lizard's `#lizard forgives` is verbatim Python-comment-shaped, so @@ -149,7 +159,7 @@ def noisy(x): return 1 return 0 "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); let noisy = find_function(&space, "noisy").expect("noisy function should be present"); assert!( noisy.suppressed.is_all(), @@ -157,6 +167,7 @@ def noisy(x): ); } +#[cfg(feature = "cpp")] #[test] fn lizard_file_marker_recognized_on_cpp() { // Lizard's `#lizard forgive global` placed in a C++ comment. @@ -165,10 +176,11 @@ fn lizard_file_marker_recognized_on_cpp() { int fine() { return 1; } "#; - let space = analyze_lang(src, "fixture.cpp"); + let space = analyze_lang(LANG::Cpp, src, "fixture.cpp"); assert!(space.suppressed.is_all()); } +#[cfg(feature = "rust")] #[test] fn nested_function_marker_lands_on_inner_function() { // The innermost containing function wins. Without that rule, a @@ -184,7 +196,7 @@ fn outer() -> i32 { inner() } "#; - let space = analyze_lang(src, "fixture.rs"); + let space = analyze_lang(LANG::Rust, src, "fixture.rs"); let outer = find_function(&space, "outer").expect("outer should be present"); let inner = find_function(&space, "inner").expect("inner should be present"); assert!(inner.suppressed.covers(Metric::Cyclomatic)); @@ -202,6 +214,7 @@ fn outer() -> i32 { ); } +#[cfg(feature = "python")] #[test] fn empty_scope_serializes_elided() { // Function spaces without markers must round-trip through JSON @@ -212,7 +225,7 @@ fn empty_scope_serializes_elided() { def fine(): return 1 "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); let json = serde_json::to_string(&space).expect("serialize"); assert!( !json.contains("\"suppressed\""), @@ -220,6 +233,7 @@ def fine(): ); } +#[cfg(feature = "python")] #[test] fn populated_scope_serializes_with_metrics_list() { // When a marker fires, the JSON output should expose the scope so @@ -231,7 +245,7 @@ fn populated_scope_serializes_with_metrics_list() { let src = r#" # bca: suppress-file(loc) "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); let json = serde_json::to_string(&space).expect("serialize"); let value: serde_json::Value = serde_json::from_str(&json).expect("valid JSON"); let suppressed = value @@ -254,6 +268,7 @@ fn populated_scope_serializes_with_metrics_list() { ); } +#[cfg(feature = "python")] #[test] fn unknown_metric_in_marker_has_no_effect() { // Typos must not silently widen scope. At the library boundary an @@ -269,7 +284,7 @@ def fine(): # bca: suppress(no_such_metric) return 1 "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); let fine = find_function(&space, "fine").expect("function fine should exist"); assert!( fine.suppressed.is_empty(), @@ -278,6 +293,7 @@ def fine(): ); } +#[cfg(feature = "python")] #[test] fn unknown_metric_beside_a_known_one_keeps_the_known_one() { // Since #1168 an unrecognized name costs its own name only. The @@ -293,7 +309,7 @@ def fine(x): return 1 return 0 "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); let fine = find_function(&space, "fine").expect("function fine should exist"); assert!( fine.suppressed.covers(Metric::Cyclomatic), @@ -302,6 +318,7 @@ def fine(x): ); } +#[cfg(feature = "python")] #[test] fn suppress_file_marker_accepts_a_trailing_rationale() { // `suppress-file` takes a rationale on the same terms as the @@ -314,7 +331,7 @@ fn suppress_file_marker_accepts_a_trailing_rationale() { def fine(): return 1 "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); assert!( space.suppressed.covers(Metric::Loc) && space.suppressed.covers(Metric::Halstead), "file-scoped marker with a rationale must attach both metrics; got {:?}", @@ -322,6 +339,7 @@ def fine(): ); } +#[cfg(feature = "python")] #[test] fn unknown_verb_in_marker_has_no_effect() { // Parallel to `unknown_metric_in_marker_has_no_effect`, but @@ -338,7 +356,7 @@ def fine(): # bca: allow(cyclomatic) return 1 "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); let fine = find_function(&space, "fine").expect("function fine should exist"); assert!( fine.suppressed.is_empty(), @@ -347,6 +365,7 @@ def fine(): ); } +#[cfg(feature = "python")] #[test] fn marker_outside_any_function_is_silently_ignored() { // A function-scoped marker that lies outside every function body @@ -358,12 +377,13 @@ fn marker_outside_any_function_is_silently_ignored() { def fine(): return 1 "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); assert!(space.suppressed.is_empty()); let fine = find_function(&space, "fine").expect("function fine should exist"); assert!(fine.suppressed.is_empty()); } +#[cfg(feature = "rust")] #[test] fn multiple_markers_union_on_same_function() { // Stacking markers should union the metric lists, so an author @@ -376,12 +396,13 @@ fn busy() -> i32 { if true { 1 } else { 0 } } "#; - let space = analyze_lang(src, "fixture.rs"); + let space = analyze_lang(LANG::Rust, src, "fixture.rs"); let busy = find_function(&space, "busy").expect("busy should be present"); assert!(busy.suppressed.covers(Metric::Cyclomatic)); assert!(busy.suppressed.covers(Metric::Cognitive)); } +#[cfg(feature = "cpp")] #[test] fn suppression_attaches_to_correct_sibling_on_same_line() { // Regression for issue #289. Two single-line C functions share @@ -394,7 +415,7 @@ fn suppression_attaches_to_correct_sibling_on_same_line() { let src = "int a() { return 1; } int b() { \ /* bca: suppress(cyclomatic) */ \ return 2; }\n"; - let space = analyze_lang(src, "fixture.cpp"); + let space = analyze_lang(LANG::Cpp, src, "fixture.cpp"); let a = find_function(&space, "a").expect("function a should be present"); let b = find_function(&space, "b").expect("function b should be present"); assert!( @@ -417,6 +438,7 @@ fn suppression_attaches_to_correct_sibling_on_same_line() { ); } +#[cfg(feature = "cpp")] #[test] fn suppression_after_function_open_brace_attaches_to_function() { // The marker sits on the same line as the opening brace but @@ -427,7 +449,7 @@ fn suppression_after_function_open_brace_attaches_to_function() { if (x > 0) { return 1; }\n\ return 0;\n\ }\n"; - let space = analyze_lang(src, "fixture.cpp"); + let space = analyze_lang(LANG::Cpp, src, "fixture.cpp"); let noisy = find_function(&space, "noisy").expect("noisy should be present"); assert!( noisy.suppressed.covers(Metric::Cognitive), @@ -440,6 +462,7 @@ fn suppression_after_function_open_brace_attaches_to_function() { ); } +#[cfg(feature = "cpp")] #[test] fn suppression_at_start_of_function_body() { // A marker as the first statement of the body — distinct from the @@ -451,7 +474,7 @@ fn suppression_at_start_of_function_body() { if (x > 0) { return 1; }\n\ return 0;\n\ }\n"; - let space = analyze_lang(src, "fixture.cpp"); + let space = analyze_lang(LANG::Cpp, src, "fixture.cpp"); let noisy = find_function(&space, "noisy").expect("noisy should be present"); assert!( noisy.suppressed.covers(Metric::Cyclomatic), @@ -460,6 +483,7 @@ fn suppression_at_start_of_function_body() { ); } +#[cfg(feature = "python")] #[test] fn function_marker_at_class_scope_is_silently_dropped() { // A function-scoped `bca: suppress` marker sitting at class scope @@ -481,7 +505,7 @@ class Holder: return 1 return 0 "#; - let space = analyze_lang(src, "fixture.py"); + let space = analyze_lang(LANG::Python, src, "fixture.py"); let holder = find_function(&space, "Holder").expect("class Holder should be present"); assert!( holder.suppressed.is_empty(), @@ -512,6 +536,7 @@ fn default_scope_does_not_cover_any_metric() { } } +#[cfg(feature = "javascript")] #[test] fn deeply_nested_function_suppression_does_not_overflow_stack() { // Regression test for issues #292 and #308. @@ -557,7 +582,7 @@ fn deeply_nested_function_suppression_does_not_overflow_stack() { src.push_str("}\n"); } - let space = analyze_lang(&src, "deeply_nested.js"); + let space = analyze_lang(LANG::Javascript, &src, "deeply_nested.js"); // Iterative walk so the assertion path itself never recurses; a // recursive search would defeat the point of the test by diff --git a/tests/corpus/csharp_test.rs b/tests/corpus/csharp_test.rs index d2a4f31d0..177f7b1f9 100644 --- a/tests/corpus/csharp_test.rs +++ b/tests/corpus/csharp_test.rs @@ -5,6 +5,15 @@ use std::path::Path; use common::compare_rca_output_with_files_under; +// Hand-written, not derived: the corpus walk picks a language per +// file at run time from its extension, so nothing in this body +// names the grammar it needs. The glob list (`*.cs`) is what +// decides it, and a build without that grammar scores every file +// zero rather than matching the snapshot (#1472). +// test-lang-gates: hand-written(csharp) — the corpus walk picks a +// language per file from its extension, so the glob list decides it +// and nothing in the body names it +#[cfg(feature = "csharp")] #[test] fn test_csharp() { let source_root = Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/tests/corpus/deepspeech_test.rs b/tests/corpus/deepspeech_test.rs index d36d9ce52..2763033ec 100644 --- a/tests/corpus/deepspeech_test.rs +++ b/tests/corpus/deepspeech_test.rs @@ -3,6 +3,18 @@ use crate::common; use common::compare_rca_output_with_files; +// Hand-written, not derived: the corpus walk picks a language per +// file at run time from its extension, so nothing in this body +// names the grammar it needs. The glob list decides it, and all four +// of `*.cc` / `*.cpp` / `*.h` / `*.hh` belong to `LANG::Cpp` — +// `LANG::C` owns `.c` alone (`mk_langs!`), so naming it too would only +// make the gate narrower than the corpus is. A build without the C++ +// grammar scores every file zero rather than matching the snapshot +// (#1472). +// test-lang-gates: hand-written(cpp) — the corpus walk picks a +// language per file from its extension, so the glob list decides +// it and nothing in the body names it +#[cfg(feature = "cpp")] #[test] fn test_deepspeech() { // FIXME: Ignoring these files temporarily due to parsing errors (originally https://github.com/dekobon/big-code-analysis/issues/83, diff --git a/tests/corpus/irules_test.rs b/tests/corpus/irules_test.rs index a20557ce0..6999e667e 100644 --- a/tests/corpus/irules_test.rs +++ b/tests/corpus/irules_test.rs @@ -24,6 +24,7 @@ use big_code_analysis::{LANG, MetricsOptions, Source, SpaceKind, analyze}; /// arm, an early `return`, and command substitutions (`[HTTP::uri]`). The /// grammar README documents these commands but ships no full sample, so the /// fixture is hand-written. +#[cfg(feature = "irules")] const SOURCE: &str = r#"when CLIENT_ACCEPTED { set start [clock clicks] } @@ -55,6 +56,7 @@ proc rewrite_path { prefix uri } { /// The analyzed tree must expose both `when` handlers and the `proc` as /// `Function` spaces under the file `Unit`, with per-space metrics matching /// the constructs each contains, and the file-level rollups summing them. +#[cfg(feature = "irules")] #[test] fn irules_end_to_end_funcspace_tree() { let unit = analyze( diff --git a/tests/corpus/main.rs b/tests/corpus/main.rs index a30fca96a..0e3aa9d56 100644 --- a/tests/corpus/main.rs +++ b/tests/corpus/main.rs @@ -7,6 +7,15 @@ //! rationale. These six modules held one `#[test]` each and linked a //! ~280 MB binary apiece to run it. +// Per-language test gating (#1472) makes "is this import live" a +// function of the enabled feature set, which no `cfg` on the import +// itself can express. Partial builds only — the build CI gates on and +// the one a contributor runs still police every unused import. Dead +// *items* are not relaxed: unlike the two library roots this test crate +// carries no `allow(dead_code)`, so every helper, `const`, macro and +// test here still needs its own gate. See `.claude/rules/testing.md`, +// "Why the import lint is off on a partial build". +#![cfg_attr(not(feature = "all-languages"), allow(unused_imports))] #[path = "../common/mod.rs"] mod common; diff --git a/tests/corpus/pdf_js_test.rs b/tests/corpus/pdf_js_test.rs index 3efbb7af7..11a5324e9 100644 --- a/tests/corpus/pdf_js_test.rs +++ b/tests/corpus/pdf_js_test.rs @@ -3,6 +3,15 @@ use crate::common; use common::compare_rca_output_with_files; +// Hand-written, not derived: the corpus walk picks a language per +// file at run time from its extension, so nothing in this body +// names the grammar it needs. The glob list (`*.js`) is what +// decides it, and a build without that grammar scores every file +// zero rather than matching the snapshot (#1472). +// test-lang-gates: hand-written(javascript) — the corpus walk picks a +// language per file from its extension, so the glob list decides it +// and nothing in the body names it +#[cfg(feature = "javascript")] #[test] fn test_pdfjs() { // The 118-entry exclude list that used to live here (mozjs-era parse diff --git a/tests/corpus/php_test.rs b/tests/corpus/php_test.rs index c5c50d515..48993d369 100644 --- a/tests/corpus/php_test.rs +++ b/tests/corpus/php_test.rs @@ -5,6 +5,15 @@ use std::path::Path; use common::compare_rca_output_with_files_under; +// Hand-written, not derived: the corpus walk picks a language per +// file at run time from its extension, so nothing in this body +// names the grammar it needs. The glob list (`*.php`) is what +// decides it, and a build without that grammar scores every file +// zero rather than matching the snapshot (#1472). +// test-lang-gates: hand-written(php) — the corpus walk picks a language +// per file from its extension, so the glob list decides it and +// nothing in the body names it +#[cfg(feature = "php")] #[test] fn test_php() { let source_root = Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/tests/corpus/serde_test.rs b/tests/corpus/serde_test.rs index 896d3725d..030db8628 100644 --- a/tests/corpus/serde_test.rs +++ b/tests/corpus/serde_test.rs @@ -3,6 +3,15 @@ use crate::common; use common::compare_rca_output_with_files; +// Hand-written, not derived: the corpus walk picks a language per +// file at run time from its extension, so nothing in this body +// names the grammar it needs. The glob list (`*.rs`) is what +// decides it, and a build without that grammar scores every file +// zero rather than matching the snapshot (#1472). +// test-lang-gates: hand-written(rust) — the corpus walk picks a +// language per file from its extension, so the glob list decides it +// and nothing in the body names it +#[cfg(feature = "rust")] #[test] fn test_serde() { compare_rca_output_with_files("serde", &["*.rs"], &[], 172); diff --git a/tests/grammars/main.rs b/tests/grammars/main.rs index e0c4f9ca5..c86c0afe6 100644 --- a/tests/grammars/main.rs +++ b/tests/grammars/main.rs @@ -4,6 +4,15 @@ //! //! Grouped into one binary by #1124 — see `tests/api/main.rs`. +// Per-language test gating (#1472) makes "is this import live" a +// function of the enabled feature set, which no `cfg` on the import +// itself can express. Partial builds only — the build CI gates on and +// the one a contributor runs still police every unused import. Dead +// *items* are not relaxed: unlike the two library roots this test crate +// carries no `allow(dead_code)`, so every helper, `const`, macro and +// test here still needs its own gate. See `.claude/rules/testing.md`, +// "Why the import lint is off on a partial build". +#![cfg_attr(not(feature = "all-languages"), allow(unused_imports))] mod alterator_string_flattening; mod c_grammar_metrics; mod mozcpp_grammar_metrics; diff --git a/tests/grammars/mozcpp_grammar_metrics.rs b/tests/grammars/mozcpp_grammar_metrics.rs index 33184b0b0..394a4a6fe 100644 --- a/tests/grammars/mozcpp_grammar_metrics.rs +++ b/tests/grammars/mozcpp_grammar_metrics.rs @@ -106,6 +106,7 @@ mod mozcpp_metrics { /// ordinary C++ is measured. (Complements `tests/parity/cpp_mozcpp_parity.rs`, /// here via a class so the `npm`/`npa`/`wmc` class arms are part of the /// comparison.) + #[cfg(all(feature = "cpp", feature = "mozcpp"))] #[test] fn mozcpp_matches_cpp_on_plain_class() { let src = "class Widget { @@ -161,6 +162,7 @@ mod mozcpp_metrics { /// (251 vs 340), `cast_expression` (343 vs 432) — so id-based matching /// silently miscounts them for Mozcpp. Reverting either helper to /// `kind_id().into()` against `Cpp` makes this test fail. + #[cfg(all(feature = "cpp", feature = "mozcpp"))] #[test] fn mozcpp_matches_cpp_on_conditions() { // Operands span every divergent-id boolean terminal: `o->ready` diff --git a/tests/output_formats/csv_test.rs b/tests/output_formats/csv_test.rs index ceb87aab5..f6b15833e 100644 --- a/tests/output_formats/csv_test.rs +++ b/tests/output_formats/csv_test.rs @@ -15,6 +15,7 @@ use std::path::{Path, PathBuf}; use big_code_analysis::{CSV_HEADER, LANG, MetricsOptions, Source, analyze, write_csv}; +#[cfg(any(feature = "cpp", feature = "python", feature = "rust"))] fn render_csv(lang: LANG, source: &[u8], path: &Path) -> String { let name = path.to_str().map(str::to_owned); let space = analyze( @@ -31,6 +32,7 @@ fn render_csv(lang: LANG, source: &[u8], path: &Path) -> String { /// fields *outside* of any quoted strings. The csv crate handles /// quoting; this smoke check just confirms we never emit a malformed /// row. +#[cfg(any(feature = "cpp", feature = "python", feature = "rust"))] fn assert_well_formed(csv_text: &str) { let mut rdr = csv::ReaderBuilder::new() .has_headers(false) @@ -50,6 +52,7 @@ fn assert_well_formed(csv_text: &str) { assert!(rows >= 2, "expected header + at least one data row"); } +#[cfg(feature = "rust")] #[test] fn csv_rust_function_and_impl() { let source = r" @@ -73,6 +76,7 @@ impl Counter { insta::assert_snapshot!("csv_rust_counter", out); } +#[cfg(feature = "python")] #[test] fn csv_python_class() { let source = r#" @@ -92,6 +96,7 @@ class Greeter: insta::assert_snapshot!("csv_python_greeter", out); } +#[cfg(feature = "cpp")] #[test] fn csv_cpp_namespace_and_class() { let source = r" @@ -112,6 +117,7 @@ private: insta::assert_snapshot!("csv_cpp_widget", out); } +#[cfg(feature = "rust")] #[test] fn csv_header_row_is_documented_constant() { // Cheap regression: if anyone reorders columns in csv.rs the diff --git a/tests/output_formats/main.rs b/tests/output_formats/main.rs index 1266d977c..6fbb06383 100644 --- a/tests/output_formats/main.rs +++ b/tests/output_formats/main.rs @@ -12,6 +12,15 @@ //! modules of this driver rather than crate roots. See "Moving a test //! file" in `tests/README.md`. +// Per-language test gating (#1472) makes "is this import live" a +// function of the enabled feature set, which no `cfg` on the import +// itself can express. Partial builds only — the build CI gates on and +// the one a contributor runs still police every unused import. Dead +// *items* are not relaxed: unlike the two library roots this test crate +// carries no `allow(dead_code)`, so every helper, `const`, macro and +// test here still needs its own gate. See `.claude/rules/testing.md`, +// "Why the import lint is off on a partial build". +#![cfg_attr(not(feature = "all-languages"), allow(unused_imports))] #[path = "../common/mod.rs"] mod common; diff --git a/tests/parity/cognitive_cross_language_parity.rs b/tests/parity/cognitive_cross_language_parity.rs index e1aceb7f2..931977065 100644 --- a/tests/parity/cognitive_cross_language_parity.rs +++ b/tests/parity/cognitive_cross_language_parity.rs @@ -36,6 +36,31 @@ use big_code_analysis::{LANG, MetricsOptions, Source, analyze}; /// Cognitive max for the single function in `source`. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn cognitive_max(lang: LANG, source: &str, ext: &str) -> f64 { let name = format!("parity.{ext}"); let space = analyze( @@ -52,6 +77,31 @@ fn cognitive_max(lang: LANG, source: &str, ext: &str) -> f64 { /// /// Every `Some` row spells the same shape: a function whose whole body /// is a two-arm switch — one explicit arm plus a fallback. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn fixture(lang: LANG) -> Option<(&'static str, &'static str)> { // Exhaustive per-language dispatch table: one arm per LANG variant // is the point of this function, so a new language cannot be added @@ -170,6 +220,31 @@ fn fixture(lang: LANG) -> Option<(&'static str, &'static str)> { Some(row) } +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] #[test] fn two_arm_wildcard_switch_cognitive_parity() { // expected: a two-arm switch/match with one explicit arm plus a @@ -234,6 +309,13 @@ fn two_arm_wildcard_switch_cognitive_parity() { /// /// Python is deliberately absent: a `def` is a statement and a lambda /// body is a single expression, so the shape is unconstructible. +#[cfg(any( + feature = "cpp", + feature = "csharp", + feature = "java", + feature = "php", + feature = "rust", +))] #[test] fn a_function_declared_inside_a_closure_scores_the_same_as_outside() { /// The innermost `g`'s own cognitive score. diff --git a/tests/parity/exit_cross_language_parity.rs b/tests/parity/exit_cross_language_parity.rs index d264500e1..5d96d3b8b 100644 --- a/tests/parity/exit_cross_language_parity.rs +++ b/tests/parity/exit_cross_language_parity.rs @@ -67,6 +67,31 @@ use big_code_analysis::{LANG, MetricsOptions, Source, analyze}; /// Exit-count file-level sum for the single function in `source`. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn nexits_sum(lang: LANG, source: &str, ext: &str) -> f64 { let name = format!("parity.{ext}"); let space = analyze( @@ -83,6 +108,31 @@ fn nexits_sum(lang: LANG, source: &str, ext: &str) -> f64 { /// Every `Some` row spells exactly two counted exits: a plain `return` /// plus the language's abrupt-exit construct, or two `return`s where /// that is the only modelled exit. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn fixture(lang: LANG) -> Option<(&'static str, &'static str)> { // Exhaustive per-language dispatch table: one arm per LANG variant // is the point of this function, so a new language cannot be added @@ -230,6 +280,31 @@ fn fixture(lang: LANG) -> Option<(&'static str, &'static str)> { Some(row) } +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] #[test] fn return_plus_abrupt_exit_parity() { // expected: every fixture has exactly two counted exits — one diff --git a/tests/parity/functions_metrics_parity.rs b/tests/parity/functions_metrics_parity.rs index bede07559..dc0b77626 100644 --- a/tests/parity/functions_metrics_parity.rs +++ b/tests/parity/functions_metrics_parity.rs @@ -55,16 +55,116 @@ use super::ops_metrics_space_parity::fixture; /// `` is the trait default; #1184 added the other four for /// constructs that carry executable code but no name token, each /// `is_func_space` without being `is_func` for exactly the reason above. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] const SYNTHESISED_NAMES: &[&str] = &["", "", "", "", ""]; +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn is_synthesised_name(name: &str) -> bool { SYNTHESISED_NAMES.contains(&name) } /// A space or span, reduced to the fields all three seams report. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] type Fun = (Option, usize, usize); /// Every `SpaceKind::Function` space in the metrics tree, in preorder. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn metrics_functions(space: &FuncSpace, out: &mut Vec) { if space.kind == SpaceKind::Function { out.push((space.name.clone(), space.start_line, space.end_line)); @@ -77,6 +177,31 @@ fn metrics_functions(space: &FuncSpace, out: &mut Vec) { /// Renders one side as sorted lines, for a failure message that shows /// both lists rather than a `Vec` debug dump the reader has to align by /// eye. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn render(funs: &[Fun]) -> String { let mut lines: Vec = funs .iter() @@ -86,6 +211,31 @@ fn render(funs: &[Fun]) -> String { lines.join("\n") } +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] #[test] fn every_named_function_space_is_reported_by_functions_and_find() { let mut checked = 0; diff --git a/tests/parity/main.rs b/tests/parity/main.rs index fd080bde0..deadcf3b4 100644 --- a/tests/parity/main.rs +++ b/tests/parity/main.rs @@ -4,6 +4,15 @@ //! //! Grouped into one binary by #1124 — see `tests/api/main.rs`. +// Per-language test gating (#1472) makes "is this import live" a +// function of the enabled feature set, which no `cfg` on the import +// itself can express. Partial builds only — the build CI gates on and +// the one a contributor runs still police every unused import. Dead +// *items* are not relaxed: unlike the two library roots this test crate +// carries no `allow(dead_code)`, so every helper, `const`, macro and +// test here still needs its own gate. See `.claude/rules/testing.md`, +// "Why the import lint is off on a partial build". +#![cfg_attr(not(feature = "all-languages"), allow(unused_imports))] mod cognitive_cross_language_parity; mod cpp_mozcpp_parity; mod cyclomatic_cross_language_parity; diff --git a/tests/parity/nargs_cross_language_parity.rs b/tests/parity/nargs_cross_language_parity.rs index 53316b5e1..145827e41 100644 --- a/tests/parity/nargs_cross_language_parity.rs +++ b/tests/parity/nargs_cross_language_parity.rs @@ -31,6 +31,31 @@ use big_code_analysis::{LANG, MetricsOptions, Source, analyze}; /// `function_args` file-level sum for the single function in `source`. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn function_args_sum(lang: LANG, source: &str, ext: &str) -> f64 { let name = format!("parity.{ext}"); let space = analyze( @@ -46,6 +71,31 @@ fn function_args_sum(lang: LANG, source: &str, ext: &str) -> f64 { /// /// Every `Some` row declares the same three parameters `a`, `b`, `c`. /// The extension only names the parsed unit; it reaches no metric. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn fixture(lang: LANG) -> Option<(&'static str, &'static str)> { // Exhaustive per-language dispatch table: one arm per LANG variant // is the point of this function, so a new language cannot be added @@ -101,6 +151,31 @@ fn fixture(lang: LANG) -> Option<(&'static str, &'static str)> { Some(row) } +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] #[test] fn three_parameter_function_parity() { // expected: three formal parameters, hand-derived from each fixture diff --git a/tests/parity/ops_metrics_space_parity.rs b/tests/parity/ops_metrics_space_parity.rs index 07fcb7cf8..80525cfe8 100644 --- a/tests/parity/ops_metrics_space_parity.rs +++ b/tests/parity/ops_metrics_space_parity.rs @@ -209,6 +209,31 @@ impl SpaceTree for Ops { /// reader sees *which* space diverged and what its neighbours were — /// the failure mode here is a missing subtree, which a pairwise /// recursion reports as a confusing count mismatch at the parent. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn render(node: &T, depth: usize, out: &mut String) { use std::fmt::Write as _; @@ -224,6 +249,31 @@ fn render(node: &T, depth: usize, out: &mut String) { } } +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn rendered(node: &T) -> String { let mut out = String::new(); render(node, 0, &mut out); @@ -233,6 +283,31 @@ fn rendered(node: &T) -> String { /// First line that differs between the two renderings, for a failure /// message that names the diverging space instead of dumping a diff the /// reader has to align by eye. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn first_divergence(metrics: &str, ops: &str) -> String { let mut metrics_lines = metrics.lines(); let mut ops_lines = ops.lines(); @@ -247,6 +322,31 @@ fn first_divergence(metrics: &str, ops: &str) -> String { } } +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] #[test] fn ops_and_metrics_agree_on_the_space_tree() { let mut checked = 0; diff --git a/tests/parity/self_reference_operand_parity.rs b/tests/parity/self_reference_operand_parity.rs index 21ddd8578..4f52dcb79 100644 --- a/tests/parity/self_reference_operand_parity.rs +++ b/tests/parity/self_reference_operand_parity.rs @@ -55,6 +55,31 @@ use big_code_analysis::{Ast, LANG, MetricsOptions, Source, analyze}; /// which is the drift these rows exist to catch. /// /// [`Ops::operands`]: big_code_analysis::Ops::operands +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn receiver_stripped(lang: LANG) -> Option<&'static str> { match lang { // `return self.x` -> `return x`. Parses clean; N2 5 -> 4. @@ -67,6 +92,31 @@ fn receiver_stripped(lang: LANG) -> Option<&'static str> { } /// Total operand occurrences (`N2`) for `source` under `lang`. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn total_operands(lang: LANG, source: &str, name: &str) -> u64 { analyze( Source::new(lang, source.as_bytes()).with_name(Some(name.to_owned())), @@ -92,6 +142,31 @@ fn total_operands(lang: LANG, source: &str, name: &str) -> u64 { /// says so at its row. They still earn their place — each is a guard /// against a grammar bump promoting the keyword to a kind of its own /// and the language falling out of the majority unnoticed. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn fixture(lang: LANG) -> Option<(&'static str, &'static str, &'static [&'static str])> { // Exhaustive per-language dispatch table: one arm per LANG variant // is the point of this function, so a new language cannot be added @@ -220,6 +295,31 @@ fn fixture(lang: LANG) -> Option<(&'static str, &'static str, &'static [&'static Some(row) } +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] #[test] fn every_language_bills_a_self_reference_as_an_operand() { let mut checked = 0; diff --git a/tests/parity/space_span_containment.rs b/tests/parity/space_span_containment.rs index ead80e97d..c9d3af2cc 100644 --- a/tests/parity/space_span_containment.rs +++ b/tests/parity/space_span_containment.rs @@ -27,6 +27,31 @@ use super::ops_metrics_space_parity::{SpaceTree, fixture}; /// Lines in `source`, counting the way an editor does: a trailing /// newline terminates the last line rather than opening an empty one. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn line_count(source: &str) -> usize { source.lines().count() } @@ -34,6 +59,31 @@ fn line_count(source: &str) -> usize { /// Asserts the containment invariant over one space subtree, returning /// the number of spaces visited so the caller can rule out a vacuous /// pass. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn check_containment( lang: LANG, walk: &str, @@ -69,6 +119,31 @@ fn check_containment( /// Asserts both invariants over both walks for one source, and returns /// the number of spaces the metrics walk produced. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] fn check_source(lang: LANG, source: &str, ext: &str) -> usize { let name = format!("span.{ext}"); @@ -101,6 +176,31 @@ fn check_source(lang: LANG, source: &str, ext: &str) -> usize { visited } +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] #[test] fn every_space_lies_within_its_parent_in_every_language() { let mut checked = 0; @@ -151,6 +251,31 @@ fn every_space_lies_within_its_parent_in_every_language() { /// An empty file is deliberately absent: it has no lines at all, so the /// `(1, line_count)` rule would demand the inverted `1..0`. That carve-out /// is pinned as a unit test beside `line_span` instead. +#[cfg(any( + feature = "bash", + feature = "c", + feature = "c-family-helpers", + feature = "cpp", + feature = "csharp", + feature = "elixir", + feature = "go", + feature = "groovy", + feature = "irules", + feature = "java", + feature = "javascript", + feature = "kotlin", + feature = "lua", + feature = "mozcpp", + feature = "mozjs", + feature = "objc", + feature = "perl", + feature = "php", + feature = "python", + feature = "ruby", + feature = "rust", + feature = "tcl", + feature = "typescript", +))] #[test] fn degenerate_sources_still_span_their_lines_in_every_language() { let mut checked = 0; diff --git a/utils/check-feature-gates-test.py b/utils/check-feature-gates-test.py index cdd542bab..6ca3db4a2 100644 --- a/utils/check-feature-gates-test.py +++ b/utils/check-feature-gates-test.py @@ -287,6 +287,152 @@ def test_the_match_is_on_whole_path_segments(self) -> None: self.assertTrue(matcher.search("a_type::inner")) self.assertFalse(matcher.search("metrics::a_type_declared_inside")) + def test_a_nested_subject_matches_on_its_whole_path(self) -> None: + """A bare `tests` would match every `::tests::` path in the crate.""" + subject = _nested_mod_subject() + matcher = gate.subject_matcher(subject) + self.assertEqual(subject.qualified_name, "outer::tests") + self.assertTrue(matcher.search("metrics::outer::tests::a_case")) + self.assertFalse(matcher.search("metrics::halstead::tests::a_case")) + + +NESTED_IN_TEST_SCOPE = """ +#[cfg(test)] +mod outer { + #[cfg(any(feature = "php", feature = "groovy"))] + mod tests { + #[test] + fn a_case() {} + } +} +""" + + +def _nested_mod_subject(): + subjects = gate.scan_source(NESTED_IN_TEST_SCOPE, "x.rs") + return next(s for s in subjects if s.kind == "mod") + + +class NestedSubjectTest(unittest.TestCase): + """#1472 item 3: a union-gated `mod` under a `#[cfg(test)]` parent. + + It carries no `test` predicate of its own, so reading only its own + attribute classified it "compile-time only" and never compared it + against the build — a false pass in exactly the #1220 shape this + gate exists to catch. + """ + + def test_it_is_checkable_despite_carrying_no_test_predicate(self) -> None: + subject = _nested_mod_subject() + self.assertFalse(subject.is_cfg_test, "it has no `test` of its own") + self.assertTrue(subject.in_cfg_test_scope) + self.assertTrue(subject.carries_tests) + + def test_a_mod_whose_brace_is_on_the_next_line_still_opens_a_scope( + self, + ) -> None: + """Requiring the brace on the header line re-creates the bug. + + rustfmt keeps it there today, so nothing in this tree reaches + the other spelling — which is exactly why it needs a fixture: + the failure is silent, and it is the same false pass this whole + change removes. + """ + subject = _nested_mod_subject_in( + """ +#[cfg(test)] +mod outer +{ + #[cfg(any(feature = "php", feature = "groovy"))] + mod tests { } +} +""" + ) + self.assertTrue(subject.in_cfg_test_scope) + self.assertTrue(subject.carries_tests) + self.assertEqual(subject.qualified_name, "outer::tests") + + def test_a_mod_declaration_opens_no_scope(self) -> None: + """`mod outer;` has no body, so it must not swallow what follows.""" + subjects = gate.scan_source( + """ +#[cfg(test)] +mod outer; + +#[cfg(any(feature = "php", feature = "groovy"))] +fn helper() {} +""", + "y.rs", + ) + (helper,) = subjects + self.assertEqual(helper.qualified_name, "helper") + self.assertFalse(helper.in_cfg_test_scope) + + def test_a_mod_declaration_is_not_opened_by_a_later_impl_block( + self, + ) -> None: + """The clearing `elif` needs an item or a `;` to fire. + + `impl` and `struct` are neither, so a pending `mod foo;` waited + for their `{` and claimed the whole block. Every subject inside + then reported `foo::…`, which `subject_matcher` cannot match + against any nextest name — a silent pass. + """ + subjects = gate.scan_source( + """ +#[cfg(test)] +mod outer; + +impl Holder { + #[test] + #[cfg(any(feature = "php", feature = "groovy"))] + fn a_union_gated_test() {} +} +""", + "y.rs", + ) + (test_fn,) = subjects + self.assertEqual(test_fn.qualified_name, "a_union_gated_test") + + def test_an_exotic_line_separator_does_not_desynchronise_the_scan( + self, + ) -> None: + """`splitlines()` breaks on more characters than `\n`. + + Masking turns a form feed inside a string into a space, so a + scanner splitting the masked text with `splitlines()` gets fewer + lines than the raw one and indexes off the end — an `IndexError` + traceback rather than a finding. + """ + subjects = gate.scan_source( + '#[cfg(any(feature = "php"))]\n' + 'const F: &str = "a\x0cb";\n' + "#[test]\n" + '#[cfg(any(feature = "php", feature = "groovy"))]\n' + "fn after_the_form_feed() {}\n", + "z.rs", + ) + self.assertIn( + "after_the_form_feed", [subject.name for subject in subjects] + ) + + def test_a_non_test_scope_mod_is_still_compile_time_only(self) -> None: + # Without a test scope there is no test name for nextest to + # report, so the clippy run owns it as it always did. + subject = _nested_mod_subject_in( + """ +mod outer { + #[cfg(any(feature = "php", feature = "groovy"))] + mod inner {} +} +""" + ) + self.assertFalse(subject.carries_tests) + + +def _nested_mod_subject_in(source: str): + return next(s for s in gate.scan_source(source, "x.rs") if s.kind == "mod") + class MainTest(unittest.TestCase): """Both directions, with the build stubbed out. diff --git a/utils/check-feature-gates.py b/utils/check-feature-gates.py index b46beb083..efa943851 100755 --- a/utils/check-feature-gates.py +++ b/utils/check-feature-gates.py @@ -227,10 +227,6 @@ def dead_spans(source: str) -> list[tuple[int, int]]: return spans -def _in_any_span(idx: int, spans: list[tuple[int, int]]) -> bool: - return any(start <= idx < end for start, end in spans) - - # --------------------------------------------------------------------------- # Subject discovery # --------------------------------------------------------------------------- @@ -249,23 +245,46 @@ class Subject: is_test_fn: bool #: Some ``cfg`` in the run also requires the bare ``test`` predicate. is_cfg_test: bool + #: Enclosing ``mod`` names, outermost first. + module_path: tuple[str, ...] = () + #: An enclosing ``mod`` carries the bare ``test`` predicate. + in_cfg_test_scope: bool = False @property def carries_tests(self) -> bool: """Whether a build can be asked whether this subject is present. - A ``#[cfg(test)] mod`` is a container of tests and a ``#[test] - fn`` is one. A bare helper ``fn`` gated on a union carries no - test name, so ``nextest`` has nothing to report about it — its - absence under a disjoint feature set is a pure compile-time - property, which the leg's ``cargo clippy --all-targets`` already - covers (an unused helper is ``dead_code``, a missing one is - ``E0425``). + A ``mod`` in a test scope is a container of tests and a + ``#[test] fn`` is one. A bare helper ``fn`` gated on a union + carries no test name, so ``nextest`` has nothing to report about + it — its absence under a disjoint feature set is a pure + compile-time property, which the leg's + ``cargo clippy --all-targets`` already covers (an unused helper + is ``dead_code``, a missing one is ``E0425``). + + The scope, not the subject's own attribute (#1472). A + union-gated ``mod`` nested inside an already-``#[cfg(test)]`` + parent has no ``test`` of its own, and reading only the + attribute classified it "compile-time only" and never checked it + — a false pass in exactly the #1220 shape this gate exists to + catch. + + "Scope" here means *in this file*. The commonest spelling in + this repo puts the marker on a `#[cfg(test)] #[path = "…"] mod + x;` line in the parent module, which a single-file scan cannot + see — every such module redundantly repeats `#[cfg(test)]` at + its own top, which is the only reason they are classified + correctly today. """ - return (self.kind == "mod" and self.is_cfg_test) or ( + return (self.kind == "mod" and (self.is_cfg_test or self.in_cfg_test_scope)) or ( self.kind == "fn" and self.is_test_fn ) + @property + def qualified_name(self) -> str: + """``a::b::name`` — what a nextest test path actually contains.""" + return "::".join((*self.module_path, self.name)) + def describe(self) -> str: return f"{self.path}:{self.line} {self.kind} {self.name}" @@ -320,22 +339,31 @@ def scan_source(source: str, path: str) -> list[Subject]: attribute and its item are legal Rust and do not break the run. """ spans = dead_spans(source) - lines = source.splitlines() - # Start-of-line character offsets, so a `#[` inside a fixture string - # can be told from a real attribute. - offsets: list[int] = [] - pos = 0 - for line in lines: - offsets.append(pos) - pos += len(line) + 1 - + masked = list(source) + for start, end in spans: + for i in range(start, min(end, len(masked))): + if masked[i] != "\n": + masked[i] = " " + # `split("\n")`, not `splitlines()`: the latter also breaks on + # `\x0b`, `\x0c`, `\u2028` and friends, and masking turns those + # into spaces — so the masked text would yield *fewer* lines than + # the raw one and every later index would be off by the + # difference, ending in an `IndexError` traceback rather than a + # diagnosable error. + masked_lines = "".join(masked).split("\n") + lines = source.split("\n") subjects: list[Subject] = [] attrs: list[str] = [] pending: list[str] = [] # partial multi-line attribute + # Enclosing `mod`s as (name, brace depth on entry, in a test scope). + stack: list[tuple[str, int, bool]] = [] + # A `mod` seen but not yet opened, for the brace-on-a-later-line + # spelling. + opening: tuple[str, int, bool] | None = None + depth = 0 index = 0 while index < len(lines): - raw = lines[index] - stripped = raw.strip() + stripped = lines[index].strip() line_no = index + 1 index += 1 @@ -350,9 +378,17 @@ def scan_source(source: str, path: str) -> list[Subject]: if not stripped or stripped.startswith("//"): continue + masked_line = masked_lines[line_no - 1] + masked_stripped = masked_line.strip() + if stripped.startswith("#[") or stripped.startswith("#!["): - hash_idx = offsets[line_no - 1] + (len(raw) - len(raw.lstrip())) - if _in_any_span(hash_idx, spans): + # The masked copy answers "is this inside a string or a + # comment" in O(1). `_in_any_span` is a linear scan of every + # dead span in the file, and tracking module nesting made + # this loop visit every code line rather than only the ones + # an attribute precedes — which cost 4.7x on a whole-tree + # scan, paid once per CI leg. + if not masked_stripped.startswith("#"): continue if stripped.count("[") > stripped.count("]"): pending = [stripped] @@ -360,28 +396,72 @@ def scan_source(source: str, path: str) -> list[Subject]: attrs.append(stripped) continue - if attrs: - item = ITEM_RE.match(stripped) - code_idx = offsets[line_no - 1] + (len(raw) - len(raw.lstrip())) - if item is not None and not _in_any_span(code_idx, spans): - features: set[str] = set() - for attr in attrs: - features.update(union_features(attr)) - if features: - subjects.append( - Subject( - path=path, - line=line_no, - kind=item.group(1), - name=item.group(2), - features=frozenset(features), - is_test_fn=any( - TEST_ATTR_RE.search(a) for a in attrs - ), - is_cfg_test=any(_cfg_requires_test(a) for a in attrs), - ) + item = ITEM_RE.match(masked_stripped) + entry_depth = depth + + if attrs and item is not None: + features: set[str] = set() + for attr in attrs: + features.update(union_features(attr)) + if features: + own_cfg_test = any(_cfg_requires_test(a) for a in attrs) + subjects.append( + Subject( + path=path, + line=line_no, + kind=item.group(1), + name=item.group(2), + features=frozenset(features), + is_test_fn=any(TEST_ATTR_RE.search(a) for a in attrs), + is_cfg_test=own_cfg_test, + module_path=tuple(name for name, _, _ in stack), + in_cfg_test_scope=any(scope for _, _, scope in stack), ) - attrs = [] + ) + if attrs: + enters_test_scope = any(_cfg_requires_test(a) for a in attrs) + else: + enters_test_scope = False + attrs = [] + + # `mod foo;` declares a module in another file and opens nothing. + # Reading it as pending leaves it waiting for the next `{` in the + # file, which is routinely an `impl` or `struct` — and since + # neither matches `ITEM_RE` nor carries a `;`, nothing below + # clears it. Every subject inside that block then reports a + # `qualified_name` prefixed with a module it is not in, and + # `subject_matcher` matches no nextest test at all: a silent + # pass, the outcome this scanner exists to prevent. + brace_at = masked_stripped.find("{") + semicolon_at = masked_stripped.find(";") + declares_only = semicolon_at != -1 and ( + brace_at == -1 or semicolon_at < brace_at + ) + + if item is not None and item.group(1) == "mod" and not declares_only: + opening = ( + item.group(2), + entry_depth, + enters_test_scope or any(s for _, _, s in stack), + ) + elif item is not None or ";" in masked_stripped: + # Another item, or the `;` of a `mod foo;` declaration: the + # pending `mod` never opened a block. + opening = None + + depth += masked_line.count("{") - masked_line.count("}") + + # Pushed when the brace actually arrives, which is not always the + # header line. Requiring it there re-created the very false pass + # this scanner exists to remove — a `mod outer` with its `{` + # below it left everything inside classified as outside a test + # scope, and truncated the qualified path back to the bare-name + # over-match. + if opening is not None and depth > opening[1]: + stack.append(opening) + opening = None + while stack and depth <= stack[-1][1]: + stack.pop() return subjects @@ -519,11 +599,18 @@ def subject_matcher(subject: Subject) -> re.Pattern[str]: """Match a nextest test name belonging to ``subject``. Test names are ``::``-separated module paths, so the subject is a - whole path segment — anchoring on the separators keeps + whole run of path segments — anchoring on the separators keeps ``a_type_declared_inside_a_function`` from matching ``a_type_declared_inside_a_function_reaches_the_root_rollup``. + + The *qualified* name, not the bare one (#1472). Admitting a nested + ``mod`` as a checkable subject means a subject can be called + ``tests``, and a bare-name match for that hits every ``::tests::`` + path in the crate — failing every disjoint leg. The enclosing module + names are what make it specific again. """ - return re.compile(rf"(?:^|::){re.escape(subject.name)}(?:::|$)") + qualified = "::".join(re.escape(part) for part in subject.qualified_name.split("::")) + return re.compile(rf"(?:^|::){qualified}(?:::|$)") # --------------------------------------------------------------------------- diff --git a/utils/check-test-lang-gates-test.py b/utils/check-test-lang-gates-test.py new file mode 100755 index 000000000..a491eaf36 --- /dev/null +++ b/utils/check-test-lang-gates-test.py @@ -0,0 +1,1800 @@ +#!/usr/bin/env python3 +"""Tests for check-test-lang-gates.py. + +Three kinds of test, matching the other `…-test.py` gates: + +* **lexer and predicate units** — the pieces a scanning gate goes + quietly wrong in. A gate that stops matching reports a clean tree, so + these are the only thing standing between a broken regex and a check + that has silently switched itself off. +* **derivation units** — the role rules (test / sweep / helper), the + dispatcher and inner-`cfg` exclusions, and the `not(feature = …)` + case. Each is a shape that has already produced a wrong gate during + #1472; the fixtures are cut down from the real ones. +* **repository smoke tests** — the live tree must be clean, and the + derivation must still reproduce every gate a human wrote by hand. + That second one is the load-bearing check: it is what earned the + derivation the right to generate the other ~2,950. + +Run with: python3 -m unittest -q utils/check-test-lang-gates-test.py +""" + +from __future__ import annotations + +import contextlib +import importlib.util +import io +import os +import pathlib +import subprocess +import sys +import tempfile +import types +import unittest + +SCRIPT_SRC = pathlib.Path(__file__).resolve().parent / "check-test-lang-gates.py" +REPO_ROOT = SCRIPT_SRC.parent.parent + + +def _load_module() -> types.ModuleType: + spec = importlib.util.spec_from_file_location("check_test_lang_gates", SCRIPT_SRC) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + # Registered before execution because `@dataclass` resolves a + # field's forward reference through `sys.modules[cls.__module__]`; + # an unregistered module makes that lookup `None` and the decorator + # raises at import time. + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +gate = _load_module() + +# A cut-down `mk_langs!`. It carries the rows whose feature name does not +# match their variant, plus `cpp` — which is what makes the +# `c-family-helpers` implication testable, since disabling the helper +# feature is only meaningful when something that enables it is present. +LANGS_FIXTURE = """ +mk_langs!( + // 1) Cargo feature name + ( + "python", + Python, + "The `Python` language", + "python", + PythonCode, + PythonParser, + tree_sitter_python, + [py], + [], + "0.25.0" + ), + ( + "typescript", + Tsx, + "The `Tsx` language", + "tsx", + TsxCode, + TsxParser, + tree_sitter_tsx, + [tsx], + [], + "0.23.2" + ), + ( + "cpp", + Cpp, + "The `C++` language", + "cpp", + CppCode, + CppParser, + tree_sitter_cpp, + [cpp], + [], + "0.23.4" + ), + ( + "c-family-helpers", + Preproc, + "The `PreProc` language", + "preproc", + PreprocCode, + PreprocParser, + tree_sitter_preproc, + [], + [], + "2.2.1" + ), + ( + "rust", + Rust, + "The `Rust` language", + "rust", + RustCode, + RustParser, + tree_sitter_rust, + [rs], + [], + "0.24.0" + ) +) +""" + + +class LanguageTableTest(unittest.TestCase): + def setUp(self) -> None: + self.table = gate.language_table(LANGS_FIXTURE) + + def test_keys_are_qualified_never_the_bare_variant(self) -> None: + # `C`, `Go`, `Java` and `Rust` are ordinary identifiers in these + # files; a table keyed on the bare variant derives gates for + # languages the item never touches. + self.assertIn("LANG::Rust", self.table) + self.assertIn("RustParser", self.table) + self.assertNotIn("Rust", self.table) + + def test_the_three_rows_whose_feature_is_not_their_slug(self) -> None: + self.assertEqual(self.table["TsxParser"], "typescript") + self.assertEqual(self.table["LANG::Tsx"], "typescript") + self.assertEqual(self.table["PreprocParser"], "c-family-helpers") + self.assertEqual(self.table["TsxCode"], "typescript") + self.assertEqual(self.table["LANG::Python"], "python") + + def test_an_empty_table_is_refused_rather_than_passing(self) -> None: + with self.assertRaises(gate.ScanError): + gate.language_table("fn main() {}") + + def test_a_row_missing_a_field_is_refused_rather_than_bleeding(self) -> None: + """`.*?` spans the descriptions, so a short row eats the next one. + + Without the check it silently files the *following* row's + `*Code` / `*Parser` under this row's feature — a wrong language + for two spellings, with no error anywhere. + """ + short = LANGS_FIXTURE.replace(" PythonCode,\n", "") + with self.assertRaises(gate.ScanError) as caught: + gate.language_table(short) + self.assertIn("run into the next one", str(caught.exception)) + + def test_the_pattern_does_not_match_a_longer_identifier(self) -> None: + pattern = gate.symbol_pattern(self.table) + self.assertEqual( + [ + m.group(0) + for m in pattern.finditer("RustParser RustParserMock LANG::Rust") + ], + ["RustParser", "LANG::Rust"], + ) + + +class PredicateTest(unittest.TestCase): + def test_any_all_not_and_nesting(self) -> None: + off = frozenset({"python"}) + self.assertFalse(gate.evaluate_predicate('feature = "python"', off)) + self.assertTrue(gate.evaluate_predicate('feature = "rust"', off)) + self.assertTrue( + gate.evaluate_predicate('any(feature = "python", feature = "rust")', off) + ) + self.assertFalse( + gate.evaluate_predicate('all(feature = "python", feature = "rust")', off) + ) + self.assertTrue(gate.evaluate_predicate('not(feature = "python")', off)) + self.assertTrue( + gate.evaluate_predicate('all(test, any(feature = "rust"))', off) + ) + + def test_non_feature_atoms_are_true_so_a_gate_only_ever_narrows(self) -> None: + # `test`, `unix`, `debug_assertions` and a non-language feature + # must not excuse a missing language gate. + self.assertTrue(gate.evaluate_predicate("test", frozenset({"python"}))) + self.assertTrue( + gate.evaluate_predicate('feature = "vcs-git"', frozenset({"python"})) + ) + + def test_disabling_the_helper_feature_disables_its_enablers(self) -> None: + # `c`, `cpp` and `mozcpp` each enable `c-family-helpers`, so a + # build without the helper grammars has none of the three. + self.assertEqual( + gate.disabled_closure("c-family-helpers"), + frozenset({"c-family-helpers", "c", "cpp", "mozcpp"}), + ) + self.assertEqual(gate.disabled_closure("python"), frozenset({"python"})) + self.assertFalse(gate.gate_admits('feature = "cpp"', "c-family-helpers")) + + def test_an_unbalanced_predicate_is_refused(self) -> None: + with self.assertRaises(gate.ScanError): + gate.evaluate_predicate('any(feature = "rust"', frozenset()) + + def test_gate_excludes_spots_a_deliberate_disabled_path_test(self) -> None: + self.assertTrue(gate.gate_excludes('not(feature = "python")', "python")) + self.assertFalse(gate.gate_excludes('feature = "python"', "python")) + self.assertFalse(gate.gate_excludes(None, "python")) + + +def _scan(source: str, path: str = "src/fixture.rs") -> list: + return gate.scan_source(source, path, gate.language_table(LANGS_FIXTURE)) + + +def _named(items: list, name: str): + return next(item for item in items if item.name == name) + + +class DerivationTest(unittest.TestCase): + def test_a_plain_test_needs_the_language_it_names(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[test] + fn python_thing() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual( + needs[_named(items, "python_thing").index], frozenset({"python"}) + ) + self.assertEqual( + [i.name for i, _ in gate.offenders(items, needs)], ["python_thing"] + ) + + def test_production_code_is_left_alone(self) -> None: + # The same reference outside a test scope is the library doing + # its job, and gating it would be a bug. + items = _scan( + """ +fn build() { + let _ = PythonParser::new(b"".to_vec()); +} +""" + ) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + def test_a_correct_gate_is_accepted_and_a_wrong_one_is_not(self) -> None: + source = """ +#[cfg(test)] +mod tests { + #[cfg(feature = "%s")] + #[test] + fn python_thing() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + right = _scan(source % "python") + self.assertEqual(gate.offenders(right, gate.resolve_needs(right)), []) + wrong = _scan(source % "rust") + self.assertEqual( + [i.name for i, _ in gate.offenders(wrong, gate.resolve_needs(wrong))], + ["python_thing"], + ) + + def test_a_multi_language_test_needs_all_of_them(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[test] + fn parity() { + check::("a", "f.py"); + check::("b", "f.rs"); + } +} +""" + ) + needs = gate.resolve_needs(items) + item = _named(items, "parity") + self.assertEqual( + gate.required_marker(items, item, needs), + 'all(feature = "python", feature = "rust")', + ) + # `any(...)` is not enough: a Rust-only build would compile it + # and then panic on the Python half. + satisfied = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(any(feature = "python", feature = "rust"))] + #[test] + fn parity() { + check::("a", "f.py"); + check::("b", "f.rs"); + } +} +""" + ) + self.assertEqual( + [ + i.name + for i, _ in gate.offenders(satisfied, gate.resolve_needs(satisfied)) + ], + ["parity"], + ) + + def test_a_reference_behind_an_inner_cfg_is_already_conditional(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[test] + fn either() { + #[cfg(feature = "python")] + { + check::("a", "f.py"); + } + } +} +""" + ) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + def test_a_language_used_both_ways_is_still_needed(self) -> None: + """Exclusion is positional, not a feature-wide subtraction. + + Subtracting the whole feature made a language named + unconditionally *and* inside its own `#[cfg]` vanish from the + needs entirely, so the test got no gate at all and panicked + without the grammar — the unsafe direction. + """ + items = _scan( + """ +#[cfg(test)] +mod tests { + #[test] + fn both_ways() { + check::("a", "f.py"); + #[cfg(feature = "python")] + { + check::("b", "f2.py"); + } + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual(needs[_named(items, "both_ways").index], frozenset({"python"})) + + def test_a_cfg_gated_tuple_row_is_conditional_too(self) -> None: + # The rows of a fixture array are parenthesised, not braced; + # counting only `{}` ends the extent on the opening paren and + # leaves every `LANG::` inside the row looking unconditional. + items = _scan( + """ +#[cfg(test)] +mod tests { + #[test] + fn table() { + let cases = &[ + #[cfg(feature = "python")] + ( + LANG::Python, + "f.py", + ), + ]; + } +} +""" + ) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + def test_a_helper_that_hardcodes_a_parser_reaches_its_callers(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + fn spells(src: &str) { + assert_fixture_spells::(src, "f.py"); + } + + #[test] + fn uses_the_helper() { + spells("a = 1"); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual( + needs[_named(items, "uses_the_helper").index], frozenset({"python"}) + ) + + def test_a_lang_parameterised_helper_does_not(self) -> None: + # Its body names languages as dispatch arms; propagating them + # marks every caller as needing every language in the match. + items = _scan( + """ +#[cfg(test)] +mod tests { + fn conditions(lang: LANG, src: &str) -> u64 { + metrics_verbatim(lang, src.as_bytes()) + } + + #[test] + fn rust_only() { + conditions(LANG::Rust, "fn f() {}"); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual(needs[_named(items, "rust_only").index], frozenset({"rust"})) + + def test_a_compared_lang_is_an_identity_test_not_a_use(self) -> None: + """#1478. `LANG` variants exist without their grammars. + + Asking which one a value *is* parses nothing, so counting the + comparison as a requirement conjoins a feature onto the gate and + drops the test from every build without it — + `container_scope_tests.rs` lost the positive half of the #1197 + contract to exactly this. + """ + items = _scan( + """ +#[cfg(test)] +mod tests { + #[test] + fn python_only() { + check_metrics::("a = 1", "f.py", |m| {}); + assert_eq!(space.lang == LANG::Rust, false); + assert!(matches!(space.lang, LANG::Cpp)); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual( + needs[_named(items, "python_only").index], frozenset({"python"}) + ) + + def test_a_dispatcher_keyed_on_a_string_does_not_either(self) -> None: + # `takes_lang_param` cannot see this one: the parameter is a + # path. Without the match-arm rule all of its callers look as + # though they need every language it can return. + items = _scan( + """ +#[cfg(test)] +mod tests { + fn analyze_lang(path: &str) -> FuncSpace { + let lang = match ext { + "py" => LANG::Python, + "rs" => LANG::Rust, + other => panic!("{other}"), + }; + analyze(lang) + } + + #[test] + fn a_python_case() { + analyze_lang("f.py"); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual(needs[_named(items, "a_python_case").index], frozenset()) + + def test_a_helper_is_resolved_in_its_own_module_not_by_bare_name(self) -> None: + # `spaces_tests.rs` declares three `analyse` helpers in sibling + # modules, one hardcoding Rust. A flat by-name map makes every + # test in the other two look as though it needs Rust. + items = _scan( + """ +#[cfg(test)] +mod a { + fn analyse(src: &str) { check::(src, "f.rs"); } + + #[test] + fn rust_case() { analyse("fn f() {}"); } +} + +#[cfg(test)] +mod b { + fn analyse(lang: LANG, src: &str) { metrics_verbatim(lang, src); } + + #[test] + fn python_case() { analyse(LANG::Python, "a = 1"); } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual(needs[_named(items, "rust_case").index], frozenset({"rust"})) + self.assertEqual( + needs[_named(items, "python_case").index], frozenset({"python"}) + ) + + def test_a_disabled_path_test_is_not_asked_to_enable_the_language(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(not(feature = "python"))] + #[test] + fn disabled_language_reports_language_disabled() { + assert!(matches!(parse(LANG::Python), Err(LanguageDisabled(_)))); + } +} +""" + ) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + def test_a_sweep_needs_any_row_not_every_row(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(any(feature = "python", feature = "rust"))] + #[test] + fn sweep() { + for lang in LANG::into_enum_iter().filter(LANG::is_enabled) { + let _ = (LANG::Python, LANG::Rust, lang); + } + } +} +""" + ) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + def test_a_reference_in_a_string_or_comment_is_not_a_reference(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[test] + fn only_looks_like_one() { + // check_metrics::(…) + let src = r#"PythonParser and LANG::Python"#; + assert_eq!(src.len(), 30); + } +} +""" + ) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + def test_a_macro_rules_body_is_a_template_not_an_item(self) -> None: + """The transcriber is a token tree, and `--fix` must not write in it. + + A fixture using only `fn $lang()` cannot fail: `$` is not + matched by `ITEM_RE`, so nothing leaks whether the skip works or + not. The literal `fn inner_probe()` is what makes this + discriminating, and the positive assertion on the macro item is + what pins that it is recorded at all. + """ + items = _scan( + """ +#[cfg(test)] +mod tests { + macro_rules! roundtrip_tests { + () => { + fn inner_probe() { check::("a", "f.py"); } + }; + } +} +""" + ) + kinds = {(item.kind, item.name) for item in items} + self.assertIn(("macro", "roundtrip_tests"), kinds) + self.assertNotIn("inner_probe", {name for _, name in kinds}) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + def test_an_attributed_macro_is_recognised_and_marked_in_place(self) -> None: + """Reading `attrs` before binding them broke this three ways. + + The macro went unrecognised, so its transcriber was walked as + ordinary source and a marker could be written inside the + template; and the item it did record inherited the *previous* + line's `attr_line`, putting any marker on the item above. + """ + items = _scan( + """ +#[cfg(test)] +mod tests { + #[allow(unused_macros)] + macro_rules! gen { + () => { + fn inner_probe() { check::("a", "f.py"); } + }; + } +} +""" + ) + macro = _named(items, "gen") + self.assertEqual(macro.kind, "macro") + # The `#[allow]`, not the `mod tests {` line above it. + self.assertEqual(macro.attr_line, macro.line - 1) + self.assertNotIn("inner_probe", {item.name for item in items}) + + def test_a_leading_macro_rules_does_not_crash_the_scan(self) -> None: + """A file whose first scanned line is a macro raised UnboundLocalError. + + It escaped the `main()` try as a traceback, so the exit status + was 1 — which a caller reads as "offenders found". + """ + items = _scan("macro_rules! foo {\n}\n", path="tests/api/x.rs") + self.assertEqual([(i.kind, i.name) for i in items], [("macro", "foo")]) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + def test_an_integration_file_is_a_test_scope_without_cfg_test(self) -> None: + items = _scan( + """ +#[test] +fn integration() { + check::("a = 1", "f.py"); +} +""", + path="tests/api/thing.rs", + ) + self.assertEqual( + [i.name for i, _ in gate.offenders(items, gate.resolve_needs(items))], + ["integration"], + ) + + +class FeatureAtomTest(unittest.TestCase): + """`feature_atom` is invisible to every other check in this file. + + Deleting the root-crate spelling leaves the repository scan green, + because `gate_admits` answers the same either way — the difference + only shows up in a real `cargo test --features c`. Its own docstring + calls getting this wrong silent, and 33 root-crate markers depend on + it, so it needs asserting directly. + """ + + def test_an_ordinary_feature_is_spelled_the_same_in_both_crates(self) -> None: + for path in ("src/metrics/abc.rs", "big-code-analysis-ast/src/checker.rs"): + self.assertEqual(gate.feature_atom("python", path), 'feature = "python"') + + def test_the_ast_crate_can_name_the_helper_feature_directly(self) -> None: + # There `cpp`/`c`/`mozcpp` each list `c-family-helpers`, so the + # bare atom is true whenever the helper grammars are compiled. + self.assertEqual( + gate.feature_atom( + "c-family-helpers", "big-code-analysis-ast/src/checker.rs" + ), + 'feature = "c-family-helpers"', + ) + + def test_the_root_crate_must_name_the_enablers_instead(self) -> None: + # The root's `cpp` forwards to `big-code-analysis-ast/cpp` and + # leaves its own `c-family-helpers` off, and `all-languages` does + # not list it — so the bare atom is false in a default build and + # the item disappears from it. + self.assertEqual( + gate.feature_atom("c-family-helpers", "src/metrics/loc.rs"), + 'any(feature = "c", feature = "c-family-helpers", ' + 'feature = "cpp", feature = "mozcpp")', + ) + + def test_c_family_helper_enablers_match_the_manifest(self) -> None: + """The one table in this gate that is hand-copied, not derived.""" + import tomllib + + manifest = tomllib.loads( + (REPO_ROOT / "big-code-analysis-ast" / "Cargo.toml").read_text() + ) + enablers = { + name + for name, enables in manifest["features"].items() + if gate.C_FAMILY_HELPER_FEATURE in enables + } + self.assertEqual(enablers, set(gate.IMPLIES_C_FAMILY_HELPERS)) + + +class SweepMarkerTest(unittest.TestCase): + def test_a_sweep_is_marked_any_not_all(self) -> None: + """`all(...)` here gates the 23-language parity suites out of + every build but the full one, and takes their helpers dead with + them. The existing sweep test only asserts `offenders == []`, + which exercises the other branch.""" + items = _scan( + """ +#[cfg(test)] +mod tests { + #[test] + fn sweep() { + for lang in LANG::into_enum_iter().filter(LANG::is_enabled) { + let _ = (LANG::Python, LANG::Rust, lang); + } + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual( + gate.required_marker(items, _named(items, "sweep"), needs), + 'any(feature = "python", feature = "rust")', + ) + + +class HelperInheritanceTest(unittest.TestCase): + def test_a_helper_inherits_the_gate_of_the_tests_that_call_it(self) -> None: + """The caller-to-callee widening fixpoint, which had no coverage. + + Deleting it left the whole suite green. + """ + items = _scan( + """ +#[cfg(test)] +mod tests { + fn shape(space: &FuncSpace) -> String { String::new() } + + #[test] + fn python_case() { + check_metrics::("a = 1", "f.py", |m| { shape(&m); }); + } + + #[test] + fn rust_case() { + check_metrics::("fn f() {}", "f.rs", |m| { shape(&m); }); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual( + needs[_named(items, "shape").index], frozenset({"python", "rust"}) + ) + self.assertEqual( + gate.required_marker(items, _named(items, "shape"), needs), + 'any(feature = "python", feature = "rust")', + ) + + +class OverGatedTest(unittest.TestCase): + """#1478. The direction no other check can see. + + A gate too *wide* panics on the leg that lacks the grammar. A gate + too *narrow* just drops the test, and the leg still looks green — so + the only thing that can notice is the derivation itself. + """ + + def test_a_gate_naming_a_language_the_body_never_uses_is_reported(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(all(feature = "python", feature = "rust"))] + #[test] + fn only_needs_python() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual( + [(i.name, sorted(w)) for i, w in gate.over_gated(items, needs)], + [("only_needs_python", ["rust"])], + ) + + def test_an_exact_gate_is_not_reported(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(feature = "python")] + #[test] + fn only_needs_python() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + ) + self.assertEqual(gate.over_gated(items, gate.resolve_needs(items)), []) + + def test_a_marker_accepts_the_features_it_names(self) -> None: + source = """ +#[cfg(test)] +mod tests { + // test-lang-gates: hand-written(%s) — the language comes from a + // glob, which no scanner can read. + #[cfg(all(feature = "python", feature = "rust"))] + #[test] + fn only_needs_python() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + accepted = _scan(source % "rust") + self.assertEqual(gate.over_gated(accepted, gate.resolve_needs(accepted)), []) + # Naming the wrong feature does not silence the real one. + wrong = _scan(source % "typescript") + self.assertEqual( + [sorted(w) for _, w in gate.over_gated(wrong, gate.resolve_needs(wrong))], + [["rust"]], + ) + + def test_a_disabled_path_gate_is_not_over_gating(self) -> None: + # `cfg(not(feature = "python"))` is false with everything on, so + # every feature would read as "required" and the whole tree would + # report. It names the language precisely because it is absent. + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(not(feature = "python"))] + #[test] + fn disabled_language_reports_language_disabled() { + assert!(matches!(parse(LANG::Python), Err(LanguageDisabled(_)))); + } +} +""" + ) + self.assertEqual(gate.over_gated(items, gate.resolve_needs(items)), []) + + def test_a_feature_that_would_disable_a_needed_one_is_not_over_gating( + self, + ) -> None: + # A `cpp` test is excluded from a build without + # `c-family-helpers`, but only because that build has no `cpp` + # either. There is no configuration it could have run in. + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(feature = "cpp")] + #[test] + fn a_cpp_test() { + check_metrics::("int f(){}", "f.cpp", |m| {}); + } +} +""", + ) + self.assertEqual(gate.over_gated(items, gate.resolve_needs(items)), []) + + def test_the_reference_build_turns_off_only_what_the_gate_wants_off( + self, + ) -> None: + """A negated conjunct must not exempt the whole item. + + `all(not(python), rust, go)` is false with everything enabled, + so measuring from an all-on baseline reads every feature as + required and the item escapes entirely. Measuring from a build + that satisfies the negation finds the `go` that is genuinely + over-declared. + """ + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(all(not(feature = "python"), feature = "rust", feature = "typescript"))] + #[test] + fn rust_without_python() { + check_metrics::("fn f() {}", "f.rs", |m| {}); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual( + [(i.name, sorted(w)) for i, w in gate.over_gated(items, needs)], + [("rust_without_python", ["typescript"])], + ) + + def test_a_feature_the_gate_never_spells_is_not_reported(self) -> None: + """The closure runs one way only. + + A gate reading `feature = "cpp"` does exclude every build + without `c-family-helpers` — such a build has no `cpp` — but the + gate never mentions the helper and narrowing it would change + nothing. Reporting it made every C-family gate in the tree carry + a marker for a feature its author never wrote. + """ + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(feature = "cpp")] + #[test] + fn walks_a_corpus() { + for file in glob("corpus/**/*") { + let _ = analyze(file); + } + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual( + [(i.name, sorted(w)) for i, w in gate.over_gated(items, needs)], + [("walks_a_corpus", ["cpp"])], + ) + + def test_a_pub_item_is_not_reported(self) -> None: + # Its callers are in files this scanner never sees, so `needs` + # is only the visible subset and a gate placed for one of the + # others reads as over-declared. `offenders` skips them too. + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(feature = "python")] + pub(crate) fn assert_python_fixture_spells(source: &str) { + assert!(!source.is_empty()); + } +} +""" + ) + self.assertEqual(gate.over_gated(items, gate.resolve_needs(items)), []) + + def test_a_sweeps_union_gate_never_reports(self) -> None: + # `any(...)` requires no feature on its own. + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(any(feature = "python", feature = "rust"))] + #[test] + fn sweep() { + for lang in LANG::into_enum_iter().filter(LANG::is_enabled) { + let _ = (LANG::Python, LANG::Rust, lang); + } + } +} +""" + ) + self.assertEqual(gate.over_gated(items, gate.resolve_needs(items)), []) + + +class StaleMarkerTest(unittest.TestCase): + """An accepted gate that stopped needing accepting. + + A stale marker silences nothing, so it can never cause a wrong + verdict on its own. It still tells the next reader the gate is wider + than the body for a reason that no longer applies, and a list of + accepted gates only stays readable as a census while every one of + them is load-bearing. + """ + + def test_a_marker_for_a_feature_that_is_not_over_declared_is_reported( + self, + ) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + // test-lang-gates: hand-written(rust) — no longer true. + #[cfg(feature = "python")] + #[test] + fn only_needs_python() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual( + [(i.name, sorted(w)) for i, w in gate.stale_markers(items, needs)], + [("only_needs_python", ["rust"])], + ) + + def test_a_load_bearing_marker_is_not_reported(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + // test-lang-gates: hand-written(rust) — the language comes from a + // glob, which no scanner can read. + #[cfg(all(feature = "python", feature = "rust"))] + #[test] + fn only_needs_python() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual(gate.stale_markers(items, needs), []) + self.assertEqual(gate.over_gated(items, needs), []) + + def test_a_doc_comment_is_not_a_marker(self) -> None: + # `///` renders into the crate documentation. A directive read + # out of prose is one a reader has no reason to think is live. + items = _scan( + """ +#[cfg(test)] +mod tests { + /// test-lang-gates: hand-written(rust) — in a doc comment. + #[cfg(all(feature = "python", feature = "rust"))] + #[test] + fn only_needs_python() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + ) + needs = gate.resolve_needs(items) + self.assertEqual(_named(items, "only_needs_python").hand_written, frozenset()) + self.assertEqual( + [sorted(w) for _, w in gate.over_gated(items, needs)], [["rust"]] + ) + + +class ComparisonSpellingTest(unittest.TestCase): + """Every spelling of "which variant is this" the tree uses. + + One of these escaping the rule conjoins a feature onto the gate and + drops the test from every build without it, silently — the #1197 + shape. They are listed separately because each is a separate regex + and a passing sibling proves nothing about the others. + """ + + def _needs(self, body: str) -> frozenset: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[test] + fn python_only() { + check_metrics::("a = 1", "f.py", |m| {}); + %s + } +} +""" + % body + ) + return gate.resolve_needs(items)[_named(items, "python_only").index] + + def test_the_variant_may_come_first(self) -> None: + self.assertEqual(self._needs("assert!(LANG::Tsx == space.lang);"), {"python"}) + + def test_a_crate_qualified_path_is_still_a_comparison(self) -> None: + self.assertEqual( + self._needs("assert!(space.lang == crate::LANG::Tsx);"), {"python"} + ) + self.assertEqual( + self._needs("assert!(matches!(space.lang, crate::LANG::Tsx));"), {"python"} + ) + + def test_an_assert_eq_is_a_comparison(self) -> None: + self.assertEqual(self._needs("assert_eq!(space.lang, LANG::Tsx);"), {"python"}) + self.assertEqual(self._needs("assert_ne!(space.lang, LANG::Tsx);"), {"python"}) + + def test_a_real_use_is_still_a_use(self) -> None: + # The guard on all of the above, and on the fixture: the rule + # must not swallow a construction, and a variant the table does + # not carry would make every assertion here vacuously true. + self.assertEqual( + self._needs("let _ = analyze(LANG::Tsx, source);"), + {"python", "typescript"}, + ) + + +class SweepHardcodesTest(unittest.TestCase): + def test_a_sweep_still_needs_the_parser_it_hardcodes(self) -> None: + """#1478. The exemption was a way round the whole gate. + + A sweep skips disabled languages at run time, so its rows need + only `any`. A parser it picks through a *type parameter* cannot + be skipped by any runtime filter, so that one is required — and + without this the union gate admits the test into a build that + panics in `Tree::new`. + """ + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(any(feature = "python", feature = "rust"))] + #[test] + fn sweep_that_also_hardcodes() { + for lang in LANG::into_enum_iter().filter(LANG::is_enabled) { + let _ = (LANG::Python, lang); + } + check_metrics::("fn f() {}", "f.rs", |m| {}); + } +} +""" + ) + needs = gate.resolve_needs(items) + item = _named(items, "sweep_that_also_hardcodes") + self.assertTrue(item.is_sweep) + self.assertEqual(item.hardcoded, frozenset({"rust"})) + self.assertEqual( + [(i.name, sorted(w)) for i, w in gate.offenders(items, needs)], + [("sweep_that_also_hardcodes", ["rust"])], + ) + + def test_a_sweep_naming_only_lang_values_is_still_exempt(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(any(feature = "python", feature = "rust"))] + #[test] + fn sweep() { + for lang in LANG::into_enum_iter().filter(LANG::is_enabled) { + let _ = (LANG::Python, LANG::Rust, lang); + } + } +} +""" + ) + items_by_name = _named(items, "sweep") + self.assertEqual(items_by_name.hardcoded, frozenset()) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + def test_a_pin_one_call_away_still_pins(self) -> None: + """#1478's own example. `hardcoded` has to propagate. + + `needs` already travels through helpers; without the same + propagation here a sweep whose only fixed-parser call sits one + hop away reads as pinning nothing, and the union gate admits it + into a build that panics in `Tree::new`. + """ + items = _scan( + """ +#[cfg(test)] +mod tests { + fn parse_rust(source: &str) { + check_metrics::(source, "f.rs", |m| {}); + } + + #[cfg(any(feature = "python", feature = "rust"))] + #[test] + fn sweep_whose_pin_is_one_call_away() { + for lang in LANG::into_enum_iter().filter(LANG::is_enabled) { + let _ = (LANG::Python, lang); + } + parse_rust("fn f() {}"); + } +} +""" + ) + item = _named(items, "sweep_whose_pin_is_one_call_away") + # The item itself names no parser -- the pin is the helper's. + self.assertEqual(item.hardcoded, frozenset()) + self.assertEqual(gate.hardcoded_closure(items, item), frozenset({"rust"})) + reported = { + i.name: sorted(w) + for i, w in gate.offenders(items, gate.resolve_needs(items)) + } + self.assertEqual(reported["sweep_whose_pin_is_one_call_away"], ["rust"]) + + def test_a_helper_that_hardcodes_is_not_pinned(self) -> None: + """A helper is live when a caller is, and simply not called + otherwise — `halstead.rs`'s `assert_js_family_counts` binds three + parsers and is correctly gated `any` of them.""" + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(any(feature = "python", feature = "rust"))] + fn assert_both(source: &str) { + check::(source, "f.py"); + check::(source, "f.rs"); + } + + #[cfg(all(feature = "python", feature = "rust"))] + #[test] + fn uses_both() { assert_both("x"); } +} +""" + ) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + +class MarkerTest(unittest.TestCase): + def test_a_short_marker_stays_on_one_line(self) -> None: + self.assertEqual( + gate.wrap_marker('feature = "python"', " "), + [' #[cfg(feature = "python")]'], + ) + + def test_a_long_marker_wraps_the_way_rustfmt_would(self) -> None: + predicate = "any(" + ", ".join(f'feature = "lang{i}"' for i in range(12)) + ")" + lines = gate.wrap_marker(predicate, "") + self.assertEqual(lines[0], "#[cfg(any(") + self.assertEqual(lines[-1], "))]") + self.assertEqual(len(lines), 14) + self.assertTrue(all(len(line) <= gate.MAX_WIDTH for line in lines)) + + +class FixTest(unittest.TestCase): + def test_fix_inserts_above_the_attribute_run_and_the_gate_then_passes(self) -> None: + source = """#[cfg(test)] +mod tests { + /// Doc comment. + #[test] + fn python_thing() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "src").mkdir() + (root / "big-code-analysis-ast" / "src").mkdir(parents=True) + (root / "big-code-analysis-ast" / "src" / "langs.rs").write_text( + LANGS_FIXTURE + ) + target = root / "src" / "fixture.rs" + target.write_text(source) + + out, err = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): + self.assertEqual(gate.main(["--root", str(root)]), 1) + self.assertEqual(gate.main(["--root", str(root), "--fix"]), 0) + + # The marker joins the attribute stack, under the doc + # comment and above `#[test]` -- not between `#[test]` and + # the signature, and not above the documentation. + self.assertIn( + ' /// Doc comment.\n #[cfg(feature = "python")]\n #[test]\n', + target.read_text(), + ) + # Idempotent, and the tree is clean afterwards. + with contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(gate.main(["--root", str(root)]), 0) + + def test_fix_converges_on_a_sweep_that_pins_a_parser(self) -> None: + """The marker `--fix` writes must satisfy the check that asked. + + A sweep needs `any` of the rows it iterates and `all` of what it + pins by type. Writing only the `any` half leaves the pin + unguarded, so the next pass asks for the same marker again -- + `--fix` stacked one `#[cfg]` per iteration and then exited 2, + having made the tree worse than it found it. + """ + source = """#[cfg(test)] +mod tests { + #[test] + fn sweep_that_also_pins() { + for lang in LANG::into_enum_iter().filter(LANG::is_enabled) { + let _ = (LANG::Python, LANG::Tsx, lang); + } + check_metrics::("fn f() {}", "f.rs", |m| {}); + } +} +""" + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "src").mkdir() + (root / "big-code-analysis-ast" / "src").mkdir(parents=True) + (root / "big-code-analysis-ast" / "src" / "langs.rs").write_text( + LANGS_FIXTURE + ) + target = root / "src" / "fixture.rs" + target.write_text(source) + + with ( + contextlib.redirect_stdout(io.StringIO()), + contextlib.redirect_stderr(io.StringIO()), + ): + self.assertEqual(gate.main(["--root", str(root), "--fix"]), 0) + # The pass that stacked: clean in one, not two. + self.assertEqual(gate.main(["--root", str(root)]), 0) + + written = target.read_text() + self.assertIn( + ' #[cfg(all(feature = "rust", ' + 'any(feature = "python", feature = "typescript")))]\n', + written, + ) + self.assertEqual(written.count("#[cfg("), 2) + + def test_both_directions_are_reported_in_one_run(self) -> None: + """They are independent defects in independent items. + + Returning on the first report hid every under-gated item behind + any single over-gated one, so a `--fix`-able failure could be + invisible until an unrelated marker was written by hand. + """ + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "src").mkdir() + (root / "big-code-analysis-ast" / "src").mkdir(parents=True) + (root / "big-code-analysis-ast" / "src" / "langs.rs").write_text( + LANGS_FIXTURE + ) + (root / "src" / "wide.rs").write_text( + """#[cfg(test)] +mod tests { + #[cfg(all(feature = "python", feature = "rust"))] + #[test] + fn gated_too_wide() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + ) + (root / "src" / "narrow.rs").write_text( + """#[cfg(test)] +mod tests { + #[test] + fn gated_not_at_all() { + check_metrics::("fn f() {}", "f.rs", |m| {}); + } +} +""" + ) + err = io.StringIO() + with ( + contextlib.redirect_stdout(io.StringIO()), + contextlib.redirect_stderr(err), + ): + self.assertEqual(gate.main(["--root", str(root)]), 1) + report = err.getvalue() + self.assertIn("gated_too_wide", report) + self.assertIn("gated_not_at_all", report) + + def test_a_tree_of_production_code_only_is_not_a_pass(self) -> None: + """The guard counts tests, not items. + + `scan_source` returns production items too, so testing whether + *anything* was found let a tree with no tests at all report OK + having checked nothing — the silent switch-off these self-tests + exist to prevent. + """ + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "src").mkdir() + (root / "big-code-analysis-ast" / "src").mkdir(parents=True) + (root / "big-code-analysis-ast" / "src" / "langs.rs").write_text( + LANGS_FIXTURE + ) + (root / "src" / "lib.rs").write_text( + "pub fn parse() { let _ = PythonParser::new(vec![]); }\n" + ) + err = io.StringIO() + with ( + contextlib.redirect_stderr(err), + contextlib.redirect_stdout(io.StringIO()), + ): + self.assertEqual(gate.main(["--root", str(root)]), 2) + self.assertIn("no tests found", err.getvalue()) + + def test_an_empty_tree_fails_rather_than_passing_vacuously(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "src").mkdir() + (root / "big-code-analysis-ast" / "src").mkdir(parents=True) + (root / "big-code-analysis-ast" / "src" / "langs.rs").write_text( + LANGS_FIXTURE + ) + err = io.StringIO() + with contextlib.redirect_stderr(err): + self.assertEqual(gate.main(["--root", str(root)]), 2) + self.assertIn("no tests found", err.getvalue()) + + +class AssertionOperandTest(unittest.TestCase): + """A variant an assertion *compares* is not a variant it parses. + + The flat `[^()]*?` this shipped with stopped at the first `(` in the + argument list and never reached the variant, so 36 tests over + `get_from_ext` / `FromStr` / `name()` — none of which is `cfg`-gated + — were gated on grammars they never touch, several down to one build + in twenty-five. Invisible to `over_gated`, whose marker mirrored the + derivation, and to `--compare`, which the narrowing label skipped. + """ + + def _needs(self, body: str) -> frozenset: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[test] + fn t() { + %s + } +} +""" + % body + ) + return gate.resolve_needs(items)[_named(items, "t").index] + + def test_a_variant_behind_a_call_in_the_expected_value_is_a_comparison( + self, + ) -> None: + self.assertEqual( + self._needs('assert_eq!(get_from_ext("rs"), Some(LANG::Rust));'), + frozenset(), + ) + self.assertEqual( + self._needs('assert_eq!(pick(buf, "x"), (Some(LANG::Tsx), "tsx"));'), + frozenset(), + ) + self.assertEqual( + self._needs("assert_eq!(collected, vec![LANG::Rust, LANG::Python]);"), + frozenset(), + ) + + def test_a_variant_handed_to_the_call_under_test_is_still_a_use(self) -> None: + # The guard on the rule above: excluding the whole argument list + # would exempt the language this test actually parses. + self.assertEqual( + self._needs("assert_eq!(conditions(LANG::Rust, src), 3);"), + frozenset({"rust"}), + ) + self.assertEqual( + self._needs('assert_eq!(check::(src, "f.rs"), 3);'), + frozenset({"rust"}), + ) + + def test_a_variant_in_receiver_position_reads_metadata(self) -> None: + # `extensions()` / `name()` read the table `mk_langs!` generates + # unconditionally, so they need no grammar. + self.assertEqual( + self._needs("assert!(LANG::Rust.extensions().is_empty());"), frozenset() + ) + + def test_except_the_two_that_hand_back_the_grammar(self) -> None: + """`tree_sitter_language()` returns `None` with the feature off. + + Every caller here follows it with `.expect(…)`, so reading the + receiver as metadata gates the test out of nothing and lets it + panic under a build without that language — the same failure as + `Tree::new`, one call earlier. + """ + self.assertEqual( + self._needs("let _ = LANG::Rust.tree_sitter_language().unwrap();"), + frozenset({"rust"}), + ) + + +class NegatedGateTest(unittest.TestCase): + def test_a_conjunction_of_negations_finds_its_reference_build(self) -> None: + """Probing one feature at a time cannot. + + `all(not(a), not(b))` is false with either alone disabled, so no + single probe ever finds the build that satisfies it, and every + feature in the tree then reads as over-declared. Reading the + negated names out of the text instead is exact. + """ + self.assertEqual( + gate.negated_features( + 'all(not(feature = "python"), not(feature = "rust"))' + ), + frozenset({"python", "rust"}), + ) + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(all(not(feature = "python"), not(feature = "rust")))] + #[test] + fn disabled_both() { + check_metrics::("int f(){}", "f.cpp", |m| {}); + } +} +""" + ) + self.assertEqual(gate.over_gated(items, gate.resolve_needs(items)), []) + + def test_a_negated_test_cfg_is_not_a_test_scope(self) -> None: + # `cfg(not(test))` marks code that exists *outside* the test + # build; reading it as a test scope pulls production items into + # `--show` and into the helper-inheritance walk. + self.assertFalse(gate.predicate_requires_test("not(test)")) + self.assertTrue(gate.predicate_requires_test("test")) + self.assertTrue(gate.predicate_requires_test('all(test, feature = "rust")')) + + +class InnerAttributeTest(unittest.TestCase): + def test_a_module_gated_by_an_inner_attribute_is_seen(self) -> None: + """`#![cfg(...)]` inside the braces, not `#[cfg(...)]` above them. + + The branch that reads one was unreachable: every caller tested + `startswith("#[")` first, which an inner attribute fails. + """ + items = _scan( + """ +#[cfg(test)] +mod tests { + #![cfg(feature = "python")] + + #[test] + fn t() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + ) + self.assertEqual( + gate.effective_predicate(items, _named(items, "t")), + 'all(feature = "python", test)', + ) + self.assertEqual(gate.offenders(items, gate.resolve_needs(items)), []) + + +class DispatchArmTest(unittest.TestCase): + def test_a_wrapped_arm_is_still_a_dispatch_arm(self) -> None: + # `=> Some(LANG::Python)` is how a fallible lookup spells it. + # Read as a use, it hands every language the helper can return + # to each of its callers. + items = _scan( + """ +#[cfg(test)] +mod tests { + fn pick(ext: &str) -> Option { + match ext { + "py" => Some(LANG::Python), + "rs" => Some(LANG::Rust), + _ => None, + } + } + + #[test] + fn t() { + let _ = pick("py"); + } +} +""" + ) + self.assertEqual( + gate.resolve_needs(items)[_named(items, "t").index], frozenset() + ) + + +class FullEnumSweepTest(unittest.TestCase): + """A sweep over the whole enum, and what it may not derive from. + + Both rules here were found by `--compare` on its first CI run, and + neither `offenders` nor `over_gated` can see the defect: the gate was + an `any(...)`, which requires no single feature, and it faithfully + mirrored a derivation that was itself wrong. + """ + + SWEEP = """ +#[cfg(test)] +mod tests { + fn fixture(lang: LANG) -> (&'static str, &'static str) { + match lang { + LANG::Python => ("a = 1", "py"), + LANG::Rust => ("fn f() {}", "rs"), + _ => ("", ""), + } + } + + #[test] + fn every_language_holds_the_property() { + let mut checked = 0; + for lang in LANG::into_enum_iter() { + if !lang.is_enabled() { + continue; + } + checked += 1; + let (source, ext) = fixture(lang); + let _ = analyze(Source::new(lang, source.as_bytes()), ext); + %s + } + %s + } +} +""" + EXCLUSION = "if !matches!(lang, LANG::Ccomment | LANG::Preproc) { let _ = 1; }" + GUARD = 'assert!(checked > 0, "no language enabled");' + + def test_a_guarded_full_enum_sweep_needs_every_language(self) -> None: + """It *fails* rather than skips with no language enabled. + + So it has to be absent then — the #1220 class. Deriving anything + narrower gates it out of builds it would have run in, and the + narrower set cannot be read off the body at all when the + fixtures come from a `LANG`-parameterised helper. + """ + items = _scan(self.SWEEP % ("", self.GUARD)) + needs = gate.resolve_needs(items) + self.assertEqual( + needs[_named(items, "every_language_holds_the_property").index], + frozenset({"python", "typescript", "cpp", "c-family-helpers", "rust"}), + ) + + def test_an_unguarded_full_enum_sweep_is_left_alone(self) -> None: + # `Display`, `FromStr` and slug round-trips walk the same enum + # and parse nothing; the variants exist without their grammars. + # Gating those stops them running on the + # `--no-default-features` leg, which is where they belong. + items = _scan(self.SWEEP % ("", "")) + needs = gate.resolve_needs(items) + self.assertNotIn( + "typescript", + needs[_named(items, "every_language_holds_the_property").index], + ) + + def test_every_alternative_of_a_matches_is_an_identity_test(self) -> None: + """Not just the first one. + + The leading alternative was already excluded; the rest were not, + so a `matches!(lang, LANG::Ccomment | LANG::Preproc)` written to + *skip* two languages read as a requirement for one of them. That + single leak was the entire gate on two `every_*_in_every_language` + parity sweeps, which ran in four builds instead of twenty-three. + """ + guarded = _scan(self.SWEEP % (self.EXCLUSION, self.GUARD)) + needs = gate.resolve_needs(guarded) + item = _named(guarded, "every_language_holds_the_property") + # Correct because of the sweep rule, whichever way the `matches!` + # is read -- so assert the narrow case too, where the leak is the + # only thing that could contribute. + self.assertEqual(len(needs[item.index]), 5) + + bare = _scan(self.SWEEP % (self.EXCLUSION, "")) + leaked = gate.resolve_needs(bare)[ + _named(bare, "every_language_holds_the_property").index + ] + self.assertNotIn("c-family-helpers", leaked) + + +class CompareTest(unittest.TestCase): + """#1478's residue: the bug in the derivation itself. + + Every static rule here compares a marker against the derivation. + When the two agree and both are wrong there is nothing left to + compare against except the previous revision, and membership per + build is computable from source — no cargo, no fifteen builds. + """ + + def test_a_qualified_name_carries_the_module_path(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + mod inner { + #[test] + fn a_case() {} + } +} +""", + path="src/metrics/nom.rs", + ) + self.assertEqual( + gate.qualified_name(items, _named(items, "a_case")), + "src/metrics/nom.rs::tests::inner::a_case", + ) + + def test_the_probe_set_comes_from_the_language_table(self) -> None: + # Not from the CI matrix: a new language then extends the probes + # for free and there is nothing to keep in step. + probes = gate.probe_builds(gate.language_table(LANGS_FIXTURE)) + self.assertEqual(probes["--no-default-features"], frozenset()) + self.assertEqual( + probes["--all-features"], + frozenset({"python", "typescript", "cpp", "c-family-helpers", "rust"}), + ) + # `cpp` enables the helper grammars, so its probe carries both. + self.assertEqual( + probes["--features cpp"], frozenset({"cpp", "c-family-helpers"}) + ) + self.assertEqual(probes["--features rust"], frozenset({"rust"})) + + def test_membership_follows_the_gate(self) -> None: + items = _scan( + """ +#[cfg(test)] +mod tests { + #[cfg(feature = "python")] + #[test] + fn python_case() { + check_metrics::("a = 1", "f.py", |m| {}); + } +} +""" + ) + probes = gate.probe_builds(gate.language_table(LANGS_FIXTURE)) + built = gate.membership({"src/fixture.rs": items}, probes) + self.assertEqual( + built["src/fixture.rs::tests::python_case"], + frozenset({"--all-features", "--features python"}), + ) + + def test_a_gate_that_narrowed_since_the_reference_is_reported(self) -> None: + wide = """#[cfg(test)] +mod tests { + #[cfg(any(feature = "python", feature = "rust"))] + #[test] + fn a_case() { + for lang in LANG::into_enum_iter().filter(LANG::is_enabled) { + let _ = (LANG::Python, LANG::Rust, lang); + } + } +} +""" + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + (root / "src").mkdir() + (root / "big-code-analysis-ast" / "src").mkdir(parents=True) + (root / "big-code-analysis-ast" / "src" / "langs.rs").write_text( + LANGS_FIXTURE + ) + target = root / "src" / "fixture.rs" + target.write_text(wide) + + def git(*args: str) -> None: + subprocess.run( + ["git", *args], + cwd=root, + check=True, + capture_output=True, + env={ + "GIT_AUTHOR_NAME": "t", + "GIT_AUTHOR_EMAIL": "t@t", + "GIT_COMMITTER_NAME": "t", + "GIT_COMMITTER_EMAIL": "t@t", + "PATH": os.environ.get("PATH", ""), + "HOME": str(root), + }, + ) + + git("init", "-q") + git("add", "-A") + git("commit", "-q", "-m", "base") + + table = gate.language_table(LANGS_FIXTURE) + # Unchanged: nothing lost. + self.assertEqual(gate.compare_revisions("HEAD", root, table), []) + + # Narrowed to Python alone. The test still exists, still + # passes under `--all-features`, and has silently stopped + # running on the Rust leg -- the one thing no marker check + # can see. + target.write_text( + wide.replace( + '#[cfg(any(feature = "python", feature = "rust"))]', + '#[cfg(feature = "python")]', + ) + ) + self.assertEqual( + [ + (name, sorted(lost)) + for name, lost in gate.compare_revisions("HEAD", root, table) + ], + [("src/fixture.rs::tests::a_case", ["--features rust"])], + ) + + # A deletion is a deliberate change, not a narrowed gate. + target.write_text("#[cfg(test)]\nmod tests {}\n") + self.assertEqual(gate.compare_revisions("HEAD", root, table), []) + + +class RepositoryTest(unittest.TestCase): + def setUp(self) -> None: + self.table = gate.language_table( + (REPO_ROOT / "big-code-analysis-ast" / "src" / "langs.rs").read_text() + ) + self.per_file = gate.scan_tree(REPO_ROOT, self.table) + + def test_the_real_language_table_has_every_row(self) -> None: + features = set(self.table.values()) + self.assertIn("c-family-helpers", features) + self.assertEqual(self.table["TsxParser"], "typescript") + # 25 variants + 25 parser aliases + 25 `*Code` tags. + self.assertEqual(len(self.table), 75) + + def test_the_scan_finds_the_metric_modules(self) -> None: + # A scanner that silently stops matching reports a clean tree. + self.assertIn("src/metrics/abc.rs", self.per_file) + self.assertGreater(len(self.per_file), 50) + + def test_the_repository_is_clean(self) -> None: + out, err = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): + code = gate.main([]) + self.assertEqual(code, 0, err.getvalue()) + + def test_the_derivation_reproduces_every_hand_written_gate(self) -> None: + """No item a human gated may be reported as under-gated. + + This is what licenses the mechanical pass: the derivation agrees + with all 167 markers written by hand over twelve months, so the + ~2,950 it generated are trustworthy for the same reason. A + change that makes the derivation stricter shows up here first. + """ + checked = 0 + for items in self.per_file.values(): + needs = gate.resolve_needs(items) + reported = {item.index for item, _ in gate.offenders(items, needs)} + for item in items: + if not item.in_test_scope: + continue + if not any("feature" in p for p in item.own_predicates): + continue + checked += 1 + self.assertNotIn(item.index, reported, item.describe()) + self.assertGreater(checked, 150, "the hand-gated population vanished") + + +if __name__ == "__main__": + unittest.main() diff --git a/utils/check-test-lang-gates.py b/utils/check-test-lang-gates.py new file mode 100755 index 000000000..38058f236 --- /dev/null +++ b/utils/check-test-lang-gates.py @@ -0,0 +1,2346 @@ +#!/usr/bin/env python3 +"""check-test-lang-gates + +Verify that every test item naming a concrete per-language parser carries +a ``cfg`` marker implying that language's Cargo feature. + +``mk_langs!`` generates each ``*Parser`` alias and each ``LANG`` variant +unconditionally -- only ``get_ts_language``'s arms are cfg'd. So +``check_metrics::(…)`` *compiles* with ``python`` disabled +and then panics inside ``Tree::new``:: + + invariant: the caller checked LANG::is_enabled, or reached this + through AnyParser + +``ParserTrait::new``'s own doc says as much and directs callers to check +``LANG::is_enabled`` first. Before #1472 almost none did, and +``--no-default-features --features rust`` failed ~2,600 tests, which +makes a partial-feature build useless for verifying anything -- the +misleading-red class #1171 fixed for the corpora. + +This gate is the companion to ``check-feature-gates.py``. That one +catches a union gate that is present but *defeated* (weakened by a +non-feature disjunct). It structurally cannot catch a *missing* one, +because deleting the marker deletes the subject it scans for. This one +derives what the marker must be from the item's own contents and +compares. + +The derivation, for every item inside a ``cfg(test)`` scope: + +1. **Needed set** -- the languages the item reaches unconditionally: + concrete ``*Parser`` aliases and ``LANG::`` literals in its + body, ignoring anything already behind a nested + ``#[cfg(feature = …)]``, plus transitively the needed set of + same-module helper ``fn``s it calls. +2. **Declared gate** -- the item's own ``cfg`` conjoined with every + enclosing ``mod``'s. +3. **Verdict** -- for each needed feature, evaluate the declared gate + with that feature (and everything implying it) off and every other + language feature on. If the gate is still satisfied, the item would + be compiled into a build that cannot run it: that is an offender. + +Evaluating the real predicate under a one-feature-off assignment is what +makes this sound without symbolic implication, and it handles ``any`` / +``all`` / ``not`` / nesting for free. + +A ``#[test] fn`` needs *every* language it names, so its marker is an +``all(…)``. A helper is live when *any* caller is, so its marker is an +``any(…)`` over its callers. An item that filters at runtime on +``LANG::is_enabled`` / ``into_enum_iter`` skips disabled languages by +itself; those are checked only for naming at least one enabled language, +and the ``checked > 0`` non-vacuity discipline (#1286) plus +``check-feature-gates.py`` own the rest. + +Run it over the whole tree:: + + ./utils/check-test-lang-gates.py + +``--show`` lists every item and its derived marker; ``--fix`` writes the +missing markers in place. It is wired into ``make pre-commit`` and +``make ci``: unlike ``check-feature-gates.py`` this is a pure source +scan with no cargo invocation, so it is cheap enough for the local gate. +""" + +from __future__ import annotations + +import argparse +import os +import shutil +import pathlib +import re +import subprocess +import sys +import tempfile +from dataclasses import dataclass, replace + +# `parents[1]`, not `parent`: these gates live in `utils/` but every path +# they read is anchored at the repository root, so the script works from +# any cwd. +REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] + +# The `mk_langs!` invocation is the single source of truth for the +# variant / feature / parser triples. Parsing it rather than copying the +# table is what keeps this gate from drifting the next time a language +# is added -- `Tsx` -> `typescript` and `Ccomment` / `Preproc` -> +# `c-family-helpers` are exactly the rows a hand-maintained copy gets +# wrong. +LANGS_RS = REPO_ROOT / "big-code-analysis-ast" / "src" / "langs.rs" + +# Roots holding this workspace's own tests. +# +# `tests/repositories/` is deliberately absent: it is the vendored corpus +# submodule, and serde's own several-hundred-test suite lives under it. +SCAN_ROOTS = ( + pathlib.Path("src"), + pathlib.Path("big-code-analysis-ast") / "src", + pathlib.Path("tests"), +) +EXCLUDED = (pathlib.Path("tests") / "repositories",) + +# rustfmt's default `max_width`, so a generated marker that fits on one +# line is already the line rustfmt would have produced. +MAX_WIDTH = 100 + +# How many times `--fix` re-scans before giving up. Three is ample: the +# observed need was two. +FIX_PASSES = 3 + +# Enabling any of these enables `c-family-helpers` +# (`big-code-analysis-ast/Cargo.toml`), so a build that disables the +# helper feature necessarily disables them too. Every other language +# feature stands alone. +# +# Hand-maintained, unlike the language table: a fourth enabler added to +# that manifest has to be added here too, and nothing in the gate would +# say so. `c_family_helper_enablers_match_the_manifest` in the +# self-tests is what notices. +C_FAMILY_HELPER_FEATURE = "c-family-helpers" +IMPLIES_C_FAMILY_HELPERS = frozenset({"c", "cpp", "mozcpp"}) + +# `#[test]`, plus the namespaced spellings (`#[tokio::test]`). +TEST_ATTR_RE = re.compile(r"#\[\s*(?:[A-Za-z_][A-Za-z0-9_]*::)*test\s*\]") +# A runtime filter over `LANG`, which makes the item skip disabled +# languages on its own rather than needing them all. +SWEEP_RE = re.compile(r"\b(?:is_enabled|into_enum_iter)\b") +# The three marks of a sweep that is live exactly when *some* language +# is: it walks the whole enum, skips the disabled rows, and then refuses +# to pass having done nothing. The last one is what makes the gate +# mandatory rather than merely tidy — without it the sweep would simply +# run zero iterations and report green. +ALL_LANGS_SWEEP_RE = re.compile(r"\binto_enum_iter\b") +ENABLED_FILTER_RE = re.compile(r"\bis_enabled\b") +NON_VACUITY_RE = re.compile(r"assert_fixtures_present|assert!\s*\(\s*\w+\s*>\s*0") +ITEM_RE = re.compile( + r"^(?P[ \t]*)" + r"(?Ppub\s*(?:\([^)]*\)\s*)?)?" + r"(?:default\s+)?(?:const\s+)?(?:async\s+)?(?:unsafe\s+)?" + r'(?:extern\s+"[^"]*"\s+)?' + r"(?Pmod|fn|const|static|type)\s+(?:mut\s+)?" + r"(?P[A-Za-z_][A-Za-z0-9_]*)" +) +# Every identifier in a body. Needed for a `const` or `type`, which are +# referenced bare. +REFERENCE_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\b") +# The call-shaped subset: `helper(`, `helper::(`. A `fn` referent is +# matched against this rather than the broad set, because the broad set +# links a local named `score` or `shape` to the helper of that name -- +# and a false link there marks the helper unconditional, leaving it +# ungated and unused on every single-language leg. +CALL_RE = re.compile(r"\b([a-z_][A-Za-z0-9_]*)\s*(?:::\s*<[^;{}()]*>\s*)?\(") +# A parameter of type `LANG`, which makes a helper language-generic. +# +# The lookbehind is load-bearing: without it the `::` of a path like +# `&[(crate::LANG, &[u8])]` reads as a parameter colon, so a `const` +# fixture table declaring its own element type was taken for a generic +# helper and its languages never reached the tests driving it (70 +# failures on the `--features go` leg). +LANG_PARAM_RE = re.compile(r"(?) — ` markers the analyser itself reads. +# Anything after the feature list is free text. +HAND_WRITTEN_RE = re.compile(r"//\s*test-lang-gates:\s*hand-written\s*\(([^)]*)\)") +# A macro invocation: `check_metrics_only_shim!(…)`. An imported macro +# is used from item position, where this scanner models nothing, so a +# name invoked this way anywhere in the file is treated as always used. +# Filled in from the language table once it is read. +KNOWN_FEATURES: frozenset[str] = frozenset() +# A `LANG` in match-arm *value* position (`"py" => LANG::Python,`), +# including the wrapped spellings a fallible lookup returns +# (`=> Some(LANG::Python)`, `=> Ok(LANG::Python)`). Without the wrappers +# a `fn pick(ext: &str) -> Option` hands every language it can +# return to each of its callers. +DISPATCH_ARM_RE = re.compile( + r"=>\s*(?:(?:Some|Ok|Err)\s*\(\s*)?(?:crate::)?(LANG::[A-Za-z][A-Za-z0-9]*)" +) +# A `LANG` being *compared*, not used: `lang == LANG::Go`, +# `matches!(lang, LANG::Cpp | LANG::Mozcpp)`. Asking which variant a value +# is needs no grammar — the enum is generated unconditionally — so +# counting one as a requirement gates the test out of builds it could run +# in. `container_scope_tests.rs` lost the positive half of the #1197 +# contract to exactly this (#1478). +COMPARISON_RES = ( + # `lang == LANG::Go`, `lang != crate::LANG::Go` + re.compile(r"[=!]=\s*(?:crate::)?(LANG::[A-Za-z][A-Za-z0-9]*)"), + # `LANG::Go == lang` + re.compile(r"(LANG::[A-Za-z][A-Za-z0-9]*)\s*[=!]="), + # `matches!(lang, LANG::Cpp …)` — the first alternative. The rest of + # the alternation is handled by `MATCHES_CALL_RE` below, which needs + # balanced parens rather than a regex. + re.compile(r"matches!\s*\([^()]*?(?:crate::)?(LANG::[A-Za-z][A-Za-z0-9]*)"), + # `assert_eq!` / `assert_ne!` are *not* here: `[^()]*?` cannot cross a + # parenthesis, so it stops at the first call in the argument list and + # never reaches the variant. `ASSERT_CALL_RE` below reads the whole + # argument list instead. +) +# Deliberately not the later arms of an or-pattern. `| LANG::X` is also +# how a per-language *dispatch table* groups its arms +# (`LANG::C | LANG::Cpp => ("…", "c")`), and excluding those strips the +# languages a sweep derives from the table it drives — eleven items lost +# their whole C-family union to that. +# +# A `matches!` is the exception, handled by `MATCHES_CALL_RE` above: it +# has no arms and drives no table, so every alternative in one is an +# identity test. Keeping them cost two `every_*_in_every_language` +# parity sweeps their whole gate — the single `c-family-helpers` that +# leaked out of a `Ccomment | Preproc` *exclusion* became the entire +# derivation, and they ran in four builds instead of twenty-three. +# +# Nothing here reports that on its own. `over_gated` compares a gate +# against the derivation, so a gate faithfully mirroring a wrong +# derivation is by construction never flagged, and an `any(...)` +# requires no single feature for it to object to. `--compare` found it, +# on its first run in CI, which is the argument for that check existing. + + +# A whole `matches!` call. *Every* alternative inside one is an identity +# test, not just the first: `matches!(lang, LANG::Ccomment | LANG::Preproc)` +# asks which variant a value is and parses nothing, so the later arms are +# no more a use than the leading one. +# +# This is not the or-pattern caveat below, which is about `match` *arms* +# — `LANG::C | LANG::Cpp => ("…", "c")` groups a dispatch table, and a +# sweep does derive its languages from the table it drives. A `matches!` +# has no arms and no table. Reading its later alternatives as uses is +# what gated two `every_*_in_every_language` parity sweeps down to the +# single `c-family-helpers` that leaked out of their `Ccomment | +# Preproc` exclusion, which `--compare` caught on its first CI run. +MATCHES_CALL_RE = re.compile(r"\bmatches!\s*\(") +# `assert_eq!(get_from_ext("rb"), Some(LANG::Ruby))`. The expected side of +# an equality assertion is a *value*, and a `LANG` variant exists without +# its grammar, so naming one there parses nothing. +# +# Reading it with a flat `[^()]*?` — which is how this shipped — stops at +# the first `(` in the argument list and never sees the variant, so 37 +# tests over `get_from_ext` / `get_from_emacs_mode` / `FromStr` / `name()` +# were gated on grammars they never touch, several down to one build in +# twenty-five. None of those functions is `cfg`-gated; only `is_enabled`, +# `get_ts_language` and the `AnyParser` entry points are. +ASSERT_CALL_RE = re.compile(r"\bassert_(?:ne|eq)!\s*\(") +# A variant in *receiver* position: `LANG::Mozcpp.extensions()`. Most +# methods on `LANG` read the table `mk_langs!` generates +# unconditionally — extensions, slug, display name — and `is_enabled` +# answers a question that is meaningful precisely when the grammar is +# absent. None of those needs a feature. +# +# The two that hand back the grammar itself do. `tree_sitter_language()` +# returns `None` with the feature off, and every caller here follows it +# with `.expect(…)`, so treating the receiver as metadata gates the test +# out of nothing and lets it panic under a build without that language — +# the same failure as `Tree::new`, one call earlier. +LANG_RECEIVER_RE = re.compile( + r"(?:crate::)?(LANG::[A-Za-z][A-Za-z0-9]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)" +) +GRAMMAR_BEARING_METHODS = frozenset({"tree_sitter_language", "get_ts_language"}) +LANG_PATH_RE = re.compile(r"(?:crate::)?(LANG::[A-Za-z][A-Za-z0-9]*)") + + +def _is_value_position(args: str, offset: int) -> bool: + """Whether the variant at ``offset`` is compared rather than used. + + ``args`` is an equality assertion's argument list. A variant wrapped + only in constructors — `Some(…)`, `Ok(…)`, a tuple, a `vec![…]` — is + the expected value and needs no grammar. One passed to a *function* + is an argument to the thing under test, which may well parse it: + `assert_eq!(conditions(LANG::Go, src), 3)` genuinely needs Go. + + The two are told apart by the identifier before each enclosing `(`, + on Rust's own naming convention: lower-case means a call, upper-case + or nothing means a constructor or a tuple. + """ + stack: list[int] = [] + for index, char in enumerate(args[:offset]): + if char == "(": + stack.append(index) + elif char == ")" and stack: + stack.pop() + for open_index in stack: + end = open_index + while end > 0 and args[end - 1].isspace(): + end -= 1 + start = end + while start > 0 and (args[start - 1].isalnum() or args[start - 1] == "_"): + start -= 1 + name = args[start:end] + if name and (name[0].islower() or name[0] == "_"): + return False + return True + + +class ScanError(Exception): + """A malformed input the scanner refuses to guess about.""" + + +# --------------------------------------------------------------------------- +# Rust literal / comment lexing +# +# Ported from `check-feature-gates.py`, itself ported from +# `check-snapshot-anchors.py`, and needed here for the same reason: a +# `PythonParser` spelled inside a fixture string or a `//` comment is not +# a live reference, and counting one derives a gate the item does not +# need. The gate scripts are hyphen-named and so not importable; every +# one of them carries its own copy. Fix a lexing bug here and check the +# siblings. +# --------------------------------------------------------------------------- + + +def char_literal_end(source: str, i: int) -> int | None: + """End index (exclusive) of the char literal at ``i``, else ``None``. + + Rust spells lifetimes (``'a``), anonymous lifetimes (``'_``) and loop + labels (``'outer:``) with the same leading quote and no terminator, + so returning ``None`` for those is what keeps a lifetime from opening + a span that swallows the rest of the file. + """ + n = len(source) + j = i + 1 + if j >= n: + return None + if source[j] == "\\": + j += 1 + if j >= n: + return None + if source[j] == "u": + close = source.find("}", j) + if close == -1: + return None + j = close + 1 + elif source[j] == "x": + j += 3 + else: + j += 1 + else: + j += 1 + return j + 1 if j < n and source[j] == "'" else None + + +def raw_string_end(source: str, i: int) -> int | None: + """End index (exclusive) of the raw string at ``i``, else ``None``. + + Covers ``r"…"``, ``r#"…"#`` and the byte-string spellings. A plain + ``b"…"`` needs no special case: the ``b`` is an ordinary character + and the ``"`` opens a regular literal. + """ + n = len(source) + j = i + if source[j] == "b" and j + 1 < n and source[j + 1] == "r": + j += 1 + if j >= n or source[j] != "r": + return None + j += 1 + hashes = 0 + while j < n and source[j] == "#": + hashes += 1 + j += 1 + if j >= n or source[j] != '"': + return None + close = '"' + ("#" * hashes) + end = source.find(close, j + 1) + return n if end == -1 else end + len(close) + + +def regular_string_end(source: str, i: int) -> int: + """End index (exclusive) of the ``"``-delimited literal at ``i``.""" + n = len(source) + j = i + 1 + while j < n: + if source[j] == "\\" and j + 1 < n: + j += 2 + continue + if source[j] == '"': + break + j += 1 + return j + 1 + + +def dead_spans(source: str) -> list[tuple[int, int]]: + """Index ranges holding comments and string / char literals. + + One walk, string literals consumed before comment openers are tested, + so a ``//`` inside a string and a ``"`` inside a comment are both + read correctly. + """ + spans: list[tuple[int, int]] = [] + i = 0 + n = len(source) + while i < n: + ch = source[i] + if ch == "/" and i + 1 < n and source[i + 1] == "/": + nl = source.find("\n", i) + end = n if nl == -1 else nl + spans.append((i, end)) + i = end + continue + if ch == "/" and i + 1 < n and source[i + 1] == "*": + start = i + depth = 1 + i += 2 + while i < n and depth > 0: + if source[i] == "/" and i + 1 < n and source[i + 1] == "*": + depth += 1 + i += 2 + continue + if source[i] == "*" and i + 1 < n and source[i + 1] == "/": + depth -= 1 + i += 2 + continue + i += 1 + spans.append((start, i)) + continue + if ch in "rb": + stop = raw_string_end(source, i) + if stop is not None: + spans.append((i, stop)) + i = stop + continue + if ch == "'": + stop = char_literal_end(source, i) + if stop is not None: + spans.append((i, stop)) + i = stop + continue + i += 1 + continue + if ch == '"': + stop = regular_string_end(source, i) + spans.append((i, stop)) + i = stop + continue + i += 1 + return spans + + +def mask_dead(source: str) -> str: + """``source`` with every comment and literal blanked to spaces. + + Blanked rather than deleted so every index and line number in the + masked text still addresses the original. + """ + chars = list(source) + for start, end in dead_spans(source): + for i in range(start, min(end, len(chars))): + if chars[i] != "\n": + chars[i] = " " + return "".join(chars) + + +# --------------------------------------------------------------------------- +# The language table +# --------------------------------------------------------------------------- + +# One `mk_langs!` row: feature literal, CamelCase variant, then (six +# fields along) the `*Parser` alias. Matching the alias by name rather +# than by counting commas keeps this robust against the description +# strings, which contain both commas and escaped newlines. +LANG_ROW_RE = re.compile( + r'\(\s*"(?P[a-z0-9-]+)"\s*,\s*' + r"(?P[A-Za-z][A-Za-z0-9]*)\s*,.*?" + r"(?P[A-Za-z][A-Za-z0-9]*Code)\s*,\s*" + r"(?P[A-Za-z][A-Za-z0-9]*Parser)\s*,", + re.DOTALL, +) + + +def enabled_closure(feature: str) -> frozenset[str]: + """``feature`` plus the language features enabling it turns on. + + The forward direction of `disabled_closure`: `cpp` lists + `c-family-helpers`, so a build asking for `cpp` gets the helper + grammars too. + """ + if feature in IMPLIES_C_FAMILY_HELPERS: + return frozenset({feature, C_FAMILY_HELPER_FEATURE}) + return frozenset({feature}) + + +def probe_builds(table: dict[str, str]) -> dict[str, frozenset[str]]: + """The feature sets membership is compared under. + + Derived from the language table rather than from the CI matrix, so + adding a language extends the probe set for free and nothing has to + be kept in step. Each single-language build is the configuration a + per-language gate is most likely to get wrong, and the two ends + catch a gate that moved without naming a language at all. + """ + languages = frozenset(table.values()) + probes = { + "--no-default-features": frozenset(), + "--all-features": languages, + } + for language in sorted(languages): + probes[f"--features {language}"] = enabled_closure(language) + return probes + + +def language_table(langs_rs: str) -> dict[str, str]: + """Map every language spelling a test body can use to its feature. + + Keys are the *qualified* spellings -- ``LANG::Python`` and + ``PythonParser`` -- never the bare variant. Half the variant names + (``C``, ``Go``, ``Java``, ``Rust``) are ordinary identifiers that + occur as generic parameters and type names all over these files, so + a table keyed on the bare form derives gates for languages the item + never touches. + """ + table: dict[str, str] = {} + for row in LANG_ROW_RE.finditer(mask_comments_only(langs_rs)): + feature = row.group("feature") + # `.*?` spans the description fields, so a row missing one would + # silently match the *next* row's `*Code` / `*Parser` and file + # them under the wrong feature. Every real row names them after + # its variant, so requiring that turns the bleed into an error. + variant = row.group("variant") + for suffix, found in ( + ("Code", row.group("code")), + ("Parser", row.group("parser")), + ): + if found != variant + suffix: + raise ScanError( + f"`mk_langs!` row for `{variant}` names `{found}` where " + f"`{variant}{suffix}` was expected — a field is missing " + "and the row has run into the next one" + ) + table[f"LANG::{row.group('variant')}"] = feature + table[row.group("parser")] = feature + # The `*Code` tag is the third spelling, and the one a generic + # test reaches the grammar through: `check::(…)` inside + # `for_each_node_with_chain::` parses exactly as + # `CppParser::new` does. Omitting it left 79 tests ungated and + # panicking on the `--features go` leg. + table[row.group("code")] = feature + global KNOWN_FEATURES + KNOWN_FEATURES = frozenset(table.values()) + if not table: + raise ScanError( + f"no `mk_langs!` rows found in {LANGS_RS}; the language table " + "cannot be derived and this gate has nothing to check against" + ) + return table + + +def symbol_pattern(table: dict[str, str]) -> re.Pattern[str]: + """Match any language spelling in ``table``, longest alternative first. + + ``LANG::Typescript`` must be tried before ``LANG::Tsx`` only in the + sense that Python's alternation is first-match: sorting by length + descending keeps a shorter key from claiming a prefix of a longer + one. + """ + alternatives = "|".join( + re.escape(key) for key in sorted(table, key=len, reverse=True) + ) + return re.compile(rf"\b(?:{alternatives})\b") + + +def mask_comments_only(source: str) -> str: + """``source`` with comments blanked but string literals kept. + + The `mk_langs!` rows are *made of* string literals, so the full mask + would erase the feature names this table is read from. Comments still + have to go: `langs.rs` documents the tuple layout in a comment that + contains a parenthesised example. + """ + chars = list(source) + for start, end in dead_spans(source): + if source[start] not in "/": + continue + for i in range(start, min(end, len(chars))): + if chars[i] != "\n": + chars[i] = " " + return "".join(chars) + + +# --------------------------------------------------------------------------- +# cfg predicate parsing and evaluation +# --------------------------------------------------------------------------- + + +def balanced_group(text: str, open_idx: int) -> tuple[str, int] | None: + """Contents of the ``(``-delimited group at ``open_idx``, and its end. + + ``None`` when the parentheses do not balance before the text runs + out, which the caller treats as a malformed attribute rather than + guessing at its shape. + """ + if open_idx >= len(text) or text[open_idx] != "(": + return None + depth = 0 + for i in range(open_idx, len(text)): + if text[i] == "(": + depth += 1 + elif text[i] == ")": + depth -= 1 + if depth == 0: + return text[open_idx + 1 : i], i + return None + + +def split_top_level(text: str) -> list[str]: + """Split on commas that are not nested inside parentheses.""" + parts: list[str] = [] + depth = 0 + current: list[str] = [] + for ch in text: + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if ch == "," and depth == 0: + parts.append("".join(current)) + current = [] + continue + current.append(ch) + tail = "".join(current) + if tail.strip(): + parts.append(tail) + return [p.strip() for p in parts if p.strip()] + + +FEATURE_ATOM_RE = re.compile(r'^feature\s*=\s*"([^"]*)"$') + + +def evaluate_predicate(predicate: str, disabled: frozenset[str]) -> bool: + """Whether ``predicate`` holds with ``disabled`` features turned off. + + Every feature not named in ``disabled`` is treated as enabled, and + every non-``feature`` atom (``test``, ``unix``, ``debug_assertions``) + as true. Both defaults point the same way: assume the item *is* + compiled unless a feature predicate says otherwise, so a gate only + clears an item when it genuinely excludes it. + """ + predicate = predicate.strip() + # `startswith` alone treats `cfg(anyhow)` and `cfg(nothing)` as + # malformed combinators and raises; the `(` is what distinguishes a + # combinator from an ordinary atom that happens to share a prefix. + for combinator, reduce_fn in (("all", all), ("any", any)): + if predicate.startswith(combinator) and predicate[ + len(combinator) : + ].lstrip().startswith("("): + rest = predicate[len(combinator) :].lstrip() + group = balanced_group(rest, 0) + if group is None: + raise ScanError(f"unbalanced `{combinator}(` in cfg: {predicate!r}") + return reduce_fn( + evaluate_predicate(part, disabled) for part in split_top_level(group[0]) + ) + if predicate.startswith("not") and predicate[len("not") :].lstrip().startswith("("): + rest = predicate[len("not") :].lstrip() + group = balanced_group(rest, 0) + if group is None: + raise ScanError(f"unbalanced `not(` in cfg: {predicate!r}") + return not evaluate_predicate(group[0], disabled) + match = FEATURE_ATOM_RE.match(predicate) + if match: + return match.group(1) not in disabled + return True + + +def disabled_closure(feature: str) -> frozenset[str]: + """``feature`` plus every feature that would re-enable it. + + Only `c-family-helpers` has any: `c`, `cpp` and `mozcpp` each list it + (`big-code-analysis-ast/Cargo.toml`), so a build without the helper + grammars is necessarily a build without those three. + """ + if feature == C_FAMILY_HELPER_FEATURE: + return frozenset({feature}) | IMPLIES_C_FAMILY_HELPERS + return frozenset({feature}) + + +def gate_excludes(predicate: str | None, feature: str) -> bool: + """Whether ``predicate`` compiles the item *only* when ``feature`` is off. + + ``#[cfg(not(feature = "javascript"))]`` marks a test of the + disabled-language path: it names `JavascriptParser` precisely + because the grammar is absent, and demanding a `javascript` gate on + it would invert the test. Three such tests exist + (`langs.rs::disabled_language_dispatch_returns_language_disabled` + and two in `tests/api/ast_seam_test.rs`). + """ + if predicate is None: + return False + return not evaluate_predicate(predicate, frozenset()) and evaluate_predicate( + predicate, disabled_closure(feature) + ) + + +def negated_features(predicate: str) -> frozenset[str]: + """Features ``predicate`` requires to be *off*, read syntactically. + + The `not(feature = …)` case. Such a gate is false with everything + enabled, so measuring over-declaration from an all-on baseline reads + every other feature as required; this names the features to turn off + first, so the measurement starts from a build the item compiles in. + + Read out of the text rather than probed one feature at a time. + Probing cannot see a conjunction of negations — `all(not(a), + not(b))` is false with either alone disabled, so no single probe + ever finds the build that satisfies it, and every feature in the + tree then reads as over-declared. No such gate exists here yet; the + first one would have produced a wall of false positives. + """ + found: set[str] = set() + for match in re.finditer(r"\bnot\s*\(", predicate): + group = balanced_group(predicate, match.end() - 1) + if group is None: + continue + found.update(hit.group(1) for hit in FEATURE_NAME_RE.finditer(group[0])) + return frozenset(found & KNOWN_FEATURES) + + +def gate_admits(predicate: str | None, feature: str) -> bool: + """Whether an item under ``predicate`` compiles with ``feature`` off. + + ``True`` is the offending answer: the item would be built into a + configuration whose grammar it needs but does not have. + """ + if predicate is None: + return True + return evaluate_predicate(predicate, disabled_closure(feature)) + + +# --------------------------------------------------------------------------- +# Item discovery +# --------------------------------------------------------------------------- + +CFG_ATTR_RE = re.compile(r"#!?\[\s*cfg\s*\(") +BARE_TEST_RE = re.compile(r"\btest\b") +FEATURE_PREDICATE_RE = re.compile(r'feature\s*=\s*"[^"]*"') +# The same atom, unanchored and capturing, for reading names out of a +# sub-expression. `FEATURE_ATOM_RE` is anchored and matches one whole +# predicate, so `finditer` over a group's contents finds nothing at all. +FEATURE_NAME_RE = re.compile(r'feature\s*=\s*"([^"]*)"') +MACRO_RULES_RE = re.compile(r"\bmacro_rules!\s*([A-Za-z_][A-Za-z0-9_]*)") + + +def cfg_predicate(attr: str) -> str | None: + """The predicate inside ``#[cfg(...)]``, or ``None`` if not a cfg.""" + match = CFG_ATTR_RE.search(attr) + if match is None: + return None + group = balanced_group(attr, match.end() - 1) + if group is None: + raise ScanError(f"unbalanced `cfg(` in attribute: {attr!r}") + return group[0].strip() + + +def predicate_requires_test(predicate: str) -> bool: + """Whether ``predicate`` carries a bare ``test``. + + Feature names are blanked first, so a language literally called + ``test`` could not be mistaken for ``cfg(test)``. Negated groups are + blanked too: `cfg(not(test))` marks code that exists *outside* the + test build, and reading it as a test scope pulls production items + into `--show` and into the helper-inheritance walk. + """ + return bool( + BARE_TEST_RE.search(_without_negations(FEATURE_PREDICATE_RE.sub("", predicate))) + ) + + +def _without_negations(predicate: str) -> str: + """``predicate`` with every ``not(...)`` group blanked out.""" + out = predicate + while True: + match = re.search(r"\bnot\s*\(", out) + if match is None: + return out + group = balanced_group(out, match.end() - 1) + if group is None: + return out + out = ( + out[: match.start()] + + " " * (group[1] - match.start() + 1) + + out[group[1] + 1 :] + ) + + +@dataclass +class Item: + """A ``mod`` or ``fn`` in a test scope, with its derived needs.""" + + path: str + #: 1-based line of the item header. + line: int + #: 1-based first line of the item's attribute run. `--fix` inserts + #: here, so a generated marker lands above `#[test]` rather than + #: between it and the signature. + attr_line: int + indent: str + kind: str + name: str + #: `cfg(...)` predicate texts written on this item. + own_predicates: tuple[str, ...] + is_test_fn: bool + is_sweep: bool + #: Iterates `LANG::into_enum_iter()` behind an `is_enabled()` filter + #: *and* asserts non-vacuity, so it fails rather than skips when no + #: language is enabled. + needs_a_language_enabled: bool + #: Visible outside this file, so its users are not all in view. + is_public: bool + #: The signature takes a runtime `LANG`, so the languages its body + #: names are dispatch arms rather than the caller's requirements. + takes_lang_param: bool + #: Language features named directly in the body, outside any inner + #: `cfg` that already excludes them. + direct: frozenset[str] + #: The subset of `direct` reached through a concrete `*Parser` / + #: `*Code` type rather than a `LANG::` value. A type parameter cannot + #: be chosen at run time, so these are requirements even for a sweep. + hardcoded: frozenset[str] + #: Features a `// test-lang-gates: hand-written(…)` comment accepts as + #: gated for a reason the derivation cannot see. + hand_written: frozenset[str] + #: Call-shaped references in the body, for resolving `fn`s. + calls: frozenset[str] + #: Every identifier in the body, for resolving `const` / `type`. + references: frozenset[str] + parent_index: int | None + #: Index into the file's item list; the parent link is by index so + #: the dataclass stays comparable and printable. + index: int + in_test_scope: bool + + def describe(self) -> str: + return f"{self.path}:{self.line} {self.kind} {self.name}" + + +def qualified_name(items: list[Item], item: Item) -> str: + """``path::mod::mod::name`` — stable across revisions. + + What makes a test the *same* test in two trees. A rename or a move + reads as one name gone and another arrived, which is what keeps a + deliberate deletion from reporting as a regression. + """ + parts = [item.name] + node = None if item.parent_index is None else items[item.parent_index] + while node is not None: + if node.kind == "mod": + parts.append(node.name) + node = None if node.parent_index is None else items[node.parent_index] + return item.path + "::" + "::".join(reversed(parts)) + + +def membership( + per_file: dict[str, list[Item]], probes: dict[str, frozenset[str]] +) -> dict[str, frozenset[str]]: + """Which probe builds compile each test, keyed by qualified name.""" + languages = frozenset().union(*probes.values()) if probes else frozenset() + result: dict[str, frozenset[str]] = {} + for items in per_file.values(): + for item in items: + if not (item.is_test_fn and item.in_test_scope): + continue + predicate = effective_predicate(items, item) + if predicate is None: + result[qualified_name(items, item)] = frozenset(probes) + continue + result[qualified_name(items, item)] = frozenset( + name + for name, enabled in probes.items() + if evaluate_predicate(predicate, languages - enabled) + ) + return result + + +def effective_predicate(items: list[Item], item: Item) -> str | None: + """``item``'s ``cfg`` conjoined with every enclosing item's.""" + parts: list[str] = [] + node: Item | None = item + while node is not None: + parts.extend(node.own_predicates) + node = None if node.parent_index is None else items[node.parent_index] + if not parts: + return None + if len(parts) == 1: + return parts[0] + return "all(" + ", ".join(parts) + ")" + + +def _join_attributes( + lines: list[str], raw: list[str], start: int +) -> tuple[list[str], int]: + """Attributes starting at ``start``; returns them and the next index. + + ``lines`` is the masked text and decides *where* the attributes are + -- a ``#[cfg(…)]`` quoted inside a fixture string is blanked there + and correctly reads as ordinary text. The attribute text itself + comes from ``raw``, because masking blanks string literals and the + feature names this gate exists to read are string literals. Reading + the predicate off the masked line yields ``feature = ""`` for every + gate in the tree, which no feature set can ever disable, so every + correctly gated item reports as an offender. + + Multi-line attributes are joined onto one line. Blank lines (which + is also what a masked comment looks like) do not end a run, because + these modules routinely put a rationale comment between two + attributes. + """ + attrs: list[str] = [] + index = start + while index < len(lines): + stripped = lines[index].strip() + if not stripped: + index += 1 + continue + if not stripped.startswith("#"): + break + buffer = [raw[index].strip()] + opened = stripped.count("[") - stripped.count("]") + while opened > 0: + index += 1 + if index >= len(lines): + raise ScanError(f"unterminated attribute at line {start + 1}") + opened += lines[index].count("[") - lines[index].count("]") + buffer.append(raw[index].strip()) + attrs.append(" ".join(buffer)) + index += 1 + return attrs, index + + +def _hand_written_features(raw: list[str], attr_line: int) -> frozenset[str]: + """Features accepted by the marker in the comment run above an item. + + Read from the unmasked text, and from *above* the attributes rather + than among them, because that is where every such rationale already + sits in this tree. + """ + named: set[str] = set() + index = attr_line - 1 + while index >= 0: + stripped = raw[index].strip() + # `//`, but not `///` or `//!`. A machine-read directive has no + # business hiding in rendered documentation, and a `//!` at the + # top of a file would attach to that file's first item. + if not stripped.startswith("//") or stripped.startswith(("///", "//!")): + break + match = HAND_WRITTEN_RE.search(stripped) + if match: + named.update( + part.strip() for part in match.group(1).split(",") if part.strip() + ) + index -= 1 + return frozenset(named) + + +def _bracket_delta(line: str) -> int: + """Net nesting change across ``line`` over ``{}``, ``()`` and ``[]``.""" + return sum(line.count(c) for c in "{([") - sum(line.count(c) for c in "})]") + + +def _inner_cfg_extents( + lines: list[str], raw: list[str], body_start: int, body_end: int +) -> list[tuple[int, int, str]]: + """``(first_line, last_line, predicate)`` for cfgs inside a body. + + A symbol sitting in one of these is already conditional, so it must + not be counted towards the enclosing item's unconditional needs. The + extent runs to the close of whatever bracket the following line + opens, and is that one line when it opens none. + + All three bracket kinds count, not just braces: the commonest cfg'd + element in this tree is a *tuple* in a fixture array, whose rows are + parenthesised rather than braced (`ops.rs`'s + `ops_classifies_space_kind_once_per_space_1110`). Counting only + `{}` ends the extent on the opening `(` and leaves every `LANG::` + inside the row looking unconditional. + """ + extents: list[tuple[int, int, str]] = [] + index = body_start + while index <= body_end and index < len(lines): + stripped = lines[index].strip() + if not stripped.startswith(("#[", "#![")): + index += 1 + continue + attrs, after = _join_attributes(lines, raw, index) + predicates = [p for p in (cfg_predicate(a) for a in attrs) if p is not None] + feature_predicates = [p for p in predicates if "feature" in p] + if not feature_predicates or after > body_end: + index = max(after, index + 1) + continue + depth = _bracket_delta(lines[after]) + end = after + while depth > 0 and end < body_end: + end += 1 + depth += _bracket_delta(lines[end]) + for predicate in feature_predicates: + extents.append((after, end, predicate)) + index = max(after, index + 1) + return extents + + +def scan_source( + source: str, + path: str, + table: dict[str, str], + *, + whole_file_is_test: bool = False, +) -> list[Item]: + """Every ``mod`` / ``fn`` in ``source`` that sits in a test scope.""" + masked = mask_dead(source) + lines = masked.split("\n") + raw = source.split("\n") + symbol_re = symbol_pattern(table) + + items: list[Item] = [] + stack: list[tuple[int, int]] = [] # (item index, brace depth on entry) + depth = 0 + index = 0 + # A `macro_rules!` body is a token tree, not items: the `#[test] fn` + # inside `roundtrip_tests!` is a template, and its `$lang` + # metavariable names no language at all. + macro_depth: int | None = None + is_integration_test = path.startswith("tests/") or whole_file_is_test + + while index < len(lines): + line = lines[index] + stripped = line.strip() + entry_depth = depth + + if macro_depth is not None: + depth += line.count("{") - line.count("}") + if depth <= macro_depth: + macro_depth = None + index += 1 + continue + attrs: list[str] = [] + attr_line = index + if stripped.startswith(("#[", "#![")): + attrs, next_index = _join_attributes(lines, raw, index) + if next_index >= len(lines): + break + index = next_index + line = lines[index] + stripped = line.strip() + entry_depth = depth + + # After the attribute run, not before it. Reading `attrs` and + # `attr_line` above their assignment raised `UnboundLocalError` + # on any file whose first scanned line is a `macro_rules!`, left + # an *attributed* macro unrecognised — so `--fix` wrote markers + # into its transcriber body — and gave every recorded macro the + # previous line's `attr_line`, putting its marker on the item + # above it. + declared_macro = MACRO_RULES_RE.search(line) + if declared_macro: + # The body is a token tree, not items -- the `#[test] fn` + # inside `roundtrip_tests!` is a template whose `$lang` + # metavariable names no language. The *definition* is still + # an item that goes dead when every caller is gated away + # (`unused macro definition: assert_no_string_matches`), so + # it is recorded with an empty body and takes `any(users)` + # from the reverse propagation like any other helper. + if stack or is_integration_test: + macro_end = index + macro_running = line.count("{") - line.count("}") + while macro_running > 0 and macro_end + 1 < len(lines): + macro_end += 1 + macro_running += lines[macro_end].count("{") - lines[ + macro_end + ].count("}") + macro_body = "\n".join(lines[index : macro_end + 1]) + items.append( + Item( + path=path, + line=index + 1, + attr_line=attr_line + 1, + indent=line[: len(line) - len(line.lstrip())], + kind="macro", + name=declared_macro.group(1), + own_predicates=tuple( + p + for p in (cfg_predicate(a) for a in attrs) + if p is not None + ), + is_test_fn=False, + is_sweep=False, + needs_a_language_enabled=False, + is_public=False, + takes_lang_param=False, + # No `direct`: the languages a macro body names + # are `$lang` metavariables, not requirements. + # Its *references* are real, though — they are + # the only record that `assert_variants_is_string!` + # expands to `assert_variant_is_string`, without + # which that helper looks to have no callers at + # all and stays ungated while the helper *it* + # calls does not. + direct=frozenset(), + hardcoded=frozenset(), + hand_written=_hand_written_features(raw, attr_line), + calls=frozenset( + m.group(1) for m in CALL_RE.finditer(macro_body) + ), + references=frozenset( + m.group(1) for m in REFERENCE_RE.finditer(macro_body) + ), + parent_index=stack[-1][0] if stack else None, + index=len(items), + in_test_scope=( + items[stack[-1][0]].in_test_scope + if stack + else is_integration_test + ), + ) + ) + macro_depth = depth + depth += line.count("{") - line.count("}") + index += 1 + continue + + match = ITEM_RE.match(line) + if match is None: + # An inner `#![cfg(...)]` applies to the enclosing item, which + # is already on the stack. + if attrs and stack and any(a.startswith("#![") for a in attrs): + owner = items[stack[-1][0]] + extra = tuple( + p + for p in (cfg_predicate(a) for a in attrs if a.startswith("#![")) + if p is not None + ) + if extra: + items[owner.index] = replace( + owner, own_predicates=owner.own_predicates + extra + ) + depth += line.count("{") - line.count("}") + while stack and depth <= stack[-1][1]: + stack.pop() + index += 1 + continue + + predicates = tuple( + p for p in (cfg_predicate(a) for a in attrs) if p is not None + ) + parent_index = stack[-1][0] if stack else None + in_test_scope = ( + is_integration_test + or any(predicate_requires_test(p) for p in predicates) + or (parent_index is not None and items[parent_index].in_test_scope) + ) + + # Body extent: find the opening brace, then brace-match to its + # partner. The brace is not always on the header line -- a + # wrapped signature puts it three or four lines down -- and + # treating the header as the whole body makes the item look as + # though it names no language and calls nothing. That silently + # under-gates every such helper and, worse, gates the helpers + # *it* calls more narrowly than itself (`E0425 cannot find + # function assert_members_score`). A declaration (`mod bash;`, + # a trait signature) reaches its `;` first and has no body. + body_end = index + opened = False + parens = 0 + for probe in range(index, len(lines)): + for char in lines[probe]: + if char in "([": + parens += 1 + elif char in ")]": + parens -= 1 + elif char == "{" and parens <= 0: + opened = True + break + elif char == ";" and parens <= 0: + break + else: + continue + body_end = probe + break + if opened: + running = 0 + for probe in range(body_end, len(lines)): + running += lines[probe].count("{") - lines[probe].count("}") + if running <= 0: + body_end = probe + break + body_end = probe + + # The header through its closing paren: enough to see whether a + # helper is parameterised on the language. + signature = line + paren = line.count("(") - line.count(")") + probe = index + while paren > 0 and probe + 1 <= body_end: + probe += 1 + signature += lines[probe] + paren += lines[probe].count("(") - lines[probe].count(")") + + body = "\n".join(lines[index : body_end + 1]) + is_test_fn = any(TEST_ATTR_RE.search(a) for a in attrs) + # Guarded *positions*, not guarded features. Subtracting the + # whole feature made a language named both unconditionally and + # inside its own `#[cfg]` disappear from the needs entirely, so + # the test got no gate and panicked without the grammar — the + # unsafe direction. + guarded: dict[int, list[str]] = {} + for first, last, predicate in _inner_cfg_extents(lines, raw, index, body_end): + for guarded_line in range(first, last + 1): + guarded.setdefault(guarded_line, []).append(predicate) + # A helper that maps one of its arguments onto a `LANG` is a + # dispatcher, and its arms are options rather than requirements. + # `tests/api/suppression_test.rs`'s `analyze_lang` picks the + # language from a file extension, so a body-literal reading makes + # all twenty of its tests look as though each needs C++, + # JavaScript, Python and Rust at once. `takes_lang_param` cannot + # see this one: the parameter is a `&str` path. + dispatched = set() + if not is_test_fn and "(" in signature: + dispatched = {hit.start(1) for hit in DISPATCH_ARM_RE.finditer(body)} + # Comparisons are skipped for every item, test or helper: the + # enum variant exists without its grammar, so asking whether a + # value *is* it never parses anything. + for pattern in COMPARISON_RES: + for hit in pattern.finditer(body): + dispatched.add(hit.start(1)) + for hit in LANG_RECEIVER_RE.finditer(body): + if hit.group(2) not in GRAMMAR_BEARING_METHODS: + dispatched.add(hit.start(1)) + # And every remaining alternative of each `matches!`. + for call in MATCHES_CALL_RE.finditer(body): + group = balanced_group(body, call.end() - 1) + if group is None: + continue + for variant in LANG_PATH_RE.finditer(group[0]): + dispatched.add(call.end() + variant.start(1)) + # And each variant an equality assertion *compares* rather than + # hands to the call under test. + for call in ASSERT_CALL_RE.finditer(body): + group = balanced_group(body, call.end() - 1) + if group is None: + continue + args = group[0] + for variant in LANG_PATH_RE.finditer(args): + if _is_value_position(args, variant.start(1)): + dispatched.add(call.end() + variant.start(1)) + direct: set[str] = set() + hardcoded: set[str] = set() + body_offset = 0 + for line_number in range(index, body_end + 1): + # Not `predicates`: that name already holds the item's own + # `cfg`, and shadowing it here silently emptied + # `own_predicates` for every item in the tree. + inner_gates = guarded.get(line_number, ()) + for hit in symbol_re.finditer(lines[line_number]): + if body_offset + hit.start() in dispatched: + continue + feature = table[hit.group(0)] + if any(not gate_admits(g, feature) for g in inner_gates): + continue + direct.add(feature) + if not hit.group(0).startswith("LANG::"): + hardcoded.add(feature) + body_offset += len(lines[line_number]) + 1 + + item = Item( + path=path, + line=index + 1, + attr_line=attr_line + 1, + indent=match.group("indent"), + kind=match.group("kind"), + name=match.group("name"), + own_predicates=predicates, + is_test_fn=is_test_fn, + is_public=bool(match.group("visibility")), + is_sweep=bool(SWEEP_RE.search(body)), + needs_a_language_enabled=bool( + ALL_LANGS_SWEEP_RE.search(body) + and ENABLED_FILTER_RE.search(body) + and NON_VACUITY_RE.search(body) + ), + takes_lang_param=( + match.group("kind") == "fn" and bool(LANG_PARAM_RE.search(signature)) + ), + direct=frozenset(direct), + hardcoded=frozenset(hardcoded), + hand_written=_hand_written_features(raw, attr_line), + calls=frozenset(m.group(1) for m in CALL_RE.finditer(body)), + references=frozenset(m.group(1) for m in REFERENCE_RE.finditer(body)), + parent_index=parent_index, + index=len(items), + in_test_scope=in_test_scope, + ) + items.append(item) + depth += line.count("{") - line.count("}") + if depth > entry_depth: + stack.append((item.index, entry_depth)) + while stack and depth <= stack[-1][1]: + stack.pop() + index += 1 + + # Every item is returned, not just the in-scope ones: `parent_index` + # and `index` address this list, so filtering here would make a + # parent link point at the wrong item -- or at the item itself, which + # spins `effective_predicate` forever. Callers filter on + # `in_test_scope` when reporting. + return items + + +def rust_sources(root: pathlib.Path) -> list[pathlib.Path]: + """Repo-relative ``.rs`` paths under the scan roots. + + The excluded directories are pruned during the walk rather than + filtered afterwards: `tests/repositories/` holds whole checked-out + repositories (DeepSpeech, pdf.js, serde), so a `rglob` that descends + into them before discarding them takes minutes. + """ + found: list[pathlib.Path] = [] + for scan_root in SCAN_ROOTS: + base = root / scan_root + if not base.is_dir(): + continue + for directory, subdirectories, filenames in os.walk(base): + here = pathlib.Path(directory).relative_to(root) + subdirectories[:] = sorted( + name + for name in subdirectories + if not any((here / name).is_relative_to(e) for e in EXCLUDED) + ) + found.extend( + here / name for name in sorted(filenames) if name.endswith(".rs") + ) + return found + + +MOD_DECL_RE = re.compile(r"^[ \t]*(?:pub\s*(?:\([^)]*\)\s*)?)?mod\s+(\w+)\s*;") +PATH_ATTR_RE = re.compile(r'#\[\s*path\s*=\s*"([^"]+)"\s*\]') + + +def test_scope_files(root: pathlib.Path, sources: list[pathlib.Path]) -> set[str]: + """Files that are wholly test modules by their declaration. + + `src/spaces_tests.rs` and its siblings hold nothing but tests, yet + carry no `#[cfg(test)]` of their own: the marker is on the + `#[cfg(test)] mod spaces_tests;` line in `lib.rs`. Reading that + declaration is what puts their 94 top-level tests in scope -- + without it they are taken for production code and left ungated, + which is 79 panics on a single-language leg. + """ + scoped: set[str] = set() + for relative in sources: + text = (root / relative).read_text(encoding="utf-8") + lines = mask_dead(text).split("\n") + raw = text.split("\n") + index = 0 + while index < len(lines): + if not lines[index].strip().startswith(("#[", "#![")): + index += 1 + continue + attrs, after = _join_attributes(lines, raw, index) + index = max(after, index + 1) + if after >= len(lines): + break + declared = MOD_DECL_RE.match(lines[after]) + if declared is None: + continue + predicates = [cfg_predicate(a) for a in attrs] + if not any(p and predicate_requires_test(p) for p in predicates): + continue + # `#[path = "…"]` renames the file the module comes from: + # `src/spaces.rs` declares `#[path = "spaces_tests.rs"] mod + # tests;`, so the name says `tests` and the file does not. + override = PATH_ATTR_RE.search(" ".join(attrs)) + name = declared.group(1) + candidates = ( + [relative.parent / override.group(1)] + if override + else [ + relative.parent / f"{name}.rs", + relative.parent / name / "mod.rs", + ] + ) + for candidate in candidates: + if (root / candidate).is_file(): + scoped.add(candidate.as_posix()) + return scoped + + +def scan_tree(root: pathlib.Path, table: dict[str, str]) -> dict[str, list[Item]]: + """Every in-scope item under the scan roots, keyed by file.""" + sources = rust_sources(root) + scoped = test_scope_files(root, sources) + found: dict[str, list[Item]] = {} + for relative in sources: + items = scan_source( + (root / relative).read_text(encoding="utf-8"), + relative.as_posix(), + table, + whole_file_is_test=relative.as_posix() in scoped, + ) + if items: + found[relative.as_posix()] = items + return found + + +# --------------------------------------------------------------------------- +# Needed-set resolution +# --------------------------------------------------------------------------- + + +def hardcoded_closure(items: list[Item], item: Item) -> frozenset[str]: + """Parsers ``item`` pins by type, its own and its helpers'. + + `needs` propagates through helpers and this has to as well, or the + shape #1478 describes walks straight through: a sweep whose only + hardcoded parser sits one call away reads as pinning nothing. + """ + scopes = _helper_scopes(items) + collected = set(item.hardcoded) + seen = {item.index} + queue = [item] + while queue: + current = queue.pop() + for name in current.references: + index = _resolve_call(items, scopes, current, name) + if index is None or index in seen: + continue + callee = items[index] + if not _uses(current, callee, strict=False): + continue + seen.add(index) + collected |= callee.hardcoded + queue.append(callee) + return frozenset(collected) + + +def _helper_scopes(items: list[Item]) -> dict[int | None, dict[str, int]]: + """Referenceable non-test items, by enclosing scope then by name. + + ``const`` and ``static`` are in here beside ``fn``: a fixture + ``const SRC: &str`` used only by gated tests goes dead in a narrow + build exactly like a helper does. + """ + scopes: dict[int | None, dict[str, int]] = {} + for item in items: + if item.kind != "mod" and not item.is_test_fn: + scopes.setdefault(item.parent_index, {}).setdefault(item.name, item.index) + return scopes + + +def _uses(item: Item, referent: Item, *, strict: bool) -> bool: + """Whether ``item``'s body references ``referent``. + + The two propagation directions want different answers, because they + fail in opposite directions: + + * Widening a gate (a helper inheriting its callers' needs) is safe + when over-linked, and *missing* a link leaves a helper ungated and + dead on a narrow leg. So it takes the broad reading -- a `fn` + handed over as a function pointer (``is_some_and(is_synthesised_ + name)``) never shows a ``(``. + * Marking something unconditional is unsafe when over-linked: a + local named ``score`` or ``find`` would keep the helper of that + name ungated and unused on every single-language leg. So that one + is ``strict`` and wants the call shape. + """ + if strict and referent.kind == "fn": + return referent.name in item.calls + return referent.name in item.references + + +def _resolve_call( + items: list[Item], scopes: dict[int | None, dict[str, int]], caller: Item, name: str +) -> int | None: + """The helper ``name`` refers to from inside ``caller``. + + Resolution walks outwards from the calling scope, the way Rust's + does. A file-wide lookup by bare name is not good enough here: + `spaces_tests.rs` declares three different `analyse` helpers in three + sibling modules, one of which hardcodes `RustParser`, so a flat map + makes every Kotlin, Java, Ruby and Groovy test in the file look as + though it needs Rust. Same shape for `conditions` in `abc.rs`. + """ + # Start at the caller's *own* scope, not its parent's: a `fn` can + # declare helpers inside itself, and `spaces_tests.rs` does exactly + # that (`find` nested in `child`). Starting a level out never + # resolves those, which leaves them with no callers, no inherited + # needs, and an ungated look that then vetoes the gate on every + # import they touch. + scope: int | None = caller.index + seen: set[int | None] = set() + while scope not in seen: + seen.add(scope) + found = scopes.get(scope, {}).get(name) + if found is not None and found != caller.index: + return found + if scope is None: + return None + scope = items[scope].parent_index + return None + + +def resolve_needs(items: list[Item]) -> dict[int, frozenset[str]]: + """The languages each item needs, by role. + + The three roles want three different answers, and conflating them + produces gates that are confidently wrong in both directions: + + * A **test** needs the languages its own body names, plus those of + any helper it calls that *hardcodes* a parser + (``assert_csharp_fixture_spells``). A helper taking ``lang: LANG`` + is generic -- its body may mention `LANG::Rust` in one arm of a + dispatch ``match`` -- and propagating that to callers marks every + Kotlin test in `spaces_tests.rs` as needing Rust. + * A **sweep** drives a per-language table that usually lives in a + helper, so it does follow generic calls; the result is only ever + used for the "enables at least one row" union check. + * A **helper** is live as soon as any caller is, so it needs the + union of its callers' needs -- the reverse direction. + + A ``mod`` then takes the union of everything inside it, which is + what keeps its imports from going unused in a build that drops every + test it holds. + """ + scopes = _helper_scopes(items) + + def reachable(item: Item, generic_too: bool) -> set[str]: + """Direct needs of ``item`` and of the helpers it calls.""" + collected = set(item.direct) + seen = {item.index} + queue: list[Item] = [item] + while queue: + current = queue.pop() + for name in current.references: + callee_index = _resolve_call(items, scopes, current, name) + if callee_index is None or callee_index in seen: + continue + callee = items[callee_index] + # `strict`: a test's marker is an `all(...)`, so an + # over-link here gates it more narrowly than it is used + # and it silently stops running. A local, field or type + # sharing a helper's name is enough to cause one. + if not _uses(current, callee, strict=True): + continue + seen.add(callee_index) + if callee.takes_lang_param and not generic_too: + continue + collected |= callee.direct + queue.append(callee) + return collected + + needs: dict[int, set[str]] = {item.index: set() for item in items} + for item in items: + if item.kind == "fn" and (item.is_test_fn or not item.takes_lang_param): + needs[item.index] = reachable(item, generic_too=item.is_sweep) + # A sweep over the whole enum hands every *enabled* variant + # to whatever parses it, so it is live exactly when some + # language is — which is also what its own `checked > 0` + # guard asserts. Its fixtures come from a + # `LANG`-parameterised helper, whose arms are deliberately + # not attributed to callers, so the body names almost + # nothing and whatever leaks through becomes the whole gate: + # two `every_*_in_every_language` parity sweeps were gated + # down to one `c-family-helpers`, and `--compare` was the + # only check that saw it. + # + # Conditioned on the sweep carrying *both halves* of the + # rule in `.claude/rules/testing.md`: an `is_enabled()` row + # filter and a non-vacuity assertion. Together those mean it + # *fails* rather than skips when no language is enabled, so + # it must be absent then — the #1220 class. A full-enum + # sweep with no such guard asserts over variants that exist + # without their grammars (`Display`, `FromStr`, slug + # round-trips, `is_enabled` itself); gating those would stop + # them running on the `--no-default-features` leg that is + # precisely where they belong. + if item.is_test_fn and item.needs_a_language_enabled: + needs[item.index] |= set(KNOWN_FEATURES) + + # Drop what an item's own gate already excludes, before any of it + # propagates. `ast_seam_test.rs`'s + # `ast_parse_returns_language_disabled_for_off_feature` is + # `cfg(not(feature = "javascript"))` and names `LANG::Javascript` + # precisely because the grammar is absent; leaving `javascript` in + # its set gates the import it makes on the one feature under which + # the test never exists (`E0433: cannot find type LANG`). + for item in items: + predicate = effective_predicate(items, item) + if predicate is None: + continue + needs[item.index] = { + feature + for feature in needs[item.index] + if not gate_excludes(predicate, feature) + } + + # A referent with even one unconditional caller must stay + # unconditional itself, whatever its other callers need. Without + # this the reverse propagation below gates a helper on the union of + # only its *language* callers and the language-agnostic test that + # also calls it loses the definition -- seven `E0425 cannot find + # function` errors on the `--features go` leg, in `args`, + # `assert_members_score`, `render`, `on_stack` and friends. + # Helpers inherit from their callers. Iterated to a fixpoint because + # a helper's callers include other helpers. + changed = True + while changed: + changed = False + for item in items: + for name in item.references: + callee_index = _resolve_call(items, scopes, item, name) + if callee_index is None or items[callee_index].is_test_fn: + continue + if not _uses(item, items[callee_index], strict=False): + continue + addition = needs[item.index] - needs[callee_index] + if addition: + needs[callee_index] |= addition + changed = True + + # Only now, once every item knows what it needs, can "always + # compiled" be read off: an item with an empty set is one `offenders` + # will not gate, so everything it uses must stay ungated too. + # Computing this *before* the propagation above misses the case that + # matters — `checker.rs`'s `assert_variant_is_string` is reached only + # from a macro body, so it inherits nothing and stays ungated while + # the `count_string_matches_for_kind` it calls gets gated from its + # other callers (`E0425` on the zero-language leg). + # + # `pub` joins the seed for the same reason: `offenders` never gates + # one, because its users are in files this scanner cannot see. + unconditional: set[int] = set() + changed = True + while changed: + changed = False + for item in items: + # A container is not a user. A `mod`'s body spans every item + # inside it, so letting one propagate marks everything it + # holds as always-compiled — which is how `checker.rs`'s two + # `assert_*_is_string` macros lost the 17-language gate their + # actual callers give them and went dead on every leg. + if not item.in_test_scope or item.kind == "mod": + continue + if item.index not in unconditional: + if needs[item.index] and not item.is_public: + continue + unconditional.add(item.index) + changed = True + for name in item.references: + index = _resolve_call(items, scopes, item, name) + if index is None or index in unconditional: + continue + if not _uses(item, items[index], strict=True): + continue + unconditional.add(index) + needs[index] = set() + changed = True + + for index in unconditional: + needs[index] = set() + + # Containers last, innermost first, so a `mod` sees its children's + # resolved sets rather than their raw ones. + for item in reversed(items): + if item.parent_index is not None: + needs[item.parent_index] |= needs[item.index] + return {index: frozenset(value) for index, value in needs.items()} + + +def _combine_gates(gates: list[str]) -> str: + """``any(...)`` over each user's gate, deduped, flattened when it can be.""" + unique = list(dict.fromkeys(gates)) + if len(unique) == 1: + return unique[0] + # A disjunction of bare `feature = "x"` atoms is just their `any`, + # which is what most of these are and much the easier read. + atoms = [g for g in unique if g.startswith("feature")] + if len(atoms) == len(unique): + return "any(" + ", ".join(sorted(atoms)) + ")" + return "any(" + ", ".join(unique) + ")" + + +def needed_features( + items: list[Item], item: Item, needs: dict[int, frozenset[str]] +) -> frozenset[str]: + """``item``'s needs, minus any its own gate deliberately excludes.""" + predicate = effective_predicate(items, item) + return frozenset(f for f in needs[item.index] if not gate_excludes(predicate, f)) + + +AST_CRATE_PREFIX = "big-code-analysis-ast/" + + +def feature_atom(feature: str, path: str) -> str: + """The `cfg` atom asserting ``feature``'s grammar, as ``path`` sees it. + + `c-family-helpers` is spelled differently in the two crates, and + getting it wrong is silent. In `big-code-analysis-ast` the + `c` / `cpp` / `mozcpp` features each list `c-family-helpers` + directly, so the bare atom is true whenever the helper grammars are + compiled. In the root crate they list + `big-code-analysis-ast/cpp` instead, which enables the *sub-crate's* + copy and leaves the root's own feature off — and `all-languages` + does not list it either. So a root-crate item gated on the bare atom + is absent from a default build, taking its `Ccomment` / `Preproc` + coverage with it, and `--all-features` hides that by enabling the + feature explicitly. + """ + if feature != C_FAMILY_HELPER_FEATURE or path.startswith(AST_CRATE_PREFIX): + return f'feature = "{feature}"' + enablers = sorted(IMPLIES_C_FAMILY_HELPERS | {feature}) + return "any(" + ", ".join(f'feature = "{f}"' for f in enablers) + ")" + + +def _render(features: frozenset[str], path: str, combinator: str) -> str: + atoms = [feature_atom(f, path) for f in sorted(features)] + if combinator == "any": + # `feature_atom` returns an `any(...)` of its own for the root + # crate's `c-family-helpers`. Nesting that inside an `any` is a + # disjunction of a disjunction — the same predicate, spelled with + # `c`, `cpp` and `mozcpp` listed twice. + flattened: list[str] = [] + for atom in atoms: + group = ( + balanced_group(atom, atom.index("(")) + if atom.startswith("any(") + else None + ) + flattened.extend(split_top_level(group[0]) if group else [atom]) + atoms = sorted(dict.fromkeys(flattened)) + if len(atoms) == 1: + return atoms[0] + return f"{combinator}(" + ", ".join(atoms) + ")" + + +def required_marker( + items: list[Item], item: Item, needs: dict[int, frozenset[str]] +) -> str | None: + """The ``cfg`` predicate ``item`` should carry, or ``None`` if any. + + A ``#[test] fn`` uses every language it names, so it needs them all. + Everything else -- a helper, a containing ``mod`` -- is live as soon + as one caller is, so it needs any. + """ + required = needed_features(items, item, needs) + if not required: + return None + # Only a plain test needs *every* language it names. A sweep skips + # the disabled ones at run time, so it needs any one of its rows — + # `all(...)` there gates the 23-language parity suites out of every + # build but the full one, and takes their helpers dead with them. + if item.is_test_fn and not item.is_sweep: + return _render(required, item.path, "all") + + # A sweep that also pins a parser by type needs both halves: `all` of + # what it pins, `any` of what it iterates. Without this the marker + # `--fix` writes can never satisfy the check that asked for it, and + # the fixpoint loop stacks one copy per pass before giving up. + if item.is_test_fn and item.is_sweep: + pinned = hardcoded_closure(items, item) & required + if pinned: + rest = required - pinned + parts = [feature_atom(f, item.path) for f in sorted(pinned)] + if rest: + parts.append(_render(rest, item.path, "any")) + return parts[0] if len(parts) == 1 else "all(" + ", ".join(parts) + ")" + + # A `macro_rules!` takes the disjunction of its callers' *gates* + # rather than of the features inside them, because flattening loses + # a conjunction: `checker.rs`'s `assert_variants_is_string!` has one + # caller, a test needing all seventeen languages at once, and + # `any(…seventeen…)` leaves the macro defined on a build with one of + # them and nothing to invoke it (`unused macro definition`). + # + # Only macros. Applied to ordinary helpers the same rule gates them + # more narrowly than they are used, because this sees only the + # callers it can resolve by name and a helper reached another way + # then loses its definition (`E0425: cannot find function analyse`). + # An over-wide `any(features)` on a helper is merely an unused + # warning, which the crate-level `cfg_attr` already covers. + caller_gates: list[str] = [] + if item.kind == "macro": + # Hoisted: rebuilding the scope index per candidate caller is + # `O(users x n)`, and it only looked cheap because `and` + # short-circuits before it for everything that is not a user. + scopes = _helper_scopes(items) + caller_gates = [ + gate + for gate in ( + _caller_marker(items, other, needs) + for other in items + if other.index != item.index + and other.in_test_scope + and other.kind != "mod" + and _uses(other, item, strict=False) + and _resolve_call(items, scopes, other, item.name) == item.index + ) + if gate + ] + if caller_gates: + return _combine_gates(caller_gates) + return _render(required, item.path, "any") + + +def _caller_marker( + items: list[Item], item: Item, needs: dict[int, frozenset[str]] +) -> str | None: + """``item``'s gate as a caller: declared, else derived for a test. + + Only tests are derived here. Recursing into another helper would + need cycle handling for no gain: a helper reached only through other + helpers still bottoms out at the tests that drive them, and those + already carry their own markers by the time this is asked. + """ + own = [ + p + for p in item.own_predicates + if any(not gate_admits(p, f) for f in KNOWN_FEATURES) + ] + if own: + return own[0] if len(own) == 1 else "all(" + ", ".join(own) + ")" + if item.is_test_fn: + required = needed_features(items, item, needs) + if not required: + return None + return _render(required, item.path, "all") + return None + + +def over_gated( + items: list[Item], needs: dict[int, frozenset[str]] +) -> list[tuple[Item, frozenset[str]]]: + """Items whose own `cfg` requires a feature they never use. + + The mirror of `offenders`, and the direction nothing else can see: a + gate that is too *wide* panics on the leg that lacks the grammar, but + one that is too *narrow* just drops the test, and a leg running two + hundred fewer tests looks exactly like a green run (#1478). + + A feature is over-declared when turning *it alone* off is what + excludes the item, and the item needs neither it nor anything that + enables it. Both halves matter: + + * Off alone, not off with its closure. A `cpp`-gated test is also + excluded from a build without `c-family-helpers` — but only + because such a build has no `cpp` either, and reading that as a + requirement makes every C-family gate report a helper feature it + never spells. + * Nothing the item needs may enable it. In the ast crate `cpp` + lists `c-family-helpers`, so a test needing `cpp` and gated on the + helper is excluded from no build it could have run in. + + The reference build is all features on *except* the ones the gate + requires to be off, so a `cfg(not(feature = "javascript"))` + disabled-path test is measured from a build it actually compiles in. + Testing against a bare all-on baseline instead exempted the whole + item, and `all(not(python), rust, go)` then hid its `go`. + + An `any(…)` requires no feature on its own, so a sweep's union gate + never reports here. + """ + found: list[tuple[Item, frozenset[str]]] = [] + for item in items: + unused = over_declared(item, needs) - item.hand_written + if unused: + found.append((item, unused)) + return found + + +def over_declared(item: Item, needs: dict[int, frozenset[str]]) -> frozenset[str]: + """The features ``item``'s own gate requires but its body never uses. + + Before the marker subtraction, so `stale_markers` can ask the same + question of the same answer rather than keeping a second copy of the + rule in step with this one. + """ + if not item.in_test_scope: + return frozenset() + # A `pub` item's users are in files this single-file scanner never + # sees, so `needs` is only the subset visible here and every gate + # placed for an out-of-file caller reads as over-declared. + # `offenders` skips them for the mirror-image reason. + if item.is_public: + return frozenset() + own = [p for p in item.own_predicates if "feature" in p] + if not own: + return frozenset() + conjunction = own[0] if len(own) == 1 else "all(" + ", ".join(own) + ")" + required = needs[item.index] + implied = frozenset().union(*(enabled_closure(f) for f in required), frozenset()) + reference = negated_features(conjunction) + return frozenset( + feature + for feature in KNOWN_FEATURES + if feature not in implied + and feature not in reference + and not evaluate_predicate(conjunction, reference | {feature}) + ) + + +def stale_markers( + items: list[Item], needs: dict[int, frozenset[str]] +) -> list[tuple[Item, frozenset[str]]]: + """Markers naming a feature that is no longer over-declared. + + A stale marker silences nothing — there is nothing left to silence — + so it cannot cause a wrong verdict on its own. It is still a lie + about why the gate is the shape it is, and the next reader has no + way to tell a load-bearing entry from a leftover. Making it fail is + what keeps the accepted gates readable as a census rather than + accumulating into one. + """ + return [ + (item, stale) + for item in items + if (stale := item.hand_written - over_declared(item, needs)) + ] + + +def offenders( + items: list[Item], needs: dict[int, frozenset[str]] +) -> list[tuple[Item, frozenset[str]]]: + """Items admitted into a build lacking a language they need. + + A sweep is exempt from the per-feature test: it skips disabled + languages at runtime. It still has to be excluded from a build + enabling *none* of them, which is the union check in the else branch + and the half `check-feature-gates.py` verifies against a real build. + """ + found: list[tuple[Item, frozenset[str]]] = [] + for item in items: + # Production code names these types unconditionally and must + # stay ungated: only the test scope is this gate's business. + if not item.in_test_scope: + continue + # Containers are deliberately not gated. Gating `mod tests` on + # the union of its contents would be the cheap way to drop its + # imports and shims in a build with no grammars at all, but it + # takes 134 language-agnostic tests down with them -- 34 in + # `suppression.rs` alone, which never parses anything. Those + # tests are exactly what the `--no-default-features` leg is for, + # so the module stays open and its helpers, shims and + # language-bound imports carry their own markers instead. + if item.kind == "mod": + continue + # A `pub` / `pub(crate)` item is reachable from files this + # single-file scanner never sees. `test_support.rs` is the whole + # story: its `pub(crate) use` re-export feeds 45 call sites in + # seven modules, and gating it on the users visible *here* drops + # the name from every build the other six need it in + # (`E0432 unresolved import`). Gate those by hand, the way + # `assert_perl_fixture_spells` already is. + if item.is_public: + continue + required = needed_features(items, item, needs) + if not required: + continue + predicate = effective_predicate(items, item) + if item.is_sweep or not item.is_test_fn: + # A sweep skips disabled languages at run time, so the rows it + # iterates need only `any`. A parser it *hardcodes* is a + # different thing: `check_metrics::` picks its + # grammar through a type parameter, which no runtime filter + # can skip, so that subset is required outright even here. + # Without this the exemption is a way round the whole gate. + # Sweeps only. A *helper* that hardcodes a parser is still + # correctly gated `any(callers)`: it exists in a build where + # one caller does, and is simply never called there. Demanding + # `all` of it gates the helper out from under callers that do + # need it. + pinned = frozenset( + f + for f in hardcoded_closure(items, item) & required + if item.is_test_fn and gate_admits(predicate, f) + ) + if pinned: + found.append((item, pinned)) + continue + disabled = frozenset().union(*(disabled_closure(f) for f in required)) + if predicate is None or evaluate_predicate(predicate, disabled): + found.append((item, required)) + continue + missing = frozenset(f for f in required if gate_admits(predicate, f)) + if missing: + found.append((item, missing)) + return found + + +# --------------------------------------------------------------------------- +# Rewriting +# --------------------------------------------------------------------------- + + +def wrap_marker(predicate: str, indent: str) -> list[str]: + """``#[cfg(...)]`` lines for ``predicate``, wrapped like rustfmt would. + + Emitted above the rest of the attribute run so the marker sits on the + function's attribute stack rather than between ``#[test]`` and the + signature, and so the whole run reads gate-then-test in every file. + """ + single = f"{indent}#[cfg({predicate})]" + if len(single) <= MAX_WIDTH: + return [single] + open_paren = predicate.find("(") + group = None if open_paren == -1 else balanced_group(predicate, open_paren) + if group is None: + return [single] + head = predicate[:open_paren] + lines = [f"{indent}#[cfg({head}("] + lines.extend(f"{indent} {part}," for part in split_top_level(group[0])) + lines.append(f"{indent}))]") + return lines + + +def apply_fixes( + path: pathlib.Path, + items: list[Item], + needs: dict[int, frozenset[str]], + missing: list[Item], +) -> int: + """Insert the derived marker above each item in ``missing``. + + Rewrites bottom-up so an earlier insertion cannot shift a later + item's recorded line. + """ + lines = path.read_text(encoding="utf-8").split("\n") + inserted = 0 + for item in sorted(missing, key=lambda i: -i.attr_line): + predicate = required_marker(items, item, needs) + if predicate is None: + continue + lines[item.attr_line - 1 : item.attr_line - 1] = wrap_marker( + predicate, item.indent + ) + inserted += 1 + if inserted: + path.write_text("\n".join(lines), encoding="utf-8") + return inserted + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def scan_revision(ref: str, root: pathlib.Path) -> dict[str, list[Item]]: + """Scan the tree as of ``ref``, extracted to a scratch directory. + + `git archive` rather than a worktree: it touches nothing in the + checkout, needs no lock, and costs about two tenths of a second. + """ + checkout = pathlib.Path(tempfile.mkdtemp(prefix="check-test-lang-gates-")) + try: + archive = subprocess.run( + ["git", "archive", ref], + cwd=root, + capture_output=True, + check=False, + ) + if archive.returncode != 0: + raise ScanError( + f"`git archive {ref}` failed: {archive.stderr.decode().strip()}" + ) + subprocess.run( + ["tar", "-x", "-C", str(checkout)], + input=archive.stdout, + check=True, + ) + table = language_table( + (checkout / LANGS_RS.relative_to(REPO_ROOT)).read_text(encoding="utf-8") + ) + return scan_tree(checkout, table) + finally: + shutil.rmtree(checkout, ignore_errors=True) + + +def compare_revisions( + ref: str, root: pathlib.Path, table: dict[str, str] +) -> list[tuple[str, frozenset[str]]]: + """Tests that still exist but stopped compiling somewhere. + + The one direction no static check can reach on its own. `over_gated` + compares a marker against the derivation; when the two agree and are + both wrong, only the previous revision says so. + """ + probes = probe_builds(table) + reference_tree = scan_revision(ref, root) + # The same guard `main` applies to the working tree. A reference with + # no tests in it makes every name miss the `after` side, so nothing + # is ever reported and the check prints OK having compared nothing — + # an `export-ignore` on a scan root, or a ref predating one, is all + # it takes. + if not any( + item.is_test_fn and item.in_test_scope + for items in reference_tree.values() + for item in items + ): + raise ScanError( + f"the tree at {ref} has no tests in it — nothing to compare " + "against. A scan root is missing from that revision, or is " + "excluded from `git archive` by `.gitattributes`." + ) + before = membership(reference_tree, probes) + after = membership(scan_tree(root, table), probes) + lost: list[tuple[str, frozenset[str]]] = [] + for name, was in sorted(before.items()): + # Only a test that is still here. One that was deleted or renamed + # is a deliberate change, not a gate that narrowed under it. + if name not in after: + continue + dropped = was - after[name] + if dropped: + lost.append((name, dropped)) + return lost + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="check-test-lang-gates", + description=__doc__.splitlines()[0], + epilog=( + "Every test naming a concrete parser must be absent from a build " + "without that language's grammar, not present and panicking." + ), + ) + parser.add_argument( + "--show", + action="store_true", + help="Print every in-scope item with its derived marker and exit 0.", + ) + parser.add_argument( + "--fix", + action="store_true", + help="Write the missing markers in place and exit 0.", + ) + parser.add_argument( + "--compare", + metavar="REF", + help=( + "Fail if a test that still exists stopped compiling under any " + "single-language build since REF." + ), + ) + parser.add_argument( + "--root", + type=pathlib.Path, + default=REPO_ROOT, + help=argparse.SUPPRESS, + ) + args = parser.parse_args(argv) + + try: + table = language_table( + (args.root / LANGS_RS.relative_to(REPO_ROOT)).read_text(encoding="utf-8") + ) + per_file = scan_tree(args.root, table) + except (ScanError, OSError, UnicodeDecodeError) as exc: + sys.stderr.write(f"error: {exc}\n") + return 2 + + # Production items are scanned too, so a bare `if not per_file` would + # be satisfied by a tree of pure library code and let the gate report + # OK having checked nothing. Count the thing it exists to find. + if not any( + item.is_test_fn and item.in_test_scope + for items in per_file.values() + for item in items + ): + sys.stderr.write( + "error: no tests found under " + f"{', '.join(str(r) for r in SCAN_ROOTS)}\n" + " the scanner is broken or the tree is wrong; this gate\n" + " cannot pass by finding nothing to check.\n" + ) + return 2 + + if args.compare: + try: + lost = compare_revisions(args.compare, args.root, table) + except ( + ScanError, + OSError, + UnicodeDecodeError, + subprocess.CalledProcessError, + ) as exc: + sys.stderr.write(f"error: {exc}\n") + return 2 + if lost: + sys.stderr.write( + f"error: {len(lost)} test(s) stopped compiling somewhere they " + f"used to, since {args.compare}\n\n" + ) + for name, dropped in lost: + sys.stderr.write(f" {name}\n") + for build in sorted(dropped): + sys.stderr.write(f" no longer built by: {build}\n") + sys.stderr.write( + "\nEach of these still exists and still passes under " + "`--all-features`, so\nnothing else reports it: a gate too " + "*wide* panics on the leg that lacks\nthe grammar, but one " + "too *narrow* just drops the test and the leg stays\ngreen. " + "Widen the gate back to what it was.\n\nIf the narrowing is " + "deliberate — the gate really was too wide and the test\ndoes " + "not need that grammar — say so on the pull request with the\n" + "`gate-narrowing-intended` label. Not with an in-source " + "marker: this\ncompares against the merge base, so a marker " + "would be stale the moment\nthe branch lands and would sit in " + "the tree as a hole nothing detects.\n\n" + "See `.claude/rules/testing.md` (#1478).\n" + ) + return 1 + print(f"test-lang-gates: OK — no test lost a build since {args.compare}") + return 0 + + if args.show: + # Only the items a marker was derived for. Printing every scanned + # item buries those under thousands of `needs -` lines, most of + # them production code the gate never looks at. + for items in per_file.values(): + needs = resolve_needs(items) + for item in items: + if not item.in_test_scope: + continue + marker = required_marker(items, item, needs) + if marker is not None: + print(f"{item.describe()} needs {marker}") + return 0 + + if args.fix: + # To a fixpoint. Gating one item changes what the items around + # it need -- an import takes the union of its users, and a user + # that just acquired a marker contributes differently -- so a + # single pass can leave a marker one feature short and the next + # run stacks a second `#[cfg]` on top of the first rather than + # widening it. + fixed = 0 + for _ in range(FIX_PASSES): + pass_fixed = 0 + try: + per_pass = scan_tree(args.root, table).items() + for relative, items in per_pass: + needs = resolve_needs(items) + found = offenders(items, needs) + if found: + pass_fixed += apply_fixes( + args.root / relative, items, needs, [i for i, _ in found] + ) + except ScanError as exc: + sys.stderr.write(f"error: {exc}\n") + return 2 + fixed += pass_fixed + if pass_fixed == 0: + break + else: + sys.stderr.write( + f"error: still not clean after {FIX_PASSES} passes; the " + "derivation is not converging\n" + ) + return 2 + print(f"test-lang-gates: inserted {fixed} marker(s)") + return 0 + + checked = 0 + failures: list[tuple[str, Item, frozenset[str]]] = [] + wider: list[tuple[str, Item, frozenset[str]]] = [] + stale: list[tuple[str, Item, frozenset[str]]] = [] + try: + for relative, items in per_file.items(): + needs = resolve_needs(items) + checked += sum( + 1 + for item in items + if item.in_test_scope and needed_features(items, item, needs) + ) + failures.extend( + (relative, item, why) for item, why in offenders(items, needs) + ) + wider.extend( + (relative, item, why) for item, why in over_gated(items, needs) + ) + stale.extend( + (relative, item, why) for item, why in stale_markers(items, needs) + ) + except ScanError as exc: + sys.stderr.write(f"error: {exc}\n") + return 2 + + # Both directions, always. They are independent defects in + # independent items, and short-circuiting on the first hid the other + # until the tree happened to be clean of it. + if wider: + sys.stderr.write( + f"error: {len(wider)} test item(s) require a feature they never use\n\n" + ) + for relative, item, why in wider: + sys.stderr.write( + f" {item.describe()}\n gated on, but never uses: " + f"{', '.join(sorted(why))}\n" + ) + sys.stderr.write( + "\nA gate wider than the item needs keeps it out of builds it " + "could run in,\nand nothing else notices: too *wide* panics on " + "the leg that lacks the\ngrammar, too *narrow* just drops the " + "test and the leg still looks green.\n\n" + "Either narrow the gate, or — when the reason is one the " + "derivation cannot\nsee, such as a language chosen from a glob " + "or a path — say so above it:\n\n" + " // test-lang-gates: hand-written(cpp) — the corpus walk " + "picks the\n // language per file from its extension\n\n" + "See `.claude/rules/testing.md` (#1478).\n" + ) + if failures: + sys.stderr.write("\n") + + if failures: + sys.stderr.write( + f"error: {len(failures)} test item(s) name a language their `cfg` " + "does not require\n\n" + ) + for relative, item, why in failures: + names = ", ".join(sorted(why)) + sys.stderr.write(f" {item.describe()}\n unguarded: {names}\n") + sys.stderr.write( + "\nEach of these compiles into a build without the grammar it " + "names and\npanics in `Tree::new`. Add the marker the derivation " + "reports:\n\n" + " ./utils/check-test-lang-gates.py --show\n" + " ./utils/check-test-lang-gates.py --fix\n\n" + 'See `.claude/rules/testing.md`, "Gate a feature-gated fixture ' + 'table on the\nunion of its rows" (#1472, #1413).\n' + ) + + if stale: + sys.stderr.write( + f"error: {len(stale)} hand-written marker(s) name a feature the " + "gate no longer\nover-declares\n\n" + ) + for relative, item, why in stale: + sys.stderr.write( + f" {item.describe()}\n no longer over-declared: " + f"{', '.join(sorted(why))}\n" + ) + sys.stderr.write( + "\nThe marker silences nothing — there is nothing left to " + "silence — but the\nnext reader cannot tell a load-bearing " + "entry from a leftover. Drop the\nfeature from the marker, or " + "the whole marker if it names nothing else.\n" + ) + + if wider or failures or stale: + return 1 + + print(f"test-lang-gates: OK — {checked} gated test item(s) checked") + return 0 + + +if __name__ == "__main__": + sys.exit(main())