use alloy_primitives::map::HashMap;
use derive_more::{derive::Display, Deref, DerefMut};
use solang_parser::doccomment::DocCommentTag;
#[derive(Clone, Debug, Display, PartialEq, Eq)]
pub enum CommentTag {
Title,
Author,
Notice,
Dev,
Param,
Return,
Inheritdoc,
Custom(String),
}
impl CommentTag {
fn from_str(s: &str) -> Option<Self> {
let trimmed = s.trim();
let tag = match trimmed {
"title" => Self::Title,
"author" => Self::Author,
"notice" => Self::Notice,
"dev" => Self::Dev,
"param" => Self::Param,
"return" => Self::Return,
"inheritdoc" => Self::Inheritdoc,
_ if trimmed.starts_with("custom:") => {
let custom_tag = trimmed.trim_start_matches("custom:").trim();
match custom_tag {
"param" => Self::Param,
_ => Self::Custom(custom_tag.to_owned()),
}
}
_ => {
warn!(target: "forge::doc", tag=trimmed, "unknown comment tag. custom tags must be preceded by `custom:`");
return None
}
};
Some(tag)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Comment {
pub tag: CommentTag,
pub value: String,
}
impl Comment {
pub fn new(tag: CommentTag, value: String) -> Self {
Self { tag, value }
}
pub fn from_doc_comment(value: DocCommentTag) -> Option<Self> {
CommentTag::from_str(&value.tag).map(|tag| Self { tag, value: value.value })
}
pub fn split_first_word(&self) -> Option<(&str, &str)> {
self.value.trim_start().split_once(' ')
}
pub fn match_first_word(&self, expected: &str) -> Option<&str> {
self.split_first_word().and_then(
|(word, rest)| {
if word == expected {
Some(rest)
} else {
None
}
},
)
}
pub fn is_custom(&self) -> bool {
matches!(self.tag, CommentTag::Custom(_))
}
}
#[derive(Clone, Debug, Default, PartialEq, Deref, DerefMut)]
pub struct Comments(Vec<Comment>);
macro_rules! ref_fn {
($vis:vis fn $name:ident(&self$(, )?$($arg_name:ident: $arg:ty),*) -> $ret:ty) => {
$vis fn $name(&self, $($arg_name: $arg),*) -> $ret {
CommentsRef::from(self).$name($($arg_name),*)
}
};
}
impl Comments {
ref_fn!(pub fn include_tag(&self, tag: CommentTag) -> CommentsRef<'_>);
ref_fn!(pub fn include_tags(&self, tags: &[CommentTag]) -> CommentsRef<'_>);
ref_fn!(pub fn exclude_tags(&self, tags: &[CommentTag]) -> CommentsRef<'_>);
ref_fn!(pub fn contains_tag(&self, tag: &Comment) -> bool);
ref_fn!(pub fn find_inheritdoc_base(&self) -> Option<&'_ str>);
pub fn merge_inheritdoc(
&self,
ident: &str,
inheritdocs: Option<HashMap<String, Self>>,
) -> Self {
let mut result = Self(Vec::from_iter(self.iter().cloned()));
if let (Some(inheritdocs), Some(base)) = (inheritdocs, self.find_inheritdoc_base()) {
let key = format!("{base}.{ident}");
if let Some(other) = inheritdocs.get(&key) {
for comment in other.iter() {
if !result.contains_tag(comment) {
result.push(comment.clone());
}
}
}
}
result
}
}
impl From<Vec<DocCommentTag>> for Comments {
fn from(value: Vec<DocCommentTag>) -> Self {
Self(value.into_iter().flat_map(Comment::from_doc_comment).collect())
}
}
#[derive(Debug, Default, PartialEq, Deref)]
pub struct CommentsRef<'a>(Vec<&'a Comment>);
impl<'a> CommentsRef<'a> {
pub fn include_tag(&self, tag: CommentTag) -> Self {
self.include_tags(&[tag])
}
pub fn include_tags(&self, tags: &[CommentTag]) -> Self {
CommentsRef(self.iter().cloned().filter(|c| tags.contains(&c.tag)).collect())
}
pub fn exclude_tags(&self, tags: &[CommentTag]) -> Self {
CommentsRef(self.iter().cloned().filter(|c| !tags.contains(&c.tag)).collect())
}
pub fn contains_tag(&self, target: &Comment) -> bool {
self.iter().any(|c| match (&c.tag, &target.tag) {
(CommentTag::Inheritdoc, CommentTag::Inheritdoc) => c.value == target.value,
(CommentTag::Param, CommentTag::Param) | (CommentTag::Return, CommentTag::Return) => {
c.split_first_word().map(|(name, _)| name) ==
target.split_first_word().map(|(name, _)| name)
}
(tag1, tag2) => tag1 == tag2,
})
}
fn find_inheritdoc_base(&self) -> Option<&'a str> {
self.iter()
.find(|c| matches!(c.tag, CommentTag::Inheritdoc))
.and_then(|c| c.value.split_whitespace().next())
}
pub fn get_custom_tags(&self) -> Self {
CommentsRef(self.iter().cloned().filter(|c| c.is_custom()).collect())
}
}
impl<'a> From<&'a Comments> for CommentsRef<'a> {
fn from(value: &'a Comments) -> Self {
Self(value.iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_comment_tag() {
assert_eq!(CommentTag::from_str("title"), Some(CommentTag::Title));
assert_eq!(CommentTag::from_str(" title "), Some(CommentTag::Title));
assert_eq!(CommentTag::from_str("author"), Some(CommentTag::Author));
assert_eq!(CommentTag::from_str("notice"), Some(CommentTag::Notice));
assert_eq!(CommentTag::from_str("dev"), Some(CommentTag::Dev));
assert_eq!(CommentTag::from_str("param"), Some(CommentTag::Param));
assert_eq!(CommentTag::from_str("return"), Some(CommentTag::Return));
assert_eq!(CommentTag::from_str("inheritdoc"), Some(CommentTag::Inheritdoc));
assert_eq!(CommentTag::from_str("custom:"), Some(CommentTag::Custom(String::new())));
assert_eq!(
CommentTag::from_str("custom:some"),
Some(CommentTag::Custom("some".to_owned()))
);
assert_eq!(
CommentTag::from_str(" custom: some "),
Some(CommentTag::Custom("some".to_owned()))
);
assert_eq!(CommentTag::from_str(""), None);
assert_eq!(CommentTag::from_str("custom"), None);
assert_eq!(CommentTag::from_str("sometag"), None);
}
#[test]
fn test_is_custom() {
let custom_comment = Comment::new(
CommentTag::from_str("custom:test").unwrap(),
"dummy custom tag".to_owned(),
);
assert!(custom_comment.is_custom(), "Custom tag should return true for is_custom");
let non_custom_tags = [
CommentTag::Title,
CommentTag::Author,
CommentTag::Notice,
CommentTag::Dev,
CommentTag::Param,
CommentTag::Return,
CommentTag::Inheritdoc,
];
for tag in non_custom_tags {
let comment = Comment::new(tag.clone(), "Non-custom comment".to_string());
assert!(
!comment.is_custom(),
"Non-custom tag {tag:?} should return false for is_custom"
);
}
}
}