1use std::{
5 collections::HashMap,
6 io,
7 ops::ControlFlow,
8 path::{Path, PathBuf},
9 str::FromStr as _,
10 sync::{Arc, Mutex},
11};
12
13use solar_parse::{
14 Parser,
15 ast::{
16 ContractKind, DocComments, FunctionKind, Item, ItemContract, ItemKind, ParameterList, Span,
17 Spanned, VariableDefinition,
18 interface::{
19 Session,
20 source_map::{FileName, SourceMap},
21 },
22 visit::Visit,
23 },
24 interface::{ColorChoice, source_map::SourceFile},
25};
26
27use crate::{
28 definitions::{
29 Attributes, Definition, Identifier, Parent, Visibility, constructor::ConstructorDefinition,
30 contract::ContractDefinition, enumeration::EnumDefinition, error::ErrorDefinition,
31 event::EventDefinition, function::FunctionDefinition, interface::InterfaceDefinition,
32 library::LibraryDefinition, modifier::ModifierDefinition, structure::StructDefinition,
33 variable::VariableDeclaration,
34 },
35 error::{ErrorKind, Result},
36 interner::INTERNER,
37 natspec::{NatSpec, parse_comment},
38 parser::{DocumentId, Parse, ParsedDocument, complete_text_ranges},
39 prelude::OrPanic as _,
40 textindex::{TextIndex, TextRange},
41};
42
43type Documents = Vec<(DocumentId, Arc<SourceFile>)>;
44
45#[derive(Clone)]
46pub struct SolarParser {
47 sess: Arc<Session>,
48 documents: Arc<Mutex<Documents>>,
49}
50
51impl Default for SolarParser {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl SolarParser {
58 #[must_use]
59 pub fn new() -> Self {
60 let source_map = SourceMap::empty();
61 let sess = Session::builder()
62 .source_map(Arc::new(source_map))
63 .with_buffer_emitter(ColorChoice::Auto)
64 .build();
65 Self {
66 sess: Arc::new(sess),
67 documents: Arc::new(Mutex::new(Vec::default())),
68 }
69 }
70}
71
72impl Parse for SolarParser {
74 fn parse_document(
75 &mut self,
76 input: impl io::Read,
77 path: Option<impl AsRef<Path>>,
78 keep_contents: bool,
79 ) -> Result<ParsedDocument> {
80 fn inner(
81 this: &mut SolarParser,
82 mut input: impl io::Read,
83 path: Option<PathBuf>,
84 keep_contents: bool,
85 ) -> Result<ParsedDocument> {
86 let pathbuf = path.clone().unwrap_or(PathBuf::from("<stdin>"));
87 let mut buf = String::new();
88 input
89 .read_to_string(&mut buf)
90 .map_err(|err| ErrorKind::IOError {
91 path: pathbuf.clone(),
92 err,
93 })?;
94 let source_map = this.sess.source_map();
95 let source_file = source_map
96 .new_source_file(path.map_or(FileName::Stdin, FileName::Real), buf) .map_err(|err| ErrorKind::IOError {
98 path: pathbuf.clone(),
99 err,
100 })?;
101
102 let mut definitions = this
103 .sess
104 .enter_sequential(|| -> solar_parse::interface::Result<_> {
105 let arena = solar_parse::ast::Arena::new();
106
107 let mut parser = Parser::from_source_file(&this.sess, &arena, &source_file);
108
109 let ast = parser.parse_file().map_err(|err| err.emit())?;
110 let mut visitor = LintspecVisitor::new(&this.sess);
111
112 let _ = visitor.visit_source_unit(&ast);
113 Ok(visitor.definitions)
114 })
115 .map_err(|_| {
116 let message = match this.sess.emitted_errors() {
117 Some(Err(diags)) => diags.to_string(),
118 None | Some(Ok(())) => "unknown error".to_string(),
119 };
120 ErrorKind::ParsingError {
121 path: pathbuf,
122 loc: TextIndex::ZERO,
123 message,
124 }
125 })?;
126
127 let document_id = DocumentId::new();
128 if keep_contents {
129 let mut documents = this
130 .documents
131 .lock()
132 .or_panic("mutex should not be poisoned");
133 documents.push((document_id, Arc::clone(&source_file)));
134 }
135 complete_text_ranges(&source_file.src, &mut definitions);
136 Ok(ParsedDocument {
137 definitions,
138 id: document_id,
139 })
140 }
141 inner(
142 self,
143 input,
144 path.map(|p| p.as_ref().to_path_buf()),
145 keep_contents,
146 )
147 }
148
149 fn get_sources(self) -> Result<HashMap<DocumentId, String>> {
150 let sess = Arc::try_unwrap(self.sess).map_err(|_| ErrorKind::DanglingParserReferences)?;
151 drop(sess);
152 Arc::try_unwrap(self.documents)
153 .map_err(|_| ErrorKind::DanglingParserReferences)?
154 .into_inner()
155 .or_panic("mutex should not be poisoned")
156 .into_iter()
157 .map(|(id, doc)| {
158 let source_file =
159 Arc::try_unwrap(doc).map_err(|_| ErrorKind::DanglingParserReferences)?;
160 Ok((
161 id,
162 Arc::try_unwrap(source_file.src)
163 .map_err(|_| ErrorKind::DanglingParserReferences)?,
164 ))
165 })
166 .collect::<Result<HashMap<_, _>>>()
167 }
168}
169
170pub struct LintspecVisitor<'ast> {
174 current_parent: Option<Parent>,
175 definitions: Vec<Definition>,
176 sess: &'ast Session,
177}
178
179impl<'ast> LintspecVisitor<'ast> {
180 pub fn new(sess: &'ast Session) -> Self {
182 Self {
183 current_parent: None,
184 definitions: Vec::default(),
185 sess,
186 }
187 }
188
189 #[must_use]
196 pub fn definitions(&self) -> &Vec<Definition> {
197 &self.definitions
198 }
199
200 fn span_to_textrange(&self, span: Span) -> TextRange {
204 let local_begin = self.sess.source_map().lookup_byte_offset(span.lo());
205 let local_end = self.sess.source_map().lookup_byte_offset(span.hi());
206
207 let start_utf8 = local_begin.pos.to_usize();
208 let end_utf8 = local_end.pos.to_usize();
209
210 let start_index = TextIndex {
211 utf8: start_utf8,
212 ..Default::default()
213 };
214
215 let end_index = TextIndex {
216 utf8: end_utf8,
217 ..Default::default()
218 };
219
220 start_index..end_index
221 }
222}
223
224impl<'ast> Visit<'ast> for LintspecVisitor<'ast> {
225 type BreakValue = ();
226
227 fn visit_item(&mut self, item: &'ast Item<'ast>) -> ControlFlow<Self::BreakValue> {
229 match &item.kind {
230 ItemKind::Contract(item_contract) => {
231 if let Some(def) = item_contract.extract_definition(item, self) {
232 self.definitions.push(def);
233 }
234 self.visit_item_contract(item_contract)?;
235 }
236 ItemKind::Function(item_function) => {
237 if let Some(def) = item_function.extract_definition(item, self) {
238 self.definitions.push(def);
239 }
240 }
241 ItemKind::Variable(var_def) => {
242 if let Some(def) = var_def.extract_definition(item, self) {
243 self.definitions.push(def);
244 }
245 }
246 ItemKind::Struct(item_struct) => {
247 if let Some(def) = item_struct.extract_definition(item, self) {
248 self.definitions.push(def);
249 }
250 }
251 ItemKind::Enum(item_enum) => {
252 if let Some(enum_def) = item_enum.extract_definition(item, self) {
253 self.definitions.push(enum_def);
254 }
255 }
256 ItemKind::Error(item_error) => {
257 if let Some(def) = item_error.extract_definition(item, self) {
258 self.definitions.push(def);
259 }
260 }
261 ItemKind::Event(item_event) => {
262 if let Some(def) = item_event.extract_definition(item, self) {
263 self.definitions.push(def);
264 }
265 }
266 ItemKind::Pragma(_) | ItemKind::Import(_) | ItemKind::Using(_) | ItemKind::Udvt(_) => {}
267 }
268
269 ControlFlow::Continue(())
270 }
271
272 fn visit_item_contract(
274 &mut self,
275 contract: &'ast ItemContract<'ast>,
276 ) -> ControlFlow<Self::BreakValue> {
277 let ItemContract { bases, body, .. } = contract;
278
279 self.current_parent = Some(contract.into());
280
281 for base in bases.iter() {
282 self.visit_modifier(base)?;
283 }
284 for item in body.iter() {
285 self.visit_item(item)?;
286 }
287
288 self.current_parent = None;
289
290 ControlFlow::Continue(())
291 }
292}
293
294trait Extract {
296 fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition>;
297}
298
299impl Extract for &solar_parse::ast::ItemContract<'_> {
300 fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
301 let name = INTERNER.get_or_intern(self.name.as_str());
302
303 let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
304 Ok(extracted) => {
305 let end_bases = self
306 .bases
307 .last()
308 .map_or(self.name.span.hi(), |b| b.span().hi());
309 let end_specifiers = self
310 .layout
311 .as_ref()
312 .map_or(self.name.span.hi(), |l| l.span.hi());
313 let contract_end = end_bases.max(end_specifiers);
314 extracted.map_or_else(
315 || {
316 (
317 None,
318 visitor.span_to_textrange(item.span.with_hi(contract_end)),
319 )
320 },
321 |(natspec, doc_span)| {
322 (
324 Some(natspec),
325 visitor.span_to_textrange(doc_span.with_hi(contract_end)),
326 )
327 },
328 )
329 }
330 Err(e) => return Some(Definition::NatspecParsingError(e.into_inner())),
331 };
332
333 Some(match self.kind {
334 ContractKind::Contract | ContractKind::AbstractContract => ContractDefinition {
335 name,
336 span,
337 natspec,
338 }
339 .into(),
340 ContractKind::Interface => InterfaceDefinition {
341 name,
342 span,
343 natspec,
344 }
345 .into(),
346 ContractKind::Library => LibraryDefinition {
347 name,
348 span,
349 natspec,
350 }
351 .into(),
352 })
353 }
354}
355
356impl Extract for &solar_parse::ast::ItemFunction<'_> {
357 fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
358 let params = variable_definitions_to_identifiers(Some(&self.header.parameters), visitor);
359
360 let returns = variable_definitions_to_identifiers(self.header.returns.as_ref(), visitor);
361 let (natspec, span) = match extract_natspec(&item.docs, visitor, &returns) {
362 Ok(extracted) => extracted.map_or_else(
363 || (None, visitor.span_to_textrange(self.header.span)),
364 |(natspec, doc_span)| {
365 (
367 Some(natspec),
368 visitor.span_to_textrange(doc_span.with_hi(self.header.span.hi())),
369 )
370 },
371 ),
372 Err(e) => return Some(Definition::NatspecParsingError(e.into_inner())),
373 };
374
375 match self.kind {
376 FunctionKind::Constructor => Some(
377 ConstructorDefinition {
378 parent: visitor.current_parent.clone(),
379 span,
380 params,
381 natspec,
382 }
383 .into(),
384 ),
385 FunctionKind::Modifier => Some(
386 ModifierDefinition {
387 parent: visitor.current_parent.clone(),
388 span,
389 params,
390 natspec,
391 name: INTERNER.get_or_intern(
392 self.header.name.as_ref().map_or("modifier", |n| n.as_str()),
393 ),
394 attributes: Attributes {
395 visibility: self.header.visibility.into(),
396 r#override: self.header.override_.is_some(),
397 },
398 }
399 .into(),
400 ),
401 FunctionKind::Function => Some(
402 FunctionDefinition {
403 parent: visitor.current_parent.clone(),
404 name: INTERNER.get_or_intern(
405 self.header.name.as_ref().map_or("function", |n| n.as_str()),
406 ),
407 returns: returns.clone(),
408 attributes: Attributes {
409 visibility: self.header.visibility.into(),
410 r#override: self.header.override_.is_some(),
411 },
412 span,
413 params,
414 natspec,
415 }
416 .into(),
417 ),
418 FunctionKind::Receive | FunctionKind::Fallback => None,
419 }
420 }
421}
422
423impl Extract for &solar_parse::ast::VariableDefinition<'_> {
424 fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
425 let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
426 Ok(extracted) => extracted.map_or_else(
427 || (None, visitor.span_to_textrange(item.span)),
428 |(natspec, doc_span)| {
429 (
430 Some(natspec),
431 visitor.span_to_textrange(doc_span.with_hi(item.span.hi())),
432 )
433 },
434 ),
435 Err(e) => return Some(Definition::NatspecParsingError(e.into_inner())),
436 };
437
438 let attributes = Attributes {
439 visibility: self.visibility.into(),
440 r#override: self.override_.is_some(),
441 };
442
443 Some(
444 VariableDeclaration {
445 parent: visitor.current_parent.clone(),
446 name: INTERNER.get_or_intern(self.name.as_ref().map_or("variable", |n| n.as_str())),
447 span,
448 natspec,
449 attributes,
450 }
451 .into(),
452 )
453 }
454}
455
456impl Extract for &solar_parse::ast::ItemStruct<'_> {
457 fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
458 let name = INTERNER.get_or_intern(self.name.as_str());
459
460 let members = self
461 .fields
462 .iter()
463 .map(|m| Identifier {
464 name: Some(
465 INTERNER.get_or_intern(m.name.as_ref().map_or("member", |n| n.as_str())),
466 ),
467 span: visitor.span_to_textrange(m.span),
468 })
469 .collect();
470
471 let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
472 Ok(extracted) => extracted.map_or_else(
473 || (None, visitor.span_to_textrange(item.span)),
474 |(natspec, doc_span)| {
475 (
476 Some(natspec),
477 visitor.span_to_textrange(doc_span.with_hi(item.span.hi())),
478 )
479 },
480 ),
481 Err(e) => return Some(Definition::NatspecParsingError(e.into_inner())),
482 };
483
484 Some(
485 StructDefinition {
486 parent: visitor.current_parent.clone(),
487 name,
488 span,
489 members,
490 natspec,
491 }
492 .into(),
493 )
494 }
495}
496
497impl Extract for &solar_parse::ast::ItemEnum<'_> {
498 fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
499 let members = self
500 .variants
501 .iter()
502 .map(|v| Identifier {
503 name: Some(INTERNER.get_or_intern(v.name.as_str())),
504 span: visitor.span_to_textrange(v.span),
505 })
506 .collect();
507
508 let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
509 Ok(extracted) => extracted.map_or_else(
510 || (None, visitor.span_to_textrange(item.span)),
511 |(natspec, doc_span)| {
512 (
513 Some(natspec),
514 visitor.span_to_textrange(doc_span.with_hi(item.span.hi())),
515 )
516 },
517 ),
518 Err(e) => return Some(Definition::NatspecParsingError(e.into_inner())),
519 };
520
521 Some(
522 EnumDefinition {
523 parent: visitor.current_parent.clone(),
524 name: INTERNER.get_or_intern(self.name.as_str()),
525 span,
526 members,
527 natspec,
528 }
529 .into(),
530 )
531 }
532}
533
534impl Extract for &solar_parse::ast::ItemError<'_> {
535 fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
536 let params = variable_definitions_to_identifiers(Some(&self.parameters), visitor);
537
538 let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
539 Ok(extracted) => extracted.map_or_else(
540 || (None, visitor.span_to_textrange(item.span)),
541 |(natspec, doc_span)| {
542 (
543 Some(natspec),
544 visitor.span_to_textrange(doc_span.with_hi(item.span.hi())),
545 )
546 },
547 ),
548 Err(e) => return Some(Definition::NatspecParsingError(e.into_inner())),
549 };
550
551 Some(
552 ErrorDefinition {
553 parent: visitor.current_parent.clone(),
554 span,
555 name: INTERNER.get_or_intern(self.name.as_str()),
556 params,
557 natspec,
558 }
559 .into(),
560 )
561 }
562}
563
564impl Extract for &solar_parse::ast::ItemEvent<'_> {
565 fn extract_definition(self, item: &Item, visitor: &mut LintspecVisitor) -> Option<Definition> {
566 let params = variable_definitions_to_identifiers(Some(&self.parameters), visitor);
567
568 let (natspec, span) = match extract_natspec(&item.docs, visitor, &[]) {
569 Ok(extracted) => extracted.map_or_else(
570 || (None, visitor.span_to_textrange(item.span)),
571 |(natspec, doc_span)| {
572 (
573 Some(natspec),
574 visitor.span_to_textrange(doc_span.with_hi(item.span.hi())),
575 )
576 },
577 ),
578 Err(e) => return Some(Definition::NatspecParsingError(e.into_inner())),
579 };
580
581 Some(
582 EventDefinition {
583 parent: visitor.current_parent.clone(),
584 name: INTERNER.get_or_intern(self.name.as_str()),
585 span,
586 params,
587 natspec,
588 }
589 .into(),
590 )
591 }
592}
593
594impl From<&ItemContract<'_>> for Parent {
596 fn from(contract: &ItemContract) -> Self {
597 let name = INTERNER
598 .get_or_intern(contract.name.as_str())
599 .resolve_with(&INTERNER);
600 match contract.kind {
601 ContractKind::Contract | ContractKind::AbstractContract => Parent::Contract(name),
602 ContractKind::Library => Parent::Library(name),
603 ContractKind::Interface => Parent::Interface(name),
604 }
605 }
606}
607
608impl From<Option<Spanned<solar_parse::ast::Visibility>>> for Visibility {
610 fn from(visibility: Option<Spanned<solar_parse::ast::Visibility>>) -> Self {
611 visibility.as_deref().into()
612 }
613}
614
615impl From<Option<solar_parse::ast::Visibility>> for Visibility {
617 fn from(visibility: Option<solar_parse::ast::Visibility>) -> Self {
618 visibility.as_ref().into()
619 }
620}
621
622impl From<Option<&solar_parse::ast::Visibility>> for Visibility {
624 fn from(visibility: Option<&solar_parse::ast::Visibility>) -> Self {
625 match visibility {
626 Some(solar_parse::ast::Visibility::Public) => Visibility::Public,
627 Some(solar_parse::ast::Visibility::Private) => Visibility::Private,
628 Some(solar_parse::ast::Visibility::External) => Visibility::External,
629 Some(solar_parse::ast::Visibility::Internal) | None => Visibility::Internal,
630 }
631 }
632}
633
634fn variable_definitions_to_identifiers(
636 variable_definitions: Option<&ParameterList>,
637 visitor: &mut LintspecVisitor,
638) -> Vec<Identifier> {
639 let Some(variable_definitions) = variable_definitions else {
640 return Vec::new();
641 };
642 variable_definitions
643 .iter()
644 .map(|r: &VariableDefinition<'_>| {
645 if let Some(name) = r.name {
647 Identifier {
648 name: Some(INTERNER.get_or_intern(name.as_str())),
649 span: visitor.span_to_textrange(name.span),
650 }
651 } else {
653 Identifier {
654 name: None,
655 span: visitor.span_to_textrange(r.span),
656 }
657 }
658 })
659 .collect()
660}
661
662fn extract_natspec(
664 docs: &DocComments,
665 visitor: &mut LintspecVisitor,
666 returns: &[Identifier],
667) -> Result<Option<(NatSpec, Span)>> {
668 if docs.is_empty() {
669 return Ok(None);
670 }
671 let mut combined = NatSpec::default();
672
673 for doc in docs.iter() {
674 let snippet = visitor
675 .sess
676 .source_map()
677 .span_to_snippet(doc.span)
678 .map_err(|e| {
679 let path = visitor.sess.source_map().files().first().map_or(
681 PathBuf::from("<stdin>"),
682 |f| {
683 PathBuf::from_str(&f.name.display().to_string())
684 .unwrap_or(PathBuf::from("<unsupported path>"))
685 },
686 );
687 ErrorKind::ParsingError {
688 path,
689 loc: visitor.span_to_textrange(doc.span).start,
690 message: format!("{e:?}"),
691 }
692 })?;
693
694 let mut parsed = parse_comment(&mut snippet.as_str())
695 .map_err(|e| ErrorKind::NatspecParsingError {
696 parent: visitor.current_parent.clone(),
697 span: visitor.span_to_textrange(doc.span),
698 message: e.to_string(),
699 })?
700 .populate_returns(returns);
701 combined.append(&mut parsed);
702 }
703
704 Ok(Some((combined, docs.span())))
705}