solang_parser/
doccomment.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Solidity parsed doc comments.
4//!
5//! See also the Solidity documentation on [natspec].
6//!
7//! [natspec]: https://docs.soliditylang.org/en/latest/natspec-format.html
8
9use crate::pt::Comment;
10
11/// A Solidity parsed doc comment.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum DocComment {
14    /// A line doc comment.
15    ///
16    /// `/// doc comment`
17    Line {
18        /// The single comment tag of the line.
19        comment: DocCommentTag,
20    },
21
22    /// A block doc comment.
23    ///
24    /// ```text
25    /// /**
26    ///  * block doc comment
27    ///  */
28    /// ```
29    Block {
30        /// The list of doc comment tags of the block.
31        comments: Vec<DocCommentTag>,
32    },
33}
34
35impl DocComment {
36    /// Returns the inner comments.
37    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    /// Consumes `self` to return the inner comments.
45    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/// A Solidity doc comment's tag, value and respective offsets.
54#[derive(Clone, Debug, Default, PartialEq, Eq)]
55pub struct DocCommentTag {
56    /// The tag of the doc comment, like the `notice` in `/// @notice Doc comment value`
57    pub tag: String,
58    /// The offset of the comment's tag, relative to the start of the source string.
59    pub tag_offset: usize,
60    /// The actual comment string, like `Doc comment value` in `/// @notice Doc comment value`
61    pub value: String,
62    /// The offset of the comment's value, relative to the start of the source string.
63    pub value_offset: usize,
64}
65
66enum CommentType {
67    Line,
68    Block,
69}
70
71/// From the start to end offset, filter all the doc comments out of the comments and parse
72/// them into tags with values.
73pub 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                // step over @
84                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                // tag value
103                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
154/// Convert the comment to lines, stripping whitespace, comment characters and leading * in block comments
155fn 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            // filter out all non-doc comments
163            Comment::Block(..) | Comment::Line(..) => None,
164            // filter out doc comments that are outside the given range
165            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                // remove the leading /// and whitespace;
173                // if we don't find a match, default to skipping the 3 `/` bytes,
174                // since they are guaranteed to be in the comment string
175                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                // remove the leading /** and tailing */
183                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            // TODO: Second block is merged into the first
255            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}