Subversion Repositories QNX 8.QNX8 LLVM/Clang compiler suite

Rev

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