253 module_path: str, module: str, check_name: str, description: str
255 wrapped_desc =
"\n".join(
257 description, width=80, initial_indent=
" ", subsequent_indent=
" "
260 check_name_dashes = f
"{module}-{check_name}"
261 filename = os.path.normpath(
262 os.path.join(module_path,
"../../docs/ReleaseNotes.rst")
264 with open(filename,
"r", encoding=
"utf8")
as f:
265 lines = f.readlines()
267 lineMatcher = re.compile(
"New checks")
268 nextSectionMatcher = re.compile(
"New check aliases")
269 checkMatcher = re.compile(
"- New :doc:`(.*)")
271 print(f
"Updating {filename}...")
272 with open(filename,
"w", encoding=
"utf8", newline=
"\n")
as f:
275 add_note_here =
False
279 if match_check := checkMatcher.match(line):
280 last_check = match_check.group(1)
281 if last_check > check_name_dashes:
284 if nextSectionMatcher.match(line):
287 if lineMatcher.match(line):
292 if line.startswith(
"^^^^"):
296 if header_found
and add_note_here:
297 if not line.startswith(
"^^^^"):
299 f
"""- New :doc:`{check_name_dashes}
300 <clang-tidy/checks/{module}/{check_name}>` check.
371 docs_dir = os.path.join(clang_tidy_path,
"../docs/clang-tidy/checks")
372 filename = os.path.normpath(os.path.join(docs_dir,
"list.rst"))
374 with open(filename,
"r", encoding=
"utf8")
as f:
375 lines = f.readlines()
378 for subdir
in filter(
379 lambda s: os.path.isdir(os.path.join(docs_dir, s)), os.listdir(docs_dir)
382 methodcaller(
"endswith",
".rst"), os.listdir(os.path.join(docs_dir, subdir))
384 doc_files.append((subdir, file))
389 def filename_from_module(module_name: str, check_name: str) -> str:
390 module_path = os.path.join(clang_tidy_path, module_name)
391 if not os.path.isdir(module_path):
394 if not os.path.isfile(module_file):
396 with open(module_file,
"r")
as f:
398 full_check_name = f
"{module_name}-{check_name}"
399 if (name_pos := code.find(f
'"{full_check_name}"')) == -1:
401 if (stmt_end_pos := code.find(
";", name_pos)) == -1:
403 if (stmt_start_pos := code.rfind(
";", 0, name_pos)) == -1
and (
404 stmt_start_pos := code.rfind(
"{", 0, name_pos)
407 stmt = code[stmt_start_pos + 1 : stmt_end_pos]
408 matches = re.search(
r'registerCheck<([^>:]*)>\(\s*"([^"]*)"\s*\)', stmt)
409 if matches
and matches[2] == full_check_name:
410 class_name = matches[1]
411 if "::" in class_name:
412 parts = class_name.split(
"::")
413 class_name = parts[-1]
414 class_path = os.path.join(
415 clang_tidy_path, module_name,
"..", *parts[0:-1]
418 class_path = os.path.join(clang_tidy_path, module_name)
424 def get_base_class(code: str, check_file: str) -> str:
425 check_class_name = os.path.splitext(os.path.basename(check_file))[0]
426 ctor_pattern = rf
"{check_class_name}\([^:]*\)\s*:\s*([A-Z][A-Za-z0-9]*Check)\("
427 matches = re.search(rf
"\s+{check_class_name}::{ctor_pattern}", code)
431 header_file = f
"{os.path.splitext(check_file)[0]}.h"
432 if not os.path.isfile(header_file):
434 with open(header_file, encoding=
"utf8")
as f:
436 matches = re.search(rf
" {ctor_pattern}", code)
438 if matches
and matches[1] !=
"ClangTidyCheck":
443 def has_fixits(code: str) -> bool:
449 "TransformerClangTidyCheck",
456 def has_auto_fix(check_name: str) -> str:
457 dirname, _, check_name = check_name.partition(
"-")
460 os.path.join(clang_tidy_path, dirname),
461 f
"{get_camel_check_name(check_name)}.cpp",
463 if not os.path.isfile(check_file):
466 os.path.join(clang_tidy_path, dirname),
467 f
"{get_camel_name(check_name)}.cpp",
469 if not os.path.isfile(check_file):
471 check_file = filename_from_module(dirname, check_name)
472 if not (check_file
and os.path.isfile(check_file)):
475 with open(check_file, encoding=
"utf8")
as f:
480 if base_class := get_base_class(code, check_file):
481 base_file = os.path.join(clang_tidy_path, dirname, f
"{base_class}.cpp")
482 if os.path.isfile(base_file):
483 with open(base_file, encoding=
"utf8")
as f:
490 def detect_alias_target(check_name: str, content: str) -> Optional[str]:
491 """Return the :doc: target for non-redirect alias pages.
493 This recognizes pages that keep their own documentation content, but
494 whose paragraph explicitly states that the current check is an
495 alias of another check.
498 re.sub(
r"\s+",
" ", paragraph.strip())
499 for paragraph
in re.split(
r"\n\s*\n", content)
503 self_alias = re.compile(
504 r"^This check is an alias(?: of check| for)\b",
507 named_alias = re.compile(
508 rf
"^The\s+`?{re.escape(check_name)}(?:\s+check)?`?"
509 rf
"(?:\s+check)?\s+is\s+an\s+alias,?\s+please\s+see\b",
513 for paragraph
in paragraphs:
514 if self_alias.search(paragraph)
or named_alias.search(paragraph):
515 if match := re.search(
r":doc:`[^`<]+?<([^>]+)>`", paragraph):
516 return match.group(1)
517 if match := re.search(
r"`[^`<]+?<(.+?)\.html(?:#[^>]+)?>`_", paragraph):
518 return match.group(1)
521 def process_doc(doc_file: Tuple[str, str]) -> Tuple[str, Optional[str]]:
522 check_name = f
"{doc_file[0]}-{doc_file[1].replace('.rst', '')}"
524 with open(os.path.join(docs_dir, *doc_file),
"r", encoding=
"utf8")
as doc:
527 if match := re.search(
".*:orphan:.*", content):
531 return check_name, detect_alias_target(check_name, content)
533 def format_link(doc_file: Tuple[str, str]) -> str:
534 check_name, match = process_doc(doc_file)
535 if not match
and check_name
and not check_name.startswith(
"clang-analyzer-"):
537 f
" :doc:`{check_name} <{doc_file[0]}/{doc_file[1].replace('.rst', '')}>`,"
538 f
"{has_auto_fix(check_name)}\n"
543 def format_link_alias(doc_file: Tuple[str, str]) -> str:
544 check_name, match = process_doc(doc_file)
545 is_clang_analyzer = check_name.startswith(
"clang-analyzer-")
546 if not check_name
or (
not match
and not is_clang_analyzer):
550 check_file = doc_file[1].replace(
".rst",
"")
551 if is_clang_analyzer:
552 title = f
"Clang Static Analyzer {check_file}"
555 with open(os.path.join(docs_dir, *doc_file),
"r", encoding=
"utf8")
as doc:
557 redirect = re.search(
558 r".*:http-equiv=refresh: \d+;URL=(.*).html(.*)", content
562 "" if not redirect
else f
"{redirect.group(1)}.html{redirect.group(2)}"
569 redirect_parts = re.search(
r"^(?:\.\./([^/]+)/)?([^/]+)$", match)
570 assert redirect_parts
571 redirect_module = redirect_parts[1]
or module
572 title = f
"{redirect_module}-{redirect_parts[2]}"
573 target = f
"{redirect_module}/{redirect_parts[2]}"
574 autofix = has_auto_fix(title)
581 f
" :doc:`{check_name} <{module}/{check_file}>`, "
582 f
"{ref_begin}`{title} <{target}>`{ref_end},{autofix}\n"
586 return f
" :doc:`{check_name} <{module}/{check_file}>`, {title},{autofix}\n"
588 print(f
"Updating {filename}...")
589 with open(filename,
"w", encoding=
"utf8", newline=
"\n")
as f:
592 if line.strip() ==
".. csv-table::":
594 f.write(
' :header: "Name", "Offers fixes"\n\n')
595 f.writelines(map(format_link, doc_files))
597 f.write(
"\nCheck aliases\n-------------\n\n")
598 f.write(
".. csv-table::\n")
599 f.write(
' :header: "Name", "Redirect", "Offers fixes"\n\n')
600 f.writelines(map(format_link_alias, doc_files))
638 language_to_extension = {
644 cpp_language_to_requirements = {
645 "c++98":
"CPlusPlus",
646 "c++11":
"CPlusPlus11",
647 "c++14":
"CPlusPlus14",
648 "c++17":
"CPlusPlus17",
649 "c++20":
"CPlusPlus20",
650 "c++23":
"CPlusPlus23",
651 "c++26":
"CPlusPlus26",
653 c_language_to_requirements = {
660 parser = argparse.ArgumentParser()
664 help=
"just update the list of documentation files, then exit",
668 help=
"language to use for new check (defaults to c++)",
669 choices=language_to_extension.keys(),
676 help=
"short description of what the check does",
677 default=
"FIXME: Write a short description",
682 help=
"Specify a specific version of the language",
685 cpp_language_to_requirements.keys(), c_language_to_requirements.keys()
693 help=
"module directory under which to place the new tidy check (e.g., misc)",
696 "check", nargs=
"?", help=
"name of new tidy check to add (e.g. foo-do-the-stuff)"
698 args = parser.parse_args()
704 if not args.module
or not args.check:
705 print(
"Module and check must be specified.")
710 check_name = args.check
712 if check_name.startswith(module):
714 f
'Check name "{check_name}" must not start with the module "{module}". Exiting.'
717 clang_tidy_path = os.path.dirname(sys.argv[0])
718 module_path = os.path.join(clang_tidy_path, module)
725 namespace = f
"{module}_check"
729 description = args.description
730 if not description.endswith(
"."):
733 language = args.language
736 if args.standard
in cpp_language_to_requirements:
737 if language
and language !=
"c++":
738 raise ValueError(
"C++ standard chosen when language is not C++")
740 elif args.standard
in c_language_to_requirements:
741 if language
and language !=
"c":
742 raise ValueError(
"C standard chosen when language is not C")
748 language_restrict =
None
751 language_restrict =
"!%(lang)s.CPlusPlus"
752 if extra := c_language_to_requirements.get(args.standard,
None):
753 language_restrict += f
" && %(lang)s.{extra}"
754 elif language ==
"c++":
755 language_restrict = (
756 f
"%(lang)s.{cpp_language_to_requirements.get(args.standard, 'CPlusPlus')}"
758 elif language
in [
"objc",
"objc++"]:
759 language_restrict =
"%(lang)s.ObjC"
761 raise ValueError(f
"Unsupported language '{language}' was specified")
773 adapt_module(module_path, module, check_name, check_name_camel)
775 test_extension = language_to_extension[language]
776 write_test(module_path, module, check_name, test_extension, args.standard)
779 print(
"Done. Now it's your turn!")