1use crate::pt::Comment;
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum DocComment {
14 Line {
18 comment: DocCommentTag,
20 },
21
22 Block {
30 comments: Vec<DocCommentTag>,
32 },
33}
34
35impl DocComment {
36 pub fn comments(&self) -> Vec<&DocCommentTag> {
38 match self {
39 DocComment::Line { comment } => vec![comment],
40 DocComment::Block { comments } => comments.iter().collect(),
41 }
42 }
43
44 pub fn into_comments(self) -> Vec<DocCommentTag> {
46 match self {
47 DocComment::Line { comment } => vec![comment],
48 DocComment::Block { comments } => comments,
49 }
50 }
51}
52
53#[derive(Clone, Debug, Default, PartialEq, Eq)]
55pub struct DocCommentTag {
56 pub tag: String,
58 pub tag_offset: usize,
60 pub value: String,
62 pub value_offset: usize,
64}
65
66enum CommentType {
67 Line,
68 Block,
69}
70
71pub fn parse_doccomments(comments: &[Comment], start: usize, end: usize) -> Vec<DocComment> {
74 let mut tags = Vec::with_capacity(comments.len());
75
76 for (ty, comment_lines) in filter_comments(comments, start, end) {
77 let mut single_tags = Vec::with_capacity(comment_lines.len());
78
79 for (start_offset, line) in comment_lines {
80 let mut chars = line.char_indices().peekable();
81
82 if let Some((_, '@')) = chars.peek() {
83 let (tag_start, _) = chars.next().unwrap();
85 let mut tag_end = tag_start;
86
87 while let Some((offset, c)) = chars.peek() {
88 if c.is_whitespace() {
89 break;
90 }
91
92 tag_end = *offset;
93
94 chars.next();
95 }
96
97 let leading = line[tag_end + 1..]
98 .chars()
99 .take_while(|ch| ch.is_whitespace())
100 .count();
101
102 single_tags.push(DocCommentTag {
104 tag_offset: start_offset + tag_start + 1,
105 tag: line[tag_start + 1..tag_end + 1].to_owned(),
106 value_offset: start_offset + tag_end + leading + 1,
107 value: line[tag_end + 1..].trim().to_owned(),
108 });
109 } else if !single_tags.is_empty() || !tags.is_empty() {
110 let line = line.trim();
111 if !line.is_empty() {
112 let single_doc_comment = if let Some(single_tag) = single_tags.last_mut() {
113 Some(single_tag)
114 } else if let Some(tag) = tags.last_mut() {
115 match tag {
116 DocComment::Line { comment } => Some(comment),
117 DocComment::Block { comments } => comments.last_mut(),
118 }
119 } else {
120 None
121 };
122
123 if let Some(comment) = single_doc_comment {
124 comment.value.push('\n');
125 comment.value.push_str(line);
126 }
127 }
128 } else {
129 let leading = line.chars().take_while(|ch| ch.is_whitespace()).count();
130
131 single_tags.push(DocCommentTag {
132 tag_offset: start_offset + start_offset + leading,
133 tag: String::from("notice"),
134 value_offset: start_offset + start_offset + leading,
135 value: line.trim().to_owned(),
136 });
137 }
138 }
139
140 match ty {
141 CommentType::Line if !single_tags.is_empty() => tags.push(DocComment::Line {
142 comment: single_tags.swap_remove(0),
143 }),
144 CommentType::Block => tags.push(DocComment::Block {
145 comments: single_tags,
146 }),
147 _ => {}
148 }
149 }
150
151 tags
152}
153
154fn filter_comments(
156 comments: &[Comment],
157 start: usize,
158 end: usize,
159) -> impl Iterator<Item = (CommentType, Vec<(usize, &str)>)> {
160 comments.iter().filter_map(move |comment| {
161 match comment {
162 Comment::Block(..) | Comment::Line(..) => None,
164 Comment::DocLine(loc, _) | Comment::DocBlock(loc, _)
166 if loc.start() >= end || loc.end() < start =>
167 {
168 None
169 }
170
171 Comment::DocLine(loc, comment) => {
172 let leading = comment
176 .find(|c: char| c != '/' && !c.is_whitespace())
177 .unwrap_or(3);
178 let comment = (loc.start() + leading, comment[leading..].trim_end());
179 Some((CommentType::Line, vec![comment]))
180 }
181 Comment::DocBlock(loc, comment) => {
182 let mut start = loc.start() + 3;
184 let mut grouped_comments = Vec::new();
185 let len = comment.len();
186 for s in comment[3..len - 2].lines() {
187 if let Some((i, _)) = s
188 .char_indices()
189 .find(|(_, ch)| !ch.is_whitespace() && *ch != '*')
190 {
191 grouped_comments.push((start + i, s[i..].trim_end()));
192 }
193
194 start += s.len() + 1;
195 }
196 Some((CommentType::Block, grouped_comments))
197 }
198 }
199 })
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn parse() {
208 let src = r#"
209pragma solidity ^0.8.19;
210/// @name Test
211/// no tag
212///@notice Cool contract
213/// @ dev This is not a dev tag
214/**
215 * @dev line one
216 * line 2
217 */
218contract Test {
219 /*** my function
220 i like whitespace
221*/
222 function test() {}
223}
224"#;
225 let (_, comments) = crate::parse(src, 0).unwrap();
226 assert_eq!(comments.len(), 6);
227
228 let actual = parse_doccomments(&comments, 0, usize::MAX);
229 let expected = vec![
230 DocComment::Line {
231 comment: DocCommentTag {
232 tag: "name".into(),
233 tag_offset: 31,
234 value: "Test\nno tag".into(),
235 value_offset: 36,
236 },
237 },
238 DocComment::Line {
239 comment: DocCommentTag {
240 tag: "notice".into(),
241 tag_offset: 57,
242 value: "Cool contract".into(),
243 value_offset: 67,
244 },
245 },
246 DocComment::Line {
247 comment: DocCommentTag {
248 tag: "".into(),
249 tag_offset: 92,
250 value: "dev This is not a dev tag".into(),
251 value_offset: 94,
252 },
253 },
254 DocComment::Block {
256 comments: vec![DocCommentTag {
257 tag: "dev".into(),
258 tag_offset: 133,
259 value: "line one\nline 2\nmy function\ni like whitespace".into(),
260 value_offset: 137,
261 }],
262 },
263 DocComment::Block { comments: vec![] },
264 ];
265
266 assert_eq!(actual, expected);
267 }
268}