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