blob: 49de387c70424df96babce282ef347f4fb7eb914 [file] [log] [blame]
Laszlo Nagybc687582016-01-12 22:38:41 +00001# -*- coding: utf-8 -*-
Chandler Carruth2946cd72019-01-19 08:50:56 +00002# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
3# See https://llvm.org/LICENSE.txt for license information.
4# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
Laszlo Nagybc687582016-01-12 22:38:41 +00005""" This module implements the 'scan-build' command API.
6
7To run the static analyzer against a build is done in multiple steps:
8
9 -- Intercept: capture the compilation command during the build,
10 -- Analyze: run the analyzer against the captured commands,
11 -- Report: create a cover report from the analyzer outputs. """
12
Laszlo Nagybc687582016-01-12 22:38:41 +000013import re
14import os
15import os.path
16import json
Laszlo Nagybc687582016-01-12 22:38:41 +000017import logging
Laszlo Nagybc687582016-01-12 22:38:41 +000018import multiprocessing
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +000019import tempfile
20import functools
21import subprocess
Laszlo Nagy258ff252017-02-14 10:43:38 +000022import contextlib
23import datetime
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +000024import shutil
25import glob
26from collections import defaultdict
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +000027
Laszlo Nagy2e9c9222017-03-04 01:08:05 +000028from libscanbuild import command_entry_point, compiler_wrapper, \
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +000029 wrapper_environment, run_build, run_command, CtuConfig
Laszlo Nagy5270bb92017-03-08 21:18:51 +000030from libscanbuild.arguments import parse_args_for_scan_build, \
31 parse_args_for_analyze_build
Laszlo Nagybc687582016-01-12 22:38:41 +000032from libscanbuild.intercept import capture
Laszlo Nagy258ff252017-02-14 10:43:38 +000033from libscanbuild.report import document
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +000034from libscanbuild.compilation import split_command, classify_source, \
35 compiler_language
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +000036from libscanbuild.clang import get_version, get_arguments, get_triple_arch
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +000037from libscanbuild.shell import decode
Laszlo Nagybc687582016-01-12 22:38:41 +000038
Laszlo Nagy5270bb92017-03-08 21:18:51 +000039__all__ = ['scan_build', 'analyze_build', 'analyze_compiler_wrapper']
Laszlo Nagybc687582016-01-12 22:38:41 +000040
41COMPILER_WRAPPER_CC = 'analyze-cc'
42COMPILER_WRAPPER_CXX = 'analyze-c++'
43
Rafael Stahl8c487052019-01-10 17:44:04 +000044CTU_EXTDEF_MAP_FILENAME = 'externalDefMap.txt'
45CTU_TEMP_DEFMAP_FOLDER = 'tmpExternalDefMaps'
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +000046
Laszlo Nagybc687582016-01-12 22:38:41 +000047
48@command_entry_point
Laszlo Nagy5270bb92017-03-08 21:18:51 +000049def scan_build():
50 """ Entry point for scan-build command. """
Laszlo Nagybc687582016-01-12 22:38:41 +000051
Laszlo Nagy5270bb92017-03-08 21:18:51 +000052 args = parse_args_for_scan_build()
Laszlo Nagy57db7c62017-03-21 10:15:18 +000053 # will re-assign the report directory as new output
54 with report_directory(args.output, args.keep_empty) as args.output:
Laszlo Nagy5270bb92017-03-08 21:18:51 +000055 # Run against a build command. there are cases, when analyzer run
56 # is not required. But we need to set up everything for the
57 # wrappers, because 'configure' needs to capture the CC/CXX values
58 # for the Makefile.
59 if args.intercept_first:
60 # Run build command with intercept module.
61 exit_code = capture(args)
62 # Run the analyzer against the captured commands.
Laszlo Nagybc687582016-01-12 22:38:41 +000063 if need_analyzer(args.build):
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +000064 govern_analyzer_runs(args)
Laszlo Nagybc687582016-01-12 22:38:41 +000065 else:
Laszlo Nagy5270bb92017-03-08 21:18:51 +000066 # Run build command and analyzer with compiler wrappers.
Laszlo Nagy57db7c62017-03-21 10:15:18 +000067 environment = setup_environment(args)
Laszlo Nagy52c1d7e2017-02-14 10:30:50 +000068 exit_code = run_build(args.build, env=environment)
Laszlo Nagy5270bb92017-03-08 21:18:51 +000069 # Cover report generation and bug counting.
Laszlo Nagy57db7c62017-03-21 10:15:18 +000070 number_of_bugs = document(args)
Laszlo Nagy5270bb92017-03-08 21:18:51 +000071 # Set exit status as it was requested.
72 return number_of_bugs if args.status_bugs else exit_code
73
74
75@command_entry_point
76def analyze_build():
77 """ Entry point for analyze-build command. """
78
79 args = parse_args_for_analyze_build()
Laszlo Nagy57db7c62017-03-21 10:15:18 +000080 # will re-assign the report directory as new output
81 with report_directory(args.output, args.keep_empty) as args.output:
Laszlo Nagy5270bb92017-03-08 21:18:51 +000082 # Run the analyzer against a compilation db.
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +000083 govern_analyzer_runs(args)
Laszlo Nagy5270bb92017-03-08 21:18:51 +000084 # Cover report generation and bug counting.
Laszlo Nagy57db7c62017-03-21 10:15:18 +000085 number_of_bugs = document(args)
Laszlo Nagy5270bb92017-03-08 21:18:51 +000086 # Set exit status as it was requested.
87 return number_of_bugs if args.status_bugs else 0
Laszlo Nagybc687582016-01-12 22:38:41 +000088
89
90def need_analyzer(args):
91 """ Check the intent of the build command.
92
93 When static analyzer run against project configure step, it should be
94 silent and no need to run the analyzer or generate report.
95
Alexander Kornienko2a8c18d2018-04-06 15:14:32 +000096 To run `scan-build` against the configure step might be necessary,
Laszlo Nagybc687582016-01-12 22:38:41 +000097 when compiler wrappers are used. That's the moment when build setup
98 check the compiler and capture the location for the build process. """
99
Serge Guelton3331b6e2019-02-11 15:03:17 +0000100 return len(args) and not re.search(r'configure|autogen', args[0])
Laszlo Nagybc687582016-01-12 22:38:41 +0000101
102
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000103def prefix_with(constant, pieces):
104 """ From a sequence create another sequence where every second element
105 is from the original sequence and the odd elements are the prefix.
106
107 eg.: prefix_with(0, [1,2,3]) creates [0, 1, 0, 2, 0, 3] """
108
109 return [elem for piece in pieces for elem in [constant, piece]]
110
111
112def get_ctu_config_from_args(args):
113 """ CTU configuration is created from the chosen phases and dir. """
114
115 return (
116 CtuConfig(collect=args.ctu_phases.collect,
117 analyze=args.ctu_phases.analyze,
118 dir=args.ctu_dir,
Rafael Stahl8c487052019-01-10 17:44:04 +0000119 extdef_map_cmd=args.extdef_map_cmd)
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000120 if hasattr(args, 'ctu_phases') and hasattr(args.ctu_phases, 'dir')
Rafael Stahl8c487052019-01-10 17:44:04 +0000121 else CtuConfig(collect=False, analyze=False, dir='', extdef_map_cmd=''))
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000122
123
124def get_ctu_config_from_json(ctu_conf_json):
125 """ CTU configuration is created from the chosen phases and dir. """
126
127 ctu_config = json.loads(ctu_conf_json)
128 # Recover namedtuple from json when coming from analyze-cc or analyze-c++
129 return CtuConfig(collect=ctu_config[0],
130 analyze=ctu_config[1],
131 dir=ctu_config[2],
Rafael Stahl8c487052019-01-10 17:44:04 +0000132 extdef_map_cmd=ctu_config[3])
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000133
134
Rafael Stahl8c487052019-01-10 17:44:04 +0000135def create_global_ctu_extdef_map(extdef_map_lines):
136 """ Takes iterator of individual external definition maps and creates a
137 global map keeping only unique names. We leave conflicting names out of
138 CTU.
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000139
Rafael Stahl8c487052019-01-10 17:44:04 +0000140 :param extdef_map_lines: Contains the id of a definition (mangled name) and
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000141 the originating source (the corresponding AST file) name.
Rafael Stahl8c487052019-01-10 17:44:04 +0000142 :type extdef_map_lines: Iterator of str.
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000143 :returns: Mangled name - AST file pairs.
144 :rtype: List of (str, str) tuples.
145 """
146
147 mangled_to_asts = defaultdict(set)
148
Rafael Stahl8c487052019-01-10 17:44:04 +0000149 for line in extdef_map_lines:
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000150 mangled_name, ast_file = line.strip().split(' ', 1)
151 mangled_to_asts[mangled_name].add(ast_file)
152
153 mangled_ast_pairs = []
154
155 for mangled_name, ast_files in mangled_to_asts.items():
156 if len(ast_files) == 1:
157 mangled_ast_pairs.append((mangled_name, next(iter(ast_files))))
158
159 return mangled_ast_pairs
160
161
Rafael Stahl8c487052019-01-10 17:44:04 +0000162def merge_ctu_extdef_maps(ctudir):
163 """ Merge individual external definition maps into a global one.
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000164
165 As the collect phase runs parallel on multiple threads, all compilation
Rafael Stahl8c487052019-01-10 17:44:04 +0000166 units are separately mapped into a temporary file in CTU_TEMP_DEFMAP_FOLDER.
167 These definition maps contain the mangled names and the source
168 (AST generated from the source) which had their definition.
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000169 These files should be merged at the end into a global map file:
Rafael Stahl8c487052019-01-10 17:44:04 +0000170 CTU_EXTDEF_MAP_FILENAME."""
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000171
Rafael Stahl8c487052019-01-10 17:44:04 +0000172 def generate_extdef_map_lines(extdefmap_dir):
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000173 """ Iterate over all lines of input files in a determined order. """
174
Rafael Stahl8c487052019-01-10 17:44:04 +0000175 files = glob.glob(os.path.join(extdefmap_dir, '*'))
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000176 files.sort()
177 for filename in files:
178 with open(filename, 'r') as in_file:
179 for line in in_file:
180 yield line
181
182 def write_global_map(arch, mangled_ast_pairs):
Rafael Stahl8c487052019-01-10 17:44:04 +0000183 """ Write (mangled name, ast file) pairs into final file. """
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000184
Rafael Stahl8c487052019-01-10 17:44:04 +0000185 extern_defs_map_file = os.path.join(ctudir, arch,
186 CTU_EXTDEF_MAP_FILENAME)
187 with open(extern_defs_map_file, 'w') as out_file:
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000188 for mangled_name, ast_file in mangled_ast_pairs:
189 out_file.write('%s %s\n' % (mangled_name, ast_file))
190
191 triple_arches = glob.glob(os.path.join(ctudir, '*'))
192 for triple_path in triple_arches:
193 if os.path.isdir(triple_path):
194 triple_arch = os.path.basename(triple_path)
Rafael Stahl8c487052019-01-10 17:44:04 +0000195 extdefmap_dir = os.path.join(ctudir, triple_arch,
196 CTU_TEMP_DEFMAP_FOLDER)
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000197
Rafael Stahl8c487052019-01-10 17:44:04 +0000198 extdef_map_lines = generate_extdef_map_lines(extdefmap_dir)
199 mangled_ast_pairs = create_global_ctu_extdef_map(extdef_map_lines)
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000200 write_global_map(triple_arch, mangled_ast_pairs)
201
202 # Remove all temporary files
Rafael Stahl8c487052019-01-10 17:44:04 +0000203 shutil.rmtree(extdefmap_dir, ignore_errors=True)
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000204
205
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +0000206def run_analyzer_parallel(args):
Laszlo Nagybc687582016-01-12 22:38:41 +0000207 """ Runs the analyzer against the given compilation database. """
208
209 def exclude(filename):
210 """ Return true when any excluded directory prefix the filename. """
211 return any(re.match(r'^' + directory, filename)
212 for directory in args.excludes)
213
214 consts = {
215 'clang': args.clang,
Laszlo Nagy57db7c62017-03-21 10:15:18 +0000216 'output_dir': args.output,
Laszlo Nagybc687582016-01-12 22:38:41 +0000217 'output_format': args.output_format,
218 'output_failures': args.output_failures,
Yury Gribova6560eb2016-02-18 11:08:46 +0000219 'direct_args': analyzer_params(args),
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000220 'force_debug': args.force_debug,
221 'ctu': get_ctu_config_from_args(args)
Laszlo Nagybc687582016-01-12 22:38:41 +0000222 }
223
224 logging.debug('run analyzer against compilation database')
225 with open(args.cdb, 'r') as handle:
226 generator = (dict(cmd, **consts)
227 for cmd in json.load(handle) if not exclude(cmd['file']))
228 # when verbose output requested execute sequentially
229 pool = multiprocessing.Pool(1 if args.verbose > 2 else None)
230 for current in pool.imap_unordered(run, generator):
231 if current is not None:
232 # display error message from the static analyzer
233 for line in current['error_output']:
234 logging.info(line.rstrip())
235 pool.close()
236 pool.join()
237
238
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000239def govern_analyzer_runs(args):
240 """ Governs multiple runs in CTU mode or runs once in normal mode. """
241
242 ctu_config = get_ctu_config_from_args(args)
243 # If we do a CTU collect (1st phase) we remove all previous collection
244 # data first.
245 if ctu_config.collect:
246 shutil.rmtree(ctu_config.dir, ignore_errors=True)
247
248 # If the user asked for a collect (1st) and analyze (2nd) phase, we do an
249 # all-in-one run where we deliberately remove collection data before and
250 # also after the run. If the user asks only for a single phase data is
251 # left so multiple analyze runs can use the same data gathered by a single
252 # collection run.
253 if ctu_config.collect and ctu_config.analyze:
Rafael Stahl8c487052019-01-10 17:44:04 +0000254 # CTU strings are coming from args.ctu_dir and extdef_map_cmd,
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000255 # so we can leave it empty
256 args.ctu_phases = CtuConfig(collect=True, analyze=False,
Rafael Stahl8c487052019-01-10 17:44:04 +0000257 dir='', extdef_map_cmd='')
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000258 run_analyzer_parallel(args)
Rafael Stahl8c487052019-01-10 17:44:04 +0000259 merge_ctu_extdef_maps(ctu_config.dir)
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000260 args.ctu_phases = CtuConfig(collect=False, analyze=True,
Rafael Stahl8c487052019-01-10 17:44:04 +0000261 dir='', extdef_map_cmd='')
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000262 run_analyzer_parallel(args)
263 shutil.rmtree(ctu_config.dir, ignore_errors=True)
264 else:
265 # Single runs (collect or analyze) are launched from here.
266 run_analyzer_parallel(args)
267 if ctu_config.collect:
Rafael Stahl8c487052019-01-10 17:44:04 +0000268 merge_ctu_extdef_maps(ctu_config.dir)
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000269
270
Laszlo Nagy57db7c62017-03-21 10:15:18 +0000271def setup_environment(args):
Laszlo Nagybc687582016-01-12 22:38:41 +0000272 """ Set up environment for build command to interpose compiler wrapper. """
273
274 environment = dict(os.environ)
Laszlo Nagy2e9c9222017-03-04 01:08:05 +0000275 environment.update(wrapper_environment(args))
Laszlo Nagybc687582016-01-12 22:38:41 +0000276 environment.update({
Laszlo Nagy5270bb92017-03-08 21:18:51 +0000277 'CC': COMPILER_WRAPPER_CC,
278 'CXX': COMPILER_WRAPPER_CXX,
Laszlo Nagybc687582016-01-12 22:38:41 +0000279 'ANALYZE_BUILD_CLANG': args.clang if need_analyzer(args.build) else '',
Laszlo Nagy57db7c62017-03-21 10:15:18 +0000280 'ANALYZE_BUILD_REPORT_DIR': args.output,
Laszlo Nagybc687582016-01-12 22:38:41 +0000281 'ANALYZE_BUILD_REPORT_FORMAT': args.output_format,
282 'ANALYZE_BUILD_REPORT_FAILURES': 'yes' if args.output_failures else '',
Yury Gribova6560eb2016-02-18 11:08:46 +0000283 'ANALYZE_BUILD_PARAMETERS': ' '.join(analyzer_params(args)),
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000284 'ANALYZE_BUILD_FORCE_DEBUG': 'yes' if args.force_debug else '',
285 'ANALYZE_BUILD_CTU': json.dumps(get_ctu_config_from_args(args))
Laszlo Nagybc687582016-01-12 22:38:41 +0000286 })
287 return environment
288
289
Laszlo Nagy2e9c9222017-03-04 01:08:05 +0000290@command_entry_point
291def analyze_compiler_wrapper():
Laszlo Nagybc687582016-01-12 22:38:41 +0000292 """ Entry point for `analyze-cc` and `analyze-c++` compiler wrappers. """
293
Laszlo Nagy2e9c9222017-03-04 01:08:05 +0000294 return compiler_wrapper(analyze_compiler_wrapper_impl)
295
296
297def analyze_compiler_wrapper_impl(result, execution):
298 """ Implements analyzer compiler wrapper functionality. """
299
300 # don't run analyzer when compilation fails. or when it's not requested.
Laszlo Nagybc687582016-01-12 22:38:41 +0000301 if result or not os.getenv('ANALYZE_BUILD_CLANG'):
Laszlo Nagy2e9c9222017-03-04 01:08:05 +0000302 return
303
304 # check is it a compilation?
305 compilation = split_command(execution.cmd)
306 if compilation is None:
307 return
308 # collect the needed parameters from environment, crash when missing
309 parameters = {
310 'clang': os.getenv('ANALYZE_BUILD_CLANG'),
311 'output_dir': os.getenv('ANALYZE_BUILD_REPORT_DIR'),
312 'output_format': os.getenv('ANALYZE_BUILD_REPORT_FORMAT'),
313 'output_failures': os.getenv('ANALYZE_BUILD_REPORT_FAILURES'),
314 'direct_args': os.getenv('ANALYZE_BUILD_PARAMETERS',
315 '').split(' '),
316 'force_debug': os.getenv('ANALYZE_BUILD_FORCE_DEBUG'),
317 'directory': execution.cwd,
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000318 'command': [execution.cmd[0], '-c'] + compilation.flags,
319 'ctu': get_ctu_config_from_json(os.getenv('ANALYZE_BUILD_CTU'))
Laszlo Nagy2e9c9222017-03-04 01:08:05 +0000320 }
321 # call static analyzer against the compilation
322 for source in compilation.files:
323 parameters.update({'file': source})
324 logging.debug('analyzer parameters %s', parameters)
325 current = run(parameters)
326 # display error message from the static analyzer
327 if current is not None:
328 for line in current['error_output']:
329 logging.info(line.rstrip())
Laszlo Nagybc687582016-01-12 22:38:41 +0000330
331
Laszlo Nagy258ff252017-02-14 10:43:38 +0000332@contextlib.contextmanager
333def report_directory(hint, keep):
334 """ Responsible for the report directory.
335
336 hint -- could specify the parent directory of the output directory.
337 keep -- a boolean value to keep or delete the empty report directory. """
338
339 stamp_format = 'scan-build-%Y-%m-%d-%H-%M-%S-%f-'
340 stamp = datetime.datetime.now().strftime(stamp_format)
341 parent_dir = os.path.abspath(hint)
342 if not os.path.exists(parent_dir):
343 os.makedirs(parent_dir)
344 name = tempfile.mkdtemp(prefix=stamp, dir=parent_dir)
345
346 logging.info('Report directory created: %s', name)
347
348 try:
349 yield name
350 finally:
351 if os.listdir(name):
352 msg = "Run 'scan-view %s' to examine bug reports."
353 keep = True
354 else:
355 if keep:
356 msg = "Report directory '%s' contains no report, but kept."
357 else:
358 msg = "Removing directory '%s' because it contains no report."
359 logging.warning(msg, name)
360
361 if not keep:
362 os.rmdir(name)
363
364
Laszlo Nagybc687582016-01-12 22:38:41 +0000365def analyzer_params(args):
366 """ A group of command line arguments can mapped to command
367 line arguments of the analyzer. This method generates those. """
368
Laszlo Nagybc687582016-01-12 22:38:41 +0000369 result = []
370
371 if args.store_model:
372 result.append('-analyzer-store={0}'.format(args.store_model))
373 if args.constraints_model:
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000374 result.append('-analyzer-constraints={0}'.format(
375 args.constraints_model))
Laszlo Nagybc687582016-01-12 22:38:41 +0000376 if args.internal_stats:
377 result.append('-analyzer-stats')
378 if args.analyze_headers:
379 result.append('-analyzer-opt-analyze-headers')
380 if args.stats:
381 result.append('-analyzer-checker=debug.Stats')
382 if args.maxloop:
383 result.extend(['-analyzer-max-loop', str(args.maxloop)])
384 if args.output_format:
385 result.append('-analyzer-output={0}'.format(args.output_format))
386 if args.analyzer_config:
Petr Hosek5518d182017-07-19 00:29:41 +0000387 result.extend(['-analyzer-config', args.analyzer_config])
Laszlo Nagybc687582016-01-12 22:38:41 +0000388 if args.verbose >= 4:
389 result.append('-analyzer-display-progress')
390 if args.plugins:
391 result.extend(prefix_with('-load', args.plugins))
392 if args.enable_checker:
393 checkers = ','.join(args.enable_checker)
394 result.extend(['-analyzer-checker', checkers])
395 if args.disable_checker:
396 checkers = ','.join(args.disable_checker)
397 result.extend(['-analyzer-disable-checker', checkers])
Laszlo Nagybc687582016-01-12 22:38:41 +0000398
399 return prefix_with('-Xclang', result)
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +0000400
401
402def require(required):
403 """ Decorator for checking the required values in state.
404
405 It checks the required attributes in the passed state and stop when
406 any of those is missing. """
407
408 def decorator(function):
409 @functools.wraps(function)
410 def wrapper(*args, **kwargs):
411 for key in required:
412 if key not in args[0]:
413 raise KeyError('{0} not passed to {1}'.format(
414 key, function.__name__))
415
416 return function(*args, **kwargs)
417
418 return wrapper
419
420 return decorator
421
422
423@require(['command', # entry from compilation database
424 'directory', # entry from compilation database
425 'file', # entry from compilation database
426 'clang', # clang executable name (and path)
427 'direct_args', # arguments from command line
428 'force_debug', # kill non debug macros
429 'output_dir', # where generated report files shall go
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000430 'output_format', # it's 'plist', 'html', both or plist-multi-file
431 'output_failures', # generate crash reports or not
432 'ctu']) # ctu control options
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +0000433def run(opts):
434 """ Entry point to run (or not) static analyzer against a single entry
435 of the compilation database.
436
437 This complex task is decomposed into smaller methods which are calling
Alexander Kornienko2a8c18d2018-04-06 15:14:32 +0000438 each other in chain. If the analyzis is not possible the given method
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +0000439 just return and break the chain.
440
441 The passed parameter is a python dictionary. Each method first check
442 that the needed parameters received. (This is done by the 'require'
443 decorator. It's like an 'assert' to check the contract between the
444 caller and the called method.) """
445
446 try:
447 command = opts.pop('command')
448 command = command if isinstance(command, list) else decode(command)
449 logging.debug("Run analyzer against '%s'", command)
450 opts.update(classify_parameters(command))
451
452 return arch_check(opts)
453 except Exception:
Malcolm Parsons51d3fb02018-01-24 10:26:09 +0000454 logging.error("Problem occurred during analyzis.", exc_info=1)
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +0000455 return None
456
457
458@require(['clang', 'directory', 'flags', 'file', 'output_dir', 'language',
459 'error_output', 'exit_code'])
460def report_failure(opts):
461 """ Create report when analyzer failed.
462
463 The major report is the preprocessor output. The output filename generated
464 randomly. The compiler output also captured into '.stderr.txt' file.
465 And some more execution context also saved into '.info.txt' file. """
466
467 def extension():
468 """ Generate preprocessor file extension. """
469
470 mapping = {'objective-c++': '.mii', 'objective-c': '.mi', 'c++': '.ii'}
471 return mapping.get(opts['language'], '.i')
472
473 def destination():
474 """ Creates failures directory if not exits yet. """
475
476 failures_dir = os.path.join(opts['output_dir'], 'failures')
477 if not os.path.isdir(failures_dir):
478 os.makedirs(failures_dir)
479 return failures_dir
480
481 # Classify error type: when Clang terminated by a signal it's a 'Crash'.
482 # (python subprocess Popen.returncode is negative when child terminated
483 # by signal.) Everything else is 'Other Error'.
484 error = 'crash' if opts['exit_code'] < 0 else 'other_error'
485 # Create preprocessor output file name. (This is blindly following the
486 # Perl implementation.)
487 (handle, name) = tempfile.mkstemp(suffix=extension(),
488 prefix='clang_' + error + '_',
489 dir=destination())
490 os.close(handle)
491 # Execute Clang again, but run the syntax check only.
492 cwd = opts['directory']
493 cmd = get_arguments(
494 [opts['clang'], '-fsyntax-only', '-E'
495 ] + opts['flags'] + [opts['file'], '-o', name], cwd)
496 run_command(cmd, cwd=cwd)
497 # write general information about the crash
498 with open(name + '.info.txt', 'w') as handle:
499 handle.write(opts['file'] + os.linesep)
500 handle.write(error.title().replace('_', ' ') + os.linesep)
501 handle.write(' '.join(cmd) + os.linesep)
502 handle.write(' '.join(os.uname()) + os.linesep)
503 handle.write(get_version(opts['clang']))
504 handle.close()
505 # write the captured output too
506 with open(name + '.stderr.txt', 'w') as handle:
507 handle.writelines(opts['error_output'])
508 handle.close()
509
510
511@require(['clang', 'directory', 'flags', 'direct_args', 'file', 'output_dir',
512 'output_format'])
513def run_analyzer(opts, continuation=report_failure):
514 """ It assembles the analysis command line and executes it. Capture the
515 output of the analysis and returns with it. If failure reports are
516 requested, it calls the continuation to generate it. """
517
518 def target():
519 """ Creates output file name for reports. """
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000520 if opts['output_format'] in {
521 'plist',
522 'plist-html',
523 'plist-multi-file'}:
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +0000524 (handle, name) = tempfile.mkstemp(prefix='report-',
525 suffix='.plist',
526 dir=opts['output_dir'])
527 os.close(handle)
528 return name
529 return opts['output_dir']
530
531 try:
532 cwd = opts['directory']
533 cmd = get_arguments([opts['clang'], '--analyze'] +
534 opts['direct_args'] + opts['flags'] +
535 [opts['file'], '-o', target()],
536 cwd)
537 output = run_command(cmd, cwd=cwd)
538 return {'error_output': output, 'exit_code': 0}
539 except subprocess.CalledProcessError as ex:
540 result = {'error_output': ex.output, 'exit_code': ex.returncode}
541 if opts.get('output_failures', False):
542 opts.update(result)
543 continuation(opts)
544 return result
545
546
Rafael Stahl8c487052019-01-10 17:44:04 +0000547def extdef_map_list_src_to_ast(extdef_src_list):
548 """ Turns textual external definition map list with source files into an
549 external definition map list with ast files. """
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000550
Rafael Stahl8c487052019-01-10 17:44:04 +0000551 extdef_ast_list = []
552 for extdef_src_txt in extdef_src_list:
553 mangled_name, path = extdef_src_txt.split(" ", 1)
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000554 # Normalize path on windows as well
555 path = os.path.splitdrive(path)[1]
556 # Make relative path out of absolute
557 path = path[1:] if path[0] == os.sep else path
558 ast_path = os.path.join("ast", path + ".ast")
Rafael Stahl8c487052019-01-10 17:44:04 +0000559 extdef_ast_list.append(mangled_name + " " + ast_path)
560 return extdef_ast_list
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000561
562
563@require(['clang', 'directory', 'flags', 'direct_args', 'file', 'ctu'])
564def ctu_collect_phase(opts):
565 """ Preprocess source by generating all data needed by CTU analysis. """
566
567 def generate_ast(triple_arch):
568 """ Generates ASTs for the current compilation command. """
569
570 args = opts['direct_args'] + opts['flags']
571 ast_joined_path = os.path.join(opts['ctu'].dir, triple_arch, 'ast',
572 os.path.realpath(opts['file'])[1:] +
573 '.ast')
574 ast_path = os.path.abspath(ast_joined_path)
575 ast_dir = os.path.dirname(ast_path)
576 if not os.path.isdir(ast_dir):
577 try:
578 os.makedirs(ast_dir)
579 except OSError:
580 # In case an other process already created it.
581 pass
582 ast_command = [opts['clang'], '-emit-ast']
583 ast_command.extend(args)
584 ast_command.append('-w')
585 ast_command.append(opts['file'])
586 ast_command.append('-o')
587 ast_command.append(ast_path)
588 logging.debug("Generating AST using '%s'", ast_command)
589 run_command(ast_command, cwd=opts['directory'])
590
Rafael Stahl8c487052019-01-10 17:44:04 +0000591 def map_extdefs(triple_arch):
592 """ Generate external definition map file for the current source. """
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000593
594 args = opts['direct_args'] + opts['flags']
Rafael Stahl8c487052019-01-10 17:44:04 +0000595 extdefmap_command = [opts['ctu'].extdef_map_cmd]
596 extdefmap_command.append(opts['file'])
597 extdefmap_command.append('--')
598 extdefmap_command.extend(args)
599 logging.debug("Generating external definition map using '%s'",
600 extdefmap_command)
601 extdef_src_list = run_command(extdefmap_command, cwd=opts['directory'])
602 extdef_ast_list = extdef_map_list_src_to_ast(extdef_src_list)
603 extern_defs_map_folder = os.path.join(opts['ctu'].dir, triple_arch,
604 CTU_TEMP_DEFMAP_FOLDER)
605 if not os.path.isdir(extern_defs_map_folder):
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000606 try:
Rafael Stahl8c487052019-01-10 17:44:04 +0000607 os.makedirs(extern_defs_map_folder)
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000608 except OSError:
609 # In case an other process already created it.
610 pass
Rafael Stahl8c487052019-01-10 17:44:04 +0000611 if extdef_ast_list:
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000612 with tempfile.NamedTemporaryFile(mode='w',
Rafael Stahl8c487052019-01-10 17:44:04 +0000613 dir=extern_defs_map_folder,
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000614 delete=False) as out_file:
Rafael Stahl8c487052019-01-10 17:44:04 +0000615 out_file.write("\n".join(extdef_ast_list) + "\n")
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000616
617 cwd = opts['directory']
618 cmd = [opts['clang'], '--analyze'] + opts['direct_args'] + opts['flags'] \
619 + [opts['file']]
620 triple_arch = get_triple_arch(cmd, cwd)
621 generate_ast(triple_arch)
Rafael Stahl8c487052019-01-10 17:44:04 +0000622 map_extdefs(triple_arch)
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000623
624
625@require(['ctu'])
626def dispatch_ctu(opts, continuation=run_analyzer):
627 """ Execute only one phase of 2 phases of CTU if needed. """
628
629 ctu_config = opts['ctu']
630
631 if ctu_config.collect or ctu_config.analyze:
632 assert ctu_config.collect != ctu_config.analyze
633 if ctu_config.collect:
634 return ctu_collect_phase(opts)
635 if ctu_config.analyze:
636 cwd = opts['directory']
637 cmd = [opts['clang'], '--analyze'] + opts['direct_args'] \
638 + opts['flags'] + [opts['file']]
639 triarch = get_triple_arch(cmd, cwd)
640 ctu_options = ['ctu-dir=' + os.path.join(ctu_config.dir, triarch),
641 'experimental-enable-naive-ctu-analysis=true']
642 analyzer_options = prefix_with('-analyzer-config', ctu_options)
643 direct_options = prefix_with('-Xanalyzer', analyzer_options)
644 opts['direct_args'].extend(direct_options)
645
646 return continuation(opts)
647
648
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +0000649@require(['flags', 'force_debug'])
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000650def filter_debug_flags(opts, continuation=dispatch_ctu):
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +0000651 """ Filter out nondebug macros when requested. """
652
653 if opts.pop('force_debug'):
654 # lazy implementation just append an undefine macro at the end
655 opts.update({'flags': opts['flags'] + ['-UNDEBUG']})
656
657 return continuation(opts)
658
659
660@require(['language', 'compiler', 'file', 'flags'])
661def language_check(opts, continuation=filter_debug_flags):
662 """ Find out the language from command line parameters or file name
663 extension. The decision also influenced by the compiler invocation. """
664
665 accepted = frozenset({
666 'c', 'c++', 'objective-c', 'objective-c++', 'c-cpp-output',
667 'c++-cpp-output', 'objective-c-cpp-output'
668 })
669
670 # language can be given as a parameter...
671 language = opts.pop('language')
672 compiler = opts.pop('compiler')
673 # ... or find out from source file extension
674 if language is None and compiler is not None:
675 language = classify_source(opts['file'], compiler == 'c')
676
677 if language is None:
678 logging.debug('skip analysis, language not known')
679 return None
680 elif language not in accepted:
681 logging.debug('skip analysis, language not supported')
682 return None
683 else:
684 logging.debug('analysis, language: %s', language)
685 opts.update({'language': language,
686 'flags': ['-x', language] + opts['flags']})
687 return continuation(opts)
688
689
690@require(['arch_list', 'flags'])
691def arch_check(opts, continuation=language_check):
692 """ Do run analyzer through one of the given architectures. """
693
694 disabled = frozenset({'ppc', 'ppc64'})
695
696 received_list = opts.pop('arch_list')
697 if received_list:
698 # filter out disabled architectures and -arch switches
699 filtered_list = [a for a in received_list if a not in disabled]
700 if filtered_list:
701 # There should be only one arch given (or the same multiple
702 # times). If there are multiple arch are given and are not
703 # the same, those should not change the pre-processing step.
704 # But that's the only pass we have before run the analyzer.
705 current = filtered_list.pop()
706 logging.debug('analysis, on arch: %s', current)
707
708 opts.update({'flags': ['-arch', current] + opts['flags']})
709 return continuation(opts)
710 else:
711 logging.debug('skip analysis, found not supported arch')
712 return None
713 else:
714 logging.debug('analysis, on default arch')
715 return continuation(opts)
716
Ilya Biryukov8b9b3bd2018-03-01 14:54:16 +0000717
Laszlo Nagy6d9a7e82017-04-07 11:04:49 +0000718# To have good results from static analyzer certain compiler options shall be
719# omitted. The compiler flag filtering only affects the static analyzer run.
720#
721# Keys are the option name, value number of options to skip
722IGNORED_FLAGS = {
723 '-c': 0, # compile option will be overwritten
724 '-fsyntax-only': 0, # static analyzer option will be overwritten
725 '-o': 1, # will set up own output file
726 # flags below are inherited from the perl implementation.
727 '-g': 0,
728 '-save-temps': 0,
729 '-install_name': 1,
730 '-exported_symbols_list': 1,
731 '-current_version': 1,
732 '-compatibility_version': 1,
733 '-init': 1,
734 '-e': 1,
735 '-seg1addr': 1,
736 '-bundle_loader': 1,
737 '-multiply_defined': 1,
738 '-sectorder': 3,
739 '--param': 1,
740 '--serialize-diagnostics': 1
741}
742
743
744def classify_parameters(command):
745 """ Prepare compiler flags (filters some and add others) and take out
746 language (-x) and architecture (-arch) flags for future processing. """
747
748 result = {
749 'flags': [], # the filtered compiler flags
750 'arch_list': [], # list of architecture flags
751 'language': None, # compilation language, None, if not specified
752 'compiler': compiler_language(command) # 'c' or 'c++'
753 }
754
755 # iterate on the compile options
756 args = iter(command[1:])
757 for arg in args:
758 # take arch flags into a separate basket
759 if arg == '-arch':
760 result['arch_list'].append(next(args))
761 # take language
762 elif arg == '-x':
763 result['language'] = next(args)
764 # parameters which looks source file are not flags
765 elif re.match(r'^[^-].+', arg) and classify_source(arg):
766 pass
767 # ignore some flags
768 elif arg in IGNORED_FLAGS:
769 count = IGNORED_FLAGS[arg]
770 for _ in range(count):
771 next(args)
772 # we don't care about extra warnings, but we should suppress ones
773 # that we don't want to see.
774 elif re.match(r'^-W.+', arg) and not re.match(r'^-Wno-.+', arg):
775 pass
776 # and consider everything else as compilation flag.
777 else:
778 result['flags'].append(arg)
779
780 return result