[clang-tidy][NFC] Fix list.rst and improve alias detection of add_new_check.py (#192228)

Follow up of https://github.com/llvm/llvm-project/pull/192224.

This commit does two things:

- Replace the original alias detection based on `:http-equiv` (we may
remove these completely in the future) with a method of directly
matching the documentation section.
- Update the list.rst

---------

Co-authored-by: Victor Chernyakin <chernyakin.victor.j@outlook.com>
This commit is contained in:
Zeyi Xu
2026-04-21 10:43:09 +08:00
committed by GitHub
parent 4acbf99789
commit 1bfcddcded
2 changed files with 122 additions and 111 deletions

View File

@@ -30,14 +30,14 @@ def adapt_cmake(module_path: str, check_name_camel: str) -> bool:
with open(filename, "r", encoding="utf8") as f:
lines = f.readlines()
cpp_file = check_name_camel + ".cpp"
cpp_file = f"{check_name_camel}.cpp"
# Figure out whether this check already exists.
for line in lines:
if line.strip() == cpp_file:
return False
print("Updating %s..." % filename)
print(f"Updating {filename}...")
with open(filename, "w", encoding="utf8", newline="\n") as f:
cpp_found = False
file_added = False
@@ -46,7 +46,7 @@ def adapt_cmake(module_path: str, check_name_camel: str) -> bool:
if (not file_added) and (cpp_line or cpp_found):
cpp_found = True
if (line.strip() > cpp_file) or (not cpp_line):
f.write(" " + cpp_file + "\n")
f.write(f" {cpp_file}\n")
file_added = True
f.write(line)
@@ -77,15 +77,12 @@ def write_header(
)
else:
override_supported = ""
filename = os.path.join(module_path, check_name_camel) + ".h"
print("Creating %s..." % filename)
filename = f"{os.path.join(module_path, check_name_camel)}.h"
print(f"Creating {filename}...")
with open(filename, "w", encoding="utf8", newline="\n") as f:
header_guard = (
"LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_"
+ module.upper()
+ "_"
+ check_name_camel.upper()
+ "_H"
f"LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_{module.upper()}_"
f"{check_name_camel.upper()}_H"
)
f.write(
"""\
@@ -136,8 +133,8 @@ public:
def write_implementation(
module_path: str, module: str, namespace: str, check_name_camel: str
) -> None:
filename = os.path.join(module_path, check_name_camel) + ".cpp"
print("Creating %s..." % filename)
filename = f"{os.path.join(module_path, check_name_camel)}.cpp"
print(f"Creating {filename}...")
with open(filename, "w", encoding="utf8", newline="\n") as f:
f.write(
"""\
@@ -182,7 +179,7 @@ void %(check_name)s::check(const MatchFinder::MatchResult &Result) {
def get_module_filename(module_path: str, module: str) -> str:
modulecpp = list(
filter(
lambda p: p.lower() == module.lower() + "tidymodule.cpp",
lambda p: p.lower() == f"{module.lower()}tidymodule.cpp",
os.listdir(module_path),
)
)[0]
@@ -197,18 +194,15 @@ def adapt_module(
with open(filename, "r", encoding="utf8") as f:
lines = f.readlines()
print("Updating %s..." % filename)
print(f"Updating {filename}...")
with open(filename, "w", encoding="utf8", newline="\n") as f:
header_added = False
header_found = False
check_added = False
check_fq_name = module + "-" + check_name
check_fq_name = f"{module}-{check_name}"
check_decl = (
" CheckFactories.registerCheck<"
+ check_name_camel
+ '>(\n "'
+ check_fq_name
+ '");\n'
f" CheckFactories.registerCheck<{check_name_camel}>(\n"
f' "{check_fq_name}");\n'
)
lines_iter = iter(lines)
@@ -220,10 +214,10 @@ def adapt_module(
header_found = True
if match.group(1) > check_name_camel:
header_added = True
f.write('#include "' + check_name_camel + '.h"\n')
f.write(f'#include "{check_name_camel}.h"\n')
elif header_found:
header_added = True
f.write('#include "' + check_name_camel + '.h"\n')
f.write(f'#include "{check_name_camel}.h"\n')
if not check_added:
if line.strip() == "}":
@@ -263,7 +257,7 @@ def add_release_notes(
description, width=80, initial_indent=" ", subsequent_indent=" "
)
)
check_name_dashes = module + "-" + check_name
check_name_dashes = f"{module}-{check_name}"
filename = os.path.normpath(
os.path.join(module_path, "../../docs/ReleaseNotes.rst")
)
@@ -274,7 +268,7 @@ def add_release_notes(
nextSectionMatcher = re.compile("New check aliases")
checkMatcher = re.compile("- New :doc:`(.*)")
print("Updating %s..." % filename)
print(f"Updating {filename}...")
with open(filename, "w", encoding="utf8", newline="\n") as f:
note_added = False
header_found = False
@@ -302,13 +296,12 @@ def add_release_notes(
if header_found and add_note_here:
if not line.startswith("^^^^"):
f.write(
"""- New :doc:`%s
<clang-tidy/checks/%s/%s>` check.
f"""- New :doc:`{check_name_dashes}
<clang-tidy/checks/{module}/{check_name}>` check.
%s
{wrapped_desc}
"""
% (check_name_dashes, module, check_name, wrapped_desc)
)
note_added = True
@@ -324,7 +317,7 @@ def write_test(
test_standard: Optional[str],
) -> None:
test_standard = f"-std={test_standard}-or-later " if test_standard else ""
check_name_dashes = module + "-" + check_name
check_name_dashes = f"{module}-{check_name}"
filename = os.path.normpath(
os.path.join(
module_path,
@@ -334,10 +327,10 @@ def write_test(
"clang-tidy",
"checkers",
module,
check_name + "." + test_extension,
f"{check_name}.{test_extension}",
)
)
print("Creating %s..." % filename)
print(f"Creating {filename}...")
with open(filename, "w", encoding="utf8", newline="\n") as f:
f.write(
"""\
@@ -402,8 +395,8 @@ def update_checks_list(clang_tidy_path: str) -> None:
return ""
with open(module_file, "r") as f:
code = f.read()
full_check_name = module_name + "-" + check_name
if (name_pos := code.find('"' + full_check_name + '"')) == -1:
full_check_name = f"{module_name}-{check_name}"
if (name_pos := code.find(f'"{full_check_name}"')) == -1:
return ""
if (stmt_end_pos := code.find(";", name_pos)) == -1:
return ""
@@ -423,24 +416,24 @@ def update_checks_list(clang_tidy_path: str) -> None:
)
else:
class_path = os.path.join(clang_tidy_path, module_name)
return get_actual_filename(class_path, class_name + ".cpp")
return get_actual_filename(class_path, f"{class_name}.cpp")
return ""
# Examine code looking for a c'tor definition to get the base class name.
def get_base_class(code: str, check_file: str) -> str:
check_class_name = os.path.splitext(os.path.basename(check_file))[0]
ctor_pattern = check_class_name + r"\([^:]*\)\s*:\s*([A-Z][A-Za-z0-9]*Check)\("
matches = re.search(r"\s+" + check_class_name + "::" + ctor_pattern, code)
ctor_pattern = rf"{check_class_name}\([^:]*\)\s*:\s*([A-Z][A-Za-z0-9]*Check)\("
matches = re.search(rf"\s+{check_class_name}::{ctor_pattern}", code)
# The constructor might be inline in the header.
if not matches:
header_file = os.path.splitext(check_file)[0] + ".h"
header_file = f"{os.path.splitext(check_file)[0]}.h"
if not os.path.isfile(header_file):
return ""
with open(header_file, encoding="utf8") as f:
code = f.read()
matches = re.search(" " + ctor_pattern, code)
matches = re.search(rf" {ctor_pattern}", code)
if matches and matches[1] != "ClangTidyCheck":
return matches[1]
@@ -465,13 +458,13 @@ def update_checks_list(clang_tidy_path: str) -> None:
check_file = get_actual_filename(
os.path.join(clang_tidy_path, dirname),
get_camel_check_name(check_name) + ".cpp",
f"{get_camel_check_name(check_name)}.cpp",
)
if not os.path.isfile(check_file):
# Some older checks don't end with 'Check.cpp'
check_file = get_actual_filename(
os.path.join(clang_tidy_path, dirname),
get_camel_name(check_name) + ".cpp",
f"{get_camel_name(check_name)}.cpp",
)
if not os.path.isfile(check_file):
# Some checks aren't in a file based on the check name.
@@ -485,7 +478,7 @@ def update_checks_list(clang_tidy_path: str) -> None:
return ' "Yes"'
if base_class := get_base_class(code, check_file):
base_file = os.path.join(clang_tidy_path, dirname, base_class + ".cpp")
base_file = os.path.join(clang_tidy_path, dirname, f"{base_class}.cpp")
if os.path.isfile(base_file):
with open(base_file, encoding="utf8") as f:
code = f.read()
@@ -494,8 +487,39 @@ def update_checks_list(clang_tidy_path: str) -> None:
return ""
def process_doc(doc_file: Tuple[str, str]) -> Tuple[str, Optional[Match[str]]]:
check_name = doc_file[0] + "-" + doc_file[1].replace(".rst", "")
def detect_alias_target(check_name: str, content: str) -> Optional[str]:
"""Return the :doc: target for non-redirect alias pages.
This recognizes pages that keep their own documentation content, but
whose paragraph explicitly states that the current check is an
alias of another check.
"""
paragraphs = [
re.sub(r"\s+", " ", paragraph.strip())
for paragraph in re.split(r"\n\s*\n", content)
if paragraph.strip()
]
self_alias = re.compile(
r"^This check is an alias(?: of check| for)\b",
re.IGNORECASE,
)
named_alias = re.compile(
rf"^The\s+`?{re.escape(check_name)}(?:\s+check)?`?"
rf"(?:\s+check)?\s+is\s+an\s+alias,?\s+please\s+see\b",
re.IGNORECASE,
)
for paragraph in paragraphs:
if self_alias.search(paragraph) or named_alias.search(paragraph):
if match := re.search(r":doc:`[^`<]+?<([^>]+)>`", paragraph):
return match.group(1)
if match := re.search(r"`[^`<]+?<(.+?)\.html(?:#[^>]+)?>`_", paragraph):
return match.group(1)
return None
def process_doc(doc_file: Tuple[str, str]) -> Tuple[str, Optional[str]]:
check_name = f"{doc_file[0]}-{doc_file[1].replace('.rst', '')}"
with open(os.path.join(docs_dir, *doc_file), "r", encoding="utf8") as doc:
content = doc.read()
@@ -504,76 +528,64 @@ def update_checks_list(clang_tidy_path: str) -> None:
# Orphan page, don't list it.
return "", None
match = re.search(r".*:http-equiv=refresh: \d+;URL=(.*).html(.*)", content)
# Is it a redirect?
return check_name, match
return check_name, detect_alias_target(check_name, content)
def format_link(doc_file: Tuple[str, str]) -> str:
check_name, match = process_doc(doc_file)
if not match and check_name and not check_name.startswith("clang-analyzer-"):
return " :doc:`%(check_name)s <%(module)s/%(check)s>`,%(autofix)s\n" % {
"check_name": check_name,
"module": doc_file[0],
"check": doc_file[1].replace(".rst", ""),
"autofix": has_auto_fix(check_name),
}
return (
f" :doc:`{check_name} <{doc_file[0]}/{doc_file[1].replace('.rst', '')}>`,"
f"{has_auto_fix(check_name)}\n"
)
else:
return ""
def format_link_alias(doc_file: Tuple[str, str]) -> str:
check_name, match = process_doc(doc_file)
if (match or (check_name.startswith("clang-analyzer-"))) and check_name:
module = doc_file[0]
check_file = doc_file[1].replace(".rst", "")
if (
not match
or match.group(1) == "https://clang.llvm.org/docs/analyzer/checkers"
):
title = "Clang Static Analyzer " + check_file
# Preserve the anchor in checkers.html from group 2.
target = "" if not match else match.group(1) + ".html" + match.group(2)
autofix = ""
ref_begin = ""
ref_end = "_"
else:
redirect_parts = re.search(r"^\.\./([^/]*)/([^/]*)$", match.group(1))
assert redirect_parts
title = redirect_parts[1] + "-" + redirect_parts[2]
target = redirect_parts[1] + "/" + redirect_parts[2]
autofix = has_auto_fix(title)
ref_begin = ":doc:"
ref_end = ""
is_clang_analyzer = check_name.startswith("clang-analyzer-")
if not check_name or (not match and not is_clang_analyzer):
return ""
if target:
# The checker is just a redirect.
return (
" :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(ref_begin)s`%(title)s <%(target)s>`%(ref_end)s,%(autofix)s\n"
% {
"check_name": check_name,
"module": module,
"check_file": check_file,
"target": target,
"title": title,
"autofix": autofix,
"ref_begin": ref_begin,
"ref_end": ref_end,
}
)
else:
# The checker is just a alias without redirect.
return (
" :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(title)s,%(autofix)s\n"
% {
"check_name": check_name,
"module": module,
"check_file": check_file,
"title": title,
"autofix": autofix,
}
)
return ""
module = doc_file[0]
check_file = doc_file[1].replace(".rst", "")
if is_clang_analyzer:
title = f"Clang Static Analyzer {check_file}"
# Clang Static Analyzer aliases still need the external redirect
# target so list.rst can link to the upstream analyzer docs.
with open(os.path.join(docs_dir, *doc_file), "r", encoding="utf8") as doc:
content = doc.read()
redirect = re.search(
r".*:http-equiv=refresh: \d+;URL=(.*).html(.*)", content
)
# Preserve the anchor in checkers.html from group 2.
target = (
"" if not redirect else f"{redirect.group(1)}.html{redirect.group(2)}"
)
autofix = ""
ref_begin = ""
ref_end = "_"
else:
# Match neighbour or current-directory doc targets.
redirect_parts = re.search(r"^(?:\.\./([^/]+)/)?([^/]+)$", match)
assert redirect_parts
redirect_module = redirect_parts[1] or module
title = f"{redirect_module}-{redirect_parts[2]}"
target = f"{redirect_module}/{redirect_parts[2]}"
autofix = has_auto_fix(title)
ref_begin = ":doc:"
ref_end = ""
print("Updating %s..." % filename)
if target:
# The checker is just a redirect.
return (
f" :doc:`{check_name} <{module}/{check_file}>`, "
f"{ref_begin}`{title} <{target}>`{ref_end},{autofix}\n"
)
# The checker is just a alias without redirect.
return f" :doc:`{check_name} <{module}/{check_file}>`, {title},{autofix}\n"
print(f"Updating {filename}...")
with open(filename, "w", encoding="utf8", newline="\n") as f:
for line in lines:
f.write(line)
@@ -591,13 +603,13 @@ def update_checks_list(clang_tidy_path: str) -> None:
# Adds a documentation for the check.
def write_docs(module_path: str, module: str, check_name: str) -> None:
check_name_dashes = module + "-" + check_name
check_name_dashes = f"{module}-{check_name}"
filename = os.path.normpath(
os.path.join(
module_path, "../../docs/clang-tidy/checks/", module, check_name + ".rst"
module_path, "../../docs/clang-tidy/checks/", module, f"{check_name}.rst"
)
)
print("Creating %s..." % filename)
print(f"Creating {filename}...")
with open(filename, "w", encoding="utf8", newline="\n") as f:
f.write(
""".. title:: clang-tidy - %(check_name_dashes)s
@@ -619,7 +631,7 @@ def get_camel_name(check_name: str) -> str:
def get_camel_check_name(check_name: str) -> str:
return get_camel_name(check_name) + "Check"
return f"{get_camel_name(check_name)}Check"
def main() -> None:
@@ -699,8 +711,7 @@ def main() -> None:
check_name_camel = get_camel_check_name(check_name)
if check_name.startswith(module):
print(
'Check name "%s" must not start with the module "%s". Exiting.'
% (check_name, module)
f'Check name "{check_name}" must not start with the module "{module}". Exiting.'
)
return
clang_tidy_path = os.path.dirname(sys.argv[0])
@@ -711,7 +722,7 @@ def main() -> None:
# Map module names to namespace names that don't conflict with widely used top-level namespaces.
if module == "llvm":
namespace = module + "_check"
namespace = f"{module}_check"
else:
namespace = module

View File

@@ -242,7 +242,6 @@ Clang-Tidy Checks
:doc:`google-runtime-int <google/runtime-int>`,
:doc:`google-runtime-operator <google/runtime-operator>`,
:doc:`google-upgrade-googletest-case <google/upgrade-googletest-case>`, "Yes"
:doc:`hicpp-exception-baseclass <hicpp/exception-baseclass>`,
:doc:`hicpp-multiway-paths-covered <hicpp/multiway-paths-covered>`,
:doc:`linuxkernel-must-check-errs <linuxkernel/must-check-errs>`,
:doc:`llvm-header-guard <llvm/header-guard>`,
@@ -352,7 +351,6 @@ Clang-Tidy Checks
:doc:`openmp-use-default-none <openmp/use-default-none>`,
:doc:`performance-avoid-endl <performance/avoid-endl>`, "Yes"
:doc:`performance-enum-size <performance/enum-size>`,
:doc:`performance-faster-string-find <performance/faster-string-find>`, "Yes"
:doc:`performance-for-range-copy <performance/for-range-copy>`, "Yes"
:doc:`performance-implicit-conversion-in-loop <performance/implicit-conversion-in-loop>`,
:doc:`performance-inefficient-algorithm <performance/inefficient-algorithm>`, "Yes"
@@ -467,6 +465,7 @@ Check aliases
:doc:`cert-err60-cpp <cert/err60-cpp>`, :doc:`bugprone-exception-copy-constructor-throws <bugprone/exception-copy-constructor-throws>`,
:doc:`cert-err61-cpp <cert/err61-cpp>`, :doc:`misc-throw-by-value-catch-by-reference <misc/throw-by-value-catch-by-reference>`,
:doc:`cert-exp42-c <cert/exp42-c>`, :doc:`bugprone-suspicious-memory-comparison <bugprone/suspicious-memory-comparison>`,
:doc:`cert-exp45-c <cert/exp45-c>`, :doc:`bugprone-assignment-in-selection-statement <bugprone/assignment-in-selection-statement>`,
:doc:`cert-fio38-c <cert/fio38-c>`, :doc:`misc-non-copyable-objects <misc/non-copyable-objects>`,
:doc:`cert-flp30-c <cert/flp30-c>`, :doc:`bugprone-float-loop-counter <bugprone/float-loop-counter>`,
:doc:`cert-flp37-c <cert/flp37-c>`, :doc:`bugprone-suspicious-memory-comparison <bugprone/suspicious-memory-comparison>`,
@@ -634,3 +633,4 @@ Check aliases
:doc:`hicpp-vararg <hicpp/vararg>`, :doc:`cppcoreguidelines-pro-type-vararg <cppcoreguidelines/pro-type-vararg>`,
:doc:`llvm-else-after-return <llvm/else-after-return>`, :doc:`readability-else-after-return <readability/else-after-return>`, "Yes"
:doc:`llvm-qualified-auto <llvm/qualified-auto>`, :doc:`readability-qualified-auto <readability/qualified-auto>`, "Yes"
:doc:`performance-faster-string-find <performance/faster-string-find>`, :doc:`performance-prefer-single-char-overloads <performance/prefer-single-char-overloads>`, "Yes"