diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h --- a/clang-tools-extra/clangd/Hover.h +++ b/clang-tools-extra/clangd/Hover.h @@ -15,6 +15,7 @@ #include "clang/Index/IndexSymbol.h" #include <optional> #include <string> +#include <vector> namespace clang { namespace clangd { @@ -110,6 +111,10 @@ }; // Set only if CalleeArgInfo is set. std::optional<PassType> CallPassType; + // Filled when hovering over the #include line. Contains the names of symbols + // from a #include'd file that are used in the main file, sorted in + // alphabetical order. + std::vector<std::string> UsedSymbolNames; /// Produce a user-readable information. markup::Document present() const; diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -12,6 +12,7 @@ #include "CodeCompletionStrings.h" #include "Config.h" #include "FindTarget.h" +#include "Headers.h" #include "IncludeCleaner.h" #include "ParsedAST.h" #include "Selection.h" @@ -38,11 +39,15 @@ #include "clang/AST/RecordLayout.h" #include "clang/AST/Type.h" #include "clang/Basic/CharInfo.h" +#include "clang/Basic/LLVM.h" #include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" #include "clang/Basic/Specifiers.h" #include "clang/Basic/TokenKinds.h" #include "clang/Index/IndexSymbol.h" #include "clang/Tooling/Syntax/Tokens.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/DenseSet.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringExtras.h" @@ -52,6 +57,7 @@ #include "llvm/Support/Format.h" #include "llvm/Support/ScopedPrinter.h" #include "llvm/Support/raw_ostream.h" +#include <algorithm> #include <optional> #include <string> #include <vector> @@ -1134,11 +1140,66 @@ HI.Provider = spellHeader(AST, SM.getFileEntryForID(SM.getMainFileID()), H); } +// FIXME: similar functions are present in FindHeaders.cpp (symbolName) +// and IncludeCleaner.cpp (getSymbolName). Introduce a name() method into +// include_cleaner::Symbol instead. +std::string getSymbolName(include_cleaner::Symbol Sym) { + std::string Name; + switch (Sym.kind()) { + case include_cleaner::Symbol::Declaration: + if (const auto *ND = llvm::dyn_cast<NamedDecl>(&Sym.declaration())) + Name = ND->getDeclName().getAsString(); + break; + case include_cleaner::Symbol::Macro: + Name = Sym.macro().Name->getName(); + break; + } + return Name; +} + +void maybeAddUsedSymbols(ParsedAST &AST, HoverInfo &HI, const Inclusion &Inc) { + const SourceManager &SM = AST.getSourceManager(); + const auto &ConvertedMainFileIncludes = + convertIncludes(SM, AST.getIncludeStructure().MainFileIncludes); + const auto &HoveredInclude = convertIncludes(SM, llvm::ArrayRef{Inc}); + llvm::DenseSet<include_cleaner::Symbol> UsedSymbols; + include_cleaner::walkUsed( + AST.getLocalTopLevelDecls(), collectMacroReferences(AST), + AST.getPragmaIncludes(), SM, + [&](const include_cleaner::SymbolReference &Ref, + llvm::ArrayRef<include_cleaner::Header> Providers) { + if (Ref.RT != include_cleaner::RefType::Explicit || + UsedSymbols.find(Ref.Target) != UsedSymbols.end()) + return; + + for (const include_cleaner::Header &H : Providers) { + auto MatchingIncludes = ConvertedMainFileIncludes.match(H); + // No match for this provider in the main file. + if (MatchingIncludes.empty()) + continue; + + // Check if the hovered include matches this provider. + if (!HoveredInclude.match(H).empty()) + UsedSymbols.insert(Ref.Target); + + // Don't look for rest of the providers once we've found a match + // in the main file. + break; + } + }); + + for (const auto &UsedSymbolDecl : UsedSymbols) + HI.UsedSymbolNames.push_back(getSymbolName(UsedSymbolDecl)); + llvm::sort(HI.UsedSymbolNames); +} + } // namespace std::optional<HoverInfo> getHover(ParsedAST &AST, Position Pos, const format::FormatStyle &Style, const SymbolIndex *Index) { + static constexpr trace::Metric HoverCountMetric( + "hover", trace::Metric::Counter, "case"); PrintingPolicy PP = getPrintingPolicy(AST.getASTContext().getPrintingPolicy()); const SourceManager &SM = AST.getSourceManager(); @@ -1157,12 +1218,14 @@ for (const auto &Inc : AST.getIncludeStructure().MainFileIncludes) { if (Inc.Resolved.empty() || Inc.HashLine != Pos.line) continue; + HoverCountMetric.record(1, "include"); HoverInfo HI; HI.Name = std::string(llvm::sys::path::filename(Inc.Resolved)); // FIXME: We don't have a fitting value for Kind. HI.Definition = URIForFile::canonicalize(Inc.Resolved, AST.tuPath()).file().str(); HI.DefinitionLanguage = ""; + maybeAddUsedSymbols(AST, HI, Inc); return HI; } @@ -1180,6 +1243,7 @@ // Prefer the identifier token as a fallback highlighting range. HighlightRange = Tok.range(SM).toCharRange(SM); if (auto M = locateMacroAt(Tok, AST.getPreprocessor())) { + HoverCountMetric.record(1, "macro"); HI = getHoverContents(*M, Tok, AST); if (auto DefLoc = M->Info->getDefinitionLoc(); DefLoc.isValid()) { include_cleaner::Macro IncludeCleanerMacro{ @@ -1190,6 +1254,7 @@ break; } } else if (Tok.kind() == tok::kw_auto || Tok.kind() == tok::kw_decltype) { + HoverCountMetric.record(1, "keyword"); if (auto Deduced = getDeducedType(AST.getASTContext(), Tok.location())) { HI = getDeducedTypeHoverContents(*Deduced, Tok, AST.getASTContext(), PP, Index); @@ -1216,6 +1281,7 @@ auto Decls = explicitReferenceTargets(N->ASTNode, DeclRelation::Alias, AST.getHeuristicResolver()); if (const auto *DeclToUse = pickDeclToUse(Decls)) { + HoverCountMetric.record(1, "decl"); HI = getHoverContents(DeclToUse, PP, Index, TB); // Layout info only shown when hovering on the field/class itself. if (DeclToUse == N->ASTNode.get<Decl>()) @@ -1226,8 +1292,10 @@ maybeAddCalleeArgInfo(N, *HI, PP); maybeAddSymbolProviders(AST, *HI, include_cleaner::Symbol{*DeclToUse}); } else if (const Expr *E = N->ASTNode.get<Expr>()) { + HoverCountMetric.record(1, "expr"); HI = getHoverContents(N, E, AST, PP, Index); } else if (const Attr *A = N->ASTNode.get<Attr>()) { + HoverCountMetric.record(1, "attribute"); HI = getHoverContents(A, AST); } // FIXME: support hovers for other nodes? @@ -1378,6 +1446,29 @@ Output.addCodeBlock(Buffer, DefinitionLanguage); } + if (!UsedSymbolNames.empty()) { + Output.addRuler(); + markup::Paragraph &P = Output.addParagraph(); + P.appendText("provides "); + + const unsigned long SymbolNamesLimit = 5; + auto Front = + llvm::ArrayRef(UsedSymbolNames) + .take_front(std::min(UsedSymbolNames.size(), SymbolNamesLimit)); + + for (const auto &Sym : Front) { + P.appendCode(Sym); + if (Sym != Front.back()) + P.appendText(", "); + } + + if (UsedSymbolNames.size() > Front.size()) { + P.appendText(" and "); + P.appendText(std::to_string(UsedSymbolNames.size() - Front.size())); + P.appendText(" more"); + } + } + return Output; } diff --git a/clang-tools-extra/clangd/IncludeCleaner.h b/clang-tools-extra/clangd/IncludeCleaner.h --- a/clang-tools-extra/clangd/IncludeCleaner.h +++ b/clang-tools-extra/clangd/IncludeCleaner.h @@ -78,6 +78,9 @@ /// representation. The spelling contains the ""<> characters. std::string spellHeader(ParsedAST &AST, const FileEntry *MainFile, include_cleaner::Header Provider); + +std::vector<include_cleaner::SymbolReference> +collectMacroReferences(ParsedAST &AST); } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/IncludeCleaner.cpp b/clang-tools-extra/clangd/IncludeCleaner.cpp --- a/clang-tools-extra/clangd/IncludeCleaner.cpp +++ b/clang-tools-extra/clangd/IncludeCleaner.cpp @@ -135,27 +135,6 @@ return true; } -std::vector<include_cleaner::SymbolReference> -collectMacroReferences(ParsedAST &AST) { - const auto &SM = AST.getSourceManager(); - // FIXME: !!this is a hacky way to collect macro references. - std::vector<include_cleaner::SymbolReference> Macros; - auto &PP = AST.getPreprocessor(); - for (const syntax::Token &Tok : - AST.getTokens().spelledTokens(SM.getMainFileID())) { - auto Macro = locateMacroAt(Tok, PP); - if (!Macro) - continue; - if (auto DefLoc = Macro->Info->getDefinitionLoc(); DefLoc.isValid()) - Macros.push_back( - {Tok.location(), - include_cleaner::Macro{/*Name=*/PP.getIdentifierInfo(Tok.text(SM)), - DefLoc}, - include_cleaner::RefType::Explicit}); - } - return Macros; -} - llvm::StringRef getResolvedPath(const include_cleaner::Header &SymProvider) { switch (SymProvider.kind()) { case include_cleaner::Header::Physical: @@ -287,6 +266,28 @@ } } // namespace + +std::vector<include_cleaner::SymbolReference> +collectMacroReferences(ParsedAST &AST) { + const auto &SM = AST.getSourceManager(); + // FIXME: !!this is a hacky way to collect macro references. + std::vector<include_cleaner::SymbolReference> Macros; + auto &PP = AST.getPreprocessor(); + for (const syntax::Token &Tok : + AST.getTokens().spelledTokens(SM.getMainFileID())) { + auto Macro = locateMacroAt(Tok, PP); + if (!Macro) + continue; + if (auto DefLoc = Macro->Info->getDefinitionLoc(); DefLoc.isValid()) + Macros.push_back( + {Tok.location(), + include_cleaner::Macro{/*Name=*/PP.getIdentifierInfo(Tok.text(SM)), + DefLoc}, + include_cleaner::RefType::Explicit}); + } + return Macros; +} + include_cleaner::Includes convertIncludes(const SourceManager &SM, const llvm::ArrayRef<Inclusion> Includes) { diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp --- a/clang-tools-extra/clangd/unittests/HoverTests.cpp +++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp @@ -10,6 +10,7 @@ #include "Annotations.h" #include "Config.h" #include "Hover.h" +#include "TestFS.h" #include "TestIndex.h" #include "TestTU.h" #include "index/MemIndex.h" @@ -2980,6 +2981,91 @@ EXPECT_EQ(Case.HI.present().asMarkdown(), Case.ExpectedMarkdown); } +TEST(Hover, UsedSymbols) { + struct { + const char *Code; + const std::function<void(HoverInfo &)> ExpectedBuilder; + } Cases[] = {{R"cpp( + #include ^"bar.h" + int fstBar = bar1(); + int sndBar = bar2(); + Bar bar; + int macroBar = BAR; + )cpp", + [](HoverInfo &HI) { + HI.UsedSymbolNames = {"BAR", "Bar", "bar1", "bar2"}; + }}, + {R"cpp( + #in^clude <vector> + std::vector<int> vec; + )cpp", + [](HoverInfo &HI) { HI.UsedSymbolNames = {"vector"}; }}, + {R"cpp( + #in^clude "public.h" + #include "private.h" + int fooVar = foo(); + )cpp", + [](HoverInfo &HI) { HI.UsedSymbolNames = {"foo"}; }}, + {R"cpp( + #include "bar.h" + #include "for^ward.h" + Bar *x; + )cpp", + [](HoverInfo &HI) { + HI.UsedSymbolNames = { + // No used symbols, since bar.h is a higher-ranked + // provider for Bar. + }; + }}, + {R"cpp( + #include "b^ar.h" + #define DEF(X) const Bar *X + DEF(a); + )cpp", + [](HoverInfo &HI) { HI.UsedSymbolNames = {"Bar"}; }}, + {R"cpp( + #in^clude "bar.h" + #define BAZ(X) const X x + BAZ(Bar); + )cpp", + [](HoverInfo &HI) { HI.UsedSymbolNames = {"Bar"}; }}}; + for (const auto &Case : Cases) { + Annotations Code{Case.Code}; + SCOPED_TRACE(Code.code()); + + TestTU TU; + TU.Filename = "foo.cpp"; + TU.Code = Code.code(); + TU.AdditionalFiles["bar.h"] = guard(R"cpp( + #define BAR 5 + int bar1(); + int bar2(); + class Bar {}; + )cpp"); + TU.AdditionalFiles["private.h"] = guard(R"cpp( + // IWYU pragma: private, include "public.h" + int foo(); + )cpp"); + TU.AdditionalFiles["public.h"] = guard(""); + TU.AdditionalFiles["system/vector"] = guard(R"cpp( + namespace std { + template<typename> + class vector{}; + } + )cpp"); + TU.AdditionalFiles["forward.h"] = guard("class Bar;"); + TU.ExtraArgs.push_back("-isystem" + testPath("system")); + + auto AST = TU.build(); + auto H = getHover(AST, Code.point(), format::getLLVMStyle(), nullptr); + ASSERT_TRUE(H); + HoverInfo Expected; + Case.ExpectedBuilder(Expected); + SCOPED_TRACE(H->present().asMarkdown()); + EXPECT_EQ(H->UsedSymbolNames, Expected.UsedSymbolNames); + } +} + TEST(Hover, DocsFromIndex) { Annotations T(R"cpp( template <typename T> class X {}; @@ -3369,7 +3455,21 @@ R"(stdio.h /usr/include/stdio.h)", - }}; + }, + {[](HoverInfo &HI) { + HI.Name = "foo.h"; + HI.UsedSymbolNames = {"Foo", "Bar", "Baz"}; + }, + R"(foo.h + +provides Foo, Bar, Baz)"}, + {[](HoverInfo &HI) { + HI.Name = "foo.h"; + HI.UsedSymbolNames = {"Foo", "Bar", "Baz", "Foobar", "Qux", "Quux"}; + }, + R"(foo.h + +provides Foo, Bar, Baz, Foobar, Qux and 1 more)"}}; for (const auto &C : Cases) { HoverInfo HI;