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