#!/usr/bin/env python from __future__ import unicode_literals import io import sys import optparse import os 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 """ Turns a cppcheck xml file into a browsable html report along with syntax highlighted source code. """ STYLE_FILE = """ body { font: normal 13px/1 Arial, Verdana, Sans-Serif; background-color: black; padding: 0; margin: 0; } .error { font-size: 13px; background-color: #ffb7b7; padding: 0; margin: 0; } #page { width: auto; margin: 30px; border: 2px solid #aaa; background-color: white; padding: 20px; overflow: auto; } #header { width: 100%; height: 70px; border-bottom: thin solid #aaa; } #menu { margin-top: 5px; text-align: left; float: left; width: 100px; height: auto; } #menu > a { margin-left: 10px; display: block; } #content { float: left; width: 80%; margin: 5px; padding: 0 10px 10px 10px; border-left: thin solid #aaa; } .linenos { color: #bbb; padding-right: 6px; border-right: thin solid #aaa; } #footer { padding-bottom: 5px; padding-top: 5px; border-top: thin solid #aaa; clear: both; font-size: 90%; } """ HTML_HEAD = """ Cppcheck - HTML report - %s
""" HTML_FOOTER = """
""" HTML_ERROR = "<--- %s\n" 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: 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" 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)), "id": attributes["id"], "severity": attributes["severity"], "msg": attributes["msg"] }) def handleVersion2(self, name, attributes): if name == "error": self.errors.append({ "file": "", "line": 0, "id": attributes["id"], "severity": attributes["severity"], "msg": attributes["msg"] }) elif name == "location": assert self.errors self.errors[-1]["file"] = attributes["file"] self.errors[-1]["line"] = int(attributes["line"]) 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() 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. stream = sys.stdin if options.file: if not os.path.exists(options.file): parser.error("cppcheck xml file: %s not found." % options.file) stream = io.open(options.file, "r") # Parse the xml file and produce a simple list of errors. print("Parsing xml report.") try: contentHandler = CppCheckHandler() xml_parse(stream, 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") for filename, data in files.items(): htmlfile = data["htmlfile"] errors = data["errors"] lines = [] for error in errors: lines.append(error["line"]) if filename == "": continue source_filename = os.path.join(source_dir, filename) if not os.path.isfile(source_filename): sys.stderr.write("ERROR: Source file '%s' not found.\n" % source_filename) continue with io.open(source_filename, 'r') as input_file: content = input_file.read() 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') as output_file: output_file.write(HTML_HEAD % (options.title, htmlFormatter.get_style_defs(".highlight"), options.title)) lexer = guess_lexer_for_filename(source_filename, "") 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) print(" " + filename) # Generate a master index.html file that will contain a list of # all the errors created. print("Creating index.html") stream = io.open(os.path.join(options.report_dir, "index.html"), "w") stream.write(HTML_HEAD % (options.title, "", options.title)) stream.write("") stream.write("") for filename, data in files.items(): stream.write("" % (data["htmlfile"], filename)) for error in data["errors"]: if error['severity'] == 'error': error_class = 'class="error"' else: error_class = '' if error["id"] == "missingInclude": stream.write("" % (error["id"], error["severity"], error["msg"])) else: stream.write("" % (data["htmlfile"], error["line"], error["line"], error["id"], error["severity"], error_class, error["msg"])) stream.write("
LineIdSeverityMessage
%s
%s%s%s
%d%s%s%s
") stream.write(HTML_FOOTER) stream.close() print("Creating style.css file") stream = io.open(os.path.join(options.report_dir, "style.css"), "w") stream.write(STYLE_FILE) stream.close()