1 //===- HTMLDiagnostics.cpp - HTML Diagnostics for Paths -------------------===//
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
7 //===----------------------------------------------------------------------===//
9 // This file defines the HTMLDiagnostics object.
11 //===----------------------------------------------------------------------===//
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/RewriteBuffer.h"
31 #include "llvm/ADT/STLExtras.h"
32 #include "llvm/ADT/Sequence.h"
33 #include "llvm/ADT/SmallString.h"
34 #include "llvm/ADT/StringRef.h"
35 #include "llvm/ADT/iterator_range.h"
36 #include "llvm/Support/Casting.h"
37 #include "llvm/Support/Errc.h"
38 #include "llvm/Support/ErrorHandling.h"
39 #include "llvm/Support/FileSystem.h"
40 #include "llvm/Support/MemoryBuffer.h"
41 #include "llvm/Support/Path.h"
42 #include "llvm/Support/raw_ostream.h"
50 #include <system_error>
54 using namespace clang
;
56 using llvm::RewriteBuffer
;
58 //===----------------------------------------------------------------------===//
60 //===----------------------------------------------------------------------===//
66 class HTMLDiagnostics
: public PathDiagnosticConsumer
{
67 PathDiagnosticConsumerOptions DiagOpts
;
68 std::string Directory
;
69 bool createdDir
= false;
71 const Preprocessor
&PP
;
72 const bool SupportsCrossFileDiagnostics
;
73 llvm::StringSet
<> EmittedHashes
;
74 html::RelexRewriteCacheRef RewriterCache
=
75 html::instantiateRelexRewriteCache();
78 HTMLDiagnostics(PathDiagnosticConsumerOptions DiagOpts
,
79 const std::string
&OutputDir
, const Preprocessor
&pp
,
80 bool supportsMultipleFiles
)
81 : DiagOpts(std::move(DiagOpts
)), Directory(OutputDir
), PP(pp
),
82 SupportsCrossFileDiagnostics(supportsMultipleFiles
) {}
84 ~HTMLDiagnostics() override
{ FlushDiagnostics(nullptr); }
86 void FlushDiagnosticsImpl(std::vector
<const PathDiagnostic
*> &Diags
,
87 FilesMade
*filesMade
) override
;
89 StringRef
getName() const override
{ return "HTMLDiagnostics"; }
91 bool supportsCrossFileDiagnostics() const override
{
92 return SupportsCrossFileDiagnostics
;
95 unsigned ProcessMacroPiece(raw_ostream
&os
, const PathDiagnosticMacroPiece
&P
,
98 unsigned ProcessControlFlowPiece(Rewriter
&R
, FileID BugFileID
,
99 const PathDiagnosticControlFlowPiece
&P
,
102 void HandlePiece(Rewriter
&R
, FileID BugFileID
, const PathDiagnosticPiece
&P
,
103 const std::vector
<SourceRange
> &PopUpRanges
, unsigned num
,
106 void HighlightRange(Rewriter
&R
, FileID BugFileID
, SourceRange Range
,
107 const char *HighlightStart
= "<span class=\"mrange\">",
108 const char *HighlightEnd
= "</span>");
110 void ReportDiag(const PathDiagnostic
&D
, FilesMade
*filesMade
);
112 // Generate the full HTML report
113 std::string
GenerateHTML(const PathDiagnostic
&D
, Rewriter
&R
,
114 const SourceManager
&SMgr
, const PathPieces
&path
,
115 const char *declName
);
117 // Add HTML header/footers to file specified by FID
118 void FinalizeHTML(const PathDiagnostic
&D
, Rewriter
&R
,
119 const SourceManager
&SMgr
, const PathPieces
&path
,
120 FileID FID
, FileEntryRef Entry
, const char *declName
);
122 // Rewrite the file specified by FID with HTML formatting.
123 void RewriteFile(Rewriter
&R
, const PathPieces
&path
, FileID FID
);
125 PathGenerationScheme
getGenerationScheme() const override
{
130 void addArrowSVGs(Rewriter
&R
, FileID BugFileID
,
131 const ArrowMap
&ArrowIndices
);
133 /// \return Javascript for displaying shortcuts help;
134 StringRef
showHelpJavascript();
136 /// \return Javascript for navigating the HTML report using j/k keys.
137 StringRef
generateKeyboardNavigationJavascript();
139 /// \return Javascript for drawing control-flow arrows.
140 StringRef
generateArrowDrawingJavascript();
142 /// \return JavaScript for an option to only show relevant lines.
143 std::string
showRelevantLinesJavascript(const PathDiagnostic
&D
,
144 const PathPieces
&path
);
146 /// Write executed lines from \p D in JSON format into \p os.
147 void dumpCoverageData(const PathDiagnostic
&D
, const PathPieces
&path
,
148 llvm::raw_string_ostream
&os
);
151 bool isArrowPiece(const PathDiagnosticPiece
&P
) {
152 return isa
<PathDiagnosticControlFlowPiece
>(P
) && P
.getString().empty();
155 unsigned getPathSizeWithoutArrows(const PathPieces
&Path
) {
156 unsigned TotalPieces
= Path
.size();
157 unsigned TotalArrowPieces
= llvm::count_if(
158 Path
, [](const PathDiagnosticPieceRef
&P
) { return isArrowPiece(*P
); });
159 return TotalPieces
- TotalArrowPieces
;
162 class ArrowMap
: public std::vector
<unsigned> {
163 using Base
= std::vector
<unsigned>;
166 ArrowMap(unsigned Size
) : Base(Size
, 0) {}
167 unsigned getTotalNumberOfArrows() const { return at(0); }
170 llvm::raw_ostream
&operator<<(llvm::raw_ostream
&OS
, const ArrowMap
&Indices
) {
172 llvm::interleave(Indices
, OS
, ",");
178 void ento::createHTMLDiagnosticConsumer(
179 PathDiagnosticConsumerOptions DiagOpts
, PathDiagnosticConsumers
&C
,
180 const std::string
&OutputDir
, const Preprocessor
&PP
,
181 const cross_tu::CrossTranslationUnitContext
&CTU
,
182 const MacroExpansionContext
&MacroExpansions
) {
184 // FIXME: HTML is currently our default output type, but if the output
185 // directory isn't specified, it acts like if it was in the minimal text
186 // output mode. This doesn't make much sense, we should have the minimal text
187 // as our default. In the case of backward compatibility concerns, this could
188 // be preserved with -analyzer-config-compatibility-mode=true.
189 createTextMinimalPathDiagnosticConsumer(DiagOpts
, C
, OutputDir
, PP
, CTU
,
192 // TODO: Emit an error here.
193 if (OutputDir
.empty())
196 C
.push_back(new HTMLDiagnostics(std::move(DiagOpts
), OutputDir
, PP
, true));
199 void ento::createHTMLSingleFileDiagnosticConsumer(
200 PathDiagnosticConsumerOptions DiagOpts
, PathDiagnosticConsumers
&C
,
201 const std::string
&OutputDir
, const Preprocessor
&PP
,
202 const cross_tu::CrossTranslationUnitContext
&CTU
,
203 const clang::MacroExpansionContext
&MacroExpansions
) {
204 createTextMinimalPathDiagnosticConsumer(DiagOpts
, C
, OutputDir
, PP
, CTU
,
207 // TODO: Emit an error here.
208 if (OutputDir
.empty())
211 C
.push_back(new HTMLDiagnostics(std::move(DiagOpts
), OutputDir
, PP
, false));
214 void ento::createPlistHTMLDiagnosticConsumer(
215 PathDiagnosticConsumerOptions DiagOpts
, PathDiagnosticConsumers
&C
,
216 const std::string
&prefix
, const Preprocessor
&PP
,
217 const cross_tu::CrossTranslationUnitContext
&CTU
,
218 const MacroExpansionContext
&MacroExpansions
) {
219 createHTMLDiagnosticConsumer(
220 DiagOpts
, C
, std::string(llvm::sys::path::parent_path(prefix
)), PP
, CTU
,
222 createPlistMultiFileDiagnosticConsumer(DiagOpts
, C
, prefix
, PP
, CTU
,
224 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts
), C
, prefix
, PP
,
225 CTU
, MacroExpansions
);
228 void ento::createSarifHTMLDiagnosticConsumer(
229 PathDiagnosticConsumerOptions DiagOpts
, PathDiagnosticConsumers
&C
,
230 const std::string
&sarif_file
, const Preprocessor
&PP
,
231 const cross_tu::CrossTranslationUnitContext
&CTU
,
232 const MacroExpansionContext
&MacroExpansions
) {
233 createHTMLDiagnosticConsumer(
234 DiagOpts
, C
, std::string(llvm::sys::path::parent_path(sarif_file
)), PP
,
235 CTU
, MacroExpansions
);
236 createSarifDiagnosticConsumer(DiagOpts
, C
, sarif_file
, PP
, CTU
,
238 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts
), C
, sarif_file
,
239 PP
, CTU
, MacroExpansions
);
242 //===----------------------------------------------------------------------===//
243 // Report processing.
244 //===----------------------------------------------------------------------===//
246 void HTMLDiagnostics::FlushDiagnosticsImpl(
247 std::vector
<const PathDiagnostic
*> &Diags
,
248 FilesMade
*filesMade
) {
249 for (const auto Diag
: Diags
)
250 ReportDiag(*Diag
, filesMade
);
253 static llvm::SmallString
<32> getIssueHash(const PathDiagnostic
&D
,
254 const Preprocessor
&PP
) {
255 SourceManager
&SMgr
= PP
.getSourceManager();
256 PathDiagnosticLocation UPDLoc
= D
.getUniqueingLoc();
257 FullSourceLoc
L(SMgr
.getExpansionLoc(UPDLoc
.isValid()
258 ? UPDLoc
.asLocation()
259 : D
.getLocation().asLocation()),
261 return getIssueHash(L
, D
.getCheckerName(), D
.getBugType(),
262 D
.getDeclWithIssue(), PP
.getLangOpts());
265 void HTMLDiagnostics::ReportDiag(const PathDiagnostic
& D
,
266 FilesMade
*filesMade
) {
267 // Create the HTML directory if it is missing.
270 if (std::error_code ec
= llvm::sys::fs::create_directories(Directory
)) {
271 llvm::errs() << "warning: could not create directory '"
272 << Directory
<< "': " << ec
.message() << '\n';
281 // First flatten out the entire path to make it easier to use.
282 PathPieces path
= D
.path
.flatten(/*ShouldFlattenMacros=*/false);
284 // The path as already been prechecked that the path is non-empty.
285 assert(!path
.empty());
286 const SourceManager
&SMgr
= path
.front()->getLocation().getManager();
288 // Create a new rewriter to generate HTML.
289 Rewriter
R(const_cast<SourceManager
&>(SMgr
), PP
.getLangOpts());
291 // Get the function/method name
292 SmallString
<128> declName("unknown");
294 if (const Decl
*DeclWithIssue
= D
.getDeclWithIssue()) {
295 if (const auto *ND
= dyn_cast
<NamedDecl
>(DeclWithIssue
))
296 declName
= ND
->getDeclName().getAsString();
298 if (const Stmt
*Body
= DeclWithIssue
->getBody()) {
299 // Retrieve the relative position of the declaration which will be used
302 SMgr
.getExpansionLoc(path
.back()->getLocation().asLocation()),
304 FullSourceLoc
FunL(SMgr
.getExpansionLoc(Body
->getBeginLoc()), SMgr
);
305 offsetDecl
= L
.getExpansionLineNumber() - FunL
.getExpansionLineNumber();
309 SmallString
<32> IssueHash
= getIssueHash(D
, PP
);
310 auto [It
, IsNew
] = EmittedHashes
.insert(IssueHash
);
312 // We've already emitted a duplicate issue. It'll get overwritten anyway.
316 std::string report
= GenerateHTML(D
, R
, SMgr
, path
, declName
.c_str());
317 if (report
.empty()) {
318 llvm::errs() << "warning: no diagnostics generated for main file.\n";
322 // Create a path for the target HTML file.
325 SmallString
<128> FileNameStr
;
326 llvm::raw_svector_ostream
FileName(FileNameStr
);
327 FileName
<< "report-";
329 // Historically, neither the stable report filename nor the unstable report
330 // filename were actually stable. That said, the stable report filename
331 // was more stable because it was mostly composed of information
332 // about the bug report instead of being completely random.
333 // Now both stable and unstable report filenames are in fact stable
334 // but the stable report filename is still more verbose.
335 if (DiagOpts
.ShouldWriteVerboseReportFilename
) {
336 // FIXME: This code relies on knowing what constitutes the issue hash.
337 // Otherwise deduplication won't work correctly.
339 path
.back()->getLocation().asLocation().getExpansionLoc().getFileID();
341 OptionalFileEntryRef Entry
= SMgr
.getFileEntryRefForID(ReportFile
);
343 FileName
<< llvm::sys::path::filename(Entry
->getName()).str() << "-"
344 << declName
.c_str() << "-" << offsetDecl
<< "-";
347 FileName
<< StringRef(IssueHash
).substr(0, 6).str() << ".html";
349 SmallString
<128> ResultPath
;
350 llvm::sys::path::append(ResultPath
, Directory
, FileName
.str());
351 if (std::error_code EC
= llvm::sys::fs::make_absolute(ResultPath
)) {
352 llvm::errs() << "warning: could not make '" << ResultPath
353 << "' absolute: " << EC
.message() << '\n';
357 if (std::error_code EC
= llvm::sys::fs::openFileForReadWrite(
358 ResultPath
, FD
, llvm::sys::fs::CD_CreateNew
,
359 llvm::sys::fs::OF_Text
)) {
360 // Existence of the file corresponds to the situation where a different
361 // Clang instance has emitted a bug report with the same issue hash.
362 // This is an entirely normal situation that does not deserve a warning,
363 // as apart from hash collisions this can happen because the reports
364 // are in fact similar enough to be considered duplicates of each other.
365 if (EC
!= llvm::errc::file_exists
) {
366 llvm::errs() << "warning: could not create file in '" << Directory
367 << "': " << EC
.message() << '\n';
372 llvm::raw_fd_ostream
os(FD
, true);
375 filesMade
->addDiagnostic(D
, getName(),
376 llvm::sys::path::filename(ResultPath
));
378 // Emit the HTML to disk.
382 std::string
HTMLDiagnostics::GenerateHTML(const PathDiagnostic
& D
, Rewriter
&R
,
383 const SourceManager
& SMgr
, const PathPieces
& path
, const char *declName
) {
384 // Rewrite source files as HTML for every new file the path crosses
385 std::vector
<FileID
> FileIDs
;
386 for (auto I
: path
) {
387 FileID FID
= I
->getLocation().asLocation().getExpansionLoc().getFileID();
388 if (llvm::is_contained(FileIDs
, FID
))
391 FileIDs
.push_back(FID
);
392 RewriteFile(R
, path
, FID
);
395 if (SupportsCrossFileDiagnostics
&& FileIDs
.size() > 1) {
396 // Prefix file names, anchor tags, and nav cursors to every file
397 for (auto I
= FileIDs
.begin(), E
= FileIDs
.end(); I
!= E
; I
++) {
399 llvm::raw_string_ostream
os(s
);
401 if (I
!= FileIDs
.begin())
402 os
<< "<hr class=divider>\n";
404 os
<< "<div id=File" << I
->getHashValue() << ">\n";
407 if (I
!= FileIDs
.begin())
408 os
<< "<div class=FileNav><a href=\"#File" << (I
- 1)->getHashValue()
409 << "\">←</a></div>";
411 os
<< "<h4 class=FileName>" << SMgr
.getFileEntryRefForID(*I
)->getName()
416 os
<< "<div class=FileNav><a href=\"#File" << (I
+ 1)->getHashValue()
417 << "\">→</a></div>";
421 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(*I
), os
.str());
424 // Append files to the main report file in the order they appear in the path
425 for (auto I
: llvm::drop_begin(FileIDs
)) {
427 llvm::raw_string_ostream
os(s
);
429 const RewriteBuffer
*Buf
= R
.getRewriteBufferFor(I
);
433 R
.InsertTextAfter(SMgr
.getLocForEndOfFile(FileIDs
[0]), os
.str());
437 const RewriteBuffer
*Buf
= R
.getRewriteBufferFor(FileIDs
[0]);
441 // Add CSS, header, and footer.
443 path
.back()->getLocation().asLocation().getExpansionLoc().getFileID();
444 OptionalFileEntryRef Entry
= SMgr
.getFileEntryRefForID(FID
);
445 FinalizeHTML(D
, R
, SMgr
, path
, FileIDs
[0], *Entry
, declName
);
448 llvm::raw_string_ostream
os(file
);
455 void HTMLDiagnostics::dumpCoverageData(
456 const PathDiagnostic
&D
,
457 const PathPieces
&path
,
458 llvm::raw_string_ostream
&os
) {
460 const FilesToLineNumsMap
&ExecutedLines
= D
.getExecutedLines();
462 os
<< "var relevant_lines = {";
463 for (auto I
= ExecutedLines
.begin(),
464 E
= ExecutedLines
.end(); I
!= E
; ++I
) {
465 if (I
!= ExecutedLines
.begin())
468 os
<< "\"" << I
->first
.getHashValue() << "\": {";
469 for (unsigned LineNo
: I
->second
) {
470 if (LineNo
!= *(I
->second
.begin()))
473 os
<< "\"" << LineNo
<< "\": 1";
481 std::string
HTMLDiagnostics::showRelevantLinesJavascript(
482 const PathDiagnostic
&D
, const PathPieces
&path
) {
484 llvm::raw_string_ostream
os(s
);
485 os
<< "<script type='text/javascript'>\n";
486 dumpCoverageData(D
, path
, os
);
489 var filterCounterexample = function (hide) {
490 var tables = document.getElementsByClassName("code
");
491 for (var t=0; t<tables.length; t++) {
492 var table = tables[t];
493 var file_id = table.getAttribute("data
-fileid
");
494 var lines_in_fid = relevant_lines[file_id];
498 var lines = table.getElementsByClassName("codeline
");
499 for (var i=0; i<lines.length; i++) {
501 var lineNo = el.getAttribute("data
-linenumber
");
502 if (!lines_in_fid[lineNo]) {
504 el.setAttribute("hidden
", "");
506 el.removeAttribute("hidden
");
513 window.addEventListener("keydown
", function (event) {
514 if (event.defaultPrevented) {
518 if (event.shiftKey && event.keyCode == 83) {
519 var checked = document.getElementsByName("showCounterexample
")[0].checked;
520 filterCounterexample(!checked);
521 document.getElementsByName("showCounterexample
")[0].click();
525 event.preventDefault();
528 document.addEventListener("DOMContentLoaded
", function() {
529 document.querySelector('input[name="showCounterexample
"]').onchange=
531 filterCounterexample(this.checked);
537 <input type="checkbox
" name="showCounterexample
" id="showCounterexample
" />
538 <label for="showCounterexample
">
539 Show only relevant lines
541 <input type="checkbox
" name="showArrows
"
542 id="showArrows
" style="margin
-left
: 10px
" />
543 <label for="showArrows
">
544 Show control flow arrows
552 void HTMLDiagnostics::FinalizeHTML(const PathDiagnostic
&D
, Rewriter
&R
,
553 const SourceManager
&SMgr
,
554 const PathPieces
&path
, FileID FID
,
555 FileEntryRef Entry
, const char *declName
) {
556 // This is a cludge; basically we want to append either the full
557 // working directory if we have no directory information. This is
558 // a work in progress.
560 llvm::SmallString
<0> DirName
;
562 if (llvm::sys::path::is_relative(Entry
.getName())) {
563 llvm::sys::fs::current_path(DirName
);
567 int LineNumber
= path
.back()->getLocation().asLocation().getExpansionLineNumber();
568 int ColumnNumber
= path
.back()->getLocation().asLocation().getExpansionColumnNumber();
570 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
), showHelpJavascript());
572 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
),
573 generateKeyboardNavigationJavascript());
575 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
),
576 generateArrowDrawingJavascript());
578 // Checkbox and javascript for filtering the output to the counterexample.
579 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
),
580 showRelevantLinesJavascript(D
, path
));
582 // Add the name of the file as an <h1> tag.
585 llvm::raw_string_ostream
os(s
);
587 os
<< "<!-- REPORTHEADER -->\n"
588 << "<h3>Bug Summary</h3>\n<table class=\"simpletable\">\n"
589 "<tr><td class=\"rowname\">File:</td><td>"
590 << html::EscapeText(DirName
)
591 << html::EscapeText(Entry
.getName())
592 << "</td></tr>\n<tr><td class=\"rowname\">Warning:</td><td>"
593 "<a href=\"#EndPath\">line "
598 << D
.getVerboseDescription() << "</td></tr>\n";
600 // The navigation across the extra notes pieces.
601 unsigned NumExtraPieces
= 0;
602 for (const auto &Piece
: path
) {
603 if (const auto *P
= dyn_cast
<PathDiagnosticNotePiece
>(Piece
.get())) {
605 P
->getLocation().asLocation().getExpansionLineNumber();
607 P
->getLocation().asLocation().getExpansionColumnNumber();
609 os
<< "<tr><td class=\"rowname\">Note:</td><td>"
610 << "<a href=\"#Note" << NumExtraPieces
<< "\">line "
611 << LineNumber
<< ", column " << ColumnNumber
<< "</a><br />"
612 << P
->getString() << "</td></tr>";
616 // Output any other meta data.
618 for (const std::string
&Metadata
:
619 llvm::make_range(D
.meta_begin(), D
.meta_end())) {
620 os
<< "<tr><td></td><td>" << html::EscapeText(Metadata
) << "</td></tr>\n";
625 <!-- REPORTSUMMARYEXTRA -->
626 <h3>Annotated Source Code</h3>
627 <p>Press <a href="#" onclick="toggleHelp(); return false;">'?'</a>
628 to see keyboard shortcuts</p>
629 <input type="checkbox" class="spoilerhider" id="showinvocation" />
630 <label for="showinvocation" >Show analyzer invocation</label>
631 <div class="spoiler">clang -cc1 )<<<";
632 os
<< html::EscapeText(DiagOpts
.ToolInvocation
);
635 <div id='tooltiphint' hidden="true">
636 <p>Keyboard shortcuts: </p>
638 <li>Use 'j/k' keys for keyboard navigation</li>
639 <li>Use 'Shift+S' to show/hide relevant lines</li>
640 <li>Use '?' to toggle this window</li>
642 <a href="#" onclick="toggleHelp(); return false;">Close</a>
646 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
), os
.str());
649 // Embed meta-data tags.
652 llvm::raw_string_ostream
os(s
);
654 StringRef BugDesc
= D
.getVerboseDescription();
655 if (!BugDesc
.empty())
656 os
<< "\n<!-- BUGDESC " << BugDesc
<< " -->\n";
658 StringRef BugType
= D
.getBugType();
659 if (!BugType
.empty())
660 os
<< "\n<!-- BUGTYPE " << BugType
<< " -->\n";
662 PathDiagnosticLocation UPDLoc
= D
.getUniqueingLoc();
663 FullSourceLoc
L(SMgr
.getExpansionLoc(UPDLoc
.isValid()
664 ? UPDLoc
.asLocation()
665 : D
.getLocation().asLocation()),
668 StringRef BugCategory
= D
.getCategory();
669 if (!BugCategory
.empty())
670 os
<< "\n<!-- BUGCATEGORY " << BugCategory
<< " -->\n";
672 os
<< "\n<!-- BUGFILE " << DirName
<< Entry
.getName() << " -->\n";
674 os
<< "\n<!-- FILENAME " << llvm::sys::path::filename(Entry
.getName()) << " -->\n";
676 os
<< "\n<!-- FUNCTIONNAME " << declName
<< " -->\n";
678 os
<< "\n<!-- ISSUEHASHCONTENTOFLINEINCONTEXT " << getIssueHash(D
, PP
)
681 os
<< "\n<!-- BUGLINE "
685 os
<< "\n<!-- BUGCOLUMN "
689 os
<< "\n<!-- BUGPATHLENGTH " << getPathSizeWithoutArrows(path
) << " -->\n";
691 // Mark the end of the tags.
692 os
<< "\n<!-- BUGMETAEND -->\n";
695 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
), os
.str());
698 html::AddHeaderFooterInternalBuiltinCSS(R
, FID
, Entry
.getName());
701 StringRef
HTMLDiagnostics::showHelpJavascript() {
703 <script type='text/javascript'>
705 var toggleHelp = function() {
706 var hint = document.querySelector("#tooltiphint");
707 var attributeName = "hidden";
708 if (hint.hasAttribute(attributeName)) {
709 hint.removeAttribute(attributeName);
711 hint.setAttribute("hidden", "true");
714 window.addEventListener("keydown", function (event) {
715 if (event.defaultPrevented) {
718 if (event.key == "?") {
723 event.preventDefault();
729 static bool shouldDisplayPopUpRange(const SourceRange
&Range
) {
730 return !(Range
.getBegin().isMacroID() || Range
.getEnd().isMacroID());
734 HandlePopUpPieceStartTag(Rewriter
&R
,
735 const std::vector
<SourceRange
> &PopUpRanges
) {
736 for (const auto &Range
: PopUpRanges
) {
737 if (!shouldDisplayPopUpRange(Range
))
740 html::HighlightRange(R
, Range
.getBegin(), Range
.getEnd(), "",
741 "<table class='variable_popup'><tbody>",
742 /*IsTokenRange=*/true);
746 static void HandlePopUpPieceEndTag(Rewriter
&R
,
747 const PathDiagnosticPopUpPiece
&Piece
,
748 std::vector
<SourceRange
> &PopUpRanges
,
749 unsigned int LastReportedPieceIndex
,
750 unsigned int PopUpPieceIndex
) {
751 SmallString
<256> Buf
;
752 llvm::raw_svector_ostream
Out(Buf
);
754 SourceRange
Range(Piece
.getLocation().asRange());
755 if (!shouldDisplayPopUpRange(Range
))
758 // Write out the path indices with a right arrow and the message as a row.
759 Out
<< "<tr><td valign='top'><div class='PathIndex PathIndexPopUp'>"
760 << LastReportedPieceIndex
;
762 // Also annotate the state transition with extra indices.
763 Out
<< '.' << PopUpPieceIndex
;
765 Out
<< "</div></td><td>" << Piece
.getString() << "</td></tr>";
767 // If no report made at this range mark the variable and add the end tags.
768 if (!llvm::is_contained(PopUpRanges
, Range
)) {
769 // Store that we create a report at this range.
770 PopUpRanges
.push_back(Range
);
772 Out
<< "</tbody></table></span>";
773 html::HighlightRange(R
, Range
.getBegin(), Range
.getEnd(),
774 "<span class='variable'>", Buf
.c_str(),
775 /*IsTokenRange=*/true);
777 // Otherwise inject just the new row at the end of the range.
778 html::HighlightRange(R
, Range
.getBegin(), Range
.getEnd(), "", Buf
.c_str(),
779 /*IsTokenRange=*/true);
783 void HTMLDiagnostics::RewriteFile(Rewriter
&R
, const PathPieces
&path
,
787 // Maintain the counts of extra note pieces separately.
788 unsigned TotalPieces
= getPathSizeWithoutArrows(path
);
789 unsigned TotalNotePieces
=
790 llvm::count_if(path
, [](const PathDiagnosticPieceRef
&p
) {
791 return isa
<PathDiagnosticNotePiece
>(*p
);
793 unsigned PopUpPieceCount
=
794 llvm::count_if(path
, [](const PathDiagnosticPieceRef
&p
) {
795 return isa
<PathDiagnosticPopUpPiece
>(*p
);
798 unsigned TotalRegularPieces
= TotalPieces
- TotalNotePieces
- PopUpPieceCount
;
799 unsigned NumRegularPieces
= TotalRegularPieces
;
800 unsigned NumNotePieces
= TotalNotePieces
;
801 unsigned NumberOfArrows
= 0;
802 // Stores the count of the regular piece indices.
803 std::map
<int, int> IndexMap
;
804 ArrowMap
ArrowIndices(TotalRegularPieces
+ 1);
806 // Stores the different ranges where we have reported something.
807 std::vector
<SourceRange
> PopUpRanges
;
808 for (const PathDiagnosticPieceRef
&I
: llvm::reverse(path
)) {
809 const auto &Piece
= *I
.get();
811 if (isa
<PathDiagnosticPopUpPiece
>(Piece
)) {
812 ++IndexMap
[NumRegularPieces
];
813 } else if (isa
<PathDiagnosticNotePiece
>(Piece
)) {
814 // This adds diagnostic bubbles, but not navigation.
815 // Navigation through note pieces would be added later,
816 // as a separate pass through the piece list.
817 HandlePiece(R
, FID
, Piece
, PopUpRanges
, NumNotePieces
, TotalNotePieces
);
820 } else if (isArrowPiece(Piece
)) {
821 NumberOfArrows
= ProcessControlFlowPiece(
822 R
, FID
, cast
<PathDiagnosticControlFlowPiece
>(Piece
), NumberOfArrows
);
823 ArrowIndices
[NumRegularPieces
] = NumberOfArrows
;
826 HandlePiece(R
, FID
, Piece
, PopUpRanges
, NumRegularPieces
,
829 ArrowIndices
[NumRegularPieces
] = ArrowIndices
[NumRegularPieces
+ 1];
832 ArrowIndices
[0] = NumberOfArrows
;
834 // At this point ArrowIndices represent the following data structure:
835 // [a_0, a_1, ..., a_N]
836 // where N is the number of events in the path.
838 // Then for every event with index i \in [0, N - 1], we can say that
839 // arrows with indices \in [a_(i+1), a_i) correspond to that event.
840 // We can say that because arrows with these indices appeared in the
841 // path in between the i-th and the (i+1)-th events.
842 assert(ArrowIndices
.back() == 0 &&
843 "No arrows should be after the last event");
844 // This assertion also guarantees that all indices in are <= NumberOfArrows.
845 assert(llvm::is_sorted(ArrowIndices
, std::greater
<unsigned>()) &&
846 "Incorrect arrow indices map");
848 // Secondary indexing if we are having multiple pop-ups between two notes.
849 // (e.g. [(13) 'a' is 'true']; [(13.1) 'b' is 'false']; [(13.2) 'c' is...)
850 NumRegularPieces
= TotalRegularPieces
;
851 for (const PathDiagnosticPieceRef
&I
: llvm::reverse(path
)) {
852 const auto &Piece
= *I
.get();
854 if (const auto *PopUpP
= dyn_cast
<PathDiagnosticPopUpPiece
>(&Piece
)) {
855 int PopUpPieceIndex
= IndexMap
[NumRegularPieces
];
857 // Pop-up pieces needs the index of the last reported piece and its count
858 // how many times we report to handle multiple reports on the same range.
859 // This marks the variable, adds the </table> end tag and the message
860 // (list element) as a row. The <table> start tag will be added after the
861 // rows has been written out. Note: It stores every different range.
862 HandlePopUpPieceEndTag(R
, *PopUpP
, PopUpRanges
, NumRegularPieces
,
865 if (PopUpPieceIndex
> 0)
866 --IndexMap
[NumRegularPieces
];
868 } else if (!isa
<PathDiagnosticNotePiece
>(Piece
) && !isArrowPiece(Piece
)) {
873 // Add the <table> start tag of pop-up pieces based on the stored ranges.
874 HandlePopUpPieceStartTag(R
, PopUpRanges
);
876 // Add line numbers, header, footer, etc.
877 html::EscapeText(R
, FID
);
878 html::AddLineNumbers(R
, FID
);
880 addArrowSVGs(R
, FID
, ArrowIndices
);
882 // If we have a preprocessor, relex the file and syntax highlight.
883 // We might not have a preprocessor if we come from a deserialized AST file,
885 html::SyntaxHighlight(R
, FID
, PP
, RewriterCache
);
886 html::HighlightMacros(R
, FID
, PP
, RewriterCache
);
889 void HTMLDiagnostics::HandlePiece(Rewriter
&R
, FileID BugFileID
,
890 const PathDiagnosticPiece
&P
,
891 const std::vector
<SourceRange
> &PopUpRanges
,
892 unsigned num
, unsigned max
) {
893 // For now, just draw a box above the line in question, and emit the
895 FullSourceLoc Pos
= P
.getLocation().asLocation();
900 SourceManager
&SM
= R
.getSourceMgr();
901 assert(&Pos
.getManager() == &SM
&& "SourceManagers are different!");
902 std::pair
<FileID
, unsigned> LPosInfo
= SM
.getDecomposedExpansionLoc(Pos
);
904 if (LPosInfo
.first
!= BugFileID
)
907 llvm::MemoryBufferRef Buf
= SM
.getBufferOrFake(LPosInfo
.first
);
908 const char *FileStart
= Buf
.getBufferStart();
910 // Compute the column number. Rewind from the current position to the start
912 unsigned ColNo
= SM
.getColumnNumber(LPosInfo
.first
, LPosInfo
.second
);
913 const char *TokInstantiationPtr
=Pos
.getExpansionLoc().getCharacterData();
914 const char *LineStart
= TokInstantiationPtr
-ColNo
;
917 const char *LineEnd
= TokInstantiationPtr
;
918 const char *FileEnd
= Buf
.getBufferEnd();
919 while (*LineEnd
!= '\n' && LineEnd
!= FileEnd
)
922 // Compute the margin offset by counting tabs and non-tabs.
924 for (const char* c
= LineStart
; c
!= TokInstantiationPtr
; ++c
)
925 PosNo
+= *c
== '\t' ? 8 : 1;
927 // Create the html for the message.
929 const char *Kind
= nullptr;
931 bool SuppressIndex
= (max
== 1);
932 switch (P
.getKind()) {
933 case PathDiagnosticPiece::Event
: Kind
= "Event"; break;
934 case PathDiagnosticPiece::ControlFlow
: Kind
= "Control"; break;
935 // Setting Kind to "Control" is intentional.
936 case PathDiagnosticPiece::Macro
: Kind
= "Control"; break;
937 case PathDiagnosticPiece::Note
:
940 SuppressIndex
= true;
942 case PathDiagnosticPiece::Call
:
943 case PathDiagnosticPiece::PopUp
:
944 llvm_unreachable("Calls and extra notes should already be handled");
948 llvm::raw_string_ostream
os(sbuf
);
950 os
<< "\n<tr><td class=\"num\"></td><td class=\"line\"><div id=\"";
959 os
<< "\" class=\"msg";
961 os
<< " msg" << Kind
;
962 os
<< "\" style=\"margin-left:" << PosNo
<< "ex";
964 // Output a maximum size.
965 if (!isa
<PathDiagnosticMacroPiece
>(P
)) {
966 // Get the string and determining its maximum substring.
967 const auto &Msg
= P
.getString();
968 unsigned max_token
= 0;
970 unsigned len
= Msg
.size();
980 if (cnt
> max_token
) max_token
= cnt
;
987 // Determine the approximate size of the message bubble in em.
989 const unsigned max_line
= 120;
991 if (max_token
>= max_line
)
994 unsigned characters
= max_line
;
995 unsigned lines
= len
/ max_line
;
998 for (; characters
> max_token
; --characters
)
999 if (len
/ characters
> lines
) {
1005 em
= characters
/ 2;
1008 if (em
< max_line
/2)
1009 os
<< "; max-width:" << em
<< "em";
1012 os
<< "; max-width:100em";
1016 if (!SuppressIndex
) {
1017 os
<< "<table class=\"msgT\"><tr><td valign=\"top\">";
1018 os
<< "<div class=\"PathIndex";
1019 if (Kind
) os
<< " PathIndex" << Kind
;
1020 os
<< "\">" << num
<< "</div>";
1023 os
<< "</td><td><div class=\"PathNav\"><a href=\"#Path"
1025 << "\" title=\"Previous event ("
1027 << ")\">←</a></div>";
1033 if (const auto *MP
= dyn_cast
<PathDiagnosticMacroPiece
>(&P
)) {
1034 os
<< "Within the expansion of the macro '";
1036 // Get the name of the macro by relexing it.
1038 FullSourceLoc L
= MP
->getLocation().asLocation().getExpansionLoc();
1039 assert(L
.isFileID());
1040 StringRef BufferInfo
= L
.getBufferData();
1041 std::pair
<FileID
, unsigned> LocInfo
= L
.getDecomposedLoc();
1042 const char* MacroName
= LocInfo
.second
+ BufferInfo
.data();
1043 Lexer
rawLexer(SM
.getLocForStartOfFile(LocInfo
.first
), PP
.getLangOpts(),
1044 BufferInfo
.begin(), MacroName
, BufferInfo
.end());
1047 rawLexer
.LexFromRawLexer(TheTok
);
1048 for (unsigned i
= 0, n
= TheTok
.getLength(); i
< n
; ++i
)
1054 if (!SuppressIndex
) {
1057 os
<< "<td><div class=\"PathNav\"><a href=\"#";
1061 os
<< "Path" << (num
+ 1);
1062 os
<< "\" title=\"Next event ("
1064 << ")\">→</a></div></td>";
1067 os
<< "</tr></table>";
1070 // Within a macro piece. Write out each event.
1071 ProcessMacroPiece(os
, *MP
, 0);
1074 os
<< html::EscapeText(P
.getString());
1076 if (!SuppressIndex
) {
1079 os
<< "<td><div class=\"PathNav\"><a href=\"#";
1083 os
<< "Path" << (num
+ 1);
1084 os
<< "\" title=\"Next event ("
1086 << ")\">→</a></div></td>";
1089 os
<< "</tr></table>";
1093 os
<< "</div></td></tr>";
1095 // Insert the new html.
1096 unsigned DisplayPos
= LineEnd
- FileStart
;
1097 SourceLocation Loc
=
1098 SM
.getLocForStartOfFile(LPosInfo
.first
).getLocWithOffset(DisplayPos
);
1100 R
.InsertTextBefore(Loc
, os
.str());
1102 // Now highlight the ranges.
1103 ArrayRef
<SourceRange
> Ranges
= P
.getRanges();
1104 for (const auto &Range
: Ranges
) {
1105 // If we have already highlighted the range as a pop-up there is no work.
1106 if (llvm::is_contained(PopUpRanges
, Range
))
1109 HighlightRange(R
, LPosInfo
.first
, Range
);
1113 static void EmitAlphaCounter(raw_ostream
&os
, unsigned n
) {
1114 unsigned x
= n
% ('z' - 'a');
1118 EmitAlphaCounter(os
, n
);
1120 os
<< char('a' + x
);
1123 unsigned HTMLDiagnostics::ProcessMacroPiece(raw_ostream
&os
,
1124 const PathDiagnosticMacroPiece
& P
,
1126 for (const auto &subPiece
: P
.subPieces
) {
1127 if (const auto *MP
= dyn_cast
<PathDiagnosticMacroPiece
>(subPiece
.get())) {
1128 num
= ProcessMacroPiece(os
, *MP
, num
);
1132 if (const auto *EP
= dyn_cast
<PathDiagnosticEventPiece
>(subPiece
.get())) {
1133 os
<< "<div class=\"msg msgEvent\" style=\"width:94%; "
1134 "margin-left:5px\">"
1135 "<table class=\"msgT\"><tr>"
1136 "<td valign=\"top\"><div class=\"PathIndex PathIndexEvent\">";
1137 EmitAlphaCounter(os
, num
++);
1138 os
<< "</div></td><td valign=\"top\">"
1139 << html::EscapeText(EP
->getString())
1140 << "</td></tr></table></div>\n";
1147 void HTMLDiagnostics::addArrowSVGs(Rewriter
&R
, FileID BugFileID
,
1148 const ArrowMap
&ArrowIndices
) {
1150 llvm::raw_string_ostream
OS(S
);
1153 <style type="text
/css
">
1160 pointer-events: none;
1164 stroke-opacity: 0.2;
1166 marker-end: url(#arrowhead);
1170 stroke-opacity: 0.6;
1172 marker-end: url(#arrowheadSelected);
1182 <svg xmlns="http
://www.w3.org/2000/svg">
1184 <marker id
="arrowheadSelected" class="arrowhead" opacity
="0.6"
1185 viewBox
="0 0 10 10" refX
="3" refY
="5"
1186 markerWidth
="4" markerHeight
="4">
1187 <path d
="M 0 0 L 10 5 L 0 10 z" />
1189 <marker id
="arrowhead" class="arrowhead" opacity
="0.2"
1190 viewBox
="0 0 10 10" refX
="3" refY
="5"
1191 markerWidth
="4" markerHeight
="4">
1192 <path d
="M 0 0 L 10 5 L 0 10 z" />
1195 <g id
="arrows" fill
="none" stroke
="blue" visibility
="hidden">
1198 for (unsigned Index : llvm::seq(0u, ArrowIndices.getTotalNumberOfArrows())) {
1199 OS << " <path
class=\"arrow
\" id
=\"arrow
" << Index << "\"/>\n";
1205 <script type
='text/javascript'>
1206 const arrowIndices
= )<<<";
1208 OS << ArrowIndices << "\n</script
>\n";
1210 R.InsertTextBefore(R.getSourceMgr().getLocForStartOfFile(BugFileID),
1214 static std::string getSpanBeginForControl(const char *ClassName,
1217 llvm::raw_string_ostream OS(Result);
1218 OS << "<span id
=\"" << ClassName << Index << "\">";
1222 static std::string getSpanBeginForControlStart(unsigned Index) {
1223 return getSpanBeginForControl("start
", Index);
1226 static std::string getSpanBeginForControlEnd(unsigned Index) {
1227 return getSpanBeginForControl("end
", Index);
1230 unsigned HTMLDiagnostics::ProcessControlFlowPiece(
1231 Rewriter &R, FileID BugFileID, const PathDiagnosticControlFlowPiece &P,
1233 for (const PathDiagnosticLocationPair &LPair : P) {
1234 std::string Start = getSpanBeginForControlStart(Number),
1235 End = getSpanBeginForControlEnd(Number++);
1237 HighlightRange(R, BugFileID, LPair.getStart().asRange().getBegin(),
1239 HighlightRange(R, BugFileID, LPair.getEnd().asRange().getBegin(),
1246 void HTMLDiagnostics::HighlightRange(Rewriter& R, FileID BugFileID,
1248 const char *HighlightStart,
1249 const char *HighlightEnd) {
1250 SourceManager &SM = R.getSourceMgr();
1251 const LangOptions &LangOpts = R.getLangOpts();
1253 SourceLocation InstantiationStart = SM.getExpansionLoc(Range.getBegin());
1254 unsigned StartLineNo = SM.getExpansionLineNumber(InstantiationStart);
1256 SourceLocation InstantiationEnd = SM.getExpansionLoc(Range.getEnd());
1257 unsigned EndLineNo = SM.getExpansionLineNumber(InstantiationEnd);
1259 if (EndLineNo < StartLineNo)
1262 if (SM.getFileID(InstantiationStart) != BugFileID ||
1263 SM.getFileID(InstantiationEnd) != BugFileID)
1266 // Compute the column number of the end.
1267 unsigned EndColNo = SM.getExpansionColumnNumber(InstantiationEnd);
1268 unsigned OldEndColNo = EndColNo;
1271 // Add in the length of the token, so that we cover multi-char tokens.
1272 EndColNo += Lexer::MeasureTokenLength(Range.getEnd(), SM, LangOpts)-1;
1275 // Highlight the range. Make the span tag the outermost tag for the
1279 InstantiationEnd.getLocWithOffset(EndColNo - OldEndColNo);
1281 html::HighlightRange(R, InstantiationStart, E, HighlightStart, HighlightEnd);
1284 StringRef HTMLDiagnostics::generateKeyboardNavigationJavascript() {
1286 <script type
='text/javascript'>
1287 var digitMatcher
= new RegExp("[0-9]+");
1289 var querySelectorAllArray
= function(selector
) {
1290 return Array
.prototype
.slice
.call(
1291 document
.querySelectorAll(selector
));
1294 document
.addEventListener("DOMContentLoaded", function() {
1295 querySelectorAllArray(".PathNav > a").forEach(
1296 function(currentValue
, currentIndex
) {
1297 var hrefValue
= currentValue
.getAttribute("href");
1298 currentValue
.onclick
= function() {
1299 scrollTo(document
.querySelector(hrefValue
));
1305 var findNum
= function() {
1306 var s
= document
.querySelector(".msg.selected");
1307 if (!s
|| s
.id
== "EndPath") {
1310 var out
= parseInt(digitMatcher
.exec(s
.id
)[0]);
1314 var classListAdd
= function(el
, theClass
) {
1315 if(!el
.className
.baseVal
)
1316 el
.className
+= " " + theClass
;
1318 el
.className
.baseVal
+= " " + theClass
;
1321 var classListRemove
= function(el
, theClass
) {
1322 var className
= (!el
.className
.baseVal
) ?
1323 el
.className
: el
.className
.baseVal
;
1324 className
= className
.replace(" " + theClass
, "");
1325 if(!el
.className
.baseVal
)
1326 el
.className
= className
;
1328 el
.className
.baseVal
= className
;
1331 var scrollTo
= function(el
) {
1332 querySelectorAllArray(".selected").forEach(function(s
) {
1333 classListRemove(s
, "selected");
1335 classListAdd(el
, "selected");
1336 window
.scrollBy(0, el
.getBoundingClientRect().top
-
1337 (window
.innerHeight
/ 2));
1338 highlightArrowsForSelectedEvent();
1341 var move
= function(num
, up
, numItems
) {
1342 if (num
== 1 && up
|| num
== numItems
- 1 && !up
) {
1344 } else if (num
== 0 && up
) {
1345 return numItems
- 1;
1346 } else if (num
== 0 && !up
) {
1347 return 1 % numItems
;
1349 return up
? num
- 1 : num
+ 1;
1352 var numToId
= function(num
) {
1354 return document
.getElementById("EndPath")
1356 return document
.getElementById("Path" + num
);
1359 var navigateTo
= function(up
) {
1360 var numItems
= document
.querySelectorAll(
1361 ".line > .msgEvent, .line > .msgControl").length
;
1362 var currentSelected
= findNum();
1363 var newSelected
= move(currentSelected
, up
, numItems
);
1364 var newEl
= numToId(newSelected
, numItems
);
1366 // Scroll element into center.
1370 window
.addEventListener("keydown", function (event
) {
1371 if (event
.defaultPrevented
) {
1375 if (event
.keyCode
== 74) {
1376 navigateTo(/*up=*/false);
1378 } else if (event
.keyCode
== 75) {
1379 navigateTo(/*up=*/true);
1383 event
.preventDefault();
1389 StringRef HTMLDiagnostics::generateArrowDrawingJavascript() {
1391 <script type
='text/javascript'>
1392 // Return range of numbers from a range [lower, upper).
1393 function
range(lower
, upper
) {
1395 for (var i
= lower
; i
<= upper
; ++i
) {
1401 var getRelatedArrowIndices
= function(pathId
) {
1402 // HTML numeration of events is a bit different than it is in the path.
1403 // Everything is rotated one step to the right, so the last element
1404 // (error diagnostic) has index 0.
1406 // arrowIndices has at least 2 elements
1407 pathId
= arrowIndices
.length
- 1;
1410 return range(arrowIndices
[pathId
], arrowIndices
[pathId
- 1]);
1413 var highlightArrowsForSelectedEvent
= function() {
1414 const selectedNum
= findNum();
1415 const arrowIndicesToHighlight
= getRelatedArrowIndices(selectedNum
);
1416 arrowIndicesToHighlight
.forEach((index
) => {
1417 var arrow
= document
.querySelector("#arrow" + index
);
1419 classListAdd(arrow
, "selected")
1424 var getAbsoluteBoundingRect
= function(element
) {
1425 const relative
= element
.getBoundingClientRect();
1427 left
: relative
.left
+ window
.pageXOffset
,
1428 right
: relative
.right
+ window
.pageXOffset
,
1429 top
: relative
.top
+ window
.pageYOffset
,
1430 bottom
: relative
.bottom
+ window
.pageYOffset
,
1431 height
: relative
.height
,
1432 width
: relative
.width
1436 var drawArrow
= function(index
) {
1437 // This function is based on the great answer from SO:
1438 // https://stackoverflow.com/a/39575674/11582326
1439 var start
= document
.querySelector("#start" + index
);
1440 var end
= document
.querySelector("#end" + index
);
1441 var arrow
= document
.querySelector("#arrow" + index
);
1443 var startRect
= getAbsoluteBoundingRect(start
);
1444 var endRect
= getAbsoluteBoundingRect(end
);
1446 // It is an arrow from a token to itself, no need to visualize it.
1447 if (startRect
.top
== endRect
.top
&&
1448 startRect
.left
== endRect
.left
)
1451 // Each arrow is a very simple Bézier curve, with two nodes and
1452 // two handles. So, we need to calculate four points in the window:
1454 var posStart
= { x
: 0, y
: 0 };
1456 var posEnd
= { x
: 0, y
: 0 };
1457 // * handle for the start node
1458 var startHandle
= { x
: 0, y
: 0 };
1459 // * handle for the end node
1460 var endHandle
= { x
: 0, y
: 0 };
1461 // One can visualize it as follows:
1477 // NOTE: (0, 0) is the top left corner of the window.
1479 // We have 3 similar, but still different scenarios to cover:
1481 // 1. Two tokens on different lines.
1486 // In this situation, we draw arrow on the left curving to the left.
1487 // 2. Two tokens on the same line, and the destination is on the right.
1492 // In this situation, we draw arrow above curving upwards.
1493 // 3. Two tokens on the same line, and the destination is on the left.
1497 // In this situation, we draw arrow below curving downwards.
1498 const onDifferentLines
= startRect
.top
<= endRect
.top
- 5 ||
1499 startRect
.top
>= endRect
.top
+ 5;
1500 const leftToRight
= startRect
.left
< endRect
.left
;
1502 // NOTE: various magic constants are chosen empirically for
1503 // better positioning and look
1504 if (onDifferentLines
) {
1506 const topToBottom
= startRect
.top
< endRect
.top
;
1507 posStart
.x
= startRect
.left
- 1;
1508 // We don't want to start it at the top left corner of the token,
1509 // it doesn't feel like this is where the arrow comes from.
1510 // For this reason, we start it in the middle of the left side
1512 posStart
.y
= startRect
.top
+ startRect
.height
/ 2;
1514 // End node has arrow head and we give it a bit more space.
1515 posEnd
.x
= endRect
.left
- 4;
1516 posEnd
.y
= endRect
.top
;
1518 // Utility object with x and y offsets for handles.
1520 // We want bottom-to-top arrow to curve a bit more, so it doesn't
1521 // overlap much with top-to-bottom curves (much more frequent).
1522 x
: topToBottom
? 15 : 25,
1523 y
: Math
.min((posEnd
.y
- posStart
.y
) / 3, 10)
1526 // When destination is on the different line, we can make a
1527 // curvier arrow because we have space for it.
1528 // So, instead of using
1530 // startHandle.x = posStart.x - curvature.x
1531 // endHandle.x = posEnd.x - curvature.x
1533 // We use the leftmost of these two values for both handles.
1534 startHandle
.x
= Math
.min(posStart
.x
, posEnd
.x
) - curvature
.x
;
1535 endHandle
.x
= startHandle
.x
;
1537 // Curving downwards from the start node...
1538 startHandle
.y
= posStart
.y
+ curvature
.y
;
1539 // ... and upwards from the end node.
1540 endHandle
.y
= posEnd
.y
- curvature
.y
;
1542 } else if (leftToRight
) {
1544 // Starting from the top right corner...
1545 posStart
.x
= startRect
.right
- 1;
1546 posStart
.y
= startRect
.top
;
1548 // ...and ending at the top left corner of the end token.
1549 posEnd
.x
= endRect
.left
+ 1;
1550 posEnd
.y
= endRect
.top
- 1;
1552 // Utility object with x and y offsets for handles.
1554 x
: Math
.min((posEnd
.x
- posStart
.x
) / 3, 15),
1558 // Curving to the right...
1559 startHandle
.x
= posStart
.x
+ curvature
.x
;
1560 // ... and upwards from the start node.
1561 startHandle
.y
= posStart
.y
- curvature
.y
;
1563 // And to the left...
1564 endHandle
.x
= posEnd
.x
- curvature
.x
;
1565 // ... and upwards from the end node.
1566 endHandle
.y
= posEnd
.y
- curvature
.y
;
1570 // Starting from the bottom right corner...
1571 posStart
.x
= startRect
.right
;
1572 posStart
.y
= startRect
.bottom
;
1574 // ...and ending also at the bottom right corner, but of the end token.
1575 posEnd
.x
= endRect
.right
- 1;
1576 posEnd
.y
= endRect
.bottom
+ 1;
1578 // Utility object with x and y offsets for handles.
1580 x
: Math
.min((posStart
.x
- posEnd
.x
) / 3, 15),
1584 // Curving to the left...
1585 startHandle
.x
= posStart
.x
- curvature
.x
;
1586 // ... and downwards from the start node.
1587 startHandle
.y
= posStart
.y
+ curvature
.y
;
1589 // And to the right...
1590 endHandle
.x
= posEnd
.x
+ curvature
.x
;
1591 // ... and downwards from the end node.
1592 endHandle
.y
= posEnd
.y
+ curvature
.y
;
1595 // Put it all together into a path.
1596 // More information on the format:
1597 // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
1598 var pathStr
= "M" + posStart
.x
+ "," + posStart
.y
+ " " +
1599 "C" + startHandle
.x
+ "," + startHandle
.y
+ " " +
1600 endHandle
.x
+ "," + endHandle
.y
+ " " +
1601 posEnd
.x
+ "," + posEnd
.y
;
1603 arrow
.setAttribute("d", pathStr
);
1606 var drawArrows
= function() {
1607 const numOfArrows
= document
.querySelectorAll("path[id^=arrow]").length
;
1608 for (var i
= 0; i
< numOfArrows
; ++i
) {
1613 var toggleArrows
= function(event
) {
1614 const arrows
= document
.querySelector("#arrows");
1615 if (event
.target
.checked
) {
1616 arrows
.setAttribute("visibility", "visible");
1618 arrows
.setAttribute("visibility", "hidden");
1622 window
.addEventListener("resize", drawArrows
);
1623 document
.addEventListener("DOMContentLoaded", function() {
1624 // Whenever we show invocation, locations change, i.e. we
1625 // need to redraw arrows.
1627 .querySelector('input[id="showinvocation"]')
1628 .addEventListener("click", drawArrows
);
1629 // Hiding irrelevant lines also should cause arrow rerender.
1631 .querySelector('input[name="showCounterexample"]')
1632 .addEventListener("change", drawArrows
);
1634 .querySelector('input[name="showArrows"]')
1635 .addEventListener("change", toggleArrows
);
1637 // Default highlighting for the last event.
1638 highlightArrowsForSelectedEvent();