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