1 //===-- ApplyReplacements.cpp - Apply and deduplicate replacements --------===//
2 //
3 //                     The LLVM Compiler Infrastructure
4 //
5 // This file is distributed under the University of Illinois Open Source
6 // License. See LICENSE.TXT for details.
7 //
8 //===----------------------------------------------------------------------===//
9 ///
10 /// \file
11 /// \brief This file provides the implementation for deduplicating, detecting
12 /// conflicts in, and applying collections of Replacements.
13 ///
14 /// FIXME: Use Diagnostics for output instead of llvm::errs().
15 ///
16 //===----------------------------------------------------------------------===//
17 #include "clang-apply-replacements/Tooling/ApplyReplacements.h"
18 #include "clang/Basic/LangOptions.h"
19 #include "clang/Basic/SourceManager.h"
20 #include "clang/Format/Format.h"
21 #include "clang/Lex/Lexer.h"
22 #include "clang/Rewrite/Core/Rewriter.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 
34 static void eatDiagnostics(const SMDiagnostic &, void *) {}
35 
36 namespace clang {
37 namespace replace {
38 
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 
83 std::error_code
84 collectReplacementsFromDirectory(const llvm::StringRef Directory,
85                                  TUDiagnostics &TUs, TUReplacementFiles &TUFiles,
86                                  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 /// \brief Dumps information for a sequence of conflicting Replacements.
129 ///
130 /// \param[in] File FileEntry for the file the conflicting Replacements are
131 /// for.
132 /// \param[in] ConflictingReplacements List of conflicting Replacements.
133 /// \param[in] SM SourceManager used for reporting.
134 static void reportConflict(
135     const FileEntry *File,
136     const llvm::ArrayRef<clang::tooling::Replacement> ConflictingReplacements,
137     SourceManager &SM) {
138   FileID FID = SM.translateFile(File);
139   if (FID.isInvalid())
140     FID = SM.createFileID(File, SourceLocation(), SrcMgr::C_User);
141 
142   // FIXME: Output something a little more user-friendly (e.g. unified diff?)
143   errs() << "The following changes conflict:\n";
144   for (const tooling::Replacement &R : ConflictingReplacements) {
145     if (R.getLength() == 0) {
146       errs() << "  Insert at " << SM.getLineNumber(FID, R.getOffset()) << ":"
147              << SM.getColumnNumber(FID, R.getOffset()) << " "
148              << R.getReplacementText() << "\n";
149     } else {
150       if (R.getReplacementText().empty())
151         errs() << "  Remove ";
152       else
153         errs() << "  Replace ";
154 
155       errs() << SM.getLineNumber(FID, R.getOffset()) << ":"
156              << SM.getColumnNumber(FID, R.getOffset()) << "-"
157              << SM.getLineNumber(FID, R.getOffset() + R.getLength() - 1) << ":"
158              << SM.getColumnNumber(FID, R.getOffset() + R.getLength() - 1);
159 
160       if (R.getReplacementText().empty())
161         errs() << "\n";
162       else
163         errs() << " with \"" << R.getReplacementText() << "\"\n";
164     }
165   }
166 }
167 
168 // FIXME: Remove this function after changing clang-apply-replacements to use
169 // Replacements class.
170 bool applyAllReplacements(const std::vector<tooling::Replacement> &Replaces,
171                           Rewriter &Rewrite) {
172   bool Result = true;
173   for (auto I = Replaces.begin(), E = Replaces.end(); I != E; ++I) {
174     if (I->isApplicable()) {
175       Result = I->apply(Rewrite) && Result;
176     } else {
177       Result = false;
178     }
179   }
180   return Result;
181 }
182 
183 // FIXME: moved from libToolingCore. remove this when std::vector<Replacement>
184 // is replaced with tooling::Replacements class.
185 static void deduplicate(std::vector<tooling::Replacement> &Replaces,
186                         std::vector<tooling::Range> &Conflicts) {
187   if (Replaces.empty())
188     return;
189 
190   auto LessNoPath = [](const tooling::Replacement &LHS,
191                        const tooling::Replacement &RHS) {
192     if (LHS.getOffset() != RHS.getOffset())
193       return LHS.getOffset() < RHS.getOffset();
194     if (LHS.getLength() != RHS.getLength())
195       return LHS.getLength() < RHS.getLength();
196     return LHS.getReplacementText() < RHS.getReplacementText();
197   };
198 
199   auto EqualNoPath = [](const tooling::Replacement &LHS,
200                         const tooling::Replacement &RHS) {
201     return LHS.getOffset() == RHS.getOffset() &&
202            LHS.getLength() == RHS.getLength() &&
203            LHS.getReplacementText() == RHS.getReplacementText();
204   };
205 
206   // Deduplicate. We don't want to deduplicate based on the path as we assume
207   // that all replacements refer to the same file (or are symlinks).
208   std::sort(Replaces.begin(), Replaces.end(), LessNoPath);
209   Replaces.erase(std::unique(Replaces.begin(), Replaces.end(), EqualNoPath),
210                  Replaces.end());
211 
212   // Detect conflicts
213   tooling::Range ConflictRange(Replaces.front().getOffset(),
214                                Replaces.front().getLength());
215   unsigned ConflictStart = 0;
216   unsigned ConflictLength = 1;
217   for (unsigned i = 1; i < Replaces.size(); ++i) {
218     tooling::Range Current(Replaces[i].getOffset(), Replaces[i].getLength());
219     if (ConflictRange.overlapsWith(Current)) {
220       // Extend conflicted range
221       ConflictRange =
222           tooling::Range(ConflictRange.getOffset(),
223                          std::max(ConflictRange.getLength(),
224                                   Current.getOffset() + Current.getLength() -
225                                       ConflictRange.getOffset()));
226       ++ConflictLength;
227     } else {
228       if (ConflictLength > 1)
229         Conflicts.push_back(tooling::Range(ConflictStart, ConflictLength));
230       ConflictRange = Current;
231       ConflictStart = i;
232       ConflictLength = 1;
233     }
234   }
235 
236   if (ConflictLength > 1)
237     Conflicts.push_back(tooling::Range(ConflictStart, ConflictLength));
238 }
239 
240 /// \brief Deduplicates and tests for conflicts among the replacements for each
241 /// file in \c Replacements. Any conflicts found are reported.
242 ///
243 /// \post Replacements[i].getOffset() <= Replacements[i+1].getOffset().
244 ///
245 /// \param[in,out] Replacements Container of all replacements grouped by file
246 /// to be deduplicated and checked for conflicts.
247 /// \param[in] SM SourceManager required for conflict reporting.
248 ///
249 /// \returns \parblock
250 ///          \li true if conflicts were detected
251 ///          \li false if no conflicts were detected
252 static bool deduplicateAndDetectConflicts(FileToReplacementsMap &Replacements,
253                                           SourceManager &SM) {
254   bool conflictsFound = false;
255 
256   for (auto &FileAndReplacements : Replacements) {
257     const FileEntry *Entry = FileAndReplacements.first;
258     auto &Replacements = FileAndReplacements.second;
259     assert(Entry != nullptr && "No file entry!");
260 
261     std::vector<tooling::Range> Conflicts;
262     deduplicate(FileAndReplacements.second, Conflicts);
263 
264     if (Conflicts.empty())
265       continue;
266 
267     conflictsFound = true;
268 
269     errs() << "There are conflicting changes to " << Entry->getName() << ":\n";
270 
271     for (const tooling::Range &Conflict : Conflicts) {
272       auto ConflictingReplacements = llvm::makeArrayRef(
273           &Replacements[Conflict.getOffset()], Conflict.getLength());
274       reportConflict(Entry, ConflictingReplacements, SM);
275     }
276   }
277 
278   return conflictsFound;
279 }
280 
281 bool mergeAndDeduplicate(const TUReplacements &TUs,
282                          FileToReplacementsMap &GroupedReplacements,
283                          clang::SourceManager &SM) {
284 
285   // Group all replacements by target file.
286   std::set<StringRef> Warned;
287   for (const auto &TU : TUs) {
288     for (const tooling::Replacement &R : TU.Replacements) {
289       // Use the file manager to deduplicate paths. FileEntries are
290       // automatically canonicalized.
291       if (const FileEntry *Entry = SM.getFileManager().getFile(R.getFilePath())) {
292         GroupedReplacements[Entry].push_back(R);
293       } else if (Warned.insert(R.getFilePath()).second) {
294           errs() << "Described file '" << R.getFilePath()
295                  << "' doesn't exist. Ignoring...\n";
296       }
297     }
298   }
299 
300   // Ask clang to deduplicate and report conflicts.
301   return !deduplicateAndDetectConflicts(GroupedReplacements, SM);
302 }
303 
304 bool mergeAndDeduplicate(const TUDiagnostics &TUs,
305                          FileToReplacementsMap &GroupedReplacements,
306                          clang::SourceManager &SM) {
307 
308   // Group all replacements by target file.
309   std::set<StringRef> Warned;
310   for (const auto &TU : TUs) {
311     for (const auto &D : TU.Diagnostics) {
312       for (const auto &Fix : D.Fix) {
313         for (const tooling::Replacement &R : Fix.second) {
314           // Use the file manager to deduplicate paths. FileEntries are
315           // automatically canonicalized.
316           if (const FileEntry *Entry = SM.getFileManager().getFile(R.getFilePath())) {
317             GroupedReplacements[Entry].push_back(R);
318           } else if (Warned.insert(R.getFilePath()).second) {
319               errs() << "Described file '" << R.getFilePath()
320                      << "' doesn't exist. Ignoring...\n";
321           }
322         }
323       }
324     }
325   }
326 
327   // Ask clang to deduplicate and report conflicts.
328   return !deduplicateAndDetectConflicts(GroupedReplacements, SM);
329 }
330 
331 bool applyReplacements(const FileToReplacementsMap &GroupedReplacements,
332                        clang::Rewriter &Rewrites) {
333 
334   // Apply all changes
335   //
336   // FIXME: No longer certain GroupedReplacements is really the best kind of
337   // data structure for applying replacements. Rewriter certainly doesn't care.
338   // However, until we nail down the design of ReplacementGroups, might as well
339   // leave this as is.
340   for (const auto &FileAndReplacements : GroupedReplacements) {
341     if (!applyAllReplacements(FileAndReplacements.second, Rewrites))
342       return false;
343   }
344 
345   return true;
346 }
347 
348 RangeVector calculateChangedRanges(
349     const std::vector<clang::tooling::Replacement> &Replaces) {
350   RangeVector ChangedRanges;
351 
352   // Generate the new ranges from the replacements.
353   int Shift = 0;
354   for (const tooling::Replacement &R : Replaces) {
355     unsigned Offset = R.getOffset() + Shift;
356     unsigned Length = R.getReplacementText().size();
357     Shift += Length - R.getLength();
358     ChangedRanges.push_back(tooling::Range(Offset, Length));
359   }
360 
361   return ChangedRanges;
362 }
363 
364 bool writeFiles(const clang::Rewriter &Rewrites) {
365 
366   for (auto BufferI = Rewrites.buffer_begin(), BufferE = Rewrites.buffer_end();
367        BufferI != BufferE; ++BufferI) {
368     StringRef FileName =
369         Rewrites.getSourceMgr().getFileEntryForID(BufferI->first)->getName();
370 
371     std::error_code EC;
372     llvm::raw_fd_ostream FileStream(FileName, EC, llvm::sys::fs::F_Text);
373     if (EC) {
374       errs() << "Warning: Could not write to " << EC.message() << "\n";
375       continue;
376     }
377     BufferI->second.write(FileStream);
378   }
379 
380   return true;
381 }
382 
383 bool deleteReplacementFiles(const TUReplacementFiles &Files,
384                             clang::DiagnosticsEngine &Diagnostics) {
385   bool Success = true;
386   for (const auto &Filename : Files) {
387     std::error_code Error = llvm::sys::fs::remove(Filename);
388     if (Error) {
389       Success = false;
390       // FIXME: Use Diagnostics for outputting errors.
391       errs() << "Error deleting file: " << Filename << "\n";
392       errs() << Error.message() << "\n";
393       errs() << "Please delete the file manually\n";
394     }
395   }
396   return Success;
397 }
398 
399 } // end namespace replace
400 } // end namespace clang
401