clang-tools 24.0.0git
add_new_check.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2#
3# ===-----------------------------------------------------------------------===#
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
11import argparse
12import itertools
13import os
14import re
15import sys
16import textwrap
17from typing import Optional, Tuple, Match
18
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: str, check_name_camel: str) -> bool:
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 `open(filename, mode, encoding='utf8')` for reading and
28 # writing files here and elsewhere.
29 with open(filename, "r", encoding="utf8") as f:
30 lines = f.readlines()
31
32 cpp_file = f"{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(f"Updating {filename}...")
40 with 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(f" {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.
57 module_path: str,
58 module: str,
59 namespace: str,
60 check_name: str,
61 check_name_camel: str,
62 description: str,
63 lang_restrict: str,
64) -> None:
65 wrapped_desc = "\n".join(
66 textwrap.wrap(
67 description, width=80, initial_indent="/// ", subsequent_indent="/// "
68 )
69 )
70 if lang_restrict:
71 override_supported = """
72 bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
73 return %s;
74 }""" % (
75 lang_restrict % {"lang": "LangOpts"}
76 )
77 else:
78 override_supported = ""
79 filename = f"{os.path.join(module_path, check_name_camel)}.h"
80 print(f"Creating {filename}...")
81 with open(filename, "w", encoding="utf8", newline="\n") as f:
82 header_guard = (
83 f"LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_{module.upper()}_"
84 f"{check_name_camel.upper()}_H"
85 )
86 f.write(
87 """\
88//===----------------------------------------------------------------------===//
89//
90// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
91// See https://llvm.org/LICENSE.txt for license information.
92// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
93//
94//===----------------------------------------------------------------------===//
95
96#ifndef %(header_guard)s
97#define %(header_guard)s
98
99#include "../ClangTidyCheck.h"
100
101namespace clang::tidy::%(namespace)s {
102
103%(description)s
104///
105/// For the user-facing documentation see:
106/// https://clang.llvm.org/extra/clang-tidy/checks/%(module)s/%(check_name)s.html
107class %(check_name_camel)s : public ClangTidyCheck {
108public:
109 %(check_name_camel)s(StringRef Name, ClangTidyContext *Context)
110 : ClangTidyCheck(Name, Context) {}
111 void registerMatchers(ast_matchers::MatchFinder *Finder) override;
112 void check(const ast_matchers::MatchFinder::MatchResult &Result) override;%(override_supported)s
113};
114
115} // namespace clang::tidy::%(namespace)s
116
117#endif // %(header_guard)s
118"""
119 % {
120 "header_guard": header_guard,
121 "check_name_camel": check_name_camel,
122 "check_name": check_name,
123 "module": module,
124 "namespace": namespace,
125 "description": wrapped_desc,
126 "override_supported": override_supported,
127 }
128 )
129
130
131# Adds the implementation of the new check.
133 module_path: str, module: str, namespace: str, check_name_camel: str
134) -> None:
135 filename = f"{os.path.join(module_path, check_name_camel)}.cpp"
136 print(f"Creating {filename}...")
137 with open(filename, "w", encoding="utf8", newline="\n") as f:
138 f.write(
139 """\
140//===----------------------------------------------------------------------===//
141//
142// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
143// See https://llvm.org/LICENSE.txt for license information.
144// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
145//
146//===----------------------------------------------------------------------===//
147
148#include "%(check_name)s.h"
149#include "clang/ASTMatchers/ASTMatchFinder.h"
150
151using namespace clang::ast_matchers;
152
153namespace clang::tidy::%(namespace)s {
154
155void %(check_name)s::registerMatchers(MatchFinder *Finder) {
156 // FIXME: Add matchers.
157 Finder->addMatcher(functionDecl().bind("x"), this);
158}
159
160void %(check_name)s::check(const MatchFinder::MatchResult &Result) {
161 // FIXME: Add callback implementation.
162 const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
163 if (!MatchedDecl->getIdentifier() || MatchedDecl->getName().starts_with("awesome_"))
164 return;
165 diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome")
166 << MatchedDecl
167 << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
168 diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note);
169}
170
171} // namespace clang::tidy::%(namespace)s
172"""
173 % {"check_name": check_name_camel, "namespace": namespace}
174 )
175
176
177# Returns the source filename that implements the module.
178def get_module_filename(module_path: str, module: str) -> str:
179 modulecpp = list(
180 filter(
181 lambda p: p.lower() == f"{module.lower()}tidymodule.cpp",
182 os.listdir(module_path),
183 )
184 )[0]
185 return os.path.join(module_path, modulecpp)
186
187
188# Modifies the module to include the new check.
190 module_path: str, module: str, check_name: str, check_name_camel: str
191) -> None:
192 filename = get_module_filename(module_path, module)
193 with open(filename, "r", encoding="utf8") as f:
194 lines = f.readlines()
195
196 print(f"Updating {filename}...")
197 with open(filename, "w", encoding="utf8", newline="\n") as f:
198 header_added = False
199 header_found = False
200 check_added = False
201 check_fq_name = f"{module}-{check_name}"
202 check_decl = (
203 f" CheckFactories.registerCheck<{check_name_camel}>(\n"
204 f' "{check_fq_name}");\n'
205 )
206
207 lines_iter = iter(lines)
208 try:
209 while True:
210 line = next(lines_iter)
211 if not header_added:
212 if match := re.search('#include "(.*)"', line):
213 header_found = True
214 if match.group(1) > check_name_camel:
215 header_added = True
216 f.write(f'#include "{check_name_camel}.h"\n')
217 elif header_found:
218 header_added = True
219 f.write(f'#include "{check_name_camel}.h"\n')
220
221 if not check_added:
222 if line.strip() == "}":
223 check_added = True
224 f.write(check_decl)
225 else:
226 prev_line = None
227 if match := re.search(
228 r'registerCheck<(.*)> *\‍( *(?:"([^"]*)")?', line
229 ):
230 current_check_name = match.group(2)
231 if current_check_name is None:
232 # If we didn't find the check name on this line, look on the
233 # next one.
234 prev_line = line
235 line = next(lines_iter)
236 match = re.search(' *"([^"]*)"', line)
237 if match:
238 current_check_name = match.group(1)
239 assert current_check_name
240 if current_check_name > check_fq_name:
241 check_added = True
242 f.write(check_decl)
243 if prev_line:
244 f.write(prev_line)
245 f.write(line)
246 except StopIteration:
247 pass
248
249
250# Adds a release notes entry.
252 module_path: str, module: str, check_name: str, description: str
253) -> None:
254 wrapped_desc = "\n".join(
255 textwrap.wrap(
256 description, width=80, initial_indent=" ", subsequent_indent=" "
257 )
258 )
259 check_name_dashes = f"{module}-{check_name}"
260 filename = os.path.normpath(os.path.join(module_path, "../../docs/ReleaseNotes.md"))
261 with open(filename, "r", encoding="utf8") as f:
262 lines = f.readlines()
263
264 lineMatcher = re.compile(r"#### New checks")
265 nextSectionMatcher = re.compile(r"#### New check aliases")
266 checkMatcher = re.compile(r"- New \{doc\}`(.*)")
267
268 print(f"Updating {filename}...")
269 with open(filename, "w", encoding="utf8", newline="\n") as f:
270 note_added = False
271 header_found = False
272 add_note_here = False
273
274 for line in lines:
275 if not note_added:
276 if match_check := checkMatcher.match(line):
277 last_check = match_check.group(1)
278 if last_check > check_name_dashes:
279 add_note_here = True
280
281 if nextSectionMatcher.match(line):
282 add_note_here = True
283
284 if lineMatcher.match(line):
285 header_found = True
286 f.write(line)
287 continue
288
289 if header_found and add_note_here:
290 f.write(
291 f"""- New {{doc}}`{check_name_dashes}
292 <clang-tidy/checks/{module}/{check_name}>` check.
293
294{wrapped_desc}
295
296"""
297 )
298 note_added = True
299
300 f.write(line)
301
302
303# Adds a test for the check.
305 module_path: str,
306 module: str,
307 check_name: str,
308 test_extension: str,
309 test_standard: Optional[str],
310) -> None:
311 test_standard = f"-std={test_standard}-or-later " if test_standard else ""
312 check_name_dashes = f"{module}-{check_name}"
313 filename = os.path.normpath(
314 os.path.join(
315 module_path,
316 "..",
317 "..",
318 "test",
319 "clang-tidy",
320 "checkers",
321 module,
322 f"{check_name}.{test_extension}",
323 )
324 )
325 print(f"Creating {filename}...")
326 with open(filename, "w", encoding="utf8", newline="\n") as f:
327 f.write(
328 """\
329// RUN: %%check_clang_tidy %(standard)s%%s %(check_name_dashes)s %%t
330
331// FIXME: Add something that triggers the check here.
332void f();
333// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s]
334
335// FIXME: Verify the applied fix.
336// * Make the CHECK patterns specific enough and try to make verified lines
337// unique to avoid incorrect matches.
338// * Use {{}} for regular expressions.
339// CHECK-FIXES: {{^}}void awesome_f();{{$}}
340
341// FIXME: Add something that doesn't trigger the check here.
342void awesome_f2();
343"""
344 % {"check_name_dashes": check_name_dashes, "standard": test_standard}
345 )
346
347
348def get_actual_filename(dirname: str, filename: str) -> str:
349 if not os.path.isdir(dirname):
350 return ""
351 name = os.path.join(dirname, filename)
352 if os.path.isfile(name):
353 return name
354 caselessname = filename.lower()
355 for file in os.listdir(dirname):
356 if file.lower() == caselessname:
357 return os.path.join(dirname, file)
358 return ""
359
360
361# Recreates the list of checks in the docs/clang-tidy/checks directory.
362def update_checks_list(clang_tidy_path: str) -> None:
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"))
365 # Read the content of the current list.md file.
366 with open(filename, "r", encoding="utf8") as f:
367 lines = f.readlines()
368 # Get all existing docs
369 doc_files = []
370 for subdir in filter(
371 lambda s: os.path.isdir(os.path.join(docs_dir, s)), os.listdir(docs_dir)
372 ):
373 # Static analyzer checks are maintained by gen-static-analyzer-docs.py.
374 if subdir == "clang-analyzer":
375 continue
376 for file in os.listdir(os.path.join(docs_dir, subdir)):
377 # TODO: Stop discovering reST files once all clang-tidy check
378 # documentation has been migrated to MyST.
379 if os.path.splitext(file)[1] in (".md", ".rst"):
380 doc_files.append((subdir, file))
381 doc_files.sort()
382
383 # Filter the file for the rows in the table corresponding to CSA check
384 # aliases.
385 check_alias_lines = lines[lines.index("## Check aliases\n") :]
386 clang_analyzer_alias_rows = [
387 row
388 for row in check_alias_lines
389 if re.match(r"^\| \{doc\}`.*clang-analyzer", row)
390 ]
391
392 # We couldn't find the source file from the check name, so try to find the
393 # class name that corresponds to the check in the module file.
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):
397 return ""
398 module_file = get_module_filename(module_path, module_name)
399 if not os.path.isfile(module_file):
400 return ""
401 with open(module_file, "r") as f:
402 code = f.read()
403 full_check_name = f"{module_name}-{check_name}"
404 if (name_pos := code.find(f'"{full_check_name}"')) == -1:
405 return ""
406 if (stmt_end_pos := code.find(";", name_pos)) == -1:
407 return ""
408 if (stmt_start_pos := code.rfind(";", 0, name_pos)) == -1 and (
409 stmt_start_pos := code.rfind("{", 0, name_pos)
410 ) == -1:
411 return ""
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]
421 )
422 else:
423 class_path = os.path.join(clang_tidy_path, module_name)
424 return get_actual_filename(class_path, f"{class_name}.cpp")
425
426 return ""
427
428 # Examine code looking for a c'tor definition to get the base class 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)
433
434 # The constructor might be inline in the header.
435 if not matches:
436 header_file = f"{os.path.splitext(check_file)[0]}.h"
437 if not os.path.isfile(header_file):
438 return ""
439 with open(header_file, encoding="utf8") as f:
440 code = f.read()
441 matches = re.search(rf" {ctor_pattern}", code)
442
443 if matches and matches[1] != "ClangTidyCheck":
444 return matches[1]
445 return ""
446
447 # Some simple heuristics to figure out if a check has an autofix or not.
448 def has_fixits(code: str) -> bool:
449 for needle in [
450 "FixItHint",
451 "ReplacementText",
452 "fixit",
453 "FixIt",
454 "TransformerClangTidyCheck",
455 ]:
456 if needle in code:
457 return True
458 return False
459
460 # Try to figure out of the check supports fixits.
461 def has_auto_fix(check_name: str) -> str:
462 dirname, _, check_name = check_name.partition("-")
463
464 check_file = get_actual_filename(
465 os.path.join(clang_tidy_path, dirname),
466 f"{get_camel_check_name(check_name)}.cpp",
467 )
468 if not os.path.isfile(check_file):
469 # Some older checks don't end with 'Check.cpp'
470 check_file = get_actual_filename(
471 os.path.join(clang_tidy_path, dirname),
472 f"{get_camel_name(check_name)}.cpp",
473 )
474 if not os.path.isfile(check_file):
475 # Some checks aren't in a file based on the check name.
476 check_file = filename_from_module(dirname, check_name)
477 if not (check_file and os.path.isfile(check_file)):
478 return ""
479
480 with open(check_file, encoding="utf8") as f:
481 code = f.read()
482 if has_fixits(code):
483 return ' "Yes"'
484
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:
489 code = f.read()
490 if has_fixits(code):
491 return ' "Yes"'
492
493 return ""
494
495 def detect_alias_target(check_name: str, content: str) -> Optional[str]:
496 """Return the documentation target for non-redirect alias pages.
497
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.
501 """
502 paragraphs = [
503 re.sub(r"\s+", " ", paragraph.strip())
504 for paragraph in re.split(r"\n\s*\n", content)
505 if paragraph.strip()
506 ]
507
508 self_alias = re.compile(
509 r"^This check is an alias(?: of check| for)\b",
510 re.IGNORECASE,
511 )
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",
515 re.IGNORECASE,
516 )
517
518 for paragraph in paragraphs:
519 if self_alias.search(paragraph) or named_alias.search(paragraph):
520 # Matches :doc:`label <target>` and {doc}`label <target>` roles.
521 # TODO: Remove the reST role handling once all clang-tidy check
522 # documentation has been migrated to MyST.
523 if match := re.search(
524 r"(?:\{doc\}|:doc:)`[^`<]+?<([^>]+)>`", paragraph
525 ):
526 return match.group(1)
527 # TODO: Remove the reST link handling once all clang-tidy check
528 # documentation has been migrated to MyST.
529 if match := re.search(r"`[^`<]+?<(.+?)\.html(?:#[^>]+)?>`_", paragraph):
530 return match.group(1)
531 # Matches a Markdown link of the form [label](target).
532 if match := re.search(r"\[[^]]+\]\‍(([^)]+)\‍)", paragraph):
533 return match.group(1)
534 return None
535
536 def doc_file_stem(doc_file: Tuple[str, str]) -> str:
537 return os.path.splitext(doc_file[1])[0]
538
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)}"
541
542 with open(os.path.join(docs_dir, *doc_file), "r", encoding="utf8") as doc:
543 content = doc.read()
544
545 # Matches `:orphan:` and `orphan: true`
546 # TODO: Remove the reST orphan handling once all clang-tidy check
547 # documentation has been migrated to MyST.
548 if re.search(r"^\s*(?::orphan:|orphan:\s*true)\s*$", content, re.MULTILINE):
549 # Orphan page, don't list it.
550 return "", None
551
552 return check_name, detect_alias_target(check_name, content)
553
554 def format_doc_ref(label: str, module: str, check_file: str) -> str:
555 return f"{{doc}}`{label} <{module}/{check_file}>`"
556
557 def has_auto_fix_cell(check_name: str) -> str:
558 return has_auto_fix(check_name).strip().strip('"')
559
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-"):
563 return (
564 f"| {format_doc_ref(check_name, doc_file[0], doc_file_stem(doc_file))} | "
565 f"{has_auto_fix_cell(check_name)} |\n"
566 )
567 else:
568 return ""
569
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:
574 return ""
575
576 module = doc_file[0]
577 check_file = doc_file_stem(doc_file)
578 # Match neighbour or current-directory doc targets.
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)
586
587 return (
588 f"| {format_doc_ref(check_name, module, check_file)} | "
589 f"{redirect} | {autofix} |\n"
590 )
591
592 print(f"Updating {filename}...")
593 with open(filename, "w", encoding="utf8", newline="\n") as f:
594 for line in lines:
595 f.write(line)
596 if line == "| Name | Offers fixes |\n":
597 # We dump the checkers
598 f.write("| --- | --- |\n")
599 f.writelines(sorted(filter(None, map(format_link, doc_files))))
600 # and the aliases
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)))
607 break
608
609
610# Adds a documentation for the check.
611def write_docs(module_path: str, module: str, check_name: str) -> None:
612 check_name_dashes = f"{module}-{check_name}"
613 filename = os.path.normpath(
614 os.path.join(
615 module_path, "../../docs/clang-tidy/checks/", module, f"{check_name}.md"
616 )
617 )
618 print(f"Creating {filename}...")
619 with open(filename, "w", encoding="utf8", newline="\n") as f:
620 f.write(
621 """```{title} clang-tidy - %(check_name_dashes)s
622```
623
624# %(check_name_dashes)s
625
626FIXME: Describe what patterns does the check detect and why. Give examples.
627"""
628 % {"check_name_dashes": check_name_dashes}
629 )
630
631
632def get_camel_name(check_name: str) -> str:
633 return "".join(map(lambda elem: elem.capitalize(), check_name.split("-")))
634
635
636def get_camel_check_name(check_name: str) -> str:
637 return f"{get_camel_name(check_name)}Check"
638
639
640def main() -> None:
641 language_to_extension = {
642 "c": "c",
643 "c++": "cpp",
644 "objc": "m",
645 "objc++": "mm",
646 }
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",
655 }
656 c_language_to_requirements = {
657 "c99": None,
658 "c11": "C11",
659 "c17": "C17",
660 "c23": "C23",
661 "c27": "C2Y",
662 }
663 parser = argparse.ArgumentParser()
664 parser.add_argument(
665 "--update-docs",
666 action="store_true",
667 help="just update the list of documentation files, then exit",
668 )
669 parser.add_argument(
670 "--language",
671 help="language to use for new check (defaults to c++)",
672 choices=language_to_extension.keys(),
673 default=None,
674 metavar="LANG",
675 )
676 parser.add_argument(
677 "--description",
678 "-d",
679 help="short description of what the check does",
680 default="FIXME: Write a short description",
681 type=str,
682 )
683 parser.add_argument(
684 "--standard",
685 help="Specify a specific version of the language",
686 choices=list(
687 itertools.chain(
688 cpp_language_to_requirements.keys(), c_language_to_requirements.keys()
689 )
690 ),
691 default=None,
692 )
693 parser.add_argument(
694 "module",
695 nargs="?",
696 help="module directory under which to place the new tidy check (e.g., misc)",
697 )
698 parser.add_argument(
699 "check", nargs="?", help="name of new tidy check to add (e.g. foo-do-the-stuff)"
700 )
701 args = parser.parse_args()
702
703 if args.update_docs:
704 update_checks_list(os.path.dirname(sys.argv[0]))
705 return
706
707 if not args.module or not args.check:
708 print("Module and check must be specified.")
709 parser.print_usage()
710 return
711
712 module = args.module
713 check_name = args.check
714 check_name_camel = get_camel_check_name(check_name)
715 if check_name.startswith(module):
716 print(
717 f'Check name "{check_name}" must not start with the module "{module}". Exiting.'
718 )
719 return
720 clang_tidy_path = os.path.dirname(sys.argv[0])
721 module_path = os.path.join(clang_tidy_path, module)
722
723 if not adapt_cmake(module_path, check_name_camel):
724 return
725
726 # Map module names to namespace names that don't conflict with widely used top-level namespaces.
727 if module == "llvm":
728 namespace = f"{module}_check"
729 else:
730 namespace = module
731
732 description = args.description
733 if not description.endswith("."):
734 description += "."
735
736 language = args.language
737
738 if args.standard:
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++")
742 language = "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")
746 language = "c"
747
748 if not language:
749 language = "c++"
750
751 language_restrict = None
752
753 if language == "c":
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')}"
760 )
761 elif language in ["objc", "objc++"]:
762 language_restrict = "%(lang)s.ObjC"
763 else:
764 raise ValueError(f"Unsupported language '{language}' was specified")
765
767 module_path,
768 module,
769 namespace,
770 check_name,
771 check_name_camel,
772 description,
773 language_restrict,
774 )
775 write_implementation(module_path, module, namespace, check_name_camel)
776 adapt_module(module_path, module, check_name, check_name_camel)
777 add_release_notes(module_path, module, check_name, description)
778 test_extension = language_to_extension[language]
779 write_test(module_path, module, check_name, test_extension, args.standard)
780 write_docs(module_path, module, check_name)
781 update_checks_list(clang_tidy_path)
782 print("Done. Now it's your turn!")
783
784
785if __name__ == "__main__":
786 main()
None write_header(str module_path, str module, str namespace, str check_name, str check_name_camel, str description, str lang_restrict)
str get_module_filename(str module_path, str module)
None update_checks_list(str clang_tidy_path)
None adapt_module(str module_path, str module, str check_name, str check_name_camel)
str get_camel_name(str check_name)
None write_docs(str module_path, str module, str check_name)
None write_implementation(str module_path, str module, str namespace, str check_name_camel)
str get_actual_filename(str dirname, str filename)
None write_test(str module_path, str module, str check_name, str test_extension, Optional[str] test_standard)
bool adapt_cmake(str module_path, str check_name_camel)
str get_camel_check_name(str check_name)
None add_release_notes(str module_path, str module, str check_name, str description)