Skip to content
Merged
Show file tree
Hide file tree
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
19 changes: 18 additions & 1 deletion scripts/rbs_format.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,30 @@ def format(source)
formatter = SyntaxTree::RBS::Formatter.new(protected_source, [], 80)
Format.new(formatter).visit(SyntaxTree::RBS.parse(protected_source))
formatter.flush
formatter.output.join.gsub(
formatted = formatter.output.join.gsub(
/# (?:class|module) #{Regexp.escape(marker)}-(\d+)\n *[^\n]+$/
) do
restorations.fetch(Regexp.last_match(1).to_i)
end

ensure_comments_preserved!(source, formatted)
formatted
end

def ensure_comments_preserved!(source, formatted)
return if comments(source) == comments(formatted)

raise "RBS formatter cannot safely preserve comments; format manually"
end

def comments(source)
RBS::Parser.lex(source).value.filter_map do |token|
token.value.sub(/[ \t\r]+\z/, "") if token.comment?
end
end

private_class_method :ensure_comments_preserved!, :comments

def alias_declarations(declarations)
declarations.flat_map do |declaration|
case declaration
Expand Down
74 changes: 74 additions & 0 deletions test/scripts/rbs_format_comment_loss_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require "minitest/autorun"
require "tempfile"

require_relative "../../scripts/rbs_format"

class RBSFormatCommentLossTest < Minitest::Test
LOSSY_SOURCES = [
"type label = String # keep explanation\n",
"Example: String # keep explanation\n",
"class Example\n def call: -> String # keep explanation\nend\n"
].freeze

def test_format_refuses_results_that_lose_trailing_comments
LOSSY_SOURCES.each do |source|
error = assert_raises(RuntimeError) { RBSFormat.format(source) }

assert_equal("RBS formatter cannot safely preserve comments; format manually", error.message)
end
end

def test_check_and_write_paths_leave_lossy_source_unchanged
LOSSY_SOURCES.each do |source|
Tempfile.create(["rbs-format-comment-loss", ".rbs"]) do |file|
file.write(source)
file.flush

assert_raises(RuntimeError) { RBSFormat.run([file.path], check: true) }
assert_equal(source, File.read(file.path))

assert_raises(RuntimeError) { RBSFormat.run([file.path], check: false) }
assert_equal(source, File.read(file.path))
end
end
end

def test_preserved_comments_and_literal_hashes_still_format_idempotently
source = <<~RBS
# first comment
# second comment
# first comment
%a{# not a comment}
type label = "# not a comment"
class Alias = ::String # alias comment
RBS
source = source.sub("# first comment\n", "# first comment \n")

formatted = RBSFormat.format(source)

assert_equal(formatted, RBSFormat.format(formatted))
assert_equal(
["# first comment", "# second comment", "# first comment", "# alias comment"],
comments(formatted)
)
end

def test_crlf_comment_whitespace_does_not_look_lossy
source = "# keep explanation\r\nclass Example\r\nend\r\n"

formatted = RBSFormat.format(source)

assert_equal("# keep explanation\nclass Example\nend\n", formatted)
assert_equal(formatted, RBSFormat.format(formatted))
end

private

def comments(source)
RBS::Parser.lex(source).value.select(&:comment?).map do |token|
token.value.sub(/[ \t\r]+\z/, "")
end
end
end