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