aboutsummaryrefslogtreecommitdiff
path: root/clang-tools-extra/clang-tidy/tool
diff options
context:
space:
mode:
Diffstat (limited to 'clang-tools-extra/clang-tidy/tool')
-rw-r--r--clang-tools-extra/clang-tidy/tool/ClangTidyMain.cpp49
-rw-r--r--clang-tools-extra/clang-tidy/tool/ClangTidyMain.h5
-rw-r--r--clang-tools-extra/clang-tidy/tool/check_alphabetical_order.py421
-rw-r--r--clang-tools-extra/clang-tidy/tool/check_alphabetical_order_test.py401
4 files changed, 851 insertions, 25 deletions
diff --git a/clang-tools-extra/clang-tidy/tool/ClangTidyMain.cpp b/clang-tools-extra/clang-tidy/tool/ClangTidyMain.cpp
index 1ae8756..6a1f61d 100644
--- a/clang-tools-extra/clang-tidy/tool/ClangTidyMain.cpp
+++ b/clang-tools-extra/clang-tidy/tool/ClangTidyMain.cpp
@@ -104,8 +104,7 @@ Configuration files:
)");
const char DefaultChecks[] = // Enable these checks by default:
- "clang-diagnostic-*," // * compiler diagnostics
- "clang-analyzer-*"; // * Static Analyzer checks
+ "clang-diagnostic-*"; // * compiler diagnostics
static cl::opt<std::string> Checks("checks", desc(R"(
Comma-separated list of globs with optional '-'
@@ -390,7 +389,7 @@ static void printStats(const ClangTidyStats &Stats) {
static std::unique_ptr<ClangTidyOptionsProvider>
createOptionsProvider(llvm::IntrusiveRefCntPtr<vfs::FileSystem> FS) {
ClangTidyGlobalOptions GlobalOptions;
- if (std::error_code Err = parseLineFilter(LineFilter, GlobalOptions)) {
+ if (const std::error_code Err = parseLineFilter(LineFilter, GlobalOptions)) {
llvm::errs() << "Invalid LineFilter: " << Err.message() << "\n\nUsage:\n";
llvm::cl::PrintHelpMessage(/*Hidden=*/false, /*Categorized=*/true);
return nullptr;
@@ -448,7 +447,7 @@ createOptionsProvider(llvm::IntrusiveRefCntPtr<vfs::FileSystem> FS) {
llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> Text =
llvm::MemoryBuffer::getFile(ConfigFile);
- if (std::error_code EC = Text.getError()) {
+ if (const std::error_code EC = Text.getError()) {
llvm::errs() << "Error: can't read config-file '" << ConfigFile
<< "': " << EC.message() << "\n";
return nullptr;
@@ -466,10 +465,9 @@ createOptionsProvider(llvm::IntrusiveRefCntPtr<vfs::FileSystem> FS) {
}
static llvm::IntrusiveRefCntPtr<vfs::FileSystem>
-getVfsFromFile(const std::string &OverlayFile,
- llvm::IntrusiveRefCntPtr<vfs::FileSystem> BaseFS) {
+getVfsFromFile(const std::string &OverlayFile, vfs::FileSystem &BaseFS) {
llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> Buffer =
- BaseFS->getBufferForFile(OverlayFile);
+ BaseFS.getBufferForFile(OverlayFile);
if (!Buffer) {
llvm::errs() << "Can't load virtual filesystem overlay file '"
<< OverlayFile << "': " << Buffer.getError().message()
@@ -491,7 +489,7 @@ static StringRef closest(StringRef Value, const StringSet<> &Allowed) {
unsigned MaxEdit = 5U;
StringRef Closest;
for (auto Item : Allowed.keys()) {
- unsigned Cur = Value.edit_distance_insensitive(Item, true, MaxEdit);
+ const unsigned Cur = Value.edit_distance_insensitive(Item, true, MaxEdit);
if (Cur < MaxEdit) {
Closest = Item;
MaxEdit = Cur;
@@ -504,7 +502,7 @@ static constexpr StringLiteral VerifyConfigWarningEnd = " [-verify-config]\n";
static bool verifyChecks(const StringSet<> &AllChecks, StringRef CheckGlob,
StringRef Source) {
- GlobList Globs(CheckGlob);
+ const GlobList Globs(CheckGlob);
bool AnyInvalid = false;
for (const auto &Item : Globs.getItems()) {
if (Item.Text.starts_with("clang-diagnostic"))
@@ -520,7 +518,7 @@ static bool verifyChecks(const StringSet<> &AllChecks, StringRef CheckGlob,
llvm::raw_ostream &Output =
llvm::WithColor::warning(llvm::errs(), Source)
<< "unknown check '" << Item.Text << '\'';
- llvm::StringRef Closest = closest(Item.Text, AllChecks);
+ const llvm::StringRef Closest = closest(Item.Text, AllChecks);
if (!Closest.empty())
Output << "; did you mean '" << Closest << '\'';
Output << VerifyConfigWarningEnd;
@@ -560,7 +558,7 @@ static bool verifyOptions(const llvm::StringSet<> &ValidOptions,
AnyInvalid = true;
auto &Output = llvm::WithColor::warning(llvm::errs(), Source)
<< "unknown check option '" << Key << '\'';
- llvm::StringRef Closest = closest(Key, ValidOptions);
+ const llvm::StringRef Closest = closest(Key, ValidOptions);
if (!Closest.empty())
Output << "; did you mean '" << Closest << '\'';
Output << VerifyConfigWarningEnd;
@@ -572,7 +570,7 @@ static SmallString<256> makeAbsolute(llvm::StringRef Input) {
if (Input.empty())
return {};
SmallString<256> AbsolutePath(Input);
- if (std::error_code EC = llvm::sys::fs::make_absolute(AbsolutePath)) {
+ if (const std::error_code EC = llvm::sys::fs::make_absolute(AbsolutePath)) {
llvm::errs() << "Can't make absolute path from " << Input << ": "
<< EC.message() << "\n";
}
@@ -585,7 +583,7 @@ static llvm::IntrusiveRefCntPtr<vfs::OverlayFileSystem> createBaseFS() {
if (!VfsOverlay.empty()) {
IntrusiveRefCntPtr<vfs::FileSystem> VfsFromFile =
- getVfsFromFile(VfsOverlay, BaseFS);
+ getVfsFromFile(VfsOverlay, *BaseFS);
if (!VfsFromFile)
return nullptr;
BaseFS->pushOverlay(std::move(VfsFromFile));
@@ -594,7 +592,7 @@ static llvm::IntrusiveRefCntPtr<vfs::OverlayFileSystem> createBaseFS() {
}
int clangTidyMain(int argc, const char **argv) {
- llvm::InitLLVM X(argc, argv);
+ const llvm::InitLLVM X(argc, argv);
SmallVector<const char *> Args{argv, argv + argc};
// expand parameters file to argc and argv.
@@ -623,7 +621,8 @@ int clangTidyMain(int argc, const char **argv) {
return 1;
}
- llvm::IntrusiveRefCntPtr<vfs::OverlayFileSystem> BaseFS = createBaseFS();
+ const llvm::IntrusiveRefCntPtr<vfs::OverlayFileSystem> BaseFS =
+ createBaseFS();
if (!BaseFS)
return 1;
@@ -632,7 +631,7 @@ int clangTidyMain(int argc, const char **argv) {
if (!OptionsProvider)
return 1;
- SmallString<256> ProfilePrefix = makeAbsolute(StoreCheckProfile);
+ const SmallString<256> ProfilePrefix = makeAbsolute(StoreCheckProfile);
StringRef FileName("dummy");
auto PathList = OptionsParser->getSourcePathList();
@@ -640,10 +639,10 @@ int clangTidyMain(int argc, const char **argv) {
FileName = PathList.front();
}
- SmallString<256> FilePath = makeAbsolute(FileName);
+ const SmallString<256> FilePath = makeAbsolute(FileName);
ClangTidyOptions EffectiveOptions = OptionsProvider->getOptions(FilePath);
- std::vector<std::string> EnabledChecks =
+ const std::vector<std::string> EnabledChecks =
getCheckNames(EffectiveOptions, AllowEnablingAnalyzerAlphaCheckers,
ExperimentalCustomChecks);
@@ -687,9 +686,9 @@ int clangTidyMain(int argc, const char **argv) {
}
if (VerifyConfig) {
- std::vector<ClangTidyOptionsProvider::OptionsSource> RawOptions =
+ const std::vector<ClangTidyOptionsProvider::OptionsSource> RawOptions =
OptionsProvider->getRawOptions(FileName);
- ChecksAndOptions Valid = getAllChecksAndOptions(
+ const ChecksAndOptions Valid = getAllChecksAndOptions(
AllowEnablingAnalyzerAlphaCheckers, ExperimentalCustomChecks);
bool AnyInvalid = false;
for (const auto &[Opts, Source] : RawOptions) {
@@ -733,14 +732,14 @@ int clangTidyMain(int argc, const char **argv) {
std::vector<ClangTidyError> Errors =
runClangTidy(Context, OptionsParser->getCompilations(), PathList, BaseFS,
FixNotes, EnableCheckProfile, ProfilePrefix, Quiet);
- bool FoundErrors = llvm::any_of(Errors, [](const ClangTidyError &E) {
+ const bool FoundErrors = llvm::any_of(Errors, [](const ClangTidyError &E) {
return E.DiagLevel == ClangTidyError::Error;
});
// --fix-errors and --fix-notes imply --fix.
- FixBehaviour Behaviour = FixNotes ? FB_FixNotes
- : (Fix || FixErrors) ? FB_Fix
- : FB_NoFix;
+ const FixBehaviour Behaviour = FixNotes ? FB_FixNotes
+ : (Fix || FixErrors) ? FB_Fix
+ : FB_NoFix;
const bool DisableFixes = FoundErrors && !FixErrors;
@@ -769,7 +768,7 @@ int clangTidyMain(int argc, const char **argv) {
if (WErrorCount) {
if (!Quiet) {
- StringRef Plural = WErrorCount == 1 ? "" : "s";
+ const StringRef Plural = WErrorCount == 1 ? "" : "s";
llvm::errs() << WErrorCount << " warning" << Plural << " treated as error"
<< Plural << "\n";
}
diff --git a/clang-tools-extra/clang-tidy/tool/ClangTidyMain.h b/clang-tools-extra/clang-tidy/tool/ClangTidyMain.h
index f86828e..44b7a37 100644
--- a/clang-tools-extra/clang-tidy/tool/ClangTidyMain.h
+++ b/clang-tools-extra/clang-tidy/tool/ClangTidyMain.h
@@ -14,8 +14,13 @@
///
//===----------------------------------------------------------------------===//
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_TOOL_CLANGTIDYMAIN_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_TOOL_CLANGTIDYMAIN_H
+
namespace clang::tidy {
int clangTidyMain(int argc, const char **argv);
} // namespace clang::tidy
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_TOOL_CLANGTIDYMAIN_H
diff --git a/clang-tools-extra/clang-tidy/tool/check_alphabetical_order.py b/clang-tools-extra/clang-tidy/tool/check_alphabetical_order.py
new file mode 100644
index 0000000..66819ab
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/tool/check_alphabetical_order.py
@@ -0,0 +1,421 @@
+#!/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")
+ 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(block[0])
+ 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 + 1 < len(lines):
+ if 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 + 1 < len(lines):
+ if 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")
+ 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:]))
diff --git a/clang-tools-extra/clang-tidy/tool/check_alphabetical_order_test.py b/clang-tools-extra/clang-tidy/tool/check_alphabetical_order_test.py
new file mode 100644
index 0000000..48a3c76
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/tool/check_alphabetical_order_test.py
@@ -0,0 +1,401 @@
+# 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
+
+# To run these tests:
+# python3 check_alphabetical_order_test.py -v
+
+import check_alphabetical_order as _mod
+from contextlib import redirect_stderr
+import io
+import os
+import tempfile
+import textwrap
+from typing import cast
+import unittest
+
+
+class TestAlphabeticalOrderCheck(unittest.TestCase):
+ def test_normalize_list_rst_sorts_rows(self) -> None:
+ input_text = textwrap.dedent(
+ """\
+ .. csv-table:: Clang-Tidy checks
+ :header: "Name", "Offers fixes"
+
+ :doc:`bugprone-virtual-near-miss <bugprone/virtual-near-miss>`, "Yes"
+ :doc:`cert-flp30-c <cert/flp30-c>`,
+ :doc:`abseil-cleanup-ctad <abseil/cleanup-ctad>`, "Yes"
+ A non-doc row that should stay after docs
+ """
+ )
+
+ expected_text = textwrap.dedent(
+ """\
+ .. csv-table:: Clang-Tidy checks
+ :header: "Name", "Offers fixes"
+
+ :doc:`abseil-cleanup-ctad <abseil/cleanup-ctad>`, "Yes"
+ :doc:`bugprone-virtual-near-miss <bugprone/virtual-near-miss>`, "Yes"
+ :doc:`cert-flp30-c <cert/flp30-c>`,
+ A non-doc row that should stay after docs
+ """
+ )
+
+ out_str = _mod.normalize_list_rst(input_text)
+ self.assertEqual(out_str, expected_text)
+
+ def test_find_heading(self) -> None:
+ text = textwrap.dedent(
+ """\
+ - Deprecated the :program:`clang-tidy` ``zircon`` module. All checks have been
+ moved to the ``fuchsia`` module instead. The ``zircon`` module will be removed
+ in the 24th release.
+
+ New checks
+ ^^^^^^^^^^
+ - New :doc:`bugprone-derived-method-shadowing-base-method
+ <clang-tidy/checks/bugprone/derived-method-shadowing-base-method>` check.
+ """
+ )
+ lines = text.splitlines(True)
+ idx = _mod.find_heading(lines, "New checks")
+ self.assertEqual(idx, 4)
+
+ def test_duplicate_detection_and_report(self) -> None:
+ # Ensure duplicate detection works properly when sorting is incorrect.
+ text = textwrap.dedent(
+ """\
+ Changes in existing checks
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ - Improved :doc:`bugprone-exception-escape
+ <clang-tidy/checks/bugprone/exception-escape>` check's handling of lambdas:
+ exceptions from captures are now diagnosed, exceptions in the bodies of
+ lambdas that aren't actually invoked are not.
+
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ """
+ )
+ lines = text.splitlines(True)
+ report = _mod._emit_duplicate_report(lines, "Changes in existing checks")
+ self.assertIsNotNone(report)
+ report_str = cast(str, report)
+
+ expected_report = textwrap.dedent(
+ """\
+ Error: Duplicate entries in 'Changes in existing checks':
+
+ -- Duplicate: - Improved :doc:`bugprone-easily-swappable-parameters
+
+ - At line 4:
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ - At line 14:
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+ """
+ )
+ self.assertEqual(report_str, expected_report)
+
+ def test_process_release_notes_with_unsorted_content(self) -> None:
+ # When content is not normalized, the function writes normalized text and returns 0.
+ rn_text = textwrap.dedent(
+ """\
+ New checks
+ ^^^^^^^^^^
+
+ - New :doc:`readability-redundant-parentheses
+ <clang-tidy/checks/readability/redundant-parentheses>` check.
+
+ Detect redundant parentheses.
+
+ - New :doc:`bugprone-derived-method-shadowing-base-method
+ <clang-tidy/checks/bugprone/derived-method-shadowing-base-method>` check.
+
+ Finds derived class methods that shadow a (non-virtual) base class method.
+
+ """
+ )
+ with tempfile.TemporaryDirectory() as td:
+ rn_doc = os.path.join(td, "ReleaseNotes.rst")
+ out_path = os.path.join(td, "out.rst")
+ with open(rn_doc, "w", encoding="utf-8") as f:
+ f.write(rn_text)
+
+ buf = io.StringIO()
+ with redirect_stderr(buf):
+ rc = _mod.process_release_notes(out_path, rn_doc)
+
+ self.assertEqual(rc, 0)
+ with open(out_path, "r", encoding="utf-8") as f:
+ out = f.read()
+
+ expected_out = textwrap.dedent(
+ """\
+ New checks
+ ^^^^^^^^^^
+
+ - New :doc:`bugprone-derived-method-shadowing-base-method
+ <clang-tidy/checks/bugprone/derived-method-shadowing-base-method>` check.
+
+ Finds derived class methods that shadow a (non-virtual) base class method.
+
+ - New :doc:`readability-redundant-parentheses
+ <clang-tidy/checks/readability/redundant-parentheses>` check.
+
+ Detect redundant parentheses.
+
+
+ """
+ )
+
+ self.assertEqual(out, expected_out)
+ self.assertIn("not alphabetically sorted", buf.getvalue())
+
+ def test_process_release_notes_prioritizes_sorting_over_duplicates(self) -> None:
+ # Sorting is incorrect and duplicates exist, should report ordering issues first.
+ rn_text = textwrap.dedent(
+ """\
+ Changes in existing checks
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ - Improved :doc:`bugprone-exception-escape
+ <clang-tidy/checks/bugprone/exception-escape>` check's handling of lambdas:
+ exceptions from captures are now diagnosed, exceptions in the bodies of
+ lambdas that aren't actually invoked are not.
+
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ """
+ )
+ with tempfile.TemporaryDirectory() as td:
+ rn_doc = os.path.join(td, "ReleaseNotes.rst")
+ out_path = os.path.join(td, "out.rst")
+ with open(rn_doc, "w", encoding="utf-8") as f:
+ f.write(rn_text)
+
+ buf = io.StringIO()
+ with redirect_stderr(buf):
+ rc = _mod.process_release_notes(out_path, rn_doc)
+ self.assertEqual(rc, 0)
+ self.assertIn(
+ "Entries in 'clang-tools-extra/docs/ReleaseNotes.rst' are not alphabetically sorted.",
+ buf.getvalue(),
+ )
+
+ with open(out_path, "r", encoding="utf-8") as f:
+ out = f.read()
+ expected_out = textwrap.dedent(
+ """\
+ Changes in existing checks
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ - Improved :doc:`bugprone-exception-escape
+ <clang-tidy/checks/bugprone/exception-escape>` check's handling of lambdas:
+ exceptions from captures are now diagnosed, exceptions in the bodies of
+ lambdas that aren't actually invoked are not.
+
+
+ """
+ )
+ self.assertEqual(out, expected_out)
+
+ def test_process_release_notes_with_duplicates_fails(self) -> None:
+ # Sorting is already correct but duplicates exist, should return 3 and report.
+ rn_text = textwrap.dedent(
+ """\
+ Changes in existing checks
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ - Improved :doc:`bugprone-exception-escape
+ <clang-tidy/checks/bugprone/exception-escape>` check's handling of lambdas:
+ exceptions from captures are now diagnosed, exceptions in the bodies of
+ lambdas that aren't actually invoked are not.
+
+ """
+ )
+ with tempfile.TemporaryDirectory() as td:
+ rn_doc = os.path.join(td, "ReleaseNotes.rst")
+ out_path = os.path.join(td, "out.rst")
+ with open(rn_doc, "w", encoding="utf-8") as f:
+ f.write(rn_text)
+
+ buf = io.StringIO()
+ with redirect_stderr(buf):
+ rc = _mod.process_release_notes(out_path, rn_doc)
+
+ self.assertEqual(rc, 3)
+ expected_report = textwrap.dedent(
+ """\
+ Error: Duplicate entries in 'Changes in existing checks':
+
+ -- Duplicate: - Improved :doc:`bugprone-easily-swappable-parameters
+
+ - At line 4:
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ - At line 9:
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ """
+ )
+ self.assertEqual(buf.getvalue(), expected_report)
+
+ with open(out_path, "r", encoding="utf-8") as f:
+ out = f.read()
+ self.assertEqual(out, rn_text)
+
+ def test_release_notes_handles_nested_sub_bullets(self) -> None:
+ rn_text = textwrap.dedent(
+ """\
+ Changes in existing checks
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ - Improved :doc:`llvm-prefer-isa-or-dyn-cast-in-conditionals
+ <clang-tidy/checks/llvm/prefer-isa-or-dyn-cast-in-conditionals>` check:
+
+ - Fix-it handles callees with nested-name-specifier correctly.
+
+ - ``if`` statements with init-statement (``if (auto X = ...; ...)``) are
+ handled correctly.
+
+ - ``for`` loops are supported.
+
+ - Improved :doc:`bugprone-exception-escape
+ <clang-tidy/checks/bugprone/exception-escape>` check's handling of lambdas:
+ exceptions from captures are now diagnosed, exceptions in the bodies of
+ lambdas that aren't actually invoked are not.
+
+ """
+ )
+
+ out = _mod.normalize_release_notes(rn_text.splitlines(True))
+
+ expected_out = textwrap.dedent(
+ """\
+ Changes in existing checks
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ - Improved :doc:`bugprone-easily-swappable-parameters
+ <clang-tidy/checks/bugprone/easily-swappable-parameters>` check by
+ correcting a spelling mistake on its option
+ ``NamePrefixSuffixSilenceDissimilarityTreshold``.
+
+ - Improved :doc:`bugprone-exception-escape
+ <clang-tidy/checks/bugprone/exception-escape>` check's handling of lambdas:
+ exceptions from captures are now diagnosed, exceptions in the bodies of
+ lambdas that aren't actually invoked are not.
+
+ - Improved :doc:`llvm-prefer-isa-or-dyn-cast-in-conditionals
+ <clang-tidy/checks/llvm/prefer-isa-or-dyn-cast-in-conditionals>` check:
+
+ - Fix-it handles callees with nested-name-specifier correctly.
+
+ - ``if`` statements with init-statement (``if (auto X = ...; ...)``) are
+ handled correctly.
+
+ - ``for`` loops are supported.
+
+
+ """
+ )
+ self.assertEqual(out, expected_out)
+
+ def test_process_checks_list_normalizes_output(self) -> None:
+ list_text = textwrap.dedent(
+ """\
+ .. csv-table:: List
+ :header: "Name", "Redirect", "Offers fixes"
+
+ :doc:`cert-dcl16-c <cert/dcl16-c>`, :doc:`readability-uppercase-literal-suffix <readability/uppercase-literal-suffix>`, "Yes"
+ :doc:`cert-con36-c <cert/con36-c>`, :doc:`bugprone-spuriously-wake-up-functions <bugprone/spuriously-wake-up-functions>`,
+ :doc:`cert-dcl37-c <cert/dcl37-c>`, :doc:`bugprone-reserved-identifier <bugprone/reserved-identifier>`, "Yes"
+ :doc:`cert-arr39-c <cert/arr39-c>`, :doc:`bugprone-sizeof-expression <bugprone/sizeof-expression>`,
+ """
+ )
+ with tempfile.TemporaryDirectory() as td:
+ in_doc = os.path.join(td, "list.rst")
+ out_doc = os.path.join(td, "out.rst")
+ with open(in_doc, "w", encoding="utf-8") as f:
+ f.write(list_text)
+ buf = io.StringIO()
+ with redirect_stderr(buf):
+ rc = _mod.process_checks_list(out_doc, in_doc)
+ self.assertEqual(rc, 0)
+ self.assertIn(
+ "Checks in 'clang-tools-extra/docs/clang-tidy/checks/list.rst' csv-table are not alphabetically sorted.",
+ buf.getvalue(),
+ )
+ self.assertEqual(rc, 0)
+ with open(out_doc, "r", encoding="utf-8") as f:
+ out = f.read()
+
+ expected_out = textwrap.dedent(
+ """\
+ .. csv-table:: List
+ :header: "Name", "Redirect", "Offers fixes"
+
+ :doc:`cert-arr39-c <cert/arr39-c>`, :doc:`bugprone-sizeof-expression <bugprone/sizeof-expression>`,
+ :doc:`cert-con36-c <cert/con36-c>`, :doc:`bugprone-spuriously-wake-up-functions <bugprone/spuriously-wake-up-functions>`,
+ :doc:`cert-dcl16-c <cert/dcl16-c>`, :doc:`readability-uppercase-literal-suffix <readability/uppercase-literal-suffix>`, "Yes"
+ :doc:`cert-dcl37-c <cert/dcl37-c>`, :doc:`bugprone-reserved-identifier <bugprone/reserved-identifier>`, "Yes"
+ """
+ )
+ self.assertEqual(out, expected_out)
+
+
+if __name__ == "__main__":
+ unittest.main()