use forge_fmt::{FormatterConfig, Visitable, Visitor};
use itertools::Itertools;
use solang_parser::{
doccomment::{parse_doccomments, DocComment},
pt::{
Comment as SolangComment, EnumDefinition, ErrorDefinition, EventDefinition,
FunctionDefinition, Identifier, Loc, SourceUnit, SourceUnitPart, StructDefinition,
TypeDefinition, VariableDefinition,
},
};
pub mod error;
use error::{ParserError, ParserResult};
mod item;
pub use item::{ParseItem, ParseSource};
mod comment;
pub use comment::{Comment, CommentTag, Comments, CommentsRef};
#[derive(Debug, Default)]
pub struct Parser {
comments: Vec<SolangComment>,
context: ParserContext,
items: Vec<ParseItem>,
source: String,
fmt: FormatterConfig,
}
#[derive(Debug, Default)]
struct ParserContext {
parent: Option<ParseItem>,
doc_start_loc: usize,
}
impl Parser {
pub fn new(comments: Vec<SolangComment>, source: String) -> Self {
Self { comments, source, ..Default::default() }
}
pub fn with_fmt(mut self, fmt: FormatterConfig) -> Self {
self.fmt = fmt;
self
}
pub fn items(self) -> Vec<ParseItem> {
self.items
}
fn with_parent(
&mut self,
mut parent: ParseItem,
mut visit: impl FnMut(&mut Self) -> ParserResult<()>,
) -> ParserResult<ParseItem> {
let curr = self.context.parent.take();
self.context.parent = Some(parent);
visit(self)?;
parent = self.context.parent.take().unwrap();
self.context.parent = curr;
Ok(parent)
}
fn add_element_to_parent(&mut self, source: ParseSource, loc: Loc) -> ParserResult<()> {
let child = self.new_item(source, loc.start())?;
if let Some(parent) = self.context.parent.as_mut() {
parent.children.push(child);
} else {
self.items.push(child);
}
self.context.doc_start_loc = loc.end();
Ok(())
}
fn new_item(&mut self, source: ParseSource, loc_start: usize) -> ParserResult<ParseItem> {
let docs = self.parse_docs(loc_start)?;
ParseItem::new(source).with_comments(docs).with_code(&self.source, self.fmt.clone())
}
fn parse_docs(&mut self, end: usize) -> ParserResult<Comments> {
self.parse_docs_range(self.context.doc_start_loc, end)
}
fn parse_docs_range(&mut self, start: usize, end: usize) -> ParserResult<Comments> {
let mut res = vec![];
for comment in parse_doccomments(&self.comments, start, end) {
match comment {
DocComment::Line { comment } => res.push(comment),
DocComment::Block { comments } => res.extend(comments),
}
}
let res = res
.into_iter()
.filter(|c| c.tag.trim() != "solidity" && !c.tag.trim().is_empty())
.collect_vec();
Ok(res.into())
}
}
impl Visitor for Parser {
type Error = ParserError;
fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> ParserResult<()> {
for source in source_unit.0.iter_mut() {
match source {
SourceUnitPart::ContractDefinition(def) => {
let contract =
self.new_item(ParseSource::Contract(def.clone()), def.loc.start())?;
self.context.doc_start_loc = def.loc.start();
let contract = self.with_parent(contract, |doc| {
def.parts
.iter_mut()
.map(|d| d.visit(doc))
.collect::<ParserResult<Vec<_>>>()?;
Ok(())
})?;
self.context.doc_start_loc = def.loc.end();
self.items.push(contract);
}
SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?,
SourceUnitPart::EventDefinition(event) => self.visit_event(event)?,
SourceUnitPart::ErrorDefinition(error) => self.visit_error(error)?,
SourceUnitPart::StructDefinition(structure) => self.visit_struct(structure)?,
SourceUnitPart::EnumDefinition(enumerable) => self.visit_enum(enumerable)?,
SourceUnitPart::VariableDefinition(var) => self.visit_var_definition(var)?,
SourceUnitPart::TypeDefinition(ty) => self.visit_type_definition(ty)?,
_ => {}
};
}
Ok(())
}
fn visit_enum(&mut self, enumerable: &mut EnumDefinition) -> ParserResult<()> {
self.add_element_to_parent(ParseSource::Enum(enumerable.clone()), enumerable.loc)
}
fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> ParserResult<()> {
self.add_element_to_parent(ParseSource::Variable(var.clone()), var.loc)
}
fn visit_function(&mut self, func: &mut FunctionDefinition) -> ParserResult<()> {
let mut start_loc = func.loc.start();
for (loc, param) in func.params.iter_mut() {
if let Some(param) = param {
if param.name.is_none() {
let docs = self.parse_docs_range(start_loc, loc.end())?;
let name_tag =
docs.iter().find(|c| c.tag == CommentTag::Custom("name".to_owned()));
if let Some(name_tag) = name_tag {
if let Some(name) = name_tag.value.trim().split(' ').next() {
param.name =
Some(Identifier { loc: Loc::Implicit, name: name.to_owned() })
}
}
}
}
start_loc = loc.end();
}
self.add_element_to_parent(ParseSource::Function(func.clone()), func.loc)
}
fn visit_struct(&mut self, structure: &mut StructDefinition) -> ParserResult<()> {
self.add_element_to_parent(ParseSource::Struct(structure.clone()), structure.loc)
}
fn visit_event(&mut self, event: &mut EventDefinition) -> ParserResult<()> {
self.add_element_to_parent(ParseSource::Event(event.clone()), event.loc)
}
fn visit_error(&mut self, error: &mut ErrorDefinition) -> ParserResult<()> {
self.add_element_to_parent(ParseSource::Error(error.clone()), error.loc)
}
fn visit_type_definition(&mut self, def: &mut TypeDefinition) -> ParserResult<()> {
self.add_element_to_parent(ParseSource::Type(def.clone()), def.loc)
}
}
#[cfg(test)]
mod tests {
use super::*;
use solang_parser::parse;
#[inline]
fn parse_source(src: &str) -> Vec<ParseItem> {
let (mut source, comments) = parse(src, 0).expect("failed to parse source");
let mut doc = Parser::new(comments, src.to_owned());
source.visit(&mut doc).expect("failed to visit source");
doc.items()
}
macro_rules! test_single_unit {
($test:ident, $src:expr, $variant:ident $identity:expr) => {
#[test]
fn $test() {
let items = parse_source($src);
assert_eq!(items.len(), 1);
let item = items.first().unwrap();
assert!(item.comments.is_empty());
assert!(item.children.is_empty());
assert_eq!(item.source.ident(), $identity);
assert!(matches!(item.source, ParseSource::$variant(_)));
}
};
}
#[test]
fn empty_source() {
assert_eq!(parse_source(""), vec![]);
}
test_single_unit!(single_function, "function someFn() { }", Function "someFn");
test_single_unit!(single_variable, "uint256 constant VALUE = 0;", Variable "VALUE");
test_single_unit!(single_event, "event SomeEvent();", Event "SomeEvent");
test_single_unit!(single_error, "error SomeError();", Error "SomeError");
test_single_unit!(single_struct, "struct SomeStruct { }", Struct "SomeStruct");
test_single_unit!(single_enum, "enum SomeEnum { SOME, OTHER }", Enum "SomeEnum");
test_single_unit!(single_contract, "contract Contract { }", Contract "Contract");
#[test]
fn multiple_shallow_contracts() {
let items = parse_source(
r"
contract A { }
contract B { }
contract C { }
",
);
assert_eq!(items.len(), 3);
let first_item = items.first().unwrap();
assert!(matches!(first_item.source, ParseSource::Contract(_)));
assert_eq!(first_item.source.ident(), "A");
let first_item = items.get(1).unwrap();
assert!(matches!(first_item.source, ParseSource::Contract(_)));
assert_eq!(first_item.source.ident(), "B");
let first_item = items.get(2).unwrap();
assert!(matches!(first_item.source, ParseSource::Contract(_)));
assert_eq!(first_item.source.ident(), "C");
}
#[test]
fn contract_with_children_items() {
let items = parse_source(
r"
event TopLevelEvent();
contract Contract {
event ContractEvent();
error ContractError();
struct ContractStruct { }
enum ContractEnum { }
uint256 constant CONTRACT_CONSTANT;
bool contractVar;
function contractFunction(uint256) external returns (uint256) {
bool localVar; // must be ignored
}
}
",
);
assert_eq!(items.len(), 2);
let event = items.first().unwrap();
assert!(event.comments.is_empty());
assert!(event.children.is_empty());
assert_eq!(event.source.ident(), "TopLevelEvent");
assert!(matches!(event.source, ParseSource::Event(_)));
let contract = items.get(1).unwrap();
assert!(contract.comments.is_empty());
assert_eq!(contract.children.len(), 7);
assert_eq!(contract.source.ident(), "Contract");
assert!(matches!(contract.source, ParseSource::Contract(_)));
assert!(contract.children.iter().all(|ch| ch.children.is_empty()));
assert!(contract.children.iter().all(|ch| ch.comments.is_empty()));
}
#[test]
fn contract_with_fallback() {
let items = parse_source(
r"
contract Contract {
fallback() external payable {}
}
",
);
assert_eq!(items.len(), 1);
let contract = items.first().unwrap();
assert!(contract.comments.is_empty());
assert_eq!(contract.children.len(), 1);
assert_eq!(contract.source.ident(), "Contract");
assert!(matches!(contract.source, ParseSource::Contract(_)));
let fallback = contract.children.first().unwrap();
assert_eq!(fallback.source.ident(), "fallback");
assert!(matches!(fallback.source, ParseSource::Function(_)));
}
}