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