Skip to main content

lintspec_core/parser/
solar.rs

1//! A parser with [`solar_parse`] as the backend
2//!
3//! This is the default parser for the CLI.
4use 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
72/// A parser using the [`solar_parse`] crate
73impl 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) // should never fail since the content was read already
97                .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
170/// A custom visitor to extract definitions from the [`solar_parse`] AST
171///
172/// Most of the items are visited using solar's default [`Visit`] implementation.
173pub struct LintspecVisitor<'ast> {
174    current_parent: Option<Parent>,
175    definitions: Vec<Definition>,
176    sess: &'ast Session,
177}
178
179impl<'ast> LintspecVisitor<'ast> {
180    /// Construct a new visitor
181    pub fn new(sess: &'ast Session) -> Self {
182        Self {
183            current_parent: None,
184            definitions: Vec::default(),
185            sess,
186        }
187    }
188
189    /// Retrieve the found definitions
190    ///
191    /// This is empty until the AST has been visited with [`LintspecVisitor::visit_source_unit`].
192    ///
193    /// Note that the spans for each definition and their corresponding members/params/returns only contain the `utf8`
194    /// byte offset but no line/column information. These can be populated with [`complete_text_ranges`].
195    #[must_use]
196    pub fn definitions(&self) -> &Vec<Definition> {
197        &self.definitions
198    }
199
200    /// Convert a [`Span`] to a pair of utf8 offsets as a [`TextRange`]
201    ///
202    /// Only the utf8 offset of the [`TextRange`] is initially populated, the rest being filled via [`complete_text_ranges`] to avoid duplicate work.
203    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    /// Visit an item and extract definitions from it, using the corresponding trait
228    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    // In order to track the parent, we need to maintain a reference to the contract being visited
273    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
294/// Extract each "component" (parent, params, span, etc) then build a [`Definition`] from it
295trait 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                        // If there are natspec in a contract, take the whole doc and contract header as span
323                        (
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                    // If there are natspec in a fn, take the whole doc and fn header as span
366                    (
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
594/// Create a [`Parent`] from solar's [`ItemContract`]
595impl 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
608/// Convert a spanned [`ast::Visibility`][solar_parse::ast::Visibility] into to the corresponding [`Visibility`] type
609impl 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
615/// Convert solar's [`ast::Visibility`][solar_parse::ast::Visibility] into to the corresponding [`Visibility`] type
616impl 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
622/// Convert a reference to [`ast::Visibility`][solar_parse::ast::Visibility] into to the corresponding [`Visibility`] type
623impl 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
634/// Convert a list of [`VariableDefinition`] (used for fn params or returns) into an [`Identifier`]
635fn 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 there is a named variable, we use its span
646            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                // Otherwise, we use the return span
652            } else {
653                Identifier {
654                    name: None,
655                    span: visitor.span_to_textrange(r.span),
656                }
657            }
658        })
659        .collect()
660}
661
662/// Convert solar's [`DocComments`] into a [`NatSpec`] and [`Span`]
663fn 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                // there should only be one file in the source map
680                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}