3 # ===- add_new_check.py - clang-tidy check generator ---------*- python -*--===#
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-exception
9 # ===-----------------------------------------------------------------------===#
19 # FIXME Python 3.9: Replace typing.Tuple with builtins.tuple.
20 from typing
import Optional
, Tuple
, Match
23 # Adapts the module's CMakelist file. Returns 'True' if it could add a new
24 # entry and 'False' if the entry already existed.
25 def adapt_cmake(module_path
: str, check_name_camel
: str) -> bool:
26 filename
= os
.path
.join(module_path
, "CMakeLists.txt")
28 # The documentation files are encoded using UTF-8, however on Windows the
29 # default encoding might be different (e.g. CP-1252). To make sure UTF-8 is
30 # always used, use `io.open(filename, mode, encoding='utf8')` for reading and
31 # writing files here and elsewhere.
32 with io
.open(filename
, "r", encoding
="utf8") as f
:
35 cpp_file
= check_name_camel
+ ".cpp"
37 # Figure out whether this check already exists.
39 if line
.strip() == cpp_file
:
42 print("Updating %s..." % filename
)
43 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
47 cpp_line
= line
.strip().endswith(".cpp")
48 if (not file_added
) and (cpp_line
or cpp_found
):
50 if (line
.strip() > cpp_file
) or (not cpp_line
):
51 f
.write(" " + cpp_file
+ "\n")
58 # Adds a header for the new check.
64 check_name_camel
: str,
68 wrapped_desc
= "\n".join(
70 description
, width
=80, initial_indent
="/// ", subsequent_indent
="/// "
74 override_supported
= """
75 bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
78 lang_restrict
% {"lang": "LangOpts"}
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
:
86 "LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_"
89 + check_name_camel
.upper()
93 f
.write(os
.path
.basename(filename
))
94 f
.write(" - clang-tidy ")
95 f
.write("-" * max(0, 42 - len(os
.path
.basename(filename
))))
96 f
.write("*- C++ -*-===//")
100 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
101 // See https://llvm.org/LICENSE.txt for license information.
102 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
104 //===----------------------------------------------------------------------===//
106 #ifndef %(header_guard)s
107 #define %(header_guard)s
109 #include "../ClangTidyCheck.h"
111 namespace clang::tidy::%(namespace)s {
115 /// For the user-facing documentation see:
116 /// http://clang.llvm.org/extra/clang-tidy/checks/%(module)s/%(check_name)s.html
117 class %(check_name_camel)s : public ClangTidyCheck {
119 %(check_name_camel)s(StringRef Name, ClangTidyContext *Context)
120 : ClangTidyCheck(Name, Context) {}
121 void registerMatchers(ast_matchers::MatchFinder *Finder) override;
122 void check(const ast_matchers::MatchFinder::MatchResult &Result) override;%(override_supported)s
125 } // namespace clang::tidy::%(namespace)s
127 #endif // %(header_guard)s
130 "header_guard": header_guard
,
131 "check_name_camel": check_name_camel
,
132 "check_name": check_name
,
134 "namespace": namespace
,
135 "description": wrapped_desc
,
136 "override_supported": override_supported
,
141 # Adds the implementation of the new check.
142 def write_implementation(
143 module_path
: str, module
: str, namespace
: str, check_name_camel
: str
145 filename
= os
.path
.join(module_path
, check_name_camel
) + ".cpp"
146 print("Creating %s..." % filename
)
147 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
149 f
.write(os
.path
.basename(filename
))
150 f
.write(" - clang-tidy ")
151 f
.write("-" * max(0, 51 - len(os
.path
.basename(filename
))))
156 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
157 // See https://llvm.org/LICENSE.txt for license information.
158 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
160 //===----------------------------------------------------------------------===//
162 #include "%(check_name)s.h"
163 #include "clang/ASTMatchers/ASTMatchFinder.h"
165 using namespace clang::ast_matchers;
167 namespace clang::tidy::%(namespace)s {
169 void %(check_name)s::registerMatchers(MatchFinder *Finder) {
170 // FIXME: Add matchers.
171 Finder->addMatcher(functionDecl().bind("x"), this);
174 void %(check_name)s::check(const MatchFinder::MatchResult &Result) {
175 // FIXME: Add callback implementation.
176 const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
177 if (!MatchedDecl->getIdentifier() || MatchedDecl->getName().starts_with("awesome_"))
179 diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome")
181 << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
182 diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note);
185 } // namespace clang::tidy::%(namespace)s
187 % {"check_name": check_name_camel
, "module": module
, "namespace": namespace
}
191 # Returns the source filename that implements the module.
192 def get_module_filename(module_path
: str, module
: str) -> str:
195 lambda p
: p
.lower() == module
.lower() + "tidymodule.cpp",
196 os
.listdir(module_path
),
199 return os
.path
.join(module_path
, modulecpp
)
202 # Modifies the module to include the new check.
204 module_path
: str, module
: str, check_name
: str, check_name_camel
: str
206 filename
= get_module_filename(module_path
, module
)
207 with io
.open(filename
, "r", encoding
="utf8") as f
:
208 lines
= f
.readlines()
210 print("Updating %s..." % filename
)
211 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
215 check_fq_name
= module
+ "-" + check_name
217 " CheckFactories.registerCheck<"
224 lines_iter
= iter(lines
)
227 line
= next(lines_iter
)
229 match
= re
.search('#include "(.*)"', line
)
232 if match
.group(1) > check_name_camel
:
234 f
.write('#include "' + check_name_camel
+ '.h"\n')
237 f
.write('#include "' + check_name_camel
+ '.h"\n')
240 if line
.strip() == "}":
245 r
'registerCheck<(.*)> *\( *(?:"([^"]*)")?', line
249 current_check_name
= match
.group(2)
250 if current_check_name
is None:
251 # If we didn't find the check name on this line, look on the
254 line
= next(lines_iter
)
255 match
= re
.search(' *"([^"]*)"', line
)
257 current_check_name
= match
.group(1)
258 assert current_check_name
259 if current_check_name
> check_fq_name
:
265 except StopIteration:
269 # Adds a release notes entry.
270 def add_release_notes(
271 module_path
: str, module
: str, check_name
: str, description
: str
273 wrapped_desc
= "\n".join(
275 description
, width
=80, initial_indent
=" ", subsequent_indent
=" "
278 check_name_dashes
= module
+ "-" + check_name
279 filename
= os
.path
.normpath(
280 os
.path
.join(module_path
, "../../docs/ReleaseNotes.rst")
282 with io
.open(filename
, "r", encoding
="utf8") as f
:
283 lines
= f
.readlines()
285 lineMatcher
= re
.compile("New checks")
286 nextSectionMatcher
= re
.compile("New check aliases")
287 checkMatcher
= re
.compile("- New :doc:`(.*)")
289 print("Updating %s..." % filename
)
290 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
293 add_note_here
= False
297 match
= lineMatcher
.match(line
)
298 match_next
= nextSectionMatcher
.match(line
)
299 match_check
= checkMatcher
.match(line
)
301 last_check
= match_check
.group(1)
302 if last_check
> check_name_dashes
:
313 if line
.startswith("^^^^"):
317 if header_found
and add_note_here
:
318 if not line
.startswith("^^^^"):
321 <clang-tidy/checks/%s/%s>` check.
326 % (check_name_dashes
, module
, check_name
, wrapped_desc
)
333 # Adds a test for the check.
339 test_standard
: Optional
[str],
341 test_standard
= f
"-std={test_standard}-or-later " if test_standard
else ""
342 check_name_dashes
= module
+ "-" + check_name
343 filename
= os
.path
.normpath(
352 check_name
+ "." + test_extension
,
355 print("Creating %s..." % filename
)
356 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
358 """// RUN: %%check_clang_tidy %(standard)s%%s %(check_name_dashes)s %%t
360 // FIXME: Add something that triggers the check here.
362 // CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s]
364 // FIXME: Verify the applied fix.
365 // * Make the CHECK patterns specific enough and try to make verified lines
366 // unique to avoid incorrect matches.
367 // * Use {{}} for regular expressions.
368 // CHECK-FIXES: {{^}}void awesome_f();{{$}}
370 // FIXME: Add something that doesn't trigger the check here.
373 % {"check_name_dashes": check_name_dashes
, "standard": test_standard
}
377 def get_actual_filename(dirname
: str, filename
: str) -> str:
378 if not os
.path
.isdir(dirname
):
380 name
= os
.path
.join(dirname
, filename
)
381 if os
.path
.isfile(name
):
383 caselessname
= filename
.lower()
384 for file in os
.listdir(dirname
):
385 if file.lower() == caselessname
:
386 return os
.path
.join(dirname
, file)
390 # Recreates the list of checks in the docs/clang-tidy/checks directory.
391 def update_checks_list(clang_tidy_path
: str) -> None:
392 docs_dir
= os
.path
.join(clang_tidy_path
, "../docs/clang-tidy/checks")
393 filename
= os
.path
.normpath(os
.path
.join(docs_dir
, "list.rst"))
394 # Read the content of the current list.rst file
395 with io
.open(filename
, "r", encoding
="utf8") as f
:
396 lines
= f
.readlines()
397 # Get all existing docs
399 for subdir
in filter(
400 lambda s
: os
.path
.isdir(os
.path
.join(docs_dir
, s
)), os
.listdir(docs_dir
)
403 lambda s
: s
.endswith(".rst"), os
.listdir(os
.path
.join(docs_dir
, subdir
))
405 doc_files
.append((subdir
, file))
408 # We couldn't find the source file from the check name, so try to find the
409 # class name that corresponds to the check in the module file.
410 def filename_from_module(module_name
: str, check_name
: str) -> str:
411 module_path
= os
.path
.join(clang_tidy_path
, module_name
)
412 if not os
.path
.isdir(module_path
):
414 module_file
= get_module_filename(module_path
, module_name
)
415 if not os
.path
.isfile(module_file
):
417 with io
.open(module_file
, "r") as f
:
419 full_check_name
= module_name
+ "-" + check_name
420 name_pos
= code
.find('"' + full_check_name
+ '"')
423 stmt_end_pos
= code
.find(";", name_pos
)
424 if stmt_end_pos
== -1:
426 stmt_start_pos
= code
.rfind(";", 0, name_pos
)
427 if stmt_start_pos
== -1:
428 stmt_start_pos
= code
.rfind("{", 0, name_pos
)
429 if stmt_start_pos
== -1:
431 stmt
= code
[stmt_start_pos
+ 1 : stmt_end_pos
]
432 matches
= re
.search(r
'registerCheck<([^>:]*)>\(\s*"([^"]*)"\s*\)', stmt
)
433 if matches
and matches
[2] == full_check_name
:
434 class_name
= matches
[1]
435 if "::" in class_name
:
436 parts
= class_name
.split("::")
437 class_name
= parts
[-1]
438 class_path
= os
.path
.join(
439 clang_tidy_path
, module_name
, "..", *parts
[0:-1]
442 class_path
= os
.path
.join(clang_tidy_path
, module_name
)
443 return get_actual_filename(class_path
, class_name
+ ".cpp")
447 # Examine code looking for a c'tor definition to get the base class name.
448 def get_base_class(code
: str, check_file
: str) -> str:
449 check_class_name
= os
.path
.splitext(os
.path
.basename(check_file
))[0]
450 ctor_pattern
= check_class_name
+ r
"\([^:]*\)\s*:\s*([A-Z][A-Za-z0-9]*Check)\("
451 matches
= re
.search(r
"\s+" + check_class_name
+ "::" + ctor_pattern
, code
)
453 # The constructor might be inline in the header.
455 header_file
= os
.path
.splitext(check_file
)[0] + ".h"
456 if not os
.path
.isfile(header_file
):
458 with io
.open(header_file
, encoding
="utf8") as f
:
460 matches
= re
.search(" " + ctor_pattern
, code
)
462 if matches
and matches
[1] != "ClangTidyCheck":
466 # Some simple heuristics to figure out if a check has an autofix or not.
467 def has_fixits(code
: str) -> bool:
472 "TransformerClangTidyCheck",
478 # Try to figure out of the check supports fixits.
479 def has_auto_fix(check_name
: str) -> str:
480 dirname
, _
, check_name
= check_name
.partition("-")
482 check_file
= get_actual_filename(
483 os
.path
.join(clang_tidy_path
, dirname
),
484 get_camel_check_name(check_name
) + ".cpp",
486 if not os
.path
.isfile(check_file
):
487 # Some older checks don't end with 'Check.cpp'
488 check_file
= get_actual_filename(
489 os
.path
.join(clang_tidy_path
, dirname
),
490 get_camel_name(check_name
) + ".cpp",
492 if not os
.path
.isfile(check_file
):
493 # Some checks aren't in a file based on the check name.
494 check_file
= filename_from_module(dirname
, check_name
)
495 if not check_file
or not os
.path
.isfile(check_file
):
498 with io
.open(check_file
, encoding
="utf8") as f
:
503 base_class
= get_base_class(code
, check_file
)
505 base_file
= os
.path
.join(clang_tidy_path
, dirname
, base_class
+ ".cpp")
506 if os
.path
.isfile(base_file
):
507 with io
.open(base_file
, encoding
="utf8") as f
:
514 def process_doc(doc_file
: Tuple
[str, str]) -> Tuple
[str, Optional
[Match
[str]]]:
515 check_name
= doc_file
[0] + "-" + doc_file
[1].replace(".rst", "")
517 with io
.open(os
.path
.join(docs_dir
, *doc_file
), "r", encoding
="utf8") as doc
:
519 match
= re
.search(".*:orphan:.*", content
)
522 # Orphan page, don't list it.
525 match
= re
.search(r
".*:http-equiv=refresh: \d+;URL=(.*).html(.*)", content
)
527 return check_name
, match
529 def format_link(doc_file
: Tuple
[str, str]) -> str:
530 check_name
, match
= process_doc(doc_file
)
531 if not match
and check_name
and not check_name
.startswith("clang-analyzer-"):
532 return " :doc:`%(check_name)s <%(module)s/%(check)s>`,%(autofix)s\n" % {
533 "check_name": check_name
,
534 "module": doc_file
[0],
535 "check": doc_file
[1].replace(".rst", ""),
536 "autofix": has_auto_fix(check_name
),
541 def format_link_alias(doc_file
: Tuple
[str, str]) -> str:
542 check_name
, match
= process_doc(doc_file
)
543 if (match
or (check_name
.startswith("clang-analyzer-"))) and check_name
:
545 check_file
= doc_file
[1].replace(".rst", "")
548 or match
.group(1) == "https://clang.llvm.org/docs/analyzer/checkers"
550 title
= "Clang Static Analyzer " + check_file
551 # Preserve the anchor in checkers.html from group 2.
552 target
= "" if not match
else match
.group(1) + ".html" + match
.group(2)
557 redirect_parts
= re
.search(r
"^\.\./([^/]*)/([^/]*)$", match
.group(1))
558 assert redirect_parts
559 title
= redirect_parts
[1] + "-" + redirect_parts
[2]
560 target
= redirect_parts
[1] + "/" + redirect_parts
[2]
561 autofix
= has_auto_fix(title
)
566 # The checker is just a redirect.
568 " :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(ref_begin)s`%(title)s <%(target)s>`%(ref_end)s,%(autofix)s\n"
570 "check_name": check_name
,
572 "check_file": check_file
,
576 "ref_begin": ref_begin
,
581 # The checker is just a alias without redirect.
583 " :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(title)s,%(autofix)s\n"
585 "check_name": check_name
,
587 "check_file": check_file
,
595 checks
= map(format_link
, doc_files
)
596 checks_alias
= map(format_link_alias
, doc_files
)
598 print("Updating %s..." % filename
)
599 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
602 if line
.strip() == ".. csv-table::":
603 # We dump the checkers
604 f
.write(' :header: "Name", "Offers fixes"\n\n')
607 f
.write("\nCheck aliases\n-------------\n\n")
608 f
.write(".. csv-table::\n")
609 f
.write(' :header: "Name", "Redirect", "Offers fixes"\n\n')
610 f
.writelines(checks_alias
)
614 # Adds a documentation for the check.
615 def write_docs(module_path
: str, module
: str, check_name
: str) -> None:
616 check_name_dashes
= module
+ "-" + check_name
617 filename
= os
.path
.normpath(
619 module_path
, "../../docs/clang-tidy/checks/", module
, check_name
+ ".rst"
622 print("Creating %s..." % filename
)
623 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
625 """.. title:: clang-tidy - %(check_name_dashes)s
627 %(check_name_dashes)s
630 FIXME: Describe what patterns does the check detect and why. Give examples.
633 "check_name_dashes": check_name_dashes
,
634 "underline": "=" * len(check_name_dashes
),
639 def get_camel_name(check_name
: str) -> str:
640 return "".join(map(lambda elem
: elem
.capitalize(), check_name
.split("-")))
643 def get_camel_check_name(check_name
: str) -> str:
644 return get_camel_name(check_name
) + "Check"
648 language_to_extension
= {
654 cpp_language_to_requirements
= {
655 "c++98": "CPlusPlus",
656 "c++11": "CPlusPlus11",
657 "c++14": "CPlusPlus14",
658 "c++17": "CPlusPlus17",
659 "c++20": "CPlusPlus20",
660 "c++23": "CPlusPlus23",
661 "c++26": "CPlusPlus26",
663 c_language_to_requirements
= {
670 parser
= argparse
.ArgumentParser()
674 help="just update the list of documentation files, then exit",
678 help="language to use for new check (defaults to c++)",
679 choices
=language_to_extension
.keys(),
686 help="short description of what the check does",
687 default
="FIXME: Write a short description",
692 help="Specify a specific version of the language",
695 cpp_language_to_requirements
.keys(), c_language_to_requirements
.keys()
703 help="module directory under which to place the new tidy check (e.g., misc)",
706 "check", nargs
="?", help="name of new tidy check to add (e.g. foo-do-the-stuff)"
708 args
= parser
.parse_args()
711 update_checks_list(os
.path
.dirname(sys
.argv
[0]))
714 if not args
.module
or not args
.check
:
715 print("Module and check must be specified.")
720 check_name
= args
.check
721 check_name_camel
= get_camel_check_name(check_name
)
722 if check_name
.startswith(module
):
724 'Check name "%s" must not start with the module "%s". Exiting.'
725 % (check_name
, module
)
728 clang_tidy_path
= os
.path
.dirname(sys
.argv
[0])
729 module_path
= os
.path
.join(clang_tidy_path
, module
)
731 if not adapt_cmake(module_path
, check_name_camel
):
734 # Map module names to namespace names that don't conflict with widely used top-level namespaces.
736 namespace
= module
+ "_check"
740 description
= args
.description
741 if not description
.endswith("."):
744 language
= args
.language
747 if args
.standard
in cpp_language_to_requirements
:
748 if language
and language
!= "c++":
749 raise ValueError("C++ standard chosen when language is not C++")
751 elif args
.standard
in c_language_to_requirements
:
752 if language
and language
!= "c":
753 raise ValueError("C standard chosen when language is not C")
759 language_restrict
= None
762 language_restrict
= "!%(lang)s.CPlusPlus"
763 extra
= c_language_to_requirements
.get(args
.standard
, None)
765 language_restrict
+= f
" && %(lang)s.{extra}"
766 elif language
== "c++":
767 language_restrict
= (
768 f
"%(lang)s.{cpp_language_to_requirements.get(args.standard, 'CPlusPlus')}"
770 elif language
in ["objc", "objc++"]:
771 language_restrict
= "%(lang)s.ObjC"
773 raise ValueError(f
"Unsupported language '{language}' was specified")
784 write_implementation(module_path
, module
, namespace
, check_name_camel
)
785 adapt_module(module_path
, module
, check_name
, check_name_camel
)
786 add_release_notes(module_path
, module
, check_name
, description
)
787 test_extension
= language_to_extension
[language
]
788 write_test(module_path
, module
, check_name
, test_extension
, args
.standard
)
789 write_docs(module_path
, module
, check_name
)
790 update_checks_list(clang_tidy_path
)
791 print("Done. Now it's your turn!")
794 if __name__
== "__main__":