| # -*- 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 | 
 | 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-func-mapping | 
 |         if not is_ctu_capable(args.func_map_cmd): | 
 |             parser.error(message="""This version of clang does not support CTU | 
 |             functionality or clang-func-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='', func_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='', func_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='', func_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-func-map-cmd', | 
 |             metavar='<path>', | 
 |             dest='func_map_cmd', | 
 |             default='clang-func-mapping', | 
 |             help="""'%(prog)s' uses the 'clang-func-mapping' executable | 
 |             relative to itself for generating function maps for static | 
 |             analysis. One can override this behavior with this option by using | 
 |             the 'clang-func-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('') |