clang-tools 19.0.0git
add_new_check.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2#
3# ===- add_new_check.py - clang-tidy check generator ---------*- python -*--===#
4#
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
8#
9# ===-----------------------------------------------------------------------===#
10
11from __future__ import print_function
12from __future__ import unicode_literals
13
14import argparse
15import io
16import os
17import re
18import sys
19
20# Adapts the module's CMakelist file. Returns 'True' if it could add a new
21# entry and 'False' if the entry already existed.
22def adapt_cmake(module_path, check_name_camel):
23 filename = os.path.join(module_path, "CMakeLists.txt")
24
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()
31
32 cpp_file = check_name_camel + ".cpp"
33
34 # Figure out whether this check already exists.
35 for line in lines:
36 if line.strip() == cpp_file:
37 return False
38
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)
51
52 return True
53
54
55# Adds a header for the new check.
56def 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"
66 )
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 """
74//
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
78//
79//===----------------------------------------------------------------------===//
80
81#ifndef %(header_guard)s
82#define %(header_guard)s
83
84#include "../ClangTidyCheck.h"
85
86namespace clang::tidy::%(namespace)s {
87
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
92class %(check_name_camel)s : public ClangTidyCheck {
93public:
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;
98};
99
100} // namespace clang::tidy::%(namespace)s
101
102#endif // %(header_guard)s
103"""
104 % {
105 "header_guard": header_guard,
106 "check_name_camel": check_name_camel,
107 "check_name": check_name,
108 "module": module,
109 "namespace": namespace,
110 }
111 )
112
113
114# Adds the implementation of the new check.
115def 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(
125 """
126//
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
130//
131//===----------------------------------------------------------------------===//
132
133#include "%(check_name)s.h"
134#include "clang/ASTMatchers/ASTMatchFinder.h"
135
136using namespace clang::ast_matchers;
137
138namespace clang::tidy::%(namespace)s {
139
140void %(check_name)s::registerMatchers(MatchFinder *Finder) {
141 // FIXME: Add matchers.
142 Finder->addMatcher(functionDecl().bind("x"), this);
143}
144
145void %(check_name)s::check(const MatchFinder::MatchResult &Result) {
146 // FIXME: Add callback implementation.
147 const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
148 if (!MatchedDecl->getIdentifier() || MatchedDecl->getName().starts_with("awesome_"))
149 return;
150 diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome")
151 << MatchedDecl
152 << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
153 diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note);
154}
155
156} // namespace clang::tidy::%(namespace)s
157"""
158 % {"check_name": check_name_camel, "module": module, "namespace": namespace}
159 )
160
161
162# Returns the source filename that implements the module.
163def get_module_filename(module_path, module):
164 modulecpp = list(
165 filter(
166 lambda p: p.lower() == module.lower() + "tidymodule.cpp",
167 os.listdir(module_path),
168 )
169 )[0]
170 return os.path.join(module_path, modulecpp)
171
172
173# Modifies the module to include the new check.
174def adapt_module(module_path, module, check_name, check_name_camel):
175 filename = get_module_filename(module_path, module)
176 with io.open(filename, "r", encoding="utf8") as f:
177 lines = f.readlines()
178
179 print("Updating %s..." % filename)
180 with io.open(filename, "w", encoding="utf8", newline="\n") as f:
181 header_added = False
182 header_found = False
183 check_added = False
184 check_fq_name = module + "-" + check_name
185 check_decl = (
186 " CheckFactories.registerCheck<"
187 + check_name_camel
188 + '>(\n "'
189 + check_fq_name
190 + '");\n'
191 )
192
193 lines = iter(lines)
194 try:
195 while True:
196 line = next(lines)
197 if not header_added:
198 match = re.search('#include "(.*)"', line)
199 if match:
200 header_found = True
201 if match.group(1) > check_name_camel:
202 header_added = True
203 f.write('#include "' + check_name_camel + '.h"\n')
204 elif header_found:
205 header_added = True
206 f.write('#include "' + check_name_camel + '.h"\n')
207
208 if not check_added:
209 if line.strip() == "}":
210 check_added = True
211 f.write(check_decl)
212 else:
213 match = re.search(
214 'registerCheck<(.*)> *\‍( *(?:"([^"]*)")?', line
215 )
216 prev_line = None
217 if match:
218 current_check_name = match.group(2)
219 if current_check_name is None:
220 # If we didn't find the check name on this line, look on the
221 # next one.
222 prev_line = line
223 line = next(lines)
224 match = re.search(' *"([^"]*)"', line)
225 if match:
226 current_check_name = match.group(1)
227 if current_check_name > check_fq_name:
228 check_added = True
229 f.write(check_decl)
230 if prev_line:
231 f.write(prev_line)
232 f.write(line)
233 except StopIteration:
234 pass
235
236
237# Adds a release notes entry.
238def add_release_notes(module_path, module, check_name):
239 check_name_dashes = module + "-" + check_name
240 filename = os.path.normpath(
241 os.path.join(module_path, "../../docs/ReleaseNotes.rst")
242 )
243 with io.open(filename, "r", encoding="utf8") as f:
244 lines = f.readlines()
245
246 lineMatcher = re.compile("New checks")
247 nextSectionMatcher = re.compile("New check aliases")
248 checkMatcher = re.compile("- New :doc:`(.*)")
249
250 print("Updating %s..." % filename)
251 with io.open(filename, "w", encoding="utf8", newline="\n") as f:
252 note_added = False
253 header_found = False
254 add_note_here = False
255
256 for line in lines:
257 if not note_added:
258 match = lineMatcher.match(line)
259 match_next = nextSectionMatcher.match(line)
260 match_check = checkMatcher.match(line)
261 if match_check:
262 last_check = match_check.group(1)
263 if last_check > check_name_dashes:
264 add_note_here = True
265
266 if match_next:
267 add_note_here = True
268
269 if match:
270 header_found = True
271 f.write(line)
272 continue
273
274 if line.startswith("^^^^"):
275 f.write(line)
276 continue
277
278 if header_found and add_note_here:
279 if not line.startswith("^^^^"):
280 f.write(
281 """- New :doc:`%s
282 <clang-tidy/checks/%s/%s>` check.
283
284 FIXME: add release notes.
285
286"""
287 % (check_name_dashes, module, check_name)
288 )
289 note_added = True
290
291 f.write(line)
292
293
294# Adds a test for the check.
295def write_test(module_path, module, check_name, test_extension):
296 check_name_dashes = module + "-" + check_name
297 filename = os.path.normpath(
298 os.path.join(
299 module_path,
300 "..",
301 "..",
302 "test",
303 "clang-tidy",
304 "checkers",
305 module,
306 check_name + "." + test_extension,
307 )
308 )
309 print("Creating %s..." % filename)
310 with io.open(filename, "w", encoding="utf8", newline="\n") as f:
311 f.write(
312 """// RUN: %%check_clang_tidy %%s %(check_name_dashes)s %%t
313
314// FIXME: Add something that triggers the check here.
315void f();
316// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s]
317
318// FIXME: Verify the applied fix.
319// * Make the CHECK patterns specific enough and try to make verified lines
320// unique to avoid incorrect matches.
321// * Use {{}} for regular expressions.
322// CHECK-FIXES: {{^}}void awesome_f();{{$}}
323
324// FIXME: Add something that doesn't trigger the check here.
325void awesome_f2();
326"""
327 % {"check_name_dashes": check_name_dashes}
328 )
329
330
331def get_actual_filename(dirname, filename):
332 if not os.path.isdir(dirname):
333 return ""
334 name = os.path.join(dirname, filename)
335 if os.path.isfile(name):
336 return name
337 caselessname = filename.lower()
338 for file in os.listdir(dirname):
339 if file.lower() == caselessname:
340 return os.path.join(dirname, file)
341 return ""
342
343
344# Recreates the list of checks in the docs/clang-tidy/checks directory.
345def update_checks_list(clang_tidy_path):
346 docs_dir = os.path.join(clang_tidy_path, "../docs/clang-tidy/checks")
347 filename = os.path.normpath(os.path.join(docs_dir, "list.rst"))
348 # Read the content of the current list.rst file
349 with io.open(filename, "r", encoding="utf8") as f:
350 lines = f.readlines()
351 # Get all existing docs
352 doc_files = []
353 for subdir in filter(
354 lambda s: os.path.isdir(os.path.join(docs_dir, s)), os.listdir(docs_dir)
355 ):
356 for file in filter(
357 lambda s: s.endswith(".rst"), os.listdir(os.path.join(docs_dir, subdir))
358 ):
359 doc_files.append([subdir, file])
360 doc_files.sort()
361
362 # We couldn't find the source file from the check name, so try to find the
363 # class name that corresponds to the check in the module file.
364 def filename_from_module(module_name, check_name):
365 module_path = os.path.join(clang_tidy_path, module_name)
366 if not os.path.isdir(module_path):
367 return ""
368 module_file = get_module_filename(module_path, module_name)
369 if not os.path.isfile(module_file):
370 return ""
371 with io.open(module_file, "r") as f:
372 code = f.read()
373 full_check_name = module_name + "-" + check_name
374 name_pos = code.find('"' + full_check_name + '"')
375 if name_pos == -1:
376 return ""
377 stmt_end_pos = code.find(";", name_pos)
378 if stmt_end_pos == -1:
379 return ""
380 stmt_start_pos = code.rfind(";", 0, name_pos)
381 if stmt_start_pos == -1:
382 stmt_start_pos = code.rfind("{", 0, name_pos)
383 if stmt_start_pos == -1:
384 return ""
385 stmt = code[stmt_start_pos + 1 : stmt_end_pos]
386 matches = re.search('registerCheck<([^>:]*)>\‍(\s*"([^"]*)"\s*\‍)', stmt)
387 if matches and matches[2] == full_check_name:
388 class_name = matches[1]
389 if "::" in class_name:
390 parts = class_name.split("::")
391 class_name = parts[-1]
392 class_path = os.path.join(
393 clang_tidy_path, module_name, "..", *parts[0:-1]
394 )
395 else:
396 class_path = os.path.join(clang_tidy_path, module_name)
397 return get_actual_filename(class_path, class_name + ".cpp")
398
399 return ""
400
401 # Examine code looking for a c'tor definition to get the base class name.
402 def get_base_class(code, check_file):
403 check_class_name = os.path.splitext(os.path.basename(check_file))[0]
404 ctor_pattern = check_class_name + "\‍([^:]*\‍)\s*:\s*([A-Z][A-Za-z0-9]*Check)\‍("
405 matches = re.search("\s+" + check_class_name + "::" + ctor_pattern, code)
406
407 # The constructor might be inline in the header.
408 if not matches:
409 header_file = os.path.splitext(check_file)[0] + ".h"
410 if not os.path.isfile(header_file):
411 return ""
412 with io.open(header_file, encoding="utf8") as f:
413 code = f.read()
414 matches = re.search(" " + ctor_pattern, code)
415
416 if matches and matches[1] != "ClangTidyCheck":
417 return matches[1]
418 return ""
419
420 # Some simple heuristics to figure out if a check has an autofix or not.
421 def has_fixits(code):
422 for needle in [
423 "FixItHint",
424 "ReplacementText",
425 "fixit",
426 "TransformerClangTidyCheck",
427 ]:
428 if needle in code:
429 return True
430 return False
431
432 # Try to figure out of the check supports fixits.
433 def has_auto_fix(check_name):
434 dirname, _, check_name = check_name.partition("-")
435
436 check_file = get_actual_filename(
437 os.path.join(clang_tidy_path, dirname),
438 get_camel_check_name(check_name) + ".cpp",
439 )
440 if not os.path.isfile(check_file):
441 # Some older checks don't end with 'Check.cpp'
442 check_file = get_actual_filename(
443 os.path.join(clang_tidy_path, dirname),
444 get_camel_name(check_name) + ".cpp",
445 )
446 if not os.path.isfile(check_file):
447 # Some checks aren't in a file based on the check name.
448 check_file = filename_from_module(dirname, check_name)
449 if not check_file or not os.path.isfile(check_file):
450 return ""
451
452 with io.open(check_file, encoding="utf8") as f:
453 code = f.read()
454 if has_fixits(code):
455 return ' "Yes"'
456
457 base_class = get_base_class(code, check_file)
458 if base_class:
459 base_file = os.path.join(clang_tidy_path, dirname, base_class + ".cpp")
460 if os.path.isfile(base_file):
461 with io.open(base_file, encoding="utf8") as f:
462 code = f.read()
463 if has_fixits(code):
464 return ' "Yes"'
465
466 return ""
467
468 def process_doc(doc_file):
469 check_name = doc_file[0] + "-" + doc_file[1].replace(".rst", "")
470
471 with io.open(os.path.join(docs_dir, *doc_file), "r", encoding="utf8") as doc:
472 content = doc.read()
473 match = re.search(".*:orphan:.*", content)
474
475 if match:
476 # Orphan page, don't list it.
477 return "", ""
478
479 match = re.search(".*:http-equiv=refresh: \d+;URL=(.*).html(.*)", content)
480 # Is it a redirect?
481 return check_name, match
482
483 def format_link(doc_file):
484 check_name, match = process_doc(doc_file)
485 if not match and check_name and not check_name.startswith("clang-analyzer-"):
486 return " :doc:`%(check_name)s <%(module)s/%(check)s>`,%(autofix)s\n" % {
487 "check_name": check_name,
488 "module": doc_file[0],
489 "check": doc_file[1].replace(".rst", ""),
490 "autofix": has_auto_fix(check_name),
491 }
492 else:
493 return ""
494
495 def format_link_alias(doc_file):
496 check_name, match = process_doc(doc_file)
497 if (match or (check_name.startswith("clang-analyzer-"))) and check_name:
498 module = doc_file[0]
499 check_file = doc_file[1].replace(".rst", "")
500 if not match or match.group(1) == "https://clang.llvm.org/docs/analyzer/checkers":
501 title = "Clang Static Analyzer " + check_file
502 # Preserve the anchor in checkers.html from group 2.
503 target = "" if not match else match.group(1) + ".html" + match.group(2)
504 autofix = ""
505 ref_begin = ""
506 ref_end = "_"
507 else:
508 redirect_parts = re.search("^\.\./([^/]*)/([^/]*)$", match.group(1))
509 title = redirect_parts[1] + "-" + redirect_parts[2]
510 target = redirect_parts[1] + "/" + redirect_parts[2]
511 autofix = has_auto_fix(title)
512 ref_begin = ":doc:"
513 ref_end = ""
514
515 if target:
516 # The checker is just a redirect.
517 return (
518 " :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(ref_begin)s`%(title)s <%(target)s>`%(ref_end)s,%(autofix)s\n"
519 % {
520 "check_name": check_name,
521 "module": module,
522 "check_file": check_file,
523 "target": target,
524 "title": title,
525 "autofix": autofix,
526 "ref_begin" : ref_begin,
527 "ref_end" : ref_end
528 })
529 else:
530 # The checker is just a alias without redirect.
531 return (
532 " :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(title)s,%(autofix)s\n"
533 % {
534 "check_name": check_name,
535 "module": module,
536 "check_file": check_file,
537 "target": target,
538 "title": title,
539 "autofix": autofix,
540 })
541 return ""
542
543 checks = map(format_link, doc_files)
544 checks_alias = map(format_link_alias, doc_files)
545
546 print("Updating %s..." % filename)
547 with io.open(filename, "w", encoding="utf8", newline="\n") as f:
548 for line in lines:
549 f.write(line)
550 if line.strip() == ".. csv-table::":
551 # We dump the checkers
552 f.write(' :header: "Name", "Offers fixes"\n\n')
553 f.writelines(checks)
554 # and the aliases
555 f.write("\n\n")
556 f.write(".. csv-table:: Aliases..\n")
557 f.write(' :header: "Name", "Redirect", "Offers fixes"\n\n')
558 f.writelines(checks_alias)
559 break
560
561
562# Adds a documentation for the check.
563def write_docs(module_path, module, check_name):
564 check_name_dashes = module + "-" + check_name
565 filename = os.path.normpath(
566 os.path.join(
567 module_path, "../../docs/clang-tidy/checks/", module, check_name + ".rst"
568 )
569 )
570 print("Creating %s..." % filename)
571 with io.open(filename, "w", encoding="utf8", newline="\n") as f:
572 f.write(
573 """.. title:: clang-tidy - %(check_name_dashes)s
574
575%(check_name_dashes)s
576%(underline)s
577
578FIXME: Describe what patterns does the check detect and why. Give examples.
579"""
580 % {
581 "check_name_dashes": check_name_dashes,
582 "underline": "=" * len(check_name_dashes),
583 }
584 )
585
586
587def get_camel_name(check_name):
588 return "".join(map(lambda elem: elem.capitalize(), check_name.split("-")))
589
590
591def get_camel_check_name(check_name):
592 return get_camel_name(check_name) + "Check"
593
594
595def main():
596 language_to_extension = {
597 "c": "c",
598 "c++": "cpp",
599 "objc": "m",
600 "objc++": "mm",
601 }
602 parser = argparse.ArgumentParser()
603 parser.add_argument(
604 "--update-docs",
605 action="store_true",
606 help="just update the list of documentation files, then exit",
607 )
608 parser.add_argument(
609 "--language",
610 help="language to use for new check (defaults to c++)",
611 choices=language_to_extension.keys(),
612 default="c++",
613 metavar="LANG",
614 )
615 parser.add_argument(
616 "module",
617 nargs="?",
618 help="module directory under which to place the new tidy check (e.g., misc)",
619 )
620 parser.add_argument(
621 "check", nargs="?", help="name of new tidy check to add (e.g. foo-do-the-stuff)"
622 )
623 args = parser.parse_args()
624
625 if args.update_docs:
626 update_checks_list(os.path.dirname(sys.argv[0]))
627 return
628
629 if not args.module or not args.check:
630 print("Module and check must be specified.")
631 parser.print_usage()
632 return
633
634 module = args.module
635 check_name = args.check
636 check_name_camel = get_camel_check_name(check_name)
637 if check_name.startswith(module):
638 print(
639 'Check name "%s" must not start with the module "%s". Exiting.'
640 % (check_name, module)
641 )
642 return
643 clang_tidy_path = os.path.dirname(sys.argv[0])
644 module_path = os.path.join(clang_tidy_path, module)
645
646 if not adapt_cmake(module_path, check_name_camel):
647 return
648
649 # Map module names to namespace names that don't conflict with widely used top-level namespaces.
650 if module == "llvm":
651 namespace = module + "_check"
652 else:
653 namespace = module
654
655 write_header(module_path, module, namespace, check_name, check_name_camel)
656 write_implementation(module_path, module, namespace, check_name_camel)
657 adapt_module(module_path, module, check_name, check_name_camel)
658 add_release_notes(module_path, module, check_name)
659 test_extension = language_to_extension.get(args.language)
660 write_test(module_path, module, check_name, test_extension)
661 write_docs(module_path, module, check_name)
662 update_checks_list(clang_tidy_path)
663 print("Done. Now it's your turn!")
664
665
666if __name__ == "__main__":
667 main()
def adapt_cmake(module_path, check_name_camel)
def update_checks_list(clang_tidy_path)
def get_camel_name(check_name)
def get_camel_check_name(check_name)
def write_implementation(module_path, module, namespace, check_name_camel)
def get_actual_filename(dirname, filename)
def write_header(module_path, module, namespace, check_name, check_name_camel)
def write_test(module_path, module, check_name, test_extension)
def adapt_module(module_path, module, check_name, check_name_camel)
def add_release_notes(module_path, module, check_name)
def write_docs(module_path, module, check_name)
def get_module_filename(module_path, module)