275 lines · cpp
1//===-- ApplyReplacements.cpp - Apply and deduplicate replacements --------===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8///9/// \file10/// This file provides the implementation for deduplicating, detecting11/// conflicts in, and applying collections of Replacements.12///13/// FIXME: Use Diagnostics for output instead of llvm::errs().14///15//===----------------------------------------------------------------------===//16#include "clang-apply-replacements/Tooling/ApplyReplacements.h"17#include "clang/Basic/LangOptions.h"18#include "clang/Basic/SourceManager.h"19#include "clang/Format/Format.h"20#include "clang/Lex/Lexer.h"21#include "clang/Rewrite/Core/Rewriter.h"22#include "clang/Tooling/Core/Diagnostic.h"23#include "clang/Tooling/DiagnosticsYaml.h"24#include "clang/Tooling/ReplacementsYaml.h"25#include "llvm/ADT/ArrayRef.h"26#include "llvm/ADT/STLExtras.h"27#include "llvm/ADT/StringRef.h"28#include "llvm/ADT/StringSet.h"29#include "llvm/Support/FileSystem.h"30#include "llvm/Support/MemoryBuffer.h"31#include "llvm/Support/Path.h"32#include "llvm/Support/raw_ostream.h"33#include <array>34#include <optional>35 36using namespace llvm;37using namespace clang;38 39static void eatDiagnostics(const SMDiagnostic &, void *) {}40 41namespace clang {42namespace replace {43 44namespace detail {45 46static constexpr std::array<StringRef, 2> AllowedExtensions = {".yaml", ".yml"};47 48template <typename TranslationUnits>49static std::error_code collectReplacementsFromDirectory(50 const llvm::StringRef Directory, TranslationUnits &TUs,51 TUReplacementFiles &TUFiles, clang::DiagnosticsEngine &Diagnostics) {52 using namespace llvm::sys::fs;53 using namespace llvm::sys::path;54 55 std::error_code ErrorCode;56 57 for (recursive_directory_iterator I(Directory, ErrorCode), E;58 I != E && !ErrorCode; I.increment(ErrorCode)) {59 if (filename(I->path())[0] == '.') {60 // Indicate not to descend into directories beginning with '.'61 I.no_push();62 continue;63 }64 65 if (!is_contained(AllowedExtensions, extension(I->path())))66 continue;67 68 TUFiles.push_back(I->path());69 70 ErrorOr<std::unique_ptr<MemoryBuffer>> Out =71 MemoryBuffer::getFile(I->path());72 if (std::error_code BufferError = Out.getError()) {73 errs() << "Error reading " << I->path() << ": " << BufferError.message()74 << "\n";75 continue;76 }77 78 yaml::Input YIn(Out.get()->getBuffer(), nullptr, &eatDiagnostics);79 typename TranslationUnits::value_type TU;80 YIn >> TU;81 if (YIn.error()) {82 // File doesn't appear to be a header change description. Ignore it.83 continue;84 }85 86 // Only keep files that properly parse.87 TUs.push_back(TU);88 }89 90 return ErrorCode;91}92} // namespace detail93 94template <>95std::error_code collectReplacementsFromDirectory(96 const llvm::StringRef Directory, TUReplacements &TUs,97 TUReplacementFiles &TUFiles, clang::DiagnosticsEngine &Diagnostics) {98 return detail::collectReplacementsFromDirectory(Directory, TUs, TUFiles,99 Diagnostics);100}101 102template <>103std::error_code collectReplacementsFromDirectory(104 const llvm::StringRef Directory, TUDiagnostics &TUs,105 TUReplacementFiles &TUFiles, clang::DiagnosticsEngine &Diagnostics) {106 return detail::collectReplacementsFromDirectory(Directory, TUs, TUFiles,107 Diagnostics);108}109 110/// Extract replacements from collected TranslationUnitReplacements and111/// TranslationUnitDiagnostics and group them per file. Identical replacements112/// from diagnostics are deduplicated.113///114/// \param[in] TUs Collection of all found and deserialized115/// TranslationUnitReplacements.116/// \param[in] TUDs Collection of all found and deserialized117/// TranslationUnitDiagnostics.118/// \param[in] SM Used to deduplicate paths.119///120/// \returns A map mapping FileEntry to a set of Replacement targeting that121/// file.122static llvm::DenseMap<FileEntryRef, std::vector<tooling::Replacement>>123groupReplacements(const TUReplacements &TUs, const TUDiagnostics &TUDs,124 const clang::SourceManager &SM) {125 llvm::StringSet<> Warned;126 llvm::DenseMap<FileEntryRef, std::vector<tooling::Replacement>>127 GroupedReplacements;128 129 // Deduplicate identical replacements in diagnostics unless they are from the130 // same TU.131 // FIXME: Find an efficient way to deduplicate on diagnostics level.132 llvm::DenseMap<const FileEntry *,133 std::map<tooling::Replacement,134 const tooling::TranslationUnitDiagnostics *>>135 DiagReplacements;136 137 auto AddToGroup = [&](const tooling::Replacement &R,138 const tooling::TranslationUnitDiagnostics *SourceTU,139 const std::optional<std::string> BuildDir) {140 // Use the file manager to deduplicate paths. FileEntries are141 // automatically canonicalized. Since relative paths can come from different142 // build directories, make them absolute immediately.143 SmallString<128> Path = R.getFilePath();144 if (BuildDir)145 llvm::sys::path::make_absolute(*BuildDir, Path);146 else147 SM.getFileManager().makeAbsolutePath(Path);148 149 if (auto Entry = SM.getFileManager().getOptionalFileRef(Path)) {150 if (SourceTU) {151 auto [It, Inserted] = DiagReplacements[*Entry].try_emplace(R, SourceTU);152 if (!Inserted && It->second != SourceTU)153 // This replacement is a duplicate of one suggested by another TU.154 return;155 }156 GroupedReplacements[*Entry].push_back(R);157 } else if (Warned.insert(Path).second) {158 errs() << "Described file '" << R.getFilePath()159 << "' doesn't exist. Ignoring...\n";160 }161 };162 163 for (const auto &TU : TUs)164 for (const tooling::Replacement &R : TU.Replacements)165 AddToGroup(R, nullptr, {});166 167 for (const auto &TU : TUDs)168 for (const auto &D : TU.Diagnostics)169 if (const auto *ChoosenFix = tooling::selectFirstFix(D)) {170 for (const auto &Fix : *ChoosenFix)171 for (const tooling::Replacement &R : Fix.second)172 AddToGroup(R, &TU, D.BuildDirectory);173 }174 175 // Sort replacements per file to keep consistent behavior when176 // clang-apply-replacements run on differents machine.177 for (auto &FileAndReplacements : GroupedReplacements) {178 llvm::sort(FileAndReplacements.second);179 }180 181 return GroupedReplacements;182}183 184bool mergeAndDeduplicate(const TUReplacements &TUs, const TUDiagnostics &TUDs,185 FileToChangesMap &FileChanges,186 clang::SourceManager &SM, bool IgnoreInsertConflict) {187 auto GroupedReplacements = groupReplacements(TUs, TUDs, SM);188 bool ConflictDetected = false;189 190 // To report conflicting replacements on corresponding file, all replacements191 // are stored into 1 big AtomicChange.192 for (const auto &FileAndReplacements : GroupedReplacements) {193 FileEntryRef Entry = FileAndReplacements.first;194 const SourceLocation BeginLoc =195 SM.getLocForStartOfFile(SM.getOrCreateFileID(Entry, SrcMgr::C_User));196 tooling::AtomicChange FileChange(Entry.getName(), Entry.getName());197 for (const auto &R : FileAndReplacements.second) {198 llvm::Error Err =199 FileChange.replace(SM, BeginLoc.getLocWithOffset(R.getOffset()),200 R.getLength(), R.getReplacementText());201 if (Err) {202 // FIXME: This will report conflicts by pair using a file+offset format203 // which is not so much human readable.204 // A first improvement could be to translate offset to line+col. For205 // this and without loosing error message some modifications around206 // `tooling::ReplacementError` are need (access to207 // `getReplacementErrString`).208 // A better strategy could be to add a pretty printer methods for209 // conflict reporting. Methods that could be parameterized to report a210 // conflict in different format, file+offset, file+line+col, or even211 // more human readable using VCS conflict markers.212 // For now, printing directly the error reported by `AtomicChange` is213 // the easiest solution.214 errs() << llvm::toString(std::move(Err)) << "\n";215 if (IgnoreInsertConflict) {216 tooling::Replacements &Replacements = FileChange.getReplacements();217 unsigned NewOffset =218 Replacements.getShiftedCodePosition(R.getOffset());219 unsigned NewLength = Replacements.getShiftedCodePosition(220 R.getOffset() + R.getLength()) -221 NewOffset;222 if (NewLength == R.getLength()) {223 tooling::Replacement RR = tooling::Replacement(224 R.getFilePath(), NewOffset, NewLength, R.getReplacementText());225 Replacements = Replacements.merge(tooling::Replacements(RR));226 } else {227 llvm::errs()228 << "Can't resolve conflict, skipping the replacement.\n";229 ConflictDetected = true;230 }231 } else232 ConflictDetected = true;233 }234 }235 FileChanges.try_emplace(Entry,236 std::vector<tooling::AtomicChange>{FileChange});237 }238 239 return !ConflictDetected;240}241 242llvm::Expected<std::string>243applyChanges(StringRef File, const std::vector<tooling::AtomicChange> &Changes,244 const tooling::ApplyChangesSpec &Spec,245 DiagnosticsEngine &Diagnostics) {246 FileManager Files((FileSystemOptions()));247 SourceManager SM(Diagnostics, Files);248 249 llvm::ErrorOr<std::unique_ptr<MemoryBuffer>> Buffer =250 SM.getFileManager().getBufferForFile(File);251 if (!Buffer)252 return errorCodeToError(Buffer.getError());253 return tooling::applyAtomicChanges(File, Buffer.get()->getBuffer(), Changes,254 Spec);255}256 257bool deleteReplacementFiles(const TUReplacementFiles &Files,258 clang::DiagnosticsEngine &Diagnostics) {259 bool Success = true;260 for (const auto &Filename : Files) {261 std::error_code Error = llvm::sys::fs::remove(Filename);262 if (Error) {263 Success = false;264 // FIXME: Use Diagnostics for outputting errors.265 errs() << "Error deleting file: " << Filename << "\n";266 errs() << Error.message() << "\n";267 errs() << "Please delete the file manually\n";268 }269 }270 return Success;271}272 273} // end namespace replace274} // end namespace clang275