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 |
Gabor Horvath | eb0584b | 2018-02-28 13:23:10 +0000 | [diff] [blame^] | 21 | from libscanbuild import reconfigure_logging, CtuConfig |
| 22 | from libscanbuild.clang import get_checkers, is_ctu_capable |
Laszlo Nagy | 908ed4c | 2017-03-08 21:22:32 +0000 | [diff] [blame] | 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 | |
Gabor Horvath | eb0584b | 2018-02-28 13:23:10 +0000 | [diff] [blame^] | 101 | # Make ctu_dir an abspath as it is needed inside clang |
| 102 | if not from_build_command and hasattr(args, 'ctu_phases') \ |
| 103 | and hasattr(args.ctu_phases, 'dir'): |
| 104 | args.ctu_dir = os.path.abspath(args.ctu_dir) |
| 105 | |
Laszlo Nagy | 908ed4c | 2017-03-08 21:22:32 +0000 | [diff] [blame] | 106 | |
| 107 | def validate_args_for_analyze(parser, args, from_build_command): |
| 108 | """ Command line parsing is done by the argparse module, but semantic |
| 109 | validation still needs to be done. This method is doing it for |
| 110 | analyze-build and scan-build commands. |
| 111 | |
| 112 | :param parser: The command line parser object. |
| 113 | :param args: Parsed argument object. |
| 114 | :param from_build_command: Boolean value tells is the command suppose |
| 115 | to run the analyzer against a build command or a compilation db. |
| 116 | :return: No return value, but this call might throw when validation |
| 117 | fails. """ |
| 118 | |
| 119 | if args.help_checkers_verbose: |
| 120 | print_checkers(get_checkers(args.clang, args.plugins)) |
| 121 | parser.exit(status=0) |
| 122 | elif args.help_checkers: |
| 123 | print_active_checkers(get_checkers(args.clang, args.plugins)) |
| 124 | parser.exit(status=0) |
| 125 | elif from_build_command and not args.build: |
| 126 | parser.error(message='missing build command') |
| 127 | elif not from_build_command and not os.path.exists(args.cdb): |
| 128 | parser.error(message='compilation database is missing') |
| 129 | |
Gabor Horvath | eb0584b | 2018-02-28 13:23:10 +0000 | [diff] [blame^] | 130 | # If the user wants CTU mode |
| 131 | if not from_build_command and hasattr(args, 'ctu_phases') \ |
| 132 | and hasattr(args.ctu_phases, 'dir'): |
| 133 | # If CTU analyze_only, the input directory should exist |
| 134 | if args.ctu_phases.analyze and not args.ctu_phases.collect \ |
| 135 | and not os.path.exists(args.ctu_dir): |
| 136 | parser.error(message='missing CTU directory') |
| 137 | # Check CTU capability via checking clang-func-mapping |
| 138 | if not is_ctu_capable(args.func_map_cmd): |
| 139 | parser.error(message="""This version of clang does not support CTU |
| 140 | functionality or clang-func-mapping command not found.""") |
| 141 | |
Laszlo Nagy | 908ed4c | 2017-03-08 21:22:32 +0000 | [diff] [blame] | 142 | |
| 143 | def create_intercept_parser(): |
| 144 | """ Creates a parser for command-line arguments to 'intercept'. """ |
| 145 | |
| 146 | parser = create_default_parser() |
| 147 | parser_add_cdb(parser) |
| 148 | |
| 149 | parser_add_prefer_wrapper(parser) |
| 150 | parser_add_compilers(parser) |
| 151 | |
| 152 | advanced = parser.add_argument_group('advanced options') |
| 153 | group = advanced.add_mutually_exclusive_group() |
| 154 | group.add_argument( |
| 155 | '--append', |
| 156 | action='store_true', |
| 157 | help="""Extend existing compilation database with new entries. |
| 158 | Duplicate entries are detected and not present in the final output. |
| 159 | The output is not continuously updated, it's done when the build |
| 160 | command finished. """) |
| 161 | |
| 162 | parser.add_argument( |
| 163 | dest='build', nargs=argparse.REMAINDER, help="""Command to run.""") |
| 164 | return parser |
| 165 | |
| 166 | |
| 167 | def create_analyze_parser(from_build_command): |
| 168 | """ Creates a parser for command-line arguments to 'analyze'. """ |
| 169 | |
| 170 | parser = create_default_parser() |
| 171 | |
| 172 | if from_build_command: |
| 173 | parser_add_prefer_wrapper(parser) |
| 174 | parser_add_compilers(parser) |
| 175 | |
| 176 | parser.add_argument( |
| 177 | '--intercept-first', |
| 178 | action='store_true', |
| 179 | help="""Run the build commands first, intercept compiler |
| 180 | calls and then run the static analyzer afterwards. |
| 181 | Generally speaking it has better coverage on build commands. |
| 182 | With '--override-compiler' it use compiler wrapper, but does |
| 183 | not run the analyzer till the build is finished.""") |
| 184 | else: |
| 185 | parser_add_cdb(parser) |
| 186 | |
| 187 | parser.add_argument( |
| 188 | '--status-bugs', |
| 189 | action='store_true', |
| 190 | help="""The exit status of '%(prog)s' is the same as the executed |
| 191 | build command. This option ignores the build exit status and sets to |
| 192 | be non zero if it found potential bugs or zero otherwise.""") |
| 193 | parser.add_argument( |
| 194 | '--exclude', |
| 195 | metavar='<directory>', |
| 196 | dest='excludes', |
| 197 | action='append', |
| 198 | default=[], |
| 199 | help="""Do not run static analyzer against files found in this |
| 200 | directory. (You can specify this option multiple times.) |
| 201 | Could be useful when project contains 3rd party libraries.""") |
| 202 | |
| 203 | output = parser.add_argument_group('output control options') |
| 204 | output.add_argument( |
| 205 | '--output', |
| 206 | '-o', |
| 207 | metavar='<path>', |
Laszlo Nagy | 0d9be63 | 2017-03-20 09:03:24 +0000 | [diff] [blame] | 208 | default=tempfile.gettempdir(), |
Laszlo Nagy | 908ed4c | 2017-03-08 21:22:32 +0000 | [diff] [blame] | 209 | help="""Specifies the output directory for analyzer reports. |
| 210 | Subdirectory will be created if default directory is targeted.""") |
| 211 | output.add_argument( |
| 212 | '--keep-empty', |
| 213 | action='store_true', |
| 214 | help="""Don't remove the build results directory even if no issues |
| 215 | were reported.""") |
| 216 | output.add_argument( |
| 217 | '--html-title', |
| 218 | metavar='<title>', |
| 219 | help="""Specify the title used on generated HTML pages. |
| 220 | If not specified, a default title will be used.""") |
| 221 | format_group = output.add_mutually_exclusive_group() |
| 222 | format_group.add_argument( |
| 223 | '--plist', |
| 224 | '-plist', |
| 225 | dest='output_format', |
| 226 | const='plist', |
| 227 | default='html', |
| 228 | action='store_const', |
| 229 | help="""Cause the results as a set of .plist files.""") |
| 230 | format_group.add_argument( |
| 231 | '--plist-html', |
| 232 | '-plist-html', |
| 233 | dest='output_format', |
| 234 | const='plist-html', |
| 235 | default='html', |
| 236 | action='store_const', |
| 237 | help="""Cause the results as a set of .html and .plist files.""") |
Gabor Horvath | eb0584b | 2018-02-28 13:23:10 +0000 | [diff] [blame^] | 238 | format_group.add_argument( |
| 239 | '--plist-multi-file', |
| 240 | '-plist-multi-file', |
| 241 | dest='output_format', |
| 242 | const='plist-multi-file', |
| 243 | default='html', |
| 244 | action='store_const', |
| 245 | help="""Cause the results as a set of .plist files with extra |
| 246 | information on related files.""") |
Laszlo Nagy | 908ed4c | 2017-03-08 21:22:32 +0000 | [diff] [blame] | 247 | |
| 248 | advanced = parser.add_argument_group('advanced options') |
| 249 | advanced.add_argument( |
| 250 | '--use-analyzer', |
| 251 | metavar='<path>', |
| 252 | dest='clang', |
| 253 | default='clang', |
| 254 | help="""'%(prog)s' uses the 'clang' executable relative to itself for |
| 255 | static analysis. One can override this behavior with this option by |
| 256 | using the 'clang' packaged with Xcode (on OS X) or from the PATH.""") |
| 257 | advanced.add_argument( |
| 258 | '--no-failure-reports', |
| 259 | '-no-failure-reports', |
| 260 | dest='output_failures', |
| 261 | action='store_false', |
| 262 | help="""Do not create a 'failures' subdirectory that includes analyzer |
| 263 | crash reports and preprocessed source files.""") |
| 264 | parser.add_argument( |
| 265 | '--analyze-headers', |
| 266 | action='store_true', |
| 267 | help="""Also analyze functions in #included files. By default, such |
| 268 | functions are skipped unless they are called by functions within the |
| 269 | main source file.""") |
| 270 | advanced.add_argument( |
| 271 | '--stats', |
| 272 | '-stats', |
| 273 | action='store_true', |
| 274 | help="""Generates visitation statistics for the project.""") |
| 275 | advanced.add_argument( |
| 276 | '--internal-stats', |
| 277 | action='store_true', |
| 278 | help="""Generate internal analyzer statistics.""") |
| 279 | advanced.add_argument( |
| 280 | '--maxloop', |
| 281 | '-maxloop', |
| 282 | metavar='<loop count>', |
| 283 | type=int, |
| 284 | help="""Specifiy the number of times a block can be visited before |
| 285 | giving up. Increase for more comprehensive coverage at a cost of |
| 286 | speed.""") |
| 287 | advanced.add_argument( |
| 288 | '--store', |
| 289 | '-store', |
| 290 | metavar='<model>', |
| 291 | dest='store_model', |
| 292 | choices=['region', 'basic'], |
| 293 | help="""Specify the store model used by the analyzer. 'region' |
| 294 | specifies a field- sensitive store model. 'basic' which is far less |
| 295 | precise but can more quickly analyze code. 'basic' was the default |
| 296 | store model for checker-0.221 and earlier.""") |
| 297 | advanced.add_argument( |
| 298 | '--constraints', |
| 299 | '-constraints', |
| 300 | metavar='<model>', |
| 301 | dest='constraints_model', |
| 302 | choices=['range', 'basic'], |
| 303 | help="""Specify the constraint engine used by the analyzer. Specifying |
| 304 | 'basic' uses a simpler, less powerful constraint model used by |
| 305 | checker-0.160 and earlier.""") |
| 306 | advanced.add_argument( |
| 307 | '--analyzer-config', |
| 308 | '-analyzer-config', |
| 309 | metavar='<options>', |
| 310 | help="""Provide options to pass through to the analyzer's |
| 311 | -analyzer-config flag. Several options are separated with comma: |
| 312 | 'key1=val1,key2=val2' |
| 313 | |
| 314 | Available options: |
| 315 | stable-report-filename=true or false (default) |
| 316 | |
| 317 | Switch the page naming to: |
| 318 | report-<filename>-<function/method name>-<id>.html |
| 319 | instead of report-XXXXXX.html""") |
| 320 | advanced.add_argument( |
| 321 | '--force-analyze-debug-code', |
| 322 | dest='force_debug', |
| 323 | action='store_true', |
| 324 | help="""Tells analyzer to enable assertions in code even if they were |
| 325 | disabled during compilation, enabling more precise results.""") |
| 326 | |
| 327 | plugins = parser.add_argument_group('checker options') |
| 328 | plugins.add_argument( |
| 329 | '--load-plugin', |
| 330 | '-load-plugin', |
| 331 | metavar='<plugin library>', |
| 332 | dest='plugins', |
| 333 | action='append', |
| 334 | help="""Loading external checkers using the clang plugin interface.""") |
| 335 | plugins.add_argument( |
| 336 | '--enable-checker', |
| 337 | '-enable-checker', |
| 338 | metavar='<checker name>', |
| 339 | action=AppendCommaSeparated, |
| 340 | help="""Enable specific checker.""") |
| 341 | plugins.add_argument( |
| 342 | '--disable-checker', |
| 343 | '-disable-checker', |
| 344 | metavar='<checker name>', |
| 345 | action=AppendCommaSeparated, |
| 346 | help="""Disable specific checker.""") |
| 347 | plugins.add_argument( |
| 348 | '--help-checkers', |
| 349 | action='store_true', |
| 350 | help="""A default group of checkers is run unless explicitly disabled. |
| 351 | Exactly which checkers constitute the default group is a function of |
| 352 | the operating system in use. These can be printed with this flag.""") |
| 353 | plugins.add_argument( |
| 354 | '--help-checkers-verbose', |
| 355 | action='store_true', |
| 356 | help="""Print all available checkers and mark the enabled ones.""") |
| 357 | |
| 358 | if from_build_command: |
| 359 | parser.add_argument( |
| 360 | dest='build', nargs=argparse.REMAINDER, help="""Command to run.""") |
Gabor Horvath | eb0584b | 2018-02-28 13:23:10 +0000 | [diff] [blame^] | 361 | else: |
| 362 | ctu = parser.add_argument_group('cross translation unit analysis') |
| 363 | ctu_mutex_group = ctu.add_mutually_exclusive_group() |
| 364 | ctu_mutex_group.add_argument( |
| 365 | '--ctu', |
| 366 | action='store_const', |
| 367 | const=CtuConfig(collect=True, analyze=True, |
| 368 | dir='', func_map_cmd=''), |
| 369 | dest='ctu_phases', |
| 370 | help="""Perform cross translation unit (ctu) analysis (both collect |
| 371 | and analyze phases) using default <ctu-dir> for temporary output. |
| 372 | At the end of the analysis, the temporary directory is removed.""") |
| 373 | ctu.add_argument( |
| 374 | '--ctu-dir', |
| 375 | metavar='<ctu-dir>', |
| 376 | dest='ctu_dir', |
| 377 | default='ctu-dir', |
| 378 | help="""Defines the temporary directory used between ctu |
| 379 | phases.""") |
| 380 | ctu_mutex_group.add_argument( |
| 381 | '--ctu-collect-only', |
| 382 | action='store_const', |
| 383 | const=CtuConfig(collect=True, analyze=False, |
| 384 | dir='', func_map_cmd=''), |
| 385 | dest='ctu_phases', |
| 386 | help="""Perform only the collect phase of ctu. |
| 387 | Keep <ctu-dir> for further use.""") |
| 388 | ctu_mutex_group.add_argument( |
| 389 | '--ctu-analyze-only', |
| 390 | action='store_const', |
| 391 | const=CtuConfig(collect=False, analyze=True, |
| 392 | dir='', func_map_cmd=''), |
| 393 | dest='ctu_phases', |
| 394 | help="""Perform only the analyze phase of ctu. <ctu-dir> should be |
| 395 | present and will not be removed after analysis.""") |
| 396 | ctu.add_argument( |
| 397 | '--use-func-map-cmd', |
| 398 | metavar='<path>', |
| 399 | dest='func_map_cmd', |
| 400 | default='clang-func-mapping', |
| 401 | help="""'%(prog)s' uses the 'clang-func-mapping' executable |
| 402 | relative to itself for generating function maps for static |
| 403 | analysis. One can override this behavior with this option by using |
| 404 | the 'clang-func-mapping' packaged with Xcode (on OS X) or from the |
| 405 | PATH.""") |
Laszlo Nagy | 908ed4c | 2017-03-08 21:22:32 +0000 | [diff] [blame] | 406 | return parser |
| 407 | |
| 408 | |
| 409 | def create_default_parser(): |
| 410 | """ Creates command line parser for all build wrapper commands. """ |
| 411 | |
| 412 | parser = argparse.ArgumentParser( |
| 413 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 414 | |
| 415 | parser.add_argument( |
| 416 | '--verbose', |
| 417 | '-v', |
| 418 | action='count', |
| 419 | default=0, |
| 420 | help="""Enable verbose output from '%(prog)s'. A second, third and |
| 421 | fourth flags increases verbosity.""") |
| 422 | return parser |
| 423 | |
| 424 | |
| 425 | def parser_add_cdb(parser): |
| 426 | parser.add_argument( |
| 427 | '--cdb', |
| 428 | metavar='<file>', |
| 429 | default="compile_commands.json", |
| 430 | help="""The JSON compilation database.""") |
| 431 | |
| 432 | |
| 433 | def parser_add_prefer_wrapper(parser): |
| 434 | parser.add_argument( |
| 435 | '--override-compiler', |
| 436 | action='store_true', |
| 437 | help="""Always resort to the compiler wrapper even when better |
| 438 | intercept methods are available.""") |
| 439 | |
| 440 | |
| 441 | def parser_add_compilers(parser): |
| 442 | parser.add_argument( |
| 443 | '--use-cc', |
| 444 | metavar='<path>', |
| 445 | dest='cc', |
| 446 | default=os.getenv('CC', 'cc'), |
| 447 | help="""When '%(prog)s' analyzes a project by interposing a compiler |
| 448 | wrapper, which executes a real compiler for compilation and do other |
| 449 | tasks (record the compiler invocation). Because of this interposing, |
| 450 | '%(prog)s' does not know what compiler your project normally uses. |
| 451 | Instead, it simply overrides the CC environment variable, and guesses |
| 452 | your default compiler. |
| 453 | |
| 454 | If you need '%(prog)s' to use a specific compiler for *compilation* |
| 455 | then you can use this option to specify a path to that compiler.""") |
| 456 | parser.add_argument( |
| 457 | '--use-c++', |
| 458 | metavar='<path>', |
| 459 | dest='cxx', |
| 460 | default=os.getenv('CXX', 'c++'), |
| 461 | help="""This is the same as "--use-cc" but for C++ code.""") |
| 462 | |
| 463 | |
| 464 | class AppendCommaSeparated(argparse.Action): |
| 465 | """ argparse Action class to support multiple comma separated lists. """ |
| 466 | |
| 467 | def __call__(self, __parser, namespace, values, __option_string): |
| 468 | # getattr(obj, attr, default) does not really returns default but none |
| 469 | if getattr(namespace, self.dest, None) is None: |
| 470 | setattr(namespace, self.dest, []) |
| 471 | # once it's fixed we can use as expected |
| 472 | actual = getattr(namespace, self.dest) |
| 473 | actual.extend(values.split(',')) |
| 474 | setattr(namespace, self.dest, actual) |
| 475 | |
| 476 | |
| 477 | def print_active_checkers(checkers): |
| 478 | """ Print active checkers to stdout. """ |
| 479 | |
| 480 | for name in sorted(name for name, (_, active) in checkers.items() |
| 481 | if active): |
| 482 | print(name) |
| 483 | |
| 484 | |
| 485 | def print_checkers(checkers): |
| 486 | """ Print verbose checker help to stdout. """ |
| 487 | |
| 488 | print('') |
| 489 | print('available checkers:') |
| 490 | print('') |
| 491 | for name in sorted(checkers.keys()): |
| 492 | description, active = checkers[name] |
| 493 | prefix = '+' if active else ' ' |
| 494 | if len(name) > 30: |
| 495 | print(' {0} {1}'.format(prefix, name)) |
| 496 | print(' ' * 35 + description) |
| 497 | else: |
| 498 | print(' {0} {1: <30} {2}'.format(prefix, name, description)) |
| 499 | print('') |
| 500 | print('NOTE: "+" indicates that an analysis is enabled by default.') |
| 501 | print('') |