Files
TheRock/compiler/amd-llvm/clang-tools-extra/clang-tidy/tool/check_alphabetical_order.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

431 lines
14 KiB
Python

#!/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
#
# ===-----------------------------------------------------------------------===#
"""
Clang-Tidy Alphabetical Order Checker
=====================================
Normalize Clang-Tidy documentation with deterministic sorting for linting/tests.
Behavior:
- Sort entries in docs/clang-tidy/checks/list.rst csv-table.
- Sort key sections in docs/ReleaseNotes.rst.
- Detect duplicated entries in 'Changes in existing checks'.
Flags:
-o/--output Write normalized content to this path instead of updating docs.
"""
import argparse
from collections import defaultdict
import io
from operator import itemgetter
import os
import re
import sys
from typing import (
DefaultDict,
Final,
Iterable,
List,
NamedTuple,
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: Final = 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: Final = re.compile(r"^\s*:doc:`(?P<label>[^`<]+?)\s*<[^>]+>`.*$")
EXTRA_DIR: Final = os.path.join(os.path.dirname(__file__), "../..")
DOCS_DIR: Final = os.path.join(EXTRA_DIR, "docs")
CLANG_TIDY_DOCS_DIR: Final = os.path.join(DOCS_DIR, "clang-tidy")
CHECKS_DOCS_DIR: Final = os.path.join(CLANG_TIDY_DOCS_DIR, "checks")
LIST_DOC: Final = os.path.join(CHECKS_DOCS_DIR, "list.rst")
RELEASE_NOTES_DOC: Final = os.path.join(DOCS_DIR, "ReleaseNotes.rst")
# Label extracted from :doc:`...`.
CheckLabel = str
Lines = List[str]
BulletBlock = List[str]
# Pair of the extracted label and its block
BulletItem = Tuple[CheckLabel, BulletBlock]
# Index of the first line of a bullet block within the full lines list.
BulletStart = int
# All occurrences for a given label.
DuplicateOccurrences = List[Tuple[BulletStart, BulletBlock]]
class BulletBlocks(NamedTuple):
"""Structured result of parsing a bullet-list section.
- prefix: lines before the first bullet within the section range.
- blocks: list of (label, block-lines) pairs for each bullet block.
- suffix: lines after the last bullet within the section range.
"""
prefix: Lines
blocks: List[BulletItem]
suffix: Lines
class ScannedBlocks(NamedTuple):
"""Result of scanning bullet blocks within a section range.
- blocks_with_pos: list of (start_index, block_lines) for each bullet block.
- next_index: index where scanning stopped; start of the suffix region.
"""
blocks_with_pos: List[Tuple[BulletStart, BulletBlock]]
next_index: int
def _scan_bullet_blocks(lines: Sequence[str], start: int, end: int) -> ScannedBlocks:
"""Scan consecutive bullet blocks and return (blocks_with_pos, next_index).
Each entry in blocks_with_pos is a tuple of (start_index, block_lines).
next_index is the index where scanning stopped (start of suffix).
"""
i = start
n = end
blocks_with_pos: List[Tuple[BulletStart, BulletBlock]] = []
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: BulletBlock = list(lines[bstart:i])
blocks_with_pos.append((bstart, block))
return ScannedBlocks(blocks_with_pos, i)
def read_text(path: str) -> str:
with io.open(path, "r", encoding="utf-8") as f:
return f.read()
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(lines: Sequence[str]) -> List[str]:
"""Return normalized content of checks list.rst as a list of lines."""
out: List[str] = []
i = 0
n = len(lines)
def check_name(line: str) -> Tuple[int, CheckLabel]:
if m := DOC_LINE_RE.match(line):
return (0, m.group("label"))
return (1, "")
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=check_name)
out.extend(entries_sorted)
continue
out.append(line)
i += 1
return out
def normalize_list_rst(data: str) -> str:
"""Normalize list.rst content and return a string."""
lines = data.splitlines(True)
return "".join(_normalize_list_rst_lines(lines))
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.
Returns index of the title line, or None if not found.
"""
for i in range(len(lines) - 1):
if lines[i].rstrip("\n") == title:
if (
(underline := lines[i + 1].rstrip("\n"))
and set(underline) == {"^"}
and len(underline) == len(title)
):
return i
return None
def extract_label(text: str) -> str:
if m := DOC_LABEL_RN_RE.search(text):
return m.group("label").strip()
return text
def _is_bullet_start(line: str) -> bool:
return line.startswith("- ")
def _parse_bullet_blocks(lines: Sequence[str], start: int, end: int) -> BulletBlocks:
i = start
n = end
first_bullet = i
while first_bullet < n and not _is_bullet_start(lines[first_bullet]):
first_bullet += 1
prefix: Lines = list(lines[i:first_bullet])
blocks: List[BulletItem] = []
res = _scan_bullet_blocks(lines, first_bullet, n)
for _, block in res.blocks_with_pos:
key: CheckLabel = extract_label("".join(block))
blocks.append((key, block))
suffix: Lines = list(lines[res.next_index : n])
return BulletBlocks(prefix, blocks, suffix)
def sort_blocks(blocks: Iterable[BulletItem]) -> List[BulletBlock]:
"""Return blocks sorted deterministically by their extracted label.
Duplicates are preserved; merging is left to authors to handle manually.
"""
return list(map(itemgetter(1), sorted(blocks, key=itemgetter(0))))
def find_duplicate_entries(
lines: Sequence[str], title: str
) -> List[Tuple[CheckLabel, DuplicateOccurrences]]:
"""Return detailed duplicate info as (key, [(start_idx, block_lines), ...]).
start_idx is the 0-based index of the first line of the bullet block in
the original lines list. Only keys with more than one occurrence are
returned, and occurrences are listed in the order they appear.
"""
bounds = _find_section_bounds(lines, title, None)
if bounds is None:
return []
_, sec_start, sec_end = bounds
i = sec_start
n = sec_end
while i < n and not _is_bullet_start(lines[i]):
i += 1
blocks_with_pos: List[Tuple[CheckLabel, BulletStart, BulletBlock]] = []
res = _scan_bullet_blocks(lines, i, n)
for bstart, block in res.blocks_with_pos:
key = extract_label(block[0])
blocks_with_pos.append((key, bstart, block))
grouped: DefaultDict[CheckLabel, DuplicateOccurrences] = defaultdict(list)
for key, start, block in blocks_with_pos:
grouped[key].append((start, block))
result: List[Tuple[CheckLabel, DuplicateOccurrences]] = []
for key, occs in grouped.items():
if len(occs) > 1:
result.append((key, occs))
result.sort(key=itemgetter(0))
return result
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`.
- 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)
"""
if (h_start := find_heading(lines, title)) 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:
if (h_end := find_heading(lines, next_title)) is None:
# Scan forward to the next heading-like underline.
h_end = sec_start
while h_end < len(lines):
if (
h_end + 1 < len(lines)
and lines[h_end].strip()
and set(lines[h_end + 1].rstrip("\n")) == {"^"}
):
break
h_end += 1
sec_end = h_end
else:
# Scan to end or until a heading underline is found.
h_end = sec_start
while h_end < len(lines):
if (
h_end + 1 < len(lines)
and lines[h_end].strip()
and set(lines[h_end + 1].rstrip("\n")) == {"^"}
):
break
h_end += 1
sec_end = h_end
return h_start, sec_start, sec_end
def _normalize_release_notes_section(
lines: Sequence[str], title: str, next_title: Optional[str]
) -> List[str]:
"""Normalize a single release-notes section and return updated lines."""
if (bounds := _find_section_bounds(lines, title, next_title)) is None:
return list(lines)
_, sec_start, sec_end = bounds
prefix, blocks, suffix = _parse_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 list(lines[:sec_start]) + new_section + list(lines[sec_end:])
def normalize_release_notes(lines: Sequence[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 _emit_duplicate_report(lines: Sequence[str], title: str) -> Optional[str]:
if not (dups_detail := find_duplicate_entries(lines, title)):
return None
out: List[str] = []
out.append(f"Error: Duplicate entries in '{title}'.\n")
out.append("\nPlease merge these entries into a single bullet point.\n")
for key, occs in dups_detail:
out.append(f"\n-- Duplicate: {key}\n")
for start_idx, block in occs:
out.append(f"- At line {start_idx + 1}:\n")
out.append("".join(block))
if not (block and block[-1].endswith("\n")):
out.append("\n")
return "".join(out)
def process_release_notes(out_path: str, rn_doc: str) -> int:
text = read_text(rn_doc)
lines = text.splitlines(True)
normalized = normalize_release_notes(lines)
write_text(out_path, normalized)
# Prefer reporting ordering issues first; let diff fail the test.
if text != normalized:
sys.stderr.write(
"\nEntries in 'clang-tools-extra/docs/ReleaseNotes.rst' are not alphabetically sorted.\n"
"Fix the ordering by applying diff printed below.\n\n"
)
return 0
# Ordering is clean then enforce duplicates.
if report := _emit_duplicate_report(lines, "Changes in existing checks"):
sys.stderr.write(report)
return 3
return 0
def process_checks_list(out_path: str, list_doc: str) -> int:
text = read_text(list_doc)
normalized = normalize_list_rst(text)
if text != normalized:
sys.stderr.write(
"\nChecks in 'clang-tools-extra/docs/clang-tidy/checks/list.rst' csv-table are not alphabetically sorted.\n"
"Fix the ordering by applying diff printed below.\n\n"
)
write_text(out_path, normalized)
return 0
def main(argv: Sequence[str]) -> int:
ap = argparse.ArgumentParser()
ap.add_argument("-o", "--output", dest="out", default=None)
args = ap.parse_args(argv)
list_doc, rn_doc = (os.path.normpath(LIST_DOC), os.path.normpath(RELEASE_NOTES_DOC))
if args.out:
out_path = args.out
out_lower = os.path.basename(out_path).lower()
if "release" in out_lower:
return process_release_notes(out_path, rn_doc)
else:
return process_checks_list(out_path, list_doc)
process_checks_list(list_doc, list_doc)
return process_release_notes(rn_doc, rn_doc)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))