-
Notifications
You must be signed in to change notification settings - Fork 12.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[clangd] Add a unit test suite for HeuristicResolver #121246
Closed
Closed
+525
−2
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@llvm/pr-subscribers-clangd @llvm/pr-subscribers-clang-tools-extra Author: Nathan Ridge (HighCommander4) ChangesFixes clangd/clangd#2154 Full diff: https://github.com/llvm/llvm-project/pull/121246.diff 3 Files Affected:
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..1e6cd3e266e600
--- /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 <typename InputNode>
+using ResolveFnT = std::function<std::vector<const NamedDecl *>(
+ 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<const NamedDecl *>`.
+// `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 <typename InputNode, typename InputMatcher, typename... OutputMatchers>
+void expectResolution(llvm::StringRef Code, ResolveFnT<InputNode> 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<InputNode>("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<NamedDecl>("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 <typename InputNode, typename InputMatcher, typename... OutputMatchers>
+void expectResolution(llvm::StringRef Code,
+ std::vector<const NamedDecl *> (
+ HeuristicResolver::*ResolveFn)(const InputNode *)
+ const,
+ const InputMatcher &IM, const OutputMatchers &...OMS) {
+ expectResolution(Code, ResolveFnT<InputNode>(std::mem_fn(ResolveFn)), IM,
+ OMS...);
+}
+
+TEST(HeuristicResolver, MemberExpr) {
+ std::string Code = R"cpp(
+ template <typename T>
+ struct S {
+ void bar() {}
+ };
+
+ template <typename T>
+ void foo(S<T> 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 <typename T>
+ struct S {
+ void bar(int);
+ void bar(float);
+ };
+
+ template <typename T, typename U>
+ void foo(S<T> 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 <typename> struct S { void foo() {} };
+ template <typename T> struct unique_ptr {
+ T* operator->();
+ };
+ template <typename T>
+ void test(unique_ptr<S<T>>& 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 <typename T>
+ 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 <typename T> T convert();
+ };
+ template <typename T>
+ void test() {
+ Foo::k(T()).template convert<T>();
+ }
+ )cpp";
+ // Test resolution of "convert" in "Foo::k(T()).template convert<T>()".
+ expectResolution(Code, &HeuristicResolver::resolveMemberExpr,
+ cxxDependentScopeMemberExpr().bind("input"),
+ functionTemplateDecl(hasName("convert")).bind("output"));
+}
+
+TEST(HeuristicResolver, MemberExpr_TypeAlias) {
+ std::string Code = R"cpp(
+ template <typename T>
+ struct Waldo {
+ void find();
+ };
+ template <typename T>
+ using Wally = Waldo<T>;
+ template <typename T>
+ void foo(Wally<T> 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 <typename T>
+ struct Waldo {
+ void find();
+ };
+ template <typename T>
+ using Wally = Waldo<T>;
+ template <typename T>
+ struct S : Wally<T> {
+ 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 <typename T>
+ struct Waldo {
+ void find();
+ };
+ template <typename T>
+ struct MetaWaldo {
+ using Type = Waldo<T>;
+ };
+ template <typename T>
+ void foo(typename MetaWaldo<T>::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 <int N>
+ struct Waldo {
+ const int found = N;
+ };
+ template <Waldo W>
+ 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 <typename T>
+ struct S {
+ static void bar() {}
+ };
+
+ template <typename T>
+ void foo() {
+ S<T>::bar();
+ }
+ )cpp";
+ // Test resolution of "bar" in "S<T>::bar()".
+ expectResolution(Code, &HeuristicResolver::resolveDeclRefExpr,
+ dependentScopeDeclRefExpr().bind("input"),
+ cxxMethodDecl(hasName("bar")).bind("output"));
+}
+
+TEST(HeuristicResolver, DeclRefExpr_StaticOverloads) {
+ std::string Code = R"cpp(
+ template <typename T>
+ struct S {
+ static void bar(int);
+ static void bar(float);
+ };
+
+ template <typename T, typename U>
+ void foo(U u) {
+ S<T>::bar(u);
+ }
+ )cpp";
+ // Test resolution of "bar" in "S<T>::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 <typename T>
+ 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 <typename Info>
+ 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 <typename>
+ struct A {
+ struct B {};
+ };
+ template <typename T>
+ void foo(typename A<T>::B);
+ )cpp";
+ // Tests resolution of "B" in "A<T>::B".
+ expectResolution(Code, &HeuristicResolver::resolveDependentNameType,
+ functionDecl(hasParameter(0, hasType(type().bind("input")))),
+ classTemplateDecl(has(cxxRecordDecl(
+ has(cxxRecordDecl(hasName("B")).bind("output"))))));
+}
+
+TEST(HeuristicResolver, DependentNameType_Nested1) {
+ std::string Code = R"cpp(
+ template <typename>
+ struct A {
+ struct B {
+ struct C {};
+ };
+ };
+ template <typename T>
+ void foo(typename A<T>::B::C);
+ )cpp";
+ // Tests resolution of "C" in "A<T>::B::C".
+ expectResolution(Code, &HeuristicResolver::resolveDependentNameType,
+ functionDecl(hasParameter(0, hasType(type().bind("input")))),
+ classTemplateDecl(has(cxxRecordDecl(has(cxxRecordDecl(
+ has(cxxRecordDecl(hasName("C")).bind("output"))))))));
+}
+
+TEST(HeuristicResolver, DependentNameType_Recursion) {
+ std::string Code = R"cpp(
+ template <int N>
+ struct Waldo {
+ using Type = typename Waldo<N - 1>::Type::Next;
+ };
+ )cpp";
+ // Test resolution of "Next" in "typename Waldo<N - 1>::Type::Next".
+ // Here, we are testing that we do not get into an infinite recursion.
+ expectResolution(Code, &HeuristicResolver::resolveDependentNameType,
+ typeAliasDecl(hasType(type().bind("input"))));
+}
+
+TEST(HeuristicResolver, DependentNameType_MutualRecursion) {
+ std::string Code = R"cpp(
+ template <int N>
+ struct Odd;
+ template <int N>
+ struct Even {
+ using Type = typename Odd<N - 1>::Type::Next;
+ };
+ template <int N>
+ struct Odd {
+ using Type = typename Even<N - 1>::Type::Next;
+ };
+ )cpp";
+ // Test resolution of "Next" in "typename Even<N - 1>::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<T>::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 <typename>
+ struct A {
+ struct B {
+ struct C {};
+ };
+ };
+ template <typename T>
+ void foo(typename A<T>::B::C);
+ )cpp";
+ // Adapt the call to resolveNestedNameSpecifierToType() to the interface
+ // expected by expectResolution() (returning a vector of decls).
+ ResolveFnT<NestedNameSpecifier> ResolveFn =
+ [](const HeuristicResolver *H,
+ const NestedNameSpecifier *NNS) -> std::vector<const NamedDecl *> {
+ 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 <typename>
+ struct A {
+ template <typename>
+ struct B {};
+ };
+ template <typename T>
+ void foo(typename A<T>::template B<int>);
+ )cpp";
+ // Test resolution of "B" in "A<T>::template B<int>".
+ 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 <typename T>
+ void nonmember(T);
+ template <typename T>
+ 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 <typename T>
+ struct A {
+ void member(T);
+ };
+ template <typename T>
+ void bar(A<T> 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 <typename T>
+ struct A {
+ static void static_member(T);
+ };
+ template <typename T>
+ void bar(T t) {
+ A<T>::static_member(t);
+ }
+ )cpp";
+ // Test resolution of "static_member" in "A<T>::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 <typename T>
+ 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 <typename T>
+ struct Base {
+ void waldo();
+ };
+ template <typename T>
+ struct Derived : Base<T> {
+ using Base<T>::waldo;
+ };
+ )cpp";
+ // Test resolution of "waldo" in "Base<T>::waldo".
+ expectResolution(Code, &HeuristicResolver::resolveUsingValueDecl,
+ unresolvedUsingValueDecl().bind("input"),
+ cxxMethodDecl(hasName("waldo")).bind("output"));
+}
+
+} // namespace
+} // namespace clang
|
HighCommander4
force-pushed
the
issue-2154
branch
from
December 30, 2024 00:31
3834e0c
to
8238ad1
Compare
Updated to take advantage of the new |
Resubmitted as #121313 in a user branch of this repo, so that I can stack dependent PRs on top of this. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes clangd/clangd#2154