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