16#include "clang-include-cleaner/Analysis.h"
17#include "clang-include-cleaner/IncludeSpeller.h"
18#include "clang-include-cleaner/Record.h"
19#include "clang-include-cleaner/Types.h"
23#include "clang/AST/ASTContext.h"
24#include "clang/Basic/Diagnostic.h"
25#include "clang/Basic/LLVM.h"
26#include "clang/Basic/SourceLocation.h"
27#include "clang/Basic/SourceManager.h"
28#include "clang/Format/Format.h"
29#include "clang/Lex/DirectoryLookup.h"
30#include "clang/Lex/HeaderSearch.h"
31#include "clang/Lex/Preprocessor.h"
32#include "clang/Tooling/Core/Replacement.h"
33#include "clang/Tooling/Inclusions/HeaderIncludes.h"
34#include "clang/Tooling/Inclusions/StandardLibrary.h"
35#include "clang/Tooling/Syntax/Tokens.h"
36#include "llvm/ADT/ArrayRef.h"
37#include "llvm/ADT/DenseSet.h"
38#include "llvm/ADT/GenericUniformityImpl.h"
39#include "llvm/ADT/STLExtras.h"
40#include "llvm/ADT/SmallString.h"
41#include "llvm/ADT/SmallVector.h"
42#include "llvm/ADT/StringRef.h"
43#include "llvm/Support/Error.h"
44#include "llvm/Support/ErrorHandling.h"
45#include "llvm/Support/FormatVariadic.h"
46#include "llvm/Support/Path.h"
47#include "llvm/Support/Regex.h"
59bool isIgnored(llvm::StringRef HeaderPath,
HeaderFilter IgnoreHeaders) {
61 llvm::SmallString<64> NormalizedPath(HeaderPath);
62 llvm::sys::path::native(NormalizedPath, llvm::sys::path::Style::posix);
63 for (
auto &Filter : IgnoreHeaders) {
64 if (Filter(NormalizedPath))
70bool mayConsiderUnused(
const Inclusion &Inc, ParsedAST &
AST,
71 const include_cleaner::PragmaIncludes *PI,
72 bool AnalyzeAngledIncludes) {
75 auto FE =
AST.getSourceManager().getFileManager().getFileRef(
76 AST.getIncludeStructure().getRealPath(HID));
78 if (FE->getDir() ==
AST.getPreprocessor()
79 .getHeaderSearchInfo()
83 if (PI && PI->shouldKeep(*FE))
89 if (Inc.Written.front() ==
'<') {
90 if (tooling::stdlib::Header::named(Inc.Written))
92 if (!AnalyzeAngledIncludes)
98 if (
auto PHeader = PI->getPublic(*FE); !PHeader.empty()) {
99 PHeader = PHeader.trim(
"<>\"");
103 if (
AST.tuPath().ends_with(PHeader))
109 if (!
AST.getPreprocessor().getHeaderSearchInfo().isFileMultipleIncludeGuarded(
111 dlog(
"{0} doesn't have header guard and will not be considered unused",
118std::vector<Diag> generateMissingIncludeDiagnostics(
119 ParsedAST &
AST, llvm::ArrayRef<MissingIncludeDiagInfo> MissingIncludes,
120 llvm::StringRef Code,
HeaderFilter IgnoreHeaders,
const ThreadsafeFS &TFS) {
121 std::vector<Diag> Result;
122 const SourceManager &SM =
AST.getSourceManager();
123 const FileEntry *
MainFile = SM.getFileEntryForID(SM.getMainFileID());
127 tooling::HeaderIncludes HeaderIncludes(
AST.tuPath(), Code,
128 FileStyle.IncludeStyle);
129 for (
const auto &SymbolWithMissingInclude : MissingIncludes) {
130 llvm::StringRef ResolvedPath =
131 SymbolWithMissingInclude.Providers.front().resolvedPath();
132 if (isIgnored(ResolvedPath, IgnoreHeaders)) {
133 dlog(
"IncludeCleaner: not diagnosing missing include {0}, filtered by "
139 std::string Spelling = include_cleaner::spellHeader(
140 {SymbolWithMissingInclude.Providers.front(),
141 AST.getPreprocessor().getHeaderSearchInfo(),
MainFile});
143 llvm::StringRef HeaderRef{Spelling};
144 bool Angled = HeaderRef.starts_with(
"<");
149 std::optional<tooling::Replacement> Replacement = HeaderIncludes.insert(
150 HeaderRef.trim(
"\"<>"), Angled, tooling::IncludeDirective::Include);
151 if (!Replacement.has_value())
154 Diag &D = Result.emplace_back();
156 llvm::formatv(
"No header providing \"{0}\" is directly included",
157 SymbolWithMissingInclude.Symbol.name());
158 D.Name =
"missing-includes";
160 D.File =
AST.tuPath();
161 D.InsideMainFile =
true;
178 D.Severity = DiagnosticsEngine::Note;
179 D.Range = clangd::Range{
181 SymbolWithMissingInclude.SymRefRange.beginOffset()),
183 SymbolWithMissingInclude.SymRefRange.endOffset())};
184 auto &F = D.Fixes.emplace_back();
185 F.Message =
"#include " + Spelling;
187 F.Edits.emplace_back(std::move(Edit));
192std::vector<Diag> generateUnusedIncludeDiagnostics(
195 std::vector<Diag> Result;
196 for (
const auto *Inc : UnusedIncludes) {
197 if (isIgnored(Inc->Resolved, IgnoreHeaders))
199 Diag &D = Result.emplace_back();
201 llvm::formatv(
"included header {0} is not used directly",
202 llvm::sys::path::filename(
203 Inc->Written.substr(1, Inc->Written.size() - 2),
204 llvm::sys::path::Style::posix));
205 D.Name =
"unused-includes";
208 D.InsideMainFile =
true;
209 D.Severity = DiagnosticsEngine::Warning;
217 auto &F = D.Fixes.emplace_back();
218 F.Message =
"remove #include directive";
219 F.Edits.emplace_back();
220 F.Edits.back().range.start.line = Inc->HashLine;
221 F.Edits.back().range.end.line = Inc->HashLine + 1;
227removeAllUnusedIncludes(llvm::ArrayRef<Diag> UnusedIncludes) {
228 if (UnusedIncludes.empty())
232 RemoveAll.Message =
"remove all unused includes";
233 for (
const auto &Diag : UnusedIncludes) {
234 assert(Diag.Fixes.size() == 1 &&
"Expected exactly one fix.");
235 RemoveAll.Edits.insert(RemoveAll.Edits.end(),
236 Diag.Fixes.front().Edits.begin(),
237 Diag.Fixes.front().Edits.end());
243addAllMissingIncludes(llvm::ArrayRef<Diag> MissingIncludeDiags) {
244 if (MissingIncludeDiags.empty())
248 AddAllMissing.Message =
"add all missing includes";
251 std::map<std::string, TextEdit> Edits;
252 for (
const auto &Diag : MissingIncludeDiags) {
253 assert(Diag.Fixes.size() == 1 &&
"Expected exactly one fix.");
254 for (
const auto &Edit : Diag.Fixes.front().Edits) {
255 Edits.try_emplace(Edit.newText, Edit);
258 for (
auto &It : Edits)
259 AddAllMissing.Edits.push_back(std::move(It.second));
260 return AddAllMissing;
262Fix fixAll(
const Fix &RemoveAllUnused,
const Fix &AddAllMissing) {
264 FixAll.Message =
"fix all includes";
266 for (
const auto &F : RemoveAllUnused.Edits)
267 FixAll.Edits.push_back(F);
268 for (
const auto &F : AddAllMissing.Edits)
269 FixAll.Edits.push_back(F);
273std::vector<const Inclusion *>
274getUnused(ParsedAST &
AST,
275 const llvm::DenseSet<IncludeStructure::HeaderID> &ReferencedFiles,
276 bool AnalyzeAngledIncludes) {
277 trace::Span Tracer(
"IncludeCleaner::getUnused");
278 std::vector<const Inclusion *>
Unused;
279 for (
const Inclusion &MFI :
AST.getIncludeStructure().MainFileIncludes) {
283 if (ReferencedFiles.contains(IncludeID))
285 if (!mayConsiderUnused(MFI,
AST, &
AST.getPragmaIncludes(),
286 AnalyzeAngledIncludes)) {
287 dlog(
"{0} was not used, but is not eligible to be diagnosed as unused",
298std::vector<include_cleaner::SymbolReference>
300 const auto &SM =
AST.getSourceManager();
301 auto &PP =
AST.getPreprocessor();
302 std::vector<include_cleaner::SymbolReference> Macros;
303 for (
const auto &[_, Refs] :
AST.getMacros().MacroRefs) {
304 for (
const auto &
Ref : Refs) {
305 auto Loc = SM.getComposedLoc(SM.getMainFileID(),
Ref.StartOffset);
306 const auto *Tok =
AST.getTokens().spelledTokenContaining(
Loc);
312 auto DefLoc =
Macro->NameLoc;
313 if (!DefLoc.isValid())
316 {include_cleaner::Macro{PP.getIdentifierInfo(Tok->
text(SM)),
319 Ref.InConditionalDirective ? include_cleaner::RefType::Ambiguous
320 : include_cleaner::RefType::Explicit});
328 auto &SM =
AST.getSourceManager();
330 include_cleaner::Includes ConvertedIncludes;
333 for (
const auto &
Dir :
AST.getIncludeStructure().SearchPathsCanonical)
334 ConvertedIncludes.addSearchDirectory(
Dir);
336 for (
const Inclusion &Inc :
AST.getIncludeStructure().MainFileIncludes) {
337 include_cleaner::Include TransformedInc;
338 llvm::StringRef WrittenRef = llvm::StringRef(Inc.Written);
339 TransformedInc.Spelled = WrittenRef.trim(
"\"<>");
340 TransformedInc.HashLocation =
341 SM.getComposedLoc(SM.getMainFileID(), Inc.HashOffset);
342 TransformedInc.Line = Inc.HashLine + 1;
343 TransformedInc.Angled = WrittenRef.starts_with(
"<");
346 auto FE = SM.getFileManager().getFileRef(Inc.Resolved);
348 elog(
"IncludeCleaner: Failed to get an entry for resolved path {0}: {1}",
349 Inc.Resolved, FE.takeError());
352 TransformedInc.Resolved = *FE;
353 ConvertedIncludes.add(std::move(TransformedInc));
355 return ConvertedIncludes;
358IncludeCleanerFindings
361 if (
AST.getLangOpts().ObjC)
363 const auto &SM =
AST.getSourceManager();
365 const FileEntry *
MainFile = SM.getFileEntryForID(SM.getMainFileID());
368 std::vector<include_cleaner::SymbolReference> Macros =
370 std::vector<MissingIncludeDiagInfo> MissingIncludes;
371 llvm::DenseSet<IncludeStructure::HeaderID> Used;
373 OptionalDirectoryEntryRef ResourceDir =
AST.getPreprocessor()
374 .getHeaderSearchInfo()
377 include_cleaner::walkUsed(
378 AST.getLocalTopLevelDecls(), Macros,
379 &
AST.getPragmaIncludes(),
AST.getPreprocessor(),
380 [&](
const include_cleaner::SymbolReference &
Ref,
381 llvm::ArrayRef<include_cleaner::Header> Providers) {
382 bool Satisfied = false;
383 for (const auto &H : Providers) {
384 if (H.kind() == include_cleaner::Header::Physical &&
385 (H.physical() == MainFile || H.physical() == PreamblePatch ||
386 H.physical().getDir() == ResourceDir)) {
390 for (auto *Inc : ConvertedIncludes.match(H)) {
393 AST.getIncludeStructure().getID(&Inc->Resolved->getFileEntry());
394 assert(HeaderID.has_value() &&
395 "ConvertedIncludes only contains resolved includes.");
396 Used.insert(*HeaderID);
400 if (Satisfied || Providers.empty() ||
401 Ref.RT != include_cleaner::RefType::Explicit)
409 std::string Spelling = include_cleaner::spellHeader(
410 {Providers.front(), AST.getPreprocessor().getHeaderSearchInfo(),
413 ConvertedIncludes.match(include_cleaner::Header{Spelling})) {
416 AST.getIncludeStructure().getID(&Inc->Resolved->getFileEntry());
417 assert(HeaderID.has_value() &&
418 "ConvertedIncludes only contains resolved includes.");
419 Used.insert(*HeaderID);
431 auto Loc = SM.getFileLoc(
Ref.RefLocation);
434 while (SM.getFileID(
Loc) != SM.getMainFileID())
435 Loc = SM.getIncludeLoc(SM.getFileID(
Loc));
436 auto TouchingTokens =
437 syntax::spelledTokensTouching(
Loc,
AST.getTokens());
438 assert(!TouchingTokens.empty());
443 Ref.Target, TouchingTokens.back().range(SM), Providers};
444 MissingIncludes.push_back(std::move(DiagInfo));
449 llvm::stable_sort(MissingIncludes, [](
const MissingIncludeDiagInfo &LHS,
450 const MissingIncludeDiagInfo &RHS) {
452 if (LHS.SymRefRange != RHS.SymRefRange) {
455 return LHS.SymRefRange.beginOffset() < RHS.SymRefRange.beginOffset();
459 using MapInfo = llvm::DenseMapInfo<include_cleaner::Symbol>;
460 return MapInfo::getHashValue(LHS.Symbol) <
461 MapInfo::getHashValue(RHS.Symbol);
463 MissingIncludes.erase(llvm::unique(MissingIncludes), MissingIncludes.end());
464 std::vector<const Inclusion *> UnusedIncludes =
465 getUnused(
AST, Used, AnalyzeAngledIncludes);
466 return {std::move(UnusedIncludes), std::move(MissingIncludes)};
470 const include_cleaner::Includes &Includes,
471 llvm::ArrayRef<include_cleaner::Header> Providers) {
472 for (
const auto &H : Providers) {
473 auto Matches = Includes.match(H);
474 for (
const include_cleaner::Include *Match : Matches)
475 if (Match->Line ==
unsigned(Inc.
HashLine + 1))
477 if (!Matches.empty())
488 trace::Span Tracer(
"IncludeCleaner::issueIncludeCleanerDiagnostics");
489 std::vector<Diag> UnusedIncludes = generateUnusedIncludeDiagnostics(
491 std::optional<Fix> RemoveAllUnused = removeAllUnusedIncludes(UnusedIncludes);
493 std::vector<Diag> MissingIncludeDiags = generateMissingIncludeDiagnostics(
495 std::optional<Fix> AddAllMissing = addAllMissingIncludes(MissingIncludeDiags);
497 std::optional<Fix> FixAll;
498 if (RemoveAllUnused && AddAllMissing)
499 FixAll = fixAll(*RemoveAllUnused, *AddAllMissing);
504 Out->Fixes.push_back(*F);
506 for (
auto &
Diag : MissingIncludeDiags) {
507 AddBatchFix(MissingIncludeDiags.size() > 1 ? AddAllMissing : std::nullopt,
509 AddBatchFix(FixAll, &
Diag);
511 for (
auto &
Diag : UnusedIncludes) {
512 AddBatchFix(UnusedIncludes.size() > 1 ? RemoveAllUnused : std::nullopt,
514 AddBatchFix(FixAll, &
Diag);
517 auto Result = std::move(MissingIncludeDiags);
518 llvm::move(UnusedIncludes, std::back_inserter(Result));
enum clang::clangd::@1062::Bracket::Direction Dir
CompiledFragmentImpl & Out
Include Cleaner is clangd functionality for providing diagnostics for misuse of transitive headers an...
Stores and provides access to parsed AST.
Stores information required to parse a TU using a (possibly stale) Baseline preamble.
static OptionalFileEntryRef getPatchEntry(llvm::StringRef MainFilePath, const SourceManager &SM)
Returns the FileEntry for the preamble patch of MainFilePath in SM, if any.
Wrapper for vfs::FileSystem for use in multithreaded programs like clangd.
Records an event whose duration is the lifetime of the Span object.
FIXME: Skip testing on windows temporarily due to the different escaping code mode.
Position offsetToPosition(llvm::StringRef Code, size_t Offset)
Turn an offset in Code into a [line, column] pair.
std::vector< include_cleaner::SymbolReference > collectMacroReferences(ParsedAST &AST)
include_cleaner::Includes convertIncludes(const ParsedAST &AST)
Converts the clangd include representation to include-cleaner include representation.
std::optional< DefinedMacro > locateMacroAt(const syntax::Token &SpelledTok, Preprocessor &PP)
Gets the macro referenced by SpelledTok.
std::vector< Diag > issueIncludeCleanerDiagnostics(ParsedAST &AST, llvm::StringRef Code, const IncludeCleanerFindings &Findings, const ThreadsafeFS &TFS, HeaderFilter IgnoreHeaders)
bool isPreferredProvider(const Inclusion &Inc, const include_cleaner::Includes &Includes, llvm::ArrayRef< include_cleaner::Header > Providers)
Whether this #include is considered to provide a particular symbol.
IncludeCleanerFindings computeIncludeCleanerFindings(ParsedAST &AST, bool AnalyzeAngledIncludes)
@ Unnecessary
Unused or unnecessary code.
llvm::ArrayRef< std::function< bool(llvm::StringRef)> > HeaderFilter
clangd::Range rangeTillEOL(llvm::StringRef Code, unsigned HashOffset)
Returns the range starting at offset and spanning the whole line.
llvm::StringRef PathRef
A typedef to represent a ref to file path.
void elog(const char *Fmt, Ts &&... Vals)
TextEdit replacementToEdit(llvm::StringRef Code, const tooling::Replacement &R)
format::FormatStyle getFormatStyleForFile(llvm::StringRef File, llvm::StringRef Content, const ThreadsafeFS &TFS, bool FormatFile)
Choose the clang-format style we should apply to a certain file.
A top-level diagnostic that may have Notes and Fixes.
std::vector< const Inclusion * > UnusedIncludes
std::vector< MissingIncludeDiagInfo > MissingIncludes
Represents a symbol occurrence in the source file.
StringRef text() const
The token text.