| # -*- 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 re |
| import os |
| import os.path |
| import json |
| import logging |
| import multiprocessing |
| import tempfile |
| import functools |
| import subprocess |
| import contextlib |
| import datetime |
| import shutil |
| import glob |
| from collections import defaultdict |
| |
| from libscanbuild import command_entry_point, compiler_wrapper, \ |
| wrapper_environment, run_build, run_command, CtuConfig |
| from libscanbuild.arguments import parse_args_for_scan_build, \ |
| parse_args_for_analyze_build |
| from libscanbuild.intercept import capture |
| from libscanbuild.report import document |
| from libscanbuild.compilation import split_command, classify_source, \ |
| compiler_language |
| from libscanbuild.clang import get_version, get_arguments, get_triple_arch |
| from libscanbuild.shell import decode |
| |
| __all__ = ['scan_build', 'analyze_build', 'analyze_compiler_wrapper'] |
| |
| COMPILER_WRAPPER_CC = 'analyze-cc' |
| COMPILER_WRAPPER_CXX = 'analyze-c++' |
| |
| CTU_FUNCTION_MAP_FILENAME = 'externalFnMap.txt' |
| CTU_TEMP_FNMAP_FOLDER = 'tmpExternalFnMaps' |
| |
| |
| @command_entry_point |
| def scan_build(): |
| """ Entry point for scan-build command. """ |
| |
| args = parse_args_for_scan_build() |
| # will re-assign the report directory as new output |
| with report_directory(args.output, args.keep_empty) as args.output: |
| # Run against a build command. there are cases, when analyzer run |
| # is not required. But we need to set up everything for the |
| # wrappers, because 'configure' needs to capture the CC/CXX values |
| # for the Makefile. |
| if args.intercept_first: |
| # Run build command with intercept module. |
| exit_code = capture(args) |
| # Run the analyzer against the captured commands. |
| if need_analyzer(args.build): |
| govern_analyzer_runs(args) |
| else: |
| # Run build command and analyzer with compiler wrappers. |
| environment = setup_environment(args) |
| exit_code = run_build(args.build, env=environment) |
| # Cover report generation and bug counting. |
| number_of_bugs = document(args) |
| # Set exit status as it was requested. |
| return number_of_bugs if args.status_bugs else exit_code |
| |
| |
| @command_entry_point |
| def analyze_build(): |
| """ Entry point for analyze-build command. """ |
| |
| args = parse_args_for_analyze_build() |
| # will re-assign the report directory as new output |
| with report_directory(args.output, args.keep_empty) as args.output: |
| # Run the analyzer against a compilation db. |
| govern_analyzer_runs(args) |
| # Cover report generation and bug counting. |
| number_of_bugs = document(args) |
| # Set exit status as it was requested. |
| return number_of_bugs if args.status_bugs else 0 |
| |
| |
| 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 necessary, |
| 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 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]] |
| |
| |
| def get_ctu_config_from_args(args): |
| """ CTU configuration is created from the chosen phases and dir. """ |
| |
| return ( |
| CtuConfig(collect=args.ctu_phases.collect, |
| analyze=args.ctu_phases.analyze, |
| dir=args.ctu_dir, |
| func_map_cmd=args.func_map_cmd) |
| if hasattr(args, 'ctu_phases') and hasattr(args.ctu_phases, 'dir') |
| else CtuConfig(collect=False, analyze=False, dir='', func_map_cmd='')) |
| |
| |
| def get_ctu_config_from_json(ctu_conf_json): |
| """ CTU configuration is created from the chosen phases and dir. """ |
| |
| ctu_config = json.loads(ctu_conf_json) |
| # Recover namedtuple from json when coming from analyze-cc or analyze-c++ |
| return CtuConfig(collect=ctu_config[0], |
| analyze=ctu_config[1], |
| dir=ctu_config[2], |
| func_map_cmd=ctu_config[3]) |
| |
| |
| def create_global_ctu_function_map(func_map_lines): |
| """ Takes iterator of individual function maps and creates a global map |
| keeping only unique names. We leave conflicting names out of CTU. |
| |
| :param func_map_lines: Contains the id of a function (mangled name) and |
| the originating source (the corresponding AST file) name. |
| :type func_map_lines: Iterator of str. |
| :returns: Mangled name - AST file pairs. |
| :rtype: List of (str, str) tuples. |
| """ |
| |
| mangled_to_asts = defaultdict(set) |
| |
| for line in func_map_lines: |
| mangled_name, ast_file = line.strip().split(' ', 1) |
| mangled_to_asts[mangled_name].add(ast_file) |
| |
| mangled_ast_pairs = [] |
| |
| for mangled_name, ast_files in mangled_to_asts.items(): |
| if len(ast_files) == 1: |
| mangled_ast_pairs.append((mangled_name, next(iter(ast_files)))) |
| |
| return mangled_ast_pairs |
| |
| |
| def merge_ctu_func_maps(ctudir): |
| """ Merge individual function maps into a global one. |
| |
| As the collect phase runs parallel on multiple threads, all compilation |
| units are separately mapped into a temporary file in CTU_TEMP_FNMAP_FOLDER. |
| These function maps contain the mangled names of functions and the source |
| (AST generated from the source) which had them. |
| These files should be merged at the end into a global map file: |
| CTU_FUNCTION_MAP_FILENAME.""" |
| |
| def generate_func_map_lines(fnmap_dir): |
| """ Iterate over all lines of input files in a determined order. """ |
| |
| files = glob.glob(os.path.join(fnmap_dir, '*')) |
| files.sort() |
| for filename in files: |
| with open(filename, 'r') as in_file: |
| for line in in_file: |
| yield line |
| |
| def write_global_map(arch, mangled_ast_pairs): |
| """ Write (mangled function name, ast file) pairs into final file. """ |
| |
| extern_fns_map_file = os.path.join(ctudir, arch, |
| CTU_FUNCTION_MAP_FILENAME) |
| with open(extern_fns_map_file, 'w') as out_file: |
| for mangled_name, ast_file in mangled_ast_pairs: |
| out_file.write('%s %s\n' % (mangled_name, ast_file)) |
| |
| triple_arches = glob.glob(os.path.join(ctudir, '*')) |
| for triple_path in triple_arches: |
| if os.path.isdir(triple_path): |
| triple_arch = os.path.basename(triple_path) |
| fnmap_dir = os.path.join(ctudir, triple_arch, |
| CTU_TEMP_FNMAP_FOLDER) |
| |
| func_map_lines = generate_func_map_lines(fnmap_dir) |
| mangled_ast_pairs = create_global_ctu_function_map(func_map_lines) |
| write_global_map(triple_arch, mangled_ast_pairs) |
| |
| # Remove all temporary files |
| shutil.rmtree(fnmap_dir, ignore_errors=True) |
| |
| |
| def run_analyzer_parallel(args): |
| """ 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': args.output, |
| 'output_format': args.output_format, |
| 'output_failures': args.output_failures, |
| 'direct_args': analyzer_params(args), |
| 'force_debug': args.force_debug, |
| 'ctu': get_ctu_config_from_args(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 govern_analyzer_runs(args): |
| """ Governs multiple runs in CTU mode or runs once in normal mode. """ |
| |
| ctu_config = get_ctu_config_from_args(args) |
| # If we do a CTU collect (1st phase) we remove all previous collection |
| # data first. |
| if ctu_config.collect: |
| shutil.rmtree(ctu_config.dir, ignore_errors=True) |
| |
| # If the user asked for a collect (1st) and analyze (2nd) phase, we do an |
| # all-in-one run where we deliberately remove collection data before and |
| # also after the run. If the user asks only for a single phase data is |
| # left so multiple analyze runs can use the same data gathered by a single |
| # collection run. |
| if ctu_config.collect and ctu_config.analyze: |
| # CTU strings are coming from args.ctu_dir and func_map_cmd, |
| # so we can leave it empty |
| args.ctu_phases = CtuConfig(collect=True, analyze=False, |
| dir='', func_map_cmd='') |
| run_analyzer_parallel(args) |
| merge_ctu_func_maps(ctu_config.dir) |
| args.ctu_phases = CtuConfig(collect=False, analyze=True, |
| dir='', func_map_cmd='') |
| run_analyzer_parallel(args) |
| shutil.rmtree(ctu_config.dir, ignore_errors=True) |
| else: |
| # Single runs (collect or analyze) are launched from here. |
| run_analyzer_parallel(args) |
| if ctu_config.collect: |
| merge_ctu_func_maps(ctu_config.dir) |
| |
| |
| def setup_environment(args): |
| """ Set up environment for build command to interpose compiler wrapper. """ |
| |
| environment = dict(os.environ) |
| environment.update(wrapper_environment(args)) |
| environment.update({ |
| 'CC': COMPILER_WRAPPER_CC, |
| 'CXX': COMPILER_WRAPPER_CXX, |
| 'ANALYZE_BUILD_CLANG': args.clang if need_analyzer(args.build) else '', |
| 'ANALYZE_BUILD_REPORT_DIR': args.output, |
| 'ANALYZE_BUILD_REPORT_FORMAT': args.output_format, |
| 'ANALYZE_BUILD_REPORT_FAILURES': 'yes' if args.output_failures else '', |
| 'ANALYZE_BUILD_PARAMETERS': ' '.join(analyzer_params(args)), |
| 'ANALYZE_BUILD_FORCE_DEBUG': 'yes' if args.force_debug else '', |
| 'ANALYZE_BUILD_CTU': json.dumps(get_ctu_config_from_args(args)) |
| }) |
| return environment |
| |
| |
| @command_entry_point |
| def analyze_compiler_wrapper(): |
| """ Entry point for `analyze-cc` and `analyze-c++` compiler wrappers. """ |
| |
| return compiler_wrapper(analyze_compiler_wrapper_impl) |
| |
| |
| def analyze_compiler_wrapper_impl(result, execution): |
| """ Implements analyzer compiler wrapper functionality. """ |
| |
| # don't run analyzer when compilation fails. or when it's not requested. |
| if result or not os.getenv('ANALYZE_BUILD_CLANG'): |
| return |
| |
| # check is it a compilation? |
| compilation = split_command(execution.cmd) |
| if compilation is None: |
| return |
| # collect the needed parameters from environment, crash when missing |
| parameters = { |
| '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(' '), |
| 'force_debug': os.getenv('ANALYZE_BUILD_FORCE_DEBUG'), |
| 'directory': execution.cwd, |
| 'command': [execution.cmd[0], '-c'] + compilation.flags, |
| 'ctu': get_ctu_config_from_json(os.getenv('ANALYZE_BUILD_CTU')) |
| } |
| # call static analyzer against the compilation |
| for source in compilation.files: |
| parameters.update({'file': source}) |
| logging.debug('analyzer parameters %s', parameters) |
| current = run(parameters) |
| # display error message from the static analyzer |
| if current is not None: |
| for line in current['error_output']: |
| logging.info(line.rstrip()) |
| |
| |
| @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_format = 'scan-build-%Y-%m-%d-%H-%M-%S-%f-' |
| stamp = datetime.datetime.now().strftime(stamp_format) |
| parent_dir = os.path.abspath(hint) |
| if not os.path.exists(parent_dir): |
| os.makedirs(parent_dir) |
| name = tempfile.mkdtemp(prefix=stamp, dir=parent_dir) |
| |
| 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' contains 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 analyzer_params(args): |
| """ A group of command line arguments can mapped to command |
| line arguments of the analyzer. This method generates those. """ |
| |
| 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.extend(['-analyzer-config', 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 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', # entry from compilation database |
| 'directory', # entry from compilation database |
| 'file', # entry from compilation database |
| 'clang', # clang executable name (and path) |
| 'direct_args', # arguments from command line |
| 'force_debug', # kill non debug macros |
| 'output_dir', # where generated report files shall go |
| 'output_format', # it's 'plist', 'html', both or plist-multi-file |
| 'output_failures', # generate crash reports or not |
| 'ctu']) # ctu control options |
| 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 possible 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') |
| command = command if isinstance(command, list) else decode(command) |
| logging.debug("Run analyzer against '%s'", command) |
| opts.update(classify_parameters(command)) |
| |
| return arch_check(opts) |
| except Exception: |
| logging.error("Problem occurred during analyzis.", exc_info=1) |
| return None |
| |
| |
| @require(['clang', 'directory', 'flags', 'file', 'output_dir', 'language', |
| '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(): |
| """ Generate preprocessor file extension. """ |
| |
| mapping = {'objective-c++': '.mii', 'objective-c': '.mi', 'c++': '.ii'} |
| return mapping.get(opts['language'], '.i') |
| |
| def destination(): |
| """ Creates failures directory if not exits yet. """ |
| |
| failures_dir = os.path.join(opts['output_dir'], 'failures') |
| if not os.path.isdir(failures_dir): |
| os.makedirs(failures_dir) |
| return failures_dir |
| |
| # Classify error type: when Clang terminated by a signal it's a 'Crash'. |
| # (python subprocess Popen.returncode is negative when child terminated |
| # by signal.) Everything else is 'Other Error'. |
| error = 'crash' if opts['exit_code'] < 0 else 'other_error' |
| # Create preprocessor output file name. (This is blindly following the |
| # Perl implementation.) |
| (handle, name) = tempfile.mkstemp(suffix=extension(), |
| prefix='clang_' + error + '_', |
| dir=destination()) |
| os.close(handle) |
| # Execute Clang again, but run the syntax check only. |
| cwd = opts['directory'] |
| cmd = get_arguments( |
| [opts['clang'], '-fsyntax-only', '-E' |
| ] + opts['flags'] + [opts['file'], '-o', name], cwd) |
| run_command(cmd, cwd=cwd) |
| # write general information about the crash |
| 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(opts['clang'])) |
| handle.close() |
| # write the captured output too |
| with open(name + '.stderr.txt', 'w') as handle: |
| handle.writelines(opts['error_output']) |
| handle.close() |
| |
| |
| @require(['clang', 'directory', 'flags', 'direct_args', 'file', 'output_dir', |
| 'output_format']) |
| 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. """ |
| |
| def target(): |
| """ Creates output file name for reports. """ |
| if opts['output_format'] in { |
| 'plist', |
| 'plist-html', |
| 'plist-multi-file'}: |
| (handle, name) = tempfile.mkstemp(prefix='report-', |
| suffix='.plist', |
| dir=opts['output_dir']) |
| os.close(handle) |
| return name |
| return opts['output_dir'] |
| |
| try: |
| cwd = opts['directory'] |
| cmd = get_arguments([opts['clang'], '--analyze'] + |
| opts['direct_args'] + opts['flags'] + |
| [opts['file'], '-o', target()], |
| cwd) |
| output = run_command(cmd, cwd=cwd) |
| return {'error_output': output, 'exit_code': 0} |
| except subprocess.CalledProcessError as ex: |
| result = {'error_output': ex.output, 'exit_code': ex.returncode} |
| if opts.get('output_failures', False): |
| opts.update(result) |
| continuation(opts) |
| return result |
| |
| |
| def func_map_list_src_to_ast(func_src_list): |
| """ Turns textual function map list with source files into a |
| function map list with ast files. """ |
| |
| func_ast_list = [] |
| for fn_src_txt in func_src_list: |
| mangled_name, path = fn_src_txt.split(" ", 1) |
| # Normalize path on windows as well |
| path = os.path.splitdrive(path)[1] |
| # Make relative path out of absolute |
| path = path[1:] if path[0] == os.sep else path |
| ast_path = os.path.join("ast", path + ".ast") |
| func_ast_list.append(mangled_name + " " + ast_path) |
| return func_ast_list |
| |
| |
| @require(['clang', 'directory', 'flags', 'direct_args', 'file', 'ctu']) |
| def ctu_collect_phase(opts): |
| """ Preprocess source by generating all data needed by CTU analysis. """ |
| |
| def generate_ast(triple_arch): |
| """ Generates ASTs for the current compilation command. """ |
| |
| args = opts['direct_args'] + opts['flags'] |
| ast_joined_path = os.path.join(opts['ctu'].dir, triple_arch, 'ast', |
| os.path.realpath(opts['file'])[1:] + |
| '.ast') |
| ast_path = os.path.abspath(ast_joined_path) |
| ast_dir = os.path.dirname(ast_path) |
| if not os.path.isdir(ast_dir): |
| try: |
| os.makedirs(ast_dir) |
| except OSError: |
| # In case an other process already created it. |
| pass |
| ast_command = [opts['clang'], '-emit-ast'] |
| ast_command.extend(args) |
| ast_command.append('-w') |
| ast_command.append(opts['file']) |
| ast_command.append('-o') |
| ast_command.append(ast_path) |
| logging.debug("Generating AST using '%s'", ast_command) |
| run_command(ast_command, cwd=opts['directory']) |
| |
| def map_functions(triple_arch): |
| """ Generate function map file for the current source. """ |
| |
| args = opts['direct_args'] + opts['flags'] |
| funcmap_command = [opts['ctu'].func_map_cmd] |
| funcmap_command.append(opts['file']) |
| funcmap_command.append('--') |
| funcmap_command.extend(args) |
| logging.debug("Generating function map using '%s'", funcmap_command) |
| func_src_list = run_command(funcmap_command, cwd=opts['directory']) |
| func_ast_list = func_map_list_src_to_ast(func_src_list) |
| extern_fns_map_folder = os.path.join(opts['ctu'].dir, triple_arch, |
| CTU_TEMP_FNMAP_FOLDER) |
| if not os.path.isdir(extern_fns_map_folder): |
| try: |
| os.makedirs(extern_fns_map_folder) |
| except OSError: |
| # In case an other process already created it. |
| pass |
| if func_ast_list: |
| with tempfile.NamedTemporaryFile(mode='w', |
| dir=extern_fns_map_folder, |
| delete=False) as out_file: |
| out_file.write("\n".join(func_ast_list) + "\n") |
| |
| cwd = opts['directory'] |
| cmd = [opts['clang'], '--analyze'] + opts['direct_args'] + opts['flags'] \ |
| + [opts['file']] |
| triple_arch = get_triple_arch(cmd, cwd) |
| generate_ast(triple_arch) |
| map_functions(triple_arch) |
| |
| |
| @require(['ctu']) |
| def dispatch_ctu(opts, continuation=run_analyzer): |
| """ Execute only one phase of 2 phases of CTU if needed. """ |
| |
| ctu_config = opts['ctu'] |
| |
| if ctu_config.collect or ctu_config.analyze: |
| assert ctu_config.collect != ctu_config.analyze |
| if ctu_config.collect: |
| return ctu_collect_phase(opts) |
| if ctu_config.analyze: |
| cwd = opts['directory'] |
| cmd = [opts['clang'], '--analyze'] + opts['direct_args'] \ |
| + opts['flags'] + [opts['file']] |
| triarch = get_triple_arch(cmd, cwd) |
| ctu_options = ['ctu-dir=' + os.path.join(ctu_config.dir, triarch), |
| 'experimental-enable-naive-ctu-analysis=true'] |
| analyzer_options = prefix_with('-analyzer-config', ctu_options) |
| direct_options = prefix_with('-Xanalyzer', analyzer_options) |
| opts['direct_args'].extend(direct_options) |
| |
| return continuation(opts) |
| |
| |
| @require(['flags', 'force_debug']) |
| def filter_debug_flags(opts, continuation=dispatch_ctu): |
| """ Filter out nondebug macros when requested. """ |
| |
| if opts.pop('force_debug'): |
| # lazy implementation just append an undefine macro at the end |
| opts.update({'flags': opts['flags'] + ['-UNDEBUG']}) |
| |
| return continuation(opts) |
| |
| |
| @require(['language', 'compiler', 'file', 'flags']) |
| def language_check(opts, continuation=filter_debug_flags): |
| """ Find out the language from command line parameters or file name |
| extension. The decision also influenced by the compiler invocation. """ |
| |
| accepted = frozenset({ |
| 'c', 'c++', 'objective-c', 'objective-c++', 'c-cpp-output', |
| 'c++-cpp-output', 'objective-c-cpp-output' |
| }) |
| |
| # language can be given as a parameter... |
| language = opts.pop('language') |
| compiler = opts.pop('compiler') |
| # ... or find out from source file extension |
| if language is None and compiler is not None: |
| language = classify_source(opts['file'], compiler == 'c') |
| |
| if language is None: |
| logging.debug('skip analysis, language not known') |
| return None |
| elif language not in accepted: |
| logging.debug('skip analysis, language not supported') |
| return None |
| else: |
| logging.debug('analysis, language: %s', language) |
| opts.update({'language': language, |
| 'flags': ['-x', language] + opts['flags']}) |
| return continuation(opts) |
| |
| |
| @require(['arch_list', 'flags']) |
| def arch_check(opts, continuation=language_check): |
| """ Do run analyzer through one of the given architectures. """ |
| |
| disabled = frozenset({'ppc', 'ppc64'}) |
| |
| received_list = opts.pop('arch_list') |
| if received_list: |
| # filter out disabled architectures and -arch switches |
| filtered_list = [a for a in received_list if a not in disabled] |
| if filtered_list: |
| # 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. |
| current = filtered_list.pop() |
| logging.debug('analysis, on arch: %s', current) |
| |
| opts.update({'flags': ['-arch', current] + opts['flags']}) |
| return continuation(opts) |
| else: |
| logging.debug('skip analysis, found not supported arch') |
| return None |
| else: |
| logging.debug('analysis, on default arch') |
| return continuation(opts) |
| |
| |
| # To have good results from static analyzer certain compiler options shall be |
| # omitted. The compiler flag filtering only affects the static analyzer run. |
| # |
| # Keys are the option name, value number of options to skip |
| IGNORED_FLAGS = { |
| '-c': 0, # compile option will be overwritten |
| '-fsyntax-only': 0, # static analyzer option will be overwritten |
| '-o': 1, # will set up own output file |
| # flags below are inherited from the perl implementation. |
| '-g': 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 |
| } |
| |
| |
| def classify_parameters(command): |
| """ Prepare compiler flags (filters some and add others) and take out |
| language (-x) and architecture (-arch) flags for future processing. """ |
| |
| result = { |
| 'flags': [], # the filtered compiler flags |
| 'arch_list': [], # list of architecture flags |
| 'language': None, # compilation language, None, if not specified |
| 'compiler': compiler_language(command) # 'c' or 'c++' |
| } |
| |
| # iterate on the compile options |
| args = iter(command[1:]) |
| for arg in args: |
| # take arch flags into a separate basket |
| if arg == '-arch': |
| result['arch_list'].append(next(args)) |
| # take language |
| elif arg == '-x': |
| result['language'] = next(args) |
| # parameters which looks source file are not flags |
| elif re.match(r'^[^-].+', arg) and classify_source(arg): |
| pass |
| # ignore some flags |
| elif arg in IGNORED_FLAGS: |
| count = IGNORED_FLAGS[arg] |
| for _ in range(count): |
| next(args) |
| # we don't care about extra warnings, but we should suppress ones |
| # that we don't want to see. |
| elif re.match(r'^-W.+', arg) and not re.match(r'^-Wno-.+', arg): |
| pass |
| # and consider everything else as compilation flag. |
| else: |
| result['flags'].append(arg) |
| |
| return result |