1 //===--- HTMLReport.cpp - Explain the analysis for humans -----------------===//
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7 //===----------------------------------------------------------------------===//
9 // If we're debugging this tool or trying to explain its conclusions, we need to
10 // be able to identify specific facts about the code and the inferences made.
12 // This library prints an annotated version of the code
14 //===----------------------------------------------------------------------===//
16 #include "AnalysisInternal.h"
17 #include "clang-include-cleaner/Types.h"
18 #include "clang/AST/ASTContext.h"
19 #include "clang/AST/PrettyPrinter.h"
20 #include "clang/Basic/SourceManager.h"
21 #include "clang/Lex/HeaderSearch.h"
22 #include "clang/Lex/Lexer.h"
23 #include "clang/Tooling/Inclusions/StandardLibrary.h"
24 #include "llvm/Support/ScopedPrinter.h"
25 #include "llvm/Support/raw_ostream.h"
28 namespace clang::include_cleaner
{
31 constexpr llvm::StringLiteral CSS
= R
"css(
33 pre { line-height: 1.5em; counter-reset: line; margin: 0; }
34 pre .line:not(.added) { counter-increment: line; }
36 content: counter(line);
37 display: inline-block;
38 background-color: #eee; border-right: 1px solid #ccc;
40 width: 3em; padding-right: 0.5em; margin-right: 0.5em;
42 pre .line.added::before { content: '+' }
43 .ref, .inc { text-decoration: underline; color: #008; }
44 .sel { position: relative; cursor: pointer; }
45 .ref.implicit { background-color: #ff8; }
48 background-color: #aaccff; border: 1px solid #444;
50 position: absolute; top: 100%; left: 0;
51 font-family: sans-serif;
54 #hover p, #hover pre { margin: 0; }
55 #hover .target.implicit, .provides .implicit { background-color: #bbb; }
56 #hover .target.ambiguous, .provides .ambiguous { background-color: #caf; }
57 .missing, .unused { background-color: #faa !important; }
58 .inserted { background-color: #bea !important; }
59 .semiused { background-color: #888 !important; }
60 #hover th { color: #008; text-align: right; padding-right: 0.5em; }
61 #hover .target:not(:first-child) {
64 border-top: 1px solid #444;
66 .ref.missing #hover .insert { background-color: #bea; }
67 .ref:not(.missing) #hover .insert { font-style: italic; }
70 constexpr llvm::StringLiteral JS
= R
"js(
71 // Recreate the #hover div inside whichever target .sel element was clicked.
72 function select(event) {
73 var target = event.target.closest('.sel');
74 var hover = document.getElementById('hover');
76 if (hover.parentElement == target) return;
77 hover.parentNode.removeChild(hover);
79 if (target == null) return;
80 hover = document.createElement('div');
82 fillHover(hover, target);
83 target.appendChild(hover);
85 // Fill the #hover div with the templates named by data-hover in the target.
86 function fillHover(hover, target) {
87 target.dataset.hover?.split(',').forEach(function(id) {
88 for (c of document.getElementById(id).content.childNodes)
89 hover.appendChild(c.cloneNode(true));
94 // Categorize the symbol, like FunctionDecl or Macro
95 llvm::StringRef
describeSymbol(const Symbol
&Sym
) {
97 case Symbol::Declaration
:
98 return Sym
.declaration().getDeclKindName();
102 llvm_unreachable("unhandled symbol kind");
105 // Return detailed symbol description (declaration), if we have any.
106 std::string
printDetails(const Symbol
&Sym
) {
108 if (Sym
.kind() == Symbol::Declaration
) {
109 // Print the declaration of the symbol, e.g. to disambiguate overloads.
110 const auto &D
= Sym
.declaration();
111 PrintingPolicy PP
= D
.getASTContext().getPrintingPolicy();
112 PP
.FullyQualifiedName
= true;
113 PP
.TerseOutput
= true;
114 PP
.SuppressInitializers
= true;
115 llvm::raw_string_ostream
SS(S
);
121 llvm::StringRef
refType(RefType T
) {
123 case RefType::Explicit
:
125 case RefType::Implicit
:
127 case RefType::Ambiguous
:
130 llvm_unreachable("unhandled RefType enum");
134 llvm::raw_ostream
&OS
;
135 const ASTContext
&Ctx
;
136 const SourceManager
&SM
;
137 const HeaderSearch
&HS
;
138 const include_cleaner::Includes
&Includes
;
139 const PragmaIncludes
*PI
;
141 const FileEntry
*MainFE
;
143 // Points within the main file that reference a Symbol.
144 // Implicit refs will be marked with a symbol just before the token.
149 SmallVector
<SymbolLocation
> Locations
= {};
150 SmallVector
<Header
> Headers
= {};
151 SmallVector
<const Include
*> Includes
= {};
152 bool Satisfied
= false; // Is the include present?
153 std::string Insert
= {}; // If we had no includes, what would we insert?
155 std::vector
<Ref
> Refs
;
156 llvm::DenseMap
<const Include
*, std::vector
<unsigned>> IncludeRefs
;
157 llvm::StringMap
<std::vector
</*RefIndex*/ unsigned>> Insertion
;
159 llvm::StringRef
includeType(const Include
*I
) {
160 auto &List
= IncludeRefs
[I
];
163 if (llvm::any_of(List
, [&](unsigned I
) {
164 return Refs
[I
].Type
== RefType::Explicit
;
170 std::string
spellHeader(const Header
&H
) {
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
+ "\"";
178 case Header::Standard
:
179 return H
.standard().name().str();
180 case Header::Verbatim
:
181 return H
.verbatim().str();
183 llvm_unreachable("Unknown Header kind");
186 void fillTarget(Ref
&R
) {
187 // Duplicates logic from walkUsed(), which doesn't expose SymbolLocations.
188 for (auto &Loc
: locateSymbol(R
.Sym
))
189 R
.Locations
.push_back(Loc
);
190 R
.Headers
= headersForSymbol(R
.Sym
, SM
, PI
);
192 for (const auto &H
: R
.Headers
) {
193 R
.Includes
.append(Includes
.match(H
));
194 // FIXME: library should signal main-file refs somehow.
195 // Non-physical refs to the main-file should be possible.
196 if (H
.kind() == Header::Physical
&& H
.physical() == MainFE
)
199 if (!R
.Includes
.empty())
201 // Include pointers are meaningfully ordered as they are backed by a vector.
202 llvm::sort(R
.Includes
);
203 R
.Includes
.erase(std::unique(R
.Includes
.begin(), R
.Includes
.end()),
206 if (!R
.Headers
.empty())
207 R
.Insert
= spellHeader(R
.Headers
.front());
211 Reporter(llvm::raw_ostream
&OS
, ASTContext
&Ctx
, const HeaderSearch
&HS
,
212 const include_cleaner::Includes
&Includes
, const PragmaIncludes
*PI
,
214 : OS(OS
), Ctx(Ctx
), SM(Ctx
.getSourceManager()), HS(HS
),
215 Includes(Includes
), PI(PI
), MainFile(MainFile
),
216 MainFE(SM
.getFileEntryForID(MainFile
)) {}
218 void addRef(const SymbolReference
&SR
) {
219 auto [File
, Offset
] = SM
.getDecomposedLoc(SM
.getFileLoc(SR
.RefLocation
));
220 if (File
!= this->MainFile
) {
221 // Can get here e.g. if there's an #include inside a root Decl.
222 // FIXME: do something more useful than this.
223 llvm::errs() << "Ref location outside file! " << SR
.Target
<< " at "
224 << SR
.RefLocation
.printToString(SM
) << "\n";
228 int RefIndex
= Refs
.size();
229 Refs
.emplace_back(Ref
{Offset
, SR
.RT
, SR
.Target
});
230 Ref
&R
= Refs
.back();
232 for (const auto *I
: R
.Includes
)
233 IncludeRefs
[I
].push_back(RefIndex
);
234 if (R
.Type
== RefType::Explicit
&& !R
.Satisfied
&& !R
.Insert
.empty())
235 Insertion
[R
.Insert
].push_back(RefIndex
);
239 OS
<< "<!doctype html>\n";
242 OS
<< "<style>" << CSS
<< "</style>\n";
243 OS
<< "<script>" << JS
<< "</script>\n";
244 for (const auto &Ins
: Insertion
) {
245 OS
<< "<template id='i";
246 escapeString(Ins
.first());
248 writeInsertion(Ins
.first(), Ins
.second
);
249 OS
<< "</template>\n";
251 for (auto &Inc
: Includes
.all()) {
252 OS
<< "<template id='i" << Inc
.Line
<< "'>";
254 OS
<< "</template>\n";
256 for (unsigned I
= 0; I
< Refs
.size(); ++I
) {
257 OS
<< "<template id='t" << I
<< "'>";
258 writeTarget(Refs
[I
]);
259 OS
<< "</template>\n";
269 void escapeChar(char C
) {
282 void escapeString(llvm::StringRef S
) {
287 // Abbreviate a path ('path/to/Foo.h') to just the filename ('Foo.h').
288 // The full path is available on hover.
289 void printFilename(llvm::StringRef Path
) {
290 llvm::StringRef File
= llvm::sys::path::filename(Path
);
292 return escapeString(Path
);
293 OS
<< "<span title='";
300 // Print a source location in compact style.
301 void printSourceLocation(SourceLocation Loc
) {
303 return escapeString("<invalid>");
304 if (!Loc
.isMacroID())
305 return printFilename(Loc
.printToString(SM
));
307 // Replicating printToString() is a bit simpler than parsing/reformatting.
308 printFilename(SM
.getExpansionLoc(Loc
).printToString(SM
));
309 OS
<< " <Spelling=";
310 printFilename(SM
.getSpellingLoc(Loc
).printToString(SM
));
314 // Write "Provides: " rows of an include or include-insertion table.
315 // These describe the symbols the header provides, referenced by RefIndices.
316 void writeProvides(llvm::ArrayRef
<unsigned> RefIndices
) {
317 // We show one ref for each symbol: first by (RefType != Explicit, Sequence)
318 llvm::DenseMap
<Symbol
, /*RefIndex*/ unsigned> FirstRef
;
319 for (unsigned RefIndex
: RefIndices
) {
320 const Ref
&R
= Refs
[RefIndex
];
321 auto I
= FirstRef
.try_emplace(R
.Sym
, RefIndex
);
322 if (!I
.second
&& R
.Type
== RefType::Explicit
&&
323 Refs
[I
.first
->second
].Type
!= RefType::Explicit
)
324 I
.first
->second
= RefIndex
;
326 std::vector
<std::pair
<Symbol
, unsigned>> Sorted
= {FirstRef
.begin(),
328 llvm::stable_sort(Sorted
, llvm::less_second
{});
329 for (auto &[S
, RefIndex
] : Sorted
) {
330 auto &R
= Refs
[RefIndex
];
331 OS
<< "<tr class='provides'><th>Provides</td><td>";
332 std::string Details
= printDetails(S
);
333 if (!Details
.empty()) {
334 OS
<< "<span class='" << refType(R
.Type
) << "' title='";
335 escapeString(Details
);
338 escapeString(llvm::to_string(S
));
339 if (!Details
.empty())
342 unsigned Line
= SM
.getLineNumber(MainFile
, R
.Offset
);
343 OS
<< ", <a href='#line" << Line
<< "'>line " << Line
<< "</a>";
348 void writeInclude(const Include
&Inc
) {
349 OS
<< "<table class='include'>";
351 OS
<< "<tr><th>Resolved</td><td>";
352 escapeString(Inc
.Resolved
->getName());
353 OS
<< "</td></tr>\n";
354 writeProvides(IncludeRefs
[&Inc
]);
359 void writeInsertion(llvm::StringRef Text
, llvm::ArrayRef
<unsigned> Refs
) {
360 OS
<< "<table class='insertion'>";
365 void writeTarget(const Ref
&R
) {
366 OS
<< "<table class='target " << refType(R
.Type
) << "'>";
368 OS
<< "<tr><th>Symbol</th><td>";
369 OS
<< describeSymbol(R
.Sym
) << " <code>";
370 escapeString(llvm::to_string(R
.Sym
));
371 OS
<< "</code></td></tr>\n";
373 std::string Details
= printDetails(R
.Sym
);
374 if (!Details
.empty()) {
375 OS
<< "<tr><td></td><td><code>";
376 escapeString(Details
);
377 OS
<< "</code></td></tr>\n";
380 for (const auto &Loc
: R
.Locations
) {
381 OS
<< "<tr><th>Location</th><td>";
382 if (Loc
.kind() == SymbolLocation::Physical
) // needs SM to print properly.
383 printSourceLocation(Loc
.physical());
385 escapeString(llvm::to_string(Loc
));
386 OS
<< "</td></tr>\n";
389 for (const auto &H
: R
.Headers
) {
390 OS
<< "<tr><th>Header</th><td>";
392 case Header::Physical
:
393 printFilename(H
.physical().getName());
395 case Header::Standard
:
396 OS
<< "stdlib " << H
.standard().name();
398 case Header::Verbatim
:
400 escapeString(H
.verbatim());
403 OS
<< "</td></tr>\n";
406 for (const auto *I
: R
.Includes
) {
407 OS
<< "<tr><th>Included</th><td>";
408 escapeString(I
->quote());
409 OS
<< ", <a href='#line" << I
->Line
<< "'>line " << I
->Line
<< "</a>";
413 if (!R
.Insert
.empty()) {
414 OS
<< "<tr><th>Insert</th><td class='insert'>";
415 escapeString(R
.Insert
);
423 llvm::StringRef Code
= SM
.getBufferData(MainFile
);
425 OS
<< "<pre onclick='select(event)' class='code'>";
427 std::vector
<llvm::StringRef
> Insertions
{Insertion
.keys().begin(),
428 Insertion
.keys().end()};
429 llvm::sort(Insertions
);
430 for (llvm::StringRef Insertion
: Insertions
) {
431 OS
<< "<code class='line added'>"
432 << "<span class='inc sel inserted' data-hover='i";
433 escapeString(Insertion
);
435 escapeString(Insertion
);
436 OS
<< "</span></code>\n";
439 const Include
*Inc
= nullptr;
440 unsigned LineNum
= 0;
441 // Lines are <code>, include lines have an inner <span>.
442 auto StartLine
= [&] {
444 OS
<< "<code class='line' id='line" << LineNum
<< "'>";
445 if ((Inc
= Includes
.atLine(LineNum
)))
446 OS
<< "<span class='inc sel " << includeType(Inc
) << "' data-hover='i"
447 << Inc
->Line
<< "'>";
455 std::vector
<unsigned> RefOrder(Refs
.size());
456 std::iota(RefOrder
.begin(), RefOrder
.end(), 0);
457 llvm::stable_sort(RefOrder
, [&](unsigned A
, unsigned B
) {
458 return std::make_pair(Refs
[A
].Offset
, Refs
[A
].Type
!= RefType::Implicit
) <
459 std::make_pair(Refs
[B
].Offset
, Refs
[B
].Type
!= RefType::Implicit
);
461 auto Rest
= llvm::ArrayRef(RefOrder
);
464 for (unsigned I
= 0; I
< Code
.size(); ++I
) {
465 // Finish refs early at EOL to avoid dealing with splitting the span.
466 if (End
&& (End
== I
|| Code
[I
] == '\n')) {
470 // Handle implicit refs, which are rendered *before* the token.
471 while (!Rest
.empty() && Refs
[Rest
.front()].Offset
== I
&&
472 Refs
[Rest
.front()].Type
== RefType::Implicit
) {
473 const Ref
&R
= Refs
[Rest
.front()];
474 OS
<< "<span class='ref sel implicit "
475 << (R
.Satisfied
? "satisfied" : "missing") << "' data-hover='t"
476 << Rest
.front() << "'>◊</span>";
477 Rest
= Rest
.drop_front();
479 // Accumulate all explicit refs that appear on the same token.
480 std::string TargetList
;
481 bool Unsatisfied
= false;
482 Rest
= Rest
.drop_while([&](unsigned RefIndex
) {
483 const Ref
&R
= Refs
[RefIndex
];
486 if (!TargetList
.empty())
487 TargetList
.push_back(',');
488 TargetList
.push_back('t');
489 TargetList
.append(std::to_string(RefIndex
));
490 Unsatisfied
= Unsatisfied
|| !R
.Satisfied
;
493 if (!TargetList
.empty()) {
494 assert(End
== 0 && "Overlapping tokens!");
495 OS
<< "<span class='ref sel" << (Unsatisfied
? " missing" : "")
496 << "' data-hover='" << TargetList
<< "'>";
497 End
= I
+ Lexer::MeasureTokenLength(SM
.getComposedLoc(MainFile
, I
), SM
,
500 if (Code
[I
] == '\n') {
513 void writeHTMLReport(FileID File
, const include_cleaner::Includes
&Includes
,
514 llvm::ArrayRef
<Decl
*> Roots
,
515 llvm::ArrayRef
<SymbolReference
> MacroRefs
, ASTContext
&Ctx
,
516 const HeaderSearch
&HS
, PragmaIncludes
*PI
,
517 llvm::raw_ostream
&OS
) {
518 Reporter
R(OS
, Ctx
, HS
, Includes
, PI
, File
);
519 const auto& SM
= Ctx
.getSourceManager();
520 for (Decl
*Root
: Roots
)
521 walkAST(*Root
, [&](SourceLocation Loc
, const NamedDecl
&D
, RefType T
) {
522 if(!SM
.isWrittenInMainFile(SM
.getSpellingLoc(Loc
)))
524 R
.addRef(SymbolReference
{D
, Loc
, T
});
526 for (const SymbolReference
&Ref
: MacroRefs
) {
527 if (!SM
.isWrittenInMainFile(SM
.getSpellingLoc(Ref
.RefLocation
)))
534 } // namespace clang::include_cleaner