diff --git a/fixtures/small/class_comment_expected.rb b/fixtures/small/class_comment_expected.rb index e07c19e0..3a637949 100644 --- a/fixtures/small/class_comment_expected.rb +++ b/fixtures/small/class_comment_expected.rb @@ -1,6 +1,6 @@ -#adsf +# adsf class Foo - #but this one doesn't get a proceeding newline + # but this one doesn't get a proceeding newline a end diff --git a/fixtures/small/comment_at_least_one_leading_space_actual.rb b/fixtures/small/comment_at_least_one_leading_space_actual.rb new file mode 100644 index 00000000..2eea9770 --- /dev/null +++ b/fixtures/small/comment_at_least_one_leading_space_actual.rb @@ -0,0 +1,40 @@ +#! /usr/bin/env ruby +# frozen_string_literal: true + +#### #### AUTHOR: coder204coolgizmos ### ## + +##DATE: 2026-06-27 +## Cool Gizmos, Inc. + +#### #### +###---------- +#: asdf +#| asdf +#** doc doc +# +# +#This is a class that frobs a gizmo. It: +# * frobs the sprog +# * then turns the sprog into the gizmo. +#### # +#### # +#Keep this class here. +class Foo + #inner class comment + + #method description comment + def self.b(&_blk) + yield + end + + #method description comment + def self.a + #some method comment + + x = b do + #some comment inside the block + y = 4 + # some other comment + end + end +end diff --git a/fixtures/small/comment_at_least_one_leading_space_expected.rb b/fixtures/small/comment_at_least_one_leading_space_expected.rb new file mode 100644 index 00000000..ef5d9ed1 --- /dev/null +++ b/fixtures/small/comment_at_least_one_leading_space_expected.rb @@ -0,0 +1,40 @@ +#! /usr/bin/env ruby +# frozen_string_literal: true + +#### #### AUTHOR: coder204coolgizmos ### ## + +## DATE: 2026-06-27 +## Cool Gizmos, Inc. + +#### #### +###---------- +#: asdf +#| asdf +#** doc doc +# +# +# This is a class that frobs a gizmo. It: +# * frobs the sprog +# * then turns the sprog into the gizmo. +#### # +#### # +# Keep this class here. +class Foo + # inner class comment + + # method description comment + def self.b(&_blk) + yield + end + + # method description comment + def self.a + # some method comment + + x = b do + # some comment inside the block + y = 4 + # some other comment + end + end +end diff --git a/fixtures/small/empty_comments_expected.rb b/fixtures/small/empty_comments_expected.rb index 606dd77f..2ba0bd57 100644 --- a/fixtures/small/empty_comments_expected.rb +++ b/fixtures/small/empty_comments_expected.rb @@ -1,8 +1,8 @@ def foo - #a - #b - #c - #d + # a + # b + # c + # d end if a diff --git a/fixtures/small/inline_comments_expected.rb b/fixtures/small/inline_comments_expected.rb index 81f7f5fc..6849419b 100644 --- a/fixtures/small/inline_comments_expected.rb +++ b/fixtures/small/inline_comments_expected.rb @@ -15,9 +15,9 @@ module MathsAndPhysics # degree_codes: The corresponding JAC codes to the subject(s) # the teacher completed their degree in. CONST = [ - #maths + # maths a, - #physics + # physics b, # foreign language diff --git a/fixtures/small/rspec_its_expected.rb b/fixtures/small/rspec_its_expected.rb index a18c2f35..bb6d5816 100644 --- a/fixtures/small/rspec_its_expected.rb +++ b/fixtures/small/rspec_its_expected.rb @@ -2,19 +2,19 @@ "a" \ "b" ) do - #hi + # hi end it "a" do - #hi + # hi end it "a", flag: true do - #hi + # hi end it "a", flag: true, other: "b", another: false do - #hi + # hi end it("a", flag: true, other: "b", another: false) { 1 } diff --git a/fixtures/small/start_of_file_comments_expected.rb b/fixtures/small/start_of_file_comments_expected.rb index 848d30c6..24083f76 100644 --- a/fixtures/small/start_of_file_comments_expected.rb +++ b/fixtures/small/start_of_file_comments_expected.rb @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -#frozen_string_literal: true +# frozen_string_literal: true def foo a diff --git a/fixtures/small/word_array_trailing_comment_expected.rb b/fixtures/small/word_array_trailing_comment_expected.rb index 727538fd..ca5b5f77 100644 --- a/fixtures/small/word_array_trailing_comment_expected.rb +++ b/fixtures/small/word_array_trailing_comment_expected.rb @@ -1,4 +1,4 @@ -#c +# c a = %w[ b ] diff --git a/librubyfmt/src/comment_block.rs b/librubyfmt/src/comment_block.rs index 30fa0fd8..d2e16e07 100644 --- a/librubyfmt/src/comment_block.rs +++ b/librubyfmt/src/comment_block.rs @@ -12,6 +12,15 @@ pub struct CommentBlock { comments: Vec>, } +const SPECIAL_CHARS_AFTER_HASH_TO_IGNORE: [u8; 6] = [ + b'!', // #! 'shebang' comments cannot be modified since they are Unix directives. + b'=', // #=== is a common delimiting pattern. + b'-', // #--- is a common delimiting pattern. + b':', // #: is used for RBS annotations. + b'|', // #| is used for RBS annotations. + b'*', // #** is used for doxygen comments. +]; + impl CommentBlock { pub fn new(span: Range, comments: Vec>) -> Self { CommentBlock { span, comments } @@ -54,6 +63,43 @@ impl CommentBlock { }) } + pub fn enforce_at_least_one_space_after_comment_symbol(mut self) -> Self { + for comment in &mut self.comments { + // Ignore empty vecs -- these represent blank lines between + // groups of comments + if comment.is_empty() || comment.starts_with(b"=begin") { + continue; + } + if let Some(start_idx) = comment.iter().position(|&c| c == b'#') { + // Allow any amount of '#' after the leading '#'. + let collapse = comment[start_idx..] + .iter() + .take_while(|&&b| b == b'#') + .count(); + + let anchor = start_idx + collapse; + if anchor == comment.len() { + // No point truncating an empty comment. + continue; + } + + if comment[anchor].is_ascii_whitespace() { + // Any amount of spaces after `#` is fine. + continue; + } + + let next_char = comment[anchor]; + if SPECIAL_CHARS_AFTER_HASH_TO_IGNORE.contains(&next_char) { + continue; + } + + // Add one space + comment.to_mut().insert(anchor, b' '); + } + } + self + } + /// Set each comment's leading indent to exactly `indent_depth` spaces pub fn apply_spaces(mut self, indent_depth: ColNumber) -> Self { let target = indent_depth as usize; diff --git a/librubyfmt/src/parser_state.rs b/librubyfmt/src/parser_state.rs index 2261886b..e157e44b 100644 --- a/librubyfmt/src/parser_state.rs +++ b/librubyfmt/src/parser_state.rs @@ -382,8 +382,11 @@ impl<'src> ParserState<'src> { } pub(crate) fn insert_comment_collection(&mut self, comments: CommentBlock) { - self.comments_to_insert - .merge(comments.apply_spaces(self.spaces_after_last_newline)); + self.comments_to_insert.merge( + comments + .enforce_at_least_one_space_after_comment_symbol() + .apply_spaces(self.spaces_after_last_newline), + ); } pub(crate) fn emit_op(&mut self, op: &'src [u8]) { @@ -890,6 +893,7 @@ impl<'src> ParserState<'src> { self.on_line(1); } Some(comments) => { + let comments = comments.enforce_at_least_one_space_after_comment_symbol(); let line_count = comments.line_count(); for token in comments.into_line_tokens() { self.push_concrete_token(token);