1#!/usr/local/bin/python3.8 2 3from __future__ import unicode_literals 4 5import datetime 6import io 7import locale 8import operator 9import optparse 10import os 11import sys 12import subprocess 13 14from collections import Counter 15from pygments import highlight 16from pygments.lexers import guess_lexer, guess_lexer_for_filename 17from pygments.formatters import HtmlFormatter # pylint: disable=no-name-in-module 18from pygments.util import ClassNotFound 19from xml.sax import parse as xml_parse 20from xml.sax import SAXParseException as XmlParseException 21from xml.sax.handler import ContentHandler as XmlContentHandler 22from xml.sax.saxutils import escape 23""" 24Turns a cppcheck xml file into a browsable html report along 25with syntax highlighted source code. 26""" 27 28STYLE_FILE = """ 29body { 30 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; 31 font-size: 13px; 32 line-height: 1.5; 33 margin: 0; 34 width: auto; 35} 36 37h1 { 38 margin: 10px; 39} 40 41.header { 42 border-bottom: thin solid #aaa; 43} 44 45.footer { 46 border-top: thin solid #aaa; 47 font-size: 90%; 48 margin-top: 5px; 49} 50 51.footer ul { 52 list-style-type: none; 53 padding-left: 0; 54} 55 56.footer > p { 57 margin: 4px; 58} 59 60.wrapper { 61 display: -webkit-box; 62 display: -ms-flexbox; 63 display: flex; 64 -webkit-box-pack: justify; 65 -ms-flex-pack: justify; 66 justify-content: space-between; 67} 68 69#menu, 70#menu_index { 71 text-align: left; 72 width: 350px; 73 height: 90vh; 74 min-height: 200px; 75 overflow: auto; 76 position: -webkit-sticky; 77 position: sticky; 78 top: 0; 79 padding: 0 15px 15px 15px; 80} 81 82#menu > a { 83 display: block; 84 margin-left: 10px; 85 font-size: 12px; 86 z-index: 1; 87} 88 89#content, 90#content_index { 91 background-color: #fff; 92 -webkit-box-sizing: content-box; 93 -moz-box-sizing: content-box; 94 box-sizing: content-box; 95 padding: 0 15px 15px 15px; 96 width: calc(100% - 350px); 97 height: 100%; 98 overflow-x: auto; 99} 100 101#filename { 102 margin-left: 10px; 103 font-size: 12px; 104 z-index: 1; 105} 106 107.error { 108 background-color: #ffb7b7; 109} 110 111.error2 { 112 background-color: #faa; 113 display: inline-block; 114 margin-left: 4px; 115} 116 117.inconclusive { 118 background-color: #b6b6b4; 119} 120 121.inconclusive2 { 122 background-color: #b6b6b4; 123 display: inline-block; 124 margin-left: 4px; 125} 126 127.verbose { 128 display: inline-block; 129 vertical-align: top; 130 cursor: help; 131} 132 133.verbose .content { 134 display: none; 135 position: absolute; 136 padding: 10px; 137 margin: 4px; 138 max-width: 40%; 139 white-space: pre-wrap; 140 border: 1px solid #000; 141 background-color: #ffffcc; 142 cursor: auto; 143} 144 145.highlight .hll { 146 padding: 1px; 147} 148 149.highlighttable { 150 background-color: #fff; 151 z-index: 10; 152 position: relative; 153 margin: -10px; 154} 155 156.linenos { 157 border-right: thin solid #aaa; 158 color: #d3d3d3; 159 padding-right: 6px; 160} 161 162.d-none { 163 display: none; 164} 165""" 166 167HTML_HEAD = """ 168<!doctype html> 169<html lang="en"> 170 <head> 171 <meta charset="utf-8"> 172 <title>Cppcheck - HTML report - %s</title> 173 <link rel="stylesheet" href="style.css"> 174 <style> 175%s 176 </style> 177 <script> 178 function getStyle(el, styleProp) { 179 var y; 180 181 if (el.currentStyle) { 182 y = el.currentStyle[styleProp]; 183 } else if (window.getComputedStyle) { 184 y = document.defaultView.getComputedStyle(el, null).getPropertyValue(styleProp); 185 } 186 187 return y; 188 } 189 190 function toggle() { 191 var el = this.expandable_content; 192 var mark = this.expandable_marker; 193 194 if (el.style.display === "block") { 195 el.style.display = "none"; 196 mark.textContent = "[+]"; 197 } else { 198 el.style.display = "block"; 199 mark.textContent = "[-]"; 200 } 201 } 202 203 function initExpandables() { 204 var elements = document.querySelectorAll(".expandable"); 205 206 for (var i = 0, len = elements.length; i < len; i++) { 207 var el = elements[i]; 208 var clickable = el.querySelector("span"); 209 var marker = clickable.querySelector(".marker"); 210 var content = el.querySelector(".content"); 211 var width = clickable.clientWidth - parseInt(getStyle(content, "padding-left")) - parseInt(getStyle(content, "padding-right")); 212 content.style.width = width + "px"; 213 clickable.expandable_content = content; 214 clickable.expandable_marker = marker; 215 clickable.addEventListener("click", toggle); 216 } 217 } 218 219 function toggleDisplay(id) { 220 var elements = document.querySelectorAll("." + id); 221 222 for (var i = 0, len = elements.length; i < len; i++) { 223 elements[i].classList.toggle("d-none"); 224 } 225 } 226 227 function toggleAll() { 228 var elements = document.querySelectorAll("input"); 229 230 // starting from 1 since 0 is the "toggle all" input 231 for (var i = 1, len = elements.length; i < len; i++) { 232 var el = elements[i]; 233 234 if (el.checked) { 235 el.checked = false; 236 } else { 237 el.checked = true; 238 } 239 240 toggleDisplay(el.id); 241 } 242 } 243 window.addEventListener("load", initExpandables); 244 </script> 245 </head> 246 <body> 247 <div id="header" class="header"> 248 <h1>Cppcheck report - %s: %s</h1> 249 </div> 250 <div class="wrapper"> 251 <div id="menu"> 252 <p id="filename"><a href="index.html">Defects:</a> %s</p> 253""" 254 255HTML_HEAD_END = """ 256 </div> 257 <div id="content"> 258""" 259 260HTML_FOOTER = """ 261 </div> <!-- /.wrapper --> 262 </div> 263 <div id="footer" class="footer"> 264 <p> 265 Cppcheck %s - a tool for static C/C++ code analysis<br> 266 <br> 267 Internet: <a href="https://cppcheck.sourceforge.io">https://cppcheck.sourceforge.io</a><br> 268 IRC: <a href="irc://irc.freenode.net/cppcheck">irc://irc.freenode.net/cppcheck</a><br> 269 </p> 270 </div> 271 </body> 272</html> 273""" 274 275HTML_ERROR = "<span class=\"error2\"><--- %s</span>\n" 276HTML_INCONCLUSIVE = "<span class=\"inconclusive2\"><--- %s</span>\n" 277 278HTML_EXPANDABLE_ERROR = "<div class=\"verbose expandable\"><span class=\"error2\"><--- %s <span class=\"marker\">[+]</span></span><div class=\"content\">%s</div></div>\n""" 279HTML_EXPANDABLE_INCONCLUSIVE = "<div class=\"verbose expandable\"><span class=\"inconclusive2\"><--- %s <span class=\"marker\">[+]</span></span><div class=\"content\">%s</div></div>\n""" 280 281# escape() and unescape() takes care of &, < and >. 282html_escape_table = { 283 '"': """, 284 "'": "'" 285} 286html_unescape_table = {v: k for k, v in html_escape_table.items()} 287 288 289def html_escape(text): 290 return escape(text, html_escape_table) 291 292 293def git_blame(line, path, file, blame_options): 294 git_blame_dict = {} 295 head, tail = os.path.split(file) 296 if head != "": 297 path = head 298 299 try: 300 os.chdir(path) 301 except: 302 return {} 303 304 try: 305 result = subprocess.check_output('git blame -L %d %s %s --porcelain -- %s' % ( 306 line, " -w" if "-w" in blame_options else "", " -M" if "-M" in blame_options else "", file)) 307 result = result.decode(locale.getpreferredencoding()) 308 except: 309 return {} 310 311 if result.startswith('fatal'): 312 return {} 313 314 disallowed_characters = '<>' 315 for line in result.split('\n')[1:]: 316 space_pos = line.find(' ') 317 if space_pos > 30: 318 break 319 key = line[:space_pos] 320 val = line[space_pos + 1:] 321 322 for character in disallowed_characters: 323 val = val.replace(character, "") 324 git_blame_dict[key] = val 325 326 datetime_object = datetime.date.fromtimestamp(float(git_blame_dict['author-time'])) 327 year = datetime_object.strftime("%Y") 328 month = datetime_object.strftime("%m") 329 day = datetime_object.strftime("%d") 330 331 git_blame_dict['author-time'] = '%s/%s/%s' % (day, month, year) 332 333 return git_blame_dict 334 335 336def tr_str(td_th, line, id, cwe, severity, message, author, author_mail, date, add_author, tr_class=None, htmlfile=None, message_class=None): 337 ret = '' 338 if htmlfile: 339 ret += '<%s><a href="%s#line-%d">%d</a></%s>' % (td_th, htmlfile, line, line, td_th) 340 for item in (id, cwe, severity): 341 ret += '<%s>%s</%s>' % (td_th, item, td_th) 342 else: 343 for item in (line, id, cwe, severity): 344 ret += '<%s>%s</%s>' % (td_th, item, td_th) 345 if message_class: 346 message_attribute = ' class="%s"' % message_class 347 else: 348 message_attribute = '' 349 ret += '<%s%s>%s</%s>' % (td_th, message_attribute, html_escape(message), td_th) 350 351 if add_author: 352 for item in (author, author_mail, date): 353 ret += '<%s>%s</%s>' % (td_th, item, td_th) 354 if tr_class: 355 tr_attributes = ' class="%s"' % tr_class 356 else: 357 tr_attributes = '' 358 return '<tr%s>%s</tr>' % (tr_attributes, ret) 359 360 361class AnnotateCodeFormatter(HtmlFormatter): 362 errors = [] 363 364 def wrap(self, source, outfile): 365 line_no = 1 366 for i, t in HtmlFormatter.wrap(self, source, outfile): 367 # If this is a source code line we want to add a span tag at the 368 # end. 369 if i == 1: 370 for error in self.errors: 371 if error['line'] == line_no: 372 try: 373 if error['inconclusive'] == 'true': 374 # only print verbose msg if it really differs 375 # from actual message 376 if error.get('verbose') and (error['verbose'] != error['msg']): 377 index = t.rfind('\n') 378 t = t[:index] + HTML_EXPANDABLE_INCONCLUSIVE % (error['msg'], html_escape(error['verbose'].replace("\\012", '\n'))) + t[index + 1:] 379 else: 380 t = t.replace('\n', HTML_INCONCLUSIVE % error['msg']) 381 except KeyError: 382 if error.get('verbose') and (error['verbose'] != error['msg']): 383 index = t.rfind('\n') 384 t = t[:index] + HTML_EXPANDABLE_ERROR % (error['msg'], html_escape(error['verbose'].replace("\\012", '\n'))) + t[index + 1:] 385 else: 386 t = t.replace('\n', HTML_ERROR % error['msg']) 387 388 line_no = line_no + 1 389 yield i, t 390 391 392class CppCheckHandler(XmlContentHandler): 393 394 """Parses the cppcheck xml file and produces a list of all its errors.""" 395 396 def __init__(self): 397 XmlContentHandler.__init__(self) 398 self.errors = [] 399 self.version = '1' 400 self.versionCppcheck = '' 401 402 def startElement(self, name, attributes): 403 if name == 'results': 404 self.version = attributes.get('version', self.version) 405 406 if self.version == '1': 407 self.handleVersion1(name, attributes) 408 else: 409 self.handleVersion2(name, attributes) 410 411 def handleVersion1(self, name, attributes): 412 if name != 'error': 413 return 414 415 self.errors.append({ 416 'file': attributes.get('file', ''), 417 'line': int(attributes.get('line', 0)), 418 'locations': [{ 419 'file': attributes.get('file', ''), 420 'line': int(attributes.get('line', 0)), 421 }], 422 'id': attributes['id'], 423 'severity': attributes['severity'], 424 'msg': attributes['msg'] 425 }) 426 427 def handleVersion2(self, name, attributes): 428 if name == 'cppcheck': 429 self.versionCppcheck = attributes['version'] 430 if name == 'error': 431 error = { 432 'locations': [], 433 'file': '', 434 'line': 0, 435 'id': attributes['id'], 436 'severity': attributes['severity'], 437 'msg': attributes['msg'], 438 'verbose': attributes.get('verbose') 439 } 440 441 if 'inconclusive' in attributes: 442 error['inconclusive'] = attributes['inconclusive'] 443 if 'cwe' in attributes: 444 error['cwe'] = attributes['cwe'] 445 446 self.errors.append(error) 447 elif name == 'location': 448 assert self.errors 449 error = self.errors[-1] 450 locations = error['locations'] 451 file = attributes['file'] 452 line = int(attributes['line']) 453 if not locations: 454 error['file'] = file 455 error['line'] = line 456 locations.append({ 457 'file': file, 458 'line': line, 459 'info': attributes.get('info') 460 }) 461 462if __name__ == '__main__': 463 # Configure all the options this little utility is using. 464 parser = optparse.OptionParser() 465 parser.add_option('--title', dest='title', 466 help='The title of the project.', 467 default='[project name]') 468 parser.add_option('--file', dest='file', action="append", 469 help='The cppcheck xml output file to read defects ' 470 'from. You can combine results from several ' 471 'xml reports i.e. "--file file1.xml --file file2.xml ..". ' 472 'Default is reading from stdin.') 473 parser.add_option('--report-dir', dest='report_dir', 474 help='The directory where the HTML report content is ' 475 'written.') 476 parser.add_option('--source-dir', dest='source_dir', 477 help='Base directory where source code files can be ' 478 'found.') 479 parser.add_option('--add-author-information', dest='add_author_information', 480 help='Initially set to false' 481 'Adds author, author-mail and time to htmlreport') 482 parser.add_option('--source-encoding', dest='source_encoding', 483 help='Encoding of source code.', default='utf-8') 484 parser.add_option('--blame-options', dest='blame_options', 485 help='[-w, -M] blame options which you can use to get author and author mail ' 486 '-w --> not including white spaces and returns original author of the line ' 487 '-M --> not including moving of lines and returns original author of the line') 488 489 # Parse options and make sure that we have an output directory set. 490 options, args = parser.parse_args() 491 492 try: 493 sys.argv[1] 494 except IndexError: # no arguments give, print --help 495 parser.print_help() 496 quit() 497 498 if not options.report_dir: 499 parser.error('No report directory set.') 500 501 # Get the directory where source code files are located. 502 cwd = os.getcwd() 503 source_dir = os.getcwd() 504 if options.source_dir: 505 source_dir = options.source_dir 506 507 add_author_information = False 508 if options.add_author_information: 509 add_author_information = True 510 511 blame_options = '' 512 if options.blame_options: 513 blame_options = options.blame_options 514 add_author_information = True 515 # Parse the xml from all files defined in file argument 516 # or from stdin. If no input is provided, stdin is used 517 # Produce a simple list of errors. 518 print('Parsing xml report.') 519 try: 520 contentHandler = CppCheckHandler() 521 for fname in options.file or [sys.stdin]: 522 xml_parse(fname, contentHandler) 523 except (XmlParseException, ValueError) as msg: 524 print('Failed to parse cppcheck xml file: %s' % msg) 525 sys.exit(1) 526 527 # We have a list of errors. But now we want to group them on 528 # each source code file. Lets create a files dictionary that 529 # will contain a list of all the errors in that file. For each 530 # file we will also generate a HTML filename to use. 531 files = {} 532 file_no = 0 533 for error in contentHandler.errors: 534 filename = error['file'] 535 if filename not in files.keys(): 536 files[filename] = { 537 'errors': [], 'htmlfile': str(file_no) + '.html'} 538 file_no = file_no + 1 539 files[filename]['errors'].append(error) 540 541 # Make sure that the report directory is created if it doesn't exist. 542 print('Creating %s directory' % options.report_dir) 543 if not os.path.exists(options.report_dir): 544 os.makedirs(options.report_dir) 545 546 # Generate a HTML file with syntax highlighted source code for each 547 # file that contains one or more errors. 548 print('Processing errors') 549 550 decode_errors = [] 551 for filename, data in sorted(files.items()): 552 htmlfile = data['htmlfile'] 553 errors = [] 554 555 for error in data['errors']: 556 for location in error['locations']: 557 if filename == location['file']: 558 newError = dict(error) 559 560 del newError['locations'] 561 newError['line'] = location['line'] 562 if location.get('info'): 563 newError['msg'] = location['info'] 564 newError['severity'] = 'information' 565 del newError['verbose'] 566 567 errors.append(newError) 568 569 lines = [] 570 for error in errors: 571 lines.append(error['line']) 572 573 if filename == '': 574 continue 575 576 source_filename = os.path.join(source_dir, filename) 577 try: 578 with io.open(source_filename, 'r', encoding=options.source_encoding) as input_file: 579 content = input_file.read() 580 except IOError: 581 if error['id'] == 'unmatchedSuppression': 582 continue # file not found, bail out 583 else: 584 sys.stderr.write("ERROR: Source file '%s' not found.\n" % 585 source_filename) 586 continue 587 except UnicodeDecodeError: 588 sys.stderr.write("WARNING: Unicode decode error in '%s'.\n" % 589 source_filename) 590 decode_errors.append(source_filename[2:]) # "[2:]" gets rid of "./" at beginning 591 continue 592 593 htmlFormatter = AnnotateCodeFormatter(linenos=True, 594 style='colorful', 595 hl_lines=lines, 596 lineanchors='line', 597 encoding=options.source_encoding) 598 htmlFormatter.errors = errors 599 600 with io.open(os.path.join(options.report_dir, htmlfile), 'w', encoding='utf-8') as output_file: 601 output_file.write(HTML_HEAD % 602 (options.title, 603 htmlFormatter.get_style_defs('.highlight'), 604 options.title, 605 filename, 606 filename.split('/')[-1])) 607 608 for error in sorted(errors, key=lambda k: k['line']): 609 output_file.write("<a href=\"%s#line-%d\"> %s %s</a>" % (data['htmlfile'], error['line'], error['id'], error['line'])) 610 611 output_file.write(HTML_HEAD_END) 612 try: 613 lexer = guess_lexer_for_filename(source_filename, '', stripnl=False) 614 except ClassNotFound: 615 try: 616 lexer = guess_lexer(content, stripnl=False) 617 except ClassNotFound: 618 sys.stderr.write("ERROR: Couldn't determine lexer for the file' " + source_filename + " '. Won't be able to syntax highlight this file.") 619 output_file.write("\n <tr><td colspan=\"5\"> Could not generate content because pygments failed to determine the code type.</td></tr>") 620 output_file.write("\n <tr><td colspan=\"5\"> Sorry about this.</td></tr>") 621 continue 622 623 if options.source_encoding: 624 lexer.encoding = options.source_encoding 625 626 output_file.write( 627 highlight(content, lexer, htmlFormatter).decode( 628 options.source_encoding)) 629 630 output_file.write(HTML_FOOTER % contentHandler.versionCppcheck) 631 632 print(' ' + filename) 633 634 # Generate a master index.html file that will contain a list of 635 # all the errors created. 636 print('Creating index.html') 637 638 with io.open(os.path.join(options.report_dir, 'index.html'), 639 'w') as output_file: 640 641 stats_count = 0 642 stats = [] 643 for filename, data in sorted(files.items()): 644 for error in data['errors']: 645 stats.append(error['id']) # get the stats 646 stats_count += 1 647 648 counter = Counter(stats) 649 650 stat_html = [] 651 # the following lines sort the stat primary by value (occurrences), 652 # but if two IDs occur equally often, then we sort them alphabetically by warning ID 653 try: 654 cnt_max = counter.most_common()[0][1] 655 except IndexError: 656 cnt_max = 0 657 658 try: 659 cnt_min = counter.most_common()[-1][1] 660 except IndexError: 661 cnt_min = 0 662 663 stat_fmt = "\n <tr><td><input type=\"checkbox\" onclick=\"toggleDisplay(this.id)\" id=\"{}\" name=\"{}\" checked></td><td>{}</td><td>{}</td></tr>" 664 for occurrences in reversed(range(cnt_min, cnt_max + 1)): 665 for _id in [k for k, v in sorted(counter.items()) if v == occurrences]: 666 stat_html.append(stat_fmt.format(_id, _id, dict(counter.most_common())[_id], _id)) 667 668 output_file.write(HTML_HEAD.replace('id="menu"', 'id="menu_index"', 1).replace("Defects:", "Defect summary;", 1) % (options.title, '', options.title, '', '')) 669 output_file.write('\n <label><input type="checkbox" onclick="toggleAll()" checked> Toggle all</label>') 670 output_file.write('\n <table>') 671 output_file.write('\n <tr><th>Show</th><th>#</th><th>Defect ID</th></tr>') 672 output_file.write(''.join(stat_html)) 673 output_file.write('\n <tr><td></td><td>' + str(stats_count) + '</td><td>total</td></tr>') 674 output_file.write('\n </table>') 675 output_file.write('\n <p><a href="stats.html">Statistics</a></p>') 676 output_file.write(HTML_HEAD_END.replace("content", "content_index", 1)) 677 678 output_file.write('\n <table>') 679 output_file.write( 680 '\n %s' % 681 tr_str('th', 'Line', 'Id', 'CWE', 'Severity', 'Message', 'Author', 'Author mail', 'Date (DD/MM/YYYY)', add_author=add_author_information)) 682 683 for filename, data in sorted(files.items()): 684 if filename in decode_errors: # don't print a link but a note 685 output_file.write("\n <tr><td colspan=\"5\">%s</td></tr>" % filename) 686 output_file.write("\n <tr><td colspan=\"5\"> Could not generated due to UnicodeDecodeError</td></tr>") 687 else: 688 if filename.endswith('*'): # assume unmatched suppression 689 output_file.write( 690 "\n <tr><td colspan=\"5\">%s</td></tr>" % 691 filename) 692 else: 693 output_file.write( 694 "\n <tr><td colspan=\"5\"><a href=\"%s\">%s</a></td></tr>" % 695 (data['htmlfile'], filename)) 696 697 for error in sorted(data['errors'], key=lambda k: k['line']): 698 if add_author_information: 699 git_blame_dict = git_blame(error['line'], source_dir, error['file'], blame_options) 700 else: 701 git_blame_dict = {} 702 message_class = None 703 try: 704 if error['inconclusive'] == 'true': 705 message_class = 'inconclusive' 706 error['severity'] += ", inconcl." 707 except KeyError: 708 pass 709 710 try: 711 if error['cwe']: 712 cwe_url = "<a href=\"https://cwe.mitre.org/data/definitions/" + error['cwe'] + ".html\">" + error['cwe'] + "</a>" 713 except KeyError: 714 cwe_url = "" 715 716 if error['severity'] == 'error': 717 message_class = 'error' 718 719 is_file = filename != '' and not filename.endswith('*') 720 line = error["line"] if is_file else "" 721 htmlfile = data.get('htmlfile') if is_file else None 722 723 output_file.write( 724 '\n %s' % 725 tr_str('td', line, error["id"], cwe_url, error["severity"], error["msg"], 726 git_blame_dict.get('author', 'Unknown'), git_blame_dict.get('author-mail', '---'), 727 git_blame_dict.get('author-time', '---'), 728 tr_class=error["id"], 729 message_class=message_class, 730 add_author=add_author_information, 731 htmlfile=htmlfile)) 732 output_file.write('\n </table>') 733 output_file.write(HTML_FOOTER % contentHandler.versionCppcheck) 734 735 if decode_errors: 736 sys.stderr.write("\nGenerating html failed for the following files: " + ' '.join(decode_errors)) 737 sys.stderr.write("\nConsider changing source-encoding (for example: \"htmlreport ... --source-encoding=\"iso8859-1\"\"\n") 738 739 print('Creating style.css file') 740 os.chdir(cwd) # going back to the cwd to find style.css 741 with io.open(os.path.join(options.report_dir, 'style.css'), 'w') as css_file: 742 css_file.write(STYLE_FILE) 743 744 print("Creating stats.html (statistics)\n") 745 stats_countlist = {} 746 747 for filename, data in sorted(files.items()): 748 if filename == '': 749 continue 750 stats_tmplist = [] 751 for error in sorted(data['errors'], key=lambda k: k['line']): 752 stats_tmplist.append(error['severity']) 753 754 stats_countlist[filename] = dict(Counter(stats_tmplist)) 755 756 # get top ten for each severity 757 SEVERITIES = "error", "warning", "portability", "performance", "style", "unusedFunction", "information", "missingInclude", "internal" 758 759 with io.open(os.path.join(options.report_dir, 'stats.html'), 'w') as stats_file: 760 761 stats_file.write(HTML_HEAD.replace('id="menu"', 'id="menu_index"', 1).replace("Defects:", "Back to summary", 1) % (options.title, '', options.title, 'Statistics', '')) 762 stats_file.write(HTML_HEAD_END.replace("content", "content_index", 1)) 763 764 for sev in SEVERITIES: 765 _sum = 0 766 stats_templist = {} 767 768 # if the we have an style warning but we are checking for 769 # portability, we have to skip it to prevent KeyError 770 try: 771 for filename in stats_countlist: 772 try: # also bail out if we have a file with no sev-results 773 _sum += stats_countlist[filename][sev] 774 stats_templist[filename] = int(stats_countlist[filename][sev]) # file : amount, 775 except KeyError: 776 continue 777 # don't print "0 style" etc, if no style warnings were found 778 if _sum == 0: 779 continue 780 except KeyError: 781 continue 782 stats_file.write("<p>Top 10 files for " + sev + " severity, total findings: " + str(_sum) + "<br>\n") 783 784 # sort, so that the file with the most severities per type is first 785 stats_list_sorted = sorted(stats_templist.items(), key=operator.itemgetter(1, 0), reverse=True) 786 it = 0 787 LENGTH = 0 788 789 for i in stats_list_sorted: # printing loop 790 # for aesthetics: if it's the first iteration of the loop, get 791 # the max length of the number string 792 if it == 0: 793 LENGTH = len(str(i[1])) # <- length of longest number, now get the difference and try to make other numbers align to it 794 795 stats_file.write(" " * 3 + str(i[1]) + " " * (1 + LENGTH - len(str(i[1]))) + "<a href=\"" + files[i[0]]['htmlfile'] + "\"> " + i[0] + "</a><br>\n") 796 it += 1 797 if it == 10: # print only the top 10 798 break 799 stats_file.write("</p>\n") 800 801 stats_file.write(HTML_FOOTER % contentHandler.versionCppcheck) 802 803 print("\nOpen '" + options.report_dir + "/index.html' to see the results.") 804