diff options
Diffstat (limited to 'tools/scan-build-py/libscanbuild')
-rw-r--r-- | tools/scan-build-py/libscanbuild/__init__.py | 82 | ||||
-rw-r--r-- | tools/scan-build-py/libscanbuild/analyze.py | 502 | ||||
-rw-r--r-- | tools/scan-build-py/libscanbuild/clang.py | 156 | ||||
-rw-r--r-- | tools/scan-build-py/libscanbuild/command.py | 133 | ||||
-rw-r--r-- | tools/scan-build-py/libscanbuild/intercept.py | 359 | ||||
-rw-r--r-- | tools/scan-build-py/libscanbuild/report.py | 530 | ||||
-rw-r--r-- | tools/scan-build-py/libscanbuild/resources/scanview.css | 62 | ||||
-rw-r--r-- | tools/scan-build-py/libscanbuild/resources/selectable.js | 47 | ||||
-rw-r--r-- | tools/scan-build-py/libscanbuild/resources/sorttable.js | 492 | ||||
-rw-r--r-- | tools/scan-build-py/libscanbuild/runner.py | 256 | ||||
-rw-r--r-- | tools/scan-build-py/libscanbuild/shell.py | 66 |
11 files changed, 2685 insertions, 0 deletions
diff --git a/tools/scan-build-py/libscanbuild/__init__.py b/tools/scan-build-py/libscanbuild/__init__.py new file mode 100644 index 0000000000..c020b4e434 --- /dev/null +++ b/tools/scan-build-py/libscanbuild/__init__.py @@ -0,0 +1,82 @@ +# -*- 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 responsible to run the Clang static analyzer against any build +and generate reports. +""" + + +def duplicate_check(method): + """ Predicate to detect duplicated entries. + + Unique hash method can be use to detect duplicates. Entries are + represented as dictionaries, which has no default hash method. + This implementation uses a set datatype to store the unique hash values. + + This method returns a method which can detect the duplicate values. """ + + def predicate(entry): + entry_hash = predicate.unique(entry) + if entry_hash not in predicate.state: + predicate.state.add(entry_hash) + return False + return True + + predicate.unique = method + predicate.state = set() + return predicate + + +def tempdir(): + """ Return the default temorary directory. """ + + from os import getenv + return getenv('TMPDIR', getenv('TEMP', getenv('TMP', '/tmp'))) + + +def initialize_logging(verbose_level): + """ Output content controlled by the verbosity level. """ + + import sys + import os.path + import logging + level = logging.WARNING - min(logging.WARNING, (10 * verbose_level)) + + if verbose_level <= 3: + fmt_string = '{0}: %(levelname)s: %(message)s' + else: + fmt_string = '{0}: %(levelname)s: %(funcName)s: %(message)s' + + program = os.path.basename(sys.argv[0]) + logging.basicConfig(format=fmt_string.format(program), level=level) + + +def command_entry_point(function): + """ Decorator for command entry points. """ + + import functools + import logging + + @functools.wraps(function) + def wrapper(*args, **kwargs): + + exit_code = 127 + try: + exit_code = function(*args, **kwargs) + except KeyboardInterrupt: + logging.warning('Keyboard interupt') + except Exception: + logging.exception('Internal error.') + if logging.getLogger().isEnabledFor(logging.DEBUG): + logging.error("Please report this bug and attach the output " + "to the bug report") + else: + logging.error("Please run this command again and turn on " + "verbose mode (add '-vvv' as argument).") + finally: + return exit_code + + return wrapper diff --git a/tools/scan-build-py/libscanbuild/analyze.py b/tools/scan-build-py/libscanbuild/analyze.py new file mode 100644 index 0000000000..0d3547befe --- /dev/null +++ b/tools/scan-build-py/libscanbuild/analyze.py @@ -0,0 +1,502 @@ +# -*- 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 implements the 'scan-build' command API. + +To run the static analyzer against a build is done in multiple steps: + + -- Intercept: capture the compilation command during the build, + -- Analyze: run the analyzer against the captured commands, + -- Report: create a cover report from the analyzer outputs. """ + +import sys +import re +import os +import os.path +import json +import argparse +import logging +import subprocess +import multiprocessing +from libscanbuild import initialize_logging, tempdir, command_entry_point +from libscanbuild.runner import run +from libscanbuild.intercept import capture +from libscanbuild.report import report_directory, document +from libscanbuild.clang import get_checkers +from libscanbuild.runner import action_check +from libscanbuild.command import classify_parameters, classify_source + +__all__ = ['analyze_build_main', 'analyze_build_wrapper'] + +COMPILER_WRAPPER_CC = 'analyze-cc' +COMPILER_WRAPPER_CXX = 'analyze-c++' + + +@command_entry_point +def analyze_build_main(bin_dir, from_build_command): + """ Entry point for 'analyze-build' and 'scan-build'. """ + + parser = create_parser(from_build_command) + args = parser.parse_args() + validate(parser, args, from_build_command) + + # setup logging + initialize_logging(args.verbose) + logging.debug('Parsed arguments: %s', args) + + with report_directory(args.output, args.keep_empty) as target_dir: + if not from_build_command: + # run analyzer only and generate cover report + run_analyzer(args, target_dir) + number_of_bugs = document(args, target_dir, True) + return number_of_bugs if args.status_bugs else 0 + elif args.intercept_first: + # run build command and capture compiler executions + exit_code = capture(args, bin_dir) + # next step to run the analyzer against the captured commands + if need_analyzer(args.build): + run_analyzer(args, target_dir) + # cover report generation and bug counting + number_of_bugs = document(args, target_dir, True) + # remove the compilation database when it was not requested + if os.path.exists(args.cdb): + os.unlink(args.cdb) + # set exit status as it was requested + return number_of_bugs if args.status_bugs else exit_code + else: + return exit_code + else: + # run the build command with compiler wrappers which + # execute the analyzer too. (interposition) + environment = setup_environment(args, target_dir, bin_dir) + logging.debug('run build in environment: %s', environment) + exit_code = subprocess.call(args.build, env=environment) + logging.debug('build finished with exit code: %d', exit_code) + # cover report generation and bug counting + number_of_bugs = document(args, target_dir, False) + # set exit status as it was requested + return number_of_bugs if args.status_bugs else exit_code + + +def need_analyzer(args): + """ Check the intent of the build command. + + When static analyzer run against project configure step, it should be + silent and no need to run the analyzer or generate report. + + To run `scan-build` against the configure step might be neccessary, + when compiler wrappers are used. That's the moment when build setup + check the compiler and capture the location for the build process. """ + + return len(args) and not re.search('configure|autogen', args[0]) + + +def run_analyzer(args, output_dir): + """ Runs the analyzer against the given compilation database. """ + + def exclude(filename): + """ Return true when any excluded directory prefix the filename. """ + return any(re.match(r'^' + directory, filename) + for directory in args.excludes) + + consts = { + 'clang': args.clang, + 'output_dir': output_dir, + 'output_format': args.output_format, + 'output_failures': args.output_failures, + 'direct_args': analyzer_params(args) + } + + logging.debug('run analyzer against compilation database') + with open(args.cdb, 'r') as handle: + generator = (dict(cmd, **consts) + for cmd in json.load(handle) if not exclude(cmd['file'])) + # when verbose output requested execute sequentially + pool = multiprocessing.Pool(1 if args.verbose > 2 else None) + for current in pool.imap_unordered(run, generator): + if current is not None: + # display error message from the static analyzer + for line in current['error_output']: + logging.info(line.rstrip()) + pool.close() + pool.join() + + +def setup_environment(args, destination, bin_dir): + """ Set up environment for build command to interpose compiler wrapper. """ + + environment = dict(os.environ) + environment.update({ + 'CC': os.path.join(bin_dir, COMPILER_WRAPPER_CC), + 'CXX': os.path.join(bin_dir, COMPILER_WRAPPER_CXX), + 'ANALYZE_BUILD_CC': args.cc, + 'ANALYZE_BUILD_CXX': args.cxx, + 'ANALYZE_BUILD_CLANG': args.clang if need_analyzer(args.build) else '', + 'ANALYZE_BUILD_VERBOSE': 'DEBUG' if args.verbose > 2 else 'WARNING', + 'ANALYZE_BUILD_REPORT_DIR': destination, + 'ANALYZE_BUILD_REPORT_FORMAT': args.output_format, + 'ANALYZE_BUILD_REPORT_FAILURES': 'yes' if args.output_failures else '', + 'ANALYZE_BUILD_PARAMETERS': ' '.join(analyzer_params(args)) + }) + return environment + + +def analyze_build_wrapper(cplusplus): + """ Entry point for `analyze-cc` and `analyze-c++` compiler wrappers. """ + + # initialize wrapper logging + logging.basicConfig(format='analyze: %(levelname)s: %(message)s', + level=os.getenv('ANALYZE_BUILD_VERBOSE', 'INFO')) + # execute with real compiler + compiler = os.getenv('ANALYZE_BUILD_CXX', 'c++') if cplusplus \ + else os.getenv('ANALYZE_BUILD_CC', 'cc') + compilation = [compiler] + sys.argv[1:] + logging.info('execute compiler: %s', compilation) + result = subprocess.call(compilation) + # exit when it fails, ... + if result or not os.getenv('ANALYZE_BUILD_CLANG'): + return result + # ... and run the analyzer if all went well. + try: + # collect the needed parameters from environment, crash when missing + consts = { + 'clang': os.getenv('ANALYZE_BUILD_CLANG'), + 'output_dir': os.getenv('ANALYZE_BUILD_REPORT_DIR'), + 'output_format': os.getenv('ANALYZE_BUILD_REPORT_FORMAT'), + 'output_failures': os.getenv('ANALYZE_BUILD_REPORT_FAILURES'), + 'direct_args': os.getenv('ANALYZE_BUILD_PARAMETERS', + '').split(' '), + 'directory': os.getcwd(), + } + # get relevant parameters from command line arguments + args = classify_parameters(sys.argv) + filenames = args.pop('files', []) + for filename in (name for name in filenames if classify_source(name)): + parameters = dict(args, file=filename, **consts) + logging.debug('analyzer parameters %s', parameters) + current = action_check(parameters) + # display error message from the static analyzer + if current is not None: + for line in current['error_output']: + logging.info(line.rstrip()) + except Exception: + logging.exception("run analyzer inside compiler wrapper failed.") + return 0 + + +def analyzer_params(args): + """ A group of command line arguments can mapped to command + line arguments of the analyzer. This method generates those. """ + + def prefix_with(constant, pieces): + """ From a sequence create another sequence where every second element + is from the original sequence and the odd elements are the prefix. + + eg.: prefix_with(0, [1,2,3]) creates [0, 1, 0, 2, 0, 3] """ + + return [elem for piece in pieces for elem in [constant, piece]] + + result = [] + + if args.store_model: + result.append('-analyzer-store={0}'.format(args.store_model)) + if args.constraints_model: + result.append( + '-analyzer-constraints={0}'.format(args.constraints_model)) + if args.internal_stats: + result.append('-analyzer-stats') + if args.analyze_headers: + result.append('-analyzer-opt-analyze-headers') + if args.stats: + result.append('-analyzer-checker=debug.Stats') + if args.maxloop: + result.extend(['-analyzer-max-loop', str(args.maxloop)]) + if args.output_format: + result.append('-analyzer-output={0}'.format(args.output_format)) + if args.analyzer_config: + result.append(args.analyzer_config) + if args.verbose >= 4: + result.append('-analyzer-display-progress') + if args.plugins: + result.extend(prefix_with('-load', args.plugins)) + if args.enable_checker: + checkers = ','.join(args.enable_checker) + result.extend(['-analyzer-checker', checkers]) + if args.disable_checker: + checkers = ','.join(args.disable_checker) + result.extend(['-analyzer-disable-checker', checkers]) + if os.getenv('UBIVIZ'): + result.append('-analyzer-viz-egraph-ubigraph') + + return prefix_with('-Xclang', result) + + +def print_active_checkers(checkers): + """ Print active checkers to stdout. """ + + for name in sorted(name for name, (_, active) in checkers.items() + if active): + print(name) + + +def print_checkers(checkers): + """ Print verbose checker help to stdout. """ + + print('') + print('available checkers:') + print('') + for name in sorted(checkers.keys()): + description, active = checkers[name] + prefix = '+' if active else ' ' + if len(name) > 30: + print(' {0} {1}'.format(prefix, name)) + print(' ' * 35 + description) + else: + print(' {0} {1: <30} {2}'.format(prefix, name, description)) + print('') + print('NOTE: "+" indicates that an analysis is enabled by default.') + print('') + + +def validate(parser, args, from_build_command): + """ Validation done by the parser itself, but semantic check still + needs to be done. This method is doing that. """ + + if args.help_checkers_verbose: + print_checkers(get_checkers(args.clang, args.plugins)) + parser.exit() + elif args.help_checkers: + print_active_checkers(get_checkers(args.clang, args.plugins)) + parser.exit() + + if from_build_command and not args.build: + parser.error('missing build command') + + +def create_parser(from_build_command): + """ Command line argument parser factory method. """ + + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument( + '--verbose', '-v', + action='count', + default=0, + help="""Enable verbose output from '%(prog)s'. A second and third + flag increases verbosity.""") + parser.add_argument( + '--override-compiler', + action='store_true', + help="""Always resort to the compiler wrapper even when better + interposition methods are available.""") + parser.add_argument( + '--intercept-first', + action='store_true', + help="""Run the build commands only, build a compilation database, + then run the static analyzer afterwards. + Generally speaking it has better coverage on build commands. + With '--override-compiler' it use compiler wrapper, but does + not run the analyzer till the build is finished. """) + parser.add_argument( + '--cdb', + metavar='<file>', + default="compile_commands.json", + help="""The JSON compilation database.""") + + parser.add_argument( + '--output', '-o', + metavar='<path>', + default=tempdir(), + help="""Specifies the output directory for analyzer reports. + Subdirectory will be created if default directory is targeted. + """) + parser.add_argument( + '--status-bugs', + action='store_true', + help="""By default, the exit status of '%(prog)s' is the same as the + executed build command. Specifying this option causes the exit + status of '%(prog)s' to be non zero if it found potential bugs + and zero otherwise.""") + parser.add_argument( + '--html-title', + metavar='<title>', + help="""Specify the title used on generated HTML pages. + If not specified, a default title will be used.""") + parser.add_argument( + '--analyze-headers', + action='store_true', + help="""Also analyze functions in #included files. By default, such + functions are skipped unless they are called by functions + within the main source file.""") + format_group = parser.add_mutually_exclusive_group() + format_group.add_argument( + '--plist', '-plist', + dest='output_format', + const='plist', + default='html', + action='store_const', + help="""This option outputs the results as a set of .plist files.""") + format_group.add_argument( + '--plist-html', '-plist-html', + dest='output_format', + const='plist-html', + default='html', + action='store_const', + help="""This option outputs the results as a set of .html and .plist + files.""") + # TODO: implement '-view ' + + advanced = parser.add_argument_group('advanced options') + advanced.add_argument( + '--keep-empty', + action='store_true', + help="""Don't remove the build results directory even if no issues + were reported.""") + advanced.add_argument( + '--no-failure-reports', '-no-failure-reports', + dest='output_failures', + action='store_false', + help="""Do not create a 'failures' subdirectory that includes analyzer + crash reports and preprocessed source files.""") + advanced.add_argument( + '--stats', '-stats', + action='store_true', + help="""Generates visitation statistics for the project being analyzed. + """) + advanced.add_argument( + '--internal-stats', + action='store_true', + help="""Generate internal analyzer statistics.""") + advanced.add_argument( + '--maxloop', '-maxloop', + metavar='<loop count>', + type=int, + help="""Specifiy the number of times a block can be visited before + giving up. Increase for more comprehensive coverage at a cost + of speed.""") + advanced.add_argument( + '--store', '-store', + metavar='<model>', + dest='store_model', + choices=['region', 'basic'], + help="""Specify the store model used by the analyzer. + 'region' specifies a field- sensitive store model. + 'basic' which is far less precise but can more quickly + analyze code. 'basic' was the default store model for + checker-0.221 and earlier.""") + advanced.add_argument( + '--constraints', '-constraints', + metavar='<model>', + dest='constraints_model', + choices=['range', 'basic'], + help="""Specify the contraint engine used by the analyzer. Specifying + 'basic' uses a simpler, less powerful constraint model used by + checker-0.160 and earlier.""") + advanced.add_argument( + '--use-analyzer', + metavar='<path>', + dest='clang', + default='clang', + help="""'%(prog)s' uses the 'clang' executable relative to itself for + static analysis. One can override this behavior with this + option by using the 'clang' packaged with Xcode (on OS X) or + from the PATH.""") + advanced.add_argument( + '--use-cc', + metavar='<path>', + dest='cc', + default='cc', + help="""When '%(prog)s' analyzes a project by interposing a "fake + compiler", which executes a real compiler for compilation and + do other tasks (to run the static analyzer or just record the + compiler invocation). Because of this interposing, '%(prog)s' + does not know what compiler your project normally uses. + Instead, it simply overrides the CC environment variable, and + guesses your default compiler. + + If you need '%(prog)s' to use a specific compiler for + *compilation* then you can use this option to specify a path + to that compiler.""") + advanced.add_argument( + '--use-c++', + metavar='<path>', + dest='cxx', + default='c++', + help="""This is the same as "--use-cc" but for C++ code.""") + advanced.add_argument( + '--analyzer-config', '-analyzer-config', + metavar='<options>', + help="""Provide options to pass through to the analyzer's + -analyzer-config flag. Several options are separated with + comma: 'key1=val1,key2=val2' + + Available options: + stable-report-filename=true or false (default) + + Switch the page naming to: + report-<filename>-<function/method name>-<id>.html + instead of report-XXXXXX.html""") + advanced.add_argument( + '--exclude', + metavar='<directory>', + dest='excludes', + action='append', + default=[], + help="""Do not run static analyzer against files found in this + directory. (You can specify this option multiple times.) + Could be usefull when project contains 3rd party libraries. + The directory path shall be absolute path as file names in + the compilation database.""") + + plugins = parser.add_argument_group('checker options') + plugins.add_argument( + '--load-plugin', '-load-plugin', + metavar='<plugin library>', + dest='plugins', + action='append', + help="""Loading external checkers using the clang plugin interface.""") + plugins.add_argument( + '--enable-checker', '-enable-checker', + metavar='<checker name>', + action=AppendCommaSeparated, + help="""Enable specific checker.""") + plugins.add_argument( + '--disable-checker', '-disable-checker', + metavar='<checker name>', + action=AppendCommaSeparated, + help="""Disable specific checker.""") + plugins.add_argument( + '--help-checkers', + action='store_true', + help="""A default group of checkers is run unless explicitly disabled. + Exactly which checkers constitute the default group is a + function of the operating system in use. These can be printed + with this flag.""") + plugins.add_argument( + '--help-checkers-verbose', + action='store_true', + help="""Print all available checkers and mark the enabled ones.""") + + if from_build_command: + parser.add_argument( + dest='build', + nargs=argparse.REMAINDER, + help="""Command to run.""") + + return parser + + +class AppendCommaSeparated(argparse.Action): + """ argparse Action class to support multiple comma separated lists. """ + + def __call__(self, __parser, namespace, values, __option_string): + # getattr(obj, attr, default) does not really returns default but none + if getattr(namespace, self.dest, None) is None: + setattr(namespace, self.dest, []) + # once it's fixed we can use as expected + actual = getattr(namespace, self.dest) + actual.extend(values.split(',')) + setattr(namespace, self.dest, actual) diff --git a/tools/scan-build-py/libscanbuild/clang.py b/tools/scan-build-py/libscanbuild/clang.py new file mode 100644 index 0000000000..0c3454b16a --- /dev/null +++ b/tools/scan-build-py/libscanbuild/clang.py @@ -0,0 +1,156 @@ +# -*- 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 for the Clang executable. + +Since Clang command line interface is so rich, but this project is using only +a subset of that, it makes sense to create a function specific wrapper. """ + +import re +import subprocess +import logging +from libscanbuild.shell import decode + +__all__ = ['get_version', 'get_arguments', 'get_checkers'] + + +def get_version(cmd): + """ Returns the compiler version as string. """ + + lines = subprocess.check_output([cmd, '-v'], stderr=subprocess.STDOUT) + return lines.decode('ascii').splitlines()[0] + + +def get_arguments(command, cwd): + """ Capture Clang invocation. + + This method returns the front-end invocation that would be executed as + a result of the given driver invocation. """ + + def lastline(stream): + last = None + for line in stream: + last = line + if last is None: + raise Exception("output not found") + return last + + cmd = command[:] + cmd.insert(1, '-###') + logging.debug('exec command in %s: %s', cwd, ' '.join(cmd)) + child = subprocess.Popen(cmd, + cwd=cwd, + universal_newlines=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + line = lastline(child.stdout) + child.stdout.close() + child.wait() + if child.returncode == 0: + if re.search(r'clang(.*): error:', line): + raise Exception(line) + return decode(line) + else: + raise Exception(line) + + +def get_active_checkers(clang, plugins): + """ To get the default plugins we execute Clang to print how this + compilation would be called. + + For input file we specify stdin and pass only language information. """ + + def checkers(language): + """ Returns a list of active checkers for the given language. """ + + load = [elem + for plugin in plugins + for elem in ['-Xclang', '-load', '-Xclang', plugin]] + cmd = [clang, '--analyze'] + load + ['-x', language, '-'] + pattern = re.compile(r'^-analyzer-checker=(.*)$') + return [pattern.match(arg).group(1) + for arg in get_arguments(cmd, '.') if pattern.match(arg)] + + result = set() + for language in ['c', 'c++', 'objective-c', 'objective-c++']: + result.update(checkers(language)) + return result + + +def get_checkers(clang, plugins): + """ Get all the available checkers from default and from the plugins. + + clang -- the compiler we are using + plugins -- list of plugins which was requested by the user + + This method returns a dictionary of all available checkers and status. + + {<plugin name>: (<plugin description>, <is active by default>)} """ + + plugins = plugins if plugins else [] + + def parse_checkers(stream): + """ Parse clang -analyzer-checker-help output. + + Below the line 'CHECKERS:' are there the name description pairs. + Many of them are in one line, but some long named plugins has the + name and the description in separate lines. + + The plugin name is always prefixed with two space character. The + name contains no whitespaces. Then followed by newline (if it's + too long) or other space characters comes the description of the + plugin. The description ends with a newline character. """ + + # find checkers header + for line in stream: + if re.match(r'^CHECKERS:', line): + break + # find entries + state = None + for line in stream: + if state and not re.match(r'^\s\s\S', line): + yield (state, line.strip()) + state = None + elif re.match(r'^\s\s\S+$', line.rstrip()): + state = line.strip() + else: + pattern = re.compile(r'^\s\s(?P<key>\S*)\s*(?P<value>.*)') + match = pattern.match(line.rstrip()) + if match: + current = match.groupdict() + yield (current['key'], current['value']) + + def is_active(actives, entry): + """ Returns true if plugin name is matching the active plugin names. + + actives -- set of active plugin names (or prefixes). + entry -- the current plugin name to judge. + + The active plugin names are specific plugin names or prefix of some + names. One example for prefix, when it say 'unix' and it shall match + on 'unix.API', 'unix.Malloc' and 'unix.MallocSizeof'. """ + + return any(re.match(r'^' + a + r'(\.|$)', entry) for a in actives) + + actives = get_active_checkers(clang, plugins) + + load = [elem for plugin in plugins for elem in ['-load', plugin]] + cmd = [clang, '-cc1'] + load + ['-analyzer-checker-help'] + + logging.debug('exec command: %s', ' '.join(cmd)) + child = subprocess.Popen(cmd, + universal_newlines=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + checkers = { + k: (v, is_active(actives, k)) + for k, v in parse_checkers(child.stdout) + } + child.stdout.close() + child.wait() + if child.returncode == 0 and len(checkers): + return checkers + else: + raise Exception('Could not query Clang for available checkers.') diff --git a/tools/scan-build-py/libscanbuild/command.py b/tools/scan-build-py/libscanbuild/command.py new file mode 100644 index 0000000000..69ca3393f9 --- /dev/null +++ b/tools/scan-build-py/libscanbuild/command.py @@ -0,0 +1,133 @@ +# -*- 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 for to parse a compiler invocation. """ + +import re +import os + +__all__ = ['Action', 'classify_parameters', 'classify_source'] + + +class Action(object): + """ Enumeration class for compiler action. """ + + Link, Compile, Ignored = range(3) + + +def classify_parameters(command): + """ Parses the command line arguments of the given invocation. """ + + # result value of this method. + # some value are preset, some will be set only when found. + result = { + 'action': Action.Link, + 'files': [], + 'output': None, + 'compile_options': [], + 'c++': is_cplusplus_compiler(command[0]) + # archs_seen + # language + } + + # data structure to ignore compiler parameters. + # key: parameter name, value: number of parameters to ignore afterwards. + ignored = { + '-g': 0, + '-fsyntax-only': 0, + '-save-temps': 0, + '-install_name': 1, + '-exported_symbols_list': 1, + '-current_version': 1, + '-compatibility_version': 1, + '-init': 1, + '-e': 1, + '-seg1addr': 1, + '-bundle_loader': 1, + '-multiply_defined': 1, + '-sectorder': 3, + '--param': 1, + '--serialize-diagnostics': 1 + } + + args = iter(command[1:]) + for arg in args: + # compiler action parameters are the most important ones... + if arg in {'-E', '-S', '-cc1', '-M', '-MM', '-###'}: + result.update({'action': Action.Ignored}) + elif arg == '-c': + result.update({'action': max(result['action'], Action.Compile)}) + # arch flags are taken... + elif arg == '-arch': + archs = result.get('archs_seen', []) + result.update({'archs_seen': archs + [next(args)]}) + # explicit language option taken... + elif arg == '-x': + result.update({'language': next(args)}) + # output flag taken... + elif arg == '-o': + result.update({'output': next(args)}) + # warning disable options are taken... + elif re.match(r'^-Wno-', arg): + result['compile_options'].append(arg) + # warning options are ignored... + elif re.match(r'^-[mW].+', arg): + pass + # some preprocessor parameters are ignored... + elif arg in {'-MD', '-MMD', '-MG', '-MP'}: + pass + elif arg in {'-MF', '-MT', '-MQ'}: + next(args) + # linker options are ignored... + elif arg in {'-static', '-shared', '-s', '-rdynamic'} or \ + re.match(r'^-[lL].+', arg): + pass + elif arg in {'-l', '-L', '-u', '-z', '-T', '-Xlinker'}: + next(args) + # some other options are ignored... + elif arg in ignored.keys(): + for _ in range(ignored[arg]): + next(args) + # parameters which looks source file are taken... + elif re.match(r'^[^-].+', arg) and classify_source(arg): + result['files'].append(arg) + # and consider everything else as compile option. + else: + result['compile_options'].append(arg) + + return result + + +def classify_source(filename, cplusplus=False): + """ Return the language from file name extension. """ + + mapping = { + '.c': 'c++' if cplusplus else 'c', + '.i': 'c++-cpp-output' if cplusplus else 'c-cpp-output', + '.ii': 'c++-cpp-output', + '.m': 'objective-c', + '.mi': 'objective-c-cpp-output', + '.mm': 'objective-c++', + '.mii': 'objective-c++-cpp-output', + '.C': 'c++', + '.cc': 'c++', + '.CC': 'c++', + '.cp': 'c++', + '.cpp': 'c++', + '.cxx': 'c++', + '.c++': 'c++', + '.C++': 'c++', + '.txx': 'c++' + } + + __, extension = os.path.splitext(os.path.basename(filename)) + return mapping.get(extension) + + +def is_cplusplus_compiler(name): + """ Returns true when the compiler name refer to a C++ compiler. """ + + match = re.match(r'^([^/]*/)*(\w*-)*(\w+\+\+)(-(\d+(\.\d+){0,3}))?$', name) + return False if match is None else True diff --git a/tools/scan-build-py/libscanbuild/intercept.py b/tools/scan-build-py/libscanbuild/intercept.py new file mode 100644 index 0000000000..6062e2ea8c --- /dev/null +++ b/tools/scan-build-py/libscanbuild/intercept.py @@ -0,0 +1,359 @@ +# -*- 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 capture the compiler invocation of any +build process. The result of that should be a compilation database. + +This implementation is using the LD_PRELOAD or DYLD_INSERT_LIBRARIES +mechanisms provided by the dynamic linker. The related library is implemented +in C language and can be found under 'libear' directory. + +The 'libear' library is capturing all child process creation and logging the +relevant information about it into separate files in a specified directory. +The parameter of this process is the output directory name, where the report +files shall be placed. This parameter is passed as an environment variable. + +The module also implements compiler wrappers to intercept the compiler calls. + +The module implements the build command execution and the post-processing of +the output files, which will condensates into a compilation database. """ + +import sys +import os +import os.path +import re +import itertools +import json +import glob +import argparse +import logging +import subprocess +from libear import build_libear, TemporaryDirectory +from libscanbuild import duplicate_check, tempdir, initialize_logging +from libscanbuild import command_entry_point +from libscanbuild.command import Action, classify_parameters +from libscanbuild.shell import encode, decode + +__all__ = ['capture', 'intercept_build_main', 'intercept_build_wrapper'] + +GS = chr(0x1d) +RS = chr(0x1e) +US = chr(0x1f) + +COMPILER_WRAPPER_CC = 'intercept-cc' +COMPILER_WRAPPER_CXX = 'intercept-c++' + + +@command_entry_point +def intercept_build_main(bin_dir): + """ Entry point for 'intercept-build' command. """ + + parser = create_parser() + args = parser.parse_args() + + initialize_logging(args.verbose) + logging.debug('Parsed arguments: %s', args) + + if not args.build: + parser.print_help() + return 0 + + return capture(args, bin_dir) + + +def capture(args, bin_dir): + """ The entry point of build command interception. """ + + def post_processing(commands): + """ To make a compilation database, it needs to filter out commands + which are not compiler calls. Needs to find the source file name + from the arguments. And do shell escaping on the command. + + To support incremental builds, it is desired to read elements from + an existing compilation database from a previous run. These elemets + shall be merged with the new elements. """ + + # create entries from the current run + current = itertools.chain.from_iterable( + # creates a sequence of entry generators from an exec, + # but filter out non compiler calls before. + (format_entry(x) for x in commands if is_compiler_call(x))) + # read entries from previous run + if 'append' in args and args.append and os.path.exists(args.cdb): + with open(args.cdb) as handle: + previous = iter(json.load(handle)) + else: + previous = iter([]) + # filter out duplicate entries from both + duplicate = duplicate_check(entry_hash) + return (entry for entry in itertools.chain(previous, current) + if os.path.exists(entry['file']) and not duplicate(entry)) + + with TemporaryDirectory(prefix='intercept-', dir=tempdir()) as tmp_dir: + # run the build command + environment = setup_environment(args, tmp_dir, bin_dir) + logging.debug('run build in environment: %s', environment) + exit_code = subprocess.call(args.build, env=environment) + logging.info('build finished with exit code: %d', exit_code) + # read the intercepted exec calls + commands = itertools.chain.from_iterable( + parse_exec_trace(os.path.join(tmp_dir, filename)) + for filename in sorted(glob.iglob(os.path.join(tmp_dir, '*.cmd')))) + # do post processing only if that was requested + if 'raw_entries' not in args or not args.raw_entries: + entries = post_processing(commands) + else: + entries = commands + # dump the compilation database + with open(args.cdb, 'w+') as handle: + json.dump(list(entries), handle, sort_keys=True, indent=4) + return exit_code + + +def setup_environment(args, destination, bin_dir): + """ Sets up the environment for the build command. + + It sets the required environment variables and execute the given command. + The exec calls will be logged by the 'libear' preloaded library or by the + 'wrapper' programs. """ + + c_compiler = args.cc if 'cc' in args else 'cc' + cxx_compiler = args.cxx if 'cxx' in args else 'c++' + + libear_path = None if args.override_compiler or is_preload_disabled( + sys.platform) else build_libear(c_compiler, destination) + + environment = dict(os.environ) + environment.update({'INTERCEPT_BUILD_TARGET_DIR': destination}) + + if not libear_path: + logging.debug('intercept gonna use compiler wrappers') + environment.update({ + 'CC': os.path.join(bin_dir, COMPILER_WRAPPER_CC), + 'CXX': os.path.join(bin_dir, COMPILER_WRAPPER_CXX), + 'INTERCEPT_BUILD_CC': c_compiler, + 'INTERCEPT_BUILD_CXX': cxx_compiler, + 'INTERCEPT_BUILD_VERBOSE': 'DEBUG' if args.verbose > 2 else 'INFO' + }) + elif sys.platform == 'darwin': + logging.debug('intercept gonna preload libear on OSX') + environment.update({ + 'DYLD_INSERT_LIBRARIES': libear_path, + 'DYLD_FORCE_FLAT_NAMESPACE': '1' + }) + else: + logging.debug('intercept gonna preload libear on UNIX') + environment.update({'LD_PRELOAD': libear_path}) + + return environment + + +def intercept_build_wrapper(cplusplus): + """ Entry point for `intercept-cc` and `intercept-c++` compiler wrappers. + + It does generate execution report into target directory. And execute + the wrapped compilation with the real compiler. The parameters for + report and execution are from environment variables. + + Those parameters which for 'libear' library can't have meaningful + values are faked. """ + + # initialize wrapper logging + logging.basicConfig(format='intercept: %(levelname)s: %(message)s', + level=os.getenv('INTERCEPT_BUILD_VERBOSE', 'INFO')) + # write report + try: + target_dir = os.getenv('INTERCEPT_BUILD_TARGET_DIR') + if not target_dir: + raise UserWarning('exec report target directory not found') + pid = str(os.getpid()) + target_file = os.path.join(target_dir, pid + '.cmd') + logging.debug('writing exec report to: %s', target_file) + with open(target_file, 'ab') as handler: + working_dir = os.getcwd() + command = US.join(sys.argv) + US + content = RS.join([pid, pid, 'wrapper', working_dir, command]) + GS + handler.write(content.encode('utf-8')) + except IOError: + logging.exception('writing exec report failed') + except UserWarning as warning: + logging.warning(warning) + # execute with real compiler + compiler = os.getenv('INTERCEPT_BUILD_CXX', 'c++') if cplusplus \ + else os.getenv('INTERCEPT_BUILD_CC', 'cc') + compilation = [compiler] + sys.argv[1:] + logging.debug('execute compiler: %s', compilation) + return subprocess.call(compilation) + + +def parse_exec_trace(filename): + """ Parse the file generated by the 'libear' preloaded library. + + Given filename points to a file which contains the basic report + generated by the interception library or wrapper command. A single + report file _might_ contain multiple process creation info. """ + + logging.debug('parse exec trace file: %s', filename) + with open(filename, 'r') as handler: + content = handler.read() + for group in filter(bool, content.split(GS)): + records = group.split(RS) + yield { + 'pid': records[0], + 'ppid': records[1], + 'function': records[2], + 'directory': records[3], + 'command': records[4].split(US)[:-1] + } + + +def format_entry(entry): + """ Generate the desired fields for compilation database entries. """ + + def abspath(cwd, name): + """ Create normalized absolute path from input filename. """ + fullname = name if os.path.isabs(name) else os.path.join(cwd, name) + return os.path.normpath(fullname) + + logging.debug('format this command: %s', entry['command']) + atoms = classify_parameters(entry['command']) + if atoms['action'] <= Action.Compile: + for source in atoms['files']: + compiler = 'c++' if atoms['c++'] else 'cc' + flags = atoms['compile_options'] + flags += ['-o', atoms['output']] if atoms['output'] else [] + flags += ['-x', atoms['language']] if 'language' in atoms else [] + flags += [elem + for arch in atoms.get('archs_seen', []) + for elem in ['-arch', arch]] + command = [compiler, '-c'] + flags + [source] + logging.debug('formated as: %s', command) + yield { + 'directory': entry['directory'], + 'command': encode(command), + 'file': abspath(entry['directory'], source) + } + + +def is_compiler_call(entry): + """ A predicate to decide the entry is a compiler call or not. """ + + patterns = [ + re.compile(r'^([^/]*/)*intercept-c(c|\+\+)$'), + re.compile(r'^([^/]*/)*c(c|\+\+)$'), + re.compile(r'^([^/]*/)*([^-]*-)*[mg](cc|\+\+)(-\d+(\.\d+){0,2})?$'), + re.compile(r'^([^/]*/)*([^-]*-)*clang(\+\+)?(-\d+(\.\d+){0,2})?$'), + re.compile(r'^([^/]*/)*llvm-g(cc|\+\+)$'), + ] + executable = entry['command'][0] + return any((pattern.match(executable) for pattern in patterns)) + + +def is_preload_disabled(platform): + """ Library-based interposition will fail silently if SIP is enabled, + so this should be detected. You can detect whether SIP is enabled on + Darwin by checking whether (1) there is a binary called 'csrutil' in + the path and, if so, (2) whether the output of executing 'csrutil status' + contains 'System Integrity Protection status: enabled'. + + Same problem on linux when SELinux is enabled. The status query program + 'sestatus' and the output when it's enabled 'SELinux status: enabled'. """ + + if platform == 'darwin': + pattern = re.compile(r'System Integrity Protection status:\s+enabled') + command = ['csrutil', 'status'] + elif platform in {'linux', 'linux2'}: + pattern = re.compile(r'SELinux status:\s+enabled') + command = ['sestatus'] + else: + return False + + try: + lines = subprocess.check_output(command).decode('utf-8') + return any((pattern.match(line) for line in lines.splitlines())) + except: + return False + + +def entry_hash(entry): + """ Implement unique hash method for compilation database entries. """ + + # For faster lookup in set filename is reverted + filename = entry['file'][::-1] + # For faster lookup in set directory is reverted + directory = entry['directory'][::-1] + # On OS X the 'cc' and 'c++' compilers are wrappers for + # 'clang' therefore both call would be logged. To avoid + # this the hash does not contain the first word of the + # command. + command = ' '.join(decode(entry['command'])[1:]) + + return '<>'.join([filename, directory, command]) + + +def create_parser(): + """ Command line argument parser factory method. """ + + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument( + '--verbose', '-v', + action='count', + default=0, + help="""Enable verbose output from '%(prog)s'. A second and third + flag increases verbosity.""") + parser.add_argument( + '--cdb', + metavar='<file>', + default="compile_commands.json", + help="""The JSON compilation database.""") + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--append', + action='store_true', + help="""Append new entries to existing compilation database.""") + group.add_argument( + '--disable-filter', '-n', + dest='raw_entries', + action='store_true', + help="""Intercepted child process creation calls (exec calls) are all + logged to the output. The output is not a compilation database. + This flag is for debug purposes.""") + + advanced = parser.add_argument_group('advanced options') + advanced.add_argument( + '--override-compiler', + action='store_true', + help="""Always resort to the compiler wrapper even when better + intercept methods are available.""") + advanced.add_argument( + '--use-cc', + metavar='<path>', + dest='cc', + default='cc', + help="""When '%(prog)s' analyzes a project by interposing a compiler + wrapper, which executes a real compiler for compilation and + do other tasks (record the compiler invocation). Because of + this interposing, '%(prog)s' does not know what compiler your + project normally uses. Instead, it simply overrides the CC + environment variable, and guesses your default compiler. + + If you need '%(prog)s' to use a specific compiler for + *compilation* then you can use this option to specify a path + to that compiler.""") + advanced.add_argument( + '--use-c++', + metavar='<path>', + dest='cxx', + default='c++', + help="""This is the same as "--use-cc" but for C++ code.""") + + parser.add_argument( + dest='build', + nargs=argparse.REMAINDER, + help="""Command to run.""") + + return parser diff --git a/tools/scan-build-py/libscanbuild/report.py b/tools/scan-build-py/libscanbuild/report.py new file mode 100644 index 0000000000..efc0a55de6 --- /dev/null +++ b/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) diff --git a/tools/scan-build-py/libscanbuild/resources/scanview.css b/tools/scan-build-py/libscanbuild/resources/scanview.css new file mode 100644 index 0000000000..cf8a5a6ad4 --- /dev/null +++ b/tools/scan-build-py/libscanbuild/resources/scanview.css @@ -0,0 +1,62 @@ +body { color:#000000; background-color:#ffffff } +body { font-family: Helvetica, sans-serif; font-size:9pt } +h1 { font-size: 14pt; } +h2 { font-size: 12pt; } +table { font-size:9pt } +table { border-spacing: 0px; border: 1px solid black } +th, table thead { + background-color:#eee; color:#666666; + font-weight: bold; cursor: default; + text-align:center; + font-weight: bold; font-family: Verdana; + white-space:nowrap; +} +.W { font-size:0px } +th, td { padding:5px; padding-left:8px; text-align:left } +td.SUMM_DESC { padding-left:12px } +td.DESC { white-space:pre } +td.Q { text-align:right } +td { text-align:left } +tbody.scrollContent { overflow:auto } + +table.form_group { + background-color: #ccc; + border: 1px solid #333; + padding: 2px; +} + +table.form_inner_group { + background-color: #ccc; + border: 1px solid #333; + padding: 0px; +} + +table.form { + background-color: #999; + border: 1px solid #333; + padding: 2px; +} + +td.form_label { + text-align: right; + vertical-align: top; +} +/* For one line entires */ +td.form_clabel { + text-align: right; + vertical-align: center; +} +td.form_value { + text-align: left; + vertical-align: top; +} +td.form_submit { + text-align: right; + vertical-align: top; +} + +h1.SubmitFail { + color: #f00; +} +h1.SubmitOk { +} diff --git a/tools/scan-build-py/libscanbuild/resources/selectable.js b/tools/scan-build-py/libscanbuild/resources/selectable.js new file mode 100644 index 0000000000..53f6a8da13 --- /dev/null +++ b/tools/scan-build-py/libscanbuild/resources/selectable.js @@ -0,0 +1,47 @@ +function SetDisplay(RowClass, DisplayVal) +{ + var Rows = document.getElementsByTagName("tr"); + for ( var i = 0 ; i < Rows.length; ++i ) { + if (Rows[i].className == RowClass) { + Rows[i].style.display = DisplayVal; + } + } +} + +function CopyCheckedStateToCheckButtons(SummaryCheckButton) { + var Inputs = document.getElementsByTagName("input"); + for ( var i = 0 ; i < Inputs.length; ++i ) { + if (Inputs[i].type == "checkbox") { + if(Inputs[i] != SummaryCheckButton) { + Inputs[i].checked = SummaryCheckButton.checked; + Inputs[i].onclick(); + } + } + } +} + +function returnObjById( id ) { + if (document.getElementById) + var returnVar = document.getElementById(id); + else if (document.all) + var returnVar = document.all[id]; + else if (document.layers) + var returnVar = document.layers[id]; + return returnVar; +} + +var NumUnchecked = 0; + +function ToggleDisplay(CheckButton, ClassName) { + if (CheckButton.checked) { + SetDisplay(ClassName, ""); + if (--NumUnchecked == 0) { + returnObjById("AllBugsCheck").checked = true; + } + } + else { + SetDisplay(ClassName, "none"); + NumUnchecked++; + returnObjById("AllBugsCheck").checked = false; + } +} diff --git a/tools/scan-build-py/libscanbuild/resources/sorttable.js b/tools/scan-build-py/libscanbuild/resources/sorttable.js new file mode 100644 index 0000000000..32faa078d8 --- /dev/null +++ b/tools/scan-build-py/libscanbuild/resources/sorttable.js @@ -0,0 +1,492 @@ +/* + SortTable + version 2 + 7th April 2007 + Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ + + Instructions: + Download this file + Add <script src="sorttable.js"></script> to your HTML + Add class="sortable" to any table you'd like to make sortable + Click on the headers to sort + + Thanks to many, many people for contributions and suggestions. + Licenced as X11: http://www.kryogenix.org/code/browser/licence.html + This basically means: do what you want with it. +*/ + + +var stIsIE = /*@cc_on!@*/false; + +sorttable = { + init: function() { + // quit if this function has already been called + if (arguments.callee.done) return; + // flag this function so we don't do the same thing twice + arguments.callee.done = true; + // kill the timer + if (_timer) clearInterval(_timer); + + if (!document.createElement || !document.getElementsByTagName) return; + + sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; + + forEach(document.getElementsByTagName('table'), function(table) { + if (table.className.search(/\bsortable\b/) != -1) { + sorttable.makeSortable(table); + } + }); + + }, + + makeSortable: function(table) { + if (table.getElementsByTagName('thead').length == 0) { + // table doesn't have a tHead. Since it should have, create one and + // put the first table row in it. + the = document.createElement('thead'); + the.appendChild(table.rows[0]); + table.insertBefore(the,table.firstChild); + } + // Safari doesn't support table.tHead, sigh + if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; + + if (table.tHead.rows.length != 1) return; // can't cope with two header rows + + // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as + // "total" rows, for example). This is B&R, since what you're supposed + // to do is put them in a tfoot. So, if there are sortbottom rows, + // for backward compatibility, move them to tfoot (creating it if needed). + sortbottomrows = []; + for (var i=0; i<table.rows.length; i++) { + if (table.rows[i].className.search(/\bsortbottom\b/) != -1) { + sortbottomrows[sortbottomrows.length] = table.rows[i]; + } + } + if (sortbottomrows) { + if (table.tFoot == null) { + // table doesn't have a tfoot. Create one. + tfo = document.createElement('tfoot'); + table.appendChild(tfo); + } + for (var i=0; i<sortbottomrows.length; i++) { + tfo.appendChild(sortbottomrows[i]); + } + delete sortbottomrows; + } + + // work through each column and calculate its type + headrow = table.tHead.rows[0].cells; + for (var i=0; i<headrow.length; i++) { + // manually override the type with a sorttable_type attribute + if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col + mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/); + if (mtch) { override = mtch[1]; } + if (mtch && typeof sorttable["sort_"+override] == 'function') { + headrow[i].sorttable_sortfunction = sorttable["sort_"+override]; + } else { + headrow[i].sorttable_sortfunction = sorttable.guessType(table,i); + } + // make it clickable to sort + headrow[i].sorttable_columnindex = i; + headrow[i].sorttable_tbody = table.tBodies[0]; + dean_addEvent(headrow[i],"click", function(e) { + + if (this.className.search(/\bsorttable_sorted\b/) != -1) { + // if we're already sorted by this column, just + // reverse the table, which is quicker + sorttable.reverse(this.sorttable_tbody); + this.className = this.className.replace('sorttable_sorted', + 'sorttable_sorted_reverse'); + this.removeChild(document.getElementById('sorttable_sortfwdind')); + sortrevind = document.createElement('span'); + sortrevind.id = "sorttable_sortrevind"; + sortrevind.innerHTML = stIsIE ? ' <font face="webdings">5</font>' : ' ▴'; + this.appendChild(sortrevind); + return; + } + if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { + // if we're already sorted by this column in reverse, just + // re-reverse the table, which is quicker + sorttable.reverse(this.sorttable_tbody); + this.className = this.className.replace('sorttable_sorted_reverse', + 'sorttable_sorted'); + this.removeChild(document.getElementById('sorttable_sortrevind')); + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾'; + this.appendChild(sortfwdind); + return; + } + + // remove sorttable_sorted classes + theadrow = this.parentNode; + forEach(theadrow.childNodes, function(cell) { + if (cell.nodeType == 1) { // an element + cell.className = cell.className.replace('sorttable_sorted_reverse',''); + cell.className = cell.className.replace('sorttable_sorted',''); + } + }); + sortfwdind = document.getElementById('sorttable_sortfwdind'); + if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } + sortrevind = document.getElementById('sorttable_sortrevind'); + if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } + + this.className += ' sorttable_sorted'; + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾'; + this.appendChild(sortfwdind); + + // build an array to sort. This is a Schwartzian transform thing, + // i.e., we "decorate" each row with the actual sort key, + // sort based on the sort keys, and then put the rows back in order + // which is a lot faster because you only do getInnerText once per row + row_array = []; + col = this.sorttable_columnindex; + rows = this.sorttable_tbody.rows; + for (var j=0; j<rows.length; j++) { + row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]]; + } + /* If you want a stable sort, uncomment the following line */ + sorttable.shaker_sort(row_array, this.sorttable_sortfunction); + /* and comment out this one */ + //row_array.sort(this.sorttable_sortfunction); + + tb = this.sorttable_tbody; + for (var j=0; j<row_array.length; j++) { + tb.appendChild(row_array[j][1]); + } + + delete row_array; + }); + } + } + }, + + guessType: function(table, column) { + // guess the type of a column based on its first non-blank row + sortfn = sorttable.sort_alpha; + for (var i=0; i<table.tBodies[0].rows.length; i++) { + text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]); + if (text != '') { + if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) { + return sorttable.sort_numeric; + } + // check for a date: dd/mm/yyyy or dd/mm/yy + // can have / or . or - as separator + // can be mm/dd as well + possdate = text.match(sorttable.DATE_RE) + if (possdate) { + // looks like a date + first = parseInt(possdate[1]); + second = parseInt(possdate[2]); + if (first > 12) { + // definitely dd/mm + return sorttable.sort_ddmm; + } else if (second > 12) { + return sorttable.sort_mmdd; + } else { + // looks like a date, but we can't tell which, so assume + // that it's dd/mm (English imperialism!) and keep looking + sortfn = sorttable.sort_ddmm; + } + } + } + } + return sortfn; + }, + + getInnerText: function(node) { + // gets the text we want to use for sorting for a cell. + // strips leading and trailing whitespace. + // this is *not* a generic getInnerText function; it's special to sorttable. + // for example, you can override the cell text with a customkey attribute. + // it also gets .value for <input> fields. + + hasInputs = (typeof node.getElementsByTagName == 'function') && + node.getElementsByTagName('input').length; + + if (node.getAttribute("sorttable_customkey") != null) { + return node.getAttribute("sorttable_customkey"); + } + else if (typeof node.textContent != 'undefined' && !hasInputs) { + return node.textContent.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.innerText != 'undefined' && !hasInputs) { + return node.innerText.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.text != 'undefined' && !hasInputs) { + return node.text.replace(/^\s+|\s+$/g, ''); + } + else { + switch (node.nodeType) { + case 3: + if (node.nodeName.toLowerCase() == 'input') { + return node.value.replace(/^\s+|\s+$/g, ''); + } + case 4: + return node.nodeValue.replace(/^\s+|\s+$/g, ''); + break; + case 1: + case 11: + var innerText = ''; + for (var i = 0; i < node.childNodes.length; i++) { + innerText += sorttable.getInnerText(node.childNodes[i]); + } + return innerText.replace(/^\s+|\s+$/g, ''); + break; + default: + return ''; + } + } + }, + + reverse: function(tbody) { + // reverse the rows in a tbody + newrows = []; + for (var i=0; i<tbody.rows.length; i++) { + newrows[newrows.length] = tbody.rows[i]; + } + for (var i=newrows.length-1; i>=0; i--) { + tbody.appendChild(newrows[i]); + } + delete newrows; + }, + + /* sort functions + each sort function takes two parameters, a and b + you are comparing a[0] and b[0] */ + sort_numeric: function(a,b) { + aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); + if (isNaN(aa)) aa = 0; + bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); + if (isNaN(bb)) bb = 0; + return aa-bb; + }, + sort_alpha: function(a,b) { + if (a[0]==b[0]) return 0; + if (a[0]<b[0]) return -1; + return 1; + }, + sort_ddmm: function(a,b) { + mtch = a[0].match(sorttable.DATE_RE); + y = mtch[3]; m = mtch[2]; d = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt1 = y+m+d; + mtch = b[0].match(sorttable.DATE_RE); + y = mtch[3]; m = mtch[2]; d = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt2 = y+m+d; + if (dt1==dt2) return 0; + if (dt1<dt2) return -1; + return 1; + }, + sort_mmdd: function(a,b) { + mtch = a[0].match(sorttable.DATE_RE); + y = mtch[3]; d = mtch[2]; m = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt1 = y+m+d; + mtch = b[0].match(sorttable.DATE_RE); + y = mtch[3]; d = mtch[2]; m = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt2 = y+m+d; + if (dt1==dt2) return 0; + if (dt1<dt2) return -1; + return 1; + }, + + shaker_sort: function(list, comp_func) { + // A stable sort function to allow multi-level sorting of data + // see: http://en.wikipedia.org/wiki/Cocktail_sort + // thanks to Joseph Nahmias + var b = 0; + var t = list.length - 1; + var swap = true; + + while(swap) { + swap = false; + for(var i = b; i < t; ++i) { + if ( comp_func(list[i], list[i+1]) > 0 ) { + var q = list[i]; list[i] = list[i+1]; list[i+1] = q; + swap = true; + } + } // for + t--; + + if (!swap) break; + + for(var i = t; i > b; --i) { + if ( comp_func(list[i], list[i-1]) < 0 ) { + var q = list[i]; list[i] = list[i-1]; list[i-1] = q; + swap = true; + } + } // for + b++; + + } // while(swap) + } +} + +/* ****************************************************************** + Supporting functions: bundled here to avoid depending on a library + ****************************************************************** */ + +// Dean Edwards/Matthias Miller/John Resig + +/* for Mozilla/Opera9 */ +if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", sorttable.init, false); +} + +/* for Internet Explorer */ +/*@cc_on @*/ +/*@if (@_win32) + document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>"); + var script = document.getElementById("__ie_onload"); + script.onreadystatechange = function() { + if (this.readyState == "complete") { + sorttable.init(); // call the onload handler + } + }; +/*@end @*/ + +/* for Safari */ +if (/WebKit/i.test(navigator.userAgent)) { // sniff + var _timer = setInterval(function() { + if (/loaded|complete/.test(document.readyState)) { + sorttable.init(); // call the onload handler + } + }, 10); +} + +/* for other browsers */ +window.onload = sorttable.init; + +// written by Dean Edwards, 2005 +// with input from Tino Zijdel, Matthias Miller, Diego Perini + +// http://dean.edwards.name/weblog/2005/10/add-event/ + +function dean_addEvent(element, type, handler) { + if (element.addEventListener) { + element.addEventListener(type, handler, false); + } else { + // assign each event handler a unique ID + if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++; + // create a hash table of event types for the element + if (!element.events) element.events = {}; + // create a hash table of event handlers for each element/event pair + var handlers = element.events[type]; + if (!handlers) { + handlers = element.events[type] = {}; + // store the existing event handler (if there is one) + if (element["on" + type]) { + handlers[0] = element["on" + type]; + } + } + // store the event handler in the hash table + handlers[handler.$$guid] = handler; + // assign a global event handler to do all the work + element["on" + type] = handleEvent; + } +}; +// a counter used to create unique IDs +dean_addEvent.guid = 1; + +function removeEvent(element, type, handler) { + if (element.removeEventListener) { + element.removeEventListener(type, handler, false); + } else { + // delete the event handler from the hash table + if (element.events && element.events[type]) { + delete element.events[type][handler.$$guid]; + } + } +}; + +function handleEvent(event) { + var returnValue = true; + // grab the event object (IE uses a global event object) + event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); + // get a reference to the hash table of event handlers + var handlers = this.events[event.type]; + // execute each event handler + for (var i in handlers) { + this.$$handleEvent = handlers[i]; + if (this.$$handleEvent(event) === false) { + returnValue = false; + } + } + return returnValue; +}; + +function fixEvent(event) { + // add W3C standard event methods + event.preventDefault = fixEvent.preventDefault; + event.stopPropagation = fixEvent.stopPropagation; + return event; +}; +fixEvent.preventDefault = function() { + this.returnValue = false; +}; +fixEvent.stopPropagation = function() { + this.cancelBubble = true; +} + +// Dean's forEach: http://dean.edwards.name/base/forEach.js +/* + forEach, version 1.0 + Copyright 2006, Dean Edwards + License: http://www.opensource.org/licenses/mit-license.php +*/ + +// array-like enumeration +if (!Array.forEach) { // mozilla already supports this + Array.forEach = function(array, block, context) { + for (var i = 0; i < array.length; i++) { + block.call(context, array[i], i, array); + } + }; +} + +// generic enumeration +Function.prototype.forEach = function(object, block, context) { + for (var key in object) { + if (typeof this.prototype[key] == "undefined") { + block.call(context, object[key], key, object); + } + } +}; + +// character enumeration +String.forEach = function(string, block, context) { + Array.forEach(string.split(""), function(chr, index) { + block.call(context, chr, index, string); + }); +}; + +// globally resolve forEach enumeration +var forEach = function(object, block, context) { + if (object) { + var resolve = Object; // default + if (object instanceof Function) { + // functions have a "length" property + resolve = Function; + } else if (object.forEach instanceof Function) { + // the object implements a custom forEach method so use that + object.forEach(block, context); + return; + } else if (typeof object == "string") { + // the object is a string + resolve = String; + } else if (typeof object.length == "number") { + // the object is array-like + resolve = Array; + } + resolve.forEach(object, block, context); + } +}; diff --git a/tools/scan-build-py/libscanbuild/runner.py b/tools/scan-build-py/libscanbuild/runner.py new file mode 100644 index 0000000000..248ca90ad3 --- /dev/null +++ b/tools/scan-build-py/libscanbuild/runner.py @@ -0,0 +1,256 @@ +# -*- 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 run the analyzer commands. """ + +import os +import os.path +import tempfile +import functools +import subprocess +import logging +from libscanbuild.command import classify_parameters, Action, classify_source +from libscanbuild.clang import get_arguments, get_version +from libscanbuild.shell import decode + +__all__ = ['run'] + + +def require(required): + """ Decorator for checking the required values in state. + + It checks the required attributes in the passed state and stop when + any of those is missing. """ + + def decorator(function): + @functools.wraps(function) + def wrapper(*args, **kwargs): + for key in required: + if key not in args[0]: + raise KeyError( + '{0} not passed to {1}'.format(key, function.__name__)) + + return function(*args, **kwargs) + + return wrapper + + return decorator + + +@require(['command', 'directory', 'file', # an entry from compilation database + 'clang', 'direct_args', # compiler name, and arguments from command + 'output_dir', 'output_format', 'output_failures']) +def run(opts): + """ Entry point to run (or not) static analyzer against a single entry + of the compilation database. + + This complex task is decomposed into smaller methods which are calling + each other in chain. If the analyzis is not possibe the given method + just return and break the chain. + + The passed parameter is a python dictionary. Each method first check + that the needed parameters received. (This is done by the 'require' + decorator. It's like an 'assert' to check the contract between the + caller and the called method.) """ + + try: + command = opts.pop('command') + logging.debug("Run analyzer against '%s'", command) + opts.update(classify_parameters(decode(command))) + + return action_check(opts) + except Exception: + logging.error("Problem occured during analyzis.", exc_info=1) + return None + + +@require(['report', 'directory', 'clang', 'output_dir', 'language', 'file', + 'error_type', 'error_output', 'exit_code']) +def report_failure(opts): + """ Create report when analyzer failed. + + The major report is the preprocessor output. The output filename generated + randomly. The compiler output also captured into '.stderr.txt' file. + And some more execution context also saved into '.info.txt' file. """ + + def extension(opts): + """ Generate preprocessor file extension. """ + + mapping = {'objective-c++': '.mii', 'objective-c': '.mi', 'c++': '.ii'} + return mapping.get(opts['language'], '.i') + + def destination(opts): + """ Creates failures directory if not exits yet. """ + + name = os.path.join(opts['output_dir'], 'failures') + if not os.path.isdir(name): + os.makedirs(name) + return name + + error = opts['error_type'] + (handle, name) = tempfile.mkstemp(suffix=extension(opts), + prefix='clang_' + error + '_', + dir=destination(opts)) + os.close(handle) + cwd = opts['directory'] + cmd = get_arguments([opts['clang']] + opts['report'] + ['-o', name], cwd) + logging.debug('exec command in %s: %s', cwd, ' '.join(cmd)) + subprocess.call(cmd, cwd=cwd) + + with open(name + '.info.txt', 'w') as handle: + handle.write(opts['file'] + os.linesep) + handle.write(error.title().replace('_', ' ') + os.linesep) + handle.write(' '.join(cmd) + os.linesep) + handle.write(' '.join(os.uname()) + os.linesep) + handle.write(get_version(cmd[0])) + handle.close() + + with open(name + '.stderr.txt', 'w') as handle: + handle.writelines(opts['error_output']) + handle.close() + + return { + 'error_output': opts['error_output'], + 'exit_code': opts['exit_code'] + } + + +@require(['clang', 'analyze', 'directory', 'output']) +def run_analyzer(opts, continuation=report_failure): + """ It assembles the analysis command line and executes it. Capture the + output of the analysis and returns with it. If failure reports are + requested, it calls the continuation to generate it. """ + + cwd = opts['directory'] + cmd = get_arguments([opts['clang']] + opts['analyze'] + opts['output'], + cwd) + logging.debug('exec command in %s: %s', cwd, ' '.join(cmd)) + child = subprocess.Popen(cmd, + cwd=cwd, + universal_newlines=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + output = child.stdout.readlines() + child.stdout.close() + # do report details if it were asked + child.wait() + if opts.get('output_failures', False) and child.returncode: + error_type = 'crash' if child.returncode & 127 else 'other_error' + opts.update({ + 'error_type': error_type, + 'error_output': output, + 'exit_code': child.returncode + }) + return continuation(opts) + return {'error_output': output, 'exit_code': child.returncode} + + +@require(['output_dir']) +def set_analyzer_output(opts, continuation=run_analyzer): + """ Create output file if was requested. + + This plays a role only if .plist files are requested. """ + + if opts.get('output_format') in {'plist', 'plist-html'}: + with tempfile.NamedTemporaryFile(prefix='report-', + suffix='.plist', + delete=False, + dir=opts['output_dir']) as output: + opts.update({'output': ['-o', output.name]}) + return continuation(opts) + else: + opts.update({'output': ['-o', opts['output_dir']]}) + return continuation(opts) + + +@require(['file', 'directory', 'clang', 'direct_args', 'language', + 'output_dir', 'output_format', 'output_failures']) +def create_commands(opts, continuation=set_analyzer_output): + """ Create command to run analyzer or failure report generation. + + It generates commands (from compilation database entries) which contains + enough information to run the analyzer (and the crash report generation + if that was requested). """ + + common = [] + if 'arch' in opts: + common.extend(['-arch', opts.pop('arch')]) + common.extend(opts.pop('compile_options', [])) + common.extend(['-x', opts['language']]) + common.append(os.path.relpath(opts['file'], opts['directory'])) + + opts.update({ + 'analyze': ['--analyze'] + opts['direct_args'] + common, + 'report': ['-fsyntax-only', '-E'] + common + }) + + return continuation(opts) + + +@require(['file', 'c++']) +def language_check(opts, continuation=create_commands): + """ Find out the language from command line parameters or file name + extension. The decision also influenced by the compiler invocation. """ + + accepteds = { + 'c', 'c++', 'objective-c', 'objective-c++', 'c-cpp-output', + 'c++-cpp-output', 'objective-c-cpp-output' + } + + key = 'language' + language = opts[key] if key in opts else \ + classify_source(opts['file'], opts['c++']) + + if language is None: + logging.debug('skip analysis, language not known') + return None + elif language not in accepteds: + logging.debug('skip analysis, language not supported') + return None + else: + logging.debug('analysis, language: %s', language) + opts.update({key: language}) + return continuation(opts) + + +@require([]) +def arch_check(opts, continuation=language_check): + """ Do run analyzer through one of the given architectures. """ + + disableds = {'ppc', 'ppc64'} + + key = 'archs_seen' + if key in opts: + # filter out disabled architectures and -arch switches + archs = [a for a in opts[key] if a not in disableds] + + if not archs: + logging.debug('skip analysis, found not supported arch') + return None + else: + # There should be only one arch given (or the same multiple + # times). If there are multiple arch are given and are not + # the same, those should not change the pre-processing step. + # But that's the only pass we have before run the analyzer. + arch = archs.pop() + logging.debug('analysis, on arch: %s', arch) + + opts.update({'arch': arch}) + del opts[key] + return continuation(opts) + else: + logging.debug('analysis, on default arch') + return continuation(opts) + + +@require(['action']) +def action_check(opts, continuation=arch_check): + """ Continue analysis only if it compilation or link. """ + + if opts.pop('action') <= Action.Compile: + return continuation(opts) + else: + logging.debug('skip analysis, not compilation nor link') + return None diff --git a/tools/scan-build-py/libscanbuild/shell.py b/tools/scan-build-py/libscanbuild/shell.py new file mode 100644 index 0000000000..a575946a95 --- /dev/null +++ b/tools/scan-build-py/libscanbuild/shell.py @@ -0,0 +1,66 @@ +# -*- 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 implements basic shell escaping/unescaping methods. """ + +import re +import shlex + +__all__ = ['encode', 'decode'] + + +def encode(command): + """ Takes a command as list and returns a string. """ + + def needs_quote(word): + """ Returns true if arguments needs to be protected by quotes. + + Previous implementation was shlex.split method, but that's not good + for this job. Currently is running through the string with a basic + state checking. """ + + reserved = {' ', '$', '%', '&', '(', ')', '[', ']', '{', '}', '*', '|', + '<', '>', '@', '?', '!'} + state = 0 + for current in word: + if state == 0 and current in reserved: + return True + elif state == 0 and current == '\\': + state = 1 + elif state == 1 and current in reserved | {'\\'}: + state = 0 + elif state == 0 and current == '"': + state = 2 + elif state == 2 and current == '"': + state = 0 + elif state == 0 and current == "'": + state = 3 + elif state == 3 and current == "'": + state = 0 + return state != 0 + + def escape(word): + """ Do protect argument if that's needed. """ + + table = {'\\': '\\\\', '"': '\\"'} + escaped = ''.join([table.get(c, c) for c in word]) + + return '"' + escaped + '"' if needs_quote(word) else escaped + + return " ".join([escape(arg) for arg in command]) + + +def decode(string): + """ Takes a command string and returns as a list. """ + + def unescape(arg): + """ Gets rid of the escaping characters. """ + + if len(arg) >= 2 and arg[0] == arg[-1] and arg[0] == '"': + arg = arg[1:-1] + return re.sub(r'\\(["\\])', r'\1', arg) + return re.sub(r'\\([\\ $%&\(\)\[\]\{\}\*|<>@?!])', r'\1', arg) + + return [unescape(arg) for arg in shlex.split(string)] |