summaryrefslogtreecommitdiffstats
path: root/tools/scan-build-py/libscanbuild/analyze.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/scan-build-py/libscanbuild/analyze.py')
-rw-r--r--tools/scan-build-py/libscanbuild/analyze.py502
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)