use std::{
io::{BufRead as _, BufReader, Read},
path::{Path, PathBuf},
sync::LazyLock,
};
use regex::Regex;
use crate::tag::{Tag, TagKind};
#[derive(Debug)]
pub enum SourceKind {
Rust,
CLike,
}
impl SourceKind {
pub fn identify(path: &Path) -> Option<Self> {
let ext = path.extension()?;
match ext.to_str()? {
"rs" => Some(Self::Rust),
"py" => None,
_ => Some(Self::CLike),
}
}
}
pub struct SourceFile<R: Read> {
path: PathBuf,
kind: SourceKind,
inner: BufReader<R>,
line: String,
line_number: usize,
}
impl<R: Read> SourceFile<R> {
pub fn new(kind: SourceKind, path: &Path, reader: R) -> Self {
Self {
path: path.to_owned(),
kind,
inner: BufReader::new(reader),
line: String::new(),
line_number: 0,
}
}
fn next_rust(&mut self) -> Option<Tag> {
loop {
if let Some(tag) = self.find_rust_todo_macro() {
self.line.clear();
return Some(tag);
}
if let Some(tag) = self.find_clike_comment() {
self.line.clear();
return Some(tag);
}
self.line.clear();
let n = self
.inner
.read_line(&mut self.line)
.expect("read line failed");
if n == 0 {
return None;
}
self.line_number += 1;
}
}
fn next_clike(&mut self) -> Option<Tag> {
loop {
self.line.clear();
let n = self.inner.read_line(&mut self.line).ok()?;
if n == 0 {
return None;
}
self.line_number += 1;
if let Some(tag) = self.find_clike_comment() {
return Some(tag);
}
}
}
}
static CLIKE_COMMENT_TAG_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"/(?:/+|\*+)!? ?(?P<tag>[!a-zA-Z0-9_]+): ?(?P<msg>[^:/].+)")
.expect("could not compile clike comment regex")
});
static RUST_TODO_MACRO: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"todo!\((?:"([^"]*)")?\)"#).expect("could not compile rust todo macro regex")
});
impl<R: Read> SourceFile<R> {
fn find_rust_todo_macro(&self) -> Option<Tag> {
let caps = RUST_TODO_MACRO.captures(&self.line)?;
let message = caps
.get(1)
.map(|x| x.as_str().to_owned())
.unwrap_or_default();
Some(Tag {
kind: TagKind::TodoMacro,
line: self.line_number,
path: self.path.clone(),
message,
git_info: None,
})
}
fn find_clike_comment(&self) -> Option<Tag> {
let caps = CLIKE_COMMENT_TAG_REGEX.captures(&self.line)?;
let raw_tag = caps.get(1)?.as_str();
let kind = TagKind::new(raw_tag);
let mut message = caps.get(2)?.as_str().trim();
if message.ends_with("*/") {
message = message[..message.len() - 2].trim();
}
Some(Tag {
kind,
line: self.line_number,
path: self.path.clone(),
message: message.to_owned(),
git_info: None,
})
}
}
impl<R: Read> Iterator for SourceFile<R> {
type Item = Tag;
fn next(&mut self) -> Option<Self::Item> {
match self.kind {
SourceKind::Rust => self.next_rust(),
SourceKind::CLike => self.next_clike(),
}
}
}
impl<R: Read> std::fmt::Debug for SourceFile<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}: {}", self.kind, self.path.display())
}
}