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