1//===--- IncludeCleaner.cpp - standalone tool for include analysis --------===//
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 "AnalysisInternal.h"
10#include "clang-include-cleaner/Analysis.h"
11#include "clang-include-cleaner/Record.h"
12#include "clang/Frontend/CompilerInstance.h"
13#include "clang/Frontend/FrontendAction.h"
14#include "clang/Lex/Preprocessor.h"
15#include "clang/Tooling/CommonOptionsParser.h"
16#include "clang/Tooling/Tooling.h"
17#include "llvm/ADT/STLFunctionalExtras.h"
18#include "llvm/ADT/SmallVector.h"
19#include "llvm/ADT/StringMap.h"
20#include "llvm/ADT/StringRef.h"
21#include "llvm/Support/CommandLine.h"
22#include "llvm/Support/FormatVariadic.h"
23#include "llvm/Support/Regex.h"
24#include "llvm/Support/Signals.h"
25#include "llvm/Support/raw_ostream.h"
26#include <functional>
27#include <memory>
28#include <string>
29#include <utility>
30#include <vector>
31
32namespace clang {
33namespace include_cleaner {
34namespace {
35namespace cl = llvm::cl;
36
37llvm::StringRef Overview = llvm::StringLiteral(R"(
38clang-include-cleaner analyzes the #include directives in source code.
39
40It suggests removing headers that the code is not using.
41It suggests inserting headers that the code relies on, but does not include.
42These changes make the file more self-contained and (at scale) make the codebase
43easier to reason about and modify.
44
45The tool operates on *working* source code. This means it can suggest including
46headers that are only indirectly included, but cannot suggest those that are
47missing entirely. (clang-include-fixer can do this).
48)")
49 .trim();
50
51cl::OptionCategory IncludeCleaner("clang-include-cleaner");
52
53cl::opt<std::string> HTMLReportPath{
54 "html",
55 cl::desc("Specify an output filename for an HTML report. "
56 "This describes both recommendations and reasons for changes."),
57 cl::cat(IncludeCleaner),
58};
59
60cl::opt<std::string> OnlyHeaders{
61 "only-headers",
62 cl::desc("A comma-separated list of regexes to match against suffix of a "
63 "header. Only headers that match will be analyzed."),
64 cl::init(Val: ""),
65 cl::cat(IncludeCleaner),
66};
67
68cl::opt<std::string> IgnoreHeaders{
69 "ignore-headers",
70 cl::desc("A comma-separated list of regexes to match against suffix of a "
71 "header, and disable analysis if matched."),
72 cl::init(Val: ""),
73 cl::cat(IncludeCleaner),
74};
75
76enum class PrintStyle { Changes, Final };
77cl::opt<PrintStyle> Print{
78 "print",
79 cl::values(
80 clEnumValN(PrintStyle::Changes, "changes", "Print symbolic changes"),
81 clEnumValN(PrintStyle::Final, "", "Print final code")),
82 cl::ValueOptional,
83 cl::init(Val: PrintStyle::Final),
84 cl::desc("Print the list of headers to insert and remove"),
85 cl::cat(IncludeCleaner),
86};
87
88cl::opt<bool> Edit{
89 "edit",
90 cl::desc("Apply edits to analyzed source files"),
91 cl::cat(IncludeCleaner),
92};
93
94cl::opt<bool> Insert{
95 "insert",
96 cl::desc("Allow header insertions"),
97 cl::init(Val: true),
98 cl::cat(IncludeCleaner),
99};
100cl::opt<bool> Remove{
101 "remove",
102 cl::desc("Allow header removals"),
103 cl::init(Val: true),
104 cl::cat(IncludeCleaner),
105};
106
107std::atomic<unsigned> Errors = ATOMIC_VAR_INIT(0);
108
109format::FormatStyle getStyle(llvm::StringRef Filename) {
110 auto S = format::getStyle(StyleName: format::DefaultFormatStyle, FileName: Filename,
111 FallbackStyle: format::DefaultFallbackStyle);
112 if (!S || !S->isCpp()) {
113 consumeError(Err: S.takeError());
114 return format::getLLVMStyle();
115 }
116 return std::move(*S);
117}
118
119class Action : public clang::ASTFrontendAction {
120public:
121 Action(llvm::function_ref<bool(llvm::StringRef)> HeaderFilter,
122 llvm::StringMap<std::string> &EditedFiles)
123 : HeaderFilter(HeaderFilter), EditedFiles(EditedFiles) {}
124
125private:
126 RecordedAST AST;
127 RecordedPP PP;
128 PragmaIncludes PI;
129 llvm::function_ref<bool(llvm::StringRef)> HeaderFilter;
130 llvm::StringMap<std::string> &EditedFiles;
131
132 bool BeginInvocation(CompilerInstance &CI) override {
133 // We only perform include-cleaner analysis. So we disable diagnostics that
134 // won't affect our analysis to make the tool more robust against
135 // in-development code.
136 CI.getLangOpts().ModulesDeclUse = false;
137 CI.getLangOpts().ModulesStrictDeclUse = false;
138 return true;
139 }
140
141 void ExecuteAction() override {
142 auto &P = getCompilerInstance().getPreprocessor();
143 P.addPPCallbacks(C: PP.record(PP: P));
144 PI.record(CI: getCompilerInstance());
145 ASTFrontendAction::ExecuteAction();
146 }
147
148 std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
149 StringRef File) override {
150 return AST.record();
151 }
152
153 void EndSourceFile() override {
154 const auto &SM = getCompilerInstance().getSourceManager();
155 if (SM.getDiagnostics().hasUncompilableErrorOccurred()) {
156 llvm::errs()
157 << "Skipping file " << getCurrentFile()
158 << " due to compiler errors. clang-include-cleaner expects to "
159 "work on compilable source code.\n";
160 return;
161 }
162
163 if (!HTMLReportPath.empty())
164 writeHTML();
165
166 llvm::StringRef Path =
167 SM.getFileEntryForID(FID: SM.getMainFileID())->tryGetRealPathName();
168 assert(!Path.empty() && "Main file path not known?");
169 llvm::StringRef Code = SM.getBufferData(FID: SM.getMainFileID());
170
171 auto Results =
172 analyze(ASTRoots: AST.Roots, MacroRefs: PP.MacroReferences, I: PP.Includes, PI: &PI,
173 PP: getCompilerInstance().getPreprocessor(), HeaderFilter);
174 if (!Insert)
175 Results.Missing.clear();
176 if (!Remove)
177 Results.Unused.clear();
178 std::string Final = fixIncludes(Results, FileName: Path, Code, IncludeStyle: getStyle(Filename: Path));
179
180 if (Print.getNumOccurrences()) {
181 switch (Print) {
182 case PrintStyle::Changes:
183 for (const Include *I : Results.Unused)
184 llvm::outs() << "- " << I->quote() << " @Line:" << I->Line << "\n";
185 for (const auto &I : Results.Missing)
186 llvm::outs() << "+ " << I << "\n";
187 break;
188 case PrintStyle::Final:
189 llvm::outs() << Final;
190 break;
191 }
192 }
193
194 if (!Results.Missing.empty() || !Results.Unused.empty())
195 EditedFiles.try_emplace(Key: Path, Args&: Final);
196 }
197
198 void writeHTML() {
199 std::error_code EC;
200 llvm::raw_fd_ostream OS(HTMLReportPath, EC);
201 if (EC) {
202 llvm::errs() << "Unable to write HTML report to " << HTMLReportPath
203 << ": " << EC.message() << "\n";
204 ++Errors;
205 return;
206 }
207 writeHTMLReport(
208 File: AST.Ctx->getSourceManager().getMainFileID(), PP.Includes, Roots: AST.Roots,
209 MacroRefs: PP.MacroReferences, Ctx&: *AST.Ctx,
210 HS: getCompilerInstance().getPreprocessor().getHeaderSearchInfo(), PI: &PI, OS);
211 }
212};
213class ActionFactory : public tooling::FrontendActionFactory {
214public:
215 ActionFactory(llvm::function_ref<bool(llvm::StringRef)> HeaderFilter)
216 : HeaderFilter(HeaderFilter) {}
217
218 std::unique_ptr<clang::FrontendAction> create() override {
219 return std::make_unique<Action>(args&: HeaderFilter, args&: EditedFiles);
220 }
221
222 const llvm::StringMap<std::string> &editedFiles() const {
223 return EditedFiles;
224 }
225
226private:
227 llvm::function_ref<bool(llvm::StringRef)> HeaderFilter;
228 // Map from file name to final code with the include edits applied.
229 llvm::StringMap<std::string> EditedFiles;
230};
231
232// Compiles a regex list into a function that return true if any match a header.
233// Prints and returns nullptr if any regexes are invalid.
234std::function<bool(llvm::StringRef)> matchesAny(llvm::StringRef RegexFlag) {
235 auto FilterRegs = std::make_shared<std::vector<llvm::Regex>>();
236 llvm::SmallVector<llvm::StringRef> Headers;
237 RegexFlag.split(A&: Headers, Separator: ',', MaxSplit: -1, /*KeepEmpty=*/false);
238 for (auto HeaderPattern : Headers) {
239 std::string AnchoredPattern = "(" + HeaderPattern.str() + ")$";
240 llvm::Regex CompiledRegex(AnchoredPattern);
241 std::string RegexError;
242 if (!CompiledRegex.isValid(Error&: RegexError)) {
243 llvm::errs() << llvm::formatv(Fmt: "Invalid regular expression '{0}': {1}\n",
244 Vals&: HeaderPattern, Vals&: RegexError);
245 return nullptr;
246 }
247 FilterRegs->push_back(x: std::move(CompiledRegex));
248 }
249 return [FilterRegs](llvm::StringRef Path) {
250 for (const auto &F : *FilterRegs) {
251 if (F.match(String: Path))
252 return true;
253 }
254 return false;
255 };
256}
257
258std::function<bool(llvm::StringRef)> headerFilter() {
259 auto OnlyMatches = matchesAny(RegexFlag: OnlyHeaders);
260 auto IgnoreMatches = matchesAny(RegexFlag: IgnoreHeaders);
261 if (!OnlyMatches || !IgnoreMatches)
262 return nullptr;
263
264 return [OnlyMatches, IgnoreMatches](llvm::StringRef Header) {
265 if (!OnlyHeaders.empty() && !OnlyMatches(Header))
266 return true;
267 if (!IgnoreHeaders.empty() && IgnoreMatches(Header))
268 return true;
269 return false;
270 };
271}
272
273} // namespace
274} // namespace include_cleaner
275} // namespace clang
276
277int main(int argc, const char **argv) {
278 using namespace clang::include_cleaner;
279
280 llvm::sys::PrintStackTraceOnErrorSignal(Argv0: argv[0]);
281 auto OptionsParser =
282 clang::tooling::CommonOptionsParser::create(argc, argv, Category&: IncludeCleaner);
283 if (!OptionsParser) {
284 llvm::errs() << toString(E: OptionsParser.takeError());
285 return 1;
286 }
287
288 if (OptionsParser->getSourcePathList().size() != 1) {
289 std::vector<cl::Option *> IncompatibleFlags = {&HTMLReportPath, &Print};
290 for (const auto *Flag : IncompatibleFlags) {
291 if (Flag->getNumOccurrences()) {
292 llvm::errs() << "-" << Flag->ArgStr << " requires a single input file";
293 return 1;
294 }
295 }
296 }
297
298 clang::tooling::ClangTool Tool(OptionsParser->getCompilations(),
299 OptionsParser->getSourcePathList());
300
301 auto HeaderFilter = headerFilter();
302 if (!HeaderFilter)
303 return 1; // error already reported.
304 ActionFactory Factory(HeaderFilter);
305 auto ErrorCode = Tool.run(Action: &Factory);
306 if (Edit) {
307 for (const auto &NameAndContent : Factory.editedFiles()) {
308 llvm::StringRef FileName = NameAndContent.first();
309 const std::string &FinalCode = NameAndContent.second;
310 if (auto Err = llvm::writeToOutput(
311 OutputFileName: FileName, Write: [&](llvm::raw_ostream &OS) -> llvm::Error {
312 OS << FinalCode;
313 return llvm::Error::success();
314 })) {
315 llvm::errs() << "Failed to apply edits to " << FileName << ": "
316 << toString(E: std::move(Err)) << "\n";
317 ++Errors;
318 }
319 }
320 }
321 return ErrorCode || Errors != 0;
322}
323

source code of clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp