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