1//===- HTMLDiagnostics.cpp - HTML Diagnostics for Paths -------------------===// 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// This file defines the HTMLDiagnostics object. 10// 11//===----------------------------------------------------------------------===// 12 13#include "clang/AST/Decl.h" 14#include "clang/AST/DeclBase.h" 15#include "clang/AST/Stmt.h" 16#include "clang/Analysis/IssueHash.h" 17#include "clang/Analysis/MacroExpansionContext.h" 18#include "clang/Analysis/PathDiagnostic.h" 19#include "clang/Basic/FileManager.h" 20#include "clang/Basic/LLVM.h" 21#include "clang/Basic/SourceLocation.h" 22#include "clang/Basic/SourceManager.h" 23#include "clang/Lex/Lexer.h" 24#include "clang/Lex/Preprocessor.h" 25#include "clang/Lex/Token.h" 26#include "clang/Rewrite/Core/HTMLRewrite.h" 27#include "clang/Rewrite/Core/Rewriter.h" 28#include "clang/StaticAnalyzer/Core/PathDiagnosticConsumers.h" 29#include "llvm/ADT/ArrayRef.h" 30#include "llvm/ADT/SmallString.h" 31#include "llvm/ADT/StringRef.h" 32#include "llvm/ADT/iterator_range.h" 33#include "llvm/Support/Casting.h" 34#include "llvm/Support/Errc.h" 35#include "llvm/Support/ErrorHandling.h" 36#include "llvm/Support/FileSystem.h" 37#include "llvm/Support/MemoryBuffer.h" 38#include "llvm/Support/Path.h" 39#include "llvm/Support/raw_ostream.h" 40#include <algorithm> 41#include <cassert> 42#include <map> 43#include <memory> 44#include <set> 45#include <sstream> 46#include <string> 47#include <system_error> 48#include <utility> 49#include <vector> 50 51using namespace clang; 52using namespace ento; 53 54//===----------------------------------------------------------------------===// 55// Boilerplate. 56//===----------------------------------------------------------------------===// 57 58namespace { 59 60class HTMLDiagnostics : public PathDiagnosticConsumer { 61 PathDiagnosticConsumerOptions DiagOpts; 62 std::string Directory; 63 bool createdDir = false; 64 bool noDir = false; 65 const Preprocessor &PP; 66 const bool SupportsCrossFileDiagnostics; 67 68public: 69 HTMLDiagnostics(PathDiagnosticConsumerOptions DiagOpts, 70 const std::string &OutputDir, const Preprocessor &pp, 71 bool supportsMultipleFiles) 72 : DiagOpts(std::move(DiagOpts)), Directory(OutputDir), PP(pp), 73 SupportsCrossFileDiagnostics(supportsMultipleFiles) {} 74 75 ~HTMLDiagnostics() override { FlushDiagnostics(nullptr); } 76 77 void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags, 78 FilesMade *filesMade) override; 79 80 StringRef getName() const override { 81 return "HTMLDiagnostics"; 82 } 83 84 bool supportsCrossFileDiagnostics() const override { 85 return SupportsCrossFileDiagnostics; 86 } 87 88 unsigned ProcessMacroPiece(raw_ostream &os, 89 const PathDiagnosticMacroPiece& P, 90 unsigned num); 91 92 void HandlePiece(Rewriter &R, FileID BugFileID, const PathDiagnosticPiece &P, 93 const std::vector<SourceRange> &PopUpRanges, unsigned num, 94 unsigned max); 95 96 void HighlightRange(Rewriter& R, FileID BugFileID, SourceRange Range, 97 const char *HighlightStart = "<span class=\"mrange\">", 98 const char *HighlightEnd = "</span>"); 99 100 void ReportDiag(const PathDiagnostic& D, 101 FilesMade *filesMade); 102 103 // Generate the full HTML report 104 std::string GenerateHTML(const PathDiagnostic& D, Rewriter &R, 105 const SourceManager& SMgr, const PathPieces& path, 106 const char *declName); 107 108 // Add HTML header/footers to file specified by FID 109 void FinalizeHTML(const PathDiagnostic& D, Rewriter &R, 110 const SourceManager& SMgr, const PathPieces& path, 111 FileID FID, const FileEntry *Entry, const char *declName); 112 113 // Rewrite the file specified by FID with HTML formatting. 114 void RewriteFile(Rewriter &R, const PathPieces& path, FileID FID); 115 116 117private: 118 /// \return Javascript for displaying shortcuts help; 119 StringRef showHelpJavascript(); 120 121 /// \return Javascript for navigating the HTML report using j/k keys. 122 StringRef generateKeyboardNavigationJavascript(); 123 124 /// \return JavaScript for an option to only show relevant lines. 125 std::string showRelevantLinesJavascript( 126 const PathDiagnostic &D, const PathPieces &path); 127 128 /// Write executed lines from \p D in JSON format into \p os. 129 void dumpCoverageData(const PathDiagnostic &D, 130 const PathPieces &path, 131 llvm::raw_string_ostream &os); 132}; 133 134} // namespace 135 136void ento::createHTMLDiagnosticConsumer( 137 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 138 const std::string &OutputDir, const Preprocessor &PP, 139 const cross_tu::CrossTranslationUnitContext &CTU, 140 const MacroExpansionContext &MacroExpansions) { 141 142 // FIXME: HTML is currently our default output type, but if the output 143 // directory isn't specified, it acts like if it was in the minimal text 144 // output mode. This doesn't make much sense, we should have the minimal text 145 // as our default. In the case of backward compatibility concerns, this could 146 // be preserved with -analyzer-config-compatibility-mode=true. 147 createTextMinimalPathDiagnosticConsumer(DiagOpts, C, OutputDir, PP, CTU, 148 MacroExpansions); 149 150 // TODO: Emit an error here. 151 if (OutputDir.empty()) 152 return; 153 154 C.push_back(new HTMLDiagnostics(std::move(DiagOpts), OutputDir, PP, true)); 155} 156 157void ento::createHTMLSingleFileDiagnosticConsumer( 158 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 159 const std::string &OutputDir, const Preprocessor &PP, 160 const cross_tu::CrossTranslationUnitContext &CTU, 161 const clang::MacroExpansionContext &MacroExpansions) { 162 createTextMinimalPathDiagnosticConsumer(DiagOpts, C, OutputDir, PP, CTU, 163 MacroExpansions); 164 165 // TODO: Emit an error here. 166 if (OutputDir.empty()) 167 return; 168 169 C.push_back(new HTMLDiagnostics(std::move(DiagOpts), OutputDir, PP, false)); 170} 171 172void ento::createPlistHTMLDiagnosticConsumer( 173 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 174 const std::string &prefix, const Preprocessor &PP, 175 const cross_tu::CrossTranslationUnitContext &CTU, 176 const MacroExpansionContext &MacroExpansions) { 177 createHTMLDiagnosticConsumer( 178 DiagOpts, C, std::string(llvm::sys::path::parent_path(prefix)), PP, CTU, 179 MacroExpansions); 180 createPlistMultiFileDiagnosticConsumer(DiagOpts, C, prefix, PP, CTU, 181 MacroExpansions); 182 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, prefix, PP, 183 CTU, MacroExpansions); 184} 185 186void ento::createSarifHTMLDiagnosticConsumer( 187 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 188 const std::string &sarif_file, const Preprocessor &PP, 189 const cross_tu::CrossTranslationUnitContext &CTU, 190 const MacroExpansionContext &MacroExpansions) { 191 createHTMLDiagnosticConsumer( 192 DiagOpts, C, std::string(llvm::sys::path::parent_path(sarif_file)), PP, 193 CTU, MacroExpansions); 194 createSarifDiagnosticConsumer(DiagOpts, C, sarif_file, PP, CTU, 195 MacroExpansions); 196 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, sarif_file, 197 PP, CTU, MacroExpansions); 198} 199 200//===----------------------------------------------------------------------===// 201// Report processing. 202//===----------------------------------------------------------------------===// 203 204void HTMLDiagnostics::FlushDiagnosticsImpl( 205 std::vector<const PathDiagnostic *> &Diags, 206 FilesMade *filesMade) { 207 for (const auto Diag : Diags) 208 ReportDiag(*Diag, filesMade); 209} 210 211void HTMLDiagnostics::ReportDiag(const PathDiagnostic& D, 212 FilesMade *filesMade) { 213 // Create the HTML directory if it is missing. 214 if (!createdDir) { 215 createdDir = true; 216 if (std::error_code ec = llvm::sys::fs::create_directories(Directory)) { 217 llvm::errs() << "warning: could not create directory '" 218 << Directory << "': " << ec.message() << '\n'; 219 noDir = true; 220 return; 221 } 222 } 223 224 if (noDir) 225 return; 226 227 // First flatten out the entire path to make it easier to use. 228 PathPieces path = D.path.flatten(/*ShouldFlattenMacros=*/false); 229 230 // The path as already been prechecked that the path is non-empty. 231 assert(!path.empty()); 232 const SourceManager &SMgr = path.front()->getLocation().getManager(); 233 234 // Create a new rewriter to generate HTML. 235 Rewriter R(const_cast<SourceManager&>(SMgr), PP.getLangOpts()); 236 237 // The file for the first path element is considered the main report file, it 238 // will usually be equivalent to SMgr.getMainFileID(); however, it might be a 239 // header when -analyzer-opt-analyze-headers is used. 240 FileID ReportFile = path.front()->getLocation().asLocation().getExpansionLoc().getFileID(); 241 242 // Get the function/method name 243 SmallString<128> declName("unknown"); 244 int offsetDecl = 0; 245 if (const Decl *DeclWithIssue = D.getDeclWithIssue()) { 246 if (const auto *ND = dyn_cast<NamedDecl>(DeclWithIssue)) 247 declName = ND->getDeclName().getAsString(); 248 249 if (const Stmt *Body = DeclWithIssue->getBody()) { 250 // Retrieve the relative position of the declaration which will be used 251 // for the file name 252 FullSourceLoc L( 253 SMgr.getExpansionLoc(path.back()->getLocation().asLocation()), 254 SMgr); 255 FullSourceLoc FunL(SMgr.getExpansionLoc(Body->getBeginLoc()), SMgr); 256 offsetDecl = L.getExpansionLineNumber() - FunL.getExpansionLineNumber(); 257 } 258 } 259 260 std::string report = GenerateHTML(D, R, SMgr, path, declName.c_str()); 261 if (report.empty()) { 262 llvm::errs() << "warning: no diagnostics generated for main file.\n"; 263 return; 264 } 265 266 // Create a path for the target HTML file. 267 int FD; 268 SmallString<128> Model, ResultPath; 269 270 if (!DiagOpts.ShouldWriteStableReportFilename) { 271 llvm::sys::path::append(Model, Directory, "report-%%%%%%.html"); 272 if (std::error_code EC = 273 llvm::sys::fs::make_absolute(Model)) { 274 llvm::errs() << "warning: could not make '" << Model 275 << "' absolute: " << EC.message() << '\n'; 276 return; 277 } 278 if (std::error_code EC = llvm::sys::fs::createUniqueFile( 279 Model, FD, ResultPath, llvm::sys::fs::OF_Text)) { 280 llvm::errs() << "warning: could not create file in '" << Directory 281 << "': " << EC.message() << '\n'; 282 return; 283 } 284 } else { 285 int i = 1; 286 std::error_code EC; 287 do { 288 // Find a filename which is not already used 289 const FileEntry* Entry = SMgr.getFileEntryForID(ReportFile); 290 std::stringstream filename; 291 Model = ""; 292 filename << "report-" 293 << llvm::sys::path::filename(Entry->getName()).str() 294 << "-" << declName.c_str() 295 << "-" << offsetDecl 296 << "-" << i << ".html"; 297 llvm::sys::path::append(Model, Directory, 298 filename.str()); 299 EC = llvm::sys::fs::openFileForReadWrite( 300 Model, FD, llvm::sys::fs::CD_CreateNew, llvm::sys::fs::OF_None); 301 if (EC && EC != llvm::errc::file_exists) { 302 llvm::errs() << "warning: could not create file '" << Model 303 << "': " << EC.message() << '\n'; 304 return; 305 } 306 i++; 307 } while (EC); 308 } 309 310 llvm::raw_fd_ostream os(FD, true); 311 312 if (filesMade) 313 filesMade->addDiagnostic(D, getName(), 314 llvm::sys::path::filename(ResultPath)); 315 316 // Emit the HTML to disk. 317 os << report; 318} 319 320std::string HTMLDiagnostics::GenerateHTML(const PathDiagnostic& D, Rewriter &R, 321 const SourceManager& SMgr, const PathPieces& path, const char *declName) { 322 // Rewrite source files as HTML for every new file the path crosses 323 std::vector<FileID> FileIDs; 324 for (auto I : path) { 325 FileID FID = I->getLocation().asLocation().getExpansionLoc().getFileID(); 326 if (llvm::is_contained(FileIDs, FID)) 327 continue; 328 329 FileIDs.push_back(FID); 330 RewriteFile(R, path, FID); 331 } 332 333 if (SupportsCrossFileDiagnostics && FileIDs.size() > 1) { 334 // Prefix file names, anchor tags, and nav cursors to every file 335 for (auto I = FileIDs.begin(), E = FileIDs.end(); I != E; I++) { 336 std::string s; 337 llvm::raw_string_ostream os(s); 338 339 if (I != FileIDs.begin()) 340 os << "<hr class=divider>\n"; 341 342 os << "<div id=File" << I->getHashValue() << ">\n"; 343 344 // Left nav arrow 345 if (I != FileIDs.begin()) 346 os << "<div class=FileNav><a href=\"#File" << (I - 1)->getHashValue() 347 << "\">←</a></div>"; 348 349 os << "<h4 class=FileName>" << SMgr.getFileEntryForID(*I)->getName() 350 << "</h4>\n"; 351 352 // Right nav arrow 353 if (I + 1 != E) 354 os << "<div class=FileNav><a href=\"#File" << (I + 1)->getHashValue() 355 << "\">→</a></div>"; 356 357 os << "</div>\n"; 358 359 R.InsertTextBefore(SMgr.getLocForStartOfFile(*I), os.str()); 360 } 361 362 // Append files to the main report file in the order they appear in the path 363 for (auto I : llvm::make_range(FileIDs.begin() + 1, FileIDs.end())) { 364 std::string s; 365 llvm::raw_string_ostream os(s); 366 367 const RewriteBuffer *Buf = R.getRewriteBufferFor(I); 368 for (auto BI : *Buf) 369 os << BI; 370 371 R.InsertTextAfter(SMgr.getLocForEndOfFile(FileIDs[0]), os.str()); 372 } 373 } 374 375 const RewriteBuffer *Buf = R.getRewriteBufferFor(FileIDs[0]); 376 if (!Buf) 377 return {}; 378 379 // Add CSS, header, and footer. 380 FileID FID = 381 path.back()->getLocation().asLocation().getExpansionLoc().getFileID(); 382 const FileEntry* Entry = SMgr.getFileEntryForID(FID); 383 FinalizeHTML(D, R, SMgr, path, FileIDs[0], Entry, declName); 384 385 std::string file; 386 llvm::raw_string_ostream os(file); 387 for (auto BI : *Buf) 388 os << BI; 389 390 return os.str(); 391} 392 393void HTMLDiagnostics::dumpCoverageData( 394 const PathDiagnostic &D, 395 const PathPieces &path, 396 llvm::raw_string_ostream &os) { 397 398 const FilesToLineNumsMap &ExecutedLines = D.getExecutedLines(); 399 400 os << "var relevant_lines = {"; 401 for (auto I = ExecutedLines.begin(), 402 E = ExecutedLines.end(); I != E; ++I) { 403 if (I != ExecutedLines.begin()) 404 os << ", "; 405 406 os << "\"" << I->first.getHashValue() << "\": {"; 407 for (unsigned LineNo : I->second) { 408 if (LineNo != *(I->second.begin())) 409 os << ", "; 410 411 os << "\"" << LineNo << "\": 1"; 412 } 413 os << "}"; 414 } 415 416 os << "};"; 417} 418 419std::string HTMLDiagnostics::showRelevantLinesJavascript( 420 const PathDiagnostic &D, const PathPieces &path) { 421 std::string s; 422 llvm::raw_string_ostream os(s); 423 os << "<script type='text/javascript'>\n"; 424 dumpCoverageData(D, path, os); 425 os << R"<<<( 426 427var filterCounterexample = function (hide) { 428 var tables = document.getElementsByClassName("code"); 429 for (var t=0; t<tables.length; t++) { 430 var table = tables[t]; 431 var file_id = table.getAttribute("data-fileid"); 432 var lines_in_fid = relevant_lines[file_id]; 433 if (!lines_in_fid) { 434 lines_in_fid = {}; 435 } 436 var lines = table.getElementsByClassName("codeline"); 437 for (var i=0; i<lines.length; i++) { 438 var el = lines[i]; 439 var lineNo = el.getAttribute("data-linenumber"); 440 if (!lines_in_fid[lineNo]) { 441 if (hide) { 442 el.setAttribute("hidden", ""); 443 } else { 444 el.removeAttribute("hidden"); 445 } 446 } 447 } 448 } 449} 450 451window.addEventListener("keydown", function (event) { 452 if (event.defaultPrevented) { 453 return; 454 } 455 if (event.key == "S") { 456 var checked = document.getElementsByName("showCounterexample")[0].checked; 457 filterCounterexample(!checked); 458 document.getElementsByName("showCounterexample")[0].checked = !checked; 459 } else { 460 return; 461 } 462 event.preventDefault(); 463}, true); 464 465document.addEventListener("DOMContentLoaded", function() { 466 document.querySelector('input[name="showCounterexample"]').onchange= 467 function (event) { 468 filterCounterexample(this.checked); 469 }; 470}); 471</script> 472 473<form> 474 <input type="checkbox" name="showCounterexample" id="showCounterexample" /> 475 <label for="showCounterexample"> 476 Show only relevant lines 477 </label> 478</form> 479)<<<"; 480 481 return os.str(); 482} 483 484void HTMLDiagnostics::FinalizeHTML(const PathDiagnostic& D, Rewriter &R, 485 const SourceManager& SMgr, const PathPieces& path, FileID FID, 486 const FileEntry *Entry, const char *declName) { 487 // This is a cludge; basically we want to append either the full 488 // working directory if we have no directory information. This is 489 // a work in progress. 490 491 llvm::SmallString<0> DirName; 492 493 if (llvm::sys::path::is_relative(Entry->getName())) { 494 llvm::sys::fs::current_path(DirName); 495 DirName += '/'; 496 } 497 498 int LineNumber = path.back()->getLocation().asLocation().getExpansionLineNumber(); 499 int ColumnNumber = path.back()->getLocation().asLocation().getExpansionColumnNumber(); 500 501 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), showHelpJavascript()); 502 503 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), 504 generateKeyboardNavigationJavascript()); 505 506 // Checkbox and javascript for filtering the output to the counterexample. 507 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), 508 showRelevantLinesJavascript(D, path)); 509 510 // Add the name of the file as an <h1> tag. 511 { 512 std::string s; 513 llvm::raw_string_ostream os(s); 514 515 os << "<!-- REPORTHEADER -->\n" 516 << "<h3>Bug Summary</h3>\n<table class=\"simpletable\">\n" 517 "<tr><td class=\"rowname\">File:</td><td>" 518 << html::EscapeText(DirName) 519 << html::EscapeText(Entry->getName()) 520 << "</td></tr>\n<tr><td class=\"rowname\">Warning:</td><td>" 521 "<a href=\"#EndPath\">line " 522 << LineNumber 523 << ", column " 524 << ColumnNumber 525 << "</a><br />" 526 << D.getVerboseDescription() << "</td></tr>\n"; 527 528 // The navigation across the extra notes pieces. 529 unsigned NumExtraPieces = 0; 530 for (const auto &Piece : path) { 531 if (const auto *P = dyn_cast<PathDiagnosticNotePiece>(Piece.get())) { 532 int LineNumber = 533 P->getLocation().asLocation().getExpansionLineNumber(); 534 int ColumnNumber = 535 P->getLocation().asLocation().getExpansionColumnNumber(); 536 os << "<tr><td class=\"rowname\">Note:</td><td>" 537 << "<a href=\"#Note" << NumExtraPieces << "\">line " 538 << LineNumber << ", column " << ColumnNumber << "</a><br />" 539 << P->getString() << "</td></tr>"; 540 ++NumExtraPieces; 541 } 542 } 543 544 // Output any other meta data. 545 546 for (PathDiagnostic::meta_iterator I = D.meta_begin(), E = D.meta_end(); 547 I != E; ++I) { 548 os << "<tr><td></td><td>" << html::EscapeText(*I) << "</td></tr>\n"; 549 } 550 551 os << R"<<<( 552</table> 553<!-- REPORTSUMMARYEXTRA --> 554<h3>Annotated Source Code</h3> 555<p>Press <a href="#" onclick="toggleHelp(); return false;">'?'</a> 556 to see keyboard shortcuts</p> 557<input type="checkbox" class="spoilerhider" id="showinvocation" /> 558<label for="showinvocation" >Show analyzer invocation</label> 559<div class="spoiler">clang -cc1 )<<<"; 560 os << html::EscapeText(DiagOpts.ToolInvocation); 561 os << R"<<<( 562</div> 563<div id='tooltiphint' hidden="true"> 564 <p>Keyboard shortcuts: </p> 565 <ul> 566 <li>Use 'j/k' keys for keyboard navigation</li> 567 <li>Use 'Shift+S' to show/hide relevant lines</li> 568 <li>Use '?' to toggle this window</li> 569 </ul> 570 <a href="#" onclick="toggleHelp(); return false;">Close</a> 571</div> 572)<<<"; 573 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str()); 574 } 575 576 // Embed meta-data tags. 577 { 578 std::string s; 579 llvm::raw_string_ostream os(s); 580 581 StringRef BugDesc = D.getVerboseDescription(); 582 if (!BugDesc.empty()) 583 os << "\n<!-- BUGDESC " << BugDesc << " -->\n"; 584 585 StringRef BugType = D.getBugType(); 586 if (!BugType.empty()) 587 os << "\n<!-- BUGTYPE " << BugType << " -->\n"; 588 589 PathDiagnosticLocation UPDLoc = D.getUniqueingLoc(); 590 FullSourceLoc L(SMgr.getExpansionLoc(UPDLoc.isValid() 591 ? UPDLoc.asLocation() 592 : D.getLocation().asLocation()), 593 SMgr); 594 const Decl *DeclWithIssue = D.getDeclWithIssue(); 595 596 StringRef BugCategory = D.getCategory(); 597 if (!BugCategory.empty()) 598 os << "\n<!-- BUGCATEGORY " << BugCategory << " -->\n"; 599 600 os << "\n<!-- BUGFILE " << DirName << Entry->getName() << " -->\n"; 601 602 os << "\n<!-- FILENAME " << llvm::sys::path::filename(Entry->getName()) << " -->\n"; 603 604 os << "\n<!-- FUNCTIONNAME " << declName << " -->\n"; 605 606 os << "\n<!-- ISSUEHASHCONTENTOFLINEINCONTEXT " 607 << getIssueHash(L, D.getCheckerName(), D.getBugType(), DeclWithIssue, 608 PP.getLangOpts()) 609 << " -->\n"; 610 611 os << "\n<!-- BUGLINE " 612 << LineNumber 613 << " -->\n"; 614 615 os << "\n<!-- BUGCOLUMN " 616 << ColumnNumber 617 << " -->\n"; 618 619 os << "\n<!-- BUGPATHLENGTH " << path.size() << " -->\n"; 620 621 // Mark the end of the tags. 622 os << "\n<!-- BUGMETAEND -->\n"; 623 624 // Insert the text. 625 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str()); 626 } 627 628 html::AddHeaderFooterInternalBuiltinCSS(R, FID, Entry->getName()); 629} 630 631StringRef HTMLDiagnostics::showHelpJavascript() { 632 return R"<<<( 633<script type='text/javascript'> 634 635var toggleHelp = function() { 636 var hint = document.querySelector("#tooltiphint"); 637 var attributeName = "hidden"; 638 if (hint.hasAttribute(attributeName)) { 639 hint.removeAttribute(attributeName); 640 } else { 641 hint.setAttribute("hidden", "true"); 642 } 643}; 644window.addEventListener("keydown", function (event) { 645 if (event.defaultPrevented) { 646 return; 647 } 648 if (event.key == "?") { 649 toggleHelp(); 650 } else { 651 return; 652 } 653 event.preventDefault(); 654}); 655</script> 656)<<<"; 657} 658 659static bool shouldDisplayPopUpRange(const SourceRange &Range) { 660 return !(Range.getBegin().isMacroID() || Range.getEnd().isMacroID()); 661} 662 663static void 664HandlePopUpPieceStartTag(Rewriter &R, 665 const std::vector<SourceRange> &PopUpRanges) { 666 for (const auto &Range : PopUpRanges) { 667 if (!shouldDisplayPopUpRange(Range)) 668 continue; 669 670 html::HighlightRange(R, Range.getBegin(), Range.getEnd(), "", 671 "<table class='variable_popup'><tbody>", 672 /*IsTokenRange=*/true); 673 } 674} 675 676static void HandlePopUpPieceEndTag(Rewriter &R, 677 const PathDiagnosticPopUpPiece &Piece, 678 std::vector<SourceRange> &PopUpRanges, 679 unsigned int LastReportedPieceIndex, 680 unsigned int PopUpPieceIndex) { 681 SmallString<256> Buf; 682 llvm::raw_svector_ostream Out(Buf); 683 684 SourceRange Range(Piece.getLocation().asRange()); 685 if (!shouldDisplayPopUpRange(Range)) 686 return; 687 688 // Write out the path indices with a right arrow and the message as a row. 689 Out << "<tr><td valign='top'><div class='PathIndex PathIndexPopUp'>" 690 << LastReportedPieceIndex; 691 692 // Also annotate the state transition with extra indices. 693 Out << '.' << PopUpPieceIndex; 694 695 Out << "</div></td><td>" << Piece.getString() << "</td></tr>"; 696 697 // If no report made at this range mark the variable and add the end tags. 698 if (std::find(PopUpRanges.begin(), PopUpRanges.end(), Range) == 699 PopUpRanges.end()) { 700 // Store that we create a report at this range. 701 PopUpRanges.push_back(Range); 702 703 Out << "</tbody></table></span>"; 704 html::HighlightRange(R, Range.getBegin(), Range.getEnd(), 705 "<span class='variable'>", Buf.c_str(), 706 /*IsTokenRange=*/true); 707 } else { 708 // Otherwise inject just the new row at the end of the range. 709 html::HighlightRange(R, Range.getBegin(), Range.getEnd(), "", Buf.c_str(), 710 /*IsTokenRange=*/true); 711 } 712} 713 714void HTMLDiagnostics::RewriteFile(Rewriter &R, 715 const PathPieces& path, FileID FID) { 716 // Process the path. 717 // Maintain the counts of extra note pieces separately. 718 unsigned TotalPieces = path.size(); 719 unsigned TotalNotePieces = std::count_if( 720 path.begin(), path.end(), [](const PathDiagnosticPieceRef &p) { 721 return isa<PathDiagnosticNotePiece>(*p); 722 }); 723 unsigned PopUpPieceCount = std::count_if( 724 path.begin(), path.end(), [](const PathDiagnosticPieceRef &p) { 725 return isa<PathDiagnosticPopUpPiece>(*p); 726 }); 727 728 unsigned TotalRegularPieces = TotalPieces - TotalNotePieces - PopUpPieceCount; 729 unsigned NumRegularPieces = TotalRegularPieces; 730 unsigned NumNotePieces = TotalNotePieces; 731 // Stores the count of the regular piece indices. 732 std::map<int, int> IndexMap; 733 734 // Stores the different ranges where we have reported something. 735 std::vector<SourceRange> PopUpRanges; 736 for (auto I = path.rbegin(), E = path.rend(); I != E; ++I) { 737 const auto &Piece = *I->get(); 738 739 if (isa<PathDiagnosticPopUpPiece>(Piece)) { 740 ++IndexMap[NumRegularPieces]; 741 } else if (isa<PathDiagnosticNotePiece>(Piece)) { 742 // This adds diagnostic bubbles, but not navigation. 743 // Navigation through note pieces would be added later, 744 // as a separate pass through the piece list. 745 HandlePiece(R, FID, Piece, PopUpRanges, NumNotePieces, TotalNotePieces); 746 --NumNotePieces; 747 } else { 748 HandlePiece(R, FID, Piece, PopUpRanges, NumRegularPieces, 749 TotalRegularPieces); 750 --NumRegularPieces; 751 } 752 } 753 754 // Secondary indexing if we are having multiple pop-ups between two notes. 755 // (e.g. [(13) 'a' is 'true']; [(13.1) 'b' is 'false']; [(13.2) 'c' is...) 756 NumRegularPieces = TotalRegularPieces; 757 for (auto I = path.rbegin(), E = path.rend(); I != E; ++I) { 758 const auto &Piece = *I->get(); 759 760 if (const auto *PopUpP = dyn_cast<PathDiagnosticPopUpPiece>(&Piece)) { 761 int PopUpPieceIndex = IndexMap[NumRegularPieces]; 762 763 // Pop-up pieces needs the index of the last reported piece and its count 764 // how many times we report to handle multiple reports on the same range. 765 // This marks the variable, adds the </table> end tag and the message 766 // (list element) as a row. The <table> start tag will be added after the 767 // rows has been written out. Note: It stores every different range. 768 HandlePopUpPieceEndTag(R, *PopUpP, PopUpRanges, NumRegularPieces, 769 PopUpPieceIndex); 770 771 if (PopUpPieceIndex > 0) 772 --IndexMap[NumRegularPieces]; 773 774 } else if (!isa<PathDiagnosticNotePiece>(Piece)) { 775 --NumRegularPieces; 776 } 777 } 778 779 // Add the <table> start tag of pop-up pieces based on the stored ranges. 780 HandlePopUpPieceStartTag(R, PopUpRanges); 781 782 // Add line numbers, header, footer, etc. 783 html::EscapeText(R, FID); 784 html::AddLineNumbers(R, FID); 785 786 // If we have a preprocessor, relex the file and syntax highlight. 787 // We might not have a preprocessor if we come from a deserialized AST file, 788 // for example. 789 html::SyntaxHighlight(R, FID, PP); 790 html::HighlightMacros(R, FID, PP); 791} 792 793void HTMLDiagnostics::HandlePiece(Rewriter &R, FileID BugFileID, 794 const PathDiagnosticPiece &P, 795 const std::vector<SourceRange> &PopUpRanges, 796 unsigned num, unsigned max) { 797 // For now, just draw a box above the line in question, and emit the 798 // warning. 799 FullSourceLoc Pos = P.getLocation().asLocation(); 800 801 if (!Pos.isValid()) 802 return; 803 804 SourceManager &SM = R.getSourceMgr(); 805 assert(&Pos.getManager() == &SM && "SourceManagers are different!"); 806 std::pair<FileID, unsigned> LPosInfo = SM.getDecomposedExpansionLoc(Pos); 807 808 if (LPosInfo.first != BugFileID) 809 return; 810 811 llvm::MemoryBufferRef Buf = SM.getBufferOrFake(LPosInfo.first); 812 const char *FileStart = Buf.getBufferStart(); 813 814 // Compute the column number. Rewind from the current position to the start 815 // of the line. 816 unsigned ColNo = SM.getColumnNumber(LPosInfo.first, LPosInfo.second); 817 const char *TokInstantiationPtr =Pos.getExpansionLoc().getCharacterData(); 818 const char *LineStart = TokInstantiationPtr-ColNo; 819 820 // Compute LineEnd. 821 const char *LineEnd = TokInstantiationPtr; 822 const char *FileEnd = Buf.getBufferEnd(); 823 while (*LineEnd != '\n' && LineEnd != FileEnd) 824 ++LineEnd; 825 826 // Compute the margin offset by counting tabs and non-tabs. 827 unsigned PosNo = 0; 828 for (const char* c = LineStart; c != TokInstantiationPtr; ++c) 829 PosNo += *c == '\t' ? 8 : 1; 830 831 // Create the html for the message. 832 833 const char *Kind = nullptr; 834 bool IsNote = false; 835 bool SuppressIndex = (max == 1); 836 switch (P.getKind()) { 837 case PathDiagnosticPiece::Event: Kind = "Event"; break; 838 case PathDiagnosticPiece::ControlFlow: Kind = "Control"; break; 839 // Setting Kind to "Control" is intentional. 840 case PathDiagnosticPiece::Macro: Kind = "Control"; break; 841 case PathDiagnosticPiece::Note: 842 Kind = "Note"; 843 IsNote = true; 844 SuppressIndex = true; 845 break; 846 case PathDiagnosticPiece::Call: 847 case PathDiagnosticPiece::PopUp: 848 llvm_unreachable("Calls and extra notes should already be handled"); 849 } 850 851 std::string sbuf; 852 llvm::raw_string_ostream os(sbuf); 853 854 os << "\n<tr><td class=\"num\"></td><td class=\"line\"><div id=\""; 855 856 if (IsNote) 857 os << "Note" << num; 858 else if (num == max) 859 os << "EndPath"; 860 else 861 os << "Path" << num; 862 863 os << "\" class=\"msg"; 864 if (Kind) 865 os << " msg" << Kind; 866 os << "\" style=\"margin-left:" << PosNo << "ex"; 867 868 // Output a maximum size. 869 if (!isa<PathDiagnosticMacroPiece>(P)) { 870 // Get the string and determining its maximum substring. 871 const auto &Msg = P.getString(); 872 unsigned max_token = 0; 873 unsigned cnt = 0; 874 unsigned len = Msg.size(); 875 876 for (char C : Msg) 877 switch (C) { 878 default: 879 ++cnt; 880 continue; 881 case ' ': 882 case '\t': 883 case '\n': 884 if (cnt > max_token) max_token = cnt; 885 cnt = 0; 886 } 887 888 if (cnt > max_token) 889 max_token = cnt; 890 891 // Determine the approximate size of the message bubble in em. 892 unsigned em; 893 const unsigned max_line = 120; 894 895 if (max_token >= max_line) 896 em = max_token / 2; 897 else { 898 unsigned characters = max_line; 899 unsigned lines = len / max_line; 900 901 if (lines > 0) { 902 for (; characters > max_token; --characters) 903 if (len / characters > lines) { 904 ++characters; 905 break; 906 } 907 } 908 909 em = characters / 2; 910 } 911 912 if (em < max_line/2) 913 os << "; max-width:" << em << "em"; 914 } 915 else 916 os << "; max-width:100em"; 917 918 os << "\">"; 919 920 if (!SuppressIndex) { 921 os << "<table class=\"msgT\"><tr><td valign=\"top\">"; 922 os << "<div class=\"PathIndex"; 923 if (Kind) os << " PathIndex" << Kind; 924 os << "\">" << num << "</div>"; 925 926 if (num > 1) { 927 os << "</td><td><div class=\"PathNav\"><a href=\"#Path" 928 << (num - 1) 929 << "\" title=\"Previous event (" 930 << (num - 1) 931 << ")\">←</a></div>"; 932 } 933 934 os << "</td><td>"; 935 } 936 937 if (const auto *MP = dyn_cast<PathDiagnosticMacroPiece>(&P)) { 938 os << "Within the expansion of the macro '"; 939 940 // Get the name of the macro by relexing it. 941 { 942 FullSourceLoc L = MP->getLocation().asLocation().getExpansionLoc(); 943 assert(L.isFileID()); 944 StringRef BufferInfo = L.getBufferData(); 945 std::pair<FileID, unsigned> LocInfo = L.getDecomposedLoc(); 946 const char* MacroName = LocInfo.second + BufferInfo.data(); 947 Lexer rawLexer(SM.getLocForStartOfFile(LocInfo.first), PP.getLangOpts(), 948 BufferInfo.begin(), MacroName, BufferInfo.end()); 949 950 Token TheTok; 951 rawLexer.LexFromRawLexer(TheTok); 952 for (unsigned i = 0, n = TheTok.getLength(); i < n; ++i) 953 os << MacroName[i]; 954 } 955 956 os << "':\n"; 957 958 if (!SuppressIndex) { 959 os << "</td>"; 960 if (num < max) { 961 os << "<td><div class=\"PathNav\"><a href=\"#"; 962 if (num == max - 1) 963 os << "EndPath"; 964 else 965 os << "Path" << (num + 1); 966 os << "\" title=\"Next event (" 967 << (num + 1) 968 << ")\">→</a></div></td>"; 969 } 970 971 os << "</tr></table>"; 972 } 973 974 // Within a macro piece. Write out each event. 975 ProcessMacroPiece(os, *MP, 0); 976 } 977 else { 978 os << html::EscapeText(P.getString()); 979 980 if (!SuppressIndex) { 981 os << "</td>"; 982 if (num < max) { 983 os << "<td><div class=\"PathNav\"><a href=\"#"; 984 if (num == max - 1) 985 os << "EndPath"; 986 else 987 os << "Path" << (num + 1); 988 os << "\" title=\"Next event (" 989 << (num + 1) 990 << ")\">→</a></div></td>"; 991 } 992 993 os << "</tr></table>"; 994 } 995 } 996 997 os << "</div></td></tr>"; 998 999 // Insert the new html. 1000 unsigned DisplayPos = LineEnd - FileStart; 1001 SourceLocation Loc = 1002 SM.getLocForStartOfFile(LPosInfo.first).getLocWithOffset(DisplayPos); 1003 1004 R.InsertTextBefore(Loc, os.str()); 1005 1006 // Now highlight the ranges. 1007 ArrayRef<SourceRange> Ranges = P.getRanges(); 1008 for (const auto &Range : Ranges) { 1009 // If we have already highlighted the range as a pop-up there is no work. 1010 if (std::find(PopUpRanges.begin(), PopUpRanges.end(), Range) != 1011 PopUpRanges.end()) 1012 continue; 1013 1014 HighlightRange(R, LPosInfo.first, Range); 1015 } 1016} 1017 1018static void EmitAlphaCounter(raw_ostream &os, unsigned n) { 1019 unsigned x = n % ('z' - 'a'); 1020 n /= 'z' - 'a'; 1021 1022 if (n > 0) 1023 EmitAlphaCounter(os, n); 1024 1025 os << char('a' + x); 1026} 1027 1028unsigned HTMLDiagnostics::ProcessMacroPiece(raw_ostream &os, 1029 const PathDiagnosticMacroPiece& P, 1030 unsigned num) { 1031 for (const auto &subPiece : P.subPieces) { 1032 if (const auto *MP = dyn_cast<PathDiagnosticMacroPiece>(subPiece.get())) { 1033 num = ProcessMacroPiece(os, *MP, num); 1034 continue; 1035 } 1036 1037 if (const auto *EP = dyn_cast<PathDiagnosticEventPiece>(subPiece.get())) { 1038 os << "<div class=\"msg msgEvent\" style=\"width:94%; " 1039 "margin-left:5px\">" 1040 "<table class=\"msgT\"><tr>" 1041 "<td valign=\"top\"><div class=\"PathIndex PathIndexEvent\">"; 1042 EmitAlphaCounter(os, num++); 1043 os << "</div></td><td valign=\"top\">" 1044 << html::EscapeText(EP->getString()) 1045 << "</td></tr></table></div>\n"; 1046 } 1047 } 1048 1049 return num; 1050} 1051 1052void HTMLDiagnostics::HighlightRange(Rewriter& R, FileID BugFileID, 1053 SourceRange Range, 1054 const char *HighlightStart, 1055 const char *HighlightEnd) { 1056 SourceManager &SM = R.getSourceMgr(); 1057 const LangOptions &LangOpts = R.getLangOpts(); 1058 1059 SourceLocation InstantiationStart = SM.getExpansionLoc(Range.getBegin()); 1060 unsigned StartLineNo = SM.getExpansionLineNumber(InstantiationStart); 1061 1062 SourceLocation InstantiationEnd = SM.getExpansionLoc(Range.getEnd()); 1063 unsigned EndLineNo = SM.getExpansionLineNumber(InstantiationEnd); 1064 1065 if (EndLineNo < StartLineNo) 1066 return; 1067 1068 if (SM.getFileID(InstantiationStart) != BugFileID || 1069 SM.getFileID(InstantiationEnd) != BugFileID) 1070 return; 1071 1072 // Compute the column number of the end. 1073 unsigned EndColNo = SM.getExpansionColumnNumber(InstantiationEnd); 1074 unsigned OldEndColNo = EndColNo; 1075 1076 if (EndColNo) { 1077 // Add in the length of the token, so that we cover multi-char tokens. 1078 EndColNo += Lexer::MeasureTokenLength(Range.getEnd(), SM, LangOpts)-1; 1079 } 1080 1081 // Highlight the range. Make the span tag the outermost tag for the 1082 // selected range. 1083 1084 SourceLocation E = 1085 InstantiationEnd.getLocWithOffset(EndColNo - OldEndColNo); 1086 1087 html::HighlightRange(R, InstantiationStart, E, HighlightStart, HighlightEnd); 1088} 1089 1090StringRef HTMLDiagnostics::generateKeyboardNavigationJavascript() { 1091 return R"<<<( 1092<script type='text/javascript'> 1093var digitMatcher = new RegExp("[0-9]+"); 1094 1095var querySelectorAllArray = function(selector) { 1096 return Array.prototype.slice.call( 1097 document.querySelectorAll(selector)); 1098} 1099 1100document.addEventListener("DOMContentLoaded", function() { 1101 querySelectorAllArray(".PathNav > a").forEach( 1102 function(currentValue, currentIndex) { 1103 var hrefValue = currentValue.getAttribute("href"); 1104 currentValue.onclick = function() { 1105 scrollTo(document.querySelector(hrefValue)); 1106 return false; 1107 }; 1108 }); 1109}); 1110 1111var findNum = function() { 1112 var s = document.querySelector(".selected"); 1113 if (!s || s.id == "EndPath") { 1114 return 0; 1115 } 1116 var out = parseInt(digitMatcher.exec(s.id)[0]); 1117 return out; 1118}; 1119 1120var scrollTo = function(el) { 1121 querySelectorAllArray(".selected").forEach(function(s) { 1122 s.classList.remove("selected"); 1123 }); 1124 el.classList.add("selected"); 1125 window.scrollBy(0, el.getBoundingClientRect().top - 1126 (window.innerHeight / 2)); 1127} 1128 1129var move = function(num, up, numItems) { 1130 if (num == 1 && up || num == numItems - 1 && !up) { 1131 return 0; 1132 } else if (num == 0 && up) { 1133 return numItems - 1; 1134 } else if (num == 0 && !up) { 1135 return 1 % numItems; 1136 } 1137 return up ? num - 1 : num + 1; 1138} 1139 1140var numToId = function(num) { 1141 if (num == 0) { 1142 return document.getElementById("EndPath") 1143 } 1144 return document.getElementById("Path" + num); 1145}; 1146 1147var navigateTo = function(up) { 1148 var numItems = document.querySelectorAll( 1149 ".line > .msgEvent, .line > .msgControl").length; 1150 var currentSelected = findNum(); 1151 var newSelected = move(currentSelected, up, numItems); 1152 var newEl = numToId(newSelected, numItems); 1153 1154 // Scroll element into center. 1155 scrollTo(newEl); 1156}; 1157 1158window.addEventListener("keydown", function (event) { 1159 if (event.defaultPrevented) { 1160 return; 1161 } 1162 if (event.key == "j") { 1163 navigateTo(/*up=*/false); 1164 } else if (event.key == "k") { 1165 navigateTo(/*up=*/true); 1166 } else { 1167 return; 1168 } 1169 event.preventDefault(); 1170}, true); 1171</script> 1172 )<<<"; 1173} 1174