//===-- 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 "URI.h" #include "clang-include-cleaner/Record.h" #include "index/SymbolCollector.h" #include "clang/Basic/FileManager.h" #include "clang/Basic/FileSystemOptions.h" #include "clang/Basic/SourceLocation.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/StringRef.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/VirtualFileSystem.h" #include "gmock/gmock-matchers.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include #include #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(hasName, Name, "") { return arg.Name == Name; } MATCHER_P(templateArgs, TemplArgs, "") { return arg.TemplateSpecializationArgs == TemplArgs; } MATCHER_P(hasKind, Kind, "") { return arg.SymInfo.Kind == Kind; } 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); 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; std::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, CollectLocalClassesAndVirtualMethods) { build(R"( namespace nx { auto f() { int Local; auto LocalLambda = [&](){ Local++; class ClassInLambda{}; return Local; }; } // auto ensures function body is parsed. auto foo() { class LocalBase { virtual void LocalVirtual(); void LocalConcrete(); int BaseMember; }; } } // namespace nx )", ""); auto AST = File.build(); EXPECT_FALSE(shouldCollect("Local", /*Qualified=*/false)); EXPECT_TRUE(shouldCollect("ClassInLambda", /*Qualified=*/false)); EXPECT_TRUE(shouldCollect("LocalBase", /*Qualified=*/false)); EXPECT_TRUE(shouldCollect("LocalVirtual", /*Qualified=*/false)); EXPECT_TRUE(shouldCollect("LocalConcrete", /*Qualified=*/false)); EXPECT_FALSE(shouldCollect("BaseMember", /*Qualified=*/false)); 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) : COpts(std::move(COpts)) {} std::unique_ptr create() override { class IndexAction : public ASTFrontendAction { public: IndexAction(std::shared_ptr DataConsumer, const index::IndexingOptions &Opts, std::shared_ptr PI) : DataConsumer(std::move(DataConsumer)), Opts(Opts), PI(std::move(PI)) {} std::unique_ptr CreateASTConsumer(CompilerInstance &CI, llvm::StringRef InFile) override { PI->record(CI); 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; std::shared_ptr PI; }; index::IndexingOptions IndexOpts; IndexOpts.SystemSymbolFilter = index::IndexingOptions::SystemSymbolFilterKind::All; IndexOpts.IndexFunctionLocals = true; std::shared_ptr PI = std::make_shared(); COpts.PragmaIncludes = PI.get(); Collector = std::make_shared(COpts); return std::make_unique(Collector, std::move(IndexOpts), std::move(PI)); } std::shared_ptr Collector; SymbolCollector::Options COpts; }; 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); 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()); // Multiple calls to runSymbolCollector with different contents will fail // to update the filesystem! Why are we sharing one across tests, anyway? EXPECT_TRUE(InMemoryFileSystem->addFile( TestHeaderName, 0, llvm::MemoryBuffer::getMemBuffer(HeaderCode))); EXPECT_TRUE(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; }; 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() { auto LocalLambda = [&](){ class ClassInLambda{}; }; } 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("ClassInLambda"), 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