xref: /llvm-project/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp (revision 223521b13e7465bc177f43e22de526b777d6ff74)
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 //===----------------------------------------------------------------------===//
12 
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>
21 
22 using namespace llvm;
23 
24 namespace {
25 
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;
42     }
43   }
44   std::string EscapedHTML;
45   {
46     raw_string_ostream OS{EscapedHTML};
47     printHTMLEscaped(TabExpandedResult, OS);
48   }
49   return EscapedHTML;
50 }
51 
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 += "'";
60   }
61   Tag += ">";
62   Tag += Str;
63   Tag += "</";
64   Tag += Name;
65   Tag += ">";
66   return Tag;
67 }
68 
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 += "' ";
77   }
78   Tag += "href='";
79   Tag += Link;
80   Tag += "'>";
81   Tag += Str;
82   Tag += "</a>";
83   return Tag;
84 }
85 
86 const char *BeginHeader =
87     "<head>"
88     "<meta name='viewport' content='width=device-width,initial-scale=1'>"
89     "<meta charset='UTF-8'>";
90 
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");
97 
98     if (!scroll_selector) {
99       scroll_selector = "tr:has(.selected) td.line-number"
100     }
101 
102     const scroll_to = document.querySelector(scroll_selector);
103     if (scroll_to) {
104       scroll_to.scrollIntoView({behavior: "smooth", block: "center", inline: "end"});
105     }
106   }
107 
108   function select_one() {
109     if (!reverse) {
110       const previously_selected = document.querySelector(".selected");
111 
112       if (previously_selected) {
113         previously_selected.classList.remove("selected");
114       }
115 
116       return document.querySelector(selector + ":not(.seen)");
117     } else {
118       const previously_selected = document.querySelector(".selected");
119 
120       if (previously_selected) {
121         previously_selected.classList.remove("selected");
122         previously_selected.classList.remove("seen");
123       }
124 
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;
131       }
132     }
133   }
134 
135   function reset_all() {
136     if (!reverse) {
137       const all_seen = document.querySelectorAll(selector + ".seen");
138 
139       if (all_seen) {
140         all_seen.forEach(e => e.classList.remove("seen"));
141       }
142     } else {
143       const all_seen = document.querySelectorAll(selector + ":not(.seen)");
144 
145       if (all_seen) {
146         all_seen.forEach(e => e.classList.add("seen"));
147       }
148     }
149 
150   }
151 
152   const uncovered = select_one();
153 
154   if (uncovered) {
155     visit_element(uncovered);
156   } else {
157     reset_all();
158 
159     const uncovered = select_one();
160 
161     if (uncovered) {
162       visit_element(uncovered);
163     }
164   }
165 }
166 
167 function next_line(reverse) {
168   next_uncovered("td.uncovered-line", reverse)
169 }
170 
171 function next_region(reverse) {
172   next_uncovered("span.red.region", reverse);
173 }
174 
175 function next_branch(reverse) {
176   next_uncovered("span.red.branch", reverse);
177 }
178 
179 document.addEventListener("keypress", function(event) {
180   const reverse = event.shiftKey;
181   if (event.code == "KeyL") {
182     next_line(reverse);
183   }
184   if (event.code == "KeyB") {
185     next_branch(reverse);
186   }
187   if (event.code == "KeyR") {
188     next_region(reverse);
189   }
190 });
191 )javascript";
192 
193 const char *CSSForCoverage =
194     R"(.red {
195   background-color: #f004;
196 }
197 .cyan {
198   background-color: cyan;
199 }
200 html {
201   scroll-behavior: smooth;
202 }
203 body {
204   font-family: -apple-system, sans-serif;
205 }
206 pre {
207   margin-top: 0px !important;
208   margin-bottom: 0px !important;
209 }
210 .source-name-title {
211   padding: 5px 10px;
212   border-bottom: 1px solid #8888;
213   background-color: #0002;
214   line-height: 35px;
215 }
216 .centered {
217   display: table;
218   margin-left: left;
219   margin-right: auto;
220   border: 1px solid #8888;
221   border-radius: 3px;
222 }
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;
230 }
231 table {
232   border-collapse: collapse;
233 }
234 .light-row {
235   border: 1px solid #8888;
236   border-left: none;
237   border-right: none;
238 }
239 .light-row-bold {
240   border: 1px solid #8888;
241   border-left: none;
242   border-right: none;
243   font-weight: bold;
244 }
245 .column-entry {
246   text-align: left;
247 }
248 .column-entry-bold {
249   font-weight: bold;
250   text-align: left;
251 }
252 .column-entry-yellow {
253   text-align: left;
254   background-color: #ff06;
255 }
256 .column-entry-red {
257   text-align: left;
258   background-color: #f004;
259 }
260 .column-entry-gray {
261   text-align: left;
262   background-color: #fff4;
263 }
264 .column-entry-green {
265   text-align: left;
266   background-color: #0f04;
267 }
268 .line-number {
269   text-align: right;
270 }
271 .covered-line {
272   text-align: right;
273   color: #06d;
274 }
275 .uncovered-line {
276   text-align: right;
277   color: #d00;
278 }
279 .uncovered-line.selected {
280   color: #f00;
281   font-weight: bold;
282 }
283 .region.red.selected {
284   background-color: #f008;
285   font-weight: bold;
286 }
287 .branch.red.selected {
288   background-color: #f008;
289   font-weight: bold;
290 }
291 .tooltip {
292   position: relative;
293   display: inline;
294   background-color: #bef;
295   text-decoration: none;
296 }
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;
308 }
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;
319 }
320 :hover.tooltip span.tooltip-content {
321   visibility: visible;
322   opacity: 0.8;
323   bottom: 30px;
324   left: 50%;
325   z-index: 999;
326 }
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;
334 }
335 td pre {
336   display: inline-block;
337   text-decoration: inherit;
338 }
339 td:first-child {
340   border-left: none;
341 }
342 td:last-child {
343   border-right: none;
344 }
345 tr:hover {
346   background-color: #eee;
347 }
348 tr:last-child {
349   border-bottom: none;
350 }
351 tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
352   background-color: #8884;
353 }
354 a {
355   color: inherit;
356 }
357 .control {
358   position: fixed;
359   top: 0em;
360   right: 0em;
361   padding: 1em;
362   background: #FFF8;
363 }
364 @media (prefers-color-scheme: dark) {
365   body {
366     background-color: #222;
367     color: whitesmoke;
368   }
369   tr:hover {
370     background-color: #111;
371   }
372   .covered-line {
373     color: #39f;
374   }
375   .uncovered-line {
376     color: #f55;
377   }
378   .tooltip {
379     background-color: #068;
380   }
381   .control {
382     background: #2228;
383   }
384   tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) {
385     background-color: #8884;
386   }
387 }
388 )";
389 
390 const char *EndHeader = "</head>";
391 
392 const char *BeginCenteredDiv = "<div class='centered'>";
393 
394 const char *EndCenteredDiv = "</div>";
395 
396 const char *BeginSourceNameDiv = "<div class='source-name-title'>";
397 
398 const char *EndSourceNameDiv = "</div>";
399 
400 const char *BeginCodeTD = "<td class='code'>";
401 
402 const char *EndCodeTD = "</td>";
403 
404 const char *BeginPre = "<pre>";
405 
406 const char *EndPre = "</pre>";
407 
408 const char *BeginExpansionDiv = "<div class='expansion-view'>";
409 
410 const char *EndExpansionDiv = "</div>";
411 
412 const char *BeginTable = "<table>";
413 
414 const char *EndTable = "</table>";
415 
416 const char *ProjectTitleTag = "h1";
417 
418 const char *ReportTitleTag = "h2";
419 
420 const char *CreatedTimeTag = "h4";
421 
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";
429 }
430 
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";
438 }
439 
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;
446 
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) << "'>";
453 
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>";
459 
460   OS << EndHeader << "<body>";
461 }
462 
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;
467 
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;
472         {
473           raw_string_ostream RSO{S};
474           if (Total)
475             RSO << format("%*.2f", 7, Pctg) << "% ";
476           else
477             RSO << "- ";
478           RSO << '(' << Hit << '/' << Total << ')';
479         }
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));
488       };
489 
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());
513 
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");
518 }
519 
520 void emitEpilog(raw_ostream &OS) {
521   OS << "</body>"
522      << "</html>";
523 }
524 
525 } // anonymous namespace
526 
527 Expected<CoveragePrinter::OwnedStream>
528 CoveragePrinterHTML::createViewFile(StringRef Path, bool InToplevel) {
529   auto OSOrErr = createOutputStream(Path, "html", InToplevel);
530   if (!OSOrErr)
531     return OSOrErr;
532 
533   OwnedStream OS = std::move(OSOrErr.get());
534 
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));
541   }
542 
543   return std::move(OS);
544 }
545 
546 void CoveragePrinterHTML::closeViewFile(OwnedStream OS) {
547   emitEpilog(*OS.get());
548 }
549 
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(), ""));
567 }
568 
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);
579 }
580 
581 Error CoveragePrinterHTML::emitStyleSheet() {
582   auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true);
583   if (Error E = CSSOrErr.takeError())
584     return E;
585 
586   OwnedStream CSS = std::move(CSSOrErr.get());
587   CSS->operator<<(CSSForCoverage);
588 
589   return Error::success();
590 }
591 
592 Error CoveragePrinterHTML::emitJavaScript() {
593   auto JSOrErr = createOutputStream("control", "js", /*InToplevel=*/true);
594   if (Error E = JSOrErr.takeError())
595     return E;
596 
597   OwnedStream JS = std::move(JSOrErr.get());
598   JS->operator<<(JSForCoverage);
599 
600   return Error::success();
601 }
602 
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));
611 
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.");
618 
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);
623 }
624 
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);
636   }
637 
638   emitTableRow(OS, Opts, Filename, FCS, IsTotals);
639 }
640 
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;
647 
648   // Emit the JavaScript UI implementation
649   if (Error E = emitJavaScript())
650     return E;
651 
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();
658 
659   assert(Opts.hasOutputDirectory() && "No output directory for index file");
660   emitPrelude(OSRef, Opts, getPathToStyle(""), getPathToJavaScript(""));
661 
662   emitReportHeader(OSRef, "Coverage Report");
663 
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;
673   }
674   emitFileSummary(OSRef, "Totals", Totals, /*IsTotals=*/true);
675   OSRef << EndTable << EndCenteredDiv;
676 
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';
689       }
690     OSRef << EndTable << EndCenteredDiv;
691   }
692 
693   OSRef << tag("h5", escape(Opts.getLLVMVersionString(), Opts));
694   emitEpilog(OSRef);
695 
696   return Error::success();
697 }
698 
699 struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport {
700   CoveragePrinterHTMLDirectory &Printer;
701 
702   Reporter(CoveragePrinterHTMLDirectory &Printer,
703            const coverage::CoverageMapping &Coverage,
704            const CoverageFiltersMatchAll &Filters)
705       : DirectoryCoverageReport(Printer.Opts, Coverage, Filters),
706         Printer(Printer) {}
707 
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");
714 
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();
723 
724     auto IndexHtmlPath = Printer.getOutputPath((LCPath + "index").str(), "html",
725                                                /*InToplevel=*/false);
726     emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath),
727                 getPathToJavaScript(IndexHtmlPath));
728 
729     auto NavLink = buildTitleLinks(LCPath);
730     Printer.emitReportHeader(OSRef, "Coverage Report (" + NavLink + ")");
731 
732     std::vector<const FileCoverageSummary *> EmptyFiles;
733 
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);
742     }
743 
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);
751     }
752 
753     // Emit the totals row.
754     emitTableRow(OSRef, Options, "Totals", SubTotals, /*IsTotals=*/false);
755     OSRef << EndTable << EndCenteredDiv;
756 
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';
768       }
769       OSRef << EndTable << EndCenteredDiv;
770     }
771 
772     // Emit epilog.
773     OSRef << tag("h5", escape(Options.getLLVMVersionString(), Options));
774     emitEpilog(OSRef);
775 
776     return Error::success();
777   }
778 
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;
785 
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);
799     }
800     Components.emplace_back(std::move(RootPath), 0);
801 
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);
809     }
810 
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;
819       }
820 
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();
827     }
828     return S;
829   }
830 
831   std::string buildRelLinkToFile(StringRef RelPath) const {
832     SmallString<128> LinkTextStr(RelPath);
833     sys::path::native(LinkTextStr);
834 
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);
838 
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";
845     }
846 
847     auto LinkText = escape(LinkTextStr, Options);
848     auto LinkTarget = escape(LinkTargetStr, Options);
849     return a(LinkTarget, LinkText);
850   }
851 };
852 
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);
860 
861   // Emit the default stylesheet.
862   if (Error E = emitStyleSheet())
863     return E;
864 
865   // Emit the JavaScript UI implementation
866   if (Error E = emitJavaScript())
867     return E;
868 
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;
875 
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>
892   )";
893 
894   return Error::success();
895 }
896 
897 void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) {
898   OS << BeginCenteredDiv << BeginTable;
899 }
900 
901 void SourceCoverageViewHTML::renderViewFooter(raw_ostream &OS) {
902   OS << EndTable << EndCenteredDiv;
903 }
904 
905 void SourceCoverageViewHTML::renderSourceName(raw_ostream &OS, bool WholeFile) {
906   OS << BeginSourceNameDiv << tag("pre", escape(getSourceName(), getOptions()))
907      << EndSourceNameDiv;
908 }
909 
910 void SourceCoverageViewHTML::renderLinePrefix(raw_ostream &OS, unsigned) {
911   OS << "<tr>";
912 }
913 
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>";
920 }
921 
922 void SourceCoverageViewHTML::renderViewDivider(raw_ostream &, unsigned) {
923   // The table-based output makes view dividers unnecessary.
924 }
925 
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;
931 
932   // Steps for handling text-escaping, highlighting, and tooltip creation:
933   //
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.
938 
939   SmallVector<std::string, 8> Snippets;
940   CoverageSegmentArray Segments = LCS.getLineSegments();
941 
942   unsigned LCol = 1;
943   auto Snip = [&](unsigned Start, unsigned Len) {
944     Snippets.push_back(std::string(Line.substr(Start, Len)));
945     LCol += Len;
946   };
947 
948   Snip(LCol - 1, Segments.empty() ? 0 : (Segments.front()->Col - 1));
949 
950   for (unsigned I = 1, E = Segments.size(); I < E; ++I)
951     Snip(LCol - 1, Segments[I]->Col - LCol);
952 
953   // |Line| + 1 is needed to avoid underflow when, e.g |Line| = 0 and LCol = 1.
954   Snip(LCol - 1, Line.size() + 1 - LCol);
955 
956   // 2. Escape all of the snippets.
957 
958   for (unsigned I = 0, E = Snippets.size(); I < E; ++I)
959     Snippets[I] = escape(Snippets[I], getOptions());
960 
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.
964 
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));
974   };
975 
976   auto CheckIfUncovered = [&](const CoverageSegment *S) {
977     return S && (!S->IsGapRegion || (Color && *Color == "red")) &&
978            S->HasCount && S->Count == 0;
979   };
980 
981   if (CheckIfUncovered(LCS.getWrappedSegment())) {
982     Color = "red";
983     if (!Snippets[0].empty())
984       Snippets[0] = Highlight(Snippets[0], 1, 1 + Snippets[0].size());
985   }
986 
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;
995 
996     if (Color)
997       Snippets[I + 1] = Highlight(Snippets[I + 1], CurSeg->Col,
998                                   CurSeg->Col + Snippets[I + 1].size());
999   }
1000 
1001   if (Color && Segments.empty())
1002     Snippets.back() = Highlight(Snippets.back(), 1, 1 + Snippets.back().size());
1003 
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";
1012     }
1013   }
1014 
1015   // 4. Snippets[1:N+1] correspond to \p Segments[0:N]: use these to generate
1016   //    sub-line region count tooltips if needed.
1017 
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;
1028 
1029       Snippets[I + 1] =
1030           tag("div",
1031               Snippets[I + 1] +
1032                   tag("span", formatCount(CurSegCount), "tooltip-content"),
1033               "tooltip");
1034 
1035       if (getOptions().Debug)
1036         errs() << "Marker at " << CurSeg->Line << ":" << CurSeg->Col << " = "
1037                << formatCount(CurSegCount) << "\n";
1038     }
1039   }
1040 
1041   OS << BeginCodeTD;
1042   OS << BeginPre;
1043   for (const auto &Snippet : Snippets)
1044     OS << Snippet;
1045   OS << EndPre;
1046 
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;
1051 }
1052 
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);
1063 }
1064 
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");
1071 }
1072 
1073 void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream &,
1074                                                  const LineCoverageStats &Line,
1075                                                  unsigned) {
1076   // Region markers are rendered in-line using tooltips.
1077 }
1078 
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);
1085 }
1086 
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;
1094 }
1095 
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';
1101 
1102   auto BranchCount = [&](StringRef Label, uint64_t Count, bool Folded,
1103                          double Total) {
1104     if (Folded)
1105       return std::string{"Folded"};
1106 
1107     std::string Str;
1108     raw_string_ostream OS(Str);
1109 
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)) << "%";
1116 
1117     return Str;
1118   };
1119 
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;
1127 
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;
1132 
1133     OS << "  Branch (";
1134     OS << tag("span",
1135               a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr),
1136                 TargetName),
1137               "line-number") +
1138               "): [";
1139 
1140     if (R.TrueFolded && R.FalseFolded) {
1141       OS << "Folded - Ignored]\n";
1142       continue;
1143     }
1144 
1145     OS << BranchCount("True", R.ExecutionCount, R.TrueFolded, Total) << ", "
1146        << BranchCount("False", R.FalseExecutionCount, R.FalseFolded, Total)
1147        << "]\n";
1148   }
1149   OS << EndPre;
1150   OS << EndExpansionDiv;
1151 }
1152 
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 (";
1159 
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";
1175 
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);
1180     }
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;
1193   }
1194 }
1195 
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;
1210 }
1211 
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()));
1219 
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");
1226 }
1227 
1228 void SourceCoverageViewHTML::renderTableHeader(raw_ostream &OS,
1229                                                unsigned ViewDepth) {
1230   std::string Links;
1231 
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);
1236 }
1237