xref: /llvm-project/clang-tools-extra/clang-apply-replacements/lib/Tooling/ApplyReplacements.cpp (revision 77c842f44cc06951975fd4a85761e0bc830d185a)
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