forked from OSchip/llvm-project
781 lines
30 KiB
Python
781 lines
30 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
""" 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_EXTDEF_MAP_FILENAME = 'externalDefMap.txt'
|
|
CTU_TEMP_DEFMAP_FOLDER = 'tmpExternalDefMaps'
|
|
|
|
|
|
@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(r'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,
|
|
extdef_map_cmd=args.extdef_map_cmd)
|
|
if hasattr(args, 'ctu_phases') and hasattr(args.ctu_phases, 'dir')
|
|
else CtuConfig(collect=False, analyze=False, dir='', extdef_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],
|
|
extdef_map_cmd=ctu_config[3])
|
|
|
|
|
|
def create_global_ctu_extdef_map(extdef_map_lines):
|
|
""" Takes iterator of individual external definition maps and creates a
|
|
global map keeping only unique names. We leave conflicting names out of
|
|
CTU.
|
|
|
|
:param extdef_map_lines: Contains the id of a definition (mangled name) and
|
|
the originating source (the corresponding AST file) name.
|
|
:type extdef_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 extdef_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_extdef_maps(ctudir):
|
|
""" Merge individual external definition 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_DEFMAP_FOLDER.
|
|
These definition maps contain the mangled names and the source
|
|
(AST generated from the source) which had their definition.
|
|
These files should be merged at the end into a global map file:
|
|
CTU_EXTDEF_MAP_FILENAME."""
|
|
|
|
def generate_extdef_map_lines(extdefmap_dir):
|
|
""" Iterate over all lines of input files in a determined order. """
|
|
|
|
files = glob.glob(os.path.join(extdefmap_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 name, ast file) pairs into final file. """
|
|
|
|
extern_defs_map_file = os.path.join(ctudir, arch,
|
|
CTU_EXTDEF_MAP_FILENAME)
|
|
with open(extern_defs_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)
|
|
extdefmap_dir = os.path.join(ctudir, triple_arch,
|
|
CTU_TEMP_DEFMAP_FOLDER)
|
|
|
|
extdef_map_lines = generate_extdef_map_lines(extdefmap_dir)
|
|
mangled_ast_pairs = create_global_ctu_extdef_map(extdef_map_lines)
|
|
write_global_map(triple_arch, mangled_ast_pairs)
|
|
|
|
# Remove all temporary files
|
|
shutil.rmtree(extdefmap_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 extdef_map_cmd,
|
|
# so we can leave it empty
|
|
args.ctu_phases = CtuConfig(collect=True, analyze=False,
|
|
dir='', extdef_map_cmd='')
|
|
run_analyzer_parallel(args)
|
|
merge_ctu_extdef_maps(ctu_config.dir)
|
|
args.ctu_phases = CtuConfig(collect=False, analyze=True,
|
|
dir='', extdef_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_extdef_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 extdef_map_list_src_to_ast(extdef_src_list):
|
|
""" Turns textual external definition map list with source files into an
|
|
external definition map list with ast files. """
|
|
|
|
extdef_ast_list = []
|
|
for extdef_src_txt in extdef_src_list:
|
|
mangled_name, path = extdef_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")
|
|
extdef_ast_list.append(mangled_name + " " + ast_path)
|
|
return extdef_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_extdefs(triple_arch):
|
|
""" Generate external definition map file for the current source. """
|
|
|
|
args = opts['direct_args'] + opts['flags']
|
|
extdefmap_command = [opts['ctu'].extdef_map_cmd]
|
|
extdefmap_command.append(opts['file'])
|
|
extdefmap_command.append('--')
|
|
extdefmap_command.extend(args)
|
|
logging.debug("Generating external definition map using '%s'",
|
|
extdefmap_command)
|
|
extdef_src_list = run_command(extdefmap_command, cwd=opts['directory'])
|
|
extdef_ast_list = extdef_map_list_src_to_ast(extdef_src_list)
|
|
extern_defs_map_folder = os.path.join(opts['ctu'].dir, triple_arch,
|
|
CTU_TEMP_DEFMAP_FOLDER)
|
|
if not os.path.isdir(extern_defs_map_folder):
|
|
try:
|
|
os.makedirs(extern_defs_map_folder)
|
|
except OSError:
|
|
# In case an other process already created it.
|
|
pass
|
|
if extdef_ast_list:
|
|
with tempfile.NamedTemporaryFile(mode='w',
|
|
dir=extern_defs_map_folder,
|
|
delete=False) as out_file:
|
|
out_file.write("\n".join(extdef_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_extdefs(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
|