| //===- EditedSource.cpp - Collection of source edits ----------------------===// | |
| // | |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| // | |
| //===----------------------------------------------------------------------===// | |
| #include "clang/Edit/EditedSource.h" | |
| #include "clang/Basic/CharInfo.h" | |
| #include "clang/Basic/Diagnostic.h" | |
| #include "clang/Basic/LLVM.h" | |
| #include "clang/Basic/SourceLocation.h" | |
| #include "clang/Basic/SourceManager.h" | |
| #include "clang/Edit/Commit.h" | |
| #include "clang/Edit/EditsReceiver.h" | |
| #include "clang/Edit/FileOffset.h" | |
| #include "clang/Lex/Lexer.h" | |
| #include "llvm/ADT/STLExtras.h" | |
| #include "llvm/ADT/StringRef.h" | |
| #include "llvm/ADT/Twine.h" | |
| #include <cassert> | |
| #include <tuple> | |
| #include <utility> | |
| using namespace clang; | |
| using namespace edit; | |
| void EditsReceiver::remove(CharSourceRange range) { | |
| replace(range, StringRef()); | |
| } | |
| void EditedSource::deconstructMacroArgLoc(SourceLocation Loc, | |
| SourceLocation &ExpansionLoc, | |
| MacroArgUse &ArgUse) { | |
| assert(SourceMgr.isMacroArgExpansion(Loc)); | |
| SourceLocation DefArgLoc = | |
| SourceMgr.getImmediateExpansionRange(Loc).getBegin(); | |
| SourceLocation ImmediateExpansionLoc = | |
| SourceMgr.getImmediateExpansionRange(DefArgLoc).getBegin(); | |
| ExpansionLoc = ImmediateExpansionLoc; | |
| while (SourceMgr.isMacroBodyExpansion(ExpansionLoc)) | |
| ExpansionLoc = | |
| SourceMgr.getImmediateExpansionRange(ExpansionLoc).getBegin(); | |
| SmallString<20> Buf; | |
| StringRef ArgName = Lexer::getSpelling(SourceMgr.getSpellingLoc(DefArgLoc), | |
| Buf, SourceMgr, LangOpts); | |
| ArgUse = MacroArgUse{nullptr, SourceLocation(), SourceLocation()}; | |
| if (!ArgName.empty()) | |
| ArgUse = {&IdentTable.get(ArgName), ImmediateExpansionLoc, | |
| SourceMgr.getSpellingLoc(DefArgLoc)}; | |
| } | |
| void EditedSource::startingCommit() {} | |
| void EditedSource::finishedCommit() { | |
| for (auto &ExpArg : CurrCommitMacroArgExps) { | |
| SourceLocation ExpLoc; | |
| MacroArgUse ArgUse; | |
| std::tie(ExpLoc, ArgUse) = ExpArg; | |
| auto &ArgUses = ExpansionToArgMap[ExpLoc]; | |
| if (!llvm::is_contained(ArgUses, ArgUse)) | |
| ArgUses.push_back(ArgUse); | |
| } | |
| CurrCommitMacroArgExps.clear(); | |
| } | |
| StringRef EditedSource::copyString(const Twine &twine) { | |
| SmallString<128> Data; | |
| return copyString(twine.toStringRef(Data)); | |
| } | |
| bool EditedSource::canInsertInOffset(SourceLocation OrigLoc, FileOffset Offs) { | |
| FileEditsTy::iterator FA = getActionForOffset(Offs); | |
| if (FA != FileEdits.end()) { | |
| if (FA->first != Offs) | |
| return false; // position has been removed. | |
| } | |
| if (SourceMgr.isMacroArgExpansion(OrigLoc)) { | |
| SourceLocation ExpLoc; | |
| MacroArgUse ArgUse; | |
| deconstructMacroArgLoc(OrigLoc, ExpLoc, ArgUse); | |
| auto I = ExpansionToArgMap.find(ExpLoc); | |
| if (I != ExpansionToArgMap.end() && | |
| llvm::any_of(I->second, [&](const MacroArgUse &U) { | |
| return ArgUse.Identifier == U.Identifier && | |
| std::tie(ArgUse.ImmediateExpansionLoc, ArgUse.UseLoc) != | |
| std::tie(U.ImmediateExpansionLoc, U.UseLoc); | |
| })) { | |
| // Trying to write in a macro argument input that has already been | |
| // written by a previous commit for another expansion of the same macro | |
| // argument name. For example: | |
| // | |
| // \code | |
| // #define MAC(x) ((x)+(x)) | |
| // MAC(a) | |
| // \endcode | |
| // | |
| // A commit modified the macro argument 'a' due to the first '(x)' | |
| // expansion inside the macro definition, and a subsequent commit tried | |
| // to modify 'a' again for the second '(x)' expansion. The edits of the | |
| // second commit will be rejected. | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool EditedSource::commitInsert(SourceLocation OrigLoc, | |
| FileOffset Offs, StringRef text, | |
| bool beforePreviousInsertions) { | |
| if (!canInsertInOffset(OrigLoc, Offs)) | |
| return false; | |
| if (text.empty()) | |
| return true; | |
| if (SourceMgr.isMacroArgExpansion(OrigLoc)) { | |
| MacroArgUse ArgUse; | |
| SourceLocation ExpLoc; | |
| deconstructMacroArgLoc(OrigLoc, ExpLoc, ArgUse); | |
| if (ArgUse.Identifier) | |
| CurrCommitMacroArgExps.emplace_back(ExpLoc, ArgUse); | |
| } | |
| FileEdit &FA = FileEdits[Offs]; | |
| if (FA.Text.empty()) { | |
| FA.Text = copyString(text); | |
| return true; | |
| } | |
| if (beforePreviousInsertions) | |
| FA.Text = copyString(Twine(text) + FA.Text); | |
| else | |
| FA.Text = copyString(Twine(FA.Text) + text); | |
| return true; | |
| } | |
| bool EditedSource::commitInsertFromRange(SourceLocation OrigLoc, | |
| FileOffset Offs, | |
| FileOffset InsertFromRangeOffs, unsigned Len, | |
| bool beforePreviousInsertions) { | |
| if (Len == 0) | |
| return true; | |
| SmallString<128> StrVec; | |
| FileOffset BeginOffs = InsertFromRangeOffs; | |
| FileOffset EndOffs = BeginOffs.getWithOffset(Len); | |
| FileEditsTy::iterator I = FileEdits.upper_bound(BeginOffs); | |
| if (I != FileEdits.begin()) | |
| --I; | |
| for (; I != FileEdits.end(); ++I) { | |
| FileEdit &FA = I->second; | |
| FileOffset B = I->first; | |
| FileOffset E = B.getWithOffset(FA.RemoveLen); | |
| if (BeginOffs == B) | |
| break; | |
| if (BeginOffs < E) { | |
| if (BeginOffs > B) { | |
| BeginOffs = E; | |
| ++I; | |
| } | |
| break; | |
| } | |
| } | |
| for (; I != FileEdits.end() && EndOffs > I->first; ++I) { | |
| FileEdit &FA = I->second; | |
| FileOffset B = I->first; | |
| FileOffset E = B.getWithOffset(FA.RemoveLen); | |
| if (BeginOffs < B) { | |
| bool Invalid = false; | |
| StringRef text = getSourceText(BeginOffs, B, Invalid); | |
| if (Invalid) | |
| return false; | |
| StrVec += text; | |
| } | |
| StrVec += FA.Text; | |
| BeginOffs = E; | |
| } | |
| if (BeginOffs < EndOffs) { | |
| bool Invalid = false; | |
| StringRef text = getSourceText(BeginOffs, EndOffs, Invalid); | |
| if (Invalid) | |
| return false; | |
| StrVec += text; | |
| } | |
| return commitInsert(OrigLoc, Offs, StrVec, beforePreviousInsertions); | |
| } | |
| void EditedSource::commitRemove(SourceLocation OrigLoc, | |
| FileOffset BeginOffs, unsigned Len) { | |
| if (Len == 0) | |
| return; | |
| FileOffset EndOffs = BeginOffs.getWithOffset(Len); | |
| FileEditsTy::iterator I = FileEdits.upper_bound(BeginOffs); | |
| if (I != FileEdits.begin()) | |
| --I; | |
| for (; I != FileEdits.end(); ++I) { | |
| FileEdit &FA = I->second; | |
| FileOffset B = I->first; | |
| FileOffset E = B.getWithOffset(FA.RemoveLen); | |
| if (BeginOffs < E) | |
| break; | |
| } | |
| FileOffset TopBegin, TopEnd; | |
| FileEdit *TopFA = nullptr; | |
| if (I == FileEdits.end()) { | |
| FileEditsTy::iterator | |
| NewI = FileEdits.insert(I, std::make_pair(BeginOffs, FileEdit())); | |
| NewI->second.RemoveLen = Len; | |
| return; | |
| } | |
| FileEdit &FA = I->second; | |
| FileOffset B = I->first; | |
| FileOffset E = B.getWithOffset(FA.RemoveLen); | |
| if (BeginOffs < B) { | |
| FileEditsTy::iterator | |
| NewI = FileEdits.insert(I, std::make_pair(BeginOffs, FileEdit())); | |
| TopBegin = BeginOffs; | |
| TopEnd = EndOffs; | |
| TopFA = &NewI->second; | |
| TopFA->RemoveLen = Len; | |
| } else { | |
| TopBegin = B; | |
| TopEnd = E; | |
| TopFA = &I->second; | |
| if (TopEnd >= EndOffs) | |
| return; | |
| unsigned diff = EndOffs.getOffset() - TopEnd.getOffset(); | |
| TopEnd = EndOffs; | |
| TopFA->RemoveLen += diff; | |
| if (B == BeginOffs) | |
| TopFA->Text = StringRef(); | |
| ++I; | |
| } | |
| while (I != FileEdits.end()) { | |
| FileEdit &FA = I->second; | |
| FileOffset B = I->first; | |
| FileOffset E = B.getWithOffset(FA.RemoveLen); | |
| if (B >= TopEnd) | |
| break; | |
| if (E <= TopEnd) { | |
| FileEdits.erase(I++); | |
| continue; | |
| } | |
| if (B < TopEnd) { | |
| unsigned diff = E.getOffset() - TopEnd.getOffset(); | |
| TopEnd = E; | |
| TopFA->RemoveLen += diff; | |
| FileEdits.erase(I); | |
| } | |
| break; | |
| } | |
| } | |
| bool EditedSource::commit(const Commit &commit) { | |
| if (!commit.isCommitable()) | |
| return false; | |
| struct CommitRAII { | |
| EditedSource &Editor; | |
| CommitRAII(EditedSource &Editor) : Editor(Editor) { | |
| Editor.startingCommit(); | |
| } | |
| ~CommitRAII() { | |
| Editor.finishedCommit(); | |
| } | |
| } CommitRAII(*this); | |
| for (edit::Commit::edit_iterator | |
| I = commit.edit_begin(), E = commit.edit_end(); I != E; ++I) { | |
| const edit::Commit::Edit &edit = *I; | |
| switch (edit.Kind) { | |
| case edit::Commit::Act_Insert: | |
| commitInsert(edit.OrigLoc, edit.Offset, edit.Text, edit.BeforePrev); | |
| break; | |
| case edit::Commit::Act_InsertFromRange: | |
| commitInsertFromRange(edit.OrigLoc, edit.Offset, | |
| edit.InsertFromRangeOffs, edit.Length, | |
| edit.BeforePrev); | |
| break; | |
| case edit::Commit::Act_Remove: | |
| commitRemove(edit.OrigLoc, edit.Offset, edit.Length); | |
| break; | |
| } | |
| } | |
| return true; | |
| } | |
| // Returns true if it is ok to make the two given characters adjacent. | |
| static bool canBeJoined(char left, char right, const LangOptions &LangOpts) { | |
| // FIXME: Should use TokenConcatenation to make sure we don't allow stuff like | |
| // making two '<' adjacent. | |
| return !(Lexer::isAsciiIdentifierContinueChar(left, LangOpts) && | |
| Lexer::isAsciiIdentifierContinueChar(right, LangOpts)); | |
| } | |
| /// Returns true if it is ok to eliminate the trailing whitespace between | |
| /// the given characters. | |
| static bool canRemoveWhitespace(char left, char beforeWSpace, char right, | |
| const LangOptions &LangOpts) { | |
| if (!canBeJoined(left, right, LangOpts)) | |
| return false; | |
| if (isWhitespace(left) || isWhitespace(right)) | |
| return true; | |
| if (canBeJoined(beforeWSpace, right, LangOpts)) | |
| return false; // the whitespace was intentional, keep it. | |
| return true; | |
| } | |
| /// Check the range that we are going to remove and: | |
| /// -Remove any trailing whitespace if possible. | |
| /// -Insert a space if removing the range is going to mess up the source tokens. | |
| static void adjustRemoval(const SourceManager &SM, const LangOptions &LangOpts, | |
| SourceLocation Loc, FileOffset offs, | |
| unsigned &len, StringRef &text) { | |
| assert(len && text.empty()); | |
| SourceLocation BeginTokLoc = Lexer::GetBeginningOfToken(Loc, SM, LangOpts); | |
| if (BeginTokLoc != Loc) | |
| return; // the range is not at the beginning of a token, keep the range. | |
| bool Invalid = false; | |
| StringRef buffer = SM.getBufferData(offs.getFID(), &Invalid); | |
| if (Invalid) | |
| return; | |
| unsigned begin = offs.getOffset(); | |
| unsigned end = begin + len; | |
| // Do not try to extend the removal if we're at the end of the buffer already. | |
| if (end == buffer.size()) | |
| return; | |
| assert(begin < buffer.size() && end < buffer.size() && "Invalid range!"); | |
| // FIXME: Remove newline. | |
| if (begin == 0) { | |
| if (buffer[end] == ' ') | |
| ++len; | |
| return; | |
| } | |
| if (buffer[end] == ' ') { | |
| assert((end + 1 != buffer.size() || buffer.data()[end + 1] == 0) && | |
| "buffer not zero-terminated!"); | |
| if (canRemoveWhitespace(/*left=*/buffer[begin-1], | |
| /*beforeWSpace=*/buffer[end-1], | |
| /*right=*/buffer.data()[end + 1], // zero-terminated | |
| LangOpts)) | |
| ++len; | |
| return; | |
| } | |
| if (!canBeJoined(buffer[begin-1], buffer[end], LangOpts)) | |
| text = " "; | |
| } | |
| static void applyRewrite(EditsReceiver &receiver, | |
| StringRef text, FileOffset offs, unsigned len, | |
| const SourceManager &SM, const LangOptions &LangOpts, | |
| bool shouldAdjustRemovals) { | |
| assert(offs.getFID().isValid()); | |
| SourceLocation Loc = SM.getLocForStartOfFile(offs.getFID()); | |
| Loc = Loc.getLocWithOffset(offs.getOffset()); | |
| assert(Loc.isFileID()); | |
| if (text.empty() && shouldAdjustRemovals) | |
| adjustRemoval(SM, LangOpts, Loc, offs, len, text); | |
| CharSourceRange range = CharSourceRange::getCharRange(Loc, | |
| Loc.getLocWithOffset(len)); | |
| if (text.empty()) { | |
| assert(len); | |
| receiver.remove(range); | |
| return; | |
| } | |
| if (len) | |
| receiver.replace(range, text); | |
| else | |
| receiver.insert(Loc, text); | |
| } | |
| void EditedSource::applyRewrites(EditsReceiver &receiver, | |
| bool shouldAdjustRemovals) { | |
| SmallString<128> StrVec; | |
| FileOffset CurOffs, CurEnd; | |
| unsigned CurLen; | |
| if (FileEdits.empty()) | |
| return; | |
| FileEditsTy::iterator I = FileEdits.begin(); | |
| CurOffs = I->first; | |
| StrVec = I->second.Text; | |
| CurLen = I->second.RemoveLen; | |
| CurEnd = CurOffs.getWithOffset(CurLen); | |
| ++I; | |
| for (FileEditsTy::iterator E = FileEdits.end(); I != E; ++I) { | |
| FileOffset offs = I->first; | |
| FileEdit act = I->second; | |
| assert(offs >= CurEnd); | |
| if (offs == CurEnd) { | |
| StrVec += act.Text; | |
| CurLen += act.RemoveLen; | |
| CurEnd.getWithOffset(act.RemoveLen); | |
| continue; | |
| } | |
| applyRewrite(receiver, StrVec, CurOffs, CurLen, SourceMgr, LangOpts, | |
| shouldAdjustRemovals); | |
| CurOffs = offs; | |
| StrVec = act.Text; | |
| CurLen = act.RemoveLen; | |
| CurEnd = CurOffs.getWithOffset(CurLen); | |
| } | |
| applyRewrite(receiver, StrVec, CurOffs, CurLen, SourceMgr, LangOpts, | |
| shouldAdjustRemovals); | |
| } | |
| void EditedSource::clearRewrites() { | |
| FileEdits.clear(); | |
| StrAlloc.Reset(); | |
| } | |
| StringRef EditedSource::getSourceText(FileOffset BeginOffs, FileOffset EndOffs, | |
| bool &Invalid) { | |
| assert(BeginOffs.getFID() == EndOffs.getFID()); | |
| assert(BeginOffs <= EndOffs); | |
| SourceLocation BLoc = SourceMgr.getLocForStartOfFile(BeginOffs.getFID()); | |
| BLoc = BLoc.getLocWithOffset(BeginOffs.getOffset()); | |
| assert(BLoc.isFileID()); | |
| SourceLocation | |
| ELoc = BLoc.getLocWithOffset(EndOffs.getOffset() - BeginOffs.getOffset()); | |
| return Lexer::getSourceText(CharSourceRange::getCharRange(BLoc, ELoc), | |
| SourceMgr, LangOpts, &Invalid); | |
| } | |
| EditedSource::FileEditsTy::iterator | |
| EditedSource::getActionForOffset(FileOffset Offs) { | |
| FileEditsTy::iterator I = FileEdits.upper_bound(Offs); | |
| if (I == FileEdits.begin()) | |
| return FileEdits.end(); | |
| --I; | |
| FileEdit &FA = I->second; | |
| FileOffset B = I->first; | |
| FileOffset E = B.getWithOffset(FA.RemoveLen); | |
| if (Offs >= B && Offs < E) | |
| return I; | |
| return FileEdits.end(); | |
| } | |
| namespace clang::edit { | |
| namespace { | |
| class FixitReceiver : public edit::EditsReceiver { | |
| SmallVectorImpl<FixItHint> &MergedFixits; | |
| public: | |
| FixitReceiver(SmallVectorImpl<FixItHint> &MergedFixits) | |
| : MergedFixits(MergedFixits) {} | |
| void insert(SourceLocation loc, StringRef text) override { | |
| MergedFixits.push_back(FixItHint::CreateInsertion(loc, text)); | |
| } | |
| void replace(CharSourceRange range, StringRef text) override { | |
| MergedFixits.push_back(FixItHint::CreateReplacement(range, text)); | |
| } | |
| }; | |
| } // namespace | |
| void mergeFixits(ArrayRef<FixItHint> FixItHints, const SourceManager &SM, | |
| const LangOptions &LangOpts, | |
| SmallVectorImpl<FixItHint> &MergedFixits) { | |
| MergedFixits.clear(); | |
| edit::Commit commit(SM, LangOpts); | |
| for (const auto &Hint : FixItHints) | |
| if (Hint.CodeToInsert.empty()) { | |
| if (Hint.InsertFromRange.isValid()) | |
| commit.insertFromRange(Hint.RemoveRange.getBegin(), | |
| Hint.InsertFromRange, /*afterToken=*/false, | |
| Hint.BeforePreviousInsertions); | |
| else | |
| commit.remove(Hint.RemoveRange); | |
| } else { | |
| if (Hint.RemoveRange.isTokenRange() || | |
| Hint.RemoveRange.getBegin() != Hint.RemoveRange.getEnd()) | |
| commit.replace(Hint.RemoveRange, Hint.CodeToInsert); | |
| else | |
| commit.insert(Hint.RemoveRange.getBegin(), Hint.CodeToInsert, | |
| /*afterToken=*/false, Hint.BeforePreviousInsertions); | |
| } | |
| edit::EditedSource Editor(SM, LangOpts); | |
| if (Editor.commit(commit)) { | |
| FixitReceiver Rec(MergedFixits); | |
| Editor.applyRewrites(Rec); | |
| } | |
| } | |
| } // namespace clang::edit |