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:::\n\n## Check aliases\n\n")
602 f.write(":::{table}\n")
603 f.write(":class: clang-tidy-checks-table\n")
604 f.write(":widths: 45 45 10\n\n")
605 f.write("| Name | Redirect | Offers fixes |\n")
606 f.write("| --- | --- | --- |\n")
607 alias_rows = list(map(format_link_alias, doc_files))
608 alias_rows.extend(clang_analyzer_alias_rows)
609 f.writelines(sorted(filter(None, alias_rows)))
610 f.write("\n:::\n")
611 break
612
613
614# Adds a documentation for the check.
615def write_docs(module_path: str, module: str, check_name: str) -> None:
616 check_name_dashes = f"{module}-{check_name}"
617 filename = os.path.normpath(
618 os.path.join(
619 module_path, "../../docs/clang-tidy/checks/", module, f"{check_name}.md"
620 )
621 )
622 print(f"Creating {filename}...")
623 with open(filename, "w", encoding="utf8", newline="\n") as f:
624 f.write(
625 """```{title} clang-tidy - %(check_name_dashes)s
626```
627
628# %(check_name_dashes)s
629
630FIXME: Describe what patterns does the check detect and why. Give examples.
631"""
632 % {"check_name_dashes": check_name_dashes}
633 )
634
635
636def get_camel_name(check_name: str) -> str:
637 return "".join(map(lambda elem: elem.capitalize(), check_name.split("-")))
638
639
640def get_camel_check_name(check_name: str) -> str:
641 return f"{get_camel_name(check_name)}Check"
642
643
644def main() -> None:
645 language_to_extension = {
646 "c": "c",
647 "c++": "cpp",
648 "objc": "m",
649 "objc++": "mm",
650 }
651 cpp_language_to_requirements = {
652 "c++98": "CPlusPlus",
653 "c++11": "CPlusPlus11",
654 "c++14": "CPlusPlus14",
655 "c++17": "CPlusPlus17",
656 "c++20": "CPlusPlus20",
657 "c++23": "CPlusPlus23",
658 "c++26": "CPlusPlus26",
659 }
660 c_language_to_requirements = {
661 "c99": None,
662 "c11": "C11",
663 "c17": "C17",
664 "c23": "C23",
665 "c27": "C2Y",
666 }
667 parser = argparse.ArgumentParser()
668 parser.add_argument(
669 "--update-docs",
670 action="store_true",
671 help="just update the list of documentation files, then exit",
672 )
673 parser.add_argument(
674 "--language",
675 help="language to use for new check (defaults to c++)",
676 choices=language_to_extension.keys(),
677 default=None,
678 metavar="LANG",
679 )
680 parser.add_argument(
681 "--description",
682 "-d",
683 help="short description of what the check does",
684 default="FIXME: Write a short description",
685 type=str,
686 )
687 parser.add_argument(
688 "--standard",
689 help="Specify a specific version of the language",
690 choices=list(
691 itertools.chain(
692 cpp_language_to_requirements.keys(), c_language_to_requirements.keys()
693 )
694 ),
695 default=None,
696 )
697 parser.add_argument(
698 "module",
699 nargs="?",
700 help="module directory under which to place the new tidy check (e.g., misc)",
701 )
702 parser.add_argument(
703 "check", nargs="?", help="name of new tidy check to add (e.g. foo-do-the-stuff)"
704 )
705 args = parser.parse_args()
706
707 if args.update_docs:
708 update_checks_list(os.path.dirname(sys.argv[0]))
709 return
710
711 if not args.module or not args.check:
712 print("Module and check must be specified.")
713 parser.print_usage()
714 return
715
716 module = args.module
717 check_name = args.check
718 check_name_camel = get_camel_check_name(check_name)
719 if check_name.startswith(module):
720 print(
721 f'Check name "{check_name}" must not start with the module "{module}". Exiting.'
722 )
723 return
724 clang_tidy_path = os.path.dirname(sys.argv[0])
725 module_path = os.path.join(clang_tidy_path, module)
726
727 if not adapt_cmake(module_path, check_name_camel):
728 return
729
730 # Map module names to namespace names that don't conflict with widely used top-level namespaces.
731 if module == "llvm":
732 namespace = f"{module}_check"
733 else:
734 namespace = module
735
736 description = args.description
737 if not description.endswith("."):
738 description += "."
739
740 language = args.language
741
742 if args.standard:
743 if args.standard in cpp_language_to_requirements:
744 if language and language != "c++":
745 raise ValueError("C++ standard chosen when language is not C++")
746 language = "c++"
747 elif args.standard in c_language_to_requirements:
748 if language and language != "c":
749 raise ValueError("C standard chosen when language is not C")
750 language = "c"
751
752 if not language:
753 language = "c++"
754
755 language_restrict = None
756
757 if language == "c":
758 language_restrict = "!%(lang)s.CPlusPlus"
759 if extra := c_language_to_requirements.get(args.standard, None):
760 language_restrict += f" && %(lang)s.{extra}"
761 elif language == "c++":
762 language_restrict = (
763 f"%(lang)s.{cpp_language_to_requirements.get(args.standard, 'CPlusPlus')}"
764 )
765 elif language in ["objc", "objc++"]:
766 language_restrict = "%(lang)s.ObjC"
767 else:
768 raise ValueError(f"Unsupported language '{language}' was specified")
769
771 module_path,
772 module,
773 namespace,
774 check_name,
775 check_name_camel,
776 description,
777 language_restrict,
778 )
779 write_implementation(module_path, module, namespace, check_name_camel)
780 adapt_module(module_path, module, check_name, check_name_camel)
781 add_release_notes(module_path, module, check_name, description)
782 test_extension = language_to_extension[language]
783 write_test(module_path, module, check_name, test_extension, args.standard)
784 write_docs(module_path, module, check_name)
785 update_checks_list(clang_tidy_path)
786 print("Done. Now it's your turn!")
787
788
789if __name__ == "__main__":
790 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)