Skip to content
Closed
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
188 changes: 109 additions & 79 deletions lib/rbi/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ def parse(source, file:)
raise ParseError.new(message, location)
end

visitor = TreeBuilder.new(source, comments: result.comments, file: file)
comments_by_line = result.comments.to_h { |c| [c.location.start_line, c] }
visitor = TreeBuilder.new(source, comments_by_line: comments_by_line, file: file)
visitor.visit(result.value)
visitor.tree
rescue ParseError => e
Expand All @@ -112,12 +113,13 @@ def parse(source, file:)
end

class Visitor < Prism::Visitor
#: (String source, file: String) -> void
def initialize(source, file:)
#: (String source, file: String, ?comments_by_line: Hash[Integer, Prism::Comment]) -> void
def initialize(source, file:, comments_by_line: {})
super()

@source = source
@file = file
@comments_by_line = comments_by_line
end

private
Expand Down Expand Up @@ -174,6 +176,90 @@ def self?(node)
def t_sig_without_runtime?(node)
!!(node.is_a?(Prism::ConstantPathNode) && node_string(node) =~ /(::)?T::Sig::WithoutRuntime/)
end

#: (Prism::Node node, ?min_line: Integer?) -> Array[Comment]
def node_comments(node, min_line: nil)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly a move from TreeBuilder to Visitor so SigBuilder can use it too. It also gained a min_line parameter so callers can bound how far upward node comment lookup will scan.

comments = []
node_start_line = node.location.start_line
rbs_continuation = [] #: Array[Prism::Comment]

node_start_line.downto(min_line || 1) do |line|
comment = @comments_by_line[line]
# Include trailing comments on the node line, but don't attach a comment from the enclosing call:
# `parent( # parent comment`
# ` foo: Integer # foo comment`
if line == node_start_line
next unless comment && inline_comment_for_node?(node, comment)
elsif line == min_line
break
end
break unless comment

text = comment.location.slice

# If we find a RBS comment continuation `#|`, we store it until we find the start with `#:`
if text.start_with?("#|")
rbs_continuation << comment
@comments_by_line.delete(line)
next
end

loc = Loc.from_prism(@file, comment.location)

# If we find the start of a RBS comment, we create a new RBSComment
# Note that we ignore RDoc directives such as `:nodoc:`
# See https://ruby.github.io/rdoc/RDoc/MarkupReference.html#class-RDoc::MarkupReference-label-Directives
if text.start_with?("#:") && !(text =~ /^#:[a-z_]+:/)
text = text.sub(/^#: ?/, "").rstrip

# If we found continuation comments, we merge them in reverse order (since we go from bottom to top)
rbs_continuation.reverse_each do |rbs_comment|
continuation_text = rbs_comment.location.slice.sub(/^#\| ?/, "").strip
continuation_loc = Loc.from_prism(@file, rbs_comment.location)
loc = loc.join(continuation_loc)
text = "#{text}#{continuation_text}"
end

rbs_continuation.clear
comments.unshift(RBSComment.new(text, loc: loc))
else
# If we have unused continuation comments, we should inject them back to not lose them
rbs_continuation.each do |rbs_comment|
comments.unshift(parse_comment(rbs_comment))
end

rbs_continuation.clear
comments.unshift(parse_comment(comment))
end

@comments_by_line.delete(line)
end

# If we have unused continuation comments, we should inject them back to not lose them
rbs_continuation.each do |rbs_comment|
comments.unshift(parse_comment(rbs_comment))
end
rbs_continuation.clear

comments
end

#: (Prism::Node node, Prism::Comment comment) -> bool
def inline_comment_for_node?(node, comment)
return false unless node.location.start_line == comment.location.start_line

between = @source[node.location.end_offset...comment.location.start_offset] || ""
between.each_char.all? do |char|
char == "," || char.match?(/\s/)
end
end

#: (Prism::Comment node) -> Comment
def parse_comment(node)
text = node.location.slice.sub(/^# ?/, "").rstrip
loc = Loc.from_prism(@file, node.location)
Comment.new(text, loc: loc)
end
end

class TreeBuilder < Visitor
Expand All @@ -183,11 +269,15 @@ class TreeBuilder < Visitor
#: Prism::Node?
attr_reader :last_node

#: (String source, comments: Array[Prism::Comment], file: String) -> void
def initialize(source, comments:, file:)
super(source, file: file)
#: (String source, file: String, ?comments: Array[Prism::Comment]?,
#| ?comments_by_line: Hash[Integer, Prism::Comment]) -> void
def initialize(source, file:, comments: nil, comments_by_line: {})
if comments
comments_by_line = comments.to_h { |comment| [comment.location.start_line, comment] }
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bundle exec spoom srb sigs export instantiates TreeBuilder with comments so for now we need to keep compatibility.


super(source, file: file, comments_by_line: comments_by_line)

@comments_by_line = comments.to_h { |c| [c.location.start_line, c] } #: Hash[Integer, Prism::Comment]
@tree = Tree.new #: Tree

@scopes_stack = [@tree] #: Array[Tree]
Expand Down Expand Up @@ -615,75 +705,6 @@ def detach_comments_from_sigs(sigs)
comments
end

#: (Prism::Node node) -> Array[Comment]
def node_comments(node)
comments = []

start_line = node.location.start_line
start_line -= 1 unless @comments_by_line.key?(start_line)

rbs_continuation = [] #: Array[Prism::Comment]

start_line.downto(1) do |line|
comment = @comments_by_line[line]
break unless comment

text = comment.location.slice

# If we find a RBS comment continuation `#|`, we store it until we find the start with `#:`
if text.start_with?("#|")
rbs_continuation << comment
@comments_by_line.delete(line)
next
end

loc = Loc.from_prism(@file, comment.location)

# If we find the start of a RBS comment, we create a new RBSComment
# Note that we ignore RDoc directives such as `:nodoc:`
# See https://ruby.github.io/rdoc/RDoc/MarkupReference.html#class-RDoc::MarkupReference-label-Directives
if text.start_with?("#:") && !(text =~ /^#:[a-z_]+:/)
text = text.sub(/^#: ?/, "").rstrip

# If we found continuation comments, we merge them in reverse order (since we go from bottom to top)
rbs_continuation.reverse_each do |rbs_comment|
continuation_text = rbs_comment.location.slice.sub(/^#\| ?/, "").strip
continuation_loc = Loc.from_prism(@file, rbs_comment.location)
loc = loc.join(continuation_loc)
text = "#{text}#{continuation_text}"
end

rbs_continuation.clear
comments.unshift(RBSComment.new(text, loc: loc))
else
# If we have unused continuation comments, we should inject them back to not lose them
rbs_continuation.each do |rbs_comment|
comments.unshift(parse_comment(rbs_comment))
end

rbs_continuation.clear
comments.unshift(parse_comment(comment))
end

@comments_by_line.delete(line)
end

# If we have unused continuation comments, we should inject them back to not lose them
rbs_continuation.each do |rbs_comment|
comments.unshift(parse_comment(rbs_comment))
end
rbs_continuation.clear

comments
end

#: (Prism::Comment node) -> Comment
def parse_comment(node)
text = node.location.slice.sub(/^# ?/, "").rstrip
loc = Loc.from_prism(@file, node.location)
Comment.new(text, loc: loc)
end

#: (Prism::Node? node) -> Array[Arg]
def parse_send_args(node)
args = [] #: Array[Arg]
Expand Down Expand Up @@ -783,7 +804,7 @@ def parse_params(node)

#: (Prism::CallNode node) -> Sig
def parse_sig(node)
builder = SigBuilder.new(@source, file: @file)
builder = SigBuilder.new(@source, comments_by_line: @comments_by_line, file: @file)
builder.current.loc = node_loc(node)
builder.visit_call_node(node)
builder.current.comments = node_comments(node)
Expand Down Expand Up @@ -936,11 +957,16 @@ class SigBuilder < Visitor
#: Sig
attr_reader :current

#: (String content, file: String) -> void
def initialize(content, file:)
# Bounds sig param comment lookup to comments inside the current `params(...)` call.
#: Integer?
attr_reader :params_start_line

#: (String content, comments_by_line: Hash[Integer, Prism::Comment], file: String) -> void
def initialize(content, comments_by_line:, file:)
super

@current = Sig.new #: Sig
@params_start_line = nil #: Integer?
end

# @override
Expand Down Expand Up @@ -974,7 +1000,9 @@ def visit_call_node(node)
when "overridable"
@current.is_overridable = true
when "params"
@params_start_line = node.location.start_line
visit(node.arguments)
@params_start_line = nil
when "returns"
return_type = sig_type_argument(node)
@current.return_type = return_type if return_type
Expand All @@ -999,6 +1027,8 @@ def visit_assoc_node(node)
@current.params << SigParam.new(
sig_param_name(node.key),
node_string!(node.value),
loc: node_loc(node),
comments: node_comments(node, min_line: params_start_line),
)
end

Expand Down
Loading
Loading