[scan-build-py] move argument parsing into separate module

Forgot to add the new module.


Cr-Mirrored-From: https://chromium.googlesource.com/external/github.com/llvm-mirror/clang
Cr-Mirrored-Commit: 6e475d1bf2b41b8f15c74515f1644c2640aa096b
diff --git a/libscanbuild/arguments.py b/libscanbuild/arguments.py
new file mode 100644
index 0000000..fe5725d
--- /dev/null
+++ b/libscanbuild/arguments.py
@@ -0,0 +1,430 @@
+# -*- coding: utf-8 -*-
+#                     The LLVM Compiler Infrastructure
+#
+# This file is distributed under the University of Illinois Open Source
+# License. See LICENSE.TXT for details.
+""" 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.
+"""
+
+import os
+import sys
+import argparse
+import logging
+from libscanbuild import reconfigure_logging, tempdir
+from libscanbuild.clang import get_checkers
+
+__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'
+
+
+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')
+
+
+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=tempdir(),
+        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.""")
+    # TODO: implement '-view '
+
+    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="""Specifiy 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.""")
+    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('')