//===--- AtomicChange.cpp - AtomicChange implementation -----------------*- C++ -*-===// // // 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/Tooling/Refactoring/AtomicChange.h" #include "clang/Tooling/ReplacementsYaml.h" #include "llvm/Support/YAMLTraits.h" #include LLVM_YAML_IS_SEQUENCE_VECTOR(clang::tooling::AtomicChange) namespace { /// Helper to (de)serialize an AtomicChange since we don't have direct /// access to its data members. /// Data members of a normalized AtomicChange can be directly mapped from/to /// YAML string. struct NormalizedAtomicChange { NormalizedAtomicChange() = default; NormalizedAtomicChange(const llvm::yaml::IO &) {} // This converts AtomicChange's internal implementation of the replacements // set to a vector of replacements. NormalizedAtomicChange(const llvm::yaml::IO &, const clang::tooling::AtomicChange &E) : Key(E.getKey()), FilePath(E.getFilePath()), Error(E.getError()), InsertedHeaders(E.getInsertedHeaders()), RemovedHeaders(E.getRemovedHeaders()), Replaces(E.getReplacements().begin(), E.getReplacements().end()) {} // This is not expected to be called but needed for template instantiation. clang::tooling::AtomicChange denormalize(const llvm::yaml::IO &) { llvm_unreachable("Do not convert YAML to AtomicChange directly with '>>'. " "Use AtomicChange::convertFromYAML instead."); } std::string Key; std::string FilePath; std::string Error; std::vector InsertedHeaders; std::vector RemovedHeaders; std::vector Replaces; }; } // anonymous namespace namespace llvm { namespace yaml { /// Specialized MappingTraits to describe how an AtomicChange is /// (de)serialized. template <> struct MappingTraits { static void mapping(IO &Io, NormalizedAtomicChange &Doc) { Io.mapRequired("Key", Doc.Key); Io.mapRequired("FilePath", Doc.FilePath); Io.mapRequired("Error", Doc.Error); Io.mapRequired("InsertedHeaders", Doc.InsertedHeaders); Io.mapRequired("RemovedHeaders", Doc.RemovedHeaders); Io.mapRequired("Replacements", Doc.Replaces); } }; /// Specialized MappingTraits to describe how an AtomicChange is /// (de)serialized. template <> struct MappingTraits { static void mapping(IO &Io, clang::tooling::AtomicChange &Doc) { MappingNormalization Keys(Io, Doc); Io.mapRequired("Key", Keys->Key); Io.mapRequired("FilePath", Keys->FilePath); Io.mapRequired("Error", Keys->Error); Io.mapRequired("InsertedHeaders", Keys->InsertedHeaders); Io.mapRequired("RemovedHeaders", Keys->RemovedHeaders); Io.mapRequired("Replacements", Keys->Replaces); } }; } // end namespace yaml } // end namespace llvm namespace clang { namespace tooling { namespace { // Returns true if there is any line that violates \p ColumnLimit in range // [Start, End]. bool violatesColumnLimit(llvm::StringRef Code, unsigned ColumnLimit, unsigned Start, unsigned End) { auto StartPos = Code.rfind('\n', Start); StartPos = (StartPos == llvm::StringRef::npos) ? 0 : StartPos + 1; auto EndPos = Code.find("\n", End); if (EndPos == llvm::StringRef::npos) EndPos = Code.size(); llvm::SmallVector Lines; Code.substr(StartPos, EndPos - StartPos).split(Lines, '\n'); for (llvm::StringRef Line : Lines) if (Line.size() > ColumnLimit) return true; return false; } std::vector getRangesForFormating(llvm::StringRef Code, unsigned ColumnLimit, ApplyChangesSpec::FormatOption Format, const clang::tooling::Replacements &Replaces) { // kNone suppresses formatting entirely. if (Format == ApplyChangesSpec::kNone) return {}; std::vector Ranges; // This works assuming that replacements are ordered by offset. // FIXME: use `getAffectedRanges()` to calculate when it does not include '\n' // at the end of an insertion in affected ranges. int Offset = 0; for (const clang::tooling::Replacement &R : Replaces) { int Start = R.getOffset() + Offset; int End = Start + R.getReplacementText().size(); if (!R.getReplacementText().empty() && R.getReplacementText().back() == '\n' && R.getLength() == 0 && R.getOffset() > 0 && R.getOffset() <= Code.size() && Code[R.getOffset() - 1] == '\n') // If we are inserting at the start of a line and the replacement ends in // a newline, we don't need to format the subsequent line. --End; Offset += R.getReplacementText().size() - R.getLength(); if (Format == ApplyChangesSpec::kAll || violatesColumnLimit(Code, ColumnLimit, Start, End)) Ranges.emplace_back(Start, End - Start); } return Ranges; } inline llvm::Error make_string_error(const llvm::Twine &Message) { return llvm::make_error(Message, llvm::inconvertibleErrorCode()); } // Creates replacements for inserting/deleting #include headers. llvm::Expected createReplacementsForHeaders(llvm::StringRef FilePath, llvm::StringRef Code, llvm::ArrayRef Changes, const format::FormatStyle &Style) { // Create header insertion/deletion replacements to be cleaned up // (i.e. converted to real insertion/deletion replacements). Replacements HeaderReplacements; for (const auto &Change : Changes) { for (llvm::StringRef Header : Change.getInsertedHeaders()) { std::string EscapedHeader = Header.startswith("<") || Header.startswith("\"") ? Header.str() : ("\"" + Header + "\"").str(); std::string ReplacementText = "#include " + EscapedHeader; // Offset UINT_MAX and length 0 indicate that the replacement is a header // insertion. llvm::Error Err = HeaderReplacements.add( tooling::Replacement(FilePath, UINT_MAX, 0, ReplacementText)); if (Err) return std::move(Err); } for (const std::string &Header : Change.getRemovedHeaders()) { // Offset UINT_MAX and length 1 indicate that the replacement is a header // deletion. llvm::Error Err = HeaderReplacements.add(Replacement(FilePath, UINT_MAX, 1, Header)); if (Err) return std::move(Err); } } // cleanupAroundReplacements() converts header insertions/deletions into // actual replacements that add/remove headers at the right location. return clang::format::cleanupAroundReplacements(Code, HeaderReplacements, Style); } // Combine replacements in all Changes as a `Replacements`. This ignores the // file path in all replacements and replaces them with \p FilePath. llvm::Expected combineReplacementsInChanges(llvm::StringRef FilePath, llvm::ArrayRef Changes) { Replacements Replaces; for (const auto &Change : Changes) for (const auto &R : Change.getReplacements()) if (auto Err = Replaces.add(Replacement( FilePath, R.getOffset(), R.getLength(), R.getReplacementText()))) return std::move(Err); return Replaces; } } // end namespace AtomicChange::AtomicChange(const SourceManager &SM, SourceLocation KeyPosition) { const FullSourceLoc FullKeyPosition(KeyPosition, SM); std::pair FileIDAndOffset = FullKeyPosition.getSpellingLoc().getDecomposedLoc(); const FileEntry *FE = SM.getFileEntryForID(FileIDAndOffset.first); assert(FE && "Cannot create AtomicChange with invalid location."); FilePath = FE->getName(); Key = FilePath + ":" + std::to_string(FileIDAndOffset.second); } AtomicChange::AtomicChange(std::string Key, std::string FilePath, std::string Error, std::vector InsertedHeaders, std::vector RemovedHeaders, clang::tooling::Replacements Replaces) : Key(std::move(Key)), FilePath(std::move(FilePath)), Error(std::move(Error)), InsertedHeaders(std::move(InsertedHeaders)), RemovedHeaders(std::move(RemovedHeaders)), Replaces(std::move(Replaces)) { } bool AtomicChange::operator==(const AtomicChange &Other) const { if (Key != Other.Key || FilePath != Other.FilePath || Error != Other.Error) return false; if (!(Replaces == Other.Replaces)) return false; // FXIME: Compare header insertions/removals. return true; } std::string AtomicChange::toYAMLString() { std::string YamlContent; llvm::raw_string_ostream YamlContentStream(YamlContent); llvm::yaml::Output YAML(YamlContentStream); YAML << *this; YamlContentStream.flush(); return YamlContent; } AtomicChange AtomicChange::convertFromYAML(llvm::StringRef YAMLContent) { NormalizedAtomicChange NE; llvm::yaml::Input YAML(YAMLContent); YAML >> NE; AtomicChange E(NE.Key, NE.FilePath, NE.Error, NE.InsertedHeaders, NE.RemovedHeaders, tooling::Replacements()); for (const auto &R : NE.Replaces) { llvm::Error Err = E.Replaces.add(R); if (Err) llvm_unreachable( "Failed to add replacement when Converting YAML to AtomicChange."); llvm::consumeError(std::move(Err)); } return E; } llvm::Error AtomicChange::replace(const SourceManager &SM, const CharSourceRange &Range, llvm::StringRef ReplacementText) { return Replaces.add(Replacement(SM, Range, ReplacementText)); } llvm::Error AtomicChange::replace(const SourceManager &SM, SourceLocation Loc, unsigned Length, llvm::StringRef Text) { return Replaces.add(Replacement(SM, Loc, Length, Text)); } llvm::Error AtomicChange::insert(const SourceManager &SM, SourceLocation Loc, llvm::StringRef Text, bool InsertAfter) { if (Text.empty()) return llvm::Error::success(); Replacement R(SM, Loc, 0, Text); llvm::Error Err = Replaces.add(R); if (Err) { return llvm::handleErrors( std::move(Err), [&](const ReplacementError &RE) -> llvm::Error { if (RE.get() != replacement_error::insert_conflict) return llvm::make_error(RE); unsigned NewOffset = Replaces.getShiftedCodePosition(R.getOffset()); if (!InsertAfter) NewOffset -= RE.getExistingReplacement()->getReplacementText().size(); Replacement NewR(R.getFilePath(), NewOffset, 0, Text); Replaces = Replaces.merge(Replacements(NewR)); return llvm::Error::success(); }); } return llvm::Error::success(); } void AtomicChange::addHeader(llvm::StringRef Header) { InsertedHeaders.push_back(Header); } void AtomicChange::removeHeader(llvm::StringRef Header) { RemovedHeaders.push_back(Header); } llvm::Expected applyAtomicChanges(llvm::StringRef FilePath, llvm::StringRef Code, llvm::ArrayRef Changes, const ApplyChangesSpec &Spec) { llvm::Expected HeaderReplacements = createReplacementsForHeaders(FilePath, Code, Changes, Spec.Style); if (!HeaderReplacements) return make_string_error( "Failed to create replacements for header changes: " + llvm::toString(HeaderReplacements.takeError())); llvm::Expected Replaces = combineReplacementsInChanges(FilePath, Changes); if (!Replaces) return make_string_error("Failed to combine replacements in all changes: " + llvm::toString(Replaces.takeError())); Replacements AllReplaces = std::move(*Replaces); for (const auto &R : *HeaderReplacements) { llvm::Error Err = AllReplaces.add(R); if (Err) return make_string_error( "Failed to combine existing replacements with header replacements: " + llvm::toString(std::move(Err))); } if (Spec.Cleanup) { llvm::Expected CleanReplaces = format::cleanupAroundReplacements(Code, AllReplaces, Spec.Style); if (!CleanReplaces) return make_string_error("Failed to cleanup around replacements: " + llvm::toString(CleanReplaces.takeError())); AllReplaces = std::move(*CleanReplaces); } // Apply all replacements. llvm::Expected ChangedCode = applyAllReplacements(Code, AllReplaces); if (!ChangedCode) return make_string_error("Failed to apply all replacements: " + llvm::toString(ChangedCode.takeError())); // Sort inserted headers. This is done even if other formatting is turned off // as incorrectly sorted headers are always just wrong, it's not a matter of // taste. Replacements HeaderSortingReplacements = format::sortIncludes( Spec.Style, *ChangedCode, AllReplaces.getAffectedRanges(), FilePath); ChangedCode = applyAllReplacements(*ChangedCode, HeaderSortingReplacements); if (!ChangedCode) return make_string_error( "Failed to apply replacements for sorting includes: " + llvm::toString(ChangedCode.takeError())); AllReplaces = AllReplaces.merge(HeaderSortingReplacements); std::vector FormatRanges = getRangesForFormating( *ChangedCode, Spec.Style.ColumnLimit, Spec.Format, AllReplaces); if (!FormatRanges.empty()) { Replacements FormatReplacements = format::reformat(Spec.Style, *ChangedCode, FormatRanges, FilePath); ChangedCode = applyAllReplacements(*ChangedCode, FormatReplacements); if (!ChangedCode) return make_string_error( "Failed to apply replacements for formatting changed code: " + llvm::toString(ChangedCode.takeError())); } return ChangedCode; } } // end namespace tooling } // end namespace clang