use std::{
collections::HashMap,
io,
ops::ControlFlow,
path::{Path, PathBuf},
str::FromStr as _,
sync::{Arc, Mutex},
};
use solar_parse::{
Parser,
ast::{
ContractKind, DocComments, FunctionKind, Ident, Item, ItemContract, ItemKind,
ParameterList, Span, Spanned, VariableDefinition,
interface::{
Session,
source_map::{FileName, SourceMap},
},
visit::Visit,
},
interface::{ColorChoice, source_map::SourceFile},
};
use crate::{
definitions::{
Attributes, Definition, Identifier, Parent, Visibility, constructor::ConstructorDefinition,
contract::ContractDefinition, enumeration::EnumDefinition, error::ErrorDefinition,
event::EventDefinition, function::FunctionDefinition, interface::InterfaceDefinition,
library::LibraryDefinition, modifier::ModifierDefinition, structure::StructDefinition,
variable::VariableDeclaration,
},
error::{Error, Result},
natspec::{NatSpec, parse_comment},
parser::{DocumentId, Parse, ParsedDocument},
prelude::OrPanic as _,
textindex::{TextIndex, TextRange, compute_indices},
};
type Documents = Vec<(DocumentId, Arc<SourceFile>)>;
#[derive(Clone)]
pub struct SolarParser {
sess: Arc<Session>,
documents: Arc<Mutex<Documents>>,
}
impl Default for SolarParser {
fn default() -> Self {
Self::new()
}
}
impl SolarParser {
#[must_use]
pub fn new() -> Self {
let source_map = SourceMap::empty();
let sess = Session::builder()
.source_map(Arc::new(source_map))
.with_buffer_emitter(ColorChoice::Auto)
.build();
Self {
sess: Arc::new(sess),
documents: Arc::new(Mutex::new(Vec::default())),
}
}
}
impl Parse for SolarParser {
fn parse_document(
&mut self,
input: impl io::Read,
path: Option<impl AsRef<Path>>,
keep_contents: bool,
) -> Result<ParsedDocument> {
fn inner(
this: &mut SolarParser,
mut input: impl io::Read,
path: Option<PathBuf>,
keep_contents: bool,
) -> Result<ParsedDocument> {
let pathbuf = path.clone().unwrap_or(PathBuf::from("<stdin>"));
let mut buf = String::new();
input
.read_to_string(&mut buf)
.map_err(|err| Error::IOError {
path: pathbuf.clone(),
err,
})?;
let source_map = this.sess.source_map();
let source_file = source_map
.new_source_file(path.map_or(FileName::Stdin, FileName::Real), buf) .map_err(|err| Error::IOError {
path: pathbuf.clone(),
err,
})?;
let mut definitions = this
.sess
.enter_sequential(|| -> solar_parse::interface::Result<_> {
let arena = solar_parse::ast::Arena::new();
let mut parser = Parser::from_source_file(&this.sess, &arena, &source_file);
let ast = parser.parse_file().map_err(|err| err.emit())?;
let mut visitor = LintspecVisitor::new(source_map);
let _ = visitor.visit_source_unit(&ast);
Ok(visitor.definitions)
})
.map_err(|_| {
let message = match this.sess.emitted_errors() {
Some(Err(diags)) => diags.to_string(),
None | Some(Ok(())) => "unknown error".to_string(),
};
Error::ParsingError {
path: pathbuf,
loc: TextIndex::ZERO,
message,
}
})?;
let document_id = DocumentId::new();
if keep_contents {
let mut documents = this
.documents
.lock()
.or_panic("mutex should not be poisoned");
documents.push((document_id, Arc::clone(&source_file)));
}
complete_text_ranges(&source_file.src, &mut definitions);
Ok(ParsedDocument {
definitions,
id: document_id,
})
}
inner(
self,
input,
path.map(|p| p.as_ref().to_path_buf()),
keep_contents,
)
}
fn get_sources(self) -> Result<HashMap<DocumentId, String>> {
let sess = Arc::try_unwrap(self.sess).map_err(|_| Error::DanglingParserReferences)?;
drop(sess);
Arc::try_unwrap(self.documents)
.map_err(|_| Error::DanglingParserReferences)?
.into_inner()
.or_panic("mutex should not be poisoned")
.into_iter()
.map(|(id, doc)| {
let source_file =
Arc::try_unwrap(doc).map_err(|_| Error::DanglingParserReferences)?;
Ok((
id,
Arc::try_unwrap(source_file.src)
.map_err(|_| Error::DanglingParserReferences)?,
))
})
.collect::<Result<HashMap<_, _>>>()
}
}
pub struct LintspecVisitor<'ast> {
current_parent: Option<Parent>,
definitions: Vec<Definition>,
source_map: &'ast SourceMap,
}
impl<'ast> LintspecVisitor<'ast> {
pub fn new(source_map: &'ast SourceMap) -> Self {
Self {
current_parent: None,
definitions: Vec::default(),
source_map,
}
}
#[must_use]
pub fn definitions(&self) -> &Vec<Definition> {
&self.definitions
}
fn span_to_textrange(&self, span: Span) -> TextRange {
let local_begin = self.source_map.lookup_byte_offset(span.lo());
let local_end = self.source_map.lookup_byte_offset(span.hi());
let start_utf8 = local_begin.pos.to_usize();
let end_utf8 = local_end.pos.to_usize();
let start_index = TextIndex {
utf8: start_utf8,
..Default::default()
};
let end_index = TextIndex {
utf8: end_utf8,
..Default::default()
};
start_index..end_index
}
}
impl<'ast> Visit<'ast> for LintspecVisitor<'ast> {
type BreakValue = ();
fn visit_item(&mut self, item: &'ast Item<'ast>) -> ControlFlow<Self::BreakValue> {
match &item.kind {
ItemKind::Contract(item_contract) => {
if let Some(def) = item_contract.extract_definition(item, self) {
self.definitions.push(def);
}
self.visit_item_contract(item_contract)?;
}
ItemKind::Function(item_function) => {
if let Some(def) = item_function.extract_definition(item, self) {
self.definitions.push(def);
}
}
ItemKind::Variable(var_def) => {
if let Some(def) = var_def.extract_definition(item, self) {
self.definitions.push(def);
}
}
ItemKind::Struct(item_struct) => {
if let Some(def) = item_struct.extract_definition(item, self) {
self.definitions.push(def);
}
}
ItemKind::Enum(item_enum) => {
if let Some(enum_def) = item_enum.extract_definition(item, self) {
self.definitions.push(enum_def);
}
}
ItemKind::Error(item_error) => {
if let Some(def) = item_error.extract_definition(item, self) {
self.definitions.push(def);
}
}
ItemKind::Event(item_event) => {
if let Some(def) = item_event.extract_definition(item, self) {
self.definitions.push(def);
}
}
ItemKind::Pragma(_) | ItemKind::Import(_) | ItemKind::Using(_) | ItemKind::Udvt(_) => {}
}
ControlFlow::Continue(())
}
fn visit_item_contract(
&mut self,
contract: &'ast ItemContract<'ast>,
) -> ControlFlow<Self::BreakValue> {
let ItemContract { bases, body, .. } = contract;
self.current_parent = Some(contract.into());
for base in bases.iter() {
self.visit_modifier(base)?;
}
for item in body.iter() {
self.visit_item(item)?;
}
self.current_parent = None;
ControlFlow::Continue(())
}
}
trait Extract {
fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition>;
}
impl Extract for &solar_parse::ast::ItemContract<'_> {
fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
let name = self.name.to_string();
let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
Ok(extracted) => {
let end_bases = self
.bases
.last()
.map_or(self.name.span.hi(), |b| b.span().hi());
let end_specifiers = self
.layout
.as_ref()
.map_or(self.name.span.hi(), |l| l.span.hi());
let contract_end = end_bases.max(end_specifiers);
extracted.map_or_else(
|| {
(
None,
visitor.span_to_textrange(item.span.with_hi(contract_end)),
)
},
|(natspec, doc_span)| {
(
Some(natspec),
visitor.span_to_textrange(doc_span.with_hi(contract_end)),
)
},
)
}
Err(e) => return Some(Definition::NatspecParsingError(e)),
};
Some(match self.kind {
ContractKind::Contract | ContractKind::AbstractContract => ContractDefinition {
name,
span,
natspec,
}
.into(),
ContractKind::Interface => InterfaceDefinition {
name,
span,
natspec,
}
.into(),
ContractKind::Library => LibraryDefinition {
name,
span,
natspec,
}
.into(),
})
}
}
impl Extract for &solar_parse::ast::ItemFunction<'_> {
fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
let params = variable_definitions_to_identifiers(Some(&self.header.parameters), visitor);
let returns = variable_definitions_to_identifiers(self.header.returns.as_ref(), visitor);
let (natspec, span) = match extract_natspec(&item.docs, visitor, &returns) {
Ok(extracted) => extracted.map_or_else(
|| (None, visitor.span_to_textrange(self.header.span)),
|(natspec, doc_span)| {
(
Some(natspec),
visitor.span_to_textrange(doc_span.with_hi(self.header.span.hi())),
)
},
),
Err(e) => return Some(Definition::NatspecParsingError(e)),
};
match self.kind {
FunctionKind::Constructor => Some(
ConstructorDefinition {
parent: visitor.current_parent.clone(),
span,
params,
natspec,
}
.into(),
),
FunctionKind::Modifier => Some(
ModifierDefinition {
parent: visitor.current_parent.clone(),
span,
params,
natspec,
name: self
.header
.name
.as_ref()
.map_or("modifier".to_string(), Ident::to_string),
attributes: Attributes {
visibility: self.header.visibility.into(),
r#override: self.header.override_.is_some(),
},
}
.into(),
),
FunctionKind::Function => Some(
FunctionDefinition {
parent: visitor.current_parent.clone(),
name: self
.header
.name
.as_ref()
.map_or("function".to_string(), Ident::to_string),
returns: returns.clone(),
attributes: Attributes {
visibility: self.header.visibility.into(),
r#override: self.header.override_.is_some(),
},
span,
params,
natspec,
}
.into(),
),
FunctionKind::Receive | FunctionKind::Fallback => None,
}
}
}
impl Extract for &solar_parse::ast::VariableDefinition<'_> {
fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
Ok(extracted) => extracted.map_or_else(
|| (None, visitor.span_to_textrange(item.span)),
|(natspec, doc_span)| {
(
Some(natspec),
visitor.span_to_textrange(doc_span.with_hi(item.span.hi())),
)
},
),
Err(e) => return Some(Definition::NatspecParsingError(e)),
};
let attributes = Attributes {
visibility: self.visibility.into(),
r#override: self.override_.is_some(),
};
Some(
VariableDeclaration {
parent: visitor.current_parent.clone(),
name: self
.name
.as_ref()
.map_or("variable".to_string(), Ident::to_string),
span,
natspec,
attributes,
}
.into(),
)
}
}
impl Extract for &solar_parse::ast::ItemStruct<'_> {
fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
let name = self.name.to_string();
let members = self
.fields
.iter()
.map(|m| Identifier {
name: Some(
m.name
.as_ref()
.map_or("member".to_string(), Ident::to_string),
),
span: visitor.span_to_textrange(m.span),
})
.collect();
let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
Ok(extracted) => extracted.map_or_else(
|| (None, visitor.span_to_textrange(item.span)),
|(natspec, doc_span)| {
(
Some(natspec),
visitor.span_to_textrange(doc_span.with_hi(item.span.hi())),
)
},
),
Err(e) => return Some(Definition::NatspecParsingError(e)),
};
Some(
StructDefinition {
parent: visitor.current_parent.clone(),
name,
span,
members,
natspec,
}
.into(),
)
}
}
impl Extract for &solar_parse::ast::ItemEnum<'_> {
fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
let members = self
.variants
.iter()
.map(|v| Identifier {
name: Some(v.name.to_string()),
span: visitor.span_to_textrange(v.span),
})
.collect();
let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
Ok(extracted) => extracted.map_or_else(
|| (None, visitor.span_to_textrange(item.span)),
|(natspec, doc_span)| {
(
Some(natspec),
visitor.span_to_textrange(doc_span.with_hi(item.span.hi())),
)
},
),
Err(e) => return Some(Definition::NatspecParsingError(e)),
};
Some(
EnumDefinition {
parent: visitor.current_parent.clone(),
name: self.name.to_string(),
span,
members,
natspec,
}
.into(),
)
}
}
impl Extract for &solar_parse::ast::ItemError<'_> {
fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
let params = variable_definitions_to_identifiers(Some(&self.parameters), visitor);
let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
Ok(extracted) => extracted.map_or_else(
|| (None, visitor.span_to_textrange(item.span)),
|(natspec, doc_span)| {
(
Some(natspec),
visitor.span_to_textrange(doc_span.with_hi(item.span.hi())),
)
},
),
Err(e) => return Some(Definition::NatspecParsingError(e)),
};
Some(
ErrorDefinition {
parent: visitor.current_parent.clone(),
span,
name: self.name.to_string(),
params,
natspec,
}
.into(),
)
}
}
impl Extract for &solar_parse::ast::ItemEvent<'_> {
fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
let params = variable_definitions_to_identifiers(Some(&self.parameters), visitor);
let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
Ok(extracted) => extracted.map_or_else(
|| (None, visitor.span_to_textrange(item.span)),
|(natspec, doc_span)| {
(
Some(natspec),
visitor.span_to_textrange(doc_span.with_hi(item.span.hi())),
)
},
),
Err(e) => return Some(Definition::NatspecParsingError(e)),
};
Some(
EventDefinition {
parent: visitor.current_parent.clone(),
name: self.name.to_string(),
span,
params,
natspec,
}
.into(),
)
}
}
impl From<&ItemContract<'_>> for Parent {
fn from(contract: &ItemContract) -> Self {
match contract.kind {
ContractKind::Contract | ContractKind::AbstractContract => {
Parent::Contract(contract.name.to_string())
}
ContractKind::Library => Parent::Library(contract.name.to_string()),
ContractKind::Interface => Parent::Interface(contract.name.to_string()),
}
}
}
impl From<Option<Spanned<solar_parse::ast::Visibility>>> for Visibility {
fn from(visibility: Option<Spanned<solar_parse::ast::Visibility>>) -> Self {
visibility.as_deref().into()
}
}
impl From<Option<solar_parse::ast::Visibility>> for Visibility {
fn from(visibility: Option<solar_parse::ast::Visibility>) -> Self {
visibility.as_ref().into()
}
}
impl From<Option<&solar_parse::ast::Visibility>> for Visibility {
fn from(visibility: Option<&solar_parse::ast::Visibility>) -> Self {
match visibility {
Some(solar_parse::ast::Visibility::Public) => Visibility::Public,
Some(solar_parse::ast::Visibility::Private) => Visibility::Private,
Some(solar_parse::ast::Visibility::External) => Visibility::External,
Some(solar_parse::ast::Visibility::Internal) | None => Visibility::Internal,
}
}
}
fn variable_definitions_to_identifiers(
variable_definitions: Option<&ParameterList>,
visitor: &mut LintspecVisitor,
) -> Vec<Identifier> {
let Some(variable_definitions) = variable_definitions else {
return Vec::new();
};
variable_definitions
.iter()
.map(|r: &VariableDefinition<'_>| {
if let Some(name) = r.name {
Identifier {
name: Some(name.to_string()),
span: visitor.span_to_textrange(name.span),
}
} else {
Identifier {
name: None,
span: visitor.span_to_textrange(r.span),
}
}
})
.collect()
}
fn extract_natspec(
docs: &DocComments,
visitor: &mut LintspecVisitor,
returns: &[Identifier],
) -> Result<Option<(NatSpec, Span)>> {
if docs.is_empty() {
return Ok(None);
}
let mut combined = NatSpec::default();
for doc in docs.iter() {
let snippet = visitor.source_map.span_to_snippet(doc.span).map_err(|e| {
let path = visitor
.source_map
.files()
.first()
.map_or(PathBuf::from("<stdin>"), |f| {
PathBuf::from_str(&f.name.display().to_string())
.unwrap_or(PathBuf::from("<unsupported path>"))
});
Error::ParsingError {
path,
loc: visitor.span_to_textrange(doc.span).start,
message: format!("{e:?}"),
}
})?;
let mut parsed = parse_comment(&mut snippet.as_str())
.map_err(|e| Error::NatspecParsingError {
parent: visitor.current_parent.clone(),
span: visitor.span_to_textrange(doc.span),
message: e.to_string(),
})?
.populate_returns(returns);
combined.append(&mut parsed);
}
Ok(Some((combined, docs.span())))
}
fn gather_offsets(definitions: &[Definition]) -> Vec<usize> {
fn register_span(offsets: &mut Vec<usize>, span: &TextRange) {
offsets.push(span.start.utf8);
offsets.push(span.end.utf8);
}
let mut offsets = Vec::with_capacity(definitions.len() * 16); for def in definitions {
def.span().inspect(|s| register_span(&mut offsets, s));
match def {
Definition::Constructor(ConstructorDefinition { params, .. })
| Definition::Error(ErrorDefinition { params, .. })
| Definition::Event(EventDefinition { params, .. })
| Definition::Modifier(ModifierDefinition { params, .. })
| Definition::Enumeration(EnumDefinition {
members: params, ..
})
| Definition::Struct(StructDefinition {
members: params, ..
}) => {
for p in params {
register_span(&mut offsets, &p.span);
}
}
Definition::Function(d) => {
d.params
.iter()
.for_each(|i| register_span(&mut offsets, &i.span));
d.returns
.iter()
.for_each(|i| register_span(&mut offsets, &i.span));
}
Definition::NatspecParsingError(Error::NatspecParsingError { span, .. }) => {
register_span(&mut offsets, span);
}
Definition::Contract(_)
| Definition::Interface(_)
| Definition::Library(_)
| Definition::Variable(_)
| Definition::NatspecParsingError(_) => {}
}
}
offsets.sort_unstable();
offsets
}
fn populate(text_indices: &[TextIndex], definitions: &mut Vec<Definition>) {
fn populate_span(indices: &[TextIndex], start_idx: usize, span: &mut TextRange) -> usize {
let idx;
(idx, span.start) = indices
.iter()
.enumerate()
.skip(start_idx)
.find_map(|(i, ti)| (ti.utf8 >= span.start.utf8).then_some((i, *ti)))
.or_panic("utf8 start offset should be present in cache");
span.end = *indices
.iter()
.skip(idx + 1)
.find(|ti| ti.utf8 >= span.end.utf8)
.or_panic("utf8 end offset should be present in cache");
idx + 1
}
let mut idx = 0;
for def in definitions {
if let Some(span) = def.span_mut() {
idx = populate_span(text_indices, idx, span);
}
match def {
Definition::Constructor(ConstructorDefinition { params, .. })
| Definition::Error(ErrorDefinition { params, .. })
| Definition::Event(EventDefinition { params, .. })
| Definition::Modifier(ModifierDefinition { params, .. })
| Definition::Enumeration(EnumDefinition {
members: params, ..
})
| Definition::Struct(StructDefinition {
members: params, ..
}) => {
for p in params {
idx = populate_span(text_indices, idx, &mut p.span);
}
}
Definition::Function(d) => {
for p in &mut d.params {
idx = populate_span(text_indices, idx, &mut p.span);
}
for p in &mut d.returns {
idx = populate_span(text_indices, idx, &mut p.span);
}
}
Definition::NatspecParsingError(Error::NatspecParsingError { span, .. }) => {
idx = populate_span(text_indices, idx, span);
}
Definition::Contract(_)
| Definition::Interface(_)
| Definition::Library(_)
| Definition::Variable(_)
| Definition::NatspecParsingError(_) => {}
}
}
}
pub fn complete_text_ranges(source: &str, definitions: &mut Vec<Definition>) {
let offsets = gather_offsets(definitions);
if offsets.is_empty() {
return;
}
let text_indices = compute_indices(source, &offsets);
populate(&text_indices, definitions);
}