369 lines · cpp
1//===--- AtomicChange.cpp - AtomicChange implementation ---------*- C++ -*-===//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-exception6//7//===----------------------------------------------------------------------===//8 9#include "clang/Tooling/Refactoring/AtomicChange.h"10#include "clang/Tooling/ReplacementsYaml.h"11#include "llvm/Support/YAMLTraits.h"12#include <string>13 14LLVM_YAML_IS_SEQUENCE_VECTOR(clang::tooling::AtomicChange)15 16namespace {17/// Helper to (de)serialize an AtomicChange since we don't have direct18/// access to its data members.19/// Data members of a normalized AtomicChange can be directly mapped from/to20/// YAML string.21struct NormalizedAtomicChange {22 NormalizedAtomicChange() = default;23 24 NormalizedAtomicChange(const llvm::yaml::IO &) {}25 26 // This converts AtomicChange's internal implementation of the replacements27 // set to a vector of replacements.28 NormalizedAtomicChange(const llvm::yaml::IO &,29 const clang::tooling::AtomicChange &E)30 : Key(E.getKey()), FilePath(E.getFilePath()), Error(E.getError()),31 InsertedHeaders(E.getInsertedHeaders()),32 RemovedHeaders(E.getRemovedHeaders()),33 Replaces(E.getReplacements().begin(), E.getReplacements().end()) {}34 35 // This is not expected to be called but needed for template instantiation.36 clang::tooling::AtomicChange denormalize(const llvm::yaml::IO &) {37 llvm_unreachable("Do not convert YAML to AtomicChange directly with '>>'. "38 "Use AtomicChange::convertFromYAML instead.");39 }40 std::string Key;41 std::string FilePath;42 std::string Error;43 std::vector<std::string> InsertedHeaders;44 std::vector<std::string> RemovedHeaders;45 std::vector<clang::tooling::Replacement> Replaces;46};47} // anonymous namespace48 49namespace llvm {50namespace yaml {51 52/// Specialized MappingTraits to describe how an AtomicChange is53/// (de)serialized.54template <> struct MappingTraits<NormalizedAtomicChange> {55 static void mapping(IO &Io, NormalizedAtomicChange &Doc) {56 Io.mapRequired("Key", Doc.Key);57 Io.mapRequired("FilePath", Doc.FilePath);58 Io.mapRequired("Error", Doc.Error);59 Io.mapRequired("InsertedHeaders", Doc.InsertedHeaders);60 Io.mapRequired("RemovedHeaders", Doc.RemovedHeaders);61 Io.mapRequired("Replacements", Doc.Replaces);62 }63};64 65/// Specialized MappingTraits to describe how an AtomicChange is66/// (de)serialized.67template <> struct MappingTraits<clang::tooling::AtomicChange> {68 static void mapping(IO &Io, clang::tooling::AtomicChange &Doc) {69 MappingNormalization<NormalizedAtomicChange, clang::tooling::AtomicChange>70 Keys(Io, Doc);71 Io.mapRequired("Key", Keys->Key);72 Io.mapRequired("FilePath", Keys->FilePath);73 Io.mapRequired("Error", Keys->Error);74 Io.mapRequired("InsertedHeaders", Keys->InsertedHeaders);75 Io.mapRequired("RemovedHeaders", Keys->RemovedHeaders);76 Io.mapRequired("Replacements", Keys->Replaces);77 }78};79 80} // end namespace yaml81} // end namespace llvm82 83namespace clang {84namespace tooling {85namespace {86 87// Returns true if there is any line that violates \p ColumnLimit in range88// [Start, End].89bool violatesColumnLimit(llvm::StringRef Code, unsigned ColumnLimit,90 unsigned Start, unsigned End) {91 auto StartPos = Code.rfind('\n', Start);92 StartPos = (StartPos == llvm::StringRef::npos) ? 0 : StartPos + 1;93 94 auto EndPos = Code.find("\n", End);95 if (EndPos == llvm::StringRef::npos)96 EndPos = Code.size();97 98 llvm::SmallVector<llvm::StringRef, 8> Lines;99 Code.substr(StartPos, EndPos - StartPos).split(Lines, '\n');100 for (llvm::StringRef Line : Lines)101 if (Line.size() > ColumnLimit)102 return true;103 return false;104}105 106std::vector<Range>107getRangesForFormatting(llvm::StringRef Code, unsigned ColumnLimit,108 ApplyChangesSpec::FormatOption Format,109 const clang::tooling::Replacements &Replaces) {110 // kNone suppresses formatting entirely.111 if (Format == ApplyChangesSpec::kNone)112 return {};113 std::vector<clang::tooling::Range> Ranges;114 // This works assuming that replacements are ordered by offset.115 // FIXME: use `getAffectedRanges()` to calculate when it does not include '\n'116 // at the end of an insertion in affected ranges.117 int Offset = 0;118 for (const clang::tooling::Replacement &R : Replaces) {119 int Start = R.getOffset() + Offset;120 int End = Start + R.getReplacementText().size();121 if (!R.getReplacementText().empty() &&122 R.getReplacementText().back() == '\n' && R.getLength() == 0 &&123 R.getOffset() > 0 && R.getOffset() <= Code.size() &&124 Code[R.getOffset() - 1] == '\n')125 // If we are inserting at the start of a line and the replacement ends in126 // a newline, we don't need to format the subsequent line.127 --End;128 Offset += R.getReplacementText().size() - R.getLength();129 130 if (Format == ApplyChangesSpec::kAll ||131 violatesColumnLimit(Code, ColumnLimit, Start, End))132 Ranges.emplace_back(Start, End - Start);133 }134 return Ranges;135}136 137inline llvm::Error make_string_error(const llvm::Twine &Message) {138 return llvm::make_error<llvm::StringError>(Message,139 llvm::inconvertibleErrorCode());140}141 142// Creates replacements for inserting/deleting #include headers.143llvm::Expected<Replacements>144createReplacementsForHeaders(llvm::StringRef FilePath, llvm::StringRef Code,145 llvm::ArrayRef<AtomicChange> Changes,146 const format::FormatStyle &Style) {147 // Create header insertion/deletion replacements to be cleaned up148 // (i.e. converted to real insertion/deletion replacements).149 Replacements HeaderReplacements;150 for (const auto &Change : Changes) {151 for (llvm::StringRef Header : Change.getInsertedHeaders()) {152 std::string EscapedHeader =153 Header.starts_with("<") || Header.starts_with("\"")154 ? Header.str()155 : ("\"" + Header + "\"").str();156 std::string ReplacementText = "#include " + EscapedHeader;157 // Offset UINT_MAX and length 0 indicate that the replacement is a header158 // insertion.159 llvm::Error Err = HeaderReplacements.add(160 tooling::Replacement(FilePath, UINT_MAX, 0, ReplacementText));161 if (Err)162 return std::move(Err);163 }164 for (const std::string &Header : Change.getRemovedHeaders()) {165 // Offset UINT_MAX and length 1 indicate that the replacement is a header166 // deletion.167 llvm::Error Err =168 HeaderReplacements.add(Replacement(FilePath, UINT_MAX, 1, Header));169 if (Err)170 return std::move(Err);171 }172 }173 174 // cleanupAroundReplacements() converts header insertions/deletions into175 // actual replacements that add/remove headers at the right location.176 return clang::format::cleanupAroundReplacements(Code, HeaderReplacements,177 Style);178}179 180// Combine replacements in all Changes as a `Replacements`. This ignores the181// file path in all replacements and replaces them with \p FilePath.182llvm::Expected<Replacements>183combineReplacementsInChanges(llvm::StringRef FilePath,184 llvm::ArrayRef<AtomicChange> Changes) {185 Replacements Replaces;186 for (const auto &Change : Changes)187 for (const auto &R : Change.getReplacements())188 if (auto Err = Replaces.add(Replacement(189 FilePath, R.getOffset(), R.getLength(), R.getReplacementText())))190 return std::move(Err);191 return Replaces;192}193 194} // end namespace195 196AtomicChange::AtomicChange(const SourceManager &SM,197 SourceLocation KeyPosition) {198 const FullSourceLoc FullKeyPosition(KeyPosition, SM);199 auto FileIDAndOffset = FullKeyPosition.getSpellingLoc().getDecomposedLoc();200 OptionalFileEntryRef FE = SM.getFileEntryRefForID(FileIDAndOffset.first);201 assert(FE && "Cannot create AtomicChange with invalid location.");202 FilePath = std::string(FE->getName());203 Key = FilePath + ":" + std::to_string(FileIDAndOffset.second);204}205 206AtomicChange::AtomicChange(const SourceManager &SM, SourceLocation KeyPosition,207 llvm::Any M)208 : AtomicChange(SM, KeyPosition) {209 Metadata = std::move(M);210}211 212AtomicChange::AtomicChange(std::string Key, std::string FilePath,213 std::string Error,214 std::vector<std::string> InsertedHeaders,215 std::vector<std::string> RemovedHeaders,216 clang::tooling::Replacements Replaces)217 : Key(std::move(Key)), FilePath(std::move(FilePath)),218 Error(std::move(Error)), InsertedHeaders(std::move(InsertedHeaders)),219 RemovedHeaders(std::move(RemovedHeaders)), Replaces(std::move(Replaces)) {220}221 222bool AtomicChange::operator==(const AtomicChange &Other) const {223 if (Key != Other.Key || FilePath != Other.FilePath || Error != Other.Error)224 return false;225 if (!(Replaces == Other.Replaces))226 return false;227 // FXIME: Compare header insertions/removals.228 return true;229}230 231std::string AtomicChange::toYAMLString() {232 std::string YamlContent;233 llvm::raw_string_ostream YamlContentStream(YamlContent);234 235 llvm::yaml::Output YAML(YamlContentStream);236 YAML << *this;237 return YamlContent;238}239 240AtomicChange AtomicChange::convertFromYAML(llvm::StringRef YAMLContent) {241 NormalizedAtomicChange NE;242 llvm::yaml::Input YAML(YAMLContent);243 YAML >> NE;244 AtomicChange E(NE.Key, NE.FilePath, NE.Error, NE.InsertedHeaders,245 NE.RemovedHeaders, tooling::Replacements());246 for (const auto &R : NE.Replaces) {247 llvm::Error Err = E.Replaces.add(R);248 if (Err)249 llvm_unreachable(250 "Failed to add replacement when Converting YAML to AtomicChange.");251 llvm::consumeError(std::move(Err));252 }253 return E;254}255 256llvm::Error AtomicChange::replace(const SourceManager &SM,257 const CharSourceRange &Range,258 llvm::StringRef ReplacementText) {259 return Replaces.add(Replacement(SM, Range, ReplacementText));260}261 262llvm::Error AtomicChange::replace(const SourceManager &SM, SourceLocation Loc,263 unsigned Length, llvm::StringRef Text) {264 return Replaces.add(Replacement(SM, Loc, Length, Text));265}266 267llvm::Error AtomicChange::insert(const SourceManager &SM, SourceLocation Loc,268 llvm::StringRef Text, bool InsertAfter) {269 if (Text.empty())270 return llvm::Error::success();271 Replacement R(SM, Loc, 0, Text);272 llvm::Error Err = Replaces.add(R);273 if (Err) {274 return llvm::handleErrors(275 std::move(Err), [&](const ReplacementError &RE) -> llvm::Error {276 if (RE.get() != replacement_error::insert_conflict)277 return llvm::make_error<ReplacementError>(RE);278 unsigned NewOffset = Replaces.getShiftedCodePosition(R.getOffset());279 if (!InsertAfter)280 NewOffset -=281 RE.getExistingReplacement()->getReplacementText().size();282 Replacement NewR(R.getFilePath(), NewOffset, 0, Text);283 Replaces = Replaces.merge(Replacements(NewR));284 return llvm::Error::success();285 });286 }287 return llvm::Error::success();288}289 290void AtomicChange::addHeader(llvm::StringRef Header) {291 InsertedHeaders.push_back(std::string(Header));292}293 294void AtomicChange::removeHeader(llvm::StringRef Header) {295 RemovedHeaders.push_back(std::string(Header));296}297 298llvm::Expected<std::string>299applyAtomicChanges(llvm::StringRef FilePath, llvm::StringRef Code,300 llvm::ArrayRef<AtomicChange> Changes,301 const ApplyChangesSpec &Spec) {302 llvm::Expected<Replacements> HeaderReplacements =303 createReplacementsForHeaders(FilePath, Code, Changes, Spec.Style);304 if (!HeaderReplacements)305 return make_string_error(306 "Failed to create replacements for header changes: " +307 llvm::toString(HeaderReplacements.takeError()));308 309 llvm::Expected<Replacements> Replaces =310 combineReplacementsInChanges(FilePath, Changes);311 if (!Replaces)312 return make_string_error("Failed to combine replacements in all changes: " +313 llvm::toString(Replaces.takeError()));314 315 Replacements AllReplaces = std::move(*Replaces);316 for (const auto &R : *HeaderReplacements) {317 llvm::Error Err = AllReplaces.add(R);318 if (Err)319 return make_string_error(320 "Failed to combine existing replacements with header replacements: " +321 llvm::toString(std::move(Err)));322 }323 324 if (Spec.Cleanup) {325 llvm::Expected<Replacements> CleanReplaces =326 format::cleanupAroundReplacements(Code, AllReplaces, Spec.Style);327 if (!CleanReplaces)328 return make_string_error("Failed to cleanup around replacements: " +329 llvm::toString(CleanReplaces.takeError()));330 AllReplaces = std::move(*CleanReplaces);331 }332 333 // Apply all replacements.334 llvm::Expected<std::string> ChangedCode =335 applyAllReplacements(Code, AllReplaces);336 if (!ChangedCode)337 return make_string_error("Failed to apply all replacements: " +338 llvm::toString(ChangedCode.takeError()));339 340 // Sort inserted headers. This is done even if other formatting is turned off341 // as incorrectly sorted headers are always just wrong, it's not a matter of342 // taste.343 Replacements HeaderSortingReplacements = format::sortIncludes(344 Spec.Style, *ChangedCode, AllReplaces.getAffectedRanges(), FilePath);345 ChangedCode = applyAllReplacements(*ChangedCode, HeaderSortingReplacements);346 if (!ChangedCode)347 return make_string_error(348 "Failed to apply replacements for sorting includes: " +349 llvm::toString(ChangedCode.takeError()));350 351 AllReplaces = AllReplaces.merge(HeaderSortingReplacements);352 353 std::vector<Range> FormatRanges = getRangesForFormatting(354 *ChangedCode, Spec.Style.ColumnLimit, Spec.Format, AllReplaces);355 if (!FormatRanges.empty()) {356 Replacements FormatReplacements =357 format::reformat(Spec.Style, *ChangedCode, FormatRanges, FilePath);358 ChangedCode = applyAllReplacements(*ChangedCode, FormatReplacements);359 if (!ChangedCode)360 return make_string_error(361 "Failed to apply replacements for formatting changed code: " +362 llvm::toString(ChangedCode.takeError()));363 }364 return ChangedCode;365}366 367} // end namespace tooling368} // end namespace clang369