568 lines
19 KiB
Python
568 lines
19 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||
|
# See https://llvm.org/LICENSE.txt for license information.
|
||
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||
|
""" 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.
|
||
|
"""
|
||
|
from __future__ import absolute_import, division, print_function
|
||
|
|
||
|
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-extdef-mapping
|
||
|
if not is_ctu_capable(args.extdef_map_cmd):
|
||
|
parser.error(
|
||
|
message="""This version of clang does not support CTU
|
||
|
functionality or clang-extdef-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.""",
|
||
|
)
|
||
|
format_group.add_argument(
|
||
|
"--sarif",
|
||
|
"-sarif",
|
||
|
dest="output_format",
|
||
|
const="sarif",
|
||
|
default="html",
|
||
|
action="store_const",
|
||
|
help="""Cause the results as a result.sarif file.""",
|
||
|
)
|
||
|
format_group.add_argument(
|
||
|
"--sarif-html",
|
||
|
"-sarif-html",
|
||
|
dest="output_format",
|
||
|
const="sarif-html",
|
||
|
default="html",
|
||
|
action="store_const",
|
||
|
help="""Cause the results as a result.sarif file and .html 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="", extdef_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="", extdef_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="", extdef_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-extdef-map-cmd",
|
||
|
metavar="<path>",
|
||
|
dest="extdef_map_cmd",
|
||
|
default="clang-extdef-mapping",
|
||
|
help="""'%(prog)s' uses the 'clang-extdef-mapping' executable
|
||
|
relative to itself for generating external definition maps for
|
||
|
static analysis. One can override this behavior with this option
|
||
|
by using the 'clang-extdef-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("")
|