[clang-tools-extra] [clang-tidy] Implement alphabetical order test (PR #166072)
via cfe-commits
cfe-commits at lists.llvm.org
Sun Nov 2 22:21:18 PST 2025
https://github.com/zeyi2 updated https://github.com/llvm/llvm-project/pull/166072
>From 7b4e4172fc3cea8e8194aa3544f2c2ec30e3616a Mon Sep 17 00:00:00 2001
From: mtx <mitchell.xu2 at gmail.com>
Date: Sun, 2 Nov 2025 22:56:53 +0800
Subject: [PATCH 1/6] [clang-tidy][docs] Implement alphabetical order check
---
.../clang-tidy-alphabetical-order-check.py | 301 ++++++++++++++++++
.../infrastructure/alphabetical-order.cpp | 6 +
clang-tools-extra/test/lit.cfg.py | 1 +
3 files changed, 308 insertions(+)
create mode 100644 clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
create mode 100644 clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp
diff --git a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
new file mode 100644
index 0000000000000..321663bb7d577
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
@@ -0,0 +1,301 @@
+#!/usr/bin/env python3
+
+"""
+Normalize clang-tidy docs with deterministic sorting for linting/tests.
+
+Subcommands:
+ - checks-list: Sort entries in docs/clang-tidy/checks/list.rst csv-table.
+ - release-notes: Sort key sections in docs/ReleaseNotes.rst and de-duplicate
+ entries in "Changes in existing checks".
+
+Usage:
+ clang-tidy-alphabetical-order-check.py <subcommand> [-i <input rst>] [-o <output rst>] [--fix]
+
+Flags:
+ -i/--input Input file.
+ -o/--output Write normalized content here; omit to write to stdout.
+ --fix Rewrite the input file in place. Cannot be combined with -o/--output.
+"""
+
+import argparse
+import io
+import os
+import re
+import sys
+from typing import List, Optional, Sequence, Tuple
+
+DOC_LABEL_RN_RE = re.compile(r":doc:`(?P<label>[^`<]+)\s*(?:<[^>]+>)?`")
+DOC_LINE_RE = re.compile(r"^\s*:doc:`(?P<label>[^`<]+?)\s*<[^>]+>`.*$")
+
+
+def script_dir() -> str:
+ return os.path.dirname(os.path.abspath(__file__))
+
+
+def read_text(path: str) -> List[str]:
+ with io.open(path, "r", encoding="utf-8") as f:
+ return f.read().splitlines(True)
+
+
+def write_text(path: str, content: str) -> None:
+ with io.open(path, "w", encoding="utf-8", newline="") as f:
+ f.write(content)
+
+
+def normalize_list_rst(lines: List[str]) -> str:
+ out: List[str] = []
+ i = 0
+ n = len(lines)
+ while i < n:
+ out.append(lines[i])
+ if lines[i].lstrip().startswith(".. csv-table::"):
+ i += 1
+ break
+ i += 1
+
+ while i < n and (lines[i].startswith(" ") or lines[i].strip() == ""):
+ if DOC_LINE_RE.match(lines[i]):
+ break
+ out.append(lines[i])
+ i += 1
+
+ entries: List[str] = []
+ while i < n and lines[i].startswith(" "):
+ if DOC_LINE_RE.match(lines[i]):
+ entries.append(lines[i])
+ else:
+ entries.append(lines[i])
+ i += 1
+
+ def key_for(line: str):
+ m = DOC_LINE_RE.match(line)
+ if not m:
+ return (1, "")
+ return (0, m.group("label"))
+
+ entries_sorted = sorted(entries, key=key_for)
+ out.extend(entries_sorted)
+ out.extend(lines[i:])
+
+ return "".join(out)
+
+
+def run_checks_list(
+ inp: Optional[str], out_path: Optional[str], fix: bool
+) -> int:
+ if not inp:
+ inp = os.path.normpath(
+ os.path.join(
+ script_dir(),
+ "..",
+ "..",
+ "docs",
+ "clang-tidy",
+ "checks",
+ "list.rst",
+ )
+ )
+ lines = read_text(inp)
+ normalized = normalize_list_rst(lines)
+ if fix and out_path:
+ sys.stderr.write("error: --fix cannot be used together with --output\n")
+ return 2
+ if fix:
+ original = "".join(lines)
+ if original != normalized:
+ write_text(inp, normalized)
+ return 0
+ if out_path:
+ write_text(out_path, normalized)
+ return 0
+ sys.stdout.write(normalized)
+ return 0
+
+
+def find_heading(lines: Sequence[str], title: str) -> Optional[int]:
+ for i in range(len(lines) - 1):
+ if lines[i].rstrip("\n") == title:
+ underline = lines[i + 1].rstrip("\n")
+ if (
+ underline
+ and set(underline) == {"^"}
+ and len(underline) >= len(title)
+ ):
+ return i
+ return None
+
+
+def extract_label(text: str) -> str:
+ m = DOC_LABEL_RN_RE.search(text)
+ return m.group("label") if m else text
+
+
+def is_bullet_start(line: str) -> bool:
+ return line.startswith("- ")
+
+
+def collect_bullet_blocks(
+ lines: Sequence[str], start: int, end: int
+) -> Tuple[List[str], List[Tuple[str, List[str]]], List[str]]:
+ i = start
+ n = end
+ first_bullet = i
+ while first_bullet < n and not is_bullet_start(lines[first_bullet]):
+ first_bullet += 1
+ prefix = list(lines[i:first_bullet])
+
+ blocks: List[Tuple[str, List[str]]] = []
+ i = first_bullet
+ while i < n:
+ if not is_bullet_start(lines[i]):
+ break
+ bstart = i
+ i += 1
+ while i < n and not is_bullet_start(lines[i]):
+ if (
+ i + 1 < n
+ and set(lines[i + 1].rstrip("\n")) == {"^"}
+ and lines[i].strip()
+ ):
+ break
+ i += 1
+ block = list(lines[bstart:i])
+ key = extract_label(block[0])
+ blocks.append((key, block))
+
+ suffix = list(lines[i:n])
+ return prefix, blocks, suffix
+
+
+def sort_and_dedup_blocks(
+ blocks: List[Tuple[str, List[str]]], dedup: bool = False
+) -> List[List[str]]:
+ seen = set()
+ filtered: List[Tuple[str, List[str]]] = []
+ for key, block in blocks:
+ if dedup:
+ if key in seen:
+ continue
+ seen.add(key)
+ filtered.append((key, block))
+ filtered.sort(key=lambda kb: kb[0])
+ return [b for _, b in filtered]
+
+
+def normalize_release_notes(lines: List[str]) -> str:
+ sections = [
+ ("New checks", False),
+ ("New check aliases", False),
+ ("Changes in existing checks", True),
+ ]
+
+ out = list(lines)
+
+ for idx in range(len(sections) - 1, -1, -1):
+ title, dedup = sections[idx]
+ h_start = find_heading(out, title)
+
+ if h_start is None:
+ continue
+
+ sec_start = h_start + 2
+
+ if idx + 1 < len(sections):
+ next_title = sections[idx + 1][0]
+ h_end = find_heading(out, next_title)
+ if h_end is None:
+ h_end = sec_start
+ while h_end + 1 < len(out):
+ if out[h_end].strip() and set(
+ out[h_end + 1].rstrip("\n")
+ ) == {"^"}:
+ break
+ h_end += 1
+ sec_end = h_end
+ else:
+ h_end = sec_start
+ while h_end + 1 < len(out):
+ if out[h_end].strip() and set(out[h_end + 1].rstrip("\n")) == {
+ "^"
+ }:
+ break
+ h_end += 1
+ sec_end = h_end
+
+ prefix, blocks, suffix = collect_bullet_blocks(out, sec_start, sec_end)
+ sorted_blocks = sort_and_dedup_blocks(blocks, dedup=dedup)
+
+ new_section: List[str] = []
+ new_section.extend(prefix)
+ for i_b, b in enumerate(sorted_blocks):
+ if i_b > 0 and (
+ not new_section
+ or (new_section and new_section[-1].strip() != "")
+ ):
+ new_section.append("\n")
+ new_section.extend(b)
+ new_section.extend(suffix)
+
+ out = out[:sec_start] + new_section + out[sec_end:]
+
+ return "".join(out)
+
+
+def run_release_notes(
+ inp: Optional[str], out_path: Optional[str], fix: bool
+) -> int:
+ if not inp:
+ inp = os.path.normpath(
+ os.path.join(script_dir(), "..", "..", "docs", "ReleaseNotes.rst")
+ )
+ lines = read_text(inp)
+ normalized = normalize_release_notes(lines)
+ if fix and out_path:
+ sys.stderr.write("error: --fix cannot be used together with --output\n")
+ return 2
+ if fix:
+ original = "".join(lines)
+ if original != normalized:
+ write_text(inp, normalized)
+ return 0
+ if out_path:
+ write_text(out_path, normalized)
+ return 0
+ sys.stdout.write(normalized)
+ return 0
+
+
+def main(argv: List[str]) -> int:
+ ap = argparse.ArgumentParser()
+ sub = ap.add_subparsers(dest="cmd", required=True)
+
+ ap_checks = sub.add_parser(
+ "checks-list", help="normalize clang-tidy checks list.rst"
+ )
+ ap_checks.add_argument("-i", "--input", dest="inp", default=None)
+ ap_checks.add_argument("-o", "--output", dest="out", default=None)
+ ap_checks.add_argument(
+ "--fix", action="store_true", help="rewrite the input file in place"
+ )
+
+ ap_rn = sub.add_parser(
+ "release-notes", help="normalize ReleaseNotes.rst sections"
+ )
+ ap_rn.add_argument("-i", "--input", dest="inp", default=None)
+ ap_rn.add_argument("-o", "--output", dest="out", default=None)
+ ap_rn.add_argument(
+ "--fix", action="store_true", help="rewrite the input file in place"
+ )
+
+ args = ap.parse_args(argv)
+
+ if args.cmd == "checks-list":
+ return run_checks_list(args.inp, args.out, args.fix)
+ if args.cmd == "release-notes":
+ return run_release_notes(args.inp, args.out, args.fix)
+
+ ap.error("unknown command")
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
diff --git a/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp b/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp
new file mode 100644
index 0000000000000..4a2598b93942b
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp
@@ -0,0 +1,6 @@
+// RUN: %python %S/../../../clang-tidy/tool/clang-tidy-alphabetical-order-check.py checks-list -i %S/../../../docs/clang-tidy/checks/list.rst -o %t.list
+// RUN: diff --strip-trailing-cr %t.list \
+// RUN: %S/../../../docs/clang-tidy/checks/list.rst
+
+// RUN: %python %S/../../../clang-tidy/tool/clang-tidy-alphabetical-order-check.py release-notes -i %S/../../../docs/ReleaseNotes.rst -o %t.rn
+// RUN: diff --strip-trailing-cr %t.rn %S/../../../docs/ReleaseNotes.rst
diff --git a/clang-tools-extra/test/lit.cfg.py b/clang-tools-extra/test/lit.cfg.py
index c1da37d61bd61..c39ea29329674 100644
--- a/clang-tools-extra/test/lit.cfg.py
+++ b/clang-tools-extra/test/lit.cfg.py
@@ -57,6 +57,7 @@
if config.clang_tidy_custom_check:
config.available_features.add("custom-check")
python_exec = shlex.quote(config.python_executable)
+config.substitutions.append(("%python", python_exec))
check_clang_tidy = os.path.join(
config.test_source_root, "clang-tidy", "check_clang_tidy.py"
)
>From c08b734ec6337afb2fbfb45fb7574ea8cf82add1 Mon Sep 17 00:00:00 2001
From: mtx <mitchell.xu2 at gmail.com>
Date: Sun, 2 Nov 2025 23:25:39 +0800
Subject: [PATCH 2/6] fix format
---
.../clang-tidy-alphabetical-order-check.py | 41 +++++++++----------
.../infrastructure/alphabetical-order.cpp | 7 +---
2 files changed, 21 insertions(+), 27 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
index 321663bb7d577..680f21ec0e02c 100644
--- a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
+++ b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
@@ -1,6 +1,18 @@
#!/usr/bin/env python3
+#
+# ===-----------------------------------------------------------------------===#
+#
+# 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
+#
+# ===-----------------------------------------------------------------------===#
"""
+
+ClangTidy Alphabetical Order Checker
+====================================
+
Normalize clang-tidy docs with deterministic sorting for linting/tests.
Subcommands:
@@ -80,9 +92,7 @@ def key_for(line: str):
return "".join(out)
-def run_checks_list(
- inp: Optional[str], out_path: Optional[str], fix: bool
-) -> int:
+def run_checks_list(inp: Optional[str], out_path: Optional[str], fix: bool) -> int:
if not inp:
inp = os.path.normpath(
os.path.join(
@@ -116,11 +126,7 @@ def find_heading(lines: Sequence[str], title: str) -> Optional[int]:
for i in range(len(lines) - 1):
if lines[i].rstrip("\n") == title:
underline = lines[i + 1].rstrip("\n")
- if (
- underline
- and set(underline) == {"^"}
- and len(underline) >= len(title)
- ):
+ if underline and set(underline) == {"^"} and len(underline) >= len(title):
return i
return None
@@ -206,18 +212,14 @@ def normalize_release_notes(lines: List[str]) -> str:
if h_end is None:
h_end = sec_start
while h_end + 1 < len(out):
- if out[h_end].strip() and set(
- out[h_end + 1].rstrip("\n")
- ) == {"^"}:
+ if out[h_end].strip() and set(out[h_end + 1].rstrip("\n")) == {"^"}:
break
h_end += 1
sec_end = h_end
else:
h_end = sec_start
while h_end + 1 < len(out):
- if out[h_end].strip() and set(out[h_end + 1].rstrip("\n")) == {
- "^"
- }:
+ if out[h_end].strip() and set(out[h_end + 1].rstrip("\n")) == {"^"}:
break
h_end += 1
sec_end = h_end
@@ -229,8 +231,7 @@ def normalize_release_notes(lines: List[str]) -> str:
new_section.extend(prefix)
for i_b, b in enumerate(sorted_blocks):
if i_b > 0 and (
- not new_section
- or (new_section and new_section[-1].strip() != "")
+ not new_section or (new_section and new_section[-1].strip() != "")
):
new_section.append("\n")
new_section.extend(b)
@@ -241,9 +242,7 @@ def normalize_release_notes(lines: List[str]) -> str:
return "".join(out)
-def run_release_notes(
- inp: Optional[str], out_path: Optional[str], fix: bool
-) -> int:
+def run_release_notes(inp: Optional[str], out_path: Optional[str], fix: bool) -> int:
if not inp:
inp = os.path.normpath(
os.path.join(script_dir(), "..", "..", "docs", "ReleaseNotes.rst")
@@ -278,9 +277,7 @@ def main(argv: List[str]) -> int:
"--fix", action="store_true", help="rewrite the input file in place"
)
- ap_rn = sub.add_parser(
- "release-notes", help="normalize ReleaseNotes.rst sections"
- )
+ ap_rn = sub.add_parser("release-notes", help="normalize ReleaseNotes.rst sections")
ap_rn.add_argument("-i", "--input", dest="inp", default=None)
ap_rn.add_argument("-o", "--output", dest="out", default=None)
ap_rn.add_argument(
diff --git a/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp b/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp
index 4a2598b93942b..0ac1484a00561 100644
--- a/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp
+++ b/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp
@@ -1,6 +1,3 @@
-// RUN: %python %S/../../../clang-tidy/tool/clang-tidy-alphabetical-order-check.py checks-list -i %S/../../../docs/clang-tidy/checks/list.rst -o %t.list
-// RUN: diff --strip-trailing-cr %t.list \
-// RUN: %S/../../../docs/clang-tidy/checks/list.rst
+// RUN: %python %S/../../../clang-tidy/tool/clang-tidy-alphabetical-order-check.py checks-list -i %S/../../../docs/clang-tidy/checks/list.rst | diff --strip-trailing-cr - %S/../../../docs/clang-tidy/checks/list.rst
-// RUN: %python %S/../../../clang-tidy/tool/clang-tidy-alphabetical-order-check.py release-notes -i %S/../../../docs/ReleaseNotes.rst -o %t.rn
-// RUN: diff --strip-trailing-cr %t.rn %S/../../../docs/ReleaseNotes.rst
+// RUN: %python %S/../../../clang-tidy/tool/clang-tidy-alphabetical-order-check.py release-notes -i %S/../../../docs/ReleaseNotes.rst | diff --strip-trailing-cr - %S/../../../docs/ReleaseNotes.rst
>From 20b66111f4ba000a5cbcba45b62674e42a4f1f1e Mon Sep 17 00:00:00 2001
From: mtx <mitchell.xu2 at gmail.com>
Date: Sun, 2 Nov 2025 23:28:13 +0800
Subject: [PATCH 3/6] ~
---
.../clang-tidy/tool/clang-tidy-alphabetical-order-check.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
index 680f21ec0e02c..fbb55efa536ff 100644
--- a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
+++ b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
@@ -231,7 +231,7 @@ def normalize_release_notes(lines: List[str]) -> str:
new_section.extend(prefix)
for i_b, b in enumerate(sorted_blocks):
if i_b > 0 and (
- not new_section or (new_section and new_section[-1].strip() != "")
+ not new_section or (new_section and new_section[-1].strip() != "")
):
new_section.append("\n")
new_section.extend(b)
>From c7dc5e99207804473cdee184bc8b435874fbf5c4 Mon Sep 17 00:00:00 2001
From: mtx <mitchell.xu2 at gmail.com>
Date: Mon, 3 Nov 2025 11:36:52 +0800
Subject: [PATCH 4/6] refactor
---
.../clang-tidy-alphabetical-order-check.py | 259 +++++++++---------
.../infrastructure/alphabetical-order.cpp | 6 +-
2 files changed, 126 insertions(+), 139 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
index fbb55efa536ff..58d93dcf31235 100644
--- a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
+++ b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
@@ -15,18 +15,13 @@
Normalize clang-tidy docs with deterministic sorting for linting/tests.
-Subcommands:
- - checks-list: Sort entries in docs/clang-tidy/checks/list.rst csv-table.
- - release-notes: Sort key sections in docs/ReleaseNotes.rst and de-duplicate
- entries in "Changes in existing checks".
-
-Usage:
- clang-tidy-alphabetical-order-check.py <subcommand> [-i <input rst>] [-o <output rst>] [--fix]
+Behavior:
+- Sort entries in docs/clang-tidy/checks/list.rst csv-table.
+- Sort key sections in docs/ReleaseNotes.rst. Does not remove duplicate
+ entries; developers should merge duplicates manually when needed.
Flags:
- -i/--input Input file.
- -o/--output Write normalized content here; omit to write to stdout.
- --fix Rewrite the input file in place. Cannot be combined with -o/--output.
+ -o/--output Write normalized content to this path instead of updating docs.
"""
import argparse
@@ -36,7 +31,13 @@
import sys
from typing import List, Optional, Sequence, Tuple
+# Matches a :doc:`label <path>` or :doc:`label` reference anywhere in text and
+# captures the label. Used to sort bullet items alphabetically in ReleaseNotes
+# items by their label.
DOC_LABEL_RN_RE = re.compile(r":doc:`(?P<label>[^`<]+)\s*(?:<[^>]+>)?`")
+
+# Matches a single csv-table row line in list.rst that begins with a :doc:
+# reference, capturing the label. Used to extract the sort key per row.
DOC_LINE_RE = re.compile(r"^\s*:doc:`(?P<label>[^`<]+?)\s*<[^>]+>`.*$")
@@ -55,6 +56,12 @@ def write_text(path: str, content: str) -> None:
def normalize_list_rst(lines: List[str]) -> str:
+ """Return normalized content of checks list.rst from given lines.
+
+ Input: full file content split into lines.
+ Output: single string with csv-table rows sorted by :doc: label while
+ preserving header/leading comments and trailing content.
+ """
out: List[str] = []
i = 0
n = len(lines)
@@ -92,37 +99,15 @@ def key_for(line: str):
return "".join(out)
-def run_checks_list(inp: Optional[str], out_path: Optional[str], fix: bool) -> int:
- if not inp:
- inp = os.path.normpath(
- os.path.join(
- script_dir(),
- "..",
- "..",
- "docs",
- "clang-tidy",
- "checks",
- "list.rst",
- )
- )
- lines = read_text(inp)
- normalized = normalize_list_rst(lines)
- if fix and out_path:
- sys.stderr.write("error: --fix cannot be used together with --output\n")
- return 2
- if fix:
- original = "".join(lines)
- if original != normalized:
- write_text(inp, normalized)
- return 0
- if out_path:
- write_text(out_path, normalized)
- return 0
- sys.stdout.write(normalized)
- return 0
+def find_heading(lines: Sequence[str], title: str) -> Optional[int]:
+ """Find heading start index for a section underlined with ^ characters.
+ The function looks for a line equal to `title` followed by a line that
+ consists solely of ^, which matches the ReleaseNotes style for subsection
+ headings used here.
-def find_heading(lines: Sequence[str], title: str) -> Optional[int]:
+ Returns index of the title line, or None if not found.
+ """
for i in range(len(lines) - 1):
if lines[i].rstrip("\n") == title:
underline = lines[i + 1].rstrip("\n")
@@ -173,125 +158,125 @@ def collect_bullet_blocks(
return prefix, blocks, suffix
-def sort_and_dedup_blocks(
- blocks: List[Tuple[str, List[str]]], dedup: bool = False
-) -> List[List[str]]:
- seen = set()
- filtered: List[Tuple[str, List[str]]] = []
- for key, block in blocks:
- if dedup:
- if key in seen:
- continue
- seen.add(key)
- filtered.append((key, block))
- filtered.sort(key=lambda kb: kb[0])
- return [b for _, b in filtered]
+def sort_blocks(blocks: List[Tuple[str, List[str]]]) -> List[List[str]]:
+ """Return blocks sorted deterministically by their extracted label.
+ Duplicates are preserved; merging is left to authors to handle manually.
+ """
+ return [b for _, b in sorted(blocks, key=lambda kb: kb[0])]
-def normalize_release_notes(lines: List[str]) -> str:
- sections = [
- ("New checks", False),
- ("New check aliases", False),
- ("Changes in existing checks", True),
- ]
- out = list(lines)
+def _find_section_bounds(
+ lines: Sequence[str], title: str, next_title: Optional[str]
+) -> Optional[Tuple[int, int, int]]:
+ """Return (h_start, sec_start, sec_end) for section `title`.
- for idx in range(len(sections) - 1, -1, -1):
- title, dedup = sections[idx]
- h_start = find_heading(out, title)
-
- if h_start is None:
- continue
-
- sec_start = h_start + 2
-
- if idx + 1 < len(sections):
- next_title = sections[idx + 1][0]
- h_end = find_heading(out, next_title)
- if h_end is None:
- h_end = sec_start
- while h_end + 1 < len(out):
- if out[h_end].strip() and set(out[h_end + 1].rstrip("\n")) == {"^"}:
- break
- h_end += 1
- sec_end = h_end
- else:
+ - h_start: index of the section title line
+ - sec_start: index of the first content line after underline
+ - sec_end: index of the first line of the next section title (or end)
+ """
+ h_start = find_heading(lines, title)
+ if h_start is None:
+ return None
+
+ sec_start = h_start + 2
+
+ # Determine end of section either from next_title or by scanning.
+ if next_title is not None:
+ h_end = find_heading(lines, next_title)
+ if h_end is None:
+ # Scan forward to the next heading-like underline.
h_end = sec_start
- while h_end + 1 < len(out):
- if out[h_end].strip() and set(out[h_end + 1].rstrip("\n")) == {"^"}:
+ while h_end + 1 < len(lines):
+ if lines[h_end].strip() and set(lines[h_end + 1].rstrip("\n")) == {"^"}:
break
h_end += 1
- sec_end = h_end
+ sec_end = h_end
+ else:
+ # Scan to end or until a heading underline is found.
+ h_end = sec_start
+ while h_end + 1 < len(lines):
+ if lines[h_end].strip() and set(lines[h_end + 1].rstrip("\n")) == {"^"}:
+ break
+ h_end += 1
+ sec_end = h_end
- prefix, blocks, suffix = collect_bullet_blocks(out, sec_start, sec_end)
- sorted_blocks = sort_and_dedup_blocks(blocks, dedup=dedup)
+ return h_start, sec_start, sec_end
- new_section: List[str] = []
- new_section.extend(prefix)
- for i_b, b in enumerate(sorted_blocks):
- if i_b > 0 and (
- not new_section or (new_section and new_section[-1].strip() != "")
- ):
- new_section.append("\n")
- new_section.extend(b)
- new_section.extend(suffix)
- out = out[:sec_start] + new_section + out[sec_end:]
+def _normalize_release_notes_section(
+ lines: List[str], title: str, next_title: Optional[str]
+) -> List[str]:
+ """Normalize a single release-notes section and return updated lines."""
+ bounds = _find_section_bounds(lines, title, next_title)
+ if bounds is None:
+ return lines
+ _, sec_start, sec_end = bounds
+
+ prefix, blocks, suffix = collect_bullet_blocks(lines, sec_start, sec_end)
+ sorted_blocks = sort_blocks(blocks)
+
+ new_section: List[str] = []
+ new_section.extend(prefix)
+ for i_b, b in enumerate(sorted_blocks):
+ if i_b > 0 and (not new_section or (new_section and new_section[-1].strip() != "")):
+ new_section.append("\n")
+ new_section.extend(b)
+ new_section.extend(suffix)
+
+ return lines[:sec_start] + new_section + lines[sec_end:]
+
+
+def normalize_release_notes(lines: List[str]) -> str:
+ sections = ["New checks", "New check aliases", "Changes in existing checks"]
+
+ out = list(lines)
+
+ for idx in range(len(sections) - 1, -1, -1):
+ title = sections[idx]
+ next_title = sections[idx + 1] if idx + 1 < len(sections) else None
+ out = _normalize_release_notes_section(out, title, next_title)
return "".join(out)
-def run_release_notes(inp: Optional[str], out_path: Optional[str], fix: bool) -> int:
- if not inp:
- inp = os.path.normpath(
- os.path.join(script_dir(), "..", "..", "docs", "ReleaseNotes.rst")
- )
- lines = read_text(inp)
- normalized = normalize_release_notes(lines)
- if fix and out_path:
- sys.stderr.write("error: --fix cannot be used together with --output\n")
- return 2
- if fix:
- original = "".join(lines)
- if original != normalized:
- write_text(inp, normalized)
- return 0
- if out_path:
- write_text(out_path, normalized)
- return 0
- sys.stdout.write(normalized)
- return 0
+def _default_paths() -> Tuple[str, str]:
+ base = os.path.normpath(os.path.join(script_dir(), "..", ".."))
+ list_doc = os.path.join(base, "docs", "clang-tidy", "checks", "list.rst")
+ rn_doc = os.path.join(base, "docs", "ReleaseNotes.rst")
+ return list_doc, rn_doc
def main(argv: List[str]) -> int:
ap = argparse.ArgumentParser()
- sub = ap.add_subparsers(dest="cmd", required=True)
-
- ap_checks = sub.add_parser(
- "checks-list", help="normalize clang-tidy checks list.rst"
- )
- ap_checks.add_argument("-i", "--input", dest="inp", default=None)
- ap_checks.add_argument("-o", "--output", dest="out", default=None)
- ap_checks.add_argument(
- "--fix", action="store_true", help="rewrite the input file in place"
- )
-
- ap_rn = sub.add_parser("release-notes", help="normalize ReleaseNotes.rst sections")
- ap_rn.add_argument("-i", "--input", dest="inp", default=None)
- ap_rn.add_argument("-o", "--output", dest="out", default=None)
- ap_rn.add_argument(
- "--fix", action="store_true", help="rewrite the input file in place"
- )
-
+ ap.add_argument("-o", "--output", dest="out", default=None)
args = ap.parse_args(argv)
- if args.cmd == "checks-list":
- return run_checks_list(args.inp, args.out, args.fix)
- if args.cmd == "release-notes":
- return run_release_notes(args.inp, args.out, args.fix)
+ list_doc, rn_doc = _default_paths()
- ap.error("unknown command")
+ if args.out:
+ out_path = args.out
+ out_lower = os.path.basename(out_path).lower()
+ if "release" in out_lower:
+ lines = read_text(rn_doc)
+ normalized = normalize_release_notes(lines)
+ write_text(out_path, normalized)
+ return 0
+ else:
+ lines = read_text(list_doc)
+ normalized = normalize_list_rst(lines)
+ write_text(out_path, normalized)
+ return 0
+
+ list_lines = read_text(list_doc)
+ rn_lines = read_text(rn_doc)
+ list_norm = normalize_list_rst(list_lines)
+ rn_norm = normalize_release_notes(rn_lines)
+ if "".join(list_lines) != list_norm:
+ write_text(list_doc, list_norm)
+ if "".join(rn_lines) != rn_norm:
+ write_text(rn_doc, rn_norm)
+ return 0
if __name__ == "__main__":
diff --git a/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp b/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp
index 0ac1484a00561..c238884007595 100644
--- a/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp
+++ b/clang-tools-extra/test/clang-tidy/infrastructure/alphabetical-order.cpp
@@ -1,3 +1,5 @@
-// RUN: %python %S/../../../clang-tidy/tool/clang-tidy-alphabetical-order-check.py checks-list -i %S/../../../docs/clang-tidy/checks/list.rst | diff --strip-trailing-cr - %S/../../../docs/clang-tidy/checks/list.rst
+// RUN: %python %S/../../../clang-tidy/tool/clang-tidy-alphabetical-order-check.py -o %t.clang-tidy-checks-list.rst
+// RUN: diff --strip-trailing-cr %t.clang-tidy-checks-list.rst %S/../../../docs/clang-tidy/checks/list.rst
-// RUN: %python %S/../../../clang-tidy/tool/clang-tidy-alphabetical-order-check.py release-notes -i %S/../../../docs/ReleaseNotes.rst | diff --strip-trailing-cr - %S/../../../docs/ReleaseNotes.rst
+// RUN: %python %S/../../../clang-tidy/tool/clang-tidy-alphabetical-order-check.py -o %t.ReleaseNotes.rst
+// RUN: diff --strip-trailing-cr %t.ReleaseNotes.rst %S/../../../docs/ReleaseNotes.rst
>From 555beb058a7ac8118c0603690a4570dc51b74733 Mon Sep 17 00:00:00 2001
From: mtx <mitchell.xu2 at gmail.com>
Date: Mon, 3 Nov 2025 11:51:23 +0800
Subject: [PATCH 5/6] fix format
---
.../clang-tidy/tool/clang-tidy-alphabetical-order-check.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
index 58d93dcf31235..80e0450c5987c 100644
--- a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
+++ b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
@@ -219,7 +219,9 @@ def _normalize_release_notes_section(
new_section: List[str] = []
new_section.extend(prefix)
for i_b, b in enumerate(sorted_blocks):
- if i_b > 0 and (not new_section or (new_section and new_section[-1].strip() != "")):
+ if i_b > 0 and (
+ not new_section or (new_section and new_section[-1].strip() != "")
+ ):
new_section.append("\n")
new_section.extend(b)
new_section.extend(suffix)
>From 66c831b396b102018c4dd875cc0074c51357c323 Mon Sep 17 00:00:00 2001
From: mtx <mitchell.xu2 at gmail.com>
Date: Mon, 3 Nov 2025 14:18:51 +0800
Subject: [PATCH 6/6] fix csv-table related issues
---
.../clang-tidy-alphabetical-order-check.py | 50 +++++++++----------
1 file changed, 25 insertions(+), 25 deletions(-)
diff --git a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
index 80e0450c5987c..5ebbbb75d92dc 100644
--- a/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
+++ b/clang-tools-extra/clang-tidy/tool/clang-tidy-alphabetical-order-check.py
@@ -59,32 +59,12 @@ def normalize_list_rst(lines: List[str]) -> str:
"""Return normalized content of checks list.rst from given lines.
Input: full file content split into lines.
- Output: single string with csv-table rows sorted by :doc: label while
- preserving header/leading comments and trailing content.
+ Output: single string with all csv-table rows sorted by :doc: label while
+ preserving non-table content and table options/headers.
"""
out: List[str] = []
i = 0
n = len(lines)
- while i < n:
- out.append(lines[i])
- if lines[i].lstrip().startswith(".. csv-table::"):
- i += 1
- break
- i += 1
-
- while i < n and (lines[i].startswith(" ") or lines[i].strip() == ""):
- if DOC_LINE_RE.match(lines[i]):
- break
- out.append(lines[i])
- i += 1
-
- entries: List[str] = []
- while i < n and lines[i].startswith(" "):
- if DOC_LINE_RE.match(lines[i]):
- entries.append(lines[i])
- else:
- entries.append(lines[i])
- i += 1
def key_for(line: str):
m = DOC_LINE_RE.match(line)
@@ -92,9 +72,29 @@ def key_for(line: str):
return (1, "")
return (0, m.group("label"))
- entries_sorted = sorted(entries, key=key_for)
- out.extend(entries_sorted)
- out.extend(lines[i:])
+ while i < n:
+ line = lines[i]
+ if line.lstrip().startswith(".. csv-table::"):
+ out.append(line)
+ i += 1
+
+ while i < n and (lines[i].startswith(" ") or lines[i].strip() == ""):
+ if DOC_LINE_RE.match(lines[i]):
+ break
+ out.append(lines[i])
+ i += 1
+
+ entries: List[str] = []
+ while i < n and lines[i].startswith(" "):
+ entries.append(lines[i])
+ i += 1
+
+ entries_sorted = sorted(entries, key=key_for)
+ out.extend(entries_sorted)
+ continue
+
+ out.append(line)
+ i += 1
return "".join(out)
More information about the cfe-commits
mailing list