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