Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
14 | pmbaty | 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 | ClangErrorException |
||
38 | from libscanbuild.shell import decode |
||
39 | |||
40 | __all__ = ['scan_build', 'analyze_build', 'analyze_compiler_wrapper'] |
||
41 | |||
42 | scanbuild_dir = os.path.dirname(os.path.realpath(__import__('sys').argv[0])) |
||
43 | |||
44 | COMPILER_WRAPPER_CC = os.path.join(scanbuild_dir, '..', 'libexec', 'analyze-cc') |
||
45 | COMPILER_WRAPPER_CXX = os.path.join(scanbuild_dir, '..', 'libexec', 'analyze-c++') |
||
46 | |||
47 | CTU_EXTDEF_MAP_FILENAME = 'externalDefMap.txt' |
||
48 | CTU_TEMP_DEFMAP_FOLDER = 'tmpExternalDefMaps' |
||
49 | |||
50 | |||
51 | @command_entry_point |
||
52 | def scan_build(): |
||
53 | """ Entry point for scan-build command. """ |
||
54 | |||
55 | args = parse_args_for_scan_build() |
||
56 | # will re-assign the report directory as new output |
||
57 | with report_directory( |
||
58 | args.output, args.keep_empty, args.output_format) as args.output: |
||
59 | # Run against a build command. there are cases, when analyzer run |
||
60 | # is not required. But we need to set up everything for the |
||
61 | # wrappers, because 'configure' needs to capture the CC/CXX values |
||
62 | # for the Makefile. |
||
63 | if args.intercept_first: |
||
64 | # Run build command with intercept module. |
||
65 | exit_code = capture(args) |
||
66 | # Run the analyzer against the captured commands. |
||
67 | if need_analyzer(args.build): |
||
68 | govern_analyzer_runs(args) |
||
69 | else: |
||
70 | # Run build command and analyzer with compiler wrappers. |
||
71 | environment = setup_environment(args) |
||
72 | exit_code = run_build(args.build, env=environment) |
||
73 | # Cover report generation and bug counting. |
||
74 | number_of_bugs = document(args) |
||
75 | # Set exit status as it was requested. |
||
76 | return number_of_bugs if args.status_bugs else exit_code |
||
77 | |||
78 | |||
79 | @command_entry_point |
||
80 | def analyze_build(): |
||
81 | """ Entry point for analyze-build command. """ |
||
82 | |||
83 | args = parse_args_for_analyze_build() |
||
84 | # will re-assign the report directory as new output |
||
85 | with report_directory(args.output, args.keep_empty, args.output_format) as args.output: |
||
86 | # Run the analyzer against a compilation db. |
||
87 | govern_analyzer_runs(args) |
||
88 | # Cover report generation and bug counting. |
||
89 | number_of_bugs = document(args) |
||
90 | # Set exit status as it was requested. |
||
91 | return number_of_bugs if args.status_bugs else 0 |
||
92 | |||
93 | |||
94 | def need_analyzer(args): |
||
95 | """ Check the intent of the build command. |
||
96 | |||
97 | When static analyzer run against project configure step, it should be |
||
98 | silent and no need to run the analyzer or generate report. |
||
99 | |||
100 | To run `scan-build` against the configure step might be necessary, |
||
101 | when compiler wrappers are used. That's the moment when build setup |
||
102 | check the compiler and capture the location for the build process. """ |
||
103 | |||
104 | return len(args) and not re.search(r'configure|autogen', args[0]) |
||
105 | |||
106 | |||
107 | def prefix_with(constant, pieces): |
||
108 | """ From a sequence create another sequence where every second element |
||
109 | is from the original sequence and the odd elements are the prefix. |
||
110 | |||
111 | eg.: prefix_with(0, [1,2,3]) creates [0, 1, 0, 2, 0, 3] """ |
||
112 | |||
113 | return [elem for piece in pieces for elem in [constant, piece]] |
||
114 | |||
115 | |||
116 | def get_ctu_config_from_args(args): |
||
117 | """ CTU configuration is created from the chosen phases and dir. """ |
||
118 | |||
119 | return ( |
||
120 | CtuConfig(collect=args.ctu_phases.collect, |
||
121 | analyze=args.ctu_phases.analyze, |
||
122 | dir=args.ctu_dir, |
||
123 | extdef_map_cmd=args.extdef_map_cmd) |
||
124 | if hasattr(args, 'ctu_phases') and hasattr(args.ctu_phases, 'dir') |
||
125 | else CtuConfig(collect=False, analyze=False, dir='', extdef_map_cmd='')) |
||
126 | |||
127 | |||
128 | def get_ctu_config_from_json(ctu_conf_json): |
||
129 | """ CTU configuration is created from the chosen phases and dir. """ |
||
130 | |||
131 | ctu_config = json.loads(ctu_conf_json) |
||
132 | # Recover namedtuple from json when coming from analyze-cc or analyze-c++ |
||
133 | return CtuConfig(collect=ctu_config[0], |
||
134 | analyze=ctu_config[1], |
||
135 | dir=ctu_config[2], |
||
136 | extdef_map_cmd=ctu_config[3]) |
||
137 | |||
138 | |||
139 | def create_global_ctu_extdef_map(extdef_map_lines): |
||
140 | """ Takes iterator of individual external definition maps and creates a |
||
141 | global map keeping only unique names. We leave conflicting names out of |
||
142 | CTU. |
||
143 | |||
144 | :param extdef_map_lines: Contains the id of a definition (mangled name) and |
||
145 | the originating source (the corresponding AST file) name. |
||
146 | :type extdef_map_lines: Iterator of str. |
||
147 | :returns: Mangled name - AST file pairs. |
||
148 | :rtype: List of (str, str) tuples. |
||
149 | """ |
||
150 | |||
151 | mangled_to_asts = defaultdict(set) |
||
152 | |||
153 | for line in extdef_map_lines: |
||
154 | mangled_name, ast_file = line.strip().split(' ', 1) |
||
155 | mangled_to_asts[mangled_name].add(ast_file) |
||
156 | |||
157 | mangled_ast_pairs = [] |
||
158 | |||
159 | for mangled_name, ast_files in mangled_to_asts.items(): |
||
160 | if len(ast_files) == 1: |
||
161 | mangled_ast_pairs.append((mangled_name, next(iter(ast_files)))) |
||
162 | |||
163 | return mangled_ast_pairs |
||
164 | |||
165 | |||
166 | def merge_ctu_extdef_maps(ctudir): |
||
167 | """ Merge individual external definition maps into a global one. |
||
168 | |||
169 | As the collect phase runs parallel on multiple threads, all compilation |
||
170 | units are separately mapped into a temporary file in CTU_TEMP_DEFMAP_FOLDER. |
||
171 | These definition maps contain the mangled names and the source |
||
172 | (AST generated from the source) which had their definition. |
||
173 | These files should be merged at the end into a global map file: |
||
174 | CTU_EXTDEF_MAP_FILENAME.""" |
||
175 | |||
176 | def generate_extdef_map_lines(extdefmap_dir): |
||
177 | """ Iterate over all lines of input files in a determined order. """ |
||
178 | |||
179 | files = glob.glob(os.path.join(extdefmap_dir, '*')) |
||
180 | files.sort() |
||
181 | for filename in files: |
||
182 | with open(filename, 'r') as in_file: |
||
183 | for line in in_file: |
||
184 | yield line |
||
185 | |||
186 | def write_global_map(arch, mangled_ast_pairs): |
||
187 | """ Write (mangled name, ast file) pairs into final file. """ |
||
188 | |||
189 | extern_defs_map_file = os.path.join(ctudir, arch, |
||
190 | CTU_EXTDEF_MAP_FILENAME) |
||
191 | with open(extern_defs_map_file, 'w') as out_file: |
||
192 | for mangled_name, ast_file in mangled_ast_pairs: |
||
193 | out_file.write('%s %s\n' % (mangled_name, ast_file)) |
||
194 | |||
195 | triple_arches = glob.glob(os.path.join(ctudir, '*')) |
||
196 | for triple_path in triple_arches: |
||
197 | if os.path.isdir(triple_path): |
||
198 | triple_arch = os.path.basename(triple_path) |
||
199 | extdefmap_dir = os.path.join(ctudir, triple_arch, |
||
200 | CTU_TEMP_DEFMAP_FOLDER) |
||
201 | |||
202 | extdef_map_lines = generate_extdef_map_lines(extdefmap_dir) |
||
203 | mangled_ast_pairs = create_global_ctu_extdef_map(extdef_map_lines) |
||
204 | write_global_map(triple_arch, mangled_ast_pairs) |
||
205 | |||
206 | # Remove all temporary files |
||
207 | shutil.rmtree(extdefmap_dir, ignore_errors=True) |
||
208 | |||
209 | |||
210 | def run_analyzer_parallel(args): |
||
211 | """ Runs the analyzer against the given compilation database. """ |
||
212 | |||
213 | def exclude(filename, directory): |
||
214 | """ Return true when any excluded directory prefix the filename. """ |
||
215 | if not os.path.isabs(filename): |
||
216 | # filename is either absolute or relative to directory. Need to turn |
||
217 | # it to absolute since 'args.excludes' are absolute paths. |
||
218 | filename = os.path.normpath(os.path.join(directory, filename)) |
||
219 | return any(re.match(r'^' + exclude_directory, filename) |
||
220 | for exclude_directory in args.excludes) |
||
221 | |||
222 | consts = { |
||
223 | 'clang': args.clang, |
||
224 | 'output_dir': args.output, |
||
225 | 'output_format': args.output_format, |
||
226 | 'output_failures': args.output_failures, |
||
227 | 'direct_args': analyzer_params(args), |
||
228 | 'force_debug': args.force_debug, |
||
229 | 'ctu': get_ctu_config_from_args(args) |
||
230 | } |
||
231 | |||
232 | logging.debug('run analyzer against compilation database') |
||
233 | with open(args.cdb, 'r') as handle: |
||
234 | generator = (dict(cmd, **consts) |
||
235 | for cmd in json.load(handle) if not exclude( |
||
236 | cmd['file'], cmd['directory'])) |
||
237 | # when verbose output requested execute sequentially |
||
238 | pool = multiprocessing.Pool(1 if args.verbose > 2 else None) |
||
239 | for current in pool.imap_unordered(run, generator): |
||
240 | if current is not None: |
||
241 | # display error message from the static analyzer |
||
242 | for line in current['error_output']: |
||
243 | logging.info(line.rstrip()) |
||
244 | pool.close() |
||
245 | pool.join() |
||
246 | |||
247 | |||
248 | def govern_analyzer_runs(args): |
||
249 | """ Governs multiple runs in CTU mode or runs once in normal mode. """ |
||
250 | |||
251 | ctu_config = get_ctu_config_from_args(args) |
||
252 | # If we do a CTU collect (1st phase) we remove all previous collection |
||
253 | # data first. |
||
254 | if ctu_config.collect: |
||
255 | shutil.rmtree(ctu_config.dir, ignore_errors=True) |
||
256 | |||
257 | # If the user asked for a collect (1st) and analyze (2nd) phase, we do an |
||
258 | # all-in-one run where we deliberately remove collection data before and |
||
259 | # also after the run. If the user asks only for a single phase data is |
||
260 | # left so multiple analyze runs can use the same data gathered by a single |
||
261 | # collection run. |
||
262 | if ctu_config.collect and ctu_config.analyze: |
||
263 | # CTU strings are coming from args.ctu_dir and extdef_map_cmd, |
||
264 | # so we can leave it empty |
||
265 | args.ctu_phases = CtuConfig(collect=True, analyze=False, |
||
266 | dir='', extdef_map_cmd='') |
||
267 | run_analyzer_parallel(args) |
||
268 | merge_ctu_extdef_maps(ctu_config.dir) |
||
269 | args.ctu_phases = CtuConfig(collect=False, analyze=True, |
||
270 | dir='', extdef_map_cmd='') |
||
271 | run_analyzer_parallel(args) |
||
272 | shutil.rmtree(ctu_config.dir, ignore_errors=True) |
||
273 | else: |
||
274 | # Single runs (collect or analyze) are launched from here. |
||
275 | run_analyzer_parallel(args) |
||
276 | if ctu_config.collect: |
||
277 | merge_ctu_extdef_maps(ctu_config.dir) |
||
278 | |||
279 | |||
280 | def setup_environment(args): |
||
281 | """ Set up environment for build command to interpose compiler wrapper. """ |
||
282 | |||
283 | environment = dict(os.environ) |
||
284 | environment.update(wrapper_environment(args)) |
||
285 | environment.update({ |
||
286 | 'CC': COMPILER_WRAPPER_CC, |
||
287 | 'CXX': COMPILER_WRAPPER_CXX, |
||
288 | 'ANALYZE_BUILD_CLANG': args.clang if need_analyzer(args.build) else '', |
||
289 | 'ANALYZE_BUILD_REPORT_DIR': args.output, |
||
290 | 'ANALYZE_BUILD_REPORT_FORMAT': args.output_format, |
||
291 | 'ANALYZE_BUILD_REPORT_FAILURES': 'yes' if args.output_failures else '', |
||
292 | 'ANALYZE_BUILD_PARAMETERS': ' '.join(analyzer_params(args)), |
||
293 | 'ANALYZE_BUILD_FORCE_DEBUG': 'yes' if args.force_debug else '', |
||
294 | 'ANALYZE_BUILD_CTU': json.dumps(get_ctu_config_from_args(args)) |
||
295 | }) |
||
296 | return environment |
||
297 | |||
298 | |||
299 | @command_entry_point |
||
300 | def analyze_compiler_wrapper(): |
||
301 | """ Entry point for `analyze-cc` and `analyze-c++` compiler wrappers. """ |
||
302 | |||
303 | return compiler_wrapper(analyze_compiler_wrapper_impl) |
||
304 | |||
305 | |||
306 | def analyze_compiler_wrapper_impl(result, execution): |
||
307 | """ Implements analyzer compiler wrapper functionality. """ |
||
308 | |||
309 | # don't run analyzer when compilation fails. or when it's not requested. |
||
310 | if result or not os.getenv('ANALYZE_BUILD_CLANG'): |
||
311 | return |
||
312 | |||
313 | # check is it a compilation? |
||
314 | compilation = split_command(execution.cmd) |
||
315 | if compilation is None: |
||
316 | return |
||
317 | # collect the needed parameters from environment, crash when missing |
||
318 | parameters = { |
||
319 | 'clang': os.getenv('ANALYZE_BUILD_CLANG'), |
||
320 | 'output_dir': os.getenv('ANALYZE_BUILD_REPORT_DIR'), |
||
321 | 'output_format': os.getenv('ANALYZE_BUILD_REPORT_FORMAT'), |
||
322 | 'output_failures': os.getenv('ANALYZE_BUILD_REPORT_FAILURES'), |
||
323 | 'direct_args': os.getenv('ANALYZE_BUILD_PARAMETERS', |
||
324 | '').split(' '), |
||
325 | 'force_debug': os.getenv('ANALYZE_BUILD_FORCE_DEBUG'), |
||
326 | 'directory': execution.cwd, |
||
327 | 'command': [execution.cmd[0], '-c'] + compilation.flags, |
||
328 | 'ctu': get_ctu_config_from_json(os.getenv('ANALYZE_BUILD_CTU')) |
||
329 | } |
||
330 | # call static analyzer against the compilation |
||
331 | for source in compilation.files: |
||
332 | parameters.update({'file': source}) |
||
333 | logging.debug('analyzer parameters %s', parameters) |
||
334 | current = run(parameters) |
||
335 | # display error message from the static analyzer |
||
336 | if current is not None: |
||
337 | for line in current['error_output']: |
||
338 | logging.info(line.rstrip()) |
||
339 | |||
340 | |||
341 | @contextlib.contextmanager |
||
342 | def report_directory(hint, keep, output_format): |
||
343 | """ Responsible for the report directory. |
||
344 | |||
345 | hint -- could specify the parent directory of the output directory. |
||
346 | keep -- a boolean value to keep or delete the empty report directory. """ |
||
347 | |||
348 | stamp_format = 'scan-build-%Y-%m-%d-%H-%M-%S-%f-' |
||
349 | stamp = datetime.datetime.now().strftime(stamp_format) |
||
350 | parent_dir = os.path.abspath(hint) |
||
351 | if not os.path.exists(parent_dir): |
||
352 | os.makedirs(parent_dir) |
||
353 | name = tempfile.mkdtemp(prefix=stamp, dir=parent_dir) |
||
354 | |||
355 | logging.info('Report directory created: %s', name) |
||
356 | |||
357 | try: |
||
358 | yield name |
||
359 | finally: |
||
360 | args = (name,) |
||
361 | if os.listdir(name): |
||
362 | if output_format not in ['sarif', 'sarif-html']: # FIXME: |
||
363 | # 'scan-view' currently does not support sarif format. |
||
364 | msg = "Run 'scan-view %s' to examine bug reports." |
||
365 | elif output_format == 'sarif-html': |
||
366 | msg = "Run 'scan-view %s' to examine bug reports or see " \ |
||
367 | "merged sarif results at %s/results-merged.sarif." |
||
368 | args = (name, name) |
||
369 | else: |
||
370 | msg = "View merged sarif results at %s/results-merged.sarif." |
||
371 | keep = True |
||
372 | else: |
||
373 | if keep: |
||
374 | msg = "Report directory '%s' contains no report, but kept." |
||
375 | else: |
||
376 | msg = "Removing directory '%s' because it contains no report." |
||
377 | logging.warning(msg, *args) |
||
378 | |||
379 | if not keep: |
||
380 | os.rmdir(name) |
||
381 | |||
382 | |||
383 | def analyzer_params(args): |
||
384 | """ A group of command line arguments can mapped to command |
||
385 | line arguments of the analyzer. This method generates those. """ |
||
386 | |||
387 | result = [] |
||
388 | |||
389 | if args.constraints_model: |
||
390 | result.append('-analyzer-constraints={0}'.format( |
||
391 | args.constraints_model)) |
||
392 | if args.internal_stats: |
||
393 | result.append('-analyzer-stats') |
||
394 | if args.analyze_headers: |
||
395 | result.append('-analyzer-opt-analyze-headers') |
||
396 | if args.stats: |
||
397 | result.append('-analyzer-checker=debug.Stats') |
||
398 | if args.maxloop: |
||
399 | result.extend(['-analyzer-max-loop', str(args.maxloop)]) |
||
400 | if args.output_format: |
||
401 | result.append('-analyzer-output={0}'.format(args.output_format)) |
||
402 | if args.analyzer_config: |
||
403 | result.extend(['-analyzer-config', args.analyzer_config]) |
||
404 | if args.verbose >= 4: |
||
405 | result.append('-analyzer-display-progress') |
||
406 | if args.plugins: |
||
407 | result.extend(prefix_with('-load', args.plugins)) |
||
408 | if args.enable_checker: |
||
409 | checkers = ','.join(args.enable_checker) |
||
410 | result.extend(['-analyzer-checker', checkers]) |
||
411 | if args.disable_checker: |
||
412 | checkers = ','.join(args.disable_checker) |
||
413 | result.extend(['-analyzer-disable-checker', checkers]) |
||
414 | |||
415 | return prefix_with('-Xclang', result) |
||
416 | |||
417 | |||
418 | def require(required): |
||
419 | """ Decorator for checking the required values in state. |
||
420 | |||
421 | It checks the required attributes in the passed state and stop when |
||
422 | any of those is missing. """ |
||
423 | |||
424 | def decorator(function): |
||
425 | @functools.wraps(function) |
||
426 | def wrapper(*args, **kwargs): |
||
427 | for key in required: |
||
428 | if key not in args[0]: |
||
429 | raise KeyError('{0} not passed to {1}'.format( |
||
430 | key, function.__name__)) |
||
431 | |||
432 | return function(*args, **kwargs) |
||
433 | |||
434 | return wrapper |
||
435 | |||
436 | return decorator |
||
437 | |||
438 | |||
439 | @require(['command', # entry from compilation database |
||
440 | 'directory', # entry from compilation database |
||
441 | 'file', # entry from compilation database |
||
442 | 'clang', # clang executable name (and path) |
||
443 | 'direct_args', # arguments from command line |
||
444 | 'force_debug', # kill non debug macros |
||
445 | 'output_dir', # where generated report files shall go |
||
446 | 'output_format', # it's 'plist', 'html', 'plist-html', 'plist-multi-file', 'sarif', or 'sarif-html' |
||
447 | 'output_failures', # generate crash reports or not |
||
448 | 'ctu']) # ctu control options |
||
449 | def run(opts): |
||
450 | """ Entry point to run (or not) static analyzer against a single entry |
||
451 | of the compilation database. |
||
452 | |||
453 | This complex task is decomposed into smaller methods which are calling |
||
454 | each other in chain. If the analysis is not possible the given method |
||
455 | just return and break the chain. |
||
456 | |||
457 | The passed parameter is a python dictionary. Each method first check |
||
458 | that the needed parameters received. (This is done by the 'require' |
||
459 | decorator. It's like an 'assert' to check the contract between the |
||
460 | caller and the called method.) """ |
||
461 | |||
462 | try: |
||
463 | command = opts.pop('command') |
||
464 | command = command if isinstance(command, list) else decode(command) |
||
465 | logging.debug("Run analyzer against '%s'", command) |
||
466 | opts.update(classify_parameters(command)) |
||
467 | |||
468 | return arch_check(opts) |
||
469 | except Exception: |
||
470 | logging.error("Problem occurred during analysis.", exc_info=1) |
||
471 | return None |
||
472 | |||
473 | |||
474 | @require(['clang', 'directory', 'flags', 'file', 'output_dir', 'language', |
||
475 | 'error_output', 'exit_code']) |
||
476 | def report_failure(opts): |
||
477 | """ Create report when analyzer failed. |
||
478 | |||
479 | The major report is the preprocessor output. The output filename generated |
||
480 | randomly. The compiler output also captured into '.stderr.txt' file. |
||
481 | And some more execution context also saved into '.info.txt' file. """ |
||
482 | |||
483 | def extension(): |
||
484 | """ Generate preprocessor file extension. """ |
||
485 | |||
486 | mapping = {'objective-c++': '.mii', 'objective-c': '.mi', 'c++': '.ii'} |
||
487 | return mapping.get(opts['language'], '.i') |
||
488 | |||
489 | def destination(): |
||
490 | """ Creates failures directory if not exits yet. """ |
||
491 | |||
492 | failures_dir = os.path.join(opts['output_dir'], 'failures') |
||
493 | if not os.path.isdir(failures_dir): |
||
494 | os.makedirs(failures_dir) |
||
495 | return failures_dir |
||
496 | |||
497 | # Classify error type: when Clang terminated by a signal it's a 'Crash'. |
||
498 | # (python subprocess Popen.returncode is negative when child terminated |
||
499 | # by signal.) Everything else is 'Other Error'. |
||
500 | error = 'crash' if opts['exit_code'] < 0 else 'other_error' |
||
501 | # Create preprocessor output file name. (This is blindly following the |
||
502 | # Perl implementation.) |
||
503 | (handle, name) = tempfile.mkstemp(suffix=extension(), |
||
504 | prefix='clang_' + error + '_', |
||
505 | dir=destination()) |
||
506 | os.close(handle) |
||
507 | # Execute Clang again, but run the syntax check only. |
||
508 | cwd = opts['directory'] |
||
509 | cmd = [opts['clang'], '-fsyntax-only', '-E'] + opts['flags'] + \ |
||
510 | [opts['file'], '-o', name] |
||
511 | try: |
||
512 | cmd = get_arguments(cmd, cwd) |
||
513 | run_command(cmd, cwd=cwd) |
||
514 | except subprocess.CalledProcessError: |
||
515 | pass |
||
516 | except ClangErrorException: |
||
517 | pass |
||
518 | # write general information about the crash |
||
519 | with open(name + '.info.txt', 'w') as handle: |
||
520 | handle.write(opts['file'] + os.linesep) |
||
521 | handle.write(error.title().replace('_', ' ') + os.linesep) |
||
522 | handle.write(' '.join(cmd) + os.linesep) |
||
523 | handle.write(' '.join(os.uname()) + os.linesep) |
||
524 | handle.write(get_version(opts['clang'])) |
||
525 | handle.close() |
||
526 | # write the captured output too |
||
527 | with open(name + '.stderr.txt', 'w') as handle: |
||
528 | handle.writelines(opts['error_output']) |
||
529 | handle.close() |
||
530 | |||
531 | |||
532 | @require(['clang', 'directory', 'flags', 'direct_args', 'file', 'output_dir', |
||
533 | 'output_format']) |
||
534 | def run_analyzer(opts, continuation=report_failure): |
||
535 | """ It assembles the analysis command line and executes it. Capture the |
||
536 | output of the analysis and returns with it. If failure reports are |
||
537 | requested, it calls the continuation to generate it. """ |
||
538 | |||
539 | def target(): |
||
540 | """ Creates output file name for reports. """ |
||
541 | if opts['output_format'] in { |
||
542 | 'plist', |
||
543 | 'plist-html', |
||
544 | 'plist-multi-file'}: |
||
545 | (handle, name) = tempfile.mkstemp(prefix='report-', |
||
546 | suffix='.plist', |
||
547 | dir=opts['output_dir']) |
||
548 | os.close(handle) |
||
549 | return name |
||
550 | elif opts['output_format'] in { |
||
551 | 'sarif', |
||
552 | 'sarif-html'}: |
||
553 | (handle, name) = tempfile.mkstemp(prefix='result-', |
||
554 | suffix='.sarif', |
||
555 | dir=opts['output_dir']) |
||
556 | os.close(handle) |
||
557 | return name |
||
558 | return opts['output_dir'] |
||
559 | |||
560 | try: |
||
561 | cwd = opts['directory'] |
||
562 | cmd = get_arguments([opts['clang'], '--analyze'] + |
||
563 | opts['direct_args'] + opts['flags'] + |
||
564 | [opts['file'], '-o', target()], |
||
565 | cwd) |
||
566 | output = run_command(cmd, cwd=cwd) |
||
567 | return {'error_output': output, 'exit_code': 0} |
||
568 | except subprocess.CalledProcessError as ex: |
||
569 | result = {'error_output': ex.output, 'exit_code': ex.returncode} |
||
570 | if opts.get('output_failures', False): |
||
571 | opts.update(result) |
||
572 | continuation(opts) |
||
573 | return result |
||
574 | except ClangErrorException as ex: |
||
575 | result = {'error_output': ex.error, 'exit_code': 0} |
||
576 | if opts.get('output_failures', False): |
||
577 | opts.update(result) |
||
578 | continuation(opts) |
||
579 | return result |
||
580 | |||
581 | |||
582 | def extdef_map_list_src_to_ast(extdef_src_list): |
||
583 | """ Turns textual external definition map list with source files into an |
||
584 | external definition map list with ast files. """ |
||
585 | |||
586 | extdef_ast_list = [] |
||
587 | for extdef_src_txt in extdef_src_list: |
||
588 | mangled_name, path = extdef_src_txt.split(" ", 1) |
||
589 | # Normalize path on windows as well |
||
590 | path = os.path.splitdrive(path)[1] |
||
591 | # Make relative path out of absolute |
||
592 | path = path[1:] if path[0] == os.sep else path |
||
593 | ast_path = os.path.join("ast", path + ".ast") |
||
594 | extdef_ast_list.append(mangled_name + " " + ast_path) |
||
595 | return extdef_ast_list |
||
596 | |||
597 | |||
598 | @require(['clang', 'directory', 'flags', 'direct_args', 'file', 'ctu']) |
||
599 | def ctu_collect_phase(opts): |
||
600 | """ Preprocess source by generating all data needed by CTU analysis. """ |
||
601 | |||
602 | def generate_ast(triple_arch): |
||
603 | """ Generates ASTs for the current compilation command. """ |
||
604 | |||
605 | args = opts['direct_args'] + opts['flags'] |
||
606 | ast_joined_path = os.path.join(opts['ctu'].dir, triple_arch, 'ast', |
||
607 | os.path.realpath(opts['file'])[1:] + |
||
608 | '.ast') |
||
609 | ast_path = os.path.abspath(ast_joined_path) |
||
610 | ast_dir = os.path.dirname(ast_path) |
||
611 | if not os.path.isdir(ast_dir): |
||
612 | try: |
||
613 | os.makedirs(ast_dir) |
||
614 | except OSError: |
||
615 | # In case an other process already created it. |
||
616 | pass |
||
617 | ast_command = [opts['clang'], '-emit-ast'] |
||
618 | ast_command.extend(args) |
||
619 | ast_command.append('-w') |
||
620 | ast_command.append(opts['file']) |
||
621 | ast_command.append('-o') |
||
622 | ast_command.append(ast_path) |
||
623 | logging.debug("Generating AST using '%s'", ast_command) |
||
624 | run_command(ast_command, cwd=opts['directory']) |
||
625 | |||
626 | def map_extdefs(triple_arch): |
||
627 | """ Generate external definition map file for the current source. """ |
||
628 | |||
629 | args = opts['direct_args'] + opts['flags'] |
||
630 | extdefmap_command = [opts['ctu'].extdef_map_cmd] |
||
631 | extdefmap_command.append(opts['file']) |
||
632 | extdefmap_command.append('--') |
||
633 | extdefmap_command.extend(args) |
||
634 | logging.debug("Generating external definition map using '%s'", |
||
635 | extdefmap_command) |
||
636 | extdef_src_list = run_command(extdefmap_command, cwd=opts['directory']) |
||
637 | extdef_ast_list = extdef_map_list_src_to_ast(extdef_src_list) |
||
638 | extern_defs_map_folder = os.path.join(opts['ctu'].dir, triple_arch, |
||
639 | CTU_TEMP_DEFMAP_FOLDER) |
||
640 | if not os.path.isdir(extern_defs_map_folder): |
||
641 | try: |
||
642 | os.makedirs(extern_defs_map_folder) |
||
643 | except OSError: |
||
644 | # In case an other process already created it. |
||
645 | pass |
||
646 | if extdef_ast_list: |
||
647 | with tempfile.NamedTemporaryFile(mode='w', |
||
648 | dir=extern_defs_map_folder, |
||
649 | delete=False) as out_file: |
||
650 | out_file.write("\n".join(extdef_ast_list) + "\n") |
||
651 | |||
652 | cwd = opts['directory'] |
||
653 | cmd = [opts['clang'], '--analyze'] + opts['direct_args'] + opts['flags'] \ |
||
654 | + [opts['file']] |
||
655 | triple_arch = get_triple_arch(cmd, cwd) |
||
656 | generate_ast(triple_arch) |
||
657 | map_extdefs(triple_arch) |
||
658 | |||
659 | |||
660 | @require(['ctu']) |
||
661 | def dispatch_ctu(opts, continuation=run_analyzer): |
||
662 | """ Execute only one phase of 2 phases of CTU if needed. """ |
||
663 | |||
664 | ctu_config = opts['ctu'] |
||
665 | |||
666 | if ctu_config.collect or ctu_config.analyze: |
||
667 | assert ctu_config.collect != ctu_config.analyze |
||
668 | if ctu_config.collect: |
||
669 | return ctu_collect_phase(opts) |
||
670 | if ctu_config.analyze: |
||
671 | cwd = opts['directory'] |
||
672 | cmd = [opts['clang'], '--analyze'] + opts['direct_args'] \ |
||
673 | + opts['flags'] + [opts['file']] |
||
674 | triarch = get_triple_arch(cmd, cwd) |
||
675 | ctu_options = ['ctu-dir=' + os.path.join(ctu_config.dir, triarch), |
||
676 | 'experimental-enable-naive-ctu-analysis=true'] |
||
677 | analyzer_options = prefix_with('-analyzer-config', ctu_options) |
||
678 | direct_options = prefix_with('-Xanalyzer', analyzer_options) |
||
679 | opts['direct_args'].extend(direct_options) |
||
680 | |||
681 | return continuation(opts) |
||
682 | |||
683 | |||
684 | @require(['flags', 'force_debug']) |
||
685 | def filter_debug_flags(opts, continuation=dispatch_ctu): |
||
686 | """ Filter out nondebug macros when requested. """ |
||
687 | |||
688 | if opts.pop('force_debug'): |
||
689 | # lazy implementation just append an undefine macro at the end |
||
690 | opts.update({'flags': opts['flags'] + ['-UNDEBUG']}) |
||
691 | |||
692 | return continuation(opts) |
||
693 | |||
694 | |||
695 | @require(['language', 'compiler', 'file', 'flags']) |
||
696 | def language_check(opts, continuation=filter_debug_flags): |
||
697 | """ Find out the language from command line parameters or file name |
||
698 | extension. The decision also influenced by the compiler invocation. """ |
||
699 | |||
700 | accepted = frozenset({ |
||
701 | 'c', 'c++', 'objective-c', 'objective-c++', 'c-cpp-output', |
||
702 | 'c++-cpp-output', 'objective-c-cpp-output' |
||
703 | }) |
||
704 | |||
705 | # language can be given as a parameter... |
||
706 | language = opts.pop('language') |
||
707 | compiler = opts.pop('compiler') |
||
708 | # ... or find out from source file extension |
||
709 | if language is None and compiler is not None: |
||
710 | language = classify_source(opts['file'], compiler == 'c') |
||
711 | |||
712 | if language is None: |
||
713 | logging.debug('skip analysis, language not known') |
||
714 | return None |
||
715 | elif language not in accepted: |
||
716 | logging.debug('skip analysis, language not supported') |
||
717 | return None |
||
718 | else: |
||
719 | logging.debug('analysis, language: %s', language) |
||
720 | opts.update({'language': language, |
||
721 | 'flags': ['-x', language] + opts['flags']}) |
||
722 | return continuation(opts) |
||
723 | |||
724 | |||
725 | @require(['arch_list', 'flags']) |
||
726 | def arch_check(opts, continuation=language_check): |
||
727 | """ Do run analyzer through one of the given architectures. """ |
||
728 | |||
729 | disabled = frozenset({'ppc', 'ppc64'}) |
||
730 | |||
731 | received_list = opts.pop('arch_list') |
||
732 | if received_list: |
||
733 | # filter out disabled architectures and -arch switches |
||
734 | filtered_list = [a for a in received_list if a not in disabled] |
||
735 | if filtered_list: |
||
736 | # There should be only one arch given (or the same multiple |
||
737 | # times). If there are multiple arch are given and are not |
||
738 | # the same, those should not change the pre-processing step. |
||
739 | # But that's the only pass we have before run the analyzer. |
||
740 | current = filtered_list.pop() |
||
741 | logging.debug('analysis, on arch: %s', current) |
||
742 | |||
743 | opts.update({'flags': ['-arch', current] + opts['flags']}) |
||
744 | return continuation(opts) |
||
745 | else: |
||
746 | logging.debug('skip analysis, found not supported arch') |
||
747 | return None |
||
748 | else: |
||
749 | logging.debug('analysis, on default arch') |
||
750 | return continuation(opts) |
||
751 | |||
752 | |||
753 | # To have good results from static analyzer certain compiler options shall be |
||
754 | # omitted. The compiler flag filtering only affects the static analyzer run. |
||
755 | # |
||
756 | # Keys are the option name, value number of options to skip |
||
757 | IGNORED_FLAGS = { |
||
758 | '-c': 0, # compile option will be overwritten |
||
759 | '-fsyntax-only': 0, # static analyzer option will be overwritten |
||
760 | '-o': 1, # will set up own output file |
||
761 | # flags below are inherited from the perl implementation. |
||
762 | '-g': 0, |
||
763 | '-save-temps': 0, |
||
764 | '-install_name': 1, |
||
765 | '-exported_symbols_list': 1, |
||
766 | '-current_version': 1, |
||
767 | '-compatibility_version': 1, |
||
768 | '-init': 1, |
||
769 | '-e': 1, |
||
770 | '-seg1addr': 1, |
||
771 | '-bundle_loader': 1, |
||
772 | '-multiply_defined': 1, |
||
773 | '-sectorder': 3, |
||
774 | '--param': 1, |
||
775 | '--serialize-diagnostics': 1 |
||
776 | } |
||
777 | |||
778 | |||
779 | def classify_parameters(command): |
||
780 | """ Prepare compiler flags (filters some and add others) and take out |
||
781 | language (-x) and architecture (-arch) flags for future processing. """ |
||
782 | |||
783 | result = { |
||
784 | 'flags': [], # the filtered compiler flags |
||
785 | 'arch_list': [], # list of architecture flags |
||
786 | 'language': None, # compilation language, None, if not specified |
||
787 | 'compiler': compiler_language(command) # 'c' or 'c++' |
||
788 | } |
||
789 | |||
790 | # iterate on the compile options |
||
791 | args = iter(command[1:]) |
||
792 | for arg in args: |
||
793 | # take arch flags into a separate basket |
||
794 | if arg == '-arch': |
||
795 | result['arch_list'].append(next(args)) |
||
796 | # take language |
||
797 | elif arg == '-x': |
||
798 | result['language'] = next(args) |
||
799 | # parameters which looks source file are not flags |
||
800 | elif re.match(r'^[^-].+', arg) and classify_source(arg): |
||
801 | pass |
||
802 | # ignore some flags |
||
803 | elif arg in IGNORED_FLAGS: |
||
804 | count = IGNORED_FLAGS[arg] |
||
805 | for _ in range(count): |
||
806 | next(args) |
||
807 | # we don't care about extra warnings, but we should suppress ones |
||
808 | # that we don't want to see. |
||
809 | elif re.match(r'^-W.+', arg) and not re.match(r'^-Wno-.+', arg): |
||
810 | pass |
||
811 | # and consider everything else as compilation flag. |
||
812 | else: |
||
813 | result['flags'].append(arg) |
||
814 | |||
815 | return result |