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/Analysis/IssueHash.h" 17 #include "clang/Analysis/MacroExpansionContext.h" 18 #include "clang/Analysis/PathDiagnostic.h" 19 #include "clang/Basic/FileManager.h" 20 #include "clang/Basic/LLVM.h" 21 #include "clang/Basic/SourceLocation.h" 22 #include "clang/Basic/SourceManager.h" 23 #include "clang/Lex/Lexer.h" 24 #include "clang/Lex/Preprocessor.h" 25 #include "clang/Lex/Token.h" 26 #include "clang/Rewrite/Core/HTMLRewrite.h" 27 #include "clang/Rewrite/Core/Rewriter.h" 28 #include "clang/StaticAnalyzer/Core/PathDiagnosticConsumers.h" 29 #include "llvm/ADT/ArrayRef.h" 30 #include "llvm/ADT/RewriteBuffer.h" 31 #include "llvm/ADT/STLExtras.h" 32 #include "llvm/ADT/Sequence.h" 33 #include "llvm/ADT/SmallString.h" 34 #include "llvm/ADT/StringRef.h" 35 #include "llvm/ADT/iterator_range.h" 36 #include "llvm/Support/Casting.h" 37 #include "llvm/Support/Errc.h" 38 #include "llvm/Support/ErrorHandling.h" 39 #include "llvm/Support/FileSystem.h" 40 #include "llvm/Support/MemoryBuffer.h" 41 #include "llvm/Support/Path.h" 42 #include "llvm/Support/raw_ostream.h" 43 #include <algorithm> 44 #include <cassert> 45 #include <map> 46 #include <memory> 47 #include <set> 48 #include <sstream> 49 #include <string> 50 #include <system_error> 51 #include <utility> 52 #include <vector> 53 54 using namespace clang; 55 using namespace ento; 56 using llvm::RewriteBuffer; 57 58 //===----------------------------------------------------------------------===// 59 // Boilerplate. 60 //===----------------------------------------------------------------------===// 61 62 namespace { 63 64 class ArrowMap; 65 66 class HTMLDiagnostics : public PathDiagnosticConsumer { 67 PathDiagnosticConsumerOptions DiagOpts; 68 std::string Directory; 69 bool createdDir = false; 70 bool noDir = false; 71 const Preprocessor &PP; 72 const bool SupportsCrossFileDiagnostics; 73 llvm::StringSet<> EmittedHashes; 74 html::RelexRewriteCacheRef RewriterCache = 75 html::instantiateRelexRewriteCache(); 76 77 public: 78 HTMLDiagnostics(PathDiagnosticConsumerOptions DiagOpts, 79 const std::string &OutputDir, const Preprocessor &pp, 80 bool supportsMultipleFiles) 81 : DiagOpts(std::move(DiagOpts)), Directory(OutputDir), PP(pp), 82 SupportsCrossFileDiagnostics(supportsMultipleFiles) {} 83 84 ~HTMLDiagnostics() override { FlushDiagnostics(nullptr); } 85 86 void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags, 87 FilesMade *filesMade) override; 88 89 StringRef getName() const override { return "HTMLDiagnostics"; } 90 91 bool supportsCrossFileDiagnostics() const override { 92 return SupportsCrossFileDiagnostics; 93 } 94 95 unsigned ProcessMacroPiece(raw_ostream &os, const PathDiagnosticMacroPiece &P, 96 unsigned num); 97 98 unsigned ProcessControlFlowPiece(Rewriter &R, FileID BugFileID, 99 const PathDiagnosticControlFlowPiece &P, 100 unsigned Number); 101 102 void HandlePiece(Rewriter &R, FileID BugFileID, const PathDiagnosticPiece &P, 103 const std::vector<SourceRange> &PopUpRanges, unsigned num, 104 unsigned max); 105 106 void HighlightRange(Rewriter &R, FileID BugFileID, SourceRange Range, 107 const char *HighlightStart = "<span class=\"mrange\">", 108 const char *HighlightEnd = "</span>"); 109 110 void ReportDiag(const PathDiagnostic &D, FilesMade *filesMade); 111 112 // Generate the full HTML report 113 std::string GenerateHTML(const PathDiagnostic &D, Rewriter &R, 114 const SourceManager &SMgr, const PathPieces &path, 115 const char *declName); 116 117 // Add HTML header/footers to file specified by FID 118 void FinalizeHTML(const PathDiagnostic &D, Rewriter &R, 119 const SourceManager &SMgr, const PathPieces &path, 120 FileID FID, FileEntryRef Entry, const char *declName); 121 122 // Rewrite the file specified by FID with HTML formatting. 123 void RewriteFile(Rewriter &R, const PathPieces &path, FileID FID); 124 125 PathGenerationScheme getGenerationScheme() const override { 126 return Everything; 127 } 128 129 private: 130 void addArrowSVGs(Rewriter &R, FileID BugFileID, 131 const ArrowMap &ArrowIndices); 132 133 /// \return Javascript for displaying shortcuts help; 134 StringRef showHelpJavascript(); 135 136 /// \return Javascript for navigating the HTML report using j/k keys. 137 StringRef generateKeyboardNavigationJavascript(); 138 139 /// \return Javascript for drawing control-flow arrows. 140 StringRef generateArrowDrawingJavascript(); 141 142 /// \return JavaScript for an option to only show relevant lines. 143 std::string showRelevantLinesJavascript(const PathDiagnostic &D, 144 const PathPieces &path); 145 146 /// Write executed lines from \p D in JSON format into \p os. 147 void dumpCoverageData(const PathDiagnostic &D, const PathPieces &path, 148 llvm::raw_string_ostream &os); 149 }; 150 151 bool isArrowPiece(const PathDiagnosticPiece &P) { 152 return isa<PathDiagnosticControlFlowPiece>(P) && P.getString().empty(); 153 } 154 155 unsigned getPathSizeWithoutArrows(const PathPieces &Path) { 156 unsigned TotalPieces = Path.size(); 157 unsigned TotalArrowPieces = llvm::count_if( 158 Path, [](const PathDiagnosticPieceRef &P) { return isArrowPiece(*P); }); 159 return TotalPieces - TotalArrowPieces; 160 } 161 162 class ArrowMap : public std::vector<unsigned> { 163 using Base = std::vector<unsigned>; 164 165 public: 166 ArrowMap(unsigned Size) : Base(Size, 0) {} 167 unsigned getTotalNumberOfArrows() const { return at(0); } 168 }; 169 170 llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const ArrowMap &Indices) { 171 OS << "[ "; 172 llvm::interleave(Indices, OS, ","); 173 return OS << " ]"; 174 } 175 176 } // namespace 177 178 void ento::createHTMLDiagnosticConsumer( 179 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 180 const std::string &OutputDir, const Preprocessor &PP, 181 const cross_tu::CrossTranslationUnitContext &CTU, 182 const MacroExpansionContext &MacroExpansions) { 183 184 // FIXME: HTML is currently our default output type, but if the output 185 // directory isn't specified, it acts like if it was in the minimal text 186 // output mode. This doesn't make much sense, we should have the minimal text 187 // as our default. In the case of backward compatibility concerns, this could 188 // be preserved with -analyzer-config-compatibility-mode=true. 189 createTextMinimalPathDiagnosticConsumer(DiagOpts, C, OutputDir, PP, CTU, 190 MacroExpansions); 191 192 // TODO: Emit an error here. 193 if (OutputDir.empty()) 194 return; 195 196 C.push_back(new HTMLDiagnostics(std::move(DiagOpts), OutputDir, PP, true)); 197 } 198 199 void ento::createHTMLSingleFileDiagnosticConsumer( 200 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 201 const std::string &OutputDir, const Preprocessor &PP, 202 const cross_tu::CrossTranslationUnitContext &CTU, 203 const clang::MacroExpansionContext &MacroExpansions) { 204 createTextMinimalPathDiagnosticConsumer(DiagOpts, C, OutputDir, PP, CTU, 205 MacroExpansions); 206 207 // TODO: Emit an error here. 208 if (OutputDir.empty()) 209 return; 210 211 C.push_back(new HTMLDiagnostics(std::move(DiagOpts), OutputDir, PP, false)); 212 } 213 214 void ento::createPlistHTMLDiagnosticConsumer( 215 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 216 const std::string &prefix, const Preprocessor &PP, 217 const cross_tu::CrossTranslationUnitContext &CTU, 218 const MacroExpansionContext &MacroExpansions) { 219 createHTMLDiagnosticConsumer( 220 DiagOpts, C, std::string(llvm::sys::path::parent_path(prefix)), PP, CTU, 221 MacroExpansions); 222 createPlistMultiFileDiagnosticConsumer(DiagOpts, C, prefix, PP, CTU, 223 MacroExpansions); 224 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, prefix, PP, 225 CTU, MacroExpansions); 226 } 227 228 void ento::createSarifHTMLDiagnosticConsumer( 229 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 230 const std::string &sarif_file, const Preprocessor &PP, 231 const cross_tu::CrossTranslationUnitContext &CTU, 232 const MacroExpansionContext &MacroExpansions) { 233 createHTMLDiagnosticConsumer( 234 DiagOpts, C, std::string(llvm::sys::path::parent_path(sarif_file)), PP, 235 CTU, MacroExpansions); 236 createSarifDiagnosticConsumer(DiagOpts, C, sarif_file, PP, CTU, 237 MacroExpansions); 238 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, sarif_file, 239 PP, CTU, MacroExpansions); 240 } 241 242 //===----------------------------------------------------------------------===// 243 // Report processing. 244 //===----------------------------------------------------------------------===// 245 246 void HTMLDiagnostics::FlushDiagnosticsImpl( 247 std::vector<const PathDiagnostic *> &Diags, 248 FilesMade *filesMade) { 249 for (const auto Diag : Diags) 250 ReportDiag(*Diag, filesMade); 251 } 252 253 static llvm::SmallString<32> getIssueHash(const PathDiagnostic &D, 254 const Preprocessor &PP) { 255 SourceManager &SMgr = PP.getSourceManager(); 256 PathDiagnosticLocation UPDLoc = D.getUniqueingLoc(); 257 FullSourceLoc L(SMgr.getExpansionLoc(UPDLoc.isValid() 258 ? UPDLoc.asLocation() 259 : D.getLocation().asLocation()), 260 SMgr); 261 return getIssueHash(L, D.getCheckerName(), D.getBugType(), 262 D.getDeclWithIssue(), PP.getLangOpts()); 263 } 264 265 void HTMLDiagnostics::ReportDiag(const PathDiagnostic& D, 266 FilesMade *filesMade) { 267 // Create the HTML directory if it is missing. 268 if (!createdDir) { 269 createdDir = true; 270 if (std::error_code ec = llvm::sys::fs::create_directories(Directory)) { 271 llvm::errs() << "warning: could not create directory '" 272 << Directory << "': " << ec.message() << '\n'; 273 noDir = true; 274 return; 275 } 276 } 277 278 if (noDir) 279 return; 280 281 // First flatten out the entire path to make it easier to use. 282 PathPieces path = D.path.flatten(/*ShouldFlattenMacros=*/false); 283 284 // The path as already been prechecked that the path is non-empty. 285 assert(!path.empty()); 286 const SourceManager &SMgr = path.front()->getLocation().getManager(); 287 288 // Create a new rewriter to generate HTML. 289 Rewriter R(const_cast<SourceManager&>(SMgr), PP.getLangOpts()); 290 291 // Get the function/method name 292 SmallString<128> declName("unknown"); 293 int offsetDecl = 0; 294 if (const Decl *DeclWithIssue = D.getDeclWithIssue()) { 295 if (const auto *ND = dyn_cast<NamedDecl>(DeclWithIssue)) 296 declName = ND->getDeclName().getAsString(); 297 298 if (const Stmt *Body = DeclWithIssue->getBody()) { 299 // Retrieve the relative position of the declaration which will be used 300 // for the file name 301 FullSourceLoc L( 302 SMgr.getExpansionLoc(path.back()->getLocation().asLocation()), 303 SMgr); 304 FullSourceLoc FunL(SMgr.getExpansionLoc(Body->getBeginLoc()), SMgr); 305 offsetDecl = L.getExpansionLineNumber() - FunL.getExpansionLineNumber(); 306 } 307 } 308 309 SmallString<32> IssueHash = getIssueHash(D, PP); 310 auto [It, IsNew] = EmittedHashes.insert(IssueHash); 311 if (!IsNew) { 312 // We've already emitted a duplicate issue. It'll get overwritten anyway. 313 return; 314 } 315 316 std::string report = GenerateHTML(D, R, SMgr, path, declName.c_str()); 317 if (report.empty()) { 318 llvm::errs() << "warning: no diagnostics generated for main file.\n"; 319 return; 320 } 321 322 // Create a path for the target HTML file. 323 int FD; 324 325 SmallString<128> FileNameStr; 326 llvm::raw_svector_ostream FileName(FileNameStr); 327 FileName << "report-"; 328 329 // Historically, neither the stable report filename nor the unstable report 330 // filename were actually stable. That said, the stable report filename 331 // was more stable because it was mostly composed of information 332 // about the bug report instead of being completely random. 333 // Now both stable and unstable report filenames are in fact stable 334 // but the stable report filename is still more verbose. 335 if (DiagOpts.ShouldWriteVerboseReportFilename) { 336 // FIXME: This code relies on knowing what constitutes the issue hash. 337 // Otherwise deduplication won't work correctly. 338 FileID ReportFile = 339 path.back()->getLocation().asLocation().getExpansionLoc().getFileID(); 340 341 OptionalFileEntryRef Entry = SMgr.getFileEntryRefForID(ReportFile); 342 343 FileName << llvm::sys::path::filename(Entry->getName()).str() << "-" 344 << declName.c_str() << "-" << offsetDecl << "-"; 345 } 346 347 FileName << StringRef(IssueHash).substr(0, 6).str() << ".html"; 348 349 SmallString<128> ResultPath; 350 llvm::sys::path::append(ResultPath, Directory, FileName.str()); 351 if (std::error_code EC = llvm::sys::fs::make_absolute(ResultPath)) { 352 llvm::errs() << "warning: could not make '" << ResultPath 353 << "' absolute: " << EC.message() << '\n'; 354 return; 355 } 356 357 if (std::error_code EC = llvm::sys::fs::openFileForReadWrite( 358 ResultPath, FD, llvm::sys::fs::CD_CreateNew, 359 llvm::sys::fs::OF_Text)) { 360 // Existence of the file corresponds to the situation where a different 361 // Clang instance has emitted a bug report with the same issue hash. 362 // This is an entirely normal situation that does not deserve a warning, 363 // as apart from hash collisions this can happen because the reports 364 // are in fact similar enough to be considered duplicates of each other. 365 if (EC != llvm::errc::file_exists) { 366 llvm::errs() << "warning: could not create file in '" << Directory 367 << "': " << EC.message() << '\n'; 368 } 369 return; 370 } 371 372 llvm::raw_fd_ostream os(FD, true); 373 374 if (filesMade) 375 filesMade->addDiagnostic(D, getName(), 376 llvm::sys::path::filename(ResultPath)); 377 378 // Emit the HTML to disk. 379 os << report; 380 } 381 382 std::string HTMLDiagnostics::GenerateHTML(const PathDiagnostic& D, Rewriter &R, 383 const SourceManager& SMgr, const PathPieces& path, const char *declName) { 384 // Rewrite source files as HTML for every new file the path crosses 385 std::vector<FileID> FileIDs; 386 for (auto I : path) { 387 FileID FID = I->getLocation().asLocation().getExpansionLoc().getFileID(); 388 if (llvm::is_contained(FileIDs, FID)) 389 continue; 390 391 FileIDs.push_back(FID); 392 RewriteFile(R, path, FID); 393 } 394 395 if (SupportsCrossFileDiagnostics && FileIDs.size() > 1) { 396 // Prefix file names, anchor tags, and nav cursors to every file 397 for (auto I = FileIDs.begin(), E = FileIDs.end(); I != E; I++) { 398 std::string s; 399 llvm::raw_string_ostream os(s); 400 401 if (I != FileIDs.begin()) 402 os << "<hr class=divider>\n"; 403 404 os << "<div id=File" << I->getHashValue() << ">\n"; 405 406 // Left nav arrow 407 if (I != FileIDs.begin()) 408 os << "<div class=FileNav><a href=\"#File" << (I - 1)->getHashValue() 409 << "\">←</a></div>"; 410 411 os << "<h4 class=FileName>" << SMgr.getFileEntryRefForID(*I)->getName() 412 << "</h4>\n"; 413 414 // Right nav arrow 415 if (I + 1 != E) 416 os << "<div class=FileNav><a href=\"#File" << (I + 1)->getHashValue() 417 << "\">→</a></div>"; 418 419 os << "</div>\n"; 420 421 R.InsertTextBefore(SMgr.getLocForStartOfFile(*I), os.str()); 422 } 423 424 // Append files to the main report file in the order they appear in the path 425 for (auto I : llvm::drop_begin(FileIDs)) { 426 std::string s; 427 llvm::raw_string_ostream os(s); 428 429 const RewriteBuffer *Buf = R.getRewriteBufferFor(I); 430 for (auto BI : *Buf) 431 os << BI; 432 433 R.InsertTextAfter(SMgr.getLocForEndOfFile(FileIDs[0]), os.str()); 434 } 435 } 436 437 const RewriteBuffer *Buf = R.getRewriteBufferFor(FileIDs[0]); 438 if (!Buf) 439 return {}; 440 441 // Add CSS, header, and footer. 442 FileID FID = 443 path.back()->getLocation().asLocation().getExpansionLoc().getFileID(); 444 OptionalFileEntryRef Entry = SMgr.getFileEntryRefForID(FID); 445 FinalizeHTML(D, R, SMgr, path, FileIDs[0], *Entry, declName); 446 447 std::string file; 448 llvm::raw_string_ostream os(file); 449 for (auto BI : *Buf) 450 os << BI; 451 452 return file; 453 } 454 455 void HTMLDiagnostics::dumpCoverageData( 456 const PathDiagnostic &D, 457 const PathPieces &path, 458 llvm::raw_string_ostream &os) { 459 460 const FilesToLineNumsMap &ExecutedLines = D.getExecutedLines(); 461 462 os << "var relevant_lines = {"; 463 for (auto I = ExecutedLines.begin(), 464 E = ExecutedLines.end(); I != E; ++I) { 465 if (I != ExecutedLines.begin()) 466 os << ", "; 467 468 os << "\"" << I->first.getHashValue() << "\": {"; 469 for (unsigned LineNo : I->second) { 470 if (LineNo != *(I->second.begin())) 471 os << ", "; 472 473 os << "\"" << LineNo << "\": 1"; 474 } 475 os << "}"; 476 } 477 478 os << "};"; 479 } 480 481 std::string HTMLDiagnostics::showRelevantLinesJavascript( 482 const PathDiagnostic &D, const PathPieces &path) { 483 std::string s; 484 llvm::raw_string_ostream os(s); 485 os << "<script type='text/javascript'>\n"; 486 dumpCoverageData(D, path, os); 487 os << R"<<<( 488 489 var filterCounterexample = function (hide) { 490 var tables = document.getElementsByClassName("code"); 491 for (var t=0; t<tables.length; t++) { 492 var table = tables[t]; 493 var file_id = table.getAttribute("data-fileid"); 494 var lines_in_fid = relevant_lines[file_id]; 495 if (!lines_in_fid) { 496 lines_in_fid = {}; 497 } 498 var lines = table.getElementsByClassName("codeline"); 499 for (var i=0; i<lines.length; i++) { 500 var el = lines[i]; 501 var lineNo = el.getAttribute("data-linenumber"); 502 if (!lines_in_fid[lineNo]) { 503 if (hide) { 504 el.setAttribute("hidden", ""); 505 } else { 506 el.removeAttribute("hidden"); 507 } 508 } 509 } 510 } 511 } 512 513 window.addEventListener("keydown", function (event) { 514 if (event.defaultPrevented) { 515 return; 516 } 517 // SHIFT + S 518 if (event.shiftKey && event.keyCode == 83) { 519 var checked = document.getElementsByName("showCounterexample")[0].checked; 520 filterCounterexample(!checked); 521 document.getElementsByName("showCounterexample")[0].click(); 522 } else { 523 return; 524 } 525 event.preventDefault(); 526 }, true); 527 528 document.addEventListener("DOMContentLoaded", function() { 529 document.querySelector('input[name="showCounterexample"]').onchange= 530 function (event) { 531 filterCounterexample(this.checked); 532 }; 533 }); 534 </script> 535 536 <form> 537 <input type="checkbox" name="showCounterexample" id="showCounterexample" /> 538 <label for="showCounterexample"> 539 Show only relevant lines 540 </label> 541 <input type="checkbox" name="showArrows" 542 id="showArrows" style="margin-left: 10px" /> 543 <label for="showArrows"> 544 Show control flow arrows 545 </label> 546 </form> 547 )<<<"; 548 549 return s; 550 } 551 552 void HTMLDiagnostics::FinalizeHTML(const PathDiagnostic &D, Rewriter &R, 553 const SourceManager &SMgr, 554 const PathPieces &path, FileID FID, 555 FileEntryRef Entry, const char *declName) { 556 // This is a cludge; basically we want to append either the full 557 // working directory if we have no directory information. This is 558 // a work in progress. 559 560 llvm::SmallString<0> DirName; 561 562 if (llvm::sys::path::is_relative(Entry.getName())) { 563 llvm::sys::fs::current_path(DirName); 564 DirName += '/'; 565 } 566 567 int LineNumber = path.back()->getLocation().asLocation().getExpansionLineNumber(); 568 int ColumnNumber = path.back()->getLocation().asLocation().getExpansionColumnNumber(); 569 570 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), showHelpJavascript()); 571 572 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), 573 generateKeyboardNavigationJavascript()); 574 575 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), 576 generateArrowDrawingJavascript()); 577 578 // Checkbox and javascript for filtering the output to the counterexample. 579 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), 580 showRelevantLinesJavascript(D, path)); 581 582 // Add the name of the file as an <h1> tag. 583 { 584 std::string s; 585 llvm::raw_string_ostream os(s); 586 587 os << "<!-- REPORTHEADER -->\n" 588 << "<h3>Bug Summary</h3>\n<table class=\"simpletable\">\n" 589 "<tr><td class=\"rowname\">File:</td><td>" 590 << html::EscapeText(DirName) 591 << html::EscapeText(Entry.getName()) 592 << "</td></tr>\n<tr><td class=\"rowname\">Warning:</td><td>" 593 "<a href=\"#EndPath\">line " 594 << LineNumber 595 << ", column " 596 << ColumnNumber 597 << "</a><br />" 598 << D.getVerboseDescription() << "</td></tr>\n"; 599 600 // The navigation across the extra notes pieces. 601 unsigned NumExtraPieces = 0; 602 for (const auto &Piece : path) { 603 if (const auto *P = dyn_cast<PathDiagnosticNotePiece>(Piece.get())) { 604 int LineNumber = 605 P->getLocation().asLocation().getExpansionLineNumber(); 606 int ColumnNumber = 607 P->getLocation().asLocation().getExpansionColumnNumber(); 608 ++NumExtraPieces; 609 os << "<tr><td class=\"rowname\">Note:</td><td>" 610 << "<a href=\"#Note" << NumExtraPieces << "\">line " 611 << LineNumber << ", column " << ColumnNumber << "</a><br />" 612 << P->getString() << "</td></tr>"; 613 } 614 } 615 616 // Output any other meta data. 617 618 for (const std::string &Metadata : 619 llvm::make_range(D.meta_begin(), D.meta_end())) { 620 os << "<tr><td></td><td>" << html::EscapeText(Metadata) << "</td></tr>\n"; 621 } 622 623 os << R"<<<( 624 </table> 625 <!-- REPORTSUMMARYEXTRA --> 626 <h3>Annotated Source Code</h3> 627 <p>Press <a href="#" onclick="toggleHelp(); return false;">'?'</a> 628 to see keyboard shortcuts</p> 629 <input type="checkbox" class="spoilerhider" id="showinvocation" /> 630 <label for="showinvocation" >Show analyzer invocation</label> 631 <div class="spoiler">clang -cc1 )<<<"; 632 os << html::EscapeText(DiagOpts.ToolInvocation); 633 os << R"<<<( 634 </div> 635 <div id='tooltiphint' hidden="true"> 636 <p>Keyboard shortcuts: </p> 637 <ul> 638 <li>Use 'j/k' keys for keyboard navigation</li> 639 <li>Use 'Shift+S' to show/hide relevant lines</li> 640 <li>Use '?' to toggle this window</li> 641 </ul> 642 <a href="#" onclick="toggleHelp(); return false;">Close</a> 643 </div> 644 )<<<"; 645 646 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str()); 647 } 648 649 // Embed meta-data tags. 650 { 651 std::string s; 652 llvm::raw_string_ostream os(s); 653 654 StringRef BugDesc = D.getVerboseDescription(); 655 if (!BugDesc.empty()) 656 os << "\n<!-- BUGDESC " << BugDesc << " -->\n"; 657 658 StringRef BugType = D.getBugType(); 659 if (!BugType.empty()) 660 os << "\n<!-- BUGTYPE " << BugType << " -->\n"; 661 662 PathDiagnosticLocation UPDLoc = D.getUniqueingLoc(); 663 FullSourceLoc L(SMgr.getExpansionLoc(UPDLoc.isValid() 664 ? UPDLoc.asLocation() 665 : D.getLocation().asLocation()), 666 SMgr); 667 668 StringRef BugCategory = D.getCategory(); 669 if (!BugCategory.empty()) 670 os << "\n<!-- BUGCATEGORY " << BugCategory << " -->\n"; 671 672 os << "\n<!-- BUGFILE " << DirName << Entry.getName() << " -->\n"; 673 674 os << "\n<!-- FILENAME " << llvm::sys::path::filename(Entry.getName()) << " -->\n"; 675 676 os << "\n<!-- FUNCTIONNAME " << declName << " -->\n"; 677 678 os << "\n<!-- ISSUEHASHCONTENTOFLINEINCONTEXT " << getIssueHash(D, PP) 679 << " -->\n"; 680 681 os << "\n<!-- BUGLINE " 682 << LineNumber 683 << " -->\n"; 684 685 os << "\n<!-- BUGCOLUMN " 686 << ColumnNumber 687 << " -->\n"; 688 689 os << "\n<!-- BUGPATHLENGTH " << getPathSizeWithoutArrows(path) << " -->\n"; 690 691 // Mark the end of the tags. 692 os << "\n<!-- BUGMETAEND -->\n"; 693 694 // Insert the text. 695 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str()); 696 } 697 698 html::AddHeaderFooterInternalBuiltinCSS(R, FID, Entry.getName()); 699 } 700 701 StringRef HTMLDiagnostics::showHelpJavascript() { 702 return R"<<<( 703 <script type='text/javascript'> 704 705 var toggleHelp = function() { 706 var hint = document.querySelector("#tooltiphint"); 707 var attributeName = "hidden"; 708 if (hint.hasAttribute(attributeName)) { 709 hint.removeAttribute(attributeName); 710 } else { 711 hint.setAttribute("hidden", "true"); 712 } 713 }; 714 window.addEventListener("keydown", function (event) { 715 if (event.defaultPrevented) { 716 return; 717 } 718 if (event.key == "?") { 719 toggleHelp(); 720 } else { 721 return; 722 } 723 event.preventDefault(); 724 }); 725 </script> 726 )<<<"; 727 } 728 729 static bool shouldDisplayPopUpRange(const SourceRange &Range) { 730 return !(Range.getBegin().isMacroID() || Range.getEnd().isMacroID()); 731 } 732 733 static void 734 HandlePopUpPieceStartTag(Rewriter &R, 735 const std::vector<SourceRange> &PopUpRanges) { 736 for (const auto &Range : PopUpRanges) { 737 if (!shouldDisplayPopUpRange(Range)) 738 continue; 739 740 html::HighlightRange(R, Range.getBegin(), Range.getEnd(), "", 741 "<table class='variable_popup'><tbody>", 742 /*IsTokenRange=*/true); 743 } 744 } 745 746 static void HandlePopUpPieceEndTag(Rewriter &R, 747 const PathDiagnosticPopUpPiece &Piece, 748 std::vector<SourceRange> &PopUpRanges, 749 unsigned int LastReportedPieceIndex, 750 unsigned int PopUpPieceIndex) { 751 SmallString<256> Buf; 752 llvm::raw_svector_ostream Out(Buf); 753 754 SourceRange Range(Piece.getLocation().asRange()); 755 if (!shouldDisplayPopUpRange(Range)) 756 return; 757 758 // Write out the path indices with a right arrow and the message as a row. 759 Out << "<tr><td valign='top'><div class='PathIndex PathIndexPopUp'>" 760 << LastReportedPieceIndex; 761 762 // Also annotate the state transition with extra indices. 763 Out << '.' << PopUpPieceIndex; 764 765 Out << "</div></td><td>" << Piece.getString() << "</td></tr>"; 766 767 // If no report made at this range mark the variable and add the end tags. 768 if (!llvm::is_contained(PopUpRanges, Range)) { 769 // Store that we create a report at this range. 770 PopUpRanges.push_back(Range); 771 772 Out << "</tbody></table></span>"; 773 html::HighlightRange(R, Range.getBegin(), Range.getEnd(), 774 "<span class='variable'>", Buf.c_str(), 775 /*IsTokenRange=*/true); 776 } else { 777 // Otherwise inject just the new row at the end of the range. 778 html::HighlightRange(R, Range.getBegin(), Range.getEnd(), "", Buf.c_str(), 779 /*IsTokenRange=*/true); 780 } 781 } 782 783 void HTMLDiagnostics::RewriteFile(Rewriter &R, const PathPieces &path, 784 FileID FID) { 785 786 // Process the path. 787 // Maintain the counts of extra note pieces separately. 788 unsigned TotalPieces = getPathSizeWithoutArrows(path); 789 unsigned TotalNotePieces = 790 llvm::count_if(path, [](const PathDiagnosticPieceRef &p) { 791 return isa<PathDiagnosticNotePiece>(*p); 792 }); 793 unsigned PopUpPieceCount = 794 llvm::count_if(path, [](const PathDiagnosticPieceRef &p) { 795 return isa<PathDiagnosticPopUpPiece>(*p); 796 }); 797 798 unsigned TotalRegularPieces = TotalPieces - TotalNotePieces - PopUpPieceCount; 799 unsigned NumRegularPieces = TotalRegularPieces; 800 unsigned NumNotePieces = TotalNotePieces; 801 unsigned NumberOfArrows = 0; 802 // Stores the count of the regular piece indices. 803 std::map<int, int> IndexMap; 804 ArrowMap ArrowIndices(TotalRegularPieces + 1); 805 806 // Stores the different ranges where we have reported something. 807 std::vector<SourceRange> PopUpRanges; 808 for (const PathDiagnosticPieceRef &I : llvm::reverse(path)) { 809 const auto &Piece = *I.get(); 810 811 if (isa<PathDiagnosticPopUpPiece>(Piece)) { 812 ++IndexMap[NumRegularPieces]; 813 } else if (isa<PathDiagnosticNotePiece>(Piece)) { 814 // This adds diagnostic bubbles, but not navigation. 815 // Navigation through note pieces would be added later, 816 // as a separate pass through the piece list. 817 HandlePiece(R, FID, Piece, PopUpRanges, NumNotePieces, TotalNotePieces); 818 --NumNotePieces; 819 820 } else if (isArrowPiece(Piece)) { 821 NumberOfArrows = ProcessControlFlowPiece( 822 R, FID, cast<PathDiagnosticControlFlowPiece>(Piece), NumberOfArrows); 823 ArrowIndices[NumRegularPieces] = NumberOfArrows; 824 825 } else { 826 HandlePiece(R, FID, Piece, PopUpRanges, NumRegularPieces, 827 TotalRegularPieces); 828 --NumRegularPieces; 829 ArrowIndices[NumRegularPieces] = ArrowIndices[NumRegularPieces + 1]; 830 } 831 } 832 ArrowIndices[0] = NumberOfArrows; 833 834 // At this point ArrowIndices represent the following data structure: 835 // [a_0, a_1, ..., a_N] 836 // where N is the number of events in the path. 837 // 838 // Then for every event with index i \in [0, N - 1], we can say that 839 // arrows with indices \in [a_(i+1), a_i) correspond to that event. 840 // We can say that because arrows with these indices appeared in the 841 // path in between the i-th and the (i+1)-th events. 842 assert(ArrowIndices.back() == 0 && 843 "No arrows should be after the last event"); 844 // This assertion also guarantees that all indices in are <= NumberOfArrows. 845 assert(llvm::is_sorted(ArrowIndices, std::greater<unsigned>()) && 846 "Incorrect arrow indices map"); 847 848 // Secondary indexing if we are having multiple pop-ups between two notes. 849 // (e.g. [(13) 'a' is 'true']; [(13.1) 'b' is 'false']; [(13.2) 'c' is...) 850 NumRegularPieces = TotalRegularPieces; 851 for (const PathDiagnosticPieceRef &I : llvm::reverse(path)) { 852 const auto &Piece = *I.get(); 853 854 if (const auto *PopUpP = dyn_cast<PathDiagnosticPopUpPiece>(&Piece)) { 855 int PopUpPieceIndex = IndexMap[NumRegularPieces]; 856 857 // Pop-up pieces needs the index of the last reported piece and its count 858 // how many times we report to handle multiple reports on the same range. 859 // This marks the variable, adds the </table> end tag and the message 860 // (list element) as a row. The <table> start tag will be added after the 861 // rows has been written out. Note: It stores every different range. 862 HandlePopUpPieceEndTag(R, *PopUpP, PopUpRanges, NumRegularPieces, 863 PopUpPieceIndex); 864 865 if (PopUpPieceIndex > 0) 866 --IndexMap[NumRegularPieces]; 867 868 } else if (!isa<PathDiagnosticNotePiece>(Piece) && !isArrowPiece(Piece)) { 869 --NumRegularPieces; 870 } 871 } 872 873 // Add the <table> start tag of pop-up pieces based on the stored ranges. 874 HandlePopUpPieceStartTag(R, PopUpRanges); 875 876 // Add line numbers, header, footer, etc. 877 html::EscapeText(R, FID); 878 html::AddLineNumbers(R, FID); 879 880 addArrowSVGs(R, FID, ArrowIndices); 881 882 // If we have a preprocessor, relex the file and syntax highlight. 883 // We might not have a preprocessor if we come from a deserialized AST file, 884 // for example. 885 html::SyntaxHighlight(R, FID, PP, RewriterCache); 886 html::HighlightMacros(R, FID, PP, RewriterCache); 887 } 888 889 void HTMLDiagnostics::HandlePiece(Rewriter &R, FileID BugFileID, 890 const PathDiagnosticPiece &P, 891 const std::vector<SourceRange> &PopUpRanges, 892 unsigned num, unsigned max) { 893 // For now, just draw a box above the line in question, and emit the 894 // warning. 895 FullSourceLoc Pos = P.getLocation().asLocation(); 896 897 if (!Pos.isValid()) 898 return; 899 900 SourceManager &SM = R.getSourceMgr(); 901 assert(&Pos.getManager() == &SM && "SourceManagers are different!"); 902 std::pair<FileID, unsigned> LPosInfo = SM.getDecomposedExpansionLoc(Pos); 903 904 if (LPosInfo.first != BugFileID) 905 return; 906 907 llvm::MemoryBufferRef Buf = SM.getBufferOrFake(LPosInfo.first); 908 const char *FileStart = Buf.getBufferStart(); 909 910 // Compute the column number. Rewind from the current position to the start 911 // of the line. 912 unsigned ColNo = SM.getColumnNumber(LPosInfo.first, LPosInfo.second); 913 const char *TokInstantiationPtr =Pos.getExpansionLoc().getCharacterData(); 914 const char *LineStart = TokInstantiationPtr-ColNo; 915 916 // Compute LineEnd. 917 const char *LineEnd = TokInstantiationPtr; 918 const char *FileEnd = Buf.getBufferEnd(); 919 while (*LineEnd != '\n' && LineEnd != FileEnd) 920 ++LineEnd; 921 922 // Compute the margin offset by counting tabs and non-tabs. 923 unsigned PosNo = 0; 924 for (const char* c = LineStart; c != TokInstantiationPtr; ++c) 925 PosNo += *c == '\t' ? 8 : 1; 926 927 // Create the html for the message. 928 929 const char *Kind = nullptr; 930 bool IsNote = false; 931 bool SuppressIndex = (max == 1); 932 switch (P.getKind()) { 933 case PathDiagnosticPiece::Event: Kind = "Event"; break; 934 case PathDiagnosticPiece::ControlFlow: Kind = "Control"; break; 935 // Setting Kind to "Control" is intentional. 936 case PathDiagnosticPiece::Macro: Kind = "Control"; break; 937 case PathDiagnosticPiece::Note: 938 Kind = "Note"; 939 IsNote = true; 940 SuppressIndex = true; 941 break; 942 case PathDiagnosticPiece::Call: 943 case PathDiagnosticPiece::PopUp: 944 llvm_unreachable("Calls and extra notes should already be handled"); 945 } 946 947 std::string sbuf; 948 llvm::raw_string_ostream os(sbuf); 949 950 os << "\n<tr><td class=\"num\"></td><td class=\"line\"><div id=\""; 951 952 if (IsNote) 953 os << "Note" << num; 954 else if (num == max) 955 os << "EndPath"; 956 else 957 os << "Path" << num; 958 959 os << "\" class=\"msg"; 960 if (Kind) 961 os << " msg" << Kind; 962 os << "\" style=\"margin-left:" << PosNo << "ex"; 963 964 // Output a maximum size. 965 if (!isa<PathDiagnosticMacroPiece>(P)) { 966 // Get the string and determining its maximum substring. 967 const auto &Msg = P.getString(); 968 unsigned max_token = 0; 969 unsigned cnt = 0; 970 unsigned len = Msg.size(); 971 972 for (char C : Msg) 973 switch (C) { 974 default: 975 ++cnt; 976 continue; 977 case ' ': 978 case '\t': 979 case '\n': 980 if (cnt > max_token) max_token = cnt; 981 cnt = 0; 982 } 983 984 if (cnt > max_token) 985 max_token = cnt; 986 987 // Determine the approximate size of the message bubble in em. 988 unsigned em; 989 const unsigned max_line = 120; 990 991 if (max_token >= max_line) 992 em = max_token / 2; 993 else { 994 unsigned characters = max_line; 995 unsigned lines = len / max_line; 996 997 if (lines > 0) { 998 for (; characters > max_token; --characters) 999 if (len / characters > lines) { 1000 ++characters; 1001 break; 1002 } 1003 } 1004 1005 em = characters / 2; 1006 } 1007 1008 if (em < max_line/2) 1009 os << "; max-width:" << em << "em"; 1010 } 1011 else 1012 os << "; max-width:100em"; 1013 1014 os << "\">"; 1015 1016 if (!SuppressIndex) { 1017 os << "<table class=\"msgT\"><tr><td valign=\"top\">"; 1018 os << "<div class=\"PathIndex"; 1019 if (Kind) os << " PathIndex" << Kind; 1020 os << "\">" << num << "</div>"; 1021 1022 if (num > 1) { 1023 os << "</td><td><div class=\"PathNav\"><a href=\"#Path" 1024 << (num - 1) 1025 << "\" title=\"Previous event (" 1026 << (num - 1) 1027 << ")\">←</a></div>"; 1028 } 1029 1030 os << "</td><td>"; 1031 } 1032 1033 if (const auto *MP = dyn_cast<PathDiagnosticMacroPiece>(&P)) { 1034 os << "Within the expansion of the macro '"; 1035 1036 // Get the name of the macro by relexing it. 1037 { 1038 FullSourceLoc L = MP->getLocation().asLocation().getExpansionLoc(); 1039 assert(L.isFileID()); 1040 StringRef BufferInfo = L.getBufferData(); 1041 std::pair<FileID, unsigned> LocInfo = L.getDecomposedLoc(); 1042 const char* MacroName = LocInfo.second + BufferInfo.data(); 1043 Lexer rawLexer(SM.getLocForStartOfFile(LocInfo.first), PP.getLangOpts(), 1044 BufferInfo.begin(), MacroName, BufferInfo.end()); 1045 1046 Token TheTok; 1047 rawLexer.LexFromRawLexer(TheTok); 1048 for (unsigned i = 0, n = TheTok.getLength(); i < n; ++i) 1049 os << MacroName[i]; 1050 } 1051 1052 os << "':\n"; 1053 1054 if (!SuppressIndex) { 1055 os << "</td>"; 1056 if (num < max) { 1057 os << "<td><div class=\"PathNav\"><a href=\"#"; 1058 if (num == max - 1) 1059 os << "EndPath"; 1060 else 1061 os << "Path" << (num + 1); 1062 os << "\" title=\"Next event (" 1063 << (num + 1) 1064 << ")\">→</a></div></td>"; 1065 } 1066 1067 os << "</tr></table>"; 1068 } 1069 1070 // Within a macro piece. Write out each event. 1071 ProcessMacroPiece(os, *MP, 0); 1072 } 1073 else { 1074 os << html::EscapeText(P.getString()); 1075 1076 if (!SuppressIndex) { 1077 os << "</td>"; 1078 if (num < max) { 1079 os << "<td><div class=\"PathNav\"><a href=\"#"; 1080 if (num == max - 1) 1081 os << "EndPath"; 1082 else 1083 os << "Path" << (num + 1); 1084 os << "\" title=\"Next event (" 1085 << (num + 1) 1086 << ")\">→</a></div></td>"; 1087 } 1088 1089 os << "</tr></table>"; 1090 } 1091 } 1092 1093 os << "</div></td></tr>"; 1094 1095 // Insert the new html. 1096 unsigned DisplayPos = LineEnd - FileStart; 1097 SourceLocation Loc = 1098 SM.getLocForStartOfFile(LPosInfo.first).getLocWithOffset(DisplayPos); 1099 1100 R.InsertTextBefore(Loc, os.str()); 1101 1102 // Now highlight the ranges. 1103 ArrayRef<SourceRange> Ranges = P.getRanges(); 1104 for (const auto &Range : Ranges) { 1105 // If we have already highlighted the range as a pop-up there is no work. 1106 if (llvm::is_contained(PopUpRanges, Range)) 1107 continue; 1108 1109 HighlightRange(R, LPosInfo.first, Range); 1110 } 1111 } 1112 1113 static void EmitAlphaCounter(raw_ostream &os, unsigned n) { 1114 unsigned x = n % ('z' - 'a'); 1115 n /= 'z' - 'a'; 1116 1117 if (n > 0) 1118 EmitAlphaCounter(os, n); 1119 1120 os << char('a' + x); 1121 } 1122 1123 unsigned HTMLDiagnostics::ProcessMacroPiece(raw_ostream &os, 1124 const PathDiagnosticMacroPiece& P, 1125 unsigned num) { 1126 for (const auto &subPiece : P.subPieces) { 1127 if (const auto *MP = dyn_cast<PathDiagnosticMacroPiece>(subPiece.get())) { 1128 num = ProcessMacroPiece(os, *MP, num); 1129 continue; 1130 } 1131 1132 if (const auto *EP = dyn_cast<PathDiagnosticEventPiece>(subPiece.get())) { 1133 os << "<div class=\"msg msgEvent\" style=\"width:94%; " 1134 "margin-left:5px\">" 1135 "<table class=\"msgT\"><tr>" 1136 "<td valign=\"top\"><div class=\"PathIndex PathIndexEvent\">"; 1137 EmitAlphaCounter(os, num++); 1138 os << "</div></td><td valign=\"top\">" 1139 << html::EscapeText(EP->getString()) 1140 << "</td></tr></table></div>\n"; 1141 } 1142 } 1143 1144 return num; 1145 } 1146 1147 void HTMLDiagnostics::addArrowSVGs(Rewriter &R, FileID BugFileID, 1148 const ArrowMap &ArrowIndices) { 1149 std::string S; 1150 llvm::raw_string_ostream OS(S); 1151 1152 OS << R"<<<( 1153 <style type="text/css"> 1154 svg { 1155 position:absolute; 1156 top:0; 1157 left:0; 1158 height:100%; 1159 width:100%; 1160 pointer-events: none; 1161 overflow: visible 1162 } 1163 .arrow { 1164 stroke-opacity: 0.2; 1165 stroke-width: 1; 1166 marker-end: url(#arrowhead); 1167 } 1168 1169 .arrow.selected { 1170 stroke-opacity: 0.6; 1171 stroke-width: 2; 1172 marker-end: url(#arrowheadSelected); 1173 } 1174 1175 .arrowhead { 1176 orient: auto; 1177 stroke: none; 1178 opacity: 0.6; 1179 fill: blue; 1180 } 1181 </style> 1182 <svg xmlns="http://www.w3.org/2000/svg"> 1183 <defs> 1184 <marker id="arrowheadSelected" class="arrowhead" opacity="0.6" 1185 viewBox="0 0 10 10" refX="3" refY="5" 1186 markerWidth="4" markerHeight="4"> 1187 <path d="M 0 0 L 10 5 L 0 10 z" /> 1188 </marker> 1189 <marker id="arrowhead" class="arrowhead" opacity="0.2" 1190 viewBox="0 0 10 10" refX="3" refY="5" 1191 markerWidth="4" markerHeight="4"> 1192 <path d="M 0 0 L 10 5 L 0 10 z" /> 1193 </marker> 1194 </defs> 1195 <g id="arrows" fill="none" stroke="blue" visibility="hidden"> 1196 )<<<"; 1197 1198 for (unsigned Index : llvm::seq(0u, ArrowIndices.getTotalNumberOfArrows())) { 1199 OS << " <path class=\"arrow\" id=\"arrow" << Index << "\"/>\n"; 1200 } 1201 1202 OS << R"<<<( 1203 </g> 1204 </svg> 1205 <script type='text/javascript'> 1206 const arrowIndices = )<<<"; 1207 1208 OS << ArrowIndices << "\n</script>\n"; 1209 1210 R.InsertTextBefore(R.getSourceMgr().getLocForStartOfFile(BugFileID), 1211 OS.str()); 1212 } 1213 1214 static std::string getSpanBeginForControl(const char *ClassName, 1215 unsigned Index) { 1216 std::string Result; 1217 llvm::raw_string_ostream OS(Result); 1218 OS << "<span id=\"" << ClassName << Index << "\">"; 1219 return Result; 1220 } 1221 1222 static std::string getSpanBeginForControlStart(unsigned Index) { 1223 return getSpanBeginForControl("start", Index); 1224 } 1225 1226 static std::string getSpanBeginForControlEnd(unsigned Index) { 1227 return getSpanBeginForControl("end", Index); 1228 } 1229 1230 unsigned HTMLDiagnostics::ProcessControlFlowPiece( 1231 Rewriter &R, FileID BugFileID, const PathDiagnosticControlFlowPiece &P, 1232 unsigned Number) { 1233 for (const PathDiagnosticLocationPair &LPair : P) { 1234 std::string Start = getSpanBeginForControlStart(Number), 1235 End = getSpanBeginForControlEnd(Number++); 1236 1237 HighlightRange(R, BugFileID, LPair.getStart().asRange().getBegin(), 1238 Start.c_str()); 1239 HighlightRange(R, BugFileID, LPair.getEnd().asRange().getBegin(), 1240 End.c_str()); 1241 } 1242 1243 return Number; 1244 } 1245 1246 void HTMLDiagnostics::HighlightRange(Rewriter& R, FileID BugFileID, 1247 SourceRange Range, 1248 const char *HighlightStart, 1249 const char *HighlightEnd) { 1250 SourceManager &SM = R.getSourceMgr(); 1251 const LangOptions &LangOpts = R.getLangOpts(); 1252 1253 SourceLocation InstantiationStart = SM.getExpansionLoc(Range.getBegin()); 1254 unsigned StartLineNo = SM.getExpansionLineNumber(InstantiationStart); 1255 1256 SourceLocation InstantiationEnd = SM.getExpansionLoc(Range.getEnd()); 1257 unsigned EndLineNo = SM.getExpansionLineNumber(InstantiationEnd); 1258 1259 if (EndLineNo < StartLineNo) 1260 return; 1261 1262 if (SM.getFileID(InstantiationStart) != BugFileID || 1263 SM.getFileID(InstantiationEnd) != BugFileID) 1264 return; 1265 1266 // Compute the column number of the end. 1267 unsigned EndColNo = SM.getExpansionColumnNumber(InstantiationEnd); 1268 unsigned OldEndColNo = EndColNo; 1269 1270 if (EndColNo) { 1271 // Add in the length of the token, so that we cover multi-char tokens. 1272 EndColNo += Lexer::MeasureTokenLength(Range.getEnd(), SM, LangOpts)-1; 1273 } 1274 1275 // Highlight the range. Make the span tag the outermost tag for the 1276 // selected range. 1277 1278 SourceLocation E = 1279 InstantiationEnd.getLocWithOffset(EndColNo - OldEndColNo); 1280 1281 html::HighlightRange(R, InstantiationStart, E, HighlightStart, HighlightEnd); 1282 } 1283 1284 StringRef HTMLDiagnostics::generateKeyboardNavigationJavascript() { 1285 return R"<<<( 1286 <script type='text/javascript'> 1287 var digitMatcher = new RegExp("[0-9]+"); 1288 1289 var querySelectorAllArray = function(selector) { 1290 return Array.prototype.slice.call( 1291 document.querySelectorAll(selector)); 1292 } 1293 1294 document.addEventListener("DOMContentLoaded", function() { 1295 querySelectorAllArray(".PathNav > a").forEach( 1296 function(currentValue, currentIndex) { 1297 var hrefValue = currentValue.getAttribute("href"); 1298 currentValue.onclick = function() { 1299 scrollTo(document.querySelector(hrefValue)); 1300 return false; 1301 }; 1302 }); 1303 }); 1304 1305 var findNum = function() { 1306 var s = document.querySelector(".msg.selected"); 1307 if (!s || s.id == "EndPath") { 1308 return 0; 1309 } 1310 var out = parseInt(digitMatcher.exec(s.id)[0]); 1311 return out; 1312 }; 1313 1314 var classListAdd = function(el, theClass) { 1315 if(!el.className.baseVal) 1316 el.className += " " + theClass; 1317 else 1318 el.className.baseVal += " " + theClass; 1319 }; 1320 1321 var classListRemove = function(el, theClass) { 1322 var className = (!el.className.baseVal) ? 1323 el.className : el.className.baseVal; 1324 className = className.replace(" " + theClass, ""); 1325 if(!el.className.baseVal) 1326 el.className = className; 1327 else 1328 el.className.baseVal = className; 1329 }; 1330 1331 var scrollTo = function(el) { 1332 querySelectorAllArray(".selected").forEach(function(s) { 1333 classListRemove(s, "selected"); 1334 }); 1335 classListAdd(el, "selected"); 1336 window.scrollBy(0, el.getBoundingClientRect().top - 1337 (window.innerHeight / 2)); 1338 highlightArrowsForSelectedEvent(); 1339 }; 1340 1341 var move = function(num, up, numItems) { 1342 if (num == 1 && up || num == numItems - 1 && !up) { 1343 return 0; 1344 } else if (num == 0 && up) { 1345 return numItems - 1; 1346 } else if (num == 0 && !up) { 1347 return 1 % numItems; 1348 } 1349 return up ? num - 1 : num + 1; 1350 } 1351 1352 var numToId = function(num) { 1353 if (num == 0) { 1354 return document.getElementById("EndPath") 1355 } 1356 return document.getElementById("Path" + num); 1357 }; 1358 1359 var navigateTo = function(up) { 1360 var numItems = document.querySelectorAll( 1361 ".line > .msgEvent, .line > .msgControl").length; 1362 var currentSelected = findNum(); 1363 var newSelected = move(currentSelected, up, numItems); 1364 var newEl = numToId(newSelected, numItems); 1365 1366 // Scroll element into center. 1367 scrollTo(newEl); 1368 }; 1369 1370 window.addEventListener("keydown", function (event) { 1371 if (event.defaultPrevented) { 1372 return; 1373 } 1374 // key 'j' 1375 if (event.keyCode == 74) { 1376 navigateTo(/*up=*/false); 1377 // key 'k' 1378 } else if (event.keyCode == 75) { 1379 navigateTo(/*up=*/true); 1380 } else { 1381 return; 1382 } 1383 event.preventDefault(); 1384 }, true); 1385 </script> 1386 )<<<"; 1387 } 1388 1389 StringRef HTMLDiagnostics::generateArrowDrawingJavascript() { 1390 return R"<<<( 1391 <script type='text/javascript'> 1392 // Return range of numbers from a range [lower, upper). 1393 function range(lower, upper) { 1394 var array = []; 1395 for (var i = lower; i <= upper; ++i) { 1396 array.push(i); 1397 } 1398 return array; 1399 } 1400 1401 var getRelatedArrowIndices = function(pathId) { 1402 // HTML numeration of events is a bit different than it is in the path. 1403 // Everything is rotated one step to the right, so the last element 1404 // (error diagnostic) has index 0. 1405 if (pathId == 0) { 1406 // arrowIndices has at least 2 elements 1407 pathId = arrowIndices.length - 1; 1408 } 1409 1410 return range(arrowIndices[pathId], arrowIndices[pathId - 1]); 1411 } 1412 1413 var highlightArrowsForSelectedEvent = function() { 1414 const selectedNum = findNum(); 1415 const arrowIndicesToHighlight = getRelatedArrowIndices(selectedNum); 1416 arrowIndicesToHighlight.forEach((index) => { 1417 var arrow = document.querySelector("#arrow" + index); 1418 if(arrow) { 1419 classListAdd(arrow, "selected") 1420 } 1421 }); 1422 } 1423 1424 var getAbsoluteBoundingRect = function(element) { 1425 const relative = element.getBoundingClientRect(); 1426 return { 1427 left: relative.left + window.pageXOffset, 1428 right: relative.right + window.pageXOffset, 1429 top: relative.top + window.pageYOffset, 1430 bottom: relative.bottom + window.pageYOffset, 1431 height: relative.height, 1432 width: relative.width 1433 }; 1434 } 1435 1436 var drawArrow = function(index) { 1437 // This function is based on the great answer from SO: 1438 // https://stackoverflow.com/a/39575674/11582326 1439 var start = document.querySelector("#start" + index); 1440 var end = document.querySelector("#end" + index); 1441 var arrow = document.querySelector("#arrow" + index); 1442 1443 var startRect = getAbsoluteBoundingRect(start); 1444 var endRect = getAbsoluteBoundingRect(end); 1445 1446 // It is an arrow from a token to itself, no need to visualize it. 1447 if (startRect.top == endRect.top && 1448 startRect.left == endRect.left) 1449 return; 1450 1451 // Each arrow is a very simple Bézier curve, with two nodes and 1452 // two handles. So, we need to calculate four points in the window: 1453 // * start node 1454 var posStart = { x: 0, y: 0 }; 1455 // * end node 1456 var posEnd = { x: 0, y: 0 }; 1457 // * handle for the start node 1458 var startHandle = { x: 0, y: 0 }; 1459 // * handle for the end node 1460 var endHandle = { x: 0, y: 0 }; 1461 // One can visualize it as follows: 1462 // 1463 // start handle 1464 // / 1465 // X"""_.-""""X 1466 // .' \ 1467 // / start node 1468 // | 1469 // | 1470 // | end node 1471 // \ / 1472 // `->X 1473 // X-' 1474 // \ 1475 // end handle 1476 // 1477 // NOTE: (0, 0) is the top left corner of the window. 1478 1479 // We have 3 similar, but still different scenarios to cover: 1480 // 1481 // 1. Two tokens on different lines. 1482 // -xxx 1483 // / 1484 // \ 1485 // -> xxx 1486 // In this situation, we draw arrow on the left curving to the left. 1487 // 2. Two tokens on the same line, and the destination is on the right. 1488 // ____ 1489 // / \ 1490 // / V 1491 // xxx xxx 1492 // In this situation, we draw arrow above curving upwards. 1493 // 3. Two tokens on the same line, and the destination is on the left. 1494 // xxx xxx 1495 // ^ / 1496 // \____/ 1497 // In this situation, we draw arrow below curving downwards. 1498 const onDifferentLines = startRect.top <= endRect.top - 5 || 1499 startRect.top >= endRect.top + 5; 1500 const leftToRight = startRect.left < endRect.left; 1501 1502 // NOTE: various magic constants are chosen empirically for 1503 // better positioning and look 1504 if (onDifferentLines) { 1505 // Case #1 1506 const topToBottom = startRect.top < endRect.top; 1507 posStart.x = startRect.left - 1; 1508 // We don't want to start it at the top left corner of the token, 1509 // it doesn't feel like this is where the arrow comes from. 1510 // For this reason, we start it in the middle of the left side 1511 // of the token. 1512 posStart.y = startRect.top + startRect.height / 2; 1513 1514 // End node has arrow head and we give it a bit more space. 1515 posEnd.x = endRect.left - 4; 1516 posEnd.y = endRect.top; 1517 1518 // Utility object with x and y offsets for handles. 1519 var curvature = { 1520 // We want bottom-to-top arrow to curve a bit more, so it doesn't 1521 // overlap much with top-to-bottom curves (much more frequent). 1522 x: topToBottom ? 15 : 25, 1523 y: Math.min((posEnd.y - posStart.y) / 3, 10) 1524 } 1525 1526 // When destination is on the different line, we can make a 1527 // curvier arrow because we have space for it. 1528 // So, instead of using 1529 // 1530 // startHandle.x = posStart.x - curvature.x 1531 // endHandle.x = posEnd.x - curvature.x 1532 // 1533 // We use the leftmost of these two values for both handles. 1534 startHandle.x = Math.min(posStart.x, posEnd.x) - curvature.x; 1535 endHandle.x = startHandle.x; 1536 1537 // Curving downwards from the start node... 1538 startHandle.y = posStart.y + curvature.y; 1539 // ... and upwards from the end node. 1540 endHandle.y = posEnd.y - curvature.y; 1541 1542 } else if (leftToRight) { 1543 // Case #2 1544 // Starting from the top right corner... 1545 posStart.x = startRect.right - 1; 1546 posStart.y = startRect.top; 1547 1548 // ...and ending at the top left corner of the end token. 1549 posEnd.x = endRect.left + 1; 1550 posEnd.y = endRect.top - 1; 1551 1552 // Utility object with x and y offsets for handles. 1553 var curvature = { 1554 x: Math.min((posEnd.x - posStart.x) / 3, 15), 1555 y: 5 1556 } 1557 1558 // Curving to the right... 1559 startHandle.x = posStart.x + curvature.x; 1560 // ... and upwards from the start node. 1561 startHandle.y = posStart.y - curvature.y; 1562 1563 // And to the left... 1564 endHandle.x = posEnd.x - curvature.x; 1565 // ... and upwards from the end node. 1566 endHandle.y = posEnd.y - curvature.y; 1567 1568 } else { 1569 // Case #3 1570 // Starting from the bottom right corner... 1571 posStart.x = startRect.right; 1572 posStart.y = startRect.bottom; 1573 1574 // ...and ending also at the bottom right corner, but of the end token. 1575 posEnd.x = endRect.right - 1; 1576 posEnd.y = endRect.bottom + 1; 1577 1578 // Utility object with x and y offsets for handles. 1579 var curvature = { 1580 x: Math.min((posStart.x - posEnd.x) / 3, 15), 1581 y: 5 1582 } 1583 1584 // Curving to the left... 1585 startHandle.x = posStart.x - curvature.x; 1586 // ... and downwards from the start node. 1587 startHandle.y = posStart.y + curvature.y; 1588 1589 // And to the right... 1590 endHandle.x = posEnd.x + curvature.x; 1591 // ... and downwards from the end node. 1592 endHandle.y = posEnd.y + curvature.y; 1593 } 1594 1595 // Put it all together into a path. 1596 // More information on the format: 1597 // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths 1598 var pathStr = "M" + posStart.x + "," + posStart.y + " " + 1599 "C" + startHandle.x + "," + startHandle.y + " " + 1600 endHandle.x + "," + endHandle.y + " " + 1601 posEnd.x + "," + posEnd.y; 1602 1603 arrow.setAttribute("d", pathStr); 1604 }; 1605 1606 var drawArrows = function() { 1607 const numOfArrows = document.querySelectorAll("path[id^=arrow]").length; 1608 for (var i = 0; i < numOfArrows; ++i) { 1609 drawArrow(i); 1610 } 1611 } 1612 1613 var toggleArrows = function(event) { 1614 const arrows = document.querySelector("#arrows"); 1615 if (event.target.checked) { 1616 arrows.setAttribute("visibility", "visible"); 1617 } else { 1618 arrows.setAttribute("visibility", "hidden"); 1619 } 1620 } 1621 1622 window.addEventListener("resize", drawArrows); 1623 document.addEventListener("DOMContentLoaded", function() { 1624 // Whenever we show invocation, locations change, i.e. we 1625 // need to redraw arrows. 1626 document 1627 .querySelector('input[id="showinvocation"]') 1628 .addEventListener("click", drawArrows); 1629 // Hiding irrelevant lines also should cause arrow rerender. 1630 document 1631 .querySelector('input[name="showCounterexample"]') 1632 .addEventListener("change", drawArrows); 1633 document 1634 .querySelector('input[name="showArrows"]') 1635 .addEventListener("change", toggleArrows); 1636 drawArrows(); 1637 // Default highlighting for the last event. 1638 highlightArrowsForSelectedEvent(); 1639 }); 1640 </script> 1641 )<<<"; 1642 } 1643