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-exception 6 // 7 //===----------------------------------------------------------------------===// 8 /// 9 /// \file 10 /// This file provides the implementation for deduplicating, detecting 11 /// 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 36 using namespace llvm; 37 using namespace clang; 38 39 static void eatDiagnostics(const SMDiagnostic &, void *) {} 40 41 namespace clang { 42 namespace replace { 43 44 namespace detail { 45 46 static constexpr std::array<StringRef, 2> AllowedExtensions = {".yaml", ".yml"}; 47 48 template <typename TranslationUnits> 49 static 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 detail 93 94 template <> 95 std::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 102 template <> 103 std::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 and 111 /// TranslationUnitDiagnostics and group them per file. Identical replacements 112 /// from diagnostics are deduplicated. 113 /// 114 /// \param[in] TUs Collection of all found and deserialized 115 /// TranslationUnitReplacements. 116 /// \param[in] TUDs Collection of all found and deserialized 117 /// TranslationUnitDiagnostics. 118 /// \param[in] SM Used to deduplicate paths. 119 /// 120 /// \returns A map mapping FileEntry to a set of Replacement targeting that 121 /// file. 122 static llvm::DenseMap<FileEntryRef, std::vector<tooling::Replacement>> 123 groupReplacements(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 the 130 // 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 are 141 // automatically canonicalized. Since relative paths can come from different 142 // build directories, make them absolute immediately. 143 SmallString<128> Path = R.getFilePath(); 144 if (BuildDir) 145 llvm::sys::fs::make_absolute(*BuildDir, Path); 146 else 147 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 when 176 // clang-apply-replacements run on differents machine. 177 for (auto &FileAndReplacements : GroupedReplacements) { 178 llvm::sort(FileAndReplacements.second); 179 } 180 181 return GroupedReplacements; 182 } 183 184 bool 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 replacements 191 // 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 format 203 // which is not so much human readable. 204 // A first improvement could be to translate offset to line+col. For 205 // this and without loosing error message some modifications around 206 // `tooling::ReplacementError` are need (access to 207 // `getReplacementErrString`). 208 // A better strategy could be to add a pretty printer methods for 209 // conflict reporting. Methods that could be parameterized to report a 210 // conflict in different format, file+offset, file+line+col, or even 211 // more human readable using VCS conflict markers. 212 // For now, printing directly the error reported by `AtomicChange` is 213 // 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 } else 232 ConflictDetected = true; 233 } 234 } 235 FileChanges.try_emplace(Entry, 236 std::vector<tooling::AtomicChange>{FileChange}); 237 } 238 239 return !ConflictDetected; 240 } 241 242 llvm::Expected<std::string> 243 applyChanges(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 257 bool 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 replace 274 } // end namespace clang 275