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