xref: /freebsd/contrib/llvm-project/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp (revision 1db9f3b21e39176dd5b67cf8ac378633b172463e)
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 *CSSForCoverage =
92     R"(.red {
93   background-color: #ffd0d0;
94 }
95 .cyan {
96   background-color: cyan;
97 }
98 body {
99   font-family: -apple-system, sans-serif;
100 }
101 pre {
102   margin-top: 0px !important;
103   margin-bottom: 0px !important;
104 }
105 .source-name-title {
106   padding: 5px 10px;
107   border-bottom: 1px solid #dbdbdb;
108   background-color: #eee;
109   line-height: 35px;
110 }
111 .centered {
112   display: table;
113   margin-left: left;
114   margin-right: auto;
115   border: 1px solid #dbdbdb;
116   border-radius: 3px;
117 }
118 .expansion-view {
119   background-color: rgba(0, 0, 0, 0);
120   margin-left: 0px;
121   margin-top: 5px;
122   margin-right: 5px;
123   margin-bottom: 5px;
124   border: 1px solid #dbdbdb;
125   border-radius: 3px;
126 }
127 table {
128   border-collapse: collapse;
129 }
130 .light-row {
131   background: #ffffff;
132   border: 1px solid #dbdbdb;
133   border-left: none;
134   border-right: none;
135 }
136 .light-row-bold {
137   background: #ffffff;
138   border: 1px solid #dbdbdb;
139   border-left: none;
140   border-right: none;
141   font-weight: bold;
142 }
143 .column-entry {
144   text-align: left;
145 }
146 .column-entry-bold {
147   font-weight: bold;
148   text-align: left;
149 }
150 .column-entry-yellow {
151   text-align: left;
152   background-color: #ffffd0;
153 }
154 .column-entry-yellow:hover, tr:hover .column-entry-yellow {
155   background-color: #fffff0;
156 }
157 .column-entry-red {
158   text-align: left;
159   background-color: #ffd0d0;
160 }
161 .column-entry-red:hover, tr:hover .column-entry-red {
162   background-color: #fff0f0;
163 }
164 .column-entry-gray {
165   text-align: left;
166   background-color: #fbfbfb;
167 }
168 .column-entry-gray:hover, tr:hover .column-entry-gray {
169   background-color: #f0f0f0;
170 }
171 .column-entry-green {
172   text-align: left;
173   background-color: #d0ffd0;
174 }
175 .column-entry-green:hover, tr:hover .column-entry-green {
176   background-color: #f0fff0;
177 }
178 .line-number {
179   text-align: right;
180   color: #aaa;
181 }
182 .covered-line {
183   text-align: right;
184   color: #0080ff;
185 }
186 .uncovered-line {
187   text-align: right;
188   color: #ff3300;
189 }
190 .tooltip {
191   position: relative;
192   display: inline;
193   background-color: #b3e6ff;
194   text-decoration: none;
195 }
196 .tooltip span.tooltip-content {
197   position: absolute;
198   width: 100px;
199   margin-left: -50px;
200   color: #FFFFFF;
201   background: #000000;
202   height: 30px;
203   line-height: 30px;
204   text-align: center;
205   visibility: hidden;
206   border-radius: 6px;
207 }
208 .tooltip span.tooltip-content:after {
209   content: '';
210   position: absolute;
211   top: 100%;
212   left: 50%;
213   margin-left: -8px;
214   width: 0; height: 0;
215   border-top: 8px solid #000000;
216   border-right: 8px solid transparent;
217   border-left: 8px solid transparent;
218 }
219 :hover.tooltip span.tooltip-content {
220   visibility: visible;
221   opacity: 0.8;
222   bottom: 30px;
223   left: 50%;
224   z-index: 999;
225 }
226 th, td {
227   vertical-align: top;
228   padding: 2px 8px;
229   border-collapse: collapse;
230   border-right: solid 1px #eee;
231   border-left: solid 1px #eee;
232   text-align: left;
233 }
234 td pre {
235   display: inline-block;
236 }
237 td:first-child {
238   border-left: none;
239 }
240 td:last-child {
241   border-right: none;
242 }
243 tr:hover {
244   background-color: #f0f0f0;
245 }
246 tr:last-child {
247   border-bottom: none;
248 }
249 )";
250 
251 const char *EndHeader = "</head>";
252 
253 const char *BeginCenteredDiv = "<div class='centered'>";
254 
255 const char *EndCenteredDiv = "</div>";
256 
257 const char *BeginSourceNameDiv = "<div class='source-name-title'>";
258 
259 const char *EndSourceNameDiv = "</div>";
260 
261 const char *BeginCodeTD = "<td class='code'>";
262 
263 const char *EndCodeTD = "</td>";
264 
265 const char *BeginPre = "<pre>";
266 
267 const char *EndPre = "</pre>";
268 
269 const char *BeginExpansionDiv = "<div class='expansion-view'>";
270 
271 const char *EndExpansionDiv = "</div>";
272 
273 const char *BeginTable = "<table>";
274 
275 const char *EndTable = "</table>";
276 
277 const char *ProjectTitleTag = "h1";
278 
279 const char *ReportTitleTag = "h2";
280 
281 const char *CreatedTimeTag = "h4";
282 
283 std::string getPathToStyle(StringRef ViewPath) {
284   std::string PathToStyle;
285   std::string PathSep = std::string(sys::path::get_separator());
286   unsigned NumSeps = ViewPath.count(PathSep);
287   for (unsigned I = 0, E = NumSeps; I < E; ++I)
288     PathToStyle += ".." + PathSep;
289   return PathToStyle + "style.css";
290 }
291 
292 void emitPrelude(raw_ostream &OS, const CoverageViewOptions &Opts,
293                  const std::string &PathToStyle = "") {
294   OS << "<!doctype html>"
295         "<html>"
296      << BeginHeader;
297 
298   // Link to a stylesheet if one is available. Otherwise, use the default style.
299   if (PathToStyle.empty())
300     OS << "<style>" << CSSForCoverage << "</style>";
301   else
302     OS << "<link rel='stylesheet' type='text/css' href='"
303        << escape(PathToStyle, Opts) << "'>";
304 
305   OS << EndHeader << "<body>";
306 }
307 
308 void emitTableRow(raw_ostream &OS, const CoverageViewOptions &Opts,
309                   const std::string &FirstCol, const FileCoverageSummary &FCS,
310                   bool IsTotals) {
311   SmallVector<std::string, 8> Columns;
312 
313   // Format a coverage triple and add the result to the list of columns.
314   auto AddCoverageTripleToColumn =
315       [&Columns, &Opts](unsigned Hit, unsigned Total, float Pctg) {
316         std::string S;
317         {
318           raw_string_ostream RSO{S};
319           if (Total)
320             RSO << format("%*.2f", 7, Pctg) << "% ";
321           else
322             RSO << "- ";
323           RSO << '(' << Hit << '/' << Total << ')';
324         }
325         const char *CellClass = "column-entry-yellow";
326         if (!Total)
327           CellClass = "column-entry-gray";
328         else if (Pctg >= Opts.HighCovWatermark)
329           CellClass = "column-entry-green";
330         else if (Pctg < Opts.LowCovWatermark)
331           CellClass = "column-entry-red";
332         Columns.emplace_back(tag("td", tag("pre", S), CellClass));
333       };
334 
335   Columns.emplace_back(tag("td", tag("pre", FirstCol)));
336   AddCoverageTripleToColumn(FCS.FunctionCoverage.getExecuted(),
337                             FCS.FunctionCoverage.getNumFunctions(),
338                             FCS.FunctionCoverage.getPercentCovered());
339   if (Opts.ShowInstantiationSummary)
340     AddCoverageTripleToColumn(FCS.InstantiationCoverage.getExecuted(),
341                               FCS.InstantiationCoverage.getNumFunctions(),
342                               FCS.InstantiationCoverage.getPercentCovered());
343   AddCoverageTripleToColumn(FCS.LineCoverage.getCovered(),
344                             FCS.LineCoverage.getNumLines(),
345                             FCS.LineCoverage.getPercentCovered());
346   if (Opts.ShowRegionSummary)
347     AddCoverageTripleToColumn(FCS.RegionCoverage.getCovered(),
348                               FCS.RegionCoverage.getNumRegions(),
349                               FCS.RegionCoverage.getPercentCovered());
350   if (Opts.ShowBranchSummary)
351     AddCoverageTripleToColumn(FCS.BranchCoverage.getCovered(),
352                               FCS.BranchCoverage.getNumBranches(),
353                               FCS.BranchCoverage.getPercentCovered());
354   if (Opts.ShowMCDCSummary)
355     AddCoverageTripleToColumn(FCS.MCDCCoverage.getCoveredPairs(),
356                               FCS.MCDCCoverage.getNumPairs(),
357                               FCS.MCDCCoverage.getPercentCovered());
358 
359   if (IsTotals)
360     OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row-bold");
361   else
362     OS << tag("tr", join(Columns.begin(), Columns.end(), ""), "light-row");
363 }
364 
365 void emitEpilog(raw_ostream &OS) {
366   OS << "</body>"
367      << "</html>";
368 }
369 
370 } // anonymous namespace
371 
372 Expected<CoveragePrinter::OwnedStream>
373 CoveragePrinterHTML::createViewFile(StringRef Path, bool InToplevel) {
374   auto OSOrErr = createOutputStream(Path, "html", InToplevel);
375   if (!OSOrErr)
376     return OSOrErr;
377 
378   OwnedStream OS = std::move(OSOrErr.get());
379 
380   if (!Opts.hasOutputDirectory()) {
381     emitPrelude(*OS.get(), Opts);
382   } else {
383     std::string ViewPath = getOutputPath(Path, "html", InToplevel);
384     emitPrelude(*OS.get(), Opts, getPathToStyle(ViewPath));
385   }
386 
387   return std::move(OS);
388 }
389 
390 void CoveragePrinterHTML::closeViewFile(OwnedStream OS) {
391   emitEpilog(*OS.get());
392 }
393 
394 /// Emit column labels for the table in the index.
395 static void emitColumnLabelsForIndex(raw_ostream &OS,
396                                      const CoverageViewOptions &Opts) {
397   SmallVector<std::string, 4> Columns;
398   Columns.emplace_back(tag("td", "Filename", "column-entry-bold"));
399   Columns.emplace_back(tag("td", "Function Coverage", "column-entry-bold"));
400   if (Opts.ShowInstantiationSummary)
401     Columns.emplace_back(
402         tag("td", "Instantiation Coverage", "column-entry-bold"));
403   Columns.emplace_back(tag("td", "Line Coverage", "column-entry-bold"));
404   if (Opts.ShowRegionSummary)
405     Columns.emplace_back(tag("td", "Region Coverage", "column-entry-bold"));
406   if (Opts.ShowBranchSummary)
407     Columns.emplace_back(tag("td", "Branch Coverage", "column-entry-bold"));
408   if (Opts.ShowMCDCSummary)
409     Columns.emplace_back(tag("td", "MC/DC", "column-entry-bold"));
410   OS << tag("tr", join(Columns.begin(), Columns.end(), ""));
411 }
412 
413 std::string
414 CoveragePrinterHTML::buildLinkToFile(StringRef SF,
415                                      const FileCoverageSummary &FCS) const {
416   SmallString<128> LinkTextStr(sys::path::relative_path(FCS.Name));
417   sys::path::remove_dots(LinkTextStr, /*remove_dot_dot=*/true);
418   sys::path::native(LinkTextStr);
419   std::string LinkText = escape(LinkTextStr, Opts);
420   std::string LinkTarget =
421       escape(getOutputPath(SF, "html", /*InToplevel=*/false), Opts);
422   return a(LinkTarget, LinkText);
423 }
424 
425 Error CoveragePrinterHTML::emitStyleSheet() {
426   auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true);
427   if (Error E = CSSOrErr.takeError())
428     return E;
429 
430   OwnedStream CSS = std::move(CSSOrErr.get());
431   CSS->operator<<(CSSForCoverage);
432 
433   return Error::success();
434 }
435 
436 void CoveragePrinterHTML::emitReportHeader(raw_ostream &OSRef,
437                                            const std::string &Title) {
438   // Emit some basic information about the coverage report.
439   if (Opts.hasProjectTitle())
440     OSRef << tag(ProjectTitleTag, escape(Opts.ProjectTitle, Opts));
441   OSRef << tag(ReportTitleTag, Title);
442   if (Opts.hasCreatedTime())
443     OSRef << tag(CreatedTimeTag, escape(Opts.CreatedTimeStr, Opts));
444 
445   // Emit a link to some documentation.
446   OSRef << tag("p", "Click " +
447                         a("http://clang.llvm.org/docs/"
448                           "SourceBasedCodeCoverage.html#interpreting-reports",
449                           "here") +
450                         " for information about interpreting this report.");
451 
452   // Emit a table containing links to reports for each file in the covmapping.
453   // Exclude files which don't contain any regions.
454   OSRef << BeginCenteredDiv << BeginTable;
455   emitColumnLabelsForIndex(OSRef, Opts);
456 }
457 
458 /// Render a file coverage summary (\p FCS) in a table row. If \p IsTotals is
459 /// false, link the summary to \p SF.
460 void CoveragePrinterHTML::emitFileSummary(raw_ostream &OS, StringRef SF,
461                                           const FileCoverageSummary &FCS,
462                                           bool IsTotals) const {
463   // Simplify the display file path, and wrap it in a link if requested.
464   std::string Filename;
465   if (IsTotals) {
466     Filename = std::string(SF);
467   } else {
468     Filename = buildLinkToFile(SF, FCS);
469   }
470 
471   emitTableRow(OS, Opts, Filename, FCS, IsTotals);
472 }
473 
474 Error CoveragePrinterHTML::createIndexFile(
475     ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
476     const CoverageFiltersMatchAll &Filters) {
477   // Emit the default stylesheet.
478   if (Error E = emitStyleSheet())
479     return E;
480 
481   // Emit a file index along with some coverage statistics.
482   auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
483   if (Error E = OSOrErr.takeError())
484     return E;
485   auto OS = std::move(OSOrErr.get());
486   raw_ostream &OSRef = *OS.get();
487 
488   assert(Opts.hasOutputDirectory() && "No output directory for index file");
489   emitPrelude(OSRef, Opts, getPathToStyle(""));
490 
491   emitReportHeader(OSRef, "Coverage Report");
492 
493   FileCoverageSummary Totals("TOTALS");
494   auto FileReports = CoverageReport::prepareFileReports(
495       Coverage, Totals, SourceFiles, Opts, Filters);
496   bool EmptyFiles = false;
497   for (unsigned I = 0, E = FileReports.size(); I < E; ++I) {
498     if (FileReports[I].FunctionCoverage.getNumFunctions())
499       emitFileSummary(OSRef, SourceFiles[I], FileReports[I]);
500     else
501       EmptyFiles = true;
502   }
503   emitFileSummary(OSRef, "Totals", Totals, /*IsTotals=*/true);
504   OSRef << EndTable << EndCenteredDiv;
505 
506   // Emit links to files which don't contain any functions. These are normally
507   // not very useful, but could be relevant for code which abuses the
508   // preprocessor.
509   if (EmptyFiles && Filters.empty()) {
510     OSRef << tag("p", "Files which contain no functions. (These "
511                       "files contain code pulled into other files "
512                       "by the preprocessor.)\n");
513     OSRef << BeginCenteredDiv << BeginTable;
514     for (unsigned I = 0, E = FileReports.size(); I < E; ++I)
515       if (!FileReports[I].FunctionCoverage.getNumFunctions()) {
516         std::string Link = buildLinkToFile(SourceFiles[I], FileReports[I]);
517         OSRef << tag("tr", tag("td", tag("pre", Link)), "light-row") << '\n';
518       }
519     OSRef << EndTable << EndCenteredDiv;
520   }
521 
522   OSRef << tag("h5", escape(Opts.getLLVMVersionString(), Opts));
523   emitEpilog(OSRef);
524 
525   return Error::success();
526 }
527 
528 struct CoveragePrinterHTMLDirectory::Reporter : public DirectoryCoverageReport {
529   CoveragePrinterHTMLDirectory &Printer;
530 
531   Reporter(CoveragePrinterHTMLDirectory &Printer,
532            const coverage::CoverageMapping &Coverage,
533            const CoverageFiltersMatchAll &Filters)
534       : DirectoryCoverageReport(Printer.Opts, Coverage, Filters),
535         Printer(Printer) {}
536 
537   Error generateSubDirectoryReport(SubFileReports &&SubFiles,
538                                    SubDirReports &&SubDirs,
539                                    FileCoverageSummary &&SubTotals) override {
540     auto &LCPath = SubTotals.Name;
541     assert(Options.hasOutputDirectory() &&
542            "No output directory for index file");
543 
544     SmallString<128> OSPath = LCPath;
545     sys::path::append(OSPath, "index");
546     auto OSOrErr = Printer.createOutputStream(OSPath, "html",
547                                               /*InToplevel=*/false);
548     if (auto E = OSOrErr.takeError())
549       return E;
550     auto OS = std::move(OSOrErr.get());
551     raw_ostream &OSRef = *OS.get();
552 
553     auto IndexHtmlPath = Printer.getOutputPath((LCPath + "index").str(), "html",
554                                                /*InToplevel=*/false);
555     emitPrelude(OSRef, Options, getPathToStyle(IndexHtmlPath));
556 
557     auto NavLink = buildTitleLinks(LCPath);
558     Printer.emitReportHeader(OSRef, "Coverage Report (" + NavLink + ")");
559 
560     std::vector<const FileCoverageSummary *> EmptyFiles;
561 
562     // Make directories at the top of the table.
563     for (auto &&SubDir : SubDirs) {
564       auto &Report = SubDir.second.first;
565       if (!Report.FunctionCoverage.getNumFunctions())
566         EmptyFiles.push_back(&Report);
567       else
568         emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report,
569                      /*IsTotals=*/false);
570     }
571 
572     for (auto &&SubFile : SubFiles) {
573       auto &Report = SubFile.second;
574       if (!Report.FunctionCoverage.getNumFunctions())
575         EmptyFiles.push_back(&Report);
576       else
577         emitTableRow(OSRef, Options, buildRelLinkToFile(Report.Name), Report,
578                      /*IsTotals=*/false);
579     }
580 
581     // Emit the totals row.
582     emitTableRow(OSRef, Options, "Totals", SubTotals, /*IsTotals=*/false);
583     OSRef << EndTable << EndCenteredDiv;
584 
585     // Emit links to files which don't contain any functions. These are normally
586     // not very useful, but could be relevant for code which abuses the
587     // preprocessor.
588     if (!EmptyFiles.empty()) {
589       OSRef << tag("p", "Files which contain no functions. (These "
590                         "files contain code pulled into other files "
591                         "by the preprocessor.)\n");
592       OSRef << BeginCenteredDiv << BeginTable;
593       for (auto FCS : EmptyFiles) {
594         auto Link = buildRelLinkToFile(FCS->Name);
595         OSRef << tag("tr", tag("td", tag("pre", Link)), "light-row") << '\n';
596       }
597       OSRef << EndTable << EndCenteredDiv;
598     }
599 
600     // Emit epilog.
601     OSRef << tag("h5", escape(Options.getLLVMVersionString(), Options));
602     emitEpilog(OSRef);
603 
604     return Error::success();
605   }
606 
607   /// Make a title with hyperlinks to the index.html files of each hierarchy
608   /// of the report.
609   std::string buildTitleLinks(StringRef LCPath) const {
610     // For each report level in LCPStack, extract the path component and
611     // calculate the number of "../" relative to current LCPath.
612     SmallVector<std::pair<SmallString<128>, unsigned>, 16> Components;
613 
614     auto Iter = LCPStack.begin(), IterE = LCPStack.end();
615     SmallString<128> RootPath;
616     if (*Iter == 0) {
617       // If llvm-cov works on relative coverage mapping data, the LCP of
618       // all source file paths can be 0, which makes the title path empty.
619       // As we like adding a slash at the back of the path to indicate a
620       // directory, in this case, we use "." as the root path to make it
621       // not be confused with the root path "/".
622       RootPath = ".";
623     } else {
624       RootPath = LCPath.substr(0, *Iter);
625       sys::path::native(RootPath);
626       sys::path::remove_dots(RootPath, /*remove_dot_dot=*/true);
627     }
628     Components.emplace_back(std::move(RootPath), 0);
629 
630     for (auto Last = *Iter; ++Iter != IterE; Last = *Iter) {
631       SmallString<128> SubPath = LCPath.substr(Last, *Iter - Last);
632       sys::path::native(SubPath);
633       sys::path::remove_dots(SubPath, /*remove_dot_dot=*/true);
634       auto Level = unsigned(SubPath.count(sys::path::get_separator())) + 1;
635       Components.back().second += Level;
636       Components.emplace_back(std::move(SubPath), Level);
637     }
638 
639     // Then we make the title accroding to Components.
640     std::string S;
641     for (auto I = Components.begin(), E = Components.end();;) {
642       auto &Name = I->first;
643       if (++I == E) {
644         S += a("./index.html", Name);
645         S += sys::path::get_separator();
646         break;
647       }
648 
649       SmallString<128> Link;
650       for (unsigned J = I->second; J > 0; --J)
651         Link += "../";
652       Link += "index.html";
653       S += a(Link, Name);
654       S += sys::path::get_separator();
655     }
656     return S;
657   }
658 
659   std::string buildRelLinkToFile(StringRef RelPath) const {
660     SmallString<128> LinkTextStr(RelPath);
661     sys::path::native(LinkTextStr);
662 
663     // remove_dots will remove trailing slash, so we need to check before it.
664     auto IsDir = LinkTextStr.ends_with(sys::path::get_separator());
665     sys::path::remove_dots(LinkTextStr, /*remove_dot_dot=*/true);
666 
667     SmallString<128> LinkTargetStr(LinkTextStr);
668     if (IsDir) {
669       LinkTextStr += sys::path::get_separator();
670       sys::path::append(LinkTargetStr, "index.html");
671     } else {
672       LinkTargetStr += ".html";
673     }
674 
675     auto LinkText = escape(LinkTextStr, Options);
676     auto LinkTarget = escape(LinkTargetStr, Options);
677     return a(LinkTarget, LinkText);
678   }
679 };
680 
681 Error CoveragePrinterHTMLDirectory::createIndexFile(
682     ArrayRef<std::string> SourceFiles, const CoverageMapping &Coverage,
683     const CoverageFiltersMatchAll &Filters) {
684   // The createSubIndexFile function only works when SourceFiles is
685   // more than one. So we fallback to CoveragePrinterHTML when it is.
686   if (SourceFiles.size() <= 1)
687     return CoveragePrinterHTML::createIndexFile(SourceFiles, Coverage, Filters);
688 
689   // Emit the default stylesheet.
690   if (Error E = emitStyleSheet())
691     return E;
692 
693   // Emit index files in every subdirectory.
694   Reporter Report(*this, Coverage, Filters);
695   auto TotalsOrErr = Report.prepareDirectoryReports(SourceFiles);
696   if (auto E = TotalsOrErr.takeError())
697     return E;
698   auto &LCPath = TotalsOrErr->Name;
699 
700   // Emit the top level index file. Top level index file is just a redirection
701   // to the index file in the LCP directory.
702   auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
703   if (auto E = OSOrErr.takeError())
704     return E;
705   auto OS = std::move(OSOrErr.get());
706   auto LCPIndexFilePath =
707       getOutputPath((LCPath + "index").str(), "html", /*InToplevel=*/false);
708   *OS.get() << R"(<!DOCTYPE html>
709   <html>
710     <head>
711       <meta http-equiv="Refresh" content="0; url=')"
712             << LCPIndexFilePath << R"('" />
713     </head>
714     <body></body>
715   </html>
716   )";
717 
718   return Error::success();
719 }
720 
721 void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) {
722   OS << BeginCenteredDiv << BeginTable;
723 }
724 
725 void SourceCoverageViewHTML::renderViewFooter(raw_ostream &OS) {
726   OS << EndTable << EndCenteredDiv;
727 }
728 
729 void SourceCoverageViewHTML::renderSourceName(raw_ostream &OS, bool WholeFile) {
730   OS << BeginSourceNameDiv << tag("pre", escape(getSourceName(), getOptions()))
731      << EndSourceNameDiv;
732 }
733 
734 void SourceCoverageViewHTML::renderLinePrefix(raw_ostream &OS, unsigned) {
735   OS << "<tr>";
736 }
737 
738 void SourceCoverageViewHTML::renderLineSuffix(raw_ostream &OS, unsigned) {
739   // If this view has sub-views, renderLine() cannot close the view's cell.
740   // Take care of it here, after all sub-views have been rendered.
741   if (hasSubViews())
742     OS << EndCodeTD;
743   OS << "</tr>";
744 }
745 
746 void SourceCoverageViewHTML::renderViewDivider(raw_ostream &, unsigned) {
747   // The table-based output makes view dividers unnecessary.
748 }
749 
750 void SourceCoverageViewHTML::renderLine(raw_ostream &OS, LineRef L,
751                                         const LineCoverageStats &LCS,
752                                         unsigned ExpansionCol, unsigned) {
753   StringRef Line = L.Line;
754   unsigned LineNo = L.LineNo;
755 
756   // Steps for handling text-escaping, highlighting, and tooltip creation:
757   //
758   // 1. Split the line into N+1 snippets, where N = |Segments|. The first
759   //    snippet starts from Col=1 and ends at the start of the first segment.
760   //    The last snippet starts at the last mapped column in the line and ends
761   //    at the end of the line. Both are required but may be empty.
762 
763   SmallVector<std::string, 8> Snippets;
764   CoverageSegmentArray Segments = LCS.getLineSegments();
765 
766   unsigned LCol = 1;
767   auto Snip = [&](unsigned Start, unsigned Len) {
768     Snippets.push_back(std::string(Line.substr(Start, Len)));
769     LCol += Len;
770   };
771 
772   Snip(LCol - 1, Segments.empty() ? 0 : (Segments.front()->Col - 1));
773 
774   for (unsigned I = 1, E = Segments.size(); I < E; ++I)
775     Snip(LCol - 1, Segments[I]->Col - LCol);
776 
777   // |Line| + 1 is needed to avoid underflow when, e.g |Line| = 0 and LCol = 1.
778   Snip(LCol - 1, Line.size() + 1 - LCol);
779 
780   // 2. Escape all of the snippets.
781 
782   for (unsigned I = 0, E = Snippets.size(); I < E; ++I)
783     Snippets[I] = escape(Snippets[I], getOptions());
784 
785   // 3. Use \p WrappedSegment to set the highlight for snippet 0. Use segment
786   //    1 to set the highlight for snippet 2, segment 2 to set the highlight for
787   //    snippet 3, and so on.
788 
789   std::optional<StringRef> Color;
790   SmallVector<std::pair<unsigned, unsigned>, 2> HighlightedRanges;
791   auto Highlight = [&](const std::string &Snippet, unsigned LC, unsigned RC) {
792     if (getOptions().Debug)
793       HighlightedRanges.emplace_back(LC, RC);
794     return tag("span", Snippet, std::string(*Color));
795   };
796 
797   auto CheckIfUncovered = [&](const CoverageSegment *S) {
798     return S && (!S->IsGapRegion || (Color && *Color == "red")) &&
799            S->HasCount && S->Count == 0;
800   };
801 
802   if (CheckIfUncovered(LCS.getWrappedSegment())) {
803     Color = "red";
804     if (!Snippets[0].empty())
805       Snippets[0] = Highlight(Snippets[0], 1, 1 + Snippets[0].size());
806   }
807 
808   for (unsigned I = 0, E = Segments.size(); I < E; ++I) {
809     const auto *CurSeg = Segments[I];
810     if (CheckIfUncovered(CurSeg))
811       Color = "red";
812     else if (CurSeg->Col == ExpansionCol)
813       Color = "cyan";
814     else
815       Color = std::nullopt;
816 
817     if (Color)
818       Snippets[I + 1] = Highlight(Snippets[I + 1], CurSeg->Col,
819                                   CurSeg->Col + Snippets[I + 1].size());
820   }
821 
822   if (Color && Segments.empty())
823     Snippets.back() = Highlight(Snippets.back(), 1, 1 + Snippets.back().size());
824 
825   if (getOptions().Debug) {
826     for (const auto &Range : HighlightedRanges) {
827       errs() << "Highlighted line " << LineNo << ", " << Range.first << " -> ";
828       if (Range.second == 0)
829         errs() << "?";
830       else
831         errs() << Range.second;
832       errs() << "\n";
833     }
834   }
835 
836   // 4. Snippets[1:N+1] correspond to \p Segments[0:N]: use these to generate
837   //    sub-line region count tooltips if needed.
838 
839   if (shouldRenderRegionMarkers(LCS)) {
840     // Just consider the segments which start *and* end on this line.
841     for (unsigned I = 0, E = Segments.size() - 1; I < E; ++I) {
842       const auto *CurSeg = Segments[I];
843       if (!CurSeg->IsRegionEntry)
844         continue;
845       if (CurSeg->Count == LCS.getExecutionCount())
846         continue;
847 
848       Snippets[I + 1] =
849           tag("div", Snippets[I + 1] + tag("span", formatCount(CurSeg->Count),
850                                            "tooltip-content"),
851               "tooltip");
852 
853       if (getOptions().Debug)
854         errs() << "Marker at " << CurSeg->Line << ":" << CurSeg->Col << " = "
855                << formatCount(CurSeg->Count) << "\n";
856     }
857   }
858 
859   OS << BeginCodeTD;
860   OS << BeginPre;
861   for (const auto &Snippet : Snippets)
862     OS << Snippet;
863   OS << EndPre;
864 
865   // If there are no sub-views left to attach to this cell, end the cell.
866   // Otherwise, end it after the sub-views are rendered (renderLineSuffix()).
867   if (!hasSubViews())
868     OS << EndCodeTD;
869 }
870 
871 void SourceCoverageViewHTML::renderLineCoverageColumn(
872     raw_ostream &OS, const LineCoverageStats &Line) {
873   std::string Count;
874   if (Line.isMapped())
875     Count = tag("pre", formatCount(Line.getExecutionCount()));
876   std::string CoverageClass =
877       (Line.getExecutionCount() > 0) ? "covered-line" : "uncovered-line";
878   OS << tag("td", Count, CoverageClass);
879 }
880 
881 void SourceCoverageViewHTML::renderLineNumberColumn(raw_ostream &OS,
882                                                     unsigned LineNo) {
883   std::string LineNoStr = utostr(uint64_t(LineNo));
884   std::string TargetName = "L" + LineNoStr;
885   OS << tag("td", a("#" + TargetName, tag("pre", LineNoStr), TargetName),
886             "line-number");
887 }
888 
889 void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream &,
890                                                  const LineCoverageStats &Line,
891                                                  unsigned) {
892   // Region markers are rendered in-line using tooltips.
893 }
894 
895 void SourceCoverageViewHTML::renderExpansionSite(raw_ostream &OS, LineRef L,
896                                                  const LineCoverageStats &LCS,
897                                                  unsigned ExpansionCol,
898                                                  unsigned ViewDepth) {
899   // Render the line containing the expansion site. No extra formatting needed.
900   renderLine(OS, L, LCS, ExpansionCol, ViewDepth);
901 }
902 
903 void SourceCoverageViewHTML::renderExpansionView(raw_ostream &OS,
904                                                  ExpansionView &ESV,
905                                                  unsigned ViewDepth) {
906   OS << BeginExpansionDiv;
907   ESV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/false,
908                   /*ShowTitle=*/false, ViewDepth + 1);
909   OS << EndExpansionDiv;
910 }
911 
912 void SourceCoverageViewHTML::renderBranchView(raw_ostream &OS, BranchView &BRV,
913                                               unsigned ViewDepth) {
914   // Render the child subview.
915   if (getOptions().Debug)
916     errs() << "Branch at line " << BRV.getLine() << '\n';
917 
918   OS << BeginExpansionDiv;
919   OS << BeginPre;
920   for (const auto &R : BRV.Regions) {
921     // Calculate TruePercent and False Percent.
922     double TruePercent = 0.0;
923     double FalsePercent = 0.0;
924     // FIXME: It may overflow when the data is too large, but I have not
925     // encountered it in actual use, and not sure whether to use __uint128_t.
926     uint64_t Total = R.ExecutionCount + R.FalseExecutionCount;
927 
928     if (!getOptions().ShowBranchCounts && Total != 0) {
929       TruePercent = ((double)(R.ExecutionCount) / (double)Total) * 100.0;
930       FalsePercent = ((double)(R.FalseExecutionCount) / (double)Total) * 100.0;
931     }
932 
933     // Display Line + Column.
934     std::string LineNoStr = utostr(uint64_t(R.LineStart));
935     std::string ColNoStr = utostr(uint64_t(R.ColumnStart));
936     std::string TargetName = "L" + LineNoStr;
937 
938     OS << "  Branch (";
939     OS << tag("span",
940               a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr),
941                 TargetName),
942               "line-number") +
943               "): [";
944 
945     if (R.Folded) {
946       OS << "Folded - Ignored]\n";
947       continue;
948     }
949 
950     // Display TrueCount or TruePercent.
951     std::string TrueColor = R.ExecutionCount ? "None" : "red";
952     std::string TrueCovClass =
953         (R.ExecutionCount > 0) ? "covered-line" : "uncovered-line";
954 
955     OS << tag("span", "True", TrueColor);
956     OS << ": ";
957     if (getOptions().ShowBranchCounts)
958       OS << tag("span", formatCount(R.ExecutionCount), TrueCovClass) << ", ";
959     else
960       OS << format("%0.2f", TruePercent) << "%, ";
961 
962     // Display FalseCount or FalsePercent.
963     std::string FalseColor = R.FalseExecutionCount ? "None" : "red";
964     std::string FalseCovClass =
965         (R.FalseExecutionCount > 0) ? "covered-line" : "uncovered-line";
966 
967     OS << tag("span", "False", FalseColor);
968     OS << ": ";
969     if (getOptions().ShowBranchCounts)
970       OS << tag("span", formatCount(R.FalseExecutionCount), FalseCovClass);
971     else
972       OS << format("%0.2f", FalsePercent) << "%";
973 
974     OS << "]\n";
975   }
976   OS << EndPre;
977   OS << EndExpansionDiv;
978 }
979 
980 void SourceCoverageViewHTML::renderMCDCView(raw_ostream &OS, MCDCView &MRV,
981                                             unsigned ViewDepth) {
982   for (auto &Record : MRV.Records) {
983     OS << BeginExpansionDiv;
984     OS << BeginPre;
985     OS << "  MC/DC Decision Region (";
986 
987     // Display Line + Column information.
988     const CounterMappingRegion &DecisionRegion = Record.getDecisionRegion();
989     std::string LineNoStr = Twine(DecisionRegion.LineStart).str();
990     std::string ColNoStr = Twine(DecisionRegion.ColumnStart).str();
991     std::string TargetName = "L" + LineNoStr;
992     OS << tag("span",
993               a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr),
994                 TargetName),
995               "line-number") +
996               ") to (";
997     LineNoStr = utostr(uint64_t(DecisionRegion.LineEnd));
998     ColNoStr = utostr(uint64_t(DecisionRegion.ColumnEnd));
999     OS << tag("span",
1000               a("#" + TargetName, tag("span", LineNoStr + ":" + ColNoStr),
1001                 TargetName),
1002               "line-number") +
1003               ")\n\n";
1004 
1005     // Display MC/DC Information.
1006     OS << "  Number of Conditions: " << Record.getNumConditions() << "\n";
1007     for (unsigned i = 0; i < Record.getNumConditions(); i++) {
1008       OS << "     " << Record.getConditionHeaderString(i);
1009     }
1010     OS << "\n";
1011     OS << "  Executed MC/DC Test Vectors:\n\n     ";
1012     OS << Record.getTestVectorHeaderString();
1013     for (unsigned i = 0; i < Record.getNumTestVectors(); i++)
1014       OS << Record.getTestVectorString(i);
1015     OS << "\n";
1016     for (unsigned i = 0; i < Record.getNumConditions(); i++)
1017       OS << Record.getConditionCoverageString(i);
1018     OS << "  MC/DC Coverage for Expression: ";
1019     OS << format("%0.2f", Record.getPercentCovered()) << "%\n";
1020     OS << EndPre;
1021     OS << EndExpansionDiv;
1022   }
1023   return;
1024 }
1025 
1026 void SourceCoverageViewHTML::renderInstantiationView(raw_ostream &OS,
1027                                                      InstantiationView &ISV,
1028                                                      unsigned ViewDepth) {
1029   OS << BeginExpansionDiv;
1030   if (!ISV.View)
1031     OS << BeginSourceNameDiv
1032        << tag("pre",
1033               escape("Unexecuted instantiation: " + ISV.FunctionName.str(),
1034                      getOptions()))
1035        << EndSourceNameDiv;
1036   else
1037     ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true,
1038                     /*ShowTitle=*/false, ViewDepth);
1039   OS << EndExpansionDiv;
1040 }
1041 
1042 void SourceCoverageViewHTML::renderTitle(raw_ostream &OS, StringRef Title) {
1043   if (getOptions().hasProjectTitle())
1044     OS << tag(ProjectTitleTag, escape(getOptions().ProjectTitle, getOptions()));
1045   OS << tag(ReportTitleTag, escape(Title, getOptions()));
1046   if (getOptions().hasCreatedTime())
1047     OS << tag(CreatedTimeTag,
1048               escape(getOptions().CreatedTimeStr, getOptions()));
1049 }
1050 
1051 void SourceCoverageViewHTML::renderTableHeader(raw_ostream &OS,
1052                                                unsigned FirstUncoveredLineNo,
1053                                                unsigned ViewDepth) {
1054   std::string SourceLabel;
1055   if (FirstUncoveredLineNo == 0) {
1056     SourceLabel = tag("td", tag("pre", "Source"));
1057   } else {
1058     std::string LinkTarget = "#L" + utostr(uint64_t(FirstUncoveredLineNo));
1059     SourceLabel =
1060         tag("td", tag("pre", "Source (" +
1061                                  a(LinkTarget, "jump to first uncovered line") +
1062                                  ")"));
1063   }
1064 
1065   renderLinePrefix(OS, ViewDepth);
1066   OS << tag("td", tag("pre", "Line")) << tag("td", tag("pre", "Count"))
1067      << SourceLabel;
1068   renderLineSuffix(OS, ViewDepth);
1069 }
1070