https://github.com/zeyi2 updated https://github.com/llvm/llvm-project/pull/166072
>From 7b4e4172fc3cea8e8194aa3544f2c2ec30e3616a Mon Sep 17 00:00:00 2001 From: mtx <[email protected]> Date: Sun, 2 Nov 2025 22:56:53 +0800 Subject: [PATCH 1/3] [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 <[email protected]> Date: Sun, 2 Nov 2025 23:25:39 +0800 Subject: [PATCH 2/3] 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 <[email protected]> Date: Sun, 2 Nov 2025 23:28:13 +0800 Subject: [PATCH 3/3] ~ --- .../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) _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
