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