1use std::ops::Range;
3
4use derive_more::IsVariant;
5use winnow::{
6 LocatingSlice,
7 ascii::{line_ending, space0, space1, till_line_ending},
8 combinator::{alt, cut_err, delimited, not, opt, repeat, separated},
9 error::{StrContext, StrContextValue},
10 seq,
11 token::{rest, take_till, take_until},
12};
13pub use winnow::{ModalResult, Parser};
14
15use crate::{
16 definitions::Identifier,
17 interner::{INTERNER, Symbol},
18 textindex::{TextIndex, TextRange},
19};
20
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct NatSpec {
24 pub items: Vec<NatSpecItem>,
25}
26
27impl NatSpec {
28 pub fn append(&mut self, other: &mut Self) {
30 self.items.append(&mut other.items);
31 }
32
33 #[must_use]
35 pub fn populate_returns(mut self, returns: &[Identifier]) -> Self {
36 for i in &mut self.items {
37 i.populate_return(returns);
38 }
39 self
40 }
41
42 #[must_use]
44 pub fn count_param(&self, ident: &Identifier) -> usize {
45 let Some(ident_name) = &ident.name else {
46 return 0;
47 };
48 self.items
49 .iter()
50 .filter(|n| match &n.kind {
51 NatSpecKind::Param { name } => name == ident_name,
52 _ => false,
53 })
54 .count()
55 }
56
57 #[must_use]
59 pub fn count_return(&self, ident: &Identifier) -> usize {
60 let Some(ident_name) = &ident.name else {
61 return 0;
62 };
63 self.items
64 .iter()
65 .filter(|n| match &n.kind {
66 NatSpecKind::Return { name: Some(name) } => name == ident_name,
67 _ => false,
68 })
69 .count()
70 }
71
72 #[must_use]
74 pub fn count_unnamed_returns(&self) -> usize {
75 self.items
76 .iter()
77 .filter(|n| matches!(&n.kind, NatSpecKind::Return { name: None }))
78 .count()
79 }
80
81 #[must_use]
83 pub fn count_all_returns(&self) -> usize {
84 self.items.iter().filter(|n| n.kind.is_return()).count()
85 }
86
87 #[must_use]
88 pub fn has_param(&self) -> bool {
89 self.items.iter().any(|n| n.kind.is_param())
90 }
91
92 #[must_use]
93 pub fn has_return(&self) -> bool {
94 self.items.iter().any(|n| n.kind.is_return())
95 }
96
97 #[must_use]
98 pub fn has_notice(&self) -> bool {
99 self.items.iter().any(|n| n.kind.is_notice())
100 }
101
102 #[must_use]
103 pub fn has_dev(&self) -> bool {
104 self.items.iter().any(|n| n.kind.is_dev())
105 }
106
107 #[must_use]
108 pub fn has_title(&self) -> bool {
109 self.items.iter().any(|n| n.kind.is_title())
110 }
111
112 #[must_use]
113 pub fn has_author(&self) -> bool {
114 self.items.iter().any(|n| n.kind.is_author())
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)]
120#[non_exhaustive]
121#[builder(on(String, into))]
122pub struct NatSpecItem {
123 pub kind: NatSpecKind,
125
126 pub comment: String,
128
129 pub span: TextRange,
131}
132
133impl NatSpecItem {
134 pub fn populate_return(&mut self, returns: &[Identifier]) {
138 if !matches!(self.kind, NatSpecKind::Return { name: _ }) {
139 return;
140 }
141 let name = self
142 .comment
143 .split_whitespace()
144 .next()
145 .and_then(|first_word| {
146 let first_word = INTERNER.get_or_intern(first_word);
147 returns
148 .iter()
149 .any(|r| match &r.name {
150 Some(name) => first_word == *name,
151 None => false,
152 })
153 .then_some(first_word)
154 });
155 if let Some(name) = &name
156 && let Some(comment) = self.comment.strip_prefix(name.resolve_with(&INTERNER))
157 {
158 self.comment = comment.trim_start().to_string();
159 }
160 self.kind = NatSpecKind::Return { name }
161 }
162
163 #[must_use]
165 pub fn is_empty(&self) -> bool {
166 self.kind == NatSpecKind::Notice && self.comment.is_empty()
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, IsVariant)]
172pub enum NatSpecKind {
173 Title,
174 Author,
175 Notice,
176 Dev,
177 Param {
178 name: Symbol,
179 },
180 Return {
182 name: Option<Symbol>,
183 },
184 Inheritdoc {
185 parent: Symbol,
186 },
187 Custom {
188 tag: Symbol,
189 },
190}
191
192impl From<NatSpecItem> for NatSpec {
193 fn from(value: NatSpecItem) -> Self {
194 Self { items: vec![value] }
195 }
196}
197
198pub fn parse_comment(input: &mut &str) -> ModalResult<NatSpec> {
200 let input = rest::<&str, _>.parse_next(input)?;
202 let (mut natspec, spans) = alt((single_line_comment, multiline_comment, empty_multiline))
204 .parse_next(&mut LocatingSlice::new(input))?;
205 if natspec.items.is_empty() {
206 return Ok(natspec);
207 }
208
209 let mut current_index = TextIndex::ZERO;
211 let mut char_iter = input.chars().peekable();
212 for (natspec_item, byte_span) in natspec.items.iter_mut().zip(spans.iter()) {
213 if current_index.utf8 == byte_span.start {
214 natspec_item.span.start = current_index;
215 } else {
216 while let Some(c) = char_iter.next() {
218 current_index.advance(c, char_iter.peek());
219 if current_index.utf8 == byte_span.start {
220 natspec_item.span.start = current_index;
221 break;
222 }
223 }
224 }
225 while let Some(c) = char_iter.next() {
227 current_index.advance(c, char_iter.peek());
228 if current_index.utf8 == byte_span.end {
229 natspec_item.span.end = current_index;
230 break;
231 }
232 }
233 }
234 Ok(natspec)
235}
236
237fn ident(input: &mut LocatingSlice<&str>) -> ModalResult<Symbol> {
239 take_till(1.., |c: char| c.is_whitespace())
240 .map(|ident: &str| INTERNER.get_or_intern(ident))
241 .parse_next(input)
242}
243
244fn natspec_kind(input: &mut LocatingSlice<&str>) -> ModalResult<NatSpecKind> {
249 alt((
250 "@title".map(|_| NatSpecKind::Title),
251 "@author".map(|_| NatSpecKind::Author),
252 "@notice".map(|_| NatSpecKind::Notice),
253 "@dev".map(|_| NatSpecKind::Dev),
254 seq! {NatSpecKind::Param {
255 _: "@param",
256 _: space1,
257 name: ident
258 }},
259 "@return".map(|_| NatSpecKind::Return { name: None }), seq! {NatSpecKind::Inheritdoc {
261 _: "@inheritdoc",
262 _: space1,
263 parent: ident
264 }},
265 seq! {NatSpecKind::Custom {
266 _: "@custom:",
267 tag: ident
268 }},
269 ))
270 .parse_next(input)
271}
272
273#[expect(clippy::unnecessary_wraps)]
275fn end_of_comment(input: &mut LocatingSlice<&str>) -> ModalResult<()> {
276 let _ = (repeat::<_, _, (), (), _>(1.., '*'), '/').parse_next(input);
277 Ok(())
278}
279
280fn one_multiline_natspec(
282 input: &mut LocatingSlice<&str>,
283) -> ModalResult<(NatSpecItem, Range<usize>)> {
284 let _ = space0.parse_next(input)?;
285 let () = repeat::<_, _, (), _, _>(0.., '*').parse_next(input)?;
286 let _ = space0.parse_next(input)?;
287 let (kind, kind_span) = opt(natspec_kind)
288 .map(|v| v.unwrap_or(NatSpecKind::Notice))
289 .with_span()
290 .parse_next(input)?;
291 let _ = space0.parse_next(input)?;
292 let (comment, comment_span) = take_until(0.., ("\r", "\n", "*/"))
293 .parse_to()
294 .with_span()
295 .parse_next(input)?;
296 Ok((
297 NatSpecItem {
298 kind,
299 comment,
300 span: TextRange::default(),
301 },
302 kind_span.start..comment_span.end,
303 ))
304}
305
306fn multiline_comment(input: &mut LocatingSlice<&str>) -> ModalResult<(NatSpec, Vec<Range<usize>>)> {
308 delimited(
309 (
310 (
311 "/**",
312 cut_err(not('*'))
315 .context(StrContext::Label("delimiter"))
316 .context(StrContext::Expected(StrContextValue::Description("/**"))),
317 ),
318 space0,
319 opt(line_ending),
320 ),
321 separated(0.., one_multiline_natspec, line_ending),
322 (opt(line_ending), space0, end_of_comment),
323 )
324 .map(|items: Vec<(NatSpecItem, Range<usize>)>| {
325 let (items, spans) = items.into_iter().unzip();
326 (NatSpec { items }, spans)
327 })
328 .parse_next(input)
329}
330
331fn empty_multiline(input: &mut LocatingSlice<&str>) -> ModalResult<(NatSpec, Vec<Range<usize>>)> {
333 let _ = ("/**", space1, repeat::<_, _, (), _, _>(1.., '*'), '/').parse_next(input)?;
334 Ok((NatSpec::default(), Vec::new()))
335}
336
337fn single_line_natspec(
339 input: &mut LocatingSlice<&str>,
340) -> ModalResult<(NatSpecItem, Range<usize>)> {
341 let _ = space0.parse_next(input)?;
342 let (kind, kind_span) = opt(natspec_kind)
343 .map(|v| v.unwrap_or(NatSpecKind::Notice))
344 .with_span()
345 .parse_next(input)?;
346 let _ = space0.parse_next(input)?;
347 let (comment, comment_span) = till_line_ending.parse_to().with_span().parse_next(input)?;
348 Ok((
349 NatSpecItem {
350 kind,
351 comment,
352 span: TextRange::default(),
353 },
354 kind_span.start..comment_span.end,
355 ))
356}
357
358fn single_line_comment(
360 input: &mut LocatingSlice<&str>,
361) -> ModalResult<(NatSpec, Vec<Range<usize>>)> {
362 let (item, range) = delimited(
363 (
364 "///",
365 cut_err(not('/'))
368 .context(StrContext::Label("delimiter"))
369 .context(StrContext::Expected(StrContextValue::Description("///"))),
370 ),
371 single_line_natspec,
372 opt(line_ending),
373 )
374 .parse_next(input)?;
375 if item.is_empty() {
376 return Ok((NatSpec::default(), Vec::new()));
377 }
378 Ok((item.into(), vec![range]))
379}
380
381#[cfg(test)]
382mod tests {
383 use similar_asserts::assert_eq;
384 use winnow::error::ParseError;
385
386 use super::*;
387
388 #[test]
389 fn test_kind() {
390 let cases = [
391 ("@title", NatSpecKind::Title),
392 ("@author", NatSpecKind::Author),
393 ("@notice", NatSpecKind::Notice),
394 ("@dev", NatSpecKind::Dev),
395 (
396 "@param foo",
397 NatSpecKind::Param {
398 name: INTERNER.get_or_intern("foo"),
399 },
400 ),
401 ("@return", NatSpecKind::Return { name: None }),
402 (
403 "@inheritdoc ISomething",
404 NatSpecKind::Inheritdoc {
405 parent: INTERNER.get_or_intern("ISomething"),
406 },
407 ),
408 (
409 "@custom:foo",
410 NatSpecKind::Custom {
411 tag: INTERNER.get_or_intern("foo"),
412 },
413 ),
414 ];
415 for case in cases {
416 let res = natspec_kind.parse(LocatingSlice::new(case.0));
417 assert!(res.is_ok(), "{res:?}");
418 let res = res.unwrap();
419 assert_eq!(res, case.1);
420 }
421 }
422
423 #[test]
424 fn test_one_multiline_item() {
425 let cases = [
426 ("@dev Hello world\n", NatSpecKind::Dev, "Hello world"),
427 ("@title The Title\n", NatSpecKind::Title, "The Title"),
428 (
429 " * @author McGyver <hi@buildanything.com>\n",
430 NatSpecKind::Author,
431 "McGyver <hi@buildanything.com>",
432 ),
433 (
434 " @param foo The bar\r\n",
435 NatSpecKind::Param {
436 name: INTERNER.get_or_intern("foo"),
437 },
438 "The bar",
439 ),
440 (
441 " @return something The return value\n",
442 NatSpecKind::Return { name: None },
443 "something The return value",
444 ),
445 (
446 "\t* @custom:foo bar\n",
447 NatSpecKind::Custom {
448 tag: INTERNER.get_or_intern("foo"),
449 },
450 "bar",
451 ),
452 (" lorem ipsum\n", NatSpecKind::Notice, "lorem ipsum"),
453 ("lorem ipsum\r\n", NatSpecKind::Notice, "lorem ipsum"),
454 ("\t* foobar\n", NatSpecKind::Notice, "foobar"),
455 (" * foobar\n", NatSpecKind::Notice, "foobar"),
456 ];
457 for case in cases {
458 let res = (one_multiline_natspec, line_ending).parse(LocatingSlice::new(case.0));
459 assert!(res.is_ok(), "{res:?}");
460 let ((res, _), _) = res.unwrap();
461 assert_eq!(
462 res,
463 NatSpecItem {
464 kind: case.1,
465 comment: case.2.to_string(),
466 span: TextRange::default()
467 }
468 );
469 }
470 }
471
472 #[test]
473 fn test_single_line() {
474 let cases = [
475 ("/// Foo bar", NatSpecKind::Notice, "Foo bar"),
476 ("/// Baz", NatSpecKind::Notice, "Baz"),
477 (
478 "/// @notice Hello world",
479 NatSpecKind::Notice,
480 "Hello world",
481 ),
482 (
483 "/// @param foo This is bar\n",
484 NatSpecKind::Param {
485 name: INTERNER.get_or_intern("foo"),
486 },
487 "This is bar",
488 ),
489 (
490 "/// @return The return value\r\n",
491 NatSpecKind::Return { name: None },
492 "The return value",
493 ),
494 (
495 "/// @custom:foo This is bar\n",
496 NatSpecKind::Custom {
497 tag: INTERNER.get_or_intern("foo"),
498 },
499 "This is bar",
500 ),
501 ];
502 for case in cases {
503 let res = single_line_comment.parse(LocatingSlice::new(case.0));
504 assert!(res.is_ok(), "{res:?}");
505 let (res, _) = res.unwrap();
506 assert_eq!(
507 res,
508 NatSpecItem {
509 kind: case.1,
510 comment: case.2.to_string(),
511 span: TextRange::default()
512 }
513 .into()
514 );
515 }
516 }
517
518 #[test]
519 fn test_single_line_empty() {
520 let res = single_line_comment.parse(LocatingSlice::new("///\n"));
521 assert!(res.is_ok(), "{res:?}");
522 let (res, _) = res.unwrap();
523 assert_eq!(res, NatSpec::default());
524 }
525
526 #[test]
527 fn test_single_line_weird() {
528 let res = single_line_comment.parse(LocatingSlice::new("//// Hello\n"));
529 assert!(matches!(res, Err(ParseError { .. })));
530 }
531
532 #[test]
533 fn test_multiline() {
534 let comment = "/**
535 * @notice Some notice text.
536 */";
537 let res = multiline_comment.parse(LocatingSlice::new(comment));
538 assert!(res.is_ok(), "{res:?}");
539 let (res, _) = res.unwrap();
540 assert_eq!(
541 res,
542 NatSpec {
543 items: vec![NatSpecItem {
544 kind: NatSpecKind::Notice,
545 comment: "Some notice text.".to_string(),
546 span: TextRange::default()
547 }]
548 }
549 );
550 }
551
552 #[test]
553 fn test_multiline2() {
554 let comment = "/**
555 * @notice Some notice text.
556 * @custom:something
557 */";
558 let res = multiline_comment.parse(LocatingSlice::new(comment));
559 assert!(res.is_ok(), "{res:?}");
560 let (res, _) = res.unwrap();
561 assert_eq!(
562 res,
563 NatSpec {
564 items: vec![
565 NatSpecItem {
566 kind: NatSpecKind::Notice,
567 comment: "Some notice text.".to_string(),
568 span: TextRange::default()
569 },
570 NatSpecItem {
571 kind: NatSpecKind::Custom {
572 tag: INTERNER.get_or_intern("something")
573 },
574 comment: String::new(),
575 span: TextRange::default()
576 }
577 ]
578 }
579 );
580 }
581
582 #[test]
583 fn test_multiline3() {
584 let comment = "/** @notice Some notice text.
585Another notice
586 * @param test
587 \t** @custom:something */";
588 let res = multiline_comment.parse(LocatingSlice::new(comment));
589 assert!(res.is_ok(), "{res:?}");
590 let (res, _) = res.unwrap();
591 assert_eq!(
592 res,
593 NatSpec {
594 items: vec![
595 NatSpecItem {
596 kind: NatSpecKind::Notice,
597 comment: "Some notice text.".to_string(),
598 span: TextRange::default()
599 },
600 NatSpecItem {
601 kind: NatSpecKind::Notice,
602 comment: "Another notice".to_string(),
603 span: TextRange::default()
604 },
605 NatSpecItem {
606 kind: NatSpecKind::Param {
607 name: INTERNER.get_or_intern("test")
608 },
609 comment: String::new(),
610 span: TextRange::default()
611 },
612 NatSpecItem {
613 kind: NatSpecKind::Custom {
614 tag: INTERNER.get_or_intern("something")
615 },
616 comment: String::new(),
617 span: TextRange::default()
618 }
619 ]
620 }
621 );
622 }
623
624 #[test]
625 fn test_multiline_empty() {
626 let comment = "/**
627 */";
628 let res = parse_comment.parse(comment);
629
630 assert!(res.is_ok(), "{res:?}");
631 let res = res.unwrap();
632 assert_eq!(res, NatSpec::default());
633
634 let comment = "/** */";
635 let res = parse_comment.parse(comment);
636
637 assert!(res.is_ok(), "{res:?}");
638 let res = res.unwrap();
639 assert_eq!(res, NatSpec::default());
640 }
641
642 #[test]
643 fn test_multiline_weird() {
644 let comment = "/**** @notice Some text
645 ** */";
646 let res = parse_comment.parse(comment);
647 assert!(matches!(res, Err(ParseError { .. })));
648 }
649}