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