xref: /llvm-project/clang-tools-extra/docs/clang-tidy/checks/gen-static-analyzer-docs.py (revision bc70c29558c6ecb53e61cc8d668768e919e81bde)
1"""
2Generates documentation based off the available static analyzers checks
3References Checkers.td to determine what checks exist
4"""
5
6import subprocess
7import json
8import os
9import re
10
11"""Get path of script so files are always in correct directory"""
12__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
13
14default_checkers_td_location = "../../../../clang/include/clang/StaticAnalyzer/Checkers/Checkers.td"
15default_checkers_rst_location = "../../../../clang/docs/analyzer/checkers.rst"
16
17"""Get dict of checker related info and parse for full check names
18
19Returns:
20  checkers: dict of checker info
21"""
22def get_checkers(checkers_td, checkers_rst):
23    p = subprocess.Popen(
24        [
25            "llvm-tblgen",
26            "--dump-json",
27            "-I",
28            os.path.dirname(checkers_td),
29            checkers_td,
30        ],
31        stdout=subprocess.PIPE,
32    )
33    table_entries = json.loads(p.communicate()[0])
34    documentable_checkers = []
35    checkers = table_entries["!instanceof"]["Checker"]
36
37    with open(checkers_rst, "r") as f:
38        checker_rst_text = f.read()
39
40    for checker_ in checkers:
41        checker = table_entries[checker_]
42        checker_name = checker["CheckerName"]
43        package_ = checker["ParentPackage"]["def"]
44        package = table_entries[package_]
45        package_name = package["PackageName"]
46        checker_package_prefix = package_name
47        parent_package_ = package["ParentPackage"]
48        hidden = (checker["Hidden"] != 0) or (package["Hidden"] != 0)
49
50        while parent_package_ is not None:
51            parent_package = table_entries[parent_package_["def"]]
52            checker_package_prefix = (
53                parent_package["PackageName"] + "." + checker_package_prefix
54            )
55            hidden = hidden or parent_package["Hidden"] != 0
56            parent_package_ = parent_package["ParentPackage"]
57
58        full_package_name = (
59            "clang-analyzer-" + checker_package_prefix + "." + checker_name
60        )
61        anchor_url = re.sub(
62            r"\.", "-", checker_package_prefix + "." + checker_name
63        ).lower()
64
65        if not hidden and "alpha" not in full_package_name.lower():
66            checker["FullPackageName"] = full_package_name
67            checker["ShortName"] = checker_package_prefix + "." + checker_name
68            checker["AnchorUrl"] = anchor_url
69            checker["Documentation"] = ".. _%s:" % (checker["ShortName"].replace(".","-")) in checker_rst_text
70            documentable_checkers.append(checker)
71
72    documentable_checkers.sort(key=lambda x: x["FullPackageName"])
73    return documentable_checkers
74
75
76"""Generate documentation for checker
77
78Args:
79  checker: Checker for which to generate documentation.
80  has_documentation: Specify that there is other documentation to link to.
81"""
82def generate_documentation(checker, has_documentation):
83
84    with open(
85        os.path.join(__location__, "clang-analyzer", checker["ShortName"] + ".rst"), "w"
86    ) as f:
87        f.write(".. title:: clang-tidy - %s\n" % checker["FullPackageName"])
88        if has_documentation:
89            f.write(".. meta::\n")
90            f.write(
91                "   :http-equiv=refresh: 5;URL=https://clang.llvm.org/docs/analyzer/checkers.html#%s\n"
92                % checker["AnchorUrl"]
93            )
94        f.write("\n")
95        f.write("%s\n" % checker["FullPackageName"])
96        f.write("=" * len(checker["FullPackageName"]) + "\n")
97        help_text = checker["HelpText"].strip()
98        if not help_text.endswith("."):
99            help_text += "."
100        characters = 80
101        for word in help_text.split(" "):
102            if characters+len(word)+1 > 80:
103                characters = len(word)
104                f.write("\n")
105                f.write(word)
106            else:
107                f.write(" ")
108                f.write(word)
109                characters += len(word) + 1
110        f.write("\n\n")
111        if has_documentation:
112            f.write(
113                "The `%s` check is an alias, please see\n" % checker["FullPackageName"]
114            )
115            f.write(
116                "`Clang Static Analyzer Available Checkers\n<https://clang.llvm.org/docs/analyzer/checkers.html#%s>`_\n"
117                % checker["AnchorUrl"]
118            )
119            f.write("for more information.\n")
120        else:
121            f.write("The %s check is an alias of\nClang Static Analyzer %s.\n" % (checker["FullPackageName"], checker["ShortName"]));
122        f.close()
123
124
125"""Update list.rst to include the new checks
126
127Args:
128  checkers: dict acquired from get_checkers()
129"""
130def update_documentation_list(checkers):
131    with open(os.path.join(__location__, "list.rst"), "r+") as f:
132        f_text = f.read()
133        check_text = f_text.split(':header: "Name", "Redirect", "Offers fixes"\n')[1]
134        checks = [x for x in check_text.split("\n") if ":header:" not in x and x]
135        old_check_text = "\n".join(checks)
136        checks = [x for x in checks if "clang-analyzer-" not in x]
137        for checker in checkers:
138            if checker["Documentation"]:
139                checks.append("   :doc:`%s <clang-analyzer/%s>`, `Clang Static Analyzer %s <https://clang.llvm.org/docs/analyzer/checkers.html#%s>`_," % (checker["FullPackageName"],
140                                                        checker["ShortName"],  checker["ShortName"], checker["AnchorUrl"]))
141            else:
142                checks.append("   :doc:`%s <clang-analyzer/%s>`, Clang Static Analyzer %s," % (checker["FullPackageName"], checker["ShortName"],  checker["ShortName"]))
143
144        checks.sort()
145
146        # Overwrite file with new data
147        f.seek(0)
148        f_text = f_text.replace(old_check_text, "\n".join(checks))
149        f.write(f_text)
150        f.close()
151
152
153def main():
154    CheckersPath = os.path.join(__location__, default_checkers_td_location)
155    if not os.path.exists(CheckersPath):
156        print("Could not find Checkers.td under %s." % (os.path.abspath(CheckersPath)))
157        exit(1)
158
159    CheckersDoc = os.path.join(__location__, default_checkers_rst_location)
160    if not os.path.exists(CheckersDoc):
161        print("Could not find checkers.rst under %s." % (os.path.abspath(CheckersDoc)))
162        exit(1)
163
164    checkers = get_checkers(CheckersPath, CheckersDoc)
165    for checker in checkers:
166        generate_documentation(checker, checker["Documentation"])
167        print("Generated documentation for: %s" % (checker["FullPackageName"]))
168    update_documentation_list(checkers)
169
170
171if __name__ == "__main__":
172    main()
173