| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779 |
- # -*- 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])
- 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
|