diff --git a/lib/rdoc/rdoc.rb b/lib/rdoc/rdoc.rb index 195bd21421..da8ba483f0 100644 --- a/lib/rdoc/rdoc.rb +++ b/lib/rdoc/rdoc.rb @@ -497,7 +497,7 @@ def document(options) if @options.coverage_report then puts - puts @stats.report.accept RDoc::Markup::ToRdoc.new + puts @stats.report elsif file_info.empty? then $stderr.puts "\nNo newer files." unless @options.quiet else @@ -510,7 +510,7 @@ def document(options) if @stats and (@options.coverage_report or not @options.quiet) then puts - puts @stats.summary.accept RDoc::Markup::ToRdoc.new + puts @stats.summary end exit @stats.fully_documented? if @options.coverage_report diff --git a/lib/rdoc/stats.rb b/lib/rdoc/stats.rb index 683fe4a19a..6daad7f296 100644 --- a/lib/rdoc/stats.rb +++ b/lib/rdoc/stats.rb @@ -7,6 +7,12 @@ class RDoc::Stats include RDoc::Text + TYPE_ORDER = %w[Class Module Constant Attribute Method].freeze + GREAT_JOB_MESSAGE = <<~MSG + 100% documentation! + Great Job! + MSG + ## # Output level for the coverage report @@ -193,17 +199,6 @@ def fully_documented? @fully_documented end - ## - # A report that says you did a great job! - - def great_job - report = RDoc::Markup::Document.new - - report << RDoc::Markup::Paragraph.new('100% documentation!') - report << RDoc::Markup::Paragraph.new('Great Job!') - - report - end ## # Calculates the percentage of items documented. @@ -230,48 +225,55 @@ def report if @coverage_level.zero? then calculate - return great_job if @num_items == @doc_items + return GREAT_JOB_MESSAGE if @num_items == @doc_items end - ucm = @store.unique_classes_and_modules + items, empty_classes = collect_undocumented_items - report = RDoc::Markup::Document.new - report << RDoc::Markup::Paragraph.new('The following items are not documented:') - report << RDoc::Markup::BlankLine.new - - ucm.sort.each do |cm| - body = report_class_module(cm) { - [ - report_constants(cm), - report_attributes(cm), - report_methods(cm), - ].compact - } + if @coverage_level > 0 then + calculate - report << body if body + return GREAT_JOB_MESSAGE if @num_items == @doc_items end - if @coverage_level > 0 then - calculate + report = +"" + report << "The following items are not documented:\n\n" - return great_job if @num_items == @doc_items + # Referenced-but-empty classes + empty_classes.each do |cm| + report << "#{cm.full_name} is referenced but empty.\n" + report << "It probably came from another project. I'm sorry I'm holding it against you.\n\n" end - report - end + # Group items by file, then by type + by_file = items.group_by { |item| item[:file] } - ## - # Returns a report on undocumented attributes in ClassModule +cm+ + by_file.sort_by { |file, _| file }.each do |file, file_items| + report << "#{file}:\n" - def report_attributes(cm) - return if cm.attributes.empty? + by_type = file_items.group_by { |item| item[:type] } - report = [] + TYPE_ORDER.each do |type| + next unless by_type[type] + + report << " #{type}:\n" + + sorted = by_type[type].sort_by { |item| [item[:line] || 0, item[:name]] } + name_width = sorted.reduce(0) { |max, item| item[:line] && item[:name].length > max ? item[:name].length : max } + + sorted.each do |item| + if item[:line] + report << " %-*s %s:%d\n" % [name_width, item[:name], item[:file], item[:line]] + else + report << " #{item[:name]}\n" + end + + if item[:undoc_params] + report << " Undocumented params: #{item[:undoc_params].join(', ')}\n" + end + end + end - cm.attributes.each do |attr| - next if attr.documented? - line = attr.line ? ":#{attr.line}" : nil - report << " #{attr.definition} :#{attr.name} # in file #{attr.file.full_name}#{line}\n" report << "\n" end @@ -279,115 +281,111 @@ def report_attributes(cm) end ## - # Returns a report on undocumented items in ClassModule +cm+ + # Collects all undocumented items across all classes and modules. + # Returns [items, empty_classes] where items is an Array of Hashes + # with keys :type, :name, :file, :line, and empty_classes is an + # Array of ClassModule objects that are referenced but have no files. - def report_class_module(cm) - return if cm.fully_documented? and @coverage_level.zero? - return unless cm.display? + def collect_undocumented_items + empty_classes = [] + items = [] - report = RDoc::Markup::Document.new + @store.unique_classes_and_modules.each do |cm| + next unless cm.display? - if cm.in_files.empty? then - report << RDoc::Markup::Paragraph.new("#{cm.definition} is referenced but empty.") - report << RDoc::Markup::Paragraph.new("It probably came from another project. I'm sorry I'm holding it against you.") - - return report - elsif cm.documented? then - documented = true - klass = RDoc::Markup::Verbatim.new("#{cm.definition} # is documented\n") - else - report << RDoc::Markup::Paragraph.new('In files:') - - list = RDoc::Markup::List.new :BULLET - - cm.in_files.each do |file| - para = RDoc::Markup::Paragraph.new file.full_name - list << RDoc::Markup::ListItem.new(nil, para) + if cm.in_files.empty? + empty_classes << cm + next end - report << list - report << RDoc::Markup::BlankLine.new + unless cm.documented? || cm.full_name == 'Object' + file = cm.in_files.first&.full_name + items << { type: cm.type.capitalize, name: cm.full_name, file: file, line: nil } if file + end - klass = RDoc::Markup::Verbatim.new("#{cm.definition}\n") + collect_undocumented_constants(cm, items) + collect_undocumented_attributes(cm, items) + collect_undocumented_methods(cm, items) end - klass << "\n" - - body = yield.flatten # HACK remove #flatten - - if body.empty? then - return if documented + [items, empty_classes] + end - klass.parts.pop - else - klass.parts.concat body - end + ## + # Collects undocumented constants from +cm+ into +items+. - klass << "end\n" + def collect_undocumented_constants(cm, items) + cm.constants.each do |constant| + next if constant.documented? || constant.is_alias_for - report << klass + file = constant.file&.full_name + next unless file - report + items << { + type: "Constant", + name: constant.full_name, + file: file, + line: constant.line, + } + end end ## - # Returns a report on undocumented constants in ClassModule +cm+ + # Collects undocumented attributes from +cm+ into +items+. - def report_constants(cm) - return if cm.constants.empty? - - report = [] + def collect_undocumented_attributes(cm, items) + cm.attributes.each do |attr| + next if attr.documented? - cm.constants.each do |constant| - # TODO constant aliases are listed in the summary but not reported - # figure out what to do here - next if constant.documented? || constant.is_alias_for + file = attr.file&.full_name + next unless file - line = constant.line ? ":#{constant.line}" : line - report << " # in file #{constant.file.full_name}#{line}\n" - report << " #{constant.name} = nil\n" - report << "\n" + scope = attr.singleton ? "." : "#" + items << { + type: "Attribute", + name: "#{cm.full_name}#{scope}#{attr.name}", + file: file, + line: attr.line, + } end - - report end ## - # Returns a report on undocumented methods in ClassModule +cm+ - - def report_methods(cm) - return if cm.method_list.empty? - - report = [] + # Collects undocumented methods from +cm+ into +items+. + # At coverage level > 0, also counts undocumented parameters. + def collect_undocumented_methods(cm, items) cm.each_method do |method| - next if method.documented? and @coverage_level.zero? + next if method.documented? && @coverage_level.zero? - if @coverage_level > 0 then - params, undoc = undoc_params method + undoc_param_names = nil + if @coverage_level > 0 + params, undoc = undoc_params method @num_params += params - unless undoc.empty? then + unless undoc.empty? @undoc_params += undoc.length - - undoc = undoc.map do |param| "+#{param}+" end - param_report = " # #{undoc.join ', '} is not documented\n" + undoc_param_names = undoc end end - next if method.documented? and not param_report + next if method.documented? && !undoc_param_names - line = method.line ? ":#{method.line}" : nil - scope = method.singleton ? 'self.' : nil + file = method.file&.full_name + next unless file - report << " # in file #{method.file.full_name}#{line}\n" - report << param_report if param_report - report << " def #{scope}#{method.name}#{method.params}; end\n" - report << "\n" - end + scope = method.singleton ? "." : "#" + item = { + type: "Method", + name: "#{cm.full_name}#{scope}#{method.name}", + file: file, + line: method.line, + } + item[:undoc_params] = undoc_param_names if undoc_param_names - report + items << item + end end ## @@ -407,12 +405,10 @@ def summary @undoc_params, ].max.to_s.length - report = RDoc::Markup::Verbatim.new + report = +"" report << "Files: %*d\n" % [num_width, @num_files] - report << "\n" - report << "Classes: %*d (%*d undocumented)\n" % [ num_width, @num_classes, undoc_width, @undoc_classes] report << "Modules: %*d (%*d undocumented)\n" % [ @@ -426,17 +422,14 @@ def summary report << "Parameters: %*d (%*d undocumented)\n" % [ num_width, @num_params, undoc_width, @undoc_params] if @coverage_level > 0 - report << "\n" - report << "Total: %*d (%*d undocumented)\n" % [ num_width, @num_items, undoc_width, @undoc_items] - report << "%6.2f%% documented\n" % percent_doc report << "\n" report << "Elapsed: %0.1fs\n" % (Time.now - @start) - RDoc::Markup::Document.new report + report end ## diff --git a/test/rdoc/rdoc_stats_test.rb b/test/rdoc/rdoc_stats_test.rb index 1f02e460c3..320cd29036 100644 --- a/test/rdoc/rdoc_stats_test.rb +++ b/test/rdoc/rdoc_stats_test.rb @@ -45,19 +45,7 @@ def test_report_attr report = @s.report - expected = - doc( - para('The following items are not documented:'), - blank_line, - verb( - "class C # is documented\n", - "\n", - " attr_accessor :a # in file file.rb\n", - "\n", - "end\n"), - blank_line) - - assert_equal expected, report + assert_match "file.rb:\n Attribute:\n C#a\n", report end def test_report_attr_documented @@ -71,9 +59,7 @@ def test_report_attr_documented @store.complete :public - report = @s.report - - assert_equal @s.great_job, report + assert_match "100% documentation!\nGreat Job!\n", @s.report end def test_report_attr_line @@ -88,7 +74,7 @@ def test_report_attr_line @store.complete :public - assert_match '# in file file.rb:3', @s.report.accept(to_rdoc) + assert_match 'C#a file.rb:3', @s.report end def test_report_constant @@ -104,20 +90,7 @@ def test_report_constant report = @s.report - expected = - doc( - para('The following items are not documented:'), - blank_line, - verb( - "module M # is documented\n", - "\n", - " # in file file.rb\n", - " C = nil\n", - "\n", - "end\n"), - blank_line) - - assert_equal expected, report + assert_match "file.rb:\n Constant:\n M::C\n", report end def test_report_constant_alias @@ -133,11 +106,8 @@ def test_report_constant_alias @store.complete :public - report = @s.report - - # TODO change this to refute match, aliases should be ignored as they are - # programmer convenience constructs - assert_match 'class Object', report.accept(to_rdoc) + # Constant aliases are skipped in the report + refute_match 'CA', @s.report end def test_report_constant_documented @@ -151,9 +121,7 @@ def test_report_constant_documented @store.complete :public - report = @s.report - - assert_equal @s.great_job, report + assert_match "100% documentation!\nGreat Job!\n", @s.report end def test_report_constant_line @@ -168,7 +136,7 @@ def test_report_constant_line @store.complete :public - assert_match '# in file file.rb:5', @s.report.accept(to_rdoc) + assert_match 'M::C file.rb:5', @s.report end def test_report_class @@ -184,18 +152,7 @@ def test_report_class report = @s.report - expected = - doc( - para('The following items are not documented:'), - blank_line, - para('In files:'), - list(:BULLET, *[ - item(nil, para('file.rb'))]), - blank_line, - verb("class C\n", "end\n"), - blank_line) - - assert_equal expected, report + assert_match "file.rb:\n Class:\n C\n", report end def test_report_skip_object @@ -209,7 +166,7 @@ def test_report_skip_object @store.complete :public - refute_match %r%^class Object$%, @s.report.accept(to_rdoc) + refute_match(/^\s+Object$/, @s.report) end def test_report_class_documented @@ -224,9 +181,7 @@ def test_report_class_documented @store.complete :public - report = @s.report - - assert_equal @s.great_job, report + assert_match "100% documentation!\nGreat Job!\n", @s.report end def test_report_class_documented_level_1 @@ -251,20 +206,7 @@ def test_report_class_documented_level_1 @s.coverage_level = 1 - report = @s.report - - expected = - doc( - para('The following items are not documented:'), - blank_line, - para('In files:'), - list(:BULLET, *[ - item(nil, para('file.rb'))]), - blank_line, - verb("class C2\n", "end\n"), - blank_line) - - assert_equal expected, report + assert_match "file.rb:\n Class:\n C2\n", @s.report end def test_report_class_empty @@ -274,15 +216,8 @@ def test_report_class_empty report = @s.report - expected = - doc( - para('The following items are not documented:'), - blank_line, - para('class C is referenced but empty.'), - para("It probably came from another project. I'm sorry I'm holding it against you."), - blank_line) - - assert_equal expected, report + assert_match "C is referenced but empty.\n", report + assert_match "It probably came from another project.", report end def test_report_class_empty_2 @@ -296,20 +231,8 @@ def test_report_class_empty_2 @store.complete :public @s.coverage_level = 1 - report = @s.report - expected = - doc( - para('The following items are not documented:'), - blank_line, - para('In files:'), - list(:BULLET, *[ - item(nil, para('file.rb'))]), - blank_line, - verb("class C1\n", "end\n"), - blank_line) - - assert_equal expected, report + assert_match "file.rb:\n Class:\n C1\n", @s.report end def test_report_class_method_documented @@ -323,39 +246,22 @@ def test_report_class_method_documented @store.complete :public - report = @s.report - - expected = - doc( - para('The following items are not documented:'), - blank_line, - para('In files:'), - list(:BULLET, *[ - item(nil, para('file.rb'))]), - blank_line, - verb("class C\n", "end\n"), - blank_line) - - assert_equal expected, report + assert_match "file.rb:\n Class:\n C\n", @s.report end - def test_report_class_module_ignore + def test_report_ignored_class_excluded c = @tl.add_class RDoc::NormalClass, 'C' c.ignore @store.complete :public - report = @s.report_class_module c - - assert_nil report + refute_match(/^\s+C$/, @s.report) end def test_report_empty @store.complete :public - report = @s.report - - assert_equal @s.great_job, report + assert_match "100% documentation!\nGreat Job!\n", @s.report end def test_report_method @@ -374,22 +280,7 @@ def test_report_method @store.complete :public - report = @s.report - - expected = - doc( - para('The following items are not documented:'), - blank_line, - verb(*[ - "class C # is documented\n", - "\n", - " # in file file.rb\n", - " def m1; end\n", - "\n", - "end\n"]), - blank_line) - - assert_equal expected, report + assert_match "file.rb:\n Method:\n C#m1\n", @s.report end def test_report_method_class @@ -408,22 +299,7 @@ def test_report_method_class @store.complete :public - report = @s.report - - expected = - doc( - para('The following items are not documented:'), - blank_line, - verb(*[ - "class C # is documented\n", - "\n", - " # in file file.rb\n", - " def self.m1; end\n", - "\n", - "end\n"]), - blank_line) - - assert_equal expected, report + assert_match "file.rb:\n Method:\n C.m1\n", @s.report end def test_report_method_documented @@ -438,9 +314,7 @@ def test_report_method_documented @store.complete :public - report = @s.report - - assert_equal @s.great_job, report + assert_match "100% documentation!\nGreat Job!\n", @s.report end def test_report_method_line @@ -455,7 +329,7 @@ def test_report_method_line @store.complete :public - assert_match '# in file file.rb:4', @s.report.accept(to_rdoc) + assert_match 'C#m1 file.rb:4', @s.report end def test_report_method_parameters @@ -477,23 +351,10 @@ def test_report_method_parameters @store.complete :public @s.coverage_level = 1 - report = @s.report - expected = - doc( - para('The following items are not documented:'), - blank_line, - verb(*[ - "class C # is documented\n", - "\n", - " # in file file.rb\n", - " # +p2+ is not documented\n", - " def m1(p1, p2); end\n", - "\n", - "end\n"]), - blank_line) + report = @s.report - assert_equal expected, report + assert_match " Method:\n C#m1\n Undocumented params: p2\n", report end def test_report_method_parameters_documented @@ -511,9 +372,8 @@ def test_report_method_parameters_documented @store.complete :public @s.coverage_level = 1 - report = @s.report - assert_equal @s.great_job, report + assert_match "100% documentation!\nGreat Job!\n", @s.report end def test_report_method_parameters_yield @@ -533,23 +393,155 @@ def test_report_method_parameters_yield @store.complete :public @s.coverage_level = 1 + + assert_match " Method:\n C#m\n Undocumented params: b, d\n", @s.report + end + + # New tests for file-centric format + + def test_report_multiple_files + tl2 = @store.add_file 'other.rb' + tl2.parser = RDoc::Parser::Ruby + + c1 = @tl.add_class RDoc::NormalClass, 'C1' + c1.record_location @tl + + c2 = tl2.add_class RDoc::NormalClass, 'C2' + c2.record_location tl2 + + @store.complete :public + report = @s.report - expected = - doc( - para('The following items are not documented:'), - blank_line, - verb( - "class C # is documented\n", - "\n", - " # in file file.rb\n", - " # +b+, +d+ is not documented\n", - " def m; end\n", - "\n", - "end\n"), - blank_line) + assert_match(/file\.rb:/, report) + assert_match(/other\.rb:/, report) + assert_match(/Class:\n\s+C1/, report) + assert_match(/Class:\n\s+C2/, report) + end + + def test_report_file_sorting + tl_b = @store.add_file 'b.rb' + tl_b.parser = RDoc::Parser::Ruby + tl_a = @store.add_file 'a.rb' + tl_a.parser = RDoc::Parser::Ruby + + c1 = tl_b.add_class RDoc::NormalClass, 'B' + c1.record_location tl_b - assert_equal expected, report + c2 = tl_a.add_class RDoc::NormalClass, 'A' + c2.record_location tl_a + + @store.complete :public + + report = @s.report + + a_pos = report.index('a.rb:') + b_pos = report.index('b.rb:') + + assert a_pos < b_pos, "a.rb should appear before b.rb" + end + + def test_report_items_without_line_sort_first + c = @tl.add_class RDoc::NormalClass, 'C' + c.record_location @tl + c.add_comment 'C', @tl + + m1 = RDoc::AnyMethod.new nil, 'with_line' + m1.record_location @tl + m1.line = 3 + c.add_method m1 + + m2 = RDoc::AnyMethod.new nil, 'no_line' + m2.record_location @tl + c.add_method m2 + + @store.complete :public + + report = @s.report + + no_line_pos = report.index('no_line') + with_line_pos = report.index('with_line') + + assert no_line_pos < with_line_pos, + "Items without line numbers should appear before items with line numbers" + end + + def test_report_item_sorting_by_line + c = @tl.add_class RDoc::NormalClass, 'C' + c.record_location @tl + c.add_comment 'C', @tl + + m1 = RDoc::AnyMethod.new nil, 'z_method' + m1.record_location @tl + m1.line = 5 + c.add_method m1 + + m2 = RDoc::AnyMethod.new nil, 'a_method' + m2.record_location @tl + m2.line = 10 + c.add_method m2 + + @store.complete :public + + report = @s.report + + z_pos = report.index('z_method') + a_pos = report.index('a_method') + + assert z_pos < a_pos, "z_method (line 5) should appear before a_method (line 10)" + end + + def test_report_mixed_types_in_file + c = @tl.add_class RDoc::NormalClass, 'C' + c.record_location @tl + c.add_comment 'C', @tl + + a = RDoc::Attr.new nil, 'a', 'RW', nil + a.record_location @tl + c.add_attribute a + + k = RDoc::Constant.new 'K', nil, nil + k.record_location @tl + c.add_constant k + + m = RDoc::AnyMethod.new nil, 'm' + m.record_location @tl + c.add_method m + + @store.complete :public + + report = @s.report + + assert_match(/file\.rb:/, report) + assert_match(/Attribute:\n\s+C#a/, report) + assert_match(/Constant:\n\s+C::K/, report) + assert_match(/Method:\n\s+C#m/, report) + end + + def test_report_multi_file_class + tl2 = @store.add_file 'ext.c' + tl2.parser = RDoc::Parser::C + + c = @tl.add_class RDoc::NormalClass, 'C' + c.record_location @tl + c.add_comment 'C', @tl + + m1 = RDoc::AnyMethod.new nil, 'ruby_method' + m1.record_location @tl + c.add_method m1 + + m2 = RDoc::AnyMethod.new nil, 'c_method' + m2.record_location tl2 + c.add_method m2 + + @store.complete :public + + report = @s.report + + assert_match(/ext\.c:/, report) + assert_match(/C#c_method/, report) + assert_match(/file\.rb:/, report) + assert_match(/C#ruby_method/, report) end def test_summary @@ -573,24 +565,24 @@ def test_summary @store.complete :public - summary = @s.summary.accept to_rdoc - summary.sub!(/ Elapsed:.*/m, '') + summary = @s.summary + summary.sub!(/Elapsed:.*/m, '') - expected = <<-EXPECTED - Files: 0 + expected = <<~EXPECTED + Files: 0 - Classes: 1 (1 undocumented) - Modules: 1 (1 undocumented) - Constants: 1 (1 undocumented) - Attributes: 1 (1 undocumented) - Methods: 1 (1 undocumented) + Classes: 1 (1 undocumented) + Modules: 1 (1 undocumented) + Constants: 1 (1 undocumented) + Attributes: 1 (1 undocumented) + Methods: 1 (1 undocumented) - Total: 5 (5 undocumented) - 0.00% documented + Total: 5 (5 undocumented) + 0.00% documented EXPECTED - assert_equal summary, expected + assert_equal expected, summary end def test_summary_level_false @@ -601,24 +593,24 @@ def test_summary_level_false @s.coverage_level = false - summary = @s.summary.accept to_rdoc - summary.sub!(/ Elapsed:.*/m, '') + summary = @s.summary + summary.sub!(/Elapsed:.*/m, '') - expected = <<-EXPECTED - Files: 0 + expected = <<~EXPECTED + Files: 0 - Classes: 1 (1 undocumented) - Modules: 0 (0 undocumented) - Constants: 0 (0 undocumented) - Attributes: 0 (0 undocumented) - Methods: 0 (0 undocumented) + Classes: 1 (1 undocumented) + Modules: 0 (0 undocumented) + Constants: 0 (0 undocumented) + Attributes: 0 (0 undocumented) + Methods: 0 (0 undocumented) - Total: 1 (1 undocumented) - 0.00% documented + Total: 1 (1 undocumented) + 0.00% documented EXPECTED - assert_equal summary, expected + assert_equal expected, summary end def test_summary_level_1 @@ -637,29 +629,25 @@ def test_summary_level_1 @s.coverage_level = 1 @s.report - summary = @s.summary.accept to_rdoc - summary.sub!(/ Elapsed:.*/m, '') + summary = @s.summary + summary.sub!(/Elapsed:.*/m, '') - expected = <<-EXPECTED - Files: 0 + expected = <<~EXPECTED + Files: 0 - Classes: 1 (0 undocumented) - Modules: 0 (0 undocumented) - Constants: 0 (0 undocumented) - Attributes: 0 (0 undocumented) - Methods: 1 (0 undocumented) - Parameters: 2 (1 undocumented) + Classes: 1 (0 undocumented) + Modules: 0 (0 undocumented) + Constants: 0 (0 undocumented) + Attributes: 0 (0 undocumented) + Methods: 1 (0 undocumented) + Parameters: 2 (1 undocumented) - Total: 4 (1 undocumented) - 75.00% documented + Total: 4 (1 undocumented) + 75.00% documented EXPECTED - assert_equal summary, expected - end - - def to_rdoc - RDoc::Markup::ToRdoc.new + assert_equal expected, summary end def test_undoc_params