blob: 72d02c85fed158bebce3c6d50f128623063a33f8 [file] [log] [blame]
Laszlo Nagybc687582016-01-12 22:38:41 +00001# -*- coding: utf-8 -*-
2# The LLVM Compiler Infrastructure
3#
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. """
7
Laszlo Nagy8bd63e52016-04-19 12:03:03 +00008import re
Laszlo Nagybc687582016-01-12 22:38:41 +00009import os
10import os.path
11import tempfile
12import functools
13import subprocess
14import logging
Laszlo Nagy8bd63e52016-04-19 12:03:03 +000015from libscanbuild.compilation import classify_source, compiler_language
16from libscanbuild.clang import get_version, get_arguments
Laszlo Nagybc687582016-01-12 22:38:41 +000017from libscanbuild.shell import decode
18
19__all__ = ['run']
20
Laszlo Nagy8bd63e52016-04-19 12:03:03 +000021# To have good results from static analyzer certain compiler options shall be
22# omitted. The compiler flag filtering only affects the static analyzer run.
23#
24# Keys are the option name, value number of options to skip
25IGNORED_FLAGS = {
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.
30 '-g': 0,
31 '-save-temps': 0,
32 '-install_name': 1,
33 '-exported_symbols_list': 1,
34 '-current_version': 1,
35 '-compatibility_version': 1,
36 '-init': 1,
37 '-e': 1,
38 '-seg1addr': 1,
39 '-bundle_loader': 1,
40 '-multiply_defined': 1,
41 '-sectorder': 3,
42 '--param': 1,
43 '--serialize-diagnostics': 1
44}
45
Laszlo Nagybc687582016-01-12 22:38:41 +000046
47def require(required):
48 """ Decorator for checking the required values in state.
49
50 It checks the required attributes in the passed state and stop when
51 any of those is missing. """
52
53 def decorator(function):
54 @functools.wraps(function)
55 def wrapper(*args, **kwargs):
56 for key in required:
57 if key not in args[0]:
Laszlo Nagy8bd63e52016-04-19 12:03:03 +000058 raise KeyError('{0} not passed to {1}'.format(
59 key, function.__name__))
Laszlo Nagybc687582016-01-12 22:38:41 +000060
61 return function(*args, **kwargs)
62
63 return wrapper
64
65 return decorator
66
67
Laszlo Nagy8bd63e52016-04-19 12:03:03 +000068@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
Laszlo Nagybc687582016-01-12 22:38:41 +000077def run(opts):
78 """ Entry point to run (or not) static analyzer against a single entry
79 of the compilation database.
80
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.
84
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.) """
89
90 try:
91 command = opts.pop('command')
Laszlo Nagy8bd63e52016-04-19 12:03:03 +000092 command = command if isinstance(command, list) else decode(command)
Laszlo Nagybc687582016-01-12 22:38:41 +000093 logging.debug("Run analyzer against '%s'", command)
Laszlo Nagy8bd63e52016-04-19 12:03:03 +000094 opts.update(classify_parameters(command))
Laszlo Nagybc687582016-01-12 22:38:41 +000095
Laszlo Nagy8bd63e52016-04-19 12:03:03 +000096 return arch_check(opts)
Laszlo Nagybc687582016-01-12 22:38:41 +000097 except Exception:
98 logging.error("Problem occured during analyzis.", exc_info=1)
99 return None
100
101
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000102@require(['clang', 'directory', 'flags', 'file', 'output_dir', 'language',
Laszlo Nagybc687582016-01-12 22:38:41 +0000103 'error_type', 'error_output', 'exit_code'])
104def report_failure(opts):
105 """ Create report when analyzer failed.
106
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. """
110
111 def extension(opts):
112 """ Generate preprocessor file extension. """
113
114 mapping = {'objective-c++': '.mii', 'objective-c': '.mi', 'c++': '.ii'}
115 return mapping.get(opts['language'], '.i')
116
117 def destination(opts):
118 """ Creates failures directory if not exits yet. """
119
120 name = os.path.join(opts['output_dir'], 'failures')
121 if not os.path.isdir(name):
122 os.makedirs(name)
123 return name
124
125 error = opts['error_type']
126 (handle, name) = tempfile.mkstemp(suffix=extension(opts),
127 prefix='clang_' + error + '_',
128 dir=destination(opts))
129 os.close(handle)
130 cwd = opts['directory']
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000131 cmd = get_arguments([opts['clang'], '-fsyntax-only', '-E'] +
132 opts['flags'] + [opts['file'], '-o', name], cwd)
Laszlo Nagybc687582016-01-12 22:38:41 +0000133 logging.debug('exec command in %s: %s', cwd, ' '.join(cmd))
134 subprocess.call(cmd, cwd=cwd)
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000135 # write general information about the crash
Laszlo Nagybc687582016-01-12 22:38:41 +0000136 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)
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000141 handle.write(get_version(opts['clang']))
Laszlo Nagybc687582016-01-12 22:38:41 +0000142 handle.close()
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000143 # write the captured output too
Laszlo Nagybc687582016-01-12 22:38:41 +0000144 with open(name + '.stderr.txt', 'w') as handle:
145 handle.writelines(opts['error_output'])
146 handle.close()
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000147 # return with the previous step exit code and output
Laszlo Nagybc687582016-01-12 22:38:41 +0000148 return {
149 'error_output': opts['error_output'],
150 'exit_code': opts['exit_code']
151 }
152
153
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000154@require(['clang', 'directory', 'flags', 'direct_args', 'file', 'output_dir',
155 'output_format'])
Laszlo Nagybc687582016-01-12 22:38:41 +0000156def 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. """
160
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000161 def output():
162 """ Creates output file name for reports. """
163 if opts['output_format'] in {'plist', 'plist-html'}:
164 (handle, name) = tempfile.mkstemp(prefix='report-',
165 suffix='.plist',
166 dir=opts['output_dir'])
167 os.close(handle)
168 return name
169 return opts['output_dir']
170
Laszlo Nagybc687582016-01-12 22:38:41 +0000171 cwd = opts['directory']
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000172 cmd = get_arguments([opts['clang'], '--analyze'] + opts['direct_args'] +
173 opts['flags'] + [opts['file'], '-o', output()],
Laszlo Nagybc687582016-01-12 22:38:41 +0000174 cwd)
175 logging.debug('exec command in %s: %s', cwd, ' '.join(cmd))
176 child = subprocess.Popen(cmd,
177 cwd=cwd,
178 universal_newlines=True,
179 stdout=subprocess.PIPE,
180 stderr=subprocess.STDOUT)
181 output = child.stdout.readlines()
182 child.stdout.close()
183 # do report details if it were asked
184 child.wait()
185 if opts.get('output_failures', False) and child.returncode:
186 error_type = 'crash' if child.returncode & 127 else 'other_error'
187 opts.update({
188 'error_type': error_type,
189 'error_output': output,
190 'exit_code': child.returncode
191 })
192 return continuation(opts)
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000193 # return the output for logging and exit code for testing
Laszlo Nagybc687582016-01-12 22:38:41 +0000194 return {'error_output': output, 'exit_code': child.returncode}
195
196
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000197@require(['flags', 'force_debug'])
198def filter_debug_flags(opts, continuation=run_analyzer):
199 """ Filter out nondebug macros when requested. """
Laszlo Nagybc687582016-01-12 22:38:41 +0000200
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000201 if opts.pop('force_debug'):
202 # lazy implementation just append an undefine macro at the end
203 opts.update({'flags': opts['flags'] + ['-UNDEBUG']})
Laszlo Nagybc687582016-01-12 22:38:41 +0000204
205 return continuation(opts)
206
207
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000208@require(['language', 'compiler', 'file', 'flags'])
Devin Coughlinaed7b8a2016-09-14 18:14:11 +0000209def language_check(opts, continuation=filter_debug_flags):
Laszlo Nagybc687582016-01-12 22:38:41 +0000210 """ Find out the language from command line parameters or file name
211 extension. The decision also influenced by the compiler invocation. """
212
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000213 accepted = frozenset({
Laszlo Nagybc687582016-01-12 22:38:41 +0000214 'c', 'c++', 'objective-c', 'objective-c++', 'c-cpp-output',
215 'c++-cpp-output', 'objective-c-cpp-output'
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000216 })
Laszlo Nagybc687582016-01-12 22:38:41 +0000217
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000218 # 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')
Laszlo Nagybc687582016-01-12 22:38:41 +0000224
225 if language is None:
226 logging.debug('skip analysis, language not known')
227 return None
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000228 elif language not in accepted:
Laszlo Nagybc687582016-01-12 22:38:41 +0000229 logging.debug('skip analysis, language not supported')
230 return None
231 else:
232 logging.debug('analysis, language: %s', language)
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000233 opts.update({'language': language,
234 'flags': ['-x', language] + opts['flags']})
Laszlo Nagybc687582016-01-12 22:38:41 +0000235 return continuation(opts)
236
237
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000238@require(['arch_list', 'flags'])
Laszlo Nagybc687582016-01-12 22:38:41 +0000239def arch_check(opts, continuation=language_check):
240 """ Do run analyzer through one of the given architectures. """
241
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000242 disabled = frozenset({'ppc', 'ppc64'})
Laszlo Nagybc687582016-01-12 22:38:41 +0000243
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000244 received_list = opts.pop('arch_list')
245 if received_list:
Laszlo Nagybc687582016-01-12 22:38:41 +0000246 # filter out disabled architectures and -arch switches
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000247 filtered_list = [a for a in received_list if a not in disabled]
248 if filtered_list:
Laszlo Nagybc687582016-01-12 22:38:41 +0000249 # 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.
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000253 current = filtered_list.pop()
254 logging.debug('analysis, on arch: %s', current)
Laszlo Nagybc687582016-01-12 22:38:41 +0000255
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000256 opts.update({'flags': ['-arch', current] + opts['flags']})
Laszlo Nagybc687582016-01-12 22:38:41 +0000257 return continuation(opts)
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000258 else:
259 logging.debug('skip analysis, found not supported arch')
260 return None
Laszlo Nagybc687582016-01-12 22:38:41 +0000261 else:
262 logging.debug('analysis, on default arch')
263 return continuation(opts)
264
265
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000266def 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. """
Laszlo Nagybc687582016-01-12 22:38:41 +0000269
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000270 result = {
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++'
275 }
276
277 # iterate on the compile options
278 args = iter(command[1:])
279 for arg in args:
280 # take arch flags into a separate basket
281 if arg == '-arch':
282 result['arch_list'].append(next(args))
283 # take language
284 elif arg == '-x':
285 result['language'] = next(args)
286 # parameters which looks source file are not flags
287 elif re.match(r'^[^-].+', arg) and classify_source(arg):
288 pass
289 # ignore some flags
290 elif arg in IGNORED_FLAGS:
291 count = IGNORED_FLAGS[arg]
292 for _ in range(count):
293 next(args)
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):
297 pass
298 # and consider everything else as compilation flag.
299 else:
300 result['flags'].append(arg)
301
302 return result