stern4rust/finding/parsing/
test_file_parser.rs1use syn::Item;
6use syn::parse_file;
7use syn::spanned::Spanned;
8
9use crate::finding::model::section::Section;
10use crate::finding::model::test_file_item::TestFileItem;
11use crate::finding::parsing::item_naming::ItemNaming;
12use crate::source_file::SourceFile;
13use syn::Attribute;
14use syn::Type;
15
16pub struct TestFileParser;
24
25impl TestFileParser {
26 pub fn parse(file: &SourceFile) -> Option<Vec<TestFileItem>> {
30 let syntax = parse_file(&file.contents()).ok()?;
31 Some(
32 syntax
33 .items
34 .iter()
35 .map(|item| Self::item(file, item))
36 .collect(),
37 )
38 }
39
40 fn item(file: &SourceFile, item: &Item) -> TestFileItem {
41 let span = item.span();
42 let first_line = Self::with_leading_comments(file, span.start().line);
43 TestFileItem::new(
44 Self::section(item),
45 Self::name(file, item, span.start().line),
46 first_line,
47 span.end().line,
48 )
49 }
50
51 fn section(item: &Item) -> Section {
52 match item {
53 Item::Use(_) => Section::Imports,
54 Item::Const(_) | Item::Static(_) => Section::Constants,
55 Item::Fn(function) if Self::is_test(&function.attrs) => Section::Tests,
56 _ => Section::Helpers,
57 }
58 }
59
60 fn is_test(attrs: &[Attribute]) -> bool {
64 attrs.iter().any(|attr| {
65 attr.path()
66 .segments
67 .last()
68 .is_some_and(|segment| segment.ident == "test")
69 })
70 }
71
72 fn name(file: &SourceFile, item: &Item, start_line: usize) -> String {
73 if let Item::Impl(inner) = item {
77 return Self::type_name(&inner.self_ty);
78 }
79 ItemNaming::identifier(item).unwrap_or_else(|| ItemNaming::source_line(file, start_line))
80 }
81
82 fn type_name(ty: &Type) -> String {
83 match ty {
84 Type::Path(path) => path
85 .path
86 .segments
87 .last()
88 .map_or_else(String::new, |segment| segment.ident.to_string()),
89 _ => String::new(),
90 }
91 }
92
93 fn with_leading_comments(file: &SourceFile, start_line: usize) -> usize {
97 let mut first = start_line;
98 while first > 1 && Self::is_comment(file, first - 1) {
99 first -= 1;
100 }
101 first
102 }
103
104 fn is_comment(file: &SourceFile, line: usize) -> bool {
105 file.lines()
106 .get(line.saturating_sub(1))
107 .is_some_and(|text| text.trim_start().starts_with("//"))
108 }
109}