Skip to content

Commit ce47189

Browse files
authored
Merge pull request #2927 from ruby/rewriter
Add RBS::Rewriter and use it in rbs annotate
2 parents 4c09768 + 478206c commit ce47189

8 files changed

Lines changed: 583 additions & 96 deletions

File tree

lib/rbs.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
require "rbs/resolver/type_name_resolver"
5252
require "rbs/ast/comment"
5353
require "rbs/writer"
54+
require "rbs/rewriter"
5455
require "rbs/prototype/helpers"
5556
require "rbs/prototype/rbi"
5657
require "rbs/prototype/rb"

lib/rbs/annotate/rdoc_annotator.rb

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,22 @@ def initialize(source:)
1313
@include_filename = true
1414
end
1515

16-
def annotate_file(path, preserve:)
17-
content = path.read()
16+
def annotate_file(path, preserve: true)
17+
buffer, _, decls = Parser.parse_signature(path.read())
1818

19-
_, _, decls = Parser.parse_signature(content)
19+
rewriter = Rewriter.new(buffer)
20+
annotate_decls(decls, rewriter)
2021

21-
annotate_decls(decls)
22-
23-
path.open("w") do |io|
24-
Writer.new(out: io).preserve!(preserve: preserve).write(decls)
25-
end
22+
path.write(rewriter.string)
2623
end
2724

28-
def annotate_decls(decls, outer: [])
25+
def annotate_decls(decls, rewriter, outer: [])
2926
decls.each do |decl|
3027
case decl
3128
when AST::Declarations::Class, AST::Declarations::Module
32-
annotate_class(decl, outer: outer)
29+
annotate_class(decl, rewriter, outer: outer)
3330
when AST::Declarations::Constant
34-
annotate_constant(decl, outer: outer)
31+
annotate_constant(decl, rewriter, outer: outer)
3532
end
3633
end
3734
end
@@ -244,44 +241,44 @@ def doc_for_attribute(typename, attr_name, require: nil, singleton:, tester:)
244241
end
245242
end
246243

247-
def annotate_class(decl, outer:)
244+
def annotate_class(decl, rewriter, outer:)
248245
annots = annotations(decl)
249246

250247
full_name = resolve_name(decl.name, outer: outer)
251248
unless annots.skip?
252249
text = resolve_doc_source(annots.copy_annotation, tester: annots) { doc_for_class(full_name, tester: annots) }
253250
end
254251

255-
replace_comment(decl, text)
252+
replace_comment(decl, text, rewriter)
256253

257254
unless annots.skip_all?
258255
outer_ = outer + [decl.name.to_namespace]
259256

260257
decl.each_member do |member|
261258
case member
262259
when AST::Members::MethodDefinition
263-
annotate_method(full_name, member)
260+
annotate_method(full_name, member, rewriter)
264261
when AST::Members::Alias
265-
annotate_alias(full_name, member)
262+
annotate_alias(full_name, member, rewriter)
266263
when AST::Members::AttrReader, AST::Members::AttrAccessor, AST::Members::AttrWriter
267-
annotate_attribute(full_name, member)
264+
annotate_attribute(full_name, member, rewriter)
268265
end
269266
end
270267

271-
annotate_decls(decl.each_decl.to_a, outer: outer_)
268+
annotate_decls(decl.each_decl.to_a, rewriter, outer: outer_)
272269
end
273270
end
274271

275-
def annotate_constant(const, outer:)
272+
def annotate_constant(const, rewriter, outer:)
276273
annots = Annotations.new([])
277274

278275
full_name = resolve_name(const.name, outer: outer)
279276
text = doc_for_constant(full_name, tester: annots)
280277

281-
replace_comment(const, text)
278+
replace_comment(const, text, rewriter)
282279
end
283280

284-
def annotate_alias(typename, als)
281+
def annotate_alias(typename, als, rewriter)
285282
annots = annotations(als)
286283

287284
unless annots.skip?
@@ -295,7 +292,7 @@ def annotate_alias(typename, als)
295292
end
296293
end
297294

298-
replace_comment(als, text)
295+
replace_comment(als, text, rewriter)
299296
end
300297

301298
def join_docs(docs, separator: "----")
@@ -311,7 +308,7 @@ def join_docs(docs, separator: "----")
311308
end
312309
end
313310

314-
def annotate_method(typename, method)
311+
def annotate_method(typename, method, rewriter)
315312
annots = annotations(method)
316313

317314
unless annots.skip?
@@ -337,10 +334,10 @@ def annotate_method(typename, method)
337334
}
338335
end
339336

340-
replace_comment(method, text)
337+
replace_comment(method, text, rewriter)
341338
end
342339

343-
def annotate_attribute(typename, attr)
340+
def annotate_attribute(typename, attr, rewriter)
344341
annots = annotations(attr)
345342

346343
unless annots.skip?
@@ -368,18 +365,17 @@ def annotate_attribute(typename, attr)
368365
end
369366
end
370367

371-
replace_comment(attr, text)
368+
replace_comment(attr, text, rewriter)
372369
end
373370

374-
def replace_comment(commented, string)
371+
def replace_comment(commented, string, rewriter)
375372
if string
376373
if string.empty?
377-
commented.instance_variable_set(:@comment, nil)
374+
rewriter.delete_comment(commented.comment) if commented.comment
375+
elsif commented.comment
376+
rewriter.replace_comment(commented.comment, content: string)
378377
else
379-
commented.instance_variable_set(
380-
:@comment,
381-
AST::Comment.new(location: nil, string: string)
382-
)
378+
rewriter.add_comment(commented.location || raise, *commented.annotations.filter_map(&:location), content: string)
383379
end
384380
end
385381
end

lib/rbs/cli.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -962,8 +962,6 @@ def run_annotate(args, options)
962962
source = RBS::Annotate::RDocSource.new()
963963
annotator = RBS::Annotate::RDocAnnotator.new(source: source)
964964

965-
preserve = true
966-
967965
OptionParser.new do |opts|
968966
opts.banner = <<-EOB
969967
Usage: rbs annotate [options...] [files...]
@@ -984,7 +982,7 @@ def run_annotate(args, options)
984982
opts.on("-d", "--dir DIRNAME", "Load RDoc from DIRNAME") {|d| source.extra_dirs << Pathname(d) }
985983
opts.on("--[no-]arglists", "Generate arglists section (defaults to true)") {|b| annotator.include_arg_lists = b }
986984
opts.on("--[no-]filename", "Include source file name in the documentation (defaults to true)") {|b| annotator.include_filename = b }
987-
opts.on("--[no-]preserve", "Try preserve the format of the original file (defaults to true)") {|b| preserve = b }
985+
opts.on("--[no-]preserve", "[Deprecated] It always preserves the format") { stdout.puts "The `--preserve` option is deprecated. The tool always preserves the format of RBS files." }
988986
end.parse!(args)
989987

990988
source.load()
@@ -994,11 +992,11 @@ def run_annotate(args, options)
994992
if path.directory?
995993
Pathname.glob((path + "**/*.rbs").to_s).each do |path|
996994
stdout.puts "Processing #{path}..."
997-
annotator.annotate_file(path, preserve: preserve)
995+
annotator.annotate_file(path)
998996
end
999997
else
1000998
stdout.puts "Processing #{path}..."
1001-
annotator.annotate_file(path, preserve: preserve)
999+
annotator.annotate_file(path)
10021000
end
10031001
end
10041002

lib/rbs/rewriter.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
module RBS
4+
class Rewriter
5+
attr_reader :buffer
6+
7+
def initialize(buffer)
8+
raise "Rewriter only supports toplevel buffers" if buffer.parent
9+
10+
@buffer = buffer
11+
@rewrites = []
12+
end
13+
14+
def rewrite(location, string)
15+
@rewrites.each do |existing_location, _|
16+
if location.start_pos < existing_location.end_pos && existing_location.start_pos < location.end_pos
17+
raise "Overlapping rewrites: #{existing_location} and #{location}"
18+
end
19+
end
20+
21+
@rewrites << [location, string]
22+
self
23+
end
24+
25+
def add_comment(*locations, content:)
26+
earliest = locations.min_by(&:start_pos) or raise "At least one location is required"
27+
insert_pos = earliest.start_pos
28+
indent = " " * earliest.start_column
29+
30+
formatted = format_comment(content, indent)
31+
32+
loc = Location.new(buffer, insert_pos, insert_pos)
33+
rewrite(loc, "#{formatted}\n#{indent}")
34+
end
35+
36+
def replace_comment(comment, content:)
37+
location = comment.location or raise "Comment must have a location"
38+
indent = " " * location.start_column
39+
40+
rewrite(location, format_comment(content, indent))
41+
end
42+
43+
def delete_comment(comment)
44+
location = comment.location or raise "Comment must have a location"
45+
line_start = location.start_pos - location.start_column
46+
line_end = location.end_pos + 1
47+
loc = Location.new(buffer, line_start, line_end)
48+
rewrite(loc, "")
49+
end
50+
51+
def string
52+
result = buffer.content.dup
53+
54+
@rewrites.sort_by { |location, _| location.start_pos }.reverse_each do |location, replacement|
55+
result[location.start_pos...location.end_pos] = replacement
56+
end
57+
58+
result
59+
end
60+
61+
private
62+
63+
def format_comment(content, indent)
64+
content.lines.map do |line|
65+
line = line.chomp
66+
line.empty? ? "#" : "# #{line}"
67+
end.join("\n#{indent}")
68+
end
69+
end
70+
end

sig/annotate/rdoc_annotater.rbs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ module RBS
88

99
def initialize: (source: RDocSource) -> void
1010

11-
def annotate_file: (Pathname, preserve: bool) -> void
11+
def annotate_file: (Pathname) -> void
12+
| %a{deprecated: it preserves the format by default} (Pathname, preserve: bool) -> void
1213

13-
def annotate_decls: (Array[AST::Declarations::t], ?outer: Array[Namespace]) -> void
14+
def annotate_decls: (Array[AST::Declarations::t], Rewriter, ?outer: Array[Namespace]) -> void
1415

1516
interface _PathTester
1617
def test_path: (String) -> bool
@@ -50,15 +51,15 @@ module RBS
5051

5152
def resolve_doc_source: (Annotations::Copy?, tester: _PathTester) { () -> String? } -> String?
5253

53-
def annotate_class: (AST::Declarations::Class | AST::Declarations::Module, outer: Array[Namespace]) -> void
54+
def annotate_class: (AST::Declarations::Class | AST::Declarations::Module, Rewriter, outer: Array[Namespace]) -> void
5455

55-
def annotate_constant: (AST::Declarations::Constant, outer: Array[Namespace]) -> void
56+
def annotate_constant: (AST::Declarations::Constant, Rewriter, outer: Array[Namespace]) -> void
5657

57-
def annotate_method: (TypeName, AST::Members::MethodDefinition) -> void
58+
def annotate_method: (TypeName, AST::Members::MethodDefinition, Rewriter) -> void
5859

59-
def annotate_alias: (TypeName, AST::Members::Alias) -> void
60+
def annotate_alias: (TypeName, AST::Members::Alias, Rewriter) -> void
6061

61-
def annotate_attribute: (TypeName, AST::Members::AttrReader | AST::Members::AttrWriter | AST::Members::AttrAccessor) -> void
62+
def annotate_attribute: (TypeName, AST::Members::AttrReader | AST::Members::AttrWriter | AST::Members::AttrAccessor, Rewriter) -> void
6263

6364
def annotations: (_Annotated) -> Annotations
6465

@@ -70,10 +71,12 @@ module RBS
7071
# - If empty string is given as `comment`, it deletes the original comment.
7172
# - If `nil` is given as `comment`, it keeps the original comment.
7273
#
73-
def replace_comment: (Object & _Commented, String? comment) -> void
74+
def replace_comment: (_Commented, String?, Rewriter) -> void
7475

7576
interface _Commented
76-
def comment: () -> AST::Comment?
77+
%a{pure} def comment: () -> AST::Comment?
78+
%a{pure} def location: () -> Location[untyped, untyped]?
79+
def annotations: () -> Array[AST::Annotation]
7780
end
7881

7982
def resolve_name: (TypeName, outer: Array[Namespace]) -> TypeName

sig/rewriter.rbs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module RBS
2+
# Rewriter performs targeted character-range replacements on Buffer content.
3+
#
4+
# Unlike Writer which regenerates entire source from AST, Rewriter preserves
5+
# everything outside the rewritten ranges, including non-documentation comments.
6+
#
7+
# Rewrite requests are buffered and applied all at once when `#string` is called.
8+
#
9+
class Rewriter
10+
attr_reader buffer: Buffer
11+
12+
# Initialize with a toplevel buffer.
13+
# Raises if the buffer has a parent (non-toplevel).
14+
#
15+
def initialize: (Buffer buffer) -> void
16+
17+
# Register a rewrite request for the given location.
18+
#
19+
def rewrite: (Location[untyped, untyped] location, String string) -> self
20+
21+
# Add a new comment before the earliest of the given locations.
22+
#
23+
def add_comment: (*Location[untyped, untyped] locations, content: String) -> self
24+
25+
# Replace an existing comment's content.
26+
#
27+
def replace_comment: (AST::Comment comment, content: String) -> self
28+
29+
# Delete an existing comment.
30+
#
31+
def delete_comment: (AST::Comment comment) -> self
32+
33+
# Apply all buffered rewrites and return the resulting string.
34+
#
35+
# Raises if any rewrite ranges overlap.
36+
#
37+
def string: () -> String
38+
39+
private
40+
41+
def format_comment: (String content, String indent) -> String
42+
43+
@rewrites: Array[[Location[untyped, untyped], String]]
44+
end
45+
end

0 commit comments

Comments
 (0)