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