diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -93,6 +93,8 @@ void onRename(const RenameParams &, Callback); void onHover(const TextDocumentPositionParams &, Callback>); + void onTypeHierarchy(const TypeHierarchyParams &, + Callback>); void onChangeConfiguration(const DidChangeConfigurationParams &); void onSymbolInfo(const TextDocumentPositionParams &, Callback>); diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -368,6 +368,7 @@ {ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND, ExecuteCommandParams::CLANGD_APPLY_TWEAK}}, }}, + {"typeHierarchyProvider", true}, }}}}); } @@ -806,6 +807,13 @@ std::move(Reply)); } +void ClangdLSPServer::onTypeHierarchy( + const TypeHierarchyParams &Params, + Callback> Reply) { + Server->findTypeHierarchy(Params.textDocument.uri.file(), Params.position, + Params.resolve, Params.direction, std::move(Reply)); +} + void ClangdLSPServer::applyConfiguration( const ConfigurationSettings &Settings) { // Per-file update to the compilation database. @@ -885,6 +893,7 @@ MsgHandler->bind("workspace/didChangeWatchedFiles", &ClangdLSPServer::onFileEvent); MsgHandler->bind("workspace/didChangeConfiguration", &ClangdLSPServer::onChangeConfiguration); MsgHandler->bind("textDocument/symbolInfo", &ClangdLSPServer::onSymbolInfo); + MsgHandler->bind("textDocument/typeHierarchy", &ClangdLSPServer::onTypeHierarchy); // clang-format on } diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -184,6 +184,11 @@ void findHover(PathRef File, Position Pos, Callback> CB); + /// Get information about type hierarchy for a given position. + void findTypeHierarchy(PathRef File, Position Pos, int Resolve, + TypeHierarchyDirection Direction, + Callback> CB); + /// Retrieve the top symbols from the workspace matching a query. void workspaceSymbols(StringRef Query, int Limit, Callback> CB); diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -362,9 +362,8 @@ void ClangdServer::applyTweak(PathRef File, Range Sel, StringRef TweakID, Callback CB) { - auto Action = [Sel](decltype(CB) CB, std::string File, - std::string TweakID, - Expected InpAST) { + auto Action = [Sel](decltype(CB) CB, std::string File, std::string TweakID, + Expected InpAST) { if (!InpAST) return CB(InpAST.takeError()); auto Selection = tweakSelection(Sel, *InpAST); @@ -523,6 +522,19 @@ WorkScheduler.runWithAST("Hover", File, Bind(Action, std::move(CB))); } +void ClangdServer::findTypeHierarchy(PathRef File, Position Pos, int Resolve, + TypeHierarchyDirection Direction, + Callback> CB) { + auto Action = [Pos, Resolve, Direction](decltype(CB) CB, + Expected InpAST) { + if (!InpAST) + return CB(InpAST.takeError()); + CB(clangd::getTypeHierarchy(InpAST->AST, Pos, Resolve, Direction)); + }; + + WorkScheduler.runWithAST("Type Hierarchy", File, Bind(Action, std::move(CB))); +} + tooling::CompileCommand ClangdServer::getCompileCommand(PathRef File) { trace::Span Span("GetCompileCommand"); llvm::Optional C = CDB.getCompileCommand(File); diff --git a/clang-tools-extra/clangd/FindSymbols.h b/clang-tools-extra/clangd/FindSymbols.h --- a/clang-tools-extra/clangd/FindSymbols.h +++ b/clang-tools-extra/clangd/FindSymbols.h @@ -13,6 +13,7 @@ #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FINDSYMBOLS_H #include "Protocol.h" +#include "clang/Index/IndexSymbol.h" #include "llvm/ADT/StringRef.h" namespace clang { @@ -20,10 +21,17 @@ class ParsedAST; class SymbolIndex; +// Convert a index::SymbolKind to clangd::SymbolKind (LSP) +// Note, some are not perfect matches and should be improved when this LSP +// issue is addressed: +// https://github.com/Microsoft/language-server-protocol/issues/344 +SymbolKind indexSymbolKindToSymbolKind(index::SymbolKind Kind); + /// Searches for the symbols matching \p Query. The syntax of \p Query can be -/// the non-qualified name or fully qualified of a symbol. For example, "vector" -/// will match the symbol std::vector and "std::vector" would also match it. -/// Direct children of scopes (namepaces, etc) can be listed with a trailing +/// the non-qualified name or fully qualified of a symbol. For example, +/// "vector" will match the symbol std::vector and "std::vector" would also +/// match it. Direct children of scopes (namepaces, etc) can be listed with a +/// trailing /// "::". For example, "std::" will list all children of the std namespace and /// "::" alone will list all children of the global namespace. /// \p Limit limits the number of results returned (0 means no limit). diff --git a/clang-tools-extra/clangd/FindSymbols.cpp b/clang-tools-extra/clangd/FindSymbols.cpp --- a/clang-tools-extra/clangd/FindSymbols.cpp +++ b/clang-tools-extra/clangd/FindSymbols.cpp @@ -26,12 +26,7 @@ namespace clang { namespace clangd { -namespace { -// Convert a index::SymbolKind to clangd::SymbolKind (LSP) -// Note, some are not perfect matches and should be improved when this LSP -// issue is addressed: -// https://github.com/Microsoft/language-server-protocol/issues/344 SymbolKind indexSymbolKindToSymbolKind(index::SymbolKind Kind) { switch (Kind) { case index::SymbolKind::Unknown: @@ -87,6 +82,7 @@ llvm_unreachable("invalid symbol kind"); } +namespace { using ScoredSymbolInfo = std::pair; struct ScoredSymbolGreater { bool operator()(const ScoredSymbolInfo &L, const ScoredSymbolInfo &R) { diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h --- a/clang-tools-extra/clangd/Protocol.h +++ b/clang-tools-extra/clangd/Protocol.h @@ -1014,6 +1014,67 @@ llvm::json::Value toJSON(const DocumentHighlight &DH); llvm::raw_ostream &operator<<(llvm::raw_ostream &, const DocumentHighlight &); +enum class TypeHierarchyDirection { Children = 0, Parents = 1, Both = 2 }; +bool fromJSON(const llvm::json::Value &E, TypeHierarchyDirection &Out); + +/// The type hierarchy params is an extension of the +/// `TextDocumentPositionsParams` with optional properties which can be used to +/// eagerly resolve the item when requesting from the server. +struct TypeHierarchyParams : public TextDocumentPositionParams { + /// The hierarchy levels to resolve. `0` indicates no level. + int resolve = 0; + + /// The direction of the hierarchy levels to resolve. + TypeHierarchyDirection direction = TypeHierarchyDirection::Parents; +}; +bool fromJSON(const llvm::json::Value &, TypeHierarchyParams &); + +struct TypeHierarchyItem { + /// The human readable name of the hierarchy item. + std::string name; + + /// Optional detail for the hierarchy item. It can be, for instance, the + /// signature of a function or method. + llvm::Optional detail; + + /// The kind of the hierarchy item. For instance, class or interface. + SymbolKind kind; + + /// `true` if the hierarchy item is deprecated. Otherwise, `false`. + bool deprecated; + + /// The URI of the text document where this type hierarchy item belongs to. + URIForFile uri; + + /// The range enclosing this type hierarchy item not including + /// leading/trailing whitespace but everything else like comments. This + /// information is typically used to determine if the client's cursor is + /// inside the type hierarch item to reveal in the symbol in the UI. + Range range; + + /// The range that should be selected and revealed when this type hierarchy + /// item is being picked, e.g. the name of a function. Must be contained by + /// the `range`. + Range selectionRange; + + /// If this type hierarchy item is resolved, it contains the direct parents. + /// Could be empty if the item does not have direct parents. If not defined, + /// the parents have not been resolved yet. + llvm::Optional> parents; + + /// If this type hierarchy item is resolved, it contains the direct children + /// of the current item. Could be empty if the item does not have any + /// descendants. If not defined, the children have not been resolved. + llvm::Optional> children; + + /// The protocol has a slot here for an optional 'data' filed, which can + /// be used to identify a type hierarchy item in a resolve request. We don't + /// need this (the item itself is sufficient to identify what to resolve) + /// so don't declare it. +}; +llvm::json::Value toJSON(const TypeHierarchyItem &); +llvm::raw_ostream &operator<<(llvm::raw_ostream &, const TypeHierarchyItem &); + struct ReferenceParams : public TextDocumentPositionParams { // For now, no options like context.includeDeclaration are supported. }; diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp --- a/clang-tools-extra/clangd/Protocol.cpp +++ b/clang-tools-extra/clangd/Protocol.cpp @@ -812,6 +812,66 @@ return true; } +bool fromJSON(const llvm::json::Value &E, TypeHierarchyDirection &Out) { + auto T = E.getAsInteger(); + if (!T) + return false; + if (*T < static_cast(TypeHierarchyDirection::Children) || + *T > static_cast(TypeHierarchyDirection::Both)) + return false; + Out = static_cast(*T); + return true; +} + +bool fromJSON(const llvm::json::Value &Params, TypeHierarchyParams &R) { + llvm::json::ObjectMapper O(Params); + return 0 && O.map("textDocument", R.textDocument) && + O.map("position", R.position) && O.map("resolve", R.resolve) && + O.map("direction", R.direction); +} + +llvm::raw_ostream &operator<<(llvm::raw_ostream &O, + const TypeHierarchyItem &I) { + return O << I.name << " - " << toJSON(I); +} + +llvm::json::Value toJSON(const TypeHierarchyItem &I) { + llvm::json::Object Result{{"name", I.name}, + {"kind", static_cast(I.kind)}, + {"range", I.range}, + {"selectionRange", I.selectionRange}, + {"uri", I.uri}}; + + if (I.detail) + Result["detail"] = I.detail; + if (I.deprecated) + Result["deprecated"] = I.deprecated; + if (I.parents) + Result["parents"] = I.parents; + if (I.children) + Result["children"] = I.children; + return std::move(Result); +} + +bool fromJSON(const llvm::json::Value &Params, TypeHierarchyItem &I) { + llvm::json::ObjectMapper O(Params); + + // Required fields. + if (!(O && O.map("name", I.name) && O.map("kind", I.kind) && + O.map("uri", I.uri) && O.map("range", I.range) && + O.map("selectionRange", I.selectionRange))) { + return false; + } + + // Optional fields. + O.map("detail", I.detail); + O.map("deprecated", I.deprecated); + O.map("parents", I.parents); + O.map("children", I.children); + + return true; +} + bool fromJSON(const llvm::json::Value &Params, ReferenceParams &R) { TextDocumentPositionParams &Base = R; return fromJSON(Params, Base); diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h --- a/clang-tools-extra/clangd/XRefs.h +++ b/clang-tools-extra/clangd/XRefs.h @@ -58,6 +58,17 @@ /// Get info about symbols at \p Pos. std::vector getSymbolInfo(ParsedAST &AST, Position Pos); +/// Find the record type references at \p Pos. +const CXXRecordDecl *findRecordTypeAt(ParsedAST &AST, Position Pos); + +/// Given a record type declaration, find its base (parent) types. +std::vector typeParents(const CXXRecordDecl *CXXRD); + +/// Get type hierarchy information at \p Pos. +llvm::Optional +getTypeHierarchy(ParsedAST &AST, Position Pos, int Resolve, + TypeHierarchyDirection Direction); + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp --- a/clang-tools-extra/clangd/XRefs.cpp +++ b/clang-tools-extra/clangd/XRefs.cpp @@ -7,13 +7,16 @@ //===----------------------------------------------------------------------===// #include "XRefs.h" #include "AST.h" +#include "FindSymbols.h" #include "Logger.h" #include "SourceCode.h" #include "URI.h" #include "index/Merge.h" +#include "index/SymbolCollector.h" #include "index/SymbolLocation.h" #include "clang/AST/DeclTemplate.h" #include "clang/AST/RecursiveASTVisitor.h" +#include "clang/AST/Type.h" #include "clang/Index/IndexDataConsumer.h" #include "clang/Index/IndexingAction.h" #include "clang/Index/USRGeneration.h" @@ -821,5 +824,142 @@ return OS; } +// TODO: Reduce duplication between this function and declToSym(). +static llvm::Optional +declToTypeHierarchyItem(ASTContext &Ctx, const NamedDecl &ND) { + auto &SM = Ctx.getSourceManager(); + + SourceLocation NameLoc = findNameLoc(&ND); + // getFileLoc is a good choice for us, but we also need to make sure + // sourceLocToPosition won't switch files, so we call getSpellingLoc on top of + // that to make sure it does not switch files. + // FIXME: sourceLocToPosition should not switch files! + SourceLocation BeginLoc = SM.getSpellingLoc(SM.getFileLoc(ND.getBeginLoc())); + SourceLocation EndLoc = SM.getSpellingLoc(SM.getFileLoc(ND.getEndLoc())); + if (NameLoc.isInvalid() || BeginLoc.isInvalid() || EndLoc.isInvalid()) + return llvm::None; + + Position NameBegin = sourceLocToPosition(SM, NameLoc); + Position NameEnd = sourceLocToPosition( + SM, Lexer::getLocForEndOfToken(NameLoc, 0, SM, Ctx.getLangOpts())); + + index::SymbolInfo SymInfo = index::getSymbolInfo(&ND); + // FIXME: this is not classifying constructors, destructors and operators + // correctly (they're all "methods"). + SymbolKind SK = indexSymbolKindToSymbolKind(SymInfo.Kind); + + TypeHierarchyItem THI; + THI.name = printName(Ctx, ND); + THI.kind = SK; + THI.deprecated = ND.isDeprecated(); + THI.range = + Range{sourceLocToPosition(SM, BeginLoc), sourceLocToPosition(SM, EndLoc)}; + THI.selectionRange = Range{NameBegin, NameEnd}; + if (!THI.range.contains(THI.selectionRange)) { + // 'selectionRange' must be contained in 'range', so in cases where clang + // reports unrelated ranges we need to reconcile somehow. + THI.range = THI.selectionRange; + } + + auto FilePath = + getCanonicalPath(SM.getFileEntryForID(SM.getFileID(BeginLoc)), SM); + auto TUPath = getCanonicalPath(SM.getFileEntryForID(SM.getMainFileID()), SM); + if (!FilePath || !TUPath) + return llvm::None; // Not useful without a uri. + THI.uri = URIForFile::canonicalize(*FilePath, *TUPath); + + return THI; +} + +static Optional getTypeAncestors(const CXXRecordDecl &CXXRD, + ASTContext &ASTCtx) { + Optional Result = declToTypeHierarchyItem(ASTCtx, CXXRD); + if (!Result) + return Result; + + Result->parents.emplace(); + + for (const CXXRecordDecl *ParentDecl : typeParents(&CXXRD)) { + if (Optional ParentSym = + getTypeAncestors(*ParentDecl, ASTCtx)) { + Result->parents->emplace_back(std::move(*ParentSym)); + } + } + + return Result; +} + +const CXXRecordDecl *findRecordTypeAt(ParsedAST &AST, Position Pos) { + ASTContext &ASTCtx = AST.getASTContext(); + const SourceManager &SourceMgr = ASTCtx.getSourceManager(); + SourceLocation SourceLocationBeg = + getBeginningOfIdentifier(AST, Pos, SourceMgr.getMainFileID()); + IdentifiedSymbol Symbols = getSymbolAtPosition(AST, SourceLocationBeg); + if (Symbols.Decls.empty()) + return nullptr; + + const Decl *D = Symbols.Decls[0]; + + if (const VarDecl *VD = dyn_cast(D)) { + // If this is a variable, use the type of the variable. + return VD->getType().getTypePtr()->getAsCXXRecordDecl(); + } + + if (const CXXMethodDecl *Method = dyn_cast(D)) { + // If this is a method, use the type of the class. + return Method->getParent(); + } + + // We don't handle FieldDecl because it's not clear what behaviour + // the user would expect: the enclosing class type (as with a + // method), or the field's type (as with a variable). + + return dyn_cast(D); +} + +std::vector typeParents(const CXXRecordDecl *CXXRD) { + std::vector Result; + + for (auto Base : CXXRD->bases()) { + const CXXRecordDecl *ParentDecl = nullptr; + + const Type *Type = Base.getType().getTypePtr(); + if (const RecordType *RT = Type->getAs()) { + ParentDecl = RT->getAsCXXRecordDecl(); + } + + if (!ParentDecl) { + // Handle a dependent base such as "Base" by using the primary + // template. + if (const TemplateSpecializationType *TS = + Type->getAs()) { + TemplateName TN = TS->getTemplateName(); + if (TemplateDecl *TD = TN.getAsTemplateDecl()) { + ParentDecl = dyn_cast(TD->getTemplatedDecl()); + } + } + } + + if (ParentDecl) + Result.push_back(ParentDecl); + } + + return Result; +} + +llvm::Optional +getTypeHierarchy(ParsedAST &AST, Position Pos, int ResolveLevels, + TypeHierarchyDirection Direction) { + const CXXRecordDecl *CXXRD = findRecordTypeAt(AST, Pos); + if (!CXXRD) + return llvm::None; + + Optional Result = + getTypeAncestors(*CXXRD, AST.getASTContext()); + // TODO: Resolve type descendants if direction is Children or Both, + // and ResolveLevels > 0. + return Result; +} + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/index/SymbolCollector.h b/clang-tools-extra/clangd/index/SymbolCollector.h --- a/clang-tools-extra/clangd/index/SymbolCollector.h +++ b/clang-tools-extra/clangd/index/SymbolCollector.h @@ -142,6 +142,18 @@ llvm::DenseMap FilesToIndexCache; }; +// Returns a URI of \p Path. Firstly, this makes the \p Path absolute using the +// current working directory of the given SourceManager if the Path is not an +// absolute path. If failed, this resolves relative paths against \p FallbackDir +// to get an absolute path. Then, this tries creating an URI for the absolute +// path with schemes specified in \p Opts. This returns an URI with the first +// working scheme, if there is any; otherwise, this returns None. +// +// The Path can be a path relative to the build directory, or retrieved from +// the SourceManager. +std::string toURI(const SourceManager &SM, llvm::StringRef Path, + llvm::StringRef FallbackDir); + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/index/SymbolCollector.cpp b/clang-tools-extra/clangd/index/SymbolCollector.cpp --- a/clang-tools-extra/clangd/index/SymbolCollector.cpp +++ b/clang-tools-extra/clangd/index/SymbolCollector.cpp @@ -32,27 +32,9 @@ namespace clang { namespace clangd { -namespace { - -/// If \p ND is a template specialization, returns the described template. -/// Otherwise, returns \p ND. -const NamedDecl &getTemplateOrThis(const NamedDecl &ND) { - if (auto T = ND.getDescribedTemplate()) - return *T; - return ND; -} -// Returns a URI of \p Path. Firstly, this makes the \p Path absolute using the -// current working directory of the given SourceManager if the Path is not an -// absolute path. If failed, this resolves relative paths against \p FallbackDir -// to get an absolute path. Then, this tries creating an URI for the absolute -// path with schemes specified in \p Opts. This returns an URI with the first -// working scheme, if there is any; otherwise, this returns None. -// -// The Path can be a path relative to the build directory, or retrieved from -// the SourceManager. std::string toURI(const SourceManager &SM, llvm::StringRef Path, - const SymbolCollector::Options &Opts) { + llvm::StringRef FallbackDir) { llvm::SmallString<128> AbsolutePath(Path); if (auto CanonPath = getCanonicalPath(SM.getFileManager().getFile(Path), SM)) { @@ -60,12 +42,22 @@ } // We don't perform is_absolute check in an else branch because makeAbsolute // might return a relative path on some InMemoryFileSystems. - if (!llvm::sys::path::is_absolute(AbsolutePath) && !Opts.FallbackDir.empty()) - llvm::sys::fs::make_absolute(Opts.FallbackDir, AbsolutePath); + if (!llvm::sys::path::is_absolute(AbsolutePath) && !FallbackDir.empty()) + llvm::sys::fs::make_absolute(FallbackDir, AbsolutePath); llvm::sys::path::remove_dots(AbsolutePath, /*remove_dot_dot=*/true); return URI::create(AbsolutePath).toString(); } +namespace { + +/// If \p ND is a template specialization, returns the described template. +/// Otherwise, returns \p ND. +const NamedDecl &getTemplateOrThis(const NamedDecl &ND) { + if (auto T = ND.getDescribedTemplate()) + return *T; + return ND; +} + // All proto generated headers should start with this line. static const char *PROTO_HEADER_COMMENT = "// Generated by the protocol buffer compiler. DO NOT EDIT!"; @@ -154,7 +146,7 @@ if (Header.startswith("<") || Header.startswith("\"")) return Header.str(); } - return toURI(SM, Header, Opts); + return toURI(SM, Header, Opts.FallbackDir); } // Return the symbol range of the token at \p TokLoc. @@ -194,7 +186,7 @@ auto Path = SM.getFilename(TokLoc); if (Path.empty()) return None; - FileURIStorage = toURI(SM, Path, Opts); + FileURIStorage = toURI(SM, Path, Opts.FallbackDir); SymbolLocation Result; Result.FileURI = FileURIStorage.c_str(); auto Range = getTokenRange(TokLoc, SM, LangOpts); @@ -483,7 +475,7 @@ auto Found = URICache.find(FID); if (Found == URICache.end()) { if (auto *FileEntry = SM.getFileEntryForID(FID)) { - auto FileURI = toURI(SM, FileEntry->getName(), Opts); + auto FileURI = toURI(SM, FileEntry->getName(), Opts.FallbackDir); Found = URICache.insert({FID, FileURI}).first; } else { // Ignore cases where we can not find a corresponding file entry diff --git a/clang-tools-extra/test/clangd/initialize-params.test b/clang-tools-extra/test/clangd/initialize-params.test --- a/clang-tools-extra/test/clangd/initialize-params.test +++ b/clang-tools-extra/test/clangd/initialize-params.test @@ -40,6 +40,7 @@ # CHECK-NEXT: ] # CHECK-NEXT: }, # CHECK-NEXT: "textDocumentSync": 2, +# CHECK-NEXT: "typeHierarchyProvider": true # CHECK-NEXT: "workspaceSymbolProvider": true # CHECK-NEXT: } # CHECK-NEXT: } diff --git a/clang-tools-extra/unittests/clangd/Matchers.h b/clang-tools-extra/unittests/clangd/Matchers.h --- a/clang-tools-extra/unittests/clangd/Matchers.h +++ b/clang-tools-extra/unittests/clangd/Matchers.h @@ -127,6 +127,73 @@ llvm::consumeError(ComputedValue.takeError()); \ } while (false) +// Implements the HasValue(m) matcher for matching an Optional whose +// value matches matcher m. +template class OptionalMatcher { +public: + explicit OptionalMatcher(const InnerMatcher &matcher) : matcher_(matcher) {} + + // This type conversion operator template allows Optional(m) to be + // used as a matcher for any Optional type whose value type is + // compatible with the inner matcher. + // + // The reason we do this instead of relying on + // MakePolymorphicMatcher() is that the latter is not flexible + // enough for implementing the DescribeTo() method of Optional(). + template operator Matcher() const { + return MakeMatcher(new Impl(matcher_)); + } + +private: + // The monomorphic implementation that works for a particular optional type. + template + class Impl : public ::testing::MatcherInterface { + public: + using Value = typename std::remove_const< + typename std::remove_reference::type>::type::value_type; + + explicit Impl(const InnerMatcher &matcher) + : matcher_(::testing::MatcherCast(matcher)) {} + + virtual void DescribeTo(::std::ostream *os) const { + *os << "has a value that "; + matcher_.DescribeTo(os); + } + + virtual void DescribeNegationTo(::std::ostream *os) const { + *os << "does not have a value that "; + matcher_.DescribeTo(os); + } + + virtual bool + MatchAndExplain(Optional optional, + ::testing::MatchResultListener *listener) const { + if (!optional.hasValue()) + return false; + + *listener << "which has a value "; + return MatchPrintAndExplain(*optional, matcher_, listener); + } + + private: + const Matcher matcher_; + + GTEST_DISALLOW_ASSIGN_(Impl); + }; + + const InnerMatcher matcher_; + + GTEST_DISALLOW_ASSIGN_(OptionalMatcher); +}; + +// Creates a matcher that matches an Optional that has a value +// that matches inner_matcher. +template +inline OptionalMatcher +HasValue(const InnerMatcher &inner_matcher) { + return OptionalMatcher(inner_matcher); +} + } // namespace clangd } // namespace clang #endif diff --git a/clang-tools-extra/unittests/clangd/XRefsTests.cpp b/clang-tools-extra/unittests/clangd/XRefsTests.cpp --- a/clang-tools-extra/unittests/clangd/XRefsTests.cpp +++ b/clang-tools-extra/unittests/clangd/XRefsTests.cpp @@ -15,6 +15,8 @@ #include "XRefs.h" #include "index/FileIndex.h" #include "index/SymbolCollector.h" +#include "clang/AST/DeclCXX.h" +#include "clang/AST/DeclTemplate.h" #include "clang/Index/IndexingAction.h" #include "llvm/Support/Path.h" #include "llvm/Support/ScopedPrinter.h" @@ -25,9 +27,13 @@ namespace clangd { namespace { +using testing::AllOf; using testing::ElementsAre; +using testing::Eq; +using testing::Field; using testing::IsEmpty; using testing::Matcher; +using testing::Pointee; using testing::UnorderedElementsAreArray; class IgnoreDiagnostics : public DiagnosticsConsumer { @@ -39,6 +45,15 @@ return Location{URIForFile::canonicalize(File, testRoot()), Range} == arg; } +// GMock helpers for matching TypeHierarchyItem. +MATCHER_P(WithName, N, "") { return arg.name == N; } +MATCHER_P(WithKind, Kind, "") { return arg.kind == Kind; } +MATCHER_P(SelectionRangeIs, R, "") { return arg.selectionRange == R; } +template +testing::Matcher Parents(ParentMatchers... ParentsM) { + return Field(&TypeHierarchyItem::parents, HasValue(ElementsAre(ParentsM...))); +} + // Extracts ranges from an annotated example, and constructs a matcher for a // highlight set. Ranges should be named $read/$write as appropriate. Matcher &> @@ -1478,6 +1493,361 @@ } } +TEST(FindRecordTypeAt, TypeOrVariable) { + Annotations Source(R"cpp( +struct Ch^ild2 { + int c; +}; + +int main() { + Ch^ild2 ch^ild2; + ch^ild2.c = 1; +} +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + for (Position Pt : Source.points()) { + const CXXRecordDecl *RD = findRecordTypeAt(AST, Pt); + EXPECT_EQ(&findDecl(AST, "Child2"), static_cast(RD)); + } +} + +TEST(FindRecordTypeAt, Method) { + Annotations Source(R"cpp( +struct Child2 { + void met^hod (); + void met^hod (int x); +}; + +int main() { + Child2 child2; + child2.met^hod(5); +} +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + for (Position Pt : Source.points()) { + const CXXRecordDecl *RD = findRecordTypeAt(AST, Pt); + EXPECT_EQ(&findDecl(AST, "Child2"), static_cast(RD)); + } +} + +TEST(FindRecordTypeAt, Field) { + Annotations Source(R"cpp( +struct Child2 { + int fi^eld; +}; + +int main() { + Child2 child2; + child2.fi^eld = 5; +} +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + for (Position Pt : Source.points()) { + const CXXRecordDecl *RD = findRecordTypeAt(AST, Pt); + // A field does not unambiguously specify a record type + // (possible associated reocrd types could be the field's type, + // or the type of the record that the field is a member of). + EXPECT_EQ(nullptr, RD); + } +} + +TEST(TypeParents, SimpleInheritance) { + Annotations Source(R"cpp( +struct Parent { + int a; +}; + +struct Child1 : Parent { + int b; +}; + +struct Child2 : Child1 { + int c; +}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent = + dyn_cast(&findDecl(AST, "Parent")); + const CXXRecordDecl *Child1 = + dyn_cast(&findDecl(AST, "Child1")); + const CXXRecordDecl *Child2 = + dyn_cast(&findDecl(AST, "Child2")); + + EXPECT_THAT(typeParents(Parent), ElementsAre()); + EXPECT_THAT(typeParents(Child1), ElementsAre(Parent)); + EXPECT_THAT(typeParents(Child2), ElementsAre(Child1)); +} + +TEST(TypeParents, MultipleInheritance) { + Annotations Source(R"cpp( +struct Parent1 { + int a; +}; + +struct Parent2 { + int b; +}; + +struct Parent3 : Parent2 { + int c; +}; + +struct Child : Parent1, Parent3 { + int d; +}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent1 = + dyn_cast(&findDecl(AST, "Parent1")); + const CXXRecordDecl *Parent2 = + dyn_cast(&findDecl(AST, "Parent2")); + const CXXRecordDecl *Parent3 = + dyn_cast(&findDecl(AST, "Parent3")); + const CXXRecordDecl *Child = dyn_cast(&findDecl(AST, "Child")); + + EXPECT_THAT(typeParents(Parent1), ElementsAre()); + EXPECT_THAT(typeParents(Parent2), ElementsAre()); + EXPECT_THAT(typeParents(Parent3), ElementsAre(Parent2)); + EXPECT_THAT(typeParents(Child), ElementsAre(Parent1, Parent3)); +} + +TEST(TypeParents, ClassTemplate) { + Annotations Source(R"cpp( +struct Parent {}; + +template +struct Child : Parent {}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent = + dyn_cast(&findDecl(AST, "Parent")); + const CXXRecordDecl *Child = + dyn_cast(&findDecl(AST, "Child"))->getTemplatedDecl(); + + EXPECT_THAT(typeParents(Child), ElementsAre(Parent)); +} + +MATCHER_P(ImplicitSpecOf, ClassTemplate, "") { + const ClassTemplateSpecializationDecl *CTS = + dyn_cast(arg); + return CTS && + CTS->getSpecializedTemplate()->getTemplatedDecl() == ClassTemplate && + CTS->getSpecializationKind() == TSK_ImplicitInstantiation; +} + +// This is similar to findDecl(AST, QName), but supports using +// a template-id as a query. +const NamedDecl &findDeclWithTemplateArgs(ParsedAST &AST, + llvm::StringRef Query) { + return findDecl(AST, [&Query](const NamedDecl &ND) { + std::string QName; + llvm::raw_string_ostream OS(QName); + PrintingPolicy Policy(ND.getASTContext().getLangOpts()); + // Use getNameForDiagnostic() which includes the template + // arguments in the printed name. + ND.getNameForDiagnostic(OS, Policy, /*Qualified=*/true); + OS.flush(); + return QName == Query; + }); +} + +TEST(TypeParents, TemplateSpec1) { + Annotations Source(R"cpp( +template +struct Parent {}; + +template <> +struct Parent {}; + +struct Child1 : Parent {}; + +struct Child2 : Parent {}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent = + dyn_cast(&findDecl(AST, "Parent"))->getTemplatedDecl(); + const CXXRecordDecl *ParentSpec = + dyn_cast(&findDeclWithTemplateArgs(AST, "Parent")); + const CXXRecordDecl *Child1 = + dyn_cast(&findDecl(AST, "Child1")); + const CXXRecordDecl *Child2 = + dyn_cast(&findDecl(AST, "Child2")); + + EXPECT_THAT(typeParents(Child1), ElementsAre(ImplicitSpecOf(Parent))); + EXPECT_THAT(typeParents(Child2), ElementsAre(ParentSpec)); +} + +TEST(TypeParents, TemplateSpec2) { + Annotations Source(R"cpp( +struct Parent {}; + +template +struct Child {}; + +template <> +struct Child : Parent {}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent = + dyn_cast(&findDecl(AST, "Parent")); + const CXXRecordDecl *Child = + dyn_cast(&findDecl(AST, "Child"))->getTemplatedDecl(); + const CXXRecordDecl *ChildSpec = + dyn_cast(&findDeclWithTemplateArgs(AST, "Child")); + + EXPECT_THAT(typeParents(Child), ElementsAre()); + EXPECT_THAT(typeParents(ChildSpec), ElementsAre(Parent)); +} + +TEST(TypeParents, DependentBase) { + Annotations Source(R"cpp( +template +struct Parent {}; + +template +struct Child1 : Parent {}; + +template +struct Child2 : Parent::Type {}; + +template +struct Child3 : T {}; +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + ASSERT_TRUE(AST.getDiagnostics().empty()); + + const CXXRecordDecl *Parent = + dyn_cast(&findDecl(AST, "Parent"))->getTemplatedDecl(); + const CXXRecordDecl *Child1 = + dyn_cast(&findDecl(AST, "Child1"))->getTemplatedDecl(); + const CXXRecordDecl *Child2 = + dyn_cast(&findDecl(AST, "Child2"))->getTemplatedDecl(); + const CXXRecordDecl *Child3 = + dyn_cast(&findDecl(AST, "Child3"))->getTemplatedDecl(); + + // For "Parent", use the primary template as a best-effort guess. + EXPECT_THAT(typeParents(Child1), ElementsAre(Parent)); + // For "Parent::Type", there is nothing we can do. + EXPECT_THAT(typeParents(Child2), ElementsAre()); + // Likewise for "T". + EXPECT_THAT(typeParents(Child3), ElementsAre()); +} + +// Parts of getTypeHierarchy() are tested in more detail by the +// FindRecordTypeAt.* and TypeParents.* tests above. This test exercises the +// entire operation. +TEST(TypeHierarchy, Parents) { + Annotations Source(R"cpp( +struct $Parent1Def[[Parent1]] { + int a; +}; + +struct $Parent2Def[[Parent2]] { + int b; +}; + +struct $Parent3Def[[Parent3]] : Parent2 { + int c; +}; + +struct Ch^ild : Parent1, Parent3 { + int d; +}; + +int main() { + Ch^ild ch^ild; + + ch^ild.a = 1; +} +)cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + for (Position Pt : Source.points()) { + // Set ResolveLevels to 0 because it's only used for Children; + // for Parents, getTypeHierarchy() always returns all levels. + llvm::Optional Result = getTypeHierarchy( + AST, Pt, /*ResolveLevels=*/0, TypeHierarchyDirection::Parents); + ASSERT_TRUE(bool(Result)); + EXPECT_THAT( + *Result, + AllOf( + WithName("Child"), WithKind(SymbolKind::Struct), + Parents(AllOf(WithName("Parent1"), WithKind(SymbolKind::Struct), + SelectionRangeIs(Source.range("Parent1Def")), + Parents()), + AllOf(WithName("Parent3"), WithKind(SymbolKind::Struct), + SelectionRangeIs(Source.range("Parent3Def")), + Parents(AllOf( + WithName("Parent2"), WithKind(SymbolKind::Struct), + SelectionRangeIs(Source.range("Parent2Def")), + Parents())))))); + } +} + +TEST(DISABLED_TypeHierarchy, RecursiveHierarchy) { + Annotations Source(R"cpp( + template + struct S : S {}; + + S^<0> s; + )cpp"); + + TestTU TU = TestTU::withCode(Source.code()); + auto AST = TU.build(); + + // The compiler should produce a diagnostic for hitting the + // template instantiation depth. + ASSERT_TRUE(!AST.getDiagnostics().empty()); + + // We're just checking that this doesn't get into an infinite recursion. + getTypeHierarchy(AST, Source.points()[0], 0, TypeHierarchyDirection::Parents); +} + } // namespace } // namespace clangd } // namespace clang