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 | |
32 | namespace clang { |
33 | namespace include_cleaner { |
34 | namespace { |
35 | namespace cl = llvm::cl; |
36 | |
37 | llvm::StringRef Overview = llvm::StringLiteral(R"( |
38 | clang-include-cleaner analyzes the #include directives in source code. |
39 | |
40 | It suggests removing headers that the code is not using. |
41 | It suggests inserting headers that the code relies on, but does not include. |
42 | These changes make the file more self-contained and (at scale) make the codebase |
43 | easier to reason about and modify. |
44 | |
45 | The tool operates on *working* source code. This means it can suggest including |
46 | headers that are only indirectly included, but cannot suggest those that are |
47 | missing entirely. (clang-include-fixer can do this). |
48 | )" ) |
49 | .trim(); |
50 | |
51 | cl::OptionCategory IncludeCleaner("clang-include-cleaner" ); |
52 | |
53 | cl::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 | |
60 | cl::opt<std::string> { |
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 | |
68 | cl::opt<std::string> { |
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 | |
76 | enum class PrintStyle { Changes, Final }; |
77 | cl::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 | |
88 | cl::opt<bool> Edit{ |
89 | "edit" , |
90 | cl::desc("Apply edits to analyzed source files" ), |
91 | cl::cat(IncludeCleaner), |
92 | }; |
93 | |
94 | cl::opt<bool> Insert{ |
95 | "insert" , |
96 | cl::desc("Allow header insertions" ), |
97 | cl::init(Val: true), |
98 | cl::cat(IncludeCleaner), |
99 | }; |
100 | cl::opt<bool> Remove{ |
101 | "remove" , |
102 | cl::desc("Allow header removals" ), |
103 | cl::init(Val: true), |
104 | cl::cat(IncludeCleaner), |
105 | }; |
106 | |
107 | std::atomic<unsigned> Errors = ATOMIC_VAR_INIT(0); |
108 | |
109 | format::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 | |
119 | class Action : public clang::ASTFrontendAction { |
120 | public: |
121 | Action(llvm::function_ref<bool(llvm::StringRef)> , |
122 | llvm::StringMap<std::string> &EditedFiles) |
123 | : HeaderFilter(HeaderFilter), EditedFiles(EditedFiles) {} |
124 | |
125 | private: |
126 | RecordedAST AST; |
127 | RecordedPP PP; |
128 | PragmaIncludes PI; |
129 | llvm::function_ref<bool(llvm::StringRef)> ; |
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 | }; |
213 | class ActionFactory : public tooling::FrontendActionFactory { |
214 | public: |
215 | ActionFactory(llvm::function_ref<bool(llvm::StringRef)> ) |
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 | |
226 | private: |
227 | llvm::function_ref<bool(llvm::StringRef)> ; |
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. |
234 | std::function<bool(llvm::StringRef)> matchesAny(llvm::StringRef RegexFlag) { |
235 | auto FilterRegs = std::make_shared<std::vector<llvm::Regex>>(); |
236 | llvm::SmallVector<llvm::StringRef> ; |
237 | RegexFlag.split(A&: Headers, Separator: ',', MaxSplit: -1, /*KeepEmpty=*/false); |
238 | for (auto : 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 | |
258 | std::function<bool(llvm::StringRef)> () { |
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 ) { |
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 | |
277 | int 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(); |
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 | |