Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 57 additions & 10 deletions crates/ast/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,34 +81,35 @@ impl std::ops::Deref for Arena {

/// A list of doc-comments.
#[derive(Default)]
pub struct DocComments<'ast>(pub Box<'ast, [DocComment]>);
pub struct DocComments<'ast> {
/// The raw doc comments.
pub comments: Box<'ast, [DocComment]>,
/// The parsed Natspec, if it exists.
pub natspec: Option<NatSpec<'ast>>,
}

impl<'ast> std::ops::Deref for DocComments<'ast> {
type Target = Box<'ast, [DocComment]>;

#[inline]
fn deref(&self) -> &Self::Target {
&self.0
&self.comments
}
}

impl std::ops::DerefMut for DocComments<'_> {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl<'ast> From<Box<'ast, [DocComment]>> for DocComments<'ast> {
fn from(comments: Box<'ast, [DocComment]>) -> Self {
Self(comments)
&mut self.comments
}
}

impl fmt::Debug for DocComments<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("DocComments")?;
self.0.fmt(f)
self.comments.fmt(f)?;
f.write_str("\nNatSpec")?;
self.natspec.fmt(f)
}
}

Expand All @@ -119,6 +120,52 @@ impl DocComments<'_> {
}
}

/// A Natspec documentation block.
#[derive(Debug, Default)]
pub struct NatSpec<'ast> {
pub span: Span,
pub items: Box<'ast, [NatSpecItem]>,
}

impl<'ast> std::ops::Deref for NatSpec<'ast> {
type Target = Box<'ast, [NatSpecItem]>;

#[inline]
fn deref(&self) -> &Self::Target {
&self.items
}
}

impl std::ops::DerefMut for NatSpec<'_> {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.items
}
}

/// A single item within a Natspec comment block.
#[derive(Clone, Copy, Debug)]
pub struct NatSpecItem {
pub span: Span,
pub kind: NatSpecKind,
pub content: Symbol,
}

/// The kind of a `NatSpec` item.
///
/// Reference: <https://docs.soliditylang.org/en/latest/natspec-format.html#tags>
#[derive(Clone, Copy, Debug)]
pub enum NatSpecKind {
Title,
Author,
Notice,
Dev,
Param { tag: Ident },
Return { tag: Ident },
Inheritdoc { tag: Ident },
Custom { tag: Ident },
}

/// A single doc-comment: `/// foo`, `/** bar */`.
#[derive(Clone, Copy, Debug)]
pub struct DocComment {
Expand Down
21 changes: 20 additions & 1 deletion crates/ast/src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,9 +623,13 @@ declare_visitors! {
}

fn visit_doc_comments(&mut self, doc_comments: &'ast #mut DocComments<'ast>) -> ControlFlow<Self::BreakValue> {
for doc_comment in doc_comments.iter #_mut() {
let DocComments { comments, natspec } = doc_comments;
for doc_comment in comments.iter #_mut() {
self.visit_doc_comment #_mut(doc_comment)?;
}
if let Some(natspec) = natspec {
self.visit_natspec #_mut(natspec)?;
}
ControlFlow::Continue(())
}

Expand All @@ -635,6 +639,21 @@ declare_visitors! {
ControlFlow::Continue(())
}

fn visit_natspec(&mut self, natspec: &'ast #mut NatSpec<'ast>) -> ControlFlow<Self::BreakValue> {
let NatSpec {span, items} = natspec;
self.visit_span #_mut(span)?;
for item in items.iter #_mut() {
self.visit_natspec_item #_mut(item)?;
}
ControlFlow::Continue(())
}

fn visit_natspec_item(&mut self, item: &'ast #mut NatSpecItem) -> ControlFlow<Self::BreakValue> {
let NatSpecItem { span, kind: _, content: _ } = item;
self.visit_span #_mut(span)?;
ControlFlow::Continue(())
}

fn visit_path(&mut self, path: &'ast #mut PathSlice) -> ControlFlow<Self::BreakValue> {
for ident in path.segments #_mut() {
self.visit_ident #_mut(ident)?;
Expand Down
113 changes: 111 additions & 2 deletions crates/parse/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -852,9 +852,118 @@ impl<'sess, 'ast> Parser<'sess, 'ast> {

#[cold]
fn parse_doc_comments_inner(&mut self) -> DocComments<'ast> {
let docs = self.arena.alloc_slice_copy(&self.docs);
let comments = self.arena.alloc_slice_copy(&self.docs);
self.docs.clear();
docs.into()
let natspec = self.parse_natspec(comments);
DocComments { comments, natspec }
}

/// A naive parser implementation for `NatSpec` comments.
///
/// Only validates that the grammar follows the NatSpec specification, but does not perform any
/// validation against the documented AST item.
fn parse_natspec(&mut self, comments: &[DocComment]) -> Option<ast::NatSpec<'ast>> {
if comments.is_empty() {
return None;
}

let mut items = SmallVec::<[ast::NatSpecItem; 8]>::new();
let mut has_tags = false;

let mut span: Option<Span> = None;
let mut kind: Option<ast::NatSpecKind> = None;
let mut content = String::new();

fn flush_item(
items: &mut SmallVec<[ast::NatSpecItem; 8]>,
kind: &mut Option<ast::NatSpecKind>,
content: &mut String,
span: &mut Option<Span>,
) {
if let Some(k) = kind.take() {
let c = content.trim();
if !c.is_empty() {
items.push(ast::NatSpecItem {
span: span.take().unwrap(),
kind: k,
content: Symbol::intern(c),
});
}
}
content.clear();
}

for comment in comments {
for line in comment.symbol.as_str().lines() {
let mut trimmed = line.trim_start();
if trimmed.starts_with('*') {
trimmed = &trimmed[1..].trim_start();
}

if trimmed.starts_with('@') {
has_tags = true;
flush_item(&mut items, &mut kind, &mut content, &mut span);

span = Some(comment.span);

let tag_line = &trimmed[1..];
let (tag, rest) =
tag_line.split_once(char::is_whitespace).unwrap_or((tag_line, ""));

let make_ident = |s: &str| Ident::new(Symbol::intern(s), comment.span);

let (k, c) = match tag {
"title" => (ast::NatSpecKind::Title, rest),
"author" => (ast::NatSpecKind::Author, rest),
"notice" => (ast::NatSpecKind::Notice, rest),
"dev" => (ast::NatSpecKind::Dev, rest),
"param" | "return" | "inheritdoc" => {
let (name, content) =
rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
let ident = make_ident(name);
let kind = match tag {
"param" => ast::NatSpecKind::Param { tag: ident },
"return" => ast::NatSpecKind::Return { tag: ident },
"inheritdoc" => ast::NatSpecKind::Inheritdoc { tag: ident },
_ => unreachable!(),
};
(kind, content)
}
custom_tag => match custom_tag.strip_prefix("custom:") {
Some(n) => (ast::NatSpecKind::Custom { tag: make_ident(n) }, rest),
None => {
self.dcx()
.err(format!("invalid natspec tag '@{custom_tag}', custom tags must use format '@custom:name'"))
.span(comment.span)
.emit();
(ast::NatSpecKind::Custom { tag: make_ident("") }, rest)
}
},
};

kind = Some(k);
content.push_str(c);
} else if kind.is_some() {
if !content.is_empty() {
content.push('\n');
}
content.push_str(trimmed);
if let Some(s) = &mut span {
*s = s.to(comment.span);
}
}
}
}
flush_item(&mut items, &mut kind, &mut content, &mut span);

if !has_tags {
return None;
}

Some(ast::NatSpec {
span: Span::join_first_last(items.iter().map(|i| i.span)),
items: self.alloc_smallvec(items),
})
}

/// Parses a qualified identifier: `foo.bar.baz`.
Expand Down
Loading