1use 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
14pub mod error;
16use error::{ParserError, ParserResult};
17
18mod item;
20pub use item::{ParseItem, ParseSource};
21
22mod comment;
24pub use comment::{Comment, CommentTag, Comments, CommentsRef};
25
26#[derive(Debug, Default)]
31pub struct Parser {
32 comments: Vec<SolangComment>,
34 context: ParserContext,
36 items: Vec<ParseItem>,
38 source: String,
40 fmt: FormatterConfig,
42}
43
44#[derive(Debug, Default)]
46struct ParserContext {
47 parent: Option<ParseItem>,
49 doc_start_loc: usize,
51}
52
53impl Parser {
54 pub fn new(comments: Vec<SolangComment>, source: String) -> Self {
56 Self { comments, source, ..Default::default() }
57 }
58
59 pub fn with_fmt(mut self, fmt: FormatterConfig) -> Self {
61 self.fmt = fmt;
62 self
63 }
64
65 pub fn items(self) -> Vec<ParseItem> {
67 self.items
68 }
69
70 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 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 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 fn parse_docs(&mut self, end: usize) -> ParserResult<Comments> {
109 self.parse_docs_range(self.context.doc_start_loc, end)
110 }
111
112 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 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 let contract =
141 self.new_item(ParseSource::Contract(def.clone()), def.loc.start())?;
142
143 self.context.doc_start_loc = def.loc.start();
145
146 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 self.context.doc_start_loc = def.loc.end();
157
158 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 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}