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.md Markdown tables.
20- Sort key sections in docs/ReleaseNotes.md.
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.md")
60RELEASE_NOTES_DOC: Final = os.path.join(DOCS_DIR,
"ReleaseNotes.md")
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]] = []
117 if lines[i].startswith(
"#"):
120 block: BulletBlock = list(lines[bstart:i])
121 blocks_with_pos.append((bstart, block))
126 with io.open(path,
"r", encoding=
"utf-8")
as f:
131 with io.open(path,
"w", encoding=
"utf-8", newline=
"")
as f:
137 index + 1 < len(lines)
138 and lines[index].startswith(
"| Name |")
139 and lines[index + 1].startswith(
"| ---")
144 """Return normalized content of checks list.md as a list of lines."""
149 def check_name(line: str) -> Tuple[int, CheckLabel]:
150 if m := DOC_LINE_RE.match(line):
151 return (0, m.group(
"label"))
157 out.append(lines[i + 1])
160 entries: List[str] = []
161 while i < n
and lines[i].startswith(
"| "):
162 entries.append(lines[i])
165 entries_sorted = sorted(entries, key=check_name)
166 out.extend(entries_sorted)
176 """Normalize list.md content and return a string."""
177 lines = data.splitlines(
True)
182 """Find heading start index for a Markdown #### section heading.
184 The function looks for a line equal to `#### {title}`, matching the
185 ReleaseNotes.md style for subsection headings.
187 Returns index of the title line, or None if not found.
189 target = f
"#### {title}"
190 for i
in range(len(lines)):
191 if lines[i].rstrip(
"\n") == target:
197 if m := DOC_LABEL_RN_RE.search(text):
198 return m.group(
"label").strip()
203 return line.startswith(
"- ")
212 prefix: Lines = list(lines[i:first_bullet])
214 blocks: List[BulletItem] = []
216 for _, block
in res.blocks_with_pos:
218 blocks.append((key, block))
220 suffix: Lines = list(lines[res.next_index : n])
224def sort_blocks(blocks: Iterable[BulletItem]) -> List[BulletBlock]:
225 """Return blocks sorted deterministically by their extracted label.
227 Duplicates are preserved; merging is left to authors to handle manually.
229 return list(map(itemgetter(1), sorted(blocks, key=itemgetter(0))))
233 lines: Sequence[str], title: str
234) -> List[Tuple[CheckLabel, DuplicateOccurrences]]:
235 """Return detailed duplicate info as (key, [(start_idx, block_lines), ...]).
237 start_idx is the 0-based index of the first line of the bullet block in
238 the original lines list. Only keys with more than one occurrence are
239 returned, and occurrences are listed in the order they appear.
244 _, sec_start, sec_end = bounds
252 blocks_with_pos: List[Tuple[CheckLabel, BulletStart, BulletBlock]] = []
254 for bstart, block
in res.blocks_with_pos:
256 blocks_with_pos.append((key, bstart, block))
258 grouped: DefaultDict[CheckLabel, DuplicateOccurrences] = defaultdict(list)
259 for key, start, block
in blocks_with_pos:
260 grouped[key].append((start, block))
262 result: List[Tuple[CheckLabel, DuplicateOccurrences]] = []
263 for key, occs
in grouped.items():
265 result.append((key, occs))
267 result.sort(key=itemgetter(0))
272 lines: Sequence[str], title: str, next_title: Optional[str]
273) -> Optional[Tuple[int, int, int]]:
274 """Return (h_start, sec_start, sec_end) for section `title`.
276 - h_start: index of the section title line
277 - sec_start: index of the first content line after the heading
278 - sec_end: index of the first line of the next section title (or end)
283 sec_start = h_start + 1
286 if next_title
is not None:
290 while h_end < len(lines):
291 if lines[h_end].startswith(
"#"):
298 while h_end < len(lines):
299 if lines[h_end].startswith(
"#"):
304 return h_start, sec_start, sec_end
308 lines: Sequence[str], title: str, next_title: Optional[str]
310 """Normalize a single release-notes section and return updated lines."""
313 _, sec_start, sec_end = bounds
318 new_section: List[str] = []
319 new_section.extend(prefix)
320 for i_b, b
in enumerate(sorted_blocks):
322 not new_section
or (new_section
and new_section[-1].strip() !=
"")
324 new_section.append(
"\n")
325 new_section.extend(b)
326 new_section.extend(suffix)
328 return list(lines[:sec_start]) + new_section + list(lines[sec_end:])
332 sections = [
"New checks",
"New check aliases",
"Changes in existing checks"]
336 for idx
in range(len(sections) - 1, -1, -1):
337 title = sections[idx]
338 next_title = sections[idx + 1]
if idx + 1 < len(sections)
else None
348 out.append(f
"Error: Duplicate entries in '{title}'.\n")
349 out.append(
"\nPlease merge these entries into a single bullet point.\n")
350 for key, occs
in dups_detail:
351 out.append(f
"\n-- Duplicate: {key}\n")
352 for start_idx, block
in occs:
353 out.append(f
"- At line {start_idx + 1}:\n")
354 out.append(
"".join(block))
355 if not (block
and block[-1].endswith(
"\n")):
362 lines = text.splitlines(
True)
367 if text != normalized:
369 "\nEntries in 'clang-tools-extra/docs/ReleaseNotes.md' are not alphabetically sorted.\n"
370 "Fix the ordering by applying diff printed below.\n\n"
376 sys.stderr.write(report)
385 if text != normalized:
387 "\nChecks in 'clang-tools-extra/docs/clang-tidy/checks/list.md' tables are not alphabetically sorted.\n"
388 "Fix the ordering by applying diff printed below.\n\n"
395def main(argv: Sequence[str]) -> int:
396 ap = argparse.ArgumentParser()
397 ap.add_argument(
"-o",
"--output", dest=
"out", default=
None)
398 args = ap.parse_args(argv)
400 list_doc, rn_doc = (os.path.normpath(LIST_DOC), os.path.normpath(RELEASE_NOTES_DOC))
404 out_lower = os.path.basename(out_path).lower()
405 if "release" in out_lower:
414if __name__ ==
"__main__":
415 sys.exit(
main(sys.argv[1:]))
int main(Sequence[str] argv)
List[Tuple[CheckLabel, DuplicateOccurrences]] find_duplicate_entries(Sequence[str] lines, str title)
List[str] _normalize_list_md_lines(Sequence[str] lines)
List[BulletBlock] sort_blocks(Iterable[BulletItem] blocks)
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_markdown_table_header(Sequence[str] lines, int index)
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)
str normalize_list_md(str data)
int process_release_notes(str out_path, str rn_doc)
BulletBlocks _parse_bullet_blocks(Sequence[str] lines, int start, int end)