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 73 public: 74 HTMLDiagnostics(PathDiagnosticConsumerOptions DiagOpts, 75 const std::string &OutputDir, const Preprocessor &pp, 76 bool supportsMultipleFiles) 77 : DiagOpts(std::move(DiagOpts)), Directory(OutputDir), PP(pp), 78 SupportsCrossFileDiagnostics(supportsMultipleFiles) {} 79 80 ~HTMLDiagnostics() override { FlushDiagnostics(nullptr); } 81 82 void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags, 83 FilesMade *filesMade) override; 84 85 StringRef getName() const override { return "HTMLDiagnostics"; } 86 87 bool supportsCrossFileDiagnostics() const override { 88 return SupportsCrossFileDiagnostics; 89 } 90 91 unsigned ProcessMacroPiece(raw_ostream &os, const PathDiagnosticMacroPiece &P, 92 unsigned num); 93 94 unsigned ProcessControlFlowPiece(Rewriter &R, FileID BugFileID, 95 const PathDiagnosticControlFlowPiece &P, 96 unsigned Number); 97 98 void HandlePiece(Rewriter &R, FileID BugFileID, const PathDiagnosticPiece &P, 99 const std::vector<SourceRange> &PopUpRanges, unsigned num, 100 unsigned max); 101 102 void HighlightRange(Rewriter &R, FileID BugFileID, SourceRange Range, 103 const char *HighlightStart = "<span class=\"mrange\">", 104 const char *HighlightEnd = "</span>"); 105 106 void ReportDiag(const PathDiagnostic &D, FilesMade *filesMade); 107 108 // Generate the full HTML report 109 std::string GenerateHTML(const PathDiagnostic &D, Rewriter &R, 110 const SourceManager &SMgr, const PathPieces &path, 111 const char *declName); 112 113 // Add HTML header/footers to file specified by FID 114 void FinalizeHTML(const PathDiagnostic &D, Rewriter &R, 115 const SourceManager &SMgr, const PathPieces &path, 116 FileID FID, FileEntryRef Entry, const char *declName); 117 118 // Rewrite the file specified by FID with HTML formatting. 119 void RewriteFile(Rewriter &R, const PathPieces &path, FileID FID); 120 121 PathGenerationScheme getGenerationScheme() const override { 122 return Everything; 123 } 124 125 private: 126 void addArrowSVGs(Rewriter &R, FileID BugFileID, 127 const ArrowMap &ArrowIndices); 128 129 /// \return Javascript for displaying shortcuts help; 130 StringRef showHelpJavascript(); 131 132 /// \return Javascript for navigating the HTML report using j/k keys. 133 StringRef generateKeyboardNavigationJavascript(); 134 135 /// \return Javascript for drawing control-flow arrows. 136 StringRef generateArrowDrawingJavascript(); 137 138 /// \return JavaScript for an option to only show relevant lines. 139 std::string showRelevantLinesJavascript(const PathDiagnostic &D, 140 const PathPieces &path); 141 142 /// Write executed lines from \p D in JSON format into \p os. 143 void dumpCoverageData(const PathDiagnostic &D, const PathPieces &path, 144 llvm::raw_string_ostream &os); 145 }; 146 147 bool isArrowPiece(const PathDiagnosticPiece &P) { 148 return isa<PathDiagnosticControlFlowPiece>(P) && P.getString().empty(); 149 } 150 151 unsigned getPathSizeWithoutArrows(const PathPieces &Path) { 152 unsigned TotalPieces = Path.size(); 153 unsigned TotalArrowPieces = llvm::count_if( 154 Path, [](const PathDiagnosticPieceRef &P) { return isArrowPiece(*P); }); 155 return TotalPieces - TotalArrowPieces; 156 } 157 158 class ArrowMap : public std::vector<unsigned> { 159 using Base = std::vector<unsigned>; 160 161 public: 162 ArrowMap(unsigned Size) : Base(Size, 0) {} 163 unsigned getTotalNumberOfArrows() const { return at(0); } 164 }; 165 166 llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const ArrowMap &Indices) { 167 OS << "[ "; 168 llvm::interleave(Indices, OS, ","); 169 return OS << " ]"; 170 } 171 172 } // namespace 173 174 void ento::createHTMLDiagnosticConsumer( 175 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 176 const std::string &OutputDir, const Preprocessor &PP, 177 const cross_tu::CrossTranslationUnitContext &CTU, 178 const MacroExpansionContext &MacroExpansions) { 179 180 // FIXME: HTML is currently our default output type, but if the output 181 // directory isn't specified, it acts like if it was in the minimal text 182 // output mode. This doesn't make much sense, we should have the minimal text 183 // as our default. In the case of backward compatibility concerns, this could 184 // be preserved with -analyzer-config-compatibility-mode=true. 185 createTextMinimalPathDiagnosticConsumer(DiagOpts, C, OutputDir, PP, CTU, 186 MacroExpansions); 187 188 // TODO: Emit an error here. 189 if (OutputDir.empty()) 190 return; 191 192 C.push_back(new HTMLDiagnostics(std::move(DiagOpts), OutputDir, PP, true)); 193 } 194 195 void ento::createHTMLSingleFileDiagnosticConsumer( 196 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 197 const std::string &OutputDir, const Preprocessor &PP, 198 const cross_tu::CrossTranslationUnitContext &CTU, 199 const clang::MacroExpansionContext &MacroExpansions) { 200 createTextMinimalPathDiagnosticConsumer(DiagOpts, C, OutputDir, PP, CTU, 201 MacroExpansions); 202 203 // TODO: Emit an error here. 204 if (OutputDir.empty()) 205 return; 206 207 C.push_back(new HTMLDiagnostics(std::move(DiagOpts), OutputDir, PP, false)); 208 } 209 210 void ento::createPlistHTMLDiagnosticConsumer( 211 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 212 const std::string &prefix, const Preprocessor &PP, 213 const cross_tu::CrossTranslationUnitContext &CTU, 214 const MacroExpansionContext &MacroExpansions) { 215 createHTMLDiagnosticConsumer( 216 DiagOpts, C, std::string(llvm::sys::path::parent_path(prefix)), PP, CTU, 217 MacroExpansions); 218 createPlistMultiFileDiagnosticConsumer(DiagOpts, C, prefix, PP, CTU, 219 MacroExpansions); 220 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, prefix, PP, 221 CTU, MacroExpansions); 222 } 223 224 void ento::createSarifHTMLDiagnosticConsumer( 225 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C, 226 const std::string &sarif_file, const Preprocessor &PP, 227 const cross_tu::CrossTranslationUnitContext &CTU, 228 const MacroExpansionContext &MacroExpansions) { 229 createHTMLDiagnosticConsumer( 230 DiagOpts, C, std::string(llvm::sys::path::parent_path(sarif_file)), PP, 231 CTU, MacroExpansions); 232 createSarifDiagnosticConsumer(DiagOpts, C, sarif_file, PP, CTU, 233 MacroExpansions); 234 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, sarif_file, 235 PP, CTU, MacroExpansions); 236 } 237 238 //===----------------------------------------------------------------------===// 239 // Report processing. 240 //===----------------------------------------------------------------------===// 241 242 void HTMLDiagnostics::FlushDiagnosticsImpl( 243 std::vector<const PathDiagnostic *> &Diags, 244 FilesMade *filesMade) { 245 for (const auto Diag : Diags) 246 ReportDiag(*Diag, filesMade); 247 } 248 249 static llvm::SmallString<32> getIssueHash(const PathDiagnostic &D, 250 const Preprocessor &PP) { 251 SourceManager &SMgr = PP.getSourceManager(); 252 PathDiagnosticLocation UPDLoc = D.getUniqueingLoc(); 253 FullSourceLoc L(SMgr.getExpansionLoc(UPDLoc.isValid() 254 ? UPDLoc.asLocation() 255 : D.getLocation().asLocation()), 256 SMgr); 257 return getIssueHash(L, D.getCheckerName(), D.getBugType(), 258 D.getDeclWithIssue(), PP.getLangOpts()); 259 } 260 261 void HTMLDiagnostics::ReportDiag(const PathDiagnostic& D, 262 FilesMade *filesMade) { 263 // Create the HTML directory if it is missing. 264 if (!createdDir) { 265 createdDir = true; 266 if (std::error_code ec = llvm::sys::fs::create_directories(Directory)) { 267 llvm::errs() << "warning: could not create directory '" 268 << Directory << "': " << ec.message() << '\n'; 269 noDir = true; 270 return; 271 } 272 } 273 274 if (noDir) 275 return; 276 277 // First flatten out the entire path to make it easier to use. 278 PathPieces path = D.path.flatten(/*ShouldFlattenMacros=*/false); 279 280 // The path as already been prechecked that the path is non-empty. 281 assert(!path.empty()); 282 const SourceManager &SMgr = path.front()->getLocation().getManager(); 283 284 // Create a new rewriter to generate HTML. 285 Rewriter R(const_cast<SourceManager&>(SMgr), PP.getLangOpts()); 286 287 // Get the function/method name 288 SmallString<128> declName("unknown"); 289 int offsetDecl = 0; 290 if (const Decl *DeclWithIssue = D.getDeclWithIssue()) { 291 if (const auto *ND = dyn_cast<NamedDecl>(DeclWithIssue)) 292 declName = ND->getDeclName().getAsString(); 293 294 if (const Stmt *Body = DeclWithIssue->getBody()) { 295 // Retrieve the relative position of the declaration which will be used 296 // for the file name 297 FullSourceLoc L( 298 SMgr.getExpansionLoc(path.back()->getLocation().asLocation()), 299 SMgr); 300 FullSourceLoc FunL(SMgr.getExpansionLoc(Body->getBeginLoc()), SMgr); 301 offsetDecl = L.getExpansionLineNumber() - FunL.getExpansionLineNumber(); 302 } 303 } 304 305 SmallString<32> IssueHash = getIssueHash(D, PP); 306 auto [It, IsNew] = EmittedHashes.insert(IssueHash); 307 if (!IsNew) { 308 // We've already emitted a duplicate issue. It'll get overwritten anyway. 309 return; 310 } 311 312 // FIXME: This causes each file to be re-parsed and syntax-highlighted 313 // and macro-expanded separately for each report. We could cache such rewrites 314 // across all reports and only re-do the part that's actually different: 315 // the warning/note bubbles. 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); 886 html::HighlightMacros(R, FID, PP); 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 std::string getSpanBeginForControl(const char *ClassName, unsigned Index) { 1215 std::string Result; 1216 llvm::raw_string_ostream OS(Result); 1217 OS << "<span id=\"" << ClassName << Index << "\">"; 1218 return Result; 1219 } 1220 1221 std::string getSpanBeginForControlStart(unsigned Index) { 1222 return getSpanBeginForControl("start", Index); 1223 } 1224 1225 std::string getSpanBeginForControlEnd(unsigned Index) { 1226 return getSpanBeginForControl("end", Index); 1227 } 1228 1229 unsigned HTMLDiagnostics::ProcessControlFlowPiece( 1230 Rewriter &R, FileID BugFileID, const PathDiagnosticControlFlowPiece &P, 1231 unsigned Number) { 1232 for (const PathDiagnosticLocationPair &LPair : P) { 1233 std::string Start = getSpanBeginForControlStart(Number), 1234 End = getSpanBeginForControlEnd(Number++); 1235 1236 HighlightRange(R, BugFileID, LPair.getStart().asRange().getBegin(), 1237 Start.c_str()); 1238 HighlightRange(R, BugFileID, LPair.getEnd().asRange().getBegin(), 1239 End.c_str()); 1240 } 1241 1242 return Number; 1243 } 1244 1245 void HTMLDiagnostics::HighlightRange(Rewriter& R, FileID BugFileID, 1246 SourceRange Range, 1247 const char *HighlightStart, 1248 const char *HighlightEnd) { 1249 SourceManager &SM = R.getSourceMgr(); 1250 const LangOptions &LangOpts = R.getLangOpts(); 1251 1252 SourceLocation InstantiationStart = SM.getExpansionLoc(Range.getBegin()); 1253 unsigned StartLineNo = SM.getExpansionLineNumber(InstantiationStart); 1254 1255 SourceLocation InstantiationEnd = SM.getExpansionLoc(Range.getEnd()); 1256 unsigned EndLineNo = SM.getExpansionLineNumber(InstantiationEnd); 1257 1258 if (EndLineNo < StartLineNo) 1259 return; 1260 1261 if (SM.getFileID(InstantiationStart) != BugFileID || 1262 SM.getFileID(InstantiationEnd) != BugFileID) 1263 return; 1264 1265 // Compute the column number of the end. 1266 unsigned EndColNo = SM.getExpansionColumnNumber(InstantiationEnd); 1267 unsigned OldEndColNo = EndColNo; 1268 1269 if (EndColNo) { 1270 // Add in the length of the token, so that we cover multi-char tokens. 1271 EndColNo += Lexer::MeasureTokenLength(Range.getEnd(), SM, LangOpts)-1; 1272 } 1273 1274 // Highlight the range. Make the span tag the outermost tag for the 1275 // selected range. 1276 1277 SourceLocation E = 1278 InstantiationEnd.getLocWithOffset(EndColNo - OldEndColNo); 1279 1280 html::HighlightRange(R, InstantiationStart, E, HighlightStart, HighlightEnd); 1281 } 1282 1283 StringRef HTMLDiagnostics::generateKeyboardNavigationJavascript() { 1284 return R"<<<( 1285 <script type='text/javascript'> 1286 var digitMatcher = new RegExp("[0-9]+"); 1287 1288 var querySelectorAllArray = function(selector) { 1289 return Array.prototype.slice.call( 1290 document.querySelectorAll(selector)); 1291 } 1292 1293 document.addEventListener("DOMContentLoaded", function() { 1294 querySelectorAllArray(".PathNav > a").forEach( 1295 function(currentValue, currentIndex) { 1296 var hrefValue = currentValue.getAttribute("href"); 1297 currentValue.onclick = function() { 1298 scrollTo(document.querySelector(hrefValue)); 1299 return false; 1300 }; 1301 }); 1302 }); 1303 1304 var findNum = function() { 1305 var s = document.querySelector(".msg.selected"); 1306 if (!s || s.id == "EndPath") { 1307 return 0; 1308 } 1309 var out = parseInt(digitMatcher.exec(s.id)[0]); 1310 return out; 1311 }; 1312 1313 var classListAdd = function(el, theClass) { 1314 if(!el.className.baseVal) 1315 el.className += " " + theClass; 1316 else 1317 el.className.baseVal += " " + theClass; 1318 }; 1319 1320 var classListRemove = function(el, theClass) { 1321 var className = (!el.className.baseVal) ? 1322 el.className : el.className.baseVal; 1323 className = className.replace(" " + theClass, ""); 1324 if(!el.className.baseVal) 1325 el.className = className; 1326 else 1327 el.className.baseVal = className; 1328 }; 1329 1330 var scrollTo = function(el) { 1331 querySelectorAllArray(".selected").forEach(function(s) { 1332 classListRemove(s, "selected"); 1333 }); 1334 classListAdd(el, "selected"); 1335 window.scrollBy(0, el.getBoundingClientRect().top - 1336 (window.innerHeight / 2)); 1337 highlightArrowsForSelectedEvent(); 1338 }; 1339 1340 var move = function(num, up, numItems) { 1341 if (num == 1 && up || num == numItems - 1 && !up) { 1342 return 0; 1343 } else if (num == 0 && up) { 1344 return numItems - 1; 1345 } else if (num == 0 && !up) { 1346 return 1 % numItems; 1347 } 1348 return up ? num - 1 : num + 1; 1349 } 1350 1351 var numToId = function(num) { 1352 if (num == 0) { 1353 return document.getElementById("EndPath") 1354 } 1355 return document.getElementById("Path" + num); 1356 }; 1357 1358 var navigateTo = function(up) { 1359 var numItems = document.querySelectorAll( 1360 ".line > .msgEvent, .line > .msgControl").length; 1361 var currentSelected = findNum(); 1362 var newSelected = move(currentSelected, up, numItems); 1363 var newEl = numToId(newSelected, numItems); 1364 1365 // Scroll element into center. 1366 scrollTo(newEl); 1367 }; 1368 1369 window.addEventListener("keydown", function (event) { 1370 if (event.defaultPrevented) { 1371 return; 1372 } 1373 // key 'j' 1374 if (event.keyCode == 74) { 1375 navigateTo(/*up=*/false); 1376 // key 'k' 1377 } else if (event.keyCode == 75) { 1378 navigateTo(/*up=*/true); 1379 } else { 1380 return; 1381 } 1382 event.preventDefault(); 1383 }, true); 1384 </script> 1385 )<<<"; 1386 } 1387 1388 StringRef HTMLDiagnostics::generateArrowDrawingJavascript() { 1389 return R"<<<( 1390 <script type='text/javascript'> 1391 // Return range of numbers from a range [lower, upper). 1392 function range(lower, upper) { 1393 var array = []; 1394 for (var i = lower; i <= upper; ++i) { 1395 array.push(i); 1396 } 1397 return array; 1398 } 1399 1400 var getRelatedArrowIndices = function(pathId) { 1401 // HTML numeration of events is a bit different than it is in the path. 1402 // Everything is rotated one step to the right, so the last element 1403 // (error diagnostic) has index 0. 1404 if (pathId == 0) { 1405 // arrowIndices has at least 2 elements 1406 pathId = arrowIndices.length - 1; 1407 } 1408 1409 return range(arrowIndices[pathId], arrowIndices[pathId - 1]); 1410 } 1411 1412 var highlightArrowsForSelectedEvent = function() { 1413 const selectedNum = findNum(); 1414 const arrowIndicesToHighlight = getRelatedArrowIndices(selectedNum); 1415 arrowIndicesToHighlight.forEach((index) => { 1416 var arrow = document.querySelector("#arrow" + index); 1417 if(arrow) { 1418 classListAdd(arrow, "selected") 1419 } 1420 }); 1421 } 1422 1423 var getAbsoluteBoundingRect = function(element) { 1424 const relative = element.getBoundingClientRect(); 1425 return { 1426 left: relative.left + window.pageXOffset, 1427 right: relative.right + window.pageXOffset, 1428 top: relative.top + window.pageYOffset, 1429 bottom: relative.bottom + window.pageYOffset, 1430 height: relative.height, 1431 width: relative.width 1432 }; 1433 } 1434 1435 var drawArrow = function(index) { 1436 // This function is based on the great answer from SO: 1437 // https://stackoverflow.com/a/39575674/11582326 1438 var start = document.querySelector("#start" + index); 1439 var end = document.querySelector("#end" + index); 1440 var arrow = document.querySelector("#arrow" + index); 1441 1442 var startRect = getAbsoluteBoundingRect(start); 1443 var endRect = getAbsoluteBoundingRect(end); 1444 1445 // It is an arrow from a token to itself, no need to visualize it. 1446 if (startRect.top == endRect.top && 1447 startRect.left == endRect.left) 1448 return; 1449 1450 // Each arrow is a very simple Bézier curve, with two nodes and 1451 // two handles. So, we need to calculate four points in the window: 1452 // * start node 1453 var posStart = { x: 0, y: 0 }; 1454 // * end node 1455 var posEnd = { x: 0, y: 0 }; 1456 // * handle for the start node 1457 var startHandle = { x: 0, y: 0 }; 1458 // * handle for the end node 1459 var endHandle = { x: 0, y: 0 }; 1460 // One can visualize it as follows: 1461 // 1462 // start handle 1463 // / 1464 // X"""_.-""""X 1465 // .' \ 1466 // / start node 1467 // | 1468 // | 1469 // | end node 1470 // \ / 1471 // `->X 1472 // X-' 1473 // \ 1474 // end handle 1475 // 1476 // NOTE: (0, 0) is the top left corner of the window. 1477 1478 // We have 3 similar, but still different scenarios to cover: 1479 // 1480 // 1. Two tokens on different lines. 1481 // -xxx 1482 // / 1483 // \ 1484 // -> xxx 1485 // In this situation, we draw arrow on the left curving to the left. 1486 // 2. Two tokens on the same line, and the destination is on the right. 1487 // ____ 1488 // / \ 1489 // / V 1490 // xxx xxx 1491 // In this situation, we draw arrow above curving upwards. 1492 // 3. Two tokens on the same line, and the destination is on the left. 1493 // xxx xxx 1494 // ^ / 1495 // \____/ 1496 // In this situation, we draw arrow below curving downwards. 1497 const onDifferentLines = startRect.top <= endRect.top - 5 || 1498 startRect.top >= endRect.top + 5; 1499 const leftToRight = startRect.left < endRect.left; 1500 1501 // NOTE: various magic constants are chosen empirically for 1502 // better positioning and look 1503 if (onDifferentLines) { 1504 // Case #1 1505 const topToBottom = startRect.top < endRect.top; 1506 posStart.x = startRect.left - 1; 1507 // We don't want to start it at the top left corner of the token, 1508 // it doesn't feel like this is where the arrow comes from. 1509 // For this reason, we start it in the middle of the left side 1510 // of the token. 1511 posStart.y = startRect.top + startRect.height / 2; 1512 1513 // End node has arrow head and we give it a bit more space. 1514 posEnd.x = endRect.left - 4; 1515 posEnd.y = endRect.top; 1516 1517 // Utility object with x and y offsets for handles. 1518 var curvature = { 1519 // We want bottom-to-top arrow to curve a bit more, so it doesn't 1520 // overlap much with top-to-bottom curves (much more frequent). 1521 x: topToBottom ? 15 : 25, 1522 y: Math.min((posEnd.y - posStart.y) / 3, 10) 1523 } 1524 1525 // When destination is on the different line, we can make a 1526 // curvier arrow because we have space for it. 1527 // So, instead of using 1528 // 1529 // startHandle.x = posStart.x - curvature.x 1530 // endHandle.x = posEnd.x - curvature.x 1531 // 1532 // We use the leftmost of these two values for both handles. 1533 startHandle.x = Math.min(posStart.x, posEnd.x) - curvature.x; 1534 endHandle.x = startHandle.x; 1535 1536 // Curving downwards from the start node... 1537 startHandle.y = posStart.y + curvature.y; 1538 // ... and upwards from the end node. 1539 endHandle.y = posEnd.y - curvature.y; 1540 1541 } else if (leftToRight) { 1542 // Case #2 1543 // Starting from the top right corner... 1544 posStart.x = startRect.right - 1; 1545 posStart.y = startRect.top; 1546 1547 // ...and ending at the top left corner of the end token. 1548 posEnd.x = endRect.left + 1; 1549 posEnd.y = endRect.top - 1; 1550 1551 // Utility object with x and y offsets for handles. 1552 var curvature = { 1553 x: Math.min((posEnd.x - posStart.x) / 3, 15), 1554 y: 5 1555 } 1556 1557 // Curving to the right... 1558 startHandle.x = posStart.x + curvature.x; 1559 // ... and upwards from the start node. 1560 startHandle.y = posStart.y - curvature.y; 1561 1562 // And to the left... 1563 endHandle.x = posEnd.x - curvature.x; 1564 // ... and upwards from the end node. 1565 endHandle.y = posEnd.y - curvature.y; 1566 1567 } else { 1568 // Case #3 1569 // Starting from the bottom right corner... 1570 posStart.x = startRect.right; 1571 posStart.y = startRect.bottom; 1572 1573 // ...and ending also at the bottom right corner, but of the end token. 1574 posEnd.x = endRect.right - 1; 1575 posEnd.y = endRect.bottom + 1; 1576 1577 // Utility object with x and y offsets for handles. 1578 var curvature = { 1579 x: Math.min((posStart.x - posEnd.x) / 3, 15), 1580 y: 5 1581 } 1582 1583 // Curving to the left... 1584 startHandle.x = posStart.x - curvature.x; 1585 // ... and downwards from the start node. 1586 startHandle.y = posStart.y + curvature.y; 1587 1588 // And to the right... 1589 endHandle.x = posEnd.x + curvature.x; 1590 // ... and downwards from the end node. 1591 endHandle.y = posEnd.y + curvature.y; 1592 } 1593 1594 // Put it all together into a path. 1595 // More information on the format: 1596 // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths 1597 var pathStr = "M" + posStart.x + "," + posStart.y + " " + 1598 "C" + startHandle.x + "," + startHandle.y + " " + 1599 endHandle.x + "," + endHandle.y + " " + 1600 posEnd.x + "," + posEnd.y; 1601 1602 arrow.setAttribute("d", pathStr); 1603 }; 1604 1605 var drawArrows = function() { 1606 const numOfArrows = document.querySelectorAll("path[id^=arrow]").length; 1607 for (var i = 0; i < numOfArrows; ++i) { 1608 drawArrow(i); 1609 } 1610 } 1611 1612 var toggleArrows = function(event) { 1613 const arrows = document.querySelector("#arrows"); 1614 if (event.target.checked) { 1615 arrows.setAttribute("visibility", "visible"); 1616 } else { 1617 arrows.setAttribute("visibility", "hidden"); 1618 } 1619 } 1620 1621 window.addEventListener("resize", drawArrows); 1622 document.addEventListener("DOMContentLoaded", function() { 1623 // Whenever we show invocation, locations change, i.e. we 1624 // need to redraw arrows. 1625 document 1626 .querySelector('input[id="showinvocation"]') 1627 .addEventListener("click", drawArrows); 1628 // Hiding irrelevant lines also should cause arrow rerender. 1629 document 1630 .querySelector('input[name="showCounterexample"]') 1631 .addEventListener("change", drawArrows); 1632 document 1633 .querySelector('input[name="showArrows"]') 1634 .addEventListener("change", toggleArrows); 1635 drawArrows(); 1636 // Default highlighting for the last event. 1637 highlightArrowsForSelectedEvent(); 1638 }); 1639 </script> 1640 )<<<"; 1641 } 1642