| #! /usr/bin/python3 |
| |
| from __future__ import unicode_literals |
| |
| import io |
| import sys |
| import optparse |
| import os |
| import operator |
| |
| from collections import Counter |
| from pygments import highlight |
| from pygments.lexers import guess_lexer_for_filename |
| from pygments.formatters import HtmlFormatter |
| from xml.sax import parse as xml_parse |
| from xml.sax import SAXParseException as XmlParseException |
| from xml.sax.handler import ContentHandler as XmlContentHandler |
| from xml.sax.saxutils import escape |
| """ |
| Turns a cppcheck xml file into a browsable html report along |
| with syntax highlighted source code. |
| """ |
| |
| STYLE_FILE = """ |
| body { |
| font: 13px Arial, Verdana, Sans-Serif; |
| margin: 0; |
| width: auto; |
| } |
| |
| h1 { |
| margin: 10px; |
| } |
| |
| #footer > p { |
| margin: 4px; |
| } |
| |
| .error { |
| background-color: #ffb7b7; |
| } |
| |
| .error2 { |
| background-color: #faa; |
| border: 1px dotted black; |
| display: inline-block; |
| margin-left: 4px; |
| } |
| |
| .inconclusive { |
| background-color: #B6B6B4; |
| } |
| |
| .inconclusive2 { |
| background-color: #B6B6B4; |
| border: 1px dotted black; |
| display: inline-block; |
| margin-left: 4px; |
| } |
| |
| div.verbose { |
| display: inline-block; |
| vertical-align: top; |
| cursor: help; |
| } |
| |
| div.verbose div.content { |
| display: none; |
| position: absolute; |
| padding: 10px; |
| margin: 4px; |
| max-width: 40%; |
| white-space: pre-wrap; |
| border: 1px solid black; |
| background-color: #FFFFCC; |
| cursor: auto; |
| } |
| |
| .highlight .hll { |
| padding: 1px; |
| } |
| |
| #header { |
| border-bottom: thin solid #aaa; |
| } |
| |
| #menu { |
| float: left; |
| margin-top: 5px; |
| text-align: left; |
| width: 150px; |
| height: 75%; |
| position: fixed; |
| overflow: auto; |
| z-index: 1; |
| } |
| |
| #menu_index { |
| float: left; |
| margin-top: 5px; |
| padding-left: 5px; |
| text-align: left; |
| width: 200px; |
| height: 75%; |
| position: fixed; |
| overflow: auto; |
| z-index: 1; |
| } |
| |
| #menu > a { |
| display: block; |
| margin-left: 10px; |
| font: 12px; |
| z-index: 1; |
| } |
| |
| #filename { |
| margin-left: 10px; |
| font: 12px; |
| z-index: 1; |
| } |
| |
| .highlighttable { |
| background-color:white; |
| z-index: 10; |
| position: relative; |
| margin: -10 px; |
| } |
| |
| #content { |
| background-color: white; |
| -webkit-box-sizing: content-box; |
| -moz-box-sizing: content-box; |
| box-sizing: content-box; |
| float: left; |
| margin: 5px; |
| margin-left: 10px; |
| padding: 0 10px 10px 10px; |
| width: 80%; |
| padding-left: 150px; |
| } |
| |
| #content_index { |
| background-color: white; |
| -webkit-box-sizing: content-box; |
| -moz-box-sizing: content-box; |
| box-sizing: content-box; |
| float: left; |
| margin: 5px; |
| margin-left: 10px; |
| padding: 0 10px 10px 10px; |
| width: 80%; |
| padding-left: 200px; |
| } |
| |
| .linenos { |
| border-right: thin solid #aaa; |
| color: lightgray; |
| padding-right: 6px; |
| } |
| |
| #footer { |
| border-top: thin solid #aaa; |
| clear: both; |
| font-size: 90%; |
| margin-top: 5px; |
| } |
| |
| #footer ul { |
| list-style-type: none; |
| padding-left: 0; |
| } |
| """ |
| |
| HTML_HEAD = """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <title>Cppcheck - HTML report - %s</title> |
| <link rel="stylesheet" href="style.css"> |
| <style> |
| %s |
| </style> |
| <script language="javascript"> |
| function getStyle(el,styleProp) { |
| if (el.currentStyle) |
| var y = el.currentStyle[styleProp]; |
| else if (window.getComputedStyle) |
| var y = document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp); |
| return y; |
| } |
| function toggle() { |
| var el = this.expandable_content; |
| var mark = this.expandable_marker; |
| if (el.style.display == "block") { |
| el.style.display = "none"; |
| mark.innerHTML = "[+]"; |
| } else { |
| el.style.display = "block"; |
| mark.innerHTML = "[-]"; |
| } |
| } |
| function init_expandables() { |
| var elts = document.getElementsByClassName("expandable"); |
| for (var i = 0; i < elts.length; i++) { |
| var el = elts[i]; |
| var clickable = el.getElementsByTagName("span")[0]; |
| var marker = clickable.getElementsByClassName("marker")[0]; |
| var content = el.getElementsByClassName("content")[0]; |
| var width = clickable.clientWidth - parseInt(getStyle(content, "padding-left")) - parseInt(getStyle(content, "padding-right")); |
| content.style.width = width + "px"; |
| clickable.expandable_content = content; |
| clickable.expandable_marker = marker; |
| clickable.onclick = toggle; |
| } |
| } |
| function set_class_display(c, st) { |
| var elements = document.querySelectorAll('.' + c), |
| len = elements.length; |
| for (i = 0; i < len; i++) { |
| elements[i].style.display = st; |
| } |
| } |
| function toggle_class_visibility(id) { |
| var box = document.getElementById(id); |
| set_class_display(id, box.checked ? '' : 'none'); |
| } |
| </script> |
| </head> |
| <body onload="init_expandables()"> |
| <div id="header"> |
| <h1>Cppcheck report - %s: %s </h1> |
| </div> |
| <div id="menu" dir="rtl"> |
| <p id="filename"><a href="index.html">Defects:</a> %s</p> |
| """ |
| |
| HTML_HEAD_END = """ |
| </div> |
| <div id="content"> |
| """ |
| |
| HTML_FOOTER = """ |
| </div> |
| <div id="footer"> |
| <p> |
| Cppcheck %s - a tool for static C/C++ code analysis</br> |
| </br> |
| Internet: <a href="http://cppcheck.net">http://cppcheck.net</a></br> |
| IRC: <a href="irc://irc.freenode.net/cppcheck">irc://irc.freenode.net/cppcheck</a></br> |
| <p> |
| </div> |
| </body> |
| </html> |
| """ |
| |
| HTML_ERROR = "<span class='error2'><--- %s</span>\n" |
| HTML_INCONCLUSIVE = "<span class='inconclusive2'><--- %s</span>\n" |
| |
| HTML_EXPANDABLE_ERROR = "<div class='verbose expandable'><span class='error2'><--- %s <span class='marker'>[+]</span></span><div class='content'>%s</div></div>\n""" |
| HTML_EXPANDABLE_INCONCLUSIVE = "<div class='verbose expandable'><span class='inconclusive2'><--- %s <span class='marker'>[+]</span></span><div class='content'>%s</div></div>\n""" |
| |
| # escape() and unescape() takes care of &, < and >. |
| html_escape_table = { |
| '"': """, |
| "'": "'" |
| } |
| html_unescape_table = {v: k for k, v in html_escape_table.items()} |
| |
| |
| def html_escape(text): |
| return escape(text, html_escape_table) |
| |
| |
| class AnnotateCodeFormatter(HtmlFormatter): |
| errors = [] |
| |
| def wrap(self, source, outfile): |
| line_no = 1 |
| for i, t in HtmlFormatter.wrap(self, source, outfile): |
| # If this is a source code line we want to add a span tag at the |
| # end. |
| if i == 1: |
| for error in self.errors: |
| if error['line'] == line_no: |
| try: |
| if error['inconclusive'] == 'true': |
| # only print verbose msg if it really differs |
| # from actual message |
| if error.get('verbose') and (error['verbose'] != error['msg']): |
| index = t.rfind('\n') |
| t = t[:index] + HTML_EXPANDABLE_INCONCLUSIVE % (error['msg'], html_escape(error['verbose'].replace("\\012", '\n'))) + t[index + 1:] |
| else: |
| t = t.replace('\n', HTML_INCONCLUSIVE % error['msg']) |
| except KeyError: |
| if error.get('verbose') and (error['verbose'] != error['msg']): |
| index = t.rfind('\n') |
| t = t[:index] + HTML_EXPANDABLE_ERROR % (error['msg'], html_escape(error['verbose'].replace("\\012", '\n'))) + t[index + 1:] |
| else: |
| t = t.replace('\n', HTML_ERROR % error['msg']) |
| |
| line_no = line_no + 1 |
| yield i, t |
| |
| |
| class CppCheckHandler(XmlContentHandler): |
| |
| """Parses the cppcheck xml file and produces a list of all its errors.""" |
| |
| def __init__(self): |
| XmlContentHandler.__init__(self) |
| self.errors = [] |
| self.version = '1' |
| self.versionCppcheck = '' |
| |
| def startElement(self, name, attributes): |
| if name == 'results': |
| self.version = attributes.get('version', self.version) |
| |
| if self.version == '1': |
| self.handleVersion1(name, attributes) |
| else: |
| self.handleVersion2(name, attributes) |
| |
| def handleVersion1(self, name, attributes): |
| if name != 'error': |
| return |
| |
| self.errors.append({ |
| 'file': attributes.get('file', ''), |
| 'line': int(attributes.get('line', 0)), |
| 'locations': [{ |
| 'file': attributes.get('file', ''), |
| 'line': int(attributes.get('line', 0)), |
| }], |
| 'id': attributes['id'], |
| 'severity': attributes['severity'], |
| 'msg': attributes['msg'] |
| }) |
| |
| def handleVersion2(self, name, attributes): |
| if name == 'cppcheck': |
| self.versionCppcheck = attributes['version'] |
| if name == 'error': |
| error = { |
| 'locations': [], |
| 'file': '', |
| 'line': 0, |
| 'id': attributes['id'], |
| 'severity': attributes['severity'], |
| 'msg': attributes['msg'], |
| 'verbose': attributes.get('verbose') |
| } |
| |
| if 'inconclusive' in attributes: |
| error['inconclusive'] = attributes['inconclusive'] |
| if 'cwe' in attributes: |
| error['cwe'] = attributes['cwe'] |
| |
| self.errors.append(error) |
| elif name == 'location': |
| assert self.errors |
| error = self.errors[-1] |
| locations = error['locations'] |
| file = attributes['file'] |
| line = int(attributes['line']) |
| if not locations: |
| error['file'] = file |
| error['line'] = line |
| locations.append({ |
| 'file': file, |
| 'line': line, |
| 'info': attributes.get('info') |
| }) |
| |
| if __name__ == '__main__': |
| # Configure all the options this little utility is using. |
| parser = optparse.OptionParser() |
| parser.add_option('--title', dest='title', |
| help='The title of the project.', |
| default='[project name]') |
| parser.add_option('--file', dest='file', |
| help='The cppcheck xml output file to read defects ' |
| 'from. Default is reading from stdin.') |
| parser.add_option('--report-dir', dest='report_dir', |
| help='The directory where the HTML report content is ' |
| 'written.') |
| parser.add_option('--source-dir', dest='source_dir', |
| help='Base directory where source code files can be ' |
| 'found.') |
| parser.add_option('--source-encoding', dest='source_encoding', |
| help='Encoding of source code.', default='utf-8') |
| |
| # Parse options and make sure that we have an output directory set. |
| options, args = parser.parse_args() |
| |
| try: |
| sys.argv[1] |
| except IndexError: # no arguments give, print --help |
| parser.print_help() |
| quit() |
| |
| if not options.report_dir: |
| parser.error('No report directory set.') |
| |
| # Get the directory where source code files are located. |
| source_dir = os.getcwd() |
| if options.source_dir: |
| source_dir = options.source_dir |
| |
| # Get the stream that we read cppcheck errors from. |
| input_file = sys.stdin |
| if options.file: |
| if not os.path.exists(options.file): |
| parser.error('cppcheck xml file: %s not found.' % options.file) |
| input_file = io.open(options.file, 'r') |
| else: |
| parser.error('No cppcheck xml file specified. (--file=)') |
| |
| # Parse the xml file and produce a simple list of errors. |
| print('Parsing xml report.') |
| try: |
| contentHandler = CppCheckHandler() |
| xml_parse(input_file, contentHandler) |
| except XmlParseException as msg: |
| print('Failed to parse cppcheck xml file: %s' % msg) |
| sys.exit(1) |
| |
| # We have a list of errors. But now we want to group them on |
| # each source code file. Lets create a files dictionary that |
| # will contain a list of all the errors in that file. For each |
| # file we will also generate a HTML filename to use. |
| files = {} |
| file_no = 0 |
| for error in contentHandler.errors: |
| filename = error['file'] |
| if filename not in files.keys(): |
| files[filename] = { |
| 'errors': [], 'htmlfile': str(file_no) + '.html'} |
| file_no = file_no + 1 |
| files[filename]['errors'].append(error) |
| |
| # Make sure that the report directory is created if it doesn't exist. |
| print('Creating %s directory' % options.report_dir) |
| if not os.path.exists(options.report_dir): |
| os.mkdir(options.report_dir) |
| |
| # Generate a HTML file with syntax highlighted source code for each |
| # file that contains one or more errors. |
| print('Processing errors') |
| |
| decode_errors = [] |
| for filename, data in sorted(files.items()): |
| htmlfile = data['htmlfile'] |
| errors = [] |
| |
| for error in data['errors']: |
| for location in error['locations']: |
| if filename == location['file']: |
| newError = dict(error) |
| |
| del newError['locations'] |
| newError['line'] = location['line'] |
| if location.get('info'): |
| newError['msg'] = location['info'] |
| newError['severity'] = 'information' |
| del newError['verbose'] |
| |
| errors.append(newError) |
| |
| lines = [] |
| for error in errors: |
| lines.append(error['line']) |
| |
| if filename == '': |
| continue |
| |
| source_filename = os.path.join(source_dir, filename) |
| try: |
| with io.open(source_filename, 'r', encoding=options.source_encoding) as input_file: |
| content = input_file.read() |
| except IOError: |
| if (error['id'] == 'unmatchedSuppression'): |
| continue # file not found, bail out |
| else: |
| sys.stderr.write("ERROR: Source file '%s' not found.\n" % |
| source_filename) |
| continue |
| except UnicodeDecodeError: |
| sys.stderr.write("WARNING: Unicode decode error in '%s'.\n" % |
| source_filename) |
| decode_errors.append(source_filename[2:]) # "[2:]" gets rid of "./" at beginning |
| continue |
| |
| htmlFormatter = AnnotateCodeFormatter(linenos=True, |
| style='colorful', |
| hl_lines=lines, |
| lineanchors='line', |
| encoding=options.source_encoding) |
| htmlFormatter.errors = errors |
| |
| with io.open(os.path.join(options.report_dir, htmlfile), 'w', encoding='utf-8') as output_file: |
| output_file.write(HTML_HEAD % |
| (options.title, |
| htmlFormatter.get_style_defs('.highlight'), |
| options.title, |
| filename, |
| filename.split('/')[-1])) |
| |
| for error in sorted(errors, key=lambda k: k['line']): |
| output_file.write("<a href='%s#line-%d'> %s %s</a>" % (data['htmlfile'], error['line'], error['id'], error['line'])) |
| |
| output_file.write(HTML_HEAD_END) |
| try: |
| lexer = guess_lexer_for_filename(source_filename, '') |
| except: |
| sys.stderr.write("ERROR: Couldn't determine lexer for the file' " + source_filename + " '. Won't be able to syntax highlight this file.") |
| output_file.write("\n <tr><td colspan='4'> Could not generated content because pygments failed to retrieve the determine code type.</td></tr>") |
| output_file.write("\n <tr><td colspan='4'> Sorry about this.</td></tr>") |
| continue |
| |
| if options.source_encoding: |
| lexer.encoding = options.source_encoding |
| |
| output_file.write( |
| highlight(content, lexer, htmlFormatter).decode( |
| options.source_encoding)) |
| |
| output_file.write(HTML_FOOTER % contentHandler.versionCppcheck) |
| |
| print(' ' + filename) |
| |
| # Generate a master index.html file that will contain a list of |
| # all the errors created. |
| print('Creating index.html') |
| |
| with io.open(os.path.join(options.report_dir, 'index.html'), |
| 'w') as output_file: |
| |
| stats_count = 0 |
| stats = [] |
| for filename, data in sorted(files.items()): |
| for error in data['errors']: |
| stats.append(error['id']) # get the stats |
| stats_count += 1 |
| |
| counter = Counter(stats) |
| |
| stat_html = [] |
| # the following lines sort the stat primary by value (occurrences), |
| # but if two IDs occur equally often, then we sort them alphabetically by warning ID |
| try: |
| cnt_max = counter.most_common()[0][1] |
| except IndexError: |
| cnt_max = 0 |
| |
| try: |
| cnt_min = counter.most_common()[-1][1] |
| except IndexError: |
| cnt_min = 0 |
| |
| stat_fmt = " <tr><td><input type='checkbox' onclick='toggle_class_visibility(this.id)' id='{}' name='{}' checked></td><td>{}</td><td>{}</td></tr>" |
| for occurrences in reversed(range(cnt_min, cnt_max + 1)): |
| for _id in [k for k, v in sorted(counter.items()) if v == occurrences]: |
| stat_html.append(stat_fmt.format(_id, _id, dict(counter.most_common())[_id], _id)) |
| |
| output_file.write(HTML_HEAD.replace('id="menu" dir="rtl"', 'id="menu_index"', 1).replace("Defects:", "Defect summary;", 1) % (options.title, '', options.title, '', '')) |
| output_file.write(' <table>') |
| output_file.write(' <tr><th>Show</th><th>#</th><th>Defect ID</th></tr>') |
| output_file.write(''.join(stat_html)) |
| output_file.write(' <tr><td></td><td>' + str(stats_count) + '</td><td>total</td></tr>') |
| output_file.write(' </table>') |
| output_file.write(' <a href="stats.html">Statistics</a></p>') |
| output_file.write(HTML_HEAD_END.replace("content", "content_index", 1)) |
| output_file.write(' <table>\n') |
| |
| output_file.write( |
| ' <tr><th>Line</th><th>Id</th><th>CWE</th><th>Severity</th><th>Message</th></tr>') |
| for filename, data in sorted(files.items()): |
| if filename in decode_errors: # don't print a link but a note |
| output_file.write("\n <tr><td colspan='4'>%s</td></tr>" % (filename)) |
| output_file.write("\n <tr><td colspan='4'> Could not generated due to UnicodeDecodeError</td></tr>") |
| else: |
| if filename.endswith('*'): # assume unmatched suppression |
| output_file.write( |
| "\n <tr><td colspan='4'>%s</td></tr>" % |
| (filename)) |
| else: |
| output_file.write( |
| "\n <tr><td colspan='4'><a href='%s'>%s</a></td></tr>" % |
| (data['htmlfile'], filename)) |
| |
| for error in sorted(data['errors'], key=lambda k: k['line']): |
| error_class = '' |
| try: |
| if error['inconclusive'] == 'true': |
| error_class = 'class="inconclusive"' |
| error['severity'] += ", inconcl." |
| except KeyError: |
| pass |
| |
| try: |
| if error['cwe']: |
| cwe_url = "<a href='https://cwe.mitre.org/data/definitions/" + error['cwe'] + ".html'>" + error['cwe'] + "</a>" |
| except KeyError: |
| cwe_url = "" |
| |
| if error['severity'] == 'error': |
| error_class = 'class="error"' |
| if error['id'] == 'missingInclude': |
| output_file.write( |
| '\n <tr class="%s"><td></td><td>%s</td><td></td><td>%s</td><td>%s</td></tr>' % |
| (error['id'], error['id'], error['severity'], error['msg'])) |
| elif (error['id'] == 'unmatchedSuppression') and filename.endswith('*'): |
| output_file.write( |
| '\n <tr class="%s"><td></td><td>%s</td><td></td><td>%s</td><td %s>%s</td></tr>' % |
| (error['id'], error['id'], error['severity'], error_class, |
| error['msg'])) |
| else: |
| output_file.write( |
| '\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>' % |
| (error['id'], data['htmlfile'], error['line'], error['line'], |
| error['id'], cwe_url, error['severity'], error_class, |
| error['msg'])) |
| |
| output_file.write('\n </table>') |
| output_file.write(HTML_FOOTER % contentHandler.versionCppcheck) |
| |
| if (decode_errors): |
| sys.stderr.write("\nGenerating html failed for the following files: " + ' '.join(decode_errors)) |
| sys.stderr.write("\nConsider changing source-encoding (for example: \"htmlreport ... --source-encoding=\"iso8859-1\"\"\n") |
| |
| print('Creating style.css file') |
| with io.open(os.path.join(options.report_dir, 'style.css'), |
| 'w') as css_file: |
| css_file.write(STYLE_FILE) |
| |
| print("Creating stats.html (statistics)\n") |
| stats_countlist = {} |
| |
| for filename, data in sorted(files.items()): |
| if (filename == ''): |
| continue |
| stats_tmplist = [] |
| for error in sorted(data['errors'], key=lambda k: k['line']): |
| stats_tmplist.append(error['severity']) |
| |
| stats_countlist[filename] = dict(Counter(stats_tmplist)) |
| |
| # get top ten for each severity |
| SEVERITIES = "error", "warning", "portability", "performance", "style", "unusedFunction", "information", "missingInclude", "internal" |
| |
| with io.open(os.path.join(options.report_dir, 'stats.html'), 'w') as stats_file: |
| |
| 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', '')) |
| stats_file.write(HTML_HEAD_END.replace("content", "content_index", 1)) |
| |
| for sev in SEVERITIES: |
| _sum = 0 |
| stats_templist = {} |
| |
| # if the we have an style warning but we are checking for |
| # portability, we have to skip it to prevent KeyError |
| try: |
| for filename in stats_countlist: |
| try: # also bail out if we have a file with no sev-results |
| _sum += stats_countlist[filename][sev] |
| stats_templist[filename] = (int)(stats_countlist[filename][sev]) # file : amount, |
| except KeyError: |
| continue |
| # don't print "0 style" etc, if no style warnings were found |
| if (_sum == 0): |
| break |
| except KeyError: |
| continue |
| stats_file.write("<p>Top 10 files for " + sev + " severity, total findings: " + str(_sum) + "</br>\n") |
| |
| # sort, so that the file with the most severities per type is first |
| stats_list_sorted = sorted(stats_templist.items(), key=operator.itemgetter(1, 0), reverse=True) |
| it = 0 |
| LENGTH = 0 |
| |
| for i in stats_list_sorted: # printing loop |
| # for aesthetics: if it's the first iteration of the loop, get |
| # the max length of the number string |
| if (it == 0): |
| LENGTH = len(str(i[1])) # <- length of longest number, now get the difference and try to make other numbers align to it |
| |
| stats_file.write(" " * 3 + str(i[1]) + " " * (1 + LENGTH - len(str(i[1]))) + "<a href=\"" + files[i[0]]['htmlfile'] + "\"> " + i[0] + "</a></br>\n") |
| it += 1 |
| if (it == 10): # print only the top 10 |
| break |
| stats_file.write("</p>\n") |
| |
| print("\nOpen '" + options.report_dir + "/index.html' to see the results.") |