13Clang-Tidy Alphabetical Order Checker
14=====================================
16Normalize Clang-Tidy documentation with deterministic sorting for linting/tests.
19- Sort entries in docs/clang-tidy/checks/list.rst csv-table.
20- Sort key sections in docs/ReleaseNotes.rst.
21- Detect duplicated entries in 'Changes in existing checks'.
24 -o/--output Write normalized content to this path instead of updating docs.
28from collections
import defaultdict
30from operator
import itemgetter
48DOC_LABEL_RN_RE: Final = re.compile(
r":doc:`(?P<label>[^`<]+)\s*(?:<[^>]+>)?`")
52DOC_LINE_RE: Final = re.compile(
r"^\s*:doc:`(?P<label>[^`<]+?)\s*<[^>]+>`.*$")
55EXTRA_DIR: Final = os.path.join(os.path.dirname(__file__),
"../..")
56DOCS_DIR: Final = os.path.join(EXTRA_DIR,
"docs")
57CLANG_TIDY_DOCS_DIR: Final = os.path.join(DOCS_DIR,
"clang-tidy")
58CHECKS_DOCS_DIR: Final = os.path.join(CLANG_TIDY_DOCS_DIR,
"checks")
59LIST_DOC: Final = os.path.join(CHECKS_DOCS_DIR,
"list.rst")
60RELEASE_NOTES_DOC: Final = os.path.join(DOCS_DIR,
"ReleaseNotes.rst")
66BulletBlock = List[str]
69BulletItem = Tuple[CheckLabel, BulletBlock]
75DuplicateOccurrences = List[Tuple[BulletStart, BulletBlock]]
79 """Structured result of parsing a bullet-list section.
81 - prefix: lines before the first bullet within the section range.
82 - blocks: list of (label, block-lines) pairs for each bullet block.
83 - suffix: lines after the last bullet within the section range.
87 blocks: List[BulletItem]
92 """Result of scanning bullet blocks within a section range.
94 - blocks_with_pos: list of (start_index, block_lines) for each bullet block.
95 - next_index: index where scanning stopped; start of the suffix region.
98 blocks_with_pos: List[Tuple[BulletStart, BulletBlock]]
103 """Scan consecutive bullet blocks and return (blocks_with_pos, next_index).
105 Each entry in blocks_with_pos is a tuple of (start_index, block_lines).
106 next_index is the index where scanning stopped (start of suffix).
110 blocks_with_pos: List[Tuple[BulletStart, BulletBlock]] = []
119 and set(lines[i + 1].rstrip(
"\n")) == {
"^"}
124 block: BulletBlock = list(lines[bstart:i])
125 blocks_with_pos.append((bstart, block))
130 with io.open(path,
"r", encoding=
"utf-8")
as f:
135 with io.open(path,
"w", encoding=
"utf-8", newline=
"")
as f:
140 """Return normalized content of checks list.rst as a list of lines."""
145 def check_name(line: str) -> Tuple[int, CheckLabel]:
146 if m := DOC_LINE_RE.match(line):
147 return (0, m.group(
"label"))
152 if line.lstrip().startswith(
".. csv-table::"):
156 while i < n
and (lines[i].startswith(
" ")
or lines[i].strip() ==
""):
157 if DOC_LINE_RE.match(lines[i]):
162 entries: List[str] = []
163 while i < n
and lines[i].startswith(
" "):
164 entries.append(lines[i])
167 entries_sorted = sorted(entries, key=check_name)
168 out.extend(entries_sorted)
178 """Normalize list.rst content and return a string."""
179 lines = data.splitlines(
True)
184 """Find heading start index for a section underlined with ^ characters.
186 The function looks for a line equal to `title` followed by a line that
187 consists solely of ^, which matches the ReleaseNotes style for subsection
190 Returns index of the title line, or None if not found.
192 for i
in range(len(lines) - 1):
193 if lines[i].rstrip(
"\n") == title:
195 (underline := lines[i + 1].rstrip(
"\n"))
196 and set(underline) == {
"^"}
197 and len(underline) == len(title)
204 if m := DOC_LABEL_RN_RE.search(text):
205 return m.group(
"label")
210 return line.startswith(
"- ")
219 prefix: Lines = list(lines[i:first_bullet])
221 blocks: List[BulletItem] = []
223 for _, block
in res.blocks_with_pos:
225 blocks.append((key, block))
227 suffix: Lines = list(lines[res.next_index : n])
231def sort_blocks(blocks: Iterable[BulletItem]) -> List[BulletBlock]:
232 """Return blocks sorted deterministically by their extracted label.
234 Duplicates are preserved; merging is left to authors to handle manually.
236 return list(map(itemgetter(1), sorted(blocks, key=itemgetter(0))))
240 lines: Sequence[str], title: str
241) -> List[Tuple[CheckLabel, DuplicateOccurrences]]:
242 """Return detailed duplicate info as (key, [(start_idx, block_lines), ...]).
244 start_idx is the 0-based index of the first line of the bullet block in
245 the original lines list. Only keys with more than one occurrence are
246 returned, and occurrences are listed in the order they appear.
251 _, sec_start, sec_end = bounds
259 blocks_with_pos: List[Tuple[CheckLabel, BulletStart, BulletBlock]] = []
261 for bstart, block
in res.blocks_with_pos:
263 blocks_with_pos.append((key, bstart, block))
265 grouped: DefaultDict[CheckLabel, DuplicateOccurrences] = defaultdict(list)
266 for key, start, block
in blocks_with_pos:
267 grouped[key].append((start, block))
269 result: List[Tuple[CheckLabel, DuplicateOccurrences]] = []
270 for key, occs
in grouped.items():
272 result.append((key, occs))
274 result.sort(key=itemgetter(0))
279 lines: Sequence[str], title: str, next_title: Optional[str]
280) -> Optional[Tuple[int, int, int]]:
281 """Return (h_start, sec_start, sec_end) for section `title`.
283 - h_start: index of the section title line
284 - sec_start: index of the first content line after underline
285 - sec_end: index of the first line of the next section title (or end)
290 sec_start = h_start + 2
293 if next_title
is not None:
297 while h_end + 1 < len(lines):
298 if lines[h_end].strip()
and set(lines[h_end + 1].rstrip(
"\n")) == {
"^"}:
305 while h_end + 1 < len(lines):
306 if lines[h_end].strip()
and set(lines[h_end + 1].rstrip(
"\n")) == {
"^"}:
311 return h_start, sec_start, sec_end
315 lines: Sequence[str], title: str, next_title: Optional[str]
317 """Normalize a single release-notes section and return updated lines."""
320 _, sec_start, sec_end = bounds
325 new_section: List[str] = []
326 new_section.extend(prefix)
327 for i_b, b
in enumerate(sorted_blocks):
329 not new_section
or (new_section
and new_section[-1].strip() !=
"")
331 new_section.append(
"\n")
332 new_section.extend(b)
333 new_section.extend(suffix)
335 return list(lines[:sec_start]) + new_section + list(lines[sec_end:])
339 sections = [
"New checks",
"New check aliases",
"Changes in existing checks"]
343 for idx
in range(len(sections) - 1, -1, -1):
344 title = sections[idx]
345 next_title = sections[idx + 1]
if idx + 1 < len(sections)
else None
355 out.append(f
"Error: Duplicate entries in '{title}':\n")
356 for key, occs
in dups_detail:
357 out.append(f
"\n-- Duplicate: {key}\n")
358 for start_idx, block
in occs:
359 out.append(f
"- At line {start_idx + 1}:\n")
360 out.append(
"".join(block))
361 if not (block
and block[-1].endswith(
"\n")):
368 lines = text.splitlines(
True)
373 if text != normalized:
375 "\nEntries in 'clang-tools-extra/docs/ReleaseNotes.rst' are not alphabetically sorted.\n"
376 "Fix the ordering by applying diff printed below.\n\n"
382 sys.stderr.write(report)
391 if text != normalized:
393 "\nChecks in 'clang-tools-extra/docs/clang-tidy/checks/list.rst' csv-table are not alphabetically sorted.\n"
394 "Fix the ordering by applying diff printed below.\n\n"
401def main(argv: Sequence[str]) -> int:
402 ap = argparse.ArgumentParser()
403 ap.add_argument(
"-o",
"--output", dest=
"out", default=
None)
404 args = ap.parse_args(argv)
406 list_doc, rn_doc = (os.path.normpath(LIST_DOC), os.path.normpath(RELEASE_NOTES_DOC))
410 out_lower = os.path.basename(out_path).lower()
411 if "release" in out_lower:
420if __name__ ==
"__main__":
421 sys.exit(
main(sys.argv[1:]))
str normalize_list_rst(str data)
int main(Sequence[str] argv)
List[Tuple[CheckLabel, DuplicateOccurrences]] find_duplicate_entries(Sequence[str] lines, str title)
List[BulletBlock] sort_blocks(Iterable[BulletItem] blocks)
List[str] _normalize_list_rst_lines(Sequence[str] lines)
Optional[int] find_heading(Sequence[str] lines, str title)
None write_text(str path, str content)
Optional[Tuple[int, int, int]] _find_section_bounds(Sequence[str] lines, str title, Optional[str] next_title)
int process_checks_list(str out_path, str list_doc)
bool _is_bullet_start(str line)
str extract_label(str text)
List[str] _normalize_release_notes_section(Sequence[str] lines, str title, Optional[str] next_title)
Optional[str] _emit_duplicate_report(Sequence[str] lines, str title)
str normalize_release_notes(Sequence[str] lines)
ScannedBlocks _scan_bullet_blocks(Sequence[str] lines, int start, int end)
int process_release_notes(str out_path, str rn_doc)
BulletBlocks _parse_bullet_blocks(Sequence[str] lines, int start, int end)