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(ec&: ErrorCode)) { |
59 | if (filename(path: I->path())[0] == '.') { |
60 | // Indicate not to descend into directories beginning with '.' |
61 | I.no_push(); |
62 | continue; |
63 | } |
64 | |
65 | if (!is_contained(Range: AllowedExtensions, Element: extension(path: I->path()))) |
66 | continue; |
67 | |
68 | TUFiles.push_back(x: I->path()); |
69 | |
70 | ErrorOr<std::unique_ptr<MemoryBuffer>> Out = |
71 | MemoryBuffer::getFile(Filename: 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(current_directory: *BuildDir, path&: Path); |
146 | else |
147 | SM.getFileManager().makeAbsolutePath(Path); |
148 | |
149 | if (auto Entry = SM.getFileManager().getOptionalFileRef(Filename: Path)) { |
150 | if (SourceTU) { |
151 | auto &Replaces = DiagReplacements[*Entry]; |
152 | auto It = Replaces.find(x: R); |
153 | if (It == Replaces.end()) |
154 | Replaces.emplace(args: R, args&: SourceTU); |
155 | else if (It->second != SourceTU) |
156 | // This replacement is a duplicate of one suggested by another TU. |
157 | return; |
158 | } |
159 | GroupedReplacements[*Entry].push_back(x: R); |
160 | } else if (Warned.insert(key: Path).second) { |
161 | errs() << "Described file '" << R.getFilePath() |
162 | << "' doesn't exist. Ignoring...\n" ; |
163 | } |
164 | }; |
165 | |
166 | for (const auto &TU : TUs) |
167 | for (const tooling::Replacement &R : TU.Replacements) |
168 | AddToGroup(R, nullptr, {}); |
169 | |
170 | for (const auto &TU : TUDs) |
171 | for (const auto &D : TU.Diagnostics) |
172 | if (const auto *ChoosenFix = tooling::selectFirstFix(D)) { |
173 | for (const auto &Fix : *ChoosenFix) |
174 | for (const tooling::Replacement &R : Fix.second) |
175 | AddToGroup(R, &TU, D.BuildDirectory); |
176 | } |
177 | |
178 | // Sort replacements per file to keep consistent behavior when |
179 | // clang-apply-replacements run on differents machine. |
180 | for (auto &FileAndReplacements : GroupedReplacements) { |
181 | llvm::sort(C&: FileAndReplacements.second); |
182 | } |
183 | |
184 | return GroupedReplacements; |
185 | } |
186 | |
187 | bool mergeAndDeduplicate(const TUReplacements &TUs, const TUDiagnostics &TUDs, |
188 | FileToChangesMap &FileChanges, |
189 | clang::SourceManager &SM, bool IgnoreInsertConflict) { |
190 | auto GroupedReplacements = groupReplacements(TUs, TUDs, SM); |
191 | bool ConflictDetected = false; |
192 | |
193 | // To report conflicting replacements on corresponding file, all replacements |
194 | // are stored into 1 big AtomicChange. |
195 | for (const auto &FileAndReplacements : GroupedReplacements) { |
196 | FileEntryRef Entry = FileAndReplacements.first; |
197 | const SourceLocation BeginLoc = |
198 | SM.getLocForStartOfFile(FID: SM.getOrCreateFileID(SourceFile: Entry, FileCharacter: SrcMgr::C_User)); |
199 | tooling::AtomicChange FileChange(Entry.getName(), Entry.getName()); |
200 | for (const auto &R : FileAndReplacements.second) { |
201 | llvm::Error Err = |
202 | FileChange.replace(SM, Loc: BeginLoc.getLocWithOffset(Offset: R.getOffset()), |
203 | Length: R.getLength(), Text: R.getReplacementText()); |
204 | if (Err) { |
205 | // FIXME: This will report conflicts by pair using a file+offset format |
206 | // which is not so much human readable. |
207 | // A first improvement could be to translate offset to line+col. For |
208 | // this and without loosing error message some modifications around |
209 | // `tooling::ReplacementError` are need (access to |
210 | // `getReplacementErrString`). |
211 | // A better strategy could be to add a pretty printer methods for |
212 | // conflict reporting. Methods that could be parameterized to report a |
213 | // conflict in different format, file+offset, file+line+col, or even |
214 | // more human readable using VCS conflict markers. |
215 | // For now, printing directly the error reported by `AtomicChange` is |
216 | // the easiest solution. |
217 | errs() << llvm::toString(E: std::move(Err)) << "\n" ; |
218 | if (IgnoreInsertConflict) { |
219 | tooling::Replacements &Replacements = FileChange.getReplacements(); |
220 | unsigned NewOffset = |
221 | Replacements.getShiftedCodePosition(Position: R.getOffset()); |
222 | unsigned NewLength = Replacements.getShiftedCodePosition( |
223 | Position: R.getOffset() + R.getLength()) - |
224 | NewOffset; |
225 | if (NewLength == R.getLength()) { |
226 | tooling::Replacement RR = tooling::Replacement( |
227 | R.getFilePath(), NewOffset, NewLength, R.getReplacementText()); |
228 | Replacements = Replacements.merge(Replaces: tooling::Replacements(RR)); |
229 | } else { |
230 | llvm::errs() |
231 | << "Can't resolve conflict, skipping the replacement.\n" ; |
232 | ConflictDetected = true; |
233 | } |
234 | } else |
235 | ConflictDetected = true; |
236 | } |
237 | } |
238 | FileChanges.try_emplace(Key: Entry, |
239 | Args: std::vector<tooling::AtomicChange>{FileChange}); |
240 | } |
241 | |
242 | return !ConflictDetected; |
243 | } |
244 | |
245 | llvm::Expected<std::string> |
246 | applyChanges(StringRef File, const std::vector<tooling::AtomicChange> &Changes, |
247 | const tooling::ApplyChangesSpec &Spec, |
248 | DiagnosticsEngine &Diagnostics) { |
249 | FileManager Files((FileSystemOptions())); |
250 | SourceManager SM(Diagnostics, Files); |
251 | |
252 | llvm::ErrorOr<std::unique_ptr<MemoryBuffer>> Buffer = |
253 | SM.getFileManager().getBufferForFile(Filename: File); |
254 | if (!Buffer) |
255 | return errorCodeToError(EC: Buffer.getError()); |
256 | return tooling::applyAtomicChanges(FilePath: File, Code: Buffer.get()->getBuffer(), Changes, |
257 | Spec); |
258 | } |
259 | |
260 | bool deleteReplacementFiles(const TUReplacementFiles &Files, |
261 | clang::DiagnosticsEngine &Diagnostics) { |
262 | bool Success = true; |
263 | for (const auto &Filename : Files) { |
264 | std::error_code Error = llvm::sys::fs::remove(path: Filename); |
265 | if (Error) { |
266 | Success = false; |
267 | // FIXME: Use Diagnostics for outputting errors. |
268 | errs() << "Error deleting file: " << Filename << "\n" ; |
269 | errs() << Error.message() << "\n" ; |
270 | errs() << "Please delete the file manually\n" ; |
271 | } |
272 | } |
273 | return Success; |
274 | } |
275 | |
276 | } // end namespace replace |
277 | } // end namespace clang |
278 | |