From 3931a1529d54050ab508aaedd48aea70a0b90cfa Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 8 Apr 2026 15:06:14 +0900 Subject: [PATCH 1/5] Add RBS::Rewriter for targeted buffer content replacement Rewriter performs character-range replacements on Buffer content using RBS::Location, preserving everything outside the rewritten ranges. Unlike Writer which regenerates entire source from AST, this allows modifying specific parts without losing non-documentation comments. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rbs.rb | 1 + lib/rbs/rewriter.rb | 35 +++++++++++++++ sig/rewriter.rbs | 31 +++++++++++++ test/rbs/rewriter_test.rb | 94 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 lib/rbs/rewriter.rb create mode 100644 sig/rewriter.rbs create mode 100644 test/rbs/rewriter_test.rb diff --git a/lib/rbs.rb b/lib/rbs.rb index d5388d849..bbc8c8382 100644 --- a/lib/rbs.rb +++ b/lib/rbs.rb @@ -51,6 +51,7 @@ require "rbs/resolver/type_name_resolver" require "rbs/ast/comment" require "rbs/writer" +require "rbs/rewriter" require "rbs/prototype/helpers" require "rbs/prototype/rbi" require "rbs/prototype/rb" diff --git a/lib/rbs/rewriter.rb b/lib/rbs/rewriter.rb new file mode 100644 index 000000000..4bc04faef --- /dev/null +++ b/lib/rbs/rewriter.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module RBS + class Rewriter + attr_reader :buffer + + def initialize(buffer) + raise "Rewriter only supports toplevel buffers" if buffer.parent + + @buffer = buffer + @rewrites = [] + end + + def rewrite(location, string) + @rewrites.each do |existing_location, _| + if location.start_pos < existing_location.end_pos && existing_location.start_pos < location.end_pos + raise "Overlapping rewrites: #{existing_location} and #{location}" + end + end + + @rewrites << [location, string] + self + end + + def string + result = buffer.content.dup + + @rewrites.sort_by { |location, _| location.start_pos }.reverse_each do |location, replacement| + result[location.start_pos...location.end_pos] = replacement + end + + result + end + end +end diff --git a/sig/rewriter.rbs b/sig/rewriter.rbs new file mode 100644 index 000000000..510f83027 --- /dev/null +++ b/sig/rewriter.rbs @@ -0,0 +1,31 @@ +module RBS + # Rewriter performs targeted character-range replacements on Buffer content. + # + # Unlike Writer which regenerates entire source from AST, Rewriter preserves + # everything outside the rewritten ranges, including non-documentation comments. + # + # Rewrite requests are buffered and applied all at once when `#string` is called. + # + class Rewriter + attr_reader buffer: Buffer + + # Initialize with a toplevel buffer. + # Raises if the buffer has a parent (non-toplevel). + # + def initialize: (Buffer buffer) -> void + + # Register a rewrite request for the given location. + # + def rewrite: (Location[untyped, untyped] location, String string) -> self + + # Apply all buffered rewrites and return the resulting string. + # + # Raises if any rewrite ranges overlap. + # + def string: () -> String + + private + + @rewrites: Array[[Location[untyped, untyped], String]] + end +end diff --git a/test/rbs/rewriter_test.rb b/test/rbs/rewriter_test.rb new file mode 100644 index 000000000..210ffcdf5 --- /dev/null +++ b/test/rbs/rewriter_test.rb @@ -0,0 +1,94 @@ +require "test_helper" + +class RBS::RewriterTest < Test::Unit::TestCase + def make_location(buffer, start_pos, end_pos) + RBS::Location.new(buffer, start_pos, end_pos) + end + + def test_no_rewrites + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rewriter = RBS::Rewriter.new(buffer) + + assert_equal "class Foo\nend\n", rewriter.string + end + + def test_single_rewrite + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rewriter = RBS::Rewriter.new(buffer) + + loc = make_location(buffer, 6, 9) # "Foo" + rewriter.rewrite(loc, "Bar") + + assert_equal "class Bar\nend\n", rewriter.string + end + + def test_multiple_non_overlapping_rewrites + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\n def bar: () -> String\nend\n") + rewriter = RBS::Rewriter.new(buffer) + + loc_name = make_location(buffer, 6, 9) # "Foo" + loc_method = make_location(buffer, 16, 19) # "bar" + rewriter.rewrite(loc_name, "Baz") + rewriter.rewrite(loc_method, "qux") + + assert_equal "class Baz\n def qux: () -> String\nend\n", rewriter.string + end + + def test_rewrite_with_different_length + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rewriter = RBS::Rewriter.new(buffer) + + loc = make_location(buffer, 6, 9) # "Foo" + rewriter.rewrite(loc, "LongerName") + + assert_equal "class LongerName\nend\n", rewriter.string + end + + def test_rewrite_deletion + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rewriter = RBS::Rewriter.new(buffer) + + loc = make_location(buffer, 5, 9) # " Foo" + rewriter.rewrite(loc, "") + + assert_equal "class\nend\n", rewriter.string + end + + def test_rewrite_insertion + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rewriter = RBS::Rewriter.new(buffer) + + loc = make_location(buffer, 9, 9) # empty range at end of "Foo" + rewriter.rewrite(loc, "[A]") + + assert_equal "class Foo[A]\nend\n", rewriter.string + end + + def test_overlapping_rewrites_raises + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rewriter = RBS::Rewriter.new(buffer) + + loc1 = make_location(buffer, 0, 9) # "class Foo" + loc2 = make_location(buffer, 6, 13) # "Foo\nend" + rewriter.rewrite(loc1, "module Bar") + + assert_raise(RuntimeError) { rewriter.rewrite(loc2, "Baz") } + end + + def test_non_toplevel_buffer_raises + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "hello\nworld\n") + sub = buffer.sub_buffer(lines: [0...5]) + + assert_raise(RuntimeError) { RBS::Rewriter.new(sub) } + end + + def test_rewrite_returns_self + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rewriter = RBS::Rewriter.new(buffer) + + loc = make_location(buffer, 6, 9) + result = rewriter.rewrite(loc, "Bar") + + assert_same rewriter, result + end +end From b8c0aeee29f6ac449e6cfe2f0c4fcf0915e746d4 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 8 Apr 2026 16:10:29 +0900 Subject: [PATCH 2/5] Add add_comment, replace_comment, delete_comment to Rewriter Split the comment rewriting API into three focused methods: - add_comment(*locations, content:) inserts before the earliest location - replace_comment(comment, content:) rewrites comment content in place - delete_comment(comment) removes comment lines including indentation Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rbs/rewriter.rb | 35 +++++ sig/rewriter.rbs | 14 ++ test/rbs/rewriter_test.rb | 309 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 344 insertions(+), 14 deletions(-) diff --git a/lib/rbs/rewriter.rb b/lib/rbs/rewriter.rb index 4bc04faef..ec1b1d94d 100644 --- a/lib/rbs/rewriter.rb +++ b/lib/rbs/rewriter.rb @@ -22,6 +22,32 @@ def rewrite(location, string) self end + def add_comment(*locations, content:) + earliest = locations.min_by(&:start_pos) or raise "At least one location is required" + insert_pos = earliest.start_pos + indent = " " * earliest.start_column + + formatted = format_comment(content, indent) + + loc = Location.new(buffer, insert_pos, insert_pos) + rewrite(loc, "#{formatted}\n#{indent}") + end + + def replace_comment(comment, content:) + location = comment.location or raise "Comment must have a location" + indent = " " * location.start_column + + rewrite(location, format_comment(content, indent)) + end + + def delete_comment(comment) + location = comment.location or raise "Comment must have a location" + line_start = location.start_pos - location.start_column + line_end = location.end_pos + 1 + loc = Location.new(buffer, line_start, line_end) + rewrite(loc, "") + end + def string result = buffer.content.dup @@ -31,5 +57,14 @@ def string result end + + private + + def format_comment(content, indent) + content.lines.map do |line| + line = line.chomp + line.empty? ? "#" : "# #{line}" + end.join("\n#{indent}") + end end end diff --git a/sig/rewriter.rbs b/sig/rewriter.rbs index 510f83027..6edeafbb7 100644 --- a/sig/rewriter.rbs +++ b/sig/rewriter.rbs @@ -18,6 +18,18 @@ module RBS # def rewrite: (Location[untyped, untyped] location, String string) -> self + # Add a new comment before the earliest of the given locations. + # + def add_comment: (*Location[untyped, untyped] locations, content: String) -> self + + # Replace an existing comment's content. + # + def replace_comment: (AST::Comment comment, content: String) -> self + + # Delete an existing comment. + # + def delete_comment: (AST::Comment comment) -> self + # Apply all buffered rewrites and return the resulting string. # # Raises if any rewrite ranges overlap. @@ -26,6 +38,8 @@ module RBS private + def format_comment: (String content, String indent) -> String + @rewrites: Array[[Location[untyped, untyped], String]] end end diff --git a/test/rbs/rewriter_test.rb b/test/rbs/rewriter_test.rb index 210ffcdf5..2471aada2 100644 --- a/test/rbs/rewriter_test.rb +++ b/test/rbs/rewriter_test.rb @@ -6,24 +6,40 @@ def make_location(buffer, start_pos, end_pos) end def test_no_rewrites - buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rbs = <<~RBS + class Foo + end + RBS + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) rewriter = RBS::Rewriter.new(buffer) - assert_equal "class Foo\nend\n", rewriter.string + assert_equal rbs, rewriter.string end def test_single_rewrite - buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rbs = <<~RBS + class Foo + end + RBS + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) rewriter = RBS::Rewriter.new(buffer) loc = make_location(buffer, 6, 9) # "Foo" rewriter.rewrite(loc, "Bar") - assert_equal "class Bar\nend\n", rewriter.string + assert_equal <<~RBS, rewriter.string + class Bar + end + RBS end def test_multiple_non_overlapping_rewrites - buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\n def bar: () -> String\nend\n") + rbs = <<~RBS + class Foo + def bar: () -> String + end + RBS + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) rewriter = RBS::Rewriter.new(buffer) loc_name = make_location(buffer, 6, 9) # "Foo" @@ -31,41 +47,70 @@ def test_multiple_non_overlapping_rewrites rewriter.rewrite(loc_name, "Baz") rewriter.rewrite(loc_method, "qux") - assert_equal "class Baz\n def qux: () -> String\nend\n", rewriter.string + assert_equal <<~RBS, rewriter.string + class Baz + def qux: () -> String + end + RBS end def test_rewrite_with_different_length - buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rbs = <<~RBS + class Foo + end + RBS + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) rewriter = RBS::Rewriter.new(buffer) loc = make_location(buffer, 6, 9) # "Foo" rewriter.rewrite(loc, "LongerName") - assert_equal "class LongerName\nend\n", rewriter.string + assert_equal <<~RBS, rewriter.string + class LongerName + end + RBS end def test_rewrite_deletion - buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rbs = <<~RBS + class Foo + end + RBS + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) rewriter = RBS::Rewriter.new(buffer) loc = make_location(buffer, 5, 9) # " Foo" rewriter.rewrite(loc, "") - assert_equal "class\nend\n", rewriter.string + assert_equal <<~RBS, rewriter.string + class + end + RBS end def test_rewrite_insertion - buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rbs = <<~RBS + class Foo + end + RBS + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) rewriter = RBS::Rewriter.new(buffer) loc = make_location(buffer, 9, 9) # empty range at end of "Foo" rewriter.rewrite(loc, "[A]") - assert_equal "class Foo[A]\nend\n", rewriter.string + assert_equal <<~RBS, rewriter.string + class Foo[A] + end + RBS end def test_overlapping_rewrites_raises - buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rbs = <<~RBS + class Foo + end + RBS + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) rewriter = RBS::Rewriter.new(buffer) loc1 = make_location(buffer, 0, 9) # "class Foo" @@ -83,7 +128,11 @@ def test_non_toplevel_buffer_raises end def test_rewrite_returns_self - buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: "class Foo\nend\n") + rbs = <<~RBS + class Foo + end + RBS + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) rewriter = RBS::Rewriter.new(buffer) loc = make_location(buffer, 6, 9) @@ -91,4 +140,236 @@ def test_rewrite_returns_self assert_same rewriter, result end + + def test_replace_comment_single_line + rbs = <<~RBS + # Hello + class Foo + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.replace_comment(decls[0].comment, content: "Goodbye\n") + + assert_equal <<~RBS, rewriter.string + # Goodbye + class Foo + end + RBS + end + + def test_replace_comment_multi_line + rbs = <<~RBS + # Hello + # World + class Foo + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.replace_comment(decls[0].comment, content: "Goodbye\nUniverse\n") + + assert_equal <<~RBS, rewriter.string + # Goodbye + # Universe + class Foo + end + RBS + end + + def test_replace_comment_with_empty_line + rbs = <<~RBS + # Hello + # + # World + class Foo + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.replace_comment(decls[0].comment, content: "Goodbye\n\nUniverse\n") + + assert_equal <<~RBS, rewriter.string + # Goodbye + # + # Universe + class Foo + end + RBS + end + + def test_replace_comment_indented + rbs = <<~RBS + class Foo + # Hello + # World + def bar: () -> void + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.replace_comment(decls[0].members[0].comment, content: "Goodbye\nUniverse\n") + + assert_equal <<~RBS, rewriter.string + class Foo + # Goodbye + # Universe + def bar: () -> void + end + RBS + end + + def test_replace_comment_with_annotation + rbs = <<~RBS + class Foo + # Hello + %a{deprecated} + def foo: () -> void + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.replace_comment(decls[0].members[0].comment, content: "Goodbye\n") + + assert_equal <<~RBS, rewriter.string + class Foo + # Goodbye + %a{deprecated} + def foo: () -> void + end + RBS + end + + def test_delete_comment + rbs = <<~RBS + # Hello + class Foo + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.delete_comment(decls[0].comment) + + assert_equal <<~RBS, rewriter.string + class Foo + end + RBS + end + + def test_delete_comment_indented + rbs = <<~RBS + class Foo + # Hello + # World + def bar: () -> void + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.delete_comment(decls[0].members[0].comment) + + assert_equal <<~RBS, rewriter.string + class Foo + def bar: () -> void + end + RBS + end + + def test_delete_comment_with_annotation + rbs = <<~RBS + class Foo + # Hello + %a{deprecated} + def foo: () -> void + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.delete_comment(decls[0].members[0].comment) + + assert_equal <<~RBS, rewriter.string + class Foo + %a{deprecated} + def foo: () -> void + end + RBS + end + + def test_add_comment + rbs = <<~RBS + class Foo + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.add_comment(decls[0].location, content: "New comment\n") + + assert_equal <<~RBS, rewriter.string + # New comment + class Foo + end + RBS + end + + def test_add_comment_indented + rbs = <<~RBS + class Foo + def bar: () -> void + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.add_comment(decls[0].members[0].location, content: "New comment\nSecond line\n") + + assert_equal <<~RBS, rewriter.string + class Foo + # New comment + # Second line + def bar: () -> void + end + RBS + end + + def test_add_comment_with_annotation + rbs = <<~RBS + class Foo + %a{deprecated} + def foo: () -> void + end + RBS + _, _, decls = RBS::Parser.parse_signature(rbs) + member = decls[0].members[0] + buffer = RBS::Buffer.new(name: Pathname("test.rbs"), content: rbs) + rewriter = RBS::Rewriter.new(buffer) + + rewriter.add_comment(member.annotations[0].location, member.location, content: "This is foo\n") + + assert_equal <<~RBS, rewriter.string + class Foo + # This is foo + %a{deprecated} + def foo: () -> void + end + RBS + end end From d1adf91feb2356823ee785e9069ca17e029af4a0 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 8 Apr 2026 16:34:05 +0900 Subject: [PATCH 3/5] Refactor rbs annotate to use Rewriter instead of Writer Replace AST mutation + Writer serialization with Rewriter-based comment editing, preserving non-documentation comments and original formatting in annotated RBS files. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rbs/annotate/rdoc_annotator.rb | 56 ++++++------- sig/annotate/rdoc_annotater.rbs | 18 ++-- test/rbs/annotate/rdoc_annotator_test.rb | 101 +++++++++++------------ 3 files changed, 86 insertions(+), 89 deletions(-) diff --git a/lib/rbs/annotate/rdoc_annotator.rb b/lib/rbs/annotate/rdoc_annotator.rb index 06958998f..9bdcaddb6 100644 --- a/lib/rbs/annotate/rdoc_annotator.rb +++ b/lib/rbs/annotate/rdoc_annotator.rb @@ -14,24 +14,21 @@ def initialize(source:) end def annotate_file(path, preserve:) - content = path.read() + buffer, _, decls = Parser.parse_signature(path.read()) - _, _, decls = Parser.parse_signature(content) + rewriter = Rewriter.new(buffer) + annotate_decls(decls, rewriter) - annotate_decls(decls) - - path.open("w") do |io| - Writer.new(out: io).preserve!(preserve: preserve).write(decls) - end + path.write(rewriter.string) end - def annotate_decls(decls, outer: []) + def annotate_decls(decls, rewriter, outer: []) decls.each do |decl| case decl when AST::Declarations::Class, AST::Declarations::Module - annotate_class(decl, outer: outer) + annotate_class(decl, rewriter, outer: outer) when AST::Declarations::Constant - annotate_constant(decl, outer: outer) + annotate_constant(decl, rewriter, outer: outer) end end end @@ -244,7 +241,7 @@ def doc_for_attribute(typename, attr_name, require: nil, singleton:, tester:) end end - def annotate_class(decl, outer:) + def annotate_class(decl, rewriter, outer:) annots = annotations(decl) full_name = resolve_name(decl.name, outer: outer) @@ -252,7 +249,7 @@ def annotate_class(decl, outer:) text = resolve_doc_source(annots.copy_annotation, tester: annots) { doc_for_class(full_name, tester: annots) } end - replace_comment(decl, text) + replace_comment(decl, text, rewriter) unless annots.skip_all? outer_ = outer + [decl.name.to_namespace] @@ -260,28 +257,28 @@ def annotate_class(decl, outer:) decl.each_member do |member| case member when AST::Members::MethodDefinition - annotate_method(full_name, member) + annotate_method(full_name, member, rewriter) when AST::Members::Alias - annotate_alias(full_name, member) + annotate_alias(full_name, member, rewriter) when AST::Members::AttrReader, AST::Members::AttrAccessor, AST::Members::AttrWriter - annotate_attribute(full_name, member) + annotate_attribute(full_name, member, rewriter) end end - annotate_decls(decl.each_decl.to_a, outer: outer_) + annotate_decls(decl.each_decl.to_a, rewriter, outer: outer_) end end - def annotate_constant(const, outer:) + def annotate_constant(const, rewriter, outer:) annots = Annotations.new([]) full_name = resolve_name(const.name, outer: outer) text = doc_for_constant(full_name, tester: annots) - replace_comment(const, text) + replace_comment(const, text, rewriter) end - def annotate_alias(typename, als) + def annotate_alias(typename, als, rewriter) annots = annotations(als) unless annots.skip? @@ -295,7 +292,7 @@ def annotate_alias(typename, als) end end - replace_comment(als, text) + replace_comment(als, text, rewriter) end def join_docs(docs, separator: "----") @@ -311,7 +308,7 @@ def join_docs(docs, separator: "----") end end - def annotate_method(typename, method) + def annotate_method(typename, method, rewriter) annots = annotations(method) unless annots.skip? @@ -337,10 +334,10 @@ def annotate_method(typename, method) } end - replace_comment(method, text) + replace_comment(method, text, rewriter) end - def annotate_attribute(typename, attr) + def annotate_attribute(typename, attr, rewriter) annots = annotations(attr) unless annots.skip? @@ -368,18 +365,17 @@ def annotate_attribute(typename, attr) end end - replace_comment(attr, text) + replace_comment(attr, text, rewriter) end - def replace_comment(commented, string) + def replace_comment(commented, string, rewriter) if string if string.empty? - commented.instance_variable_set(:@comment, nil) + rewriter.delete_comment(commented.comment) if commented.comment + elsif commented.comment + rewriter.replace_comment(commented.comment, content: string) else - commented.instance_variable_set( - :@comment, - AST::Comment.new(location: nil, string: string) - ) + rewriter.add_comment(commented.location || raise, *commented.annotations.filter_map(&:location), content: string) end end end diff --git a/sig/annotate/rdoc_annotater.rbs b/sig/annotate/rdoc_annotater.rbs index 5cb4dc712..5699b41f4 100644 --- a/sig/annotate/rdoc_annotater.rbs +++ b/sig/annotate/rdoc_annotater.rbs @@ -10,7 +10,7 @@ module RBS def annotate_file: (Pathname, preserve: bool) -> void - def annotate_decls: (Array[AST::Declarations::t], ?outer: Array[Namespace]) -> void + def annotate_decls: (Array[AST::Declarations::t], Rewriter, ?outer: Array[Namespace]) -> void interface _PathTester def test_path: (String) -> bool @@ -50,15 +50,15 @@ module RBS def resolve_doc_source: (Annotations::Copy?, tester: _PathTester) { () -> String? } -> String? - def annotate_class: (AST::Declarations::Class | AST::Declarations::Module, outer: Array[Namespace]) -> void + def annotate_class: (AST::Declarations::Class | AST::Declarations::Module, Rewriter, outer: Array[Namespace]) -> void - def annotate_constant: (AST::Declarations::Constant, outer: Array[Namespace]) -> void + def annotate_constant: (AST::Declarations::Constant, Rewriter, outer: Array[Namespace]) -> void - def annotate_method: (TypeName, AST::Members::MethodDefinition) -> void + def annotate_method: (TypeName, AST::Members::MethodDefinition, Rewriter) -> void - def annotate_alias: (TypeName, AST::Members::Alias) -> void + def annotate_alias: (TypeName, AST::Members::Alias, Rewriter) -> void - def annotate_attribute: (TypeName, AST::Members::AttrReader | AST::Members::AttrWriter | AST::Members::AttrAccessor) -> void + def annotate_attribute: (TypeName, AST::Members::AttrReader | AST::Members::AttrWriter | AST::Members::AttrAccessor, Rewriter) -> void def annotations: (_Annotated) -> Annotations @@ -70,10 +70,12 @@ module RBS # - If empty string is given as `comment`, it deletes the original comment. # - If `nil` is given as `comment`, it keeps the original comment. # - def replace_comment: (Object & _Commented, String? comment) -> void + def replace_comment: (_Commented, String?, Rewriter) -> void interface _Commented - def comment: () -> AST::Comment? + %a{pure} def comment: () -> AST::Comment? + %a{pure} def location: () -> Location[untyped, untyped]? + def annotations: () -> Array[AST::Annotation] end def resolve_name: (TypeName, outer: Array[Namespace]) -> TypeName diff --git a/test/rbs/annotate/rdoc_annotator_test.rb b/test/rbs/annotate/rdoc_annotator_test.rb index 2491ee537..775e6aa03 100644 --- a/test/rbs/annotate/rdoc_annotator_test.rb +++ b/test/rbs/annotate/rdoc_annotator_test.rb @@ -28,8 +28,8 @@ def load_source(files) end def parse_rbs(src) - _, _, decls = RBS::Parser.parse_signature(src) - decls + buffer, _, decls = RBS::Parser.parse_signature(src) + [buffer, decls] end def tester(*false_paths) @@ -57,7 +57,7 @@ class Helper } ) - decls = parse_rbs(<<-RBS) + buffer, decls = parse_rbs(<<-RBS) class CLI class Helper end @@ -66,28 +66,24 @@ class Helper annotator = RBS::Annotate::RDocAnnotator.new(source: source) - decls[0].tap do |decl| - annotator.annotate_class(decl, outer: []) + rewriter = RBS::Rewriter.new(buffer) + annotator.annotate_class(decls[0], rewriter, outer: []) - assert_equal <<-TEXT, decl.comment.string - -This is a doc for CLI. - - -This is another doc for CLI. - - TEXT - - decl.members[0].tap do |decl| - annotator.annotate_class(decl, outer: [RBS::TypeName.parse("CLI").to_namespace]) - - assert_equal <<-TEXT, decl.comment.string - -This is a doc for CLI::Helper - - TEXT - end - end + assert_annotated <<-RBS, rewriter +# +# This is a doc for CLI. +# +# +# This is another doc for CLI. +# +class CLI + # + # This is a doc for CLI::Helper + # + class Helper + end +end + RBS end def test_docs_for_method_method @@ -143,12 +139,8 @@ def m1; end assert_nil annotator.doc_for_method(RBS::TypeName.parse("Foo"), instance_method: :m4=, tester: tester) end - def assert_annotated_decls(expected, decls) - strio = StringIO.new - writer = RBS::Writer.new(out: strio) - writer.write(decls) - - assert_equal expected, strio.string + def assert_annotated(expected, rewriter) + assert_equal expected, rewriter.string end def test_annotate1_defs @@ -197,7 +189,7 @@ def m1; end annotator = RBS::Annotate::RDocAnnotator.new(source: source) - decls = parse_rbs(<<-RBS) + buffer, decls = parse_rbs(<<-RBS) class Foo def m1: () -> void @@ -235,9 +227,10 @@ def self.m5: () -> void end RBS - annotator.annotate_decls(decls) + rewriter = RBS::Rewriter.new(buffer) + annotator.annotate_decls(decls, rewriter) - assert_annotated_decls(<<-RBS, decls) + assert_annotated(<<-RBS, rewriter) # # Doc for Foo # @@ -342,7 +335,7 @@ def m3; end } ) - decls = parse_rbs(<<-RBS) + buffer, decls = parse_rbs(<<-RBS) class Foo def m1: () -> void @@ -355,9 +348,10 @@ def m1: () -> void RBS annotator = RBS::Annotate::RDocAnnotator.new(source: source) - annotator.annotate_decls(decls) + rewriter = RBS::Rewriter.new(buffer) + annotator.annotate_decls(decls, rewriter) - assert_annotated_decls(<<-RBS, decls) + assert_annotated(<<-RBS, rewriter) class Foo # # Doc for m1 (attr_accessor) @@ -471,7 +466,7 @@ def foo; end } ) - decls = parse_rbs(<<-RBS) + buffer, decls = parse_rbs(<<-RBS) %a{annotate:rdoc:skip} class Foo def foo: () -> void @@ -484,9 +479,10 @@ def foo: () -> void RBS annotator = RBS::Annotate::RDocAnnotator.new(source: source) - annotator.annotate_decls(decls) + rewriter = RBS::Rewriter.new(buffer) + annotator.annotate_decls(decls, rewriter) - assert_annotated_decls(<<-RBS, decls) + assert_annotated(<<-RBS, rewriter) %a{annotate:rdoc:skip} class Foo # # Doc of Foo from bar.rb # @@ -579,7 +576,7 @@ def self.bar; end } ) - decls = parse_rbs(<<-RBS) + buffer, decls = parse_rbs(<<-RBS) %a{annotate:rdoc:copy:Foo} class A %a{annotate:rdoc:copy:Foo#foo} @@ -591,9 +588,10 @@ def b: () -> void RBS annotator = RBS::Annotate::RDocAnnotator.new(source: source) - annotator.annotate_decls(decls) + rewriter = RBS::Rewriter.new(buffer) + annotator.annotate_decls(decls, rewriter) - assert_annotated_decls(<<-RBS, decls) + assert_annotated(<<-RBS, rewriter) # # This is doc for Foo # @@ -632,16 +630,17 @@ def initialize; end } ) - decls = parse_rbs(<<-RBS) + buffer, decls = parse_rbs(<<-RBS) class Foo def initialize: () -> void end RBS annotator = RBS::Annotate::RDocAnnotator.new(source: source) - annotator.annotate_decls(decls) + rewriter = RBS::Rewriter.new(buffer) + annotator.annotate_decls(decls, rewriter) - assert_annotated_decls(<<-RBS, decls) + assert_annotated(<<-RBS, rewriter) class Foo #