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::openFileForWrite(Model, 242 FD, 243 llvm::sys::fs::F_RW | 244 llvm::sys::fs::F_Excl); 245 if (EC && EC != llvm::errc::file_exists) { 246 llvm::errs() << "warning: could not create file '" << Model 247 << "': " << EC.message() << '\n'; 248 return; 249 } 250 i++; 251 } while (EC); 252 } 253 254 llvm::raw_fd_ostream os(FD, true); 255 256 if (filesMade) 257 filesMade->addDiagnostic(D, getName(), 258 llvm::sys::path::filename(ResultPath)); 259 260 // Emit the HTML to disk. 261 os << report; 262 } 263 264 std::string HTMLDiagnostics::GenerateHTML(const PathDiagnostic& D, Rewriter &R, 265 const SourceManager& SMgr, const PathPieces& path, const char *declName) { 266 // Rewrite source files as HTML for every new file the path crosses 267 std::vector<FileID> FileIDs; 268 for (auto I : path) { 269 FileID FID = I->getLocation().asLocation().getExpansionLoc().getFileID(); 270 if (std::find(FileIDs.begin(), FileIDs.end(), FID) != FileIDs.end()) 271 continue; 272 273 FileIDs.push_back(FID); 274 RewriteFile(R, SMgr, path, FID); 275 } 276 277 if (SupportsCrossFileDiagnostics && FileIDs.size() > 1) { 278 // Prefix file names, anchor tags, and nav cursors to every file 279 for (auto I = FileIDs.begin(), E = FileIDs.end(); I != E; I++) { 280 std::string s; 281 llvm::raw_string_ostream os(s); 282 283 if (I != FileIDs.begin()) 284 os << "<hr class=divider>\n"; 285 286 os << "<div id=File" << I->getHashValue() << ">\n"; 287 288 // Left nav arrow 289 if (I != FileIDs.begin()) 290 os << "<div class=FileNav><a href=\"#File" << (I - 1)->getHashValue() 291 << "\">←</a></div>"; 292 293 os << "<h4 class=FileName>" << SMgr.getFileEntryForID(*I)->getName() 294 << "</h4>\n"; 295 296 // Right nav arrow 297 if (I + 1 != E) 298 os << "<div class=FileNav><a href=\"#File" << (I + 1)->getHashValue() 299 << "\">→</a></div>"; 300 301 os << "</div>\n"; 302 303 R.InsertTextBefore(SMgr.getLocForStartOfFile(*I), os.str()); 304 } 305 306 // Append files to the main report file in the order they appear in the path 307 for (auto I : llvm::make_range(FileIDs.begin() + 1, FileIDs.end())) { 308 std::string s; 309 llvm::raw_string_ostream os(s); 310 311 const RewriteBuffer *Buf = R.getRewriteBufferFor(I); 312 for (auto BI : *Buf) 313 os << BI; 314 315 R.InsertTextAfter(SMgr.getLocForEndOfFile(FileIDs[0]), os.str()); 316 } 317 } 318 319 const RewriteBuffer *Buf = R.getRewriteBufferFor(FileIDs[0]); 320 if (!Buf) 321 return {}; 322 323 // Add CSS, header, and footer. 324 FileID FID = 325 path.back()->getLocation().asLocation().getExpansionLoc().getFileID(); 326 const FileEntry* Entry = SMgr.getFileEntryForID(FID); 327 FinalizeHTML(D, R, SMgr, path, FileIDs[0], Entry, declName); 328 329 std::string file; 330 llvm::raw_string_ostream os(file); 331 for (auto BI : *Buf) 332 os << BI; 333 334 return os.str(); 335 } 336 337 /// Write executed lines from \p D in JSON format into \p os. 338 static void serializeExecutedLines( 339 const PathDiagnostic &D, 340 const PathPieces &path, 341 llvm::raw_string_ostream &os) { 342 // Copy executed lines from path diagnostics. 343 std::map<unsigned, std::set<unsigned>> ExecutedLines; 344 for (auto I = D.executedLines_begin(), 345 E = D.executedLines_end(); I != E; ++I) { 346 std::set<unsigned> &LinesInFile = ExecutedLines[I->first]; 347 for (unsigned LineNo : I->second) { 348 LinesInFile.insert(LineNo); 349 } 350 } 351 352 // We need to include all lines for which any kind of diagnostics appears. 353 for (const auto &P : path) { 354 FullSourceLoc Loc = P->getLocation().asLocation().getExpansionLoc(); 355 FileID FID = Loc.getFileID(); 356 unsigned LineNo = Loc.getLineNumber(); 357 ExecutedLines[FID.getHashValue()].insert(LineNo); 358 } 359 360 os << "var relevant_lines = {"; 361 for (auto I = ExecutedLines.begin(), 362 E = ExecutedLines.end(); I != E; ++I) { 363 if (I != ExecutedLines.begin()) 364 os << ", "; 365 366 os << "\"" << I->first << "\": {"; 367 for (unsigned LineNo : I->second) { 368 if (LineNo != *(I->second.begin())) 369 os << ", "; 370 371 os << "\"" << LineNo << "\": 1"; 372 } 373 os << "}"; 374 } 375 376 os << "};"; 377 } 378 379 /// \return JavaScript for an option to only show relevant lines. 380 static std::string showRelevantLinesJavascript( 381 const PathDiagnostic &D, const PathPieces &path) { 382 std::string s; 383 llvm::raw_string_ostream os(s); 384 os << "<script type='text/javascript'>\n"; 385 serializeExecutedLines(D, path, os); 386 os << R"<<<( 387 388 var filterCounterexample = function (hide) { 389 var tables = document.getElementsByClassName("code"); 390 for (var t=0; t<tables.length; t++) { 391 var table = tables[t]; 392 var file_id = table.getAttribute("data-fileid"); 393 var lines_in_fid = relevant_lines[file_id]; 394 if (!lines_in_fid) { 395 lines_in_fid = {}; 396 } 397 var lines = table.getElementsByClassName("codeline"); 398 for (var i=0; i<lines.length; i++) { 399 var el = lines[i]; 400 var lineNo = el.getAttribute("data-linenumber"); 401 if (!lines_in_fid[lineNo]) { 402 if (hide) { 403 el.setAttribute("hidden", ""); 404 } else { 405 el.removeAttribute("hidden"); 406 } 407 } 408 } 409 } 410 } 411 412 window.addEventListener("keydown", function (event) { 413 if (event.defaultPrevented) { 414 return; 415 } 416 if (event.key == "S") { 417 var checked = document.getElementsByName("showCounterexample")[0].checked; 418 filterCounterexample(!checked); 419 document.getElementsByName("showCounterexample")[0].checked = !checked; 420 } else { 421 return; 422 } 423 event.preventDefault(); 424 }, true); 425 426 document.addEventListener("DOMContentLoaded", function() { 427 document.querySelector('input[name="showCounterexample"]').onchange= 428 function (event) { 429 filterCounterexample(this.checked); 430 }; 431 }); 432 </script> 433 434 <form> 435 <input type="checkbox" name="showCounterexample" id="showCounterexample" /> 436 <label for="showCounterexample"> 437 Show only relevant lines 438 </label> 439 </form> 440 )<<<"; 441 442 return os.str(); 443 } 444 445 void HTMLDiagnostics::FinalizeHTML(const PathDiagnostic& D, Rewriter &R, 446 const SourceManager& SMgr, const PathPieces& path, FileID FID, 447 const FileEntry *Entry, const char *declName) { 448 // This is a cludge; basically we want to append either the full 449 // working directory if we have no directory information. This is 450 // a work in progress. 451 452 llvm::SmallString<0> DirName; 453 454 if (llvm::sys::path::is_relative(Entry->getName())) { 455 llvm::sys::fs::current_path(DirName); 456 DirName += '/'; 457 } 458 459 int LineNumber = path.back()->getLocation().asLocation().getExpansionLineNumber(); 460 int ColumnNumber = path.back()->getLocation().asLocation().getExpansionColumnNumber(); 461 462 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), showHelpJavascript()); 463 464 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), 465 generateKeyboardNavigationJavascript()); 466 467 // Checkbox and javascript for filtering the output to the counterexample. 468 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), 469 showRelevantLinesJavascript(D, path)); 470 471 // Add the name of the file as an <h1> tag. 472 { 473 std::string s; 474 llvm::raw_string_ostream os(s); 475 476 os << "<!-- REPORTHEADER -->\n" 477 << "<h3>Bug Summary</h3>\n<table class=\"simpletable\">\n" 478 "<tr><td class=\"rowname\">File:</td><td>" 479 << html::EscapeText(DirName) 480 << html::EscapeText(Entry->getName()) 481 << "</td></tr>\n<tr><td class=\"rowname\">Warning:</td><td>" 482 "<a href=\"#EndPath\">line " 483 << LineNumber 484 << ", column " 485 << ColumnNumber 486 << "</a><br />" 487 << D.getVerboseDescription() << "</td></tr>\n"; 488 489 // The navigation across the extra notes pieces. 490 unsigned NumExtraPieces = 0; 491 for (const auto &Piece : path) { 492 if (const auto *P = dyn_cast<PathDiagnosticNotePiece>(Piece.get())) { 493 int LineNumber = 494 P->getLocation().asLocation().getExpansionLineNumber(); 495 int ColumnNumber = 496 P->getLocation().asLocation().getExpansionColumnNumber(); 497 os << "<tr><td class=\"rowname\">Note:</td><td>" 498 << "<a href=\"#Note" << NumExtraPieces << "\">line " 499 << LineNumber << ", column " << ColumnNumber << "</a><br />" 500 << P->getString() << "</td></tr>"; 501 ++NumExtraPieces; 502 } 503 } 504 505 // Output any other meta data. 506 507 for (PathDiagnostic::meta_iterator I = D.meta_begin(), E = D.meta_end(); 508 I != E; ++I) { 509 os << "<tr><td></td><td>" << html::EscapeText(*I) << "</td></tr>\n"; 510 } 511 512 os << R"<<<( 513 </table> 514 <!-- REPORTSUMMARYEXTRA --> 515 <h3>Annotated Source Code</h3> 516 <p>Press <a href="#" onclick="toggleHelp(); return false;">'?'</a> 517 to see keyboard shortcuts</p> 518 <input type="checkbox" class="spoilerhider" id="showinvocation" /> 519 <label for="showinvocation" >Show analyzer invocation</label> 520 <div class="spoiler">clang -cc1 )<<<"; 521 os << html::EscapeText(AnalyzerOpts.FullCompilerInvocation); 522 os << R"<<<( 523 </div> 524 <div id='tooltiphint' hidden="true"> 525 <p>Keyboard shortcuts: </p> 526 <ul> 527 <li>Use 'j/k' keys for keyboard navigation</li> 528 <li>Use 'Shift+S' to show/hide relevant lines</li> 529 <li>Use '?' to toggle this window</li> 530 </ul> 531 <a href="#" onclick="toggleHelp(); return false;">Close</a> 532 </div> 533 )<<<"; 534 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str()); 535 } 536 537 // Embed meta-data tags. 538 { 539 std::string s; 540 llvm::raw_string_ostream os(s); 541 542 StringRef BugDesc = D.getVerboseDescription(); 543 if (!BugDesc.empty()) 544 os << "\n<!-- BUGDESC " << BugDesc << " -->\n"; 545 546 StringRef BugType = D.getBugType(); 547 if (!BugType.empty()) 548 os << "\n<!-- BUGTYPE " << BugType << " -->\n"; 549 550 PathDiagnosticLocation UPDLoc = D.getUniqueingLoc(); 551 FullSourceLoc L(SMgr.getExpansionLoc(UPDLoc.isValid() 552 ? UPDLoc.asLocation() 553 : D.getLocation().asLocation()), 554 SMgr); 555 const Decl *DeclWithIssue = D.getDeclWithIssue(); 556 557 StringRef BugCategory = D.getCategory(); 558 if (!BugCategory.empty()) 559 os << "\n<!-- BUGCATEGORY " << BugCategory << " -->\n"; 560 561 os << "\n<!-- BUGFILE " << DirName << Entry->getName() << " -->\n"; 562 563 os << "\n<!-- FILENAME " << llvm::sys::path::filename(Entry->getName()) << " -->\n"; 564 565 os << "\n<!-- FUNCTIONNAME " << declName << " -->\n"; 566 567 os << "\n<!-- ISSUEHASHCONTENTOFLINEINCONTEXT " 568 << GetIssueHash(SMgr, L, D.getCheckName(), D.getBugType(), DeclWithIssue, 569 PP.getLangOpts()) << " -->\n"; 570 571 os << "\n<!-- BUGLINE " 572 << LineNumber 573 << " -->\n"; 574 575 os << "\n<!-- BUGCOLUMN " 576 << ColumnNumber 577 << " -->\n"; 578 579 os << "\n<!-- BUGPATHLENGTH " << path.size() << " -->\n"; 580 581 // Mark the end of the tags. 582 os << "\n<!-- BUGMETAEND -->\n"; 583 584 // Insert the text. 585 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str()); 586 } 587 588 html::AddHeaderFooterInternalBuiltinCSS(R, FID, Entry->getName()); 589 } 590 591 std::string HTMLDiagnostics::showHelpJavascript() { 592 return R"<<<( 593 <script type='text/javascript'> 594 595 var toggleHelp = function() { 596 var hint = document.querySelector("#tooltiphint"); 597 var attributeName = "hidden"; 598 if (hint.hasAttribute(attributeName)) { 599 hint.removeAttribute(attributeName); 600 } else { 601 hint.setAttribute("hidden", "true"); 602 } 603 }; 604 window.addEventListener("keydown", function (event) { 605 if (event.defaultPrevented) { 606 return; 607 } 608 if (event.key == "?") { 609 toggleHelp(); 610 } else { 611 return; 612 } 613 event.preventDefault(); 614 }); 615 </script> 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 // For now, just draw a box above the line in question, and emit the 664 // warning. 665 FullSourceLoc Pos = P.getLocation().asLocation(); 666 667 if (!Pos.isValid()) 668 return; 669 670 SourceManager &SM = R.getSourceMgr(); 671 assert(&Pos.getManager() == &SM && "SourceManagers are different!"); 672 std::pair<FileID, unsigned> LPosInfo = SM.getDecomposedExpansionLoc(Pos); 673 674 if (LPosInfo.first != BugFileID) 675 return; 676 677 const llvm::MemoryBuffer *Buf = SM.getBuffer(LPosInfo.first); 678 const char* FileStart = Buf->getBufferStart(); 679 680 // Compute the column number. Rewind from the current position to the start 681 // of the line. 682 unsigned ColNo = SM.getColumnNumber(LPosInfo.first, LPosInfo.second); 683 const char *TokInstantiationPtr =Pos.getExpansionLoc().getCharacterData(); 684 const char *LineStart = TokInstantiationPtr-ColNo; 685 686 // Compute LineEnd. 687 const char *LineEnd = TokInstantiationPtr; 688 const char* FileEnd = Buf->getBufferEnd(); 689 while (*LineEnd != '\n' && LineEnd != FileEnd) 690 ++LineEnd; 691 692 // Compute the margin offset by counting tabs and non-tabs. 693 unsigned PosNo = 0; 694 for (const char* c = LineStart; c != TokInstantiationPtr; ++c) 695 PosNo += *c == '\t' ? 8 : 1; 696 697 // Create the html for the message. 698 699 const char *Kind = nullptr; 700 bool IsNote = false; 701 bool SuppressIndex = (max == 1); 702 switch (P.getKind()) { 703 case PathDiagnosticPiece::Call: 704 llvm_unreachable("Calls and extra notes should already be handled"); 705 case PathDiagnosticPiece::Event: Kind = "Event"; break; 706 case PathDiagnosticPiece::ControlFlow: Kind = "Control"; break; 707 // Setting Kind to "Control" is intentional. 708 case PathDiagnosticPiece::Macro: Kind = "Control"; break; 709 case PathDiagnosticPiece::Note: 710 Kind = "Note"; 711 IsNote = true; 712 SuppressIndex = true; 713 break; 714 } 715 716 std::string sbuf; 717 llvm::raw_string_ostream os(sbuf); 718 719 os << "\n<tr><td class=\"num\"></td><td class=\"line\"><div id=\""; 720 721 if (IsNote) 722 os << "Note" << num; 723 else if (num == max) 724 os << "EndPath"; 725 else 726 os << "Path" << num; 727 728 os << "\" class=\"msg"; 729 if (Kind) 730 os << " msg" << Kind; 731 os << "\" style=\"margin-left:" << PosNo << "ex"; 732 733 // Output a maximum size. 734 if (!isa<PathDiagnosticMacroPiece>(P)) { 735 // Get the string and determining its maximum substring. 736 const auto &Msg = P.getString(); 737 unsigned max_token = 0; 738 unsigned cnt = 0; 739 unsigned len = Msg.size(); 740 741 for (char C : Msg) 742 switch (C) { 743 default: 744 ++cnt; 745 continue; 746 case ' ': 747 case '\t': 748 case '\n': 749 if (cnt > max_token) max_token = cnt; 750 cnt = 0; 751 } 752 753 if (cnt > max_token) 754 max_token = cnt; 755 756 // Determine the approximate size of the message bubble in em. 757 unsigned em; 758 const unsigned max_line = 120; 759 760 if (max_token >= max_line) 761 em = max_token / 2; 762 else { 763 unsigned characters = max_line; 764 unsigned lines = len / max_line; 765 766 if (lines > 0) { 767 for (; characters > max_token; --characters) 768 if (len / characters > lines) { 769 ++characters; 770 break; 771 } 772 } 773 774 em = characters / 2; 775 } 776 777 if (em < max_line/2) 778 os << "; max-width:" << em << "em"; 779 } 780 else 781 os << "; max-width:100em"; 782 783 os << "\">"; 784 785 if (!SuppressIndex) { 786 os << "<table class=\"msgT\"><tr><td valign=\"top\">"; 787 os << "<div class=\"PathIndex"; 788 if (Kind) os << " PathIndex" << Kind; 789 os << "\">" << num << "</div>"; 790 791 if (num > 1) { 792 os << "</td><td><div class=\"PathNav\"><a href=\"#Path" 793 << (num - 1) 794 << "\" title=\"Previous event (" 795 << (num - 1) 796 << ")\">←</a></div></td>"; 797 } 798 799 os << "</td><td>"; 800 } 801 802 if (const auto *MP = dyn_cast<PathDiagnosticMacroPiece>(&P)) { 803 os << "Within the expansion of the macro '"; 804 805 // Get the name of the macro by relexing it. 806 { 807 FullSourceLoc L = MP->getLocation().asLocation().getExpansionLoc(); 808 assert(L.isFileID()); 809 StringRef BufferInfo = L.getBufferData(); 810 std::pair<FileID, unsigned> LocInfo = L.getDecomposedLoc(); 811 const char* MacroName = LocInfo.second + BufferInfo.data(); 812 Lexer rawLexer(SM.getLocForStartOfFile(LocInfo.first), PP.getLangOpts(), 813 BufferInfo.begin(), MacroName, BufferInfo.end()); 814 815 Token TheTok; 816 rawLexer.LexFromRawLexer(TheTok); 817 for (unsigned i = 0, n = TheTok.getLength(); i < n; ++i) 818 os << MacroName[i]; 819 } 820 821 os << "':\n"; 822 823 if (!SuppressIndex) { 824 os << "</td>"; 825 if (num < max) { 826 os << "<td><div class=\"PathNav\"><a href=\"#"; 827 if (num == max - 1) 828 os << "EndPath"; 829 else 830 os << "Path" << (num + 1); 831 os << "\" title=\"Next event (" 832 << (num + 1) 833 << ")\">→</a></div></td>"; 834 } 835 836 os << "</tr></table>"; 837 } 838 839 // Within a macro piece. Write out each event. 840 ProcessMacroPiece(os, *MP, 0); 841 } 842 else { 843 os << html::EscapeText(P.getString()); 844 845 if (!SuppressIndex) { 846 os << "</td>"; 847 if (num < max) { 848 os << "<td><div class=\"PathNav\"><a href=\"#"; 849 if (num == max - 1) 850 os << "EndPath"; 851 else 852 os << "Path" << (num + 1); 853 os << "\" title=\"Next event (" 854 << (num + 1) 855 << ")\">→</a></div></td>"; 856 } 857 858 os << "</tr></table>"; 859 } 860 } 861 862 os << "</div></td></tr>"; 863 864 // Insert the new html. 865 unsigned DisplayPos = LineEnd - FileStart; 866 SourceLocation Loc = 867 SM.getLocForStartOfFile(LPosInfo.first).getLocWithOffset(DisplayPos); 868 869 R.InsertTextBefore(Loc, os.str()); 870 871 // Now highlight the ranges. 872 ArrayRef<SourceRange> Ranges = P.getRanges(); 873 for (const auto &Range : Ranges) 874 HighlightRange(R, LPosInfo.first, Range); 875 } 876 877 static void EmitAlphaCounter(raw_ostream &os, unsigned n) { 878 unsigned x = n % ('z' - 'a'); 879 n /= 'z' - 'a'; 880 881 if (n > 0) 882 EmitAlphaCounter(os, n); 883 884 os << char('a' + x); 885 } 886 887 unsigned HTMLDiagnostics::ProcessMacroPiece(raw_ostream &os, 888 const PathDiagnosticMacroPiece& P, 889 unsigned num) { 890 for (const auto &subPiece : P.subPieces) { 891 if (const auto *MP = dyn_cast<PathDiagnosticMacroPiece>(subPiece.get())) { 892 num = ProcessMacroPiece(os, *MP, num); 893 continue; 894 } 895 896 if (const auto *EP = dyn_cast<PathDiagnosticEventPiece>(subPiece.get())) { 897 os << "<div class=\"msg msgEvent\" style=\"width:94%; " 898 "margin-left:5px\">" 899 "<table class=\"msgT\"><tr>" 900 "<td valign=\"top\"><div class=\"PathIndex PathIndexEvent\">"; 901 EmitAlphaCounter(os, num++); 902 os << "</div></td><td valign=\"top\">" 903 << html::EscapeText(EP->getString()) 904 << "</td></tr></table></div>\n"; 905 } 906 } 907 908 return num; 909 } 910 911 void HTMLDiagnostics::HighlightRange(Rewriter& R, FileID BugFileID, 912 SourceRange Range, 913 const char *HighlightStart, 914 const char *HighlightEnd) { 915 SourceManager &SM = R.getSourceMgr(); 916 const LangOptions &LangOpts = R.getLangOpts(); 917 918 SourceLocation InstantiationStart = SM.getExpansionLoc(Range.getBegin()); 919 unsigned StartLineNo = SM.getExpansionLineNumber(InstantiationStart); 920 921 SourceLocation InstantiationEnd = SM.getExpansionLoc(Range.getEnd()); 922 unsigned EndLineNo = SM.getExpansionLineNumber(InstantiationEnd); 923 924 if (EndLineNo < StartLineNo) 925 return; 926 927 if (SM.getFileID(InstantiationStart) != BugFileID || 928 SM.getFileID(InstantiationEnd) != BugFileID) 929 return; 930 931 // Compute the column number of the end. 932 unsigned EndColNo = SM.getExpansionColumnNumber(InstantiationEnd); 933 unsigned OldEndColNo = EndColNo; 934 935 if (EndColNo) { 936 // Add in the length of the token, so that we cover multi-char tokens. 937 EndColNo += Lexer::MeasureTokenLength(Range.getEnd(), SM, LangOpts)-1; 938 } 939 940 // Highlight the range. Make the span tag the outermost tag for the 941 // selected range. 942 943 SourceLocation E = 944 InstantiationEnd.getLocWithOffset(EndColNo - OldEndColNo); 945 946 html::HighlightRange(R, InstantiationStart, E, HighlightStart, HighlightEnd); 947 } 948 949 std::string HTMLDiagnostics::generateKeyboardNavigationJavascript() { 950 return R"<<<( 951 <script type='text/javascript'> 952 var digitMatcher = new RegExp("[0-9]+"); 953 954 document.addEventListener("DOMContentLoaded", function() { 955 document.querySelectorAll(".PathNav > a").forEach( 956 function(currentValue, currentIndex) { 957 var hrefValue = currentValue.getAttribute("href"); 958 currentValue.onclick = function() { 959 scrollTo(document.querySelector(hrefValue)); 960 return false; 961 }; 962 }); 963 }); 964 965 var findNum = function() { 966 var s = document.querySelector(".selected"); 967 if (!s || s.id == "EndPath") { 968 return 0; 969 } 970 var out = parseInt(digitMatcher.exec(s.id)[0]); 971 return out; 972 }; 973 974 var scrollTo = function(el) { 975 document.querySelectorAll(".selected").forEach(function(s) { 976 s.classList.remove("selected"); 977 }); 978 el.classList.add("selected"); 979 window.scrollBy(0, el.getBoundingClientRect().top - 980 (window.innerHeight / 2)); 981 } 982 983 var move = function(num, up, numItems) { 984 if (num == 1 && up || num == numItems - 1 && !up) { 985 return 0; 986 } else if (num == 0 && up) { 987 return numItems - 1; 988 } else if (num == 0 && !up) { 989 return 1 % numItems; 990 } 991 return up ? num - 1 : num + 1; 992 } 993 994 var numToId = function(num) { 995 if (num == 0) { 996 return document.getElementById("EndPath") 997 } 998 return document.getElementById("Path" + num); 999 }; 1000 1001 var navigateTo = function(up) { 1002 var numItems = document.querySelectorAll(".line > .msg").length; 1003 var currentSelected = findNum(); 1004 var newSelected = move(currentSelected, up, numItems); 1005 var newEl = numToId(newSelected, numItems); 1006 1007 // Scroll element into center. 1008 scrollTo(newEl); 1009 }; 1010 1011 window.addEventListener("keydown", function (event) { 1012 if (event.defaultPrevented) { 1013 return; 1014 } 1015 if (event.key == "j") { 1016 navigateTo(/*up=*/false); 1017 } else if (event.key == "k") { 1018 navigateTo(/*up=*/true); 1019 } else { 1020 return; 1021 } 1022 event.preventDefault(); 1023 }, true); 1024 </script> 1025 )<<<"; 1026 } 1027