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