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;
13
14pub struct TestFileParser;
22
23impl TestFileParser {
24 pub fn parse(file: &SourceFile) -> Option<Vec<TestFileItem>> {
28 let syntax = parse_file(&file.contents()).ok()?;
29 Some(
30 syntax
31 .items
32 .iter()
33 .map(|item| Self::item(file, item))
34 .collect(),
35 )
36 }
37
38 fn item(file: &SourceFile, item: &Item) -> TestFileItem {
39 let span = item.span();
40 let first_line = Self::with_leading_comments(file, span.start().line);
41 TestFileItem::new(
42 Self::section(item),
43 Self::name(file, item, span.start().line),
44 first_line,
45 span.end().line,
46 )
47 }
48
49 fn section(item: &Item) -> Section {
50 match item {
51 Item::Use(_) => Section::Imports,
52 Item::Const(_) | Item::Static(_) => Section::Constants,
53 Item::Fn(function) if Self::is_test(&function.attrs) => Section::Tests,
54 _ => Section::Helpers,
55 }
56 }
57
58 fn is_test(attrs: &[syn::Attribute]) -> bool {
62 attrs.iter().any(|attr| {
63 attr.path()
64 .segments
65 .last()
66 .is_some_and(|segment| segment.ident == "test")
67 })
68 }
69
70 fn name(file: &SourceFile, item: &Item, start_line: usize) -> String {
71 if let Item::Impl(inner) = item {
75 return Self::type_name(&inner.self_ty);
76 }
77 ItemNaming::identifier(item).unwrap_or_else(|| ItemNaming::source_line(file, start_line))
78 }
79
80 fn type_name(ty: &syn::Type) -> String {
81 match ty {
82 syn::Type::Path(path) => path
83 .path
84 .segments
85 .last()
86 .map_or_else(String::new, |segment| segment.ident.to_string()),
87 _ => String::new(),
88 }
89 }
90
91 fn with_leading_comments(file: &SourceFile, start_line: usize) -> usize {
95 let mut first = start_line;
96 while first > 1 && Self::is_comment(file, first - 1) {
97 first -= 1;
98 }
99 first
100 }
101
102 fn is_comment(file: &SourceFile, line: usize) -> bool {
103 file.lines()
104 .get(line.saturating_sub(1))
105 .is_some_and(|text| text.trim_start().starts_with("//"))
106 }
107}