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