xref: /llvm-project/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp (revision 127d0502a0a86bf737934d8f018175a0aefc611d)
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 
308   // Steps for handling text-escaping, highlighting, and tooltip creation:
309   //
310   // 1. Split the line into N+1 snippets, where N = |Segments|. The first
311   //    snippet starts from Col=1 and ends at the start of the first segment.
312   //    The last snippet starts at the last mapped column in the line and ends
313   //    at the end of the line. Both are required but may be empty.
314 
315   SmallVector<std::string, 8> Snippets;
316 
317   unsigned LCol = 1;
318   auto Snip = [&](unsigned Start, unsigned Len) {
319     assert(Start + Len <= Line.size() && "Snippet extends past the EOL");
320     Snippets.push_back(Line.substr(Start, Len));
321     LCol += Len;
322   };
323 
324   Snip(LCol - 1, Segments.empty() ? 0 : (Segments.front()->Col - 1));
325 
326   for (unsigned I = 1, E = Segments.size(); I < E; ++I) {
327     assert(LCol == Segments[I - 1]->Col && "Snippet start position is wrong");
328     Snip(LCol - 1, Segments[I]->Col - LCol);
329   }
330 
331   // |Line| + 1 is needed to avoid underflow when, e.g |Line| = 0 and LCol = 1.
332   Snip(LCol - 1, Line.size() + 1 - LCol);
333   assert(LCol == Line.size() + 1 && "Final snippet doesn't reach the EOL");
334 
335   // 2. Escape all of the snippets.
336 
337   for (unsigned I = 0, E = Snippets.size(); I < E; ++I)
338     Snippets[I] = escape(Snippets[I]);
339 
340   // 3. Use \p WrappedSegment to set the highlight for snippets 0 and 1. Use
341   //    segment 1 to set the highlight for snippet 2, segment 2 to set the
342   //    highlight for snippet 3, and so on.
343 
344   Optional<std::string> Color;
345   auto Highlight = [&](const std::string &Snippet) {
346     return tag("span", Snippet, Color.getValue());
347   };
348 
349   auto CheckIfUncovered = [](const coverage::CoverageSegment *S) {
350     return S && S->HasCount && S->Count == 0;
351   };
352 
353   if (CheckIfUncovered(WrappedSegment) ||
354       CheckIfUncovered(Segments.empty() ? nullptr : Segments.front())) {
355     Color = "red";
356     Snippets[0] = Highlight(Snippets[0]);
357     Snippets[1] = Highlight(Snippets[1]);
358   }
359 
360   for (unsigned I = 1, E = Segments.size(); I < E; ++I) {
361     const auto *CurSeg = Segments[I];
362     if (CurSeg->Col == ExpansionCol)
363       Color = "cyan";
364     else if (CheckIfUncovered(CurSeg))
365       Color = "red";
366     else
367       Color = None;
368 
369     if (Color.hasValue())
370       Snippets[I + 1] = Highlight(Snippets[I + 1]);
371   }
372 
373   // 4. Snippets[1:N+1] correspond to \p Segments[0:N]: use these to generate
374   //    sub-line region count tooltips if needed.
375 
376   bool HasMultipleRegions = [&] {
377     unsigned RegionCount = 0;
378     for (const auto *S : Segments)
379       if (S->HasCount && S->IsRegionEntry)
380         if (++RegionCount > 1)
381           return true;
382     return false;
383   }();
384 
385   if (shouldRenderRegionMarkers(HasMultipleRegions)) {
386     for (unsigned I = 0, E = Segments.size(); I < E; ++I) {
387       const auto *CurSeg = Segments[I];
388       if (!CurSeg->IsRegionEntry || !CurSeg->HasCount)
389         continue;
390 
391       Snippets[I + 1] =
392           tag("div", Snippets[I + 1] + tag("span", formatCount(CurSeg->Count),
393                                           "tooltip-content"),
394               "tooltip");
395     }
396   }
397 
398   OS << BeginCodeTD;
399   OS << BeginPre;
400   for (const auto &Snippet : Snippets)
401     OS << Snippet;
402   OS << EndPre;
403 
404   // If there are no sub-views left to attach to this cell, end the cell.
405   // Otherwise, end it after the sub-views are rendered (renderLineSuffix()).
406   if (!hasSubViews())
407     OS << EndCodeTD;
408 }
409 
410 void SourceCoverageViewHTML::renderLineCoverageColumn(
411     raw_ostream &OS, const LineCoverageStats &Line) {
412   std::string Count = "";
413   if (Line.isMapped())
414     Count = tag("pre", formatCount(Line.ExecutionCount));
415   std::string CoverageClass =
416       (Line.ExecutionCount > 0) ? "covered-line" : "uncovered-line";
417   OS << tag("td", Count, CoverageClass);
418 }
419 
420 void SourceCoverageViewHTML::renderLineNumberColumn(raw_ostream &OS,
421                                                     unsigned LineNo) {
422   std::string LineNoStr = utostr(uint64_t(LineNo));
423   OS << tag("td", a("L" + LineNoStr, tag("pre", LineNoStr), "name"),
424             "line-number");
425 }
426 
427 void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream &,
428                                                  CoverageSegmentArray,
429                                                  unsigned) {
430   // Region markers are rendered in-line using tooltips.
431 }
432 
433 void SourceCoverageViewHTML::renderExpansionSite(
434     raw_ostream &OS, LineRef L, const coverage::CoverageSegment *WrappedSegment,
435     CoverageSegmentArray Segments, unsigned ExpansionCol, unsigned ViewDepth) {
436   // Render the line containing the expansion site. No extra formatting needed.
437   renderLine(OS, L, WrappedSegment, Segments, ExpansionCol, ViewDepth);
438 }
439 
440 void SourceCoverageViewHTML::renderExpansionView(raw_ostream &OS,
441                                                  ExpansionView &ESV,
442                                                  unsigned ViewDepth) {
443   OS << BeginExpansionDiv;
444   ESV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/false,
445                   ViewDepth + 1);
446   OS << EndExpansionDiv;
447 }
448 
449 void SourceCoverageViewHTML::renderInstantiationView(raw_ostream &OS,
450                                                      InstantiationView &ISV,
451                                                      unsigned ViewDepth) {
452   OS << BeginExpansionDiv;
453   ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true, ViewDepth);
454   OS << EndExpansionDiv;
455 }
456