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