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