forge_doc/parser/
mod.rs

1//! The parser module.
2
3use crate::solang_ext::{Visitable, Visitor};
4use itertools::Itertools;
5use solang_parser::{
6    doccomment::{DocComment, parse_doccomments},
7    pt::{
8        Comment as SolangComment, EnumDefinition, ErrorDefinition, EventDefinition,
9        FunctionDefinition, Identifier, Loc, SourceUnit, SourceUnitPart, StructDefinition,
10        TypeDefinition, VariableDefinition,
11    },
12};
13
14/// Parser error.
15pub mod error;
16use error::{ParserError, ParserResult};
17
18/// Parser item.
19mod item;
20pub use item::{ParseItem, ParseSource};
21
22/// Doc comment.
23mod comment;
24pub use comment::{Comment, CommentTag, Comments, CommentsRef};
25
26/// The documentation parser. This type implements a [Visitor] trait.
27///
28/// While walking the parse tree, [Parser] will collect relevant source items and corresponding
29/// doc comments. The resulting [ParseItem]s can be accessed by calling [Parser::items].
30#[derive(Debug, Default)]
31pub struct Parser {
32    /// Initial comments from solang parser.
33    comments: Vec<SolangComment>,
34    /// Parser context.
35    context: ParserContext,
36    /// Parsed results.
37    items: Vec<ParseItem>,
38    /// Source file.
39    source: String,
40}
41
42/// [Parser] context.
43#[derive(Debug, Default)]
44struct ParserContext {
45    /// Current visited parent.
46    parent: Option<ParseItem>,
47    /// Current start pointer for parsing doc comments.
48    doc_start_loc: usize,
49}
50
51impl Parser {
52    /// Create a new instance of [Parser].
53    pub fn new(comments: Vec<SolangComment>, source: String) -> Self {
54        Self { comments, source, ..Default::default() }
55    }
56
57    /// Return the parsed items. Consumes the parser.
58    pub fn items(self) -> Vec<ParseItem> {
59        self.items
60    }
61
62    /// Visit the children elements with parent context.
63    /// This function memoizes the previous parent, sets the context
64    /// to a new one and invokes a visit function. The context will be reset
65    /// to the previous parent at the end of the function.
66    fn with_parent(
67        &mut self,
68        mut parent: ParseItem,
69        mut visit: impl FnMut(&mut Self) -> ParserResult<()>,
70    ) -> ParserResult<ParseItem> {
71        let curr = self.context.parent.take();
72        self.context.parent = Some(parent);
73        visit(self)?;
74        parent = self.context.parent.take().unwrap();
75        self.context.parent = curr;
76        Ok(parent)
77    }
78
79    /// Adds a child element to the parent item if it exists.
80    /// Otherwise the element will be added to a top-level items collection.
81    /// Moves the doc comment pointer to the end location of the child element.
82    fn add_element_to_parent(&mut self, source: ParseSource, loc: Loc) -> ParserResult<()> {
83        let child = self.new_item(source, loc.start())?;
84        if let Some(parent) = self.context.parent.as_mut() {
85            parent.children.push(child);
86        } else {
87            self.items.push(child);
88        }
89        self.context.doc_start_loc = loc.end();
90        Ok(())
91    }
92
93    /// Create new [ParseItem] with comments and formatted code.
94    fn new_item(&mut self, source: ParseSource, loc_start: usize) -> ParserResult<ParseItem> {
95        let docs = self.parse_docs(loc_start)?;
96        Ok(ParseItem::new(source).with_comments(docs).with_code(&self.source))
97    }
98
99    /// Parse the doc comments from the current start location.
100    fn parse_docs(&mut self, end: usize) -> ParserResult<Comments> {
101        self.parse_docs_range(self.context.doc_start_loc, end)
102    }
103
104    /// Parse doc comments from the within specified range.
105    fn parse_docs_range(&mut self, start: usize, end: usize) -> ParserResult<Comments> {
106        let mut res = vec![];
107        for comment in parse_doccomments(&self.comments, start, end) {
108            match comment {
109                DocComment::Line { comment } => res.push(comment),
110                DocComment::Block { comments } => res.extend(comments),
111            }
112        }
113
114        // Filter out `@solidity` and empty tags
115        // See https://docs.soliditylang.org/en/v0.8.17/assembly.html#memory-safety
116        let res = res
117            .into_iter()
118            .filter(|c| c.tag.trim() != "solidity" && !c.tag.trim().is_empty())
119            .collect_vec();
120        Ok(res.into())
121    }
122}
123
124impl Visitor for Parser {
125    type Error = ParserError;
126
127    fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> ParserResult<()> {
128        for source in &mut source_unit.0 {
129            match source {
130                SourceUnitPart::ContractDefinition(def) => {
131                    // Create new contract parse item.
132                    let contract =
133                        self.new_item(ParseSource::Contract(def.clone()), def.loc.start())?;
134
135                    // Move the doc pointer to the contract location start.
136                    self.context.doc_start_loc = def.loc.start();
137
138                    // Parse child elements with current contract as parent
139                    let contract = self.with_parent(contract, |doc| {
140                        def.parts
141                            .iter_mut()
142                            .map(|d| d.visit(doc))
143                            .collect::<ParserResult<Vec<_>>>()?;
144                        Ok(())
145                    })?;
146
147                    // Move the doc pointer to the contract location end.
148                    self.context.doc_start_loc = def.loc.end();
149
150                    // Add contract to the parsed items.
151                    self.items.push(contract);
152                }
153                SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?,
154                SourceUnitPart::EventDefinition(event) => self.visit_event(event)?,
155                SourceUnitPart::ErrorDefinition(error) => self.visit_error(error)?,
156                SourceUnitPart::StructDefinition(structure) => self.visit_struct(structure)?,
157                SourceUnitPart::EnumDefinition(enumerable) => self.visit_enum(enumerable)?,
158                SourceUnitPart::VariableDefinition(var) => self.visit_var_definition(var)?,
159                SourceUnitPart::TypeDefinition(ty) => self.visit_type_definition(ty)?,
160                _ => {}
161            };
162        }
163
164        Ok(())
165    }
166
167    fn visit_enum(&mut self, enumerable: &mut EnumDefinition) -> ParserResult<()> {
168        self.add_element_to_parent(ParseSource::Enum(enumerable.clone()), enumerable.loc)
169    }
170
171    fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> ParserResult<()> {
172        self.add_element_to_parent(ParseSource::Variable(var.clone()), var.loc)
173    }
174
175    fn visit_function(&mut self, func: &mut FunctionDefinition) -> ParserResult<()> {
176        // If the function parameter doesn't have a name, try to set it with
177        // `@custom:name` tag if any was provided
178        let mut start_loc = func.loc.start();
179        for (loc, param) in &mut func.params {
180            if let Some(param) = param
181                && param.name.is_none()
182            {
183                let docs = self.parse_docs_range(start_loc, loc.end())?;
184                let name_tag = docs.iter().find(|c| c.tag == CommentTag::Custom("name".to_owned()));
185                if let Some(name_tag) = name_tag
186                    && let Some(name) = name_tag.value.trim().split(' ').next()
187                {
188                    param.name = Some(Identifier { loc: Loc::Implicit, name: name.to_owned() })
189                }
190            }
191            start_loc = loc.end();
192        }
193
194        self.add_element_to_parent(ParseSource::Function(func.clone()), func.loc)
195    }
196
197    fn visit_struct(&mut self, structure: &mut StructDefinition) -> ParserResult<()> {
198        self.add_element_to_parent(ParseSource::Struct(structure.clone()), structure.loc)
199    }
200
201    fn visit_event(&mut self, event: &mut EventDefinition) -> ParserResult<()> {
202        self.add_element_to_parent(ParseSource::Event(event.clone()), event.loc)
203    }
204
205    fn visit_error(&mut self, error: &mut ErrorDefinition) -> ParserResult<()> {
206        self.add_element_to_parent(ParseSource::Error(error.clone()), error.loc)
207    }
208
209    fn visit_type_definition(&mut self, def: &mut TypeDefinition) -> ParserResult<()> {
210        self.add_element_to_parent(ParseSource::Type(def.clone()), def.loc)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use solang_parser::parse;
218
219    fn parse_source(src: &str) -> Vec<ParseItem> {
220        let (mut source, comments) = parse(src, 0).expect("failed to parse source");
221        let mut doc = Parser::new(comments, src.to_owned());
222        source.visit(&mut doc).expect("failed to visit source");
223        doc.items()
224    }
225
226    macro_rules! test_single_unit {
227        ($test:ident, $src:expr, $variant:ident $identity:expr) => {
228            #[test]
229            fn $test() {
230                let items = parse_source($src);
231                assert_eq!(items.len(), 1);
232                let item = items.first().unwrap();
233                assert!(item.comments.is_empty());
234                assert!(item.children.is_empty());
235                assert_eq!(item.source.ident(), $identity);
236                assert!(matches!(item.source, ParseSource::$variant(_)));
237            }
238        };
239    }
240
241    #[test]
242    fn empty_source() {
243        assert_eq!(parse_source(""), vec![]);
244    }
245
246    test_single_unit!(single_function, "function someFn() { }", Function "someFn");
247    test_single_unit!(single_variable, "uint256 constant VALUE = 0;", Variable "VALUE");
248    test_single_unit!(single_event, "event SomeEvent();", Event "SomeEvent");
249    test_single_unit!(single_error, "error SomeError();", Error "SomeError");
250    test_single_unit!(single_struct, "struct SomeStruct { }", Struct "SomeStruct");
251    test_single_unit!(single_enum, "enum SomeEnum { SOME, OTHER }", Enum "SomeEnum");
252    test_single_unit!(single_contract, "contract Contract { }", Contract "Contract");
253
254    #[test]
255    fn multiple_shallow_contracts() {
256        let items = parse_source(
257            r"
258            contract A { }
259            contract B { }
260            contract C { }
261        ",
262        );
263        assert_eq!(items.len(), 3);
264
265        let first_item = items.first().unwrap();
266        assert!(matches!(first_item.source, ParseSource::Contract(_)));
267        assert_eq!(first_item.source.ident(), "A");
268
269        let first_item = items.get(1).unwrap();
270        assert!(matches!(first_item.source, ParseSource::Contract(_)));
271        assert_eq!(first_item.source.ident(), "B");
272
273        let first_item = items.get(2).unwrap();
274        assert!(matches!(first_item.source, ParseSource::Contract(_)));
275        assert_eq!(first_item.source.ident(), "C");
276    }
277
278    #[test]
279    fn contract_with_children_items() {
280        let items = parse_source(
281            r"
282            event TopLevelEvent();
283
284            contract Contract {
285                event ContractEvent();
286                error ContractError();
287                struct ContractStruct { }
288                enum ContractEnum { }
289
290                uint256 constant CONTRACT_CONSTANT = 0;
291                bool contractVar;
292
293                function contractFunction(uint256) external returns (uint256) {
294                    bool localVar; // must be ignored
295                }
296            }
297        ",
298        );
299
300        assert_eq!(items.len(), 2);
301
302        let event = items.first().unwrap();
303        assert!(event.comments.is_empty());
304        assert!(event.children.is_empty());
305        assert_eq!(event.source.ident(), "TopLevelEvent");
306        assert!(matches!(event.source, ParseSource::Event(_)));
307
308        let contract = items.get(1).unwrap();
309        assert!(contract.comments.is_empty());
310        assert_eq!(contract.children.len(), 7);
311        assert_eq!(contract.source.ident(), "Contract");
312        assert!(matches!(contract.source, ParseSource::Contract(_)));
313        assert!(contract.children.iter().all(|ch| ch.children.is_empty()));
314        assert!(contract.children.iter().all(|ch| ch.comments.is_empty()));
315    }
316
317    #[test]
318    fn contract_with_fallback() {
319        let items = parse_source(
320            r"
321            contract Contract {
322                fallback() external payable {}
323            }
324        ",
325        );
326
327        assert_eq!(items.len(), 1);
328
329        let contract = items.first().unwrap();
330        assert!(contract.comments.is_empty());
331        assert_eq!(contract.children.len(), 1);
332        assert_eq!(contract.source.ident(), "Contract");
333        assert!(matches!(contract.source, ParseSource::Contract(_)));
334
335        let fallback = contract.children.first().unwrap();
336        assert_eq!(fallback.source.ident(), "fallback");
337        assert!(matches!(fallback.source, ParseSource::Function(_)));
338    }
339
340    #[test]
341    fn contract_with_doc_comments() {
342        let items = parse_source(
343            r"
344            pragma solidity ^0.8.19;
345            /// @name Test
346            ///  no tag
347            ///@notice    Cool contract
348            ///   @  dev     This is not a dev tag
349            /**
350             * @dev line one
351             *    line 2
352             */
353            contract Test {
354                /** my function
355                      i like whitespace
356            */
357                function test() {}
358            }
359        ",
360        );
361
362        assert_eq!(items.len(), 1);
363
364        let contract = items.first().unwrap();
365        assert_eq!(contract.comments.len(), 2);
366        assert_eq!(
367            *contract.comments.first().unwrap(),
368            Comment::new(CommentTag::Notice, "Cool contract".to_owned())
369        );
370        assert_eq!(
371            *contract.comments.get(1).unwrap(),
372            Comment::new(CommentTag::Dev, "line one\nline 2".to_owned())
373        );
374
375        let function = contract.children.first().unwrap();
376        assert_eq!(
377            *function.comments.first().unwrap(),
378            Comment::new(CommentTag::Notice, "my function\ni like whitespace".to_owned())
379        );
380    }
381}