Skip to content

Commit 1a8dd74

Browse files
committed
[include-cleaner] clang-include-cleaner can print/apply edits
This adds command-line flags to the tool: + -print: prints changed source code + -print=changes: prints headers added/removed + -edit: rewrites code in place + -insert=0/-remove=0: disables additions/deletions for the above These are supported by a couple of new functions dumped into Analysis: analyze() sits on top of walkUsed and makes used/unused decisions for Includes. fixIncludes() applies those results to source code. Differential Revision: https://reviews.llvm.org/D139013
1 parent 6dc6f43 commit 1a8dd74

File tree

8 files changed

+302
-14
lines changed

8 files changed

+302
-14
lines changed

clang-tools-extra/include-cleaner/include/clang-include-cleaner/Analysis.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,21 @@
1313

1414
#include "clang-include-cleaner/Record.h"
1515
#include "clang-include-cleaner/Types.h"
16+
#include "clang/Format/Format.h"
1617
#include "llvm/ADT/ArrayRef.h"
1718
#include "llvm/ADT/STLFunctionalExtras.h"
19+
#include "llvm/Support/MemoryBufferRef.h"
1820
#include <variant>
1921

2022
namespace clang {
2123
class SourceLocation;
2224
class Decl;
2325
class FileEntry;
26+
class HeaderSearch;
27+
namespace tooling {
28+
class Replacements;
29+
struct IncludeStyle;
30+
} // namespace tooling
2431
namespace include_cleaner {
2532

2633
/// A UsedSymbolCB is a callback invoked for each symbol reference seen.
@@ -47,6 +54,24 @@ void walkUsed(llvm::ArrayRef<Decl *> ASTRoots,
4754
llvm::ArrayRef<SymbolReference> MacroRefs,
4855
const PragmaIncludes *PI, const SourceManager &, UsedSymbolCB CB);
4956

57+
struct AnalysisResults {
58+
std::vector<const Include *> Unused;
59+
std::vector<std::string> Missing; // Spellings, like "<vector>"
60+
};
61+
62+
/// Determine which headers should be inserted or removed from the main file.
63+
/// This exposes conclusions but not reasons: use lower-level walkUsed for that.
64+
AnalysisResults analyze(llvm::ArrayRef<Decl *> ASTRoots,
65+
llvm::ArrayRef<SymbolReference> MacroRefs,
66+
const Includes &I, const PragmaIncludes *PI,
67+
const SourceManager &SM, HeaderSearch &HS);
68+
69+
/// Removes unused includes and inserts missing ones in the main file.
70+
/// Returns the modified main-file code.
71+
/// The FormatStyle must be C++ or ObjC (to support include ordering).
72+
std::string fixIncludes(const AnalysisResults &Results, llvm::StringRef Code,
73+
const format::FormatStyle &IncludeStyle);
74+
5075
} // namespace include_cleaner
5176
} // namespace clang
5277

clang-tools-extra/include-cleaner/lib/Analysis.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
#include "clang-include-cleaner/Types.h"
1212
#include "clang/AST/ASTContext.h"
1313
#include "clang/Basic/SourceManager.h"
14+
#include "clang/Format/Format.h"
15+
#include "clang/Lex/HeaderSearch.h"
16+
#include "clang/Tooling/Core/Replacement.h"
17+
#include "clang/Tooling/Inclusions/HeaderIncludes.h"
1418
#include "clang/Tooling/Inclusions/StandardLibrary.h"
1519
#include "llvm/ADT/ArrayRef.h"
1620
#include "llvm/ADT/SmallVector.h"
@@ -42,4 +46,69 @@ void walkUsed(llvm::ArrayRef<Decl *> ASTRoots,
4246
}
4347
}
4448

49+
static std::string spellHeader(const Header &H, HeaderSearch &HS,
50+
const FileEntry *Main) {
51+
switch (H.kind()) {
52+
case Header::Physical: {
53+
bool IsSystem = false;
54+
std::string Path = HS.suggestPathToFileForDiagnostics(
55+
H.physical(), Main->tryGetRealPathName(), &IsSystem);
56+
return IsSystem ? "<" + Path + ">" : "\"" + Path + "\"";
57+
}
58+
case Header::Standard:
59+
return H.standard().name().str();
60+
case Header::Verbatim:
61+
return H.verbatim().str();
62+
}
63+
llvm_unreachable("Unknown Header kind");
64+
}
65+
66+
AnalysisResults analyze(llvm::ArrayRef<Decl *> ASTRoots,
67+
llvm::ArrayRef<SymbolReference> MacroRefs,
68+
const Includes &Inc, const PragmaIncludes *PI,
69+
const SourceManager &SM, HeaderSearch &HS) {
70+
const FileEntry *MainFile = SM.getFileEntryForID(SM.getMainFileID());
71+
llvm::DenseSet<const Include *> Used;
72+
llvm::StringSet<> Missing;
73+
walkUsed(ASTRoots, MacroRefs, PI, SM,
74+
[&](const SymbolReference &Ref, llvm::ArrayRef<Header> Providers) {
75+
bool Satisfied = false;
76+
for (const Header &H : Providers) {
77+
if (H.kind() == Header::Physical && H.physical() == MainFile)
78+
Satisfied = true;
79+
for (const Include *I : Inc.match(H)) {
80+
Used.insert(I);
81+
Satisfied = true;
82+
}
83+
}
84+
if (!Satisfied && !Providers.empty() &&
85+
Ref.RT == RefType::Explicit)
86+
Missing.insert(spellHeader(Providers.front(), HS, MainFile));
87+
});
88+
89+
AnalysisResults Results;
90+
for (const Include &I : Inc.all())
91+
if (!Used.contains(&I))
92+
Results.Unused.push_back(&I);
93+
for (llvm::StringRef S : Missing.keys())
94+
Results.Missing.push_back(S.str());
95+
llvm::sort(Results.Missing);
96+
return Results;
97+
}
98+
99+
std::string fixIncludes(const AnalysisResults &Results, llvm::StringRef Code,
100+
const format::FormatStyle &Style) {
101+
assert(Style.isCpp() && "Only C++ style supports include insertions!");
102+
tooling::Replacements R;
103+
// Encode insertions/deletions in the magic way clang-format understands.
104+
for (const Include *I : Results.Unused)
105+
cantFail(R.add(tooling::Replacement("input", UINT_MAX, 1, I->quote())));
106+
for (llvm::StringRef Spelled : Results.Missing)
107+
cantFail(R.add(tooling::Replacement("input", UINT_MAX, 0,
108+
("#include " + Spelled).str())));
109+
// "cleanup" actually turns the UINT_MAX replacements into concrete edits.
110+
auto Positioned = cantFail(format::cleanupAroundReplacements(Code, R, Style));
111+
return cantFail(tooling::applyAllReplacements(Code, Positioned));
112+
}
113+
45114
} // namespace clang::include_cleaner

clang-tools-extra/include-cleaner/lib/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ clang_target_link_libraries(clangIncludeCleaner
1414
PRIVATE
1515
clangAST
1616
clangBasic
17+
clangFormat
1718
clangLex
1819
clangToolingInclusions
1920
clangToolingInclusionsStdlib
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#pragma once
2+
#include "bar.h"
3+
#include "foo.h"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#include "foobar.h"
2+
3+
int x = foo();
4+
5+
// RUN: clang-include-cleaner -print=changes %s -- -I%S/Inputs/ | FileCheck --check-prefix=CHANGE %s
6+
// CHANGE: - "foobar.h"
7+
// CHANGE-NEXT: + "foo.h"
8+
9+
// RUN: clang-include-cleaner -remove=0 -print=changes %s -- -I%S/Inputs/ | FileCheck --check-prefix=INSERT %s
10+
// INSERT-NOT: - "foobar.h"
11+
// INSERT: + "foo.h"
12+
13+
// RUN: clang-include-cleaner -insert=0 -print=changes %s -- -I%S/Inputs/ | FileCheck --check-prefix=REMOVE %s
14+
// REMOVE: - "foobar.h"
15+
// REMOVE-NOT: + "foo.h"
16+
17+
// RUN: clang-include-cleaner -print %s -- -I%S/Inputs/ | FileCheck --match-full-lines --check-prefix=PRINT %s
18+
// PRINT: #include "foo.h"
19+
// PRINT-NOT: {{^}}#include "foobar.h"{{$}}
20+
21+
// RUN: cp %s %t.cpp
22+
// RUN: clang-include-cleaner -edit %t.cpp -- -I%S/Inputs/
23+
// RUN: FileCheck --match-full-lines --check-prefix=EDIT %s < %t.cpp
24+
// EDIT: #include "foo.h"
25+
// EDIT-NOT: {{^}}#include "foobar.h"{{$}}

clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//===----------------------------------------------------------------------===//
88

99
#include "AnalysisInternal.h"
10+
#include "clang-include-cleaner/Analysis.h"
1011
#include "clang-include-cleaner/Record.h"
1112
#include "clang/Frontend/CompilerInstance.h"
1213
#include "clang/Frontend/FrontendAction.h"
@@ -46,7 +47,48 @@ cl::opt<std::string> HTMLReportPath{
4647
cl::cat(IncludeCleaner),
4748
};
4849

49-
class HTMLReportAction : public clang::ASTFrontendAction {
50+
enum class PrintStyle { Changes, Final };
51+
cl::opt<PrintStyle> Print{
52+
"print",
53+
cl::values(
54+
clEnumValN(PrintStyle::Changes, "changes", "Print symbolic changes"),
55+
clEnumValN(PrintStyle::Final, "", "Print final code")),
56+
cl::ValueOptional,
57+
cl::init(PrintStyle::Final),
58+
cl::desc("Print the list of headers to insert and remove"),
59+
cl::cat(IncludeCleaner),
60+
};
61+
62+
cl::opt<bool> Edit{
63+
"edit",
64+
cl::desc("Apply edits to analyzed source files"),
65+
cl::cat(IncludeCleaner),
66+
};
67+
68+
cl::opt<bool> Insert{
69+
"insert",
70+
cl::desc("Allow header insertions"),
71+
cl::init(true),
72+
};
73+
cl::opt<bool> Remove{
74+
"remove",
75+
cl::desc("Allow header removals"),
76+
cl::init(true),
77+
};
78+
79+
std::atomic<unsigned> Errors = ATOMIC_VAR_INIT(0);
80+
81+
format::FormatStyle getStyle(llvm::StringRef Filename) {
82+
auto S = format::getStyle(format::DefaultFormatStyle, Filename,
83+
format::DefaultFallbackStyle);
84+
if (!S || !S->isCpp()) {
85+
consumeError(S.takeError());
86+
return format::getLLVMStyle();
87+
}
88+
return std::move(*S);
89+
}
90+
91+
class Action : public clang::ASTFrontendAction {
5092
RecordedAST AST;
5193
RecordedPP PP;
5294
PragmaIncludes PI;
@@ -64,12 +106,59 @@ class HTMLReportAction : public clang::ASTFrontendAction {
64106
}
65107

66108
void EndSourceFile() override {
109+
if (!HTMLReportPath.empty())
110+
writeHTML();
111+
112+
const auto &SM = getCompilerInstance().getSourceManager();
113+
auto &HS = getCompilerInstance().getPreprocessor().getHeaderSearchInfo();
114+
llvm::StringRef Path =
115+
SM.getFileEntryForID(SM.getMainFileID())->tryGetRealPathName();
116+
assert(!Path.empty() && "Main file path not known?");
117+
llvm::StringRef Code = SM.getBufferData(SM.getMainFileID());
118+
119+
auto Results =
120+
analyze(AST.Roots, PP.MacroReferences, PP.Includes, &PI, SM, HS);
121+
if (!Insert)
122+
Results.Missing.clear();
123+
if (!Remove)
124+
Results.Unused.clear();
125+
std::string Final = fixIncludes(Results, Code, getStyle(Path));
126+
127+
if (Print.getNumOccurrences()) {
128+
switch (Print) {
129+
case PrintStyle::Changes:
130+
for (const Include *I : Results.Unused)
131+
llvm::outs() << "- " << I->quote() << "\n";
132+
for (const auto &I : Results.Missing)
133+
llvm::outs() << "+ " << I << "\n";
134+
break;
135+
case PrintStyle::Final:
136+
llvm::outs() << Final;
137+
break;
138+
}
139+
}
140+
141+
if (Edit) {
142+
if (auto Err = llvm::writeToOutput(
143+
Path, [&](llvm::raw_ostream &OS) -> llvm::Error {
144+
OS << Final;
145+
return llvm::Error::success();
146+
})) {
147+
llvm::errs() << "Failed to apply edits to " << Path << ": "
148+
<< toString(std::move(Err)) << "\n";
149+
++Errors;
150+
}
151+
}
152+
}
153+
154+
void writeHTML() {
67155
std::error_code EC;
68156
llvm::raw_fd_ostream OS(HTMLReportPath, EC);
69157
if (EC) {
70158
llvm::errs() << "Unable to write HTML report to " << HTMLReportPath
71159
<< ": " << EC.message() << "\n";
72-
exit(1);
160+
++Errors;
161+
return;
73162
}
74163
writeHTMLReport(
75164
AST.Ctx->getSourceManager().getMainFileID(), PP.Includes, AST.Roots,
@@ -93,20 +182,17 @@ int main(int argc, const char **argv) {
93182
return 1;
94183
}
95184

96-
std::unique_ptr<clang::tooling::FrontendActionFactory> Factory;
97-
if (HTMLReportPath.getNumOccurrences()) {
98-
if (OptionsParser->getSourcePathList().size() != 1) {
99-
llvm::errs() << "-" << HTMLReportPath.ArgStr
100-
<< " requires a single input file";
185+
if (OptionsParser->getSourcePathList().size() != 1) {
186+
std::vector<cl::Option *> IncompatibleFlags = {&HTMLReportPath, &Print};
187+
for (const auto *Flag : IncompatibleFlags) {
188+
if (Flag->getNumOccurrences())
189+
llvm::errs() << "-" << Flag->ArgStr << " requires a single input file";
101190
return 1;
102191
}
103-
Factory = clang::tooling::newFrontendActionFactory<HTMLReportAction>();
104-
} else {
105-
llvm::errs() << "Unimplemented\n";
106-
return 1;
107192
}
108-
193+
auto Factory = clang::tooling::newFrontendActionFactory<Action>();
109194
return clang::tooling::ClangTool(OptionsParser->getCompilations(),
110195
OptionsParser->getSourcePathList())
111-
.run(Factory.get());
196+
.run(Factory.get()) ||
197+
Errors != 0;
112198
}

0 commit comments

Comments
 (0)