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