stern4rust/rules/testing/
tested_public_api_rule.rs1use std::collections::BTreeSet;
6
7use crate::finding::model::public_entry_point::PublicEntryPoint;
8use crate::finding::parsing::call_site_finder::CallSiteFinder;
9use crate::finding::parsing::public_entry_point_finder::PublicEntryPointFinder;
10use crate::reporting::offence::Offence;
11use crate::rule::Rule;
12use crate::source_file::SourceFile;
13
14pub struct TestedPublicApiRule;
37
38impl TestedPublicApiRule {
39 pub const SOURCE_ROOT: &'static str = "src/";
40 pub const TESTS_ROOT: &'static str = "tests/";
41
42 pub fn new() -> Self {
43 Self
44 }
45
46 fn declared_in(files: &[SourceFile]) -> Vec<(String, PublicEntryPoint)> {
47 files
48 .iter()
49 .filter(|file| file.relative_path().starts_with(Self::SOURCE_ROOT))
50 .flat_map(|file| {
51 PublicEntryPointFinder::find(file)
52 .unwrap_or_default()
53 .into_iter()
54 .map(|entry| (file.relative_path().to_string(), entry))
55 })
56 .collect()
57 }
58
59 fn called_in(files: &[SourceFile]) -> BTreeSet<PublicEntryPoint> {
60 files
61 .iter()
62 .filter(|file| file.relative_path().starts_with(Self::TESTS_ROOT))
63 .filter_map(CallSiteFinder::find)
64 .flatten()
65 .collect()
66 }
67
68 fn offence(&self, file: &str, entry: &PublicEntryPoint) -> Offence {
69 Offence::new(
70 file,
71 1,
72 self.name(),
73 format!(
74 "`{}` is public but no test calls it with {} argument(s)",
75 entry.name, entry.arity
76 ),
77 format!(
78 "call `{}` from a test, or stop exposing it if nothing outside needs it",
79 entry.name
80 ),
81 )
82 .with_subject(&entry.signature())
83 }
84}
85
86impl Default for TestedPublicApiRule {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl Rule for TestedPublicApiRule {
93 fn name(&self) -> &'static str {
94 "tested-public-api"
95 }
96
97 fn check(&self, _file: &SourceFile) -> Vec<Offence> {
98 Vec::new()
99 }
100
101 fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence> {
104 let called = Self::called_in(files);
105 Self::declared_in(files)
106 .iter()
107 .filter(|(_, entry)| !called.contains(entry))
108 .map(|(file, entry)| self.offence(file, entry))
109 .collect()
110 }
111
112 fn requirement(&self) -> Option<&'static str> {
113 None
114 }
115
116 fn is_configured(&self) -> bool {
117 true
118 }
119}