363 docs_dir = os.path.join(clang_tidy_path,
"../docs/clang-tidy/checks")
364 filename = os.path.normpath(os.path.join(docs_dir,
"list.md"))
366 with open(filename,
"r", encoding=
"utf8")
as f:
367 lines = f.readlines()
370 for subdir
in filter(
371 lambda s: os.path.isdir(os.path.join(docs_dir, s)), os.listdir(docs_dir)
374 if subdir ==
"clang-analyzer":
376 for file
in os.listdir(os.path.join(docs_dir, subdir)):
379 if os.path.splitext(file)[1]
in (
".md",
".rst"):
380 doc_files.append((subdir, file))
385 check_alias_lines = lines[lines.index(
"## Check aliases\n") :]
386 clang_analyzer_alias_rows = [
388 for row
in check_alias_lines
389 if re.match(
r"^\| \{doc\}`.*clang-analyzer", row)
394 def filename_from_module(module_name: str, check_name: str) -> str:
395 module_path = os.path.join(clang_tidy_path, module_name)
396 if not os.path.isdir(module_path):
399 if not os.path.isfile(module_file):
401 with open(module_file,
"r")
as f:
403 full_check_name = f
"{module_name}-{check_name}"
404 if (name_pos := code.find(f
'"{full_check_name}"')) == -1:
406 if (stmt_end_pos := code.find(
";", name_pos)) == -1:
408 if (stmt_start_pos := code.rfind(
";", 0, name_pos)) == -1
and (
409 stmt_start_pos := code.rfind(
"{", 0, name_pos)
412 stmt = code[stmt_start_pos + 1 : stmt_end_pos]
413 matches = re.search(
r'registerCheck<([^>:]*)>\(\s*"([^"]*)"\s*\)', stmt)
414 if matches
and matches[2] == full_check_name:
415 class_name = matches[1]
416 if "::" in class_name:
417 parts = class_name.split(
"::")
418 class_name = parts[-1]
419 class_path = os.path.join(
420 clang_tidy_path, module_name,
"..", *parts[0:-1]
423 class_path = os.path.join(clang_tidy_path, module_name)
429 def get_base_class(code: str, check_file: str) -> str:
430 check_class_name = os.path.splitext(os.path.basename(check_file))[0]
431 ctor_pattern = rf
"{check_class_name}\([^:]*\)\s*:\s*([A-Z][A-Za-z0-9]*Check)\("
432 matches = re.search(rf
"\s+{check_class_name}::{ctor_pattern}", code)
436 header_file = f
"{os.path.splitext(check_file)[0]}.h"
437 if not os.path.isfile(header_file):
439 with open(header_file, encoding=
"utf8")
as f:
441 matches = re.search(rf
" {ctor_pattern}", code)
443 if matches
and matches[1] !=
"ClangTidyCheck":
448 def has_fixits(code: str) -> bool:
454 "TransformerClangTidyCheck",
461 def has_auto_fix(check_name: str) -> str:
462 dirname, _, check_name = check_name.partition(
"-")
465 os.path.join(clang_tidy_path, dirname),
466 f
"{get_camel_check_name(check_name)}.cpp",
468 if not os.path.isfile(check_file):
471 os.path.join(clang_tidy_path, dirname),
472 f
"{get_camel_name(check_name)}.cpp",
474 if not os.path.isfile(check_file):
476 check_file = filename_from_module(dirname, check_name)
477 if not (check_file
and os.path.isfile(check_file)):
480 with open(check_file, encoding=
"utf8")
as f:
485 if base_class := get_base_class(code, check_file):
486 base_file = os.path.join(clang_tidy_path, dirname, f
"{base_class}.cpp")
487 if os.path.isfile(base_file):
488 with open(base_file, encoding=
"utf8")
as f:
495 def detect_alias_target(check_name: str, content: str) -> Optional[str]:
496 """Return the documentation target for non-redirect alias pages.
498 This recognizes pages that keep their own documentation content, but
499 whose paragraph explicitly states that the current check is an
500 alias of another check.
503 re.sub(
r"\s+",
" ", paragraph.strip())
504 for paragraph
in re.split(
r"\n\s*\n", content)
508 self_alias = re.compile(
509 r"^This check is an alias(?: of check| for)\b",
512 named_alias = re.compile(
513 rf
"^The\s+`?{re.escape(check_name)}(?:\s+check)?`?"
514 rf
"(?:\s+check)?\s+is\s+an\s+alias,?\s+please\s+see\b",
518 for paragraph
in paragraphs:
519 if self_alias.search(paragraph)
or named_alias.search(paragraph):
523 if match := re.search(
524 r"(?:\{doc\}|:doc:)`[^`<]+?<([^>]+)>`", paragraph
526 return match.group(1)
529 if match := re.search(
r"`[^`<]+?<(.+?)\.html(?:#[^>]+)?>`_", paragraph):
530 return match.group(1)
532 if match := re.search(
r"\[[^]]+\]\(([^)]+)\)", paragraph):
533 return match.group(1)
536 def doc_file_stem(doc_file: Tuple[str, str]) -> str:
537 return os.path.splitext(doc_file[1])[0]
539 def process_doc(doc_file: Tuple[str, str]) -> Tuple[str, Optional[str]]:
540 check_name = f
"{doc_file[0]}-{doc_file_stem(doc_file)}"
542 with open(os.path.join(docs_dir, *doc_file),
"r", encoding=
"utf8")
as doc:
548 if re.search(
r"^\s*(?::orphan:|orphan:\s*true)\s*$", content, re.MULTILINE):
552 return check_name, detect_alias_target(check_name, content)
554 def format_doc_ref(label: str, module: str, check_file: str) -> str:
555 return f
"{{doc}}`{label} <{module}/{check_file}>`"
557 def has_auto_fix_cell(check_name: str) -> str:
558 return has_auto_fix(check_name).strip().strip(
'"')
560 def format_link(doc_file: Tuple[str, str]) -> str:
561 check_name, match = process_doc(doc_file)
562 if not match
and check_name
and not check_name.startswith(
"clang-analyzer-"):
564 f
"| {format_doc_ref(check_name, doc_file[0], doc_file_stem(doc_file))} | "
565 f
"{has_auto_fix_cell(check_name)} |\n"
570 def format_link_alias(doc_file: Tuple[str, str]) -> str:
571 check_name, match = process_doc(doc_file)
572 is_clang_analyzer = check_name.startswith(
"clang-analyzer-")
573 if not check_name
or is_clang_analyzer
or not match:
577 check_file = doc_file_stem(doc_file)
579 redirect_parts = re.search(
r"^(?:\.\./([^/]+)/)?([^/]+)$", match)
580 assert redirect_parts
581 redirect_module = redirect_parts[1]
or module
582 redirect_check = redirect_parts[2]
583 title = f
"{redirect_module}-{redirect_check}"
584 redirect = format_doc_ref(title, redirect_module, redirect_check)
585 autofix = has_auto_fix_cell(title)
588 f
"| {format_doc_ref(check_name, module, check_file)} | "
589 f
"{redirect} | {autofix} |\n"
592 print(f
"Updating {filename}...")
593 with open(filename,
"w", encoding=
"utf8", newline=
"\n")
as f:
596 if line ==
"| Name | Offers fixes |\n":
598 f.write(
"| --- | --- |\n")
599 f.writelines(sorted(filter(
None, map(format_link, doc_files))))
601 f.write(
"\n## Check aliases\n\n")
602 f.write(
"| Name | Redirect | Offers fixes |\n")
603 f.write(
"| --- | --- | --- |\n")
604 alias_rows = list(map(format_link_alias, doc_files))
605 alias_rows.extend(clang_analyzer_alias_rows)
606 f.writelines(sorted(filter(
None, alias_rows)))
641 language_to_extension = {
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",
656 c_language_to_requirements = {
663 parser = argparse.ArgumentParser()
667 help=
"just update the list of documentation files, then exit",
671 help=
"language to use for new check (defaults to c++)",
672 choices=language_to_extension.keys(),
679 help=
"short description of what the check does",
680 default=
"FIXME: Write a short description",
685 help=
"Specify a specific version of the language",
688 cpp_language_to_requirements.keys(), c_language_to_requirements.keys()
696 help=
"module directory under which to place the new tidy check (e.g., misc)",
699 "check", nargs=
"?", help=
"name of new tidy check to add (e.g. foo-do-the-stuff)"
701 args = parser.parse_args()
707 if not args.module
or not args.check:
708 print(
"Module and check must be specified.")
713 check_name = args.check
715 if check_name.startswith(module):
717 f
'Check name "{check_name}" must not start with the module "{module}". Exiting.'
720 clang_tidy_path = os.path.dirname(sys.argv[0])
721 module_path = os.path.join(clang_tidy_path, module)
728 namespace = f
"{module}_check"
732 description = args.description
733 if not description.endswith(
"."):
736 language = args.language
739 if args.standard
in cpp_language_to_requirements:
740 if language
and language !=
"c++":
741 raise ValueError(
"C++ standard chosen when language is not C++")
743 elif args.standard
in c_language_to_requirements:
744 if language
and language !=
"c":
745 raise ValueError(
"C standard chosen when language is not C")
751 language_restrict =
None
754 language_restrict =
"!%(lang)s.CPlusPlus"
755 if extra := c_language_to_requirements.get(args.standard,
None):
756 language_restrict += f
" && %(lang)s.{extra}"
757 elif language ==
"c++":
758 language_restrict = (
759 f
"%(lang)s.{cpp_language_to_requirements.get(args.standard, 'CPlusPlus')}"
761 elif language
in [
"objc",
"objc++"]:
762 language_restrict =
"%(lang)s.ObjC"
764 raise ValueError(f
"Unsupported language '{language}' was specified")
776 adapt_module(module_path, module, check_name, check_name_camel)
778 test_extension = language_to_extension[language]
779 write_test(module_path, module, check_name, test_extension, args.standard)
782 print(
"Done. Now it's your turn!")