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 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}