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
31using namespace llvm;
32using namespace clang;
33
34static void eatDiagnostics(const SMDiagnostic &, void *) {}
35
36namespace clang {
37namespace replace {
38
39std::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
83std::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.
139static llvm::DenseMap<const FileEntry *, std::vector<tooling::Replacement>>
140groupReplacements(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
197bool 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
238llvm::Expected<std::string>
239applyChanges(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
253bool 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