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/STLExtras.h"
31 #include "llvm/ADT/Sequence.h"
32 #include "llvm/ADT/SmallString.h"
33 #include "llvm/ADT/StringRef.h"
34 #include "llvm/ADT/iterator_range.h"
35 #include "llvm/Support/Casting.h"
36 #include "llvm/Support/Errc.h"
37 #include "llvm/Support/ErrorHandling.h"
38 #include "llvm/Support/FileSystem.h"
39 #include "llvm/Support/MemoryBuffer.h"
40 #include "llvm/Support/Path.h"
41 #include "llvm/Support/raw_ostream.h"
49 #include <system_error>
53 using namespace clang
;
56 //===----------------------------------------------------------------------===//
58 //===----------------------------------------------------------------------===//
64 class HTMLDiagnostics
: public PathDiagnosticConsumer
{
65 PathDiagnosticConsumerOptions DiagOpts
;
66 std::string Directory
;
67 bool createdDir
= false;
69 const Preprocessor
&PP
;
70 const bool SupportsCrossFileDiagnostics
;
73 HTMLDiagnostics(PathDiagnosticConsumerOptions DiagOpts
,
74 const std::string
&OutputDir
, const Preprocessor
&pp
,
75 bool supportsMultipleFiles
)
76 : DiagOpts(std::move(DiagOpts
)), Directory(OutputDir
), PP(pp
),
77 SupportsCrossFileDiagnostics(supportsMultipleFiles
) {}
79 ~HTMLDiagnostics() override
{ FlushDiagnostics(nullptr); }
81 void FlushDiagnosticsImpl(std::vector
<const PathDiagnostic
*> &Diags
,
82 FilesMade
*filesMade
) override
;
84 StringRef
getName() const override
{ return "HTMLDiagnostics"; }
86 bool supportsCrossFileDiagnostics() const override
{
87 return SupportsCrossFileDiagnostics
;
90 unsigned ProcessMacroPiece(raw_ostream
&os
, const PathDiagnosticMacroPiece
&P
,
93 unsigned ProcessControlFlowPiece(Rewriter
&R
, FileID BugFileID
,
94 const PathDiagnosticControlFlowPiece
&P
,
97 void HandlePiece(Rewriter
&R
, FileID BugFileID
, const PathDiagnosticPiece
&P
,
98 const std::vector
<SourceRange
> &PopUpRanges
, unsigned num
,
101 void HighlightRange(Rewriter
&R
, FileID BugFileID
, SourceRange Range
,
102 const char *HighlightStart
= "<span class=\"mrange\">",
103 const char *HighlightEnd
= "</span>");
105 void ReportDiag(const PathDiagnostic
&D
, FilesMade
*filesMade
);
107 // Generate the full HTML report
108 std::string
GenerateHTML(const PathDiagnostic
&D
, Rewriter
&R
,
109 const SourceManager
&SMgr
, const PathPieces
&path
,
110 const char *declName
);
112 // Add HTML header/footers to file specified by FID
113 void FinalizeHTML(const PathDiagnostic
&D
, Rewriter
&R
,
114 const SourceManager
&SMgr
, const PathPieces
&path
,
115 FileID FID
, FileEntryRef Entry
, const char *declName
);
117 // Rewrite the file specified by FID with HTML formatting.
118 void RewriteFile(Rewriter
&R
, const PathPieces
&path
, FileID FID
);
120 PathGenerationScheme
getGenerationScheme() const override
{
125 void addArrowSVGs(Rewriter
&R
, FileID BugFileID
,
126 const ArrowMap
&ArrowIndices
);
128 /// \return Javascript for displaying shortcuts help;
129 StringRef
showHelpJavascript();
131 /// \return Javascript for navigating the HTML report using j/k keys.
132 StringRef
generateKeyboardNavigationJavascript();
134 /// \return Javascript for drawing control-flow arrows.
135 StringRef
generateArrowDrawingJavascript();
137 /// \return JavaScript for an option to only show relevant lines.
138 std::string
showRelevantLinesJavascript(const PathDiagnostic
&D
,
139 const PathPieces
&path
);
141 /// Write executed lines from \p D in JSON format into \p os.
142 void dumpCoverageData(const PathDiagnostic
&D
, const PathPieces
&path
,
143 llvm::raw_string_ostream
&os
);
146 bool isArrowPiece(const PathDiagnosticPiece
&P
) {
147 return isa
<PathDiagnosticControlFlowPiece
>(P
) && P
.getString().empty();
150 unsigned getPathSizeWithoutArrows(const PathPieces
&Path
) {
151 unsigned TotalPieces
= Path
.size();
152 unsigned TotalArrowPieces
= llvm::count_if(
153 Path
, [](const PathDiagnosticPieceRef
&P
) { return isArrowPiece(*P
); });
154 return TotalPieces
- TotalArrowPieces
;
157 class ArrowMap
: public std::vector
<unsigned> {
158 using Base
= std::vector
<unsigned>;
161 ArrowMap(unsigned Size
) : Base(Size
, 0) {}
162 unsigned getTotalNumberOfArrows() const { return at(0); }
165 llvm::raw_ostream
&operator<<(llvm::raw_ostream
&OS
, const ArrowMap
&Indices
) {
167 llvm::interleave(Indices
, OS
, ",");
173 void ento::createHTMLDiagnosticConsumer(
174 PathDiagnosticConsumerOptions DiagOpts
, PathDiagnosticConsumers
&C
,
175 const std::string
&OutputDir
, const Preprocessor
&PP
,
176 const cross_tu::CrossTranslationUnitContext
&CTU
,
177 const MacroExpansionContext
&MacroExpansions
) {
179 // FIXME: HTML is currently our default output type, but if the output
180 // directory isn't specified, it acts like if it was in the minimal text
181 // output mode. This doesn't make much sense, we should have the minimal text
182 // as our default. In the case of backward compatibility concerns, this could
183 // be preserved with -analyzer-config-compatibility-mode=true.
184 createTextMinimalPathDiagnosticConsumer(DiagOpts
, C
, OutputDir
, PP
, CTU
,
187 // TODO: Emit an error here.
188 if (OutputDir
.empty())
191 C
.push_back(new HTMLDiagnostics(std::move(DiagOpts
), OutputDir
, PP
, true));
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
,
202 // TODO: Emit an error here.
203 if (OutputDir
.empty())
206 C
.push_back(new HTMLDiagnostics(std::move(DiagOpts
), OutputDir
, PP
, false));
209 void ento::createPlistHTMLDiagnosticConsumer(
210 PathDiagnosticConsumerOptions DiagOpts
, PathDiagnosticConsumers
&C
,
211 const std::string
&prefix
, const Preprocessor
&PP
,
212 const cross_tu::CrossTranslationUnitContext
&CTU
,
213 const MacroExpansionContext
&MacroExpansions
) {
214 createHTMLDiagnosticConsumer(
215 DiagOpts
, C
, std::string(llvm::sys::path::parent_path(prefix
)), PP
, CTU
,
217 createPlistMultiFileDiagnosticConsumer(DiagOpts
, C
, prefix
, PP
, CTU
,
219 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts
), C
, prefix
, PP
,
220 CTU
, MacroExpansions
);
223 void ento::createSarifHTMLDiagnosticConsumer(
224 PathDiagnosticConsumerOptions DiagOpts
, PathDiagnosticConsumers
&C
,
225 const std::string
&sarif_file
, const Preprocessor
&PP
,
226 const cross_tu::CrossTranslationUnitContext
&CTU
,
227 const MacroExpansionContext
&MacroExpansions
) {
228 createHTMLDiagnosticConsumer(
229 DiagOpts
, C
, std::string(llvm::sys::path::parent_path(sarif_file
)), PP
,
230 CTU
, MacroExpansions
);
231 createSarifDiagnosticConsumer(DiagOpts
, C
, sarif_file
, PP
, CTU
,
233 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts
), C
, sarif_file
,
234 PP
, CTU
, MacroExpansions
);
237 //===----------------------------------------------------------------------===//
238 // Report processing.
239 //===----------------------------------------------------------------------===//
241 void HTMLDiagnostics::FlushDiagnosticsImpl(
242 std::vector
<const PathDiagnostic
*> &Diags
,
243 FilesMade
*filesMade
) {
244 for (const auto Diag
: Diags
)
245 ReportDiag(*Diag
, filesMade
);
248 static llvm::SmallString
<32> getIssueHash(const PathDiagnostic
&D
,
249 const Preprocessor
&PP
) {
250 SourceManager
&SMgr
= PP
.getSourceManager();
251 PathDiagnosticLocation UPDLoc
= D
.getUniqueingLoc();
252 FullSourceLoc
L(SMgr
.getExpansionLoc(UPDLoc
.isValid()
253 ? UPDLoc
.asLocation()
254 : D
.getLocation().asLocation()),
256 return getIssueHash(L
, D
.getCheckerName(), D
.getBugType(),
257 D
.getDeclWithIssue(), PP
.getLangOpts());
260 void HTMLDiagnostics::ReportDiag(const PathDiagnostic
& D
,
261 FilesMade
*filesMade
) {
262 // Create the HTML directory if it is missing.
265 if (std::error_code ec
= llvm::sys::fs::create_directories(Directory
)) {
266 llvm::errs() << "warning: could not create directory '"
267 << Directory
<< "': " << ec
.message() << '\n';
276 // First flatten out the entire path to make it easier to use.
277 PathPieces path
= D
.path
.flatten(/*ShouldFlattenMacros=*/false);
279 // The path as already been prechecked that the path is non-empty.
280 assert(!path
.empty());
281 const SourceManager
&SMgr
= path
.front()->getLocation().getManager();
283 // Create a new rewriter to generate HTML.
284 Rewriter
R(const_cast<SourceManager
&>(SMgr
), PP
.getLangOpts());
286 // Get the function/method name
287 SmallString
<128> declName("unknown");
289 if (const Decl
*DeclWithIssue
= D
.getDeclWithIssue()) {
290 if (const auto *ND
= dyn_cast
<NamedDecl
>(DeclWithIssue
))
291 declName
= ND
->getDeclName().getAsString();
293 if (const Stmt
*Body
= DeclWithIssue
->getBody()) {
294 // Retrieve the relative position of the declaration which will be used
297 SMgr
.getExpansionLoc(path
.back()->getLocation().asLocation()),
299 FullSourceLoc
FunL(SMgr
.getExpansionLoc(Body
->getBeginLoc()), SMgr
);
300 offsetDecl
= L
.getExpansionLineNumber() - FunL
.getExpansionLineNumber();
304 std::string report
= GenerateHTML(D
, R
, SMgr
, path
, declName
.c_str());
305 if (report
.empty()) {
306 llvm::errs() << "warning: no diagnostics generated for main file.\n";
310 // Create a path for the target HTML file.
313 SmallString
<128> FileNameStr
;
314 llvm::raw_svector_ostream
FileName(FileNameStr
);
315 FileName
<< "report-";
317 // Historically, neither the stable report filename nor the unstable report
318 // filename were actually stable. That said, the stable report filename
319 // was more stable because it was mostly composed of information
320 // about the bug report instead of being completely random.
321 // Now both stable and unstable report filenames are in fact stable
322 // but the stable report filename is still more verbose.
323 if (DiagOpts
.ShouldWriteVerboseReportFilename
) {
324 // FIXME: This code relies on knowing what constitutes the issue hash.
325 // Otherwise deduplication won't work correctly.
327 path
.back()->getLocation().asLocation().getExpansionLoc().getFileID();
329 OptionalFileEntryRef Entry
= SMgr
.getFileEntryRefForID(ReportFile
);
331 FileName
<< llvm::sys::path::filename(Entry
->getName()).str() << "-"
332 << declName
.c_str() << "-" << offsetDecl
<< "-";
335 FileName
<< StringRef(getIssueHash(D
, PP
)).substr(0, 6).str() << ".html";
337 SmallString
<128> ResultPath
;
338 llvm::sys::path::append(ResultPath
, Directory
, FileName
.str());
339 if (std::error_code EC
= llvm::sys::fs::make_absolute(ResultPath
)) {
340 llvm::errs() << "warning: could not make '" << ResultPath
341 << "' absolute: " << EC
.message() << '\n';
345 if (std::error_code EC
= llvm::sys::fs::openFileForReadWrite(
346 ResultPath
, FD
, llvm::sys::fs::CD_CreateNew
,
347 llvm::sys::fs::OF_Text
)) {
348 // Existence of the file corresponds to the situation where a different
349 // Clang instance has emitted a bug report with the same issue hash.
350 // This is an entirely normal situation that does not deserve a warning,
351 // as apart from hash collisions this can happen because the reports
352 // are in fact similar enough to be considered duplicates of each other.
353 if (EC
!= llvm::errc::file_exists
) {
354 llvm::errs() << "warning: could not create file in '" << Directory
355 << "': " << EC
.message() << '\n';
360 llvm::raw_fd_ostream
os(FD
, true);
363 filesMade
->addDiagnostic(D
, getName(),
364 llvm::sys::path::filename(ResultPath
));
366 // Emit the HTML to disk.
370 std::string
HTMLDiagnostics::GenerateHTML(const PathDiagnostic
& D
, Rewriter
&R
,
371 const SourceManager
& SMgr
, const PathPieces
& path
, const char *declName
) {
372 // Rewrite source files as HTML for every new file the path crosses
373 std::vector
<FileID
> FileIDs
;
374 for (auto I
: path
) {
375 FileID FID
= I
->getLocation().asLocation().getExpansionLoc().getFileID();
376 if (llvm::is_contained(FileIDs
, FID
))
379 FileIDs
.push_back(FID
);
380 RewriteFile(R
, path
, FID
);
383 if (SupportsCrossFileDiagnostics
&& FileIDs
.size() > 1) {
384 // Prefix file names, anchor tags, and nav cursors to every file
385 for (auto I
= FileIDs
.begin(), E
= FileIDs
.end(); I
!= E
; I
++) {
387 llvm::raw_string_ostream
os(s
);
389 if (I
!= FileIDs
.begin())
390 os
<< "<hr class=divider>\n";
392 os
<< "<div id=File" << I
->getHashValue() << ">\n";
395 if (I
!= FileIDs
.begin())
396 os
<< "<div class=FileNav><a href=\"#File" << (I
- 1)->getHashValue()
397 << "\">←</a></div>";
399 os
<< "<h4 class=FileName>" << SMgr
.getFileEntryRefForID(*I
)->getName()
404 os
<< "<div class=FileNav><a href=\"#File" << (I
+ 1)->getHashValue()
405 << "\">→</a></div>";
409 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(*I
), os
.str());
412 // Append files to the main report file in the order they appear in the path
413 for (auto I
: llvm::drop_begin(FileIDs
)) {
415 llvm::raw_string_ostream
os(s
);
417 const RewriteBuffer
*Buf
= R
.getRewriteBufferFor(I
);
421 R
.InsertTextAfter(SMgr
.getLocForEndOfFile(FileIDs
[0]), os
.str());
425 const RewriteBuffer
*Buf
= R
.getRewriteBufferFor(FileIDs
[0]);
429 // Add CSS, header, and footer.
431 path
.back()->getLocation().asLocation().getExpansionLoc().getFileID();
432 OptionalFileEntryRef Entry
= SMgr
.getFileEntryRefForID(FID
);
433 FinalizeHTML(D
, R
, SMgr
, path
, FileIDs
[0], *Entry
, declName
);
436 llvm::raw_string_ostream
os(file
);
443 void HTMLDiagnostics::dumpCoverageData(
444 const PathDiagnostic
&D
,
445 const PathPieces
&path
,
446 llvm::raw_string_ostream
&os
) {
448 const FilesToLineNumsMap
&ExecutedLines
= D
.getExecutedLines();
450 os
<< "var relevant_lines = {";
451 for (auto I
= ExecutedLines
.begin(),
452 E
= ExecutedLines
.end(); I
!= E
; ++I
) {
453 if (I
!= ExecutedLines
.begin())
456 os
<< "\"" << I
->first
.getHashValue() << "\": {";
457 for (unsigned LineNo
: I
->second
) {
458 if (LineNo
!= *(I
->second
.begin()))
461 os
<< "\"" << LineNo
<< "\": 1";
469 std::string
HTMLDiagnostics::showRelevantLinesJavascript(
470 const PathDiagnostic
&D
, const PathPieces
&path
) {
472 llvm::raw_string_ostream
os(s
);
473 os
<< "<script type='text/javascript'>\n";
474 dumpCoverageData(D
, path
, os
);
477 var filterCounterexample = function (hide) {
478 var tables = document.getElementsByClassName("code
");
479 for (var t=0; t<tables.length; t++) {
480 var table = tables[t];
481 var file_id = table.getAttribute("data
-fileid
");
482 var lines_in_fid = relevant_lines[file_id];
486 var lines = table.getElementsByClassName("codeline
");
487 for (var i=0; i<lines.length; i++) {
489 var lineNo = el.getAttribute("data
-linenumber
");
490 if (!lines_in_fid[lineNo]) {
492 el.setAttribute("hidden
", "");
494 el.removeAttribute("hidden
");
501 window.addEventListener("keydown
", function (event) {
502 if (event.defaultPrevented) {
506 if (event.shiftKey && event.keyCode == 83) {
507 var checked = document.getElementsByName("showCounterexample
")[0].checked;
508 filterCounterexample(!checked);
509 document.getElementsByName("showCounterexample
")[0].click();
513 event.preventDefault();
516 document.addEventListener("DOMContentLoaded
", function() {
517 document.querySelector('input[name="showCounterexample
"]').onchange=
519 filterCounterexample(this.checked);
525 <input type="checkbox
" name="showCounterexample
" id="showCounterexample
" />
526 <label for="showCounterexample
">
527 Show only relevant lines
529 <input type="checkbox
" name="showArrows
"
530 id="showArrows
" style="margin
-left
: 10px
" />
531 <label for="showArrows
">
532 Show control flow arrows
540 void HTMLDiagnostics::FinalizeHTML(const PathDiagnostic
&D
, Rewriter
&R
,
541 const SourceManager
&SMgr
,
542 const PathPieces
&path
, FileID FID
,
543 FileEntryRef Entry
, const char *declName
) {
544 // This is a cludge; basically we want to append either the full
545 // working directory if we have no directory information. This is
546 // a work in progress.
548 llvm::SmallString
<0> DirName
;
550 if (llvm::sys::path::is_relative(Entry
.getName())) {
551 llvm::sys::fs::current_path(DirName
);
555 int LineNumber
= path
.back()->getLocation().asLocation().getExpansionLineNumber();
556 int ColumnNumber
= path
.back()->getLocation().asLocation().getExpansionColumnNumber();
558 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
), showHelpJavascript());
560 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
),
561 generateKeyboardNavigationJavascript());
563 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
),
564 generateArrowDrawingJavascript());
566 // Checkbox and javascript for filtering the output to the counterexample.
567 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
),
568 showRelevantLinesJavascript(D
, path
));
570 // Add the name of the file as an <h1> tag.
573 llvm::raw_string_ostream
os(s
);
575 os
<< "<!-- REPORTHEADER -->\n"
576 << "<h3>Bug Summary</h3>\n<table class=\"simpletable\">\n"
577 "<tr><td class=\"rowname\">File:</td><td>"
578 << html::EscapeText(DirName
)
579 << html::EscapeText(Entry
.getName())
580 << "</td></tr>\n<tr><td class=\"rowname\">Warning:</td><td>"
581 "<a href=\"#EndPath\">line "
586 << D
.getVerboseDescription() << "</td></tr>\n";
588 // The navigation across the extra notes pieces.
589 unsigned NumExtraPieces
= 0;
590 for (const auto &Piece
: path
) {
591 if (const auto *P
= dyn_cast
<PathDiagnosticNotePiece
>(Piece
.get())) {
593 P
->getLocation().asLocation().getExpansionLineNumber();
595 P
->getLocation().asLocation().getExpansionColumnNumber();
597 os
<< "<tr><td class=\"rowname\">Note:</td><td>"
598 << "<a href=\"#Note" << NumExtraPieces
<< "\">line "
599 << LineNumber
<< ", column " << ColumnNumber
<< "</a><br />"
600 << P
->getString() << "</td></tr>";
604 // Output any other meta data.
606 for (const std::string
&Metadata
:
607 llvm::make_range(D
.meta_begin(), D
.meta_end())) {
608 os
<< "<tr><td></td><td>" << html::EscapeText(Metadata
) << "</td></tr>\n";
613 <!-- REPORTSUMMARYEXTRA -->
614 <h3>Annotated Source Code</h3>
615 <p>Press <a href="#" onclick="toggleHelp(); return false;">'?'</a>
616 to see keyboard shortcuts</p>
617 <input type="checkbox" class="spoilerhider" id="showinvocation" />
618 <label for="showinvocation" >Show analyzer invocation</label>
619 <div class="spoiler">clang -cc1 )<<<";
620 os
<< html::EscapeText(DiagOpts
.ToolInvocation
);
623 <div id='tooltiphint' hidden="true">
624 <p>Keyboard shortcuts: </p>
626 <li>Use 'j/k' keys for keyboard navigation</li>
627 <li>Use 'Shift+S' to show/hide relevant lines</li>
628 <li>Use '?' to toggle this window</li>
630 <a href="#" onclick="toggleHelp(); return false;">Close</a>
634 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
), os
.str());
637 // Embed meta-data tags.
640 llvm::raw_string_ostream
os(s
);
642 StringRef BugDesc
= D
.getVerboseDescription();
643 if (!BugDesc
.empty())
644 os
<< "\n<!-- BUGDESC " << BugDesc
<< " -->\n";
646 StringRef BugType
= D
.getBugType();
647 if (!BugType
.empty())
648 os
<< "\n<!-- BUGTYPE " << BugType
<< " -->\n";
650 PathDiagnosticLocation UPDLoc
= D
.getUniqueingLoc();
651 FullSourceLoc
L(SMgr
.getExpansionLoc(UPDLoc
.isValid()
652 ? UPDLoc
.asLocation()
653 : D
.getLocation().asLocation()),
656 StringRef BugCategory
= D
.getCategory();
657 if (!BugCategory
.empty())
658 os
<< "\n<!-- BUGCATEGORY " << BugCategory
<< " -->\n";
660 os
<< "\n<!-- BUGFILE " << DirName
<< Entry
.getName() << " -->\n";
662 os
<< "\n<!-- FILENAME " << llvm::sys::path::filename(Entry
.getName()) << " -->\n";
664 os
<< "\n<!-- FUNCTIONNAME " << declName
<< " -->\n";
666 os
<< "\n<!-- ISSUEHASHCONTENTOFLINEINCONTEXT " << getIssueHash(D
, PP
)
669 os
<< "\n<!-- BUGLINE "
673 os
<< "\n<!-- BUGCOLUMN "
677 os
<< "\n<!-- BUGPATHLENGTH " << getPathSizeWithoutArrows(path
) << " -->\n";
679 // Mark the end of the tags.
680 os
<< "\n<!-- BUGMETAEND -->\n";
683 R
.InsertTextBefore(SMgr
.getLocForStartOfFile(FID
), os
.str());
686 html::AddHeaderFooterInternalBuiltinCSS(R
, FID
, Entry
.getName());
689 StringRef
HTMLDiagnostics::showHelpJavascript() {
691 <script type='text/javascript'>
693 var toggleHelp = function() {
694 var hint = document.querySelector("#tooltiphint");
695 var attributeName = "hidden";
696 if (hint.hasAttribute(attributeName)) {
697 hint.removeAttribute(attributeName);
699 hint.setAttribute("hidden", "true");
702 window.addEventListener("keydown", function (event) {
703 if (event.defaultPrevented) {
706 if (event.key == "?") {
711 event.preventDefault();
717 static bool shouldDisplayPopUpRange(const SourceRange
&Range
) {
718 return !(Range
.getBegin().isMacroID() || Range
.getEnd().isMacroID());
722 HandlePopUpPieceStartTag(Rewriter
&R
,
723 const std::vector
<SourceRange
> &PopUpRanges
) {
724 for (const auto &Range
: PopUpRanges
) {
725 if (!shouldDisplayPopUpRange(Range
))
728 html::HighlightRange(R
, Range
.getBegin(), Range
.getEnd(), "",
729 "<table class='variable_popup'><tbody>",
730 /*IsTokenRange=*/true);
734 static void HandlePopUpPieceEndTag(Rewriter
&R
,
735 const PathDiagnosticPopUpPiece
&Piece
,
736 std::vector
<SourceRange
> &PopUpRanges
,
737 unsigned int LastReportedPieceIndex
,
738 unsigned int PopUpPieceIndex
) {
739 SmallString
<256> Buf
;
740 llvm::raw_svector_ostream
Out(Buf
);
742 SourceRange
Range(Piece
.getLocation().asRange());
743 if (!shouldDisplayPopUpRange(Range
))
746 // Write out the path indices with a right arrow and the message as a row.
747 Out
<< "<tr><td valign='top'><div class='PathIndex PathIndexPopUp'>"
748 << LastReportedPieceIndex
;
750 // Also annotate the state transition with extra indices.
751 Out
<< '.' << PopUpPieceIndex
;
753 Out
<< "</div></td><td>" << Piece
.getString() << "</td></tr>";
755 // If no report made at this range mark the variable and add the end tags.
756 if (!llvm::is_contained(PopUpRanges
, Range
)) {
757 // Store that we create a report at this range.
758 PopUpRanges
.push_back(Range
);
760 Out
<< "</tbody></table></span>";
761 html::HighlightRange(R
, Range
.getBegin(), Range
.getEnd(),
762 "<span class='variable'>", Buf
.c_str(),
763 /*IsTokenRange=*/true);
765 // Otherwise inject just the new row at the end of the range.
766 html::HighlightRange(R
, Range
.getBegin(), Range
.getEnd(), "", Buf
.c_str(),
767 /*IsTokenRange=*/true);
771 void HTMLDiagnostics::RewriteFile(Rewriter
&R
, const PathPieces
&path
,
775 // Maintain the counts of extra note pieces separately.
776 unsigned TotalPieces
= getPathSizeWithoutArrows(path
);
777 unsigned TotalNotePieces
=
778 llvm::count_if(path
, [](const PathDiagnosticPieceRef
&p
) {
779 return isa
<PathDiagnosticNotePiece
>(*p
);
781 unsigned PopUpPieceCount
=
782 llvm::count_if(path
, [](const PathDiagnosticPieceRef
&p
) {
783 return isa
<PathDiagnosticPopUpPiece
>(*p
);
786 unsigned TotalRegularPieces
= TotalPieces
- TotalNotePieces
- PopUpPieceCount
;
787 unsigned NumRegularPieces
= TotalRegularPieces
;
788 unsigned NumNotePieces
= TotalNotePieces
;
789 unsigned NumberOfArrows
= 0;
790 // Stores the count of the regular piece indices.
791 std::map
<int, int> IndexMap
;
792 ArrowMap
ArrowIndices(TotalRegularPieces
+ 1);
794 // Stores the different ranges where we have reported something.
795 std::vector
<SourceRange
> PopUpRanges
;
796 for (const PathDiagnosticPieceRef
&I
: llvm::reverse(path
)) {
797 const auto &Piece
= *I
.get();
799 if (isa
<PathDiagnosticPopUpPiece
>(Piece
)) {
800 ++IndexMap
[NumRegularPieces
];
801 } else if (isa
<PathDiagnosticNotePiece
>(Piece
)) {
802 // This adds diagnostic bubbles, but not navigation.
803 // Navigation through note pieces would be added later,
804 // as a separate pass through the piece list.
805 HandlePiece(R
, FID
, Piece
, PopUpRanges
, NumNotePieces
, TotalNotePieces
);
808 } else if (isArrowPiece(Piece
)) {
809 NumberOfArrows
= ProcessControlFlowPiece(
810 R
, FID
, cast
<PathDiagnosticControlFlowPiece
>(Piece
), NumberOfArrows
);
811 ArrowIndices
[NumRegularPieces
] = NumberOfArrows
;
814 HandlePiece(R
, FID
, Piece
, PopUpRanges
, NumRegularPieces
,
817 ArrowIndices
[NumRegularPieces
] = ArrowIndices
[NumRegularPieces
+ 1];
820 ArrowIndices
[0] = NumberOfArrows
;
822 // At this point ArrowIndices represent the following data structure:
823 // [a_0, a_1, ..., a_N]
824 // where N is the number of events in the path.
826 // Then for every event with index i \in [0, N - 1], we can say that
827 // arrows with indices \in [a_(i+1), a_i) correspond to that event.
828 // We can say that because arrows with these indices appeared in the
829 // path in between the i-th and the (i+1)-th events.
830 assert(ArrowIndices
.back() == 0 &&
831 "No arrows should be after the last event");
832 // This assertion also guarantees that all indices in are <= NumberOfArrows.
833 assert(llvm::is_sorted(ArrowIndices
, std::greater
<unsigned>()) &&
834 "Incorrect arrow indices map");
836 // Secondary indexing if we are having multiple pop-ups between two notes.
837 // (e.g. [(13) 'a' is 'true']; [(13.1) 'b' is 'false']; [(13.2) 'c' is...)
838 NumRegularPieces
= TotalRegularPieces
;
839 for (const PathDiagnosticPieceRef
&I
: llvm::reverse(path
)) {
840 const auto &Piece
= *I
.get();
842 if (const auto *PopUpP
= dyn_cast
<PathDiagnosticPopUpPiece
>(&Piece
)) {
843 int PopUpPieceIndex
= IndexMap
[NumRegularPieces
];
845 // Pop-up pieces needs the index of the last reported piece and its count
846 // how many times we report to handle multiple reports on the same range.
847 // This marks the variable, adds the </table> end tag and the message
848 // (list element) as a row. The <table> start tag will be added after the
849 // rows has been written out. Note: It stores every different range.
850 HandlePopUpPieceEndTag(R
, *PopUpP
, PopUpRanges
, NumRegularPieces
,
853 if (PopUpPieceIndex
> 0)
854 --IndexMap
[NumRegularPieces
];
856 } else if (!isa
<PathDiagnosticNotePiece
>(Piece
) && !isArrowPiece(Piece
)) {
861 // Add the <table> start tag of pop-up pieces based on the stored ranges.
862 HandlePopUpPieceStartTag(R
, PopUpRanges
);
864 // Add line numbers, header, footer, etc.
865 html::EscapeText(R
, FID
);
866 html::AddLineNumbers(R
, FID
);
868 addArrowSVGs(R
, FID
, ArrowIndices
);
870 // If we have a preprocessor, relex the file and syntax highlight.
871 // We might not have a preprocessor if we come from a deserialized AST file,
873 html::SyntaxHighlight(R
, FID
, PP
);
874 html::HighlightMacros(R
, FID
, PP
);
877 void HTMLDiagnostics::HandlePiece(Rewriter
&R
, FileID BugFileID
,
878 const PathDiagnosticPiece
&P
,
879 const std::vector
<SourceRange
> &PopUpRanges
,
880 unsigned num
, unsigned max
) {
881 // For now, just draw a box above the line in question, and emit the
883 FullSourceLoc Pos
= P
.getLocation().asLocation();
888 SourceManager
&SM
= R
.getSourceMgr();
889 assert(&Pos
.getManager() == &SM
&& "SourceManagers are different!");
890 std::pair
<FileID
, unsigned> LPosInfo
= SM
.getDecomposedExpansionLoc(Pos
);
892 if (LPosInfo
.first
!= BugFileID
)
895 llvm::MemoryBufferRef Buf
= SM
.getBufferOrFake(LPosInfo
.first
);
896 const char *FileStart
= Buf
.getBufferStart();
898 // Compute the column number. Rewind from the current position to the start
900 unsigned ColNo
= SM
.getColumnNumber(LPosInfo
.first
, LPosInfo
.second
);
901 const char *TokInstantiationPtr
=Pos
.getExpansionLoc().getCharacterData();
902 const char *LineStart
= TokInstantiationPtr
-ColNo
;
905 const char *LineEnd
= TokInstantiationPtr
;
906 const char *FileEnd
= Buf
.getBufferEnd();
907 while (*LineEnd
!= '\n' && LineEnd
!= FileEnd
)
910 // Compute the margin offset by counting tabs and non-tabs.
912 for (const char* c
= LineStart
; c
!= TokInstantiationPtr
; ++c
)
913 PosNo
+= *c
== '\t' ? 8 : 1;
915 // Create the html for the message.
917 const char *Kind
= nullptr;
919 bool SuppressIndex
= (max
== 1);
920 switch (P
.getKind()) {
921 case PathDiagnosticPiece::Event
: Kind
= "Event"; break;
922 case PathDiagnosticPiece::ControlFlow
: Kind
= "Control"; break;
923 // Setting Kind to "Control" is intentional.
924 case PathDiagnosticPiece::Macro
: Kind
= "Control"; break;
925 case PathDiagnosticPiece::Note
:
928 SuppressIndex
= true;
930 case PathDiagnosticPiece::Call
:
931 case PathDiagnosticPiece::PopUp
:
932 llvm_unreachable("Calls and extra notes should already be handled");
936 llvm::raw_string_ostream
os(sbuf
);
938 os
<< "\n<tr><td class=\"num\"></td><td class=\"line\"><div id=\"";
947 os
<< "\" class=\"msg";
949 os
<< " msg" << Kind
;
950 os
<< "\" style=\"margin-left:" << PosNo
<< "ex";
952 // Output a maximum size.
953 if (!isa
<PathDiagnosticMacroPiece
>(P
)) {
954 // Get the string and determining its maximum substring.
955 const auto &Msg
= P
.getString();
956 unsigned max_token
= 0;
958 unsigned len
= Msg
.size();
968 if (cnt
> max_token
) max_token
= cnt
;
975 // Determine the approximate size of the message bubble in em.
977 const unsigned max_line
= 120;
979 if (max_token
>= max_line
)
982 unsigned characters
= max_line
;
983 unsigned lines
= len
/ max_line
;
986 for (; characters
> max_token
; --characters
)
987 if (len
/ characters
> lines
) {
997 os
<< "; max-width:" << em
<< "em";
1000 os
<< "; max-width:100em";
1004 if (!SuppressIndex
) {
1005 os
<< "<table class=\"msgT\"><tr><td valign=\"top\">";
1006 os
<< "<div class=\"PathIndex";
1007 if (Kind
) os
<< " PathIndex" << Kind
;
1008 os
<< "\">" << num
<< "</div>";
1011 os
<< "</td><td><div class=\"PathNav\"><a href=\"#Path"
1013 << "\" title=\"Previous event ("
1015 << ")\">←</a></div>";
1021 if (const auto *MP
= dyn_cast
<PathDiagnosticMacroPiece
>(&P
)) {
1022 os
<< "Within the expansion of the macro '";
1024 // Get the name of the macro by relexing it.
1026 FullSourceLoc L
= MP
->getLocation().asLocation().getExpansionLoc();
1027 assert(L
.isFileID());
1028 StringRef BufferInfo
= L
.getBufferData();
1029 std::pair
<FileID
, unsigned> LocInfo
= L
.getDecomposedLoc();
1030 const char* MacroName
= LocInfo
.second
+ BufferInfo
.data();
1031 Lexer
rawLexer(SM
.getLocForStartOfFile(LocInfo
.first
), PP
.getLangOpts(),
1032 BufferInfo
.begin(), MacroName
, BufferInfo
.end());
1035 rawLexer
.LexFromRawLexer(TheTok
);
1036 for (unsigned i
= 0, n
= TheTok
.getLength(); i
< n
; ++i
)
1042 if (!SuppressIndex
) {
1045 os
<< "<td><div class=\"PathNav\"><a href=\"#";
1049 os
<< "Path" << (num
+ 1);
1050 os
<< "\" title=\"Next event ("
1052 << ")\">→</a></div></td>";
1055 os
<< "</tr></table>";
1058 // Within a macro piece. Write out each event.
1059 ProcessMacroPiece(os
, *MP
, 0);
1062 os
<< html::EscapeText(P
.getString());
1064 if (!SuppressIndex
) {
1067 os
<< "<td><div class=\"PathNav\"><a href=\"#";
1071 os
<< "Path" << (num
+ 1);
1072 os
<< "\" title=\"Next event ("
1074 << ")\">→</a></div></td>";
1077 os
<< "</tr></table>";
1081 os
<< "</div></td></tr>";
1083 // Insert the new html.
1084 unsigned DisplayPos
= LineEnd
- FileStart
;
1085 SourceLocation Loc
=
1086 SM
.getLocForStartOfFile(LPosInfo
.first
).getLocWithOffset(DisplayPos
);
1088 R
.InsertTextBefore(Loc
, os
.str());
1090 // Now highlight the ranges.
1091 ArrayRef
<SourceRange
> Ranges
= P
.getRanges();
1092 for (const auto &Range
: Ranges
) {
1093 // If we have already highlighted the range as a pop-up there is no work.
1094 if (llvm::is_contained(PopUpRanges
, Range
))
1097 HighlightRange(R
, LPosInfo
.first
, Range
);
1101 static void EmitAlphaCounter(raw_ostream
&os
, unsigned n
) {
1102 unsigned x
= n
% ('z' - 'a');
1106 EmitAlphaCounter(os
, n
);
1108 os
<< char('a' + x
);
1111 unsigned HTMLDiagnostics::ProcessMacroPiece(raw_ostream
&os
,
1112 const PathDiagnosticMacroPiece
& P
,
1114 for (const auto &subPiece
: P
.subPieces
) {
1115 if (const auto *MP
= dyn_cast
<PathDiagnosticMacroPiece
>(subPiece
.get())) {
1116 num
= ProcessMacroPiece(os
, *MP
, num
);
1120 if (const auto *EP
= dyn_cast
<PathDiagnosticEventPiece
>(subPiece
.get())) {
1121 os
<< "<div class=\"msg msgEvent\" style=\"width:94%; "
1122 "margin-left:5px\">"
1123 "<table class=\"msgT\"><tr>"
1124 "<td valign=\"top\"><div class=\"PathIndex PathIndexEvent\">";
1125 EmitAlphaCounter(os
, num
++);
1126 os
<< "</div></td><td valign=\"top\">"
1127 << html::EscapeText(EP
->getString())
1128 << "</td></tr></table></div>\n";
1135 void HTMLDiagnostics::addArrowSVGs(Rewriter
&R
, FileID BugFileID
,
1136 const ArrowMap
&ArrowIndices
) {
1138 llvm::raw_string_ostream
OS(S
);
1141 <style type="text
/css
">
1148 pointer-events: none;
1152 stroke-opacity: 0.2;
1154 marker-end: url(#arrowhead);
1158 stroke-opacity: 0.6;
1160 marker-end: url(#arrowheadSelected);
1170 <svg xmlns="http
://www.w3.org/2000/svg">
1172 <marker id
="arrowheadSelected" class="arrowhead" opacity
="0.6"
1173 viewBox
="0 0 10 10" refX
="3" refY
="5"
1174 markerWidth
="4" markerHeight
="4">
1175 <path d
="M 0 0 L 10 5 L 0 10 z" />
1177 <marker id
="arrowhead" class="arrowhead" opacity
="0.2"
1178 viewBox
="0 0 10 10" refX
="3" refY
="5"
1179 markerWidth
="4" markerHeight
="4">
1180 <path d
="M 0 0 L 10 5 L 0 10 z" />
1183 <g id
="arrows" fill
="none" stroke
="blue" visibility
="hidden">
1186 for (unsigned Index : llvm::seq(0u, ArrowIndices.getTotalNumberOfArrows())) {
1187 OS << " <path
class=\"arrow
\" id
=\"arrow
" << Index << "\"/>\n";
1193 <script type
='text/javascript'>
1194 const arrowIndices
= )<<<";
1196 OS << ArrowIndices << "\n</script
>\n";
1198 R.InsertTextBefore(R.getSourceMgr().getLocForStartOfFile(BugFileID),
1202 std::string getSpanBeginForControl(const char *ClassName, unsigned Index) {
1204 llvm::raw_string_ostream OS(Result);
1205 OS << "<span id
=\"" << ClassName << Index << "\">";
1209 std::string getSpanBeginForControlStart(unsigned Index) {
1210 return getSpanBeginForControl("start
", Index);
1213 std::string getSpanBeginForControlEnd(unsigned Index) {
1214 return getSpanBeginForControl("end
", Index);
1217 unsigned HTMLDiagnostics::ProcessControlFlowPiece(
1218 Rewriter &R, FileID BugFileID, const PathDiagnosticControlFlowPiece &P,
1220 for (const PathDiagnosticLocationPair &LPair : P) {
1221 std::string Start = getSpanBeginForControlStart(Number),
1222 End = getSpanBeginForControlEnd(Number++);
1224 HighlightRange(R, BugFileID, LPair.getStart().asRange().getBegin(),
1226 HighlightRange(R, BugFileID, LPair.getEnd().asRange().getBegin(),
1233 void HTMLDiagnostics::HighlightRange(Rewriter& R, FileID BugFileID,
1235 const char *HighlightStart,
1236 const char *HighlightEnd) {
1237 SourceManager &SM = R.getSourceMgr();
1238 const LangOptions &LangOpts = R.getLangOpts();
1240 SourceLocation InstantiationStart = SM.getExpansionLoc(Range.getBegin());
1241 unsigned StartLineNo = SM.getExpansionLineNumber(InstantiationStart);
1243 SourceLocation InstantiationEnd = SM.getExpansionLoc(Range.getEnd());
1244 unsigned EndLineNo = SM.getExpansionLineNumber(InstantiationEnd);
1246 if (EndLineNo < StartLineNo)
1249 if (SM.getFileID(InstantiationStart) != BugFileID ||
1250 SM.getFileID(InstantiationEnd) != BugFileID)
1253 // Compute the column number of the end.
1254 unsigned EndColNo = SM.getExpansionColumnNumber(InstantiationEnd);
1255 unsigned OldEndColNo = EndColNo;
1258 // Add in the length of the token, so that we cover multi-char tokens.
1259 EndColNo += Lexer::MeasureTokenLength(Range.getEnd(), SM, LangOpts)-1;
1262 // Highlight the range. Make the span tag the outermost tag for the
1266 InstantiationEnd.getLocWithOffset(EndColNo - OldEndColNo);
1268 html::HighlightRange(R, InstantiationStart, E, HighlightStart, HighlightEnd);
1271 StringRef HTMLDiagnostics::generateKeyboardNavigationJavascript() {
1273 <script type
='text/javascript'>
1274 var digitMatcher
= new RegExp("[0-9]+");
1276 var querySelectorAllArray
= function(selector
) {
1277 return Array
.prototype
.slice
.call(
1278 document
.querySelectorAll(selector
));
1281 document
.addEventListener("DOMContentLoaded", function() {
1282 querySelectorAllArray(".PathNav > a").forEach(
1283 function(currentValue
, currentIndex
) {
1284 var hrefValue
= currentValue
.getAttribute("href");
1285 currentValue
.onclick
= function() {
1286 scrollTo(document
.querySelector(hrefValue
));
1292 var findNum
= function() {
1293 var s
= document
.querySelector(".msg.selected");
1294 if (!s
|| s
.id
== "EndPath") {
1297 var out
= parseInt(digitMatcher
.exec(s
.id
)[0]);
1301 var classListAdd
= function(el
, theClass
) {
1302 if(!el
.className
.baseVal
)
1303 el
.className
+= " " + theClass
;
1305 el
.className
.baseVal
+= " " + theClass
;
1308 var classListRemove
= function(el
, theClass
) {
1309 var className
= (!el
.className
.baseVal
) ?
1310 el
.className
: el
.className
.baseVal
;
1311 className
= className
.replace(" " + theClass
, "");
1312 if(!el
.className
.baseVal
)
1313 el
.className
= className
;
1315 el
.className
.baseVal
= className
;
1318 var scrollTo
= function(el
) {
1319 querySelectorAllArray(".selected").forEach(function(s
) {
1320 classListRemove(s
, "selected");
1322 classListAdd(el
, "selected");
1323 window
.scrollBy(0, el
.getBoundingClientRect().top
-
1324 (window
.innerHeight
/ 2));
1325 highlightArrowsForSelectedEvent();
1328 var move
= function(num
, up
, numItems
) {
1329 if (num
== 1 && up
|| num
== numItems
- 1 && !up
) {
1331 } else if (num
== 0 && up
) {
1332 return numItems
- 1;
1333 } else if (num
== 0 && !up
) {
1334 return 1 % numItems
;
1336 return up
? num
- 1 : num
+ 1;
1339 var numToId
= function(num
) {
1341 return document
.getElementById("EndPath")
1343 return document
.getElementById("Path" + num
);
1346 var navigateTo
= function(up
) {
1347 var numItems
= document
.querySelectorAll(
1348 ".line > .msgEvent, .line > .msgControl").length
;
1349 var currentSelected
= findNum();
1350 var newSelected
= move(currentSelected
, up
, numItems
);
1351 var newEl
= numToId(newSelected
, numItems
);
1353 // Scroll element into center.
1357 window
.addEventListener("keydown", function (event
) {
1358 if (event
.defaultPrevented
) {
1362 if (event
.keyCode
== 74) {
1363 navigateTo(/*up=*/false);
1365 } else if (event
.keyCode
== 75) {
1366 navigateTo(/*up=*/true);
1370 event
.preventDefault();
1376 StringRef HTMLDiagnostics::generateArrowDrawingJavascript() {
1378 <script type
='text/javascript'>
1379 // Return range of numbers from a range [lower, upper).
1380 function
range(lower
, upper
) {
1382 for (var i
= lower
; i
<= upper
; ++i
) {
1388 var getRelatedArrowIndices
= function(pathId
) {
1389 // HTML numeration of events is a bit different than it is in the path.
1390 // Everything is rotated one step to the right, so the last element
1391 // (error diagnostic) has index 0.
1393 // arrowIndices has at least 2 elements
1394 pathId
= arrowIndices
.length
- 1;
1397 return range(arrowIndices
[pathId
], arrowIndices
[pathId
- 1]);
1400 var highlightArrowsForSelectedEvent
= function() {
1401 const selectedNum
= findNum();
1402 const arrowIndicesToHighlight
= getRelatedArrowIndices(selectedNum
);
1403 arrowIndicesToHighlight
.forEach((index
) => {
1404 var arrow
= document
.querySelector("#arrow" + index
);
1406 classListAdd(arrow
, "selected")
1411 var getAbsoluteBoundingRect
= function(element
) {
1412 const relative
= element
.getBoundingClientRect();
1414 left
: relative
.left
+ window
.pageXOffset
,
1415 right
: relative
.right
+ window
.pageXOffset
,
1416 top
: relative
.top
+ window
.pageYOffset
,
1417 bottom
: relative
.bottom
+ window
.pageYOffset
,
1418 height
: relative
.height
,
1419 width
: relative
.width
1423 var drawArrow
= function(index
) {
1424 // This function is based on the great answer from SO:
1425 // https://stackoverflow.com/a/39575674/11582326
1426 var start
= document
.querySelector("#start" + index
);
1427 var end
= document
.querySelector("#end" + index
);
1428 var arrow
= document
.querySelector("#arrow" + index
);
1430 var startRect
= getAbsoluteBoundingRect(start
);
1431 var endRect
= getAbsoluteBoundingRect(end
);
1433 // It is an arrow from a token to itself, no need to visualize it.
1434 if (startRect
.top
== endRect
.top
&&
1435 startRect
.left
== endRect
.left
)
1438 // Each arrow is a very simple Bézier curve, with two nodes and
1439 // two handles. So, we need to calculate four points in the window:
1441 var posStart
= { x
: 0, y
: 0 };
1443 var posEnd
= { x
: 0, y
: 0 };
1444 // * handle for the start node
1445 var startHandle
= { x
: 0, y
: 0 };
1446 // * handle for the end node
1447 var endHandle
= { x
: 0, y
: 0 };
1448 // One can visualize it as follows:
1464 // NOTE: (0, 0) is the top left corner of the window.
1466 // We have 3 similar, but still different scenarios to cover:
1468 // 1. Two tokens on different lines.
1473 // In this situation, we draw arrow on the left curving to the left.
1474 // 2. Two tokens on the same line, and the destination is on the right.
1479 // In this situation, we draw arrow above curving upwards.
1480 // 3. Two tokens on the same line, and the destination is on the left.
1484 // In this situation, we draw arrow below curving downwards.
1485 const onDifferentLines
= startRect
.top
<= endRect
.top
- 5 ||
1486 startRect
.top
>= endRect
.top
+ 5;
1487 const leftToRight
= startRect
.left
< endRect
.left
;
1489 // NOTE: various magic constants are chosen empirically for
1490 // better positioning and look
1491 if (onDifferentLines
) {
1493 const topToBottom
= startRect
.top
< endRect
.top
;
1494 posStart
.x
= startRect
.left
- 1;
1495 // We don't want to start it at the top left corner of the token,
1496 // it doesn't feel like this is where the arrow comes from.
1497 // For this reason, we start it in the middle of the left side
1499 posStart
.y
= startRect
.top
+ startRect
.height
/ 2;
1501 // End node has arrow head and we give it a bit more space.
1502 posEnd
.x
= endRect
.left
- 4;
1503 posEnd
.y
= endRect
.top
;
1505 // Utility object with x and y offsets for handles.
1507 // We want bottom-to-top arrow to curve a bit more, so it doesn't
1508 // overlap much with top-to-bottom curves (much more frequent).
1509 x
: topToBottom
? 15 : 25,
1510 y
: Math
.min((posEnd
.y
- posStart
.y
) / 3, 10)
1513 // When destination is on the different line, we can make a
1514 // curvier arrow because we have space for it.
1515 // So, instead of using
1517 // startHandle.x = posStart.x - curvature.x
1518 // endHandle.x = posEnd.x - curvature.x
1520 // We use the leftmost of these two values for both handles.
1521 startHandle
.x
= Math
.min(posStart
.x
, posEnd
.x
) - curvature
.x
;
1522 endHandle
.x
= startHandle
.x
;
1524 // Curving downwards from the start node...
1525 startHandle
.y
= posStart
.y
+ curvature
.y
;
1526 // ... and upwards from the end node.
1527 endHandle
.y
= posEnd
.y
- curvature
.y
;
1529 } else if (leftToRight
) {
1531 // Starting from the top right corner...
1532 posStart
.x
= startRect
.right
- 1;
1533 posStart
.y
= startRect
.top
;
1535 // ...and ending at the top left corner of the end token.
1536 posEnd
.x
= endRect
.left
+ 1;
1537 posEnd
.y
= endRect
.top
- 1;
1539 // Utility object with x and y offsets for handles.
1541 x
: Math
.min((posEnd
.x
- posStart
.x
) / 3, 15),
1545 // Curving to the right...
1546 startHandle
.x
= posStart
.x
+ curvature
.x
;
1547 // ... and upwards from the start node.
1548 startHandle
.y
= posStart
.y
- curvature
.y
;
1550 // And to the left...
1551 endHandle
.x
= posEnd
.x
- curvature
.x
;
1552 // ... and upwards from the end node.
1553 endHandle
.y
= posEnd
.y
- curvature
.y
;
1557 // Starting from the bottom right corner...
1558 posStart
.x
= startRect
.right
;
1559 posStart
.y
= startRect
.bottom
;
1561 // ...and ending also at the bottom right corner, but of the end token.
1562 posEnd
.x
= endRect
.right
- 1;
1563 posEnd
.y
= endRect
.bottom
+ 1;
1565 // Utility object with x and y offsets for handles.
1567 x
: Math
.min((posStart
.x
- posEnd
.x
) / 3, 15),
1571 // Curving to the left...
1572 startHandle
.x
= posStart
.x
- curvature
.x
;
1573 // ... and downwards from the start node.
1574 startHandle
.y
= posStart
.y
+ curvature
.y
;
1576 // And to the right...
1577 endHandle
.x
= posEnd
.x
+ curvature
.x
;
1578 // ... and downwards from the end node.
1579 endHandle
.y
= posEnd
.y
+ curvature
.y
;
1582 // Put it all together into a path.
1583 // More information on the format:
1584 // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
1585 var pathStr
= "M" + posStart
.x
+ "," + posStart
.y
+ " " +
1586 "C" + startHandle
.x
+ "," + startHandle
.y
+ " " +
1587 endHandle
.x
+ "," + endHandle
.y
+ " " +
1588 posEnd
.x
+ "," + posEnd
.y
;
1590 arrow
.setAttribute("d", pathStr
);
1593 var drawArrows
= function() {
1594 const numOfArrows
= document
.querySelectorAll("path[id^=arrow]").length
;
1595 for (var i
= 0; i
< numOfArrows
; ++i
) {
1600 var toggleArrows
= function(event
) {
1601 const arrows
= document
.querySelector("#arrows");
1602 if (event
.target
.checked
) {
1603 arrows
.setAttribute("visibility", "visible");
1605 arrows
.setAttribute("visibility", "hidden");
1609 window
.addEventListener("resize", drawArrows
);
1610 document
.addEventListener("DOMContentLoaded", function() {
1611 // Whenever we show invocation, locations change, i.e. we
1612 // need to redraw arrows.
1614 .querySelector('input[id="showinvocation"]')
1615 .addEventListener("click", drawArrows
);
1616 // Hiding irrelevant lines also should cause arrow rerender.
1618 .querySelector('input[name="showCounterexample"]')
1619 .addEventListener("change", drawArrows
);
1621 .querySelector('input[name="showArrows"]')
1622 .addEventListener("change", toggleArrows
);
1624 // Default highlighting for the last event.
1625 highlightArrowsForSelectedEvent();