xref: /llvm-project/clang-tools-extra/include-cleaner/lib/HTMLReport.cpp (revision ec6c3448d31056db5d63d7aed3e9f207edb49321)
1 //===--- HTMLReport.cpp - Explain the analysis for humans -----------------===//
2 //
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
6 //
7 //===----------------------------------------------------------------------===//
8 //
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.
11 //
12 // This library prints an annotated version of the code
13 //
14 //===----------------------------------------------------------------------===//
15 
16 #include "AnalysisInternal.h"
17 #include "clang-include-cleaner/IncludeSpeller.h"
18 #include "clang-include-cleaner/Types.h"
19 #include "clang/AST/ASTContext.h"
20 #include "clang/AST/PrettyPrinter.h"
21 #include "clang/Basic/SourceManager.h"
22 #include "clang/Lex/HeaderSearch.h"
23 #include "clang/Lex/Lexer.h"
24 #include "clang/Lex/Preprocessor.h"
25 #include "clang/Tooling/Inclusions/StandardLibrary.h"
26 #include "llvm/Support/ScopedPrinter.h"
27 #include "llvm/Support/raw_ostream.h"
28 #include <numeric>
29 
30 namespace clang::include_cleaner {
31 namespace {
32 
33 constexpr llvm::StringLiteral CSS = R"css(
34   body { margin: 0; }
35   pre { line-height: 1.5em; counter-reset: line; margin: 0; }
36   pre .line:not(.added) { counter-increment: line; }
37   pre .line::before {
38     content: counter(line);
39     display: inline-block;
40     background-color: #eee; border-right: 1px solid #ccc;
41     text-align: right;
42     width: 3em; padding-right: 0.5em; margin-right: 0.5em;
43   }
44   pre .line.added::before { content: '+' }
45   .ref, .inc { text-decoration: underline; color: #008; }
46   .sel { position: relative; cursor: pointer; }
47   .ref.implicit { background-color: #ff8; }
48   #hover {
49     color: black;
50     background-color: #aaccff; border: 1px solid #444;
51     z-index: 1;
52     position: absolute; top: 100%; left: 0;
53     font-family: sans-serif;
54     padding: 0.5em;
55   }
56   #hover p, #hover pre { margin: 0; }
57   #hover .target.implicit, .provides .implicit { background-color: #bbb; }
58   #hover .target.ambiguous, .provides .ambiguous { background-color: #caf; }
59   .missing, .unused { background-color: #faa !important; }
60   .inserted { background-color: #bea !important; }
61   .semiused { background-color: #888 !important; }
62   #hover th { color: #008; text-align: right; padding-right: 0.5em; }
63   #hover .target:not(:first-child) {
64     margin-top: 1em;
65     padding-top: 1em;
66     border-top: 1px solid #444;
67   }
68   .ref.missing #hover .insert { background-color: #bea; }
69   .ref:not(.missing) #hover .insert { font-style: italic; }
70 )css";
71 
72 constexpr llvm::StringLiteral JS = R"js(
73   // Recreate the #hover div inside whichever target .sel element was clicked.
74   function select(event) {
75     var target = event.target.closest('.sel');
76     var hover = document.getElementById('hover');
77     if (hover) {
78       if (hover.parentElement == target) return;
79       hover.parentNode.removeChild(hover);
80     }
81     if (target == null) return;
82     hover = document.createElement('div');
83     hover.id = 'hover';
84     fillHover(hover, target);
85     target.appendChild(hover);
86   }
87   // Fill the #hover div with the templates named by data-hover in the target.
88   function fillHover(hover, target) {
89     target.dataset.hover?.split(',').forEach(function(id) {
90       for (c of document.getElementById(id).content.childNodes)
91         hover.appendChild(c.cloneNode(true));
92     })
93   }
94 )js";
95 
96 // Categorize the symbol, like FunctionDecl or Macro
97 llvm::StringRef describeSymbol(const Symbol &Sym) {
98   switch (Sym.kind()) {
99   case Symbol::Declaration:
100     return Sym.declaration().getDeclKindName();
101   case Symbol::Macro:
102     return "Macro";
103   }
104   llvm_unreachable("unhandled symbol kind");
105 }
106 
107 // Return detailed symbol description (declaration), if we have any.
108 std::string printDetails(const Symbol &Sym) {
109   std::string S;
110   if (Sym.kind() == Symbol::Declaration) {
111     // Print the declaration of the symbol, e.g. to disambiguate overloads.
112     const auto &D = Sym.declaration();
113     PrintingPolicy PP = D.getASTContext().getPrintingPolicy();
114     PP.FullyQualifiedName = true;
115     PP.TerseOutput = true;
116     PP.SuppressInitializers = true;
117     llvm::raw_string_ostream SS(S);
118     D.print(SS, PP);
119   }
120   return S;
121 }
122 
123 llvm::StringRef refType(RefType T) {
124   switch (T) {
125   case RefType::Explicit:
126     return "explicit";
127   case RefType::Implicit:
128     return "implicit";
129   case RefType::Ambiguous:
130     return "ambiguous";
131   }
132   llvm_unreachable("unhandled RefType enum");
133 }
134 
135 class Reporter {
136   llvm::raw_ostream &OS;
137   const ASTContext &Ctx;
138   const SourceManager &SM;
139   const Preprocessor &PP;
140   const include_cleaner::Includes &Includes;
141   const PragmaIncludes *PI;
142   FileID MainFile;
143   const FileEntry *MainFE;
144 
145   // Points within the main file that reference a Symbol.
146   // Implicit refs will be marked with a symbol just before the token.
147   struct Ref {
148     unsigned Offset;
149     RefType Type;
150     Symbol Sym;
151     SmallVector<SymbolLocation> Locations = {};
152     SmallVector<Header> Headers = {};
153     SmallVector<const Include *> Includes = {};
154     bool Satisfied = false;  // Is the include present?
155     std::string Insert = {}; // If we had no includes, what would we insert?
156   };
157   std::vector<Ref> Refs;
158   llvm::DenseMap<const Include *, std::vector<unsigned>> IncludeRefs;
159   llvm::StringMap<std::vector</*RefIndex*/ unsigned>> Insertion;
160 
161   llvm::StringRef includeType(const Include *I) {
162     auto &List = IncludeRefs[I];
163     if (List.empty())
164       return "unused";
165     if (llvm::any_of(List, [&](unsigned I) {
166           return Refs[I].Type == RefType::Explicit;
167         }))
168       return "used";
169     return "semiused";
170   }
171 
172   void fillTarget(Ref &R) {
173     // Duplicates logic from walkUsed(), which doesn't expose SymbolLocations.
174     for (auto &Loc : locateSymbol(R.Sym, Ctx.getLangOpts()))
175       R.Locations.push_back(Loc);
176     R.Headers = headersForSymbol(R.Sym, PP, PI);
177 
178     for (const auto &H : R.Headers) {
179       R.Includes.append(Includes.match(H));
180       // FIXME: library should signal main-file refs somehow.
181       // Non-physical refs to the main-file should be possible.
182       if (H.kind() == Header::Physical && H.physical() == MainFE)
183         R.Satisfied = true;
184     }
185     if (!R.Includes.empty())
186       R.Satisfied = true;
187     // Include pointers are meaningfully ordered as they are backed by a vector.
188     llvm::sort(R.Includes);
189     R.Includes.erase(std::unique(R.Includes.begin(), R.Includes.end()),
190                      R.Includes.end());
191 
192     if (!R.Headers.empty())
193       R.Insert =
194           spellHeader({R.Headers.front(), PP.getHeaderSearchInfo(), MainFE});
195   }
196 
197 public:
198   Reporter(llvm::raw_ostream &OS, ASTContext &Ctx, const Preprocessor &PP,
199            const include_cleaner::Includes &Includes, const PragmaIncludes *PI,
200            FileID MainFile)
201       : OS(OS), Ctx(Ctx), SM(Ctx.getSourceManager()), PP(PP),
202         Includes(Includes), PI(PI), MainFile(MainFile),
203         MainFE(SM.getFileEntryForID(MainFile)) {}
204 
205   void addRef(const SymbolReference &SR) {
206     auto [File, Offset] = SM.getDecomposedLoc(SM.getFileLoc(SR.RefLocation));
207     if (File != this->MainFile) {
208       // Can get here e.g. if there's an #include inside a root Decl.
209       // FIXME: do something more useful than this.
210       llvm::errs() << "Ref location outside file! " << SR.Target << " at "
211                    << SR.RefLocation.printToString(SM) << "\n";
212       return;
213     }
214 
215     int RefIndex = Refs.size();
216     Refs.emplace_back(Ref{Offset, SR.RT, SR.Target});
217     Ref &R = Refs.back();
218     fillTarget(R);
219     for (const auto *I : R.Includes)
220       IncludeRefs[I].push_back(RefIndex);
221     if (R.Type == RefType::Explicit && !R.Satisfied && !R.Insert.empty())
222       Insertion[R.Insert].push_back(RefIndex);
223   }
224 
225   void write() {
226     OS << "<!doctype html>\n";
227     OS << "<html>\n";
228     OS << "<head>\n";
229     OS << "<style>" << CSS << "</style>\n";
230     OS << "<script>" << JS << "</script>\n";
231     for (const auto &Ins : Insertion) {
232       OS << "<template id='i";
233       escapeString(Ins.first());
234       OS << "'>";
235       writeInsertion(Ins.first(), Ins.second);
236       OS << "</template>\n";
237     }
238     for (auto &Inc : Includes.all()) {
239       OS << "<template id='i" << Inc.Line << "'>";
240       writeInclude(Inc);
241       OS << "</template>\n";
242     }
243     for (unsigned I = 0; I < Refs.size(); ++I) {
244       OS << "<template id='t" << I << "'>";
245       writeTarget(Refs[I]);
246       OS << "</template>\n";
247     }
248     OS << "</head>\n";
249     OS << "<body>\n";
250     writeCode();
251     OS << "</body>\n";
252     OS << "</html>\n";
253   }
254 
255 private:
256   void escapeChar(char C) {
257     switch (C) {
258     case '<':
259       OS << "&lt;";
260       break;
261     case '&':
262       OS << "&amp;";
263       break;
264     default:
265       OS << C;
266     }
267   }
268 
269   void escapeString(llvm::StringRef S) {
270     for (char C : S)
271       escapeChar(C);
272   }
273 
274   // Abbreviate a path ('path/to/Foo.h') to just the filename ('Foo.h').
275   // The full path is available on hover.
276   void printFilename(llvm::StringRef Path) {
277     llvm::StringRef File = llvm::sys::path::filename(Path);
278     if (File == Path)
279       return escapeString(Path);
280     OS << "<span title='";
281     escapeString(Path);
282     OS << "'>";
283     escapeString(File);
284     OS << "</span>";
285   }
286 
287   // Print a source location in compact style.
288   void printSourceLocation(SourceLocation Loc) {
289     if (Loc.isInvalid())
290       return escapeString("<invalid>");
291     if (!Loc.isMacroID())
292       return printFilename(Loc.printToString(SM));
293 
294     // Replicating printToString() is a bit simpler than parsing/reformatting.
295     printFilename(SM.getExpansionLoc(Loc).printToString(SM));
296     OS << " &lt;Spelling=";
297     printFilename(SM.getSpellingLoc(Loc).printToString(SM));
298     OS << ">";
299   }
300 
301   // Write "Provides: " rows of an include or include-insertion table.
302   // These describe the symbols the header provides, referenced by RefIndices.
303   void writeProvides(llvm::ArrayRef<unsigned> RefIndices) {
304     // We show one ref for each symbol: first by (RefType != Explicit, Sequence)
305     llvm::DenseMap<Symbol, /*RefIndex*/ unsigned> FirstRef;
306     for (unsigned RefIndex : RefIndices) {
307       const Ref &R = Refs[RefIndex];
308       auto I = FirstRef.try_emplace(R.Sym, RefIndex);
309       if (!I.second && R.Type == RefType::Explicit &&
310           Refs[I.first->second].Type != RefType::Explicit)
311         I.first->second = RefIndex;
312     }
313     std::vector<std::pair<Symbol, unsigned>> Sorted = {FirstRef.begin(),
314                                                        FirstRef.end()};
315     llvm::stable_sort(Sorted, llvm::less_second{});
316     for (auto &[S, RefIndex] : Sorted) {
317       auto &R = Refs[RefIndex];
318       OS << "<tr class='provides'><th>Provides</td><td>";
319       std::string Details = printDetails(S);
320       if (!Details.empty()) {
321         OS << "<span class='" << refType(R.Type) << "' title='";
322         escapeString(Details);
323         OS << "'>";
324       }
325       escapeString(llvm::to_string(S));
326       if (!Details.empty())
327         OS << "</span>";
328 
329       unsigned Line = SM.getLineNumber(MainFile, R.Offset);
330       OS << ", <a href='#line" << Line << "'>line " << Line << "</a>";
331       OS << "</td></tr>";
332     }
333   }
334 
335   void writeInclude(const Include &Inc) {
336     OS << "<table class='include'>";
337     if (Inc.Resolved) {
338       OS << "<tr><th>Resolved</td><td>";
339       escapeString(Inc.Resolved->getName());
340       OS << "</td></tr>\n";
341       writeProvides(IncludeRefs[&Inc]);
342     }
343     OS << "</table>";
344   }
345 
346   void writeInsertion(llvm::StringRef Text, llvm::ArrayRef<unsigned> Refs) {
347     OS << "<table class='insertion'>";
348     writeProvides(Refs);
349     OS << "</table>";
350   }
351 
352   void writeTarget(const Ref &R) {
353     OS << "<table class='target " << refType(R.Type) << "'>";
354 
355     OS << "<tr><th>Symbol</th><td>";
356     OS << describeSymbol(R.Sym) << " <code>";
357     escapeString(llvm::to_string(R.Sym));
358     OS << "</code></td></tr>\n";
359 
360     std::string Details = printDetails(R.Sym);
361     if (!Details.empty()) {
362       OS << "<tr><td></td><td><code>";
363       escapeString(Details);
364       OS << "</code></td></tr>\n";
365     }
366 
367     for (const auto &Loc : R.Locations) {
368       OS << "<tr><th>Location</th><td>";
369       if (Loc.kind() == SymbolLocation::Physical) // needs SM to print properly.
370         printSourceLocation(Loc.physical());
371       else
372         escapeString(llvm::to_string(Loc));
373       OS << "</td></tr>\n";
374     }
375 
376     for (const auto &H : R.Headers) {
377       OS << "<tr><th>Header</th><td>";
378       switch (H.kind()) {
379       case Header::Physical:
380         printFilename(H.physical().getName());
381         break;
382       case Header::Standard:
383         OS << "stdlib " << H.standard().name();
384         break;
385       case Header::Verbatim:
386         OS << "verbatim ";
387         escapeString(H.verbatim());
388         break;
389       }
390       OS << "</td></tr>\n";
391     }
392 
393     for (const auto *I : R.Includes) {
394       OS << "<tr><th>Included</th><td>";
395       escapeString(I->quote());
396       OS << ", <a href='#line" << I->Line << "'>line " << I->Line << "</a>";
397       OS << "</td></tr>";
398     }
399 
400     if (!R.Insert.empty()) {
401       OS << "<tr><th>Insert</th><td class='insert'>";
402       escapeString(R.Insert);
403       OS << "</td></tr>";
404     }
405 
406     OS << "</table>";
407   }
408 
409   void writeCode() {
410     llvm::StringRef Code = SM.getBufferData(MainFile);
411 
412     OS << "<pre onclick='select(event)' class='code'>";
413 
414     std::vector<llvm::StringRef> Insertions{Insertion.keys().begin(),
415                                             Insertion.keys().end()};
416     llvm::sort(Insertions);
417     for (llvm::StringRef Insertion : Insertions) {
418       OS << "<code class='line added'>"
419          << "<span class='inc sel inserted' data-hover='i";
420       escapeString(Insertion);
421       OS << "'>#include ";
422       escapeString(Insertion);
423       OS << "</span></code>\n";
424     }
425 
426     const Include *Inc = nullptr;
427     unsigned LineNum = 0;
428     // Lines are <code>, include lines have an inner <span>.
429     auto StartLine = [&] {
430       ++LineNum;
431       OS << "<code class='line' id='line" << LineNum << "'>";
432       if ((Inc = Includes.atLine(LineNum)))
433         OS << "<span class='inc sel " << includeType(Inc) << "' data-hover='i"
434            << Inc->Line << "'>";
435     };
436     auto EndLine = [&] {
437       if (Inc)
438         OS << "</span>";
439       OS << "</code>\n";
440     };
441 
442     std::vector<unsigned> RefOrder(Refs.size());
443     std::iota(RefOrder.begin(), RefOrder.end(), 0);
444     llvm::stable_sort(RefOrder, [&](unsigned A, unsigned B) {
445       return std::make_pair(Refs[A].Offset, Refs[A].Type != RefType::Implicit) <
446              std::make_pair(Refs[B].Offset, Refs[B].Type != RefType::Implicit);
447     });
448     auto Rest = llvm::ArrayRef(RefOrder);
449     unsigned End = 0;
450     StartLine();
451     for (unsigned I = 0; I < Code.size(); ++I) {
452       // Finish refs early at EOL to avoid dealing with splitting the span.
453       if (End && (End == I || Code[I] == '\n')) {
454         OS << "</span>";
455         End = 0;
456       }
457       // Handle implicit refs, which are rendered *before* the token.
458       while (!Rest.empty() && Refs[Rest.front()].Offset == I &&
459              Refs[Rest.front()].Type == RefType::Implicit) {
460         const Ref &R = Refs[Rest.front()];
461         OS << "<span class='ref sel implicit "
462            << (R.Satisfied ? "satisfied" : "missing") << "' data-hover='t"
463            << Rest.front() << "'>&loz;</span>";
464         Rest = Rest.drop_front();
465       };
466       // Accumulate all explicit refs that appear on the same token.
467       std::string TargetList;
468       bool Unsatisfied = false;
469       Rest = Rest.drop_while([&](unsigned RefIndex) {
470         const Ref &R = Refs[RefIndex];
471         if (R.Offset != I)
472           return false;
473         if (!TargetList.empty())
474           TargetList.push_back(',');
475         TargetList.push_back('t');
476         TargetList.append(std::to_string(RefIndex));
477         Unsatisfied = Unsatisfied || !R.Satisfied;
478         return true;
479       });
480       if (!TargetList.empty()) {
481         assert(End == 0 && "Overlapping tokens!");
482         OS << "<span class='ref sel" << (Unsatisfied ? " missing" : "")
483            << "' data-hover='" << TargetList << "'>";
484         End = I + Lexer::MeasureTokenLength(SM.getComposedLoc(MainFile, I), SM,
485                                             Ctx.getLangOpts());
486       }
487       if (Code[I] == '\n') {
488         EndLine();
489         StartLine();
490       } else
491         escapeChar(Code[I]);
492     }
493     EndLine();
494     OS << "</pre>\n";
495   }
496 };
497 
498 } // namespace
499 
500 void writeHTMLReport(FileID File, const include_cleaner::Includes &Includes,
501                      llvm::ArrayRef<Decl *> Roots,
502                      llvm::ArrayRef<SymbolReference> MacroRefs, ASTContext &Ctx,
503                      const Preprocessor &PP, PragmaIncludes *PI,
504                      llvm::raw_ostream &OS) {
505   Reporter R(OS, Ctx, PP, Includes, PI, File);
506   const auto& SM = Ctx.getSourceManager();
507   for (Decl *Root : Roots)
508     walkAST(*Root, [&](SourceLocation Loc, const NamedDecl &D, RefType T) {
509       if(!SM.isWrittenInMainFile(SM.getSpellingLoc(Loc)))
510         return;
511       R.addRef(SymbolReference{D, Loc, T});
512     });
513   for (const SymbolReference &Ref : MacroRefs) {
514     if (!SM.isWrittenInMainFile(SM.getSpellingLoc(Ref.RefLocation)))
515       continue;
516     R.addRef(Ref);
517   }
518   R.write();
519 }
520 
521 } // namespace clang::include_cleaner
522