Skip to content

Commit 2f5dc59

Browse files
authored
[IncludeCleaner] Also check for spellings of physical headers (#99843)
Some physical headers can have "conflicting" spellings, when same filename exists under two different include search paths. e.g. <stdlib.h> can be provided by both standard library and underlying libc headers. In such scenarios if the usage is from the latter include-search path, users can't spell it directly. This patch ensures we also consider spellings of such includes, in addition to their physical files to prevent conflicting suggestions.
1 parent 5b8479b commit 2f5dc59

File tree

4 files changed

+82
-3
lines changed

4 files changed

+82
-3
lines changed

clang-tools-extra/clangd/IncludeCleaner.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,26 @@ computeIncludeCleanerFindings(ParsedAST &AST, bool AnalyzeAngledIncludes) {
401401
Ref.RT != include_cleaner::RefType::Explicit)
402402
return;
403403

404+
// Check if we have any headers with the same spelling, in edge cases
405+
// like `#include_next "foo.h"`, the user can't ever include the
406+
// physical foo.h, but can have a spelling that refers to it.
407+
// We postpone this check because spelling a header for every usage is
408+
// expensive.
409+
std::string Spelling = include_cleaner::spellHeader(
410+
{Providers.front(), AST.getPreprocessor().getHeaderSearchInfo(),
411+
MainFile});
412+
for (auto *Inc :
413+
ConvertedIncludes.match(include_cleaner::Header{Spelling})) {
414+
Satisfied = true;
415+
auto HeaderID =
416+
AST.getIncludeStructure().getID(&Inc->Resolved->getFileEntry());
417+
assert(HeaderID.has_value() &&
418+
"ConvertedIncludes only contains resolved includes.");
419+
Used.insert(*HeaderID);
420+
}
421+
if (Satisfied)
422+
return;
423+
404424
// We actually always want to map usages to their spellings, but
405425
// spelling locations can point into preamble section. Using these
406426
// offsets could lead into crashes in presence of stale preambles. Hence

clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,28 @@ TEST(IncludeCleaner, ResourceDirIsIgnored) {
644644
EXPECT_THAT(Findings.MissingIncludes, IsEmpty());
645645
}
646646

647+
TEST(IncludeCleaner, DifferentHeaderSameSpelling) {
648+
// `foo` is declared in foo_inner/foo.h, but there's no way to spell it
649+
// directly. Make sure we don't generate unusued/missing include findings in
650+
// such cases.
651+
auto TU = TestTU::withCode(R"cpp(
652+
#include <foo.h>
653+
void baz() {
654+
foo();
655+
}
656+
)cpp");
657+
TU.AdditionalFiles["foo/foo.h"] = guard("#include_next <foo.h>");
658+
TU.AdditionalFiles["foo_inner/foo.h"] = guard(R"cpp(
659+
void foo();
660+
)cpp");
661+
TU.ExtraArgs.push_back("-Ifoo");
662+
TU.ExtraArgs.push_back("-Ifoo_inner");
663+
664+
auto AST = TU.build();
665+
auto Findings = computeIncludeCleanerFindings(AST);
666+
EXPECT_THAT(Findings.UnusedIncludes, IsEmpty());
667+
EXPECT_THAT(Findings.MissingIncludes, IsEmpty());
668+
}
647669
} // namespace
648670
} // namespace clangd
649671
} // namespace clang

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,20 @@ analyze(llvm::ArrayRef<Decl *> ASTRoots,
105105
}
106106
if (!Satisfied && !Providers.empty() &&
107107
Ref.RT == RefType::Explicit &&
108-
!HeaderFilter(Providers.front().resolvedPath()))
109-
Missing.insert(spellHeader(
110-
{Providers.front(), PP.getHeaderSearchInfo(), MainFile}));
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));
121+
}
111122
});
112123

113124
AnalysisResults Results;

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "clang-include-cleaner/Record.h"
1313
#include "clang-include-cleaner/Types.h"
1414
#include "clang/AST/ASTContext.h"
15+
#include "clang/AST/DeclBase.h"
1516
#include "clang/Basic/FileManager.h"
1617
#include "clang/Basic/IdentifierTable.h"
1718
#include "clang/Basic/SourceLocation.h"
@@ -296,6 +297,31 @@ TEST_F(AnalyzeTest, ResourceDirIsIgnored) {
296297
EXPECT_THAT(Results.Missing, testing::IsEmpty());
297298
}
298299

300+
TEST_F(AnalyzeTest, DifferentHeaderSameSpelling) {
301+
Inputs.ExtraArgs.push_back("-Ifoo");
302+
Inputs.ExtraArgs.push_back("-Ifoo_inner");
303+
// `foo` is declared in foo_inner/foo.h, but there's no way to spell it
304+
// directly. Make sure we don't generate unusued/missing include findings in
305+
// such cases.
306+
Inputs.Code = R"cpp(
307+
#include <foo.h>
308+
void baz() {
309+
foo();
310+
}
311+
)cpp";
312+
Inputs.ExtraFiles["foo/foo.h"] = guard("#include_next <foo.h>");
313+
Inputs.ExtraFiles["foo_inner/foo.h"] = guard(R"cpp(
314+
void foo();
315+
)cpp");
316+
TestAST AST(Inputs);
317+
std::vector<Decl *> DeclsInTU;
318+
for (auto *D : AST.context().getTranslationUnitDecl()->decls())
319+
DeclsInTU.push_back(D);
320+
auto Results = analyze(DeclsInTU, {}, PP.Includes, &PI, AST.preprocessor());
321+
EXPECT_THAT(Results.Unused, testing::IsEmpty());
322+
EXPECT_THAT(Results.Missing, testing::IsEmpty());
323+
}
324+
299325
TEST(FixIncludes, Basic) {
300326
llvm::StringRef Code = R"cpp(#include "d.h"
301327
#include "a.h"

0 commit comments

Comments
 (0)