1//===- Commit.cpp - A unit of edits ---------------------------------------===//
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#include "clang/Edit/Commit.h"
10#include "clang/Basic/LLVM.h"
11#include "clang/Basic/SourceLocation.h"
12#include "clang/Basic/SourceManager.h"
13#include "clang/Edit/EditedSource.h"
14#include "clang/Edit/FileOffset.h"
15#include "clang/Lex/Lexer.h"
16#include "clang/Lex/PPConditionalDirectiveRecord.h"
17#include "llvm/ADT/StringRef.h"
18#include <cassert>
19
20using namespace clang;
21using namespace edit;
22
23SourceLocation Commit::Edit::getFileLocation(SourceManager &SM) const {
24 SourceLocation Loc = SM.getLocForStartOfFile(FID: Offset.getFID());
25 Loc = Loc.getLocWithOffset(Offset: Offset.getOffset());
26 assert(Loc.isFileID());
27 return Loc;
28}
29
30CharSourceRange Commit::Edit::getFileRange(SourceManager &SM) const {
31 SourceLocation Loc = getFileLocation(SM);
32 return CharSourceRange::getCharRange(B: Loc, E: Loc.getLocWithOffset(Offset: Length));
33}
34
35CharSourceRange Commit::Edit::getInsertFromRange(SourceManager &SM) const {
36 SourceLocation Loc = SM.getLocForStartOfFile(FID: InsertFromRangeOffs.getFID());
37 Loc = Loc.getLocWithOffset(Offset: InsertFromRangeOffs.getOffset());
38 assert(Loc.isFileID());
39 return CharSourceRange::getCharRange(B: Loc, E: Loc.getLocWithOffset(Offset: Length));
40}
41
42Commit::Commit(EditedSource &Editor)
43 : SourceMgr(Editor.getSourceManager()), LangOpts(Editor.getLangOpts()),
44 PPRec(Editor.getPPCondDirectiveRecord()),
45 Editor(&Editor) {}
46
47bool Commit::insert(SourceLocation loc, StringRef text,
48 bool afterToken, bool beforePreviousInsertions) {
49 if (text.empty())
50 return true;
51
52 FileOffset Offs;
53 if ((!afterToken && !canInsert(loc, Offset&: Offs)) ||
54 ( afterToken && !canInsertAfterToken(loc, Offset&: Offs, AfterLoc&: loc))) {
55 IsCommitable = false;
56 return false;
57 }
58
59 addInsert(OrigLoc: loc, Offs, text, beforePreviousInsertions);
60 return true;
61}
62
63bool Commit::insertFromRange(SourceLocation loc,
64 CharSourceRange range,
65 bool afterToken, bool beforePreviousInsertions) {
66 FileOffset RangeOffs;
67 unsigned RangeLen;
68 if (!canRemoveRange(range, Offs&: RangeOffs, Len&: RangeLen)) {
69 IsCommitable = false;
70 return false;
71 }
72
73 FileOffset Offs;
74 if ((!afterToken && !canInsert(loc, Offset&: Offs)) ||
75 ( afterToken && !canInsertAfterToken(loc, Offset&: Offs, AfterLoc&: loc))) {
76 IsCommitable = false;
77 return false;
78 }
79
80 if (PPRec &&
81 PPRec->areInDifferentConditionalDirectiveRegion(LHS: loc, RHS: range.getBegin())) {
82 IsCommitable = false;
83 return false;
84 }
85
86 addInsertFromRange(OrigLoc: loc, Offs, RangeOffs, RangeLen, beforePreviousInsertions);
87 return true;
88}
89
90bool Commit::remove(CharSourceRange range) {
91 FileOffset Offs;
92 unsigned Len;
93 if (!canRemoveRange(range, Offs, Len)) {
94 IsCommitable = false;
95 return false;
96 }
97
98 addRemove(OrigLoc: range.getBegin(), Offs, Len);
99 return true;
100}
101
102bool Commit::insertWrap(StringRef before, CharSourceRange range,
103 StringRef after) {
104 bool commitableBefore = insert(loc: range.getBegin(), text: before, /*afterToken=*/false,
105 /*beforePreviousInsertions=*/true);
106 bool commitableAfter;
107 if (range.isTokenRange())
108 commitableAfter = insertAfterToken(loc: range.getEnd(), text: after);
109 else
110 commitableAfter = insert(loc: range.getEnd(), text: after);
111
112 return commitableBefore && commitableAfter;
113}
114
115bool Commit::replace(CharSourceRange range, StringRef text) {
116 if (text.empty())
117 return remove(range);
118
119 FileOffset Offs;
120 unsigned Len;
121 if (!canInsert(loc: range.getBegin(), Offset&: Offs) || !canRemoveRange(range, Offs, Len)) {
122 IsCommitable = false;
123 return false;
124 }
125
126 addRemove(OrigLoc: range.getBegin(), Offs, Len);
127 addInsert(OrigLoc: range.getBegin(), Offs, text, beforePreviousInsertions: false);
128 return true;
129}
130
131bool Commit::replaceWithInner(CharSourceRange range,
132 CharSourceRange replacementRange) {
133 FileOffset OuterBegin;
134 unsigned OuterLen;
135 if (!canRemoveRange(range, Offs&: OuterBegin, Len&: OuterLen)) {
136 IsCommitable = false;
137 return false;
138 }
139
140 FileOffset InnerBegin;
141 unsigned InnerLen;
142 if (!canRemoveRange(range: replacementRange, Offs&: InnerBegin, Len&: InnerLen)) {
143 IsCommitable = false;
144 return false;
145 }
146
147 FileOffset OuterEnd = OuterBegin.getWithOffset(offset: OuterLen);
148 FileOffset InnerEnd = InnerBegin.getWithOffset(offset: InnerLen);
149 if (OuterBegin.getFID() != InnerBegin.getFID() ||
150 InnerBegin < OuterBegin ||
151 InnerBegin > OuterEnd ||
152 InnerEnd > OuterEnd) {
153 IsCommitable = false;
154 return false;
155 }
156
157 addRemove(OrigLoc: range.getBegin(),
158 Offs: OuterBegin, Len: InnerBegin.getOffset() - OuterBegin.getOffset());
159 addRemove(OrigLoc: replacementRange.getEnd(),
160 Offs: InnerEnd, Len: OuterEnd.getOffset() - InnerEnd.getOffset());
161 return true;
162}
163
164bool Commit::replaceText(SourceLocation loc, StringRef text,
165 StringRef replacementText) {
166 if (text.empty() || replacementText.empty())
167 return true;
168
169 FileOffset Offs;
170 unsigned Len;
171 if (!canReplaceText(loc, text: replacementText, Offs, Len)) {
172 IsCommitable = false;
173 return false;
174 }
175
176 addRemove(OrigLoc: loc, Offs, Len);
177 addInsert(OrigLoc: loc, Offs, text, beforePreviousInsertions: false);
178 return true;
179}
180
181void Commit::addInsert(SourceLocation OrigLoc, FileOffset Offs, StringRef text,
182 bool beforePreviousInsertions) {
183 if (text.empty())
184 return;
185
186 Edit data;
187 data.Kind = Act_Insert;
188 data.OrigLoc = OrigLoc;
189 data.Offset = Offs;
190 data.Text = text.copy(A&: StrAlloc);
191 data.BeforePrev = beforePreviousInsertions;
192 CachedEdits.push_back(Elt: data);
193}
194
195void Commit::addInsertFromRange(SourceLocation OrigLoc, FileOffset Offs,
196 FileOffset RangeOffs, unsigned RangeLen,
197 bool beforePreviousInsertions) {
198 if (RangeLen == 0)
199 return;
200
201 Edit data;
202 data.Kind = Act_InsertFromRange;
203 data.OrigLoc = OrigLoc;
204 data.Offset = Offs;
205 data.InsertFromRangeOffs = RangeOffs;
206 data.Length = RangeLen;
207 data.BeforePrev = beforePreviousInsertions;
208 CachedEdits.push_back(Elt: data);
209}
210
211void Commit::addRemove(SourceLocation OrigLoc,
212 FileOffset Offs, unsigned Len) {
213 if (Len == 0)
214 return;
215
216 Edit data;
217 data.Kind = Act_Remove;
218 data.OrigLoc = OrigLoc;
219 data.Offset = Offs;
220 data.Length = Len;
221 CachedEdits.push_back(Elt: data);
222}
223
224bool Commit::canInsert(SourceLocation loc, FileOffset &offs) {
225 if (loc.isInvalid())
226 return false;
227
228 if (loc.isMacroID())
229 isAtStartOfMacroExpansion(loc, MacroBegin: &loc);
230
231 const SourceManager &SM = SourceMgr;
232 loc = SM.getTopMacroCallerLoc(Loc: loc);
233
234 if (loc.isMacroID())
235 if (!isAtStartOfMacroExpansion(loc, MacroBegin: &loc))
236 return false;
237
238 if (SM.isInSystemHeader(Loc: loc))
239 return false;
240
241 FileIDAndOffset locInfo = SM.getDecomposedLoc(Loc: loc);
242 if (locInfo.first.isInvalid())
243 return false;
244 offs = FileOffset(locInfo.first, locInfo.second);
245 return canInsertInOffset(OrigLoc: loc, Offs: offs);
246}
247
248bool Commit::canInsertAfterToken(SourceLocation loc, FileOffset &offs,
249 SourceLocation &AfterLoc) {
250 if (loc.isInvalid())
251
252 return false;
253
254 SourceLocation spellLoc = SourceMgr.getSpellingLoc(Loc: loc);
255 unsigned tokLen = Lexer::MeasureTokenLength(Loc: spellLoc, SM: SourceMgr, LangOpts);
256 AfterLoc = loc.getLocWithOffset(Offset: tokLen);
257
258 if (loc.isMacroID())
259 isAtEndOfMacroExpansion(loc, MacroEnd: &loc);
260
261 const SourceManager &SM = SourceMgr;
262 loc = SM.getTopMacroCallerLoc(Loc: loc);
263
264 if (loc.isMacroID())
265 if (!isAtEndOfMacroExpansion(loc, MacroEnd: &loc))
266 return false;
267
268 if (SM.isInSystemHeader(Loc: loc))
269 return false;
270
271 loc = Lexer::getLocForEndOfToken(Loc: loc, Offset: 0, SM: SourceMgr, LangOpts);
272 if (loc.isInvalid())
273 return false;
274
275 FileIDAndOffset locInfo = SM.getDecomposedLoc(Loc: loc);
276 if (locInfo.first.isInvalid())
277 return false;
278 offs = FileOffset(locInfo.first, locInfo.second);
279 return canInsertInOffset(OrigLoc: loc, Offs: offs);
280}
281
282bool Commit::canInsertInOffset(SourceLocation OrigLoc, FileOffset Offs) {
283 for (const auto &act : CachedEdits)
284 if (act.Kind == Act_Remove) {
285 if (act.Offset.getFID() == Offs.getFID() &&
286 Offs > act.Offset && Offs < act.Offset.getWithOffset(offset: act.Length))
287 return false; // position has been removed.
288 }
289
290 if (!Editor)
291 return true;
292 return Editor->canInsertInOffset(OrigLoc, Offs);
293}
294
295bool Commit::canRemoveRange(CharSourceRange range,
296 FileOffset &Offs, unsigned &Len) {
297 const SourceManager &SM = SourceMgr;
298 range = Lexer::makeFileCharRange(Range: range, SM, LangOpts);
299 if (range.isInvalid())
300 return false;
301
302 if (range.getBegin().isMacroID() || range.getEnd().isMacroID())
303 return false;
304 if (SM.isInSystemHeader(Loc: range.getBegin()) ||
305 SM.isInSystemHeader(Loc: range.getEnd()))
306 return false;
307
308 if (PPRec && PPRec->rangeIntersectsConditionalDirective(Range: range.getAsRange()))
309 return false;
310
311 FileIDAndOffset beginInfo = SM.getDecomposedLoc(Loc: range.getBegin());
312 FileIDAndOffset endInfo = SM.getDecomposedLoc(Loc: range.getEnd());
313 if (beginInfo.first != endInfo.first ||
314 beginInfo.second > endInfo.second)
315 return false;
316
317 Offs = FileOffset(beginInfo.first, beginInfo.second);
318 Len = endInfo.second - beginInfo.second;
319 return true;
320}
321
322bool Commit::canReplaceText(SourceLocation loc, StringRef text,
323 FileOffset &Offs, unsigned &Len) {
324 assert(!text.empty());
325
326 if (!canInsert(loc, offs&: Offs))
327 return false;
328
329 // Try to load the file buffer.
330 bool invalidTemp = false;
331 StringRef file = SourceMgr.getBufferData(FID: Offs.getFID(), Invalid: &invalidTemp);
332 if (invalidTemp)
333 return false;
334
335 Len = text.size();
336 return file.substr(Start: Offs.getOffset()).starts_with(Prefix: text);
337}
338
339bool Commit::isAtStartOfMacroExpansion(SourceLocation loc,
340 SourceLocation *MacroBegin) const {
341 return Lexer::isAtStartOfMacroExpansion(loc, SM: SourceMgr, LangOpts, MacroBegin);
342}
343
344bool Commit::isAtEndOfMacroExpansion(SourceLocation loc,
345 SourceLocation *MacroEnd) const {
346 return Lexer::isAtEndOfMacroExpansion(loc, SM: SourceMgr, LangOpts, MacroEnd);
347}
348