Skip to content

Commit 8c5a7a1

Browse files
authored
[clangd] Add config option to allow detection of unused angled includes (#87208)
This PR adds a new `AnalyzeAngledIncludes` option to `Includes` section of clangd config. This option enables unused include checks for all includes that use the `<>` syntax, not just standard library includes.
1 parent 737a301 commit 8c5a7a1

11 files changed

+144
-38
lines changed

Diff for: clang-tools-extra/clangd/Config.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,11 @@ struct Config {
110110
IncludesPolicy UnusedIncludes = IncludesPolicy::Strict;
111111
IncludesPolicy MissingIncludes = IncludesPolicy::None;
112112

113-
/// IncludeCleaner will not diagnose usages of these headers matched by
114-
/// these regexes.
115113
struct {
114+
/// IncludeCleaner will not diagnose usages of these headers matched by
115+
/// these regexes.
116116
std::vector<std::function<bool(llvm::StringRef)>> IgnoreHeader;
117+
bool AnalyzeAngledIncludes = false;
117118
} Includes;
118119
} Diagnostics;
119120

Diff for: clang-tools-extra/clangd/ConfigCompile.cpp

+37-23
Original file line numberDiff line numberDiff line change
@@ -572,32 +572,46 @@ struct FragmentCompiler {
572572
#else
573573
static llvm::Regex::RegexFlags Flags = llvm::Regex::NoFlags;
574574
#endif
575-
auto Filters = std::make_shared<std::vector<llvm::Regex>>();
576-
for (auto &HeaderPattern : F.IgnoreHeader) {
577-
// Anchor on the right.
578-
std::string AnchoredPattern = "(" + *HeaderPattern + ")$";
579-
llvm::Regex CompiledRegex(AnchoredPattern, Flags);
580-
std::string RegexError;
581-
if (!CompiledRegex.isValid(RegexError)) {
582-
diag(Warning,
583-
llvm::formatv("Invalid regular expression '{0}': {1}",
584-
*HeaderPattern, RegexError)
585-
.str(),
586-
HeaderPattern.Range);
587-
continue;
575+
std::shared_ptr<std::vector<llvm::Regex>> Filters;
576+
if (!F.IgnoreHeader.empty()) {
577+
Filters = std::make_shared<std::vector<llvm::Regex>>();
578+
for (auto &HeaderPattern : F.IgnoreHeader) {
579+
// Anchor on the right.
580+
std::string AnchoredPattern = "(" + *HeaderPattern + ")$";
581+
llvm::Regex CompiledRegex(AnchoredPattern, Flags);
582+
std::string RegexError;
583+
if (!CompiledRegex.isValid(RegexError)) {
584+
diag(Warning,
585+
llvm::formatv("Invalid regular expression '{0}': {1}",
586+
*HeaderPattern, RegexError)
587+
.str(),
588+
HeaderPattern.Range);
589+
continue;
590+
}
591+
Filters->push_back(std::move(CompiledRegex));
588592
}
589-
Filters->push_back(std::move(CompiledRegex));
590593
}
591-
if (Filters->empty())
594+
// Optional to override the resulting AnalyzeAngledIncludes
595+
// only if it's explicitly set in the current fragment.
596+
// Otherwise it's inherited from parent fragment.
597+
std::optional<bool> AnalyzeAngledIncludes;
598+
if (F.AnalyzeAngledIncludes.has_value())
599+
AnalyzeAngledIncludes = **F.AnalyzeAngledIncludes;
600+
if (!Filters && !AnalyzeAngledIncludes.has_value())
592601
return;
593-
auto Filter = [Filters](llvm::StringRef Path) {
594-
for (auto &Regex : *Filters)
595-
if (Regex.match(Path))
596-
return true;
597-
return false;
598-
};
599-
Out.Apply.push_back([Filter](const Params &, Config &C) {
600-
C.Diagnostics.Includes.IgnoreHeader.emplace_back(Filter);
602+
Out.Apply.push_back([Filters = std::move(Filters),
603+
AnalyzeAngledIncludes](const Params &, Config &C) {
604+
if (Filters) {
605+
auto Filter = [Filters](llvm::StringRef Path) {
606+
for (auto &Regex : *Filters)
607+
if (Regex.match(Path))
608+
return true;
609+
return false;
610+
};
611+
C.Diagnostics.Includes.IgnoreHeader.emplace_back(std::move(Filter));
612+
}
613+
if (AnalyzeAngledIncludes.has_value())
614+
C.Diagnostics.Includes.AnalyzeAngledIncludes = *AnalyzeAngledIncludes;
601615
});
602616
}
603617

Diff for: clang-tools-extra/clangd/ConfigFragment.h

+4
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ struct Fragment {
254254
/// unused or missing. These can match any suffix of the header file in
255255
/// question.
256256
std::vector<Located<std::string>> IgnoreHeader;
257+
258+
/// If false (default), unused system headers will be ignored.
259+
/// Standard library headers are analyzed regardless of this option.
260+
std::optional<Located<bool>> AnalyzeAngledIncludes;
257261
};
258262
IncludesBlock Includes;
259263

Diff for: clang-tools-extra/clangd/ConfigYAML.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ class Parser {
169169
if (auto Values = scalarValues(N))
170170
F.IgnoreHeader = std::move(*Values);
171171
});
172+
Dict.handle("AnalyzeAngledIncludes", [&](Node &N) {
173+
if (auto Value = boolValue(N, "AnalyzeAngledIncludes"))
174+
F.AnalyzeAngledIncludes = *Value;
175+
});
172176
Dict.parse(N);
173177
}
174178

Diff for: clang-tools-extra/clangd/IncludeCleaner.cpp

+21-11
Original file line numberDiff line numberDiff line change
@@ -68,24 +68,30 @@ bool isIgnored(llvm::StringRef HeaderPath, HeaderFilter IgnoreHeaders) {
6868
}
6969

7070
bool mayConsiderUnused(const Inclusion &Inc, ParsedAST &AST,
71-
const include_cleaner::PragmaIncludes *PI) {
71+
const include_cleaner::PragmaIncludes *PI,
72+
bool AnalyzeAngledIncludes) {
7273
assert(Inc.HeaderID);
7374
auto HID = static_cast<IncludeStructure::HeaderID>(*Inc.HeaderID);
7475
auto FE = AST.getSourceManager().getFileManager().getFileRef(
7576
AST.getIncludeStructure().getRealPath(HID));
7677
assert(FE);
7778
if (FE->getDir() == AST.getPreprocessor()
78-
.getHeaderSearchInfo()
79-
.getModuleMap()
80-
.getBuiltinDir())
79+
.getHeaderSearchInfo()
80+
.getModuleMap()
81+
.getBuiltinDir())
8182
return false;
8283
if (PI && PI->shouldKeep(*FE))
8384
return false;
8485
// FIXME(kirillbobyrev): We currently do not support the umbrella headers.
8586
// System headers are likely to be standard library headers.
86-
// Until we have good support for umbrella headers, don't warn about them.
87-
if (Inc.Written.front() == '<')
88-
return tooling::stdlib::Header::named(Inc.Written).has_value();
87+
// Until we have good support for umbrella headers, don't warn about them
88+
// (unless analysis is explicitly enabled).
89+
if (Inc.Written.front() == '<') {
90+
if (tooling::stdlib::Header::named(Inc.Written))
91+
return true;
92+
if (!AnalyzeAngledIncludes)
93+
return false;
94+
}
8995
if (PI) {
9096
// Check if main file is the public interface for a private header. If so we
9197
// shouldn't diagnose it as unused.
@@ -266,7 +272,8 @@ Fix fixAll(const Fix &RemoveAllUnused, const Fix &AddAllMissing) {
266272

267273
std::vector<const Inclusion *>
268274
getUnused(ParsedAST &AST,
269-
const llvm::DenseSet<IncludeStructure::HeaderID> &ReferencedFiles) {
275+
const llvm::DenseSet<IncludeStructure::HeaderID> &ReferencedFiles,
276+
bool AnalyzeAngledIncludes) {
270277
trace::Span Tracer("IncludeCleaner::getUnused");
271278
std::vector<const Inclusion *> Unused;
272279
for (const Inclusion &MFI : AST.getIncludeStructure().MainFileIncludes) {
@@ -275,7 +282,8 @@ getUnused(ParsedAST &AST,
275282
auto IncludeID = static_cast<IncludeStructure::HeaderID>(*MFI.HeaderID);
276283
if (ReferencedFiles.contains(IncludeID))
277284
continue;
278-
if (!mayConsiderUnused(MFI, AST, &AST.getPragmaIncludes())) {
285+
if (!mayConsiderUnused(MFI, AST, &AST.getPragmaIncludes(),
286+
AnalyzeAngledIncludes)) {
279287
dlog("{0} was not used, but is not eligible to be diagnosed as unused",
280288
MFI.Written);
281289
continue;
@@ -347,7 +355,8 @@ include_cleaner::Includes convertIncludes(const ParsedAST &AST) {
347355
return ConvertedIncludes;
348356
}
349357

350-
IncludeCleanerFindings computeIncludeCleanerFindings(ParsedAST &AST) {
358+
IncludeCleanerFindings
359+
computeIncludeCleanerFindings(ParsedAST &AST, bool AnalyzeAngledIncludes) {
351360
// Interaction is only polished for C/CPP.
352361
if (AST.getLangOpts().ObjC)
353362
return {};
@@ -432,7 +441,8 @@ IncludeCleanerFindings computeIncludeCleanerFindings(ParsedAST &AST) {
432441
MapInfo::getHashValue(RHS.Symbol);
433442
});
434443
MissingIncludes.erase(llvm::unique(MissingIncludes), MissingIncludes.end());
435-
std::vector<const Inclusion *> UnusedIncludes = getUnused(AST, Used);
444+
std::vector<const Inclusion *> UnusedIncludes =
445+
getUnused(AST, Used, AnalyzeAngledIncludes);
436446
return {std::move(UnusedIncludes), std::move(MissingIncludes)};
437447
}
438448

Diff for: clang-tools-extra/clangd/IncludeCleaner.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ struct IncludeCleanerFindings {
5353
std::vector<MissingIncludeDiagInfo> MissingIncludes;
5454
};
5555

56-
IncludeCleanerFindings computeIncludeCleanerFindings(ParsedAST &AST);
56+
IncludeCleanerFindings
57+
computeIncludeCleanerFindings(ParsedAST &AST,
58+
bool AnalyzeAngledIncludes = false);
5759

5860
using HeaderFilter = llvm::ArrayRef<std::function<bool(llvm::StringRef)>>;
5961
std::vector<Diag>

Diff for: clang-tools-extra/clangd/ParsedAST.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,8 @@ std::vector<Diag> getIncludeCleanerDiags(ParsedAST &AST, llvm::StringRef Code,
373373
Cfg.Diagnostics.UnusedIncludes == Config::IncludesPolicy::None;
374374
if (SuppressMissing && SuppressUnused)
375375
return {};
376-
auto Findings = computeIncludeCleanerFindings(AST);
376+
auto Findings = computeIncludeCleanerFindings(
377+
AST, Cfg.Diagnostics.Includes.AnalyzeAngledIncludes);
377378
if (SuppressMissing)
378379
Findings.MissingIncludes.clear();
379380
if (SuppressUnused)

Diff for: clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,12 @@ TEST_F(ConfigCompileTests, DiagnosticsIncludeCleaner) {
277277
};
278278
EXPECT_TRUE(HeaderFilter("foo.h"));
279279
EXPECT_FALSE(HeaderFilter("bar.h"));
280+
281+
Frag = {};
282+
EXPECT_FALSE(Conf.Diagnostics.Includes.AnalyzeAngledIncludes);
283+
Frag.Diagnostics.Includes.AnalyzeAngledIncludes = true;
284+
EXPECT_TRUE(compileAndApply());
285+
EXPECT_TRUE(Conf.Diagnostics.Includes.AnalyzeAngledIncludes);
280286
}
281287

282288
TEST_F(ConfigCompileTests, DiagnosticSuppression) {

Diff for: clang-tools-extra/clangd/unittests/ConfigYAMLTests.cpp

+15
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,21 @@ TEST(ParseYAML, IncludesIgnoreHeader) {
278278
ElementsAre(val("foo"), val("bar")));
279279
}
280280

281+
TEST(ParseYAML, IncludesAnalyzeAngledIncludes) {
282+
CapturedDiags Diags;
283+
Annotations YAML(R"yaml(
284+
Diagnostics:
285+
Includes:
286+
AnalyzeAngledIncludes: true
287+
)yaml");
288+
auto Results =
289+
Fragment::parseYAML(YAML.code(), "config.yaml", Diags.callback());
290+
ASSERT_THAT(Diags.Diagnostics, IsEmpty());
291+
ASSERT_EQ(Results.size(), 1u);
292+
EXPECT_THAT(Results[0].Diagnostics.Includes.AnalyzeAngledIncludes,
293+
llvm::ValueIs(val(true)));
294+
}
295+
281296
TEST(ParseYAML, Style) {
282297
CapturedDiags Diags;
283298
Annotations YAML(R"yaml(

Diff for: clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp

+44
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ TEST(IncludeCleaner, GetUnusedHeaders) {
108108
#include "unguarded.h"
109109
#include "unused.h"
110110
#include <system_header.h>
111+
#include <non_system_angled_header.h>
111112
void foo() {
112113
a();
113114
b();
@@ -122,6 +123,7 @@ TEST(IncludeCleaner, GetUnusedHeaders) {
122123
TU.AdditionalFiles["dir/c.h"] = guard("void c();");
123124
TU.AdditionalFiles["unused.h"] = guard("void unused();");
124125
TU.AdditionalFiles["dir/unused.h"] = guard("void dirUnused();");
126+
TU.AdditionalFiles["dir/non_system_angled_header.h"] = guard("");
125127
TU.AdditionalFiles["system/system_header.h"] = guard("");
126128
TU.AdditionalFiles["unguarded.h"] = "";
127129
TU.ExtraArgs.push_back("-I" + testPath("dir"));
@@ -135,6 +137,48 @@ TEST(IncludeCleaner, GetUnusedHeaders) {
135137
Pointee(writtenInclusion("\"dir/unused.h\""))));
136138
}
137139

140+
TEST(IncludeCleaner, IgnoredAngledHeaders) {
141+
// Currently the default behavior is to ignore unused angled includes
142+
auto TU = TestTU::withCode(R"cpp(
143+
#include <system_header.h>
144+
#include <system_unused.h>
145+
#include <non_system_angled_unused.h>
146+
SystemClass x;
147+
)cpp");
148+
TU.AdditionalFiles["system/system_header.h"] = guard("class SystemClass {};");
149+
TU.AdditionalFiles["system/system_unused.h"] = guard("");
150+
TU.AdditionalFiles["dir/non_system_angled_unused.h"] = guard("");
151+
TU.ExtraArgs = {
152+
"-isystem" + testPath("system"),
153+
"-I" + testPath("dir"),
154+
};
155+
auto AST = TU.build();
156+
IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST);
157+
EXPECT_THAT(Findings.UnusedIncludes, IsEmpty());
158+
}
159+
160+
TEST(IncludeCleaner, UnusedAngledHeaders) {
161+
auto TU = TestTU::withCode(R"cpp(
162+
#include <system_header.h>
163+
#include <system_unused.h>
164+
#include <non_system_angled_unused.h>
165+
SystemClass x;
166+
)cpp");
167+
TU.AdditionalFiles["system/system_header.h"] = guard("class SystemClass {};");
168+
TU.AdditionalFiles["system/system_unused.h"] = guard("");
169+
TU.AdditionalFiles["dir/non_system_angled_unused.h"] = guard("");
170+
TU.ExtraArgs = {
171+
"-isystem" + testPath("system"),
172+
"-I" + testPath("dir"),
173+
};
174+
auto AST = TU.build();
175+
IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST, true);
176+
EXPECT_THAT(Findings.UnusedIncludes,
177+
UnorderedElementsAre(
178+
Pointee(writtenInclusion("<system_unused.h>")),
179+
Pointee(writtenInclusion("<non_system_angled_unused.h>"))));
180+
}
181+
138182
TEST(IncludeCleaner, ComputeMissingHeaders) {
139183
Annotations MainFile(R"cpp(
140184
#include "a.h"

Diff for: clang-tools-extra/docs/ReleaseNotes.rst

+5
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ Objective-C
8484
Miscellaneous
8585
^^^^^^^^^^^^^
8686

87+
- Added a boolean option `AnalyzeAngledIncludes` to `Includes` config section,
88+
which allows to enable unused includes detection for all angled ("system") headers.
89+
At this moment umbrella headers are not supported, so enabling this option
90+
may result in false-positives.
91+
8792
Improvements to clang-doc
8893
-------------------------
8994

0 commit comments

Comments
 (0)