diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt --- a/clang-tools-extra/clangd/CMakeLists.txt +++ b/clang-tools-extra/clangd/CMakeLists.txt @@ -139,6 +139,7 @@ add_subdirectory(tool) add_subdirectory(indexer) add_subdirectory(index/dex/dexp) +add_subdirectory(eval-rename) if (LLVM_INCLUDE_BENCHMARKS) add_subdirectory(benchmarks) diff --git a/clang-tools-extra/clangd/eval-rename/CMakeLists.txt b/clang-tools-extra/clangd/eval-rename/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/eval-rename/CMakeLists.txt @@ -0,0 +1,23 @@ +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../) + +set(LLVM_LINK_COMPONENTS + Support + ) + +add_clang_tool(clangd-rename + RenameMain.cpp + ) + +clang_target_link_libraries(clangd-rename + PRIVATE + clangAST + clangBasic + clangFrontend + clangIndex + clangLex + clangTooling +) +target_link_libraries(clangd-rename + PRIVATE + clangDaemon +) diff --git a/clang-tools-extra/clangd/eval-rename/RenameMain.cpp b/clang-tools-extra/clangd/eval-rename/RenameMain.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/eval-rename/RenameMain.cpp @@ -0,0 +1,208 @@ +//===--- RenameMain.cpp ------------------------------------------*- C++-*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "CompileCommands.h" +#include "Compiler.h" +#include "FS.h" +#include "FSProvider.h" +#include "FindTarget.h" +#include "GlobalCompilationDatabase.h" +#include "Logger.h" +#include "ParsedAST.h" +#include "SourceCode.h" +#include "index/MemIndex.h" +#include "index/Serialization.h" +#include "refactor/Rename.h" +#include "clang/AST/Decl.h" +#include "clang/Rewrite/Core/Rewriter.h" +#include "clang/Tooling/ArgumentsAdjusters.h" +#include "clang/Tooling/CommonOptionsParser.h" +#include "clang/Tooling/ReplacementsYaml.h" +#include "llvm/ADT/STLExtras.h" + +static llvm::cl::opt + IndexPath("index-path", llvm::cl::desc("Path to the index file"), + llvm::cl::init("")); +static llvm::cl::opt + RenameSymbol("rename-symbol", + llvm::cl::desc("Qualified symbol name to rename"), + llvm::cl::Required); +static llvm::cl::opt + Fix("fix", llvm::cl::desc("Apply the rename edits to disk files"), + llvm::cl::init(false)); + +namespace clang { +namespace clangd { +namespace { + +llvm::Expected getAbsolutePath(StringRef File, + const FileSystemProvider &FS) { + SmallString<1024> AbsolutePath = File; + if (auto EC = FS.getFileSystem()->makeAbsolute(AbsolutePath)) + return llvm::errorCodeToError(EC); + return clangd::removeDots(AbsolutePath.str()); +} + +class RenameExecutor { +public: + explicit RenameExecutor(llvm::StringRef SymbolOnRename, + llvm::StringRef FileOnRename, + const FileSystemProvider &FSProvider, + const GlobalCompilationDatabase &CDB, + std::unique_ptr Index) + : QualifiedName(SymbolOnRename), FileName(FileOnRename), + FSProvider(FSProvider), CDB(CDB), Index(std::move(Index)) {} + + ParsedAST buildAST() { + ParseInputs PInputs; + PInputs.CompileCommand = *CDB.getCompileCommand(FileName); + auto Buffer = FSProvider.getFileSystem()->getBufferForFile(FileName); + llvm::StringRef Content = Buffer->get()->getBuffer(); + PInputs.Contents = Content.str(); + PInputs.FS = FSProvider.getFileSystem(); + IgnoreDiagnostics Diags; + + auto CI = buildCompilerInvocation(PInputs, Diags); + assert(CI && "Failed to build compilation invocation."); + auto Preamble = + buildPreamble(FileName, *CI, + /*OldPreamble=*/nullptr, + /*OldCompileCommand=*/PInputs.CompileCommand, PInputs, + /*StoreInMemory=*/true, /*PreambleCallback=*/nullptr); + auto AST = ::clang::clangd::buildAST(FileName, std::move(CI), {}, PInputs, + Preamble); + assert(AST && "Failed to build AST."); + return std::move(*AST); + } + + llvm::Expected execute(ParsedAST &AST) { + const auto *RenameDecl = findRenameDecl(AST, QualifiedName); + auto &SM = AST.getSourceManager(); + assert(RenameDecl); + + auto Pos = + clang::clangd::sourceLocToPosition(SM, RenameDecl->getLocation()); + RenameOptions RenameOpts; + RenameOpts.AllowCrossFile = true; + RenameOpts.LimitFiles = 0; // unlimited. + RenameOpts.WantFormat = true; + clang::clangd::RenameInputs Inputs{Pos, "_test_rename_", AST, + FileName, Index.get(), RenameOpts}; + return clang::clangd::rename(Inputs); + } + + const clang::NamedDecl *findRenameDecl(const clang::clangd::ParsedAST &AST, + llvm::StringRef QName) { + const NamedDecl *TargetDecl = nullptr; + findExplicitReferences(AST.getASTContext(), [&](ReferenceLoc Loc) { + if (!Loc.IsDecl) + return; + if (TargetDecl) + return; + for (const auto *D : Loc.Targets) { + if (const auto *ND = llvm::dyn_cast(D)) { + if (ND->getQualifiedNameAsString() == QName) { + TargetDecl = ND; + break; + } + } + } + }); + return TargetDecl; + } + +private: + llvm::StringRef QualifiedName; + llvm::StringRef FileName; + const FileSystemProvider &FSProvider; + const GlobalCompilationDatabase &CDB; + std::unique_ptr Index; +}; + +std::unique_ptr openIndex(llvm::StringRef IndexPath) { + if (IndexPath.empty()) // create an empty one. + return clang::clangd::MemIndex::build({}, {}, {}); + auto S = loadIndex(IndexPath, /*UseDex=*/true); + return S; +} + +} // namespace +} // namespace clangd +} // namespace clang + +int main(int Argc, const char **Argv) { + clang::tooling::CommonOptionsParser OP(Argc, Argv, llvm::cl::GeneralCategory); + assert(OP.getSourcePathList().size() == 1 && "Expect exactly one file path."); + clang::clangd::StreamLogger Logger(llvm::errs(), + clang::clangd::Logger::Error); + clang::clangd::LoggingSession LoggingSession(Logger); + + clang::clangd::RealFileSystemProvider FSProvider; + clang::clangd::DirectoryBasedGlobalCompilationDatabase DCDB(llvm::None); + // inject the standard resource directory. + auto Mangler = clang::clangd::CommandMangler::detect(); + clang::clangd::OverlayCDB OCDB(&DCDB, {}, + clang::tooling::ArgumentsAdjuster(Mangler)); + + auto AbsFile = + clang::clangd::getAbsolutePath(OP.getSourcePathList()[0], FSProvider); + if (!AbsFile) { + clang::clangd::elog("Error on getting an absolute path {0}: {1}", + OP.getSourcePathList()[0], AbsFile.takeError()); + return 1; + } + clang::clangd::RenameExecutor RExecutor(RenameSymbol, *AbsFile, FSProvider, + OCDB, + clang::clangd::openIndex(IndexPath)); + clang::clangd::ParsedAST AST = RExecutor.buildAST(); + auto FatalDiag = + llvm::find_if(AST.getDiagnostics(), [](const clang::clangd::Diag &D) { + return D.Severity != clang::DiagnosticsEngine::Fatal; + }); + if (FatalDiag != AST.getDiagnostics().end()) { + clang::clangd::elog("source file {0} is illformed: {1}", + *AbsFile, + FatalDiag->Message); + return 1; + } + + auto RenameResults = RExecutor.execute(AST); + if (!RenameResults) { + clang::clangd::elog("Failed to rename: {0}", RenameResults.takeError()); + return 1; + } + + if (Fix) { + auto &SM = AST.getSourceManager(); + clang::Rewriter Rewriter(SM, AST.getLangOpts()); + bool Succeed = true; + for (const auto &R : *RenameResults) { + bool Success = clang::tooling::applyAllReplacements( + R.getValue().Replacements, Rewriter); + // Succeed &= Success; + if (!Success) { + clang::clangd::elog("Failed to apply replacements for file: {0}", + R.first()); + Succeed = false; + continue; + } + } + Rewriter.overwriteChangedFiles(); + return Succeed ? 0 : 1; + } + // Emit the serialized rename edit (YAML format) to the stdout. + llvm::yaml::Output YAML(llvm::outs()); + for (auto &RenameEditForFile : *RenameResults) { + clang::tooling::TranslationUnitReplacements Output; + Output.MainSourceFile = RenameEditForFile.first().str(); + Output.Replacements = {RenameEditForFile.second.Replacements.begin(), + RenameEditForFile.second.Replacements.end()}; + YAML << Output; + } + return 0; +} diff --git a/clang-tools-extra/clangd/eval-rename/eval-rename.py b/clang-tools-extra/clangd/eval-rename/eval-rename.py new file mode 100755 --- /dev/null +++ b/clang-tools-extra/clangd/eval-rename/eval-rename.py @@ -0,0 +1,88 @@ +#!/usr/bin/python +""" +A script to perform cross-file rename and evalute the results. + +Usage: + +$ cd /llvm-project +$ ninja -C build clangd-indexer +$ ./build/bin/clangd-indexer -format=binary -executor=all-TUs . > llvm-index.idx +$ ninja -C build clangd-rename +$ clang-tools-extra/clangd/eval-rename/eval-rename.py --index=llvm-index.idx --file=clang-tools-extra/clangd/eval-rename/symbol_to_rename.txt +""" + +from __future__ import print_function + +import argparse +import os +import re +import subprocess +import sys +import tempfile + +RENAME_EXECUTOR='build/bin/clangd-rename' + +def Run(cmd): + s = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print('>', ' '.join(cmd)) + [stdout, stderr] = s.communicate() + return (s.returncode, stdout, stderr) + +if __name__ == '__main__': + if not os.getcwd().endswith('llvm-project'): + sys.exit('The tool must be run from llvm-project/ root.') + + ap = argparse.ArgumentParser() + ap.add_argument( + '-f', + '--file', + required=True, + help='A file containing rename symbols to be evaluted.' + ) + ap.add_argument( + '-idx', + '--index', + required=True, + help='A path to the index file' + ) + args = ap.parse_args() + + with open(args.file) as f: + out = f.read() + test_cases = [line.strip().split() for line in out.split('\n') \ + if line and not line.startswith('#') and line.strip()] + + log_output_dir = tempfile.mkdtemp(prefix='eval-rename_') + success_cnt = 0 + for file_name, rename_symbol, verify_target in test_cases: + Run(['git', 'reset', '--hard']) + execute_rename_cmd = [ + RENAME_EXECUTOR, + '--index-path=%s' % args.index, + '--rename-symbol=%s' % rename_symbol, + '-fix', + file_name, + ] + rename_results = Run(execute_rename_cmd) + if rename_results[0] != 0: + log_file = open(os.path.join( + log_output_dir, rename_symbol.replace("::", "_") + '_RenameFailed'), 'w') + log_file.write(rename_results[1]) # stdout + log_file.write(rename_results[2]) # stderr + log_file.close() + continue + + build_results = Run(['ninja', '-C', 'build', verify_target]) + if build_results[0] != 0: + print('failed on renaming %s' % rename_symbol) + log_file = open(os.path.join( + log_output_dir, rename_symbol.replace("::", "_") + '_BuildFailed'), 'w') + log_file.write(build_results[1]) # stdout + log_file.write(build_results[2]) # stderr + log_file.close() + continue + print('succeed on renaming %s' % rename_symbol) + success_cnt += 1 + Run(['git', 'reset', '--hard']) + print('Evaluated rename on %d symbols, %d symbol succeeded, go %s for failure logs' % + (len(test_cases), success_cnt, log_output_dir)) diff --git a/clang-tools-extra/clangd/eval-rename/symbol_to_rename.txt b/clang-tools-extra/clangd/eval-rename/symbol_to_rename.txt new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/eval-rename/symbol_to_rename.txt @@ -0,0 +1,16 @@ +# normal functions +clang-tools-extra/clangd/XRefs.h clang::clangd::findReferences clangd +# normal class methods +clang-tools-extra/clangd/index/Index.h clang::clangd::SwapIndex::reset clangd +# normal classes +clang-tools-extra/clangd/index/Index.h clang::clangd::SymbolIndex clangd +clang-tools-extra/clangd/ClangdServer.h clang::clangd::ClangdServer clangd +clang-tools-extra/clangd/GlobalCompilationDatabase.h clang::clangd::GlobalCompilationDatabase clangd +clang-tools-extra/clangd/GlobalCompilationDatabase.h clang::clangd::OverlayCDB clangd +clang-tools-extra/clangd/Protocol.h clang::clangd::CodeAction clangd +# rename enum +clang-tools-extra/clangd/index/Ref.h clang::clangd::RefKind clangd +# rename enum constants +clang-tools-extra/clangd/FindTarget.h clang::clangd::DeclRelation::Alias clangd +# class with template constructors +clang-tools-extra/clangd/index/MemIndex.h clang::clangd::MemIndex clangd \ No newline at end of file