clang-tools 24.0.0git
rename_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 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 fileRename(fileName: str, sFrom: str, sTo: str) -> str:
55 if sFrom not in fileName or sFrom == sTo:
56 return fileName
57 newFileName = fileName.replace(sFrom, sTo)
58 print("Renaming '%s' -> '%s'..." % (fileName, newFileName))
59 os.rename(fileName, newFileName)
60 return newFileName
61
62
63def deleteMatchingLines(fileName: str, pattern: str) -> bool:
64 lines = None
65 with io.open(fileName, "r", encoding="utf8") as f:
66 lines = f.readlines()
67
68 not_matching_lines = [line for line in lines if not re.search(pattern, line)]
69 if len(not_matching_lines) == len(lines):
70 return False
71
72 print("Removing lines matching '%s' in '%s'..." % (pattern, fileName))
73 print(" " + " ".join(line for line in lines if re.search(pattern, line)))
74 with io.open(fileName, "w", encoding="utf8") as f:
75 f.writelines(not_matching_lines)
76
77 return True
78
79
80def getListOfFiles(clang_tidy_path: str) -> List[str]:
81 files = glob.glob(os.path.join(clang_tidy_path, "**"), recursive=True)
82 files += [
83 os.path.normpath(os.path.join(clang_tidy_path, "../docs/ReleaseNotes.md"))
84 ]
85 files += glob.glob(
86 os.path.join(clang_tidy_path, "..", "test", "clang-tidy", "checkers", "**"),
87 recursive=True,
88 )
89 docs_path = os.path.join(clang_tidy_path, "..", "docs", "clang-tidy", "checks")
90 # TODO: Stop discovering reST files once all clang-tidy check
91 # documentation has been migrated to MyST.
92 for extension in (".md", ".rst"):
93 files += glob.glob(os.path.join(docs_path, f"*{extension}"))
94 files += glob.glob(os.path.join(docs_path, "*", f"*{extension}"))
95 return [filename for filename in files if os.path.isfile(filename)]
96
97
98# Adapts the module's CMakelist file. Returns 'True' if it could add a new
99# entry and 'False' if the entry already existed.
100def adapt_cmake(module_path: str, check_name_camel: str) -> bool:
101 filename = os.path.join(module_path, "CMakeLists.txt")
102 with io.open(filename, "r", encoding="utf8") as f:
103 lines = f.readlines()
104
105 cpp_file = check_name_camel + ".cpp"
106
107 # Figure out whether this check already exists.
108 for line in lines:
109 if line.strip() == cpp_file:
110 return False
111
112 print("Updating %s..." % filename)
113 with io.open(filename, "w", encoding="utf8") as f:
114 cpp_found = False
115 file_added = False
116 for line in lines:
117 cpp_line = line.strip().endswith(".cpp")
118 if (not file_added) and (cpp_line or cpp_found):
119 cpp_found = True
120 if (line.strip() > cpp_file) or (not cpp_line):
121 f.write(" " + cpp_file + "\n")
122 file_added = True
123 f.write(line)
124
125 return True
126
127
128# Modifies the module to include the new check.
130 module_path: str, module: str, check_name: str, check_name_camel: str
131) -> None:
132 modulecpp = next(
133 iter(
134 filter(
135 lambda p: p.lower() == module.lower() + "tidymodule.cpp",
136 os.listdir(module_path),
137 )
138 )
139 )
140 filename = os.path.join(module_path, modulecpp)
141 with io.open(filename, "r", encoding="utf8") as f:
142 lines = f.readlines()
143
144 print("Updating %s..." % filename)
145 with io.open(filename, "w", encoding="utf8") as f:
146 header_added = False
147 header_found = False
148 check_added = False
149 check_decl = (
150 " CheckFactories.registerCheck<"
151 + check_name_camel
152 + '>(\n "'
153 + check_name
154 + '");\n'
155 )
156
157 for line in lines:
158 if not header_added:
159 match = re.search('#include "(.*)"', line)
160 if match:
161 header_found = True
162 if match.group(1) > check_name_camel:
163 header_added = True
164 f.write('#include "' + check_name_camel + '.h"\n')
165 elif header_found:
166 header_added = True
167 f.write('#include "' + check_name_camel + '.h"\n')
168
169 if not check_added:
170 if line.strip() == "}":
171 check_added = True
172 f.write(check_decl)
173 else:
174 match = re.search("registerCheck<(.*)>", line)
175 if match and match.group(1) > check_name_camel:
176 check_added = True
177 f.write(check_decl)
178 f.write(line)
179
180
181# Adds a release notes entry.
183 clang_tidy_path: str, old_check_name: str, new_check_name: str
184) -> None:
185 filename = os.path.normpath(
186 os.path.join(clang_tidy_path, "../docs/ReleaseNotes.md")
187 )
188 with io.open(filename, "r", encoding="utf8") as f:
189 lines = f.readlines()
190
191 lineMatcher = re.compile(r"#### Renamed checks")
192 nextSectionMatcher = re.compile(r"### Improvements to include-fixer")
193 checkMatcher = re.compile("- The '(.*)")
194
195 print("Updating %s..." % filename)
196 with io.open(filename, "w", encoding="utf8") as f:
197 note_added = False
198 header_found = False
199 add_note_here = False
200
201 for line in lines:
202 if not note_added:
203 match = lineMatcher.match(line)
204 match_next = nextSectionMatcher.match(line)
205 match_check = checkMatcher.match(line)
206 if match_check:
207 last_check = match_check.group(1)
208 if last_check > old_check_name:
209 add_note_here = True
210
211 if match_next:
212 add_note_here = True
213
214 # When inside the Renamed checks section and we reach any
215 # heading, insert before it (handles empty sections).
216 if header_found and line.startswith("#"):
217 add_note_here = True
218
219 if match:
220 header_found = True
221 f.write(line)
222 continue
223
224 if header_found and add_note_here:
225 f.write(
226 "- The '%s' check was renamed to {doc}`%s\n"
227 " <clang-tidy/checks/%s/%s>`\n"
228 "\n"
229 % (
230 old_check_name,
231 new_check_name,
232 new_check_name.split("-", 1)[0],
233 "-".join(new_check_name.split("-")[1:]),
234 )
235 )
236 note_added = True
237
238 f.write(line)
239
240
241def main() -> None:
242 parser = argparse.ArgumentParser(description="Rename clang-tidy check.")
243 parser.add_argument("old_check_name", type=str, help="Old check name.")
244 parser.add_argument("new_check_name", type=str, help="New check name.")
245 parser.add_argument(
246 "--check_class_name",
247 type=str,
248 help="Old name of the class implementing the check.",
249 )
250 args = parser.parse_args()
251
252 old_module = args.old_check_name.split("-")[0]
253 new_module = args.new_check_name.split("-")[0]
254 old_name = "-".join(args.old_check_name.split("-")[1:])
255 new_name = "-".join(args.new_check_name.split("-")[1:])
256
257 if args.check_class_name:
258 check_name_camel = args.check_class_name
259 else:
260 check_name_camel = (
261 "".join(map(lambda elem: elem.capitalize(), old_name.split("-"))) + "Check"
262 )
263
264 new_check_name_camel = (
265 "".join(map(lambda elem: elem.capitalize(), new_name.split("-"))) + "Check"
266 )
267
268 clang_tidy_path = os.path.dirname(__file__)
269
270 header_guard_variants = [
271 (args.old_check_name.replace("-", "_")).upper() + "_CHECK",
272 (old_module + "_" + check_name_camel).upper(),
273 (old_module + "_" + new_check_name_camel).upper(),
274 args.old_check_name.replace("-", "_").upper(),
275 ]
276 header_guard_new = (new_module + "_" + new_check_name_camel).upper()
277
278 old_module_path = os.path.join(clang_tidy_path, old_module)
279 new_module_path = os.path.join(clang_tidy_path, new_module)
280
281 if old_module != new_module:
282 # Remove the check from the old module.
283 cmake_lists = os.path.join(old_module_path, "CMakeLists.txt")
284 check_found = deleteMatchingLines(cmake_lists, "\\b" + check_name_camel)
285 if not check_found:
286 print(
287 "Check name '%s' not found in %s. Exiting."
288 % (check_name_camel, cmake_lists)
289 )
290 sys.exit(1)
291
292 modulecpp = next(
293 iter(
294 filter(
295 lambda p: p.lower() == old_module.lower() + "tidymodule.cpp",
296 os.listdir(old_module_path),
297 )
298 )
299 )
301 os.path.join(old_module_path, modulecpp),
302 "\\b" + check_name_camel + "|\\b" + args.old_check_name,
303 )
304
305 for filename in getListOfFiles(clang_tidy_path):
306 filename = fileRename(
307 filename, old_module + "/" + old_name, new_module + "/" + new_name
308 )
309 filename = fileRename(filename, args.old_check_name, args.new_check_name)
310 filename = fileRename(filename, check_name_camel, new_check_name_camel)
311 for header_guard in header_guard_variants:
312 replaceInFile(filename, header_guard, header_guard_new)
313
314 # TODO: Remove the reST heading handling once all clang-tidy check
315 # documentation has been migrated to MyST.
316 if new_module + "/" + new_name + ".rst" in filename:
318 filename,
319 args.old_check_name + "\n" + "=" * len(args.old_check_name) + "\n",
320 args.new_check_name + "\n" + "=" * len(args.new_check_name) + "\n",
321 )
322
323 replaceInFile(filename, args.old_check_name, args.new_check_name)
325 filename,
326 old_module + "::" + check_name_camel,
327 new_module + "::" + new_check_name_camel,
328 )
330 filename,
331 old_module + "/" + check_name_camel,
332 new_module + "/" + new_check_name_camel,
333 )
335 filename, old_module + "/" + old_name, new_module + "/" + new_name
336 )
337 replaceInFile(filename, check_name_camel, new_check_name_camel)
338
339 if old_module != new_module or new_module == "llvm":
340 if new_module == "llvm":
341 new_namespace = new_module + "_check"
342 else:
343 new_namespace = new_module
344 check_implementation_files = glob.glob(
345 os.path.join(old_module_path, new_check_name_camel + "*")
346 )
347 for filename in check_implementation_files:
348 # Move check implementation to the directory of the new module.
349 filename = fileRename(filename, old_module_path, new_module_path)
351 filename,
352 "namespace clang::tidy::" + old_module + "[^ \n]*",
353 "namespace clang::tidy::" + new_namespace,
354 )
355
356 if old_module != new_module:
357
358 # Add check to the new module.
359 adapt_cmake(new_module_path, new_check_name_camel)
361 new_module_path, new_module, args.new_check_name, new_check_name_camel
362 )
363
364 os.system(os.path.join(clang_tidy_path, "add_new_check.py") + " --update-docs")
365 add_release_notes(clang_tidy_path, args.old_check_name, args.new_check_name)
366
367
368if __name__ == "__main__":
369 main()
List[str] getListOfFiles(str clang_tidy_path)
None adapt_module(str module_path, str module, str check_name, str check_name_camel)
str fileRename(str fileName, str sFrom, str sTo)
None replaceInFileRegex(str fileName, str sFrom, str sTo)
None add_release_notes(str clang_tidy_path, str old_check_name, str new_check_name)
bool adapt_cmake(str module_path, str check_name_camel)
bool deleteMatchingLines(str fileName, str pattern)
None replaceInFile(str fileName, str sFrom, str sTo)