-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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 #121313
[clangd] Add a unit test suite for HeuristicResolver #121313
Conversation
@llvm/pr-subscribers-clang-tools-extra @llvm/pr-subscribers-clangd Author: Nathan Ridge (HighCommander4) ChangesFixes clangd/clangd#2154 Full diff: https://github.com/llvm/llvm-project/pull/121313.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..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 <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,
+ dependentNameType().bind("input"),
+ classTemplateDecl(has(cxxRecordDecl(
+ has(cxxRecordDecl(hasName("B")).bind("output"))))));
+}
+
+TEST(HeuristicResolver, DependentNameType_Nested) {
+ 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,
+ dependentNameType().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,
+ dependentNameType().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
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, feel free to merge unless you prefer additional input from other maintainers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks good to me in general.
)cpp"; | ||
// Test resolution of "bar" in "arg.bar()". | ||
expectResolution(Code, &HeuristicResolver::resolveMemberExpr, | ||
cxxDependentScopeMemberExpr().bind("input"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make the input matcher more specific (applies to the following cases as well)? For example, something like cxxDependentScopeMemberExpr(hasName("bar"))
. This would enhance the code readability, as currently, I have to rely on the comments to understand the intention of the matcher.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fairly inexperienced with AST matchers and so may be overlooking something, but it looks like hasName()
only works on matchers for NamedDecl
nodes.
We could conceivably extend hasName()
to support CXXDependentScopeMemberExpr
as well, but that seems like an enhancement beyond the scope of this patch.
In an earlier local draft of this patch, I used matchers that got at the dependent node in a more indirect way (e.g. "the callee of a call expression that ..."). Partway through writing the tests this way, I discovered that the dependent nodes have their own matchers, and I switched to using them as I figured it's neat that e.g. in most testcases exercising HeuristicResolver::resolveMemberExpr()
, the input code has a single CXXDependentScopeMemberExpr
node and I can select it simply with cxxDependentScopeMemberExpr()
.
I suppose I could go back to the earlier indirect approach of matching, if you'd prefer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never mind, I was overlooking something: there is a hasMemberName()
matcher that works with CXXDependentScopeMemberExpr
. I'll look at using that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I've updated the patch to make use of hasMemberName()
for CXXDependentScopeMemberExpr
, and also added more detail to some other matchers.
Two exceptions are dependentScopeDeclRefExpr()
and dependentNameType()
-- these matchers were both added recently, and do not yet have any matchers for their properties similar to hasMemberName()
. I can explore adding some (though I'd prefer to do that as a follow-up enhancement), or switch to the indirect matching approach for these, or I'm open to other ideas.
9bb4393
to
052d67a
Compare
052d67a
to
dfde3a1
Compare
(Rebased) |
dfde3a1
to
63d9512
Compare
In the latest update, I use the new |
63d9512
to
d5403c8
Compare
In the latest update, matchers for the @hokein I believe your comment about using more specific matchers should be fully addressed now :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this looks nice.
d5403c8
to
c342743
Compare
Fixes clangd/clangd#2154