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/Optional.h"
27 #include "llvm/Support/FileSystem.h"
28 #include "llvm/Support/MemoryBuffer.h"
29 #include "llvm/Support/Path.h"
30 #include "llvm/Support/raw_ostream.h"
31 
32 using namespace llvm;
33 using namespace clang;
34 
eatDiagnostics(const SMDiagnostic &,void *)35 static void eatDiagnostics(const SMDiagnostic &, void *) {}
36 
37 namespace clang {
38 namespace replace {
39 
collectReplacementsFromDirectory(const llvm::StringRef Directory,TUReplacements & TUs,TUReplacementFiles & TUFiles,clang::DiagnosticsEngine & Diagnostics)40 std::error_code collectReplacementsFromDirectory(
41     const llvm::StringRef Directory, TUReplacements &TUs,
42     TUReplacementFiles &TUFiles, clang::DiagnosticsEngine &Diagnostics) {
43   using namespace llvm::sys::fs;
44   using namespace llvm::sys::path;
45 
46   std::error_code ErrorCode;
47 
48   for (recursive_directory_iterator I(Directory, ErrorCode), E;
49        I != E && !ErrorCode; I.increment(ErrorCode)) {
50     if (filename(I->path())[0] == '.') {
51       // Indicate not to descend into directories beginning with '.'
52       I.no_push();
53       continue;
54     }
55 
56     if (extension(I->path()) != ".yaml")
57       continue;
58 
59     TUFiles.push_back(I->path());
60 
61     ErrorOr<std::unique_ptr<MemoryBuffer>> Out =
62         MemoryBuffer::getFile(I->path());
63     if (std::error_code BufferError = Out.getError()) {
64       errs() << "Error reading " << I->path() << ": " << BufferError.message()
65              << "\n";
66       continue;
67     }
68 
69     yaml::Input YIn(Out.get()->getBuffer(), nullptr, &eatDiagnostics);
70     tooling::TranslationUnitReplacements TU;
71     YIn >> TU;
72     if (YIn.error()) {
73       // File doesn't appear to be a header change description. Ignore it.
74       continue;
75     }
76 
77     // Only keep files that properly parse.
78     TUs.push_back(TU);
79   }
80 
81   return ErrorCode;
82 }
83 
collectReplacementsFromDirectory(const llvm::StringRef Directory,TUDiagnostics & TUs,TUReplacementFiles & TUFiles,clang::DiagnosticsEngine & Diagnostics)84 std::error_code collectReplacementsFromDirectory(
85     const llvm::StringRef Directory, TUDiagnostics &TUs,
86     TUReplacementFiles &TUFiles, clang::DiagnosticsEngine &Diagnostics) {
87   using namespace llvm::sys::fs;
88   using namespace llvm::sys::path;
89 
90   std::error_code ErrorCode;
91 
92   for (recursive_directory_iterator I(Directory, ErrorCode), E;
93        I != E && !ErrorCode; I.increment(ErrorCode)) {
94     if (filename(I->path())[0] == '.') {
95       // Indicate not to descend into directories beginning with '.'
96       I.no_push();
97       continue;
98     }
99 
100     if (extension(I->path()) != ".yaml")
101       continue;
102 
103     TUFiles.push_back(I->path());
104 
105     ErrorOr<std::unique_ptr<MemoryBuffer>> Out =
106         MemoryBuffer::getFile(I->path());
107     if (std::error_code BufferError = Out.getError()) {
108       errs() << "Error reading " << I->path() << ": " << BufferError.message()
109              << "\n";
110       continue;
111     }
112 
113     yaml::Input YIn(Out.get()->getBuffer(), nullptr, &eatDiagnostics);
114     tooling::TranslationUnitDiagnostics TU;
115     YIn >> TU;
116     if (YIn.error()) {
117       // File doesn't appear to be a header change description. Ignore it.
118       continue;
119     }
120 
121     // Only keep files that properly parse.
122     TUs.push_back(TU);
123   }
124 
125   return ErrorCode;
126 }
127 
128 /// Extract replacements from collected TranslationUnitReplacements and
129 /// TranslationUnitDiagnostics and group them per file. Identical replacements
130 /// from diagnostics are deduplicated.
131 ///
132 /// \param[in] TUs Collection of all found and deserialized
133 /// TranslationUnitReplacements.
134 /// \param[in] TUDs Collection of all found and deserialized
135 /// TranslationUnitDiagnostics.
136 /// \param[in] SM Used to deduplicate paths.
137 ///
138 /// \returns A map mapping FileEntry to a set of Replacement targeting that
139 /// file.
140 static llvm::DenseMap<const FileEntry *, std::vector<tooling::Replacement>>
groupReplacements(const TUReplacements & TUs,const TUDiagnostics & TUDs,const clang::SourceManager & SM)141 groupReplacements(const TUReplacements &TUs, const TUDiagnostics &TUDs,
142                   const clang::SourceManager &SM) {
143   std::set<StringRef> Warned;
144   llvm::DenseMap<const FileEntry *, std::vector<tooling::Replacement>>
145       GroupedReplacements;
146 
147   // Deduplicate identical replacements in diagnostics unless they are from the
148   // same TU.
149   // FIXME: Find an efficient way to deduplicate on diagnostics level.
150   llvm::DenseMap<const FileEntry *,
151                  std::map<tooling::Replacement,
152                           const tooling::TranslationUnitDiagnostics *>>
153       DiagReplacements;
154 
155   auto AddToGroup = [&](const tooling::Replacement &R,
156                         const tooling::TranslationUnitDiagnostics *SourceTU,
157                         const llvm::Optional<std::string> BuildDir) {
158     // Use the file manager to deduplicate paths. FileEntries are
159     // automatically canonicalized.
160     auto PrevWorkingDir = SM.getFileManager().getFileSystemOpts().WorkingDir;
161     if (BuildDir)
162       SM.getFileManager().getFileSystemOpts().WorkingDir = std::move(*BuildDir);
163     if (auto Entry = SM.getFileManager().getFile(R.getFilePath())) {
164       if (SourceTU) {
165         auto &Replaces = DiagReplacements[*Entry];
166         auto It = Replaces.find(R);
167         if (It == Replaces.end())
168           Replaces.emplace(R, SourceTU);
169         else if (It->second != SourceTU)
170           // This replacement is a duplicate of one suggested by another TU.
171           return;
172       }
173       GroupedReplacements[*Entry].push_back(R);
174     } else if (Warned.insert(R.getFilePath()).second) {
175       errs() << "Described file '" << R.getFilePath()
176              << "' doesn't exist. Ignoring...\n";
177     }
178     SM.getFileManager().getFileSystemOpts().WorkingDir = PrevWorkingDir;
179   };
180 
181   for (const auto &TU : TUs)
182     for (const tooling::Replacement &R : TU.Replacements)
183       AddToGroup(R, nullptr, {});
184 
185   for (const auto &TU : TUDs)
186     for (const auto &D : TU.Diagnostics)
187       if (const auto *ChoosenFix = tooling::selectFirstFix(D)) {
188         for (const auto &Fix : *ChoosenFix)
189           for (const tooling::Replacement &R : Fix.second)
190             AddToGroup(R, &TU, D.BuildDirectory);
191       }
192 
193   // Sort replacements per file to keep consistent behavior when
194   // clang-apply-replacements run on differents machine.
195   for (auto &FileAndReplacements : GroupedReplacements) {
196     llvm::sort(FileAndReplacements.second);
197   }
198 
199   return GroupedReplacements;
200 }
201 
mergeAndDeduplicate(const TUReplacements & TUs,const TUDiagnostics & TUDs,FileToChangesMap & FileChanges,clang::SourceManager & SM,bool IgnoreInsertConflict)202 bool mergeAndDeduplicate(const TUReplacements &TUs, const TUDiagnostics &TUDs,
203                          FileToChangesMap &FileChanges,
204                          clang::SourceManager &SM, bool IgnoreInsertConflict) {
205   auto GroupedReplacements = groupReplacements(TUs, TUDs, SM);
206   bool ConflictDetected = false;
207 
208   // To report conflicting replacements on corresponding file, all replacements
209   // are stored into 1 big AtomicChange.
210   for (const auto &FileAndReplacements : GroupedReplacements) {
211     const FileEntry *Entry = FileAndReplacements.first;
212     const SourceLocation BeginLoc =
213         SM.getLocForStartOfFile(SM.getOrCreateFileID(Entry, SrcMgr::C_User));
214     tooling::AtomicChange FileChange(Entry->getName(), Entry->getName());
215     for (const auto &R : FileAndReplacements.second) {
216       llvm::Error Err =
217           FileChange.replace(SM, BeginLoc.getLocWithOffset(R.getOffset()),
218                              R.getLength(), R.getReplacementText());
219       if (Err) {
220         // FIXME: This will report conflicts by pair using a file+offset format
221         // which is not so much human readable.
222         // A first improvement could be to translate offset to line+col. For
223         // this and without loosing error message some modifications around
224         // `tooling::ReplacementError` are need (access to
225         // `getReplacementErrString`).
226         // A better strategy could be to add a pretty printer methods for
227         // conflict reporting. Methods that could be parameterized to report a
228         // conflict in different format, file+offset, file+line+col, or even
229         // more human readable using VCS conflict markers.
230         // For now, printing directly the error reported by `AtomicChange` is
231         // the easiest solution.
232         errs() << llvm::toString(std::move(Err)) << "\n";
233         if (IgnoreInsertConflict) {
234           tooling::Replacements &Replacements = FileChange.getReplacements();
235           unsigned NewOffset =
236               Replacements.getShiftedCodePosition(R.getOffset());
237           unsigned NewLength = Replacements.getShiftedCodePosition(
238                                    R.getOffset() + R.getLength()) -
239                                NewOffset;
240           if (NewLength == R.getLength()) {
241             tooling::Replacement RR = tooling::Replacement(
242                 R.getFilePath(), NewOffset, NewLength, R.getReplacementText());
243             Replacements = Replacements.merge(tooling::Replacements(RR));
244           } else {
245             llvm::errs()
246                 << "Can't resolve conflict, skipping the replacement.\n";
247             ConflictDetected = true;
248           }
249         } else
250           ConflictDetected = true;
251       }
252     }
253     FileChanges.try_emplace(Entry,
254                             std::vector<tooling::AtomicChange>{FileChange});
255   }
256 
257   return !ConflictDetected;
258 }
259 
260 llvm::Expected<std::string>
applyChanges(StringRef File,const std::vector<tooling::AtomicChange> & Changes,const tooling::ApplyChangesSpec & Spec,DiagnosticsEngine & Diagnostics)261 applyChanges(StringRef File, const std::vector<tooling::AtomicChange> &Changes,
262              const tooling::ApplyChangesSpec &Spec,
263              DiagnosticsEngine &Diagnostics) {
264   FileManager Files((FileSystemOptions()));
265   SourceManager SM(Diagnostics, Files);
266 
267   llvm::ErrorOr<std::unique_ptr<MemoryBuffer>> Buffer =
268       SM.getFileManager().getBufferForFile(File);
269   if (!Buffer)
270     return errorCodeToError(Buffer.getError());
271   return tooling::applyAtomicChanges(File, Buffer.get()->getBuffer(), Changes,
272                                      Spec);
273 }
274 
deleteReplacementFiles(const TUReplacementFiles & Files,clang::DiagnosticsEngine & Diagnostics)275 bool deleteReplacementFiles(const TUReplacementFiles &Files,
276                             clang::DiagnosticsEngine &Diagnostics) {
277   bool Success = true;
278   for (const auto &Filename : Files) {
279     std::error_code Error = llvm::sys::fs::remove(Filename);
280     if (Error) {
281       Success = false;
282       // FIXME: Use Diagnostics for outputting errors.
283       errs() << "Error deleting file: " << Filename << "\n";
284       errs() << Error.message() << "\n";
285       errs() << "Please delete the file manually\n";
286     }
287   }
288   return Success;
289 }
290 
291 } // end namespace replace
292 } // end namespace clang
293