D9600: Add scan-build python implementation
llvm-svn: 257533
diff --git a/clang/tools/scan-build-py/libscanbuild/report.py b/clang/tools/scan-build-py/libscanbuild/report.py
new file mode 100644
index 0000000..efc0a55
--- /dev/null
+++ b/clang/tools/scan-build-py/libscanbuild/report.py
@@ -0,0 +1,530 @@
+# -*- coding: utf-8 -*-
+# The LLVM Compiler Infrastructure
+#
+# This file is distributed under the University of Illinois Open Source
+# License. See LICENSE.TXT for details.
+""" This module is responsible to generate 'index.html' for the report.
+
+The input for this step is the output directory, where individual reports
+could be found. It parses those reports and generates 'index.html'. """
+
+import re
+import os
+import os.path
+import sys
+import shutil
+import time
+import tempfile
+import itertools
+import plistlib
+import glob
+import json
+import logging
+import contextlib
+from libscanbuild import duplicate_check
+from libscanbuild.clang import get_version
+
+__all__ = ['report_directory', 'document']
+
+
+@contextlib.contextmanager
+def report_directory(hint, keep):
+ """ Responsible for the report directory.
+
+ hint -- could specify the parent directory of the output directory.
+ keep -- a boolean value to keep or delete the empty report directory. """
+
+ stamp = time.strftime('scan-build-%Y-%m-%d-%H%M%S-', time.localtime())
+ name = tempfile.mkdtemp(prefix=stamp, dir=hint)
+
+ logging.info('Report directory created: %s', name)
+
+ try:
+ yield name
+ finally:
+ if os.listdir(name):
+ msg = "Run 'scan-view %s' to examine bug reports."
+ keep = True
+ else:
+ if keep:
+ msg = "Report directory '%s' contans no report, but kept."
+ else:
+ msg = "Removing directory '%s' because it contains no report."
+ logging.warning(msg, name)
+
+ if not keep:
+ os.rmdir(name)
+
+
+def document(args, output_dir, use_cdb):
+ """ Generates cover report and returns the number of bugs/crashes. """
+
+ html_reports_available = args.output_format in {'html', 'plist-html'}
+
+ logging.debug('count crashes and bugs')
+ crash_count = sum(1 for _ in read_crashes(output_dir))
+ bug_counter = create_counters()
+ for bug in read_bugs(output_dir, html_reports_available):
+ bug_counter(bug)
+ result = crash_count + bug_counter.total
+
+ if html_reports_available and result:
+ logging.debug('generate index.html file')
+ # common prefix for source files to have sort filenames
+ prefix = commonprefix_from(args.cdb) if use_cdb else os.getcwd()
+ # assemble the cover from multiple fragments
+ try:
+ fragments = []
+ if bug_counter.total:
+ fragments.append(bug_summary(output_dir, bug_counter))
+ fragments.append(bug_report(output_dir, prefix))
+ if crash_count:
+ fragments.append(crash_report(output_dir, prefix))
+ assemble_cover(output_dir, prefix, args, fragments)
+ # copy additinal files to the report
+ copy_resource_files(output_dir)
+ if use_cdb:
+ shutil.copy(args.cdb, output_dir)
+ finally:
+ for fragment in fragments:
+ os.remove(fragment)
+ return result
+
+
+def assemble_cover(output_dir, prefix, args, fragments):
+ """ Put together the fragments into a final report. """
+
+ import getpass
+ import socket
+ import datetime
+
+ if args.html_title is None:
+ args.html_title = os.path.basename(prefix) + ' - analyzer results'
+
+ with open(os.path.join(output_dir, 'index.html'), 'w') as handle:
+ indent = 0
+ handle.write(reindent("""
+ |<!DOCTYPE html>
+ |<html>
+ | <head>
+ | <title>{html_title}</title>
+ | <link type="text/css" rel="stylesheet" href="scanview.css"/>
+ | <script type='text/javascript' src="sorttable.js"></script>
+ | <script type='text/javascript' src='selectable.js'></script>
+ | </head>""", indent).format(html_title=args.html_title))
+ handle.write(comment('SUMMARYENDHEAD'))
+ handle.write(reindent("""
+ | <body>
+ | <h1>{html_title}</h1>
+ | <table>
+ | <tr><th>User:</th><td>{user_name}@{host_name}</td></tr>
+ | <tr><th>Working Directory:</th><td>{current_dir}</td></tr>
+ | <tr><th>Command Line:</th><td>{cmd_args}</td></tr>
+ | <tr><th>Clang Version:</th><td>{clang_version}</td></tr>
+ | <tr><th>Date:</th><td>{date}</td></tr>
+ | </table>""", indent).format(html_title=args.html_title,
+ user_name=getpass.getuser(),
+ host_name=socket.gethostname(),
+ current_dir=prefix,
+ cmd_args=' '.join(sys.argv),
+ clang_version=get_version(args.clang),
+ date=datetime.datetime.today(
+ ).strftime('%c')))
+ for fragment in fragments:
+ # copy the content of fragments
+ with open(fragment, 'r') as input_handle:
+ shutil.copyfileobj(input_handle, handle)
+ handle.write(reindent("""
+ | </body>
+ |</html>""", indent))
+
+
+def bug_summary(output_dir, bug_counter):
+ """ Bug summary is a HTML table to give a better overview of the bugs. """
+
+ name = os.path.join(output_dir, 'summary.html.fragment')
+ with open(name, 'w') as handle:
+ indent = 4
+ handle.write(reindent("""
+ |<h2>Bug Summary</h2>
+ |<table>
+ | <thead>
+ | <tr>
+ | <td>Bug Type</td>
+ | <td>Quantity</td>
+ | <td class="sorttable_nosort">Display?</td>
+ | </tr>
+ | </thead>
+ | <tbody>""", indent))
+ handle.write(reindent("""
+ | <tr style="font-weight:bold">
+ | <td class="SUMM_DESC">All Bugs</td>
+ | <td class="Q">{0}</td>
+ | <td>
+ | <center>
+ | <input checked type="checkbox" id="AllBugsCheck"
+ | onClick="CopyCheckedStateToCheckButtons(this);"/>
+ | </center>
+ | </td>
+ | </tr>""", indent).format(bug_counter.total))
+ for category, types in bug_counter.categories.items():
+ handle.write(reindent("""
+ | <tr>
+ | <th>{0}</th><th colspan=2></th>
+ | </tr>""", indent).format(category))
+ for bug_type in types.values():
+ handle.write(reindent("""
+ | <tr>
+ | <td class="SUMM_DESC">{bug_type}</td>
+ | <td class="Q">{bug_count}</td>
+ | <td>
+ | <center>
+ | <input checked type="checkbox"
+ | onClick="ToggleDisplay(this,'{bug_type_class}');"/>
+ | </center>
+ | </td>
+ | </tr>""", indent).format(**bug_type))
+ handle.write(reindent("""
+ | </tbody>
+ |</table>""", indent))
+ handle.write(comment('SUMMARYBUGEND'))
+ return name
+
+
+def bug_report(output_dir, prefix):
+ """ Creates a fragment from the analyzer reports. """
+
+ pretty = prettify_bug(prefix, output_dir)
+ bugs = (pretty(bug) for bug in read_bugs(output_dir, True))
+
+ name = os.path.join(output_dir, 'bugs.html.fragment')
+ with open(name, 'w') as handle:
+ indent = 4
+ handle.write(reindent("""
+ |<h2>Reports</h2>
+ |<table class="sortable" style="table-layout:automatic">
+ | <thead>
+ | <tr>
+ | <td>Bug Group</td>
+ | <td class="sorttable_sorted">
+ | Bug Type
+ | <span id="sorttable_sortfwdind"> ▾</span>
+ | </td>
+ | <td>File</td>
+ | <td>Function/Method</td>
+ | <td class="Q">Line</td>
+ | <td class="Q">Path Length</td>
+ | <td class="sorttable_nosort"></td>
+ | </tr>
+ | </thead>
+ | <tbody>""", indent))
+ handle.write(comment('REPORTBUGCOL'))
+ for current in bugs:
+ handle.write(reindent("""
+ | <tr class="{bug_type_class}">
+ | <td class="DESC">{bug_category}</td>
+ | <td class="DESC">{bug_type}</td>
+ | <td>{bug_file}</td>
+ | <td class="DESC">{bug_function}</td>
+ | <td class="Q">{bug_line}</td>
+ | <td class="Q">{bug_path_length}</td>
+ | <td><a href="{report_file}#EndPath">View Report</a></td>
+ | </tr>""", indent).format(**current))
+ handle.write(comment('REPORTBUG', {'id': current['report_file']}))
+ handle.write(reindent("""
+ | </tbody>
+ |</table>""", indent))
+ handle.write(comment('REPORTBUGEND'))
+ return name
+
+
+def crash_report(output_dir, prefix):
+ """ Creates a fragment from the compiler crashes. """
+
+ pretty = prettify_crash(prefix, output_dir)
+ crashes = (pretty(crash) for crash in read_crashes(output_dir))
+
+ name = os.path.join(output_dir, 'crashes.html.fragment')
+ with open(name, 'w') as handle:
+ indent = 4
+ handle.write(reindent("""
+ |<h2>Analyzer Failures</h2>
+ |<p>The analyzer had problems processing the following files:</p>
+ |<table>
+ | <thead>
+ | <tr>
+ | <td>Problem</td>
+ | <td>Source File</td>
+ | <td>Preprocessed File</td>
+ | <td>STDERR Output</td>
+ | </tr>
+ | </thead>
+ | <tbody>""", indent))
+ for current in crashes:
+ handle.write(reindent("""
+ | <tr>
+ | <td>{problem}</td>
+ | <td>{source}</td>
+ | <td><a href="{file}">preprocessor output</a></td>
+ | <td><a href="{stderr}">analyzer std err</a></td>
+ | </tr>""", indent).format(**current))
+ handle.write(comment('REPORTPROBLEM', current))
+ handle.write(reindent("""
+ | </tbody>
+ |</table>""", indent))
+ handle.write(comment('REPORTCRASHES'))
+ return name
+
+
+def read_crashes(output_dir):
+ """ Generate a unique sequence of crashes from given output directory. """
+
+ return (parse_crash(filename)
+ for filename in glob.iglob(os.path.join(output_dir, 'failures',
+ '*.info.txt')))
+
+
+def read_bugs(output_dir, html):
+ """ Generate a unique sequence of bugs from given output directory.
+
+ Duplicates can be in a project if the same module was compiled multiple
+ times with different compiler options. These would be better to show in
+ the final report (cover) only once. """
+
+ parser = parse_bug_html if html else parse_bug_plist
+ pattern = '*.html' if html else '*.plist'
+
+ duplicate = duplicate_check(
+ lambda bug: '{bug_line}.{bug_path_length}:{bug_file}'.format(**bug))
+
+ bugs = itertools.chain.from_iterable(
+ # parser creates a bug generator not the bug itself
+ parser(filename)
+ for filename in glob.iglob(os.path.join(output_dir, pattern)))
+
+ return (bug for bug in bugs if not duplicate(bug))
+
+
+def parse_bug_plist(filename):
+ """ Returns the generator of bugs from a single .plist file. """
+
+ content = plistlib.readPlist(filename)
+ files = content.get('files')
+ for bug in content.get('diagnostics', []):
+ if len(files) <= int(bug['location']['file']):
+ logging.warning('Parsing bug from "%s" failed', filename)
+ continue
+
+ yield {
+ 'result': filename,
+ 'bug_type': bug['type'],
+ 'bug_category': bug['category'],
+ 'bug_line': int(bug['location']['line']),
+ 'bug_path_length': int(bug['location']['col']),
+ 'bug_file': files[int(bug['location']['file'])]
+ }
+
+
+def parse_bug_html(filename):
+ """ Parse out the bug information from HTML output. """
+
+ patterns = [re.compile(r'<!-- BUGTYPE (?P<bug_type>.*) -->$'),
+ re.compile(r'<!-- BUGFILE (?P<bug_file>.*) -->$'),
+ re.compile(r'<!-- BUGPATHLENGTH (?P<bug_path_length>.*) -->$'),
+ re.compile(r'<!-- BUGLINE (?P<bug_line>.*) -->$'),
+ re.compile(r'<!-- BUGCATEGORY (?P<bug_category>.*) -->$'),
+ re.compile(r'<!-- BUGDESC (?P<bug_description>.*) -->$'),
+ re.compile(r'<!-- FUNCTIONNAME (?P<bug_function>.*) -->$')]
+ endsign = re.compile(r'<!-- BUGMETAEND -->')
+
+ bug = {
+ 'report_file': filename,
+ 'bug_function': 'n/a', # compatibility with < clang-3.5
+ 'bug_category': 'Other',
+ 'bug_line': 0,
+ 'bug_path_length': 1
+ }
+
+ with open(filename) as handler:
+ for line in handler.readlines():
+ # do not read the file further
+ if endsign.match(line):
+ break
+ # search for the right lines
+ for regex in patterns:
+ match = regex.match(line.strip())
+ if match:
+ bug.update(match.groupdict())
+ break
+
+ encode_value(bug, 'bug_line', int)
+ encode_value(bug, 'bug_path_length', int)
+
+ yield bug
+
+
+def parse_crash(filename):
+ """ Parse out the crash information from the report file. """
+
+ match = re.match(r'(.*)\.info\.txt', filename)
+ name = match.group(1) if match else None
+ with open(filename) as handler:
+ lines = handler.readlines()
+ return {
+ 'source': lines[0].rstrip(),
+ 'problem': lines[1].rstrip(),
+ 'file': name,
+ 'info': name + '.info.txt',
+ 'stderr': name + '.stderr.txt'
+ }
+
+
+def category_type_name(bug):
+ """ Create a new bug attribute from bug by category and type.
+
+ The result will be used as CSS class selector in the final report. """
+
+ def smash(key):
+ """ Make value ready to be HTML attribute value. """
+
+ return bug.get(key, '').lower().replace(' ', '_').replace("'", '')
+
+ return escape('bt_' + smash('bug_category') + '_' + smash('bug_type'))
+
+
+def create_counters():
+ """ Create counters for bug statistics.
+
+ Two entries are maintained: 'total' is an integer, represents the
+ number of bugs. The 'categories' is a two level categorisation of bug
+ counters. The first level is 'bug category' the second is 'bug type'.
+ Each entry in this classification is a dictionary of 'count', 'type'
+ and 'label'. """
+
+ def predicate(bug):
+ bug_category = bug['bug_category']
+ bug_type = bug['bug_type']
+ current_category = predicate.categories.get(bug_category, dict())
+ current_type = current_category.get(bug_type, {
+ 'bug_type': bug_type,
+ 'bug_type_class': category_type_name(bug),
+ 'bug_count': 0
+ })
+ current_type.update({'bug_count': current_type['bug_count'] + 1})
+ current_category.update({bug_type: current_type})
+ predicate.categories.update({bug_category: current_category})
+ predicate.total += 1
+
+ predicate.total = 0
+ predicate.categories = dict()
+ return predicate
+
+
+def prettify_bug(prefix, output_dir):
+ def predicate(bug):
+ """ Make safe this values to embed into HTML. """
+
+ bug['bug_type_class'] = category_type_name(bug)
+
+ encode_value(bug, 'bug_file', lambda x: escape(chop(prefix, x)))
+ encode_value(bug, 'bug_category', escape)
+ encode_value(bug, 'bug_type', escape)
+ encode_value(bug, 'report_file', lambda x: escape(chop(output_dir, x)))
+ return bug
+
+ return predicate
+
+
+def prettify_crash(prefix, output_dir):
+ def predicate(crash):
+ """ Make safe this values to embed into HTML. """
+
+ encode_value(crash, 'source', lambda x: escape(chop(prefix, x)))
+ encode_value(crash, 'problem', escape)
+ encode_value(crash, 'file', lambda x: escape(chop(output_dir, x)))
+ encode_value(crash, 'info', lambda x: escape(chop(output_dir, x)))
+ encode_value(crash, 'stderr', lambda x: escape(chop(output_dir, x)))
+ return crash
+
+ return predicate
+
+
+def copy_resource_files(output_dir):
+ """ Copy the javascript and css files to the report directory. """
+
+ this_dir = os.path.dirname(os.path.realpath(__file__))
+ for resource in os.listdir(os.path.join(this_dir, 'resources')):
+ shutil.copy(os.path.join(this_dir, 'resources', resource), output_dir)
+
+
+def encode_value(container, key, encode):
+ """ Run 'encode' on 'container[key]' value and update it. """
+
+ if key in container:
+ value = encode(container[key])
+ container.update({key: value})
+
+
+def chop(prefix, filename):
+ """ Create 'filename' from '/prefix/filename' """
+
+ return filename if not len(prefix) else os.path.relpath(filename, prefix)
+
+
+def escape(text):
+ """ Paranoid HTML escape method. (Python version independent) """
+
+ escape_table = {
+ '&': '&',
+ '"': '"',
+ "'": ''',
+ '>': '>',
+ '<': '<'
+ }
+ return ''.join(escape_table.get(c, c) for c in text)
+
+
+def reindent(text, indent):
+ """ Utility function to format html output and keep indentation. """
+
+ result = ''
+ for line in text.splitlines():
+ if len(line.strip()):
+ result += ' ' * indent + line.split('|')[1] + os.linesep
+ return result
+
+
+def comment(name, opts=dict()):
+ """ Utility function to format meta information as comment. """
+
+ attributes = ''
+ for key, value in opts.items():
+ attributes += ' {0}="{1}"'.format(key, value)
+
+ return '<!-- {0}{1} -->{2}'.format(name, attributes, os.linesep)
+
+
+def commonprefix_from(filename):
+ """ Create file prefix from a compilation database entries. """
+
+ with open(filename, 'r') as handle:
+ return commonprefix(item['file'] for item in json.load(handle))
+
+
+def commonprefix(files):
+ """ Fixed version of os.path.commonprefix. Return the longest path prefix
+ that is a prefix of all paths in filenames. """
+
+ result = None
+ for current in files:
+ if result is not None:
+ result = os.path.commonprefix([result, current])
+ else:
+ result = current
+
+ if result is None:
+ return ''
+ elif not os.path.isdir(result):
+ return os.path.dirname(result)
+ else:
+ return os.path.abspath(result)