521 lines · cpp
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-exception6//7//===----------------------------------------------------------------------===//8//9// If we're debugging this tool or trying to explain its conclusions, we need to10// be able to identify specific facts about the code and the inferences made.11//12// This library prints an annotated version of the code13//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 30namespace clang::include_cleaner {31namespace {32 33constexpr 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 72constexpr 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 Macro97llvm::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.108std::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 123llvm::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 135class 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(llvm::unique(R.Includes), R.Includes.end());190 191 if (!R.Headers.empty())192 R.Insert =193 spellHeader({R.Headers.front(), PP.getHeaderSearchInfo(), MainFE});194 }195 196public:197 Reporter(llvm::raw_ostream &OS, ASTContext &Ctx, const Preprocessor &PP,198 const include_cleaner::Includes &Includes, const PragmaIncludes *PI,199 FileID MainFile)200 : OS(OS), Ctx(Ctx), SM(Ctx.getSourceManager()), PP(PP),201 Includes(Includes), PI(PI), MainFile(MainFile),202 MainFE(SM.getFileEntryForID(MainFile)) {}203 204 void addRef(const SymbolReference &SR) {205 auto [File, Offset] = SM.getDecomposedLoc(SM.getFileLoc(SR.RefLocation));206 if (File != this->MainFile) {207 // Can get here e.g. if there's an #include inside a root Decl.208 // FIXME: do something more useful than this.209 llvm::errs() << "Ref location outside file! " << SR.Target << " at "210 << SR.RefLocation.printToString(SM) << "\n";211 return;212 }213 214 int RefIndex = Refs.size();215 Refs.emplace_back(Ref{Offset, SR.RT, SR.Target});216 Ref &R = Refs.back();217 fillTarget(R);218 for (const auto *I : R.Includes)219 IncludeRefs[I].push_back(RefIndex);220 if (R.Type == RefType::Explicit && !R.Satisfied && !R.Insert.empty())221 Insertion[R.Insert].push_back(RefIndex);222 }223 224 void write() {225 OS << "<!doctype html>\n";226 OS << "<html>\n";227 OS << "<head>\n";228 OS << "<style>" << CSS << "</style>\n";229 OS << "<script>" << JS << "</script>\n";230 for (const auto &Ins : Insertion) {231 OS << "<template id='i";232 escapeString(Ins.first());233 OS << "'>";234 writeInsertion(Ins.first(), Ins.second);235 OS << "</template>\n";236 }237 for (auto &Inc : Includes.all()) {238 OS << "<template id='i" << Inc.Line << "'>";239 writeInclude(Inc);240 OS << "</template>\n";241 }242 for (unsigned I = 0; I < Refs.size(); ++I) {243 OS << "<template id='t" << I << "'>";244 writeTarget(Refs[I]);245 OS << "</template>\n";246 }247 OS << "</head>\n";248 OS << "<body>\n";249 writeCode();250 OS << "</body>\n";251 OS << "</html>\n";252 }253 254private:255 void escapeChar(char C) {256 switch (C) {257 case '<':258 OS << "<";259 break;260 case '&':261 OS << "&";262 break;263 default:264 OS << C;265 }266 }267 268 void escapeString(llvm::StringRef S) {269 for (char C : S)270 escapeChar(C);271 }272 273 // Abbreviate a path ('path/to/Foo.h') to just the filename ('Foo.h').274 // The full path is available on hover.275 void printFilename(llvm::StringRef Path) {276 llvm::StringRef File = llvm::sys::path::filename(Path);277 if (File == Path)278 return escapeString(Path);279 OS << "<span title='";280 escapeString(Path);281 OS << "'>";282 escapeString(File);283 OS << "</span>";284 }285 286 // Print a source location in compact style.287 void printSourceLocation(SourceLocation Loc) {288 if (Loc.isInvalid())289 return escapeString("<invalid>");290 if (!Loc.isMacroID())291 return printFilename(Loc.printToString(SM));292 293 // Replicating printToString() is a bit simpler than parsing/reformatting.294 printFilename(SM.getExpansionLoc(Loc).printToString(SM));295 OS << " <Spelling=";296 printFilename(SM.getSpellingLoc(Loc).printToString(SM));297 OS << ">";298 }299 300 // Write "Provides: " rows of an include or include-insertion table.301 // These describe the symbols the header provides, referenced by RefIndices.302 void writeProvides(llvm::ArrayRef<unsigned> RefIndices) {303 // We show one ref for each symbol: first by (RefType != Explicit, Sequence)304 llvm::DenseMap<Symbol, /*RefIndex*/ unsigned> FirstRef;305 for (unsigned RefIndex : RefIndices) {306 const Ref &R = Refs[RefIndex];307 auto I = FirstRef.try_emplace(R.Sym, RefIndex);308 if (!I.second && R.Type == RefType::Explicit &&309 Refs[I.first->second].Type != RefType::Explicit)310 I.first->second = RefIndex;311 }312 std::vector<std::pair<Symbol, unsigned>> Sorted = {FirstRef.begin(),313 FirstRef.end()};314 llvm::stable_sort(Sorted, llvm::less_second{});315 for (auto &[S, RefIndex] : Sorted) {316 auto &R = Refs[RefIndex];317 OS << "<tr class='provides'><th>Provides</td><td>";318 std::string Details = printDetails(S);319 if (!Details.empty()) {320 OS << "<span class='" << refType(R.Type) << "' title='";321 escapeString(Details);322 OS << "'>";323 }324 escapeString(llvm::to_string(S));325 if (!Details.empty())326 OS << "</span>";327 328 unsigned Line = SM.getLineNumber(MainFile, R.Offset);329 OS << ", <a href='#line" << Line << "'>line " << Line << "</a>";330 OS << "</td></tr>";331 }332 }333 334 void writeInclude(const Include &Inc) {335 OS << "<table class='include'>";336 if (Inc.Resolved) {337 OS << "<tr><th>Resolved</td><td>";338 escapeString(Inc.Resolved->getName());339 OS << "</td></tr>\n";340 writeProvides(IncludeRefs[&Inc]);341 }342 OS << "</table>";343 }344 345 void writeInsertion(llvm::StringRef Text, llvm::ArrayRef<unsigned> Refs) {346 OS << "<table class='insertion'>";347 writeProvides(Refs);348 OS << "</table>";349 }350 351 void writeTarget(const Ref &R) {352 OS << "<table class='target " << refType(R.Type) << "'>";353 354 OS << "<tr><th>Symbol</th><td>";355 OS << describeSymbol(R.Sym) << " <code>";356 escapeString(llvm::to_string(R.Sym));357 OS << "</code></td></tr>\n";358 359 std::string Details = printDetails(R.Sym);360 if (!Details.empty()) {361 OS << "<tr><td></td><td><code>";362 escapeString(Details);363 OS << "</code></td></tr>\n";364 }365 366 for (const auto &Loc : R.Locations) {367 OS << "<tr><th>Location</th><td>";368 if (Loc.kind() == SymbolLocation::Physical) // needs SM to print properly.369 printSourceLocation(Loc.physical());370 else371 escapeString(llvm::to_string(Loc));372 OS << "</td></tr>\n";373 }374 375 for (const auto &H : R.Headers) {376 OS << "<tr><th>Header</th><td>";377 switch (H.kind()) {378 case Header::Physical:379 printFilename(H.physical().getName());380 break;381 case Header::Standard:382 OS << "stdlib " << H.standard().name();383 break;384 case Header::Verbatim:385 OS << "verbatim ";386 escapeString(H.verbatim());387 break;388 }389 OS << "</td></tr>\n";390 }391 392 for (const auto *I : R.Includes) {393 OS << "<tr><th>Included</th><td>";394 escapeString(I->quote());395 OS << ", <a href='#line" << I->Line << "'>line " << I->Line << "</a>";396 OS << "</td></tr>";397 }398 399 if (!R.Insert.empty()) {400 OS << "<tr><th>Insert</th><td class='insert'>";401 escapeString(R.Insert);402 OS << "</td></tr>";403 }404 405 OS << "</table>";406 }407 408 void writeCode() {409 llvm::StringRef Code = SM.getBufferData(MainFile);410 411 OS << "<pre onclick='select(event)' class='code'>";412 413 std::vector<llvm::StringRef> Insertions{Insertion.keys().begin(),414 Insertion.keys().end()};415 llvm::sort(Insertions);416 for (llvm::StringRef Insertion : Insertions) {417 OS << "<code class='line added'>"418 << "<span class='inc sel inserted' data-hover='i";419 escapeString(Insertion);420 OS << "'>#include ";421 escapeString(Insertion);422 OS << "</span></code>\n";423 }424 425 const Include *Inc = nullptr;426 unsigned LineNum = 0;427 // Lines are <code>, include lines have an inner <span>.428 auto StartLine = [&] {429 ++LineNum;430 OS << "<code class='line' id='line" << LineNum << "'>";431 if ((Inc = Includes.atLine(LineNum)))432 OS << "<span class='inc sel " << includeType(Inc) << "' data-hover='i"433 << Inc->Line << "'>";434 };435 auto EndLine = [&] {436 if (Inc)437 OS << "</span>";438 OS << "</code>\n";439 };440 441 std::vector<unsigned> RefOrder(Refs.size());442 std::iota(RefOrder.begin(), RefOrder.end(), 0);443 llvm::stable_sort(RefOrder, [&](unsigned A, unsigned B) {444 return std::make_pair(Refs[A].Offset, Refs[A].Type != RefType::Implicit) <445 std::make_pair(Refs[B].Offset, Refs[B].Type != RefType::Implicit);446 });447 auto Rest = llvm::ArrayRef(RefOrder);448 unsigned End = 0;449 StartLine();450 for (unsigned I = 0; I < Code.size(); ++I) {451 // Finish refs early at EOL to avoid dealing with splitting the span.452 if (End && (End == I || Code[I] == '\n')) {453 OS << "</span>";454 End = 0;455 }456 // Handle implicit refs, which are rendered *before* the token.457 while (!Rest.empty() && Refs[Rest.front()].Offset == I &&458 Refs[Rest.front()].Type == RefType::Implicit) {459 const Ref &R = Refs[Rest.front()];460 OS << "<span class='ref sel implicit "461 << (R.Satisfied ? "satisfied" : "missing") << "' data-hover='t"462 << Rest.front() << "'>◊</span>";463 Rest = Rest.drop_front();464 };465 // Accumulate all explicit refs that appear on the same token.466 std::string TargetList;467 bool Unsatisfied = false;468 Rest = Rest.drop_while([&](unsigned RefIndex) {469 const Ref &R = Refs[RefIndex];470 if (R.Offset != I)471 return false;472 if (!TargetList.empty())473 TargetList.push_back(',');474 TargetList.push_back('t');475 TargetList.append(std::to_string(RefIndex));476 Unsatisfied = Unsatisfied || !R.Satisfied;477 return true;478 });479 if (!TargetList.empty()) {480 assert(End == 0 && "Overlapping tokens!");481 OS << "<span class='ref sel" << (Unsatisfied ? " missing" : "")482 << "' data-hover='" << TargetList << "'>";483 End = I + Lexer::MeasureTokenLength(SM.getComposedLoc(MainFile, I), SM,484 Ctx.getLangOpts());485 }486 if (Code[I] == '\n') {487 EndLine();488 StartLine();489 } else490 escapeChar(Code[I]);491 }492 EndLine();493 OS << "</pre>\n";494 }495};496 497} // namespace498 499void writeHTMLReport(FileID File, const include_cleaner::Includes &Includes,500 llvm::ArrayRef<Decl *> Roots,501 llvm::ArrayRef<SymbolReference> MacroRefs, ASTContext &Ctx,502 const Preprocessor &PP, PragmaIncludes *PI,503 llvm::raw_ostream &OS) {504 Reporter R(OS, Ctx, PP, Includes, PI, File);505 const auto& SM = Ctx.getSourceManager();506 for (Decl *Root : Roots)507 walkAST(*Root, [&](SourceLocation Loc, const NamedDecl &D, RefType T) {508 if(!SM.isWrittenInMainFile(SM.getSpellingLoc(Loc)))509 return;510 R.addRef(SymbolReference{D, Loc, T});511 });512 for (const SymbolReference &Ref : MacroRefs) {513 if (!SM.isWrittenInMainFile(SM.getSpellingLoc(Ref.RefLocation)))514 continue;515 R.addRef(Ref);516 }517 R.write();518}519 520} // namespace clang::include_cleaner521