summaryrefslogtreecommitdiffstats
path: root/clang/tools/scan-build-py/libscanbuild/runner.py
diff options
context:
space:
mode:
Diffstat (limited to 'clang/tools/scan-build-py/libscanbuild/runner.py')
-rw-r--r--clang/tools/scan-build-py/libscanbuild/runner.py230
1 files changed, 140 insertions, 90 deletions
diff --git a/clang/tools/scan-build-py/libscanbuild/runner.py b/clang/tools/scan-build-py/libscanbuild/runner.py
index 63b9f743699..628ad90d627 100644
--- a/clang/tools/scan-build-py/libscanbuild/runner.py
+++ b/clang/tools/scan-build-py/libscanbuild/runner.py
@@ -5,18 +5,44 @@
# License. See LICENSE.TXT for details.
""" This module is responsible to run the analyzer commands. """
+import re
import os
import os.path
import tempfile
import functools
import subprocess
import logging
-from libscanbuild.command import classify_parameters, Action, classify_source
-from libscanbuild.clang import get_arguments, get_version
+from libscanbuild.compilation import classify_source, compiler_language
+from libscanbuild.clang import get_version, get_arguments
from libscanbuild.shell import decode
__all__ = ['run']
+# 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 require(required):
""" Decorator for checking the required values in state.
@@ -29,8 +55,8 @@ def require(required):
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__))
+ raise KeyError('{0} not passed to {1}'.format(
+ key, function.__name__))
return function(*args, **kwargs)
@@ -39,10 +65,15 @@ def require(required):
return decorator
-@require(['command', 'directory', 'file', # an entry from compilation database
- 'clang', 'direct_args', # compiler name, and arguments from command
- 'force_analyze_debug_code', # preprocessing options
- 'output_dir', 'output_format', 'output_failures'])
+@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' or 'html' or both
+ 'output_failures']) # generate crash reports or not
def run(opts):
""" Entry point to run (or not) static analyzer against a single entry
of the compilation database.
@@ -58,16 +89,17 @@ def run(opts):
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(decode(command)))
+ opts.update(classify_parameters(command))
- return action_check(opts)
+ return arch_check(opts)
except Exception:
logging.error("Problem occured during analyzis.", exc_info=1)
return None
-@require(['report', 'directory', 'clang', 'output_dir', 'language', 'file',
+@require(['clang', 'directory', 'flags', 'file', 'output_dir', 'language',
'error_type', 'error_output', 'exit_code'])
def report_failure(opts):
""" Create report when analyzer failed.
@@ -96,36 +128,49 @@ def report_failure(opts):
dir=destination(opts))
os.close(handle)
cwd = opts['directory']
- cmd = get_arguments([opts['clang']] + opts['report'] + ['-o', name], cwd)
+ cmd = get_arguments([opts['clang'], '-fsyntax-only', '-E'] +
+ opts['flags'] + [opts['file'], '-o', name], cwd)
logging.debug('exec command in %s: %s', cwd, ' '.join(cmd))
subprocess.call(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(cmd[0]))
+ 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()
-
+ # return with the previous step exit code and output
return {
'error_output': opts['error_output'],
'exit_code': opts['exit_code']
}
-@require(['clang', 'analyze', 'directory', 'output'])
+@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 output():
+ """ Creates output file name for reports. """
+ if opts['output_format'] in {'plist', 'plist-html'}:
+ (handle, name) = tempfile.mkstemp(prefix='report-',
+ suffix='.plist',
+ dir=opts['output_dir'])
+ os.close(handle)
+ return name
+ return opts['output_dir']
+
cwd = opts['directory']
- cmd = get_arguments([opts['clang']] + opts['analyze'] + opts['output'],
+ cmd = get_arguments([opts['clang'], '--analyze'] + opts['direct_args'] +
+ opts['flags'] + [opts['file'], '-o', output()],
cwd)
logging.debug('exec command in %s: %s', cwd, ' '.join(cmd))
child = subprocess.Popen(cmd,
@@ -145,119 +190,124 @@ def run_analyzer(opts, continuation=report_failure):
'exit_code': child.returncode
})
return continuation(opts)
+ # return the output for logging and exit code for testing
return {'error_output': output, 'exit_code': child.returncode}
-@require(['output_dir'])
-def set_analyzer_output(opts, continuation=run_analyzer):
- """ Create output file if was requested.
+@require(['flags', 'force_debug'])
+def filter_debug_flags(opts, continuation=run_analyzer):
+ """ Filter out nondebug macros when requested. """
- This plays a role only if .plist files are requested. """
+ if opts.pop('force_debug'):
+ # lazy implementation just append an undefine macro at the end
+ opts.update({'flags': opts['flags'] + ['-UNDEBUG']})
- if opts.get('output_format') in {'plist', 'plist-html'}:
- with tempfile.NamedTemporaryFile(prefix='report-',
- suffix='.plist',
- delete=False,
- dir=opts['output_dir']) as output:
- opts.update({'output': ['-o', output.name]})
- return continuation(opts)
- else:
- opts.update({'output': ['-o', opts['output_dir']]})
- return continuation(opts)
+ return continuation(opts)
-def force_analyze_debug_code(cmd):
- """ Enable assert()'s by undefining NDEBUG. """
- cmd.append('-UNDEBUG')
-
-@require(['file', 'directory', 'clang', 'direct_args',
- 'force_analyze_debug_code', 'language', 'output_dir',
- 'output_format', 'output_failures'])
-def create_commands(opts, continuation=set_analyzer_output):
- """ Create command to run analyzer or failure report generation.
-
- It generates commands (from compilation database entries) which contains
- enough information to run the analyzer (and the crash report generation
- if that was requested). """
-
- common = []
- if 'arch' in opts:
- common.extend(['-arch', opts.pop('arch')])
- common.extend(opts.pop('compile_options', []))
- if opts['force_analyze_debug_code']:
- force_analyze_debug_code(common)
- common.extend(['-x', opts['language']])
- common.append(os.path.relpath(opts['file'], opts['directory']))
-
- opts.update({
- 'analyze': ['--analyze'] + opts['direct_args'] + common,
- 'report': ['-fsyntax-only', '-E'] + common
- })
+
+@require(['file', 'directory'])
+def set_file_path_relative(opts, continuation=filter_debug_flags):
+ """ Set source file path to relative to the working directory.
+
+ The only purpose of this function is to pass the SATestBuild.py tests. """
+
+ opts.update({'file': os.path.relpath(opts['file'], opts['directory'])})
return continuation(opts)
-@require(['file', 'c++'])
-def language_check(opts, continuation=create_commands):
+@require(['language', 'compiler', 'file', 'flags'])
+def language_check(opts, continuation=set_file_path_relative):
""" Find out the language from command line parameters or file name
extension. The decision also influenced by the compiler invocation. """
- accepteds = {
+ accepted = frozenset({
'c', 'c++', 'objective-c', 'objective-c++', 'c-cpp-output',
'c++-cpp-output', 'objective-c-cpp-output'
- }
+ })
- key = 'language'
- language = opts[key] if key in opts else \
- classify_source(opts['file'], opts['c++'])
+ # 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 accepteds:
+ elif language not in accepted:
logging.debug('skip analysis, language not supported')
return None
else:
logging.debug('analysis, language: %s', language)
- opts.update({key: language})
+ opts.update({'language': language,
+ 'flags': ['-x', language] + opts['flags']})
return continuation(opts)
-@require([])
+@require(['arch_list', 'flags'])
def arch_check(opts, continuation=language_check):
""" Do run analyzer through one of the given architectures. """
- disableds = {'ppc', 'ppc64'}
+ disabled = frozenset({'ppc', 'ppc64'})
- key = 'archs_seen'
- if key in opts:
+ received_list = opts.pop('arch_list')
+ if received_list:
# filter out disabled architectures and -arch switches
- archs = [a for a in opts[key] if a not in disableds]
-
- if not archs:
- logging.debug('skip analysis, found not supported arch')
- return None
- else:
+ 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.
- arch = archs.pop()
- logging.debug('analysis, on arch: %s', arch)
+ current = filtered_list.pop()
+ logging.debug('analysis, on arch: %s', current)
- opts.update({'arch': arch})
- del opts[key]
+ 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)
-@require(['action'])
-def action_check(opts, continuation=arch_check):
- """ Continue analysis only if it compilation or link. """
+def classify_parameters(command):
+ """ Prepare compiler flags (filters some and add others) and take out
+ language (-x) and architecture (-arch) flags for future processing. """
- if opts.pop('action') <= Action.Compile:
- return continuation(opts)
- else:
- logging.debug('skip analysis, not compilation nor link')
- return None
+ 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
OpenPOWER on IntegriCloud