forge_doc/parser/
mod.rs

1//! The parser module.
2
3use forge_fmt::{FormatterConfig, Visitable, Visitor};
4use itertools::Itertools;
5use solang_parser::{
6    doccomment::{parse_doccomments, DocComment},
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                if param.name.is_none() {
190                    let docs = self.parse_docs_range(start_loc, loc.end())?;
191                    let name_tag =
192                        docs.iter().find(|c| c.tag == CommentTag::Custom("name".to_owned()));
193                    if let Some(name_tag) = name_tag {
194                        if let Some(name) = name_tag.value.trim().split(' ').next() {
195                            param.name =
196                                Some(Identifier { loc: Loc::Implicit, name: name.to_owned() })
197                        }
198                    }
199                }
200            }
201            start_loc = loc.end();
202        }
203
204        self.add_element_to_parent(ParseSource::Function(func.clone()), func.loc)
205    }
206
207    fn visit_struct(&mut self, structure: &mut StructDefinition) -> ParserResult<()> {
208        self.add_element_to_parent(ParseSource::Struct(structure.clone()), structure.loc)
209    }
210
211    fn visit_event(&mut self, event: &mut EventDefinition) -> ParserResult<()> {
212        self.add_element_to_parent(ParseSource::Event(event.clone()), event.loc)
213    }
214
215    fn visit_error(&mut self, error: &mut ErrorDefinition) -> ParserResult<()> {
216        self.add_element_to_parent(ParseSource::Error(error.clone()), error.loc)
217    }
218
219    fn visit_type_definition(&mut self, def: &mut TypeDefinition) -> ParserResult<()> {
220        self.add_element_to_parent(ParseSource::Type(def.clone()), def.loc)
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use solang_parser::parse;
228
229    #[inline]
230    fn parse_source(src: &str) -> Vec<ParseItem> {
231        let (mut source, comments) = parse(src, 0).expect("failed to parse source");
232        let mut doc = Parser::new(comments, src.to_owned());
233        source.visit(&mut doc).expect("failed to visit source");
234        doc.items()
235    }
236
237    macro_rules! test_single_unit {
238        ($test:ident, $src:expr, $variant:ident $identity:expr) => {
239            #[test]
240            fn $test() {
241                let items = parse_source($src);
242                assert_eq!(items.len(), 1);
243                let item = items.first().unwrap();
244                assert!(item.comments.is_empty());
245                assert!(item.children.is_empty());
246                assert_eq!(item.source.ident(), $identity);
247                assert!(matches!(item.source, ParseSource::$variant(_)));
248            }
249        };
250    }
251
252    #[test]
253    fn empty_source() {
254        assert_eq!(parse_source(""), vec![]);
255    }
256
257    test_single_unit!(single_function, "function someFn() { }", Function "someFn");
258    test_single_unit!(single_variable, "uint256 constant VALUE = 0;", Variable "VALUE");
259    test_single_unit!(single_event, "event SomeEvent();", Event "SomeEvent");
260    test_single_unit!(single_error, "error SomeError();", Error "SomeError");
261    test_single_unit!(single_struct, "struct SomeStruct { }", Struct "SomeStruct");
262    test_single_unit!(single_enum, "enum SomeEnum { SOME, OTHER }", Enum "SomeEnum");
263    test_single_unit!(single_contract, "contract Contract { }", Contract "Contract");
264
265    #[test]
266    fn multiple_shallow_contracts() {
267        let items = parse_source(
268            r"
269            contract A { }
270            contract B { }
271            contract C { }
272        ",
273        );
274        assert_eq!(items.len(), 3);
275
276        let first_item = items.first().unwrap();
277        assert!(matches!(first_item.source, ParseSource::Contract(_)));
278        assert_eq!(first_item.source.ident(), "A");
279
280        let first_item = items.get(1).unwrap();
281        assert!(matches!(first_item.source, ParseSource::Contract(_)));
282        assert_eq!(first_item.source.ident(), "B");
283
284        let first_item = items.get(2).unwrap();
285        assert!(matches!(first_item.source, ParseSource::Contract(_)));
286        assert_eq!(first_item.source.ident(), "C");
287    }
288
289    #[test]
290    fn contract_with_children_items() {
291        let items = parse_source(
292            r"
293            event TopLevelEvent();
294
295            contract Contract {
296                event ContractEvent();
297                error ContractError();
298                struct ContractStruct { }
299                enum ContractEnum { }
300
301                uint256 constant CONTRACT_CONSTANT;
302                bool contractVar;
303
304                function contractFunction(uint256) external returns (uint256) {
305                    bool localVar; // must be ignored
306                }
307            }
308        ",
309        );
310
311        assert_eq!(items.len(), 2);
312
313        let event = items.first().unwrap();
314        assert!(event.comments.is_empty());
315        assert!(event.children.is_empty());
316        assert_eq!(event.source.ident(), "TopLevelEvent");
317        assert!(matches!(event.source, ParseSource::Event(_)));
318
319        let contract = items.get(1).unwrap();
320        assert!(contract.comments.is_empty());
321        assert_eq!(contract.children.len(), 7);
322        assert_eq!(contract.source.ident(), "Contract");
323        assert!(matches!(contract.source, ParseSource::Contract(_)));
324        assert!(contract.children.iter().all(|ch| ch.children.is_empty()));
325        assert!(contract.children.iter().all(|ch| ch.comments.is_empty()));
326    }
327
328    #[test]
329    fn contract_with_fallback() {
330        let items = parse_source(
331            r"
332            contract Contract {
333                fallback() external payable {}
334            }
335        ",
336        );
337
338        assert_eq!(items.len(), 1);
339
340        let contract = items.first().unwrap();
341        assert!(contract.comments.is_empty());
342        assert_eq!(contract.children.len(), 1);
343        assert_eq!(contract.source.ident(), "Contract");
344        assert!(matches!(contract.source, ParseSource::Contract(_)));
345
346        let fallback = contract.children.first().unwrap();
347        assert_eq!(fallback.source.ident(), "fallback");
348        assert!(matches!(fallback.source, ParseSource::Function(_)));
349    }
350
351    #[test]
352    fn contract_with_doc_comments() {
353        let items = parse_source(
354            r"
355            pragma solidity ^0.8.19;
356            /// @name Test
357            ///  no tag
358            ///@notice    Cool contract    
359            ///   @  dev     This is not a dev tag 
360            /**
361             * @dev line one
362             *    line 2
363             */
364            contract Test {
365                /*** my function    
366                      i like whitespace    
367            */
368                function test() {}
369            }
370        ",
371        );
372
373        assert_eq!(items.len(), 1);
374
375        let contract = items.first().unwrap();
376        assert_eq!(contract.comments.len(), 2);
377        assert_eq!(
378            *contract.comments.first().unwrap(),
379            Comment::new(CommentTag::Notice, "Cool contract".to_owned())
380        );
381        assert_eq!(
382            *contract.comments.get(1).unwrap(),
383            Comment::new(CommentTag::Dev, "line one\nline 2".to_owned())
384        );
385
386        let function = contract.children.first().unwrap();
387        assert_eq!(
388            *function.comments.first().unwrap(),
389            Comment::new(CommentTag::Notice, "my function\ni like whitespace".to_owned())
390        );
391    }
392}