//===-- SymbolCollectorTests.cpp -------------------------------*- C++ -*-===// // // 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 // //===----------------------------------------------------------------------===// #include "Annotations.h" #include "TestFS.h" #include "TestTU.h" #include "index/SymbolCollector.h" #include "clang/Basic/FileManager.h" #include "clang/Basic/FileSystemOptions.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Index/IndexingAction.h" #include "clang/Index/IndexingOptions.h" #include "clang/Tooling/Tooling.h" #include "llvm/ADT/IntrusiveRefCntPtr.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/VirtualFileSystem.h" #include "gmock/gmock-matchers.h" #include "gmock/gmock-more-matchers.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include #include namespace clang { namespace clangd { namespace { using ::testing::_; using ::testing::AllOf; using ::testing::Contains; using ::testing::Each; using ::testing::ElementsAre; using ::testing::Field; using ::testing::IsEmpty; using ::testing::Not; using ::testing::Pair; using ::testing::UnorderedElementsAre; using ::testing::UnorderedElementsAreArray; // GMock helpers for matching Symbol. MATCHER_P(Labeled, Label, "") { return (arg.Name + arg.Signature).str() == Label; } MATCHER_P(ReturnType, D, "") { return arg.ReturnType == D; } MATCHER_P(Doc, D, "") { return arg.Documentation == D; } MATCHER_P(Snippet, S, "") { return (arg.Name + arg.CompletionSnippetSuffix).str() == S; } MATCHER_P(QName, Name, "") { return (arg.Scope + arg.Name).str() == Name; } MATCHER_P(TemplateArgs, TemplArgs, "") { return arg.TemplateSpecializationArgs == TemplArgs; } MATCHER_P(DeclURI, P, "") { return StringRef(arg.CanonicalDeclaration.FileURI) == P; } MATCHER_P(DefURI, P, "") { return StringRef(arg.Definition.FileURI) == P; } MATCHER(IncludeHeader, "") { return !arg.IncludeHeaders.empty(); } MATCHER_P(IncludeHeader, P, "") { return (arg.IncludeHeaders.size() == 1) && (arg.IncludeHeaders.begin()->IncludeHeader == P); } MATCHER_P2(IncludeHeaderWithRef, IncludeHeader, References, "") { return (arg.IncludeHeader == IncludeHeader) && (arg.References == References); } bool rangesMatch(const SymbolLocation &Loc, const Range &R) { return std::make_tuple(Loc.Start.line(), Loc.Start.column(), Loc.End.line(), Loc.End.column()) == std::make_tuple(R.start.line, R.start.character, R.end.line, R.end.character); } MATCHER_P(DeclRange, Pos, "") { return rangesMatch(arg.CanonicalDeclaration, Pos); } MATCHER_P(DefRange, Pos, "") { return rangesMatch(arg.Definition, Pos); } MATCHER_P(RefCount, R, "") { return int(arg.References) == R; } MATCHER_P(ForCodeCompletion, IsIndexedForCodeCompletion, "") { return static_cast(arg.Flags & Symbol::IndexedForCodeCompletion) == IsIndexedForCodeCompletion; } MATCHER(Deprecated, "") { return arg.Flags & Symbol::Deprecated; } MATCHER(ImplementationDetail, "") { return arg.Flags & Symbol::ImplementationDetail; } MATCHER(VisibleOutsideFile, "") { return static_cast(arg.Flags & Symbol::VisibleOutsideFile); } MATCHER(RefRange, "") { const Ref &Pos = ::testing::get<0>(arg); const Range &Range = ::testing::get<1>(arg); return rangesMatch(Pos.Location, Range); } MATCHER_P2(OverriddenBy, Subject, Object, "") { return arg == Relation{Subject.ID, RelationKind::OverriddenBy, Object.ID}; } ::testing::Matcher &> HaveRanges(const std::vector Ranges) { return ::testing::UnorderedPointwise(RefRange(), Ranges); } class ShouldCollectSymbolTest : public ::testing::Test { public: void build(llvm::StringRef HeaderCode, llvm::StringRef Code = "") { File.HeaderFilename = HeaderName; File.Filename = FileName; File.HeaderCode = std::string(HeaderCode); File.Code = std::string(Code); AST = File.build(); } // build() must have been called. bool shouldCollect(llvm::StringRef Name, bool Qualified = true) { assert(AST.hasValue()); const NamedDecl &ND = Qualified ? findDecl(*AST, Name) : findUnqualifiedDecl(*AST, Name); const SourceManager &SM = AST->getSourceManager(); bool MainFile = isInsideMainFile(ND.getBeginLoc(), SM); return SymbolCollector::shouldCollectSymbol( ND, AST->getASTContext(), SymbolCollector::Options(), MainFile); } protected: std::string HeaderName = "f.h"; std::string FileName = "f.cpp"; TestTU File; llvm::Optional AST; // Initialized after build. }; TEST_F(ShouldCollectSymbolTest, ShouldCollectSymbol) { build(R"( namespace nx { class X{}; auto f() { int Local; } // auto ensures function body is parsed. struct { int x; } var; } )", R"( class InMain {}; namespace { class InAnonymous {}; } static void g(); )"); auto AST = File.build(); EXPECT_TRUE(shouldCollect("nx")); EXPECT_TRUE(shouldCollect("nx::X")); EXPECT_TRUE(shouldCollect("nx::f")); EXPECT_TRUE(shouldCollect("InMain")); EXPECT_TRUE(shouldCollect("InAnonymous", /*Qualified=*/false)); EXPECT_TRUE(shouldCollect("g")); EXPECT_FALSE(shouldCollect("Local", /*Qualified=*/false)); } TEST_F(ShouldCollectSymbolTest, NoPrivateProtoSymbol) { HeaderName = "f.proto.h"; build( R"(// Generated by the protocol buffer compiler. DO NOT EDIT! namespace nx { class Top_Level {}; class TopLevel {}; enum Kind { KIND_OK, Kind_Not_Ok, }; })"); EXPECT_TRUE(shouldCollect("nx::TopLevel")); EXPECT_TRUE(shouldCollect("nx::Kind::KIND_OK")); EXPECT_TRUE(shouldCollect("nx::Kind")); EXPECT_FALSE(shouldCollect("nx::Top_Level")); EXPECT_FALSE(shouldCollect("nx::Kind::Kind_Not_Ok")); } TEST_F(ShouldCollectSymbolTest, DoubleCheckProtoHeaderComment) { HeaderName = "f.proto.h"; build(R"( namespace nx { class Top_Level {}; enum Kind { Kind_Fine }; } )"); EXPECT_TRUE(shouldCollect("nx::Top_Level")); EXPECT_TRUE(shouldCollect("nx::Kind_Fine")); } class SymbolIndexActionFactory : public tooling::FrontendActionFactory { public: SymbolIndexActionFactory(SymbolCollector::Options COpts, CommentHandler *PragmaHandler) : COpts(std::move(COpts)), PragmaHandler(PragmaHandler) {} std::unique_ptr create() override { class IndexAction : public ASTFrontendAction { public: IndexAction(std::shared_ptr DataConsumer, const index::IndexingOptions &Opts, CommentHandler *PragmaHandler) : DataConsumer(std::move(DataConsumer)), Opts(Opts), PragmaHandler(PragmaHandler) {} std::unique_ptr CreateASTConsumer(CompilerInstance &CI, llvm::StringRef InFile) override { if (PragmaHandler) CI.getPreprocessor().addCommentHandler(PragmaHandler); return createIndexingASTConsumer(DataConsumer, Opts, CI.getPreprocessorPtr()); } bool BeginInvocation(CompilerInstance &CI) override { // Make the compiler parse all comments. CI.getLangOpts().CommentOpts.ParseAllComments = true; return true; } private: std::shared_ptr DataConsumer; index::IndexingOptions Opts; CommentHandler *PragmaHandler; }; index::IndexingOptions IndexOpts; IndexOpts.SystemSymbolFilter = index::IndexingOptions::SystemSymbolFilterKind::All; IndexOpts.IndexFunctionLocals = false; Collector = std::make_shared(COpts); return std::make_unique(Collector, std::move(IndexOpts), PragmaHandler); } std::shared_ptr Collector; SymbolCollector::Options COpts; CommentHandler *PragmaHandler; }; class SymbolCollectorTest : public ::testing::Test { public: SymbolCollectorTest() : InMemoryFileSystem(new llvm::vfs::InMemoryFileSystem), TestHeaderName(testPath("symbol.h")), TestFileName(testPath("symbol.cc")) { TestHeaderURI = URI::create(TestHeaderName).toString(); TestFileURI = URI::create(TestFileName).toString(); } // Note that unlike TestTU, no automatic header guard is added. // HeaderCode should start with #pragma once to be treated as modular. bool runSymbolCollector(llvm::StringRef HeaderCode, llvm::StringRef MainCode, const std::vector &ExtraArgs = {}) { llvm::IntrusiveRefCntPtr Files( new FileManager(FileSystemOptions(), InMemoryFileSystem)); auto Factory = std::make_unique( CollectorOpts, PragmaHandler.get()); std::vector Args = {"symbol_collector", "-fsyntax-only", "-xc++", "-include", TestHeaderName}; Args.insert(Args.end(), ExtraArgs.begin(), ExtraArgs.end()); // This allows to override the "-xc++" with something else, i.e. // -xobjective-c++. Args.push_back(TestFileName); tooling::ToolInvocation Invocation( Args, Factory->create(), Files.get(), std::make_shared()); InMemoryFileSystem->addFile(TestHeaderName, 0, llvm::MemoryBuffer::getMemBuffer(HeaderCode)); InMemoryFileSystem->addFile(TestFileName, 0, llvm::MemoryBuffer::getMemBuffer(MainCode)); Invocation.run(); Symbols = Factory->Collector->takeSymbols(); Refs = Factory->Collector->takeRefs(); Relations = Factory->Collector->takeRelations(); return true; } protected: llvm::IntrusiveRefCntPtr InMemoryFileSystem; std::string TestHeaderName; std::string TestHeaderURI; std::string TestFileName; std::string TestFileURI; SymbolSlab Symbols; RefSlab Refs; RelationSlab Relations; SymbolCollector::Options CollectorOpts; std::unique_ptr PragmaHandler; }; TEST_F(SymbolCollectorTest, CollectSymbols) { const std::string Header = R"( class Foo { Foo() {} Foo(int a) {} void f(); friend void f1(); friend class Friend; Foo& operator=(const Foo&); ~Foo(); class Nested { void f(); }; }; class Friend { }; void f1(); inline void f2() {} static const int KInt = 2; const char* kStr = "123"; namespace { void ff() {} // ignore } void f1() {} namespace foo { // Type alias typedef int int32; using int32_t = int32; // Variable int v1; // Namespace namespace bar { int v2; } // Namespace alias namespace baz = bar; using bar::v2; } // namespace foo )"; runSymbolCollector(Header, /*Main=*/""); EXPECT_THAT(Symbols, UnorderedElementsAreArray( {AllOf(QName("Foo"), ForCodeCompletion(true)), AllOf(QName("Foo::Foo"), ForCodeCompletion(false)), AllOf(QName("Foo::Foo"), ForCodeCompletion(false)), AllOf(QName("Foo::f"), ForCodeCompletion(false)), AllOf(QName("Foo::~Foo"), ForCodeCompletion(false)), AllOf(QName("Foo::operator="), ForCodeCompletion(false)), AllOf(QName("Foo::Nested"), ForCodeCompletion(false)), AllOf(QName("Foo::Nested::f"), ForCodeCompletion(false)), AllOf(QName("Friend"), ForCodeCompletion(true)), AllOf(QName("f1"), ForCodeCompletion(true)), AllOf(QName("f2"), ForCodeCompletion(true)), AllOf(QName("KInt"), ForCodeCompletion(true)), AllOf(QName("kStr"), ForCodeCompletion(true)), AllOf(QName("foo"), ForCodeCompletion(true)), AllOf(QName("foo::bar"), ForCodeCompletion(true)), AllOf(QName("foo::int32"), ForCodeCompletion(true)), AllOf(QName("foo::int32_t"), ForCodeCompletion(true)), AllOf(QName("foo::v1"), ForCodeCompletion(true)), AllOf(QName("foo::bar::v2"), ForCodeCompletion(true)), AllOf(QName("foo::v2"), ForCodeCompletion(true)), AllOf(QName("foo::baz"), ForCodeCompletion(true))})); } TEST_F(SymbolCollectorTest, FileLocal) { const std::string Header = R"( class Foo {}; namespace { class Ignored {}; } void bar(); )"; const std::string Main = R"( class ForwardDecl; void bar() {} static void a(); class B {}; namespace { void c(); } )"; runSymbolCollector(Header, Main); EXPECT_THAT(Symbols, UnorderedElementsAre( AllOf(QName("Foo"), VisibleOutsideFile()), AllOf(QName("bar"), VisibleOutsideFile()), AllOf(QName("a"), Not(VisibleOutsideFile())), AllOf(QName("B"), Not(VisibleOutsideFile())), AllOf(QName("c"), Not(VisibleOutsideFile())), // FIXME: ForwardDecl likely *is* visible outside. AllOf(QName("ForwardDecl"), Not(VisibleOutsideFile())))); } TEST_F(SymbolCollectorTest, Template) { Annotations Header(R"( // Primary template and explicit specialization are indexed, instantiation // is not. template struct [[Tmpl]] {T $xdecl[[x]] = 0;}; template <> struct $specdecl[[Tmpl]] {}; template struct $partspecdecl[[Tmpl]] {}; extern template struct Tmpl; template struct Tmpl; )"); runSymbolCollector(Header.code(), /*Main=*/""); EXPECT_THAT(Symbols, UnorderedElementsAre( AllOf(QName("Tmpl"), DeclRange(Header.range()), ForCodeCompletion(true)), AllOf(QName("Tmpl"), DeclRange(Header.range("specdecl")), ForCodeCompletion(false)), AllOf(QName("Tmpl"), DeclRange(Header.range("partspecdecl")), ForCodeCompletion(false)), AllOf(QName("Tmpl::x"), DeclRange(Header.range("xdecl")), ForCodeCompletion(false)))); } TEST_F(SymbolCollectorTest, TemplateArgs) { Annotations Header(R"( template class $barclasstemp[[Bar]] {}; template class Z, int Q> struct [[Tmpl]] { T $xdecl[[x]] = 0; }; // template-template, non-type and type full spec template <> struct $specdecl[[Tmpl]] {}; // template-template, non-type and type partial spec template struct $partspecdecl[[Tmpl]] {}; // instantiation extern template struct Tmpl; // instantiation template struct Tmpl; template class $fooclasstemp[[Foo]] {}; // parameter-packs full spec template<> class $parampack[[Foo]], int, double> {}; // parameter-packs partial spec template class $parampackpartial[[Foo]] {}; template class $bazclasstemp[[Baz]] {}; // non-type parameter-packs full spec template<> class $parampacknontype[[Baz]]<3, 5, 8> {}; // non-type parameter-packs partial spec template class $parampacknontypepartial[[Baz]] {}; template