Skip to main content

stern4rust/rules/source/
test_free_source_rule.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use crate::finding::parsing::unit_test_finder::UnitTestFinder;
6use crate::reporting::offence::Offence;
7use crate::rule::Rule;
8use crate::source_file::SourceFile;
9
10// Tests live in tests/, and the production source tree carries none of them.
11//
12// A unit test inside src/ is a test nobody can find from the outside. It does
13// not appear in the mirrored test file twin4rust checks for, it is not declared
14// from all_tests.rs, and it is compiled under a configuration the shipped build
15// never uses -- so the file reads as covered while the coverage lives somewhere
16// nothing else in the toolchain looks.
17//
18// `#[cfg_attr(test, ...)]` is the same door under a different name: a type
19// carrying a derive only under test is a type that means one thing to the tests
20// and another to the shipped build. Only the test-gated spelling is forbidden --
21// `#[cfg_attr(feature = "serde", ...)]` is ordinary library work, and so is
22// `#[cfg(feature = "...")]`, because both gate on something the shipped build
23// can also select.
24pub struct TestFreeSourceRule;
25
26impl TestFreeSourceRule {
27    pub const ROOT: &'static str = "tests/";
28
29    pub fn new() -> Self {
30        Self
31    }
32
33    // tests/ is exempt, and not as a concession. A #[test] under tests/ is the
34    // entire point of tests/, and a rule that reported it would report every
35    // test in the workspace.
36    fn applies_to(file: &SourceFile) -> bool {
37        !file.relative_path().starts_with(Self::ROOT)
38    }
39}
40
41impl Default for TestFreeSourceRule {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl Rule for TestFreeSourceRule {
48    fn name(&self) -> &'static str {
49        "test-free-source"
50    }
51
52    fn check(&self, file: &SourceFile) -> Vec<Offence> {
53        if !Self::applies_to(file) {
54            return Vec::new();
55        }
56        UnitTestFinder::sites(file)
57            .unwrap_or_default()
58            .into_iter()
59            .map(|site| {
60                Offence::new(
61                    file.relative_path(),
62                    site.line,
63                    self.name(),
64                    format!("{} does not belong in the source tree", site.label),
65                    site.correction.clone(),
66                )
67                .with_subject(&site.label)
68            })
69            .collect()
70    }
71
72    fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
73        Vec::new()
74    }
75
76    fn requirement(&self) -> Option<&'static str> {
77        None
78    }
79
80    fn is_configured(&self) -> bool {
81        true
82    }
83}