blob: 9d792272ab9489f30c22c1005246fc3a65dbf8b4 [file] [log] [blame]
//===---------- IncludeSorter.cpp - clang-tidy ----------------------------===//
//
// 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 "IncludeSorter.h"
#include "clang/Lex/Lexer.h"
namespace clang {
namespace tidy {
namespace utils {
namespace {
StringRef RemoveFirstSuffix(StringRef Str, ArrayRef<const char *> Suffixes) {
for (StringRef Suffix : Suffixes) {
if (Str.endswith(Suffix)) {
return Str.substr(0, Str.size() - Suffix.size());
}
}
return Str;
}
StringRef MakeCanonicalName(StringRef Str, IncludeSorter::IncludeStyle Style) {
// The list of suffixes to remove from source file names to get the
// "canonical" file names.
// E.g. tools/sort_includes.cc and tools/sort_includes_test.cc
// would both canonicalize to tools/sort_includes and tools/sort_includes.h
// (once canonicalized) will match as being the main include file associated
// with the source files.
if (Style == IncludeSorter::IS_LLVM) {
return RemoveFirstSuffix(
RemoveFirstSuffix(Str, {".cc", ".cpp", ".c", ".h", ".hpp"}), {"Test"});
}
return RemoveFirstSuffix(
RemoveFirstSuffix(Str, {".cc", ".cpp", ".c", ".h", ".hpp"}),
{"_unittest", "_regtest", "_test"});
}
// Scan to the end of the line and return the offset of the next line.
size_t FindNextLine(const char *Text) {
size_t EOLIndex = std::strcspn(Text, "\n");
return Text[EOLIndex] == '\0' ? EOLIndex : EOLIndex + 1;
}
IncludeSorter::IncludeKinds
DetermineIncludeKind(StringRef CanonicalFile, StringRef IncludeFile,
bool IsAngled, IncludeSorter::IncludeStyle Style) {
// Compute the two "canonical" forms of the include's filename sans extension.
// The first form is the include's filename without ".h" or "-inl.h" at the
// end. The second form is the first form with "/public/" in the file path
// replaced by "/internal/".
if (IsAngled) {
// If the system include (<foo>) ends with ".h", then it is a normal C-style
// include. Otherwise assume it is a C++-style extensionless include.
return IncludeFile.endswith(".h") ? IncludeSorter::IK_CSystemInclude
: IncludeSorter::IK_CXXSystemInclude;
}
StringRef CanonicalInclude = MakeCanonicalName(IncludeFile, Style);
if (CanonicalFile.endswith(CanonicalInclude)
|| CanonicalInclude.endswith(CanonicalFile)) {
return IncludeSorter::IK_MainTUInclude;
}
if (Style == IncludeSorter::IS_Google) {
std::pair<StringRef, StringRef> Parts = CanonicalInclude.split("/public/");
std::string AltCanonicalInclude =
Parts.first.str() + "/internal/" + Parts.second.str();
std::string ProtoCanonicalInclude =
Parts.first.str() + "/proto/" + Parts.second.str();
// Determine the kind of this inclusion.
if (CanonicalFile.equals(AltCanonicalInclude) ||
CanonicalFile.equals(ProtoCanonicalInclude)) {
return IncludeSorter::IK_MainTUInclude;
}
}
return IncludeSorter::IK_NonSystemInclude;
}
} // namespace
IncludeSorter::IncludeSorter(const SourceManager *SourceMgr,
const LangOptions *LangOpts, const FileID FileID,
StringRef FileName, IncludeStyle Style)
: SourceMgr(SourceMgr), LangOpts(LangOpts), Style(Style),
CurrentFileID(FileID), CanonicalFile(MakeCanonicalName(FileName, Style)) {
}
void IncludeSorter::AddInclude(StringRef FileName, bool IsAngled,
SourceLocation HashLocation,
SourceLocation EndLocation) {
int Offset = FindNextLine(SourceMgr->getCharacterData(EndLocation));
// Record the relevant location information for this inclusion directive.
IncludeLocations[FileName].push_back(
SourceRange(HashLocation, EndLocation.getLocWithOffset(Offset)));
SourceLocations.push_back(IncludeLocations[FileName].back());
// Stop if this inclusion is a duplicate.
if (IncludeLocations[FileName].size() > 1)
return;
// Add the included file's name to the appropriate bucket.
IncludeKinds Kind =
DetermineIncludeKind(CanonicalFile, FileName, IsAngled, Style);
if (Kind != IK_InvalidInclude)
IncludeBucket[Kind].push_back(FileName.str());
}
Optional<FixItHint> IncludeSorter::CreateIncludeInsertion(StringRef FileName,
bool IsAngled) {
std::string IncludeStmt =
IsAngled ? llvm::Twine("#include <" + FileName + ">\n").str()
: llvm::Twine("#include \"" + FileName + "\"\n").str();
if (SourceLocations.empty()) {
// If there are no includes in this file, add it in the first line.
// FIXME: insert after the file comment or the header guard, if present.
IncludeStmt.append("\n");
return FixItHint::CreateInsertion(
SourceMgr->getLocForStartOfFile(CurrentFileID), IncludeStmt);
}
auto IncludeKind =
DetermineIncludeKind(CanonicalFile, FileName, IsAngled, Style);
if (!IncludeBucket[IncludeKind].empty()) {
for (const std::string &IncludeEntry : IncludeBucket[IncludeKind]) {
if (FileName < IncludeEntry) {
const auto &Location = IncludeLocations[IncludeEntry][0];
return FixItHint::CreateInsertion(Location.getBegin(), IncludeStmt);
} else if (FileName == IncludeEntry) {
return llvm::None;
}
}
// FileName comes after all include entries in bucket, insert it after
// last.
const std::string &LastInclude = IncludeBucket[IncludeKind].back();
SourceRange LastIncludeLocation = IncludeLocations[LastInclude].back();
return FixItHint::CreateInsertion(LastIncludeLocation.getEnd(),
IncludeStmt);
}
// Find the non-empty include bucket to be sorted directly above
// 'IncludeKind'. If such a bucket exists, we'll want to sort the include
// after that bucket. If no such bucket exists, find the first non-empty
// include bucket in the file. In that case, we'll want to sort the include
// before that bucket.
IncludeKinds NonEmptyKind = IK_InvalidInclude;
for (int i = IK_InvalidInclude - 1; i >= 0; --i) {
if (!IncludeBucket[i].empty()) {
NonEmptyKind = static_cast<IncludeKinds>(i);
if (NonEmptyKind < IncludeKind)
break;
}
}
if (NonEmptyKind == IK_InvalidInclude) {
return llvm::None;
}
if (NonEmptyKind < IncludeKind) {
// Create a block after.
const std::string &LastInclude = IncludeBucket[NonEmptyKind].back();
SourceRange LastIncludeLocation = IncludeLocations[LastInclude].back();
IncludeStmt = '\n' + IncludeStmt;
return FixItHint::CreateInsertion(LastIncludeLocation.getEnd(),
IncludeStmt);
}
// Create a block before.
const std::string &FirstInclude = IncludeBucket[NonEmptyKind][0];
SourceRange FirstIncludeLocation = IncludeLocations[FirstInclude].back();
IncludeStmt.append("\n");
return FixItHint::CreateInsertion(FirstIncludeLocation.getBegin(),
IncludeStmt);
}
std::vector<FixItHint> IncludeSorter::GetEdits() {
if (SourceLocations.empty())
return {};
typedef std::map<int, std::pair<SourceRange, std::string>>
FileLineToSourceEditMap;
FileLineToSourceEditMap Edits;
auto SourceLocationIterator = SourceLocations.begin();
auto SourceLocationIteratorEnd = SourceLocations.end();
// Compute the Edits that need to be done to each line to add, replace, or
// delete inclusions.
for (int IncludeKind = 0; IncludeKind < IK_InvalidInclude; ++IncludeKind) {
std::sort(IncludeBucket[IncludeKind].begin(),
IncludeBucket[IncludeKind].end());
for (const auto &IncludeEntry : IncludeBucket[IncludeKind]) {
auto &Location = IncludeLocations[IncludeEntry];
SourceRangeVector::iterator LocationIterator = Location.begin();
SourceRangeVector::iterator LocationIteratorEnd = Location.end();
SourceRange FirstLocation = *LocationIterator;
// If the first occurrence of a particular include is on the current
// source line we are examining, leave it alone.
if (FirstLocation == *SourceLocationIterator)
++LocationIterator;
// Add the deletion Edits for any (remaining) instances of this inclusion,
// and remove their Locations from the source Locations to be processed.
for (; LocationIterator != LocationIteratorEnd; ++LocationIterator) {
int LineNumber =
SourceMgr->getSpellingLineNumber(LocationIterator->getBegin());
Edits[LineNumber] = std::make_pair(*LocationIterator, "");
SourceLocationIteratorEnd =
std::remove(SourceLocationIterator, SourceLocationIteratorEnd,
*LocationIterator);
}
if (FirstLocation == *SourceLocationIterator) {
// Do nothing except move to the next source Location (Location of an
// inclusion in the original, unchanged source file).
++SourceLocationIterator;
continue;
}
// Add (or append to) the replacement text for this line in source file.
int LineNumber =
SourceMgr->getSpellingLineNumber(SourceLocationIterator->getBegin());
if (Edits.find(LineNumber) == Edits.end()) {
Edits[LineNumber].first =
SourceRange(SourceLocationIterator->getBegin());
}
StringRef SourceText = Lexer::getSourceText(
CharSourceRange::getCharRange(FirstLocation), *SourceMgr, *LangOpts);
Edits[LineNumber].second.append(SourceText.data(), SourceText.size());
}
// Clear the bucket.
IncludeBucket[IncludeKind].clear();
}
// Go through the single-line Edits and combine them into blocks of Edits.
int CurrentEndLine = 0;
SourceRange CurrentRange;
std::string CurrentText;
std::vector<FixItHint> Fixes;
for (const auto &LineEdit : Edits) {
// If the current edit is on the next line after the previous edit, add it
// to the current block edit.
if (LineEdit.first == CurrentEndLine + 1 &&
CurrentRange.getBegin() != CurrentRange.getEnd()) {
SourceRange EditRange = LineEdit.second.first;
if (EditRange.getBegin() != EditRange.getEnd()) {
++CurrentEndLine;
CurrentRange.setEnd(EditRange.getEnd());
}
CurrentText += LineEdit.second.second;
// Otherwise report the current block edit and start a new block.
} else {
if (CurrentEndLine) {
Fixes.push_back(FixItHint::CreateReplacement(
CharSourceRange::getCharRange(CurrentRange), CurrentText));
}
CurrentEndLine = LineEdit.first;
CurrentRange = LineEdit.second.first;
CurrentText = LineEdit.second.second;
}
}
// Finally, report the current block edit if there is one.
if (CurrentEndLine) {
Fixes.push_back(FixItHint::CreateReplacement(
CharSourceRange::getCharRange(CurrentRange), CurrentText));
}
// Reset the remaining internal state.
SourceLocations.clear();
IncludeLocations.clear();
return Fixes;
}
IncludeSorter::IncludeStyle
IncludeSorter::parseIncludeStyle(const std::string &Value) {
return Value == "llvm" ? IS_LLVM : IS_Google;
}
StringRef IncludeSorter::toString(IncludeStyle Style) {
return Style == IS_LLVM ? "llvm" : "google";
}
} // namespace utils
} // namespace tidy
} // namespace clang