1 | # -*- coding: utf-8 -*- |
2 | # 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 |
5 | """ This module implements the 'scan-build' command API. |
6 | |
7 | To 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 | |
13 | import re |
14 | import os |
15 | import os.path |
16 | import json |
17 | import logging |
18 | import multiprocessing |
19 | import tempfile |
20 | import functools |
21 | import subprocess |
22 | import contextlib |
23 | import datetime |
24 | import shutil |
25 | import glob |
26 | from collections import defaultdict |
27 | |
28 | from libscanbuild import command_entry_point, compiler_wrapper, \ |
29 | wrapper_environment, run_build, run_command, CtuConfig |
30 | from libscanbuild.arguments import parse_args_for_scan_build, \ |
31 | parse_args_for_analyze_build |
32 | from libscanbuild.intercept import capture |
33 | from libscanbuild.report import document |
34 | from libscanbuild.compilation import split_command, classify_source, \ |
35 | compiler_language |
36 | from libscanbuild.clang import get_version, get_arguments, get_triple_arch |
37 | from libscanbuild.shell import decode |
38 | |
39 | __all__ = ['scan_build', 'analyze_build', 'analyze_compiler_wrapper'] |
40 | |
41 | COMPILER_WRAPPER_CC = 'analyze-cc' |
42 | COMPILER_WRAPPER_CXX = 'analyze-c++' |
43 | |
44 | CTU_EXTDEF_MAP_FILENAME = 'externalDefMap.txt' |
45 | CTU_TEMP_DEFMAP_FOLDER = 'tmpExternalDefMaps' |
46 | |
47 | |
48 | @command_entry_point |
49 | def scan_build(): |
50 | """ Entry point for scan-build command. """ |
51 | |
52 | args = parse_args_for_scan_build() |
53 | # will re-assign the report directory as new output |
54 | with report_directory(args.output, args.keep_empty) as args.output: |
55 | # 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. |
63 | if need_analyzer(args.build): |
64 | govern_analyzer_runs(args) |
65 | else: |
66 | # Run build command and analyzer with compiler wrappers. |
67 | environment = setup_environment(args) |
68 | exit_code = run_build(args.build, env=environment) |
69 | # Cover report generation and bug counting. |
70 | number_of_bugs = document(args) |
71 | # 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 |
76 | def analyze_build(): |
77 | """ Entry point for analyze-build command. """ |
78 | |
79 | args = parse_args_for_analyze_build() |
80 | # will re-assign the report directory as new output |
81 | with report_directory(args.output, args.keep_empty) as args.output: |
82 | # Run the analyzer against a compilation db. |
83 | govern_analyzer_runs(args) |
84 | # Cover report generation and bug counting. |
85 | number_of_bugs = document(args) |
86 | # Set exit status as it was requested. |
87 | return number_of_bugs if args.status_bugs else 0 |
88 | |
89 | |
90 | def 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 | |
96 | To run `scan-build` against the configure step might be necessary, |
97 | 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 | |
100 | return len(args) and not re.search(r'configure|autogen', args[0]) |
101 | |
102 | |
103 | def 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 | |
112 | def 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, |
119 | extdef_map_cmd=args.extdef_map_cmd) |
120 | if hasattr(args, 'ctu_phases') and hasattr(args.ctu_phases, 'dir') |
121 | else CtuConfig(collect=False, analyze=False, dir='', extdef_map_cmd='')) |
122 | |
123 | |
124 | def 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], |
132 | extdef_map_cmd=ctu_config[3]) |
133 | |
134 | |
135 | def 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. |
139 | |
140 | :param extdef_map_lines: Contains the id of a definition (mangled name) and |
141 | the originating source (the corresponding AST file) name. |
142 | :type extdef_map_lines: Iterator of str. |
143 | :returns: Mangled name - AST file pairs. |
144 | :rtype: List of (str, str) tuples. |
145 | """ |
146 | |
147 | mangled_to_asts = defaultdict(set) |
148 | |
149 | for line in extdef_map_lines: |
150 | 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 | |
162 | def merge_ctu_extdef_maps(ctudir): |
163 | """ Merge individual external definition maps into a global one. |
164 | |
165 | As the collect phase runs parallel on multiple threads, all compilation |
166 | 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. |
169 | These files should be merged at the end into a global map file: |
170 | CTU_EXTDEF_MAP_FILENAME.""" |
171 | |
172 | def generate_extdef_map_lines(extdefmap_dir): |
173 | """ Iterate over all lines of input files in a determined order. """ |
174 | |
175 | files = glob.glob(os.path.join(extdefmap_dir, '*')) |
176 | 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): |
183 | """ Write (mangled name, ast file) pairs into final file. """ |
184 | |
185 | 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: |
188 | 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) |
195 | extdefmap_dir = os.path.join(ctudir, triple_arch, |
196 | CTU_TEMP_DEFMAP_FOLDER) |
197 | |
198 | extdef_map_lines = generate_extdef_map_lines(extdefmap_dir) |
199 | mangled_ast_pairs = create_global_ctu_extdef_map(extdef_map_lines) |
200 | write_global_map(triple_arch, mangled_ast_pairs) |
201 | |
202 | # Remove all temporary files |
203 | shutil.rmtree(extdefmap_dir, ignore_errors=True) |
204 | |
205 | |
206 | def run_analyzer_parallel(args): |
207 | """ 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, |
216 | 'output_dir': args.output, |
217 | 'output_format': args.output_format, |
218 | 'output_failures': args.output_failures, |
219 | 'direct_args': analyzer_params(args), |
220 | 'force_debug': args.force_debug, |
221 | 'ctu': get_ctu_config_from_args(args) |
222 | } |
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 | |
239 | def 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: |
254 | # CTU strings are coming from args.ctu_dir and extdef_map_cmd, |
255 | # so we can leave it empty |
256 | args.ctu_phases = CtuConfig(collect=True, analyze=False, |
257 | dir='', extdef_map_cmd='') |
258 | run_analyzer_parallel(args) |
259 | merge_ctu_extdef_maps(ctu_config.dir) |
260 | args.ctu_phases = CtuConfig(collect=False, analyze=True, |
261 | dir='', extdef_map_cmd='') |
262 | 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: |
268 | merge_ctu_extdef_maps(ctu_config.dir) |
269 | |
270 | |
271 | def setup_environment(args): |
272 | """ Set up environment for build command to interpose compiler wrapper. """ |
273 | |
274 | environment = dict(os.environ) |
275 | environment.update(wrapper_environment(args)) |
276 | environment.update({ |
277 | 'CC': COMPILER_WRAPPER_CC, |
278 | 'CXX': COMPILER_WRAPPER_CXX, |
279 | 'ANALYZE_BUILD_CLANG': args.clang if need_analyzer(args.build) else '', |
280 | 'ANALYZE_BUILD_REPORT_DIR': args.output, |
281 | 'ANALYZE_BUILD_REPORT_FORMAT': args.output_format, |
282 | 'ANALYZE_BUILD_REPORT_FAILURES': 'yes' if args.output_failures else '', |
283 | 'ANALYZE_BUILD_PARAMETERS': ' '.join(analyzer_params(args)), |
284 | 'ANALYZE_BUILD_FORCE_DEBUG': 'yes' if args.force_debug else '', |
285 | 'ANALYZE_BUILD_CTU': json.dumps(get_ctu_config_from_args(args)) |
286 | }) |
287 | return environment |
288 | |
289 | |
290 | @command_entry_point |
291 | def analyze_compiler_wrapper(): |
292 | """ Entry point for `analyze-cc` and `analyze-c++` compiler wrappers. """ |
293 | |
294 | return compiler_wrapper(analyze_compiler_wrapper_impl) |
295 | |
296 | |
297 | def 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. |
301 | if result or not os.getenv('ANALYZE_BUILD_CLANG'): |
302 | 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, |
318 | 'command': [execution.cmd[0], '-c'] + compilation.flags, |
319 | 'ctu': get_ctu_config_from_json(os.getenv('ANALYZE_BUILD_CTU')) |
320 | } |
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()) |
330 | |
331 | |
332 | @contextlib.contextmanager |
333 | def 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 | |
365 | def 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 | |
369 | result = [] |
370 | |
371 | if args.store_model: |
372 | result.append('-analyzer-store={0}'.format(args.store_model)) |
373 | if args.constraints_model: |
374 | result.append('-analyzer-constraints={0}'.format( |
375 | args.constraints_model)) |
376 | 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: |
387 | result.extend(['-analyzer-config', args.analyzer_config]) |
388 | 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]) |
398 | |
399 | return prefix_with('-Xclang', result) |
400 | |
401 | |
402 | def 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 |
430 | 'output_format', # it's 'plist', 'html', both or plist-multi-file |
431 | 'output_failures', # generate crash reports or not |
432 | 'ctu']) # ctu control options |
433 | def 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 |
438 | each other in chain. If the analyzis is not possible the given method |
439 | 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: |
454 | logging.error("Problem occurred during analyzis.", exc_info=1) |
455 | return None |
456 | |
457 | |
458 | @require(['clang', 'directory', 'flags', 'file', 'output_dir', 'language', |
459 | 'error_output', 'exit_code']) |
460 | def 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']) |
513 | def 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. """ |
520 | if opts['output_format'] in { |
521 | 'plist', |
522 | 'plist-html', |
523 | 'plist-multi-file'}: |
524 | (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 | |
547 | def 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. """ |
550 | |
551 | extdef_ast_list = [] |
552 | for extdef_src_txt in extdef_src_list: |
553 | mangled_name, path = extdef_src_txt.split(" ", 1) |
554 | # 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") |
559 | extdef_ast_list.append(mangled_name + " " + ast_path) |
560 | return extdef_ast_list |
561 | |
562 | |
563 | @require(['clang', 'directory', 'flags', 'direct_args', 'file', 'ctu']) |
564 | def 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 | |
591 | def map_extdefs(triple_arch): |
592 | """ Generate external definition map file for the current source. """ |
593 | |
594 | args = opts['direct_args'] + opts['flags'] |
595 | 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): |
606 | try: |
607 | os.makedirs(extern_defs_map_folder) |
608 | except OSError: |
609 | # In case an other process already created it. |
610 | pass |
611 | if extdef_ast_list: |
612 | with tempfile.NamedTemporaryFile(mode='w', |
613 | dir=extern_defs_map_folder, |
614 | delete=False) as out_file: |
615 | out_file.write("\n".join(extdef_ast_list) + "\n") |
616 | |
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) |
622 | map_extdefs(triple_arch) |
623 | |
624 | |
625 | @require(['ctu']) |
626 | def 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 | |
649 | @require(['flags', 'force_debug']) |
650 | def filter_debug_flags(opts, continuation=dispatch_ctu): |
651 | """ 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']) |
661 | def 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']) |
691 | def 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 | |
717 | |
718 | # 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 |
722 | IGNORED_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 | |
744 | def 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 |
781 | |