Files
Laszlo NagyandClaude Fable 5 d43f55b2d6 refactor: remove unreachable branches from normalize_args
The isinstance(item, list) branches could never run: the checker
options are argparse 'append' actions, which only produce strings.
The hasattr guards were equally moot since the parser always
defines both attributes. Flatten with a comprehension instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 22:12:34 +10:00

294 lines
11 KiB
Python

# SPDX-License-Identifier: MIT
"""This module parses and validates arguments for command-line interfaces.
It uses the argparse module to create the command line parser.
It also implements basic validation methods, related to the command.
Validations are mostly calling specific help methods, or mangling values.
"""
import argparse
import logging
import os
import sys
import tempfile
from clanganalyzer.clang import get_checkers
# Default values
DEFAULT_OUTPUT_DIR = tempfile.gettempdir()
DEFAULT_CDB_FILE = "compile_commands.json"
__all__ = ["parse_args"]
def parse_args() -> argparse.Namespace:
"""Parse and validate command-line arguments for clanganalyzer."""
parser = create_parser()
args = parser.parse_args()
reconfigure_logging(args.verbose)
logging.debug("Raw arguments %s", sys.argv)
normalize_args(args)
validate_args(parser, args)
logging.debug("Parsed arguments: %s", args)
return args
def reconfigure_logging(verbose_level: int) -> None:
"""Reconfigure logging level and format based on verbosity.
Args:
verbose_level: Number of `-v` flags received (0 means no change)
"""
if verbose_level == 0:
return
root = logging.getLogger()
# Calculate log level: more verbose means lower level
level = max(logging.DEBUG, logging.WARNING - (10 * verbose_level))
root.setLevel(level)
# Choose format based on verbosity
if verbose_level <= 3:
fmt_string = "%(name)s: %(levelname)s: %(message)s"
else:
fmt_string = "%(name)s: %(levelname)s: %(funcName)s: %(message)s"
# Replace existing handlers
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter(fmt=fmt_string))
root.handlers = [handler]
def normalize_args(args: argparse.Namespace) -> None:
"""Normalize parsed arguments for clanganalyzer.
:param args: Parsed argument object. (Will be mutated.)"""
# 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 = {os.path.abspath(entry) for entry in args.excludes}
args.excludes = list(uniq_excludes)
# flatten comma-separated lists for checker options
if args.enable_checker:
args.enable_checker = [checker for item in args.enable_checker for checker in item.split(",")]
if args.disable_checker:
args.disable_checker = [checker for item in args.disable_checker for checker in item.split(",")]
def validate_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None:
"""Command line parsing is done by the argparse module, but semantic
validation still needs to be done. This method is doing it for
clanganalyzer.
:param parser: The command line parser object.
:param args: Parsed argument object.
: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 not os.path.exists(args.cdb):
parser.error(message="compilation database is missing")
def create_parser() -> argparse.ArgumentParser:
"""Creates a parser for command-line arguments."""
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
_ = parser.add_argument(
"--verbose",
"-v",
action="count",
default=0,
help="Enable verbose output from clanganalyzer. A second, third and fourth flags increases verbosity.",
)
_ = parser.add_argument("--cdb", metavar="<file>", default=DEFAULT_CDB_FILE, help="The JSON compilation database.")
_ = parser.add_argument(
"--status-bugs",
action="store_true",
help="The exit status of clanganalyzer is non-zero if potential bugs are found, 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=DEFAULT_OUTPUT_DIR,
help="Specifies the output directory for analyzer reports. A time-stamped subdirectory will be created inside it.",
)
_ = 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",
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",
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",
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="Use the given clang executable for the static analysis. By default the 'clang' executable found in the PATH is used.",
)
_ = advanced.add_argument(
"--analyzer-target",
dest="analyzer_target",
metavar="<target triple name for analysis>",
help="This provides target triple information to clang static analyzer. It only changes the target for analysis but doesn't change the target of a real compiler.",
)
_ = advanced.add_argument(
"--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", 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",
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",
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",
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",
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",
metavar="<plugin library>",
dest="plugins",
action="append",
help="Loading external checkers using the clang plugin interface.",
)
_ = plugins.add_argument(
"--enable-checker",
metavar="<checker name>",
action="append",
help="Enable specific checker. Supports comma-separated lists.",
)
_ = plugins.add_argument(
"--disable-checker",
metavar="<checker name>",
action="append",
help="Disable specific checker. Supports comma-separated lists.",
)
_ = 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."
)
return parser
def print_active_checkers(checkers: dict[str, tuple[str, bool]]) -> None:
"""Print active checkers to stdout."""
for name in sorted(name for name, (_, active) in checkers.items() if active):
print(name)
def print_checkers(checkers: dict[str, tuple[str, bool]]) -> None:
"""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(f" {prefix} {name}")
print(" " * 35 + description)
else:
print(f" {prefix} {name: <30} {description}")
print("")
print('NOTE: "+" indicates that an analysis is enabled by default.')
print("")