diff --git a/compiler/noirc_frontend/src/lexer/lexer.rs b/compiler/noirc_frontend/src/lexer/lexer.rs index 12425ba96b1..6e9409c12e3 100644 --- a/compiler/noirc_frontend/src/lexer/lexer.rs +++ b/compiler/noirc_frontend/src/lexer/lexer.rs @@ -73,6 +73,10 @@ impl<'a> Lexer<'a> { self } + pub fn set_skip_whitespaces_flag(&mut self, flag: bool) { + self.skip_whitespaces = flag; + } + /// Iterates the cursor and returns the char at the new cursor position fn next_char(&mut self) -> Option { let (position, ch) = self.chars.next()?; diff --git a/compiler/noirc_frontend/src/parser/parser.rs b/compiler/noirc_frontend/src/parser/parser.rs index b6a59293fb8..dc5eb8293fd 100644 --- a/compiler/noirc_frontend/src/parser/parser.rs +++ b/compiler/noirc_frontend/src/parser/parser.rs @@ -355,8 +355,15 @@ impl<'a> Parser<'a> { } fn eat_attribute_start(&mut self) -> Option { - if matches!(self.token.token(), Token::AttributeStart { is_inner: false, .. }) { + if let Token::AttributeStart { is_inner: false, is_tag } = self.token.token() { + // We have parsed the attribute start token `#[`. + // Disable the "skip whitespaces" flag only for tag attributes so that the next `self.bump()` + // does not consume the whitespace following the upcoming token. + if *is_tag { + self.set_lexer_skip_whitespaces_flag(false); + } let token = self.bump(); + self.set_lexer_skip_whitespaces_flag(true); match token.into_token() { Token::AttributeStart { is_tag, .. } => Some(is_tag), _ => unreachable!(), @@ -367,8 +374,15 @@ impl<'a> Parser<'a> { } fn eat_inner_attribute_start(&mut self) -> Option { - if matches!(self.token.token(), Token::AttributeStart { is_inner: true, .. }) { + if let Token::AttributeStart { is_inner: true, is_tag } = self.token.token() { + // We have parsed the inner attribute start token `#![`. + // Disable the "skip whitespaces" flag only for tag attributes so that the next `self.bump()` + // does not consume the whitespace following the upcoming token. + if *is_tag { + self.set_lexer_skip_whitespaces_flag(false); + } let token = self.bump(); + self.set_lexer_skip_whitespaces_flag(true); match token.into_token() { Token::AttributeStart { is_tag, .. } => Some(is_tag), _ => unreachable!(), @@ -479,6 +493,16 @@ impl<'a> Parser<'a> { self.at(Token::Keyword(keyword)) } + fn at_whitespace(&self) -> bool { + matches!(self.token.token(), Token::Whitespace(_)) + } + + fn set_lexer_skip_whitespaces_flag(&mut self, flag: bool) { + if let TokenStream::Lexer(lexer) = &mut self.tokens { + lexer.set_skip_whitespaces_flag(flag); + }; + } + fn next_is(&self, token: Token) -> bool { self.next_token.token() == &token } diff --git a/compiler/noirc_frontend/src/parser/parser/attributes.rs b/compiler/noirc_frontend/src/parser/parser/attributes.rs index 068dc486256..9ba856604c9 100644 --- a/compiler/noirc_frontend/src/parser/parser/attributes.rs +++ b/compiler/noirc_frontend/src/parser/parser/attributes.rs @@ -110,6 +110,11 @@ impl Parser<'_> { let mut contents = String::new(); let mut brackets_count = 1; // 1 because of the starting `#[` + // Note: Keep trailing whitespace tokens. + // If we skip them, only non-whitespace tokens are parsed. + // When converting those tokens into a `String` for the tag attribute, + // the result will lose whitespace and no longer match the original content. + self.set_lexer_skip_whitespaces_flag(false); while !self.at_eof() { if self.at(Token::LeftBracket) { @@ -126,6 +131,11 @@ impl Parser<'_> { self.bump(); } + self.set_lexer_skip_whitespaces_flag(true); + while self.at_whitespace() { + self.bump(); + } + let location = self.location_since(start_location); let kind = SecondaryAttributeKind::Tag(contents); let attr = SecondaryAttribute { kind, location }; @@ -790,4 +800,56 @@ mod tests { }; assert!(matches!(attr.kind, SecondaryAttributeKind::Deprecated(None))); } + + #[test] + fn parses_inner_tag_attribute_with_whitespace() { + let src = "#!['hello world]"; + let mut parser = Parser::for_str_with_dummy_file(src); + let SecondaryAttributeKind::Tag(contents) = parser.parse_inner_attribute().unwrap().kind + else { + panic!("Expected inner tag attribute"); + }; + expect_no_errors(&parser.errors); + assert_eq!(contents, "hello world"); + } + + #[test] + fn parses_inner_tag_attribute_with_multiple_whitespaces() { + let src = "#!['x as u32]"; + let mut parser = Parser::for_str_with_dummy_file(src); + let SecondaryAttributeKind::Tag(contents) = parser.parse_inner_attribute().unwrap().kind + else { + panic!("Expected inner tag attribute"); + }; + expect_no_errors(&parser.errors); + assert_eq!(contents, "x as u32"); + } + #[test] + fn parses_tag_attribute_with_multiple_whitespaces() { + let src = "#['y as i16]"; + let mut parser = Parser::for_str_with_dummy_file(src); + let (attribute, _span) = parser.parse_attribute().unwrap(); + expect_no_errors(&parser.errors); + let Attribute::Secondary(attribute) = attribute else { + panic!("Expected secondary attribute"); + }; + let SecondaryAttributeKind::Tag(contents) = attribute.kind else { + panic!("Expected meta attribute"); + }; + assert_eq!(contents, "y as i16"); + } + #[test] + fn parses_tag_attribute_with_whitespace() { + let src = "#['foo bar]"; + let mut parser = Parser::for_str_with_dummy_file(src); + let (attribute, _span) = parser.parse_attribute().unwrap(); + expect_no_errors(&parser.errors); + let Attribute::Secondary(attribute) = attribute else { + panic!("Expected secondary attribute"); + }; + let SecondaryAttributeKind::Tag(contents) = attribute.kind else { + panic!("Expected meta attribute"); + }; + assert_eq!(contents, "foo bar"); + } } diff --git a/tooling/nargo_fmt/src/formatter.rs b/tooling/nargo_fmt/src/formatter.rs index c7fce04d20b..c44ec7d3e79 100644 --- a/tooling/nargo_fmt/src/formatter.rs +++ b/tooling/nargo_fmt/src/formatter.rs @@ -227,6 +227,13 @@ impl<'a> Formatter<'a> { self.write(&self.source[span.start() as usize..span.end() as usize]); } + /// Writes whatever is in the given span relative to the file's source that's being formatted + /// but trims the whitespaces at the end. + pub(crate) fn write_source_span_trimmed(&mut self, span: Span) { + let source = self.source[span.start() as usize..span.end() as usize].trim_end(); + self.write(source); + } + /// Writes the current indentation to the buffer, but only if the buffer /// is empty or it ends with a newline (otherwise we'd be indenting when not needed). pub(crate) fn write_indentation(&mut self) { diff --git a/tooling/nargo_fmt/src/formatter/attribute.rs b/tooling/nargo_fmt/src/formatter/attribute.rs index 9deb2808659..7c0e2a662a0 100644 --- a/tooling/nargo_fmt/src/formatter/attribute.rs +++ b/tooling/nargo_fmt/src/formatter/attribute.rs @@ -1,3 +1,4 @@ +use noirc_errors::Span; use noirc_frontend::token::{ Attribute, Attributes, FunctionAttribute, FunctionAttributeKind, FuzzingScope, MetaAttribute, MetaAttributeName, SecondaryAttribute, SecondaryAttributeKind, TestScope, Token, @@ -97,7 +98,7 @@ impl Formatter<'_> { self.format_one_arg_attribute(); } SecondaryAttributeKind::Tag(_) => { - self.write_and_skip_span_without_formatting(attribute.location.span); + self.format_tag_attribute(attribute.location.span); } SecondaryAttributeKind::Meta(meta_attribute) => { self.format_meta_attribute(meta_attribute); @@ -248,6 +249,15 @@ impl Formatter<'_> { } self.write_right_bracket(); // ] } + + /// Removes the trailing whitespaces from the tag attribute. + fn format_tag_attribute(&mut self, span: Span) { + self.write_source_span_trimmed(span); + + while self.token_span.start() < span.end() && self.token != Token::EOF { + self.bump(); + } + } } #[cfg(test)] @@ -267,6 +277,13 @@ mod tests { assert_format(src, expected); } + #[test] + fn format_inner_tag_attribute_new_line() { + let src = "#!['bar] \n"; + let expected = "#!['bar]\n"; + assert_format(src, expected); + } + #[test] fn format_deprecated_attribute() { let src = " #[ deprecated ] ";