1 //===- HTMLDiagnostics.cpp - HTML Diagnostics for Paths -------------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 //
9 // This file defines the HTMLDiagnostics object.
10 //
11 //===----------------------------------------------------------------------===//
12
13 #include "clang/AST/Decl.h"
14 #include "clang/AST/DeclBase.h"
15 #include "clang/AST/Stmt.h"
16 #include "clang/Analysis/IssueHash.h"
17 #include "clang/Analysis/MacroExpansionContext.h"
18 #include "clang/Analysis/PathDiagnostic.h"
19 #include "clang/Basic/FileManager.h"
20 #include "clang/Basic/LLVM.h"
21 #include "clang/Basic/SourceLocation.h"
22 #include "clang/Basic/SourceManager.h"
23 #include "clang/Lex/Lexer.h"
24 #include "clang/Lex/Preprocessor.h"
25 #include "clang/Lex/Token.h"
26 #include "clang/Rewrite/Core/HTMLRewrite.h"
27 #include "clang/Rewrite/Core/Rewriter.h"
28 #include "clang/StaticAnalyzer/Core/PathDiagnosticConsumers.h"
29 #include "llvm/ADT/ArrayRef.h"
30 #include "llvm/ADT/SmallString.h"
31 #include "llvm/ADT/StringRef.h"
32 #include "llvm/ADT/iterator_range.h"
33 #include "llvm/Support/Casting.h"
34 #include "llvm/Support/Errc.h"
35 #include "llvm/Support/ErrorHandling.h"
36 #include "llvm/Support/FileSystem.h"
37 #include "llvm/Support/MemoryBuffer.h"
38 #include "llvm/Support/Path.h"
39 #include "llvm/Support/raw_ostream.h"
40 #include <algorithm>
41 #include <cassert>
42 #include <map>
43 #include <memory>
44 #include <set>
45 #include <sstream>
46 #include <string>
47 #include <system_error>
48 #include <utility>
49 #include <vector>
50
51 using namespace clang;
52 using namespace ento;
53
54 //===----------------------------------------------------------------------===//
55 // Boilerplate.
56 //===----------------------------------------------------------------------===//
57
58 namespace {
59
60 class HTMLDiagnostics : public PathDiagnosticConsumer {
61 PathDiagnosticConsumerOptions DiagOpts;
62 std::string Directory;
63 bool createdDir = false;
64 bool noDir = false;
65 const Preprocessor &PP;
66 const bool SupportsCrossFileDiagnostics;
67
68 public:
HTMLDiagnostics(PathDiagnosticConsumerOptions DiagOpts,const std::string & OutputDir,const Preprocessor & pp,bool supportsMultipleFiles)69 HTMLDiagnostics(PathDiagnosticConsumerOptions DiagOpts,
70 const std::string &OutputDir, const Preprocessor &pp,
71 bool supportsMultipleFiles)
72 : DiagOpts(std::move(DiagOpts)), Directory(OutputDir), PP(pp),
73 SupportsCrossFileDiagnostics(supportsMultipleFiles) {}
74
~HTMLDiagnostics()75 ~HTMLDiagnostics() override { FlushDiagnostics(nullptr); }
76
77 void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags,
78 FilesMade *filesMade) override;
79
getName() const80 StringRef getName() const override {
81 return "HTMLDiagnostics";
82 }
83
supportsCrossFileDiagnostics() const84 bool supportsCrossFileDiagnostics() const override {
85 return SupportsCrossFileDiagnostics;
86 }
87
88 unsigned ProcessMacroPiece(raw_ostream &os,
89 const PathDiagnosticMacroPiece& P,
90 unsigned num);
91
92 void HandlePiece(Rewriter &R, FileID BugFileID, const PathDiagnosticPiece &P,
93 const std::vector<SourceRange> &PopUpRanges, unsigned num,
94 unsigned max);
95
96 void HighlightRange(Rewriter& R, FileID BugFileID, SourceRange Range,
97 const char *HighlightStart = "<span class=\"mrange\">",
98 const char *HighlightEnd = "</span>");
99
100 void ReportDiag(const PathDiagnostic& D,
101 FilesMade *filesMade);
102
103 // Generate the full HTML report
104 std::string GenerateHTML(const PathDiagnostic& D, Rewriter &R,
105 const SourceManager& SMgr, const PathPieces& path,
106 const char *declName);
107
108 // Add HTML header/footers to file specified by FID
109 void FinalizeHTML(const PathDiagnostic& D, Rewriter &R,
110 const SourceManager& SMgr, const PathPieces& path,
111 FileID FID, const FileEntry *Entry, const char *declName);
112
113 // Rewrite the file specified by FID with HTML formatting.
114 void RewriteFile(Rewriter &R, const PathPieces& path, FileID FID);
115
116
117 private:
118 /// \return Javascript for displaying shortcuts help;
119 StringRef showHelpJavascript();
120
121 /// \return Javascript for navigating the HTML report using j/k keys.
122 StringRef generateKeyboardNavigationJavascript();
123
124 /// \return JavaScript for an option to only show relevant lines.
125 std::string showRelevantLinesJavascript(
126 const PathDiagnostic &D, const PathPieces &path);
127
128 /// Write executed lines from \p D in JSON format into \p os.
129 void dumpCoverageData(const PathDiagnostic &D,
130 const PathPieces &path,
131 llvm::raw_string_ostream &os);
132 };
133
134 } // namespace
135
createHTMLDiagnosticConsumer(PathDiagnosticConsumerOptions DiagOpts,PathDiagnosticConsumers & C,const std::string & OutputDir,const Preprocessor & PP,const cross_tu::CrossTranslationUnitContext & CTU,const MacroExpansionContext & MacroExpansions)136 void ento::createHTMLDiagnosticConsumer(
137 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
138 const std::string &OutputDir, const Preprocessor &PP,
139 const cross_tu::CrossTranslationUnitContext &CTU,
140 const MacroExpansionContext &MacroExpansions) {
141
142 // FIXME: HTML is currently our default output type, but if the output
143 // directory isn't specified, it acts like if it was in the minimal text
144 // output mode. This doesn't make much sense, we should have the minimal text
145 // as our default. In the case of backward compatibility concerns, this could
146 // be preserved with -analyzer-config-compatibility-mode=true.
147 createTextMinimalPathDiagnosticConsumer(DiagOpts, C, OutputDir, PP, CTU,
148 MacroExpansions);
149
150 // TODO: Emit an error here.
151 if (OutputDir.empty())
152 return;
153
154 C.push_back(new HTMLDiagnostics(std::move(DiagOpts), OutputDir, PP, true));
155 }
156
createHTMLSingleFileDiagnosticConsumer(PathDiagnosticConsumerOptions DiagOpts,PathDiagnosticConsumers & C,const std::string & OutputDir,const Preprocessor & PP,const cross_tu::CrossTranslationUnitContext & CTU,const clang::MacroExpansionContext & MacroExpansions)157 void ento::createHTMLSingleFileDiagnosticConsumer(
158 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
159 const std::string &OutputDir, const Preprocessor &PP,
160 const cross_tu::CrossTranslationUnitContext &CTU,
161 const clang::MacroExpansionContext &MacroExpansions) {
162 createTextMinimalPathDiagnosticConsumer(DiagOpts, C, OutputDir, PP, CTU,
163 MacroExpansions);
164
165 // TODO: Emit an error here.
166 if (OutputDir.empty())
167 return;
168
169 C.push_back(new HTMLDiagnostics(std::move(DiagOpts), OutputDir, PP, false));
170 }
171
createPlistHTMLDiagnosticConsumer(PathDiagnosticConsumerOptions DiagOpts,PathDiagnosticConsumers & C,const std::string & prefix,const Preprocessor & PP,const cross_tu::CrossTranslationUnitContext & CTU,const MacroExpansionContext & MacroExpansions)172 void ento::createPlistHTMLDiagnosticConsumer(
173 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
174 const std::string &prefix, const Preprocessor &PP,
175 const cross_tu::CrossTranslationUnitContext &CTU,
176 const MacroExpansionContext &MacroExpansions) {
177 createHTMLDiagnosticConsumer(
178 DiagOpts, C, std::string(llvm::sys::path::parent_path(prefix)), PP, CTU,
179 MacroExpansions);
180 createPlistMultiFileDiagnosticConsumer(DiagOpts, C, prefix, PP, CTU,
181 MacroExpansions);
182 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, prefix, PP,
183 CTU, MacroExpansions);
184 }
185
createSarifHTMLDiagnosticConsumer(PathDiagnosticConsumerOptions DiagOpts,PathDiagnosticConsumers & C,const std::string & sarif_file,const Preprocessor & PP,const cross_tu::CrossTranslationUnitContext & CTU,const MacroExpansionContext & MacroExpansions)186 void ento::createSarifHTMLDiagnosticConsumer(
187 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
188 const std::string &sarif_file, const Preprocessor &PP,
189 const cross_tu::CrossTranslationUnitContext &CTU,
190 const MacroExpansionContext &MacroExpansions) {
191 createHTMLDiagnosticConsumer(
192 DiagOpts, C, std::string(llvm::sys::path::parent_path(sarif_file)), PP,
193 CTU, MacroExpansions);
194 createSarifDiagnosticConsumer(DiagOpts, C, sarif_file, PP, CTU,
195 MacroExpansions);
196 createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, sarif_file,
197 PP, CTU, MacroExpansions);
198 }
199
200 //===----------------------------------------------------------------------===//
201 // Report processing.
202 //===----------------------------------------------------------------------===//
203
FlushDiagnosticsImpl(std::vector<const PathDiagnostic * > & Diags,FilesMade * filesMade)204 void HTMLDiagnostics::FlushDiagnosticsImpl(
205 std::vector<const PathDiagnostic *> &Diags,
206 FilesMade *filesMade) {
207 for (const auto Diag : Diags)
208 ReportDiag(*Diag, filesMade);
209 }
210
ReportDiag(const PathDiagnostic & D,FilesMade * filesMade)211 void HTMLDiagnostics::ReportDiag(const PathDiagnostic& D,
212 FilesMade *filesMade) {
213 // Create the HTML directory if it is missing.
214 if (!createdDir) {
215 createdDir = true;
216 if (std::error_code ec = llvm::sys::fs::create_directories(Directory)) {
217 llvm::errs() << "warning: could not create directory '"
218 << Directory << "': " << ec.message() << '\n';
219 noDir = true;
220 return;
221 }
222 }
223
224 if (noDir)
225 return;
226
227 // First flatten out the entire path to make it easier to use.
228 PathPieces path = D.path.flatten(/*ShouldFlattenMacros=*/false);
229
230 // The path as already been prechecked that the path is non-empty.
231 assert(!path.empty());
232 const SourceManager &SMgr = path.front()->getLocation().getManager();
233
234 // Create a new rewriter to generate HTML.
235 Rewriter R(const_cast<SourceManager&>(SMgr), PP.getLangOpts());
236
237 // The file for the first path element is considered the main report file, it
238 // will usually be equivalent to SMgr.getMainFileID(); however, it might be a
239 // header when -analyzer-opt-analyze-headers is used.
240 FileID ReportFile = path.front()->getLocation().asLocation().getExpansionLoc().getFileID();
241
242 // Get the function/method name
243 SmallString<128> declName("unknown");
244 int offsetDecl = 0;
245 if (const Decl *DeclWithIssue = D.getDeclWithIssue()) {
246 if (const auto *ND = dyn_cast<NamedDecl>(DeclWithIssue))
247 declName = ND->getDeclName().getAsString();
248
249 if (const Stmt *Body = DeclWithIssue->getBody()) {
250 // Retrieve the relative position of the declaration which will be used
251 // for the file name
252 FullSourceLoc L(
253 SMgr.getExpansionLoc(path.back()->getLocation().asLocation()),
254 SMgr);
255 FullSourceLoc FunL(SMgr.getExpansionLoc(Body->getBeginLoc()), SMgr);
256 offsetDecl = L.getExpansionLineNumber() - FunL.getExpansionLineNumber();
257 }
258 }
259
260 std::string report = GenerateHTML(D, R, SMgr, path, declName.c_str());
261 if (report.empty()) {
262 llvm::errs() << "warning: no diagnostics generated for main file.\n";
263 return;
264 }
265
266 // Create a path for the target HTML file.
267 int FD;
268 SmallString<128> Model, ResultPath;
269
270 if (!DiagOpts.ShouldWriteStableReportFilename) {
271 llvm::sys::path::append(Model, Directory, "report-%%%%%%.html");
272 if (std::error_code EC =
273 llvm::sys::fs::make_absolute(Model)) {
274 llvm::errs() << "warning: could not make '" << Model
275 << "' absolute: " << EC.message() << '\n';
276 return;
277 }
278 if (std::error_code EC = llvm::sys::fs::createUniqueFile(
279 Model, FD, ResultPath, llvm::sys::fs::OF_Text)) {
280 llvm::errs() << "warning: could not create file in '" << Directory
281 << "': " << EC.message() << '\n';
282 return;
283 }
284 } else {
285 int i = 1;
286 std::error_code EC;
287 do {
288 // Find a filename which is not already used
289 const FileEntry* Entry = SMgr.getFileEntryForID(ReportFile);
290 std::stringstream filename;
291 Model = "";
292 filename << "report-"
293 << llvm::sys::path::filename(Entry->getName()).str()
294 << "-" << declName.c_str()
295 << "-" << offsetDecl
296 << "-" << i << ".html";
297 llvm::sys::path::append(Model, Directory,
298 filename.str());
299 EC = llvm::sys::fs::openFileForReadWrite(
300 Model, FD, llvm::sys::fs::CD_CreateNew, llvm::sys::fs::OF_None);
301 if (EC && EC != llvm::errc::file_exists) {
302 llvm::errs() << "warning: could not create file '" << Model
303 << "': " << EC.message() << '\n';
304 return;
305 }
306 i++;
307 } while (EC);
308 }
309
310 llvm::raw_fd_ostream os(FD, true);
311
312 if (filesMade)
313 filesMade->addDiagnostic(D, getName(),
314 llvm::sys::path::filename(ResultPath));
315
316 // Emit the HTML to disk.
317 os << report;
318 }
319
GenerateHTML(const PathDiagnostic & D,Rewriter & R,const SourceManager & SMgr,const PathPieces & path,const char * declName)320 std::string HTMLDiagnostics::GenerateHTML(const PathDiagnostic& D, Rewriter &R,
321 const SourceManager& SMgr, const PathPieces& path, const char *declName) {
322 // Rewrite source files as HTML for every new file the path crosses
323 std::vector<FileID> FileIDs;
324 for (auto I : path) {
325 FileID FID = I->getLocation().asLocation().getExpansionLoc().getFileID();
326 if (llvm::is_contained(FileIDs, FID))
327 continue;
328
329 FileIDs.push_back(FID);
330 RewriteFile(R, path, FID);
331 }
332
333 if (SupportsCrossFileDiagnostics && FileIDs.size() > 1) {
334 // Prefix file names, anchor tags, and nav cursors to every file
335 for (auto I = FileIDs.begin(), E = FileIDs.end(); I != E; I++) {
336 std::string s;
337 llvm::raw_string_ostream os(s);
338
339 if (I != FileIDs.begin())
340 os << "<hr class=divider>\n";
341
342 os << "<div id=File" << I->getHashValue() << ">\n";
343
344 // Left nav arrow
345 if (I != FileIDs.begin())
346 os << "<div class=FileNav><a href=\"#File" << (I - 1)->getHashValue()
347 << "\">←</a></div>";
348
349 os << "<h4 class=FileName>" << SMgr.getFileEntryForID(*I)->getName()
350 << "</h4>\n";
351
352 // Right nav arrow
353 if (I + 1 != E)
354 os << "<div class=FileNav><a href=\"#File" << (I + 1)->getHashValue()
355 << "\">→</a></div>";
356
357 os << "</div>\n";
358
359 R.InsertTextBefore(SMgr.getLocForStartOfFile(*I), os.str());
360 }
361
362 // Append files to the main report file in the order they appear in the path
363 for (auto I : llvm::make_range(FileIDs.begin() + 1, FileIDs.end())) {
364 std::string s;
365 llvm::raw_string_ostream os(s);
366
367 const RewriteBuffer *Buf = R.getRewriteBufferFor(I);
368 for (auto BI : *Buf)
369 os << BI;
370
371 R.InsertTextAfter(SMgr.getLocForEndOfFile(FileIDs[0]), os.str());
372 }
373 }
374
375 const RewriteBuffer *Buf = R.getRewriteBufferFor(FileIDs[0]);
376 if (!Buf)
377 return {};
378
379 // Add CSS, header, and footer.
380 FileID FID =
381 path.back()->getLocation().asLocation().getExpansionLoc().getFileID();
382 const FileEntry* Entry = SMgr.getFileEntryForID(FID);
383 FinalizeHTML(D, R, SMgr, path, FileIDs[0], Entry, declName);
384
385 std::string file;
386 llvm::raw_string_ostream os(file);
387 for (auto BI : *Buf)
388 os << BI;
389
390 return os.str();
391 }
392
dumpCoverageData(const PathDiagnostic & D,const PathPieces & path,llvm::raw_string_ostream & os)393 void HTMLDiagnostics::dumpCoverageData(
394 const PathDiagnostic &D,
395 const PathPieces &path,
396 llvm::raw_string_ostream &os) {
397
398 const FilesToLineNumsMap &ExecutedLines = D.getExecutedLines();
399
400 os << "var relevant_lines = {";
401 for (auto I = ExecutedLines.begin(),
402 E = ExecutedLines.end(); I != E; ++I) {
403 if (I != ExecutedLines.begin())
404 os << ", ";
405
406 os << "\"" << I->first.getHashValue() << "\": {";
407 for (unsigned LineNo : I->second) {
408 if (LineNo != *(I->second.begin()))
409 os << ", ";
410
411 os << "\"" << LineNo << "\": 1";
412 }
413 os << "}";
414 }
415
416 os << "};";
417 }
418
showRelevantLinesJavascript(const PathDiagnostic & D,const PathPieces & path)419 std::string HTMLDiagnostics::showRelevantLinesJavascript(
420 const PathDiagnostic &D, const PathPieces &path) {
421 std::string s;
422 llvm::raw_string_ostream os(s);
423 os << "<script type='text/javascript'>\n";
424 dumpCoverageData(D, path, os);
425 os << R"<<<(
426
427 var filterCounterexample = function (hide) {
428 var tables = document.getElementsByClassName("code");
429 for (var t=0; t<tables.length; t++) {
430 var table = tables[t];
431 var file_id = table.getAttribute("data-fileid");
432 var lines_in_fid = relevant_lines[file_id];
433 if (!lines_in_fid) {
434 lines_in_fid = {};
435 }
436 var lines = table.getElementsByClassName("codeline");
437 for (var i=0; i<lines.length; i++) {
438 var el = lines[i];
439 var lineNo = el.getAttribute("data-linenumber");
440 if (!lines_in_fid[lineNo]) {
441 if (hide) {
442 el.setAttribute("hidden", "");
443 } else {
444 el.removeAttribute("hidden");
445 }
446 }
447 }
448 }
449 }
450
451 window.addEventListener("keydown", function (event) {
452 if (event.defaultPrevented) {
453 return;
454 }
455 if (event.key == "S") {
456 var checked = document.getElementsByName("showCounterexample")[0].checked;
457 filterCounterexample(!checked);
458 document.getElementsByName("showCounterexample")[0].checked = !checked;
459 } else {
460 return;
461 }
462 event.preventDefault();
463 }, true);
464
465 document.addEventListener("DOMContentLoaded", function() {
466 document.querySelector('input[name="showCounterexample"]').onchange=
467 function (event) {
468 filterCounterexample(this.checked);
469 };
470 });
471 </script>
472
473 <form>
474 <input type="checkbox" name="showCounterexample" id="showCounterexample" />
475 <label for="showCounterexample">
476 Show only relevant lines
477 </label>
478 </form>
479 )<<<";
480
481 return os.str();
482 }
483
FinalizeHTML(const PathDiagnostic & D,Rewriter & R,const SourceManager & SMgr,const PathPieces & path,FileID FID,const FileEntry * Entry,const char * declName)484 void HTMLDiagnostics::FinalizeHTML(const PathDiagnostic& D, Rewriter &R,
485 const SourceManager& SMgr, const PathPieces& path, FileID FID,
486 const FileEntry *Entry, const char *declName) {
487 // This is a cludge; basically we want to append either the full
488 // working directory if we have no directory information. This is
489 // a work in progress.
490
491 llvm::SmallString<0> DirName;
492
493 if (llvm::sys::path::is_relative(Entry->getName())) {
494 llvm::sys::fs::current_path(DirName);
495 DirName += '/';
496 }
497
498 int LineNumber = path.back()->getLocation().asLocation().getExpansionLineNumber();
499 int ColumnNumber = path.back()->getLocation().asLocation().getExpansionColumnNumber();
500
501 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), showHelpJavascript());
502
503 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID),
504 generateKeyboardNavigationJavascript());
505
506 // Checkbox and javascript for filtering the output to the counterexample.
507 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID),
508 showRelevantLinesJavascript(D, path));
509
510 // Add the name of the file as an <h1> tag.
511 {
512 std::string s;
513 llvm::raw_string_ostream os(s);
514
515 os << "<!-- REPORTHEADER -->\n"
516 << "<h3>Bug Summary</h3>\n<table class=\"simpletable\">\n"
517 "<tr><td class=\"rowname\">File:</td><td>"
518 << html::EscapeText(DirName)
519 << html::EscapeText(Entry->getName())
520 << "</td></tr>\n<tr><td class=\"rowname\">Warning:</td><td>"
521 "<a href=\"#EndPath\">line "
522 << LineNumber
523 << ", column "
524 << ColumnNumber
525 << "</a><br />"
526 << D.getVerboseDescription() << "</td></tr>\n";
527
528 // The navigation across the extra notes pieces.
529 unsigned NumExtraPieces = 0;
530 for (const auto &Piece : path) {
531 if (const auto *P = dyn_cast<PathDiagnosticNotePiece>(Piece.get())) {
532 int LineNumber =
533 P->getLocation().asLocation().getExpansionLineNumber();
534 int ColumnNumber =
535 P->getLocation().asLocation().getExpansionColumnNumber();
536 os << "<tr><td class=\"rowname\">Note:</td><td>"
537 << "<a href=\"#Note" << NumExtraPieces << "\">line "
538 << LineNumber << ", column " << ColumnNumber << "</a><br />"
539 << P->getString() << "</td></tr>";
540 ++NumExtraPieces;
541 }
542 }
543
544 // Output any other meta data.
545
546 for (PathDiagnostic::meta_iterator I = D.meta_begin(), E = D.meta_end();
547 I != E; ++I) {
548 os << "<tr><td></td><td>" << html::EscapeText(*I) << "</td></tr>\n";
549 }
550
551 os << R"<<<(
552 </table>
553 <!-- REPORTSUMMARYEXTRA -->
554 <h3>Annotated Source Code</h3>
555 <p>Press <a href="#" onclick="toggleHelp(); return false;">'?'</a>
556 to see keyboard shortcuts</p>
557 <input type="checkbox" class="spoilerhider" id="showinvocation" />
558 <label for="showinvocation" >Show analyzer invocation</label>
559 <div class="spoiler">clang -cc1 )<<<";
560 os << html::EscapeText(DiagOpts.ToolInvocation);
561 os << R"<<<(
562 </div>
563 <div id='tooltiphint' hidden="true">
564 <p>Keyboard shortcuts: </p>
565 <ul>
566 <li>Use 'j/k' keys for keyboard navigation</li>
567 <li>Use 'Shift+S' to show/hide relevant lines</li>
568 <li>Use '?' to toggle this window</li>
569 </ul>
570 <a href="#" onclick="toggleHelp(); return false;">Close</a>
571 </div>
572 )<<<";
573 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str());
574 }
575
576 // Embed meta-data tags.
577 {
578 std::string s;
579 llvm::raw_string_ostream os(s);
580
581 StringRef BugDesc = D.getVerboseDescription();
582 if (!BugDesc.empty())
583 os << "\n<!-- BUGDESC " << BugDesc << " -->\n";
584
585 StringRef BugType = D.getBugType();
586 if (!BugType.empty())
587 os << "\n<!-- BUGTYPE " << BugType << " -->\n";
588
589 PathDiagnosticLocation UPDLoc = D.getUniqueingLoc();
590 FullSourceLoc L(SMgr.getExpansionLoc(UPDLoc.isValid()
591 ? UPDLoc.asLocation()
592 : D.getLocation().asLocation()),
593 SMgr);
594 const Decl *DeclWithIssue = D.getDeclWithIssue();
595
596 StringRef BugCategory = D.getCategory();
597 if (!BugCategory.empty())
598 os << "\n<!-- BUGCATEGORY " << BugCategory << " -->\n";
599
600 os << "\n<!-- BUGFILE " << DirName << Entry->getName() << " -->\n";
601
602 os << "\n<!-- FILENAME " << llvm::sys::path::filename(Entry->getName()) << " -->\n";
603
604 os << "\n<!-- FUNCTIONNAME " << declName << " -->\n";
605
606 os << "\n<!-- ISSUEHASHCONTENTOFLINEINCONTEXT "
607 << getIssueHash(L, D.getCheckerName(), D.getBugType(), DeclWithIssue,
608 PP.getLangOpts())
609 << " -->\n";
610
611 os << "\n<!-- BUGLINE "
612 << LineNumber
613 << " -->\n";
614
615 os << "\n<!-- BUGCOLUMN "
616 << ColumnNumber
617 << " -->\n";
618
619 os << "\n<!-- BUGPATHLENGTH " << path.size() << " -->\n";
620
621 // Mark the end of the tags.
622 os << "\n<!-- BUGMETAEND -->\n";
623
624 // Insert the text.
625 R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str());
626 }
627
628 html::AddHeaderFooterInternalBuiltinCSS(R, FID, Entry->getName());
629 }
630
showHelpJavascript()631 StringRef HTMLDiagnostics::showHelpJavascript() {
632 return R"<<<(
633 <script type='text/javascript'>
634
635 var toggleHelp = function() {
636 var hint = document.querySelector("#tooltiphint");
637 var attributeName = "hidden";
638 if (hint.hasAttribute(attributeName)) {
639 hint.removeAttribute(attributeName);
640 } else {
641 hint.setAttribute("hidden", "true");
642 }
643 };
644 window.addEventListener("keydown", function (event) {
645 if (event.defaultPrevented) {
646 return;
647 }
648 if (event.key == "?") {
649 toggleHelp();
650 } else {
651 return;
652 }
653 event.preventDefault();
654 });
655 </script>
656 )<<<";
657 }
658
shouldDisplayPopUpRange(const SourceRange & Range)659 static bool shouldDisplayPopUpRange(const SourceRange &Range) {
660 return !(Range.getBegin().isMacroID() || Range.getEnd().isMacroID());
661 }
662
663 static void
HandlePopUpPieceStartTag(Rewriter & R,const std::vector<SourceRange> & PopUpRanges)664 HandlePopUpPieceStartTag(Rewriter &R,
665 const std::vector<SourceRange> &PopUpRanges) {
666 for (const auto &Range : PopUpRanges) {
667 if (!shouldDisplayPopUpRange(Range))
668 continue;
669
670 html::HighlightRange(R, Range.getBegin(), Range.getEnd(), "",
671 "<table class='variable_popup'><tbody>",
672 /*IsTokenRange=*/true);
673 }
674 }
675
HandlePopUpPieceEndTag(Rewriter & R,const PathDiagnosticPopUpPiece & Piece,std::vector<SourceRange> & PopUpRanges,unsigned int LastReportedPieceIndex,unsigned int PopUpPieceIndex)676 static void HandlePopUpPieceEndTag(Rewriter &R,
677 const PathDiagnosticPopUpPiece &Piece,
678 std::vector<SourceRange> &PopUpRanges,
679 unsigned int LastReportedPieceIndex,
680 unsigned int PopUpPieceIndex) {
681 SmallString<256> Buf;
682 llvm::raw_svector_ostream Out(Buf);
683
684 SourceRange Range(Piece.getLocation().asRange());
685 if (!shouldDisplayPopUpRange(Range))
686 return;
687
688 // Write out the path indices with a right arrow and the message as a row.
689 Out << "<tr><td valign='top'><div class='PathIndex PathIndexPopUp'>"
690 << LastReportedPieceIndex;
691
692 // Also annotate the state transition with extra indices.
693 Out << '.' << PopUpPieceIndex;
694
695 Out << "</div></td><td>" << Piece.getString() << "</td></tr>";
696
697 // If no report made at this range mark the variable and add the end tags.
698 if (std::find(PopUpRanges.begin(), PopUpRanges.end(), Range) ==
699 PopUpRanges.end()) {
700 // Store that we create a report at this range.
701 PopUpRanges.push_back(Range);
702
703 Out << "</tbody></table></span>";
704 html::HighlightRange(R, Range.getBegin(), Range.getEnd(),
705 "<span class='variable'>", Buf.c_str(),
706 /*IsTokenRange=*/true);
707 } else {
708 // Otherwise inject just the new row at the end of the range.
709 html::HighlightRange(R, Range.getBegin(), Range.getEnd(), "", Buf.c_str(),
710 /*IsTokenRange=*/true);
711 }
712 }
713
RewriteFile(Rewriter & R,const PathPieces & path,FileID FID)714 void HTMLDiagnostics::RewriteFile(Rewriter &R,
715 const PathPieces& path, FileID FID) {
716 // Process the path.
717 // Maintain the counts of extra note pieces separately.
718 unsigned TotalPieces = path.size();
719 unsigned TotalNotePieces = std::count_if(
720 path.begin(), path.end(), [](const PathDiagnosticPieceRef &p) {
721 return isa<PathDiagnosticNotePiece>(*p);
722 });
723 unsigned PopUpPieceCount = std::count_if(
724 path.begin(), path.end(), [](const PathDiagnosticPieceRef &p) {
725 return isa<PathDiagnosticPopUpPiece>(*p);
726 });
727
728 unsigned TotalRegularPieces = TotalPieces - TotalNotePieces - PopUpPieceCount;
729 unsigned NumRegularPieces = TotalRegularPieces;
730 unsigned NumNotePieces = TotalNotePieces;
731 // Stores the count of the regular piece indices.
732 std::map<int, int> IndexMap;
733
734 // Stores the different ranges where we have reported something.
735 std::vector<SourceRange> PopUpRanges;
736 for (auto I = path.rbegin(), E = path.rend(); I != E; ++I) {
737 const auto &Piece = *I->get();
738
739 if (isa<PathDiagnosticPopUpPiece>(Piece)) {
740 ++IndexMap[NumRegularPieces];
741 } else if (isa<PathDiagnosticNotePiece>(Piece)) {
742 // This adds diagnostic bubbles, but not navigation.
743 // Navigation through note pieces would be added later,
744 // as a separate pass through the piece list.
745 HandlePiece(R, FID, Piece, PopUpRanges, NumNotePieces, TotalNotePieces);
746 --NumNotePieces;
747 } else {
748 HandlePiece(R, FID, Piece, PopUpRanges, NumRegularPieces,
749 TotalRegularPieces);
750 --NumRegularPieces;
751 }
752 }
753
754 // Secondary indexing if we are having multiple pop-ups between two notes.
755 // (e.g. [(13) 'a' is 'true']; [(13.1) 'b' is 'false']; [(13.2) 'c' is...)
756 NumRegularPieces = TotalRegularPieces;
757 for (auto I = path.rbegin(), E = path.rend(); I != E; ++I) {
758 const auto &Piece = *I->get();
759
760 if (const auto *PopUpP = dyn_cast<PathDiagnosticPopUpPiece>(&Piece)) {
761 int PopUpPieceIndex = IndexMap[NumRegularPieces];
762
763 // Pop-up pieces needs the index of the last reported piece and its count
764 // how many times we report to handle multiple reports on the same range.
765 // This marks the variable, adds the </table> end tag and the message
766 // (list element) as a row. The <table> start tag will be added after the
767 // rows has been written out. Note: It stores every different range.
768 HandlePopUpPieceEndTag(R, *PopUpP, PopUpRanges, NumRegularPieces,
769 PopUpPieceIndex);
770
771 if (PopUpPieceIndex > 0)
772 --IndexMap[NumRegularPieces];
773
774 } else if (!isa<PathDiagnosticNotePiece>(Piece)) {
775 --NumRegularPieces;
776 }
777 }
778
779 // Add the <table> start tag of pop-up pieces based on the stored ranges.
780 HandlePopUpPieceStartTag(R, PopUpRanges);
781
782 // Add line numbers, header, footer, etc.
783 html::EscapeText(R, FID);
784 html::AddLineNumbers(R, FID);
785
786 // If we have a preprocessor, relex the file and syntax highlight.
787 // We might not have a preprocessor if we come from a deserialized AST file,
788 // for example.
789 html::SyntaxHighlight(R, FID, PP);
790 html::HighlightMacros(R, FID, PP);
791 }
792
HandlePiece(Rewriter & R,FileID BugFileID,const PathDiagnosticPiece & P,const std::vector<SourceRange> & PopUpRanges,unsigned num,unsigned max)793 void HTMLDiagnostics::HandlePiece(Rewriter &R, FileID BugFileID,
794 const PathDiagnosticPiece &P,
795 const std::vector<SourceRange> &PopUpRanges,
796 unsigned num, unsigned max) {
797 // For now, just draw a box above the line in question, and emit the
798 // warning.
799 FullSourceLoc Pos = P.getLocation().asLocation();
800
801 if (!Pos.isValid())
802 return;
803
804 SourceManager &SM = R.getSourceMgr();
805 assert(&Pos.getManager() == &SM && "SourceManagers are different!");
806 std::pair<FileID, unsigned> LPosInfo = SM.getDecomposedExpansionLoc(Pos);
807
808 if (LPosInfo.first != BugFileID)
809 return;
810
811 llvm::MemoryBufferRef Buf = SM.getBufferOrFake(LPosInfo.first);
812 const char *FileStart = Buf.getBufferStart();
813
814 // Compute the column number. Rewind from the current position to the start
815 // of the line.
816 unsigned ColNo = SM.getColumnNumber(LPosInfo.first, LPosInfo.second);
817 const char *TokInstantiationPtr =Pos.getExpansionLoc().getCharacterData();
818 const char *LineStart = TokInstantiationPtr-ColNo;
819
820 // Compute LineEnd.
821 const char *LineEnd = TokInstantiationPtr;
822 const char *FileEnd = Buf.getBufferEnd();
823 while (*LineEnd != '\n' && LineEnd != FileEnd)
824 ++LineEnd;
825
826 // Compute the margin offset by counting tabs and non-tabs.
827 unsigned PosNo = 0;
828 for (const char* c = LineStart; c != TokInstantiationPtr; ++c)
829 PosNo += *c == '\t' ? 8 : 1;
830
831 // Create the html for the message.
832
833 const char *Kind = nullptr;
834 bool IsNote = false;
835 bool SuppressIndex = (max == 1);
836 switch (P.getKind()) {
837 case PathDiagnosticPiece::Event: Kind = "Event"; break;
838 case PathDiagnosticPiece::ControlFlow: Kind = "Control"; break;
839 // Setting Kind to "Control" is intentional.
840 case PathDiagnosticPiece::Macro: Kind = "Control"; break;
841 case PathDiagnosticPiece::Note:
842 Kind = "Note";
843 IsNote = true;
844 SuppressIndex = true;
845 break;
846 case PathDiagnosticPiece::Call:
847 case PathDiagnosticPiece::PopUp:
848 llvm_unreachable("Calls and extra notes should already be handled");
849 }
850
851 std::string sbuf;
852 llvm::raw_string_ostream os(sbuf);
853
854 os << "\n<tr><td class=\"num\"></td><td class=\"line\"><div id=\"";
855
856 if (IsNote)
857 os << "Note" << num;
858 else if (num == max)
859 os << "EndPath";
860 else
861 os << "Path" << num;
862
863 os << "\" class=\"msg";
864 if (Kind)
865 os << " msg" << Kind;
866 os << "\" style=\"margin-left:" << PosNo << "ex";
867
868 // Output a maximum size.
869 if (!isa<PathDiagnosticMacroPiece>(P)) {
870 // Get the string and determining its maximum substring.
871 const auto &Msg = P.getString();
872 unsigned max_token = 0;
873 unsigned cnt = 0;
874 unsigned len = Msg.size();
875
876 for (char C : Msg)
877 switch (C) {
878 default:
879 ++cnt;
880 continue;
881 case ' ':
882 case '\t':
883 case '\n':
884 if (cnt > max_token) max_token = cnt;
885 cnt = 0;
886 }
887
888 if (cnt > max_token)
889 max_token = cnt;
890
891 // Determine the approximate size of the message bubble in em.
892 unsigned em;
893 const unsigned max_line = 120;
894
895 if (max_token >= max_line)
896 em = max_token / 2;
897 else {
898 unsigned characters = max_line;
899 unsigned lines = len / max_line;
900
901 if (lines > 0) {
902 for (; characters > max_token; --characters)
903 if (len / characters > lines) {
904 ++characters;
905 break;
906 }
907 }
908
909 em = characters / 2;
910 }
911
912 if (em < max_line/2)
913 os << "; max-width:" << em << "em";
914 }
915 else
916 os << "; max-width:100em";
917
918 os << "\">";
919
920 if (!SuppressIndex) {
921 os << "<table class=\"msgT\"><tr><td valign=\"top\">";
922 os << "<div class=\"PathIndex";
923 if (Kind) os << " PathIndex" << Kind;
924 os << "\">" << num << "</div>";
925
926 if (num > 1) {
927 os << "</td><td><div class=\"PathNav\"><a href=\"#Path"
928 << (num - 1)
929 << "\" title=\"Previous event ("
930 << (num - 1)
931 << ")\">←</a></div>";
932 }
933
934 os << "</td><td>";
935 }
936
937 if (const auto *MP = dyn_cast<PathDiagnosticMacroPiece>(&P)) {
938 os << "Within the expansion of the macro '";
939
940 // Get the name of the macro by relexing it.
941 {
942 FullSourceLoc L = MP->getLocation().asLocation().getExpansionLoc();
943 assert(L.isFileID());
944 StringRef BufferInfo = L.getBufferData();
945 std::pair<FileID, unsigned> LocInfo = L.getDecomposedLoc();
946 const char* MacroName = LocInfo.second + BufferInfo.data();
947 Lexer rawLexer(SM.getLocForStartOfFile(LocInfo.first), PP.getLangOpts(),
948 BufferInfo.begin(), MacroName, BufferInfo.end());
949
950 Token TheTok;
951 rawLexer.LexFromRawLexer(TheTok);
952 for (unsigned i = 0, n = TheTok.getLength(); i < n; ++i)
953 os << MacroName[i];
954 }
955
956 os << "':\n";
957
958 if (!SuppressIndex) {
959 os << "</td>";
960 if (num < max) {
961 os << "<td><div class=\"PathNav\"><a href=\"#";
962 if (num == max - 1)
963 os << "EndPath";
964 else
965 os << "Path" << (num + 1);
966 os << "\" title=\"Next event ("
967 << (num + 1)
968 << ")\">→</a></div></td>";
969 }
970
971 os << "</tr></table>";
972 }
973
974 // Within a macro piece. Write out each event.
975 ProcessMacroPiece(os, *MP, 0);
976 }
977 else {
978 os << html::EscapeText(P.getString());
979
980 if (!SuppressIndex) {
981 os << "</td>";
982 if (num < max) {
983 os << "<td><div class=\"PathNav\"><a href=\"#";
984 if (num == max - 1)
985 os << "EndPath";
986 else
987 os << "Path" << (num + 1);
988 os << "\" title=\"Next event ("
989 << (num + 1)
990 << ")\">→</a></div></td>";
991 }
992
993 os << "</tr></table>";
994 }
995 }
996
997 os << "</div></td></tr>";
998
999 // Insert the new html.
1000 unsigned DisplayPos = LineEnd - FileStart;
1001 SourceLocation Loc =
1002 SM.getLocForStartOfFile(LPosInfo.first).getLocWithOffset(DisplayPos);
1003
1004 R.InsertTextBefore(Loc, os.str());
1005
1006 // Now highlight the ranges.
1007 ArrayRef<SourceRange> Ranges = P.getRanges();
1008 for (const auto &Range : Ranges) {
1009 // If we have already highlighted the range as a pop-up there is no work.
1010 if (std::find(PopUpRanges.begin(), PopUpRanges.end(), Range) !=
1011 PopUpRanges.end())
1012 continue;
1013
1014 HighlightRange(R, LPosInfo.first, Range);
1015 }
1016 }
1017
EmitAlphaCounter(raw_ostream & os,unsigned n)1018 static void EmitAlphaCounter(raw_ostream &os, unsigned n) {
1019 unsigned x = n % ('z' - 'a');
1020 n /= 'z' - 'a';
1021
1022 if (n > 0)
1023 EmitAlphaCounter(os, n);
1024
1025 os << char('a' + x);
1026 }
1027
ProcessMacroPiece(raw_ostream & os,const PathDiagnosticMacroPiece & P,unsigned num)1028 unsigned HTMLDiagnostics::ProcessMacroPiece(raw_ostream &os,
1029 const PathDiagnosticMacroPiece& P,
1030 unsigned num) {
1031 for (const auto &subPiece : P.subPieces) {
1032 if (const auto *MP = dyn_cast<PathDiagnosticMacroPiece>(subPiece.get())) {
1033 num = ProcessMacroPiece(os, *MP, num);
1034 continue;
1035 }
1036
1037 if (const auto *EP = dyn_cast<PathDiagnosticEventPiece>(subPiece.get())) {
1038 os << "<div class=\"msg msgEvent\" style=\"width:94%; "
1039 "margin-left:5px\">"
1040 "<table class=\"msgT\"><tr>"
1041 "<td valign=\"top\"><div class=\"PathIndex PathIndexEvent\">";
1042 EmitAlphaCounter(os, num++);
1043 os << "</div></td><td valign=\"top\">"
1044 << html::EscapeText(EP->getString())
1045 << "</td></tr></table></div>\n";
1046 }
1047 }
1048
1049 return num;
1050 }
1051
HighlightRange(Rewriter & R,FileID BugFileID,SourceRange Range,const char * HighlightStart,const char * HighlightEnd)1052 void HTMLDiagnostics::HighlightRange(Rewriter& R, FileID BugFileID,
1053 SourceRange Range,
1054 const char *HighlightStart,
1055 const char *HighlightEnd) {
1056 SourceManager &SM = R.getSourceMgr();
1057 const LangOptions &LangOpts = R.getLangOpts();
1058
1059 SourceLocation InstantiationStart = SM.getExpansionLoc(Range.getBegin());
1060 unsigned StartLineNo = SM.getExpansionLineNumber(InstantiationStart);
1061
1062 SourceLocation InstantiationEnd = SM.getExpansionLoc(Range.getEnd());
1063 unsigned EndLineNo = SM.getExpansionLineNumber(InstantiationEnd);
1064
1065 if (EndLineNo < StartLineNo)
1066 return;
1067
1068 if (SM.getFileID(InstantiationStart) != BugFileID ||
1069 SM.getFileID(InstantiationEnd) != BugFileID)
1070 return;
1071
1072 // Compute the column number of the end.
1073 unsigned EndColNo = SM.getExpansionColumnNumber(InstantiationEnd);
1074 unsigned OldEndColNo = EndColNo;
1075
1076 if (EndColNo) {
1077 // Add in the length of the token, so that we cover multi-char tokens.
1078 EndColNo += Lexer::MeasureTokenLength(Range.getEnd(), SM, LangOpts)-1;
1079 }
1080
1081 // Highlight the range. Make the span tag the outermost tag for the
1082 // selected range.
1083
1084 SourceLocation E =
1085 InstantiationEnd.getLocWithOffset(EndColNo - OldEndColNo);
1086
1087 html::HighlightRange(R, InstantiationStart, E, HighlightStart, HighlightEnd);
1088 }
1089
generateKeyboardNavigationJavascript()1090 StringRef HTMLDiagnostics::generateKeyboardNavigationJavascript() {
1091 return R"<<<(
1092 <script type='text/javascript'>
1093 var digitMatcher = new RegExp("[0-9]+");
1094
1095 var querySelectorAllArray = function(selector) {
1096 return Array.prototype.slice.call(
1097 document.querySelectorAll(selector));
1098 }
1099
1100 document.addEventListener("DOMContentLoaded", function() {
1101 querySelectorAllArray(".PathNav > a").forEach(
1102 function(currentValue, currentIndex) {
1103 var hrefValue = currentValue.getAttribute("href");
1104 currentValue.onclick = function() {
1105 scrollTo(document.querySelector(hrefValue));
1106 return false;
1107 };
1108 });
1109 });
1110
1111 var findNum = function() {
1112 var s = document.querySelector(".selected");
1113 if (!s || s.id == "EndPath") {
1114 return 0;
1115 }
1116 var out = parseInt(digitMatcher.exec(s.id)[0]);
1117 return out;
1118 };
1119
1120 var scrollTo = function(el) {
1121 querySelectorAllArray(".selected").forEach(function(s) {
1122 s.classList.remove("selected");
1123 });
1124 el.classList.add("selected");
1125 window.scrollBy(0, el.getBoundingClientRect().top -
1126 (window.innerHeight / 2));
1127 }
1128
1129 var move = function(num, up, numItems) {
1130 if (num == 1 && up || num == numItems - 1 && !up) {
1131 return 0;
1132 } else if (num == 0 && up) {
1133 return numItems - 1;
1134 } else if (num == 0 && !up) {
1135 return 1 % numItems;
1136 }
1137 return up ? num - 1 : num + 1;
1138 }
1139
1140 var numToId = function(num) {
1141 if (num == 0) {
1142 return document.getElementById("EndPath")
1143 }
1144 return document.getElementById("Path" + num);
1145 };
1146
1147 var navigateTo = function(up) {
1148 var numItems = document.querySelectorAll(
1149 ".line > .msgEvent, .line > .msgControl").length;
1150 var currentSelected = findNum();
1151 var newSelected = move(currentSelected, up, numItems);
1152 var newEl = numToId(newSelected, numItems);
1153
1154 // Scroll element into center.
1155 scrollTo(newEl);
1156 };
1157
1158 window.addEventListener("keydown", function (event) {
1159 if (event.defaultPrevented) {
1160 return;
1161 }
1162 if (event.key == "j") {
1163 navigateTo(/*up=*/false);
1164 } else if (event.key == "k") {
1165 navigateTo(/*up=*/true);
1166 } else {
1167 return;
1168 }
1169 event.preventDefault();
1170 }, true);
1171 </script>
1172 )<<<";
1173 }
1174