stats.rb 9.5 KB

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