diff options
Diffstat (limited to 'tools/scan-build-py/libscanbuild/analyze.py')
-rw-r--r-- | tools/scan-build-py/libscanbuild/analyze.py | 502 |
1 files changed, 502 insertions, 0 deletions
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) |