Preserve comments inside Sorbet signatures - #653
Conversation
Co-authored-by: Kaan Ozkan <kaanozkan97@gmail.com>
Co-authored-by: Kaan Ozkan <kaanozkan97@gmail.com>
Co-authored-by: Kaan Ozkan <kaanozkan97@gmail.com>
Co-authored-by: Kaan Ozkan <kaanozkan97@gmail.com>
| def initialize(source, file:, comments: nil, comments_by_line: {}) | ||
| if comments | ||
| comments_by_line = comments.to_h { |comment| [comment.location.start_line, comment] } | ||
| end |
There was a problem hiding this comment.
Wdyt of raising if both comments and comments_by_line are supplied so we don't ignore it silently?
If we don't go that route: Is there a downside of prioritizing comments_by_line instead of calculating it based on comments? Using what's supplied feels better.
|
|
||
| visit(node.receiver) | ||
| visit(node.block) | ||
| @current.comments.concat(comments_inside(node)) if node.message == "sig" |
There was a problem hiding this comment.
Agent brought up that sig comments aren't printed so this case fails now:
def test_preserves_comments_inside_sig_in_rbs
rbi = parse_rbi(<<~RBI)
sig do
# keep me
void
end
def foo; end
RBI
assert_includes(rbi.rbs_string, "# keep me")
end| # 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_]+:/) |
There was a problem hiding this comment.
esoteric syntax
| if text.start_with?("#:") && !(text =~ /^#:[a-z_]+:/) | |
| if text.start_with?("#:") && !text.match?(/^#:[a-z_]+:/) |
| 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}" |
There was a problem hiding this comment.
We can avoid the reallocation here
| text = "#{text}#{continuation_text}" | |
| text.concat(continuation_text) |
| end | ||
|
|
||
| rbs_continuation.clear | ||
| comments.unshift(RBSComment.new(text, loc: loc)) |
There was a problem hiding this comment.
How about we << them to the end instead, and only reverse! at the 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 |
There was a problem hiding this comment.
Would be helpful to pair these comments with a visual example.
| @comments_by_line.delete(line) | ||
| end | ||
|
|
||
| # If we have unused continuation comments, we should inject them back to not lose them |
There was a problem hiding this comment.
How does this differ from the same logic on line 220?
| def parse_comment(node) | ||
| text = node.location.slice.sub(/^# ?/, "").rstrip | ||
| loc = Loc.from_prism(@file, node.location) | ||
| Comment.new(text, loc: loc) |
There was a problem hiding this comment.
Perhaps this should be a factory function on Comment
| Comment.new(text, loc: loc) | |
| Comment.from_prism_node(node) |
| 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: {}) |
There was a problem hiding this comment.
This parameter pack is starting to get unwieldy. I think we should extract a class like something like SourceFile, which stores all this stuff
| comments = [] #: Array[Comment] | ||
|
|
||
| sigs.each do |sig| | ||
| comments += sig.comments.dup | ||
| sig.comments.clear | ||
| inside, outside = sig.comments.partition { |comment| loc_inside?(comment.loc, sig.loc) } | ||
| comments.concat(outside) | ||
| sig.comments.replace(inside) | ||
| end | ||
|
|
||
| comments |
There was a problem hiding this comment.
How do you feel about flat_map with a sideffect?
| comments = [] #: Array[Comment] | |
| sigs.each do |sig| | |
| comments += sig.comments.dup | |
| sig.comments.clear | |
| inside, outside = sig.comments.partition { |comment| loc_inside?(comment.loc, sig.loc) } | |
| comments.concat(outside) | |
| sig.comments.replace(inside) | |
| end | |
| comments | |
| sigs.flat_map do |sig| | |
| inside, outside = sig.comments.partition { |comment| loc_inside?(comment.loc, sig.loc) } | |
| sig.comments.replace(inside) | |
| outside | |
| end |
There was a problem hiding this comment.
The partition+replace combo is a bit tricky.
What do you think of vendoring a copy of extract! from ActiveSupport?
Then this could just be:
| comments = [] #: Array[Comment] | |
| sigs.each do |sig| | |
| comments += sig.comments.dup | |
| sig.comments.clear | |
| inside, outside = sig.comments.partition { |comment| loc_inside?(comment.loc, sig.loc) } | |
| comments.concat(outside) | |
| sig.comments.replace(inside) | |
| end | |
| comments | |
| sigs.flat_map do |sig| | |
| sig.comments.extract! { |comment| !loc_inside?(comment.loc, sig.loc) } | |
| end |
| builder.current.loc = node_loc(node) | ||
| builder.visit_call_node(node) | ||
| builder.current.comments = node_comments(node) | ||
| builder.current.comments = node_comments(node) + builder.current.comments |
There was a problem hiding this comment.
One less alloc + copy
| builder.current.comments = node_comments(node) + builder.current.comments | |
| builder.current.comments = node_comments(node).concat(builder.current.comments) |
|
|
||
| #: (String content, file: String) -> void | ||
| def initialize(content, file:) | ||
| # Bounds sig param comment lookup to comments inside the current `params(...)` call. |
Summary
Preserve comments attached to parameters inside Sorbet signatures when parsing RBI files and printing RBS.
This work is based on and supersedes #612 by @KaanOzkan.
TreeBuilderandSigBuilderconsume the same comment map.Sig#commentsSigParam#commentsparams(...)call.TreeBuilder(..., comments:)keyword used by Spoom.Example
Given:
RBS output becomes:
Overloads remain inline:
Multiple comments on an ordinary parameter remain aligned: