Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(natspec): parser improvements to support weird edge-cases #39

Merged
merged 5 commits into from
Feb 23, 2025
Merged
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
164 changes: 110 additions & 54 deletions src/natspec.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! NatSpec Comment Parser
use winnow::{
ascii::{line_ending, space0, space1, till_line_ending},
combinator::{alt, delimited, opt, preceded, repeat, terminated},
combinator::{alt, delimited, opt, repeat, separated},
seq,
token::take_till,
token::{take_till, take_until},
Parser as _, Result,
};

Expand Down Expand Up @@ -149,21 +149,16 @@ impl From<NatSpecItem> for NatSpec {

/// Parse a Solidity doc-comment to extract the NatSpec information
pub fn parse_comment(input: &mut &str) -> Result<NatSpec> {
alt((
parse_single_line_comment,
parse_multiline_comment,
parse_empty_multiline,
))
.parse_next(input)
alt((single_line_comment, multiline_comment, empty_multiline)).parse_next(input)
}

fn parse_ident(input: &mut &str) -> Result<String> {
fn ident(input: &mut &str) -> Result<String> {
take_till(1.., |c: char| c.is_whitespace())
.map(|ident: &str| ident.to_owned())
.parse_next(input)
}

fn parse_natspec_kind(input: &mut &str) -> Result<NatSpecKind> {
fn natspec_kind(input: &mut &str) -> Result<NatSpecKind> {
alt((
"@title".map(|_| NatSpecKind::Title),
"@author".map(|_| NatSpecKind::Author),
Expand All @@ -172,64 +167,81 @@ fn parse_natspec_kind(input: &mut &str) -> Result<NatSpecKind> {
seq! {NatSpecKind::Param {
_: "@param",
_: space1,
name: parse_ident
name: ident
}},
"@return".map(|_| NatSpecKind::Return { name: None }), // we will process the name later since it's optional
seq! {NatSpecKind::Inheritdoc {
_: "@inheritdoc",
_: space1,
parent: parse_ident
parent: ident
}},
seq! {NatSpecKind::Custom {
_: "@custom:",
tag: parse_ident
tag: ident
}},
))
.parse_next(input)
}

fn parse_comment_line(input: &mut &str) -> Result<NatSpecItem> {
fn end_of_comment(input: &mut &str) -> Result<()> {
let _ = (repeat::<_, _, (), (), _>(1.., '*'), '/').parse_next(input);
Ok(())
}

fn one_multiline_natspec(input: &mut &str) -> Result<NatSpecItem> {
seq! {NatSpecItem {
_: space0,
_: opt(terminated('*', space0)),
kind: opt(parse_natspec_kind).map(|v| v.unwrap_or(NatSpecKind::Notice)),
_: repeat::<_, _, (), _, _>(0.., '*'),
_: space0,
comment: till_line_ending.parse_to(),
_: line_ending
kind: opt(natspec_kind).map(|v| v.unwrap_or(NatSpecKind::Notice)),
_: space0,
comment: take_until(0.., ("\r", "\n", "*/")).parse_to(),
}}
.parse_next(input)
}

fn parse_multiline_comment(input: &mut &str) -> Result<NatSpec> {
fn multiline_comment(input: &mut &str) -> Result<NatSpec> {
delimited(
("/**", space0, line_ending),
repeat(0.., parse_comment_line),
(space0, "*/"),
(
'/',
repeat::<_, _, (), _, _>(2.., '*'),
space0,
opt(line_ending),
),
separated(0.., one_multiline_natspec, line_ending),
(opt(line_ending), space0, end_of_comment),
)
.map(|items| NatSpec { items })
.parse_next(input)
}

fn parse_empty_multiline(input: &mut &str) -> Result<NatSpec> {
let _ = ("/**", space1, "*/").parse_next(input)?;
fn empty_multiline(input: &mut &str) -> Result<NatSpec> {
let _ = (
'/',
repeat::<_, _, (), _, _>(2.., '*'),
space1,
repeat::<_, _, (), _, _>(1.., '*'),
'/',
)
.parse_next(input)?;
Ok(NatSpec::default())
}

fn parse_comment_line_single(input: &mut &str) -> Result<NatSpecItem> {
fn single_line_natspec(input: &mut &str) -> Result<NatSpecItem> {
seq! {NatSpecItem {
_: space0,
_: opt(terminated('*', space0)),
kind: opt(parse_natspec_kind).map(|v| v.unwrap_or(NatSpecKind::Notice)),
kind: opt(natspec_kind).map(|v| v.unwrap_or(NatSpecKind::Notice)),
_: space0,
comment: till_line_ending.parse_to(),
}}
.parse_next(input)
}

fn parse_single_line_comment(input: &mut &str) -> Result<NatSpec> {
let item = preceded(
fn single_line_comment(input: &mut &str) -> Result<NatSpec> {
let item = delimited(
repeat::<_, _, (), _, _>(3.., '/'),
parse_comment_line_single,
single_line_natspec,
opt(line_ending),
)
.parse_next(input)?;
if item.is_empty() {
Expand Down Expand Up @@ -272,27 +284,25 @@ mod tests {
),
];
for case in cases {
let res = parse_natspec_kind.parse(case.0);
let res = natspec_kind.parse(case.0);
assert!(res.is_ok(), "{res:?}");
let res = res.unwrap();
assert_eq!(res, case.1);
}
}

#[test]
fn test_comment_line() {
fn test_one_multiline_item() {
let cases = [
(" lorem ipsum\n", NatSpecKind::Notice, "lorem ipsum"),
("\t* foobar\n", NatSpecKind::Notice, "foobar"),
(" * foobar\r\n", NatSpecKind::Notice, "foobar"),
("@dev Hello world\n", NatSpecKind::Dev, "Hello world"),
("@title The Title\n", NatSpecKind::Title, "The Title"),
(
" * @author McGyver <[email protected]>\n",
NatSpecKind::Author,
"McGyver <[email protected]>",
),
(
" @param foo The bar\n",
" @param foo The bar\r\n",
NatSpecKind::Param {
name: "foo".to_string(),
},
Expand All @@ -310,11 +320,15 @@ mod tests {
},
"bar",
),
(" lorem ipsum\n", NatSpecKind::Notice, "lorem ipsum"),
("lorem ipsum\r\n", NatSpecKind::Notice, "lorem ipsum"),
("\t* foobar\n", NatSpecKind::Notice, "foobar"),
(" * foobar\n", NatSpecKind::Notice, "foobar"),
];
for case in cases {
let res = parse_comment_line.parse(case.0);
let res = (one_multiline_natspec, line_ending).parse(case.0);
assert!(res.is_ok(), "{res:?}");
let res = res.unwrap();
let (res, _) = res.unwrap();
assert_eq!(
res,
NatSpecItem {
Expand All @@ -328,35 +342,35 @@ mod tests {
#[test]
fn test_single_line() {
let cases = [
("/// Foo bar", NatSpecKind::Notice, "Foo bar"),
("/// Baz", NatSpecKind::Notice, "Baz"),
("/// Foo bar\n", NatSpecKind::Notice, "Foo bar"),
("/// Baz\n", NatSpecKind::Notice, "Baz"),
(
"/// @notice Hello world",
"/// @notice Hello world\n",
NatSpecKind::Notice,
"Hello world",
),
(
"/// @param foo This is bar",
"/// @param foo This is bar\n",
NatSpecKind::Param {
name: "foo".to_string(),
},
"This is bar",
),
(
"/// @return The return value",
"/// @return The return value\r\n",
NatSpecKind::Return { name: None },
"The return value",
),
(
"/// @custom:foo This is bar",
"/// @custom:foo This is bar\n",
NatSpecKind::Custom {
tag: "foo".to_string(),
},
"This is bar",
),
];
for case in cases {
let res = parse_single_line_comment.parse(case.0);
let res = single_line_comment.parse(case.0);
assert!(res.is_ok(), "{res:?}");
let res = res.unwrap();
assert_eq!(
Expand All @@ -372,7 +386,7 @@ mod tests {

#[test]
fn test_single_line_empty() {
let res = parse_single_line_comment.parse("///");
let res = single_line_comment.parse("///\n");
assert!(res.is_ok(), "{res:?}");
let res = res.unwrap();
assert_eq!(res, NatSpec::default());
Expand All @@ -383,7 +397,7 @@ mod tests {
let comment = "/**
* @notice Some notice text.
*/";
let res = parse_multiline_comment.parse(comment);
let res = multiline_comment.parse(comment);
assert!(res.is_ok(), "{res:?}");
let res = res.unwrap();
assert_eq!(
Expand All @@ -403,7 +417,7 @@ mod tests {
* @notice Some notice text.
* @custom:something
*/";
let res = parse_multiline_comment.parse(comment);
let res = multiline_comment.parse(comment);
assert!(res.is_ok(), "{res:?}");
let res = res.unwrap();
assert_eq!(
Expand All @@ -425,6 +439,44 @@ mod tests {
);
}

#[test]
fn test_multiline3() {
let comment = "/** @notice Some notice text.
Another notice
* @param test
\t** @custom:something */";
let res = multiline_comment.parse(comment);
assert!(res.is_ok(), "{res:?}");
let res = res.unwrap();
assert_eq!(
res,
NatSpec {
items: vec![
NatSpecItem {
kind: NatSpecKind::Notice,
comment: "Some notice text.".to_string()
},
NatSpecItem {
kind: NatSpecKind::Notice,
comment: "Another notice".to_string()
},
NatSpecItem {
kind: NatSpecKind::Param {
name: "test".to_string()
},
comment: "".to_string()
},
NatSpecItem {
kind: NatSpecKind::Custom {
tag: "something".to_string()
},
comment: String::new()
}
]
}
);
}

#[test]
fn test_multiline_empty() {
let comment = "/**
Expand All @@ -441,10 +493,8 @@ mod tests {
assert_eq!(res, NatSpec::default());
}

#[ignore]
#[test]
fn test_multiline_weird() {
// FIXME: this doesn't pass
let comment = "/**** @notice Some text
** */";
let res = parse_comment.parse(comment);
Expand All @@ -453,10 +503,16 @@ mod tests {
assert_eq!(
res,
NatSpec {
items: vec![NatSpecItem {
kind: NatSpecKind::Notice,
comment: "Some text".to_string()
},]
items: vec![
NatSpecItem {
kind: NatSpecKind::Notice,
comment: "Some text".to_string()
},
NatSpecItem {
kind: NatSpecKind::Notice,
comment: "".to_string()
}
]
}
);
}
Expand Down
Loading