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