1use 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
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 && 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}