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