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::reporting::rule_explanation::RuleExplanation;
12use crate::rule::Rule;
13use crate::source_file::SourceFile;
14
15pub struct TestedPublicApiRule;
38
39impl TestedPublicApiRule {
40 pub const SOURCE_ROOT: &'static str = "src/";
41 pub const TESTS_ROOT: &'static str = "tests/";
42
43 pub fn new() -> Self {
44 Self
45 }
46
47 fn declared_in(files: &[SourceFile]) -> Vec<(String, PublicEntryPoint)> {
48 files
49 .iter()
50 .filter(|file| file.relative_path().starts_with(Self::SOURCE_ROOT))
51 .flat_map(|file| {
52 PublicEntryPointFinder::find(file)
53 .unwrap_or_default()
54 .into_iter()
55 .map(|entry| (file.relative_path().to_string(), entry))
56 })
57 .collect()
58 }
59
60 fn called_in(files: &[SourceFile]) -> BTreeSet<PublicEntryPoint> {
61 files
62 .iter()
63 .filter(|file| file.relative_path().starts_with(Self::TESTS_ROOT))
64 .filter_map(CallSiteFinder::find)
65 .flatten()
66 .collect()
67 }
68
69 fn offence(&self, file: &str, entry: &PublicEntryPoint) -> Offence {
70 Offence::new(
71 file,
72 1,
73 self.name(),
74 format!(
75 "`{}` is public but no test calls it with {} argument(s)",
76 entry.name, entry.arity
77 ),
78 format!(
79 "call `{}` from a test, or stop exposing it if nothing outside needs it",
80 entry.name
81 ),
82 )
83 .with_subject(&entry.signature())
84 }
85}
86
87impl Default for TestedPublicApiRule {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl Rule for TestedPublicApiRule {
94 fn name(&self) -> &'static str {
95 "tested-public-api"
96 }
97
98 fn check(&self, _file: &SourceFile) -> Vec<Offence> {
99 Vec::new()
100 }
101
102 fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence> {
105 let called = Self::called_in(files);
106 Self::declared_in(files)
107 .iter()
108 .filter(|(_, entry)| !called.contains(entry))
109 .map(|(file, entry)| self.offence(file, entry))
110 .collect()
111 }
112
113 fn requirement(&self) -> Option<&'static str> {
114 None
115 }
116
117 fn is_configured(&self) -> bool {
118 true
119 }
120
121 fn explanation(&self) -> RuleExplanation {
122 RuleExplanation::new(
123 self.name(),
124 "Every public entry point is called by at least one test.",
125 "pub fn commit(&self) -> bool -- no test calls it",
126 "#[test]\nfn commit_without_a_quorum_returns_false() {\n assert!(!store.commit());\n}",
127 )
128 }
129}