Laszlo Nagy | 908ed4c | 2017-03-08 21:22:32 +0000 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | # The LLVM Compiler Infrastructure |
| 3 | # |
| 4 | # This file is distributed under the University of Illinois Open Source |
| 5 | # License. See LICENSE.TXT for details. |
| 6 | """ This module parses and validates arguments for command-line interfaces. |
| 7 | |
| 8 | It uses argparse module to create the command line parser. (This library is |
| 9 | in the standard python library since 3.2 and backported to 2.7, but not |
| 10 | earlier.) |
| 11 | |
| 12 | It also implements basic validation methods, related to the command. |
| 13 | Validations are mostly calling specific help methods, or mangling values. |
| 14 | """ |
| 15 | |
| 16 | import os |
| 17 | import sys |
| 18 | import argparse |
| 19 | import logging |
Laszlo Nagy | 0d9be63 | 2017-03-20 09:03:24 +0000 | [diff] [blame^] | 20 | import tempfile |
| 21 | from libscanbuild import reconfigure_logging |
Laszlo Nagy | 908ed4c | 2017-03-08 21:22:32 +0000 | [diff] [blame] | 22 | from libscanbuild.clang import get_checkers |
| 23 | |
| 24 | __all__ = ['parse_args_for_intercept_build', 'parse_args_for_analyze_build', |
| 25 | 'parse_args_for_scan_build'] |
| 26 | |
| 27 | |
| 28 | def parse_args_for_intercept_build(): |
| 29 | """ Parse and validate command-line arguments for intercept-build. """ |
| 30 | |
| 31 | parser = create_intercept_parser() |
| 32 | args = parser.parse_args() |
| 33 | |
| 34 | reconfigure_logging(args.verbose) |
| 35 | logging.debug('Raw arguments %s', sys.argv) |
| 36 | |
| 37 | # short validation logic |
| 38 | if not args.build: |
| 39 | parser.error(message='missing build command') |
| 40 | |
| 41 | logging.debug('Parsed arguments: %s', args) |
| 42 | return args |
| 43 | |
| 44 | |
| 45 | def parse_args_for_analyze_build(): |
| 46 | """ Parse and validate command-line arguments for analyze-build. """ |
| 47 | |
| 48 | from_build_command = False |
| 49 | parser = create_analyze_parser(from_build_command) |
| 50 | args = parser.parse_args() |
| 51 | |
| 52 | reconfigure_logging(args.verbose) |
| 53 | logging.debug('Raw arguments %s', sys.argv) |
| 54 | |
| 55 | normalize_args_for_analyze(args, from_build_command) |
| 56 | validate_args_for_analyze(parser, args, from_build_command) |
| 57 | logging.debug('Parsed arguments: %s', args) |
| 58 | return args |
| 59 | |
| 60 | |
| 61 | def parse_args_for_scan_build(): |
| 62 | """ Parse and validate command-line arguments for scan-build. """ |
| 63 | |
| 64 | from_build_command = True |
| 65 | parser = create_analyze_parser(from_build_command) |
| 66 | args = parser.parse_args() |
| 67 | |
| 68 | reconfigure_logging(args.verbose) |
| 69 | logging.debug('Raw arguments %s', sys.argv) |
| 70 | |
| 71 | normalize_args_for_analyze(args, from_build_command) |
| 72 | validate_args_for_analyze(parser, args, from_build_command) |
| 73 | logging.debug('Parsed arguments: %s', args) |
| 74 | return args |
| 75 | |
| 76 | |
| 77 | def normalize_args_for_analyze(args, from_build_command): |
| 78 | """ Normalize parsed arguments for analyze-build and scan-build. |
| 79 | |
| 80 | :param args: Parsed argument object. (Will be mutated.) |
| 81 | :param from_build_command: Boolean value tells is the command suppose |
| 82 | to run the analyzer against a build command or a compilation db. """ |
| 83 | |
| 84 | # make plugins always a list. (it might be None when not specified.) |
| 85 | if args.plugins is None: |
| 86 | args.plugins = [] |
| 87 | |
| 88 | # make exclude directory list unique and absolute. |
| 89 | uniq_excludes = set(os.path.abspath(entry) for entry in args.excludes) |
| 90 | args.excludes = list(uniq_excludes) |
| 91 | |
| 92 | # because shared codes for all tools, some common used methods are |
| 93 | # expecting some argument to be present. so, instead of query the args |
| 94 | # object about the presence of the flag, we fake it here. to make those |
| 95 | # methods more readable. (it's an arguable choice, took it only for those |
| 96 | # which have good default value.) |
| 97 | if from_build_command: |
| 98 | # add cdb parameter invisibly to make report module working. |
| 99 | args.cdb = 'compile_commands.json' |
| 100 | |
| 101 | |
| 102 | def validate_args_for_analyze(parser, args, from_build_command): |
| 103 | """ Command line parsing is done by the argparse module, but semantic |
| 104 | validation still needs to be done. This method is doing it for |
| 105 | analyze-build and scan-build commands. |
| 106 | |
| 107 | :param parser: The command line parser object. |
| 108 | :param args: Parsed argument object. |
| 109 | :param from_build_command: Boolean value tells is the command suppose |
| 110 | to run the analyzer against a build command or a compilation db. |
| 111 | :return: No return value, but this call might throw when validation |
| 112 | fails. """ |
| 113 | |
| 114 | if args.help_checkers_verbose: |
| 115 | print_checkers(get_checkers(args.clang, args.plugins)) |
| 116 | parser.exit(status=0) |
| 117 | elif args.help_checkers: |
| 118 | print_active_checkers(get_checkers(args.clang, args.plugins)) |
| 119 | parser.exit(status=0) |
| 120 | elif from_build_command and not args.build: |
| 121 | parser.error(message='missing build command') |
| 122 | elif not from_build_command and not os.path.exists(args.cdb): |
| 123 | parser.error(message='compilation database is missing') |
| 124 | |
| 125 | |
| 126 | def create_intercept_parser(): |
| 127 | """ Creates a parser for command-line arguments to 'intercept'. """ |
| 128 | |
| 129 | parser = create_default_parser() |
| 130 | parser_add_cdb(parser) |
| 131 | |
| 132 | parser_add_prefer_wrapper(parser) |
| 133 | parser_add_compilers(parser) |
| 134 | |
| 135 | advanced = parser.add_argument_group('advanced options') |
| 136 | group = advanced.add_mutually_exclusive_group() |
| 137 | group.add_argument( |
| 138 | '--append', |
| 139 | action='store_true', |
| 140 | help="""Extend existing compilation database with new entries. |
| 141 | Duplicate entries are detected and not present in the final output. |
| 142 | The output is not continuously updated, it's done when the build |
| 143 | command finished. """) |
| 144 | |
| 145 | parser.add_argument( |
| 146 | dest='build', nargs=argparse.REMAINDER, help="""Command to run.""") |
| 147 | return parser |
| 148 | |
| 149 | |
| 150 | def create_analyze_parser(from_build_command): |
| 151 | """ Creates a parser for command-line arguments to 'analyze'. """ |
| 152 | |
| 153 | parser = create_default_parser() |
| 154 | |
| 155 | if from_build_command: |
| 156 | parser_add_prefer_wrapper(parser) |
| 157 | parser_add_compilers(parser) |
| 158 | |
| 159 | parser.add_argument( |
| 160 | '--intercept-first', |
| 161 | action='store_true', |
| 162 | help="""Run the build commands first, intercept compiler |
| 163 | calls and then run the static analyzer afterwards. |
| 164 | Generally speaking it has better coverage on build commands. |
| 165 | With '--override-compiler' it use compiler wrapper, but does |
| 166 | not run the analyzer till the build is finished.""") |
| 167 | else: |
| 168 | parser_add_cdb(parser) |
| 169 | |
| 170 | parser.add_argument( |
| 171 | '--status-bugs', |
| 172 | action='store_true', |
| 173 | help="""The exit status of '%(prog)s' is the same as the executed |
| 174 | build command. This option ignores the build exit status and sets to |
| 175 | be non zero if it found potential bugs or zero otherwise.""") |
| 176 | parser.add_argument( |
| 177 | '--exclude', |
| 178 | metavar='<directory>', |
| 179 | dest='excludes', |
| 180 | action='append', |
| 181 | default=[], |
| 182 | help="""Do not run static analyzer against files found in this |
| 183 | directory. (You can specify this option multiple times.) |
| 184 | Could be useful when project contains 3rd party libraries.""") |
| 185 | |
| 186 | output = parser.add_argument_group('output control options') |
| 187 | output.add_argument( |
| 188 | '--output', |
| 189 | '-o', |
| 190 | metavar='<path>', |
Laszlo Nagy | 0d9be63 | 2017-03-20 09:03:24 +0000 | [diff] [blame^] | 191 | default=tempfile.gettempdir(), |
Laszlo Nagy | 908ed4c | 2017-03-08 21:22:32 +0000 | [diff] [blame] | 192 | help="""Specifies the output directory for analyzer reports. |
| 193 | Subdirectory will be created if default directory is targeted.""") |
| 194 | output.add_argument( |
| 195 | '--keep-empty', |
| 196 | action='store_true', |
| 197 | help="""Don't remove the build results directory even if no issues |
| 198 | were reported.""") |
| 199 | output.add_argument( |
| 200 | '--html-title', |
| 201 | metavar='<title>', |
| 202 | help="""Specify the title used on generated HTML pages. |
| 203 | If not specified, a default title will be used.""") |
| 204 | format_group = output.add_mutually_exclusive_group() |
| 205 | format_group.add_argument( |
| 206 | '--plist', |
| 207 | '-plist', |
| 208 | dest='output_format', |
| 209 | const='plist', |
| 210 | default='html', |
| 211 | action='store_const', |
| 212 | help="""Cause the results as a set of .plist files.""") |
| 213 | format_group.add_argument( |
| 214 | '--plist-html', |
| 215 | '-plist-html', |
| 216 | dest='output_format', |
| 217 | const='plist-html', |
| 218 | default='html', |
| 219 | action='store_const', |
| 220 | help="""Cause the results as a set of .html and .plist files.""") |
| 221 | # TODO: implement '-view ' |
| 222 | |
| 223 | advanced = parser.add_argument_group('advanced options') |
| 224 | advanced.add_argument( |
| 225 | '--use-analyzer', |
| 226 | metavar='<path>', |
| 227 | dest='clang', |
| 228 | default='clang', |
| 229 | help="""'%(prog)s' uses the 'clang' executable relative to itself for |
| 230 | static analysis. One can override this behavior with this option by |
| 231 | using the 'clang' packaged with Xcode (on OS X) or from the PATH.""") |
| 232 | advanced.add_argument( |
| 233 | '--no-failure-reports', |
| 234 | '-no-failure-reports', |
| 235 | dest='output_failures', |
| 236 | action='store_false', |
| 237 | help="""Do not create a 'failures' subdirectory that includes analyzer |
| 238 | crash reports and preprocessed source files.""") |
| 239 | parser.add_argument( |
| 240 | '--analyze-headers', |
| 241 | action='store_true', |
| 242 | help="""Also analyze functions in #included files. By default, such |
| 243 | functions are skipped unless they are called by functions within the |
| 244 | main source file.""") |
| 245 | advanced.add_argument( |
| 246 | '--stats', |
| 247 | '-stats', |
| 248 | action='store_true', |
| 249 | help="""Generates visitation statistics for the project.""") |
| 250 | advanced.add_argument( |
| 251 | '--internal-stats', |
| 252 | action='store_true', |
| 253 | help="""Generate internal analyzer statistics.""") |
| 254 | advanced.add_argument( |
| 255 | '--maxloop', |
| 256 | '-maxloop', |
| 257 | metavar='<loop count>', |
| 258 | type=int, |
| 259 | help="""Specifiy the number of times a block can be visited before |
| 260 | giving up. Increase for more comprehensive coverage at a cost of |
| 261 | speed.""") |
| 262 | advanced.add_argument( |
| 263 | '--store', |
| 264 | '-store', |
| 265 | metavar='<model>', |
| 266 | dest='store_model', |
| 267 | choices=['region', 'basic'], |
| 268 | help="""Specify the store model used by the analyzer. 'region' |
| 269 | specifies a field- sensitive store model. 'basic' which is far less |
| 270 | precise but can more quickly analyze code. 'basic' was the default |
| 271 | store model for checker-0.221 and earlier.""") |
| 272 | advanced.add_argument( |
| 273 | '--constraints', |
| 274 | '-constraints', |
| 275 | metavar='<model>', |
| 276 | dest='constraints_model', |
| 277 | choices=['range', 'basic'], |
| 278 | help="""Specify the constraint engine used by the analyzer. Specifying |
| 279 | 'basic' uses a simpler, less powerful constraint model used by |
| 280 | checker-0.160 and earlier.""") |
| 281 | advanced.add_argument( |
| 282 | '--analyzer-config', |
| 283 | '-analyzer-config', |
| 284 | metavar='<options>', |
| 285 | help="""Provide options to pass through to the analyzer's |
| 286 | -analyzer-config flag. Several options are separated with comma: |
| 287 | 'key1=val1,key2=val2' |
| 288 | |
| 289 | Available options: |
| 290 | stable-report-filename=true or false (default) |
| 291 | |
| 292 | Switch the page naming to: |
| 293 | report-<filename>-<function/method name>-<id>.html |
| 294 | instead of report-XXXXXX.html""") |
| 295 | advanced.add_argument( |
| 296 | '--force-analyze-debug-code', |
| 297 | dest='force_debug', |
| 298 | action='store_true', |
| 299 | help="""Tells analyzer to enable assertions in code even if they were |
| 300 | disabled during compilation, enabling more precise results.""") |
| 301 | |
| 302 | plugins = parser.add_argument_group('checker options') |
| 303 | plugins.add_argument( |
| 304 | '--load-plugin', |
| 305 | '-load-plugin', |
| 306 | metavar='<plugin library>', |
| 307 | dest='plugins', |
| 308 | action='append', |
| 309 | help="""Loading external checkers using the clang plugin interface.""") |
| 310 | plugins.add_argument( |
| 311 | '--enable-checker', |
| 312 | '-enable-checker', |
| 313 | metavar='<checker name>', |
| 314 | action=AppendCommaSeparated, |
| 315 | help="""Enable specific checker.""") |
| 316 | plugins.add_argument( |
| 317 | '--disable-checker', |
| 318 | '-disable-checker', |
| 319 | metavar='<checker name>', |
| 320 | action=AppendCommaSeparated, |
| 321 | help="""Disable specific checker.""") |
| 322 | plugins.add_argument( |
| 323 | '--help-checkers', |
| 324 | action='store_true', |
| 325 | help="""A default group of checkers is run unless explicitly disabled. |
| 326 | Exactly which checkers constitute the default group is a function of |
| 327 | the operating system in use. These can be printed with this flag.""") |
| 328 | plugins.add_argument( |
| 329 | '--help-checkers-verbose', |
| 330 | action='store_true', |
| 331 | help="""Print all available checkers and mark the enabled ones.""") |
| 332 | |
| 333 | if from_build_command: |
| 334 | parser.add_argument( |
| 335 | dest='build', nargs=argparse.REMAINDER, help="""Command to run.""") |
| 336 | return parser |
| 337 | |
| 338 | |
| 339 | def create_default_parser(): |
| 340 | """ Creates command line parser for all build wrapper commands. """ |
| 341 | |
| 342 | parser = argparse.ArgumentParser( |
| 343 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 344 | |
| 345 | parser.add_argument( |
| 346 | '--verbose', |
| 347 | '-v', |
| 348 | action='count', |
| 349 | default=0, |
| 350 | help="""Enable verbose output from '%(prog)s'. A second, third and |
| 351 | fourth flags increases verbosity.""") |
| 352 | return parser |
| 353 | |
| 354 | |
| 355 | def parser_add_cdb(parser): |
| 356 | parser.add_argument( |
| 357 | '--cdb', |
| 358 | metavar='<file>', |
| 359 | default="compile_commands.json", |
| 360 | help="""The JSON compilation database.""") |
| 361 | |
| 362 | |
| 363 | def parser_add_prefer_wrapper(parser): |
| 364 | parser.add_argument( |
| 365 | '--override-compiler', |
| 366 | action='store_true', |
| 367 | help="""Always resort to the compiler wrapper even when better |
| 368 | intercept methods are available.""") |
| 369 | |
| 370 | |
| 371 | def parser_add_compilers(parser): |
| 372 | parser.add_argument( |
| 373 | '--use-cc', |
| 374 | metavar='<path>', |
| 375 | dest='cc', |
| 376 | default=os.getenv('CC', 'cc'), |
| 377 | help="""When '%(prog)s' analyzes a project by interposing a compiler |
| 378 | wrapper, which executes a real compiler for compilation and do other |
| 379 | tasks (record the compiler invocation). Because of this interposing, |
| 380 | '%(prog)s' does not know what compiler your project normally uses. |
| 381 | Instead, it simply overrides the CC environment variable, and guesses |
| 382 | your default compiler. |
| 383 | |
| 384 | If you need '%(prog)s' to use a specific compiler for *compilation* |
| 385 | then you can use this option to specify a path to that compiler.""") |
| 386 | parser.add_argument( |
| 387 | '--use-c++', |
| 388 | metavar='<path>', |
| 389 | dest='cxx', |
| 390 | default=os.getenv('CXX', 'c++'), |
| 391 | help="""This is the same as "--use-cc" but for C++ code.""") |
| 392 | |
| 393 | |
| 394 | class AppendCommaSeparated(argparse.Action): |
| 395 | """ argparse Action class to support multiple comma separated lists. """ |
| 396 | |
| 397 | def __call__(self, __parser, namespace, values, __option_string): |
| 398 | # getattr(obj, attr, default) does not really returns default but none |
| 399 | if getattr(namespace, self.dest, None) is None: |
| 400 | setattr(namespace, self.dest, []) |
| 401 | # once it's fixed we can use as expected |
| 402 | actual = getattr(namespace, self.dest) |
| 403 | actual.extend(values.split(',')) |
| 404 | setattr(namespace, self.dest, actual) |
| 405 | |
| 406 | |
| 407 | def print_active_checkers(checkers): |
| 408 | """ Print active checkers to stdout. """ |
| 409 | |
| 410 | for name in sorted(name for name, (_, active) in checkers.items() |
| 411 | if active): |
| 412 | print(name) |
| 413 | |
| 414 | |
| 415 | def print_checkers(checkers): |
| 416 | """ Print verbose checker help to stdout. """ |
| 417 | |
| 418 | print('') |
| 419 | print('available checkers:') |
| 420 | print('') |
| 421 | for name in sorted(checkers.keys()): |
| 422 | description, active = checkers[name] |
| 423 | prefix = '+' if active else ' ' |
| 424 | if len(name) > 30: |
| 425 | print(' {0} {1}'.format(prefix, name)) |
| 426 | print(' ' * 35 + description) |
| 427 | else: |
| 428 | print(' {0} {1: <30} {2}'.format(prefix, name, description)) |
| 429 | print('') |
| 430 | print('NOTE: "+" indicates that an analysis is enabled by default.') |
| 431 | print('') |