forge_doc/parser/
mod.rs

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