Skip to main content

solar_parse/parser/
mod.rs

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
23/// Maximum allowed recursive descent depth for selected parser entry points.
24const PARSER_RECURSION_LIMIT: usize = 128;
25
26/// Solidity and Yul parser.
27///
28/// # Examples
29///
30/// ```
31/// # mod solar { pub use {solar_ast as ast, solar_interface as interface, solar_parse as parse}; }
32/// # fn main() {}
33#[doc = include_str!("../../doc-examples/parser.rs")]
34/// ```
35pub struct Parser<'sess, 'ast, 'cb> {
36    /// The parser session.
37    pub sess: &'sess Session,
38    /// The arena where the AST nodes are allocated.
39    pub arena: &'ast ast::Arena,
40
41    /// The current token.
42    pub token: Token,
43    /// The previous token.
44    pub prev_token: Token,
45    /// List of expected tokens. Cleared after each `bump` call.
46    expected_tokens: Vec<ExpectedToken>,
47    /// The span of the last unexpected token.
48    last_unexpected_token_span: Option<Span>,
49    /// The current doc-comments.
50    docs: Vec<DocComment<'ast>>,
51
52    /// The token stream.
53    tokens: std::vec::IntoIter<Token>,
54
55    /// Whether the parser is in Yul mode.
56    ///
57    /// Currently, this can only happen when parsing a Yul "assembly" block.
58    in_yul: bool,
59    /// Whether the parser is currently parsing a contract block.
60    in_contract: bool,
61
62    /// Current recursion depth for recursive parsing operations.
63    recursion_depth: usize,
64    /// Callback invoked after a top-level import directive is parsed.
65    #[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/// A sequence separator.
109#[derive(Debug)]
110struct SeqSep {
111    /// The separator token.
112    sep: Option<TokenKind>,
113    /// `true` if a trailing separator is allowed.
114    trailing_sep_allowed: bool,
115    /// `true` if a trailing separator is required.
116    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/// Indicates whether the parser took a recovery path and continued.
139#[derive(Copy, Clone, Debug, PartialEq, Eq)]
140pub enum Recovered {
141    No,
142    Yes,
143}
144
145impl<'sess, 'ast, 'cb> Parser<'sess, 'ast, 'cb> {
146    /// Creates a new parser.
147    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    /// Sets the import callback.
168    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    /// Creates a new parser from a source code string.
176    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    /// Creates a new parser from a file.
186    ///
187    /// The file will not be read if it has already been added into the source map.
188    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    /// Creates a new parser from a source code closure.
195    ///
196    /// The closure will not be called if the file name has already been added into the source map.
197    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    /// Creates a new parser from a source file.
211    ///
212    /// Note that the source file must be added to the source map before calling this function.
213    /// Prefer using [`from_source_code`](Self::from_source_code) or [`from_file`](Self::from_file)
214    /// instead.
215    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    /// Creates a new parser from a lexer.
224    pub fn from_lexer(arena: &'ast ast::Arena, lexer: Lexer<'sess, '_>) -> Self {
225        Self::new(lexer.sess, arena, lexer.into_tokens())
226    }
227
228    /// Returns the diagnostic context.
229    #[inline]
230    pub fn dcx(&self) -> &'sess DiagCtxt {
231        &self.sess.dcx
232    }
233
234    /// Allocates an object on the AST arena.
235    pub fn alloc<T>(&self, value: T) -> Box<'ast, T> {
236        self.arena.alloc(value)
237    }
238
239    /// Allocates a list of objects on the AST arena.
240    ///
241    /// # Panics
242    ///
243    /// Panics if the list is empty.
244    pub fn alloc_path(&self, segments: &[Ident]) -> AstPath<'ast> {
245        // SAFETY: `Ident` is `Copy`.
246        AstPath::new_in(self.arena.bump(), segments)
247    }
248
249    /// Allocates a list of objects on the AST arena.
250    pub fn alloc_vec<T>(&self, values: Vec<T>) -> BoxSlice<'ast, T> {
251        self.arena.alloc_vec_thin((), values)
252    }
253
254    /// Allocates a list of objects on the AST arena.
255    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    /// Returns an "unexpected token" error in a [`PResult`] for the current token.
263    #[inline]
264    #[track_caller]
265    pub fn unexpected<T>(&mut self) -> PResult<'sess, T> {
266        Err(self.unexpected_error())
267    }
268
269    /// Returns an "unexpected token" error for the current token.
270    #[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    /// Expects and consumes the token `t`. Signals an error if the next token is not `t`.
280    #[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    /// Expect next token to be edible or inedible token. If edible,
292    /// then consume it; if inedible, then return without consuming
293    /// anything. Signal a fatal error if next token is unexpected.
294    #[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            // leave it in the input
305            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                // Filter out suggestions that suggest the same token
332                // which was found and deemed incorrect.
333                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 the suggestion is a keyword and the found token is an ident,
345                    // the content of which are equal to the suggestion's content,
346                    // we can remove that suggestion (see the `return false` below).
347
348                    // If this isn't the case however, and the suggestion is a token the
349                    // content of which is the same as the found token's, we remove it as well.
350                    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            // This is EOF; don't want to point at the following char, but rather the last token.
385            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            // When the spans are in the same line, it means that the only content between
398            // them is whitespace, point at the found token in that case.
399            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    /// Expects and consumes a semicolon.
409    #[inline]
410    #[track_caller]
411    fn expect_semi(&mut self) -> PResult<'sess, ()> {
412        self.expect(TokenKind::Semi).map(drop)
413    }
414
415    /// Checks if the next token is `tok`, and returns `true` if so.
416    ///
417    /// This method will automatically add `tok` to `expected_tokens` if `tok` is not
418    /// encountered.
419    #[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    /// Consumes a token 'tok' if it exists. Returns whether the given token was present.
436    ///
437    /// the main purpose of this function is to reduce the cluttering of the suggestions list
438    /// which using the normal eat method could introduce in some cases.
439    #[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    /// Consumes a token 'tok' if it exists. Returns whether the given token was present.
450    #[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    /// If the next token is the given keyword, returns `true` without eating it.
461    /// An expectation is also added for diagnostics purposes.
462    #[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    /// If the next token is the given keyword, eats it and returns `true`.
473    /// Otherwise, returns `false`. An expectation is also added for diagnostics purposes.
474    #[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    /// If the given word is not a keyword, signals an error.
485    /// If the next token is not the given word, signals an error.
486    /// Otherwise, eats it.
487    #[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    // #[inline(never)]
531    fn push_expected(&mut self, expected: ExpectedToken) {
532        self.expected_tokens.push(expected);
533    }
534
535    /// Parses a comma-separated sequence delimited by parentheses (e.g. `(x, y)`).
536    /// The function `f` must consume tokens until reaching the next separator or
537    /// closing bracket.
538    #[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    /// Parses a comma-separated sequence, including both delimiters.
549    /// The function `f` must consume tokens until reaching the next separator or
550    /// closing bracket.
551    #[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    /// Parses a comma-separated sequence.
563    /// The function `f` must consume tokens until reaching the next separator.
564    #[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    /// Parses a `sep`-separated sequence, including both delimiters.
582    /// The function `f` must consume tokens until reaching the next separator or
583    /// closing bracket.
584    #[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    /// Parses a sequence, including both delimiters. The function
603    /// `f` must consume tokens until reaching the next separator or
604    /// closing bracket.
605    #[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    /// Parses a sequence, including only the closing delimiter. The function
620    /// `f` must consume tokens until reaching the next separator or
621    /// closing bracket.
622    #[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    /// Parses a sequence, not including the delimiters. The function
639    /// `f` must consume tokens until reaching the next separator or
640    /// closing bracket.
641    #[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    /// Parses a sequence until the specified delimiters. The function
654    /// `f` must consume tokens until reaching the next separator or
655    /// closing bracket.
656    #[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                    // no separator for the first element
683                    first = false;
684                } else {
685                    // check for separator
686                    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    /// Advance the parser by one token.
721    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    /// Advance the parser by one token using provided token as the next one.
730    ///
731    /// # Panics
732    ///
733    /// Panics if the provided token is a comment.
734    pub fn bump_with(&mut self, next: Token) {
735        self.inlined_bump_with(next);
736    }
737
738    /// This always-inlined version should only be used on hot code paths.
739    #[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    /// Bumps comments and docs.
751    ///
752    /// Pushes docs to `self.docs`. Retrieve them with `parse_doc_comments`.
753    #[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            // Don't set `prev_token` on purpose.
771            self.token = self.next_token();
772        }
773
774        self.expected_tokens.clear();
775    }
776
777    /// Advances the internal `tokens` iterator, without updating the parser state.
778    ///
779    /// Use [`bump`](Self::bump) and [`token`](Self::token) instead.
780    #[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    /// Returns the token `dist` tokens ahead of the current one.
786    ///
787    /// [`Eof`](Token::EOF) will be returned if the look-ahead is any distance past the end of the
788    /// tokens.
789    #[inline]
790    pub fn look_ahead(&self, dist: usize) -> Token {
791        // Specialize for the common `dist` cases.
792        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    /// Calls `f` with the token `dist` tokens ahead of the current one.
811    ///
812    /// See [`look_ahead`](Self::look_ahead) for more information.
813    #[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    /// Runs `f` with the parser in a contract context.
819    #[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    /// Runs `f` with the parser in a Yul context.
828    #[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    /// Runs `f` with recursion depth tracking and limit enforcement.
837    #[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
863/// Common parsing methods.
864impl<'sess, 'ast, 'cb> Parser<'sess, 'ast, 'cb> {
865    /// Provides a spanned parser.
866    #[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    /// Parses contiguous doc comments. Can be empty.
882    #[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        // SAFETY: Doesn't have `Drop` and we clear right after to pass ownership to the caller.
890        // We use this to avoid deallocating the vector's memory.
891        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    /// Parses a qualified identifier: `foo.bar.baz`.
898    #[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    /// Parses a qualified identifier starting with the given identifier.
905    #[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    /// Parses either an identifier or a Yul EVM builtin.
915    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    /// Parses a qualified identifier: `foo.bar.baz`.
925    #[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    /// Parses a qualified identifier starting with the given identifier.
932    #[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    /// Parses an identifier.
951    #[track_caller]
952    pub fn parse_ident(&mut self) -> PResult<'sess, Ident> {
953        self.parse_ident_common(true)
954    }
955
956    /// Parses an identifier. Does not check if the identifier is a reserved keyword.
957    #[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    /// Parses an optional identifier.
965    #[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    /// Returns Ok if the current token is an identifier. Does not advance the parser.
986    #[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
1032// Default @notice behavior:
1033// - Per Solidity spec, doc comments without any `@` tags default to `@notice`.
1034//
1035// We use a simplified approach compared to Solc's NatSpec parsing.
1036//
1037// Solc implementation notes:
1038// - Reference: https://github.com/ethereum/solidity/blob/develop/libsolidity/analysis/DocStringTagParser.cpp
1039// - Parses, validates, and stores tag content directly when parsing.
1040// - Lexer merges consecutive line comments (`///`) into a single doc comment token.
1041//
1042// Our implementation:
1043// - Each `///` line is a separate doc comment (not merged by lexer). Because of that, each line
1044//   without tags becomes an individual `@notice` item.
1045// - Defers validation to lowering phase.
1046// - Follows Solc's Yul behavior: silently ignores unknown tags in Yul context https://github.com/argotorg/solidity/blob/2ca5fb3b6adcb1a8fb2c0904fb37526121cf2c72/libyul/AsmParser.cpp#L151
1047
1048/// Parses NatSpec items from a single doc comment.
1049fn 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    // Early-exit if no tag is found.
1060    if memchr::memchr(b'@', bytes).is_none() {
1061        if content.trim().is_empty() {
1062            return None;
1063        }
1064
1065        // Create a synthetic @notice tag for the entire comment
1066        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    // Line comments: '///', Block comments: '/**'.
1078    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    // Check if '@' is located at the logical start of the line.
1102    let is_line_start = |line_start: usize, at_pos: usize| {
1103        match comment_kind {
1104            // can only be followed by whitespace
1105            ast::CommentKind::Line => bytes[line_start..at_pos].trim_ascii().is_empty(),
1106
1107            // can only be followed by whitespaces + ('*') + whitespaces
1108            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    // Iterate over each line and look for tags.
1117    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            // Skip leading whitespace after '@'
1133            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            // Calculate span: from first non-whitespace char after '@' to end of tag name.
1139            let tag_lo =
1140                comment_span.lo().0 + PREFIX_BYTES + 1 + (line_start + tag_offset + trimmed) as u32; // +1 for '@'
1141            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                        // Emit error for invalid solidity tags, but ignore in Yul.
1171                        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}