diff --git a/lib/tapioca/gem/listeners/documentation.rb b/lib/tapioca/gem/listeners/documentation.rb index 0527189ff..c671f21f4 100644 --- a/lib/tapioca/gem/listeners/documentation.rb +++ b/lib/tapioca/gem/listeners/documentation.rb @@ -69,13 +69,11 @@ def documentation_comments(name, sigs: []) end return [] unless declaration - comments = declaration.definitions.flat_map(&:comments) - comments.uniq! - return [] if comments.empty? - - lines = comments - .map { |comment| comment.string.gsub(/^#+ ?/, "") } - .reject { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } || rbs_comment?(line) } + # Definitions often share a comment block, such as a license header, so de-duplicate them + lines = declaration.definitions + .map { |definition| comment_lines(definition) } + .uniq + .flatten # Strip leading and trailing blank lines, matching YARD's behavior lines = lines.reverse_each.drop_while(&:empty?).reverse_each.drop_while(&:empty?) @@ -83,6 +81,13 @@ def documentation_comments(name, sigs: []) lines.map! { |line| RBI::Comment.new(line) } end + #: (Rubydex::Definition definition) -> Array[String] + def comment_lines(definition) + lines = definition.comments.map { |comment| comment.string.gsub(/^#+ ?/, "") } + lines.reject! { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } || rbs_comment?(line) } + lines + end + # @override #: (NodeAdded event) -> bool def ignore?(event) diff --git a/spec/tapioca/gem/pipeline_spec.rb b/spec/tapioca/gem/pipeline_spec.rb index 307b47c54..98f59b1de 100644 --- a/spec/tapioca/gem/pipeline_spec.rb +++ b/spec/tapioca/gem/pipeline_spec.rb @@ -4468,6 +4468,78 @@ def something; end assert_equal(output, compile(include_doc: false)) end + it "does not repeat comments shared by multiple definitions" do + add_ruby_file("bar.rb", <<~RUBY) + # rubocop:disable Style/Documentation + # Licensed under the Foo License, Version 2.0 + module Namespace + # The answer + ANSWER = 42 + + # Does the thing + def self.thing; end + end + RUBY + + add_ruby_file("baz.rb", <<~RUBY) + # Namespace also holds Baz + module Namespace + class Baz; end + end + RUBY + + add_ruby_file("foo.rb", <<~RUBY) + # Licensed under the Foo License, Version 2.0 + module Namespace + # The answer + ANSWER = 42 + + # Does the thing + def self.thing; end + end + RUBY + + output = template(<<~RBI) + # Licensed under the Foo License, Version 2.0 + # Namespace also holds Baz + module Namespace + class << self + # Does the thing + def thing; end + end + end + + # The answer + Namespace::ANSWER = T.let(T.unsafe(nil), Integer) + + class Namespace::Baz; end + RBI + + assert_equal(output, compile(include_doc: true)) + end + + it "does not de-duplicate comment blocks that partially overlap" do + add_ruby_file("bar.rb", <<~RUBY) + # Licensed under the Foo License, Version 2.0 + module Namespace; end + RUBY + + add_ruby_file("foo.rb", <<~RUBY) + # Licensed under the Foo License, Version 2.0 + # Namespace is defined here because of whatever + module Namespace; end + RUBY + + output = template(<<~RBI) + # Licensed under the Foo License, Version 2.0 + # Licensed under the Foo License, Version 2.0 + # Namespace is defined here because of whatever + module Namespace; end + RBI + + assert_equal(output, compile(include_doc: true)) + end + it "properly processes void in type aliases" do add_ruby_file("foo.rb", <<~RUBY) module Foo