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