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