[clang] [NFC][analyzer] Document configuration options (PR #135169)
DonĂ¡t Nagy via cfe-commits
cfe-commits at lists.llvm.org
Thu Apr 10 05:15:32 PDT 2025
https://github.com/NagyDonat created https://github.com/llvm/llvm-project/pull/135169
This commit documents the process of specifying values for the analyzer options and checker options implemented in the static analyzer, and adds a script which includes the documentation of the analyzer options (which was previously only available through a command-line flag) in the RST-based web documentation.
>From 705372a8a2f6e87f5fdf6b0e99bfa6a13408c5d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.nagy at ericsson.com>
Date: Thu, 3 Apr 2025 20:13:04 +0200
Subject: [PATCH] [NFC][analyzer] Document configuration options
This commit documents the process of specifying values for the analyzer
options and checker options implemented in the static analyzer, and adds
a script which includes the documentation of the analyzer options (which
was previously only available through a command-line flag) in the
RST-based web documentation.
---
clang/docs/CMakeLists.txt | 26 ++
clang/docs/analyzer/user-docs.rst | 1 +
.../analyzer/user-docs/CommandLineUsage.rst | 2 +
clang/docs/analyzer/user-docs/Options.rst.in | 102 ++++++++
.../tools/generate_analyzer_options_docs.py | 242 ++++++++++++++++++
.../StaticAnalyzer/Core/AnalyzerOptions.def | 3 +
6 files changed, 376 insertions(+)
create mode 100644 clang/docs/analyzer/user-docs/Options.rst.in
create mode 100644 clang/docs/tools/generate_analyzer_options_docs.py
diff --git a/clang/docs/CMakeLists.txt b/clang/docs/CMakeLists.txt
index 4fecc007f5995..9dfcc692ff87d 100644
--- a/clang/docs/CMakeLists.txt
+++ b/clang/docs/CMakeLists.txt
@@ -143,6 +143,32 @@ if (LLVM_ENABLE_SPHINX)
gen_rst_file_from_td(DiagnosticsReference.rst -gen-diag-docs ../include/clang/Basic/Diagnostic.td "${docs_targets}")
gen_rst_file_from_td(ClangCommandLineReference.rst -gen-opt-docs ../include/clang/Driver/ClangOptionDocs.td "${docs_targets}")
+ # Another generated file from a different source
+ set(docs_tools_dir ${CMAKE_CURRENT_SOURCE_DIR}/tools)
+ set(aopts_rst_rel_path analyzer/user-docs/Options.rst)
+ set(aopts_rst "${CMAKE_CURRENT_BINARY_DIR}/${aopts_rst_rel_path}")
+ set(analyzeroptions_def "${CMAKE_CURRENT_SOURCE_DIR}/../include/clang/StaticAnalyzer/Core/AnalyzerOptions.def")
+ set(aopts_rst_in "${CMAKE_CURRENT_SOURCE_DIR}/${aopts_rst_rel_path}.in")
+ set(generate_aopts_docs generate_analyzer_options_docs.py)
+ add_custom_command(
+ OUTPUT ${aopts_rst}
+ COMMAND ${Python3_EXECUTABLE} ${generate_aopts_docs} -i ${analyzeroptions_def} -t ${aopts_rst_in} -o ${aopts_rst}
+ WORKING_DIRECTORY ${docs_tools_dir}
+ VERBATIM
+ COMMENT "Generating ${aopts_rst}"
+ DEPENDS ${docs_tools_dir}/${generate_aopts_docs}
+ ${aopts_rst_in}
+ copy-clang-rst-docs
+ )
+ add_custom_target(generate-analyzer-options-rst DEPENDS ${aopts_rst})
+ foreach(target ${docs_targets})
+ add_dependencies(${target} generate-analyzer-options-rst)
+ endforeach()
+
+ # Technically this is redundant because generate-analyzer-options-rst
+ # depends on the copy operation (because it wants to drop a generated file
+ # into a subdirectory of the copied tree), but I'm leaving it here for the
+ # sake of clarity.
foreach(target ${docs_targets})
add_dependencies(${target} copy-clang-rst-docs)
endforeach()
diff --git a/clang/docs/analyzer/user-docs.rst b/clang/docs/analyzer/user-docs.rst
index e265f033a2c54..67c1dfaa40965 100644
--- a/clang/docs/analyzer/user-docs.rst
+++ b/clang/docs/analyzer/user-docs.rst
@@ -8,6 +8,7 @@ Contents:
user-docs/Installation
user-docs/CommandLineUsage
+ user-docs/Options
user-docs/UsingWithXCode
user-docs/FilingBugs
user-docs/CrossTranslationUnit
diff --git a/clang/docs/analyzer/user-docs/CommandLineUsage.rst b/clang/docs/analyzer/user-docs/CommandLineUsage.rst
index 59f8187f374a9..0252de80b788f 100644
--- a/clang/docs/analyzer/user-docs/CommandLineUsage.rst
+++ b/clang/docs/analyzer/user-docs/CommandLineUsage.rst
@@ -194,6 +194,8 @@ When compiling your application to run on the simulator, it is important that **
If you aren't certain which compiler Xcode uses to build your project, try just running ``xcodebuild`` (without **scan-build**). You should see the full path to the compiler that Xcode is using, and use that as an argument to ``--use-cc``.
+.. _command-line-usage-CodeChecker:
+
CodeChecker
-----------
diff --git a/clang/docs/analyzer/user-docs/Options.rst.in b/clang/docs/analyzer/user-docs/Options.rst.in
new file mode 100644
index 0000000000000..eced3597ed567
--- /dev/null
+++ b/clang/docs/analyzer/user-docs/Options.rst.in
@@ -0,0 +1,102 @@
+========================
+Configuring the Analyzer
+========================
+
+The clang static analyzer supports two kinds of options:
+
+1. Global **analyzer options** influence the behavior of the analyzer engine.
+ They are documented on this page, in the section :ref:`List of analyzer
+ options<list-of-analyzer-options>`.
+2. The **checker options** belong to individual checkers (e.g.
+ ``core.BitwiseShift:Pedantic`` and ``unix.Stream:Pedantic`` are completely
+ separate options) and customize the behavior of that particular checker.
+ These are documented within the documentation of each individual checker at
+ :doc:`../checkers`.
+
+Assigning values to options
+===========================
+
+With the compiler frontend
+--------------------------
+
+All options can be configured by using the ``-analyzer-config`` flag of ``clang
+-cc1`` (the so-called *compiler frontend* part of clang). The values of the
+options are specified with the syntax ``-analyzer-config
+OPT=VAL,OPT2=VAL2,...`` which supports specifying multiple options, but
+separate flags like ``-analyzer-config OPT=VAL -analyzer-config OPT2=VAL2`` are
+also accepted (with equivalent behavior). Analyzer options and checker options
+can be freely intermixed here because it's easy to recognize that checker
+option names are always prefixed with ``some.groups.NameOfChecker:``.
+
+With the clang driver
+---------------------
+
+In a conventional workflow ``clang -cc1`` (which is a low-level internal
+interface) is invoked indirectly by the clang *driver* (i.e. plain ``clang``
+without the ``-cc1`` flag), which acts as an "even more frontend" wrapper layer
+around the ``clang -cc1`` *compiler frontend*. In this situation **each**
+command line argument intended for the *compiler frontend* must be prefixed
+with ``-Xclang``.
+
+For example the following command analyzes ``foo.c`` in :ref:`shallow mode
+<analyzer-option-mode>` with :ref:`loop unrolling
+<analyzer-option-unroll-loops>`:
+
+::
+
+ clang --analyze -Xclang -analyzer-config -Xclang mode=shallow,unroll-loops=true foo.c
+
+When this is executed, the *driver* will compose and execute the following
+``clang -cc1`` command (which can be inspected by passing the ``-v`` flag to
+the *driver*):
+
+::
+
+ clang -cc1 -analyze [...] -analyzer-config mode=shallow,unroll-loops=true foo.c
+
+Here ``[...]`` stands for dozens of low-level flags which ensure that ``clang
+-cc1`` does the right thing (e.g. ``-fcolor-diagnostics`` when it's suitable;
+``-analyzer-checker`` flags to enable a sane default set of checkers). Also
+note the distinction that the ``clang`` *driver* requires ``--analyze`` (double
+dashes) while the ``clang -cc1`` *compiler frontend* requires ``-analyze``
+(single dash).
+
+With CodeChecker
+----------------
+
+If the analysis is performed through :ref:`CodeChecker
+<command-line-usage-CodeChecker>` (which e.g. supports the analysis of a whole
+project instead of a single file) then it will act as another indirection
+layer. CodeChecker provides separate command-line flags called
+``--analyzer-config`` (for analyzer options) and ``--checker-config`` (for
+checker options):
+
+::
+
+ CodeChecker analyze -o outdir --checker-config clangsa:unix.Stream:Pedantic=true \
+ --analyzer-config clangsa:mode=shallow clangsa:unroll-loops=true \
+ -- compile_commands.json
+
+These CodeChecker flags may be followed by multiple ``OPT=VAL`` pairs as
+separate arguments (and this is why the example needs to use ``--`` before
+``compile_commands.json``). The option names are all prefixed with ``clangsa:``
+to ensure that they are passed to the clang static analyzer (and not other
+analyzer tools that are also supported by CodeChecker).
+
+.. _list-of-analyzer-options:
+
+List of analyzer options
+========================
+
+.. warning::
+ These options are primarily intended for development purposes. Changing
+ their values may drastically alter the behavior of the analyzer, and may
+ even result in instabilities or crashes!
+
+..
+ The contents of this section are automatically generated by the script
+ clang/docs/tools/generate_analyzer_options_docs.py from the header file
+ AnalyzerOptions.def to ensure that the RST/web documentation is synchronized
+ with the command line help options.
+
+.. OPTIONS_LIST_PLACEHOLDER
diff --git a/clang/docs/tools/generate_analyzer_options_docs.py b/clang/docs/tools/generate_analyzer_options_docs.py
new file mode 100644
index 0000000000000..5dfc571deb9a0
--- /dev/null
+++ b/clang/docs/tools/generate_analyzer_options_docs.py
@@ -0,0 +1,242 @@
+#!/usr/bin/env python3
+# A tool to automatically generate documentation for the config options of the
+# clang static analyzer by reading `AnalyzerOptions.def`.
+
+import argparse
+from collections import namedtuple
+from enum import Enum, auto
+import re
+import sys
+import textwrap
+
+
+# The following code implements a trivial parser for the narrow subset of C++
+# which is used in AnalyzerOptions.def. This supports the following features:
+# - ignores preprocessor directives, even if they are continued with \ at EOL
+# - ignores comments: both /* ... */ and // ...
+# - parses string literals (even if they contain \" escapes)
+# - concatenates adjacent string literals
+# - parses numbers even if they contain ' as a thousands separator
+# - recognizes MACRO(arg1, arg2, ..., argN) calls
+
+
+class TT(Enum):
+ "Token type enum."
+ number = auto()
+ ident = auto()
+ string = auto()
+ punct = auto()
+
+
+TOKENS = [
+ (re.compile(r"-?[0-9']+"), TT.number),
+ (re.compile(r"\w+"), TT.ident),
+ (re.compile(r'"([^\\"]|\\.)*"'), TT.string),
+ (re.compile(r"[(),]"), TT.punct),
+ (re.compile(r"/\*((?!\*/).)*\*/", re.S), None), # C-style comment
+ (re.compile(r"//.*\n"), None), # C++ style oneline comment
+ (re.compile(r"#.*(\\\n.*)*(?<!\\)\n"), None), # preprocessor directive
+ (re.compile(r"\s+"), None), # whitespace
+]
+
+Token = namedtuple("Token", "kind code")
+
+
+def report_unexpected(s, pos):
+ lines = (s[:pos] + "X").split("\n")
+ lineno, col = (len(lines), len(lines[-1]))
+ print(
+ "unexpected character %r in AnalyzerOptions.def at line %d column %d"
+ % (s[pos], lineno, col),
+ file=sys.stderr,
+ )
+
+
+def tokenize(s):
+ result = []
+ pos = 0
+ while pos < len(s):
+ for regex, kind in TOKENS:
+ if m := regex.match(s, pos):
+ if kind is not None:
+ result.append(Token(kind, m.group(0)))
+ pos = m.end()
+ break
+ else:
+ report_unexpected(s, pos)
+ pos += 1
+ return result
+
+
+def join_strings(tokens):
+ result = []
+ for tok in tokens:
+ if tok.kind == TT.string and result and result[-1].kind == TT.string:
+ # If this token is a string, and the previous non-ignored token is
+ # also a string, then merge them into a single token. We need to
+ # discard the closing " of the previous string and the opening " of
+ # this string.
+ prev = result.pop()
+ result.append(Token(TT.string, prev.code[:-1] + tok.code[1:]))
+ else:
+ result.append(tok)
+ return result
+
+
+MacroCall = namedtuple("MacroCall", "name args")
+
+
+class State(Enum):
+ "States of the state machine used for parsing the macro calls."
+ init = auto()
+ after_ident = auto()
+ before_arg = auto()
+ after_arg = auto()
+
+
+def get_calls(tokens, macro_names):
+ state = State.init
+ result = []
+ current = None
+ for tok in tokens:
+ if state == State.init and tok.kind == TT.ident and tok.code in macro_names:
+ current = MacroCall(tok.code, [])
+ state = State.after_ident
+ elif state == State.after_ident and tok == Token(TT.punct, "("):
+ state = State.before_arg
+ elif state == State.before_arg:
+ if current is not None:
+ current.args.append(tok)
+ state = State.after_arg
+ elif state == State.after_arg and tok.kind == TT.punct:
+ if tok.code == ")":
+ result.append(current)
+ current = None
+ state = State.init
+ elif tok.code == ",":
+ state = State.before_arg
+ else:
+ current = None
+ state = State.init
+ return result
+
+
+# The information will be extracted from calls to these two macros:
+# #define ANALYZER_OPTION(TYPE, NAME, CMDFLAG, DESC, DEFAULT_VAL)
+# #define ANALYZER_OPTION_DEPENDS_ON_USER_MODE(TYPE, NAME, CMDFLAG, DESC,
+# SHALLOW_VAL, DEEP_VAL)
+
+MACRO_NAMES_ARGCOUNTS = {
+ "ANALYZER_OPTION": 5,
+ "ANALYZER_OPTION_DEPENDS_ON_USER_MODE": 6,
+}
+
+
+def string_value(tok):
+ if tok.kind != TT.string:
+ raise ValueError(f"expected a string token, got {tok.kind.name}")
+ text = tok.code[1:-1] # Remove quotes
+ text = re.sub(r"\\(.)", r"\1", text) # Resolve backslash escapes
+ return text
+
+
+def cmdflag_to_rst_title(cmdflag_tok):
+ text = string_value(cmdflag_tok)
+ underline = "-" * len(text)
+ ref = f".. _analyzer-option-{text}:"
+
+ return f"{ref}\n\n{text}\n{underline}\n\n"
+
+
+def desc_to_rst_paragraphs(tok):
+ desc = string_value(tok)
+
+ # Escape a star that would act as inline emphasis within RST.
+ desc = desc.replace("ctu-max-nodes-*", r"ctu-max-nodes-\*")
+
+ # Many descriptions end with "Value: <list of accepted values>", which is
+ # OK for a terse command line printout, but should be prettified for web
+ # documentation.
+ # Moreover, the option ctu-invocation-list shows some example file content
+ # which is formatted as a preformatted block.
+ paragraphs = [desc]
+ extra = ""
+ if m := re.search(r"(^|\s)Value:", desc):
+ paragraphs = [desc[: m.start()], "Accepted values:" + desc[m.end() :]]
+ elif m := re.search(r"\s*Example file.content:", desc):
+ paragraphs = [desc[: m.start()]]
+ extra = "Example file content::\n\n " + desc[m.end() :] + "\n\n"
+
+ wrapped = [textwrap.fill(p, width=80) for p in paragraphs if p.strip()]
+
+ return "\n\n".join(wrapped + [""]) + extra
+
+
+def default_to_rst(tok):
+ if tok.kind == TT.string:
+ if tok.code == '""':
+ return "(empty string)"
+ return tok.code
+ if tok.kind == TT.ident:
+ return tok.code
+ if tok.kind == TT.number:
+ return tok.code.replace("'", "")
+ raise ValueError(f"unexpected token as default value: {tok.kind.name}")
+
+
+def defaults_to_rst_paragraph(defaults):
+ strs = [default_to_rst(d) for d in defaults]
+
+ if len(strs) == 1:
+ return f"Default value: {strs[0]}\n\n"
+ if len(strs) == 2:
+ return (
+ f"Default value: {strs[0]} (in shallow mode) / {strs[1]} (in deep mode)\n\n"
+ )
+ raise ValueError("unexpected count of default values: %d" % len(defaults))
+
+
+def macro_call_to_rst_paragraphs(macro_call):
+ if len(macro_call.args) != MACRO_NAMES_ARGCOUNTS[macro_call.name]:
+ return ""
+
+ try:
+ _, _, cmdflag, desc, *defaults = macro_call.args
+
+ return (
+ cmdflag_to_rst_title(cmdflag)
+ + desc_to_rst_paragraphs(desc)
+ + defaults_to_rst_paragraph(defaults)
+ )
+ except ValueError as ve:
+ print(ve.args[0], file=sys.stderr)
+ return ""
+
+
+def get_option_list(input_file):
+ with open(input_file, encoding="utf-8") as f:
+ contents = f.read()
+ tokens = join_strings(tokenize(contents))
+ macro_calls = get_calls(tokens, MACRO_NAMES_ARGCOUNTS)
+
+ result = ""
+ for mc in macro_calls:
+ result += macro_call_to_rst_paragraphs(mc)
+ return result
+
+
+p = argparse.ArgumentParser()
+p.add_argument("-i", "--input", help="path to AnalyzerOptions.def")
+p.add_argument("-t", "--template", help="path of template file")
+p.add_argument("-o", "--output", help="path of output file")
+opts = p.parse_args()
+
+with open(opts.template, encoding="utf-8") as f:
+ doc_template = f.read()
+
+PLACEHOLDER = ".. OPTIONS_LIST_PLACEHOLDER\n"
+
+rst_output = doc_template.replace(PLACEHOLDER, get_option_list(opts.input))
+
+with open(opts.output, "w", newline="", encoding="utf-8") as f:
+ f.write(rst_output)
diff --git a/clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def b/clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def
index f9f22a9ced650..8326f5309035e 100644
--- a/clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def
+++ b/clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def
@@ -7,6 +7,9 @@
//===----------------------------------------------------------------------===//
//
// This file defines the analyzer options avaible with -analyzer-config.
+// Note that clang/docs/tools/generate_analyzer_options_docs.py relies on the
+// structure of this file, so if this file is refactored, then make sure to
+// update that script as well.
//
//===----------------------------------------------------------------------===//
More information about the cfe-commits
mailing list