//===--- HTMLReport.cpp - Explain the analysis for humans -----------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // If we're debugging this tool or trying to explain its conclusions, we need to // be able to identify specific facts about the code and the inferences made. // // This library prints an annotated version of the code // //===----------------------------------------------------------------------===// #include "AnalysisInternal.h" #include "clang-include-cleaner/IncludeSpeller.h" #include "clang-include-cleaner/Types.h" #include "clang/AST/ASTContext.h" #include "clang/AST/PrettyPrinter.h" #include "clang/Basic/SourceManager.h" #include "clang/Lex/HeaderSearch.h" #include "clang/Lex/Lexer.h" #include "clang/Lex/Preprocessor.h" #include "clang/Tooling/Inclusions/StandardLibrary.h" #include "llvm/Support/ScopedPrinter.h" #include "llvm/Support/raw_ostream.h" #include namespace clang::include_cleaner { namespace { constexpr llvm::StringLiteral CSS = R"css( body { margin: 0; } pre { line-height: 1.5em; counter-reset: line; margin: 0; } pre .line:not(.added) { counter-increment: line; } pre .line::before { content: counter(line); display: inline-block; background-color: #eee; border-right: 1px solid #ccc; text-align: right; width: 3em; padding-right: 0.5em; margin-right: 0.5em; } pre .line.added::before { content: '+' } .ref, .inc { text-decoration: underline; color: #008; } .sel { position: relative; cursor: pointer; } .ref.implicit { background-color: #ff8; } #hover { color: black; background-color: #aaccff; border: 1px solid #444; z-index: 1; position: absolute; top: 100%; left: 0; font-family: sans-serif; padding: 0.5em; } #hover p, #hover pre { margin: 0; } #hover .target.implicit, .provides .implicit { background-color: #bbb; } #hover .target.ambiguous, .provides .ambiguous { background-color: #caf; } .missing, .unused { background-color: #faa !important; } .inserted { background-color: #bea !important; } .semiused { background-color: #888 !important; } #hover th { color: #008; text-align: right; padding-right: 0.5em; } #hover .target:not(:first-child) { margin-top: 1em; padding-top: 1em; border-top: 1px solid #444; } .ref.missing #hover .insert { background-color: #bea; } .ref:not(.missing) #hover .insert { font-style: italic; } )css"; constexpr llvm::StringLiteral JS = R"js( // Recreate the #hover div inside whichever target .sel element was clicked. function select(event) { var target = event.target.closest('.sel'); var hover = document.getElementById('hover'); if (hover) { if (hover.parentElement == target) return; hover.parentNode.removeChild(hover); } if (target == null) return; hover = document.createElement('div'); hover.id = 'hover'; fillHover(hover, target); target.appendChild(hover); } // Fill the #hover div with the templates named by data-hover in the target. function fillHover(hover, target) { target.dataset.hover?.split(',').forEach(function(id) { for (c of document.getElementById(id).content.childNodes) hover.appendChild(c.cloneNode(true)); }) } )js"; // Categorize the symbol, like FunctionDecl or Macro llvm::StringRef describeSymbol(const Symbol &Sym) { switch (Sym.kind()) { case Symbol::Declaration: return Sym.declaration().getDeclKindName(); case Symbol::Macro: return "Macro"; } llvm_unreachable("unhandled symbol kind"); } // Return detailed symbol description (declaration), if we have any. std::string printDetails(const Symbol &Sym) { std::string S; if (Sym.kind() == Symbol::Declaration) { // Print the declaration of the symbol, e.g. to disambiguate overloads. const auto &D = Sym.declaration(); PrintingPolicy PP = D.getASTContext().getPrintingPolicy(); PP.FullyQualifiedName = true; PP.TerseOutput = true; PP.SuppressInitializers = true; llvm::raw_string_ostream SS(S); D.print(SS, PP); } return S; } llvm::StringRef refType(RefType T) { switch (T) { case RefType::Explicit: return "explicit"; case RefType::Implicit: return "implicit"; case RefType::Ambiguous: return "ambiguous"; } llvm_unreachable("unhandled RefType enum"); } class Reporter { llvm::raw_ostream &OS; const ASTContext &Ctx; const SourceManager &SM; const Preprocessor &PP; const include_cleaner::Includes &Includes; const PragmaIncludes *PI; FileID MainFile; const FileEntry *MainFE; // Points within the main file that reference a Symbol. // Implicit refs will be marked with a symbol just before the token. struct Ref { unsigned Offset; RefType Type; Symbol Sym; SmallVector Locations = {}; SmallVector
Headers = {}; SmallVector Includes = {}; bool Satisfied = false; // Is the include present? std::string Insert = {}; // If we had no includes, what would we insert? }; std::vector Refs; llvm::DenseMap> IncludeRefs; llvm::StringMap> Insertion; llvm::StringRef includeType(const Include *I) { auto &List = IncludeRefs[I]; if (List.empty()) return "unused"; if (llvm::any_of(List, [&](unsigned I) { return Refs[I].Type == RefType::Explicit; })) return "used"; return "semiused"; } void fillTarget(Ref &R) { // Duplicates logic from walkUsed(), which doesn't expose SymbolLocations. for (auto &Loc : locateSymbol(R.Sym, Ctx.getLangOpts())) R.Locations.push_back(Loc); R.Headers = headersForSymbol(R.Sym, PP, PI); for (const auto &H : R.Headers) { R.Includes.append(Includes.match(H)); // FIXME: library should signal main-file refs somehow. // Non-physical refs to the main-file should be possible. if (H.kind() == Header::Physical && H.physical() == MainFE) R.Satisfied = true; } if (!R.Includes.empty()) R.Satisfied = true; // Include pointers are meaningfully ordered as they are backed by a vector. llvm::sort(R.Includes); R.Includes.erase(std::unique(R.Includes.begin(), R.Includes.end()), R.Includes.end()); if (!R.Headers.empty()) R.Insert = spellHeader({R.Headers.front(), PP.getHeaderSearchInfo(), MainFE}); } public: Reporter(llvm::raw_ostream &OS, ASTContext &Ctx, const Preprocessor &PP, const include_cleaner::Includes &Includes, const PragmaIncludes *PI, FileID MainFile) : OS(OS), Ctx(Ctx), SM(Ctx.getSourceManager()), PP(PP), Includes(Includes), PI(PI), MainFile(MainFile), MainFE(SM.getFileEntryForID(MainFile)) {} void addRef(const SymbolReference &SR) { auto [File, Offset] = SM.getDecomposedLoc(SM.getFileLoc(SR.RefLocation)); if (File != this->MainFile) { // Can get here e.g. if there's an #include inside a root Decl. // FIXME: do something more useful than this. llvm::errs() << "Ref location outside file! " << SR.Target << " at " << SR.RefLocation.printToString(SM) << "\n"; return; } int RefIndex = Refs.size(); Refs.emplace_back(Ref{Offset, SR.RT, SR.Target}); Ref &R = Refs.back(); fillTarget(R); for (const auto *I : R.Includes) IncludeRefs[I].push_back(RefIndex); if (R.Type == RefType::Explicit && !R.Satisfied && !R.Insert.empty()) Insertion[R.Insert].push_back(RefIndex); } void write() { OS << "\n"; OS << "\n"; OS << "\n"; OS << "\n"; OS << "\n"; for (const auto &Ins : Insertion) { OS << "\n"; } for (auto &Inc : Includes.all()) { OS << "\n"; } for (unsigned I = 0; I < Refs.size(); ++I) { OS << "\n"; } OS << "\n"; OS << "\n"; writeCode(); OS << "\n"; OS << "\n"; } private: void escapeChar(char C) { switch (C) { case '<': OS << "<"; break; case '&': OS << "&"; break; default: OS << C; } } void escapeString(llvm::StringRef S) { for (char C : S) escapeChar(C); } // Abbreviate a path ('path/to/Foo.h') to just the filename ('Foo.h'). // The full path is available on hover. void printFilename(llvm::StringRef Path) { llvm::StringRef File = llvm::sys::path::filename(Path); if (File == Path) return escapeString(Path); OS << ""; escapeString(File); OS << ""; } // Print a source location in compact style. void printSourceLocation(SourceLocation Loc) { if (Loc.isInvalid()) return escapeString(""); if (!Loc.isMacroID()) return printFilename(Loc.printToString(SM)); // Replicating printToString() is a bit simpler than parsing/reformatting. printFilename(SM.getExpansionLoc(Loc).printToString(SM)); OS << " <Spelling="; printFilename(SM.getSpellingLoc(Loc).printToString(SM)); OS << ">"; } // Write "Provides: " rows of an include or include-insertion table. // These describe the symbols the header provides, referenced by RefIndices. void writeProvides(llvm::ArrayRef RefIndices) { // We show one ref for each symbol: first by (RefType != Explicit, Sequence) llvm::DenseMap FirstRef; for (unsigned RefIndex : RefIndices) { const Ref &R = Refs[RefIndex]; auto I = FirstRef.try_emplace(R.Sym, RefIndex); if (!I.second && R.Type == RefType::Explicit && Refs[I.first->second].Type != RefType::Explicit) I.first->second = RefIndex; } std::vector> Sorted = {FirstRef.begin(), FirstRef.end()}; llvm::stable_sort(Sorted, llvm::less_second{}); for (auto &[S, RefIndex] : Sorted) { auto &R = Refs[RefIndex]; OS << "Provides"; std::string Details = printDetails(S); if (!Details.empty()) { OS << ""; } escapeString(llvm::to_string(S)); if (!Details.empty()) OS << ""; unsigned Line = SM.getLineNumber(MainFile, R.Offset); OS << ", line " << Line << ""; OS << ""; } } void writeInclude(const Include &Inc) { OS << ""; if (Inc.Resolved) { OS << "\n"; writeProvides(IncludeRefs[&Inc]); } OS << "
Resolved"; escapeString(Inc.Resolved->getName()); OS << "
"; } void writeInsertion(llvm::StringRef Text, llvm::ArrayRef Refs) { OS << ""; writeProvides(Refs); OS << "
"; } void writeTarget(const Ref &R) { OS << ""; OS << "\n"; std::string Details = printDetails(R.Sym); if (!Details.empty()) { OS << "\n"; } for (const auto &Loc : R.Locations) { OS << "\n"; } for (const auto &H : R.Headers) { OS << "\n"; } for (const auto *I : R.Includes) { OS << ""; } if (!R.Insert.empty()) { OS << ""; } OS << "
Symbol"; OS << describeSymbol(R.Sym) << " "; escapeString(llvm::to_string(R.Sym)); OS << "
"; escapeString(Details); OS << "
Location"; if (Loc.kind() == SymbolLocation::Physical) // needs SM to print properly. printSourceLocation(Loc.physical()); else escapeString(llvm::to_string(Loc)); OS << "
Header"; switch (H.kind()) { case Header::Physical: printFilename(H.physical().getName()); break; case Header::Standard: OS << "stdlib " << H.standard().name(); break; case Header::Verbatim: OS << "verbatim "; escapeString(H.verbatim()); break; } OS << "
Included"; escapeString(I->quote()); OS << ", line " << I->Line << ""; OS << "
Insert"; escapeString(R.Insert); OS << "
"; } void writeCode() { llvm::StringRef Code = SM.getBufferData(MainFile); OS << "
";

    std::vector Insertions{Insertion.keys().begin(),
                                            Insertion.keys().end()};
    llvm::sort(Insertions);
    for (llvm::StringRef Insertion : Insertions) {
      OS << ""
         << "#include ";
      escapeString(Insertion);
      OS << "\n";
    }

    const Include *Inc = nullptr;
    unsigned LineNum = 0;
    // Lines are , include lines have an inner .
    auto StartLine = [&] {
      ++LineNum;
      OS << "";
      if ((Inc = Includes.atLine(LineNum)))
        OS << "";
    };
    auto EndLine = [&] {
      if (Inc)
        OS << "";
      OS << "\n";
    };

    std::vector RefOrder(Refs.size());
    std::iota(RefOrder.begin(), RefOrder.end(), 0);
    llvm::stable_sort(RefOrder, [&](unsigned A, unsigned B) {
      return std::make_pair(Refs[A].Offset, Refs[A].Type != RefType::Implicit) <
             std::make_pair(Refs[B].Offset, Refs[B].Type != RefType::Implicit);
    });
    auto Rest = llvm::ArrayRef(RefOrder);
    unsigned End = 0;
    StartLine();
    for (unsigned I = 0; I < Code.size(); ++I) {
      // Finish refs early at EOL to avoid dealing with splitting the span.
      if (End && (End == I || Code[I] == '\n')) {
        OS << "";
        End = 0;
      }
      // Handle implicit refs, which are rendered *before* the token.
      while (!Rest.empty() && Refs[Rest.front()].Offset == I &&
             Refs[Rest.front()].Type == RefType::Implicit) {
        const Ref &R = Refs[Rest.front()];
        OS << "";
        Rest = Rest.drop_front();
      };
      // Accumulate all explicit refs that appear on the same token.
      std::string TargetList;
      bool Unsatisfied = false;
      Rest = Rest.drop_while([&](unsigned RefIndex) {
        const Ref &R = Refs[RefIndex];
        if (R.Offset != I)
          return false;
        if (!TargetList.empty())
          TargetList.push_back(',');
        TargetList.push_back('t');
        TargetList.append(std::to_string(RefIndex));
        Unsatisfied = Unsatisfied || !R.Satisfied;
        return true;
      });
      if (!TargetList.empty()) {
        assert(End == 0 && "Overlapping tokens!");
        OS << "";
        End = I + Lexer::MeasureTokenLength(SM.getComposedLoc(MainFile, I), SM,
                                            Ctx.getLangOpts());
      }
      if (Code[I] == '\n') {
        EndLine();
        StartLine();
      } else
        escapeChar(Code[I]);
    }
    EndLine();
    OS << "
\n"; } }; } // namespace void writeHTMLReport(FileID File, const include_cleaner::Includes &Includes, llvm::ArrayRef Roots, llvm::ArrayRef MacroRefs, ASTContext &Ctx, const Preprocessor &PP, PragmaIncludes *PI, llvm::raw_ostream &OS) { Reporter R(OS, Ctx, PP, Includes, PI, File); const auto& SM = Ctx.getSourceManager(); for (Decl *Root : Roots) walkAST(*Root, [&](SourceLocation Loc, const NamedDecl &D, RefType T) { if(!SM.isWrittenInMainFile(SM.getSpellingLoc(Loc))) return; R.addRef(SymbolReference{D, Loc, T}); }); for (const SymbolReference &Ref : MacroRefs) { if (!SM.isWrittenInMainFile(SM.getSpellingLoc(Ref.RefLocation))) continue; R.addRef(Ref); } R.write(); } } // namespace clang::include_cleaner