https://github.com/NagyDonat updated https://github.com/llvm/llvm-project/pull/135169
From 705372a8a2f6e87f5fdf6b0e99bfa6a13408c5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.n...@ericsson.com> Date: Thu, 3 Apr 2025 20:13:04 +0200 Subject: [PATCH 1/5] [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. // //===----------------------------------------------------------------------===// From c9ade009da319d7e7bec336eaa6d507bff5a3bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.n...@ericsson.com> Date: Mon, 28 Apr 2025 18:41:10 +0200 Subject: [PATCH 2/5] Inline the name of the script --- clang/docs/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/clang/docs/CMakeLists.txt b/clang/docs/CMakeLists.txt index 9dfcc692ff87d..50fdbcc06a1f4 100644 --- a/clang/docs/CMakeLists.txt +++ b/clang/docs/CMakeLists.txt @@ -149,10 +149,9 @@ if (LLVM_ENABLE_SPHINX) 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} + COMMAND ${Python3_EXECUTABLE} generate_analyzer_options_docs.py -i ${analyzeroptions_def} -t ${aopts_rst_in} -o ${aopts_rst} WORKING_DIRECTORY ${docs_tools_dir} VERBATIM COMMENT "Generating ${aopts_rst}" From 4fae8627073111934583f885ba23d7edf478bbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.n...@ericsson.com> Date: Mon, 28 Apr 2025 18:56:17 +0200 Subject: [PATCH 3/5] Clarify argument names of the script generating the docs --- clang/docs/CMakeLists.txt | 5 ++++- clang/docs/tools/generate_analyzer_options_docs.py | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/clang/docs/CMakeLists.txt b/clang/docs/CMakeLists.txt index 50fdbcc06a1f4..e3ba12166534b 100644 --- a/clang/docs/CMakeLists.txt +++ b/clang/docs/CMakeLists.txt @@ -151,7 +151,10 @@ if (LLVM_ENABLE_SPHINX) set(aopts_rst_in "${CMAKE_CURRENT_SOURCE_DIR}/${aopts_rst_rel_path}.in") add_custom_command( OUTPUT ${aopts_rst} - COMMAND ${Python3_EXECUTABLE} generate_analyzer_options_docs.py -i ${analyzeroptions_def} -t ${aopts_rst_in} -o ${aopts_rst} + COMMAND ${Python3_EXECUTABLE} generate_analyzer_options_docs.py + --options-def "${analyzeroptions_def}" + --template "${aopts_rst_in}" + --out "${aopts_rst}" WORKING_DIRECTORY ${docs_tools_dir} VERBATIM COMMENT "Generating ${aopts_rst}" diff --git a/clang/docs/tools/generate_analyzer_options_docs.py b/clang/docs/tools/generate_analyzer_options_docs.py index 5dfc571deb9a0..bbe3b48404a45 100644 --- a/clang/docs/tools/generate_analyzer_options_docs.py +++ b/clang/docs/tools/generate_analyzer_options_docs.py @@ -226,9 +226,9 @@ def get_option_list(input_file): 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") +p.add_argument("--options-def", help="path to AnalyzerOptions.def") +p.add_argument("--template", help="path of template file") +p.add_argument("--out", help="path of output file") opts = p.parse_args() with open(opts.template, encoding="utf-8") as f: @@ -236,7 +236,7 @@ def get_option_list(input_file): PLACEHOLDER = ".. OPTIONS_LIST_PLACEHOLDER\n" -rst_output = doc_template.replace(PLACEHOLDER, get_option_list(opts.input)) +rst_output = doc_template.replace(PLACEHOLDER, get_option_list(opts.options_def)) -with open(opts.output, "w", newline="", encoding="utf-8") as f: +with open(opts.out, "w", newline="", encoding="utf-8") as f: f.write(rst_output) From 461d3db784bd66574c9d175dbc8f5e7190342b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.n...@ericsson.com> Date: Mon, 28 Apr 2025 20:22:24 +0200 Subject: [PATCH 4/5] Extend some disclaimers --- clang/docs/analyzer/user-docs/Options.rst.in | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/clang/docs/analyzer/user-docs/Options.rst.in b/clang/docs/analyzer/user-docs/Options.rst.in index eced3597ed567..96e92bb5a4092 100644 --- a/clang/docs/analyzer/user-docs/Options.rst.in +++ b/clang/docs/analyzer/user-docs/Options.rst.in @@ -28,6 +28,13 @@ 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:``. +.. warning:: + This is an internal interface, Clang does not intend to preserve backwards + compatibility or announce breaking changes within the flags accepted by + ``clang -cc1``. However, ``-analyzer-config`` survived many years without + significant changes and there is no "more official" interface for + configuring the analyzer options. + With the clang driver --------------------- @@ -61,6 +68,10 @@ note the distinction that the ``clang`` *driver* requires ``--analyze`` (double dashes) while the ``clang -cc1`` *compiler frontend* requires ``-analyze`` (single dash). +.. note:: + The flag ``-Xanalyzer`` is equivalent to ``-Xclang`` in these situations + (but doesn't forward other options of the clang frontend). + With CodeChecker ---------------- @@ -89,9 +100,11 @@ 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! + These options are primarily intended for development purposes and + non-default values are usually unsupported. Changing their values may + drastically alter the behavior of the analyzer, and may even result in + instabilities or crashes! Crash reports are welcome and depending on the + severity they may be fixed. .. The contents of this section are automatically generated by the script From 180afc02b5f1ae640c892fa7df22984daff9c2d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.n...@ericsson.com> Date: Tue, 29 Apr 2025 18:41:03 +0200 Subject: [PATCH 5/5] Test generate_analyzer_options_docs.py --- .../tools/generate_analyzer_options_docs.py | 72 +++++++++++++++---- .../generate_analyzer_options_docs.test | 11 +++ clang/test/lit.cfg.py | 2 + 3 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 clang/test/Analysis/generate_analyzer_options_docs.test diff --git a/clang/docs/tools/generate_analyzer_options_docs.py b/clang/docs/tools/generate_analyzer_options_docs.py index bbe3b48404a45..bb38c5f52ca5e 100644 --- a/clang/docs/tools/generate_analyzer_options_docs.py +++ b/clang/docs/tools/generate_analyzer_options_docs.py @@ -42,14 +42,46 @@ class TT(Enum): 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, - ) +class ErrorHandler: + def __init__(self): + self.seen_errors = False + + # This script uses some heuristical tweaks to modify the documentation + # of some analyzer options. As this code is fragile, we record the use + # of these tweaks and report them if they become obsolete: + self.unused_tweaks = [ + "ctu-max-nodes-*", + "accepted values", + "example file content", + ] + + def record_use_of_tweak(self, tweak_name): + try: + self.unused_tweaks.remove(tweak_name) + except ValueError: + pass + + def report_error(self, msg): + print("Error:", msg, file=sys.stderr) + self.seen_errors = True + + def report_unexpected_char(self, s, pos): + lines = (s[:pos] + "X").split("\n") + lineno, col = (len(lines), len(lines[-1])) + self.report_error( + "unexpected character %r in AnalyzerOptions.def at line %d column %d" + % (s[pos], lineno, col), + ) + + def report_unused_tweaks(self): + if not self.unused_tweaks: + return + _is = " is" if len(self.unused_tweaks) == 1 else "s are" + names = ", ".join(self.unused_tweaks) + self.report_error(f"textual tweak{_is} unused in script: {names}") + + +err_handler = ErrorHandler() def tokenize(s): @@ -63,7 +95,7 @@ def tokenize(s): pos = m.end() break else: - report_unexpected(s, pos) + err_handler.report_unexpected_char(s, pos) pos += 1 return result @@ -149,10 +181,12 @@ def cmdflag_to_rst_title(cmdflag_tok): def desc_to_rst_paragraphs(tok): - desc = string_value(tok) + base_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-\*") + desc = base_desc.replace("ctu-max-nodes-*", r"ctu-max-nodes-\*") + if desc != base_desc: + err_handler.record_use_of_tweak("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 @@ -162,8 +196,10 @@ def desc_to_rst_paragraphs(tok): paragraphs = [desc] extra = "" if m := re.search(r"(^|\s)Value:", desc): + err_handler.record_use_of_tweak("accepted values") paragraphs = [desc[: m.start()], "Accepted values:" + desc[m.end() :]] elif m := re.search(r"\s*Example file.content:", desc): + err_handler.record_use_of_tweak("example file content") paragraphs = [desc[: m.start()]] extra = "Example file content::\n\n " + desc[m.end() :] + "\n\n" @@ -209,7 +245,7 @@ def macro_call_to_rst_paragraphs(macro_call): + defaults_to_rst_paragraph(defaults) ) except ValueError as ve: - print(ve.args[0], file=sys.stderr) + err_handler.report_error(ve.args[0]) return "" @@ -227,8 +263,11 @@ def get_option_list(input_file): p = argparse.ArgumentParser() p.add_argument("--options-def", help="path to AnalyzerOptions.def") -p.add_argument("--template", help="path of template file") -p.add_argument("--out", help="path of output file") +p.add_argument("--template", help="template file") +p.add_argument("--out", help="output file") +p.add_argument( + "--validate", action="store_true", help="exit with failure on parsing error" +) opts = p.parse_args() with open(opts.template, encoding="utf-8") as f: @@ -238,5 +277,10 @@ def get_option_list(input_file): rst_output = doc_template.replace(PLACEHOLDER, get_option_list(opts.options_def)) +err_handler.report_unused_tweaks() + with open(opts.out, "w", newline="", encoding="utf-8") as f: f.write(rst_output) + +if opts.validate and err_handler.seen_errors: + sys.exit(1) diff --git a/clang/test/Analysis/generate_analyzer_options_docs.test b/clang/test/Analysis/generate_analyzer_options_docs.test new file mode 100644 index 0000000000000..ae78bd74b2965 --- /dev/null +++ b/clang/test/Analysis/generate_analyzer_options_docs.test @@ -0,0 +1,11 @@ +The documentation of analyzer options is generated by a script that parses +AnalyzerOptions.def. The following line validates that this script +"understands" everything in its input files: + +RUN: %python %src_dir/docs/tools/generate_analyzer_options_docs.py --validate --options-def %src_include_dir/clang/StaticAnalyzer/Core/AnalyzerOptions.def --template %src_dir/docs/analyzer/user-docs/Options.rst.in --out %t.rst + +Moreover, verify that the documentation (e.g. this fragment of the +documentation of the "mode" option) can be found in the output file: + +RUN: FileCheck --input-file=%t.rst %s +CHECK: Controls the high-level analyzer mode diff --git a/clang/test/lit.cfg.py b/clang/test/lit.cfg.py index 8f1392b6a1f8f..51ee3684ac21f 100644 --- a/clang/test/lit.cfg.py +++ b/clang/test/lit.cfg.py @@ -70,6 +70,8 @@ llvm_config.use_clang() +config.substitutions.append(("%src_dir", config.clang_src_dir)) + config.substitutions.append(("%src_include_dir", config.clang_src_dir + "/include")) config.substitutions.append(("%target_triple", config.target_triple)) _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits