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