From c419a1e240afe833f1a4b6a892a956a56c6ad463 Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Wed, 16 Sep 2026 18:45:54 -0700 Subject: [PATCH 1/8] test(features): add a gate deriving each test's language cfg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mk_langs!` generates every `*Parser` alias, `*Code` tag and `LANG` variant unconditionally; only `get_ts_language`'s arms are cfg'd. So `check_metrics::(…)` compiles with `python` off and then panics in `Tree::new`. Nothing caught that: `make pre-commit` builds only the default and `--all-features` flavours, and the feature-matrix legs compiled the tests without running them. `utils/check-test-lang-gates.py` derives the marker an item needs from the languages its body reaches, reading the language table out of the `mk_langs!` invocation rather than keeping a copy of it. `--fix` writes the markers, `--show` prints the derivation. It checks three directions, because each is blind to the next. **Too narrow** — the item is admitted into a build lacking a grammar it names, and panics. A test needs `all(…)` of what it names; a helper or fixture table needs `any(…)` of its users; a sweep needs only `any(…)` of the rows it iterates, since `is_enabled` skips the rest, but still needs whatever parser it pins through a type parameter. **Too wide** — the gate requires a feature the body never uses, keeping the item out of builds it could have run in. Nothing else can see this: too wide panics, too narrow just drops the test and the leg stays green. Sixteen gates in the tree are deliberately wider than their bodies and say so with a `// test-lang-gates: hand-written() — ` marker; a marker that stops being load-bearing fails too, so the list stays a census rather than accumulating. **Neither** — when a marker and the derivation agree and are both wrong, only history says so. `--compare ` scans the tree at `ref` as well (`git archive`, no cargo), derives which single-language builds compile each test in each, and fails on one that still exists but stopped being built somewhere. The derivation rules that took measuring: - A `LANG` being *compared* is an identity test, not a parse — the enum exists without its grammar. Covers `==`, `matches!` (every alternative, not just the first), `assert_eq!` (read with balanced parens, so a variant behind a call in the expected value is still a comparison) and receiver position, except the two methods that hand back the grammar itself. - Deliberately *not* extended to the later arms of a `match`, where `|` groups a dispatch table a sweep legitimately derives from. - A sweep over the whole enum that filters on `is_enabled` *and* asserts non-vacuity needs `any()`: it fails rather than skips with none enabled, and its fixtures come from a `LANG`-parameterised helper whose arms are not attributed to callers. - `c-family-helpers` is spelled differently in the two crates, and getting it wrong is silent. 81 self-tests, including the repository check that the derivation reproduces all 167 gates humans had written by hand — which is what earned it the right to generate the rest. Refs #1472, #1413, #1478 --- utils/check-test-lang-gates-test.py | 1800 ++++++++++++++++++++ utils/check-test-lang-gates.py | 2346 +++++++++++++++++++++++++++ 2 files changed, 4146 insertions(+) create mode 100755 utils/check-test-lang-gates-test.py create mode 100755 utils/check-test-lang-gates.py diff --git a/utils/check-test-lang-gates-test.py b/utils/check-test-lang-gates-test.py new file mode 100755 index 00000000..a491eaf3 --- /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 00000000..38058f23 --- /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()) From 2947203b519c04efea70ff69da73f3b44322102c Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Wed, 16 Sep 2026 18:46:10 -0700 Subject: [PATCH 2/8] test(features): gate every per-language test on its grammar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured across `src/`, `big-code-analysis-ast/src/` and `tests/`: about 2,950 tests named a grammar without gating on it, so `cargo nextest run --no-default-features --features rust -p big-code-analysis` failed in the thousands and a partial-feature build could not be used to verify anything — the misleading-red class #1171 fixed for the corpora. The markers are derived and written by `check-test-lang-gates.py`, not hand-placed, and sit on the attribute stack alongside `#[test]`. No test changed its name, its assertions, or whether it runs under `--all-features`: `cargo nextest list --all-features --workspace` is byte-identical before and after, which is the safety net for a mechanical pass this size and the reason per-`fn` attributes were used rather than new wrapping modules, which would have renamed every test path and destroyed it. Four shapes the derivation cannot see carry a hand-written marker instead, sixteen items in all: a corpus walk choosing a language per file from a glob, a non-vacuity anchor, a `mod` declaration with no body to read, and a grammar picked out of a `"foo.rs"` extension string. Three source changes come with it: - `suppression_test.rs`'s `analyze_lang` took the language from the path's extension, which hid it from the scan and from the reader; it now takes a `LANG` at the call site. - The two library roots and the five integration-test crate roots relax `unused_imports` on partial builds only. Per-language gating makes "is this import live" a function of the enabled feature set, which no `cfg` on the import can express; `all-languages` is on by default and under `--all-features`, so the builds that matter still police it. - `assert_csharp_fixture_spells` is gated to match its callers, the "also noticed" item in #1472. One latent defect surfaced: six `Ccomment` / `Preproc` tests would have been gated out of every default build by the root crate's `c-family-helpers` not being what its ast-crate namesake enables. Refs #1472, #1413 --- big-code-analysis-ast/src/alterator.rs | 63 ++ big-code-analysis-ast/src/ast.rs | 12 + big-code-analysis-ast/src/checker.rs | 199 ++++++ big-code-analysis-ast/src/comment_rm.rs | 26 + big-code-analysis-ast/src/getter.rs | 15 + big-code-analysis-ast/src/langs.rs | 28 + big-code-analysis-ast/src/lib.rs | 11 +- big-code-analysis-ast/src/node.rs | 53 +- big-code-analysis-ast/src/parser.rs | 6 + big-code-analysis-ast/src/preproc_tests.rs | 12 + src/c_family_space_names_tests.rs | 10 + src/from_path_error.rs | 2 + src/function_tests.rs | 1 + src/lib.rs | 11 +- src/metrics/abc.rs | 604 ++++++++++++++++- src/metrics/abc/cpp.rs | 8 + src/metrics/abc/csharp.rs | 1 + src/metrics/cognitive.rs | 365 +++++++++++ src/metrics/cognitive/python.rs | 1 + src/metrics/cyclomatic.rs | 230 ++++++- src/metrics/halstead.rs | 304 +++++++++ src/metrics/loc.rs | 613 ++++++++++++++++++ src/metrics/mi.rs | 9 + src/metrics/nargs.rs | 143 ++++ src/metrics/nexits.rs | 118 ++++ src/metrics/nom.rs | 80 +++ src/metrics/npa.rs | 198 ++++++ src/metrics/npm.rs | 207 ++++++ src/metrics/tokens.rs | 52 ++ src/metrics/wmc.rs | 190 ++++++ src/ops.rs | 32 + src/output/dump.rs | 13 + src/output/dump_metrics.rs | 6 + src/spaces_tests.rs | 118 ++++ src/suppression.rs | 16 + src/test_support.rs | 14 + src/wire.rs | 13 + src/wire_ops_tests.rs | 3 + tests/api/ast_seam_test.rs | 5 + tests/api/book_ast_traversal_examples.rs | 8 + tests/api/book_library_examples.rs | 3 + tests/api/derive_eq_hash_ord.rs | 7 + tests/api/main.rs | 24 +- tests/api/parser_reuse.rs | 15 + tests/api/suppression_test.rs | 83 ++- tests/corpus/csharp_test.rs | 9 + tests/corpus/deepspeech_test.rs | 12 + tests/corpus/irules_test.rs | 2 + tests/corpus/main.rs | 9 + tests/corpus/pdf_js_test.rs | 9 + tests/corpus/php_test.rs | 9 + tests/corpus/serde_test.rs | 9 + tests/grammars/main.rs | 9 + tests/grammars/mozcpp_grammar_metrics.rs | 2 + tests/output_formats/csv_test.rs | 6 + tests/output_formats/main.rs | 9 + .../parity/cognitive_cross_language_parity.rs | 82 +++ tests/parity/exit_cross_language_parity.rs | 75 +++ tests/parity/functions_metrics_parity.rs | 150 +++++ tests/parity/main.rs | 9 + tests/parity/nargs_cross_language_parity.rs | 75 +++ tests/parity/ops_metrics_space_parity.rs | 100 +++ tests/parity/self_reference_operand_parity.rs | 100 +++ tests/parity/space_span_containment.rs | 125 ++++ 64 files changed, 4696 insertions(+), 37 deletions(-) diff --git a/big-code-analysis-ast/src/alterator.rs b/big-code-analysis-ast/src/alterator.rs index 1d4d0f50..d5de2f35 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 629aec7e..9fe81673 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 3f289c70..9eccf6a3 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 6d14a1f3..4ea442f7 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 2b284659..83c835e3 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 8b0c4730..966a6e02 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 63c8aedd..23e37828 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 cd2638e4..d5e30b99 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 52dd0aa4..203512da 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/src/c_family_space_names_tests.rs b/src/c_family_space_names_tests.rs index 709cf86b..2f433ea8 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 68cbe213..2f9d604d 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 f728c894..73cbf3ea 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 4d50d301..0b570241 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 e9f0b6ac..0a2d5e03 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 b07149af..f7983815 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 64fe1f46..b77fd9d9 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 f0791229..94a311ca 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 39ff9b6b..3cece379 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 95b8c920..e7051b2c 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 b9f44393..f0f8f743 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 89dec441..ef35a832 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 8f4cb7e6..67f26815 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 69fdb3b0..c05153a0 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 bedfda0d..3c355a78 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 99faf8b7..940cd273 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 8a98bc03..cdc93b95 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 7a630968..608505e0 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 3d37533f..e3c62a6e 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 2166f15f..c9f8a9bc 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 44899ced..b3583772 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 8098e3b5..17b62e5c 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 bda28e0b..9446c92e 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 027edb9f..00ee0b41 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 0f9ec9a6..587ea544 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 a8a6ba4b..7ed7827e 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 8087df76..5d69764c 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 83b65720..af5a337d 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 80664c23..02ef30ca 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 9eb029de..a86715ca 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 42f75725..7603a094 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 cb725843..f114c16d 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 f1327d7b..559878c4 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 b77b3600..a7f605e1 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 79f1e622..3e96aafa 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 d2a4f31d..177f7b1f 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 d36d9ce5..2763033e 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 a20557ce..6999e667 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 a30fca96..0e3aa9d5 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 3efbb7af..11a5324e 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 c5c50d51..48993d36 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 896d3725..030db862 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 e0c4f9ca..c86c0afe 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 33184b0b..394a4a6f 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 ceb87aab..f6b15833 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 1266d977..6fbb0638 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 e1aceb7f..93197706 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 d264500e..5d96d3b8 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 bede0755..dc0b7762 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 fd080bde..deadcf3b 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 53316b5e..145827e4 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 07fcb7cf..80525cfe 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 21ddd857..4f52dcb7 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 ead80e97..c9d3af2c 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; From 3503b2cf0e10d5b4a333f165a0ee04b4b57b50f6 Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Wed, 16 Sep 2026 18:46:28 -0700 Subject: [PATCH 3/8] fix(features): check a union-gated mod nested in a test scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check-feature-gates.py` classified a `mod` as test-bearing only when it carried its own bare `cfg(test)`, so a union-gated `mod` inside an already-`#[cfg(test)]` parent was never checked at all. Latent while every subject was top-level, and about to stop being latent: gating the per-language tests creates exactly those nested gated modules. The naive repair breaks `subject_matcher`, which anchors on the bare name — admitting any `mod` makes a subject called `tests` match every `::tests::` path. Subjects now carry their full module path and match on that. Two smaller defects in the same scan, both found while testing it: a brace on a later line made a declaration read as a body and pass vacuously, and `splitlines()` disagreed with the line indices everywhere else in the file, raising `IndexError` on a trailing form feed. Fixes the third item of #1472. --- utils/check-feature-gates-test.py | 146 ++++++++++++++++++++++++ utils/check-feature-gates.py | 183 ++++++++++++++++++++++-------- 2 files changed, 281 insertions(+), 48 deletions(-) diff --git a/utils/check-feature-gates-test.py b/utils/check-feature-gates-test.py index cdd542ba..6ca3db4a 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 b46beb08..efa94385 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}(?:::|$)") # --------------------------------------------------------------------------- From 8a791e2833f9ec51310ec3df2ce429f830b365c3 Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Wed, 16 Sep 2026 18:46:28 -0700 Subject: [PATCH 4/8] ci(features): run the suite on each feature-matrix leg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legs ran `cargo check --all-targets` and nothing else, so the "minimal grammar set" guarantee from #252 was compile-only and a regression that shows up solely in a partial build could land unseen. They now run `cargo nextest run`, which is what makes the per-language gating regress-testable rather than a one-off pass. Three binaries are excluded by exact name: their fixtures live in the `tests/repositories/` submodules this job does not check out. Exact matchers, because `not binary(corpus)` is a substring match that excluded nothing from the CLI package — 1,412 tests before and after — and 19 of them read a real source file from the DeepSpeech corpus. The `lint` job also gains `check-test-lang-gates.py --compare` against the base branch head, the one direction the marker checks cannot reach. It reads the `gate-narrowing-intended` label **live** rather than from `github.event.pull_request.labels`: that payload is a snapshot from when the run was queued, so a label applied in response to the failing run is invisible to it, and a re-run replays the same stale copy — the documented remedy could never have taken effect. The job escalates to `pull-requests: read` for that, per this file's convention. Closes #1285. --- .github/workflows/ci.yml | 87 +++++++++++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02b75fbf..28a9098e 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 From 6b030fcf76853eca771efe024b408955346ea86b Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Wed, 16 Sep 2026 18:46:51 -0700 Subject: [PATCH 5/8] chore(features): wire the per-language gate into make and pre-commit `check-test-lang-gates` and its self-tests join `make pre-commit` and `make ci`, and the pre-commit hooks. Unlike `check-feature-gates`, both arms go in: this one is a pure source scan with no cargo invocation, so it costs about three seconds. `check-test-lang-gates-compare` is deliberately left out of both. It needs a base revision to mean anything and a working tree mid-edit has none, so CI supplies the PR's base; by hand it is `make check-test-lang-gates-compare COMPARE_REF=origin/main`. The variable is spelled out rather than a bare `REF`, which `?=` would pick up from an exported environment variable of that name. --- .pre-commit-config.yaml | 27 +++++++++++++++++ Makefile | 64 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df43e211..989acff3 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/Makefile b/Makefile index 5c4557f1..5308d75c 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 From baf4905a65c0dd2e1f9ba16d618b56e6a080b070 Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Wed, 16 Sep 2026 18:46:51 -0700 Subject: [PATCH 6/8] docs(features): document per-language test gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.claude/rules/testing.md` gains the per-test rule under the existing fixture-table section: what the derivation reads, the four shapes it cannot see and must be told about by marker, the two directions the gate checks, and `--compare` for the third. Also the trust boundaries, which matter more than the happy path — the sweep pin covers a parser named as a *type* and nothing else (#1480), and the `allow(unused_imports)` carve-out is scoped to partial builds so the full build still polices every import. `AGENTS.md` lists both gates and the out-of-band comparison; `CHANGELOG` records the change under Unreleased. It is test- and CI-only, so nothing for `STABILITY.md`. The counts are given as approximate on purpose. They move with the derivation rules themselves — every refinement that stops reading something as a use lowers them — and an earlier draft of this section quoted a figure taken before the comparison rules landed. --- .claude/rules/testing.md | 183 +++++++++++++++++++++++++++++++++++++++ AGENTS.md | 23 ++++- CHANGELOG.md | 49 +++++++++++ 3 files changed, 253 insertions(+), 2 deletions(-) diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index a0d1c9a6..8b3623b1 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/AGENTS.md b/AGENTS.md index 686f2c1c..d015c2a9 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 cc20a4b2..6dd0e461 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 From 1cdeb63e1d138756d77dc30a2db3330258695b39 Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Wed, 16 Sep 2026 18:46:51 -0700 Subject: [PATCH 7/8] docs(rules): a pgrep wait loop that matches itself never exits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pgrep -f` matches the full command line, so a loop passed as `-c` text — which is how the assistant's shell tool issues everything — finds its own argv: until ! pgrep -f 'make pre-commit' >/dev/null; do sleep 30; done Ten of these accumulated in one session, seven waiting on a gate that had written `BCA_GATE: pass` hours earlier. They match each other too, so killing one does not release the rest. Silent in the way the rest of this file is about: it reads as "still running", which is what the truth looks like until it isn't. Prefer a condition that is not a process — and bound it, because an unbounded wait on a job that dies is the same hang by another route. Where it must be a process, `[m]ake` breaks the self-match. `$$` does not: the command substitution and subshell inherit the argv too, so filtering one PID can never empty the list. Every snippet in the section was run before being written down, including one extracted back out of the committed file. The first draft was not, and both of its unrun examples were wrong. Unrelated to the rest of this branch; it is the tooling lesson from doing the work. --- .claude/rules/shell.md | 126 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/.claude/rules/shell.md b/.claude/rules/shell.md index 7f30b208..9a3617de 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. From 0c7a88b0dfd48590530379112ee2b4d2c1d64342 Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Fri, 18 Sep 2026 09:16:48 -0700 Subject: [PATCH 8/8] docs(lessons): extend lesson 6 to generated gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lesson 6 said to derive an assertion from an external source rather than from the code's own output, and framed it around snapshots. The same mechanism cost the most on #1478: `over_gated` checked ~3,200 generated markers against the derivation that generated them, so it could only catch hand edits; and a triage computed from that derivation certified 34 over-gated tests as legitimate. The Lesson paragraph now covers any expected value that is produced rather than asserted, and asks for a named oracle outside the model. The sub-example records which oracles actually found the defects — prior hand-written gates, the previous revision, a fresh-context review — and that the history comparison was the one check the narrowing label switched off. --- docs/development/lessons_learned.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/development/lessons_learned.md b/docs/development/lessons_learned.md index 072cd741..a0f1cd73 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