diff options
Diffstat (limited to 'clang-tools-extra/clang-tidy/tool')
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() |
