373 lines · python
1#!/usr/bin/env python32#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-exception8#9# ===-----------------------------------------------------------------------===#10 11import argparse12import glob13import io14import os15import re16import sys17from typing import List18 19 20def replaceInFileRegex(fileName: str, sFrom: str, sTo: str) -> None:21 if sFrom == sTo:22 return23 24 # The documentation files are encoded using UTF-8, however on Windows the25 # default encoding might be different (e.g. CP-1252). To make sure UTF-8 is26 # always used, use `io.open(filename, mode, encoding='utf8')` for reading and27 # writing files here and elsewhere.28 txt = None29 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 return41 txt = None42 with io.open(fileName, "r", encoding="utf8") as f:43 txt = f.read()44 45 if sFrom not in txt:46 return47 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 fileName57 newFileName = fileName.replace(sFrom, sTo)58 print("Renaming '%s' -> '%s'..." % (fileName, newFileName))59 os.rename(fileName, newFileName)60 return newFileName61 62 63def deleteMatchingLines(fileName: str, pattern: str) -> bool:64 lines = None65 with io.open(fileName, "r", encoding="utf8") as f:66 lines = f.readlines()67 68 not_matching_lines = [l for l in lines if not re.search(pattern, l)]69 if len(not_matching_lines) == len(lines):70 return False71 72 print("Removing lines matching '%s' in '%s'..." % (pattern, fileName))73 print(" " + " ".join([l for l in lines if re.search(pattern, l)]))74 with io.open(fileName, "w", encoding="utf8") as f:75 f.writelines(not_matching_lines)76 77 return True78 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.rst"))84 ]85 files += glob.glob(86 os.path.join(clang_tidy_path, "..", "test", "clang-tidy", "checkers", "**"),87 recursive=True,88 )89 files += glob.glob(90 os.path.join(clang_tidy_path, "..", "docs", "clang-tidy", "checks", "*.rst")91 )92 files += glob.glob(93 os.path.join(94 clang_tidy_path, "..", "docs", "clang-tidy", "checks", "*", "*.rst"95 ),96 recursive=True,97 )98 return [filename for filename in files if os.path.isfile(filename)]99 100 101# Adapts the module's CMakelist file. Returns 'True' if it could add a new102# entry and 'False' if the entry already existed.103def adapt_cmake(module_path: str, check_name_camel: str) -> bool:104 filename = os.path.join(module_path, "CMakeLists.txt")105 with io.open(filename, "r", encoding="utf8") as f:106 lines = f.readlines()107 108 cpp_file = check_name_camel + ".cpp"109 110 # Figure out whether this check already exists.111 for line in lines:112 if line.strip() == cpp_file:113 return False114 115 print("Updating %s..." % filename)116 with io.open(filename, "w", encoding="utf8") as f:117 cpp_found = False118 file_added = False119 for line in lines:120 cpp_line = line.strip().endswith(".cpp")121 if (not file_added) and (cpp_line or cpp_found):122 cpp_found = True123 if (line.strip() > cpp_file) or (not cpp_line):124 f.write(" " + cpp_file + "\n")125 file_added = True126 f.write(line)127 128 return True129 130 131# Modifies the module to include the new check.132def adapt_module(133 module_path: str, module: str, check_name: str, check_name_camel: str134) -> None:135 modulecpp = next(136 iter(137 filter(138 lambda p: p.lower() == module.lower() + "tidymodule.cpp",139 os.listdir(module_path),140 )141 )142 )143 filename = os.path.join(module_path, modulecpp)144 with io.open(filename, "r", encoding="utf8") as f:145 lines = f.readlines()146 147 print("Updating %s..." % filename)148 with io.open(filename, "w", encoding="utf8") as f:149 header_added = False150 header_found = False151 check_added = False152 check_decl = (153 " CheckFactories.registerCheck<"154 + check_name_camel155 + '>(\n "'156 + check_name157 + '");\n'158 )159 160 for line in lines:161 if not header_added:162 match = re.search('#include "(.*)"', line)163 if match:164 header_found = True165 if match.group(1) > check_name_camel:166 header_added = True167 f.write('#include "' + check_name_camel + '.h"\n')168 elif header_found:169 header_added = True170 f.write('#include "' + check_name_camel + '.h"\n')171 172 if not check_added:173 if line.strip() == "}":174 check_added = True175 f.write(check_decl)176 else:177 match = re.search("registerCheck<(.*)>", line)178 if match and match.group(1) > check_name_camel:179 check_added = True180 f.write(check_decl)181 f.write(line)182 183 184# Adds a release notes entry.185def add_release_notes(186 clang_tidy_path: str, old_check_name: str, new_check_name: str187) -> None:188 filename = os.path.normpath(189 os.path.join(clang_tidy_path, "../docs/ReleaseNotes.rst")190 )191 with io.open(filename, "r", encoding="utf8") as f:192 lines = f.readlines()193 194 lineMatcher = re.compile("Renamed checks")195 nextSectionMatcher = re.compile("Improvements to include-fixer")196 checkMatcher = re.compile("- The '(.*)")197 198 print("Updating %s..." % filename)199 with io.open(filename, "w", encoding="utf8") as f:200 note_added = False201 header_found = False202 add_note_here = False203 204 for line in lines:205 if not note_added:206 match = lineMatcher.match(line)207 match_next = nextSectionMatcher.match(line)208 match_check = checkMatcher.match(line)209 if match_check:210 last_check = match_check.group(1)211 if last_check > old_check_name:212 add_note_here = True213 214 if match_next:215 add_note_here = True216 217 if match:218 header_found = True219 f.write(line)220 continue221 222 if line.startswith("^^^^"):223 f.write(line)224 continue225 226 if header_found and add_note_here:227 if not line.startswith("^^^^"):228 f.write(229 """- The '%s' check was renamed to :doc:`%s230 <clang-tidy/checks/%s/%s>`231 232 """233 % (234 old_check_name,235 new_check_name,236 new_check_name.split("-", 1)[0],237 "-".join(new_check_name.split("-")[1:]),238 )239 )240 note_added = True241 242 f.write(line)243 244 245def main() -> None:246 parser = argparse.ArgumentParser(description="Rename clang-tidy check.")247 parser.add_argument("old_check_name", type=str, help="Old check name.")248 parser.add_argument("new_check_name", type=str, help="New check name.")249 parser.add_argument(250 "--check_class_name",251 type=str,252 help="Old name of the class implementing the check.",253 )254 args = parser.parse_args()255 256 old_module = args.old_check_name.split("-")[0]257 new_module = args.new_check_name.split("-")[0]258 old_name = "-".join(args.old_check_name.split("-")[1:])259 new_name = "-".join(args.new_check_name.split("-")[1:])260 261 if args.check_class_name:262 check_name_camel = args.check_class_name263 else:264 check_name_camel = (265 "".join(map(lambda elem: elem.capitalize(), old_name.split("-"))) + "Check"266 )267 268 new_check_name_camel = (269 "".join(map(lambda elem: elem.capitalize(), new_name.split("-"))) + "Check"270 )271 272 clang_tidy_path = os.path.dirname(__file__)273 274 header_guard_variants = [275 (args.old_check_name.replace("-", "_")).upper() + "_CHECK",276 (old_module + "_" + check_name_camel).upper(),277 (old_module + "_" + new_check_name_camel).upper(),278 args.old_check_name.replace("-", "_").upper(),279 ]280 header_guard_new = (new_module + "_" + new_check_name_camel).upper()281 282 old_module_path = os.path.join(clang_tidy_path, old_module)283 new_module_path = os.path.join(clang_tidy_path, new_module)284 285 if old_module != new_module:286 # Remove the check from the old module.287 cmake_lists = os.path.join(old_module_path, "CMakeLists.txt")288 check_found = deleteMatchingLines(cmake_lists, "\\b" + check_name_camel)289 if not check_found:290 print(291 "Check name '%s' not found in %s. Exiting."292 % (check_name_camel, cmake_lists)293 )294 sys.exit(1)295 296 modulecpp = next(297 iter(298 filter(299 lambda p: p.lower() == old_module.lower() + "tidymodule.cpp",300 os.listdir(old_module_path),301 )302 )303 )304 deleteMatchingLines(305 os.path.join(old_module_path, modulecpp),306 "\\b" + check_name_camel + "|\\b" + args.old_check_name,307 )308 309 for filename in getListOfFiles(clang_tidy_path):310 originalName = filename311 filename = fileRename(312 filename, old_module + "/" + old_name, new_module + "/" + new_name313 )314 filename = fileRename(filename, args.old_check_name, args.new_check_name)315 filename = fileRename(filename, check_name_camel, new_check_name_camel)316 for header_guard in header_guard_variants:317 replaceInFile(filename, header_guard, header_guard_new)318 319 if new_module + "/" + new_name + ".rst" in filename:320 replaceInFile(321 filename,322 args.old_check_name + "\n" + "=" * len(args.old_check_name) + "\n",323 args.new_check_name + "\n" + "=" * len(args.new_check_name) + "\n",324 )325 326 replaceInFile(filename, args.old_check_name, args.new_check_name)327 replaceInFile(328 filename,329 old_module + "::" + check_name_camel,330 new_module + "::" + new_check_name_camel,331 )332 replaceInFile(333 filename,334 old_module + "/" + check_name_camel,335 new_module + "/" + new_check_name_camel,336 )337 replaceInFile(338 filename, old_module + "/" + old_name, new_module + "/" + new_name339 )340 replaceInFile(filename, check_name_camel, new_check_name_camel)341 342 if old_module != new_module or new_module == "llvm":343 if new_module == "llvm":344 new_namespace = new_module + "_check"345 else:346 new_namespace = new_module347 check_implementation_files = glob.glob(348 os.path.join(old_module_path, new_check_name_camel + "*")349 )350 for filename in check_implementation_files:351 # Move check implementation to the directory of the new module.352 filename = fileRename(filename, old_module_path, new_module_path)353 replaceInFileRegex(354 filename,355 "namespace clang::tidy::" + old_module + "[^ \n]*",356 "namespace clang::tidy::" + new_namespace,357 )358 359 if old_module != new_module:360 361 # Add check to the new module.362 adapt_cmake(new_module_path, new_check_name_camel)363 adapt_module(364 new_module_path, new_module, args.new_check_name, new_check_name_camel365 )366 367 os.system(os.path.join(clang_tidy_path, "add_new_check.py") + " --update-docs")368 add_release_notes(clang_tidy_path, args.old_check_name, args.new_check_name)369 370 371if __name__ == "__main__":372 main()373