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