1use crate::{Lexer, PErr, PResult};
2use smallvec::SmallVec;
3use solar_ast::{
4 self as ast, AstPath, Box, BoxSlice, DocComment, DocComments,
5 token::{Delimiter, Token, TokenKind},
6};
7use solar_data_structures::{BumpExt, fmt::or_list};
8use solar_interface::{
9 BytePos, Ident, Result, Session, Span, Symbol,
10 diagnostics::DiagCtxt,
11 error_code,
12 source_map::{FileName, SourceFile},
13};
14use std::{fmt, path::Path};
15
16mod expr;
17mod item;
18mod lit;
19mod stmt;
20mod ty;
21mod yul;
22
23const PARSER_RECURSION_LIMIT: usize = 128;
25
26#[doc = include_str!("../../doc-examples/parser.rs")]
34pub struct Parser<'sess, 'ast, 'cb> {
36 pub sess: &'sess Session,
38 pub arena: &'ast ast::Arena,
40
41 pub token: Token,
43 pub prev_token: Token,
45 expected_tokens: Vec<ExpectedToken>,
47 last_unexpected_token_span: Option<Span>,
49 docs: Vec<DocComment<'ast>>,
51
52 tokens: std::vec::IntoIter<Token>,
54
55 in_yul: bool,
59 in_contract: bool,
61
62 recursion_depth: usize,
64 #[allow(clippy::type_complexity)]
66 import_callback:
67 Option<std::boxed::Box<dyn FnMut(ast::ItemId, Span, &ast::ImportDirective<'ast>) + 'cb>>,
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
71enum ExpectedToken {
72 Token(TokenKind),
73 Keyword(Symbol),
74 Lit,
75 StrLit,
76 Ident,
77 Path,
78 ElementaryType,
79}
80
81impl fmt::Display for ExpectedToken {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 f.write_str(match self {
84 Self::Token(t) => return write!(f, "`{t}`"),
85 Self::Keyword(kw) => return write!(f, "`{kw}`"),
86 Self::StrLit => "string literal",
87 Self::Lit => "literal",
88 Self::Ident => "identifier",
89 Self::Path => "path",
90 Self::ElementaryType => "elementary type name",
91 })
92 }
93}
94
95impl ExpectedToken {
96 fn to_string_many(tokens: &[Self]) -> String {
97 or_list(tokens).to_string()
98 }
99
100 fn eq_kind(&self, other: TokenKind) -> bool {
101 match *self {
102 Self::Token(kind) => kind == other,
103 _ => false,
104 }
105 }
106}
107
108#[derive(Debug)]
110struct SeqSep {
111 sep: Option<TokenKind>,
113 trailing_sep_allowed: bool,
115 trailing_sep_required: bool,
117}
118
119impl SeqSep {
120 fn trailing_enforced(t: TokenKind) -> Self {
121 Self { sep: Some(t), trailing_sep_required: true, trailing_sep_allowed: true }
122 }
123
124 #[allow(dead_code)]
125 fn trailing_allowed(t: TokenKind) -> Self {
126 Self { sep: Some(t), trailing_sep_required: false, trailing_sep_allowed: true }
127 }
128
129 fn trailing_disallowed(t: TokenKind) -> Self {
130 Self { sep: Some(t), trailing_sep_required: false, trailing_sep_allowed: false }
131 }
132
133 fn none() -> Self {
134 Self { sep: None, trailing_sep_required: false, trailing_sep_allowed: false }
135 }
136}
137
138#[derive(Copy, Clone, Debug, PartialEq, Eq)]
140pub enum Recovered {
141 No,
142 Yes,
143}
144
145impl<'sess, 'ast, 'cb> Parser<'sess, 'ast, 'cb> {
146 pub fn new(sess: &'sess Session, arena: &'ast ast::Arena, tokens: Vec<Token>) -> Self {
148 assert!(sess.is_entered(), "session should be entered before parsing");
149 let mut parser = Self {
150 sess,
151 arena,
152 token: Token::DUMMY,
153 prev_token: Token::DUMMY,
154 expected_tokens: Vec::with_capacity(8),
155 last_unexpected_token_span: None,
156 docs: Vec::with_capacity(4),
157 tokens: tokens.into_iter(),
158 in_yul: false,
159 in_contract: false,
160 recursion_depth: 0,
161 import_callback: None,
162 };
163 parser.bump();
164 parser
165 }
166
167 pub fn set_import_callback(
169 &mut self,
170 import_callback: impl FnMut(ast::ItemId, Span, &ast::ImportDirective<'ast>) + 'cb,
171 ) {
172 self.import_callback = Some(std::boxed::Box::new(import_callback));
173 }
174
175 pub fn from_source_code(
177 sess: &'sess Session,
178 arena: &'ast ast::Arena,
179 filename: FileName,
180 src: impl Into<String>,
181 ) -> Result<Self> {
182 Self::from_lazy_source_code(sess, arena, filename, || Ok(src.into()))
183 }
184
185 pub fn from_file(sess: &'sess Session, arena: &'ast ast::Arena, path: &Path) -> Result<Self> {
189 Self::from_lazy_source_code(sess, arena, FileName::Real(path.to_path_buf()), || {
190 sess.source_map().file_loader().load_file(path)
191 })
192 }
193
194 pub fn from_lazy_source_code(
198 sess: &'sess Session,
199 arena: &'ast ast::Arena,
200 filename: FileName,
201 get_src: impl FnOnce() -> std::io::Result<String>,
202 ) -> Result<Self> {
203 let file = sess
204 .source_map()
205 .new_source_file_with(filename, get_src)
206 .map_err(|e| sess.dcx.err(e.to_string()).emit())?;
207 Ok(Self::from_source_file(sess, arena, &file))
208 }
209
210 pub fn from_source_file(
216 sess: &'sess Session,
217 arena: &'ast ast::Arena,
218 file: &SourceFile,
219 ) -> Self {
220 Self::from_lexer(arena, Lexer::from_source_file(sess, file))
221 }
222
223 pub fn from_lexer(arena: &'ast ast::Arena, lexer: Lexer<'sess, '_>) -> Self {
225 Self::new(lexer.sess, arena, lexer.into_tokens())
226 }
227
228 #[inline]
230 pub fn dcx(&self) -> &'sess DiagCtxt {
231 &self.sess.dcx
232 }
233
234 pub fn alloc<T>(&self, value: T) -> Box<'ast, T> {
236 self.arena.alloc(value)
237 }
238
239 pub fn alloc_path(&self, segments: &[Ident]) -> AstPath<'ast> {
245 AstPath::new_in(self.arena.bump(), segments)
247 }
248
249 pub fn alloc_vec<T>(&self, values: Vec<T>) -> BoxSlice<'ast, T> {
251 self.arena.alloc_vec_thin((), values)
252 }
253
254 pub fn alloc_smallvec<A: smallvec::Array>(
256 &self,
257 values: SmallVec<A>,
258 ) -> BoxSlice<'ast, A::Item> {
259 self.arena.alloc_smallvec_thin((), values)
260 }
261
262 #[inline]
264 #[track_caller]
265 pub fn unexpected<T>(&mut self) -> PResult<'sess, T> {
266 Err(self.unexpected_error())
267 }
268
269 #[cold]
271 #[track_caller]
272 pub fn unexpected_error(&mut self) -> PErr<'sess> {
273 match self.expected_one_of_not_found(&[], &[]) {
274 Ok(b) => unreachable!("`unexpected()` returned Ok({b:?})"),
275 Err(e) => e,
276 }
277 }
278
279 #[inline]
281 #[track_caller]
282 pub fn expect(&mut self, tok: TokenKind) -> PResult<'sess, Recovered> {
283 if self.check_noexpect(tok) {
284 self.bump();
285 Ok(Recovered::No)
286 } else {
287 self.expected_one_of_not_found(std::slice::from_ref(&tok), &[])
288 }
289 }
290
291 #[track_caller]
295 pub fn expect_one_of(
296 &mut self,
297 edible: &[TokenKind],
298 inedible: &[TokenKind],
299 ) -> PResult<'sess, Recovered> {
300 if edible.contains(&self.token.kind) {
301 self.bump();
302 Ok(Recovered::No)
303 } else if inedible.contains(&self.token.kind) {
304 Ok(Recovered::No)
306 } else {
307 self.expected_one_of_not_found(edible, inedible)
308 }
309 }
310
311 #[cold]
312 #[track_caller]
313 fn expected_one_of_not_found(
314 &mut self,
315 edible: &[TokenKind],
316 inedible: &[TokenKind],
317 ) -> PResult<'sess, Recovered> {
318 if self.token.kind != TokenKind::Eof
319 && self.last_unexpected_token_span == Some(self.token.span)
320 {
321 panic!("called unexpected twice on the same token");
322 }
323
324 let mut expected = edible
325 .iter()
326 .chain(inedible)
327 .cloned()
328 .map(ExpectedToken::Token)
329 .chain(self.expected_tokens.iter().cloned())
330 .filter(|token| {
331 fn is_ident_eq_keyword(found: TokenKind, expected: &ExpectedToken) -> bool {
334 if let TokenKind::Ident(current_sym) = found
335 && let ExpectedToken::Keyword(suggested_sym) = expected
336 {
337 return current_sym == *suggested_sym;
338 }
339 false
340 }
341
342 if !token.eq_kind(self.token.kind) {
343 let eq = is_ident_eq_keyword(self.token.kind, token);
344 if !eq {
351 if let ExpectedToken::Token(kind) = token
352 && *kind == self.token.kind
353 {
354 return false;
355 }
356 return true;
357 }
358 }
359 false
360 })
361 .collect::<Vec<_>>();
362 expected.sort_by_cached_key(ToString::to_string);
363 expected.dedup();
364
365 let expect = ExpectedToken::to_string_many(&expected);
366 let actual = self.token.full_description();
367 let (msg_exp, (mut label_span, label_exp)) = match expected.len() {
368 0 => (
369 format!("unexpected token: {actual}"),
370 (self.prev_token.span, "unexpected token after this".to_string()),
371 ),
372 1 => (
373 format!("expected {expect}, found {actual}"),
374 (self.prev_token.span.shrink_to_hi(), format!("expected {expect}")),
375 ),
376 len => {
377 let fmt = format!("expected one of {expect}, found {actual}");
378 let short_expect = if len > 6 { format!("{len} possible tokens") } else { expect };
379 let s = self.prev_token.span.shrink_to_hi();
380 (fmt, (s, format!("expected one of {short_expect}")))
381 }
382 };
383 if self.token.is_eof() {
384 label_span = self.prev_token.span;
386 };
387
388 self.last_unexpected_token_span = Some(self.token.span);
389 let mut err = self.dcx().err(msg_exp).span(self.token.span);
390
391 if self.prev_token.span.is_dummy()
392 || !self
393 .sess
394 .source_map()
395 .is_multiline(self.token.span.shrink_to_hi().until(label_span.shrink_to_lo()))
396 {
397 err = err.span_label(self.token.span, label_exp);
400 } else {
401 err = err.span_label(label_span, label_exp);
402 err = err.span_label(self.token.span, "unexpected token");
403 }
404
405 Err(err)
406 }
407
408 #[inline]
410 #[track_caller]
411 fn expect_semi(&mut self) -> PResult<'sess, ()> {
412 self.expect(TokenKind::Semi).map(drop)
413 }
414
415 #[inline]
420 #[must_use]
421 fn check(&mut self, tok: TokenKind) -> bool {
422 let is_present = self.check_noexpect(tok);
423 if !is_present {
424 self.push_expected(ExpectedToken::Token(tok));
425 }
426 is_present
427 }
428
429 #[inline]
430 #[must_use]
431 fn check_noexpect(&self, tok: TokenKind) -> bool {
432 self.token.kind == tok
433 }
434
435 #[inline]
440 #[must_use]
441 pub fn eat_noexpect(&mut self, tok: TokenKind) -> bool {
442 let is_present = self.check_noexpect(tok);
443 if is_present {
444 self.bump()
445 }
446 is_present
447 }
448
449 #[inline]
451 #[must_use]
452 pub fn eat(&mut self, tok: TokenKind) -> bool {
453 let is_present = self.check(tok);
454 if is_present {
455 self.bump()
456 }
457 is_present
458 }
459
460 #[inline]
463 #[must_use]
464 fn check_keyword(&mut self, kw: Symbol) -> bool {
465 let is_keyword = self.token.is_keyword(kw);
466 if !is_keyword {
467 self.push_expected(ExpectedToken::Keyword(kw));
468 }
469 is_keyword
470 }
471
472 #[inline]
475 #[must_use]
476 pub fn eat_keyword(&mut self, kw: Symbol) -> bool {
477 let is_keyword = self.check_keyword(kw);
478 if is_keyword {
479 self.bump();
480 }
481 is_keyword
482 }
483
484 #[track_caller]
488 fn expect_keyword(&mut self, kw: Symbol) -> PResult<'sess, ()> {
489 if !self.eat_keyword(kw) { self.unexpected() } else { Ok(()) }
490 }
491
492 #[must_use]
493 fn check_ident(&mut self) -> bool {
494 self.check_or_expected(self.token.is_ident(), ExpectedToken::Ident)
495 }
496
497 #[must_use]
498 fn check_nr_ident(&mut self) -> bool {
499 self.check_or_expected(self.token.is_non_reserved_ident(self.in_yul), ExpectedToken::Ident)
500 }
501
502 #[must_use]
503 fn check_path(&mut self) -> bool {
504 self.check_or_expected(self.token.is_ident(), ExpectedToken::Path)
505 }
506
507 #[must_use]
508 fn check_lit(&mut self) -> bool {
509 self.check_or_expected(self.token.is_lit(), ExpectedToken::Lit)
510 }
511
512 #[must_use]
513 fn check_str_lit(&mut self) -> bool {
514 self.check_or_expected(self.token.is_str_lit(), ExpectedToken::StrLit)
515 }
516
517 #[must_use]
518 fn check_elementary_type(&mut self) -> bool {
519 self.check_or_expected(self.token.is_elementary_type(), ExpectedToken::ElementaryType)
520 }
521
522 #[must_use]
523 fn check_or_expected(&mut self, ok: bool, t: ExpectedToken) -> bool {
524 if !ok {
525 self.push_expected(t);
526 }
527 ok
528 }
529
530 fn push_expected(&mut self, expected: ExpectedToken) {
532 self.expected_tokens.push(expected);
533 }
534
535 #[track_caller]
539 #[inline]
540 fn parse_paren_comma_seq<T>(
541 &mut self,
542 allow_empty: bool,
543 f: impl FnMut(&mut Self) -> PResult<'sess, T>,
544 ) -> PResult<'sess, BoxSlice<'ast, T>> {
545 self.parse_delim_comma_seq(Delimiter::Parenthesis, allow_empty, f)
546 }
547
548 #[track_caller]
552 #[inline]
553 fn parse_delim_comma_seq<T>(
554 &mut self,
555 delim: Delimiter,
556 allow_empty: bool,
557 f: impl FnMut(&mut Self) -> PResult<'sess, T>,
558 ) -> PResult<'sess, BoxSlice<'ast, T>> {
559 self.parse_delim_seq(delim, SeqSep::trailing_disallowed(TokenKind::Comma), allow_empty, f)
560 }
561
562 #[track_caller]
565 #[inline]
566 fn parse_nodelim_comma_seq<T>(
567 &mut self,
568 stop: TokenKind,
569 allow_empty: bool,
570 f: impl FnMut(&mut Self) -> PResult<'sess, T>,
571 ) -> PResult<'sess, BoxSlice<'ast, T>> {
572 self.parse_seq_to_before_end(
573 stop,
574 SeqSep::trailing_disallowed(TokenKind::Comma),
575 allow_empty,
576 f,
577 )
578 .map(|(v, _recovered)| v)
579 }
580
581 #[track_caller]
585 #[inline]
586 fn parse_delim_seq<T>(
587 &mut self,
588 delim: Delimiter,
589 sep: SeqSep,
590 allow_empty: bool,
591 f: impl FnMut(&mut Self) -> PResult<'sess, T>,
592 ) -> PResult<'sess, BoxSlice<'ast, T>> {
593 self.parse_unspanned_seq(
594 TokenKind::OpenDelim(delim),
595 TokenKind::CloseDelim(delim),
596 sep,
597 allow_empty,
598 f,
599 )
600 }
601
602 #[track_caller]
606 #[inline]
607 fn parse_unspanned_seq<T>(
608 &mut self,
609 bra: TokenKind,
610 ket: TokenKind,
611 sep: SeqSep,
612 allow_empty: bool,
613 f: impl FnMut(&mut Self) -> PResult<'sess, T>,
614 ) -> PResult<'sess, BoxSlice<'ast, T>> {
615 self.expect(bra)?;
616 self.parse_seq_to_end(ket, sep, allow_empty, f)
617 }
618
619 #[track_caller]
623 #[inline]
624 fn parse_seq_to_end<T>(
625 &mut self,
626 ket: TokenKind,
627 sep: SeqSep,
628 allow_empty: bool,
629 f: impl FnMut(&mut Self) -> PResult<'sess, T>,
630 ) -> PResult<'sess, BoxSlice<'ast, T>> {
631 let (val, recovered) = self.parse_seq_to_before_end(ket, sep, allow_empty, f)?;
632 if recovered == Recovered::No {
633 self.expect(ket)?;
634 }
635 Ok(val)
636 }
637
638 #[track_caller]
642 #[inline]
643 fn parse_seq_to_before_end<T>(
644 &mut self,
645 ket: TokenKind,
646 sep: SeqSep,
647 allow_empty: bool,
648 f: impl FnMut(&mut Self) -> PResult<'sess, T>,
649 ) -> PResult<'sess, (BoxSlice<'ast, T>, Recovered)> {
650 self.parse_seq_to_before_tokens(ket, sep, allow_empty, f)
651 }
652
653 #[track_caller]
657 fn parse_seq_to_before_tokens<T>(
658 &mut self,
659 ket: TokenKind,
660 sep: SeqSep,
661 allow_empty: bool,
662 mut f: impl FnMut(&mut Self) -> PResult<'sess, T>,
663 ) -> PResult<'sess, (BoxSlice<'ast, T>, Recovered)> {
664 let mut first = true;
665 let mut recovered = Recovered::No;
666 let mut trailing = false;
667 let mut v = SmallVec::<[T; 8]>::new();
668
669 if !allow_empty {
670 v.push(f(self)?);
671 first = false;
672 }
673
674 while !self.check(ket) {
675 if self.token.kind == TokenKind::Eof {
676 recovered = Recovered::Yes;
677 break;
678 }
679
680 if let Some(sep_kind) = sep.sep {
681 if first {
682 first = false;
684 } else {
685 let recovered_ = self.expect(sep_kind)?;
687 if recovered_ == Recovered::Yes {
688 recovered = Recovered::Yes;
689 break;
690 }
691
692 if self.check(ket) {
693 trailing = true;
694 break;
695 }
696 }
697 }
698
699 v.push(f(self)?);
700 }
701
702 if let Some(sep_kind) = sep.sep {
703 let open_close_delim = first && allow_empty;
704 if !open_close_delim
705 && sep.trailing_sep_required
706 && !trailing
707 && let Err(e) = self.expect(sep_kind)
708 {
709 e.emit();
710 }
711 if !sep.trailing_sep_allowed && trailing {
712 let msg = format!("trailing `{sep_kind}` separator is not allowed");
713 self.dcx().emit_err(self.prev_token.span, msg);
714 }
715 }
716
717 Ok((self.alloc_smallvec(v), recovered))
718 }
719
720 pub fn bump(&mut self) {
722 let next = self.next_token();
723 if next.is_comment_or_doc() {
724 return self.bump_trivia(next);
725 }
726 self.inlined_bump_with(next);
727 }
728
729 pub fn bump_with(&mut self, next: Token) {
735 self.inlined_bump_with(next);
736 }
737
738 #[inline(always)]
740 fn inlined_bump_with(&mut self, next: Token) {
741 #[cfg(debug_assertions)]
742 if next.is_comment_or_doc() {
743 self.dcx().bug("`bump_with` should not be used with comments").span(next.span).emit();
744 }
745 self.prev_token = std::mem::replace(&mut self.token, next);
746 self.expected_tokens.clear();
747 self.docs.clear();
748 }
749
750 #[cold]
754 fn bump_trivia(&mut self, next: Token) {
755 self.docs.clear();
756
757 debug_assert!(next.is_comment_or_doc());
758 self.prev_token = std::mem::replace(&mut self.token, next);
759 while let Some((is_doc, kind, symbol)) = self.token.comment() {
760 if is_doc {
761 let natspec = if let Some(items) =
762 parse_natspec(self.token.span, symbol, kind, self.in_yul, self.dcx())
763 {
764 self.alloc_smallvec(items)
765 } else {
766 BoxSlice::default()
767 };
768 self.docs.push(DocComment { kind, span: self.token.span, symbol, natspec });
769 }
770 self.token = self.next_token();
772 }
773
774 self.expected_tokens.clear();
775 }
776
777 #[inline(always)]
781 fn next_token(&mut self) -> Token {
782 self.tokens.next().unwrap_or(Token::new(TokenKind::Eof, self.token.span))
783 }
784
785 #[inline]
790 pub fn look_ahead(&self, dist: usize) -> Token {
791 match dist {
793 0 => self.token,
794 1 => self.look_ahead_full(1),
795 2 => self.look_ahead_full(2),
796 dist => self.look_ahead_full(dist),
797 }
798 }
799
800 fn look_ahead_full(&self, dist: usize) -> Token {
801 self.tokens
802 .as_slice()
803 .iter()
804 .copied()
805 .filter(|t| !t.is_comment_or_doc())
806 .nth(dist - 1)
807 .unwrap_or(Token::EOF)
808 }
809
810 #[inline]
814 pub fn look_ahead_with<R>(&self, dist: usize, f: impl FnOnce(Token) -> R) -> R {
815 f(self.look_ahead(dist))
816 }
817
818 #[inline]
820 fn in_contract<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
821 let old = std::mem::replace(&mut self.in_contract, true);
822 let res = f(self);
823 self.in_contract = old;
824 res
825 }
826
827 #[inline]
829 fn in_yul<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
830 let old = std::mem::replace(&mut self.in_yul, true);
831 let res = f(self);
832 self.in_yul = old;
833 res
834 }
835
836 #[inline]
838 pub fn with_recursion_limit<T>(
839 &mut self,
840 context: &str,
841 f: impl FnOnce(&mut Self) -> PResult<'sess, T>,
842 ) -> PResult<'sess, T> {
843 self.recursion_depth += 1;
844 let res = if self.recursion_depth > PARSER_RECURSION_LIMIT {
845 Err(self.recursion_limit_reached(context))
846 } else {
847 f(self)
848 };
849 self.recursion_depth -= 1;
850 res
851 }
852
853 #[cold]
854 fn recursion_limit_reached(&mut self, context: &str) -> PErr<'sess> {
855 let mut err = self.dcx().err("recursion limit reached").span(self.token.span);
856 if !self.prev_token.span.is_dummy() {
857 err = err.span_label(self.prev_token.span, format!("while parsing {context}"));
858 }
859 err
860 }
861}
862
863impl<'sess, 'ast, 'cb> Parser<'sess, 'ast, 'cb> {
865 #[track_caller]
867 pub fn parse_spanned<T>(
868 &mut self,
869 f: impl FnOnce(&mut Self) -> PResult<'sess, T>,
870 ) -> PResult<'sess, (Span, T)> {
871 let lo = self.token.span;
872 let res = f(self);
873 let span = lo.to(self.prev_token.span);
874 match res {
875 Ok(t) => Ok((span, t)),
876 Err(e) if e.span.is_dummy() => Err(e.span(span)),
877 Err(e) => Err(e),
878 }
879 }
880
881 #[inline]
883 pub fn parse_doc_comments(&mut self) -> DocComments<'ast> {
884 if !self.docs.is_empty() { self.parse_doc_comments_inner() } else { Default::default() }
885 }
886
887 #[cold]
888 fn parse_doc_comments_inner(&mut self) -> DocComments<'ast> {
889 assert!(!std::mem::needs_drop::<DocComments<'_>>());
892 let docs = unsafe { self.arena.alloc_thin_slice_unchecked((), &self.docs) };
893 self.docs.clear();
894 docs.into()
895 }
896
897 #[track_caller]
899 pub fn parse_path(&mut self) -> PResult<'sess, AstPath<'ast>> {
900 let first = self.parse_ident()?;
901 self.parse_path_with(first)
902 }
903
904 #[track_caller]
906 pub fn parse_path_with(&mut self, first: Ident) -> PResult<'sess, AstPath<'ast>> {
907 if self.in_yul {
908 self.parse_path_with_f(first, Self::parse_yul_path_ident)
909 } else {
910 self.parse_path_with_f(first, Self::parse_ident)
911 }
912 }
913
914 fn parse_yul_path_ident(&mut self) -> PResult<'sess, Ident> {
916 let ident = self.ident_or_err(true)?;
917 if !ident.is_yul_builtin() && ident.is_reserved(true) {
918 self.expected_ident_found_err().emit();
919 }
920 self.bump();
921 Ok(ident)
922 }
923
924 #[track_caller]
926 pub fn parse_path_any(&mut self) -> PResult<'sess, AstPath<'ast>> {
927 let first = self.parse_ident_any()?;
928 self.parse_path_with_f(first, Self::parse_ident_any)
929 }
930
931 #[track_caller]
933 fn parse_path_with_f(
934 &mut self,
935 first: Ident,
936 mut f: impl FnMut(&mut Self) -> PResult<'sess, Ident>,
937 ) -> PResult<'sess, AstPath<'ast>> {
938 if !self.check_noexpect(TokenKind::Dot) {
939 return Ok(self.alloc_path(&[first]));
940 }
941
942 let mut path = SmallVec::<[_; 4]>::new();
943 path.push(first);
944 while self.eat(TokenKind::Dot) {
945 path.push(f(self)?);
946 }
947 Ok(self.alloc_path(&path))
948 }
949
950 #[track_caller]
952 pub fn parse_ident(&mut self) -> PResult<'sess, Ident> {
953 self.parse_ident_common(true)
954 }
955
956 #[track_caller]
958 pub fn parse_ident_any(&mut self) -> PResult<'sess, Ident> {
959 let ident = self.ident_or_err(true)?;
960 self.bump();
961 Ok(ident)
962 }
963
964 #[track_caller]
966 pub fn parse_ident_opt(&mut self) -> PResult<'sess, Option<Ident>> {
967 if self.check_ident() { self.parse_ident().map(Some) } else { Ok(None) }
968 }
969
970 #[track_caller]
971 fn parse_ident_common(&mut self, recover: bool) -> PResult<'sess, Ident> {
972 let ident = self.ident_or_err(recover)?;
973 if ident.is_reserved(self.in_yul) {
974 let err = self.expected_ident_found_err();
975 if recover {
976 err.emit();
977 } else {
978 return Err(err);
979 }
980 }
981 self.bump();
982 Ok(ident)
983 }
984
985 #[track_caller]
987 fn ident_or_err(&mut self, recover: bool) -> PResult<'sess, Ident> {
988 match self.token.ident() {
989 Some(ident) => Ok(ident),
990 None => self.expected_ident_found(recover),
991 }
992 }
993
994 #[cold]
995 #[track_caller]
996 fn expected_ident_found(&mut self, recover: bool) -> PResult<'sess, Ident> {
997 self.expected_ident_found_other(self.token, recover)
998 }
999
1000 #[cold]
1001 #[track_caller]
1002 fn expected_ident_found_other(&mut self, token: Token, recover: bool) -> PResult<'sess, Ident> {
1003 let msg = format!("expected identifier, found {}", token.full_description());
1004 let span = token.span;
1005 let mut err = self.dcx().err(msg).span(span);
1006
1007 let mut recovered_ident = None;
1008
1009 let suggest_remove_comma = token.kind == TokenKind::Comma && self.look_ahead(1).is_ident();
1010 if suggest_remove_comma {
1011 if recover {
1012 self.bump();
1013 recovered_ident = self.ident_or_err(false).ok();
1014 }
1015 err = err.span_help(span, "remove this comma");
1016 }
1017
1018 if recover && let Some(ident) = recovered_ident {
1019 err.emit();
1020 return Ok(ident);
1021 }
1022 Err(err)
1023 }
1024
1025 #[cold]
1026 #[track_caller]
1027 fn expected_ident_found_err(&mut self) -> PErr<'sess> {
1028 self.expected_ident_found(false).unwrap_err()
1029 }
1030}
1031
1032fn parse_natspec(
1050 comment_span: Span,
1051 comment_symbol: Symbol,
1052 comment_kind: ast::CommentKind,
1053 in_yul: bool,
1054 dcx: &DiagCtxt,
1055) -> Option<SmallVec<[ast::NatSpecItem; 6]>> {
1056 let content = comment_symbol.as_str();
1057 let bytes = content.as_bytes();
1058
1059 if memchr::memchr(b'@', bytes).is_none() {
1061 if content.trim().is_empty() {
1062 return None;
1063 }
1064
1065 let mut items = SmallVec::<[ast::NatSpecItem; 6]>::new();
1067 items.push(ast::NatSpecItem {
1068 kind: ast::NatSpecKind::Notice,
1069 span: comment_span,
1070 symbol: comment_symbol,
1071 content_start: 0,
1072 content_end: content.len() as u32,
1073 });
1074 return Some(items);
1075 }
1076
1077 const PREFIX_BYTES: u32 = 3;
1079 let (mut line_start, mut content_start, mut span, mut kind) = (0, 0usize, None, None);
1080 let mut items = SmallVec::<[ast::NatSpecItem; 6]>::new();
1081
1082 fn flush_item(
1083 items: &mut SmallVec<[ast::NatSpecItem; 6]>,
1084 kind: &mut Option<ast::NatSpecKind>,
1085 span: &mut Option<Span>,
1086 symbol: Symbol,
1087 content_start: usize,
1088 content_end: usize,
1089 ) {
1090 if let Some(k) = kind.take() {
1091 items.push(ast::NatSpecItem {
1092 span: span.take().unwrap(),
1093 kind: k,
1094 symbol,
1095 content_start: content_start as u32,
1096 content_end: content_end as u32,
1097 });
1098 }
1099 }
1100
1101 let is_line_start = |line_start: usize, at_pos: usize| {
1103 match comment_kind {
1104 ast::CommentKind::Line => bytes[line_start..at_pos].trim_ascii().is_empty(),
1106
1107 ast::CommentKind::Block => {
1109 let trimmed = &bytes[line_start..at_pos].trim_ascii_start();
1110 trimmed.is_empty()
1111 || (trimmed.starts_with(b"*") && trimmed[1..].trim_ascii_end().is_empty())
1112 }
1113 }
1114 };
1115
1116 let mut prev_line_end = 0;
1118 for line_end in memchr::memchr_iter(b'\n', bytes).chain(std::iter::once(bytes.len())) {
1119 if let Some(tag_offset) = memchr::memchr(b'@', &bytes[line_start..line_end])
1120 && is_line_start(line_start, line_start + tag_offset)
1121 {
1122 let tag_start = line_start + tag_offset + 1;
1123 flush_item(
1124 &mut items,
1125 &mut kind,
1126 &mut span,
1127 comment_symbol,
1128 content_start,
1129 prev_line_end,
1130 );
1131
1132 let tag_slice = &bytes[tag_start..line_end];
1134 let trimmed = tag_slice.len() - tag_slice.trim_ascii_start().len();
1135 let (tag, rest_start) =
1136 crate::natspec::split_once_ws(content, tag_start + trimmed, line_end);
1137
1138 let tag_lo =
1140 comment_span.lo().0 + PREFIX_BYTES + 1 + (line_start + tag_offset + trimmed) as u32; let tag_hi = tag_lo + tag.len() as u32;
1142 span = Some(Span::new(BytePos(tag_lo), BytePos(tag_hi)));
1143 content_start = rest_start;
1144
1145 kind = Some(match tag {
1146 "title" => ast::NatSpecKind::Title,
1147 "author" => ast::NatSpecKind::Author,
1148 "notice" => ast::NatSpecKind::Notice,
1149 "dev" => ast::NatSpecKind::Dev,
1150 "param" | "inheritdoc" => {
1151 let (name, content_start_pos) =
1152 crate::natspec::split_once_ws(content, rest_start, line_end);
1153 content_start = content_start_pos;
1154 let ident = Ident::new(Symbol::intern(name), comment_span);
1155 match tag {
1156 "param" => ast::NatSpecKind::Param { name: ident },
1157 "inheritdoc" => ast::NatSpecKind::Inheritdoc { contract: ident },
1158 _ => unreachable!(),
1159 }
1160 }
1161 "return" => ast::NatSpecKind::Return { name: None },
1162 _ => {
1163 if let Some(custom_tag) = tag.strip_prefix("custom:") {
1164 let ident = Ident::new(Symbol::intern(custom_tag), comment_span);
1165 ast::NatSpecKind::Custom { name: ident }
1166 } else if ast::NATSPEC_INTERNAL_TAGS[..].contains(&tag) {
1167 let ident = Ident::new(Symbol::intern(tag), comment_span);
1168 ast::NatSpecKind::Internal { tag: ident }
1169 } else {
1170 if !in_yul {
1172 dcx
1173 .warn(format!("invalid natspec tag '@{tag}', custom tags must use format '@custom:name'"))
1174 .code(error_code!(6546))
1175 .span(comment_span)
1176 .emit();
1177 }
1178 line_start = line_end + 1;
1179 prev_line_end = line_end;
1180 continue;
1181 }
1182 }
1183 });
1184 }
1185
1186 prev_line_end = line_end;
1187 line_start = line_end + 1;
1188 }
1189 flush_item(&mut items, &mut kind, &mut span, comment_symbol, content_start, bytes.len());
1190 Some(items)
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 use super::*;
1196 use solar_interface::{Session, SourceMap};
1197
1198 fn check_natspec_item(
1199 sm: &SourceMap,
1200 symbol: Symbol,
1201 item: &ast::NatSpecItem,
1202 snip: &str,
1203 kind: &str,
1204 name: Option<&str>,
1205 content: Option<&str>,
1206 ) {
1207 assert_eq!(sm.span_to_snippet(item.span).unwrap(), snip);
1208
1209 let actual_name = match &item.kind {
1210 ast::NatSpecKind::Title if kind == "title" => None,
1211 ast::NatSpecKind::Author if kind == "author" => None,
1212 ast::NatSpecKind::Notice if kind == "notice" => None,
1213 ast::NatSpecKind::Dev if kind == "dev" => None,
1214 ast::NatSpecKind::Param { name } if kind == "param" => Some(name.name.as_str()),
1215 ast::NatSpecKind::Return { .. } if kind == "return" => None,
1216 ast::NatSpecKind::Inheritdoc { contract } if kind == "inheritdoc" => {
1217 Some(contract.name.as_str())
1218 }
1219 ast::NatSpecKind::Custom { name } if kind == "custom" => Some(name.name.as_str()),
1220 ast::NatSpecKind::Internal { tag } if kind == "internal" => Some(tag.name.as_str()),
1221 _ => panic!("kind mismatch: expected {kind}, got {:?}", item.kind),
1222 };
1223 assert_eq!(actual_name, name);
1224
1225 if let Some(expected) = content {
1226 let actual = &symbol.as_str()[item.content_start as usize..item.content_end as usize];
1227 assert_eq!(actual.trim(), expected.trim());
1228 }
1229 }
1230
1231 #[test]
1232 fn parse_file_import_callback() {
1233 let src = r#"
1234import "a.sol";
1235contract C {}
1236import * as B from "b.sol";
1237"#;
1238
1239 let sess =
1240 Session::builder().with_buffer_emitter(Default::default()).single_threaded().build();
1241 sess.enter_sequential(|| {
1242 let arena = ast::Arena::new();
1243 let mut imports = Vec::new();
1244 let mut parser =
1245 Parser::from_source_code(&sess, &arena, "test.sol".to_string().into(), src)
1246 .expect("failed to create parser");
1247
1248 parser.set_import_callback(|id, span, import| {
1249 imports.push((id, span, import.path.value.as_str().to_string()));
1250 });
1251 let ast = parser.parse_file().expect("failed to parse file");
1252 drop(parser);
1253
1254 assert_eq!(ast.items.len(), 3);
1255 assert_eq!(imports.len(), 2);
1256 assert_eq!(imports[0].0, ast::ItemId::new(0));
1257 assert_eq!(imports[0].2, "a.sol");
1258 assert_eq!(imports[1].0, ast::ItemId::new(2));
1259 assert_eq!(imports[1].2, "b.sol");
1260 assert_eq!(
1261 sess.source_map().span_to_snippet(imports[0].1).unwrap(),
1262 r#"import "a.sol";"#
1263 );
1264 });
1265 }
1266
1267 #[test]
1268 fn parse_natspec_line_cmnts() {
1269 let src = r#"
1270/// @title MyContract
1271/// @author Alice
1272/// @notice This is a notice
1273/// that spans multiple lines
1274/// and continues here
1275/// @dev This is dev documentation
1276/// @param x The input parameter
1277/// @return result The return value
1278/// @inheritdoc BaseContract
1279/// @custom:security High priority
1280/// @solidity memory-safe
1281/// @ notice with space
1282"#;
1283
1284 let sess =
1285 Session::builder().with_buffer_emitter(Default::default()).single_threaded().build();
1286 sess.enter_sequential(|| {
1287 let arena = ast::Arena::new();
1288 let mut parser = Parser::from_source_code(&sess, &arena, "test.sol".to_string().into(), src)
1289 .expect("failed to create parser");
1290
1291 let sm = sess.source_map();
1292 let docs = parser.parse_doc_comments();
1293
1294 let natspec_items: Vec<_> = docs.iter().flat_map(|d| d.natspec.iter().map(move |i| (d.symbol, i))).collect();
1295 assert_eq!(natspec_items.len(), 12);
1296
1297 let check = |i: usize, snip, kind, name, content| {
1298 check_natspec_item(sm, natspec_items[i].0, natspec_items[i].1, snip, kind, name, content)
1299 };
1300
1301 check(0, "title", "title", None, Some("MyContract"));
1302 check(1, "author", "author", None, Some("Alice"));
1303 check(2, "notice", "notice", None, Some("This is a notice"));
1304 let span3 = sm.span_to_snippet(natspec_items[3].1.span).unwrap();
1305 check(3, &span3, "notice", None, Some("that spans multiple lines"));
1306 let span4 = sm.span_to_snippet(natspec_items[4].1.span).unwrap();
1307 check(4, &span4, "notice", None, Some("and continues here"));
1308 check(5, "dev", "dev", None, Some("This is dev documentation"));
1309 check(6, "param", "param", Some("x"), Some("The input parameter"));
1310 check(7, "return", "return", None, Some("result The return value"));
1311 check(8, "inheritdoc", "inheritdoc", Some("BaseContract"), Some(""));
1312 check(9, "custom:security", "custom", Some("security"), Some("High priority"));
1313 check(10, "solidity", "internal", Some("solidity"), Some("memory-safe"));
1314 check(11, "notice", "notice", None, Some("with space"));
1315
1316 assert_eq!(sm.span_to_snippet(docs.span()).unwrap(), "/// @title MyContract\n/// @author Alice\n/// @notice This is a notice\n/// that spans multiple lines\n/// and continues here\n/// @dev This is dev documentation\n/// @param x The input parameter\n/// @return result The return value\n/// @inheritdoc BaseContract\n/// @custom:security High priority\n/// @solidity memory-safe\n/// @ notice with space");
1317 });
1318 }
1319
1320 #[test]
1321 fn parse_natspec_block_cmnts() {
1322 let src = r#"
1323/**
1324 * @title MyContract
1325 * @author Alice
1326 * @notice This is a notice
1327 * that spans multiple lines
1328 * and continues here
1329 * @dev This is dev documentation
1330 * @param x The input parameter
1331 * @return result The return value
1332 * @inheritdoc BaseContract
1333 * @custom:security High priority
1334 * @src 0:123:456
1335 */
1336"#;
1337
1338 let sess =
1339 Session::builder().with_buffer_emitter(Default::default()).single_threaded().build();
1340 sess.enter_sequential(|| {
1341 let arena = ast::Arena::new();
1342 let mut parser =
1343 Parser::from_source_code(&sess, &arena, "test.sol".to_string().into(), src)
1344 .expect("failed to create parser");
1345
1346 let sm = sess.source_map();
1347 let docs = parser.parse_doc_comments();
1348 assert_eq!(docs.len(), 1);
1349
1350 let (sym, items) = (docs[0].symbol, &docs[0].natspec);
1351 assert_eq!(items.len(), 9);
1352
1353 let check = |i: usize, span, kind, name, content| {
1354 check_natspec_item(sm, sym, &items[i], span, kind, name, content)
1355 };
1356
1357 check(0, "title", "title", None, Some("MyContract"));
1358 check(1, "author", "author", None, Some("Alice"));
1359 check(2, "notice", "notice", None, Some("This is a notice\n * that spans multiple lines\n * and continues here"));
1360 check(3, "dev", "dev", None, Some("This is dev documentation"));
1361 check(4, "param", "param", Some("x"), Some("The input parameter"));
1362 check(5, "return", "return", None, Some("result The return value"));
1363 check(6, "inheritdoc", "inheritdoc", Some("BaseContract"), Some(""));
1364 check(7, "custom:security", "custom", Some("security"), Some("High priority"));
1365 check(8, "src", "internal", Some("src"), Some("0:123:456"));
1366
1367 assert_eq!(sm.span_to_snippet(docs.span()).unwrap(), "/**\n * @title MyContract\n * @author Alice\n * @notice This is a notice\n * that spans multiple lines\n * and continues here\n * @dev This is dev documentation\n * @param x The input parameter\n * @return result The return value\n * @inheritdoc BaseContract\n * @custom:security High priority\n * @src 0:123:456\n */");
1368 });
1369 }
1370
1371 #[test]
1372 fn parse_natspec_line_cmnts_no_tags() {
1373 let src = r#"
1374/// This is a simple comment
1375/// It has no tags at all
1376/// Just plain documentation
1377contract Test {}
1378"#;
1379
1380 let sess =
1381 Session::builder().with_buffer_emitter(Default::default()).single_threaded().build();
1382 sess.enter_sequential(|| {
1383 let arena = ast::Arena::new();
1384 let mut parser =
1385 Parser::from_source_code(&sess, &arena, "test.sol".to_string().into(), src)
1386 .expect("failed to create parser");
1387
1388 let sm = sess.source_map();
1389 let docs = parser.parse_doc_comments();
1390 assert_eq!(docs.len(), 3);
1391
1392 for (doc, expected) in docs.iter().zip([
1393 "This is a simple comment",
1394 "It has no tags at all",
1395 "Just plain documentation",
1396 ]) {
1397 assert_eq!(doc.natspec.len(), 1);
1398 let item = &doc.natspec[0];
1399 let span = sm.span_to_snippet(item.span).unwrap();
1400 check_natspec_item(sm, doc.symbol, item, &span, "notice", None, Some(expected));
1401 }
1402 });
1403 }
1404
1405 #[test]
1406 fn parse_natspec_block_cmnt_no_tags() {
1407 let src = r#"
1408/**
1409 * This is a block comment
1410 * with multiple lines
1411 * but no tags at all
1412 */
1413contract Test {}
1414"#;
1415
1416 let sess =
1417 Session::builder().with_buffer_emitter(Default::default()).single_threaded().build();
1418 sess.enter_sequential(|| {
1419 let arena = ast::Arena::new();
1420 let mut parser =
1421 Parser::from_source_code(&sess, &arena, "test.sol".to_string().into(), src)
1422 .expect("failed to create parser");
1423
1424 let sm = sess.source_map();
1425 let docs = parser.parse_doc_comments();
1426 assert_eq!(docs.len(), 1);
1427 assert_eq!(docs[0].natspec.len(), 1);
1428
1429 let item = &docs[0].natspec[0];
1430 let snip = sm.span_to_snippet(item.span).unwrap();
1431 check_natspec_item(
1432 sm,
1433 docs[0].symbol,
1434 item,
1435 &snip,
1436 "notice",
1437 None,
1438 Some("* This is a block comment\n * with multiple lines\n * but no tags at all"),
1439 );
1440 });
1441 }
1442}