forked from OSchip/llvm-project
				
			
		
			
				
	
	
		
			502 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			502 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Python
		
	
	
	
# -*- coding: utf-8 -*-
 | 
						|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 | 
						|
# See https://llvm.org/LICENSE.txt for license information.
 | 
						|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 | 
						|
""" This module parses and validates arguments for command-line interfaces.
 | 
						|
 | 
						|
It uses argparse module to create the command line parser. (This library is
 | 
						|
in the standard python library since 3.2 and backported to 2.7, but not
 | 
						|
earlier.)
 | 
						|
 | 
						|
It also implements basic validation methods, related to the command.
 | 
						|
Validations are mostly calling specific help methods, or mangling values.
 | 
						|
"""
 | 
						|
from __future__ import absolute_import, division, print_function
 | 
						|
 | 
						|
import os
 | 
						|
import sys
 | 
						|
import argparse
 | 
						|
import logging
 | 
						|
import tempfile
 | 
						|
from libscanbuild import reconfigure_logging, CtuConfig
 | 
						|
from libscanbuild.clang import get_checkers, is_ctu_capable
 | 
						|
 | 
						|
__all__ = ['parse_args_for_intercept_build', 'parse_args_for_analyze_build',
 | 
						|
           'parse_args_for_scan_build']
 | 
						|
 | 
						|
 | 
						|
def parse_args_for_intercept_build():
 | 
						|
    """ Parse and validate command-line arguments for intercept-build. """
 | 
						|
 | 
						|
    parser = create_intercept_parser()
 | 
						|
    args = parser.parse_args()
 | 
						|
 | 
						|
    reconfigure_logging(args.verbose)
 | 
						|
    logging.debug('Raw arguments %s', sys.argv)
 | 
						|
 | 
						|
    # short validation logic
 | 
						|
    if not args.build:
 | 
						|
        parser.error(message='missing build command')
 | 
						|
 | 
						|
    logging.debug('Parsed arguments: %s', args)
 | 
						|
    return args
 | 
						|
 | 
						|
 | 
						|
def parse_args_for_analyze_build():
 | 
						|
    """ Parse and validate command-line arguments for analyze-build. """
 | 
						|
 | 
						|
    from_build_command = False
 | 
						|
    parser = create_analyze_parser(from_build_command)
 | 
						|
    args = parser.parse_args()
 | 
						|
 | 
						|
    reconfigure_logging(args.verbose)
 | 
						|
    logging.debug('Raw arguments %s', sys.argv)
 | 
						|
 | 
						|
    normalize_args_for_analyze(args, from_build_command)
 | 
						|
    validate_args_for_analyze(parser, args, from_build_command)
 | 
						|
    logging.debug('Parsed arguments: %s', args)
 | 
						|
    return args
 | 
						|
 | 
						|
 | 
						|
def parse_args_for_scan_build():
 | 
						|
    """ Parse and validate command-line arguments for scan-build. """
 | 
						|
 | 
						|
    from_build_command = True
 | 
						|
    parser = create_analyze_parser(from_build_command)
 | 
						|
    args = parser.parse_args()
 | 
						|
 | 
						|
    reconfigure_logging(args.verbose)
 | 
						|
    logging.debug('Raw arguments %s', sys.argv)
 | 
						|
 | 
						|
    normalize_args_for_analyze(args, from_build_command)
 | 
						|
    validate_args_for_analyze(parser, args, from_build_command)
 | 
						|
    logging.debug('Parsed arguments: %s', args)
 | 
						|
    return args
 | 
						|
 | 
						|
 | 
						|
def normalize_args_for_analyze(args, from_build_command):
 | 
						|
    """ Normalize parsed arguments for analyze-build and scan-build.
 | 
						|
 | 
						|
    :param args: Parsed argument object. (Will be mutated.)
 | 
						|
    :param from_build_command: Boolean value tells is the command suppose
 | 
						|
    to run the analyzer against a build command or a compilation db. """
 | 
						|
 | 
						|
    # make plugins always a list. (it might be None when not specified.)
 | 
						|
    if args.plugins is None:
 | 
						|
        args.plugins = []
 | 
						|
 | 
						|
    # make exclude directory list unique and absolute.
 | 
						|
    uniq_excludes = set(os.path.abspath(entry) for entry in args.excludes)
 | 
						|
    args.excludes = list(uniq_excludes)
 | 
						|
 | 
						|
    # because shared codes for all tools, some common used methods are
 | 
						|
    # expecting some argument to be present. so, instead of query the args
 | 
						|
    # object about the presence of the flag, we fake it here. to make those
 | 
						|
    # methods more readable. (it's an arguable choice, took it only for those
 | 
						|
    # which have good default value.)
 | 
						|
    if from_build_command:
 | 
						|
        # add cdb parameter invisibly to make report module working.
 | 
						|
        args.cdb = 'compile_commands.json'
 | 
						|
 | 
						|
    # Make ctu_dir an abspath as it is needed inside clang
 | 
						|
    if not from_build_command and hasattr(args, 'ctu_phases') \
 | 
						|
            and hasattr(args.ctu_phases, 'dir'):
 | 
						|
        args.ctu_dir = os.path.abspath(args.ctu_dir)
 | 
						|
 | 
						|
 | 
						|
def validate_args_for_analyze(parser, args, from_build_command):
 | 
						|
    """ Command line parsing is done by the argparse module, but semantic
 | 
						|
    validation still needs to be done. This method is doing it for
 | 
						|
    analyze-build and scan-build commands.
 | 
						|
 | 
						|
    :param parser: The command line parser object.
 | 
						|
    :param args: Parsed argument object.
 | 
						|
    :param from_build_command: Boolean value tells is the command suppose
 | 
						|
    to run the analyzer against a build command or a compilation db.
 | 
						|
    :return: No return value, but this call might throw when validation
 | 
						|
    fails. """
 | 
						|
 | 
						|
    if args.help_checkers_verbose:
 | 
						|
        print_checkers(get_checkers(args.clang, args.plugins))
 | 
						|
        parser.exit(status=0)
 | 
						|
    elif args.help_checkers:
 | 
						|
        print_active_checkers(get_checkers(args.clang, args.plugins))
 | 
						|
        parser.exit(status=0)
 | 
						|
    elif from_build_command and not args.build:
 | 
						|
        parser.error(message='missing build command')
 | 
						|
    elif not from_build_command and not os.path.exists(args.cdb):
 | 
						|
        parser.error(message='compilation database is missing')
 | 
						|
 | 
						|
    # If the user wants CTU mode
 | 
						|
    if not from_build_command and hasattr(args, 'ctu_phases') \
 | 
						|
            and hasattr(args.ctu_phases, 'dir'):
 | 
						|
        # If CTU analyze_only, the input directory should exist
 | 
						|
        if args.ctu_phases.analyze and not args.ctu_phases.collect \
 | 
						|
                and not os.path.exists(args.ctu_dir):
 | 
						|
            parser.error(message='missing CTU directory')
 | 
						|
        # Check CTU capability via checking clang-extdef-mapping
 | 
						|
        if not is_ctu_capable(args.extdef_map_cmd):
 | 
						|
            parser.error(message="""This version of clang does not support CTU
 | 
						|
            functionality or clang-extdef-mapping command not found.""")
 | 
						|
 | 
						|
 | 
						|
def create_intercept_parser():
 | 
						|
    """ Creates a parser for command-line arguments to 'intercept'. """
 | 
						|
 | 
						|
    parser = create_default_parser()
 | 
						|
    parser_add_cdb(parser)
 | 
						|
 | 
						|
    parser_add_prefer_wrapper(parser)
 | 
						|
    parser_add_compilers(parser)
 | 
						|
 | 
						|
    advanced = parser.add_argument_group('advanced options')
 | 
						|
    group = advanced.add_mutually_exclusive_group()
 | 
						|
    group.add_argument(
 | 
						|
        '--append',
 | 
						|
        action='store_true',
 | 
						|
        help="""Extend existing compilation database with new entries.
 | 
						|
        Duplicate entries are detected and not present in the final output.
 | 
						|
        The output is not continuously updated, it's done when the build
 | 
						|
        command finished. """)
 | 
						|
 | 
						|
    parser.add_argument(
 | 
						|
        dest='build', nargs=argparse.REMAINDER, help="""Command to run.""")
 | 
						|
    return parser
 | 
						|
 | 
						|
 | 
						|
def create_analyze_parser(from_build_command):
 | 
						|
    """ Creates a parser for command-line arguments to 'analyze'. """
 | 
						|
 | 
						|
    parser = create_default_parser()
 | 
						|
 | 
						|
    if from_build_command:
 | 
						|
        parser_add_prefer_wrapper(parser)
 | 
						|
        parser_add_compilers(parser)
 | 
						|
 | 
						|
        parser.add_argument(
 | 
						|
            '--intercept-first',
 | 
						|
            action='store_true',
 | 
						|
            help="""Run the build commands first, intercept compiler
 | 
						|
            calls and then run the static analyzer afterwards.
 | 
						|
            Generally speaking it has better coverage on build commands.
 | 
						|
            With '--override-compiler' it use compiler wrapper, but does
 | 
						|
            not run the analyzer till the build is finished.""")
 | 
						|
    else:
 | 
						|
        parser_add_cdb(parser)
 | 
						|
 | 
						|
    parser.add_argument(
 | 
						|
        '--status-bugs',
 | 
						|
        action='store_true',
 | 
						|
        help="""The exit status of '%(prog)s' is the same as the executed
 | 
						|
        build command. This option ignores the build exit status and sets to
 | 
						|
        be non zero if it found potential bugs or zero otherwise.""")
 | 
						|
    parser.add_argument(
 | 
						|
        '--exclude',
 | 
						|
        metavar='<directory>',
 | 
						|
        dest='excludes',
 | 
						|
        action='append',
 | 
						|
        default=[],
 | 
						|
        help="""Do not run static analyzer against files found in this
 | 
						|
        directory. (You can specify this option multiple times.)
 | 
						|
        Could be useful when project contains 3rd party libraries.""")
 | 
						|
 | 
						|
    output = parser.add_argument_group('output control options')
 | 
						|
    output.add_argument(
 | 
						|
        '--output',
 | 
						|
        '-o',
 | 
						|
        metavar='<path>',
 | 
						|
        default=tempfile.gettempdir(),
 | 
						|
        help="""Specifies the output directory for analyzer reports.
 | 
						|
        Subdirectory will be created if default directory is targeted.""")
 | 
						|
    output.add_argument(
 | 
						|
        '--keep-empty',
 | 
						|
        action='store_true',
 | 
						|
        help="""Don't remove the build results directory even if no issues
 | 
						|
        were reported.""")
 | 
						|
    output.add_argument(
 | 
						|
        '--html-title',
 | 
						|
        metavar='<title>',
 | 
						|
        help="""Specify the title used on generated HTML pages.
 | 
						|
        If not specified, a default title will be used.""")
 | 
						|
    format_group = output.add_mutually_exclusive_group()
 | 
						|
    format_group.add_argument(
 | 
						|
        '--plist',
 | 
						|
        '-plist',
 | 
						|
        dest='output_format',
 | 
						|
        const='plist',
 | 
						|
        default='html',
 | 
						|
        action='store_const',
 | 
						|
        help="""Cause the results as a set of .plist files.""")
 | 
						|
    format_group.add_argument(
 | 
						|
        '--plist-html',
 | 
						|
        '-plist-html',
 | 
						|
        dest='output_format',
 | 
						|
        const='plist-html',
 | 
						|
        default='html',
 | 
						|
        action='store_const',
 | 
						|
        help="""Cause the results as a set of .html and .plist files.""")
 | 
						|
    format_group.add_argument(
 | 
						|
        '--plist-multi-file',
 | 
						|
        '-plist-multi-file',
 | 
						|
        dest='output_format',
 | 
						|
        const='plist-multi-file',
 | 
						|
        default='html',
 | 
						|
        action='store_const',
 | 
						|
        help="""Cause the results as a set of .plist files with extra
 | 
						|
        information on related files.""")
 | 
						|
 | 
						|
    advanced = parser.add_argument_group('advanced options')
 | 
						|
    advanced.add_argument(
 | 
						|
        '--use-analyzer',
 | 
						|
        metavar='<path>',
 | 
						|
        dest='clang',
 | 
						|
        default='clang',
 | 
						|
        help="""'%(prog)s' uses the 'clang' executable relative to itself for
 | 
						|
        static analysis. One can override this behavior with this option by
 | 
						|
        using the 'clang' packaged with Xcode (on OS X) or from the PATH.""")
 | 
						|
    advanced.add_argument(
 | 
						|
        '--no-failure-reports',
 | 
						|
        '-no-failure-reports',
 | 
						|
        dest='output_failures',
 | 
						|
        action='store_false',
 | 
						|
        help="""Do not create a 'failures' subdirectory that includes analyzer
 | 
						|
        crash reports and preprocessed source files.""")
 | 
						|
    parser.add_argument(
 | 
						|
        '--analyze-headers',
 | 
						|
        action='store_true',
 | 
						|
        help="""Also analyze functions in #included files. By default, such
 | 
						|
        functions are skipped unless they are called by functions within the
 | 
						|
        main source file.""")
 | 
						|
    advanced.add_argument(
 | 
						|
        '--stats',
 | 
						|
        '-stats',
 | 
						|
        action='store_true',
 | 
						|
        help="""Generates visitation statistics for the project.""")
 | 
						|
    advanced.add_argument(
 | 
						|
        '--internal-stats',
 | 
						|
        action='store_true',
 | 
						|
        help="""Generate internal analyzer statistics.""")
 | 
						|
    advanced.add_argument(
 | 
						|
        '--maxloop',
 | 
						|
        '-maxloop',
 | 
						|
        metavar='<loop count>',
 | 
						|
        type=int,
 | 
						|
        help="""Specify the number of times a block can be visited before
 | 
						|
        giving up. Increase for more comprehensive coverage at a cost of
 | 
						|
        speed.""")
 | 
						|
    advanced.add_argument(
 | 
						|
        '--store',
 | 
						|
        '-store',
 | 
						|
        metavar='<model>',
 | 
						|
        dest='store_model',
 | 
						|
        choices=['region', 'basic'],
 | 
						|
        help="""Specify the store model used by the analyzer. 'region'
 | 
						|
        specifies a field- sensitive store model. 'basic' which is far less
 | 
						|
        precise but can more quickly analyze code. 'basic' was the default
 | 
						|
        store model for checker-0.221 and earlier.""")
 | 
						|
    advanced.add_argument(
 | 
						|
        '--constraints',
 | 
						|
        '-constraints',
 | 
						|
        metavar='<model>',
 | 
						|
        dest='constraints_model',
 | 
						|
        choices=['range', 'basic'],
 | 
						|
        help="""Specify the constraint engine used by the analyzer. Specifying
 | 
						|
        'basic' uses a simpler, less powerful constraint model used by
 | 
						|
        checker-0.160 and earlier.""")
 | 
						|
    advanced.add_argument(
 | 
						|
        '--analyzer-config',
 | 
						|
        '-analyzer-config',
 | 
						|
        metavar='<options>',
 | 
						|
        help="""Provide options to pass through to the analyzer's
 | 
						|
        -analyzer-config flag. Several options are separated with comma:
 | 
						|
        'key1=val1,key2=val2'
 | 
						|
 | 
						|
        Available options:
 | 
						|
            stable-report-filename=true or false (default)
 | 
						|
 | 
						|
        Switch the page naming to:
 | 
						|
        report-<filename>-<function/method name>-<id>.html
 | 
						|
        instead of report-XXXXXX.html""")
 | 
						|
    advanced.add_argument(
 | 
						|
        '--force-analyze-debug-code',
 | 
						|
        dest='force_debug',
 | 
						|
        action='store_true',
 | 
						|
        help="""Tells analyzer to enable assertions in code even if they were
 | 
						|
        disabled during compilation, enabling more precise results.""")
 | 
						|
 | 
						|
    plugins = parser.add_argument_group('checker options')
 | 
						|
    plugins.add_argument(
 | 
						|
        '--load-plugin',
 | 
						|
        '-load-plugin',
 | 
						|
        metavar='<plugin library>',
 | 
						|
        dest='plugins',
 | 
						|
        action='append',
 | 
						|
        help="""Loading external checkers using the clang plugin interface.""")
 | 
						|
    plugins.add_argument(
 | 
						|
        '--enable-checker',
 | 
						|
        '-enable-checker',
 | 
						|
        metavar='<checker name>',
 | 
						|
        action=AppendCommaSeparated,
 | 
						|
        help="""Enable specific checker.""")
 | 
						|
    plugins.add_argument(
 | 
						|
        '--disable-checker',
 | 
						|
        '-disable-checker',
 | 
						|
        metavar='<checker name>',
 | 
						|
        action=AppendCommaSeparated,
 | 
						|
        help="""Disable specific checker.""")
 | 
						|
    plugins.add_argument(
 | 
						|
        '--help-checkers',
 | 
						|
        action='store_true',
 | 
						|
        help="""A default group of checkers is run unless explicitly disabled.
 | 
						|
        Exactly which checkers constitute the default group is a function of
 | 
						|
        the operating system in use. These can be printed with this flag.""")
 | 
						|
    plugins.add_argument(
 | 
						|
        '--help-checkers-verbose',
 | 
						|
        action='store_true',
 | 
						|
        help="""Print all available checkers and mark the enabled ones.""")
 | 
						|
 | 
						|
    if from_build_command:
 | 
						|
        parser.add_argument(
 | 
						|
            dest='build', nargs=argparse.REMAINDER, help="""Command to run.""")
 | 
						|
    else:
 | 
						|
        ctu = parser.add_argument_group('cross translation unit analysis')
 | 
						|
        ctu_mutex_group = ctu.add_mutually_exclusive_group()
 | 
						|
        ctu_mutex_group.add_argument(
 | 
						|
            '--ctu',
 | 
						|
            action='store_const',
 | 
						|
            const=CtuConfig(collect=True, analyze=True,
 | 
						|
                            dir='', extdef_map_cmd=''),
 | 
						|
            dest='ctu_phases',
 | 
						|
            help="""Perform cross translation unit (ctu) analysis (both collect
 | 
						|
            and analyze phases) using default <ctu-dir> for temporary output.
 | 
						|
            At the end of the analysis, the temporary directory is removed.""")
 | 
						|
        ctu.add_argument(
 | 
						|
            '--ctu-dir',
 | 
						|
            metavar='<ctu-dir>',
 | 
						|
            dest='ctu_dir',
 | 
						|
            default='ctu-dir',
 | 
						|
            help="""Defines the temporary directory used between ctu
 | 
						|
            phases.""")
 | 
						|
        ctu_mutex_group.add_argument(
 | 
						|
            '--ctu-collect-only',
 | 
						|
            action='store_const',
 | 
						|
            const=CtuConfig(collect=True, analyze=False,
 | 
						|
                            dir='', extdef_map_cmd=''),
 | 
						|
            dest='ctu_phases',
 | 
						|
            help="""Perform only the collect phase of ctu.
 | 
						|
            Keep <ctu-dir> for further use.""")
 | 
						|
        ctu_mutex_group.add_argument(
 | 
						|
            '--ctu-analyze-only',
 | 
						|
            action='store_const',
 | 
						|
            const=CtuConfig(collect=False, analyze=True,
 | 
						|
                            dir='', extdef_map_cmd=''),
 | 
						|
            dest='ctu_phases',
 | 
						|
            help="""Perform only the analyze phase of ctu. <ctu-dir> should be
 | 
						|
            present and will not be removed after analysis.""")
 | 
						|
        ctu.add_argument(
 | 
						|
            '--use-extdef-map-cmd',
 | 
						|
            metavar='<path>',
 | 
						|
            dest='extdef_map_cmd',
 | 
						|
            default='clang-extdef-mapping',
 | 
						|
            help="""'%(prog)s' uses the 'clang-extdef-mapping' executable
 | 
						|
            relative to itself for generating external definition maps for
 | 
						|
            static analysis. One can override this behavior with this option
 | 
						|
            by using the 'clang-extdef-mapping' packaged with Xcode (on OS X)
 | 
						|
            or from the PATH.""")
 | 
						|
    return parser
 | 
						|
 | 
						|
 | 
						|
def create_default_parser():
 | 
						|
    """ Creates command line parser for all build wrapper commands. """
 | 
						|
 | 
						|
    parser = argparse.ArgumentParser(
 | 
						|
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
 | 
						|
 | 
						|
    parser.add_argument(
 | 
						|
        '--verbose',
 | 
						|
        '-v',
 | 
						|
        action='count',
 | 
						|
        default=0,
 | 
						|
        help="""Enable verbose output from '%(prog)s'. A second, third and
 | 
						|
        fourth flags increases verbosity.""")
 | 
						|
    return parser
 | 
						|
 | 
						|
 | 
						|
def parser_add_cdb(parser):
 | 
						|
    parser.add_argument(
 | 
						|
        '--cdb',
 | 
						|
        metavar='<file>',
 | 
						|
        default="compile_commands.json",
 | 
						|
        help="""The JSON compilation database.""")
 | 
						|
 | 
						|
 | 
						|
def parser_add_prefer_wrapper(parser):
 | 
						|
    parser.add_argument(
 | 
						|
        '--override-compiler',
 | 
						|
        action='store_true',
 | 
						|
        help="""Always resort to the compiler wrapper even when better
 | 
						|
        intercept methods are available.""")
 | 
						|
 | 
						|
 | 
						|
def parser_add_compilers(parser):
 | 
						|
    parser.add_argument(
 | 
						|
        '--use-cc',
 | 
						|
        metavar='<path>',
 | 
						|
        dest='cc',
 | 
						|
        default=os.getenv('CC', 'cc'),
 | 
						|
        help="""When '%(prog)s' analyzes a project by interposing a compiler
 | 
						|
        wrapper, which executes a real compiler for compilation and do other
 | 
						|
        tasks (record the compiler invocation). Because of this interposing,
 | 
						|
        '%(prog)s' does not know what compiler your project normally uses.
 | 
						|
        Instead, it simply overrides the CC environment variable, and guesses
 | 
						|
        your default compiler.
 | 
						|
 | 
						|
        If you need '%(prog)s' to use a specific compiler for *compilation*
 | 
						|
        then you can use this option to specify a path to that compiler.""")
 | 
						|
    parser.add_argument(
 | 
						|
        '--use-c++',
 | 
						|
        metavar='<path>',
 | 
						|
        dest='cxx',
 | 
						|
        default=os.getenv('CXX', 'c++'),
 | 
						|
        help="""This is the same as "--use-cc" but for C++ code.""")
 | 
						|
 | 
						|
 | 
						|
class AppendCommaSeparated(argparse.Action):
 | 
						|
    """ argparse Action class to support multiple comma separated lists. """
 | 
						|
 | 
						|
    def __call__(self, __parser, namespace, values, __option_string):
 | 
						|
        # getattr(obj, attr, default) does not really returns default but none
 | 
						|
        if getattr(namespace, self.dest, None) is None:
 | 
						|
            setattr(namespace, self.dest, [])
 | 
						|
        # once it's fixed we can use as expected
 | 
						|
        actual = getattr(namespace, self.dest)
 | 
						|
        actual.extend(values.split(','))
 | 
						|
        setattr(namespace, self.dest, actual)
 | 
						|
 | 
						|
 | 
						|
def print_active_checkers(checkers):
 | 
						|
    """ Print active checkers to stdout. """
 | 
						|
 | 
						|
    for name in sorted(name for name, (_, active) in checkers.items()
 | 
						|
                       if active):
 | 
						|
        print(name)
 | 
						|
 | 
						|
 | 
						|
def print_checkers(checkers):
 | 
						|
    """ Print verbose checker help to stdout. """
 | 
						|
 | 
						|
    print('')
 | 
						|
    print('available checkers:')
 | 
						|
    print('')
 | 
						|
    for name in sorted(checkers.keys()):
 | 
						|
        description, active = checkers[name]
 | 
						|
        prefix = '+' if active else ' '
 | 
						|
        if len(name) > 30:
 | 
						|
            print(' {0} {1}'.format(prefix, name))
 | 
						|
            print(' ' * 35 + description)
 | 
						|
        else:
 | 
						|
            print(' {0} {1: <30}  {2}'.format(prefix, name, description))
 | 
						|
    print('')
 | 
						|
    print('NOTE: "+" indicates that an analysis is enabled by default.')
 | 
						|
    print('')
 |