Skip to content

Commit 64d9713

Browse files
authored
[include-cleaner] Unify symlink handling (#102615)
We were using tryGetRealPathName in certain places, which resolves symlinks (sometimes). This was resulting in discrepancies in behavior, depending on how a file was first reached. This path migrates all usages of tryGetRealPathName to regular getName instead. This implies one backward incompatible change for header-filtering. Our ignore-header option used to filter against suffixes of absolute paths, whereas now filter can receive working-directory relative paths in some cases, possibly braking existing filters. Chances of really braking users is pretty low: - We'll still filter against absolute paths when header is outside the working directory (e.g. /usr/bin/include/some/linux/header.h.) - Most projects run builds in a working directory that's nested inside the repository, hence relative paths still contain all the segments relative to repository root and anything else is unlikely to be meaningful. e.g. if a header is in `$HOME/work/llvm-project/clang-tools-extra/header.h` with builds being run in `$home/work/llvm-project/build`, we'll still filter against `../clang-tools-extra/header.h` which has all the useful segments as a suffix. - This is also a change in how we handle symlinks, but this is aligned with what we do in rest of our tools (clangd, tidy checks etc.). We tend to not resolve any symlinks for the file.
1 parent 8fc3a79 commit 64d9713

File tree

8 files changed

+105
-48
lines changed

8 files changed

+105
-48
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ struct Header {
136136
}
137137
StringRef verbatim() const { return std::get<Verbatim>(Storage); }
138138

139-
/// Absolute path for the header when it's a physical file. Otherwise just
140-
/// the spelling without surrounding quotes/brackets.
139+
/// For phiscal files, either absolute path or path relative to the execution
140+
/// root. Otherwise just the spelling without surrounding quotes/brackets.
141141
llvm::StringRef resolvedPath() const;
142142

143143
private:

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

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ analyze(llvm::ArrayRef<Decl *> ASTRoots,
8282
const PragmaIncludes *PI, const Preprocessor &PP,
8383
llvm::function_ref<bool(llvm::StringRef)> HeaderFilter) {
8484
auto &SM = PP.getSourceManager();
85-
const FileEntry *MainFile = SM.getFileEntryForID(SM.getMainFileID());
85+
const auto MainFile = *SM.getFileEntryRefForID(SM.getMainFileID());
8686
llvm::DenseSet<const Include *> Used;
8787
llvm::StringSet<> Missing;
8888
if (!HeaderFilter)
@@ -95,37 +95,38 @@ analyze(llvm::ArrayRef<Decl *> ASTRoots,
9595
for (const Header &H : Providers) {
9696
if (H.kind() == Header::Physical &&
9797
(H.physical() == MainFile ||
98-
(ResourceDir && H.physical().getDir() == *ResourceDir))) {
98+
H.physical().getDir() == ResourceDir)) {
9999
Satisfied = true;
100100
}
101101
for (const Include *I : Inc.match(H)) {
102102
Used.insert(I);
103103
Satisfied = true;
104104
}
105105
}
106-
if (!Satisfied && !Providers.empty() &&
107-
Ref.RT == RefType::Explicit &&
108-
!HeaderFilter(Providers.front().resolvedPath())) {
109-
// Check if we have any headers with the same spelling, in edge
110-
// cases like `#include_next "foo.h"`, the user can't ever
111-
// include the physical foo.h, but can have a spelling that
112-
// refers to it.
113-
auto Spelling = spellHeader(
114-
{Providers.front(), PP.getHeaderSearchInfo(), MainFile});
115-
for (const Include *I : Inc.match(Header{Spelling})) {
116-
Used.insert(I);
117-
Satisfied = true;
118-
}
119-
if (!Satisfied)
120-
Missing.insert(std::move(Spelling));
106+
// Bail out if we can't (or need not) insert an include.
107+
if (Satisfied || Providers.empty() || Ref.RT != RefType::Explicit)
108+
return;
109+
if (HeaderFilter(Providers.front().resolvedPath()))
110+
return;
111+
// Check if we have any headers with the same spelling, in edge
112+
// cases like `#include_next "foo.h"`, the user can't ever
113+
// include the physical foo.h, but can have a spelling that
114+
// refers to it.
115+
auto Spelling = spellHeader(
116+
{Providers.front(), PP.getHeaderSearchInfo(), MainFile});
117+
for (const Include *I : Inc.match(Header{Spelling})) {
118+
Used.insert(I);
119+
Satisfied = true;
121120
}
121+
if (!Satisfied)
122+
Missing.insert(std::move(Spelling));
122123
});
123124

124125
AnalysisResults Results;
125126
for (const Include &I : Inc.all()) {
126127
if (Used.contains(&I) || !I.Resolved ||
127-
HeaderFilter(I.Resolved->getFileEntry().tryGetRealPathName()) ||
128-
(ResourceDir && I.Resolved->getFileEntry().getDir() == *ResourceDir))
128+
HeaderFilter(I.Resolved->getName()) ||
129+
I.Resolved->getDir() == ResourceDir)
129130
continue;
130131
if (PI) {
131132
if (PI->shouldKeep(*I.Resolved))
@@ -137,7 +138,7 @@ analyze(llvm::ArrayRef<Decl *> ASTRoots,
137138
// Since most private -> public mappings happen in a verbatim way, we
138139
// check textually here. This might go wrong in presence of symlinks or
139140
// header mappings. But that's not different than rest of the places.
140-
if (MainFile->tryGetRealPathName().ends_with(PHeader))
141+
if (MainFile.getName().ends_with(PHeader))
141142
continue;
142143
}
143144
}

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

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
//===----------------------------------------------------------------------===//
1515

1616
#include "AnalysisInternal.h"
17+
#include "clang-include-cleaner/IncludeSpeller.h"
1718
#include "clang-include-cleaner/Types.h"
1819
#include "clang/AST/ASTContext.h"
1920
#include "clang/AST/PrettyPrinter.h"
@@ -167,22 +168,6 @@ class Reporter {
167168
return "semiused";
168169
}
169170

170-
std::string spellHeader(const Header &H) {
171-
switch (H.kind()) {
172-
case Header::Physical: {
173-
bool IsAngled = false;
174-
std::string Path = HS.suggestPathToFileForDiagnostics(
175-
H.physical(), MainFE->tryGetRealPathName(), &IsAngled);
176-
return IsAngled ? "<" + Path + ">" : "\"" + Path + "\"";
177-
}
178-
case Header::Standard:
179-
return H.standard().name().str();
180-
case Header::Verbatim:
181-
return H.verbatim().str();
182-
}
183-
llvm_unreachable("Unknown Header kind");
184-
}
185-
186171
void fillTarget(Ref &R) {
187172
// Duplicates logic from walkUsed(), which doesn't expose SymbolLocations.
188173
for (auto &Loc : locateSymbol(R.Sym))
@@ -204,7 +189,7 @@ class Reporter {
204189
R.Includes.end());
205190

206191
if (!R.Headers.empty())
207-
R.Insert = spellHeader(R.Headers.front());
192+
R.Insert = spellHeader({R.Headers.front(), HS, MainFE});
208193
}
209194

210195
public:

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
#include "TypesInternal.h"
1111
#include "clang/AST/Decl.h"
1212
#include "clang/Basic/FileEntry.h"
13-
#include "llvm/ADT/ArrayRef.h"
1413
#include "llvm/ADT/STLExtras.h"
1514
#include "llvm/ADT/SmallString.h"
1615
#include "llvm/ADT/SmallVector.h"
@@ -48,7 +47,7 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Symbol &S) {
4847
llvm::StringRef Header::resolvedPath() const {
4948
switch (kind()) {
5049
case include_cleaner::Header::Physical:
51-
return physical().getFileEntry().tryGetRealPathName();
50+
return physical().getName();
5251
case include_cleaner::Header::Standard:
5352
return standard().name().trim("<>\"");
5453
case include_cleaner::Header::Verbatim:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ class Action : public clang::ASTFrontendAction {
164164
writeHTML();
165165

166166
llvm::StringRef Path =
167-
SM.getFileEntryForID(SM.getMainFileID())->tryGetRealPathName();
167+
SM.getFileEntryRefForID(SM.getMainFileID())->getName();
168168
assert(!Path.empty() && "Main file path not known?");
169169
llvm::StringRef Code = SM.getBufferData(SM.getMainFileID());
170170

clang-tools-extra/include-cleaner/unittests/AnalysisTest.cpp

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222
#include "clang/Testing/TestAST.h"
2323
#include "clang/Tooling/Inclusions/StandardLibrary.h"
2424
#include "llvm/ADT/ArrayRef.h"
25+
#include "llvm/ADT/IntrusiveRefCntPtr.h"
2526
#include "llvm/ADT/SmallVector.h"
2627
#include "llvm/ADT/StringRef.h"
28+
#include "llvm/Support/MemoryBuffer.h"
2729
#include "llvm/Support/ScopedPrinter.h"
30+
#include "llvm/Support/VirtualFileSystem.h"
2831
#include "llvm/Testing/Annotations/Annotations.h"
2932
#include "gmock/gmock.h"
3033
#include "gtest/gtest.h"
@@ -204,21 +207,37 @@ class AnalyzeTest : public testing::Test {
204207
TestInputs Inputs;
205208
PragmaIncludes PI;
206209
RecordedPP PP;
210+
llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> ExtraFS = nullptr;
211+
207212
AnalyzeTest() {
208213
Inputs.MakeAction = [this] {
209214
struct Hook : public SyntaxOnlyAction {
210215
public:
211-
Hook(RecordedPP &PP, PragmaIncludes &PI) : PP(PP), PI(PI) {}
216+
Hook(RecordedPP &PP, PragmaIncludes &PI,
217+
llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> ExtraFS)
218+
: PP(PP), PI(PI), ExtraFS(std::move(ExtraFS)) {}
212219
bool BeginSourceFileAction(clang::CompilerInstance &CI) override {
213220
CI.getPreprocessor().addPPCallbacks(PP.record(CI.getPreprocessor()));
214221
PI.record(CI);
215222
return true;
216223
}
217224

225+
bool BeginInvocation(CompilerInstance &CI) override {
226+
if (!ExtraFS)
227+
return true;
228+
auto OverlayFS =
229+
llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(
230+
CI.getFileManager().getVirtualFileSystemPtr());
231+
OverlayFS->pushOverlay(ExtraFS);
232+
CI.getFileManager().setVirtualFileSystem(std::move(OverlayFS));
233+
return true;
234+
}
235+
218236
RecordedPP &PP;
219237
PragmaIncludes &PI;
238+
llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> ExtraFS;
220239
};
221-
return std::make_unique<Hook>(PP, PI);
240+
return std::make_unique<Hook>(PP, PI, ExtraFS);
222241
};
223242
}
224243
};
@@ -322,6 +341,58 @@ TEST_F(AnalyzeTest, DifferentHeaderSameSpelling) {
322341
EXPECT_THAT(Results.Missing, testing::IsEmpty());
323342
}
324343

344+
TEST_F(AnalyzeTest, SpellingIncludesWithSymlinks) {
345+
llvm::Annotations Code(R"cpp(
346+
#include "header.h"
347+
void $bar^bar() {
348+
$foo^foo();
349+
}
350+
)cpp");
351+
Inputs.Code = Code.code();
352+
ExtraFS = llvm::makeIntrusiveRefCnt<llvm::vfs::InMemoryFileSystem>();
353+
ExtraFS->addFile("content_for/0", /*ModificationTime=*/{},
354+
llvm::MemoryBuffer::getMemBufferCopy(guard(R"cpp(
355+
#include "inner.h"
356+
)cpp")));
357+
ExtraFS->addSymbolicLink("header.h", "content_for/0",
358+
/*ModificationTime=*/{});
359+
ExtraFS->addFile("content_for/1", /*ModificationTime=*/{},
360+
llvm::MemoryBuffer::getMemBufferCopy(guard(R"cpp(
361+
void foo();
362+
)cpp")));
363+
ExtraFS->addSymbolicLink("inner.h", "content_for/1",
364+
/*ModificationTime=*/{});
365+
366+
TestAST AST(Inputs);
367+
std::vector<Decl *> DeclsInTU;
368+
for (auto *D : AST.context().getTranslationUnitDecl()->decls())
369+
DeclsInTU.push_back(D);
370+
auto Results = analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor());
371+
// Check that we're spelling header using the symlink, and not underlying
372+
// path.
373+
EXPECT_THAT(Results.Missing, testing::ElementsAre("\"inner.h\""));
374+
// header.h should be unused.
375+
EXPECT_THAT(Results.Unused, Not(testing::IsEmpty()));
376+
377+
{
378+
// Make sure filtering is also applied to symlink, not underlying file.
379+
auto HeaderFilter = [](llvm::StringRef Path) { return Path == "inner.h"; };
380+
Results = analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor(),
381+
HeaderFilter);
382+
EXPECT_THAT(Results.Missing, testing::ElementsAre("\"inner.h\""));
383+
// header.h should be unused.
384+
EXPECT_THAT(Results.Unused, Not(testing::IsEmpty()));
385+
}
386+
{
387+
auto HeaderFilter = [](llvm::StringRef Path) { return Path == "header.h"; };
388+
Results = analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor(),
389+
HeaderFilter);
390+
// header.h should be ignored now.
391+
EXPECT_THAT(Results.Unused, Not(testing::IsEmpty()));
392+
EXPECT_THAT(Results.Missing, testing::ElementsAre("\"inner.h\""));
393+
}
394+
}
395+
325396
TEST(FixIncludes, Basic) {
326397
llvm::StringRef Code = R"cpp(#include "d.h"
327398
#include "a.h"

clang-tools-extra/include-cleaner/unittests/IncludeSpellerTest.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ class DummyIncludeSpeller : public IncludeSpeller {
4747
return "<bits/stdc++.h>";
4848
if (Input.H.kind() != Header::Physical)
4949
return "";
50-
llvm::StringRef AbsolutePath =
51-
Input.H.physical().getFileEntry().tryGetRealPathName();
50+
llvm::StringRef AbsolutePath = Input.H.resolvedPath();
5251
std::string RootWithSeparator{testRoot()};
5352
RootWithSeparator += llvm::sys::path::get_separator();
5453
if (!AbsolutePath.consume_front(llvm::StringRef{RootWithSeparator}))

clang-tools-extra/include-cleaner/unittests/RecordTest.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ MATCHER_P(named, N, "") {
5353
}
5454

5555
MATCHER_P(FileNamed, N, "") {
56-
if (arg.getFileEntry().tryGetRealPathName() == N)
56+
llvm::StringRef ActualName = arg.getName();
57+
ActualName.consume_front("./");
58+
if (ActualName == N)
5759
return true;
58-
*result_listener << arg.getFileEntry().tryGetRealPathName().str();
60+
*result_listener << ActualName.str();
5961
return false;
6062
}
6163

0 commit comments

Comments
 (0)