xref: /llvm-project/clang-tools-extra/clang-tidy/rename_check.py (revision 4aba20fecaa09089132afe451aa04a22cd3794ca)
1#!/usr/bin/env python3
2#
3# ===- rename_check.py - clang-tidy check renamer ------------*- 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 glob
13import io
14import os
15import re
16import sys
17from typing import List
18
19
20def replaceInFileRegex(fileName: str, sFrom: str, sTo: str) -> None:
21    if sFrom == sTo:
22        return
23
24    # The documentation files are encoded using UTF-8, however on Windows the
25    # default encoding might be different (e.g. CP-1252). To make sure UTF-8 is
26    # always used, use `io.open(filename, mode, encoding='utf8')` for reading and
27    # writing files here and elsewhere.
28    txt = None
29    with io.open(fileName, "r", encoding="utf8") as f:
30        txt = f.read()
31
32    txt = re.sub(sFrom, sTo, txt)
33    print("Replacing '%s' -> '%s' in '%s'..." % (sFrom, sTo, fileName))
34    with io.open(fileName, "w", encoding="utf8") as f:
35        f.write(txt)
36
37
38def replaceInFile(fileName: str, sFrom: str, sTo: str) -> None:
39    if sFrom == sTo:
40        return
41    txt = None
42    with io.open(fileName, "r", encoding="utf8") as f:
43        txt = f.read()
44
45    if sFrom not in txt:
46        return
47
48    txt = txt.replace(sFrom, sTo)
49    print("Replacing '%s' -> '%s' in '%s'..." % (sFrom, sTo, fileName))
50    with io.open(fileName, "w", encoding="utf8") as f:
51        f.write(txt)
52
53
54def generateCommentLineHeader(filename: str) -> str:
55    return "".join(
56        [
57            "//===--- ",
58            os.path.basename(filename),
59            " - clang-tidy ",
60            "-" * max(0, 42 - len(os.path.basename(filename))),
61            "*- C++ -*-===//",
62        ]
63    )
64
65
66def generateCommentLineSource(filename: str) -> str:
67    return "".join(
68        [
69            "//===--- ",
70            os.path.basename(filename),
71            " - clang-tidy",
72            "-" * max(0, 52 - len(os.path.basename(filename))),
73            "-===//",
74        ]
75    )
76
77
78def fileRename(fileName: str, sFrom: str, sTo: str) -> str:
79    if sFrom not in fileName or sFrom == sTo:
80        return fileName
81    newFileName = fileName.replace(sFrom, sTo)
82    print("Renaming '%s' -> '%s'..." % (fileName, newFileName))
83    os.rename(fileName, newFileName)
84    return newFileName
85
86
87def deleteMatchingLines(fileName: str, pattern: str) -> bool:
88    lines = None
89    with io.open(fileName, "r", encoding="utf8") as f:
90        lines = f.readlines()
91
92    not_matching_lines = [l for l in lines if not re.search(pattern, l)]
93    if len(not_matching_lines) == len(lines):
94        return False
95
96    print("Removing lines matching '%s' in '%s'..." % (pattern, fileName))
97    print("  " + "  ".join([l for l in lines if re.search(pattern, l)]))
98    with io.open(fileName, "w", encoding="utf8") as f:
99        f.writelines(not_matching_lines)
100
101    return True
102
103
104def getListOfFiles(clang_tidy_path: str) -> List[str]:
105    files = glob.glob(os.path.join(clang_tidy_path, "**"), recursive=True)
106    files += [
107        os.path.normpath(os.path.join(clang_tidy_path, "../docs/ReleaseNotes.rst"))
108    ]
109    files += glob.glob(
110        os.path.join(clang_tidy_path, "..", "test", "clang-tidy", "checkers", "**"),
111        recursive=True,
112    )
113    files += glob.glob(
114        os.path.join(clang_tidy_path, "..", "docs", "clang-tidy", "checks", "*.rst")
115    )
116    files += glob.glob(
117        os.path.join(
118            clang_tidy_path, "..", "docs", "clang-tidy", "checks", "*", "*.rst"
119        ),
120        recursive=True,
121    )
122    return [filename for filename in files if os.path.isfile(filename)]
123
124
125# Adapts the module's CMakelist file. Returns 'True' if it could add a new
126# entry and 'False' if the entry already existed.
127def adapt_cmake(module_path: str, check_name_camel: str) -> bool:
128    filename = os.path.join(module_path, "CMakeLists.txt")
129    with io.open(filename, "r", encoding="utf8") as f:
130        lines = f.readlines()
131
132    cpp_file = check_name_camel + ".cpp"
133
134    # Figure out whether this check already exists.
135    for line in lines:
136        if line.strip() == cpp_file:
137            return False
138
139    print("Updating %s..." % filename)
140    with io.open(filename, "w", encoding="utf8") as f:
141        cpp_found = False
142        file_added = False
143        for line in lines:
144            cpp_line = line.strip().endswith(".cpp")
145            if (not file_added) and (cpp_line or cpp_found):
146                cpp_found = True
147                if (line.strip() > cpp_file) or (not cpp_line):
148                    f.write("  " + cpp_file + "\n")
149                    file_added = True
150            f.write(line)
151
152    return True
153
154
155# Modifies the module to include the new check.
156def adapt_module(
157    module_path: str, module: str, check_name: str, check_name_camel: str
158) -> None:
159    modulecpp = next(
160        iter(
161            filter(
162                lambda p: p.lower() == module.lower() + "tidymodule.cpp",
163                os.listdir(module_path),
164            )
165        )
166    )
167    filename = os.path.join(module_path, modulecpp)
168    with io.open(filename, "r", encoding="utf8") as f:
169        lines = f.readlines()
170
171    print("Updating %s..." % filename)
172    with io.open(filename, "w", encoding="utf8") as f:
173        header_added = False
174        header_found = False
175        check_added = False
176        check_decl = (
177            "    CheckFactories.registerCheck<"
178            + check_name_camel
179            + '>(\n        "'
180            + check_name
181            + '");\n'
182        )
183
184        for line in lines:
185            if not header_added:
186                match = re.search('#include "(.*)"', line)
187                if match:
188                    header_found = True
189                    if match.group(1) > check_name_camel:
190                        header_added = True
191                        f.write('#include "' + check_name_camel + '.h"\n')
192                elif header_found:
193                    header_added = True
194                    f.write('#include "' + check_name_camel + '.h"\n')
195
196            if not check_added:
197                if line.strip() == "}":
198                    check_added = True
199                    f.write(check_decl)
200                else:
201                    match = re.search("registerCheck<(.*)>", line)
202                    if match and match.group(1) > check_name_camel:
203                        check_added = True
204                        f.write(check_decl)
205            f.write(line)
206
207
208# Adds a release notes entry.
209def add_release_notes(
210    clang_tidy_path: str, old_check_name: str, new_check_name: str
211) -> None:
212    filename = os.path.normpath(
213        os.path.join(clang_tidy_path, "../docs/ReleaseNotes.rst")
214    )
215    with io.open(filename, "r", encoding="utf8") as f:
216        lines = f.readlines()
217
218    lineMatcher = re.compile("Renamed checks")
219    nextSectionMatcher = re.compile("Improvements to include-fixer")
220    checkMatcher = re.compile("- The '(.*)")
221
222    print("Updating %s..." % filename)
223    with io.open(filename, "w", encoding="utf8") as f:
224        note_added = False
225        header_found = False
226        add_note_here = False
227
228        for line in lines:
229            if not note_added:
230                match = lineMatcher.match(line)
231                match_next = nextSectionMatcher.match(line)
232                match_check = checkMatcher.match(line)
233                if match_check:
234                    last_check = match_check.group(1)
235                    if last_check > old_check_name:
236                        add_note_here = True
237
238                if match_next:
239                    add_note_here = True
240
241                if match:
242                    header_found = True
243                    f.write(line)
244                    continue
245
246                if line.startswith("^^^^"):
247                    f.write(line)
248                    continue
249
250                if header_found and add_note_here:
251                    if not line.startswith("^^^^"):
252                        f.write(
253                            """- The '%s' check was renamed to :doc:`%s
254  <clang-tidy/checks/%s/%s>`
255
256                    """
257                            % (
258                                old_check_name,
259                                new_check_name,
260                                new_check_name.split("-", 1)[0],
261                                "-".join(new_check_name.split("-")[1:]),
262                            )
263                        )
264                        note_added = True
265
266            f.write(line)
267
268
269def main() -> None:
270    parser = argparse.ArgumentParser(description="Rename clang-tidy check.")
271    parser.add_argument("old_check_name", type=str, help="Old check name.")
272    parser.add_argument("new_check_name", type=str, help="New check name.")
273    parser.add_argument(
274        "--check_class_name",
275        type=str,
276        help="Old name of the class implementing the check.",
277    )
278    args = parser.parse_args()
279
280    old_module = args.old_check_name.split("-")[0]
281    new_module = args.new_check_name.split("-")[0]
282    old_name = "-".join(args.old_check_name.split("-")[1:])
283    new_name = "-".join(args.new_check_name.split("-")[1:])
284
285    if args.check_class_name:
286        check_name_camel = args.check_class_name
287    else:
288        check_name_camel = (
289            "".join(map(lambda elem: elem.capitalize(), old_name.split("-"))) + "Check"
290        )
291
292    new_check_name_camel = (
293        "".join(map(lambda elem: elem.capitalize(), new_name.split("-"))) + "Check"
294    )
295
296    clang_tidy_path = os.path.dirname(__file__)
297
298    header_guard_variants = [
299        (args.old_check_name.replace("-", "_")).upper() + "_CHECK",
300        (old_module + "_" + check_name_camel).upper(),
301        (old_module + "_" + new_check_name_camel).upper(),
302        args.old_check_name.replace("-", "_").upper(),
303    ]
304    header_guard_new = (new_module + "_" + new_check_name_camel).upper()
305
306    old_module_path = os.path.join(clang_tidy_path, old_module)
307    new_module_path = os.path.join(clang_tidy_path, new_module)
308
309    if old_module != new_module:
310        # Remove the check from the old module.
311        cmake_lists = os.path.join(old_module_path, "CMakeLists.txt")
312        check_found = deleteMatchingLines(cmake_lists, "\\b" + check_name_camel)
313        if not check_found:
314            print(
315                "Check name '%s' not found in %s. Exiting."
316                % (check_name_camel, cmake_lists)
317            )
318            sys.exit(1)
319
320        modulecpp = next(
321            iter(
322                filter(
323                    lambda p: p.lower() == old_module.lower() + "tidymodule.cpp",
324                    os.listdir(old_module_path),
325                )
326            )
327        )
328        deleteMatchingLines(
329            os.path.join(old_module_path, modulecpp),
330            "\\b" + check_name_camel + "|\\b" + args.old_check_name,
331        )
332
333    for filename in getListOfFiles(clang_tidy_path):
334        originalName = filename
335        filename = fileRename(
336            filename, old_module + "/" + old_name, new_module + "/" + new_name
337        )
338        filename = fileRename(filename, args.old_check_name, args.new_check_name)
339        filename = fileRename(filename, check_name_camel, new_check_name_camel)
340        replaceInFile(
341            filename,
342            generateCommentLineHeader(originalName),
343            generateCommentLineHeader(filename),
344        )
345        replaceInFile(
346            filename,
347            generateCommentLineSource(originalName),
348            generateCommentLineSource(filename),
349        )
350        for header_guard in header_guard_variants:
351            replaceInFile(filename, header_guard, header_guard_new)
352
353        if new_module + "/" + new_name + ".rst" in filename:
354            replaceInFile(
355                filename,
356                args.old_check_name + "\n" + "=" * len(args.old_check_name) + "\n",
357                args.new_check_name + "\n" + "=" * len(args.new_check_name) + "\n",
358            )
359
360        replaceInFile(filename, args.old_check_name, args.new_check_name)
361        replaceInFile(
362            filename,
363            old_module + "::" + check_name_camel,
364            new_module + "::" + new_check_name_camel,
365        )
366        replaceInFile(
367            filename,
368            old_module + "/" + check_name_camel,
369            new_module + "/" + new_check_name_camel,
370        )
371        replaceInFile(
372            filename, old_module + "/" + old_name, new_module + "/" + new_name
373        )
374        replaceInFile(filename, check_name_camel, new_check_name_camel)
375
376    if old_module != new_module or new_module == "llvm":
377        if new_module == "llvm":
378            new_namespace = new_module + "_check"
379        else:
380            new_namespace = new_module
381        check_implementation_files = glob.glob(
382            os.path.join(old_module_path, new_check_name_camel + "*")
383        )
384        for filename in check_implementation_files:
385            # Move check implementation to the directory of the new module.
386            filename = fileRename(filename, old_module_path, new_module_path)
387            replaceInFileRegex(
388                filename,
389                "namespace clang::tidy::" + old_module + "[^ \n]*",
390                "namespace clang::tidy::" + new_namespace,
391            )
392
393    if old_module != new_module:
394
395        # Add check to the new module.
396        adapt_cmake(new_module_path, new_check_name_camel)
397        adapt_module(
398            new_module_path, new_module, args.new_check_name, new_check_name_camel
399        )
400
401    os.system(os.path.join(clang_tidy_path, "add_new_check.py") + " --update-docs")
402    add_release_notes(clang_tidy_path, args.old_check_name, args.new_check_name)
403
404
405if __name__ == "__main__":
406    main()
407