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}