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