blob: afc73802364a081349f972b988d9f30f053007bf [file] [log] [blame]
Roman Okhrimenko0142a682022-03-31 14:40:48 +03001#! /usr/bin/python3
2
3from __future__ import unicode_literals
4
5import io
6import sys
7import optparse
8import os
9import operator
10
11from collections import Counter
12from pygments import highlight
13from pygments.lexers import guess_lexer_for_filename
14from pygments.formatters import HtmlFormatter
15from xml.sax import parse as xml_parse
16from xml.sax import SAXParseException as XmlParseException
17from xml.sax.handler import ContentHandler as XmlContentHandler
18from xml.sax.saxutils import escape
19"""
20Turns a cppcheck xml file into a browsable html report along
21with syntax highlighted source code.
22"""
23
24STYLE_FILE = """
25body {
26 font: 13px Arial, Verdana, Sans-Serif;
27 margin: 0;
28 width: auto;
29}
30
31h1 {
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
61div.verbose {
62 display: inline-block;
63 vertical-align: top;
64 cursor: help;
65}
66
67div.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
175HTML_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
239HTML_HEAD_END = """
240 </div>
241 <div id="content">
242"""
243
244HTML_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
258HTML_ERROR = "<span class='error2'>&lt;--- %s</span>\n"
259HTML_INCONCLUSIVE = "<span class='inconclusive2'>&lt;--- %s</span>\n"
260
261HTML_EXPANDABLE_ERROR = "<div class='verbose expandable'><span class='error2'>&lt;--- %s <span class='marker'>[+]</span></span><div class='content'>%s</div></div>\n"""
262HTML_EXPANDABLE_INCONCLUSIVE = "<div class='verbose expandable'><span class='inconclusive2'>&lt;--- %s <span class='marker'>[+]</span></span><div class='content'>%s</div></div>\n"""
263
264# escape() and unescape() takes care of &, < and >.
265html_escape_table = {
266 '"': "&quot;",
267 "'": "&apos;"
268}
269html_unescape_table = {v: k for k, v in html_escape_table.items()}
270
271
272def html_escape(text):
273 return escape(text, html_escape_table)
274
275
276class 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
307class 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
377if __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("&#160;" * 3 + str(i[1]) + "&#160;" * (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.")