stern4rust/finding/
unit_test_finder.rs1use proc_macro2::TokenTree;
6use quote::ToTokens;
7use syn::Attribute;
8use syn::Item;
9use syn::parse_file;
10use syn::spanned::Spanned;
11
12use crate::finding::unit_test_site::UnitTestSite;
13use crate::source_file::SourceFile;
14
15pub struct UnitTestFinder;
28
29impl UnitTestFinder {
30 pub fn sites(file: &SourceFile) -> Option<Vec<UnitTestSite>> {
32 let syntax = parse_file(&file.contents()).ok()?;
33 Some(Self::in_items(file, &syntax.items))
34 }
35
36 fn in_items(file: &SourceFile, items: &[Item]) -> Vec<UnitTestSite> {
41 let mut sites = Vec::new();
42 for item in items {
43 if let Some(site) = Self::site_of(file, item) {
44 sites.push(site);
45 continue;
46 }
47 if let Item::Mod(module) = item {
48 if let Some((_, inner)) = &module.content {
49 sites.extend(Self::in_items(file, inner));
50 }
51 }
52 }
53 sites
54 }
55
56 fn site_of(file: &SourceFile, item: &Item) -> Option<UnitTestSite> {
57 let line = item.span().start().line.max(1);
58 let attrs = Self::attributes(item);
59 let subject = Self::describe(file, item, line);
60 let mirror = Self::mirror_of(file.relative_path());
61
62 if attrs.iter().any(Self::is_cfg_attr_test) {
63 return Some(UnitTestSite::new(
64 line,
65 &format!("the `#[cfg_attr(test, ...)]` on the {subject}"),
66 &format!(
67 "apply the attribute unconditionally, or move what it guards into {mirror}"
68 ),
69 ));
70 }
71 if attrs.iter().any(Self::is_cfg_test) {
72 return Some(UnitTestSite::new(
73 line,
74 &format!("the `#[cfg(test)]` {subject}"),
75 &format!("move the tests to {mirror} and delete this from the source tree"),
76 ));
77 }
78 if attrs.iter().any(Self::is_test) {
79 return Some(UnitTestSite::new(
80 line,
81 &format!("the test {subject}"),
82 &format!("move the tests to {mirror} and delete this from the source tree"),
83 ));
84 }
85 None
86 }
87
88 fn is_cfg_attr_test(attr: &Attribute) -> bool {
93 attr.path().is_ident("cfg_attr") && Self::mentions_test(attr)
94 }
95
96 fn is_cfg_test(attr: &Attribute) -> bool {
99 attr.path().is_ident("cfg") && Self::mentions_test(attr)
100 }
101
102 fn is_test(attr: &Attribute) -> bool {
104 attr.path()
105 .segments
106 .last()
107 .is_some_and(|segment| segment.ident == "test")
108 }
109
110 fn mentions_test(attr: &Attribute) -> bool {
114 Self::has_test_ident(attr.meta.to_token_stream())
115 }
116
117 fn has_test_ident(stream: proc_macro2::TokenStream) -> bool {
118 stream.into_iter().any(|tree| match tree {
119 TokenTree::Ident(ident) => ident == "test",
120 TokenTree::Group(group) => Self::has_test_ident(group.stream()),
121 _ => false,
122 })
123 }
124
125 fn mirror_of(relative_path: &str) -> String {
129 let without_root = relative_path.strip_prefix("src/").unwrap_or(relative_path);
130 let stem = without_root.strip_suffix(".rs").unwrap_or(without_root);
131 format!("tests/{stem}_tests.rs")
132 }
133
134 fn describe(file: &SourceFile, item: &Item, line: usize) -> String {
135 match item {
136 Item::Const(inner) => format!("constant `{}`", inner.ident),
137 Item::Enum(inner) => format!("enum `{}`", inner.ident),
138 Item::Fn(inner) => format!("function `{}`", inner.sig.ident),
139 Item::Mod(inner) => format!("module `{}`", inner.ident),
140 Item::Static(inner) => format!("static `{}`", inner.ident),
141 Item::Struct(inner) => format!("struct `{}`", inner.ident),
142 Item::Trait(inner) => format!("trait `{}`", inner.ident),
143 Item::Type(inner) => format!("type alias `{}`", inner.ident),
144 _ => format!("`{}`", Self::source_line(file, line)),
145 }
146 }
147
148 fn source_line(file: &SourceFile, line: usize) -> String {
149 file.lines()
150 .get(line.saturating_sub(1))
151 .map_or_else(String::new, |text| text.trim().to_string())
152 }
153
154 fn attributes(item: &Item) -> &[Attribute] {
155 match item {
156 Item::Const(inner) => &inner.attrs,
157 Item::Enum(inner) => &inner.attrs,
158 Item::ExternCrate(inner) => &inner.attrs,
159 Item::Fn(inner) => &inner.attrs,
160 Item::ForeignMod(inner) => &inner.attrs,
161 Item::Impl(inner) => &inner.attrs,
162 Item::Macro(inner) => &inner.attrs,
163 Item::Mod(inner) => &inner.attrs,
164 Item::Static(inner) => &inner.attrs,
165 Item::Struct(inner) => &inner.attrs,
166 Item::Trait(inner) => &inner.attrs,
167 Item::TraitAlias(inner) => &inner.attrs,
168 Item::Type(inner) => &inner.attrs,
169 Item::Union(inner) => &inner.attrs,
170 Item::Use(inner) => &inner.attrs,
171 _ => &[],
172 }
173 }
174}