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