From 8238ad159c0b8123c03d953d18340147c72372e9 Mon Sep 17 00:00:00 2001 From: Nathan Ridge Date: Sun, 6 Oct 2024 00:41:48 -0400 Subject: [PATCH] [clangd] Add a unit test suite for HeuristicResolver Fixes https://github.com/clangd/clangd/issues/2154 --- clang-tools-extra/clangd/HeuristicResolver.h | 5 +- .../clangd/unittests/CMakeLists.txt | 1 + .../unittests/HeuristicResolverTests.cpp | 521 ++++++++++++++++++ 3 files changed, 525 insertions(+), 2 deletions(-) create mode 100644 clang-tools-extra/clangd/unittests/HeuristicResolverTests.cpp diff --git a/clang-tools-extra/clangd/HeuristicResolver.h b/clang-tools-extra/clangd/HeuristicResolver.h index dcc063bbc4adc0..c130e0677e86dd 100644 --- a/clang-tools-extra/clangd/HeuristicResolver.h +++ b/clang-tools-extra/clangd/HeuristicResolver.h @@ -26,13 +26,14 @@ class UnresolvedUsingValueDecl; namespace clangd { -// This class heuristic resolution of declarations and types in template code. +// This class handles heuristic resolution of declarations and types in template +// code. // // As a compiler, clang only needs to perform certain types of processing on // template code (such as resolving dependent names to declarations, or // resolving the type of a dependent expression) after instantiation. Indeed, // C++ language features such as template specialization mean such resolution -// cannot be done accurately before instantiation +// cannot be done accurately before instantiation. // // However, template code is written and read in uninstantiated form, and clangd // would like to provide editor features like go-to-definition in template code diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt index dffdcd5d014ca9..8dba8088908d5e 100644 --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -64,6 +64,7 @@ add_unittest(ClangdUnitTests ClangdTests GlobalCompilationDatabaseTests.cpp HeadersTests.cpp HeaderSourceSwitchTests.cpp + HeuristicResolverTests.cpp HoverTests.cpp IncludeCleanerTests.cpp IndexActionTests.cpp diff --git a/clang-tools-extra/clangd/unittests/HeuristicResolverTests.cpp b/clang-tools-extra/clangd/unittests/HeuristicResolverTests.cpp new file mode 100644 index 00000000000000..5665fb2519267f --- /dev/null +++ b/clang-tools-extra/clangd/unittests/HeuristicResolverTests.cpp @@ -0,0 +1,521 @@ +//===-- HeuristicResolverTests.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 "HeuristicResolver.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Tooling/Tooling.h" +#include "gmock/gmock-matchers.h" +#include "gtest/gtest.h" + +using namespace clang::ast_matchers; +using clang::clangd::HeuristicResolver; +using testing::ElementsAre; + +namespace clang { +namespace { + +// Helper for matching a sequence of elements with a variadic list of matchers. +// Usage: `ElementsAre(matchAdapter(Vs, MatchFunction)...)`, where `Vs...` is +// a variadic list of matchers. +// For each `V` in `Vs`, this will match the corresponding element `E` if +// `MatchFunction(V, E)` is true. +MATCHER_P2(matchAdapter, MatcherForElement, MatchFunction, "matchAdapter") { + return MatchFunction(MatcherForElement, arg); +} + +template +using ResolveFnT = std::function( + const HeuristicResolver *, const InputNode *)>; + +// Test heuristic resolution on `Code` using the resolution procedure +// `ResolveFn`, which takes a `HeuristicResolver` and an input AST node of type +// `InputNode` and returns a `std::vector`. +// `InputMatcher` should be an AST matcher that matches a single node to pass as +// input to `ResolveFn`, bound to the ID "input". `OutputMatchers` should be AST +// matchers that each match a single node, bound to the ID "output". +template +void expectResolution(llvm::StringRef Code, ResolveFnT ResolveFn, + const InputMatcher &IM, const OutputMatchers &...OMS) { + auto TU = tooling::buildASTFromCodeWithArgs(Code, {"-std=c++20"}); + auto &Ctx = TU->getASTContext(); + auto InputMatches = match(IM, Ctx); + ASSERT_EQ(1u, InputMatches.size()); + const auto *Input = InputMatches[0].template getNodeAs("input"); + ASSERT_TRUE(Input); + + auto OutputNodeMatches = [&](auto &OutputMatcher, auto &Actual) { + auto OutputMatches = match(OutputMatcher, Ctx); + if (OutputMatches.size() != 1u) + return false; + const auto *ExpectedOutput = + OutputMatches[0].template getNodeAs("output"); + if (!ExpectedOutput) + return false; + return ExpectedOutput == Actual; + }; + + HeuristicResolver H(Ctx); + auto Results = ResolveFn(&H, Input); + EXPECT_THAT(Results, ElementsAre(matchAdapter(OMS, OutputNodeMatches)...)); +} + +// Wrapper for the above that accepts a HeuristicResolver member function +// pointer directly. +template +void expectResolution(llvm::StringRef Code, + std::vector ( + HeuristicResolver::*ResolveFn)(const InputNode *) + const, + const InputMatcher &IM, const OutputMatchers &...OMS) { + expectResolution(Code, ResolveFnT(std::mem_fn(ResolveFn)), IM, + OMS...); +} + +TEST(HeuristicResolver, MemberExpr) { + std::string Code = R"cpp( + template + struct S { + void bar() {} + }; + + template + void foo(S arg) { + arg.bar(); + } + )cpp"; + // Test resolution of "bar" in "arg.bar()". + expectResolution(Code, &HeuristicResolver::resolveMemberExpr, + cxxDependentScopeMemberExpr().bind("input"), + cxxMethodDecl(hasName("bar")).bind("output")); +} + +TEST(HeuristicResolver, MemberExpr_Overloads) { + std::string Code = R"cpp( + template + struct S { + void bar(int); + void bar(float); + }; + + template + void foo(S arg, U u) { + arg.bar(u); + } + )cpp"; + // Test resolution of "bar" in "arg.bar(u)". Both overloads should be found. + expectResolution( + Code, &HeuristicResolver::resolveMemberExpr, + cxxDependentScopeMemberExpr().bind("input"), + cxxMethodDecl(hasName("bar"), hasParameter(0, hasType(asString("int")))) + .bind("output"), + cxxMethodDecl(hasName("bar"), hasParameter(0, hasType(asString("float")))) + .bind("output")); +} + +TEST(HeuristicResolver, MemberExpr_SmartPointer) { + std::string Code = R"cpp( + template struct S { void foo() {} }; + template struct unique_ptr { + T* operator->(); + }; + template + void test(unique_ptr>& v) { + v->foo(); + } + )cpp"; + // Test resolution of "foo" in "v->foo()". + expectResolution(Code, &HeuristicResolver::resolveMemberExpr, + cxxDependentScopeMemberExpr().bind("input"), + cxxMethodDecl(hasName("foo")).bind("output")); +} + +TEST(HeuristicResolver, MemberExpr_Chained) { + std::string Code = R"cpp( + struct A { void foo() {} }; + template + struct B { + A func(int); + void bar() { + func(1).foo(); + } + }; + )cpp"; + // Test resolution of "foo" in "func(1).foo()". + expectResolution(Code, &HeuristicResolver::resolveMemberExpr, + cxxDependentScopeMemberExpr().bind("input"), + cxxMethodDecl(hasName("foo")).bind("output")); +} + +TEST(HeuristicResolver, MemberExpr_TemplateArgs) { + std::string Code = R"cpp( + struct Foo { + static Foo k(int); + template T convert(); + }; + template + void test() { + Foo::k(T()).template convert(); + } + )cpp"; + // Test resolution of "convert" in "Foo::k(T()).template convert()". + expectResolution(Code, &HeuristicResolver::resolveMemberExpr, + cxxDependentScopeMemberExpr().bind("input"), + functionTemplateDecl(hasName("convert")).bind("output")); +} + +TEST(HeuristicResolver, MemberExpr_TypeAlias) { + std::string Code = R"cpp( + template + struct Waldo { + void find(); + }; + template + using Wally = Waldo; + template + void foo(Wally w) { + w.find(); + } + )cpp"; + // Test resolution of "find" in "w.find()". + expectResolution(Code, &HeuristicResolver::resolveMemberExpr, + cxxDependentScopeMemberExpr().bind("input"), + cxxMethodDecl(hasName("find")).bind("output")); +} + +TEST(HeuristicResolver, MemberExpr_BaseClass_TypeAlias) { + std::string Code = R"cpp( + template + struct Waldo { + void find(); + }; + template + using Wally = Waldo; + template + struct S : Wally { + void foo() { + this->find(); + } + }; + )cpp"; + // Test resolution of "find" in "this->find()". + expectResolution(Code, &HeuristicResolver::resolveMemberExpr, + cxxDependentScopeMemberExpr().bind("input"), + cxxMethodDecl(hasName("find")).bind("output")); +} + +TEST(HeuristicResolver, MemberExpr_Metafunction) { + std::string Code = R"cpp( + template + struct Waldo { + void find(); + }; + template + struct MetaWaldo { + using Type = Waldo; + }; + template + void foo(typename MetaWaldo::Type w) { + w.find(); + } + )cpp"; + // Test resolution of "find" in "w.find()". + expectResolution(Code, &HeuristicResolver::resolveMemberExpr, + cxxDependentScopeMemberExpr().bind("input"), + cxxMethodDecl(hasName("find")).bind("output")); +} + +TEST(HeuristicResolver, MemberExpr_DeducedNonTypeTemplateParameter) { + std::string Code = R"cpp( + template + struct Waldo { + const int found = N; + }; + template + int foo() { + return W.found; + } + )cpp"; + // Test resolution of "found" in "W.found". + expectResolution(Code, &HeuristicResolver::resolveMemberExpr, + cxxDependentScopeMemberExpr().bind("input"), + fieldDecl(hasName("found")).bind("output")); +} + +TEST(HeuristicResolver, DeclRefExpr_StaticMethod) { + std::string Code = R"cpp( + template + struct S { + static void bar() {} + }; + + template + void foo() { + S::bar(); + } + )cpp"; + // Test resolution of "bar" in "S::bar()". + expectResolution(Code, &HeuristicResolver::resolveDeclRefExpr, + dependentScopeDeclRefExpr().bind("input"), + cxxMethodDecl(hasName("bar")).bind("output")); +} + +TEST(HeuristicResolver, DeclRefExpr_StaticOverloads) { + std::string Code = R"cpp( + template + struct S { + static void bar(int); + static void bar(float); + }; + + template + void foo(U u) { + S::bar(u); + } + )cpp"; + // Test resolution of "bar" in "S::bar(u)". Both overloads should be found. + expectResolution( + Code, &HeuristicResolver::resolveDeclRefExpr, + dependentScopeDeclRefExpr().bind("input"), + cxxMethodDecl(hasName("bar"), hasParameter(0, hasType(asString("int")))) + .bind("output"), + cxxMethodDecl(hasName("bar"), hasParameter(0, hasType(asString("float")))) + .bind("output")); +} + +TEST(HeuristicResolver, DeclRefExpr_Enumerator) { + std::string Code = R"cpp( + template + struct Foo { + enum class E { A, B }; + E e = E::A; + }; + )cpp"; + // Test resolution of "A" in "E::A". + expectResolution(Code, &HeuristicResolver::resolveDeclRefExpr, + dependentScopeDeclRefExpr().bind("input"), + enumConstantDecl(hasName("A")).bind("output")); +} + +TEST(HeuristicResolver, DeclRefExpr_RespectScope) { + std::string Code = R"cpp( + template + struct PointerIntPair { + void *getPointer() const { return Info::getPointer(); } + }; + )cpp"; + // Test resolution of "getPointer" in "Info::getPointer()". + // Here, we are testing that we do not incorrectly get the enclosing + // getPointer() function as a result. + expectResolution(Code, &HeuristicResolver::resolveDeclRefExpr, + dependentScopeDeclRefExpr().bind("input")); +} + +TEST(HeuristicResolver, DependentNameType) { + std::string Code = R"cpp( + template + struct A { + struct B {}; + }; + template + void foo(typename A::B); + )cpp"; + // Tests resolution of "B" in "A::B". + expectResolution(Code, &HeuristicResolver::resolveDependentNameType, + dependentNameType().bind("input"), + classTemplateDecl(has(cxxRecordDecl( + has(cxxRecordDecl(hasName("B")).bind("output")))))); +} + +TEST(HeuristicResolver, DependentNameType_Nested) { + std::string Code = R"cpp( + template + struct A { + struct B { + struct C {}; + }; + }; + template + void foo(typename A::B::C); + )cpp"; + // Tests resolution of "C" in "A::B::C". + expectResolution(Code, &HeuristicResolver::resolveDependentNameType, + dependentNameType().bind("input"), + classTemplateDecl(has(cxxRecordDecl(has(cxxRecordDecl( + has(cxxRecordDecl(hasName("C")).bind("output")))))))); +} + +TEST(HeuristicResolver, DependentNameType_Recursion) { + std::string Code = R"cpp( + template + struct Waldo { + using Type = typename Waldo::Type::Next; + }; + )cpp"; + // Test resolution of "Next" in "typename Waldo::Type::Next". + // Here, we are testing that we do not get into an infinite recursion. + expectResolution(Code, &HeuristicResolver::resolveDependentNameType, + dependentNameType().bind("input")); +} + +TEST(HeuristicResolver, DependentNameType_MutualRecursion) { + std::string Code = R"cpp( + template + struct Odd; + template + struct Even { + using Type = typename Odd::Type::Next; + }; + template + struct Odd { + using Type = typename Even::Type::Next; + }; + )cpp"; + // Test resolution of "Next" in "typename Even::Type::Next". + // Similar to the above but we have two mutually recursive templates. + expectResolution( + Code, &HeuristicResolver::resolveDependentNameType, + classTemplateDecl(hasName("Odd"), has(cxxRecordDecl(has(typeAliasDecl( + hasType(type().bind("input")))))))); +} + +// FIXME: Make this nicer (reuse code in impl.) +TEST(HeuristicResolver, NestedNameSpecifier) { + // Test resolution of "B" in "A::B::C". + // Unlike the "C", the "B" does not get its own DependentNameTypeLoc node, + // so the resolution uses the NestedNameSpecifier as input. + std::string Code = R"cpp( + template + struct A { + struct B { + struct C {}; + }; + }; + template + void foo(typename A::B::C); + )cpp"; + // Adapt the call to resolveNestedNameSpecifierToType() to the interface + // expected by expectResolution() (returning a vector of decls). + ResolveFnT ResolveFn = + [](const HeuristicResolver *H, + const NestedNameSpecifier *NNS) -> std::vector { + return {H->resolveNestedNameSpecifierToType(NNS)->getAsCXXRecordDecl()}; + }; + expectResolution(Code, ResolveFn, + nestedNameSpecifier(hasPrefix(specifiesType(hasDeclaration( + classTemplateDecl(hasName("A")))))) + .bind("input"), + classTemplateDecl(has(cxxRecordDecl( + has(cxxRecordDecl(hasName("B")).bind("output")))))); +} + +TEST(HeuristicResolver, TemplateSpecializationType) { + std::string Code = R"cpp( + template + struct A { + template + struct B {}; + }; + template + void foo(typename A::template B); + )cpp"; + // Test resolution of "B" in "A::template B". + expectResolution(Code, &HeuristicResolver::resolveTemplateSpecializationType, + functionDecl(hasParameter(0, hasType(type().bind("input")))), + classTemplateDecl(has(cxxRecordDecl( + has(classTemplateDecl(hasName("B")).bind("output")))))); +} + +TEST(HeuristicResolver, DependentCall_NonMember) { + std::string Code = R"cpp( + template + void nonmember(T); + template + void bar(T t) { + nonmember(t); + } + )cpp"; + // Test resolution of "nonmember" in "nonmember(t)". + expectResolution(Code, &HeuristicResolver::resolveCalleeOfCallExpr, + callExpr().bind("input"), + functionTemplateDecl(hasName("nonmember")).bind("output")); +} + +TEST(HeuristicResolver, DependentCall_Member) { + std::string Code = R"cpp( + template + struct A { + void member(T); + }; + template + void bar(A a, T t) { + a.member(t); + } + )cpp"; + // Test resolution of "member" in "a.member(t)". + expectResolution(Code, &HeuristicResolver::resolveCalleeOfCallExpr, + callExpr().bind("input"), + cxxMethodDecl(hasName("member")).bind("output")); +} + +TEST(HeuristicResolver, DependentCall_StaticMember) { + std::string Code = R"cpp( + template + struct A { + static void static_member(T); + }; + template + void bar(T t) { + A::static_member(t); + } + )cpp"; + // Test resolution of "static_member" in "A::static_member(t)". + expectResolution(Code, &HeuristicResolver::resolveCalleeOfCallExpr, + callExpr().bind("input"), + cxxMethodDecl(hasName("static_member")).bind("output")); +} + +TEST(HeuristicResolver, DependentCall_Overload) { + std::string Code = R"cpp( + void overload(int); + void overload(double); + template + void bar(T t) { + overload(t); + } + )cpp"; + // Test resolution of "overload" in "overload(t)". Both overload should be + // found. + expectResolution(Code, &HeuristicResolver::resolveCalleeOfCallExpr, + callExpr().bind("input"), + functionDecl(hasName("overload"), + hasParameter(0, hasType(asString("double")))) + .bind("output"), + functionDecl(hasName("overload"), + hasParameter(0, hasType(asString("int")))) + .bind("output")); +} + +TEST(HeuristicResolver, UsingValueDecl) { + std::string Code = R"cpp( + template + struct Base { + void waldo(); + }; + template + struct Derived : Base { + using Base::waldo; + }; + )cpp"; + // Test resolution of "waldo" in "Base::waldo". + expectResolution(Code, &HeuristicResolver::resolveUsingValueDecl, + unresolvedUsingValueDecl().bind("input"), + cxxMethodDecl(hasName("waldo")).bind("output")); +} + +} // namespace +} // namespace clang