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