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
42scanbuild_dir = os.path.dirname(os.path.realpath(__import__('sys').argv[0]))
43
44COMPILER_WRAPPER_CC = os.path.join(scanbuild_dir, '..', 'libexec', 'analyze-cc')
45COMPILER_WRAPPER_CXX = os.path.join(scanbuild_dir, '..', 'libexec', 'analyze-c++')
46
47CTU_EXTDEF_MAP_FILENAME = 'externalDefMap.txt'
48CTU_TEMP_DEFMAP_FOLDER = 'tmpExternalDefMaps'
49
50
51@command_entry_point
52def 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
80def 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
94def 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
107def 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
116def 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
128def 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
139def 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
166def 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
210def 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
248def 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
280def 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
300def 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
306def 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
342def 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
383def 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
418def 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
449def 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'])
476def 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'])
534def 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
582def 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'])
599def 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'])
661def 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'])
685def 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'])
696def 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'])
726def 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
757IGNORED_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
779def 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
816