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    #[inline]
228    fn parse_source(src: &str) -> Vec<ParseItem> {
229        let (mut source, comments) = parse(src, 0).expect("failed to parse source");
230        let mut doc = Parser::new(comments, src.to_owned());
231        source.visit(&mut doc).expect("failed to visit source");
232        doc.items()
233    }
234
235    macro_rules! test_single_unit {
236        ($test:ident, $src:expr, $variant:ident $identity:expr) => {
237            #[test]
238            fn $test() {
239                let items = parse_source($src);
240                assert_eq!(items.len(), 1);
241                let item = items.first().unwrap();
242                assert!(item.comments.is_empty());
243                assert!(item.children.is_empty());
244                assert_eq!(item.source.ident(), $identity);
245                assert!(matches!(item.source, ParseSource::$variant(_)));
246            }
247        };
248    }
249
250    #[test]
251    fn empty_source() {
252        assert_eq!(parse_source(""), vec![]);
253    }
254
255    test_single_unit!(single_function, "function someFn() { }", Function "someFn");
256    test_single_unit!(single_variable, "uint256 constant VALUE = 0;", Variable "VALUE");
257    test_single_unit!(single_event, "event SomeEvent();", Event "SomeEvent");
258    test_single_unit!(single_error, "error SomeError();", Error "SomeError");
259    test_single_unit!(single_struct, "struct SomeStruct { }", Struct "SomeStruct");
260    test_single_unit!(single_enum, "enum SomeEnum { SOME, OTHER }", Enum "SomeEnum");
261    test_single_unit!(single_contract, "contract Contract { }", Contract "Contract");
262
263    #[test]
264    fn multiple_shallow_contracts() {
265        let items = parse_source(
266            r"
267            contract A { }
268            contract B { }
269            contract C { }
270        ",
271        );
272        assert_eq!(items.len(), 3);
273
274        let first_item = items.first().unwrap();
275        assert!(matches!(first_item.source, ParseSource::Contract(_)));
276        assert_eq!(first_item.source.ident(), "A");
277
278        let first_item = items.get(1).unwrap();
279        assert!(matches!(first_item.source, ParseSource::Contract(_)));
280        assert_eq!(first_item.source.ident(), "B");
281
282        let first_item = items.get(2).unwrap();
283        assert!(matches!(first_item.source, ParseSource::Contract(_)));
284        assert_eq!(first_item.source.ident(), "C");
285    }
286
287    #[test]
288    fn contract_with_children_items() {
289        let items = parse_source(
290            r"
291            event TopLevelEvent();
292
293            contract Contract {
294                event ContractEvent();
295                error ContractError();
296                struct ContractStruct { }
297                enum ContractEnum { }
298
299                uint256 constant CONTRACT_CONSTANT;
300                bool contractVar;
301
302                function contractFunction(uint256) external returns (uint256) {
303                    bool localVar; // must be ignored
304                }
305            }
306        ",
307        );
308
309        assert_eq!(items.len(), 2);
310
311        let event = items.first().unwrap();
312        assert!(event.comments.is_empty());
313        assert!(event.children.is_empty());
314        assert_eq!(event.source.ident(), "TopLevelEvent");
315        assert!(matches!(event.source, ParseSource::Event(_)));
316
317        let contract = items.get(1).unwrap();
318        assert!(contract.comments.is_empty());
319        assert_eq!(contract.children.len(), 7);
320        assert_eq!(contract.source.ident(), "Contract");
321        assert!(matches!(contract.source, ParseSource::Contract(_)));
322        assert!(contract.children.iter().all(|ch| ch.children.is_empty()));
323        assert!(contract.children.iter().all(|ch| ch.comments.is_empty()));
324    }
325
326    #[test]
327    fn contract_with_fallback() {
328        let items = parse_source(
329            r"
330            contract Contract {
331                fallback() external payable {}
332            }
333        ",
334        );
335
336        assert_eq!(items.len(), 1);
337
338        let contract = items.first().unwrap();
339        assert!(contract.comments.is_empty());
340        assert_eq!(contract.children.len(), 1);
341        assert_eq!(contract.source.ident(), "Contract");
342        assert!(matches!(contract.source, ParseSource::Contract(_)));
343
344        let fallback = contract.children.first().unwrap();
345        assert_eq!(fallback.source.ident(), "fallback");
346        assert!(matches!(fallback.source, ParseSource::Function(_)));
347    }
348
349    #[test]
350    fn contract_with_doc_comments() {
351        let items = parse_source(
352            r"
353            pragma solidity ^0.8.19;
354            /// @name Test
355            ///  no tag
356            ///@notice    Cool contract    
357            ///   @  dev     This is not a dev tag 
358            /**
359             * @dev line one
360             *    line 2
361             */
362            contract Test {
363                /*** my function    
364                      i like whitespace    
365            */
366                function test() {}
367            }
368        ",
369        );
370
371        assert_eq!(items.len(), 1);
372
373        let contract = items.first().unwrap();
374        assert_eq!(contract.comments.len(), 2);
375        assert_eq!(
376            *contract.comments.first().unwrap(),
377            Comment::new(CommentTag::Notice, "Cool contract".to_owned())
378        );
379        assert_eq!(
380            *contract.comments.get(1).unwrap(),
381            Comment::new(CommentTag::Dev, "line one\nline 2".to_owned())
382        );
383
384        let function = contract.children.first().unwrap();
385        assert_eq!(
386            *function.comments.first().unwrap(),
387            Comment::new(CommentTag::Notice, "my function\ni like whitespace".to_owned())
388        );
389    }
390}