xref: /llvm-project/llvm/tools/llvm-cov/SourceCoverageViewHTML.cpp (revision c076c490761998da900ed955e60dba1befc84866)
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   emitPrelude(OSRef);
251   OSRef << BeginSourceNameDiv << "Index" << EndSourceNameDiv;
252   OSRef << BeginTable;
253   for (StringRef SF : SourceFiles) {
254     std::string LinkText = escape(sys::path::relative_path(SF));
255     std::string LinkTarget =
256         escape(getOutputPath(SF, "html", /*InToplevel=*/false));
257     OSRef << tag("tr", tag("td", tag("pre", a(LinkTarget, LinkText), "code")));
258   }
259   OSRef << EndTable;
260   emitEpilog(OSRef);
261 
262   // Emit the default stylesheet.
263   auto CSSOrErr = createOutputStream("style", "css", /*InToplevel=*/true);
264   if (Error E = CSSOrErr.takeError())
265     return E;
266 
267   OwnedStream CSS = std::move(CSSOrErr.get());
268   CSS->operator<<(CSSForCoverage);
269 
270   return Error::success();
271 }
272 
273 void SourceCoverageViewHTML::renderViewHeader(raw_ostream &OS) {
274   OS << BeginTable;
275 }
276 
277 void SourceCoverageViewHTML::renderViewFooter(raw_ostream &OS) {
278   OS << EndTable;
279 }
280 
281 void SourceCoverageViewHTML::renderSourceName(raw_ostream &OS) {
282   OS << BeginSourceNameDiv << tag("pre", escape(getSourceName()))
283      << EndSourceNameDiv;
284 }
285 
286 void SourceCoverageViewHTML::renderLinePrefix(raw_ostream &OS, unsigned) {
287   OS << "<tr>";
288 }
289 
290 void SourceCoverageViewHTML::renderLineSuffix(raw_ostream &OS, unsigned) {
291   // If this view has sub-views, renderLine() cannot close the view's cell.
292   // Take care of it here, after all sub-views have been rendered.
293   if (hasSubViews())
294     OS << EndCodeTD;
295   OS << "</tr>";
296 }
297 
298 void SourceCoverageViewHTML::renderViewDivider(raw_ostream &, unsigned) {
299   // The table-based output makes view dividers unnecessary.
300 }
301 
302 void SourceCoverageViewHTML::renderLine(
303     raw_ostream &OS, LineRef L, const coverage::CoverageSegment *WrappedSegment,
304     CoverageSegmentArray Segments, unsigned ExpansionCol, unsigned) {
305   StringRef Line = L.Line;
306 
307   // Steps for handling text-escaping, highlighting, and tooltip creation:
308   //
309   // 1. Split the line into N+1 snippets, where N = |Segments|. The first
310   //    snippet starts from Col=1 and ends at the start of the first segment.
311   //    The last snippet starts at the last mapped column in the line and ends
312   //    at the end of the line. Both are required but may be empty.
313 
314   SmallVector<std::string, 8> Snippets;
315 
316   unsigned LCol = 1;
317   auto Snip = [&](unsigned Start, unsigned Len) {
318     assert(Start + Len <= Line.size() && "Snippet extends past the EOL");
319     Snippets.push_back(Line.substr(Start, Len));
320     LCol += Len;
321   };
322 
323   Snip(LCol - 1, Segments.empty() ? 0 : (Segments.front()->Col - 1));
324 
325   for (unsigned I = 1, E = Segments.size(); I < E; ++I) {
326     assert(LCol == Segments[I - 1]->Col && "Snippet start position is wrong");
327     Snip(LCol - 1, Segments[I]->Col - LCol);
328   }
329 
330   // |Line| + 1 is needed to avoid underflow when, e.g |Line| = 0 and LCol = 1.
331   Snip(LCol - 1, Line.size() + 1 - LCol);
332   assert(LCol == Line.size() + 1 && "Final snippet doesn't reach the EOL");
333 
334   // 2. Escape all of the snippets.
335 
336   for (unsigned I = 0, E = Snippets.size(); I < E; ++I)
337     Snippets[I] = escape(Snippets[I]);
338 
339   // 3. Use \p WrappedSegment to set the highlight for snippets 0 and 1. Use
340   //    segment 1 to set the highlight for snippet 2, segment 2 to set the
341   //    highlight for snippet 3, and so on.
342 
343   Optional<std::string> Color;
344   auto Highlight = [&](const std::string &Snippet) {
345     return tag("span", Snippet, Color.getValue());
346   };
347 
348   auto CheckIfUncovered = [](const coverage::CoverageSegment *S) {
349     return S && S->HasCount && S->Count == 0;
350   };
351 
352   if (CheckIfUncovered(WrappedSegment) ||
353       CheckIfUncovered(Segments.empty() ? nullptr : Segments.front())) {
354     Color = "red";
355     Snippets[0] = Highlight(Snippets[0]);
356     Snippets[1] = Highlight(Snippets[1]);
357   }
358 
359   for (unsigned I = 1, E = Segments.size(); I < E; ++I) {
360     const auto *CurSeg = Segments[I];
361     if (CurSeg->Col == ExpansionCol)
362       Color = "cyan";
363     else if (CheckIfUncovered(CurSeg))
364       Color = "red";
365     else
366       Color = None;
367 
368     if (Color.hasValue())
369       Snippets[I + 1] = Highlight(Snippets[I + 1]);
370   }
371 
372   // 4. Snippets[1:N+1] correspond to \p Segments[0:N]: use these to generate
373   //    sub-line region count tooltips if needed.
374 
375   bool HasMultipleRegions = [&] {
376     unsigned RegionCount = 0;
377     for (const auto *S : Segments)
378       if (S->HasCount && S->IsRegionEntry)
379         if (++RegionCount > 1)
380           return true;
381     return false;
382   }();
383 
384   if (shouldRenderRegionMarkers(HasMultipleRegions)) {
385     for (unsigned I = 0, E = Segments.size(); I < E; ++I) {
386       const auto *CurSeg = Segments[I];
387       if (!CurSeg->IsRegionEntry || !CurSeg->HasCount)
388         continue;
389 
390       Snippets[I + 1] =
391           tag("div", Snippets[I + 1] + tag("span", formatCount(CurSeg->Count),
392                                           "tooltip-content"),
393               "tooltip");
394     }
395   }
396 
397   OS << BeginCodeTD;
398   OS << BeginPre;
399   for (const auto &Snippet : Snippets)
400     OS << Snippet;
401   OS << EndPre;
402 
403   // If there are no sub-views left to attach to this cell, end the cell.
404   // Otherwise, end it after the sub-views are rendered (renderLineSuffix()).
405   if (!hasSubViews())
406     OS << EndCodeTD;
407 }
408 
409 void SourceCoverageViewHTML::renderLineCoverageColumn(
410     raw_ostream &OS, const LineCoverageStats &Line) {
411   std::string Count = "";
412   if (Line.isMapped())
413     Count = tag("pre", formatCount(Line.ExecutionCount));
414   std::string CoverageClass =
415       (Line.ExecutionCount > 0) ? "covered-line" : "uncovered-line";
416   OS << tag("td", Count, CoverageClass);
417 }
418 
419 void SourceCoverageViewHTML::renderLineNumberColumn(raw_ostream &OS,
420                                                     unsigned LineNo) {
421   std::string LineNoStr = utostr(uint64_t(LineNo));
422   OS << tag("td", a("L" + LineNoStr, tag("pre", LineNoStr), "name"),
423             "line-number");
424 }
425 
426 void SourceCoverageViewHTML::renderRegionMarkers(raw_ostream &,
427                                                  CoverageSegmentArray,
428                                                  unsigned) {
429   // Region markers are rendered in-line using tooltips.
430 }
431 
432 void SourceCoverageViewHTML::renderExpansionSite(
433     raw_ostream &OS, LineRef L, const coverage::CoverageSegment *WrappedSegment,
434     CoverageSegmentArray Segments, unsigned ExpansionCol, unsigned ViewDepth) {
435   // Render the line containing the expansion site. No extra formatting needed.
436   renderLine(OS, L, WrappedSegment, Segments, ExpansionCol, ViewDepth);
437 }
438 
439 void SourceCoverageViewHTML::renderExpansionView(raw_ostream &OS,
440                                                  ExpansionView &ESV,
441                                                  unsigned ViewDepth) {
442   OS << BeginExpansionDiv;
443   ESV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/false,
444                   ViewDepth + 1);
445   OS << EndExpansionDiv;
446 }
447 
448 void SourceCoverageViewHTML::renderInstantiationView(raw_ostream &OS,
449                                                      InstantiationView &ISV,
450                                                      unsigned ViewDepth) {
451   OS << BeginExpansionDiv;
452   ISV.View->print(OS, /*WholeFile=*/false, /*ShowSourceName=*/true, ViewDepth);
453   OS << EndExpansionDiv;
454 }
455