diff --git a/lib/rbi/parser.rb b/lib/rbi/parser.rb index f71c7a09..697cefdc 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,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) + 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 @@ -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 + + 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 +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] @@ -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) @@ -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 @@ -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 @@ -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 diff --git a/lib/rbi/rbs_printer.rb b/lib/rbi/rbs_printer.rb index c31e27f0..c12b9d05 100644 --- a/lib/rbi/rbs_printer.rb +++ b/lib/rbi/rbs_printer.rb @@ -371,9 +371,17 @@ def visit_method(node) print("self.") if node.is_singleton print(node.name) sigs = node.sigs - print(": ") + print(":") if sigs.any? first, *rest = sigs + if method_sig_starts_on_next_line?( + node, + first, #: as !nil + ) + printn + else + print(" ") + end print_method_sig( node, first, #: as !nil @@ -383,11 +391,19 @@ def visit_method(node) rest.each do |sig| printn printt - print("#{" " * spaces}| ") + print("#{" " * spaces}|") + if method_sig_starts_on_next_line?(node, sig) + printn + print_method_sig(node, sig) + next + end + + 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 +431,9 @@ 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 +442,10 @@ 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) } + no_kw_params = node.params.grep(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?]] + params = sig_method_param_pairs(node, sig) #: Array[[SigParam?, Param]] block_params, params = params.partition do |_sig_param, method_param| method_param.is_a?(BlockParam) end @@ -441,10 +454,22 @@ 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 = multiline && + sig_block_param&.comments? && + print_sig_block_param?(sig_block_param) + + unless sig.type_params.empty? + if sig_block_param_has_comments && params.empty? + printn("[#{sig.type_params.join(", ")}]") + else + print("[#{sig.type_params.join(", ")}] ") + end + end unless params.empty? if multiline - printl("(") + printn("(") indent else print("(") @@ -452,6 +477,7 @@ def print_method_sig_content(node, sig, multiline:) params.each_with_index do |(sig_param, method_param), index| if multiline + print_sig_param_comments(sig_param) if sig_param printt elsif index > 0 print(", ") @@ -461,7 +487,7 @@ def print_method_sig_content(node, sig, multiline:) else print_sig_param( sig_param, #: as !nil - method_param, #: as !nil + method_param, ) end if multiline @@ -472,13 +498,17 @@ def print_method_sig_content(node, sig, multiline:) if multiline dedent - printt(") ") + if sig_block_param_has_comments + printl(")") + else + printt(") ") + end else print(") ") end end - print_method_sig_block(block_params.first&.first) + print_method_sig_block(sig_block_param, multiline: multiline) type = parse_type(sig.return_type) print("-> #{type.rbs_string}") @@ -487,8 +517,8 @@ def print_method_sig_content(node, sig, multiline:) print(" # #{loc}") if loc && print_locs end - #: (SigParam? sig_param) -> void - def print_method_sig_block(sig_param) + #: (SigParam? sig_param, multiline: bool) -> void + def print_method_sig_block(sig_param, multiline:) return unless sig_param block_type = sig_param.type @@ -512,11 +542,20 @@ def print_method_sig_block(sig_param) type_string = "(?) -> untyped" end + has_comments = multiline && sig_param.comments? + if has_comments + indent + print_sig_param_comments(sig_param) + printt + end + if block_is_nilable print("?{ #{type_string} } ") else print("{ #{type_string} } ") end + + dedent if has_comments end #: (Sig node) -> void @@ -888,6 +927,84 @@ def print_sig_param(sig_param, method_param) end end + #: (RBI::Method node, Sig sig) -> bool + def sig_params_have_printable_comments?(node, sig) + sig_method_param_pairs(node, sig).any? do |sig_param, method_param| + next false unless sig_param.comments? + + !method_param.is_a?(BlockParam) || print_sig_block_param?(sig_param) + end + end + + #: (RBI::Method node, Sig sig) -> Array[[SigParam, Param]] + def sig_method_param_pairs(node, sig) + method_params = node.params.grep_v(NoKwParam) + return sig.params.zip(method_params) if sig.params.size == method_params.size + + unless node.sigs.size > 1 + raise Error, "Arity mismatch between method and signature" + end + + unmatched_method_params = method_params.dup + sig.params.map do |sig_param| + index = unmatched_method_params.index do |method_param| + sig_param_matches_method_param?(sig_param, method_param) + end + raise Error, "Arity mismatch between method and signature" unless index + + method_param = unmatched_method_params.delete_at(index) + raise Error, "Arity mismatch between method and signature" unless method_param + + [sig_param, method_param] + end + end + + #: (SigParam sig_param, Param method_param) -> bool + def sig_param_matches_method_param?(sig_param, method_param) + return sig_param.name == method_param.name unless method_param.anonymous? + + case method_param + when RestParam + sig_param.name == "*" + when KwRestParam + sig_param.name == "**" + when BlockParam + sig_param.name == "&" + else + false + 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 + + #: (RBI::Method node, Sig sig) -> bool + def method_sig_starts_on_next_line?(node, sig) + return false unless sig.type_params.empty? + return false if node.params.any? { |param| param.is_a?(NoKwParam) } + + pairs = sig_method_param_pairs(node, sig) + block_pair = pairs.find { |_sig_param, method_param| method_param.is_a?(BlockParam) } + return false unless block_pair + + sig_block_param = block_pair.first + return false unless sig_block_param.comments? + return false unless print_sig_block_param?(sig_block_param) + + pairs.none? { |_sig_param, method_param| !method_param.is_a?(BlockParam) } + end + + #: (SigParam param) -> void + def print_sig_param_comments(param) + visit_all(param.comments) + end + #: (Param node, last: bool) -> void def print_param_comment_leading_space(node, last:) printn diff --git a/rbi/rbi.rbi b/rbi/rbi.rbi index eb65ddce..52d4e4bf 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,12 @@ class RBI::Parser::Visitor < ::Prism::Visitor sig { params(node: ::Prism::Node).returns(T::Boolean) } def braceless_shape?(node); end + sig { params(node: ::Prism::Node, comment: ::Prism::Comment).returns(T::Boolean) } + def inline_comment_for_node?(node, comment); 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 +1199,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 @@ -1625,8 +1638,8 @@ class RBI::RBSPrinter < ::RBI::Visitor sig { params(node: ::RBI::Method, sig: ::RBI::Sig).void } def print_method_sig(node, sig); end - sig { params(sig_param: T.nilable(::RBI::SigParam)).void } - def print_method_sig_block(sig_param); end + sig { params(sig_param: T.nilable(::RBI::SigParam), multiline: T::Boolean).void } + def print_method_sig_block(sig_param, multiline:); end sig { params(node: ::RBI::Method, sig: ::RBI::Sig, multiline: T::Boolean).void } def print_method_sig_content(node, sig, multiline:); end @@ -1798,6 +1811,9 @@ class RBI::RBSPrinter < ::RBI::Visitor private + sig { params(node: ::RBI::Method, sig: ::RBI::Sig).returns(T::Boolean) } + def method_sig_starts_on_next_line?(node, sig); end + sig { params(node: ::RBI::Node).returns(T::Boolean) } def oneline?(node); end @@ -1816,11 +1832,26 @@ 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(param: ::RBI::SigParam).void } + def print_sig_param_comments(param); end + + sig { params(node: ::RBI::Method, sig: ::RBI::Sig).returns(T::Array[[::RBI::SigParam, ::RBI::Param]]) } + def sig_method_param_pairs(node, sig); end + + sig { params(sig_param: ::RBI::SigParam, method_param: ::RBI::Param).returns(T::Boolean) } + def sig_param_matches_method_param?(sig_param, method_param); 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 diff --git a/test/rbi/parser_test.rb b/test/rbi/parser_test.rb index 3cf664d1..78fa3acc 100644 --- a/test/rbi/parser_test.rb +++ b/test/rbi/parser_test.rb @@ -405,6 +405,104 @@ 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(["`foo` comment"], param.comments.map(&:text)) + 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(["`foo` comment"], param.comments.map(&:text)) + 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)) + 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(param.comments) + assert_equal(["sig comment"], method.comments.map(&:text)) + end + def test_parse_methods_with_visibility rbi = <<~RBI private def m1; end diff --git a/test/rbi/rbs_printer_test.rb b/test/rbi/rbs_printer_test.rb index b70525aa..c4f005a1 100644 --- a/test/rbi/rbs_printer_test.rb +++ b/test/rbi/rbs_printer_test.rb @@ -704,6 +704,246 @@ 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")]) + 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) + ( + # First param + Integer a, + 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) + ( + # First param + Integer a, + # Second param + String b, + # Keyword param + c: Symbol + ) -> 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, + # Commented param + String b, + c: Symbol + ) -> void + RBI + end + + def test_prints_sig_param_comments_in_nested_scope + rbi = parse_rbi(<<~RBI) + class Foo + sig do + params( + a: Integer, + # Commented param + b: String + ).void + end + def foo(a, b); end + end + RBI + + rbs = rbi.rbs_string + + assert_equal(<<~RBS, rbs) + class Foo + def foo: ( + Integer a, + # Commented param + String b + ) -> void + end + RBS + ::RBS::Parser.parse_signature(rbs) + end + + def test_prints_multiline_signature_with_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", "T.proc.void", 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(" # Block param\n { -> void } -> void", out.string) + end + + def test_prints_multiline_signature_with_param_and_block_sig_param_comments + rbi_def = Method.new("foo") do |node| + node.params << ReqParam.new("a") + node.params << BlockParam.new("block") + end + + rbi_sig = Sig.new do |sig| + sig.params << SigParam.new("a", "Integer", comments: [Comment.new("Positional param")]) + sig.params << SigParam.new("block", "T.proc.void", 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( + "(\n # Positional param\n Integer a\n)\n # Block param\n { -> void } -> void", + out.string, + ) + 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("[U]\n # Block param\n { -> U } -> U", out.string) + 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 + | + # Block param + { -> void } -> void + 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_prints_overload_with_nil_block_sig_param_comments + rbi = parse_rbi(<<~RBI) + sig { void } + 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: -> void + | (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)) }