789 lines · python
1#!/usr/bin/env python32#3# ===-----------------------------------------------------------------------===#4#5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.6# See https://llvm.org/LICENSE.txt for license information.7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception8#9# ===-----------------------------------------------------------------------===#10 11import argparse12import io13import itertools14import os15import re16import sys17import textwrap18 19# FIXME Python 3.9: Replace typing.Tuple with builtins.tuple.20from typing import Optional, Tuple, Match21 22 23# Adapts the module's CMakelist file. Returns 'True' if it could add a new24# entry and 'False' if the entry already existed.25def adapt_cmake(module_path: str, check_name_camel: str) -> bool:26 filename = os.path.join(module_path, "CMakeLists.txt")27 28 # The documentation files are encoded using UTF-8, however on Windows the29 # default encoding might be different (e.g. CP-1252). To make sure UTF-8 is30 # always used, use `io.open(filename, mode, encoding='utf8')` for reading and31 # writing files here and elsewhere.32 with io.open(filename, "r", encoding="utf8") as f:33 lines = f.readlines()34 35 cpp_file = check_name_camel + ".cpp"36 37 # Figure out whether this check already exists.38 for line in lines:39 if line.strip() == cpp_file:40 return False41 42 print("Updating %s..." % filename)43 with io.open(filename, "w", encoding="utf8", newline="\n") as f:44 cpp_found = False45 file_added = False46 for line in lines:47 cpp_line = line.strip().endswith(".cpp")48 if (not file_added) and (cpp_line or cpp_found):49 cpp_found = True50 if (line.strip() > cpp_file) or (not cpp_line):51 f.write(" " + cpp_file + "\n")52 file_added = True53 f.write(line)54 55 return True56 57 58# Adds a header for the new check.59def write_header(60 module_path: str,61 module: str,62 namespace: str,63 check_name: str,64 check_name_camel: str,65 description: str,66 lang_restrict: str,67) -> None:68 wrapped_desc = "\n".join(69 textwrap.wrap(70 description, width=80, initial_indent="/// ", subsequent_indent="/// "71 )72 )73 if lang_restrict:74 override_supported = """75 bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {76 return %s;77 }""" % (78 lang_restrict % {"lang": "LangOpts"}79 )80 else:81 override_supported = ""82 filename = os.path.join(module_path, check_name_camel) + ".h"83 print("Creating %s..." % filename)84 with io.open(filename, "w", encoding="utf8", newline="\n") as f:85 header_guard = (86 "LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_"87 + module.upper()88 + "_"89 + check_name_camel.upper()90 + "_H"91 )92 f.write(93 """\94//===----------------------------------------------------------------------===//95//96// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.97// See https://llvm.org/LICENSE.txt for license information.98// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception99//100//===----------------------------------------------------------------------===//101 102#ifndef %(header_guard)s103#define %(header_guard)s104 105#include "../ClangTidyCheck.h"106 107namespace clang::tidy::%(namespace)s {108 109%(description)s110///111/// For the user-facing documentation see:112/// https://clang.llvm.org/extra/clang-tidy/checks/%(module)s/%(check_name)s.html113class %(check_name_camel)s : public ClangTidyCheck {114public:115 %(check_name_camel)s(StringRef Name, ClangTidyContext *Context)116 : ClangTidyCheck(Name, Context) {}117 void registerMatchers(ast_matchers::MatchFinder *Finder) override;118 void check(const ast_matchers::MatchFinder::MatchResult &Result) override;%(override_supported)s119};120 121} // namespace clang::tidy::%(namespace)s122 123#endif // %(header_guard)s124"""125 % {126 "header_guard": header_guard,127 "check_name_camel": check_name_camel,128 "check_name": check_name,129 "module": module,130 "namespace": namespace,131 "description": wrapped_desc,132 "override_supported": override_supported,133 }134 )135 136 137# Adds the implementation of the new check.138def write_implementation(139 module_path: str, module: str, namespace: str, check_name_camel: str140) -> None:141 filename = os.path.join(module_path, check_name_camel) + ".cpp"142 print("Creating %s..." % filename)143 with io.open(filename, "w", encoding="utf8", newline="\n") as f:144 f.write(145 """\146//===----------------------------------------------------------------------===//147//148// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.149// See https://llvm.org/LICENSE.txt for license information.150// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception151//152//===----------------------------------------------------------------------===//153 154#include "%(check_name)s.h"155#include "clang/ASTMatchers/ASTMatchFinder.h"156 157using namespace clang::ast_matchers;158 159namespace clang::tidy::%(namespace)s {160 161void %(check_name)s::registerMatchers(MatchFinder *Finder) {162 // FIXME: Add matchers.163 Finder->addMatcher(functionDecl().bind("x"), this);164}165 166void %(check_name)s::check(const MatchFinder::MatchResult &Result) {167 // FIXME: Add callback implementation.168 const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");169 if (!MatchedDecl->getIdentifier() || MatchedDecl->getName().starts_with("awesome_"))170 return;171 diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome")172 << MatchedDecl173 << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");174 diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note);175}176 177} // namespace clang::tidy::%(namespace)s178"""179 % {"check_name": check_name_camel, "module": module, "namespace": namespace}180 )181 182 183# Returns the source filename that implements the module.184def get_module_filename(module_path: str, module: str) -> str:185 modulecpp = list(186 filter(187 lambda p: p.lower() == module.lower() + "tidymodule.cpp",188 os.listdir(module_path),189 )190 )[0]191 return os.path.join(module_path, modulecpp)192 193 194# Modifies the module to include the new check.195def adapt_module(196 module_path: str, module: str, check_name: str, check_name_camel: str197) -> None:198 filename = get_module_filename(module_path, module)199 with io.open(filename, "r", encoding="utf8") as f:200 lines = f.readlines()201 202 print("Updating %s..." % filename)203 with io.open(filename, "w", encoding="utf8", newline="\n") as f:204 header_added = False205 header_found = False206 check_added = False207 check_fq_name = module + "-" + check_name208 check_decl = (209 " CheckFactories.registerCheck<"210 + check_name_camel211 + '>(\n "'212 + check_fq_name213 + '");\n'214 )215 216 lines_iter = iter(lines)217 try:218 while True:219 line = next(lines_iter)220 if not header_added:221 match = re.search('#include "(.*)"', line)222 if match:223 header_found = True224 if match.group(1) > check_name_camel:225 header_added = True226 f.write('#include "' + check_name_camel + '.h"\n')227 elif header_found:228 header_added = True229 f.write('#include "' + check_name_camel + '.h"\n')230 231 if not check_added:232 if line.strip() == "}":233 check_added = True234 f.write(check_decl)235 else:236 match = re.search(237 r'registerCheck<(.*)> *\( *(?:"([^"]*)")?', line238 )239 prev_line = None240 if match:241 current_check_name = match.group(2)242 if current_check_name is None:243 # If we didn't find the check name on this line, look on the244 # next one.245 prev_line = line246 line = next(lines_iter)247 match = re.search(' *"([^"]*)"', line)248 if match:249 current_check_name = match.group(1)250 assert current_check_name251 if current_check_name > check_fq_name:252 check_added = True253 f.write(check_decl)254 if prev_line:255 f.write(prev_line)256 f.write(line)257 except StopIteration:258 pass259 260 261# Adds a release notes entry.262def add_release_notes(263 module_path: str, module: str, check_name: str, description: str264) -> None:265 wrapped_desc = "\n".join(266 textwrap.wrap(267 description, width=80, initial_indent=" ", subsequent_indent=" "268 )269 )270 check_name_dashes = module + "-" + check_name271 filename = os.path.normpath(272 os.path.join(module_path, "../../docs/ReleaseNotes.rst")273 )274 with io.open(filename, "r", encoding="utf8") as f:275 lines = f.readlines()276 277 lineMatcher = re.compile("New checks")278 nextSectionMatcher = re.compile("New check aliases")279 checkMatcher = re.compile("- New :doc:`(.*)")280 281 print("Updating %s..." % filename)282 with io.open(filename, "w", encoding="utf8", newline="\n") as f:283 note_added = False284 header_found = False285 add_note_here = False286 287 for line in lines:288 if not note_added:289 match = lineMatcher.match(line)290 match_next = nextSectionMatcher.match(line)291 match_check = checkMatcher.match(line)292 if match_check:293 last_check = match_check.group(1)294 if last_check > check_name_dashes:295 add_note_here = True296 297 if match_next:298 add_note_here = True299 300 if match:301 header_found = True302 f.write(line)303 continue304 305 if line.startswith("^^^^"):306 f.write(line)307 continue308 309 if header_found and add_note_here:310 if not line.startswith("^^^^"):311 f.write(312 """- New :doc:`%s313 <clang-tidy/checks/%s/%s>` check.314 315%s316 317"""318 % (check_name_dashes, module, check_name, wrapped_desc)319 )320 note_added = True321 322 f.write(line)323 324 325# Adds a test for the check.326def write_test(327 module_path: str,328 module: str,329 check_name: str,330 test_extension: str,331 test_standard: Optional[str],332) -> None:333 test_standard = f"-std={test_standard}-or-later " if test_standard else ""334 check_name_dashes = module + "-" + check_name335 filename = os.path.normpath(336 os.path.join(337 module_path,338 "..",339 "..",340 "test",341 "clang-tidy",342 "checkers",343 module,344 check_name + "." + test_extension,345 )346 )347 print("Creating %s..." % filename)348 with io.open(filename, "w", encoding="utf8", newline="\n") as f:349 f.write(350 """\351// RUN: %%check_clang_tidy %(standard)s%%s %(check_name_dashes)s %%t352 353// FIXME: Add something that triggers the check here.354void f();355// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s]356 357// FIXME: Verify the applied fix.358// * Make the CHECK patterns specific enough and try to make verified lines359// unique to avoid incorrect matches.360// * Use {{}} for regular expressions.361// CHECK-FIXES: {{^}}void awesome_f();{{$}}362 363// FIXME: Add something that doesn't trigger the check here.364void awesome_f2();365"""366 % {"check_name_dashes": check_name_dashes, "standard": test_standard}367 )368 369 370def get_actual_filename(dirname: str, filename: str) -> str:371 if not os.path.isdir(dirname):372 return ""373 name = os.path.join(dirname, filename)374 if os.path.isfile(name):375 return name376 caselessname = filename.lower()377 for file in os.listdir(dirname):378 if file.lower() == caselessname:379 return os.path.join(dirname, file)380 return ""381 382 383# Recreates the list of checks in the docs/clang-tidy/checks directory.384def update_checks_list(clang_tidy_path: str) -> None:385 docs_dir = os.path.join(clang_tidy_path, "../docs/clang-tidy/checks")386 filename = os.path.normpath(os.path.join(docs_dir, "list.rst"))387 # Read the content of the current list.rst file388 with io.open(filename, "r", encoding="utf8") as f:389 lines = f.readlines()390 # Get all existing docs391 doc_files = []392 for subdir in filter(393 lambda s: os.path.isdir(os.path.join(docs_dir, s)), os.listdir(docs_dir)394 ):395 for file in filter(396 lambda s: s.endswith(".rst"), os.listdir(os.path.join(docs_dir, subdir))397 ):398 doc_files.append((subdir, file))399 doc_files.sort()400 401 # We couldn't find the source file from the check name, so try to find the402 # class name that corresponds to the check in the module file.403 def filename_from_module(module_name: str, check_name: str) -> str:404 module_path = os.path.join(clang_tidy_path, module_name)405 if not os.path.isdir(module_path):406 return ""407 module_file = get_module_filename(module_path, module_name)408 if not os.path.isfile(module_file):409 return ""410 with io.open(module_file, "r") as f:411 code = f.read()412 full_check_name = module_name + "-" + check_name413 name_pos = code.find('"' + full_check_name + '"')414 if name_pos == -1:415 return ""416 stmt_end_pos = code.find(";", name_pos)417 if stmt_end_pos == -1:418 return ""419 stmt_start_pos = code.rfind(";", 0, name_pos)420 if stmt_start_pos == -1:421 stmt_start_pos = code.rfind("{", 0, name_pos)422 if stmt_start_pos == -1:423 return ""424 stmt = code[stmt_start_pos + 1 : stmt_end_pos]425 matches = re.search(r'registerCheck<([^>:]*)>\(\s*"([^"]*)"\s*\)', stmt)426 if matches and matches[2] == full_check_name:427 class_name = matches[1]428 if "::" in class_name:429 parts = class_name.split("::")430 class_name = parts[-1]431 class_path = os.path.join(432 clang_tidy_path, module_name, "..", *parts[0:-1]433 )434 else:435 class_path = os.path.join(clang_tidy_path, module_name)436 return get_actual_filename(class_path, class_name + ".cpp")437 438 return ""439 440 # Examine code looking for a c'tor definition to get the base class name.441 def get_base_class(code: str, check_file: str) -> str:442 check_class_name = os.path.splitext(os.path.basename(check_file))[0]443 ctor_pattern = check_class_name + r"\([^:]*\)\s*:\s*([A-Z][A-Za-z0-9]*Check)\("444 matches = re.search(r"\s+" + check_class_name + "::" + ctor_pattern, code)445 446 # The constructor might be inline in the header.447 if not matches:448 header_file = os.path.splitext(check_file)[0] + ".h"449 if not os.path.isfile(header_file):450 return ""451 with io.open(header_file, encoding="utf8") as f:452 code = f.read()453 matches = re.search(" " + ctor_pattern, code)454 455 if matches and matches[1] != "ClangTidyCheck":456 return matches[1]457 return ""458 459 # Some simple heuristics to figure out if a check has an autofix or not.460 def has_fixits(code: str) -> bool:461 for needle in [462 "FixItHint",463 "ReplacementText",464 "fixit",465 "TransformerClangTidyCheck",466 ]:467 if needle in code:468 return True469 return False470 471 # Try to figure out of the check supports fixits.472 def has_auto_fix(check_name: str) -> str:473 dirname, _, check_name = check_name.partition("-")474 475 check_file = get_actual_filename(476 os.path.join(clang_tidy_path, dirname),477 get_camel_check_name(check_name) + ".cpp",478 )479 if not os.path.isfile(check_file):480 # Some older checks don't end with 'Check.cpp'481 check_file = get_actual_filename(482 os.path.join(clang_tidy_path, dirname),483 get_camel_name(check_name) + ".cpp",484 )485 if not os.path.isfile(check_file):486 # Some checks aren't in a file based on the check name.487 check_file = filename_from_module(dirname, check_name)488 if not check_file or not os.path.isfile(check_file):489 return ""490 491 with io.open(check_file, encoding="utf8") as f:492 code = f.read()493 if has_fixits(code):494 return ' "Yes"'495 496 base_class = get_base_class(code, check_file)497 if base_class:498 base_file = os.path.join(clang_tidy_path, dirname, base_class + ".cpp")499 if os.path.isfile(base_file):500 with io.open(base_file, encoding="utf8") as f:501 code = f.read()502 if has_fixits(code):503 return ' "Yes"'504 505 return ""506 507 def process_doc(doc_file: Tuple[str, str]) -> Tuple[str, Optional[Match[str]]]:508 check_name = doc_file[0] + "-" + doc_file[1].replace(".rst", "")509 510 with io.open(os.path.join(docs_dir, *doc_file), "r", encoding="utf8") as doc:511 content = doc.read()512 match = re.search(".*:orphan:.*", content)513 514 if match:515 # Orphan page, don't list it.516 return "", None517 518 match = re.search(r".*:http-equiv=refresh: \d+;URL=(.*).html(.*)", content)519 # Is it a redirect?520 return check_name, match521 522 def format_link(doc_file: Tuple[str, str]) -> str:523 check_name, match = process_doc(doc_file)524 if not match and check_name and not check_name.startswith("clang-analyzer-"):525 return " :doc:`%(check_name)s <%(module)s/%(check)s>`,%(autofix)s\n" % {526 "check_name": check_name,527 "module": doc_file[0],528 "check": doc_file[1].replace(".rst", ""),529 "autofix": has_auto_fix(check_name),530 }531 else:532 return ""533 534 def format_link_alias(doc_file: Tuple[str, str]) -> str:535 check_name, match = process_doc(doc_file)536 if (match or (check_name.startswith("clang-analyzer-"))) and check_name:537 module = doc_file[0]538 check_file = doc_file[1].replace(".rst", "")539 if (540 not match541 or match.group(1) == "https://clang.llvm.org/docs/analyzer/checkers"542 ):543 title = "Clang Static Analyzer " + check_file544 # Preserve the anchor in checkers.html from group 2.545 target = "" if not match else match.group(1) + ".html" + match.group(2)546 autofix = ""547 ref_begin = ""548 ref_end = "_"549 else:550 redirect_parts = re.search(r"^\.\./([^/]*)/([^/]*)$", match.group(1))551 assert redirect_parts552 title = redirect_parts[1] + "-" + redirect_parts[2]553 target = redirect_parts[1] + "/" + redirect_parts[2]554 autofix = has_auto_fix(title)555 ref_begin = ":doc:"556 ref_end = ""557 558 if target:559 # The checker is just a redirect.560 return (561 " :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(ref_begin)s`%(title)s <%(target)s>`%(ref_end)s,%(autofix)s\n"562 % {563 "check_name": check_name,564 "module": module,565 "check_file": check_file,566 "target": target,567 "title": title,568 "autofix": autofix,569 "ref_begin": ref_begin,570 "ref_end": ref_end,571 }572 )573 else:574 # The checker is just a alias without redirect.575 return (576 " :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(title)s,%(autofix)s\n"577 % {578 "check_name": check_name,579 "module": module,580 "check_file": check_file,581 "target": target,582 "title": title,583 "autofix": autofix,584 }585 )586 return ""587 588 checks = map(format_link, doc_files)589 checks_alias = map(format_link_alias, doc_files)590 591 print("Updating %s..." % filename)592 with io.open(filename, "w", encoding="utf8", newline="\n") as f:593 for line in lines:594 f.write(line)595 if line.strip() == ".. csv-table::":596 # We dump the checkers597 f.write(' :header: "Name", "Offers fixes"\n\n')598 f.writelines(checks)599 # and the aliases600 f.write("\nCheck aliases\n-------------\n\n")601 f.write(".. csv-table::\n")602 f.write(' :header: "Name", "Redirect", "Offers fixes"\n\n')603 f.writelines(checks_alias)604 break605 606 607# Adds a documentation for the check.608def write_docs(module_path: str, module: str, check_name: str) -> None:609 check_name_dashes = module + "-" + check_name610 filename = os.path.normpath(611 os.path.join(612 module_path, "../../docs/clang-tidy/checks/", module, check_name + ".rst"613 )614 )615 print("Creating %s..." % filename)616 with io.open(filename, "w", encoding="utf8", newline="\n") as f:617 f.write(618 """.. title:: clang-tidy - %(check_name_dashes)s619 620%(check_name_dashes)s621%(underline)s622 623FIXME: Describe what patterns does the check detect and why. Give examples.624"""625 % {626 "check_name_dashes": check_name_dashes,627 "underline": "=" * len(check_name_dashes),628 }629 )630 631 632def get_camel_name(check_name: str) -> str:633 return "".join(map(lambda elem: elem.capitalize(), check_name.split("-")))634 635 636def get_camel_check_name(check_name: str) -> str:637 return get_camel_name(check_name) + "Check"638 639 640def main() -> None:641 language_to_extension = {642 "c": "c",643 "c++": "cpp",644 "objc": "m",645 "objc++": "mm",646 }647 cpp_language_to_requirements = {648 "c++98": "CPlusPlus",649 "c++11": "CPlusPlus11",650 "c++14": "CPlusPlus14",651 "c++17": "CPlusPlus17",652 "c++20": "CPlusPlus20",653 "c++23": "CPlusPlus23",654 "c++26": "CPlusPlus26",655 }656 c_language_to_requirements = {657 "c99": None,658 "c11": "C11",659 "c17": "C17",660 "c23": "C23",661 "c27": "C2Y",662 }663 parser = argparse.ArgumentParser()664 parser.add_argument(665 "--update-docs",666 action="store_true",667 help="just update the list of documentation files, then exit",668 )669 parser.add_argument(670 "--language",671 help="language to use for new check (defaults to c++)",672 choices=language_to_extension.keys(),673 default=None,674 metavar="LANG",675 )676 parser.add_argument(677 "--description",678 "-d",679 help="short description of what the check does",680 default="FIXME: Write a short description",681 type=str,682 )683 parser.add_argument(684 "--standard",685 help="Specify a specific version of the language",686 choices=list(687 itertools.chain(688 cpp_language_to_requirements.keys(), c_language_to_requirements.keys()689 )690 ),691 default=None,692 )693 parser.add_argument(694 "module",695 nargs="?",696 help="module directory under which to place the new tidy check (e.g., misc)",697 )698 parser.add_argument(699 "check", nargs="?", help="name of new tidy check to add (e.g. foo-do-the-stuff)"700 )701 args = parser.parse_args()702 703 if args.update_docs:704 update_checks_list(os.path.dirname(sys.argv[0]))705 return706 707 if not args.module or not args.check:708 print("Module and check must be specified.")709 parser.print_usage()710 return711 712 module = args.module713 check_name = args.check714 check_name_camel = get_camel_check_name(check_name)715 if check_name.startswith(module):716 print(717 'Check name "%s" must not start with the module "%s". Exiting.'718 % (check_name, module)719 )720 return721 clang_tidy_path = os.path.dirname(sys.argv[0])722 module_path = os.path.join(clang_tidy_path, module)723 724 if not adapt_cmake(module_path, check_name_camel):725 return726 727 # Map module names to namespace names that don't conflict with widely used top-level namespaces.728 if module == "llvm":729 namespace = module + "_check"730 else:731 namespace = module732 733 description = args.description734 if not description.endswith("."):735 description += "."736 737 language = args.language738 739 if args.standard:740 if args.standard in cpp_language_to_requirements:741 if language and language != "c++":742 raise ValueError("C++ standard chosen when language is not C++")743 language = "c++"744 elif args.standard in c_language_to_requirements:745 if language and language != "c":746 raise ValueError("C standard chosen when language is not C")747 language = "c"748 749 if not language:750 language = "c++"751 752 language_restrict = None753 754 if language == "c":755 language_restrict = "!%(lang)s.CPlusPlus"756 extra = c_language_to_requirements.get(args.standard, None)757 if extra:758 language_restrict += f" && %(lang)s.{extra}"759 elif language == "c++":760 language_restrict = (761 f"%(lang)s.{cpp_language_to_requirements.get(args.standard, 'CPlusPlus')}"762 )763 elif language in ["objc", "objc++"]:764 language_restrict = "%(lang)s.ObjC"765 else:766 raise ValueError(f"Unsupported language '{language}' was specified")767 768 write_header(769 module_path,770 module,771 namespace,772 check_name,773 check_name_camel,774 description,775 language_restrict,776 )777 write_implementation(module_path, module, namespace, check_name_camel)778 adapt_module(module_path, module, check_name, check_name_camel)779 add_release_notes(module_path, module, check_name, description)780 test_extension = language_to_extension[language]781 write_test(module_path, module, check_name, test_extension, args.standard)782 write_docs(module_path, module, check_name)783 update_checks_list(clang_tidy_path)784 print("Done. Now it's your turn!")785 786 787if __name__ == "__main__":788 main()789