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