Skip to content

Commit 15d2614

Browse files
committed
Add CommentBlock
1 parent 13478cf commit 15d2614

File tree

4 files changed

+545
-0
lines changed

4 files changed

+545
-0
lines changed

lib/rbs.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
require "rbs/ast/members"
2626
require "rbs/ast/annotation"
2727
require "rbs/ast/visitor"
28+
require "rbs/ast/ruby/comment_block"
2829
require "rbs/ast/ruby/helpers/constant_helper"
2930
require "rbs/ast/ruby/helpers/location_helper"
3031
require "rbs/ast/ruby/annotations"

lib/rbs/ast/ruby/comment_block.rb

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# frozen_string_literal: true
2+
3+
module RBS
4+
module AST
5+
module Ruby
6+
class CommentBlock
7+
attr_reader :name, :offsets, :comment_buffer
8+
9+
def initialize(source_buffer, comments)
10+
@name = source_buffer.name
11+
12+
@offsets = []
13+
14+
# Assume the comment starts with a prefix whitespace
15+
prefix_str = "# "
16+
17+
ranges = [] #: Array[Range[Integer]]
18+
19+
comments.each do |comment|
20+
tuple = [comment, 2] #: [Prism::Comment, Integer]
21+
22+
unless comment.location.slice.start_with?(prefix_str)
23+
tuple[1] = 1
24+
end
25+
26+
offsets << tuple
27+
28+
start_char = comment.location.start_character_offset + tuple[1]
29+
end_char = comment.location.end_character_offset
30+
ranges << (start_char ... end_char)
31+
end
32+
33+
@comment_buffer = source_buffer.sub_buffer(lines: ranges)
34+
end
35+
36+
def leading?
37+
comment = offsets[0][0] or raise
38+
comment.location.start_line_slice.index(/\S/) ? false : true
39+
end
40+
41+
def trailing?
42+
comment = offsets[0][0] or raise
43+
comment.location.start_line_slice.index(/\S/) ? true : false
44+
end
45+
46+
def start_line
47+
comments[0].location.start_line
48+
end
49+
50+
def end_line
51+
comments[-1].location.end_line
52+
end
53+
54+
def line_starts
55+
offsets.map do |comment, prefix_size|
56+
comment.location.start_character_offset + prefix_size
57+
end
58+
end
59+
60+
def self.build(buffer, comments)
61+
blocks = [] #: Array[CommentBlock]
62+
63+
comments = comments.filter {|comment| comment.is_a?(Prism::InlineComment) }
64+
65+
until comments.empty?
66+
block_comments = [] #: Array[Prism::Comment]
67+
68+
until comments.empty?
69+
comment = comments.first or raise
70+
last_comment = block_comments.last
71+
72+
if last_comment
73+
if last_comment.location.end_line + 1 == comment.location.start_line
74+
if last_comment.location.start_column == comment.location.start_column
75+
unless comment.location.start_line_slice.index(/\S/)
76+
block_comments << comments.shift
77+
next
78+
end
79+
end
80+
end
81+
82+
break
83+
else
84+
block_comments << comments.shift
85+
end
86+
end
87+
88+
unless block_comments.empty?
89+
blocks << CommentBlock.new(buffer, block_comments.dup)
90+
end
91+
end
92+
93+
blocks
94+
end
95+
96+
AnnotationSyntaxError = _ = Data.define(:location, :error)
97+
98+
def each_paragraph(variables, &block)
99+
if block
100+
if leading_annotation?(0)
101+
yield_annotation(0, 0, 0, variables, &block)
102+
else
103+
yield_paragraph(0, 0, variables, &block)
104+
end
105+
else
106+
enum_for :each_paragraph, variables
107+
end
108+
end
109+
110+
def yield_paragraph(start_line, current_line, variables, &block)
111+
# We already know at start_line..current_line are paragraph.
112+
113+
while true
114+
next_line = current_line + 1
115+
116+
if next_line >= comment_buffer.line_count
117+
yield line_location(start_line, current_line)
118+
return
119+
end
120+
121+
if leading_annotation?(next_line)
122+
yield line_location(start_line, current_line)
123+
return yield_annotation(next_line, next_line, next_line, variables, &block)
124+
else
125+
current_line = next_line
126+
end
127+
end
128+
end
129+
130+
def yield_annotation(start_line, end_line, current_line, variables, &block)
131+
# We already know at start_line..end_line are annotation.
132+
while true
133+
next_line = current_line + 1
134+
135+
if next_line >= comment_buffer.line_count
136+
annotation = parse_annotation_lines(start_line, end_line, variables)
137+
yield annotation
138+
139+
if end_line > current_line
140+
yield_paragraph(end_line + 1, end_line + 1, variables, &block)
141+
end
142+
143+
return
144+
end
145+
146+
line_text = text(next_line)
147+
if leading_spaces = line_text.index(/\S/)
148+
if leading_spaces == 0
149+
# End of annotation
150+
yield parse_annotation_lines(start_line, end_line, variables)
151+
152+
if leading_annotation?(end_line + 1)
153+
yield_annotation(end_line + 1, end_line + 1, end_line + 1, variables, &block)
154+
else
155+
yield_paragraph(end_line + 1, end_line + 1, variables, &block)
156+
end
157+
158+
return
159+
else
160+
current_line = next_line
161+
end_line = next_line
162+
end
163+
else
164+
current_line = next_line
165+
end
166+
end
167+
end
168+
169+
def text(comment_index)
170+
range = comment_buffer.ranges[comment_index]
171+
comment_buffer.content[range] or raise
172+
end
173+
174+
def line_location(start_line, end_line)
175+
start_offset = comment_buffer.ranges[start_line].begin
176+
end_offset = comment_buffer.ranges[end_line].end
177+
Location.new(comment_buffer, start_offset, end_offset)
178+
end
179+
180+
def parse_annotation_lines(start_line, end_line, variables)
181+
start_pos = comment_buffer.ranges[start_line].begin
182+
end_pos = comment_buffer.ranges[end_line].end
183+
begin
184+
Parser.parse_inline_leading_annotation(comment_buffer, start_pos...end_pos, variables: variables)
185+
rescue ParsingError => error
186+
AnnotationSyntaxError.new(line_location(start_line, end_line), error)
187+
end
188+
end
189+
190+
def trailing_annotation(variables)
191+
if trailing?
192+
comment = comments[0] or raise
193+
if comment.location.slice.start_with?(/#[:\[]/)
194+
begin
195+
Parser.parse_inline_trailing_annotation(comment_buffer, 0...comment_buffer.last_position, variables: variables)
196+
rescue ParsingError => error
197+
location = line_location(0, offsets.size - 1)
198+
AnnotationSyntaxError.new(location, error)
199+
end
200+
end
201+
end
202+
end
203+
204+
def comments
205+
offsets.map { _1[0]}
206+
end
207+
208+
def leading_annotation?(index)
209+
if index < comment_buffer.line_count
210+
text(index).start_with?(/@rbs\b/) and return true
211+
212+
comment = offsets[index][0]
213+
comment.location.slice.start_with?(/\#:/) and return true
214+
end
215+
216+
false
217+
end
218+
end
219+
end
220+
end
221+
end

sig/ast/ruby/comment_block.rbs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
use Prism::Comment
2+
3+
module RBS
4+
module AST
5+
module Ruby
6+
# CommentBlock is a collection of comments
7+
#
8+
# ```ruby
9+
# # Comment1 < block1
10+
# # Comment2 <
11+
#
12+
# # Comment3 < block2
13+
# ```
14+
#
15+
# A comment block is a *leading* block or *trailing* block.
16+
#
17+
# ```ruby
18+
# # This is leading block.
19+
# # This is the second line of the leading block.
20+
#
21+
# foo # This is trailing block.
22+
# # This is second line of the trailing block.
23+
# ```
24+
#
25+
# A leading block is a comment block where all of the comments are at the start of the line content.
26+
# A trailing block is a comment block where the first comment of the block has something at the line before the comment.
27+
#
28+
class CommentBlock
29+
attr_reader name: Pathname
30+
31+
# Sub buffer of the contents of the comments
32+
#
33+
attr_reader comment_buffer: Buffer
34+
35+
attr_reader offsets: Array[
36+
[
37+
Comment,
38+
Integer, # -- prefix size
39+
]
40+
]
41+
42+
def initialize: (Buffer source_buffer, Array[Comment]) -> void
43+
44+
# Build comment block instances
45+
def self.build: (Buffer, Array[Comment]) -> Array[instance]
46+
47+
# Returns true if the comment block is a *leading* comment, which is attached to the successor node
48+
def leading?: () -> bool
49+
50+
# Returns true if the comment block is a *trailing* comment, which is attached to the predecessor node
51+
def trailing?: () -> bool
52+
53+
# The line number of the first comment in the block
54+
def start_line: () -> Integer
55+
56+
# The line number of the last comment in the block
57+
def end_line: () -> Integer
58+
59+
# The character index of `#comment_buffer` at the start of the lines
60+
#
61+
def line_starts: () -> Array[Integer]
62+
63+
# Returns the text content of the comment
64+
def text: (Integer index) -> String
65+
66+
# Yields paragraph and annotation
67+
#
68+
# A paragraph is a sequence of lines that are separated by annotations.
69+
# An annotation starts with a line starting with `@rbs` or `:`, and may continue with lines that has more leading spaces.
70+
#
71+
# ```
72+
# # Line 1 ^ Paragraph 1
73+
# # Line 2 |
74+
# # |
75+
# # Line 3 v
76+
# # @rbs ... < Annotation 1
77+
# # @rbs ... ^ Annotation 2
78+
# # ... |
79+
# # |
80+
# # ... v
81+
# # ^ Paragraph 2
82+
# # Line 4 |
83+
# # Line 5 v
84+
# ```
85+
#
86+
def each_paragraph: (Array[Symbol] variables) { (Location | AST::Ruby::Annotations::leading_annotation | AnnotationSyntaxError) -> void } -> void
87+
| (Array[Symbol] variables) -> Enumerator[Location | AST::Ruby::Annotations::leading_annotation | AnnotationSyntaxError]
88+
89+
# Returns a trailing annotation if it exists
90+
#
91+
# * Returns `nil` if the block is not a type annotation
92+
# * Returns an annotation if the block has a type annotation
93+
# * Returns AnnotationSyntaxError if the annotation has a syntax error
94+
#
95+
def trailing_annotation: (Array[Symbol] variables) -> (AST::Ruby::Annotations::trailing_annotation | AnnotationSyntaxError | nil)
96+
97+
class AnnotationSyntaxError
98+
attr_reader location: Location
99+
100+
attr_reader error: ParsingError
101+
102+
def initialize: (Location, ParsingError) -> void
103+
end
104+
105+
private def yield_paragraph: (Integer start_line, Integer current_line, Array[Symbol] variables) { (Location | AST::Ruby::Annotations::leading_annotation | AnnotationSyntaxError) -> void } -> void
106+
107+
private def yield_annotation: (Integer start_line, Integer end_line, Integer current_line, Array[Symbol] variables) { (Location | AST::Ruby::Annotations::leading_annotation | AnnotationSyntaxError) -> void } -> void
108+
109+
private def parse_annotation_lines: (Integer start_line, Integer end_line, Array[Symbol] variables) -> (AST::Ruby::Annotations::leading_annotation | AnnotationSyntaxError)
110+
111+
def comments: () -> Array[Comment]
112+
113+
def line_location: (Integer start_line, Integer end_line) -> Location
114+
115+
private def leading_annotation?: (Integer index) -> bool
116+
end
117+
end
118+
end
119+
end

0 commit comments

Comments
 (0)