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 << "<"; 260 break; 261 case '&': 262 OS << "&"; 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 << " <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() << "'>◊</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