stern4rust/
test_file_parser.rs1use syn::Item;
6use syn::parse_file;
7use syn::spanned::Spanned;
8
9use crate::section::Section;
10use crate::source_file::SourceFile;
11use crate::test_file_item::TestFileItem;
12
13pub struct TestFileParser;
21
22impl TestFileParser {
23 pub fn parse(file: &SourceFile) -> Option<Vec<TestFileItem>> {
27 let syntax = parse_file(&file.contents()).ok()?;
28 Some(
29 syntax
30 .items
31 .iter()
32 .map(|item| Self::item(file, item))
33 .collect(),
34 )
35 }
36
37 fn item(file: &SourceFile, item: &Item) -> TestFileItem {
38 let span = item.span();
39 let first_line = Self::with_leading_comments(file, span.start().line);
40 TestFileItem::new(
41 Self::section(item),
42 Self::name(file, item, span.start().line),
43 first_line,
44 span.end().line,
45 )
46 }
47
48 fn section(item: &Item) -> Section {
49 match item {
50 Item::Use(_) => Section::Imports,
51 Item::Const(_) | Item::Static(_) => Section::Constants,
52 Item::Fn(function) if Self::is_test(&function.attrs) => Section::Tests,
53 _ => Section::Helpers,
54 }
55 }
56
57 fn is_test(attrs: &[syn::Attribute]) -> bool {
61 attrs.iter().any(|attr| {
62 attr.path()
63 .segments
64 .last()
65 .is_some_and(|segment| segment.ident == "test")
66 })
67 }
68
69 fn name(file: &SourceFile, item: &Item, start_line: usize) -> String {
70 match item {
71 Item::Const(inner) => inner.ident.to_string(),
72 Item::Enum(inner) => inner.ident.to_string(),
73 Item::Fn(inner) => inner.sig.ident.to_string(),
74 Item::Mod(inner) => inner.ident.to_string(),
75 Item::Static(inner) => inner.ident.to_string(),
76 Item::Struct(inner) => inner.ident.to_string(),
77 Item::Trait(inner) => inner.ident.to_string(),
78 Item::Type(inner) => inner.ident.to_string(),
79 Item::Impl(inner) => Self::type_name(&inner.self_ty),
83 _ => Self::source_line(file, start_line),
84 }
85 }
86
87 fn type_name(ty: &syn::Type) -> String {
88 match ty {
89 syn::Type::Path(path) => path
90 .path
91 .segments
92 .last()
93 .map_or_else(String::new, |segment| segment.ident.to_string()),
94 _ => String::new(),
95 }
96 }
97
98 fn source_line(file: &SourceFile, line: usize) -> String {
101 file.lines()
102 .get(line.saturating_sub(1))
103 .map_or_else(String::new, |text| text.trim().to_string())
104 }
105
106 fn with_leading_comments(file: &SourceFile, start_line: usize) -> usize {
107 let mut first = start_line;
108 while first > 1 && Self::is_comment(file, first - 1) {
109 first -= 1;
110 }
111 first
112 }
113
114 fn is_comment(file: &SourceFile, line: usize) -> bool {
115 file.lines()
116 .get(line.saturating_sub(1))
117 .is_some_and(|text| text.trim_start().starts_with("//"))
118 }
119}