#!/usr/bin/env python3 # #===- rename_check.py - clang-tidy check renamer ------------*- python -*--===# # # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # #===-----------------------------------------------------------------------===# from __future__ import unicode_literals import argparse import glob import io import os import re def replaceInFileRegex(fileName, sFrom, sTo): if sFrom == sTo: return # The documentation files are encoded using UTF-8, however on Windows the # default encoding might be different (e.g. CP-1252). To make sure UTF-8 is # always used, use `io.open(filename, mode, encoding='utf8')` for reading and # writing files here and elsewhere. txt = None with io.open(fileName, 'r', encoding='utf8') as f: txt = f.read() txt = re.sub(sFrom, sTo, txt) print("Replacing '%s' -> '%s' in '%s'..." % (sFrom, sTo, fileName)) with io.open(fileName, 'w', encoding='utf8') as f: f.write(txt) def replaceInFile(fileName, sFrom, sTo): if sFrom == sTo: return txt = None with io.open(fileName, 'r', encoding='utf8') as f: txt = f.read() if sFrom not in txt: return txt = txt.replace(sFrom, sTo) print("Replacing '%s' -> '%s' in '%s'..." % (sFrom, sTo, fileName)) with io.open(fileName, 'w', encoding='utf8') as f: f.write(txt) def generateCommentLineHeader(filename): return ''.join(['//===--- ', os.path.basename(filename), ' - clang-tidy ', '-' * max(0, 42 - len(os.path.basename(filename))), '*- C++ -*-===//']) def generateCommentLineSource(filename): return ''.join(['//===--- ', os.path.basename(filename), ' - clang-tidy', '-' * max(0, 52 - len(os.path.basename(filename))), '-===//']) def fileRename(fileName, sFrom, sTo): if sFrom not in fileName or sFrom == sTo: return fileName newFileName = fileName.replace(sFrom, sTo) print("Renaming '%s' -> '%s'..." % (fileName, newFileName)) os.rename(fileName, newFileName) return newFileName def deleteMatchingLines(fileName, pattern): lines = None with io.open(fileName, 'r', encoding='utf8') as f: lines = f.readlines() not_matching_lines = [l for l in lines if not re.search(pattern, l)] if len(not_matching_lines) == len(lines): return False print("Removing lines matching '%s' in '%s'..." % (pattern, fileName)) print(' ' + ' '.join([l for l in lines if re.search(pattern, l)])) with io.open(fileName, 'w', encoding='utf8') as f: f.writelines(not_matching_lines) return True def getListOfFiles(clang_tidy_path): files = glob.glob(os.path.join(clang_tidy_path, '**'), recursive=True) files += [os.path.normpath(os.path.join(clang_tidy_path, '../docs/ReleaseNotes.rst'))] files += glob.glob(os.path.join(clang_tidy_path, '..', 'test', 'clang-tidy', 'checkers', '**'), recursive=True) files += glob.glob(os.path.join(clang_tidy_path, '..', 'docs', 'clang-tidy', 'checks', '*.rst')) files += glob.glob(os.path.join(clang_tidy_path, '..', 'docs', 'clang-tidy', 'checks', "*", "*.rst"), recursive=True) return [filename for filename in files if os.path.isfile(filename)] # Adapts the module's CMakelist file. Returns 'True' if it could add a new # entry and 'False' if the entry already existed. def adapt_cmake(module_path, check_name_camel): filename = os.path.join(module_path, 'CMakeLists.txt') with io.open(filename, 'r', encoding='utf8') as f: lines = f.readlines() cpp_file = check_name_camel + '.cpp' # Figure out whether this check already exists. for line in lines: if line.strip() == cpp_file: return False print('Updating %s...' % filename) with io.open(filename, 'w', encoding='utf8') as f: cpp_found = False file_added = False for line in lines: cpp_line = line.strip().endswith('.cpp') if (not file_added) and (cpp_line or cpp_found): cpp_found = True if (line.strip() > cpp_file) or (not cpp_line): f.write(' ' + cpp_file + '\n') file_added = True f.write(line) return True # Modifies the module to include the new check. def adapt_module(module_path, module, check_name, check_name_camel): modulecpp = next(iter(filter( lambda p: p.lower() == module.lower() + 'tidymodule.cpp', os.listdir(module_path)))) filename = os.path.join(module_path, modulecpp) with io.open(filename, 'r', encoding='utf8') as f: lines = f.readlines() print('Updating %s...' % filename) with io.open(filename, 'w', encoding='utf8') as f: header_added = False header_found = False check_added = False check_decl = (' CheckFactories.registerCheck<' + check_name_camel + '>(\n "' + check_name + '");\n') for line in lines: if not header_added: match = re.search('#include "(.*)"', line) if match: header_found = True if match.group(1) > check_name_camel: header_added = True f.write('#include "' + check_name_camel + '.h"\n') elif header_found: header_added = True f.write('#include "' + check_name_camel + '.h"\n') if not check_added: if line.strip() == '}': check_added = True f.write(check_decl) else: match = re.search('registerCheck<(.*)>', line) if match and match.group(1) > check_name_camel: check_added = True f.write(check_decl) f.write(line) # Adds a release notes entry. def add_release_notes(clang_tidy_path, old_check_name, new_check_name): filename = os.path.normpath(os.path.join(clang_tidy_path, '../docs/ReleaseNotes.rst')) with io.open(filename, 'r', encoding='utf8') as f: lines = f.readlines() lineMatcher = re.compile('Renamed checks') nextSectionMatcher = re.compile('Improvements to include-fixer') checkMatcher = re.compile('- The \'(.*)') print('Updating %s...' % filename) with io.open(filename, 'w', encoding='utf8') as f: note_added = False header_found = False add_note_here = False for line in lines: if not note_added: match = lineMatcher.match(line) match_next = nextSectionMatcher.match(line) match_check = checkMatcher.match(line) if match_check: last_check = match_check.group(1) if last_check > old_check_name: add_note_here = True if match_next: add_note_here = True if match: header_found = True f.write(line) continue if line.startswith('^^^^'): f.write(line) continue if header_found and add_note_here: if not line.startswith('^^^^'): f.write("""- The '%s' check was renamed to :doc:`%s ` """ % (old_check_name, new_check_name, new_check_name.split('-', 1)[0], '-'.join(new_check_name.split('-')[1:]))) note_added = True f.write(line) def main(): parser = argparse.ArgumentParser(description='Rename clang-tidy check.') parser.add_argument('old_check_name', type=str, help='Old check name.') parser.add_argument('new_check_name', type=str, help='New check name.') parser.add_argument('--check_class_name', type=str, help='Old name of the class implementing the check.') args = parser.parse_args() old_module = args.old_check_name.split('-')[0] new_module = args.new_check_name.split('-')[0] old_name = '-'.join(args.old_check_name.split('-')[1:]) new_name = '-'.join(args.new_check_name.split('-')[1:]) if args.check_class_name: check_name_camel = args.check_class_name else: check_name_camel = (''.join(map(lambda elem: elem.capitalize(), old_name.split('-'))) + 'Check') new_check_name_camel = (''.join(map(lambda elem: elem.capitalize(), new_name.split('-'))) + 'Check') clang_tidy_path = os.path.dirname(__file__) header_guard_variants = [ (args.old_check_name.replace('-', '_')).upper() + '_CHECK', (old_module + '_' + check_name_camel).upper(), (old_module + '_' + new_check_name_camel).upper(), args.old_check_name.replace('-', '_').upper()] header_guard_new = (new_module + '_' + new_check_name_camel).upper() old_module_path = os.path.join(clang_tidy_path, old_module) new_module_path = os.path.join(clang_tidy_path, new_module) if (old_module != new_module): # Remove the check from the old module. cmake_lists = os.path.join(old_module_path, 'CMakeLists.txt') check_found = deleteMatchingLines(cmake_lists, '\\b' + check_name_camel) if not check_found: print("Check name '%s' not found in %s. Exiting." % (check_name_camel, cmake_lists)) return 1 modulecpp = next(iter(filter( lambda p: p.lower() == old_module.lower() + 'tidymodule.cpp', os.listdir(old_module_path)))) deleteMatchingLines(os.path.join(old_module_path, modulecpp), '\\b' + check_name_camel + '|\\b' + args.old_check_name) for filename in getListOfFiles(clang_tidy_path): originalName = filename filename = fileRename(filename, old_module + "/" + old_name, new_module + "/" + new_name) filename = fileRename(filename, args.old_check_name, args.new_check_name) filename = fileRename(filename, check_name_camel, new_check_name_camel) replaceInFile(filename, generateCommentLineHeader(originalName), generateCommentLineHeader(filename)) replaceInFile(filename, generateCommentLineSource(originalName), generateCommentLineSource(filename)) for header_guard in header_guard_variants: replaceInFile(filename, header_guard, header_guard_new) if new_module + '/'+ new_name + '.rst' in filename: replaceInFile( filename, args.old_check_name + '\n' + '=' * len(args.old_check_name) + '\n', args.new_check_name + '\n' + '=' * len(args.new_check_name) + '\n') replaceInFile(filename, args.old_check_name, args.new_check_name) replaceInFile(filename, old_module + '::' + check_name_camel, new_module + '::' + new_check_name_camel) replaceInFile(filename, old_module + '/' + check_name_camel, new_module + '/' + new_check_name_camel) replaceInFile(filename, old_module + '/' + old_name, new_module + '/' + new_name) replaceInFile(filename, check_name_camel, new_check_name_camel) if old_module != new_module or new_module == 'llvm': if new_module == 'llvm': new_namespace = new_module + '_check' else: new_namespace = new_module check_implementation_files = glob.glob( os.path.join(old_module_path, new_check_name_camel + '*')) for filename in check_implementation_files: # Move check implementation to the directory of the new module. filename = fileRename(filename, old_module_path, new_module_path) replaceInFileRegex(filename, 'namespace clang::tidy::' + old_module + '[^ \n]*', 'namespace clang::tidy::' + new_namespace) if old_module != new_module: # Add check to the new module. adapt_cmake(new_module_path, new_check_name_camel) adapt_module(new_module_path, new_module, args.new_check_name, new_check_name_camel) os.system(os.path.join(clang_tidy_path, 'add_new_check.py') + ' --update-docs') add_release_notes(clang_tidy_path, args.old_check_name, args.new_check_name) if __name__ == '__main__': main()