use syn::Item;
use syn::parse_file;
use syn::spanned::Spanned;
use crate::finding::section::Section;
use crate::finding::test_file_item::TestFileItem;
use crate::source_file::SourceFile;
pub struct TestFileParser;
impl TestFileParser {
pub fn parse(file: &SourceFile) -> Option<Vec<TestFileItem>> {
let syntax = parse_file(&file.contents()).ok()?;
Some(
syntax
.items
.iter()
.map(|item| Self::item(file, item))
.collect(),
)
}
fn item(file: &SourceFile, item: &Item) -> TestFileItem {
let span = item.span();
let first_line = Self::with_leading_comments(file, span.start().line);
TestFileItem::new(
Self::section(item),
Self::name(file, item, span.start().line),
first_line,
span.end().line,
)
}
fn section(item: &Item) -> Section {
match item {
Item::Use(_) => Section::Imports,
Item::Const(_) | Item::Static(_) => Section::Constants,
Item::Fn(function) if Self::is_test(&function.attrs) => Section::Tests,
_ => Section::Helpers,
}
}
fn is_test(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
attr.path()
.segments
.last()
.is_some_and(|segment| segment.ident == "test")
})
}
fn name(file: &SourceFile, item: &Item, start_line: usize) -> String {
match item {
Item::Const(inner) => inner.ident.to_string(),
Item::Enum(inner) => inner.ident.to_string(),
Item::Fn(inner) => inner.sig.ident.to_string(),
Item::Mod(inner) => inner.ident.to_string(),
Item::Static(inner) => inner.ident.to_string(),
Item::Struct(inner) => inner.ident.to_string(),
Item::Trait(inner) => inner.ident.to_string(),
Item::Type(inner) => inner.ident.to_string(),
Item::Impl(inner) => Self::type_name(&inner.self_ty),
_ => Self::source_line(file, start_line),
}
}
fn type_name(ty: &syn::Type) -> String {
match ty {
syn::Type::Path(path) => path
.path
.segments
.last()
.map_or_else(String::new, |segment| segment.ident.to_string()),
_ => String::new(),
}
}
fn source_line(file: &SourceFile, line: usize) -> String {
file.lines()
.get(line.saturating_sub(1))
.map_or_else(String::new, |text| text.trim().to_string())
}
fn with_leading_comments(file: &SourceFile, start_line: usize) -> usize {
let mut first = start_line;
while first > 1 && Self::is_comment(file, first - 1) {
first -= 1;
}
first
}
fn is_comment(file: &SourceFile, line: usize) -> bool {
file.lines()
.get(line.saturating_sub(1))
.is_some_and(|text| text.trim_start().starts_with("//"))
}
}