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(
131 &self,
132 ident: &str,
133 inheritdocs: Option<HashMap<String, Self>>,
134 ) -> Self {
135 let mut result = self.clone();
136
137 if let (Some(inheritdocs), Some(base)) = (inheritdocs, self.find_inheritdoc_base()) {
138 let key = format!("{base}.{ident}");
139 if let Some(other) = inheritdocs.get(&key) {
140 for comment in other.iter() {
141 if !result.contains_tag(comment) {
142 result.push(comment.clone());
143 }
144 }
145 }
146 }
147
148 result
149 }
150}
151
152impl From<Vec<DocCommentTag>> for Comments {
153 fn from(value: Vec<DocCommentTag>) -> Self {
154 Self(value.into_iter().flat_map(Comment::from_doc_comment).collect())
155 }
156}
157
158#[derive(Debug, Default, PartialEq, Deref)]
160pub struct CommentsRef<'a>(Vec<&'a Comment>);
161
162impl<'a> CommentsRef<'a> {
163 pub fn include_tag(&self, tag: CommentTag) -> Self {
165 self.include_tags(&[tag])
166 }
167
168 pub fn include_tags(&self, tags: &[CommentTag]) -> Self {
170 CommentsRef(self.iter().copied().filter(|c| tags.contains(&c.tag)).collect())
172 }
173
174 pub fn exclude_tags(&self, tags: &[CommentTag]) -> Self {
176 CommentsRef(self.iter().copied().filter(|c| !tags.contains(&c.tag)).collect())
178 }
179
180 pub fn contains_tag(&self, target: &Comment) -> bool {
182 self.iter().any(|c| match (&c.tag, &target.tag) {
183 (CommentTag::Inheritdoc, CommentTag::Inheritdoc) => c.value == target.value,
184 (CommentTag::Param, CommentTag::Param) | (CommentTag::Return, CommentTag::Return) => {
185 c.split_first_word().map(|(name, _)| name)
186 == target.split_first_word().map(|(name, _)| name)
187 }
188 (tag1, tag2) => tag1 == tag2,
189 })
190 }
191
192 fn find_inheritdoc_base(&self) -> Option<&'a str> {
194 self.iter()
195 .find(|c| matches!(c.tag, CommentTag::Inheritdoc))
196 .and_then(|c| c.value.split_whitespace().next())
197 }
198
199 pub fn get_custom_tags(&self) -> Self {
201 CommentsRef(self.iter().copied().filter(|c| c.is_custom()).collect())
202 }
203}
204
205impl<'a> From<&'a Comments> for CommentsRef<'a> {
206 fn from(value: &'a Comments) -> Self {
207 Self(value.iter().collect())
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn parse_comment_tag() {
217 assert_eq!(CommentTag::from_str("title"), Some(CommentTag::Title));
218 assert_eq!(CommentTag::from_str(" title "), Some(CommentTag::Title));
219 assert_eq!(CommentTag::from_str("author"), Some(CommentTag::Author));
220 assert_eq!(CommentTag::from_str("notice"), Some(CommentTag::Notice));
221 assert_eq!(CommentTag::from_str("dev"), Some(CommentTag::Dev));
222 assert_eq!(CommentTag::from_str("param"), Some(CommentTag::Param));
223 assert_eq!(CommentTag::from_str("return"), Some(CommentTag::Return));
224 assert_eq!(CommentTag::from_str("inheritdoc"), Some(CommentTag::Inheritdoc));
225 assert_eq!(CommentTag::from_str("custom:"), Some(CommentTag::Custom(String::new())));
226 assert_eq!(
227 CommentTag::from_str("custom:some"),
228 Some(CommentTag::Custom("some".to_owned()))
229 );
230 assert_eq!(
231 CommentTag::from_str(" custom: some "),
232 Some(CommentTag::Custom("some".to_owned()))
233 );
234
235 assert_eq!(CommentTag::from_str(""), None);
236 assert_eq!(CommentTag::from_str("custom"), None);
237 assert_eq!(CommentTag::from_str("sometag"), None);
238 }
239
240 #[test]
241 fn test_is_custom() {
242 let custom_comment = Comment::new(
244 CommentTag::from_str("custom:test").unwrap(),
245 "dummy custom tag".to_owned(),
246 );
247 assert!(custom_comment.is_custom(), "Custom tag should return true for is_custom");
248
249 let non_custom_tags = [
251 CommentTag::Title,
252 CommentTag::Author,
253 CommentTag::Notice,
254 CommentTag::Dev,
255 CommentTag::Param,
256 CommentTag::Return,
257 CommentTag::Inheritdoc,
258 ];
259 for tag in non_custom_tags {
260 let comment = Comment::new(tag.clone(), "Non-custom comment".to_string());
261 assert!(
262 !comment.is_custom(),
263 "Non-custom tag {tag:?} should return false for is_custom"
264 );
265 }
266 }
267}