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