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 # ===-----------------------------------------------------------------------===#
11 from __future__
import print_function
12 from __future__
import unicode_literals
20 # Adapts the module's CMakelist file. Returns 'True' if it could add a new
21 # entry and 'False' if the entry already existed.
22 def adapt_cmake(module_path
, check_name_camel
):
23 filename
= os
.path
.join(module_path
, "CMakeLists.txt")
25 # The documentation files are encoded using UTF-8, however on Windows the
26 # default encoding might be different (e.g. CP-1252). To make sure UTF-8 is
27 # always used, use `io.open(filename, mode, encoding='utf8')` for reading and
28 # writing files here and elsewhere.
29 with io
.open(filename
, "r", encoding
="utf8") as f
:
32 cpp_file
= check_name_camel
+ ".cpp"
34 # Figure out whether this check already exists.
36 if line
.strip() == cpp_file
:
39 print("Updating %s..." % filename
)
40 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
44 cpp_line
= line
.strip().endswith(".cpp")
45 if (not file_added
) and (cpp_line
or cpp_found
):
47 if (line
.strip() > cpp_file
) or (not cpp_line
):
48 f
.write(" " + cpp_file
+ "\n")
55 # Adds a header for the new check.
56 def write_header(module_path
, module
, namespace
, check_name
, check_name_camel
):
57 filename
= os
.path
.join(module_path
, check_name_camel
) + ".h"
58 print("Creating %s..." % filename
)
59 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
61 "LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_"
64 + check_name_camel
.upper()
68 f
.write(os
.path
.basename(filename
))
69 f
.write(" - clang-tidy ")
70 f
.write("-" * max(0, 42 - len(os
.path
.basename(filename
))))
71 f
.write("*- C++ -*-===//")
75 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
76 // See https://llvm.org/LICENSE.txt for license information.
77 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
79 //===----------------------------------------------------------------------===//
81 #ifndef %(header_guard)s
82 #define %(header_guard)s
84 #include "../ClangTidyCheck.h"
86 namespace clang::tidy::%(namespace)s {
88 /// FIXME: Write a short description.
90 /// For the user-facing documentation see:
91 /// http://clang.llvm.org/extra/clang-tidy/checks/%(module)s/%(check_name)s.html
92 class %(check_name_camel)s : public ClangTidyCheck {
94 %(check_name_camel)s(StringRef Name, ClangTidyContext *Context)
95 : ClangTidyCheck(Name, Context) {}
96 void registerMatchers(ast_matchers::MatchFinder *Finder) override;
97 void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
100 } // namespace clang::tidy::%(namespace)s
102 #endif // %(header_guard)s
105 "header_guard": header_guard
,
106 "check_name_camel": check_name_camel
,
107 "check_name": check_name
,
109 "namespace": namespace
,
114 # Adds the implementation of the new check.
115 def write_implementation(module_path
, module
, namespace
, check_name_camel
):
116 filename
= os
.path
.join(module_path
, check_name_camel
) + ".cpp"
117 print("Creating %s..." % filename
)
118 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
120 f
.write(os
.path
.basename(filename
))
121 f
.write(" - clang-tidy ")
122 f
.write("-" * max(0, 51 - len(os
.path
.basename(filename
))))
127 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
128 // See https://llvm.org/LICENSE.txt for license information.
129 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
131 //===----------------------------------------------------------------------===//
133 #include "%(check_name)s.h"
134 #include "clang/AST/ASTContext.h"
135 #include "clang/ASTMatchers/ASTMatchFinder.h"
137 using namespace clang::ast_matchers;
139 namespace clang::tidy::%(namespace)s {
141 void %(check_name)s::registerMatchers(MatchFinder *Finder) {
142 // FIXME: Add matchers.
143 Finder->addMatcher(functionDecl().bind("x"), this);
146 void %(check_name)s::check(const MatchFinder::MatchResult &Result) {
147 // FIXME: Add callback implementation.
148 const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
149 if (!MatchedDecl->getIdentifier() || MatchedDecl->getName().startswith("awesome_"))
151 diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome")
153 << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
154 diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note);
157 } // namespace clang::tidy::%(namespace)s
159 % {"check_name": check_name_camel
, "module": module
, "namespace": namespace
}
163 # Returns the source filename that implements the module.
164 def get_module_filename(module_path
, module
):
167 lambda p
: p
.lower() == module
.lower() + "tidymodule.cpp",
168 os
.listdir(module_path
),
171 return os
.path
.join(module_path
, modulecpp
)
174 # Modifies the module to include the new check.
175 def adapt_module(module_path
, module
, check_name
, check_name_camel
):
176 filename
= get_module_filename(module_path
, module
)
177 with io
.open(filename
, "r", encoding
="utf8") as f
:
178 lines
= f
.readlines()
180 print("Updating %s..." % filename
)
181 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
185 check_fq_name
= module
+ "-" + check_name
187 " CheckFactories.registerCheck<"
199 match
= re
.search('#include "(.*)"', line
)
202 if match
.group(1) > check_name_camel
:
204 f
.write('#include "' + check_name_camel
+ '.h"\n')
207 f
.write('#include "' + check_name_camel
+ '.h"\n')
210 if line
.strip() == "}":
215 'registerCheck<(.*)> *\( *(?:"([^"]*)")?', line
219 current_check_name
= match
.group(2)
220 if current_check_name
is None:
221 # If we didn't find the check name on this line, look on the
225 match
= re
.search(' *"([^"]*)"', line
)
227 current_check_name
= match
.group(1)
228 if current_check_name
> check_fq_name
:
234 except StopIteration:
238 # Adds a release notes entry.
239 def add_release_notes(module_path
, module
, check_name
):
240 check_name_dashes
= module
+ "-" + check_name
241 filename
= os
.path
.normpath(
242 os
.path
.join(module_path
, "../../docs/ReleaseNotes.rst")
244 with io
.open(filename
, "r", encoding
="utf8") as f
:
245 lines
= f
.readlines()
247 lineMatcher
= re
.compile("New checks")
248 nextSectionMatcher
= re
.compile("New check aliases")
249 checkMatcher
= re
.compile("- New :doc:`(.*)")
251 print("Updating %s..." % filename
)
252 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
255 add_note_here
= False
259 match
= lineMatcher
.match(line
)
260 match_next
= nextSectionMatcher
.match(line
)
261 match_check
= checkMatcher
.match(line
)
263 last_check
= match_check
.group(1)
264 if last_check
> check_name_dashes
:
275 if line
.startswith("^^^^"):
279 if header_found
and add_note_here
:
280 if not line
.startswith("^^^^"):
283 <clang-tidy/checks/%s/%s>` check.
285 FIXME: add release notes.
288 % (check_name_dashes
, module
, check_name
)
295 # Adds a test for the check.
296 def write_test(module_path
, module
, check_name
, test_extension
):
297 check_name_dashes
= module
+ "-" + check_name
298 filename
= os
.path
.normpath(
307 check_name
+ "." + test_extension
,
310 print("Creating %s..." % filename
)
311 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
313 """// RUN: %%check_clang_tidy %%s %(check_name_dashes)s %%t
315 // FIXME: Add something that triggers the check here.
317 // CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s]
319 // FIXME: Verify the applied fix.
320 // * Make the CHECK patterns specific enough and try to make verified lines
321 // unique to avoid incorrect matches.
322 // * Use {{}} for regular expressions.
323 // CHECK-FIXES: {{^}}void awesome_f();{{$}}
325 // FIXME: Add something that doesn't trigger the check here.
328 % {"check_name_dashes": check_name_dashes
}
332 def get_actual_filename(dirname
, filename
):
333 if not os
.path
.isdir(dirname
):
335 name
= os
.path
.join(dirname
, filename
)
336 if os
.path
.isfile(name
):
338 caselessname
= filename
.lower()
339 for file in os
.listdir(dirname
):
340 if file.lower() == caselessname
:
341 return os
.path
.join(dirname
, file)
345 # Recreates the list of checks in the docs/clang-tidy/checks directory.
346 def update_checks_list(clang_tidy_path
):
347 docs_dir
= os
.path
.join(clang_tidy_path
, "../docs/clang-tidy/checks")
348 filename
= os
.path
.normpath(os
.path
.join(docs_dir
, "list.rst"))
349 # Read the content of the current list.rst file
350 with io
.open(filename
, "r", encoding
="utf8") as f
:
351 lines
= f
.readlines()
352 # Get all existing docs
354 for subdir
in filter(
355 lambda s
: os
.path
.isdir(os
.path
.join(docs_dir
, s
)), os
.listdir(docs_dir
)
358 lambda s
: s
.endswith(".rst"), os
.listdir(os
.path
.join(docs_dir
, subdir
))
360 doc_files
.append([subdir
, file])
363 # We couldn't find the source file from the check name, so try to find the
364 # class name that corresponds to the check in the module file.
365 def filename_from_module(module_name
, check_name
):
366 module_path
= os
.path
.join(clang_tidy_path
, module_name
)
367 if not os
.path
.isdir(module_path
):
369 module_file
= get_module_filename(module_path
, module_name
)
370 if not os
.path
.isfile(module_file
):
372 with io
.open(module_file
, "r") as f
:
374 full_check_name
= module_name
+ "-" + check_name
375 name_pos
= code
.find('"' + full_check_name
+ '"')
378 stmt_end_pos
= code
.find(";", name_pos
)
379 if stmt_end_pos
== -1:
381 stmt_start_pos
= code
.rfind(";", 0, name_pos
)
382 if stmt_start_pos
== -1:
383 stmt_start_pos
= code
.rfind("{", 0, name_pos
)
384 if stmt_start_pos
== -1:
386 stmt
= code
[stmt_start_pos
+ 1 : stmt_end_pos
]
387 matches
= re
.search('registerCheck<([^>:]*)>\(\s*"([^"]*)"\s*\)', stmt
)
388 if matches
and matches
[2] == full_check_name
:
389 class_name
= matches
[1]
390 if "::" in class_name
:
391 parts
= class_name
.split("::")
392 class_name
= parts
[-1]
393 class_path
= os
.path
.join(
394 clang_tidy_path
, module_name
, "..", *parts
[0:-1]
397 class_path
= os
.path
.join(clang_tidy_path
, module_name
)
398 return get_actual_filename(class_path
, class_name
+ ".cpp")
402 # Examine code looking for a c'tor definition to get the base class name.
403 def get_base_class(code
, check_file
):
404 check_class_name
= os
.path
.splitext(os
.path
.basename(check_file
))[0]
405 ctor_pattern
= check_class_name
+ "\([^:]*\)\s*:\s*([A-Z][A-Za-z0-9]*Check)\("
406 matches
= re
.search("\s+" + check_class_name
+ "::" + ctor_pattern
, code
)
408 # The constructor might be inline in the header.
410 header_file
= os
.path
.splitext(check_file
)[0] + ".h"
411 if not os
.path
.isfile(header_file
):
413 with io
.open(header_file
, encoding
="utf8") as f
:
415 matches
= re
.search(" " + ctor_pattern
, code
)
417 if matches
and matches
[1] != "ClangTidyCheck":
421 # Some simple heuristics to figure out if a check has an autofix or not.
422 def has_fixits(code
):
427 "TransformerClangTidyCheck",
433 # Try to figure out of the check supports fixits.
434 def has_auto_fix(check_name
):
435 dirname
, _
, check_name
= check_name
.partition("-")
437 check_file
= get_actual_filename(
438 os
.path
.join(clang_tidy_path
, dirname
),
439 get_camel_check_name(check_name
) + ".cpp",
441 if not os
.path
.isfile(check_file
):
442 # Some older checks don't end with 'Check.cpp'
443 check_file
= get_actual_filename(
444 os
.path
.join(clang_tidy_path
, dirname
),
445 get_camel_name(check_name
) + ".cpp",
447 if not os
.path
.isfile(check_file
):
448 # Some checks aren't in a file based on the check name.
449 check_file
= filename_from_module(dirname
, check_name
)
450 if not check_file
or not os
.path
.isfile(check_file
):
453 with io
.open(check_file
, encoding
="utf8") as f
:
458 base_class
= get_base_class(code
, check_file
)
460 base_file
= os
.path
.join(clang_tidy_path
, dirname
, base_class
+ ".cpp")
461 if os
.path
.isfile(base_file
):
462 with io
.open(base_file
, encoding
="utf8") as f
:
469 def process_doc(doc_file
):
470 check_name
= doc_file
[0] + "-" + doc_file
[1].replace(".rst", "")
472 with io
.open(os
.path
.join(docs_dir
, *doc_file
), "r", encoding
="utf8") as doc
:
474 match
= re
.search(".*:orphan:.*", content
)
477 # Orphan page, don't list it.
480 match
= re
.search(".*:http-equiv=refresh: \d+;URL=(.*).html(.*)", content
)
482 return check_name
, match
484 def format_link(doc_file
):
485 check_name
, match
= process_doc(doc_file
)
486 if not match
and check_name
and not check_name
.startswith("clang-analyzer-"):
487 return " :doc:`%(check_name)s <%(module)s/%(check)s>`,%(autofix)s\n" % {
488 "check_name": check_name
,
489 "module": doc_file
[0],
490 "check": doc_file
[1].replace(".rst", ""),
491 "autofix": has_auto_fix(check_name
),
496 def format_link_alias(doc_file
):
497 check_name
, match
= process_doc(doc_file
)
498 if (match
or (check_name
.startswith("clang-analyzer-"))) and check_name
:
500 check_file
= doc_file
[1].replace(".rst", "")
501 if not match
or match
.group(1) == "https://clang.llvm.org/docs/analyzer/checkers":
502 title
= "Clang Static Analyzer " + check_file
503 # Preserve the anchor in checkers.html from group 2.
504 target
= "" if not match
else match
.group(1) + ".html" + match
.group(2)
509 redirect_parts
= re
.search("^\.\./([^/]*)/([^/]*)$", match
.group(1))
510 title
= redirect_parts
[1] + "-" + redirect_parts
[2]
511 target
= redirect_parts
[1] + "/" + redirect_parts
[2]
512 autofix
= has_auto_fix(title
)
517 # The checker is just a redirect.
519 " :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(ref_begin)s`%(title)s <%(target)s>`%(ref_end)s,%(autofix)s\n"
521 "check_name": check_name
,
523 "check_file": check_file
,
527 "ref_begin" : ref_begin
,
531 # The checker is just a alias without redirect.
533 " :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(title)s,%(autofix)s\n"
535 "check_name": check_name
,
537 "check_file": check_file
,
544 checks
= map(format_link
, doc_files
)
545 checks_alias
= map(format_link_alias
, doc_files
)
547 print("Updating %s..." % filename
)
548 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
551 if line
.strip() == ".. csv-table::":
552 # We dump the checkers
553 f
.write(' :header: "Name", "Offers fixes"\n\n')
557 f
.write(".. csv-table:: Aliases..\n")
558 f
.write(' :header: "Name", "Redirect", "Offers fixes"\n\n')
559 f
.writelines(checks_alias
)
563 # Adds a documentation for the check.
564 def write_docs(module_path
, module
, check_name
):
565 check_name_dashes
= module
+ "-" + check_name
566 filename
= os
.path
.normpath(
568 module_path
, "../../docs/clang-tidy/checks/", module
, check_name
+ ".rst"
571 print("Creating %s..." % filename
)
572 with io
.open(filename
, "w", encoding
="utf8", newline
="\n") as f
:
574 """.. title:: clang-tidy - %(check_name_dashes)s
576 %(check_name_dashes)s
579 FIXME: Describe what patterns does the check detect and why. Give examples.
582 "check_name_dashes": check_name_dashes
,
583 "underline": "=" * len(check_name_dashes
),
588 def get_camel_name(check_name
):
589 return "".join(map(lambda elem
: elem
.capitalize(), check_name
.split("-")))
592 def get_camel_check_name(check_name
):
593 return get_camel_name(check_name
) + "Check"
597 language_to_extension
= {
603 parser
= argparse
.ArgumentParser()
607 help="just update the list of documentation files, then exit",
611 help="language to use for new check (defaults to c++)",
612 choices
=language_to_extension
.keys(),
619 help="module directory under which to place the new tidy check (e.g., misc)",
622 "check", nargs
="?", help="name of new tidy check to add (e.g. foo-do-the-stuff)"
624 args
= parser
.parse_args()
627 update_checks_list(os
.path
.dirname(sys
.argv
[0]))
630 if not args
.module
or not args
.check
:
631 print("Module and check must be specified.")
636 check_name
= args
.check
637 check_name_camel
= get_camel_check_name(check_name
)
638 if check_name
.startswith(module
):
640 'Check name "%s" must not start with the module "%s". Exiting.'
641 % (check_name
, module
)
644 clang_tidy_path
= os
.path
.dirname(sys
.argv
[0])
645 module_path
= os
.path
.join(clang_tidy_path
, module
)
647 if not adapt_cmake(module_path
, check_name_camel
):
650 # Map module names to namespace names that don't conflict with widely used top-level namespaces.
652 namespace
= module
+ "_check"
656 write_header(module_path
, module
, namespace
, check_name
, check_name_camel
)
657 write_implementation(module_path
, module
, namespace
, check_name_camel
)
658 adapt_module(module_path
, module
, check_name
, check_name_camel
)
659 add_release_notes(module_path
, module
, check_name
)
660 test_extension
= language_to_extension
.get(args
.language
)
661 write_test(module_path
, module
, check_name
, test_extension
)
662 write_docs(module_path
, module
, check_name
)
663 update_checks_list(clang_tidy_path
)
664 print("Done. Now it's your turn!")
667 if __name__
== "__main__":