Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions compiler/noirc_frontend/src/parser/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub enum ParserErrorReason {
InvalidPattern,
#[error("Documentation comment does not document anything")]
DocCommentDoesNotDocumentAnything,
#[error("Documentation comments cannot be applied to function parameters")]
DocCommentCannotBeAppliedToFunctionParameters,

#[error("Missing type for function parameter")]
MissingTypeForFunctionParameter,
Expand Down
20 changes: 14 additions & 6 deletions compiler/noirc_frontend/src/parser/parser/doc_comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ impl Parser<'_> {
})
}

/// OuterDocComments = outer_doc_comments*
/// OuterDocComments = OuterDocComment*
pub(super) fn parse_outer_doc_comments(&mut self) -> Vec<String> {
self.parse_many("outer doc comments", without_separator(), Self::parse_outer_doc_comment)
}

fn parse_outer_doc_comment(&mut self) -> Option<String> {
/// OuterDocComment = outer_doc_comment
pub(super) fn parse_outer_doc_comment(&mut self) -> Option<String> {
self.eat_kind(TokenKind::OuterDocComment).map(|token| match token.into_token() {
Token::LineComment(comment, Some(DocStyle::Outer))
| Token::BlockComment(comment, Some(DocStyle::Outer)) => comment,
Expand All @@ -34,13 +35,20 @@ impl Parser<'_> {

/// Skips any outer doc comments but produces a warning saying that they don't document anything.
pub(super) fn warn_on_outer_doc_comments(&mut self) {
self.skip_doc_comments_with_reason(ParserErrorReason::DocCommentDoesNotDocumentAnything);
}

/// Skips any outer doc comments but produces an error saying that they can't be applied to parameters
pub(super) fn error_on_outer_doc_comments_on_parameter(&mut self) {
let reason = ParserErrorReason::DocCommentCannotBeAppliedToFunctionParameters;
self.skip_doc_comments_with_reason(reason);
}

fn skip_doc_comments_with_reason(&mut self, reason: ParserErrorReason) {
let location_before_doc_comments = self.current_token_location;
let doc_comments = self.parse_outer_doc_comments();
if !doc_comments.is_empty() {
self.push_error(
ParserErrorReason::DocCommentDoesNotDocumentAnything,
location_before_doc_comments,
);
self.push_error(reason, self.location_since(location_before_doc_comments));
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions compiler/noirc_frontend/src/parser/parser/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ impl Parser<'_> {

fn parse_function_parameter(&mut self, allow_self: bool) -> Option<Param> {
loop {
self.error_on_outer_doc_comments_on_parameter();

let start_location = self.current_token_location;

let pattern_or_self = if allow_self {
Expand Down Expand Up @@ -608,4 +610,19 @@ mod tests {
assert!(matches!(call.object.kind, ExpressionKind::If(_)));
assert_eq!(call.method_name.to_string(), "bar");
}

#[test]
fn errors_on_doc_comments_on_parameter() {
let src = "
fn foo(
/// Doc comment
x: Field,
)
";
let (_module, errors) = parse_program_with_dummy_file(src);
assert_eq!(errors.len(), 1);

let reason = errors[0].reason().unwrap();
assert_eq!(reason, &ParserErrorReason::DocCommentCannotBeAppliedToFunctionParameters);
}
}
59 changes: 52 additions & 7 deletions compiler/noirc_frontend/src/parser/parser/item.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use iter_extended::vecmap;
use noirc_errors::Location;

use crate::{
parser::{Item, ItemKind, ParserErrorReason, labels::ParsingRuleLabel},
token::{Keyword, Token},
token::{Attribute, Keyword, Token},
};

use super::{Parser, impls::Impl, parse_many::without_separator};
Expand Down Expand Up @@ -87,11 +88,24 @@ impl<'a> Parser<'a> {
}
}

/// Item = OuterDocComments ItemKind
/// Item = ( Attribute | OuterDocComments )* ItemKind
fn parse_item(&mut self) -> Vec<Item> {
let start_location = self.current_token_location;
let doc_comments = self.parse_outer_doc_comments();
let kinds = self.parse_item_kind();

// Attributes and doc comments can come in any order, and can even be interspersed
let mut doc_comments = Vec::new();
let mut attributes = Vec::new();
loop {
if let Some(doc_comment) = self.parse_outer_doc_comment() {
doc_comments.push(doc_comment);
} else if let Some(attribute) = self.parse_attribute() {
attributes.push(attribute);
} else {
break;
}
}

let kinds = self.parse_item_kind(attributes);
let location = self.location_since(start_location);

if kinds.is_empty() && !doc_comments.is_empty() {
Expand All @@ -118,13 +132,12 @@ impl<'a> Parser<'a> {
/// | TypeAlias
/// | Function
/// )
fn parse_item_kind(&mut self) -> Vec<ItemKind> {
fn parse_item_kind(&mut self, attributes: Vec<(Attribute, Location)>) -> Vec<ItemKind> {
if let Some(kind) = self.parse_inner_attribute() {
return vec![ItemKind::InnerAttribute(kind)];
}

let start_location = self.current_token_location;
let attributes = self.parse_attributes();

let modifiers = self.parse_modifiers(
true, // allow mut
Expand Down Expand Up @@ -236,7 +249,10 @@ impl<'a> Parser<'a> {
mod tests {
use crate::{
parse_program_with_dummy_file,
parser::parser::tests::{get_single_error, get_source_with_error_span},
parser::{
ItemKind,
parser::tests::{get_single_error, get_source_with_error_span},
},
};

#[test]
Expand Down Expand Up @@ -278,4 +294,33 @@ mod tests {
let error = get_single_error(&errors, span);
assert!(error.to_string().contains("This doc comment doesn't document anything"));
}

#[test]
fn parse_item_with_mixed_attributes_and_doc_comments() {
let src = "
/// One
#[one]
/// Two
#[two]
/// Three
fn foo() {}
";

let (module, errors) = parse_program_with_dummy_file(src);
assert!(errors.is_empty());

assert_eq!(module.items.len(), 1);
let item = &module.items[0];
assert_eq!(
item.doc_comments,
vec![" One".to_string(), " Two".to_string(), " Three".to_string(),]
);
let ItemKind::Function(func) = &item.kind else {
panic!("Expected function");
};
let attributes = &func.attributes().secondary;
assert_eq!(attributes.len(), 2);
assert_eq!(attributes[0].to_string(), "#[one]");
assert_eq!(attributes[1].to_string(), "#[two]");
}
}
5 changes: 5 additions & 0 deletions tooling/nargo_fmt/src/formatter/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@ impl Formatter<'_> {
if let Some((function_attribute, index)) = attributes.function {
all_attributes.insert(index, Attribute::Function(function_attribute));
}

// Attributes and doc comments can be mixed, so between each attribute
// we format potential doc comments
for attribute in all_attributes {
self.format_outer_doc_comments();
self.format_attribute(attribute);
}
self.format_outer_doc_comments();
}

pub(super) fn format_secondary_attributes(&mut self, attributes: Vec<SecondaryAttribute>) {
Expand Down
25 changes: 25 additions & 0 deletions tooling/nargo_fmt/src/formatter/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,28 @@ struct ImportGroup {
visibility: ItemVisibility,
span_end: u32,
}

#[cfg(test)]
mod tests {
use crate::assert_format;

#[test]
fn formats_item_with_mixed_attributes_and_doc_comments() {
let src = "
/// One
#[one]
/// Two
#[two]
/// Three
fn foo( ) { }
";
let expected = "/// One
#[one]
/// Two
#[two]
/// Three
fn foo() {}
";
assert_format(src, expected);
}
}