1 //===-- ApplyReplacements.cpp - Apply and deduplicate replacements --------===//
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
7 //===----------------------------------------------------------------------===//
10 /// This file provides the implementation for deduplicating, detecting
11 /// conflicts in, and applying collections of Replacements.
13 /// FIXME: Use Diagnostics for output instead of llvm::errs().
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"
37 using namespace clang
;
39 static void eatDiagnostics(const SMDiagnostic
&, void *) {}
46 static constexpr std::array
<StringRef
, 2> AllowedExtensions
= {".yaml", ".yml"};
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
;
55 std::error_code ErrorCode
;
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 '.'
65 if (!is_contained(AllowedExtensions
, extension(I
->path())))
68 TUFiles
.push_back(I
->path());
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()
78 yaml::Input
YIn(Out
.get()->getBuffer(), nullptr, &eatDiagnostics
);
79 typename
TranslationUnits::value_type TU
;
82 // File doesn't appear to be a header change description. Ignore it.
86 // Only keep files that properly parse.
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
,
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
,
110 /// Extract replacements from collected TranslationUnitReplacements and
111 /// TranslationUnitDiagnostics and group them per file. Identical replacements
112 /// from diagnostics are deduplicated.
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.
120 /// \returns A map mapping FileEntry to a set of Replacement targeting that
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
>>
129 // Deduplicate identical replacements in diagnostics unless they are from the
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
*>>
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();
145 llvm::sys::fs::make_absolute(*BuildDir
, Path
);
147 SM
.getFileManager().makeAbsolutePath(Path
);
149 if (auto Entry
= SM
.getFileManager().getOptionalFileRef(Path
)) {
151 auto &Replaces
= DiagReplacements
[*Entry
];
152 auto It
= Replaces
.find(R
);
153 if (It
== Replaces
.end())
154 Replaces
.emplace(R
, SourceTU
);
155 else if (It
->second
!= SourceTU
)
156 // This replacement is a duplicate of one suggested by another TU.
159 GroupedReplacements
[*Entry
].push_back(R
);
160 } else if (Warned
.insert(Path
).second
) {
161 errs() << "Described file '" << R
.getFilePath()
162 << "' doesn't exist. Ignoring...\n";
166 for (const auto &TU
: TUs
)
167 for (const tooling::Replacement
&R
: TU
.Replacements
)
168 AddToGroup(R
, nullptr, {});
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
);
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(FileAndReplacements
.second
);
184 return GroupedReplacements
;
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;
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(SM
.getOrCreateFileID(Entry
, SrcMgr::C_User
));
199 tooling::AtomicChange
FileChange(Entry
.getName(), Entry
.getName());
200 for (const auto &R
: FileAndReplacements
.second
) {
202 FileChange
.replace(SM
, BeginLoc
.getLocWithOffset(R
.getOffset()),
203 R
.getLength(), R
.getReplacementText());
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(std::move(Err
)) << "\n";
218 if (IgnoreInsertConflict
) {
219 tooling::Replacements
&Replacements
= FileChange
.getReplacements();
221 Replacements
.getShiftedCodePosition(R
.getOffset());
222 unsigned NewLength
= Replacements
.getShiftedCodePosition(
223 R
.getOffset() + R
.getLength()) -
225 if (NewLength
== R
.getLength()) {
226 tooling::Replacement RR
= tooling::Replacement(
227 R
.getFilePath(), NewOffset
, NewLength
, R
.getReplacementText());
228 Replacements
= Replacements
.merge(tooling::Replacements(RR
));
231 << "Can't resolve conflict, skipping the replacement.\n";
232 ConflictDetected
= true;
235 ConflictDetected
= true;
238 FileChanges
.try_emplace(Entry
,
239 std::vector
<tooling::AtomicChange
>{FileChange
});
242 return !ConflictDetected
;
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
);
252 llvm::ErrorOr
<std::unique_ptr
<MemoryBuffer
>> Buffer
=
253 SM
.getFileManager().getBufferForFile(File
);
255 return errorCodeToError(Buffer
.getError());
256 return tooling::applyAtomicChanges(File
, Buffer
.get()->getBuffer(), Changes
,
260 bool deleteReplacementFiles(const TUReplacementFiles
&Files
,
261 clang::DiagnosticsEngine
&Diagnostics
) {
263 for (const auto &Filename
: Files
) {
264 std::error_code Error
= llvm::sys::fs::remove(Filename
);
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";
276 } // end namespace replace
277 } // end namespace clang