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