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            << "\">&#x2190;</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            << "\">&#x2192;</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          << ")\">&#x2190;</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         << ")\">&#x2192;</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            << ")\">&#x2192;</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