| # -*- 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('') |