1use alloy_primitives::map::HashMap;
2use derive_more::{Deref, DerefMut, derive::Display};
3use solang_parser::doccomment::DocCommentTag;
4
5#[derive(Clone, Debug, Display, PartialEq, Eq)]
8pub enum CommentTag {
9 Title,
11 Author,
13 Notice,
15 Dev,
17 Param,
19 Return,
21 Inheritdoc,
23 Custom(String),
25}
26
27impl CommentTag {
28 fn from_str(s: &str) -> Option<Self> {
29 let trimmed = s.trim();
30 let tag = match trimmed {
31 "title" => Self::Title,
32 "author" => Self::Author,
33 "notice" => Self::Notice,
34 "dev" => Self::Dev,
35 "param" => Self::Param,
36 "return" => Self::Return,
37 "inheritdoc" => Self::Inheritdoc,
38 _ if trimmed.starts_with("custom:") => {
39 let custom_tag = trimmed.trim_start_matches("custom:").trim();
42 match custom_tag {
43 "param" => Self::Param,
44 _ => Self::Custom(custom_tag.to_owned()),
45 }
46 }
47 _ => {
48 warn!(target: "forge::doc", tag=trimmed, "unknown comment tag. custom tags must be preceded by `custom:`");
49 return None;
50 }
51 };
52 Some(tag)
53 }
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct Comment {
61 pub tag: CommentTag,
63 pub value: String,
65}
66
67impl Comment {
68 pub fn new(tag: CommentTag, value: String) -> Self {
70 Self { tag, value }
71 }
72
73 pub fn from_doc_comment(value: DocCommentTag) -> Option<Self> {
76 CommentTag::from_str(&value.tag).map(|tag| Self { tag, value: value.value })
77 }
78
79 pub fn split_first_word(&self) -> Option<(&str, &str)> {
82 self.value.trim_start().split_once(' ')
83 }
84
85 pub fn match_first_word(&self, expected: &str) -> Option<&str> {
89 self.split_first_word().and_then(|(word, rest)| (word == expected).then_some(rest))
90 }
91
92 pub fn is_custom(&self) -> bool {
94 matches!(self.tag, CommentTag::Custom(_))
95 }
96}
97
98#[derive(Clone, Debug, Default, PartialEq, Deref, DerefMut)]
100pub struct Comments(Vec<Comment>);
101
102macro_rules! ref_fn {
105 ($vis:vis fn $name:ident(&self$(, )?$($arg_name:ident: $arg:ty),*) -> $ret:ty) => {
106 $vis fn $name(&self, $($arg_name: $arg),*) -> $ret {
108 CommentsRef::from(self).$name($($arg_name),*)
109 }
110 };
111}
112
113impl Comments {
114 ref_fn!(pub fn include_tag(&self, tag: CommentTag) -> CommentsRef<'_>);
115 ref_fn!(pub fn include_tags(&self, tags: &[CommentTag]) -> CommentsRef<'_>);
116 ref_fn!(pub fn exclude_tags(&self, tags: &[CommentTag]) -> CommentsRef<'_>);
117 ref_fn!(pub fn contains_tag(&self, tag: &Comment) -> bool);
118 ref_fn!(pub fn find_inheritdoc_base(&self) -> Option<&'_ str>);
119
120 pub fn merge_inheritdoc(
127 &self,
128 ident: &str,
129 inheritdocs: Option<HashMap<String, Self>>,
130 ) -> Self {
131 let mut result = self.clone();
132
133 if let (Some(inheritdocs), Some(base)) = (inheritdocs, self.find_inheritdoc_base()) {
134 let key = format!("{base}.{ident}");
135 if let Some(other) = inheritdocs.get(&key) {
136 for comment in other.iter() {
137 if !result.contains_tag(comment) {
138 result.push(comment.clone());
139 }
140 }
141 }
142 }
143
144 result
145 }
146}
147
148impl From<Vec<DocCommentTag>> for Comments {
149 fn from(value: Vec<DocCommentTag>) -> Self {
150 Self(value.into_iter().flat_map(Comment::from_doc_comment).collect())
151 }
152}
153
154#[derive(Debug, Default, PartialEq, Deref)]
156pub struct CommentsRef<'a>(Vec<&'a Comment>);
157
158impl<'a> CommentsRef<'a> {
159 pub fn include_tag(&self, tag: CommentTag) -> Self {
161 self.include_tags(&[tag])
162 }
163
164 pub fn include_tags(&self, tags: &[CommentTag]) -> Self {
166 CommentsRef(self.iter().copied().filter(|c| tags.contains(&c.tag)).collect())
168 }
169
170 pub fn exclude_tags(&self, tags: &[CommentTag]) -> Self {
172 CommentsRef(self.iter().copied().filter(|c| !tags.contains(&c.tag)).collect())
174 }
175
176 pub fn contains_tag(&self, target: &Comment) -> bool {
178 self.iter().any(|c| match (&c.tag, &target.tag) {
179 (CommentTag::Inheritdoc, CommentTag::Inheritdoc) => c.value == target.value,
180 (CommentTag::Param, CommentTag::Param) | (CommentTag::Return, CommentTag::Return) => {
181 c.split_first_word().map(|(name, _)| name)
182 == target.split_first_word().map(|(name, _)| name)
183 }
184 (tag1, tag2) => tag1 == tag2,
185 })
186 }
187
188 fn find_inheritdoc_base(&self) -> Option<&'a str> {
190 self.iter()
191 .find(|c| matches!(c.tag, CommentTag::Inheritdoc))
192 .and_then(|c| c.value.split_whitespace().next())
193 }
194
195 pub fn get_custom_tags(&self) -> Self {
197 CommentsRef(self.iter().copied().filter(|c| c.is_custom()).collect())
198 }
199}
200
201impl<'a> From<&'a Comments> for CommentsRef<'a> {
202 fn from(value: &'a Comments) -> Self {
203 Self(value.iter().collect())
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn parse_comment_tag() {
213 assert_eq!(CommentTag::from_str("title"), Some(CommentTag::Title));
214 assert_eq!(CommentTag::from_str(" title "), Some(CommentTag::Title));
215 assert_eq!(CommentTag::from_str("author"), Some(CommentTag::Author));
216 assert_eq!(CommentTag::from_str("notice"), Some(CommentTag::Notice));
217 assert_eq!(CommentTag::from_str("dev"), Some(CommentTag::Dev));
218 assert_eq!(CommentTag::from_str("param"), Some(CommentTag::Param));
219 assert_eq!(CommentTag::from_str("return"), Some(CommentTag::Return));
220 assert_eq!(CommentTag::from_str("inheritdoc"), Some(CommentTag::Inheritdoc));
221 assert_eq!(CommentTag::from_str("custom:"), Some(CommentTag::Custom(String::new())));
222 assert_eq!(
223 CommentTag::from_str("custom:some"),
224 Some(CommentTag::Custom("some".to_owned()))
225 );
226 assert_eq!(
227 CommentTag::from_str(" custom: some "),
228 Some(CommentTag::Custom("some".to_owned()))
229 );
230
231 assert_eq!(CommentTag::from_str(""), None);
232 assert_eq!(CommentTag::from_str("custom"), None);
233 assert_eq!(CommentTag::from_str("sometag"), None);
234 }
235
236 #[test]
237 fn test_is_custom() {
238 let custom_comment = Comment::new(
240 CommentTag::from_str("custom:test").unwrap(),
241 "dummy custom tag".to_owned(),
242 );
243 assert!(custom_comment.is_custom(), "Custom tag should return true for is_custom");
244
245 let non_custom_tags = [
247 CommentTag::Title,
248 CommentTag::Author,
249 CommentTag::Notice,
250 CommentTag::Dev,
251 CommentTag::Param,
252 CommentTag::Return,
253 CommentTag::Inheritdoc,
254 ];
255 for tag in non_custom_tags {
256 let comment = Comment::new(tag.clone(), "Non-custom comment".to_string());
257 assert!(
258 !comment.is_custom(),
259 "Non-custom tag {tag:?} should return false for is_custom"
260 );
261 }
262 }
263}