codekingpro/portable-devtools
114k
1#!/usr/bin/env python32 3from __future__ import unicode_literals4 5from datetime import date6import argparse7import io8import locale9import operator10import os11import re12import sys13import subprocess14import time15 16from collections import Counter17from pygments import highlight18from pygments.lexers import guess_lexer, guess_lexer_for_filename19from pygments.formatters import HtmlFormatter # pylint: disable=no-name-in-module20from pygments.util import ClassNotFound21from xml.sax import parse as xml_parse22from xml.sax import SAXParseException as XmlParseException23from xml.sax.handler import ContentHandler as XmlContentHandler24from xml.sax.saxutils import escape25"""26Turns a cppcheck xml file into a browsable html report along27with syntax highlighted source code.28"""29 30STYLE_FILE = """31body {32 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;33 font-size: 13px;34 line-height: 1.5;35 height: 100%;36 margin: 0;37}38 39#wrapper {40 position: fixed;41 height: 100vh;42 width: 100vw;43 display: grid;44 grid-template-rows: fit-content(8rem) auto fit-content(8rem);45 grid-template-columns: fit-content(25%) 1fr;46 grid-template-areas:47 "header header"48 "menu content"49 "footer footer";50}51 52h1 {53 margin: 0 0 8px -2px;54 font-size: 175%;55}56 57.header {58 padding: 0 0 5px 15px;59 grid-area: header;60 border-bottom: thin solid #aaa;61}62 63.footer {64 grid-area: footer;65 border-top: thin solid #aaa;66 font-size: 85%;67 68}69 70.footer > p {71 margin: 4px;72}73 74#menu,75#menu_index {76 grid-area: menu;77 text-align: left;78 overflow: auto;79 padding: 0 23px 15px 15px;80 border-right: thin solid #aaa;81 min-width: 200px;82}83 84#menu > a {85 display: block;86 margin-left: 10px;87 font-size: 12px;88}89 90#content,91#content_index {92 grid-area: content;93 padding: 0px 5px 15px 15px;94 overflow: auto;95}96 97label {98 white-space: nowrap;99}100 101label.checkBtn.disabled {102 color: #606060;103 background: #e0e0e0;104 font-style: italic;105}106 107label.checkBtn, input[type="text"] {108 border: 1px solid grey;109 border-radius: 4px;110 box-shadow: 1px 1px inset;111 padding: 1px 5px;112}113 114label.checkBtn {115 white-space: nowrap;116 background: #ccddff;117}118 119label.unchecked {120 background: #eff8ff;121 box-shadow: 1px 1px 1px;122}123 124label.checkBtn:hover, label.unchecked:hover{125 box-shadow: 0 0 2px;126}127 128label.disabled:hover {129 box-shadow: 1px 1px inset;130}131 132label.checkBtn > input {133 display:none;134}135 136.summaryTable {137 width: 100%;138}139 140table.summaryTable td { padding: 0 5px 0 5px; }141 142.statHeader, .severityHeader, .classificationHeader {143 font-weight: bold;144}145 146.warning {147 background-color: #ffffa7;148}149 150.error {151 background-color: #ffb7b7;152}153 154.error2 {155 background-color: #faa;156 display: inline-block;157 margin-left: 4px;158}159 160.inconclusive {161 background-color: #b6b6b4;162}163 164.inconclusive2 {165 background-color: #b6b6b4;166 display: inline-block;167 margin-left: 4px;168}169 170.verbose {171 display: inline-block;172 vertical-align: top;173 cursor: help;174}175 176.verbose .content {177 display: none;178 position: absolute;179 padding: 10px;180 margin: 4px;181 max-width: 40%;182 white-space: pre-wrap;183 border: 1px solid #000;184 background-color: #ffffcc;185 cursor: auto;186}187 188.highlight .hll {189 padding: 1px;190}191 192.highlighttable {193 background-color: #fff;194 position: relative;195 margin: -10px;196}197 198.linenos {199 border-right: thin solid #aaa;200 color: #d3d3d3;201 padding-right: 6px;202}203 204.id-filtered, .severity-filtered, .classification-filtered, .file-filtered, .tool-filtered, .text-filtered {205 visibility: collapse;206}207"""208 209HTML_HEAD = """210<!doctype html>211<html lang="en">212 <head>213 <meta charset="utf-8">214 <title>Cppcheck - HTML report - %s</title>215 <link rel="stylesheet" href="style.css">216 <style>217%s218 </style>219 <script>220 function getStyle(el, styleProp) {221 var y;222 223 if (el.currentStyle) {224 y = el.currentStyle[styleProp];225 } else if (window.getComputedStyle) {226 y = document.defaultView.getComputedStyle(el, null).getPropertyValue(styleProp);227 }228 229 return y;230 }231 232 function toggle() {233 var el = this.expandable_content;234 var mark = this.expandable_marker;235 236 if (el.style.display === "block") {237 el.style.display = "none";238 mark.textContent = "[+]";239 } else {240 el.style.display = "block";241 mark.textContent = "[-]";242 }243 }244 245 function initExpandables() {246 var elements = document.querySelectorAll(".expandable");247 248 for (var i = 0, len = elements.length; i < len; i++) {249 var el = elements[i];250 var clickable = el.querySelector("span");251 var marker = clickable.querySelector(".marker");252 var content = el.querySelector(".content");253 var width = clickable.clientWidth - parseInt(getStyle(content, "padding-left")) - parseInt(getStyle(content, "padding-right"));254 content.style.width = width + "px";255 clickable.expandable_content = content;256 clickable.expandable_marker = marker;257 clickable.addEventListener("click", toggle);258 }259 }260 261 function toggleDisplay(cb) {262 var elements = document.querySelectorAll("." + cb.id);263 264 for (var i = 0, len = elements.length; i < len; i++) {265 elements[i].classList.toggle("id-filtered", !cb.checked);266 }267 268 updateFileRows();269 }270 271 function toggleSeverity(cb) {272 cb.parentElement.classList.toggle("unchecked", !cb.checked);273 var elements = document.querySelectorAll(".sev_" + cb.id);274 275 for (var i = 0, len = elements.length; i < len; i++) {276 elements[i].classList.toggle("severity-filtered", !cb.checked);277 }278 279 updateFileRows();280 }281 282 function toggleClassification(cb) {283 cb.parentElement.classList.toggle("unchecked", !cb.checked);284 var elements = document.querySelectorAll(".class_" + cb.id);285 286 for (var i = 0, len = elements.length; i < len; i++) {287 elements[i].classList.toggle("classification-filtered", !cb.checked);288 }289 290 updateFileRows();291 }292 293 function toggleTool(cb) {294 cb.parentElement.classList.toggle("unchecked", !cb.checked);295 296 var elements;297 if (cb.id == "clang-tidy")298 elements = document.querySelectorAll("[class^=clang-tidy-]");299 else300 elements = document.querySelectorAll(".issue:not([class^=clang-tidy-])");301 302 for (var i = 0, len = elements.length; i < len; i++) {303 elements[i].classList.toggle("tool-filtered", !cb.checked);304 }305 306 updateFileRows();307 }308 309 function toggleAll() {310 var elements = document.querySelectorAll(".idToggle");311 312 // starting from 1 since 0 is the "toggle all" input313 for (var i = 1, len = elements.length; i < len; i++) {314 var changed = elements[i].checked != elements[0].checked;315 if (changed) {316 elements[i].checked = elements[0].checked;317 toggleDisplay(elements[i]);318 }319 }320 }321 322 function filterFile(filter) {323 var elements = document.querySelectorAll(".fileEntry");324 325 for (var i = 0, len = elements.length; i < len; i++) {326 var visible = elements[i].querySelector("tr").querySelector("td").textContent.toLowerCase().includes(filter.toLowerCase());327 elements[i].classList.toggle("text-filtered", !visible);328 }329 }330 331 function filterText(text) {332 filter = text.toLowerCase();333 var elements = document.querySelectorAll(".issue");334 335 for (var i = 0, len = elements.length; i < len; i++) {336 var visible = false;337 var fields = elements[i].querySelectorAll("td");338 for (var n = 0, num = fields.length; n < num; n++) {339 if (fields[n].textContent.toLowerCase().includes(filter)) {340 visible = true;341 break;342 }343 }344 elements[i].classList.toggle("text-filtered", !visible);345 }346 347 updateFileRows();348 }349 350 function updateFileRows(element) {351 var elements = document.querySelectorAll(".fileEntry");352 353 for (var i = 0, len = elements.length; i < len; i++) {354 var visible = elements[i].querySelector(".issue:not(.id-filtered):not(.severity-filtered):not(.classification-filtered):not(.tool-filtered):not(.text-filtered)");355 elements[i].classList.toggle("file-filtered", !visible);356 }357 }358 359 function reapplyFilters() {360 // Reapply ID filters361 var idToggles = document.querySelectorAll(".idToggle");362 for (var i = 1; i < idToggles.length; i++) {363 var cb = idToggles[i];364 var elements = document.querySelectorAll("." + cb.id);365 for (var j = 0; j < elements.length; j++) {366 elements[j].classList.toggle("id-filtered", !cb.checked);367 }368 }369 370 // Reapply severity filters371 var sevToggles = document.querySelectorAll(".sev_toggle");372 for (var i = 0; i < sevToggles.length; i++) {373 var cb = sevToggles[i];374 var elements = document.querySelectorAll(".sev_" + cb.id);375 for (var j = 0; j < elements.length; j++) {376 elements[j].classList.toggle("severity-filtered", !cb.checked);377 }378 }379 380 // Reapply classification filters381 var classToggles = document.querySelectorAll(".class_toggle");382 for (var i = 0; i < classToggles.length; i++) {383 var cb = classToggles[i];384 var elements = document.querySelectorAll(".class_" + cb.id);385 for (var j = 0; j < elements.length; j++) {386 elements[j].classList.toggle("classification-filtered", !cb.checked);387 }388 }389 390 // Reapply tool filters391 var toolToggles = document.querySelectorAll(".tool_toggle");392 for (var i = 0; i < toolToggles.length; i++) {393 var cb = toolToggles[i];394 var elements;395 if (cb.id == "clang-tidy")396 elements = document.querySelectorAll("[class^=clang-tidy-]");397 else398 elements = document.querySelectorAll(".issue:not([class^=clang-tidy-])");399 400 for (var j = 0; j < elements.length; j++) {401 elements[j].classList.toggle("tool-filtered", !cb.checked);402 }403 }404 405 // Update file rows406 updateFileRows();407 }408 409 window.addEventListener("pageshow", reapplyFilters);410 window.addEventListener("load", initExpandables);411 </script>412 </head>413 <body>414 <div id="wrapper">415 <div id="header" class="header">416 <h1>Cppcheck report - %s%s</h1>417"""418 419HTML_HEAD_END = """420 </div>421"""422 423HTML_MENU = """424 <div id="menu">425 <p><a href="index.html">Defects:</a> %s</p>426"""427 428HTML_MENU_END = """429 </div>430 <div id="content">431"""432 433HTML_FOOTER = """434 </div>435 <div id="footer" class="footer">436 <p>437 Created by Cppcheck %s (<a href="https://cppcheck.sourceforge.io">Sourceforge</a>, <a href="irc://irc.libera.chat/#cppcheck">IRC</a>)438 </p>439 </div>440 </div>441 </body>442</html>443"""444 445HTML_ERROR = "<span class=\"error2\"><--- %s</span>\n"446HTML_INCONCLUSIVE = "<span class=\"inconclusive2\"><--- %s</span>\n"447 448HTML_EXPANDABLE_ERROR = "<div class=\"verbose expandable\"><span class=\"error2\"><--- %s <span class=\"marker\">[+]</span></span><div class=\"content\">%s</div></div>\n"449HTML_EXPANDABLE_INCONCLUSIVE = "<div class=\"verbose expandable\"><span class=\"inconclusive2\"><--- %s <span class=\"marker\">[+]</span></span><div class=\"content\">%s</div></div>\n"450 451# escape() and unescape() takes care of &, < and >.452html_escape_table = {453 '"': """,454 "'": "'"455}456html_unescape_table = {v: k for k, v in html_escape_table.items()}457 458 459def html_escape(text):460 return escape(text, html_escape_table)461 462def filter_button(enabled_filters, id, function):463 enabled = enabled_filters.get(id, False)464 if not id:465 id = 'None'466 return '\n <label class="checkBtn%s"><input type="checkbox" onclick="%s(this)" id="%s" %s>%s</label>'\467 % (' disabled' if not enabled else '', function, id, 'checked' if enabled else 'disabled', id)468 469def filter_bar(enabled):470 severity_bar = ''.join([filter_button(enabled, severity, 'toggleSeverity') for severity in ['error', 'warning', 'portability', 'performance', 'style', 'information']]) + '\n | '471 classification_bar = ''.join([filter_button(enabled, _class, 'toggleClassification') for _class in ['Mandatory', 'Required', 'Advisory', 'Document', 'Disapplied', 'L1','L2','L3','']]) + '\n | '472 if "checked/>" not in severity_bar:473 severity_bar = ''474 if "checked/>" not in classification_bar:475 classification_bar = ''476 return ''.join([477 ' <div id="filters">\n'478 ,severity_bar479 , classification_bar480 ,''.join([filter_button(enabled, tool, 'toggleTool') for tool in ['cppcheck', 'clang-tidy']])481 ,'\n | '482 ,'\n <label class="severityHeader">File: <input type="search" oninput="filterFile(this.value)"></label>'483 ,'\n <label class="severityHeader">Filter: <input type="search" oninput="filterText(this.value)"></label>'484 ,'\n </div>\n'])485def git_blame(errors, path, file, blame_options):486 last_line= errors[-1]['line']487 if last_line == 0:488 return {}489 490 first_line = next((error for error in errors if error['line'] > 0))['line']491 492 full_path = os.path.join(path, file)493 path, filename = os.path.split(full_path)494 495 if not path:496 path = None # make sure this is None497 498 cmd_args = ['git', 'blame', '-L %d,%d' % (first_line, last_line)]499 if '-w' in blame_options:500 cmd_args.append('-w')501 if '-M' in blame_options:502 cmd_args.append('-M')503 cmd_args = cmd_args + ['--porcelain', '--incremental', '--', filename]504 505 try:506 result = subprocess.check_output(cmd_args, cwd=path)507 result = result.decode(locale.getpreferredencoding())508 except:509 return []510 511 if result.startswith('fatal'):512 return []513 514 blame_data = []515 line_from = 0516 line_to = 0517 blame_info = {}518 519 for line_str in result.splitlines():520 field, _, value = line_str.partition(' ')521 if len(field) == 40:522 line_nr, count = value.split(' ')[1:]523 line_from = int(line_nr)524 line_to = line_from + int(count)525 elif field.startswith('author'):526 blame_info[field] = value527 if field == 'author-time':528 author_date = date.fromtimestamp(int(blame_info['author-time']))529 blame_info['author-time'] = author_date.strftime("%d/%m/%Y")530 elif field == 'filename':531 blame_data.append([line_from, line_to, blame_info.copy()])532 533 return blame_data534 535 536def blame_lookup(blame_data, line):537 return next((data for start, end, data in blame_data if line >= start and line < end), {})538 539 540def tr_str(td_th, line, id, cwe, severity, classification, guideline, message, timestamp, author, author_mail, date, add_author, tr_class=None, htmlfile=None, message_class=None):541 ret = ''542 items = [id, cwe]543 if severity:544 items.append(severity)545 if classification:546 items.extend([classification, guideline])547 if htmlfile:548 if htmlfile.startswith("http://") or htmlfile.startswith("https://"):549 # GitHub/GitLab style line anchor550 href = f"{htmlfile.rstrip('#L1')}#L{line}"551 # Emit **line number with link**552 ret += f'<{td_th}><a href="{href}" target="_blank" rel="noopener noreferrer">{line}</a></{td_th}>'553 else:554 # local HTML annotated555 href = f"{htmlfile}#line-{line}"556 # Emit **line number with link**557 ret += f'<{td_th}><a href="{href}">{line}</a></{td_th}>'558 559 # Emit id, cwe, severity, classification, ...560 for item in items:561 ret += f'<{td_th}>{item}</{td_th}>'562 else:563 items.insert(0,line)564 for item in items:565 ret += '<%s>%s</%s>' % (td_th, item, td_th)566 if message_class:567 message_attribute = ' class="%s"' % message_class568 else:569 message_attribute = ''570 ret += '<%s%s>%s</%s>' % (td_th, message_attribute, html_escape(message), td_th)571 572 if timestamp:573 ret += '<%s>%s</%s>' % (td_th, timestamp, td_th)574 575 for field in add_author:576 if field == 'name':577 ret += '<%s>%s</%s>' % (td_th, html_escape(author), td_th)578 elif field == 'email':579 ret += '<%s>%s</%s>' % (td_th, html_escape(author_mail), td_th)580 elif field == 'date':581 ret += '<%s>%s</%s>' % (td_th, date, td_th)582 583 if tr_class:584 tr_attributes = ' class="%s"' % tr_class585 else:586 tr_attributes = ''587 return '<tr%s>%s</tr>' % (tr_attributes, ret)588 589 590def to_css_selector(tag):591 # https://www.w3.org/TR/CSS2/syndata.html#characters592 # Note: for our usage, we don't consider escaped characters593 invalid_css_chars = re.compile(r"[^-_a-zA-Z0-9\u00A0-\uFFFF]")594 invalid_start = re.compile(r"^([0-9]|-[0-9]|--)")595 # Replace forbidden characters by an hyphen596 valid_chars = invalid_css_chars.sub("-", tag)597 # Check that the start of the tag doesn't break the rules598 start_invalid = invalid_start.match(valid_chars)599 if start_invalid:600 # otherwise, append a token to make it valid601 valid_chars_valid_start = "cpp" + valid_chars602 else:603 valid_chars_valid_start = valid_chars604 return valid_chars_valid_start605 606class AnnotateCodeFormatter(HtmlFormatter):607 errors = []608 609 def wrap(self, *args, **kwargs):610 line_no = 1611 for i, t in HtmlFormatter.wrap(self, *args, **kwargs):612 # If this is a source code line we want to add a span tag at the613 # end.614 if i == 1:615 for error in self.errors:616 if error['line'] == line_no:617 try:618 if error['inconclusive'] == 'true':619 # only print verbose msg if it really differs620 # from actual message621 if error.get('verbose') and (error['verbose'] != error['msg']):622 index = t.rfind('\n')623 t = t[:index] + HTML_EXPANDABLE_INCONCLUSIVE % (error['msg'], html_escape(error['verbose'].replace("\\012", '\n'))) + t[index + 1:]624 else:625 t = t.replace('\n', HTML_INCONCLUSIVE % error['msg'])626 except KeyError:627 if error.get('verbose') and (error['verbose'] != error['msg']):628 index = t.rfind('\n')629 t = t[:index] + HTML_EXPANDABLE_ERROR % (error['msg'], html_escape(error['verbose'].replace("\\012", '\n'))) + t[index + 1:]630 else:631 t = t.replace('\n', HTML_ERROR % error['msg'])632 633 line_no = line_no + 1634 yield i, t635 636 637class CppCheckHandler(XmlContentHandler):638 639 """Parses the cppcheck xml file and produces a list of all its errors."""640 641 def __init__(self):642 XmlContentHandler.__init__(self)643 self.errors = []644 self.version = '1'645 self.versionCppcheck = ''646 self.timestamp = ''647 self.report_type = False648 649 def startElement(self, name, attributes):650 if name == 'results':651 self.version = attributes.get('version', self.version)652 653 if self.version == '1':654 self.handleVersion1(name, attributes)655 else:656 self.handleVersion2(name, attributes)657 658 def handleVersion1(self, name, attributes):659 if name != 'error':660 return661 att_class = attributes.get('classification', '')662 att_guide = attributes.get('guideline', '')663 if att_class and not self.report_type:664 self.report_type = True665 self.errors.append({666 'file': attributes.get('file', ''),667 'line': int(attributes.get('line', 0)),668 'locations': [{669 'file': attributes.get('file', ''),670 'line': int(attributes.get('line', 0)),671 }],672 'id': attributes['id'],673 'severity': attributes['severity'],674 'classification': att_class,675 'guideline': att_guide,676 'timestamp': self.timestamp,677 'msg': attributes['msg']678 })679 680 def handleVersion2(self, name, attributes):681 if name == 'cppcheck':682 self.versionCppcheck = attributes['version']683 if name == 'error':684 att_class = attributes.get('classification', '')685 att_guide = attributes.get('guideline', '')686 if att_class and not self.report_type:687 self.report_type = True688 error = {689 'locations': [],690 'file': '',691 'line': 0,692 'id': attributes['id'],693 'classification': att_class,694 'severity': attributes['severity'],695 'guideline': att_guide,696 'timestamp': self.timestamp,697 'msg': attributes['msg'],698 'verbose': attributes.get('verbose')699 }700 701 if 'inconclusive' in attributes:702 error['inconclusive'] = attributes['inconclusive']703 if 'cwe' in attributes:704 error['cwe'] = attributes['cwe']705 706 self.errors.append(error)707 elif name == 'location':708 assert self.errors709 error = self.errors[-1]710 locations = error['locations']711 file = attributes['file']712 line = int(attributes['line'])713 if not locations:714 error['file'] = file715 error['line'] = line716 locations.append({717 'file': file,718 'line': line,719 'info': attributes.get('info')720 })721def main() -> None:722 # Configure all the options this little utility is using.723 parser = argparse.ArgumentParser()724 parser.add_argument('--title', dest='title',725 help='The title of the project.',726 default='[project name]')727 parser.add_argument('--file', dest='file', action="append",728 help='The cppcheck xml output file to read defects '729 'from. You can combine results from several '730 'xml reports i.e. "--file file1.xml --file file2.xml ..". '731 'Default is reading from stdin.')732 parser.add_argument('--checkers-report-file', dest='checkers_report',733 help='The cppcheck checkers report file as produced '734 'with the "--checkers-report" option of cppcheck.')735 parser.add_argument('--report-dir', dest='report_dir',736 help='The directory where the HTML report content is '737 'written.')738 parser.add_argument('--source-dir', dest='source_dir',739 help='Base directory where source code files can be '740 'found, or a URL to a remote GitHub/GitLab '741 'repository including a branch, e.g. '742 '--source-dir=https://github.com/<username>/<repo>/blob/<branch>/')743 parser.add_argument('--add-author-information', dest='add_author_information',744 help='Blame information to include. '745 'Adds specified author information. '746 'Specify as comma-separated list of either "name", "email", "date" or "n","e","d". '747 'Default: "n,e,d"')748 parser.add_argument('--source-encoding', dest='source_encoding',749 help='Encoding of source code.', default='utf-8')750 parser.add_argument('--blame-options', dest='blame_options',751 help='[-w, -M] blame options which you can use to get author and author mail '752 '-w --> not including white spaces and returns original author of the line '753 '-M --> not including moving of lines and returns original author of the line')754 755 # Parse options and make sure that we have an output directory set.756 options = parser.parse_args()757 758 try:759 sys.argv[1]760 except IndexError: # no arguments give, print --help761 parser.print_help()762 sys.exit(0)763 764 if not options.report_dir:765 parser.error('No report directory set.')766 767 # Get the directory where source code files are located.768 source_dir = os.getcwd()769 if options.source_dir:770 source_dir = options.source_dir771 772 is_remote = False773 if source_dir.startswith("http://") or source_dir.startswith("https://"):774 is_remote = True775 776 add_author_information = []777 if options.add_author_information:778 fields = [x.strip() for x in options.add_author_information.split(',')]779 for x in fields:780 if x.lower() in ['n', 'name']:781 add_author_information.append('name')782 elif x.lower() in ['e', 'email']:783 add_author_information.append('email')784 elif x.lower() in ['d', 'date']:785 add_author_information.append('date')786 else:787 print('Unrecognized value "%s" for author information, using default (name, email, date)' % x)788 add_author_information = ['name', 'email', 'date']789 break790 791 blame_options = ''792 if options.blame_options:793 blame_options = options.blame_options794 add_author_information = add_author_information or ['name', 'email', 'date']795 796 # Parse the xml from all files defined in file argument797 # or from stdin. If no input is provided, stdin is used798 # Produce a simple list of errors.799 print('Parsing xml report.')800 try:801 contentHandler = CppCheckHandler()802 for fname in options.file or [sys.stdin]:803 if options.file is not None:804 t = os.path.getmtime(fname)805 else:806 t = time.time()807 t_s = time.ctime(t)808 if t_s is not None:809 contentHandler.timestamp = t_s810 xml_parse(fname, contentHandler)811 except (XmlParseException, ValueError) as msg:812 print('Failed to parse cppcheck xml file: %s' % msg)813 sys.exit(1)814 815 # We have a list of errors. But now we want to group them on816 # each source code file. Lets create a files dictionary that817 # will contain a list of all the errors in that file. For each818 # file we will also generate a HTML filename to use.819 files = {}820 file_no = 0821 for error in contentHandler.errors:822 filename = error['file']823 if filename not in files:824 if is_remote:825 # Construct remote URL for GitHub/GitLab826 # tr_str() will use the actual line number, so we can just start with line 1827 remote_url = source_dir.rstrip('/') + '/' + filename + '#L1'828 files[filename] = {'errors': [], 'htmlfile': remote_url}829 else:830 files[filename] = {'errors': [], 'htmlfile': str(file_no) + '.html'}831 file_no += 1832 files[filename]['errors'].append(error)833 834 # Make sure that the report directory is created if it doesn't exist.835 print('Creating %s directory' % options.report_dir)836 if not os.path.exists(options.report_dir):837 os.makedirs(options.report_dir)838 839 # Generate a HTML file with syntax highlighted source code for each840 # file that contains one or more errors.841 print('Processing errors')842 843 decode_errors = []844 for filename, data in sorted(files.items()):845 htmlfile = data['htmlfile']846 errors = []847 848 for error in data['errors']:849 for location in error['locations']:850 if filename == location['file']:851 newError = dict(error)852 853 del newError['locations']854 newError['line'] = location['line']855 if location.get('info'):856 newError['msg'] = location['info']857 newError['severity'] = 'information'858 newError['classification'] = ''859 newError['guideline'] = ''860 del newError['verbose']861 862 errors.append(newError)863 864 lines = []865 for error in errors:866 lines.append(error['line'])867 868 if filename == '':869 continue870 871 if is_remote:872 # Remote source: do NOT generate local annotated HTML files.873 # The index will still point directly to GitHub/GitLab URLs.874 continue875 876 source_filename = os.path.join(source_dir, filename)877 try:878 with io.open(source_filename, 'r', encoding=options.source_encoding) as input_file:879 content = input_file.read()880 except IOError:881 if error['id'] != 'unmatchedSuppression':882 sys.stderr.write("ERROR: Source file '%s' not found.\n" %883 source_filename)884 continue885 except UnicodeDecodeError:886 sys.stderr.write("WARNING: Unicode decode error in '%s'.\n" %887 source_filename)888 decode_errors.append(source_filename[2:]) # "[2:]" gets rid of "./" at beginning889 continue890 891 htmlFormatter = AnnotateCodeFormatter(linenos=True,892 style='colorful',893 hl_lines=lines,894 lineanchors='line',895 encoding=options.source_encoding)896 htmlFormatter.errors = errors897 898 with io.open(os.path.join(options.report_dir, htmlfile), 'w', encoding='utf-8') as output_file:899 output_file.write(HTML_HEAD %900 (options.title,901 htmlFormatter.get_style_defs('.highlight'),902 options.title,903 ': ' + filename))904 output_file.write(HTML_HEAD_END)905 906 output_file.write(HTML_MENU % (filename.split('/')[-1]))907 for error in sorted(errors, key=lambda k: k['line']):908 output_file.write("<a href=\"%s#line-%d\"> %s %s</a>" % (data['htmlfile'], error['line'], error['id'], error['line']))909 output_file.write(HTML_MENU_END)910 911 try:912 lexer = guess_lexer_for_filename(source_filename, '', stripnl=False)913 except ClassNotFound:914 try:915 lexer = guess_lexer(content, stripnl=False)916 except ClassNotFound:917 sys.stderr.write("ERROR: Couldn't determine lexer for the file' " + source_filename + " '. Won't be able to syntax highlight this file.")918 output_file.write("\n <tr><td colspan=\"6\"> Could not generate content because pygments failed to determine the code type.</td></tr>")919 output_file.write("\n <tr><td colspan=\"6\"> Sorry about this.</td></tr>")920 continue921 922 if options.source_encoding:923 lexer.encoding = options.source_encoding924 925 output_file.write(926 highlight(content, lexer, htmlFormatter).decode(927 options.source_encoding))928 929 output_file.write(HTML_FOOTER % contentHandler.versionCppcheck)930 931 print(' ' + filename)932 933 # Generate a master index.html file that will contain a list of934 # all the errors created.935 print('Creating index.html')936 937 with io.open(os.path.join(options.report_dir, 'index.html'),938 'w') as output_file:939 940 stats_count = 0941 stats = []942 filter_enabled = {}943 for filename, data in sorted(files.items()):944 for error in data['errors']:945 stats.append(error['id']) # get the stats946 if contentHandler.report_type:947 filter_enabled[error['severity']] = False948 filter_enabled[error['classification']] = True949 else:950 filter_enabled[error['severity']] = True951 filter_enabled[error['classification']] = False952 filter_enabled['clang-tidy' if error['id'].startswith('clang-tidy-') else 'cppcheck'] = True953 stats_count += 1954 955 counter = Counter(stats)956 957 stat_html = []958 # the following lines sort the stat primary by value (occurrences),959 # but if two IDs occur equally often, then we sort them alphabetically by warning ID960 try:961 cnt_max = counter.most_common()[0][1]962 except IndexError:963 cnt_max = 0964 965 try:966 cnt_min = counter.most_common()[-1][1]967 except IndexError:968 cnt_min = 0969 970 stat_fmt = "\n <tr><td><input type=\"checkbox\" class=\"idToggle\" onclick=\"toggleDisplay(this)\" id=\"{}\" name=\"{}\" checked></td><td>{}</td><td>{}</td></tr>"971 for occurrences in reversed(range(cnt_min, cnt_max + 1)):972 for _id in [k for k, v in sorted(counter.items()) if v == occurrences]:973 stat_html.append(stat_fmt.format(974 to_css_selector(_id),975 to_css_selector(_id),976 dict(counter.most_common())[_id],977 _id978 ))979 980 output_file.write(HTML_HEAD % (options.title, '', options.title, ''))981 output_file.write(filter_bar(filter_enabled))982 output_file.write(HTML_HEAD_END)983 output_file.write(HTML_MENU.replace('id="menu"', 'id="menu_index"', 1).replace("Defects:", "Defect summary", 1) % (''))984 output_file.write('\n <label><input type="checkbox" class=\"idToggle\" onclick="toggleAll()" checked> Toggle all</label>')985 output_file.write('\n <table>')986 output_file.write('\n <tr><th>Show</th><th>#</th><th>Defect ID</th></tr>')987 output_file.write(''.join(stat_html))988 output_file.write('\n <tr><td></td><td>' + str(stats_count) + '</td><td>total</td></tr>')989 output_file.write('\n </table>')990 output_file.write('\n <p><a href="stats.html">Statistics</a></p>')991 if options.checkers_report:992 output_file.write('\n <p><a href="checkers.html">Checkers</a></p>')993 output_file.write(HTML_MENU_END.replace("content", "content_index", 1))994 995 output_file.write('\n <table class=\"summaryTable\">')996 if contentHandler.report_type:997 output_file.write(998 '\n %s' %999 tr_str('th', 'Line', 'Id', 'CWE', '', 'Classification', 'Guideline', 'Message', 'Timestamp',1000 'Author', 'Author mail', 'Date (DD/MM/YYYY)', add_author=add_author_information))1001 else:1002 output_file.write(1003 '\n %s' %1004 tr_str('th', 'Line', 'Id', 'CWE', 'Severity', '', '', 'Message', 'Timestamp', 'Author', 'Author mail', 'Date (DD/MM/YYYY)', add_author=add_author_information))1005 1006 for filename, data in sorted(files.items()):1007 file_error = filename in decode_errors or filename.endswith('*')1008 is_file = filename != '' and not file_error1009 row_content = filename if file_error else "<a href=\"%s\">%s</a>" % (data['htmlfile'], filename)1010 htmlfile = data.get('htmlfile') if is_file else None1011 1012 output_file.write("\n <tbody class=\"fileEntry\">")1013 output_file.write("\n <tr><td colspan=\"6\">%s</td></tr>" % row_content)1014 1015 if filename in decode_errors:1016 output_file.write("\n <tr><td colspan=\"6\">Could not generated due to UnicodeDecodeError</td></tr>")1017 1018 sorted_errors = sorted(data['errors'], key=lambda k: k['line'])1019 blame_data = git_blame(sorted_errors, source_dir, filename, blame_options) if add_author_information else []1020 for error in sorted_errors:1021 git_blame_dict = blame_lookup(blame_data, error['line'])1022 message_class = None1023 try:1024 if error['inconclusive'] == 'true':1025 message_class = 'inconclusive'1026 error['severity'] += ", inconcl."1027 except KeyError:1028 pass1029 1030 cwe_url = ""1031 try:1032 if error['cwe']:1033 cwe_url = "<a href=\"https://cwe.mitre.org/data/definitions/" + error['cwe'] + ".html\">" + error['cwe'] + "</a>"1034 except KeyError:1035 pass1036 1037 if error['severity'] in ['error', 'warning']:1038 message_class = error['severity']1039 1040 line = error["line"] if is_file else ""1041 _severity = error.get('severity', '')1042 _classification = error.get('classification', '')1043 _guideline = error.get('guideline', '')1044 if contentHandler.report_type:1045 _severity = ''1046 if not _classification:1047 _classification = 'None'1048 _guideline = 'None'1049 output_file.write(1050 '\n %s' %1051 tr_str('td', line, error["id"], cwe_url, _severity, _classification, _guideline, error["msg"], error["timestamp"],1052 git_blame_dict.get('author', 'Unknown'), git_blame_dict.get('author-mail', '---'),1053 git_blame_dict.get('author-time', '---'),1054 tr_class=to_css_selector(error["id"]) + ' sev_' + _severity +' class_' + _classification + ' issue',1055 message_class=message_class,1056 add_author=add_author_information,1057 htmlfile=htmlfile))1058 output_file.write("\n </tbody>")1059 output_file.write('\n </table>')1060 output_file.write(HTML_FOOTER % contentHandler.versionCppcheck)1061 1062 if decode_errors:1063 sys.stderr.write("\nGenerating html failed for the following files: " + ' '.join(decode_errors))1064 sys.stderr.write("\nConsider changing source-encoding (for example: \"htmlreport ... --source-encoding=\"iso8859-1\"\"\n")1065 1066 print('Creating style.css file')1067 with io.open(os.path.join(options.report_dir, 'style.css'), 'w') as css_file:1068 css_file.write(STYLE_FILE)1069 1070 print("Creating stats.html (statistics)\n")1071 stats_countlist = {}1072 1073 for filename, data in sorted(files.items()):1074 if filename == '':1075 continue1076 stats_tmplist = []1077 for error in sorted(data['errors'], key=lambda k: k['line']):1078 stats_tmplist.append(error['severity'])1079 1080 stats_countlist[filename] = dict(Counter(stats_tmplist))1081 1082 # get top ten for each severity1083 SEVERITIES = "error", "warning", "portability", "performance", "style", "unusedFunction", "information", "missingInclude", "internal"1084 1085 with io.open(os.path.join(options.report_dir, 'stats.html'), 'w') as stats_file:1086 1087 stats_file.write(HTML_HEAD % (options.title, '', options.title, ': Statistics'))1088 stats_file.write(HTML_HEAD_END)1089 1090 stats_file.write(HTML_MENU.replace('id="menu"', 'id="menu_index"', 1).replace("Defects:", "Back to summary", 1) % (''))1091 stats_file.write(HTML_MENU_END.replace("content", "content_index", 1))1092 1093 1094 for sev in SEVERITIES:1095 _sum = 01096 stats_templist = {}1097 1098 # if the we have an style warning but we are checking for1099 # portability, we have to skip it to prevent KeyError1100 try:1101 for filename in stats_countlist:1102 try: # also bail out if we have a file with no sev-results1103 _sum += stats_countlist[filename][sev]1104 stats_templist[filename] = int(stats_countlist[filename][sev]) # file : amount,1105 except KeyError:1106 continue1107 # don't print "0 style" etc, if no style warnings were found1108 if _sum == 0:1109 continue1110 except KeyError:1111 continue1112 stats_file.write("<p><span class=\"statHeader\">Top 10 files for " + sev + " severity, total findings: " + str(_sum) + "</span><br>\n")1113 1114 # sort, so that the file with the most severities per type is first1115 stats_list_sorted = sorted(stats_templist.items(), key=operator.itemgetter(1, 0), reverse=True)1116 it = 01117 LENGTH = 01118 1119 for i in stats_list_sorted: # printing loop1120 # for aesthetics: if it's the first iteration of the loop, get1121 # the max length of the number string1122 if it == 0:1123 LENGTH = len(str(i[1])) # <- length of longest number, now get the difference and try to make other numbers align to it1124 1125 stats_file.write(" " * 3 + str(i[1]) + " " * (1 + LENGTH - len(str(i[1]))) + "<a href=\"" + files[i[0]]['htmlfile'] + "\"> " + i[0] + "</a><br>\n")1126 it += 11127 if it == 10: # print only the top 101128 break1129 stats_file.write("</p>\n")1130 1131 stats_file.write(HTML_FOOTER % contentHandler.versionCppcheck)1132 1133 if options.checkers_report:1134 print("Creating checkers.html (checkers report)\n")1135 1136 with io.open(os.path.join(options.report_dir, 'checkers.html'), 'w') as checkers_file:1137 1138 checkers_file.write(HTML_HEAD % (options.title, '', options.title, ': Checkers'))1139 checkers_file.write(HTML_HEAD_END)1140 1141 checkers_file.write(HTML_MENU.replace('id="menu"', 'id="menu_index"', 1).replace("Defects:", "Back to summary", 1) % (''))1142 checkers_file.write(HTML_MENU_END.replace("content", "content_index", 1))1143 1144 with io.open(options.checkers_report, 'r', encoding=options.source_encoding) as checkers_report:1145 content = checkers_report.read()1146 1147 checkers_file.write("<pre>\n")1148 checkers_file.write(html_escape(content))1149 checkers_file.write("</pre>\n")1150 1151 checkers_file.write(HTML_FOOTER % contentHandler.versionCppcheck)1152 1153 print("\nOpen '" + options.report_dir + "/index.html' to see the results.")1154 1155 1156if __name__ == "__main__":1157 main()1158 