xref: /aosp_15_r20/external/clang/tools/scan-build-py/libscanbuild/report.py (revision 67e74705e28f6214e480b399dd47ea732279e315)
1*67e74705SXin Li# -*- coding: utf-8 -*-
2*67e74705SXin Li#                     The LLVM Compiler Infrastructure
3*67e74705SXin Li#
4*67e74705SXin Li# This file is distributed under the University of Illinois Open Source
5*67e74705SXin Li# License. See LICENSE.TXT for details.
6*67e74705SXin Li""" This module is responsible to generate 'index.html' for the report.
7*67e74705SXin Li
8*67e74705SXin LiThe input for this step is the output directory, where individual reports
9*67e74705SXin Licould be found. It parses those reports and generates 'index.html'. """
10*67e74705SXin Li
11*67e74705SXin Liimport re
12*67e74705SXin Liimport os
13*67e74705SXin Liimport os.path
14*67e74705SXin Liimport sys
15*67e74705SXin Liimport shutil
16*67e74705SXin Liimport time
17*67e74705SXin Liimport tempfile
18*67e74705SXin Liimport itertools
19*67e74705SXin Liimport plistlib
20*67e74705SXin Liimport glob
21*67e74705SXin Liimport json
22*67e74705SXin Liimport logging
23*67e74705SXin Liimport contextlib
24*67e74705SXin Lifrom libscanbuild import duplicate_check
25*67e74705SXin Lifrom libscanbuild.clang import get_version
26*67e74705SXin Li
27*67e74705SXin Li__all__ = ['report_directory', 'document']
28*67e74705SXin Li
29*67e74705SXin Li
30*67e74705SXin Li@contextlib.contextmanager
31*67e74705SXin Lidef report_directory(hint, keep):
32*67e74705SXin Li    """ Responsible for the report directory.
33*67e74705SXin Li
34*67e74705SXin Li    hint -- could specify the parent directory of the output directory.
35*67e74705SXin Li    keep -- a boolean value to keep or delete the empty report directory. """
36*67e74705SXin Li
37*67e74705SXin Li    stamp = time.strftime('scan-build-%Y-%m-%d-%H%M%S-', time.localtime())
38*67e74705SXin Li
39*67e74705SXin Li    parentdir = os.path.abspath(hint)
40*67e74705SXin Li    if not os.path.exists(parentdir):
41*67e74705SXin Li        os.makedirs(parentdir)
42*67e74705SXin Li
43*67e74705SXin Li    name = tempfile.mkdtemp(prefix=stamp, dir=parentdir)
44*67e74705SXin Li
45*67e74705SXin Li    logging.info('Report directory created: %s', name)
46*67e74705SXin Li
47*67e74705SXin Li    try:
48*67e74705SXin Li        yield name
49*67e74705SXin Li    finally:
50*67e74705SXin Li        if os.listdir(name):
51*67e74705SXin Li            msg = "Run 'scan-view %s' to examine bug reports."
52*67e74705SXin Li            keep = True
53*67e74705SXin Li        else:
54*67e74705SXin Li            if keep:
55*67e74705SXin Li                msg = "Report directory '%s' contans no report, but kept."
56*67e74705SXin Li            else:
57*67e74705SXin Li                msg = "Removing directory '%s' because it contains no report."
58*67e74705SXin Li        logging.warning(msg, name)
59*67e74705SXin Li
60*67e74705SXin Li        if not keep:
61*67e74705SXin Li            os.rmdir(name)
62*67e74705SXin Li
63*67e74705SXin Li
64*67e74705SXin Lidef document(args, output_dir, use_cdb):
65*67e74705SXin Li    """ Generates cover report and returns the number of bugs/crashes. """
66*67e74705SXin Li
67*67e74705SXin Li    html_reports_available = args.output_format in {'html', 'plist-html'}
68*67e74705SXin Li
69*67e74705SXin Li    logging.debug('count crashes and bugs')
70*67e74705SXin Li    crash_count = sum(1 for _ in read_crashes(output_dir))
71*67e74705SXin Li    bug_counter = create_counters()
72*67e74705SXin Li    for bug in read_bugs(output_dir, html_reports_available):
73*67e74705SXin Li        bug_counter(bug)
74*67e74705SXin Li    result = crash_count + bug_counter.total
75*67e74705SXin Li
76*67e74705SXin Li    if html_reports_available and result:
77*67e74705SXin Li        logging.debug('generate index.html file')
78*67e74705SXin Li        # common prefix for source files to have sort filenames
79*67e74705SXin Li        prefix = commonprefix_from(args.cdb) if use_cdb else os.getcwd()
80*67e74705SXin Li        # assemble the cover from multiple fragments
81*67e74705SXin Li        try:
82*67e74705SXin Li            fragments = []
83*67e74705SXin Li            if bug_counter.total:
84*67e74705SXin Li                fragments.append(bug_summary(output_dir, bug_counter))
85*67e74705SXin Li                fragments.append(bug_report(output_dir, prefix))
86*67e74705SXin Li            if crash_count:
87*67e74705SXin Li                fragments.append(crash_report(output_dir, prefix))
88*67e74705SXin Li            assemble_cover(output_dir, prefix, args, fragments)
89*67e74705SXin Li            # copy additinal files to the report
90*67e74705SXin Li            copy_resource_files(output_dir)
91*67e74705SXin Li            if use_cdb:
92*67e74705SXin Li                shutil.copy(args.cdb, output_dir)
93*67e74705SXin Li        finally:
94*67e74705SXin Li            for fragment in fragments:
95*67e74705SXin Li                os.remove(fragment)
96*67e74705SXin Li    return result
97*67e74705SXin Li
98*67e74705SXin Li
99*67e74705SXin Lidef assemble_cover(output_dir, prefix, args, fragments):
100*67e74705SXin Li    """ Put together the fragments into a final report. """
101*67e74705SXin Li
102*67e74705SXin Li    import getpass
103*67e74705SXin Li    import socket
104*67e74705SXin Li    import datetime
105*67e74705SXin Li
106*67e74705SXin Li    if args.html_title is None:
107*67e74705SXin Li        args.html_title = os.path.basename(prefix) + ' - analyzer results'
108*67e74705SXin Li
109*67e74705SXin Li    with open(os.path.join(output_dir, 'index.html'), 'w') as handle:
110*67e74705SXin Li        indent = 0
111*67e74705SXin Li        handle.write(reindent("""
112*67e74705SXin Li        |<!DOCTYPE html>
113*67e74705SXin Li        |<html>
114*67e74705SXin Li        |  <head>
115*67e74705SXin Li        |    <title>{html_title}</title>
116*67e74705SXin Li        |    <link type="text/css" rel="stylesheet" href="scanview.css"/>
117*67e74705SXin Li        |    <script type='text/javascript' src="sorttable.js"></script>
118*67e74705SXin Li        |    <script type='text/javascript' src='selectable.js'></script>
119*67e74705SXin Li        |  </head>""", indent).format(html_title=args.html_title))
120*67e74705SXin Li        handle.write(comment('SUMMARYENDHEAD'))
121*67e74705SXin Li        handle.write(reindent("""
122*67e74705SXin Li        |  <body>
123*67e74705SXin Li        |    <h1>{html_title}</h1>
124*67e74705SXin Li        |    <table>
125*67e74705SXin Li        |      <tr><th>User:</th><td>{user_name}@{host_name}</td></tr>
126*67e74705SXin Li        |      <tr><th>Working Directory:</th><td>{current_dir}</td></tr>
127*67e74705SXin Li        |      <tr><th>Command Line:</th><td>{cmd_args}</td></tr>
128*67e74705SXin Li        |      <tr><th>Clang Version:</th><td>{clang_version}</td></tr>
129*67e74705SXin Li        |      <tr><th>Date:</th><td>{date}</td></tr>
130*67e74705SXin Li        |    </table>""", indent).format(html_title=args.html_title,
131*67e74705SXin Li                                         user_name=getpass.getuser(),
132*67e74705SXin Li                                         host_name=socket.gethostname(),
133*67e74705SXin Li                                         current_dir=prefix,
134*67e74705SXin Li                                         cmd_args=' '.join(sys.argv),
135*67e74705SXin Li                                         clang_version=get_version(args.clang),
136*67e74705SXin Li                                         date=datetime.datetime.today(
137*67e74705SXin Li                                         ).strftime('%c')))
138*67e74705SXin Li        for fragment in fragments:
139*67e74705SXin Li            # copy the content of fragments
140*67e74705SXin Li            with open(fragment, 'r') as input_handle:
141*67e74705SXin Li                shutil.copyfileobj(input_handle, handle)
142*67e74705SXin Li        handle.write(reindent("""
143*67e74705SXin Li        |  </body>
144*67e74705SXin Li        |</html>""", indent))
145*67e74705SXin Li
146*67e74705SXin Li
147*67e74705SXin Lidef bug_summary(output_dir, bug_counter):
148*67e74705SXin Li    """ Bug summary is a HTML table to give a better overview of the bugs. """
149*67e74705SXin Li
150*67e74705SXin Li    name = os.path.join(output_dir, 'summary.html.fragment')
151*67e74705SXin Li    with open(name, 'w') as handle:
152*67e74705SXin Li        indent = 4
153*67e74705SXin Li        handle.write(reindent("""
154*67e74705SXin Li        |<h2>Bug Summary</h2>
155*67e74705SXin Li        |<table>
156*67e74705SXin Li        |  <thead>
157*67e74705SXin Li        |    <tr>
158*67e74705SXin Li        |      <td>Bug Type</td>
159*67e74705SXin Li        |      <td>Quantity</td>
160*67e74705SXin Li        |      <td class="sorttable_nosort">Display?</td>
161*67e74705SXin Li        |    </tr>
162*67e74705SXin Li        |  </thead>
163*67e74705SXin Li        |  <tbody>""", indent))
164*67e74705SXin Li        handle.write(reindent("""
165*67e74705SXin Li        |    <tr style="font-weight:bold">
166*67e74705SXin Li        |      <td class="SUMM_DESC">All Bugs</td>
167*67e74705SXin Li        |      <td class="Q">{0}</td>
168*67e74705SXin Li        |      <td>
169*67e74705SXin Li        |        <center>
170*67e74705SXin Li        |          <input checked type="checkbox" id="AllBugsCheck"
171*67e74705SXin Li        |                 onClick="CopyCheckedStateToCheckButtons(this);"/>
172*67e74705SXin Li        |        </center>
173*67e74705SXin Li        |      </td>
174*67e74705SXin Li        |    </tr>""", indent).format(bug_counter.total))
175*67e74705SXin Li        for category, types in bug_counter.categories.items():
176*67e74705SXin Li            handle.write(reindent("""
177*67e74705SXin Li        |    <tr>
178*67e74705SXin Li        |      <th>{0}</th><th colspan=2></th>
179*67e74705SXin Li        |    </tr>""", indent).format(category))
180*67e74705SXin Li            for bug_type in types.values():
181*67e74705SXin Li                handle.write(reindent("""
182*67e74705SXin Li        |    <tr>
183*67e74705SXin Li        |      <td class="SUMM_DESC">{bug_type}</td>
184*67e74705SXin Li        |      <td class="Q">{bug_count}</td>
185*67e74705SXin Li        |      <td>
186*67e74705SXin Li        |        <center>
187*67e74705SXin Li        |          <input checked type="checkbox"
188*67e74705SXin Li        |                 onClick="ToggleDisplay(this,'{bug_type_class}');"/>
189*67e74705SXin Li        |        </center>
190*67e74705SXin Li        |      </td>
191*67e74705SXin Li        |    </tr>""", indent).format(**bug_type))
192*67e74705SXin Li        handle.write(reindent("""
193*67e74705SXin Li        |  </tbody>
194*67e74705SXin Li        |</table>""", indent))
195*67e74705SXin Li        handle.write(comment('SUMMARYBUGEND'))
196*67e74705SXin Li    return name
197*67e74705SXin Li
198*67e74705SXin Li
199*67e74705SXin Lidef bug_report(output_dir, prefix):
200*67e74705SXin Li    """ Creates a fragment from the analyzer reports. """
201*67e74705SXin Li
202*67e74705SXin Li    pretty = prettify_bug(prefix, output_dir)
203*67e74705SXin Li    bugs = (pretty(bug) for bug in read_bugs(output_dir, True))
204*67e74705SXin Li
205*67e74705SXin Li    name = os.path.join(output_dir, 'bugs.html.fragment')
206*67e74705SXin Li    with open(name, 'w') as handle:
207*67e74705SXin Li        indent = 4
208*67e74705SXin Li        handle.write(reindent("""
209*67e74705SXin Li        |<h2>Reports</h2>
210*67e74705SXin Li        |<table class="sortable" style="table-layout:automatic">
211*67e74705SXin Li        |  <thead>
212*67e74705SXin Li        |    <tr>
213*67e74705SXin Li        |      <td>Bug Group</td>
214*67e74705SXin Li        |      <td class="sorttable_sorted">
215*67e74705SXin Li        |        Bug Type
216*67e74705SXin Li        |        <span id="sorttable_sortfwdind">&nbsp;&#x25BE;</span>
217*67e74705SXin Li        |      </td>
218*67e74705SXin Li        |      <td>File</td>
219*67e74705SXin Li        |      <td>Function/Method</td>
220*67e74705SXin Li        |      <td class="Q">Line</td>
221*67e74705SXin Li        |      <td class="Q">Path Length</td>
222*67e74705SXin Li        |      <td class="sorttable_nosort"></td>
223*67e74705SXin Li        |    </tr>
224*67e74705SXin Li        |  </thead>
225*67e74705SXin Li        |  <tbody>""", indent))
226*67e74705SXin Li        handle.write(comment('REPORTBUGCOL'))
227*67e74705SXin Li        for current in bugs:
228*67e74705SXin Li            handle.write(reindent("""
229*67e74705SXin Li        |    <tr class="{bug_type_class}">
230*67e74705SXin Li        |      <td class="DESC">{bug_category}</td>
231*67e74705SXin Li        |      <td class="DESC">{bug_type}</td>
232*67e74705SXin Li        |      <td>{bug_file}</td>
233*67e74705SXin Li        |      <td class="DESC">{bug_function}</td>
234*67e74705SXin Li        |      <td class="Q">{bug_line}</td>
235*67e74705SXin Li        |      <td class="Q">{bug_path_length}</td>
236*67e74705SXin Li        |      <td><a href="{report_file}#EndPath">View Report</a></td>
237*67e74705SXin Li        |    </tr>""", indent).format(**current))
238*67e74705SXin Li            handle.write(comment('REPORTBUG', {'id': current['report_file']}))
239*67e74705SXin Li        handle.write(reindent("""
240*67e74705SXin Li        |  </tbody>
241*67e74705SXin Li        |</table>""", indent))
242*67e74705SXin Li        handle.write(comment('REPORTBUGEND'))
243*67e74705SXin Li    return name
244*67e74705SXin Li
245*67e74705SXin Li
246*67e74705SXin Lidef crash_report(output_dir, prefix):
247*67e74705SXin Li    """ Creates a fragment from the compiler crashes. """
248*67e74705SXin Li
249*67e74705SXin Li    pretty = prettify_crash(prefix, output_dir)
250*67e74705SXin Li    crashes = (pretty(crash) for crash in read_crashes(output_dir))
251*67e74705SXin Li
252*67e74705SXin Li    name = os.path.join(output_dir, 'crashes.html.fragment')
253*67e74705SXin Li    with open(name, 'w') as handle:
254*67e74705SXin Li        indent = 4
255*67e74705SXin Li        handle.write(reindent("""
256*67e74705SXin Li        |<h2>Analyzer Failures</h2>
257*67e74705SXin Li        |<p>The analyzer had problems processing the following files:</p>
258*67e74705SXin Li        |<table>
259*67e74705SXin Li        |  <thead>
260*67e74705SXin Li        |    <tr>
261*67e74705SXin Li        |      <td>Problem</td>
262*67e74705SXin Li        |      <td>Source File</td>
263*67e74705SXin Li        |      <td>Preprocessed File</td>
264*67e74705SXin Li        |      <td>STDERR Output</td>
265*67e74705SXin Li        |    </tr>
266*67e74705SXin Li        |  </thead>
267*67e74705SXin Li        |  <tbody>""", indent))
268*67e74705SXin Li        for current in crashes:
269*67e74705SXin Li            handle.write(reindent("""
270*67e74705SXin Li        |    <tr>
271*67e74705SXin Li        |      <td>{problem}</td>
272*67e74705SXin Li        |      <td>{source}</td>
273*67e74705SXin Li        |      <td><a href="{file}">preprocessor output</a></td>
274*67e74705SXin Li        |      <td><a href="{stderr}">analyzer std err</a></td>
275*67e74705SXin Li        |    </tr>""", indent).format(**current))
276*67e74705SXin Li            handle.write(comment('REPORTPROBLEM', current))
277*67e74705SXin Li        handle.write(reindent("""
278*67e74705SXin Li        |  </tbody>
279*67e74705SXin Li        |</table>""", indent))
280*67e74705SXin Li        handle.write(comment('REPORTCRASHES'))
281*67e74705SXin Li    return name
282*67e74705SXin Li
283*67e74705SXin Li
284*67e74705SXin Lidef read_crashes(output_dir):
285*67e74705SXin Li    """ Generate a unique sequence of crashes from given output directory. """
286*67e74705SXin Li
287*67e74705SXin Li    return (parse_crash(filename)
288*67e74705SXin Li            for filename in glob.iglob(os.path.join(output_dir, 'failures',
289*67e74705SXin Li                                                    '*.info.txt')))
290*67e74705SXin Li
291*67e74705SXin Li
292*67e74705SXin Lidef read_bugs(output_dir, html):
293*67e74705SXin Li    """ Generate a unique sequence of bugs from given output directory.
294*67e74705SXin Li
295*67e74705SXin Li    Duplicates can be in a project if the same module was compiled multiple
296*67e74705SXin Li    times with different compiler options. These would be better to show in
297*67e74705SXin Li    the final report (cover) only once. """
298*67e74705SXin Li
299*67e74705SXin Li    parser = parse_bug_html if html else parse_bug_plist
300*67e74705SXin Li    pattern = '*.html' if html else '*.plist'
301*67e74705SXin Li
302*67e74705SXin Li    duplicate = duplicate_check(
303*67e74705SXin Li        lambda bug: '{bug_line}.{bug_path_length}:{bug_file}'.format(**bug))
304*67e74705SXin Li
305*67e74705SXin Li    bugs = itertools.chain.from_iterable(
306*67e74705SXin Li        # parser creates a bug generator not the bug itself
307*67e74705SXin Li        parser(filename)
308*67e74705SXin Li        for filename in glob.iglob(os.path.join(output_dir, pattern)))
309*67e74705SXin Li
310*67e74705SXin Li    return (bug for bug in bugs if not duplicate(bug))
311*67e74705SXin Li
312*67e74705SXin Li
313*67e74705SXin Lidef parse_bug_plist(filename):
314*67e74705SXin Li    """ Returns the generator of bugs from a single .plist file. """
315*67e74705SXin Li
316*67e74705SXin Li    content = plistlib.readPlist(filename)
317*67e74705SXin Li    files = content.get('files')
318*67e74705SXin Li    for bug in content.get('diagnostics', []):
319*67e74705SXin Li        if len(files) <= int(bug['location']['file']):
320*67e74705SXin Li            logging.warning('Parsing bug from "%s" failed', filename)
321*67e74705SXin Li            continue
322*67e74705SXin Li
323*67e74705SXin Li        yield {
324*67e74705SXin Li            'result': filename,
325*67e74705SXin Li            'bug_type': bug['type'],
326*67e74705SXin Li            'bug_category': bug['category'],
327*67e74705SXin Li            'bug_line': int(bug['location']['line']),
328*67e74705SXin Li            'bug_path_length': int(bug['location']['col']),
329*67e74705SXin Li            'bug_file': files[int(bug['location']['file'])]
330*67e74705SXin Li        }
331*67e74705SXin Li
332*67e74705SXin Li
333*67e74705SXin Lidef parse_bug_html(filename):
334*67e74705SXin Li    """ Parse out the bug information from HTML output. """
335*67e74705SXin Li
336*67e74705SXin Li    patterns = [re.compile(r'<!-- BUGTYPE (?P<bug_type>.*) -->$'),
337*67e74705SXin Li                re.compile(r'<!-- BUGFILE (?P<bug_file>.*) -->$'),
338*67e74705SXin Li                re.compile(r'<!-- BUGPATHLENGTH (?P<bug_path_length>.*) -->$'),
339*67e74705SXin Li                re.compile(r'<!-- BUGLINE (?P<bug_line>.*) -->$'),
340*67e74705SXin Li                re.compile(r'<!-- BUGCATEGORY (?P<bug_category>.*) -->$'),
341*67e74705SXin Li                re.compile(r'<!-- BUGDESC (?P<bug_description>.*) -->$'),
342*67e74705SXin Li                re.compile(r'<!-- FUNCTIONNAME (?P<bug_function>.*) -->$')]
343*67e74705SXin Li    endsign = re.compile(r'<!-- BUGMETAEND -->')
344*67e74705SXin Li
345*67e74705SXin Li    bug = {
346*67e74705SXin Li        'report_file': filename,
347*67e74705SXin Li        'bug_function': 'n/a',  # compatibility with < clang-3.5
348*67e74705SXin Li        'bug_category': 'Other',
349*67e74705SXin Li        'bug_line': 0,
350*67e74705SXin Li        'bug_path_length': 1
351*67e74705SXin Li    }
352*67e74705SXin Li
353*67e74705SXin Li    with open(filename) as handler:
354*67e74705SXin Li        for line in handler.readlines():
355*67e74705SXin Li            # do not read the file further
356*67e74705SXin Li            if endsign.match(line):
357*67e74705SXin Li                break
358*67e74705SXin Li            # search for the right lines
359*67e74705SXin Li            for regex in patterns:
360*67e74705SXin Li                match = regex.match(line.strip())
361*67e74705SXin Li                if match:
362*67e74705SXin Li                    bug.update(match.groupdict())
363*67e74705SXin Li                    break
364*67e74705SXin Li
365*67e74705SXin Li    encode_value(bug, 'bug_line', int)
366*67e74705SXin Li    encode_value(bug, 'bug_path_length', int)
367*67e74705SXin Li
368*67e74705SXin Li    yield bug
369*67e74705SXin Li
370*67e74705SXin Li
371*67e74705SXin Lidef parse_crash(filename):
372*67e74705SXin Li    """ Parse out the crash information from the report file. """
373*67e74705SXin Li
374*67e74705SXin Li    match = re.match(r'(.*)\.info\.txt', filename)
375*67e74705SXin Li    name = match.group(1) if match else None
376*67e74705SXin Li    with open(filename) as handler:
377*67e74705SXin Li        lines = handler.readlines()
378*67e74705SXin Li        return {
379*67e74705SXin Li            'source': lines[0].rstrip(),
380*67e74705SXin Li            'problem': lines[1].rstrip(),
381*67e74705SXin Li            'file': name,
382*67e74705SXin Li            'info': name + '.info.txt',
383*67e74705SXin Li            'stderr': name + '.stderr.txt'
384*67e74705SXin Li        }
385*67e74705SXin Li
386*67e74705SXin Li
387*67e74705SXin Lidef category_type_name(bug):
388*67e74705SXin Li    """ Create a new bug attribute from bug by category and type.
389*67e74705SXin Li
390*67e74705SXin Li    The result will be used as CSS class selector in the final report. """
391*67e74705SXin Li
392*67e74705SXin Li    def smash(key):
393*67e74705SXin Li        """ Make value ready to be HTML attribute value. """
394*67e74705SXin Li
395*67e74705SXin Li        return bug.get(key, '').lower().replace(' ', '_').replace("'", '')
396*67e74705SXin Li
397*67e74705SXin Li    return escape('bt_' + smash('bug_category') + '_' + smash('bug_type'))
398*67e74705SXin Li
399*67e74705SXin Li
400*67e74705SXin Lidef create_counters():
401*67e74705SXin Li    """ Create counters for bug statistics.
402*67e74705SXin Li
403*67e74705SXin Li    Two entries are maintained: 'total' is an integer, represents the
404*67e74705SXin Li    number of bugs. The 'categories' is a two level categorisation of bug
405*67e74705SXin Li    counters. The first level is 'bug category' the second is 'bug type'.
406*67e74705SXin Li    Each entry in this classification is a dictionary of 'count', 'type'
407*67e74705SXin Li    and 'label'. """
408*67e74705SXin Li
409*67e74705SXin Li    def predicate(bug):
410*67e74705SXin Li        bug_category = bug['bug_category']
411*67e74705SXin Li        bug_type = bug['bug_type']
412*67e74705SXin Li        current_category = predicate.categories.get(bug_category, dict())
413*67e74705SXin Li        current_type = current_category.get(bug_type, {
414*67e74705SXin Li            'bug_type': bug_type,
415*67e74705SXin Li            'bug_type_class': category_type_name(bug),
416*67e74705SXin Li            'bug_count': 0
417*67e74705SXin Li        })
418*67e74705SXin Li        current_type.update({'bug_count': current_type['bug_count'] + 1})
419*67e74705SXin Li        current_category.update({bug_type: current_type})
420*67e74705SXin Li        predicate.categories.update({bug_category: current_category})
421*67e74705SXin Li        predicate.total += 1
422*67e74705SXin Li
423*67e74705SXin Li    predicate.total = 0
424*67e74705SXin Li    predicate.categories = dict()
425*67e74705SXin Li    return predicate
426*67e74705SXin Li
427*67e74705SXin Li
428*67e74705SXin Lidef prettify_bug(prefix, output_dir):
429*67e74705SXin Li    def predicate(bug):
430*67e74705SXin Li        """ Make safe this values to embed into HTML. """
431*67e74705SXin Li
432*67e74705SXin Li        bug['bug_type_class'] = category_type_name(bug)
433*67e74705SXin Li
434*67e74705SXin Li        encode_value(bug, 'bug_file', lambda x: escape(chop(prefix, x)))
435*67e74705SXin Li        encode_value(bug, 'bug_category', escape)
436*67e74705SXin Li        encode_value(bug, 'bug_type', escape)
437*67e74705SXin Li        encode_value(bug, 'report_file', lambda x: escape(chop(output_dir, x)))
438*67e74705SXin Li        return bug
439*67e74705SXin Li
440*67e74705SXin Li    return predicate
441*67e74705SXin Li
442*67e74705SXin Li
443*67e74705SXin Lidef prettify_crash(prefix, output_dir):
444*67e74705SXin Li    def predicate(crash):
445*67e74705SXin Li        """ Make safe this values to embed into HTML. """
446*67e74705SXin Li
447*67e74705SXin Li        encode_value(crash, 'source', lambda x: escape(chop(prefix, x)))
448*67e74705SXin Li        encode_value(crash, 'problem', escape)
449*67e74705SXin Li        encode_value(crash, 'file', lambda x: escape(chop(output_dir, x)))
450*67e74705SXin Li        encode_value(crash, 'info', lambda x: escape(chop(output_dir, x)))
451*67e74705SXin Li        encode_value(crash, 'stderr', lambda x: escape(chop(output_dir, x)))
452*67e74705SXin Li        return crash
453*67e74705SXin Li
454*67e74705SXin Li    return predicate
455*67e74705SXin Li
456*67e74705SXin Li
457*67e74705SXin Lidef copy_resource_files(output_dir):
458*67e74705SXin Li    """ Copy the javascript and css files to the report directory. """
459*67e74705SXin Li
460*67e74705SXin Li    this_dir = os.path.dirname(os.path.realpath(__file__))
461*67e74705SXin Li    for resource in os.listdir(os.path.join(this_dir, 'resources')):
462*67e74705SXin Li        shutil.copy(os.path.join(this_dir, 'resources', resource), output_dir)
463*67e74705SXin Li
464*67e74705SXin Li
465*67e74705SXin Lidef encode_value(container, key, encode):
466*67e74705SXin Li    """ Run 'encode' on 'container[key]' value and update it. """
467*67e74705SXin Li
468*67e74705SXin Li    if key in container:
469*67e74705SXin Li        value = encode(container[key])
470*67e74705SXin Li        container.update({key: value})
471*67e74705SXin Li
472*67e74705SXin Li
473*67e74705SXin Lidef chop(prefix, filename):
474*67e74705SXin Li    """ Create 'filename' from '/prefix/filename' """
475*67e74705SXin Li
476*67e74705SXin Li    return filename if not len(prefix) else os.path.relpath(filename, prefix)
477*67e74705SXin Li
478*67e74705SXin Li
479*67e74705SXin Lidef escape(text):
480*67e74705SXin Li    """ Paranoid HTML escape method. (Python version independent) """
481*67e74705SXin Li
482*67e74705SXin Li    escape_table = {
483*67e74705SXin Li        '&': '&amp;',
484*67e74705SXin Li        '"': '&quot;',
485*67e74705SXin Li        "'": '&apos;',
486*67e74705SXin Li        '>': '&gt;',
487*67e74705SXin Li        '<': '&lt;'
488*67e74705SXin Li    }
489*67e74705SXin Li    return ''.join(escape_table.get(c, c) for c in text)
490*67e74705SXin Li
491*67e74705SXin Li
492*67e74705SXin Lidef reindent(text, indent):
493*67e74705SXin Li    """ Utility function to format html output and keep indentation. """
494*67e74705SXin Li
495*67e74705SXin Li    result = ''
496*67e74705SXin Li    for line in text.splitlines():
497*67e74705SXin Li        if len(line.strip()):
498*67e74705SXin Li            result += ' ' * indent + line.split('|')[1] + os.linesep
499*67e74705SXin Li    return result
500*67e74705SXin Li
501*67e74705SXin Li
502*67e74705SXin Lidef comment(name, opts=dict()):
503*67e74705SXin Li    """ Utility function to format meta information as comment. """
504*67e74705SXin Li
505*67e74705SXin Li    attributes = ''
506*67e74705SXin Li    for key, value in opts.items():
507*67e74705SXin Li        attributes += ' {0}="{1}"'.format(key, value)
508*67e74705SXin Li
509*67e74705SXin Li    return '<!-- {0}{1} -->{2}'.format(name, attributes, os.linesep)
510*67e74705SXin Li
511*67e74705SXin Li
512*67e74705SXin Lidef commonprefix_from(filename):
513*67e74705SXin Li    """ Create file prefix from a compilation database entries. """
514*67e74705SXin Li
515*67e74705SXin Li    with open(filename, 'r') as handle:
516*67e74705SXin Li        return commonprefix(item['file'] for item in json.load(handle))
517*67e74705SXin Li
518*67e74705SXin Li
519*67e74705SXin Lidef commonprefix(files):
520*67e74705SXin Li    """ Fixed version of os.path.commonprefix. Return the longest path prefix
521*67e74705SXin Li    that is a prefix of all paths in filenames. """
522*67e74705SXin Li
523*67e74705SXin Li    result = None
524*67e74705SXin Li    for current in files:
525*67e74705SXin Li        if result is not None:
526*67e74705SXin Li            result = os.path.commonprefix([result, current])
527*67e74705SXin Li        else:
528*67e74705SXin Li            result = current
529*67e74705SXin Li
530*67e74705SXin Li    if result is None:
531*67e74705SXin Li        return ''
532*67e74705SXin Li    elif not os.path.isdir(result):
533*67e74705SXin Li        return os.path.dirname(result)
534*67e74705SXin Li    else:
535*67e74705SXin Li        return os.path.abspath(result)
536