From 94db21411f02b0c57cbc2437abdb2427c5dc1838 Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Wed, 19 Aug 2026 09:01:10 -0400 Subject: [PATCH 1/4] Move comment parsing to the base visitor Co-authored-by: Kaan Ozkan --- lib/rbi/parser.rb | 235 ++++++++++++++++++++++++++++++---------------- 1 file changed, 156 insertions(+), 79 deletions(-) diff --git a/lib/rbi/parser.rb b/lib/rbi/parser.rb index f71c7a09..65121f70 100644 --- a/lib/rbi/parser.rb +++ b/lib/rbi/parser.rb @@ -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 @@ -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 @@ -174,6 +176,146 @@ 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) + comments = [] + + latest_line, earliest_line = comment_lookup_range(node, min_line: min_line) + + rbs_continuation = [] #: Array[Prism::Comment] + + latest_line.downto(earliest_line) 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::Node node, ?min_line: Integer?) -> [Integer, Integer] + def comment_lookup_range(node, min_line: nil) + node_start_line = node.location.start_line + latest_line = node_start_line + comment = @comments_by_line[latest_line] + # Start comment lookup from line above the node if there are no trailing comments. + latest_line -= 1 unless comment && inline_comment_for_node?(node, comment) + + earliest_line = min_line || 1 + # End comment lookup at the line after `min_line` if `min_line` solely + # belongs to the surrounding node. + # The parent comment should not attach to the `foo` node. + # Example: + # `parent( # parent comment` + # ` foo: Integer # foo comment` + earliest_line += 1 if min_line && min_line < node_start_line + + [latest_line, earliest_line] + 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::Node node) -> Array[Comment] + def comments_inside(node) + comments = [] #: Array[Comment] + + node.location.start_line.upto(node.location.end_line) do |line| + comment = @comments_by_line[line] + next unless comment + + location = comment.location + next if location.start_offset < node.location.start_offset + next if location.end_offset > node.location.end_offset + + comments << parse_comment(comment) + @comments_by_line.delete(line) + end + + comments + end + + #: (Loc? inner, Loc? outer) -> bool + def loc_inside?(inner, outer) + return false unless inner && outer + + inner_begin_line = inner.begin_line + inner_begin_column = inner.begin_column + inner_end_line = inner.end_line + inner_end_column = inner.end_column + outer_begin_line = outer.begin_line + outer_begin_column = outer.begin_column + outer_end_line = outer.end_line + outer_end_column = outer.end_column + return false unless inner_begin_line && inner_begin_column && inner_end_line && inner_end_column + return false unless outer_begin_line && outer_begin_column && outer_end_line && outer_end_column + + begins_inside = inner_begin_line > outer_begin_line || + (inner_begin_line == outer_begin_line && inner_begin_column >= outer_begin_column) + ends_inside = inner_end_line < outer_end_line || + (inner_end_line == outer_end_line && inner_end_column <= outer_end_column) + + begins_inside && ends_inside + 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 @@ -183,11 +325,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 + + 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] @@ -615,75 +761,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] @@ -783,7 +860,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) @@ -936,8 +1013,8 @@ class SigBuilder < Visitor #: Sig attr_reader :current - #: (String content, file: String) -> void - def initialize(content, file:) + #: (String content, comments_by_line: Hash[Integer, Prism::Comment], file: String) -> void + def initialize(content, comments_by_line:, file:) super @current = Sig.new #: Sig From 807d34759fcbd7c2c3217b5a370c22a9d9fb8ed9 Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Wed, 19 Aug 2026 09:01:57 -0400 Subject: [PATCH 2/4] Parse comments found inside signatures Co-authored-by: Kaan Ozkan --- lib/rbi/parser.rb | 17 ++++- test/rbi/parser_test.rb | 138 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 3 deletions(-) diff --git a/lib/rbi/parser.rb b/lib/rbi/parser.rb index 65121f70..ccff3f4c 100644 --- a/lib/rbi/parser.rb +++ b/lib/rbi/parser.rb @@ -754,8 +754,9 @@ def detach_comments_from_sigs(sigs) 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 @@ -863,7 +864,7 @@ def parse_sig(node) 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) + builder.current.comments = node_comments(node) + builder.current.comments builder.current end @@ -1013,11 +1014,16 @@ class SigBuilder < Visitor #: Sig attr_reader :current + # 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 @@ -1051,7 +1057,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 @@ -1068,6 +1076,7 @@ def visit_call_node(node) visit(node.receiver) visit(node.block) + @current.comments.concat(comments_inside(node)) if node.message == "sig" end # @override @@ -1076,6 +1085,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 diff --git a/test/rbi/parser_test.rb b/test/rbi/parser_test.rb index 3cf664d1..6c9cb78b 100644 --- a/test/rbi/parser_test.rb +++ b/test/rbi/parser_test.rb @@ -405,6 +405,144 @@ def test_parse_sig_comments assert_equal(rbi, out.string) end + def test_parse_sig_param_comments + rbi = <<~RBI + sig do + params( + # `a` comment + a: Integer, + # `b` comment 1 + # `b` comment 2 + b: String + ).void + end + def foo(a, b); end + RBI + + out = Parser.parse_string(rbi) + assert_equal(<<~RBI, out.string) + sig do + params( + a: Integer, # `a` comment + b: String # `b` comment 1 + # `b` comment 2 + ).void + end + def foo(a, b); end + RBI + end + + def test_parse_sig_param_comments_do_not_escape_params_call + rbi = <<~RBI + sig do + # sig comment + params( # parent comment + # `foo` comment + foo: Integer + ).void + end + def foo(foo); end + RBI + + tree = parse_rbi(rbi) + method = tree.nodes.first #: as Method + sig = method.sigs.first #: as Sig + param = sig.params.first #: as SigParam + + assert_equal(["sig comment", "parent comment"], sig.comments.map(&:text)) + assert_equal(["`foo` comment"], param.comments.map(&:text)) + + assert_equal(<<~RBI, tree.string) + # sig comment + # parent comment + sig do + params( + foo: Integer # `foo` comment + ).void + end + def foo(foo); end + RBI + end + + def test_parse_trailing_sig_param_comments_do_not_escape_params_call + rbi = <<~RBI + sig do + params( # parent comment + foo: Integer # `foo` comment + ).void + end + def foo(foo); end + RBI + + tree = parse_rbi(rbi) + method = tree.nodes.first #: as Method + sig = method.sigs.first #: as Sig + param = sig.params.first #: as SigParam + + assert_equal(["parent comment"], sig.comments.map(&:text)) + assert_equal(["`foo` comment"], param.comments.map(&:text)) + + assert_equal(<<~RBI, tree.string) + # parent comment + sig do + params( + foo: Integer # `foo` comment + ).void + end + def foo(foo); end + RBI + end + + def test_parse_same_line_sig_param_comments + rbi = <<~RBI + sig do + params(foo: Integer, # `foo` comment + bar: String + ).void + end + def foo(foo, bar); end + RBI + + tree = parse_rbi(rbi) + method = tree.nodes.first #: as Method + sig = method.sigs.first #: as Sig + param = sig.params.first #: as SigParam + + assert_equal(["`foo` comment"], param.comments.map(&:text)) + + assert_equal(<<~RBI, tree.string) + sig do + params( + foo: Integer, # `foo` comment + bar: String + ).void + end + def foo(foo, bar); end + RBI + end + + def test_parse_sig_param_comments_ignore_sig_line_trailing_comment + rbi = <<~RBI + sig { params(a: Integer).void } # sig comment + def foo(a); end + RBI + + tree = parse_rbi(rbi) + method = tree.nodes.first #: as Method + sig = method.sigs.first #: as Sig + param = sig.params.first #: as SigParam + + assert_empty(sig.comments) + assert_empty(param.comments) + assert_equal(["sig comment"], method.comments.map(&:text)) + + assert_equal(<<~RBI, tree.string) + # sig comment + sig { params(a: Integer).void } + def foo(a); end + RBI + end + def test_parse_methods_with_visibility rbi = <<~RBI private def m1; end From 2e29a815c7a342cb32276c83efd80c4db59d18fa Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Wed, 19 Aug 2026 11:28:49 -0400 Subject: [PATCH 3/4] Print comments inside RBS signatures Co-authored-by: Kaan Ozkan --- lib/rbi/rbs_printer.rb | 99 ++++++++++++++++-- test/rbi/rbs_printer_test.rb | 192 +++++++++++++++++++++++++++++++++++ 2 files changed, 281 insertions(+), 10 deletions(-) diff --git a/lib/rbi/rbs_printer.rb b/lib/rbi/rbs_printer.rb index c31e27f0..37af248c 100644 --- a/lib/rbi/rbs_printer.rb +++ b/lib/rbi/rbs_printer.rb @@ -371,9 +371,10 @@ def visit_method(node) print("self.") if node.is_singleton print(node.name) sigs = node.sigs - print(": ") + print(":") if sigs.any? first, *rest = sigs + print(" ") print_method_sig( node, first, #: as !nil @@ -383,11 +384,13 @@ def visit_method(node) rest.each do |sig| printn printt - print("#{" " * spaces}| ") + print("#{" " * spaces}|") + print(" ") print_method_sig(node, sig) end end else + print(" ") if node.params.any? params = node.params.grep_v(BlockParam) block = node.params.find { |param| param.is_a?(BlockParam) } @@ -415,7 +418,8 @@ def print_method_sig(node, sig) @out = old_out max_line_length = @max_line_length - if @force_multiline_signatures || (max_line_length && new_out.string.size > max_line_length) + if @force_multiline_signatures || sig_params_have_printable_comments?(node, sig) || + (max_line_length && new_out.string.size > max_line_length) print_method_sig_content(node, sig, multiline: true) else print(new_out.string) @@ -424,15 +428,16 @@ def print_method_sig(node, sig) #: (RBI::Method node, Sig sig, multiline: bool) -> void def print_method_sig_content(node, sig, multiline:) - unless sig.type_params.empty? - print("[#{sig.type_params.join(", ")}] ") - end - no_kw_params, method_params = node.params.partition { |param| param.is_a?(NoKwParam) } raise Error, "Multiple no-keywords parameters found" if no_kw_params.size > 1 - raise Error, "Arity mismatch between method and signature" if sig.params.size != method_params.size params = sig.params.zip(method_params) #: Array[[SigParam?, Param?]] + if sig.params.size + 1 == method_params.size && method_params.last.is_a?(BlockParam) + params << [nil, method_params.last] + elsif sig.params.size != method_params.size + raise Error, "Arity mismatch between method and signature" + end + block_params, params = params.partition do |_sig_param, method_param| method_param.is_a?(BlockParam) end @@ -442,6 +447,15 @@ def print_method_sig_content(node, sig, multiline:) no_kw_param = no_kw_params.first params << [nil, no_kw_param] if no_kw_param + sig_block_param = block_params.first&.first + sig_block_param_has_comments = !!( + sig_block_param&.comments? && sig_block_param && print_sig_block_param?(sig_block_param) + ) + + unless sig.type_params.empty? + print("[#{sig.type_params.join(", ")}] ") + end + unless params.empty? if multiline printl("(") @@ -456,6 +470,21 @@ def print_method_sig_content(node, sig, multiline:) elsif index > 0 print(", ") end + + comment_lines = if multiline && sig_param + sig_param.comments.flat_map { |comment| comment.text.lines.map(&:rstrip) } + else + [] + end + + old_out = nil + param_out = nil + unless comment_lines.empty? + old_out = @out + param_out = StringIO.new + @out = param_out + end + if method_param.is_a?(NoKwParam) visit(method_param) else @@ -464,8 +493,26 @@ def print_method_sig_content(node, sig, multiline:) method_param, #: as !nil ) end + + if param_out && old_out + @out = old_out + print(param_out.string) + end + if multiline - print(",") if index < params.size - 1 + is_last = index == params.size - 1 + print(",") unless is_last + comment_lines.each_with_index do |comment, comment_index| + if comment_index == 0 + print(" ") + else + printn + printt + width = param_out&.string&.length || 0 + print(" " * (width + (is_last ? 1 : 2))) + end + print("# #{comment}") + end printn end end @@ -478,13 +525,28 @@ def print_method_sig_content(node, sig, multiline:) end end - print_method_sig_block(block_params.first&.first) + print_method_sig_block(sig_block_param) type = parse_type(sig.return_type) print("-> #{type.rbs_string}") loc = sig.loc print(" # #{loc}") if loc && print_locs + + if sig_block_param_has_comments && sig_block_param + comment_lines = sig_block_param.comments.flat_map { |comment| comment.text.lines.map(&:rstrip) } + comment_lines.each_with_index do |comment, index| + if index == 0 + print(" ") + else + printn + indent + printt + dedent + end + print("# #{comment}") + end + end end #: (SigParam? sig_param) -> void @@ -888,6 +950,23 @@ def print_sig_param(sig_param, method_param) end end + #: (RBI::Method node, Sig sig) -> bool + def sig_params_have_printable_comments?(node, sig) + block_param = node.params.find { |param| param.is_a?(BlockParam) } + sig.params.any? do |param| + param.comments? && param.name != block_param&.name + end + end + + #: (SigParam param) -> bool + def print_sig_block_param?(param) + block_type = param.type + block_type = Type.parse_string(block_type) if block_type.is_a?(String) + block_type = block_type.type if block_type.is_a?(Type::Nilable) + + !block_type.is_a?(Type::Simple) || block_type.name != "NilClass" + end + #: (Param node, last: bool) -> void def print_param_comment_leading_space(node, last:) printn diff --git a/test/rbi/rbs_printer_test.rb b/test/rbi/rbs_printer_test.rb index b70525aa..ce275207 100644 --- a/test/rbi/rbs_printer_test.rb +++ b/test/rbi/rbs_printer_test.rb @@ -704,6 +704,198 @@ def test_print_force_multiline_signatures RBI end + def test_print_breaks_signatures_with_sig_param_comments + rbi_def = Method.new("foo") do |node| + node.params << ReqParam.new("a") + node.params << ReqParam.new("b") + end + + rbi_sig = Sig.new do |sig| + sig.params << SigParam.new( + "a", + "Integer", + comments: [Comment.new("First param"), Comment.new("More details")], + ) + sig.params << SigParam.new("b", "String") + sig.return_type = "void" + end + + out = StringIO.new + printer = RBI::RBSPrinter.new(out: out) + printer.print_method_sig(rbi_def, rbi_sig) + + assert_equal(<<~RBI.strip, out.string) + ( + Integer a, # First param + # More details + String b + ) -> void + RBI + end + + def test_print_breaks_signatures_with_multiple_sig_param_comments + rbi_def = Method.new("foo") do |node| + node.params << ReqParam.new("a") + node.params << ReqParam.new("b") + node.params << KwParam.new("c") + end + + rbi_sig = Sig.new do |sig| + sig.params << SigParam.new("a", "Integer", comments: [Comment.new("First param")]) + sig.params << SigParam.new("b", "String", comments: [Comment.new("Second param")]) + sig.params << SigParam.new("c", "Symbol", comments: [Comment.new("Keyword param")]) + sig.return_type = "void" + end + + out = StringIO.new + printer = RBI::RBSPrinter.new(out: out) + printer.print_method_sig(rbi_def, rbi_sig) + + assert_equal(<<~RBI.strip, out.string) + ( + Integer a, # First param + String b, # Second param + c: Symbol # Keyword param + ) -> void + RBI + end + + def test_print_breaks_signatures_with_mixed_sig_param_comments + rbi = parse_rbi(<<~RBI) + sig do + params( + a: Integer, + # Commented param + b: String, + c: Symbol + ).void + end + def foo(a, b, c:); end + RBI + + assert_equal(<<~RBI, rbi.rbs_string) + def foo: ( + Integer a, + String b, # Commented param + c: Symbol + ) -> void + RBI + end + + def test_prints_multiline_signature_with_block_sig_param_comments + rbi = parse_rbi(<<~RBI) + sig do + params( + # Block param + # More details + block: T.proc.void + ).void + end + def foo(&block); end + RBI + + assert_equal(<<~RBS, rbi.rbs_string) + def foo: { -> void } -> void # Block param + # More details + RBS + end + + def test_prints_multiline_signature_with_param_and_block_sig_param_comments + rbi = parse_rbi(<<~RBI) + sig do + params( + # Positional param + a: Integer, + # Block param + block: T.proc.void + ).void + end + def foo(a, &block); end + RBI + + assert_equal(<<~RBS, rbi.rbs_string) + def foo: ( + Integer a # Positional param + ) { -> void } -> void # Block param + RBS + end + + def test_prints_multiline_signature_with_type_params_and_block_sig_param_comments + rbi_def = Method.new("foo") do |node| + node.params << BlockParam.new("block") + end + + rbi_sig = Sig.new do |sig| + sig.type_params << "U" + sig.params << SigParam.new( + "block", + "T.proc.returns(T.type_parameter(:U))", + comments: [Comment.new("Block param")], + ) + sig.return_type = "T.type_parameter(:U)" + end + + out = StringIO.new + printer = RBI::RBSPrinter.new(out: out) + printer.print_method_sig(rbi_def, rbi_sig) + + assert_equal(<<~RBS.strip, out.string) + [U] { -> U } -> U # Block param + RBS + end + + def test_prints_signature_with_nil_block_sig_param_comments + rbi_def = Method.new("foo") do |node| + node.params << BlockParam.new("block") + end + + rbi_sig = Sig.new do |sig| + sig.params << SigParam.new("block", "NilClass", comments: [Comment.new("Block param")]) + sig.return_type = "void" + end + + out = StringIO.new + printer = RBI::RBSPrinter.new(out: out) + printer.print_method_sig(rbi_def, rbi_sig) + + assert_equal("-> void", out.string) + end + + def test_prints_multiline_overload_with_block_sig_param_comments + rbi = parse_rbi(<<~RBI) + sig { void } + sig do + params( + # Block param + block: T.proc.void + ).void + end + def foo(&block); end + RBI + + assert_equal(<<~RBI, rbi.rbs_string) + def foo: -> void + | { -> void } -> void # Block param + RBI + end + + def test_prints_signature_with_nil_block_sig_param_comments_after_positional_params + rbi = parse_rbi(<<~RBI) + sig do + params( + a: Integer, + # Block param + block: NilClass + ).void + end + def foo(a, &block); end + RBI + + assert_equal(<<~RBI, rbi.rbs_string) + def foo: (Integer a) -> void + RBI + end + def test_print_simplified_types rbi = parse_rbi(<<~RBI) sig { returns(T.any(String, String, NilClass, T.nilable(T.nilable(Integer)), TrueClass, FalseClass)) } From 017bd2d4914e9e855b35128508b8622879b8145f Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Wed, 19 Aug 2026 13:39:35 -0400 Subject: [PATCH 4/4] Update exported RBI Co-authored-by: Kaan Ozkan --- rbi/rbi.rbi | 52 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/rbi/rbi.rbi b/rbi/rbi.rbi index eb65ddce..513a6a3a 100644 --- a/rbi/rbi.rbi +++ b/rbi/rbi.rbi @@ -1051,8 +1051,8 @@ class RBI::Parser::HeredocLocationVisitor < ::Prism::Visitor end class RBI::Parser::SigBuilder < ::RBI::Parser::Visitor - sig { params(content: ::String, file: ::String).void } - def initialize(content, file:); end + sig { params(content: ::String, comments_by_line: T::Hash[::Integer, ::Prism::Comment], file: ::String).void } + def initialize(content, comments_by_line:, file:); end sig { params(node: ::Prism::CallNode, value: ::String).returns(T::Boolean) } def allow_incompatible_override?(node, value); end @@ -1060,6 +1060,9 @@ class RBI::Parser::SigBuilder < ::RBI::Parser::Visitor sig { returns(::RBI::Sig) } def current; end + sig { returns(T.nilable(::Integer)) } + def params_start_line; end + sig { params(node: ::Prism::Node).returns(::String) } def sig_param_name(node); end @@ -1074,8 +1077,15 @@ class RBI::Parser::SigBuilder < ::RBI::Parser::Visitor end class RBI::Parser::TreeBuilder < ::RBI::Parser::Visitor - sig { params(source: ::String, comments: T::Array[::Prism::Comment], file: ::String).void } - def initialize(source, comments:, file:); end + sig do + params( + source: ::String, + file: ::String, + comments: T.nilable(T::Array[::Prism::Comment]), + comments_by_line: T::Hash[::Integer, ::Prism::Comment] + ).void + end + def initialize(source, file:, comments: T.unsafe(nil), comments_by_line: T.unsafe(nil)); end sig { returns(T.nilable(::Prism::Node)) } def last_node; end @@ -1127,12 +1137,6 @@ class RBI::Parser::TreeBuilder < ::RBI::Parser::Visitor sig { params(sigs: T::Array[::RBI::Sig]).returns(T::Array[::RBI::Comment]) } def detach_comments_from_sigs(sigs); end - sig { params(node: ::Prism::Node).returns(T::Array[::RBI::Comment]) } - def node_comments(node); end - - sig { params(node: ::Prism::Comment).returns(::RBI::Comment) } - def parse_comment(node); end - sig { params(node: T.nilable(::Prism::Node)).returns(T::Array[::RBI::Param]) } def parse_params(node); end @@ -1169,8 +1173,8 @@ class RBI::Parser::TreeBuilder < ::RBI::Parser::Visitor end class RBI::Parser::Visitor < ::Prism::Visitor - sig { params(source: ::String, file: ::String).void } - def initialize(source, file:); end + sig { params(source: ::String, file: ::String, comments_by_line: T::Hash[::Integer, ::Prism::Comment]).void } + def initialize(source, file:, comments_by_line: T.unsafe(nil)); end private @@ -1180,6 +1184,21 @@ class RBI::Parser::Visitor < ::Prism::Visitor sig { params(node: ::Prism::Node).returns(T::Boolean) } def braceless_shape?(node); end + sig { params(node: ::Prism::Node, min_line: T.nilable(::Integer)).returns([::Integer, ::Integer]) } + def comment_lookup_range(node, min_line: T.unsafe(nil)); end + + sig { params(node: ::Prism::Node).returns(T::Array[::RBI::Comment]) } + def comments_inside(node); end + + sig { params(node: ::Prism::Node, comment: ::Prism::Comment).returns(T::Boolean) } + def inline_comment_for_node?(node, comment); end + + sig { params(inner: T.nilable(::RBI::Loc), outer: T.nilable(::RBI::Loc)).returns(T::Boolean) } + def loc_inside?(inner, outer); end + + sig { params(node: ::Prism::Node, min_line: T.nilable(::Integer)).returns(T::Array[::RBI::Comment]) } + def node_comments(node, min_line: T.unsafe(nil)); end + sig { params(node: ::Prism::Node).returns(::RBI::Loc) } def node_loc(node); end @@ -1189,6 +1208,9 @@ class RBI::Parser::Visitor < ::Prism::Visitor sig { params(node: ::Prism::Node).returns(::String) } def node_string!(node); end + sig { params(node: ::Prism::Comment).returns(::RBI::Comment) } + def parse_comment(node); end + sig { params(node: T.nilable(::Prism::Node)).returns(T::Boolean) } def self?(node); end @@ -1816,11 +1838,17 @@ class RBI::RBSPrinter < ::RBI::Visitor sig { params(node: ::RBI::Param, last: T::Boolean).void } def print_param_comment_leading_space(node, last:); end + sig { params(param: ::RBI::SigParam).returns(T::Boolean) } + def print_sig_block_param?(param); end + sig { params(sig_param: ::RBI::SigParam, method_param: ::RBI::Param).void } def print_sig_param(sig_param, method_param); end sig { params(node: ::RBI::SigParam, last: T::Boolean).void } def print_sig_param_comment_leading_space(node, last:); end + + sig { params(node: ::RBI::Method, sig: ::RBI::Sig).returns(T::Boolean) } + def sig_params_have_printable_comments?(node, sig); end end class RBI::RBSPrinter::Error < ::RBI::Error; end