clang-tools 23.0.0git
IncludeSorter.cpp
Go to the documentation of this file.
1//===----------------------------------------------------------------------===//
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 "IncludeSorter.h"
10#include "clang/Basic/SourceManager.h"
11#include "clang/Lex/Lexer.h"
12#include <optional>
13
14namespace clang::tidy {
15namespace utils {
16
17static StringRef removeFirstSuffix(StringRef Str,
18 ArrayRef<const char *> Suffixes) {
19 for (const StringRef Suffix : Suffixes)
20 if (Str.consume_back(Suffix))
21 return Str;
22 return Str;
23}
24
25static StringRef makeCanonicalName(StringRef Str,
26 IncludeSorter::IncludeStyle Style) {
27 // The list of suffixes to remove from source file names to get the
28 // "canonical" file names.
29 // E.g. tools/sort_includes.cc and tools/sort_includes_test.cc
30 // would both canonicalize to tools/sort_includes and tools/sort_includes.h
31 // (once canonicalized) will match as being the main include file associated
32 // with the source files.
33 if (Style == IncludeSorter::IS_LLVM) {
34 return removeFirstSuffix(
35 removeFirstSuffix(Str, {".cc", ".cpp", ".c", ".h", ".hpp"}), {"Test"});
36 }
37 if (Style == IncludeSorter::IS_Google_ObjC) {
38 const StringRef Canonical =
39 removeFirstSuffix(removeFirstSuffix(Str, {".cc", ".cpp", ".c", ".h",
40 ".hpp", ".mm", ".m"}),
41 {"_unittest", "_regtest", "_test", "Test"});
42
43 // Objective-C categories have a `+suffix` format, but should be grouped
44 // with the file they are a category of.
45 size_t StartIndex = Canonical.find_last_of('/');
46 if (StartIndex == StringRef::npos)
47 StartIndex = 0;
48 return Canonical.substr(0, Canonical.find_first_of('+', StartIndex));
49 }
50 return removeFirstSuffix(
51 removeFirstSuffix(Str, {".cc", ".cpp", ".c", ".h", ".hpp"}),
52 {"_unittest", "_regtest", "_test"});
53}
54
55// Scan to the end of the line and return the offset of the next line.
56static size_t findNextLine(const char *Text) {
57 const size_t EOLIndex = std::strcspn(Text, "\n");
58 return Text[EOLIndex] == '\0' ? EOLIndex : EOLIndex + 1;
59}
60
61static IncludeSorter::IncludeKinds
62determineIncludeKind(StringRef CanonicalFile, StringRef IncludeFile,
63 bool IsAngled, IncludeSorter::IncludeStyle Style) {
64 // Compute the two "canonical" forms of the include's filename sans extension.
65 // The first form is the include's filename without ".h" or "-inl.h" at the
66 // end. The second form is the first form with "/public/" in the file path
67 // replaced by "/internal/".
68 if (IsAngled) {
69 // If the system include (<foo>) ends with ".h", then it is a normal C-style
70 // include. Otherwise assume it is a C++-style extensionless include.
71 return IncludeFile.ends_with(".h") ? IncludeSorter::IK_CSystemInclude
72 : IncludeSorter::IK_CXXSystemInclude;
73 }
74 const StringRef CanonicalInclude = makeCanonicalName(IncludeFile, Style);
75 if (CanonicalFile.ends_with(CanonicalInclude) ||
76 CanonicalInclude.ends_with(CanonicalFile)) {
77 return IncludeSorter::IK_MainTUInclude;
78 }
79 if ((Style == IncludeSorter::IS_Google) ||
80 (Style == IncludeSorter::IS_Google_ObjC)) {
81 const std::pair<StringRef, StringRef> Parts =
82 CanonicalInclude.split("/public/");
83 StringRef FileCopy = CanonicalFile;
84 if (FileCopy.consume_front(Parts.first) &&
85 FileCopy.consume_back(Parts.second)) {
86 // Determine the kind of this inclusion.
87 if (FileCopy == "/internal/" || FileCopy == "/proto/")
88 return IncludeSorter::IK_MainTUInclude;
89 }
90 }
91 if (Style == IncludeSorter::IS_Google_ObjC) {
92 if (IncludeFile.ends_with(".generated.h") ||
93 IncludeFile.ends_with(".proto.h") ||
94 IncludeFile.ends_with(".pbobjc.h")) {
95 return IncludeSorter::IK_GeneratedInclude;
96 }
97 }
98 return IncludeSorter::IK_NonSystemInclude;
99}
100
101static int compareHeaders(StringRef LHS, StringRef RHS,
102 IncludeSorter::IncludeStyle Style) {
103 if (Style == IncludeSorter::IncludeStyle::IS_Google_ObjC) {
104 const std::pair<const char *, const char *> &Mismatch =
105 llvm::mismatch(LHS, RHS);
106 if ((Mismatch.first != LHS.end()) && (Mismatch.second != RHS.end())) {
107 if ((*Mismatch.first == '.') && (*Mismatch.second == '+'))
108 return -1;
109 if ((*Mismatch.first == '+') && (*Mismatch.second == '.'))
110 return 1;
111 }
112 }
113 return LHS.compare(RHS);
114}
115
116IncludeSorter::IncludeSorter(const SourceManager *SourceMgr, FileID FileID,
117 StringRef FileName, IncludeStyle Style)
118 : SourceMgr(SourceMgr), Style(Style), CurrentFileID(FileID),
119 CanonicalFile(makeCanonicalName(FileName, Style)) {}
120
121void IncludeSorter::addInclude(StringRef FileName, bool IsAngled,
122 SourceLocation HashLocation,
123 SourceLocation EndLocation) {
124 const int Offset = findNextLine(SourceMgr->getCharacterData(EndLocation));
125
126 // Record the relevant location information for this inclusion directive.
127 auto &IncludeLocation = IncludeLocations[FileName];
128 IncludeLocation.push_back(
129 SourceRange(HashLocation, EndLocation.getLocWithOffset(Offset)));
130 SourceLocations.push_back(IncludeLocation.back());
131
132 // Stop if this inclusion is a duplicate.
133 if (IncludeLocation.size() > 1)
134 return;
135
136 // Add the included file's name to the appropriate bucket.
137 const IncludeKinds Kind =
138 determineIncludeKind(CanonicalFile, FileName, IsAngled, Style);
139 if (Kind != IK_InvalidInclude)
140 IncludeBucket[Kind].push_back(FileName.str());
141}
142
143std::optional<FixItHint>
144IncludeSorter::createIncludeInsertion(StringRef FileName, bool IsAngled) {
145 const StringRef LineEnding =
146 SourceMgr->getBufferData(CurrentFileID).detectEOL();
147 std::string IncludeStmt;
148 if (Style == IncludeStyle::IS_Google_ObjC) {
149 IncludeStmt =
150 IsAngled
151 ? llvm::Twine("#import <" + FileName + ">" + LineEnding).str()
152 : llvm::Twine("#import \"" + FileName + "\"" + LineEnding).str();
153 } else {
154 IncludeStmt =
155 IsAngled
156 ? llvm::Twine("#include <" + FileName + ">" + LineEnding).str()
157 : llvm::Twine("#include \"" + FileName + "\"" + LineEnding).str();
158 }
159 if (SourceLocations.empty()) {
160 // If there are no includes in this file, add it in the first line.
161 // FIXME: insert after the file comment or the header guard, if present.
162 IncludeStmt.append(LineEnding);
163 return FixItHint::CreateInsertion(
164 SourceMgr->getLocForStartOfFile(CurrentFileID), IncludeStmt);
165 }
166
167 auto IncludeKind =
168 determineIncludeKind(CanonicalFile, FileName, IsAngled, Style);
169
170 if (!IncludeBucket[IncludeKind].empty()) {
171 for (const std::string &IncludeEntry : IncludeBucket[IncludeKind]) {
172 if (compareHeaders(FileName, IncludeEntry, Style) < 0) {
173 const auto &Location = IncludeLocations[IncludeEntry][0];
174 return FixItHint::CreateInsertion(Location.getBegin(), IncludeStmt);
175 }
176 if (FileName == IncludeEntry)
177 return std::nullopt;
178 }
179 // FileName comes after all include entries in bucket, insert it after
180 // last.
181 const std::string &LastInclude = IncludeBucket[IncludeKind].back();
182 const SourceRange LastIncludeLocation =
183 IncludeLocations[LastInclude].back();
184 return FixItHint::CreateInsertion(LastIncludeLocation.getEnd(),
185 IncludeStmt);
186 }
187 // Find the non-empty include bucket to be sorted directly above
188 // 'IncludeKind'. If such a bucket exists, we'll want to sort the include
189 // after that bucket. If no such bucket exists, find the first non-empty
190 // include bucket in the file. In that case, we'll want to sort the include
191 // before that bucket.
192 IncludeKinds NonEmptyKind = IK_InvalidInclude;
193 for (int I = IK_InvalidInclude - 1; I >= 0; --I) {
194 if (!IncludeBucket[I].empty()) {
195 NonEmptyKind = static_cast<IncludeKinds>(I);
196 if (NonEmptyKind < IncludeKind)
197 break;
198 }
199 }
200 if (NonEmptyKind == IK_InvalidInclude)
201 return std::nullopt;
202
203 if (NonEmptyKind < IncludeKind) {
204 // Create a block after.
205 const std::string &LastInclude = IncludeBucket[NonEmptyKind].back();
206 const SourceRange LastIncludeLocation =
207 IncludeLocations[LastInclude].back();
208 IncludeStmt.insert(0, LineEnding);
209 return FixItHint::CreateInsertion(LastIncludeLocation.getEnd(),
210 IncludeStmt);
211 }
212 // Create a block before.
213 const std::string &FirstInclude = IncludeBucket[NonEmptyKind][0];
214 const SourceRange FirstIncludeLocation =
215 IncludeLocations[FirstInclude].back();
216 IncludeStmt.append(LineEnding);
217 return FixItHint::CreateInsertion(FirstIncludeLocation.getBegin(),
218 IncludeStmt);
219}
220
221} // namespace utils
222
223llvm::ArrayRef<std::pair<utils::IncludeSorter::IncludeStyle, StringRef>>
225 static constexpr std::pair<utils::IncludeSorter::IncludeStyle, StringRef>
226 Mapping[] = {{utils::IncludeSorter::IS_LLVM, "llvm"},
227 {utils::IncludeSorter::IS_Google, "google"},
228 {utils::IncludeSorter::IS_Google_ObjC, "google-objc"}};
229 return {Mapping};
230}
231} // namespace clang::tidy
static IncludeSorter::IncludeKinds determineIncludeKind(StringRef CanonicalFile, StringRef IncludeFile, bool IsAngled, IncludeSorter::IncludeStyle Style)
static StringRef makeCanonicalName(StringRef Str, IncludeSorter::IncludeStyle Style)
static StringRef removeFirstSuffix(StringRef Str, ArrayRef< const char * > Suffixes)
static size_t findNextLine(const char *Text)
static int compareHeaders(StringRef LHS, StringRef RHS, IncludeSorter::IncludeStyle Style)
static ArrayRef< std::pair< utils::IncludeSorter::IncludeStyle, StringRef > > getEnumMapping()