1##
2# RDoc statistics collector which prints a summary and report of a project's
3# documentation totals.
4
5class RDoc::Stats
6
7  ##
8  # Output level for the coverage report
9
10  attr_reader :coverage_level
11
12  ##
13  # Count of files parsed during parsing
14
15  attr_reader :files_so_far
16
17  ##
18  # Total number of files found
19
20  attr_reader :num_files
21
22  ##
23  # Creates a new Stats that will have +num_files+.  +verbosity+ defaults to 1
24  # which will create an RDoc::Stats::Normal outputter.
25
26  def initialize store, num_files, verbosity = 1
27    @num_files = num_files
28    @store     = store
29
30    @coverage_level   = 0
31    @doc_items        = nil
32    @files_so_far     = 0
33    @fully_documented = false
34    @num_params       = 0
35    @percent_doc      = nil
36    @start            = Time.now
37    @undoc_params     = 0
38
39    @display = case verbosity
40               when 0 then Quiet.new   num_files
41               when 1 then Normal.new  num_files
42               else        Verbose.new num_files
43               end
44  end
45
46  ##
47  # Records the parsing of an alias +as+.
48
49  def add_alias as
50    @display.print_alias as
51  end
52
53  ##
54  # Records the parsing of an attribute +attribute+
55
56  def add_attribute attribute
57    @display.print_attribute attribute
58  end
59
60  ##
61  # Records the parsing of a class +klass+
62
63  def add_class klass
64    @display.print_class klass
65  end
66
67  ##
68  # Records the parsing of +constant+
69
70  def add_constant constant
71    @display.print_constant constant
72  end
73
74  ##
75  # Records the parsing of +file+
76
77  def add_file(file)
78    @files_so_far += 1
79    @display.print_file @files_so_far, file
80  end
81
82  ##
83  # Records the parsing of +method+
84
85  def add_method(method)
86    @display.print_method method
87  end
88
89  ##
90  # Records the parsing of a module +mod+
91
92  def add_module(mod)
93    @display.print_module mod
94  end
95
96  ##
97  # Call this to mark the beginning of parsing for display purposes
98
99  def begin_adding
100    @display.begin_adding
101  end
102
103  ##
104  # Calculates documentation totals and percentages for classes, modules,
105  # constants, attributes and methods.
106
107  def calculate
108    return if @doc_items
109
110    ucm = @store.unique_classes_and_modules
111
112    classes = @store.unique_classes.reject { |cm| cm.full_name == 'Object' }
113
114    constants = []
115    ucm.each { |cm| constants.concat cm.constants }
116
117    methods = []
118    ucm.each { |cm| methods.concat cm.method_list }
119
120    attributes = []
121    ucm.each { |cm| attributes.concat cm.attributes }
122
123    @num_attributes, @undoc_attributes = doc_stats attributes
124    @num_classes,    @undoc_classes    = doc_stats classes
125    @num_constants,  @undoc_constants  = doc_stats constants
126    @num_methods,    @undoc_methods    = doc_stats methods
127    @num_modules,    @undoc_modules    = doc_stats @store.unique_modules
128
129    @num_items =
130      @num_attributes +
131      @num_classes +
132      @num_constants +
133      @num_methods +
134      @num_modules +
135      @num_params
136
137    @undoc_items =
138      @undoc_attributes +
139      @undoc_classes +
140      @undoc_constants +
141      @undoc_methods +
142      @undoc_modules +
143      @undoc_params
144
145    @doc_items = @num_items - @undoc_items
146  end
147
148  ##
149  # Sets coverage report level.  Accepted values are:
150  #
151  # false or nil:: No report
152  # 0:: Classes, modules, constants, attributes, methods
153  # 1:: Level 0 + method parameters
154
155  def coverage_level= level
156    level = -1 unless level
157
158    @coverage_level = level
159  end
160
161  ##
162  # Returns the length and number of undocumented items in +collection+.
163
164  def doc_stats collection
165    visible = collection.select { |item| item.display? }
166    [visible.length, visible.count { |item| not item.documented? }]
167  end
168
169  ##
170  # Call this to mark the end of parsing for display purposes
171
172  def done_adding
173    @display.done_adding
174  end
175
176  ##
177  # The documentation status of this project.  +true+ when 100%, +false+ when
178  # less than 100% and +nil+ when unknown.
179  #
180  # Set by calling #calculate
181
182  def fully_documented?
183    @fully_documented
184  end
185
186  ##
187  # A report that says you did a great job!
188
189  def great_job
190    report = []
191    report << '100% documentation!'
192    report << nil
193    report << 'Great Job!'
194
195    report.join "\n"
196  end
197
198  ##
199  # Calculates the percentage of items documented.
200
201  def percent_doc
202    return @percent_doc if @percent_doc
203
204    @fully_documented = (@num_items - @doc_items) == 0
205
206    @percent_doc = @doc_items.to_f / @num_items * 100 if @num_items.nonzero?
207    @percent_doc ||= 0
208
209    @percent_doc
210  end
211
212  ##
213  # Returns a report on which items are not documented
214
215  def report
216    if @coverage_level > 0 then
217      extend RDoc::Text
218    end
219
220    report = []
221
222    if @coverage_level.zero? then
223      calculate
224
225      return great_job if @num_items == @doc_items
226    end
227
228    ucm = @store.unique_classes_and_modules
229
230    ucm.sort.each do |cm|
231      report << report_class_module(cm) {
232        [
233          report_constants(cm),
234          report_attributes(cm),
235          report_methods(cm),
236        ].compact
237      }
238    end
239
240    if @coverage_level > 0 then
241      calculate
242
243      return great_job if @num_items == @doc_items
244    end
245
246    report.unshift nil
247    report.unshift 'The following items are not documented:'
248
249    report.join "\n"
250  end
251
252  ##
253  # Returns a report on undocumented attributes in ClassModule +cm+
254
255  def report_attributes cm
256    return if cm.attributes.empty?
257
258    report = []
259
260    cm.each_attribute do |attr|
261      next if attr.documented?
262      line = attr.line ? ":#{attr.line}" : nil
263      report << "  #{attr.definition} :#{attr.name} # in file #{attr.file.full_name}#{line}"
264    end
265
266    report
267  end
268
269  ##
270  # Returns a report on undocumented items in ClassModule +cm+
271
272  def report_class_module cm
273    return if cm.fully_documented? and @coverage_level.zero?
274    return unless cm.display?
275
276    report = []
277
278    if cm.in_files.empty? then
279      report << "# #{cm.definition} is referenced but empty."
280      report << "#"
281      report << "# It probably came from another project.  I'm sorry I'm holding it against you."
282      report << nil
283
284      return report
285    elsif cm.documented? then
286      documented = true
287      report << "#{cm.definition} # is documented"
288    else
289      report << '# in files:'
290
291      cm.in_files.each do |file|
292        report << "#   #{file.full_name}"
293      end
294
295      report << nil
296
297      report << "#{cm.definition}"
298    end
299
300    body = yield.flatten # HACK remove #flatten
301
302    return if body.empty? and documented
303
304    report << nil << body unless body.empty?
305
306    report << 'end'
307    report << nil
308
309    report
310  end
311
312  ##
313  # Returns a report on undocumented constants in ClassModule +cm+
314
315  def report_constants cm
316    return if cm.constants.empty?
317
318    report = []
319
320    cm.each_constant do |constant|
321      # TODO constant aliases are listed in the summary but not reported
322      # figure out what to do here
323      next if constant.documented? || constant.is_alias_for
324
325      line = constant.line ? ":#{constant.line}" : line
326      report << "  # in file #{constant.file.full_name}#{line}"
327      report << "  #{constant.name} = nil"
328    end
329
330    report
331  end
332
333  ##
334  # Returns a report on undocumented methods in ClassModule +cm+
335
336  def report_methods cm
337    return if cm.method_list.empty?
338
339    report = []
340
341    cm.each_method do |method|
342      next if method.documented? and @coverage_level.zero?
343
344      if @coverage_level > 0 then
345        params, undoc = undoc_params method
346
347        @num_params += params
348
349        unless undoc.empty? then
350          @undoc_params += undoc.length
351
352          undoc = undoc.map do |param| "+#{param}+" end
353          param_report = "  # #{undoc.join ', '} is not documented"
354        end
355      end
356
357      next if method.documented? and not param_report
358
359      line = method.line ? ":#{method.line}" : nil
360      scope = method.singleton ? 'self.' : nil
361
362      report << "  # in file #{method.file.full_name}#{line}"
363      report << param_report if param_report
364      report << "  def #{scope}#{method.name}#{method.params}; end"
365      report << nil
366    end
367
368    report
369  end
370
371  ##
372  # Returns a summary of the collected statistics.
373
374  def summary
375    calculate
376
377    num_width = [@num_files, @num_items].max.to_s.length
378    undoc_width = [
379      @undoc_attributes,
380      @undoc_classes,
381      @undoc_constants,
382      @undoc_items,
383      @undoc_methods,
384      @undoc_modules,
385      @undoc_params,
386    ].max.to_s.length
387
388    report = []
389    report << 'Files:      %*d' % [num_width, @num_files]
390
391    report << nil
392
393    report << 'Classes:    %*d (%*d undocumented)' % [
394      num_width, @num_classes, undoc_width, @undoc_classes]
395    report << 'Modules:    %*d (%*d undocumented)' % [
396      num_width, @num_modules, undoc_width, @undoc_modules]
397    report << 'Constants:  %*d (%*d undocumented)' % [
398      num_width, @num_constants, undoc_width, @undoc_constants]
399    report << 'Attributes: %*d (%*d undocumented)' % [
400      num_width, @num_attributes, undoc_width, @undoc_attributes]
401    report << 'Methods:    %*d (%*d undocumented)' % [
402      num_width, @num_methods, undoc_width, @undoc_methods]
403    report << 'Parameters: %*d (%*d undocumented)' % [
404      num_width, @num_params, undoc_width, @undoc_params] if
405        @coverage_level > 0
406
407    report << nil
408
409    report << 'Total:      %*d (%*d undocumented)' % [
410      num_width, @num_items, undoc_width, @undoc_items]
411
412    report << '%6.2f%% documented' % percent_doc
413    report << nil
414    report << 'Elapsed: %0.1fs' % (Time.now - @start)
415
416    report.join "\n"
417  end
418
419  ##
420  # Determines which parameters in +method+ were not documented.  Returns a
421  # total parameter count and an Array of undocumented methods.
422
423  def undoc_params method
424    @formatter ||= RDoc::Markup::ToTtOnly.new
425
426    params = method.param_list
427
428    return 0, [] if params.empty?
429
430    document = parse method.comment
431
432    tts = document.accept @formatter
433
434    undoc = params - tts
435
436    [params.length, undoc]
437  end
438
439  autoload :Quiet,   'rdoc/stats/quiet'
440  autoload :Normal,  'rdoc/stats/normal'
441  autoload :Verbose, 'rdoc/stats/verbose'
442
443end
444
445