[clang][modules] Don't prevent translation of FW_Private includes when explicitly...
[llvm-project.git] / clang-tools-extra / clang-tidy / add_new_check.py
blobada2ee1119cf90c1ad1c881f1bcfd8134226946b
1 #!/usr/bin/env python3
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
14 import argparse
15 import io
16 import os
17 import re
18 import sys
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:
30 lines = f.readlines()
32 cpp_file = check_name_camel + ".cpp"
34 # Figure out whether this check already exists.
35 for line in lines:
36 if line.strip() == cpp_file:
37 return False
39 print("Updating %s..." % filename)
40 with io.open(filename, "w", encoding="utf8", newline="\n") as f:
41 cpp_found = False
42 file_added = False
43 for line in lines:
44 cpp_line = line.strip().endswith(".cpp")
45 if (not file_added) and (cpp_line or cpp_found):
46 cpp_found = True
47 if (line.strip() > cpp_file) or (not cpp_line):
48 f.write(" " + cpp_file + "\n")
49 file_added = True
50 f.write(line)
52 return True
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:
60 header_guard = (
61 "LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_"
62 + module.upper()
63 + "_"
64 + check_name_camel.upper()
65 + "_H"
67 f.write("//===--- ")
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++ -*-===//")
72 f.write(
73 """
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.
89 ///
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 {
93 public:
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,
108 "module": module,
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:
119 f.write("//===--- ")
120 f.write(os.path.basename(filename))
121 f.write(" - clang-tidy ")
122 f.write("-" * max(0, 51 - len(os.path.basename(filename))))
123 f.write("-===//")
124 f.write(
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_"))
150 return;
151 diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome")
152 << MatchedDecl
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):
165 modulecpp = list(
166 filter(
167 lambda p: p.lower() == module.lower() + "tidymodule.cpp",
168 os.listdir(module_path),
170 )[0]
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:
182 header_added = False
183 header_found = False
184 check_added = False
185 check_fq_name = module + "-" + check_name
186 check_decl = (
187 " CheckFactories.registerCheck<"
188 + check_name_camel
189 + '>(\n "'
190 + check_fq_name
191 + '");\n'
194 lines = iter(lines)
195 try:
196 while True:
197 line = next(lines)
198 if not header_added:
199 match = re.search('#include "(.*)"', line)
200 if match:
201 header_found = True
202 if match.group(1) > check_name_camel:
203 header_added = True
204 f.write('#include "' + check_name_camel + '.h"\n')
205 elif header_found:
206 header_added = True
207 f.write('#include "' + check_name_camel + '.h"\n')
209 if not check_added:
210 if line.strip() == "}":
211 check_added = True
212 f.write(check_decl)
213 else:
214 match = re.search(
215 'registerCheck<(.*)> *\( *(?:"([^"]*)")?', line
217 prev_line = None
218 if match:
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
222 # next one.
223 prev_line = line
224 line = next(lines)
225 match = re.search(' *"([^"]*)"', line)
226 if match:
227 current_check_name = match.group(1)
228 if current_check_name > check_fq_name:
229 check_added = True
230 f.write(check_decl)
231 if prev_line:
232 f.write(prev_line)
233 f.write(line)
234 except StopIteration:
235 pass
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:
253 note_added = False
254 header_found = False
255 add_note_here = False
257 for line in lines:
258 if not note_added:
259 match = lineMatcher.match(line)
260 match_next = nextSectionMatcher.match(line)
261 match_check = checkMatcher.match(line)
262 if match_check:
263 last_check = match_check.group(1)
264 if last_check > check_name_dashes:
265 add_note_here = True
267 if match_next:
268 add_note_here = True
270 if match:
271 header_found = True
272 f.write(line)
273 continue
275 if line.startswith("^^^^"):
276 f.write(line)
277 continue
279 if header_found and add_note_here:
280 if not line.startswith("^^^^"):
281 f.write(
282 """- New :doc:`%s
283 <clang-tidy/checks/%s/%s>` check.
285 FIXME: add release notes.
288 % (check_name_dashes, module, check_name)
290 note_added = True
292 f.write(line)
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(
299 os.path.join(
300 module_path,
301 "..",
302 "..",
303 "test",
304 "clang-tidy",
305 "checkers",
306 module,
307 check_name + "." + test_extension,
310 print("Creating %s..." % filename)
311 with io.open(filename, "w", encoding="utf8", newline="\n") as f:
312 f.write(
313 """// RUN: %%check_clang_tidy %%s %(check_name_dashes)s %%t
315 // FIXME: Add something that triggers the check here.
316 void f();
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.
326 void awesome_f2();
328 % {"check_name_dashes": check_name_dashes}
332 def get_actual_filename(dirname, filename):
333 if not os.path.isdir(dirname):
334 return ""
335 name = os.path.join(dirname, filename)
336 if os.path.isfile(name):
337 return name
338 caselessname = filename.lower()
339 for file in os.listdir(dirname):
340 if file.lower() == caselessname:
341 return os.path.join(dirname, file)
342 return ""
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
353 doc_files = []
354 for subdir in filter(
355 lambda s: os.path.isdir(os.path.join(docs_dir, s)), os.listdir(docs_dir)
357 for file in filter(
358 lambda s: s.endswith(".rst"), os.listdir(os.path.join(docs_dir, subdir))
360 doc_files.append([subdir, file])
361 doc_files.sort()
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):
368 return ""
369 module_file = get_module_filename(module_path, module_name)
370 if not os.path.isfile(module_file):
371 return ""
372 with io.open(module_file, "r") as f:
373 code = f.read()
374 full_check_name = module_name + "-" + check_name
375 name_pos = code.find('"' + full_check_name + '"')
376 if name_pos == -1:
377 return ""
378 stmt_end_pos = code.find(";", name_pos)
379 if stmt_end_pos == -1:
380 return ""
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:
385 return ""
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]
396 else:
397 class_path = os.path.join(clang_tidy_path, module_name)
398 return get_actual_filename(class_path, class_name + ".cpp")
400 return ""
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.
409 if not matches:
410 header_file = os.path.splitext(check_file)[0] + ".h"
411 if not os.path.isfile(header_file):
412 return ""
413 with io.open(header_file, encoding="utf8") as f:
414 code = f.read()
415 matches = re.search(" " + ctor_pattern, code)
417 if matches and matches[1] != "ClangTidyCheck":
418 return matches[1]
419 return ""
421 # Some simple heuristics to figure out if a check has an autofix or not.
422 def has_fixits(code):
423 for needle in [
424 "FixItHint",
425 "ReplacementText",
426 "fixit",
427 "TransformerClangTidyCheck",
429 if needle in code:
430 return True
431 return False
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):
451 return ""
453 with io.open(check_file, encoding="utf8") as f:
454 code = f.read()
455 if has_fixits(code):
456 return ' "Yes"'
458 base_class = get_base_class(code, check_file)
459 if base_class:
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:
463 code = f.read()
464 if has_fixits(code):
465 return ' "Yes"'
467 return ""
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:
473 content = doc.read()
474 match = re.search(".*:orphan:.*", content)
476 if match:
477 # Orphan page, don't list it.
478 return "", ""
480 match = re.search(".*:http-equiv=refresh: \d+;URL=(.*).html(.*)", content)
481 # Is it a redirect?
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),
493 else:
494 return ""
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:
499 module = doc_file[0]
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)
505 autofix = ""
506 ref_begin = ""
507 ref_end = "_"
508 else:
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)
513 ref_begin = ":doc:"
514 ref_end = ""
516 if target:
517 # The checker is just a redirect.
518 return (
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,
522 "module": module,
523 "check_file": check_file,
524 "target": target,
525 "title": title,
526 "autofix": autofix,
527 "ref_begin" : ref_begin,
528 "ref_end" : ref_end
530 else:
531 # The checker is just a alias without redirect.
532 return (
533 " :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(title)s,%(autofix)s\n"
535 "check_name": check_name,
536 "module": module,
537 "check_file": check_file,
538 "target": target,
539 "title": title,
540 "autofix": autofix,
542 return ""
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:
549 for line in lines:
550 f.write(line)
551 if line.strip() == ".. csv-table::":
552 # We dump the checkers
553 f.write(' :header: "Name", "Offers fixes"\n\n')
554 f.writelines(checks)
555 # and the aliases
556 f.write("\n\n")
557 f.write(".. csv-table:: Aliases..\n")
558 f.write(' :header: "Name", "Redirect", "Offers fixes"\n\n')
559 f.writelines(checks_alias)
560 break
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(
567 os.path.join(
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:
573 f.write(
574 """.. title:: clang-tidy - %(check_name_dashes)s
576 %(check_name_dashes)s
577 %(underline)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"
596 def main():
597 language_to_extension = {
598 "c": "c",
599 "c++": "cpp",
600 "objc": "m",
601 "objc++": "mm",
603 parser = argparse.ArgumentParser()
604 parser.add_argument(
605 "--update-docs",
606 action="store_true",
607 help="just update the list of documentation files, then exit",
609 parser.add_argument(
610 "--language",
611 help="language to use for new check (defaults to c++)",
612 choices=language_to_extension.keys(),
613 default="c++",
614 metavar="LANG",
616 parser.add_argument(
617 "module",
618 nargs="?",
619 help="module directory under which to place the new tidy check (e.g., misc)",
621 parser.add_argument(
622 "check", nargs="?", help="name of new tidy check to add (e.g. foo-do-the-stuff)"
624 args = parser.parse_args()
626 if args.update_docs:
627 update_checks_list(os.path.dirname(sys.argv[0]))
628 return
630 if not args.module or not args.check:
631 print("Module and check must be specified.")
632 parser.print_usage()
633 return
635 module = args.module
636 check_name = args.check
637 check_name_camel = get_camel_check_name(check_name)
638 if check_name.startswith(module):
639 print(
640 'Check name "%s" must not start with the module "%s". Exiting.'
641 % (check_name, module)
643 return
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):
648 return
650 # Map module names to namespace names that don't conflict with widely used top-level namespaces.
651 if module == "llvm":
652 namespace = module + "_check"
653 else:
654 namespace = module
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__":
668 main()