1 # -*- coding: utf-8 -*-
2 # The LLVM Compiler Infrastructure
4 # This file is distributed under the University of Illinois Open Source
5 # License. See LICENSE.TXT for details.
6 """ This module is responsible to run the analyzer commands. """
15 from libscanbuild.compilation import classify_source, compiler_language
16 from libscanbuild.clang import get_version, get_arguments
17 from libscanbuild.shell import decode
21 # To have good results from static analyzer certain compiler options shall be
22 # omitted. The compiler flag filtering only affects the static analyzer run.
24 # Keys are the option name, value number of options to skip
26 '-c': 0, # compile option will be overwritten
27 '-fsyntax-only': 0, # static analyzer option will be overwritten
28 '-o': 1, # will set up own output file
29 # flags below are inherited from the perl implementation.
33 '-exported_symbols_list': 1,
34 '-current_version': 1,
35 '-compatibility_version': 1,
40 '-multiply_defined': 1,
43 '--serialize-diagnostics': 1
47 def require(required):
48 """ Decorator for checking the required values in state.
50 It checks the required attributes in the passed state and stop when
51 any of those is missing. """
53 def decorator(function):
54 @functools.wraps(function)
55 def wrapper(*args, **kwargs):
57 if key not in args[0]:
58 raise KeyError('{0} not passed to {1}'.format(
59 key, function.__name__))
61 return function(*args, **kwargs)
68 @require(['command', # entry from compilation database
69 'directory', # entry from compilation database
70 'file', # entry from compilation database
71 'clang', # clang executable name (and path)
72 'direct_args', # arguments from command line
73 'force_debug', # kill non debug macros
74 'output_dir', # where generated report files shall go
75 'output_format', # it's 'plist' or 'html' or both
76 'output_failures']) # generate crash reports or not
78 """ Entry point to run (or not) static analyzer against a single entry
79 of the compilation database.
81 This complex task is decomposed into smaller methods which are calling
82 each other in chain. If the analyzis is not possibe the given method
83 just return and break the chain.
85 The passed parameter is a python dictionary. Each method first check
86 that the needed parameters received. (This is done by the 'require'
87 decorator. It's like an 'assert' to check the contract between the
88 caller and the called method.) """
91 command = opts.pop('command')
92 command = command if isinstance(command, list) else decode(command)
93 logging.debug("Run analyzer against '%s'", command)
94 opts.update(classify_parameters(command))
96 return arch_check(opts)
98 logging.error("Problem occured during analyzis.", exc_info=1)
102 @require(['clang', 'directory', 'flags', 'file', 'output_dir', 'language',
103 'error_type', 'error_output', 'exit_code'])
104 def report_failure(opts):
105 """ Create report when analyzer failed.
107 The major report is the preprocessor output. The output filename generated
108 randomly. The compiler output also captured into '.stderr.txt' file.
109 And some more execution context also saved into '.info.txt' file. """
112 """ Generate preprocessor file extension. """
114 mapping = {'objective-c++': '.mii', 'objective-c': '.mi', 'c++': '.ii'}
115 return mapping.get(opts['language'], '.i')
117 def destination(opts):
118 """ Creates failures directory if not exits yet. """
120 name = os.path.join(opts['output_dir'], 'failures')
121 if not os.path.isdir(name):
125 error = opts['error_type']
126 (handle, name) = tempfile.mkstemp(suffix=extension(opts),
127 prefix='clang_' + error + '_',
128 dir=destination(opts))
130 cwd = opts['directory']
131 cmd = get_arguments([opts['clang'], '-fsyntax-only', '-E'] +
132 opts['flags'] + [opts['file'], '-o', name], cwd)
133 logging.debug('exec command in %s: %s', cwd, ' '.join(cmd))
134 subprocess.call(cmd, cwd=cwd)
135 # write general information about the crash
136 with open(name + '.info.txt', 'w') as handle:
137 handle.write(opts['file'] + os.linesep)
138 handle.write(error.title().replace('_', ' ') + os.linesep)
139 handle.write(' '.join(cmd) + os.linesep)
140 handle.write(' '.join(os.uname()) + os.linesep)
141 handle.write(get_version(opts['clang']))
143 # write the captured output too
144 with open(name + '.stderr.txt', 'w') as handle:
145 handle.writelines(opts['error_output'])
147 # return with the previous step exit code and output
149 'error_output': opts['error_output'],
150 'exit_code': opts['exit_code']
154 @require(['clang', 'directory', 'flags', 'direct_args', 'file', 'output_dir',
156 def run_analyzer(opts, continuation=report_failure):
157 """ It assembles the analysis command line and executes it. Capture the
158 output of the analysis and returns with it. If failure reports are
159 requested, it calls the continuation to generate it. """
162 """ Creates output file name for reports. """
163 if opts['output_format'] in {'plist', 'plist-html'}:
164 (handle, name) = tempfile.mkstemp(prefix='report-',
166 dir=opts['output_dir'])
169 return opts['output_dir']
171 cwd = opts['directory']
172 cmd = get_arguments([opts['clang'], '--analyze'] + opts['direct_args'] +
173 opts['flags'] + [opts['file'], '-o', output()],
175 logging.debug('exec command in %s: %s', cwd, ' '.join(cmd))
176 child = subprocess.Popen(cmd,
178 universal_newlines=True,
179 stdout=subprocess.PIPE,
180 stderr=subprocess.STDOUT)
181 output = child.stdout.readlines()
183 # do report details if it were asked
185 if opts.get('output_failures', False) and child.returncode:
186 error_type = 'crash' if child.returncode & 127 else 'other_error'
188 'error_type': error_type,
189 'error_output': output,
190 'exit_code': child.returncode
192 return continuation(opts)
193 # return the output for logging and exit code for testing
194 return {'error_output': output, 'exit_code': child.returncode}
197 @require(['flags', 'force_debug'])
198 def filter_debug_flags(opts, continuation=run_analyzer):
199 """ Filter out nondebug macros when requested. """
201 if opts.pop('force_debug'):
202 # lazy implementation just append an undefine macro at the end
203 opts.update({'flags': opts['flags'] + ['-UNDEBUG']})
205 return continuation(opts)
208 @require(['language', 'compiler', 'file', 'flags'])
209 def language_check(opts, continuation=filter_debug_flags):
210 """ Find out the language from command line parameters or file name
211 extension. The decision also influenced by the compiler invocation. """
213 accepted = frozenset({
214 'c', 'c++', 'objective-c', 'objective-c++', 'c-cpp-output',
215 'c++-cpp-output', 'objective-c-cpp-output'
218 # language can be given as a parameter...
219 language = opts.pop('language')
220 compiler = opts.pop('compiler')
221 # ... or find out from source file extension
222 if language is None and compiler is not None:
223 language = classify_source(opts['file'], compiler == 'c')
226 logging.debug('skip analysis, language not known')
228 elif language not in accepted:
229 logging.debug('skip analysis, language not supported')
232 logging.debug('analysis, language: %s', language)
233 opts.update({'language': language,
234 'flags': ['-x', language] + opts['flags']})
235 return continuation(opts)
238 @require(['arch_list', 'flags'])
239 def arch_check(opts, continuation=language_check):
240 """ Do run analyzer through one of the given architectures. """
242 disabled = frozenset({'ppc', 'ppc64'})
244 received_list = opts.pop('arch_list')
246 # filter out disabled architectures and -arch switches
247 filtered_list = [a for a in received_list if a not in disabled]
249 # There should be only one arch given (or the same multiple
250 # times). If there are multiple arch are given and are not
251 # the same, those should not change the pre-processing step.
252 # But that's the only pass we have before run the analyzer.
253 current = filtered_list.pop()
254 logging.debug('analysis, on arch: %s', current)
256 opts.update({'flags': ['-arch', current] + opts['flags']})
257 return continuation(opts)
259 logging.debug('skip analysis, found not supported arch')
262 logging.debug('analysis, on default arch')
263 return continuation(opts)
266 def classify_parameters(command):
267 """ Prepare compiler flags (filters some and add others) and take out
268 language (-x) and architecture (-arch) flags for future processing. """
271 'flags': [], # the filtered compiler flags
272 'arch_list': [], # list of architecture flags
273 'language': None, # compilation language, None, if not specified
274 'compiler': compiler_language(command) # 'c' or 'c++'
277 # iterate on the compile options
278 args = iter(command[1:])
280 # take arch flags into a separate basket
282 result['arch_list'].append(next(args))
285 result['language'] = next(args)
286 # parameters which looks source file are not flags
287 elif re.match(r'^[^-].+', arg) and classify_source(arg):
290 elif arg in IGNORED_FLAGS:
291 count = IGNORED_FLAGS[arg]
292 for _ in range(count):
294 # we don't care about extra warnings, but we should suppress ones
295 # that we don't want to see.
296 elif re.match(r'^-W.+', arg) and not re.match(r'^-Wno-.+', arg):
298 # and consider everything else as compilation flag.
300 result['flags'].append(arg)