1use alloy_primitives::map::HashMap;
2use derive_more::{derive::Display, Deref, DerefMut};
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(
90 |(word, rest)| {
91 if word == expected {
92 Some(rest)
93 } else {
94 None
95 }
96 },
97 )
98 }
99
100 pub fn is_custom(&self) -> bool {
102 matches!(self.tag, CommentTag::Custom(_))
103 }
104}
105
106#[derive(Clone, Debug, Default, PartialEq, Deref, DerefMut)]
108pub struct Comments(Vec<Comment>);
109
110macro_rules! ref_fn {
113 ($vis:vis fn $name:ident(&self$(, )?$($arg_name:ident: $arg:ty),*) -> $ret:ty) => {
114 $vis fn $name(&self, $($arg_name: $arg),*) -> $ret {
116 CommentsRef::from(self).$name($($arg_name),*)
117 }
118 };
119}
120
121impl Comments {
122 ref_fn!(pub fn include_tag(&self, tag: CommentTag) -> CommentsRef<'_>);
123 ref_fn!(pub fn include_tags(&self, tags: &[CommentTag]) -> CommentsRef<'_>);
124 ref_fn!(pub fn exclude_tags(&self, tags: &[CommentTag]) -> CommentsRef<'_>);
125 ref_fn!(pub fn contains_tag(&self, tag: &Comment) -> bool);
126 ref_fn!(pub fn find_inheritdoc_base(&self) -> Option<&'_ str>);
127
128 pub fn merge_inheritdoc(
133 &self,
134 ident: &str,
135 inheritdocs: Option<HashMap<String, Self>>,
136 ) -> Self {
137 let mut result = Self(Vec::from_iter(self.iter().cloned()));
138
139 if let (Some(inheritdocs), Some(base)) = (inheritdocs, self.find_inheritdoc_base()) {
140 let key = format!("{base}.{ident}");
141 if let Some(other) = inheritdocs.get(&key) {
142 for comment in other.iter() {
143 if !result.contains_tag(comment) {
144 result.push(comment.clone());
145 }
146 }
147 }
148 }
149
150 result
151 }
152}
153
154impl From<Vec<DocCommentTag>> for Comments {
155 fn from(value: Vec<DocCommentTag>) -> Self {
156 Self(value.into_iter().flat_map(Comment::from_doc_comment).collect())
157 }
158}
159
160#[derive(Debug, Default, PartialEq, Deref)]
162pub struct CommentsRef<'a>(Vec<&'a Comment>);
163
164impl<'a> CommentsRef<'a> {
165 pub fn include_tag(&self, tag: CommentTag) -> Self {
167 self.include_tags(&[tag])
168 }
169
170 pub fn include_tags(&self, tags: &[CommentTag]) -> Self {
172 CommentsRef(self.iter().cloned().filter(|c| tags.contains(&c.tag)).collect())
174 }
175
176 pub fn exclude_tags(&self, tags: &[CommentTag]) -> Self {
178 CommentsRef(self.iter().cloned().filter(|c| !tags.contains(&c.tag)).collect())
180 }
181
182 pub fn contains_tag(&self, target: &Comment) -> bool {
184 self.iter().any(|c| match (&c.tag, &target.tag) {
185 (CommentTag::Inheritdoc, CommentTag::Inheritdoc) => c.value == target.value,
186 (CommentTag::Param, CommentTag::Param) | (CommentTag::Return, CommentTag::Return) => {
187 c.split_first_word().map(|(name, _)| name) ==
188 target.split_first_word().map(|(name, _)| name)
189 }
190 (tag1, tag2) => tag1 == tag2,
191 })
192 }
193
194 fn find_inheritdoc_base(&self) -> Option<&'a str> {
196 self.iter()
197 .find(|c| matches!(c.tag, CommentTag::Inheritdoc))
198 .and_then(|c| c.value.split_whitespace().next())
199 }
200
201 pub fn get_custom_tags(&self) -> Self {
203 CommentsRef(self.iter().cloned().filter(|c| c.is_custom()).collect())
204 }
205}
206
207impl<'a> From<&'a Comments> for CommentsRef<'a> {
208 fn from(value: &'a Comments) -> Self {
209 Self(value.iter().collect())
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn parse_comment_tag() {
219 assert_eq!(CommentTag::from_str("title"), Some(CommentTag::Title));
220 assert_eq!(CommentTag::from_str(" title "), Some(CommentTag::Title));
221 assert_eq!(CommentTag::from_str("author"), Some(CommentTag::Author));
222 assert_eq!(CommentTag::from_str("notice"), Some(CommentTag::Notice));
223 assert_eq!(CommentTag::from_str("dev"), Some(CommentTag::Dev));
224 assert_eq!(CommentTag::from_str("param"), Some(CommentTag::Param));
225 assert_eq!(CommentTag::from_str("return"), Some(CommentTag::Return));
226 assert_eq!(CommentTag::from_str("inheritdoc"), Some(CommentTag::Inheritdoc));
227 assert_eq!(CommentTag::from_str("custom:"), Some(CommentTag::Custom(String::new())));
228 assert_eq!(
229 CommentTag::from_str("custom:some"),
230 Some(CommentTag::Custom("some".to_owned()))
231 );
232 assert_eq!(
233 CommentTag::from_str(" custom: some "),
234 Some(CommentTag::Custom("some".to_owned()))
235 );
236
237 assert_eq!(CommentTag::from_str(""), None);
238 assert_eq!(CommentTag::from_str("custom"), None);
239 assert_eq!(CommentTag::from_str("sometag"), None);
240 }
241
242 #[test]
243 fn test_is_custom() {
244 let custom_comment = Comment::new(
246 CommentTag::from_str("custom:test").unwrap(),
247 "dummy custom tag".to_owned(),
248 );
249 assert!(custom_comment.is_custom(), "Custom tag should return true for is_custom");
250
251 let non_custom_tags = [
253 CommentTag::Title,
254 CommentTag::Author,
255 CommentTag::Notice,
256 CommentTag::Dev,
257 CommentTag::Param,
258 CommentTag::Return,
259 CommentTag::Inheritdoc,
260 ];
261 for tag in non_custom_tags {
262 let comment = Comment::new(tag.clone(), "Non-custom comment".to_string());
263 assert!(
264 !comment.is_custom(),
265 "Non-custom tag {tag:?} should return false for is_custom"
266 );
267 }
268 }
269}