xref: /llvm-project/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp (revision fc07e8b428b08235eb44cc2c418436b388ffc51e)
1 //===- SourceCoverageViewHTML.cpp - A html code coverage view -------------===//
2 //
3 //                     The LLVM Compiler Infrastructure
4 //
5 // This file is distributed under the University of Illinois Open Source
6 // License. See LICENSE.TXT for details.
7 //
8 //===----------------------------------------------------------------------===//
9 ///
10 /// \file This file implements the html coverage renderer.
11 ///
12 //===----------------------------------------------------------------------===//
13 
14 #include "SourceCoverageViewHTML.h"
15 #include "llvm/ADT/Optional.h"
16 #include "llvm/ADT/SmallString.h"
17 #include "llvm/ADT/StringExtras.h"
18 #include "llvm/Support/Path.h"
19 
20 using namespace llvm;
21 
22 namespace {
23 
24 // Return a string with the special characters in \p Str escaped.
25 std::string escape(StringRef Str) {
26   std::string Result;
27   for (char C : Str) {
28     if (C == '&')
29       Result += "&";
30     else if (C == '<')
31       Result += "&lt;";
32     else if (C == '>')
33       Result += "&gt;";
34     else if (C == '\"')
35       Result += "&quot;";
36     else
37       Result += C;
38   }
39   return Result;
40 }
41 
42 // Create a \p Name tag around \p Str, and optionally set its \p ClassName.
43 std::string tag(const std::string &Name, const std::string &Str,
44                 const std::string &ClassName = "") {
45   std::string Tag = "<" + Name;
46   if (ClassName != "")
47     Tag += " class='" + ClassName + "'";
48   return Tag + ">" + Str + "</" + Name + ">";
49 }
50 
51 // Create an anchor to \p Link with the label \p Str.
52 std::string a(const std::string &Link, const std::string &Str,
53               const std::string &TargetType = "href") {
54   return "<a " + TargetType + "='" + Link + "'>" + Str + "</a>";
55 }
56 
57 const char *BeginHeader =
58   "<head>"
59     "<meta name='viewport' content='width=device-width,initial-scale=1'>"
60     "<meta charset='UTF-8'>";
61 
62 const char *CSSForCoverage =
63     R"(.red {
64   background-color: #FFD0D0;
65 }
66 .cyan {
67   background-color: cyan;
68 }
69 body {
70   font-family: -apple-system, sans-serif;
71 }
72 pre {
73   margin-top: 0px !important;
74   margin-bottom: 0px !important;
75 }
76 .source-name-title {
77   padding: 5px 10px;
78   border-bottom: 1px solid #dbdbdb;
79   background-color: #eee;
80 }
81 .centered {
82   display: table;
83   margin-left: auto;
84   margin-right: auto;
85   border: 1px solid #dbdbdb;
86   border-radius: 3px;
87 }
88 .expansion-view {
89   background-color: rgba(0, 0, 0, 0);
90   margin-left: 0px;
91   margin-top: 5px;
92   margin-right: 5px;
93   margin-bottom: 5px;
94   border: 1px solid #dbdbdb;
95   border-radius: 3px;
96 }
97 table {
98   border-collapse: collapse;
99 }
100 .line-number {
101   text-align: right;
102   color: #aaa;
103 }
104 .covered-line {
105   text-align: right;
106   color: #0080ff;
107 }
108 .uncovered-line {
109   text-align: right;
110   color: #ff3300;
111 }
112 .tooltip {
113   position: relative;
114   display: inline;
115   background-color: #b3e6ff;
116   text-decoration: none;
117 }
118 .tooltip span.tooltip-content {
119   position: absolute;
120   width: 100px;
121   margin-left: -50px;
122   color: #FFFFFF;
123   background: #000000;
124   height: 30px;
125   line-height: 30px;
126   text-align: center;
127   visibility: hidden;
128   border-radius: 6px;
129 }
130 .tooltip span.tooltip-content:after {
131   content: '';
132   position: absolute;
133   top: 100%;
134   left: 50%;
135   margin-left: -8px;
136   width: 0; height: 0;
137   border-top: 8px solid #000000;
138   border-right: 8px solid transparent;
139   border-left: 8px solid transparent;
140 }
141 :hover.tooltip span.tooltip-content {
142   visibility: visible;
143   opacity: 0.8;
144   bottom: 30px;
145   left: 50%;
146   z-index: 999;
147 }
148 th, td {
149   vertical-align: top;
150   padding: 2px 5px;
151   border-collapse: collapse;
152   border-right: solid 1px #eee;
153   border-left: solid 1px #eee;
154 }
155 td:first-child {
156   border-left: none;
157 }
158 td:last-child {
159   border-right: none;
160 }
161 )";
162 
163 const char *EndHeader = "</head>";
164 
165 const char *BeginCenteredDiv = "<div class='centered'>";
166 
167 const char *EndCenteredDiv = "</div>";
168 
169 const char *BeginSourceNameDiv = "<div class='source-name-title'>";
170 
171 const char *EndSourceNameDiv = "</div>";
172 
173 const char *BeginCodeTD = "<td class='code'>";
174 
175 const char *EndCodeTD = "</td>";
176 
177 const char *BeginPre = "<pre>";
178 
179 const char *EndPre = "</pre>";
180 
181 const char *BeginExpansionDiv = "<div class='expansion-view'>";
182 
183 const char *EndExpansionDiv = "</div>";
184 
185 const char *BeginTable = "<table>";
186 
187 const char *EndTable = "</table>";
188 
189 std::string getPathToStyle(StringRef ViewPath) {
190   std::string PathToStyle = "";
191   std::string PathSep = sys::path::get_separator();
192   unsigned NumSeps = ViewPath.count(PathSep);
193   for (unsigned I = 0, E = NumSeps; I < E; ++I)
194     PathToStyle += ".." + PathSep;
195   return PathToStyle + "style.css";
196 }
197 
198 void emitPrelude(raw_ostream &OS, const std::string &PathToStyle = "") {
199   OS << "<!doctype html>"
200         "<html>"
201      << BeginHeader;
202 
203   // Link to a stylesheet if one is available. Otherwise, use the default style.
204   if (PathToStyle.empty())
205     OS << "<style>" << CSSForCoverage << "</style>";
206   else
207     OS << "<link rel='stylesheet' type='text/css' href='" << escape(PathToStyle)
208        << "'>";
209 
210   OS << EndHeader << "<body>" << BeginCenteredDiv;
211 }
212 
213 void emitEpilog(raw_ostream &OS) {
214   OS << EndCenteredDiv << "</body>"
215                           "</html>";
216 }
217 
218 } // anonymous namespace
219 
220 Expected<CoveragePrinter::OwnedStream>
221 CoveragePrinterHTML::createViewFile(StringRef Path, bool InToplevel) {
222   auto OSOrErr = createOutputStream(Path, "html", InToplevel);
223   if (!OSOrErr)
224     return OSOrErr;
225 
226   OwnedStream OS = std::move(OSOrErr.get());
227 
228   if (!Opts.hasOutputDirectory()) {
229     emitPrelude(*OS.get());
230   } else {
231     std::string ViewPath = getOutputPath(Path, "html", InToplevel);
232     emitPrelude(*OS.get(), getPathToStyle(ViewPath));
233   }
234 
235   return std::move(OS);
236 }
237 
238 void CoveragePrinterHTML::closeViewFile(OwnedStream OS) {
239   emitEpilog(*OS.get());
240 }
241 
242 Error CoveragePrinterHTML::createIndexFile(ArrayRef<StringRef> SourceFiles) {
243   auto OSOrErr = createOutputStream("index", "html", /*InToplevel=*/true);
244   if (Error E = OSOrErr.takeError())
245     return E;
246   auto OS = std::move(OSOrErr.get());
247   raw_ostream &OSRef = *OS.get();
248 
249   // Emit a table containing links to reports for each file in the covmapping.
250   assert(Opts.hasOutputDirectory() && "No output directory for index file");
251   emitPrelude(OSRef, getPathToStyle(""));
252   OSRef << BeginSourceNameDiv << "Index" << EndSourceNameDiv;
253   OSRef << BeginTable;
254   for (StringRef SF : SourceFiles) {
255     std::string LinkText = escape(sys::path::relative_path(SF));
256     std::string LinkTarget =
257         escape(getOutputPath(SF, "html", /*InToplevel=*/false));
258     OSRef << tag("tr", tag("td", tag("pre", a(LinkTarget, LinkText), "code")));
259   }
260   OSRef << EndTable;
261   emitEpilog(OSRef);
262 
263   // Emit the default stylesheet.
264   auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true);
265   if (Error E = CSSOrErr.takeError())
266     return E;
267 
268   OwnedStream CSS = std::move(CSSOrErr.get());
269   CSS->operator<<(CSSForCoverage);
270 
271   return Error::success();
272 }
273 
274 void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) {
275   OS << BeginTable;
276 }
277 
278 void SourceCoverageViewHTML::renderViewFooter(raw_ostream &OS) {
279   OS << EndTable;
280 }
281 
282 void SourceCoverageViewHTML::renderSourceName(raw_ostream &OS) {
283   OS << BeginSourceNameDiv << tag("pre", escape(getSourceName()))
284      << EndSourceNameDiv;
285 }
286 
287 void SourceCoverageViewHTML::renderLinePrefix(raw_ostream &OS, unsigned) {
288   OS << "<tr>";
289 }
290 
291 void SourceCoverageViewHTML::renderLineSuffix(raw_ostream &OS, unsigned) {
292   // If this view has sub-views, renderLine() cannot close the view's cell.
293   // Take care of it here, after all sub-views have been rendered.
294   if (hasSubViews())
295     OS << EndCodeTD;
296   OS << "</tr>";
297 }
298 
299 void SourceCoverageViewHTML::renderViewDivider(raw_ostream &, unsigned) {
300   // The table-based output makes view dividers unnecessary.
301 }
302 
303 void SourceCoverageViewHTML::renderLine(
304     raw_ostream &OS, LineRef L, const coverage::CoverageSegment *WrappedSegment,
305     CoverageSegmentArray Segments, unsigned ExpansionCol, unsigned) {
306   StringRef Line = L.Line;
307   unsigned LineNo = L.LineNo;
308 
309   // Steps for handling text-escaping, highlighting, and tooltip creation:
310   //
311   // 1. Split the line into N+1 snippets, where N = |Segments|. The first
312   //    snippet starts from Col=1 and ends at the start of the first segment.
313   //    The last snippet starts at the last mapped column in the line and ends
314   //    at the end of the line. Both are required but may be empty.
315 
316   SmallVector<std::string, 8> Snippets;
317 
318   unsigned LCol = 1;
319   auto Snip = [&](unsigned Start, unsigned Len) {
320     assert(Start + Len <= Line.size() && "Snippet extends past the EOL");
321     Snippets.push_back(Line.substr(Start, Len));
322     LCol += Len;
323   };
324 
325   Snip(LCol - 1, Segments.empty() ? 0 : (Segments.front()->Col - 1));
326 
327   for (unsigned I = 1, E = Segments.size(); I < E; ++I) {
328     assert(LCol == Segments[I - 1]->Col && "Snippet start position is wrong");
329     Snip(LCol - 1, Segments[I]->Col - LCol);
330   }
331 
332   // |Line| + 1 is needed to avoid underflow when, e.g |Line| = 0 and LCol = 1.
333   Snip(LCol - 1, Line.size() + 1 - LCol);
334   assert(LCol == Line.size() + 1 && "Final snippet doesn't reach the EOL");
335 
336   // 2. Escape all of the snippets.
337 
338   for (unsigned I = 0, E = Snippets.size(); I < E; ++I)
339     Snippets[I] = escape(Snippets[I]);
340 
341   // 3. Use \p WrappedSegment to set the highlight for snippet 0. Use segment
342   //    1 to set the highlight for snippet 2, segment 2 to set the highlight for
343   //    snippet 3, and so on.
344 
345   Optional<std::string> Color;
346   SmallVector<std::pair<unsigned, unsigned>, 4> HighlightedRanges;
347   auto Highlight = [&](const std::string &Snippet, unsigned LC, unsigned RC) {
348     if (getOptions().Debug) {
349       if (!HighlightedRanges.empty() &&
350           HighlightedRanges.back().second == LC - 1) {
351         HighlightedRanges.back().second = RC;
352       } else
353         HighlightedRanges.emplace_back(LC, RC);
354     }
355     return tag("span", Snippet, Color.getValue());
356   };
357 
358   auto CheckIfUncovered = [](const coverage::CoverageSegment *S) {
359     return S && (S->HasCount && S->Count == 0);
360   };
361 
362   if (CheckIfUncovered(WrappedSegment) ||
363       CheckIfUncovered(Segments.empty() ? nullptr : Segments.front())) {
364     Color = "red";
365     Snippets[0] = Highlight(Snippets[0], 0, Snippets[0].size());
366   }
367 
368   for (unsigned I = 0, E = Segments.size(); I < E; ++I) {
369     const auto *CurSeg = Segments[I];
370     if (CurSeg->Col == ExpansionCol)
371       Color = "cyan";
372     else if (CheckIfUncovered(CurSeg))
373       Color = "red";
374     else
375       Color = None;
376 
377     if (Color.hasValue())
378       Snippets[I + 1] = Highlight(Snippets[I + 1], CurSeg->Col,
379                                   CurSeg->Col + Snippets[I + 1].size());
380   }
381 
382   if (Color.hasValue() && Segments.empty())
383     Snippets.back() = Highlight(Snippets.back(), Snippets[0].size(), 0);
384 
385   if (getOptions().Debug) {
386     for (const auto &Range : HighlightedRanges) {
387       errs() << "Highlighted line " << LineNo << ", " << Range.first + 1
388              << " -> ";
389       if (Range.second == 0)
390         errs() << "?";
391       else
392         errs() << Range.second + 1;
393       errs() << "\n";
394     }
395   }
396 
397   // 4. Snippets[1:N+1] correspond to \p Segments[0:N]: use these to generate
398   //    sub-line region count tooltips if needed.
399 
400   bool HasMultipleRegions = [&] {
401     unsigned RegionCount = 0;
402     for (const auto *S : Segments)
403       if (S->HasCount && S->IsRegionEntry)
404         if (++RegionCount > 1)
405           return true;
406     return false;
407   }();
408 
409   if (shouldRenderRegionMarkers(HasMultipleRegions)) {
410     for (unsigned I = 0, E = Segments.size(); I < E; ++I) {
411       const auto *CurSeg = Segments[I];
412       if (!CurSeg->IsRegionEntry || !CurSeg->HasCount)
413         continue;
414 
415       Snippets[I + 1] =
416           tag("div", Snippets[I + 1] + tag("span", formatCount(CurSeg->Count),
417                                            "tooltip-content"),
418               "tooltip");
419     }
420   }
421 
422   OS << BeginCodeTD;
423   OS << BeginPre;
424   for (const auto &Snippet : Snippets)
425     OS << Snippet;
426   OS << EndPre;
427 
428   // If there are no sub-views left to attach to this cell, end the cell.
429   // Otherwise, end it after the sub-views are rendered (renderLineSuffix()).
430   if (!hasSubViews())
431     OS << EndCodeTD;
432 }
433 
434 void SourceCoverageViewHTML::renderLineCoverageColumn(
435     raw_ostream &OS, const LineCoverageStats &Line) {
436   std::string Count = "";
437   if (Line.isMapped())
438     Count = tag("pre", formatCount(Line.ExecutionCount));
439   std::string CoverageClass =
440       (Line.ExecutionCount > 0) ? "covered-line" : "uncovered-line";
441   OS << tag("td", Count, CoverageClass);
442 }
443 
444 void SourceCoverageViewHTML::renderLineNumberColumn(raw_ostream &OS,
445                                                     unsigned LineNo) {
446   std::string LineNoStr = utostr(uint64_t(LineNo));
447   OS << tag("td", a("L" + LineNoStr, tag("pre", LineNoStr), "name"),
448             "line-number");
449 }
450 
451 void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream &,
452                                                  CoverageSegmentArray,
453                                                  unsigned) {
454   // Region markers are rendered in-line using tooltips.
455 }
456 
457 void SourceCoverageViewHTML::renderExpansionSite(
458     raw_ostream &OS, LineRef L, const coverage::CoverageSegment *WrappedSegment,
459     CoverageSegmentArray Segments, unsigned ExpansionCol, unsigned ViewDepth) {
460   // Render the line containing the expansion site. No extra formatting needed.
461   renderLine(OS, L, WrappedSegment, Segments, ExpansionCol, ViewDepth);
462 }
463 
464 void SourceCoverageViewHTML::renderExpansionView(raw_ostream &OS,
465                                                  ExpansionView &ESV,
466                                                  unsigned ViewDepth) {
467   OS << BeginExpansionDiv;
468   ESV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/false,
469                   ViewDepth + 1);
470   OS << EndExpansionDiv;
471 }
472 
473 void SourceCoverageViewHTML::renderInstantiationView(raw_ostream &OS,
474                                                      InstantiationView &ISV,
475                                                      unsigned ViewDepth) {
476   OS << BeginExpansionDiv;
477   ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true, ViewDepth);
478   OS << EndExpansionDiv;
479 }
480