[clang][bytecode][NFC] Only get expr when checking for UB (#125397)
[llvm-project.git] / llvm / tools / llvm-cov / SourceCoverageViewHTML.cpp
blobc94d3853fc0143f5bcbbe32f5df1eb053edaca40
1 //===- SourceCoverageViewHTML.cpp - A html code coverage view -------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 ///
9 /// \file This file implements the html coverage renderer.
10 ///
11 //===----------------------------------------------------------------------===//
13 #include "SourceCoverageViewHTML.h"
14 #include "CoverageReport.h"
15 #include "llvm/ADT/SmallString.h"
16 #include "llvm/ADT/StringExtras.h"
17 #include "llvm/Support/Format.h"
18 #include "llvm/Support/Path.h"
19 #include "llvm/Support/ThreadPool.h"
20 #include <optional>
22 using namespace llvm;
24 namespace {
26 // Return a string with the special characters in \p Str escaped.
27 std::string escape(StringRef Str, const CoverageViewOptions &Opts) {
28 std::string TabExpandedResult;
29 unsigned ColNum = 0; // Record the column number.
30 for (char C : Str) {
31 if (C == '\t') {
32 // Replace '\t' with up to TabSize spaces.
33 unsigned NumSpaces = Opts.TabSize - (ColNum % Opts.TabSize);
34 TabExpandedResult.append(NumSpaces, ' ');
35 ColNum += NumSpaces;
36 } else {
37 TabExpandedResult += C;
38 if (C == '\n' || C == '\r')
39 ColNum = 0;
40 else
41 ++ColNum;
44 std::string EscapedHTML;
46 raw_string_ostream OS{EscapedHTML};
47 printHTMLEscaped(TabExpandedResult, OS);
49 return EscapedHTML;
52 // Create a \p Name tag around \p Str, and optionally set its \p ClassName.
53 std::string tag(StringRef Name, StringRef Str, StringRef ClassName = "") {
54 std::string Tag = "<";
55 Tag += Name;
56 if (!ClassName.empty()) {
57 Tag += " class='";
58 Tag += ClassName;
59 Tag += "'";
61 Tag += ">";
62 Tag += Str;
63 Tag += "</";
64 Tag += Name;
65 Tag += ">";
66 return Tag;
69 // Create an anchor to \p Link with the label \p Str.
70 std::string a(StringRef Link, StringRef Str, StringRef TargetName = "") {
71 std::string Tag;
72 Tag += "<a ";
73 if (!TargetName.empty()) {
74 Tag += "name='";
75 Tag += TargetName;
76 Tag += "' ";
78 Tag += "href='";
79 Tag += Link;
80 Tag += "'>";
81 Tag += Str;
82 Tag += "</a>";
83 return Tag;
86 const char *BeginHeader =
87 "<head>"
88 "<meta name='viewport' content='width=device-width,initial-scale=1'>"
89 "<meta charset='UTF-8'>";
91 const char *JSForCoverage =
92 R"javascript(
93 function next_uncovered(selector, reverse, scroll_selector) {
94 function visit_element(element) {
95 element.classList.add("seen");
96 element.classList.add("selected");
98 if (!scroll_selector) {
99 scroll_selector = "tr:has(.selected) td.line-number"
102 const scroll_to = document.querySelector(scroll_selector);
103 if (scroll_to) {
104 scroll_to.scrollIntoView({behavior: "smooth", block: "center", inline: "end"});
108 function select_one() {
109 if (!reverse) {
110 const previously_selected = document.querySelector(".selected");
112 if (previously_selected) {
113 previously_selected.classList.remove("selected");
116 return document.querySelector(selector + ":not(.seen)");
117 } else {
118 const previously_selected = document.querySelector(".selected");
120 if (previously_selected) {
121 previously_selected.classList.remove("selected");
122 previously_selected.classList.remove("seen");
125 const nodes = document.querySelectorAll(selector + ".seen");
126 if (nodes) {
127 const last = nodes[nodes.length - 1]; // last
128 return last;
129 } else {
130 return undefined;
135 function reset_all() {
136 if (!reverse) {
137 const all_seen = document.querySelectorAll(selector + ".seen");
139 if (all_seen) {
140 all_seen.forEach(e => e.classList.remove("seen"));
142 } else {
143 const all_seen = document.querySelectorAll(selector + ":not(.seen)");
145 if (all_seen) {
146 all_seen.forEach(e => e.classList.add("seen"));
152 const uncovered = select_one();
154 if (uncovered) {
155 visit_element(uncovered);
156 } else {
157 reset_all();
159 const uncovered = select_one();
161 if (uncovered) {
162 visit_element(uncovered);
167 function next_line(reverse) {
168 next_uncovered("td.uncovered-line", reverse)
171 function next_region(reverse) {
172 next_uncovered("span.red.region", reverse);
175 function next_branch(reverse) {
176 next_uncovered("span.red.branch", reverse);
179 document.addEventListener("keypress", function(event) {
180 const reverse = event.shiftKey;
181 if (event.code == "KeyL") {
182 next_line(reverse);
184 if (event.code == "KeyB") {
185 next_branch(reverse);
187 if (event.code == "KeyR") {
188 next_region(reverse);
191 )javascript";
193 const char *CSSForCoverage =
194 R"(.red {
195 background-color: #f004;
197 .cyan {
198 background-color: cyan;
200 html {
201 scroll-behavior: smooth;
203 body {
204 font-family: -apple-system, sans-serif;
206 pre {
207 margin-top: 0px !important;
208 margin-bottom: 0px !important;
210 .source-name-title {
211 padding: 5px 10px;
212 border-bottom: 1px solid #8888;
213 background-color: #0002;
214 line-height: 35px;
216 .centered {
217 display: table;
218 margin-left: left;
219 margin-right: auto;
220 border: 1px solid #8888;
221 border-radius: 3px;
223 .expansion-view {
224 margin-left: 0px;
225 margin-top: 5px;
226 margin-right: 5px;
227 margin-bottom: 5px;
228 border: 1px solid #8888;
229 border-radius: 3px;
231 table {
232 border-collapse: collapse;
234 .light-row {
235 border: 1px solid #8888;
236 border-left: none;
237 border-right: none;
239 .light-row-bold {
240 border: 1px solid #8888;
241 border-left: none;
242 border-right: none;
243 font-weight: bold;
245 .column-entry {
246 text-align: left;
248 .column-entry-bold {
249 font-weight: bold;
250 text-align: left;
252 .column-entry-yellow {
253 text-align: left;
254 background-color: #ff06;
256 .column-entry-red {
257 text-align: left;
258 background-color: #f004;
260 .column-entry-gray {
261 text-align: left;
262 background-color: #fff4;
264 .column-entry-green {
265 text-align: left;
266 background-color: #0f04;
268 .line-number {
269 text-align: right;
271 .covered-line {
272 text-align: right;
273 color: #06d;
275 .uncovered-line {
276 text-align: right;
277 color: #d00;
279 .uncovered-line.selected {
280 color: #f00;
281 font-weight: bold;
283 .region.red.selected {
284 background-color: #f008;
285 font-weight: bold;
287 .branch.red.selected {
288 background-color: #f008;
289 font-weight: bold;
291 .tooltip {
292 position: relative;
293 display: inline;
294 background-color: #bef;
295 text-decoration: none;
297 .tooltip span.tooltip-content {
298 position: absolute;
299 width: 100px;
300 margin-left: -50px;
301 color: #FFFFFF;
302 background: #000000;
303 height: 30px;
304 line-height: 30px;
305 text-align: center;
306 visibility: hidden;
307 border-radius: 6px;
309 .tooltip span.tooltip-content:after {
310 content: '';
311 position: absolute;
312 top: 100%;
313 left: 50%;
314 margin-left: -8px;
315 width: 0; height: 0;
316 border-top: 8px solid #000000;
317 border-right: 8px solid transparent;
318 border-left: 8px solid transparent;
320 :hover.tooltip span.tooltip-content {
321 visibility: visible;
322 opacity: 0.8;
323 bottom: 30px;
324 left: 50%;
325 z-index: 999;
327 th, td {
328 vertical-align: top;
329 padding: 2px 8px;
330 border-collapse: collapse;
331 border-right: 1px solid #8888;
332 border-left: 1px solid #8888;
333 text-align: left;
335 td pre {
336 display: inline-block;
337 text-decoration: inherit;
339 td:first-child {
340 border-left: none;
342 td:last-child {
343 border-right: none;
345 tr:hover {
346 background-color: #eee;
348 tr:last-child {
349 border-bottom: none;
351 tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
352 background-color: #8884;
355 color: inherit;
357 .control {
358 position: fixed;
359 top: 0em;
360 right: 0em;
361 padding: 1em;
362 background: #FFF8;
364 @media (prefers-color-scheme: dark) {
365 body {
366 background-color: #222;
367 color: whitesmoke;
369 tr:hover {
370 background-color: #111;
372 .covered-line {
373 color: #39f;
375 .uncovered-line {
376 color: #f55;
378 .tooltip {
379 background-color: #068;
381 .control {
382 background: #2228;
384 tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
385 background-color: #8884;
390 const char *EndHeader = "</head>";
392 const char *BeginCenteredDiv = "<div class='centered'>";
394 const char *EndCenteredDiv = "</div>";
396 const char *BeginSourceNameDiv = "<div class='source-name-title'>";
398 const char *EndSourceNameDiv = "</div>";
400 const char *BeginCodeTD = "<td class='code'>";
402 const char *EndCodeTD = "</td>";
404 const char *BeginPre = "<pre>";
406 const char *EndPre = "</pre>";
408 const char *BeginExpansionDiv = "<div class='expansion-view'>";
410 const char *EndExpansionDiv = "</div>";
412 const char *BeginTable = "<table>";
414 const char *EndTable = "</table>";
416 const char *ProjectTitleTag = "h1";
418 const char *ReportTitleTag = "h2";
420 const char *CreatedTimeTag = "h4";
422 std::string getPathToStyle(StringRef ViewPath) {
423 std::string PathToStyle;
424 std::string PathSep = std::string(sys::path::get_separator());
425 unsigned NumSeps = ViewPath.count(PathSep);
426 for (unsigned I = 0, E = NumSeps; I < E; ++I)
427 PathToStyle += ".." + PathSep;
428 return PathToStyle + "style.css";
431 std::string getPathToJavaScript(StringRef ViewPath) {
432 std::string PathToJavaScript;
433 std::string PathSep = std::string(sys::path::get_separator());
434 unsigned NumSeps = ViewPath.count(PathSep);
435 for (unsigned I = 0, E = NumSeps; I < E; ++I)
436 PathToJavaScript += ".." + PathSep;
437 return PathToJavaScript + "control.js";
440 void emitPrelude(raw_ostream &OS, const CoverageViewOptions &Opts,
441 const std::string &PathToStyle = "",
442 const std::string &PathToJavaScript = "") {
443 OS << "<!doctype html>"
444 "<html>"
445 << BeginHeader;
447 // Link to a stylesheet if one is available. Otherwise, use the default style.
448 if (PathToStyle.empty())
449 OS << "<style>" << CSSForCoverage << "</style>";
450 else
451 OS << "<link rel='stylesheet' type='text/css' href='"
452 << escape(PathToStyle, Opts) << "'>";
454 // Link to a JavaScript if one is available
455 if (PathToJavaScript.empty())
456 OS << "<script>" << JSForCoverage << "</script>";
457 else
458 OS << "<script src='" << escape(PathToJavaScript, Opts) << "'></script>";
460 OS << EndHeader << "<body>";
463 void emitTableRow(raw_ostream &OS, const CoverageViewOptions &Opts,
464 const std::string &FirstCol, const FileCoverageSummary &FCS,
465 bool IsTotals) {
466 SmallVector<std::string, 8> Columns;
468 // Format a coverage triple and add the result to the list of columns.
469 auto AddCoverageTripleToColumn =
470 [&Columns, &Opts](unsigned Hit, unsigned Total, float Pctg) {
471 std::string S;
473 raw_string_ostream RSO{S};
474 if (Total)
475 RSO << format("%*.2f", 7, Pctg) << "% ";
476 else
477 RSO << "- ";
478 RSO << '(' << Hit << '/' << Total << ')';
480 const char *CellClass = "column-entry-yellow";
481 if (!Total)
482 CellClass = "column-entry-gray";
483 else if (Pctg >= Opts.HighCovWatermark)
484 CellClass = "column-entry-green";
485 else if (Pctg < Opts.LowCovWatermark)
486 CellClass = "column-entry-red";
487 Columns.emplace_back(tag("td", tag("pre", S), CellClass));
490 Columns.emplace_back(tag("td", tag("pre", FirstCol)));
491 AddCoverageTripleToColumn(FCS.FunctionCoverage.getExecuted(),
492 FCS.FunctionCoverage.getNumFunctions(),
493 FCS.FunctionCoverage.getPercentCovered());
494 if (Opts.ShowInstantiationSummary)
495 AddCoverageTripleToColumn(FCS.InstantiationCoverage.getExecuted(),
496 FCS.InstantiationCoverage.getNumFunctions(),
497 FCS.InstantiationCoverage.getPercentCovered());
498 AddCoverageTripleToColumn(FCS.LineCoverage.getCovered(),
499 FCS.LineCoverage.getNumLines(),
500 FCS.LineCoverage.getPercentCovered());
501 if (Opts.ShowRegionSummary)
502 AddCoverageTripleToColumn(FCS.RegionCoverage.getCovered(),
503 FCS.RegionCoverage.getNumRegions(),
504 FCS.RegionCoverage.getPercentCovered());
505 if (Opts.ShowBranchSummary)
506 AddCoverageTripleToColumn(FCS.BranchCoverage.getCovered(),
507 FCS.BranchCoverage.getNumBranches(),
508 FCS.BranchCoverage.getPercentCovered());
509 if (Opts.ShowMCDCSummary)
510 AddCoverageTripleToColumn(FCS.MCDCCoverage.getCoveredPairs(),
511 FCS.MCDCCoverage.getNumPairs(),
512 FCS.MCDCCoverage.getPercentCovered());
514 if (IsTotals)
515 OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row-bold");
516 else
517 OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row");
520 void emitEpilog(raw_ostream &OS) {
521 OS << "</body>"
522 << "</html>";
525 } // anonymous namespace
527 Expected<CoveragePrinter::OwnedStream>
528 CoveragePrinterHTML::createViewFile(StringRef Path, bool InToplevel) {
529 auto OSOrErr = createOutputStream(Path, "html", InToplevel);
530 if (!OSOrErr)
531 return OSOrErr;
533 OwnedStream OS = std::move(OSOrErr.get());
535 if (!Opts.hasOutputDirectory()) {
536 emitPrelude(*OS.get(), Opts);
537 } else {
538 std::string ViewPath = getOutputPath(Path, "html", InToplevel);
539 emitPrelude(*OS.get(), Opts, getPathToStyle(ViewPath),
540 getPathToJavaScript(ViewPath));
543 return std::move(OS);
546 void CoveragePrinterHTML::closeViewFile(OwnedStream OS) {
547 emitEpilog(*OS.get());
550 /// Emit column labels for the table in the index.
551 static void emitColumnLabelsForIndex(raw_ostream &OS,
552 const CoverageViewOptions &Opts) {
553 SmallVector<std::string, 4> Columns;
554 Columns.emplace_back(tag("td", "Filename", "column-entry-bold"));
555 Columns.emplace_back(tag("td", "Function Coverage", "column-entry-bold"));
556 if (Opts.ShowInstantiationSummary)
557 Columns.emplace_back(
558 tag("td", "Instantiation Coverage", "column-entry-bold"));
559 Columns.emplace_back(tag("td", "Line Coverage", "column-entry-bold"));
560 if (Opts.ShowRegionSummary)
561 Columns.emplace_back(tag("td", "Region Coverage", "column-entry-bold"));
562 if (Opts.ShowBranchSummary)
563 Columns.emplace_back(tag("td", "Branch Coverage", "column-entry-bold"));
564 if (Opts.ShowMCDCSummary)
565 Columns.emplace_back(tag("td", "MC/DC", "column-entry-bold"));
566 OS << tag("tr", join(Columns.begin(), Columns.end(), ""));
569 std::string
570 CoveragePrinterHTML::buildLinkToFile(StringRef SF,
571 const FileCoverageSummary &FCS) const {
572 SmallString<128> LinkTextStr(sys::path::relative_path(FCS.Name));
573 sys::path::remove_dots(LinkTextStr, /*remove_dot_dot=*/true);
574 sys::path::native(LinkTextStr);
575 std::string LinkText = escape(LinkTextStr, Opts);
576 std::string LinkTarget =
577 escape(getOutputPath(SF, "html", /*InToplevel=*/false), Opts);
578 return a(LinkTarget, LinkText);
581 Error CoveragePrinterHTML::emitStyleSheet() {
582 auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true);
583 if (Error E = CSSOrErr.takeError())
584 return E;
586 OwnedStream CSS = std::move(CSSOrErr.get());
587 CSS->operator<<(CSSForCoverage);
589 return Error::success();
592 Error CoveragePrinterHTML::emitJavaScript() {
593 auto JSOrErr = createOutputStream("control", "js", /*InToplevel=*/true);
594 if (Error E = JSOrErr.takeError())
595 return E;
597 OwnedStream JS = std::move(JSOrErr.get());
598 JS->operator<<(JSForCoverage);
600 return Error::success();
603 void CoveragePrinterHTML::emitReportHeader(raw_ostream &OSRef,
604 const std::string &Title) {
605 // Emit some basic information about the coverage report.
606 if (Opts.hasProjectTitle())
607 OSRef << tag(ProjectTitleTag, escape(Opts.ProjectTitle, Opts));
608 OSRef << tag(ReportTitleTag, Title);
609 if (Opts.hasCreatedTime())
610 OSRef << tag(CreatedTimeTag, escape(Opts.CreatedTimeStr, Opts));
612 // Emit a link to some documentation.
613 OSRef << tag("p", "Click " +
614 a("http://clang.llvm.org/docs/"
615 "SourceBasedCodeCoverage.html#interpreting-reports",
616 "here") +
617 " for information about interpreting this report.");
619 // Emit a table containing links to reports for each file in the covmapping.
620 // Exclude files which don't contain any regions.
621 OSRef << BeginCenteredDiv << BeginTable;
622 emitColumnLabelsForIndex(OSRef, Opts);
625 /// Render a file coverage summary (\p FCS) in a table row. If \p IsTotals is
626 /// false, link the summary to \p SF.
627 void CoveragePrinterHTML::emitFileSummary(raw_ostream &OS, StringRef SF,
628 const FileCoverageSummary &FCS,
629 bool IsTotals) const {
630 // Simplify the display file path, and wrap it in a link if requested.
631 std::string Filename;
632 if (IsTotals) {
633 Filename = std::string(SF);
634 } else {
635 Filename = buildLinkToFile(SF, FCS);
638 emitTableRow(OS, Opts, Filename, FCS, IsTotals);
641 Error CoveragePrinterHTML::createIndexFile(
642 ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
643 const CoverageFiltersMatchAll &Filters) {
644 // Emit the default stylesheet.
645 if (Error E = emitStyleSheet())
646 return E;
648 // Emit the JavaScript UI implementation
649 if (Error E = emitJavaScript())
650 return E;
652 // Emit a file index along with some coverage statistics.
653 auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
654 if (Error E = OSOrErr.takeError())
655 return E;
656 auto OS = std::move(OSOrErr.get());
657 raw_ostream &OSRef = *OS.get();
659 assert(Opts.hasOutputDirectory() && "No output directory for index file");
660 emitPrelude(OSRef, Opts, getPathToStyle(""), getPathToJavaScript(""));
662 emitReportHeader(OSRef, "Coverage Report");
664 FileCoverageSummary Totals("TOTALS");
665 auto FileReports = CoverageReport::prepareFileReports(
666 Coverage, Totals, SourceFiles, Opts, Filters);
667 bool EmptyFiles = false;
668 for (unsigned I = 0, E = FileReports.size(); I < E; ++I) {
669 if (FileReports[I].FunctionCoverage.getNumFunctions())
670 emitFileSummary(OSRef, SourceFiles[I], FileReports[I]);
671 else
672 EmptyFiles = true;
674 emitFileSummary(OSRef, "Totals", Totals, /*IsTotals=*/true);
675 OSRef << EndTable << EndCenteredDiv;
677 // Emit links to files which don't contain any functions. These are normally
678 // not very useful, but could be relevant for code which abuses the
679 // preprocessor.
680 if (EmptyFiles && Filters.empty()) {
681 OSRef << tag("p", "Files which contain no functions. (These "
682 "files contain code pulled into other files "
683 "by the preprocessor.)\n");
684 OSRef << BeginCenteredDiv << BeginTable;
685 for (unsigned I = 0, E = FileReports.size(); I < E; ++I)
686 if (!FileReports[I].FunctionCoverage.getNumFunctions()) {
687 std::string Link = buildLinkToFile(SourceFiles[I], FileReports[I]);
688 OSRef << tag("tr", tag("td", tag("pre", Link)), "light-row") << '\n';
690 OSRef << EndTable << EndCenteredDiv;
693 OSRef << tag("h5", escape(Opts.getLLVMVersionString(), Opts));
694 emitEpilog(OSRef);
696 return Error::success();
699 struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport {
700 CoveragePrinterHTMLDirectory &Printer;
702 Reporter(CoveragePrinterHTMLDirectory &Printer,
703 const coverage::CoverageMapping &Coverage,
704 const CoverageFiltersMatchAll &Filters)
705 : DirectoryCoverageReport(Printer.Opts, Coverage, Filters),
706 Printer(Printer) {}
708 Error generateSubDirectoryReport(SubFileReports &&SubFiles,
709 SubDirReports &&SubDirs,
710 FileCoverageSummary &&SubTotals) override {
711 auto &LCPath = SubTotals.Name;
712 assert(Options.hasOutputDirectory() &&
713 "No output directory for index file");
715 SmallString<128> OSPath = LCPath;
716 sys::path::append(OSPath, "index");
717 auto OSOrErr = Printer.createOutputStream(OSPath, "html",
718 /*InToplevel=*/false);
719 if (auto E = OSOrErr.takeError())
720 return E;
721 auto OS = std::move(OSOrErr.get());
722 raw_ostream &OSRef = *OS.get();
724 auto IndexHtmlPath = Printer.getOutputPath((LCPath + "index").str(), "html",
725 /*InToplevel=*/false);
726 emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath),
727 getPathToJavaScript(IndexHtmlPath));
729 auto NavLink = buildTitleLinks(LCPath);
730 Printer.emitReportHeader(OSRef, "Coverage Report (" + NavLink + ")");
732 std::vector<const FileCoverageSummary *> EmptyFiles;
734 // Make directories at the top of the table.
735 for (auto &&SubDir : SubDirs) {
736 auto &Report = SubDir.second.first;
737 if (!Report.FunctionCoverage.getNumFunctions())
738 EmptyFiles.push_back(&Report);
739 else
740 emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report,
741 /*IsTotals=*/false);
744 for (auto &&SubFile : SubFiles) {
745 auto &Report = SubFile.second;
746 if (!Report.FunctionCoverage.getNumFunctions())
747 EmptyFiles.push_back(&Report);
748 else
749 emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report,
750 /*IsTotals=*/false);
753 // Emit the totals row.
754 emitTableRow(OSRef, Options, "Totals", SubTotals, /*IsTotals=*/false);
755 OSRef << EndTable << EndCenteredDiv;
757 // Emit links to files which don't contain any functions. These are normally
758 // not very useful, but could be relevant for code which abuses the
759 // preprocessor.
760 if (!EmptyFiles.empty()) {
761 OSRef << tag("p", "Files which contain no functions. (These "
762 "files contain code pulled into other files "
763 "by the preprocessor.)\n");
764 OSRef << BeginCenteredDiv << BeginTable;
765 for (auto FCS : EmptyFiles) {
766 auto Link = buildRelLinkToFile(FCS->Name);
767 OSRef << tag("tr", tag("td", tag("pre", Link)), "light-row") << '\n';
769 OSRef << EndTable << EndCenteredDiv;
772 // Emit epilog.
773 OSRef << tag("h5", escape(Options.getLLVMVersionString(), Options));
774 emitEpilog(OSRef);
776 return Error::success();
779 /// Make a title with hyperlinks to the index.html files of each hierarchy
780 /// of the report.
781 std::string buildTitleLinks(StringRef LCPath) const {
782 // For each report level in LCPStack, extract the path component and
783 // calculate the number of "../" relative to current LCPath.
784 SmallVector<std::pair<SmallString<128>, unsigned>, 16> Components;
786 auto Iter = LCPStack.begin(), IterE = LCPStack.end();
787 SmallString<128> RootPath;
788 if (*Iter == 0) {
789 // If llvm-cov works on relative coverage mapping data, the LCP of
790 // all source file paths can be 0, which makes the title path empty.
791 // As we like adding a slash at the back of the path to indicate a
792 // directory, in this case, we use "." as the root path to make it
793 // not be confused with the root path "/".
794 RootPath = ".";
795 } else {
796 RootPath = LCPath.substr(0, *Iter);
797 sys::path::native(RootPath);
798 sys::path::remove_dots(RootPath, /*remove_dot_dot=*/true);
800 Components.emplace_back(std::move(RootPath), 0);
802 for (auto Last = *Iter; ++Iter != IterE; Last = *Iter) {
803 SmallString<128> SubPath = LCPath.substr(Last, *Iter - Last);
804 sys::path::native(SubPath);
805 sys::path::remove_dots(SubPath, /*remove_dot_dot=*/true);
806 auto Level = unsigned(SubPath.count(sys::path::get_separator())) + 1;
807 Components.back().second += Level;
808 Components.emplace_back(std::move(SubPath), Level);
811 // Then we make the title accroding to Components.
812 std::string S;
813 for (auto I = Components.begin(), E = Components.end();;) {
814 auto &Name = I->first;
815 if (++I == E) {
816 S += a("./index.html", Name);
817 S += sys::path::get_separator();
818 break;
821 SmallString<128> Link;
822 for (unsigned J = I->second; J > 0; --J)
823 Link += "../";
824 Link += "index.html";
825 S += a(Link, Name);
826 S += sys::path::get_separator();
828 return S;
831 std::string buildRelLinkToFile(StringRef RelPath) const {
832 SmallString<128> LinkTextStr(RelPath);
833 sys::path::native(LinkTextStr);
835 // remove_dots will remove trailing slash, so we need to check before it.
836 auto IsDir = LinkTextStr.ends_with(sys::path::get_separator());
837 sys::path::remove_dots(LinkTextStr, /*remove_dot_dot=*/true);
839 SmallString<128> LinkTargetStr(LinkTextStr);
840 if (IsDir) {
841 LinkTextStr += sys::path::get_separator();
842 sys::path::append(LinkTargetStr, "index.html");
843 } else {
844 LinkTargetStr += ".html";
847 auto LinkText = escape(LinkTextStr, Options);
848 auto LinkTarget = escape(LinkTargetStr, Options);
849 return a(LinkTarget, LinkText);
853 Error CoveragePrinterHTMLDirectory::createIndexFile(
854 ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
855 const CoverageFiltersMatchAll &Filters) {
856 // The createSubIndexFile function only works when SourceFiles is
857 // more than one. So we fallback to CoveragePrinterHTML when it is.
858 if (SourceFiles.size() <= 1)
859 return CoveragePrinterHTML::createIndexFile(SourceFiles, Coverage, Filters);
861 // Emit the default stylesheet.
862 if (Error E = emitStyleSheet())
863 return E;
865 // Emit the JavaScript UI implementation
866 if (Error E = emitJavaScript())
867 return E;
869 // Emit index files in every subdirectory.
870 Reporter Report(*this, Coverage, Filters);
871 auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles);
872 if (auto E = TotalsOrErr.takeError())
873 return E;
874 auto &LCPath = TotalsOrErr->Name;
876 // Emit the top level index file. Top level index file is just a redirection
877 // to the index file in the LCP directory.
878 auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
879 if (auto E = OSOrErr.takeError())
880 return E;
881 auto OS = std::move(OSOrErr.get());
882 auto LCPIndexFilePath =
883 getOutputPath((LCPath + "index").str(), "html", /*InToplevel=*/false);
884 *OS.get() << R"(<!DOCTYPE html>
885 <html>
886 <head>
887 <meta http-equiv="Refresh" content="0; url=')"
888 << LCPIndexFilePath << R"('" />
889 </head>
890 <body></body>
891 </html>
894 return Error::success();
897 void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) {
898 OS << BeginCenteredDiv << BeginTable;
901 void SourceCoverageViewHTML::renderViewFooter(raw_ostream &OS) {
902 OS << EndTable << EndCenteredDiv;
905 void SourceCoverageViewHTML::renderSourceName(raw_ostream &OS, bool WholeFile) {
906 OS << BeginSourceNameDiv << tag("pre", escape(getSourceName(), getOptions()))
907 << EndSourceNameDiv;
910 void SourceCoverageViewHTML::renderLinePrefix(raw_ostream &OS, unsigned) {
911 OS << "<tr>";
914 void SourceCoverageViewHTML::renderLineSuffix(raw_ostream &OS, unsigned) {
915 // If this view has sub-views, renderLine() cannot close the view's cell.
916 // Take care of it here, after all sub-views have been rendered.
917 if (hasSubViews())
918 OS << EndCodeTD;
919 OS << "</tr>";
922 void SourceCoverageViewHTML::renderViewDivider(raw_ostream &, unsigned) {
923 // The table-based output makes view dividers unnecessary.
926 void SourceCoverageViewHTML::renderLine(raw_ostream &OS, LineRef L,
927 const LineCoverageStats &LCS,
928 unsigned ExpansionCol, unsigned) {
929 StringRef Line = L.Line;
930 unsigned LineNo = L.LineNo;
932 // Steps for handling text-escaping, highlighting, and tooltip creation:
934 // 1. Split the line into N+1 snippets, where N = |Segments|. The first
935 // snippet starts from Col=1 and ends at the start of the first segment.
936 // The last snippet starts at the last mapped column in the line and ends
937 // at the end of the line. Both are required but may be empty.
939 SmallVector<std::string, 8> Snippets;
940 CoverageSegmentArray Segments = LCS.getLineSegments();
942 unsigned LCol = 1;
943 auto Snip = [&](unsigned Start, unsigned Len) {
944 Snippets.push_back(std::string(Line.substr(Start, Len)));
945 LCol += Len;
948 Snip(LCol - 1, Segments.empty() ? 0 : (Segments.front()->Col - 1));
950 for (unsigned I = 1, E = Segments.size(); I < E; ++I)
951 Snip(LCol - 1, Segments[I]->Col - LCol);
953 // |Line| + 1 is needed to avoid underflow when, e.g |Line| = 0 and LCol = 1.
954 Snip(LCol - 1, Line.size() + 1 - LCol);
956 // 2. Escape all of the snippets.
958 for (unsigned I = 0, E = Snippets.size(); I < E; ++I)
959 Snippets[I] = escape(Snippets[I], getOptions());
961 // 3. Use \p WrappedSegment to set the highlight for snippet 0. Use segment
962 // 1 to set the highlight for snippet 2, segment 2 to set the highlight for
963 // snippet 3, and so on.
965 std::optional<StringRef> Color;
966 SmallVector<std::pair<unsigned, unsigned>, 2> HighlightedRanges;
967 auto Highlight = [&](const std::string &Snippet, unsigned LC, unsigned RC) {
968 if (getOptions().Debug)
969 HighlightedRanges.emplace_back(LC, RC);
970 if (Snippet.empty())
971 return tag("span", Snippet, std::string(*Color));
972 else
973 return tag("span", Snippet, "region " + std::string(*Color));
976 auto CheckIfUncovered = [&](const CoverageSegment *S) {
977 return S && (!S->IsGapRegion || (Color && *Color == "red")) &&
978 S->HasCount && S->Count == 0;
981 if (CheckIfUncovered(LCS.getWrappedSegment())) {
982 Color = "red";
983 if (!Snippets[0].empty())
984 Snippets[0] = Highlight(Snippets[0], 1, 1 + Snippets[0].size());
987 for (unsigned I = 0, E = Segments.size(); I < E; ++I) {
988 const auto *CurSeg = Segments[I];
989 if (CheckIfUncovered(CurSeg))
990 Color = "red";
991 else if (CurSeg->Col == ExpansionCol)
992 Color = "cyan";
993 else
994 Color = std::nullopt;
996 if (Color)
997 Snippets[I + 1] = Highlight(Snippets[I + 1], CurSeg->Col,
998 CurSeg->Col + Snippets[I + 1].size());
1001 if (Color && Segments.empty())
1002 Snippets.back() = Highlight(Snippets.back(), 1, 1 + Snippets.back().size());
1004 if (getOptions().Debug) {
1005 for (const auto &Range : HighlightedRanges) {
1006 errs() << "Highlighted line " << LineNo << ", " << Range.first << " -> ";
1007 if (Range.second == 0)
1008 errs() << "?";
1009 else
1010 errs() << Range.second;
1011 errs() << "\n";
1015 // 4. Snippets[1:N+1] correspond to \p Segments[0:N]: use these to generate
1016 // sub-line region count tooltips if needed.
1018 if (shouldRenderRegionMarkers(LCS)) {
1019 // Just consider the segments which start *and* end on this line.
1020 for (unsigned I = 0, E = Segments.size() - 1; I < E; ++I) {
1021 const auto *CurSeg = Segments[I];
1022 auto CurSegCount = BinaryCount(CurSeg->Count);
1023 auto LCSCount = BinaryCount(LCS.getExecutionCount());
1024 if (!CurSeg->IsRegionEntry)
1025 continue;
1026 if (CurSegCount == LCSCount)
1027 continue;
1029 Snippets[I + 1] =
1030 tag("div",
1031 Snippets[I + 1] +
1032 tag("span", formatCount(CurSegCount), "tooltip-content"),
1033 "tooltip");
1035 if (getOptions().Debug)
1036 errs() << "Marker at " << CurSeg->Line << ":" << CurSeg->Col << " = "
1037 << formatCount(CurSegCount) << "\n";
1041 OS << BeginCodeTD;
1042 OS << BeginPre;
1043 for (const auto &Snippet : Snippets)
1044 OS << Snippet;
1045 OS << EndPre;
1047 // If there are no sub-views left to attach to this cell, end the cell.
1048 // Otherwise, end it after the sub-views are rendered (renderLineSuffix()).
1049 if (!hasSubViews())
1050 OS << EndCodeTD;
1053 void SourceCoverageViewHTML::renderLineCoverageColumn(
1054 raw_ostream &OS, const LineCoverageStats &Line) {
1055 std::string Count;
1056 if (Line.isMapped())
1057 Count = tag("pre", formatBinaryCount(Line.getExecutionCount()));
1058 std::string CoverageClass =
1059 (Line.getExecutionCount() > 0)
1060 ? "covered-line"
1061 : (Line.isMapped() ? "uncovered-line" : "skipped-line");
1062 OS << tag("td", Count, CoverageClass);
1065 void SourceCoverageViewHTML::renderLineNumberColumn(raw_ostream &OS,
1066 unsigned LineNo) {
1067 std::string LineNoStr = utostr(uint64_t(LineNo));
1068 std::string TargetName = "L" + LineNoStr;
1069 OS << tag("td", a("#" + TargetName, tag("pre", LineNoStr), TargetName),
1070 "line-number");
1073 void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream &,
1074 const LineCoverageStats &Line,
1075 unsigned) {
1076 // Region markers are rendered in-line using tooltips.
1079 void SourceCoverageViewHTML::renderExpansionSite(raw_ostream &OS, LineRef L,
1080 const LineCoverageStats &LCS,
1081 unsigned ExpansionCol,
1082 unsigned ViewDepth) {
1083 // Render the line containing the expansion site. No extra formatting needed.
1084 renderLine(OS, L, LCS, ExpansionCol, ViewDepth);
1087 void SourceCoverageViewHTML::renderExpansionView(raw_ostream &OS,
1088 ExpansionView &ESV,
1089 unsigned ViewDepth) {
1090 OS << BeginExpansionDiv;
1091 ESV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/false,
1092 /*ShowTitle=*/false, ViewDepth + 1);
1093 OS << EndExpansionDiv;
1096 void SourceCoverageViewHTML::renderBranchView(raw_ostream &OS, BranchView &BRV,
1097 unsigned ViewDepth) {
1098 // Render the child subview.
1099 if (getOptions().Debug)
1100 errs() << "Branch at line " << BRV.getLine() << '\n';
1102 auto BranchCount = [&](StringRef Label, uint64_t Count, bool Folded,
1103 double Total) {
1104 if (Folded)
1105 return std::string{"Folded"};
1107 std::string Str;
1108 raw_string_ostream OS(Str);
1110 OS << tag("span", Label, (Count ? "None" : "red branch")) << ": ";
1111 if (getOptions().ShowBranchCounts)
1112 OS << tag("span", formatBinaryCount(Count),
1113 (Count ? "covered-line" : "uncovered-line"));
1114 else
1115 OS << format("%0.2f", (Total != 0 ? 100.0 * Count / Total : 0.0)) << "%";
1117 return Str;
1120 OS << BeginExpansionDiv;
1121 OS << BeginPre;
1122 for (const auto &R : BRV.Regions) {
1123 // This can be `double` since it is only used as a denominator.
1124 // FIXME: It is still inaccurate if Count is greater than (1LL << 53).
1125 double Total =
1126 static_cast<double>(R.ExecutionCount) + R.FalseExecutionCount;
1128 // Display Line + Column.
1129 std::string LineNoStr = utostr(uint64_t(R.LineStart));
1130 std::string ColNoStr = utostr(uint64_t(R.ColumnStart));
1131 std::string TargetName = "L" + LineNoStr;
1133 OS << " Branch (";
1134 OS << tag("span",
1135 a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr),
1136 TargetName),
1137 "line-number") +
1138 "): [";
1140 if (R.TrueFolded && R.FalseFolded) {
1141 OS << "Folded - Ignored]\n";
1142 continue;
1145 OS << BranchCount("True", R.ExecutionCount, R.TrueFolded, Total) << ", "
1146 << BranchCount("False", R.FalseExecutionCount, R.FalseFolded, Total)
1147 << "]\n";
1149 OS << EndPre;
1150 OS << EndExpansionDiv;
1153 void SourceCoverageViewHTML::renderMCDCView(raw_ostream &OS, MCDCView &MRV,
1154 unsigned ViewDepth) {
1155 for (auto &Record : MRV.Records) {
1156 OS << BeginExpansionDiv;
1157 OS << BeginPre;
1158 OS << " MC/DC Decision Region (";
1160 // Display Line + Column information.
1161 const CounterMappingRegion &DecisionRegion = Record.getDecisionRegion();
1162 std::string LineNoStr = Twine(DecisionRegion.LineStart).str();
1163 std::string ColNoStr = Twine(DecisionRegion.ColumnStart).str();
1164 std::string TargetName = "L" + LineNoStr;
1165 OS << tag("span",
1166 a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr)),
1167 "line-number") +
1168 ") to (";
1169 LineNoStr = utostr(uint64_t(DecisionRegion.LineEnd));
1170 ColNoStr = utostr(uint64_t(DecisionRegion.ColumnEnd));
1171 OS << tag("span",
1172 a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr)),
1173 "line-number") +
1174 ")\n\n";
1176 // Display MC/DC Information.
1177 OS << " Number of Conditions: " << Record.getNumConditions() << "\n";
1178 for (unsigned i = 0; i < Record.getNumConditions(); i++) {
1179 OS << " " << Record.getConditionHeaderString(i);
1181 OS << "\n";
1182 OS << " Executed MC/DC Test Vectors:\n\n ";
1183 OS << Record.getTestVectorHeaderString();
1184 for (unsigned i = 0; i < Record.getNumTestVectors(); i++)
1185 OS << Record.getTestVectorString(i);
1186 OS << "\n";
1187 for (unsigned i = 0; i < Record.getNumConditions(); i++)
1188 OS << Record.getConditionCoverageString(i);
1189 OS << " MC/DC Coverage for Expression: ";
1190 OS << format("%0.2f", Record.getPercentCovered()) << "%\n";
1191 OS << EndPre;
1192 OS << EndExpansionDiv;
1196 void SourceCoverageViewHTML::renderInstantiationView(raw_ostream &OS,
1197 InstantiationView &ISV,
1198 unsigned ViewDepth) {
1199 OS << BeginExpansionDiv;
1200 if (!ISV.View)
1201 OS << BeginSourceNameDiv
1202 << tag("pre",
1203 escape("Unexecuted instantiation: " + ISV.FunctionName.str(),
1204 getOptions()))
1205 << EndSourceNameDiv;
1206 else
1207 ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true,
1208 /*ShowTitle=*/false, ViewDepth);
1209 OS << EndExpansionDiv;
1212 void SourceCoverageViewHTML::renderTitle(raw_ostream &OS, StringRef Title) {
1213 if (getOptions().hasProjectTitle())
1214 OS << tag(ProjectTitleTag, escape(getOptions().ProjectTitle, getOptions()));
1215 OS << tag(ReportTitleTag, escape(Title, getOptions()));
1216 if (getOptions().hasCreatedTime())
1217 OS << tag(CreatedTimeTag,
1218 escape(getOptions().CreatedTimeStr, getOptions()));
1220 OS << tag("span",
1221 a("javascript:next_line()", "next uncovered line (L)") + ", " +
1222 a("javascript:next_region()", "next uncovered region (R)") +
1223 ", " +
1224 a("javascript:next_branch()", "next uncovered branch (B)"),
1225 "control");
1228 void SourceCoverageViewHTML::renderTableHeader(raw_ostream &OS,
1229 unsigned ViewDepth) {
1230 std::string Links;
1232 renderLinePrefix(OS, ViewDepth);
1233 OS << tag("td", tag("pre", "Line")) << tag("td", tag("pre", "Count"));
1234 OS << tag("td", tag("pre", "Source" + Links));
1235 renderLineSuffix(OS, ViewDepth);